View Issue Details

IDProjectCategoryView StatusLast Update
0012067Talerwallet (Android 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 
Summary0012067: 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)
DescriptionWhen wallet-core cannot complete a transaction (exchange unreachable, exchange answering 5xx, ...) it keeps retrying and reports the last error of the transaction in TransactionCommon.error, and offers the "retry" action in txActions. Outside developer mode the Android wallet shows neither: the transaction just says "This transaction is pending" for ever, with Abort as the only action. The error and the Retry button only exist in developer mode (raw JSON behind an "Error" button).

What the user sees (KUDOS p2p "Send" from the demo exchange whose follow-up requests fail):
- Android, developer mode off: "This transaction is pending", amounts, purpose, "Abort". Nothing says why it does not progress, no way to retry now (before-android-1.png). wallet-core's getTransactionById for this transaction at that moment returned "error": {"code":7003,"hint":"HTTP request failed: Failed to connect to exchange.demo.taler.net/...:443"} and "txActions": ["retry","abort","suspend"].
- Android, developer mode on: the same screen shows "Retry", "Suspend", "Show JSON" and a red "Error" button with the JSON - the information is there, just hidden from normal users (before-android-devmode-1.png).
- iOS (same defect, filed separately and related): "Pending", "This transaction is not yet ready...", "Abort" (before-ios-1.png, before-ios-2.png).
- 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).

Code (origin/master 4c483527):
- wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt:103-108: Resume, Suspend and Retry are rendered only if (devMode) (commit fe7d07be "[wallet] Only show "Retry" action in dev mode", 2024-06).
- The error is shown only in developer mode, as JSON (transactions/ErrorTransactionComposable.kt): transactions/TransactionDetailScreen.kt:402-406, 445-451, 497-503; withdraw/TransactionWithdrawalComposable.kt:203-207; deposit/TransactionDepositComposable.kt:143-146; refund/TransactionRefundComposable.kt:105-109; payment/TransactionPaymentComposable.kt:150-153.
- Everything needed is already decoded (Transaction.error, txActions) and wired (TransactionManager.retryTransaction, R.string.transactions_retry).

Why it matters: a pending transaction that is stuck on an error looks exactly like one that is waiting for the counterparty. The user cannot tell that the exchange (or the network) is the problem and has to wait for wallet-core's back-off, which grows to minutes.

The patch (android-F403-tx-error-and-retry.patch, 2 files):
- TransitionsComposable.kt (used by every transaction detail screen): when the transaction carries an error and developer mode is off, a banner "The wallet reported a problem" with the error text and "The wallet will retry automatically. You can also retry now."; the Retry action is shown to everybody when there is an error (Suspend/Resume stay developer-only, keeping the intent of fe7d07be for healthy transactions).
- strings.xml: two new English strings.
The error text is still wallet-core's English hint; a translated message per error code is the subject of a separate report (Android error mapping), which this banner would pick up.

Verified: :wallet:assembleFdroidSim and :wallet:testFdroidSimUnitTest pass; after-android-1.png is the same transaction with the patched build (banner + Retry); tapping Retry sends retryTransaction (logcat).

Assisted by LLM
Steps To Reproduce1. Fund the wallet (e.g. KUDOS:10 from https://bank.demo.taler.net/).
2. Send money to another wallet (p2p push), KUDOS:1.
3. Make the exchange unreachable for the device (here: iptables REJECT for exchange.demo.taler.net inside the emulator; a dead network or a broken exchange does the same), wait until the purse long-poll has failed once (~60 s).
4. Open the pending transaction with developer mode off: "This transaction is pending" and "Abort" only.
(Alternative with developer mode: taler://dev-experiment/start-tc?fake_500=1 before step 2 makes every exchange request fail with 500; switching developer mode off afterwards removes the experiment again, so the hidden-error screen needs the network variant on Android.)
TagsNo tags attached.
Attached Files
before-android-1.png (70,879 bytes)   
before-android-1.png (70,879 bytes)   
before-android-devmode-1.png (101,176 bytes)   
before-android-devmode-1.png (101,176 bytes)   
before-ios-1.png (234,103 bytes)   
before-ios-1.png (234,103 bytes)   
before-ios-2.png (575,636 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)   
after-android-1.png (111,190 bytes)   
after-android-1.png (111,190 bytes)   
android-F403-tx-error-and-retry.patch (5,451 bytes)   
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt
index f654604f..e0045197 100644
--- a/wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt
+++ b/wallet/src/main/java/net/taler/wallet/transactions/TransitionsComposable.kt
@@ -18,6 +18,7 @@ package net.taler.wallet.transactions
 
 import androidx.compose.foundation.layout.Arrangement.Center
 import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.ExperimentalLayoutApi
 import androidx.compose.foundation.layout.FlowRow
 import androidx.compose.foundation.layout.Row
@@ -27,6 +28,7 @@ import androidx.compose.foundation.verticalScroll
 import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.Button
 import androidx.compose.material3.ButtonDefaults
+import androidx.compose.material3.CardDefaults
 import androidx.compose.material3.Icon
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
@@ -42,11 +44,15 @@ import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
 import kotlinx.serialization.json.Json
 import net.taler.lib.android.copyToClipBoard
 import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.Banner
 import net.taler.wallet.transactions.TransactionAction.*
 
 @OptIn(ExperimentalLayoutApi::class)
@@ -99,9 +105,19 @@ fun TransitionsComposable(
             })
     }
     
