taler-ios

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

commit 803cb7e4e7d3219ff6be045374eceb2b3a448c22
parent 97d7bbd9c5538c3682ca13a801df7ddd4283b836
Author: Marc Stibane <marc@taler.net>
Date:   Sun, 28 Jan 2024 14:42:29 +0100

TransactionSummaryV

Diffstat:
MTalerWallet.xcodeproj/project.pbxproj | 12++++++------
MTalerWallet1/Controllers/DebugViewC.swift | 6++++--
MTalerWallet1/Controllers/PublicConstants.swift | 4++--
MTalerWallet1/Views/Banking/ManualWithdrawDone.swift | 2+-
MTalerWallet1/Views/Peer2peer/P2PReadyV.swift | 6+++---
MTalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift | 2+-
MTalerWallet1/Views/Sheets/Refund/RefundURIView.swift | 2+-
MTalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptDone.swift | 2+-
DTalerWallet1/Views/Transactions/TransactionDetailView.swift | 396-------------------------------------------------------------------------------
ATalerWallet1/Views/Transactions/TransactionSummaryV.swift | 400+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
MTalerWallet1/Views/Transactions/TransactionsListView.swift | 2+-
11 files changed, 420 insertions(+), 414 deletions(-)

diff --git a/TalerWallet.xcodeproj/project.pbxproj b/TalerWallet.xcodeproj/project.pbxproj @@ -89,7 +89,7 @@ 4E3EAE5F2A990778009F1BE8 /* QRSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EEC157929F9427F00D46A03 /* QRSheet.swift */; }; 4E3EAE602A990778009F1BE8 /* P2pReceiveURIView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E3B4BC02A41E6C200CC88B8 /* P2pReceiveURIView.swift */; }; 4E3EAE612A990778009F1BE8 /* ListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E6EDD862A363D8D0031D520 /* ListStyle.swift */; }; - 4E3EAE622A990778009F1BE8 /* TransactionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095312989CBFE0043A8A1 /* TransactionDetailView.swift */; }; + 4E3EAE622A990778009F1BE8 /* TransactionSummaryV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095312989CBFE0043A8A1 /* TransactionSummaryV.swift */; }; 4E3EAE632A990778009F1BE8 /* WalletCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0951C2989CBCB0043A8A1 /* WalletCore.swift */; }; 4E3EAE642A990778009F1BE8 /* LaunchAnimationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095432989CBFE0043A8A1 /* LaunchAnimationView.swift */; }; 4E3EAE682A990778009F1BE8 /* WalletModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095112989CBB00043A8A1 /* WalletModel.swift */; }; @@ -197,7 +197,7 @@ 4EB095552989CBFE0043A8A1 /* PaymentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0952D2989CBFE0043A8A1 /* PaymentView.swift */; }; 4EB095562989CBFE0043A8A1 /* TransactionsListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB0952F2989CBFE0043A8A1 /* TransactionsListView.swift */; }; 4EB095572989CBFE0043A8A1 /* TransactionRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095302989CBFE0043A8A1 /* TransactionRowView.swift */; }; - 4EB095582989CBFE0043A8A1 /* TransactionDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095312989CBFE0043A8A1 /* TransactionDetailView.swift */; }; + 4EB095582989CBFE0043A8A1 /* TransactionSummaryV.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095312989CBFE0043A8A1 /* TransactionSummaryV.swift */; }; 4EB095592989CBFE0043A8A1 /* Model+Transactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095322989CBFE0043A8A1 /* Model+Transactions.swift */; }; 4EB0955A2989CBFE0043A8A1 /* URLSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095332989CBFE0043A8A1 /* URLSheet.swift */; }; 4EB0955C2989CBFE0043A8A1 /* BalanceRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4EB095362989CBFE0043A8A1 /* BalanceRowView.swift */; }; @@ -375,7 +375,7 @@ 4EB0952D2989CBFE0043A8A1 /* PaymentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PaymentView.swift; sourceTree = "<group>"; }; 4EB0952F2989CBFE0043A8A1 /* TransactionsListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionsListView.swift; sourceTree = "<group>"; }; 4EB095302989CBFE0043A8A1 /* TransactionRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionRowView.swift; sourceTree = "<group>"; }; - 4EB095312989CBFE0043A8A1 /* TransactionDetailView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionDetailView.swift; sourceTree = "<group>"; }; + 4EB095312989CBFE0043A8A1 /* TransactionSummaryV.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TransactionSummaryV.swift; sourceTree = "<group>"; }; 4EB095322989CBFE0043A8A1 /* Model+Transactions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Model+Transactions.swift"; sourceTree = "<group>"; }; 4EB095332989CBFE0043A8A1 /* URLSheet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSheet.swift; sourceTree = "<group>"; }; 4EB095362989CBFE0043A8A1 /* BalanceRowView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BalanceRowView.swift; sourceTree = "<group>"; }; @@ -689,7 +689,7 @@ children = ( 4EB0952F2989CBFE0043A8A1 /* TransactionsListView.swift */, 4EB095302989CBFE0043A8A1 /* TransactionRowView.swift */, - 4EB095312989CBFE0043A8A1 /* TransactionDetailView.swift */, + 4EB095312989CBFE0043A8A1 /* TransactionSummaryV.swift */, 4E87C8722A31CB7F001C6406 /* TransactionsEmptyView.swift */, 4E6EDD842A3615BE0031D520 /* ManualDetailsV.swift */, 4ED2F94A2A278F5100453B40 /* ThreeAmountsV.swift */, @@ -1120,7 +1120,7 @@ 4E3EAE5F2A990778009F1BE8 /* QRSheet.swift in Sources */, 4E3EAE602A990778009F1BE8 /* P2pReceiveURIView.swift in Sources */, 4E3EAE612A990778009F1BE8 /* ListStyle.swift in Sources */, - 4E3EAE622A990778009F1BE8 /* TransactionDetailView.swift in Sources */, + 4E3EAE622A990778009F1BE8 /* TransactionSummaryV.swift in Sources */, 4E3EAE632A990778009F1BE8 /* WalletCore.swift in Sources */, 4E3EAE642A990778009F1BE8 /* LaunchAnimationView.swift in Sources */, E37AA62A2AF197E5003850CF /* Model+Refund.swift in Sources */, @@ -1226,7 +1226,7 @@ 4EEC157A29F9427F00D46A03 /* QRSheet.swift in Sources */, 4E3B4BC12A41E6C200CC88B8 /* P2pReceiveURIView.swift in Sources */, 4E6EDD872A363D8D0031D520 /* ListStyle.swift in Sources */, - 4EB095582989CBFE0043A8A1 /* TransactionDetailView.swift in Sources */, + 4EB095582989CBFE0043A8A1 /* TransactionSummaryV.swift in Sources */, 4EB095202989CBCB0043A8A1 /* WalletCore.swift in Sources */, 4EB095672989CBFE0043A8A1 /* LaunchAnimationView.swift in Sources */, E37AA62B2AF197E5003850CF /* Model+Refund.swift in Sources */, diff --git a/TalerWallet1/Controllers/DebugViewC.swift b/TalerWallet1/Controllers/DebugViewC.swift @@ -34,7 +34,8 @@ public let VIEW_PENDING = VIEW_ABOUT + 1 // 15 Pendin // MARK: Transactions public let VIEW_TRANSACTIONLIST = VIEW_EMPTY + 10 // 20 TransactionsListView -public let VIEW_TRANSACTIONDETAIL = VIEW_TRANSACTIONLIST + 1 // 21 TransactionDetail +public let VIEW_TRANSACTIONSUMMARY = VIEW_TRANSACTIONLIST + 1 // 21 TransactionSummary +public let VIEW_TRANSACTIONDETAIL = VIEW_TRANSACTIONSUMMARY + 1 // 22 TransactionDetail @@ -79,7 +80,8 @@ public let SHEET_WITHDRAW_CONFIRM = SHEET_WITHDRAW_ACCEPT + 1 // 133 waiti // MARK: Merchant Payment // openURL (Link, NFC or scan QR) ==> pays merchant public let SHEET_PAYMENT = SHEET_WITHDRAWAL + 10 // 140 Pay Merchant -public let SHEET_PAY_TEMPLATE = SHEET_PAYMENT + 2 // 142 Pay Merchant Template +public let SHEET_PAY_TEMPLATE = SHEET_PAYMENT + 1 // 141 Pay Merchant Template +public let SHEET_PAY_ACCEPT = SHEET_PAY_TEMPLATE + 1 // 142 Pay Accept // MARK: P2P Pay Invoice // p2p pull debit - openURL (Link or scan QR) diff --git a/TalerWallet1/Controllers/PublicConstants.swift b/TalerWallet1/Controllers/PublicConstants.swift @@ -14,8 +14,8 @@ public let SEVENDAYS: UInt = 7 // 3..9 public let THIRTYDAYS: UInt = 30 // 10..30 public let EMPTYSTRING = "" // avoid automatic translation of empty "" textLiterals in Text() -public let CONFIRM_BANK = "circle.fill" // badge in PendingRow, TransactionRow and TransactionDetail -public let NEEDS_KYC = "star.fill" // badge in PendingRow, TransactionRow and TransactionDetail +public let CONFIRM_BANK = "circle.fill" // badge in PendingRow, TransactionRow and TransactionSummary +public let NEEDS_KYC = "star.fill" // badge in PendingRow, TransactionRow and TransactionSummary public let PENDING_INCOMING = "plus.diamond" public let PENDING_OUTGOING = "minus.diamond" public let DONE_INCOMING = "plus.circle.fill" diff --git a/TalerWallet1/Views/Banking/ManualWithdrawDone.swift b/TalerWallet1/Views/Banking/ManualWithdrawDone.swift @@ -34,7 +34,7 @@ struct ManualWithdrawDone: View { #endif Group { if let transactionId { - TransactionDetailView(stack: stack.push(), + TransactionSummaryV(stack: stack.push(), transactionId: transactionId, reloadAction: reloadOneAction, navTitle: navTitle, diff --git a/TalerWallet1/Views/Peer2peer/P2PReadyV.swift b/TalerWallet1/Views/Peer2peer/P2PReadyV.swift @@ -38,7 +38,7 @@ struct P2PReadyV: View { #endif Group { if let transactionId { - TransactionDetailView(stack: stack.push(), + TransactionSummaryV(stack: stack.push(), transactionId: transactionId, reloadAction: reloadOneAction, navTitle: navTitle, @@ -80,7 +80,7 @@ struct P2PReadyV: View { purse_expiration: timestamp) // TODO: let user choose baseURL let response = try await model.initiatePeerPushDebitM(nil, terms: terms) - // will switch from WithdrawProgressView to TransactionDetailView + // will switch from WithdrawProgressView to TransactionSummaryV transactionId = response.transactionId } else { let terms = PeerContractTerms(amount: amountToTransfer, @@ -88,7 +88,7 @@ struct P2PReadyV: View { purse_expiration: timestamp) // TODO: let user choose baseURL let response = try await model.initiatePeerPullCreditM(nil, terms: terms) - // will switch from WithdrawProgressView to TransactionDetailView + // will switch from WithdrawProgressView to TransactionSummaryV transactionId = response.transactionId } } catch { // TODO: error diff --git a/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift b/TalerWallet1/Views/Sheets/P2P_Sheets/P2pAcceptDone.swift @@ -33,7 +33,7 @@ struct P2pAcceptDone: View { #endif let navTitle = incoming ? String(localized: "Received P2P", comment: "Title, short") : String(localized: "Paid P2P", comment: "Title, short") - TransactionDetailView(stack: stack.push(), + TransactionSummaryV(stack: stack.push(), transactionId: transactionId, reloadAction: reloadOneAction, navTitle: navTitle, diff --git a/TalerWallet1/Views/Sheets/Refund/RefundURIView.swift b/TalerWallet1/Views/Sheets/Refund/RefundURIView.swift @@ -23,7 +23,7 @@ struct RefundURIView: View { var body: some View { if let refundTransactionId { - TransactionDetailView(stack: stack.push(), + TransactionSummaryV(stack: stack.push(), transactionId: refundTransactionId, reloadAction: reloadOneAction, navTitle: nil, // navTitle, diff --git a/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptDone.swift b/TalerWallet1/Views/Sheets/WithdrawBankIntegrated/WithdrawAcceptDone.swift @@ -33,7 +33,7 @@ struct WithdrawAcceptDone: View { #endif Group { if let transactionId { - TransactionDetailView(stack: stack.push(), + TransactionSummaryV(stack: stack.push(), transactionId: transactionId, reloadAction: reloadOneAction, navTitle: navTitle, diff --git a/TalerWallet1/Views/Transactions/TransactionDetailView.swift b/TalerWallet1/Views/Transactions/TransactionDetailView.swift @@ -1,396 +0,0 @@ -/* - * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. - * See LICENSE.md - */ -import SwiftUI -import taler_swift -import SymLog - -extension Transaction { // for Dummys - init(dummyCurrency: String) { - let amount = Amount.zero(currency: dummyCurrency) - let now = Timestamp.now() - let common = TransactionCommon(type: .dummy, - txState: TransactionState(major: .pending), - amountEffective: amount, - amountRaw: amount, - transactionId: "", - timestamp: now, - txActions: []) - self = .dummy(DummyTransaction(common: common)) - } -} -// MARK: - -struct TransactionDetailView: View { - private let symLog = SymLogV(0) - let stack: CallStack - let transactionId: String - let reloadAction: ((_ transactionId: String) async throws -> Transaction) - let navTitle: String? - let doneAction: ((_ stack: CallStack) -> Void)? - let abortAction: ((_ transactionId: String) async throws -> Void)? - let deleteAction: ((_ transactionId: String) async throws -> Void)? - let failAction: ((_ transactionId: String) async throws -> Void)? - let suspendAction: ((_ transactionId: String) async throws -> Void)? - let resumeAction: ((_ transactionId: String) async throws -> Void)? - - @Environment(\.colorSchemeContrast) private var colorSchemeContrast - @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic -#if DEBUG - @AppStorage("developerMode") var developerMode: Bool = true -#else - @AppStorage("developerMode") var developerMode: Bool = false -#endif - - @State var transaction: Transaction = Transaction(dummyCurrency: DEMOCURRENCY) - @State var viewId = UUID() - - func loadTransaction() async { - do { - let reloadedTransaction = try await reloadAction(transactionId) - symLog.log("reloaded transaction: \(reloadedTransaction.common.txState.major)") - withAnimation() { transaction = reloadedTransaction; viewId = UUID() } // redraw - } catch { - symLog.log(error.localizedDescription) - withAnimation() { transaction = Transaction(dummyCurrency: DEMOCURRENCY); viewId = UUID() } - } - } - - @discardableResult - func checkDismiss(_ notification: Notification, _ logStr: String = "") -> Bool { - if let doneAction { - if let transition = notification.userInfo?[TRANSACTIONTRANSITION] as? TransactionTransition { - if transition.transactionId == transaction.common.transactionId { // is the transition for THIS transaction? - symLog.log(logStr) - doneAction(stack.push()) // if this view is in a sheet then dissmiss the sheet - return true - } - } - } else { // no sheet but the details view -> reload - checkReload(notification, logStr) - } - return false - } - - func checkReload(_ notification: Notification, _ logStr: String = "") { - if let transition = notification.userInfo?[TRANSACTIONTRANSITION] as? TransactionTransition { - if transition.transactionId == transactionId { // is the transition for THIS transaction? - let newMajor = transition.newTxState.major - Task { // runs on MainActor - // flush the screen first, then reload - withAnimation() { transaction = Transaction(dummyCurrency: DEMOCURRENCY); viewId = UUID() } - symLog.log("newState: \(newMajor), reloading transaction") - await loadTransaction() - } - } - } else { // Yikes - should never happen -// TODO: logger.warning("Can't get notification.userInfo as TransactionTransition") - symLog.log(notification.userInfo as Any) - } - } - - 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 common = transaction.common - let pending = transaction.isPending - let locale = TalerDater.shared.locale - let (dateString, date) = TalerDater.dateString(from: common.timestamp) - let accessibilityDate = TalerDater.accessibilityDate(date) ?? dateString - let navTitle2 = transaction.localizedType - VStack { - List { - if developerMode { - if transaction.isSuspendable { if let suspendAction { - TransactionButton(transactionId: common.transactionId, - command: .suspend, warning: nil, action: suspendAction) - } } - if transaction.isResumable { if let resumeAction { - TransactionButton(transactionId: common.transactionId, - command: .resume, warning: nil, action: resumeAction) - } } - } // Suspend + Resume buttons - Text(dateString) - .accessibilityFont(.body) - .accessibilityLabel(accessibilityDate) - .foregroundColor(colorSchemeContrast == .increased ? .primary : .secondary) - .listRowSeparator(.hidden) - VStack(alignment: .trailing) { - let majorState = common.txState.major.localizedState - let minorState = common.txState.minor?.localizedState ?? nil - let state = transaction.isPending ? minorState ?? majorState - : majorState - HStack { - Text(verbatim: "|") // only reason for this leading-aligned text is to get a nice full length listRowSeparator - .accessibilityHidden(true) - .foregroundColor(Color.clear) - Spacer() - Text("Status: \(state)") - .multilineTextAlignment(.trailing) - } - } .listRowSeparator(.automatic) - .accessibilityFont(.title) - TypeDetail(transaction: $transaction, hasDone: doneAction != nil) - -// if transaction.isRetryable { if let retryAction { -// TransactionButton(transactionId: common.transactionId, command: .retry, -// warning: nil, action: abortAction) -// } } // Retry button - if transaction.isAbortable { if let abortAction { - TransactionButton(transactionId: common.transactionId, command: .abort, - warning: String(localized: "Are you sure you want to abort this transaction?"), - action: abortAction) - } } // Abort button - if transaction.isFailable { if let failAction { - TransactionButton(transactionId: common.transactionId, command: .fail, - warning: String(localized: "Are you sure you want to fail this transaction?"), - action: failAction) - } } // Fail button - if transaction.isDeleteable { if let deleteAction { - TransactionButton(transactionId: common.transactionId, command: .delete, - warning: String(localized: "Are you sure you want to delete this transaction?"), - action: deleteAction) - } } // Delete button - if let doneAction { - Button(transaction.shouldConfirm ? "Confirm later" : "Done", action: { doneAction(stack.push()) } ) - .buttonStyle(TalerButtonStyle(type: transaction.shouldConfirm ? .bordered : .prominent)) - } // Done button - }.id(viewId) // change viewId to enforce a draw update - .listStyle(myListStyle.style).anyView - } // Group - .onNotification(.TransactionExpired) { notification in - // TODO: Alert user that this tx just expired - if checkDismiss(notification, "newTxState.major == expired => dismiss sheet") { - // TODO: logger.info("newTxState.major == expired => dismiss sheet") - } - } - .onNotification(.TransactionDone) { notification in - checkDismiss(notification, "newTxState.major == done => dismiss sheet") - } - .onNotification(.DismissSheet) { notification in - checkDismiss(notification, "exchangeWaitReserve or withdrawCoins => dismiss sheet") - } - .onNotification(.PendingReady) { notification in - checkReload(notification, "pending ready ==> reload for talerURI") - } - .onNotification(.TransactionStateTransition) { notification in - checkReload(notification, "some transition ==> reload") - } - .navigationTitle(navTitle ?? navTitle2) - .task { - symLog.log("task - load transaction") - await loadTransaction() - } - .onAppear { - symLog.log("onAppear") - DebugViewC.shared.setViewID(VIEW_TRANSACTIONDETAIL, stack: stack.push()) - } - .onDisappear { - symLog.log("onDisappear") - } - } -//} -// -//extension TransactionDetail { - struct KycButton: View { - let destination: URL - @AppStorage("iconOnly") var iconOnly: Bool = false - - var body: some View { - VStack(alignment: .leading) { // Show Hint that User must pass KYC on website - if !iconOnly { - Text("You need to pass a KYC procedure") - .fixedSize(horizontal: false, vertical: true) // wrap in scrollview - .multilineTextAlignment(.leading) // otherwise - .listRowSeparator(.hidden) - } - Link("Open KYC website", destination: destination) - .buttonStyle(TalerButtonStyle(type: .prominent, badge: NEEDS_KYC)) - .accessibilityHint("Will go to KYC website to confirm this withdrawal.") - } - } - } - - struct ConfirmationButton: View { - let destination: URL - @AppStorage("iconOnly") var iconOnly: Bool = false - - var body: some View { - VStack(alignment: .leading) { // Show Hint that User should Confirm on bank website - if !iconOnly { - Text("The bank is waiting for your confirmation.") -// .fixedSize(horizontal: false, vertical: true) // wrap in scrollview - .multilineTextAlignment(.leading) // otherwise - .listRowSeparator(.hidden) - } - Link("Confirm now", destination: destination) - .buttonStyle(TalerButtonStyle(type: .prominent, badge: CONFIRM_BANK)) - .accessibilityHint("Will go to bank website to confirm this withdrawal.") - } - } - } - - struct TypeDetail: View { - @Binding var transaction: Transaction - let hasDone: Bool - - var body: some View { - let common = transaction.common - let pending = transaction.isPending - Group { - switch transaction { - case .dummy(_): - let title = EMPTYSTRING - Text(title) - .accessibilityFont(.body) - case .withdrawal(let withdrawalTransaction): Group { - let details = withdrawalTransaction.details - if pending { - if transaction.isPendingKYC { - if let kycUrl = common.kycUrl { - if let destination = URL(string: kycUrl) { - KycButton(destination: destination) - } - } - } - let withdrawalDetails = details.withdrawalDetails - switch withdrawalDetails.type { - case .manual: // "Make a wire transfer of \(amount) to" - ManualDetailsV(common: common, details: withdrawalDetails) - - case .bankIntegrated: // "Confirm now" (with bank) - if !transaction.isPendingKYC { // cannot confirm if KYC is needed first - let confirmed = withdrawalDetails.confirmed ?? false - if !confirmed { - if let confirmationUrl = withdrawalDetails.bankConfirmationUrl { - if let destination = URL(string: confirmationUrl) { - ConfirmationButton(destination: destination) - } } } } - } // switch - } // ManualDetails or Confirm now (with bank) - ThreeAmountsSheet(common: common, topAbbrev: String(localized: "Chosen:"), - topTitle: String(localized: "Chosen amount to withdraw:"), - baseURL: details.exchangeBaseUrl, large: false, summary: nil) - } - case .payment(let paymentTransaction): Group { - let details = paymentTransaction.details - Text(details.info.summary) - .accessibilityFont(.title3) - ThreeAmountsSheet(common: common, topAbbrev: String(localized: "Pay:"), - topTitle: String(localized: "Sum to be paid:"), - baseURL: nil, large: true, summary: details.info.summary) // TODO: baseURL - } - case .refund(let refundTransaction): Group { - let details = refundTransaction.details // TODO: more details - ThreeAmountsSheet(common: common, topAbbrev: String(localized: "Refunded:"), - topTitle: String(localized: "Refunded amount:"), - baseURL: nil, large: true, summary: nil) // TODO: baseURL, summary - } - case .reward(let rewardTransaction): Group { - let details = rewardTransaction.details - ThreeAmountsSheet(common: common, topAbbrev: String(localized: "Reward:"), - topTitle: String(localized: "Received Reward:"), - baseURL: details.exchangeBaseUrl, large: true, summary: nil) // TODO: summary - } - case .refresh(let refreshTransaction): Group { - let details = refreshTransaction.details // TODO: details - Text(details.refreshReason.rawValue) - ThreeAmountsSheet(common: common, topAbbrev: String(localized: "Refreshed:"), - topTitle: String(localized: "Refreshed amount:"), - baseURL: nil, large: true, summary: nil) // TODO: baseURL - } - case .peer2peer(let p2pTransaction): Group { - let details = p2pTransaction.details - let expiration = details.info.expiration - let (dateString, date) = TalerDater.dateString(from: expiration) - let accessibilityDate = TalerDater.accessibilityDate(date) ?? dateString - let accessibilityLabel = String(localized: "Expires: \(accessibilityDate)") - Text("Expires: \(dateString)") - .accessibilityFont(.body) - .accessibilityLabel(accessibilityLabel) -// .foregroundColor(colorSchemeContrast == .increased ? .primary : .secondary) - // TODO: isSendCoins should show QR only while not yet expired - either set timer or wallet-core should do so and send a state-changed notification - if pending { - if transaction.isPendingReady { - QRCodeDetails(transaction: transaction) - if hasDone { - Text("QR code and link can also be scanned or copied / shared from Transactions later.") - .multilineTextAlignment(.leading) - .accessibilityFont(.subheadline) - .padding(.top) - } - } else { - Text("This transaction is not yet ready...") - .multilineTextAlignment(.leading) - .accessibilityFont(.subheadline) - } - } - let colon = ":" - ThreeAmountsSheet(common: common, topAbbrev: transaction.localizedType + colon, - topTitle: transaction.localizedType + colon, - baseURL: details.exchangeBaseUrl, large: false, - summary: details.info.summary) - } // p2p - } // switch - } // Group - } - } - - struct QRCodeDetails: View { - var transaction : Transaction - var body: some View { - let details = transaction.detailsToShow() - let keys = details.keys - if keys.contains(TALERURI) { - if let talerURI = details[TALERURI] { - if talerURI.count > 10 { - QRCodeDetailView(talerURI: talerURI, - incoming: transaction.isP2pIncoming, - amount: transaction.common.amountRaw) - } - } - } else if keys.contains(EXCHANGEBASEURL) { - if let baseURL = details[EXCHANGEBASEURL] { - Text("from \(baseURL.trimURL())") - .accessibilityFont(.title2) - .padding(.bottom) - } - } - } - } - struct DoneButton: View { - var doneAction: () -> Void - - var body: some View { - Button("Done") { - doneAction() - } - .buttonStyle(TalerButtonStyle(type: .prominent)) - .padding(.horizontal) - } - } -} -// MARK: - -#if DEBUG -//struct TransactionDetail_Previews: PreviewProvider { -// static func deleteTransactionDummy(transactionId: String) async throws {} -// static func doneActionDummy() {} -// static var withdrawal = Transaction(incoming: true, -// pending: true, -// id: "some withdrawal ID", -// time: Timestamp(from: 1_666_000_000_000)) -// static var payment = Transaction(incoming: false, -// pending: false, -// id: "some payment ID", -// time: Timestamp(from: 1_666_666_000_000)) -// static func reloadActionDummy(transactionId: String) async -> Transaction { return withdrawal } -// static var previews: some View { -// Group { -// TransactionDetailView(transaction: withdrawal, reloadAction: reloadActionDummy, doneAction: doneActionDummy) -// TransactionDetailView(transaction: payment, reloadAction: reloadActionDummy, deleteAction: deleteTransactionDummy) -// } -// } -//} -#endif diff --git a/TalerWallet1/Views/Transactions/TransactionSummaryV.swift b/TalerWallet1/Views/Transactions/TransactionSummaryV.swift @@ -0,0 +1,400 @@ +/* + * This file is part of GNU Taler, ©2022-23 Taler Systems S.A. + * See LICENSE.md + */ +import SwiftUI +import taler_swift +import SymLog + +extension Transaction { // for Dummys + init(dummyCurrency: String) { + let amount = Amount.zero(currency: dummyCurrency) + let now = Timestamp.now() + let common = TransactionCommon(type: .dummy, + txState: TransactionState(major: .pending), + amountEffective: amount, + amountRaw: amount, + transactionId: "", + timestamp: now, + txActions: []) + self = .dummy(DummyTransaction(common: common)) + } +} +// MARK: - +struct TransactionSummaryV: View { + private let symLog = SymLogV(0) + let stack: CallStack + let transactionId: String + let reloadAction: ((_ transactionId: String) async throws -> Transaction) + let navTitle: String? + let doneAction: ((_ stack: CallStack) -> Void)? + let abortAction: ((_ transactionId: String) async throws -> Void)? + let deleteAction: ((_ transactionId: String) async throws -> Void)? + let failAction: ((_ transactionId: String) async throws -> Void)? + let suspendAction: ((_ transactionId: String) async throws -> Void)? + let resumeAction: ((_ transactionId: String) async throws -> Void)? + + @Environment(\.colorSchemeContrast) private var colorSchemeContrast + @AppStorage("myListStyle") var myListStyle: MyListStyle = .automatic +#if DEBUG + @AppStorage("developerMode") var developerMode: Bool = true +#else + @AppStorage("developerMode") var developerMode: Bool = false +#endif + + @State var transaction: Transaction = Transaction(dummyCurrency: DEMOCURRENCY) + @State var viewId = UUID() + + func loadTransaction() async { + do { + let reloadedTransaction = try await reloadAction(transactionId) + symLog.log("reloaded transaction: \(reloadedTransaction.common.txState.major)") + withAnimation() { transaction = reloadedTransaction; viewId = UUID() } // redraw + } catch { + symLog.log(error.localizedDescription) + withAnimation() { transaction = Transaction(dummyCurrency: DEMOCURRENCY); viewId = UUID() } + } + } + + @discardableResult + func checkDismiss(_ notification: Notification, _ logStr: String = "") -> Bool { + if let doneAction { + if let transition = notification.userInfo?[TRANSACTIONTRANSITION] as? TransactionTransition { + if transition.transactionId == transaction.common.transactionId { // is the transition for THIS transaction? + symLog.log(logStr) + doneAction(stack.push()) // if this view is in a sheet then dissmiss the sheet + return true + } + } + } else { // no sheet but the details view -> reload + checkReload(notification, logStr) + } + return false + } + + func checkReload(_ notification: Notification, _ logStr: String = "") { + if let transition = notification.userInfo?[TRANSACTIONTRANSITION] as? TransactionTransition { + if transition.transactionId == transactionId { // is the transition for THIS transaction? + let newMajor = transition.newTxState.major + Task { // runs on MainActor + // flush the screen first, then reload + withAnimation() { transaction = Transaction(dummyCurrency: DEMOCURRENCY); viewId = UUID() } + symLog.log("newState: \(newMajor), reloading transaction") + await loadTransaction() + } + } + } else { // Yikes - should never happen +// TODO: logger.warning("Can't get notification.userInfo as TransactionTransition") + symLog.log(notification.userInfo as Any) + } + } + + 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 common = transaction.common + let pending = transaction.isPending + let locale = TalerDater.shared.locale + let (dateString, date) = TalerDater.dateString(from: common.timestamp) + let accessibilityDate = TalerDater.accessibilityDate(date) ?? dateString + let navTitle2 = transaction.localizedType + VStack { + List { + if developerMode { + if transaction.isSuspendable { if let suspendAction { + TransactionButton(transactionId: common.transactionId, + command: .suspend, warning: nil, action: suspendAction) + } } + if transaction.isResumable { if let resumeAction { + TransactionButton(transactionId: common.transactionId, + command: .resume, warning: nil, action: resumeAction) + } } + } // Suspend + Resume buttons + Text(dateString) + .accessibilityFont(.body) + .accessibilityLabel(accessibilityDate) + .foregroundColor(colorSchemeContrast == .increased ? .primary : .secondary) + .listRowSeparator(.hidden) + VStack(alignment: .trailing) { + let majorState = common.txState.major.localizedState + let minorState = common.txState.minor?.localizedState ?? nil + let state = transaction.isPending ? minorState ?? majorState + : majorState + HStack { + Text(verbatim: "|") // only reason for this leading-aligned text is to get a nice full length listRowSeparator + .accessibilityHidden(true) + .foregroundColor(Color.clear) + Spacer() + Text("Status: \(state)") + .multilineTextAlignment(.trailing) + } + } .listRowSeparator(.automatic) + .accessibilityFont(.title) + TypeDetail(transaction: $transaction, hasDone: doneAction != nil) + +// if transaction.isRetryable { if let retryAction { +// TransactionButton(transactionId: common.transactionId, command: .retry, +// warning: nil, action: abortAction) +// } } // Retry button + if transaction.isAbortable { if let abortAction { + TransactionButton(transactionId: common.transactionId, command: .abort, + warning: String(localized: "Are you sure you want to abort this transaction?"), + action: abortAction) + } } // Abort button + if transaction.isFailable { if let failAction { + TransactionButton(transactionId: common.transactionId, command: .fail, + warning: String(localized: "Are you sure you want to fail this transaction?"), + action: failAction) + } } // Fail button + if transaction.isDeleteable { if let deleteAction { + TransactionButton(transactionId: common.transactionId, command: .delete, + warning: String(localized: "Are you sure you want to delete this transaction?"), + action: deleteAction) + } } // Delete button + if let doneAction { + Button(transaction.shouldConfirm ? "Confirm later" : "Done", action: { doneAction(stack.push()) } ) + .buttonStyle(TalerButtonStyle(type: transaction.shouldConfirm ? .bordered : .prominent)) + } // Done button + }.id(viewId) // change viewId to enforce a draw update + .listStyle(myListStyle.style).anyView + } // Group + .onNotification(.TransactionExpired) { notification in + // TODO: Alert user that this tx just expired + if checkDismiss(notification, "newTxState.major == expired => dismiss sheet") { + // TODO: logger.info("newTxState.major == expired => dismiss sheet") + } + } + .onNotification(.TransactionDone) { notification in + checkDismiss(notification, "newTxState.major == done => dismiss sheet") + } + .onNotification(.DismissSheet) { notification in + checkDismiss(notification, "exchangeWaitReserve or withdrawCoins => dismiss sheet") + } + .onNotification(.PendingReady) { notification in + checkReload(notification, "pending ready ==> reload for talerURI") + } + .onNotification(.TransactionStateTransition) { notification in + checkReload(notification, "some transition ==> reload") + } + .navigationTitle(navTitle ?? navTitle2) + .task { + symLog.log("task - load transaction") + await loadTransaction() + } + .onAppear { + symLog.log("onAppear") + DebugViewC.shared.setViewID(VIEW_TRANSACTIONSUMMARY, stack: stack.push()) + } + .onDisappear { + symLog.log("onDisappear") + } + } +//} +// +//extension TransactionSummaryV { + struct KycButton: View { + let destination: URL + @AppStorage("iconOnly") var iconOnly: Bool = false + + var body: some View { + VStack(alignment: .leading) { // Show Hint that User must pass KYC on website + if !iconOnly { + Text("You need to pass a KYC procedure") + .fixedSize(horizontal: false, vertical: true) // wrap in scrollview + .multilineTextAlignment(.leading) // otherwise + .listRowSeparator(.hidden) + } + Link("Open KYC website", destination: destination) + .buttonStyle(TalerButtonStyle(type: .prominent, badge: NEEDS_KYC)) + .accessibilityHint("Will go to KYC website to confirm this withdrawal.") + } + } + } + + struct ConfirmationButton: View { + let destination: URL + @AppStorage("iconOnly") var iconOnly: Bool = false + + var body: some View { + VStack(alignment: .leading) { // Show Hint that User should Confirm on bank website + if !iconOnly { + Text("The bank is waiting for your confirmation.") +// .fixedSize(horizontal: false, vertical: true) // wrap in scrollview + .multilineTextAlignment(.leading) // otherwise + .listRowSeparator(.hidden) + } + Link("Confirm now", destination: destination) + .buttonStyle(TalerButtonStyle(type: .prominent, badge: CONFIRM_BANK)) + .accessibilityHint("Will go to bank website to confirm this withdrawal.") + } + } + } + + struct TypeDetail: View { + @Binding var transaction: Transaction + let hasDone: Bool + + var body: some View { + let common = transaction.common + let pending = transaction.isPending + Group { + switch transaction { + case .dummy(_): + let title = EMPTYSTRING + Text(title) + .accessibilityFont(.body) + case .withdrawal(let withdrawalTransaction): Group { + let details = withdrawalTransaction.details + if pending { + if transaction.isPendingKYC { + if let kycUrl = common.kycUrl { + if let destination = URL(string: kycUrl) { + KycButton(destination: destination) + } + } + } + let withdrawalDetails = details.withdrawalDetails + switch withdrawalDetails.type { + case .manual: // "Make a wire transfer of \(amount) to" + ManualDetailsV(common: common, details: withdrawalDetails) + + case .bankIntegrated: // "Confirm now" (with bank) + if !transaction.isPendingKYC { // cannot confirm if KYC is needed first + let confirmed = withdrawalDetails.confirmed ?? false + if !confirmed { + if let confirmationUrl = withdrawalDetails.bankConfirmationUrl { + if let destination = URL(string: confirmationUrl) { + ConfirmationButton(destination: destination) + } } } } + } // switch + } // ManualDetails or Confirm now (with bank) + ThreeAmountsSheet(common: common, topAbbrev: String(localized: "Chosen:"), + topTitle: String(localized: "Chosen amount to withdraw:"), + baseURL: details.exchangeBaseUrl, large: false, summary: nil) + } + case .payment(let paymentTransaction): Group { + let details = paymentTransaction.details + NavigationLink(destination: LazyView { + LoadingView(url: nil, message: "Details") + }) { + Text("Show details") + } +// .buttonStyle(TalerButtonStyle(type: .bordered)) + ThreeAmountsSheet(common: common, topAbbrev: String(localized: "Pay:"), + topTitle: String(localized: "Sum to be paid:"), + baseURL: nil, large: true, summary: details.info.summary) // TODO: baseURL + } + case .refund(let refundTransaction): Group { + let details = refundTransaction.details // TODO: more details + ThreeAmountsSheet(common: common, topAbbrev: String(localized: "Refunded:"), + topTitle: String(localized: "Refunded amount:"), + baseURL: nil, large: true, summary: nil) // TODO: baseURL, summary + } + case .reward(let rewardTransaction): Group { + let details = rewardTransaction.details + ThreeAmountsSheet(common: common, topAbbrev: String(localized: "Reward:"), + topTitle: String(localized: "Received Reward:"), + baseURL: details.exchangeBaseUrl, large: true, summary: nil) // TODO: summary + } + case .refresh(let refreshTransaction): Group { + let details = refreshTransaction.details // TODO: details + Text(details.refreshReason.rawValue) + ThreeAmountsSheet(common: common, topAbbrev: String(localized: "Refreshed:"), + topTitle: String(localized: "Refreshed amount:"), + baseURL: nil, large: true, summary: nil) // TODO: baseURL + } + case .peer2peer(let p2pTransaction): Group { + let details = p2pTransaction.details + let expiration = details.info.expiration + let (dateString, date) = TalerDater.dateString(from: expiration) + let accessibilityDate = TalerDater.accessibilityDate(date) ?? dateString + let accessibilityLabel = String(localized: "Expires: \(accessibilityDate)") + Text("Expires: \(dateString)") + .accessibilityFont(.body) + .accessibilityLabel(accessibilityLabel) +// .foregroundColor(colorSchemeContrast == .increased ? .primary : .secondary) + // TODO: isSendCoins should show QR only while not yet expired - either set timer or wallet-core should do so and send a state-changed notification + if pending { + if transaction.isPendingReady { + QRCodeDetails(transaction: transaction) + if hasDone { + Text("QR code and link can also be scanned or copied / shared from Transactions later.") + .multilineTextAlignment(.leading) + .accessibilityFont(.subheadline) + .padding(.top) + } + } else { + Text("This transaction is not yet ready...") + .multilineTextAlignment(.leading) + .accessibilityFont(.subheadline) + } + } + let colon = ":" + ThreeAmountsSheet(common: common, topAbbrev: transaction.localizedType + colon, + topTitle: transaction.localizedType + colon, + baseURL: details.exchangeBaseUrl, large: false, + summary: details.info.summary) + } // p2p + } // switch + } // Group + } + } + + struct QRCodeDetails: View { + var transaction : Transaction + var body: some View { + let details = transaction.detailsToShow() + let keys = details.keys + if keys.contains(TALERURI) { + if let talerURI = details[TALERURI] { + if talerURI.count > 10 { + QRCodeDetailView(talerURI: talerURI, + incoming: transaction.isP2pIncoming, + amount: transaction.common.amountRaw) + } + } + } else if keys.contains(EXCHANGEBASEURL) { + if let baseURL = details[EXCHANGEBASEURL] { + Text("from \(baseURL.trimURL())") + .accessibilityFont(.title2) + .padding(.bottom) + } + } + } + } + struct DoneButton: View { + var doneAction: () -> Void + + var body: some View { + Button("Done") { + doneAction() + } + .buttonStyle(TalerButtonStyle(type: .prominent)) + .padding(.horizontal) + } + } +} +// MARK: - +#if DEBUG +//struct TransactionSummary_Previews: PreviewProvider { +// static func deleteTransactionDummy(transactionId: String) async throws {} +// static func doneActionDummy() {} +// static var withdrawal = Transaction(incoming: true, +// pending: true, +// id: "some withdrawal ID", +// time: Timestamp(from: 1_666_000_000_000)) +// static var payment = Transaction(incoming: false, +// pending: false, +// id: "some payment ID", +// time: Timestamp(from: 1_666_666_000_000)) +// static func reloadActionDummy(transactionId: String) async -> Transaction { return withdrawal } +// static var previews: some View { +// Group { +// TransactionSummaryV(transaction: withdrawal, reloadAction: reloadActionDummy, doneAction: doneActionDummy) +// TransactionSummaryV(transaction: payment, reloadAction: reloadActionDummy, deleteAction: deleteTransactionDummy) +// } +// } +//} +#endif diff --git a/TalerWallet1/Views/Transactions/TransactionsListView.swift b/TalerWallet1/Views/Transactions/TransactionsListView.swift @@ -99,7 +99,7 @@ struct TransactionsArraySliceV: View { ForEach(Array(zip(transactions.indices, transactions)), id: \.1) { index, transaction in NavigationLink { LazyView { - TransactionDetailView(stack: stack.push(), + TransactionSummaryV(stack: stack.push(), transactionId: transaction.id, reloadAction: reloadOneAction, navTitle: nil,