secant-ios-wallet/modules/Sources/Features/ImportWallet/ImportWalletStore.swift

238 lines
8.0 KiB
Swift

//
// ImportWalletStore.swift
// secant-testnet
//
// Created by Lukáš Korba on 02/25/2022.
//
import ComposableArchitecture
import ZcashLightClientKit
import SwiftUI
import Utils
import Generated
import WalletStorage
import MnemonicClient
import ZcashSDKEnvironment
public typealias ImportWalletStore = Store<ImportWalletReducer.State, ImportWalletReducer.Action>
public typealias ImportWalletViewStore = ViewStore<ImportWalletReducer.State, ImportWalletReducer.Action>
public struct ImportWalletReducer: Reducer {
public struct State: Equatable {
public enum Destination: Equatable {
case birthday
}
@PresentationState public var alert: AlertState<Action>?
public var birthdayHeight = "".redacted
public var birthdayHeightValue: RedactableBlockHeight?
public var destination: Destination?
public var importedSeedPhrase = "".redacted
public var isValidMnemonic = false
public var isValidNumberOfWords = false
public var maxWordsCount = 0
public var wordsCount = 0
public var mnemonicStatus: String {
if isValidMnemonic {
return L10n.ImportWallet.Seed.valid
} else {
return "\(wordsCount)/\(maxWordsCount)"
}
}
public var isValidForm: Bool {
isValidMnemonic &&
(birthdayHeight.data.isEmpty ||
(!birthdayHeight.data.isEmpty && birthdayHeightValue != nil))
}
public init(
birthdayHeight: RedactableString = "".redacted,
birthdayHeightValue: RedactableBlockHeight? = nil,
destination: Destination? = nil,
importedSeedPhrase: RedactableString = "".redacted,
isValidMnemonic: Bool = false,
isValidNumberOfWords: Bool = false,
maxWordsCount: Int = 0,
wordsCount: Int = 0
) {
self.alert = alert
self.birthdayHeight = birthdayHeight
self.birthdayHeightValue = birthdayHeightValue
self.destination = destination
self.importedSeedPhrase = importedSeedPhrase
self.isValidMnemonic = isValidMnemonic
self.isValidNumberOfWords = isValidNumberOfWords
self.maxWordsCount = maxWordsCount
self.wordsCount = wordsCount
}
}
public enum Action: Equatable {
case alert(PresentationAction<Action>)
case birthdayInputChanged(RedactableString)
case importPrivateOrViewingKey
case initializeSDK
case nextPressed
case onAppear
case restoreWallet
case seedPhraseInputChanged(RedactableString)
case successfullyRecovered
case updateDestination(ImportWalletReducer.State.Destination?)
}
@Dependency(\.mnemonic) var mnemonic
@Dependency(\.walletStorage) var walletStorage
@Dependency(\.zcashSDKEnvironment) var zcashSDKEnvironment
public init() { }
public var body: some Reducer<State, Action> {
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 .alert(.presented(let action)):
return Effect.send(action)
case .alert(.dismiss):
state.alert = nil
return .none
case .alert:
return .none
case .nextPressed:
return .none
case .restoreWallet:
do {
// validate the seed
try mnemonic.isValid(state.importedSeedPhrase.data)
// store it to the keychain, if the user did not input a height,
// fall back to sapling activation
let birthday = state.birthdayHeightValue ?? zcashSDKEnvironment.network.constants.saplingActivationHeight.redacted
try walletStorage.importWallet(state.importedSeedPhrase.data, birthday.data, .english, false)
// update the backup phrase validation flag
try walletStorage.markUserPassedPhraseBackupTest(true)
state.birthdayHeight = "".redacted
state.importedSeedPhrase = "".redacted
state.destination = nil
// notify user
return .concatenate(
Effect.send(.successfullyRecovered),
Effect.send(.initializeSDK)
)
} catch {
state.alert = AlertState.failed(error.toZcashError())
}
return .none
case .updateDestination(let destination):
state.destination = destination
return .none
case .importPrivateOrViewingKey:
return .none
case .successfullyRecovered:
return .none
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: Alerts
extension AlertState where Action == ImportWalletReducer.Action {
public static func failed(_ error: ZcashError) -> AlertState {
AlertState {
TextState(L10n.ImportWallet.Alert.Failed.title)
} actions: {
ButtonState(action: .alert(.dismiss)) {
TextState(L10n.General.ok)
}
} message: {
TextState(L10n.ImportWallet.Alert.Failed.message(error.detailedMessage))
}
}
}
// MARK: - Placeholders
extension ImportWalletReducer.State {
public static let initial = ImportWalletReducer.State()
}
extension ImportWalletStore {
public static let demo = Store(
initialState: .initial
) {
ImportWalletReducer()
}
}