[#445] Migrate RecoveryPhraseValidationFlowStore to ReducerProtocol (#446)

- RecoveryPhraseValidationFlow migrated to the ReducerProtocol
- unit tests fixed
This commit is contained in:
Lukas Korba 2022-11-02 17:43:42 +01:00 committed by GitHub
parent f7be225e01
commit d44eb5ef1b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 221 additions and 211 deletions

View File

@ -144,6 +144,7 @@
9E6713F8289BC58C00A6796F /* BalanceBreakdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6713F5289BC58C00A6796F /* BalanceBreakdownView.swift */; }; 9E6713F8289BC58C00A6796F /* BalanceBreakdownView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6713F5289BC58C00A6796F /* BalanceBreakdownView.swift */; };
9E6713FA289BE0E100A6796F /* ClearBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6713F9289BE0E100A6796F /* ClearBackgroundView.swift */; }; 9E6713FA289BE0E100A6796F /* ClearBackgroundView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6713F9289BE0E100A6796F /* ClearBackgroundView.swift */; };
9E69A24D27FB002800A55317 /* WelcomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* WelcomeStore.swift */; }; 9E69A24D27FB002800A55317 /* WelcomeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E69A24C27FB002800A55317 /* WelcomeStore.swift */; };
9E6EF2CB291287BB00CA007B /* FeedbackGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E6EF2CA291287BB00CA007B /* FeedbackGenerator.swift */; };
9E7225F12889539300DF7F17 /* SettingsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */; }; 9E7225F12889539300DF7F17 /* SettingsSnapshotTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */; };
9E7225F3288AB6DD00DF7F17 /* MultipleLineTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */; }; 9E7225F3288AB6DD00DF7F17 /* MultipleLineTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */; };
9E7225F6288AC71A00DF7F17 /* MultiLineTextFieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F5288AC71A00DF7F17 /* MultiLineTextFieldStore.swift */; }; 9E7225F6288AC71A00DF7F17 /* MultiLineTextFieldStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7225F5288AC71A00DF7F17 /* MultiLineTextFieldStore.swift */; };
@ -389,6 +390,7 @@
9E6713F6289BC58C00A6796F /* BalanceBreakdownStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownStore.swift; sourceTree = "<group>"; }; 9E6713F6289BC58C00A6796F /* BalanceBreakdownStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownStore.swift; sourceTree = "<group>"; };
9E6713F9289BE0E100A6796F /* ClearBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearBackgroundView.swift; sourceTree = "<group>"; }; 9E6713F9289BE0E100A6796F /* ClearBackgroundView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearBackgroundView.swift; sourceTree = "<group>"; };
9E69A24C27FB002800A55317 /* WelcomeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStore.swift; sourceTree = "<group>"; }; 9E69A24C27FB002800A55317 /* WelcomeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WelcomeStore.swift; sourceTree = "<group>"; };
9E6EF2CA291287BB00CA007B /* FeedbackGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbackGenerator.swift; sourceTree = "<group>"; };
9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSnapshotTests.swift; sourceTree = "<group>"; }; 9E7225F02889539300DF7F17 /* SettingsSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSnapshotTests.swift; sourceTree = "<group>"; };
9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleLineTextField.swift; sourceTree = "<group>"; }; 9E7225F2288AB6DD00DF7F17 /* MultipleLineTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultipleLineTextField.swift; sourceTree = "<group>"; };
9E7225F5288AC71A00DF7F17 /* MultiLineTextFieldStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiLineTextFieldStore.swift; sourceTree = "<group>"; }; 9E7225F5288AC71A00DF7F17 /* MultiLineTextFieldStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiLineTextFieldStore.swift; sourceTree = "<group>"; };
@ -1128,6 +1130,7 @@
9E3911452848EEB90073DD9A /* ZCashSDKEnvironment.swift */, 9E3911452848EEB90073DD9A /* ZCashSDKEnvironment.swift */,
9EF1082A29114B93003D8097 /* Pasteboard.swift */, 9EF1082A29114B93003D8097 /* Pasteboard.swift */,
9EF1082C29114BCD003D8097 /* NewRecoveryPhrase.swift */, 9EF1082C29114BCD003D8097 /* NewRecoveryPhrase.swift */,
9E6EF2CA291287BB00CA007B /* FeedbackGenerator.swift */,
); );
path = Dependencies; path = Dependencies;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1671,6 +1674,7 @@
9E7CB6292875AC2D00A02233 /* AppVersionHandler.swift in Sources */, 9E7CB6292875AC2D00A02233 /* AppVersionHandler.swift in Sources */,
0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */, 0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */,
9EAB467A2861EA6A002904A0 /* TransactionRowView.swift in Sources */, 9EAB467A2861EA6A002904A0 /* TransactionRowView.swift in Sources */,
9E6EF2CB291287BB00CA007B /* FeedbackGenerator.swift in Sources */,
0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidationFlowStore.swift in Sources */, 0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidationFlowStore.swift in Sources */,
9E2DF99E27CF704D00649636 /* ImportWalletView.swift in Sources */, 9E2DF99E27CF704D00649636 /* ImportWalletView.swift in Sources */,
0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */, 0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */,

