taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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