-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
feat(card): cp-7.58.0 add card experimental deeplink #21599
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
fd1bd72
ee26ca0
7ffef0a
6f0038e
bc92d45
935418c
81fad13
931ae8f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<boolean>(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; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { default } from './CardNotification'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| }); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| 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'; | ||
| 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 | ||
| * - 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'); | ||
|
|
||
| // 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', | ||
| ); | ||
| 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', | ||
| ); | ||
| } | ||
| }; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The recommended implementation is to leverage universal links instead of traditional links. In other words, let's move this logic to |
Uh oh!
There was an error while loading. Please reload this page.