Repository navigation
feat(ui): replace loading bar with bounce/morph logo animation - #478
Conversation
Deploying wallet-mutinynet with
|
| 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 |
WalkthroughThe PR replaces the simple Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Deploying wallet-bitcoin with
|
| 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 |
🔍 Arkana Review —
|
There was a problem hiding this comment.
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 | 🟡 MinorThis faucet flow still skips the new exit animation.
LoadingLogoonly helps here if success is routed throughdone/onExitComplete. The current success path clearsfaucetingimmediately, 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:
Missing
initWalletin deps: While theeslint-disableacknowledges this,initWalletcloses overaspInfo,config, andwalletstate. 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.Navigate after state update: The
setInitialized(true)ininitWallet(line 418) runs beforenavigate(Pages.Wallet)returns, but React's async state batching means theinitializedstate may not reflect immediately inApp.tsx'sshouldHoldOnLoadingcheck. 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
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.lockpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (32)
package.jsonsrc/App.tsxsrc/components/Loading.tsxsrc/components/LoadingLogo.tsxsrc/components/OnboardingLogo.tsxsrc/components/PixelLogoSvg.tsxsrc/components/WaitingForRound.tsxsrc/components/WalletLoadIn.tsxsrc/hooks/useBounceMorph.tssrc/icons/LoadingBar.tsxsrc/lib/animations.tssrc/lib/logoAnchor.tssrc/providers/navigation.tsxsrc/providers/wallet.tsxsrc/screens/Apps/Assets/Burn.tsxsrc/screens/Apps/Assets/Detail.tsxsrc/screens/Apps/Assets/Import.tsxsrc/screens/Apps/Assets/Index.tsxsrc/screens/Apps/Assets/Mint.tsxsrc/screens/Apps/Assets/Reissue.tsxsrc/screens/Apps/Boltz/Swap.tsxsrc/screens/Init/Connect.tsxsrc/screens/Init/Restore.tsxsrc/screens/Settings/Server.tsxsrc/screens/Settings/Vtxos.tsxsrc/screens/Wallet/Index.tsxsrc/screens/Wallet/Notes/Redeem.tsxsrc/screens/Wallet/Receive/Amount.tsxsrc/screens/Wallet/Receive/QrCode.tsxsrc/screens/Wallet/Send/Details.tsxsrc/screens/Wallet/Send/Form.tsxsrc/screens/Wallet/Unlock.tsx
💤 Files with no reviewable changes (2)
- src/icons/LoadingBar.tsx
- src/components/Loading.tsx
| "optionalDependencies": { | ||
| "@rollup/rollup-darwin-x64": "^4.60.0" |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
npm view `@rollup/rollup-darwin-x64` os cpu --json
npm view `@rollup/rollup-darwin-arm64` os cpu --jsonRepository: 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.
| useEffect(() => { | ||
| if (reducedMotion) { | ||
| setStopped(true) | ||
| return | ||
| } | ||
|
|
||
| cancelled.current = false | ||
|
|
There was a problem hiding this comment.
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.
🔍 Review —
|
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>
a13460f to
01a2e5f
Compare
|
Followup review (2 new commits: Lock files removed — addresses the dual lockfile feedback from previous review. ✅ Unlock.tsx rebase fix: replaced the broken Nothing blocking. |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
src/hooks/useBounceMorph.ts (1)
45-50:⚠️ Potential issue | 🟡 MinorReset 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 currenty/scaleX/scaleYvalues 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
📒 Files selected for processing (30)
src/App.tsxsrc/components/Loading.tsxsrc/components/LoadingLogo.tsxsrc/components/OnboardingLogo.tsxsrc/components/PixelLogoSvg.tsxsrc/components/WaitingForRound.tsxsrc/components/WalletLoadIn.tsxsrc/hooks/useBounceMorph.tssrc/icons/LoadingBar.tsxsrc/lib/animations.tssrc/lib/logoAnchor.tssrc/providers/navigation.tsxsrc/screens/Apps/Assets/Burn.tsxsrc/screens/Apps/Assets/Detail.tsxsrc/screens/Apps/Assets/Import.tsxsrc/screens/Apps/Assets/Index.tsxsrc/screens/Apps/Assets/Mint.tsxsrc/screens/Apps/Assets/Reissue.tsxsrc/screens/Apps/Boltz/Swap.tsxsrc/screens/Init/Connect.tsxsrc/screens/Init/Restore.tsxsrc/screens/Settings/Server.tsxsrc/screens/Settings/Vtxos.tsxsrc/screens/Wallet/Index.tsxsrc/screens/Wallet/Notes/Redeem.tsxsrc/screens/Wallet/Receive/Amount.tsxsrc/screens/Wallet/Receive/QrCode.tsxsrc/screens/Wallet/Send/Details.tsxsrc/screens/Wallet/Send/Form.tsxsrc/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
| // 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]) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, confirm the file exists and check line count
wc -l src/components/LoadingLogo.tsxRepository: 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.tsxRepository: arkade-os/wallet
Length of output: 2094
🏁 Script executed:
# Check the full useEffect context including dependency array
sed -n '40,115p' src/components/LoadingLogo.tsxRepository: arkade-os/wallet
Length of output: 2330
🏁 Script executed:
# Find the flyControlsRef declaration and its type
sed -n '1,50p' src/components/LoadingLogo.tsxRepository: arkade-os/wallet
Length of output: 1856
🏁 Script executed:
# Look for useRef declarations
grep -n "flyControlsRef\|containerRef\|exitModeRef\|onExitCompleteRef" src/components/LoadingLogo.tsx | head -20Repository: arkade-os/wallet
Length of output: 787
🏁 Script executed:
# Check the imports and type of motion control
head -30 src/components/LoadingLogo.tsxRepository: 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 -20Repository: 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.tsxRepository: arkade-os/wallet
Length of output: 468
🏁 Script executed:
# Check Connect.tsx usage
grep -n "onExitComplete\|LoadingLogo" src/screens/Init/Connect.tsx | head -20Repository: 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.tsxRepository: 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 methodRepository: 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:
- 1: https://www.framer.com/motion/use-animation-controls/
- 2: https://www.framer.com/motion/utilities/
- 3: https://github.com/code/lib-framer-motion
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.
| <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> |
There was a problem hiding this comment.
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.
| <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.
| const handleProceed = () => { | ||
| setInitInfo({ ...initInfo, password: undefined, privateKey: undefined }) | ||
| navigate(Pages.Wallet) | ||
| pendingNav.current = () => { | ||
| setInitInfo({ ...initInfo, password: undefined, privateKey: undefined }) | ||
| navigate(Pages.Wallet) | ||
| } | ||
| setConnectDone(true) |
There was a problem hiding this comment.
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)
}📝 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.
| 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.
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)
Boot animation sequence (new users)
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
logoAnchor.ts) synced synchronously before React re-render so Wallet reads correctbootAnimActiveon the same frame LoadingLogo unmounts — no blank frame flashholdprop (no useEffect delay) to avoid one-paint-late blank framevisibility: hiddenduring boot animation prevents double-logo appearance during fly-to-targetprefers-reduced-motionsupport — skips all animationsFiles changed
src/components/LoadingLogo.tsx— new bounce/morph loading componentsrc/components/PixelLogoSvg.tsx— morphing pixel logo SVGsrc/hooks/useBounceMorph.ts— bounce loop + morph sequencing hooksrc/components/WalletLoadIn.tsx— stagger container withholdpropsrc/lib/logoAnchor.ts— module-level store for boot animation state + logo anchorsrc/lib/animations.ts— added ease-in-out-quint, tightened stagger to 300mssrc/App.tsx— boot animation orchestration with sync store updatessrc/screens/Wallet/Index.tsx— header logo visibility + stagger coordination<Loading />updated to<LoadingLogo />Test plan
--ion-background-colorSummary by CodeRabbit
Release Notes