// // ImportWalletStore.swift // secant-testnet // // Created by Lukáš Korba on 02/25/2022. // import ComposableArchitecture import ZcashLightClientKit import SwiftUI typealias ImportWalletStore = Store typealias ImportWalletViewStore = ViewStore struct ImportWalletReducer: ReducerProtocol { struct State: Equatable { enum Destination: Equatable { case birthday } @BindingState var alert: AlertState? var birthdayHeight = "".redacted var birthdayHeightValue: RedactableBlockHeight? var destination: Destination? var importedSeedPhrase = "".redacted var isValidMnemonic = false var isValidNumberOfWords = false var maxWordsCount = 0 var wordsCount = 0 var mnemonicStatus: String { if isValidMnemonic { return L10n.ImportWallet.Seed.valid } else { return "\(wordsCount)/\(maxWordsCount)" } } var isValidForm: Bool { isValidMnemonic && (birthdayHeight.data.isEmpty || (!birthdayHeight.data.isEmpty && birthdayHeightValue != nil)) } } enum Action: Equatable, BindableAction { case binding(BindingAction) case birthdayInputChanged(RedactableString) case dismissAlert case restoreWallet case importPrivateOrViewingKey case initializeSDK case onAppear case seedPhraseInputChanged(RedactableString) case successfullyRecovered case updateDestination(ImportWalletReducer.State.Destination?) } @Dependency(\.mnemonic) var mnemonic @Dependency(\.walletStorage) var walletStorage @Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment var body: some ReducerProtocol { BindingReducer() Reduce { state, action in switch action { case .onAppear: state.maxWordsCount = zcashSDKEnvironment.mnemonicWordsMaxCount return .none case .seedPhraseInputChanged(let redactedSeedPhrase): state.importedSeedPhrase = redactedSeedPhrase state.wordsCount = state.importedSeedPhrase.data.split(separator: " ").count state.isValidNumberOfWords = state.wordsCount == state.maxWordsCount // is the mnemonic valid one? do { try mnemonic.isValid(state.importedSeedPhrase.data) } catch { state.isValidMnemonic = false return .none } state.isValidMnemonic = true return .none case .birthdayInputChanged(let redactedBirthday): let saplingActivation = zcashSDKEnvironment.network.constants.saplingActivationHeight state.birthdayHeight = redactedBirthday if let birthdayHeight = BlockHeight(state.birthdayHeight.data), birthdayHeight >= saplingActivation { state.birthdayHeightValue = birthdayHeight.redacted } else { state.birthdayHeightValue = nil } return .none case .binding: return .none case .dismissAlert: state.alert = nil return .none case .restoreWallet: do { // validate the seed try mnemonic.isValid(state.importedSeedPhrase.data) // store it to the keychain let birthday = state.birthdayHeightValue ?? zcashSDKEnvironment.latestCheckpoint.redacted try walletStorage.importWallet(state.importedSeedPhrase.data, birthday.data, .english, false) // update the backup phrase validation flag try walletStorage.markUserPassedPhraseBackupTest(true) // notify user // TODO: [#221] Proper Error/Success handling (https://github.com/zcash/secant-ios-wallet/issues/221) state.alert = AlertState( title: TextState(L10n.General.success), message: TextState(L10n.ImportWallet.Alert.Success.message), dismissButton: .default( TextState(L10n.General.ok), action: .send(.successfullyRecovered) ) ) return EffectTask(value: .initializeSDK) } catch { // TODO: [#221] Proper Error/Success handling (https://github.com/zcash/secant-ios-wallet/issues/221) state.alert = AlertState( title: TextState(L10n.ImportWallet.Alert.Failed.title), message: TextState(L10n.ImportWallet.Alert.Failed.message(error.localizedDescription)), dismissButton: .default( TextState(L10n.General.ok), action: .send(.dismissAlert) ) ) } return .none case .updateDestination(let destination): state.destination = destination return .none case .importPrivateOrViewingKey: return .none case .successfullyRecovered: return EffectTask(value: .dismissAlert) case .initializeSDK: return .none } } } } // MARK: - ViewStore extension ImportWalletViewStore { func bindingForDestination(_ destination: ImportWalletReducer.State.Destination) -> Binding { self.binding( get: { $0.destination == destination }, send: { isActive in return .updateDestination(isActive ? destination : nil) } ) } func bindingForRedactableSeedPhrase(_ importedSeedPhrase: RedactableString) -> Binding { self.binding( get: { _ in importedSeedPhrase.data }, send: { .seedPhraseInputChanged($0.redacted) } ) } func bindingForRedactableBirthday(_ birthdayHeight: RedactableString) -> Binding { self.binding( get: { _ in birthdayHeight.data }, send: { .birthdayInputChanged($0.redacted) } ) } } // MARK: - Placeholders extension ImportWalletReducer.State { static let placeholder = ImportWalletReducer.State() static let live = ImportWalletReducer.State() } extension ImportWalletStore { static let demo = Store( initialState: .placeholder, reducer: ImportWalletReducer() ) }