diff --git a/RadixWallet/Core/DesignSystem/Components/AppTextField.swift b/RadixWallet/Core/DesignSystem/Components/AppTextField.swift index ca7bfd7e3c..21b59c7978 100644 --- a/RadixWallet/Core/DesignSystem/Components/AppTextField.swift +++ b/RadixWallet/Core/DesignSystem/Components/AppTextField.swift @@ -38,7 +38,7 @@ public struct AppTextField - let hint: Hint? + let hint: Hint.ViewState? let focus: Focus? let showClearButton: Bool let accessory: Accessory @@ -53,7 +53,7 @@ public struct AppTextField, - hint: Hint? = nil, + hint: Hint.ViewState? = nil, focus: Focus, showClearButton: Bool = false, @ViewBuilder accessory: () -> Accessory = { EmptyView() }, @@ -79,7 +79,7 @@ public struct AppTextField, - hint: Hint? = nil, + hint: Hint.ViewState? = nil, showClearButton: Bool = false, @ViewBuilder accessory: () -> Accessory = { EmptyView() }, @ViewBuilder innerAccessory: () -> InnerAccessory = { EmptyView() } @@ -99,7 +99,7 @@ public struct AppTextField Color { - switch hint?.viewState.kind { + switch hint?.kind { case .none, .info: isFocused ? .app.gray1 : .app.gray4 case .error: @@ -207,6 +209,10 @@ extension VerticalAlignment { fileprivate static let textFieldAlignment = VerticalAlignment(TextFieldAlignment.self) } +extension Constants { + static let appTextFieldSpacing: CGFloat = .small1 +} + #if DEBUG struct AppTextField_Previews: PreviewProvider { static var previews: some View { diff --git a/RadixWallet/Core/DesignSystem/Components/Hint.swift b/RadixWallet/Core/DesignSystem/Components/Hint.swift index 640b758534..66b79b9d0b 100644 --- a/RadixWallet/Core/DesignSystem/Components/Hint.swift +++ b/RadixWallet/Core/DesignSystem/Components/Hint.swift @@ -1,68 +1,64 @@ // MARK: - Hint public struct Hint: View, Equatable { - public struct ViewState: Equatable { + public struct ViewState: Sendable, Equatable { public let kind: Kind - public let text: Text? + public let text: AttributedString? - public init(kind: Kind, text: Text?) { + public init(kind: Kind, text: String?) { self.kind = kind - self.text = text + self.text = text.map { .init(stringLiteral: $0) } } - public init(kind: Kind, text: some StringProtocol) { + public init(kind: Kind, attributed: AttributedString?) { self.kind = kind - self.text = Text(text) + self.text = attributed + } + + public static func info(_ string: String) -> Self { + .init(kind: .info, text: string) + } + + public static func error(_ string: String) -> Self { + .init(kind: .error(imageSize: .smallest), text: string) + } + + public static func error() -> Self { + .init(kind: .error(imageSize: .smallest), text: nil) + } + + public static func iconError(_ string: String) -> Self { + .init(kind: .error(imageSize: .icon), text: string) + } + + public static func iconError() -> Self { + .init(kind: .error(imageSize: .icon), text: nil) } } - public enum Kind: Equatable { + public enum Kind: Sendable, Equatable { case info - case error + case error(imageSize: HitTargetSize) case warning case detail } public let viewState: ViewState - private init(kind: Kind, text: Text?) { - self.viewState = .init(kind: kind, text: text) - } - public init(viewState: ViewState) { self.viewState = viewState } - public static func info(_ text: () -> Text) -> Self { - .init(kind: .info, text: text()) - } - - public static func info(_ string: some StringProtocol) -> Self { - .init(kind: .info, text: Text(string)) - } - - public static func error(_ text: () -> Text) -> Self { - .init(kind: .error, text: text()) - } - - public static func error(_ string: some StringProtocol) -> Self { - .init(kind: .error, text: Text(string)) - } - - public static func error() -> Self { - .init(kind: .error, text: nil) - } - public var body: some View { if let text = viewState.text { HStack(spacing: .small3) { - if let iconResource { - Image(iconResource) + if let imageResource { + Image(imageResource) .renderingMode(.template) .resizable() .scaledToFit() - .frame(.smallest) + .frame(imageSize) } - text + Text(text) .lineSpacing(0) .textStyle(textStyle) } @@ -83,7 +79,7 @@ private extension Hint { } } - var iconResource: ImageResource? { + var imageResource: ImageResource? { switch viewState.kind { case .info, .detail: nil @@ -100,4 +96,13 @@ private extension Hint { .body1Regular } } + + var imageSize: HitTargetSize { + switch viewState.kind { + case .info, .detail, .warning: + .smallest + case let .error(imageSize): + imageSize + } + } } diff --git a/RadixWallet/Core/DesignSystem/HitTargetSize.swift b/RadixWallet/Core/DesignSystem/HitTargetSize.swift index f34c97d6fa..dddcdc6f56 100644 --- a/RadixWallet/Core/DesignSystem/HitTargetSize.swift +++ b/RadixWallet/Core/DesignSystem/HitTargetSize.swift @@ -1,5 +1,5 @@ // MARK: - HitTargetSize -public enum HitTargetSize: CGFloat { +public enum HitTargetSize: CGFloat, Sendable { /// 18 case icon = 18 diff --git a/RadixWallet/Features/AccountPreferencesFeature/Children/ThirdPartyDeposits/AddAssets+View.swift b/RadixWallet/Features/AccountPreferencesFeature/Children/ThirdPartyDeposits/AddAssets+View.swift index 3c3c05dca5..c5e0208538 100644 --- a/RadixWallet/Features/AccountPreferencesFeature/Children/ThirdPartyDeposits/AddAssets+View.swift +++ b/RadixWallet/Features/AccountPreferencesFeature/Children/ThirdPartyDeposits/AddAssets+View.swift @@ -15,11 +15,11 @@ extension AddAsset.State { case .valid: return .none case .wrongNetwork: - return .error(L10n.AssetTransfer.Error.wrongNetwork) + return .iconError(L10n.AssetTransfer.Error.wrongNetwork) case .alreadyAdded: - return .error(L10n.AssetTransfer.Error.resourceAlreadyAdded) + return .iconError(L10n.AssetTransfer.Error.resourceAlreadyAdded) case .invalid: - return .error(L10n.AssetTransfer.ChooseReceivingAccount.invalidAddressError) + return .iconError(L10n.AssetTransfer.ChooseReceivingAccount.invalidAddressError) } }(), resourceAddressFieldFocused: resourceAddressFieldFocused, @@ -32,7 +32,7 @@ extension AddAsset { public struct ViewState: Equatable { let resourceAddress: String let validatedResourceAddress: ResourceViewState.Address? - let addressHint: Hint? + let addressHint: Hint.ViewState? let resourceAddressFieldFocused: Bool let mode: ResourcesListMode } diff --git a/RadixWallet/Features/AccountPreferencesFeature/Children/UpdateAccountLabel+View.swift b/RadixWallet/Features/AccountPreferencesFeature/Children/UpdateAccountLabel+View.swift index 1555c6aefa..4d361b8f7e 100644 --- a/RadixWallet/Features/AccountPreferencesFeature/Children/UpdateAccountLabel+View.swift +++ b/RadixWallet/Features/AccountPreferencesFeature/Children/UpdateAccountLabel+View.swift @@ -13,13 +13,13 @@ extension UpdateAccountLabel.State { ) } - private var hintAndControlState: (ControlState, Hint?) { + private var hintAndControlState: (ControlState, Hint.ViewState?) { if let sanitizedName { if sanitizedName.count > Account.nameMaxLength { - return (.disabled, .error(L10n.Error.AccountLabel.tooLong)) + return (.disabled, .iconError(L10n.Error.AccountLabel.tooLong)) } } else { - return (.disabled, .error(L10n.Error.AccountLabel.missing)) + return (.disabled, .iconError(L10n.Error.AccountLabel.missing)) } return (.enabled, nil) @@ -31,7 +31,7 @@ extension UpdateAccountLabel { let accountLabel: String let sanitizedName: NonEmptyString? let updateButtonControlState: ControlState - let hint: Hint? + let hint: Hint.ViewState? let textFieldFocused: Bool } diff --git a/RadixWallet/Features/AccountPreferencesFeature/Common/PreferenceList.swift b/RadixWallet/Features/AccountPreferencesFeature/Common/PreferenceList.swift index e3155570bf..3dea6f0b90 100644 --- a/RadixWallet/Features/AccountPreferencesFeature/Common/PreferenceList.swift +++ b/RadixWallet/Features/AccountPreferencesFeature/Common/PreferenceList.swift @@ -100,7 +100,7 @@ struct PreferenceSection: View { guard let hint = row.hint else { return [] } - return [.init(kind: .detail, text: Text(hint))] + return [.init(kind: .detail, text: hint)] } } diff --git a/RadixWallet/Features/AssetTransferFeature/Components/ChooseAccount/ChooseReceivingAccount+View.swift b/RadixWallet/Features/AssetTransferFeature/Components/ChooseAccount/ChooseReceivingAccount+View.swift index e60bb6c972..72d0b3ab17 100644 --- a/RadixWallet/Features/AssetTransferFeature/Components/ChooseAccount/ChooseReceivingAccount+View.swift +++ b/RadixWallet/Features/AssetTransferFeature/Components/ChooseAccount/ChooseReceivingAccount+View.swift @@ -38,7 +38,7 @@ extension ChooseReceivingAccount.State { manualAccountAddress.isEmpty } - var manualAddressHint: Hint? { + var manualAddressHint: Hint.ViewState? { guard !manualAccountAddressFocused, !manualAccountAddress.isEmpty else { return .none } diff --git a/RadixWallet/Features/CreateAccount/Children/NameAccount/NameAccount+View.swift b/RadixWallet/Features/CreateAccount/Children/NameAccount/NameAccount+View.swift index f4e16fd3b5..572b961c16 100644 --- a/RadixWallet/Features/CreateAccount/Children/NameAccount/NameAccount+View.swift +++ b/RadixWallet/Features/CreateAccount/Children/NameAccount/NameAccount+View.swift @@ -14,7 +14,7 @@ extension NameAccount { public let subtitleText: String public let entityName: String public let sanitizedNameRequirement: SanitizedNameRequirement? - public let hint: Hint? + public let hint: Hint.ViewState? public let useLedgerAsFactorSource: Bool public struct SanitizedNameRequirement: Equatable { @@ -31,7 +31,7 @@ extension NameAccount { if let sanitizedName = state.sanitizedName { if sanitizedName.count > Account.nameMaxLength { self.sanitizedNameRequirement = nil - self.hint = .error(L10n.Error.AccountLabel.tooLong) + self.hint = .iconError(L10n.Error.AccountLabel.tooLong) } else { self.sanitizedNameRequirement = .init(sanitizedName: sanitizedName) self.hint = nil diff --git a/RadixWallet/Features/DappInteractionFeature/Children/PersonaDataPermission/PersonaDataPermissionBox+View.swift b/RadixWallet/Features/DappInteractionFeature/Children/PersonaDataPermission/PersonaDataPermissionBox+View.swift index 62285ad557..ad13350e51 100644 --- a/RadixWallet/Features/DappInteractionFeature/Children/PersonaDataPermission/PersonaDataPermissionBox+View.swift +++ b/RadixWallet/Features/DappInteractionFeature/Children/PersonaDataPermission/PersonaDataPermissionBox+View.swift @@ -16,23 +16,31 @@ extension PersonaDataPermissionBox.State { .keys .nilIfEmpty .map { kinds in - .error { - Text { - L10n.DAppRequest.PersonalDataBox.requiredInformation.text.bold() - " " - kinds.sorted().map(\.title.localizedLowercase).joined(separator: ", ") - } - } + let items = kinds.sorted().map(\.title.localizedLowercase).joined(separator: ", ") + return try? AttributedString(markdown: "**\(L10n.DAppRequest.PersonalDataBox.requiredInformation)** \(items)") + } + .map { + .init(kind: .error(imageSize: .icon), attributed: $0) } ) } + + private var missingEntries: String? { + responseValidation.missingEntries + .keys + .nilIfEmpty + .map { kinds in + let items = kinds.sorted().map(\.title.localizedLowercase).joined(separator: ", ") + return items + } + } } extension PersonaDataPermissionBox { struct ViewState: Equatable { let personaLabel: String let existingRequiredEntries: String? - let missingRequiredEntries: Hint? + let missingRequiredEntries: Hint.ViewState? } @MainActor @@ -77,7 +85,9 @@ extension PersonaDataPermissionBox { .textStyle(.body2Regular) } - viewStore.missingRequiredEntries + if let viewState = viewStore.missingRequiredEntries { + Hint(viewState: viewState) + } Button(L10n.DAppRequest.PersonalDataBox.edit) { viewStore.send(.editButtonTapped) diff --git a/RadixWallet/Features/EditPersonaFeature/EditPersonaField/EditPersonaField+View.swift b/RadixWallet/Features/EditPersonaFeature/EditPersonaField/EditPersonaField+View.swift index 2616bdd24b..7e4c72d6be 100644 --- a/RadixWallet/Features/EditPersonaFeature/EditPersonaField/EditPersonaField+View.swift +++ b/RadixWallet/Features/EditPersonaFeature/EditPersonaField/EditPersonaField+View.swift @@ -6,9 +6,9 @@ extension EditPersonaField.State { showsTitle ? behaviour.title : nil } - var inputHint: Hint? { + var inputHint: Hint.ViewState? { if let error = $input.errors?.first { - return .error(error) + return .iconError(error) } else if let defaultInfoHint { return .info(defaultInfoHint) } diff --git a/RadixWallet/Features/GatewaySettingsFeature/Children/AddNewGateway/AddNewGateway+View.swift b/RadixWallet/Features/GatewaySettingsFeature/Children/AddNewGateway/AddNewGateway+View.swift index b15dec7c3f..657bad620a 100644 --- a/RadixWallet/Features/GatewaySettingsFeature/Children/AddNewGateway/AddNewGateway+View.swift +++ b/RadixWallet/Features/GatewaySettingsFeature/Children/AddNewGateway/AddNewGateway+View.swift @@ -2,8 +2,8 @@ import ComposableArchitecture import SwiftUI extension AddNewGateway.State { - var fieldHint: Hint? { - errorText.map(Hint.error) + var fieldHint: Hint.ViewState? { + errorText.map(Hint.ViewState.iconError) } } diff --git a/RadixWallet/Features/ImportMnemonic/ImportMnemonic+View.swift b/RadixWallet/Features/ImportMnemonic/ImportMnemonic+View.swift index e0e2a76ed8..064570224f 100644 --- a/RadixWallet/Features/ImportMnemonic/ImportMnemonic+View.swift +++ b/RadixWallet/Features/ImportMnemonic/ImportMnemonic+View.swift @@ -6,7 +6,7 @@ extension ImportMnemonic.State { var viewState: ImportMnemonic.ViewState { var viewState = ImportMnemonic.ViewState( readonlyMode: mode.readonly?.context, - hideAdvancedMode: mode.write?.hideAdvancedMode ?? false, + hideAdvancedMode: hideAdvancedMode, showCloseButton: showCloseButton, isProgressing: mode.write?.isProgressing ?? false, isWordCountFixed: isWordCountFixed, @@ -34,6 +34,15 @@ extension ImportMnemonic.State { } } + private var hideAdvancedMode: Bool { + switch mode { + case .readonly: + true + case .write: + isWordCountFixed + } + } + var rowCount: Int { words.count / ImportMnemonic.wordsPerRow } @@ -113,17 +122,13 @@ extension ImportMnemonic { public var body: some SwiftUI.View { WithViewStore(store, observe: \.viewState, send: { .view($0) }) { viewStore in ScrollView(showsIndicators: false) { - VStack(spacing: 0) { + VStack(spacing: .medium3) { if let header = viewStore.header { HeaderView(header: header) - .padding(.bottom, viewStore.isWordCountFixed ? .medium3 : 0) } if let warning = viewStore.warning { WarningErrorView(text: warning, type: .warning) - .padding(.top, viewStore.header == nil ? .medium3 : 0) - .padding(.horizontal, .large3) - .padding(.bottom, .large3) } if !viewStore.isWordCountFixed { @@ -144,8 +149,6 @@ extension ImportMnemonic { } .pickerStyle(.segmented) } - .padding(.horizontal, .large3) - .padding(.bottom, .medium2) } #if DEBUG @@ -153,13 +156,10 @@ extension ImportMnemonic { #endif wordsGrid(with: viewStore) - .padding(.horizontal, .medium2) - .padding(.bottom, .large3) + .padding(.vertical, .small1) if viewStore.isShowingPassphrase { passphrase(with: viewStore) - .padding(.horizontal, .medium2) - .padding(.bottom, .medium2) } if viewStore.showModeButton { @@ -168,26 +168,26 @@ extension ImportMnemonic { } .buttonStyle(.blueText) .frame(height: .large1) - .padding(.bottom, .medium1) + .padding(.bottom, .small2) } footer(with: viewStore) - .padding(.bottom, .medium2) } - .navigationBarBackButtonHidden(viewStore.showBackButton || viewStore.showCloseButton) // need to be able to hook "back" button press - .toolbar { - if viewStore.showBackButton { - ToolbarItem(placement: .navigationBarLeading) { - BackButton { - viewStore.send(.backButtonTapped) - } + .padding(.medium3) + } + .navigationBarBackButtonHidden(viewStore.showBackButton || viewStore.showCloseButton) // need to be able to hook "back" button press + .toolbar { + if viewStore.showBackButton { + ToolbarItem(placement: .navigationBarLeading) { + BackButton { + viewStore.send(.backButtonTapped) } } - if viewStore.showCloseButton { - ToolbarItem(placement: .navigationBarLeading) { - CloseButton { - viewStore.send(.closeButtonTapped) - } + } + if viewStore.showCloseButton { + ToolbarItem(placement: .navigationBarLeading) { + CloseButton { + viewStore.send(.closeButtonTapped) } } } @@ -206,10 +206,9 @@ extension ImportMnemonic { let header: State.Header var body: some SwiftUI.View { - VStack(spacing: 0) { + VStack(spacing: .large2) { Text(header.title) .textStyle(.sheetTitle) - .padding(.bottom, .large2) if let subtitle = header.subtitle { Text(subtitle) @@ -218,7 +217,7 @@ extension ImportMnemonic { } .foregroundColor(.app.gray1) .multilineTextAlignment(.center) - .padding(.horizontal, .large3) + .padding(.horizontal, .small1) } } } @@ -294,7 +293,6 @@ extension ImportMnemonic.View { viewStore.send(.doneViewing) } .buttonStyle(.primaryRectangular) - .padding(.horizontal, .medium2) } else { WithControlRequirements( viewStore.mnemonic, @@ -307,77 +305,67 @@ extension ImportMnemonic.View { .buttonStyle(.primaryRectangular) } .controlState(viewStore.isProgressing ? .loading(.local) : .enabled) - .padding(.horizontal, .medium2) } } #if DEBUG - @ViewBuilder private func debugSection(with viewStore: ViewStoreOf) -> some View { - if viewStore.isReadonlyMode { - Button("DEBUG ONLY Copy") { - viewStore.send(.debugCopyMnemonic) - } - .buttonStyle(.secondaryRectangular(shouldExpand: true, isDestructive: true, isInToolbar: true)) - .padding(.horizontal, .medium2) - .padding(.bottom, .medium3) - } else { - if !(viewStore.isWordCountFixed && viewStore.wordCount == .twentyFour) { - Button("DEBUG AccRecScan Olympia 15") { - viewStore.send(.debugUseOlympiaTestingMnemonicWithActiveAccounts(continue: true)) + VStack(spacing: .medium3) { + if viewStore.isReadonlyMode { + Button("DEBUG ONLY Copy") { + viewStore.send(.debugCopyMnemonic) + } + .buttonStyle(.secondaryRectangular(shouldExpand: true, isDestructive: true, isInToolbar: true)) + } else { + if !(viewStore.isWordCountFixed && viewStore.wordCount == .twentyFour) { + Button("DEBUG AccRecScan Olympia 15") { + viewStore.send(.debugUseOlympiaTestingMnemonicWithActiveAccounts(continue: true)) + } + .buttonStyle(.secondaryRectangular(shouldExpand: true, isDestructive: true, isInToolbar: true)) + .overlay(alignment: .trailing) { + Button("M") { + viewStore.send(.debugUseOlympiaTestingMnemonicWithActiveAccounts(continue: false)) + } + .frame(width: 40) + } + } + + Button("DEBUG AccRecScan Babylon 24") { + viewStore.send(.debugUseBabylonTestingMnemonicWithActiveAccounts(continue: true)) } .buttonStyle(.secondaryRectangular(shouldExpand: true, isDestructive: true, isInToolbar: true)) .overlay(alignment: .trailing) { Button("M") { - viewStore.send(.debugUseOlympiaTestingMnemonicWithActiveAccounts(continue: false)) + viewStore.send(.debugUseBabylonTestingMnemonicWithActiveAccounts(continue: false)) } .frame(width: 40) } - .padding(.horizontal, .medium2) - .padding(.bottom, .medium3) - } - Button("DEBUG AccRecScan Babylon 24") { - viewStore.send(.debugUseBabylonTestingMnemonicWithActiveAccounts(continue: true)) - } - .buttonStyle(.secondaryRectangular(shouldExpand: true, isDestructive: true, isInToolbar: true)) - .overlay(alignment: .trailing) { - Button("M") { - viewStore.send(.debugUseBabylonTestingMnemonicWithActiveAccounts(continue: false)) + Button("DEBUG zoo..vote (24)") { + viewStore.send(.debugUseTestingMnemonicZooVote(continue: true)) } - .frame(width: 40) - } - .padding(.horizontal, .medium2) - .padding(.bottom, .medium3) - - Button("DEBUG zoo..vote (24)") { - viewStore.send(.debugUseTestingMnemonicZooVote(continue: true)) - } - .buttonStyle(.secondaryRectangular(shouldExpand: true, isDestructive: true, isInToolbar: true)) - .overlay(alignment: .trailing) { - Button("M") { - viewStore.send(.debugUseTestingMnemonicZooVote(continue: false)) - } - .frame(width: 40) - } - .padding(.horizontal, .medium2) - .padding(.bottom, .medium3) - - AppTextField( - placeholder: "DEBUG ONLY paste mnemonic", - text: viewStore.binding( - get: { $0.debugMnemonicPhraseSingleField }, - send: { .debugMnemonicChanged($0) } - ), - innerAccessory: { - Button("Paste") { - viewStore.send(.debugPasteMnemonic) + .buttonStyle(.secondaryRectangular(shouldExpand: true, isDestructive: true, isInToolbar: true)) + .overlay(alignment: .trailing) { + Button("M") { + viewStore.send(.debugUseTestingMnemonicZooVote(continue: false)) } - .buttonStyle(.borderedProminent) + .frame(width: 40) } - ) - .padding(.horizontal, .medium2) - .padding(.bottom, .medium2) + + AppTextField( + placeholder: "DEBUG ONLY paste mnemonic", + text: viewStore.binding( + get: { $0.debugMnemonicPhraseSingleField }, + send: { .debugMnemonicChanged($0) } + ), + innerAccessory: { + Button("Paste") { + viewStore.send(.debugPasteMnemonic) + } + .buttonStyle(.borderedProminent) + } + ) + } } } #endif diff --git a/RadixWallet/Features/ImportMnemonic/ImportMnemonic.swift b/RadixWallet/Features/ImportMnemonic/ImportMnemonic.swift index c480cef284..e4a2c7a288 100644 --- a/RadixWallet/Features/ImportMnemonic/ImportMnemonic.swift +++ b/RadixWallet/Features/ImportMnemonic/ImportMnemonic.swift @@ -10,8 +10,6 @@ public struct ImportMnemonic: Sendable, FeatureReducer { public typealias Words = IdentifiedArrayOf public var words: Words - public var idOfWordWithTextFieldFocus: ImportMnemonicWord.State.ID? - public var language: BIP39Language public var wordCount: BIP39WordCount { guard let wordCount = BIP39WordCount(wordCount: words.count) else { @@ -29,11 +27,6 @@ public struct ImportMnemonic: Sendable, FeatureReducer { words.append(contentsOf: (wordCount ..< Int(newWordCount.rawValue)).map { .init( id: $0, - placeholder: ImportMnemonic.placeholder( - index: $0, - wordCount: newWordCount, - language: language - ), isReadonlyMode: mode.readonly != nil ) }) @@ -89,7 +82,6 @@ public struct ImportMnemonic: Sendable, FeatureReducer { public struct WriteMode: Sendable, Hashable { public var isProgressing: Bool public let persistStrategy: PersistStrategy? - public let hideAdvancedMode: Bool public let showCloseButton: Bool } @@ -169,10 +161,9 @@ public struct ImportMnemonic: Sendable, FeatureReducer { public init( header: Header? = nil, warning: String? = nil, - hideAdvancedMode: Bool = false, showCloseButton: Bool = false, warningOnContinue: OnContinueWarning? = nil, - isWordCountFixed: Bool = false, + isWordCountFixed: Bool, persistStrategy: PersistStrategy?, language: BIP39Language = .english, wordCount: BIP39WordCount = .twelve, @@ -184,7 +175,6 @@ public struct ImportMnemonic: Sendable, FeatureReducer { .init( isProgressing: false, persistStrategy: persistStrategy, - hideAdvancedMode: hideAdvancedMode, showCloseButton: showCloseButton ) ) @@ -232,11 +222,6 @@ public struct ImportMnemonic: Sendable, FeatureReducer { word: $0.element, completion: .auto(match: .exact) ), - placeholder: ImportMnemonic.placeholder( - index: $0.offset, - wordCount: mnemonic.wordCount, - language: mnemonic.language - ), isReadonlyMode: isReadonlyMode ) } @@ -281,10 +266,8 @@ public struct ImportMnemonic: Sendable, FeatureReducer { public let savedIntoProfile: Bool } - case focusNext(ImportMnemonicWord.State.ID) - case saveFactorSourceResult( - TaskResult - ) + case focusOn(ImportMnemonicWord.State.ID) + case saveFactorSourceResult(TaskResult) } public enum ChildAction: Sendable, Equatable { @@ -404,6 +387,9 @@ public struct ImportMnemonic: Sendable, FeatureReducer { &state ) + case let .word(id, child: .delegate(.didSubmit)): + return focusNext(&state, after: id) + default: return .none } @@ -412,7 +398,7 @@ public struct ImportMnemonic: Sendable, FeatureReducer { public func reduce(into state: inout State, viewAction: ViewAction) -> Effect { switch viewAction { case .appeared: - return focusNext(&state) + return focusNext(&state, after: nil) case let .passphraseChanged(passphrase): state.bip39Passphrase = passphrase @@ -535,8 +521,7 @@ public struct ImportMnemonic: Sendable, FeatureReducer { public func reduce(into state: inout State, internalAction: InternalAction) -> Effect { switch internalAction { - case let .focusNext(id): - state.idOfWordWithTextFieldFocus = id + case let .focusOn(id): state.words[id: id]?.focus() return .none @@ -616,7 +601,7 @@ extension ImportMnemonic { _ state: inout State ) -> Effect { state.words[id: id]?.value = .complete(text: input, word: word, completion: completion) - return focusNext(&state) + return .none } private func updateWord( @@ -662,48 +647,15 @@ extension ImportMnemonic { } } - private func focusNext(_ state: inout State) -> Effect { - if let current = state.idOfWordWithTextFieldFocus { + private func focusNext(_ state: inout State, after current: Int?) -> Effect { + if let current { state.words[id: current]?.resignFocus() - } - guard let nextID = state.words.first(where: { !$0.isComplete })?.id else { + return delayedEffect(delay: .milliseconds(75), for: .internal(.focusOn(current + 1))) + } else if let firstIncomplete = state.words.first(where: { !$0.isComplete })?.id { + return delayedEffect(delay: .milliseconds(75), for: .internal(.focusOn(firstIncomplete))) + } else { return .none } - - return .run { send in - try? await clock.sleep(for: .milliseconds(75)) - await send(.internal(.focusNext(nextID))) - } - } -} - -extension ImportMnemonic { - static func placeholder( - index: Int, - wordCount: BIP39WordCount, - language: BIP39Language - ) -> String { - precondition(index <= 23, "Invalid BIP39 word index, got index: \(index), exected less than 24.") - let word: BIP39Word = { - let wordList = language.wordlist() // BIP39.wordList(for: language) - switch language { - case .english: - let bip39Alphabet = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", /* X is missing */ "y", "z"] - return wordList - // we use `last` simply because we did not like the words "abandon baby" - // which we get by using `first`, too sad a combination. - .last( - where: { $0.word.hasPrefix(bip39Alphabet[index]) } - )! - - default: - let scale = UInt16(89) // 2048 / 23 - let indexScaled = U11(inner: scale * UInt16(index)) - return wordList.first(where: { $0.index == indexScaled })! - } - - }() - return word.word } } diff --git a/RadixWallet/Features/ImportMnemonic/ImportWord/ImportMnemonicWord+View.swift b/RadixWallet/Features/ImportMnemonic/ImportWord/ImportMnemonicWord+View.swift index b4b936a494..c1539d31f8 100644 --- a/RadixWallet/Features/ImportMnemonic/ImportWord/ImportMnemonicWord+View.swift +++ b/RadixWallet/Features/ImportMnemonic/ImportWord/ImportMnemonicWord+View.swift @@ -7,7 +7,6 @@ extension ImportMnemonicWord.State { .init( isReadonlyMode: isReadonlyMode, index: id, - placeholder: placeholder, displayText: value.text, autocompletionCandidates: autocompletionCandidates, focusedField: focusedField, @@ -37,18 +36,17 @@ extension ImportMnemonicWord { public struct ViewState: Equatable { let isReadonlyMode: Bool let index: Int - let placeholder: String let displayText: String let autocompletionCandidates: ImportMnemonicWord.State.AutocompletionCandidates? let focusedField: State.Field? let validation: MnemonicValidation? - var hint: Hint? { + var hint: Hint.ViewState? { guard let validation, validation == .invalid else { return nil } - return .error(L10n.Common.invalid) + return .iconError(L10n.Common.invalid) } var showClearButton: Bool { @@ -71,24 +69,23 @@ extension ImportMnemonicWord { public var body: some SwiftUI.View { WithViewStore(store, observe: \.viewState, send: { .view($0) }) { viewStore in - VStack(spacing: .small3) { + VStack(spacing: Constants.appTextFieldSpacing) { AppTextField( primaryHeading: .init(text: L10n.ImportMnemonic.wordHeading(viewStore.index + 1), isProminent: true), - placeholder: viewStore.placeholder, + placeholder: "", text: .init( get: { viewStore.displayText }, set: { viewStore.send(.wordChanged(input: $0.lowercased().trimmingWhitespacesAndNewlines())) } ), hint: viewStore.hint, - // FIXME: Bring back autofocus - // focus: .on( - // .textField, - // binding: viewStore.binding( - // get: \.focusedField, - // send: { .textFieldFocused($0) } - // ), - // to: $focusedField - // ), + focus: .on( + State.Field.textField, + binding: viewStore.binding( + get: \.focusedField, + send: ViewAction.focusChanged + ), + to: $focusedField + ), showClearButton: viewStore.showClearButton, innerAccessory: { if viewStore.displayValidAccessory { @@ -98,7 +95,7 @@ extension ImportMnemonicWord { } } ) - .disabled(viewStore.isReadonlyMode) + .allowsHitTesting(!viewStore.isReadonlyMode) .minimumScaleFactor(0.9) .keyboardType(.alphabet) .textInputAutocapitalization(.never) @@ -126,9 +123,13 @@ extension ImportMnemonicWord { } } } + .submitLabel(.next) + .onSubmit { + viewStore.send(.onSubmit) + } if viewStore.hint == nil { - Hint.error(L10n.Common.invalid) // Dummy spacer + Hint(viewState: .iconError(L10n.Common.invalid)) // Dummy spacer .opacity(0) } } diff --git a/RadixWallet/Features/ImportMnemonic/ImportWord/ImportMnemonicWord.swift b/RadixWallet/Features/ImportMnemonic/ImportWord/ImportMnemonicWord.swift index 3100015fa4..48255e9f63 100644 --- a/RadixWallet/Features/ImportMnemonic/ImportWord/ImportMnemonicWord.swift +++ b/RadixWallet/Features/ImportMnemonic/ImportWord/ImportMnemonicWord.swift @@ -69,7 +69,6 @@ public struct ImportMnemonicWord: Sendable, FeatureReducer { public typealias ID = Int public let id: ID public var value: WordValue - public let placeholder: String public let isReadonlyMode: Bool public var autocompletionCandidates: AutocompletionCandidates? = nil @@ -78,12 +77,10 @@ public struct ImportMnemonicWord: Sendable, FeatureReducer { public init( id: ID, value: WordValue = .incomplete(text: "", hasFailedValidation: false), - placeholder: String, isReadonlyMode: Bool ) { self.id = id self.value = value - self.placeholder = placeholder self.isReadonlyMode = isReadonlyMode } @@ -110,13 +107,15 @@ public struct ImportMnemonicWord: Sendable, FeatureReducer { public enum ViewAction: Sendable, Hashable { case wordChanged(input: String) case userSelectedCandidate(BIP39Word) - case textFieldFocused(State.Field?) + case focusChanged(State.Field?) + case onSubmit } public enum DelegateAction: Sendable, Hashable { case lookupWord(input: String) case lostFocus(displayText: String) case userSelectedCandidate(BIP39Word, fromPartial: String) + case didSubmit } public init() {} @@ -158,9 +157,12 @@ public struct ImportMnemonicWord: Sendable, FeatureReducer { fromPartial: state.value.text ))) - case let .textFieldFocused(field): + case let .focusChanged(field): state.focusedField = field - return field == nil ? .send(.delegate(.lostFocus(displayText: state.value.text))) : .none + return .none + + case .onSubmit: + return .send(.delegate(.didSubmit)) } } } diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts+View.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts+View.swift index 9118b0c050..2b750177a2 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts+View.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/ImportSeedPhrasesFlow/ImportMnemonicControllingAccounts+View.swift @@ -109,6 +109,7 @@ private extension View { private func importMnemonic(with destinationStore: PresentationStoreOf) -> some View { sheet(store: destinationStore.scope(state: \.importMnemonic, action: \.importMnemonic)) { store in ImportMnemonic.View(store: store) + // TODO: Consider moving this into the view that should always set toolbar instead of using header .radixToolbar(title: L10n.EnterSeedPhrase.Header.title, alwaysVisible: false) .inNavigationStack } @@ -121,24 +122,3 @@ private extension View { } } } - -// #if DEBUG -// import SwiftUI -import ComposableArchitecture // - -//// MARK: - ImportMnemonicControllingAccounts_Preview -// struct ImportMnemonicControllingAccounts_Preview: PreviewProvider { -// static var previews: some View { -// ImportMnemonicControllingAccounts.View( -// store: .init( -// initialState: .previewValue, -// reducer: ImportMnemonicControllingAccounts.init -// ) -// ) -// } -// } -// -// extension ImportMnemonicControllingAccounts.State { -// public static let previewValue = Self() -// } -// #endif diff --git a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/RecoverWalletWithoutProfile/Coordinator/RecoverWalletWithoutProfileCoordinator.swift b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/RecoverWalletWithoutProfile/Coordinator/RecoverWalletWithoutProfileCoordinator.swift index de6815b1bf..8cb78c60b1 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/RecoverWalletWithoutProfile/Coordinator/RecoverWalletWithoutProfileCoordinator.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/RestoreProfileFromBackup/Children/RecoverWalletWithoutProfile/Coordinator/RecoverWalletWithoutProfileCoordinator.swift @@ -119,7 +119,6 @@ public struct RecoverWalletWithoutProfileCoordinator: Sendable, FeatureReducer { .init( header: .init(title: L10n.EnterSeedPhrase.Header.titleMain), warning: L10n.EnterSeedPhrase.warning, - hideAdvancedMode: true, // we require 24 word Babylon mnemonic isWordCountFixed: true, persistStrategy: nil, diff --git a/RadixWallet/Features/ProfileBackupsFeature/Shared/EncryptOrDecryptProfile+View.swift b/RadixWallet/Features/ProfileBackupsFeature/Shared/EncryptOrDecryptProfile+View.swift index 276526879f..c8f8a9ec0b 100644 --- a/RadixWallet/Features/ProfileBackupsFeature/Shared/EncryptOrDecryptProfile+View.swift +++ b/RadixWallet/Features/ProfileBackupsFeature/Shared/EncryptOrDecryptProfile+View.swift @@ -39,13 +39,13 @@ extension EncryptOrDecryptProfile { return confirmedEncryptionPassword == enteredEncryptionPassword } - var confirmHint: Hint? { + var confirmHint: Hint.ViewState? { guard needToConfirm else { return nil } if enteredEncryptionPassword.isEmpty || !confirmedEncryptionPassword.isEmpty && focusedField != .confirmPassword { return nil } if !confirmedEncryptionPassword.isEmpty, confirmedEncryptionPassword != enteredEncryptionPassword { - return .error(L10n.ProfileBackup.ManualBackups.passwordsMissmatchError) + return .iconError(L10n.ProfileBackup.ManualBackups.passwordsMissmatchError) } return nil diff --git a/RadixWallet/Features/SettingsFeature/DebugSettings/Children/DebugManageFactorSourcesFeature/DebugManageFactorSources.swift b/RadixWallet/Features/SettingsFeature/DebugSettings/Children/DebugManageFactorSourcesFeature/DebugManageFactorSources.swift index 7dfbd989f6..98e22dc717 100644 --- a/RadixWallet/Features/SettingsFeature/DebugSettings/Children/DebugManageFactorSourcesFeature/DebugManageFactorSources.swift +++ b/RadixWallet/Features/SettingsFeature/DebugSettings/Children/DebugManageFactorSourcesFeature/DebugManageFactorSources.swift @@ -76,6 +76,7 @@ public struct DebugManageFactorSources: Sendable, FeatureReducer { case .importOlympiaMnemonicButtonTapped: state.destination = .importMnemonic( .init( + isWordCountFixed: false, persistStrategy: .init( factorSourceKindOfMnemonic: .olympia, location: .intoKeychainAndProfile, diff --git a/RadixWallet/Features/TransactionReviewFeature/CustomizeFees/AdvancedFeesCustomization+View.swift b/RadixWallet/Features/TransactionReviewFeature/CustomizeFees/AdvancedFeesCustomization+View.swift index 6a7e75c084..6ee23a166e 100644 --- a/RadixWallet/Features/TransactionReviewFeature/CustomizeFees/AdvancedFeesCustomization+View.swift +++ b/RadixWallet/Features/TransactionReviewFeature/CustomizeFees/AdvancedFeesCustomization+View.swift @@ -13,14 +13,14 @@ extension AdvancedFeesCustomization.State { ) } - private var paddingAmountHint: Hint? { + private var paddingAmountHint: Hint.ViewState? { guard parsedPaddingFee == nil else { return nil } - return .error() + return .iconError() } - private var tipPercentageHint: Hint? { + private var tipPercentageHint: Hint.ViewState? { guard parsedTipPercentage == nil else { return nil } - return .error() + return .iconError() } } @@ -29,9 +29,9 @@ extension AdvancedFeesCustomization { let feesViewState: FeesView.ViewState let paddingAmount: String - let paddingAmountHint: Hint? + let paddingAmountHint: Hint.ViewState? let tipPercentage: String - let tipPercentageHint: Hint? + let tipPercentageHint: Hint.ViewState? let focusField: State.FocusField? } diff --git a/RadixWalletTests/Features/ImportMnemonicTests.swift b/RadixWalletTests/Features/ImportMnemonicTests.swift index 84fee9a2bd..33fa93f6e4 100644 --- a/RadixWalletTests/Features/ImportMnemonicTests.swift +++ b/RadixWalletTests/Features/ImportMnemonicTests.swift @@ -27,10 +27,10 @@ final class ImportMnemonicTests: TestCase { func test_addi_erase_results_in_add_not_addict() async throws { let mnemonic = try Mnemonic(phrase: "add addict address pen penalty pencil act action actor actress zoo wreck", language: .english) let wordsBIP39 = mnemonic.words - let wordStrings = wordsBIP39.map(\.word) let testClock = TestClock() let store = TestStore( initialState: ImportMnemonic.State( + isWordCountFixed: false, persistStrategy: nil, wordCount: .twelve ) @@ -95,6 +95,7 @@ extension ImportMnemonicTests { let testClock = TestClock() let store = TestStore( initialState: ImportMnemonic.State( + isWordCountFixed: false, persistStrategy: nil, wordCount: mnemonic.wordCount )