Skip to content

feat(receive): redesign receive flow with improved sheets and toasts#480

Open
sahilc0 wants to merge 41 commits intowt-navbar-root-only-20260325from
wt-receive-on-navbar-20260326
Open

feat(receive): redesign receive flow with improved sheets and toasts#480
sahilc0 wants to merge 41 commits intowt-navbar-root-only-20260325from
wt-receive-on-navbar-20260326

Conversation

@sahilc0
Copy link
Copy Markdown
Contributor

@sahilc0 sahilc0 commented Mar 26, 2026

Summary

BLOCKED BY: Kukks' LNURL work. This PR redesigns the receive flow to show the QR code immediately (skipping the amount screen), but we should not merge until we have zero-invoice Lightning via LNURL. Without that, showing the QR first means no Lightning option unless the user manually sets an amount. The full UX improvement depends on LNURL landing first.

  • Receive layout: QR centered vertically with actions pinned to bottom for balanced spacing
  • Sheet modal (Vaul-inspired): Visual elevation via Ionic CSS custom properties (shadow, border, overflow), wider pill handle, no close button
  • Toast (Sonner-inspired): Custom toast system replacing useIonToast across all call sites — dark bg, top-positioned, 250ms ease-out animations, reduced-motion compliant
  • Copy sheet: Reordered addresses (Unified > Lightning > Arkade > Bitcoin), proper header styling, accessible 44px copy buttons with aria-labels
  • Accessibility: :focus-visible instead of :focus (no purple flash on tap), semantic <button> elements, touch-action: manipulation

Dependency

This PR is blocked by Kukks' LNURL implementation. The current receive flow shows the QR immediately without an amount, which means Lightning is unavailable until the user sets an amount (500+ sats). Once LNURL lands, we'll have zero-invoice Lightning support and the "show QR first" UX will work end-to-end.

Do not merge until LNURL is available.

Base branch

