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() + ); + } +}