1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-13 13:17:13 +01:00
metamask-extension/ui/components/app/dropdowns/network-dropdown.js
Dan J Miller 2647be9662
Ensure one and only one localhost option appears in network list (#12786)
* Ensure one and only one localhost option appears in network list

* Fix unit tests
2021-11-22 12:54:42 -03:30

392 lines
12 KiB
JavaScript

import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { compose } from 'redux';
import classnames from 'classnames';
import Button from '../../ui/button';
import * as actions from '../../../store/actions';
import { openAlert as displayInvalidCustomNetworkAlert } from '../../../ducks/alerts/invalid-custom-network';
import {
NETWORK_TYPE_RPC,
LOCALHOST_RPC_URL,
} from '../../../../shared/constants/network';
import { isPrefixedFormattedHexString } from '../../../../shared/modules/network.utils';
import ColorIndicator from '../../ui/color-indicator';
import { COLORS, SIZES } from '../../../helpers/constants/design-system';
import { getShowTestNetworks } from '../../../selectors';
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
import {
ADD_NETWORK_ROUTE,
ADVANCED_ROUTE,
} from '../../../helpers/constants/routes';
import { Dropdown, DropdownMenuItem } from './dropdown';
// classes from nodes of the toggle element.
const notToggleElementClassnames = [
'menu-icon',
'network-name',
'network-indicator',
'network-caret',
'network-component',
'modal-container__footer-button',
];
const DROP_DOWN_MENU_ITEM_STYLE = {
fontSize: '16px',
lineHeight: '20px',
padding: '12px 0',
};
function mapStateToProps(state) {
return {
provider: state.metamask.provider,
shouldShowTestNetworks: getShowTestNetworks(state),
frequentRpcListDetail: state.metamask.frequentRpcListDetail || [],
networkDropdownOpen: state.appState.networkDropdownOpen,
showTestnetMessageInDropdown: state.metamask.showTestnetMessageInDropdown,
};
}
function mapDispatchToProps(dispatch) {
return {
setProviderType: (type) => {
dispatch(actions.setProviderType(type));
},
setRpcTarget: (target, chainId, ticker, nickname) => {
dispatch(actions.setRpcTarget(target, chainId, ticker, nickname));
},
hideNetworkDropdown: () => dispatch(actions.hideNetworkDropdown()),
displayInvalidCustomNetworkAlert: (networkName) => {
dispatch(displayInvalidCustomNetworkAlert(networkName));
},
showConfirmDeleteNetworkModal: ({ target, onConfirm }) => {
return dispatch(
actions.showModal({
name: 'CONFIRM_DELETE_NETWORK',
target,
onConfirm,
}),
);
},
hideTestNetMessage: () => actions.hideTestNetMessage(),
};
}
class NetworkDropdown extends Component {
static contextTypes = {
t: PropTypes.func,
metricsEvent: PropTypes.func,
};
static propTypes = {
provider: PropTypes.shape({
nickname: PropTypes.string,
rpcUrl: PropTypes.string,
type: PropTypes.string,
ticker: PropTypes.string,
}).isRequired,
setProviderType: PropTypes.func.isRequired,
setRpcTarget: PropTypes.func.isRequired,
hideNetworkDropdown: PropTypes.func.isRequired,
frequentRpcListDetail: PropTypes.array.isRequired,
shouldShowTestNetworks: PropTypes.bool,
networkDropdownOpen: PropTypes.bool.isRequired,
displayInvalidCustomNetworkAlert: PropTypes.func.isRequired,
showConfirmDeleteNetworkModal: PropTypes.func.isRequired,
showTestnetMessageInDropdown: PropTypes.bool.isRequired,
hideTestNetMessage: PropTypes.func.isRequired,
history: PropTypes.object,
};
handleClick(newProviderType) {
const {
provider: { type: providerType },
setProviderType,
} = this.props;
const { metricsEvent } = this.context;
metricsEvent({
eventOpts: {
category: 'Navigation',
action: 'Home',
name: 'Switched Networks',
},
customVariables: {
fromNetwork: providerType,
toNetwork: newProviderType,
},
});
setProviderType(newProviderType);
}
renderAddCustomButton() {
const style = {
width: '100%',
left: '40px',
color: 'white',
background: 'rgba(0, 0, 0, 0.75)',
borderRadius: '20px',
textTransform: 'none',
};
return (
<Button
type="submit"
style={style}
variant="contained"
size="large"
onClick={() => {
if (getEnvironmentType() === ENVIRONMENT_TYPE_POPUP) {
global.platform.openExtensionInBrowser(ADD_NETWORK_ROUTE);
} else {
this.props.history.push(ADD_NETWORK_ROUTE);
}
this.props.hideNetworkDropdown();
}}
>
{this.context.t('addNetwork')}
</Button>
);
}
renderCustomRpcList(rpcListDetail, provider, opts = {}) {
const reversedRpcListDetail = rpcListDetail.slice().reverse();
return reversedRpcListDetail.map((entry) => {
const { rpcUrl, chainId, ticker = 'ETH', nickname = '' } = entry;
const isCurrentRpcTarget =
provider.type === NETWORK_TYPE_RPC && rpcUrl === provider.rpcUrl;
let borderColor = COLORS.UI2;
if (isCurrentRpcTarget) {
borderColor = COLORS.WHITE;
}
if (opts.isLocalHost) {
borderColor = 'localhost';
}
return (
<DropdownMenuItem
key={`common${rpcUrl}`}
closeMenu={() => this.props.hideNetworkDropdown()}
onClick={() => {
if (isPrefixedFormattedHexString(chainId)) {
this.props.setRpcTarget(rpcUrl, chainId, ticker, nickname);
} else {
this.props.displayInvalidCustomNetworkAlert(nickname || rpcUrl);
}
}}
style={{
fontSize: '16px',
lineHeight: '20px',
padding: '12px 0',
}}
>
{isCurrentRpcTarget ? (
<i className="fa fa-check" />
) : (
<div className="network-check__transparent"></div>
)}
<ColorIndicator
color={opts.isLocalHost ? 'localhost' : COLORS.UI2}
size={SIZES.LG}
type={ColorIndicator.TYPES.FILLED}
borderColor={borderColor}
/>
<span
className="network-name-item"
style={{
color: isCurrentRpcTarget ? '#ffffff' : '#9b9b9b',
}}
>
{nickname || rpcUrl}
</span>
{isCurrentRpcTarget ? null : (
<i
className="fa fa-times delete"
onClick={(e) => {
e.stopPropagation();
this.props.showConfirmDeleteNetworkModal({
target: rpcUrl,
onConfirm: () => undefined,
});
}}
/>
)}
</DropdownMenuItem>
);
});
}
getNetworkName() {
const { provider } = this.props;
const providerName = provider.type;
let name;
if (providerName === 'mainnet') {
name = this.context.t('mainnet');
} else if (providerName === 'ropsten') {
name = this.context.t('ropsten');
} else if (providerName === 'kovan') {
name = this.context.t('kovan');
} else if (providerName === 'rinkeby') {
name = this.context.t('rinkeby');
} else if (providerName === 'goerli') {
name = this.context.t('goerli');
} else if (providerName === 'localhost') {
name = this.context.t('localhost');
} else {
name = provider.nickname || this.context.t('unknownNetwork');
}
return name;
}
renderNetworkEntry(network) {
const {
provider: { type: providerType },
} = this.props;
return (
<DropdownMenuItem
key={network}
closeMenu={this.props.hideNetworkDropdown}
onClick={() => this.handleClick(network)}
style={DROP_DOWN_MENU_ITEM_STYLE}
>
{providerType === network ? (
<i className="fa fa-check" />
) : (
<div className="network-check__transparent"></div>
)}
<ColorIndicator
color={network}
size={SIZES.LG}
type={ColorIndicator.TYPES.FILLED}
borderColor={providerType === network ? COLORS.WHITE : network}
/>
<span
className="network-name-item"
style={{
color: providerType === network ? '#ffffff' : '#9b9b9b',
}}
>
{this.context.t(network)}
</span>
</DropdownMenuItem>
);
}
render() {
const {
history,
hideNetworkDropdown,
shouldShowTestNetworks,
showTestnetMessageInDropdown,
hideTestNetMessage,
} = this.props;
const rpcListDetail = this.props.frequentRpcListDetail;
const rpcListDetailWithoutLocalHost = rpcListDetail.filter(
(rpc) => rpc.rpcUrl !== LOCALHOST_RPC_URL,
);
const rpcListDetailForLocalHost = rpcListDetail.filter(
(rpc) => rpc.rpcUrl === LOCALHOST_RPC_URL,
);
const isOpen = this.props.networkDropdownOpen;
const { t } = this.context;
return (
<Dropdown
isOpen={isOpen}
onClickOutside={(event) => {
const { classList } = event.target;
const isInClassList = (className) => classList.contains(className);
const notToggleElementIndex = notToggleElementClassnames.findIndex(
isInClassList,
);
if (notToggleElementIndex === -1) {
event.stopPropagation();
hideNetworkDropdown();
}
}}
containerClassName="network-droppo"
zIndex={55}
style={{
position: 'absolute',
top: '58px',
width: '309px',
zIndex: '55px',
}}
innerStyle={{
padding: '18px 8px',
}}
>
<div className="network-dropdown-header">
<div className="network-dropdown-title">{t('networks')}</div>
<div className="network-dropdown-divider" />
{showTestnetMessageInDropdown ? (
<div className="network-dropdown-content">
{t('toggleTestNetworks', [
<a
href="#"
key="advancedSettingsLink"
className="network-dropdown-content--link"
onClick={(e) => {
e.preventDefault();
hideNetworkDropdown();
history.push(`${ADVANCED_ROUTE}#show-testnets`);
}}
>
{t('showHide')}
</a>,
])}
<button
onClick={hideTestNetMessage}
className="network-dropdown-content--dismiss"
>
{t('dismiss')}
</button>
</div>
) : null}
</div>
<div className="network-dropdown-list">
{this.renderNetworkEntry('mainnet')}
{this.renderCustomRpcList(
rpcListDetailWithoutLocalHost,
this.props.provider,
)}
<div
className={classnames('network-dropdown-testnets', {
'network-dropdown-testnets--no-visibility': !shouldShowTestNetworks,
})}
>
{this.renderNetworkEntry('ropsten')}
{this.renderNetworkEntry('kovan')}
{this.renderNetworkEntry('rinkeby')}
{this.renderNetworkEntry('goerli')}
{this.renderCustomRpcList(
rpcListDetailForLocalHost,
this.props.provider,
{ isLocalHost: true },
)}
</div>
</div>
{this.renderAddCustomButton()}
</Dropdown>
);
}
}
export default compose(
withRouter,
connect(mapStateToProps, mapDispatchToProps),
)(NetworkDropdown);