fix: keep Cashu receive invoice on screen during mint polling

The Receive dialog polls the mint every 3s to see if the bolt11 has
been paid. Each poll flipped the flow state AwaitingPayment ->
Completing -> AwaitingPayment, and since the dialog renders a totally
different body for Completing (an "issuing proofs" spinner, no invoice,
no buttons), the invoice view was replaced by a spinner and then
recreated every 3 seconds — a constant flicker.

Add a `checking` flag to AwaitingPayment instead. The routine poll now
stays in AwaitingPayment and only toggles that flag, so the invoice (and
the Discard button) remain on screen and the dialog just swaps its
status line between "Waiting for the invoice to be paid…" and "Checking
the mint…". The full Completing body is shown only once payment is
actually confirmed and proofs are being issued.

The compareAndSet gate that prevents two concurrent polls from both
reaching completeMintFromLightning is preserved (now gating on
checking=false -> checking=true), with an extra early-out when a check
is already in flight.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GjNqJZFxQinvuR4sGwpWQR
This commit is contained in:
Claude
2026-06-17 19:30:20 +00:00
parent fa5d126ba7
commit 2684ffef54
3 changed files with 33 additions and 6 deletions
@@ -793,11 +793,21 @@ private fun ReceiveDialog(
Text(stringRes(R.string.cashu_copy_invoice))
}
Spacer(modifier = Modifier.height(4.dp))
// The mint is polled every 3s in the background. We
// keep the invoice on screen and only toggle this
// status line between "waiting" and "checking" so the
// dialog never swaps its whole body mid-poll.
Row(verticalAlignment = Alignment.CenterVertically) {
CircularProgressIndicator(modifier = Modifier.size(14.dp), strokeWidth = 2.dp)
Spacer(modifier = Modifier.width(8.dp))
Text(
stringRes(R.string.cashu_waiting_for_payment),
stringRes(
if (s.checking) {
R.string.cashu_checking_mint
} else {
R.string.cashu_waiting_for_payment
},
),
style = MaterialTheme.typography.bodySmall,
)
}
@@ -62,6 +62,14 @@ sealed class CashuMintFlowState {
val flow: MintQuoteStarted,
val mintUrl: String,
val amountSats: Long,
/**
* True while a background poll is asking the mint whether the
* invoice has been paid. The receive dialog keeps the invoice on
* screen and shows a small inline indicator instead of swapping to
* the [Completing] body, so the routine 3s check no longer flickers
* the whole dialog.
*/
val checking: Boolean = false,
) : CashuMintFlowState()
data object Completing : CashuMintFlowState()
@@ -419,21 +427,29 @@ class CashuWalletViewModel : ViewModel() {
fun checkAndCompleteMint() {
val vm = accountViewModel ?: return
val current = _mintState.value as? CashuMintFlowState.AwaitingPayment ?: return
// Atomic flip to Completing so a concurrent poll (the receive
// Already polling — don't stack a second request.
if (current.checking) return
// Atomic flip to checking=true so a concurrent poll (the receive
// dialog fires this every 3s) can't both reach
// completeMintFromLightning. Without the gate, poll 1 consumed
// the mint quote and poll 2 hit "outputs already signed".
if (!_mintState.compareAndSet(current, CashuMintFlowState.Completing)) return
// the mint quote and poll 2 hit "outputs already signed". We stay
// in AwaitingPayment so the invoice keeps showing — the dialog
// renders an inline "checking the mint" indicator off `checking`
// instead of swapping its whole body, which used to flicker.
if (!_mintState.compareAndSet(current, current.copy(checking = true))) return
vm.launchSigner {
try {
val status = ops.checkMintQuote(current.mintUrl, current.flow.mintQuote.quote)
val paid = status.isSettled()
if (!paid) {
// Roll back to AwaitingPayment so the polling
// LaunchedEffect picks up again on the next tick.
// Clear the checking flag so the polling LaunchedEffect
// picks up again on the next tick, invoice still on screen.
_mintState.value = current
return@launchSigner
}
// Payment confirmed — now it's worth showing the full
// "issuing proofs" body while we finalize the mint.
_mintState.value = CashuMintFlowState.Completing
ops.completeMintFromLightning(current.mintUrl, current.flow.quoteEvent, current.amountSats)
_mintState.value = CashuMintFlowState.Completed(current.amountSats)
} catch (e: Exception) {
+1
View File
@@ -2240,6 +2240,7 @@
<string name="cashu_done">Done</string>
<string name="cashu_requesting_invoice">Asking mint for an invoice…</string>
<string name="cashu_waiting_for_payment">Waiting for the invoice to be paid…</string>
<string name="cashu_checking_mint">Checking the mint…</string>
<string name="cashu_completing_mint">Issuing proofs…</string>
<string name="cashu_paying_invoice">Paying via mint…</string>
<string name="cashu_building_token">Swapping proofs…</string>