commit d05e1bb523e80112e62a0f49a2520e8b697c074d
parent 1da8cdbd06e2c885647ab8da7a0ea9a4b475935f
Author: Iván Ávalos <avalos@disroot.org>
Date: Wed, 12 Aug 2026 10:32:08 +0200
[wallet] wait for currency spec non-blockingly
Diffstat:
8 files changed, 178 insertions(+), 105 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/compose/CurrencySpec.kt b/wallet/src/main/java/net/taler/wallet/compose/CurrencySpec.kt
@@ -0,0 +1,53 @@
+/*
+ * 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/DepositManager.kt b/wallet/src/main/java/net/taler/wallet/deposit/DepositManager.kt
@@ -22,7 +22,6 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.jsonObject
@@ -36,6 +35,7 @@ import net.taler.wallet.accounts.PaytoUriTalerBank
import net.taler.wallet.backend.BackendManager
import net.taler.wallet.backend.TalerErrorCode.WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE
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 org.json.JSONObject
@@ -66,11 +66,31 @@ class DepositManager(
suspend fun checkDepositFees(paytoUri: String, amount: Amount): CheckDepositResult {
var response: CheckDepositResult = CheckDepositResult.None
- api.request("checkDeposit", CheckDepositResponse.serializer()) {
+ when (val res = api.request("checkDeposit", CheckDepositResponse.serializer()) {
put("depositPaytoUri", paytoUri)
put("amount", amount.toJSONString())
- }.onSuccess {
- runBlocking {
+ }) {
+ is WalletResponse.Error -> {
+ Log.e(TAG, "Error checkDeposit ${res.error}")
+ if (res.error.code == WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE) {
+ res.error.extra["insufficientBalanceDetails"]?.let { details ->
+ val maxAmountRaw = details.jsonObject["balanceAvailable"]?.let { amount ->
+ Amount.fromJSONString(amount.jsonPrimitive.content)
+ }
+
+ val maxAmountEffective = details.jsonObject["maxEffectiveSpendAmount"]?.let { amount ->
+ Amount.fromJSONString(amount.jsonPrimitive.content)
+ } ?: maxAmountRaw
+
+ response = CheckDepositResult.InsufficientBalance(
+ maxAmountEffective = maxAmountEffective,
+ maxAmountRaw = maxAmountRaw,
+ )
+ }
+ }
+ }
+
+ is WalletResponse.Success -> {
val max = getMaxDepositAmount(amount.currency, paytoUri)
response = if (max?.effectiveAmount != null && amount > max.effectiveAmount) {
CheckDepositResult.ExceedsLimit(
@@ -79,29 +99,11 @@ class DepositManager(
)
} else {
CheckDepositResult.Success(
- totalDepositCost = it.totalDepositCost,
- effectiveDepositAmount = it.effectiveDepositAmount,
- kycSoftLimit = it.kycSoftLimit,
- kycHardLimit = it.kycHardLimit,
- kycExchanges = it.kycExchanges,
- )
- }
- }
- }.onError { error ->
- Log.e(TAG, "Error checkDeposit $error")
- if (error.code == WALLET_DEPOSIT_GROUP_INSUFFICIENT_BALANCE) {
- error.extra["insufficientBalanceDetails"]?.let { details ->
- val maxAmountRaw = details.jsonObject["balanceAvailable"]?.let { amount ->
- Amount.fromJSONString(amount.jsonPrimitive.content)
- }
-
- val maxAmountEffective = details.jsonObject["maxEffectiveSpendAmount"]?.let { amount ->
- Amount.fromJSONString(amount.jsonPrimitive.content)
- } ?: maxAmountRaw
-
- response = CheckDepositResult.InsufficientBalance(
- maxAmountEffective = maxAmountEffective,
- maxAmountRaw = maxAmountRaw,
+ totalDepositCost = res.result.totalDepositCost,
+ effectiveDepositAmount = res.result.effectiveDepositAmount,
+ kycSoftLimit = res.result.kycSoftLimit,
+ kycHardLimit = res.result.kycHardLimit,
+ kycExchanges = res.result.kycExchanges,
)
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt b/wallet/src/main/java/net/taler/wallet/exchanges/ExchangeManager.kt
@@ -25,7 +25,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import net.taler.common.CurrencySpecification
import net.taler.common.Event
@@ -308,12 +307,11 @@ class ExchangeManager(
}?.let { currencySpecs[it] }
}
- fun getSpecForCurrency(currency: String, scopes: List<ScopeInfo>) =
+ suspend fun getSpecForCurrency(currency: String, scopes: List<ScopeInfo>): CurrencySpecification? =
scopes.find { it.currency == currency }?.let { scope ->
- runBlocking { getCurrencySpecification(scope) }
+ getCurrencySpecification(scope)
}
- fun getSpecForScopeInfo(scopeInfo: ScopeInfo): CurrencySpecification? {
- return runBlocking { getCurrencySpecification(scopeInfo) }
- }
+ suspend fun getSpecForScopeInfo(scopeInfo: ScopeInfo): CurrencySpecification? =
+ getCurrencySpecification(scopeInfo)
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
@@ -32,6 +32,7 @@ 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.backend.WalletResponse
import net.taler.wallet.balances.ScopeInfo
import net.taler.wallet.donau.DonauInfo
import net.taler.wallet.donau.GetDonauResponse
@@ -126,71 +127,74 @@ class PaymentManager(
transactionId: String,
onSuccess: () -> Unit,
) = scope.launch {
- api.request("getChoicesForPayment", GetChoicesForPaymentResponse.serializer()) {
+ when (val response = api.request("getChoicesForPayment", GetChoicesForPaymentResponse.serializer()) {
put("transactionId", transactionId)
- }.onSuccess { res ->
- if (res.automaticExecution == true && res.automaticExecutableIndex != null) {
- confirmPay(transactionId, res.automaticExecutableIndex, automaticExecution = true)
- return@onSuccess
- }
+ }) {
+ is WalletResponse.Error -> handleError("getChoicesForPayment", response.error)
- mPayStatus.value = PayStatus.Choices(
- transactionId = transactionId,
- contractTerms = res.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)
- )
- }
+ is WalletResponse.Success -> {
+ val res = response.result
+ if (res.automaticExecution == true && res.automaticExecutableIndex != null) {
+ confirmPay(transactionId, res.automaticExecutableIndex, automaticExecution = true)
+ return@launch
+ }
- when (choice) {
- is PaymentPossible -> {
- choice.copy(
- amountRaw = choice.amountRaw.withSpec(spec),
- amountEffective = choice.amountEffective.withSpec(spec),
+ mPayStatus.value = PayStatus.Choices(
+ transactionId = transactionId,
+ contractTerms = res.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)
)
}
- is ChoiceSelectionDetail.InsufficientBalance -> {
- choice.copy(amountRaw = choice.amountRaw.withSpec(spec))
+ when (choice) {
+ is PaymentPossible -> {
+ choice.copy(
+ amountRaw = choice.amountRaw.withSpec(spec),
+ amountEffective = choice.amountEffective.withSpec(spec),
+ )
+ }
+
+ is ChoiceSelectionDetail.InsufficientBalance -> {
+ choice.copy(amountRaw = choice.amountRaw.withSpec(spec))
+ }
}
- }
- }.mapIndexed { i, choice ->
- PayChoiceDetails(
- choiceIndex = i,
- description = choice.description,
- descriptionI18n = choice.descriptionI18n,
- amountRaw = choice.amountRaw,
- inputs = (res.contractTerms as? ContractTerms.V1)
- ?.choices?.get(i)?.inputs ?: listOf(),
- outputs = (res.contractTerms as? ContractTerms.V1)
- ?.choices?.get(i)?.outputs ?: listOf(),
- details = choice,
- )
- }.filter {
- // Hide auto executable choice
- res.automaticExecutableIndex != it.choiceIndex
- }.sortedWith(
- compareByDescending<PayChoiceDetails> {
- it.choiceIndex == res.defaultChoiceIndex
- }.thenByDescending {
- it.details is PaymentPossible
- }.thenByDescending {
- it.amountRaw.toString()
- },
- ),
- )
+ }.mapIndexed { i, choice ->
+ PayChoiceDetails(
+ choiceIndex = i,
+ description = choice.description,
+ descriptionI18n = choice.descriptionI18n,
+ amountRaw = choice.amountRaw,
+ inputs = (res.contractTerms as? ContractTerms.V1)
+ ?.choices?.get(i)?.inputs ?: listOf(),
+ outputs = (res.contractTerms as? ContractTerms.V1)
+ ?.choices?.get(i)?.outputs ?: listOf(),
+ details = choice,
+ )
+ }.filter {
+ // Hide auto executable choice
+ res.automaticExecutableIndex != it.choiceIndex
+ }.sortedWith(
+ compareByDescending<PayChoiceDetails> {
+ it.choiceIndex == res.defaultChoiceIndex
+ }.thenByDescending {
+ it.details is PaymentPossible
+ }.thenByDescending {
+ it.amountRaw.toString()
+ },
+ ),
+ )
- onSuccess()
- }.onError { error ->
- handleError("getChoicesForPayment", error)
+ onSuccess()
+ }
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPullComposable.kt
@@ -34,6 +34,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@@ -72,7 +73,7 @@ fun OutgoingPullComposable(
defaultScope: ScopeInfo?,
scopes: List<ScopeInfo>,
devMode: Boolean,
- getCurrencySpec: (scope: ScopeInfo) -> CurrencySpecification?,
+ getCurrencySpec: suspend (scope: ScopeInfo) -> CurrencySpecification?,
checkPeerPullCredit: suspend (amount: AmountScope, loading: Boolean) -> CheckPeerPullCreditResult?,
onCreateInvoice: (amount: AmountScope, subject: String, hours: Long, exchangeBaseUrl: String) -> Unit,
onTosAccept: (exchangeBaseUrl: String) -> Unit,
@@ -84,7 +85,12 @@ fun OutgoingPullComposable(
val currency = scope.currency
mutableStateOf(AmountScope(Amount.zero(currency), scope))
}
- val selectedSpec = remember(amount.scope) { getCurrencySpec(amount.scope) }
+ val selectedSpec by produceState<CurrencySpecification?>(
+ initialValue = null,
+ key1 = amount.scope,
+ ) {
+ value = getCurrencySpec(amount.scope)
+ }
var checkResult by remember { mutableStateOf<CheckPeerPullCreditResult?>(null) }
val res = checkResult
diff --git a/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt b/wallet/src/main/java/net/taler/wallet/peer/OutgoingPushComposable.kt
@@ -34,6 +34,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
@@ -77,7 +78,7 @@ fun OutgoingPushComposable(
defaultScope: ScopeInfo?,
scopes: List<ScopeInfo>,
devMode: Boolean,
- getCurrencySpec: (scope: ScopeInfo) -> CurrencySpecification?,
+ getCurrencySpec: suspend (scope: ScopeInfo) -> CurrencySpecification?,
getFees: suspend (amount: AmountScope) -> CheckFeeResult?,
onSend: (amount: AmountScope, summary: String, hours: Long) -> Unit,
modifier: Modifier = Modifier,
@@ -103,7 +104,7 @@ fun OutgoingPushIntroComposable(
defaultScope: ScopeInfo?,
scopes: List<ScopeInfo>,
devMode: Boolean,
- getCurrencySpec: (scope: ScopeInfo) -> CurrencySpecification?,
+ getCurrencySpec: suspend (scope: ScopeInfo) -> CurrencySpecification?,
getFees: suspend (amount: AmountScope) -> CheckFeeResult?,
onSend: (amount: AmountScope, summary: String, hours: Long) -> Unit,
modifier: Modifier = Modifier,
@@ -113,7 +114,12 @@ fun OutgoingPushIntroComposable(
val currency = scope.currency
mutableStateOf(AmountScope(Amount.zero(currency), scope))
}
- val selectedSpec = remember(amount.scope) { getCurrencySpec(amount.scope) }
+ val selectedSpec by produceState<CurrencySpecification?>(
+ initialValue = null,
+ key1 = amount.scope,
+ ) {
+ value = getCurrencySpec(amount.scope)
+ }
var feeResult by remember { mutableStateOf<CheckFeeResult>(None()) }
var subject by rememberSaveable { mutableStateOf("") }
diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailScreen.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailScreen.kt
@@ -73,6 +73,7 @@ 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
@@ -206,7 +207,7 @@ fun TransactionDetailScreen(
.observeAsState(PayStatus.None).value,
devMode = devMode,
promptMode = destination.promptMode,
- spec = exchangeManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes),
+ spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency),
modifier = modifier,
onFulfill = { url ->
launchInAppBrowser(context, url)
@@ -237,7 +238,7 @@ fun TransactionDetailScreen(
modifier = modifier,
t = tx,
devMode = devMode,
- spec = exchangeManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes),
+ spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency),
onSelectOption = { it?.let { transactionManager.selectTransferOption(it) } },
onConfirmKyc = { url ->
launchInAppBrowser(context, url)
@@ -270,7 +271,7 @@ fun TransactionDetailScreen(
modifier = modifier,
t = tx,
devMode = devMode,
- spec = exchangeManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes),
+ spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency),
onSelectOption = { it?.let { transactionManager.selectTransferOption(it) } },
onConfirmKyc = { url ->
launchInAppBrowser(context, url)
@@ -302,7 +303,7 @@ fun TransactionDetailScreen(
modifier = modifier,
t = tx,
devMode = devMode,
- spec = exchangeManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes),
+ spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency),
onTransition = {
handleTransactionAction(tx, it, model, onNavigateBack)
},
@@ -316,7 +317,7 @@ fun TransactionDetailScreen(
modifier = modifier,
t = tx,
devMode = devMode,
- spec = exchangeManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes),
+ spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency),
) {
handleTransactionAction(tx, it, model, onNavigateBack)
}
@@ -330,7 +331,7 @@ fun TransactionDetailScreen(
modifier = modifier,
t = tx,
devMode = devMode,
- spec = exchangeManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes),
+ spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency),
onConfirmKyc = { url -> launchInAppBrowser(context, url) }
) {
handleTransactionAction(tx, it, model, onNavigateBack)
@@ -344,7 +345,7 @@ fun TransactionDetailScreen(
modifier = modifier,
t = tx,
devMode = devMode,
- spec = exchangeManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes)
+ spec = rememberCurrencySpec(tx.amountRaw.currency, tx.scopes, exchangeManager::getSpecForCurrency)
) {
handleTransactionAction(tx, it, model, onNavigateBack)
}
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,6 +43,7 @@ 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
@@ -118,9 +119,11 @@ fun WireTransferDetailsScreen(
) { paddingValues ->
val tx = selectedTx ?: return@GlobalScaffold
- val spec = tx.amountRaw.currency.let { currency ->
- exchangeManager.getSpecForCurrency(currency, tx.scopes)
- }
+ val spec = rememberCurrencySpec(
+ tx.amountRaw.currency,
+ tx.scopes,
+ exchangeManager::getSpecForCurrency,
+ )
val bankAppClick: (TransferData) -> Unit = { transfer ->
context.openUri(uri = transfer.withdrawalAccount.paytoUri, title = sharePaymentTitle)