Merge branch 'main' into 147_phrase_preamble_screen

This commit is contained in:
Lukas Korba 2022-03-02 10:27:29 +01:00
commit 28c7cf98b5
16 changed files with 394 additions and 36 deletions

View File

@ -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
}
}

View File

@ -113,7 +113,9 @@ extension AppState {
static var placeholder: Self { static var placeholder: Self {
.init( .init(
homeState: .placeholder, homeState: .placeholder,
onboardingState: .init(), onboardingState: .init(
importWalletState: .placeholder
),
phraseValidationState: RecoveryPhraseValidationState.placeholder, phraseValidationState: RecoveryPhraseValidationState.placeholder,
phraseDisplayState: RecoveryPhraseDisplayState( phraseDisplayState: RecoveryPhraseDisplayState(
phrase: .placeholder phrase: .placeholder

View File

@ -20,12 +20,15 @@ struct AppView: View {
.navigationViewStyle(StackNavigationViewStyle()) .navigationViewStyle(StackNavigationViewStyle())
case .onboarding: case .onboarding:
OnboardingScreen( NavigationView {
store: store.scope( OnboardingScreen(
state: \.onboardingState, store: store.scope(
action: AppAction.onboarding state: \.onboardingState,
action: AppAction.onboarding
)
) )
) }
.navigationViewStyle(StackNavigationViewStyle())
case .startup: case .startup:
ZStack(alignment: .topTrailing) { ZStack(alignment: .topTrailing) {

View File

@ -0,0 +1,47 @@
//
// ImportWalletStore.swift
// secant-testnet
//
// Created by Lukáš Korba on 02/25/2022.
//
import ComposableArchitecture
typealias ImportWalletStore = Store<ImportWalletState, ImportWalletAction>
struct ImportWalletState: Equatable {
@BindableState var importedSeedPhrase: String = ""
}
enum ImportWalletAction: Equatable, BindableAction {
case importRecoveryPhrase
case importPrivateOrViewingKey
case binding(BindingAction<ImportWalletState>)
}
struct ImportWalletEnvironment { }
extension ImportWalletEnvironment {
static let demo = Self()
static let live = Self()
}
typealias ImportWalletReducer = Reducer<ImportWalletState, ImportWalletAction, ImportWalletEnvironment>
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()
}

View File

@ -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))
}
}

View File

@ -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)
}
}

View File

@ -9,7 +9,14 @@ import Foundation
import SwiftUI import SwiftUI
import ComposableArchitecture import ComposableArchitecture
typealias OnboardingViewStore = ViewStore<OnboardingState, OnboardingAction>
struct OnboardingState: Equatable { struct OnboardingState: Equatable {
enum Route: Equatable, CaseIterable {
case createNewWallet
case importExistingWallet
}
struct Step: Equatable, Identifiable { struct Step: Equatable, Identifiable {
let id: UUID let id: UUID
let title: LocalizedStringKey let title: LocalizedStringKey
@ -21,7 +28,8 @@ struct OnboardingState: Equatable {
var steps: IdentifiedArrayOf<Step> = Self.onboardingSteps var steps: IdentifiedArrayOf<Step> = Self.onboardingSteps
var index = 0 var index = 0
var skippedAtindex: Int? var skippedAtindex: Int?
var route: Route?
var currentStep: Step { steps[index] } var currentStep: Step { steps[index] }
var isFinalStep: Bool { steps.count == index + 1 } var isFinalStep: Bool { steps.count == index + 1 }
var isInitialStep: Bool { index == 0 } var isInitialStep: Bool { index == 0 }
@ -32,20 +40,43 @@ struct OnboardingState: Equatable {
guard index != 0 else { return .zero } guard index != 0 else { return .zero }
return stepOffset * CGFloat(index) return stepOffset * CGFloat(index)
} }
/// Import Wallet
var importWalletState: ImportWalletState
}
extension OnboardingViewStore {
func bindingForRoute(_ route: OnboardingState.Route) -> Binding<Bool> {
self.binding(
get: { $0.route == route },
send: { isActive in
return .updateRoute(isActive ? route : nil)
}
)
}
} }
enum OnboardingAction: Equatable { enum OnboardingAction: Equatable {
case next case next
case back case back
case skip case skip
case updateRoute(OnboardingState.Route?)
case createNewWallet case createNewWallet
case importExistingWallet case importExistingWallet
case importWallet(ImportWalletAction)
} }
typealias OnboardingReducer = Reducer<OnboardingState, OnboardingAction, Void> typealias OnboardingReducer = Reducer<OnboardingState, OnboardingAction, Void>
extension OnboardingReducer { extension OnboardingReducer {
static let `default` = Reducer<OnboardingState, OnboardingAction, Void> { state, action, _ in static let `default` = OnboardingReducer.combine(
[
onboardingReducer,
importWalletReducer
]
)
private static let onboardingReducer = OnboardingReducer { state, action, _ in
switch action { switch action {
case .back: case .back:
guard state.index > 0 else { return .none } guard state.index > 0 else { return .none }
@ -68,11 +99,26 @@ extension OnboardingReducer {
state.index = state.steps.count - 1 state.index = state.steps.count - 1
return .none return .none
case .updateRoute(let route):
state.route = route
return .none
case .createNewWallet: case .createNewWallet:
state.route = .createNewWallet
return .none return .none
case .importExistingWallet: case .importExistingWallet:
state.route = .importExistingWallet
return .none
case .importWallet(let route):
return .none return .none
} }
} }
private static let importWalletReducer: OnboardingReducer = ImportWalletReducer.default.pullback(
state: \OnboardingState.importWalletState,
action: /OnboardingAction.importWallet,
environment: { _ in ImportWalletEnvironment.live }
)
} }

View File

@ -93,7 +93,9 @@ struct Onboarding_Previews: PreviewProvider {
Group { Group {
OnboardingView( OnboardingView(
store: Store( store: Store(
initialState: OnboardingState(), initialState: OnboardingState(
importWalletState: .placeholder
),
reducer: .default, reducer: .default,
environment: () environment: ()
) )

View File

@ -92,6 +92,7 @@ internal enum Asset {
internal static let body = ColorAsset(name: "Body") internal static let body = ColorAsset(name: "Body")
internal static let button = ColorAsset(name: "Button") internal static let button = ColorAsset(name: "Button")
internal static let heading = ColorAsset(name: "Heading") internal static let heading = ColorAsset(name: "Heading")
internal static let importSeedEditor = ColorAsset(name: "ImportSeedEditor")
internal static let medium = ColorAsset(name: "Medium") internal static let medium = ColorAsset(name: "Medium")
internal static let regular = ColorAsset(name: "Regular") internal static let regular = ColorAsset(name: "Regular")
internal static let secondaryButtonText = ColorAsset(name: "SecondaryButtonText") internal static let secondaryButtonText = ColorAsset(name: "SecondaryButtonText")

View File

@ -41,14 +41,12 @@
"validationFailed.description" = "Your placed words did not match your secret recovery phrase."; "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.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"; "validationFailed.button.tryAgain" = "I'm ready to try again";
// MARK: - Recovery Phrase Test Preamble // MARK: - Import Wallet Screen
"recoveryPhraseTestPreamble.title" = "First things first"; "importWallet.title" = "Wallet Import";
"recoveryPhraseTestPreamble.paragraph1" = "It is important to understand that you are in charge here. Great, right? YOU get to be the bank!"; "importWallet.description" = "You can import your backed up wallet by entering your backup recovery phrase (aka seed phrase) now.";
"recoveryPhraseTestPreamble.paragraph2" = "But it also means that YOU are the customer, and you need to be self-reliant."; "importWallet.button.importPhrase" = "Import Recovery Phrase";
"recoveryPhraseTestPreamble.paragraph3" = "So how do you recover funds that you've hidden on a completely decentralized and private block-chain?"; "importWallet.button.importPrivateKey" = "Import a private or viewing key";
"recoveryPhraseTestPreamble.button.goNext" = "By understanding and preparing";
// MARK: - Common & Shared // MARK: - Common & Shared
"Back" = "Back"; "Back" = "Back";

View File

@ -127,7 +127,10 @@ extension OnboardingContentView {
struct OnboardingContentView_Previews: PreviewProvider { struct OnboardingContentView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let store = Store( let store = Store(
initialState: OnboardingState(index: 0), initialState: OnboardingState(
index: 0,
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )

View File

@ -23,12 +23,12 @@ struct OnboardingFooterView: View {
viewStore.send(.createNewWallet) viewStore.send(.createNewWallet)
} }
} }
.createButtonStyle .activeButtonStyle
.onboardingFooterButtonLayout() .onboardingFooterButtonLayout()
Button("onboarding.button.importWallet") { Button("onboarding.button.importWallet") {
withAnimation(.easeInOut(duration: animationDuration)) { withAnimation(.easeInOut(duration: animationDuration)) {
viewStore.send(.createNewWallet) viewStore.send(.importExistingWallet)
} }
} }
.secondaryButtonStyle .secondaryButtonStyle
@ -52,6 +52,17 @@ struct OnboardingFooterView: View {
.padding(.vertical, 20) .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 { struct OnboardingFooterView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let store = Store<OnboardingState, OnboardingAction>( let store = Store<OnboardingState, OnboardingAction>(
initialState: OnboardingState(index: 3), initialState: OnboardingState(
index: 3,
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )

View File

@ -63,7 +63,10 @@ struct OnboardingHeaderView: View {
struct OnboardingHeaderView_Previews: PreviewProvider { struct OnboardingHeaderView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
let store = Store<OnboardingState, OnboardingAction>( let store = Store<OnboardingState, OnboardingAction>(
initialState: OnboardingState(index: 0), initialState: OnboardingState(
index: 0,
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )

View File

@ -10,7 +10,6 @@ import ComposableArchitecture
struct OnboardingScreen: View { struct OnboardingScreen: View {
let store: Store<OnboardingState, OnboardingAction> let store: Store<OnboardingState, OnboardingAction>
let animationDuration: CGFloat = 0.8
var body: some View { var body: some View {
GeometryReader { proxy in GeometryReader { proxy in
@ -42,6 +41,7 @@ struct OnboardingScreen: View {
OnboardingFooterView(store: store) OnboardingFooterView(store: store)
} }
} }
.navigationBarHidden(true)
.applyScreenBackground() .applyScreenBackground()
} }
} }
@ -50,7 +50,9 @@ struct OnboardingScreen_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
OnboardingScreen( OnboardingScreen(
store: Store( store: Store(
initialState: OnboardingState(), initialState: OnboardingState(
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
@ -60,7 +62,9 @@ struct OnboardingScreen_Previews: PreviewProvider {
OnboardingScreen( OnboardingScreen(
store: Store( store: Store(
initialState: OnboardingState(), initialState: OnboardingState(
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
@ -70,7 +74,9 @@ struct OnboardingScreen_Previews: PreviewProvider {
OnboardingScreen( OnboardingScreen(
store: Store( store: Store(
initialState: OnboardingState(), initialState: OnboardingState(
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
@ -80,7 +86,9 @@ struct OnboardingScreen_Previews: PreviewProvider {
OnboardingScreen( OnboardingScreen(
store: Store( store: Store(
initialState: OnboardingState(), initialState: OnboardingState(
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
@ -90,7 +98,9 @@ struct OnboardingScreen_Previews: PreviewProvider {
OnboardingScreen( OnboardingScreen(
store: Store( store: Store(
initialState: OnboardingState(), initialState: OnboardingState(
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
@ -100,7 +110,9 @@ struct OnboardingScreen_Previews: PreviewProvider {
OnboardingScreen( OnboardingScreen(
store: Store( store: Store(
initialState: OnboardingState(), initialState: OnboardingState(
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )

View File

@ -9,7 +9,6 @@ import SwiftUI
struct OnboardingProgressStyle: ProgressViewStyle { struct OnboardingProgressStyle: ProgressViewStyle {
let height: CGFloat = 3 let height: CGFloat = 3
let animation: Animation = .easeInOut
let gradient = LinearGradient( let gradient = LinearGradient(
colors: [ colors: [
Asset.Colors.ProgressIndicator.gradientLeft.color, Asset.Colors.ProgressIndicator.gradientLeft.color,
@ -53,7 +52,7 @@ struct OnboardingProgressStyle: ProgressViewStyle {
} }
} }
.frame(height: height) .frame(height: height)
.animation(animation) // FIXME: .animation(.easeInOut) breaks the Onboarding UI when onAppear, fallback to .linear for now
} }
} }
} }

View File

@ -12,7 +12,9 @@ import ComposableArchitecture
class OnboardingStoreTests: XCTestCase { class OnboardingStoreTests: XCTestCase {
func testIncrementingOnboarding() { func testIncrementingOnboarding() {
let store = TestStore( let store = TestStore(
initialState: OnboardingState(), initialState: OnboardingState(
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
@ -50,7 +52,10 @@ class OnboardingStoreTests: XCTestCase {
func testIncrementingPastTotalStepsDoesNothing() { func testIncrementingPastTotalStepsDoesNothing() {
let store = TestStore( let store = TestStore(
initialState: OnboardingState(index: 3), initialState: OnboardingState(
index: 3,
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
@ -74,7 +79,10 @@ class OnboardingStoreTests: XCTestCase {
func testDecrementingOnboarding() { func testDecrementingOnboarding() {
let store = TestStore( let store = TestStore(
initialState: OnboardingState(index: 2), initialState: OnboardingState(
index: 2,
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
@ -102,7 +110,9 @@ class OnboardingStoreTests: XCTestCase {
func testDecrementingPastFirstStepDoesNothing() { func testDecrementingPastFirstStepDoesNothing() {
let store = TestStore( let store = TestStore(
initialState: OnboardingState(), initialState: OnboardingState(
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )
@ -128,7 +138,10 @@ class OnboardingStoreTests: XCTestCase {
let initialIndex = 1 let initialIndex = 1
let store = TestStore( let store = TestStore(
initialState: OnboardingState(index: initialIndex), initialState: OnboardingState(
index: initialIndex,
importWalletState: .placeholder
),
reducer: OnboardingReducer.default, reducer: OnboardingReducer.default,
environment: () environment: ()
) )