Skip to content

Commit

Permalink
Feat: display a Network switch button when connected on the wrong cha…
Browse files Browse the repository at this point in the history
…in (#4027)

* feat: add nwtwork check to CheckWallet component

* add missing network checks

* Add network switch button for notifications settings toggle

lint

remove wrong chain warning

remove commented code .

* tests: add unit test for new state in  CheckWallet

* remove calls to assertwalletchain

* Dont check the network if button is disabled

* restore WrongChainWarning

* add WrongChainWarning to CF activation screen

* show network logo, align text styles, rename NetworkFee prop

* Use network warning with switch network button, and disable CTA when on wrong network

* remove unused WrongChainWarning component

* fix: update unit tests for network warnings

* fix: disable delegate propose button when on wrong network
  • Loading branch information
jmealy authored Sep 10, 2024
1 parent f0d5530 commit 3cb5f57
Show file tree
Hide file tree
Showing 35 changed files with 186 additions and 164 deletions.
45 changes: 36 additions & 9 deletions src/components/common/ChainSwitcher/index.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,57 @@
import type { ReactElement } from 'react'
import { useCallback } from 'react'
import { Box, Button } from '@mui/material'
import { useCallback, useState } from 'react'
import { Button, CircularProgress, Typography } from '@mui/material'
import { useCurrentChain } from '@/hooks/useChains'
import useOnboard from '@/hooks/wallets/useOnboard'
import useIsWrongChain from '@/hooks/useIsWrongChain'
import css from './styles.module.css'
import { switchWalletChain } from '@/services/tx/tx-sender/sdk'

const ChainSwitcher = ({ fullWidth }: { fullWidth?: boolean }): ReactElement | null => {
const ChainSwitcher = ({
fullWidth,
primaryCta = false,
}: {
fullWidth?: boolean
primaryCta?: boolean
}): ReactElement | null => {
const chain = useCurrentChain()
const onboard = useOnboard()
const isWrongChain = useIsWrongChain()
const [loading, setIsLoading] = useState<boolean>(false)

const handleChainSwitch = useCallback(async () => {
if (!onboard || !chain) return

setIsLoading(true)
await switchWalletChain(onboard, chain.chainId)
setIsLoading(false)
}, [chain, onboard])

if (!isWrongChain) return null

return (
<Button onClick={handleChainSwitch} variant="outlined" size="small" fullWidth={fullWidth} color="primary">
Switch to&nbsp;
<Box className={css.circle} bgcolor={chain?.theme?.backgroundColor || ''} />
&nbsp;{chain?.chainName}
<Button
onClick={handleChainSwitch}
variant={primaryCta ? 'contained' : 'outlined'}
sx={{ minWidth: '200px' }}
size={primaryCta ? 'medium' : 'small'}
fullWidth={fullWidth}
color="primary"
disabled={loading}
>
{loading ? (
<CircularProgress size={20} />
) : (
<>
<Typography noWrap>Switch to&nbsp;</Typography>
<img
src={chain?.chainLogoUri ?? undefined}
alt={`${chain?.chainName} Logo`}
width={24}
height={24}
loading="lazy"
/>
<Typography noWrap>&nbsp;{chain?.chainName}</Typography>
</>
)}
</Button>
)
}
Expand Down
22 changes: 21 additions & 1 deletion src/components/common/CheckWallet/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { render } from '@/tests/test-utils'
import CheckWallet from '.'
import useIsOnlySpendingLimitBeneficiary from '@/hooks/useIsOnlySpendingLimitBeneficiary'
import useIsSafeOwner from '@/hooks/useIsSafeOwner'
import useIsWrongChain from '@/hooks/useIsWrongChain'
import useWallet from '@/hooks/wallets/useWallet'
import { chainBuilder } from '@/tests/builders/chains'

Expand Down Expand Up @@ -31,7 +32,14 @@ jest.mock('@/hooks/useChains', () => ({
useCurrentChain: jest.fn(() => chainBuilder().build()),
}))

const renderButton = () => render(<CheckWallet>{(isOk) => <button disabled={!isOk}>Continue</button>}</CheckWallet>)
// mock useIsWrongChain
jest.mock('@/hooks/useIsWrongChain', () => ({
__esModule: true,
default: jest.fn(() => false),
}))

const renderButton = () =>
render(<CheckWallet checkNetwork={false}>{(isOk) => <button disabled={!isOk}>Continue</button>}</CheckWallet>)

describe('CheckWallet', () => {
beforeEach(() => {
Expand Down Expand Up @@ -69,6 +77,18 @@ describe('CheckWallet', () => {
)
})

it('should be disabled when connected to the wrong network', () => {
;(useIsWrongChain as jest.MockedFunction<typeof useIsWrongChain>).mockReturnValue(true)
;(useIsSafeOwner as jest.MockedFunction<typeof useIsSafeOwner>).mockReturnValueOnce(true)

const renderButtonWithNetworkCheck = () =>
render(<CheckWallet checkNetwork={true}>{(isOk) => <button disabled={true}></button>}</CheckWallet>)

const { container } = renderButtonWithNetworkCheck()

expect(container.querySelector('button')).toBeDisabled()
})

it('should not disable the button for non-owner spending limit benificiaries', () => {
;(useIsSafeOwner as jest.MockedFunction<typeof useIsSafeOwner>).mockReturnValueOnce(false)
;(
Expand Down
16 changes: 13 additions & 3 deletions src/components/common/CheckWallet/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { useIsWalletDelegate } from '@/hooks/useDelegates'
import { type ReactElement } from 'react'
import { Tooltip } from '@mui/material'
import useIsOnlySpendingLimitBeneficiary from '@/hooks/useIsOnlySpendingLimitBeneficiary'
import useIsSafeOwner from '@/hooks/useIsSafeOwner'
import useWallet from '@/hooks/wallets/useWallet'
import useConnectWallet from '../ConnectWallet/useConnectWallet'
import useIsWrongChain from '@/hooks/useIsWrongChain'
import { Tooltip } from '@mui/material'
import useSafeInfo from '@/hooks/useSafeInfo'

type CheckWalletProps = {
children: (ok: boolean) => ReactElement
allowSpendingLimit?: boolean
allowNonOwner?: boolean
noTooltip?: boolean
checkNetwork?: boolean
}

enum Message {
Expand All @@ -20,14 +22,22 @@ enum Message {
CounterfactualMultisig = 'You need to activate the Safe before transacting',
}

const CheckWallet = ({ children, allowSpendingLimit, allowNonOwner, noTooltip }: CheckWalletProps): ReactElement => {
const CheckWallet = ({
children,
allowSpendingLimit,
allowNonOwner,
noTooltip,
checkNetwork = false,
}: CheckWalletProps): ReactElement => {
const wallet = useWallet()
const isSafeOwner = useIsSafeOwner()
const isSpendingLimit = useIsOnlySpendingLimitBeneficiary()
const connectWallet = useConnectWallet()
const isWrongChain = useIsWrongChain()
const isDelegate = useIsWalletDelegate()

const { safe } = useSafeInfo()

const isCounterfactualMultiSig = !allowNonOwner && !safe.deployed && safe.threshold > 1

const message =
Expand All @@ -41,8 +51,8 @@ const CheckWallet = ({ children, allowSpendingLimit, allowNonOwner, noTooltip }:
? Message.CounterfactualMultisig
: Message.NotSafeOwner

if (checkNetwork && isWrongChain) return children(false)
if (!message) return children(true)

if (noTooltip) return children(false)

return (
Expand Down
12 changes: 7 additions & 5 deletions src/components/new-safe/create/NetworkWarning/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { Alert, AlertTitle, Box } from '@mui/material'
import { useCurrentChain } from '@/hooks/useChains'
import ChainSwitcher from '@/components/common/ChainSwitcher'
import useIsWrongChain from '@/hooks/useIsWrongChain'

const NetworkWarning = () => {
const NetworkWarning = ({ action }: { action?: string }) => {
const chain = useCurrentChain()
const isWrongChain = useIsWrongChain()

if (!chain) return null
if (!chain || !isWrongChain) return null

return (
<Alert severity="warning" sx={{ mt: 3 }}>
<Alert severity="warning">
<AlertTitle sx={{ fontWeight: 700 }}>Change your wallet network</AlertTitle>
You are trying to create a Safe Account on {chain.chainName}. Make sure that your wallet is set to the same
network.
You are trying to {action || 'sign or execute a transaction'} on {chain.chainName}. Make sure that your wallet is
set to the same network.
<Box mt={2}>
<ChainSwitcher />
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ describe('NetworkFee', () => {
it('should display the total fee', () => {
jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'MetaMask' } as unknown as ConnectedWallet)
const mockTotalFee = '0.0123'
const result = render(<NetworkFee totalFee={mockTotalFee} chain={mockChainInfo} willRelay={true} />)
const result = render(<NetworkFee totalFee={mockTotalFee} chain={mockChainInfo} isWaived={true} />)

expect(result.getByText(`≈ ${mockTotalFee} ${mockChainInfo.nativeCurrency.symbol}`)).toBeInTheDocument()
})
Expand Down
12 changes: 6 additions & 6 deletions src/components/new-safe/create/steps/ReviewStep/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,17 @@ import { ECOSYSTEM_ID_ADDRESS } from '@/config/constants'
export const NetworkFee = ({
totalFee,
chain,
willRelay,
isWaived,
inline = false,
}: {
totalFee: string
chain: ChainInfo | undefined
willRelay: boolean
isWaived: boolean
inline?: boolean
}) => {
return (
<Box className={classnames(css.networkFee, { [css.networkFeeInline]: inline })}>
<Typography className={classnames({ [css.sponsoredFee]: willRelay })}>
<Typography className={classnames({ [css.strikethrough]: isWaived })}>
<b>
&asymp; {totalFee} {chain?.nativeCurrency.symbol}
</b>
Expand Down Expand Up @@ -288,7 +288,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe
<Grid item>
<Typography component="div" mt={2}>
You will have to confirm a transaction and pay an estimated fee of{' '}
<NetworkFee totalFee={totalFee} willRelay={willRelay} chain={chain} inline /> with your connected
<NetworkFee totalFee={totalFee} isWaived={willRelay} chain={chain} inline /> with your connected
wallet
</Typography>
</Grid>
Expand Down Expand Up @@ -321,7 +321,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe
name="Est. network fee"
value={
<>
<NetworkFee totalFee={totalFee} willRelay={willRelay} chain={chain} />
<NetworkFee totalFee={totalFee} isWaived={willRelay} chain={chain} />

{!willRelay && (
<Typography variant="body2" color="text.secondary" mt={1}>
Expand All @@ -333,7 +333,7 @@ const ReviewStep = ({ data, onSubmit, onBack, setStep }: StepRenderProps<NewSafe
/>
</Grid>

{isWrongChain && <NetworkWarning />}
<NetworkWarning action="create a Safe Account" />

{!walletCanPay && !willRelay && (
<ErrorMessage>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
font-size: 14px;
}

.sponsoredFee {
.strikethrough {
text-decoration: line-through;
color: var(--color-text-secondary);
}
Expand Down
5 changes: 4 additions & 1 deletion src/components/new-safe/create/steps/SetNameStep/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,10 @@ function SetNameStep({
.
</Typography>

{isWrongChain && <NetworkWarning />}
<Box sx={{ '&:not(:empty)': { mt: 3 } }}>
<NetworkWarning action="create a Safe Account" />
</Box>

<NoWalletConnectedWarning />
</Box>
<Divider />
Expand Down
21 changes: 8 additions & 13 deletions src/components/settings/PushNotifications/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
Switch,
Divider,
Link as MuiLink,
useMediaQuery,
useTheme,
} from '@mui/material'
import Link from 'next/link'
import { useState } from 'react'
Expand All @@ -28,10 +30,10 @@ import { AppRoutes } from '@/config/routes'
import CheckWallet from '@/components/common/CheckWallet'
import { useIsMac } from '@/hooks/useIsMac'
import useOnboard from '@/hooks/wallets/useOnboard'
import { assertWalletChain } from '@/services/tx/tx-sender/sdk'
import ExternalLink from '@/components/common/ExternalLink'

import css from './styles.module.css'
import NetworkWarning from '@/components/new-safe/create/NetworkWarning'

export const PushNotifications = (): ReactElement => {
const { safe, safeLoaded } = useSafeInfo()
Expand All @@ -40,6 +42,8 @@ export const PushNotifications = (): ReactElement => {
const [isRegistering, setIsRegistering] = useState(false)
const [isUpdatingIndexedDb, setIsUpdatingIndexedDb] = useState(false)
const onboard = useOnboard()
const theme = useTheme()
const isLargeScreen = useMediaQuery(theme.breakpoints.up('lg'))

const { updatePreferences, getPreferences, getAllPreferences } = useNotificationPreferences()
const { unregisterSafeNotifications, unregisterDeviceNotifications, registerNotifications } =
Expand All @@ -58,18 +62,8 @@ export const PushNotifications = (): ReactElement => {
const shouldShowMacHelper = isMac || IS_DEV

const handleOnChange = async () => {
if (!onboard) {
return
}

setIsRegistering(true)

try {
await assertWalletChain(onboard, safe.chainId)
} catch {
return
}

if (!preferences) {
await registerNotifications({ [safe.chainId]: [safe.address.value] })
trackEvent(PUSH_NOTIFICATION_EVENTS.ENABLE_SAFE)
Expand Down Expand Up @@ -126,16 +120,17 @@ export const PushNotifications = (): ReactElement => {
{safeLoaded ? (
<>
<Divider />
<NetworkWarning action="change your notification settings" />

<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<EthHashInfo
address={safe.address.value}
showCopyButton
shortAddress={false}
shortAddress={!isLargeScreen}
showName={true}
hasExplorer
/>
<CheckWallet allowNonOwner>
<CheckWallet allowNonOwner checkNetwork={!isRegistering && safe.deployed}>
{(isOk) => (
<FormControlLabel
data-testid="notifications-switch"
Expand Down
2 changes: 1 addition & 1 deletion src/components/transactions/ExecuteTxButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const ExecuteTxButton = ({

return (
<>
<CheckWallet allowNonOwner>
<CheckWallet allowNonOwner checkNetwork={!isDisabled}>
{(isOk) => (
<Tooltip title={isOk && !isNext ? 'You must execute the transaction with the lowest nonce first' : ''}>
<span>
Expand Down
2 changes: 1 addition & 1 deletion src/components/transactions/SignTxButton/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ const SignTxButton = ({
}

return (
<CheckWallet>
<CheckWallet checkNetwork={!isDisabled}>
{(isOk) => (
<Tooltip title={isOk && !isSignable ? "You've already signed this transaction" : ''}>
<span>
Expand Down
2 changes: 1 addition & 1 deletion src/components/tx-flow/common/TxLayout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const TxLayout = ({

const theme = useTheme()
const isSmallScreen = useMediaQuery(theme.breakpoints.down('md'))
const isDesktop = useMediaQuery(theme.breakpoints.down('lg'))
const isDesktop = useMediaQuery(theme.breakpoints.down(1399.95))

const steps = Array.isArray(children) ? children : [children]
const progress = Math.round(((step + 1) / steps.length) * 100)
Expand Down
2 changes: 1 addition & 1 deletion src/components/tx-flow/common/TxLayout/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@
/* Height of transaction type title */
margin-top: 46px;
}
@media (max-width: 1199.95px) {
@media (max-width: 1399.95px) {
.backButton {
left: 50%;
transform: translateX(-50%);
Expand Down
Loading

0 comments on commit 3cb5f57

Please sign in to comment.