spb/os-vault Public
Self-custody, multi-chain crypto wallet for macOS. One recovery phrase, six chain families, zero API keys — nothing leaves your Mac.
Swift 96%
Shell 3.4%
Makefile 0.6%
1//2// SettingsView.swift3// OS Vault4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//89import SwiftUI1011struct SettingsView: View {12 @EnvironmentObject var app: AppState13 @Environment(\.dismiss) private var dismiss1415 @State private var rpcOverride = ""16 @State private var exportPassword = ""17 @State private var exportedMnemonic: String?18 @State private var exportError: String?19 @State private var deleteConfirmation = ""20 @State private var deleteError: String?2122 var body: some View {23 VStack(alignment: .leading, spacing: 0) {24 HStack {25 Text("Settings").font(.title2.bold())26 Spacer()27 Button("Done") { dismiss() }28 .keyboardShortcut(.defaultAction)29 }30 .padding(.bottom, 12)3132 ScrollView {33 VStack(alignment: .leading, spacing: 20) {34 networkSection35 Divider()36 pricesSection37 Divider()38 rpcSection39 Divider()40 exportSection41 Divider()42 deleteSection43 }44 .padding(.vertical, 4)45 }46 }47 .padding(24)48 .frame(width: 480, height: 560)49 .onAppear {50 rpcOverride = UserDefaults.standard.string(forKey: app.network.rpcOverrideKey) ?? ""51 }52 }5354 // MARK: - Network5556 private var networkSection: some View {57 VStack(alignment: .leading, spacing: 8) {58 Text("Network").font(.headline)59 Picker("Active network", selection: $app.network) {60 ForEach(Network.allCases) { network in61 Text(network.config.displayName + (network.config.isTestnet ? " (testnet)" : ""))62 .tag(network)63 }64 }65 .pickerStyle(.menu)66 if !app.network.config.isTestnet {67 Label("Mainnet moves real funds. Double-check every send.",68 systemImage: "exclamationmark.triangle.fill")69 .font(.callout)70 .foregroundStyle(.orange)71 }72 }73 .onChange(of: app.network) { _, newNetwork in74 rpcOverride = UserDefaults.standard.string(forKey: newNetwork.rpcOverrideKey) ?? ""75 }76 }7778 // MARK: - Prices7980 private var pricesSection: some View {81 VStack(alignment: .leading, spacing: 8) {82 Text("Fiat values").font(.headline)83 Toggle("Show fiat values (fetches prices from CoinGecko, keyless)", isOn: $app.pricesEnabled)84 if app.pricesEnabled {85 Picker("Currency", selection: $app.fiatCurrency) {86 ForEach(PriceService.fiatOptions, id: \.self) { Text($0).tag($0) }87 }88 .pickerStyle(.segmented)89 .frame(maxWidth: 260)90 }91 Text("Balances are always exact on-chain amounts; fiat values are display estimates. Turning this off removes the only non-blockchain network call.")92 .font(.caption)93 .foregroundStyle(.secondary)94 }95 }9697 // MARK: - RPC9899 private var rpcSection: some View {100 VStack(alignment: .leading, spacing: 8) {101 Text("Custom RPC for \(app.network.config.displayName)").font(.headline)102 Text("Leave empty to use the built-in keyless endpoints (\(app.network.config.rpcs.map(\.host!).joined(separator: ", "))) with automatic failover. A dedicated Alchemy/QuickNode URL is more reliable; no key is ever required to run OS Vault.")103 .font(.caption)104 .foregroundStyle(.secondary)105 HStack {106 TextField("https://…", text: $rpcOverride)107 .textFieldStyle(.roundedBorder)108 .autocorrectionDisabled()109 Button("Save") {110 let trimmed = rpcOverride.trimmingCharacters(in: .whitespacesAndNewlines)111 if trimmed.isEmpty {112 UserDefaults.standard.removeObject(forKey: app.network.rpcOverrideKey)113 } else {114 UserDefaults.standard.set(trimmed, forKey: app.network.rpcOverrideKey)115 }116 Task { await app.refreshBalances() }117 }118 }119 }120 }121122 // MARK: - Export seed123124 private var exportSection: some View {125 VStack(alignment: .leading, spacing: 8) {126 Text("Recovery phrase").font(.headline)127 if let exportedMnemonic {128 Text(exportedMnemonic)129 .font(.callout.monospaced())130 .textSelection(.enabled)131 .padding(10)132 .frame(maxWidth: .infinity, alignment: .leading)133 .background(Color.orange.opacity(0.12))134 .clipShape(RoundedRectangle(cornerRadius: 8))135 Button("Hide") {136 self.exportedMnemonic = nil137 exportPassword = ""138 }139 } else {140 Text("Reveals the 12 words that control your funds. Make sure nobody can see your screen.")141 .font(.caption)142 .foregroundStyle(.secondary)143 HStack {144 SecureField("Vault password", text: $exportPassword)145 .textFieldStyle(.roundedBorder)146 Button("Reveal") { export() }147 .disabled(exportPassword.isEmpty)148 }149 if let exportError {150 Text(exportError).foregroundStyle(.red).font(.callout)151 }152 }153 }154 }155156 private func export() {157 exportError = nil158 let manager = app.keyManager159 let candidate = exportPassword160 Task {161 let result = await Task.detached {162 Result { try manager.exportMnemonic(password: candidate) }163 }.value164 switch result {165 case .success(let mnemonic): exportedMnemonic = mnemonic166 case .failure(let error): exportError = error.localizedDescription167 }168 }169 }170171 // MARK: - Delete172173 private var deleteSection: some View {174 VStack(alignment: .leading, spacing: 8) {175 Text("Danger zone").font(.headline).foregroundStyle(.red)176 Text("Deletes the encrypted vault from this Mac. Without your written recovery phrase, the funds are unrecoverable. Type DELETE to confirm.")177 .font(.caption)178 .foregroundStyle(.secondary)179 HStack {180 TextField("Type DELETE", text: $deleteConfirmation)181 .textFieldStyle(.roundedBorder)182 Button("Delete wallet", role: .destructive) {183 guard deleteConfirmation == "DELETE" else {184 deleteError = "Type DELETE (all caps) to confirm."185 return186 }187 do {188 try app.keyManager.deleteVault()189 dismiss()190 app.walletDeleted()191 } catch {192 deleteError = error.localizedDescription193 }194 }195 }196 if let deleteError {197 Text(deleteError).foregroundStyle(.red).font(.callout)198 }199 }200 }201}202