SendAmountView.swift (9191B)
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 if let effective { 60 let fee = try effective - raw 61 return fee 62 } 63 } catch {} 64 return nil 65 } 66 67 private func feeIsNotZero() -> Bool? { 68 if let hasNoFees = exchange?.noFees { 69 if hasNoFees { 70 return nil // this exchange never has fees 71 } 72 } 73 return peerPushCheck == nil ? false 74 : true // TODO: !(feeAmount?.isZero ?? false) 75 } 76 77 @MainActor 78 private func computeFee(_ amount: Amount) async -> ComputeFeeResult? { 79 if amount.isZero { 80 return ComputeFeeResult.zero() 81 } 82 let insufficient = (try? amount > amountAvailable) ?? true 83 if insufficient { 84 return ComputeFeeResult.insufficient() 85 } 86 do { 87 let ppCheck = try await model.checkPeerPushDebit(amount, scope: balance.scopeInfo, viewHandles: true) 88 let raw = ppCheck.amountRaw 89 if let effective = ppCheck.amountEffective { 90 if let fee = fee(raw: raw, effective: effective) { 91 feeString = fee.formatted(balance.scopeInfo, isNegative: false) 92 symLog.log("Fee = \(feeString.0)") 93 let insufficient = (try? effective > amountAvailable) ?? true 94 95 peerPushCheck = ppCheck 96 let feeLabel = (feeLabel(feeString.0), feeLabel(feeString.1)) 97 // announce("\(amountVoiceOver), \(feeLabel)") 98 return ComputeFeeResult(insufficient: insufficient, 99 feeAmount: fee, 100 feeStr: feeLabel, 101 numCoins: nil) 102 } else { 103 peerPushCheck = nil 104 } 105 } 106 } catch { 107 // handle cancel, errors 108 symLog.log("❗️ \(error), \(error.localizedDescription)") 109 switch error { 110 case let walletError as WalletBackendError: 111 switch walletError { 112 case .walletCoreError(let wError): 113 if wError?.code == 7027 { 114 if let details = wError?.insufficientBalanceDetails { 115 symLog.log(details.causeHint?.localizedCause(amount.currencyStr)) 116 } 117 // TODO: [10.2] AmountInputV ignores the insufficient flag, 118 // so neither this verdict nor its localized cause is shown 119 return ComputeFeeResult.insufficient() 120 } 121 default: break 122 } 123 default: break 124 } 125 } 126 return nil 127 } 128 129 @MainActor 130 private func newBalance() async { 131 let scope = balance.scopeInfo 132 symLog.log("❗️ task \(scope.currency)") 133 134 if let available = try? await model.getMaxPeerPushDebitAmount(scope, viewHandles: true) { 135 amountAvailable = available 136 } else { 137 amountAvailable = balance.available 138 } 139 } 140 141 var body: some View { 142 #if PRINT_CHANGES 143 let _ = Self._printChanges() 144 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 145 #endif 146 Group { 147 // let availableStr = amountAvailable.formatted(balance.scopeInfo, isNegative: false) 148 // let availableA11y = amountAvailable.formatted(currencyInfo, isNegative: false, useISO: true, a11y: ".") 149 // let amountVoiceOver = amountToTransfer.formatted(currencyInfo, isNegative: false) 150 // let insufficientLabel2 = String(localized: "but you only have \(availableStr) to send.") 151 152 let inputDestination = P2PSubjectV(stack: stack.push(), 153 cash: cash, 154 scope: balance.scopeInfo, 155 available: balance.available, 156 feeLabel: (feeLabel(feeString.0), feeLabel(feeString.1)), 157 feeIsNotZero: feeIsNotZero(), 158 outgoing: true, 159 amountToTransfer: $amountToTransfer, // from the textedit 160 summary: $summary, 161 iconID: $iconID, 162 expireDays: $expireDays) 163 let shortcutDestination = P2PSubjectV(stack: stack.push(), 164 cash: cash, 165 scope: balance.scopeInfo, 166 available: balance.available, 167 feeLabel: nil, 168 feeIsNotZero: feeIsNotZero(), 169 outgoing: true, 170 amountToTransfer: $amountShortcut, // from the tapped shortcut button 171 summary: $summary, 172 iconID: $iconID, 173 expireDays: $expireDays) 174 let actions = Group { 175 NavLink($buttonSelected) { inputDestination } 176 NavLink($shortcutSelected) { shortcutDestination } 177 } 178 // let a11yLabel = String(localized: "Amount to send:", comment: "a11y, no abbreviations") 179 AmountInputV(stack: stack.push(), 180 scope: balance.scopeInfo, 181 amountAvailable: $amountAvailable, 182 amountLabel: nil, // will use "Available for transfer: xxx", trailing 183 a11yLabel: EMPTYSTRING, // a11yLabel, 184 amountToTransfer: $amountToTransfer, 185 amountLastUsed: amountLastUsed, 186 wireFee: nil, 187 summary: $summary, 188 shortcutAction: shortcutAction, 189 buttonAction: buttonAction, 190 isIncoming: false, 191 computeFee: computeFee) 192 .background(actions) 193 } // Group 194 .task(id: balance) { await newBalance() } 195 .onAppear { 196 DebugViewC.shared.setViewID(VIEW_P2P_SEND, stack: stack.push()) 197 symLog.log("❗️ onAppear") 198 } 199 } // body 200 }