SendAmountView.swift (9033B)
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 // Called by SendAmountV when tapping [Send] 13 struct SendAmountView: View { 14 private let symLog = SymLogV(0) 15 let stack: CallStack 16 let cash: OIMcash 17 let balance: Balance 18 @Binding var buttonSelected: Bool 19 @Binding var amountLastUsed: Amount 20 @Binding var amountToTransfer: Amount 21 @Binding var amountAvailable: Amount 22 @Binding var summary: String 23 @Binding var iconID: String? 24 25 @EnvironmentObject private var controller: Controller 26 @EnvironmentObject private var model: WalletModel 27 @AppStorage("minimalistic") var minimalistic: Bool = false 28 29 @State var peerPushCheck: CheckPeerPushDebitResponse? = nil 30 @State private var expireDays = SEVENDAYS 31 @State private var insufficient = false 32 // @State private var feeAmount: Amount? = nil 33 @State private var feeString = (EMPTYSTRING, EMPTYSTRING) 34 @State private var shortcutSelected = false 35 @State private var amountShortcut = Amount.zero(currency: EMPTYSTRING) // Update currency when used 36 @State private var exchange: Exchange? = nil // wg. noFees 37 38 private func shortcutAction(_ shortcut: Amount) { 39 amountShortcut = shortcut 40 shortcutSelected = true 41 } 42 private func buttonAction() { buttonSelected = true } 43 44 public static func navTitle(_ currency: String, _ condition: Bool = false) -> String { 45 condition ? String(localized: "NavTitle_Send_Currency", 46 defaultValue: "Send \(currency)", 47 comment: "NavTitle: Send 'currency'") 48 : String(localized: "NavTitle_Send", 49 defaultValue: "Send", 50 comment: "NavTitle: Send") 51 } 52 53 private func feeLabel(_ feeStr: String) -> String { 54 feeStr.isEmpty ? EMPTYSTRING : String(localized: "+ \(feeStr) fee") 55 } 56 57 private func fee(raw: Amount, effective: Amount) -> Amount? { 58 do { // Outgoing: fee = effective - raw 59 let fee = try effective - raw 60 return fee 61 } catch {} 62 return nil 63 } 64 65 private func feeIsNotZero() -> Bool? { 66 if let hasNoFees = exchange?.noFees { 67 if hasNoFees { 68 return nil // this exchange never has fees 69 } 70 } 71 return peerPushCheck == nil ? false 72 : true // TODO: !(feeAmount?.isZero ?? false) 73 } 74 75 @MainActor 76 private func computeFee(_ amount: Amount) async -> ComputeFeeResult? { 77 if amount.isZero { 78 return ComputeFeeResult.zero() 79 } 80 let insufficient = (try? amount > amountAvailable) ?? true 81 if insufficient { 82 return ComputeFeeResult.insufficient() 83 } 84 do { 85 let ppCheck = try await model.checkPeerPushDebit(amount, scope: balance.scopeInfo, viewHandles: true) 86 let raw = ppCheck.amountRaw 87 let effective = ppCheck.amountEffective 88 if let fee = fee(raw: raw, effective: effective) { 89 feeString = fee.formatted(balance.scopeInfo, isNegative: false) 90 symLog.log("Fee = \(feeString.0)") 91 let insufficient = (try? effective > amountAvailable) ?? true 92 93 peerPushCheck = ppCheck 94 let feeLabel = (feeLabel(feeString.0), feeLabel(feeString.1)) 95 // announce("\(amountVoiceOver), \(feeLabel)") 96 return ComputeFeeResult(insufficient: insufficient, 97 feeAmount: fee, 98 feeStr: feeLabel, 99 numCoins: nil) 100 } else { 101 peerPushCheck = nil 102 } 103 } catch { 104 // handle cancel, errors 105 symLog.log("❗️ \(error), \(error.localizedDescription)") 106 switch error { 107 case let walletError as WalletBackendError: 108 switch walletError { 109 case .walletCoreError(let wError): 110 if wError?.code == 7027 { 111 if let details = wError?.insufficientBalanceDetails { 112 symLog.log(details.causeHint?.localizedCause(amount.currencyStr)) 113 } 114 // TODO: [10.2] AmountInputV ignores the insufficient flag, 115 // so neither this verdict nor its localized cause is shown 116 return ComputeFeeResult.insufficient() 117 } 118 default: break 119 } 120 default: break 121 } 122 } 123 return nil 124 } 125 126 @MainActor 127 private func newBalance() async { 128 let scope = balance.scopeInfo 129 symLog.log("❗️ task \(scope.currency)") 130 131 if let available = try? await model.getMaxPeerPushDebitAmount(scope, viewHandles: true) { 132 amountAvailable = available 133 } else { 134 amountAvailable = balance.available 135 } 136 } 137 138 var body: some View { 139 #if PRINT_CHANGES 140 let _ = Self._printChanges() 141 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 142 #endif 143 Group { 144 let availableStr = amountAvailable.formatted(balance.scopeInfo, isNegative: false) 145 // let availableA11y = amountAvailable.formatted(currencyInfo, isNegative: false, useISO: true, a11y: ".") 146 // let amountVoiceOver = amountToTransfer.formatted(currencyInfo, isNegative: false) 147 // let insufficientLabel2 = String(localized: "but you only have \(availableStr) to send.") 148 149 let inputDestination = P2PSubjectV(stack: stack.push(), 150 cash: cash, 151 scope: balance.scopeInfo, 152 available: balance.available, 153 feeLabel: (feeLabel(feeString.0), feeLabel(feeString.1)), 154 feeIsNotZero: feeIsNotZero(), 155 outgoing: true, 156 amountToTransfer: $amountToTransfer, // from the textedit 157 summary: $summary, 158 iconID: $iconID, 159 expireDays: $expireDays) 160 let shortcutDestination = P2PSubjectV(stack: stack.push(), 161 cash: cash, 162 scope: balance.scopeInfo, 163 available: balance.available, 164 feeLabel: nil, 165 feeIsNotZero: feeIsNotZero(), 166 outgoing: true, 167 amountToTransfer: $amountShortcut, // from the tapped shortcut button 168 summary: $summary, 169 iconID: $iconID, 170 expireDays: $expireDays) 171 let actions = Group { 172 NavLink($buttonSelected) { inputDestination } 173 NavLink($shortcutSelected) { shortcutDestination } 174 } 175 let a11yLabel = String(localized: "Amount to send:", comment: "a11y, no abbreviations") 176 AmountInputV(stack: stack.push(), 177 scope: balance.scopeInfo, 178 amountAvailable: $amountAvailable, 179 amountLabel: nil, // will use "Available for transfer: xxx", trailing 180 a11yLabel: a11yLabel, 181 amountToTransfer: $amountToTransfer, 182 amountLastUsed: amountLastUsed, 183 wireFee: nil, 184 summary: $summary, 185 shortcutAction: shortcutAction, 186 buttonAction: buttonAction, 187 isIncoming: false, 188 computeFee: computeFee) 189 .background(actions) 190 } // Group 191 .task(id: balance) { await newBalance() } 192 .onAppear { 193 DebugViewC.shared.setViewID(VIEW_P2P_SEND, stack: stack.push()) 194 symLog.log("❗️ onAppear") 195 } 196 } // body 197 }