taler-ios

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

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 }