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": {
|
||||
"message": "Network is busy. Gas prices are high and estimates are less accurate."
|
||||
},
|
||||
"networkMenuHeading": {
|
||||
"message": "Select a network"
|
||||
},
|
||||
"networkName": {
|
||||
"message": "Network name"
|
||||
},
|
||||
|
@ -8,3 +8,5 @@ export { MultichainImportTokenLink } from './multichain-import-token-link';
|
||||
export { MultichainTokenListItem } from './multichain-token-list-item';
|
||||
export { AddressCopyButton } from './address-copy-button';
|
||||
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-picker/index';
|
||||
@import 'multichain-connected-site-menu/index';
|
||||
@import 'account-list-menu/';
|
||||
@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,
|
||||
NETWORK_TYPES,
|
||||
NetworkStatus,
|
||||
SEPOLIA_DISPLAY_NAME,
|
||||
GOERLI_DISPLAY_NAME,
|
||||
ETH_TOKEN_IMAGE_URL,
|
||||
LINEA_TESTNET_DISPLAY_NAME,
|
||||
} from '../../shared/constants/network';
|
||||
import {
|
||||
WebHIDConnectedStatuses,
|
||||
@ -1116,6 +1120,63 @@ export function getNetworkConfigurations(state) {
|
||||
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) {
|
||||
return (
|
||||
getCurrentChainId(state) === CHAIN_IDS.OPTIMISM ||
|
||||
|
@ -1,5 +1,10 @@
|
||||
import mockState from '../../test/data/mock-state.json';
|
||||
import { KeyringType } from '../../shared/constants/keyring';
|
||||
import {
|
||||
CHAIN_IDS,
|
||||
LOCALHOST_DISPLAY_NAME,
|
||||
MAINNET_DISPLAY_NAME,
|
||||
} from '../../shared/constants/network';
|
||||
import * as selectors from './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', () => {
|
||||
it('returns false if it is not a HW wallet', () => {
|
||||
mockState.metamask.keyrings[0].type = KeyringType.imported;
|
||||
|
Loading…
x
Reference in New Issue
Block a user