commit 79848a0448d8c9551e7b73ca144a569d475d8735
parent e6334cdc426658961be1990331a03080805e9f60
Author: Iván Ávalos <avalos@disroot.org>
Date: Wed, 26 Aug 2026 17:19:17 +0200
[wallet] show claim order error even when wallet-core deletes the transaction
Diffstat:
7 files changed, 80 insertions(+), 60 deletions(-)
diff --git a/wallet/src/main/java/net/taler/wallet/HandleUriScreen.kt b/wallet/src/main/java/net/taler/wallet/HandleUriScreen.kt
@@ -158,11 +158,6 @@ 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/compose/ErrorComposable.kt b/wallet/src/main/java/net/taler/wallet/compose/ErrorComposable.kt
@@ -58,6 +58,7 @@ fun ErrorComposable(
devMode: Boolean,
message: String? = null,
scrollable: Boolean = true,
+ onRetry: (() -> Unit)? = null,
onClose: (() -> Unit)? = null,
) {
val scrollState = rememberScrollState()
@@ -143,6 +144,13 @@ fun ErrorComposable(
}
}
+ if (onRetry != null) Button(
+ modifier = Modifier.padding(bottom = 16.dp),
+ onClick = onRetry,
+ ) {
+ Text(text = stringResource(R.string.transactions_retry))
+ }
+
if (onClose != null) Button(
modifier = Modifier.padding(bottom = 16.dp),
onClick = onClose,
diff --git a/wallet/src/main/java/net/taler/wallet/main/MainViewModel.kt b/wallet/src/main/java/net/taler/wallet/main/MainViewModel.kt
@@ -165,6 +165,7 @@ class MainViewModel(
}
is NotificationPayload.TransactionStateTransition -> {
+ paymentManager.onTransactionStateTransition(payload)
viewModelScope.launch(Dispatchers.Main) {
payload.transactionId?.let { id ->
// update currently selected transaction
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,36 +16,30 @@
package net.taler.wallet.payment
-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
-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.CurrencySpecification
import net.taler.wallet.R
+import net.taler.wallet.backend.TalerErrorInfo
+import net.taler.wallet.compose.ErrorComposable
import net.taler.wallet.compose.LoadingScreen
import net.taler.wallet.compose.TalerSurface
-import net.taler.wallet.systemBarsPaddingBottom
@Composable
fun PayTemplateComposable(
currencies: List<String>,
payStatus: PayStatus,
+ devMode: Boolean = false,
getCurrencySpec: (String) -> CurrencySpecification?,
onSubmit: (params: TemplateParams) -> Unit,
onRetry: (() -> Unit)? = null,
) {
if (currencies.isEmpty()) {
- PayTemplateError(stringResource(R.string.payment_balance_insufficient))
+ ErrorComposable(
+ error = TalerErrorInfo.makeCustomError(stringResource(R.string.payment_balance_insufficient)),
+ devMode = devMode,
+ )
} else when (val p = payStatus) {
is PayStatus.Checked -> {
val usableCurrencies = currencies
@@ -53,7 +47,10 @@ fun PayTemplateComposable(
.toList()
if (usableCurrencies.isEmpty()) {
// If user doesn't have any supported currency, they can't pay either
- PayTemplateError(stringResource(R.string.payment_balance_insufficient))
+ ErrorComposable(
+ error = TalerErrorInfo.makeCustomError(stringResource(R.string.payment_balance_insufficient)),
+ devMode = devMode,
+ )
} else if (!p.details.isTemplateEditable(usableCurrencies)) {
// Non-editable: auto-preparing, show loading instead of flashing the form
PayTemplateLoading()
@@ -67,17 +64,16 @@ fun PayTemplateComposable(
}
}
is PayStatus.None, is PayStatus.Loading -> PayTemplateLoading()
- 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.Error -> ErrorComposable(
+ error = p.error,
+ message = stringResource(R.string.payment_template_error),
+ devMode = devMode,
onRetry = onRetry,
)
is PayStatus.Prepared -> PayTemplateLoading()
// not emitted by template flow
+ is PayStatus.Pending,
is PayStatus.AlreadyPaid,
is PayStatus.InsufficientBalance,
is PayStatus.Success,
@@ -86,37 +82,6 @@ fun PayTemplateComposable(
}
@Composable
-fun PayTemplateError(
- message: String,
- onRetry: (() -> Unit)? = null,
-) {
- Column(
- modifier = Modifier
- .padding(16.dp)
- .fillMaxSize()
- .systemBarsPaddingBottom(),
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center,
- ) {
- Text(
- text = message,
- style = MaterialTheme.typography.titleLarge,
- 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))
- }
- }
- }
-}
-
-@Composable
fun PayTemplateLoading() {
LoadingScreen()
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt b/wallet/src/main/java/net/taler/wallet/payment/PayTemplateScreen.kt
@@ -84,9 +84,7 @@ fun PayTemplateScreen(
}
}
- is PayStatus.Pending -> if (s.error != null) {
- onShowError(s.error)
- }
+ is PayStatus.Error -> onShowError(s.error)
is PayStatus.Checked -> {
val usableCurrencies = balanceManager.getCurrencies()
@@ -115,6 +113,7 @@ fun PayTemplateScreen(
is BalanceState.Success -> PayTemplateComposable(
currencies = state.balances.map { it.currency },
payStatus = payStatus,
+ devMode = devMode,
onSubmit = { params ->
scope.launch { prepareTemplate(uri, params) }
},
diff --git a/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt b/wallet/src/main/java/net/taler/wallet/payment/PaymentManager.kt
@@ -30,6 +30,7 @@ import net.taler.common.ContractTerms
import net.taler.common.TalerUtils.getLocalizedString
import net.taler.wallet.main.TAG
import net.taler.wallet.backend.BackendManager
+import net.taler.wallet.backend.NotificationPayload
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.backend.WalletBackendApi
import net.taler.wallet.backend.WalletResponse
@@ -40,6 +41,7 @@ import net.taler.wallet.exchanges.ExchangeManager
import org.json.JSONObject
import net.taler.wallet.payment.GetChoicesForPaymentResponse.ChoiceSelectionDetail
import net.taler.wallet.payment.GetChoicesForPaymentResponse.ChoiceSelectionDetail.PaymentPossible
+import net.taler.wallet.transactions.TransactionMajorState
sealed class PayStatus {
data object None : PayStatus()
@@ -75,6 +77,11 @@ sealed class PayStatus {
val transactionId: String? = null,
val error: TalerErrorInfo? = null,
) : PayStatus()
+
+ data class Error(
+ val error: TalerErrorInfo,
+ ) : PayStatus()
+
data class Success(
val transactionId: String,
val automaticExecution: Boolean,
@@ -110,6 +117,15 @@ class PaymentManager(
private val mPayStatus = MutableLiveData<PayStatus>(PayStatus.None)
internal val payStatus: LiveData<PayStatus> = mPayStatus
+ /**
+ * Transaction id of the currently prepared payment. Watched for
+ * transaction-state-transition notifications so that a transaction
+ * deleted by wallet-core (e.g. after a failed claim) surfaces an
+ * error in the UI instead of leaving the user on a loading screen.
+ */
+ @Volatile
+ private var currentTransactionId: String? = null
+
suspend fun preparePay(url: String): String? {
var transactionId: String? = null
api.request("preparePayForUriV2", PreparePayV2Response.serializer()) {
@@ -117,6 +133,7 @@ class PaymentManager(
}.onError {
handleError("preparePayForUriV2", it)
}.onSuccess { response ->
+ currentTransactionId = response.transactionId
transactionId = response.transactionId
}
return transactionId
@@ -282,14 +299,35 @@ class PaymentManager(
}.onError {
handleError("preparePayForTemplate", it)
}.onSuccess { response ->
+ currentTransactionId = response.transactionId
transactionId = response.transactionId
}
return transactionId
}
+ /**
+ * Called on transaction-state-transition notifications. If the currently
+ * prepared transaction is deleted by wallet-core (e.g. claim failed), the
+ * UI is taken out of its loading state and shown the error instead.
+ */
+ fun onTransactionStateTransition(payload: NotificationPayload.TransactionStateTransition) {
+ val transactionId = payload.transactionId
+ if (transactionId == null || transactionId != currentTransactionId) return
+ val newTxState = payload.newTxState ?: return
+ if (newTxState.major != TransactionMajorState.Deleted) return
+ currentTransactionId = null
+ val errorInfo = payload.errorInfo
+ if (errorInfo == null) {
+ Log.e(TAG, "prepared transaction $transactionId deleted by wallet-core without error info")
+ return
+ }
+ Log.e(TAG, "prepared transaction $transactionId deleted by wallet-core: $errorInfo")
+ mPayStatus.postValue(PayStatus.Error(error = errorInfo))
+ }
+
private fun handleError(operation: String, error: TalerErrorInfo) {
Log.e(TAG, "got $operation error result $error")
- mPayStatus.postValue(PayStatus.Pending(error = error))
+ mPayStatus.postValue(PayStatus.Error(error = error))
}
}
diff --git a/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt
@@ -50,6 +50,7 @@ import net.taler.wallet.R
import net.taler.wallet.backend.TalerErrorCode
import net.taler.wallet.backend.TalerErrorInfo
import net.taler.wallet.balances.ScopeInfo
+import net.taler.wallet.compose.ErrorComposable
import net.taler.wallet.compose.LoadingScreen
import net.taler.wallet.compose.TalerSurface
import net.taler.wallet.transactions.AmountType
@@ -92,6 +93,8 @@ fun TransactionPaymentComposable(
) {
return TransactionPaymentPrompt(
payStatus = payStatus,
+ devMode = devMode,
+ modifier = modifier,
onConfirmPay = onConfirmPay,
onAbortPay = { onTransition(Abort) },
onSetupDonau = onSetupDonau,
@@ -156,6 +159,8 @@ fun TransactionPaymentComposable(
@Composable
fun TransactionPaymentPrompt(
payStatus: PayStatus,
+ devMode: Boolean,
+ modifier: Modifier = Modifier,
onConfirmPay: (Int?, useDonau: Boolean) -> Unit,
onAbortPay: () -> Unit,
onSetupDonau: (donauBaseUrl: String) -> Unit,
@@ -187,6 +192,15 @@ fun TransactionPaymentPrompt(
onSetupDonau(donauBaseUrl)
},
)
+
+ is PayStatus.Error -> ErrorComposable(
+ error = status.error,
+ modifier = modifier,
+ devMode = devMode,
+ message = stringResource(R.string.payment_template_error),
+ scrollable = false,
+ )
+
else -> {}
}