SettingsView.swift (11426B)
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 var isRegistered: Bool { 38 UIApplication.shared.isRegisteredForRemoteNotifications 39 } 40 41 func checkRegisterState(delaySeconds: Double) { 42 // update isRegistered ==> icon shown 43 DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) { 44 registerState = isRegistered 45 } 46 } 47 48 private var dismissAlertButton: some View { 49 Button("Cancel", role: .cancel) { 50 pushNotifications = false 51 mayNotUsePush = false 52 } 53 } 54 55 private var openSettingsButton: some View { 56 Button("Open Settings") { 57 mayNotUsePush = false 58 UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:]) 59 } 60 } 61 62 func checkPushNotifications(_ shouldUsePush: Bool) { 63 #if TALER_NIGHTLY 64 // 1. Request authorisation for remote (push) notifications. 65 // For background-only (silent) pushes the alert/badge/sound 66 // entitlements are not strictly required, but requesting them 67 // avoids surprises when you later add user-visible notifications. 68 DispatchQueue.main.async { 69 if isRegistered { 70 registerState = true 71 if shouldUsePush { 72 self.symLog.log("already registered for remote notifications") 73 } else { 74 self.symLog.log("unregisterForRemoteNotifications") 75 UIApplication.shared.unregisterForRemoteNotifications() 76 controller.deviceTokenAPNs = nil // TODO: tell walletCore 77 checkRegisterState(delaySeconds: 0.5) 78 } 79 } else { 80 registerState = false 81 if !shouldUsePush { 82 self.symLog.log("remote notifications are disabled") 83 } else { 84 UNUserNotificationCenter.current().requestAuthorization( 85 options: [.alert, .badge, .sound] 86 ) { granted, error in 87 DispatchQueue.main.async { 88 if granted { 89 self.symLog.log("registerForRemoteNotifications") 90 /// result in didRegisterForRemoteNotificationsWithDeviceToken 91 UIApplication.shared.registerForRemoteNotifications() 92 checkRegisterState(delaySeconds: 0.5) 93 } else { 94 self.symLog.log("Error requesting notification permissions: \(error?.localizedDescription ?? "unknown")") 95 mayNotUsePush = true 96 registerState = false 97 pushNotifications = false 98 } 99 } 100 } 101 } 102 } 103 } 104 #endif 105 106 } 107 108 var body: some View { 109 #if PRINT_CHANGES 110 let _ = Self._printChanges() 111 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 112 #endif 113 #if TALER_WALLET 114 let appName = "Taler Wallet" 115 #elseif TALER_NIGHTLY 116 let appName = "Taler Nightly" 117 #else 118 let appName = "GNU Taler" 119 #endif 120 let localizedAppName = Bundle.main.bundleName ?? appName 121 let list = List { 122 let aboutStr = String(localized: "About \(localizedAppName)") 123 NavigationLink { // whole row like in a tableView 124 AboutView(stack: stack.push(), localizedAppName: localizedAppName, navTitle: aboutStr) 125 } label: { 126 SettingsItem(name: aboutStr, id1: "about", imageName: TALER_LOGO) {} 127 } 128 129 NavigationLink { // whole row like in a tableView 130 ExchangeListView(stack: stack.push(), url: .constant(nil)) 131 } label: { 132 SettingsItem(name: TITLE_EXCHANGES, id1: "exchanges", imageName: EXCHANGE_LOGO, 133 description: String(localized: "Manage payment services")) {} 134 } 135 136 let bankAccountsTitle = String(localized: "TitleBankAccounts", defaultValue: "Bank Accounts") 137 let bankAccountsDest = BankListView(stack: stack.push(bankAccountsTitle), 138 navTitle: bankAccountsTitle) 139 NavigationLink { // whole row like in a tableView 140 bankAccountsDest 141 } label: { 142 SettingsItem(name: bankAccountsTitle, id1: "bankAccounts", 143 imageName: "building.columns", 144 description: String(localized: "Your accounts for deposit")) {} 145 } 146 147 let biometryType = controller.biometryType() ?? biometricService.biometryType() 148 let hasFaceID = biometryType == .faceID 149 let hasTouchID = biometryType == .touchID 150 let biometryString = hasFaceID ? String(localized: "Use FaceID") 151 : hasTouchID ? String(localized: "Use TouchID") 152 : EMPTYSTRING 153 if !biometryString.isEmpty { 154 SettingsToggle(name: biometryString, 155 value: $useAuthentication, 156 id1: "useFaceID", 157 imageName: hasFaceID ? "faceid" : "touchid", // 158 description: String(localized: "Protect your money")) { _ in 159 biometricService.isAuthenticated = false 160 } 161 // } else { 162 // biometricService.isAuthenticated = false 163 } 164 #if !TALER_WALLET 165 SettingsToggle(name: "NYM mixnet", 166 value: $useMixnet, 167 id1: "useMixnet", 168 imageName: "eye.slash", // 169 description: String(localized: "Protect your privacy")) 170 #endif 171 #if TALER_NIGHTLY 172 SettingsToggle(name: String(localized: "Push Notifications"), value: $pushNotifications, 173 id1: "pushNotifications", 174 imageName: registerState ? NOTIFICATION2 : NOTIFICATION1, // or 175 description: String(localized: "Check pending payments in the background") 176 ) { newVal in 177 checkPushNotifications(newVal) 178 } 179 #endif 180 SettingsToggle(name: String(localized: "Minimalistic"), value: $minimalistic, id1: "minimal", 181 imageName: "heart", // 182 description: String(localized: "Omit text where possible")) 183 184 SettingsToggle(name: String(localized: "Show Warnings"), value: $shouldShowWarning, 185 id1: "warnings", 186 imageName: "exclamationmark.triangle", // 187 description: String(localized: "For Delete, Abandon & Abort buttons")) 188 189 /// Report 190 let reportTitle = String(localized: "TitleReport", defaultValue: "Report diagnostics") 191 let reportDest = ReportView(stack: stack.push(reportTitle), 192 navTitle: reportTitle) 193 NavigationLink { 194 reportDest 195 } label: { 196 SettingsItem(name: reportTitle, id1: "report", 197 imageName: "arrow.up.message", // 198 description: String(localized: "Help improve \(localizedAppName)")) {} 199 } 200 201 #if DEBUG 202 let showDiagnostic = true 203 #else 204 let showDiagnostic = controller.diagnosticModeEnabled 205 #endif 206 if showDiagnostic { 207 let devTitle = String(localized: "TitleDeveloper", defaultValue: "Developer") 208 let devDest = DebugSettingsView(stack: stack.push(devTitle), 209 navTitle: devTitle) 210 NavigationLink { 211 devDest 212 } label: { 213 SettingsItem(name: devTitle, id1: "developer", 214 imageName: "hammer", // 215 description: String(localized: "Help debug \(localizedAppName)")) {} 216 } 217 } 218 219 let moreItem = String(localized: "TitleMore", defaultValue: "More") 220 let moreTitle = String(localized: "TitleMoreSettings", defaultValue: "More Settings") 221 let moreDest = MoreSettingsView(stack: stack.push(moreTitle), 222 navTitle: moreTitle) 223 NavigationLink { 224 moreDest 225 } label: { 226 SettingsItem(name: moreItem, id1: "more", 227 imageName: "ellipsis", // 228 description: nil) {} 229 } 230 } 231 .id(listID) 232 .listStyle(myListStyle.style).anyView 233 .navigationTitle(navTitle) 234 .onAppear() { 235 DebugViewC.shared.setViewID(VIEW_SETTINGS, stack: stack.push()) 236 registerState = isRegistered 237 } 238 .alert("Push Notifications are disabled", 239 isPresented: $mayNotUsePush, 240 actions: { openSettingsButton 241 dismissAlertButton }, 242 message: { Text("Please go to Settings > \(localizedAppName) > Notifications and turn them on.") } 243 ) 244 if #available(iOS 26.0, *) { 245 list 246 } else { 247 list 248 .padding(.bottom) 249 } 250 } // body 251 } 252 // MARK: - 253 #if DEBUG 254 //struct SettingsView_Previews: PreviewProvider { 255 // static var previews: some View { 256 // SettingsView(stack: CallStack("Preview"), balances: <#Binding<[Balance]>#>, navTitle: "Settings") 257 // } 258 //} 259 #endif