View Issue Details

IDProjectCategoryView StatusLast Update
0012086Talerwallet (Android App)public2026-11-11 01:41
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 
Summary0012086: Android: payment-service list never shows that an exchange is unavailable or why its last update failed
DescriptionWhen wallet-core cannot update an exchange (unreachable, HTTP error, bad response), Android's "Payment services" list keeps showing it exactly like a healthy one: name and "Currency: KUDOS". "Reload information" does not report anything either (updateExchangeEntry itself succeeds; the update fails in the background). The user only meets the problem later as a failed withdrawal or payment.

What the user sees (same exchange, made to fail with HTTP 503):
- WebExtension: list entry with badge "Update unavailable", "In use · terms ...", and the reason "Unexpected HTTP status 503 in response" (before-web-1-list.png); detail page "Update status: Update unavailable" and a card "The last exchange update failed" with technical details (before-web-2-detail.png).
- iOS: the Payment Services row shows "Status: Unavailable" and "Last Error: <code>" plus hint/detail (TalerWallet1/Views/Settings/Exchange/ExchangeRowView.swift:47, ExchangeStatus view :92-121; added for 0010167). Not screenshotted for this failure.
- Android: "http://localhost:18091 / Currency: KUDOS", nothing else (before-android-1.png). wallet-core's listExchanges answer for it has exchangeUpdateStatus "unavailable-update" and lastUpdateErrorInfo (seen in logcat).

Code (origin/master 4c483527):
- wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt:30-41: ExchangeItem does not decode exchangeUpdateStatus or lastUpdateErrorInfo.
- exchanges/ExchangeListScreen.kt:219-260: the row shows only name and currency. (The "Payment service not contacted" branch for currency == null is never taken: wallet-core always sends a currency, "UNKNOWN" for never-contacted entries.)

Reproduction: a local proxy in front of exchange.demo.taler.net that can answer 503 (lib/wc_fault_proxy.py of our test setup, base_url rewriting); on the emulator via adb reverse tcp:18091 tcp:18091, because the app allows cleartext only to localhost.
1. Settings > Payment services > (+) http://localhost:18091/ > OK (added, "Currency: KUDOS").
2. Switch the proxy to "status-503".
3. Row menu > "Reload information". The row stays unchanged.

Patch (android-exchange-unavailable.patch): decode exchangeUpdateStatus and lastUpdateErrorInfo (optional fields); when the status is unavailable/unavailable-update or an update error is present, the row adds "Payment service currently unavailable" and "Last update failed: <wallet-core hint>" in the error colour. Two new English strings.
Verified: :wallet:assembleFdroidSim builds; after-android-1.png (failing exchange: both lines shown); after-android-2-recovered.png (proxy back to normal, reload: lines gone).

Assisted by LLM
Steps To ReproduceSee description (local proxy answering 503).
TagsNo tags attached.
Attached Files
before-web-1-list.png (137,723 bytes)   
before-web-1-list.png (137,723 bytes)   
before-web-2-detail.png (109,966 bytes)   
before-web-2-detail.png (109,966 bytes)   
before-android-1.png (78,456 bytes)   
before-android-1.png (78,456 bytes)   
after-android-1.png (99,492 bytes)   
after-android-1.png (99,492 bytes)   
after-android-2-recovered.png (78,456 bytes)   
after-android-2-recovered.png (78,456 bytes)   
android-exchange-unavailable.patch (3,706 bytes)   
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt
index 7f64d6db..eeb12428 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt
@@ -235,6 +235,20 @@ fun ExchangeItemComposable(
                 stringResource(R.string.exchange_list_currency, it)
             } ?: stringResource(R.string.exchange_not_contacted)
             Text(text = currencyText, style = MaterialTheme.typography.bodyMedium)
+            if (exchange.isUnavailable) {
+                Text(
+                    text = stringResource(R.string.exchange_status_unavailable),
+                    style = MaterialTheme.typography.bodyMedium,
+                    color = MaterialTheme.colorScheme.error,
+                )
+                exchange.lastUpdateErrorInfo?.error?.userFacingMsg?.takeIf { it.isNotEmpty() }?.let { msg ->
+                    Text(
+                        text = stringResource(R.string.exchange_last_error, msg),
+                        style = MaterialTheme.typography.bodySmall,
+                        color = MaterialTheme.colorScheme.error,
+                    )
+                }
+            }
         }
         if (exchange.currency != null) {
             Box {
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
index d110f044..fe721c07 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
@@ -18,6 +18,7 @@ package net.taler.wallet.exchanges
 
 import kotlinx.serialization.SerialName
 import kotlinx.serialization.Serializable
+import net.taler.wallet.backend.TalerErrorInfo
 import net.taler.wallet.balances.ScopeInfo
 import net.taler.wallet.cleanExchange
 
@@ -36,10 +37,23 @@ data class ExchangeItem(
     val paytoUris: List<String>,
     val scopeInfo: ScopeInfo? = null,
     val tosStatus: ExchangeTosStatus = ExchangeTosStatus.Unknown,
+    val exchangeUpdateStatus: String? = null,
+    val lastUpdateErrorInfo: ExchangeUpdateErrorInfo? = null,
 ) {
     val name: String get() = cleanExchange(exchangeBaseUrl)
+
+    /** wallet-core could not update this exchange (unreachable, bad response, …) */
+    val isUnavailable: Boolean
+        get() = exchangeUpdateStatus == "unavailable"
+                || exchangeUpdateStatus == "unavailable-update"
+                || lastUpdateErrorInfo != null
 }
 
+@Serializable
+data class ExchangeUpdateErrorInfo(
+    val error: TalerErrorInfo,
+)
+
 @Serializable
 enum class ExchangeTosStatus {
     Unknown,
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
index 89750709..3fe10021 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -470,6 +470,8 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish
     <string name="exchange_list_title">Payment services</string>
     <string name="exchange_menu_manual_withdraw">Withdraw</string>
     <string name="exchange_not_contacted">Payment service not contacted</string>
+    <string name="exchange_status_unavailable">Payment service currently unavailable</string>
+    <string name="exchange_last_error">Last update failed: %1$s</string>
     <string name="exchange_reload">Reload information</string>
     <string name="exchange_settings_summary">Manage the payment services known to this wallet</string>
     <string name="exchange_settings_title">Payment services</string>

Required notifications

Address or linkRemarkAdded byNotifiedActions

Relationships

related to 0010167 closedMarcS iOS wallet doesn't show error states of exchange entries [2d] 

Activities

Issue History

Date ModifiedUsernameFieldChange
2026-11-11 01:41bot-cgNew Issue
2026-11-11 01:41bot-cgRelationship addedrelated to 0010167
2026-11-11 01:41bot-cgFile Added: before-web-1-list.png
2026-11-11 01:41bot-cgFile Added: before-web-2-detail.png
2026-11-11 01:41bot-cgFile Added: before-android-1.png
2026-11-11 01:41bot-cgFile Added: after-android-1.png
2026-11-11 01:41bot-cgFile Added: after-android-2-recovered.png
2026-11-11 01:41bot-cgFile Added: android-exchange-unavailable.patch