taler-ios

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

SendAmountV.swift (9149B)


      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 SendAmountV: View {
     14     private let symLog = SymLogV(0)
     15     let stack: CallStack
     16     // when Action is tapped while in currency TransactionList…
     17     @Binding var selectedBalance: Balance?   // …then use THIS balance, otherwise show picker
     18                                     // coming from transaction list or OIM
     19     @Binding var amountLastUsed: Amount
     20     @Binding var summary: String
     21     @Binding var iconID: String?
     22     let oimEuro: Bool
     23 
     24     @EnvironmentObject private var controller: Controller
     25     @EnvironmentObject private var model: WalletModel
     26 
     27     @StateObject private var cash: OIMcash
     28     @State private var balanceIndex = 0
     29     @State private var balance: Balance? = nil      // nil only when balances == []
     30     @State private var currencyInfo: CurrencyInfo = CurrencyInfo.zero(UNKNOWN)
     31     @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING)    // Update currency when used
     32     @State private var amountAvailable = Amount.zero(currency: EMPTYSTRING)     // GetMaxPeerPushAmount
     33     @State private var buttonSelected = false
     34     @Namespace var namespace
     35 
     36     init(stack: CallStack,
     37          selectedBalance: Binding<Balance?>,
     38          amountLastUsed: Binding<Amount>,
     39          summary: Binding<String>,
     40          iconID: Binding<String?>,
     41          oimEuro: Bool
     42     ) {
     43         // SwiftUI ensures that the initialization uses the
     44         // closure only once during the lifetime of the view, so
     45         // later changes to the currency have no effect.
     46         self.stack = stack
     47         self._selectedBalance = selectedBalance
     48         self._amountLastUsed = amountLastUsed
     49         self._summary = summary
     50         self._iconID = iconID
     51         self.oimEuro = oimEuro
     52         let oimCurrency = oimCurrency(selectedBalance.wrappedValue?.scopeInfo, oimEuro: oimEuro)  // might be nil ==> OIMeuros
     53         let oimCash = OIMcash(oimCurrency)
     54         self._cash = StateObject(wrappedValue: { oimCash }())
     55     }
     56 
     57     @MainActor
     58     private func viewDidLoad() async {
     59         let balances = controller.balances
     60         if let selectedBalance {
     61             if selectedBalance.available.isZero {
     62                 // find another balance
     63                 balance = Balance.firstNonZero(controller.balances)
     64             } else {
     65                 balance = selectedBalance
     66             }
     67         } else {
     68             balance = Balance.firstNonZero(controller.balances)
     69         }
     70         if let balance {
     71             balanceIndex = balances.firstIndex(of: balance) ?? 0
     72         } else {
     73             balanceIndex = 0
     74             balance = (balances.count > 0) ? balances[0] : nil
     75         }
     76     }
     77 
     78     @MainActor
     79     private func newBalance() async {
     80         // runs whenever the user changes the exchange via ScopePicker, or on new currencyInfo
     81         symLog.log("❗️ task \(balanceIndex)")
     82         if let balance {
     83             let scope = balance.scopeInfo
     84             amountToTransfer.setCurrency(scope.currency)
     85             currencyInfo = controller.info(for: scope, controller.currencyTicker)
     86             do {
     87                 amountAvailable = try await model.getMaxPeerPushDebitAmount(scope)
     88             } catch {
     89                 // TODO: Error
     90                 amountAvailable = balance.available
     91             }
     92     print("🚩SendAmountV.newBalance() set selectedBalance and cash.currency")
     93             let oimCurrency = oimCurrency(balance.scopeInfo, oimEuro: oimEuro)  // might be nil ==> OIMeuros
     94             selectedBalance = balance
     95             cash.currency = oimCurrency
     96         }
     97     }
     98 
     99     var body: some View {
    100 #if PRINT_CHANGES
    101         let _ = Self._printChanges()
    102         let _ = symLog.vlog()       // just to get the # to compare it with .onAppear & onDisappear
    103 #endif
    104         let currencySymbol = currencyInfo.symbol
    105         let navA11y = SendAmountView.navTitle(currencyInfo.name)                // always include currency for a11y
    106         let navTitle = SendAmountView.navTitle(currencySymbol, currencyInfo.hasSymbol)
    107         let count = controller.balances.count
    108         let _ = symLog.log("count = \(count)")
    109         let taskID = balanceIndex + (1000 * controller.currencyTicker)
    110         let scrollView = ScrollView {
    111             if count > 0 {
    112                 ScopePicker(value: $balanceIndex,
    113                       onlyNonZero: true)                                        // can only send what exists
    114                 { index in
    115                     balanceIndex = index
    116                     balance = controller.balances[index]
    117                 }
    118                 .padding(.horizontal)
    119                 .padding(.bottom, 4)
    120             }
    121             if let balance {
    122                 SendAmountView(stack: stack.push(),
    123                                 cash: cash,
    124                              balance: balance,
    125                       buttonSelected: $buttonSelected,                          // call P2PSubjectV
    126                       amountLastUsed: $amountLastUsed,
    127                     amountToTransfer: $amountToTransfer,
    128                      amountAvailable: $amountAvailable,
    129                              summary: $summary,
    130                               iconID: $iconID)
    131             } else {    // TODO: Error no balance - Yikes
    132                 Text("No balance. There seems to be a problem with the database...")
    133             }
    134         } // ScrollView
    135             .navigationTitle(navTitle)
    136             .frame(maxWidth: .infinity, alignment: .leading)
    137             .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
    138             .task { await viewDidLoad() }
    139             .task(id: taskID) {
    140                 let avail = amountAvailable
    141                 await newBalance()
    142 //        print(">>> SendAmountV.task", taskID, avail, amountAvailable, stack.peek()?.file)
    143             }
    144 #if OIM
    145             .overlay { if #available(iOS 16.4, *) {
    146                 if controller.oimModeActive {
    147 //  let _ = print(">>> SendAmountV", amountAvailable, amountToTransfer, stack.peek()?.file)
    148                     let available = !amountAvailable.isZero ? amountAvailable
    149                                                             : balance?.available ??
    150                                                       selectedBalance?.available ??
    151                                                               amountAvailable
    152                     OIMEditView(stack: stack.push(),
    153                                  cash: cash,
    154                      amountToTransfer: $amountToTransfer,
    155                             available: available,
    156                          useAvailable: true,                                    // cannot send more than we have
    157                       fwdButtonTapped: $buttonSelected)                         // call P2PSubjectV
    158                     .environmentObject(NamespaceWrapper(namespace))             // keep OIMviews apart
    159                 }
    160             } }
    161 #endif
    162         if #available(iOS 16.4, *) {
    163             scrollView.toolbar(.hidden, for: .tabBar)
    164                 .scrollBounceBehavior(.basedOnSize)
    165         } else {
    166             scrollView
    167         }
    168     }
    169 }
    170 // MARK: -
    171 #if DEBUG
    172 fileprivate struct Preview_Content: View {
    173     @State private var amountToPreview = Amount(currency: DEMOCURRENCY, cent: 510)
    174     @State private var summary: String = EMPTYSTRING
    175     @State private var iconID: String? = nil
    176     @State private var currencyInfoL: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY)
    177     @State private var noBalance: Balance? = nil
    178 
    179     var body: some View {
    180         let amount = Amount(currency: DEMOCURRENCY, cent: 1000)
    181         let pending = Amount(currency: DEMOCURRENCY, cent: 0)
    182         let scope = ScopeInfo.zero(DEMOCURRENCY)
    183         let exchange2 = Exchange(exchangeBaseUrl: ARS_EXP_EXCHANGE,
    184                                        masterPub: "masterPub",
    185                                        scopeInfo: scope,
    186                                        paytoUris: [],
    187                                        tosStatus: .proposed,
    188                              exchangeEntryStatus: .ephemeral,
    189                             exchangeUpdateStatus: .ready,
    190                            ageRestrictionOptions: [])
    191         let balance = Balance(scopeInfo: scope,
    192                               available: amount,
    193                         pendingIncoming: pending,
    194                         pendingOutgoing: pending,
    195                                   flags: [])
    196         SendAmountV(stack: CallStack("Preview"),
    197           selectedBalance: $noBalance,
    198            amountLastUsed: $amountToPreview,
    199                   summary: $summary,
    200                    iconID: $iconID,
    201                   oimEuro: false)
    202     }
    203 }
    204 
    205 fileprivate struct Previews: PreviewProvider {
    206     @MainActor
    207     struct StateContainer: View {
    208         @StateObject private var controller = Controller.shared
    209         var body: some View {
    210             Preview_Content()
    211                 .environmentObject(controller)
    212         }
    213     }
    214     static var previews: some View {
    215         StateContainer()
    216     }
    217 }
    218 #endif