2021-12-13 16:51:31 -08:00
|
|
|
import ComposableArchitecture
|
|
|
|
|
|
|
|
struct AppState: Equatable {
|
2021-12-13 12:50:04 -08:00
|
|
|
enum Route: Equatable {
|
2022-02-18 06:06:17 -08:00
|
|
|
case welcome
|
2021-12-13 16:51:31 -08:00
|
|
|
case startup
|
|
|
|
case onboarding
|
|
|
|
case home
|
2021-12-13 12:50:04 -08:00
|
|
|
case phraseValidation
|
|
|
|
case phraseDisplay
|
2021-12-13 16:51:31 -08:00
|
|
|
}
|
2021-12-13 12:50:04 -08:00
|
|
|
|
2021-12-13 16:51:31 -08:00
|
|
|
var homeState: HomeState
|
|
|
|
var onboardingState: OnboardingState
|
2021-12-13 12:50:04 -08:00
|
|
|
var phraseValidationState: RecoveryPhraseValidationState
|
|
|
|
var phraseDisplayState: RecoveryPhraseDisplayState
|
2022-04-04 04:23:57 -07:00
|
|
|
var welcomeState: WelcomeState
|
2022-02-18 06:06:17 -08:00
|
|
|
var route: Route = .welcome
|
2022-04-04 05:04:01 -07:00
|
|
|
var storedWallet: StoredWallet?
|
|
|
|
var appInitializationState: InitializationState = .uninitialized
|
2021-12-13 16:51:31 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
enum AppAction: Equatable {
|
2022-04-04 05:04:01 -07:00
|
|
|
case appDelegate(AppDelegateAction)
|
|
|
|
case checkWalletInitialization
|
2022-04-01 04:27:37 -07:00
|
|
|
case createNewWallet
|
2021-12-13 16:51:31 -08:00
|
|
|
case home(HomeAction)
|
2022-04-04 05:04:01 -07:00
|
|
|
case initializeApp
|
2021-12-13 16:51:31 -08:00
|
|
|
case onboarding(OnboardingAction)
|
2021-12-13 12:50:04 -08:00
|
|
|
case phraseDisplay(RecoveryPhraseDisplayAction)
|
|
|
|
case phraseValidation(RecoveryPhraseValidationAction)
|
2022-04-04 05:04:01 -07:00
|
|
|
case updateRoute(AppState.Route)
|
2022-04-04 04:23:57 -07:00
|
|
|
case welcome(WelcomeAction)
|
2021-12-13 16:51:31 -08:00
|
|
|
}
|
|
|
|
|
2022-04-01 04:27:37 -07:00
|
|
|
struct AppEnvironment {
|
2022-04-07 07:13:40 -07:00
|
|
|
let databaseFiles: DatabaseFilesInteractor
|
2022-04-04 05:04:01 -07:00
|
|
|
let scheduler: AnySchedulerOf<DispatchQueue>
|
2022-04-01 04:27:37 -07:00
|
|
|
let mnemonicSeedPhraseProvider: MnemonicSeedPhraseProvider
|
2022-03-28 04:02:17 -07:00
|
|
|
let walletStorage: WalletStorageInteractor
|
2022-04-01 04:27:37 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
extension AppEnvironment {
|
|
|
|
static let live = AppEnvironment(
|
2022-04-07 07:13:40 -07:00
|
|
|
databaseFiles: .live(),
|
2022-04-04 05:04:01 -07:00
|
|
|
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
2022-04-01 04:27:37 -07:00
|
|
|
mnemonicSeedPhraseProvider: .live,
|
2022-04-07 07:13:40 -07:00
|
|
|
walletStorage: .live()
|
2022-04-01 04:27:37 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
static let mock = AppEnvironment(
|
2022-04-07 07:13:40 -07:00
|
|
|
databaseFiles: .live(),
|
2022-04-04 05:04:01 -07:00
|
|
|
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
|
2022-04-01 04:27:37 -07:00
|
|
|
mnemonicSeedPhraseProvider: .mock,
|
2022-04-07 07:13:40 -07:00
|
|
|
walletStorage: .live()
|
2022-04-01 04:27:37 -07:00
|
|
|
)
|
|
|
|
}
|
2021-12-13 16:51:31 -08:00
|
|
|
|
|
|
|
// MARK: - AppReducer
|
|
|
|
|
2022-04-04 04:23:57 -07:00
|
|
|
private struct ListenerId: Hashable {}
|
|
|
|
|
2021-12-13 16:51:31 -08:00
|
|
|
typealias AppReducer = Reducer<AppState, AppAction, AppEnvironment>
|
|
|
|
|
|
|
|
extension AppReducer {
|
|
|
|
static let `default` = AppReducer.combine(
|
|
|
|
[
|
2022-04-01 04:27:37 -07:00
|
|
|
appReducer,
|
2021-12-13 16:51:31 -08:00
|
|
|
routeReducer,
|
|
|
|
homeReducer,
|
2021-12-13 12:50:04 -08:00
|
|
|
onboardingReducer,
|
2022-03-02 08:33:58 -08:00
|
|
|
phraseValidationReducer,
|
|
|
|
phraseDisplayReducer
|
2021-12-13 16:51:31 -08:00
|
|
|
]
|
|
|
|
)
|
2022-03-02 08:33:58 -08:00
|
|
|
.debug()
|
2021-12-13 16:51:31 -08:00
|
|
|
|
2022-04-01 04:27:37 -07:00
|
|
|
private static let appReducer = AppReducer { state, action, environment in
|
|
|
|
switch action {
|
|
|
|
case .createNewWallet:
|
|
|
|
let randomPhraseWords: [String]
|
|
|
|
do {
|
|
|
|
let randomPhrase = try environment.mnemonicSeedPhraseProvider.randomMnemonic()
|
|
|
|
randomPhraseWords = try environment.mnemonicSeedPhraseProvider.asWords(randomPhrase)
|
|
|
|
// TODO: - Get birthday from the integrated SDK, issue 228 (https://github.com/zcash/secant-ios-wallet/issues/228)
|
|
|
|
let birthday = BlockHeight(12345678)
|
|
|
|
|
2022-03-28 04:02:17 -07:00
|
|
|
try environment.walletStorage.importWallet(randomPhrase, birthday, .english, false)
|
2022-04-01 04:27:37 -07:00
|
|
|
} catch {
|
|
|
|
// TODO: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States
|
|
|
|
return .none
|
|
|
|
}
|
|
|
|
|
|
|
|
let recoveryPhrase = RecoveryPhrase(words: randomPhraseWords)
|
|
|
|
state.phraseDisplayState.phrase = recoveryPhrase
|
|
|
|
state.phraseValidationState = RecoveryPhraseValidationState.random(phrase: recoveryPhrase)
|
|
|
|
|
|
|
|
return Effect(value: .phraseValidation(.displayBackedUpPhrase))
|
|
|
|
|
2022-04-04 05:04:01 -07:00
|
|
|
/// Checking presense of stored wallet in the keychain and presense of database files in documents directory.
|
|
|
|
case .checkWalletInitialization:
|
|
|
|
do {
|
2022-04-07 07:13:40 -07:00
|
|
|
// TODO: replace the hardcoded network with the environmental value, issue 239 (https://github.com/zcash/secant-ios-wallet/issues/239)
|
|
|
|
let databaseFilesPresent = try environment.databaseFiles.areDbFilesPresentFor("mainnet")
|
2022-03-28 04:02:17 -07:00
|
|
|
let keysPresent = try environment.walletStorage.areKeysPresent()
|
|
|
|
|
2022-04-04 05:04:01 -07:00
|
|
|
switch (keysPresent, databaseFilesPresent) {
|
|
|
|
case (false, false):
|
2022-03-28 04:02:17 -07:00
|
|
|
state.appInitializationState = .uninitialized
|
2022-04-04 05:04:01 -07:00
|
|
|
case (false, true):
|
|
|
|
state.appInitializationState = .keysMissing
|
|
|
|
// TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
|
|
|
|
case (true, false), (true, true):
|
|
|
|
return Effect(value: .initializeApp)
|
|
|
|
}
|
|
|
|
} catch CocoaError.fileNoSuchFile, CocoaError.fileReadNoSuchFile {
|
2022-03-28 04:02:17 -07:00
|
|
|
state.appInitializationState = .filesMissing
|
2022-04-04 05:04:01 -07:00
|
|
|
} catch {
|
|
|
|
state.appInitializationState = .failed
|
|
|
|
// TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
|
|
|
|
}
|
2022-03-28 04:02:17 -07:00
|
|
|
|
|
|
|
if state.appInitializationState == .uninitialized || state.appInitializationState == .filesMissing {
|
|
|
|
return Effect(value: .updateRoute(.onboarding))
|
|
|
|
.delay(for: 3, scheduler: environment.scheduler)
|
|
|
|
.eraseToEffect()
|
2022-04-04 04:23:57 -07:00
|
|
|
.cancellable(id: ListenerId(), cancelInFlight: true)
|
2022-03-28 04:02:17 -07:00
|
|
|
}
|
|
|
|
|
2022-04-04 05:04:01 -07:00
|
|
|
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 .initializeApp:
|
|
|
|
do {
|
|
|
|
state.storedWallet = try environment.walletStorage.exportWallet()
|
|
|
|
} catch {
|
|
|
|
state.appInitializationState = .failed
|
|
|
|
// TODO: error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221)
|
|
|
|
return .none
|
|
|
|
}
|
|
|
|
|
|
|
|
state.appInitializationState = .initialized
|
|
|
|
return Effect(value: .updateRoute(.startup))
|
|
|
|
.delay(for: 3, scheduler: environment.scheduler)
|
|
|
|
.eraseToEffect()
|
|
|
|
|
2022-04-04 04:23:57 -07:00
|
|
|
case .welcome(.debugMenuHome):
|
|
|
|
return .concatenate(
|
|
|
|
Effect.cancel(id: ListenerId()),
|
|
|
|
Effect(value: .updateRoute(.home))
|
|
|
|
)
|
|
|
|
|
|
|
|
case .welcome(.debugMenuStartup):
|
|
|
|
return .concatenate(
|
|
|
|
Effect.cancel(id: ListenerId()),
|
|
|
|
Effect(value: .updateRoute(.startup))
|
|
|
|
)
|
|
|
|
|
2022-04-04 05:04:01 -07:00
|
|
|
/// Default is meaningful here because there's `routeReducer` handling routes and this reducer is handling only actions. We don't here plenty of unused cases.
|
2022-04-01 04:27:37 -07:00
|
|
|
default:
|
|
|
|
return .none
|
|
|
|
}
|
|
|
|
}
|
2022-04-04 05:04:01 -07:00
|
|
|
|
2021-12-13 18:16:03 -08:00
|
|
|
private static let routeReducer = AppReducer { state, action, _ in
|
2021-12-13 16:51:31 -08:00
|
|
|
switch action {
|
|
|
|
case let .updateRoute(route):
|
|
|
|
state.route = route
|
2021-12-13 12:50:04 -08:00
|
|
|
|
2021-12-13 16:20:56 -08:00
|
|
|
case .home(.reset):
|
|
|
|
state.route = .startup
|
2021-12-13 12:50:04 -08:00
|
|
|
|
2022-04-01 04:27:37 -07:00
|
|
|
case .onboarding(.createNewWallet):
|
|
|
|
return Effect(value: .createNewWallet)
|
|
|
|
|
|
|
|
case .phraseValidation(.proceedToHome):
|
2021-12-13 16:51:31 -08:00
|
|
|
state.route = .home
|
2021-12-13 12:50:04 -08:00
|
|
|
|
|
|
|
case .phraseValidation(.displayBackedUpPhrase),
|
|
|
|
.phraseDisplay(.createPhrase):
|
|
|
|
state.route = .phraseDisplay
|
|
|
|
|
|
|
|
case .phraseDisplay(.finishedPressed):
|
|
|
|
state.route = .phraseValidation
|
|
|
|
|
2022-04-04 05:04:01 -07:00
|
|
|
/// Default is meaningful here because there's `appReducer` handling actions and this reducer is handling only routes. We don't here plenty of unused cases.
|
2021-12-13 16:51:31 -08:00
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
2021-12-13 12:50:04 -08:00
|
|
|
|
2021-12-13 16:51:31 -08:00
|
|
|
return .none
|
|
|
|
}
|
|
|
|
|
|
|
|
private static let homeReducer: AppReducer = HomeReducer.default.pullback(
|
|
|
|
state: \AppState.homeState,
|
|
|
|
action: /AppAction.home,
|
|
|
|
environment: { _ in }
|
|
|
|
)
|
|
|
|
|
|
|
|
private static let onboardingReducer: AppReducer = OnboardingReducer.default.pullback(
|
|
|
|
state: \AppState.onboardingState,
|
|
|
|
action: /AppAction.onboarding,
|
2021-12-13 18:16:03 -08:00
|
|
|
environment: { _ in }
|
2021-12-13 16:51:31 -08:00
|
|
|
)
|
2021-12-13 12:50:04 -08:00
|
|
|
|
|
|
|
private static let phraseValidationReducer: AppReducer = RecoveryPhraseValidationReducer.default.pullback(
|
|
|
|
state: \AppState.phraseValidationState,
|
|
|
|
action: /AppAction.phraseValidation,
|
|
|
|
environment: { _ in BackupPhraseEnvironment.demo }
|
|
|
|
)
|
|
|
|
|
|
|
|
private static let phraseDisplayReducer: AppReducer = RecoveryPhraseDisplayReducer.default.pullback(
|
|
|
|
state: \AppState.phraseDisplayState,
|
|
|
|
action: /AppAction.phraseDisplay,
|
|
|
|
environment: { _ in BackupPhraseEnvironment.demo }
|
|
|
|
)
|
2022-04-04 04:23:57 -07:00
|
|
|
|
|
|
|
private static let welcomeReducer: AppReducer = WelcomeReducer.default.pullback(
|
|
|
|
state: \AppState.welcomeState,
|
|
|
|
action: /AppAction.welcome,
|
|
|
|
environment: { _ in }
|
|
|
|
)
|
2021-12-13 16:51:31 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - AppStore
|
|
|
|
|
|
|
|
typealias AppStore = Store<AppState, AppAction>
|
|
|
|
|
|
|
|
extension AppStore {
|
2022-04-04 05:04:01 -07:00
|
|
|
static var placeholder: AppStore {
|
|
|
|
AppStore(
|
|
|
|
initialState: .placeholder,
|
|
|
|
reducer: .default,
|
|
|
|
environment: .mock
|
|
|
|
)
|
|
|
|
}
|
2021-12-13 16:51:31 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: - AppViewStore
|
|
|
|
|
|
|
|
typealias AppViewStore = ViewStore<AppState, AppAction>
|
|
|
|
|
|
|
|
extension AppViewStore {
|
|
|
|
}
|
|
|
|
|
|
|
|
// MARK: PlaceHolders
|
|
|
|
|
|
|
|
extension AppState {
|
|
|
|
static var placeholder: Self {
|
|
|
|
.init(
|
|
|
|
homeState: .placeholder,
|
2022-03-01 00:12:50 -08:00
|
|
|
onboardingState: .init(
|
|
|
|
importWalletState: .placeholder
|
|
|
|
),
|
2021-12-13 12:50:04 -08:00
|
|
|
phraseValidationState: RecoveryPhraseValidationState.placeholder,
|
|
|
|
phraseDisplayState: RecoveryPhraseDisplayState(
|
|
|
|
phrase: .placeholder
|
2022-04-04 04:23:57 -07:00
|
|
|
),
|
|
|
|
welcomeState: .placeholder
|
2021-12-13 16:51:31 -08:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|