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