Skip to content

feat(ui): replace loading bar with bounce/morph logo animation - #478

Merged
sahilc0 merged 9 commits into
masterfrom
wt-loading-bounce-animation-20260325
Mar 26, 2026
Merged

sahilc0 merged 9 commits into
masterfrom
wt-loading-bounce-animation-20260325

Conversation

@sahilc0

@sahilc0 sahilc0 commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Replaces the old loading bar with a bouncing/morphing pixel logo animation throughout the app. The logo cycles through Arcade → Space Invader → Heart shapes while bouncing, with particle burst effects on each bounce.

Boot animation sequence (returning users)

  1. Logo bounces on solid white background during wallet initialization
  2. Logo flies to header position (ease-in-out-quint, 400ms) — background stays opaque throughout
  3. LoadingLogo unmounts, header logo appears
  4. Wallet content staggers in (balance, buttons, transactions) with 300ms ease-out-quint, 60ms stagger delay

Boot animation sequence (new users)

  1. Logo bounces on solid white background
  2. Logo flies up and exits — transitions to onboarding

Inline loading states

All loading screens (send, receive, redeem, assets, settings, etc.) now show the bouncing logo instead of the loading bar.

Key implementation details

  • Frame-timing: External store (logoAnchor.ts) synced synchronously before React re-render so Wallet reads correct bootAnimActive on the same frame LoadingLogo unmounts — no blank frame flash
  • Stagger hold: Wallet stagger container driven directly from hold prop (no useEffect delay) to avoid one-paint-late blank frame
  • Header logo hidden: visibility: hidden during boot animation prevents double-logo appearance during fly-to-target
  • Anchor retry: Up to 10 rAF retries for header logo anchor, falls back to fly-up if unavailable
  • Reduced motion: Full prefers-reduced-motion support — skips all animations
  • Easing: ease-in-out-quint for on-screen movement (fly), ease-out-quint for entrances (stagger) per Emil Kowalski's guidelines

Files changed

  • src/components/LoadingLogo.tsx — new bounce/morph loading component
  • src/components/PixelLogoSvg.tsx — morphing pixel logo SVG
  • src/hooks/useBounceMorph.ts — bounce loop + morph sequencing hook
  • src/components/WalletLoadIn.tsx — stagger container with hold prop
  • src/lib/logoAnchor.ts — module-level store for boot animation state + logo anchor
  • src/lib/animations.ts — added ease-in-out-quint, tightened stagger to 300ms
  • src/App.tsx — boot animation orchestration with sync store updates
  • src/screens/Wallet/Index.tsx — header logo visibility + stagger coordination
  • All screens using <Loading /> updated to <LoadingLogo />

Test plan

  • Returning user boot: logo bounces → flies to header → content staggers in (no flash, no double logo)
  • New user boot: logo bounces → flies up → onboarding screen
  • Inline loading states: send, receive, redeem, assets all show bouncing logo
  • Reduced motion: animations skipped entirely
  • Dark mode: background uses --ion-background-color
  • Mobile (HTTPS via cloudflared tunnel): tested on device

Summary by CodeRabbit

Release Notes

  • New Features
    • Replaced basic loading indicators with animated logo graphics throughout the app for enhanced visual polish
    • Added smooth boot animation sequence on startup with dynamic exit transitions (flying up/to target)
    • Improved visual feedback during key operations like transactions, minting, and wallet connections

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Mar 26, 2026 •

Copy link
Copy Markdown

Deploying wallet-mutinynet with  Cloudflare Pages  Cloudflare Pages

Latest commit: 01a2e5f
Status: ✅  Deploy successful!
Preview URL: https://53ba5094.arkade-wallet.pages.dev
Branch Preview URL: https://wt-loading-bounce-animation.arkade-wallet.pages.dev

View logs

@coderabbitai

coderabbitai Bot commented Mar 26, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

The PR replaces the simple Loading component with a sophisticated LoadingLogo component that renders an animated logo with bounce/morph effects. It introduces boot animation state coordination through a new logoAnchor module, extracts SVG logo rendering into PixelLogoSvg, and modifies multiple screens to defer navigation until exit animations complete.

Changes

Cohort / File(s) Summary
Boot Animation Infrastructure
src/App.tsx, src/lib/logoAnchor.ts, src/providers/navigation.tsx
Added boot animation state management with logoAnchor module tracking logo anchor element and boot animation active flag via subscriptions. App.tsx now orchestrates boot animation lifecycle during page transitions, controlling exit modes (fly-to-target, fly-up) and triggering LoadingLogo overlay.
Loading Component Replacement
src/components/Loading.tsx, src/components/LoadingLogo.tsx, src/icons/LoadingBar.tsx
Removed Loading and LoadingBar components; added new LoadingLogo that renders centered animated logo with bounce/morph animation, supports exit sequences (fly-to-target, fly-up, none) with optional background overlay, and invokes completion callback when animation finishes.
Logo/Animation Components
src/components/OnboardingLogo.tsx, src/components/PixelLogoSvg.tsx, src/hooks/useBounceMorph.ts
Extracted logo SVG rendering and pixel animation into new PixelLogoSvg component; added useBounceMorph hook that orchestrates looping bounce/morph animations through shape sequence (Arcade → Invader → Heart) with morph transitions, reducedMotion support, and requestStop control.
Animation Constants
src/lib/animations.ts
Added EASE_IN_OUT_QUINT easing curve; reduced STAGGER_DURATION from 0.4 to 0.3 affecting wallet load-in stagger timing.
Wallet UI State Management
src/components/WalletLoadIn.tsx, src/screens/Wallet/Index.tsx
Updated WalletStaggerContainer with hold prop to pause animation while boot animation active; modified Wallet screen to register logo DOM anchor, subscribe to boot animation state, and conditionally render logo outside stagger tree based on boot animation state.
Loading UI — Simple Replacements
src/screens/Apps/Assets/Detail.tsx, src/screens/Apps/Assets/Import.tsx, src/screens/Apps/Assets/Index.tsx, src/screens/Settings/Server.tsx, src/screens/Settings/Vtxos.tsx, src/screens/Wallet/Receive/QrCode.tsx, src/screens/Wallet/Receive/Amount.tsx, src/screens/Wallet/Send/Form.tsx, src/screens/Wallet/Notes/Redeem.tsx
Swapped Loading component for LoadingLogo in conditional early-return or loading-state branches without changing control flow or state logic.
Loading UI — Deferred Navigation
src/screens/Apps/Assets/Burn.tsx, src/screens/Apps/Assets/Mint.tsx, src/screens/Apps/Assets/Reissue.tsx, src/screens/Apps/Boltz/Swap.tsx, src/screens/Init/Connect.tsx, src/screens/Init/Restore.tsx, src/screens/Wallet/Send/Details.tsx
Introduced *Done completion state flags and pendingNav refs to defer navigation until LoadingLogo exit animation completes via onExitComplete callback; replaces immediate post-success navigation with deferred callback pattern.
Other UI Updates
src/components/WaitingForRound.tsx, src/screens/Wallet/Unlock.tsx
Extended WaitingForRound props to support done, exitMode, and onExitComplete forwarding to LoadingLogo; changed Unlock to return null during unlock instead of rendering Loading.

