// // RootCoordinator.swift // modules // // Created by Lukáš Korba on 07.03.2025. // import ComposableArchitecture import ZcashLightClientKit extension Root { public func coordinatorReduce() -> Reduce { Reduce { state, action in switch action { // MARK: - Accounts case .home(.walletAccountTapped(let walletAccount)): guard state.selectedWalletAccount != walletAccount else { return .none } state.$selectedWalletAccount.withLock { $0 = walletAccount } state.homeState.transactionListState.isInvalidated = true return .merge( .send(.home(.smartBanner(.walletAccountChanged))), .send(.home(.walletBalances(.updateBalances))), .send(.loadContacts), .send(.resolveMetadataEncryptionKeys), .send(.loadUserMetadata), .send(.fetchTransactionsForTheSelectedAccount) ) // MARK: - Add Keystone HW Wallet Coord Flow case .addKeystoneHWWalletCoordFlow(.path(.element(id: _, action: .accountHWWalletSelection(.forgetThisDeviceTapped)))): state.path = nil return .none case .addKeystoneHWWalletCoordFlow(.path(.element(id: _, action: .accountHWWalletSelection(.accountImportSucceeded)))): state.path = nil return .merge( .send(.loadContacts), .send(.resolveMetadataEncryptionKeys), .send(.loadUserMetadata), .send(.fetchTransactionsForTheSelectedAccount) ) // MARK: - Add Keystone HW Wallet from Settings case .settings(.path(.element(id: _, action: .accountHWWalletSelection(.accountImportSucceeded)))): state.path = nil return .merge( .send(.loadContacts), .send(.resolveMetadataEncryptionKeys), .send(.loadUserMetadata), .send(.fetchTransactionsForTheSelectedAccount) ) // MARK: - Flexa case .flexaOpenRequest: flexaHandler.open() return .publisher { flexaHandler.onTransactionRequest() .map(Root.Action.flexaOnTransactionRequest) .receive(on: mainQueue) } .cancellable(id: CancelFlexaId, cancelInFlight: true) // MARK: - Currency Conversion Setup case .currencyConversionSetup(.skipTapped), .currencyConversionSetup(.enableTapped): state.path = nil state.homeState.isRateEducationEnabled = false return .send(.home(.smartBanner(.closeAndCleanupBanner))) // MARK: - Home case .home(.settingsTapped): state.settingsState = .initial state.path = .settings return .none case .home(.receiveTapped): state.receiveState = .initial state.path = .receive return .none case .home(.sendTapped): state.sendCoordFlowState = .initial state.path = .sendCoordFlow exchangeRate.refreshExchangeRateUSD() return .none case .home(.scanTapped): state.scanCoordFlowState = .initial state.path = .scanCoordFlow return .none case .home(.getSomeZecTapped): state.requestZecCoordFlowState = .initial state.path = .requestZecCoordFlow return .none case .home(.flexaTapped): return .send(.flexaOpenRequest) case .home(.addKeystoneHWWalletTapped): state.addKeystoneHWWalletCoordFlowState = .initial state.path = .addKeystoneHWWalletCoordFlow return .none case .home(.transactionList(.transactionTapped(let txId))): state.transactionsCoordFlowState = .initial state.transactionsCoordFlowState.transactionToOpen = txId if let index = state.transactions.index(id: txId) { state.transactionsCoordFlowState.transactionDetailsState.transaction = state.transactions[index] } state.path = .transactionsCoordFlow return .none case .home(.seeAllTransactionsTapped): state.transactionsCoordFlowState = .initial state.path = .transactionsCoordFlow return .none case .home(.currencyConversionSetupTapped): state.currencyConversionSetupState = .initial state.path = .currencyConversionSetup return .none case .home(.smartBanner(.walletBackupTapped)): state.walletBackupCoordFlowState = .initial state.path = .walletBackup return .none // MARK: - Integrations case .settings(.path(.element(id: _, action: .integrations(.flexaTapped)))): return .send(.flexaOpenRequest) // MARK: - Keystone case .sendCoordFlow(.path(.element(id: _, action: .confirmWithKeystone(.rejectTapped)))), .signWithKeystoneCoordFlow(.sendConfirmation(.rejectTapped)): state.path = nil return .none case .signWithKeystoneRequested: //state.path = .signWithKeystoneCoordFlow state.signWithKeystoneCoordFlowBinding = true return .send(.signWithKeystoneCoordFlow(.sendConfirmation(.resolvePCZT))) // MARK: - Request Zec case .requestZecCoordFlow(.path(.element(id: _, action: .requestZecSummary(.cancelRequestTapped)))): state.path = nil return .none // MARK: - Reset Zashi case .settings(.path(.element(id: _, action: .resetZashi(.deleteTapped)))): return .send(.initialization(.resetZashiRequest)) // MARK: - Restore Wallet Coord Flow from Onboarding case .onboarding(.restoreWalletCoordFlow(.path(.element(id: _, action: .restoreInfo(.gotItTapped))))): var leavesScreenOpen = false for element in state.onboardingState.restoreWalletCoordFlowState.path { if case .restoreInfo(let restoreInfoState) = element { leavesScreenOpen = restoreInfoState.isAcknowledged } } userDefaults.setValue(leavesScreenOpen, Constants.udLeavesScreenOpen) state.isRestoringWallet = true userDefaults.setValue(true, Constants.udIsRestoringWallet) state.$walletStatus.withLock { $0 = .restoring } return .concatenate( .send(.initialization(.initializeSDK(.restoreWallet))), .send(.initialization(.checkBackupPhraseValidation)), .send(.batteryStateChanged(nil)) ) // MARK: - Scan Coord Flow case .scanCoordFlow(.scan(.cancelTapped)): state.path = nil return .none case .scanCoordFlow(.path(.element(id: _, action: .sendForm(.dismissRequired)))): state.path = nil return .none case .scanCoordFlow(.path(.element(id: _, action: .transactionDetails(.closeDetailTapped)))): state.path = nil return .none case .scanCoordFlow(.path(.element(id: _, action: .sendResultSuccess(.closeTapped)))), .scanCoordFlow(.path(.element(id: _, action: .sendResultResubmission(.closeTapped)))), .scanCoordFlow(.path(.element(id: _, action: .sendResultPartial(.dismiss)))): state.path = nil return .none // MARK: - Self case .sendAgainRequested(let transactionState): state.sendCoordFlowState = .initial state.path = .sendCoordFlow state.sendCoordFlowState.sendFormState.memoState.text = state.transactionMemos[transactionState.id]?.first ?? "" return .merge( .send(.sendCoordFlow(.sendForm(.zecAmountUpdated(transactionState.amountWithoutFee.decimalString().redacted)))), .send(.sendCoordFlow(.sendForm(.addressUpdated(transactionState.address.redacted)))) ) case .deeplinkWarning(.rescanInZashi): state = .initial state.splashAppeared = true return .merge( .send(.destination(.updateDestination(.home))), .send(.home(.scanTapped)) ) // MARK: - Send Coord Flow case .sendCoordFlow(.path(.element(id: _, action: .sendResultSuccess(.closeTapped)))), .sendCoordFlow(.path(.element(id: _, action: .sendResultResubmission(.closeTapped)))), .sendCoordFlow(.path(.element(id: _, action: .sendResultPartial(.dismiss)))): state.path = nil return .none case .sendCoordFlow(.path(.element(id: _, action: .transactionDetails(.closeDetailTapped)))): state.path = nil return .none case .sendCoordFlow(.dismissRequired): state.path = nil return .none // MARK: - Sign with Keystone Coord Flow case .signWithKeystoneCoordFlow(.path(.element(id: _, action: .sendResultSuccess(.closeTapped)))), .signWithKeystoneCoordFlow(.path(.element(id: _, action: .sendResultResubmission(.closeTapped)))): state.signWithKeystoneCoordFlowBinding = false return .none case .signWithKeystoneCoordFlow(.path(.element(id: _, action: .transactionDetails(.closeDetailTapped)))): state.signWithKeystoneCoordFlowBinding = false return .none // MARK: - Transactions Coord Flow case .transactionsCoordFlow(.transactionDetails(.closeDetailTapped)): state.path = nil return .none case .transactionsCoordFlow(.transactionsManager(.dismissRequired)): state.path = nil return .none case .transactionsCoordFlow(.transactionDetails(.sendAgainTapped)): state.path = nil let transactionState = state.transactionsCoordFlowState.transactionDetailsState.transaction return .run { send in try? await mainQueue.sleep(for: .seconds(0.8)) await send(.sendAgainRequested(transactionState)) } case .transactionsCoordFlow(.path(.element(id: _, action: .transactionDetails(.sendAgainTapped)))): for element in state.transactionsCoordFlowState.path { if case .transactionDetails(let transactionDetailsState) = element { state.path = nil return .run { send in try? await mainQueue.sleep(for: .seconds(0.8)) await send(.sendAgainRequested(transactionDetailsState.transaction)) } } } return .none // MARK: - Wallet Backup Coord Flow case .walletBackupCoordFlow(.path(.element(id: _, action: .phrase(.remindMeLaterTapped)))): state.path = nil return .send(.home(.smartBanner(.remindMeLaterTapped(.priority6)))) case .walletBackupCoordFlow(.path(.element(id: _, action: .phrase(.seedSavedTapped)))): state.path = nil do { try walletStorage.markUserPassedPhraseBackupTest(true) } catch { state.alert = AlertState.cantStoreThatUserPassedPhraseBackupTest(error.toZcashError()) } return .merge( .send(.home(.smartBanner(.closeAndCleanupBanner))), .send(.home(.smartBanner(.closeSheetTapped))) ) default: return .none } } } }