ManualWithdraw.swift (14262B)
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 [Withdraw] 13 // or from WithdrawExchangeV after a withdraw-exchange QR was scanned 14 struct ManualWithdraw: View { 15 private let symLog = SymLogV(0) 16 let stack: CallStack 17 let url: URL? 18 // when Action is tapped while in currency TransactionList… 19 let selectedBalance: Balance? // …then use THIS balance, otherwise show picker 20 @Binding var amountLastUsed: Amount 21 @Binding var amountToTransfer: Amount // Update currency when used 22 let exchange: Exchange? // only for withdraw-exchange 23 let maySwitchCurrencies: Bool // not for withdraw-exchange 24 let isSheet: Bool 25 26 @EnvironmentObject private var controller: Controller 27 @EnvironmentObject private var model: WalletModel 28 29 @State private var balanceIndex = 0 30 @State private var balance: Balance? = nil // nil only when balances == [] 31 @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN) 32 33 private func viewDidLoad() async { 34 if let exchange { 35 currencyInfo = controller.info(for: exchange.scopeInfo, controller.currencyTicker) 36 return 37 } else if let selectedBalance { 38 balance = selectedBalance 39 balanceIndex = controller.balances.firstIndex(of: selectedBalance) ?? 0 40 } else { 41 balanceIndex = 0 42 balance = (controller.balances.count > 0) ? controller.balances[0] : nil 43 } 44 if let balance { 45 currencyInfo = controller.info(for: balance.scopeInfo, controller.currencyTicker) 46 } 47 } 48 49 func navTitle(_ currency: String, _ condition: Bool = false) -> String { 50 condition ? String(localized: "NavTitle_Withdraw_Currency)", 51 defaultValue: "Withdraw \(currency)", 52 comment: "NavTitle: Withdraw 'currency'") 53 : String(localized: "NavTitle_Withdraw", 54 defaultValue: "Withdraw", 55 comment: "NavTitle: Withdraw") 56 } 57 58 func updateCurrInfo(_ scope: ScopeInfo) { 59 symLog.log("balance = \(scope.url)") 60 amountToTransfer.setCurrency(scope.currency) 61 currencyInfo = controller.info(for: scope, controller.currencyTicker) 62 } 63 64 var body: some View { 65 #if PRINT_CHANGES 66 let _ = Self._printChanges() 67 #endif 68 let currencySymbol = currencyInfo.symbol 69 let navA11y = navTitle(currencyInfo.name) // always include currency for a11y 70 let navTitle = navTitle(currencySymbol, currencyInfo.hasSymbol) 71 let count = controller.balances.count 72 let scrollView = ScrollView { 73 if maySwitchCurrencies && count > 0 { 74 ScopePicker(value: $balanceIndex, onlyNonZero: false) { index in 75 balanceIndex = index 76 balance = controller.balances[index] 77 if let balance { 78 updateCurrInfo(balance.scopeInfo) 79 } 80 } 81 .padding(.horizontal) 82 .padding(.bottom, 4) 83 } 84 if let scope = balance?.scopeInfo ?? exchange?.scopeInfo { 85 let _ = symLog.log("exchange = \(exchange?.exchangeBaseUrl), scope = \(scope.url), amountToTransfer = \(amountToTransfer.currencyStr)") 86 ManualWithdrawContent(stack: stack.push(), 87 url: url, 88 scope: scope, 89 amountLastUsed: $amountLastUsed, 90 amountToTransfer: $amountToTransfer, 91 exchange: exchange) 92 } else { // should never happen, we either have an exchange or a balance 93 let title = "ManualWithdrawContent: Cannot determine scope" // should not happen, so no L10N 94 ErrorView(title: title, message: nil, copyable: true) { 95 dismissTop(stack.push()) 96 } 97 } 98 } // ScrollView 99 .navigationTitle(navTitle) 100 .frame(maxWidth: .infinity, alignment: .leading) 101 .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all)) 102 .onAppear { 103 if isSheet { 104 DebugViewC.shared.setSheetID(SHEET_WITHDRAW_ACCEPT) // 132 WithdrawAcceptView 105 } else { 106 DebugViewC.shared.setViewID(VIEW_WITHDRAWAL, // 30 WithdrawAmount 107 stack: stack.push()) 108 } 109 symLog.log("❗️ \(navTitle) onAppear") 110 } 111 .onDisappear { 112 symLog.log("❗️ \(navTitle) onDisappear") 113 } 114 .task { await viewDidLoad() } 115 .task(id: controller.currencyTicker) { 116 // runs whenever a new currencyInfo is available 117 symLog.log("❗️ task \(controller.currencyTicker)") 118 let scopeInfo = maySwitchCurrencies ? balance?.scopeInfo 119 : exchange?.scopeInfo 120 if let scopeInfo { 121 updateCurrInfo(scopeInfo) 122 } 123 } 124 125 if #available(iOS 16.4, *) { 126 scrollView.toolbar(.hidden, for: .tabBar) 127 .scrollBounceBehavior(.basedOnSize) 128 } else { 129 scrollView 130 } 131 } 132 } 133 // MARK: - 134 struct ManualWithdrawContent: View { 135 private let symLog = SymLogV(0) 136 let stack: CallStack 137 let url: URL? 138 let scope: ScopeInfo 139 @Binding var amountLastUsed: Amount 140 @Binding var amountToTransfer: Amount 141 let exchange: Exchange? 142 143 @EnvironmentObject private var controller: Controller 144 @EnvironmentObject private var model: WalletModel 145 @AppStorage("minimalistic") var minimalistic: Bool = false 146 147 @State private var detailsForAmount: WithdrawalDetailsForAmount? = nil 148 // @State var ageMenuList: [Int] = [] 149 // @State var selectedAge = 0 150 @State private var tosAccepted = false 151 152 // private func exchangeVia(_ baseURL: String?) -> String? { 153 // if let baseURL { 154 // return String(localized: "via \(baseURL.trimURL)", comment: "currency/exchange chooser") 155 // } 156 // return nil 157 // } 158 // 159 @MainActor 160 private func reloadExchange(_ baseURL: String) async { 161 symLog.log("getExchangeByUrl(\(baseURL))") 162 let exchange = try? await model.getExchangeByUrl(url: baseURL) 163 tosAccepted = (exchange?.tosStatus == .accepted) ?? false 164 } 165 166 @MainActor 167 private func getWithdrawalDetailsForAmount(_ amount: Amount, _ reload: Bool = false) async { 168 do { 169 let details = try await model.getWithdrawalDetailsForAmount(amount, 170 baseUrl: nil, 171 scope: scope, 172 viewHandles: true) 173 if reload { 174 await reloadExchange(details.exchangeBaseUrl) 175 } 176 detailsForAmount = details 177 // agePicker.setAges(ages: detailsForAmount?.ageRestrictionOptions) 178 } catch WalletBackendError.walletCoreError(let walletBackendResponseError) { 179 symLog.log(walletBackendResponseError?.hint) 180 // TODO: ignore WALLET_CORE_REQUEST_CANCELLED but handle all others 181 // Passing non-nil to clientCancellationId will throw WALLET_CORE_REQUEST_CANCELLED 182 // when calling getWithdrawalDetailsForAmount again before the last call returned. 183 // Since amountToTransfer changed and we don't need the old fee anymore, we just 184 // ignore it and do nothing. 185 // especially DON'T set detailsForAmount to nil 186 } catch { 187 symLog.log(error.localizedDescription) 188 detailsForAmount = nil 189 } 190 } 191 192 @MainActor 193 private func computeFee(_ amount: Amount) async -> ComputeFeeResult? { 194 if amount.isZero { 195 return ComputeFeeResult.zero() 196 } 197 await getWithdrawalDetailsForAmount(amount) 198 // TODO: actually compute the fee 199 return nil 200 } // computeFee 201 202 @MainActor 203 private func viewDidLoad2() async { 204 // neues scope wenn balance geändert wird? 205 await getWithdrawalDetailsForAmount(amountToTransfer, true) 206 } 207 208 private func withdrawButtonTitle(_ currency: String) -> String { 209 switch currency { 210 case CHF_4217: 211 String(localized: "WITHDRAW_CONFIRM_BUTTONTITLE_CHF", defaultValue: "Confirm Withdrawal") 212 case EUR_4217: 213 String(localized: "WITHDRAW_CONFIRM_BUTTONTITLE_EUR", defaultValue: "Confirm Withdrawal") 214 default: 215 String(localized: "WITHDRAW_CONFIRM_BUTTONTITLE", defaultValue: "Confirm Withdrawal") 216 } 217 } 218 219 var body: some View { 220 #if PRINT_CHANGES 221 let _ = Self._printChanges() 222 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 223 #endif 224 225 if let detailsForAmount { 226 Group { 227 let coinData = CoinData(details: detailsForAmount) 228 let currency = detailsForAmount.scopeInfo.currency 229 let baseURL = detailsForAmount.exchangeBaseUrl 230 // let agePicker = AgePicker(ageMenuList: $ageMenuList, selectedAge: $selectedAge) 231 // let restrictAge: Int? = (selectedAge == 0) ? nil 232 // : selectedAge 233 // let _ = print(selectedAge, restrictAge) 234 let destination = ManualWithdrawDone(stack: stack.push(), 235 // scope: detailsForAmount.scopeInfo, 236 url: url, 237 baseURL: baseURL, 238 amountToTransfer: amountToTransfer) 239 // restrictAge: restrictAge) 240 let disabled = amountToTransfer.isZero || coinData.invalid || coinData.tooMany 241 242 if tosAccepted { // Von der Bank abzuhebender Betrag 243 let a11yLabel = String(localized: "Amount to withdraw from bank", comment: "a11y, no abbreviations") 244 let title = String(localized: "Amount to withdraw from bank:") 245 let amountLabel = minimalistic ? String(localized: "Amount:") 246 : title 247 CurrencyInputView(scope: scope, 248 amount: $amountToTransfer, 249 amountLastUsed: amountLastUsed, 250 available: nil, // enable shortcuts always 251 title: amountLabel, 252 a11yTitle: a11yLabel, 253 shortcutAction: nil) 254 .padding(.top) 255 .task(id: amountToTransfer.value) { // re-run this whenever amountToTransfer changes 256 await computeFee(amountToTransfer) 257 } 258 QuiteSomeCoins(scope: scope, 259 coinData: coinData, 260 shouldShowFee: true, // TODO: set to false if we never charge withdrawal fees 261 feeIsNegative: true) 262 // agePicker 263 NavigationLink(destination: destination) { // VIEW_WITHDRAW_ACCEPT 264 Text(withdrawButtonTitle(currency)) 265 } 266 .buttonStyle(TalerButtonStyle(type: .prominent, disabled: disabled)) 267 .disabled(disabled) 268 .padding(.top) 269 } else { 270 ToSButtonView(stack: stack.push(), 271 exchangeBaseUrl: scope.url ?? baseURL, 272 viewID: VIEW_WITHDRAW_TOS, // 31 WithdrawTOSView TODO: might be withdraw-exchange 273 p2p: false, 274 acceptAction: nil) 275 .padding(.top) 276 } 277 } // Group 278 .padding(.horizontal) 279 .task(id: amountToTransfer.currencyStr) { 280 await getWithdrawalDetailsForAmount(amountToTransfer, true) 281 } 282 } else { 283 LoadingView(stack: stack.push(), scopeInfo: scope, message: nil) 284 .task { await viewDidLoad2() } 285 } 286 } 287 } 288 // MARK: - 289 #if DEBUG 290 //struct ManualWithdraw_Previews: PreviewProvider { 291 // struct StateContainer : View { 292 // @State private var amountToPreview = Amount(currency: LONGCURRENCY, cent: 510) 293 // @State private var exchange: Exchange? = Exchange(exchangeBaseUrl: DEMOEXCHANGE, 294 // scopeInfo: ScopeInfo(type: .exchange, currency: LONGCURRENCY), 295 // paytoUris: [], 296 // tosStatus: .accepted, 297 // exchangeEntryStatus: .ephemeral, 298 // exchangeUpdateStatus: .ready, 299 // ageRestrictionOptions: []) 300 // 301 // var body: some View { 302 // ManualWithdraw(stack: CallStack("Preview"), isSheet: false, 303 // scopeInfo: <#ScopeInfo?#>, 304 // exchangeBaseUrl: DEMOEXCHANGE, 305 // exchange: $exchange, 306 // amountToTransfer: $amountToPreview) 307 // } 308 // } 309 // 310 // static var previews: some View { 311 // StateContainer() 312 // } 313 //} 314 #endif