commit 4d09e9a362acfb978af4d67a60b793b3d794fd87
parent 558792bdef1db024daecc6019483d57984ea2122
Author: Iván Ávalos <avalos@disroot.org>
Date: Mon, 31 Aug 2026 19:11:35 +0200
[wallet] add support for new exchange-legacy-keys currency scope
Diffstat:
13 files changed, 142 insertions(+), 14 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt b/wallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt
@@ -30,6 +30,7 @@ import net.taler.wallet.main.TAG
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
import net.taler.wallet.backend.WalletResponse
+import net.taler.wallet.balances.ScopeInfo.ExchangeLegacyKeys
import net.taler.wallet.donau.DonauSummaryItem
import net.taler.wallet.exchanges.ExchangeItem
import net.taler.wallet.exchanges.ExchangeManager
@@ -115,18 +116,18 @@ class BalanceManager(
}
@UiThread
- fun getCurrencies() = balances.value?.map { balanceItem ->
+ fun getCurrencies() = spendableBalances().map { balanceItem ->
balanceItem.currency
- } ?: emptyList()
+ }
@UiThread
- fun getScopes(forPeer: Boolean = false) = balances.value?.filter {
+ fun getScopes(forPeer: Boolean = false) = spendableBalances().filter {
!forPeer || !it.disablePeerPayments
- }?.map { it.scopeInfo } ?: emptyList()
+ }.map { it.scopeInfo }
@UiThread
fun hasSufficientBalance(amount: Amount): Boolean {
- balances.value?.forEach { balanceItem ->
+ spendableBalances().forEach { balanceItem ->
if (balanceItem.currency == amount.currency) {
return balanceItem.available >= amount
}
@@ -134,6 +135,10 @@ class BalanceManager(
return false
}
+ private fun spendableBalances() = balances.value
+ ?.filter { it.scopeInfo !is ExchangeLegacyKeys }
+ ?: emptyList()
+
fun addGlobalCurrencyExchange(
currency: String,
exchange: ExchangeItem,
diff --git a/wallet/src/main/java/net/taler/wallet/balances/Balances.kt b/wallet/src/main/java/net/taler/wallet/balances/Balances.kt
@@ -58,16 +58,31 @@ sealed class ScopeInfo {
val url: String,
): ScopeInfo()
+ @Serializable
+ @SerialName("exchange-legacy-keys")
+ data class ExchangeLegacyKeys(
+ override val currency: String,
+ val url: String,
+ val masterPub: String,
+ ): ScopeInfo()
+
fun toPrefs(): PrefsScopeInfo {
val type = when (this) {
is Global -> ScopeInfoType.GLOBAL
is Exchange -> ScopeInfoType.EXCHANGE
is Auditor -> ScopeInfoType.AUDITOR
+ is ExchangeLegacyKeys -> ScopeInfoType.EXCHANGE_LEGACY_KEYS
}
val url = when (this) {
is Exchange -> url
is Auditor -> url
+ is ExchangeLegacyKeys -> url
+ else -> null
+ }
+
+ val masterPub = when (this) {
+ is ExchangeLegacyKeys -> masterPub
else -> null
}
@@ -78,6 +93,8 @@ sealed class ScopeInfo {
.apply {
if (url != null)
setUrl(url)
+ if (masterPub != null)
+ setMasterPub(masterPub)
}.build()
}
@@ -95,6 +112,12 @@ sealed class ScopeInfo {
url = scope.url,
)
+ ScopeInfoType.EXCHANGE_LEGACY_KEYS -> ExchangeLegacyKeys(
+ currency = scope.currency,
+ url = scope.url,
+ masterPub = scope.masterPub,
+ )
+
else -> null
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalancesComposable.kt b/wallet/src/main/java/net/taler/wallet/balances/BalancesComposable.kt
@@ -32,26 +32,38 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.EventRepeat
+import androidx.compose.material.icons.filled.Warning
import androidx.compose.material3.Badge
+import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
+import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Text
+import androidx.compose.material3.TooltipAnchorPosition
+import androidx.compose.material3.TooltipBox
+import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.rememberTooltipState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.launch
import net.taler.common.Amount
import net.taler.wallet.R
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.balances.ScopeInfo.Auditor
import net.taler.wallet.balances.ScopeInfo.Exchange
+import net.taler.wallet.balances.ScopeInfo.ExchangeLegacyKeys
import net.taler.wallet.balances.ScopeInfo.Global
import net.taler.wallet.cleanExchange
import net.taler.wallet.compose.ErrorComposable
@@ -190,6 +202,11 @@ fun BalanceRow(
Text(
balance.available.toString(),
style = MaterialTheme.typography.displaySmall,
+ textDecoration = if (balance.scopeInfo is ExchangeLegacyKeys) {
+ TextDecoration.LineThrough
+ } else {
+ TextDecoration.None
+ },
)
},
overlineContent = {
@@ -209,10 +226,22 @@ fun BalanceRow(
),
)
+ is ExchangeLegacyKeys -> Text(
+ stringResource(
+ R.string.balance_scope_exchange,
+ cleanExchange(balance.scopeInfo.url)
+ ),
+ )
+
else -> {}
}
}
},
+ trailingContent = {
+ if (balance.scopeInfo is ExchangeLegacyKeys) {
+ LegacyBalanceWarningTooltip()
+ }
+ }
)
if (!balance.pendingIncoming.isZero() || !balance.pendingOutgoing.isZero()) {
@@ -223,6 +252,31 @@ fun BalanceRow(
}
}
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun LegacyBalanceWarningTooltip() {
+ val tooltipState = rememberTooltipState()
+ val coroutineScope = rememberCoroutineScope()
+
+ TooltipBox(
+ positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above, 4.dp),
+ tooltip = { PlainTooltip { Text(stringResource(R.string.balances_legacy_unavailable)) } },
+ state = tooltipState,
+ ) {
+ Box(
+ modifier = Modifier
+ .clickable { coroutineScope.launch { tooltipState.show() } },
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(
+ Icons.Default.Warning,
+ tint = MaterialTheme.colorScheme.error,
+ contentDescription = stringResource(R.string.balances_legacy_unavailable),
+ )
+ }
+ }
+}
+
@Composable
fun StatementRow(
summaryItem: DonauSummaryItem,
@@ -331,12 +385,19 @@ fun BalancesComposablePreview() {
disablePeerPayments = false,
),
BalanceItem(
- scopeInfo = Auditor("MXN", "https://auditor.taler.banxico.org.mx"),
+ scopeInfo = Auditor("MXN", "https://auditor.taler.mx"),
available = Amount.fromJSONString("MXN:5.50"),
pendingIncoming = Amount.fromJSONString("MXN:1.40"),
pendingOutgoing = Amount.fromJSONString("MXN:0"),
disablePeerPayments = false,
),
+ BalanceItem(
+ scopeInfo = ExchangeLegacyKeys("KUDOS", "https://legacy.taler.net", masterPub = "123"),
+ available = Amount.fromJSONString("KUDOS:5.50"),
+ pendingIncoming = Amount.fromJSONString("KUDOS:1.40"),
+ pendingOutgoing = Amount.fromJSONString("KUDOS:0"),
+ disablePeerPayments = false,
+ ),
)
val donauSummary = listOf(
diff --git a/wallet/src/main/java/net/taler/wallet/compose/AmountScopeField.kt b/wallet/src/main/java/net/taler/wallet/compose/AmountScopeField.kt
@@ -162,6 +162,7 @@ fun ScopeDropdown(
is ScopeInfo.Global -> scope.currency
is ScopeInfo.Exchange -> cleanExchange(scope.url)
is ScopeInfo.Auditor -> cleanExchange(scope.url)
+ is ScopeInfo.ExchangeLegacyKeys -> cleanExchange(scope.url)
}
val colors = OutlinedTextFieldDefaults.colors()
@@ -242,6 +243,12 @@ fun ScopeDropdown(
s.currency,
cleanExchange(s.url),
)
+
+ is ScopeInfo.ExchangeLegacyKeys -> stringResource(
+ R.string.currency_url,
+ s.currency,
+ cleanExchange(s.url),
+ )
}
)
},
diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositScreen.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositScreen.kt
@@ -33,6 +33,7 @@ import net.taler.wallet.NavigateCallback
import net.taler.wallet.R
import net.taler.wallet.WalletDestination
import net.taler.wallet.accounts.ListBankAccountsResult
+import net.taler.wallet.balances.ScopeInfo.ExchangeLegacyKeys
import net.taler.wallet.compose.ErrorComposable
import net.taler.wallet.compose.GlobalScaffold
import net.taler.wallet.compose.LoadingScreen
@@ -87,7 +88,7 @@ fun DepositScreen(
) { paddingValues ->
val knownBankAccounts by accountManager.bankAccounts.collectAsStateLifecycleAware()
val knownCurrencies by model.balanceManager.balances.map {
- it.map { bl -> bl.currency }
+ it.filter { bl -> bl.scopeInfo !is ExchangeLegacyKeys }.map { bl -> bl.currency }
}.observeAsState(emptyList())
val devMode by model.devMode.observeAsState(false)
diff --git a/wallet/src/main/java/net/taler/wallet/main/MainScreen.kt b/wallet/src/main/java/net/taler/wallet/main/MainScreen.kt
@@ -57,6 +57,7 @@ import net.taler.wallet.WalletDestination
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.balances.BalanceState
import net.taler.wallet.balances.BalancesComposable
+import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.compose.GlobalScaffold
import net.taler.wallet.compose.collectAsStateLifecycleAware
import net.taler.wallet.scan.ScanTab
@@ -332,10 +333,6 @@ fun MainScreen(
)
}
- val disableActions = remember(balanceState, online) {
- !online || (balanceState as? BalanceState.Success)?.balances?.isEmpty() ?: true
- }
-
val selectedScope = (viewMode as? ViewMode.Transactions)?.selectedScope
val selectedBalance = remember(balanceState, selectedScope) {
@@ -345,6 +342,12 @@ fun MainScreen(
}
}
+ val disableActions = remember(balanceState, online) {
+ val hasSpendableBalance = (balanceState as? BalanceState.Success)?.balances
+ ?.any { it.scopeInfo !is ScopeInfo.ExchangeLegacyKeys } ?: false
+ !online || !hasSpendableBalance
+ }
+
TalerActionsModal(
showSheet = showSheet,
sheetState = sheetState,
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt
@@ -36,6 +36,7 @@ import net.taler.wallet.R
import net.taler.wallet.WalletDestination
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.balances.BalanceState
+import net.taler.wallet.balances.ScopeInfo.ExchangeLegacyKeys
import net.taler.wallet.compose.ErrorComposable
import net.taler.wallet.compose.GlobalScaffold
import net.taler.wallet.compose.LoadingScreen
@@ -111,7 +112,7 @@ fun PayTemplateScreen(
is BalanceState.None, is BalanceState.Loading -> LoadingScreen()
is BalanceState.Error -> ErrorComposable(state.error, devMode = devMode)
is BalanceState.Success -> PayTemplateComposable(
- currencies = state.balances.map { it.currency },
+ currencies = state.balances.filter { it.scopeInfo !is ExchangeLegacyKeys }.map { it.currency },
payStatus = payStatus,
devMode = devMode,
onSubmit = { params ->
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsComposable.kt
@@ -59,6 +59,7 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -70,7 +71,9 @@ import net.taler.wallet.R
import net.taler.wallet.backend.TalerErrorCode
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.balances.BalanceItem
+import net.taler.wallet.balances.LegacyBalanceWarningTooltip
import net.taler.wallet.balances.ScopeInfo.Exchange
+import net.taler.wallet.balances.ScopeInfo.ExchangeLegacyKeys
import net.taler.wallet.cleanExchange
import net.taler.wallet.compose.Banner
import net.taler.wallet.compose.LoadingScreen
@@ -243,10 +246,23 @@ fun TransactionsHeader(
.basicMarquee(),
style = MaterialTheme.typography.bodySmall,
)
+ } else if (balance.scopeInfo is ExchangeLegacyKeys) {
+ Text(
+ cleanExchange(balance.scopeInfo.url),
+ modifier = Modifier
+ .padding(top = 3.dp)
+ .basicMarquee(),
+ style = MaterialTheme.typography.bodySmall,
+ )
}
},
trailingContent = {
- Icon(Icons.Default.ArrowDropDown, contentDescription = null)
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ if (balance.scopeInfo is ExchangeLegacyKeys) {
+ LegacyBalanceWarningTooltip()
+ }
+ Icon(Icons.Default.ArrowDropDown, contentDescription = null)
+ }
}
)
}
@@ -265,6 +281,11 @@ fun TransactionsHeader(
balance.available.toString(showSymbol = false),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
+ textDecoration = if (balance.scopeInfo is ExchangeLegacyKeys) {
+ TextDecoration.LineThrough
+ } else {
+ TextDecoration.None
+ },
)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawScreen.kt b/wallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawScreen.kt
@@ -41,6 +41,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import net.taler.wallet.NavigateCallback
+import net.taler.wallet.balances.ScopeInfo.ExchangeLegacyKeys
@Composable
fun PromptWithdrawScreen(
@@ -66,7 +67,7 @@ fun PromptWithdrawScreen(
}
val scopes by balanceManager.balances
- .map { bl -> bl.map { it.scopeInfo } }
+ .map { bl -> bl.filter { it.scopeInfo !is ExchangeLegacyKeys }.map { it.scopeInfo } }
.map { bl ->
val scope = status.amountInfo?.scopeInfo
if (scope != null && !bl.contains(scope)) {
diff --git a/wallet/src/main/proto/user_prefs.proto b/wallet/src/main/proto/user_prefs.proto
@@ -7,12 +7,14 @@ enum ScopeInfoType {
GLOBAL = 0;
EXCHANGE = 1;
AUDITOR = 2;
+ EXCHANGE_LEGACY_KEYS = 3;
}
message PrefsScopeInfo {
ScopeInfoType type = 1;
string currency = 2;
optional string url = 3;
+ optional string masterPub = 4;
}
enum PrefsStateFilter {
diff --git a/wallet/src/main/res/values-de/strings.xml b/wallet/src/main/res/values-de/strings.xml
@@ -245,6 +245,7 @@
<string name="amount_transfer">Betrag</string>
<string name="balances_empty_withdraw_kudos_button">Demo-Geld erhalten</string>
<string name="balances_inbound_amount">+%1$s eingehend</string>
+ <string name="balances_legacy_unavailable">Legacy-Guthaben nicht verfügbar</string>
<string name="balances_outbound_amount">−%1$s abgehend</string>
<string name="transaction_deposit_to">Einzahlung an %1$s</string>
<string name="transaction_peer_push_credit">Erhalten</string>
diff --git a/wallet/src/main/res/values-fr/strings.xml b/wallet/src/main/res/values-fr/strings.xml
@@ -256,6 +256,7 @@
<string name="balance_scope_auditor">Auditeur : %1$s</string>
<string name="balances_empty_withdraw_kudos_message">Obtenez de l\'argent fictif pour découvrir comment payer avec de l\'argent numérique.</string>
<string name="balances_inbound_amount">+%1$s entrant</string>
+ <string name="balances_legacy_unavailable">Solde hérité indisponible</string>
<string name="balances_outbound_amount">−%1$s sortant</string>
<string name="exchange_delete_force">Forcer la suppression (purge)</string>
<string name="payment_template_error">Erreur de création de la commande</string>
diff --git a/wallet/src/main/res/values/strings.xml b/wallet/src/main/res/values/strings.xml
@@ -158,6 +158,7 @@ GNU Taler is immune to many types of fraud such as credit card data theft, phish
<string name="balances_empty_withdraw_kudos_button">Get demo cash</string>
<string name="balances_empty_withdraw_kudos_message">Get demo cash to experience how to pay with digital cash.</string>
<string name="balances_inbound_amount">+%1$s incoming</string>
+ <string name="balances_legacy_unavailable">Legacy balance unavailable</string>
<!-- must be minus sign! (− U+2212) -->
<string name="balances_outbound_amount">−%1$s outgoing</string>
<string name="balances_section_statements">Donation statements</string>