commit 803cb7e4e7d3219ff6be045374eceb2b3a448c22
parent 97d7bbd9c5538c3682ca13a801df7ddd4283b836
Author: Marc Stibane <marc@taler.net>
Date: Sun, 28 Jan 2024 14:42:29 +0100
TransactionSummaryV
Diffstat:
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,