taler-android

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

commit 4dbf30569ee3bf018a56d4da2134de3d97ed4e0e
parent 2d5de16d5e5f472cb46724036c27b3e18e79088a
Author: Iván Ávalos <avalos@disroot.org>
Date:   Thu, 23 Jul 2026 17:20:34 +0200

[wallet] smoother payment prompt flow

Diffstat:
Mwallet/src/main/java/net/taler/wallet/HandleUriScreen.kt | 9++++++++-
Mwallet/src/main/java/net/taler/wallet/WalletNavHost.kt | 5+++--
Mwallet/src/main/java/net/taler/wallet/WalletNavigation.kt | 2+-
Mwallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt | 3+++
Mwallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt | 4++--
Mwallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt | 49+++++++++++++++++++------------------------------
Mwallet/src/main/java/net/taler/wallet/transactions/TransactionDetailScreen.kt | 6++++++
Mwallet/src/main/java/net/taler/wallet/transactions/TransactionStateComposable.kt | 2++
Mwallet/src/main/java/net/taler/wallet/transactions/Transactions.kt | 2+-
9 files changed, 45 insertions(+), 37 deletions(-)

diff --git a/wallet/src/main/java/net/taler/wallet/HandleUriScreen.kt b/wallet/src/main/java/net/taler/wallet/HandleUriScreen.kt @@ -23,6 +23,7 @@ import net.taler.wallet.compose.LoadingScreen import net.taler.wallet.compose.RetryScreen import net.taler.wallet.main.MainViewModel import net.taler.wallet.main.TAG +import net.taler.wallet.payment.PayStatus import net.taler.wallet.refund.RefundStatus import java.io.IOException import java.net.HttpURLConnection @@ -40,6 +41,7 @@ fun HandleUriScreen( var processing by remember { mutableStateOf(false) } var errorInfo by remember { mutableStateOf<TalerErrorInfo?>(null) } val networkStatus by model.networkManager.networkStatus.observeAsState() + val payStatus by model.paymentManager.payStatus.observeAsState(PayStatus.None) val scope = rememberCoroutineScope() fun processTalerUri() { @@ -80,7 +82,7 @@ fun HandleUriScreen( action.startsWith("pay/", ignoreCase = true) -> { model.paymentManager.preparePay(u2)?.let { transactionId -> if (model.transactionManager.selectTransaction(transactionId)) { - onNavigate(WalletDestination.TransactionPayment, true) + onNavigate(WalletDestination.TransactionPayment(promptMode = true), true) } } } @@ -153,6 +155,11 @@ fun HandleUriScreen( } } + LaunchedEffect(payStatus) { + val error = (payStatus as? PayStatus.Pending)?.error ?: return@LaunchedEffect + errorInfo = error + } + Box(Modifier.fillMaxSize()) { if (networkStatus == true) { LoadingScreen() diff --git a/wallet/src/main/java/net/taler/wallet/WalletNavHost.kt b/wallet/src/main/java/net/taler/wallet/WalletNavHost.kt @@ -264,10 +264,11 @@ fun WalletNavHost( onNavigateBack = onNavigateBack, ) } - composable<WalletDestination.TransactionPayment> { + composable<WalletDestination.TransactionPayment> { backStackEntry -> + val dest = backStackEntry.toRoute<WalletDestination.TransactionPayment>() TransactionDetailScreen( model = model, - destination = WalletDestination.TransactionPayment, + destination = dest, onNavigate = onNavigate, onNavigateBack = onNavigateBack, ) diff --git a/wallet/src/main/java/net/taler/wallet/WalletNavigation.kt b/wallet/src/main/java/net/taler/wallet/WalletNavigation.kt @@ -104,7 +104,7 @@ sealed interface WalletDestination { @Serializable data object TransactionWithdrawal : WalletDestination @Serializable - data object TransactionPayment : WalletDestination + data class TransactionPayment(val promptMode: Boolean = false) : WalletDestination @Serializable data object TransactionRefund : WalletDestination @Serializable diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateComposable.kt @@ -54,6 +54,9 @@ fun PayTemplateComposable( if (usableCurrencies.isEmpty()) { // If user doesn't have any supported currency, they can't pay either PayTemplateError(stringResource(R.string.payment_balance_insufficient)) + } else if (!p.details.isTemplateEditable(usableCurrencies)) { + // Non-editable: auto-preparing, show loading instead of flashing the form + PayTemplateLoading() } else { PayTemplateOrderComposable( usableCurrencies = usableCurrencies, diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt @@ -64,7 +64,7 @@ fun PayTemplateScreen( model.paymentManager.preparePayForTemplate(uri, params) ?.let { transactionId -> if (model.transactionManager.selectTransaction(transactionId)) { - onNavigate(WalletDestination.TransactionPayment, true) + onNavigate(WalletDestination.TransactionPayment(promptMode = true), true) } } } @@ -80,7 +80,7 @@ fun PayTemplateScreen( when (val s = payStatus) { is PayStatus.Prepared -> { if (transactionManager.selectTransaction(s.transactionId)) { - onNavigate(WalletDestination.TransactionPayment, true) + onNavigate(WalletDestination.TransactionPayment(promptMode = true), true) } } diff --git a/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt @@ -20,13 +20,11 @@ import android.graphics.Bitmap import androidx.compose.foundation.Image import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Card -import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -66,7 +64,6 @@ import net.taler.wallet.transactions.TransactionInfoComposable import net.taler.wallet.transactions.TransactionLinkComposable import net.taler.wallet.transactions.TransactionMajorState import net.taler.wallet.transactions.TransactionMajorState.Pending -import net.taler.wallet.transactions.TransactionMinorState import net.taler.wallet.transactions.TransactionPayment import net.taler.wallet.transactions.TransactionState import net.taler.wallet.transactions.TransactionStateComposable @@ -78,6 +75,7 @@ fun TransactionPaymentComposable( payStatus: PayStatus, devMode: Boolean, spec: CurrencySpecification?, + promptMode: Boolean = false, modifier: Modifier = Modifier, onFulfill: (url: String) -> Unit, onTransition: (t: TransactionAction) -> Unit, @@ -85,7 +83,7 @@ fun TransactionPaymentComposable( onSetupDonau: (donauBaseUrl: String) -> Unit, checkDonauForChoice: suspend (PayChoiceDetails) -> DonauStatus?, ) { - if (t.txState.major == TransactionMajorState.Dialog) { + if (t.txState.major == TransactionMajorState.Dialog || (promptMode && t.txState.major == Pending)) { return TransactionPaymentPrompt( payStatus = payStatus, onConfirmPay = onConfirmPay, @@ -110,37 +108,27 @@ fun TransactionPaymentComposable( style = MaterialTheme.typography.bodyLarge, ) - if (t.txState.minor == TransactionMinorState.ClaimProposal) { - CircularProgressIndicator( - modifier = Modifier - .padding(45.dp) - .fillMaxWidth() - .aspectRatio(1f) - .align(CenterHorizontally) - ) - } else { - TransactionAmountComposable( - label = stringResource(id = R.string.transaction_order_total), - amount = t.amountRaw.withSpec(spec), - amountType = AmountType.Neutral, - ) - - if (t.amountEffective > t.amountRaw) { - val fee = t.amountEffective - t.amountRaw - TransactionAmountComposable( - label = stringResource(id = R.string.amount_fee), - amount = fee.withSpec(spec), - amountType = AmountType.Negative, - ) - } + TransactionAmountComposable( + label = stringResource(id = R.string.transaction_order_total), + amount = t.amountRaw.withSpec(spec), + amountType = AmountType.Neutral, + ) + if (t.amountEffective > t.amountRaw) { + val fee = t.amountEffective - t.amountRaw TransactionAmountComposable( - label = stringResource(id = R.string.transaction_paid), - amount = t.amountEffective.withSpec(spec), + label = stringResource(id = R.string.amount_fee), + amount = fee.withSpec(spec), amountType = AmountType.Negative, ) } + TransactionAmountComposable( + label = stringResource(id = R.string.transaction_paid), + amount = t.amountEffective.withSpec(spec), + amountType = AmountType.Negative, + ) + if (t.posConfirmation != null) PayTotpComposable( totpString = t.posConfirmation, enableNfc = t.posConfirmationViaNfc == true, @@ -171,7 +159,8 @@ fun TransactionPaymentPrompt( when (val status = payStatus) { is PayStatus.None, is PayStatus.Loading, - is PayStatus.Prepared -> LoadingScreen() + is PayStatus.Prepared, + is PayStatus.Checked -> LoadingScreen() is PayStatus.Choices -> PromptPaymentComposable( status = status, onConfirm = { index, useDonau -> diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailScreen.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionDetailScreen.kt @@ -67,6 +67,7 @@ import net.taler.wallet.backend.TalerErrorCode import net.taler.wallet.backend.TalerErrorInfo 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.deposit.TransactionDepositComposable import net.taler.wallet.launchInAppBrowser @@ -121,6 +122,10 @@ fun TransactionDetailScreen( onNavigateBack = onNavigateBack, ) { paddingValues -> val t by transactionManager.selectedTransaction.collectAsStateLifecycleAware() + if (t == null) { + LoadingScreen() + return@GlobalScaffold + } val modifier = Modifier .fillMaxSize() .padding(paddingValues) @@ -139,6 +144,7 @@ fun TransactionDetailScreen( payStatus = model.paymentManager.payStatus .observeAsState(PayStatus.None).value, devMode = devMode, + promptMode = destination.promptMode, spec = exchangeManager.getSpecForCurrency(tx.amountRaw.currency, tx.scopes), modifier = modifier, onFulfill = { url -> diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionStateComposable.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionStateComposable.kt @@ -47,6 +47,7 @@ import net.taler.wallet.transactions.TransactionMajorState.Pending import net.taler.wallet.transactions.TransactionMajorState.Suspended import net.taler.wallet.transactions.TransactionMinorState.BalanceKycRequired import net.taler.wallet.transactions.TransactionMinorState.BankConfirmTransfer +import net.taler.wallet.transactions.TransactionMinorState.ClaimProposal import net.taler.wallet.transactions.TransactionMinorState.KycAuthRequired import net.taler.wallet.transactions.TransactionMinorState.KycInit import net.taler.wallet.transactions.TransactionMinorState.KycRequired @@ -67,6 +68,7 @@ fun TransactionStateComposable( TransactionState(Pending, KycRequired) -> stringResource(R.string.transaction_state_pending_kyc_bank) TransactionState(Pending, BalanceKycRequired) -> stringResource(R.string.transaction_state_pending_kyc_bank) TransactionState(Pending, KycAuthRequired) -> stringResource(R.string.transaction_state_pending_kyc_auth) + TransactionState(Pending, ClaimProposal) -> stringResource(R.string.transaction_state_pending) TransactionState(Pending) -> stringResource(R.string.transaction_state_pending) TransactionState(Aborted) -> if (tx is TransactionWithdrawal && tx.withdrawalDetails is ManualTransfer) { stringResource( diff --git a/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt b/wallet/src/main/java/net/taler/wallet/transactions/Transactions.kt @@ -451,7 +451,7 @@ class TransactionPayment( val posConfirmationViaNfc: Boolean? = null, ) : Transaction() { override val icon = R.drawable.transaction_payment - override val detailPageNav = WalletDestination.TransactionPayment + override val detailPageNav = WalletDestination.TransactionPayment() @Transient override val amountType = AmountType.Negative