diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 14221f4..bc12b25 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -446,6 +446,7 @@ 9E7FE0EC282E7D9400C374E8 /* TransactionState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E5BF63B2818305D00BA3F17 /* TransactionState.swift */; }; 9E7FE0F628327F6F00C374E8 /* ScanUIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F528327F6F00C374E8 /* ScanUIView.swift */; }; 9E7FE0F92832824C00C374E8 /* QRCodeScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */; }; + 9E852D5C29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E852D5B29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift */; }; 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 */; }; @@ -768,6 +769,7 @@ 9E7FE0E5282E7B1100C374E8 /* StoredWallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoredWallet.swift; sourceTree = ""; }; 9E7FE0F528327F6F00C374E8 /* ScanUIView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanUIView.swift; sourceTree = ""; }; 9E7FE0F82832824C00C374E8 /* QRCodeScanView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRCodeScanView.swift; sourceTree = ""; }; + 9E852D5B29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecoveryPhraseValidationFlowFeatureFlagTests.swift; sourceTree = ""; }; 9E92AF0728530EBF007367AD /* View+UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+UIImage.swift"; sourceTree = ""; }; 9E94C61F28AA7DEE008256E9 /* BalanceBreakdownTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownTests.swift; sourceTree = ""; }; 9E94C62228AA7EE0008256E9 /* BalanceBreakdownSnapshotTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceBreakdownSnapshotTests.swift; sourceTree = ""; }; @@ -1091,7 +1093,6 @@ children = ( 9EF8135F27F043CC0075AF48 /* AppDelegate.swift */, 0D6D628A276A528D002FB4CC /* DropDelegate.swift */, - 34F682E429A75EB60022C079 /* WalletConfig.swift */, 9EF8139B27F47AED0075AF48 /* InitializationState.swift */, 9E7FE0D6282D286500C374E8 /* RecoveryPhrase.swift */, 9E612C7C2991476F00D09B09 /* SensitiveData.swift */, @@ -1099,6 +1100,7 @@ 9E66122B2877188700C75B70 /* SyncStatusSnapshot.swift */, 9E5BF63B2818305D00BA3F17 /* TransactionState.swift */, 9E7FE0DC282D298900C374E8 /* ValidationWord.swift */, + 34F682E429A75EB60022C079 /* WalletConfig.swift */, 9EAB46772860A1D2002904A0 /* WalletEvent.swift */, ); path = Models; @@ -1145,6 +1147,7 @@ isa = PBXGroup; children = ( 0DFE93E5272CB6F7000FCCA5 /* RecoveryPhraseValidationTests.swift */, + 9E852D5B29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift */, ); path = RecoveryPhraseValidationTests; sourceTree = ""; @@ -3052,6 +3055,7 @@ 9E9ECC9828589E150099D5A2 /* WelcomeSnapshotTests.swift in Sources */, 9E7CB6122869882D00A02233 /* WalletEventsSnapshotTests.swift in Sources */, 9E5BF644281FEC9900BA3F17 /* SendTests.swift in Sources */, + 9E852D5C29AF8EB200CF4AC1 /* RecoveryPhraseValidationFlowFeatureFlagTests.swift in Sources */, 0D1C1AA327611EFD0004AF6A /* RecoveryPhraseDisplayReducerTests.swift in Sources */, 9E9ECC9A28589E150099D5A2 /* RecoveryPhraseValidationFlowSnapshotTests.swift in Sources */, 9E0F574B2980260D005304FA /* LoggerTests.swift in Sources */, diff --git a/secant/Dependencies/RecoveryPhraseRandomizer/RecoveryPhraseRandomizerInterface.swift b/secant/Dependencies/RecoveryPhraseRandomizer/RecoveryPhraseRandomizerInterface.swift index ce45ca1..07776fa 100644 --- a/secant/Dependencies/RecoveryPhraseRandomizer/RecoveryPhraseRandomizerInterface.swift +++ b/secant/Dependencies/RecoveryPhraseRandomizer/RecoveryPhraseRandomizerInterface.swift @@ -15,5 +15,5 @@ extension DependencyValues { } struct RecoveryPhraseRandomizerClient { - let random: (RecoveryPhrase) -> RecoveryPhraseValidationFlowReducer.State + var random: (RecoveryPhrase) -> RecoveryPhraseValidationFlowReducer.State } diff --git a/secant/Dependencies/WalletStorage/WalletStorage.swift b/secant/Dependencies/WalletStorage/WalletStorage.swift index 3b7fac3..b788ebd 100644 --- a/secant/Dependencies/WalletStorage/WalletStorage.swift +++ b/secant/Dependencies/WalletStorage/WalletStorage.swift @@ -116,10 +116,10 @@ struct WalletStorage { } } - func markUserPassedPhraseBackupTest() throws { + func markUserPassedPhraseBackupTest(_ flag: Bool = true) throws { do { var wallet = try exportWallet() - wallet.hasUserPassedPhraseBackupTest = true + wallet.hasUserPassedPhraseBackupTest = flag guard let data = try encode(object: wallet) else { throw KeychainError.encoding diff --git a/secant/Dependencies/WalletStorage/WalletStorageInterface.swift b/secant/Dependencies/WalletStorage/WalletStorageInterface.swift index b369f2f..8db8964 100644 --- a/secant/Dependencies/WalletStorage/WalletStorageInterface.swift +++ b/secant/Dependencies/WalletStorage/WalletStorageInterface.swift @@ -30,6 +30,7 @@ struct WalletStorageClient { /// - bip39: Mnemonic/Seed phrase from `MnemonicSwift` /// - birthday: BlockHeight from SDK /// - language: Mnemonic's language + /// - hasUserPassedPhraseBackupTest: If user passed the puzzle phrase backup /// - Throws: /// - `WalletStorageError.unsupportedLanguage`: when mnemonic's language is anything other than English /// - `WalletStorageError.alreadyImported` when valid wallet is already in the storage @@ -66,7 +67,7 @@ struct WalletStorageClient { /// - Throws: /// - `WalletStorage.KeychainError.encoding`: when encoding the wallet's data failed. /// - `WalletStorageError.storageError` when some unrecognized error occurred. - let markUserPassedPhraseBackupTest: () throws -> Void + let markUserPassedPhraseBackupTest: (Bool) throws -> Void /// Use carefully: deletes the stored wallet. /// There's no fate but what we make for ourselves - Sarah Connor. diff --git a/secant/Dependencies/WalletStorage/WalletStorageLiveKey.swift b/secant/Dependencies/WalletStorage/WalletStorageLiveKey.swift index 7d4d392..4a6990b 100644 --- a/secant/Dependencies/WalletStorage/WalletStorageLiveKey.swift +++ b/secant/Dependencies/WalletStorage/WalletStorageLiveKey.swift @@ -32,8 +32,8 @@ extension WalletStorageClient: DependencyKey { updateBirthday: { birthday in try walletStorage.updateBirthday(birthday) }, - markUserPassedPhraseBackupTest: { - try walletStorage.markUserPassedPhraseBackupTest() + markUserPassedPhraseBackupTest: { flag in + try walletStorage.markUserPassedPhraseBackupTest(flag) }, nukeWallet: { walletStorage.nukeWallet() diff --git a/secant/Dependencies/WalletStorage/WalletStorageTestKey.swift b/secant/Dependencies/WalletStorage/WalletStorageTestKey.swift index db87032..55af5f9 100644 --- a/secant/Dependencies/WalletStorage/WalletStorageTestKey.swift +++ b/secant/Dependencies/WalletStorage/WalletStorageTestKey.swift @@ -25,7 +25,7 @@ extension WalletStorageClient { exportWallet: { .placeholder }, areKeysPresent: { false }, updateBirthday: { _ in }, - markUserPassedPhraseBackupTest: { }, + markUserPassedPhraseBackupTest: { _ in }, nukeWallet: { } ) } diff --git a/secant/Features/ImportWallet/ImportWalletStore.swift b/secant/Features/ImportWallet/ImportWalletStore.swift index 4fd45df..ce7d5f5 100644 --- a/secant/Features/ImportWallet/ImportWalletStore.swift +++ b/secant/Features/ImportWallet/ImportWalletStore.swift @@ -107,7 +107,7 @@ struct ImportWalletReducer: ReducerProtocol { try walletStorage.importWallet(state.importedSeedPhrase.data, birthday.data, .english, false) // update the backup phrase validation flag - try walletStorage.markUserPassedPhraseBackupTest() + try walletStorage.markUserPassedPhraseBackupTest(true) // notify user // TODO: [#221] Proper Error/Success handling (https://github.com/zcash/secant-ios-wallet/issues/221) diff --git a/secant/Features/RecoveryPhraseDisplay/RecoveryPhraseDisplayStore.swift b/secant/Features/RecoveryPhraseDisplay/RecoveryPhraseDisplayStore.swift index 37bb353..9757560 100644 --- a/secant/Features/RecoveryPhraseDisplay/RecoveryPhraseDisplayStore.swift +++ b/secant/Features/RecoveryPhraseDisplay/RecoveryPhraseDisplayStore.swift @@ -33,7 +33,6 @@ struct RecoveryPhraseDisplayReducer: ReducerProtocol { return .none case .finishedPressed: - // TODO: [#47] remove this when feature is implemented in https://github.com/zcash/secant-ios-wallet/issues/47 return .none case let .phraseResponse(phrase): diff --git a/secant/Features/RecoveryPhraseDisplay/RecoveryPhraseDisplayView.swift b/secant/Features/RecoveryPhraseDisplay/RecoveryPhraseDisplayView.swift index d3fe776..c99e8c1 100644 --- a/secant/Features/RecoveryPhraseDisplay/RecoveryPhraseDisplayView.swift +++ b/secant/Features/RecoveryPhraseDisplay/RecoveryPhraseDisplayView.swift @@ -13,57 +13,57 @@ struct RecoveryPhraseDisplayView: View { var body: some View { WithViewStore(self.store) { viewStore in - ScrollView { - VStack(alignment: .center, spacing: 0) { - if let groups = viewStore.phrase?.toGroups() { - VStack(spacing: 20) { - Text("recoveryPhraseDisplay.title") - .titleText() - .multilineTextAlignment(.center) - - VStack(alignment: .center, spacing: 4) { - Text("recoveryPhraseDisplay.description") - .bodyText() - - Text("recoveryPhraseDisplay.backItUp") - .bodyText() - } - } - .padding(.top, 0) - .padding(.bottom, 20) + VStack(alignment: .center, spacing: 0) { + if let groups = viewStore.phrase?.toGroups(groupSizeOverride: 2) { + VStack(spacing: 20) { + Text("recoveryPhraseDisplay.title") + .titleText() + .multilineTextAlignment(.center) - VStack(alignment: .leading, spacing: 35) { - ForEach(groups, id: \.startIndex) { group in - VStack { - WordChipGrid(words: group.words, startingAt: group.startIndex) - } - } + VStack(alignment: .center, spacing: 4) { + Text("recoveryPhraseDisplay.description") + .bodyText() } - .padding(.horizontal, 5) - - VStack { - Button( - action: { viewStore.send(.finishedPressed) }, - label: { Text("recoveryPhraseDisplay.button.finished") } - ) - .activeButtonStyle - .frame(height: 60) - - Button( - action: { - viewStore.send(.copyToBufferPressed) - }, - label: { - Text("recoveryPhraseDisplay.button.copyToBuffer") - .bodyText() - } - ) - .frame(height: 60) - } - .padding() - } else { - Text("recoveryPhraseDisplay.noWords") } + .padding(.top, 0) + .padding(.bottom, 20) + + Spacer() + + VStack(alignment: .leading, spacing: 10) { + ForEach(groups, id: \.startIndex) { group in + VStack { + HStack(alignment: .center) { + HStack { + Spacer() + Text("\(group.startIndex). \(group.words[0].data)") + Spacer() + } + .padding(.leading, 20) + HStack { + Spacer() + Text("\(group.startIndex + 1). \(group.words[1].data)") + Spacer() + } + .padding(.trailing, 20) + } + } + } + } + + Spacer() + + VStack { + Button( + action: { viewStore.send(.finishedPressed) }, + label: { Text("recoveryPhraseDisplay.button.wroteItDown") } + ) + .activeButtonStyle + .frame(height: 60) + } + .padding() + } else { + Text("recoveryPhraseDisplay.noWords") } } .padding(.bottom, 20) diff --git a/secant/Features/Root/RootDestination.swift b/secant/Features/Root/RootDestination.swift index 4b7490c..ed2326d 100644 --- a/secant/Features/Root/RootDestination.swift +++ b/secant/Features/Root/RootDestination.swift @@ -62,12 +62,15 @@ extension RootReducer { case .phraseDisplay(.finishedPressed): // user is still supposed to do the backup phrase validation test - if state.destinationState.previousDestination == .welcome - || state.destinationState.previousDestination == .onboarding { + if (state.destinationState.previousDestination == .welcome + || state.destinationState.previousDestination == .onboarding + || state.destinationState.previousDestination == .startup) + && state.walletConfig.isEnabled(.testBackupPhraseFlow) { state.destinationState.destination = .phraseValidation } // user wanted to see the backup phrase once again (at validation finished screen) - if state.destinationState.previousDestination == .phraseValidation { + if state.destinationState.previousDestination == .phraseValidation + || !state.walletConfig.isEnabled(.testBackupPhraseFlow) { state.destinationState.destination = .home } diff --git a/secant/Features/Root/RootInitialization.swift b/secant/Features/Root/RootInitialization.swift index 03f3516..d77b3fb 100644 --- a/secant/Features/Root/RootInitialization.swift +++ b/secant/Features/Root/RootInitialization.swift @@ -149,8 +149,8 @@ extension RootReducer { } var landingDestination = RootReducer.DestinationState.Destination.home - - if !storedWallet.hasUserPassedPhraseBackupTest { + + if !storedWallet.hasUserPassedPhraseBackupTest && state.walletConfig.isEnabled(.testBackupPhraseFlow) { do { let phraseWords = try mnemonic.asWords(storedWallet.seedPhrase.value()) @@ -178,7 +178,7 @@ extension RootReducer { let birthday = zcashSDKEnvironment.latestCheckpoint // store the wallet to the keychain - try walletStorage.importWallet(newRandomPhrase, birthday, .english, false) + try walletStorage.importWallet(newRandomPhrase, birthday, .english, !state.walletConfig.isEnabled(.testBackupPhraseFlow)) // start the backup phrase validation test let randomRecoveryPhraseWords = try mnemonic.asWords(newRandomPhrase) @@ -198,7 +198,7 @@ extension RootReducer { case .phraseValidation(.succeed): do { - try walletStorage.markUserPassedPhraseBackupTest() + try walletStorage.markUserPassedPhraseBackupTest(true) } catch { // TODO: [#221] error we need to handle, issue #221 (https://github.com/zcash/secant-ios-wallet/issues/221) } diff --git a/secant/Models/RecoveryPhrase.swift b/secant/Models/RecoveryPhrase.swift index 3299880..1ecf9f5 100644 --- a/secant/Models/RecoveryPhrase.swift +++ b/secant/Models/RecoveryPhrase.swift @@ -23,10 +23,11 @@ struct RecoveryPhrase: Equatable, Redactable { private let groupSize = 6 - func toGroups() -> [Group] { - let chunks = words.count / groupSize - return zip(0 ..< chunks, words.chunked(into: groupSize)).map { - Group(startIndex: $0 * groupSize + 1, words: $1) + func toGroups(groupSizeOverride: Int? = nil) -> [Group] { + let internalGroupSize = groupSizeOverride ?? groupSize + let chunks = words.count / internalGroupSize + return zip(0 ..< chunks, words.chunked(into: internalGroupSize)).map { + Group(startIndex: $0 * internalGroupSize + 1, words: $1) } } diff --git a/secant/Models/WalletConfig.swift b/secant/Models/WalletConfig.swift index fc2b8bf..4c35941 100644 --- a/secant/Models/WalletConfig.swift +++ b/secant/Models/WalletConfig.swift @@ -10,11 +10,13 @@ enum FeatureFlag: String, CaseIterable, Codable { case testFlag1 case testFlag2 case onboardingFlow + case testBackupPhraseFlow var enabledByDefault: Bool { switch self { case .testFlag1, .testFlag2: return false case .onboardingFlow: return false + case .testBackupPhraseFlow: return false } } } diff --git a/secant/Resources/Localizable.strings b/secant/Resources/Localizable.strings index 925f63d..775be40 100644 --- a/secant/Resources/Localizable.strings +++ b/secant/Resources/Localizable.strings @@ -20,9 +20,8 @@ // MARK: - Secret Recovery Phrase Display "recoveryPhraseDisplay.title" = "Your Secret Recovery Phrase"; -"recoveryPhraseDisplay.description" = "The following 24 words represent your funds and the security used to protect them."; -"recoveryPhraseDisplay.backItUp" = "Back them up now! There will be a test."; -"recoveryPhraseDisplay.button.finished" = "Finished!"; +"recoveryPhraseDisplay.description" = "The following 24 words represent your funds and the security used to protect them. Back them up now!"; +"recoveryPhraseDisplay.button.wroteItDown" = "I wrote it down!"; "recoveryPhraseDisplay.button.copyToBuffer" = "Copy To Buffer"; "recoveryPhraseDisplay.noWords" = "Oops no words"; diff --git a/secant/UI Components/FontStyles/SecantTextStyles.swift b/secant/UI Components/FontStyles/SecantTextStyles.swift index 88a4f42..6368c2c 100644 --- a/secant/UI Components/FontStyles/SecantTextStyles.swift +++ b/secant/UI Components/FontStyles/SecantTextStyles.swift @@ -47,7 +47,7 @@ extension Text { func body(content: Content) -> some View { content .foregroundColor(Asset.Colors.Text.heading.color) - .font(.custom(FontFamily.Rubik.medium.name, size: 33, relativeTo: .callout)) + .font(.custom(FontFamily.Rubik.medium.name, size: 24, relativeTo: .callout)) .shadow(color: Asset.Colors.Text.captionTextShadow.color, radius: 1, x: 0, y: 1) } } diff --git a/secantTests/RecoveryPhraseValidationTests/RecoveryPhraseValidationFlowFeatureFlagTests.swift b/secantTests/RecoveryPhraseValidationTests/RecoveryPhraseValidationFlowFeatureFlagTests.swift new file mode 100644 index 0000000..8608590 --- /dev/null +++ b/secantTests/RecoveryPhraseValidationTests/RecoveryPhraseValidationFlowFeatureFlagTests.swift @@ -0,0 +1,232 @@ +// +// RecoveryPhraseValidationFlowFeatureFlagTests.swift +// secantTests +// +// Created by Lukáš Korba on 01.03.2023. +// + +import XCTest +@testable import secant_testnet +import ComposableArchitecture + +// swiftlint:disable:next type_name +class RecoveryPhraseValidationFlowFeatureFlagTests: XCTestCase { + override func setUp() { + super.setUp() + + UserDefaultsWalletConfigStorage().clearAll() + } + + func testRecoveryPhraseValidationFlowOffByDefault() throws { + XCTAssertFalse(WalletConfig.default.isEnabled(.testBackupPhraseFlow)) + } + + func testRecoveryPhraseValidationFlow_SkipPuzzleUserConfirmedBackup() { + let store = TestStore( + initialState: .placeholder, + reducer: RootReducer() + ) + + store.send(.phraseDisplay(.finishedPressed)) { state in + state.destinationState.internalDestination = .home + state.destinationState.previousDestination = .welcome + } + } + + func testRecoveryPhraseValidationFlow_StartPuzzle() { + var rootState = RootReducer.State.placeholder + + var defaultRawFlags = WalletConfig.default.flags + defaultRawFlags[.testBackupPhraseFlow] = true + rootState.walletConfig = WalletConfig(flags: defaultRawFlags) + + let store = TestStore( + initialState: rootState, + reducer: RootReducer() + ) + + store.send(.phraseDisplay(.finishedPressed)) + } + + func testRecoveryPhraseValidationFlow_SkipPuzzleStartOfTheApp() { + var rootState = RootReducer.State.placeholder + rootState.storedWallet = .placeholder + + let store = TestStore( + initialState: rootState, + reducer: RootReducer() + ) + + store.dependencies.mainQueue = .immediate + + store.send(.initialization(.checkBackupPhraseValidation)) { state in + state.appInitializationState = .initialized + } + + store.receive(.destination(.updateDestination(.home))) { state in + state.destinationState.internalDestination = .home + state.destinationState.previousDestination = .welcome + } + } + + func testRecoveryPhraseValidationFlow_StartPuzzleStartOfTheApp() { + var rootState = RootReducer.State.placeholder + rootState.storedWallet = .placeholder + + var defaultRawFlags = WalletConfig.default.flags + defaultRawFlags[.testBackupPhraseFlow] = true + rootState.walletConfig = WalletConfig(flags: defaultRawFlags) + + let store = TestStore( + initialState: rootState, + reducer: RootReducer() + ) + + let mnemonic = + """ + still champion voice habit trend flight \ + survey between bitter process artefact blind \ + carbon truly provide dizzy crush flush \ + breeze blouse charge solid fish spread + """ + + let randomRecoveryPhrase = RecoveryPhraseValidationFlowReducer.State.placeholder + + store.dependencies.mainQueue = .immediate + store.dependencies.mnemonic = .noOp + store.dependencies.mnemonic.asWords = { _ in mnemonic.components(separatedBy: " ") } + store.dependencies.randomRecoveryPhrase.random = { _ in randomRecoveryPhrase } + + store.send(.initialization(.checkBackupPhraseValidation)) { state in + state.appInitializationState = .initialized + state.phraseDisplayState.phrase = RecoveryPhrase(words: mnemonic.components(separatedBy: " ").map { $0.redacted }) + } + + store.receive(.destination(.updateDestination(.phraseDisplay))) { state in + state.destinationState.internalDestination = .phraseDisplay + state.destinationState.previousDestination = .welcome + } + } + + @MainActor func testDidFinishLaunching_to_InitializedWalletPhraseBackupPuzzleOff() async throws { + // setup the store and environment to be fully mocked + let recoveryPhrase = RecoveryPhrase(words: try MnemonicClient.mock.randomMnemonicWords().map { $0.redacted }) + + let phraseValidationState = RecoveryPhraseValidationFlowReducer.State( + phrase: recoveryPhrase, + missingIndices: [2, 0, 3, 5], + missingWordChips: [ + .unassigned(word: "voice".redacted), + .empty, + .unassigned(word: "survey".redacted), + .unassigned(word: "spread".redacted) + ], + validationWords: [ + .init(groupIndex: 2, word: "dizzy".redacted) + ], + destination: nil + ) + + let recoveryPhraseRandomizer = RecoveryPhraseRandomizerClient( + random: { _ in + let missingIndices = [2, 0, 3, 5] + let missingWordChipKind = [ + PhraseChip.Kind.unassigned( + word: "voice".redacted, + color: Asset.Colors.Buttons.activeButton.color + ), + PhraseChip.Kind.empty, + PhraseChip.Kind.unassigned( + word: "survey".redacted, + color: Asset.Colors.Buttons.activeButton.color + ), + PhraseChip.Kind.unassigned( + word: "spread".redacted, + color: Asset.Colors.Buttons.activeButton.color + ) + ] + + return RecoveryPhraseValidationFlowReducer.State( + phrase: recoveryPhrase, + missingIndices: missingIndices, + missingWordChips: missingWordChipKind, + validationWords: [ + ValidationWord( + groupIndex: 2, + word: "dizzy".redacted + ) + ] + ) + } + ) + + let appState = RootReducer.State( + destinationState: .placeholder, + homeState: .placeholder, + onboardingState: .init( + walletConfig: .default, + importWalletState: .placeholder + ), + phraseValidationState: phraseValidationState, + phraseDisplayState: RecoveryPhraseDisplayReducer.State( + phrase: recoveryPhrase + ), + sandboxState: .placeholder, + walletConfig: .default, + welcomeState: .placeholder + ) + + let store = TestStore( + initialState: appState, + reducer: RootReducer() + ) + + let testQueue = DispatchQueue.test + + store.dependencies.databaseFiles = .noOp + store.dependencies.databaseFiles.areDbFilesPresentFor = { _ in true } + store.dependencies.derivationTool = .liveValue + store.dependencies.mainQueue = .immediate// testQueue.eraseToAnyScheduler() + store.dependencies.mnemonic = .mock + store.dependencies.randomRecoveryPhrase = recoveryPhraseRandomizer + store.dependencies.walletStorage.exportWallet = { .placeholder } + store.dependencies.walletStorage.areKeysPresent = { true } + store.dependencies.walletConfigProvider = .noOp + + // Root of the test, the app finished the launch process and triggers the checks and initializations. + await store.send(.initialization(.appDelegate(.didFinishLaunching))) + + await testQueue.advance(by: 0.02) + + await store.receive(.initialization(.checkWalletConfig)) + + await store.receive(.walletConfigLoaded(WalletConfig.default)) + + await store.receive(.initialization(.initialSetups)) + + await testQueue.advance(by: 0.02) + + await store.receive(.initialization(.configureCrashReporter)) + + await store.receive(.initialization(.checkWalletInitialization)) + + await store.receive(.initialization(.respondToWalletInitializationState(.initialized))) + + await testQueue.advance(by: 3.00) + + await store.receive(.initialization(.initializeSDK)) { state in + state.storedWallet = .placeholder + } + + await store.receive(.initialization(.checkBackupPhraseValidation)) { state in + state.appInitializationState = .initialized + } + + await store.receive(.destination(.updateDestination(.home))) { state in + state.destinationState.previousDestination = .welcome + state.destinationState.internalDestination = .home + } + + await store.finish() + } +} diff --git a/secantTests/RootTests/AppInitializationTests.swift b/secantTests/RootTests/AppInitializationTests.swift index c7d267a..dade445 100644 --- a/secantTests/RootTests/AppInitializationTests.swift +++ b/secantTests/RootTests/AppInitializationTests.swift @@ -63,6 +63,10 @@ class AppInitializationTests: XCTestCase { } ) + var defaultRawFlags = WalletConfig.default.flags + defaultRawFlags[.testBackupPhraseFlow] = true + let walletConfig = WalletConfig(flags: defaultRawFlags) + let appState = RootReducer.State( destinationState: .placeholder, homeState: .placeholder, @@ -75,7 +79,7 @@ class AppInitializationTests: XCTestCase { phrase: recoveryPhrase ), sandboxState: .placeholder, - walletConfig: .default, + walletConfig: walletConfig, welcomeState: .placeholder ) diff --git a/secantTests/SettingsTests/SettingsTests.swift b/secantTests/SettingsTests/SettingsTests.swift index 2570ace..9b66d20 100644 --- a/secantTests/SettingsTests/SettingsTests.swift +++ b/secantTests/SettingsTests/SettingsTests.swift @@ -38,7 +38,7 @@ class SettingsTests: XCTestCase { updateBirthday: { _ in throw WalletStorage.KeychainError.encoding }, - markUserPassedPhraseBackupTest: { + markUserPassedPhraseBackupTest: { _ in throw WalletStorage.KeychainError.encoding }, nukeWallet: { } diff --git a/secantTests/WalletConfigProviderTests/WalletConfigProviderTests.swift b/secantTests/WalletConfigProviderTests/WalletConfigProviderTests.swift index c35c7e0..caf8a12 100644 --- a/secantTests/WalletConfigProviderTests/WalletConfigProviderTests.swift +++ b/secantTests/WalletConfigProviderTests/WalletConfigProviderTests.swift @@ -8,6 +8,7 @@ import Combine import XCTest @testable import secant_testnet +import ComposableArchitecture class WalletConfigProviderTests: XCTestCase { var cancellables: [AnyCancellable] = [] @@ -114,6 +115,24 @@ class WalletConfigProviderTests: XCTestCase { return configuration } + + func testPropagationOfFlagUpdate() throws { + let store = TestStore( + initialState: .placeholder, + reducer: RootReducer() + ) + + // Change any of the flags from the default value + var defaultRawFlags = WalletConfig.default.flags + defaultRawFlags[.onboardingFlow] = true + let flags = WalletConfig(flags: defaultRawFlags) + + // The new flag's value has to be propagated to all `walletConfig` instances + store.send(.debug(.walletConfigLoaded(flags))) { state in + state.walletConfig = flags + state.onboardingState.walletConfig = flags + } + } } struct WalletConfigSourceProviderMock: WalletConfigSourceProvider {