DepositAmountView.swift (10087B)
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 from DepositAmountV 13 struct DepositAmountView: View { 14 private let symLog = SymLogV(0) 15 let stack: CallStack 16 @Binding var balance: Balance? 17 @Binding var balanceIndex: Int 18 @Binding var amountLastUsed: Amount 19 @Binding var amountToTransfer: Amount 20 let amountAvailable: Amount 21 let paytoUri: String? 22 23 @EnvironmentObject private var controller: Controller 24 @EnvironmentObject private var model: WalletModel 25 @Environment(\.colorScheme) private var colorScheme 26 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 27 @AppStorage("minimalistic") var minimalistic: Bool = false 28 29 @State var checkDepositResult: CheckDepositResponse? = nil 30 @State private var insufficient = false 31 @State private var feeAmount: Amount? = nil 32 @State private var feeStr: String = EMPTYSTRING 33 @State private var depositStarted = false 34 @State private var exchange: Exchange? = nil // wg. noFees 35 36 public static func navTitle(_ currency: String = EMPTYSTRING, 37 _ condition: Bool = false 38 ) -> String { 39 condition ? String(localized: "NavTitle_Deposit_Currency", 40 defaultValue: "Deposit \(currency)", 41 comment: "currencySymbol") 42 : String(localized: "NavTitle_Deposit", 43 defaultValue: "Deposit") 44 } 45 46 private func feeLabel(_ feeString: String) -> String { 47 feeString.isEmpty ? EMPTYSTRING : String(localized: "+ \(feeString) fee") 48 } 49 50 private func feeIsNotZero() -> Bool? { 51 if let hasNoFees = exchange?.noFees { 52 if hasNoFees { 53 return nil // this exchange never has fees 54 } 55 } 56 return checkDepositResult == nil ? false 57 : true // TODO: !(feeAmount?.isZero ?? false) 58 } 59 60 @MainActor 61 private func computeFeeDeposit(_ amount: Amount) async -> ComputeFeeResult? { 62 if amount.isZero { 63 return ComputeFeeResult.zero() 64 } 65 let insufficient = (try? amount > amountAvailable) ?? true 66 if insufficient { 67 return ComputeFeeResult.insufficient() 68 } 69 // private func fee(ppCheck: CheckDepositResponse?) -> Amount? { 70 do { 71 // if let ppCheck { 72 // // Outgoing: fee = effective - raw 73 // feeAmount = try ppCheck.fees.coin + ppCheck.fees.wire + ppCheck.fees.refresh 74 // return feeAmount 75 // } 76 } catch { 77 78 } 79 return nil 80 } 81 82 private func buttonTitle(_ amount: Amount) -> String { 83 if let balance { 84 let amountWithCurrency = amount.formatted(balance.scopeInfo, isNegative: false, useISO: true) 85 return DepositAmountView.navTitle(amountWithCurrency.0, true) 86 } 87 return DepositAmountView.navTitle() // should never happen 88 } 89 90 @MainActor 91 private func startDeposit(_ scope: ScopeInfo) { 92 if let paytoUri { 93 depositStarted = true // don't run twice 94 Task { 95 symLog.log("Deposit") 96 if let result = try? await model.createDepositGroup(paytoUri, 97 scope: scope, 98 amount: amountToTransfer) { 99 symLog.log(result.transactionId) 100 if let txState = result.txState { 101 if txState.isKYC || txState.isKYCauth { 102 symLog.log("Deposit KYC") 103 104 } else { 105 // ViewState2.shared.popToRootView(stack.push()) 106 NotificationCenter.default.post(name: .TransactionDone, object: nil, userInfo: nil) 107 dismissTop(stack.push()) 108 } 109 } 110 } else { 111 depositStarted = false 112 } 113 } 114 } 115 } 116 117 var body: some View { 118 #if PRINT_CHANGES 119 let _ = Self._printChanges() 120 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 121 #endif 122 if depositStarted { 123 let message = String(localized: "Depositing...", comment: "loading") 124 LoadingView(stack: stack.push(), scopeInfo: nil, message: message) 125 .navigationBarBackButtonHidden(true) 126 .interactiveDismissDisabled() // can only use "Done" button to dismiss 127 .safeAreaInset(edge: .bottom) { 128 let buttonTitle = String(localized: "Abort", comment: "deposit") 129 Button(buttonTitle) { dismissTop(stack.push()) } 130 .buttonStyle(TalerButtonStyle(type: .prominent)) 131 .padding(.horizontal) 132 } 133 } else { ScrollView { 134 if let balance { 135 let scopeInfo = balance.scopeInfo 136 let availableStr = amountAvailable.formatted(scopeInfo, isNegative: false) 137 138 let currencyInfo = controller.info(for: scopeInfo, controller.currencyTicker) 139 // let amountVoiceOver = amountToTransfer.formatted(scopeInfo, isNegative: false) 140 let insufficientLabel = String(localized: "You don't have enough \(currencyInfo.specs.name).") 141 // let insufficientLabel2 = String(localized: "but you only have \(available) to deposit.") 142 143 let disabled = insufficient || amountToTransfer.isZero 144 145 Text("Available: \(availableStr.0)") 146 .accessibilityLabel(Text("Available: \(availableStr.1)", comment: "a11y")) 147 .talerFont(.title3) 148 .frame(maxWidth: .infinity, alignment: .trailing) 149 .padding(.horizontal) 150 // .padding(.bottom, 2) 151 let a11yLabel = String(localized: "Amount to deposit:", comment: "a11y, no abbreviations") 152 let amountLabel = minimalistic ? String(localized: "Amount:") 153 : a11yLabel 154 CurrencyInputView(scope: scopeInfo, 155 amount: $amountToTransfer, 156 amountLastUsed: amountLastUsed, 157 available: amountAvailable, // enable shortcuts if available >= value 158 title: amountLabel, 159 a11yTitle: a11yLabel, 160 shortcutAction: nil) 161 .padding(.horizontal) 162 163 Text(insufficient ? insufficientLabel 164 : feeLabel(feeStr)) 165 .talerFont(.body) 166 .foregroundColor(insufficient ? WalletColors().attention 167 : (feeAmount?.isZero ?? true) ? WalletColors().secondary(colorScheme, colorSchemeContrast) 168 : WalletColors().negative) 169 .padding(4) 170 let hint = String(localized: "enabled when amount is non-zero, but not higher than your available amount", 171 comment: "a11y") 172 Button(buttonTitle(amountToTransfer)) { startDeposit(balance.scopeInfo) } // TODO: use exchange scope 173 .buttonStyle(TalerButtonStyle(type: .prominent, disabled: disabled || depositStarted)) 174 .padding(.horizontal) 175 .disabled(disabled || depositStarted) 176 .accessibilityHint(disabled ? hint : EMPTYSTRING) 177 } else { // no balance - Yikes 178 Text("No balance. There seems to be a problem with the database...") 179 } 180 } 181 // .ignoresSafeArea(.keyboard, edges: .bottom) 182 .onAppear { 183 DebugViewC.shared.setViewID(VIEW_DEPOSIT, stack: stack.push()) 184 symLog.log("❗️ onAppear") 185 } 186 .onDisappear { 187 symLog.log("❗️ onDisappear") 188 } 189 190 // .task(id: amountToTransfer.value) { 191 // if let amountAvailable { 192 // do { 193 // insufficient = try amountToTransfer > amountAvailable 194 // } catch { 195 // print("Yikes❗️ insufficient failed❗️") 196 // insufficient = true 197 // } 198 // 199 // if insufficient { 200 // announce("\(amountVoiceOver), \(insufficientLabel2)") 201 // feeStr = EMPTYSTRING 202 // } 203 // } 204 // if !insufficient { 205 // if amountToTransfer.isZero { 206 // feeStr = EMPTYSTRING 207 // checkDepositResult = nil 208 // } else if let paytoUri { 209 // if let ppCheck = try? await model.checkDepositM(paytoUri, amount: amountToTransfer) { 210 // if let feeAmount = fee(ppCheck: ppCheck) { 211 // feeStr = feeAmount.formatted(scopeInfo, isNegative: false) 212 // let feeLabel = feeLabel(feeStr) 213 // announce("\(amountVoiceOver), \(feeLabel)") 214 // } else { 215 // feeStr = EMPTYSTRING 216 // announce(amountVoiceOver) 217 // } 218 // checkDepositResult = ppCheck 219 // } else { 220 // checkDepositResult = nil 221 // } 222 // } 223 // } 224 // } 225 } // else 226 } // body 227 } 228 // MARK: - 229 #if DEBUG 230 #endif