taler-ios

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

WithdrawURIView.swift (10499B)


      1 /*
      2  * This file is part of GNU Taler, ©2022-25 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 // Called either when scanning a QR code or tapping the provided link, both from the bank's website.
     13 // We show the user the bank-integrated withdrawal details in a sheet - but first the ToS must be accepted.
     14 // After the user confirmed the withdrawal, we show a button to return to the bank website to authorize (2FA)
     15 struct WithdrawURIView: View {
     16     private let symLog = SymLogV(0)
     17     let stack: CallStack
     18 
     19     // the URL from the bank website
     20     let url: URL
     21 
     22     @EnvironmentObject private var controller: Controller
     23     @EnvironmentObject private var model: WalletModel
     24     @AppStorage("minimalistic") var minimalistic: Bool = false
     25     @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic
     26 
     27     @State private var withdrawUriInfo: WithdrawUriInfoResponse? = nil
     28     @State private var amountIsEditable = false
     29     @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING)    // Update currency when used
     30     @State private var amountShortcut = Amount.zero(currency: EMPTYSTRING)      // Update currency when used
     31     @State private var amountLastUsed = Amount.zero(currency: EMPTYSTRING)      // Update currency when used
     32     @State private var amountZero = Amount.zero(currency: EMPTYSTRING)          // needed for isZero
     33     @State private var amountLastChecked = Amount.zero(currency: EMPTYSTRING)   //
     34     @State private var buttonSelected = false
     35     @State private var shortcutSelected = false
     36     @State private var amountAvailable: Amount? = nil
     37     @State private var wireFee: Amount? = nil
     38     @State private var summary = EMPTYSTRING
     39 
     40     @State private var selectedExchange = EMPTYSTRING
     41     @State private var exchange: Exchange? = nil
     42     @State private var possibleExchanges: [Exchange] = []
     43     @State private var defaultExchangeBaseUrl: String?     // if nil then use possibleExchanges
     44     @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN)
     45 
     46 //    @State private var feeAmount: Amount? = nil
     47     @State private var withdrawalDetails: WithdrawalDetailsForAmount? = nil
     48 
     49     let navTitle = String(localized: "Withdrawal")
     50 
     51     @MainActor
     52     func loadExchange(_ baseUrl: String) async {        // TODO: throws?
     53         if let someExchange = try? await model.getExchangeByUrl(url: baseUrl) {
     54             symLog.log("Loaded \(baseUrl.trimURL)")
     55             exchange = someExchange
     56         }
     57     }
     58 
     59     private func shortcutAction(_ shortcut: Amount) {
     60         amountShortcut = shortcut
     61         shortcutSelected = true
     62     }
     63     private func buttonAction() { buttonSelected = true }
     64 
     65     private func feeLabel(_ feeString: String) -> String {
     66         feeString.count > 0 ? String(localized: "\(feeString) fee")
     67         : EMPTYSTRING
     68     }
     69 
     70     @MainActor
     71     private func computeFeeWithdraw(_ amount: Amount) async -> ComputeFeeResult? {
     72         if let exchange {
     73           if amountLastChecked != amount {
     74             amountLastChecked = amount
     75 
     76             if let details = try? await model.getWithdrawalDetailsForAmount(amount,
     77                                                                    baseUrl: exchange.exchangeBaseUrl,
     78                                                                      scope: nil) {     // TODO: scope
     79                 let fee = try? details.amountRaw - details.amountEffective
     80                 let feeStr = fee?.formatted(currencyInfo, isNegative: true) ?? ("0", "0")
     81                 symLog.log("Fee = \(feeStr.0)")
     82                 let insufficient = if let amountAvailable {
     83                     (try? details.amountRaw < amountAvailable) ?? true
     84                 } else {
     85                     false
     86                 }
     87                 withdrawalDetails = details
     88                 return ComputeFeeResult(insufficient: insufficient, feeAmount: fee,
     89                                               feeStr: (feeLabel(feeStr.0), feeLabel(feeStr.1)),
     90                                             numCoins: details.numCoins)
     91             } else {
     92                 // TODO: don't call this again
     93             }
     94           }
     95         } else {
     96             symLog.log("No exchange!")
     97         }
     98         return nil
     99     }
    100 
    101     @MainActor
    102     private func viewDidLoad() async {
    103         symLog.log(".task")
    104         do {
    105             let uriInfoResponse = try await model.getWithdrawalDetailsForUri(url.absoluteString)
    106             let amount = uriInfoResponse.amount
    107             let currency = amount?.currencyStr ?? uriInfoResponse.currency
    108             amountToTransfer = amount ?? Amount.zero(currency: currency)
    109             amountIsEditable = uriInfoResponse.editableAmount
    110             amountAvailable = uriInfoResponse.maxAmount     // may be nil
    111             if let available = amountAvailable {
    112                 amountZero = available
    113             }
    114             wireFee = uriInfoResponse.wireFee               // may be nil
    115             let baseUrl = uriInfoResponse.defaultExchangeBaseUrl
    116                        ?? uriInfoResponse.possibleExchanges[0].exchangeBaseUrl
    117             defaultExchangeBaseUrl = uriInfoResponse.defaultExchangeBaseUrl
    118             possibleExchanges = uriInfoResponse.possibleExchanges
    119 
    120 //            await loadExchange(baseUrl)       <- checkCurrencyInfo now returns the exchange
    121 //            symLog.log("\(baseUrl.trimURL) loaded")
    122 
    123             if let someExchange = try? await controller.checkCurrencyInfo(for: baseUrl, model: model) {
    124                 symLog.log("Info(for: \(baseUrl.trimURL) loaded")
    125                 exchange = someExchange
    126             }
    127         } catch {
    128             // TODO: error, dismiss
    129         }
    130     }
    131 
    132     var body: some View {
    133 #if PRINT_CHANGES
    134         let _ = Self._printChanges()
    135         let _ = symLog.vlog(url.absoluteString)       // just to get the # to compare it with .onAppear & onDisappear
    136 #endif
    137         if let exchange2 = exchange {
    138             if let defaultBaseUrl = defaultExchangeBaseUrl ?? possibleExchanges.first?.exchangeBaseUrl {
    139                 VStack {
    140                     let title = String(localized: "using:", comment: "using: exchange.taler.net")
    141                     if possibleExchanges.count > 1 {
    142                         Picker(title, selection: $selectedExchange) {
    143                             ForEach(possibleExchanges, id: \.self) { exchange in
    144                                 let baseUrl = exchange.exchangeBaseUrl
    145                                 Text(baseUrl)
    146                             }
    147                         }
    148                         .talerFont(.title3)
    149                         .pickerStyle(.menu)
    150                         .onAppear() {  selectedExchange = defaultBaseUrl }
    151                         .onChange(of: selectedExchange) { selected in
    152                             Task { await loadExchange(selected) }
    153                         }
    154                     } // load defaultBaseUrl
    155                     let acceptDestination = WithdrawAcceptView(stack: stack.push(),
    156                                                                  url: url,
    157                                                                scope: exchange2.scopeInfo,
    158                                                     amountToTransfer: $amountToTransfer,
    159                                                              wireFee: $wireFee,
    160                                                             exchange: $exchange)        // from possibleExchanges
    161                     if amountIsEditable {
    162                         ScrollView {
    163                             let shortcutDest = WithdrawAcceptView(stack: stack.push(),
    164                                                                     url: url,
    165                                                                   scope: exchange2.scopeInfo,
    166                                                        amountToTransfer: $amountShortcut,
    167                                                                 wireFee: $wireFee,
    168                                                                exchange: $exchange)    // from possibleExchanges
    169                             let actions = Group {
    170                                 NavLink($shortcutSelected) { shortcutDest }
    171                                 NavLink($buttonSelected) { acceptDestination }
    172                             }
    173                             let a11yLabel = String(localized: "Amount to withdraw:", comment: "a11y, no abbreviations")
    174                             let amountLabel = minimalistic ? String(localized: "Amount:")        // maxAmount
    175                                                            : a11yLabel
    176                             // TODO: input amount, then
    177                             AmountInputV(stack: stack.push(),
    178                                          scope: exchange2.scopeInfo,
    179                                amountAvailable: $amountZero,
    180                                    amountLabel: amountZero.isZero ? amountLabel : nil,
    181                                      a11yLabel: a11yLabel,
    182                               amountToTransfer: $amountToTransfer,
    183                                 amountLastUsed: amountLastUsed,
    184                                        wireFee: wireFee,
    185                                        summary: $summary,
    186                                 shortcutAction: shortcutAction,
    187                                   buttonAction: buttonAction,
    188                                     isIncoming: true,
    189                                     computeFee: computeFeeWithdraw)
    190                             .background(actions)
    191                         } // ScrollView
    192                     } else {
    193                         acceptDestination
    194                     } // directly show the accept view
    195                 }
    196                 .navigationTitle(navTitle)
    197                 .task(id: controller.currencyTicker) {
    198                     currencyInfo = controller.info(for: exchange2.scopeInfo, controller.currencyTicker)
    199                 }
    200                 .onAppear() {
    201                     symLog.log("onAppear")
    202                     DebugViewC.shared.setSheetID(SHEET_WITHDRAWAL)
    203                 }
    204 //              agePicker.setAges(ages: details?.ageRestrictionOptions)
    205             }
    206 
    207 
    208         } else {        // no details or no exchange
    209 #if DEBUG
    210             let message = url.host
    211 #else
    212             let message: String? = nil
    213 #endif
    214             LoadingView(stack: stack.push(), scopeInfo: nil, message: message)
    215                 .task { await viewDidLoad() }
    216         }
    217     }
    218 }