taler-ios

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

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