[#881] UI tweaks for the latest Zashi (#883)

- Balances tab name (from Details)
- Restore flow - birthday header + optional
- Swipe across tabs to change it
- Splash screen with bigger steps
- Restore flow seed screen, scrollable + nav bar fixes + scroll to see the seed field
- Restore seed box to have a placeholder
- Fix the suggestions for the restore seed editor
- Rectangle around recovery phrase editor is now a shape instead of a full background with padding 1
This commit is contained in:
Lukas Korba 2023-11-02 10:43:08 +01:00 committed by GitHub
parent 74a2936b26
commit 3fa10e5147
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 163 additions and 127 deletions

View File

@ -28,9 +28,10 @@ public struct ImportBirthdayView: View {
.foregroundColor(Asset.Colors.primary.color)
.minimumScaleFactor(0.3)
.multilineTextAlignment(.center)
.padding(.bottom, 10)
TextField(L10n.ImportWallet.optionalBirthday, text: viewStore.bindingForRedactableBirthday(viewStore.birthdayHeight))
Text(L10n.ImportWallet.optionalBirthday)
TextField("", text: viewStore.bindingForRedactableBirthday(viewStore.birthdayHeight))
.frame(height: 40)
.font(.custom(FontFamily.Archivo.semiBold.name, size: 25))
.keyboardType(.numberPad)

View File

@ -9,11 +9,17 @@ import SwiftUI
import ComposableArchitecture
import Generated
import UIComponents
import Utils
public struct ImportWalletView: View {
private enum InputID: Hashable {
case seed
}
var store: ImportWalletStore
@FocusState private var seedFieldFocused: Bool
@FocusState public var isFocused: Bool
@State private var message = ""
public init(store: ImportWalletStore) {
self.store = store
@ -21,62 +27,100 @@ public struct ImportWalletView: View {
public var body: some View {
ScrollView {
WithViewStore(store) { viewStore in
VStack(alignment: .center) {
ZashiIcon()
.padding(.vertical, 30)
Text(L10n.ImportWallet.description)
.font(.custom(FontFamily.Archivo.semiBold.name, size: 25))
.foregroundColor(Asset.Colors.primary.color)
.multilineTextAlignment(.center)
.padding(.bottom, 10)
Text(L10n.ImportWallet.message)
.font(.custom(FontFamily.Inter.medium.name, size: 14))
.foregroundColor(Asset.Colors.primary.color)
.multilineTextAlignment(.center)
.padding(.bottom, 20)
.padding(.horizontal, 10)
ImportSeedEditor(store: store)
.frame(minWidth: 270)
.frame(height: 215)
.focused($seedFieldFocused)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button(L10n.General.done.uppercased()) {
seedFieldFocused = false
ScrollViewReader { value in
WithViewStore(store) { viewStore in
VStack(alignment: .center) {
ZashiIcon()
.padding(.vertical, 30)
Text(L10n.ImportWallet.description)
.font(.custom(FontFamily.Archivo.semiBold.name, size: 25))
.foregroundColor(Asset.Colors.primary.color)
.multilineTextAlignment(.center)
.padding(.bottom, 10)
Text(L10n.ImportWallet.message)
.font(.custom(FontFamily.Inter.medium.name, size: 14))
.foregroundColor(Asset.Colors.primary.color)
.multilineTextAlignment(.center)
.padding(.bottom, 20)
.padding(.horizontal, 10)
TextEditor(text: $message)
.autocapitalization(.none)
.recoveryPhraseShape()
.frame(minWidth: 270)
.frame(height: 215)
.focused($isFocused)
.id(InputID.seed)
.toolbar {
ToolbarItemGroup(placement: .keyboard) {
Spacer()
Button(L10n.General.done.uppercased()) {
isFocused = false
}
.foregroundColor(Asset.Colors.primary.color)
.font(.custom(FontFamily.Inter.regular.name, size: 14))
}
.foregroundColor(Asset.Colors.primary.color)
.font(.custom(FontFamily.Inter.regular.name, size: 14))
}
.overlay {
if message.isEmpty {
HStack {
VStack {
Text(L10n.ImportWallet.enterPlaceholder)
.font(.custom(FontFamily.Inter.regular.name, size: 13))
.foregroundColor(Asset.Colors.suppressed72.color)
.onTapGesture {
isFocused = true
}
Spacer()
}
.padding(.top, 10)
Spacer()
}
.padding(.leading, 10)
} else {
EmptyView()
}
}
.onChange(of: message) { value in
viewStore.send(.seedPhraseInputChanged(RedactableString(message)))
}
.onChange(of: isFocused) { update in
withAnimation {
if update {
value.scrollTo(InputID.seed, anchor: .center)
}
}
}
Button(L10n.General.next.uppercased()) {
viewStore.send(.updateDestination(.birthday))
}
Button(L10n.General.next.uppercased()) {
viewStore.send(.updateDestination(.birthday))
.zcashStyle()
.frame(width: 236)
.disabled(!viewStore.isValidForm)
.padding(.top, 50)
}
.zcashStyle()
.frame(width: 236)
.disabled(!viewStore.isValidForm)
.padding(.top, 50)
.padding(.horizontal, 70)
.onAppear(perform: { viewStore.send(.onAppear) })
.navigationLinkEmpty(
isActive: viewStore.bindingForDestination(.birthday),
destination: { ImportBirthdayView(store: store) }
)
.alert(store: store.scope(
state: \.$alert,
action: { .alert($0) }
))
.zashiBack()
}
.applyScreenBackground()
.padding(.horizontal, 70)
.onAppear(perform: { viewStore.send(.onAppear) })
.navigationLinkEmpty(
isActive: viewStore.bindingForDestination(.birthday),
destination: { ImportBirthdayView(store: store) }
)
.alert(store: store.scope(
state: \.$alert,
action: { .alert($0) }
))
.zashiBack()
}
}
.padding(.vertical, 1)
.applyScreenBackground()
}
}

View File

@ -1,54 +0,0 @@
//
// ImportSeedEditor.swift
// secant-testnet
//
// Created by Lukáš Korba on 02/25/2022.
//
import SwiftUI
import ComposableArchitecture
import Generated
public struct ImportSeedEditor: View {
var store: ImportWalletStore
public var body: some View {
WithViewStore(store) { viewStore in
TextEditor(text: viewStore.bindingForRedactableSeedPhrase(viewStore.importedSeedPhrase))
.autocapitalization(.none)
.importSeedEditorModifier(Asset.Colors.primary.color)
}
}
}
struct ImportSeedEditorModifier: ViewModifier {
var backgroundColor = Color.white
func body(content: Content) -> some View {
content
.foregroundColor(Asset.Colors.primary.color)
.padding(1)
.background(backgroundColor)
}
}
extension View {
func importSeedEditorModifier(_ backgroundColor: Color = .white) -> some View {
modifier(ImportSeedEditorModifier(backgroundColor: backgroundColor))
}
}
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)
}
.previewLayout(.fixed(width: width + 50, height: height + 50))
}
}

View File

@ -32,7 +32,7 @@ public struct TabsReducer: ReducerProtocol {
case account = 0
case send
case receive
case details
case balances
public var title: String {
switch self {
@ -42,8 +42,8 @@ public struct TabsReducer: ReducerProtocol {
return L10n.Tabs.send
case .receive:
return L10n.Tabs.receive
case .details:
return L10n.Tabs.details
case .balances:
return L10n.Tabs.balances
}
}
}
@ -120,7 +120,7 @@ public struct TabsReducer: ReducerProtocol {
return .none
case .home(.balanceBreakdown):
state.selectedTab = .details
state.selectedTab = .balances
return .none
case .home:

View File

@ -62,8 +62,10 @@ public struct TabsView: View {
),
tokenName: tokenName
)
.tag(TabsReducer.State.Tab.details)
.tag(TabsReducer.State.Tab.balances)
}
.tabViewStyle(.page(indexDisplayMode: .never))
.padding(.bottom, 50)
VStack {
Spacer()
@ -122,7 +124,7 @@ public struct TabsView: View {
.resizable()
.frame(width: 62, height: 17)
case .details:
case .balances:
Text(L10n.Tabs.balances.uppercased())
.font(.custom(FontFamily.Archivo.bold.name, size: 14))
}

View File

@ -166,10 +166,12 @@ public enum L10n {
/// Enter secret
/// recovery phrase
public static let description = L10n.tr("Localizable", "importWallet.description", fallback: "Enter secret\nrecovery phrase")
/// Enter private seed here
public static let enterPlaceholder = L10n.tr("Localizable", "importWallet.enterPlaceholder", fallback: "Enter private seed here…")
/// Enter your 24-word seed phrase to restore the associated wallet.
public static let message = L10n.tr("Localizable", "importWallet.message", fallback: "Enter your 24-word seed phrase to restore the associated wallet.")
/// optional
public static let optionalBirthday = L10n.tr("Localizable", "importWallet.optionalBirthday", fallback: "optional")
/// (optional)
public static let optionalBirthday = L10n.tr("Localizable", "importWallet.optionalBirthday", fallback: "(optional)")
/// Wallet Import
public static let title = L10n.tr("Localizable", "importWallet.title", fallback: "Wallet Import")
public enum Alert {
@ -189,9 +191,8 @@ public enum L10n {
}
}
public enum Birthday {
/// Enter birthday
/// height
public static let title = L10n.tr("Localizable", "importWallet.birthday.title", fallback: "Enter birthday\nheight")
/// Wallet birthday height
public static let title = L10n.tr("Localizable", "importWallet.birthday.title", fallback: "Wallet birthday height")
}
public enum Button {
/// Restore
@ -647,8 +648,6 @@ public enum L10n {
public static let account = L10n.tr("Localizable", "tabs.account", fallback: "Account")
/// Balances
public static let balances = L10n.tr("Localizable", "tabs.balances", fallback: "Balances")
/// Details
public static let details = L10n.tr("Localizable", "tabs.details", fallback: "Details")
/// Receive
public static let receive = L10n.tr("Localizable", "tabs.receive", fallback: "Receive")
/// Send

View File

@ -72,19 +72,20 @@
"importWallet.description" = "Enter secret\nrecovery phrase";
"importWallet.message" = "Enter your 24-word seed phrase to restore the associated wallet.";
"importWallet.button.restoreWallet" = "Restore";
"importWallet.birthday.title" = "Enter birthday\nheight";
"importWallet.birthday.title" = "Wallet birthday height";
"importWallet.seed.valid" = "VALID SEED PHRASE";
"importWallet.alert.success.title" = "Success";
"importWallet.alert.success.message" = "The wallet has been successfully recovered.";
"importWallet.alert.failed.title" = "Failed to restore wallet";
"importWallet.alert.failed.message" = "Error: %@ (code: %@)";
"importWallet.optionalBirthday" = "optional";
"importWallet.optionalBirthday" = "(optional)";
"importWallet.enterPlaceholder" = "Enter private seed here…";
// MARK: - Tabs
"tabs.account" = "Account";
"tabs.send" = "Send";
"tabs.receive" = "Receive";
"tabs.details" = "Details";
"tabs.balances" = "Balances";
// MARK: - Home Screen

View File

@ -79,7 +79,7 @@ final class SplashManager: ObservableObject {
let y = screenSize.height + prevHeight
if (allPoints - i) % 2 == 0 {
prevHeight += CGFloat.random(in: 10...40)
prevHeight += CGFloat.random(in: 30...70)
}
points.append(CGPoint(x: x, y: y))

View File

@ -0,0 +1,43 @@
//
// RecoveryPhraseEditorShape.swift
//
//
// Created by Lukáš Korba on 30.10.2023.
//
import SwiftUI
import Generated
struct RecoveryPhraseEditorShape: Shape {
func path(in rect: CGRect) -> Path {
Path { path in
path.move(to: CGPoint(x: 0, y: 0))
path.addLine(to: CGPoint(x: 0, y: rect.height))
path.addLine(to: CGPoint(x: rect.width, y: rect.height))
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.closeSubpath()
}
}
}
struct RecoveryPhraseEditorModifier: ViewModifier {
func body(content: Content) -> some View {
content
.overlay {
RecoveryPhraseEditorShape()
.stroke()
}
}
}
extension View {
public func recoveryPhraseShape() -> some View {
modifier(RecoveryPhraseEditorModifier())
}
}
#Preview {
Text("some message")
.frame(width: 320, height: 145)
.recoveryPhraseShape()
}

View File

@ -22,7 +22,7 @@ class TabsTests: XCTestCase {
)
await store.send(.home(.balanceBreakdown)) { state in
state.selectedTab = .details
state.selectedTab = .balances
}
}
@ -97,12 +97,12 @@ class TabsTests: XCTestCase {
func testDetailsTabTitle() {
var tabsState = TabsReducer.State.placeholder
tabsState.selectedTab = .details
tabsState.selectedTab = .balances
XCTAssertEqual(
tabsState.selectedTab.title,
L10n.Tabs.details,
"Name of the details tab should be '\(L10n.Tabs.details)' but received \(tabsState.selectedTab.title)"
L10n.Tabs.balances,
"Name of the balances tab should be '\(L10n.Tabs.balances)' but received \(tabsState.selectedTab.title)"
)
}