- 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:
parent
5847088ce9
commit
d11a67cee1
|
@ -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 */,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue