mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
UX: Multichain: Network Menu (#18229)
This commit is contained in:
parent
0bbfd38cc6
commit
a71a06965c
3
app/_locales/en/messages.json
generated
3
app/_locales/en/messages.json
generated
@ -2184,6 +2184,9 @@
|
|||||||
"networkIsBusy": {
|
"networkIsBusy": {
|
||||||
"message": "Network is busy. Gas prices are high and estimates are less accurate."
|
"message": "Network is busy. Gas prices are high and estimates are less accurate."
|
||||||
},
|
},
|
||||||
|
"networkMenuHeading": {
|
||||||
|
"message": "Select a network"
|
||||||
|
},
|
||||||
"networkName": {
|
"networkName": {
|
||||||
"message": "Network name"
|
"message": "Network name"
|
||||||
},
|
},
|
||||||
|
@ -8,3 +8,5 @@ export { MultichainImportTokenLink } from './multichain-import-token-link';
|
|||||||
export { MultichainTokenListItem } from './multichain-token-list-item';
|
export { MultichainTokenListItem } from './multichain-token-list-item';
|
||||||
export { AddressCopyButton } from './address-copy-button';
|
export { AddressCopyButton } from './address-copy-button';
|
||||||
export { MultichainConnectedSiteMenu } from './multichain-connected-site-menu';
|
export { MultichainConnectedSiteMenu } from './multichain-connected-site-menu';
|
||||||
|
export { NetworkListItem } from './network-list-item';
|
||||||
|
export { NetworkListMenu } from './network-list-menu';
|
||||||
|
@ -9,4 +9,7 @@
|
|||||||
@import 'account-list-menu/index';
|
@import 'account-list-menu/index';
|
||||||
@import 'account-picker/index';
|
@import 'account-picker/index';
|
||||||
@import 'multichain-connected-site-menu/index';
|
@import 'multichain-connected-site-menu/index';
|
||||||
|
@import 'account-list-menu/';
|
||||||
@import 'multichain-token-list-item/multichain-token-list-item';
|
@import 'multichain-token-list-item/multichain-token-list-item';
|
||||||
|
@import 'network-list-item/';
|
||||||
|
@import 'network-list-menu/';
|
||||||
|
@ -0,0 +1,41 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`NetworkListItem renders properly 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="box multichain-network-list-item box--padding-4 box--gap-2 box--flex-direction-row box--justify-content-space-between box--align-items-center box--width-full box--background-color-transparent box--display-flex"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="box mm-text mm-avatar-base mm-avatar-base--size-md mm-avatar-network mm-text--body-sm mm-text--text-transform-uppercase box--display-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default box--background-color-background-alternative box--rounded-full box--border-color-transparent box--border-style-solid box--border-width-1"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Polygon logo"
|
||||||
|
class="mm-avatar-network__network-image"
|
||||||
|
src="./images/matic-token.png"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="box multichain-network-list-item__network-name box--flex-direction-row"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="box mm-text mm-button-base mm-button-base--ellipsis mm-button-link mm-button-link--size-auto mm-text--body-md mm-text--ellipsis box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-text-default box--background-color-transparent"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="box mm-text mm-text--inherit mm-text--ellipsis box--flex-direction-row box--color-text-default"
|
||||||
|
>
|
||||||
|
Polygon
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
aria-label="[deleteNetwork]"
|
||||||
|
class="box mm-button-icon mm-button-icon--size-sm multichain-network-list-item__delete box--display-inline-flex box--flex-direction-row box--justify-content-center box--align-items-center box--color-error-default box--background-color-transparent box--rounded-lg"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="box mm-icon mm-icon--size-sm box--display-inline-block box--flex-direction-row box--color-inherit"
|
||||||
|
style="mask-image: url('./images/icons/trash.svg');"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
1
ui/components/multichain/network-list-item/index.js
Normal file
1
ui/components/multichain/network-list-item/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { NetworkListItem } from './network-list-item';
|
51
ui/components/multichain/network-list-item/index.scss
Normal file
51
ui/components/multichain/network-list-item/index.scss
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
.multichain-network-list-item {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:not(.multichain-network-list-item--selected) {
|
||||||
|
&:hover,
|
||||||
|
&:focus-within {
|
||||||
|
background: var(--color-background-default-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
a:focus {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:focus-within {
|
||||||
|
.multichain-network-list-item__delete {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__network-name {
|
||||||
|
width: 100%;
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-align: start;
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__tooltip {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__selected-indicator {
|
||||||
|
width: 4px;
|
||||||
|
height: calc(100% - 8px);
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__delete {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
}
|
108
ui/components/multichain/network-list-item/network-list-item.js
Normal file
108
ui/components/multichain/network-list-item/network-list-item.js
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Box from '../../ui/box/box';
|
||||||
|
import {
|
||||||
|
AlignItems,
|
||||||
|
IconColor,
|
||||||
|
BorderRadius,
|
||||||
|
Color,
|
||||||
|
Size,
|
||||||
|
JustifyContent,
|
||||||
|
TextColor,
|
||||||
|
BLOCK_SIZES,
|
||||||
|
} from '../../../helpers/constants/design-system';
|
||||||
|
import {
|
||||||
|
AvatarNetwork,
|
||||||
|
ButtonIcon,
|
||||||
|
ButtonLink,
|
||||||
|
ICON_NAMES,
|
||||||
|
} from '../../component-library';
|
||||||
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
|
import Tooltip from '../../ui/tooltip/tooltip';
|
||||||
|
|
||||||
|
const MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP = 17;
|
||||||
|
|
||||||
|
export const NetworkListItem = ({
|
||||||
|
name,
|
||||||
|
iconSrc,
|
||||||
|
selected = false,
|
||||||
|
onClick,
|
||||||
|
onDeleteClick,
|
||||||
|
}) => {
|
||||||
|
const t = useI18nContext();
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
onClick={onClick}
|
||||||
|
padding={4}
|
||||||
|
gap={2}
|
||||||
|
backgroundColor={selected ? Color.primaryMuted : Color.transparent}
|
||||||
|
className={classnames('multichain-network-list-item', {
|
||||||
|
'multichain-network-list-item--selected': selected,
|
||||||
|
})}
|
||||||
|
alignItems={AlignItems.center}
|
||||||
|
justifyContent={JustifyContent.spaceBetween}
|
||||||
|
width={BLOCK_SIZES.FULL}
|
||||||
|
>
|
||||||
|
{selected && (
|
||||||
|
<Box
|
||||||
|
className="multichain-network-list-item__selected-indicator"
|
||||||
|
borderRadius={BorderRadius.pill}
|
||||||
|
backgroundColor={Color.primaryDefault}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<AvatarNetwork name={name} src={iconSrc} />
|
||||||
|
<Box className="multichain-network-list-item__network-name">
|
||||||
|
<ButtonLink onClick={onClick} color={TextColor.textDefault} ellipsis>
|
||||||
|
{name.length > MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP ? (
|
||||||
|
<Tooltip
|
||||||
|
title={name}
|
||||||
|
position="bottom"
|
||||||
|
wrapperClassName="multichain-network-list-item__tooltip"
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
name
|
||||||
|
)}
|
||||||
|
</ButtonLink>
|
||||||
|
</Box>
|
||||||
|
{onDeleteClick ? (
|
||||||
|
<ButtonIcon
|
||||||
|
className="multichain-network-list-item__delete"
|
||||||
|
color={IconColor.errorDefault}
|
||||||
|
iconName={ICON_NAMES.TRASH}
|
||||||
|
ariaLabel={t('deleteNetwork')}
|
||||||
|
size={Size.SM}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onDeleteClick();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworkListItem.propTypes = {
|
||||||
|
/**
|
||||||
|
* The name of the network
|
||||||
|
*/
|
||||||
|
name: PropTypes.string.isRequired,
|
||||||
|
/**
|
||||||
|
* Path to the Icon image
|
||||||
|
*/
|
||||||
|
iconSrc: PropTypes.string,
|
||||||
|
/**
|
||||||
|
* Represents if the network item is selected
|
||||||
|
*/
|
||||||
|
selected: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Executes when the item is clicked
|
||||||
|
*/
|
||||||
|
onClick: PropTypes.func.isRequired,
|
||||||
|
/**
|
||||||
|
* Executes when the delete icon is clicked
|
||||||
|
*/
|
||||||
|
onDeleteClick: PropTypes.func,
|
||||||
|
};
|
@ -0,0 +1,67 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { NetworkListItem } from '.';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/Multichain/NetworkListItem',
|
||||||
|
component: NetworkListItem,
|
||||||
|
argTypes: {
|
||||||
|
name: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
selected: {
|
||||||
|
control: 'boolean',
|
||||||
|
},
|
||||||
|
onClick: {
|
||||||
|
action: 'onClick',
|
||||||
|
},
|
||||||
|
onDeleteClick: {
|
||||||
|
action: 'onDeleteClick',
|
||||||
|
},
|
||||||
|
iconSrc: {
|
||||||
|
action: 'text',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
name: 'Ethereum',
|
||||||
|
iconSrc: '',
|
||||||
|
selected: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DefaultStory = (args) => (
|
||||||
|
<div
|
||||||
|
style={{ width: '328px', border: '1px solid var(--color-border-muted)' }}
|
||||||
|
>
|
||||||
|
<NetworkListItem {...args} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const IconStory = (args) => (
|
||||||
|
<div
|
||||||
|
style={{ width: '328px', border: '1px solid var(--color-border-muted)' }}
|
||||||
|
>
|
||||||
|
<NetworkListItem {...args} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
IconStory.args = { iconSrc: './images/matic-token.png', name: 'Polygon' };
|
||||||
|
|
||||||
|
export const SelectedStory = (args) => (
|
||||||
|
<div
|
||||||
|
style={{ width: '328px', border: '1px solid var(--color-border-muted)' }}
|
||||||
|
>
|
||||||
|
<NetworkListItem {...args} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
SelectedStory.args = { selected: true };
|
||||||
|
|
||||||
|
export const ChaosStory = (args) => (
|
||||||
|
<div
|
||||||
|
style={{ width: '328px', border: '1px solid var(--color-border-muted)' }}
|
||||||
|
>
|
||||||
|
<NetworkListItem {...args} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
ChaosStory.args = {
|
||||||
|
name: 'This is a super long network name that should be ellipsized',
|
||||||
|
selected: true,
|
||||||
|
};
|
@ -0,0 +1,81 @@
|
|||||||
|
/* eslint-disable jest/require-top-level-describe */
|
||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, render } from '@testing-library/react';
|
||||||
|
import {
|
||||||
|
MATIC_TOKEN_IMAGE_URL,
|
||||||
|
POLYGON_DISPLAY_NAME,
|
||||||
|
} from '../../../../shared/constants/network';
|
||||||
|
import { NetworkListItem } from '.';
|
||||||
|
|
||||||
|
const DEFAULT_PROPS = {
|
||||||
|
name: POLYGON_DISPLAY_NAME,
|
||||||
|
iconSrc: MATIC_TOKEN_IMAGE_URL,
|
||||||
|
selected: false,
|
||||||
|
onClick: () => undefined,
|
||||||
|
onDeleteClick: () => undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('NetworkListItem', () => {
|
||||||
|
it('renders properly', () => {
|
||||||
|
const { container } = render(<NetworkListItem {...DEFAULT_PROPS} />);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not render the delete icon when no onDeleteClick is clicked', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<NetworkListItem {...DEFAULT_PROPS} onDeleteClick={null} />,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
container.querySelector('.multichain-network-list-item__delete'),
|
||||||
|
).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows as selected when selected', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<NetworkListItem {...DEFAULT_PROPS} selected />,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
container.querySelector(
|
||||||
|
'.multichain-network-list-item__selected-indicator',
|
||||||
|
),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a tooltip when the network name is very long', () => {
|
||||||
|
const { container } = render(
|
||||||
|
<NetworkListItem
|
||||||
|
{...DEFAULT_PROPS}
|
||||||
|
name="This is a very long network name that will be truncated"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
container.querySelector('.multichain-network-list-item__tooltip'),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('executes onClick when the item is clicked', () => {
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const { container } = render(
|
||||||
|
<NetworkListItem {...DEFAULT_PROPS} onClick={onClick} />,
|
||||||
|
);
|
||||||
|
fireEvent.click(container.querySelector('.multichain-network-list-item'));
|
||||||
|
expect(onClick).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('executes onDeleteClick when the delete button is clicked', () => {
|
||||||
|
const onDeleteClick = jest.fn();
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const { container } = render(
|
||||||
|
<NetworkListItem
|
||||||
|
{...DEFAULT_PROPS}
|
||||||
|
onDeleteClick={onDeleteClick}
|
||||||
|
onClick={onClick}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
fireEvent.click(
|
||||||
|
container.querySelector('.multichain-network-list-item__delete'),
|
||||||
|
);
|
||||||
|
expect(onDeleteClick).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onClick).toHaveBeenCalledTimes(0);
|
||||||
|
});
|
||||||
|
});
|
1
ui/components/multichain/network-list-menu/index.js
Normal file
1
ui/components/multichain/network-list-menu/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { NetworkListMenu } from './network-list-menu';
|
4
ui/components/multichain/network-list-menu/index.scss
Normal file
4
ui/components/multichain/network-list-menu/index.scss
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.multichain-network-list-menu {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
122
ui/components/multichain/network-list-menu/network-list-menu.js
Normal file
122
ui/components/multichain/network-list-menu/network-list-menu.js
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import { useI18nContext } from '../../../hooks/useI18nContext';
|
||||||
|
import Popover from '../../ui/popover/popover.component';
|
||||||
|
import { NetworkListItem } from '../network-list-item';
|
||||||
|
import {
|
||||||
|
setActiveNetwork,
|
||||||
|
showModal,
|
||||||
|
setShowTestNetworks,
|
||||||
|
setProviderType,
|
||||||
|
} from '../../../store/actions';
|
||||||
|
import { CHAIN_IDS, TEST_CHAINS } from '../../../../shared/constants/network';
|
||||||
|
import {
|
||||||
|
getShowTestNetworks,
|
||||||
|
getAllNetworks,
|
||||||
|
getCurrentChainId,
|
||||||
|
} from '../../../selectors';
|
||||||
|
import Box from '../../ui/box/box';
|
||||||
|
import ToggleButton from '../../ui/toggle-button';
|
||||||
|
import {
|
||||||
|
DISPLAY,
|
||||||
|
JustifyContent,
|
||||||
|
} from '../../../helpers/constants/design-system';
|
||||||
|
import { Button, BUTTON_TYPES, Text } from '../../component-library';
|
||||||
|
import { ADD_POPULAR_CUSTOM_NETWORK } from '../../../helpers/constants/routes';
|
||||||
|
import { getEnvironmentType } from '../../../../app/scripts/lib/util';
|
||||||
|
import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app';
|
||||||
|
|
||||||
|
const UNREMOVABLE_CHAIN_IDS = [CHAIN_IDS.MAINNET, ...TEST_CHAINS];
|
||||||
|
|
||||||
|
export const NetworkListMenu = ({ closeMenu }) => {
|
||||||
|
const t = useI18nContext();
|
||||||
|
const networks = useSelector(getAllNetworks);
|
||||||
|
const showTestNetworks = useSelector(getShowTestNetworks);
|
||||||
|
const currentChainId = useSelector(getCurrentChainId);
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
const environmentType = getEnvironmentType();
|
||||||
|
const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover onClose={closeMenu} centerTitle title={t('networkMenuHeading')}>
|
||||||
|
<>
|
||||||
|
<Box className="multichain-network-list-menu">
|
||||||
|
{networks.map((network) => {
|
||||||
|
const isCurrentNetwork = currentChainId === network.chainId;
|
||||||
|
const canDeleteNetwork =
|
||||||
|
!isCurrentNetwork &&
|
||||||
|
!UNREMOVABLE_CHAIN_IDS.includes(network.chainId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NetworkListItem
|
||||||
|
name={network.nickname}
|
||||||
|
iconSrc={network?.rpcPrefs?.imageUrl}
|
||||||
|
key={network.id || network.chainId}
|
||||||
|
selected={isCurrentNetwork}
|
||||||
|
onClick={() => {
|
||||||
|
if (network.providerType) {
|
||||||
|
dispatch(setProviderType(network.providerType));
|
||||||
|
} else {
|
||||||
|
dispatch(setActiveNetwork(network.id));
|
||||||
|
}
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
onDeleteClick={
|
||||||
|
canDeleteNetwork
|
||||||
|
? () => {
|
||||||
|
dispatch(
|
||||||
|
showModal({
|
||||||
|
name: 'CONFIRM_DELETE_NETWORK',
|
||||||
|
target: network.id || network.chainId,
|
||||||
|
onConfirm: () => undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
closeMenu();
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
padding={4}
|
||||||
|
display={DISPLAY.FLEX}
|
||||||
|
justifyContent={JustifyContent.spaceBetween}
|
||||||
|
>
|
||||||
|
<Text>{t('showTestnetNetworks')}</Text>
|
||||||
|
<ToggleButton
|
||||||
|
value={showTestNetworks}
|
||||||
|
onToggle={(value) => dispatch(setShowTestNetworks(!value))}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box padding={4}>
|
||||||
|
<Button
|
||||||
|
type={BUTTON_TYPES.SECONDARY}
|
||||||
|
block
|
||||||
|
onClick={() => {
|
||||||
|
isFullScreen
|
||||||
|
? history.push(ADD_POPULAR_CUSTOM_NETWORK)
|
||||||
|
: global.platform.openExtensionInBrowser(
|
||||||
|
ADD_POPULAR_CUSTOM_NETWORK,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('addNetwork')}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
NetworkListMenu.propTypes = {
|
||||||
|
/**
|
||||||
|
* Executes when the menu should be closed
|
||||||
|
*/
|
||||||
|
closeMenu: PropTypes.func.isRequired,
|
||||||
|
};
|
@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import testData from '../../../../.storybook/test-data';
|
||||||
|
import configureStore from '../../../store/store';
|
||||||
|
import {
|
||||||
|
OPTIMISM_DISPLAY_NAME,
|
||||||
|
CHAIN_IDS,
|
||||||
|
OPTIMISM_TOKEN_IMAGE_URL,
|
||||||
|
BSC_DISPLAY_NAME,
|
||||||
|
BNB_TOKEN_IMAGE_URL,
|
||||||
|
} from '../../../../shared/constants/network';
|
||||||
|
import { NetworkListMenu } from '.';
|
||||||
|
|
||||||
|
const customNetworkStore = configureStore({
|
||||||
|
...testData,
|
||||||
|
metamask: {
|
||||||
|
...testData.metamask,
|
||||||
|
preferences: {
|
||||||
|
showTestNetworks: true,
|
||||||
|
},
|
||||||
|
networkConfigurations: {
|
||||||
|
...testData.metamask.networkConfigurations,
|
||||||
|
...{
|
||||||
|
'test-networkConfigurationId-3': {
|
||||||
|
rpcUrl: 'https://testrpc.com',
|
||||||
|
chainId: CHAIN_IDS.OPTIMISM,
|
||||||
|
nickname: OPTIMISM_DISPLAY_NAME,
|
||||||
|
rpcPrefs: { imageUrl: OPTIMISM_TOKEN_IMAGE_URL },
|
||||||
|
},
|
||||||
|
'test-networkConfigurationId-4': {
|
||||||
|
rpcUrl: 'https://testrpc.com',
|
||||||
|
chainId: CHAIN_IDS.BSC,
|
||||||
|
nickname: BSC_DISPLAY_NAME,
|
||||||
|
rpcPrefs: { imageUrl: BNB_TOKEN_IMAGE_URL },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Components/Multichain/NetworkListMenu',
|
||||||
|
component: NetworkListMenu,
|
||||||
|
argTypes: {
|
||||||
|
closeMenu: {
|
||||||
|
action: 'closeMenu',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DefaultStory = (args) => <NetworkListMenu {...args} />;
|
||||||
|
DefaultStory.decorators = [
|
||||||
|
(Story) => (
|
||||||
|
<Provider store={customNetworkStore}>
|
||||||
|
<Story />
|
||||||
|
</Provider>
|
||||||
|
),
|
||||||
|
];
|
@ -0,0 +1,61 @@
|
|||||||
|
/* eslint-disable jest/require-top-level-describe */
|
||||||
|
import React from 'react';
|
||||||
|
import { fireEvent, renderWithProvider } from '../../../../test/jest';
|
||||||
|
import configureStore from '../../../store/store';
|
||||||
|
import mockState from '../../../../test/data/mock-state.json';
|
||||||
|
import {
|
||||||
|
MAINNET_DISPLAY_NAME,
|
||||||
|
SEPOLIA_DISPLAY_NAME,
|
||||||
|
} from '../../../../shared/constants/network';
|
||||||
|
import { NetworkListMenu } from '.';
|
||||||
|
|
||||||
|
const mockSetShowTestNetworks = jest.fn();
|
||||||
|
const mockSetProviderType = jest.fn();
|
||||||
|
jest.mock('../../../store/actions.ts', () => ({
|
||||||
|
setShowTestNetworks: () => mockSetShowTestNetworks,
|
||||||
|
setProviderType: () => mockSetProviderType,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const render = (showTestNetworks = false) => {
|
||||||
|
const store = configureStore({
|
||||||
|
metamask: {
|
||||||
|
...mockState.metamask,
|
||||||
|
preferences: {
|
||||||
|
showTestNetworks,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return renderWithProvider(<NetworkListMenu closeMenu={jest.fn()} />, store);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('NetworkListMenu', () => {
|
||||||
|
it('displays important controls', () => {
|
||||||
|
const { getByText } = render();
|
||||||
|
|
||||||
|
expect(getByText('Add network')).toBeInTheDocument();
|
||||||
|
expect(getByText('Show test networks')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders mainnet item', () => {
|
||||||
|
const { getByText } = render();
|
||||||
|
expect(getByText(MAINNET_DISPLAY_NAME)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders test networks when it should', () => {
|
||||||
|
const { getByText } = render(true);
|
||||||
|
expect(getByText(SEPOLIA_DISPLAY_NAME)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles showTestNetworks when toggle is clicked', () => {
|
||||||
|
const { queryAllByRole } = render();
|
||||||
|
const [testNetworkToggle] = queryAllByRole('checkbox');
|
||||||
|
fireEvent.click(testNetworkToggle);
|
||||||
|
expect(mockSetShowTestNetworks).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('switches networks when an item is clicked', () => {
|
||||||
|
const { getByText } = render();
|
||||||
|
fireEvent.click(getByText(MAINNET_DISPLAY_NAME));
|
||||||
|
expect(mockSetProviderType).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -25,6 +25,10 @@ import {
|
|||||||
CHAIN_IDS,
|
CHAIN_IDS,
|
||||||
NETWORK_TYPES,
|
NETWORK_TYPES,
|
||||||
NetworkStatus,
|
NetworkStatus,
|
||||||
|
SEPOLIA_DISPLAY_NAME,
|
||||||
|
GOERLI_DISPLAY_NAME,
|
||||||
|
ETH_TOKEN_IMAGE_URL,
|
||||||
|
LINEA_TESTNET_DISPLAY_NAME,
|
||||||
} from '../../shared/constants/network';
|
} from '../../shared/constants/network';
|
||||||
import {
|
import {
|
||||||
WebHIDConnectedStatuses,
|
WebHIDConnectedStatuses,
|
||||||
@ -1116,6 +1120,63 @@ export function getNetworkConfigurations(state) {
|
|||||||
return state.metamask.networkConfigurations;
|
return state.metamask.networkConfigurations;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAllNetworks(state) {
|
||||||
|
const networkConfigurations = getNetworkConfigurations(state) || {};
|
||||||
|
const showTestnetNetworks = getShowTestNetworks(state);
|
||||||
|
const localhostFilter = (network) => network.chainId === CHAIN_IDS.LOCALHOST;
|
||||||
|
|
||||||
|
const networks = [];
|
||||||
|
// Mainnet always first
|
||||||
|
networks.push({
|
||||||
|
chainId: CHAIN_IDS.MAINNET,
|
||||||
|
nickname: MAINNET_DISPLAY_NAME,
|
||||||
|
rpcUrl: CHAIN_ID_TO_RPC_URL_MAP[CHAIN_IDS.MAINNET],
|
||||||
|
rpcPrefs: {
|
||||||
|
imageUrl: ETH_TOKEN_IMAGE_URL,
|
||||||
|
},
|
||||||
|
providerType: NETWORK_TYPES.MAINNET,
|
||||||
|
});
|
||||||
|
// Custom networks added
|
||||||
|
networks.push(
|
||||||
|
...Object.entries(networkConfigurations)
|
||||||
|
.filter(
|
||||||
|
([, network]) =>
|
||||||
|
!localhostFilter(network) && network.chainId !== CHAIN_IDS.MAINNET,
|
||||||
|
)
|
||||||
|
.map(([, network]) => network),
|
||||||
|
);
|
||||||
|
// Test networks if flag is on
|
||||||
|
if (showTestnetNetworks) {
|
||||||
|
networks.push(
|
||||||
|
...[
|
||||||
|
{
|
||||||
|
chainId: CHAIN_IDS.GOERLI,
|
||||||
|
nickname: GOERLI_DISPLAY_NAME,
|
||||||
|
rpcUrl: CHAIN_ID_TO_RPC_URL_MAP[CHAIN_IDS.GOERLI],
|
||||||
|
providerType: NETWORK_TYPES.GOERLI,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chainId: CHAIN_IDS.SEPOLIA,
|
||||||
|
nickname: SEPOLIA_DISPLAY_NAME,
|
||||||
|
rpcUrl: CHAIN_ID_TO_RPC_URL_MAP[CHAIN_IDS.SEPOLIA],
|
||||||
|
providerType: NETWORK_TYPES.SEPOLIA,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
chainId: CHAIN_IDS.LINEA_TESTNET,
|
||||||
|
nickname: LINEA_TESTNET_DISPLAY_NAME,
|
||||||
|
rpcUrl: CHAIN_ID_TO_RPC_URL_MAP[CHAIN_IDS.LINEA_TESTNET],
|
||||||
|
provderType: NETWORK_TYPES.LINEA_TESTNET,
|
||||||
|
},
|
||||||
|
], // Localhosts
|
||||||
|
...Object.entries(networkConfigurations)
|
||||||
|
.filter(([, network]) => localhostFilter(network))
|
||||||
|
.map(([, network]) => network),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return networks;
|
||||||
|
}
|
||||||
|
|
||||||
export function getIsOptimism(state) {
|
export function getIsOptimism(state) {
|
||||||
return (
|
return (
|
||||||
getCurrentChainId(state) === CHAIN_IDS.OPTIMISM ||
|
getCurrentChainId(state) === CHAIN_IDS.OPTIMISM ||
|
||||||
|
@ -1,5 +1,10 @@
|
|||||||
import mockState from '../../test/data/mock-state.json';
|
import mockState from '../../test/data/mock-state.json';
|
||||||
import { KeyringType } from '../../shared/constants/keyring';
|
import { KeyringType } from '../../shared/constants/keyring';
|
||||||
|
import {
|
||||||
|
CHAIN_IDS,
|
||||||
|
LOCALHOST_DISPLAY_NAME,
|
||||||
|
MAINNET_DISPLAY_NAME,
|
||||||
|
} from '../../shared/constants/network';
|
||||||
import * as selectors from './selectors';
|
import * as selectors from './selectors';
|
||||||
|
|
||||||
describe('Selectors', () => {
|
describe('Selectors', () => {
|
||||||
@ -103,6 +108,51 @@ describe('Selectors', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#getAllNetworks', () => {
|
||||||
|
it('returns an array even if there are no custom networks', () => {
|
||||||
|
const networks = selectors.getAllNetworks({
|
||||||
|
metamask: {
|
||||||
|
preferences: {
|
||||||
|
showTestNetworks: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(networks instanceof Array).toBe(true);
|
||||||
|
// The only returning item should be Ethereum Mainnet
|
||||||
|
expect(networks).toHaveLength(1);
|
||||||
|
expect(networks[0].nickname).toStrictEqual(MAINNET_DISPLAY_NAME);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns more test networks with showTestNetworks on', () => {
|
||||||
|
const networks = selectors.getAllNetworks({
|
||||||
|
metamask: {
|
||||||
|
preferences: {
|
||||||
|
showTestNetworks: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(networks.length).toBeGreaterThan(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sorts Localhost to the bottom of the test lists', () => {
|
||||||
|
const networks = selectors.getAllNetworks({
|
||||||
|
metamask: {
|
||||||
|
preferences: {
|
||||||
|
showTestNetworks: true,
|
||||||
|
},
|
||||||
|
networkConfigurations: {
|
||||||
|
'some-config-name': {
|
||||||
|
chainId: CHAIN_IDS.LOCALHOST,
|
||||||
|
nickname: LOCALHOST_DISPLAY_NAME,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const lastItem = networks.pop();
|
||||||
|
expect(lastItem.nickname.toLowerCase()).toContain('localhost');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('#isHardwareWallet', () => {
|
describe('#isHardwareWallet', () => {
|
||||||
it('returns false if it is not a HW wallet', () => {
|
it('returns false if it is not a HW wallet', () => {
|
||||||
mockState.metamask.keyrings[0].type = KeyringType.imported;
|
mockState.metamask.keyrings[0].type = KeyringType.imported;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user