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