Skip to content

feat(ui): branded payment progress overlay + global incoming-payment detection#135

Merged
BenGWeeks merged 15 commits into
mainfrom
feat/payment-popup-bubbles
Apr 21, 2026
Merged

feat(ui): branded payment progress overlay + global incoming-payment detection#135
BenGWeeks merged 15 commits into
mainfrom
feat/payment-popup-bubbles

Conversation

@BenGWeeks

Copy link
Copy Markdown
Owner

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

  • "Sending payment…" spinner + amount on a rounded card, pink bubbles rising behind it. Bubble count 140, size 22–68 px, quadratic stagger over 5 s so density ramps up as time passes.
  • On success: bubbles morph pink → green, a green tick swaps in, card requires an explicit OK tap to dismiss (no auto-close — a receiver who wasn't looking should still see the outcome when they come back).
  • On failure: red × + error message, "Dismiss" button.

Receive flow

  • Global overlay at the app root — lives above any screen.
  • "Payment received!" card with on-brand confetti (pink, light pink, violet, blue, cyan, lavender) bursting radially from behind the card using projectile-with-gravity physics.
  • Amount and sending wallet shown. Requires an OK tap to dismiss.
  • 50 % more confetti pieces (135) vs. original prototype so the burst reads as a full celebration.

Detection + polling model

Poll ownership moved from ReceiveSheet up to WalletContext so 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.

scenario cadence lives in
ReceiveSheet-generated invoice pending 1 s for 3 min WalletContext.expectPayment(walletId, paymentHash)
Lightning-address receive while app foregrounded ≤ 30 s WalletContext baseline poll
App resume from background one-shot refresh WalletContext AppState listener
On-chain receive ❌ see #134
App closed / phone locked ❌ see #45

Fast poll uses NIP-47 lookup_invoice on the specific payment hash (lighter + faster than get_balance on most backends) with a parallel balance refresh as fallback. On paid: 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

  • Baseline fix: ignore the cached balance at sheet open / wallet switch and treat the first observed balance as the baseline. A prior invoice that settled while the app was backgrounded no longer fires a bogus celebration attributed to the next invoice.
  • Numeric-only amount inputs in Send and Receive sheets — digits only (sats), digits + single decimal capped at 2 places (fiat). Blocks hardware-keyboard / paste / autocomplete injection that the soft-keyboard numeric hint alone doesn't catch.
  • Send / receive copy no longer wraps around the unbranded Android Alert dialog.

Build

  • No new dependencies. Uses react-native-reanimated (already in the project) for bubbles + confetti; light-bolt11-decoder (already in the project) for payment-hash extraction.
  • TypeScript, ESLint, Prettier clean on changed files. (Two pre-existing Keyboard / TouchableWithoutFeedback unused warnings in ReceiveSheet.tsx are on main already.)

Related issues

Shipped here:

  • Closes nothing directly; this is a UX feature PR.

Filed as follow-ups, referenced for context:

Upstream:

  • lnbits/nwcprovider#30 — NIP-47 notifications (kind 23196). Would eliminate polling entirely; currently not merged.

Test plan

  • Send → Alert.alert is gone; rounded card with spinner + pink bubbles; on success, bubbles turn green and a tick appears; OK dismisses.
  • Send failure → red × + error message, Dismiss button.
  • Receive → generate an amount invoice, pay it, overlay pops with confetti bursting radially from behind the card; OK closes.
  • Receive, cross-screen → generate invoice, close sheet, navigate to Friends, pay invoice from an external wallet, overlay still pops over Friends.
  • Receive, lightning address → sit on Home, pay the user's lightning address from another wallet, overlay fires within ≤ 30 s.
  • Receive after backgrounding → generate invoice, pay it while app is backgrounded, return to app, overlay fires on resume.
  • Baseline fix → previously-settled-but-uncached payment does not fire a bogus celebration attributed to a freshly-generated invoice.
  • Amount input → paste abc123.45xyz into sats and fiat inputs; only the numeric portion sticks.
  • Tx list refresh → after receiving, the Home / Transactions screens show the new row without a manual pull-to-refresh.

🤖 Generated with Claude Code

BenGWeeks and others added 13 commits April 21, 2026 22:50
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>
@BenGWeeks BenGWeeks requested a review from Copilot April 21, 2026 22:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 WalletContext via balance-diff baselining, plus expectPayment() 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.

Comment thread src/components/SendSheet.tsx Outdated
Comment thread src/contexts/WalletContext.tsx
Comment thread src/contexts/WalletContext.tsx
Comment thread src/components/PaymentProgressOverlay.tsx Outdated
Comment thread App.tsx
## 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>
@BenGWeeks BenGWeeks merged commit ac0cf73 into main Apr 21, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants