taler-ios

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

ExchangeListView.swift (8271B)


      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 @MainActor
     13 fileprivate
     14 func addExchange(_ exchange: String, model: WalletModel) -> Void {
     15     Task { // runs on MainActor
     16         if let _ = try? await model.addExchange(uri: exchange) {
     17 //            symLog.log("added: \(exchange)")
     18 //            View.announce("added: \(exchange)")
     19             if UIAccessibility.isVoiceOverRunning {
     20                 UIAccessibility.post(notification: .announcement, argument: "added: \(exchange)")
     21             }
     22             Controller.shared.hapticNotification(.success)
     23             NotificationCenter.default.post(name: .ExchangeAdded, object: nil, userInfo: nil)
     24             NotificationCenter.default.post(name: .BalanceChange, object: nil, userInfo: nil)       // TODO: ExchangeAdded should suffice
     25         }
     26     }
     27 }
     28 
     29 /// This view shows the list of exchanges
     30 struct ExchangeListView: View {
     31     private let symLog = SymLogV(0)
     32     let stack: CallStack
     33     @Binding var url: URL?
     34 
     35     @EnvironmentObject private var model: WalletModel
     36     @EnvironmentObject private var controller: Controller
     37 #if DEBUG
     38     @AppStorage("developerMode") var developerMode: Bool = true
     39 #else
     40     @AppStorage("developerMode") var developerMode: Bool = false
     41 #endif
     42 
     43     @State private var showAlert: Bool = false
     44     @State private var newExchange: String = EXCHANGE + DEMO        // without HTTPS
     45 
     46     func checkExchanges() {
     47         var hasNoDEMO = true
     48         var hasNoTEST = true
     49         for balance in controller.balances {
     50             if let baseUrl = balance.scopeInfo.url {
     51                 if baseUrl.contains(EXCHANGE + DEMO) {
     52                     hasNoDEMO = false
     53                 } else if baseUrl.contains(EXCHANGE + TEST) {
     54                     hasNoTEST = false
     55                 }
     56             }
     57         }
     58         newExchange = hasNoDEMO ? EXCHANGE + DEMO
     59                                 : hasNoTEST && developerMode ? EXCHANGE + TEST
     60                                                              : EMPTYSTRING
     61     }
     62 
     63     var body: some View {
     64         let a11yLabelStr = String(localized: "Add payment service", comment: "a11y for the + button")
     65         let plusButton = PlusButton(accessibilityLabelStr: a11yLabelStr) {
     66             showAlert = true
     67         }
     68         let addTitleStr = String(localized: "Add payment service", comment: "title of the addExchange alert")
     69         let addButtonStr = String(localized: "Add", comment: "button in the addExchange alert")
     70         let enterURL = String(localized: "Enter the URL")
     71         let listView = ExchangeListCommonV(symLog: symLog, stack: stack.push(),
     72                                       newExchange:  $newExchange,
     73                                    checkExchanges: checkExchanges)
     74                         .navigationTitle(TITLE_EXCHANGES)
     75                         .navigationBarItems(trailing: plusButton)
     76                         .task {
     77                             if let newUrl = url {
     78                                 newExchange = newUrl.trimmedString
     79                                 showAlert = true
     80                                 url = nil
     81                             } else {
     82                                 checkExchanges()
     83                             }
     84                         }
     85         if #available(iOS 16.4, *) {
     86             listView.alert(addTitleStr, isPresented: $showAlert) {
     87                     TextField("Address of the payment service", text: $newExchange)
     88                         .accessibilityLabel(enterURL)
     89 //                    .textFieldStyle(.roundedBorder)   Yikes: when adding style the alert will stop showing the textfield! Don't do this.
     90                     Button(addButtonStr) {
     91                         addExchange(newExchange, model: model)
     92                         DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
     93                             checkExchanges()
     94                         }
     95                     }
     96                     Button("Cancel", role: .cancel) { }
     97                 } message: {
     98                     Text(enterURL)
     99                         .accessibilityHidden(true)
    100                 }
    101         } else {    // iOS 15 cannot have a textfield in an alert, so we must
    102             listView
    103                 .textFieldAlert(isPresented: $showAlert,
    104                                       title: addTitleStr,
    105                                    doneText: addButtonStr,
    106                                        text: $newExchange) { text in
    107                     addExchange(text, model: model)
    108                 }
    109         }
    110     }
    111 }
    112 // MARK: -
    113 struct ExchangeListCommonV: View {
    114     let symLog: SymLogV?
    115     let stack: CallStack
    116     @Binding var newExchange: String
    117     let checkExchanges: () -> Void
    118 
    119     @EnvironmentObject private var controller: Controller
    120     @EnvironmentObject private var model: WalletModel
    121     @AppStorage("minimalistic") var minimalistic: Bool = false
    122     @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
    123 #if DEBUG
    124     @AppStorage("developerMode") var developerMode: Bool = true
    125 #else
    126     @AppStorage("developerMode") var developerMode: Bool = false
    127 #endif
    128 
    129     var body: some View {
    130 #if PRINT_CHANGES
    131         let _ = Self._printChanges()
    132         let _ = symLog?.vlog()       // just to get the # to compare it with .onAppear & onDisappear
    133 #endif
    134 #if DEBUG
    135         let excSection = Section {
    136             let cyclos = "https://guava.box.fdold.eu"
    137             Button(cyclos) {
    138                 addExchange(cyclos, model: model)
    139             }
    140 
    141             let exchanges = ["glsint.fdold.eu", "taler.magnetbank.hu", "stage.taler-ops.ch", "e.netzbon-basel.ch"]
    142             ForEach(exchanges, id: \.self) { exchange in
    143                 let urlStr = "exchange." + exchange
    144                 Button(urlStr) {
    145                     addExchange(urlStr, model: model)
    146                 }
    147 //                Link(exchange, destination: URL(string: urlStr)!)
    148             }
    149         }
    150 #endif
    151         let balanceList = List {
    152             ForEachWithIndex(array: controller.balances) { index, balance in
    153                 ExchangeSectionView(stack: stack.push(),
    154                                   balance: balance,
    155                                  thousand: index * MAXEXCHANGES,        // unique ID
    156                             developerMode: developerMode) {
    157                     DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    158                         checkExchanges()
    159                     }
    160                 }
    161             }
    162 #if DEBUG
    163             if developerMode {
    164                 excSection
    165             }
    166 #endif
    167         }
    168 
    169         let emptyList = List {
    170             Section {
    171                 Text("There are no payment services yet.")
    172                     .talerFont(.title3)
    173             }
    174             Section {
    175                 let plus = Image(systemName: PLUS)
    176                 if !minimalistic {
    177                     Text("Tap the \(plus) button to add a service.")
    178                         .listRowSeparator(.hidden)
    179                     Text("You can also scan a withdrawal QR code from your bank in the Action menu to automatically add a payment service.")
    180                 } else {
    181                     Text("Tap \(plus) or scan a withdrawal QR code from your bank to add a payment service.")
    182                 }
    183             }
    184                 .talerFont(.body)
    185 #if DEBUG
    186             if developerMode {
    187                 excSection
    188             }
    189 #endif
    190         }
    191 
    192         Group {
    193             if controller.balances.isEmpty {
    194                 emptyList
    195             } else {
    196                 balanceList                 // sorted by wallet-core
    197             }
    198         }
    199         .listStyle(myListStyle.style).anyView
    200 //        .ignoresSafeArea(.keyboard, edges: .bottom)
    201         .refreshable {
    202             controller.hapticNotification(.success)
    203             symLog?.log("refreshing")
    204 //            await reloadExchanges()
    205             NotificationCenter.default.post(name: .BalanceChange, object: nil, userInfo: nil)
    206             DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
    207                 checkExchanges()
    208             }
    209         }
    210         .onAppear() {
    211             DebugViewC.shared.setViewID(VIEW_PAYMENT_SERVICES, stack: stack.push())
    212         }
    213     } // body
    214 }