View Issue Details

IDProjectCategoryView StatusLast Update
0011619Talerwallet (Android App)public2026-11-11 02:03
Reportervecirex Assigned Toavalos Resolution deadline
Implementeravalos Reviewervecirex  
Release signed byavalos Deployed byavalos  
Verifiervecirex Communicator 
Data protection authority notification requiredNo  
CSIRT notification requiredNo  
Estimated effort (hours)4Actual effort (hours)0 
Fast-trackNo
Work packagenone
PrioritynormalSeverityminorReproducibilityalways
Status implementingResolutionopen 
Target Version1.7 
Summary0011619: notify user what to do to remove an exchange from payment provider list
DescriptionTo successfully remove an exchange (with some lag), tied bank accounts of the exchange world (e.g., x-taler-bank accounts) need to be removed first.

Problem: the user is not informed about this. Nothing at all happens. The exchange just sticks there.
Steps To ReproduceTested using https://bank.hacktivism.ch/
- adding exchange, step 1
- withdrawing money, step 2 (w/ auto-confirm), receiving an x-taler-bank account; the x-taler-bank account also pops up if the withdraw doesn't complete (yet) / stays on hold ("isPending")
Tagsandroid, quality, ux
Attached Files

Required notifications

Address or linkRemarkAdded byNotifiedActions

Activities

vecirex

2026-07-09 16:43

administrator   ~0029127

iOS fine: just tested in QC to remove exchange as expected (both: stable and nightly), w/ double confirmation required as accounts/coins get purged by this op.

Christian Grothoff

2026-07-10 09:36

manager   ~0029129

Well, I see no reason why the bank accounts should in any way block an exchange from being removed.

vecirex

2026-07-10 10:14

administrator   ~0029130

iOS doesn't block; just asks for double-confirm, fine there!

Florian Dold

2026-07-17 23:21

manager   ~0029187

Indeed an android issue => assigned to Ivan

bot-cg [agent]

2026-11-11 02:03

updater   ~0030400

Current state on Android master (4c483527, 2026-11-11), with fresh screenshots and a patch.

