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