taler-ios

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

SendAmountV.swift (9416B)


      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 by MainContent 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     private func firstWithP2P(_ balances : inout [Balance]) {
     58         let first = Balance.firstWithP2P(balances)
     59         symLog.log(first?.scopeInfo.currency)
     60         balance = first
     61     }
     62     @MainActor
     63     private func viewDidLoad() async {
     64         var balances = controller.balances
     65         if let selectedBalance {
     66             let disablePeerPayments = selectedBalance.disablePeerPayments ?? false
     67             if disablePeerPayments {
     68                 // find another balance
     69                 firstWithP2P(&balances)
     70             } else {
     71                 balance = selectedBalance
     72             }
     73         } else {
     74             firstWithP2P(&balances)
     75         }
     76         if let balance {
     77             balanceIndex = balances.firstIndex(of: balance) ?? 0
     78         } else {
     79             balanceIndex = 0
     80             balance = (balances.count > 0) ? balances[0] : nil
     81         }
     82     }
     83 
     84     @MainActor
     85     private func newBalance() async {
     86         // runs whenever the user changes the exchange via ScopePicker, or on new currencyInfo
     87         symLog.log("❗️ task \(balanceIndex)")
     88         if let balance {
     89             let scope = balance.scopeInfo
     90             amountToTransfer.setCurrency(scope.currency)
     91             currencyInfo = controller.info(for: scope, controller.currencyTicker)
     92             do {
     93                 amountAvailable = try await model.getMaxPeerPushDebitAmount(scope)
     94             } catch {
     95                 // TODO: Error
     96                 amountAvailable = balance.available
     97             }
     98     print("🚩SendAmountV.newBalance() set selectedBalance and cash.currency")
     99             let oimCurrency = oimCurrency(balance.scopeInfo, oimEuro: oimEuro)  // might be nil ==> OIMeuros
    100             selectedBalance = balance
    101             cash.currency = oimCurrency
    102         }
    103     }
    104 
    105     var body: some View {
    106 #if PRINT_CHANGES
    107         let _ = Self._printChanges()
    108         let _ = symLog.vlog()       // just to get the # to compare it with .onAppear & onDisappear
    109 #endif
    110         let currencySymbol = currencyInfo.symbol
    111         let navA11y = SendAmountView.navTitle(currencyInfo.name, true)          // always include currency for a11y
    112         let navTitle = SendAmountView.navTitle(currencySymbol, currencyInfo.hasSymbol)
    113         let count = controller.balances.count
    114         let _ = symLog.log("count = \(count)")
    115         let taskID = balanceIndex + (1000 * controller.currencyTicker)
    116         let scrollView = ScrollView {
    117             if count > 0 {
    118                 ScopePicker(stack: stack.push(),
    119                             value: $balanceIndex,
    120                       onlyNonZero: true)                                        // can only send what exists
    121                 { index in
    122                     balanceIndex = index
    123                     balance = controller.balances[index]
    124                 }
    125                 .padding(.horizontal)
    126                 .padding(.bottom, 4)
    127             }
    128             if let balance {
    129                 SendAmountView(stack: stack.push(),
    130                                 cash: cash,
    131                              balance: balance,
    132                       buttonSelected: $buttonSelected,                          // call P2PSubjectV
    133                       amountLastUsed: $amountLastUsed,
    134                     amountToTransfer: $amountToTransfer,
    135                      amountAvailable: $amountAvailable,
    136                              summary: $summary,
    137                               iconID: $iconID)
    138             } else {    // TODO: Error no balance - Yikes
    139                 Text("No balance. There seems to be a problem with the database...")
    140             }
    141         } // ScrollView
    142             .navigationTitle(navTitle)
    143             .frame(maxWidth: .infinity, alignment: .leading)
    144             .background(WalletColors().backgroundColor.edgesIgnoringSafeArea(.all))
    145             .task { await viewDidLoad() }
    146             .task(id: taskID) {
    147                 let avail = amountAvailable
    148                 await newBalance()
    149 //        print(">>> SendAmountV.task", taskID, avail, amountAvailable, stack.peek()?.file)
    150             }
    151 #if OIM
    152             .overlay { if #available(iOS 16.4, *) {
    153                 if controller.oimModeActive {
    154 //  let _ = print(">>> SendAmountV", amountAvailable, amountToTransfer, stack.peek()?.file)
    155                     let available = !amountAvailable.isZero ? amountAvailable
    156                                                             : balance?.available ??
    157                                                       selectedBalance?.available ??
    158                                                               amountAvailable
    159                     OIMEditView(stack: stack.push(),
    160                                  cash: cash,
    161                      amountToTransfer: $amountToTransfer,
    162                             available: available,
    163                          useAvailable: true,                                    // cannot send more than we have
    164                       fwdButtonTapped: $buttonSelected)                         // call P2PSubjectV
    165                     .environmentObject(NamespaceWrapper(namespace))             // keep OIMviews apart
    166                 }
    167             } }
    168 #endif
    169         if #available(iOS 16.4, *) {
    170             scrollView.toolbar(.hidden, for: .tabBar)
    171                 .scrollBounceBehavior(.basedOnSize)
    172         } else {
    173             scrollView
    174         }
    175     }
    176 }
    177 // MARK: -
    178 #if DEBUG
    179 fileprivate struct Preview_Content: View {
    180     @State private var amountToPreview = Amount(currency: DEMOCURRENCY, cent: 510)
    181     @State private var summary: String = EMPTYSTRING
    182     @State private var iconID: String? = nil
    183     @State private var currencyInfoL: CurrencyInfo = CurrencyInfo.zero(DEMOCURRENCY)
    184     @State private var noBalance: Balance? = nil
    185 
    186     var body: some View {
    187         let amount = Amount(currency: DEMOCURRENCY, cent: 1000)
    188         let pending = Amount(currency: DEMOCURRENCY, cent: 0)
    189         let scope = ScopeInfo.zero(DEMOCURRENCY)
    190         let exchange2 = Exchange(exchangeBaseUrl: ARS_EXP_EXCHANGE,
    191                                        masterPub: "masterPub",
    192                                        scopeInfo: scope,
    193                                        paytoUris: [],
    194                                        tosStatus: .proposed,
    195                              exchangeEntryStatus: .ephemeral,
    196                             exchangeUpdateStatus: .ready,
    197                            ageRestrictionOptions: [])
    198         let balance = Balance(scopeInfo: scope,
    199                               available: amount,
    200                         pendingIncoming: pending,
    201                         pendingOutgoing: pending,
    202                                   flags: [])
    203         SendAmountV(stack: CallStack("Preview"),
    204           selectedBalance: $noBalance,
    205            amountLastUsed: $amountToPreview,
    206                   summary: $summary,
    207                    iconID: $iconID,
    208                   oimEuro: false)
    209     }
    210 }
    211 
    212 fileprivate struct Previews: PreviewProvider {
    213     @MainActor
    214     struct StateContainer: View {
    215         @StateObject private var controller = Controller.shared
    216         var body: some View {
    217             Preview_Content()
    218                 .environmentObject(controller)
    219         }
    220     }
    221     static var previews: some View {
    222         StateContainer()
    223     }
    224 }
    225 #endif