From df61f724598ec3b1ab18fc84bbc135a63ca91481 Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Sat, 5 Nov 2022 08:16:10 +0100 Subject: [PATCH] [#452] Migrate Settings to ReducerProtocol (#459) - Settings migrated to ReducerProtocol - unit and snapshot tests fixed --- .../LocalAuthenticationHandler.swift | 13 ++ secant/Features/Profile/ProfileStore.swift | 21 +- secant/Features/Settings/SettingsStore.swift | 186 ++++++++---------- secantTests/SettingsTests/SettingsTests.swift | 76 ++----- .../SettingsSnapshotTests.swift | 14 +- 5 files changed, 116 insertions(+), 194 deletions(-) diff --git a/secant/Dependencies/LocalAuthenticationHandler.swift b/secant/Dependencies/LocalAuthenticationHandler.swift index 7b9eba1..861c182 100644 --- a/secant/Dependencies/LocalAuthenticationHandler.swift +++ b/secant/Dependencies/LocalAuthenticationHandler.swift @@ -7,6 +7,7 @@ import Foundation import LocalAuthentication +import ComposableArchitecture struct LocalAuthenticationHandler { let authenticate: @Sendable () async -> Bool @@ -43,3 +44,15 @@ extension LocalAuthenticationHandler { authenticate: { false } ) } + +private enum LocalAuthenticationHandlerKey: DependencyKey { + static let liveValue = LocalAuthenticationHandler.live + static let testValue = LocalAuthenticationHandler(authenticate: { true }) +} + +extension DependencyValues { + var localAuthenticationHandler: LocalAuthenticationHandler { + get { self[LocalAuthenticationHandlerKey.self] } + set { self[LocalAuthenticationHandlerKey.self] = newValue } + } +} diff --git a/secant/Features/Profile/ProfileStore.swift b/secant/Features/Profile/ProfileStore.swift index 5c5cdcd..af47dff 100644 --- a/secant/Features/Profile/ProfileStore.swift +++ b/secant/Features/Profile/ProfileStore.swift @@ -5,6 +5,8 @@ typealias ProfileReducer = Reducer typealias ProfileViewStore = ViewStore +typealias AnySettingsReducer = AnyReducer + // MARK: - State struct ProfileState: Equatable { @@ -19,7 +21,7 @@ struct ProfileState: Equatable { var appVersion = "" var route: Route? var sdkVersion = "" - var settingsState: SettingsState + var settingsState: SettingsReducer.State } // MARK: - Action @@ -28,7 +30,7 @@ enum ProfileAction: Equatable { case addressDetails(AddressDetailsAction) case back case onAppear - case settings(SettingsAction) + case settings(SettingsReducer.Action) case updateRoute(ProfileState.Route?) } @@ -105,18 +107,13 @@ extension ProfileReducer { } ) - private static let settingsReducer: ProfileReducer = SettingsReducer.default.pullback( + private static let settingsReducer: ProfileReducer = AnySettingsReducer { _ in + SettingsReducer() + } + .pullback( state: \ProfileState.settingsState, action: /ProfileAction.settings, - environment: { environment in - SettingsEnvironment( - localAuthenticationHandler: .live, - mnemonic: environment.mnemonic, - SDKSynchronizer: environment.SDKSynchronizer, - userPreferencesStorage: .live, - walletStorage: environment.walletStorage - ) - } + environment: { $0 } ) } diff --git a/secant/Features/Settings/SettingsStore.swift b/secant/Features/Settings/SettingsStore.swift index 11109b8..42e4cc6 100644 --- a/secant/Features/Settings/SettingsStore.swift +++ b/secant/Features/Settings/SettingsStore.swift @@ -1,120 +1,97 @@ import ComposableArchitecture import SwiftUI -typealias SettingsReducer = Reducer -typealias AnySettingsReducer = AnyReducer -typealias SettingsStore = Store -typealias SettingsViewStore = ViewStore +typealias SettingsStore = Store +typealias SettingsViewStore = ViewStore -// MARK: - State +struct SettingsReducer: ReducerProtocol { + struct State: Equatable { + enum Route { + case backupPhrase + } -struct SettingsState: Equatable { - enum Route { - case backupPhrase + var phraseDisplayState: RecoveryPhraseDisplayReducer.State + var rescanDialog: ConfirmationDialogState? + var route: Route? } - var phraseDisplayState: RecoveryPhraseDisplayReducer.State - var rescanDialog: ConfirmationDialogState? - var route: Route? -} - -// MARK: - Action - -enum SettingsAction: Equatable { - case backupWallet - case backupWalletAccessRequest - case cancelRescan - case fullRescan - case phraseDisplay(RecoveryPhraseDisplayReducer.Action) - case quickRescan - case rescanBlockchain - case updateRoute(SettingsState.Route?) -} - -// MARK: - Environment - -struct SettingsEnvironment { - let localAuthenticationHandler: LocalAuthenticationHandler - let mnemonic: WrappedMnemonic - let SDKSynchronizer: WrappedSDKSynchronizer - let userPreferencesStorage: UserPreferencesStorage - let walletStorage: WrappedWalletStorage -} - -// MARK: - Reducer - -extension SettingsReducer { - static let `default` = SettingsReducer.combine( - [ - settingsReducer, - backupPhraseReducer - ] - ) + enum Action: Equatable { + case backupWallet + case backupWalletAccessRequest + case cancelRescan + case fullRescan + case phraseDisplay(RecoveryPhraseDisplayReducer.Action) + case quickRescan + case rescanBlockchain + case updateRoute(SettingsReducer.State.Route?) + } - private static let settingsReducer = SettingsReducer { state, action, environment in - switch action { - case .backupWalletAccessRequest: - return .run { send in - if await environment.localAuthenticationHandler.authenticate() { - await send(.backupWallet) + @Dependency(\.mnemonic) var mnemonic + @Dependency(\.sdkSynchronizer) var sdkSynchronizer + @Dependency(\.walletStorage) var walletStorage + @Dependency(\.localAuthenticationHandler) var localAuthenticationHandler + + var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .backupWalletAccessRequest: + return .run { send in + if await localAuthenticationHandler.authenticate() { + await send(.backupWallet) + } } - } - - case .backupWallet: - do { - let storedWallet = try environment.walletStorage.exportWallet() - let phraseWords = try environment.mnemonic.asWords(storedWallet.seedPhrase) - let recoveryPhrase = RecoveryPhrase(words: phraseWords) - state.phraseDisplayState.phrase = recoveryPhrase - return Effect(value: .updateRoute(.backupPhrase)) - } catch { - // TODO [#201]: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States + + case .backupWallet: + do { + let storedWallet = try walletStorage.exportWallet() + let phraseWords = try mnemonic.asWords(storedWallet.seedPhrase) + let recoveryPhrase = RecoveryPhrase(words: phraseWords) + state.phraseDisplayState.phrase = recoveryPhrase + return Effect(value: .updateRoute(.backupPhrase)) + } catch { + // TODO [#201]: - merge with issue 201 (https://github.com/zcash/secant-ios-wallet/issues/201) and its Error States + return .none + } + + case .cancelRescan, .quickRescan, .fullRescan: + state.rescanDialog = nil + return .none + + case .rescanBlockchain: + state.rescanDialog = .init( + title: TextState("Rescan"), + message: TextState("Select the rescan you want"), + buttons: [ + .default(TextState("Quick rescan"), action: .send(.quickRescan)), + .default(TextState("Full rescan"), action: .send(.fullRescan)), + .cancel(TextState("Cancel")) + ] + ) + return .none + + case .phraseDisplay: + state.route = nil + return .none + + case .updateRoute(let route): + state.route = route return .none } - - case .cancelRescan, .quickRescan, .fullRescan: - state.rescanDialog = nil - return .none - - case .rescanBlockchain: - state.rescanDialog = .init( - title: TextState("Rescan"), - message: TextState("Select the rescan you want"), - buttons: [ - .default(TextState("Quick rescan"), action: .send(.quickRescan)), - .default(TextState("Full rescan"), action: .send(.fullRescan)), - .cancel(TextState("Cancel")) - ] - ) - return .none - - case .phraseDisplay: - state.route = nil - return .none - - case .updateRoute(let route): - state.route = route - return .none + } + + Scope(state: \.phraseDisplayState, action: /Action.phraseDisplay) { + RecoveryPhraseDisplayReducer() } } - - private static let backupPhraseReducer: SettingsReducer = AnySettingsReducer { _ in - RecoveryPhraseDisplayReducer() - } - .pullback( - state: \SettingsState.phraseDisplayState, - action: /SettingsAction.phraseDisplay, - environment: { $0 } - ) } // MARK: - ViewStore extension SettingsViewStore { - var routeBinding: Binding { + var routeBinding: Binding { self.binding( get: \.route, - send: SettingsAction.updateRoute + send: SettingsReducer.Action.updateRoute ) } @@ -132,15 +109,15 @@ extension SettingsStore { func backupPhraseStore() -> RecoveryPhraseDisplayStore { self.scope( state: \.phraseDisplayState, - action: SettingsAction.phraseDisplay + action: SettingsReducer.Action.phraseDisplay ) } } // MARK: Placeholders -extension SettingsState { - static let placeholder = SettingsState( +extension SettingsReducer.State { + static let placeholder = SettingsReducer.State( phraseDisplayState: RecoveryPhraseDisplayReducer.State( phrase: .placeholder ) @@ -150,13 +127,6 @@ extension SettingsState { extension SettingsStore { static let placeholder = SettingsStore( initialState: .placeholder, - reducer: .default, - environment: SettingsEnvironment( - localAuthenticationHandler: .live, - mnemonic: .live, - SDKSynchronizer: MockWrappedSDKSynchronizer(), - userPreferencesStorage: .live, - walletStorage: .live() - ) + reducer: SettingsReducer() ) } diff --git a/secantTests/SettingsTests/SettingsTests.swift b/secantTests/SettingsTests/SettingsTests.swift index 92849a1..68c572f 100644 --- a/secantTests/SettingsTests/SettingsTests.swift +++ b/secantTests/SettingsTests/SettingsTests.swift @@ -44,18 +44,10 @@ class SettingsTests: XCTestCase { nukeWallet: { } ) - let testEnvironment = SettingsEnvironment( - localAuthenticationHandler: LocalAuthenticationHandler(authenticate: { true }), - mnemonic: .live, - SDKSynchronizer: TestWrappedSDKSynchronizer(), - userPreferencesStorage: .mock, - walletStorage: mockedWalletStorage - ) - let store = TestStore( - initialState: SettingsState(phraseDisplayState: RecoveryPhraseDisplayReducer.State(phrase: nil)), - reducer: SettingsReducer.default, - environment: testEnvironment + initialState: SettingsReducer.State(phraseDisplayState: RecoveryPhraseDisplayReducer.State(phrase: nil)), + reducer: SettingsReducer() + .dependency(\.walletStorage, mockedWalletStorage) ) _ = await store.send(.backupWalletAccessRequest) @@ -69,18 +61,10 @@ class SettingsTests: XCTestCase { } func testBackupWalletAccessRequest_AuthenticateFailedPath() async throws { - let testEnvironment = SettingsEnvironment( - localAuthenticationHandler: .unimplemented, - mnemonic: .mock, - SDKSynchronizer: TestWrappedSDKSynchronizer(), - userPreferencesStorage: .mock, - walletStorage: .throwing - ) - let store = TestStore( initialState: .placeholder, - reducer: SettingsReducer.default, - environment: testEnvironment + reducer: SettingsReducer() + .dependency(\.localAuthenticationHandler, .unimplemented) ) _ = await store.send(.backupWalletAccessRequest) @@ -89,18 +73,9 @@ class SettingsTests: XCTestCase { } func testRescanBlockchain() async throws { - let testEnvironment = SettingsEnvironment( - localAuthenticationHandler: .unimplemented, - mnemonic: .mock, - SDKSynchronizer: TestWrappedSDKSynchronizer(), - userPreferencesStorage: .mock, - walletStorage: .throwing - ) - let store = TestStore( initialState: .placeholder, - reducer: SettingsReducer.default, - environment: testEnvironment + reducer: SettingsReducer() ) _ = await store.send(.rescanBlockchain) { state in @@ -117,16 +92,8 @@ class SettingsTests: XCTestCase { } func testRescanBlockchain_Cancelling() async throws { - let testEnvironment = SettingsEnvironment( - localAuthenticationHandler: .unimplemented, - mnemonic: .mock, - SDKSynchronizer: TestWrappedSDKSynchronizer(), - userPreferencesStorage: .mock, - walletStorage: .throwing - ) - let store = TestStore( - initialState: SettingsState( + initialState: SettingsReducer.State( phraseDisplayState: .init(), rescanDialog: .init( title: TextState("Rescan"), @@ -139,8 +106,7 @@ class SettingsTests: XCTestCase { ), route: nil ), - reducer: SettingsReducer.default, - environment: testEnvironment + reducer: SettingsReducer() ) _ = await store.send(.cancelRescan) { state in @@ -149,16 +115,8 @@ class SettingsTests: XCTestCase { } func testRescanBlockchain_QuickRescanClearance() async throws { - let testEnvironment = SettingsEnvironment( - localAuthenticationHandler: .unimplemented, - mnemonic: .mock, - SDKSynchronizer: TestWrappedSDKSynchronizer(), - userPreferencesStorage: .mock, - walletStorage: .throwing - ) - let store = TestStore( - initialState: SettingsState( + initialState: SettingsReducer.State( phraseDisplayState: .init(), rescanDialog: .init( title: TextState("Rescan"), @@ -171,8 +129,7 @@ class SettingsTests: XCTestCase { ), route: nil ), - reducer: SettingsReducer.default, - environment: testEnvironment + reducer: SettingsReducer() ) _ = await store.send(.quickRescan) { state in @@ -181,16 +138,8 @@ class SettingsTests: XCTestCase { } func testRescanBlockchain_FullRescanClearance() async throws { - let testEnvironment = SettingsEnvironment( - localAuthenticationHandler: .unimplemented, - mnemonic: .mock, - SDKSynchronizer: TestWrappedSDKSynchronizer(), - userPreferencesStorage: .mock, - walletStorage: .throwing - ) - let store = TestStore( - initialState: SettingsState( + initialState: SettingsReducer.State( phraseDisplayState: .init(), rescanDialog: .init( title: TextState("Rescan"), @@ -203,8 +152,7 @@ class SettingsTests: XCTestCase { ), route: nil ), - reducer: SettingsReducer.default, - environment: testEnvironment + reducer: SettingsReducer() ) _ = await store.send(.fullRescan) { state in diff --git a/secantTests/SnapshotTests/SettingsSnapshotTests/SettingsSnapshotTests.swift b/secantTests/SnapshotTests/SettingsSnapshotTests/SettingsSnapshotTests.swift index aa5838e..080a552 100644 --- a/secantTests/SnapshotTests/SettingsSnapshotTests/SettingsSnapshotTests.swift +++ b/secantTests/SnapshotTests/SettingsSnapshotTests/SettingsSnapshotTests.swift @@ -12,18 +12,12 @@ import SwiftUI class SettingsSnapshotTests: XCTestCase { func testSettingsSnapshot() throws { - let testEnvironment = SettingsEnvironment( - localAuthenticationHandler: .unimplemented, - mnemonic: .mock, - SDKSynchronizer: TestWrappedSDKSynchronizer(), - userPreferencesStorage: .mock, - walletStorage: .throwing - ) - let store = Store( initialState: .placeholder, - reducer: SettingsReducer.default, - environment: testEnvironment + reducer: SettingsReducer() + .dependency(\.localAuthenticationHandler, .unimplemented) + .dependency(\.sdkSynchronizer, TestWrappedSDKSynchronizer()) + .dependency(\.walletStorage, .throwing) ) addAttachments(SettingsView(store: store))