From 93bba5d58dacdd482ba2d9e0e4e109df3d049b22 Mon Sep 17 00:00:00 2001 From: "Mateo \"Kuruk\" Miccino" Date: Thu, 7 Nov 2024 18:59:46 -0300 Subject: [PATCH 1/6] feat: add multiple target configs feat: add deep links refactor: unify connectionOptions instead of social/web3 --- src/components/Connection/Connection.spec.tsx | 91 ++++-------- src/components/Connection/Connection.tsx | 108 ++++++-------- src/components/Connection/Connection.types.ts | 18 ++- src/components/Connection/constants.ts | 7 +- src/components/Pages/LoginPage/LoginPage.tsx | 19 +-- src/components/Pages/LoginPage/utils.ts | 2 +- .../Pages/RequestPage/RequestPage.tsx | 6 +- src/hooks/targetConfig.spec.ts | 76 ++++++++++ src/hooks/targetConfig.ts | 140 ++++++++++++++++-- 9 files changed, 305 insertions(+), 162 deletions(-) create mode 100644 src/hooks/targetConfig.spec.ts diff --git a/src/components/Connection/Connection.spec.tsx b/src/components/Connection/Connection.spec.tsx index f2d7e23..98e41bf 100644 --- a/src/components/Connection/Connection.spec.tsx +++ b/src/components/Connection/Connection.spec.tsx @@ -1,12 +1,6 @@ import { act, fireEvent, render } from '@testing-library/react' import { Connection } from './Connection' -import { - SHOW_MORE_BUTTON_TEST_ID, - SOCIAL_PRIMARY_TEST_ID, - SOCIAL_SECONDARY_TEST_ID, - WEB3_PRIMARY_TEST_ID, - WEB3_SECONDARY_TEST_ID -} from './constants' +import { EXTRA_TEST_ID, PRIMARY_TEST_ID, SHOW_MORE_BUTTON_TEST_ID } from './constants' import { ConnectionOptionType, ConnectionProps } from './Connection.types' function renderConnection(props: Partial) { @@ -36,40 +30,28 @@ function renderConnection(props: Partial) { let screen: ReturnType describe('when rendering the component', () => { - let socialOptions: ConnectionProps['socialOptions'] | undefined - let web3Options: ConnectionProps['web3Options'] | undefined + let connectionOptions: ConnectionProps['connectionOptions'] let onConnect: jest.Mock - describe('and there are no social options', () => { - beforeEach(() => { - socialOptions = undefined - screen = renderConnection({ socialOptions }) - }) - - it('should not render the primary social option', () => { - const { queryByTestId } = screen - expect(queryByTestId(SOCIAL_PRIMARY_TEST_ID)).not.toBeInTheDocument() - }) - }) - describe('and there are social options', () => { beforeEach(() => { onConnect = jest.fn() - socialOptions = { + connectionOptions = { primary: ConnectionOptionType.GOOGLE, - secondary: [ConnectionOptionType.APPLE, ConnectionOptionType.X, ConnectionOptionType.DISCORD] + secondary: ConnectionOptionType.APPLE, + extraOptions: [ConnectionOptionType.X, ConnectionOptionType.DISCORD] } - screen = renderConnection({ socialOptions, onConnect }) + screen = renderConnection({ connectionOptions, onConnect }) }) it('should render the primary social option', () => { const { getByTestId } = screen - expect(getByTestId(SOCIAL_PRIMARY_TEST_ID)).toBeInTheDocument() + expect(getByTestId(PRIMARY_TEST_ID)).toBeInTheDocument() }) it('should call the onConnect method prop when clicking the button', () => { const { getByTestId } = screen - fireEvent.click(getByTestId(`${SOCIAL_PRIMARY_TEST_ID}-${ConnectionOptionType.GOOGLE}-button`)) + fireEvent.click(getByTestId(`${PRIMARY_TEST_ID}-${ConnectionOptionType.GOOGLE}-button`)) expect(onConnect).toHaveBeenCalledWith(ConnectionOptionType.GOOGLE) }) @@ -78,8 +60,8 @@ describe('when rendering the component', () => { act(() => { fireEvent.click(getByTestId(SHOW_MORE_BUTTON_TEST_ID)) }) - socialOptions?.secondary.forEach(option => { - expect(getByTestId(`${SOCIAL_SECONDARY_TEST_ID}-${option}-button`)).toBeInTheDocument() + connectionOptions?.extraOptions?.forEach(option => { + expect(getByTestId(`${EXTRA_TEST_ID}-${option}-button`)).toBeInTheDocument() }) }) @@ -88,25 +70,13 @@ describe('when rendering the component', () => { act(() => { fireEvent.click(getByTestId(SHOW_MORE_BUTTON_TEST_ID)) }) - socialOptions?.secondary.forEach(option => { - fireEvent.click(getByTestId(`${SOCIAL_SECONDARY_TEST_ID}-${option}-button`)) + connectionOptions?.extraOptions?.forEach(option => { + fireEvent.click(getByTestId(`${EXTRA_TEST_ID}-${option}-button`)) expect(onConnect).toHaveBeenCalledWith(option) }) }) }) - describe('and there are no primary web3 options', () => { - beforeEach(() => { - socialOptions = undefined - screen = renderConnection({ socialOptions }) - }) - - it('should not render the primary web3 option', () => { - const { queryByTestId } = screen - expect(queryByTestId(WEB3_PRIMARY_TEST_ID)).not.toBeInTheDocument() - }) - }) - describe('and the user has metamask installed', () => { let oldEthereum: typeof window.ethereum @@ -116,12 +86,12 @@ describe('when rendering the component', () => { ...window.ethereum, isMetaMask: true } - web3Options = { + connectionOptions = { primary: ConnectionOptionType.METAMASK, - secondary: [ConnectionOptionType.FORTMATIC, ConnectionOptionType.WALLET_CONNECT, ConnectionOptionType.COINBASE] + extraOptions: [ConnectionOptionType.FORTMATIC, ConnectionOptionType.WALLET_CONNECT, ConnectionOptionType.COINBASE] } onConnect = jest.fn() - screen = renderConnection({ web3Options, onConnect }) + screen = renderConnection({ connectionOptions, onConnect }) }) afterEach(() => { @@ -130,7 +100,7 @@ describe('when rendering the component', () => { it('should call the onConnect method prop when clicking the button', () => { const { getByTestId } = screen - fireEvent.click(getByTestId(`${WEB3_PRIMARY_TEST_ID}-${ConnectionOptionType.METAMASK}-button`)) + fireEvent.click(getByTestId(`${PRIMARY_TEST_ID}-${ConnectionOptionType.METAMASK}-button`)) expect(onConnect).toHaveBeenCalledWith(ConnectionOptionType.METAMASK) }) }) @@ -141,12 +111,12 @@ describe('when rendering the component', () => { beforeEach(() => { oldEthereum = window.ethereum window.ethereum = undefined - web3Options = { + connectionOptions = { primary: ConnectionOptionType.METAMASK, - secondary: [ConnectionOptionType.FORTMATIC, ConnectionOptionType.WALLET_CONNECT, ConnectionOptionType.COINBASE] + extraOptions: [ConnectionOptionType.FORTMATIC, ConnectionOptionType.WALLET_CONNECT, ConnectionOptionType.COINBASE] } onConnect = jest.fn() - screen = renderConnection({ web3Options, onConnect }) + screen = renderConnection({ connectionOptions, onConnect }) }) afterEach(() => { @@ -155,7 +125,7 @@ describe('when rendering the component', () => { it('should not call the onConnect method prop when clicking the button', () => { const { getByTestId } = screen - fireEvent.click(getByTestId(`${WEB3_PRIMARY_TEST_ID}-${ConnectionOptionType.METAMASK}-button`)) + fireEvent.click(getByTestId(`${PRIMARY_TEST_ID}-${ConnectionOptionType.METAMASK}-button`)) expect(onConnect).not.toHaveBeenCalled() }) }) @@ -163,16 +133,16 @@ describe('when rendering the component', () => { describe('and there are web3 options', () => { beforeEach(() => { onConnect = jest.fn() - web3Options = { + connectionOptions = { primary: ConnectionOptionType.METAMASK, - secondary: [ConnectionOptionType.FORTMATIC, ConnectionOptionType.WALLET_CONNECT, ConnectionOptionType.COINBASE] + extraOptions: [ConnectionOptionType.FORTMATIC, ConnectionOptionType.WALLET_CONNECT, ConnectionOptionType.COINBASE] } - screen = renderConnection({ web3Options, onConnect }) + screen = renderConnection({ connectionOptions, onConnect }) }) it('should render the primary social option', () => { const { getByTestId } = screen - expect(getByTestId(WEB3_PRIMARY_TEST_ID)).toBeInTheDocument() + expect(getByTestId(PRIMARY_TEST_ID)).toBeInTheDocument() }) it('should render all the secondary options', () => { @@ -180,8 +150,8 @@ describe('when rendering the component', () => { act(() => { fireEvent.click(getByTestId(SHOW_MORE_BUTTON_TEST_ID)) }) - web3Options?.secondary.forEach(option => { - expect(getByTestId(`${WEB3_SECONDARY_TEST_ID}-${option}-button`)).toBeInTheDocument() + connectionOptions?.extraOptions?.forEach(option => { + expect(getByTestId(`${EXTRA_TEST_ID}-${option}-button`)).toBeInTheDocument() }) }) @@ -190,8 +160,8 @@ describe('when rendering the component', () => { act(() => { fireEvent.click(getByTestId(SHOW_MORE_BUTTON_TEST_ID)) }) - web3Options?.secondary.forEach(option => { - fireEvent.click(getByTestId(`${WEB3_SECONDARY_TEST_ID}-${option}-button`)) + connectionOptions?.extraOptions?.forEach(option => { + fireEvent.click(getByTestId(`${EXTRA_TEST_ID}-${option}-button`)) expect(onConnect).toHaveBeenCalledWith(option) }) }) @@ -199,9 +169,8 @@ describe('when rendering the component', () => { describe('and there are no web3 nor social secondary options', () => { beforeEach(() => { - socialOptions = undefined - web3Options = undefined - screen = renderConnection({ socialOptions, web3Options }) + connectionOptions = undefined + screen = renderConnection({ connectionOptions }) }) it('should not render the more options button', () => { diff --git a/src/components/Connection/Connection.tsx b/src/components/Connection/Connection.tsx index a9307be..208eb2c 100644 --- a/src/components/Connection/Connection.tsx +++ b/src/components/Connection/Connection.tsx @@ -4,14 +4,14 @@ import Icon from 'semantic-ui-react/dist/commonjs/elements/Icon/Icon' import { Button } from 'decentraland-ui/dist/components/Button/Button' import logoSrc from '../../assets/images/logo.svg' import { ConnectionOption } from './ConnectionOption' +import { EXTRA_TEST_ID, PRIMARY_TEST_ID, SECONDARY_TEST_ID, SHOW_MORE_BUTTON_TEST_ID } from './constants' import { - SHOW_MORE_BUTTON_TEST_ID, - SOCIAL_PRIMARY_TEST_ID, - SOCIAL_SECONDARY_TEST_ID, - WEB3_PRIMARY_TEST_ID, - WEB3_SECONDARY_TEST_ID -} from './constants' -import { ConnectionOptionType, ConnectionProps, MetamaskEthereumWindow, connectionOptionTitles } from './Connection.types' + ConnectionOptionType, + ConnectionProps, + MetamaskEthereumWindow, + connectionOptionTitles, + isMagicConnection +} from './Connection.types' import styles from './Connection.module.css' const Primary = ({ @@ -91,7 +91,7 @@ const defaultProps = { } export const Connection = (props: ConnectionProps): JSX.Element => { - const { i18n = defaultProps.i18n, onConnect, onLearnMore, socialOptions, web3Options, className, loadingOption } = props + const { i18n = defaultProps.i18n, onConnect, onLearnMore, connectionOptions, className, loadingOption } = props const [showMore, setShowMore] = useState(false) const handleShowMore = useCallback(() => { @@ -99,8 +99,39 @@ export const Connection = (props: ConnectionProps): JSX.Element => { }, [showMore]) const isMetamaskAvailable = (window.ethereum as MetamaskEthereumWindow)?.isMetaMask - const hasSocialSecondaryOptions = socialOptions && socialOptions.secondary && socialOptions.secondary.length > 0 - const hasWeb3SecondaryOptions = web3Options && web3Options.secondary && web3Options.secondary.length > 0 + const hasExtraOptions = connectionOptions?.extraOptions && connectionOptions.extraOptions.length > 0 + + const renderPrimary = (option: ConnectionOptionType, testId: string) => ( + + {i18n.socialMessage(
)} + onLearnMore(option)}> + Learn More + + + ) : ( + i18n.web3Message(element => ( + onLearnMore(option)}> + {element} + + )) + ) + } + > + <>{isMagicConnection(option) ? i18n.accessWith(option) : i18n.connectWith(option)} + + ) return (
@@ -108,48 +139,12 @@ export const Connection = (props: ConnectionProps): JSX.Element => {

{i18n.title}

{i18n.subtitle}

- {socialOptions ? ( - - {i18n.socialMessage(
)} - onLearnMore(socialOptions?.primary)}> - Learn More - - - } - > - <>{i18n.accessWith(socialOptions?.primary)} - - ) : null} - {web3Options ? ( - ( - onLearnMore(web3Options.primary)}> - {element} - - ))} - > - <>{i18n.connectWith(web3Options?.primary)} - - ) : null} + {connectionOptions && renderPrimary(connectionOptions.primary, PRIMARY_TEST_ID)} + {connectionOptions?.secondary && renderPrimary(connectionOptions.secondary, SECONDARY_TEST_ID)}
- {(hasWeb3SecondaryOptions || hasSocialSecondaryOptions) && ( + {hasExtraOptions && ( )} - {showMore && hasSocialSecondaryOptions && ( + {showMore && hasExtraOptions && connectionOptions.extraOptions && ( )} - {showMore && hasWeb3SecondaryOptions && ( - - )}
) diff --git a/src/components/Connection/Connection.types.ts b/src/components/Connection/Connection.types.ts index c06a5b3..340956d 100644 --- a/src/components/Connection/Connection.types.ts +++ b/src/components/Connection/Connection.types.ts @@ -28,6 +28,15 @@ export const connectionOptionTitles: { [key in ConnectionOptionType]: string } = [ConnectionOptionType.X]: 'X' } +export function isMagicConnection(option: ConnectionOptionType): boolean { + return ( + option === ConnectionOptionType.X || + option === ConnectionOptionType.APPLE || + option === ConnectionOptionType.GOOGLE || + option === ConnectionOptionType.DISCORD + ) +} + export type MetamaskEthereumWindow = typeof window.ethereum & { isMetaMask?: boolean } export type ConnectionI18N = { @@ -42,13 +51,10 @@ export type ConnectionI18N = { export type ConnectionProps = { i18n?: ConnectionI18N - socialOptions?: { - primary: ConnectionOptionType - secondary: ConnectionOptionType[] - } - web3Options?: { + connectionOptions?: { primary: ConnectionOptionType - secondary: ConnectionOptionType[] + secondary?: ConnectionOptionType + extraOptions?: ConnectionOptionType[] } className?: string loadingOption?: ConnectionOptionType diff --git a/src/components/Connection/constants.ts b/src/components/Connection/constants.ts index c2a4772..9fb637a 100644 --- a/src/components/Connection/constants.ts +++ b/src/components/Connection/constants.ts @@ -1,5 +1,4 @@ -export const SOCIAL_PRIMARY_TEST_ID = 'social-primary-test-id' -export const SOCIAL_SECONDARY_TEST_ID = 'social-secondary-test-id' -export const WEB3_PRIMARY_TEST_ID = 'web3-primary-test-id' -export const WEB3_SECONDARY_TEST_ID = 'web3-secondary-test-id' +export const PRIMARY_TEST_ID = 'primary-test-id' +export const SECONDARY_TEST_ID = 'secondary-test-id' +export const EXTRA_TEST_ID = 'extra-options-test-id' export const SHOW_MORE_BUTTON_TEST_ID = 'show-more-button-test-id' diff --git a/src/components/Pages/LoginPage/LoginPage.tsx b/src/components/Pages/LoginPage/LoginPage.tsx index 8b0fa2f..ac363ad 100644 --- a/src/components/Pages/LoginPage/LoginPage.tsx +++ b/src/components/Pages/LoginPage/LoginPage.tsx @@ -24,7 +24,7 @@ import { ConnectionModal, ConnectionModalState } from '../../ConnectionModal' import { FeatureFlagsContext } from '../../FeatureFlagsProvider' import { MagicInformationModal } from '../../MagicInformationModal' import { WalletInformationModal } from '../../WalletInformationModal' -import { getIdentitySignature, connectToProvider, isSocialLogin, fromConnectionOptionToProviderType, getIsMobile } from './utils' +import { getIdentitySignature, connectToProvider, isSocialLogin, fromConnectionOptionToProviderType } from './utils' import styles from './LoginPage.module.css' const BACKGROUND_IMAGES = [Image1, Image2, Image3, Image4, Image5, Image6, Image7, Image8, Image9, Image10] @@ -37,7 +37,6 @@ export const LoginPage = () => { const [showMagicLearnMore, setShowMagicLearnMore] = useState(false) const [showConnectionModal, setShowConnectionModal] = useState(false) const [currentConnectionType, setCurrentConnectionType] = useState() - const [isMobile] = useState(getIsMobile()) const { url: redirectTo, redirect } = useAfterLoginRedirection() const showGuestOption = redirectTo && new URL(redirectTo).pathname.includes('/play') const [currentBackgroundIndex, setCurrentBackgroundIndex] = useState(0) @@ -193,21 +192,7 @@ export const LoginPage = () => { onLearnMore={handleLearnMore} onConnect={handleOnConnect} loadingOption={currentConnectionType} - socialOptions={{ - primary: ConnectionOptionType.GOOGLE, - secondary: [ConnectionOptionType.DISCORD, ConnectionOptionType.APPLE, ConnectionOptionType.X] - }} - web3Options={ - isMobile - ? { - primary: ConnectionOptionType.WALLET_CONNECT, - secondary: [ConnectionOptionType.FORTMATIC, ConnectionOptionType.COINBASE] - } - : { - primary: ConnectionOptionType.METAMASK, - secondary: [ConnectionOptionType.FORTMATIC, ConnectionOptionType.COINBASE, ConnectionOptionType.WALLET_CONNECT] - } - } + connectionOptions={targetConfig.connectionOptions} /> {showGuestOption && (
diff --git a/src/components/Pages/LoginPage/utils.ts b/src/components/Pages/LoginPage/utils.ts index f2445cd..89f9d1e 100644 --- a/src/components/Pages/LoginPage/utils.ts +++ b/src/components/Pages/LoginPage/utils.ts @@ -106,7 +106,7 @@ export async function getIdentitySignature(address: string, provider: Provider): return identity } -export function getIsMobile() { +export function isMobile() { const userAgent = navigator.userAgent return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent) } diff --git a/src/components/Pages/RequestPage/RequestPage.tsx b/src/components/Pages/RequestPage/RequestPage.tsx index fefef14..8989146 100644 --- a/src/components/Pages/RequestPage/RequestPage.tsx +++ b/src/components/Pages/RequestPage/RequestPage.tsx @@ -54,7 +54,7 @@ export const RequestPage = () => { const timeoutRef = useRef() const connectedAccountRef = useRef() const requestId = params.requestId ?? '' - const [targetConfig, targetConfigId] = useTargetConfig() + const [targetConfig] = useTargetConfig() // Goes to the login page where the user will have to connect a wallet. const toLoginPage = useCallback(() => { @@ -185,6 +185,10 @@ export const RequestPage = () => { throw new Error(result.error) } else { setView(View.VERIFY_SIGN_IN_COMPLETE) + + if (targetConfig.deepLink) { + window.location.href = targetConfig.deepLink + } } } catch (e) { setError(isErrorWithMessage(e) ? e.message : 'Unknown error') diff --git a/src/hooks/targetConfig.spec.ts b/src/hooks/targetConfig.spec.ts new file mode 100644 index 0000000..e503ec1 --- /dev/null +++ b/src/hooks/targetConfig.spec.ts @@ -0,0 +1,76 @@ +import { useLocation } from 'react-router-dom' +import { renderHook } from '@testing-library/react-hooks' +import { ConnectionOptionType } from '../components/Connection' +import { isMobile } from '../components/Pages/LoginPage/utils' +import { _targetConfigs, useTargetConfig } from './targetConfig' + +jest.mock('react-router-dom') +jest.mock('../components/Pages/LoginPage/utils') + +describe('useTargetConfig', () => { + const mockLocation = { search: '' } + + beforeEach(() => { + jest.clearAllMocks() + ;(useLocation as jest.Mock).mockReturnValue(mockLocation) + }) + + it('returns default config when no targetConfigId is provided', () => { + ;(isMobile as jest.Mock).mockReturnValue(false) + + const { result } = renderHook(() => useTargetConfig()) + const [config, targetConfigId] = result.current + + expect(targetConfigId).toBe('default') + expect(config).toEqual(_targetConfigs.default) + }) + + it('returns ios config when targetConfigId=ios', () => { + ;(useLocation as jest.Mock).mockReturnValue({ search: '?targetConfigId=ios' }) + ;(isMobile as jest.Mock).mockReturnValue(false) + + const { result } = renderHook(() => useTargetConfig()) + const [config, targetConfigId] = result.current + + expect(targetConfigId).toBe('ios') + expect(config).toEqual(_targetConfigs.ios) + }) + + it('returns the mobile-adjusted config for default config on mobile', () => { + ;(useLocation as jest.Mock).mockReturnValue({ search: '?targetConfigId=default' }) + ;(isMobile as jest.Mock).mockReturnValue(true) + + const { result } = renderHook(() => useTargetConfig()) + const [config] = result.current + + expect(config.connectionOptions.primary).toBe(ConnectionOptionType.GOOGLE) + expect(config.connectionOptions.secondary).toBe(ConnectionOptionType.WALLET_CONNECT) + expect(config.connectionOptions.extraOptions).not.toContain(ConnectionOptionType.WALLET_CONNECT) + expect(config.connectionOptions.extraOptions).not.toContain(ConnectionOptionType.METAMASK) + }) + + it('applies mobile adjustments for all targetConfigId configurations', () => { + ;(useLocation as jest.Mock).mockReturnValue({ search: '?targetConfigId=alternative' }) + ;(isMobile as jest.Mock).mockReturnValue(true) + + const { result } = renderHook(() => useTargetConfig()) + const [config, targetConfigId] = result.current + + expect(targetConfigId).toBe('alternative') + expect(config.connectionOptions.primary).toBe(ConnectionOptionType.GOOGLE) + expect(config.connectionOptions.secondary).toBe(ConnectionOptionType.WALLET_CONNECT) + expect(config.connectionOptions.extraOptions).not.toContain(ConnectionOptionType.WALLET_CONNECT) + expect(config.connectionOptions.extraOptions).not.toContain(ConnectionOptionType.METAMASK) + }) + + it('returns default config when an invalid targetConfigId is provided', () => { + ;(useLocation as jest.Mock).mockReturnValue({ search: '?targetConfigId=invalid' }) + ;(isMobile as jest.Mock).mockReturnValue(false) + + const { result } = renderHook(() => useTargetConfig()) + const [config, targetConfigId] = result.current + + expect(targetConfigId).toBe('default') + expect(config).toEqual(_targetConfigs.default) + }) +}) diff --git a/src/hooks/targetConfig.ts b/src/hooks/targetConfig.ts index 40ccc50..419693e 100644 --- a/src/hooks/targetConfig.ts +++ b/src/hooks/targetConfig.ts @@ -1,30 +1,148 @@ -import { useLocation } from 'react-router-dom' +import { useLocation, Location } from 'react-router-dom' +import { ConnectionOptionType } from '../components/Connection' +import { isMobile } from '../components/Pages/LoginPage/utils' + +type TargetConfigId = 'default' | 'alternative' | 'ios' | 'android' | 'androidSocial' | 'androidWeb3' + +type ConnectionOptions = { + primary: ConnectionOptionType + secondary?: ConnectionOptionType + extraOptions?: ConnectionOptionType[] +} -type TargetConfigId = 'default' | 'alternative' type TargetConfig = { skipSetup: boolean showWearablePreview: boolean explorerText: string + connectionOptions: ConnectionOptions + deepLink?: string +} + +const defaultConfig: TargetConfig = { + skipSetup: false, + showWearablePreview: true, + explorerText: 'Desktop App', + connectionOptions: { + primary: ConnectionOptionType.GOOGLE, + secondary: ConnectionOptionType.METAMASK, + extraOptions: [ + ConnectionOptionType.DISCORD, + ConnectionOptionType.APPLE, + ConnectionOptionType.X, + ConnectionOptionType.FORTMATIC, + ConnectionOptionType.COINBASE, + ConnectionOptionType.WALLET_CONNECT + ] + } } -const targetConfigRecord: Record = { +const targetConfigs: Record = { default: { - skipSetup: false, - showWearablePreview: true, - explorerText: 'Desktop App' + ...defaultConfig }, alternative: { + ...defaultConfig, skipSetup: true, showWearablePreview: false, explorerText: 'Explorer' + }, + ios: { + ...defaultConfig, + skipSetup: true, + showWearablePreview: false, + explorerText: 'Mobile App', + connectionOptions: { + primary: ConnectionOptionType.APPLE, + secondary: ConnectionOptionType.WALLET_CONNECT, + extraOptions: [ + ConnectionOptionType.GOOGLE, + ConnectionOptionType.DISCORD, + ConnectionOptionType.X, + ConnectionOptionType.FORTMATIC, + ConnectionOptionType.COINBASE + ] + }, + deepLink: 'decentraland://' + }, + android: { + ...defaultConfig, + skipSetup: true, + showWearablePreview: false, + explorerText: 'Mobile App', + connectionOptions: { + primary: ConnectionOptionType.GOOGLE, + secondary: ConnectionOptionType.WALLET_CONNECT, + extraOptions: [ + ConnectionOptionType.DISCORD, + ConnectionOptionType.APPLE, + ConnectionOptionType.X, + ConnectionOptionType.FORTMATIC, + ConnectionOptionType.COINBASE + ] + }, + deepLink: 'decentraland://' + }, + androidSocial: { + ...defaultConfig, + skipSetup: true, + showWearablePreview: false, + explorerText: 'Mobile App', + connectionOptions: { + primary: ConnectionOptionType.GOOGLE, + secondary: ConnectionOptionType.X, + extraOptions: [ConnectionOptionType.APPLE, ConnectionOptionType.DISCORD] + }, + deepLink: 'decentraland://' + }, + androidWeb3: { + ...defaultConfig, + skipSetup: true, + showWearablePreview: false, + explorerText: 'Mobile App', + connectionOptions: { + primary: ConnectionOptionType.WALLET_CONNECT + }, + deepLink: 'decentraland://' + } +} as const + +const adjustWeb3OptionsForMobile = (config: TargetConfig): TargetConfig => { + let { primary, secondary, extraOptions } = config.connectionOptions + + // Replace Metamask Extension for Wallet Connect on Mobile + if (primary === ConnectionOptionType.METAMASK) { + primary = ConnectionOptionType.WALLET_CONNECT + extraOptions = extraOptions?.filter(option => option !== ConnectionOptionType.WALLET_CONNECT) + } + + if (secondary === ConnectionOptionType.METAMASK) { + secondary = ConnectionOptionType.WALLET_CONNECT + extraOptions = extraOptions?.filter(option => option !== ConnectionOptionType.WALLET_CONNECT) + } + + extraOptions = extraOptions?.filter(option => option !== ConnectionOptionType.METAMASK) + + return { + ...config, + connectionOptions: { primary, secondary, extraOptions } } } +// Exporting targetConfigs specifically for testing +export const _targetConfigs = targetConfigs + +const getTargetConfigId = (location: Location): TargetConfigId => { + const search = new URLSearchParams(location.search) + const targetConfigIdParam = search.get('targetConfigId') as TargetConfigId + return targetConfigIdParam in targetConfigs ? targetConfigIdParam : 'default' +} + export const useTargetConfig = (): [TargetConfig, TargetConfigId] => { const location = useLocation() - const search = new URLSearchParams(location.search) - const targetConfigIdSearchParam = search.get('targetConfigId') || 'default' - const targetConfigId = - targetConfigIdSearchParam && targetConfigIdSearchParam in targetConfigRecord ? (targetConfigIdSearchParam as TargetConfigId) : 'default' - return [targetConfigRecord[targetConfigId], targetConfigId] + const targetConfigId = getTargetConfigId(location) + let config = targetConfigs[targetConfigId] + if (isMobile()) { + config = adjustWeb3OptionsForMobile(config) + } + return [config, targetConfigId] } From 636df61b84b5b738787f608162de1ffd8f0ad282 Mon Sep 17 00:00:00 2001 From: "Mateo \"Kuruk\" Miccino" Date: Thu, 7 Nov 2024 19:12:11 -0300 Subject: [PATCH 2/6] fix --- src/components/Pages/RequestPage/RequestPage.tsx | 2 +- src/hooks/targetConfig.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Pages/RequestPage/RequestPage.tsx b/src/components/Pages/RequestPage/RequestPage.tsx index 8989146..31e4e3b 100644 --- a/src/components/Pages/RequestPage/RequestPage.tsx +++ b/src/components/Pages/RequestPage/RequestPage.tsx @@ -54,7 +54,7 @@ export const RequestPage = () => { const timeoutRef = useRef() const connectedAccountRef = useRef() const requestId = params.requestId ?? '' - const [targetConfig] = useTargetConfig() + const [targetConfig, targetConfigId] = useTargetConfig() // Goes to the login page where the user will have to connect a wallet. const toLoginPage = useCallback(() => { diff --git a/src/hooks/targetConfig.ts b/src/hooks/targetConfig.ts index 419693e..9166567 100644 --- a/src/hooks/targetConfig.ts +++ b/src/hooks/targetConfig.ts @@ -104,7 +104,7 @@ const targetConfigs: Record = { }, deepLink: 'decentraland://' } -} as const +} const adjustWeb3OptionsForMobile = (config: TargetConfig): TargetConfig => { let { primary, secondary, extraOptions } = config.connectionOptions From cd57b8e03052b7306d43e87ff8e333b8b45f74c3 Mon Sep 17 00:00:00 2001 From: "Mateo \"Kuruk\" Miccino" Date: Wed, 20 Nov 2024 19:49:00 -0300 Subject: [PATCH 3/6] add defaultMobileConfig --- src/hooks/targetConfig.ts | 40 ++++++++++++++++----------------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/src/hooks/targetConfig.ts b/src/hooks/targetConfig.ts index 9166567..d38a300 100644 --- a/src/hooks/targetConfig.ts +++ b/src/hooks/targetConfig.ts @@ -36,6 +36,14 @@ const defaultConfig: TargetConfig = { } } +const defaultMobileConfig: TargetConfig = { + ...defaultConfig, + skipSetup: true, + showWearablePreview: false, + explorerText: 'Mobile App', + deepLink: 'decentraland://' +} + const targetConfigs: Record = { default: { ...defaultConfig @@ -47,10 +55,7 @@ const targetConfigs: Record = { explorerText: 'Explorer' }, ios: { - ...defaultConfig, - skipSetup: true, - showWearablePreview: false, - explorerText: 'Mobile App', + ...defaultMobileConfig, connectionOptions: { primary: ConnectionOptionType.APPLE, secondary: ConnectionOptionType.WALLET_CONNECT, @@ -61,14 +66,10 @@ const targetConfigs: Record = { ConnectionOptionType.FORTMATIC, ConnectionOptionType.COINBASE ] - }, - deepLink: 'decentraland://' + } }, android: { - ...defaultConfig, - skipSetup: true, - showWearablePreview: false, - explorerText: 'Mobile App', + ...defaultMobileConfig, connectionOptions: { primary: ConnectionOptionType.GOOGLE, secondary: ConnectionOptionType.WALLET_CONNECT, @@ -79,30 +80,21 @@ const targetConfigs: Record = { ConnectionOptionType.FORTMATIC, ConnectionOptionType.COINBASE ] - }, - deepLink: 'decentraland://' + } }, androidSocial: { - ...defaultConfig, - skipSetup: true, - showWearablePreview: false, - explorerText: 'Mobile App', + ...defaultMobileConfig, connectionOptions: { primary: ConnectionOptionType.GOOGLE, secondary: ConnectionOptionType.X, extraOptions: [ConnectionOptionType.APPLE, ConnectionOptionType.DISCORD] - }, - deepLink: 'decentraland://' + } }, androidWeb3: { - ...defaultConfig, - skipSetup: true, - showWearablePreview: false, - explorerText: 'Mobile App', + ...defaultMobileConfig, connectionOptions: { primary: ConnectionOptionType.WALLET_CONNECT - }, - deepLink: 'decentraland://' + } } } From 2f5e7410d9979bd53ccc7d5b5922cdb6eccefe4f Mon Sep 17 00:00:00 2001 From: "Mateo \"Kuruk\" Miccino" Date: Wed, 20 Nov 2024 19:49:24 -0300 Subject: [PATCH 4/6] add required tests in targetConfig from feedback --- src/hooks/targetConfig.spec.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/hooks/targetConfig.spec.ts b/src/hooks/targetConfig.spec.ts index e503ec1..b261f12 100644 --- a/src/hooks/targetConfig.spec.ts +++ b/src/hooks/targetConfig.spec.ts @@ -36,6 +36,39 @@ describe('useTargetConfig', () => { expect(config).toEqual(_targetConfigs.ios) }) + it('returns android config when targetConfigId=android', () => { + ;(useLocation as jest.Mock).mockReturnValue({ search: '?targetConfigId=android' }) + ;(isMobile as jest.Mock).mockReturnValue(false) + + const { result } = renderHook(() => useTargetConfig()) + const [config, targetConfigId] = result.current + + expect(targetConfigId).toBe('android') + expect(config).toEqual(_targetConfigs.android) + }) + + it('returns androidSocial config when targetConfigId=androidSocial', () => { + ;(useLocation as jest.Mock).mockReturnValue({ search: '?targetConfigId=androidSocial' }) + ;(isMobile as jest.Mock).mockReturnValue(false) + + const { result } = renderHook(() => useTargetConfig()) + const [config, targetConfigId] = result.current + + expect(targetConfigId).toBe('androidSocial') + expect(config).toEqual(_targetConfigs.androidSocial) + }) + + it('returns androidWeb3 config when targetConfigId=androidWeb3', () => { + ;(useLocation as jest.Mock).mockReturnValue({ search: '?targetConfigId=androidWeb3' }) + ;(isMobile as jest.Mock).mockReturnValue(false) + + const { result } = renderHook(() => useTargetConfig()) + const [config, targetConfigId] = result.current + + expect(targetConfigId).toBe('androidWeb3') + expect(config).toEqual(_targetConfigs.androidWeb3) + }) + it('returns the mobile-adjusted config for default config on mobile', () => { ;(useLocation as jest.Mock).mockReturnValue({ search: '?targetConfigId=default' }) ;(isMobile as jest.Mock).mockReturnValue(true) From 4155925a43a00f1872acc8dea6437bcae3a4920f Mon Sep 17 00:00:00 2001 From: "Mateo \"Kuruk\" Miccino" Date: Wed, 20 Nov 2024 19:50:18 -0300 Subject: [PATCH 5/6] fix connection spec tests, rename: social->primary, web3->secondary, secondary->extra, and add tests from feedback --- src/components/Connection/Connection.spec.tsx | 46 +++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/src/components/Connection/Connection.spec.tsx b/src/components/Connection/Connection.spec.tsx index 98e41bf..9e16296 100644 --- a/src/components/Connection/Connection.spec.tsx +++ b/src/components/Connection/Connection.spec.tsx @@ -1,6 +1,6 @@ import { act, fireEvent, render } from '@testing-library/react' import { Connection } from './Connection' -import { EXTRA_TEST_ID, PRIMARY_TEST_ID, SHOW_MORE_BUTTON_TEST_ID } from './constants' +import { EXTRA_TEST_ID, PRIMARY_TEST_ID, SECONDARY_TEST_ID, SHOW_MORE_BUTTON_TEST_ID } from './constants' import { ConnectionOptionType, ConnectionProps } from './Connection.types' function renderConnection(props: Partial) { @@ -33,7 +33,7 @@ describe('when rendering the component', () => { let connectionOptions: ConnectionProps['connectionOptions'] let onConnect: jest.Mock - describe('and there are social options', () => { + describe('and there are primary, secondary and extra options', () => { beforeEach(() => { onConnect = jest.fn() connectionOptions = { @@ -44,9 +44,10 @@ describe('when rendering the component', () => { screen = renderConnection({ connectionOptions, onConnect }) }) - it('should render the primary social option', () => { + it('should render the primary and secondary option', () => { const { getByTestId } = screen expect(getByTestId(PRIMARY_TEST_ID)).toBeInTheDocument() + expect(getByTestId(SECONDARY_TEST_ID)).toBeInTheDocument() }) it('should call the onConnect method prop when clicking the button', () => { @@ -55,7 +56,7 @@ describe('when rendering the component', () => { expect(onConnect).toHaveBeenCalledWith(ConnectionOptionType.GOOGLE) }) - it('should render all the secondary options', () => { + it('should render all the extra options', () => { const { getByTestId } = screen act(() => { fireEvent.click(getByTestId(SHOW_MORE_BUTTON_TEST_ID)) @@ -130,7 +131,7 @@ describe('when rendering the component', () => { }) }) - describe('and there are web3 options', () => { + describe('and there are is not secondary options', () => { beforeEach(() => { onConnect = jest.fn() connectionOptions = { @@ -140,12 +141,17 @@ describe('when rendering the component', () => { screen = renderConnection({ connectionOptions, onConnect }) }) - it('should render the primary social option', () => { + it('should render the primary option', () => { const { getByTestId } = screen expect(getByTestId(PRIMARY_TEST_ID)).toBeInTheDocument() }) - it('should render all the secondary options', () => { + it('should not render the secondary', () => { + const { queryByTestId } = screen + expect(queryByTestId(SECONDARY_TEST_ID)).not.toBeInTheDocument() + }) + + it('should render all the extra options', () => { const { getByTestId } = screen act(() => { fireEvent.click(getByTestId(SHOW_MORE_BUTTON_TEST_ID)) @@ -167,7 +173,31 @@ describe('when rendering the component', () => { }) }) - describe('and there are no web3 nor social secondary options', () => { + describe('and there are primary, but not secondary nor extra options', () => { + beforeEach(() => { + connectionOptions = { + primary: ConnectionOptionType.METAMASK + } + screen = renderConnection({ connectionOptions }) + }) + + it('should render the primary', () => { + const { queryByTestId } = screen + expect(queryByTestId(PRIMARY_TEST_ID)).toBeInTheDocument() + }) + + it('should not render the secondary', () => { + const { queryByTestId } = screen + expect(queryByTestId(SECONDARY_TEST_ID)).not.toBeInTheDocument() + }) + + it('should not render the more options button', () => { + const { queryByTestId } = screen + expect(queryByTestId(SHOW_MORE_BUTTON_TEST_ID)).not.toBeInTheDocument() + }) + }) + + describe('and there are no primary nor secondary nor extra options', () => { beforeEach(() => { connectionOptions = undefined screen = renderConnection({ connectionOptions }) From b244cbdbcf6d2a8ddb7e8c5e28bc54af413dbff8 Mon Sep 17 00:00:00 2001 From: "Mateo \"Kuruk\" Miccino" Date: Wed, 20 Nov 2024 19:52:15 -0300 Subject: [PATCH 6/6] use `isSocialLogin` instead of `isMagicConnection` --- src/components/Connection/Connection.tsx | 6 +++--- src/components/Connection/Connection.types.ts | 9 --------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/components/Connection/Connection.tsx b/src/components/Connection/Connection.tsx index 208eb2c..14212a4 100644 --- a/src/components/Connection/Connection.tsx +++ b/src/components/Connection/Connection.tsx @@ -10,9 +10,9 @@ import { ConnectionProps, MetamaskEthereumWindow, connectionOptionTitles, - isMagicConnection } from './Connection.types' import styles from './Connection.module.css' +import { isSocialLogin } from '../Pages/LoginPage/utils' const Primary = ({ message, @@ -113,7 +113,7 @@ export const Connection = (props: ConnectionProps): JSX.Element => { : undefined } message={ - isMagicConnection(option) ? ( + isSocialLogin(option) ? ( <> {i18n.socialMessage(
)} onLearnMore(option)}> @@ -129,7 +129,7 @@ export const Connection = (props: ConnectionProps): JSX.Element => { ) } > - <>{isMagicConnection(option) ? i18n.accessWith(option) : i18n.connectWith(option)} + <>{isSocialLogin(option) ? i18n.accessWith(option) : i18n.connectWith(option)} ) diff --git a/src/components/Connection/Connection.types.ts b/src/components/Connection/Connection.types.ts index 340956d..2afc51f 100644 --- a/src/components/Connection/Connection.types.ts +++ b/src/components/Connection/Connection.types.ts @@ -28,15 +28,6 @@ export const connectionOptionTitles: { [key in ConnectionOptionType]: string } = [ConnectionOptionType.X]: 'X' } -export function isMagicConnection(option: ConnectionOptionType): boolean { - return ( - option === ConnectionOptionType.X || - option === ConnectionOptionType.APPLE || - option === ConnectionOptionType.GOOGLE || - option === ConnectionOptionType.DISCORD - ) -} - export type MetamaskEthereumWindow = typeof window.ethereum & { isMetaMask?: boolean } export type ConnectionI18N = {