Skip to content

Commit

Permalink
fix: Add feature flag for social login (#2770)
Browse files Browse the repository at this point in the history
* fix: Add feature flag for social login

* fix: Show simple Connect button in case feature is disabled

* fix: Remove redirects on the welcome page
  • Loading branch information
usame-algan authored Nov 8, 2023
1 parent 8e2d324 commit ea3763b
Show file tree
Hide file tree
Showing 12 changed files with 116 additions and 48 deletions.
22 changes: 19 additions & 3 deletions src/components/common/ConnectWallet/ConnectionCenter.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { Popover, ButtonBase, Typography, Paper } from '@mui/material'
import ConnectWalletButton from '@/components/common/ConnectWallet/ConnectWalletButton'
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'
import madProps from '@/utils/mad-props'
import { Popover, ButtonBase, Typography, Paper, Box } from '@mui/material'
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'
import ExpandLessIcon from '@mui/icons-material/ExpandLess'
import classnames from 'classnames'
Expand All @@ -9,7 +13,7 @@ import WalletDetails from '@/components/common/ConnectWallet/WalletDetails'

import css from '@/components/common/ConnectWallet/styles.module.css'

const ConnectionCenter = (): ReactElement => {
export const ConnectionCenter = ({ isSocialLoginEnabled }: { isSocialLoginEnabled: boolean }): ReactElement => {
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null)
const open = !!anchorEl

Expand All @@ -23,6 +27,14 @@ const ConnectionCenter = (): ReactElement => {

const ExpandIcon = open ? ExpandLessIcon : ExpandMoreIcon

if (!isSocialLoginEnabled) {
return (
<Box className={css.buttonContainer}>
<ConnectWalletButton />
</Box>
)
}

return (
<>
<ButtonBase disableRipple onClick={handleClick} className={css.buttonContainer}>
Expand Down Expand Up @@ -60,4 +72,8 @@ const ConnectionCenter = (): ReactElement => {
)
}

export default ConnectionCenter
const useIsSocialLoginEnabled = () => useHasFeature(FEATURES.SOCIAL_LOGIN)

export default madProps(ConnectionCenter, {
isSocialLoginEnabled: useIsSocialLoginEnabled,
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { ConnectionCenter } from '@/components/common/ConnectWallet/ConnectionCenter'
import { render } from '@/tests/test-utils'

describe('ConnectionCenter', () => {
it('displays a Connect wallet button if the social login feature is enabled', () => {
const { getByText, queryByText } = render(<ConnectionCenter isSocialLoginEnabled={true} />)

expect(getByText('Connect wallet')).toBeInTheDocument()
expect(queryByText('Connect')).not.toBeInTheDocument()
})

it('displays the ConnectWalletButton if the social login feature is disabled', () => {
const { getByText, queryByText } = render(<ConnectionCenter isSocialLoginEnabled={false} />)

expect(queryByText('Connect wallet')).not.toBeInTheDocument()
expect(getByText('Connect')).toBeInTheDocument()
})
})
2 changes: 1 addition & 1 deletion src/components/common/SocialSigner/PasswordRecovery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const PasswordRecovery = ({
onSuccess,
}: {
recoverFactorWithPassword: (password: string, storeDeviceFactor: boolean) => Promise<void>
onSuccess: (() => void) | undefined
onSuccess?: (() => void) | undefined
}) => {
const [storeDeviceFactor, setStoreDeviceFactor] = useState(false)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { act, render, waitFor } from '@/tests/test-utils'

import { SocialSigner, _getSupportedChains } from '@/components/common/SocialSigner'
import { ONBOARD_MPC_MODULE_LABEL } from '@/services/mpc/SocialLoginModule'
import { COREKIT_STATUS, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit'
import { COREKIT_STATUS, type UserInfo, type Web3AuthMPCCoreKit } from '@web3auth/mpc-core-kit'
import SocialWalletService from '@/services/mpc/SocialWalletService'
import { TxModalProvider } from '@/components/tx-flow'
import { fireEvent } from '@testing-library/react'
Expand Down Expand Up @@ -51,7 +51,7 @@ describe('SocialSignerLogin', () => {
expect(mockOnLogin).toHaveBeenCalled()
})

it('should render google login button and invoke the callback on connection if no wallet is connected on gnosis chain', async () => {
it('should render google login button if no wallet is connected on gnosis chain', async () => {
const mockOnLogin = jest.fn()

const result = render(
Expand All @@ -70,18 +70,30 @@ describe('SocialSignerLogin', () => {
expect(result.findByText('Continue with Google')).resolves.toBeDefined()
expect(await result.findByRole('button')).toBeEnabled()
})
})

// We do not automatically invoke the callback as the user did not actively connect
expect(mockOnLogin).not.toHaveBeenCalled()
it('should display a Continue as button and call onLogin when clicked', () => {
const mockOnLogin = jest.fn()
mockSocialWalletService.loginAndCreate = jest.fn(() => Promise.resolve(COREKIT_STATUS.LOGGED_IN))

const button = await result.findByRole('button')
act(() => {
button.click()
})
const result = render(
<TxModalProvider>
<SocialSigner
socialWalletService={mockSocialWalletService}
wallet={mockWallet}
supportedChains={['Goerli']}
isMPCLoginEnabled={true}
onLogin={mockOnLogin}
/>
</TxModalProvider>,
)

await waitFor(async () => {
expect(mockOnLogin).toHaveBeenCalled()
})
expect(result.getByText('Continue as Test Testermann')).toBeInTheDocument()

const button = result.getByRole('button')
button.click()

expect(mockOnLogin).toHaveBeenCalled()
})

it('should disable the Google Login button with a message when not on gnosis chain', async () => {
Expand All @@ -98,10 +110,11 @@ describe('SocialSignerLogin', () => {
expect(await result.findByRole('button')).toBeDisabled()
})

it('should display Password Recovery form and call onLogin if password recovery succeeds', async () => {
it('should display Password Recovery form and display a Continue as button when login succeeds', async () => {
const mockOnLogin = jest.fn()
mockSocialWalletService.loginAndCreate = jest.fn(() => Promise.resolve(COREKIT_STATUS.REQUIRED_SHARE))
mockSocialWalletService.getUserInfo = jest.fn(undefined)
mockSocialWalletService.recoverAccountWithPassword = jest.fn(() => Promise.resolve(true))

const result = render(
<TxModalProvider>
Expand Down Expand Up @@ -140,8 +153,17 @@ describe('SocialSignerLogin', () => {
submitButton.click()
})

mockSocialWalletService.getUserInfo = jest.fn(
() =>
({
email: '[email protected]',
name: 'Test Testermann',
profileImage: 'test.testermann.local/profile.png',
} as unknown as UserInfo),
)

await waitFor(() => {
expect(mockOnLogin).toHaveBeenCalled()
expect(result.getByText('Continue as Test Testermann')).toBeInTheDocument()
})
})

Expand Down
10 changes: 2 additions & 8 deletions src/components/common/SocialSigner/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,10 @@ export const SocialSigner = ({
const success = await socialWalletService.recoverAccountWithPassword(password, storeDeviceFactor)

if (success) {
onLogin?.()
setTxFlow(undefined)
}
},
[onLogin, setTxFlow, socialWalletService],
[setTxFlow, socialWalletService],
)

const login = async () => {
Expand All @@ -86,19 +85,14 @@ export const SocialSigner = ({
const status = await socialWalletService.loginAndCreate()

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

if (status === COREKIT_STATUS.REQUIRED_SHARE) {
onRequirePassword?.()

setTxFlow(
<PasswordRecovery recoverFactorWithPassword={recoverPassword} onSuccess={onLogin} />,
() => setLoginPending(false),
false,
)
setTxFlow(<PasswordRecovery recoverFactorWithPassword={recoverPassword} />, () => setLoginPending(false), false)
return
}
} catch (err) {
Expand Down
17 changes: 8 additions & 9 deletions src/components/welcome/WelcomeLogin/WalletLogin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,6 @@ const WalletLogin = ({ onLogin }: { onLogin: () => void }) => {
const wallet = useWallet()
const connectWallet = useConnectWallet()

const login = async () => {
const walletState = await connectWallet()

if (walletState && walletState.length > 0) {
onLogin()
}
}

const isSocialLogin = isSocialLoginWallet(wallet?.label)

if (wallet !== null && !isSocialLogin) {
Expand Down Expand Up @@ -55,7 +47,14 @@ const WalletLogin = ({ onLogin }: { onLogin: () => void }) => {
}

return (
<Button onClick={login} sx={{ minHeight: '42px' }} variant="contained" size="small" disableElevation fullWidth>
<Button
onClick={connectWallet}
sx={{ minHeight: '42px' }}
variant="contained"
size="small"
disableElevation
fullWidth
>
Connect wallet
</Button>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('WalletLogin', () => {
expect(mockOnLogin).toHaveBeenCalled()
})

it('should render connect wallet and invoke the callback on connection if no wallet is connected', async () => {
it('should render connect wallet if no wallet is connected', async () => {
const mockOnLogin = jest.fn()
const walletAddress = hexZeroPad('0x1', 20)
const mockUseWallet = jest.spyOn(useWallet, 'default').mockReturnValue(null)
Expand All @@ -49,7 +49,7 @@ describe('WalletLogin', () => {
expect(result.findByText('Connect wallet')).resolves.toBeDefined()
})

// We do not automatically invoke the callback as the user did not actively connect
// We do not automatically invoke the callback
expect(mockOnLogin).not.toHaveBeenCalled()

await act(async () => {
Expand All @@ -65,7 +65,7 @@ describe('WalletLogin', () => {
})

await waitFor(() => {
expect(mockOnLogin).toHaveBeenCalled()
expect(result.getByText('Connect wallet')).toBeInTheDocument()
})
})

Expand Down
19 changes: 13 additions & 6 deletions src/components/welcome/WelcomeLogin/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import SocialSigner from '@/components/common/SocialSigner'
import { AppRoutes } from '@/config/routes'
import { useHasFeature } from '@/hooks/useChains'
import { FEATURES } from '@/utils/chains'
import { Paper, SvgIcon, Typography, Divider, Link, Box } from '@mui/material'
import SafeLogo from '@/public/images/logo-text.svg'
import css from './styles.module.css'
Expand All @@ -11,6 +13,7 @@ import { trackEvent } from '@/services/analytics'

const WelcomeLogin = () => {
const router = useRouter()
const isSocialLoginEnabled = useHasFeature(FEATURES.SOCIAL_LOGIN)

const continueToCreation = () => {
trackEvent(CREATE_SAFE_EVENTS.OPEN_SAFE_CREATION)
Expand All @@ -29,13 +32,17 @@ const WelcomeLogin = () => {
</Typography>
<WalletLogin onLogin={continueToCreation} />

<Divider sx={{ mt: 2, mb: 2, width: '100%' }}>
<Typography color="text.secondary" fontWeight={700} variant="overline">
or
</Typography>
</Divider>
{isSocialLoginEnabled && (
<>
<Divider sx={{ mt: 2, mb: 2, width: '100%' }}>
<Typography color="text.secondary" fontWeight={700} variant="overline">
or
</Typography>
</Divider>

<SocialSigner onLogin={continueToCreation} />
<SocialSigner onLogin={continueToCreation} />
</>
)}

<Typography mt={2} textAlign="center">
Already have a Safe Account?
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/wallets/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const WALLET_MODULES: { [key in WALLET_KEYS]: (chain: ChainInfo) => WalletInit }
[WALLET_KEYS.WALLETCONNECT_V2]: (chain) => walletConnectV2(chain),
[WALLET_KEYS.COINBASE]: () => coinbaseModule({ darkMode: prefersDarkMode() }),
[WALLET_KEYS.PAIRING]: () => pairingModule(),
[WALLET_KEYS.SOCIAL]: () => MpcModule(),
[WALLET_KEYS.SOCIAL]: (chain) => MpcModule(chain),
[WALLET_KEYS.LEDGER]: () => ledgerModule(),
[WALLET_KEYS.TREZOR]: () => trezorModule({ appUrl: TREZOR_APP_URL, email: TREZOR_EMAIL }),
[WALLET_KEYS.KEYSTONE]: () => keystoneModule(),
Expand Down
6 changes: 5 additions & 1 deletion src/services/mpc/SocialLoginModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { _getMPCCoreKitInstance } from '@/hooks/wallets/mpc/useMPC'
import { getSocialWalletService } from '@/hooks/wallets/mpc/useSocialWallet'
import { getWeb3ReadOnly } from '@/hooks/wallets/web3'
import * as PasswordRecoveryModal from '@/services/mpc/PasswordRecoveryModal'
import { FEATURES, hasFeature } from '@/utils/chains'
import type { ChainInfo } from '@safe-global/safe-gateway-typescript-sdk'
import { type WalletInit, ProviderRpcError } from '@web3-onboard/common'
import { type EIP1193Provider } from '@web3-onboard/core'
import { COREKIT_STATUS } from '@web3auth/mpc-core-kit'
Expand Down Expand Up @@ -40,7 +42,9 @@ const getConnectedAccounts = async () => {
*
* @returns Custom Onboard MpcModule
*/
function MpcModule(): WalletInit {
function MpcModule(chain: ChainInfo): WalletInit {
if (!hasFeature(chain, FEATURES.SOCIAL_LOGIN)) return () => null

return () => {
return {
label: ONBOARD_MPC_MODULE_LABEL,
Expand Down
13 changes: 10 additions & 3 deletions src/services/mpc/__tests__/module.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { chainBuilder } from '@/tests/builders/chains'
import { FEATURES } from '@/utils/chains'
import MpcModule, { ONBOARD_MPC_MODULE_LABEL } from '../SocialLoginModule'
import { type WalletModule } from '@web3-onboard/common'

import * as web3 from '@/hooks/wallets/web3'
import * as useMPC from '@/hooks/wallets/mpc/useMPC'
import { hexZeroPad } from 'ethers/lib/utils'

const mockChain = chainBuilder()
// @ts-expect-error - we are using a local FEATURES enum
.with({ features: [FEATURES.SOCIAL_LOGIN] })
.build()

describe('MPC Onboard module', () => {
it('should return correct metadata', async () => {
const mpcModule = MpcModule()({
const mpcModule = MpcModule(mockChain)({
device: {
browser: {
name: 'Firefox',
Expand Down Expand Up @@ -42,7 +49,7 @@ describe('MPC Onboard module', () => {
} as any
})

const mpcModule = MpcModule()({
const mpcModule = MpcModule(mockChain)({
device: {
browser: {
name: 'Firefox',
Expand Down Expand Up @@ -93,7 +100,7 @@ describe('MPC Onboard module', () => {
} as any
})

const mpcModule = MpcModule()({
const mpcModule = MpcModule(mockChain)({
device: {
browser: {
name: 'Firefox',
Expand Down
1 change: 1 addition & 0 deletions src/utils/chains.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export enum FEATURES {
RISK_MITIGATION = 'RISK_MITIGATION',
PUSH_NOTIFICATIONS = 'PUSH_NOTIFICATIONS',
NATIVE_WALLETCONNECT = 'NATIVE_WALLETCONNECT',
SOCIAL_LOGIN = 'SOCIAL_LOGIN',
}

export const hasFeature = (chain: ChainInfo, feature: FEATURES): boolean => {
Expand Down

0 comments on commit ea3763b

Please sign in to comment.