Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: display a Network switch button when connected on the wrong chain #4027

Merged
merged 17 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading