taler-android

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

commit 77c51c3919bc25de82cd1d6154dae97083e948cc
parent 3b44ea0e5ed210a8abf832301c3e658b63babcf0
Author: Iván Ávalos <avalos@disroot.org>
Date:   Thu, 22 Oct 2026 14:09:47 +0200

[wallet] implement single source of truth for currency spec

Diffstat:
Mtaler-kotlin-android/src/main/java/net/taler/common/Amount.kt | 13+++++++++----
Mtaler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt | 47+++++++++++++++++++++++++++++++++++++++++++++++
Ataler-kotlin-android/src/test/java/net/taler/common/ContractTermsSpecTest.kt | 83+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mwallet/src/main/java/net/taler/wallet/balances/BalancesPagerComposable.kt | 1-
Dwallet/src/main/java/net/taler/wallet/compose/CurrencySpec.kt | 53-----------------------------------------------------
Mwallet/src/main/java/net/taler/wallet/deposit/DepositAmountComposable.kt | 8++++----
Mwallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt | 26+++++++++++++++++++-------
Mwallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt | 9+++------
Mwallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt | 5+++++
Mwallet/src/main/java/net/taler/wallet/main/MainViewModel.kt | 16++--------------
Mwallet/src/main/java/net/taler/wallet/main/ViewMode.kt | 2--
Mwallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt | 58+++++++++++++++++++++++++++++++++++++++++++++-------------
Mwallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt | 9+++------
Mwallet/src/main/java/net/taler/wallet/peer/PeerManager.kt | 38+++++++++++++++++++++++++++++---------
Mwallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt | 11++++-------
Mwallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt | 10++++------
Mwallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt | 9+++------
Mwallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt | 11++++-------
Mwallet/src/main/java/net/taler/wallet/refund/TransactionRefundComposable.kt | 10++++------
Mwallet/src/main/java/net/taler/wallet/transactions/TransactionDetailScreen.kt | 27+++++++--------------------
Mwallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt | 100++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Mwallet/src/main/java/net/taler/wallet/transactions/Transactions.kt | 33+++++++++++++++++++--------------
Mwallet/src/main/java/net/taler/wallet/transactions/TransactionsComposable.kt | 8+++-----
Mwallet/src/main/java/net/taler/wallet/transfer/ScreenTransfer.kt | 10+++-------
Mwallet/src/main/java/net/taler/wallet/transfer/TransferBitcoin.kt | 3++-
Mwallet/src/main/java/net/taler/wallet/transfer/WireTransferDetailsScreen.kt | 10----------
Mwallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt | 9+++------
Mwallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt | 4++--
Mwallet/src/main/java/net/taler/wallet/withdraw/WithdrawalShowInfo.kt | 3++-
29 files changed, 403 insertions(+), 223 deletions(-)

