diff --git a/test/e2e/tests/permissions.spec.js b/test/e2e/tests/permissions.spec.js index 648e0552e..be4fa78e7 100644 --- a/test/e2e/tests/permissions.spec.js +++ b/test/e2e/tests/permissions.spec.js @@ -55,7 +55,7 @@ describe('Permissions', function () { await driver.clickElement( '[data-testid="account-options-menu-button"]', ); - await driver.clickElement('.menu-item'); + await driver.clickElement('.menu-item:nth-of-type(3)'); await driver.findElement({ text: 'Connected sites', diff --git a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js index d066dcc7f..e3e2eeec4 100644 --- a/ui/components/multichain/account-list-item-menu/account-list-item-menu.js +++ b/ui/components/multichain/account-list-item-menu/account-list-item-menu.js @@ -1,16 +1,12 @@ import React, { useContext, useRef, useEffect } from 'react'; import { useDispatch, useSelector } from 'react-redux'; -import { useHistory } from 'react-router-dom'; import PropTypes from 'prop-types'; -import { getAccountLink } from '@metamask/etherscan-link'; ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) import { mmiActionsFactory } from '../../../store/institutional/institution-background'; ///: END:ONLY_INCLUDE_IN import { MetaMetricsContext } from '../../../contexts/metametrics'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { - getRpcPrefsForCurrentProvider, - getBlockExplorerLinkText, getCurrentChainId, getHardwareWalletType, getAccountTypeForKeyring, @@ -22,7 +18,6 @@ import { import { toChecksumHexAddress } from '../../../../shared/modules/hexstring-utils'; ///: END:ONLY_INCLUDE_IN import { findKeyringForAddress } from '../../../ducks/metamask/metamask'; -import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; import { MenuItem } from '../../ui/menu'; import { IconName, @@ -34,17 +29,17 @@ import { } from '../../component-library'; import { MetaMetricsEventCategory, - MetaMetricsEventLinkType, MetaMetricsEventName, } from '../../../../shared/constants/metametrics'; -import { getURLHostName } from '../../../helpers/utils/util'; -import { setAccountDetailsAddress, showModal } from '../../../store/actions'; +import { showModal } from '../../../store/actions'; import { TextVariant } from '../../../helpers/constants/design-system'; import { formatAccountType } from '../../../helpers/utils/metrics'; +import { AccountDetailsMenuItem, ViewExplorerMenuItem } from '..'; + +const METRICS_LOCATION = 'Account Options'; export const AccountListItemMenu = ({ anchorElement, - blockExplorerUrlSubTitle, onClose, closeMenu, isRemovable, @@ -54,11 +49,8 @@ export const AccountListItemMenu = ({ const t = useI18nContext(); const trackEvent = useContext(MetaMetricsContext); const dispatch = useDispatch(); - const history = useHistory(); const chainId = useSelector(getCurrentChainId); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - const addressLink = getAccountLink(identity.address, chainId, rpcPrefs); const deviceName = useSelector(getHardwareWalletType); @@ -67,28 +59,6 @@ export const AccountListItemMenu = ({ ); const accountType = formatAccountType(getAccountTypeForKeyring(keyring)); - const blockExplorerLinkText = useSelector(getBlockExplorerLinkText); - const openBlockExplorer = () => { - trackEvent({ - event: MetaMetricsEventName.ExternalLinkClicked, - category: MetaMetricsEventCategory.Navigation, - properties: { - link_type: MetaMetricsEventLinkType.AccountTracker, - location: 'Account Options', - url_domain: getURLHostName(addressLink), - }, - }); - - global.platform.openTab({ - url: addressLink, - }); - onClose(); - }; - - const routeToAddBlockExplorerUrl = () => { - history.push(`${NETWORKS_ROUTE}#blockExplorerUrl`); - }; - ///: BEGIN:ONLY_INCLUDE_IN(build-mmi) const isCustodial = keyring?.type ? /Custody/u.test(keyring.type) : false; const accounts = useSelector(getMetaMaskAccountsOrdered); @@ -158,46 +128,17 @@ export const AccountListItemMenu = ({ >
- { - blockExplorerLinkText.firstPart === 'addBlockExplorer' - ? routeToAddBlockExplorerUrl() - : openBlockExplorer(); - - trackEvent({ - event: MetaMetricsEventName.BlockExplorerLinkClicked, - category: MetaMetricsEventCategory.Accounts, - properties: { - location: 'Account Options', - chain_id: chainId, - }, - }); - }} - subtitle={blockExplorerUrlSubTitle || null} - iconName={IconName.Export} - data-testid="account-list-menu-open-explorer" - > - {t('viewOnExplorer')} - - { - dispatch(setAccountDetailsAddress(identity.address)); - trackEvent({ - event: MetaMetricsEventName.NavAccountDetailsOpened, - category: MetaMetricsEventCategory.Navigation, - properties: { - location: 'Account Options', - }, - }); - onClose(); - closeMenu?.(); - }} - iconName={IconName.ScanBarcode} - data-testid="account-list-menu-details" - > - {t('accountDetails')} - + + {isRemovable ? ( { }; describe('AccountListItem', () => { - it('renders the URL for explorer', () => { - const blockExplorerDomain = 'etherscan.io'; - const { getByText, getByTestId } = render({ - blockExplorerUrlSubTitle: blockExplorerDomain, - }); - expect(getByText(blockExplorerDomain)).toBeInTheDocument(); - - Object.defineProperty(global, 'platform', { - value: { - openTab: jest.fn(), - }, - }); - const openExplorerTabSpy = jest.spyOn(global.platform, 'openTab'); - fireEvent.click(getByTestId('account-list-menu-open-explorer')); - expect(openExplorerTabSpy).toHaveBeenCalled(); - }); - it('renders remove icon with isRemovable', () => { const { getByTestId } = render({ isRemovable: true }); expect(getByTestId('account-list-menu-remove')).toBeInTheDocument(); diff --git a/ui/components/multichain/account-list-item/account-list-item.js b/ui/components/multichain/account-list-item/account-list-item.js index becb5f2b7..1939e0d41 100644 --- a/ui/components/multichain/account-list-item/account-list-item.js +++ b/ui/components/multichain/account-list-item/account-list-item.js @@ -4,8 +4,7 @@ import classnames from 'classnames'; import { useSelector } from 'react-redux'; import { useI18nContext } from '../../../hooks/useI18nContext'; -import { getRpcPrefsForCurrentProvider } from '../../../selectors'; -import { getURLHostName, shortenAddress } from '../../../helpers/utils/util'; +import { shortenAddress } from '../../../helpers/utils/util'; import { AccountListItemMenu } from '..'; import { @@ -92,10 +91,6 @@ export const AccountListItem = ({ ); const label = getLabel(keyring, t); - const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); - const { blockExplorerUrl } = rpcPrefs; - const blockExplorerUrlSubTitle = getURLHostName(blockExplorerUrl); - const trackEvent = useContext(MetaMetricsContext); return ( @@ -250,7 +245,6 @@ export const AccountListItem = ({ /> setAccountOptionsMenuOpen(false)} isOpen={accountOptionsMenuOpen} diff --git a/ui/components/multichain/global-menu/global-menu.js b/ui/components/multichain/global-menu/global-menu.js index 1de3222ac..908057f6a 100644 --- a/ui/components/multichain/global-menu/global-menu.js +++ b/ui/components/multichain/global-menu/global-menu.js @@ -42,6 +42,7 @@ import { ///: END:ONLY_INCLUDE_IN import { getMetaMetricsId, + getSelectedAddress, ///: BEGIN:ONLY_INCLUDE_IN(snaps) getUnreadNotificationsCount, ///: END:ONLY_INCLUDE_IN @@ -57,6 +58,9 @@ import { TextVariant, } from '../../../helpers/constants/design-system'; ///: END:ONLY_INCLUDE_IN +import { AccountDetailsMenuItem, ViewExplorerMenuItem } from '..'; + +const METRICS_LOCATION = 'Global Menu'; export const GlobalMenu = ({ closeMenu, anchorElement }) => { const t = useI18nContext(); @@ -64,6 +68,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { const trackEvent = useContext(MetaMetricsContext); const history = useHistory(); const metaMetricsId = useSelector(getMetaMetricsId); + const address = useSelector(getSelectedAddress); const hasUnapprovedTransactions = useSelector( (state) => Object.keys(state.metamask.unapprovedTxs).length > 0, @@ -86,6 +91,15 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { return ( + + { event: MetaMetricsEventName.NavConnectedSitesOpened, category: MetaMetricsEventCategory.Navigation, properties: { - location: 'Global Menu', + location: METRICS_LOCATION, }, }); closeMenu(); @@ -141,7 +155,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { event: MetaMetricsEventName.PortfolioLinkClicked, properties: { url: portfolioUrl, - location: 'Global Menu', + location: METRICS_LOCATION, }, }, { @@ -168,7 +182,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { event: MetaMetricsEventName.AppWindowExpanded, category: MetaMetricsEventCategory.Navigation, properties: { - location: 'Global Menu', + location: METRICS_LOCATION, }, }); closeMenu(); @@ -226,7 +240,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { event: MetaMetricsEventName.SupportLinkClicked, properties: { url: supportLink, - location: 'Global Menu', + location: METRICS_LOCATION, }, }, { @@ -250,7 +264,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { category: MetaMetricsEventCategory.Navigation, event: MetaMetricsEventName.NavSettingsOpened, properties: { - location: 'Global Menu', + location: METRICS_LOCATION, }, }); closeMenu(); @@ -268,7 +282,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => { category: MetaMetricsEventCategory.Navigation, event: MetaMetricsEventName.AppLocked, properties: { - location: 'Global Menu', + location: METRICS_LOCATION, }, }); closeMenu(); diff --git a/ui/components/multichain/global-menu/global-menu.test.js b/ui/components/multichain/global-menu/global-menu.test.js index a2fbb8c5f..0f635c968 100644 --- a/ui/components/multichain/global-menu/global-menu.test.js +++ b/ui/components/multichain/global-menu/global-menu.test.js @@ -18,8 +18,10 @@ const render = (metamaskStateChanges = {}) => { }; const mockLockMetaMask = jest.fn(); +const mockSetAccountDetailsAddress = jest.fn(); jest.mock('../../../store/actions', () => ({ lockMetamask: () => mockLockMetaMask, + setAccountDetailsAddress: () => mockSetAccountDetailsAddress, })); describe('AccountListItem', () => { diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js index 36183e015..95a9b191b 100644 --- a/ui/components/multichain/index.js +++ b/ui/components/multichain/index.js @@ -17,3 +17,4 @@ export { AccountDetails } from './account-details'; export { CreateAccount } from './create-account'; export { ImportAccount } from './import-account'; export { ImportNftsModal } from './import-nfts-modal'; +export { AccountDetailsMenuItem, ViewExplorerMenuItem } from './menu-items'; diff --git a/ui/components/multichain/menu-items/account-details-menu-item.js b/ui/components/multichain/menu-items/account-details-menu-item.js new file mode 100644 index 000000000..a5526e9cb --- /dev/null +++ b/ui/components/multichain/menu-items/account-details-menu-item.js @@ -0,0 +1,54 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { useDispatch } from 'react-redux'; + +import { setAccountDetailsAddress } from '../../../store/actions'; + +import { MenuItem } from '../../ui/menu'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { IconName, Text } from '../../component-library'; + +export const AccountDetailsMenuItem = ({ + metricsLocation, + closeMenu, + address, + textProps, +}) => { + const t = useI18nContext(); + const dispatch = useDispatch(); + const trackEvent = useContext(MetaMetricsContext); + + const LABEL = t('accountDetails'); + + return ( + { + dispatch(setAccountDetailsAddress(address)); + trackEvent({ + event: MetaMetricsEventName.NavAccountDetailsOpened, + category: MetaMetricsEventCategory.Navigation, + properties: { + location: metricsLocation, + }, + }); + closeMenu?.(); + }} + iconName={IconName.ScanBarcode} + data-testid="account-list-menu-details" + > + {textProps ? {LABEL} : LABEL} + + ); +}; + +AccountDetailsMenuItem.propTypes = { + metricsLocation: PropTypes.string.isRequired, + closeMenu: PropTypes.func, + address: PropTypes.string.isRequired, + textProps: PropTypes.object, +}; diff --git a/ui/components/multichain/menu-items/account-details-menu-item.test.js b/ui/components/multichain/menu-items/account-details-menu-item.test.js new file mode 100644 index 000000000..f13c27d63 --- /dev/null +++ b/ui/components/multichain/menu-items/account-details-menu-item.test.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { renderWithProvider, fireEvent } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import * as actions from '../../../store/actions'; +import { AccountDetailsMenuItem } from '.'; + +const render = () => { + const store = configureStore(mockState); + return renderWithProvider( + , + store, + ); +}; + +jest.mock('../../../store/actions', () => ({ + ...jest.requireActual('../../../store/actions.ts'), + setAccountDetailsAddress: jest.fn().mockReturnValue({ type: 'TYPE' }), +})); + +describe('AccountDetailsMenuItem', () => { + it('opens the Account Details modal with the correct address', () => { + global.platform = { openTab: jest.fn() }; + + const { getByText, getByTestId } = render(); + expect(getByText('Account details')).toBeInTheDocument(); + + fireEvent.click(getByTestId('account-list-menu-details')); + + expect(actions.setAccountDetailsAddress).toHaveBeenCalledWith( + mockState.metamask.selectedAddress, + ); + }); +}); diff --git a/ui/components/multichain/menu-items/index.js b/ui/components/multichain/menu-items/index.js new file mode 100644 index 000000000..f1c9632d5 --- /dev/null +++ b/ui/components/multichain/menu-items/index.js @@ -0,0 +1,2 @@ +export { AccountDetailsMenuItem } from './account-details-menu-item'; +export { ViewExplorerMenuItem } from './view-explorer-menu-item'; diff --git a/ui/components/multichain/menu-items/view-explorer-menu-item.js b/ui/components/multichain/menu-items/view-explorer-menu-item.js new file mode 100644 index 000000000..534090be7 --- /dev/null +++ b/ui/components/multichain/menu-items/view-explorer-menu-item.js @@ -0,0 +1,97 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; + +import { getAccountLink } from '@metamask/etherscan-link'; + +import { MenuItem } from '../../ui/menu'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + MetaMetricsEventCategory, + MetaMetricsEventLinkType, + MetaMetricsEventName, +} from '../../../../shared/constants/metametrics'; +import { IconName, Text } from '../../component-library'; +import { + getBlockExplorerLinkText, + getCurrentChainId, + getRpcPrefsForCurrentProvider, + getSelectedAddress, +} from '../../../selectors'; +import { getURLHostName } from '../../../helpers/utils/util'; +import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; + +export const ViewExplorerMenuItem = ({ + metricsLocation, + closeMenu, + textProps, +}) => { + const t = useI18nContext(); + const trackEvent = useContext(MetaMetricsContext); + const history = useHistory(); + + const currentAddress = useSelector(getSelectedAddress); + const chainId = useSelector(getCurrentChainId); + const rpcPrefs = useSelector(getRpcPrefsForCurrentProvider); + const addressLink = getAccountLink(currentAddress, chainId, rpcPrefs); + + const { blockExplorerUrl } = rpcPrefs; + const blockExplorerUrlSubTitle = getURLHostName(blockExplorerUrl); + const blockExplorerLinkText = useSelector(getBlockExplorerLinkText); + const openBlockExplorer = () => { + trackEvent({ + event: MetaMetricsEventName.ExternalLinkClicked, + category: MetaMetricsEventCategory.Navigation, + properties: { + link_type: MetaMetricsEventLinkType.AccountTracker, + location: metricsLocation, + url_domain: getURLHostName(addressLink), + }, + }); + + global.platform.openTab({ + url: addressLink, + }); + closeMenu(); + }; + + const routeToAddBlockExplorerUrl = () => { + history.push(`${NETWORKS_ROUTE}#blockExplorerUrl`); + }; + + const LABEL = t('viewOnExplorer'); + + return ( + { + blockExplorerLinkText.firstPart === 'addBlockExplorer' + ? routeToAddBlockExplorerUrl() + : openBlockExplorer(); + + trackEvent({ + event: MetaMetricsEventName.BlockExplorerLinkClicked, + category: MetaMetricsEventCategory.Accounts, + properties: { + location: metricsLocation, + chain_id: chainId, + }, + }); + + closeMenu?.(); + }} + subtitle={blockExplorerUrlSubTitle || null} + iconName={IconName.Export} + data-testid="account-list-menu-open-explorer" + > + {textProps ? {LABEL} : LABEL} + + ); +}; + +ViewExplorerMenuItem.propTypes = { + metricsLocation: PropTypes.string.isRequired, + closeMenu: PropTypes.func, + textProps: PropTypes.object, +}; diff --git a/ui/components/multichain/menu-items/view-explorer-menu-item.test.js b/ui/components/multichain/menu-items/view-explorer-menu-item.test.js new file mode 100644 index 000000000..58fd8b115 --- /dev/null +++ b/ui/components/multichain/menu-items/view-explorer-menu-item.test.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { renderWithProvider, fireEvent } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import { ViewExplorerMenuItem } from '.'; + +const render = () => { + const store = configureStore(mockState); + return renderWithProvider( + , + store, + ); +}; + +describe('ViewExplorerMenuItem', () => { + it('renders "View on explorer"', () => { + global.platform = { openTab: jest.fn() }; + + const { getByText, getByTestId } = render(); + expect(getByText('View on explorer')).toBeInTheDocument(); + + const openExplorerTabSpy = jest.spyOn(global.platform, 'openTab'); + fireEvent.click(getByTestId('account-list-menu-open-explorer')); + expect(openExplorerTabSpy).toHaveBeenCalled(); + }); +});