Sequence Diagram

sequenceDiagram
    participant App
    participant LoadingLogo
    participant AnimController as useBounceMorph
    participant ExitAnim as Exit Animation
    participant Screen as Target Screen
    
    App->>App: Enter Pages.Loading
    App->>LoadingLogo: Mount with bootAnimActive=true
    LoadingLogo->>AnimController: Start bounce/morph loop
    AnimController->>LoadingLogo: Run shape sequence (Arcade→Invader→Heart)
    
    alt Page reaches Pages.Wallet
        App->>LoadingLogo: Set exitMode='fly-to-target', done=true
        LoadingLogo->>ExitAnim: Animate logo flying to wallet anchor
    else Page transitions to Pages.Init or other
        App->>LoadingLogo: Set exitMode='fly-up', done=true
        LoadingLogo->>ExitAnim: Animate logo flying up with fade
    end
    
    ExitAnim->>LoadingLogo: Exit animation complete
    LoadingLogo->>App: Invoke onExitComplete callback
    App->>LoadingLogo: Unmount (bootAnimActive=false)
    App->>Screen: Render target screen content
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • bordalix
  • tiero
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: replacing a loading bar component with a new bounce/morph logo animation system used throughout the app.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch wt-loading-bounce-animation-20260325

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.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Mar 26, 2026 •

Copy link
Copy Markdown

Deploying wallet-bitcoin with  Cloudflare Pages  Cloudflare Pages

Latest commit: 01a2e5f
Status: ✅  Deploy successful!
Preview URL: https://3015745e.wallet-bitcoin.pages.dev
Branch Preview URL: https://wt-loading-bounce-animation.wallet-bitcoin.pages.dev

View logs

@sahilc0
sahilc0 marked this pull request as ready for review March 26, 2026 11:10
@sahilc0
sahilc0 requested review from bordalix and pietro909 March 26, 2026 11:10
@ghost

ghost commented Mar 26, 2026

Copy link
Copy Markdown

🔍 Arkana Review — wallet#478

UI-only PR — replaces loading bar with a bouncing/morphing pixel logo animation. 34 files changed, but ~8000+ additions are lockfile (bun.lock + package-lock.json).

Architecture

  • Boot animation sequence is well-thought-out: bounce → fly-to-header → stagger-in content.
  • Frame-timing strategy — external store (logoAnchor.ts) synced before React re-render prevents blank-frame flashes. This is a solid pattern for coordinating mount/unmount animations.
  • Reduced motion — prefers-reduced-motion support is included, which is good for accessibility.
  • New dependency — framer-motion@^12.34.3 added. This is a significant dependency for what's primarily a loading animation. Worth considering if the bundle size impact is acceptable for the wallet app.

Observations

  1. Lockfile additions — Both bun.lock and package-lock.json are added as new files. If the project was previously using only one package manager, having both lockfiles could cause confusion. Clarify which is canonical.

  2. Anchor retry logic — 10 rAF retries for header logo anchor with fly-up fallback is pragmatic. The fallback ensures the animation degrades gracefully.

  3. No security concerns — purely presentational changes, no state management or data handling affected.

  4. Test plan is manual — no automated tests for the animation. Given the visual nature, this is acceptable, but consider snapshot tests for the SVG component if it becomes more complex.

No blocking issues for a UI change. Recommend verifying the framer-motion bundle impact on the wallet build.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/screens/Wallet/Receive/Amount.tsx (1)

158-164: ⚠️ Potential issue | 🟡 Minor

This faucet flow still skips the new exit animation.

LoadingLogo only helps here if success is routed through done/onExitComplete. The current success path clears fauceting immediately, so this branch unmounts the loader before the fly-up exit can run.

🩹 Suggested wiring
-          <LoadingLogo text='Getting sats from a faucet. This may take a few moments.' />
+          <LoadingLogo
+            text='Getting sats from a faucet. This may take a few moments.'
+            done={faucetDone}
+            exitMode='fly-up'
+            onExitComplete={completeFaucetSuccess}
+          />
const completeFaucetSuccess = () => {
  setFauceting(false)
  setFaucetSuccess(true)
  setFaucetDone(false)
}

