diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 932bc957df88..826f10feaa44 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1667,6 +1667,9 @@ "message": "$1 disconnected from $2", "description": "$1 is name of the name and $2 represents the dapp name`" }, + "discover": { + "message": "Discover" + }, "discoverSnaps": { "message": "Discover Snaps", "description": "Text that links to the Snaps website. Displayed in a banner on Snaps list page in settings." diff --git a/app/_locales/en_GB/messages.json b/app/_locales/en_GB/messages.json index 932bc957df88..826f10feaa44 100644 --- a/app/_locales/en_GB/messages.json +++ b/app/_locales/en_GB/messages.json @@ -1667,6 +1667,9 @@ "message": "$1 disconnected from $2", "description": "$1 is name of the name and $2 represents the dapp name`" }, + "discover": { + "message": "Discover" + }, "discoverSnaps": { "message": "Discover Snaps", "description": "Text that links to the Snaps website. Displayed in a banner on Snaps list page in settings." diff --git a/shared/constants/network.ts b/shared/constants/network.ts index 9069434877dc..d08f6664e65d 100644 --- a/shared/constants/network.ts +++ b/shared/constants/network.ts @@ -975,6 +975,14 @@ export const CHAIN_ID_TOKEN_IMAGE_MAP = { [CHAINLIST_CHAIN_IDS_MAP.UNICHAIN_SEPOLIA]: ETH_TOKEN_IMAGE_URL, } as const; +/** + * A mapping for networks with enabled profolio landing page to their URLs. + */ +export const CHAIN_ID_PROFOLIO_LANDING_PAGE_URL_MAP: Record = { + [CHAIN_IDS.LINEA_MAINNET]: + 'https://portfolio.metamask.io/explore/networks/linea', +} as const; + export const INFURA_BLOCKED_KEY = 'countryBlocked'; const defaultEtherscanDomain = 'etherscan.io'; diff --git a/ui/components/multichain/network-list-item-menu/network-list-item-menu.js b/ui/components/multichain/network-list-item-menu/network-list-item-menu.js index 124be1999f1f..f4baa17bd7e4 100644 --- a/ui/components/multichain/network-list-item-menu/network-list-item-menu.js +++ b/ui/components/multichain/network-list-item-menu/network-list-item-menu.js @@ -18,6 +18,7 @@ export const NetworkListItemMenu = ({ onClose, onEditClick, onDeleteClick, + onDiscoverClick, isOpen, }) => { const t = useI18nContext(); @@ -38,6 +39,18 @@ export const NetworkListItemMenu = ({ > + {onDiscoverClick ? ( + { + e.stopPropagation(); + onDiscoverClick(); + }} + data-testid="network-list-item-options-discover" + > + {t('discover')} + + ) : null} {onEditClick ? ( void; onDeleteClick?: () => void; onEditClick?: () => void; + onDiscoverClick?: () => void; focus?: boolean; startAccessory?: ReactNode; endAccessory?: ReactNode; @@ -82,8 +90,8 @@ export const NetworkListItem = ({ }; const [networkOptionsMenuOpen, setNetworkOptionsMenuOpen] = useState(false); - const renderButton = () => { - return onDeleteClick || onEditClick ? ( + const renderButton = useCallback(() => { + return onDeleteClick || onEditClick || onDiscoverClick ? ( ) : null; - }; + }, [ + onDeleteClick, + onEditClick, + onDiscoverClick, + chainId, + t, + setNetworkListItemMenuRef, + setNetworkOptionsMenuOpen, + ]); useEffect(() => { if (networkRef.current && focus) { networkRef.current.focus(); @@ -217,6 +233,7 @@ export const NetworkListItem = ({ isOpen={networkOptionsMenuOpen} onDeleteClick={onDeleteClick} onEditClick={onEditClick} + onDiscoverClick={onDiscoverClick} onClose={() => setNetworkOptionsMenuOpen(false)} /> ) diff --git a/ui/components/multichain/network-list-menu/network-list-menu.test.js b/ui/components/multichain/network-list-menu/network-list-menu.test.js index e8039e0fa5fd..6d8bd5ba4e9e 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.test.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.test.js @@ -13,6 +13,7 @@ import { BNB_DISPLAY_NAME, LINEA_SEPOLIA_DISPLAY_NAME, } from '../../../../shared/constants/network'; +import { hexToDecimal } from '../../../../shared/modules/conversion.utils'; import { NetworkListMenu } from '.'; const mockSetShowTestNetworks = jest.fn(); @@ -46,6 +47,7 @@ const render = ({ selectedTabOriginInDomainsState = true, isAddingNewNetwork = false, editedNetwork = undefined, + nePortfolioDiscoverButton = false, } = {}) => { const state = { appState: { @@ -80,6 +82,8 @@ const render = ({ networkClientId: 'linea-mainnet', }, ], + portfolioDiscoverUrl: + 'https://portfolio.metamask.io/explore/networks/linea', }, '0x38': { nativeCurrency: 'BNB', @@ -148,6 +152,9 @@ const render = ({ ? { [origin]: selectedNetworkClientId } : {}), }, + remoteFeatureFlags: { + nePortfolioDiscoverButton, + }, }, activeTab: { origin: selectedTabOriginInDomainsState ? origin : undefined, @@ -261,7 +268,7 @@ describe('NetworkListMenu', () => { expect(queryByText('Add a custom network')).toBeEnabled(); }); - it('enables the "AAdd a custom network" button when MetaMask is true', () => { + it('enables the "Add a custom network" button when MetaMask is true', () => { const { queryByText } = render({ isUnlocked: true }); expect(queryByText('Add a custom network')).toBeEnabled(); }); @@ -273,6 +280,58 @@ describe('NetworkListMenu', () => { ).toHaveLength(0); }); + // For now, we only have Linea Mainnet enabled for the discover button. + it('enables the "Discover" button when the Feature Flag `nePortfolioDiscoverButton` is true and the network is supported', () => { + const { queryByTestId } = render({ + nePortfolioDiscoverButton: true, + }); + + const menuButton = queryByTestId( + `network-list-item-options-button-eip155:${hexToDecimal( + CHAIN_IDS.LINEA_MAINNET, + )}`, + ); + fireEvent.click(menuButton); + + expect( + queryByTestId('network-list-item-options-discover'), + ).toBeInTheDocument(); + }); + + it('disables the "Discover" button when the Feature Flag `nePortfolioDiscoverButton` is false even if the network is supported', () => { + const { queryByTestId } = render({ + nePortfolioDiscoverButton: false, + }); + + const menuButton = queryByTestId( + `network-list-item-options-button-eip155:${hexToDecimal( + CHAIN_IDS.LINEA_MAINNET, + )}`, + ); + fireEvent.click(menuButton); + + expect( + queryByTestId('network-list-item-options-discover'), + ).not.toBeInTheDocument(); + }); + + it('disables the "Discover" button when the network is not in the list of `CHAIN_ID_PROFOLIO_LANDING_PAGE_URL_MAP`', () => { + const { queryByTestId } = render({ + nePortfolioDiscoverButton: true, + }); + + const menuButton = queryByTestId( + `network-list-item-options-button-eip155:${hexToDecimal( + CHAIN_IDS.MAINNET, + )}`, + ); + fireEvent.click(menuButton); + + expect( + queryByTestId('network-list-item-options-discover'), + ).not.toBeInTheDocument(); + }); + describe('selectedTabOrigin is connected to wallet', () => { it('fires setNetworkClientIdForDomain when network item is clicked', () => { const { getByText } = render(); diff --git a/ui/components/multichain/network-list-menu/network-list-menu.tsx b/ui/components/multichain/network-list-menu/network-list-menu.tsx index 3f76a5f27ef8..7cc5f73f0bd0 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.tsx +++ b/ui/components/multichain/network-list-menu/network-list-menu.tsx @@ -22,6 +22,7 @@ import { import { type MultichainNetworkConfiguration } from '@metamask/multichain-network-controller'; import { type CaipChainId, + type Hex, parseCaipChainId, KnownCaipNamespace, } from '@metamask/utils'; @@ -47,6 +48,7 @@ import { import { FEATURED_RPCS, TEST_CHAINS, + CHAIN_ID_PROFOLIO_LANDING_PAGE_URL_MAP, } from '../../../../shared/constants/network'; import { MULTICHAIN_NETWORK_TO_NICKNAME } from '../../../../shared/constants/multichain/networks'; import { @@ -64,6 +66,7 @@ import { getPreferences, getMultichainNetworkConfigurationsByChainId, getSelectedMultichainNetworkChainId, + getIsPortfolioDiscoverButtonEnabled, getAllChainsToPoll, } from '../../../selectors'; import ToggleButton from '../../ui/toggle-button'; @@ -109,6 +112,7 @@ import { } from '../../../ducks/metamask/metamask'; import NetworksForm from '../../../pages/settings/networks-tab/networks-form'; import { useNetworkFormState } from '../../../pages/settings/networks-tab/networks-form/networks-form-state'; +import { openWindow } from '../../../helpers/utils/window'; import PopularNetworkList from './popular-network-list/popular-network-list'; import NetworkListSearch from './network-list-search/network-list-search'; import AddRpcUrlModal from './add-rpc-url-modal/add-rpc-url-modal'; @@ -148,6 +152,11 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { const completedOnboarding = useSelector(getCompletedOnboarding); const onboardedInThisUISession = useSelector(getOnboardedInThisUISession); const showNetworkBanner = useSelector(getShowNetworkBanner); + // This selector provides the indication if the "Discover" button + // is enabled based on the remote feature flag. + const isPortfolioDiscoverButtonEnabled = useSelector( + getIsPortfolioDiscoverButtonEnabled, + ); // This selector provides an array with two elements. // 1 - All network configurations including EVM and non-EVM with the data type // MultichainNetworkConfiguration from @metamask/multichain-network-controller @@ -382,6 +391,18 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { }); }; + const isDiscoverBtnEnabled = useCallback( + (hexChainId: Hex): boolean => { + // For now, the "Discover" button should be enabled only for Linea network base on + // the feature flag and the constants `CHAIN_ID_PROFOLIO_LANDING_PAGE_URL_MAP`. + return ( + isPortfolioDiscoverButtonEnabled && + CHAIN_ID_PROFOLIO_LANDING_PAGE_URL_MAP[hexChainId] !== undefined + ); + }, + [isPortfolioDiscoverButtonEnabled], + ); + const hasMultiRpcOptions = useCallback( (network: MultichainNetworkConfiguration): boolean => network.isEvm && @@ -435,6 +456,14 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { ); setActionMode(ACTION_MODE.ADD_EDIT); }, + onDiscoverClick: isDiscoverBtnEnabled(hexChainId) + ? () => { + openWindow( + CHAIN_ID_PROFOLIO_LANDING_PAGE_URL_MAP[hexChainId], + '_blank', + ); + } + : undefined, onRpcConfigEdit: hasMultiRpcOptions(network) ? () => { setActionMode(ACTION_MODE.SELECT_RPC); @@ -447,7 +476,13 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { : undefined, }; }, - [currentChainId, dispatch, hasMultiRpcOptions, isUnlocked], + [ + currentChainId, + dispatch, + hasMultiRpcOptions, + isUnlocked, + isDiscoverBtnEnabled, + ], ); // Renders a network in the network list @@ -456,7 +491,8 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { ) => { const { chainId } = network; const isCurrentNetwork = chainId === currentChainId; - const { onDelete, onEdit, onRpcConfigEdit } = getItemCallbacks(network); + const { onDelete, onEdit, onDiscoverClick, onRpcConfigEdit } = + getItemCallbacks(network); const iconSrc = getNetworkIcon(network); return ( @@ -478,6 +514,7 @@ export const NetworkListMenu = ({ onClose }: { onClose: () => void }) => { }} onDeleteClick={onDelete} onEditClick={onEdit} + onDiscoverClick={onDiscoverClick} onRpcEndpointClick={onRpcConfigEdit} disabled={!isNetworkEnabled(network)} /> diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index ef69a41003f4..568424ebaab2 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2880,6 +2880,18 @@ export function getIsCustomNetwork(state) { return !CHAIN_ID_TO_RPC_URL_MAP[chainId]; } +/** + * Get the state of the `nePortfolioDiscoverButton` remote feature flag. + * This flag determines whether the user should see a `Discover` button on the network menu list. + * + * @param {*} state + * @returns The state of the `nePortfolioDiscoverButton` remote feature flag. + */ +export function getIsPortfolioDiscoverButtonEnabled(state) { + const { nePortfolioDiscoverButton } = getRemoteFeatureFlags(state); + return Boolean(nePortfolioDiscoverButton); +} + export function getBlockExplorerLinkText( state, accountDetailsModalComponent = false,