+    // Developer mode has the raw JSON (ErrorTransactionButton); everybody
+    // else gets told that the wallet is stuck on an error and why.
+    val error = t.error
+    if (error != null && !devMode) {
+        TransactionErrorBanner(error)
+    }
+
     FlowRow(horizontalArrangement = Center) {
         t.txActions.forEach {
-            if (it in arrayOf(Resume, Suspend, Retry)) {
+            if (it == Retry && error != null) {
+                // a failing transaction can be retried by anybody
+                TransitionComposable(it, onTransition)
+            } else if (it in arrayOf(Resume, Suspend, Retry)) {
                 if (devMode) TransitionComposable(it, onTransition)
             } else {
                 TransitionComposable(it, onTransition)
@@ -117,6 +133,42 @@ fun TransitionsComposable(
     }
 }
 
+@Composable
+fun TransactionErrorBanner(error: TalerErrorInfo) {
+    Banner(
+        modifier = Modifier.padding(top = 16.dp),
+        colors = CardDefaults.cardColors(
+            containerColor = MaterialTheme.colorScheme.errorContainer,
+            contentColor = MaterialTheme.colorScheme.onErrorContainer,
+        ),
+    ) {
+        Column(Modifier.fillMaxWidth()) {
+            Text(
+                text = stringResource(R.string.transaction_error_title),
+                fontWeight = FontWeight.Bold,
+                textAlign = TextAlign.Center,
+                modifier = Modifier.fillMaxWidth(),
+            )
+            Text(
+                text = error.userFacingMsg.ifEmpty {
+                    stringResource(R.string.error_code_only, error.rawCode)
+                },
+                textAlign = TextAlign.Center,
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(top = 8.dp),
+            )
+            Text(
+                text = stringResource(R.string.transaction_error_retry_hint),
+                textAlign = TextAlign.Center,
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .padding(top = 8.dp),
+            )
+        }
+    }
+}
+
 @Composable
 fun TransitionComposable(t: TransactionAction, onClick: (t: TransactionAction) -> Unit) {
     var showConfirmDialog by remember { mutableStateOf(false) }
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
index 89750709..8ac36512 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -210,6 +210,8 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish
     <string name="transaction_state_failed">This transaction has failed or been abandoned</string>
     <string name="transaction_state_finalizing">This transaction is finalizing</string>
     <string name="transaction_state_pending">This transaction is pending</string>
+    <string name="transaction_error_title">The wallet reported a problem</string>
+    <string name="transaction_error_retry_hint">The wallet will retry automatically. You can also retry now.</string>
     <string name="transaction_state_pending_bank">Waiting for authorization in the bank</string>
     <string name="transaction_state_pending_kyc_bank">This transaction would exceed a limit set by the payment service. To increase the limit, follow the instructions linked below.</string>
     <string name="transaction_state_pending_kyc_auth">You need to prove having control over the bank account for the deposit. To continue, follow the instructions linked below.</string>

Required notifications

Address or linkRemarkAdded byNotifiedActions

Relationships

related to 0012068 new 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) 

Activities

Issue History

Date ModifiedUsernameFieldChange
2026-11-11 01:26bot-cgNew Issue
2026-11-11 01:26bot-cgFile Added: before-android-1.png
2026-11-11 01:26bot-cgFile Added: before-android-devmode-1.png
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-web-1.png
2026-11-11 01:26bot-cgFile Added: before-web-2.png
2026-11-11 01:26bot-cgFile Added: after-android-1.png
2026-11-11 01:26bot-cgFile Added: android-F403-tx-error-and-retry.patch