SendAmountView.swift (8627B)
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 return ComputeFeeResult.insufficient() 112 } 113 default: break 114 } 115 default: break 116 } 117 } 118 return nil 119 } 120 121 @MainActor 122 private func newBalance() async { 123 let scope = balance.scopeInfo 124 symLog.log("❗️ task \(scope.currency)") 125 126 if let available = try? await model.getMaxPeerPushDebitAmount(scope, viewHandles: true) { 127 amountAvailable = available 128 } else { 129 amountAvailable = Amount.zero(currency: scope.currency) 130 } 131 } 132 133 var body: some View { 134 #if PRINT_CHANGES 135 let _ = Self._printChanges() 136 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 137 #endif 138 Group { 139 let availableStr = amountAvailable.formatted(balance.scopeInfo, isNegative: false) 140 // let availableA11y = amountAvailable.formatted(currencyInfo, isNegative: false, useISO: true, a11y: ".") 141 // let amountVoiceOver = amountToTransfer.formatted(currencyInfo, isNegative: false) 142 // let insufficientLabel2 = String(localized: "but you only have \(availableStr) to send.") 143 144 let inputDestination = P2PSubjectV(stack: stack.push(), 145 cash: cash, 146 scope: balance.scopeInfo, 147 available: balance.available, 148 feeLabel: (feeLabel(feeString.0), feeLabel(feeString.1)), 149 feeIsNotZero: feeIsNotZero(), 150 outgoing: true, 151 amountToTransfer: $amountToTransfer, // from the textedit 152 summary: $summary, 153 iconID: $iconID, 154 expireDays: $expireDays) 155 let shortcutDestination = P2PSubjectV(stack: stack.push(), 156 cash: cash, 157 scope: balance.scopeInfo, 158 available: balance.available, 159 feeLabel: nil, 160 feeIsNotZero: feeIsNotZero(), 161 outgoing: true, 162 amountToTransfer: $amountShortcut, // from the tapped shortcut button 163 summary: $summary, 164 iconID: $iconID, 165 expireDays: $expireDays) 166 let actions = Group { 167 NavLink($buttonSelected) { inputDestination } 168 NavLink($shortcutSelected) { shortcutDestination } 169 } 170 let a11yLabel = String(localized: "Amount to send:", comment: "a11y, no abbreviations") 171 AmountInputV(stack: stack.push(), 172 scope: balance.scopeInfo, 173 amountAvailable: $amountAvailable, 174 amountLabel: nil, // will use "Available for transfer: xxx", trailing 175 a11yLabel: a11yLabel, 176 amountToTransfer: $amountToTransfer, 177 amountLastUsed: amountLastUsed, 178 wireFee: nil, 179 summary: $summary, 180 shortcutAction: shortcutAction, 181 buttonAction: buttonAction, 182 isIncoming: false, 183 computeFee: computeFee) 184 .background(actions) 185 } // Group 186 .task(id: balance) { await newBalance() } 187 .onAppear { 188 DebugViewC.shared.setViewID(VIEW_P2P_SEND, stack: stack.push()) 189 symLog.log("❗️ onAppear") 190 } 191 } // body 192 }