diff --git a/secant.xcodeproj/project.pbxproj b/secant.xcodeproj/project.pbxproj index 99d5aff..bfd9ed7 100644 --- a/secant.xcodeproj/project.pbxproj +++ b/secant.xcodeproj/project.pbxproj @@ -82,6 +82,9 @@ 66A0807B271993C500118B79 /* OnboardingProgressIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */; }; 66D50668271D9B6100E51F0D /* NavigationButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66D50667271D9B6100E51F0D /* NavigationButtonStyle.swift */; }; 66DC733F271D88CC0053CBB6 /* StandardButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66DC733E271D88CC0053CBB6 /* StandardButtonStyle.swift */; }; + 9E2DF99C27CF704D00649636 /* ImportWalletStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99827CF704D00649636 /* ImportWalletStore.swift */; }; + 9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99A27CF704D00649636 /* ImportSeedEditor.swift */; }; + 9E2DF99E27CF704D00649636 /* ImportWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E2DF99B27CF704D00649636 /* ImportWalletView.swift */; }; 9E37A2B827C8F59F00AE57B3 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 9E37A2B727C8F59F00AE57B3 /* Localizable.strings */; }; 9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */; }; 9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */; }; @@ -213,6 +216,9 @@ 66A0807A271993C500118B79 /* OnboardingProgressIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingProgressIndicator.swift; sourceTree = ""; }; 66D50667271D9B6100E51F0D /* NavigationButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationButtonStyle.swift; sourceTree = ""; }; 66DC733E271D88CC0053CBB6 /* StandardButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandardButtonStyle.swift; sourceTree = ""; }; + 9E2DF99827CF704D00649636 /* ImportWalletStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletStore.swift; sourceTree = ""; }; + 9E2DF99A27CF704D00649636 /* ImportSeedEditor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportSeedEditor.swift; sourceTree = ""; }; + 9E2DF99B27CF704D00649636 /* ImportWalletView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImportWalletView.swift; sourceTree = ""; }; 9E37A2B727C8F59F00AE57B3 /* Localizable.strings */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; 9E4DC6DF27C409A100E657F4 /* NeumorphicDesignModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeumorphicDesignModifier.swift; sourceTree = ""; }; 9E4DC6E127C4C6B700E657F4 /* SecantButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecantButtonStyles.swift; sourceTree = ""; }; @@ -593,6 +599,7 @@ 0D3D04052728B2D70032ABC1 /* BackupFlow */, 6654C73C2715A3FA00901167 /* Onboarding */, F9971A6727680E1000A2DB75 /* WalletInfo */, + 9E2DF99727CF704D00649636 /* ImportWallet */, ); path = Features; sourceTree = ""; @@ -648,6 +655,24 @@ path = CircularFrame; sourceTree = ""; }; + 9E2DF99727CF704D00649636 /* ImportWallet */ = { + isa = PBXGroup; + children = ( + 9E2DF99827CF704D00649636 /* ImportWalletStore.swift */, + 9E2DF99927CF704D00649636 /* Views */, + ); + path = ImportWallet; + sourceTree = ""; + }; + 9E2DF99927CF704D00649636 /* Views */ = { + isa = PBXGroup; + children = ( + 9E2DF99A27CF704D00649636 /* ImportSeedEditor.swift */, + 9E2DF99B27CF704D00649636 /* ImportWalletView.swift */, + ); + path = Views; + sourceTree = ""; + }; 9EBEF87827CE365D00B4F343 /* Preamble */ = { isa = PBXGroup; children = ( @@ -1024,6 +1049,7 @@ 9E4DC6E027C409A100E657F4 /* NeumorphicDesignModifier.swift in Sources */, 0DACFA7F27208CE00039EEA5 /* Clamped.swift in Sources */, 0DFE93E3272CA1AA000FCCA5 /* RecoveryPhraseValidation.swift in Sources */, + 9E2DF99E27CF704D00649636 /* ImportWalletView.swift in Sources */, 0D354A0B26D5A9D000315F45 /* MnemonicSeedPhraseHandling.swift in Sources */, 0D535FE2271F9476009A9E3E /* EnumeratedChip.swift in Sources */, 6654C73E2715A41300901167 /* OnboardingStore.swift in Sources */, @@ -1031,6 +1057,7 @@ 9E4DC6E227C4C6B700E657F4 /* SecantButtonStyles.swift in Sources */, 0DDB6A5127737D4A0012A410 /* ValidationFailedView.swift in Sources */, 0D6D628B276A528E002FB4CC /* DropDelegate.swift in Sources */, + 9E2DF99D27CF704D00649636 /* ImportSeedEditor.swift in Sources */, F9971A5327680DD000A2DB75 /* Profile.swift in Sources */, F93874F0273C4DE200F0E875 /* HomeStore.swift in Sources */, 669FDAEB272C23C2007B9422 /* CircularFrameBadge.swift in Sources */, @@ -1078,6 +1105,7 @@ 0D7DF08C271DCC0E00530046 /* ScreenBackground.swift in Sources */, F9C165C22740403600592F76 /* CreateView.swift in Sources */, F9C165B4274031F600592F76 /* Bindings.swift in Sources */, + 9E2DF99C27CF704D00649636 /* ImportWalletStore.swift in Sources */, F9971A6627680DFE00A2DB75 /* SettingsView.swift in Sources */, F96B41EB273B50520021B49A /* Strings.swift in Sources */, 0D354A0A26D5A9D000315F45 /* KeyStoring.swift in Sources */, diff --git a/secant/Features/App/App.swift b/secant/Features/App/App.swift index 1546fcb..6801361 100644 --- a/secant/Features/App/App.swift +++ b/secant/Features/App/App.swift @@ -37,10 +37,11 @@ extension AppReducer { routeReducer, homeReducer, onboardingReducer, - phraseValidationReducer.debug(), - phraseDisplayReducer.debug() + phraseValidationReducer, + phraseDisplayReducer ] ) + .debug() private static let routeReducer = AppReducer { state, action, _ in switch action { diff --git a/secant/Features/App/Views/AppView.swift b/secant/Features/App/Views/AppView.swift index 5f1f619..7aaecee 100644 --- a/secant/Features/App/Views/AppView.swift +++ b/secant/Features/App/Views/AppView.swift @@ -38,8 +38,6 @@ struct AppView: View { case .phraseValidation: NavigationView { -// RecoveryPhraseTestPreambleView -// RecoveryPhraseBackupValidationView RecoveryPhraseTestPreambleView( store: store.scope( state: \.phraseValidationState, diff --git a/secant/Features/BackupFlow/Preamble/RecoveryPhraseTestPreambleView.swift b/secant/Features/BackupFlow/Preamble/RecoveryPhraseTestPreambleView.swift index 70e1d42..e311952 100644 --- a/secant/Features/BackupFlow/Preamble/RecoveryPhraseTestPreambleView.swift +++ b/secant/Features/BackupFlow/Preamble/RecoveryPhraseTestPreambleView.swift @@ -55,7 +55,7 @@ struct RecoveryPhraseTestPreambleView: View { } Button( - action: { viewStore.send(.recoveryBackupPhraseValidation) }, + action: { viewStore.send(.updateRoute(.validation)) }, label: { Text("recoveryPhraseTestPreamble.button.goNext") } ) .activeButtonStyle @@ -75,8 +75,10 @@ struct RecoveryPhraseTestPreambleView: View { .frame(width: proxy.size.width) .scrollableWhenScaledUp() .navigationLinkEmpty( - isActive: viewStore.bindingForRoute(.recoveryBackupPhraseValidation), - destination: { RecoveryPhraseBackupValidationView(store: store) } + isActive: viewStore.bindingForValidation, + destination: { + RecoveryPhraseBackupValidationView(store: store) + } ) } .padding() @@ -105,7 +107,9 @@ extension RecoveryPhraseTestPreambleView { struct RecoveryPhraseTestPreambleView_Previews: PreviewProvider { static var previews: some View { Group { - RecoveryPhraseTestPreambleView(store: .demo) + NavigationView { + RecoveryPhraseTestPreambleView(store: .demo) + } RecoveryPhraseTestPreambleView(store: .demo) .preferredColorScheme(.dark) diff --git a/secant/Features/BackupFlow/RecoveryPhraseValidation.swift b/secant/Features/BackupFlow/RecoveryPhraseValidation.swift index 63c64ff..bf0a3e5 100644 --- a/secant/Features/BackupFlow/RecoveryPhraseValidation.swift +++ b/secant/Features/BackupFlow/RecoveryPhraseValidation.swift @@ -19,9 +19,11 @@ struct ValidationWord: Equatable { var word: String } +// MARK: - State + struct RecoveryPhraseValidationState: Equatable { enum Route: Equatable, CaseIterable { - case recoveryBackupPhraseValidation + case validation case success case failure } @@ -45,17 +47,6 @@ struct RecoveryPhraseValidationState: Equatable { } } -extension RecoveryPhraseValidationViewStore { - func bindingForRoute(_ route: RecoveryPhraseValidationState.Route) -> Binding { - self.binding( - get: { $0.route == route }, - send: { isActive in - return .updateRoute(isActive ? route : nil) - } - ) - } -} - extension RecoveryPhraseValidationState { /// creates an initial `RecoveryPhraseValidationState` with no completions and random missing indices. /// - Note: Use this function to create a random validation puzzle for a given phrase. @@ -128,8 +119,9 @@ extension RecoveryPhrase.Group { } } +// MARK: - Action + enum RecoveryPhraseValidationAction: Equatable { - case recoveryBackupPhraseValidation case updateRoute(RecoveryPhraseValidationState.Route?) case reset case move(wordChip: PhraseChip.Kind, intoGroup: Int) @@ -140,6 +132,8 @@ enum RecoveryPhraseValidationAction: Equatable { case displayBackedUpPhrase } +// MARK: - Reducer + typealias RecoveryPhraseValidationReducer = Reducer extension RecoveryPhraseValidationReducer { @@ -147,6 +141,7 @@ extension RecoveryPhraseValidationReducer { switch action { case .reset: state = RecoveryPhraseValidationState.random(phrase: state.phrase) + state.route = .validation case let .move(wordChip, group): guard @@ -162,7 +157,7 @@ extension RecoveryPhraseValidationReducer { let effect = Effect(value: value) .delay(for: 1, scheduler: environment.mainQueue) .eraseToEffect() - + if value == .succeed { return effect } else { @@ -184,6 +179,10 @@ extension RecoveryPhraseValidationReducer { environment.feedbackGenerator.generateFeedback() case .updateRoute(let route): + guard let route = route else { + state = RecoveryPhraseValidationState.random(phrase: state.phrase) + return .none + } state.route = route case .proceedToHome: @@ -191,10 +190,49 @@ extension RecoveryPhraseValidationReducer { case .displayBackedUpPhrase: break - - case .recoveryBackupPhraseValidation: - state.route = .recoveryBackupPhraseValidation } return .none } } + +// MARK: - ViewStore + +extension RecoveryPhraseValidationViewStore { + func bindingForRoute(_ route: RecoveryPhraseValidationState.Route) -> Binding { + self.binding( + get: { $0.route == route }, + send: { isActive in + return .updateRoute(isActive ? route : nil) + } + ) + } +} + +extension RecoveryPhraseValidationViewStore { + var bindingForValidation: Binding { + self.binding( + get: { $0.route != nil }, + send: { isActive in + return .updateRoute(isActive ? .validation : nil) + } + ) + } + + var bindingForSuccess: Binding { + self.binding( + get: { $0.route == .success }, + send: { isActive in + return .updateRoute(isActive ? .success : .validation) + } + ) + } + + var bindingForFailure: Binding { + self.binding( + get: { $0.route == .failure }, + send: { isActive in + return .updateRoute(isActive ? .failure : .validation) + } + ) + } +} diff --git a/secant/Features/BackupFlow/Views/RecoveryPhraseBackupValidationView.swift b/secant/Features/BackupFlow/Views/RecoveryPhraseBackupValidationView.swift index b2617c3..59d7e1d 100644 --- a/secant/Features/BackupFlow/Views/RecoveryPhraseBackupValidationView.swift +++ b/secant/Features/BackupFlow/Views/RecoveryPhraseBackupValidationView.swift @@ -11,28 +11,31 @@ import ComposableArchitecture struct RecoveryPhraseBackupValidationView: View { let store: RecoveryPhraseValidationStore + var viewStore: RecoveryPhraseValidationViewStore { + ViewStore(store) + } + var body: some View { - WithViewStore(self.store) { viewStore in - VStack(alignment: .center) { - header(for: viewStore) - .padding(.horizontal) - .padding(.bottom, 10) - - ZStack { - Asset.Colors.BackgroundColors.phraseGridDarkGray.color - .edgesIgnoringSafeArea(.bottom) - - VStack(alignment: .center, spacing: 35) { - let state = viewStore.state - let groups = state.phrase.toGroups() - - ForEach(Array(zip(groups.indices, groups)), id: \.0) { index, group in - WordChipGrid( - state: state, - groupIndex: index, - wordGroup: group, - misingIndex: index - ) + VStack(alignment: .center) { + header(for: viewStore) + .padding(.horizontal) + .padding(.bottom, 10) + + ZStack { + Asset.Colors.BackgroundColors.phraseGridDarkGray.color + .edgesIgnoringSafeArea(.bottom) + + VStack(alignment: .center, spacing: 35) { + let state = viewStore.state + let groups = state.phrase.toGroups() + + ForEach(Array(zip(groups.indices, groups)), id: \.0) { index, group in + WordChipGrid( + state: state, + groupIndex: index, + wordGroup: group, + misingIndex: index + ) .frame(alignment: .center) .background(Asset.Colors.BackgroundColors.phraseGridDarkGray.color) .whenIsDroppable( @@ -41,28 +44,27 @@ struct RecoveryPhraseBackupValidationView: View { viewStore.send(.move(wordChip: chipKind, intoGroup: index)) } ) - } - - Spacer() } - .padding() - .padding(.top, 0) - .navigationLinkEmpty( - isActive: viewStore.bindingForRoute(.success), - destination: { ValidationSucceededView(store: store) } - ) - .navigationLinkEmpty( - isActive: viewStore.bindingForRoute(.failure), - destination: { ValidationFailedView(store: store) } - ) + + Spacer() } - .frame(alignment: .top) + .padding() + .padding(.top, 0) + .navigationLinkEmpty( + isActive: viewStore.bindingForSuccess, + destination: { ValidationSucceededView(store: store) } + ) + .navigationLinkEmpty( + isActive: viewStore.bindingForFailure, + destination: { ValidationFailedView(store: store) } + ) } - .applyScreenBackground() - .scrollableWhenScaledUp() - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(Text("recoveryPhraseBackupValidation.title")) + .frame(alignment: .top) } + .applyScreenBackground() + .scrollableWhenScaledUp() + .navigationBarTitleDisplayMode(.inline) + .navigationTitle(Text("recoveryPhraseBackupValidation.title")) } @ViewBuilder func header(for viewStore: RecoveryPhraseValidationViewStore) -> some View { diff --git a/secant/Localizable.strings b/secant/Localizable.strings index 4467270..c9a5142 100644 --- a/secant/Localizable.strings +++ b/secant/Localizable.strings @@ -41,7 +41,14 @@ "validationFailed.description" = "Your placed words did not match your secret recovery phrase."; "validationFailed.incorrectBackupDescription" = "Remember, you can't recover your funds if you lose (or incorrectly save) these 24 words."; "validationFailed.button.tryAgain" = "I'm ready to try again"; - + +// MARK: - Recovery Phrase Test Preamble +"recoveryPhraseTestPreamble.title" = "First things first"; +"recoveryPhraseTestPreamble.paragraph1" = "It is important to understand that you are in charge here. Great, right? YOU get to be the bank!"; +"recoveryPhraseTestPreamble.paragraph2" = "But it also means that YOU are the customer, and you need to be self-reliant."; +"recoveryPhraseTestPreamble.paragraph3" = "So how do you recover funds that you've hidden on a completely decentralized and private block-chain?"; +"recoveryPhraseTestPreamble.button.goNext" = "By understanding and preparing"; + // MARK: - Import Wallet Screen "importWallet.title" = "Wallet Import"; "importWallet.description" = "You can import your backed up wallet by entering your backup recovery phrase (aka seed phrase) now.";