[#494] Simplification of the AppReducer's body property (#501)

- destination related actions have been isolated
- app/wallet initialization related actions have been isolated
- AppViewStore helpers encapsulating nested actions for the destinations and deeplinking
- destination enum and state moved to separate file as well
This commit is contained in:
Lukas Korba 2022-12-07 14:47:42 +01:00 committed by GitHub
parent 5847088ce9
commit d11a67cee1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 435 additions and 354 deletions

View File

@ -168,6 +168,8 @@
9E92AF0828530EBF007367AD /* View+UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E92AF0728530EBF007367AD /* View+UIImage.swift */; };
9E94C62028AA7DEE008256E9 /* BalanceBreakdownTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E94C61F28AA7DEE008256E9 /* BalanceBreakdownTests.swift */; };
9E94C62328AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */; };
9E9ADA7D2938F4C00071767B /* RootInitialization.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9ADA7C2938F4C00071767B /* RootInitialization.swift */; };
9E9ADA7F2938F5EC0071767B /* RootDestination.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9ADA7E2938F5EC0071767B /* RootDestination.swift */; };
9E9ECC9728589E150099D5A2 /* HomeSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9ECC8C28589E150099D5A2 /* HomeSnapshotTests.swift */; };
9E9ECC9828589E150099D5A2 /* WelcomeSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9ECC8E28589E150099D5A2 /* WelcomeSnapshotTests.swift */; };
9E9ECC9928589E150099D5A2 /* RecoveryPhraseDisplaySnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E9ECC9028589E150099D5A2 /* RecoveryPhraseDisplaySnapshotTests.swift */; };
@ -459,6 +461,8 @@
9E92AF0728530EBF007367AD /* View+UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+UIImage.swift"; sourceTree = "<group>"; };
9E94C61F28AA7DEE008256E9 /* BalanceBreakdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownTests.swift; sourceTree = "<group>"; };
9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownSnapshotTests.swift; sourceTree = "<group>"; };
9E9ADA7C2938F4C00071767B /* RootInitialization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootInitialization.swift; sourceTree = "<group>"; };
9E9ADA7E2938F5EC0071767B /* RootDestination.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootDestination.swift; sourceTree = "<group>"; };
9E9ECC8C28589E150099D5A2 /* HomeSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeSnapshotTests.swift; sourceTree = "<group>"; };
9E9ECC8E28589E150099D5A2 /* WelcomeSnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WelcomeSnapshotTests.swift; sourceTree = "<group>"; };
9E9ECC9028589E150099D5A2 /* RecoveryPhraseDisplaySnapshotTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseDisplaySnapshotTests.swift; sourceTree = "<group>"; };
@ -1661,6 +1665,8 @@
children = (
F9971A4A27680DC400A2DB75 /* RootStore.swift */,
F9971A4C27680DC400A2DB75 /* RootView.swift */,
9E9ADA7E2938F5EC0071767B /* RootDestination.swift */,
9E9ADA7C2938F4C00071767B /* RootInitialization.swift */,
);
path = Root;
sourceTree = "<group>";
@ -1987,6 +1993,7 @@
9EB8638C2922CD4A003D0F8B /* FeedbackGeneratorTestKey.swift in Sources */,
0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidationFlowStore.swift in Sources */,
9E2DF99E27CF704D00649636 /* ImportWalletView.swift in Sources */,
9E9ADA7D2938F4C00071767B /* RootInitialization.swift in Sources */,
9EBDF967291ECDA2000A1A05 /* AudioServicesTestKey.swift in Sources */,
0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */,
9EBDF97E291F7EB0000A1A05 /* AppVersionInterface.swift in Sources */,
@ -2101,6 +2108,7 @@
9E5BF64F2823E94900BA3F17 /* TransactionAddressTextField.swift in Sources */,
2E35F99227B28E7600EB79CD /* SingleLineTextField.swift in Sources */,
0D8A43C6272B129C005A6414 /* WordChipGrid.swift in Sources */,
9E9ADA7F2938F5EC0071767B /* RootDestination.swift in Sources */,
66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */,
9EBDF96D291ECED4000A1A05 /* CaptureDeviceInterface.swift in Sources */,
9EB8638D2922CD4A003D0F8B /* FeedbackGeneratorLiveKey.swift in Sources */,

View File

