diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 2d5151465..f26eb8c03 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -121,6 +121,9 @@ "addAlias": { "message": "Add alias" }, + "addBlockExplorer": { + "message": "Add a block explorer" + }, "addContact": { "message": "Add contact" }, diff --git a/ui/components/app/menu-bar/account-options-menu.js b/ui/components/app/menu-bar/account-options-menu.js index f712631b4..2d8a22826 100644 --- a/ui/components/app/menu-bar/account-options-menu.js +++ b/ui/components/app/menu-bar/account-options-menu.js @@ -5,10 +5,14 @@ import { useDispatch, useSelector } from 'react-redux'; import { getAccountLink } from '@metamask/etherscan-link'; import { showModal } from '../../../store/actions'; -import { CONNECTED_ROUTE } from '../../../helpers/constants/routes'; +import { + CONNECTED_ROUTE, + NETWORKS_ROUTE, +} from '../../../helpers/constants/routes'; import { getURLHostName } from '../../../helpers/utils/util'; import { Menu, MenuItem } from '../../ui/menu'; import { + getBlockExplorerLinkText, getCurrentChainId, getCurrentKeyring, getRpcPrefsForCurrentProvider, @@ -34,9 +38,30 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { const { blockExplorerUrl } = rpcPrefs; const blockExplorerUrlSubTitle = getURLHostName(blockExplorerUrl); const trackEvent = useContext(MetaMetricsContext); + const blockExplorerLinkText = useSelector(getBlockExplorerLinkText); const isRemovable = keyring.type !== 'HD Key Tree'; + const routeToAddBlockExplorerUrl = () => { + history.push(`${NETWORKS_ROUTE}#blockExplorerUrl`); + }; + + const openBlockExplorer = () => { + trackEvent({ + event: 'Clicked Block Explorer Link', + category: EVENT.CATEGORIES.NAVIGATION, + properties: { + link_type: 'Account Tracker', + action: 'Account Options', + block_explorer_domain: getURLHostName(addressLink), + }, + }); + global.platform.openTab({ + url: addressLink, + }); + onClose(); + }; + return ( { - trackEvent({ - event: 'Clicked Block Explorer Link', - category: EVENT.CATEGORIES.NAVIGATION, - properties: { - link_type: 'Account Tracker', - action: 'Account Options', - block_explorer_domain: getURLHostName(addressLink), - }, - }); - global.platform.openTab({ - url: addressLink, - }); - onClose(); - }} + onClick={ + blockExplorerLinkText.firstPart === 'addBlockExplorer' + ? routeToAddBlockExplorerUrl + : openBlockExplorer + } subtitle={ blockExplorerUrlSubTitle ? ( @@ -68,9 +83,12 @@ export default function AccountOptionsMenu({ anchorElement, onClose }) { } iconClassName="fas fa-external-link-alt" > - {rpcPrefs.blockExplorerUrl - ? t('viewinExplorer', [t('blockExplorerAccountAction')]) - : t('viewOnEtherscan', [t('blockExplorerAccountAction')])} + {t( + blockExplorerLinkText.firstPart, + blockExplorerLinkText.secondPart === '' + ? null + : [t(blockExplorerLinkText.secondPart)], + )} {getEnvironmentType() === ENVIRONMENT_TYPE_FULLSCREEN ? null : ( { + hideModal(); + history.push(`${NETWORKS_ROUTE}#blockExplorerUrl`); + }; + + const openBlockExplorer = () => { + const accountLink = getAccountLink(address, chainId, rpcPrefs); + this.context.trackEvent({ + category: EVENT.CATEGORIES.NAVIGATION, + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Account Tracker', + action: 'Account Details Modal', + block_explorer_domain: getURLHostName(accountLink), + }, + }); + global.platform.openTab({ + url: accountLink, + }); + }; + return ( { - const accountLink = getAccountLink(address, chainId, rpcPrefs); - this.context.trackEvent({ - category: EVENT.CATEGORIES.NAVIGATION, - event: 'Clicked Block Explorer Link', - properties: { - link_type: 'Account Tracker', - action: 'Account Details Modal', - block_explorer_domain: getURLHostName(accountLink), - }, - }); - global.platform.openTab({ - url: accountLink, - }); - }} + onClick={ + blockExplorerLinkText.firstPart === 'addBlockExplorer' + ? routeToAddBlockExplorerUrl + : openBlockExplorer + } > - {rpcPrefs.blockExplorerUrl - ? this.context.t('blockExplorerView', [ - getURLHostName(rpcPrefs.blockExplorerUrl), - ]) - : this.context.t('etherscanViewOn')} + {this.context.t( + blockExplorerLinkText.firstPart, + blockExplorerLinkText.secondPart === '' + ? null + : [blockExplorerLinkText.secondPart], + )} {exportPrivateKeyFeatureEnabled ? ( diff --git a/ui/components/app/modals/account-details-modal/account-details-modal.container.js b/ui/components/app/modals/account-details-modal/account-details-modal.container.js index 0241c4d2a..f5270b1b9 100644 --- a/ui/components/app/modals/account-details-modal/account-details-modal.container.js +++ b/ui/components/app/modals/account-details-modal/account-details-modal.container.js @@ -1,10 +1,17 @@ import { connect } from 'react-redux'; -import { showModal, setAccountLabel } from '../../../../store/actions'; +import { compose } from 'redux'; +import { withRouter } from 'react-router-dom'; +import { + showModal, + setAccountLabel, + hideModal, +} from '../../../../store/actions'; import { getSelectedIdentity, getRpcPrefsForCurrentProvider, getCurrentChainId, getMetaMaskAccountsOrdered, + getBlockExplorerLinkText, } from '../../../../selectors'; import AccountDetailsModal from './account-details-modal.component'; @@ -15,6 +22,7 @@ const mapStateToProps = (state) => { keyrings: state.metamask.keyrings, rpcPrefs: getRpcPrefsForCurrentProvider(state), accounts: getMetaMaskAccountsOrdered(state), + blockExplorerLinkText: getBlockExplorerLinkText(state, true), }; }; @@ -24,10 +32,13 @@ const mapDispatchToProps = (dispatch) => { dispatch(showModal({ name: 'EXPORT_PRIVATE_KEY' })), setAccountLabel: (address, label) => dispatch(setAccountLabel(address, label)), + hideModal: () => { + dispatch(hideModal()); + }, }; }; -export default connect( - mapStateToProps, - mapDispatchToProps, +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps), )(AccountDetailsModal); diff --git a/ui/components/app/modals/account-details-modal/account-details-modal.test.js b/ui/components/app/modals/account-details-modal/account-details-modal.test.js index 8b61394d8..a2a810769 100644 --- a/ui/components/app/modals/account-details-modal/account-details-modal.test.js +++ b/ui/components/app/modals/account-details-modal/account-details-modal.test.js @@ -30,14 +30,16 @@ describe('Account Details Modal', () => { name: 'Account 1', }, }, - accounts: [ - { - address: '0xAddress', - lastSelected: 1637764711510, - name: 'Account 1', - balance: '0x543a5fb6caccf599', - }, - ], + accounts: { + address: '0xAddress', + lastSelected: 1637764711510, + name: 'Account 1', + balance: '0x543a5fb6caccf599', + }, + blockExplorerLinkText: { + firstPart: 'addBlockExplorer', + secondPart: '', + }, }; beforeEach(() => { @@ -58,6 +60,12 @@ describe('Account Details Modal', () => { }); it('opens new tab when view block explorer is clicked', () => { + wrapper.setProps({ + blockExplorerLinkText: { + firstPart: 'viewOnEtherscan', + secondPart: 'blockExplorerAccountAction', + }, + }); const modalButton = wrapper.find('.account-details-modal__button'); const etherscanLink = modalButton.first(); @@ -75,7 +83,10 @@ describe('Account Details Modal', () => { it('sets blockexplorerview text when block explorer url in rpcPrefs exists', () => { const blockExplorerUrl = 'https://block.explorer'; - wrapper.setProps({ rpcPrefs: { blockExplorerUrl } }); + wrapper.setProps({ + rpcPrefs: { blockExplorerUrl }, + blockExplorerLinkText: { firstPart: 'blockExplorerView' }, + }); const modalButton = wrapper.find('.account-details-modal__button'); const blockExplorerLink = modalButton.first().shallow(); diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js index f65c82f63..92592a5b0 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.js @@ -16,6 +16,7 @@ import { EVENT } from '../../../../shared/constants/metametrics'; import { TRANSACTION_TYPES } from '../../../../shared/constants/transaction'; import { getURLHostName } from '../../../helpers/utils/util'; import TransactionDecoding from '../transaction-decoding'; +import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; export default class TransactionListItemDetails extends PureComponent { static contextTypes = { @@ -46,6 +47,9 @@ export default class TransactionListItemDetails extends PureComponent { senderNickname: PropTypes.string.isRequired, recipientNickname: PropTypes.string, transactionStatus: PropTypes.func, + isCustomNetwork: PropTypes.bool, + history: PropTypes.object, + blockExplorerLinkText: PropTypes.object, }; state = { @@ -56,26 +60,33 @@ export default class TransactionListItemDetails extends PureComponent { const { transactionGroup: { primaryTransaction }, rpcPrefs, + isCustomNetwork, + history, + onClose, } = this.props; const blockExplorerLink = getBlockExplorerLink( primaryTransaction, rpcPrefs, ); - this.context.trackEvent({ - category: EVENT.CATEGORIES.TRANSACTIONS, - event: 'Clicked Block Explorer Link', - properties: { - link_type: 'Transaction Block Explorer', - action: 'Transaction Details', - block_explorer_domain: getURLHostName(blockExplorerLink), - legacy_event: true, - }, - }); + if (!rpcPrefs.blockExplorerUrl && isCustomNetwork) { + onClose(); + history.push(`${NETWORKS_ROUTE}#blockExplorerUrl`); + } else { + this.context.trackEvent({ + category: EVENT.CATEGORIES.TRANSACTIONS, + event: 'Clicked Block Explorer Link', + properties: { + link_type: 'Transaction Block Explorer', + action: 'Transaction Details', + block_explorer_domain: getURLHostName(blockExplorerLink), + }, + }); - global.platform.openTab({ - url: blockExplorerLink, - }); + global.platform.openTab({ + url: blockExplorerLink, + }); + } }; handleCancel = (event) => { @@ -136,6 +147,7 @@ export default class TransactionListItemDetails extends PureComponent { recipientNickname, showCancel, transactionStatus: TransactionStatus, + blockExplorerLinkText, } = this.props; const { primaryTransaction: transaction, @@ -191,7 +203,9 @@ export default class TransactionListItemDetails extends PureComponent { onClick={this.handleBlockExplorerClick} disabled={!hash} > - {t('viewOnBlockExplorer')} + {blockExplorerLinkText.firstPart === 'addBlockExplorer' + ? t('addBlockExplorer') + : t('viewOnBlockExplorer')}
diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js index 18417627c..eef1225c3 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.component.test.js @@ -31,6 +31,15 @@ describe('TransactionListItemDetails Component', () => { initialTransaction: transaction, }; + const rpcPrefs = { + blockExplorerUrl: 'https://customblockexplorer.com/', + }; + + const blockExplorerLinkText = { + firstPart: 'addBlockExplorer', + secondPart: '', + }; + const wrapper = shallow( undefined} @@ -42,6 +51,8 @@ describe('TransactionListItemDetails Component', () => { senderNickname="sender-nickname" recipientNickname="recipient-nickname" transactionStatus={TransactionStatus} + rpcPrefs={rpcPrefs} + blockExplorerLinkText={blockExplorerLinkText} />, { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); @@ -77,6 +88,15 @@ describe('TransactionListItemDetails Component', () => { hasCancelled: false, }; + const rpcPrefs = { + blockExplorerUrl: 'https://customblockexplorer.com/', + }; + + const blockExplorerLinkText = { + firstPart: 'addBlockExplorer', + secondPart: '', + }; + const wrapper = shallow( undefined} @@ -89,6 +109,8 @@ describe('TransactionListItemDetails Component', () => { senderNickname="sender-nickname" recipientNickname="recipient-nickname" transactionStatus={TransactionStatus} + rpcPrefs={rpcPrefs} + blockExplorerLinkText={blockExplorerLinkText} />, { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); @@ -120,6 +142,15 @@ describe('TransactionListItemDetails Component', () => { initialTransaction: transaction, }; + const rpcPrefs = { + blockExplorerUrl: 'https://customblockexplorer.com/', + }; + + const blockExplorerLinkText = { + firstPart: 'addBlockExplorer', + secondPart: '', + }; + const wrapper = shallow( undefined} @@ -131,6 +162,8 @@ describe('TransactionListItemDetails Component', () => { senderNickname="sender-nickname" recipientNickname="recipient-nickname" transactionStatus={TransactionStatus} + rpcPrefs={rpcPrefs} + blockExplorerLinkText={blockExplorerLinkText} />, { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); @@ -165,6 +198,15 @@ describe('TransactionListItemDetails Component', () => { initialTransaction: transaction, }; + const rpcPrefs = { + blockExplorerUrl: 'https://customblockexplorer.com/', + }; + + const blockExplorerLinkText = { + firstPart: 'addBlockExplorer', + secondPart: '', + }; + const wrapper = shallow( undefined} @@ -176,6 +218,8 @@ describe('TransactionListItemDetails Component', () => { senderNickname="sender-nickname" recipientNickname="recipient-nickname" transactionStatus={TransactionStatus} + rpcPrefs={rpcPrefs} + blockExplorerLinkText={blockExplorerLinkText} />, { context: { t: (str1, str2) => (str2 ? str1 + str2 : str1) } }, ); diff --git a/ui/components/app/transaction-list-item-details/transaction-list-item-details.container.js b/ui/components/app/transaction-list-item-details/transaction-list-item-details.container.js index dc3775fb2..61731c092 100644 --- a/ui/components/app/transaction-list-item-details/transaction-list-item-details.container.js +++ b/ui/components/app/transaction-list-item-details/transaction-list-item-details.container.js @@ -1,7 +1,11 @@ import { connect } from 'react-redux'; +import { compose } from 'redux'; +import { withRouter } from 'react-router-dom'; import { tryReverseResolveAddress } from '../../../store/actions'; import { getAddressBook, + getBlockExplorerLinkText, + getIsCustomNetwork, getRpcPrefsForCurrentProvider, getEnsResolutionByAddress, } from '../../../selectors'; @@ -25,11 +29,15 @@ const mapStateToProps = (state, ownProps) => { }; const rpcPrefs = getRpcPrefsForCurrentProvider(state); + const isCustomNetwork = getIsCustomNetwork(state); + return { rpcPrefs, recipientEns, senderNickname: getNickName(senderAddress), recipientNickname: recipientAddress ? getNickName(recipientAddress) : null, + isCustomNetwork, + blockExplorerLinkText: getBlockExplorerLinkText(state), }; }; @@ -41,7 +49,7 @@ const mapDispatchToProps = (dispatch) => { }; }; -export default connect( - mapStateToProps, - mapDispatchToProps, +export default compose( + withRouter, + connect(mapStateToProps, mapDispatchToProps), )(TransactionListItemDetails); diff --git a/ui/components/ui/nickname-popover/nickname-popover.component.js b/ui/components/ui/nickname-popover/nickname-popover.component.js index d0f8a194f..03ebb0fa3 100644 --- a/ui/components/ui/nickname-popover/nickname-popover.component.js +++ b/ui/components/ui/nickname-popover/nickname-popover.component.js @@ -1,6 +1,7 @@ import React, { useCallback, useContext } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; +import { useHistory } from 'react-router-dom'; import { I18nContext } from '../../../contexts/i18n'; import Tooltip from '../tooltip'; import Popover from '../popover'; @@ -9,7 +10,13 @@ import Identicon from '../identicon/identicon.component'; import { shortenAddress } from '../../../helpers/utils/util'; import CopyIcon from '../icon/copy-icon.component'; import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard'; -import { getUseTokenDetection, getTokenList } from '../../../selectors'; +import { + getUseTokenDetection, + getTokenList, + getBlockExplorerLinkText, +} from '../../../selectors'; + +import { NETWORKS_ROUTE } from '../../../helpers/constants/routes'; const NicknamePopover = ({ address, @@ -19,6 +26,7 @@ const NicknamePopover = ({ explorerLink, }) => { const t = useContext(I18nContext); + const history = useHistory(); const onAddClick = useCallback(() => { onAdd(); @@ -27,6 +35,17 @@ const NicknamePopover = ({ const [copied, handleCopy] = useCopyToClipboard(); const useTokenDetection = useSelector(getUseTokenDetection); const tokenList = useSelector(getTokenList); + const blockExplorerLinkText = useSelector(getBlockExplorerLinkText); + + const routeToAddBlockExplorerUrl = () => { + history.push(`${NETWORKS_ROUTE}#blockExplorerUrl`); + }; + + const openBlockExplorer = () => { + global.platform.openTab({ + url: explorerLink, + }); + }; return (
@@ -66,16 +85,22 @@ const NicknamePopover = ({
{ const environmentType = getEnvironmentType(); const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN; const shouldRenderNetworkForm = - isFullScreen || Boolean(pathname.match(NETWORKS_FORM_ROUTE)); + isFullScreen || + Boolean(pathname.match(NETWORKS_FORM_ROUTE)) || + window.location.hash.split('#')[2] === 'blockExplorerUrl'; const frequentRpcListDetail = useSelector(getFrequentRpcListDetail); const provider = useSelector(getProvider); diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 3dc5ef412..e4406a2ff 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -18,6 +18,7 @@ import { BSC_DISPLAY_NAME, POLYGON_DISPLAY_NAME, AVALANCHE_DISPLAY_NAME, + CHAIN_ID_TO_RPC_URL_MAP, } from '../../shared/constants/network'; import { KEYRING_TYPES, @@ -41,7 +42,11 @@ import { ALLOWED_DEV_SWAPS_CHAIN_IDS, } from '../../shared/constants/swaps'; -import { shortenAddress, getAccountByAddress } from '../helpers/utils/util'; +import { + shortenAddress, + getAccountByAddress, + getURLHostName, +} from '../helpers/utils/util'; import { getValueFromWeiHex, hexToDecimal, @@ -1073,3 +1078,43 @@ export function getNewTokensImported(state) { export function getIsCustomNetworkListEnabled(state) { return state.metamask.customNetworkListEnabled; } + +export function getIsCustomNetwork(state) { + const chainId = getCurrentChainId(state); + + return !CHAIN_ID_TO_RPC_URL_MAP[chainId]; +} + +export function getBlockExplorerLinkText( + state, + accountDetailsModalComponent = false, +) { + const isCustomNetwork = getIsCustomNetwork(state); + const rpcPrefs = getRpcPrefsForCurrentProvider(state); + + let blockExplorerLinkText = { + firstPart: 'addBlockExplorer', + secondPart: '', + }; + + if (rpcPrefs.blockExplorerUrl) { + blockExplorerLinkText = accountDetailsModalComponent + ? { + firstPart: 'blockExplorerView', + secondPart: getURLHostName(rpcPrefs.blockExplorerUrl), + } + : { + firstPart: 'viewinExplorer', + secondPart: 'blockExplorerAccountAction', + }; + } else if (isCustomNetwork === false) { + blockExplorerLinkText = accountDetailsModalComponent + ? { firstPart: 'etherscanViewOn', secondPart: '' } + : { + firstPart: 'viewOnEtherscan', + secondPart: 'blockExplorerAccountAction', + }; + } + + return blockExplorerLinkText; +}