From fd1bd722257d4afab31558375725e23ab25ae68a Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Fri, 24 Oct 2025 00:37:43 -0300 Subject: [PATCH 1/4] feat(card): add card experimental deeplink --- app/constants/deeplinks.ts | 2 + app/core/DeeplinkManager/DeeplinkManager.ts | 5 + .../Handlers/handleEnableCardButton.test.ts | 133 ++++++++++++++++++ .../Handlers/handleEnableCardButton.ts | 55 ++++++++ .../handleMetaMaskDeeplink.test.ts | 21 +++ .../ParseManager/handleMetaMaskDeeplink.ts | 4 + 6 files changed, 220 insertions(+) create mode 100644 app/core/DeeplinkManager/Handlers/handleEnableCardButton.test.ts create mode 100644 app/core/DeeplinkManager/Handlers/handleEnableCardButton.ts diff --git a/app/constants/deeplinks.ts b/app/constants/deeplinks.ts index a757d3efbd49..78ed9320eab4 100644 --- a/app/constants/deeplinks.ts +++ b/app/constants/deeplinks.ts @@ -13,6 +13,7 @@ export enum PROTOCOLS { } export enum ACTIONS { + ENABLE_CARD_BUTTON = 'enable-card-button', DAPP = 'dapp', SEND = 'send', APPROVE = 'approve', @@ -62,5 +63,6 @@ export const PREFIXES = { [ACTIONS.PERPS_ASSET]: '', [ACTIONS.REWARDS]: '', [ACTIONS.ONBOARDING]: '', + [ACTIONS.ENABLE_CARD_BUTTON]: '', METAMASK: 'metamask://', }; diff --git a/app/core/DeeplinkManager/DeeplinkManager.ts b/app/core/DeeplinkManager/DeeplinkManager.ts index afa7c677adc3..1663638462c7 100644 --- a/app/core/DeeplinkManager/DeeplinkManager.ts +++ b/app/core/DeeplinkManager/DeeplinkManager.ts @@ -25,6 +25,7 @@ import SharedDeeplinkManager from './SharedDeeplinkManager'; import FCMService from '../../util/notifications/services/FCMService'; import { handleRewardsUrl } from './Handlers/handleRewardsUrl'; import handleFastOnboarding from './Handlers/handleFastOnboarding'; +import { handleEnableCardButton } from './Handlers/handleEnableCardButton'; class DeeplinkManager { // TODO: Replace "any" with type @@ -146,6 +147,10 @@ class DeeplinkManager { handleFastOnboarding({ onboardingPath }); } + _handleEnableCardButton() { + handleEnableCardButton(); + } + async parse( url: string, { diff --git a/app/core/DeeplinkManager/Handlers/handleEnableCardButton.test.ts b/app/core/DeeplinkManager/Handlers/handleEnableCardButton.test.ts new file mode 100644 index 000000000000..5605398882c7 --- /dev/null +++ b/app/core/DeeplinkManager/Handlers/handleEnableCardButton.test.ts @@ -0,0 +1,133 @@ +import { handleEnableCardButton } from './handleEnableCardButton'; +import { store } from '../../../store'; +import { setAlwaysShowCardButton } from '../../../core/redux/slices/card'; +import { selectCardExperimentalSwitch } from '../../../selectors/featureFlagController/card'; +import DevLogger from '../../SDKConnect/utils/DevLogger'; +import Logger from '../../../util/Logger'; + +jest.mock('../../../store'); +jest.mock('../../../core/redux/slices/card'); +jest.mock('../../../selectors/featureFlagController/card'); +jest.mock('../../SDKConnect/utils/DevLogger'); +jest.mock('../../../util/Logger'); + +describe('handleEnableCardButton', () => { + const mockGetState = jest.fn(); + const mockDispatch = jest.fn(); + const mockDevLogger = DevLogger.log as jest.Mock; + const mockLogger = Logger.log as jest.Mock; + const mockLoggerError = Logger.error as jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + + (store.getState as jest.Mock) = mockGetState; + (store.dispatch as jest.Mock) = mockDispatch; + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('when card experimental switch is enabled', () => { + beforeEach(() => { + (selectCardExperimentalSwitch as unknown as jest.Mock).mockReturnValue( + true, + ); + mockGetState.mockReturnValue({}); + }); + + it('dispatches setAlwaysShowCardButton with true', () => { + handleEnableCardButton(); + + expect(mockDispatch).toHaveBeenCalledWith(setAlwaysShowCardButton(true)); + }); + + it('logs successful enablement', () => { + handleEnableCardButton(); + + expect(mockDevLogger).toHaveBeenCalledWith( + '[handleEnableCardButton] Successfully enabled card button', + ); + expect(mockLogger).toHaveBeenCalledWith( + '[handleEnableCardButton] Card button enabled via deeplink', + ); + }); + + it('logs starting message', () => { + handleEnableCardButton(); + + expect(mockDevLogger).toHaveBeenCalledWith( + '[handleEnableCardButton] Starting card button enable deeplink handling', + ); + }); + }); + + describe('when card experimental switch is disabled', () => { + beforeEach(() => { + (selectCardExperimentalSwitch as unknown as jest.Mock).mockReturnValue( + false, + ); + mockGetState.mockReturnValue({}); + }); + + it('does not dispatch setAlwaysShowCardButton', () => { + handleEnableCardButton(); + + expect(mockDispatch).not.toHaveBeenCalled(); + }); + + it('logs that feature flag is disabled', () => { + handleEnableCardButton(); + + expect(mockDevLogger).toHaveBeenCalledWith( + '[handleEnableCardButton] Card experimental switch is disabled, skipping', + ); + expect(mockLogger).toHaveBeenCalledWith( + '[handleEnableCardButton] Card experimental switch feature flag is disabled', + ); + }); + + it('logs starting message', () => { + handleEnableCardButton(); + + expect(mockDevLogger).toHaveBeenCalledWith( + '[handleEnableCardButton] Starting card button enable deeplink handling', + ); + }); + }); + + describe('when an error occurs', () => { + const mockError = new Error('Test error'); + + beforeEach(() => { + mockGetState.mockImplementation(() => { + throw mockError; + }); + }); + + it('logs error with DevLogger', () => { + handleEnableCardButton(); + + expect(mockDevLogger).toHaveBeenCalledWith( + '[handleEnableCardButton] Failed to enable card button:', + mockError, + ); + }); + + it('logs error with Logger', () => { + handleEnableCardButton(); + + expect(mockLoggerError).toHaveBeenCalledWith( + mockError, + '[handleEnableCardButton] Error enabling card button', + ); + }); + + it('does not dispatch setAlwaysShowCardButton', () => { + handleEnableCardButton(); + + expect(mockDispatch).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/app/core/DeeplinkManager/Handlers/handleEnableCardButton.ts b/app/core/DeeplinkManager/Handlers/handleEnableCardButton.ts new file mode 100644 index 000000000000..a994e895136d --- /dev/null +++ b/app/core/DeeplinkManager/Handlers/handleEnableCardButton.ts @@ -0,0 +1,55 @@ +import DevLogger from '../../SDKConnect/utils/DevLogger'; +import Logger from '../../../util/Logger'; +import { store } from '../../../store'; +import { setAlwaysShowCardButton } from '../../../core/redux/slices/card'; +import { selectCardExperimentalSwitch } from '../../../selectors/featureFlagController/card'; + +/** + * Card deeplink handler to enable the card button + * + * This handler enables the card button by setting the alwaysShowCardButton flag + * to true, but only if the cardExperimentalSwitch feature flag is enabled. + * + * Supported URL formats: + * - https://link.metamask.io/enable-card-button + * - https://metamask.app.link/enable-card-button + */ +export const handleEnableCardButton = () => { + DevLogger.log( + '[handleEnableCardButton] Starting card button enable deeplink handling', + ); + + try { + const state = store.getState(); + const cardExperimentalSwitchEnabled = selectCardExperimentalSwitch(state); + + DevLogger.log( + '[handleEnableCardButton] Card experimental switch enabled:', + cardExperimentalSwitchEnabled, + ); + + if (cardExperimentalSwitchEnabled) { + store.dispatch(setAlwaysShowCardButton(true)); + DevLogger.log( + '[handleEnableCardButton] Successfully enabled card button', + ); + Logger.log('[handleEnableCardButton] Card button enabled via deeplink'); + } else { + DevLogger.log( + '[handleEnableCardButton] Card experimental switch is disabled, skipping', + ); + Logger.log( + '[handleEnableCardButton] Card experimental switch feature flag is disabled', + ); + } + } catch (error) { + DevLogger.log( + '[handleEnableCardButton] Failed to enable card button:', + error, + ); + Logger.error( + error as Error, + '[handleEnableCardButton] Error enabling card button', + ); + } +}; diff --git a/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts b/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts index 56e59df0f58d..d5fcdd698e1c 100644 --- a/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts +++ b/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts @@ -26,6 +26,7 @@ describe('handleMetaMaskProtocol', () => { const mockHandleSellCrypto = jest.fn(); const mockHandleDepositCash = jest.fn(); const mockHandleBrowserUrl = jest.fn(); + const mockHandleEnableCardButton = jest.fn(); const mockConnectToChannel = jest.fn(); const mockGetConnections = jest.fn(); const mockRevalidateChannel = jest.fn(); @@ -45,6 +46,7 @@ describe('handleMetaMaskProtocol', () => { _handleSellCrypto: mockHandleSellCrypto, _handleDepositCash: mockHandleDepositCash, _handleBrowserUrl: mockHandleBrowserUrl, + _handleEnableCardButton: mockHandleEnableCardButton, } as unknown as DeeplinkManager; const handled = jest.fn(); @@ -553,4 +555,23 @@ describe('handleMetaMaskProtocol', () => { expect(mockHandleDepositCash).toHaveBeenCalled(); }); }); + + describe('when url starts with ${PREFIXES.METAMASK}${ACTIONS.ENABLE_CARD_BUTTON}', () => { + beforeEach(() => { + url = `${PREFIXES.METAMASK}${ACTIONS.ENABLE_CARD_BUTTON}`; + }); + + it('calls _handleEnableCardButton', () => { + handleMetaMaskDeeplink({ + instance, + handled, + params, + url, + origin, + wcURL, + }); + + expect(mockHandleEnableCardButton).toHaveBeenCalled(); + }); + }); }); diff --git a/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts b/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts index 7a4b2c84447f..a379b4a34146 100644 --- a/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts +++ b/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts @@ -165,6 +165,10 @@ export function handleMetaMaskDeeplink({ '', ); instance._handleDepositCash(depositCashPath); + } else if ( + url.startsWith(`${PREFIXES.METAMASK}${ACTIONS.ENABLE_CARD_BUTTON}`) + ) { + instance._handleEnableCardButton(); } } From 7ffef0ad134045a9e6bf105222f32d6fa283a798 Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Fri, 24 Oct 2025 18:38:23 -0300 Subject: [PATCH 2/4] feat(card): add toast after enabling the button --- app/components/Nav/App/App.tsx | 5 +++ .../CardNotification/CardNotification.tsx | 41 +++++++++++++++++++ .../Views/CardNotification/index.tsx | 1 + app/constants/navigation/Routes.ts | 1 + .../Handlers/handleEnableCardButton.ts | 8 ++++ locales/languages/en.json | 1 + 6 files changed, 57 insertions(+) create mode 100644 app/components/Views/CardNotification/CardNotification.tsx create mode 100644 app/components/Views/CardNotification/index.tsx diff --git a/app/components/Nav/App/App.tsx b/app/components/Nav/App/App.tsx index 01db0f8e8ef7..26f387c76e6c 100644 --- a/app/components/Nav/App/App.tsx +++ b/app/components/Nav/App/App.tsx @@ -71,6 +71,7 @@ import WalletActions from '../../Views/WalletActions'; import FundActionMenu from '../../UI/FundActionMenu'; import NetworkSelector from '../../../components/Views/NetworkSelector'; import ReturnToAppNotification from '../../Views/ReturnToAppNotification'; +import CardNotification from '../../Views/CardNotification'; import EditAccountName from '../../Views/EditAccountName/EditAccountName'; import LegacyEditMultichainAccountName from '../../Views/MultichainAccounts/sheets/EditAccountName'; import { EditMultichainAccountName } from '../../Views/MultichainAccounts/sheets/EditMultichainAccountName'; @@ -568,6 +569,10 @@ const RootModalFlow = (props: RootModalFlowProps) => ( component={ReturnToAppNotification} initialParams={{ ...props.route.params }} /> + ); diff --git a/app/components/Views/CardNotification/CardNotification.tsx b/app/components/Views/CardNotification/CardNotification.tsx new file mode 100644 index 000000000000..9bc081f5f55c --- /dev/null +++ b/app/components/Views/CardNotification/CardNotification.tsx @@ -0,0 +1,41 @@ +import React, { useContext, useEffect, useRef } from 'react'; +import { ToastContext } from '../../../component-library/components/Toast'; +import { ToastVariants } from '../../../component-library/components/Toast/Toast.types'; +import { useNavigation } from '@react-navigation/native'; +import { IconName } from '../../../component-library/components/Icons/Icon'; +import { strings } from '../../../../locales/i18n'; + +/** + * Fake modal that displays a toast for card-related deeplinks. + * Similar to ReturnToAppNotification but for card feature. + * + * This component is used to trigger toasts from non-React contexts (like deeplink handlers) + * by navigating to this route with toast parameters, then immediately going back. + */ +const CardNotification = () => { + const navigation = useNavigation(); + const { toastRef } = useContext(ToastContext); + const hasExecuted = useRef(false); + + useEffect(() => { + if (toastRef && toastRef.current !== null && !hasExecuted.current) { + hasExecuted.current = true; + + toastRef.current.showToast({ + variant: ToastVariants.Icon, + labelOptions: [ + { label: strings('card.card_home.card_button_enabled_toast') }, + ], + hasNoTimeout: false, + iconName: IconName.Info, + }); + + // Hide the fake modal + navigation?.goBack(); + } + }, [toastRef, navigation]); + + return <>; +}; + +export default CardNotification; diff --git a/app/components/Views/CardNotification/index.tsx b/app/components/Views/CardNotification/index.tsx new file mode 100644 index 000000000000..087cdbec8112 --- /dev/null +++ b/app/components/Views/CardNotification/index.tsx @@ -0,0 +1 @@ +export { default } from './CardNotification'; diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 9ceba06f4689..ce09b5280501 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -353,6 +353,7 @@ const Routes = { HOME: 'CardHome', WELCOME: 'CardWelcome', AUTHENTICATION: 'CardAuthentication', + NOTIFICATION: 'CardNotification', ONBOARDING: { ROOT: 'CardOnboarding', SIGN_UP: 'CardOnboardingSignUp', diff --git a/app/core/DeeplinkManager/Handlers/handleEnableCardButton.ts b/app/core/DeeplinkManager/Handlers/handleEnableCardButton.ts index a994e895136d..e312e7f476c2 100644 --- a/app/core/DeeplinkManager/Handlers/handleEnableCardButton.ts +++ b/app/core/DeeplinkManager/Handlers/handleEnableCardButton.ts @@ -3,12 +3,15 @@ import Logger from '../../../util/Logger'; import { store } from '../../../store'; import { setAlwaysShowCardButton } from '../../../core/redux/slices/card'; import { selectCardExperimentalSwitch } from '../../../selectors/featureFlagController/card'; +import NavigationService from '../../NavigationService'; +import Routes from '../../../constants/navigation/Routes'; /** * Card deeplink handler to enable the card button * * This handler enables the card button by setting the alwaysShowCardButton flag * to true, but only if the cardExperimentalSwitch feature flag is enabled. + * It shows a success toast notification after enabling the button. * * Supported URL formats: * - https://link.metamask.io/enable-card-button @@ -34,6 +37,11 @@ export const handleEnableCardButton = () => { '[handleEnableCardButton] Successfully enabled card button', ); Logger.log('[handleEnableCardButton] Card button enabled via deeplink'); + + // Show success toast via navigation + NavigationService.navigation?.navigate(Routes.MODAL.ROOT_MODAL_FLOW, { + screen: Routes.CARD.NOTIFICATION, + }); } else { DevLogger.log( '[handleEnableCardButton] Card experimental switch is disabled, skipping', diff --git a/locales/languages/en.json b/locales/languages/en.json index d9e83c3ac0c8..1d0aba576323 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -6100,6 +6100,7 @@ "change_asset": "Change asset", "enable_card_button_label": "Enable Card", "enable_assets_button_label": "Enable Assets", + "card_button_enabled_toast": "You can now sign up for MetaMask Card!", "warnings": { "close_spending_limit": { "title": "You're close to your spending limit", From bc92d45480dfa388c6607b9603b0d11ee7f674e4 Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Mon, 27 Oct 2025 08:43:13 -0300 Subject: [PATCH 3/4] test(card): add tests for CardNotification --- .../CardNotification.test.tsx | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 app/components/Views/CardNotification/CardNotification.test.tsx diff --git a/app/components/Views/CardNotification/CardNotification.test.tsx b/app/components/Views/CardNotification/CardNotification.test.tsx new file mode 100644 index 000000000000..aa9951de0c99 --- /dev/null +++ b/app/components/Views/CardNotification/CardNotification.test.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { render, waitFor } from '@testing-library/react-native'; +import CardNotification from './CardNotification'; +import { ToastContext } from '../../../component-library/components/Toast'; +import { ToastVariants } from '../../../component-library/components/Toast/Toast.types'; +import { IconName } from '../../../component-library/components/Icons/Icon'; + +jest.mock('../../../../locales/i18n', () => ({ + strings: jest.fn((key: string) => key), +})); + +const mockGoBack = jest.fn(); +jest.mock('@react-navigation/native', () => { + const actual = jest.requireActual('@react-navigation/native'); + return { + ...actual, + useNavigation: () => ({ + goBack: mockGoBack, + }), + }; +}); + +describe('CardNotification', () => { + const createToastRef = () => ({ + current: { showToast: jest.fn(), closeToast: jest.fn() }, + }); + + const renderWithProviders = (toastRef = createToastRef()) => { + const ui = ( + + + + ); + + const utils = render(ui); + return { ...utils, toastRef }; + }; + + beforeEach(() => { + jest.clearAllMocks(); + mockGoBack.mockClear(); + }); + + it('renders without crashing', () => { + const { toastRef } = renderWithProviders(); + + expect(toastRef.current).toBeDefined(); + }); + + it('displays toast with correct configuration when toastRef is available', async () => { + const { toastRef } = renderWithProviders(); + + await waitFor(() => { + expect(toastRef.current.showToast).toHaveBeenCalledWith({ + variant: ToastVariants.Icon, + labelOptions: [{ label: 'card.card_home.card_button_enabled_toast' }], + hasNoTimeout: false, + iconName: IconName.Info, + }); + }); + }); + + it('calls navigation goBack after showing toast', async () => { + renderWithProviders(); + + await waitFor(() => { + expect(mockGoBack).toHaveBeenCalledTimes(1); + }); + }); + + it('does not show toast when toastRef is null', () => { + const nullToastRef = { + current: null, + } as unknown as { + current: { showToast: jest.Mock; closeToast: jest.Mock }; + }; + const { toastRef } = renderWithProviders(nullToastRef); + + expect(toastRef.current).toBeNull(); + expect(mockGoBack).not.toHaveBeenCalled(); + }); + + it('shows toast only once on multiple renders', async () => { + const { toastRef, rerender } = renderWithProviders(); + + await waitFor(() => { + expect(toastRef.current.showToast).toHaveBeenCalledTimes(1); + }); + + rerender( + + + , + ); + + await waitFor(() => { + expect(toastRef.current.showToast).toHaveBeenCalledTimes(1); + }); + }); + + it('translates toast label using i18n strings', async () => { + const mockStrings = jest.requireMock('../../../../locales/i18n').strings; + const { toastRef } = renderWithProviders(); + + await waitFor(() => { + expect(mockStrings).toHaveBeenCalledWith( + 'card.card_home.card_button_enabled_toast', + ); + expect(toastRef.current.showToast).toHaveBeenCalled(); + }); + }); + + it('renders empty fragment as component output', () => { + const { toJSON } = renderWithProviders(); + + expect(toJSON()).toBeNull(); + }); +}); From 81fad13a0916aa6a600e43d51c6425e4c046a797 Mon Sep 17 00:00:00 2001 From: Bruno Nascimento Date: Wed, 29 Oct 2025 22:57:52 -0300 Subject: [PATCH 4/4] feat: use handleUniversalLink instead of handleMetaMaskDeeplink for card exp. switch --- .../handleMetaMaskDeeplink.test.ts | 21 --------- .../ParseManager/handleMetaMaskDeeplink.ts | 4 -- .../ParseManager/handleUniversalLink.test.ts | 47 +++++++++++++++++++ .../ParseManager/handleUniversalLink.ts | 8 +++- 4 files changed, 54 insertions(+), 26 deletions(-) diff --git a/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts b/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts index d5fcdd698e1c..56e59df0f58d 100644 --- a/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts +++ b/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.test.ts @@ -26,7 +26,6 @@ describe('handleMetaMaskProtocol', () => { const mockHandleSellCrypto = jest.fn(); const mockHandleDepositCash = jest.fn(); const mockHandleBrowserUrl = jest.fn(); - const mockHandleEnableCardButton = jest.fn(); const mockConnectToChannel = jest.fn(); const mockGetConnections = jest.fn(); const mockRevalidateChannel = jest.fn(); @@ -46,7 +45,6 @@ describe('handleMetaMaskProtocol', () => { _handleSellCrypto: mockHandleSellCrypto, _handleDepositCash: mockHandleDepositCash, _handleBrowserUrl: mockHandleBrowserUrl, - _handleEnableCardButton: mockHandleEnableCardButton, } as unknown as DeeplinkManager; const handled = jest.fn(); @@ -555,23 +553,4 @@ describe('handleMetaMaskProtocol', () => { expect(mockHandleDepositCash).toHaveBeenCalled(); }); }); - - describe('when url starts with ${PREFIXES.METAMASK}${ACTIONS.ENABLE_CARD_BUTTON}', () => { - beforeEach(() => { - url = `${PREFIXES.METAMASK}${ACTIONS.ENABLE_CARD_BUTTON}`; - }); - - it('calls _handleEnableCardButton', () => { - handleMetaMaskDeeplink({ - instance, - handled, - params, - url, - origin, - wcURL, - }); - - expect(mockHandleEnableCardButton).toHaveBeenCalled(); - }); - }); }); diff --git a/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts b/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts index a379b4a34146..7a4b2c84447f 100644 --- a/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts +++ b/app/core/DeeplinkManager/ParseManager/handleMetaMaskDeeplink.ts @@ -165,10 +165,6 @@ export function handleMetaMaskDeeplink({ '', ); instance._handleDepositCash(depositCashPath); - } else if ( - url.startsWith(`${PREFIXES.METAMASK}${ACTIONS.ENABLE_CARD_BUTTON}`) - ) { - instance._handleEnableCardButton(); } } diff --git a/app/core/DeeplinkManager/ParseManager/handleUniversalLink.test.ts b/app/core/DeeplinkManager/ParseManager/handleUniversalLink.test.ts index 56f8901d2679..5eba45b5f6ed 100644 --- a/app/core/DeeplinkManager/ParseManager/handleUniversalLink.test.ts +++ b/app/core/DeeplinkManager/ParseManager/handleUniversalLink.test.ts @@ -50,6 +50,7 @@ describe('handleUniversalLinks', () => { const mockHandlePerps = jest.fn(); const mockHandleRewards = jest.fn(); const mockHandleFastOnboarding = jest.fn(); + const mockHandleEnableCardButton = jest.fn(); const mockConnectToChannel = jest.fn(); const mockGetConnections = jest.fn(); const mockRevalidateChannel = jest.fn(); @@ -77,6 +78,7 @@ describe('handleUniversalLinks', () => { _handlePerps: mockHandlePerps, _handleRewards: mockHandleRewards, _handleFastOnboarding: mockHandleFastOnboarding, + _handleEnableCardButton: mockHandleEnableCardButton, } as unknown as DeeplinkManager; const handled = jest.fn(); @@ -697,6 +699,51 @@ describe('handleUniversalLinks', () => { ); }); + describe('ACTIONS.ENABLE_CARD_BUTTON', () => { + const testCases = [ + { + domain: AppConstants.MM_UNIVERSAL_LINK_HOST, + description: 'old deeplink domain', + }, + { + domain: AppConstants.MM_IO_UNIVERSAL_LINK_HOST, + description: 'new deeplink domain', + }, + { + domain: AppConstants.MM_IO_UNIVERSAL_LINK_TEST_HOST, + description: 'test deeplink domain', + }, + ] as const; + + it.each(testCases)( + 'calls _handleEnableCardButton without showing modal for $description', + async ({ domain }) => { + const enableCardButtonUrl = `${PROTOCOLS.HTTPS}://${domain}/${ACTIONS.ENABLE_CARD_BUTTON}`; + const origin = `${PROTOCOLS.HTTPS}://${domain}`; + const enableCardButtonUrlObj = { + ...urlObj, + hostname: domain, + href: enableCardButtonUrl, + pathname: `/${ACTIONS.ENABLE_CARD_BUTTON}`, + origin, + }; + + await handleUniversalLink({ + instance, + handled, + urlObj: enableCardButtonUrlObj, + browserCallBack: mockBrowserCallBack, + url: enableCardButtonUrl, + source: 'test-source', + }); + + expect(mockHandleDeepLinkModalDisplay).not.toHaveBeenCalled(); + expect(handled).toHaveBeenCalled(); + expect(mockHandleEnableCardButton).toHaveBeenCalled(); + }, + ); + }); + describe('signature verification', () => { beforeEach(() => { DevLogger.log = jest.fn(); diff --git a/app/core/DeeplinkManager/ParseManager/handleUniversalLink.ts b/app/core/DeeplinkManager/ParseManager/handleUniversalLink.ts index f35b371219a0..268373239159 100644 --- a/app/core/DeeplinkManager/ParseManager/handleUniversalLink.ts +++ b/app/core/DeeplinkManager/ParseManager/handleUniversalLink.ts @@ -38,6 +38,7 @@ enum SUPPORTED_ACTIONS { REWARDS = ACTIONS.REWARDS, WC = ACTIONS.WC, ONBOARDING = ACTIONS.ONBOARDING, + ENABLE_CARD_BUTTON = ACTIONS.ENABLE_CARD_BUTTON, // MetaMask SDK specific actions ANDROID_SDK = ACTIONS.ANDROID_SDK, CONNECT = ACTIONS.CONNECT, @@ -47,7 +48,10 @@ enum SUPPORTED_ACTIONS { /** * Actions that should not show the deep link modal */ -const WHITELISTED_ACTIONS: SUPPORTED_ACTIONS[] = [SUPPORTED_ACTIONS.WC]; +const WHITELISTED_ACTIONS: SUPPORTED_ACTIONS[] = [ + SUPPORTED_ACTIONS.WC, + SUPPORTED_ACTIONS.ENABLE_CARD_BUTTON, +]; /** * MetaMask SDK actions that should be handled by handleMetaMaskDeeplink @@ -264,6 +268,8 @@ async function handleUniversalLink({ } else if (action === SUPPORTED_ACTIONS.ONBOARDING) { const onboardingPath = urlObj.href.replace(BASE_URL_ACTION, ''); instance._handleFastOnboarding(onboardingPath); + } else if (action === SUPPORTED_ACTIONS.ENABLE_CARD_BUTTON) { + instance._handleEnableCardButton(); } }