diff --git a/secant/Colors.xcassets/Text/ImportSeedEditor.colorset/Contents.json b/secant/Colors.xcassets/Text/ImportSeedEditor.colorset/Contents.json new file mode 100644 index 0000000..8a83ad4 --- /dev/null +++ b/secant/Colors.xcassets/Text/ImportSeedEditor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xA0", + "green" : "0x81", + "red" : "0x6E" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xA0", + "green" : "0x81", + "red" : "0x6E" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/secant/Features/App/App.swift b/secant/Features/App/App.swift index 6ced310..1546fcb 100644 --- a/secant/Features/App/App.swift +++ b/secant/Features/App/App.swift @@ -113,7 +113,9 @@ extension AppState { static var placeholder: Self { .init( homeState: .placeholder, - onboardingState: .init(), + onboardingState: .init( + importWalletState: .placeholder + ), phraseValidationState: RecoveryPhraseValidationState.placeholder, phraseDisplayState: RecoveryPhraseDisplayState( phrase: .placeholder diff --git a/secant/Features/App/Views/AppView.swift b/secant/Features/App/Views/AppView.swift index f51f190..5f1f619 100644 --- a/secant/Features/App/Views/AppView.swift +++ b/secant/Features/App/Views/AppView.swift @@ -20,12 +20,15 @@ struct AppView: View { .navigationViewStyle(StackNavigationViewStyle()) case .onboarding: - OnboardingScreen( - store: store.scope( - state: \.onboardingState, - action: AppAction.onboarding + NavigationView { + OnboardingScreen( + store: store.scope( + state: \.onboardingState, + action: AppAction.onboarding + ) ) - ) + } + .navigationViewStyle(StackNavigationViewStyle()) case .startup: ZStack(alignment: .topTrailing) { diff --git a/secant/Features/ImportWallet/ImportWalletStore.swift b/secant/Features/ImportWallet/ImportWalletStore.swift new file mode 100644 index 0000000..adfb0a5 --- /dev/null +++ b/secant/Features/ImportWallet/ImportWalletStore.swift @@ -0,0 +1,47 @@ +// +// ImportWalletStore.swift +// secant-testnet +// +// Created by Lukáš Korba on 02/25/2022. +// + +import ComposableArchitecture + +typealias ImportWalletStore = Store + +struct ImportWalletState: Equatable { + @BindableState var importedSeedPhrase: String = "" +} + +enum ImportWalletAction: Equatable, BindableAction { + case importRecoveryPhrase + case importPrivateOrViewingKey + case binding(BindingAction) +} + +struct ImportWalletEnvironment { } + +extension ImportWalletEnvironment { + static let demo = Self() + + static let live = Self() +} + +typealias ImportWalletReducer = Reducer + +extension ImportWalletReducer { + static let `default` = ImportWalletReducer { _, action, _ in + switch action { + case .importRecoveryPhrase: + // TODO: once connected to SDK, use the state.importedSeedPhrase (Issue #166) + return .none + + case .importPrivateOrViewingKey: + return .none + + case .binding: + return .none + } + } + .binding() +} diff --git a/secant/Features/ImportWallet/Views/ImportSeedEditor.swift b/secant/Features/ImportWallet/Views/ImportSeedEditor.swift new file mode 100644 index 0000000..9f0630b --- /dev/null +++ b/secant/Features/ImportWallet/Views/ImportSeedEditor.swift @@ -0,0 +1,67 @@ +// +// ImportSeedEditor.swift +// secant-testnet +// +// Created by Lukáš Korba on 02/25/2022. +// + +import SwiftUI +import ComposableArchitecture + +struct ImportSeedEditor: View { + var store: ImportWalletStore + + /// Clearance of the black color for the TextEditor under the text (.dark colorScheme) + init(store: ImportWalletStore) { + self.store = store + UITextView.appearance().backgroundColor = .clear + } + + var body: some View { + WithViewStore(store) { viewStore in + TextEditor(text: viewStore.binding(\.$importedSeedPhrase)) + .importSeedEditorModifier() + .padding(28) + } + } +} + +struct ImportSeedEditorModifier: ViewModifier { + func body(content: Content) -> some View { + content + .foregroundColor(Asset.Colors.Text.importSeedEditor.color) + .padding() + .background(Color.white) + .cornerRadius(4) + .overlay( + RoundedRectangle(cornerRadius: 4) + .stroke(Color.black, lineWidth: 2) + ) + } +} + +extension View { + func importSeedEditorModifier() -> some View { + modifier(ImportSeedEditorModifier()) + } +} + +struct ImportSeedInputField_Previews: PreviewProvider { + static let width: CGFloat = 400 + static let height: CGFloat = 200 + + static var previews: some View { + Group { + ImportSeedEditor(store: .demo) + .frame(width: width, height: height) + .applyScreenBackground() + .preferredColorScheme(.light) + + ImportSeedEditor(store: .demo) + .frame(width: width, height: height) + .applyScreenBackground() + .preferredColorScheme(.dark) + } + .previewLayout(.fixed(width: width + 50, height: height + 50)) + } +} diff --git a/secant/Features/ImportWallet/Views/ImportWalletView.swift b/secant/Features/ImportWallet/Views/ImportWalletView.swift new file mode 100644 index 0000000..a804465 --- /dev/null +++ b/secant/Features/ImportWallet/Views/ImportWalletView.swift @@ -0,0 +1,110 @@ +// +// ImportWalletView.swift +// secant-testnet +// +// Created by Lukáš Korba on 02/25/2022. +// + +import SwiftUI +import ComposableArchitecture + +struct ImportWalletView: View { + @Environment(\.presentationMode) var presentationMode + + var store: ImportWalletStore + + var body: some View { + WithViewStore(store) { viewStore in + VStack { + VStack(alignment: .leading, spacing: 30) { + HStack { + Button("Back") { presentationMode.wrappedValue.dismiss() } + .navigationButtonStyle + .frame(width: 75, height: 40) + + Text("importWallet.title") + .titleText() + } + + Text("importWallet.description") + .paragraphText() + .lineSpacing(4) + .opacity(0.53) + } + .padding(18) + + ImportSeedEditor(store: store) + .frame(width: nil, height: 200, alignment: .center) + + Button("importWallet.button.importPhrase") { + viewStore.send(.importRecoveryPhrase) + } + .activeButtonStyle + .importWalletButtonLayout() + + Button("importWallet.button.importPrivateKey") { + viewStore.send(.importPrivateOrViewingKey) + } + .secondaryButtonStyle + .importWalletButtonLayout() + + Spacer() + } + .navigationBarHidden(true) + .applyScreenBackground() + .scrollableWhenScaledUp() + } + } +} + +// swiftlint:disable:next private_over_fileprivate strict_fileprivate +fileprivate struct ImportWalletButtonLayout: ViewModifier { + func body(content: Content) -> some View { + content + .frame( + minWidth: 0, + maxWidth: .infinity, + minHeight: 64, + maxHeight: .infinity, + alignment: .center + ) + .fixedSize(horizontal: false, vertical: true) + .padding(.horizontal, 28) + .transition(.opacity) + } +} + +extension View { + func importWalletButtonLayout() -> some View { + modifier(ImportWalletButtonLayout()) + } +} + +extension ImportWalletStore { + static let demo = Store( + initialState: .placeholder, + reducer: .default, + environment: .demo + ) +} + +extension ImportWalletState { + static let placeholder = ImportWalletState(importedSeedPhrase: "") + + static let live = ImportWalletState(importedSeedPhrase: "") +} + +struct ImportWalletView_Previews: PreviewProvider { + static var previews: some View { + ImportWalletView(store: .demo) + .preferredColorScheme(.light) + + ImportWalletView(store: .demo) + .preferredColorScheme(.dark) + + ImportWalletView(store: .demo) + .previewDevice(PreviewDevice(rawValue: "iPhone SE (2nd generation)")) + .preferredColorScheme(.light) + .environment(\.sizeCategory, .accessibilityLarge) + } +} diff --git a/secant/Features/Onboarding/OnboardingStore.swift b/secant/Features/Onboarding/OnboardingStore.swift index 1f396bf..bac66dd 100644 --- a/secant/Features/Onboarding/OnboardingStore.swift +++ b/secant/Features/Onboarding/OnboardingStore.swift @@ -9,7 +9,14 @@ import Foundation import SwiftUI import ComposableArchitecture +typealias OnboardingViewStore = ViewStore + struct OnboardingState: Equatable { + enum Route: Equatable, CaseIterable { + case createNewWallet + case importExistingWallet + } + struct Step: Equatable, Identifiable { let id: UUID let title: LocalizedStringKey @@ -21,7 +28,8 @@ struct OnboardingState: Equatable { var steps: IdentifiedArrayOf = Self.onboardingSteps var index = 0 var skippedAtindex: Int? - + var route: Route? + var currentStep: Step { steps[index] } var isFinalStep: Bool { steps.count == index + 1 } var isInitialStep: Bool { index == 0 } @@ -32,20 +40,43 @@ struct OnboardingState: Equatable { guard index != 0 else { return .zero } return stepOffset * CGFloat(index) } + + /// Import Wallet + var importWalletState: ImportWalletState +} + +extension OnboardingViewStore { + func bindingForRoute(_ route: OnboardingState.Route) -> Binding { + self.binding( + get: { $0.route == route }, + send: { isActive in + return .updateRoute(isActive ? route : nil) + } + ) + } } enum OnboardingAction: Equatable { case next case back case skip + case updateRoute(OnboardingState.Route?) case createNewWallet case importExistingWallet + case importWallet(ImportWalletAction) } typealias OnboardingReducer = Reducer extension OnboardingReducer { - static let `default` = Reducer { state, action, _ in + static let `default` = OnboardingReducer.combine( + [ + onboardingReducer, + importWalletReducer + ] + ) + + private static let onboardingReducer = OnboardingReducer { state, action, _ in switch action { case .back: guard state.index > 0 else { return .none } @@ -68,11 +99,26 @@ extension OnboardingReducer { 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: OnboardingReducer = ImportWalletReducer.default.pullback( + state: \OnboardingState.importWalletState, + action: /OnboardingAction.importWallet, + environment: { _ in ImportWalletEnvironment.live } + ) } diff --git a/secant/Features/Onboarding/Views/Onboarding.swift b/secant/Features/Onboarding/Views/Onboarding.swift index c4295ea..b81cf50 100644 --- a/secant/Features/Onboarding/Views/Onboarding.swift +++ b/secant/Features/Onboarding/Views/Onboarding.swift @@ -93,7 +93,9 @@ struct Onboarding_Previews: PreviewProvider { Group { OnboardingView( store: Store( - initialState: OnboardingState(), + initialState: OnboardingState( + importWalletState: .placeholder + ), reducer: .default, environment: () ) diff --git a/secant/Generated/XCAssets+Generated.swift b/secant/Generated/XCAssets+Generated.swift index b064bd7..e65d9fb 100644 --- a/secant/Generated/XCAssets+Generated.swift +++ b/secant/Generated/XCAssets+Generated.swift @@ -92,6 +92,7 @@ internal enum Asset { internal static let body = ColorAsset(name: "Body") internal static let button = ColorAsset(name: "Button") internal static let heading = ColorAsset(name: "Heading") + internal static let importSeedEditor = ColorAsset(name: "ImportSeedEditor") internal static let medium = ColorAsset(name: "Medium") internal static let regular = ColorAsset(name: "Regular") internal static let secondaryButtonText = ColorAsset(name: "SecondaryButtonText") diff --git a/secant/Localizable.strings b/secant/Localizable.strings index b4ca205..4467270 100644 --- a/secant/Localizable.strings +++ b/secant/Localizable.strings @@ -41,14 +41,12 @@ "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."; +"importWallet.button.importPhrase" = "Import Recovery Phrase"; +"importWallet.button.importPrivateKey" = "Import a private or viewing key"; // MARK: - Common & Shared "Back" = "Back"; diff --git a/secant/Screens/Onboarding/OnboardingContentView.swift b/secant/Screens/Onboarding/OnboardingContentView.swift index 81e6e8f..1cb064b 100644 --- a/secant/Screens/Onboarding/OnboardingContentView.swift +++ b/secant/Screens/Onboarding/OnboardingContentView.swift @@ -127,7 +127,10 @@ extension OnboardingContentView { struct OnboardingContentView_Previews: PreviewProvider { static var previews: some View { let store = Store( - initialState: OnboardingState(index: 0), + initialState: OnboardingState( + index: 0, + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) diff --git a/secant/Screens/Onboarding/OnboardingFooterView.swift b/secant/Screens/Onboarding/OnboardingFooterView.swift index 8bb8b31..c3e9d71 100644 --- a/secant/Screens/Onboarding/OnboardingFooterView.swift +++ b/secant/Screens/Onboarding/OnboardingFooterView.swift @@ -23,12 +23,12 @@ struct OnboardingFooterView: View { viewStore.send(.createNewWallet) } } - .createButtonStyle + .activeButtonStyle .onboardingFooterButtonLayout() Button("onboarding.button.importWallet") { withAnimation(.easeInOut(duration: animationDuration)) { - viewStore.send(.createNewWallet) + viewStore.send(.importExistingWallet) } } .secondaryButtonStyle @@ -52,6 +52,17 @@ struct OnboardingFooterView: View { .padding(.vertical, 20) } } + .navigationLinkEmpty( + isActive: viewStore.bindingForRoute(.importExistingWallet), + destination: { + ImportWalletView( + store: store.scope( + state: \.importWalletState, + action: OnboardingAction.importWallet + ) + ) + } + ) } } } @@ -75,7 +86,10 @@ extension View { struct OnboardingFooterView_Previews: PreviewProvider { static var previews: some View { let store = Store( - initialState: OnboardingState(index: 3), + initialState: OnboardingState( + index: 3, + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) diff --git a/secant/Screens/Onboarding/OnboardingHeaderView.swift b/secant/Screens/Onboarding/OnboardingHeaderView.swift index 9df7987..a84f419 100644 --- a/secant/Screens/Onboarding/OnboardingHeaderView.swift +++ b/secant/Screens/Onboarding/OnboardingHeaderView.swift @@ -63,7 +63,10 @@ struct OnboardingHeaderView: View { struct OnboardingHeaderView_Previews: PreviewProvider { static var previews: some View { let store = Store( - initialState: OnboardingState(index: 0), + initialState: OnboardingState( + index: 0, + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) diff --git a/secant/Screens/Onboarding/OnboardingScreen.swift b/secant/Screens/Onboarding/OnboardingScreen.swift index b234617..2b623e4 100644 --- a/secant/Screens/Onboarding/OnboardingScreen.swift +++ b/secant/Screens/Onboarding/OnboardingScreen.swift @@ -10,7 +10,6 @@ import ComposableArchitecture struct OnboardingScreen: View { let store: Store - let animationDuration: CGFloat = 0.8 var body: some View { GeometryReader { proxy in @@ -42,6 +41,7 @@ struct OnboardingScreen: View { OnboardingFooterView(store: store) } } + .navigationBarHidden(true) .applyScreenBackground() } } @@ -50,7 +50,9 @@ struct OnboardingScreen_Previews: PreviewProvider { static var previews: some View { OnboardingScreen( store: Store( - initialState: OnboardingState(), + initialState: OnboardingState( + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) @@ -60,7 +62,9 @@ struct OnboardingScreen_Previews: PreviewProvider { OnboardingScreen( store: Store( - initialState: OnboardingState(), + initialState: OnboardingState( + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) @@ -70,7 +74,9 @@ struct OnboardingScreen_Previews: PreviewProvider { OnboardingScreen( store: Store( - initialState: OnboardingState(), + initialState: OnboardingState( + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) @@ -80,7 +86,9 @@ struct OnboardingScreen_Previews: PreviewProvider { OnboardingScreen( store: Store( - initialState: OnboardingState(), + initialState: OnboardingState( + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) @@ -90,7 +98,9 @@ struct OnboardingScreen_Previews: PreviewProvider { OnboardingScreen( store: Store( - initialState: OnboardingState(), + initialState: OnboardingState( + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) @@ -100,7 +110,9 @@ struct OnboardingScreen_Previews: PreviewProvider { OnboardingScreen( store: Store( - initialState: OnboardingState(), + initialState: OnboardingState( + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) diff --git a/secant/UIComponents/ProgressIndicators/OnboardingProgressIndicator.swift b/secant/UIComponents/ProgressIndicators/OnboardingProgressIndicator.swift index 7c6ab26..5e80831 100644 --- a/secant/UIComponents/ProgressIndicators/OnboardingProgressIndicator.swift +++ b/secant/UIComponents/ProgressIndicators/OnboardingProgressIndicator.swift @@ -9,7 +9,6 @@ import SwiftUI struct OnboardingProgressStyle: ProgressViewStyle { let height: CGFloat = 3 - let animation: Animation = .easeInOut let gradient = LinearGradient( colors: [ Asset.Colors.ProgressIndicator.gradientLeft.color, @@ -53,7 +52,7 @@ struct OnboardingProgressStyle: ProgressViewStyle { } } .frame(height: height) - .animation(animation) + // FIXME: .animation(.easeInOut) breaks the Onboarding UI when onAppear, fallback to .linear for now } } } diff --git a/secantTests/OnboardingTests/OnboardingStoreTests.swift b/secantTests/OnboardingTests/OnboardingStoreTests.swift index 3fea50f..0cf270b 100644 --- a/secantTests/OnboardingTests/OnboardingStoreTests.swift +++ b/secantTests/OnboardingTests/OnboardingStoreTests.swift @@ -12,7 +12,9 @@ import ComposableArchitecture class OnboardingStoreTests: XCTestCase { func testIncrementingOnboarding() { let store = TestStore( - initialState: OnboardingState(), + initialState: OnboardingState( + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) @@ -50,7 +52,10 @@ class OnboardingStoreTests: XCTestCase { func testIncrementingPastTotalStepsDoesNothing() { let store = TestStore( - initialState: OnboardingState(index: 3), + initialState: OnboardingState( + index: 3, + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) @@ -74,7 +79,10 @@ class OnboardingStoreTests: XCTestCase { func testDecrementingOnboarding() { let store = TestStore( - initialState: OnboardingState(index: 2), + initialState: OnboardingState( + index: 2, + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) @@ -102,7 +110,9 @@ class OnboardingStoreTests: XCTestCase { func testDecrementingPastFirstStepDoesNothing() { let store = TestStore( - initialState: OnboardingState(), + initialState: OnboardingState( + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () ) @@ -128,7 +138,10 @@ class OnboardingStoreTests: XCTestCase { let initialIndex = 1 let store = TestStore( - initialState: OnboardingState(index: initialIndex), + initialState: OnboardingState( + index: initialIndex, + importWalletState: .placeholder + ), reducer: OnboardingReducer.default, environment: () )