diff --git a/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt b/taler-kotlin-android/src/main/java/net/taler/common/Amount.kt @@ -173,19 +173,24 @@ data class Amount( if (resultValue > MAX_VALUE) throw AmountOverflowException() val resultFraction = (fraction + other.fraction) % FRACTIONAL_BASE - return Amount(currency, resultValue, resultFraction) + return Amount(currency, resultValue, resultFraction, spec ?: other.spec) } operator fun times(factor: Int): Amount { // TODO consider replacing with a faster implementation - if (factor == 0) return zero(currency) + if (factor == 0) return zero(currency).withSpec(spec) var result = this for (i in 1 until factor) result += this return result } fun withCurrency(currency: String): Amount { - return Amount(checkCurrency(currency), this.value, this.fraction) + return Amount( + checkCurrency(currency), + this.value, + this.fraction, + if (currency == this.currency) spec else null, + ) } fun withSpec(spec: CurrencySpecification?) = copy(spec = spec) @@ -205,7 +210,7 @@ data class Amount( if (resultValue < other.value) throw AmountOverflowException() resultValue -= other.value - return Amount(currency, resultValue, resultFraction) + return Amount(currency, resultValue, resultFraction, spec ?: other.spec) } fun isZero(): Boolean { diff --git a/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt b/taler-kotlin-android/src/main/java/net/taler/common/ContractTerms.kt @@ -301,3 +301,50 @@ object ContractTermsSerializer : JsonContentPolymorphicSerializer<ContractTerms> } } } + +/** + * Attaches a currency specification to all amounts contained in these terms. + * The resolver is called with each amount's own currency, so multi-currency + * V1 choices are handled correctly. + */ +fun ContractTerms.withSpec( + specForCurrency: (currency: String) -> CurrencySpecification?, +): ContractTerms = when (this) { + is ContractTerms.V0 -> copy( + amount = amount.withSpec(specForCurrency(amount.currency)), + maxFee = maxFee.withSpec(specForCurrency(maxFee.currency)), + products = products?.map { it.withSpec(specForCurrency) }, + ) + + is ContractTerms.V1 -> copy( + choices = choices.map { it.withSpec(specForCurrency) }, + products = products?.map { it.withSpec(specForCurrency) }, + ) +} + +private fun ContractProduct.withSpec( + specForCurrency: (currency: String) -> CurrencySpecification?, +) = copy( + price = price?.let { it.withSpec(specForCurrency(it.currency)) }, + taxes = taxes?.map { + it.copy(tax = it.tax.withSpec(specForCurrency(it.tax.currency))) + }?.toSet(), +) + +private fun ContractChoice.withSpec( + specForCurrency: (currency: String) -> CurrencySpecification?, +) = copy( + amount = amount.withSpec(specForCurrency(amount.currency)), + maxFee = maxFee.withSpec(specForCurrency(maxFee.currency)), + outputs = outputs.map { output -> + if (output is ContractOutput.TaxReceipt) { + output.copy( + amount = output.amount.withSpec( + specForCurrency(output.amount.currency), + ), + ) + } else { + output + } + }, +) diff --git a/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsSpecTest.kt b/taler-kotlin-android/src/test/java/net/taler/common/ContractTermsSpecTest.kt @@ -0,0 +1,83 @@ +/* + * 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.common + +import org.junit.Assert.assertEquals +import org.junit.Test + +class ContractTermsSpecTest { + + @Test + fun multiCurrencyChoicesUseTheirOwnCurrencySpec() { + val eur = CurrencySpecification( + name = "Euro", + numFractionalInputDigits = 2, + numFractionalNormalDigits = 2, + numFractionalTrailingZeroDigits = 2, + altUnitNames = mapOf(0 to "€"), + ) + val usd = CurrencySpecification( + name = "US Dollar", + numFractionalInputDigits = 2, + numFractionalNormalDigits = 2, + numFractionalTrailingZeroDigits = 2, + altUnitNames = mapOf(0 to "$"), + ) + val terms = ContractTerms.V1( + summary = "Order", + orderId = "order-1", + merchantBaseUrl = "https://merchant.example/", + merchant = Merchant(name = "Merchant"), + exchanges = listOf(Exchange("https://exchange.example/")), + products = listOf( + ContractProduct( + description = "EUR product", + price = Amount.fromJSONString("EUR:3"), + ), + ContractProduct( + description = "USD product", + price = Amount.fromJSONString("USD:4"), + ), + ), + choices = listOf( + ContractChoice( + amount = Amount.fromJSONString("EUR:10"), + inputs = emptyList(), + outputs = emptyList(), + maxFee = Amount.fromJSONString("EUR:0.1"), + ), + ContractChoice( + amount = Amount.fromJSONString("USD:20"), + inputs = emptyList(), + outputs = emptyList(), + maxFee = Amount.fromJSONString("USD:0.2"), + ), + ), + tokenFamilies = emptyMap(), + ) + + val specs = mapOf("EUR" to eur, "USD" to usd) + val enriched = terms.withSpec { specs[it] } as ContractTerms.V1 + + assertEquals(eur, enriched.choices[0].amount.spec) + assertEquals(eur, enriched.choices[0].maxFee.spec) + assertEquals(usd, enriched.choices[1].amount.spec) + assertEquals(usd, enriched.choices[1].maxFee.spec) + assertEquals(eur, enriched.products!![0].price!!.spec) + assertEquals(usd, enriched.products!![1].price!!.spec) + } +} diff --git a/wallet/src/main/java/net/taler/wallet/balances/BalancesPagerComposable.kt b/wallet/src/main/java/net/taler/wallet/balances/BalancesPagerComposable.kt @@ -609,7 +609,6 @@ fun BalanceRecentTransactions( } else res.transactions.forEach { tx -> TransactionRow( tx = tx, - spec = balance.available.spec, isSelected = false, selectionMode = false, onTransactionClick = { onTransactionClicked(tx) }, diff --git a/wallet/src/main/java/net/taler/wallet/compose/CurrencySpec.kt b/wallet/src/main/java/net/taler/wallet/compose/CurrencySpec.kt @@ -1,53 +0,0 @@ -/* - * 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.compose - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState -import net.taler.common.CurrencySpecification -import net.taler.wallet.balances.ScopeInfo - -@Composable -fun rememberCurrencySpec( - currency: String, - scopes: List<ScopeInfo>, - getSpec: suspend (String, List<ScopeInfo>) -> CurrencySpecification?, -): CurrencySpecification? { - val spec by produceState<CurrencySpecification?>( - initialValue = null, - key1 = currency, - key2 = scopes, - ) { - value = getSpec(currency, scopes) - } - return spec -} - -@Composable -fun rememberCurrencySpec( - scope: ScopeInfo, - getSpec: suspend (ScopeInfo) -> CurrencySpecification?, -): CurrencySpecification? { - val spec by produceState<CurrencySpecification?>( - initialValue = null, - key1 = scope, - ) { - value = getSpec(scope) - } - return spec -} diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositAmountComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositAmountComposable.kt @@ -152,12 +152,12 @@ fun DepositAmountComposable( text = if (maxDepositable.effectiveAmount == it) { stringResource( R.string.amount_available_transfer, - it.withSpec(spec), + it.withSpec(it.spec ?: spec), ) } else { stringResource( R.string.amount_available_transfer_fees, - it.withSpec(spec), + it.withSpec(it.spec ?: spec), ) }, ) @@ -201,13 +201,13 @@ fun DepositAmountComposable( TransactionAmountComposable( label = stringResource(R.string.amount_fee), - amount = fee.withSpec(amount?.spec), + amount = fee.withSpec(fee.spec ?: amount?.spec), amountType = Negative, ) TransactionAmountComposable( label = stringResource(R.string.amount_send), - amount = effectiveAmount.withSpec(amount?.spec), + amount = effectiveAmount.withSpec(effectiveAmount.spec ?: amount?.spec), amountType = Positive, ) } diff --git a/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt @@ -27,6 +27,7 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import net.taler.common.Amount +import net.taler.common.CurrencySpecification import net.taler.wallet.main.TAG import net.taler.wallet.accounts.KnownBankAccountInfo import net.taler.wallet.accounts.PaytoUriBitcoin @@ -38,6 +39,7 @@ import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.backend.WalletResponse import net.taler.wallet.balances.BalanceManager import net.taler.wallet.balances.ScopeInfo +import net.taler.wallet.exchanges.ExchangeManager import org.json.JSONObject import androidx.core.net.toUri @@ -45,11 +47,16 @@ class DepositManager( private val api: WalletBackendApi, private val scope: CoroutineScope, private val balanceManager: BalanceManager, + private val exchangeManager: ExchangeManager, ) { private val mDepositState = MutableStateFlow<DepositState>(DepositState.Start) internal val depositState = mDepositState.asStateFlow() + private suspend fun getCurrencySpec(currency: String): CurrencySpecification? = + exchangeManager.getCachedSpecForCurrency(currency) + ?: exchangeManager.getSpecForScopeInfo(ScopeInfo.Global(currency)) + fun isSupportedPayToUri(uriString: String): Boolean { if (!uriString.startsWith("payto://")) return false val u = uriString.toUri() @@ -65,6 +72,7 @@ class DepositManager( } suspend fun checkDepositFees(paytoUri: String, amount: Amount): CheckDepositResult { + val spec = getCurrencySpec(amount.currency) var response: CheckDepositResult = CheckDepositResult.None when (val res = api.request("checkDeposit", CheckDepositResponse.serializer()) { put("depositPaytoUri", paytoUri) @@ -76,11 +84,11 @@ class DepositManager( res.error.extra["insufficientBalanceDetails"]?.let { details -> val maxAmountRaw = details.jsonObject["balanceAvailable"]?.let { amount -> Amount.fromJSONString(amount.jsonPrimitive.content) - } + }?.withSpec(spec) val maxAmountEffective = details.jsonObject["maxEffectiveSpendAmount"]?.let { amount -> Amount.fromJSONString(amount.jsonPrimitive.content) - } ?: maxAmountRaw + }?.withSpec(spec) ?: maxAmountRaw response = CheckDepositResult.InsufficientBalance( maxAmountEffective = maxAmountEffective, @@ -99,10 +107,10 @@ class DepositManager( ) } else { CheckDepositResult.Success( - totalDepositCost = res.result.totalDepositCost, - effectiveDepositAmount = res.result.effectiveDepositAmount, - kycSoftLimit = res.result.kycSoftLimit, - kycHardLimit = res.result.kycHardLimit, + totalDepositCost = res.result.totalDepositCost.withSpec(spec), + effectiveDepositAmount = res.result.effectiveDepositAmount.withSpec(spec), + kycSoftLimit = res.result.kycSoftLimit?.withSpec(spec), + kycHardLimit = res.result.kycHardLimit?.withSpec(spec), kycExchanges = res.result.kycExchanges, ) } @@ -123,7 +131,11 @@ class DepositManager( }.onError { error -> Log.e(TAG, "Error getMaxDepositAmount $error") }.onSuccess { - response = it + val spec = getCurrencySpec(currency) + response = it.copy( + effectiveAmount = it.effectiveAmount.withSpec(spec), + rawAmount = it.rawAmount.withSpec(spec), + ) } return response diff --git a/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt b/wallet/src/main/java/net/taler/wallet/deposit/TransactionDepositComposable.kt @@ -39,7 +39,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import net.taler.common.Amount -import net.taler.common.CurrencySpecification import net.taler.common.Timestamp import net.taler.lib.android.toAbsoluteTime import net.taler.wallet.BottomInsetsSpacer @@ -68,7 +67,6 @@ import net.taler.wallet.withdraw.TransferOptionChooser fun TransactionDepositComposable( t: TransactionDeposit, devMode: Boolean, - spec: CurrencySpecification?, onSelectOption: (option: TransferOption?) -> Unit, onWireTransfer: () -> Unit, onShowQrCodes: () -> Unit, @@ -123,7 +121,7 @@ fun TransactionDepositComposable( TransactionAmountComposable( label = stringResource(id = R.string.amount_chosen), - amount = t.amountRaw.withSpec(spec), + amount = t.amountRaw, amountType = AmountType.Neutral, ) @@ -131,14 +129,14 @@ fun TransactionDepositComposable( val fee = t.amountEffective - t.amountRaw TransactionAmountComposable( label = stringResource(id = R.string.amount_fee), - amount = fee.withSpec(spec), + amount = fee, amountType = AmountType.Negative, ) } TransactionAmountComposable( label = stringResource(id = R.string.amount_sent), - amount = t.amountEffective.withSpec(spec), + amount = t.amountEffective, amountType = AmountType.Negative, ) @@ -173,7 +171,6 @@ fun TransactionDepositComposablePreview() { TransactionDepositComposable( t = t, devMode = true, - spec = null, onSelectOption = {}, onConfirmKyc = {}, onWireTransfer = {}, diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt @@ -292,6 +292,11 @@ class ExchangeManager( @Deprecated("Please find spec via scopeInfo instead", ReplaceWith("getSpecForScopeInfo")) fun getSpecForCurrency(currency: String): CurrencySpecification? { + return getCachedSpecForCurrency(currency) + } + + /** Returns a spec for [currency] if one is already cached, without fetching. */ + fun getCachedSpecForCurrency(currency: String): CurrencySpecification? { return currencySpecs.keys.firstOrNull { it.currency == currency }?.let { currencySpecs[it] } diff --git a/wallet/src/main/java/net/taler/wallet/main/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/main/MainViewModel.kt @@ -120,13 +120,13 @@ class MainViewModel( val exchangeManager: ExchangeManager = ExchangeManager(api, viewModelScope) val balanceManager = BalanceManager(api, viewModelScope, exchangeManager) val paymentManager = PaymentManager(api, viewModelScope, exchangeManager) - val transactionManager: TransactionManager = TransactionManager(api, viewModelScope) + val transactionManager: TransactionManager = TransactionManager(api, viewModelScope, exchangeManager) val refundManager = RefundManager(api, viewModelScope) val withdrawManager = WithdrawManager(api, viewModelScope, exchangeManager, transactionManager) val peerManager: PeerManager = PeerManager(api, exchangeManager, viewModelScope) val settingsManager: SettingsManager = SettingsManager(app.applicationContext, api, viewModelScope, balanceManager) val accountManager: AccountManager = AccountManager(api, viewModelScope) - val depositManager: DepositManager = DepositManager(api, viewModelScope, balanceManager) + val depositManager: DepositManager = DepositManager(api, viewModelScope, balanceManager, exchangeManager) val tokenManager: TokenManager = TokenManager(api) val donauManager: DonauManager = DonauManager(api, viewModelScope, exchangeManager) val contactManager: ContactManager = ContactManager(api, viewModelScope) @@ -254,18 +254,6 @@ class MainViewModel( // change is persisted immediately. if (current.selectedScope == newMode.selectedScope) return mViewMode.value = newMode - if (newMode.selectedScope != null) { - viewModelScope.launch { - val spec = exchangeManager.getCurrencySpecification(newMode.selectedScope) - mViewMode.getAndUpdate { vm -> - if (vm.selectedScope == newMode.selectedScope) { - vm.copy(selectedSpec = spec) - } else { - vm - } - } - } - } settingsManager.saveViewMode(newMode) } diff --git a/wallet/src/main/java/net/taler/wallet/main/ViewMode.kt b/wallet/src/main/java/net/taler/wallet/main/ViewMode.kt @@ -16,13 +16,11 @@ package net.taler.wallet.main -import net.taler.common.CurrencySpecification import net.taler.wallet.PrefsViewMode2 import net.taler.wallet.balances.ScopeInfo data class ViewMode( val selectedScope: ScopeInfo? = null, - val selectedSpec: CurrencySpecification? = null, ) { fun toPrefs(): PrefsViewMode2 = PrefsViewMode2 .newBuilder() diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt @@ -28,7 +28,9 @@ import net.taler.common.Amount import net.taler.common.ContractInput import net.taler.common.ContractOutput import net.taler.common.ContractTerms +import net.taler.common.CurrencySpecification import net.taler.common.TalerUtils.getLocalizedString +import net.taler.common.withSpec import net.taler.wallet.main.TAG import net.taler.wallet.backend.BackendManager import net.taler.wallet.backend.NotificationPayload @@ -211,21 +213,20 @@ class PaymentManager( return@startNewAttempt } + val contractSpecs = mutableMapOf<String, CurrencySpecification?>() + for (currency in res.contractTerms.amountCurrencies()) { + contractSpecs[currency] = res.contractTerms.specFor(currency) + } + val contractTerms = res.contractTerms.withSpec { currency -> + contractSpecs[currency] + } mPayStatus.value = PayStatus.Choices( transactionId = transactionId, - contractTerms = res.contractTerms, + contractTerms = contractTerms, defaultChoiceIndex = res.defaultChoiceIndex, choices = res.choices.map { choice -> - val spec = exchangeManager.getSpecForCurrency( - choice.amountRaw.currency, - res.contractTerms.exchanges.map { - ScopeInfo.Exchange(choice.amountRaw.currency, it.url) - }, - ) ?: res.contractTerms.exchanges.firstOrNull()?.let { - exchangeManager.getSpecForScopeInfo( - ScopeInfo.Exchange(choice.amountRaw.currency, it.url) - ) - } + val spec = contractSpecs[choice.amountRaw.currency] + ?: contractTerms.specFor(choice.amountRaw.currency) when (choice) { is PaymentPossible -> { @@ -245,9 +246,9 @@ class PaymentManager( description = choice.description, descriptionI18n = choice.descriptionI18n, amountRaw = choice.amountRaw, - inputs = (res.contractTerms as? ContractTerms.V1) + inputs = (contractTerms as? ContractTerms.V1) ?.choices?.get(i)?.inputs ?: listOf(), - outputs = (res.contractTerms as? ContractTerms.V1) + outputs = (contractTerms as? ContractTerms.V1) ?.choices?.get(i)?.outputs ?: listOf(), details = choice, ) @@ -399,6 +400,37 @@ class PaymentManager( mPayStatus.postValue(PayStatus.Error(error = errorInfo, transactionId = transactionId)) } + private suspend fun ContractTerms.specFor(currency: String): CurrencySpecification? { + val scopes = exchanges.map { ScopeInfo.Exchange(currency, it.url) } + return exchangeManager.getSpecForCurrency(currency, scopes) + ?: exchangeManager.getSpecForScopeInfo(ScopeInfo.Global(currency)) + } + + private fun ContractTerms.amountCurrencies(): Set<String> = buildSet { + when (this@amountCurrencies) { + is ContractTerms.V0 -> { + add(amount.currency) + add(maxFee.currency) + } + + is ContractTerms.V1 -> choices.forEach { + add(it.amount.currency) + add(it.maxFee.currency) + } + } + + products?.forEach { product -> + product.price?.let { add(it.currency) } + product.taxes?.forEach { add(it.tax.currency) } + } + + if (this@amountCurrencies is ContractTerms.V1) { + choices.flatMap { it.outputs } + .filterIsInstance<ContractOutput.TaxReceipt>() + .forEach { add(it.amount.currency) } + } + } + private fun handleError(operation: String, error: TalerErrorInfo, transactionId: String? = null) { Log.e(TAG, "got $operation error result $error") mPayStatus.postValue(PayStatus.Error(error = error, transactionId = transactionId)) diff --git a/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import net.taler.common.Amount -import net.taler.common.CurrencySpecification import net.taler.common.Merchant import net.taler.common.Timestamp import net.taler.lib.android.toAbsoluteTime @@ -76,7 +75,6 @@ fun TransactionPaymentComposable( t: TransactionPayment, payStatus: PayStatus, devMode: Boolean, - spec: CurrencySpecification?, promptMode: Boolean = false, modifier: Modifier = Modifier, onFulfill: (url: String) -> Unit, @@ -121,7 +119,7 @@ fun TransactionPaymentComposable( TransactionAmountComposable( label = stringResource(id = R.string.transaction_order_total), - amount = t.amountRaw.withSpec(spec), + amount = t.amountRaw, amountType = AmountType.Neutral, ) @@ -129,14 +127,14 @@ fun TransactionPaymentComposable( val fee = t.amountEffective - t.amountRaw TransactionAmountComposable( label = stringResource(id = R.string.amount_fee), - amount = fee.withSpec(spec), + amount = fee, amountType = AmountType.Negative, ) } TransactionAmountComposable( label = stringResource(id = R.string.transaction_paid), - amount = t.amountEffective.withSpec(spec), + amount = t.amountEffective, amountType = AmountType.Negative, ) @@ -293,7 +291,6 @@ fun TransactionPaymentComposablePreview() { t = t, payStatus = PayStatus.None, devMode = true, - spec = null, onFulfill = {}, onTransition = {}, onConfirmPay = { _, _ ->}, diff --git a/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt b/wallet/src/main/java/net/taler/wallet/peer/PeerManager.kt @@ -27,6 +27,7 @@ import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import net.taler.common.Amount +import net.taler.common.CurrencySpecification import net.taler.common.RelativeTime import net.taler.common.Timestamp import net.taler.wallet.main.TAG @@ -98,6 +99,10 @@ class PeerManager( private val _incomingPushState = MutableStateFlow<IncomingState>(IncomingChecking) val incomingPushState: StateFlow<IncomingState> = _incomingPushState + private suspend fun getSpecForCurrency(currency: String): CurrencySpecification? = + exchangeManager.getCachedSpecForCurrency(currency) + ?: exchangeManager.getSpecForScopeInfo(ScopeInfo.Global(currency)) + suspend fun checkPeerPullCredit( amount: Amount, scopeInfo: ScopeInfo, @@ -272,10 +277,13 @@ class PeerManager( transactionId?.let { put("transactionId", transactionId) } this }.onSuccess { response -> + val spec = getSpecForCurrency(response.amountRaw.currency) _incomingPullState.value = IncomingTerms( - amountRaw = response.amountRaw, - amountEffective = response.amountEffective, - contractTerms = response.contractTerms, + amountRaw = response.amountRaw.withSpec(spec), + amountEffective = response.amountEffective.withSpec(spec), + contractTerms = response.contractTerms.copy( + amount = response.contractTerms.amount.withSpec(spec), + ), id = response.transactionId, ) }.onError { error -> @@ -323,18 +331,30 @@ class PeerManager( return@b } + val spec = exchangeManager.getSpecForScopeInfo( + exchange.scopeInfo ?: ScopeInfo.Exchange( + response.amountRaw.currency, + response.exchangeBaseUrl, + ), + ) + val amountRaw = response.amountRaw.withSpec(spec) + val amountEffective = response.amountEffective.withSpec(spec) + val contractTerms = response.contractTerms.copy( + amount = response.contractTerms.amount.withSpec(spec), + ) + _incomingPushState.value = if (exchange.tosStatus.isAccepted()) { IncomingTerms( - amountRaw = response.amountRaw, - amountEffective = response.amountEffective, - contractTerms = response.contractTerms, + amountRaw = amountRaw, + amountEffective = amountEffective, + contractTerms = contractTerms, id = response.transactionId, ) } else { IncomingTosReview( - amountRaw = response.amountRaw, - amountEffective = response.amountEffective, - contractTerms = response.contractTerms, + amountRaw = amountRaw, + amountEffective = amountEffective, + contractTerms = contractTerms, exchangeBaseUrl = response.exchangeBaseUrl, id = response.transactionId, ) diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullCredit.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import net.taler.common.Amount -import net.taler.common.CurrencySpecification import net.taler.common.Timestamp import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED @@ -48,7 +47,6 @@ import net.taler.wallet.transactions.TransactionState @Composable fun ColumnScope.TransactionPeerPullCreditComposable( t: TransactionPeerPullCredit, - spec: CurrencySpecification?, devMode: Boolean, onConfirmKyc: (url: String) -> Unit, onForward: (uri: String) -> Unit, @@ -57,7 +55,7 @@ fun ColumnScope.TransactionPeerPullCreditComposable( if (t.error == null) PeerQrCode( state = t.txState, - amount = t.amountRaw.withSpec(spec), + amount = t.amountRaw, talerUri = t.talerUri, instructionResId = R.string.receive_peer_payment_instruction, devMode = devMode, @@ -66,7 +64,7 @@ fun ColumnScope.TransactionPeerPullCreditComposable( TransactionAmountComposable( label = stringResource(id = R.string.amount_invoiced), - amount = t.amountRaw.withSpec(spec), + amount = t.amountRaw, amountType = AmountType.Neutral, ) @@ -74,7 +72,7 @@ fun ColumnScope.TransactionPeerPullCreditComposable( val fee = t.amountRaw - t.amountEffective TransactionAmountComposable( label = stringResource(id = R.string.amount_fee), - amount = fee.withSpec(spec), + amount = fee, amountType = AmountType.Negative, ) } @@ -85,7 +83,7 @@ fun ColumnScope.TransactionPeerPullCreditComposable( } else { stringResource(id = R.string.amount_receive) }, - amount = t.amountEffective.withSpec(spec), + amount = t.amountEffective, amountType = AmountType.Positive, ) @@ -121,7 +119,6 @@ fun TransactionPeerPullCreditPreview(loading: Boolean = false) { TransactionPeerComposable( t = t, devMode = true, - spec = null, onConfirmKyc = {}, onForward = {}, onTransition = {}, diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPullDebit.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import net.taler.common.Amount -import net.taler.common.CurrencySpecification import net.taler.common.Timestamp import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED @@ -40,10 +39,10 @@ import net.taler.wallet.transactions.TransactionPeerPullDebit import net.taler.wallet.transactions.TransactionState @Composable -fun TransactionPeerPullDebitComposable(t: TransactionPeerPullDebit, spec: CurrencySpecification?) { +fun TransactionPeerPullDebitComposable(t: TransactionPeerPullDebit) { TransactionAmountComposable( label = stringResource(id = R.string.transaction_order_total), - amount = t.amountRaw.withSpec(spec), + amount = t.amountRaw, amountType = AmountType.Neutral, ) @@ -51,14 +50,14 @@ fun TransactionPeerPullDebitComposable(t: TransactionPeerPullDebit, spec: Curren val fee = t.amountEffective - t.amountRaw TransactionAmountComposable( label = stringResource(id = R.string.amount_fee), - amount = fee.withSpec(spec), + amount = fee, amountType = AmountType.Negative, ) } TransactionAmountComposable( label = stringResource(id = R.string.transaction_paid), - amount = t.amountEffective.withSpec(spec), + amount = t.amountEffective, amountType = AmountType.Negative, ) @@ -93,7 +92,6 @@ fun TransactionPeerPullDebitPreview() { TransactionPeerComposable( t = t, devMode = true, - spec = null, onConfirmKyc = {}, onForward = {}, onTransition = {}, diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushCredit.kt @@ -22,7 +22,6 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import net.taler.common.Amount -import net.taler.common.CurrencySpecification import net.taler.common.Timestamp import net.taler.wallet.R import net.taler.wallet.backend.TalerErrorCode.EXCHANGE_GENERIC_KYC_REQUIRED @@ -44,14 +43,13 @@ import net.taler.wallet.transactions.TransactionState @Composable fun ColumnScope.TransactionPeerPushCreditComposable( t: TransactionPeerPushCredit, - spec: CurrencySpecification?, onConfirmKyc: (url: String) -> Unit, ) { PeerActions(t, onConfirmKyc) TransactionAmountComposable( label = stringResource(id = R.string.amount_sent), - amount = t.amountRaw.withSpec(spec), + amount = t.amountRaw, amountType = AmountType.Neutral, ) @@ -59,14 +57,14 @@ fun ColumnScope.TransactionPeerPushCreditComposable( val fee = t.amountRaw - t.amountEffective TransactionAmountComposable( label = stringResource(id = R.string.amount_fee), - amount = fee.withSpec(spec), + amount = fee, amountType = AmountType.Negative, ) } TransactionAmountComposable( label = stringResource(id = R.string.amount_received), - amount = t.amountEffective.withSpec(spec), + amount = t.amountEffective, amountType = AmountType.Positive, ) @@ -101,7 +99,6 @@ fun TransactionPeerPushCreditPreview() { TransactionPeerComposable( t = t, devMode = true, - spec = null, onConfirmKyc = {}, onForward = {}, onTransition = {}, diff --git a/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt b/wallet/src/main/java/net/taler/wallet/peer/TransactionPeerPushDebit.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import net.taler.common.Amount -import net.taler.common.CurrencySpecification import net.taler.common.Timestamp import net.taler.wallet.R import net.taler.wallet.balances.ScopeInfo @@ -59,13 +58,12 @@ import net.taler.wallet.transactions.TransactionState @Composable fun ColumnScope.TransactionPeerPushDebitComposable( t: TransactionPeerPushDebit, - spec: CurrencySpecification?, devMode: Boolean, onForward: ((String) -> Unit)? = null, ) { if (t.error == null) PeerQrCode( state = t.txState, - amount = t.amountRaw.withSpec(spec), + amount = t.amountRaw, talerUri = t.talerUri, instructionResId = R.string.send_peer_payment_instruction, devMode = devMode, @@ -74,7 +72,7 @@ fun ColumnScope.TransactionPeerPushDebitComposable( TransactionAmountComposable( label = stringResource(id = R.string.transaction_order_total), - amount = t.amountRaw.withSpec(spec), + amount = t.amountRaw, amountType = AmountType.Neutral, ) @@ -82,7 +80,7 @@ fun ColumnScope.TransactionPeerPushDebitComposable( val fee = t.amountEffective - t.amountRaw TransactionAmountComposable( label = stringResource(id = R.string.amount_fee), - amount = fee.withSpec(spec), + amount = fee, amountType = AmountType.Negative, ) } @@ -93,7 +91,7 @@ fun ColumnScope.TransactionPeerPushDebitComposable( } else { stringResource(R.string.amount_send) }, - amount = t.amountEffective.withSpec(spec), + amount = t.amountEffective, amountType = AmountType.Negative, ) @@ -175,7 +173,6 @@ fun TransactionPeerPushDebitPreview(loading: Boolean = false) { TransactionPeerComposable( t = t, devMode = true, - spec = null, onConfirmKyc = {}, onForward = {}, onTransition = {}, diff --git a/wallet/src/main/java/net/taler/wallet/refund/TransactionRefundComposable.kt b/wallet/src/main/java/net/taler/wallet/refund/TransactionRefundComposable.kt @@ -31,7 +31,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import net.taler.common.Amount -import net.taler.common.CurrencySpecification import net.taler.common.Timestamp import net.taler.lib.android.toAbsoluteTime import net.taler.wallet.BottomInsetsSpacer @@ -58,7 +57,6 @@ import net.taler.wallet.transactions.TransitionsComposable fun TransactionRefundComposable( t: TransactionRefund, devMode: Boolean, - spec: CurrencySpecification?, onTransition: (t: TransactionAction) -> Unit, modifier: Modifier = Modifier, ) { @@ -79,13 +77,13 @@ fun TransactionRefundComposable( TransactionAmountComposable( label = stringResource(id = R.string.transaction_refund), - amount = t.amountEffective.withSpec(spec), + amount = t.amountEffective, amountType = AmountType.Positive, ) TransactionAmountComposable( label = stringResource(id = R.string.transaction_order_total), - amount = t.amountRaw.withSpec(spec), + amount = t.amountRaw, amountType = AmountType.Neutral, ) @@ -93,7 +91,7 @@ fun TransactionRefundComposable( val fee = t.amountRaw - t.amountEffective TransactionAmountComposable( label = stringResource(id = R.string.amount_fee), - amount = fee.withSpec(spec), + amount = fee, amountType = AmountType.Negative, ) } @@ -135,6 +133,6 @@ fun TransactionRefundComposablePreview() { )) ) TalerSurface { - TransactionRefundComposable(t = t, devMode = true, spec = null, onTransition = {}) + TransactionRefundComposable(t = t, devMode = true, onTransition = {}) } } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailScreen.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailScreen.kt @@ -59,7 +59,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import net.taler.common.Amount -import net.taler.common.CurrencySpecification import net.taler.common.Timestamp import net.taler.lib.android.copyToClipBoard import net.taler.lib.android.toAbsoluteTime @@ -74,7 +73,6 @@ import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.compose.GlobalScaffold import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.collectAsStateLifecycleAware -import net.taler.wallet.compose.rememberCurrencySpec import net.taler.wallet.deposit.TransactionDepositComposable import net.taler.wallet.launchInAppBrowser import net.taler.wallet.main.MainViewModel @@ -98,7 +96,6 @@ fun TransactionDetailScreen( onNavigateBack: () -> Unit, ) { val transactionManager = model.transactionManager - val exchangeManager = model.exchangeManager val devMode by model.devMode.observeAsState(false) val context = LocalContext.current var keepSelectedTx: Boolean by remember { mutableStateOf(false) } @@ -212,7 +209,6 @@ fun TransactionDetailScreen( .observeAsState(PayStatus.None).value, devMode = devMode, promptMode = destination.promptMode, - spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency), modifier = modifier, onFulfill = { url -> launchInAppBrowser(context, url) @@ -243,7 +239,6 @@ fun TransactionDetailScreen( modifier = modifier, t = tx, devMode = devMode, - spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency), onSelectOption = { it?.let { transactionManager.selectTransferOption(it) } }, onConfirmKyc = { url -> launchInAppBrowser(context, url) @@ -276,7 +271,6 @@ fun TransactionDetailScreen( modifier = modifier, t = tx, devMode = devMode, - spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency), onSelectOption = { it?.let { transactionManager.selectTransferOption(it) } }, onConfirmKyc = { url -> launchInAppBrowser(context, url) @@ -308,7 +302,6 @@ fun TransactionDetailScreen( modifier = modifier, t = tx, devMode = devMode, - spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency), onTransition = { handleTransactionAction(tx, it, model, onNavigateBack) }, @@ -322,7 +315,6 @@ fun TransactionDetailScreen( modifier = modifier, t = tx, devMode = devMode, - spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency), ) { handleTransactionAction(tx, it, model, onNavigateBack) } @@ -336,7 +328,6 @@ fun TransactionDetailScreen( modifier = modifier, t = tx, devMode = devMode, - spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency), onConfirmKyc = { url -> launchInAppBrowser(context, url) }, onForward = { uri -> val scopeJson = tx.scopes.firstOrNull()?.let { @@ -357,7 +348,6 @@ fun TransactionDetailScreen( modifier = modifier, t = tx, devMode = devMode, - spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency) ) { handleTransactionAction(tx, it, model, onNavigateBack) } @@ -383,7 +373,6 @@ fun TransactionDetailScreen( fun TransactionRefreshComposable( t: TransactionRefresh, devMode: Boolean, - spec: CurrencySpecification?, modifier: Modifier = Modifier, onTransition: (t: TransactionAction) -> Unit, ) { @@ -404,7 +393,7 @@ fun TransactionRefreshComposable( TransactionAmountComposable( label = stringResource(id = R.string.amount_fee), - amount = t.amountEffective.withSpec(spec), + amount = t.amountEffective, amountType = AmountType.Negative, ) @@ -423,7 +412,6 @@ fun TransactionRefreshComposable( fun TransactionPeerComposable( t: Transaction, devMode: Boolean, - spec: CurrencySpecification?, modifier: Modifier = Modifier, onConfirmKyc: (url: String) -> Unit, onForward: (uri: String) -> Unit, @@ -445,10 +433,10 @@ fun TransactionPeerComposable( ) when (t) { - is TransactionPeerPullCredit -> TransactionPeerPullCreditComposable(t, spec, devMode, onConfirmKyc, onForward) - is TransactionPeerPushCredit -> TransactionPeerPushCreditComposable(t, spec, onConfirmKyc) - is TransactionPeerPullDebit -> TransactionPeerPullDebitComposable(t, spec) - is TransactionPeerPushDebit -> TransactionPeerPushDebitComposable(t, spec, devMode, onForward) + is TransactionPeerPullCredit -> TransactionPeerPullCreditComposable(t, devMode, onConfirmKyc, onForward) + is TransactionPeerPushCredit -> TransactionPeerPushCreditComposable(t, onConfirmKyc) + is TransactionPeerPullDebit -> TransactionPeerPullDebitComposable(t) + is TransactionPeerPushDebit -> TransactionPeerPushDebitComposable(t, devMode, onForward) else -> {} } @@ -468,7 +456,6 @@ fun TransactionPeerComposable( fun TransitionLossComposable( t: TransactionDenomLoss, devMode: Boolean, - spec: CurrencySpecification?, modifier: Modifier = Modifier, onTransition: (t: TransactionAction) -> Unit, ) { @@ -490,7 +477,7 @@ fun TransitionLossComposable( TransactionAmountComposable( label = stringResource(id = R.string.amount_lost), - amount = t.amountEffective.withSpec(spec), + amount = t.amountEffective, amountType = AmountType.Negative, ) @@ -652,6 +639,6 @@ fun TransactionRefreshComposablePreview() { )) ) Surface { - TransactionRefreshComposable(t, true, null) {} + TransactionRefreshComposable(t, true) {} } } diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt @@ -29,11 +29,13 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.jsonPrimitive +import net.taler.common.CurrencySpecification import net.taler.wallet.main.TAG import net.taler.wallet.backend.BackendManager import net.taler.wallet.backend.TalerErrorInfo import net.taler.wallet.backend.WalletBackendApi import net.taler.wallet.balances.ScopeInfo +import net.taler.wallet.exchanges.ExchangeManager import net.taler.wallet.transactions.TransactionAction.Delete import org.json.JSONObject @@ -58,6 +60,7 @@ enum class TransactionStateFilter { class TransactionManager( private val api: WalletBackendApi, private val scope: CoroutineScope, + private val exchangeManager: ExchangeManager, ) { private companion object { /** @@ -209,13 +212,12 @@ class TransactionManager( Log.e(TAG, "Error: getTransactions error result: $error") result = TransactionsResult.Error(error) }.onSuccess { res -> - result = if (limit == null || limit >= 0) { - TransactionsResult.Success(res - .transactions - .reversed()) + val transactions = if (limit == null || limit >= 0) { + res.transactions.reversed() } else { - TransactionsResult.Success(res.transactions) + res.transactions } + result = TransactionsResult.Success(enrichAll(transactions)) } return result @@ -228,13 +230,99 @@ class TransactionManager( }.onError { Log.e(TAG, "Error getting transaction $it") }.onSuccess { result -> - transaction = result + transaction = enrich(result) } return transaction } /** + * Resolves the currency spec once per currency/scope combination and attaches + * it to the transaction amounts, so composables can render them directly. + */ + private suspend fun enrichAll(transactions: List<Transaction>): List<Transaction> { + val specs = mutableMapOf<Pair<String, List<ScopeInfo>>, CurrencySpecification?>() + return transactions.map { transaction -> + if (transaction is DummyTransaction) return@map transaction + val key = transaction.amountRaw.currency to transaction.scopes + val spec = if (specs.containsKey(key)) { + specs[key] + } else { + exchangeManager.getSpecForCurrency( + transaction.amountRaw.currency, + transaction.scopes, + ).also { specs[key] = it } + } + transaction.withCurrencySpec(spec) + } + } + + private suspend fun enrich(transaction: Transaction): Transaction { + if (transaction is DummyTransaction) return transaction + val spec = exchangeManager.getSpecForCurrency( + transaction.amountRaw.currency, + transaction.scopes, + ) + return transaction.withCurrencySpec(spec) + } + + private fun Transaction.withCurrencySpec( + spec: CurrencySpecification?, + ): Transaction = when (this) { + is TransactionWithdrawal -> copy( + amountRaw = amountRaw.withSpec(spec), + amountEffective = amountEffective.withSpec(spec), + ) + + is TransactionPayment -> copy( + amountRaw = amountRaw.withSpec(spec), + amountEffective = amountEffective.withSpec(spec), + ) + + is TransactionRefund -> copy( + amountRaw = amountRaw.withSpec(spec), + amountEffective = amountEffective.withSpec(spec), + ) + + is TransactionRefresh -> copy( + amountRaw = amountRaw.withSpec(spec), + amountEffective = amountEffective.withSpec(spec), + ) + + is TransactionDeposit -> copy( + amountRaw = amountRaw.withSpec(spec), + amountEffective = amountEffective.withSpec(spec), + ) + + is TransactionPeerPullDebit -> copy( + amountRaw = amountRaw.withSpec(spec), + amountEffective = amountEffective.withSpec(spec), + ) + + is TransactionPeerPullCredit -> copy( + amountRaw = amountRaw.withSpec(spec), + amountEffective = amountEffective.withSpec(spec), + ) + + is TransactionPeerPushDebit -> copy( + amountRaw = amountRaw.withSpec(spec), + amountEffective = amountEffective.withSpec(spec), + ) + + is TransactionPeerPushCredit -> copy( + amountRaw = amountRaw.withSpec(spec), + amountEffective = amountEffective.withSpec(spec), + ) + + is TransactionDenomLoss -> copy( + amountRaw = amountRaw.withSpec(spec), + amountEffective = amountEffective.withSpec(spec), + ) + + is DummyTransaction -> this + } + + /** * Returns true if given [transactionId] was found and selected, false otherwise. * * The [transactionId] is recorded as selected before the transaction is fetched, diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -162,7 +162,7 @@ sealed class AmountType { @Serializable @SerialName("withdrawal") -class TransactionWithdrawal( +data class TransactionWithdrawal( override val transactionId: String, override val timestamp: Timestamp, override val txState: TransactionState, @@ -299,10 +299,16 @@ data class WithdrawalExchangeAccountDetails ( transferOption: TransferOption, ): TransferData? { val uri = paytoUri.trim().toUri() - val transferAmount = (transferAmount + val fallbackAmount = transferAmount ?: uri.getQueryParameter("amount") ?.let { Amount.fromJSONString(it) } - ?: amountEffective).withSpec(currencySpecification) + ?: amountEffective + val transferSpec = currencySpecification + ?: fallbackAmount.spec + ?: amountEffective.spec.takeIf { + fallbackAmount.currency == amountEffective.currency + } + val transferAmount = fallbackAmount.withSpec(transferSpec) return if ("bitcoin".equals(uri.authority, true)) { val msg = uri.getQueryParameter("message").orEmpty() val reg = "\\b([A-Z0-9]{52})\\b".toRegex().find(msg) @@ -363,8 +369,7 @@ data class WithdrawalExchangeAccountDetails ( subject = uri.getQueryParameter("message") ?: return@let null, amountRaw = amountRaw, amountEffective = amountEffective, - transferAmount = transferAmount - .withSpec(currencySpecification), + transferAmount = transferAmount, withdrawalAccount = copy(paytoUri = uri.toString()), transferOption = transferOption, ) @@ -436,7 +441,7 @@ sealed class TransferOption { @Serializable @SerialName("payment") -class TransactionPayment( +data class TransactionPayment( override val transactionId: String, override val timestamp: Timestamp, override val txState: TransactionState, @@ -481,7 +486,7 @@ class TransactionInfo( @Serializable @SerialName("refund") -class TransactionRefund( +data class TransactionRefund( override val transactionId: String, override val timestamp: Timestamp, override val txState: TransactionState, @@ -506,7 +511,7 @@ class TransactionRefund( @Serializable @SerialName("refresh") -class TransactionRefresh( +data class TransactionRefresh( override val transactionId: String, override val timestamp: Timestamp, override val txState: TransactionState, @@ -531,7 +536,7 @@ class TransactionRefresh( @Serializable @SerialName("deposit") -class TransactionDeposit( +data class TransactionDeposit( override val transactionId: String, override val timestamp: Timestamp, override val txState: TransactionState, @@ -580,7 +585,7 @@ data class PeerInfoShort( */ @Serializable @SerialName("peer-pull-debit") -class TransactionPeerPullDebit( +data class TransactionPeerPullDebit( override val transactionId: String, override val timestamp: Timestamp, override val txState: TransactionState, @@ -614,7 +619,7 @@ class TransactionPeerPullDebit( */ @Serializable @SerialName("peer-pull-credit") -class TransactionPeerPullCredit( +data class TransactionPeerPullCredit( override val transactionId: String, override val timestamp: Timestamp, override val txState: TransactionState, @@ -646,7 +651,7 @@ class TransactionPeerPullCredit( */ @Serializable @SerialName("peer-push-debit") -class TransactionPeerPushDebit( +data class TransactionPeerPushDebit( override val transactionId: String, override val timestamp: Timestamp, override val txState: TransactionState, @@ -682,7 +687,7 @@ class TransactionPeerPushDebit( */ @Serializable @SerialName("peer-push-credit") -class TransactionPeerPushCredit( +data class TransactionPeerPushCredit( override val transactionId: String, override val timestamp: Timestamp, override val txState: TransactionState, @@ -718,7 +723,7 @@ class TransactionPeerPushCredit( */ @Serializable @SerialName("denom-loss") -class TransactionDenomLoss( +data class TransactionDenomLoss( override val transactionId: String, override val timestamp: Timestamp, override val txState: TransactionState, diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionsComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionsComposable.kt @@ -146,7 +146,7 @@ fun TransactionsComposable( val isSelected = selectedItems.contains(tx.transactionId) TransactionRow( - tx, balance.available.spec, + tx, isSelected = isSelected, selectionMode = selectionMode, onTransactionClick = { @@ -266,7 +266,6 @@ fun TransactionsHeader( @Composable fun TransactionRow( tx: Transaction, - spec: CurrencySpecification?, isSelected: Boolean, selectionMode: Boolean, onTransactionClick: () -> Unit, @@ -293,7 +292,7 @@ fun TransactionRow( .padding(8.dp), contentAlignment = Center, ) { - TransactionAmountInfo(tx, spec) + TransactionAmountInfo(tx) } }, leadingContent = { @@ -357,11 +356,10 @@ fun TransactionRow( @Composable fun TransactionAmountInfo( tx: Transaction, - spec: CurrencySpecification?, ) { Column(horizontalAlignment = Alignment.End) { ProvideTextStyle(MaterialTheme.typography.titleLarge) { - val amountStr = tx.amountEffective.withSpec(spec).toString(showSymbol = false) + val amountStr = tx.amountEffective.toString(showSymbol = false) if (tx.amountEffective.isZero()) { Text(amountStr) } else when (tx.amountType) { diff --git a/wallet/src/main/java/net/taler/wallet/transfer/ScreenTransfer.kt b/wallet/src/main/java/net/taler/wallet/transfer/ScreenTransfer.kt @@ -81,7 +81,6 @@ fun TransferOption.subjectLabelResId(): Int = when (this) { fun ScreenTransfer( modifier: Modifier = Modifier, transfer: TransferData, - spec: CurrencySpecification?, showQrCodes: Boolean, bankAppClick: ((transfer: TransferData) -> Unit)?, shareClick: ((transfer: TransferData) -> Unit)?, @@ -133,19 +132,19 @@ fun ScreenTransfer( when (transfer) { is TransferData.Taler -> TransferTaler( transfer = transfer, - transactionAmountEffective = transfer.amountEffective.withSpec(spec), + transactionAmountEffective = transfer.amountEffective, transferContext = transferContext, ) is TransferData.IBAN -> TransferIBAN( transfer = transfer, - transactionAmountEffective = transfer.amountEffective.withSpec(spec), + transactionAmountEffective = transfer.amountEffective, transferContext = transferContext, ) is TransferData.Cyclos -> TransferCyclos( transfer = transfer, - transactionAmountEffective = transfer.amountEffective.withSpec(spec), + transactionAmountEffective = transfer.amountEffective, ) is TransferData.Bitcoin -> TransferBitcoin( @@ -334,7 +333,6 @@ fun ScreenTransferIBANPreview( ) ), ), - spec = null, bankAppClick = {}, shareClick = {}, showQrCodes = showQrCodes, @@ -367,7 +365,6 @@ fun ScreenTransferCyclosPreview( status = Ok, ), ), - spec = null, bankAppClick = {}, shareClick = {}, showQrCodes = showQrCodes, @@ -409,7 +406,6 @@ fun ScreenTransferBitcoinPreview( ), ), ), - spec = null, bankAppClick = {}, shareClick = {}, showQrCodes = showQrCodes, diff --git a/wallet/src/main/java/net/taler/wallet/transfer/TransferBitcoin.kt b/wallet/src/main/java/net/taler/wallet/transfer/TransferBitcoin.kt @@ -57,7 +57,8 @@ fun TransferBitcoin( transfer.withdrawalAccount.transferAmount?.let { amount -> WithdrawalAmountTransfer( conversionAmountRaw = amount.withSpec( - transfer.withdrawalAccount.currencySpecification, + transfer.withdrawalAccount.currencySpecification + ?: transfer.transferAmount.spec, ), ) } diff --git a/wallet/src/main/java/net/taler/wallet/transfer/WireTransferDetailsScreen.kt b/wallet/src/main/java/net/taler/wallet/transfer/WireTransferDetailsScreen.kt @@ -43,7 +43,6 @@ import net.taler.wallet.R import net.taler.wallet.compose.EmptyComposable import net.taler.wallet.compose.GlobalScaffold import net.taler.wallet.compose.collectAsStateLifecycleAware -import net.taler.wallet.compose.rememberCurrencySpec import net.taler.wallet.main.MainViewModel import net.taler.wallet.transactions.TransactionDeposit import net.taler.wallet.transactions.TransactionMajorState.Done @@ -63,7 +62,6 @@ fun WireTransferDetailsScreen( val context = LocalContext.current val sharePaymentTitle = stringResource(R.string.share_payment) val transactionManager = model.transactionManager - val exchangeManager = model.exchangeManager val selectedTx by transactionManager.selectedTransaction.collectAsStateLifecycleAware() val selectedOption by transactionManager.selectedTransferOption.collectAsStateLifecycleAware() @@ -119,12 +117,6 @@ fun WireTransferDetailsScreen( ) { paddingValues -> val tx = selectedTx ?: return@GlobalScaffold - val spec = rememberCurrencySpec( - tx.amountRaw.currency, - tx.scopes, - exchangeManager::getSpecForCurrency, - ) - val bankAppClick: (TransferData) -> Unit = { transfer -> context.openUri(uri = transfer.withdrawalAccount.paytoUri, title = sharePaymentTitle) } @@ -153,7 +145,6 @@ fun WireTransferDetailsScreen( ScreenTransfer( modifier = Modifier.padding(paddingValues), transfer = transferData, - spec = spec, bankAppClick = bankAppClick, shareClick = shareClick, showQrCodes = showQrCodes, @@ -181,7 +172,6 @@ fun WireTransferDetailsScreen( ScreenTransfer( modifier = Modifier.padding(paddingValues), transfer = transferData, - spec = spec, bankAppClick = bankAppClick, shareClick = shareClick, showQrCodes = showQrCodes, diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt b/wallet/src/main/java/net/taler/wallet/withdraw/TransactionWithdrawalComposable.kt @@ -93,7 +93,6 @@ fun transferOptionAt( fun TransactionWithdrawalComposable( t: TransactionWithdrawal, devMode: Boolean, - spec: CurrencySpecification?, onSelectOption: (option: TransferOption?) -> Unit, onConfirmKyc: (url: String) -> Unit, onConfirmBank: () -> Unit, @@ -163,7 +162,7 @@ fun TransactionWithdrawalComposable( if (t.amountRaw != t.amountEffective) { TransactionAmountComposable( label = stringResource(R.string.amount_chosen), - amount = t.amountRaw.withSpec(spec), + amount = t.amountRaw, amountType = AmountType.Neutral, ) } @@ -172,14 +171,14 @@ fun TransactionWithdrawalComposable( val fee = t.amountRaw - t.amountEffective TransactionAmountComposable( label = stringResource(id = R.string.amount_fee), - amount = fee.withSpec(spec), + amount = fee, amountType = AmountType.Negative, ) } TransactionAmountComposable( label = stringResource(id = R.string.amount_total), - amount = t.amountEffective.withSpec(spec), + amount = t.amountEffective, amountType = AmountType.Positive, ) @@ -321,7 +320,6 @@ fun TransactionWithdrawalComposableSingleQrPreview() { TransactionWithdrawalComposable( t = previewWithdrawalTx, devMode = true, - spec = null, onSelectOption = {}, onConfirmKyc = {}, onConfirmBank = {}, @@ -339,7 +337,6 @@ fun TransactionWithdrawalComposableMultiQrPreview() { TransactionWithdrawalComposable( t = previewWithdrawalTx, devMode = true, - spec = null, onSelectOption = {}, onConfirmKyc = {}, onConfirmBank = {}, diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawManager.kt @@ -572,8 +572,8 @@ class WithdrawManager( val details = status.amountInfo ?: return@mapNotNull null val opt = it.transferOptions.firstOrNull() ?: return@mapNotNull null it.getTransferDetails( - amountRaw = details.amountRaw, - amountEffective = details.amountEffective, + amountRaw = details.amountRaw.withSpec(status.selectedSpec), + amountEffective = details.amountEffective.withSpec(status.selectedSpec), transferOption = opt, ) }, diff --git a/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawalShowInfo.kt b/wallet/src/main/java/net/taler/wallet/withdraw/WithdrawalShowInfo.kt @@ -218,7 +218,8 @@ fun WithdrawalShowInfo( isError = selectedAmount.amount.isZero() || insufficientBalance, supportingText = { if (insufficientBalance && maxAmount != null) { - Text(stringResource(R.string.amount_excess, maxAmount)) + Text(stringResource(R.string.amount_excess, + maxAmount.withSpec(maxAmount.spec ?: status.selectedSpec))) } }, showShortcuts = true,