secant-ios-wallet/modules/Sources/Features/Root/RootStore.swift

629 lines
23 KiB
Swift

import ComposableArchitecture
import ZcashLightClientKit
import DatabaseFiles
import Deeplink
import DiskSpaceChecker
import ZcashSDKEnvironment
import WalletStorage
import WalletConfigProvider
import UserPreferencesStorage
import Models
import NotEnoughFreeSpace
import Welcome
import Generated
import Foundation
import ExportLogs
import OnboardingFlow
import ReadTransactionsStorage
import BackgroundTasks
import Utils
import UserDefaults
import ExchangeRate
import FlexaHandler
import Flexa
import AutolockHandler
import UIComponents
import LocalAuthenticationHandler
import DeeplinkWarning
import URIParser
import OSStatusError
import AddressBookClient
import UserMetadataProvider
import AudioServices
import ShieldingProcessor
import SupportDataGenerator
// Path
import CurrencyConversionSetup
import Home
import Receive
import RecoveryPhraseDisplay
import CoordFlows
import ServerSetup
import Settings
@Reducer
public struct Root {
public enum ResetZashiConstants {
static let maxResetZashiAppAttempts = 3
static let maxResetZashiSDKAttempts = 3
}
let CancelId = UUID()
let CancelStateId = UUID()
let CancelBatteryStateId = UUID()
let SynchronizerCancelId = UUID()
let WalletConfigCancelId = UUID()
let DidFinishLaunchingId = UUID()
let CancelFlexaId = UUID()
@ObservableState
public struct State {
public enum Path {
case addKeystoneHWWalletCoordFlow
case currencyConversionSetup
case receive
case requestZecCoordFlow
case scanCoordFlow
case sendCoordFlow
case settings
case transactionsCoordFlow
case walletBackup
}
public var CancelEventId = UUID()
public var CancelStateId = UUID()
public var shieldingProcessorCancelId = UUID()
@Shared(.inMemory(.addressBookContacts)) public var addressBookContacts: AddressBookContacts = .empty
@Presents public var alert: AlertState<Action>?
public var appInitializationState: InitializationState = .uninitialized
public var appStartState: AppStartState = .unknown
public var bgTask: BGProcessingTask?
@Presents public var confirmationDialog: ConfirmationDialogState<Action.ConfirmationDialog>?
public var debugState: DebugState
public var deeplinkWarningState: DeeplinkWarning.State = .initial
public var destinationState: DestinationState
public var exportLogsState: ExportLogs.State
@Shared(.inMemory(.featureFlags)) public var featureFlags: FeatureFlags = .initial
public var homeState: Home.State = .initial
public var isLockedInKeychainUnavailableState = false
public var isRestoringWallet = false
@Shared(.appStorage(.lastAuthenticationTimestamp)) public var lastAuthenticationTimestamp: Int = 0
public var maxResetZashiAppAttempts = ResetZashiConstants.maxResetZashiAppAttempts
public var maxResetZashiSDKAttempts = ResetZashiConstants.maxResetZashiSDKAttempts
public var messageToBeShared = ""
public var messageShareBinding: String?
public var notEnoughFreeSpaceState: NotEnoughFreeSpace.State
public var onboardingState: OnboardingFlow.State
public var osStatusErrorState: OSStatusError.State
public var path: Path? = nil
public var phraseDisplayState: RecoveryPhraseDisplay.State
@Shared(.inMemory(.selectedWalletAccount)) public var selectedWalletAccount: WalletAccount? = nil
public var serverSetupState: ServerSetup.State
public var serverSetupViewBinding = false
public var signWithKeystoneCoordFlowBinding = false
public var splashAppeared = false
public var supportData: SupportData?
@Shared(.inMemory(.transactions)) public var transactions: IdentifiedArrayOf<TransactionState> = []
@Shared(.inMemory(.transactionMemos)) public var transactionMemos: [String: [String]] = [:]
@Shared(.inMemory(.walletAccounts)) public var walletAccounts: [WalletAccount] = []
public var walletConfig: WalletConfig
@Shared(.inMemory(.walletStatus)) public var walletStatus: WalletStatus = .none
public var wasRestoringWhenDisconnected = false
public var welcomeState: Welcome.State
@Shared(.inMemory(.zashiWalletAccount)) public var zashiWalletAccount: WalletAccount? = nil
// Path
public var addKeystoneHWWalletCoordFlowState = AddKeystoneHWWalletCoordFlow.State.initial
public var currencyConversionSetupState = CurrencyConversionSetup.State.initial
public var receiveState = Receive.State.initial
public var requestZecCoordFlowState = RequestZecCoordFlow.State.initial
public var scanCoordFlowState = ScanCoordFlow.State.initial
public var sendCoordFlowState = SendCoordFlow.State.initial
public var settingsState = Settings.State.initial
public var signWithKeystoneCoordFlowState = SignWithKeystoneCoordFlow.State.initial
public var transactionsCoordFlowState = TransactionsCoordFlow.State.initial
public var walletBackupCoordFlowState = WalletBackupCoordFlow.State.initial
public init(
appInitializationState: InitializationState = .uninitialized,
appStartState: AppStartState = .unknown,
debugState: DebugState,
destinationState: DestinationState,
exportLogsState: ExportLogs.State,
isLockedInKeychainUnavailableState: Bool = false,
isRestoringWallet: Bool = false,
notEnoughFreeSpaceState: NotEnoughFreeSpace.State = .initial,
onboardingState: OnboardingFlow.State,
osStatusErrorState: OSStatusError.State = .initial,
phraseDisplayState: RecoveryPhraseDisplay.State,
serverSetupState: ServerSetup.State = .initial,
walletConfig: WalletConfig,
welcomeState: Welcome.State
) {
self.appInitializationState = appInitializationState
self.appStartState = appStartState
self.debugState = debugState
self.destinationState = destinationState
self.exportLogsState = exportLogsState
self.isLockedInKeychainUnavailableState = isLockedInKeychainUnavailableState
self.isRestoringWallet = isRestoringWallet
self.onboardingState = onboardingState
self.osStatusErrorState = osStatusErrorState
self.notEnoughFreeSpaceState = notEnoughFreeSpaceState
self.phraseDisplayState = phraseDisplayState
self.serverSetupState = serverSetupState
self.walletConfig = walletConfig
self.welcomeState = welcomeState
}
}
public enum Action: BindableAction {
public enum ConfirmationDialog: Equatable {
case fullRescan
case quickRescan
}
case alert(PresentationAction<Action>)
case batteryStateChanged(Notification?)
case binding(BindingAction<Root.State>)
case cancelAllRunningEffects
case confirmationDialog(PresentationAction<ConfirmationDialog>)
case debug(DebugAction)
case deeplinkWarning(DeeplinkWarning.Action)
case destination(DestinationAction)
case exportLogs(ExportLogs.Action)
case flexaOnTransactionRequest(FlexaTransaction?)
case flexaOpenRequest
case flexaTransactionFailed(String)
case home(Home.Action)
case initialization(InitializationAction)
case notEnoughFreeSpace(NotEnoughFreeSpace.Action)
case resetZashiFinishProcessing
case resetZashiKeychainFailed(OSStatus)
case resetZashiKeychainFailedWithCorruptedData(String)
case resetZashiKeychainRequest
case resetZashiSDKFailed
case resetZashiSDKSucceeded
case onboarding(OnboardingFlow.Action)
case osStatusError(OSStatusError.Action)
case phraseDisplay(RecoveryPhraseDisplay.Action)
case serverSetup(ServerSetup.Action)
case serverSetupBindingUpdated(Bool)
case splashFinished
case splashRemovalRequested
case synchronizerStateChanged(RedactableSynchronizerState)
case transactionDetailsOpen(String)
case updateStateAfterConfigUpdate(WalletConfig)
case walletConfigLoaded(WalletConfig)
case welcome(Welcome.Action)
// Path
case addKeystoneHWWalletCoordFlow(AddKeystoneHWWalletCoordFlow.Action)
case currencyConversionSetup(CurrencyConversionSetup.Action)
case receive(Receive.Action)
case requestZecCoordFlow(RequestZecCoordFlow.Action)
case scanCoordFlow(ScanCoordFlow.Action)
case sendAgainRequested(TransactionState)
case sendCoordFlow(SendCoordFlow.Action)
case settings(Settings.Action)
case signWithKeystoneCoordFlow(SignWithKeystoneCoordFlow.Action)
case signWithKeystoneRequested
case transactionsCoordFlow(TransactionsCoordFlow.Action)
case walletBackupCoordFlow(WalletBackupCoordFlow.Action)
// Transactions
case observeTransactions
case foundTransactions([ZcashTransaction.Overview])
case minedTransaction(ZcashTransaction.Overview)
case fetchTransactionsForTheSelectedAccount
case fetchedTransactions([TransactionState])
case noChangeInTransactions
// Address Book
case loadContacts
case contactsLoaded(AddressBookContacts)
// UserMetadata
case loadUserMetadata
case resolveMetadataEncryptionKeys
// Shielding
case observeShieldingProcessor
case reportShieldingFailure
case shareFinished
case shieldingProcessorStateChanged(ShieldingProcessorClient.State)
}
@Dependency(\.addressBook) var addressBook
@Dependency(\.audioServices) var audioServices
@Dependency(\.autolockHandler) var autolockHandler
@Dependency(\.databaseFiles) var databaseFiles
@Dependency(\.deeplink) var deeplink
@Dependency(\.derivationTool) var derivationTool
@Dependency(\.diskSpaceChecker) var diskSpaceChecker
@Dependency(\.exchangeRate) var exchangeRate
@Dependency(\.flexaHandler) var flexaHandler
@Dependency(\.localAuthentication) var localAuthentication
@Dependency(\.mainQueue) var mainQueue
@Dependency(\.mnemonic) var mnemonic
@Dependency(\.numberFormatter) var numberFormatter
@Dependency(\.pasteboard) var pasteboard
@Dependency(\.sdkSynchronizer) var sdkSynchronizer
@Dependency(\.shieldingProcessor) var shieldingProcessor
@Dependency(\.uriParser) var uriParser
@Dependency(\.userDefaults) var userDefaults
@Dependency(\.userMetadataProvider) var userMetadataProvider
@Dependency(\.userStoredPreferences) var userStoredPreferences
@Dependency(\.walletConfigProvider) var walletConfigProvider
@Dependency(\.walletStorage) var walletStorage
@Dependency(\.readTransactionsStorage) var readTransactionsStorage
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
public init() { }
@ReducerBuilder<State, Action>
var core: some Reducer<State, Action> {
BindingReducer()
Scope(state: \.deeplinkWarningState, action: \.deeplinkWarning) {
DeeplinkWarning()
}
Scope(state: \.serverSetupState, action: \.serverSetup) {
ServerSetup()
}
Scope(state: \.homeState, action: \.home) {
Home()
}
Scope(state: \.exportLogsState, action: \.exportLogs) {
ExportLogs()
}
Scope(state: \.notEnoughFreeSpaceState, action: \.notEnoughFreeSpace) {
NotEnoughFreeSpace()
}
Scope(state: \.onboardingState, action: \.onboarding) {
OnboardingFlow()
}
Scope(state: \.welcomeState, action: \.welcome) {
Welcome()
}
Scope(state: \.phraseDisplayState, action: \.phraseDisplay) {
RecoveryPhraseDisplay()
}
Scope(state: \.osStatusErrorState, action: \.osStatusError) {
OSStatusError()
}
Scope(state: \.settingsState, action: \.settings) {
Settings()
}
Scope(state: \.receiveState, action: \.receive) {
Receive()
}
Scope(state: \.requestZecCoordFlowState, action: \.requestZecCoordFlow) {
RequestZecCoordFlow()
}
Scope(state: \.sendCoordFlowState, action: \.sendCoordFlow) {
SendCoordFlow()
}
Scope(state: \.scanCoordFlowState, action: \.scanCoordFlow) {
ScanCoordFlow()
}
Scope(state: \.addKeystoneHWWalletCoordFlowState, action: \.addKeystoneHWWalletCoordFlow) {
AddKeystoneHWWalletCoordFlow()
}
Scope(state: \.transactionsCoordFlowState, action: \.transactionsCoordFlow) {
TransactionsCoordFlow()
}
Scope(state: \.walletBackupCoordFlowState, action: \.walletBackupCoordFlow) {
WalletBackupCoordFlow()
}
Scope(state: \.currencyConversionSetupState, action: \.currencyConversionSetup) {
CurrencyConversionSetup()
}
Scope(state: \.signWithKeystoneCoordFlowState, action: \.signWithKeystoneCoordFlow) {
SignWithKeystoneCoordFlow()
}
initializationReduce()
destinationReduce()
debugReduce()
transactionsReduce()
addressBookReduce()
userMetadataReduce()
coordinatorReduce()
shieldingProcessorReduce()
}
public var body: some Reducer<State, Action> {
self.core
Reduce { state, action in
switch action {
case .alert(.presented(let action)):
return .send(action)
case .alert(.dismiss):
state.alert = nil
return .none
case .serverSetup:
return .none
case .serverSetupBindingUpdated(let newValue):
state.serverSetupViewBinding = newValue
return .none
case .batteryStateChanged:
let leavesScreenOpen = userDefaults.objectForKey(Constants.udLeavesScreenOpen) as? Bool ?? false
autolockHandler.value(state.walletStatus == .restoring && leavesScreenOpen)
return .none
case .cancelAllRunningEffects:
return .concatenate(
.cancel(id: CancelId),
.cancel(id: CancelStateId),
.cancel(id: CancelBatteryStateId),
.cancel(id: SynchronizerCancelId),
.cancel(id: WalletConfigCancelId),
.cancel(id: DidFinishLaunchingId)
)
case .onboarding(.newWalletSuccessfulyCreated):
return .send(.initialization(.initializeSDK(.newWallet)))
default: return .none
}
}
}
}
extension Root {
public static func walletInitializationState(
databaseFiles: DatabaseFilesClient,
walletStorage: WalletStorageClient,
zcashNetwork: ZcashNetwork
) -> InitializationState {
var keysPresent = false
do {
keysPresent = try walletStorage.areKeysPresent()
let databaseFilesPresent = databaseFiles.areDbFilesPresentFor(zcashNetwork)
switch (keysPresent, databaseFilesPresent) {
case (false, false):
return .uninitialized
case (false, true):
return .keysMissing
case (true, false):
return .filesMissing
case (true, true):
return .initialized
}
} catch WalletStorage.WalletStorageError.uninitializedWallet {
if databaseFiles.areDbFilesPresentFor(zcashNetwork) {
return .keysMissing
}
} catch WalletStorage.KeychainError.unknown(let osStatus) {
return .osStatus(osStatus)
} catch {
return .failed
}
return .uninitialized
}
}
// MARK: Alerts
extension AlertState where Action == Root.Action {
public static func cantLoadSeedPhrase() -> AlertState {
AlertState {
TextState(L10n.Root.Initialization.Alert.Failed.title)
} message: {
TextState(L10n.Root.Initialization.Alert.CantLoadSeedPhrase.message)
}
}
public static func cantStartSync(_ error: ZcashError) -> AlertState {
AlertState {
TextState(L10n.Root.Debug.Alert.Rewind.CantStartSync.title)
} message: {
TextState(L10n.Root.Debug.Alert.Rewind.CantStartSync.message(error.detailedMessage))
}
}
public static func cantStoreThatUserPassedPhraseBackupTest(_ error: ZcashError) -> AlertState {
AlertState {
TextState(L10n.Root.Initialization.Alert.Failed.title)
} message: {
TextState(
L10n.Root.Initialization.Alert.CantStoreThatUserPassedPhraseBackupTest.message(error.detailedMessage)
)
}
}
public static func failedToProcessDeeplink(_ url: URL, _ error: ZcashError) -> AlertState {
AlertState {
TextState(L10n.Root.Destination.Alert.FailedToProcessDeeplink.title)
} message: {
TextState(L10n.Root.Destination.Alert.FailedToProcessDeeplink.message(url, error.message, error.code.rawValue))
}
}
public static func initializationFailed(_ error: ZcashError) -> AlertState {
AlertState {
TextState(L10n.Root.Initialization.Alert.SdkInitFailed.title)
} message: {
TextState(L10n.Root.Initialization.Alert.Error.message(error.detailedMessage))
}
}
public static func rewindFailed(_ error: ZcashError) -> AlertState {
AlertState {
TextState(L10n.Root.Debug.Alert.Rewind.Failed.title)
} message: {
TextState(L10n.Root.Debug.Alert.Rewind.Failed.message(error.detailedMessage))
}
}
public static func walletStateFailed(_ walletState: InitializationState) -> AlertState {
AlertState {
TextState(L10n.Root.Initialization.Alert.Failed.title)
} actions: {
ButtonState(role: .destructive, action: .initialization(.resetZashi)) {
TextState(L10n.Settings.deleteZashi)
}
ButtonState(role: .cancel, action: .alert(.dismiss)) {
TextState(L10n.General.ok)
}
} message: {
TextState(L10n.Root.Initialization.Alert.WalletStateFailed.message(walletState))
}
}
public static func wipeFailed(_ osStatus: OSStatus) -> AlertState {
AlertState {
TextState(L10n.Root.Initialization.Alert.WipeFailed.title)
} message: {
TextState("OSStatus: \(osStatus), \(L10n.Root.Initialization.Alert.WipeFailed.message)")
}
}
public static func wipeKeychainFailed(_ errMsg: String) -> AlertState {
AlertState {
TextState(L10n.Root.Initialization.Alert.WipeFailed.title)
} message: {
TextState("Keychain failed: \(errMsg)")
}
}
public static func wipeRequest() -> AlertState {
AlertState {
TextState(L10n.Root.Initialization.Alert.Wipe.title)
} actions: {
ButtonState(role: .destructive, action: .initialization(.resetZashi)) {
TextState(L10n.General.yes)
}
ButtonState(role: .cancel, action: .initialization(.resetZashiRequestCanceled)) {
TextState(L10n.General.no)
}
} message: {
TextState(L10n.Root.Initialization.Alert.Wipe.message)
}
}
public static func differentSeed() -> AlertState {
AlertState {
TextState(L10n.General.Alert.warning)
} actions: {
ButtonState(role: .cancel, action: .alert(.dismiss)) {
TextState(L10n.Root.SeedPhrase.DifferentSeed.tryAgain)
}
ButtonState(role: .destructive, action: .initialization(.resetZashi)) {
TextState(L10n.General.Alert.continue)
}
} message: {
TextState(L10n.Root.SeedPhrase.DifferentSeed.message)
}
}
public static func existingWallet() -> AlertState {
AlertState {
TextState(L10n.General.Alert.warning)
} actions: {
ButtonState(role: .cancel, action: .initialization(.restoreExistingWallet)) {
TextState(L10n.Root.ExistingWallet.restore)
}
ButtonState(role: .destructive, action: .initialization(.resetZashi)) {
TextState(L10n.General.Alert.continue)
}
} message: {
TextState(L10n.Root.ExistingWallet.message)
}
}
public static func serviceUnavailable() -> AlertState {
AlertState {
TextState(L10n.General.Alert.caution)
} actions: {
ButtonState(action: .alert(.dismiss)) {
TextState(L10n.General.Alert.ignore)
}
ButtonState(action: .destination(.serverSwitch)) {
TextState(L10n.Root.ServiceUnavailable.switchServer)
}
} message: {
TextState(L10n.Root.ServiceUnavailable.message)
}
}
public static func shieldFundsFailure(_ error: ZcashError) -> AlertState {
AlertState {
TextState(L10n.ShieldFunds.Error.title)
} actions: {
ButtonState(action: .alert(.dismiss)) {
TextState(L10n.General.ok)
}
ButtonState(action: .reportShieldingFailure) {
TextState(L10n.Send.report)
}
} message: {
TextState(L10n.ShieldFunds.Error.Failure.message(error.detailedMessage))
}
}
public static func shieldFundsGrpc() -> AlertState {
AlertState {
TextState(L10n.ShieldFunds.Error.title)
} message: {
TextState(L10n.ShieldFunds.Error.Gprc.message)
}
}
}
extension ConfirmationDialogState where Action == Root.Action.ConfirmationDialog {
public static func rescanRequest() -> ConfirmationDialogState {
ConfirmationDialogState {
TextState(L10n.Root.Debug.Dialog.Rescan.title)
} actions: {
ButtonState(role: .destructive, action: .quickRescan) {
TextState(L10n.Root.Debug.Dialog.Rescan.Option.quick)
}
ButtonState(role: .destructive, action: .fullRescan) {
TextState(L10n.Root.Debug.Dialog.Rescan.Option.full)
}
ButtonState(role: .cancel) {
TextState(L10n.General.cancel)
}
} message: {
TextState(L10n.Root.Debug.Dialog.Rescan.message)
}
}
}