taler-ios

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

AmountInputV.swift (6116B)


      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 struct ComputeFeeResult {
     13     let insufficient: Bool
     14     let feeAmount: Amount?
     15     let feeStr: (String, String)
     16     let numCoins: Int?
     17 
     18     static func zero() -> ComputeFeeResult {
     19         ComputeFeeResult(insufficient: false,
     20                             feeAmount: nil,
     21                                feeStr: (EMPTYSTRING, EMPTYSTRING),
     22                              numCoins: 0)
     23     }
     24     static func insufficient() -> ComputeFeeResult {
     25         ComputeFeeResult(insufficient: true,
     26                             feeAmount: nil,
     27                                feeStr: (EMPTYSTRING, EMPTYSTRING),
     28                              numCoins: -1)
     29     }
     30 }
     31 // MARK: -
     32 struct AmountInputV: View {
     33     private let symLog = SymLogV(0)
     34     let stack: CallStack
     35     let scope: ScopeInfo?
     36     @Binding var amountAvailable: Amount
     37     let amountLabel: String?
     38     let a11yLabel: String
     39     @Binding var amountToTransfer: Amount
     40     let amountLastUsed: Amount
     41     let wireFee: Amount?
     42     @Binding var summary: String
     43 //    @Binding var insufficient: Bool
     44 //    @Binding var feeAmount: Amount?
     45     let shortcutAction: ((_ amount: Amount) -> Void)?
     46     let buttonAction: () -> Void
     47 //    let feeIsNegative: Bool
     48     let isIncoming: Bool
     49     let computeFee: ((_ amount: Amount) async -> ComputeFeeResult?)?
     50 
     51     @EnvironmentObject private var controller: Controller
     52     @Environment(\.colorScheme) private var colorScheme
     53     @Environment(\.colorSchemeContrast) private var colorSchemeContrast
     54 
     55     @State private var feeAmount: Amount? = nil
     56     @State private var feeStr = (EMPTYSTRING, EMPTYSTRING)
     57     @State private var numCoins: Int?
     58     @State private var coreInsufficient: Bool = false    // wallet-core's own verdict
     59 
     60     struct Flags {
     61         let insufficient: Bool
     62         let disabled: Bool
     63     }
     64 
     65     func checkAvailable(_ coinData: CoinData) -> Flags {
     66         let isZero = amountToTransfer.isZero
     67         // wallet-core's verdict is authoritative and comes first: it accounts for fees,
     68         // age restrictions, per-exchange limits and pending refreshes, none of which a
     69         // local `amountToTransfer > amountAvailable` can see.
     70         if coreInsufficient {
     71             return Flags(insufficient: true, disabled: true)
     72         }
     73         if !amountAvailable.isZero {
     74             do {
     75                 let insufficient: Bool
     76 //                if let feeAmount {
     77 //                    if feeIsNegative {
     78 //                        insufficient = try amountToTransfer > amountAvailable
     79 //                    } else {
     80 //                        insufficient = try (amountToTransfer + feeAmount) > amountAvailable
     81 //                    }
     82 //                } else {
     83                     insufficient = try amountToTransfer > amountAvailable
     84 //                }
     85                 let disabled = insufficient || isZero || coinData.invalid || coinData.tooMany
     86                 return Flags(insufficient: insufficient, disabled: disabled)
     87             } catch {
     88                 // TODO: error Amounts don't match
     89                 symLog.log("❗️Cannot compare amountAvailable.\(amountAvailable.currencyStr) to amountToTransfer.\(amountToTransfer.currencyStr))")
     90             }
     91         }
     92         // No usable local bound (an incoming amount, or a caller that never sets
     93         // amountAvailable — see PayTemplateScan.swift:36). Deliberately NOT failing closed
     94         // here: those callers still get the wallet-core verdict above, and failing closed
     95         // would permanently disable a pay-template that has no availability to compare to.
     96         return Flags(insufficient: false, disabled: isZero || coinData.invalid || coinData.tooMany)
     97     }
     98 
     99     var body: some View {
    100         let currency = amountToTransfer.currencyStr
    101         ScrollView { VStack(alignment: .trailing) {
    102             CurrencyInputView(scope: scope,
    103                              amount: $amountToTransfer,
    104                      amountLastUsed: amountLastUsed,
    105                           available: isIncoming ? nil : amountAvailable,
    106                               title: amountLabel,
    107                           a11yTitle: a11yLabel,
    108                      shortcutAction: shortcutAction)
    109 //                .accessibility(sortPriority: 2)
    110 
    111             let coinData = CoinData(coins: numCoins, fee: feeAmount)
    112             QuiteSomeCoins(scope: scope,
    113                         coinData: coinData,
    114                    shouldShowFee: true,       // TODO: set to false if we never charge withdrawal fees
    115                    feeIsNegative: isIncoming)
    116             let flags = checkAvailable(coinData)
    117             let hint = String(localized: "enabled when amount is non-zero", comment: "a11y")
    118             Button("Next") { buttonAction() }
    119                 .buttonStyle(TalerButtonStyle(type: .prominent, disabled: flags.disabled))
    120                 .disabled(flags.disabled)
    121                 .accessibilityHint(flags.disabled ? hint : EMPTYSTRING)
    122                 .padding(.bottom, 333)
    123         }.padding(.horizontal) } // ScrollVStack
    124         .frame(maxWidth: .infinity, alignment: .leading)
    125         .background(FullBackground())
    126         .task(id: amountToTransfer.value) {
    127             // re-compute the fees on every tapped digit or backspace
    128             if let computeFee {
    129                 symLog.log(".task \(amountToTransfer.value)")
    130                 if let result: ComputeFeeResult = await computeFee(amountToTransfer) {
    131                     symLog.log("computeFee() finished")
    132                     feeStr = result.feeStr
    133                     coreInsufficient = result.insufficient
    134                     feeAmount = result.feeAmount
    135                     numCoins = result.numCoins
    136                 } else {
    137                     // No verdict this round - keep the previous one rather than silently
    138                     // treating an unanswered question as "you can afford it".
    139                     symLog.log("computeFee() failed ❗️") //  \(error)")
    140                 }
    141             }
    142         }
    143     }
    144 }