ExchangeSectionView.swift (9125B)
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 /// This view shows the currency name in an exchange section 13 /// currency 14 struct ExchangeSectionView: View { 15 private let symLog = SymLogV(0) 16 let stack: CallStack 17 let balance: Balance 18 let thousand: Int // index of balance times 1000 (MAXEXCHANGES per currency) 19 let developerMode: Bool 20 let deleteTask: () -> Void 21 22 @EnvironmentObject private var model: WalletModel 23 @EnvironmentObject private var controller: Controller 24 @AppStorage("minimalistic") var minimalistic: Bool = false 25 @AppStorage("demoHints") var demoHints: Bool = true 26 @AppStorage("fakeNoFees") var fakeNoFees: Bool = true 27 28 @State private var exchanges: [Exchange] = [] 29 @State private var reloadTransactions: Int = 0 30 @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) 31 @State private var withDrawStarted = false 32 @State private var didDelete: Bool = false 33 @State private var disabled: Bool = false 34 @State private var showAlert: Bool = false 35 @State private var global: Bool = false 36 @State private var legacy: Bool = false 37 38 @MainActor 39 private func viewDidLoad() async { 40 if let exc = try? await model.listExchanges(scope: balance.scopeInfo) { 41 if let exchange = exc.first { 42 global = exchange.scopeInfo.type == .global 43 legacy = exchange.scopeInfo.type == .exchangeLegacyKeys 44 } 45 withAnimation { exchanges = exc } 46 } 47 } 48 49 @MainActor 50 private func currencyTickerChanged(_ scopeInfo: ScopeInfo) async { 51 symLog.log("task \(didDelete ? 1 : 0)") 52 currencyInfo = controller.info(for: scopeInfo, controller.currencyTicker) 53 } 54 55 /// purge must never be remembered - it deletes the exchange even if it still holds coins, 56 /// so it may only ever come from the escalated alert (warningText2/3) which says so 57 @MainActor 58 private func deleteExchange(purge: Bool) { 59 disabled = true // don't try this more than once 60 Task { // runs on MainActor 61 if let exchange = exchanges.first { 62 let baseUrl = exchange.exchangeBaseUrl 63 if let _ = try? await model.deleteExchange(url: baseUrl, purge: purge, viewHandles: !purge) { 64 symLog.log("deleted \(baseUrl.trimURL)") 65 didDelete = true // change button text 66 NotificationCenter.default.post(name: .ExchangeDeleted, object: nil, userInfo: nil) 67 NotificationCenter.default.post(name: .BalanceChange, object: nil, userInfo: nil) 68 demoHints = true 69 deleteTask() 70 } else { 71 showAlert = true 72 disabled = false 73 } 74 } 75 } 76 } 77 78 @MainActor 79 private func setGlobal() { 80 if let exchange = exchanges.first { 81 let scope = exchange.scopeInfo 82 Task { 83 if scope.type != .global { 84 try? await model.addGlobalCurrencyExchange(currency: exchange.scopeInfo.currency, 85 baseUrl: exchange.exchangeBaseUrl, 86 masterPub: exchange.masterPub ?? EMPTYSTRING) 87 } else { 88 try? await model.rmvGlobalCurrencyExchange(currency: exchange.scopeInfo.currency, 89 baseUrl: exchange.exchangeBaseUrl, 90 masterPub: exchange.masterPub ?? EMPTYSTRING) 91 } 92 } 93 } 94 } 95 var body: some View { 96 #if PRINT_CHANGES 97 let _ = Self._printChanges() 98 // let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 99 #endif 100 let scopeInfo = balance.scopeInfo 101 let currency = scopeInfo.currency 102 Section { 103 ForEachWithIndex(array: exchanges) { index, exchange in 104 let _ = symLog.log("Index: \(thousand + index)") 105 ExchangeRowView(stack: stack.push(), 106 exchange: exchange, 107 index: thousand + index + 1, 108 developerMode: developerMode) 109 .listRowSeparator(.hidden) 110 } 111 if developerMode && (DEMOCURRENCY == currency || TESTCURRENCY == currency) { 112 SettingsToggle(name: String("Global"), value: $global.onChange({ isGlobal in 113 setGlobal() 114 }), id1: "global", 115 description: String("Treat this as a global exchange")) 116 .listRowSeparator(.hidden) 117 SettingsToggle(name: String("Fake no fees"), value: $fakeNoFees, 118 id1: "fakeNoFees", 119 description: String("Remove fee and gros from details")) 120 } 121 if DEMOCURRENCY == currency { 122 let title = String(localized: "LinkTitle_Test_Money2", defaultValue: "Get more demo cash") 123 Button(title) { 124 withDrawStarted = true // don't run twice 125 Task { // runs on MainActor 126 let amount = Amount(currency: DEMOCURRENCY, cent: 1000) 127 symLog.log("Withdraw 10 KUDOS") 128 try? await model.loadTestKudos(0, amount: amount) 129 DispatchQueue.main.asyncAfter(deadline: .now() + 5) { 130 withDrawStarted = false 131 } 132 } 133 } 134 .buttonStyle(TalerButtonStyle(type: .bordered, narrow: false, disabled: withDrawStarted, aligned: .center)) 135 .disabled(withDrawStarted) 136 137 SettingsToggle(name: String(localized: "Demo Hints"), value: $demoHints, 138 id1: "demoHints", 139 description: String(localized: "Show hints for demo money")) 140 .listRowSeparator(.hidden) 141 142 let bankingHint = String(localized: "Since the demo bank supports the Taler integration, you can start a withdrawal directly on the") 143 let linkTitle = String(localized: "LinkTitle_DEMOBANK", defaultValue: "Demo Bank Website") 144 if demoHints { 145 VStack { 146 if !minimalistic { 147 Text(bankingHint) 148 } 149 Link(linkTitle, destination: URL(string: DEMOBANK)!) 150 .buttonStyle(TalerButtonStyle(type: .bordered, narrow: false, aligned: .center)) 151 } 152 .accessibilityElement(children: .combine) 153 .accessibilityLabel(bankingHint + SPACE + linkTitle) 154 .padding(.top) 155 .listRowSeparator(.hidden) 156 } 157 } 158 let dialect = exchanges.first?.bankComplianceLanguage 159 let buttonTitle = localizedString(.deleteExchange, forDialect: dialect) 160 let warningText1 = localizedString(.warningText1, forDialect: dialect) 161 let warningText2 = localizedString(.warningText2, forDialect: dialect) 162 let warningText3 = localizedString(.warningText3, forDialect: dialect, currency) 163 WarningButton(warningText: warningText1, 164 buttonTitle: buttonTitle, 165 buttonIcon: "trash", 166 role: .destructive, // optional: use WalletColors().errorColor 167 disabled: $disabled, 168 action: { deleteExchange(purge: false) }) 169 .padding(.top) 170 .alert(warningText2, isPresented: $showAlert, actions: { 171 Button("Cancel", role: .cancel) { 172 showAlert = false 173 } 174 Button("Deposit") { 175 showAlert = false 176 // dismissTop(stack.push()) don't do this - will dismiss the Deposit view 177 NotificationCenter.default.post(name: .DepositAction, object: nil) // will trigger NavigationLink 178 } 179 Button(buttonTitle) { 180 deleteExchange(purge: true) 181 showAlert = false 182 } 183 }, message: { 184 Text(warningText3) 185 }) 186 } header: { 187 BarGraphHeader(stack: stack.push(), 188 scope: scopeInfo, 189 reloadTransactions: $reloadTransactions) 190 } 191 .task { await viewDidLoad() } 192 .task(id: controller.currencyTicker) { await currencyTickerChanged(scopeInfo) } 193 .onDisappear() { 194 disabled = false 195 } 196 } 197 }