From 68d40a92b2b479886eaa1b1bcb062194ab846557 Mon Sep 17 00:00:00 2001 From: mmarfinetz Date: Fri, 8 Aug 2025 13:09:57 -0400 Subject: [PATCH 1/2] fix: prevent app crash when wallet locks during Supply modal interaction Fixes #2382 - Implements comprehensive wallet disconnection handling Changes: - Fix UserAuthenticated component to handle disconnection gracefully - Remove invariant assertion that caused crashes - Show "Connect Wallet" UI when user becomes undefined - Add WalletGuard provider for global disconnection monitoring - Monitors wallet connection state via wagmi - Automatically closes all modals on disconnection - Prevents stale transaction state - Enhance SupplyModal with disconnection guards - Check wallet connection before rendering modal content - Show appropriate "Connect Wallet" message when disconnected - Add TransactionErrorBoundary for additional safety - Catches errors in transaction modals - Provides recovery mechanism for wallet-related errors - Prevents crashes from propagating to app level This fix ensures the app handles wallet disconnection events (MetaMask lock, Rabby lock, Ambire lock, etc.) gracefully without crashing, improving overall UX reliability and preventing data loss during wallet disconnection events. --- pages/_app.page.tsx | 52 ++++---- .../TransactionErrorBoundary.tsx | 121 ++++++++++++++++++ src/components/UserAuthenticated.tsx | 20 ++- .../transactions/Supply/SupplyModal.tsx | 35 +++-- src/providers/WalletGuard.tsx | 37 ++++++ 5 files changed, 228 insertions(+), 37 deletions(-) create mode 100644 src/components/ErrorBoundary/TransactionErrorBoundary.tsx create mode 100644 src/providers/WalletGuard.tsx diff --git a/pages/_app.page.tsx b/pages/_app.page.tsx index 170a2a8826..9898e5aec4 100644 --- a/pages/_app.page.tsx +++ b/pages/_app.page.tsx @@ -12,6 +12,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'; @@ -20,6 +21,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'; @@ -150,29 +152,33 @@ export default function MyApp(props: MyAppProps) { - - - - {getLayout()} - - - - - - - - - - - - - - - - - - - + + + + + {getLayout()} + + + + + + + + + + + + + + + + + + + + + + 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 From ca9c21e303d9c5b4f6bdafe0386486877ca7c0dc Mon Sep 17 00:00:00 2001 From: mmarfinetz Date: Fri, 22 Aug 2025 13:04:13 -0400 Subject: [PATCH 2/2] Restore AaveProvider/AaveClient wrap in _app.page.tsx; keep WalletGuard and TransactionErrorBoundary --- pages/_app.page.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/pages/_app.page.tsx b/pages/_app.page.tsx index 9898e5aec4..7cf4844cf4 100644 --- a/pages/_app.page.tsx +++ b/pages/_app.page.tsx @@ -1,6 +1,7 @@ import '/public/fonts/inter/inter.css'; import '/src/styles/variables.css'; +import { AaveClient, AaveProvider } from '@aave/react'; import { CacheProvider, EmotionCache } from '@emotion/react'; import { NoSsr } from '@mui/material'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; @@ -94,6 +95,8 @@ type NextPageWithLayout = NextPage & { getLayout?: (page: React.ReactElement) => React.ReactNode; }; +export const client = AaveClient.create(); + interface MyAppProps extends AppProps { emotionCache?: EmotionCache; Component: NextPageWithLayout; @@ -140,7 +143,8 @@ export default function MyApp(props: MyAppProps) { imageUrl="https://app.aave.com/aave-com-opengraph.png" /> - + + - + + );