This PR is based on wt-navbar-root-only-20260325 (#474 — navbar only on root pages) to avoid navbar interference with the receive sheet modals.

Test plan

  • QR centered vertically, actions pinned to bottom on various screen heights
  • Sheet modals show visible elevation (shadow + border)
  • Toast appears at top with dark bg, auto-dismisses in 2s
  • Copy buttons have 44px tap targets, no purple focus flash on tap
  • Address order in copy sheet: Unified, Lightning (when available), Arkade, Bitcoin
  • Amount sheet header matches copy sheet styling
  • Dark mode: toast slightly lighter gray, sheet has stronger shadow
  • Reduced motion: no animation on toast enter/exit

pietro909 and others added 5 commits March 25, 2026 18:45
- Skip amount screen, show QR immediately on receive
- Custom SVG QR with circular dots, rounded finder patterns, Arkade logo center
- Logo uses brand color (--logo-color), adapts to light/dark theme
- QR dots animate in with staggered ripple on value change
- Buttons inline with content (not fixed footer) — scrollable layout
- Amount entry and copy address via bottom sheet modals
- SheetModal accounts for pill navbar clearance
- Content noFade prop to disable scroll mask on receive screen
- Fetch addresses on mount (merged from Amount screen)
- Sheet modal: iOS-inspired with shadow, handle bar, rounded close button
- Sheet z-index above navbar (z-index: 200), backdrop dismiss enabled
- Haptics on sheet close
- Amount keyboard bypasses sheet on save — goes straight to QR
- Button label: "Add amount" / "Edit amount" instead of sats number
- Clear amount option in sheet to reset to no-amount QR
- "500 sats min for Lightning" hint tightly coupled under QR
- "Requesting X sats" text positioned closer to QR
Receive flow layout:
- Center QR vertically in available space, pin actions to bottom
- Add breathing room with 1.5rem bottom padding on actions
- Reorder copy addresses: Unified > Lightning > Arkade > Bitcoin
- Rename BIP21 to "Unified", BTC address to "Bitcoin address"

Sheet modal (Vaul-inspired):
- Move visual styling to Ionic CSS custom properties (--background,
  --box-shadow, --overflow, --border-width) to escape shadow DOM
  overflow clipping that was hiding shadows on inner elements
- Remove close X button, use handle bar + backdrop dismiss
- Wider pill handle (40px), softer shadow, 16px border-radius
- Dark mode: stronger shadow + light border for elevation contrast

Toast (Sonner-inspired):
- New custom Toast component replacing useIonToast across 6 files
- Dark bg on light mode, slightly lighter gray on dark mode
- Top-positioned, 250ms ease-out enter/exit, full reduced-motion support
- 44px accessible copy buttons with aria-labels and touch-action

Accessibility:
- Copy icon buttons: div -> button with aria-label, 44px min tap target
- Focus highlight: :focus -> :focus-visible (no purple flash on tap)

Dev auto-init:
- Set authState to 'authenticated' directly instead of persisting
  encrypted key to localStorage (keeps dev NSEC in-memory only)
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • next-version

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ee118602-7957-458d-bbde-a4836ca69460

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wt-receive-on-navbar-20260326

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sahilc0 sahilc0 requested review from Kukks, bordalix and pietro909 March 26, 2026 13:51
@sahilc0
Copy link
Copy Markdown
Contributor Author

sahilc0 commented Mar 26, 2026

⚠️ Blocked: depends on LNURL

@Kukks — this PR redesigns the receive flow to show the QR code immediately (no amount screen first). However, this UX only makes sense once we have zero-invoice Lightning via LNURL.

Right now, without an amount set, there's no Lightning invoice in the QR — just Arkade + on-chain. That's a worse experience than the current flow where we prompt for amount first.

This should not be merged until LNURL support lands. Once it does, the receive flow will show a unified QR that supports Lightning without requiring the user to enter an amount upfront.

Keeping this as draft until then.

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Mar 26, 2026

Deploying wallet-bitcoin with  Cloudflare Pages  Cloudflare Pages

Latest commit: f5ef1b2
Status: ✅  Deploy successful!
Preview URL: https://ff98fc5b.wallet-bitcoin.pages.dev
Branch Preview URL: https://wt-receive-on-navbar-2026032.wallet-bitcoin.pages.dev

View logs

@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages bot commented Mar 26, 2026

Deploying wallet-mutinynet with  Cloudflare Pages  Cloudflare Pages

Latest commit: f5ef1b2
Status: ✅  Deploy successful!
Preview URL: https://f8a9b660.arkade-wallet.pages.dev
Branch Preview URL: https://wt-receive-on-navbar-2026032.arkade-wallet.pages.dev

View logs

pietro909 and others added 18 commits March 26, 2026 15:46
* Strip icon field from asset metadata before persisting to localStorage

Asset metadata icons are base64-encoded image strings that can be tens
or hundreds of KB each. Persisting them fills up the ~5MB localStorage
quota unnecessarily since they are re-fetched on demand. The hasIcon
flag is preserved so the UI still knows an icon exists.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Evict expired entries from asset metadata cache on save

Entries older than 24h (ASSET_METADATA_TTL_MS) are now dropped when
persisting the cache to localStorage, preventing unbounded growth.
The TTL constant is shared between storage.ts and wallet.tsx.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert icon stripping from asset metadata cache

Keep icons persisted in localStorage; only rely on TTL-based eviction
to control cache size.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Fix code split issue in service worker

* Upgrade ts-sdk 0.4.12 - boltz-swap 0.3.12
* style(ui): soften header divider and redraw back/close icons

- Header divider: replace hardcoded #444 border with var(--dark10)
  for a subtler, theme-aware separator across all sub-pages
- Back icon: redraw as a Heroicons-style left arrow (shaft + head)
  at a slightly larger size within the 32x32 viewbox
- Close icon: redraw as a clean stroke-based X mark, replacing the
  filled path with fillOpacity 0.5 to match the back icon's
  currentColor stroke treatment

* style(close-icon): fix prettier formatting

---------

Co-authored-by: Sahil Chaturvedi <sahilc0@users.noreply.github.com>
Replace the SVG loading bar across all loading screens with the
bounce/morph pixel logo animation (Arcade → Invader → Heart → loop).

- Extract shared SVG rendering into PixelLogoSvg component (DRY)
- Create useBounceMorph hook with checkpoint-style graceful stop
- Create LoadingLogo component with three exit modes:
  - fly-to-target: boot animation flies to LogoIcon position
  - fly-up: operation screens fly up before navigating
  - none: gate patterns unmount naturally
- Boot animation persists Loading → Wallet, white overlay during loop
- Wallet LogoIcon anchor moved outside stagger tree for correct measurement
- Migrate ~20 usage sites (gate patterns + operation patterns with exit)
- Delete old Loading.tsx and LoadingBar.tsx
- Cherry-pick VITE_DEV_NSEC auto-init for dev testing
…omplete

- Hide wallet header LogoIcon while boot animation is active (visibility: hidden)
  so the fly-to-target doesn't show two logos
- Hold wallet stagger animation until boot animation completes, preventing
  the stagger from playing invisibly behind the white overlay
- Switch fly-to-target easing from ease-out-quint to ease-in-out-quint
  per Emil's guideline: on-screen movement uses ease-in-out
- Add EASE_IN_OUT_QUINT constant to animations.ts
- Tighten stagger duration from 400ms to 300ms
- Remove useEffect-based stagger start; drive motion state directly
  from hold prop to avoid one-paint-late blank frame
- Sync bootAnimActive to external store synchronously (before React
  re-render) so Wallet reads correct value on same render that
  unmounts LoadingLogo
- Retry getLogoAnchor() up to 10 rAFs before falling back to fly-up
  if header anchor isn't mounted yet
- Replace dead setShowBackground state with plain const
- Use useRef to capture isInitialLoad at mount (satisfies
  react/hook-use-state lint rule)
…ot anim API

- Extract duplicated fly-up animation into a local helper function
- Name the anchor retry count (ANCHOR_RETRY_FRAMES = 10)
- Rename updateBootAnimActive → updateBootAnim for clarity
- Revert lock file changes (dev environment artifacts)
- Remove rollup-darwin-x64 optionalDep (pnpm handles native binaries)
- Reset bootAnimDone/bootExitMode before starting new loading cycle
  to prevent stale state if app re-enters Pages.Loading
- Use measured anchor size instead of hardcoded 35px for fly-to-target
  scale, so it adapts to CSS/breakpoint changes
- Reset bounce loop state (stopRequested, activeShape) when
  reducedMotion changes to prevent frozen intermediate shapes
- Don't block cancel button on Init/Connect — only show opaque
  background when connectDone is true
Unlock.tsx still imported the deleted Loading component after rebase
conflict resolution. Since the boot animation in App.tsx covers the
unlock loading state, render nothing while unlocking instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…260325

feat(ui): replace loading bar with bounce/morph logo animation
* Create one VtxoManager and reuse it

* Use VtxoManager from SW

* Use vtxoManager functions were possible

* Preserve Sentry error capture for settlement errors

* restored settleVtxos and renewCoins to address risky behavioral changes

* Import Sentry in Vtxos.tsx

* Reduce e2e tests timeout to 30 min

* Error handling, Missing dependency, Stale response cancellation

* Do not throw
* prevent duplicate funding of vhtlc

* Upgrade ts-sdk 0.4.12 - boltz-swap 0.3.12

* Use ContractManager to fetch vtxos

---------

Co-authored-by: Pietro Grandi <dev@pietro.uno>
…#487)

* fix: unregister stuck service worker after exhausting retries

When the service worker is permanently unresponsive (browser stopped it
and MessageBus.start() fails silently on wake), all 5 retry attempts
time out. The PR #421 one-time reload then reuses the same broken SW
registration, leaving the app stuck on Loading forever.

Unregistering the SW after retries are exhausted ensures the reload
gets a fresh registration with a clean MessageBus.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: increase MessageBus init timeout to 30s for cold starts

The INITIALIZE_MESSAGE_BUS handler in the SDK calls buildServices()
which creates Wallet.create() — a network call to the ARK server.
The SDK intentionally skips wrapping this with a timeout because it
"performs network calls that legitimately exceed message timeout."

After long idle periods (days), the cold connection to the ARK server
(DNS, TLS, server wake-up) easily exceeds the previous 5s timeout.
Each of the 5 retries sent a NEW INITIALIZE_MESSAGE_BUS, restarting
the network calls from scratch every time, never letting them finish.

- Increase messageBusTimeoutMs from 5s to 30s (matches SDK default)
- Reduce maxRetries from 5 to 2 (30s × 2 retries is sufficient)
- Keep serviceWorkerActivationTimeoutMs at 5s (no network involved)
- Keep SW unregistration as fallback if retries are still exhausted

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: detect and replace zombie service workers before initialization

Some browsers (Vivaldi) keep the SW registered as "activated" after
long idle periods but never actually wake the worker thread on
postMessage. The 30s timeout and retry logic can't help because the
SW is not processing messages at all — it's a zombie.

Before calling ServiceWorkerWallet.setup(), ping the existing SW via
a MessageChannel. If it doesn't respond within 2s, unregister it so
setup() gets a fresh registration that actually works.

- Add PING/PONG handler as the first thing in the SW script (works
  even if subsequent MessageBus initialization fails)
- Add pre-flight ping in initSvcWorkerWallet before setup()
- Rebuild wallet-service-worker.mjs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: untrack built wallet-service-worker.mjs

This file was intentionally removed from the repo in #437.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* perf: speed up cold start by pre-registering SW and parallelizing init

Pre-register the service worker on page load so activation runs in
parallel with React bootstrap, ASP fetch and auth check instead of
blocking inside ServiceWorkerWallet.setup().

Also run the zombie-SW detection (timeout reduced from 2s to 500ms)
concurrently with the IndexedDB getWalletState() warmup.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: harden zombie SW ping against race and unexpected messages

Capture registration.active into a local before the null check so it
can't become null between the guard and postMessage. Only treat the
response as alive when event.data.type is the expected 'PONG'.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Only render the commit hash when showBackground is true (full-screen
boot animation), avoiding overlap with page content like the Cancel
button on the Connect screen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TaprootFreak and others added 18 commits March 28, 2026 12:03
* Move DFX Buy with Fiat from wallet button to Apps page

- Remove Buy with Fiat button from wallet dashboard
- Add DFX as an app with iframe integration, auth signing, loading state and error handling
- Use official DFX logo mark with theme-aware colors
- Use headless mode and hardcoded sign message (no extra API call)

* Address PR review feedback from bordalix

- Fix app description: "Ark" → "Arkade"
- Move DFX card to alphabetical position (after Boltz)
- Remove unused className='dfx-iframe' from iframe
- Use hard navigate for back button to avoid iframe history issues

* Fix prettier formatting for DFX iframe

* Fix Loading component rename to LoadingLogo

Apply upstream change: Loading component was renamed to LoadingLogo
* fix: include swap ID in error log for swap processing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: show friendly error messages for swap refund failures

Map known boltz-swap error patterns to user-readable explanations:
- Locktime not passed: show the date when funds become recoverable
- VHTLC already spent: explain the swap was already refunded/claimed
- VHTLC not found: explain no funds at the swap address

Raw errors remain in the console log for debugging.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* integrate branta for zk send side only

* use branta payment type

* Update src/screens/Wallet/Send/Form.tsx

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Signed-off-by: Keith <74844722+keith-gardner@users.noreply.github.com>

* Update payment type in QR code handler.

Signed-off-by: Keith <74844722+keith-gardner@users.noreply.github.com>

* URL Sanitation.

* Payment type.

* check branta url for https

* bump @branta-ops/branta version to 0.0.9

* lock @branta-ops/branta to version 0.0.9

* fix: address review comments on branta integration

- Use `Payment | null` type instead of `any` for brantaPayment state
- Clear loading state on early returns (empty rawScanData, non-ZK code)
- Log Branta API errors via consoleError before clearing state

* chore: regenerate pnpm-lock.yaml after rebase

---------

Signed-off-by: Keith <74844722+keith-gardner@users.noreply.github.com>
Co-authored-by: Kyle McCullen <kmccullen@protonmail.com>
Co-authored-by: Keith <74844722+keith-gardner@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: keith <keith@keiths-iMac.local>
* feat: add Docker image build and publish to ghcr

Add Dockerfile (multi-stage node build + nginx serve), nginx config
with SPA routing fallback, and GitHub Actions workflow to build and
push to ghcr.io/arkade-os/wallet:latest on master pushes.

* feat: add runtime env var substitution for Docker image

Build with placeholder values for VITE_* env vars, then substitute
them at container startup via docker-entrypoint.sh. This allows a
single image to serve multiple environments by passing env vars at
runtime (e.g. docker run -e VITE_ARK_SERVER=https://...).

* docs: add Docker image usage to README

* fix(docker): run nginx as non-root and escape sed substitution values

Address CodeRabbit review:
- Add USER nginx directive so the container no longer runs as root
- Escape &, |, and \ in env values before sed replacement to prevent
  injection/breakage in URLs and DSNs
* feat: show loading status messages during wallet initialization

Surface contextual status text below the boot animation logo so users
know what the app is doing while loading (e.g. connecting to service
worker, fetching transactions, restoring swaps).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: show git commit hash at bottom of loading screen

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: hold loading screen until dataReady to prevent Init page flash

On slow networks, there was a window between initialized=true and
dataReady=true where the page fell through to the stale screen value
(Pages.Init), briefly showing the landing page before wallet.tsx's
dataReady effect navigated to Wallet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: show git commit hash at bottom of boot loading screen only

Only render the commit hash when showBackground is true (full-screen
boot animation), avoiding overlap with page content like the Cancel
button on the Connect screen.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Increase e2e timeout

* fix: prevent loading screen deadlock when reloadWallet fails

If the first reloadWallet call threw, dataReady was never set to true,
leaving shouldHoldOnLoading permanently true and trapping the user on
the boot loading screen. Move the dataReady/clearLoadingStatus logic
into a finally block so the loading gate always resolves.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: recover from init failures instead of deadlocking

- unlockWallet: reset authState to locked when initWallet throws so the
  user returns to the password screen instead of being stranded on the
  boot loader
- Unlock: surface a "Connection failed" error for non-password failures
- Connect: add cancel guard so initWallet won't start after unmount
- send.test.ts: align waitForSelector timeout with pay() helper (60s)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: remove cosmetic Cancel button from Connect screen

Init is fire-and-forget once the screen mounts — setPrivateKey and
initWallet run immediately with no abort path. Instead of building
cancellation machinery around an irreversible operation, remove the
Cancel button and back header so the UI matches the actual contract.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: show retry prompt when initial wallet data load fails

Instead of silently falling through to an empty wallet when the first
reloadWallet fails, surface a BootError screen with Retry and Continue
buttons so the user can recover without reloading the page.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(e2e): handle BootError overlay in wallet readiness checks

The boot flow now holds the loading screen until dataReady and shows
a BootError overlay on reloadWallet failure. E2e tests were waiting
only for "Send" text, which never appears behind the overlay.

Add waitForWalletPage() helper that waits for either "Send" or
"Continue anyway", dismisses the error if shown, then waits for
the wallet page. Timeout raised to 60s to account for the extra
data-load wait.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(e2e): increase waitForPaymentReceived timeout to 60s

The default 30s was borderline on CI even on master. With the new
boot flow waiting for dataReady, the overall test timing is tighter
and Boltz reverse-swap claims consistently exceed 30s on slow CI
runners.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(e2e): prevent loading screen from unmounting Connect during init flow

shouldHoldOnLoading (which waits for dataReady) was unmounting the
Connect component before swap recovery could complete during wallet
creation/restore. Skip the hold during the init flow so Connect stays
mounted. Also switch fire-and-forget exec('payinvoice') to awaited
execAsync and increase test timeouts for slower operations.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The boot animation overlay (z-index 9 white background) was intentionally
kept alive when page transitioned to Unlock, but this made the password
input completely invisible. For passwordless wallets the page never reaches
Unlock (Loading → Wallet), so the exclusion was dead code — except when
the passwordless auto-boot fails (e.g. Vivaldi SW timeout sets authState
to 'locked'), where it caused a permanent deadlock.

Dismiss the overlay for any non-Loading page, including Unlock. Also fix
two pre-existing broken tests that looked for removed 'Loading...' text.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Change the default currency display from "Show both" (sats + fiat) to
"Fiat only" for new users. Existing users with saved preferences are
unaffected. Users can still switch to "Show both" or "Sats only" in
settings.

Also fix responsive spacing in the Balance component — reduce bottom
margin when only a single balance is shown, eliminating empty whitespace
where the second row would be.

- src/providers/config.tsx: default CurrencyDisplay.Both → CurrencyDisplay.Fiat
- src/components/Balance.tsx: dynamic margin based on showBoth flag

Co-authored-by: Sahil Chaturvedi <sahilc0@users.noreply.github.com>
showBoth was referenced on line 34 but never defined, causing a
ReferenceError at runtime after merging #473. Define it from
config.currencyDisplay and reuse it for the conditional render.
* Filter Google Translate widget errors from Sentry

Users with browser translation enabled trigger noisy errors from
translate.google.com and translate.googleapis.com (including stack
overflows). These are not actionable bugs in our code. Add
ignoreErrors, denyUrls, and beforeSend filters to drop them.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix formatting in Sentry init

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Narrow Sentry stack overflow filter to Google Translate origin

The blanket ignoreErrors for "Maximum call stack size exceeded" could
hide real bugs. Gate it behind translate-origin detection so only
stack overflows caused by translate.google(apis).com are dropped.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Simplify beforeSend to just check isTranslateOrigin

The a[je] check is already covered by ignoreErrors, and the stack
overflow clause was redundant since isTranslateOrigin alone was
already a prior branch.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* add lock tests

* add lock tests
PR #473 changed the default currency display from "Show both" to
"Fiat only", which broke e2e tests that assert on SATS amounts in the
balance display. Pre-set currency to "Show both" in the Playwright
test fixture via addInitScript, and update the wallet unit test to
expect USD instead of SATS.
* fix: guard serviceWorker access, add error boundary, fix notification and refresher errors

- Move navigator.serviceWorker.controller access inside the
  'serviceWorker' in navigator guard to prevent TypeError on
  browsers where the API is undefined (ARKADE-WALLET-3V, 36 users)

- Add React ErrorBoundary that captures component stacks to Sentry,
  preventing white-screen crashes and enabling diagnosis of
  Error #31 issues (ARKADE-WALLET-2M/2E, 31 users)

- Check Notification.permission before calling showNotification and
  handle the promise rejection to prevent unhandled errors
  (ARKADE-WALLET-2Y, 8 users)

- Wrap Refresher pull-to-refresh handler in try/catch to prevent
  unhandled errors when MessageBus times out (ARKADE-WALLET-3D, 11 users)

* fix: add user-friendly explanation to ErrorBoundary fallback UI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Pietro Grandi <dev@pietro.uno>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: run prettier on all source files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Add LNURL support for amountless Lightning receives

When the user skips the amount on the receive screen and
VITE_LNURL_SERVER_URL is configured, the wallet opens an SSE
session with the lnurl-server and displays the resulting LNURL
in the QR code. When a payer scans and chooses an amount, the
wallet creates a reverse swap on-the-fly and returns the bolt11.

- LNURL embedded in BIP21 URI via lightning= parameter
- LNURL shown in expanded address list
- Session auto-closes when navigating away

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address review feedback: harden SSE parsing and validate amounts

- Wrap JSON.parse in try/catch to prevent SSE stream crash on
  malformed payloads
- Validate amountMsat is a positive number before creating swap
- Strip trailing slash from LNURL server URL
- Add missing isAmountlessLnurl to useEffect dependency array

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix import alias for lnurlServerUrl

The replace_all swapped the alias direction — import the
correct export name from constants.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Address CodeRabbit review: error handling, swap readiness, amount validation

- postInvoice now checks response.ok and throws on failure
- Clear LNURL state on invoice request failure and in finally block
- Gate LNURL session on arkadeSwaps being initialized and connected
- Add validLnSwap check before creating reverse swap

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix SSE parsing: persist eventType across chunks

The eventType variable was declared inside the while loop,
causing it to reset on each chunk read. When the event: and
data: lines arrive in separate chunks (common in browsers),
the data line was silently ignored. Move eventType outside
the loop so it persists across chunk boundaries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Send errors back to lnurl-server instead of killing session

When the wallet can't create a swap (e.g. amount outside limits),
post { error: "reason" } back to the server so the payer gets an
immediate error instead of waiting for timeout. Remove validLnSwap
pre-check since the server enforces min/max via LNURL spec and
createReverseSwap will fail naturally with a descriptive error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Use auth token for invoice endpoint requests

Store the token from session_created SSE event and include it
as Authorization: Bearer header on all invoice POST requests.
Prevents unauthorized parties from submitting fake invoices
even if they discover the session ID.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix race condition and error handling in useLnurlSession

- Capture sessionId in local const before awaiting to prevent
  race between concurrent events
- Pass abort signal to postInvoice/postError so in-flight POSTs
  cancel on teardown
- Send error back to server on invalid amountMsat instead of
  silently dropping (which left payer waiting until timeout)

* fix: check response.ok in postError to surface server-side failures

postError silently ignored non-2xx HTTP responses, unlike postInvoice
which throws on failures. Now logs the status and response text when
the server rejects the error notification.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Keep master's boot animation, error boundary, LNURL session
- Keep PR's redesigned receive UI with sheets and toasts
- Integrate LNURL amountless receive into new QR-first flow
- Replace removed Loading component with LoadingLogo
The noUserDefinedPassword() check races with the dev auto-init,
overriding authState to 'locked' and blocking the boot flow.
The fast auto-init from VITE_DEV_NSEC races with the boot animation
timing, causing the overlay to never dismiss.
@sahilc0 sahilc0 marked this pull request as ready for review April 1, 2026 20:01
@arkanaai
Copy link
Copy Markdown

arkanaai bot commented Apr 1, 2026

🔍 Review — feat(receive): redesign receive flow with improved sheets and toasts

Large PR: +11,336/-587 across 85 files. Absorbs multiple merged PRs from master (Docker, LNURL, Branta, error boundary, SW recovery, loading status, etc.) plus the receive redesign itself.


Architecture

The PR layers on wt-navbar-root-only-20260325 (#474) and integrates master, so it carries a lot of merged-in work. The receive-specific changes are well-structured:

  • useLnurlSession hook for SSE-based LNURL amountless receives
  • SheetModal redesign with Vaul-inspired elevation
  • Custom Toast system replacing useIonToast
  • QrCode component rewrite with SVG rendering and staggered animations

Security Review

LNURL session (useLnurlSession.ts) — Overall solid:

  • ✅ Auth token stored from session_created event, sent as Bearer header on invoice POSTs
  • ✅ Abort controller properly cancels on unmount
  • amountMsat validated as positive number before creating swap
  • ✅ Errors sent back to lnurl-server so payers get immediate feedback
  • ✅ SSE parser wraps JSON.parse in try/catch
  • eventType persisted outside while loop to handle cross-chunk SSE boundaries

One concern: tokenRef.current is never cleared between sessions except on full unmount (the finally block sets it to null). If the SSE stream reconnects (e.g. network blip) within the same mount, a stale token could be sent. This is low risk since the component unmounts on navigation, but worth noting.

Service worker hardening (merged from #487):

  • ✅ Zombie SW detection via PING/PONG before init
  • ✅ SW unregistration after exhausted retries
  • ✅ MessageBus timeout increased from 5s to 30s (appropriate for cold starts)
  • navigator.serviceWorker access guarded behind feature check

LNURL reverse swap flow in QrCode.tsx:

  • isAmountlessLnurl properly gated on: no amount set, no asset, lnurl server configured, connected, swaps initialized, no swap error
  • waitAndClaim runs in background with error logging
  • ⚠️ The handleInvoiceRequest callback captures recvInfo in its closure via the dependency array. If recvInfo changes between the initial render and when the invoice request arrives, the spread { ...recvInfo } may overwrite newer state. Consider using a ref for recvInfo in this callback, similar to how onInvoiceRequestRef is used in the hook itself.

ErrorBoundary (merged from #496):

  • ✅ Captures to Sentry with component stack
  • ✅ User-friendly fallback UI

UX Observations

  • BLOCKED BY LNURL — PR description clearly states this shouldn't merge until LNURL lands. Since the LNURL work (Add LNURL support for amountless Lightning receives #482) is now merged into this branch, the blocker appears resolved assuming the lnurl-server is deployed.
  • QR-first flow means Lightning is unavailable without amount unless LNURL session is active — the isAmountlessLnurl check handles this correctly.
  • Address ordering (Unified > Lightning > Arkade > Bitcoin) in copy sheet matches the hierarchy of payment rails.
  • package-lock.json (+8555 lines) — this is a npm lockfile in a pnpm project. Intentional? It's listed in the diff and would add significant repo bloat.

Cross-repo Impact

  • Depends on @arkade-os/sdk IVtxoManager interface and VtxoManager.getExpiringVtxos() — ensure ts-sdk exports are stable before merging.
  • @arkade-os/boltz-swap integration for LNURL reverse swaps uses the existing createReverseSwap flow — no new boltz-swap API surface needed.

Dev auto-init (VITE_DEV_NSEC)

The final commits fix race conditions where dev auto-init conflicted with boot animation and auth state checks. The approach (skip boot animation and auth check when VITE_DEV_NSEC is active) is pragmatic. Ensure VITE_DEV_NSEC is never set in production builds — the Vite env var prefix means it's compile-time, so this should be safe as long as .env.production doesn't define it.

Summary

Area Status
LNURL security ✅ Token auth, validation, abort handling
SW recovery ✅ Zombie detection, retry limits, timeout tuning
Error handling ✅ ErrorBoundary, BootError, Sentry filtering
Receive UX ✅ QR-first, sheet modals, accessibility
State management ⚠️ recvInfo closure in handleInvoiceRequest
package-lock.json ⚠️ npm lockfile in pnpm project — likely unintended

Verdict: Well-structured feature PR with good security practices on the LNURL flow. Two minor issues flagged. The npm lockfile should be removed before merge.

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.

6 participants