SettingsView.swift (21271B)
1 /* 2 * This file is part of GNU Taler, ©2022-25 Taler Systems S.A. 3 * See LICENSE.md 4 */ 5 /** 6 * @author Marc Stibane 7 */ 8 import SwiftUI 9 import taler_swift 10 import SymLog 11 import LocalConsole 12 13 /* 14 * Backup 15 * Last backup: 5 hr. ago 16 * 17 * Debug log 18 * View/send internal log 19 * 20 */ 21 22 struct SettingsView: View { 23 private let symLog = SymLogV(0) 24 let stack: CallStack 25 let navTitle: String 26 27 @EnvironmentObject private var controller: Controller 28 @EnvironmentObject private var model: WalletModel 29 @EnvironmentObject private var biometricService: BiometricService 30 // @Environment(\.colorSchemeContrast) private var colorSchemeContrast 31 #if DEBUG 32 @AppStorage("developerMode") var developerMode: Bool = true 33 #else 34 @AppStorage("developerMode") var developerMode: Bool = false 35 #endif 36 @AppStorage("useHaptics") var useHaptics: Bool = true 37 @AppStorage("playSoundsI") var playSoundsI: Int = 1 38 @AppStorage("playSoundsB") var playSoundsB: Bool = false 39 @AppStorage("shouldShowWarning") var shouldShowWarning: Bool = true 40 // @AppStorage("increaseContrast") var increaseContrast: Bool = false 41 @AppStorage("talerFontIndex") var talerFontIndex: Int = 0 42 @AppStorage("developDelay") var developDelay: Bool = false 43 @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic 44 @AppStorage("minimalistic") var minimalistic: Bool = false 45 @AppStorage("localConsoleL") var localConsoleL: Bool = false // for Logs 46 @AppStorage("localConsoleO") var localConsoleO: Int = 0 // for Observability 47 @AppStorage("useAuthentication") var useAuthentication: Bool = false 48 @AppStorage("showQRauto16") var showQRauto16: Bool = true 49 @AppStorage("showQRauto17") var showQRauto17: Bool = false 50 @AppStorage("oimEuro") var oimEuro: Bool = false 51 @AppStorage("oimChart") var oimChart: Bool = false 52 53 @State private var checkDisabled = false 54 @State private var withDrawDisabled = false 55 #if DEBUG 56 @State private var diagnosticModeEnabled = true 57 #endif 58 @State private var showDevelopItems = false 59 @State private var hideDescriptions = false 60 @State private var showResetAlert: Bool = false 61 @State private var didReset: Bool = false 62 63 private var dismissAlertButton: some View { 64 Button("Cancel", role: .cancel) { 65 showResetAlert = false 66 } 67 } 68 private var resetButton: some View { 69 Button("Reset", role: .destructive) { // TODO: WalletColors().errorColor 70 didReset = true 71 showResetAlert = false 72 Task { // runs on MainActor 73 symLog.log("❗️Reset wallet-core❗️") 74 try? await model.resetWalletCore() 75 } 76 } 77 } 78 @State private var listID = UUID() 79 80 func redraw(_ newFont: Int) -> Void { 81 if newFont != talerFontIndex { 82 talerFontIndex = newFont 83 withAnimation { listID = UUID() } 84 } 85 } 86 var body: some View { 87 #if PRINT_CHANGES 88 let _ = Self._printChanges() 89 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 90 #endif 91 let walletCore = WalletCore.shared 92 Group { 93 List { 94 #if TALER_WALLET 95 let appName = "Taler Wallet" 96 #elseif TALER_NIGHTLY 97 let appName = "Taler Nightly" 98 #else 99 let appName = "GNU Taler" 100 #endif 101 let localizedAppName = Bundle.main.bundleName ?? appName 102 let aboutStr = String(localized: "About \(localizedAppName)") 103 NavigationLink { // whole row like in a tableView 104 AboutView(stack: stack.push(), navTitle: aboutStr) 105 } label: { 106 SettingsItem(name: aboutStr, id1: "about", 107 description: hideDescriptions ? nil : String(localized: "More info about this app...")) {} 108 } 109 110 let exchangesTitle = String(localized: "TitleExchanges", defaultValue: "Payment Services") 111 let exchangesDest = ExchangeListView(stack: stack.push(exchangesTitle), 112 navTitle: exchangesTitle) 113 NavigationLink { // whole row like in a tableView 114 exchangesDest 115 } label: { 116 SettingsItem(name: exchangesTitle, id1: "exchanges", 117 description: hideDescriptions ? nil : String(localized: "Manage payment services...")) {} 118 } 119 let bankAccountsTitle = String(localized: "TitleBankAccounts", defaultValue: "Bank Accounts") 120 let bankAccountsDest = BankListView(stack: stack.push(bankAccountsTitle), 121 navTitle: bankAccountsTitle) 122 NavigationLink { // whole row like in a tableView 123 bankAccountsDest 124 } label: { 125 SettingsItem(name: bankAccountsTitle, id1: "bankAccounts", 126 description: hideDescriptions ? nil : String(localized: "Your accounts for deposit...")) {} 127 } 128 let showQRstring = String(localized: "Show QR codes") 129 let showQRhint = String(localized: "Automatically for P2P transactions") 130 if #available(iOS 17.7, *) { 131 SettingsToggle(name: showQRstring, value: $showQRauto17, id1: "showQRautomatic", 132 description: minimalistic ? nil : showQRhint) {} 133 } else { 134 SettingsToggle(name: showQRstring, value: $showQRauto16, id1: "showQRautomatic", 135 description: minimalistic ? nil : showQRhint) {} 136 } 137 SettingsToggle(name: String(localized: "Use FaceID / TouchID"), value: $useAuthentication, id1: "useFaceID", 138 description: minimalistic ? nil : String(localized: "Protect your money")) { 139 biometricService.isAuthenticated = false 140 } 141 142 /// Backup 143 let backupTitle = String(localized: "TitleBackup", defaultValue: "Backup / Restore") 144 let backupDest = BackupView(stack: stack.push(backupTitle), 145 navTitle: backupTitle) 146 NavigationLink { // whole row like in a tableView 147 backupDest 148 } label: { 149 SettingsItem(name: backupTitle, id1: "backup", 150 description: hideDescriptions ? nil : String(localized: "Backup your money...")) {} 151 } 152 153 #if OIM 154 SettingsToggle(name: String(localized: "OIM: Euro"), value: $oimEuro, id1: "oimEuro", 155 description: minimalistic ? nil : String(localized: "OIM currency for KUDOS")) 156 #endif 157 #if OIM 158 SettingsToggle(name: String(localized: "OIM: Chart"), value: $oimChart, id1: "oimChart", 159 description: minimalistic ? nil : String(localized: "OIM history as chart")) 160 #endif 161 162 SettingsToggle(name: String(localized: "Minimalistic"), value: $minimalistic, id1: "minimal", 163 description: hideDescriptions ? nil : String(localized: "Omit text where possible")) { 164 hideDescriptions = minimalistic //withAnimation { hideDescriptions = minimalistic } 165 } 166 if controller.hapticCapability.supportsHaptics { 167 SettingsToggle(name: String(localized: "Haptics"), value: $useHaptics, id1: "haptics", 168 description: hideDescriptions ? nil : String(localized: "Vibration Feedback")) 169 } 170 SettingsToggle(name: String(localized: "Play Payment Sounds"), value: $playSoundsB, id1: "playSounds", 171 description: hideDescriptions ? nil : String(localized: "When a transaction finished")) 172 SettingsToggle(name: String(localized: "Show Warnings"), value: $shouldShowWarning, id1: "warnings", 173 description: hideDescriptions ? nil : String(localized: "For Delete, Abandon & Abort buttons")) 174 // SettingsFont(title: String(localized: "Font:"), value: talerFontIndex, action: redraw) 175 // .id("font") 176 SettingsStyle(title: String(localized: "List Style:"), myListStyle: $myListStyle) 177 .id("liststyle") 178 #if DEBUG 179 let showDiagnostic = diagnosticModeEnabled 180 #else 181 let showDiagnostic = controller.diagnosticModeEnabled 182 #endif 183 if showDiagnostic { 184 let localConsStr = String("on LocalConsole") 185 let observability = String("Observe walletCore") 186 SettingsTriState(name: observability, value: $localConsoleO.onChange({ isObserving in 187 walletCore.isObserving = isObserving}), 188 description: hideDescriptions ? nil : localConsStr) { isObserving in 189 let consoleManager = LCManager.shared 190 consoleManager.isVisible = localConsoleO != 0 || localConsoleL 191 consoleManager.clear() 192 } 193 let showLogs = String("Show logs") 194 SettingsToggle(name: showLogs, value: $localConsoleL.onChange({ isLogging in 195 walletCore.isLogging = isLogging}), id1: "localConsoleL", 196 description: hideDescriptions ? nil : localConsStr) { 197 let consoleManager = LCManager.shared 198 consoleManager.isVisible = localConsoleO != 0 || localConsoleL 199 consoleManager.clear() 200 } 201 SettingsToggle(name: String("Developer Mode"), value: $developerMode, id1: "devMode", 202 description: hideDescriptions ? nil : String("More information intended for debugging")) { 203 withAnimation(Animation.linear.delay(0.8)) { showDevelopItems = developerMode } 204 } 205 #if DEBUG 206 if showDevelopItems { 207 let banks = ["glstest.taler.net", "glsint.fdold.eu", "taler.fdold.eu", "regio-taler.fdold.eu", 208 "taler.grothoff.org", "taler.ar", 209 "head.taler.net", "test.taler.net", "demo.taler.net", "kyctest.taler.net"] 210 ForEach(banks, id: \.self) { bank in 211 let urlStr = "https://bank." + bank 212 Link(bank, destination: URL(string: urlStr)!) 213 } 214 } 215 #endif 216 if showDevelopItems { // show or hide the following items 217 SettingsItem(name: String("DEMO"), id1: "demo1with", 218 description: hideDescriptions ? nil : String("Get money for testing")) { 219 let title = "Withdraw" 220 Button(title) { 221 withDrawDisabled = true // don't run twice 222 Task { // runs on MainActor 223 symLog.log("Withdraw DEMO KUDOS") 224 let amount = Amount(currency: DEMOCURRENCY, cent: 11100) 225 try? await model.loadTestKudos(0, amount: amount) 226 } 227 } 228 .buttonStyle(.bordered) 229 .disabled(withDrawDisabled) 230 }.id("demo1withdraw") 231 SettingsItem(name: String("TEST"), id1: "test1with", 232 description: hideDescriptions ? nil : String("Get money for testing")) { 233 let title = "Withdraw" 234 Button(title) { 235 withDrawDisabled = true // don't run twice 236 Task { // runs on MainActor 237 symLog.log("Withdraw TESTKUDOS") 238 let cent = UInt64.random(in: 110...195) * 100 239 let amount = Amount(currency: TESTCURRENCY, cent: cent) 240 try? await model.loadTestKudos(1, amount: amount) 241 } 242 } 243 .buttonStyle(.bordered) 244 .disabled(withDrawDisabled) 245 }.id("test1withdraw") 246 SettingsItem(name: String("HEAD"), id1: "head1with", 247 description: hideDescriptions ? nil : String("Get money for testing")) { 248 let title = "Withdraw" 249 Button(title) { 250 withDrawDisabled = true // don't run twice 251 Task { // runs on MainActor 252 symLog.log("Withdraw HEAD KUDOS") 253 let amount = Amount(currency: DEMOCURRENCY, cent: 1100) 254 try? await model.loadTestKudos(2, amount: amount) 255 } 256 } 257 .buttonStyle(.bordered) 258 .disabled(withDrawDisabled) 259 }.id("head1withdraw") 260 SettingsToggle(name: String("Set 2 seconds delay"), 261 value: $developDelay.onChange({ delay in 262 walletCore.developDelay = delay}), 263 id1: "delay", 264 description: hideDescriptions ? nil : String("After each wallet-core action")) 265 .id("delay") 266 #if DEBUG 267 SettingsItem(name: String("Run Dev Experiment Refresh"), id1: "applyDevExperiment", 268 description: hideDescriptions ? nil : "dev-experiment/insert-pending-refresh") { 269 let title = "Refresh" 270 Button(title) { 271 Task { // runs on MainActor 272 symLog.log("running applyDevExperiment Refresh") 273 try? await model.setConfig(setTesting: true) 274 try? await model.devExperimentT(talerUri: "taler://dev-experiment/start-block-refresh") 275 try? await model.devExperimentT(talerUri: "taler://dev-experiment/insert-pending-refresh") 276 } 277 } 278 .buttonStyle(.bordered) 279 }.id("Refresh") 280 #endif 281 SettingsItem(name: String("Run Integration Test"), id1: "demo1test", 282 description: hideDescriptions ? nil : String("Perform basic test transactions")) { 283 let title = "Demo 1" 284 Button(title) { 285 checkDisabled = true // don't run twice 286 Task { // runs on MainActor 287 symLog.log("running integration test on demo") 288 try? await model.runIntegrationTest(newVersion: false, test: false) 289 } 290 } 291 .buttonStyle(.bordered) 292 .disabled(checkDisabled) 293 }.id("demo1runTest") 294 SettingsItem(name: String("Run Integration Test"), id1: "test1test", 295 description: hideDescriptions ? nil : "Perform basic test transactions") { 296 let title = "Test 1" 297 Button(title) { 298 checkDisabled = true // don't run twice 299 Task { // runs on MainActor 300 symLog.log("running integration test on test") 301 try? await model.runIntegrationTest(newVersion: false, test: true) 302 } 303 } 304 .buttonStyle(.bordered) 305 .disabled(checkDisabled) 306 }.id("test1runTest") 307 SettingsItem(name: String("Run Integration Test V2"), id1: "demo2test", 308 description: hideDescriptions ? nil : String("Perform more test transactions")) { 309 let title = "Demo 2" 310 Button(title) { 311 checkDisabled = true // don't run twice 312 Task { // runs on MainActor 313 symLog.log("running integration test V2 on demo") 314 try? await model.runIntegrationTest(newVersion: true, test: false) 315 } 316 } 317 .buttonStyle(.bordered) 318 .disabled(checkDisabled) 319 }.id("demo2runTest") 320 SettingsItem(name: String("Run Integration Test V2"), id1: "test2test", 321 description: hideDescriptions ? nil : String("Perform more test transactions")) { 322 let title = "Test 2" 323 Button(title) { 324 checkDisabled = true // don't run twice 325 Task { // runs on MainActor 326 symLog.log("running integration test V2 on test") 327 try? await model.runIntegrationTest(newVersion: true, test: true) 328 } 329 } 330 .buttonStyle(.bordered) 331 .disabled(checkDisabled) 332 }.id("test2runTest") 333 SettingsItem(name: String("Run Infinite Transaction Loop"), id1: "runInfinite", 334 description: hideDescriptions ? nil : String("Check DB in background")) { 335 let title = "Loop" 336 Button(title) { 337 checkDisabled = true // don't run twice 338 Task { // runs on MainActor 339 symLog.log("Running Infinite Transaction Loop") 340 try? await model.testingInfiniteTransaction(delayMs: 10_000, shouldFetch: true) 341 } 342 } 343 .buttonStyle(.bordered) 344 .disabled(checkDisabled) 345 }.id("runInfiniteLoop") 346 SettingsItem(name: String("Save Logfile"), id1: "save", 347 description: hideDescriptions ? nil : String("Help debugging wallet-core")) { 348 Button("Save") { 349 symLog.log("Saving Log") 350 // FIXME: Save Logfile 351 } 352 .buttonStyle(.bordered) 353 .disabled(true) 354 }.id("saveLog") 355 SettingsItem(name: String("Reset Wallet"), id1: "reset", 356 description: hideDescriptions ? nil : String("Throw away all your money")) { 357 Button("Reset") { 358 showResetAlert = true 359 } 360 .buttonStyle(.bordered) 361 // .disabled(didReset) 362 }.id("resetWallet") 363 } 364 } 365 } 366 .id(listID) 367 .listStyle(myListStyle.style).anyView 368 } 369 .navigationTitle(navTitle) 370 .onAppear() { 371 showDevelopItems = developerMode 372 hideDescriptions = minimalistic 373 DebugViewC.shared.setViewID(VIEW_SETTINGS, stack: stack.push()) 374 } 375 .onDisappear() { 376 checkDisabled = false // reset 377 withDrawDisabled = false 378 } 379 .alert("Reset Wallet", 380 isPresented: $showResetAlert, 381 actions: { dismissAlertButton 382 resetButton }, 383 message: { Text(verbatim: "Are you sure you want to reset your wallet?\nThis cannot be reverted, all money will be lost.") }) 384 385 } // body 386 } 387 // MARK: - 388 #if DEBUG 389 //struct SettingsView_Previews: PreviewProvider { 390 // static var previews: some View { 391 // SettingsView(stack: CallStack("Preview"), balances: <#Binding<[Balance]>#>, navTitle: "Settings") 392 // } 393 //} 394 #endif