taler-ios

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

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 }