commit 436c440b2a4e5cc0e72b4efc3204fbd6173d7328
parent cccdd4e597cbc656926b1fa4217467baa4e35df6
Author: Marc Stibane <marc@taler.net>
Date: Wed, 5 Jun 2024 11:56:04 +0200
Overview
Diffstat:
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)
+ }
+ }
+}