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.
+
+
)}
);