Deleting a payment service that still holds coins (here KUDOS 3 at tripwire.taler.net:8445):
- The dialog is only the title "Delete payment service" and a checkbox "Force deletion (purge)" (before-android-1.png). Nothing says what purge does.
- "Delete" without the checkbox: an error sheet "Error / Exchange in use." (wallet-core's English hint for 7033 WALLET_EXCHANGE_ENTRY_USED) with Copy/Share/Close (before-android-2.png). No hint that the purge checkbox is the way forward, nor that it destroys the coins.
- With the checkbox ticked, the exchange, its coins and its transactions are removed at once without any warning.

For comparison, same wallet state:
- iOS: "Are you sure you want to delete this payment service?" (before-ios-1.png), then "This payment service is in use. Delete anyway? / You will lose all KUDOS of this payment service." with Deposit / Delete / Cancel (before-ios-2.png).
- WebExtension: checks getExchangeResources first and shows "Force-remove exchange data? ... This exchange still has coins or withdrawal records. Force removal permanently deletes its digital cash, withdrawals, and related transaction history from this wallet..." with a required "I understand ..." checkbox (before-web-1.png).

Code: wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt:384-409 (DeleteExchangeDialog), :122-124 (delete error -> onShowError); exchanges/ExchangeManager.kt:139-153.

Patch (android-delete-exchange-explained.patch): before the dialog, ExchangeManager.hasResources() asks wallet-core (getExchangeResources). An exchange without coins/withdrawals gets "This wallet holds no digital cash or withdrawals from this payment service." and a plain Delete (after-android-2-unused.png). One that is in use gets "This payment service is in use. Deleting it permanently removes all KUDOS of this payment service and their transaction history from this wallet. This cannot be undone." and Delete stays disabled until "I understand that this digital cash will be lost." is ticked; it then purges (after-android-1-in-use.png). If the check fails, the dialog assumes "in use". The bare "Force deletion (purge)" checkbox is gone (its string is kept). Built with :wallet:assembleFdroidSim and checked on the emulator as shown.

Assisted by LLM
before-android-1.png (100,825 bytes)   
before-android-1.png (100,825 bytes)   
before-android-2.png (116,164 bytes)   
before-android-2.png (116,164 bytes)   
before-ios-1.png (398,681 bytes)
before-ios-2.png (425,788 bytes)
before-web-1.png (152,827 bytes)   
before-web-1.png (152,827 bytes)   
after-android-1-in-use.png (130,793 bytes)   
after-android-1-in-use.png (130,793 bytes)   
after-android-2-unused.png (109,914 bytes)   
after-android-2-unused.png (109,914 bytes)   
android-delete-exchange-explained.patch (6,225 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..26bef0be 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeListScreen.kt
@@ -98,6 +98,8 @@ fun ExchangeListScreen(
     var addInProgress by remember { mutableStateOf(false) }
     var addErrorMessage by remember { mutableStateOf<String?>(null) }
     var exchangeToDelete by remember { mutableStateOf<ExchangeItem?>(null) }
+    // null while unknown; when the check fails, assume the exchange is in use
+    var exchangeInUse by remember { mutableStateOf<Boolean?>(null) }
 
     val addErrorText = stringResource(R.string.exchange_add_error)
 
@@ -167,7 +169,11 @@ fun ExchangeListScreen(
                         devMode = devMode,
                         onAction = { action ->
                             if (action == ExchangeAction.Delete) {
-                                exchangeToDelete = exchange
+                                scope.launch {
+                                    exchangeInUse = exchangeManager
+                                        .hasResources(exchange.exchangeBaseUrl) ?: true
+                                    exchangeToDelete = exchange
+                                }
                             } else {
                                 handleExchangeAction(
                                     scope = scope,
@@ -204,6 +210,8 @@ fun ExchangeListScreen(
 
     if (exchangeToDelete != null) {
         DeleteExchangeDialog(
+            inUse = exchangeInUse ?: true,
+            currency = exchangeToDelete?.currency,
             onDismiss = {
                 exchangeToDelete = null
             },
@@ -382,21 +390,38 @@ fun AddExchangeDialog(
 
 @Composable
 fun DeleteExchangeDialog(
+    inUse: Boolean,
+    currency: String?,
     onDismiss: () -> Unit,
     onConfirm: (purge: Boolean) -> Unit,
 ) {
-    var purge by remember { mutableStateOf(false) }
+    // deleting an exchange that is in use needs a purge, which destroys its coins:
+    // say so, and require an explicit acknowledgement
+    var acknowledged by remember { mutableStateOf(false) }
     AlertDialog(
         onDismissRequest = onDismiss,
         title = { Text(stringResource(R.string.exchange_delete)) },
         text = {
-            Row(verticalAlignment = Alignment.CenterVertically) {
-                Checkbox(checked = purge, onCheckedChange = { purge = it })
-                Text(stringResource(R.string.exchange_delete_force))
+            Column {
+                if (inUse) {
+                    Text(stringResource(R.string.exchange_delete_in_use, currency ?: ""))
+                    Row(
+                        modifier = Modifier.padding(top = 8.dp),
+                        verticalAlignment = Alignment.CenterVertically,
+                    ) {
+                        Checkbox(checked = acknowledged, onCheckedChange = { acknowledged = it })
+                        Text(stringResource(R.string.exchange_delete_in_use_ack))
+                    }
+                } else {
+                    Text(stringResource(R.string.exchange_delete_unused))
+                }
             }
         },
         confirmButton = {
-            TextButton(onClick = { onConfirm(purge) }) {
+            TextButton(
+                onClick = { onConfirm(inUse) },
+                enabled = !inUse || acknowledged,
+            ) {
                 Text(stringResource(R.string.transactions_delete))
             }
         },
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
index 5dfc8b62..7eea04a3 100644
--- a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
+++ b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
@@ -43,6 +43,11 @@ data class ExchangeListResponse(
     val exchanges: List<ExchangeItem>,
 )
 
+@Serializable
+data class ExchangeResourcesResponse(
+    val hasResources: Boolean,
+)
+
 class ExchangeManager(
     private val api: WalletBackendApi,
     private val scope: CoroutineScope,
@@ -152,6 +157,22 @@ class ExchangeManager(
         }
     }
 
+    /**
+     * Whether the wallet still holds coins or withdrawals of this exchange,
+     * i.e. whether deleting it needs a purge.  Null if that could not be checked.
+     */
+    suspend fun hasResources(exchangeUrl: String): Boolean? {
+        var result: Boolean? = null
+        api.request("getExchangeResources", ExchangeResourcesResponse.serializer()) {
+            put("exchangeBaseUrl", exchangeUrl)
+        }.onError {
+            Log.e(TAG, "Error getExchangeResources: $it")
+        }.onSuccess {
+            result = it.hasResources
+        }
+        return result
+    }
+
     fun findExchangeForCurrency(currency: String): Flow<ExchangeItem?> = flow {
         emit(findExchangeByCurrency(currency))
     }
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
index 89750709..fee29a66 100644
--- a/wallet/src/main/res/values/strings.xml
+++ b/wallet/src/main/res/values/strings.xml
@@ -441,6 +441,9 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish
     <string name="exchange_add_url">Enter address of payment service</string>
     <string name="exchange_delete">Delete payment service</string>
     <string name="exchange_delete_force">Force deletion (purge)</string>
+    <string name="exchange_delete_unused">This wallet holds no digital cash or withdrawals from this payment service.</string>
+    <string name="exchange_delete_in_use">This payment service is in use. Deleting it permanently removes all %1$s of this payment service and their transaction history from this wallet. This cannot be undone.</string>
+    <string name="exchange_delete_in_use_ack">I understand that this digital cash will be lost.</string>
     <plurals name="exchange_fee_coin">
         <item quantity="one">Coin: %s (used %d time)</item>
         <item quantity="other">Coin: %s (used %d times)</item>

Issue History

Date ModifiedUsernameFieldChange
2026-07-09 08:22vecirexNew Issue
2026-07-09 08:40vecirexTag Attached: ux
2026-07-09 08:40vecirexTag Attached: quality
2026-07-09 08:40vecirexTag Attached: android
2026-07-09 08:43vecirexSummarynotify user what to do to remove an exchange form payment provider list => notify user what to do to remove an exchange from payment provider list
2026-07-09 16:43vecirexNote Added: 0029127
2026-07-10 09:36Christian GrothoffNote Added: 0029129
2026-07-10 09:36Christian GrothoffAssigned To => Florian Dold
2026-07-10 09:36Christian GrothoffStatusnew => assigned
2026-07-10 09:36Christian GrothoffTarget Version => 1.6
2026-07-10 10:14vecirexNote Added: 0029130
2026-07-17 23:21Florian DoldAssigned ToFlorian Dold => avalos
2026-07-17 23:21Florian DoldNote Added: 0029187
2026-08-09 22:24Christian GrothoffTarget Version1.6 => 1.7
2026-11-03 07:21Statusassigned => implementing
2026-11-03 14:04Christian GrothoffImplementer => avalos
2026-11-03 14:04Christian GrothoffReviewer => vecirex
2026-11-03 14:04Christian GrothoffRelease signed by => avalos
2026-11-03 14:04Christian GrothoffDeployed by => avalos
2026-11-03 14:04Christian GrothoffVerifier => vecirex
2026-11-03 14:04Christian GrothoffEstimated effort (hours) => 4
2026-11-11 02:03bot-cgNote Added: 0030400
2026-11-11 02:03bot-cgFile Added: before-android-1.png
2026-11-11 02:03bot-cgFile Added: before-android-2.png
2026-11-11 02:03bot-cgFile Added: before-ios-1.png
2026-11-11 02:03bot-cgFile Added: before-ios-2.png
2026-11-11 02:03bot-cgFile Added: before-web-1.png
2026-11-11 02:03bot-cgFile Added: after-android-1-in-use.png
2026-11-11 02:03bot-cgFile Added: after-android-2-unused.png
2026-11-11 02:03bot-cgFile Added: android-delete-exchange-explained.patch