diff --git a/src/components/common/ConnectWallet/MPCLogin.tsx b/src/components/common/ConnectWallet/MPCLogin.tsx index 687d3446dd..32d50e47bd 100644 --- a/src/components/common/ConnectWallet/MPCLogin.tsx +++ b/src/components/common/ConnectWallet/MPCLogin.tsx @@ -1,6 +1,6 @@ import { MPCWalletState } from '@/hooks/wallets/mpc/useMPCWallet' import { Box, Button, SvgIcon, Typography } from '@mui/material' -import { useContext, useMemo } from 'react' +import { useCallback, useContext, useMemo } from 'react' import { MpcWalletContext } from './MPCWalletProvider' import { PasswordRecovery } from './PasswordRecovery' import GoogleLogo from '@/public/images/welcome/logo-google.svg' @@ -16,6 +16,8 @@ import { isSocialWalletEnabled } from '@/hooks/wallets/wallets' import { isSocialLoginWallet } from '@/services/mpc/module' import { CGW_NAMES } from '@/hooks/wallets/consts' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { TxModalContext } from '@/components/tx-flow' +import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' export const _getSupportedChains = (chains: ChainInfo[]) => { return chains @@ -37,7 +39,9 @@ const useIsSocialWalletEnabled = () => { } const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { - const { triggerLogin, userInfo, walletState, recoverFactorWithPassword } = useContext(MpcWalletContext) + const { triggerLogin, userInfo, walletState, setWalletState, recoverFactorWithPassword } = + useContext(MpcWalletContext) + const { setTxFlow } = useContext(TxModalContext) const wallet = useWallet() const loginPending = walletState === MPCWalletState.AUTHENTICATING @@ -48,21 +52,33 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { const isDisabled = loginPending || !isMPCLoginEnabled const login = async () => { - const success = await triggerLogin() + const status = await triggerLogin() - if (success) { + if (status === COREKIT_STATUS.LOGGED_IN) { onLogin?.() } - } - - const recoverPassword = async (password: string, storeDeviceFactor: boolean) => { - const success = await recoverFactorWithPassword(password, storeDeviceFactor) - if (success) { - onLogin?.() + if (status === COREKIT_STATUS.REQUIRED_SHARE) { + setTxFlow( + , + () => setWalletState(MPCWalletState.NOT_INITIALIZED), + false, + ) } } + const recoverPassword = useCallback( + async (password: string, storeDeviceFactor: boolean) => { + const success = await recoverFactorWithPassword(password, storeDeviceFactor) + + if (success) { + onLogin?.() + setTxFlow(undefined) + } + }, + [onLogin, recoverFactorWithPassword, setTxFlow], + ) + const isSocialLogin = isSocialLoginWallet(wallet?.label) return ( @@ -128,10 +144,6 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => { Currently only supported on {supportedChains.join(', ')} )} - - {walletState === MPCWalletState.MANUAL_RECOVERY && ( - - )} ) } diff --git a/src/components/common/ConnectWallet/MPCWalletProvider.tsx b/src/components/common/ConnectWallet/MPCWalletProvider.tsx index 06162aab82..27872ceddc 100644 --- a/src/components/common/ConnectWallet/MPCWalletProvider.tsx +++ b/src/components/common/ConnectWallet/MPCWalletProvider.tsx @@ -1,9 +1,11 @@ -import { useMPCWallet, MPCWalletState, type MPCWalletHook } from '@/hooks/wallets/mpc/useMPCWallet' +import { type MPCWalletHook, MPCWalletState, useMPCWallet } from '@/hooks/wallets/mpc/useMPCWallet' import { createContext, type ReactElement } from 'react' +import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' export const MpcWalletContext = createContext({ walletState: MPCWalletState.NOT_INITIALIZED, - triggerLogin: () => Promise.resolve(false), + setWalletState: () => {}, + triggerLogin: () => Promise.resolve(COREKIT_STATUS.NOT_INITIALIZED), resetAccount: () => Promise.resolve(), upsertPasswordBackup: () => Promise.resolve(), recoverFactorWithPassword: () => Promise.resolve(false), diff --git a/src/components/common/ConnectWallet/PasswordRecovery.tsx b/src/components/common/ConnectWallet/PasswordRecovery.tsx index 72ed86a3ac..053a533a27 100644 --- a/src/components/common/ConnectWallet/PasswordRecovery.tsx +++ b/src/components/common/ConnectWallet/PasswordRecovery.tsx @@ -1,70 +1,106 @@ import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet' -import { VisibilityOff, Visibility } from '@mui/icons-material' import { - DialogContent, Typography, - TextField, - IconButton, FormControlLabel, Checkbox, Button, Box, + Divider, + Grid, + LinearProgress, + FormControl, } from '@mui/material' import { useState } from 'react' -import ModalDialog from '../ModalDialog' import Track from '../Track' +import { FormProvider, useForm } from 'react-hook-form' +import PasswordInput from '@/components/settings/SignerAccountMFA/PasswordInput' + +type PasswordFormData = { + password: string +} export const PasswordRecovery = ({ recoverFactorWithPassword, + onSuccess, }: { recoverFactorWithPassword: (password: string, storeDeviceFactor: boolean) => Promise + onSuccess: (() => void) | undefined }) => { - const [showPassword, setShowPassword] = useState(false) - const [recoveryPassword, setRecoveryPassword] = useState('') const [storeDeviceFactor, setStoreDeviceFactor] = useState(false) + + const formMethods = useForm({ + mode: 'all', + defaultValues: { + password: '', + }, + }) + + const { handleSubmit, formState, setError } = formMethods + + const onSubmit = async (data: PasswordFormData) => { + try { + await recoverFactorWithPassword(data.password, storeDeviceFactor) + onSuccess?.() + } catch (e) { + setError('password', { type: 'custom', message: 'Incorrect password' }) + } + } + + const isDisabled = formState.isSubmitting + return ( - - - - - This browser is not registered with your Account yet. Please enter your recovery password to restore access - to this Account. - - - { - setRecoveryPassword(event.target.value) - }} - InputProps={{ - endAdornment: ( - setShowPassword((prev) => !prev)} - edge="end" - > - {showPassword ? : } - - ), - }} - /> - setStoreDeviceFactor((prev) => !prev)} />} - label="Do not ask again on this device" - /> - - - - - - - + +
+ + + + Verify your account + + + + + + Enter security password + + + This browser is not registered with your Account yet. Please enter your recovery password to restore + access to this Account. + + + + + + + + setStoreDeviceFactor((prev) => !prev)} /> + } + label="Do not ask again on this device" + /> + + + + + + + + + + +
+
) } diff --git a/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx index 8c33d79441..249e8da820 100644 --- a/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx +++ b/src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx @@ -9,6 +9,7 @@ import { type EIP1193Provider } from '@web3-onboard/common' import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module' import { MpcWalletProvider } from '../MPCWalletProvider' import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk' +import { COREKIT_STATUS } from '@web3auth/mpc-core-kit' describe('MPCLogin', () => { beforeEach(() => { @@ -62,7 +63,7 @@ describe('MPCLogin', () => { .spyOn(chains, 'useCurrentChain') .mockReturnValue({ chainId: '100', disabledWallets: [] } as unknown as ChainInfo) jest.spyOn(useWallet, 'default').mockReturnValue(null) - const mockTriggerLogin = jest.fn(() => true) + const mockTriggerLogin = jest.fn(() => COREKIT_STATUS.LOGGED_IN) jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({ triggerLogin: mockTriggerLogin, } as unknown as useMPCWallet.MPCWalletHook) diff --git a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx index 1fbbd76d89..34041dbdfb 100644 --- a/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx +++ b/src/components/common/EthHashInfo/SrcEthHashInfo/index.tsx @@ -71,7 +71,7 @@ const SrcEthHashInfo = ({ {name && ( - + {name} )} diff --git a/src/components/tx-flow/index.tsx b/src/components/tx-flow/index.tsx index 0cf7a993aa..0f4471b5de 100644 --- a/src/components/tx-flow/index.tsx +++ b/src/components/tx-flow/index.tsx @@ -17,6 +17,7 @@ export const TxModalContext = createContext({ setFullWidth: noop, }) +// TODO: Rename TxModalProvider, setTxFlow, TxModalDialog to not contain Tx since it can be used for any type of modal as a global provider export const TxModalProvider = ({ children }: { children: ReactNode }): ReactElement => { const [txFlow, setFlow] = useState(undefined) const [shouldWarn, setShouldWarn] = useState(true) diff --git a/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts b/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts index 6a134ea0b7..53a0b60985 100644 --- a/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts +++ b/src/hooks/wallets/mpc/__tests__/useMPCWallet.test.ts @@ -113,8 +113,9 @@ describe('useMPCWallet', () => { ) const { result } = renderHook(() => useMPCWallet()) + let status: Promise act(() => { - result.current.triggerLogin() + status = result.current.triggerLogin() }) // While the login resolves we are in Authenticating state @@ -128,6 +129,7 @@ describe('useMPCWallet', () => { // We should be logged in and onboard should get connected await waitFor(() => { + expect(status).resolves.toEqual(COREKIT_STATUS.LOGGED_IN) expect(result.current.walletState === MPCWalletState.READY) expect(connectWalletSpy).toBeCalledWith(expect.anything(), { autoSelect: { @@ -158,8 +160,9 @@ describe('useMPCWallet', () => { const { result } = renderHook(() => useMPCWallet()) + let status: Promise act(() => { - result.current.triggerLogin() + status = result.current.triggerLogin() }) // While the login resolves we are in Authenticating state @@ -173,6 +176,7 @@ describe('useMPCWallet', () => { // We should be logged in and onboard should get connected await waitFor(() => { + expect(status).resolves.toEqual(COREKIT_STATUS.LOGGED_IN) expect(result.current.walletState === MPCWalletState.READY) expect(connectWalletSpy).toBeCalledWith(expect.anything(), { autoSelect: { @@ -201,8 +205,9 @@ describe('useMPCWallet', () => { const { result } = renderHook(() => useMPCWallet()) + let status: Promise act(() => { - result.current.triggerLogin() + status = result.current.triggerLogin() }) // While the login resolves we are in Authenticating state @@ -216,6 +221,7 @@ describe('useMPCWallet', () => { // A missing second factor should result in manual recovery state await waitFor(() => { + expect(status).resolves.toEqual(COREKIT_STATUS.REQUIRED_SHARE) expect(result.current.walletState === MPCWalletState.MANUAL_RECOVERY) expect(connectWalletSpy).not.toBeCalled() }) diff --git a/src/hooks/wallets/mpc/useMPCWallet.ts b/src/hooks/wallets/mpc/useMPCWallet.ts index 753b199a48..06328e4e82 100644 --- a/src/hooks/wallets/mpc/useMPCWallet.ts +++ b/src/hooks/wallets/mpc/useMPCWallet.ts @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { type Dispatch, type SetStateAction, useState } from 'react' import useMPC from './useMPC' import BN from 'bn.js' import { GOOGLE_CLIENT_ID, WEB3AUTH_VERIFIER_ID } from '@/config/constants' @@ -21,7 +21,8 @@ export type MPCWalletHook = { upsertPasswordBackup: (password: string) => Promise recoverFactorWithPassword: (password: string, storeDeviceShare: boolean) => Promise walletState: MPCWalletState - triggerLogin: () => Promise + setWalletState: Dispatch> + triggerLogin: () => Promise resetAccount: () => Promise userInfo: UserInfo | undefined exportPk: (password: string) => Promise @@ -78,17 +79,17 @@ export const useMPCWallet = (): MPCWalletHook => { if (securityQuestions.isEnabled()) { trackEvent(MPC_WALLET_EVENTS.MANUAL_RECOVERY) setWalletState(MPCWalletState.MANUAL_RECOVERY) - return false + return mpcCoreKit.status } } } await finalizeLogin() - return mpcCoreKit.status === COREKIT_STATUS.LOGGED_IN + return mpcCoreKit.status } catch (error) { setWalletState(MPCWalletState.NOT_INITIALIZED) console.error(error) - return false + return mpcCoreKit.status } } @@ -154,6 +155,7 @@ export const useMPCWallet = (): MPCWalletHook => { return { triggerLogin, walletState, + setWalletState, recoverFactorWithPassword, resetAccount: criticalResetAccount, upsertPasswordBackup: () => Promise.resolve(),