taler-ios

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

commit 436c440b2a4e5cc0e72b4efc3204fbd6173d7328
parent cccdd4e597cbc656926b1fa4217467baa4e35df6
Author: Marc Stibane <marc@taler.net>
Date:   Wed,  5 Jun 2024 11:56:04 +0200

Overview

Diffstat:
MTalerWallet.xcodeproj/project.pbxproj | 26++++++++++++++++++++++++++
ATalerWallet1/Views/Overview/OverviewListV.swift | 175+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Views/Overview/OverviewRowV.swift | 109+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ATalerWallet1/Views/Overview/OverviewSectionV.swift | 255+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 565 insertions(+), 0 deletions(-)

diff --git a/TalerWallet.xcodeproj/project.pbxproj b/TalerWallet.xcodeproj/project.pbxproj @@ -261,6 +261,12 @@ 4EE77E7F2C0280E5007C9064 /* Taler_Wallet InfoPlist.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 4EE77E7E2C0280E5007C9064 /* Taler_Wallet InfoPlist.xcstrings */; }; 4EE77E812C06E513007C9064 /* WithdrawAcceptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE77E802C06E513007C9064 /* WithdrawAcceptView.swift */; }; 4EE77E822C06E513007C9064 /* WithdrawAcceptView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE77E802C06E513007C9064 /* WithdrawAcceptView.swift */; }; + 4EE77E852C101493007C9064 /* OverviewListV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE77E842C101493007C9064 /* OverviewListV.swift */; }; + 4EE77E862C101493007C9064 /* OverviewListV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE77E842C101493007C9064 /* OverviewListV.swift */; }; + 4EE77E882C101F5B007C9064 /* OverviewSectionV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE77E872C101F5B007C9064 /* OverviewSectionV.swift */; }; + 4EE77E892C101F5B007C9064 /* OverviewSectionV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE77E872C101F5B007C9064 /* OverviewSectionV.swift */; }; + 4EE77E8B2C104506007C9064 /* OverviewRowV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE77E8A2C104506007C9064 /* OverviewRowV.swift */; }; + 4EE77E8C2C104506007C9064 /* OverviewRowV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EE77E8A2C104506007C9064 /* OverviewRowV.swift */; }; 4EEC118D2B83DE4800146CFF /* AmountInputV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC118C2B83DE4700146CFF /* AmountInputV.swift */; }; 4EEC118E2B83DE4800146CFF /* AmountInputV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC118C2B83DE4700146CFF /* AmountInputV.swift */; }; 4EEC11932B83FB7A00146CFF /* SubjectInputV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC11922B83FB7A00146CFF /* SubjectInputV.swift */; }; @@ -455,6 +461,9 @@ 4EE77E7C2C0280E5007C9064 /* GNU_Taler InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = "GNU_Taler InfoPlist.xcstrings"; sourceTree = "<group>"; }; 4EE77E7E2C0280E5007C9064 /* Taler_Wallet InfoPlist.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = "Taler_Wallet InfoPlist.xcstrings"; sourceTree = "<group>"; }; 4EE77E802C06E513007C9064 /* WithdrawAcceptView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WithdrawAcceptView.swift; sourceTree = "<group>"; }; + 4EE77E842C101493007C9064 /* OverviewListV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewListV.swift; sourceTree = "<group>"; }; + 4EE77E872C101F5B007C9064 /* OverviewSectionV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewSectionV.swift; sourceTree = "<group>"; }; + 4EE77E8A2C104506007C9064 /* OverviewRowV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewRowV.swift; sourceTree = "<group>"; }; 4EEC118C2B83DE4700146CFF /* AmountInputV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AmountInputV.swift; sourceTree = "<group>"; }; 4EEC11922B83FB7A00146CFF /* SubjectInputV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SubjectInputV.swift; sourceTree = "<group>"; }; 4EEC11952B840F1100146CFF /* PayTemplateV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PayTemplateV.swift; sourceTree = "<group>"; }; @@ -706,6 +715,7 @@ children = ( 4EB095412989CBFE0043A8A1 /* Main */, 4EB095342989CBFE0043A8A1 /* Balances */, + 4EE77E832C1012F7007C9064 /* Overview */, 4EB0952E2989CBFE0043A8A1 /* Transactions */, 4EB095272989CBFE0043A8A1 /* Banking */, 4EB095242989CBFE0043A8A1 /* Settings */, @@ -838,6 +848,16 @@ path = Peer2peer; sourceTree = "<group>"; }; + 4EE77E832C1012F7007C9064 /* Overview */ = { + isa = PBXGroup; + children = ( + 4EE77E842C101493007C9064 /* OverviewListV.swift */, + 4EE77E872C101F5B007C9064 /* OverviewSectionV.swift */, + 4EE77E8A2C104506007C9064 /* OverviewRowV.swift */, + ); + path = Overview; + sourceTree = "<group>"; + }; 4EEC157129F7188B00D46A03 /* Sheets */ = { isa = PBXGroup; children = ( @@ -1134,6 +1154,7 @@ 4EEC118D2B83DE4800146CFF /* AmountInputV.swift in Sources */, 4E3EAE232A990778009F1BE8 /* BalancesSectionView.swift in Sources */, 4E3EAE242A990778009F1BE8 /* QRGeneratorView.swift in Sources */, + 4EE77E852C101493007C9064 /* OverviewListV.swift in Sources */, 4E3EAE252A990778009F1BE8 /* WithdrawAcceptDone.swift in Sources */, 4E3EAE262A990778009F1BE8 /* Transaction.swift in Sources */, 4E605DB72AB05E48002FB9A7 /* View+flippedDirection.swift in Sources */, @@ -1152,6 +1173,7 @@ 4EBC0F012B7B3CD600C0CB19 /* DepositIbanV.swift in Sources */, 4E3EAE2E2A990778009F1BE8 /* QRCodeDetailView.swift in Sources */, 4E3EAE2F2A990778009F1BE8 /* TransactionsEmptyView.swift in Sources */, + 4EE77E8B2C104506007C9064 /* OverviewRowV.swift in Sources */, 4E605DAF2AADDD13002FB9A7 /* UIScreen+screenSize.swift in Sources */, 4E3EAE312A990778009F1BE8 /* SendAmount.swift in Sources */, 4E3EAE332A990778009F1BE8 /* EqualIconWidthDomain.swift in Sources */, @@ -1193,6 +1215,7 @@ 4E3EAE502A990778009F1BE8 /* Model+Transactions.swift in Sources */, 4E6EF56E2B669C7000AF252A /* TransactionDetailV.swift in Sources */, 4E3EAE512A990778009F1BE8 /* Controller+playSound.swift in Sources */, + 4EE77E882C101F5B007C9064 /* OverviewSectionV.swift in Sources */, 4E3EAE522A990778009F1BE8 /* WalletEmptyView.swift in Sources */, 4E3EAE532A990778009F1BE8 /* CurrencySpecification.swift in Sources */, 4E3EAE542A990778009F1BE8 /* TalerDater.swift in Sources */, @@ -1254,6 +1277,7 @@ 4EEC118E2B83DE4800146CFF /* AmountInputV.swift in Sources */, 4EB095602989CBFE0043A8A1 /* BalancesSectionView.swift in Sources */, 4EEC157329F8242800D46A03 /* QRGeneratorView.swift in Sources */, + 4EE77E862C101493007C9064 /* OverviewListV.swift in Sources */, 4E5A88F72A3B9E5B00072618 /* WithdrawAcceptDone.swift in Sources */, 4EB095222989CBCB0043A8A1 /* Transaction.swift in Sources */, 4E605DB82AB05E48002FB9A7 /* View+flippedDirection.swift in Sources */, @@ -1272,6 +1296,7 @@ 4EBC0F022B7B3CD600C0CB19 /* DepositIbanV.swift in Sources */, 4E5A88F52A38A4FD00072618 /* QRCodeDetailView.swift in Sources */, 4E87C8732A31CB7F001C6406 /* TransactionsEmptyView.swift in Sources */, + 4EE77E8C2C104506007C9064 /* OverviewRowV.swift in Sources */, 4E605DB02AADDD13002FB9A7 /* UIScreen+screenSize.swift in Sources */, 4E40E0BE29F25ABB00B85369 /* SendAmount.swift in Sources */, 4E8E25332A1CD39700A27BFA /* EqualIconWidthDomain.swift in Sources */, @@ -1313,6 +1338,7 @@ 4EB095592989CBFE0043A8A1 /* Model+Transactions.swift in Sources */, 4E6EF56F2B669C7000AF252A /* TransactionDetailV.swift in Sources */, 4E578E922A481D8600F21F1C /* Controller+playSound.swift in Sources */, + 4EE77E892C101F5B007C9064 /* OverviewSectionV.swift in Sources */, 4EB0955F2989CBFE0043A8A1 /* WalletEmptyView.swift in Sources */, 4E16E12329F3BB99008B9C86 /* CurrencySpecification.swift in Sources */, 4EB095092989CB7C0043A8A1 /* TalerDater.swift in Sources */, diff --git a/TalerWallet1/Views/Overview/OverviewListV.swift b/TalerWallet1/Views/Overview/OverviewListV.swift @@ -0,0 +1,175 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import SymLog +import AVFoundation + +/// This view shows the list of balances / currencies, each in its own section +struct OverviewListV: View { + private let symLog = SymLogV(0) + let stack: CallStack + let navTitle: String + @Binding var balances: [Balance] +// @Binding var shouldReloadPending: Int + @Binding var shouldReloadBalances: Int + + @EnvironmentObject private var model: WalletModel + @EnvironmentObject private var controller: Controller + + @State private var lastReloadedBalances = 0 + @State private var amountToTransfer = Amount.zero(currency: EMPTYSTRING) // Update currency when used + @State private var summary: String = "" + @State private var showQRScanner: Bool = false + @State private var showCameraAlert: Bool = false + + private var openSettingsButton: some View { + Button("Open Settings") { + showCameraAlert = false + UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) + } + } + let ClosingAnnouncement = AttributedString(localized: "Closing Camera") + private var dismissAlertButton: some View { + Button("Cancel", role: .cancel) { + if #available(iOS 17.0, *) { + AccessibilityNotification.Announcement(ClosingAnnouncement).post() + } + showCameraAlert = false + } + } + private func dismissingSheet() { + if #available(iOS 17.0, *) { + AccessibilityNotification.Announcement(ClosingAnnouncement).post() + } + } + + var defaultPriorityAnnouncement = AttributedString(localized: "Opening Camera") + var lowPriorityAnnouncement: AttributedString { + var lowPriorityString = AttributedString ("Camera Loading") + if #available(iOS 17.0, *) { + lowPriorityString.accessibilitySpeechAnnouncementPriority = .low + } + return lowPriorityString + } + var highPriorityAnnouncement: AttributedString { + var highPriorityString = AttributedString("Camera Active") + if #available(iOS 17.0, *) { + highPriorityString.accessibilitySpeechAnnouncementPriority = .high + } + return highPriorityString + } + private func checkCameraAvailable() -> Void { + // Open Camera when QR-Button was tapped + if #available(iOS 17.0, *) { + AccessibilityNotification.Announcement(defaultPriorityAnnouncement).post() + } + AVCaptureDevice.requestAccess(for: .video, completionHandler: { (granted: Bool) -> Void in + if granted { + showQRScanner = true // present sheet + if #available(iOS 17.0, *) { + AccessibilityNotification.Announcement(highPriorityAnnouncement).post() + } + } else { + showCameraAlert = true + } + }) + } + + /// runs on MainActor if called in some Task {} + @discardableResult + private func reloadBalances(_ stack: CallStack, _ invalidateCache: Bool) async -> Int? { + if invalidateCache { + model.cachedBalances = nil + } + + if let reloaded = try? await model.balancesM(stack.push()) { + let count = reloaded.count + balances = reloaded // redraw + return count + } + + return nil + } + + var body: some View { +#if PRINT_CHANGES + let _ = Self._printChanges() + let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear +#endif + Content(symLog: symLog, stack: stack.push(), balances: $balances, + amountToTransfer: $amountToTransfer, summary: $summary, +// shouldReloadPending: $shouldReloadPending, + shouldReloadBalances: $shouldReloadBalances, + reloadBalances: reloadBalances) + .navigationTitle(navTitle) + .navigationBarItems(trailing: QRButton(action: checkCameraAvailable)) + .alert("Scanning QR-codes requires access to the camera", + isPresented: $showCameraAlert, + actions: { openSettingsButton + dismissAlertButton }, + message: { Text("Please allow camera access in settings.") }) + .sheet(isPresented: $showQRScanner, onDismiss: dismissingSheet) { + let sheet = AnyView(QRSheet(stack: stack.push(".sheet"))) + Sheet(sheetView: sheet) + } // sheet + .task(id: shouldReloadBalances) { + symLog.log(".task shouldReloadBalances \(shouldReloadBalances)") + let invalidateCache = (lastReloadedBalances != shouldReloadBalances) + lastReloadedBalances = shouldReloadBalances + await reloadBalances(stack.push(".task"), invalidateCache) + } // task + } +} +// MARK: - +extension OverviewListV { + struct Content: View { + let symLog: SymLogV? + let stack: CallStack + @Binding var balances: [Balance] + @Binding var amountToTransfer: Amount + @Binding var summary: String +// @Binding var shouldReloadPending: Int + @Binding var shouldReloadBalances: Int + var reloadBalances: (_ stack: CallStack, _ invalidateCache: Bool) async -> Int? + + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic + + var body: some View { +#if PRINT_CHANGES + let _ = Self._printChanges() + let _ = symLog?.vlog() // just to get the # to compare it with .onAppear & onDisappear +#endif + Group { // necessary for .backslide transition (bug in SwiftUI) + let count = balances.count + if balances.isEmpty { + WalletEmptyView(stack: stack.push("isEmpty")) + } else { + List(balances, id: \.self) { balance in + OverviewSectionV(stack: stack.push("\(balance.scopeInfo.currency)"), + balance: balance, // this is the currency to be used + sectionCount: count, + amountToTransfer: $amountToTransfer, // does still have the wrong currency + summary: $summary, + shouldReloadBalances: $shouldReloadBalances) + } + .onAppear() { + DebugViewC.shared.setViewID(VIEW_BALANCES, stack: stack.push("onAppear")) + } + .listStyle(myListStyle.style).anyView + } + } +#if REFRESHABLE + .refreshable { // already async + symLog?.log("refreshing balances") + let count = await reloadBalances(stack.push("refreshing balances"), true) + if let count, count > 0 { + NotificationCenter.default.post(name: .BalanceReloaded, object: nil) + } + } +#endif + } // body + } // Content +} diff --git a/TalerWallet1/Views/Overview/OverviewRowV.swift b/TalerWallet1/Views/Overview/OverviewRowV.swift @@ -0,0 +1,109 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift + +struct CurrenciesCell: View { + let amount: Amount + let sizeCategory: ContentSizeCategory + let rowAction: () -> Void + let balanceDest: LazyView<TransactionsListView>? + + @Environment(\.colorScheme) private var colorScheme + @Environment(\.colorSchemeContrast) private var colorSchemeContrast + @AppStorage("minimalistic") var minimalistic: Bool = false + + /// Renders the Balance button. "Balance" leading, amountStr trailing. If it doesn't fit in one row then + /// amount (trailing) goes underneath "Balance" (leading). + var body: some View { + let amountV = AmountV(amount: amount, large: true) + .foregroundColor(.primary) + let hLayout = amountV + .frame(maxWidth: .infinity, alignment: .trailing) + let balanceCell = Group { + if minimalistic { + hLayout + } else { + let balanceText = Text("Balance:", comment: "Main view") + .talerFont(.title2) + .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) + let vLayout = VStack(alignment: .leading, spacing: 0) { + balanceText + hLayout + } + + if #available(iOS 16.0, *) { + ViewThatFits(in: .horizontal) { + HStack(spacing: HSPACING) { + balanceText + hLayout + } + vLayout + } + } else { vLayout } // view for iOS 15 + } + } + NavigationLink { balanceDest } label: { + balanceCell + .accessibilityElement(children: .combine) + .accessibilityHint(String(localized: "Will go to main transactions list.")) +// .accessibilityLabel(balanceTitleStr + " " + amountStr) // TODO: CurrencyFormatter! + } + } +} + + +/// This view shows the currency row in a currency section, and two action buttons below +/// Balance: amount +/// [Send Money] [Request Payment] +struct OverviewRowV: View { + let stack: CallStack + let amount: Amount + let sendAction: () -> Void + let recvAction: () -> Void + let rowAction: () -> Void + let balanceDest: LazyView<TransactionsListView>? + + @Environment(\.sizeCategory) var sizeCategory + @EnvironmentObject private var controller: Controller + @AppStorage("minimalistic") var minimalistic: Bool = false + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic + + var body: some View { + VStack (alignment: .trailing, spacing: 6) { + CurrenciesCell(amount: amount, + sizeCategory: sizeCategory, + rowAction: rowAction, + balanceDest: balanceDest) +// .border(.red) + } + } +} +// MARK: - +#if DEBUG +struct OverviewRowV_Previews: PreviewProvider { + @MainActor + struct StateContainer: View { + var body: some View { + let test = Amount(currency: TESTCURRENCY, cent: 123) + let demo = Amount(currency: DEMOCURRENCY, cent: 123456) + + List { + Section { + OverviewRowV(stack: CallStack("Preview"), amount: demo, + sendAction: {}, recvAction: {}, rowAction: {}, balanceDest: nil) + } + OverviewRowV(stack: CallStack("Preview"), amount: test, + sendAction: {}, recvAction: {}, rowAction: {}, balanceDest: nil) + } + } + } + + static var previews: some View { + StateContainer() +// .environment(\.sizeCategory, .extraExtraLarge) Canvas Device Settings + } +} +#endif diff --git a/TalerWallet1/Views/Overview/OverviewSectionV.swift b/TalerWallet1/Views/Overview/OverviewSectionV.swift @@ -0,0 +1,255 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import SymLog + +/// This view shows a currency section +/// Currency Name +/// [Send Coins] [Receive Coins] Balance +/// tapping on Balance leads to completed Transactions (.done) +/// optional: Pending Incoming +/// optional: Pending Outgoing +/// optional: Suspended / Aborting / Aborted / Expired + +struct OverviewSectionV { + private let symLog = SymLogV(0) + let stack: CallStack + let balance: Balance // this is the currency to be used + let sectionCount: Int + @Binding var amountToTransfer: Amount // does still have the wrong currency + @Binding var summary: String + @Binding var shouldReloadBalances: Int + + @EnvironmentObject private var model: WalletModel + @Environment(\.colorScheme) private var colorScheme + @Environment(\.colorSchemeContrast) private var colorSchemeContrast + @EnvironmentObject private var controller: Controller +#if DEBUG + @AppStorage("developerMode") var developerMode: Bool = true +#else + @AppStorage("developerMode") var developerMode: Bool = false +#endif + @AppStorage("minimalistic") var minimalistic: Bool = false + + @State private var showSpendingHint = true + @State private var isShowingDetailView = false + @State private var transactions: [Transaction] = [] + @State private var completedTransactions: [Transaction] = [] + @State private var pendingTransactions: [Transaction] = [] + + func reloadOneAction(_ transactionId: String, _ viewHandles: Bool) async throws -> Transaction { + return try await model.getTransactionByIdT(transactionId, viewHandles: viewHandles) + } + + @State private var sectionID = UUID() + @State private var shownSectionID = UUID() // guaranteed to be different the first time + + func reloadCompleted(_ stack: CallStack) async -> () { + if let transactions = try? await model.transactionsT(stack.push(), scopeInfo: balance.scopeInfo, includeRefreshes: developerMode) { + completedTransactions = WalletModel.completedTransactions(transactions) + } + } + + func reloadPending(_ stack: CallStack) async -> () { + if let transactions = try? await model.transactionsT(stack.push(), scopeInfo: balance.scopeInfo, includeRefreshes: developerMode) { + pendingTransactions = WalletModel.pendingTransactions(transactions) + } + } +} + +extension OverviewSectionV: View { + var body: some View { +#if PRINT_CHANGES + let _ = Self._printChanges() + let _ = symLog.vlog() // just to get the # to compare it with .onAppear & onDisappear +#endif + let scopeInfo = balance.scopeInfo + let currency = scopeInfo.currency + let currencyInfo = controller.info(for: currency) + + Section { + if scopeInfo.type == .exchange { + let baseURL = scopeInfo.url?.trimURL() ?? String(localized: "Unknown Payment Provider", comment: "exchange url") + Text(baseURL) + .talerFont(.headline) + .listRowSeparator(.hidden) + } + CurrenciesNavigationLinksV(symLog: symLog, + stack: stack.push(), + balance: balance, + amountToTransfer: $amountToTransfer, // does still have the wrong currency + summary: $summary, + completedTransactions: $completedTransactions, + reloadAllAction: reloadCompleted, + reloadOneAction: reloadOneAction) + if pendingTransactions.count > 0 { + CurrenciesPendingRowV(symLog: symLog, + stack: stack.push(), + balance: balance, + pendingTransactions: $pendingTransactions, + reloadPending: reloadPending, + reloadOneAction: reloadOneAction) + .padding(.leading, ICONLEADING) + } + } header: { + BarGraphHeader(stack: stack.push(), scopeInfo: scopeInfo, + currencyName: currencyInfo?.scope.currency ?? currency, + shouldReloadBalances: $shouldReloadBalances) + }.id(sectionID) + .task(id: shouldReloadBalances + 1_000_000) { +// if shownSectionID != sectionID { + symLog.log(".task for BalancesSectionView - reload Transactions") + // TODO: only load the MAXRECENT most recent transactions + if let response = try? await model.transactionsT(stack.push(".task - reload Transactions"), scopeInfo: scopeInfo, includeRefreshes: developerMode) { + transactions = response + pendingTransactions = WalletModel.pendingTransactions(response) + completedTransactions = WalletModel.completedTransactions(response) + shownSectionID = sectionID + } +// } else { +// symLog.log("task for BalancesSectionView \(sectionID) ❗️ skip reloading Transactions") +// } + } + let transactionCount = completedTransactions.count + /// if there is only one currency, then show MAXRECENT recent transactions + if sectionCount == 1 && transactionCount > 0 { + Section { + let slice = completedTransactions.prefix(MAXRECENT) // already sorted + let threeTransactions = Array(slice) + TransactionsArraySliceV(symLog: symLog, + stack: stack.push(), + scopeInfo: scopeInfo, + transactions: threeTransactions, + reloadOneAction: reloadOneAction) + .padding(.leading, ICONLEADING) + } header: { + if !minimalistic { + let count = transactionCount > MAXRECENT ? MAXRECENT : transactionCount + Text(count > 1 ? "Recent \(count) transactions" + : "Recent transaction") + .talerFont(.title3) + .foregroundColor(WalletColors().secondary(colorScheme, colorSchemeContrast)) + } + } + } // recent transactions + } // body +} // BalancesSectionView +// MARK: - +fileprivate struct CurrenciesPendingRowV: View { + let symLog: SymLogV? + let stack: CallStack +// let currency: String // = currencyInfo.scope.currency + let balance: Balance // this is the currency to be used + @Binding var pendingTransactions: [Transaction] + let reloadPending: (_ stack: CallStack) async -> () + let reloadOneAction: ((_ transactionId: String, _ viewHandles: Bool) async throws -> Transaction) + + var body: some View { + let pendingIncoming = balance.pendingIncoming + let pendingOutgoing = balance.pendingOutgoing + let needsKYCin = balance.flags.contains(.incomingKyc) + let needsKYCout = balance.flags.contains(.outgoingKyc) + let shouldConfirm = balance.flags.contains(.incomingConfirmation) + + NavigationLink { + //let _ = print("button: Pending Transactions: \(currency)") + LazyView { + TransactionsListView(stack: stack.push(), + navTitle: String(localized: "Pending", comment: "ViewTitle of TransactionList"), + scopeInfo: balance.scopeInfo, + transactions: pendingTransactions, + showUpDown: false, + reloadAllAction: reloadPending, + reloadOneAction: reloadOneAction) + } + } label: { + let needsKYC = needsKYCin || needsKYCout + let needsKYCStr = String(localized: ". Needs K Y C", comment: "VoiceOver") + let needsConfStr = String(localized: ". Needs bank confirmation", comment: "VoiceOver") + VStack(spacing: 6) { + let hasIncoming = !pendingIncoming.isZero + if hasIncoming { + PendingRowView(amount: pendingIncoming, incoming: true, + shouldConfirm: shouldConfirm, needsKYC: needsKYCin) + } + let hasOutgoing = !pendingOutgoing.isZero + if hasOutgoing { + PendingRowView(amount: pendingOutgoing, incoming: false, + shouldConfirm: false, needsKYC: needsKYCout) + } + if !hasIncoming && !hasOutgoing { // should never happen + Text("Some pending transactions") + .talerFont(.body) + } + } + .accessibilityElement(children: .combine) + .accessibilityValue(needsKYC && shouldConfirm ? needsKYCStr + needsConfStr : + needsKYC ? needsKYCStr : + shouldConfirm ? needsConfStr + : EMPTYSTRING) + .accessibilityHint(String(localized: "Will go to Pending transactions.")) + } // NavLinkLabel + } // body +} // CurrenciesPendingRowV + +fileprivate struct CurrenciesNavigationLinksV: View { + let symLog: SymLogV? + let stack: CallStack + let balance: Balance + +// let sectionCount: Int + @Binding var amountToTransfer: Amount // does still have the wrong currency + @Binding var summary: String + @Binding var completedTransactions: [Transaction] + let reloadAllAction: (_ stack: CallStack) async -> () + let reloadOneAction: ((_ transactionId: String, _ viewHandles: Bool) async throws -> Transaction) +// @EnvironmentObject private var model: WalletModel + + @State private var buttonSelected: Int? = nil + + func selectAndUpdate(_ button: Int) { + let currency = balance.scopeInfo.currency + amountToTransfer.setCurrency(currency) // replace wrong currency here + symLog?.log("balance.scopeInfo.currency: \(currency)") + + buttonSelected = button // will trigger NavigationLink + // while navigation animation runs, contact Exchange to update Fees +// Task { // runs on MainActor +// do { +// try await model.updateExchange(scopeInfo: balance.scopeInfo) +// } catch { // TODO: error handling - couldn't updateExchange +// symLog.log("error: \(error)") +// } +// } + } + + var body: some View { + let scopeInfo = balance.scopeInfo + HStack(spacing: 0) { + let balanceDest = LazyView { + TransactionsListView(stack: stack.push(), + navTitle: String(localized: "Transactions", comment: "ViewTitle of TransactionList"), + scopeInfo: scopeInfo, + transactions: completedTransactions, + showUpDown: true, + reloadAllAction: reloadAllAction, + reloadOneAction: reloadOneAction) + } + NavigationLink(destination: balanceDest, tag: 3, selection: $buttonSelected) + { EmptyView() }.frame(width: 0).opacity(0).hidden() // TransactionsListView + + OverviewRowV(stack: stack.push(), + amount: balance.available, + sendAction: { + selectAndUpdate(1) // trigger SendAmount NavigationLink + }, recvAction: { + selectAndUpdate(2) // trigger RequestPayment NavigationLink + }, rowAction: { + buttonSelected = 3 // trigger TransactionList NavigationLink + }, balanceDest: balanceDest) + } + } +}