@ -201,7 +201,7 @@
{
"identity" : "zcash-light-client-ffi",
"kind" : "remoteSourceControl",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi",
"location" : "https://github.com/zcash-hackworks/zcash-light-client-ffi.git",
"state" : {
"revision" : "fad9802b907822d5a1877584c91f3786824521b7",
"version" : "0.1.0"

View File

@ -0,0 +1,157 @@
//
// RootDestination.swift
// secant-testnet
//
// Created by Lukáš Korba on 01.12.2022.
//
import Foundation
import ComposableArchitecture
import ZcashLightClientKit
/// In this file is a collection of helpers that control all state and action related operations
/// for the `RootReducer` with a connection to the UI navigation.
extension RootReducer {
struct DestinationState: Equatable {
enum Destination: Equatable {
case welcome
case startup
case onboarding
case sandbox
case home
case phraseValidation
case phraseDisplay
}
var internalDestination: Destination = .welcome
var previousDestination: Destination?
var destination: Destination {
get { internalDestination }
set {
previousDestination = internalDestination
internalDestination = newValue
}
}
}
enum DestinationAction: Equatable {
case deeplink(URL)
case deeplinkHome
case deeplinkSend(Zatoshi, String, String)
case updateDestination(RootReducer.DestinationState.Destination)
}
// swiftlint:disable:next cyclomatic_complexity
func destinationReduce() -> Reduce<RootReducer.State, RootReducer.Action> {
Reduce { state, action in
switch action {
case let .destination(.updateDestination(destination)):
state.destinationState.destination = destination
case .sandbox(.reset):
state.destinationState.destination = .startup
case .phraseValidation(.proceedToHome):
state.destinationState.destination = .home
case .phraseValidation(.displayBackedUpPhrase):
state.destinationState.destination = .phraseDisplay
case .phraseDisplay(.finishedPressed):
// user is still supposed to do the backup phrase validation test
if state.destinationState.previousDestination == .welcome
|| state.destinationState.previousDestination == .onboarding {
state.destinationState.destination = .phraseValidation
}
// user wanted to see the backup phrase once again (at validation finished screen)
if state.destinationState.previousDestination == .phraseValidation {
state.destinationState.destination = .home
}
case .destination(.deeplink(let url)):
// get the latest synchronizer state
let synchronizerStatus = sdkSynchronizer.stateChanged.value
// process the deeplink only if app is initialized and synchronizer synced
guard state.appInitializationState == .initialized && synchronizerStatus == .synced else {
// TODO [#370]: There are many different states and edge cases we need to handle here
// (https://github.com/zcash/secant-ios-wallet/issues/370)
return .none
}
return .run { send in
do {
await send(
try await process(
url: url,
deeplink: deeplink,
derivationTool: derivationTool
)
)
} catch {
// TODO [#221]: error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
}
}
case .destination(.deeplinkHome):
state.destinationState.destination = .home
state.homeState.destination = nil
return .none
case let .destination(.deeplinkSend(amount, address, memo)):
state.destinationState.destination = .home
state.homeState.destination = .send
state.homeState.sendState.amount = amount
state.homeState.sendState.address = address
state.homeState.sendState.memoState.text = memo
return .none
case .home(.walletEvents(.replyTo(let address))):
guard let url = URL(string: "zcash:\(address)") else {
return .none
}
return Effect(value: .destination(.deeplink(url)))
case .home, .initialization, .onboarding, .phraseDisplay, .phraseValidation, .sandbox, .welcome:
return .none
}
return .none
}
}
}
private extension RootReducer {
func process(
url: URL,
deeplink: DeeplinkClient,
derivationTool: DerivationToolClient
) async throws -> RootReducer.Action {
let deeplink = try deeplink.resolveDeeplinkURL(url, derivationTool)
switch deeplink {
case .home:
return .destination(.deeplinkHome)
case let .send(amount, address, memo):
return .destination(.deeplinkSend(Zatoshi(amount), address, memo))
}
}
}
extension RootViewStore {
func goToDestination(_ destination: RootReducer.DestinationState.Destination) {
send(.destination(.updateDestination(destination)))
}
func goToDeeplink(_ deeplink: URL) {
send(.destination(.deeplink(deeplink)))
}
}
// MARK: Placeholders
extension RootReducer.DestinationState {
static var placeholder: Self {
.init()
}
}

View File

@ -0,0 +1,197 @@
//
// RootInitialization.swift
// secant-testnet
//
// Created by Lukáš Korba on 01.12.2022.
//
import ComposableArchitecture
/// In this file is a collection of helpers that control all state and action related operations
/// for the `RootReducer` with a connection to the app/wallet initalization and erasure of the wallet.
extension RootReducer {
enum InitializationAction: Equatable {
case appDelegate(AppDelegateAction)
case checkBackupPhraseValidation
case checkWalletInitialization
case createNewWallet
case initializeSDK
case nukeWallet
case respondToWalletInitializationState(InitializationState)
}
// swiftlint:disable:next cyclomatic_complexity
func initializationReduce() -> Reduce<RootReducer.State, RootReducer.Action> {
Reduce { state, action in
switch action {
case .initialization(.appDelegate(.didFinishLaunching)):
/// We need to fetch data from keychain, in order to be 100% sure the kecyhain can be read we delay the check a bit
return Effect(value: .initialization(.checkWalletInitialization))
.delay(for: 0.02, scheduler: mainQueue)
.eraseToEffect()
/// Evaluate the wallet's state based on keychain keys and database files presence
case .initialization(.checkWalletInitialization):
let walletState = RootReducer.walletInitializationState(
databaseFiles: databaseFiles,
walletStorage: walletStorage,
zcashSDKEnvironment: zcashSDKEnvironment
)
return Effect(value: .initialization(.respondToWalletInitializationState(walletState)))
/// Respond to all possible states of the wallet and initiate appropriate side effects including errors handling
case .initialization(.respondToWalletInitializationState(let walletState)):
switch walletState {
case .failed:
// TODO [#221]: error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
state.appInitializationState = .failed
case .keysMissing:
// TODO [#221]: error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
state.appInitializationState = .keysMissing
case .initialized, .filesMissing:
if walletState == .filesMissing {
state.appInitializationState = .filesMissing
}
return .concatenate(
Effect(value: .initialization(.initializeSDK)),
Effect(value: .initialization(.checkBackupPhraseValidation))
)
case .uninitialized:
state.appInitializationState = .uninitialized
return Effect(value: .destination(.updateDestination(.onboarding)))
.delay(for: 3, scheduler: mainQueue)
.eraseToEffect()
.cancellable(id: CancelId.self, cancelInFlight: true)
}
return .none
/// Stored wallet is present, database files may or may not be present, trying to initialize app state variables and environments.
/// When initialization succeeds user is taken to the home screen.
case .initialization(.initializeSDK):
do {
state.storedWallet = try walletStorage.exportWallet()
guard let storedWallet = state.storedWallet else {
state.appInitializationState = .failed
// TODO [#221]: fatal error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
return .none
}
try mnemonic.isValid(storedWallet.seedPhrase)
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase)
let birthday = state.storedWallet?.birthday ?? zcashSDKEnvironment.defaultBirthday
let initializer = try RootReducer.prepareInitializer(
for: storedWallet.seedPhrase,
birthday: birthday,
databaseFiles: databaseFiles,
derivationTool: derivationTool,
mnemonic: mnemonic,
zcashSDKEnvironment: zcashSDKEnvironment
)
try sdkSynchronizer.prepareWith(initializer: initializer, seedBytes: seedBytes)
try sdkSynchronizer.start()
return .none
} catch {
// TODO [#221]: error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
state.appInitializationState = .failed
return .none
}
case .initialization(.checkBackupPhraseValidation):
guard let storedWallet = state.storedWallet else {
state.appInitializationState = .failed
// TODO [#221]: fatal error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
return .none
}
var landingDestination = RootReducer.DestinationState.Destination.home
if !storedWallet.hasUserPassedPhraseBackupTest {
do {
let phraseWords = try mnemonic.asWords(storedWallet.seedPhrase)
let recoveryPhrase = RecoveryPhrase(words: phraseWords)
state.phraseDisplayState.phrase = recoveryPhrase
state.phraseValidationState = randomRecoveryPhrase.random(recoveryPhrase)
landingDestination = .phraseDisplay
} catch {
// TODO [#201]: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States
return .none
}
}
state.appInitializationState = .initialized
return Effect(value: .destination(.updateDestination(landingDestination)))
.delay(for: 3, scheduler: mainQueue)
.eraseToEffect()
.cancellable(id: CancelId.self, cancelInFlight: true)
case .initialization(.createNewWallet):
do {
// get the random english mnemonic
let newRandomPhrase = try mnemonic.randomMnemonic()
let birthday = try zcashSDKEnvironment.lightWalletService.latestBlockHeight()
// store the wallet to the keychain
try walletStorage.importWallet(newRandomPhrase, birthday, .english, false)
// start the backup phrase validation test
let randomRecoveryPhraseWords = try mnemonic.asWords(newRandomPhrase)
let recoveryPhrase = RecoveryPhrase(words: randomRecoveryPhraseWords)
state.phraseDisplayState.phrase = recoveryPhrase
state.phraseValidationState = randomRecoveryPhrase.random(recoveryPhrase)
return .concatenate(
Effect(value: .initialization(.initializeSDK)),
Effect(value: .phraseValidation(.displayBackedUpPhrase))
)
} catch {
// TODO [#201]: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States
}
return .none
case .phraseValidation(.succeed):
do {
try walletStorage.markUserPassedPhraseBackupTest()
} catch {
// TODO [#221]: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
}
return .none
case .initialization(.nukeWallet):
walletStorage.nukeWallet()
do {
try databaseFiles.nukeDbFilesFor(zcashSDKEnvironment.network)
} catch {
// TODO [#221]: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
}
return .none
case .welcome(.debugMenuStartup), .home(.debugMenuStartup):
return .concatenate(
Effect.cancel(id: CancelId.self),
Effect(value: .destination(.updateDestination(.startup)))
)
case .onboarding(.importWallet(.successfullyRecovered)):
return Effect(value: .destination(.updateDestination(.home)))
case .onboarding(.importWallet(.initializeSDK)):
return Effect(value: .initialization(.initializeSDK))
case .onboarding(.createNewWallet):
return Effect(value: .initialization(.createNewWallet))
case .home, .destination, .onboarding, .phraseDisplay, .phraseValidation, .sandbox, .welcome:
return .none
}
}
}
}

View File

@ -1,62 +1,33 @@
import ComposableArchitecture
import ZcashLightClientKit
import Foundation
typealias RootStore = Store<RootReducer.State, RootReducer.Action>
typealias RootViewStore = ViewStore<RootReducer.State, RootReducer.Action>
// swiftlint:disable type_body_length
struct RootReducer: ReducerProtocol {
private enum CancelId {}
enum CancelId {}
struct State: Equatable {
enum Destination: Equatable {
case welcome
case startup
case onboarding
case sandbox
case home
case phraseValidation
case phraseDisplay
}
var appInitializationState: InitializationState = .uninitialized
var destinationState: DestinationState
var homeState: HomeReducer.State
var onboardingState: OnboardingFlowReducer.State
var phraseValidationState: RecoveryPhraseValidationFlowReducer.State
var phraseDisplayState: RecoveryPhraseDisplayReducer.State
var prevDestination: Destination?
var internalDestination: Destination = .welcome
var sandboxState: SandboxReducer.State
var storedWallet: StoredWallet?
var welcomeState: WelcomeReducer.State
var destination: Destination {
get { internalDestination }
set {
prevDestination = internalDestination
internalDestination = newValue
}
}
}
enum Action: Equatable {
case appDelegate(AppDelegateAction)
case checkBackupPhraseValidation
case checkWalletInitialization
case createNewWallet
case deeplink(URL)
case deeplinkHome
case deeplinkSend(Zatoshi, String, String)
case destination(DestinationAction)
case home(HomeReducer.Action)
case initializeSDK
case nukeWallet
case initialization(InitializationAction)
case onboarding(OnboardingFlowReducer.Action)
case phraseDisplay(RecoveryPhraseDisplayReducer.Action)
case phraseValidation(RecoveryPhraseValidationFlowReducer.Action)
case respondToWalletInitializationState(InitializationState)
case sandbox(SandboxReducer.Action)
case updateDestination(RootReducer.State.Destination)
case welcome(WelcomeReducer.Action)
}
@ -91,254 +62,13 @@ struct RootReducer: ReducerProtocol {
SandboxReducer()
}
Reduce { state, action in
switch action {
case let .updateDestination(destination):
state.destination = destination
case .sandbox(.reset):
state.destination = .startup
case .onboarding(.createNewWallet):
return Effect(value: .createNewWallet)
case .phraseValidation(.proceedToHome):
state.destination = .home
case .phraseValidation(.displayBackedUpPhrase):
state.destination = .phraseDisplay
case .phraseDisplay(.finishedPressed):
// user is still supposed to do the backup phrase validation test
if state.prevDestination == .welcome || state.prevDestination == .onboarding {
state.destination = .phraseValidation
}
// user wanted to see the backup phrase once again (at validation finished screen)
if state.prevDestination == .phraseValidation {
state.destination = .home
}
case .deeplink(let url):
// get the latest synchronizer state
var synchronizerStatus = SDKSynchronizerState.unknown
_ = sdkSynchronizer.stateChanged.sink { synchronizerStatus = $0 }
// process the deeplink only if app is initialized and synchronizer synced
guard state.appInitializationState == .initialized && synchronizerStatus == .synced else {
// TODO [#370]: There are many different states and edge cases we need to handle here
// (https://github.com/zcash/secant-ios-wallet/issues/370)
return .none
}
return .run { send in
do {
await send(
try await RootReducer.process(
url: url,
deeplink: deeplink,
derivationTool: derivationTool
)
)
} catch {
// TODO [#221]: error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
}
}
case .deeplinkHome:
state.destination = .home
state.homeState.destination = nil
return .none
case let .deeplinkSend(amount, address, memo):
state.destination = .home
state.homeState.destination = .send
state.homeState.sendState.amount = amount
state.homeState.sendState.address = address
state.homeState.sendState.memoState.text = memo
return .none
case .home(.walletEvents(.replyTo(let address))):
guard let url = URL(string: "zcash:\(address)") else {
return .none
}
return Effect(value: .deeplink(url))
/// Default is meaningful here because there's `rootReducer` handling actions and this reducer is handling only destinations. We don't here plenty of unused cases.
default:
break
}
return .none
Scope(state: \.welcomeState, action: /Action.welcome) {
WelcomeReducer()
}
Reduce { state, action in
switch action {
case .appDelegate(.didFinishLaunching):
/// We need to fetch data from keychain, in order to be 100% sure the kecyhain can be read we delay the check a bit
return Effect(value: .checkWalletInitialization)
.delay(for: 0.02, scheduler: mainQueue)
.eraseToEffect()
/// Evaluate the wallet's state based on keychain keys and database files presence
case .checkWalletInitialization:
let walletState = RootReducer.walletInitializationState(
databaseFiles: databaseFiles,
walletStorage: walletStorage,
zcashSDKEnvironment: zcashSDKEnvironment
)
return Effect(value: .respondToWalletInitializationState(walletState))
/// Respond to all possible states of the wallet and initiate appropriate side effects including errors handling
case .respondToWalletInitializationState(let walletState):
switch walletState {
case .failed:
// TODO [#221]: error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
state.appInitializationState = .failed
case .keysMissing:
// TODO [#221]: error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
state.appInitializationState = .keysMissing
case .initialized, .filesMissing:
if walletState == .filesMissing {
state.appInitializationState = .filesMissing
}
return .concatenate(
Effect(value: .initializeSDK),
Effect(value: .checkBackupPhraseValidation)
)
case .uninitialized:
state.appInitializationState = .uninitialized
return Effect(value: .updateDestination(.onboarding))
.delay(for: 3, scheduler: mainQueue)
.eraseToEffect()
.cancellable(id: CancelId.self, cancelInFlight: true)
}
return .none
/// Stored wallet is present, database files may or may not be present, trying to initialize app state variables and environments.
/// When initialization succeeds user is taken to the home screen.
case .initializeSDK:
do {
state.storedWallet = try walletStorage.exportWallet()
guard let storedWallet = state.storedWallet else {
state.appInitializationState = .failed
// TODO [#221]: fatal error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
return .none
}
try mnemonic.isValid(storedWallet.seedPhrase)
let seedBytes = try mnemonic.toSeed(storedWallet.seedPhrase)
initializationReduce()
let birthday = state.storedWallet?.birthday ?? zcashSDKEnvironment.defaultBirthday
let initializer = try RootReducer.prepareInitializer(
for: storedWallet.seedPhrase,
birthday: birthday,
databaseFiles: databaseFiles,
derivationTool: derivationTool,
mnemonic: mnemonic,
zcashSDKEnvironment: zcashSDKEnvironment
)
try sdkSynchronizer.prepareWith(initializer: initializer, seedBytes: seedBytes)
try sdkSynchronizer.start()
return .none
} catch {
// TODO [#221]: error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
state.appInitializationState = .failed
return .none
}
case .checkBackupPhraseValidation:
guard let storedWallet = state.storedWallet else {
state.appInitializationState = .failed
// TODO [#221]: fatal error we need to handle (https://github.com/zcash/secant-ios-wallet/issues/221)
return .none
}
var landingDestination: RootReducer.State.Destination = .home
if !storedWallet.hasUserPassedPhraseBackupTest {
do {
let phraseWords = try mnemonic.asWords(storedWallet.seedPhrase)
let recoveryPhrase = RecoveryPhrase(words: phraseWords)
state.phraseDisplayState.phrase = recoveryPhrase
state.phraseValidationState = randomRecoveryPhrase.random(recoveryPhrase)
landingDestination = .phraseDisplay
} catch {
// TODO [#201]: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States
return .none
}
}
state.appInitializationState = .initialized
return Effect(value: .updateDestination(landingDestination))
.delay(for: 3, scheduler: mainQueue)
.eraseToEffect()
.cancellable(id: CancelId.self, cancelInFlight: true)
case .createNewWallet:
do {
// get the random english mnemonic
let newRandomPhrase = try mnemonic.randomMnemonic()
let birthday = try zcashSDKEnvironment.lightWalletService.latestBlockHeight()
// store the wallet to the keychain
try walletStorage.importWallet(newRandomPhrase, birthday, .english, false)
// start the backup phrase validation test
let randomRecoveryPhraseWords = try mnemonic.asWords(newRandomPhrase)
let recoveryPhrase = RecoveryPhrase(words: randomRecoveryPhraseWords)
state.phraseDisplayState.phrase = recoveryPhrase
state.phraseValidationState = randomRecoveryPhrase.random(recoveryPhrase)
return .concatenate(
Effect(value: .initializeSDK),
Effect(value: .phraseValidation(.displayBackedUpPhrase))
)
} catch {
// TODO [#201]: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States
}
return .none
case .phraseValidation(.succeed):
do {
try walletStorage.markUserPassedPhraseBackupTest()
} catch {
// TODO [#221]: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
}
return .none
case .nukeWallet:
walletStorage.nukeWallet()
do {
try databaseFiles.nukeDbFilesFor(zcashSDKEnvironment.network)
} catch {
// TODO [#221]: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
}
return .none
case .welcome(.debugMenuStartup), .home(.debugMenuStartup):
return .concatenate(
Effect.cancel(id: CancelId.self),
Effect(value: .updateDestination(.startup))
)
case .onboarding(.importWallet(.successfullyRecovered)):
return Effect(value: .updateDestination(.home))
case .onboarding(.importWallet(.initializeSDK)):
return Effect(value: .initializeSDK)
/// Default is meaningful here because there's `destinationReducer` handling destinations and this reducer is handling only actions. We don't here plenty of unused cases.
default:
return .none
}
}
destinationReduce()
}
}
@ -418,21 +148,6 @@ extension RootReducer {
throw SDKInitializationError.failed
}
}
static func process(
url: URL,
deeplink: DeeplinkClient,
derivationTool: DerivationToolClient
) async throws -> RootReducer.Action {
let deeplink = try deeplink.resolveDeeplinkURL(url, derivationTool)
switch deeplink {
case .home:
return .deeplinkHome
case let .send(amount, address, memo):
return .deeplinkSend(Zatoshi(amount), address, memo)
}
}
}
// MARK: Placeholders
@ -440,6 +155,7 @@ extension RootReducer {
extension RootReducer.State {
static var placeholder: Self {
.init(
destinationState: .placeholder,
homeState: .placeholder,
onboardingState: .init(
importWalletState: .placeholder

View File

@ -8,7 +8,7 @@ struct RootView: View {
var body: some View {
WithViewStore(store) { viewStore in
Group {
switch viewStore.destination {
switch viewStore.destinationState.destination {
case .home:
NavigationView {
HomeView(
@ -19,7 +19,7 @@ struct RootView: View {
)
}
.navigationViewStyle(.stack)
case .sandbox:
NavigationView {
SandboxView(
@ -30,7 +30,7 @@ struct RootView: View {
)
}
.navigationViewStyle(.stack)
case .onboarding:
NavigationView {
OnboardingScreen(
@ -41,13 +41,13 @@ struct RootView: View {
)
}
.navigationViewStyle(.stack)
case .startup:
ZStack(alignment: .topTrailing) {
debugView(viewStore)
.transition(.opacity)
}
case .phraseValidation:
NavigationView {
RecoveryPhraseValidationFlowView(
@ -58,7 +58,7 @@ struct RootView: View {
)
}
.navigationViewStyle(.stack)
case .phraseDisplay:
NavigationView {
RecoveryPhraseDisplayView(
@ -68,7 +68,7 @@ struct RootView: View {
)
)
}
case .welcome:
WelcomeView(
store: store.scope(
@ -78,7 +78,7 @@ struct RootView: View {
)
}
}
.onOpenURL(perform: { viewStore.send(.deeplink($0)) })
.onOpenURL(perform: { viewStore.goToDeeplink($0) })
}
}
}
@ -88,23 +88,23 @@ private extension RootView {
List {
Section(header: Text("Navigation Stack Destinations")) {
Button("Go To Sandbox (navigation proof)") {
viewStore.send(.updateDestination(.sandbox))
viewStore.goToDestination(.sandbox)
}
Button("Go To Onboarding") {
viewStore.send(.updateDestination(.onboarding))
viewStore.goToDestination(.onboarding)
}
Button("Go To Phrase Validation Demo") {
viewStore.send(.updateDestination(.phraseValidation))
viewStore.goToDestination(.phraseValidation)
}
Button("Restart the app") {
viewStore.send(.updateDestination(.welcome))
viewStore.goToDestination(.welcome)
}
Button("[Be careful] Nuke Wallet") {
viewStore.send(.nukeWallet)
viewStore.send(.initialization(.nukeWallet))
}
}
}

View File

@ -146,7 +146,9 @@ extension WalletEventsFlowViewStore {
return WalletEventsFlowReducer.Action.updateDestination(nil)
}
return WalletEventsFlowReducer.Action.updateDestination( isActive ? WalletEventsFlowReducer.State.Destination.showWalletEvent(walletEvent) : nil)
return WalletEventsFlowReducer.Action.updateDestination(
isActive ? WalletEventsFlowReducer.State.Destination.showWalletEvent(walletEvent) : nil
)
}
)
}

View File

@ -22,7 +22,7 @@ final class AppDelegate: NSObject, UIApplicationDelegate {
) -> Bool {
// set the default behavior for the NSDecimalNumber
NSDecimalNumber.defaultBehavior = Zatoshi.decimalHandler
rootViewStore.send(.appDelegate(.didFinishLaunching))
rootViewStore.send(.initialization(.appDelegate(.didFinishLaunching)))
return true
}

View File

@ -71,6 +71,7 @@ class AppInitializationTests: XCTestCase {
)
let appState = RootReducer.State(
destinationState: .placeholder,
homeState: .placeholder,
onboardingState: .init(
importWalletState: .placeholder
@ -98,23 +99,23 @@ class AppInitializationTests: XCTestCase {
}
// Root of the test, the app finished the launch process and triggers the checks and initializations.
_ = await store.send(.appDelegate(.didFinishLaunching))
_ = await store.send(.initialization(.appDelegate(.didFinishLaunching)))
// the 0.02 delay ensures keychain is ready
await testScheduler.advance(by: 0.02)
// ad 1.
await store.receive(.checkWalletInitialization)
await store.receive(.initialization(.checkWalletInitialization))
// ad 2.
await store.receive(.respondToWalletInitializationState(.initialized))
await store.receive(.initialization(.respondToWalletInitializationState(.initialized)))
// ad 3.
await store.receive(.initializeSDK) { state in
await store.receive(.initialization(.initializeSDK)) { state in
state.storedWallet = .placeholder
}
// ad 4.
await store.receive(.checkBackupPhraseValidation) { state in
await store.receive(.initialization(.checkBackupPhraseValidation)) { state in
state.appInitializationState = .initialized
}
@ -122,9 +123,9 @@ class AppInitializationTests: XCTestCase {
await testScheduler.advance(by: 3.00)
// ad 5.
await store.receive(.updateDestination(.phraseDisplay)) { state in
state.prevDestination = .welcome
state.internalDestination = .phraseDisplay
await store.receive(.destination(.updateDestination(.phraseDisplay))) { state in
state.destinationState.previousDestination = .welcome
state.destinationState.internalDestination = .phraseDisplay
}
}
@ -146,16 +147,16 @@ class AppInitializationTests: XCTestCase {
}
// Root of the test, the app finished the launch process and triggers the checks and initializations.
store.send(.appDelegate(.didFinishLaunching))
store.send(.initialization(.appDelegate(.didFinishLaunching)))
// the 0.02 delay ensures keychain is ready
testScheduler.advance(by: 0.02)
// ad 1.
store.receive(.checkWalletInitialization)
store.receive(.initialization(.checkWalletInitialization))
// ad 2.
store.receive(.respondToWalletInitializationState(.keysMissing)) { state in
store.receive(.initialization(.respondToWalletInitializationState(.keysMissing))) { state in
state.appInitializationState = .keysMissing
}
}
@ -178,22 +179,22 @@ class AppInitializationTests: XCTestCase {
}
// Root of the test, the app finished the launch process and triggers the checks and initializations.
store.send(.appDelegate(.didFinishLaunching))
store.send(.initialization(.appDelegate(.didFinishLaunching)))
// the 0.02 delay ensures keychain is ready
// the 3.0 delay ensures the welcome screen is visible till the initialization check is done
testScheduler.advance(by: 3.02)
// ad 1.
store.receive(.checkWalletInitialization)
store.receive(.initialization(.checkWalletInitialization))
// ad 2.
store.receive(.respondToWalletInitializationState(.uninitialized))
store.receive(.initialization(.respondToWalletInitializationState(.uninitialized)))
// ad 3.
store.receive(.updateDestination(.onboarding)) { state in
state.prevDestination = .welcome
state.internalDestination = .onboarding
store.receive(.destination(.updateDestination(.onboarding))) { state in
state.destinationState.previousDestination = .welcome
state.destinationState.internalDestination = .onboarding
}
}
}

View File

@ -66,12 +66,12 @@ class AppTests: XCTestCase {
$0.mainQueue = Self.testScheduler.eraseToAnyScheduler()
}
store.send(.respondToWalletInitializationState(.uninitialized))
store.send(.initialization(.respondToWalletInitializationState(.uninitialized)))
Self.testScheduler.advance(by: 3)
store.receive(.updateDestination(.onboarding)) {
$0.destination = .onboarding
store.receive(.destination(.updateDestination(.onboarding))) {
$0.destinationState.destination = .onboarding
$0.appInitializationState = .uninitialized
}
}
@ -82,7 +82,7 @@ class AppTests: XCTestCase {
reducer: RootReducer()
)
store.send(.respondToWalletInitializationState(.keysMissing)) { state in
store.send(.initialization(.respondToWalletInitializationState(.keysMissing))) { state in
state.appInitializationState = .keysMissing
}
}
@ -96,16 +96,16 @@ class AppTests: XCTestCase {
dependencies.walletStorage.exportWallet = { throw "export failed" }
}
store.send(.respondToWalletInitializationState(.filesMissing)) { state in
store.send(.initialization(.respondToWalletInitializationState(.filesMissing))) { state in
state.appInitializationState = .filesMissing
}
store.receive(.initializeSDK) { state in
store.receive(.initialization(.initializeSDK)) { state in
// failed is expected because environment is throwing errors
state.appInitializationState = .failed
}
store.receive(.checkBackupPhraseValidation)
store.receive(.initialization(.checkBackupPhraseValidation))
}
func testRespondToWalletInitializationState_Initialized() throws {
@ -117,14 +117,14 @@ class AppTests: XCTestCase {
dependencies.walletStorage.exportWallet = { throw "export failed" }
}
store.send(.respondToWalletInitializationState(.initialized))
store.send(.initialization(.respondToWalletInitializationState(.initialized)))
store.receive(.initializeSDK) { state in
store.receive(.initialization(.initializeSDK)) { state in
// failed is expected because environment is throwing errors
state.appInitializationState = .failed
}
store.receive(.checkBackupPhraseValidation)
store.receive(.initialization(.checkBackupPhraseValidation))
}
func testWalletEventReplyTo_validAddress() throws {
@ -137,7 +137,7 @@ class AppTests: XCTestCase {
store.send(.home(.walletEvents(.replyTo(address))))
if let url = URL(string: "zcash:\(address)") {
store.receive(.deeplink(url))
store.receive(.destination(.deeplink(url)))
}
}
}

View File

@ -14,21 +14,21 @@ import ZcashLightClientKit
class DeeplinkTests: XCTestCase {
func testActionDeeplinkHome_SameDestinationLevel() throws {
var appState = RootReducer.State.placeholder
appState.destination = .welcome
appState.destinationState.destination = .welcome
let store = TestStore(
initialState: appState,
reducer: RootReducer()
)
store.send(.deeplinkHome) { state in
state.destination = .home
store.send(.destination(.deeplinkHome)) { state in
state.destinationState.destination = .home
}
}
func testActionDeeplinkHome_GeetingBack() throws {
var appState = RootReducer.State.placeholder
appState.destination = .home
appState.destinationState.destination = .home
appState.homeState.destination = .send
let store = TestStore(
@ -36,15 +36,15 @@ class DeeplinkTests: XCTestCase {
reducer: RootReducer()
)
store.send(.deeplinkHome) { state in
state.destination = .home
store.send(.destination(.deeplinkHome)) { state in
state.destinationState.destination = .home
state.homeState.destination = nil
}
}
func testActionDeeplinkSend() throws {
var appState = RootReducer.State.placeholder
appState.destination = .welcome
appState.destinationState.destination = .welcome
let store = TestStore(
initialState: appState,
@ -55,8 +55,8 @@ class DeeplinkTests: XCTestCase {
let address = "address"
let memo = "testing some memo"
store.send(.deeplinkSend(amount, address, memo)) { state in
state.destination = .home
store.send(.destination(.deeplinkSend(amount, address, memo))) { state in
state.destinationState.destination = .home
state.homeState.destination = .send
state.homeState.sendState.amount = amount
state.homeState.sendState.address = address
@ -76,7 +76,7 @@ class DeeplinkTests: XCTestCase {
func testDeeplinkRequest_Received_Home() async throws {
var appState = RootReducer.State.placeholder
appState.destination = .welcome
appState.destinationState.destination = .welcome
appState.appInitializationState = .initialized
let store = TestStore(
@ -95,10 +95,10 @@ class DeeplinkTests: XCTestCase {
return XCTFail("Deeplink: 'testDeeplinkRequest_homeURL' URL is expected to be valid.")
}
_ = await store.send(.deeplink(url))
_ = await store.send(.destination(.deeplink(url)))
await store.receive(.deeplinkHome) { state in
state.destination = .home
await store.receive(.destination(.deeplinkHome)) { state in
state.destinationState.destination = .home
}
await store.finish()
@ -119,7 +119,7 @@ class DeeplinkTests: XCTestCase {
synchronizer.updateStateChanged(.synced)
var appState = RootReducer.State.placeholder
appState.destination = .welcome
appState.destinationState.destination = .welcome
appState.appInitializationState = .initialized
let store = TestStore(
@ -136,14 +136,14 @@ class DeeplinkTests: XCTestCase {
return XCTFail("Deeplink: 'testDeeplinkRequest_sendURL_amount' URL is expected to be valid.")
}
_ = await store.send(.deeplink(url))
_ = await store.send(.destination(.deeplink(url)))
let amount = Zatoshi(123_000_000)
let address = "address"
let memo = "some text"
await store.receive(.deeplinkSend(amount, address, memo)) { state in
state.destination = .home
await store.receive(.destination(.deeplinkSend(amount, address, memo))) { state in
state.destinationState.destination = .home
state.homeState.destination = .send
state.homeState.sendState.amount = amount
state.homeState.sendState.address = address