taler-android

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

commit 7c55f7a84115f0f5333af84165d47c50dfd3c614
parent 0e4ae9635d12ca36a791cf54d9686fb07047e769
Author: Iván Ávalos <avalos@disroot.org>
Date:   Wed, 22 Jul 2026 23:41:18 +0200

[wallet] template fixes and improvements

Diffstat:
Mwallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt | 120+++++++++++++++++++++++--------------------------------------------------------
Mwallet/src/main/java/net/taler/wallet/payment/PayTemplateDetails.kt | 5++---
Mwallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt | 25+++++++++++++++++++++++--
Mwallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt | 14+++++++++-----
Mwallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt | 12+++++-------
Mwallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt | 14++------------
6 files changed, 75 insertions(+), 115 deletions(-)

diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt @@ -16,22 +16,22 @@ package net.taler.wallet.payment -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment.Companion.Center +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource 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.ContractTerms import net.taler.common.CurrencySpecification -import net.taler.common.Merchant import net.taler.wallet.main.AmountResult import net.taler.wallet.R import net.taler.wallet.compose.LoadingScreen @@ -46,8 +46,8 @@ fun PayTemplateComposable( onCreateAmount: (String, String) -> AmountResult, onSubmit: (params: TemplateParams) -> Unit, onError: (msg: String) -> Unit, + onRetry: (() -> Unit)? = null, ) { - // If wallet is empty, there's no way the user can pay something if (currencies.isEmpty()) { PayTemplateError(stringResource(R.string.payment_balance_insufficient)) } else when (val p = payStatus) { @@ -69,39 +69,37 @@ fun PayTemplateComposable( ) } } - is PayStatus.None, is PayStatus.Loading -> PayTemplateLoading() - is PayStatus.AlreadyPaid -> PayTemplateError(stringResource(R.string.payment_already_paid)) - is PayStatus.InsufficientBalance -> { - var errorMsg = stringResource(R.string.payment_balance_insufficient) - p.balanceDetails.causeHint?.stringResId()?.let { - errorMsg += "\n\n" - errorMsg += stringResource(it) - } - PayTemplateError(errorMsg) - } - is PayStatus.Pending -> { - val error = p.error - PayTemplateError(if (error != null) { - stringResource(R.string.payment_error, error.userFacingMsg) + is PayStatus.Pending -> PayTemplateError( + message = if (p.error != null) { + stringResource(R.string.payment_error, p.error.userFacingMsg) } else { stringResource(R.string.payment_template_error) - }) - } - is PayStatus.Prepared -> {} // handled in fragment, will redirect - is PayStatus.Success -> {} // handled by other UI flow, no need for content here - is PayStatus.Choices -> {} // only applies to regular payments + }, + onRetry = onRetry, + ) + is PayStatus.Prepared -> PayTemplateLoading() + + // not emitted by template flow + is PayStatus.AlreadyPaid, + is PayStatus.InsufficientBalance, + is PayStatus.Success, + is PayStatus.Choices -> {} } } @Composable -fun PayTemplateError(message: String) { - Box( +fun PayTemplateError( + message: String, + onRetry: (() -> Unit)? = null, +) { + Column( modifier = Modifier .padding(16.dp) .fillMaxSize() .systemBarsPaddingBottom(), - contentAlignment = Center, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, ) { Text( text = message, @@ -109,6 +107,15 @@ fun PayTemplateError(message: String) { color = MaterialTheme.colorScheme.error, textAlign = TextAlign.Center, ) + + if (onRetry != null) { + Button( + modifier = Modifier.padding(top = 24.dp), + onClick = onRetry, + ) { + Text(stringResource(R.string.transactions_retry)) + } + } } } @@ -136,65 +143,6 @@ fun PayTemplateLoadingPreview() { @Preview @Composable -fun PayTemplateInsufficientBalancePreview() { - TalerSurface { - PayTemplateComposable( - payStatus = PayStatus.InsufficientBalance( - "txn:3409F039F09", - ContractTerms.V0( - "test", - amount = Amount.zero("TESTKUDOS"), - products = emptyList(), - orderId = "xxxxx", - merchantBaseUrl = "https://backend.test.taler.net/", - merchant = Merchant(name = "Test Backend"), - maxFee = Amount.zero("TESTKUDOS") - ), - Amount.zero("TESTKUDOS"), - PaymentInsufficientBalanceDetails( - amountRequested = Amount.fromJSONString("TESTKUDOS:1"), - causeHint = InsufficientBalanceHint.MerchantDepositInsufficient, - balanceAvailable = Amount.fromJSONString("TESTKUDOS:1"), - balanceMaterial = Amount.fromJSONString("TESTKUDOS:1"), - balanceAgeAcceptable = Amount.fromJSONString("TESTKUDOS:1"), - balanceReceiverAcceptable = Amount.fromJSONString("TESTKUDOS:0"), - balanceReceiverDepositable = Amount.fromJSONString("TESTKUDOS:0"), - balanceExchangeDepositable = Amount.fromJSONString("TESTKUDOS:1"), - maxEffectiveSpendAmount = Amount.fromJSONString("TESTKUDOS:1"), - perExchange = emptyMap(), - ) - ), - currencies = listOf("KUDOS", "ARS"), - onCreateAmount = { text, currency -> - AmountResult.Success(amount = Amount.fromString(currency, text)) - }, - onSubmit = { _ -> }, - onError = { _ -> }, - getCurrencySpec = { null }, - ) - } -} - -@Preview(widthDp = 300) -@Composable -fun PayTemplateAlreadyPaidPreview() { - TalerSurface { - PayTemplateComposable( - payStatus = PayStatus.AlreadyPaid(transactionId = "transactionId"), - currencies = listOf("KUDOS", "ARS"), - onCreateAmount = { text, currency -> - AmountResult.Success(amount = Amount.fromString(currency, text)) - }, - onSubmit = { _ -> }, - onError = { _ -> }, - getCurrencySpec = { null }, - ) - } -} - - -@Preview -@Composable fun PayTemplateNoCurrenciesPreview() { TalerSurface { PayTemplateComposable( diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateDetails.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateDetails.kt @@ -117,10 +117,9 @@ class WalletTemplateDetails( || isAmountEditable() || isCurrencyEditable(usableCurrencies) - // NOTE: it is important to nullify non-editable values! fun toTemplateParams() = TemplateParams( - amount = if(isAmountEditable()) templateContract.amount else null, - summary = if(isSummaryEditable()) templateContract.summary else null, + amount = if (isAmountEditable()) null else templateContract.amount, + summary = if (isSummaryEditable()) null else templateContract.summary, ) } diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateOrderComposable.kt @@ -143,6 +143,27 @@ val defaultTemplateDetails = WalletTemplateDetails( ), ) +val fixedAmountDetails = WalletTemplateDetails( + templateContract = TemplateContractDetails( + templateType = TemplateType.FixedOrder, + summary = "Pizza Margherita", + amount = Amount.fromJSONString("KUDOS:10.0"), + minimumAge = 18, + payDuration = RelativeTime.forever(), + ), +) + +val blankSubjectDetails = WalletTemplateDetails( + templateContract = TemplateContractDetails( + templateType = TemplateType.FixedOrder, + minimumAge = 18, + payDuration = RelativeTime.forever(), + ), + editableDefaults = TemplateContractDetailsDefaults( + amount = Amount.fromJSONString("KUDOS:10.0"), + ), +) + @Preview @Composable fun PayTemplateDefaultPreview() { @@ -165,7 +186,7 @@ fun PayTemplateDefaultPreview() { fun PayTemplateFixedAmountPreview() { TalerSurface { PayTemplateOrderComposable( - templateDetails = defaultTemplateDetails, + templateDetails = fixedAmountDetails, usableCurrencies = listOf("KUDOS", "ARS"), onCreateAmount = { text, currency -> AmountResult.Success(amount = Amount.fromString(currency, text)) @@ -182,7 +203,7 @@ fun PayTemplateFixedAmountPreview() { fun PayTemplateBlankSubjectPreview() { TalerSurface { PayTemplateOrderComposable( - templateDetails = defaultTemplateDetails, + templateDetails = blankSubjectDetails, usableCurrencies = listOf("KUDOS", "ARS"), onCreateAmount = { text, currency -> AmountResult.Success(amount = Amount.fromString(currency, text)) diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt @@ -24,10 +24,12 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.lifecycle.asFlow import kotlinx.coroutines.launch import net.taler.wallet.NavigateCallback import net.taler.wallet.R @@ -38,7 +40,6 @@ import net.taler.wallet.compose.ErrorComposable import net.taler.wallet.compose.GlobalScaffold import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.TalerSurface -import net.taler.wallet.compose.collectAsStateLifecycleAware import net.taler.wallet.main.MainViewModel @Composable @@ -54,7 +55,7 @@ fun PayTemplateScreen( val exchangeManager = model.exchangeManager val transactionManager = model.transactionManager - val payStatus by paymentManager.payStatus.asFlow().collectAsStateLifecycleAware(PayStatus.None) + val payStatus by paymentManager.payStatus.observeAsState(PayStatus.None) val balanceState by balanceManager.state.observeAsState(BalanceState.None) val devMode by model.devMode.observeAsState(false) val scope = rememberCoroutineScope() @@ -68,12 +69,14 @@ fun PayTemplateScreen( } } - LaunchedEffect(Unit) { + var retryTrigger by remember { mutableIntStateOf(0) } + + LaunchedEffect(retryTrigger) { balanceManager.loadAssets() paymentManager.checkPayForTemplate(uri) } - LaunchedEffect(payStatus) { + LaunchedEffect(payStatus, retryTrigger) { when (val s = payStatus) { is PayStatus.Prepared -> { if (transactionManager.selectTransaction(s.transactionId)) { @@ -119,6 +122,7 @@ fun PayTemplateScreen( onError = { errorMsg -> onShowError(TalerErrorInfo.makeCustomError(errorMsg)) }, + onRetry = { retryTrigger++ }, getCurrencySpec = exchangeManager::getSpecForCurrency, ) } diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt @@ -36,11 +36,6 @@ import net.taler.wallet.balances.ScopeInfo import net.taler.wallet.donau.DonauInfo import net.taler.wallet.donau.GetDonauResponse import net.taler.wallet.exchanges.ExchangeManager -import net.taler.wallet.payment.PayStatus.AlreadyPaid -import net.taler.wallet.payment.PayStatus.InsufficientBalance -import net.taler.wallet.payment.PreparePayResponse.AlreadyConfirmedResponse -import net.taler.wallet.payment.PreparePayResponse.InsufficientBalanceResponse -import net.taler.wallet.payment.PreparePayResponse.PaymentPossibleResponse import org.json.JSONObject import net.taler.wallet.payment.GetChoicesForPaymentResponse.ChoiceSelectionDetail import net.taler.wallet.payment.GetChoicesForPaymentResponse.ChoiceSelectionDetail.PaymentPossible @@ -50,7 +45,6 @@ sealed class PayStatus { data object Loading : PayStatus() data class Prepared( val transactionId: String, - val contractTerms: ContractTerms, ) : PayStatus() data class Choices( @@ -258,7 +252,11 @@ class PaymentManager( handleError("checkPayForTemplate", it) }.onSuccess { response -> if (response.templateDetails.templateContract.templateType == TemplateType.Paivana) { - scope.launch { preparePayForTemplate(url, TemplateParams()) } + scope.launch { + preparePayForTemplate(url, TemplateParams())?.let { transactionId -> + mPayStatus.value = PayStatus.Prepared(transactionId = transactionId) + } + } } else { mPayStatus.value = PayStatus.Checked( details = response.templateDetails, diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentResponses.kt @@ -45,12 +45,7 @@ sealed class PreparePayResponse { data class PaymentPossibleResponse( val transactionId: String, val contractTerms: ContractTerms, - ) : PreparePayResponse() { - fun toPayStatusPrepared() = PayStatus.Prepared( - contractTerms = contractTerms, - transactionId = transactionId, - ) - } + ) : PreparePayResponse() @Serializable @SerialName("insufficient-balance") @@ -79,12 +74,7 @@ sealed class PreparePayResponse { data class ChoiceSelection( val transactionId: String, val contractTerms: ContractTerms, - ) : PreparePayResponse() { - fun toPayStatusPrepared() = PayStatus.Prepared( - contractTerms = contractTerms, - transactionId = transactionId, - ) - } + ) : PreparePayResponse() } @Serializable