Skip to content

Commit

Permalink
refactor: Change MPCLogin name and use madProps, adjust tests
Browse files Browse the repository at this point in the history
  • Loading branch information
usame-algan committed Oct 27, 2023
1 parent 186a3d4 commit 6db406c
Show file tree
Hide file tree
Showing 11 changed files with 165 additions and 174 deletions.
4 changes: 2 additions & 2 deletions src/components/common/ConnectWallet/WalletDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Divider, Typography } from '@mui/material'
import type { ReactElement } from 'react'

import LockIcon from '@/public/images/common/lock.svg'
import MPCLogin from './MPCLogin'
import SocialSigner from '@/components/common/SocialSigner'
import WalletLogin from '@/components/welcome/WelcomeLogin/WalletLogin'

const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement => {
Expand All @@ -18,7 +18,7 @@ const WalletDetails = ({ onConnect }: { onConnect: () => void }): ReactElement =
</Typography>
</Divider>

<MPCLogin />
<SocialSigner />
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
FormControl,
} from '@mui/material'
import { useState } from 'react'
import Track from '../Track'
import Track from '@/components/common/Track'
import { FormProvider, useForm } from 'react-hook-form'
import PasswordInput from '@/components/settings/SecurityLogin/SocialSignerMFA/PasswordInput'
import ErrorMessage from '@/components/tx/ErrorMessage'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { fireEvent, render } from '@/tests/test-utils'
import { PasswordRecovery } from '@/components/common/SocialSigner/PasswordRecovery'
import { act, waitFor } from '@testing-library/react'

describe('PasswordRecovery', () => {
it('displays an error if password is wrong', async () => {
const mockRecoverWithPassword = jest.fn(() => Promise.reject())
const mockOnSuccess = jest.fn()

const { getByText, getByLabelText } = render(
<PasswordRecovery recoverFactorWithPassword={mockRecoverWithPassword} onSuccess={mockOnSuccess} />,
)

const passwordField = getByLabelText('Recovery password')
const submitButton = getByText('Submit')

act(() => {
fireEvent.change(passwordField, { target: { value: 'somethingwrong' } })
submitButton.click()
})

await waitFor(() => {
expect(mockOnSuccess).not.toHaveBeenCalled()
expect(getByText('Incorrect password')).toBeInTheDocument()
})
})

it('calls onSuccess if password is correct', async () => {
const mockRecoverWithPassword = jest.fn(() => Promise.resolve())
const mockOnSuccess = jest.fn()

const { getByText, getByLabelText } = render(
<PasswordRecovery recoverFactorWithPassword={mockRecoverWithPassword} onSuccess={mockOnSuccess} />,
)

const passwordField = getByLabelText('Recovery password')
const submitButton = getByText('Submit')

act(() => {
fireEvent.change(passwordField, { target: { value: 'somethingCorrect' } })
submitButton.click()
})

await waitFor(() => {
expect(mockOnSuccess).toHaveBeenCalled()
})
})
})
Original file line number Diff line number Diff line change
@@ -1,49 +1,46 @@
import { act, fireEvent, render, waitFor } from '@/tests/test-utils'
import * as useWallet from '@/hooks/wallets/useWallet'
import * as chains from '@/hooks/useChains'
import { act, render, waitFor } from '@/tests/test-utils'

