taler-ios

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

AmountInputV.swift (6418B)


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