// In handleFaucet() on success:
setFaucetDone(true)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/screens/Wallet/Receive/Amount.tsx` around lines 158 - 164, The fauceting
branch currently clears fauceting immediately which unmounts LoadingLogo before
the exit animation can run; instead, change the success flow in handleFaucet to
set the "done" flag (e.g. call setFaucetDone(true)) rather than clearing
fauceting, and move the actual state teardown into the exit-complete handler
(implement a function like completeFaucetSuccess that calls setFauceting(false)
and setFaucetSuccess(true) — invoked from LoadingLogo or the component's
onExitComplete), so LoadingLogo can run its fly-up exit animation before the UI
switches away.
🧹 Nitpick comments (1)
src/providers/wallet.tsx (1)

135-153: Dev auto-init implementation looks reasonable, but consider stabilizing dependencies.

The logic correctly gates on DEV mode, env var presence, initialization state, and ASP availability. A few observations:

  1. Missing initWallet in deps: While the eslint-disable acknowledges this, initWallet closes over aspInfo, config, and wallet state. If any of these change between effect trigger and execution, you could get stale values. In practice this is unlikely for a one-shot dev flow, but using a ref or including it in deps would be more robust.

  2. Navigate after state update: The setInitialized(true) in initWallet (line 418) runs before navigate(Pages.Wallet) returns, but React's async state batching means the initialized state may not reflect immediately in App.tsx's shouldHoldOnLoading check. For dev-only code, this transient inconsistency is acceptable.

💡 Optional: Use ref to avoid stale closure concerns
+  const initWalletRef = useRef(initWallet)
+  useEffect(() => {
+    initWalletRef.current = initWallet
+  }, [initWallet])
+
   // dev-only: auto-initialize wallet from VITE_DEV_NSEC, bypassing onboarding and unlock
   useEffect(() => {
     if (!import.meta.env.DEV || !import.meta.env.VITE_DEV_NSEC) return
     if (initialized) return
     if (!aspInfo.url) return

     const autoInit = async () => {
       try {
         const privateKey = nsecToPrivateKey(import.meta.env.VITE_DEV_NSEC)
-        await initWallet(privateKey)
+        await initWalletRef.current(privateKey)
         navigate(Pages.Wallet)
       } catch (err) {
         consoleError(err, 'Dev auto-init failed')
       }
     }

     autoInit()
-    // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [aspInfo.url, initialized])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/providers/wallet.tsx` around lines 135 - 153, The dev auto-init effect
closes over initWallet and other state which can become stale; update the effect
to stabilize dependencies by either (A) adding initWallet (and any used values
like nsecToPrivateKey or navigate) to the dependency array, or (B) wrap
initWallet in a ref (e.g., initWalletRef.current) and call that ref inside the
effect so the effect can safely depend only on aspInfo.url and initialized;
ensure the effect still gates on import.meta.env.DEV and VITE_DEV_NSEC and keeps
the same try/catch with consoleError and navigate(Pages.Wallet).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@package.json`:
- Around line 119-120: The package.json optionalDependencies currently includes
only the Intel macOS Rollup binary (`@rollup/rollup-darwin-x64`); update
optionalDependencies to also include the Apple Silicon variant
(`@rollup/rollup-darwin-arm64`) or switch to a cross-arch solution so macOS ARM64
machines get a native binary; locate the optionalDependencies block and add
"@rollup/rollup-darwin-arm64": "<same-version-or-^4.60.0>" (or replace the
single entry with both entries) to ensure both architectures are covered.

In `@src/App.tsx`:
- Around line 217-238: When starting a new loading cycle inside the useEffect
that watches page/bootAnimActive/bootAnimDone, reset the boot state so stale
values don't persist: in the branch where page === Pages.Loading &&
!bootAnimActive (where updateBootAnim(true) is called) also call
setBootAnimDone(false) and reset setBootExitMode to your default (e.g., empty
string or null) so subsequent loads start fresh; this ensures bootAnimDone and
bootExitMode do not carry over from a previous run.

In `@src/components/LoadingLogo.tsx`:
- Around line 83-96: The scale currently hardcodes 35px (targetScale = 35 /
LARGE_SIZE) which breaks when the destination logo size varies; update the scale
calculation to use the measured target rect (e.g., compute targetSize =
Math.min(targetRect.width, targetRect.height) and set targetScale = targetSize /
LARGE_SIZE) before calling flyControlsRef.current.start so the fly-to animation
uses the actual anchor size instead of a fixed 35px.

In `@src/hooks/useBounceMorph.ts`:
- Around line 45-52: When reducedMotion changes you must not only flip stopped
but also reset the loop control flags so the animation can't freeze or carry old
stop state; inside the useEffect branch that handles reducedMotion, after
calling setStopped(true) also set cancelled.current = true and clear the stop
request (reset stopRequested — either stopRequested.current = false if it's a
ref or call setStopRequested(false) if it's state) so future restarts use a
fresh loop state.

In `@src/screens/Init/Connect.tsx`:
- Around line 62-67: LoadingLogo in fly-up mode renders a full-screen opaque
portal that blocks the cancel/back actions; to fix, stop using the blocking
portal here by either switching the animation to a non-blocking one (e.g.,
change exitMode='fly-up' to exitMode='fade') or pass props to disable the
portal/backdrop (e.g., disablePortal={true} and/or blockingBackdrop={false}) so
the underlying page remains interactive; update the LoadingLogo usage (the
component call with text='Connecting to server', done={connectDone},
exitMode='fly-up', onExitComplete={handleExitComplete}) accordingly, or extend
LoadingLogo to accept and honor these non-blocking props if they don't exist.

---

Outside diff comments:
In `@src/screens/Wallet/Receive/Amount.tsx`:
- Around line 158-164: The fauceting branch currently clears fauceting
immediately which unmounts LoadingLogo before the exit animation can run;
instead, change the success flow in handleFaucet to set the "done" flag (e.g.
call setFaucetDone(true)) rather than clearing fauceting, and move the actual
state teardown into the exit-complete handler (implement a function like
completeFaucetSuccess that calls setFauceting(false) and setFaucetSuccess(true)
— invoked from LoadingLogo or the component's onExitComplete), so LoadingLogo
can run its fly-up exit animation before the UI switches away.

---

Nitpick comments:
In `@src/providers/wallet.tsx`:
- Around line 135-153: The dev auto-init effect closes over initWallet and other
state which can become stale; update the effect to stabilize dependencies by
either (A) adding initWallet (and any used values like nsecToPrivateKey or
navigate) to the dependency array, or (B) wrap initWallet in a ref (e.g.,
initWalletRef.current) and call that ref inside the effect so the effect can
safely depend only on aspInfo.url and initialized; ensure the effect still gates
on import.meta.env.DEV and VITE_DEV_NSEC and keeps the same try/catch with
consoleError and navigate(Pages.Wallet).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7624d7fb-a579-4a3e-9eb5-f5a9b806d91a

📥 Commits

Reviewing files that changed from the base of the PR and between a511a88 and ef4d2be.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (32)
  • package.json
  • src/App.tsx
  • src/components/Loading.tsx
  • src/components/LoadingLogo.tsx
  • src/components/OnboardingLogo.tsx
  • src/components/PixelLogoSvg.tsx
  • src/components/WaitingForRound.tsx
  • src/components/WalletLoadIn.tsx
  • src/hooks/useBounceMorph.ts
  • src/icons/LoadingBar.tsx
  • src/lib/animations.ts
  • src/lib/logoAnchor.ts
  • src/providers/navigation.tsx
  • src/providers/wallet.tsx
  • src/screens/Apps/Assets/Burn.tsx
  • src/screens/Apps/Assets/Detail.tsx
  • src/screens/Apps/Assets/Import.tsx
  • src/screens/Apps/Assets/Index.tsx
  • src/screens/Apps/Assets/Mint.tsx
  • src/screens/Apps/Assets/Reissue.tsx
  • src/screens/Apps/Boltz/Swap.tsx
  • src/screens/Init/Connect.tsx
  • src/screens/Init/Restore.tsx
  • src/screens/Settings/Server.tsx
  • src/screens/Settings/Vtxos.tsx
  • src/screens/Wallet/Index.tsx
  • src/screens/Wallet/Notes/Redeem.tsx
  • src/screens/Wallet/Receive/Amount.tsx
  • src/screens/Wallet/Receive/QrCode.tsx
  • src/screens/Wallet/Send/Details.tsx
  • src/screens/Wallet/Send/Form.tsx
  • src/screens/Wallet/Unlock.tsx
💤 Files with no reviewable changes (2)
  • src/icons/LoadingBar.tsx
  • src/components/Loading.tsx

Comment thread package.json Outdated
Comment on lines +119 to +120
"optionalDependencies": {
"@rollup/rollup-darwin-x64": "^4.60.0"

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.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
npm view `@rollup/rollup-darwin-x64` os cpu --json
npm view `@rollup/rollup-darwin-arm64` os cpu --json

Repository: arkade-os/wallet

Length of output: 175


Avoid hard-coding only one Rollup native binary.

@rollup/rollup-darwin-x64 is Rollup's Intel macOS binary. Since Rollup also publishes @rollup/rollup-darwin-arm64 for Apple Silicon, adding only the x64 version in optionalDependencies means ARM64 Macs won't have the native binary available and may require a different install path or fallback behavior. Consider adding the ARM64 variant as well if this workaround is needed across all macOS developer machines.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 119 - 120, The package.json optionalDependencies
currently includes only the Intel macOS Rollup binary
(`@rollup/rollup-darwin-x64`); update optionalDependencies to also include the
Apple Silicon variant (`@rollup/rollup-darwin-arm64`) or switch to a cross-arch
solution so macOS ARM64 machines get a native binary; locate the
optionalDependencies block and add "@rollup/rollup-darwin-arm64":
"<same-version-or-^4.60.0>" (or replace the single entry with both entries) to
ensure both architectures are covered.

Comment thread src/App.tsx Resolved
Comment thread src/components/LoadingLogo.tsx Resolved
Comment on lines +45 to +52
useEffect(() => {
if (reducedMotion) {
setStopped(true)
return
}

cancelled.current = false

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.

⚠️ Potential issue | 🟡 Minor

Reset the loop state when reducedMotion changes.

This branch only flips stopped. If the preference updates after the loop has started, the logo can freeze on Invader/Heart instead of the static Arcade frame, and a later restart inherits the old stopRequested state.

🩹 Suggested change
   useEffect(() => {
     if (reducedMotion) {
+      stopRequested.current = false
+      setActiveShape(0)
       setStopped(true)
       return
     }
 
+    stopRequested.current = false
+    setStopped(false)
     cancelled.current = false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useBounceMorph.ts` around lines 45 - 52, When reducedMotion changes