import MPCLogin, { _getSupportedChains } from '../MPCLogin'
import { SocialSigner, _getSupportedChains } from '@/components/common/SocialSigner'
import { hexZeroPad } from '@ethersproject/bytes'
import { type EIP1193Provider } from '@web3-onboard/common'
import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule'
import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { COREKIT_STATUS, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit'
import SocialWalletService from '@/services/mpc/SocialWalletService'
import { getSocialWalletService, __setSocialWalletService } from '@/hooks/wallets/mpc/useSocialWallet'
import type TestSocialWalletService from '@/services/mpc/__mocks__/SocialWalletService'
import { MPCWalletState } from '@/services/mpc/interfaces'
import { TxModalProvider } from '@/components/tx-flow'
import { fireEvent } from '@testing-library/react'
import { type ISocialWalletService } from '@/services/mpc/interfaces'

jest.mock('@/services/mpc/SocialWalletService')

describe('MPCLogin', () => {
const mockWallet = {
address: hexZeroPad('0x1', 20),
chainId: '5',
label: ONBOARD_MPC_MODULE_LABEL,
provider: {} as unknown as EIP1193Provider,
}

describe('SocialSignerLogin', () => {
let mockSocialWalletService: ISocialWalletService

beforeEach(() => {
jest.resetAllMocks()
// set the mock social wallet service into our external store
__setSocialWalletService(new SocialWalletService({} as unknown as Web3AuthMPCCoreKit))
const mockEthereumChain = { chainId: '1', chainName: 'Ethereum', disabledWallets: ['socialLogin'] } as ChainInfo
const mockGoerliChain = { chainId: '5', chainName: 'Goerli', disabledWallets: ['TallyHo'] } as ChainInfo
jest.spyOn(chains, 'default').mockReturnValue({ configs: [mockEthereumChain, mockGoerliChain] })

mockSocialWalletService = new SocialWalletService({} as unknown as Web3AuthMPCCoreKit)
})

it('should render continue with connected account when on gnosis chain', async () => {
const mockOnLogin = jest.fn()
// Mock a successful login
getSocialWalletService()?.setWalletState(MPCWalletState.READY)
const walletAddress = hexZeroPad('0x1', 20)
jest
.spyOn(chains, 'useCurrentChain')
.mockReturnValue({ chainId: '100', disabledWallets: [] } as unknown as ChainInfo)
jest.spyOn(useWallet, 'default').mockReturnValue({
address: walletAddress,
chainId: '5',
label: ONBOARD_MPC_MODULE_LABEL,
provider: {} as unknown as EIP1193Provider,
})

const result = render(
<TxModalProvider>
<MPCLogin onLogin={mockOnLogin} />
<SocialSigner
socialWalletService={mockSocialWalletService}
wallet={mockWallet}
supportedChains={['Goerli']}
isMPCLoginEnabled={true}
onLogin={mockOnLogin}
/>
</TxModalProvider>,
)

Expand All @@ -61,15 +58,17 @@ describe('MPCLogin', () => {
})

it('should render google login button and invoke the callback on connection if no wallet is connected on gnosis chain', async () => {
jest
.spyOn(chains, 'useCurrentChain')
.mockReturnValue({ chainId: '100', disabledWallets: [] } as unknown as ChainInfo)
jest.spyOn(useWallet, 'default').mockReturnValue(null)

const mockOnLogin = jest.fn()

const result = render(
<TxModalProvider>
<MPCLogin onLogin={mockOnLogin} />
<SocialSigner
socialWalletService={mockSocialWalletService}
wallet={null}
supportedChains={['Goerli']}
isMPCLoginEnabled={true}
onLogin={mockOnLogin}
/>
</TxModalProvider>,
)

Expand All @@ -92,27 +91,33 @@ describe('MPCLogin', () => {
})

it('should disable the Google Login button with a message when not on gnosis chain', async () => {
jest
.spyOn(chains, 'useCurrentChain')
.mockReturnValue({ chainId: '1', disabledWallets: ['socialLogin'] } as unknown as ChainInfo)

const result = render(<MPCLogin />)
const result = render(
<SocialSigner
socialWalletService={mockSocialWalletService}
wallet={mockWallet}
supportedChains={['Goerli']}
isMPCLoginEnabled={false}
/>,
)

expect(result.getByText('Currently only supported on Goerli')).toBeInTheDocument()
expect(await result.findByRole('button')).toBeDisabled()
})

it('should display Password Recovery and recover with correct password', async () => {
;(getSocialWalletService() as TestSocialWalletService).__setPostLoginState(COREKIT_STATUS.REQUIRED_SHARE)
it('should display Password Recovery form and call onLogin if password recovery succeeds', async () => {
const mockOnLogin = jest.fn()
jest
.spyOn(chains, 'useCurrentChain')
.mockReturnValue({ chainId: '100', disabledWallets: [] } as unknown as ChainInfo)
jest.spyOn(useWallet, 'default').mockReturnValue(null)
mockSocialWalletService.loginAndCreate = jest.fn(() => Promise.resolve(COREKIT_STATUS.REQUIRED_SHARE))
mockSocialWalletService.getUserInfo = jest.fn(undefined)

const result = render(
<TxModalProvider>
<MPCLogin onLogin={mockOnLogin} />
<SocialSigner
socialWalletService={mockSocialWalletService}
wallet={mockWallet}
supportedChains={['Goerli']}
isMPCLoginEnabled={true}
onLogin={mockOnLogin}
/>
</TxModalProvider>,
)

Expand All @@ -124,6 +129,7 @@ describe('MPCLogin', () => {
expect(mockOnLogin).not.toHaveBeenCalled()

const button = await result.findByRole('button')

act(() => {
button.click()
})
Expand All @@ -145,50 +151,6 @@ describe('MPCLogin', () => {
})
})

it('should display Password Recovery and not recover with wrong password', async () => {
;(getSocialWalletService() as TestSocialWalletService).__setPostLoginState(COREKIT_STATUS.REQUIRED_SHARE)
const mockOnLogin = jest.fn()
jest
.spyOn(chains, 'useCurrentChain')
.mockReturnValue({ chainId: '100', disabledWallets: [] } as unknown as ChainInfo)
jest.spyOn(useWallet, 'default').mockReturnValue(null)

const result = render(
<TxModalProvider>
<MPCLogin onLogin={mockOnLogin} />
</TxModalProvider>,
)

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()

const button = await result.findByRole('button')
act(() => {
button.click()
})

await waitFor(() => {
expect(result.findByText('Enter security password')).resolves.toBeDefined()
})

const passwordField = await result.findByLabelText('Recovery password')
const submitButton = await result.findByText('Submit')

act(() => {
fireEvent.change(passwordField, { target: { value: 'Invalid password' } })
submitButton.click()
})

await waitFor(() => {
expect(mockOnLogin).not.toHaveBeenCalled()
expect(result.findByText('Incorrect Password')).resolves.toBeDefined()
})
})

describe('getSupportedChains', () => {
it('returns chain names where social login is enabled', () => {
const mockEthereumChain = { chainId: '1', chainName: 'Ethereum', disabledWallets: ['socialLogin'] } as ChainInfo
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Box, Button, SvgIcon, Typography } from '@mui/material'
import { useCallback, useContext, useMemo } from 'react'
import { PasswordRecovery } from './PasswordRecovery'
import { useCallback, useContext, useMemo, useState } from 'react'
import { PasswordRecovery } from '@/components/common/SocialSigner/PasswordRecovery'
import GoogleLogo from '@/public/images/welcome/logo-google.svg'
import InfoIcon from '@/public/images/notifications/info.svg'

import css from './styles.module.css'
import useWallet from '@/hooks/wallets/useWallet'
import Track from '../Track'
import Track from '@/components/common/Track'
import { CREATE_SAFE_EVENTS } from '@/services/analytics'
import { MPC_WALLET_EVENTS } from '@/services/analytics/events/mpcWallet'
import useChains, { useCurrentChain } from '@/hooks/useChains'
Expand All @@ -17,7 +17,7 @@ import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { TxModalContext } from '@/components/tx-flow'
import { COREKIT_STATUS } from '@web3auth/mpc-core-kit'
import useSocialWallet from '@/hooks/wallets/mpc/useSocialWallet'
import { MPCWalletState } from '@/services/mpc/interfaces'
import madProps from '@/utils/mad-props'

export const _getSupportedChains = (chains: ChainInfo[]) => {
return chains
Expand All @@ -38,17 +38,24 @@ const useIsSocialWalletEnabled = () => {
return isSocialWalletEnabled(currentChain)
}

const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => {
const socialWalletService = useSocialWallet()
const userInfo = socialWalletService?.getUserInfo()
const { setTxFlow } = useContext(TxModalContext)

const wallet = useWallet()
const loginPending = socialWalletService?.walletState === MPCWalletState.AUTHENTICATING

const supportedChains = useGetSupportedChains()
const isMPCLoginEnabled = useIsSocialWalletEnabled()
type SocialSignerLoginProps = {
socialWalletService: ReturnType<typeof useSocialWallet>
wallet: ReturnType<typeof useWallet>
supportedChains: ReturnType<typeof useGetSupportedChains>
isMPCLoginEnabled: ReturnType<typeof useIsSocialWalletEnabled>
onLogin?: () => void
}

export const SocialSigner = ({
socialWalletService,
wallet,
supportedChains,
isMPCLoginEnabled,
onLogin,
}: SocialSignerLoginProps) => {
const [loginPending, setLoginPending] = useState<boolean>(false)
const { setTxFlow } = useContext(TxModalContext)
const userInfo = socialWalletService?.getUserInfo()
const isDisabled = loginPending || !isMPCLoginEnabled

const recoverPassword = useCallback(
Expand All @@ -68,16 +75,25 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => {
const login = async () => {
if (!socialWalletService) return

setLoginPending(true)

const status = await socialWalletService.loginAndCreate()

if (status === COREKIT_STATUS.LOGGED_IN) {
onLogin?.()
setLoginPending(false)
}

if (status === COREKIT_STATUS.REQUIRED_SHARE) {
setTxFlow(
<PasswordRecovery recoverFactorWithPassword={recoverPassword} onSuccess={onLogin} />,
() => socialWalletService.setWalletState(MPCWalletState.NOT_INITIALIZED),
<PasswordRecovery
recoverFactorWithPassword={recoverPassword}
onSuccess={() => {
onLogin?.()
setLoginPending(false)
}}
/>,
() => {},
false,
)
}
Expand Down Expand Up @@ -152,4 +168,9 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => {
)
}

export default MPCLogin
export default madProps(SocialSigner, {
socialWalletService: useSocialWallet,
wallet: useWallet,
supportedChains: useGetSupportedChains,
isMPCLoginEnabled: useIsSocialWalletEnabled,
})
16 changes: 16 additions & 0 deletions src/components/common/SocialSigner/styles.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
.profileImg {
border-radius: var(--space-2);
width: 32px;
height: 32px;
}

.profileData {
display: flex;
flex-direction: column;
align-items: flex-start;
}

.loginError {
width: 100%;
margin: 0;
}
Loading

0 comments on commit 6db406c

Please sign in to comment.