TransactionsListView.swift (10497B)
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 os.log 10 import SymLog 11 12 #if DEBUG 13 fileprivate let showUpDown = 8 // show up+down buttons in the menubar if list has many lines 14 #else 15 fileprivate let showUpDown = 25 // show up+down buttons in the menubar if list has many lines 16 #endif 17 struct TransactionsListView: View { 18 private let symLog = SymLogV(0) 19 let stack: CallStack 20 let scope: ScopeInfo 21 let balance: Balance // this is the currency to be used 22 @Binding var selectedBalance: Balance? // <- return here the balance when we go to Transactions 23 let navTitle: String? 24 25 @Binding var transactions: [TalerTransaction] 26 27 let reloadAllAction: (_ stack: CallStack) async -> () 28 29 let logger = Logger(subsystem: "net.taler.gnu", category: "TransactionsList") 30 @EnvironmentObject private var controller: Controller 31 @Environment(\.colorScheme) private var colorScheme 32 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 33 @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic 34 @AppStorage("preferredColorScheme") var preferredColorScheme: Int = 0 35 @State private var viewId = UUID() 36 @StateObject private var cash: OIMcash 37 @Namespace var namespace 38 39 init(stack: CallStack, 40 scope: ScopeInfo, 41 balance: Balance, 42 selectedBalance: Binding<Balance?>, 43 navTitle: String?, 44 oimEuro: Bool, 45 transactions: Binding<[TalerTransaction]>, 46 reloadAllAction: @escaping (_ stack: CallStack) async -> () 47 ) { 48 // SwiftUI ensures that the initialization uses the 49 // closure only once during the lifetime of the view, so 50 // later changes to the currency have no effect. 51 self.stack = stack 52 self.scope = scope 53 self.balance = balance 54 self.navTitle = navTitle 55 self._transactions = transactions 56 self.reloadAllAction = reloadAllAction 57 self._selectedBalance = selectedBalance 58 let oimCurrency = oimCurrency(balance.scopeInfo, oimEuro: oimEuro) 59 let oimCash = OIMcash(oimCurrency) 60 self._cash = StateObject(wrappedValue: { oimCash }()) 61 } 62 var body: some View { 63 #if PRINT_CHANGES 64 let _ = Self._printChanges() 65 let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear 66 #endif 67 let isLegacy = scope.type == .exchangeLegacyKeys 68 if isLegacy { 69 UnconfirmedKeyChangeView(baseURL: scope.url ?? "unknown", balance: balance, isLegacy: true) 70 } else if let exchange = controller.exchange(for: scope), 71 let unconfirmedKeyChange = exchange.unconfirmedKeyChange { 72 UnconfirmedKeyChangeView(baseURL: scope.url ?? "unknown", balance: balance, isLegacy: false) 73 } else if transactions.isEmpty { 74 TransactionsEmptyView(stack: stack.push(), currency: scope.currency) 75 .refreshable { 76 controller.hapticNotification(.success) 77 symLog.log("refreshing") 78 await reloadAllAction(stack.push()) 79 } 80 } else { 81 let list = ScrollViewReader { scrollProxy in 82 List { 83 let header = scope.url?.trimURL ?? scope.currency 84 TransactionsArraySection(symLog: symLog, 85 logger: logger, 86 stack: stack.push(), 87 header: header, 88 scope: scope, 89 transactions: $transactions, 90 reloadAllAction: reloadAllAction) 91 } 92 .id(viewId) 93 .listStyle(myListStyle.style).anyView 94 .background(FullBackground()) 95 .refreshable { 96 controller.hapticNotification(.success) 97 symLog.log("refreshing") 98 await reloadAllAction(stack.push()) 99 } 100 #if false // SCROLLBUTTONS 101 .if(count > showUpDown) { view in 102 view.navigationBarItems(trailing: HStack { 103 ArrowUpButton { 104 // print("up") 105 withAnimation { scrollProxy.scrollTo(0) } 106 } 107 ArrowDownButton { 108 // print("down") 109 withAnimation { scrollProxy.scrollTo(transactions.count - 1) } 110 } 111 }) 112 } 113 #endif 114 } // ScrollViewReader 115 // .navigationTitle("EURO") // Fake EUR instead of the real Currency 116 // .navigationTitle("CHF") // Fake CHF instead of the real Currency 117 .navigationTitle(navTitle ?? scope.currency) 118 .accessibilityHint(String(localized: "Transaction list", comment: "a11y")) 119 .task { 120 symLog.log("❗️.task List❗️") 121 await reloadAllAction(stack.push()) 122 } 123 .onAppear { 124 DebugViewC.shared.setViewID(VIEW_TRANSACTIONLIST, stack: stack.push()) 125 print("🚩,32TransactionsListView.onAppear() set selectedBalance to", balance.scopeInfo.currency) 126 selectedBalance = balance // set this balance (fix) for send/request/deposit/withdraw 127 } 128 129 ZStack { 130 if preferredColorScheme == 3, #available(iOS 17.0, *) { 131 list.scrollContentBackground(.hidden) 132 } else { 133 list 134 } 135 } 136 #if OIM 137 .overlay { if #available(iOS 16.4, *) { 138 if controller.oimModeActive { 139 OIMtransactions(stack: stack.push(), 140 balance: balance, 141 cash: cash, 142 history: transactions) 143 .environmentObject(NamespaceWrapper(namespace)) // keep OIMviews apart 144 } 145 } } 146 #endif 147 } // not empty 148 } // body 149 } 150 // MARK: - 151 // used by TransactionsListView, and by BalancesSectionView to show the last 4 transactions 152 struct TransactionsArraySection: View { 153 let symLog: SymLogV? 154 let logger: Logger? 155 let stack: CallStack 156 let header: String? 157 let scope: ScopeInfo 158 @Binding var transactions: [TalerTransaction] 159 let reloadAllAction: (_ stack: CallStack) async -> () 160 161 @EnvironmentObject private var model: WalletModel 162 @Environment(\.colorScheme) private var colorScheme 163 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 164 #if DEBUG 165 @AppStorage("developerMode") var developerMode: Bool = true 166 #else 167 @AppStorage("developerMode") var developerMode: Bool = false 168 #endif 169 @AppStorage("debugViews") var debugViews: Bool = false 170 171 @State private var talerTX: TalerTransaction = TalerTransaction(dummyCurrency: DEMOCURRENCY) 172 173 @State private var padd = 0 174 175 @ViewBuilder 176 func headerView(_ header: String) -> some View { 177 Text(header) 178 .talerFont(.title3) 179 .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) 180 } 181 182 var body: some View { 183 #if PRINT_CHANGES 184 let _ = Self._printChanges() 185 let _ = symLog?.vlog() // just to get the # to compare it with .onAppear & onDisappear 186 #endif 187 let deleteAction = model.deleteTransaction 188 189 Section { 190 ForEach(transactions, id: \.self) { transaction in 191 let destination = TransactionSummaryList(stack: stack.push("TransactionsArraySection"), 192 transactionId: transaction.id, 193 talerTX: $talerTX, 194 navTitle: nil, 195 hasDone: false, // just show an old tx 196 showDone: nil, 197 url: nil, 198 withActions: true) 199 let row = NavigationLink { destination } label: { 200 TransactionRowView(stack: stack.push("TransactionsArraySection"), 201 logger: logger, scope: scope, transaction: transaction) 202 .padding(.leading, ICONLEADING) 203 .padding(.trailing, CGFloat(padd)) 204 }.id(transaction.id) 205 if transaction.isDeleteable { 206 row.swipeActions(edge: .trailing) { 207 Button { 208 symLog?.log("deleteAction") 209 Task { // runs on MainActor 210 try? await deleteAction(transaction.id, false) 211 await reloadAllAction(stack.push()) 212 } 213 } label: { 214 Label("Delete", systemImage: "trash") 215 } 216 .tint(WalletColors().negative) 217 } 218 } else { 219 row 220 } 221 } 222 } header: { 223 #if TALER_NIGHTLY 224 if developerMode && debugViews { 225 HStack { 226 Button("<<") { padd += 10 } 227 Spacer() 228 Button("<") { padd += 1 } 229 Spacer() 230 Button("\(padd)") { padd = 0 } 231 Spacer() 232 Button(">") { padd -= 1 } 233 Spacer() 234 Button(">>") { padd -= 10 } 235 }.font(.body) 236 } else { 237 if let header { 238 headerView(header) 239 } 240 } 241 #else 242 if let header { 243 headerView(header) 244 } 245 #endif 246 } 247 } 248 }