taler-ios

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

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 }