1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 03:12:42 +02:00

Fix #20006 - Add Address Details and View on Explorer to Global Menu (#20013)

* Fix #20006 - Add Address Details and View on Explorer to Global Menu

* Fix tests
This commit is contained in:
David Walsh 2023-07-18 17:01:07 -05:00 committed by Dan J Miller
parent d8c49d9a99
commit 78a0587c97
13 changed files with 261 additions and 114 deletions

View File

@ -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',

View File

@ -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 {
Text,
@ -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 = ({
>
<ModalFocus restoreFocus initialFocusRef={anchorElement}>
<div onKeyDown={handleKeyDown} ref={popoverDialogRef}>
<MenuItem
onClick={() => {
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"
>
<Text variant={TextVariant.bodySm}>{t('viewOnExplorer')}</Text>
</MenuItem>
<MenuItem
ref={accountDetailsItemRef}
onClick={() => {
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"
>
<Text variant={TextVariant.bodySm}>{t('accountDetails')}</Text>
</MenuItem>
<AccountDetailsMenuItem
metricsLocation={METRICS_LOCATION}
closeMenu={closeMenu}
address={identity.address}
textProps={{ variant: TextVariant.bodySm }}
/>
<ViewExplorerMenuItem
metricsLocation={METRICS_LOCATION}
closeMenu={closeMenu}
textProps={{ variant: TextVariant.bodySm }}
/>
{isRemovable ? (
<MenuItem
ref={removeAccountItemRef}
@ -284,10 +225,6 @@ AccountListItemMenu.propTypes = {
* Function that closes the menu
*/
closeMenu: PropTypes.func,
/**
* Domain of the block explorer
*/
blockExplorerUrlSubTitle: PropTypes.string,
/**
* Represents if the account should be removable
*/

View File

@ -14,9 +14,6 @@ export default {
closeMenu: {
action: 'closeMenu',
},
blockExplorerUrlSubTitle: {
control: 'text',
},
isRemovable: {
control: 'boolean',
},
@ -36,7 +33,6 @@ export default {
tokenBalance: '32.09 ETH',
},
isRemovable: true,
blockExplorerUrlSubTitle: 'etherscan.io',
isOpen: true,
},
};

View File

@ -1,6 +1,6 @@
/* eslint-disable jest/require-top-level-describe */
import React from 'react';
import { renderWithProvider, fireEvent } from '../../../../test/jest';
import { renderWithProvider } from '../../../../test/jest';
import configureStore from '../../../store/store';
import mockState from '../../../../test/data/mock-state.json';
import { AccountListItemMenu } from '.';
@ -31,23 +31,6 @@ const render = (props = {}) => {
};
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();

View File

@ -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 = ({
/>
<AccountListItemMenu
anchorElement={accountListItemMenuElement}
blockExplorerUrlSubTitle={blockExplorerUrlSubTitle}
identity={identity}
onClose={() => setAccountOptionsMenuOpen(false)}
isOpen={accountOptionsMenuOpen}

View File

@ -41,6 +41,7 @@ import {
///: END:ONLY_INCLUDE_IN
import {
getMetaMetricsId,
getSelectedAddress,
///: BEGIN:ONLY_INCLUDE_IN(snaps)
getUnreadNotificationsCount,
///: END:ONLY_INCLUDE_IN
@ -56,6 +57,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();
@ -63,6 +67,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,
@ -85,6 +90,15 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => {
return (
<Menu anchorElement={anchorElement} onHide={closeMenu}>
<AccountDetailsMenuItem
metricsLocation={METRICS_LOCATION}
closeMenu={closeMenu}
address={address}
/>
<ViewExplorerMenuItem
metricsLocation={METRICS_LOCATION}
closeMenu={closeMenu}
/>
<MenuItem
iconName={IconName.Connect}
disabled={hasUnapprovedTransactions}
@ -94,7 +108,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => {
event: MetaMetricsEventName.NavConnectedSitesOpened,
category: MetaMetricsEventCategory.Navigation,
properties: {
location: 'Global Menu',
location: METRICS_LOCATION,
},
});
closeMenu();
@ -140,7 +154,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => {
event: MetaMetricsEventName.PortfolioLinkClicked,
properties: {
url: portfolioUrl,
location: 'Global Menu',
location: METRICS_LOCATION,
},
},
{
@ -167,7 +181,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => {
event: MetaMetricsEventName.AppWindowExpanded,
category: MetaMetricsEventCategory.Navigation,
properties: {
location: 'Global Menu',
location: METRICS_LOCATION,
},
});
closeMenu();
@ -225,7 +239,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => {
event: MetaMetricsEventName.SupportLinkClicked,
properties: {
url: supportLink,
location: 'Global Menu',
location: METRICS_LOCATION,
},
},
{
@ -249,7 +263,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => {
category: MetaMetricsEventCategory.Navigation,
event: MetaMetricsEventName.NavSettingsOpened,
properties: {
location: 'Global Menu',
location: METRICS_LOCATION,
},
});
closeMenu();
@ -267,7 +281,7 @@ export const GlobalMenu = ({ closeMenu, anchorElement }) => {
category: MetaMetricsEventCategory.Navigation,
event: MetaMetricsEventName.AppLocked,
properties: {
location: 'Global Menu',
location: METRICS_LOCATION,
},
});
closeMenu();

View File

@ -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', () => {

View File

@ -15,3 +15,4 @@ export { ProductTour } from './product-tour-popover';
export { AccountDetails } from './account-details';
export { CreateAccount } from './create-account';
export { ImportAccount } from './import-account';
export { AccountDetailsMenuItem, ViewExplorerMenuItem } from './menu-items';

View File

@ -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 (
<MenuItem
onClick={() => {
dispatch(setAccountDetailsAddress(address));
trackEvent({
event: MetaMetricsEventName.NavAccountDetailsOpened,
category: MetaMetricsEventCategory.Navigation,
properties: {
location: metricsLocation,
},
});
closeMenu?.();
}}
iconName={IconName.ScanBarcode}
data-testid="account-list-menu-details"
>
{textProps ? <Text {...textProps}>{LABEL}</Text> : LABEL}
</MenuItem>
);
};
AccountDetailsMenuItem.propTypes = {
metricsLocation: PropTypes.string.isRequired,
closeMenu: PropTypes.func,
address: PropTypes.string.isRequired,
textProps: PropTypes.object,
};

View File

@ -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(
<AccountDetailsMenuItem
metricsLocation="Global Menu"
address={mockState.metamask.selectedAddress}
closeMenu={jest.fn()}
/>,
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,
);
});
});

View File

@ -0,0 +1,2 @@
export { AccountDetailsMenuItem } from './account-details-menu-item';
export { ViewExplorerMenuItem } from './view-explorer-menu-item';

View File

@ -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 (
<MenuItem
onClick={() => {
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 ? <Text {...textProps}>{LABEL}</Text> : LABEL}
</MenuItem>
);
};
ViewExplorerMenuItem.propTypes = {
metricsLocation: PropTypes.string.isRequired,
closeMenu: PropTypes.func,
textProps: PropTypes.object,
};

View File

@ -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(
<ViewExplorerMenuItem
metricsLocation="Global Menu"
closeMenu={jest.fn()}
/>,
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();
});
});