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