View Issue Details
| ID | Project | Category | View Status | Date Submitted | Last Update |
|---|---|---|---|---|---|
| 0011619 | Taler | wallet (Android App) | public | 2026-07-09 08:22 | 2026-11-11 02:03 |
| Reporter | vecirex | Assigned To | avalos | Resolution deadline | |
| Implementer | avalos | Reviewer | vecirex | ||
| Release signed by | avalos | Deployed by | avalos | ||
| Verifier | vecirex | Communicator | |||
| Data protection authority notification required | No | ||||
| CSIRT notification required | No | ||||
| Estimated effort (hours) | 4 | Actual effort (hours) | 0 | ||
| Fast-track | No | ||||
| Work package | none | ||||
| Priority | normal | Severity | minor | Reproducibility | always |
| Status | implementing | Resolution | open | ||
| Target Version | 1.7 | ||||
| Summary | 0011619: notify user what to do to remove an exchange from payment provider list | ||||
| Description | To 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 Reproduce | Tested 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") | ||||
| Tags | android, quality, ux | ||||
| Attached Files | |||||
| 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. | |
| Well, I see no reason why the bank accounts should in any way block an exchange from being removed. | |
| iOS doesn't block; just asks for double-confirm, fine there! | |
| Indeed an android issue => assigned to Ivan | |
| 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 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>
| |
| Date Modified | Username | Field | Change |
|---|---|---|---|
| 2026-07-09 08:22 | vecirex | New Issue | |
| 2026-07-09 08:40 | vecirex | Tag Attached: ux | |
| 2026-07-09 08:40 | vecirex | Tag Attached: quality | |
| 2026-07-09 08:40 | vecirex | Tag Attached: android | |
| 2026-07-09 08:43 | vecirex | Summary | notify 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:43 | vecirex | Note Added: 0029127 | |
| 2026-07-10 09:36 | Christian Grothoff | Note Added: 0029129 | |
| 2026-07-10 09:36 | Christian Grothoff | Assigned To | => Florian Dold |
| 2026-07-10 09:36 | Christian Grothoff | Status | new => assigned |
| 2026-07-10 09:36 | Christian Grothoff | Target Version | => 1.6 |
| 2026-07-10 10:14 | vecirex | Note Added: 0029130 | |
| 2026-07-17 23:21 | Florian Dold | Assigned To | Florian Dold => avalos |
| 2026-07-17 23:21 | Florian Dold | Note Added: 0029187 | |
| 2026-08-09 22:24 | Christian Grothoff | Target Version | 1.6 => 1.7 |
| 2026-11-03 07:21 | Status | assigned => implementing | |
| 2026-11-03 14:04 | Christian Grothoff | Implementer | => avalos |
| 2026-11-03 14:04 | Christian Grothoff | Reviewer | => vecirex |
| 2026-11-03 14:04 | Christian Grothoff | Release signed by | => avalos |
| 2026-11-03 14:04 | Christian Grothoff | Deployed by | => avalos |
| 2026-11-03 14:04 | Christian Grothoff | Verifier | => vecirex |
| 2026-11-03 14:04 | Christian Grothoff | Estimated effort (hours) | => 4 |
| 2026-11-11 02:03 | bot-cg | Note Added: 0030400 | |
| 2026-11-11 02:03 | bot-cg | File Added: before-android-1.png | |
| 2026-11-11 02:03 | bot-cg | File Added: before-android-2.png | |
| 2026-11-11 02:03 | bot-cg | File Added: before-ios-1.png | |
| 2026-11-11 02:03 | bot-cg | File Added: before-ios-2.png | |
| 2026-11-11 02:03 | bot-cg | File Added: before-web-1.png | |
| 2026-11-11 02:03 | bot-cg | File Added: after-android-1-in-use.png | |
| 2026-11-11 02:03 | bot-cg | File Added: after-android-2-unused.png | |
| 2026-11-11 02:03 | bot-cg | File Added: android-delete-exchange-explained.patch |