taler-android

Android apps for GNU Taler (wallet, PoS, cashier)
Log | Files | Refs | README | LICENSE

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:
Mwallet/src/main/java/net/taler/wallet/balances/BalanceManager.kt | 15++++++++++-----
Mwallet/src/main/java/net/taler/wallet/balances/Balances.kt | 23+++++++++++++++++++++++
Mwallet/src/main/java/net/taler/wallet/balances/BalancesComposable.kt | 63++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Mwallet/src/main/java/net/taler/wallet/compose/AmountScopeField.kt | 7+++++++
Mwallet/src/main/java/net/taler/wallet/deposit/DepositScreen.kt | 3++-
Mwallet/src/main/java/net/taler/wallet/main/MainScreen.kt | 11+++++++----
Mwallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt | 3++-
Mwallet/src/main/java/net/taler/wallet/transactions/TransactionsComposable.kt | 23++++++++++++++++++++++-
Mwallet/src/main/java/net/taler/wallet/withdraw/PromptWithdrawScreen.kt | 3++-
Mwallet/src/main/proto/user_prefs.proto | 2++
Mwallet/src/main/res/values-de/strings.xml | 1+
Mwallet/src/main/res/values-fr/strings.xml | 1+
Mwallet/src/main/res/values/strings.xml | 1+
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>