import SwiftUI struct CreateWalletView: View { @EnvironmentObject private var model: AppModel @Binding var route: Route @Binding var seedBox: SeedBox? @State private var name = "" @State private var localError: String? var body: some View { VStack(alignment: .leading, spacing: 18) { ScreenHeader(title: "Create wallet", subtitle: "A new \(model.chain.title) wallet on this device", onBack: { route = .home }) VStack(alignment: .leading, spacing: 6) { FieldLabel(text: "Wallet name") KoinsTextField(placeholder: "My \(model.symbol) wallet", text: $name) } if model.chain == .xmr { Card { Text("The wallet file is created locally and encrypted on this device. Syncing starts right away and can take a while for a new Monero wallet.") .font(.system(size: 12)) .foregroundStyle(Theme.muted) } } if let message = localError ?? model.errorMessage { MessageBanner(message: message, isError: true) } PrimaryButton(title: model.isBusy ? "Generating…" : "Generate wallet", disabled: model.isBusy) { Task { localError = nil model.clearMessages() do { let seed = try await model.createWallet(name: name) seedBox = SeedBox(seed: seed, symbol: model.symbol) route = .dashboard } catch { localError = error.localizedDescription } } } Spacer(minLength: 0) } } } struct ImportWalletView: View { @EnvironmentObject private var model: AppModel @Binding var route: Route @State private var name = "" @State private var secret = "" @State private var restoreHeight = "" @State private var localError: String? @State private var tipError: String? @State private var isLoadingTip = false var body: some View { VStack(alignment: .leading, spacing: 18) { ScreenHeader(title: "Import wallet", subtitle: "Restore a \(model.chain.title) wallet from its seed", onBack: { route = .home }) VStack(alignment: .leading, spacing: 6) { FieldLabel(text: "Wallet name") KoinsTextField(placeholder: "My \(model.symbol) wallet", text: $name) } VStack(alignment: .leading, spacing: 6) { FieldLabel(text: model.chain == .xrp ? "Recovery phrase" : "Mnemonic seed") KoinsTextField(placeholder: model.chain == .xrp ? "12 words, or a family seed (sEd…)" : "25 word seed phrase", text: $secret, multiline: model.chain == .xmr) } if model.chain == .xmr { VStack(alignment: .leading, spacing: 6) { FieldLabel(text: "Restore height (optional)") KoinsTextField(placeholder: "e.g. 3200000", text: $restoreHeight, keyboard: .numberPad) } Card { VStack(alignment: .leading, spacing: 10) { Text("Without a height the wallet scans the whole chain — millions of blocks, which takes hours on a phone. Set the block your wallet was first used, or start from the current tip if you only care about new funds.") .font(.system(size: 12)) .foregroundStyle(Theme.muted) Button { Task { tipError = nil isLoadingTip = true defer { isLoadingTip = false } do { let tip = try await MoneroService.shared.daemonHeight() restoreHeight = String(tip) } catch { tipError = error.localizedDescription } } } label: { Text(isLoadingTip ? "Fetching chain tip…" : "Use the current chain tip") .font(.system(size: 13, weight: .medium)) .foregroundStyle(Theme.accent) } .disabled(isLoadingTip) if let tipError { Text(tipError) .font(.system(size: 11)) .foregroundStyle(Theme.danger) } } } } if let message = localError ?? model.errorMessage { MessageBanner(message: message, isError: true) } PrimaryButton(title: model.isBusy ? "Importing…" : "Import wallet", disabled: model.isBusy) { Task { localError = nil model.clearMessages() do { let height = UInt64(restoreHeight.trimmingCharacters(in: .whitespaces)) try await model.importWallet(name: name, secret: secret, restoreHeight: height) route = .dashboard } catch { localError = error.localizedDescription } } } Spacer(minLength: 0) } } }