View Issue Details

IDProjectCategoryView StatusLast Update
0012068Talerwallet (iOS App)public2026-11-11 01:26
Reporterbot-cg [agent] Assigned ToResolution deadline
ImplementerReviewer 
Release signed byDeployed by 
VerifierCommunicatorCM_7  
Data protection authority notification requiredNo  
CSIRT notification requiredNo  
Estimated effort (hours)Actual effort (hours)0 
Fast-trackNo
Work packagenone
PrioritynormalSeverityminorReproducibilityhave not tried
Status newResolutionopen 
Summary0012068: iOS: a pending transaction stuck on an error only says "Pending" -- wallet-core's error is never shown and there is no "Retry now" (patch attached)
DescriptionWhen wallet-core cannot complete a transaction it keeps retrying, reports the last error in TransactionCommon.error and offers "retry" in txActions. The iOS wallet never shows that error and has no Retry button at all: a stuck transaction shows "Pending" / "This transaction is not yet ready..." with "Abort" as the only action. Same defect as the Android report 0012067 (related).

What the user sees (KUDOS p2p "Send" from the demo exchange; every exchange request answered with 500 via wallet-core's dev experiment start-tc?fake_500=1, which the GNU_Taler flavour applies with the app's developer mode off):
- iOS: "Send Money", spinning logo, "Pending", "This transaction is not yet ready...", amounts, "Abort" (before-ios-1.png, before-ios-2.png; the "Ready" screen right after sending looks the same: before-ios-ready-1.png). wallet-core returns "error": {"code":7005,"hint":"Unexpected HTTP status 500 in response", ...} and "txActions": ["retry","abort","suspend"] for it.
- WebExtension (reference): the transaction detail shows the state "Pending", a red card "Wallet reported a problem" with the error and a "Technical details" section (code, hint, JSON, copy button), and the actions "Try now", "Abort", "Suspend" (before-web-1.png, before-web-2.png).
- Android has the same defect (0012067); there the information is visible in developer mode only.

Code (origin/master 674b7915):
- TalerWallet1/Views/Transactions/TransactionSummaryList.swift:251-284 (abortFailDelete: abort, fail, delete - no retry), :320 (suspend/resume only in developer mode). TransactionCommon.error (Model/Transaction.swift:367) is decoded but never read by any transaction view (only refresh errors in TransactionTypeDetail.swift:197 and a payment's failReason hint in PaidTransactionDetailV.swift:96).
- Model/Transaction.swift:215-260 already has TxAction.retry with the translated title "Retry now" / "Retrying..." and :402 isRetryable, but Model/Model+Transactions.swift has no retryTransaction request.

The patch (ios-F403-tx-error-and-retry.patch, 3 files):
- Model+Transactions.swift: RetryTransaction request + WalletModel.retryTransaction (copy of resumeTransaction).
- TransactionSummaryList.swift: a section "The wallet reported a problem" with the error's hint (errorResponse.hint preferred) and "The wallet will retry automatically. You can also retry now." when common.error is set, and a "Retry now" TransactionButton when isRetryable and there is an error.
- Localizable.xcstrings: the two new English strings.

Verified: wc-ios build (GNU_Taler, Release) succeeds; after-ios-1.png / after-ios-2.png show the same scenario with the patched build (error section, "Retry now" above "Abort"); tapping "Retry now" sends retryTransaction without an error sheet.

Assisted by LLM
Steps To Reproduce1. Fund the wallet (KUDOS from https://bank.demo.taler.net/).
2. GNU_Taler build: open taler://dev-experiment/start-tc?fake_500=1 (or make the exchange unreachable).
3. Actions > Send, KUDOS 5, "Send KUDOS 5.00 now", Done.
4. Balances > Pending outgoing > the new transaction: "Pending", no reason, no Retry.
TagsNo tags attached.
Attached Files
before-ios-1.png (234,103 bytes)   
before-ios-1.png (234,103 bytes)   
before-ios-2.png (575,636 bytes)
before-ios-ready-1.png (223,318 bytes)   
before-ios-ready-1.png (223,318 bytes)   
before-web-1.png (109,870 bytes)   
before-web-1.png (109,870 bytes)   
before-web-2.png (166,428 bytes)   
before-web-2.png (166,428 bytes)   
before-android-1.png (70,879 bytes)   
before-android-1.png (70,879 bytes)   
after-ios-1.png (258,089 bytes)   
after-ios-1.png (258,089 bytes)   
after-ios-2.png (525,862 bytes)
ios-F403-tx-error-and-retry.patch (4,716 bytes)   
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,

Required notifications

Address or linkRemarkAdded byNotifiedActions

Relationships

related to 0012067 new Android: a pending transaction stuck on an error only says "pending" -- wallet-core's error and the Retry action are shown in developer mode only (patch attached) 

Activities

Issue History

Date ModifiedUsernameFieldChange
2026-11-11 01:26bot-cgNew Issue
2026-11-11 01:26bot-cgRelationship addedrelated to 0012067
2026-11-11 01:26bot-cgFile Added: before-ios-1.png
2026-11-11 01:26bot-cgFile Added: before-ios-2.png
2026-11-11 01:26bot-cgFile Added: before-ios-ready-1.png
2026-11-11 01:26bot-cgFile Added: before-web-1.png
2026-11-11 01:26bot-cgFile Added: before-web-2.png
2026-11-11 01:26bot-cgFile Added: before-android-1.png
2026-11-11 01:26bot-cgFile Added: after-ios-1.png
2026-11-11 01:26bot-cgFile Added: after-ios-2.png
2026-11-11 01:26bot-cgFile Added: ios-F403-tx-error-and-retry.patch