diff --git a/pages/_app.page.tsx b/pages/_app.page.tsx
index 14ba54c8b3..a4f5abd691 100644
--- a/pages/_app.page.tsx
+++ b/pages/_app.page.tsx
@@ -13,6 +13,7 @@ import dynamic from 'next/dynamic';
import Head from 'next/head';
import { ReactNode, useEffect, useState } from 'react';
import { AddressBlocked } from 'src/components/AddressBlocked';
+import { TransactionErrorBoundary } from 'src/components/ErrorBoundary/TransactionErrorBoundary';
import { Meta } from 'src/components/Meta';
import { TransactionEventHandler } from 'src/components/TransactionEventHandler';
import { GasStationProvider } from 'src/components/transactions/GasStation/GasStationProvider';
@@ -21,6 +22,7 @@ import { AppDataProvider } from 'src/hooks/app-data-provider/useAppDataProvider'
import { CowOrderToastProvider } from 'src/hooks/useCowOrderToast';
import { ModalContextProvider } from 'src/hooks/useModal';
import { Web3ContextProvider } from 'src/libs/web3-data-provider/Web3Provider';
+import { WalletGuard } from 'src/providers/WalletGuard';
import { useRootStore } from 'src/store/root';
import { SharedDependenciesProvider } from 'src/ui-config/SharedDependenciesProvider';
import { wagmiConfig } from 'src/ui-config/wagmiConfig';
@@ -99,6 +101,7 @@ interface MyAppProps extends AppProps {
emotionCache?: EmotionCache;
Component: NextPageWithLayout;
}
+
export default function MyApp(props: MyAppProps) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
const getLayout = Component.getLayout ?? ((page: ReactNode) => page);
@@ -154,29 +157,33 @@ export default function MyApp(props: MyAppProps) {
-
-
-
- {getLayout()}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+ {getLayout()}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -191,4 +198,4 @@ export default function MyApp(props: MyAppProps) {
);
-}
+}
\ No newline at end of file
diff --git a/src/components/ErrorBoundary/TransactionErrorBoundary.tsx b/src/components/ErrorBoundary/TransactionErrorBoundary.tsx
new file mode 100644
index 0000000000..9f75f390ab
--- /dev/null
+++ b/src/components/ErrorBoundary/TransactionErrorBoundary.tsx
@@ -0,0 +1,121 @@
+import { Trans } from '@lingui/macro';
+import { Alert, Box, Button, Typography } from '@mui/material';
+import React, { Component, ErrorInfo, ReactNode } from 'react';
+import { ConnectWalletButton } from 'src/components/WalletConnection/ConnectWalletButton';
+
+interface Props {
+ children: ReactNode;
+ fallback?: ReactNode;
+}
+
+interface State {
+ hasError: boolean;
+ error: Error | null;
+ errorInfo: ErrorInfo | null;
+}
+
+/**
+ * TransactionErrorBoundary
+ *
+ * Catches errors that occur within transaction modals and provides
+ * a graceful recovery mechanism. This prevents the entire app from
+ * crashing when wallet-related errors occur.
+ *
+ * Specifically handles:
+ * - Wallet disconnection errors
+ * - Transaction failures
+ * - Unexpected modal state errors
+ */
+export class TransactionErrorBoundary extends Component {
+ constructor(props: Props) {
+ super(props);
+ this.state = { hasError: false, error: null, errorInfo: null };
+ }
+
+ static getDerivedStateFromError(error: Error): State {
+ // Update state so the next render will show the fallback UI
+ return { hasError: true, error, errorInfo: null };
+ }
+
+ componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ // Log error details for debugging
+ console.error('[TransactionErrorBoundary] Caught error:', error, errorInfo);
+ this.setState({
+ error,
+ errorInfo,
+ });
+ }
+
+ handleReset = () => {
+ this.setState({ hasError: false, error: null, errorInfo: null });
+ };
+
+ render() {
+ if (this.state.hasError) {
+ // Check if the error is wallet-related
+ const isWalletError =
+ this.state.error?.message?.toLowerCase().includes('wallet') ||
+ this.state.error?.message?.toLowerCase().includes('user') ||
+ this.state.error?.message?.toLowerCase().includes('account');
+
+ // Use custom fallback if provided
+ if (this.props.fallback) {
+ return <>{this.props.fallback}>;
+ }
+
+ // Default error UI
+ return (
+
+
+
+ Something went wrong
+
+
+ {isWalletError ? (
+
+ There was an issue with your wallet connection. Please reconnect your wallet and try again.
+
+ ) : (
+
+ An unexpected error occurred. Please refresh the page and try again.
+
+ )}
+
+ {process.env.NODE_ENV === 'development' && this.state.error && (
+
+ {this.state.error.toString()}
+
+ )}
+
+
+
+ {isWalletError ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
\ No newline at end of file
diff --git a/src/components/UserAuthenticated.tsx b/src/components/UserAuthenticated.tsx
index 067925abee..47192d6632 100644
--- a/src/components/UserAuthenticated.tsx
+++ b/src/components/UserAuthenticated.tsx
@@ -1,10 +1,11 @@
-import { Box, CircularProgress } from '@mui/material';
+import { Trans } from '@lingui/macro';
+import { Box, CircularProgress, Typography } from '@mui/material';
import React, { ReactNode } from 'react';
import {
ExtendedFormattedUser,
useAppDataContext,
} from 'src/hooks/app-data-provider/useAppDataProvider';
-import invariant from 'tiny-invariant';
+import { ConnectWalletButton } from 'src/components/WalletConnection/ConnectWalletButton';
interface UserAuthenticatedProps {
children: (user: ExtendedFormattedUser) => ReactNode;
@@ -12,6 +13,7 @@ interface UserAuthenticatedProps {
export const UserAuthenticated = ({ children }: UserAuthenticatedProps) => {
const { user, loading } = useAppDataContext();
+
if (loading) {
return (
@@ -19,6 +21,18 @@ export const UserAuthenticated = ({ children }: UserAuthenticatedProps) => {
);
}
- invariant(user, 'User data loaded but no user found');
+
+ // Handle disconnection gracefully instead of crashing
+ if (!user) {
+ return (
+
+
+ Please connect your wallet to continue.
+
+
+
+ );
+ }
+
return <>{children(user)}>;
};
diff --git a/src/components/transactions/Supply/SupplyModal.tsx b/src/components/transactions/Supply/SupplyModal.tsx
index 9f6e8b6a06..d7d66b5018 100644
--- a/src/components/transactions/Supply/SupplyModal.tsx
+++ b/src/components/transactions/Supply/SupplyModal.tsx
@@ -1,7 +1,10 @@
import { Trans } from '@lingui/macro';
+import { Box, Typography } from '@mui/material';
import React from 'react';
import { UserAuthenticated } from 'src/components/UserAuthenticated';
+import { ConnectWalletButton } from 'src/components/WalletConnection/ConnectWalletButton';
import { ModalContextType, ModalType, useModalContext } from 'src/hooks/useModal';
+import { useRootStore } from 'src/store/root';
import { BasicModal } from '../../primitives/BasicModal';
import { ModalWrapper } from '../FlowCommons/ModalWrapper';
@@ -11,20 +14,30 @@ export const SupplyModal = () => {
const { type, close, args } = useModalContext() as ModalContextType<{
underlyingAsset: string;
}>;
+ const account = useRootStore((store) => store.account);
return (
- Supply}
- underlyingAsset={args.underlyingAsset}
- >
- {(params) => (
-
- {(user) => }
-
- )}
-
+ {!account ? (
+
+
+ Please connect your wallet to supply assets.
+
+ close()} />
+
+ ) : (
+ Supply}
+ underlyingAsset={args.underlyingAsset}
+ >
+ {(params) => (
+
+ {(user) => }
+
+ )}
+
+ )}
);
};
diff --git a/src/providers/WalletGuard.tsx b/src/providers/WalletGuard.tsx
new file mode 100644
index 0000000000..55371ada44
--- /dev/null
+++ b/src/providers/WalletGuard.tsx
@@ -0,0 +1,37 @@
+import { PropsWithChildren, useEffect, useRef } from 'react';
+import { useModalContext } from 'src/hooks/useModal';
+import { useAccount } from 'wagmi';
+
+/**
+ * WalletGuard Provider
+ *
+ * Monitors wallet connection state and automatically closes modals
+ * when the wallet disconnects to prevent crashes and stale state.
+ *
+ * This prevents the app from crashing when users lock their wallet
+ * extensions (MetaMask, Rabby, Ambire, etc.) while modals are open.
+ */
+export const WalletGuard = ({ children }: PropsWithChildren) => {
+ const { isConnected } = useAccount();
+ const { close } = useModalContext();
+ const wasConnectedRef = useRef(false);
+
+ useEffect(() => {
+ // Track if wallet was previously connected
+ if (isConnected) {
+ wasConnectedRef.current = true;
+ return;
+ }
+
+ // Only close modals if wallet was connected and now disconnected
+ // This prevents closing modals on initial load
+ if (wasConnectedRef.current && !isConnected) {
+ console.debug('[WalletGuard] Wallet disconnected, closing modals');
+ close();
+ // Reset transaction states are handled by the close function
+ wasConnectedRef.current = false;
+ }
+ }, [isConnected, close]);
+
+ return <>{children}>;
+};
\ No newline at end of file