taler-ios

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

SettingsView.swift (11199B)


      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         let localizedAppName = controller.localizedAppName
    114         let list = List {
    115             let aboutStr = String(localized: "About \(localizedAppName)")
    116             NavigationLink {        // whole row like in a tableView
    117                 AboutView(stack: stack.push(), localizedAppName: localizedAppName,
    118                        navTitle: aboutStr)
    119             } label: {
    120                 SettingsItem(name: aboutStr, id1: "about", imageName: TALER_LOGO) {}
    121             }
    122 
    123             NavigationLink {        // whole row like in a tableView
    124                 ExchangeListView(stack: stack.push(), url: .constant(nil))
    125             } label: {
    126                 SettingsItem(name: TITLE_EXCHANGES, id1: "exchanges", imageName: EXCHANGE_LOGO,
    127                       description: String(localized: "Manage payment services")) {}
    128             }
    129 
    130             let bankAccountsTitle = String(localized: "TitleBankAccounts", defaultValue: "Bank Accounts")
    131             let bankAccountsDest = BankListView(stack: stack.push(bankAccountsTitle),
    132                                              navTitle: bankAccountsTitle)
    133             NavigationLink {        // whole row like in a tableView
    134                 bankAccountsDest
    135             } label: {
    136                 SettingsItem(name: bankAccountsTitle, id1: "bankAccounts",
    137                         imageName: "building.columns",
    138                       description: String(localized: "Your accounts for deposit")) {}
    139             }
    140 
    141             let biometryType = controller.biometryType() ?? biometricService.biometryType()
    142             let hasFaceID = biometryType == .faceID
    143             let hasTouchID = biometryType == .touchID
    144             let biometryString = hasFaceID ? String(localized: "Use FaceID")
    145                                : hasTouchID ? String(localized: "Use TouchID")
    146                                : EMPTYSTRING
    147             if !biometryString.isEmpty {
    148                 SettingsToggle(name: biometryString,
    149                               value: $useAuthentication,
    150                                 id1: "useFaceID",
    151                           imageName: hasFaceID ? "faceid" : "touchid", // 􀎽  􀟒
    152                         description: String(localized: "Protect your money")) { _ in
    153                     biometricService.isAuthenticated = false
    154                 }
    155                 // TODO: add another toggle with snail/hare for 60 sec / 10 sec
    156             }
    157 #if !TALER_WALLET
    158             SettingsToggle(name: "NYM mixnet",
    159                           value: $useMixnet,
    160                             id1: "useMixnet",
    161                       imageName: "eye.slash", // 􀋯
    162                     description: String(localized: "Protect your privacy"))
    163 #endif
    164 #if TALER_NIGHTLY
    165                 SettingsToggle(name: String(localized: "Push Notifications"), value: $pushNotifications,
    166                                 id1: "pushNotifications",
    167                           imageName: registerState ? NOTIFICATION2 : NOTIFICATION1,  // 􀝖 or 􀋙
    168                         description: String(localized: "Check pending payments in the background")
    169                 ) { newVal in
    170                     checkPushNotifications(newVal)
    171                 }
    172 #endif
    173                 SettingsToggle(name: String(localized: "Minimalistic"), value: $minimalistic, id1: "minimal",
    174                           imageName: "heart",                   // 􀊴
    175                         description: String(localized: "Omit text where possible"))
    176 
    177                 SettingsToggle(name: String(localized: "Show Warnings"), value: $shouldShowWarning,
    178                                 id1: "warnings",
    179                           imageName: "exclamationmark.triangle", // 􀇾
    180                         description: String(localized: "For Delete, Abandon & Abort buttons"))
    181 
    182               /// Report
    183                 let reportTitle = String(localized: "TitleReport", defaultValue: "Report diagnostics")
    184                 let reportDest = ReportView(stack: stack.push(reportTitle),
    185                                          navTitle: reportTitle)
    186                 NavigationLink {
    187                     reportDest
    188                 } label: {
    189                     SettingsItem(name: reportTitle, id1: "report",
    190                             imageName: "arrow.up.message",    // 􀜃
    191                           description: String(localized: "Help improve \(localizedAppName)")) {}
    192                 }
    193 
    194 #if DEBUG
    195                 let showDiagnostic = true
    196 #else
    197                 let showDiagnostic = controller.diagnosticModeEnabled
    198 #endif
    199                 if showDiagnostic {
    200                     let devTitle = String(localized: "TitleDeveloper", defaultValue: "Developer")
    201                     let devDest = DebugSettingsView(stack: stack.push(devTitle),
    202                                                  navTitle: devTitle)
    203                     NavigationLink {
    204                         devDest
    205                     } label: {
    206                         SettingsItem(name: devTitle, id1: "developer",
    207                                 imageName: "hammer",        // 􀙄
    208                                      description: String(localized: "Help debug \(localizedAppName)")) {}
    209                     }
    210                 }
    211 
    212                 let moreItem = String(localized: "TitleMore", defaultValue: "More")
    213                 let moreTitle = String(localized: "TitleMoreSettings", defaultValue: "More Settings")
    214                 let moreDest = MoreSettingsView(stack: stack.push(moreTitle),
    215                                              navTitle: moreTitle)
    216                 NavigationLink {
    217                     moreDest
    218                 } label: {
    219                     SettingsItem(name: moreItem, id1: "more",
    220                             imageName: "ellipsis",          // 􀍠
    221                           description: nil) {}
    222                 }
    223         }
    224             .id(listID)
    225             .listStyle(myListStyle.style).anyView
    226             .navigationTitle(navTitle)
    227             .onAppear() {
    228                 DebugViewC.shared.setViewID(VIEW_SETTINGS, stack: stack.push())
    229                 registerState = isRegistered
    230             }
    231             .alert("Push Notifications are disabled",
    232                    isPresented: $mayNotUsePush,
    233                    actions: { openSettingsButton
    234                               dismissAlertButton },
    235                    message: { Text("Please go to Settings > \(localizedAppName) > Notifications and turn them on.") }
    236             )
    237         if #available(iOS 26.0, *) {
    238             list
    239         } else {
    240             list
    241                 .padding(.bottom)
    242         }
    243     } // body
    244 }
    245 // MARK: -
    246 #if DEBUG
    247 //struct SettingsView_Previews: PreviewProvider {
    248 //    static var previews: some View {
    249 //        SettingsView(stack: CallStack("Preview"), balances: <#Binding<[Balance]>#>, navTitle: "Settings")
    250 //    }
    251 //}
    252 #endif