// // ScanCoordFlowCoordinator.swift // Zashi // // Created by Lukáš Korba on 2025-03-19. // import Foundation import ComposableArchitecture import ZcashLightClientKit import ZcashPaymentURI import Generated import AudioServices // Path import AddressBook import PartialProposalError import Scan import SendConfirmation import SendForm import TransactionDetails extension ScanCoordFlow { public func coordinatorReduce() -> Reduce { Reduce { state, action in switch action { // MARK: - Address Book case .path(.element(id: _, action: .addressBook(.editId(let address)))): let _ = state.path.removeLast() audioServices.systemSoundVibrate() if let first = state.path.ids.first { return .send(.path(.element(id: first, action: .sendForm(.addressUpdated(address.redacted))))) } return .none case .path(.element(id: _, action: .addressBook(.walletAccountTapped(let contact)))): if let address = contact.unifiedAddress { let _ = state.path.removeLast() audioServices.systemSoundVibrate() if let first = state.path.ids.first { return .send(.path(.element(id: first, action: .sendForm(.addressUpdated(address.redacted))))) } } return .none case .path(.element(id: _, action: .addressBook(.addManualButtonTapped))): var addressBookState = AddressBook.State.initial addressBookState.isAddressFocused = true state.path.append(.addressBookContact(addressBookState)) return .none case .path(.element(id: _, action: .addressBook(.scanButtonTapped))): var scanState = Scan.State.initial scanState.checkers = [.zcashAddressScanChecker] state.path.append(.scan(scanState)) return .none // MARK: - Address Book Contact case .path(.element(id: _, action: .addressBookContact(.dismissAddContactRequired))): let _ = state.path.popLast() // handling the path in the transaction details for element in state.path { if element.is(\.transactionDetails) { return .none } } // handling the path in send confirmation for element in state.path { if element.is(\.sendConfirmation) { return .none } } // handling the path in send form for element in state.path { if element.is(\.scan) { let _ = state.path.popLast() return .none } } return .none // MARK: - Keystone case .path(.element(id: _, action: .sendConfirmation(.confirmWithKeystoneTapped))): for element in state.path { if case .sendConfirmation(let sendConfirmationState) = element { state.path.append(.confirmWithKeystone(sendConfirmationState)) if let last = state.path.ids.last { return .send(.path(.element(id: last, action: .confirmWithKeystone(.resolvePCZT)))) } } } return .none case .path(.element(id: _, action: .requestZecConfirmation(.confirmWithKeystoneTapped))): for element in state.path { if case .requestZecConfirmation(let sendConfirmationState) = element { state.path.append(.confirmWithKeystone(sendConfirmationState)) if let last = state.path.ids.last { return .send(.path(.element(id: last, action: .confirmWithKeystone(.resolvePCZT)))) } } } return .none case .path(.element(id: _, action: .confirmWithKeystone(.getSignatureTapped))): var scanState = Scan.State.initial scanState.checkers = [.keystonePCZTScanChecker] state.path.append(.scan(scanState)) return .none case .path(.element(id: _, action: .scan(.foundPCZT(let pcztWithSigs)))): for (id, element) in zip(state.path.ids, state.path) { if case .confirmWithKeystone(let sendConfirmationState) = element { state.path.append(.sending(sendConfirmationState)) return .send(.path(.element(id: id, action: .confirmWithKeystone(.foundPCZT(pcztWithSigs))))) } } return .none case .path(.element(id: _, action: .confirmWithKeystone(.updateResult(let result)))): for element in state.path { if case .confirmWithKeystone(let sendConfirmationState) = element { return .send(.resolveSendResult(result, sendConfirmationState)) } } return .none case .path(.element(id: _, action: .confirmWithKeystone(.pcztSendFailed(let error)))): for element in state.path.reversed() { if element.is(\.sending) { for (id, element2) in zip(state.path.ids, state.path) { if element2.is(\.confirmWithKeystone) { return .send(.path(.element(id: id, action: .confirmWithKeystone(.sendFailed(error?.toZcashError(), true))))) } } break } else if element.is(\.scan) || element.is(\.confirmWithKeystone) { for element2 in state.path { if case .confirmWithKeystone(let sendConfirmationState) = element2 { state.path.append(.preSendingFailure(sendConfirmationState)) } } break } } return .none // MARK: - Request ZEC Confirmation case .path(.element(id: _, action: .requestZecConfirmation(.goBackTappedFromRequestZec))): for (id, element) in zip(state.path.ids, state.path) { if element.is(\.sendForm) { state.path.pop(to: id) } } return .none case .path(.element(id: _, action: .requestZecConfirmation(.sendRequested))): for element in state.path { if case .requestZecConfirmation(let sendConfirmationState) = element { state.path.append(.sending(sendConfirmationState)) break } } return .none case .path(.element(id: _, action: .requestZecConfirmation(.saveAddressTapped(let address)))): var addressBookState = AddressBook.State.initial addressBookState.isNameFocused = true addressBookState.address = address.data addressBookState.isValidZcashAddress = true state.path.append(.addressBookContact(addressBookState)) return .none case .path(.element(id: _, action: .requestZecConfirmation(.updateResult(let result)))): for element in state.path { if case .requestZecConfirmation(let sendConfirmationState) = element { return .send(.resolveSendResult(result, sendConfirmationState)) } } return .none // MARK: - Scan case .path(.element(id: _, action: .scan(.foundAddress(let address)))): for (id, element) in zip(state.path.ids, state.path) { if element.is(\.sendForm) { let _ = state.path.removeLast() audioServices.systemSoundVibrate() return .send(.path(.element(id: id, action: .sendForm(.addressUpdated(address))))) } } return .none case .path(.element(id: _, action: .scan(.cancelTapped))): let _ = state.path.popLast() return .none case .path(.element(id: _, action: .scan(.foundRequestZec(let requestPayment)))): if case .legacy(let address) = requestPayment { for (id, element) in zip(state.path.ids, state.path) { if element.is(\.sendForm) { let _ = state.path.removeLast() audioServices.systemSoundVibrate() state.path[id: id, case: \.sendForm]?.memoState.text = "" return .merge( .send(.path(.element(id: id, action: .sendForm(.zecAmountUpdated("".redacted))))), .send(.path(.element(id: id, action: .sendForm(.addressUpdated(address.value.redacted))))) ) } } } else if case .request(let paymentRequest) = requestPayment { return .send(.getProposal(paymentRequest)) } return .none // MARK: - Self case .scan(.foundAddress(let address)): audioServices.systemSoundVibrate() state.path.append(.sendForm(SendForm.State.initial)) if let first = state.path.ids.first { return .send(.path(.element(id: first, action: .sendForm(.addressUpdated(address))))) } return .none case .scan(.foundRequestZec(let requestPayment)): if case .legacy(let address) = requestPayment { return .send(.scan(.foundAddress(address.value.redacted))) } else if case .request(let paymentRequest) = requestPayment { return .send(.getProposal(paymentRequest)) } return .none case .getProposal(let paymentRequest): guard let account = state.selectedWalletAccount else { return .none } do { if let payment = paymentRequest.payments.first { var textMemo = "" if let memoBytes = payment.memo, let memo = try? Memo(bytes: [UInt8](memoBytes.memoData)) { textMemo = memo.toString() ?? "" } let numberLocale = numberFormatter.convertUSToLocale(payment.amount.toString()) ?? "" state.recipient = try Recipient(payment.recipientAddress.value, network: zcashSDKEnvironment.network.networkType) state.memo = textMemo.isEmpty ? nil : try Memo(string: textMemo) if let number = numberFormatter.number(numberLocale) { state.amount = Zatoshi(NSDecimalNumber( decimal: number.decimalValue * Decimal(Zatoshi.Constants.oneZecInZatoshi) ).roundedZec.int64Value) } } } catch { return .send(.requestZecFailed) } return .run { [state] send in guard let recipient = state.recipient else { return } do { let proposal = try await sdkSynchronizer.proposeTransfer(account.id, recipient, state.amount, state.memo) await send(.proposalResolved(proposal)) } catch { await send(.requestZecFailed) } } case .proposalResolved(let proposal): if state.path.ids.isEmpty { return .send(.proposalResolvedNoSendForm(proposal)) } return .send(.proposalResolvedExistingSendForm(proposal)) case .proposalResolvedExistingSendForm(let proposal): state.proposal = proposal guard let address = state.recipient?.stringEncoded else { return .send(.requestZecFailed) } var sendConfirmationState = SendConfirmation.State.initial sendConfirmationState.amount = state.amount sendConfirmationState.address = address sendConfirmationState.proposal = proposal sendConfirmationState.feeRequired = proposal.totalFeeRequired() sendConfirmationState.message = state.memo?.toString() ?? "" sendConfirmationState.currencyAmount = state.currencyConversion?.convert(state.amount).redacted ?? .empty state.path.append(.requestZecConfirmation(sendConfirmationState)) audioServices.systemSoundVibrate() if let first = state.path.ids.first { state.path[id: first, case: \.sendForm]?.memoState.text = sendConfirmationState.message return .merge( .send(.path(.element(id: first, action: .sendForm(.zecAmountUpdated(state.amount.decimalString().redacted))))), .send(.path(.element(id: first, action: .sendForm(.addressUpdated(address.redacted))))) ) } return .none case .proposalResolvedNoSendForm(let proposal): state.proposal = proposal guard let address = state.recipient?.stringEncoded else { return .send(.requestZecFailed) } var sendFormState = SendForm.State.initial sendFormState.memoState.text = state.memo?.toString() ?? "" state.path.append(.sendForm(sendFormState)) var sendConfirmationState = SendConfirmation.State.initial sendConfirmationState.amount = state.amount sendConfirmationState.address = address sendConfirmationState.proposal = proposal sendConfirmationState.feeRequired = proposal.totalFeeRequired() sendConfirmationState.message = sendFormState.memoState.text sendConfirmationState.currencyAmount = state.currencyConversion?.convert(state.amount).redacted ?? .empty state.path.append(.requestZecConfirmation(sendConfirmationState)) audioServices.systemSoundVibrate() if let first = state.path.ids.first { return .merge( .send(.path(.element(id: first, action: .sendForm(.zecAmountUpdated(state.amount.decimalString().redacted))))), .send(.path(.element(id: first, action: .sendForm(.addressUpdated(address.redacted))))) ) } return .none case .requestZecFailed: if state.path.ids.isEmpty { return .send(.requestZecFailedNoSendForm) } return .send(.requestZecFailedExistingSendForm) case .requestZecFailedExistingSendForm: let _ = state.path.removeLast() for (id, element) in zip(state.path.ids, state.path) { if element.is(\.sendForm) { audioServices.systemSoundVibrate() let address = state.recipient?.stringEncoded ?? "" let memo = state.memo?.toString() ?? "" state.path[id: id, case: \.sendForm]?.memoState.text = memo return .merge( .send(.path(.element(id: id, action: .sendForm(.zecAmountUpdated(state.amount.decimalString().redacted))))), .send(.path(.element(id: id, action: .sendForm(.addressUpdated(address.redacted))))) ) } } return .none case .requestZecFailedNoSendForm: let address = state.recipient?.stringEncoded ?? "" var sendFormState = SendForm.State.initial sendFormState.memoState.text = state.memo?.toString() ?? "" state.path.append(.sendForm(sendFormState)) audioServices.systemSoundVibrate() if let first = state.path.ids.first { return .merge( .send(.path(.element(id: first, action: .sendForm(.zecAmountUpdated(state.amount.decimalString().redacted))))), .send(.path(.element(id: first, action: .sendForm(.addressUpdated(address.redacted))))) ) } return .none case let .resolveSendResult(result, sendConfirmationState): switch result { case .failure: state.path.append(.sendResultFailure(sendConfirmationState)) break case .partial: var partialProposalErrorState = PartialProposalError.State.initial partialProposalErrorState.statuses = sendConfirmationState.partialFailureStatuses partialProposalErrorState.txIds = sendConfirmationState.partialFailureTxIds state.path.append(.sendResultPartial(partialProposalErrorState)) break case .resubmission: state.path.append(.sendResultResubmission(sendConfirmationState)) break case .success: state.path.append(.sendResultSuccess(sendConfirmationState)) default: break } return .none case .viewTransactionRequested(let sendConfirmationState): if let txid = sendConfirmationState.txIdToExpand { if let index = state.transactions.index(id: txid) { var transactionDetailsState = TransactionDetails.State.initial transactionDetailsState.transaction = state.transactions[index] transactionDetailsState.isCloseButtonRequired = true state.path.append(.transactionDetails(transactionDetailsState)) } } return .none // MARK: - Send case .path(.element(id: _, action: .sendForm(.addressBookTapped))): var addressBookState = AddressBook.State.initial addressBookState.isInSelectMode = true state.path.append(.addressBook(addressBookState)) return .none case .path(.element(id: _, action: .sendForm(.addNewContactTapped(let address)))): var addressBookState = AddressBook.State.initial addressBookState.isNameFocused = true addressBookState.address = address.data addressBookState.isValidZcashAddress = true state.path.append(.addressBookContact(addressBookState)) return .none case .path(.element(id: _, action: .sendForm(.scanTapped))): var scanState = Scan.State.initial scanState.checkers = [.zcashAddressScanChecker, .requestZecScanChecker] state.path.append(.scan(scanState)) return .none case .path(.element(id: _, action: .sendForm(.confirmationRequired(let confirmationType)))): for element in state.path { if case .sendForm(let sendFormState) = element { var sendConfirmationState = SendConfirmation.State.initial sendConfirmationState.amount = sendFormState.amount sendConfirmationState.address = sendFormState.address.data sendConfirmationState.proposal = sendFormState.proposal sendConfirmationState.feeRequired = sendFormState.feeRequired sendConfirmationState.message = sendFormState.message let currencyAmount = sendFormState.currencyConversion?.convert(sendFormState.amount).redacted ?? .empty sendConfirmationState.currencyAmount = currencyAmount if confirmationType == .send { state.path.append(.sendConfirmation(sendConfirmationState)) } } } return .none // MARK: - Send Confirmation case .path(.element(id: _, action: .sendConfirmation(.cancelTapped))): let _ = state.path.removeLast() return .none case .path(.element(id: _, action: .sendConfirmation(.sendRequested))): for element in state.path { if case .sendConfirmation(let sendConfirmationState) = element { state.path.append(.sending(sendConfirmationState)) break } } return .none case .path(.element(id: _, action: .sendConfirmation(.updateResult(let result)))): for element in state.path { if case .sendConfirmation(let sendConfirmationState) = element { return .send(.resolveSendResult(result, sendConfirmationState)) } } return .none case .path(.element(id: _, action: .sendResultFailure(.backFromFailureTapped))), .path(.element(id: _, action: .preSendingFailure(.backFromPCZTFailureTapped))): for (id, element) in zip(state.path.ids, state.path) { if element.is(\.sendForm) { state.path.pop(to: id) } } return .none case .path(.element(id: _, action: .sendResultSuccess(.viewTransactionTapped))), .path(.element(id: _, action: .sendResultFailure(.viewTransactionTapped))), .path(.element(id: _, action: .sendResultResubmission(.viewTransactionTapped))): for element in state.path.reversed() { if case .sendConfirmation(let sendConfirmationState) = element { return .send(.viewTransactionRequested(sendConfirmationState)) } else if case .requestZecConfirmation(let sendConfirmationState) = element { return .send(.viewTransactionRequested(sendConfirmationState)) } else if case .confirmWithKeystone(let sendConfirmationState) = element { return .send(.viewTransactionRequested(sendConfirmationState)) } } return .none // MARK: - Transaction Details case .path(.element(id: _, action: .transactionDetails(.saveAddressTapped))): for element in state.path { if case .transactionDetails(let transactionDetailsState) = element { var addressBookState = AddressBook.State.initial addressBookState.address = transactionDetailsState.transaction.address addressBookState.isNameFocused = true addressBookState.isValidZcashAddress = true state.path.append(.addressBookContact(addressBookState)) } } return .none case .path(.element(id: _, action: .transactionDetails(.sendAgainTapped))): for (id, element) in zip(state.path.ids, state.path) { if element.is(\.sendForm) { state.path.pop(to: id) } } return .none // MARK: - // Handling of scan inside address book // for element in state.path { // if element.is(\.addressBook) { // var addressBookState = AddressBook.State.initial // addressBookState.address = address.data // addressBookState.isValidZcashAddress = true // addressBookState.isNameFocused = true // state.path.append(.addressBookContact(addressBookState)) // audioServices.systemSoundVibrate() // return .none // } // } // // handling of scan for the send form // let _ = state.path.popLast() // audioServices.systemSoundVibrate() // return .send(.sendForm(.addressUpdated(address))) // case .scan(.foundRequestZec(let requestPayment)): // return .send(.sendForm(.requestZec(requestPayment))) /* // MARK: Address Book case .path(.element(id: _, action: .addressBook(.editId(let address)))): state.path.removeAll() audioServices.systemSoundVibrate() return .send(.sendForm(.addressUpdated(address.redacted))) case .path(.element(id: _, action: .addressBook(.addManualButtonTapped))): var addressBookState = AddressBook.State.initial addressBookState.isAddressFocused = true state.path.append(.addressBookContact(addressBookState)) return .none case .path(.element(id: _, action: .addressBook(.scanButtonTapped))): var scanState = Scan.State.initial scanState.checkers = [.zcashAddressScanChecker] state.path.append(.scan(scanState)) return .none // MARK: Address Book Contact case .path(.element(id: _, action: .addressBookContact(.dismissAddContactRequired))): let _ = state.path.popLast() for element in state.path { if element.is(\.scan) { let _ = state.path.popLast() return .none } } return .none // MARK: Request ZEC Confirmation case .path(.element(id: _, action: .requestZecConfirmation(.goBackTappedFromRequestZec))): state.path.removeAll() return .none case .path(.element(id: _, action: .requestZecConfirmation(.sendTapped))): for element in state.path { if case .requestZecConfirmation(let sendConfirmationState) = element { state.path.append(.sending(sendConfirmationState)) break } } return .none case .path(.element(id: _, action: .requestZecConfirmation(.sendFailed))): state.path.removeAll() return .none // MARK: Send case .sendForm(.addressBookTapped): var addressBookState = AddressBook.State.initial addressBookState.isInSelectMode = true state.path.append(.addressBook(addressBookState)) return .none case .sendForm(.scanTapped): var scanState = Scan.State.initial scanState.checkers = [.zcashAddressScanChecker, .requestZecScanChecker] state.path.append(.scan(scanState)) return .none case .sendForm(.confirmationRequired(let confirmationType)): var sendConfirmationState = SendConfirmation.State.initial sendConfirmationState.amount = state.sendFormState.amount sendConfirmationState.address = state.sendFormState.address.data sendConfirmationState.proposal = state.sendFormState.proposal sendConfirmationState.feeRequired = state.sendFormState.feeRequired sendConfirmationState.message = state.sendFormState.message let currencyAmount = state.sendFormState.currencyConversion?.convert(state.sendFormState.amount).redacted ?? .empty sendConfirmationState.currencyAmount = currencyAmount if confirmationType == .send { state.path.append(.sendConfirmation(sendConfirmationState)) } else if confirmationType == .requestPayment { state.path.append(.requestZecConfirmation(sendConfirmationState)) } return .none case let .sendForm(.sendFailed(_, confirmationType)): if confirmationType == .requestPayment { state.path.removeAll() } return .none // MARK: Send Confirmation case .path(.element(id: _, action: .sendConfirmation(.cancelTapped))): let _ = state.path.popLast() return .none case .path(.element(id: _, action: .sendConfirmation(.sendTapped))): for element in state.path { if case .sendConfirmation(let sendConfirmationState) = element { state.path.append(.sending(sendConfirmationState)) break } } return .none case .path(.element(id: _, action: .sendConfirmation(.updateResult(let result)))), .path(.element(id: _, action: .requestZecConfirmation(.updateResult(let result)))): for element in state.path { if case .sending(let sendConfirmationState) = element { switch result { case .failure: state.path.append(.sendResultFailure(sendConfirmationState)) break case .partial: var partialProposalErrorState = PartialProposalError.State.initial partialProposalErrorState.statuses = sendConfirmationState.partialFailureStatuses partialProposalErrorState.txIds = sendConfirmationState.partialFailureTxIds state.path.append(.sendResultPartial(partialProposalErrorState)) break case .resubmission: state.path.append(.sendResultResubmission(sendConfirmationState)) break case .success: state.path.append(.sendResultSuccess(sendConfirmationState)) default: break } } } return .none case .path(.element(id: _, action: .sendResultFailure(.backFromFailureTapped))): state.path.removeAll() return .none case .path(.element(id: _, action: .preSendingFailure(.backFromPCZTFailureTapped))): state.path.removeAll() return .none case .path(.element(id: _, action: .sendResultSuccess(.viewTransactionTapped))), .path(.element(id: _, action: .sendResultFailure(.viewTransactionTapped))), .path(.element(id: _, action: .sendResultResubmission(.viewTransactionTapped))): var transactionDetailsState = TransactionDetails.State.initial for element in state.path { if case .sendConfirmation(let sendConfirmationState) = element { if let txid = sendConfirmationState.txIdToExpand { if let index = state.transactions.index(id: txid) { transactionDetailsState.transaction = state.transactions[index] transactionDetailsState.isCloseButtonRequired = true state.path.append(.transactionDetails(transactionDetailsState)) break } } } } return .none // MARK: Self case .dismissRequired: return .none // MARK: Transaction Details case .path(.element(id: _, action: .transactionDetails(.saveAddressTapped))): var addressBookState = AddressBook.State.initial addressBookState.address = state.sendFormState.address.data addressBookState.isNameFocused = true addressBookState.isValidZcashAddress = true state.path.append(.addressBookContact(addressBookState)) return .none case .path(.element(id: _, action: .transactionDetails(.sendAgainTapped))): state.path.removeAll() return .none */ default: return .none } } } }