Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using different placeholder words in mnemonic #528

Merged
merged 44 commits into from
May 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
36ef40a
WIP
CyonAlexRDX May 24, 2023
373cabd
WIP
CyonAlexRDX May 24, 2023
2c271c7
WIP
CyonAlexRDX May 25, 2023
afed607
merge
CyonAlexRDX May 26, 2023
c8ba2fd
merge Import Mnemonic
CyonAlexRDX May 26, 2023
72b7f3e
WIP
CyonAlexRDX May 26, 2023
fdcd130
WIP
CyonAlexRDX May 26, 2023
8394a4c
WIP
CyonAlexRDX May 26, 2023
2fb8afc
WIP
CyonAlexRDX May 26, 2023
7f63a19
WIP
CyonAlexRDX May 26, 2023
581e7e2
WIP
CyonAlexRDX May 26, 2023
6fb9754
rename to seed phrases
CyonAlexRDX May 26, 2023
b226632
WIP
CyonAlexRDX May 26, 2023
cdfc64c
WIP
CyonAlexRDX May 26, 2023
bb1fbd0
WIP
CyonAlexRDX May 26, 2023
0803941
WIP
CyonAlexRDX May 26, 2023
10112e3
WIP
CyonAlexRDX May 26, 2023
3dfbeea
WIP
CyonAlexRDX May 26, 2023
377481e
WIP
CyonAlexRDX May 26, 2023
45326e1
Merge branch 'main' into ABW-1549_display_mnemonics
CyonAlexRDX May 26, 2023
bf508dd
WIP
CyonAlexRDX May 26, 2023
a15c54a
merge
CyonAlexRDX May 27, 2023
8ef1a8f
WIP
CyonAlexRDX May 27, 2023
2288b9f
WIP
CyonAlexRDX May 27, 2023
b62ec70
WIP
CyonAlexRDX May 27, 2023
facc63e
WIP
CyonAlexRDX May 27, 2023
10f4199
WIP
CyonAlexRDX May 27, 2023
bc03107
WIP
CyonAlexRDX May 27, 2023
158a621
WIP
CyonAlexRDX May 27, 2023
c41ad5c
WIP
CyonAlexRDX May 27, 2023
7a2d6db
WIP
CyonAlexRDX May 27, 2023
4940570
Using different placeholder words in mnemonic
CyonAlexRDX May 27, 2023
6a98fa2
WIP
CyonAlexRDX May 27, 2023
563ec89
WIP
CyonAlexRDX May 27, 2023
539a1fa
WIP
CyonAlexRDX May 27, 2023
05ff5f7
WIP
CyonAlexRDX May 28, 2023
1f84466
Merge branch 'main' into ABW-XYZ_unique_placeholder_words
CyonAlexRDX May 28, 2023
a032d20
WIP
CyonAlexRDX May 28, 2023
c2f10dc
WIP
CyonAlexRDX May 28, 2023
7dfc652
WIP
CyonAlexRDX May 28, 2023
a1f105d
WIP
CyonAlexRDX May 28, 2023
da4bc5d
WIP
CyonAlexRDX May 28, 2023
6e092d0
WIP
CyonAlexRDX May 28, 2023
9bd3ecd
WIP
CyonAlexRDX May 28, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions Sources/Core/DesignSystem/Components/AppTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,20 @@ public struct AppTextField<FocusValue: Hashable, Accessory: View, InnerAccessory
}
}

let primaryHeading: String?
public struct PrimaryHeading: Sendable, Hashable, ExpressibleByStringLiteral {
public let text: String
public let isProminent: Bool
public init(text: String, isProminent: Bool = true) {
self.text = text
self.isProminent = isProminent
}

public init(stringLiteral value: StringLiteralType) {
self.init(text: value)
}
}

let primaryHeading: PrimaryHeading?
let secondaryHeading: String?
let placeholder: String
let text: Binding<String>
Expand All @@ -29,7 +42,7 @@ public struct AppTextField<FocusValue: Hashable, Accessory: View, InnerAccessory
let innerAccesory: InnerAccessory

