zashi-ios-wallet-private/secant/Features/ImportWallet/ImportWalletStore.swift

202 lines
7.1 KiB
Swift

//
// ImportWalletStore.swift
// secant-testnet
//
// Created by Lukáš Korba on 02/25/2022.
//
import ComposableArchitecture
import ZcashLightClientKit
import SwiftUI
typealias ImportWalletStore = Store<ImportWalletReducer.State, ImportWalletReducer.Action>
typealias ImportWalletViewStore = ViewStore<ImportWalletReducer.State, ImportWalletReducer.Action>
struct ImportWalletReducer: ReducerProtocol {
struct State: Equatable {
enum Destination: Equatable {
case birthday
}
@BindingState var alert: AlertState<ImportWalletReducer.Action>?
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<ImportWalletReducer.State>)
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<State, Action> {
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<Bool> {
self.binding(
get: { $0.destination == destination },
send: { isActive in
return .updateDestination(isActive ? destination : nil)
}
)
}
func bindingForRedactableSeedPhrase(_ importedSeedPhrase: RedactableString) -> Binding<String> {
self.binding(
get: { _ in importedSeedPhrase.data },
send: { .seedPhraseInputChanged($0.redacted) }
)
}
func bindingForRedactableBirthday(_ birthdayHeight: RedactableString) -> Binding<String> {
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()
)
}