taler-ios

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

SettingsView.swift (11424B)


      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                 // TODO: add another toggle with snail/hare for 60 sec / 10 sec
    162             }
    163 #if !TALER_WALLET
    164             SettingsToggle(name: "NYM mixnet",
    165                           value: $useMixnet,
    166                             id1: "useMixnet",
    167                       imageName: "eye.slash", // 􀋯
    168                     description: String(localized: "Protect your privacy"))
    169 #endif
    170 #if TALER_NIGHTLY
    171                 SettingsToggle(name: String(localized: "Push Notifications"), value: $pushNotifications,
    172                                 id1: "pushNotifications",
    173                           imageName: registerState ? NOTIFICATION2 : NOTIFICATION1,  // 􀝖 or 􀋙
    174                         description: String(localized: "Check pending payments in the background")
    175                 ) { newVal in
    176                     checkPushNotifications(newVal)
    177                 }
    178 #endif
    179                 SettingsToggle(name: String(localized: "Minimalistic"), value: $minimalistic, id1: "minimal",
    180                           imageName: "heart",                   // 􀊴
    181                         description: String(localized: "Omit text where possible"))
    182 
    183                 SettingsToggle(name: String(localized: "Show Warnings"), value: $shouldShowWarning,
    184                                 id1: "warnings",
    185                           imageName: "exclamationmark.triangle", // 􀇾
    186                         description: String(localized: "For Delete, Abandon & Abort buttons"))
    187 
    188               /// Report
    189                 let reportTitle = String(localized: "TitleReport", defaultValue: "Report diagnostics")
    190                 let reportDest = ReportView(stack: stack.push(reportTitle),
    191                                          navTitle: reportTitle)
    192                 NavigationLink {
    193                     reportDest
    194                 } label: {
    195                     SettingsItem(name: reportTitle, id1: "report",
    196                             imageName: "arrow.up.message",    // 􀜃
    197                           description: String(localized: "Help improve \(localizedAppName)")) {}
    198                 }
    199 
    200 #if DEBUG
    201                 let showDiagnostic = true
    202 #else
    203                 let showDiagnostic = controller.diagnosticModeEnabled
    204 #endif
    205                 if showDiagnostic {
    206                     let devTitle = String(localized: "TitleDeveloper", defaultValue: "Developer")
    207                     let devDest = DebugSettingsView(stack: stack.push(devTitle),
    208                                                  navTitle: devTitle)
    209                     NavigationLink {
    210                         devDest
    211                     } label: {
    212                         SettingsItem(name: devTitle, id1: "developer",
    213                                 imageName: "hammer",        // 􀙄
    214                               description: String(localized: "Help debug \(localizedAppName)")) {}
    215                     }
    216                 }
    217 
    218                 let moreItem = String(localized: "TitleMore", defaultValue: "More")
    219                 let moreTitle = String(localized: "TitleMoreSettings", defaultValue: "More Settings")
    220                 let moreDest = MoreSettingsView(stack: stack.push(moreTitle),
    221                                              navTitle: moreTitle)
    222                 NavigationLink {
    223                     moreDest
    224                 } label: {
    225                     SettingsItem(name: moreItem, id1: "more",
    226                             imageName: "ellipsis",          // 􀍠
    227                           description: nil) {}
    228                 }
    229         }
    230             .id(listID)
    231             .listStyle(myListStyle.style).anyView
    232             .navigationTitle(navTitle)
    233             .onAppear() {
    234                 DebugViewC.shared.setViewID(VIEW_SETTINGS, stack: stack.push())
    235                 registerState = isRegistered
    236             }
    237             .alert("Push Notifications are disabled",
    238                    isPresented: $mayNotUsePush,
    239                    actions: { openSettingsButton
    240                               dismissAlertButton },
    241                    message: { Text("Please go to Settings > \(localizedAppName) > Notifications and turn them on.") }
    242             )
    243         if #available(iOS 26.0, *) {
    244             list
    245         } else {
    246             list
    247                 .padding(.bottom)
    248         }
    249     } // body
    250 }
    251 // MARK: -
    252 #if DEBUG
    253 //struct SettingsView_Previews: PreviewProvider {
    254 //    static var previews: some View {
    255 //        SettingsView(stack: CallStack("Preview"), balances: <#Binding<[Balance]>#>, navTitle: "Settings")
    256 //    }
    257 //}
    258 #endif