diff --git a/apple/Sources/Sargon/Extensions/Methods/AppearanceID+Wrap+Functions.swift b/apple/Sources/Sargon/Extensions/Methods/AppearanceID+Wrap+Functions.swift
new file mode 100644
index 000000000..c2295d447
--- /dev/null
+++ b/apple/Sources/Sargon/Extensions/Methods/AppearanceID+Wrap+Functions.swift
@@ -0,0 +1,5 @@
+extension AppearanceID: CaseIterable {
+ public static var allCases: [Self] {
+ appearanceIdsAll()
+ }
+}
diff --git a/apple/Sources/Sargon/Extensions/Methods/BagOfBytes+Wrap+Functions.swift b/apple/Sources/Sargon/Extensions/Methods/BagOfBytes+Wrap+Functions.swift
new file mode 100644
index 000000000..89f567b45
--- /dev/null
+++ b/apple/Sources/Sargon/Extensions/Methods/BagOfBytes+Wrap+Functions.swift
@@ -0,0 +1,5 @@
+extension BagOfBytes {
+ public init(data: Data) {
+ self = newBagOfBytesFrom(bytes: data)
+ }
+}
diff --git a/apple/Sources/Sargon/Extensions/Methods/Decimal192+Methods.swift b/apple/Sources/Sargon/Extensions/Methods/Decimal192+Wrap+Functions.swift
similarity index 100%
rename from apple/Sources/Sargon/Extensions/Methods/Decimal192+Methods.swift
rename to apple/Sources/Sargon/Extensions/Methods/Decimal192+Wrap+Functions.swift
diff --git a/apple/Sources/Sargon/Extensions/Methods/DisplayName+Wrap+Functions.swift b/apple/Sources/Sargon/Extensions/Methods/DisplayName+Wrap+Functions.swift
new file mode 100644
index 000000000..2fdc40db2
--- /dev/null
+++ b/apple/Sources/Sargon/Extensions/Methods/DisplayName+Wrap+Functions.swift
@@ -0,0 +1,5 @@
+extension DisplayName {
+ public init(validating name: String) throws {
+ self = try newDisplayName(name: name)
+ }
+}
diff --git a/apple/Sources/Sargon/Extensions/Methods/Mnemonic+Wrap+Functions.swift b/apple/Sources/Sargon/Extensions/Methods/Mnemonic+Wrap+Functions.swift
new file mode 100644
index 000000000..b32fe1f5c
--- /dev/null
+++ b/apple/Sources/Sargon/Extensions/Methods/Mnemonic+Wrap+Functions.swift
@@ -0,0 +1,5 @@
+extension Mnemonic {
+ public var phrase: String {
+ mnemonicPhrase(from: self)
+ }
+}
diff --git a/apple/Sources/Sargon/Extensions/Methods/SecureStorageKey+Methods.swift b/apple/Sources/Sargon/Extensions/Methods/SecureStorageKey+Wrap+Functions.swift
similarity index 100%
rename from apple/Sources/Sargon/Extensions/Methods/SecureStorageKey+Methods.swift
rename to apple/Sources/Sargon/Extensions/Methods/SecureStorageKey+Wrap+Functions.swift
diff --git a/apple/Sources/Sargon/Extensions/Swiftified/AppearanceID+Swiftified.swift b/apple/Sources/Sargon/Extensions/Swiftified/AppearanceID+Swiftified.swift
index 3251092d3..d4deaca59 100644
--- a/apple/Sources/Sargon/Extensions/Swiftified/AppearanceID+Swiftified.swift
+++ b/apple/Sources/Sargon/Extensions/Swiftified/AppearanceID+Swiftified.swift
@@ -1,8 +1,14 @@
public typealias AppearanceID = AppearanceId
extension AppearanceID: Sendable {}
-extension AppearanceID: CaseIterable {
- public static var allCases: [Self] {
- appearanceIdsAll()
+extension AppearanceID: Identifiable {
+ public typealias ID = UInt8
+ public var id: ID {
+ value
+ }
+}
+extension AppearanceID: CustomStringConvertible {
+ public var description: String {
+ value.description
}
}
diff --git a/apple/Sources/Sargon/Extensions/Swiftified/BagOfBytes+Random.swift b/apple/Sources/Sargon/Extensions/Swiftified/BagOfBytes+Random.swift
index c44318e59..d67975f15 100644
--- a/apple/Sources/Sargon/Extensions/Swiftified/BagOfBytes+Random.swift
+++ b/apple/Sources/Sargon/Extensions/Swiftified/BagOfBytes+Random.swift
@@ -1,7 +1,4 @@
extension BagOfBytes {
- public init(data: Data) {
- self = newBagOfBytesFrom(bytes: data)
- }
public static func random(byteCount: Int) -> Self {
var data = Data(repeating: 0, count: byteCount)
data.withUnsafeMutableBytes {
diff --git a/apple/Sources/Sargon/Extensions/Swiftified/DisplayName+Swiftified.swift b/apple/Sources/Sargon/Extensions/Swiftified/DisplayName+Swiftified.swift
index 144d13308..830bd0373 100644
--- a/apple/Sources/Sargon/Extensions/Swiftified/DisplayName+Swiftified.swift
+++ b/apple/Sources/Sargon/Extensions/Swiftified/DisplayName+Swiftified.swift
@@ -1,9 +1,4 @@
extension DisplayName: Sendable {}
-extension DisplayName {
- public init(validating name: String) throws {
- self = try newDisplayName(name: name)
- }
-}
#if DEBUG
extension DisplayName: ExpressibleByStringLiteral {
diff --git a/apple/Sources/Sargon/Extensions/Swiftified/Mnemonic+Swiftified.swift b/apple/Sources/Sargon/Extensions/Swiftified/Mnemonic+Swiftified.swift
new file mode 100644
index 000000000..e360de9b2
--- /dev/null
+++ b/apple/Sources/Sargon/Extensions/Swiftified/Mnemonic+Swiftified.swift
@@ -0,0 +1 @@
+extension Mnemonic: Sendable {}
diff --git a/apple/Sources/UniFFI/Sargon.swift b/apple/Sources/UniFFI/Sargon.swift
index 149bd440a..d354b503a 100644
--- a/apple/Sources/UniFFI/Sargon.swift
+++ b/apple/Sources/UniFFI/Sargon.swift
@@ -740,6 +740,18 @@ public protocol WalletProtocol: AnyObject {
*/
func jsonSnapshot() -> String
+ /**
+ * Tries to load the `MnemonicWithPassphrase` for the main "Babylon"
+ * `DeviceFactorSource` from secure storage.
+ */
+ func mainBdfsMnemonicWithPassphrase() throws -> MnemonicWithPassphrase
+
+ /**
+ * Tries to load a `MnemonicWithPassphrase` from secure storage
+ * by `factor_source_id`.
+ */
+ func mnemonicWithPassphraseOfDeviceFactorSourceByFactorSourceId(factorSourceId: FactorSourceId) throws -> MnemonicWithPassphrase
+
/**
* Clone the profile and return it.
*/
@@ -881,6 +893,31 @@ public class Wallet:
)
}
+ /**
+ * Tries to load the `MnemonicWithPassphrase` for the main "Babylon"
+ * `DeviceFactorSource` from secure storage.
+ */
+ public func mainBdfsMnemonicWithPassphrase() throws -> MnemonicWithPassphrase {
+ return try FfiConverterTypeMnemonicWithPassphrase.lift(
+ rustCallWithError(FfiConverterTypeCommonError.lift) {
+ uniffi_sargon_fn_method_wallet_main_bdfs_mnemonic_with_passphrase(self.uniffiClonePointer(), $0)
+ }
+ )
+ }
+
+ /**
+ * Tries to load a `MnemonicWithPassphrase` from secure storage
+ * by `factor_source_id`.
+ */
+ public func mnemonicWithPassphraseOfDeviceFactorSourceByFactorSourceId(factorSourceId: FactorSourceId) throws -> MnemonicWithPassphrase {
+ return try FfiConverterTypeMnemonicWithPassphrase.lift(
+ rustCallWithError(FfiConverterTypeCommonError.lift) {
+ uniffi_sargon_fn_method_wallet_mnemonic_with_passphrase_of_device_factor_source_by_factor_source_id(self.uniffiClonePointer(),
+ FfiConverterTypeFactorSourceID.lower(factorSourceId), $0)
+ }
+ )
+ }
+
/**
* Clone the profile and return it.
*/
@@ -10307,6 +10344,12 @@ private var initializationResult: InitializationResult {
if uniffi_sargon_checksum_method_wallet_json_snapshot() != 24850 {
return InitializationResult.apiChecksumMismatch
}
+ if uniffi_sargon_checksum_method_wallet_main_bdfs_mnemonic_with_passphrase() != 59906 {
+ return InitializationResult.apiChecksumMismatch
+ }
+ if uniffi_sargon_checksum_method_wallet_mnemonic_with_passphrase_of_device_factor_source_by_factor_source_id() != 48090 {
+ return InitializationResult.apiChecksumMismatch
+ }
if uniffi_sargon_checksum_method_wallet_profile() != 5221 {
return InitializationResult.apiChecksumMismatch
}
diff --git a/examples/iOS/Planbok.xcworkspace/contents.xcworkspacedata b/examples/iOS/Planbok.xcworkspace/contents.xcworkspacedata
index 362494596..b5b8e572b 100644
--- a/examples/iOS/Planbok.xcworkspace/contents.xcworkspacedata
+++ b/examples/iOS/Planbok.xcworkspace/contents.xcworkspacedata
@@ -22,12 +22,24 @@
name = "Extensions">
+ name = "WrapFunctions">
+ location = "group:/Users/sajjon/Developer/Radix/sargon/apple/Sources/Sargon/Extensions/Methods/SecureStorageKey+Wrap+Functions.swift">
+ location = "group:/Users/sajjon/Developer/Radix/sargon/apple/Sources/Sargon/Extensions/Methods/Decimal192+Wrap+Functions.swift">
+
+
+
+
+
+
+
+
+
+
()
+ public var nameAccount: NameNewAccountFeature.State
+
+ public init(walletHolder: WalletHolder) {
+ self.walletHolder = walletHolder
+ self.nameAccount = NameNewAccountFeature.State(walletHolder: walletHolder)
+ }
+
+ public init(wallet: Wallet) {
+ self.init(walletHolder: .init(wallet: wallet))
+ }
+ }
+
+ public enum Action {
+ public enum DelegateAction {
+ case createdAccount
+ }
+ case path(StackAction)
+ case nameAccount(NameNewAccountFeature.Action)
+ case delegate(DelegateAction)
+ }
+
+ public struct View: SwiftUI.View {
+ @Bindable var store: StoreOf
+ public init(store: StoreOf) {
+ self.store = store
+ }
+ public var body: some SwiftUI.View {
+ NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
+ NameNewAccountFeature.View(
+ store: store.scope(state: \.nameAccount, action: \.nameAccount)
+ )
+ } destination: { store in
+ switch store.state {
+ case .selectGradient:
+ if let store = store.scope(state: \.selectGradient, action: \.selectGradient) {
+ SelectGradientFeature.View(store: store)
+ }
+ }
+ }
+ }
+ }
+
+ public init() {}
+
+ public var body: some ReducerOf {
+ Scope(state: \.nameAccount, action: \.nameAccount) {
+ NameNewAccountFeature()
+ }
+
+ Reduce { state, action in
+ switch action {
+
+ case let .nameAccount(.delegate(.named(name))):
+ state.path.append(.selectGradient(.init(name: name)))
+ return .none
+
+ case .path(let pathAction):
+ switch pathAction {
+
+ case let .element(
+ id: _,
+ action: .selectGradient(.delegate(.selected(appearanceID, displayName)))
+ ):
+ do {
+ let wallet = state.walletHolder.wallet
+ var account = try wallet.createNewAccount(
+ networkId: .mainnet,
+ name: displayName
+ )
+ account.appearanceId = appearanceID
+
+ try wallet.addAccount(account: account)
+
+ return .send(.delegate(.createdAccount))
+
+ } catch {
+ fatalError("TODO error handling: \(error)")
+ }
+
+ case .element(id: _, action: _):
+ return .none
+ case .popFrom(id: _):
+ return .none
+ case .push(id: _, state: _):
+ return .none
+ }
+ return .none
+
+ case .nameAccount(.view):
+ return .none
+
+ case .delegate:
+ return .none
+ }
+ }
+ .forEach(\.path, action: \.path)
+ }
+}
diff --git a/examples/iOS/Sources/Planbok/Features/CreateAccountFeature.swift b/examples/iOS/Sources/Planbok/Features/Flows/CreateAccount/Steps/NameAccountFeature.swift
similarity index 53%
rename from examples/iOS/Sources/Planbok/Features/CreateAccountFeature.swift
rename to examples/iOS/Sources/Planbok/Features/Flows/CreateAccount/Steps/NameAccountFeature.swift
index 7fe903449..7030ff97e 100644
--- a/examples/iOS/Sources/Planbok/Features/CreateAccountFeature.swift
+++ b/examples/iOS/Sources/Planbok/Features/Flows/CreateAccount/Steps/NameAccountFeature.swift
@@ -1,5 +1,5 @@
@Reducer
-public struct CreateAccountFeature {
+public struct NameNewAccountFeature {
@ObservableState
public struct State: Equatable {
@@ -14,22 +14,30 @@ public struct CreateAccountFeature {
}
}
- public enum Action {
- case accountNameChanged(String)
- case createAccountButtonTapped
- case createdAccount
+ public enum Action: ViewAction {
+ public enum Delegate {
+ case named(DisplayName)
+ }
+ @CasePathable
+ public enum ViewAction {
+ case accountNameChanged(String)
+ case continueButtonTapped
+ }
+ case delegate(Delegate)
+ case view(ViewAction)
}
+ @ViewAction(for: NameNewAccountFeature.self)
public struct View: SwiftUI.View {
- @Bindable var store: StoreOf
- public init(store: StoreOf) {
+ @Bindable public var store: StoreOf
+ public init(store: StoreOf) {
self.store = store
}
public var body: some SwiftUI.View {
VStack {
- Text("Create Account").font(.largeTitle)
+ Text("Name Account").font(.largeTitle)
Spacer()
- LabeledTextField(label: "Account Name", text: $store.accountName.sending(\.accountNameChanged))
+ LabeledTextField(label: "Account Name", text: $store.accountName.sending(\.view.accountNameChanged))
if let error = store.state.errorMessage {
Text("\(error)")
.foregroundStyle(Color.red)
@@ -37,10 +45,10 @@ public struct CreateAccountFeature {
.fontWeight(.bold)
}
Spacer()
- Button("Create Account") {
- store.send(.createAccountButtonTapped)
+ Button("Continue") {
+ send(.continueButtonTapped)
}
-
+ .buttonStyle(.borderedProminent)
}
.padding()
}
@@ -51,44 +59,25 @@ public struct CreateAccountFeature {
public var body: some ReducerOf {
Reduce { state, action in
switch action {
- case let .accountNameChanged(name):
+ case let .view(.accountNameChanged(name)):
state.errorMessage = nil
state.accountName = name
return .none
- case .createAccountButtonTapped:
+ case .view(.continueButtonTapped):
state.errorMessage = nil
do {
let displayName = try DisplayName(validating: state.accountName)
- do {
- _ = try state.walletHolder.wallet.createAndSaveNewAccount(
- networkId: .mainnet,
- name: displayName
- )
- return .send(.createdAccount)
- } catch {
- state.errorMessage = "Failed to create and save account. This is really bad."
- return .none
- }
+ return .send(.delegate(.named(displayName)))
} catch {
state.errorMessage = "Invalid DisplayName, can't be empty or too long."
return .none
}
- case .createdAccount:
+ case .delegate:
return .none
+
}
}
}
}
-
-public struct LabeledTextField: SwiftUI.View {
- public let label: LocalizedStringKey
- @Binding public var text: String
- public var body: some View {
- VStack {
- Text(label)
- TextField(label, text: $text)
- }
- }
-}
diff --git a/examples/iOS/Sources/Planbok/Features/Flows/CreateAccount/Steps/SelectGradientFeature.swift b/examples/iOS/Sources/Planbok/Features/Flows/CreateAccount/Steps/SelectGradientFeature.swift
new file mode 100644
index 000000000..da4352d5d
--- /dev/null
+++ b/examples/iOS/Sources/Planbok/Features/Flows/CreateAccount/Steps/SelectGradientFeature.swift
@@ -0,0 +1,105 @@
+//
+// File.swift
+//
+//
+// Created by Alexander Cyon on 2024-02-16.
+//
+
+import Foundation
+
+@Reducer
+public struct SelectGradientFeature {
+
+ @ObservableState
+ public struct State: Equatable {
+ public let name: DisplayName
+ public var gradient: AppearanceID
+ public init(
+ name: DisplayName,
+ gradient: AppearanceID = AppearanceID.allCases.first!
+ ) {
+ self.name = name
+ self.gradient = gradient
+ }
+ }
+
+ @CasePathable
+ public enum Action: ViewAction {
+ public enum Delegate {
+ case selected(AppearanceID, DisplayName)
+ }
+ @CasePathable
+ public enum ViewAction {
+ case selectedGradient(AppearanceID)
+ case confirmedGradientButtonTapped
+ }
+ case view(ViewAction)
+ case delegate(Delegate)
+ }
+
+ @ViewAction(for: SelectGradientFeature.self)
+ public struct View: SwiftUI.View {
+ public let store: StoreOf
+ public init(store: StoreOf) {
+ self.store = store
+ }
+ public var body: some SwiftUI.View {
+ VStack {
+ Text("Select account gradient").font(.title)
+ ScrollView {
+ let height: CGFloat = 20
+ ForEach(AppearanceID.allCases) { appearanceID in
+ let isSelected = appearanceID == store.state.gradient
+ Button.init(action: { send(.selectedGradient(appearanceID)) }, label: {
+ HStack {
+ Text("Gradient \(String(describing: appearanceID))")
+ .font(isSelected ? .headline : .subheadline)
+ .fontWeight(isSelected ? .bold : .regular)
+
+ Spacer()
+
+ if isSelected {
+ Image(systemName: "checkmark")
+ .resizable()
+ .scaledToFit()
+ }
+ }
+ })
+
+ .foregroundColor(.app.white)
+ .frame(maxWidth: .infinity, idealHeight: height, alignment: .leading)
+ .padding()
+ .background(appearanceID.gradient)
+ .cornerRadius(height)
+
+ }
+ }
+ Button("Confirm Gradient") {
+ send(.confirmedGradientButtonTapped)
+ }
+ .buttonStyle(.borderedProminent)
+ }
+ .padding()
+ }
+ }
+
+ public init() {}
+
+ public var body: some ReducerOf {
+ Reduce { state, action in
+ switch action {
+
+ case let .view(.selectedGradient(gradient)):
+ state.gradient = gradient
+ return .none
+
+ case .view(.confirmedGradientButtonTapped):
+ return .send(.delegate(.selected(state.gradient, state.name)))
+
+ default:
+ return .none
+
+ }
+ }
+ }
+}
diff --git a/examples/iOS/Sources/Planbok/Features/Flows/Onboarding/OnboardingFeature.swift b/examples/iOS/Sources/Planbok/Features/Flows/Onboarding/OnboardingFeature.swift
new file mode 100644
index 000000000..ab2a65e70
--- /dev/null
+++ b/examples/iOS/Sources/Planbok/Features/Flows/Onboarding/OnboardingFeature.swift
@@ -0,0 +1,119 @@
+@Reducer
+public struct OnboardingFeature {
+
+ @Reducer(state: .equatable)
+ public enum Path {
+ case writeDownMnemonic(WriteDownMnemonicFeature)
+ }
+
+ @Reducer(state: .equatable)
+ public enum Destination {
+ case createAccount(CreateAccountFlowFeature)
+ }
+
+ @ObservableState
+ public struct State: Equatable {
+ public let walletHolder: WalletHolder
+ public var path = StackState()
+ public var welcome: WelcomeFeature.State
+
+ @Presents var destination: Destination.State?
+
+ public init(walletHolder: WalletHolder) {
+ self.walletHolder = walletHolder
+ self.welcome = WelcomeFeature.State()
+ }
+
+ public init(wallet: Wallet) {
+ self.init(walletHolder: .init(wallet: wallet))
+ }
+ }
+
+ @CasePathable
+ public enum Action {
+ @CasePathable
+ public enum DelegateAction {
+ case createdAccount(with: WalletHolder)
+ }
+
+ case destination(PresentationAction)
+ case path(StackAction)
+ case welcome(WelcomeFeature.Action)
+ case delegate(DelegateAction)
+ }
+
+ public init() {}
+
+ public var body: some ReducerOf {
+ Scope(state: \.welcome, action: \.welcome) {
+ WelcomeFeature()
+ }
+ Reduce { state, action in
+ switch action {
+
+ case .path(let pathAction):
+ switch pathAction {
+ case .element(id: _, action: .writeDownMnemonic(.delegate(.done))):
+ return .send(.delegate(.createdAccount(with: state.walletHolder)))
+ case .popFrom(id: _):
+ return .none
+ case .push(id: _, state: _):
+ return .none
+ default:
+ return .none
+ }
+ case .welcome(.delegate(.done)):
+ state.destination = .createAccount(CreateAccountFlowFeature.State(walletHolder: state.walletHolder))
+ return .none
+ case .welcome(.view):
+ return .none
+ case .delegate:
+ return .none
+ case .destination(.presented(.createAccount(.delegate(.createdAccount)))):
+ state.destination = nil
+ state.path.append(.writeDownMnemonic(.init(walletHolder: state.walletHolder)))
+ return .none
+
+ default:
+ return .none
+ }
+ }
+ .forEach(\.path, action: \.path)
+ .ifLet(\.$destination, action: \.destination)
+ }
+
+ public struct View: SwiftUI.View {
+ @Bindable var store: StoreOf
+
+ public init(store: StoreOf) {
+ self.store = store
+ }
+
+ public var body: some SwiftUI.View {
+ NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
+ WelcomeFeature.View(
+ store: store.scope(state: \.welcome, action: \.welcome)
+ )
+ } destination: { store in
+ switch store.case {
+ case .writeDownMnemonic:
+ if let store = store.scope(state: \.writeDownMnemonic, action: \.writeDownMnemonic) {
+ WriteDownMnemonicFeature.View(store: store)
+ }
+ }
+ }
+ .sheet(
+ item: $store.scope(
+ state: \.destination?.createAccount,
+ action: \.destination.createAccount
+ )
+ ) { store in
+ CreateAccountFlowFeature.View(store: store)
+ }
+
+ }
+
+ }
+
+
+}
diff --git a/examples/iOS/Sources/Planbok/Features/Flows/Onboarding/WelcomeFeature.swift b/examples/iOS/Sources/Planbok/Features/Flows/Onboarding/WelcomeFeature.swift
new file mode 100644
index 000000000..e95e2e6f2
--- /dev/null
+++ b/examples/iOS/Sources/Planbok/Features/Flows/Onboarding/WelcomeFeature.swift
@@ -0,0 +1,70 @@
+//
+// File.swift
+//
+//
+// Created by Alexander Cyon on 2024-02-16.
+//
+
+import Foundation
+
+@Reducer
+public struct WelcomeFeature {
+ public init() {}
+
+ @ObservableState
+ public struct State: Equatable {
+ public init() {}
+ }
+
+ public enum Action: ViewAction {
+ public enum DelegateAction {
+ case done
+ }
+ public enum ViewAction {
+ case continueButtonTapped
+ }
+ case delegate(DelegateAction)
+ case view(ViewAction)
+ }
+
+ @ViewAction(for: WelcomeFeature.self)
+ public struct View: SwiftUI.View {
+ public let store: StoreOf
+
+ public var body: some SwiftUI.View {
+ VStack {
+ Text("Welcome to Sargon demo").font(.title)
+ ScrollView {
+ Text(
+"""
+This tiny app demonstrates how Sargon written in Rust can be used in an iOS app, thanks to the Swift bindings that we have generated with UniFFI.
+
+The build artifacts of UniFFI are have three major components:
+1) A set of binaries we have grouped to together with lipo and put in a .xcframework vendored as a binaryTarget in the Sargon Swift Package.
+
+2) A single HUGE Swift file with Swift models exported from Rust and with function pointers that use the binaryTarget in the Sargon Swift Package
+
+3) A set of Swift extension's on the bindgen generated Swift models, e.g. making `Decimal192` conform to `ExpressibleByIntegerLiteral` which of course is a pure Swift construct. Also marking all types as `Sendable` and `CustomStringConvertible` making use of their `std::fmt::Display` impl in Rust land.
+"""
+ )
+ .padding()
+ }
+ Button("Start") {
+ send(.continueButtonTapped)
+ }
+ }
+ .padding()
+ }
+ }
+
+ public var body: some ReducerOf {
+ Reduce { state, action in
+ switch action {
+ case .view(.continueButtonTapped):
+ .send(.delegate(.done))
+ case .delegate:
+ .none
+ }
+ }
+ }
+}
diff --git a/examples/iOS/Sources/Planbok/Features/Flows/Onboarding/WriteDownMnemonicFeature.swift b/examples/iOS/Sources/Planbok/Features/Flows/Onboarding/WriteDownMnemonicFeature.swift
new file mode 100644
index 000000000..44b0350d3
--- /dev/null
+++ b/examples/iOS/Sources/Planbok/Features/Flows/Onboarding/WriteDownMnemonicFeature.swift
@@ -0,0 +1,84 @@
+//
+// File.swift
+//
+//
+// Created by Alexander Cyon on 2024-02-16.
+//
+
+import Foundation
+
+@Reducer
+public struct WriteDownMnemonicFeature {
+
+ @Dependency(\.keychain) var keychain
+
+ public init() {}
+
+ @ObservableState
+ public struct State: Equatable {
+ public let walletHolder: WalletHolder
+ public var mnemonic: String?
+ public init(walletHolder: WalletHolder) {
+ self.walletHolder = walletHolder
+ }
+ }
+
+ public enum Action: ViewAction {
+ public enum DelegateAction {
+ case done
+ }
+ public enum ViewAction {
+ case revealMnemonicButtonTapped
+ case continueButtonTapped
+ }
+ case delegate(DelegateAction)
+ case view(ViewAction)
+ }
+
+ @ViewAction(for: WriteDownMnemonicFeature.self)
+ public struct View: SwiftUI.View {
+ public let store: StoreOf
+
+ public var body: some SwiftUI.View {
+ VStack {
+ Text("Write down your mnemonic on a piece of paper and put it in a safe")
+ .font(.title)
+ Spacer()
+ if let mnemonic = store.state.mnemonic {
+ Text("`\(mnemonic)`")
+ .border(.yellow)
+ } else {
+ Button("Reveal") {
+ send(.revealMnemonicButtonTapped)
+ }
+ }
+ Spacer()
+ Button("Continue") {
+ send(.continueButtonTapped)
+ }
+ }
+ .padding()
+ }
+ }
+
+ public var body: some ReducerOf {
+ Reduce { state, action in
+ switch action {
+ case .view(.revealMnemonicButtonTapped):
+ let wallet = state.walletHolder.wallet
+
+ do {
+ let bdfsMnemonic = try wallet.mainBdfsMnemonicWithPassphrase()
+ state.mnemonic = bdfsMnemonic.mnemonic.phrase
+ } catch {
+ fatalError("handle error: \(error)")
+ }
+ return .none
+ case .view(.continueButtonTapped):
+ return .send(.delegate(.done))
+ case .delegate:
+ return .none
+ }
+ }
+ }
+}
diff --git a/examples/iOS/Sources/Planbok/Features/MainFeature.swift b/examples/iOS/Sources/Planbok/Features/MainFeature.swift
index 9eb80dc7b..34d322dbc 100644
--- a/examples/iOS/Sources/Planbok/Features/MainFeature.swift
+++ b/examples/iOS/Sources/Planbok/Features/MainFeature.swift
@@ -1,6 +1,47 @@
@Reducer
public struct MainFeature {
+ @Reducer(state: .equatable)
+ public enum Destination {
+ case createAccount(CreateAccountFlowFeature)
+ case alert(AlertState)
+
+ public enum Alert {
+ case confirmedDeleteWallet
+ }
+ }
+
+ @ObservableState
+ public struct State: Equatable {
+
+ @Presents var destination: Destination.State?
+
+ public var accounts: AccountsFeature.State
+ public let walletHolder: WalletHolder
+
+ public init(walletHolder: WalletHolder) {
+ self.walletHolder = walletHolder
+ self.accounts = AccountsFeature.State(walletHolder: walletHolder)
+ }
+
+ public init(wallet: Wallet) {
+ self.init(walletHolder: .init(wallet: wallet))
+ }
+ }
+
+ @CasePathable
+ public enum Action {
+ @CasePathable
+ public enum DelegateAction {
+ case deletedWallet
+ }
+ case destination(PresentationAction)
+ case accounts(AccountsFeature.Action)
+
+ case delegate(DelegateAction)
+
+ }
+
@Dependency(\.keychain) var keychain
public init() {}
@@ -27,7 +68,7 @@ public struct MainFeature {
case .accounts(.delegate(.createNewAccount)):
state.destination = .createAccount(
- CreateAccountFeature.State(
+ CreateAccountFlowFeature.State(
walletHolder: state.walletHolder
)
)
@@ -45,7 +86,7 @@ public struct MainFeature {
fatalError("Fix error handling, error: \(error)")
}
- case .destination(.presented(.createAccount(.createdAccount))):
+ case .destination(.presented(.createAccount(.delegate(.createdAccount)))):
state.destination = nil
state.accounts.refresh() // FIXME: we really do not want this.
return .none
@@ -57,44 +98,7 @@ public struct MainFeature {
.ifLet(\.$destination, action: \.destination)
}
- @Reducer(state: .equatable)
- public enum Destination {
- case createAccount(CreateAccountFeature)
- case alert(AlertState)
-
- public enum Alert {
- case confirmedDeleteWallet
- }
- }
-
- @ObservableState
- public struct State: Equatable {
-
- @Presents var destination: Destination.State?
-
- public var accounts: AccountsFeature.State
- public let walletHolder: WalletHolder
-
- public init(walletHolder: WalletHolder) {
- self.walletHolder = walletHolder
- self.accounts = AccountsFeature.State(walletHolder: walletHolder)
- }
-
- public init(wallet: Wallet) {
- self.init(walletHolder: .init(wallet: wallet))
- }
- }
-
- public enum Action {
- public enum DelegateAction {
- case deletedWallet
- }
- case destination(PresentationAction)
- case accounts(AccountsFeature.Action)
-
- case delegate(DelegateAction)
-
- }
+
public struct View: SwiftUI.View {
@@ -121,7 +125,7 @@ public struct MainFeature {
action: \.destination.createAccount
)
) { store in
- CreateAccountFeature.View(store: store)
+ CreateAccountFlowFeature.View(store: store)
}
.alert($store.scope(state: \.destination?.alert, action: \.destination.alert))
}
diff --git a/examples/iOS/Sources/Planbok/Features/OnboardingFeature.swift b/examples/iOS/Sources/Planbok/Features/OnboardingFeature.swift
deleted file mode 100644
index b651ab6e0..000000000
--- a/examples/iOS/Sources/Planbok/Features/OnboardingFeature.swift
+++ /dev/null
@@ -1,64 +0,0 @@
-@Reducer
-public struct OnboardingFeature {
-
-
- @Reducer(state: .equatable)
- public enum Path {
- case createAccount(CreateAccountFeature)
- }
-
- @ObservableState
- public struct State: Equatable {
- public var path = StackState()
- public var createAccount: CreateAccountFeature.State
-
- public init(walletHolder: WalletHolder) {
- self.createAccount = CreateAccountFeature.State(walletHolder: walletHolder)
- }
-
- public init(wallet: Wallet) {
- self.init(walletHolder: .init(wallet: wallet))
- }
- }
-
- public enum Action {
- case path(StackAction)
- case createAccount(CreateAccountFeature.Action)
-
- case createdAccount(with: WalletHolder)
- }
-
- public struct View: SwiftUI.View {
- @Bindable var store: StoreOf
- public init(store: StoreOf) {
- self.store = store
- }
- public var body: some SwiftUI.View {
- NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
- CreateAccountFeature.View(
- store: store.scope(state: \.createAccount, action: \.createAccount)
- )
- } destination: { _ in
- Text("Never seen")
- }
- }
- }
-
- public init() {}
-
- public var body: some ReducerOf {
- Scope(state: \.createAccount, action: \.createAccount) {
- CreateAccountFeature()
- }
- Reduce { state, action in
- switch action {
-
- case .createAccount(.createdAccount):
- return .send(.createdAccount(with: state.createAccount.walletHolder))
- default:
- return .none
- }
- }
- .forEach(\.path, action: \.path)
- }
-}
diff --git a/examples/iOS/Sources/Planbok/View/Views/AccountView.swift b/examples/iOS/Sources/Planbok/View/Views/AccountView.swift
index 1e116b1cf..aea5b0987 100644
--- a/examples/iOS/Sources/Planbok/View/Views/AccountView.swift
+++ b/examples/iOS/Sources/Planbok/View/Views/AccountView.swift
@@ -13,7 +13,6 @@ public struct AccountView: SwiftUI.View {
AddressView(account.address)
.foregroundColor(.app.whiteTransparent)
- .foregroundColor(.app.whiteTransparent)
}
.padding(.horizontal, .medium1)
.padding(.vertical, .medium2)
diff --git a/examples/iOS/Sources/Planbok/View/Views/LabeledTextField.swift b/examples/iOS/Sources/Planbok/View/Views/LabeledTextField.swift
new file mode 100644
index 000000000..1c21bdfae
--- /dev/null
+++ b/examples/iOS/Sources/Planbok/View/Views/LabeledTextField.swift
@@ -0,0 +1,14 @@
+
+public struct LabeledTextField: SwiftUI.View {
+
+ public let label: LocalizedStringKey
+ @Binding public var text: String
+
+ public var body: some View {
+ VStack(alignment: .leading) {
+ Text(label).padding(.leading, 5)
+ TextField(label, text: $text)
+ }
+ .textFieldStyle(.roundedBorder)
+ }
+}
diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs
index 896e85acf..880b4cb6a 100644
--- a/src/wallet/mod.rs
+++ b/src/wallet/mod.rs
@@ -1,9 +1,11 @@
mod secure_storage;
mod wallet;
mod wallet_accounts;
+mod wallet_device_factor_sources;
mod wallet_profile_io;
pub use secure_storage::*;
pub use wallet::*;
+pub use wallet_device_factor_sources::*;
pub use wallet_accounts::*;
pub use wallet_profile_io::*;
diff --git a/src/wallet/wallet_accounts.rs b/src/wallet/wallet_accounts.rs
index 9ecf3e690..aa192fc4c 100644
--- a/src/wallet/wallet_accounts.rs
+++ b/src/wallet/wallet_accounts.rs
@@ -73,48 +73,6 @@ impl Wallet {
})
}
- /// Loads a `MnemonicWithPassphrase` with the `id` of `device_factor_source`,
- /// from SecureStorage, and returns a `PrivateHierarchicalDeterministicFactorSource`
- /// built from both.
- ///
- /// Useful for when you will want to sign transactions or derive public keys for
- /// creation of new entities.
- ///
- /// Returns `Err` if loading or decoding of `MnemonicWithPassphrase` from
- /// SecureStorage fails.
- pub fn load_private_device_factor_source(
- &self,
- device_factor_source: &DeviceFactorSource,
- ) -> Result {
- info!(
- "Load Private DeviceFactorSource from SecureStorage, factor source id: {}",
- &device_factor_source.id
- );
- self.wallet_client_storage
- .load_mnemonic_with_passphrase(&device_factor_source.id)
- .map(|mwp| {
- PrivateHierarchicalDeterministicFactorSource::new(mwp, device_factor_source.clone())
- })
- .log_info("Successfully loaded Private DeviceFactorSource from SecureStorage")
- }
-
- /// Loads a `MnemonicWithPassphrase` with the `id` of `device_factor_source`,
- /// from SecureStorage, and returns a `PrivateHierarchicalDeterministicFactorSource`
- /// built from both.
- ///
- /// Useful for when you will want to sign transactions or derive public keys for
- /// creation of new entities.
- ///
- /// Returns `Err` if loading or decoding of `MnemonicWithPassphrase` from
- /// SecureStorage fails.
- pub fn load_private_device_factor_source_by_id(
- &self,
- id: &FactorSourceIDFromHash,
- ) -> Result {
- let device_factor_source =
- self.profile().device_factor_source_by_id(id)?;
- self.load_private_device_factor_source(&device_factor_source)
- }
}
//========
diff --git a/src/wallet/wallet_device_factor_sources.rs b/src/wallet/wallet_device_factor_sources.rs
new file mode 100644
index 000000000..2c79e9be9
--- /dev/null
+++ b/src/wallet/wallet_device_factor_sources.rs
@@ -0,0 +1,113 @@
+use crate::prelude::*;
+
+impl Wallet {
+ /// Tries to load a `MnemonicWithPassphrase` from secure storage
+ /// by `id` of type `FactorSourceIDFromHash`.
+ pub fn mnemonic_with_passphrase_of_device_factor_source_by_id(
+ &self,
+ id: &FactorSourceIDFromHash,
+ ) -> Result {
+ self.wallet_client_storage.load_mnemonic_with_passphrase(id)
+ }
+
+ /// Loads a `MnemonicWithPassphrase` with the `id` of `device_factor_source`,
+ /// from SecureStorage, and returns a `PrivateHierarchicalDeterministicFactorSource`
+ /// built from both.
+ ///
+ /// Useful for when you will want to sign transactions or derive public keys for
+ /// creation of new entities.
+ ///
+ /// Returns `Err` if loading or decoding of `MnemonicWithPassphrase` from
+ /// SecureStorage fails.
+ pub fn load_private_device_factor_source(
+ &self,
+ device_factor_source: &DeviceFactorSource,
+ ) -> Result {
+ info!(
+ "Load Private DeviceFactorSource from SecureStorage, factor source id: {}",
+ &device_factor_source.id
+ );
+ self.mnemonic_with_passphrase_of_device_factor_source_by_id(
+ &device_factor_source.id,
+ )
+ .map(|mwp| {
+ PrivateHierarchicalDeterministicFactorSource::new(
+ mwp,
+ device_factor_source.clone(),
+ )
+ })
+ .log_info(
+ "Successfully loaded Private DeviceFactorSource from SecureStorage",
+ )
+ }
+
+ /// Loads a `MnemonicWithPassphrase` with the `id` of `device_factor_source`,
+ /// from SecureStorage, and returns a `PrivateHierarchicalDeterministicFactorSource`
+ /// built from both.
+ ///
+ /// Useful for when you will want to sign transactions or derive public keys for
+ /// creation of new entities.
+ ///
+ /// Returns `Err` if loading or decoding of `MnemonicWithPassphrase` from
+ /// SecureStorage fails.
+ pub fn load_private_device_factor_source_by_id(
+ &self,
+ id: &FactorSourceIDFromHash,
+ ) -> Result {
+ let device_factor_source =
+ self.profile().device_factor_source_by_id(id)?;
+ self.load_private_device_factor_source(&device_factor_source)
+ }
+}
+
+#[uniffi::export]
+impl Wallet {
+ /// Tries to load a `MnemonicWithPassphrase` from secure storage
+ /// by `factor_source_id`.
+ pub fn mnemonic_with_passphrase_of_device_factor_source_by_factor_source_id(
+ &self,
+ factor_source_id: &FactorSourceID,
+ ) -> Result {
+ factor_source_id
+ .clone()
+ .into_hash()
+ .map_err(|_| CommonError::FactorSourceIDNotFromHash)
+ .and_then(|id| {
+ self.mnemonic_with_passphrase_of_device_factor_source_by_id(&id)
+ })
+ }
+
+ /// Tries to load the `MnemonicWithPassphrase` for the main "Babylon"
+ /// `DeviceFactorSource` from secure storage.
+ pub fn main_bdfs_mnemonic_with_passphrase(
+ &self,
+ ) -> Result {
+ let profile = &self.profile();
+ let bdfs = profile.bdfs();
+ self.mnemonic_with_passphrase_of_device_factor_source_by_id(&bdfs.id)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::prelude::*;
+
+ #[test]
+ fn main_bdfs_mnemonic_with_passphrase() {
+ let private =
+ PrivateHierarchicalDeterministicFactorSource::placeholder();
+ let dfs = private.factor_source;
+ let profile = Profile::placeholder();
+ let (wallet, storage) = Wallet::ephemeral(profile.clone());
+ let data =
+ serde_json::to_vec(&private.mnemonic_with_passphrase).unwrap();
+ let key = SecureStorageKey::DeviceFactorSourceMnemonic {
+ factor_source_id: dfs.id.clone(),
+ };
+ storage.save_data(key.clone(), data.clone()).unwrap();
+ assert_eq!(
+ wallet.main_bdfs_mnemonic_with_passphrase().unwrap(),
+ MnemonicWithPassphrase::placeholder()
+ );
+ }
+}