TransactionsListView.swift (9950B)
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 if transactions.isEmpty { 68 TransactionsEmptyView(stack: stack.push(), currency: scope.currency) 69 .refreshable { 70 controller.hapticNotification(.success) 71 symLog.log("refreshing") 72 await reloadAllAction(stack.push()) 73 } 74 } else { 75 let list = ScrollViewReader { scrollProxy in 76 List { 77 let header = scope.url?.trimURL ?? scope.currency 78 TransactionsArraySection(symLog: symLog, 79 logger: logger, 80 stack: stack.push(), 81 header: header, 82 scope: scope, 83 transactions: $transactions, 84 reloadAllAction: reloadAllAction) 85 } 86 .id(viewId) 87 .listStyle(myListStyle.style).anyView 88 .background(FullBackground()) 89 .refreshable { 90 controller.hapticNotification(.success) 91 symLog.log("refreshing") 92 await reloadAllAction(stack.push()) 93 } 94 #if false // SCROLLBUTTONS 95 .if(count > showUpDown) { view in 96 view.navigationBarItems(trailing: HStack { 97 ArrowUpButton { 98 // print("up") 99 withAnimation { scrollProxy.scrollTo(0) } 100 } 101 ArrowDownButton { 102 // print("down") 103 withAnimation { scrollProxy.scrollTo(transactions.count - 1) } 104 } 105 }) 106 } 107 #endif 108 } // ScrollViewReader 109 // .navigationTitle("EURO") // Fake EUR instead of the real Currency 110 // .navigationTitle("CHF") // Fake CHF instead of the real Currency 111 .navigationTitle(navTitle ?? scope.currency) 112 .accessibilityHint(String(localized: "Transaction list", comment: "a11y")) 113 .task { 114 symLog.log("❗️.task List❗️") 115 await reloadAllAction(stack.push()) 116 } 117 .onAppear { 118 DebugViewC.shared.setViewID(VIEW_TRANSACTIONLIST, stack: stack.push()) 119 print("🚩,32TransactionsListView.onAppear() set selectedBalance to", balance.scopeInfo.currency) 120 selectedBalance = balance // set this balance (fix) for send/request/deposit/withdraw 121 } 122 123 ZStack { 124 if preferredColorScheme == 3, #available(iOS 17.0, *) { 125 list.scrollContentBackground(.hidden) 126 } else { 127 list 128 } 129 } 130 #if OIM 131 .overlay { if #available(iOS 16.4, *) { 132 if controller.oimModeActive { 133 OIMtransactions(stack: stack.push(), 134 balance: balance, 135 cash: cash, 136 history: transactions) 137 .environmentObject(NamespaceWrapper(namespace)) // keep OIMviews apart 138 } 139 } } 140 #endif 141 } // not empty 142 } // body 143 } 144 // MARK: - 145 // used by TransactionsListView, and by BalancesSectionView to show the last 4 transactions 146 struct TransactionsArraySection: View { 147 let symLog: SymLogV? 148 let logger: Logger? 149 let stack: CallStack 150 let header: String? 151 let scope: ScopeInfo 152 @Binding var transactions: [TalerTransaction] 153 let reloadAllAction: (_ stack: CallStack) async -> () 154 155 @EnvironmentObject private var model: WalletModel 156 @Environment(\.colorScheme) private var colorScheme 157 @Environment(\.colorSchemeContrast) private var colorSchemeContrast 158 #if DEBUG 159 @AppStorage("developerMode") var developerMode: Bool = true 160 #else 161 @AppStorage("developerMode") var developerMode: Bool = false 162 #endif 163 @AppStorage("debugViews") var debugViews: Bool = false 164 165 @State private var talerTX: TalerTransaction = TalerTransaction(dummyCurrency: DEMOCURRENCY) 166 167 @State private var padd = 0 168 169 @ViewBuilder 170 func headerView(_ header: String) -> some View { 171 Text(header) 172 .talerFont(.title3) 173 .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) 174 } 175 176 var body: some View { 177 #if PRINT_CHANGES 178 let _ = Self._printChanges() 179 let _ = symLog?.vlog() // just to get the # to compare it with .onAppear & onDisappear 180 #endif 181 let deleteAction = model.deleteTransaction 182 183 Section { 184 ForEach(transactions, id: \.self) { transaction in 185 let destination = TransactionSummaryList(stack: stack.push("TransactionsArraySection"), 186 transactionId: transaction.id, 187 talerTX: $talerTX, 188 navTitle: nil, 189 hasDone: false, 190 showDone: nil, 191 url: nil, 192 withActions: true) 193 let row = NavigationLink { destination } label: { 194 TransactionRowView(logger: logger, scope: scope, transaction: transaction) 195 .padding(.leading, ICONLEADING) 196 .padding(.trailing, CGFloat(padd)) 197 }.id(transaction.id) 198 if transaction.isDeleteable { 199 row.swipeActions(edge: .trailing) { 200 Button { 201 symLog?.log("deleteAction") 202 Task { // runs on MainActor 203 let _ = try? await deleteAction(transaction.id, false) 204 await reloadAllAction(stack.push()) 205 } 206 } label: { 207 Label("Delete", systemImage: "trash") 208 } 209 .tint(WalletColors().negative) 210 } 211 } else { 212 row 213 } 214 } 215 } header: { 216 #if TALER_NIGHTLY 217 if developerMode && debugViews { 218 HStack { 219 Button("<<") { padd += 10 } 220 Spacer() 221 Button("<") { padd += 1 } 222 Spacer() 223 Button("\(padd)") { padd = 0 } 224 Spacer() 225 Button(">") { padd -= 1 } 226 Spacer() 227 Button(">>") { padd -= 10 } 228 }.font(.body) 229 } else { 230 if let header { 231 headerView(header) 232 } 233 } 234 #else 235 if let header { 236 headerView(header) 237 } 238 #endif 239 } 240 } 241 }