diff --git a/src/components/common/ConnectWallet/AccountCenter.tsx b/src/components/common/ConnectWallet/AccountCenter.tsx index 118b07a4a9..1f12eb6acb 100644 --- a/src/components/common/ConnectWallet/AccountCenter.tsx +++ b/src/components/common/ConnectWallet/AccountCenter.tsx @@ -1,75 +1,38 @@ import type { MouseEvent } from 'react' -import { useContext, useState } from 'react' -import { Box, Button, ButtonBase, Paper, Popover } from '@mui/material' +import { useState } from 'react' +import { Box, ButtonBase, Paper, Popover } from '@mui/material' import css from '@/components/common/ConnectWallet/styles.module.css' -import EthHashInfo from '@/components/common/EthHashInfo' import ExpandLessIcon from '@mui/icons-material/ExpandLess' import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import useOnboard, { switchWallet } from '@/hooks/wallets/useOnboard' -import { useAppSelector } from '@/store' -import { selectChainById } from '@/store/chainsSlice' -import ChainSwitcher from '../ChainSwitcher' -import useAddressBook from '@/hooks/useAddressBook' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' -import WalletInfo from '../WalletInfo' -import ChainIndicator from '@/components/common/ChainIndicator' -import { isSocialLoginWallet } from '@/services/mpc/module' -import SocialLoginInfo from '@/components/common/SocialLoginInfo' +import WalletOverview from '../WalletOverview' +import WalletInfo from '@/components/common/WalletInfo' -import LockIcon from '@/public/images/common/lock-small.svg' -import Link from 'next/link' -import { AppRoutes } from '@/config/routes' -import { useRouter } from 'next/router' -import { MpcWalletContext } from '@/components/common/ConnectWallet/MPCWalletProvider' -import { IS_PRODUCTION } from '@/config/constants' -import useMPC from '@/hooks/wallets/mpc/useMPC' -import { isMFAEnabled } from '@/components/settings/SignerAccountMFA/helper' - -const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { - const { resetAccount } = useContext(MpcWalletContext) - const mpcCoreKit = useMPC() - const router = useRouter() +export const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { const [anchorEl, setAnchorEl] = useState(null) - const onboard = useOnboard() - const chainInfo = useAppSelector((state) => selectChainById(state, wallet.chainId)) - const addressBook = useAddressBook() - const prefix = chainInfo?.shortName - - const handleSwitchWallet = () => { - if (onboard) { - handleClose() - switchWallet(onboard) - } - } - - const handleDisconnect = () => { - if (!wallet) return - - onboard?.disconnectWallet({ - label: wallet.label, - }) - - handleClose() - } - const handleClick = (event: MouseEvent) => { + const openWalletInfo = (event: MouseEvent) => { setAnchorEl(event.currentTarget) } - const handleClose = () => { + const closeWalletInfo = () => { setAnchorEl(null) } const open = Boolean(anchorEl) const id = open ? 'simple-popover' : undefined - const isSocialLogin = isSocialLoginWallet(wallet.label) - return ( <> - + - + {open ? : } @@ -81,7 +44,7 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { id={id} open={open} anchorEl={anchorEl} - onClose={handleClose} + onClose={closeWalletInfo} anchorOrigin={{ vertical: 'bottom', horizontal: 'center', @@ -93,57 +56,7 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { sx={{ marginTop: 1 }} > - - - - {isSocialLogin ? ( - <> - - {mpcCoreKit && !isMFAEnabled(mpcCoreKit) && ( - - - - )} - - ) : ( - - )} - - - - - - - - - - {!IS_PRODUCTION && isSocialLogin && ( - - )} + diff --git a/src/components/common/ConnectWallet/__tests__/AccountCenter.test.tsx b/src/components/common/ConnectWallet/__tests__/AccountCenter.test.tsx new file mode 100644 index 0000000000..000a4d6a05 --- /dev/null +++ b/src/components/common/ConnectWallet/__tests__/AccountCenter.test.tsx @@ -0,0 +1,47 @@ +import { render } from '@/tests/test-utils' +import { AccountCenter } from '@/components/common/ConnectWallet/AccountCenter' +import { type EIP1193Provider, type OnboardAPI } from '@web3-onboard/core' +import { type NextRouter } from 'next/router' +import { act, waitFor } from '@testing-library/react' + +const mockWallet = { + address: '0x1234567890123456789012345678901234567890', + chainId: '5', + label: '', + provider: null as unknown as EIP1193Provider, +} + +const mockRouter = { + query: {}, + pathname: '', +} as NextRouter + +const mockOnboard = { + connectWallet: jest.fn(), + disconnectWallet: jest.fn(), + setChain: jest.fn(), +} as unknown as OnboardAPI + +describe('AccountCenter', () => { + it('should open and close the account center on click', async () => { + const { getByText, getByTestId } = render() + + const openButton = getByTestId('open-account-center') + + act(() => { + openButton.click() + }) + + const disconnectButton = getByText('Disconnect') + + expect(disconnectButton).toBeInTheDocument() + + act(() => { + disconnectButton.click() + }) + + await waitFor(() => { + expect(disconnectButton).not.toBeInTheDocument() + }) + }) +}) diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index 13b7128eb0..afa1289886 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -70,33 +70,6 @@ min-height: 42px; } -.accountContainer { - width: 100%; - margin-bottom: var(--space-1); -} - -.accountContainer > span { - border-radius: 8px 8px 0 0; -} - -.addressContainer { - border-radius: 0 0 8px 8px; - padding: 12px; - border: 1px solid var(--color-border-light); - border-top: 0; - font-size: 14px; -} - -.warningButton { - background-color: var(--color-warning-background); - color: var(--color-warning-main); - font-size: 12px; -} - -.warningButton:global.MuiButton-root:hover { - background-color: var(--color-warning-background); -} - @media (max-width: 599.95px) { .socialLoginInfo > div > div { display: none; diff --git a/src/components/common/WalletInfo/index.test.tsx b/src/components/common/WalletInfo/index.test.tsx new file mode 100644 index 0000000000..8821659c21 --- /dev/null +++ b/src/components/common/WalletInfo/index.test.tsx @@ -0,0 +1,175 @@ +import { render } from '@/tests/test-utils' +import { WalletInfo } from '@/components/common/WalletInfo/index' +import { type EIP1193Provider, type OnboardAPI } from '@web3-onboard/core' +import { type NextRouter } from 'next/router' +import * as mpcModule from '@/services/mpc/module' +import * as constants from '@/config/constants' +import * as mfaHelper from '@/components/settings/SignerAccountMFA/helper' +import { type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit' + +const mockWallet = { + address: '0x1234567890123456789012345678901234567890', + chainId: '5', + label: '', + provider: null as unknown as EIP1193Provider, +} + +const mockRouter = { + query: {}, + pathname: '', +} as NextRouter + +const mockOnboard = { + connectWallet: jest.fn(), + disconnectWallet: jest.fn(), + setChain: jest.fn(), +} as unknown as OnboardAPI + +describe('WalletInfo', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should display the wallet address', () => { + const { getByText } = render( + , + ) + + expect(getByText('0x1234...7890')).toBeInTheDocument() + }) + + it('should display a switch wallet button', () => { + const { getByText } = render( + , + ) + + expect(getByText('Switch wallet')).toBeInTheDocument() + }) + + it('should display a Disconnect button', () => { + const { getByText } = render( + , + ) + + expect(getByText('Disconnect')).toBeInTheDocument() + }) + + it('should display a Delete Account button on dev for social login', () => { + jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) + jest.spyOn(constants, 'IS_PRODUCTION', 'get').mockImplementation(() => false) + + const { getByText } = render( + , + ) + + expect(getByText('Delete Account')).toBeInTheDocument() + }) + + it('should not display a Delete Account on prod', () => { + jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) + jest.spyOn(constants, 'IS_PRODUCTION', 'get').mockImplementation(() => true) + + const { queryByText } = render( + , + ) + + expect(queryByText('Delete Account')).not.toBeInTheDocument() + }) + + it('should not display a Delete Account if not social login', () => { + jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(false) + jest.spyOn(constants, 'IS_PRODUCTION', 'get').mockImplementation(() => false) + + const { queryByText } = render( + , + ) + + expect(queryByText('Delete Account')).not.toBeInTheDocument() + }) + + it('should display an enable mfa button if mfa is not enabled', () => { + jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) + jest.spyOn(mfaHelper, 'isMFAEnabled').mockReturnValue(false) + + const { getByText } = render( + , + ) + + expect(getByText('Add multifactor authentication')).toBeInTheDocument() + }) + + it('should not display an enable mfa button if mfa is already enabled', () => { + jest.spyOn(mpcModule, 'isSocialLoginWallet').mockReturnValue(true) + jest.spyOn(mfaHelper, 'isMFAEnabled').mockReturnValue(true) + + const { queryByText } = render( + , + ) + + expect(queryByText('Add multifactor authentication')).not.toBeInTheDocument() + }) +}) diff --git a/src/components/common/WalletInfo/index.tsx b/src/components/common/WalletInfo/index.tsx index 7a04d80c27..952bb19218 100644 --- a/src/components/common/WalletInfo/index.tsx +++ b/src/components/common/WalletInfo/index.tsx @@ -1,56 +1,130 @@ -import { Box, Typography } from '@mui/material' -import { Suspense } from 'react' -import type { ReactElement } from 'react' - +import { Box, Button } from '@mui/material' +import css from './styles.module.css' +import ChainIndicator from '@/components/common/ChainIndicator' +import SocialLoginInfo from '@/components/common/SocialLoginInfo' +import { isMFAEnabled } from '@/components/settings/SignerAccountMFA/helper' +import Link from 'next/link' +import { AppRoutes } from '@/config/routes' +import LockIcon from '@/public/images/common/lock-small.svg' import EthHashInfo from '@/components/common/EthHashInfo' -import WalletIcon from '@/components/common/WalletIcon' -import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' +import ChainSwitcher from '@/components/common/ChainSwitcher' +import { IS_PRODUCTION } from '@/config/constants' +import { isSocialLoginWallet } from '@/services/mpc/module' +import useOnboard, { type ConnectedWallet, switchWallet } from '@/hooks/wallets/useOnboard' +import { type MPCWalletHook } from '@/hooks/wallets/mpc/useMPCWallet' +import useMPC from '@/hooks/wallets/mpc/useMPC' +import { useRouter } from 'next/router' +import useAddressBook from '@/hooks/useAddressBook' import { useAppSelector } from '@/store' import { selectChainById } from '@/store/chainsSlice' +import madProps from '@/utils/mad-props' +import { useContext } from 'react' +import { MpcWalletContext } from '@/components/common/ConnectWallet/MPCWalletProvider' -import css from './styles.module.css' -import { isSocialLoginWallet } from '@/services/mpc/module' -import SocialLoginInfo from '@/components/common/SocialLoginInfo' +type WalletInfoProps = { + wallet: ConnectedWallet + resetAccount: MPCWalletHook['resetAccount'] + mpcCoreKit: ReturnType + router: ReturnType + onboard: ReturnType + addressBook: ReturnType + handleClose: () => void +} -export const UNKNOWN_CHAIN_NAME = 'Unknown' +export const WalletInfo = ({ + wallet, + resetAccount, + mpcCoreKit, + router, + onboard, + addressBook, + handleClose, +}: WalletInfoProps) => { + const chainInfo = useAppSelector((state) => selectChainById(state, wallet.chainId)) + const prefix = chainInfo?.shortName -const WalletInfo = ({ wallet }: { wallet: ConnectedWallet }): ReactElement => { - const walletChain = useAppSelector((state) => selectChainById(state, wallet.chainId)) - const prefix = walletChain?.shortName + const handleSwitchWallet = () => { + if (onboard) { + handleClose() + switchWallet(onboard) + } + } - const isSocialLogin = isSocialLoginWallet(wallet.label) + const handleDisconnect = () => { + if (!wallet) return - if (isSocialLogin) { - return ( -
- -
- ) - } + onboard?.disconnectWallet({ + label: wallet.label, + }) - return ( - - - - - - + handleClose() + } - - - {wallet.label} @ {walletChain?.chainName || UNKNOWN_CHAIN_NAME} - + const isSocialLogin = isSocialLoginWallet(wallet.label) - - {wallet.ens ? ( -
{wallet.ens}
+ return ( + <> + + + + {isSocialLogin ? ( + <> + + {mpcCoreKit && !isMFAEnabled(mpcCoreKit) && ( + + + + )} + ) : ( - + )} -
+
-
+ + + + + + + + {!IS_PRODUCTION && isSocialLogin && ( + + )} + ) } -export default WalletInfo +const useResetAccount = () => useContext(MpcWalletContext).resetAccount + +export default madProps(WalletInfo, { + resetAccount: useResetAccount, + mpcCoreKit: useMPC, + router: useRouter, + onboard: useOnboard, + addressBook: useAddressBook, +}) diff --git a/src/components/common/WalletInfo/styles.module.css b/src/components/common/WalletInfo/styles.module.css index 96ddc9f7b5..a8c3a97308 100644 --- a/src/components/common/WalletInfo/styles.module.css +++ b/src/components/common/WalletInfo/styles.module.css @@ -1,38 +1,26 @@ -.container { - display: flex; - align-items: center; - gap: var(--space-1); - justify-content: center; +.accountContainer { + width: 100%; + margin-bottom: var(--space-1); } -.imageContainer { - display: flex; - justify-content: center; +.accountContainer > span { + border-radius: 8px 8px 0 0; } -[data-theme='dark'] .imageContainer img[alt*='Ledger'] { - filter: invert(100%); +.addressContainer { + border-radius: 0 0 8px 8px; + padding: 12px; + border: 1px solid var(--color-border-light); + border-top: 0; + font-size: 14px; } -@media (max-width: 599.95px) { - .buttonContainer button { - font-size: 12px; - } - - .walletDetails { - display: none; - } - - .imageContainer img { - width: 22px; - height: auto; - } - - .walletName { - display: none; - } +.warningButton { + background-color: var(--color-warning-background); + color: var(--color-warning-main); + font-size: 12px; +} - .socialLoginInfo > div > div { - display: none; - } +.warningButton:global.MuiButton-root:hover { + background-color: var(--color-warning-background); } diff --git a/src/components/common/WalletOverview/index.tsx b/src/components/common/WalletOverview/index.tsx new file mode 100644 index 0000000000..ff0d0ba11e --- /dev/null +++ b/src/components/common/WalletOverview/index.tsx @@ -0,0 +1,56 @@ +import { Box, Typography } from '@mui/material' +import { Suspense } from 'react' +import type { ReactElement } from 'react' + +import EthHashInfo from '@/components/common/EthHashInfo' +import WalletIcon from '@/components/common/WalletIcon' +import type { ConnectedWallet } from '@/hooks/wallets/useOnboard' +import { useAppSelector } from '@/store' +import { selectChainById } from '@/store/chainsSlice' + +import css from './styles.module.css' +import { isSocialLoginWallet } from '@/services/mpc/module' +import SocialLoginInfo from '@/components/common/SocialLoginInfo' + +export const UNKNOWN_CHAIN_NAME = 'Unknown' + +const WalletOverview = ({ wallet }: { wallet: ConnectedWallet }): ReactElement => { + const walletChain = useAppSelector((state) => selectChainById(state, wallet.chainId)) + const prefix = walletChain?.shortName + + const isSocialLogin = isSocialLoginWallet(wallet.label) + + if (isSocialLogin) { + return ( +
+ +
+ ) + } + + return ( + + + + + + + + + + {wallet.label} @ {walletChain?.chainName || UNKNOWN_CHAIN_NAME} + + + + {wallet.ens ? ( +
{wallet.ens}
+ ) : ( + + )} +
+
+
+ ) +} + +export default WalletOverview diff --git a/src/components/common/WalletOverview/styles.module.css b/src/components/common/WalletOverview/styles.module.css new file mode 100644 index 0000000000..96ddc9f7b5 --- /dev/null +++ b/src/components/common/WalletOverview/styles.module.css @@ -0,0 +1,38 @@ +.container { + display: flex; + align-items: center; + gap: var(--space-1); + justify-content: center; +} + +.imageContainer { + display: flex; + justify-content: center; +} + +[data-theme='dark'] .imageContainer img[alt*='Ledger'] { + filter: invert(100%); +} + +@media (max-width: 599.95px) { + .buttonContainer button { + font-size: 12px; + } + + .walletDetails { + display: none; + } + + .imageContainer img { + width: 22px; + height: auto; + } + + .walletName { + display: none; + } + + .socialLoginInfo > div > div { + display: none; + } +} diff --git a/src/components/new-safe/create/OverviewWidget/index.tsx b/src/components/new-safe/create/OverviewWidget/index.tsx index b6f463ec6a..2d0a5f8586 100644 --- a/src/components/new-safe/create/OverviewWidget/index.tsx +++ b/src/components/new-safe/create/OverviewWidget/index.tsx @@ -1,5 +1,5 @@ import ChainIndicator from '@/components/common/ChainIndicator' -import WalletInfo from '@/components/common/WalletInfo' +import WalletOverview from 'src/components/common/WalletOverview' import { useCurrentChain } from '@/hooks/useChains' import useWallet from '@/hooks/wallets/useWallet' import { Card, Grid, Typography } from '@mui/material' @@ -14,7 +14,7 @@ const OverviewWidget = ({ safeName }: { safeName: string }): ReactElement | null const wallet = useWallet() const chain = useCurrentChain() const rows = [ - ...(wallet ? [{ title: 'Wallet', component: }] : []), + ...(wallet ? [{ title: 'Wallet', component: }] : []), ...(chain ? [{ title: 'Network', component: }] : []), ...(safeName !== '' ? [{ title: 'Name', component: {safeName} }] : []), ]