taler-ios

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

SettingsView.swift (12264B)


      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     // MARK: -
     38     var isRegistered: Bool {
     39         UIApplication.shared.isRegisteredForRemoteNotifications
     40     }
     41 
     42     func checkRegisterState(delaySeconds: Double) {
     43         // update isRegistered ==> icon shown
     44         DispatchQueue.main.asyncAfter(deadline: .now() + delaySeconds) {
     45             registerState = isRegistered
     46         }
     47     }
     48 
     49     private var dismissAlertButton: some View {
     50         Button("Cancel", role: .cancel) {
     51             pushNotifications = false
     52             mayNotUsePush = false
     53         }
     54     }
     55 
     56     private var openSettingsButton: some View {
     57         Button("Open Settings") {
     58             mayNotUsePush = false
     59             UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [:])
     60         }
     61     }
     62 
     63     func checkPushNotifications(_ shouldUsePush: Bool) {
     64 #if TALER_NIGHTLY
     65         // 1. Request authorisation for remote (push) notifications.
     66         //    For background-only (silent) pushes the alert/badge/sound
     67         //    entitlements are not strictly required, but requesting them
     68         //    avoids surprises when you later add user-visible notifications.
     69         DispatchQueue.main.async {
     70             if isRegistered {
     71                 registerState = true
     72                 if shouldUsePush {
     73                     self.symLog.log("already registered for remote notifications")
     74                 } else {
     75                     self.symLog.log("unregisterForRemoteNotifications")
     76                     UIApplication.shared.unregisterForRemoteNotifications()
     77                     controller.deviceTokenAPNs = nil                            // TODO: tell walletCore
     78                     checkRegisterState(delaySeconds: 0.5)
     79                 }
     80             } else {
     81                 registerState = false
     82                 if !shouldUsePush {
     83                     self.symLog.log("remote notifications are disabled")
     84                 } else {
     85                     UNUserNotificationCenter.current().requestAuthorization(
     86                         options: [.alert, .badge, .sound]
     87                     ) { granted, error in
     88                         DispatchQueue.main.async {
     89                             if granted {
     90                                 self.symLog.log("registerForRemoteNotifications")
     91                                 /// result in didRegisterForRemoteNotificationsWithDeviceToken
     92                                 UIApplication.shared.registerForRemoteNotifications()
     93                                 checkRegisterState(delaySeconds: 0.5)
     94                             } else {
     95                                 self.symLog.log("Error requesting notification permissions: \(error?.localizedDescription ?? "unknown")")
     96                                 mayNotUsePush = true
     97                                 registerState = false
     98                                 pushNotifications = false
     99                             }
    100                         }
    101                     }
    102                 }
    103             }
    104         }
    105 #endif
    106 
    107     }
    108     // MARK: -
    109     @ViewBuilder func aboutV(_ localizedAppName: String) -> some View {
    110         let aboutStr = String(localized: "About \(localizedAppName)")
    111         NavigationLink {        // whole row like in a tableView
    112             AboutView(stack: stack.push(),
    113                     appName: localizedAppName,
    114                    navTitle: aboutStr)
    115         } label: {
    116             SettingsItem(name: aboutStr, id1: "about", imageName: TALER_LOGO) {}
    117         }
    118     }
    119     // MARK: -
    120     @ViewBuilder func paymentServicesV() -> some View {
    121         NavigationLink {
    122             ExchangeListView(stack: stack.push(), url: .constant(nil))
    123         } label: {
    124             SettingsItem(name: TITLE_EXCHANGES, id1: "exchanges",
    125                     imageName: EXCHANGE_LOGO,
    126                   description: String(localized: "Manage payment services")) {}
    127         }
    128     }
    129     // MARK: -
    130     @ViewBuilder func bankAccountsV() -> some View {
    131         let bankAccountsTitle = String(localized: "TitleBankAccounts", defaultValue: "Bank Accounts")
    132         NavigationLink {
    133             BankListView(stack: stack.push(),
    134                       navTitle: bankAccountsTitle)
    135         } label: {
    136             SettingsItem(name: bankAccountsTitle, id1: "bankAccounts",
    137                     imageName: ICONNAME_BANK,
    138                   description: String(localized: "Your accounts for deposit")) {}
    139         }
    140     }
    141     // MARK: -
    142     @ViewBuilder func biometryV() -> some View {
    143         let biometryType = controller.biometryType() ?? biometricService.biometryType()
    144         let hasFaceID = biometryType == .faceID
    145         let hasTouchID = biometryType == .touchID
    146         let biometryString = hasFaceID ? String(localized: "Use FaceID")
    147                            : hasTouchID ? String(localized: "Use TouchID")
    148                                         : EMPTYSTRING
    149         if !biometryString.isEmpty {
    150             SettingsToggle(name: biometryString,
    151                           value: $useAuthentication,
    152                             id1: "useFaceID",
    153                       imageName: hasFaceID ? "faceid" : "touchid", // 􀎽  􀟒
    154                     description: String(localized: "Protect your money")
    155             ) { _ in
    156                 biometricService.isAuthenticated = false
    157             }
    158             // TODO: add another toggle with snail/hare for 60 sec / 10 sec
    159         }
    160     }
    161 #if GNU_TALER || TALER_NIGHTLY
    162     @ViewBuilder func talDirV() -> some View {
    163         let talDirTitle = String(localized: "TitleTalDir", defaultValue: "Taler Directory")
    164         NavigationLink {
    165             TalDirSettingsView(stack: stack.push(),
    166                             navTitle: talDirTitle)
    167         } label: {
    168             SettingsItem(name: talDirTitle, id1: "talDir",
    169                     imageName: ICONNAME_PERSON3,
    170                   description: String(localized: "Your payment contacts", comment: "TalDir")) {}
    171         }
    172     }
    173 #endif
    174     @ViewBuilder func pushNotificationsV() -> some View {
    175         SettingsToggle(name: String(localized: "Push Notifications"),
    176                       value: $pushNotifications, id1: "pushNotifications",
    177                   imageName: registerState ? NOTIFICATION2 : NOTIFICATION1,  // 􀝖 or 􀋙
    178                 description: String(localized: "Get notified when pending payments change status")
    179         ) { newVal in
    180             checkPushNotifications(newVal)
    181         }
    182     }
    183 #if !TALER_WALLET
    184     @ViewBuilder func useNymV() -> some View {
    185         SettingsToggle(name: "NYM mixnet",
    186                       value: $useMixnet,
    187                         id1: "useMixnet",
    188                   imageName: "eye.slash", // 􀋯
    189                 description: String(localized: "Protect your privacy"))
    190     }
    191 #endif // !TALER_WALLET
    192     // MARK: -
    193     @ViewBuilder func minimalV() -> some View {
    194         SettingsToggle(name: String(localized: "Minimalistic"), value: $minimalistic, id1: "minimal",
    195                   imageName: "heart",                   // 􀊴
    196                 description: String(localized: "Omit text where possible"))
    197     }
    198     // MARK: -
    199     @ViewBuilder func warningsV() -> some View {
    200         SettingsToggle(name: String(localized: "Show Warnings"),
    201                       value: $shouldShowWarning,
    202                         id1: "warnings",
    203                   imageName: WARNING,           // 􀇾
    204                 description: String(localized: "For Delete, Abandon & Abort buttons"))
    205     }
    206     // MARK: -
    207     @ViewBuilder func reportV(_ localizedAppName: String) -> some View {
    208         let reportTitle = String(localized: "TitleReport", defaultValue: "Report diagnostics")
    209         NavigationLink {
    210             ReportView(stack: stack.push(reportTitle),
    211                     navTitle: reportTitle)
    212         } label: {
    213             SettingsItem(name: reportTitle, id1: "report",
    214                     imageName: "arrow.up.message",    // 􀜃
    215                   description: String(localized: "Help improve \(localizedAppName)")) {}
    216         }
    217     }
    218     // MARK: -
    219     @ViewBuilder func developerV(_ localizedAppName: String) -> some View {
    220         let devTitle = String(localized: "TitleDeveloper", defaultValue: "Developer")
    221         NavigationLink {
    222             DebugSettingsView(stack: stack.push(devTitle),
    223                            navTitle: devTitle)
    224         } label: {
    225             SettingsItem(name: devTitle, id1: "developer",
    226                     imageName: "hammer",        // 􀙄
    227                   description: String(localized: "Help debug \(localizedAppName)")) {}
    228         }
    229     }
    230     // MARK: -
    231     @ViewBuilder func moreV() -> some View {
    232         let moreItem = String(localized: "TitleMore", defaultValue: "More")
    233         let moreTitle = String(localized: "TitleMoreSettings", defaultValue: "More Settings")
    234         NavigationLink {
    235             MoreSettingsView(stack: stack.push(),
    236                           navTitle: moreTitle)
    237         } label: {
    238             SettingsItem(name: moreItem, id1: "more",
    239                     imageName: "ellipsis",          // 􀍠
    240                   description: nil) {}
    241         }
    242     }
    243     // MARK: -
    244     var body: some View {
    245 #if PRINT_CHANGES
    246         let _ = Self._printChanges()
    247         let _ = symLog.vlog()       // just to get the # to compare it with .onAppear & onDisappear
    248 #endif
    249         let localizedAppName = controller.localizedAppName
    250         let list = List {
    251             aboutV(localizedAppName)        // About... is always the first
    252             paymentServicesV()
    253             bankAccountsV()
    254             biometryV()
    255 #if GNU_TALER || TALER_NIGHTLY
    256             talDirV()
    257 #endif
    258 #if TALER_NIGHTLY
    259             pushNotificationsV()
    260                 .alert("Push Notifications are disabled",
    261                    isPresented: $mayNotUsePush,
    262                    actions: { openSettingsButton
    263                               dismissAlertButton },
    264                    message: { Text("Please go to Settings > \(localizedAppName) > Notifications and turn them on.") }
    265                 )
    266 #endif
    267 #if !TALER_WALLET
    268             useNymV()
    269 #endif
    270             minimalV()
    271             warningsV()
    272             reportV(localizedAppName)
    273 
    274 #if DEBUG
    275             let showDiagnostic = true
    276 #else
    277             let showDiagnostic = controller.diagnosticModeEnabled
    278 #endif
    279             if showDiagnostic {
    280                 developerV(localizedAppName)
    281             }
    282             moreV()                         // More... is always the last
    283         }
    284             .id(listID)
    285             .listStyle(myListStyle.style).anyView
    286             .navigationTitle(navTitle)
    287             .onAppear() {
    288                 DebugViewC.shared.setViewID(VIEW_SETTINGS, stack: stack.push())
    289                 registerState = isRegistered
    290             }
    291         if #available(iOS 26.0, *) {
    292             list
    293         } else {
    294             list
    295                 .padding(.bottom)
    296         }
    297     } // body
    298 }
    299 // MARK: -
    300 #if DEBUG
    301 //struct SettingsView_Previews: PreviewProvider {
    302 //    static var previews: some View {
    303 //        SettingsView(stack: CallStack("Preview"), balances: <#Binding<[Balance]>#>, navTitle: "Settings")
    304 //    }
    305 //}
    306 #endif