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 }