diff --git a/cypress/e2e/pages/create_wallet.pages.js b/cypress/e2e/pages/create_wallet.pages.js index 57ad95b318..5f1b3eefb3 100644 --- a/cypress/e2e/pages/create_wallet.pages.js +++ b/cypress/e2e/pages/create_wallet.pages.js @@ -1,6 +1,6 @@ import * as constants from '../../support/constants' -const newAccountBtnStr = 'Create new Account' +const newAccountBtnStr = 'Continue with E2E Wallet' const nameInput = 'input[name="name"]' const selectNetworkBtn = '[data-cy="create-safe-select-network"]' diff --git a/cypress/e2e/pages/import_export.pages.js b/cypress/e2e/pages/import_export.pages.js index bbd76cdcbe..328d04fd07 100644 --- a/cypress/e2e/pages/import_export.pages.js +++ b/cypress/e2e/pages/import_export.pages.js @@ -24,7 +24,7 @@ export function clickOnImportBtn() { } export function clickOnImportBtnDataImportModal() { - cy.contains(dataImportModalStr).parent().contains('button', 'Import').click() + cy.contains('button', 'Import').click() } export function uploadFile(filePath) { @@ -43,6 +43,10 @@ export function clickOnImportedSafe(safe) { cy.contains(safe).click() } +export function clickOnOpenSafeListSidebar() { + cy.contains('My Safe Accounts').click() +} + export function clickOnClosePushNotificationsBanner() { cy.waitForSelector(() => { return cy.get('h6').contains(enablePushNotificationsStr).siblings('.MuiButtonBase-root').click({ force: true }) diff --git a/cypress/e2e/pages/load_safe.pages.js b/cypress/e2e/pages/load_safe.pages.js index 3d36669e48..d1d02d9edb 100644 --- a/cypress/e2e/pages/load_safe.pages.js +++ b/cypress/e2e/pages/load_safe.pages.js @@ -1,6 +1,6 @@ import * as constants from '../../support/constants' -const addExistingAccountBtnStr = 'Add existing Account' +const addExistingAccountBtnStr = 'Add existing one' const contactStr = 'Name, address & network' const invalidAddressFormatErrorMsg = 'Invalid address format' @@ -16,7 +16,7 @@ const ownersConfirmationsStr = 'Owners and confirmations' const transactionStr = 'Transactions' export function openLoadSafeForm() { - cy.contains('button', addExistingAccountBtnStr).click() + cy.contains('a', addExistingAccountBtnStr).click() cy.contains(contactStr) } diff --git a/cypress/e2e/smoke/import_export_data.cy.js b/cypress/e2e/smoke/import_export_data.cy.js index 55ea6fedae..3097d8bafb 100644 --- a/cypress/e2e/smoke/import_export_data.cy.js +++ b/cypress/e2e/smoke/import_export_data.cy.js @@ -6,19 +6,19 @@ import * as constants from '../../support/constants' describe('Import Export Data', () => { before(() => { cy.clearLocalStorage() - cy.visit(constants.welcomeUrl) + cy.visit(constants.dataSettingsUrl) main.acceptCookies() - file.verifyImportBtnIsVisible() }) it('Uploads test file and access safe', () => { const filePath = '../fixtures/data_import.json' const safe = 'safe 1 goerli' - file.clickOnImportBtn() file.uploadFile(filePath) file.verifyImportModalData() file.clickOnImportBtnDataImportModal() + cy.visit(constants.welcomeUrl) + file.clickOnOpenSafeListSidebar() file.clickOnImportedSafe(safe) file.clickOnClosePushNotificationsBanner() }) diff --git a/cypress/support/constants.js b/cypress/support/constants.js index 7368cb64e6..9a20c0c7cb 100644 --- a/cypress/support/constants.js +++ b/cypress/support/constants.js @@ -36,6 +36,7 @@ export const requestPermissionsUrl = '/request-permissions' export const getPermissionsUrl = '/get-permissions' export const appSettingsUrl = '/settings/safe-apps' export const setupUrl = '/settings/setup?safe=' +export const dataSettingsUrl = '/settings/data' export const invalidAppUrl = 'https://my-invalid-custom-app.com/manifest.json' export const validAppUrlJson = 'https://my-valid-custom-app.com/manifest.json' export const validAppUrl = 'https://my-valid-custom-app.com' diff --git a/public/images/common/lock.svg b/public/images/common/lock.svg new file mode 100644 index 0000000000..9535a642e0 --- /dev/null +++ b/public/images/common/lock.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/common/ConnectWallet/AccountCenter.tsx b/src/components/common/ConnectWallet/AccountCenter.tsx index 03a0aab974..6f3ac1d140 100644 --- a/src/components/common/ConnectWallet/AccountCenter.tsx +++ b/src/components/common/ConnectWallet/AccountCenter.tsx @@ -1,6 +1,6 @@ import type { MouseEvent } from 'react' import { useState } from 'react' -import { Box, Button, ButtonBase, Paper, Popover, Typography } from '@mui/material' +import { Box, Button, 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' @@ -8,11 +8,13 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import useOnboard, { switchWallet } from '@/hooks/wallets/useOnboard' import { useAppSelector } from '@/store' import { selectChainById } from '@/store/chainsSlice' -import Identicon from '@/components/common/Identicon' import ChainSwitcher from '../ChainSwitcher' import useAddressBook from '@/hooks/useAddressBook' import { type ConnectedWallet } from '@/hooks/wallets/useOnboard' -import WalletInfo, { UNKNOWN_CHAIN_NAME } from '../WalletInfo' +import WalletInfo from '../WalletInfo' +import ChainIndicator from '@/components/common/ChainIndicator' +import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' +import SocialLoginInfo from '@/components/common/SocialLoginInfo' const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { const [anchorEl, setAnchorEl] = useState(null) @@ -53,7 +55,13 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { <> - + {wallet.label === ONBOARD_MPC_MODULE_LABEL ? ( +
+ +
+ ) : ( + + )} {open ? : } @@ -77,31 +85,21 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => { sx={{ marginTop: 1 }} > - - - - {addressBook[wallet.address] || wallet.ens} - - - - - - - - - Wallet - {wallet.label} - - - Connected network - {chainInfo?.chainName || UNKNOWN_CHAIN_NAME} + + + + {wallet.label === ONBOARD_MPC_MODULE_LABEL ? ( + + ) : ( + + )} diff --git a/src/components/common/ConnectWallet/MPCLogin.tsx b/src/components/common/ConnectWallet/MPCLogin.tsx index e55a32647b..03ead96b6e 100644 --- a/src/components/common/ConnectWallet/MPCLogin.tsx +++ b/src/components/common/ConnectWallet/MPCLogin.tsx @@ -1,32 +1,34 @@ import { MPCWalletState } from '@/hooks/wallets/mpc/useMPCWallet' import { Box, Button, SvgIcon, Typography } from '@mui/material' -import { useContext, useEffect, useState } from 'react' +import { useContext } 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 { triggerLogin, userInfo, walletState, recoverFactorWithPassword } = useContext(MpcWalletContext) const wallet = useWallet() - - const [loginTriggered, setLoginTriggered] = useState(false) + const loginPending = walletState === MPCWalletState.AUTHENTICATING const login = async () => { - setLoginTriggered(true) - await triggerLogin() + const success = await triggerLogin() + + if (success) { + onLogin?.() + } } - // 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() + const recoverPassword = async (password: string, storeDeviceFactor: boolean) => { + const success = await recoverFactorWithPassword(password, storeDeviceFactor) + + if (success) { + onLogin?.() } - }, [loginTriggered, onLogin, wallet]) + } return ( <> @@ -34,20 +36,13 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { <> ) : ( - + + + or + + diff --git a/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx index 41952da2b2..a98afa4d4e 100644 --- a/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx +++ b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx @@ -54,7 +54,7 @@ describe('MPCLogin', () => { const mockOnLogin = jest.fn() const walletAddress = hexZeroPad('0x1', 20) const mockUseWallet = jest.spyOn(useWallet, 'default').mockReturnValue(null) - const mockTriggerLogin = jest.fn() + const mockTriggerLogin = jest.fn(() => true) const mockUseMPCWallet = jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ userInfo: { email: undefined, diff --git a/src/components/common/ConnectWallet/styles.module.css b/src/components/common/ConnectWallet/styles.module.css index aa127889c6..b6aa736f13 100644 --- a/src/components/common/ConnectWallet/styles.module.css +++ b/src/components/common/ConnectWallet/styles.module.css @@ -12,11 +12,11 @@ .popoverContainer { padding: var(--space-2); - width: 250px; + width: 300px; display: flex; flex-direction: column; align-items: center; - gap: var(--space-2); + gap: var(--space-1); border: 1px solid var(--color-border-light); } @@ -66,9 +66,30 @@ gap: var(--space-2); } +.loginButton { + 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; +} + @media (max-width: 599.95px) { - .buttonContainer { - transform: scale(0.8); + .socialLoginInfo > div > div { + display: none; } .notConnected { diff --git a/src/components/common/ConnectWallet/useConnectWallet.ts b/src/components/common/ConnectWallet/useConnectWallet.ts index 9dc10e7930..b3f832229e 100644 --- a/src/components/common/ConnectWallet/useConnectWallet.ts +++ b/src/components/common/ConnectWallet/useConnectWallet.ts @@ -1,19 +1,17 @@ -import { useMemo } from 'react' +import { useCallback } from 'react' import useOnboard, { connectWallet } from '@/hooks/wallets/useOnboard' import { OVERVIEW_EVENTS, trackEvent } from '@/services/analytics' -const useConnectWallet = (): (() => void) => { +const useConnectWallet = () => { const onboard = useOnboard() - return useMemo(() => { + return useCallback(() => { if (!onboard) { - return () => {} + return Promise.resolve(undefined) } - return () => { - trackEvent(OVERVIEW_EVENTS.OPEN_ONBOARD) - connectWallet(onboard) - } + trackEvent(OVERVIEW_EVENTS.OPEN_ONBOARD) + return connectWallet(onboard) }, [onboard]) } diff --git a/src/components/common/SocialLoginInfo/index.tsx b/src/components/common/SocialLoginInfo/index.tsx new file mode 100644 index 0000000000..8615a8a3be --- /dev/null +++ b/src/components/common/SocialLoginInfo/index.tsx @@ -0,0 +1,52 @@ +import { Box, Typography } from '@mui/material' +import css from './styles.module.css' +import { useContext } from 'react' +import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { type ConnectedWallet } from '@/services/onboard' +import { MpcWalletContext } from '@/components/common/ConnectWallet/MPCWalletProvider' +import CopyAddressButton from '@/components/common/CopyAddressButton' +import ExplorerButton from '@/components/common/ExplorerButton' +import { getBlockExplorerLink } from '@/utils/chains' +import { useAppSelector } from '@/store' +import { selectSettings } from '@/store/settingsSlice' + +const SocialLoginInfo = ({ + wallet, + chainInfo, + hideActions = false, +}: { + wallet: ConnectedWallet + chainInfo?: ChainInfo + hideActions?: boolean +}) => { + const { userInfo } = useContext(MpcWalletContext) + const prefix = chainInfo?.shortName + const link = chainInfo ? getBlockExplorerLink(chainInfo, wallet.address) : undefined + const settings = useAppSelector(selectSettings) + + if (!userInfo) return <> + + return ( + + Profile Image +
+ + {userInfo.name} + + + {userInfo.email} + +
+ {!hideActions && ( +
+ + + + +
+ )} +
+ ) +} + +export default SocialLoginInfo diff --git a/src/components/common/SocialLoginInfo/styles.module.css b/src/components/common/SocialLoginInfo/styles.module.css new file mode 100644 index 0000000000..6b694addb6 --- /dev/null +++ b/src/components/common/SocialLoginInfo/styles.module.css @@ -0,0 +1,23 @@ +.profileImg { + border-radius: var(--space-2); + width: 32px; + height: 32px; +} + +.profileData { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; +} + +.text { + font-size: 12px; + line-height: 14px; +} + +.actionButtons { + display: flex; + justify-self: flex-end; + margin-left: auto; +} diff --git a/src/components/welcome/WelcomeLogin/WalletLogin.tsx b/src/components/welcome/WelcomeLogin/WalletLogin.tsx index 758a3b23c7..451bf5d502 100644 --- a/src/components/welcome/WelcomeLogin/WalletLogin.tsx +++ b/src/components/welcome/WelcomeLogin/WalletLogin.tsx @@ -3,26 +3,16 @@ 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 WalletLogin = ({ onLogin }: { onLogin: () => void }) => { const wallet = useWallet() const connectWallet = useConnectWallet() - const [loginTriggered, setLoginTriggered] = useState(false) - const login = async () => { - setLoginTriggered(true) await connectWallet() + onLogin() } - // 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 ( ) diff --git a/src/components/welcome/styles.module.css b/src/components/welcome/styles.module.css index cb91034710..2b8c377b76 100644 --- a/src/components/welcome/styles.module.css +++ b/src/components/welcome/styles.module.css @@ -28,6 +28,7 @@ .bulletList { list-style: none; margin-bottom: auto; + padding: 0; } .bulletList li { diff --git a/src/hooks/wallets/mpc/useMPCWallet.ts b/src/hooks/wallets/mpc/useMPCWallet.ts index c8c9a2d9b0..caa61fe945 100644 --- a/src/hooks/wallets/mpc/useMPCWallet.ts +++ b/src/hooks/wallets/mpc/useMPCWallet.ts @@ -17,9 +17,9 @@ export enum MPCWalletState { export type MPCWalletHook = { upsertPasswordBackup: (password: string) => Promise - recoverFactorWithPassword: (password: string, storeDeviceShare: boolean) => Promise + recoverFactorWithPassword: (password: string, storeDeviceShare: boolean) => Promise walletState: MPCWalletState - triggerLogin: () => Promise + triggerLogin: () => Promise resetAccount: () => Promise userInfo: UserInfo | undefined } @@ -74,15 +74,17 @@ export const useMPCWallet = (): MPCWalletHook => { const securityQuestions = new SecurityQuestionRecovery(mpcCoreKit) if (securityQuestions.isEnabled()) { setWalletState(MPCWalletState.MANUAL_RECOVERY) - return + return false } } } await finalizeLogin() + return mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN } catch (error) { setWalletState(MPCWalletState.NOT_INITIALIZED) console.error(error) + return false } } @@ -123,6 +125,8 @@ export const useMPCWallet = (): MPCWalletHook => { await finalizeLogin() } + + return mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN } return {