View Issue Details

IDProjectCategoryView StatusLast Update
0012112Talerwallet (Android App)public2026-11-11 02:27
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 
Summary0012112: Android: ToS status decoded from "status" but wallet-core sends "tosStatus"; an exchange without terms shows "NULL" instead of "terms not provided"
DescriptionFor an exchange that publishes no terms of service, Android shows a terms page whose only content is the word "NULL", instead of its own message "Terms of service were not provided by payment service".

Cause: wallet-core's getExchangeTos answer names the field "tosStatus" (GetExchangeTosResult; for a missing /terms it returns {"tosStatus":"missing-tos","contentType":"text/plain","content":"NULL","currentEtag":"NULL",...}, taler-wallet-core/src/exchanges.ts getExchangeTos), but Android's TosResponse decodes it from "status" (wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt:63-64). The JSON decoder ignores unknown keys, so status is always Unknown and the MissingTos branch in ReviewExchangeTosScreen.kt:159-162 can never run. The same field drives nothing else on that screen.

What the user sees (exchange.demo.taler.net behind a local proxy whose /terms answers 404, so wallet-core reports missing-tos):
- Android: "PSP's terms of service" / "NULL" (before-android-1.png). The menu offers "Review terms of service" because the exchange list (which decodes tosStatus correctly) counts missing-tos as accepted.
- iOS: "This payment service has not published any terms of service." (before-ios-1.png).
- WebExtension: "Status: Terms unavailable" above a "NULL" body (before-web-1.png).
Real exchanges hit this when /terms is missing or temporarily unavailable (404/501), cf. 0011620 and 0010928.

Patch (android-tos-status-field.patch):
- @SerialName("tosStatus") on TosResponse.status;
- the existing missing-terms message (R.string.exchange_tos_missing) is shown inside the screen's scaffold, so title and back navigation stay, and no Accept button is offered when there is nothing to accept (previously the message replaced the whole screen);
- unit test wallet/src/test/java/net/taler/wallet/exchanges/TosResponseTest.kt decoding wallet-core's missing-tos and proposed answers.
Verified: ./gradlew :wallet:testFdroidSimUnitTest --tests net.taler.wallet.exchanges.TosResponseTest passes (2/2); with the @SerialName line removed both tests fail. :wallet:assembleFdroidSim builds; after-android-1.png shows "Terms of service were not provided by payment service" for the same exchange.

