feat(ui): branded payment progress overlay + global incoming-payment detection#135
Merged
Conversation
Replace the bare Alert.alert "Payment Sent" native dialog (white square with no rounding) with a rounded on-brand overlay that also fills the gap between "Send" press and "Sent" — the user now sees a sending state with animated feedback, then a clear success tick. Send flow: - "Sending payment…" spinner + amount + rounded card - Pink bubbles rise from the bottom of the screen behind the card; quadratic stagger over 5s ramps density from sparse → packed - On success, bubbles morph to green and a big green tick swaps in - Auto-dismisses 2.2s after success - On failure, shows red X + error message (user can retry or dismiss) Receive flow: - When the receive sheet detects balance has gone up, the same overlay fires in "receive" mode: on-brand confetti (pinks, blues, purples) cascades behind a "Payment received!" card with the received amount Built with react-native-reanimated (already a dep) — no new packages. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses overlay refinement feedback: - Require user acknowledgement: removed the auto-dismiss timer. The success / error card now shows an explicit "OK" (or "Dismiss") button. If the user wasn't looking at the phone when a receive arrived they still see the outcome whenever they come back. - Radial confetti burst: on receive success, confetti now launches from the card centre outward using a projectile-with-gravity equation (v0·t + ½·g·t²), with random angles across the full circle and a slight upward bias so it reads as an explosion rather than a dribble. A 280 ms delay after state → success lets the card visibly spring in first, so pieces burst from *behind* it. - Bigger bubbles on send: min/max bumped from 10–44 px to 22–68 px so the sending state reads more clearly at a glance. - Confetti sits above the card on the receive flow so pieces visibly fly past the card edges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Later siblings stack above earlier ones in React Native. The confetti layer was rendered AFTER the card, so pieces flew over the top of it rather than bursting out from behind. Moved the particle layer to be the first sibling inside the root View — the card now overlays it and confetti visibly erupts from behind the card as intended. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CONFETTI_COUNT 90 → 135. The burst now reads more as a full celebration than a scattered pop, especially at the edges of the screen where trajectories fan out thin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The receive sheet had a 5 s poll interval, giving up to 5 s of wait on top of the NWC round-trip before we noticed a settled invoice. Dropping to 1.5 s shaves ~3-4 s off the worst-case latency vs. other wallets (e.g. Wallet of Satoshi confirms several seconds faster because it uses a native push/subscription pipeline, not polling). NIP-47 notifications would eliminate polling entirely, but the LNbits NWC provider extension we use (riccardobl/nwcprovider) does not implement kind 23196 `payment_received` / `payment_sent` events — its info event advertises methods but not a notifications capability, and the subscription only listens for 23194/23195. Tracked as follow-up; until then, short-poll is the pragmatic fallback that also covers non-LNbits NWC backends without notification support. The interval is cleared the moment the balance increment is detected or the sheet closes, so battery/network cost is bounded to the window where the user is actively waiting for a payment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs made the celebration overlay misfire: 1. Baseline was seeded from the cached balance at sheet-open. If a prior invoice settled while the app was backgrounded, the first poll after open would see balance > cached and fire a "received" celebration for an unrelated earlier payment. Fix: ignore the cache and treat the FIRST balance we observe after open (or wallet-switch, or invoice-regenerate) as the baseline. Pending credits from previous invoices get absorbed into the baseline instead of misfiring. 2. After the celebration fired, the interval was cleared and prevBalance was not advanced to the post-credit amount. If the user then created a second invoice without closing the sheet, polling stopped entirely and the real new payment was missed. Fix: on fire, advance prevBalance to the new balance and leave polling running; clearing the interval on payment detection was load-bearing only for the since-removed auto-dismiss flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The payment-received celebration was previously rendered inside
ReceiveSheet, so it only fired when the user had that sheet open with
an amount-specific invoice generated. A payment to the user's lightning
address while on Home, or a zap landing while chatting in Conversation,
silently landed without a confetti pop.
Hoist the detection + overlay render up to the app root:
- WalletContext maintains per-wallet balance baselines in a ref and
exposes \`lastIncomingPayment\` on the context. Any balance increase
on any connected wallet fires the event with \`{ walletId, amountSats,
walletAlias }\`. Decrements (sends) silently re-baseline — a send is
not a "received" event.
- App.tsx mounts a single \`GlobalIncomingPaymentOverlay\` that reads
the event and renders \`PaymentProgressOverlay\` over whatever screen
is active. The existing ReceiveSheet-specific overlay is removed.
- WalletContext also runs a 10 s foreground poll of the active
wallet's balance so lightning-address payments get noticed even when
the user isn't on ReceiveSheet. The existing 1.5 s ReceiveSheet poll
stays for fast in-flow detection; on-chain is skipped (BDK sync is
expensive and runs on its own cadence). Paused via AppState when
backgrounded.
Out of scope here: OS push notifications for payments that arrive
while the app is backgrounded or the phone is locked — that needs
FCM / APNs plus a backend relay, tracked in #45.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 1.5 s ReceiveSheet poll ran forever while the sheet was open, so a user who generated an invoice and walked away kept a steady 1.5 s network heartbeat going — battery and relay traffic for nothing. Now: after 3 minutes we clear the aggressive interval, and the global WalletContext 10 s foreground poll continues to catch payments from then on (just slightly slower). Most lightning invoices settle within seconds so this window is comfortably long for the happy path, and the fallback means a late payment still pops the celebration — just on the 10 s cadence instead of 1.5 s. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A 10 s foreground poll against NWC burns ~360 relay round-trips per hour per user — not a sensible baseline for the "no payment expected" common case. Replace with a single refresh whenever AppState flips to 'active', which catches anything that arrived while the app was backgrounded at near-zero steady-state cost. Detection coverage after this change: - Actively waiting (ReceiveSheet open, invoice created): 1.5 s poll for up to 3 min (unchanged). - App resumed from background: one-shot refresh on the foreground transition — enough to pop the celebration for anything that settled while the phone was away. - Organic refreshes (pull-to-refresh, post-send sync, tab switches): still feed the detector — whenever balance increments, overlay fires. Lightning-address / zap / on-chain receives that land while the app is in foreground on a non-ReceiveSheet screen and with no other refresh triggered are now detected on the user's next organic interaction rather than within 10 s. Acceptable trade — true background delivery needs OS push (issue #45) regardless. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closer the gap vs. WoS by polling the SPECIFIC invoice's settled state via NIP-47 \`lookup_invoice\` rather than polling wallet-wide balance: - LNbits flips \`settled_at\` the instant LND signals settle — no wait for balance aggregation / cache propagation. - Targeted request (one invoice) vs. balance roll-up (full wallet totals) — lighter and less prone to slow responses on busy backends. - On settle, fall through to a one-shot \`refreshBalanceForWallet\` call so the WalletContext diff-detector still fires the overlay — single source of truth for "balance went up ⇒ celebrate". - Poll cadence tightened 1500 ms → 1000 ms (lookup_invoice is a lighter request than get_balance on most backends). - Fallback to balance polling retained for the rare case where the bolt11 doesn't yield an extractable payment_hash. Remaining gap vs. WoS is architectural: WoS talks to its own LN node directly with push/subscription; we go through LNbits + NWC + a Nostr relay, so there's always some round-trip floor. The remaining cure-all is NIP-47 notifications (tracked: lnbits/nwcprovider#30). Also restrict amount inputs in Send and Receive sheets to digits only (sats) or digits + single decimal (fiat). Hardware keyboards, paste, and autocomplete can bypass the soft-keyboard numeric hint and inject junk characters that caused silent "Invalid amount" rejections. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Payment-received detection was silent when the NWC backend (LNbits in our case) was slow to answer get_balance — requests timed out and the WalletContext diff-detector never saw the new balance. The previous implementation early-returned from the lookup-only path, so the balance refresh never got a chance to land. Now we fire BOTH on every tick: - \`lookup_invoice\` against the specific payment hash (fastest signal) - \`refreshBalanceForWallet\` which feeds the global diff-detector \`Promise.allSettled\` so one failing doesn't block the other. Whichever succeeds first drives detection. The lookup's \`paid=true\` response still stops the poll loop early. Add dev-mode logs: - \`[Receive] starting 1 s poll · paymentHash=…\` at generateInvoice - \`[Receive] lookup_invoice reports PAID — stopping poll\` on settle - \`[Wallet] incoming payment detected: +X sats on …\` in the context diff-detector Makes it obvious from logcat whether bolt11 decoding yielded a hash, whether lookup_invoice is firing, and whether the detector actually saw a delta. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The balance diff detector flipped \`lastIncomingPayment\` (driving the confetti overlay) but did not touch the wallet's cached transaction list. After dismissing the overlay the new tx wouldn't appear on Home or the Transactions screen until the user manually pull-to-refreshed or triggered something else that ran \`fetchTransactionsForWallet\`. Now a dedicated effect watches \`lastIncomingPayment\` and triggers a tx-list refresh on the affected wallet as soon as detection fires — so by the time the user taps OK on the celebration, the transaction row is already there. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…erage Two related improvements to incoming-payment detection: 1. The 1 s fast poll for a specific invoice is now owned by the WalletContext via a new \`expectPayment(walletId, paymentHash)\` API. ReceiveSheet calls it when an invoice is generated, then doesn't care whether the sheet stays mounted — the poll lives on the provider and survives sheet-close, navigation, wallet switching the receive sheet, etc. User flow enabled: generate invoice → share QR with a friend → close the sheet → go chat in Friends → when the friend pays, the confetti + "Payment received!" card still pops, globally. Stops early on \`paid:true\` from \`lookup_invoice\`, caps at 3 minutes, and a subsequent \`expectPayment\` replaces the in-flight expectation (one at a time; the balance-diff detector still catches the displaced invoice when it eventually settles). 2. Re-introduce a slow (30 s) baseline poll for the active NWC wallet while the app is foregrounded. Previously removed as wasteful, but necessary to fire the overlay on lightning-address payments that arrive without an in-app invoice-generation trigger. Worst-case latency ~30 s vs. 1 s during active wait — acceptable for casual address receives. Paused on background via AppState. Net coverage summary: - ReceiveSheet-pending invoice, anywhere in app: 1 s (expectPayment) - Lightning-address receive while foregrounded, anywhere: ≤30 s - App resume from background: one-shot refresh on foreground transition - On-chain: still not covered, tracked in #134 - App closed / phone locked: needs OS push (#45) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR introduces a branded PaymentProgressOverlay to replace the unstyled native “Payment Sent” alert, and centralizes incoming-payment detection/polling into WalletContext so the receive celebration can appear over any screen.
Changes:
- Added
PaymentProgressOverlay(send bubbles + receive confetti) and wired it into Send + app root receive celebrations. - Hoisted incoming-payment detection into
WalletContextvia balance-diff baselining, plusexpectPayment()for fast invoice polling and an AppState-driven baseline poll. - Added numeric-only sanitization for sats/fiat amount inputs in Send/Receive sheets.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| App.tsx | Adds a global incoming-payment overlay above the navigation stack. |
| src/contexts/WalletContext.tsx | Adds incoming-payment event bus, balance-diff detector, aggressive invoice polling, and foreground polling. |
| src/components/SendSheet.tsx | Replaces success/failure alerts with the overlay; sanitizes amount input. |
| src/components/ReceiveSheet.tsx | Moves invoice settlement polling to WalletContext.expectPayment; adds baseline handling + input sanitization. |
| src/components/PaymentProgressOverlay.tsx | New overlay UI component implementing bubbles/confetti animations and branded result cards. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
## Copilot comments
- **SendSheet stale comment** — \`handleOverlayDismiss\` said "success
auto-dismisses via the overlay's timer" but the overlay requires an
explicit OK tap now. Rewritten to match actual behaviour.
- **Context foreground poll ignored connection changes** — the 30 s
baseline poll keyed only on \`activeWalletId\`, so a wallet that
reconnected (or disconnected) without an id change would leave the
poll running against nothing (or not start at all). Now depends on
a derived \`activeWalletConnected\` boolean and reads latest state
via \`walletsRef\` inside the AppState handler.
- **\`onRequestClose\` undefined** — RN Modal warns on Android when
\`onRequestClose\` is undefined; extracted a stable handler that
swallows the back press only while \`state === 'sending'\`.
- **Second payment didn't re-arm overlay** — two payments in quick
succession kept \`state === 'success'\` so the confetti transition
never fired again. App.tsx now passes \`key={lastIncomingPayment.at}\`
to force a re-mount per event.
- **expectPayment tick pile-up** — addressed already in the previous
commit via an \`inFlight\` ref guard; the Copilot note confirmed the
concern was genuine.
## Self-review items
1. **Baseline map growth** — \`paymentBaselinesRef\` is now
garbage-collected in the detector effect; entries for walletIds no
longer in \`wallets\` are pruned on every run.
2. **Duplicated baseline logic** in ReceiveSheet removed.
\`paymentReceived\` (the QR-thumbnail checkmark) now derives from
\`lastIncomingPayment\` in context rather than running its own
parallel balance-diff with its own \`prevBalance\` / \`needsBaseline\`
refs. Single source of truth.
3. **Exact invoice amount over balance delta** — \`expectPayment\` now
takes an optional \`expectedAmountSats\`. When \`lookup_invoice\`
reports \`paid: true\` we fire \`lastIncomingPayment\` with the known
invoice amount (advancing the baseline first so the diff path
doesn't double-fire). Fixes the edge case of two invoices settling
between polls reporting a combined delta.
5. **Silent bolt11 decode failure** — now \`console.warn\`s in
\`__DEV__\` so the fallback-to-balance-poll path is traceable.
## Other
- \`IncomingPayment.walletAlias\` dropped — stale if the user renames
between detection and dismissal. App overlay only reads \`walletId\`
now and can derive alias at render if needed.
- \`expectPayment\` JSDoc expanded with the replacement-semantics
justification (balance-diff detector catches the displaced invoice).
- Added \`paymentHash\` to \`IncomingPayment\` so consumers can tell a
known-invoice settle from a generic balance-diff (lightning-address)
receive.
## Not addressed here
- Issue 6 (SendSheet re-indentation noise) — damage is done on the
current commit sequence; file to track hoisting the send overlay
state into context (symmetric with receive) so future PRs on this
area don't suffer the fragment-wrap reformatting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the bare
Alert.alert("Payment Sent")native dialog (white square, no rounding) with a rounded, on-brand PaymentProgressOverlay covering both send and receive flows, and lifts incoming-payment detection up to the app root so the celebration pops on any screen.Send flow
Receive flow
Detection + polling model
Poll ownership moved from
ReceiveSheetup toWalletContextso the detector survives the sheet closing. User can generate an invoice, close the sheet, wander into Friends / Home, and still get the pop when the payment lands.WalletContext.expectPayment(walletId, paymentHash)Fast poll uses NIP-47
lookup_invoiceon the specific payment hash (lighter + faster thanget_balanceon most backends) with a parallel balance refresh as fallback. Onpaid: true, a tx-list refresh is auto-triggered so the transaction row is visible by the time the user taps OK.Also in this PR
numerichint alone doesn't catch.Build
react-native-reanimated(already in the project) for bubbles + confetti;light-bolt11-decoder(already in the project) for payment-hash extraction.Keyboard/TouchableWithoutFeedbackunused warnings inReceiveSheet.tsxare onmainalready.)Related issues
Shipped here:
Filed as follow-ups, referenced for context:
fix(send): characters dropped while typing amount in Send sheet(separate from input sanitisation — that's a re-render race)perf(nwc): get_balance reply timeouts add ~10 s latency to payment detection(server-side NWC round-trip latency; SSH-verified LNbits is healthy, NIP-20 fix present, no IN_FLIGHT spam — the remaining gap is likely SDK reply-timeout tuning or upstream relay)feat(receive): celebration overlay for on-chain incoming payments(BDK sync isn't foreground-polled; on-chain needs its own trigger)Add OS push notifications for incoming payments(background / app-closed delivery — out of scope for this PR, needs FCM/APNs + backend)Upstream:
Test plan
Alert.alertis gone; rounded card with spinner + pink bubbles; on success, bubbles turn green and a tick appears; OK dismisses.abc123.45xyzinto sats and fiat inputs; only the numeric portion sticks.🤖 Generated with Claude Code