SettingsView.swift (12264B)
1 /* 2 * This file is part of GNU Taler, ©2022-26 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 12 struct SettingsView: View { 13 private let symLog = SymLogV(0) 14 let stack: CallStack 15 let navTitle: String 16 17 @EnvironmentObject private var controller: Controller 18 @EnvironmentObject private var model: WalletModel 19 @EnvironmentObject private var biometricService: BiometricService 20 // @Environment(\.colorSchemeContrast) private var colorSchemeContrast 21 #if DEBUG 22 @AppStorage("developerMode") var developerMode: Bool = true 23 #else 24 @AppStorage("developerMode") var developerMode: Bool = false 25 #endif 26 @AppStorage("shouldShowWarning") var shouldShowWarning: Bool = true 27 @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic 28 @AppStorage("minimalistic") var minimalistic: Bool = false 29 @AppStorage("useAuthentication") var useAuthentication: Bool = false 30 @AppStorage("useMixnet") var useMixnet: Bool = false 31 @AppStorage("pushNotifications") var pushNotifications: Bool = false 32 33 @State private var listID = UUID() 34 @State private var mayNotUsePush: Bool = false 35 @State private var registerState: Bool = false 36 37 // MARK: - 38 var isRegistered: Bool { 39 UIApplication.shared.isRegisteredForRemoteNotifications 40 } 41 42 func checkRegisterState(delaySeconds: Double) { 43 // update isRegistered ==> icon shown 44 DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) { 45 registerState = isRegistered 46 } 47 } 48 49 private var dismissAlertButton: some View { 50 Button("Cancel", role: .cancel) { 51 pushNotifications = false 52 mayNotUsePush = false 53 } 54 } 55 56 private var openSettingsButton: some View { 57 Button("Open Settings") { 58 mayNotUsePush = false 59 UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:]) 60 } 61 } 62 63 func checkPushNotifications(_ shouldUsePush: Bool) { 64 #if TALER_NIGHTLY 65 // 1. Request authorisation for remote (push) notifications. 66 // For background-only (silent) pushes the alert/badge/sound 67 // entitlements are not strictly required, but requesting them 68 // avoids surprises when you later add user-visible notifications. 69 DispatchQueue.main.async { 70 if isRegistered { 71 registerState = true 72 if shouldUsePush { 73 self.symLog.log("already registered for remote notifications") 74 } else { 75 self.symLog.log("unregisterForRemoteNotifications") 76 UIApplication.shared.unregisterForRemoteNotifications() 77 controller.deviceTokenAPNs = nil // TODO: tell walletCore 78 checkRegisterState(delaySeconds: 0.5) 79 } 80 } else { 81 registerState = false 82 if !shouldUsePush { 83 self.symLog.log("remote notifications are disabled") 84 } else { 85 UNUserNotificationCenter.current().requestAuthorization( 86 options: [.alert, .badge, .sound] 87 ) { granted, error in 88 DispatchQueue.main.async { 89 if granted { 90 self.symLog.log("registerForRemoteNotifications") 91 /// result in didRegisterForRemoteNotificationsWithDeviceToken 92 UIApplication.shared.registerForRemoteNotifications() 93 checkRegisterState(delaySeconds: 0.5) 94 } else { 95 self.symLog.log("Error requesting notification permissions: \(error?.localizedDescription ?? "unknown")") 96 mayNotUsePush = true 97 registerState = false 98 pushNotifications = false 99 } 100 } 101 } 102 } 103 } 104 } 105 #endif 106 107 } 108 // MARK: - 109 @ViewBuilder func aboutV(_ localizedAppName: String) -> some View { 110 let aboutStr = String(localized: "About \(localizedAppName)") 111 NavigationLink { // whole row like in a tableView 112 AboutView(stack: stack.push(), 113 appName: localizedAppName, 114 navTitle: aboutStr) 115 } label: { 116 SettingsItem(name: aboutStr, id1: "about", imageName: TALER_LOGO) {} 117 } 118 } 119 // MARK: - 120 @ViewBuilder func paymentServicesV() -> some View { 121 NavigationLink { 122 ExchangeListView(stack: stack.push(), url: .constant(nil)) 123 } label: { 124 SettingsItem(name: TITLE_EXCHANGES, id1: "exchanges", 125 imageName: EXCHANGE_LOGO, 126 description: String(localized: "Manage payment services")) {} 127 } 128 } 129 // MARK: - 130 @ViewBuilder func bankAccountsV() -> some View { 131 let bankAccountsTitle = String(localized: "TitleBankAccounts", defaultValue: "Bank Accounts") 132 NavigationLink { 133 BankListView(stack: stack.push(), 134 navTitle: bankAccountsTitle) 135 } label: { 136 SettingsItem(name: bankAccountsTitle, id1: "bankAccounts", 137 imageName: ICONNAME_BANK, 138 description: String(localized: "Your accounts for deposit")) {} 139 } 140 } 141 // MARK: - 142 @ViewBuilder func biometryV() -> some View { 143 let biometryType = controller.biometryType() ?? biometricService.biometryType() 144 let hasFaceID = biometryType == .faceID 145 let hasTouchID = biometryType == .touchID 146 let biometryString = hasFaceID ? String(localized: "Use FaceID") 147 : hasTouchID ? String(localized: "Use TouchID") 148 : EMPTYSTRING 149 if !biometryString.isEmpty { 150 SettingsToggle(name: biometryString, 151 value: $useAuthentication, 152 id1: "useFaceID", 153 imageName: hasFaceID ? "faceid" : "touchid", // 154 description: String(localized: "Protect your money") 155 ) { _ in 156 biometricService.isAuthenticated = false 157 } 158 // TODO: add another toggle with snail/hare for 60 sec / 10 sec 159 } 160 } 161 #if GNU_TALER || TALER_NIGHTLY 162 @ViewBuilder func talDirV() -> some View { 163 let talDirTitle = String(localized: "TitleTalDir", defaultValue: "Taler Directory") 164 NavigationLink { 165 TalDirSettingsView(stack: stack.push(), 166 navTitle: talDirTitle) 167 } label: { 168 SettingsItem(name: talDirTitle, id1: "talDir", 169 imageName: ICONNAME_PERSON3, 170 description: String(localized: "Your payment contacts", comment: "TalDir")) {} 171 } 172 } 173 #endif 174 @ViewBuilder func pushNotificationsV() -> some View { 175 SettingsToggle(name: String(localized: "Push Notifications"), 176 value: $pushNotifications, id1: "pushNotifications", 177 imageName: registerState ? NOTIFICATION2 : NOTIFICATION1, // or 178 description: String(localized: "Get notified when pending payments change status") 179 ) { newVal in 180 checkPushNotifications(newVal) 181 } 182 } 183 #if !TALER_WALLET 184 @ViewBuilder func useNymV() -> some View { 185 SettingsToggle(name: "NYM mixnet", 186 value: $useMixnet, 187 id1: "useMixnet", 188 imageName: "eye.slash", // 189 description: String(localized: "Protect your privacy")) 190 } 191 #endif // !TALER_WALLET 192 // MARK: - 193 @ViewBuilder func minimalV() -> some View { 194 SettingsToggle(name: String(localized: "Minimalistic"), value: $minimalistic, id1: "minimal", 195 imageName: "heart", // 196 description: String(localized: "Omit text where possible")) 197 } 198 // MARK: - 199 @ViewBuilder func warningsV() -> some View { 200 SettingsToggle(name: String(localized: "Show Warnings"), 201 value: $shouldShowWarning, 202 id1: "warnings", 203 imageName: WARNING, // 204 description: String(localized: "For Delete, Abandon & Abort buttons")) 205 } 206 // MARK: - 207 @ViewBuilder func reportV(_ localizedAppName: String) -> some View { 208 let reportTitle = String(localized: "TitleReport", defaultValue: "Report diagnostics") 209 NavigationLink { 210 ReportView(stack: stack.push(reportTitle), 211 navTitle: reportTitle) 212 } label: { 213 SettingsItem(name: reportTitle, id1: "report", 214 imageName: "arrow.up.message", // 215 description: String(localized: "Help improve \(localizedAppName)")) {} 216 } 217 } 218 // MARK: - 219 @ViewBuilder func developerV(_ localizedAppName: String) -> some View { 220 let devTitle = String(localized: "TitleDeveloper", defaultValue: "Developer") 221 NavigationLink { 222 DebugSettingsView(stack: stack.push(devTitle), 223 navTitle: devTitle) 224 } label: { 225 SettingsItem(name: devTitle, id1: "developer", 226 imageName: "hammer", // 227 description: String(localized: "Help debug \(localizedAppName)")) {} 228 } 229 } 230 // MARK: - 231 @ViewBuilder func moreV() -> some View { 232 let moreItem = String(localized: "TitleMore", defaultValue: "More") 233 let moreTitle = String(localized: "TitleMoreSettings", defaultValue: "More Settings") 234 NavigationLink { 235 MoreSettingsView(stack: stack.push(), 236 navTitle: moreTitle) 237 } label: { 238 SettingsItem(name: moreItem, id1: "more", 239 imageName: "ellipsis", // 240 description: nil) {} 241 } 242 } 243 // MARK: - 244 var body: some View { 245 #if PRINT_CHANGES 246 let _ = Self._printChanges() 247 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 248 #endif 249 let localizedAppName = controller.localizedAppName 250 let list = List { 251 aboutV(localizedAppName) // About... is always the first 252 paymentServicesV() 253 bankAccountsV() 254 biometryV() 255 #if GNU_TALER || TALER_NIGHTLY 256 talDirV() 257 #endif 258 #if TALER_NIGHTLY 259 pushNotificationsV() 260 .alert("Push Notifications are disabled", 261 isPresented: $mayNotUsePush, 262 actions: { openSettingsButton 263 dismissAlertButton }, 264 message: { Text("Please go to Settings > \(localizedAppName) > Notifications and turn them on.") } 265 ) 266 #endif 267 #if !TALER_WALLET 268 useNymV() 269 #endif 270 minimalV() 271 warningsV() 272 reportV(localizedAppName) 273 274 #if DEBUG 275 let showDiagnostic = true 276 #else 277 let showDiagnostic = controller.diagnosticModeEnabled 278 #endif 279 if showDiagnostic { 280 developerV(localizedAppName) 281 } 282 moreV() // More... is always the last 283 } 284 .id(listID) 285 .listStyle(myListStyle.style).anyView 286 .navigationTitle(navTitle) 287 .onAppear() { 288 DebugViewC.shared.setViewID(VIEW_SETTINGS, stack: stack.push()) 289 registerState = isRegistered 290 } 291 if #available(iOS 26.0, *) { 292 list 293 } else { 294 list 295 .padding(.bottom) 296 } 297 } // body 298 } 299 // MARK: - 300 #if DEBUG 301 //struct SettingsView_Previews: PreviewProvider { 302 // static var previews: some View { 303 // SettingsView(stack: CallStack("Preview"), balances: <#Binding<[Balance]>#>, navTitle: "Settings") 304 // } 305 //} 306 #endif