diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 5770bc805..cf2ebbab5 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -1967,6 +1967,9 @@ "lock": { "message": "Lock" }, + "lockMetaMask": { + "message": "Lock MetaMask" + }, "lockTimeTooGreat": { "message": "Lock time is too great" }, @@ -2949,6 +2952,9 @@ "portfolio": { "message": "Portfolio" }, + "portfolioView": { + "message": "Portfolio view" + }, "preferredLedgerConnectionType": { "message": "Preferred Ledger connection type", "description": "A header for a dropdown in Settings > Advanced. Appears above the ledgerConnectionPreferenceDescription message" diff --git a/ui/components/app/menu-bar/menu-bar.js b/ui/components/app/menu-bar/menu-bar.js index 548d3e7e1..bf80cad5d 100644 --- a/ui/components/app/menu-bar/menu-bar.js +++ b/ui/components/app/menu-bar/menu-bar.js @@ -12,6 +12,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { getOriginOfCurrentTab } from '../../../selectors'; import { MetaMetricsContext } from '../../../contexts/metametrics'; import { ButtonIcon, ICON_NAMES } from '../../component-library'; +import { GlobalMenu } from '../../multichain/global-menu'; import AccountOptionsMenu from './account-options-menu'; export default function MenuBar() { @@ -53,12 +54,18 @@ export default function MenuBar() { }} /> - {accountOptionsMenuOpen ? ( - setAccountOptionsMenuOpen(false)} - /> - ) : null} + {accountOptionsMenuOpen && + (process.env.MULTICHAIN ? ( + setAccountOptionsMenuOpen(false)} + /> + ) : ( + setAccountOptionsMenuOpen(false)} + /> + ))} ); } diff --git a/ui/components/multichain/global-menu/global-menu.js b/ui/components/multichain/global-menu/global-menu.js new file mode 100644 index 000000000..4c2f738b9 --- /dev/null +++ b/ui/components/multichain/global-menu/global-menu.js @@ -0,0 +1,155 @@ +import React, { useContext } from 'react'; +import PropTypes from 'prop-types'; +import { useHistory } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { + CONNECTED_ROUTE, + SETTINGS_ROUTE, + DEFAULT_ROUTE, +} from '../../../helpers/constants/routes'; +import { lockMetamask } from '../../../store/actions'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { ICON_NAMES } from '../../component-library'; +import { Menu, MenuItem } from '../../ui/menu'; +import { getEnvironmentType } from '../../../../app/scripts/lib/util'; +import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; +import { SUPPORT_LINK } from '../../../../shared/lib/ui-utils'; + +import { MetaMetricsContext } from '../../../contexts/metametrics'; +import { + EVENT_NAMES, + EVENT, + CONTEXT_PROPS, +} from '../../../../shared/constants/metametrics'; + +export const GlobalMenu = ({ closeMenu, anchorElement }) => { + const t = useI18nContext(); + const dispatch = useDispatch(); + const trackEvent = useContext(MetaMetricsContext); + const history = useHistory(); + + return ( + + { + history.push(CONNECTED_ROUTE); + trackEvent({ + event: EVENT_NAMES.NAV_CONNECTED_SITES_OPENED, + category: EVENT.CATEGORIES.NAVIGATION, + properties: { + location: 'Account Options', + }, + }); + closeMenu(); + }} + > + {t('connectedSites')} + + { + const portfolioUrl = process.env.PORTFOLIO_URL; + global.platform.openTab({ + url: `${portfolioUrl}?metamaskEntry=ext`, + }); + trackEvent( + { + category: EVENT.CATEGORIES.HOME, + event: EVENT_NAMES.PORTFOLIO_LINK_CLICKED, + properties: { + url: portfolioUrl, + }, + }, + { + contextPropsIntoEventProperties: [CONTEXT_PROPS.PAGE_TITLE], + }, + ); + closeMenu(); + }} + data-testid="global-menu-portfolio" + > + {t('portfolioView')} + + {getEnvironmentType() === ENVIRONMENT_TYPE_FULLSCREEN ? null : ( + { + global.platform.openExtensionInBrowser(); + trackEvent({ + event: EVENT_NAMES.APP_WINDOW_EXPANDED, + category: EVENT.CATEGORIES.NAVIGATION, + properties: { + location: 'Account Options', + }, + }); + closeMenu(); + }} + data-testid="global-menu-expand" + > + {t('expandView')} + + )} + { + global.platform.openTab({ url: SUPPORT_LINK }); + trackEvent( + { + category: EVENT.CATEGORIES.HOME, + event: EVENT_NAMES.SUPPORT_LINK_CLICKED, + properties: { + url: SUPPORT_LINK, + }, + }, + { + contextPropsIntoEventProperties: [CONTEXT_PROPS.PAGE_TITLE], + }, + ); + closeMenu(); + }} + data-testid="global-menu-support" + > + {t('support')} + + { + history.push(SETTINGS_ROUTE); + trackEvent({ + category: EVENT.CATEGORIES.NAVIGATION, + event: EVENT_NAMES.NAV_SETTINGS_OPENED, + properties: { + location: 'Main Menu', + }, + }); + closeMenu(); + }} + > + {t('settings')} + + { + dispatch(lockMetamask()); + history.push(DEFAULT_ROUTE); + closeMenu(); + }} + data-testid="global-menu-lock" + > + {t('lockMetaMask')} + + + ); +}; + +GlobalMenu.propTypes = { + /** + * The element that the menu should display next to + */ + anchorElement: PropTypes.instanceOf(window.Element), + /** + * Function that closes this menu + */ + closeMenu: PropTypes.func.isRequired, +}; diff --git a/ui/components/multichain/global-menu/global-menu.stories.js b/ui/components/multichain/global-menu/global-menu.stories.js new file mode 100644 index 000000000..6206b21d1 --- /dev/null +++ b/ui/components/multichain/global-menu/global-menu.stories.js @@ -0,0 +1,22 @@ +import React from 'react'; +import { GlobalMenu } from '.'; + +export default { + title: 'Components/Multichain/GlobalMenu', + component: GlobalMenu, + argTypes: { + closeMenu: { + action: 'closeMenu', + }, + anchorElement: { + control: 'func', + }, + }, + args: { + closeMenu: () => console.log('Closing menu!'), + anchorElement: null, + }, +}; + +export const DefaultStory = (args) => ; +DefaultStory.storyName = 'Default'; diff --git a/ui/components/multichain/global-menu/global-menu.test.js b/ui/components/multichain/global-menu/global-menu.test.js new file mode 100644 index 000000000..f3ed60fd3 --- /dev/null +++ b/ui/components/multichain/global-menu/global-menu.test.js @@ -0,0 +1,69 @@ +import React from 'react'; +import { renderWithProvider, fireEvent, waitFor } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import { SUPPORT_LINK } from '../../../../shared/lib/ui-utils'; +import { GlobalMenu } from '.'; + +const render = () => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + }, + }); + return renderWithProvider( + undefined} />, + store, + ); +}; + +const mockLockMetaMask = jest.fn(); +jest.mock('../../../store/actions', () => ({ + lockMetamask: () => mockLockMetaMask, +})); + +describe('AccountListItem', () => { + it('locks MetaMask when item is clicked', async () => { + render(); + fireEvent.click(document.querySelector('[data-testid="global-menu-lock"]')); + await waitFor(() => { + expect(mockLockMetaMask).toHaveBeenCalled(); + }); + }); + + it('opens the portfolio site when item is clicked', async () => { + global.platform = { openTab: jest.fn() }; + + const { getByTestId } = render(); + fireEvent.click(getByTestId('global-menu-portfolio')); + await waitFor(() => { + expect(global.platform.openTab).toHaveBeenCalledWith({ + url: `${process.env.PORTFOLIO_URL}?metamaskEntry=ext`, + }); + }); + }); + + it('opens the support site when item is clicked', async () => { + global.platform = { openTab: jest.fn() }; + + const { getByTestId } = render(); + fireEvent.click(getByTestId('global-menu-support')); + await waitFor(() => { + expect(global.platform.openTab).toHaveBeenCalledWith({ + url: SUPPORT_LINK, + }); + }); + }); + + it('expands metamask to tab when item is clicked', async () => { + global.platform = { openExtensionInBrowser: jest.fn() }; + + render(); + fireEvent.click( + document.querySelector('[data-testid="global-menu-expand"]'), + ); + await waitFor(() => { + expect(global.platform.openExtensionInBrowser).toHaveBeenCalled(); + }); + }); +}); diff --git a/ui/components/multichain/global-menu/index.js b/ui/components/multichain/global-menu/index.js new file mode 100644 index 000000000..4850b1f92 --- /dev/null +++ b/ui/components/multichain/global-menu/index.js @@ -0,0 +1 @@ +export { GlobalMenu } from './global-menu'; diff --git a/ui/components/multichain/index.js b/ui/components/multichain/index.js index d34e42751..29562dc83 100644 --- a/ui/components/multichain/index.js +++ b/ui/components/multichain/index.js @@ -2,5 +2,6 @@ export { AccountListItem } from './account-list-item'; export { AccountListItemMenu } from './account-list-item-menu'; export { AccountListMenu } from './account-list-menu'; export { DetectedTokensBanner } from './detected-token-banner'; +export { GlobalMenu } from './global-menu'; export { MultichainImportTokenLink } from './multichain-import-token-link'; export { MultichainTokenListItem } from './multichain-token-list-item';