Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
| // Shake detection for mobile | ||
| useEffect(() => { | ||
| // Only set up if permission was granted | ||
| const permissionGranted = localStorage.getItem(MOTION_PERMISSION_KEY); | ||
| if (!permissionGranted) return; | ||
|
|
||
| // Use shake.js library | ||
| const Shake = require('shake.js'); | ||
| const myShakeEvent = new Shake({ | ||
| threshold: 15, // Sensitivity of shake detection | ||
| timeout: 1000, // Time between shakes | ||
| }); | ||
|
|
||
| myShakeEvent.start(); | ||
|
|
||
| const handleShake = () => { | ||
| setShowModal(true); | ||
| }; | ||
|
|
||
| window.addEventListener('shake', handleShake, false); | ||
|
|
||
| return () => { | ||
| window.removeEventListener('shake', handleShake, false); | ||
| myShakeEvent.stop(); | ||
| }; | ||
| }, []); |
There was a problem hiding this comment.
The shake detection effect runs once on mount with empty dependencies. If the user grants motion permission after page load, the shake detector won't be initialized because the effect won't re-run.
View Details
📝 Patch Details
diff --git a/apps/scan/src/app/(home)/(overview)/_components/verified-filter-wrapper.tsx b/apps/scan/src/app/(home)/(overview)/_components/verified-filter-wrapper.tsx
index 1a7519b9..0882cdff 100644
--- a/apps/scan/src/app/(home)/(overview)/_components/verified-filter-wrapper.tsx
+++ b/apps/scan/src/app/(home)/(overview)/_components/verified-filter-wrapper.tsx
@@ -131,6 +131,25 @@ const VerifiedToggleSwitch = () => {
// Dialog with keyboard shortcut (desktop) and shake detection (mobile)
const VerifiedFilterDialog = () => {
const [showModal, setShowModal] = useState(false);
+ const [permissionGranted, setPermissionGranted] = useState(
+ () => localStorage.getItem(MOTION_PERMISSION_KEY) !== null
+ );
+
+ // Listen for permission changes in localStorage
+ useEffect(() => {
+ const handleStorageChange = (e: StorageEvent) => {
+ if (e.key === MOTION_PERMISSION_KEY) {
+ setPermissionGranted(e.newValue !== null);
+ }
+ };
+
+ // Listen for storage changes from other tabs/windows
+ window.addEventListener('storage', handleStorageChange);
+
+ return () => {
+ window.removeEventListener('storage', handleStorageChange);
+ };
+ }, []);
// Keyboard shortcut for desktop (triple tap 'v')
useEffect(() => {
@@ -172,7 +191,6 @@ const VerifiedFilterDialog = () => {
// Shake detection for mobile
useEffect(() => {
// Only set up if permission was granted
- const permissionGranted = localStorage.getItem(MOTION_PERMISSION_KEY);
if (!permissionGranted) return;
// Use shake.js library
@@ -194,7 +212,7 @@ const VerifiedFilterDialog = () => {
window.removeEventListener('shake', handleShake, false);
myShakeEvent.stop();
};
- }, []);
+ }, [permissionGranted]);
return (
<Dialog open={showModal} onOpenChange={setShowModal}>
Analysis
Shake detector not initialized when motion permission granted after page load
What fails: VerifiedFilterDialog component does not initialize the shake detector if the user grants motion permission after the initial page load. The shake detection effect has an empty dependency array but depends on localStorage state that can change.
How to reproduce:
- Load the page on an iOS device or browser that supports motion events requiring permission
- The
VerifiedFilterDialogmounts and its useEffect runs with empty dependencies - At this point,
MOTION_PERMISSION_KEYis not in localStorage yet, so the effect exits early - Wait 2 seconds for
MotionPermissionPromptto appear - Click "Enable" to grant motion permission
MotionPermissionPromptsets the localStorage flag- Result: Shake detector never initializes even though permission was granted
- Expected: Shake detector should initialize when permission is granted
Root cause: The shake detection effect had an empty dependency array [] but checked localStorage.getItem(MOTION_PERMISSION_KEY) inside. This meant the effect only ran once on mount, before permission was granted. When the user later granted permission by setting the localStorage flag, the effect had no mechanism to re-run.
Fix applied:
- Added a
permissionGrantedstate variable that tracks whether the motion permission has been granted - Created a separate effect to listen for storage changes and update this state
- Modified the shake detection effect to depend on
permissionGranted, so it runs whenever the permission status changes - This ensures the shake detector initializes as soon as permission is granted, regardless of when the user grants it
The fix properly handles the async nature of the permission flow while maintaining clean React patterns with appropriate dependency arrays.
There was a problem hiding this comment.
Additional Suggestion:
The getAcceptsAddresses() API return type changed from returning a direct mapping to returning { addressToOrigins, originVerification }, but this file wasn't updated. This causes Object.keys(originsByAddress) to return ['addressToOrigins', 'originVerification'] instead of addresses, which will fail address validation.
View Details
📝 Patch Details
diff --git a/apps/scan/src/trpc/routers/public/stats.ts b/apps/scan/src/trpc/routers/public/stats.ts
index 837310a7..63bc6791 100644
--- a/apps/scan/src/trpc/routers/public/stats.ts
+++ b/apps/scan/src/trpc/routers/public/stats.ts
@@ -36,14 +36,14 @@ export const statsRouter = createTRPCRouter({
overall: publicProcedure
.input(overallStatisticsMVInputSchema)
.query(async ({ input, ctx }) => {
- const originsByAddress = await getAcceptsAddresses({
+ const { addressToOrigins } = await getAcceptsAddresses({
chain: input.chain,
});
return await getOverallStatisticsMV(
{
...input,
recipients: {
- include: Object.keys(originsByAddress).map(addr =>
+ include: Object.keys(addressToOrigins).map(addr =>
mixedAddressSchema.parse(addr)
),
},
@@ -54,14 +54,14 @@ export const statsRouter = createTRPCRouter({
bucketed: publicProcedure
.input(bucketedStatisticsMVInputSchema)
.query(async ({ input, ctx }) => {
- const originsByAddress = await getAcceptsAddresses({
+ const { addressToOrigins } = await getAcceptsAddresses({
chain: input.chain,
});
return await getBucketedStatisticsMV(
{
...input,
recipients: {
- include: Object.keys(originsByAddress).map(addr =>
+ include: Object.keys(addressToOrigins).map(addr =>
mixedAddressSchema.parse(addr)
),
},
Analysis
API return type change not reflected in stats.ts
What fails: The bazaar.overall and bazaar.bucketed procedures in apps/scan/src/trpc/routers/public/stats.ts call getAcceptsAddresses() and attempt to use the entire returned object as a mapping with Object.keys(). Since the API now returns { addressToOrigins, originVerification } instead of a direct mapping, Object.keys() returns ['addressToOrigins', 'originVerification'] instead of actual addresses. These keys are then passed to mixedAddressSchema.parse(), which validates Ethereum/Solana addresses and will reject the strings 'addressToOrigins' and 'originVerification', causing a Zod validation error at runtime.
How to reproduce:
- Make a request to the
bazaar.overallorbazaar.bucketedprocedures - The endpoint throws a Zod validation error:
Invalid addresswhen trying to parse 'addressToOrigins' or 'originVerification'
Result vs Expected:
- Result: Runtime error when validating non-address keys
- Expected: Should extract the
addressToOriginsproperty and use those keys (actual addresses) for validation
Solution: Update both procedures to destructure the return value: const { addressToOrigins } = await getAcceptsAddresses(...) and use addressToOrigins in Object.keys(). This pattern is already correctly used in other files (origins.ts, list-mv.ts, bucketed-mv.ts, overall-mv.ts, list.ts).
| const handleLogoClick = () => { | ||
| const newCount = clickCount + 1; | ||
| setClickCount(newCount); | ||
|
|
||
| if (newCount === 5) { | ||
| // Dispatch custom event to open modal | ||
| window.dispatchEvent(new CustomEvent('open-verified-filter-modal')); | ||
| setClickCount(0); // Reset counter | ||
| } | ||
|
|
||
| // Reset counter after 1 second of no clicks | ||
| setTimeout(() => { | ||
| setClickCount(0); | ||
| }, 1000); | ||
| }; |
There was a problem hiding this comment.
The timeout management in the logo click handler doesn't properly reset the counter. Each click creates a new timeout without clearing previous ones, which can cause the click counter to reset unexpectedly if there's a delay between clicks, making it difficult or impossible to trigger the 5-click easter egg.
View Details
📝 Patch Details
diff --git a/apps/scan/src/app/(home)/(overview)/_components/heading.tsx b/apps/scan/src/app/(home)/(overview)/_components/heading.tsx
index c8f2a3a9..49bbe440 100644
--- a/apps/scan/src/app/(home)/(overview)/_components/heading.tsx
+++ b/apps/scan/src/app/(home)/(overview)/_components/heading.tsx
@@ -10,12 +10,18 @@ import { Button } from '@/components/ui/button';
import { Logo } from '@/components/logo';
import { SearchButton } from './search-button';
-import { useState } from 'react';
+import { useState, useRef } from 'react';
export const HomeHeading = () => {
const [clickCount, setClickCount] = useState(0);
+ const timeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleLogoClick = () => {
+ // Clear previous timeout if it exists
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ }
+
const newCount = clickCount + 1;
setClickCount(newCount);
@@ -23,11 +29,14 @@ export const HomeHeading = () => {
// Dispatch custom event to open modal
window.dispatchEvent(new CustomEvent('open-verified-filter-modal'));
setClickCount(0); // Reset counter
+ timeoutRef.current = null;
+ return;
}
// Reset counter after 1 second of no clicks
- setTimeout(() => {
+ timeoutRef.current = setTimeout(() => {
setClickCount(0);
+ timeoutRef.current = null;
}, 1000);
};
Analysis
Multiple Timeouts Prevent 5-Click Easter Egg in Logo Click Handler
What fails: The handleLogoClick() function in apps/scan/src/app/(home)/(overview)/_components/heading.tsx creates a new 1-second timeout on every click without clearing previous timeouts. When clicks are spaced apart, multiple timeouts accumulate and fire independently, resetting the click counter unexpectedly and preventing the user from reaching 5 clicks to trigger the easter egg event.
How to reproduce:
- Open the application in the browser
- Click the logo/heading area slowly with ~400ms delay between clicks
- After click 2, wait for the first timeout to expire (~1 second after click 1)
- The click counter resets to 0 despite click 2 having occurred
- Attempt to reach 5 clicks - this becomes impossible due to unexpected resets
What happens: Multiple setTimeout() calls accumulate in the event queue. Each creates its own independent timeout that will fire and call setClickCount(0). When click 1 occurs at t=0 and click 2 at t=400ms:
- Timeout A fires at t=1000ms → counter resets to 0
- Timeout B fires at t=1400ms → counter resets to 0 again
- Click 3 at t=1100ms sees counter already reset to 0, not the expected count of 2
Expected: Only one timeout should be active at any time. When a new click occurs, the previous timeout should be cleared before creating a new one. This ensures the counter only resets after 1 second of NO clicks, allowing users to reliably reach 5 clicks to trigger the easter egg.
Fix implemented: Use a useRef to maintain a reference to the active timeout ID, clear any previous timeout before setting a new one, and set the ref to null after the timeout fires or when the 5-click event triggers. This ensures only one timer is ever active simultaneously.
This is mostly a hidden concept in x402scan for now as we have not educated the masses on adopting this.
for now to trigger:
This persists in session storage