From 2684ffef54d723e2c36526e5aa3f4586a5340fc7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 17 Jun 2026 19:28:21 +0000 Subject: [PATCH] fix: keep Cashu receive invoice on screen during mint polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01GjNqJZFxQinvuR4sGwpWQR --- .../loggedIn/wallet/CashuWalletScreen.kt | 12 ++++++++- .../loggedIn/wallet/CashuWalletViewModel.kt | 26 +++++++++++++++---- amethyst/src/main/res/values/strings.xml | 1 + 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt index 1c700dae95..ade2f60022 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletScreen.kt @@ -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, ) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletViewModel.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletViewModel.kt index 789dc1b927..d625617eea 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletViewModel.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/wallet/CashuWalletViewModel.kt @@ -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) { diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 3acb23326d..b4900926d9 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2240,6 +2240,7 @@ Done Asking mint for an invoice… Waiting for the invoice to be paid… + Checking the mint… Issuing proofs… Paying via mint… Swapping proofs…