From d758cf4928afdc5094a1380c642cd0dfc198fb1e Mon Sep 17 00:00:00 2001 From: Lukas Korba Date: Tue, 8 Nov 2022 09:36:42 +0100 Subject: [PATCH] [#461] Migrate OnboardingFlow to ReducerProtocol (#485) - OnboardingFlow migrated to ReducerProtocol - unit and snapshot tests fixed --- secant/Features/App/AppStore.swift | 26 ++- .../OnboardingFlow/OnboardingFlowStore.swift | 211 +++++++----------- .../OnboardingFlow/OnboardingFlowView.swift | 32 ++- .../Views/OnboardingContentView.swift | 9 +- .../Views/OnboardingFooterView.swift | 11 +- .../Views/OnboardingHeaderView.swift | 7 +- .../OnboardingStoreTests.swift | 25 +-- .../OnboardingSnapshotTests.swift | 5 +- 8 files changed, 136 insertions(+), 190 deletions(-) diff --git a/secant/Features/App/AppStore.swift b/secant/Features/App/AppStore.swift index 950c7ba..88b5f1e 100644 --- a/secant/Features/App/AppStore.swift +++ b/secant/Features/App/AppStore.swift @@ -7,9 +7,14 @@ typealias AppStore = Store typealias AppViewStore = ViewStore typealias AnyRecoveryPhraseDisplayReducer = AnyReducer -typealias AnyRecoveryPhraseValidationFlowReducer = AnyReducer +typealias AnyRecoveryPhraseValidationFlowReducer = AnyReducer< + RecoveryPhraseValidationFlowReducer.State, + RecoveryPhraseValidationFlowReducer.Action, + AppEnvironment +> typealias AnyWelcomeReducer = AnyReducer typealias AnySandboxReducer = AnyReducer +typealias AnyOnboardingFlowReducer = AnyReducer // MARK: - State @@ -26,7 +31,7 @@ struct AppState: Equatable { var appInitializationState: InitializationState = .uninitialized var homeState: HomeState - var onboardingState: OnboardingFlowState + var onboardingState: OnboardingFlowReducer.State var phraseValidationState: RecoveryPhraseValidationFlowReducer.State var phraseDisplayState: RecoveryPhraseDisplayReducer.State var prevRoute: Route? @@ -57,7 +62,7 @@ enum AppAction: Equatable { case home(HomeAction) case initializeSDK case nukeWallet - case onboarding(OnboardingFlowAction) + case onboarding(OnboardingFlowReducer.Action) case phraseDisplay(RecoveryPhraseDisplayReducer.Action) case phraseValidation(RecoveryPhraseValidationFlowReducer.Action) case respondToWalletInitializationState(InitializationState) @@ -382,18 +387,15 @@ extension AppReducer { } ) - private static let onboardingReducer: AppReducer = OnboardingFlowReducer.default.pullback( + private static let onboardingReducer: AppReducer = AnyOnboardingFlowReducer { _ in + OnboardingFlowReducer() + } + .pullback( state: \AppState.onboardingState, action: /AppAction.onboarding, - environment: { environment in - OnboardingFlowEnvironment( - mnemonic: environment.mnemonic, - walletStorage: environment.walletStorage, - zcashSDKEnvironment: environment.zcashSDKEnvironment - ) - } + environment: { $0 } ) - + private static let phraseValidationReducer: AppReducer = AnyRecoveryPhraseValidationFlowReducer { _ in RecoveryPhraseValidationFlowReducer() } diff --git a/secant/Features/OnboardingFlow/OnboardingFlowStore.swift b/secant/Features/OnboardingFlow/OnboardingFlowStore.swift index 28b0ebc..f7bc38e 100644 --- a/secant/Features/OnboardingFlow/OnboardingFlowStore.swift +++ b/secant/Features/OnboardingFlow/OnboardingFlowStore.swift @@ -9,49 +9,102 @@ import Foundation import SwiftUI import ComposableArchitecture -typealias OnboardingFlowReducer = Reducer -typealias OnboardingFlowStore = Store -typealias OnboardingFlowViewStore = ViewStore +typealias OnboardingFlowStore = Store +typealias OnboardingFlowViewStore = ViewStore -typealias AnyImportWalletReducer = AnyReducer +struct OnboardingFlowReducer: ReducerProtocol { + struct State: Equatable { + enum Route: Equatable, CaseIterable { + case createNewWallet + case importExistingWallet + } + + struct Step: Equatable, Identifiable { + let id: UUID + let title: LocalizedStringKey + let description: LocalizedStringKey + let background: Image + let badge: Badge + } -// MARK: - State + var steps: IdentifiedArrayOf = Self.onboardingSteps + var index = 0 + var skippedAtindex: Int? + var route: Route? -struct OnboardingFlowState: Equatable { - enum Route: Equatable, CaseIterable { + var currentStep: Step { steps[index] } + var isFinalStep: Bool { steps.count == index + 1 } + var isInitialStep: Bool { index == 0 } + var progress: Int { ((index + 1) * 100) / (steps.count) } + var offset: CGFloat { + let maxOffset = CGFloat(-60) + let stepOffset = CGFloat(maxOffset / CGFloat(steps.count - 1)) + guard index != 0 else { return .zero } + return stepOffset * CGFloat(index) + } + + /// Import Wallet + var importWalletState: ImportWalletReducer.State + } + + enum Action: Equatable { + case next + case back + case skip + case updateRoute(OnboardingFlowReducer.State.Route?) case createNewWallet case importExistingWallet + case importWallet(ImportWalletReducer.Action) } - struct Step: Equatable, Identifiable { - let id: UUID - let title: LocalizedStringKey - let description: LocalizedStringKey - let background: Image - let badge: Badge - } + var body: some ReducerProtocol { + Scope(state: \.importWalletState, action: /Action.importWallet) { + ImportWalletReducer() + } + + Reduce { state, action in + switch action { + case .back: + guard state.index > 0 else { return .none } + if let skippedFrom = state.skippedAtindex { + state.index = skippedFrom + state.skippedAtindex = nil + } else { + state.index -= 1 + } + return .none + + case .next: + guard state.index < state.steps.count - 1 else { return .none } + state.index += 1 + return .none + + case .skip: + guard state.skippedAtindex == nil else { return .none } + state.skippedAtindex = state.index + state.index = state.steps.count - 1 + return .none + + case .updateRoute(let route): + state.route = route + return .none - var steps: IdentifiedArrayOf = Self.onboardingSteps - var index = 0 - var skippedAtindex: Int? - var route: Route? + case .createNewWallet: + state.route = .createNewWallet + return .none - var currentStep: Step { steps[index] } - var isFinalStep: Bool { steps.count == index + 1 } - var isInitialStep: Bool { index == 0 } - var progress: Int { ((index + 1) * 100) / (steps.count) } - var offset: CGFloat { - let maxOffset = CGFloat(-60) - let stepOffset = CGFloat(maxOffset / CGFloat(steps.count - 1)) - guard index != 0 else { return .zero } - return stepOffset * CGFloat(index) + case .importExistingWallet: + state.route = .importExistingWallet + return .none + + case .importWallet: + return .none + } + } } - - /// Import Wallet - var importWalletState: ImportWalletReducer.State } -extension OnboardingFlowState { +extension OnboardingFlowReducer.State { static let onboardingSteps = IdentifiedArray( uniqueElements: [ Step( @@ -86,104 +139,10 @@ extension OnboardingFlowState { ) } -// MARK: - Action - -enum OnboardingFlowAction: Equatable { - case next - case back - case skip - case updateRoute(OnboardingFlowState.Route?) - case createNewWallet - case importExistingWallet - case importWallet(ImportWalletReducer.Action) -} - -// MARK: - Environment - -struct OnboardingFlowEnvironment { - let mnemonic: WrappedMnemonic - let walletStorage: WrappedWalletStorage - let zcashSDKEnvironment: ZCashSDKEnvironment -} - -extension OnboardingFlowEnvironment { - static let live = OnboardingFlowEnvironment( - mnemonic: .live, - walletStorage: .live(), - zcashSDKEnvironment: .mainnet - ) - - static let demo = OnboardingFlowEnvironment( - mnemonic: .mock, - walletStorage: .live(), - zcashSDKEnvironment: .testnet - ) -} - -// MARK: - Reducer - -extension OnboardingFlowReducer { - static let `default` = OnboardingFlowReducer.combine( - [ - onboardingReducer, - importWalletReducer - ] - ) - - private static let onboardingReducer = OnboardingFlowReducer { state, action, _ in - switch action { - case .back: - guard state.index > 0 else { return .none } - if let skippedFrom = state.skippedAtindex { - state.index = skippedFrom - state.skippedAtindex = nil - } else { - state.index -= 1 - } - return .none - - case .next: - guard state.index < state.steps.count - 1 else { return .none } - state.index += 1 - return .none - - case .skip: - guard state.skippedAtindex == nil else { return .none } - state.skippedAtindex = state.index - state.index = state.steps.count - 1 - return .none - - case .updateRoute(let route): - state.route = route - return .none - - case .createNewWallet: - state.route = .createNewWallet - return .none - - case .importExistingWallet: - state.route = .importExistingWallet - return .none - - case .importWallet(let route): - return .none - } - } - - private static let importWalletReducer: OnboardingFlowReducer = AnyImportWalletReducer { _ in - ImportWalletReducer() - } - .pullback( - state: \OnboardingFlowState.importWalletState, - action: /OnboardingFlowAction.importWallet, - environment: { $0 } - ) -} - // MARK: - ViewStore extension OnboardingFlowViewStore { - func bindingForRoute(_ route: OnboardingFlowState.Route) -> Binding { + func bindingForRoute(_ route: OnboardingFlowReducer.State.Route) -> Binding { self.binding( get: { $0.route == route }, send: { isActive in diff --git a/secant/Features/OnboardingFlow/OnboardingFlowView.swift b/secant/Features/OnboardingFlow/OnboardingFlowView.swift index 8aea634..14288d8 100644 --- a/secant/Features/OnboardingFlow/OnboardingFlowView.swift +++ b/secant/Features/OnboardingFlow/OnboardingFlowView.swift @@ -9,7 +9,7 @@ import SwiftUI import ComposableArchitecture struct OnboardingScreen: View { - let store: Store + let store: Store var body: some View { GeometryReader { proxy in @@ -52,11 +52,10 @@ struct OnboardingScreen_Previews: PreviewProvider { static var previews: some View { OnboardingScreen( store: Store( - initialState: OnboardingFlowState( + initialState: OnboardingFlowReducer.State( importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: (.demo) + reducer: OnboardingFlowReducer() ) ) .preferredColorScheme(.light) @@ -64,11 +63,10 @@ struct OnboardingScreen_Previews: PreviewProvider { OnboardingScreen( store: Store( - initialState: OnboardingFlowState( + initialState: OnboardingFlowReducer.State( importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: (.demo) + reducer: OnboardingFlowReducer() ) ) .preferredColorScheme(.light) @@ -76,11 +74,10 @@ struct OnboardingScreen_Previews: PreviewProvider { OnboardingScreen( store: Store( - initialState: OnboardingFlowState( + initialState: OnboardingFlowReducer.State( importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: (.demo) + reducer: OnboardingFlowReducer() ) ) .preferredColorScheme(.light) @@ -88,11 +85,10 @@ struct OnboardingScreen_Previews: PreviewProvider { OnboardingScreen( store: Store( - initialState: OnboardingFlowState( + initialState: OnboardingFlowReducer.State( importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: (.demo) + reducer: OnboardingFlowReducer() ) ) .preferredColorScheme(.dark) @@ -100,11 +96,10 @@ struct OnboardingScreen_Previews: PreviewProvider { OnboardingScreen( store: Store( - initialState: OnboardingFlowState( + initialState: OnboardingFlowReducer.State( importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: (.demo) + reducer: OnboardingFlowReducer() ) ) .preferredColorScheme(.dark) @@ -112,11 +107,10 @@ struct OnboardingScreen_Previews: PreviewProvider { OnboardingScreen( store: Store( - initialState: OnboardingFlowState( + initialState: OnboardingFlowReducer.State( importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: (.demo) + reducer: OnboardingFlowReducer() ) ) .preferredColorScheme(.dark) diff --git a/secant/Features/OnboardingFlow/Views/OnboardingContentView.swift b/secant/Features/OnboardingFlow/Views/OnboardingContentView.swift index 763e7cc..a13d938 100644 --- a/secant/Features/OnboardingFlow/Views/OnboardingContentView.swift +++ b/secant/Features/OnboardingFlow/Views/OnboardingContentView.swift @@ -9,7 +9,7 @@ import SwiftUI import ComposableArchitecture struct OnboardingContentView: View { - let store: Store + let store: Store let width: Double let height: Double @@ -76,12 +76,11 @@ extension OnboardingContentView { struct OnboardingContentView_Previews: PreviewProvider { static var previews: some View { let store = Store( - initialState: OnboardingFlowState( + initialState: OnboardingFlowReducer.State( index: 0, importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: (.demo) + reducer: OnboardingFlowReducer() ) OnboardingContentView_Previews.example(store) @@ -98,7 +97,7 @@ struct OnboardingContentView_Previews: PreviewProvider { // MARK: - Previews extension OnboardingContentView_Previews { - static func example(_ store: Store) -> some View { + static func example(_ store: Store) -> some View { GeometryReader { proxy in ZStack { OnboardingHeaderView( diff --git a/secant/Features/OnboardingFlow/Views/OnboardingFooterView.swift b/secant/Features/OnboardingFlow/Views/OnboardingFooterView.swift index fbd80a5..9be6691 100644 --- a/secant/Features/OnboardingFlow/Views/OnboardingFooterView.swift +++ b/secant/Features/OnboardingFlow/Views/OnboardingFooterView.swift @@ -9,7 +9,7 @@ import SwiftUI import ComposableArchitecture struct OnboardingFooterView: View { - let store: Store + let store: Store let animationDuration: CGFloat = 0.8 var body: some View { @@ -52,7 +52,7 @@ struct OnboardingFooterView: View { ImportWalletView( store: store.scope( state: \.importWalletState, - action: OnboardingFlowAction.importWallet + action: OnboardingFlowReducer.Action.importWallet ) ) } @@ -81,13 +81,12 @@ extension View { struct OnboardingFooterView_Previews: PreviewProvider { static var previews: some View { - let store = Store( - initialState: OnboardingFlowState( + let store = Store( + initialState: OnboardingFlowReducer.State( index: 3, importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: (.demo) + reducer: OnboardingFlowReducer() ) Group { diff --git a/secant/Features/OnboardingFlow/Views/OnboardingHeaderView.swift b/secant/Features/OnboardingFlow/Views/OnboardingHeaderView.swift index 2edf97e..c032761 100644 --- a/secant/Features/OnboardingFlow/Views/OnboardingHeaderView.swift +++ b/secant/Features/OnboardingFlow/Views/OnboardingHeaderView.swift @@ -60,13 +60,12 @@ struct OnboardingHeaderView: View { struct OnboardingHeaderView_Previews: PreviewProvider { static var previews: some View { - let store = Store( - initialState: OnboardingFlowState( + let store = Store( + initialState: OnboardingFlowReducer.State( index: 0, importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: (.demo) + reducer: OnboardingFlowReducer() ) OnboardingHeaderView( diff --git a/secantTests/OnboardingTests/OnboardingStoreTests.swift b/secantTests/OnboardingTests/OnboardingStoreTests.swift index 2de21d5..65c6978 100644 --- a/secantTests/OnboardingTests/OnboardingStoreTests.swift +++ b/secantTests/OnboardingTests/OnboardingStoreTests.swift @@ -12,11 +12,10 @@ import ComposableArchitecture class OnboardingStoreTests: XCTestCase { func testIncrementingOnboarding() { let store = TestStore( - initialState: OnboardingFlowState( + initialState: OnboardingFlowReducer.State( importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: .demo + reducer: OnboardingFlowReducer() ) store.send(.next) { @@ -52,12 +51,11 @@ class OnboardingStoreTests: XCTestCase { func testIncrementingPastTotalStepsDoesNothing() { let store = TestStore( - initialState: OnboardingFlowState( + initialState: OnboardingFlowReducer.State( index: 3, importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: .demo + reducer: OnboardingFlowReducer() ) store.send(.next) @@ -66,12 +64,11 @@ class OnboardingStoreTests: XCTestCase { func testDecrementingOnboarding() { let store = TestStore( - initialState: OnboardingFlowState( + initialState: OnboardingFlowReducer.State( index: 2, importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: .demo + reducer: OnboardingFlowReducer() ) store.send(.back) { @@ -97,11 +94,10 @@ class OnboardingStoreTests: XCTestCase { func testDecrementingPastFirstStepDoesNothing() { let store = TestStore( - initialState: OnboardingFlowState( + initialState: OnboardingFlowReducer.State( importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: .demo + reducer: OnboardingFlowReducer() ) store.send(.back) @@ -112,12 +108,11 @@ class OnboardingStoreTests: XCTestCase { let initialIndex = 1 let store = TestStore( - initialState: OnboardingFlowState( + initialState: OnboardingFlowReducer.State( index: initialIndex, importWalletState: .placeholder ), - reducer: OnboardingFlowReducer.default, - environment: .demo + reducer: OnboardingFlowReducer() ) store.send(.skip) { diff --git a/secantTests/SnapshotTests/OnboardingSnapshotTests/OnboardingSnapshotTests.swift b/secantTests/SnapshotTests/OnboardingSnapshotTests/OnboardingSnapshotTests.swift index 826a3d0..844424f 100644 --- a/secantTests/SnapshotTests/OnboardingSnapshotTests/OnboardingSnapshotTests.swift +++ b/secantTests/SnapshotTests/OnboardingSnapshotTests/OnboardingSnapshotTests.swift @@ -12,9 +12,8 @@ import ComposableArchitecture class OnboardingSnapshotTests: XCTestCase { func testOnboardingFlowSnapshot() throws { let store = OnboardingFlowStore( - initialState: OnboardingFlowState(importWalletState: .placeholder), - reducer: .default, - environment: .demo + initialState: OnboardingFlowReducer.State(importWalletState: .placeholder), + reducer: OnboardingFlowReducer() ) let viewStore = ViewStore(store)