Assisted by LLM
Steps To ReproduceUse an exchange whose /terms answers 404 (e.g. a proxy in front of exchange.demo.taler.net), add it, then Settings > Payment services > menu > Review terms of service.
TagsNo tags attached.
Attached Files
before-android-1.png (24,784 bytes)   
before-android-1.png (24,784 bytes)   
before-ios-1.png (127,070 bytes)   
before-ios-1.png (127,070 bytes)   
before-web-1.png (53,496 bytes)   
before-web-1.png (53,496 bytes)   
after-android-1.png (38,406 bytes)   
after-android-1.png (38,406 bytes)   
android-tos-status-field.patch (4,488 bytes)   
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..9e918c4d 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/Exchanges.kt
@@ -61,6 +61,8 @@ enum class ExchangeTosStatus {
 
 @Serializable
 data class TosResponse(
+    // wallet-core's GetExchangeTosResult calls it "tosStatus"
+    @SerialName("tosStatus")
     val status: ExchangeTosStatus = ExchangeTosStatus.Unknown,
     val content: String,
     val currentEtag: String,
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ReviewExchangeTosScreen.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ReviewExchangeTosScreen.kt
index 34701d08..444b0a81 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ReviewExchangeTosScreen.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ReviewExchangeTosScreen.kt
@@ -156,11 +156,7 @@ fun ReviewExchangeTosComposable(
     onNavigateBack: () -> Unit,
     model: MainViewModel? = null,
 ) {
-    if (tos.status == ExchangeTosStatus.MissingTos) {
-        EmptyComposable(message = stringResource(R.string.exchange_tos_missing))
-        return
-    }
-
+    val missingTos = tos.status == ExchangeTosStatus.MissingTos
     var expanded by remember { mutableStateOf(false) }
 
     GlobalScaffold(
@@ -168,7 +164,8 @@ fun ReviewExchangeTosComposable(
         title = { Text(stringResource(R.string.nav_exchange_tos)) },
         onNavigateBack = onNavigateBack,
         bottomBar = {
-            if (!readOnly) BottomButtonBox {
+            // nothing to accept when the exchange publishes no terms
+            if (!readOnly && !missingTos) BottomButtonBox {
                 Button(
                     modifier = Modifier
                         .systemBarsPaddingBottom(),
@@ -179,7 +176,10 @@ fun ReviewExchangeTosComposable(
             }
         },
     ) { innerPadding ->
-        LazyColumn(Modifier.padding(innerPadding)) {
+        if (missingTos) EmptyComposable(
+            modifier = Modifier.padding(innerPadding),
+            message = stringResource(R.string.exchange_tos_missing),
+        ) else LazyColumn(Modifier.padding(innerPadding)) {
             if (tos.tosAvailableLanguages.size > 1) item {
                 ExposedDropdownMenuBox(
                     expanded = expanded,
diff --git a/wallet/src/test/java/net/taler/wallet/exchanges/TosResponseTest.kt b/wallet/src/test/java/net/taler/wallet/exchanges/TosResponseTest.kt
new file mode 100644
index 00000000..dd9e52b3
--- /dev/null
+++ b/wallet/src/test/java/net/taler/wallet/exchanges/TosResponseTest.kt
@@ -0,0 +1,43 @@
+/*
+ * This file is part of GNU Taler
+ * (C) 2026 Taler Systems S.A.
+ *
+ * GNU Taler is free software; you can redistribute it and/or modify it under the
+ * terms of the GNU General Public License as published by the Free Software
+ * Foundation; either version 3, or (at your option) any later version.
+ *
+ * GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
+ * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
+ * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with
+ * GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+ */
+
+package net.taler.wallet.exchanges
+
+import net.taler.wallet.backend.BackendManager
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class TosResponseTest {
+
+    private fun decode(s: String): TosResponse =
+        BackendManager.json.decodeFromString(TosResponse.serializer(), s)
+
+    /** wallet-core's answer for an exchange that publishes no terms (GET /terms -> 404) */
+    @Test
+    fun testMissingTos() {
+        val tos = decode("""{"tosStatus":"missing-tos","contentType":"text/plain","content":"NULL",""" +
+            """"currentEtag":"NULL","tosAvailableLanguages":[]}""")
+        assertEquals(ExchangeTosStatus.MissingTos, tos.status)
+    }
+
+    @Test
+    fun testProposedTos() {
+        val tos = decode("""{"tosStatus":"proposed","contentType":"text/markdown","content":"# Terms",""" +
+            """"currentEtag":"v1","contentLanguage":"en","tosAvailableLanguages":["en"]}""")
+        assertEquals(ExchangeTosStatus.Proposed, tos.status)
+        assertEquals("v1", tos.currentEtag)
+    }
+}
android-tos-status-field.patch (4,488 bytes)   

Required notifications

Address or linkRemarkAdded byNotifiedActions

Relationships

related to 0011620 closedvecirex TEST: Wallet apps (incl. taller-wallet-cli) just hang if /terms (exchange) missing + "NULL" terms cannot be accepted 
related to 0012113 new WebExtension: exchange without terms of service shows a "NULL" body and still offers "Accept terms" 

Activities

Issue History

Date ModifiedUsernameFieldChange
2026-11-11 02:25bot-cgNew Issue
2026-11-11 02:25bot-cgRelationship addedrelated to 0011620
2026-11-11 02:25bot-cgFile Added: before-android-1.png
2026-11-11 02:25bot-cgFile Added: before-ios-1.png
2026-11-11 02:25bot-cgFile Added: before-web-1.png
2026-11-11 02:25bot-cgFile Added: after-android-1.png
2026-11-11 02:25bot-cgFile Added: android-tos-status-field.patch