From 74fb3655c76ef9af910a2b3a01b24480ada854b9 Mon Sep 17 00:00:00 2001 From: Daniel Constantin Date: Wed, 24 Jun 2026 18:32:46 +0300 Subject: [PATCH 1/7] fix: quote loading for walletconnect --- .../src/api/hooks/useWalletCapabilities.test.ts | 13 ++++++++++++- .../src/api/hooks/useWalletCapabilities.utils.ts | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/libs/wallet/src/api/hooks/useWalletCapabilities.test.ts b/libs/wallet/src/api/hooks/useWalletCapabilities.test.ts index 50334be38b..914753c98f 100644 --- a/libs/wallet/src/api/hooks/useWalletCapabilities.test.ts +++ b/libs/wallet/src/api/hooks/useWalletCapabilities.test.ts @@ -22,7 +22,8 @@ describe('shouldCheckCapabilities', () => { ).toBe(false) }) - it('skips capabilities for mobile WalletConnect sessions', () => { + it('skips capabilities for WalletConnect sessions', () => { + expect(shouldCheckCapabilities(true, { isLoading: false }, desktopEnvironment)).toBe(false) expect(shouldCheckCapabilities(true, { isLoading: false }, mobileEnvironment)).toBe(false) }) @@ -30,6 +31,16 @@ describe('shouldCheckCapabilities', () => { expect(shouldCheckCapabilities(false, { data: null, isLoading: false }, desktopEnvironment)).toBe(true) }) + it('skips capabilities for widget-derived WalletConnect sessions', () => { + expect( + shouldCheckCapabilities( + false, + { data: { providerWcMetadata: {} }, isLoading: false }, + { ...desktopEnvironment, isInjectedWidget: true }, + ), + ).toBe(false) + }) + it('waits for widget provider metadata before deciding on mobile', () => { expect(shouldCheckCapabilities(false, { isLoading: true }, { ...mobileEnvironment, isInjectedWidget: true })).toBe( false, diff --git a/libs/wallet/src/api/hooks/useWalletCapabilities.utils.ts b/libs/wallet/src/api/hooks/useWalletCapabilities.utils.ts index 9a7ac7c127..62cbf5b316 100644 --- a/libs/wallet/src/api/hooks/useWalletCapabilities.utils.ts +++ b/libs/wallet/src/api/hooks/useWalletCapabilities.utils.ts @@ -22,7 +22,7 @@ export function shouldCheckCapabilities( const isWalletConnectViaWidget = Boolean(data?.providerWcMetadata) - if ((isWalletConnect || isWalletConnectViaWidget) && environment.isMobile) { + if (isWalletConnect || isWalletConnectViaWidget) { return false } From f9139c010cccd1656b229ada6d6842a31a3ece0f Mon Sep 17 00:00:00 2001 From: Daniel Constantin Date: Wed, 24 Jun 2026 21:54:40 +0300 Subject: [PATCH 2/7] refactor: add a timeout to wallet capabilities --- .../api/hooks/useWalletCapabilities.test.ts | 79 +++++++++---------- .../src/api/hooks/useWalletCapabilities.ts | 61 +++++++------- .../api/hooks/useWalletCapabilities.utils.ts | 36 --------- 3 files changed, 71 insertions(+), 105 deletions(-) delete mode 100644 libs/wallet/src/api/hooks/useWalletCapabilities.utils.ts diff --git a/libs/wallet/src/api/hooks/useWalletCapabilities.test.ts b/libs/wallet/src/api/hooks/useWalletCapabilities.test.ts index 914753c98f..723f6f7b8d 100644 --- a/libs/wallet/src/api/hooks/useWalletCapabilities.test.ts +++ b/libs/wallet/src/api/hooks/useWalletCapabilities.test.ts @@ -1,49 +1,48 @@ -import { shouldCheckCapabilities } from './useWalletCapabilities.utils' - -const desktopEnvironment = { - isInjectedMobileBrowser: false, - isInjectedWidget: false, - isMobile: false, -} - -const mobileEnvironment = { - ...desktopEnvironment, - isMobile: true, -} - -describe('shouldCheckCapabilities', () => { - it('checks capabilities for desktop injected wallets', () => { - expect(shouldCheckCapabilities(false, { isLoading: false }, desktopEnvironment)).toBe(true) - }) +import { act, renderHook } from '@testing-library/react' +import { useCapabilities } from 'wagmi' - it('skips capabilities for injected mobile browsers', () => { - expect( - shouldCheckCapabilities(false, { isLoading: false }, { ...mobileEnvironment, isInjectedMobileBrowser: true }), - ).toBe(false) - }) +import { useWalletCapabilities } from './useWalletCapabilities' - it('skips capabilities for WalletConnect sessions', () => { - expect(shouldCheckCapabilities(true, { isLoading: false }, desktopEnvironment)).toBe(false) - expect(shouldCheckCapabilities(true, { isLoading: false }, mobileEnvironment)).toBe(false) - }) +jest.mock('wagmi', () => ({ + useCapabilities: jest.fn(), +})) + +jest.mock('../../wagmi/hooks/useWalletMetadata', () => ({ + useIsSafeViaWc: jest.fn(() => false), +})) + +jest.mock('../hooks', () => ({ + useWalletInfo: jest.fn(() => ({ account: '0x0000000000000000000000000000000000000001', chainId: 1 })), +})) + +describe('useWalletCapabilities', () => { + const mockUseCapabilities = useCapabilities as jest.MockedFunction - it('treats null widget metadata as regular injected wallet metadata', () => { - expect(shouldCheckCapabilities(false, { data: null, isLoading: false }, desktopEnvironment)).toBe(true) + beforeEach(() => { + jest.useFakeTimers() + mockUseCapabilities.mockReturnValue({ data: undefined, isLoading: true } as ReturnType) }) - it('skips capabilities for widget-derived WalletConnect sessions', () => { - expect( - shouldCheckCapabilities( - false, - { data: { providerWcMetadata: {} }, isLoading: false }, - { ...desktopEnvironment, isInjectedWidget: true }, - ), - ).toBe(false) + afterEach(() => { + jest.useRealTimers() + jest.clearAllMocks() }) - it('waits for widget provider metadata before deciding on mobile', () => { - expect(shouldCheckCapabilities(false, { isLoading: true }, { ...mobileEnvironment, isInjectedWidget: true })).toBe( - false, - ) + it('stops reporting loading when capabilities do not settle before timeout', () => { + const { result } = renderHook(() => useWalletCapabilities()) + + expect(result.current.isLoading).toBe(true) + + act(() => { + jest.advanceTimersByTime(4_999) + }) + + expect(result.current.isLoading).toBe(true) + + act(() => { + jest.advanceTimersByTime(1) + }) + + expect(result.current.isLoading).toBe(false) }) }) diff --git a/libs/wallet/src/api/hooks/useWalletCapabilities.ts b/libs/wallet/src/api/hooks/useWalletCapabilities.ts index 9a66c86840..5f2f5c9153 100644 --- a/libs/wallet/src/api/hooks/useWalletCapabilities.ts +++ b/libs/wallet/src/api/hooks/useWalletCapabilities.ts @@ -1,18 +1,11 @@ -import { useCallback, useMemo } from 'react' - -import { isInjectedWidget, isMobile } from '@cowprotocol/common-utils' +import { useCallback, useEffect, useMemo, useState } from 'react' +import ms from 'ms.macro' import { useCapabilities } from 'wagmi' -import { shouldCheckCapabilities } from './useWalletCapabilities.utils' -import { useWidgetProviderMetaInfo } from './useWidgetProviderMetaInfo' - -import { useIsWalletConnect } from '../../wagmi/hooks/useIsWalletConnect' import { useIsSafeViaWc } from '../../wagmi/hooks/useWalletMetadata' import { useWalletInfo } from '../hooks' -import { getIsInjectedMobileBrowser } from '../utils/connection' -import type { WalletCapabilitiesEnvironment } from './useWalletCapabilities.utils' import type { GetCapabilitiesData } from '@wagmi/core/query' export type WalletCapabilities = { @@ -20,33 +13,17 @@ export type WalletCapabilities = { atomicBatch?: { supported: boolean } } -function getWalletCapabilitiesEnvironment(): WalletCapabilitiesEnvironment { - return { - isInjectedMobileBrowser: getIsInjectedMobileBrowser(), - isInjectedWidget: isInjectedWidget(), - isMobile, - } -} +const WALLET_CAPABILITIES_LOADING_TIMEOUT = ms`5s` export function useWalletCapabilities(): { data: WalletCapabilities | undefined; isLoading: boolean } { - const isWalletConnect = useIsWalletConnect() - const widgetProviderMetaInfo = useWidgetProviderMetaInfo() const { chainId, account } = useWalletInfo() const isSafeViaWc = useIsSafeViaWc() - const shouldFetchCapabilities = useMemo( - () => - Boolean( - shouldCheckCapabilities(isWalletConnect, widgetProviderMetaInfo, getWalletCapabilitiesEnvironment()) && - account && - chainId, - ), - [isWalletConnect, widgetProviderMetaInfo, account, chainId], - ) + const shouldFetchCapabilities = useMemo(() => Boolean(account && chainId), [account, chainId]) const select = useCallback( (capabilities: GetCapabilitiesData) => { - if (!capabilities || !chainId) return undefined as WalletCapabilities | undefined + if (!capabilities || !chainId) return undefined // Only apply the Safe wallet fallback (first-entry) when connected via Safe WalletConnect, // since Safe's wallet_getCapabilities response may omit the chain ID key. @@ -62,7 +39,7 @@ export function useWalletCapabilities(): { data: WalletCapabilities | undefined; // Fetch capabilities for all chains (no chainId filter) so we can apply // the Safe wallet fallback: if the exact chain is missing, use the first entry. // See https://github.com/safe-global/safe-wallet-monorepo/issues/6906 - return useCapabilities({ + const capabilitiesState = useCapabilities({ account, query: { enabled: shouldFetchCapabilities, @@ -74,4 +51,30 @@ export function useWalletCapabilities(): { data: WalletCapabilities | undefined; select, }, }) + + const [hasLoadingTimedOut, setHasLoadingTimedOut] = useState(false) + + useEffect(() => { + if (!shouldFetchCapabilities || !capabilitiesState.isLoading) { + console.debug('[COW][WalletCapabilities]', 'Wallet capabilities timeout reset') + setHasLoadingTimedOut(false) + return + } + + const timeoutId = setTimeout(() => { + setHasLoadingTimedOut(true) + console.warn( + '[COW][WalletCapabilities]', + `Wallet capabilities loading timed out after ${WALLET_CAPABILITIES_LOADING_TIMEOUT / 1000}s`, + ) + }, WALLET_CAPABILITIES_LOADING_TIMEOUT) + + return () => clearTimeout(timeoutId) + }, [shouldFetchCapabilities, capabilitiesState.isLoading]) + + if (hasLoadingTimedOut && capabilitiesState.isLoading) { + return { data: capabilitiesState.data, isLoading: false } + } + + return capabilitiesState } diff --git a/libs/wallet/src/api/hooks/useWalletCapabilities.utils.ts b/libs/wallet/src/api/hooks/useWalletCapabilities.utils.ts deleted file mode 100644 index 62cbf5b316..0000000000 --- a/libs/wallet/src/api/hooks/useWalletCapabilities.utils.ts +++ /dev/null @@ -1,36 +0,0 @@ -export interface WidgetProviderMetaInfoState { - data?: { providerWcMetadata?: unknown } | null - isLoading: boolean -} - -export interface WalletCapabilitiesEnvironment { - isInjectedMobileBrowser: boolean - isInjectedWidget: boolean - isMobile: boolean -} - -export function shouldCheckCapabilities( - isWalletConnect: boolean, - { data, isLoading }: WidgetProviderMetaInfoState, - environment: WalletCapabilitiesEnvironment, -): boolean { - // When widget in the mobile device, wait till providerWcMetadata is loaded - // In order to detect if is connected to WalletConnect - if (environment.isInjectedWidget && environment.isMobile && isLoading) { - return false - } - - const isWalletConnectViaWidget = Boolean(data?.providerWcMetadata) - - if (isWalletConnect || isWalletConnectViaWidget) { - return false - } - - // Some injected mobile browsers expose wallet_getCapabilities but never settle the request. - // Treat them as capability-unknown so regular trading is not blocked by WalletCapabilitiesLoading. - if (environment.isInjectedMobileBrowser) { - return false - } - - return true -} From 0c285caedaa60abce73a7d4a91412c00d46805ad Mon Sep 17 00:00:00 2001 From: Daniel Constantin Date: Thu, 25 Jun 2026 12:11:13 +0300 Subject: [PATCH 3/7] fix: catch page crash earlier in the tree --- apps/cowswap-frontend/src/cow-react/index.tsx | 133 ++++++++++++++++-- 1 file changed, 124 insertions(+), 9 deletions(-) diff --git a/apps/cowswap-frontend/src/cow-react/index.tsx b/apps/cowswap-frontend/src/cow-react/index.tsx index 9edd93b728..724ee61e21 100644 --- a/apps/cowswap-frontend/src/cow-react/index.tsx +++ b/apps/cowswap-frontend/src/cow-react/index.tsx @@ -1,6 +1,6 @@ import '@reach/dialog/styles.css' import { Provider as AtomProvider } from 'jotai' -import { type ReactNode, StrictMode } from 'react' +import { Component, type ErrorInfo, type PropsWithChildren, type ReactNode, StrictMode } from 'react' import './sentry' import { CowAnalyticsProvider, createNoopCowAnalytics, initGtm } from '@cowprotocol/analytics' @@ -10,6 +10,7 @@ import { SnackbarsWidget } from '@cowprotocol/snackbars' import { WalletProvider, Web3Provider } from '@cowprotocol/wallet' import { Messages } from '@lingui/core' +import * as Sentry from '@sentry/react' import { useInjectedWidgetParams } from 'entities/injectedWidget' import { LanguageProvider } from 'i18n' import { createRoot } from 'react-dom/client' @@ -57,6 +58,120 @@ interface MainProps { localeMessages: Messages | undefined } +interface RootCrashBoundaryState { + error: Error | null + eventId: string | null +} + +interface RootCrashFallbackProps { + error: Error + eventId: string | null +} + +class RootCrashBoundary extends Component { + override state: RootCrashBoundaryState = { error: null, eventId: null } + + static getDerivedStateFromError(error: Error): RootCrashBoundaryState { + return { error, eventId: null } + } + + override componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + const eventId = Sentry.captureException(error, { + tags: { errorBoundary: 'root' }, + contexts: { react: { componentStack: errorInfo.componentStack ?? undefined } }, + }) + + this.setState({ error, eventId }) + } + + override componentDidMount(): void { + window.addEventListener('error', this.handleWindowError) + window.addEventListener('unhandledrejection', this.handleUnhandledRejection) + } + + override componentWillUnmount(): void { + window.removeEventListener('error', this.handleWindowError) + window.removeEventListener('unhandledrejection', this.handleUnhandledRejection) + } + + override render(): ReactNode { + const { error, eventId } = this.state + + if (error) { + return + } + + return this.props.children + } + + private handleWindowError = (event: ErrorEvent): void => { + this.captureGlobalError(event.error, event.message || 'Unhandled window error') + } + + private handleUnhandledRejection = (event: PromiseRejectionEvent): void => { + this.captureGlobalError(event.reason, 'Unhandled promise rejection') + } + + private captureGlobalError(errorLike: unknown, fallbackMessage: string): void { + const error = toError(errorLike, fallbackMessage) + const eventId = Sentry.captureException(error, { tags: { errorBoundary: 'root-global' } }) + + this.setState({ error, eventId }) + } +} + +function RootCrashFallback({ error, eventId }: RootCrashFallbackProps): ReactNode { + return ( +
+
+

Something went wrong

+

Reload the page. If it keeps failing, contact support.

+ +

+ {eventId ? `Event ID: ${eventId}` : error.message} +

+
+
+ ) +} + +function toError(errorLike: unknown, fallbackMessage: string): Error { + if (errorLike instanceof Error) { + return errorLike + } + + if (typeof errorLike === 'string') { + return new Error(errorLike) + } + + return new Error(fallbackMessage) +} + export function Main({ localeMessages }: MainProps): ReactNode { return ( @@ -107,16 +222,16 @@ async function initApp(): Promise { const root = createRoot(container) try { const localeMessages = await loadActiveLocaleMessages() - root.render(
) - } catch (err) { - console.error('Failed to init app', err) - const message = err instanceof Error ? err.message : String(err) root.render( -
-

Failed to load

-

{message}

-
, + +
+ , ) + } catch (err) { + console.error('Failed to init app', err) + const error = toError(err, 'Failed to init app') + const eventId = Sentry.captureException(error, { tags: { errorBoundary: 'root-init' } }) + root.render() } } From dd2063e786de1aec385c522e86c3d8ba88e35a8f Mon Sep 17 00:00:00 2001 From: Daniel Constantin Date: Mon, 22 Jun 2026 15:11:42 +0300 Subject: [PATCH 4/7] fix: mm crash on iphone mobile --- .../@reown__appkit-adapter-wagmi@1.8.19.patch | 23 ++++++++++++++++++- pnpm-lock.yaml | 10 ++++---- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/patches/@reown__appkit-adapter-wagmi@1.8.19.patch b/patches/@reown__appkit-adapter-wagmi@1.8.19.patch index 09e044de55..5e38c51e26 100644 --- a/patches/@reown__appkit-adapter-wagmi@1.8.19.patch +++ b/patches/@reown__appkit-adapter-wagmi@1.8.19.patch @@ -2,7 +2,28 @@ diff --git a/dist/esm/src/client.js b/dist/esm/src/client.js index 79697e342068039131b11f6f47d97702e28c9ff2..0fe9085611f5d0d446fe7803462226f6a13c64fe 100644 --- a/dist/esm/src/client.js +++ b/dist/esm/src/client.js -@@ -520,13 +524,14 @@ export class WagmiAdapter extends AdapterBlueprint { +@@ -150,15 +150,18 @@ export class WagmiAdapter extends AdapterBlueprint { + }); + } + if (accountData.status === 'connected') { ++ const connector = accountData.connector; ++ if (!connector) ++ return; + const hasAccountChanged = accountData.address !== prevAccountData?.address; +- const hasConnectorChanged = accountData.connector.id !== prevAccountData.connector?.id; ++ const hasConnectorChanged = connector.id !== prevAccountData.connector?.id; + const hasConnectionStatusChanged = prevAccountData.status !== 'connected'; + if (hasAccountChanged || hasConnectorChanged || hasConnectionStatusChanged) { + this.setupWatchPendingTransactions(); + this.handleAccountChanged({ + address: accountData.address, + chainId: accountData.chainId, +- connector: accountData.connector ++ connector + }); + } + } +@@ -520,13 +523,14 @@ export class WagmiAdapter extends AdapterBlueprint { chainId, token: params.tokens?.[caipNetwork.caipNetworkId]?.address }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ba5b29190..20f04c409c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,7 +21,7 @@ packageExtensionsChecksum: sha256-q7R3iA9KyuwX1I24FPBePzkCHJmQXkID7pwH/yC+Cgw= patchedDependencies: '@reown/appkit-adapter-wagmi@1.8.19': - hash: b762004636088a2b38d255e990181fd088b8ae189b70b9c34e67e69f4a5e00c6 + hash: e1f6a7396c89aa33c016c902cf5094ec1f92d1a06258ce01542f8139760e5c40 path: patches/@reown__appkit-adapter-wagmi@1.8.19.patch importers: @@ -659,7 +659,7 @@ importers: version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) '@reown/appkit-adapter-wagmi': specifier: 1.8.19 - version: 1.8.19(patch_hash=b762004636088a2b38d255e990181fd088b8ae189b70b9c34e67e69f4a5e00c6)(725f042c52896b22a29c385487b96d7a) + version: 1.8.19(patch_hash=e1f6a7396c89aa33c016c902cf5094ec1f92d1a06258ce01542f8139760e5c40)(725f042c52896b22a29c385487b96d7a) '@reown/appkit-controllers': specifier: 1.8.19 version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) @@ -1269,7 +1269,7 @@ importers: version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) '@reown/appkit-adapter-wagmi': specifier: 1.8.19 - version: 1.8.19(patch_hash=b762004636088a2b38d255e990181fd088b8ae189b70b9c34e67e69f4a5e00c6)(725f042c52896b22a29c385487b96d7a) + version: 1.8.19(patch_hash=e1f6a7396c89aa33c016c902cf5094ec1f92d1a06258ce01542f8139760e5c40)(725f042c52896b22a29c385487b96d7a) '@reown/appkit-controllers': specifier: 1.8.19 version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(react@19.1.2)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) @@ -2109,7 +2109,7 @@ importers: version: 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) '@reown/appkit-adapter-wagmi': specifier: 1.8.19 - version: 1.8.19(patch_hash=b762004636088a2b38d255e990181fd088b8ae189b70b9c34e67e69f4a5e00c6)(725f042c52896b22a29c385487b96d7a) + version: 1.8.19(patch_hash=e1f6a7396c89aa33c016c902cf5094ec1f92d1a06258ce01542f8139760e5c40)(725f042c52896b22a29c385487b96d7a) '@reown/appkit-common': specifier: 1.8.19 version: 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) @@ -22815,7 +22815,7 @@ snapshots: - utf-8-validate - zod - '@reown/appkit-adapter-wagmi@1.8.19(patch_hash=b762004636088a2b38d255e990181fd088b8ae189b70b9c34e67e69f4a5e00c6)(725f042c52896b22a29c385487b96d7a)': + '@reown/appkit-adapter-wagmi@1.8.19(patch_hash=e1f6a7396c89aa33c016c902cf5094ec1f92d1a06258ce01542f8139760e5c40)(725f042c52896b22a29c385487b96d7a)': dependencies: '@reown/appkit': 1.8.19(@types/react@19.1.3)(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(immer@10.0.2)(react@19.1.2)(typescript@5.9.3)(use-sync-external-store@1.5.0(react@19.1.2))(utf-8-validate@6.0.6)(zod@3.25.76) '@reown/appkit-common': 1.8.19(bufferutil@4.0.8)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) From 60bc5b683dec9b6b2640b0484ad09ac4ef0e1b15 Mon Sep 17 00:00:00 2001 From: Daniel Constantin Date: Thu, 25 Jun 2026 12:40:58 +0300 Subject: [PATCH 5/7] refactor: remove toError fn --- apps/cowswap-frontend/src/cow-react/index.tsx | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/apps/cowswap-frontend/src/cow-react/index.tsx b/apps/cowswap-frontend/src/cow-react/index.tsx index 724ee61e21..23c613ce18 100644 --- a/apps/cowswap-frontend/src/cow-react/index.tsx +++ b/apps/cowswap-frontend/src/cow-react/index.tsx @@ -4,7 +4,7 @@ import { Component, type ErrorInfo, type PropsWithChildren, type ReactNode, Stri import './sentry' import { CowAnalyticsProvider, createNoopCowAnalytics, initGtm } from '@cowprotocol/analytics' -import { isInjectedWidget, nodeRemoveChildFix } from '@cowprotocol/common-utils' +import { isInjectedWidget, nodeRemoveChildFix, normalizeError } from '@cowprotocol/common-utils' import { jotaiStore } from '@cowprotocol/core' import { SnackbarsWidget } from '@cowprotocol/snackbars' import { WalletProvider, Web3Provider } from '@cowprotocol/wallet' @@ -113,7 +113,7 @@ class RootCrashBoundary extends Component @@ -227,9 +215,9 @@ async function initApp(): Promise {
, ) - } catch (err) { - console.error('Failed to init app', err) - const error = toError(err, 'Failed to init app') + } catch (err: unknown) { + const error = normalizeError(err) + console.error('Failed to init app', error) const eventId = Sentry.captureException(error, { tags: { errorBoundary: 'root-init' } }) root.render() } From d4f2383fd9e64a4ad789172dc972eb1c2f551fca Mon Sep 17 00:00:00 2001 From: Daniel Constantin Date: Thu, 25 Jun 2026 13:22:47 +0300 Subject: [PATCH 6/7] fix(cowswap): avoid crash page for global errors --- apps/cowswap-frontend/src/cow-react/index.tsx | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/apps/cowswap-frontend/src/cow-react/index.tsx b/apps/cowswap-frontend/src/cow-react/index.tsx index 23c613ce18..56c856379a 100644 --- a/apps/cowswap-frontend/src/cow-react/index.tsx +++ b/apps/cowswap-frontend/src/cow-react/index.tsx @@ -84,16 +84,6 @@ class RootCrashBoundary extends Component { - this.captureGlobalError(event.error, event.message || 'Unhandled window error') - } - - private handleUnhandledRejection = (event: PromiseRejectionEvent): void => { - this.captureGlobalError(event.reason, 'Unhandled promise rejection') - } - - private captureGlobalError(errorLike: unknown, fallbackMessage: string): void { - const error = normalizeError(errorLike ?? fallbackMessage) - const eventId = Sentry.captureException(error, { tags: { errorBoundary: 'root-global' } }) - - this.setState({ error, eventId }) - } } function RootCrashFallback({ error, eventId }: RootCrashFallbackProps): ReactNode { From 92763c19d6b6c9688db919461595b9fc731f947f Mon Sep 17 00:00:00 2001 From: Daniel Constantin Date: Thu, 25 Jun 2026 14:10:09 +0300 Subject: [PATCH 7/7] fix: handle wallet disconnect error --- .../containers/AccountDetails/index.tsx | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/index.tsx b/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/index.tsx index cbd2f8525d..44330a6151 100644 --- a/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/index.tsx +++ b/apps/cowswap-frontend/src/modules/account/containers/AccountDetails/index.tsx @@ -2,7 +2,14 @@ import { Fragment, ReactNode } from 'react' import { CHAIN_INFO } from '@cowprotocol/common-const' import { styled } from '@cowprotocol/common-hooks' -import { getEtherscanLink, getExplorerAddressLink, getExplorerLabel, shortenAddress } from '@cowprotocol/common-utils' +import { + captureError, + getEtherscanLink, + getExplorerAddressLink, + getExplorerLabel, + normalizeError, + shortenAddress, +} from '@cowprotocol/common-utils' import { Command } from '@cowprotocol/types' import { ExternalLink } from '@cowprotocol/ui' import { @@ -104,7 +111,20 @@ export function AccountDetails({ const walletConnectSuffix = isWalletConnect && walletDetails?.walletName ? ` ` + t`(via WalletConnect)` : '' const handleDisconnectClick = async (): Promise => { - await disconnectWallet() + try { + await disconnectWallet() + } catch (err: unknown) { + const error = normalizeError(err) + + captureError( + error, + undefined, + { source: 'AccountDetails.handleDisconnectClick' }, + { errorType: 'walletDisconnect' }, + ) + console.debug('[AccountDetails] wallet disconnect failed', error) + } + handleCloseOrdersPanel() dispatch(updateSelectedWallet({ wallet: undefined })) }