you must not only flip stopped but also reset the loop control flags so the
animation can't freeze or carry old stop state; inside the useEffect branch that
handles reducedMotion, after calling setStopped(true) also set cancelled.current
= true and clear the stop request (reset stopRequested — either
stopRequested.current = false if it's a ref or call setStopRequested(false) if
it's state) so future restarts use a fresh loop state.

Comment thread src/screens/Init/Connect.tsx Resolved
@ghost

ghost commented Mar 26, 2026

Copy link
Copy Markdown

🔍 Review — arkade-os/wallet#478

Scope: Replace loading bar with bouncing/morphing pixel logo animation throughout the app. Large UI-only PR (8851+/303-), bulk from lock files.

Architecture

The approach is well-thought-out with several tricky problems solved:

  1. Boot animation orchestration (App.tsx): LoadingLogo renders as a portal overlay with opaque background, persists across Loading → Unlock → Wallet page transitions, then flies to the header logo position.

  2. Frame-sync store (logoAnchor.ts): Module-level external store synced via useSyncExternalStore in Wallet/Index.tsx. setBootAnimActive is called synchronously before React re-render, preventing blank-frame flash between LoadingLogo unmount and Wallet content paint. Smart approach.

  3. useBounceMorph hook: Clean async loop with cancellation via cancelled.current ref. The requestStop → "finish current cycle back to Arcade shape" → stopped=true flow is graceful.

  4. PixelLogoSvg extraction: Good refactor — shared between LoadingLogo and OnboardingLogo, removing the duplicated SVG+morph logic from OnboardingLogo.tsx.

Observations

  • Lock files (+8194 lines): Both bun.lock and package-lock.json are committed. If the project uses one package manager, the other lock file may be unnecessary noise. Not blocking.

  • WalletStaggerContainer hold prop: Driven directly from bootAnimActive — stagger stays in initial variant until boot animation completes. This avoids the previous useEffect-based started state which could flash one paint late. Good improvement.

  • Anchor retry loop (ANCHOR_RETRY_FRAMES = 10): Falls back to fly-up if header logo anchor isn't mounted after 10 rAF frames. Reasonable fallback, though in practice the Wallet screen should mount the anchor before the bounce loop finishes.

  • Reduced motion: Full prefers-reduced-motion support — all animations skipped. ✅

  • VITE_DEV_NSEC auto-init (wallet.tsx): New dev convenience that auto-initializes from an env nsec, bypassing onboarding. Uses nsecToPrivateKey then initWallet. Only runs in DEV mode. Fine for dev ergonomics, just ensure .env files with nsec values are gitignored.

  • Inline loading states: All Loading to LoadingLogo replacements are mechanical. The done + exitMode + onExitComplete callback pattern is consistent across Burn, Mint, Reissue, Send, Swap, Restore, Connect screens. pendingNav ref pattern ensures navigation happens after exit animation completes.

  • Unlock.tsx: Changed from showing Loading to returning null during auto-try, relying on the boot animation overlay to cover the visual gap. This creates a tight coupling — if LoadingLogo is removed or has a bug, the Unlock screen briefly renders nothing. Low risk given the overlay is always present during boot.

Security

No protocol, key handling, or transaction logic changes. Pure UI/UX. No concerns.

Verdict

Well-executed UI overhaul. The frame-sync approach to avoid visual glitches is thoughtful engineering. Main suggestion is cleaning up the dual lock file situation.

sahilc0 and others added 9 commits March 26, 2026 17:55
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>
@pietro909
pietro909 force-pushed the wt-loading-bounce-animation-20260325 branch from a13460f to 01a2e5f Compare March 26, 2026 16:59
@ghost

ghost commented Mar 26, 2026

Copy link
Copy Markdown

Followup review (2 new commits: bb28d35f, 01a2e5f0)

Lock files removed — addresses the dual lockfile feedback from previous review. ✅

Unlock.tsx rebase fix: replaced the broken Loading import with return null while unlocking, consistent with the boot animation overlay covering this state. Clean fix.

Nothing blocking.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/hooks/useBounceMorph.ts (1)

45-50: ⚠️ Potential issue | 🟡 Minor

Reset the transform when reduced motion turns on.

This is the same reduced-motion edge case flagged earlier: the state reset landed, but Line 49 still only stops the loop. Cleanup calls stop(), which leaves Framer Motion’s current y/scaleX/scaleY values in place, so toggling reduced motion mid-bounce can still leave the Arcade frame squashed or offset.

🩹 Suggested fix
   useEffect(() => {
     if (reducedMotion) {
       stopRequested.current = false
+      controlsRef.current.stop()
+      controlsRef.current.set({ y: 0, scaleY: 1, scaleX: 1 })
       setActiveShape(0)
       setStopped(true)
       return
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useBounceMorph.ts` around lines 45 - 50, When reducedMotion becomes
true in useBounceMorph, you currently stop the loop but don't reset the Framer
Motion transforms; update the reducedMotion branch to call the existing stop()
then explicitly reset the motion values for y, scaleX, and scaleY to their
defaults (e.g., via the motion controls used in useBounceMorph, such as
controls.set({...})), while preserving the existing stopRequested,
setActiveShape(0) and setStopped(true) behavior so the Arcade frame is restored
to its neutral transform when reduced motion is enabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/LoadingLogo.tsx`:
- Around line 130-170: Add proper live-region semantics so screen readers
announce the loading state: on the outer wrapper div (the fixed container in
LoadingLogo) add role="status" aria-live="polite" and aria-atomic="true"; ensure
the visible Text (the Text component rendering {text}) is inside that region so
updates are announced, and when no text is passed render a visually-hidden
accessible label (e.g., "Loading") inside the same region so assistive tech
receives a meaningful message while the animation remains decorative.
- Around line 45-111: The exit sequence (runExit) can complete after unmount and
call setVisible/onExitCompleteRef causing stale updates; fix by introducing a
cancellation flag or AbortController captured by the useEffect, check it inside
the rAF retry loop and before/after each await, and in the effect cleanup set
the flag and stop any in-flight animation via flyControlsRef.current?.stop() (or
equivalent) to prevent further flyControls.start resolution; only call
setVisible(false) and onExitCompleteRef.current?.() when not cancelled. Ensure
the flag/abort is used around getLogoAnchor retry loop, before awaiting
flyControlsRef.current.start, and before final state/callback calls in runExit
to avoid stale callbacks.

In `@src/screens/Init/Connect.tsx`:
- Around line 46-51: Clear sensitive fields from FlowContext immediately in
handleProceed instead of waiting for pendingNav/onExitComplete: call
setInitInfo({ ...initInfo, password: undefined, privateKey: undefined })
synchronously in handleProceed (before starting the exit animation or setting
pendingNav), then set pendingNav.current to just perform navigation
(navigate(Pages.Wallet)) or any non-sensitive follow-up. Update handleProceed to
remove any deferred clearing logic so password/privateKey are not retained
during the animation.

---

Duplicate comments:
In `@src/hooks/useBounceMorph.ts`:
- Around line 45-50: When reducedMotion becomes true in useBounceMorph, you
currently stop the loop but don't reset the Framer Motion transforms; update the
reducedMotion branch to call the existing stop() then explicitly reset the
motion values for y, scaleX, and scaleY to their defaults (e.g., via the motion
controls used in useBounceMorph, such as controls.set({...})), while preserving
the existing stopRequested, setActiveShape(0) and setStopped(true) behavior so
the Arcade frame is restored to its neutral transform when reduced motion is
enabled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4aaf74ef-bdf4-49a3-90fe-a4cdfafcf745

📥 Commits

Reviewing files that changed from the base of the PR and between a13460f and 01a2e5f.

📒 Files selected for processing (30)
  • src/App.tsx
  • src/components/Loading.tsx
  • src/components/LoadingLogo.tsx
  • src/components/OnboardingLogo.tsx
  • src/components/PixelLogoSvg.tsx
  • src/components/WaitingForRound.tsx
  • src/components/WalletLoadIn.tsx
  • src/hooks/useBounceMorph.ts
  • src/icons/LoadingBar.tsx
  • src/lib/animations.ts
  • src/lib/logoAnchor.ts
  • src/providers/navigation.tsx
  • src/screens/Apps/Assets/Burn.tsx
  • src/screens/Apps/Assets/Detail.tsx
  • src/screens/Apps/Assets/Import.tsx
  • src/screens/Apps/Assets/Index.tsx
  • src/screens/Apps/Assets/Mint.tsx
  • src/screens/Apps/Assets/Reissue.tsx
  • src/screens/Apps/Boltz/Swap.tsx
  • src/screens/Init/Connect.tsx
  • src/screens/Init/Restore.tsx
  • src/screens/Settings/Server.tsx
  • src/screens/Settings/Vtxos.tsx
  • src/screens/Wallet/Index.tsx
  • src/screens/Wallet/Notes/Redeem.tsx
  • src/screens/Wallet/Receive/Amount.tsx
  • src/screens/Wallet/Receive/QrCode.tsx
  • src/screens/Wallet/Send/Details.tsx
  • src/screens/Wallet/Send/Form.tsx
  • src/screens/Wallet/Unlock.tsx
💤 Files with no reviewable changes (2)
  • src/components/Loading.tsx
  • src/icons/LoadingBar.tsx
✅ Files skipped from review due to trivial changes (9)
  • src/screens/Wallet/Send/Form.tsx
  • src/providers/navigation.tsx
  • src/screens/Wallet/Receive/QrCode.tsx
  • src/screens/Wallet/Receive/Amount.tsx
  • src/screens/Apps/Assets/Import.tsx
  • src/screens/Apps/Assets/Index.tsx
  • src/screens/Settings/Server.tsx
  • src/screens/Apps/Assets/Detail.tsx
  • src/screens/Wallet/Notes/Redeem.tsx
🚧 Files skipped from review as they are similar to previous changes (11)
  • src/screens/Apps/Boltz/Swap.tsx
  • src/screens/Apps/Assets/Mint.tsx
  • src/screens/Wallet/Send/Details.tsx
  • src/screens/Wallet/Index.tsx
  • src/lib/logoAnchor.ts
  • src/screens/Wallet/Unlock.tsx
  • src/components/WalletLoadIn.tsx
  • src/lib/animations.ts
  • src/components/PixelLogoSvg.tsx
  • src/components/WaitingForRound.tsx
  • src/App.tsx

Comment on lines +45 to +111
// When bounce loop has stopped, play exit animation
useEffect(() => {
if (!stopped || !done) return

const mode = exitModeRef.current

if (mode === 'none') {
onExitCompleteRef.current?.()
return
}

if (reducedMotion) {
setVisible(false)
onExitCompleteRef.current?.()
return
}

const flyUp = () =>
flyControlsRef.current.start({
y: -200,
scale: 0.5,
opacity: 0,
transition: { duration: 0.35, ease: EASE_OUT_QUINT_TUPLE },
})

async function runExit() {
if (mode === 'fly-to-target') {
// Wait for anchor to be mounted (may lag behind page render)
let target = getLogoAnchor()
if (!target) {
for (let i = 0; i < ANCHOR_RETRY_FRAMES; i++) {
await new Promise<void>((r) => requestAnimationFrame(() => r()))
target = getLogoAnchor()
if (target) break
}
}
const container = containerRef.current
if (target && container) {
const targetRect = target.getBoundingClientRect()
const containerRect = container.getBoundingClientRect()

const dx = targetRect.left + targetRect.width / 2 - (containerRect.left + containerRect.width / 2)
const dy = targetRect.top + targetRect.height / 2 - (containerRect.top + containerRect.height / 2)
const targetScale = Math.min(targetRect.width, targetRect.height) / LARGE_SIZE

// Fly to the logo position (background stays opaque throughout)
await flyControlsRef.current.start({
x: dx,
y: dy,
scale: targetScale,
opacity: 1,
transition: { duration: 0.4, ease: EASE_IN_OUT_QUINT_TUPLE },
})
} else {
await flyUp()
}
} else {
await flyUp()
}

setVisible(false)
onExitCompleteRef.current?.()
}

runExit()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [stopped, done])

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, confirm the file exists and check line count
wc -l src/components/LoadingLogo.tsx

Repository: arkade-os/wallet

Length of output: 94


🏁 Script executed:

# Read the exact lines 45-111 to confirm the code snippet
sed -n '45,111p' src/components/LoadingLogo.tsx

Repository: arkade-os/wallet

Length of output: 2094


🏁 Script executed:

# Check the full useEffect context including dependency array
sed -n '40,115p' src/components/LoadingLogo.tsx

Repository: arkade-os/wallet

Length of output: 2330


🏁 Script executed:

# Find the flyControlsRef declaration and its type
sed -n '1,50p' src/components/LoadingLogo.tsx

Repository: arkade-os/wallet

Length of output: 1856


🏁 Script executed:

# Look for useRef declarations
grep -n "flyControlsRef\|containerRef\|exitModeRef\|onExitCompleteRef" src/components/LoadingLogo.tsx | head -20

Repository: arkade-os/wallet

Length of output: 787


🏁 Script executed:

# Check the imports and type of motion control
head -30 src/components/LoadingLogo.tsx

Repository: arkade-os/wallet

Length of output: 1282


🏁 Script executed:

# Check how onExitComplete is used in App.tsx
grep -n "onExitComplete\|LoadingLogo" src/App.tsx | head -20

Repository: arkade-os/wallet

Length of output: 292


🏁 Script executed:

# Check the actual usage context in App.tsx around line 383
sed -n '375,395p' src/App.tsx

Repository: arkade-os/wallet

Length of output: 468


🏁 Script executed:

# Check Connect.tsx usage
grep -n "onExitComplete\|LoadingLogo" src/screens/Init/Connect.tsx | head -20

Repository: arkade-os/wallet

Length of output: 189


🏁 Script executed:

# Check the actual usage in Connect.tsx around line 62
sed -n '55,75p' src/screens/Init/Connect.tsx

Repository: arkade-os/wallet

Length of output: 540


🏁 Script executed:

# Search for framer-motion AnimationControls.stop() in docs or verify method exists
web_search
framer-motion AnimationControls stop method

Repository: arkade-os/wallet

Length of output: 162


🌐 Web query:

framer-motion AnimationControls.stop method API

💡 Result:

The framer-motion AnimationControls.stop method immediately stops any running animations on all motion components linked to those controls. AnimationControls are obtained via the useAnimationControls hook (previously useAnimation in older versions): import { motion, useAnimationControls } from "framer-motion" function MyComponent { const controls = useAnimationControls // Link controls to motion component return <motion.div animate={controls} /> } Usage: controls.stop // Stops all animations on linked components This applies the current animated values as the final styles and cancels any pending animations. Note: Official docs recommend useAnimate as the preferred modern alternative for manual animations, which returns an animate function with its own playback controls including stop. However, AnimationControls remains available for variant-based animations. Sources confirm this API from Framer's official documentation, consistent across versions up to Motion 12.x (successor to framer-motion).

Citations:


Add cleanup to cancel the exit animation and prevent stale callbacks after unmount.

The runExit() async function starting at line 109 can await multiple promises (the rAF retry loop in the fly-to-target path and flyControls.start()) before calling setVisible(false) and onExitCompleteRef.current?.(). If the component unmounts while these awaits are pending, the callbacks will still execute, causing stale state updates and navigation triggers in src/App.tsx and src/screens/Init/Connect.tsx.

🧯 Suggested fix
   useEffect(() => {
     if (!stopped || !done) return
+    let cancelled = false
 
     const mode = exitModeRef.current
 
     if (mode === 'none') {
-      onExitCompleteRef.current?.()
+      if (!cancelled) onExitCompleteRef.current?.()
       return
     }
 
     if (reducedMotion) {
-      setVisible(false)
-      onExitCompleteRef.current?.()
+      if (!cancelled) {
+        setVisible(false)
+        onExitCompleteRef.current?.()
+      }
       return
     }
 
     const flyUp = () =>
       flyControlsRef.current.start({
         y: -200,
         scale: 0.5,
         opacity: 0,
         transition: { duration: 0.35, ease: EASE_OUT_QUINT_TUPLE },
       })
 
     async function runExit() {
       if (mode === 'fly-to-target') {
         // Wait for anchor to be mounted (may lag behind page render)
         let target = getLogoAnchor()
         if (!target) {
           for (let i = 0; i < ANCHOR_RETRY_FRAMES; i++) {
             await new Promise<void>((r) => requestAnimationFrame(() => r()))
+            if (cancelled) return
             target = getLogoAnchor()
             if (target) break
           }
         }
         const container = containerRef.current
         if (target && container) {
           const targetRect = target.getBoundingClientRect()
           const containerRect = container.getBoundingClientRect()
 
           const dx = targetRect.left + targetRect.width / 2 - (containerRect.left + containerRect.width / 2)
           const dy = targetRect.top + targetRect.height / 2 - (containerRect.top + containerRect.height / 2)
           const targetScale = Math.min(targetRect.width, targetRect.height) / LARGE_SIZE
 
           // Fly to the logo position (background stays opaque throughout)
           await flyControlsRef.current.start({
             x: dx,
             y: dy,
             scale: targetScale,
             opacity: 1,
             transition: { duration: 0.4, ease: EASE_IN_OUT_QUINT_TUPLE },
           })
         } else {
           await flyUp()
         }
       } else {
         await flyUp()
       }
 
+      if (cancelled) return
       setVisible(false)
       onExitCompleteRef.current?.()
     }
 
-    runExit()
+    void runExit()
+    return () => {
+      cancelled = true
+      flyControlsRef.current.stop()
+    }
     // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [stopped, done])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/LoadingLogo.tsx` around lines 45 - 111, The exit sequence
(runExit) can complete after unmount and call setVisible/onExitCompleteRef
causing stale updates; fix by introducing a cancellation flag or AbortController
captured by the useEffect, check it inside the rAF retry loop and before/after
each await, and in the effect cleanup set the flag and stop any in-flight
animation via flyControlsRef.current?.stop() (or equivalent) to prevent further
flyControls.start resolution; only call setVisible(false) and
onExitCompleteRef.current?.() when not cancelled. Ensure the flag/abort is used
around getLogoAnchor retry loop, before awaiting flyControlsRef.current.start,
and before final state/callback calls in runExit to avoid stale callbacks.

Comment on lines +130 to +170
<div
style={{
position: 'fixed',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
zIndex: 10,
flexDirection: 'column',
gap: '1rem',
}}
>
<motion.div
ref={containerRef}
animate={flyControls}
style={{ position: 'relative', x: 0, y: 0, scale: 1, opacity: 1 }}
>
<motion.div
initial={reducedMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.25, ease: EASE_OUT_QUINT_TUPLE }}
>
<motion.div animate={bounceControls} style={{ y: 0, scaleY: 1, scaleX: 1 }}>
<PixelLogoSvg activeShape={activeShape} size={LARGE_SIZE} />
</motion.div>
</motion.div>
<PixelSplash bounceCount={bounceCount} reducedMotion={reducedMotion} />
</motion.div>
{text ? (
<motion.div
style={{ paddingTop: '0.5rem' }}
animate={{ opacity: exiting ? 0 : 1 }}
transition={{ duration: 0.2, ease: EASE_OUT_QUINT_TUPLE }}
>
<Text centered small wrap heading>
{text}
</Text>
</motion.div>
) : null}
</div>

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.

⚠️ Potential issue | 🟠 Major

Expose the loading portal as a live status region.

This component now replaces loading UI across boot and inline flows, but the portal has no role="status"/aria-live semantics. Screen-reader users get a decorative animation with no announced loading state.

♿ Suggested fix
       <div
+        role='status'
+        aria-live='polite'
+        aria-atomic='true'
+        aria-label={text ?? 'Loading'}
         style={{
           position: 'fixed',
           inset: 0,
@@
         <motion.div
+          aria-hidden='true'
           ref={containerRef}
           animate={flyControls}
           style={{ position: 'relative', x: 0, y: 0, scale: 1, opacity: 1 }}
         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div
style={{
position: 'fixed',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
zIndex: 10,
flexDirection: 'column',
gap: '1rem',
}}
>
<motion.div
ref={containerRef}
animate={flyControls}
style={{ position: 'relative', x: 0, y: 0, scale: 1, opacity: 1 }}
>
<motion.div
initial={reducedMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.25, ease: EASE_OUT_QUINT_TUPLE }}
>
<motion.div animate={bounceControls} style={{ y: 0, scaleY: 1, scaleX: 1 }}>
<PixelLogoSvg activeShape={activeShape} size={LARGE_SIZE} />
</motion.div>
</motion.div>
<PixelSplash bounceCount={bounceCount} reducedMotion={reducedMotion} />
</motion.div>
{text ? (
<motion.div
style={{ paddingTop: '0.5rem' }}
animate={{ opacity: exiting ? 0 : 1 }}
transition={{ duration: 0.2, ease: EASE_OUT_QUINT_TUPLE }}
>
<Text centered small wrap heading>
{text}
</Text>
</motion.div>
) : null}
</div>
<div
role='status'
aria-live='polite'
aria-atomic='true'
aria-label={text ?? 'Loading'}
style={{
position: 'fixed',
inset: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
pointerEvents: 'none',
zIndex: 10,
flexDirection: 'column',
gap: '1rem',
}}
>
<motion.div
aria-hidden='true'
ref={containerRef}
animate={flyControls}
style={{ position: 'relative', x: 0, y: 0, scale: 1, opacity: 1 }}
>
<motion.div
initial={reducedMotion ? false : { opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.25, ease: EASE_OUT_QUINT_TUPLE }}
>
<motion.div animate={bounceControls} style={{ y: 0, scaleY: 1, scaleX: 1 }}>
<PixelLogoSvg activeShape={activeShape} size={LARGE_SIZE} />
</motion.div>
</motion.div>
<PixelSplash bounceCount={bounceCount} reducedMotion={reducedMotion} />
</motion.div>
{text ? (
<motion.div
style={{ paddingTop: '0.5rem' }}
animate={{ opacity: exiting ? 0 : 1 }}
transition={{ duration: 0.2, ease: EASE_OUT_QUINT_TUPLE }}
>
<Text centered small wrap heading>
{text}
</Text>
</motion.div>
) : null}
</div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/LoadingLogo.tsx` around lines 130 - 170, Add proper
live-region semantics so screen readers announce the loading state: on the outer
wrapper div (the fixed container in LoadingLogo) add role="status"
aria-live="polite" and aria-atomic="true"; ensure the visible Text (the Text
component rendering {text}) is inside that region so updates are announced, and
when no text is passed render a visually-hidden accessible label (e.g.,
"Loading") inside the same region so assistive tech receives a meaningful
message while the animation remains decorative.

Comment on lines 46 to +51
const handleProceed = () => {
setInitInfo({ ...initInfo, password: undefined, privateKey: undefined })
navigate(Pages.Wallet)
pendingNav.current = () => {
setInitInfo({ ...initInfo, password: undefined, privateKey: undefined })
navigate(Pages.Wallet)
}
setConnectDone(true)

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.

⚠️ Potential issue | 🟠 Major

Clear initInfo secrets before starting the exit animation.

Line 47 defers clearing password and privateKey until onExitComplete. The wallet is already initialized here, so keeping both values in FlowContext for the animation window is unnecessary and widens the exposure surface if the exit callback never fires.

🔐 Suggested fix
   const handleProceed = () => {
+    setInitInfo({ ...initInfo, password: undefined, privateKey: undefined })
     pendingNav.current = () => {
-      setInitInfo({ ...initInfo, password: undefined, privateKey: undefined })
       navigate(Pages.Wallet)
     }
     setConnectDone(true)
   }
Based on learnings, restore keys are treated as sensitive data and should not remain in UI state longer than necessary.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleProceed = () => {
setInitInfo({ ...initInfo, password: undefined, privateKey: undefined })
navigate(Pages.Wallet)
pendingNav.current = () => {
setInitInfo({ ...initInfo, password: undefined, privateKey: undefined })
navigate(Pages.Wallet)
}
setConnectDone(true)
const handleProceed = () => {
setInitInfo({ ...initInfo, password: undefined, privateKey: undefined })
pendingNav.current = () => {
navigate(Pages.Wallet)
}
setConnectDone(true)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/screens/Init/Connect.tsx` around lines 46 - 51, Clear sensitive fields
from FlowContext immediately in handleProceed instead of waiting for
pendingNav/onExitComplete: call setInitInfo({ ...initInfo, password: undefined,
privateKey: undefined }) synchronously in handleProceed (before starting the
exit animation or setting pendingNav), then set pendingNav.current to just
perform navigation (navigate(Pages.Wallet)) or any non-sensitive follow-up.
Update handleProceed to remove any deferred clearing logic so
password/privateKey are not retained during the animation.

@sahilc0
sahilc0 merged commit 310ed29 into master Mar 26, 2026
5 checks passed
@bordalix
bordalix deleted the wt-loading-bounce-animation-20260325 branch May 18, 2026 16:15
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