diff --git a/TalerWallet1/Model/Model+Transactions.swift b/TalerWallet1/Model/Model+Transactions.swift index 60a4ec3a..6fc991dd 100755 --- a/TalerWallet1/Model/Model+Transactions.swift +++ b/TalerWallet1/Model/Model+Transactions.swift @@ -147,6 +147,17 @@ struct ResumeTransaction: WalletBackendFormattedRequest { var transactionId: String } } +/// A request to retry a transaction now, instead of waiting for wallet-core's next scheduled attempt. +struct RetryTransaction: WalletBackendFormattedRequest { + struct Response: Decodable {} // no result - getting no error back means success + var operation: String { "retryTransaction" } + func args() -> Args { Args(transactionId: transactionId) } + + var transactionId: String + struct Args: Encodable { + var transactionId: String + } +} // MARK: - extension WalletModel { /// ask wallet-core for its list of transactions filtered by state @@ -198,4 +209,10 @@ extension WalletModel { logger.notice("resumeTransaction: \(transactionId, privacy: .private(mask: .hash))") _ = try await sendRequest(request, viewHandles: viewHandles) } + + nonisolated func retryTransaction(transactionId: String, viewHandles: Bool = false) async throws { + let request = RetryTransaction(transactionId: transactionId) + logger.notice("retryTransaction: \(transactionId, privacy: .private(mask: .hash))") + _ = try await sendRequest(request, viewHandles: viewHandles) + } } diff --git a/TalerWallet1/Resources/Localizable.xcstrings b/TalerWallet1/Resources/Localizable.xcstrings index e283ca45..38afddf4 100755 --- a/TalerWallet1/Resources/Localizable.xcstrings +++ b/TalerWallet1/Resources/Localizable.xcstrings @@ -19336,6 +19336,12 @@ } } }, + "The wallet reported a problem" : { + "comment" : "Transaction detail: heading above wallet-core's last error for this transaction" + }, + "The wallet will retry automatically. You can also retry now." : { + "comment" : "Transaction detail: below wallet-core's last error, next to the Retry now button" + }, "The withdrawal was aborted.\n\n%@" : { "localizations" : { "de" : { diff --git a/TalerWallet1/Views/Transactions/TransactionSummaryList.swift b/TalerWallet1/Views/Transactions/TransactionSummaryList.swift index 74f04b65..f6250fbf 100755 --- a/TalerWallet1/Views/Transactions/TransactionSummaryList.swift +++ b/TalerWallet1/Views/Transactions/TransactionSummaryList.swift @@ -247,8 +247,40 @@ struct TransactionSummaryList: View { } } + /// wallet-core keeps the last error of a transaction it cannot complete; + /// tell the user why it is stuck instead of only showing "Pending" + @ViewBuilder + func lastError(_ common: TransactionCommon) -> some View { + if let error = common.error { + let errorColor = WalletColors().errorColor + Section { + VStack(alignment: .leading, spacing: 6) { + Text("The wallet reported a problem") + .talerFont(.headline) + if let hint = error.errorResponse?.hint ?? error.hint, !hint.isEmpty { + Text(hint) + .talerFont(.body) + } + if talerTX.isRetryable { + Text("The wallet will retry automatically. You can also retry now.") + .talerFont(.body) + } + } + .foregroundColor(errorColor) + .listRowSeparator(.hidden) + } + } + } + @ViewBuilder func abortFailDelete(_ common: TransactionCommon) -> some View { + if talerTX.isRetryable && common.error != nil { + TransactionButton(transactionId: common.transactionId, + command: .retry, + warning: nil, + didExecute: $ignoreThis, + action: model.retryTransaction) + } // Retry button if talerTX.isAbortable { let warning = String(localized: "Are you sure you want to abort this transaction?") TransactionButton(transactionId: common.transactionId, @@ -335,6 +367,7 @@ struct TransactionSummaryList: View { } } } + if !isPayDialog && withActions { lastError(common) } if !controller.stalledConnection { TransactionTypeDetail(stack: stack.push(), transaction: $talerTX,