Skip to content

Commit

Permalink
fix: Enable 1-click safe create for social login (#2620)
Browse files Browse the repository at this point in the history
  • Loading branch information
usame-algan authored Oct 12, 2023
1 parent cc7398b commit daf6bdb
Show file tree
Hide file tree
Showing 11 changed files with 268 additions and 121 deletions.
8 changes: 1 addition & 7 deletions src/components/common/ConnectWallet/AccountCenter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,7 @@ const AccountCenter = ({ wallet }: { wallet: ConnectedWallet }) => {
<>
<ButtonBase onClick={handleClick} aria-describedby={id} disableRipple sx={{ alignSelf: 'stretch' }}>
<Box className={css.buttonContainer}>
{wallet.label === ONBOARD_MPC_MODULE_LABEL ? (
<div className={css.socialLoginInfo}>
<SocialLoginInfo wallet={wallet} chainInfo={chainInfo} hideActions={true} />
</div>
) : (
<WalletInfo wallet={wallet} />
)}
<WalletInfo wallet={wallet} />

<Box display="flex" alignItems="center" justifyContent="flex-end" marginLeft="auto">
{open ? <ExpandLessIcon color="border" /> : <ExpandMoreIcon color="border" />}
Expand Down
28 changes: 26 additions & 2 deletions src/components/common/ConnectWallet/MPCLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@ import { useContext } from 'react'
import { MpcWalletContext } from './MPCWalletProvider'
import { PasswordRecovery } from './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 { useCurrentChain } from '@/hooks/useChains'
import chains from '@/config/chains'

const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => {
const currentChain = useCurrentChain()
const { triggerLogin, userInfo, walletState, recoverFactorWithPassword } = useContext(MpcWalletContext)

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

// TODO: Replace with feature flag from config service
const isMPCLoginEnabled = currentChain?.chainId === chains.gno
const isDisabled = loginPending || !isMPCLoginEnabled

const login = async () => {
const success = await triggerLogin()

Expand All @@ -39,7 +47,7 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => {
sx={{ px: 2, py: 1, borderWidth: '1px !important' }}
onClick={onLogin}
size="small"
disabled={loginPending}
disabled={isDisabled}
fullWidth
>
<Box width="100%" display="flex" flexDirection="row" alignItems="center" gap={1}>
Expand All @@ -64,7 +72,7 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => {
variant="outlined"
onClick={login}
size="small"
disabled={loginPending}
disabled={isDisabled}
fullWidth
sx={{ borderWidth: '1px !important' }}
>
Expand All @@ -74,6 +82,22 @@ const MPCLogin = ({ onLogin }: { onLogin?: () => void }) => {
</Button>
)}

{!isMPCLoginEnabled && (
<Typography variant="body2" color="text.secondary" display="flex" gap={1} alignItems="center">
<SvgIcon
component={InfoIcon}
inheritViewBox
color="border"
fontSize="small"
sx={{
verticalAlign: 'middle',
ml: 0.5,
}}
/>
Currently only supported on Gnosis Chain
</Typography>
)}

{walletState === MPCWalletState.MANUAL_RECOVERY && (
<PasswordRecovery recoverFactorWithPassword={recoverPassword} />
)}
Expand Down
61 changes: 28 additions & 33 deletions src/components/common/ConnectWallet/__tests__/MPCLogin.test.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { act, render, waitFor } from '@/tests/test-utils'
import { render, waitFor } from '@/tests/test-utils'
import * as useWallet from '@/hooks/wallets/useWallet'
import * as useMPCWallet from '@/hooks/wallets/mpc/useMPCWallet'
import * as chains from '@/hooks/useChains'

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'
import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'

describe('MPCLogin', () => {
beforeEach(() => {
jest.resetAllMocks()
})

it('should render continue with connected account', async () => {
it('should render continue with connected account when on gnosis chain', async () => {
const mockOnLogin = jest.fn()
const walletAddress = hexZeroPad('0x1', 20)
jest.spyOn(chains, 'useCurrentChain').mockReturnValue({ chainId: '100' } as unknown as ChainInfo)
jest.spyOn(useWallet, 'default').mockReturnValue({
address: walletAddress,
chainId: '5',
Expand Down Expand Up @@ -50,19 +54,13 @@ describe('MPCLogin', () => {
expect(mockOnLogin).toHaveBeenCalled()
})

it('should render google login button and invoke the callback on connection if no wallet is connected', async () => {
it('should render google login button and invoke the callback on connection if no wallet is connected on gnosis chain', async () => {
const mockOnLogin = jest.fn()
const walletAddress = hexZeroPad('0x1', 20)
const mockUseWallet = jest.spyOn(useWallet, 'default').mockReturnValue(null)
jest.spyOn(chains, 'useCurrentChain').mockReturnValue({ chainId: '100' } as unknown as ChainInfo)
jest.spyOn(useWallet, 'default').mockReturnValue(null)
const mockTriggerLogin = jest.fn(() => true)
const mockUseMPCWallet = jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({
userInfo: {
email: undefined,
name: undefined,
profileImage: undefined,
},
jest.spyOn(useMPCWallet, 'useMPCWallet').mockReturnValue({
triggerLogin: mockTriggerLogin,
walletState: useMPCWallet.MPCWalletState.NOT_INITIALIZED,
} as unknown as useMPCWallet.MPCWalletHook)

const result = render(
Expand All @@ -78,30 +76,27 @@ describe('MPCLogin', () => {
// 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: '[email protected]',
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,
})
})
const button = await result.findByRole('button')
button.click()

await waitFor(() => {
expect(mockOnLogin).toHaveBeenCalled()
})
})

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

const result = render(
<MpcWalletProvider>
<MPCLogin />
</MpcWalletProvider>,
)

expect(result.getByText('Currently only supported on Gnosis Chain')).toBeInTheDocument()
expect(await result.findByRole('button')).toBeDisabled()
})
})
73 changes: 39 additions & 34 deletions src/components/common/NetworkSelector/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Link from 'next/link'
import type { SelectChangeEvent } from '@mui/material'
import { MenuItem, Select, Skeleton } from '@mui/material'
import { FormControl, MenuItem, Select, Skeleton } from '@mui/material'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import useChains from '@/hooks/useChains'
import { useRouter } from 'next/router'
Expand All @@ -11,10 +11,13 @@ import type { ReactElement } from 'react'
import { useCallback } from 'react'
import { AppRoutes } from '@/config/routes'
import { trackEvent, OVERVIEW_EVENTS } from '@/services/analytics'
import useWallet from '@/hooks/wallets/useWallet'
import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module'

const keepPathRoutes = [AppRoutes.welcome, AppRoutes.newSafe.create, AppRoutes.newSafe.load]

const NetworkSelector = (): ReactElement => {
const wallet = useWallet()
const { configs } = useChains()
const chainId = useChainId()
const router = useRouter()
Expand Down Expand Up @@ -54,40 +57,42 @@ const NetworkSelector = (): ReactElement => {
}

return configs.length ? (
<Select
value={chainId}
onChange={onChange}
size="small"
className={css.select}
variant="standard"
IconComponent={ExpandMoreIcon}
MenuProps={{
sx: {
'& .MuiPaper-root': {
mt: 2,
overflow: 'auto',
<FormControl disabled={wallet?.label === ONBOARD_MPC_MODULE_LABEL}>
<Select
value={chainId}
onChange={onChange}
size="small"
className={css.select}
variant="standard"
IconComponent={ExpandMoreIcon}
MenuProps={{
sx: {
'& .MuiPaper-root': {
mt: 2,
overflow: 'auto',
},
},
},
}}
sx={{
'& .MuiSelect-select': {
py: 0,
},
'& svg path': {
fill: ({ palette }) => palette.border.main,
},
}}
>
{configs.map((chain) => {
return (
<MenuItem key={chain.chainId} value={chain.chainId}>
<Link href={getNetworkLink(chain.shortName)} passHref>
<ChainIndicator chainId={chain.chainId} inline />
</Link>
</MenuItem>
)
})}
</Select>
}}
sx={{
'& .MuiSelect-select': {
py: 0,
},
'& svg path': {
fill: ({ palette }) => palette.border.main,
},
}}
>
{configs.map((chain) => {
return (
<MenuItem key={chain.chainId} value={chain.chainId}>
<Link href={getNetworkLink(chain.shortName)} passHref>
<ChainIndicator chainId={chain.chainId} inline />
</Link>
</MenuItem>
)
})}
</Select>
</FormControl>
) : (
<Skeleton width={94} height={31} sx={{ mx: 2 }} />
)
Expand Down
4 changes: 4 additions & 0 deletions src/components/common/NetworkSelector/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@
margin-right: var(--space-2);
}

.select :global .Mui-disabled {
pointer-events: none;
}

.newChip {
font-weight: bold;
letter-spacing: -0.1px;
Expand Down
12 changes: 12 additions & 0 deletions src/components/common/WalletInfo/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,25 @@ import { useAppSelector } from '@/store'
import { selectChainById } from '@/store/chainsSlice'

import css from './styles.module.css'
import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module'
import SocialLoginInfo from '@/components/common/SocialLoginInfo'

export const UNKNOWN_CHAIN_NAME = 'Unknown'

const WalletInfo = ({ wallet }: { wallet: ConnectedWallet }): ReactElement => {
const walletChain = useAppSelector((state) => selectChainById(state, wallet.chainId))
const prefix = walletChain?.shortName

const isSocialLogin = wallet.label === ONBOARD_MPC_MODULE_LABEL

if (isSocialLogin) {
return (
<div className={css.socialLoginInfo}>
<SocialLoginInfo wallet={wallet} chainInfo={walletChain} hideActions={true} />
</div>
)
}

return (
<Box className={css.container}>
<Box className={css.imageContainer}>
Expand Down
4 changes: 4 additions & 0 deletions src/components/common/WalletInfo/styles.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,8 @@
.walletName {
display: none;
}

.socialLoginInfo > div > div {
display: none;
}
}
5 changes: 5 additions & 0 deletions src/components/new-safe/create/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import CreateSafeInfos from '@/components/new-safe/create/CreateSafeInfos'
import { type ReactElement, useMemo, useState } from 'react'
import ExternalLink from '@/components/common/ExternalLink'
import { HelpCenterArticle } from '@/config/constants'
import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module'

export type NewSafeFormData = {
name: string
Expand Down Expand Up @@ -163,6 +164,9 @@ const CreateSafe = () => {
saltNonce: Date.now(),
}

// Jump to review screen when using social login
const initialStep = wallet?.label === ONBOARD_MPC_MODULE_LABEL ? 2 : 0

const onClose = () => {
router.push(AppRoutes.welcome)
}
Expand All @@ -178,6 +182,7 @@ const CreateSafe = () => {
<Grid item xs={12} md={8} order={[1, null, 0]}>
<CardStepper
initialData={initialData}
initialStep={initialStep}
onClose={onClose}
steps={CreateSafeSteps}
eventCategory={CREATE_SAFE_CATEGORY}
Expand Down
41 changes: 41 additions & 0 deletions src/components/new-safe/create/steps/ReviewStep/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'

import { render } from '@/tests/test-utils'
import { NetworkFee } from '@/components/new-safe/create/steps/ReviewStep/index'
import * as useWallet from '@/hooks/wallets/useWallet'
import { type ConnectedWallet } from '@/services/onboard'
import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/module'

const mockChainInfo = {
chainId: '100',
l2: false,
nativeCurrency: {
symbol: 'ETH',
},
} as ChainInfo

describe('NetworkFee', () => {
it('should display the total fee if not social login', () => {
jest.spyOn(useWallet, 'default').mockReturnValue({ label: 'MetaMask' } as unknown as ConnectedWallet)
const mockTotalFee = '0.0123'
const result = render(<NetworkFee totalFee={mockTotalFee} chain={mockChainInfo} willRelay={true} />)

expect(result.getByText(`≈ ${mockTotalFee} ${mockChainInfo.nativeCurrency.symbol}`)).toBeInTheDocument()
})

it('displays a sponsored by message for social login', () => {
jest.spyOn(useWallet, 'default').mockReturnValue({ label: ONBOARD_MPC_MODULE_LABEL } as unknown as ConnectedWallet)
const result = render(<NetworkFee totalFee="0" chain={mockChainInfo} willRelay={true} />)

expect(result.getByText(/Your account is sponsored by Gnosis Chain/)).toBeInTheDocument()
})

it('displays an error message for social login if there are no relays left', () => {
jest.spyOn(useWallet, 'default').mockReturnValue({ label: ONBOARD_MPC_MODULE_LABEL } as unknown as ConnectedWallet)
const result = render(<NetworkFee totalFee="0" chain={mockChainInfo} willRelay={false} />)

expect(
result.getByText(/You have used up your 5 free transactions per hour. Please try again later/),
).toBeInTheDocument()
})
})
Loading

0 comments on commit daf6bdb

Please sign in to comment.