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