public init(
primaryHeading: String? = nil,
primaryHeading: PrimaryHeading? = nil,
secondaryHeading: String? = nil,
placeholder: String,
text: Binding<String>,
Expand All @@ -51,7 +64,7 @@ public struct AppTextField<FocusValue: Hashable, Accessory: View, InnerAccessory
}

public init(
primaryHeading: String? = nil,
primaryHeading: PrimaryHeading? = nil,
secondaryHeading: String? = nil,
placeholder: String,
text: Binding<String>,
Expand All @@ -76,9 +89,9 @@ public struct AppTextField<FocusValue: Hashable, Accessory: View, InnerAccessory
VStack(alignment: .leading, spacing: .small3) {
HStack(spacing: 0) {
if let primaryHeading {
Text(primaryHeading)
.textStyle(.body1HighImportance)
.foregroundColor(accentColor)
Text(primaryHeading.text)
.textStyle(primaryHeading.isProminent ? .body1HighImportance : .body2Regular)
.foregroundColor(primaryHeading.isProminent ? accentColor : .app.gray2)
.multilineTextAlignment(.leading)
}

Expand Down
4 changes: 2 additions & 2 deletions Sources/Cryptography/Mnemonic/BIP39/BIP39+WordList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ extension BIP39 {
public struct WordList: Sendable, Hashable {
public typealias Word = BIP39.Word
public let language: Language
private let indexToWord: [Word.Index: Word]
private let wordToIndex: [NonEmptyString: Word.Index]
public let indexToWord: [Word.Index: Word]
public let wordToIndex: [NonEmptyString: Word.Index]
Comment on lines +18 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't the first one basically the same as the words property? And the second one could be computed, using firstIndex(of:), right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first one is not ordered. I want an ordered set. I tried NonEmpty<OrderedDict, but that does not work because OrderedDict does not conform to Collection.

We have the two of them for fastest possible look up. We don't care about slightly more data, we care about performance. But it was many years ago (I lifted this in from some
Old repo of mine) since ago I wrote this code so yeah, there is room for improvements.


public let words: NonEmpty<OrderedSet<Word>>

Expand Down
3 changes: 2 additions & 1 deletion Sources/Features/AppFeature/App+Reducer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public struct App: Sendable, FeatureReducer {
}
}

@Dependency(\.continuousClock) var clock
@Dependency(\.errorQueue) var errorQueue
@Dependency(\.deviceFactorSourceClient) var deviceFactorSourceClient
@Dependency(\.appPreferencesClient) var appPreferencesClient
Expand Down Expand Up @@ -114,7 +115,7 @@ public struct App: Sendable, FeatureReducer {
if error is Profile.ProfileIsUsedOnAnotherDeviceError {
await send(.internal(.toOnboarding))
// A slight delay to allow any modal that may be shown to be dismissed.
try? await Task.sleep(for: .seconds(0.5))
try? await clock.sleep(for: .seconds(0.5))
}
await send(.internal(.displayErrorAlert(UserFacingError(error))))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ extension EditPersonaField {
public var body: some SwiftUI.View {
WithViewStore(store, observe: ViewState.init(state:), send: { .view($0) }) { viewStore in
AppTextField(
primaryHeading: viewStore.primaryHeading,
primaryHeading: .init(text: viewStore.primaryHeading),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking of prominence as a separate property, so that when it's omitted it doesn't affect the primaryHeading parameter, but if you prefer this way then sure.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did not wanna further increase property count in AppTextField. The whole thing is a mess anyway...

secondaryHeading: viewStore.secondaryHeading,
placeholder: "",
text: viewStore.validation(
Expand Down
221 changes: 132 additions & 89 deletions Sources/Features/ImportMnemonic/ImportMnemonic+View.swift
CyonAlexRDX marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,7 @@ extension ImportMnemonic.State {
isReadonlyMode: isReadonlyMode,
isHidingSecrets: isHidingSecrets,
rowCount: rowCount,
wordCount: wordCount.rawValue,
isAddRowButtonEnabled: isAddRowButtonEnabled,
isRemoveRowButtonEnabled: isRemoveRowButtonEnabled,
wordCount: wordCount,
completedWords: completedWords,
mnemonic: mnemonic,
bip39Passphrase: bip39Passphrase
Expand All @@ -24,20 +22,43 @@ extension ImportMnemonic.State {
}
}

// MARK: - ImportMnemonic.View
// MARK: - ImportMnemonic.ViewState
extension ImportMnemonic {
public struct ViewState: Equatable {
let isReadonlyMode: Bool
let isHidingSecrets: Bool
let rowCount: Int
let wordCount: Int
let isAddRowButtonEnabled: Bool
let isRemoveRowButtonEnabled: Bool
let wordCount: BIP39.WordCount
let completedWords: [BIP39.Word]
let mnemonic: Mnemonic?
let bip39Passphrase: String
}
}

extension ImportMnemonic.ViewState {
var isNonChecksummed: Bool {
mnemonic == nil && completedWords.count == wordCount.rawValue
}

var isAddRowButtonEnabled: Bool {
wordCount != .twentyFour
}

var isRemoveRowButtonEnabled: Bool {
wordCount != .twelve
}

var isShowingPassphrase: Bool {
!(isReadonlyMode && bip39Passphrase.isEmpty)
}

var isShowingChangeWordCountButtons: Bool {
!isReadonlyMode
}
}
CyonAlexRDX marked this conversation as resolved.
Show resolved Hide resolved

// MARK: - ImportMnemonic.View
extension ImportMnemonic {
@MainActor
public struct View: SwiftUI.View {
@Environment(\.scenePhase) var scenePhase
Expand All @@ -51,98 +72,22 @@ extension ImportMnemonic {
WithViewStore(store, observe: \.viewState, send: { .view($0) }) { viewStore in
ScrollView(showsIndicators: false) {
VStack(spacing: .large1) {
LazyVGrid(
columns: .init(
repeating: .init(.flexible()),
count: 3
)
) {
ForEachStore(
store.scope(state: \.words, action: { .child(.word(id: $0, child: $1)) }),
content: { importMnemonicWordStore in
VStack(spacing: 0) {
ImportMnemonicWord.View(store: importMnemonicWordStore)
Spacer(minLength: .medium2)
}
}
)
}
wordsGrid(with: viewStore)

if !viewStore.isReadonlyMode {
HStack {
Button {
viewStore.send(.removeRowButtonTapped)
} label: {
// FIXME: strings
HStack {
Text("Less words")
.foregroundColor(viewStore.isRemoveRowButtonEnabled ? .app.gray1 : .app.white)
Image(systemName: "text.badge.plus")
.foregroundColor(viewStore.isRemoveRowButtonEnabled ? .app.red1 : .app.white)
}
}
.controlState(viewStore.isRemoveRowButtonEnabled ? .enabled : .disabled)

Spacer(minLength: 0)

Button {
viewStore.send(.addRowButtonTapped)
} label: {
// FIXME: strings
HStack {
Text("More words")
.foregroundColor(viewStore.isAddRowButtonEnabled ? .app.gray1 : .app.white)
Image(systemName: "text.badge.plus")
.foregroundColor(viewStore.isAddRowButtonEnabled ? .app.green1 : .app.white)
}
}
.controlState(viewStore.isAddRowButtonEnabled ? .enabled : .disabled)
}
.buttonStyle(.secondaryRectangular)
if viewStore.isShowingChangeWordCountButtons {
changeWordCountButtons(with: viewStore)
}

if !(viewStore.isReadonlyMode && viewStore.bip39Passphrase.isEmpty) {
AppTextField(
// FIXME: strings
primaryHeading: "Passhprase",
placeholder: "Passphrase",
text: viewStore.binding(
get: \.bip39Passphrase,
send: { .passphraseChanged($0) }
),
// FIXME: strings
hint: viewStore.isReadonlyMode ? nil : .info("BIP39 Passphrase is often called a '25th word'.")
)
.disabled(viewStore.isReadonlyMode)
.autocorrectionDisabled()
if viewStore.isShowingPassphrase {
passphrase(with: viewStore)
}
}
.redacted(reason: .privacy, if: viewStore.isHidingSecrets)
.onChange(of: scenePhase) { newPhase in
viewStore.send(.scenePhase(newPhase))
}
.footer {
WithControlRequirements(
viewStore.mnemonic,
forAction: { viewStore.send(.continueButtonTapped($0)) }
) { action in
if !viewStore.isReadonlyMode {
if viewStore.mnemonic == nil, viewStore.completedWords.count == viewStore.wordCount {
// FIXME: strings
Text("Mnemonic not checksummed")
.foregroundColor(.app.red1)
}
// FIXME: strings
Button("Import mnemonic", action: action)
.buttonStyle(.primaryRectangular)
} else {
// FIXME: strings
Button("Done") {
viewStore.send(.doneViewing)
}
.buttonStyle(.primaryRectangular)
}
}
footer(with: viewStore)
}
}
.animation(.default, value: viewStore.wordCount)
Expand All @@ -156,6 +101,104 @@ extension ImportMnemonic {
}
}

extension ImportMnemonic.View {
@ViewBuilder
private func wordsGrid(with viewStore: ViewStoreOf<ImportMnemonic>) -> some SwiftUI.View {
LazyVGrid(
columns: .init(
repeating: .init(.flexible()),
count: 3
)
) {
ForEachStore(
store.scope(state: \.words, action: { .child(.word(id: $0, child: $1)) }),
content: { importMnemonicWordStore in
VStack(spacing: 0) {
ImportMnemonicWord.View(store: importMnemonicWordStore)
Spacer(minLength: .medium2)
}
}
)
}
}

@ViewBuilder
private func passphrase(with viewStore: ViewStoreOf<ImportMnemonic>) -> some SwiftUI.View {
AppTextField(
// FIXME: strings
primaryHeading: .init(text: "Passhprase", isProminent: false),
placeholder: "Passphrase",
text: viewStore.binding(
get: \.bip39Passphrase,
send: { .passphraseChanged($0) }
),
// FIXME: strings
hint: viewStore.isReadonlyMode ? nil : .info("BIP39 Passphrase is often called a '25th word'.")
)
.disabled(viewStore.isReadonlyMode)
.autocorrectionDisabled()
}

@ViewBuilder
private func changeWordCountButtons(with viewStore: ViewStoreOf<ImportMnemonic>) -> some SwiftUI.View {
HStack {
Button {
viewStore.send(.removeRowButtonTapped)
} label: {
// FIXME: strings
HStack {
Text("Less words")
.foregroundColor(viewStore.isRemoveRowButtonEnabled ? .app.gray1 : .app.white)
Image(systemName: "text.badge.plus")
.foregroundColor(viewStore.isRemoveRowButtonEnabled ? .app.red1 : .app.white)
}
}
.controlState(viewStore.isRemoveRowButtonEnabled ? .enabled : .disabled)

Spacer(minLength: 0)

Button {
viewStore.send(.addRowButtonTapped)
} label: {
// FIXME: strings
HStack {
Text("More words")
.foregroundColor(viewStore.isAddRowButtonEnabled ? .app.gray1 : .app.white)
Image(systemName: "text.badge.plus")
.foregroundColor(viewStore.isAddRowButtonEnabled ? .app.green1 : .app.white)
}
}
.controlState(viewStore.isAddRowButtonEnabled ? .enabled : .disabled)
}
.buttonStyle(.secondaryRectangular)
}

@ViewBuilder
private func footer(with viewStore: ViewStoreOf<ImportMnemonic>) -> some SwiftUI.View {
WithControlRequirements(
viewStore.mnemonic,
forAction: { viewStore.send(.continueButtonTapped($0)) }
) { action in
if !viewStore.isReadonlyMode {
if viewStore.isNonChecksummed {
// FIXME: strings
Text("Mnemonic not checksummed")
.foregroundColor(.app.red1)
}
// FIXME: strings
Button("Import mnemonic", action: action)
.buttonStyle(.primaryRectangular)
} else {
// FIXME: strings
Button("Done") {
viewStore.send(.doneViewing)
}
.buttonStyle(.primaryRectangular)
}
}
}
}

extension View {
/// Conditionally adds a reason to apply a redaction to this view hierarchy.
///
Expand Down
Loading