View File

@ -0,0 +1,20 @@
//
// FeedbackGenerator.swift
// secant-testnet
//
// Created by Lukáš Korba on 02.11.2022.
//
import ComposableArchitecture
private enum FeedbackGenerator: DependencyKey {
static let liveValue = WrappedFeedbackGenerator.haptic
static let testValue = WrappedFeedbackGenerator.silent
}
extension DependencyValues {
var feedbackGenerator: WrappedFeedbackGenerator {
get { self[FeedbackGenerator.self] }
set { self[FeedbackGenerator.self] = newValue }
}
}

View File

@ -8,11 +8,11 @@
import Foundation import Foundation
struct RecoveryPhraseRandomizer { struct RecoveryPhraseRandomizer {
func random(phrase: RecoveryPhrase) -> RecoveryPhraseValidationFlowState { func random(phrase: RecoveryPhrase) -> RecoveryPhraseValidationFlow.State {
let missingIndices = randomIndices() let missingIndices = randomIndices()
let missingWordChipKind = phrase.words(fromMissingIndices: missingIndices).shuffled() let missingWordChipKind = phrase.words(fromMissingIndices: missingIndices).shuffled()
return RecoveryPhraseValidationFlowState( return RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: missingWordChipKind, missingWordChips: missingWordChipKind,
@ -21,8 +21,21 @@ struct RecoveryPhraseRandomizer {
} }
func randomIndices() -> [Int] { func randomIndices() -> [Int] {
return (0..<RecoveryPhraseValidationFlowState.phraseChunks).map { _ in return (0..<RecoveryPhraseValidationFlow.State.phraseChunks).map { _ in
Int.random(in: 0 ..< RecoveryPhraseValidationFlowState.wordGroupSize) Int.random(in: 0 ..< RecoveryPhraseValidationFlow.State.wordGroupSize)
} }
} }
} }
import ComposableArchitecture
private enum RecoveryPhraseRandomKey: DependencyKey {
static let liveValue = WrappedRecoveryPhraseRandomizer.live
}
extension DependencyValues {
var randomPhrase: WrappedRecoveryPhraseRandomizer {
get { self[RecoveryPhraseRandomKey.self] }
set { self[RecoveryPhraseRandomKey.self] = newValue }
}
}

View File

@ -3,10 +3,12 @@ import ZcashLightClientKit
import Foundation import Foundation
typealias AppReducer = Reducer<AppState, AppAction, AppEnvironment> typealias AppReducer = Reducer<AppState, AppAction, AppEnvironment>
typealias AnyAppReducer = AnyReducer<RecoveryPhraseDisplay.State, RecoveryPhraseDisplay.Action, AppEnvironment>
typealias AppStore = Store<AppState, AppAction> typealias AppStore = Store<AppState, AppAction>
typealias AppViewStore = ViewStore<AppState, AppAction> typealias AppViewStore = ViewStore<AppState, AppAction>
typealias AnyRecoveryPhraseDisplayReducer = AnyReducer<RecoveryPhraseDisplay.State, RecoveryPhraseDisplay.Action, AppEnvironment>
typealias AnyRecoveryPhraseValidationFlowReducer = AnyReducer<RecoveryPhraseValidationFlow.State, RecoveryPhraseValidationFlow.Action, AppEnvironment>
// MARK: - State // MARK: - State
struct AppState: Equatable { struct AppState: Equatable {
@ -23,7 +25,7 @@ struct AppState: Equatable {
var appInitializationState: InitializationState = .uninitialized var appInitializationState: InitializationState = .uninitialized
var homeState: HomeState var homeState: HomeState
var onboardingState: OnboardingFlowState var onboardingState: OnboardingFlowState
var phraseValidationState: RecoveryPhraseValidationFlowState var phraseValidationState: RecoveryPhraseValidationFlow.State
var phraseDisplayState: RecoveryPhraseDisplay.State var phraseDisplayState: RecoveryPhraseDisplay.State
var prevRoute: Route? var prevRoute: Route?
var internalRoute: Route = .welcome var internalRoute: Route = .welcome
@ -55,7 +57,7 @@ enum AppAction: Equatable {
case nukeWallet case nukeWallet
case onboarding(OnboardingFlowAction) case onboarding(OnboardingFlowAction)
case phraseDisplay(RecoveryPhraseDisplay.Action) case phraseDisplay(RecoveryPhraseDisplay.Action)
case phraseValidation(RecoveryPhraseValidationFlowAction) case phraseValidation(RecoveryPhraseValidationFlow.Action)
case respondToWalletInitializationState(InitializationState) case respondToWalletInitializationState(InitializationState)
case sandbox(SandboxAction) case sandbox(SandboxAction)
case updateRoute(AppState.Route) case updateRoute(AppState.Route)
@ -390,20 +392,16 @@ extension AppReducer {
} }
) )
private static let phraseValidationReducer: AppReducer = RecoveryPhraseValidationFlowReducer.default.pullback( private static let phraseValidationReducer: AppReducer = AnyRecoveryPhraseValidationFlowReducer { _ in
RecoveryPhraseValidationFlow()
}
.pullback(
state: \AppState.phraseValidationState, state: \AppState.phraseValidationState,
action: /AppAction.phraseValidation, action: /AppAction.phraseValidation,
environment: { environment in environment: { $0 }
RecoveryPhraseValidationFlowEnvironment(
scheduler: environment.scheduler,
pasteboard: .test,
feedbackGenerator: .silent,
recoveryPhraseRandomizer: environment.recoveryPhraseRandomizer
)
}
) )
private static let phraseDisplayReducer: AppReducer = AnyAppReducer { _ in private static let phraseDisplayReducer: AppReducer = AnyRecoveryPhraseDisplayReducer { _ in
RecoveryPhraseDisplay() RecoveryPhraseDisplay()
} }
.pullback( .pullback(

View File

@ -9,43 +9,114 @@ import Foundation
import ComposableArchitecture import ComposableArchitecture
import SwiftUI import SwiftUI
typealias RecoveryPhraseValidationFlowReducer = Reducer< typealias RecoveryPhraseValidationFlowStore = Store<RecoveryPhraseValidationFlow.State, RecoveryPhraseValidationFlow.Action>
RecoveryPhraseValidationFlowState, typealias RecoveryPhraseValidationFlowViewStore = ViewStore<RecoveryPhraseValidationFlow.State, RecoveryPhraseValidationFlow.Action>
RecoveryPhraseValidationFlowAction,
RecoveryPhraseValidationFlowEnvironment
>
typealias RecoveryPhraseValidationFlowStore = Store<RecoveryPhraseValidationFlowState, RecoveryPhraseValidationFlowAction>
typealias RecoveryPhraseValidationFlowViewStore = ViewStore<RecoveryPhraseValidationFlowState, RecoveryPhraseValidationFlowAction>
// MARK: - State struct RecoveryPhraseValidationFlow: ReducerProtocol {
struct State: Equatable {
enum Route: Equatable, CaseIterable {
case validation
case success
case failure
}
struct RecoveryPhraseValidationFlowState: Equatable { static let wordGroupSize = 6
enum Route: Equatable, CaseIterable { static let phraseChunks = 4
case validation
case success var phrase: RecoveryPhrase
case failure var missingIndices: [Int]
var missingWordChips: [PhraseChip.Kind]
var validationWords: [ValidationWord]
var route: Route?
var isComplete: Bool {
!validationWords.isEmpty && validationWords.count == missingIndices.count
}
var isValid: Bool {
guard let resultingPhrase = self.resultingPhrase else { return false }
return resultingPhrase == phrase.words
}
} }
static let wordGroupSize = 6
static let phraseChunks = 4
var phrase: RecoveryPhrase
var missingIndices: [Int]
var missingWordChips: [PhraseChip.Kind]
var validationWords: [ValidationWord]
var route: Route?
var isComplete: Bool { @Dependency(\.randomPhrase) var randomPhrase
!validationWords.isEmpty && validationWords.count == missingIndices.count @Dependency(\.mainQueue) var mainQueue
} @Dependency(\.pasteboard) var pasteboard
@Dependency(\.feedbackGenerator) var feedbackGenerator
var isValid: Bool { enum Action: Equatable {
guard let resultingPhrase = self.resultingPhrase else { return false } case updateRoute(RecoveryPhraseValidationFlow.State.Route?)
return resultingPhrase == phrase.words case reset
case move(wordChip: PhraseChip.Kind, intoGroup: Int)
case succeed
case fail
case failureFeedback
case proceedToHome
case displayBackedUpPhrase
}
// swiftlint:disable:next cyclomatic_complexity
func reduce(into state: inout State, action: Action) -> ComposableArchitecture.EffectTask<Action> {
switch action {
case .reset:
state = randomPhrase.random(state.phrase)
state.route = .validation
// FIXME [#186]: Resetting causes route to be nil = preamble screen, hence setting the .validation. The transition back is not animated
// though
case let .move(wordChip, group):
guard
case let PhraseChip.Kind.unassigned(word, _) = wordChip,
let missingChipIndex = state.missingWordChips.firstIndex(of: wordChip)
else { return .none }
state.missingWordChips[missingChipIndex] = .empty
state.validationWords.append(ValidationWord(groupIndex: group, word: word))
if state.isComplete {
let value: RecoveryPhraseValidationFlow.Action = state.isValid ? .succeed : .fail
let effect = Effect<RecoveryPhraseValidationFlow.Action, Never>(value: value)
.delay(for: 1, scheduler: mainQueue)
.eraseToEffect()
if value == .succeed {
return effect
} else {
return .concatenate(
Effect(value: .failureFeedback),
effect
)
}
}
return .none
case .succeed:
state.route = .success
case .fail:
state.route = .failure
case .failureFeedback:
feedbackGenerator.generateErrorFeedback()
case .updateRoute(let route):
guard let route = route else {
state = randomPhrase.random(state.phrase)
return .none
}
state.route = route
case .proceedToHome:
break
case .displayBackedUpPhrase:
break
}
return .none
} }
} }
extension RecoveryPhraseValidationFlowState { extension RecoveryPhraseValidationFlow.State {
/// Given an array of RecoveryPhraseStepCompletion, missing indices, original phrase and the number of groups it was split into, /// Given an array of RecoveryPhraseStepCompletion, missing indices, original phrase and the number of groups it was split into,
/// assembly the resulting phrase. This comes up with the "proposed solution" for the recovery phrase validation challenge. /// assembly the resulting phrase. This comes up with the "proposed solution" for the recovery phrase validation challenge.
/// - returns:an array of String containing the recovery phrase words ordered by the original phrase order, or `nil` /// - returns:an array of String containing the recovery phrase words ordered by the original phrase order, or `nil`
@ -95,111 +166,10 @@ extension RecoveryPhrase.Group {
} }
} }
// MARK: - Action
enum RecoveryPhraseValidationFlowAction: Equatable {
case updateRoute(RecoveryPhraseValidationFlowState.Route?)
case reset
case move(wordChip: PhraseChip.Kind, intoGroup: Int)
case succeed
case fail
case failureFeedback
case proceedToHome
case displayBackedUpPhrase
}
// MARK: - Environment
struct RecoveryPhraseValidationFlowEnvironment {
let scheduler: AnySchedulerOf<DispatchQueue>
let pasteboard: WrappedPasteboard
let feedbackGenerator: WrappedFeedbackGenerator
let recoveryPhraseRandomizer: WrappedRecoveryPhraseRandomizer
}
extension RecoveryPhraseValidationFlowEnvironment {
static let demo = Self(
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
pasteboard: .test,
feedbackGenerator: .silent,
recoveryPhraseRandomizer: .live
)
static let live = Self(
scheduler: DispatchQueue.main.eraseToAnyScheduler(),
pasteboard: .live,
feedbackGenerator: .haptic,
recoveryPhraseRandomizer: .live
)
}
// MARK: - Reducer
extension RecoveryPhraseValidationFlowReducer {
static let `default` = RecoveryPhraseValidationFlowReducer { state, action, environment in
switch action {
case .reset:
state = environment.recoveryPhraseRandomizer.random(state.phrase)
state.route = .validation
// FIXME [#186]: Resetting causes route to be nil = preamble screen, hence setting the .validation. The transition back is not animated
// though
case let .move(wordChip, group):
guard
case let PhraseChip.Kind.unassigned(word, color) = wordChip,
let missingChipIndex = state.missingWordChips.firstIndex(of: wordChip)
else { return .none }
state.missingWordChips[missingChipIndex] = .empty
state.validationWords.append(ValidationWord(groupIndex: group, word: word))
if state.isComplete {
let value: RecoveryPhraseValidationFlowAction = state.isValid ? .succeed : .fail
let effect = Effect<RecoveryPhraseValidationFlowAction, Never>(value: value)
.delay(for: 1, scheduler: environment.scheduler)
.eraseToEffect()
if value == .succeed {
return effect
} else {
return .concatenate(
Effect(value: .failureFeedback),
effect
)
}
}
return .none
case .succeed:
state.route = .success
case .fail:
state.route = .failure
case .failureFeedback:
environment.feedbackGenerator.generateErrorFeedback()
case .updateRoute(let route):
guard let route = route else {
state = environment.recoveryPhraseRandomizer.random(state.phrase)
return .none
}
state.route = route
case .proceedToHome:
break
case .displayBackedUpPhrase:
break
}
return .none
}
}
// MARK: - ViewStore // MARK: - ViewStore
extension RecoveryPhraseValidationFlowViewStore { extension RecoveryPhraseValidationFlowViewStore {
func bindingForRoute(_ route: RecoveryPhraseValidationFlowState.Route) -> Binding<Bool> { func bindingForRoute(_ route: RecoveryPhraseValidationFlow.State.Route) -> Binding<Bool> {
self.binding( self.binding(
get: { $0.route == route }, get: { $0.route == route },
send: { isActive in send: { isActive in
@ -240,8 +210,8 @@ extension RecoveryPhraseValidationFlowViewStore {
// MARK: - Placeholders // MARK: - Placeholders
extension RecoveryPhraseValidationFlowState { extension RecoveryPhraseValidationFlow.State {
static let placeholder = RecoveryPhraseValidationFlowState( static let placeholder = RecoveryPhraseValidationFlow.State(
phrase: .placeholder, phrase: .placeholder,
missingIndices: [2, 0, 3, 5], missingIndices: [2, 0, 3, 5],
missingWordChips: [ missingWordChips: [
@ -254,7 +224,7 @@ extension RecoveryPhraseValidationFlowState {
route: nil route: nil
) )
static let placeholderStep1 = RecoveryPhraseValidationFlowState( static let placeholderStep1 = RecoveryPhraseValidationFlow.State(
phrase: .placeholder, phrase: .placeholder,
missingIndices: [2, 0, 3, 5], missingIndices: [2, 0, 3, 5],
missingWordChips: [ missingWordChips: [
@ -269,7 +239,7 @@ extension RecoveryPhraseValidationFlowState {
route: nil route: nil
) )
static let placeholderStep2 = RecoveryPhraseValidationFlowState( static let placeholderStep2 = RecoveryPhraseValidationFlow.State(
phrase: .placeholder, phrase: .placeholder,
missingIndices: [2, 0, 3, 5], missingIndices: [2, 0, 3, 5],
missingWordChips: [ missingWordChips: [
@ -285,7 +255,7 @@ extension RecoveryPhraseValidationFlowState {
route: nil route: nil
) )
static let placeholderStep3 = RecoveryPhraseValidationFlowState( static let placeholderStep3 = RecoveryPhraseValidationFlow.State(
phrase: .placeholder, phrase: .placeholder,
missingIndices: [2, 0, 3, 5], missingIndices: [2, 0, 3, 5],
missingWordChips: [ missingWordChips: [
@ -302,7 +272,7 @@ extension RecoveryPhraseValidationFlowState {
route: nil route: nil
) )
static let placeholderStep4 = RecoveryPhraseValidationFlowState( static let placeholderStep4 = RecoveryPhraseValidationFlow.State(
phrase: .placeholder, phrase: .placeholder,
missingIndices: [2, 0, 3, 5], missingIndices: [2, 0, 3, 5],
missingWordChips: [ missingWordChips: [
@ -324,31 +294,26 @@ extension RecoveryPhraseValidationFlowState {
extension RecoveryPhraseValidationFlowStore { extension RecoveryPhraseValidationFlowStore {
static let demo = Store( static let demo = Store(
initialState: .placeholder, initialState: .placeholder,
reducer: .default, reducer: RecoveryPhraseValidationFlow()
environment: .demo
) )
static let demoStep1 = Store( static let demoStep1 = Store(
initialState: .placeholderStep1, initialState: .placeholderStep1,
reducer: .default, reducer: RecoveryPhraseValidationFlow()
environment: .demo
) )
static let demoStep2 = Store( static let demoStep2 = Store(
initialState: .placeholderStep1, initialState: .placeholderStep1,
reducer: .default, reducer: RecoveryPhraseValidationFlow()
environment: .demo
) )
static let demoStep3 = Store( static let demoStep3 = Store(
initialState: .placeholderStep3, initialState: .placeholderStep3,
reducer: .default, reducer: RecoveryPhraseValidationFlow()
environment: .demo
) )
static let demoStep4 = Store( static let demoStep4 = Store(
initialState: .placeholderStep4, initialState: .placeholderStep4,
reducer: .default, reducer: RecoveryPhraseValidationFlow()
environment: .demo
) )
} }

View File

@ -83,7 +83,7 @@ private extension RecoveryPhraseBackupView {
.padding(.horizontal, 30) .padding(.horizontal, 30)
} }
@ViewBuilder func completeHeader(for state: RecoveryPhraseValidationFlowState) -> some View { @ViewBuilder func completeHeader(for state: RecoveryPhraseValidationFlow.State) -> some View {
if state.isValid { if state.isValid {
Text("recoveryPhraseBackupValidation.successResult") Text("recoveryPhraseBackupValidation.successResult")
.bodyText() .bodyText()
@ -94,7 +94,7 @@ private extension RecoveryPhraseBackupView {
} }
} }
private extension RecoveryPhraseValidationFlowState { private extension RecoveryPhraseValidationFlow.State {
@ViewBuilder func missingWordGrid() -> some View { @ViewBuilder func missingWordGrid() -> some View {
let columns = Array( let columns = Array(
repeating: GridItem(.flexible(minimum: 100, maximum: 120), spacing: 20), repeating: GridItem(.flexible(minimum: 100, maximum: 120), spacing: 20),
@ -116,7 +116,7 @@ private extension RecoveryPhraseValidationFlowState {
} }
} }
extension RecoveryPhraseValidationFlowState { extension RecoveryPhraseValidationFlow.State {
func wordsChips( func wordsChips(
for groupIndex: Int, for groupIndex: Int,
groupSize: Int, groupSize: Int,
@ -140,14 +140,14 @@ extension RecoveryPhraseValidationFlowState {
private extension WordChipGrid { private extension WordChipGrid {
init( init(
state: RecoveryPhraseValidationFlowState, state: RecoveryPhraseValidationFlow.State,
groupIndex: Int, groupIndex: Int,
wordGroup: RecoveryPhrase.Group, wordGroup: RecoveryPhrase.Group,
misingIndex: Int misingIndex: Int
) { ) {
let chips = state.wordsChips( let chips = state.wordsChips(
for: groupIndex, for: groupIndex,
groupSize: RecoveryPhraseValidationFlowState.wordGroupSize, groupSize: RecoveryPhraseValidationFlow.State.wordGroupSize,
from: wordGroup from: wordGroup
) )
@ -155,7 +155,7 @@ private extension WordChipGrid {
} }
} }
private extension RecoveryPhraseValidationFlowState { private extension RecoveryPhraseValidationFlow.State {
var coloredChipColor: Color { var coloredChipColor: Color {
if self.isComplete { if self.isComplete {
return isValid ? Asset.Colors.Buttons.activeButton.color : Asset.Colors.BackgroundColors.red.color return isValid ? Asset.Colors.Buttons.activeButton.color : Asset.Colors.BackgroundColors.red.color

View File

@ -36,7 +36,7 @@ struct WordChipDropDelegate: DropDelegate {
} }
} }
extension RecoveryPhraseValidationFlowState { extension RecoveryPhraseValidationFlow.State {
func groupCompleted(index: Int) -> Bool { func groupCompleted(index: Int) -> Bool {
validationWords.first(where: { $0.groupIndex == index }) != nil validationWords.first(where: { $0.groupIndex == index }) != nil
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
struct WrappedRecoveryPhraseRandomizer { struct WrappedRecoveryPhraseRandomizer {
let random: (RecoveryPhrase) -> RecoveryPhraseValidationFlowState let random: (RecoveryPhrase) -> RecoveryPhraseValidationFlow.State
} }
extension WrappedRecoveryPhraseRandomizer { extension WrappedRecoveryPhraseRandomizer {

View File

@ -33,7 +33,7 @@ class AppInitializationTests: XCTestCase {
let recoveryPhrase = RecoveryPhrase(words: try WrappedMnemonic.mock.randomMnemonicWords()) let recoveryPhrase = RecoveryPhrase(words: try WrappedMnemonic.mock.randomMnemonicWords())
let phraseValidationState = RecoveryPhraseValidationFlowState( let phraseValidationState = RecoveryPhraseValidationFlow.State(
phrase: recoveryPhrase, phrase: recoveryPhrase,
missingIndices: [2, 0, 3, 5], missingIndices: [2, 0, 3, 5],
missingWordChips: [ missingWordChips: [
@ -67,7 +67,7 @@ class AppInitializationTests: XCTestCase {
) )
] ]
return RecoveryPhraseValidationFlowState( return RecoveryPhraseValidationFlow.State(
phrase: recoveryPhrase, phrase: recoveryPhrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: missingWordChipKind, missingWordChips: missingWordChipKind,

View File

@ -12,13 +12,6 @@ import ComposableArchitecture
class RecoveryPhraseValidationTests: XCTestCase { class RecoveryPhraseValidationTests: XCTestCase {
static let testScheduler = DispatchQueue.test static let testScheduler = DispatchQueue.test
let testEnvironment = RecoveryPhraseValidationFlowEnvironment(
scheduler: testScheduler.eraseToAnyScheduler(),
pasteboard: .test,
feedbackGenerator: .silent,
recoveryPhraseRandomizer: .live
)
func testPickWordsFromMissingIndices() throws { func testPickWordsFromMissingIndices() throws {
let words = [ let words = [
"bring", "salute", "thank", "bring", "salute", "thank",
@ -66,14 +59,17 @@ class RecoveryPhraseValidationTests: XCTestCase {
let missingWordChips: [PhraseChip.Kind] = ["salute", "boil", "cancel", "pizza"].map({ PhraseChip.Kind.unassigned(word: $0) }) let missingWordChips: [PhraseChip.Kind] = ["salute", "boil", "cancel", "pizza"].map({ PhraseChip.Kind.unassigned(word: $0) })
let initialStep = RecoveryPhraseValidationFlowState( let initialStep = RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: missingWordChips, missingWordChips: missingWordChips,
validationWords: [] validationWords: []
) )
let store = TestStore(initialState: initialStep, reducer: RecoveryPhraseValidationFlowReducer.default, environment: testEnvironment) let store = TestStore(
initialState: initialStep,
reducer: RecoveryPhraseValidationFlow()
)
let expectedMissingChips = [ let expectedMissingChips = [
PhraseChip.Kind.empty, PhraseChip.Kind.empty,
@ -113,13 +109,16 @@ class RecoveryPhraseValidationTests: XCTestCase {
let missingWordChips = ["salute", "boil", "cancel", "pizza"].map({ PhraseChip.Kind.unassigned(word: $0) }) let missingWordChips = ["salute", "boil", "cancel", "pizza"].map({ PhraseChip.Kind.unassigned(word: $0) })
let initialStep = RecoveryPhraseValidationFlowState.initial( let initialStep = RecoveryPhraseValidationFlow.State.initial(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordsChips: missingWordChips missingWordsChips: missingWordChips
) )
let store = TestStore(initialState: initialStep, reducer: RecoveryPhraseValidationFlowReducer.default, environment: testEnvironment) let store = TestStore(
initialState: initialStep,
reducer: RecoveryPhraseValidationFlow()
)
let expectedMissingChips = [ let expectedMissingChips = [
PhraseChip.Kind.unassigned(word: "salute"), PhraseChip.Kind.unassigned(word: "salute"),
@ -157,7 +156,7 @@ class RecoveryPhraseValidationTests: XCTestCase {
let phrase = RecoveryPhrase(words: words) let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationFlowState( let currentStep = RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: [ missingWordChips: [
@ -169,7 +168,10 @@ class RecoveryPhraseValidationTests: XCTestCase {
validationWords: [ValidationWord(groupIndex: 0, word: "salute")] validationWords: [ValidationWord(groupIndex: 0, word: "salute")]
) )
let store = TestStore(initialState: currentStep, reducer: RecoveryPhraseValidationFlowReducer.default, environment: testEnvironment) let store = TestStore(
initialState: currentStep,
reducer: RecoveryPhraseValidationFlow()
)
let expectedMissingWordChips = [ let expectedMissingWordChips = [
PhraseChip.Kind.empty, PhraseChip.Kind.empty,
@ -210,7 +212,7 @@ class RecoveryPhraseValidationTests: XCTestCase {
let phrase = RecoveryPhrase(words: words) let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationFlowState( let currentStep = RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: [ missingWordChips: [
@ -225,7 +227,10 @@ class RecoveryPhraseValidationTests: XCTestCase {
] ]
) )
let store = TestStore(initialState: currentStep, reducer: RecoveryPhraseValidationFlowReducer.default, environment: testEnvironment) let store = TestStore(
initialState: currentStep,
reducer: RecoveryPhraseValidationFlow()
)
let expectedMissingWordChips = [ let expectedMissingWordChips = [
PhraseChip.Kind.empty, PhraseChip.Kind.empty,
@ -267,7 +272,7 @@ class RecoveryPhraseValidationTests: XCTestCase {
let phrase = RecoveryPhrase(words: words) let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationFlowState( let currentStep = RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: [ missingWordChips: [
@ -283,7 +288,11 @@ class RecoveryPhraseValidationTests: XCTestCase {
] ]
) )
let store = TestStore(initialState: currentStep, reducer: RecoveryPhraseValidationFlowReducer.default, environment: testEnvironment) let store = TestStore(
initialState: currentStep,
reducer: RecoveryPhraseValidationFlow()
.dependency(\.mainQueue, RecoveryPhraseValidationTests.testScheduler.eraseToAnyScheduler())
)
let expectedMissingWordChips = [ let expectedMissingWordChips = [
PhraseChip.Kind.empty, PhraseChip.Kind.empty,
@ -334,7 +343,7 @@ class RecoveryPhraseValidationTests: XCTestCase {
let phrase = RecoveryPhrase(words: words) let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationFlowState( let currentStep = RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: [ missingWordChips: [
@ -350,7 +359,11 @@ class RecoveryPhraseValidationTests: XCTestCase {
] ]
) )
let store = TestStore(initialState: currentStep, reducer: RecoveryPhraseValidationFlowReducer.default, environment: testEnvironment) let store = TestStore(
initialState: currentStep,
reducer: RecoveryPhraseValidationFlow()
.dependency(\.mainQueue, RecoveryPhraseValidationTests.testScheduler.eraseToAnyScheduler())
)
let expectedMissingWordChips = [ let expectedMissingWordChips = [
PhraseChip.Kind.empty, PhraseChip.Kind.empty,
@ -402,7 +415,7 @@ class RecoveryPhraseValidationTests: XCTestCase {
let phrase = RecoveryPhrase(words: words) let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationFlowState( let currentStep = RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: [ missingWordChips: [
@ -454,7 +467,7 @@ class RecoveryPhraseValidationTests: XCTestCase {
let phrase = RecoveryPhrase(words: words) let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationFlowState( let currentStep = RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: [ missingWordChips: [
@ -507,7 +520,7 @@ class RecoveryPhraseValidationTests: XCTestCase {
let phrase = RecoveryPhrase(words: words) let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationFlowState( let currentStep = RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: [ missingWordChips: [
@ -545,7 +558,7 @@ class RecoveryPhraseValidationTests: XCTestCase {
let phrase = RecoveryPhrase(words: words) let phrase = RecoveryPhrase(words: words)
let currentStep = RecoveryPhraseValidationFlowState( let currentStep = RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: [ missingWordChips: [
@ -591,7 +604,7 @@ class RecoveryPhraseValidationTests: XCTestCase {
ValidationWord(groupIndex: 3, word: "pizza") ValidationWord(groupIndex: 3, word: "pizza")
] ]
let result = RecoveryPhraseValidationFlowState( let result = RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: phrase.words(fromMissingIndices: missingIndices), missingWordChips: phrase.words(fromMissingIndices: missingIndices),
@ -630,7 +643,7 @@ class RecoveryPhraseValidationTests: XCTestCase {
ValidationWord(groupIndex: 2, word: "pizza") ValidationWord(groupIndex: 2, word: "pizza")
] ]
let result = RecoveryPhraseValidationFlowState( let result = RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: phrase.words(fromMissingIndices: missingIndices), missingWordChips: phrase.words(fromMissingIndices: missingIndices),
@ -644,13 +657,13 @@ class RecoveryPhraseValidationTests: XCTestCase {
} }
} }
extension RecoveryPhraseValidationFlowState { extension RecoveryPhraseValidationFlow.State {
static func initial( static func initial(
phrase: RecoveryPhrase, phrase: RecoveryPhrase,
missingIndices: [Int], missingIndices: [Int],
missingWordsChips: [PhraseChip.Kind] missingWordsChips: [PhraseChip.Kind]
) -> Self { ) -> Self {
RecoveryPhraseValidationFlowState( RecoveryPhraseValidationFlow.State(
phrase: phrase, phrase: phrase,
missingIndices: missingIndices, missingIndices: missingIndices,
missingWordChips: missingWordsChips, missingWordChips: missingWordsChips,

View File

@ -14,8 +14,7 @@ class RecoveryPhraseValidationFlowSnapshotTests: XCTestCase {
func testRecoveryPhraseValidationFlowPreambleSnapshot() throws { func testRecoveryPhraseValidationFlowPreambleSnapshot() throws {
let store = RecoveryPhraseValidationFlowStore( let store = RecoveryPhraseValidationFlowStore(
initialState: .placeholder, initialState: .placeholder,
reducer: .default, reducer: RecoveryPhraseValidationFlow()
environment: .demo
) )
addAttachments(RecoveryPhraseValidationFlowView(store: store)) addAttachments(RecoveryPhraseValidationFlowView(store: store))
@ -24,8 +23,8 @@ class RecoveryPhraseValidationFlowSnapshotTests: XCTestCase {
func testRecoveryPhraseValidationFlowBackupSnapshot() throws { func testRecoveryPhraseValidationFlowBackupSnapshot() throws {
let store = RecoveryPhraseValidationFlowStore( let store = RecoveryPhraseValidationFlowStore(
initialState: .placeholder, initialState: .placeholder,
reducer: .default, reducer: RecoveryPhraseValidationFlow()
environment: .demo .dependency(\.mainQueue, RecoveryPhraseValidationTests.testScheduler.eraseToAnyScheduler())
) )
let viewStore = ViewStore(store) let viewStore = ViewStore(store)
@ -67,8 +66,7 @@ class RecoveryPhraseValidationFlowSnapshotTests: XCTestCase {
func testRecoveryPhraseValidationFlowSucceededSnapshot() throws { func testRecoveryPhraseValidationFlowSucceededSnapshot() throws {
let store = RecoveryPhraseValidationFlowStore( let store = RecoveryPhraseValidationFlowStore(
initialState: .placeholder, initialState: .placeholder,
reducer: .default, reducer: RecoveryPhraseValidationFlow()
environment: .demo
) )
addAttachments(RecoveryPhraseBackupSucceededView(store: store)) addAttachments(RecoveryPhraseBackupSucceededView(store: store))
@ -77,8 +75,7 @@ class RecoveryPhraseValidationFlowSnapshotTests: XCTestCase {
func testRecoveryPhraseValidationFlowFailedSnapshot() throws { func testRecoveryPhraseValidationFlowFailedSnapshot() throws {
let store = RecoveryPhraseValidationFlowStore( let store = RecoveryPhraseValidationFlowStore(
initialState: .placeholder, initialState: .placeholder,
reducer: .default, reducer: RecoveryPhraseValidationFlow()
environment: .demo
) )
addAttachments(RecoveryPhraseBackupFailedView(store: store)) addAttachments(RecoveryPhraseBackupFailedView(store: store))