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 }