diff --git a/pages/popup/src/Popup.tsx b/pages/popup/src/Popup.tsx index 47187fb..02b332b 100644 --- a/pages/popup/src/Popup.tsx +++ b/pages/popup/src/Popup.tsx @@ -1,12 +1,34 @@ import { withErrorBoundary, withSuspense } from '@extension/shared'; +import { Button, Flex, Spinner, Text } from '@chakra-ui/react'; import EventsViewer from './components/Events'; const Popup = () => { return (
- +
); }; -export default withErrorBoundary(withSuspense(Popup,
Loading ...
),
Error Occur
); +const LoadingFallback = ( + + + + Loading... + + +); + +const ErrorFallback = ( + + Something went wrong + + The approval window hit an unexpected error. You can close this window and retry the request in your dapp. + + + +); + +export default withErrorBoundary(withSuspense(Popup, LoadingFallback), ErrorFallback); diff --git a/pages/popup/src/components/Events.tsx b/pages/popup/src/components/Events.tsx index 96c691b..662bac2 100644 --- a/pages/popup/src/components/Events.tsx +++ b/pages/popup/src/components/Events.tsx @@ -1,96 +1,122 @@ -import React, { useEffect, useState, useCallback } from 'react'; -import { Box, Spinner } from '@chakra-ui/react'; +import { useEffect, useState, useCallback, useRef } from 'react'; +import { Box, Spinner, Flex, Text } from '@chakra-ui/react'; import { requestStorage } from '@extension/storage'; import Transaction from './Transaction'; +// Events older than this are treated as abandoned and dropped on load. +const MAX_EVENT_AGE_MINUTES = 10; +// How long to show the empty state before auto-closing the popup. Long enough +// for the post-sign "signature_complete" → cleanup → Transaction.tsx window.close() +// to settle, short enough that a stuck/no-event popup doesn't linger. +const EMPTY_STATE_AUTO_CLOSE_MS = 3000; + const EventsViewer = () => { const [events, setEvents] = useState([]); const [currentIndex, setCurrentIndex] = useState(0); const [loading, setLoading] = useState(true); + const [fetchError, setFetchError] = useState(null); + const autoCloseTimerRef = useRef | null>(null); - // Function to calculate the age of the event in minutes - const getEventAgeInMinutes = (timestamp: string) => { - const eventTime = new Date(timestamp).getTime(); - const currentTime = Date.now(); - const ageInMinutes = (currentTime - eventTime) / 60000; // Convert milliseconds to minutes - return ageInMinutes; - }; - - // Optimized event fetching to prevent endless loops const fetchEvents = useCallback(async () => { - setLoading(true); // Show spinner while fetching events - const storedEvents = await requestStorage.getEvents(); - const validEvents = []; + try { + const storedEvents = (await requestStorage.getEvents()) || []; + const now = Date.now(); + const valid: any[] = []; - for (const event of storedEvents) { - const ageInMinutes = getEventAgeInMinutes(event.timestamp); - if (ageInMinutes <= 10) { - validEvents.push(event); // Keep events that are within 10 minutes - } else { - await requestStorage.removeEventById(event.id); // Remove events older than 10 minutes + for (const event of storedEvents) { + const ageMs = now - new Date(event.timestamp).getTime(); + if (ageMs <= MAX_EVENT_AGE_MINUTES * 60_000) { + valid.push(event); + } else { + // Fire-and-forget; don't block the fetch on cleanup. + void requestStorage.removeEventById(event.id); + } } - } - - // Set the valid events and reverse them to show latest first - setEvents(validEvents.reverse()); - setLoading(false); // Stop spinner after events are loaded - // If no events are found, close the window - // if (validEvents.length === 0) { - // window.close(); - // } + setEvents(valid.reverse()); + setFetchError(null); + } catch (e: any) { + console.error('EventsViewer: fetchEvents failed', e); + setFetchError(e?.message || 'Failed to load pending requests'); + } finally { + setLoading(false); + } }, []); useEffect(() => { fetchEvents(); + // Live-refresh when events are added/removed by the background. + const unsubscribe = requestStorage.subscribe?.(() => { + fetchEvents(); + }); + return () => { + if (typeof unsubscribe === 'function') unsubscribe(); + }; }, [fetchEvents]); - const nextEvent = () => { - if (currentIndex < events.length - 1) { - setCurrentIndex(currentIndex + 1); - resetTransactionState(); - } - }; - - const previousEvent = () => { - if (currentIndex > 0) { - setCurrentIndex(currentIndex - 1); - resetTransactionState(); - } - }; - - const clearRequestEvents = async () => { - await requestStorage.clearEvents(); - fetchEvents(); - setCurrentIndex(0); - }; + // Auto-close the popup whenever there's nothing the user can act on — + // empty state (dapp cancelled, cleanup ran, no pending request) OR a fetch + // failure (storage corruption/null). Both UIs tell the user the window + // will close itself, so the timer needs to cover both. + useEffect(() => { + if (loading) return; + const shouldAutoClose = fetchError !== null || events.length === 0; + if (!shouldAutoClose) return; + autoCloseTimerRef.current = setTimeout(() => { + console.log('EventsViewer: dead-end state timeout, closing popup'); + window.close(); + }, EMPTY_STATE_AUTO_CLOSE_MS); + return () => { + if (autoCloseTimerRef.current) { + clearTimeout(autoCloseTimerRef.current); + autoCloseTimerRef.current = null; + } + }; + }, [events.length, loading, fetchError]); - // Reset transaction state when switching between events - const resetTransactionState = () => { - // Here you can reset any transaction-related state - setLoading(false); - }; + // Clamp currentIndex inline so a mid-render shrink of the events list never + // hands `undefined` to . Doing this in a useEffect leaves a + // one-render gap where events[currentIndex] is undefined and crashes the + // child before the effect can snap the index back. + const safeIndex = events.length > 0 ? Math.min(currentIndex, events.length - 1) : 0; return ( - {/* Show spinner if events are being fetched */} - {loading && } + {loading && ( + + + + Loading pending requests... + + + )} + + {!loading && fetchError && ( + + Couldn't load pending requests + + {fetchError} + + + This window will close automatically. + + + )} - {/* Only show event details if events are loaded */} - {events.length > 0 && !loading ? ( - - {/* Show the age of the current event */} - {/**/} - {/* Chain: {events[currentIndex].chain}*/} - {/*
*/} - {/* Event Age: {Math.floor(getEventAgeInMinutes(events[currentIndex].timestamp))} minutes*/} - {/*
*/} + {!loading && !fetchError && events.length > 0 && ( + + )} - {/* Pass the current event to the Transaction component */} - -
- ) : ( -
No events
+ {!loading && !fetchError && events.length === 0 && ( + + No pending requests + + Nothing to approve right now. + + + This window will close automatically. + + )}
);