taler-android

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

commit 6f5fb297f18ab98ad6e40ec8ddc5895e98f78cd0
parent 88f1f73d949e3cbbf2f0dabc2b933044caf60e2d
Author: Iván Ávalos <avalos@disroot.org>
Date:   Sun, 16 Aug 2026 22:54:39 +0200

[wallet] fix tx state race condition on payment

Diffstat:
Mwallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt | 8+++++++-
Mwallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt | 49++++++++++++++++++++++++++++++++++---------------
2 files changed, 41 insertions(+), 16 deletions(-)

diff --git a/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt b/wallet/src/main/java/net/taler/wallet/payment/TransactionPaymentComposable.kt @@ -64,6 +64,7 @@ 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 @@ -83,7 +84,12 @@ fun TransactionPaymentComposable( onSetupDonau: (donauBaseUrl: String) -> Unit, checkDonauForChoice: suspend (PayChoiceDetails) -> DonauStatus?, ) { - if (t.txState.major == TransactionMajorState.Dialog || (promptMode && t.txState.major == Pending)) { + if (t.txState.major == TransactionMajorState.Dialog || ( + promptMode && + t.txState.major == Pending && + t.txState.minor == TransactionMinorState.ClaimProposal + ) + ) { return TransactionPaymentPrompt( payStatus = payStatus, onConfirmPay = onConfirmPay, diff --git a/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt b/wallet/src/main/java/net/taler/wallet/transactions/TransactionManager.kt @@ -23,6 +23,8 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.json.encodeToJsonElement @@ -77,6 +79,8 @@ class TransactionManager( private val allTransactions = HashMap<ScopeInfo, List<Transaction>>() private val mTransactions = HashMap<ScopeInfo, MutableStateFlow<TransactionsResult>>() private val mSelectedTransaction = MutableStateFlow<Transaction?>(null) + private val mSelectedTransactionId = MutableStateFlow<String?>(null) + private val selectionMutex = Mutex() private val mSelectedTransferOption = MutableStateFlow<TransferOption?>(null) val selectedTransaction = mSelectedTransaction.asStateFlow() @@ -177,14 +181,29 @@ class TransactionManager( /** * Returns true if given [transactionId] was found and selected, false otherwise. + * + * The [transactionId] is recorded as selected before the transaction is fetched, + * so that a notification arriving while the fetch is in flight is not dropped. + * The initial fetch and all notification-driven refreshes are serialized via + * [selectionMutex] so that an older snapshot cannot overwrite a newer one. */ suspend fun selectTransaction(transactionId: String): Boolean { - val transaction = getTransactionById(transactionId) - if (transaction != null) { - mSelectedTransaction.emit(transaction) - return true - } else { - return false + mSelectedTransactionId.value = transactionId + return refreshSelectedTransaction() + } + + private suspend fun refreshSelectedTransaction(): Boolean { + val transactionId = mSelectedTransactionId.value ?: return false + return selectionMutex.withLock { + val transaction = getTransactionById(transactionId) + if (transaction != null) { + if (mSelectedTransactionId.value == transactionId) { + mSelectedTransaction.value = transaction + } + true + } else { + false + } } } @@ -194,18 +213,18 @@ class TransactionManager( @UiThread fun updateTransactionIfSelected(id: String) = scope.launch { - val selectedTransaction = selectedTransaction.value - if (selectedTransaction?.transactionId != id) return@launch - getTransactionById(id)?.let { tx -> - if (tx.transactionId == selectedTransaction.transactionId) { - Log.d(TAG, "updating selected transaction: ${tx.transactionId}") - mSelectedTransaction.value = tx - } - } ?: Log.d(TAG, "Error updating selected transaction $id") + if (mSelectedTransactionId.value != id) return@launch + refreshSelectedTransaction() } fun selectTransaction(tx: Transaction?) = scope.launch { - mSelectedTransaction.value = tx + if (tx == null) { + mSelectedTransactionId.value = null + mSelectedTransaction.value = null + } else { + mSelectedTransactionId.value = tx.transactionId + mSelectedTransaction.value = tx + } } fun deleteTransaction(transactionId: String, onError: (it: TalerErrorInfo) -> Unit) =