secant-ios-wallet/modules/Sources/Features/CoordFlows/RestoreWalletCoordFlowStore...

168 lines
5.6 KiB
Swift

//
// RestoreWalletCoordFlowStore.swift
// Zashi
//
// Created by Lukáš Korba on 27-03-2025.
//
import SwiftUI
import ComposableArchitecture
import ZcashLightClientKit
import MnemonicSwift
import Pasteboard
import WalletStorage
// Path
import RestoreInfo
import WalletBirthday
@Reducer
public struct RestoreWalletCoordFlow {
@Reducer
public enum Path {
case estimateBirthdaysDate(WalletBirthday)
case estimatedBirthday(WalletBirthday)
case restoreInfo(RestoreInfo)
case walletBirthday(WalletBirthday)
}
@ObservableState
public struct State {
public var isHelpSheetPresented = false
public var isKeyboardVisible = false
public var isValidSeed = false
public var nextIndex: Int?
public var path = StackState<Path.State>()
public var prevWords: [String] = Array(repeating: "", count: 24)
public var selectedIndex: Int?
public var suggestedWords: [String] = []
public var words: [String] = Array(repeating: "", count: 24)
public var wordsValidity: [Bool] = Array(repeating: true, count: 24)
public init() { }
}
public enum Action: BindableAction {
case binding(BindingAction<RestoreWalletCoordFlow.State>)
case evaluateSeedValidity
case failedToRecover(ZcashError)
case helpSheetRequested
case nextTapped
case path(StackActionOf<Path>)
case resolveRestoreWithBirthday(BlockHeight)
case selectedIndex(Int?)
case successfullyRecovered
case suggestedWordTapped(String)
case suggestionsRequested(Int)
case updateKeyboardFlag(Bool)
#if DEBUG
case debugPasteSeed
#endif
}
@Dependency(\.mnemonic) var mnemonic
@Dependency(\.pasteboard) var pasteboard
@Dependency(\.walletStorage) var walletStorage
public init() { }
public var body: some Reducer<State, Action> {
coordinatorReduce()
BindingReducer()
Reduce { state, action in
switch action {
case .binding(\.words):
let changedIndices = state.words.indices.filter { state.words[$0] != state.prevWords[$0] }
state.prevWords = state.words
if let index = changedIndices.first {
let word = state.words[index]
if word.hasSuffix(" ") {
state.words[index] = word.trimmingCharacters(in: .whitespaces)
state.prevWords = state.words
return .send(.suggestedWordTapped(state.words[index]))
}
return .send(.suggestionsRequested(index))
}
return .none
case .selectedIndex(let index):
state.selectedIndex = index
state.nextIndex = state.selectedIndex
if let index {
return .send(.suggestionsRequested(index))
}
return .none
case .suggestionsRequested(let index):
let prefix = state.words[index]
if prefix.isEmpty {
state.suggestedWords = []
} else {
state.suggestedWords = mnemonic.suggestWords(prefix)
state.wordsValidity[index] = !state.suggestedWords.isEmpty
}
return .send(.evaluateSeedValidity)
case .suggestedWordTapped(let word):
if let index = state.selectedIndex {
state.words[index] = word
if !state.isValidSeed && state.selectedIndex != 23 {
state.prevWords = state.words
state.nextIndex = index + 1 < 24 ? index + 1 : 0
}
return .send(.evaluateSeedValidity)
}
return .none
case .helpSheetRequested:
state.isHelpSheetPresented.toggle()
return .none
case .evaluateSeedValidity:
do {
try mnemonic.isValid(state.words.joined(separator: " "))
state.isValidSeed = true
state.isKeyboardVisible = false
} catch {
state.isValidSeed = false
if let index = state.selectedIndex {
let prefix = state.words[index]
if let first = state.suggestedWords.first, first == prefix && !state.isValidSeed && state.suggestedWords.count == 1 {
state.prevWords = state.words
state.nextIndex = index + 1 < 24 ? index + 1 : 0
}
}
}
return .none
case .updateKeyboardFlag(let value):
state.isKeyboardVisible = value
return .none
#if DEBUG
case .debugPasteSeed:
do {
let seedToPaste = pasteboard.getString()?.data ?? ""
try mnemonic.isValid(seedToPaste)
state.isValidSeed = true
state.isKeyboardVisible = false
state.words = seedToPaste.components(separatedBy: " ")
} catch {
state.isValidSeed = false
}
return .none
#endif
default: return .none
}
}
.forEach(\.path, action: \.path)
}
}