From 0adcd478340cfee66b055b9b3e38e36ca141a4d3 Mon Sep 17 00:00:00 2001 From: iamacook Date: Fri, 20 Oct 2023 10:47:47 +0200 Subject: [PATCH] fix: wrap popups in ErrorBoundary --- .../dashboard/FeaturedApps/FeaturedApps.tsx | 8 ++-- .../HeaderWidget/ErrorFalllback.tsx | 32 ++++++++++++++ .../walletconnect/HeaderWidget/Icon.tsx | 10 +---- .../walletconnect/HeaderWidget/index.tsx | 41 ++++++++++++----- src/pages/_app.tsx | 5 +-- .../walletconnect/WalletConnectContext.tsx | 9 +--- src/store/__tests__/popupSlice.test.ts | 44 +++++++++++++++++++ src/store/popupSlice.ts | 16 ++++++- 8 files changed, 129 insertions(+), 36 deletions(-) create mode 100644 src/components/walletconnect/HeaderWidget/ErrorFalllback.tsx create mode 100644 src/store/__tests__/popupSlice.test.ts diff --git a/src/components/dashboard/FeaturedApps/FeaturedApps.tsx b/src/components/dashboard/FeaturedApps/FeaturedApps.tsx index 6f9ebe139c..6ab5badff5 100644 --- a/src/components/dashboard/FeaturedApps/FeaturedApps.tsx +++ b/src/components/dashboard/FeaturedApps/FeaturedApps.tsx @@ -1,5 +1,5 @@ import type { ReactElement } from 'react' -import { useContext } from 'react' +import { useAppDispatch } from '@/store' import { Box, Grid, Typography, Link } from '@mui/material' import type { SafeAppData } from '@safe-global/safe-gateway-typescript-sdk' import { Card, WidgetBody, WidgetContainer } from '../styled' @@ -9,7 +9,7 @@ import { AppRoutes } from '@/config/routes' import { SafeAppsTag } from '@/config/constants' import { useRemoteSafeApps } from '@/hooks/safe-apps/useRemoteSafeApps' import SafeAppIconCard from '@/components/safe-apps/SafeAppIconCard' -import { WalletConnectContext } from '@/services/walletconnect/WalletConnectContext' +import { openWalletConnect } from '@/store/popupSlice' const isWalletConnectSafeApp = (app: SafeAppData): boolean => { const WALLET_CONNECT = /wallet-connect/ @@ -39,12 +39,12 @@ const FeaturedAppCard = ({ app }: { app: SafeAppData }) => ( export const FeaturedApps = ({ stackedLayout }: { stackedLayout: boolean }): ReactElement | null => { const router = useRouter() const [featuredApps, _, remoteSafeAppsLoading] = useRemoteSafeApps(SafeAppsTag.DASHBOARD_FEATURED) - const { setOpen } = useContext(WalletConnectContext) + const dispatch = useAppDispatch() if (!featuredApps?.length && !remoteSafeAppsLoading) return null const onWcWidgetClick = () => { - setOpen(true) + dispatch(openWalletConnect()) } return ( diff --git a/src/components/walletconnect/HeaderWidget/ErrorFalllback.tsx b/src/components/walletconnect/HeaderWidget/ErrorFalllback.tsx new file mode 100644 index 0000000000..bfe59f819c --- /dev/null +++ b/src/components/walletconnect/HeaderWidget/ErrorFalllback.tsx @@ -0,0 +1,32 @@ +import { useRef } from 'react' +import type { ReactElement } from 'react' + +import Popup from '../Popup' +import Icon from './Icon' +import { WalletConnectErrorMessage } from '../SessionManager/ErrorMessage' + +export const ErrorFalllback = ({ + onOpen, + onClose, + open, + error, +}: { + onOpen: () => void + onClose: () => void + open: boolean + error: Error +}): ReactElement => { + const iconRef = useRef(null) + + return ( + <> +
+ +
+ + + + + + ) +} diff --git a/src/components/walletconnect/HeaderWidget/Icon.tsx b/src/components/walletconnect/HeaderWidget/Icon.tsx index a237c381eb..9c21d72ddf 100644 --- a/src/components/walletconnect/HeaderWidget/Icon.tsx +++ b/src/components/walletconnect/HeaderWidget/Icon.tsx @@ -1,21 +1,15 @@ import { Badge, ButtonBase, SvgIcon } from '@mui/material' -import { useContext } from 'react' import WalletConnectIcon from '@/public/images/common/walletconnect.svg' import { useDarkMode } from '@/hooks/useDarkMode' -import { WalletConnectContext } from '@/services/walletconnect/WalletConnectContext' type IconProps = { onClick: () => void sessionCount: number - sessionInfo?: { - name: string - iconUrl: string - } + error: boolean } -const Icon = ({ sessionCount, sessionInfo, ...props }: IconProps): React.ReactElement => { - const { error } = useContext(WalletConnectContext) +const Icon = ({ sessionCount, error = false, ...props }: IconProps): React.ReactElement => { const isDarkMode = useDarkMode() return ( diff --git a/src/components/walletconnect/HeaderWidget/index.tsx b/src/components/walletconnect/HeaderWidget/index.tsx index f7590a4252..36dfa6e800 100644 --- a/src/components/walletconnect/HeaderWidget/index.tsx +++ b/src/components/walletconnect/HeaderWidget/index.tsx @@ -1,8 +1,9 @@ +import { ErrorBoundary } from '@sentry/react' import { useCallback, useContext, useEffect, useRef, useState } from 'react' import type { CoreTypes, SessionTypes } from '@walletconnect/types' import type { ReactElement } from 'react' -import { WalletConnectContext } from '@/services/walletconnect/WalletConnectContext' +import { WalletConnectContext, WalletConnectProvider } from '@/services/walletconnect/WalletConnectContext' import useWalletConnectSessions from '@/services/walletconnect/useWalletConnectSessions' import { useWalletConnectClipboardUri } from '@/services/walletconnect/useWalletConnectClipboardUri' import { useWalletConnectSearchParamUri } from '@/services/walletconnect/useWalletConnectSearchParamUri' @@ -10,6 +11,9 @@ import Icon from './Icon' import SessionManager from '../SessionManager' import Popup from '../Popup' import { SuccessBanner } from '../SuccessBanner' +import { useAppDispatch, useAppSelector } from '@/store' +import { closeWalletConnect, openWalletConnect, selectWalletConnectPopup } from '@/store/popupSlice' +import { ErrorFalllback } from './ErrorFalllback' const usePrepopulatedUri = (): [string, () => void] => { const [searchParamWcUri, setSearchParamWcUri] = useWalletConnectSearchParamUri() @@ -25,20 +29,22 @@ const usePrepopulatedUri = (): [string, () => void] => { return [uri, clearUri] } -const WalletConnectHeaderWidget = (): ReactElement => { - const { walletConnect, setError, open, setOpen } = useContext(WalletConnectContext) +const HeaderWidget = (): ReactElement => { + const { walletConnect, error, setError } = useContext(WalletConnectContext) + const { open } = useAppSelector(selectWalletConnectPopup) + const dispatch = useAppDispatch() const iconRef = useRef(null) const sessions = useWalletConnectSessions() const [uri, clearUri] = usePrepopulatedUri() const [metadata, setMetadata] = useState() - const onOpenSessionManager = useCallback(() => setOpen(true), [setOpen]) + const onOpenSessionManager = useCallback(() => dispatch(openWalletConnect()), [dispatch]) const onCloseSessionManager = useCallback(() => { - setOpen(false) + dispatch(closeWalletConnect()) clearUri() setError(null) - }, [setOpen, clearUri, setError]) + }, [dispatch, clearUri, setError]) const onCloseSuccesBanner = useCallback(() => setMetadata(undefined), []) @@ -74,13 +80,17 @@ const WalletConnectHeaderWidget = (): ReactElement => { // Open the popup when a prepopulated uri is found useEffect(() => { - if (uri) setOpen(true) - }, [uri, setOpen]) + if (uri) dispatch(openWalletConnect()) + }, [uri, dispatch]) return ( - <> + ( + + )} + >
- +
@@ -90,7 +100,16 @@ const WalletConnectHeaderWidget = (): ReactElement => { {metadata && } - +
+ ) +} + +const WalletConnectHeaderWidget = (): ReactElement => { + // Provider wraps widget so ErrorBoundary is isolated to this component + return ( + + + ) } diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 6e8cea4cbd..cc753328ae 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -37,7 +37,6 @@ import useSafeMessageNotifications from '@/hooks/messages/useSafeMessageNotifica import useSafeMessagePendingStatuses from '@/hooks/messages/useSafeMessagePendingStatuses' import useChangedValue from '@/hooks/useChangedValue' import { TxModalProvider } from '@/components/tx-flow' -import { WalletConnectProvider } from '@/services/walletconnect/WalletConnectContext' import useABTesting from '@/services/tracking/useAbTesting' import { AbTest } from '@/services/tracking/abTesting' import { useNotificationTracking } from '@/components/settings/PushNotifications/hooks/useNotificationTracking' @@ -79,9 +78,7 @@ export const AppProviders = ({ children }: { children: ReactNode | ReactNode[] } {(safeTheme: Theme) => ( - - {children} - + {children} )} diff --git a/src/services/walletconnect/WalletConnectContext.tsx b/src/services/walletconnect/WalletConnectContext.tsx index 98b66d6ffb..98b3f5c1a8 100644 --- a/src/services/walletconnect/WalletConnectContext.tsx +++ b/src/services/walletconnect/WalletConnectContext.tsx @@ -14,14 +14,10 @@ export const WalletConnectContext = createContext<{ walletConnect: WalletConnectWallet | null error: Error | null setError: Dispatch> - open: boolean - setOpen: (open: boolean) => void }>({ walletConnect: null, error: null, setError: () => {}, - open: false, - setOpen: (_open: boolean) => {}, }) export const WalletConnectProvider = ({ children }: { children: ReactNode }) => { @@ -30,7 +26,6 @@ export const WalletConnectProvider = ({ children }: { children: ReactNode }) => safeAddress, } = useSafeInfo() const [walletConnect, setWalletConnect] = useState(null) - const [open, setOpen] = useState(false) const [error, setError] = useState(null) const safeWalletProvider = useSafeWalletProvider() @@ -86,8 +81,6 @@ export const WalletConnectProvider = ({ children }: { children: ReactNode }) => }, [walletConnect, chainId, safeWalletProvider]) return ( - - {children} - + {children} ) } diff --git a/src/store/__tests__/popupSlice.test.ts b/src/store/__tests__/popupSlice.test.ts new file mode 100644 index 0000000000..07da1642f3 --- /dev/null +++ b/src/store/__tests__/popupSlice.test.ts @@ -0,0 +1,44 @@ +import { popupSlice, PopupType } from '@/store/popupSlice' +import { CookieType } from '../cookiesSlice' + +describe('popupSlice', () => { + it('should open the cookie banner', () => { + const initialState = { + [PopupType.COOKIES]: { open: false }, + [PopupType.WALLET_CONNECT]: { open: false }, + } + const { reducer, actions } = popupSlice + const newState = reducer(initialState, actions.openCookieBanner({ warningKey: CookieType.ANALYTICS })) + expect(newState[PopupType.COOKIES]).toEqual({ open: true, warningKey: CookieType.ANALYTICS }) + }) + + it('should close the cookie banner', () => { + const initialState = { + [PopupType.COOKIES]: { open: true }, + [PopupType.WALLET_CONNECT]: { open: false }, + } + const { reducer, actions } = popupSlice + const newState = reducer(initialState, actions.closeCookieBanner()) + expect(newState[PopupType.COOKIES]).toEqual({ open: false }) + }) + + it('should open the wallet connect popup', () => { + const initialState = { + [PopupType.COOKIES]: { open: false }, + [PopupType.WALLET_CONNECT]: { open: false }, + } + const { reducer, actions } = popupSlice + const newState = reducer(initialState, actions.openWalletConnect()) + expect(newState[PopupType.WALLET_CONNECT]).toEqual({ open: true }) + }) + + it('should close the wallet connect popup', () => { + const initialState = { + [PopupType.COOKIES]: { open: false }, + [PopupType.WALLET_CONNECT]: { open: true }, + } + const { reducer, actions } = popupSlice + const newState = reducer(initialState, actions.closeWalletConnect()) + expect(newState[PopupType.WALLET_CONNECT]).toEqual({ open: false }) + }) +}) diff --git a/src/store/popupSlice.ts b/src/store/popupSlice.ts index c077d558a7..ab85c1c6e8 100644 --- a/src/store/popupSlice.ts +++ b/src/store/popupSlice.ts @@ -5,6 +5,7 @@ import type { RootState } from '.' export enum PopupType { COOKIES = 'cookies', + WALLET_CONNECT = 'walletConnect', } type PopupState = { @@ -12,12 +13,18 @@ type PopupState = { open: boolean warningKey?: CookieType } + [PopupType.WALLET_CONNECT]: { + open: boolean + } } const initialState: PopupState = { [PopupType.COOKIES]: { open: false, }, + [PopupType.WALLET_CONNECT]: { + open: false, + }, } export const popupSlice = createSlice({ @@ -33,9 +40,16 @@ export const popupSlice = createSlice({ closeCookieBanner: (state) => { state[PopupType.COOKIES] = { open: false } }, + openWalletConnect: (state) => { + state[PopupType.WALLET_CONNECT] = { open: true } + }, + closeWalletConnect: (state) => { + state[PopupType.WALLET_CONNECT] = { open: false } + }, }, }) -export const { openCookieBanner, closeCookieBanner } = popupSlice.actions +export const { openCookieBanner, closeCookieBanner, openWalletConnect, closeWalletConnect } = popupSlice.actions export const selectCookieBanner = (state: RootState) => state[popupSlice.name][PopupType.COOKIES] +export const selectWalletConnectPopup = (state: RootState) => state[popupSlice.name][PopupType.WALLET_CONNECT]