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