diff --git a/public/images/common/check-filled.svg b/public/images/common/check-filled.svg new file mode 100644 index 0000000000..284624fc19 --- /dev/null +++ b/public/images/common/check-filled.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/logo-text.svg b/public/images/logo-text.svg new file mode 100644 index 0000000000..b71ae734a4 --- /dev/null +++ b/public/images/logo-text.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/public/images/welcome/logo-google.svg b/public/images/welcome/logo-google.svg new file mode 100644 index 0000000000..65781d4881 --- /dev/null +++ b/public/images/welcome/logo-google.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/components/common/ConnectWallet/AccountCenter.tsx b/src/components/common/ConnectWallet/AccountCenter.tsx index 082a0fab54..03a0aab974 100644 --- a/src/components/common/ConnectWallet/AccountCenter.tsx +++ b/src/components/common/ConnectWallet/AccountCenter.tsx @@ -13,7 +13,6 @@ import ChainSwitcher from '../ChainSwitcher' import useAddressBook from '@/hooks/useAddressBook' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' import WalletInfo, { UNKNOWN_CHAIN_NAME } from '../WalletInfo' -import { MPCWallet } from './MPCWallet' const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { const [anchorEl, setAnchorEl] = useState(null) @@ -115,8 +114,6 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { Disconnect - - > diff --git a/src/components/common/ConnectWallet/MPCLogin.tsx b/src/components/common/ConnectWallet/MPCLogin.tsx new file mode 100644 index 0000000000..e55a32647b --- /dev/null +++ b/src/components/common/ConnectWallet/MPCLogin.tsx @@ -0,0 +1,82 @@ +import { MPCWalletState } from '@/hooks/wallets/mpc/useMPCWallet' +import { Box, Button, SvgIcon, Typography } from '@mui/material' +import { useContext, useEffect, useState } from 'react' +import { MpcWalletContext } from './MPCWalletProvider' +import { PasswordRecovery } from './PasswordRecovery' +import GoogleLogo from '@/public/images/welcome/logo-google.svg' + +import css from './styles.module.css' +import useWallet from '@/hooks/wallets/useWallet' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' + +const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { + const { loginPending, triggerLogin, userInfo, walletState, recoverFactorWithPassword } = useContext(MpcWalletContext) + + const wallet = useWallet() + + const [loginTriggered, setLoginTriggered] = useState(false) + + const login = async () => { + setLoginTriggered(true) + await triggerLogin() + } + + // If login was triggered through the Button we immediately continue if logged in + useEffect(() => { + if (loginTriggered && wallet && wallet.label === ONBOARD_MPC_MODULE_LABEL && onLogin) { + onLogin() + } + }, [loginTriggered, onLogin, wallet]) + + return ( + <> + {wallet && userInfo ? ( + <> + + + + + + Continue as {userInfo.name} + + {userInfo.email} + + + + + > + ) : ( + + + Continue with Google + + + )} + + {walletState === MPCWalletState.MANUAL_RECOVERY && ( + + )} + > + ) +} + +export default MPCLogin diff --git a/src/components/common/ConnectWallet/MPCWallet.tsx b/src/components/common/ConnectWallet/MPCWallet.tsx deleted file mode 100644 index 86b5e05ca6..0000000000 --- a/src/components/common/ConnectWallet/MPCWallet.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { MPCWalletState } from '@/hooks/wallets/mpc/useMPCWallet' -import { Box, Button, CircularProgress } from '@mui/material' -import { useContext } from 'react' -import { MpcWalletContext } from './MPCWalletProvider' -import { PasswordRecovery } from './PasswordRecovery' - -export const MPCWallet = () => { - const { loginPending, triggerLogin, resetAccount, userInfo, walletState, recoverFactorWithPassword } = - useContext(MpcWalletContext) - - return ( - <> - {userInfo.email ? ( - <> - - - - Reset - - - - > - ) : ( - - {loginPending ? ( - <> - Login Pending - > - ) : ( - 'Login with Socials' - )} - - )} - - {walletState === MPCWalletState.MANUAL_RECOVERY && ( - - )} - > - ) -} diff --git a/src/components/common/ConnectWallet/MPCWalletProvider.tsx b/src/components/common/ConnectWallet/MPCWalletProvider.tsx index 30360bdcb2..4f91450e26 100644 --- a/src/components/common/ConnectWallet/MPCWalletProvider.tsx +++ b/src/components/common/ConnectWallet/MPCWalletProvider.tsx @@ -1,4 +1,5 @@ import { useMPCWallet, MPCWalletState } from '@/hooks/wallets/mpc/useMPCWallet' +import { type UserInfo } from '@web3auth/mpc-core-kit' import { createContext, type ReactElement } from 'react' type MPCWalletContext = { @@ -8,9 +9,7 @@ type MPCWalletContext = { upsertPasswordBackup: (password: string) => Promise recoverFactorWithPassword: (password: string, storeDeviceFactor: boolean) => Promise walletState: MPCWalletState - userInfo: { - email: string | undefined - } + userInfo: UserInfo | undefined } export const MpcWalletContext = createContext({ @@ -20,9 +19,7 @@ export const MpcWalletContext = createContext({ resetAccount: () => Promise.resolve(), upsertPasswordBackup: () => Promise.resolve(), recoverFactorWithPassword: () => Promise.resolve(), - userInfo: { - email: undefined, - }, + userInfo: undefined, }) export const MpcWalletProvider = ({ children }: { children: ReactElement }) => { diff --git a/src/components/common/ConnectWallet/WalletDetails.tsx b/src/components/common/ConnectWallet/WalletDetails.tsx index c4fb5f8c2d..1104d9dd23 100644 --- a/src/components/common/ConnectWallet/WalletDetails.tsx +++ b/src/components/common/ConnectWallet/WalletDetails.tsx @@ -3,7 +3,7 @@ import type { ReactElement } from 'react' import KeyholeIcon from '@/components/common/icons/KeyholeIcon' import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' -import { MPCWallet } from './MPCWallet' +import MPCLogin from './MPCLogin' const WalletDetails = ({ onConnect }: { onConnect?: () => void }): ReactElement => { const connectWallet = useConnectWallet() @@ -23,7 +23,7 @@ const WalletDetails = ({ onConnect }: { onConnect?: () => void }): ReactElement Connect - + > ) } diff --git a/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx new file mode 100644 index 0000000000..41952da2b2 --- /dev/null +++ b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx @@ -0,0 +1,107 @@ +import { act, render, waitFor } from '@/tests/test-utils' +import * as useWallet from '@/hooks/wallets/useWallet' +import * as useMPCWallet from '@/hooks/wallets/mpc/useMPCWallet' +import MPCLogin from '../MPCLogin' +import { hexZeroPad } from '@ethersproject/bytes' +import { type EIP1193Provider } from '@web3-onboard/common' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import { MpcWalletProvider } from '../MPCWalletProvider' + +describe('MPCLogin', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should render continue with connected account', async () => { + const mockOnLogin = jest.fn() + const walletAddress = hexZeroPad('0x1', 20) + jest.spyOn(useWallet, 'default').mockReturnValue({ + address: walletAddress, + chainId: '5', + label: ONBOARD_MPC_MODULE_LABEL, + provider: {} as unknown as EIP1193Provider, + }) + jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ + userInfo: { + email: 'test@safe.test', + name: 'Test Testermann', + profileImage: 'test.png', + }, + triggerLogin: jest.fn(), + walletState: useMPCWallet.MPCWalletState.READY, + } as unknown as useMPCWallet.MPCWalletHook) + + const result = render( + + + , + ) + + await waitFor(() => { + expect(result.findByText('Continue as Test Testermann')).resolves.toBeDefined() + }) + + // We do not automatically invoke the callback as the user did not actively connect + expect(mockOnLogin).not.toHaveBeenCalled() + + const button = await result.findByRole('button') + button.click() + + expect(mockOnLogin).toHaveBeenCalled() + }) + + it('should render google login button and invoke the callback on connection if no wallet is connected', async () => { + const mockOnLogin = jest.fn() + const walletAddress = hexZeroPad('0x1', 20) + const mockUseWallet = jest.spyOn(useWallet, 'default').mockReturnValue(null) + const mockTriggerLogin = jest.fn() + const mockUseMPCWallet = jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ + userInfo: { + email: undefined, + name: undefined, + profileImage: undefined, + }, + triggerLogin: mockTriggerLogin, + walletState: useMPCWallet.MPCWalletState.NOT_INITIALIZED, + } as unknown as useMPCWallet.MPCWalletHook) + + const result = render( + + + , + ) + + await waitFor(() => { + expect(result.findByText('Continue with Google')).resolves.toBeDefined() + }) + + // We do not automatically invoke the callback as the user did not actively connect + expect(mockOnLogin).not.toHaveBeenCalled() + + await act(async () => { + // Click the button and mock a successful login + const button = await result.findByRole('button') + button.click() + mockUseMPCWallet.mockReset().mockReturnValue({ + userInfo: { + email: 'test@safe.test', + name: 'Test Testermann', + profileImage: 'test.png', + }, + triggerLogin: jest.fn(), + walletState: useMPCWallet.MPCWalletState.READY, + } as unknown as useMPCWallet.MPCWalletHook) + + mockUseWallet.mockReset().mockReturnValue({ + address: walletAddress, + chainId: '5', + label: ONBOARD_MPC_MODULE_LABEL, + provider: {} as unknown as EIP1193Provider, + }) + }) + + await waitFor(() => { + expect(mockOnLogin).toHaveBeenCalled() + }) + }) +}) diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index b189d44bee..aa127889c6 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -28,6 +28,18 @@ width: 100%; } +.profileImg { + border-radius: var(--space-2); + width: 32px; + height: 32px; +} + +.profileData { + display: flex; + flex-direction: column; + align-items: flex-start; +} + .rowContainer { align-self: stretch; margin-left: calc(var(--space-2) * -1); diff --git a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts index 807f848364..a3bc08f3da 100644 --- a/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts +++ b/src/components/new-safe/create/__tests__/useSyncSafeCreationStep.test.ts @@ -5,6 +5,9 @@ import * as localStorage from '@/services/local-storage/useLocalStorage' import type { ConnectedWallet } from '@/services/onboard' import * as usePendingSafe from '../steps/StatusStep/usePendingSafe' import * as useIsWrongChain from '@/hooks/useIsWrongChain' +import * as useRouter from 'next/router' +import { type NextRouter } from 'next/router' +import { AppRoutes } from '@/config/routes' describe('useSyncSafeCreationStep', () => { const mockPendingSafe = { @@ -22,13 +25,18 @@ describe('useSyncSafeCreationStep', () => { }) it('should go to the first step if no wallet is connected', async () => { + const mockPushRoute = jest.fn() jest.spyOn(wallet, 'default').mockReturnValue(null) jest.spyOn(usePendingSafe, 'usePendingSafe').mockReturnValue([undefined, setPendingSafeSpy]) + jest.spyOn(useRouter, 'useRouter').mockReturnValue({ + push: mockPushRoute, + } as unknown as NextRouter) const mockSetStep = jest.fn() renderHook(() => useSyncSafeCreationStep(mockSetStep)) - expect(mockSetStep).toHaveBeenCalledWith(0) + expect(mockSetStep).not.toHaveBeenCalled() + expect(mockPushRoute).toHaveBeenCalledWith(AppRoutes.welcome) }) it('should go to the fourth step if there is a pending safe', async () => { @@ -40,7 +48,7 @@ describe('useSyncSafeCreationStep', () => { renderHook(() => useSyncSafeCreationStep(mockSetStep)) - expect(mockSetStep).toHaveBeenCalledWith(4) + expect(mockSetStep).toHaveBeenCalledWith(3) }) it('should go to the second step if the wrong chain is connected', async () => { @@ -53,7 +61,7 @@ describe('useSyncSafeCreationStep', () => { renderHook(() => useSyncSafeCreationStep(mockSetStep)) - expect(mockSetStep).toHaveBeenCalledWith(1) + expect(mockSetStep).toHaveBeenCalledWith(0) }) it('should not do anything if wallet is connected and there is no pending safe', async () => { diff --git a/src/components/new-safe/create/index.tsx b/src/components/new-safe/create/index.tsx index a0807aa59a..f2b179d593 100644 --- a/src/components/new-safe/create/index.tsx +++ b/src/components/new-safe/create/index.tsx @@ -5,7 +5,6 @@ import useWallet from '@/hooks/wallets/useWallet' import OverviewWidget from '@/components/new-safe/create/OverviewWidget' import type { NamedAddress } from '@/components/new-safe/create/types' import type { TxStepperProps } from '@/components/new-safe/CardStepper/useCardStepper' -import ConnectWalletStep from '@/components/new-safe/create/steps/ConnectWalletStep' import SetNameStep from '@/components/new-safe/create/steps/SetNameStep' import OwnerPolicyStep from '@/components/new-safe/create/steps/OwnerPolicyStep' import ReviewStep from '@/components/new-safe/create/steps/ReviewStep' @@ -111,13 +110,6 @@ const CreateSafe = () => { const [activeStep, setActiveStep] = useState(0) const CreateSafeSteps: TxStepperProps['steps'] = [ - { - title: 'Connect wallet', - subtitle: 'The connected wallet will pay the network fees for the Safe Account creation.', - render: (data, onSubmit, onBack, setStep) => ( - - ), - }, { title: 'Select network and name of your Safe Account', subtitle: 'Select the network on which to create your Safe Account', diff --git a/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx b/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx deleted file mode 100644 index c242060b5d..0000000000 --- a/src/components/new-safe/create/steps/ConnectWalletStep/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import { useEffect, useState } from 'react' -import { Box, Button, Divider, Typography } from '@mui/material' -import useWallet from '@/hooks/wallets/useWallet' - -import type { NewSafeFormData } from '@/components/new-safe/create' -import type { StepRenderProps } from '@/components/new-safe/CardStepper/useCardStepper' -import useSyncSafeCreationStep from '@/components/new-safe/create/useSyncSafeCreationStep' -import layoutCss from '@/components/new-safe/create/styles.module.css' -import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' -import KeyholeIcon from '@/components/common/icons/KeyholeIcon' -import { usePendingSafe } from '../StatusStep/usePendingSafe' -import { MPCWallet } from '@/components/common/ConnectWallet/MPCWallet' - -const ConnectWalletStep = ({ onSubmit, setStep }: StepRenderProps) => { - const [pendingSafe] = usePendingSafe() - const wallet = useWallet() - const handleConnect = useConnectWallet() - const [, setSubmitted] = useState(false) - useSyncSafeCreationStep(setStep) - - useEffect(() => { - if (!wallet || pendingSafe) return - - setSubmitted((prev) => { - if (prev) return prev - onSubmit({ owners: [{ address: wallet.address, name: wallet.ens || '' }] }) - return true - }) - }, [onSubmit, wallet, pendingSafe]) - - return ( - <> - - - - - - - - Connect - - - - or - - - - - - > - ) -} - -export default ConnectWalletStep diff --git a/src/components/new-safe/create/useSyncSafeCreationStep.ts b/src/components/new-safe/create/useSyncSafeCreationStep.ts index 6b4ad6ff94..3033daf932 100644 --- a/src/components/new-safe/create/useSyncSafeCreationStep.ts +++ b/src/components/new-safe/create/useSyncSafeCreationStep.ts @@ -4,31 +4,33 @@ import type { NewSafeFormData } from '@/components/new-safe/create/index' import useWallet from '@/hooks/wallets/useWallet' import { usePendingSafe } from './steps/StatusStep/usePendingSafe' import useIsWrongChain from '@/hooks/useIsWrongChain' +import { useRouter } from 'next/router' +import { AppRoutes } from '@/config/routes' const useSyncSafeCreationStep = (setStep: StepRenderProps['setStep']) => { const [pendingSafe] = usePendingSafe() const wallet = useWallet() const isWrongChain = useIsWrongChain() + const router = useRouter() useEffect(() => { // Jump to the status screen if there is already a tx submitted if (pendingSafe) { - setStep(4) + setStep(3) return } // Jump to connect wallet step if there is no wallet and no pending Safe if (!wallet) { - setStep(0) - return + router.push(AppRoutes.welcome) } // Jump to choose name and network step if the wallet is connected to the wrong chain and there is no pending Safe if (isWrongChain) { - setStep(1) + setStep(0) return } - }, [wallet, setStep, pendingSafe, isWrongChain]) + }, [wallet, setStep, pendingSafe, isWrongChain, router]) } export default useSyncSafeCreationStep diff --git a/src/components/welcome/NewSafe.tsx b/src/components/welcome/NewSafe.tsx index 9387296884..02994badc6 100644 --- a/src/components/welcome/NewSafe.tsx +++ b/src/components/welcome/NewSafe.tsx @@ -1,156 +1,79 @@ -import React, { useEffect, useState } from 'react' -import { - Button, - Grid, - Paper, - SvgIcon, - Typography, - AccordionSummary, - AccordionDetails, - Accordion, - useMediaQuery, - Box, -} from '@mui/material' -import { useRouter } from 'next/router' -import { CREATE_SAFE_EVENTS, LOAD_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' -import Track from '../common/Track' -import { AppRoutes } from '@/config/routes' +import React, { useState } from 'react' +import { Accordion, AccordionDetails, AccordionSummary, Box, Grid, SvgIcon, Typography } from '@mui/material' import SafeList from '@/components/sidebar/SafeList' import css from './styles.module.css' -import NewSafeIcon from '@/public/images/welcome/new-safe.svg' -import LoadSafeIcon from '@/public/images/welcome/load-safe.svg' +import CheckFilled from '@/public/images/common/check-filled.svg' + import ExpandMoreIcon from '@mui/icons-material/ExpandMore' -import { useTheme } from '@mui/material/styles' import { DataWidget } from '@/components/welcome/DataWidget' -import useWallet from '@/hooks/wallets/useWallet' -import { useCurrentChain } from '@/hooks/useChains' +import WelcomeLogin from './WelcomeLogin' +import { useAppSelector } from '@/store' +import { selectTotalAdded } from '@/store/addedSafesSlice' const NewSafe = () => { - const [expanded, setExpanded] = useState(true) - const router = useRouter() - const theme = useTheme() - const isSmallScreen = useMediaQuery(theme.breakpoints.down('md')) - const wallet = useWallet() - const currentChain = useCurrentChain() + const [expanded, setExpanded] = useState(false) - useEffect(() => { - setExpanded(!isSmallScreen) - }, [isSmallScreen]) + const addedSafes = useAppSelector(selectTotalAdded) const toggleSafeList = () => { - return isSmallScreen ? setExpanded((prev) => !prev) : null + return setExpanded((prev) => !prev) } return ( - - - - - - }> - - My Safe Accounts - - - - event.stopPropagation()}> - - - - - - - - - + + + - - - - - - - + - - Welcome to {'Safe{Wallet}'} + {addedSafes > 0 && ( + + + + + }> + + My Safe Accounts ({addedSafes}) + + + + event.stopPropagation()}> + + + + + + + + + + )} + + Unlock a new way of ownership - + The most trusted decentralized custody protocol and collective asset management platform. - - - - - - Create Safe Account - - - - A new Account that is controlled by one or multiple owners. - - - - - - router.push({ - pathname: AppRoutes.newSafe.create, - query: { chain: currentChain?.shortName }, - }) - } - > - + Create new Account - - - + + + + + Stealth security with multiple owners + - - - - - - Add existing Account - - - - Already have a Safe Account? Add it via its address. - - - - - - router.push({ - pathname: AppRoutes.newSafe.load, - query: { chain: currentChain?.shortName }, - }) - } - > - Add existing Account - - - + + + + Make it yours with modules and guards + + + + + + Access 130+ ecosystem apps + diff --git a/src/components/welcome/WelcomeLogin/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx new file mode 100644 index 0000000000..758a3b23c7 --- /dev/null +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -0,0 +1,56 @@ +import useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' +import useWallet from '@/hooks/wallets/useWallet' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import { Box, Button, Typography } from '@mui/material' +import { EthHashInfo } from '@safe-global/safe-react-components' +import { useState, useEffect } from 'react' + +const WalletLogin = ({ onLogin }: { onLogin?: () => void }) => { + const wallet = useWallet() + const connectWallet = useConnectWallet() + + const [loginTriggered, setLoginTriggered] = useState(false) + + const login = async () => { + setLoginTriggered(true) + await connectWallet() + } + + // If login was triggered through the Button we immediately continue if logged in + useEffect(() => { + if (loginTriggered && wallet && onLogin) { + onLogin() + } + }, [loginTriggered, onLogin, wallet]) + + if (wallet !== null && wallet?.label !== ONBOARD_MPC_MODULE_LABEL) { + return ( + + + + + Continue with {wallet.label} + + {wallet.address && } + + {wallet.icon && ( + + )} + + + ) + } + + return ( + + Connect wallet + + ) +} + +export default WalletLogin diff --git a/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx b/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx new file mode 100644 index 0000000000..d3dcf4d715 --- /dev/null +++ b/src/components/welcome/WelcomeLogin/__tests__/WalletLogin.test.tsx @@ -0,0 +1,71 @@ +import { act, render, waitFor } from '@/tests/test-utils' +import * as useConnectWallet from '@/components/common/ConnectWallet/useConnectWallet' +import * as useWallet from '@/hooks/wallets/useWallet' +import WalletLogin from '../WalletLogin' +import { hexZeroPad } from '@ethersproject/bytes' +import { type EIP1193Provider } from '@web3-onboard/common' +import { shortenAddress } from '@/utils/formatters' + +describe('WalletLogin', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should render continue with connected wallet', async () => { + const mockOnLogin = jest.fn() + const walletAddress = hexZeroPad('0x1', 20) + jest.spyOn(useWallet, 'default').mockReturnValue({ + address: walletAddress, + chainId: '5', + label: 'MetaMask', + provider: {} as unknown as EIP1193Provider, + }) + jest.spyOn(useConnectWallet, 'default').mockReturnValue(jest.fn()) + + const result = render() + + await waitFor(() => { + expect(result.findByText(shortenAddress(walletAddress))).resolves.toBeDefined() + }) + + // We do not automatically invoke the callback as the user did not actively connect + expect(mockOnLogin).not.toHaveBeenCalled() + + const button = await result.findByRole('button') + button.click() + + expect(mockOnLogin).toHaveBeenCalled() + }) + + it('should render connect wallet and invoke the callback on connection if no wallet is connected', async () => { + const mockOnLogin = jest.fn() + const walletAddress = hexZeroPad('0x1', 20) + const mockUseWallet = jest.spyOn(useWallet, 'default').mockReturnValue(null) + jest.spyOn(useConnectWallet, 'default').mockReturnValue(jest.fn()) + + const result = render() + + await waitFor(() => { + expect(result.findByText('Connect wallet')).resolves.toBeDefined() + }) + + // We do not automatically invoke the callback as the user did not actively connect + expect(mockOnLogin).not.toHaveBeenCalled() + + await act(async () => { + // Click the button and mock a wallet connection + const button = await result.findByRole('button') + button.click() + mockUseWallet.mockReset().mockReturnValue({ + address: walletAddress, + chainId: '5', + label: 'MetaMask', + provider: {} as unknown as EIP1193Provider, + }) + }) + + await waitFor(() => { + expect(mockOnLogin).toHaveBeenCalled() + }) + }) +}) diff --git a/src/components/welcome/WelcomeLogin/index.tsx b/src/components/welcome/WelcomeLogin/index.tsx new file mode 100644 index 0000000000..4d08ea716a --- /dev/null +++ b/src/components/welcome/WelcomeLogin/index.tsx @@ -0,0 +1,53 @@ +import MPCLogin from '@/components/common/ConnectWallet/MPCLogin' +import { AppRoutes } from '@/config/routes' +import { Paper, SvgIcon, Typography, Divider, Link, Box } from '@mui/material' +import SafeLogo from '@/public/images/logo-text.svg' +import css from './styles.module.css' +import { useRouter } from 'next/router' +import WalletLogin from './WalletLogin' +import { CREATE_SAFE_EVENTS, LOAD_SAFE_EVENTS } from '@/services/analytics/events/createLoadSafe' +import Track from '@/components/common/Track' +import { trackEvent } from '@/services/analytics' + +const WelcomeLogin = () => { + const router = useRouter() + + const continueToCreation = () => { + trackEvent(CREATE_SAFE_EVENTS.CREATE_BUTTON) + router.push(AppRoutes.newSafe.create) + } + + return ( + + + + + Create Account + + + Choose how you would like to create your Safe Account + + + + + + or + + + + + + + Already have a Safe Account? + + + + Add existing one + + + + + ) +} + +export default WelcomeLogin diff --git a/src/components/welcome/WelcomeLogin/styles.module.css b/src/components/welcome/WelcomeLogin/styles.module.css new file mode 100644 index 0000000000..e9c586e392 --- /dev/null +++ b/src/components/welcome/WelcomeLogin/styles.module.css @@ -0,0 +1,18 @@ +.loginCard { + width: 100%; + height: 100%; + display: flex; + border-radius: 6px; + justify-content: center; + align-items: center; +} + +.loginContent { + display: flex; + height: 100%; + width: 320px; + gap: var(--space-1); + flex-direction: column; + align-items: center; + justify-content: center; +} diff --git a/src/components/welcome/styles.module.css b/src/components/welcome/styles.module.css index 6b2c573afb..dedc5f6fcf 100644 --- a/src/components/welcome/styles.module.css +++ b/src/components/welcome/styles.module.css @@ -1,5 +1,4 @@ -.accordion :global .MuiAccordionDetails-root > div > div:first-child, -.accordion :global .MuiAccordionSummary-expandIconWrapper { +.accordion :global .MuiAccordionDetails-root > div > div:first-child { display: none; } @@ -22,6 +21,10 @@ .sidebar { max-height: calc(100vh - var(--header-height) - var(--footer-height) - 8px); overflow-y: auto; + align-self: flex-start; + margin-top: -24px; + margin-bottom: auto; + width: 100%; } .content { @@ -33,10 +36,15 @@ display: flex; flex-direction: column; justify-content: center; + gap: var(--space-2); width: 100%; height: 100%; } +.checkIcon { + color: var(--color-static-main); +} + .createAddCard { display: flex; flex-direction: column; diff --git a/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts b/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts index c6af1f84dd..2b7cf6ec42 100644 --- a/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts +++ b/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts @@ -79,7 +79,7 @@ describe('useMPCWallet', () => { it('should have state NOT_INITIALIZED initially', () => { const { result } = renderHook(() => useMPCWallet()) expect(result.current.walletState).toBe(MPCWalletState.NOT_INITIALIZED) - expect(result.current.userInfo.email).toBeUndefined() + expect(result.current.userInfo?.email).toBeUndefined() }) describe('triggerLogin', () => { diff --git a/src/hooks/wallets/mpc/useMPCWallet.ts b/src/hooks/wallets/mpc/useMPCWallet.ts index 9fc69b4d06..9b32f78056 100644 --- a/src/hooks/wallets/mpc/useMPCWallet.ts +++ b/src/hooks/wallets/mpc/useMPCWallet.ts @@ -2,7 +2,7 @@ import { useState } from 'react' import useMPC from './useMPC' import BN from 'bn.js' import { GOOGLE_CLIENT_ID, WEB3AUTH_VERIFIER_ID } from '@/config/constants' -import { COREKIT_STATUS, getWebBrowserFactor } from '@web3auth/mpc-core-kit' +import { COREKIT_STATUS, getWebBrowserFactor, type UserInfo } from '@web3auth/mpc-core-kit' import useOnboard, { connectWallet } from '../useOnboard' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' import { SecurityQuestionRecovery } from './recovery/SecurityQuestionRecovery' @@ -21,9 +21,7 @@ export type MPCWalletHook = { walletState: MPCWalletState triggerLogin: () => Promise resetAccount: () => Promise - userInfo: { - email: string | undefined - } + userInfo: UserInfo | undefined } export const useMPCWallet = (): MPCWalletHook => { @@ -81,19 +79,19 @@ export const useMPCWallet = (): MPCWalletHook => { } } - finalizeLogin() + await finalizeLogin() } catch (error) { setWalletState(MPCWalletState.NOT_INITIALIZED) console.error(error) } } - const finalizeLogin = () => { + const finalizeLogin = async () => { if (!mpcCoreKit || !onboard) { return } if (mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN) { - connectWallet(onboard, { + await connectWallet(onboard, { autoSelect: { label: ONBOARD_MPC_MODULE_LABEL, disableModals: true, @@ -120,7 +118,7 @@ export const useMPCWallet = (): MPCWalletHook => { await deviceShareRecovery.createAndStoreDeviceFactor() } - finalizeLogin() + await finalizeLogin() } } @@ -130,8 +128,6 @@ export const useMPCWallet = (): MPCWalletHook => { recoverFactorWithPassword, resetAccount: criticalResetAccount, upsertPasswordBackup: () => Promise.resolve(), - userInfo: { - email: mpcCoreKit?.state.userInfo?.email, - }, + userInfo: mpcCoreKit?.state.userInfo, } }