taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

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