taler-ios

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

ManualWithdraw.swift (14287B)


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