diff --git a/app/_locales/de/messages.json b/app/_locales/de/messages.json
index dd34a61bf4e4..750420f06675 100644
--- a/app/_locales/de/messages.json
+++ b/app/_locales/de/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "Zusätzliche Netzwerke"
},
- "additionalRpcUrl": {
- "message": "Weitere RPC-URL"
- },
"address": {
"message": "Adresse"
},
diff --git a/app/_locales/el/messages.json b/app/_locales/el/messages.json
index 45be1ea60597..65b4f9f07bec 100644
--- a/app/_locales/el/messages.json
+++ b/app/_locales/el/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "Επιπλέον δίκτυα"
},
- "additionalRpcUrl": {
- "message": "Επιπλέον διεύθυνση URL RPC"
- },
"address": {
"message": "Διεύθυνση"
},
diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json
index 088c48829273..10b95c2e1c4f 100644
--- a/app/_locales/en/messages.json
+++ b/app/_locales/en/messages.json
@@ -345,9 +345,6 @@
"additionalNetworks": {
"message": "Additional networks"
},
- "additionalRpcUrl": {
- "message": "Additional RPC URL"
- },
"address": {
"message": "Address"
},
@@ -1842,6 +1839,9 @@
"ensUnknownError": {
"message": "ENS lookup failed."
},
+ "enterANameToIdentifyTheUrl": {
+ "message": "Enter a name to identify the URL"
+ },
"enterANumber": {
"message": "Enter a number"
},
@@ -1857,6 +1857,9 @@
"enterPasswordContinue": {
"message": "Enter password to continue"
},
+ "enterRpcUrl": {
+ "message": "Enter RPC URL"
+ },
"enterTokenNameOrAddress": {
"message": "Enter token name or paste address"
},
@@ -4528,6 +4531,9 @@
"revokeSpendingCapTooltipText": {
"message": "This third party will be unable to spend any more of your current or future tokens."
},
+ "rpcNameOptional": {
+ "message": "RPC Name (Optional)"
+ },
"rpcUrl": {
"message": "New RPC URL"
},
@@ -5435,6 +5441,9 @@
"supportCenter": {
"message": "Visit our support center"
},
+ "supportMultiRpcInformation": {
+ "message": "We now support multiple RPCs for a single network. Your most recent RPC has been selected as the default one to resolve conflicting information."
+ },
"surveyConversion": {
"message": "Take our survey"
},
@@ -6297,6 +6306,9 @@
"updateRequest": {
"message": "Update request"
},
+ "updatedRpcForNetworks": {
+ "message": "Network RPCs Updated"
+ },
"updatedWithDate": {
"message": "Updated $1"
},
diff --git a/app/_locales/es/messages.json b/app/_locales/es/messages.json
index 4bf761eef0ba..2cfa0ad872c3 100644
--- a/app/_locales/es/messages.json
+++ b/app/_locales/es/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "Redes adicionales"
},
- "additionalRpcUrl": {
- "message": "URL RPC adicional"
- },
"address": {
"message": "Dirección"
},
diff --git a/app/_locales/fr/messages.json b/app/_locales/fr/messages.json
index 8980dfb0fd48..1027e859fbf6 100644
--- a/app/_locales/fr/messages.json
+++ b/app/_locales/fr/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "Réseaux supplémentaires"
},
- "additionalRpcUrl": {
- "message": "URL supplémentaire de RPC"
- },
"address": {
"message": "Adresse"
},
diff --git a/app/_locales/hi/messages.json b/app/_locales/hi/messages.json
index 2d0caae66f01..57ee2cf4cccb 100644
--- a/app/_locales/hi/messages.json
+++ b/app/_locales/hi/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "अतिरिक्त नेटवर्क"
},
- "additionalRpcUrl": {
- "message": "अतिरिक्त RPC URL"
- },
"address": {
"message": "एड्रेस"
},
diff --git a/app/_locales/id/messages.json b/app/_locales/id/messages.json
index 4047a026b379..8a4198e34b9b 100644
--- a/app/_locales/id/messages.json
+++ b/app/_locales/id/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "Jaringan tambahan"
},
- "additionalRpcUrl": {
- "message": "URL RPC Tambahan"
- },
"address": {
"message": "Alamat"
},
diff --git a/app/_locales/ja/messages.json b/app/_locales/ja/messages.json
index 46f0ef9d1d95..39e329fd2809 100644
--- a/app/_locales/ja/messages.json
+++ b/app/_locales/ja/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "他のネットワーク"
},
- "additionalRpcUrl": {
- "message": "他のRPC URL"
- },
"address": {
"message": "アドレス"
},
diff --git a/app/_locales/ko/messages.json b/app/_locales/ko/messages.json
index 904cd16deb49..b5c85683bc55 100644
--- a/app/_locales/ko/messages.json
+++ b/app/_locales/ko/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "추가 네트워크"
},
- "additionalRpcUrl": {
- "message": "추가 RPC URL"
- },
"address": {
"message": "주소"
},
diff --git a/app/_locales/pt/messages.json b/app/_locales/pt/messages.json
index a1340fd2568a..3e38602dbf97 100644
--- a/app/_locales/pt/messages.json
+++ b/app/_locales/pt/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "Redes adicionais"
},
- "additionalRpcUrl": {
- "message": "URL da RPC adicional"
- },
"address": {
"message": "Endereço"
},
diff --git a/app/_locales/ru/messages.json b/app/_locales/ru/messages.json
index 8c94d3ad9074..c16fbf8b770f 100644
--- a/app/_locales/ru/messages.json
+++ b/app/_locales/ru/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "Дополнительные сети"
},
- "additionalRpcUrl": {
- "message": "Дополнительный URL-адрес RPC"
- },
"address": {
"message": "Адрес"
},
diff --git a/app/_locales/tl/messages.json b/app/_locales/tl/messages.json
index 1b86e07a8628..5f5f3d73d6c4 100644
--- a/app/_locales/tl/messages.json
+++ b/app/_locales/tl/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "Mga karagdagang network"
},
- "additionalRpcUrl": {
- "message": "Karagdagang RPC URL"
- },
"address": {
"message": "Address"
},
diff --git a/app/_locales/tr/messages.json b/app/_locales/tr/messages.json
index f1833f56875d..88709f34eea8 100644
--- a/app/_locales/tr/messages.json
+++ b/app/_locales/tr/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "İlave ağlar"
},
- "additionalRpcUrl": {
- "message": "Diğer RPC URL"
- },
"address": {
"message": "Adres"
},
diff --git a/app/_locales/vi/messages.json b/app/_locales/vi/messages.json
index 0f1d335eb92b..09720f543a32 100644
--- a/app/_locales/vi/messages.json
+++ b/app/_locales/vi/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "Mạng bổ sung"
},
- "additionalRpcUrl": {
- "message": "URL RPC Bổ sung"
- },
"address": {
"message": "Địa chỉ"
},
diff --git a/app/_locales/zh_CN/messages.json b/app/_locales/zh_CN/messages.json
index 584299b4c155..ea37ffaddd49 100644
--- a/app/_locales/zh_CN/messages.json
+++ b/app/_locales/zh_CN/messages.json
@@ -332,9 +332,6 @@
"additionalNetworks": {
"message": "其他网络"
},
- "additionalRpcUrl": {
- "message": "其他 RPC(远程过程调用)URL"
- },
"address": {
"message": "地址"
},
diff --git a/ui/components/app/multi-rpc-edit-modal/__snapshots__/multi-rpc-edit-modal.test.tsx.snap b/ui/components/app/multi-rpc-edit-modal/__snapshots__/multi-rpc-edit-modal.test.tsx.snap
new file mode 100644
index 000000000000..7c77aff77a3e
--- /dev/null
+++ b/ui/components/app/multi-rpc-edit-modal/__snapshots__/multi-rpc-edit-modal.test.tsx.snap
@@ -0,0 +1,85 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MultiRpcEditModal renders correctly with required props 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ updatedRpcForNetworks
+
+
+ supportMultiRpcInformation
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/ui/components/app/multi-rpc-edit-modal/multi-rpc-edit-modal.test.tsx b/ui/components/app/multi-rpc-edit-modal/multi-rpc-edit-modal.test.tsx
new file mode 100644
index 000000000000..59f11999deb2
--- /dev/null
+++ b/ui/components/app/multi-rpc-edit-modal/multi-rpc-edit-modal.test.tsx
@@ -0,0 +1,113 @@
+import React from 'react';
+import { render, screen } from '@testing-library/react';
+import { Provider } from 'react-redux';
+import configureStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import MultiRpcEditModal from './multi-rpc-edit-modal';
+import '@testing-library/jest-dom/extend-expect';
+
+const mockStore = configureStore([thunk]);
+
+const initialState = {
+ metamask: {
+ networkConfigurationsByChainId: {
+ '0x1': {
+ blockExplorerUrls: [],
+ chainId: '0x1',
+ defaultRpcEndpointIndex: 0,
+ name: 'Ethereum Mainnet',
+ nativeCurrency: 'ETH',
+ rpcEndpoints: [
+ {
+ name: 'Eth test 1',
+ networkClientId: '96d93309-dab5-45dd-9fff-0d9d7f0843cc',
+ type: 'custom',
+ url: 'https://eth-mainnet.public.blastapi.io',
+ },
+ {
+ networkClientId: 'mainnet',
+ type: 'infura',
+ url: 'https://mainnet.infura.io/v3/{infuraProjectId}',
+ },
+ {
+ name: 'Alchemyyyy',
+ networkClientId: '40cd2a17-1085-4077-8ffb-1ea1bdc65289',
+ type: 'custom',
+ url: 'https://eth-mainnet.g.alchemy.com/v2/fCe_AL0z95whoz8H6hvdKvwNAE3goTa0',
+ },
+ {
+ name: 'onfinality',
+ networkClientId: '42d0d494-b92f-43f0-9270-51eb660d35a0',
+ type: 'custom',
+ url: 'https://eth.api.onfinality.io/public',
+ },
+ {
+ name: 'mevBlocker',
+ networkClientId: '53107ebb-6184-44b5-ae53-057772795de7',
+ type: 'custom',
+ url: 'https://rpc.mevblocker.io',
+ },
+ ],
+ },
+ },
+ },
+ TransactionController: {
+ transactions: [
+ {
+ chainId: '0x1',
+ history: [{ networkClientId: 'mainnet' }],
+ },
+ ],
+ },
+};
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const setup = (props: any) => {
+ const store = mockStore(initialState);
+ return render(
+
+
+ ,
+ );
+};
+
+jest.mock('../../../hooks/useI18nContext', () => ({
+ useI18nContext: jest.fn(),
+}));
+
+describe('MultiRpcEditModal', () => {
+ const useI18nContextMock = useI18nContext as jest.Mock;
+
+ beforeEach(() => {
+ useI18nContextMock.mockReturnValue((key: string) => key);
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('renders correctly with required props', () => {
+ const onCloseMock = jest.fn();
+
+ const { baseElement } = setup({
+ isOpen: true,
+ onClose: onCloseMock,
+ });
+ expect(baseElement).toMatchSnapshot();
+ });
+
+ it('renders the modal and displays the network information', () => {
+ const onCloseMock = jest.fn();
+
+ setup({
+ isOpen: true,
+ onClose: onCloseMock,
+ });
+
+ expect(screen.getByTestId('multi-rpc-edit-modal')).toBeInTheDocument();
+ // TODO: enable with network controller v21 upgrade after `getNetworkConfigurationsByChainId` is implemented
+ // expect(screen.getByText('Ethereum Mainnet')).toBeInTheDocument();
+ expect(screen.getByText('supportMultiRpcInformation')).toBeInTheDocument();
+ });
+});
diff --git a/ui/components/app/multi-rpc-edit-modal/multi-rpc-edit-modal.tsx b/ui/components/app/multi-rpc-edit-modal/multi-rpc-edit-modal.tsx
new file mode 100644
index 000000000000..7dab45d4a9d7
--- /dev/null
+++ b/ui/components/app/multi-rpc-edit-modal/multi-rpc-edit-modal.tsx
@@ -0,0 +1,97 @@
+import React from 'react';
+import { useDispatch } from 'react-redux';
+import { Hex } from '@metamask/utils';
+import {
+ Modal,
+ ModalContent,
+ ModalOverlay,
+ ModalBody,
+ ModalFooter,
+ Box,
+ Text,
+} from '../../component-library';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import {
+ AlignItems,
+ Display,
+ FlexDirection,
+ JustifyContent,
+ BorderRadius,
+ TextAlign,
+ TextVariant,
+} from '../../../helpers/constants/design-system';
+import { setShowMultiRpcModal } from '../../../store/actions';
+import { getEnvironmentType } from '../../../../app/scripts/lib/util';
+import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app';
+import NetworkListItem from './network-list-item/network-list-item';
+
+function MultiRpcEditModal() {
+ const t = useI18nContext();
+ const dispatch = useDispatch();
+ const isPopUp = getEnvironmentType() === ENVIRONMENT_TYPE_POPUP;
+
+ // TODO: useSelector(getNetworkConfigurationsByChainId) with network controller v21 upgrade
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const networkConfigurations = {} as Record;
+
+ return (
+ dispatch(setShowMultiRpcModal(false))}
+ isClosedOnOutsideClick={false}
+ isClosedOnEscapeKey={false}
+ data-testid="multi-rpc-edit-modal"
+ autoFocus={false}
+ >
+
+
+
+
+
+
+
+ {t('updatedRpcForNetworks')}
+
+
+
+ {t('supportMultiRpcInformation')}
+
+
+
+
+ {Object.values(networkConfigurations).map(
+ (networkConfiguration) =>
+ networkConfiguration.rpcEndpoints.length > 1 ? (
+
+ ) : null,
+ )}
+
+
+
+ {
+ dispatch(setShowMultiRpcModal(false));
+ }}
+ submitButtonProps={{
+ children: t('accept'),
+ block: true,
+ }}
+ />
+
+
+ );
+}
+
+export default MultiRpcEditModal;
diff --git a/ui/components/app/multi-rpc-edit-modal/network-list-item/__snapshots__/network-list-item.test.tsx.snap b/ui/components/app/multi-rpc-edit-modal/network-list-item/__snapshots__/network-list-item.test.tsx.snap
new file mode 100644
index 000000000000..acb76cf700c8
--- /dev/null
+++ b/ui/components/app/multi-rpc-edit-modal/network-list-item/__snapshots__/network-list-item.test.tsx.snap
@@ -0,0 +1,56 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NetworkListItem renders correctly with required props 1`] = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/ui/components/app/multi-rpc-edit-modal/network-list-item/network-list-item.test.tsx b/ui/components/app/multi-rpc-edit-modal/network-list-item/network-list-item.test.tsx
new file mode 100644
index 000000000000..19ed8e77120c
--- /dev/null
+++ b/ui/components/app/multi-rpc-edit-modal/network-list-item/network-list-item.test.tsx
@@ -0,0 +1,77 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import configureMockStore from 'redux-mock-store';
+import thunk from 'redux-thunk';
+import { Provider } from 'react-redux';
+import NetworkListItem from './network-list-item';
+import '@testing-library/jest-dom/extend-expect';
+
+const mockStore = configureMockStore([thunk]);
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const setup = (props: any) => {
+ const store = mockStore({});
+ return render(
+
+
+ ,
+ );
+};
+
+describe('NetworkListItem', () => {
+ it('renders correctly with required props', () => {
+ const { container } = setup({
+ networkConfiguration: {
+ chainId: '0x1',
+ name: 'Ethereum Mainnet',
+ defaultRpcEndpointIndex: 0,
+ rpcEndpoints: [
+ {
+ name: 'Infura Mainnet',
+ url: 'https://mainnet.infura.io/v3/YOUR_INFURA_KEY',
+ },
+ ],
+ },
+ });
+ expect(container).toMatchSnapshot();
+ });
+ it('renders the network list item with correct data', () => {
+ setup({
+ networkConfiguration: {
+ chainId: '0x1',
+ name: 'Ethereum Mainnet',
+ defaultRpcEndpointIndex: 0,
+ rpcEndpoints: [
+ {
+ name: 'Infura Mainnet',
+ url: 'https://mainnet.infura.io/v3/YOUR_INFURA_KEY',
+ },
+ ],
+ },
+ });
+
+ expect(screen.getByText('Ethereum Mainnet')).toBeInTheDocument();
+ expect(screen.getByText('Infura Mainnet')).toBeInTheDocument();
+ });
+
+ it('shows tooltip with rpcUrl on hover', () => {
+ setup({
+ networkConfiguration: {
+ chainId: '0x1',
+ name: 'Ethereum Mainnet',
+ defaultRpcEndpointIndex: 0,
+ rpcEndpoints: [
+ {
+ name: 'Infura Mainnet',
+ url: 'https://mainnet.infura.io/v3/YOUR_INFURA_KEY',
+ },
+ ],
+ },
+ });
+
+ fireEvent.mouseOver(screen.getByText('Infura Mainnet'));
+ expect(
+ screen.getByText('https://mainnet.infura.io/v3/YOUR_INFURA_KEY'),
+ ).toBeInTheDocument();
+ });
+});
diff --git a/ui/components/app/multi-rpc-edit-modal/network-list-item/network-list-item.tsx b/ui/components/app/multi-rpc-edit-modal/network-list-item/network-list-item.tsx
new file mode 100644
index 000000000000..551d34d70643
--- /dev/null
+++ b/ui/components/app/multi-rpc-edit-modal/network-list-item/network-list-item.tsx
@@ -0,0 +1,166 @@
+import React, { useState } from 'react';
+import { useDispatch } from 'react-redux';
+import {
+ Box,
+ Text,
+ AvatarNetwork,
+ AvatarNetworkSize,
+ Button,
+ Popover,
+ PopoverPosition,
+ ButtonVariant,
+} from '../../../component-library';
+import {
+ AlignItems,
+ BackgroundColor,
+ Display,
+ FlexDirection,
+ TextColor,
+ TextVariant,
+ TextAlign,
+ JustifyContent,
+ BlockSize,
+} from '../../../../helpers/constants/design-system';
+import { useI18nContext } from '../../../../hooks/useI18nContext';
+import { setEditedNetwork, toggleNetworkMenu } from '../../../../store/actions';
+import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../shared/constants/network';
+
+// TODO: Use version from network controller with v21 upgrade
+enum RpcEndpointType {
+ Custom = 'custom',
+ Infura = 'infura',
+}
+
+const NetworkListItem = ({
+ networkConfiguration,
+}: {
+ // TODO: `NetworkConfiguration` with network controller v21 upgrade
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ networkConfiguration: any;
+}) => {
+ const rpcEndpoint =
+ networkConfiguration.rpcEndpoints[
+ networkConfiguration.defaultRpcEndpointIndex
+ ];
+
+ const t = useI18nContext();
+ const [isOpenTooltip, setIsOpenTooltip] = useState(false);
+ const dispatch = useDispatch();
+
+ const [referenceElement, setReferenceElement] =
+ useState();
+ const setBoxRef = (anchorRef: HTMLElement | null) => {
+ setReferenceElement(anchorRef);
+ };
+
+ const handleMouseEnter = () => {
+ setIsOpenTooltip(true);
+ };
+
+ const handleMouseLeave = () => {
+ setIsOpenTooltip(false);
+ };
+
+ return (
+
+
+
+
+
+
+ {networkConfiguration.name}
+
+
+
+
+ {rpcEndpoint.name ?? new URL(rpcEndpoint.url).host}
+
+
+
+ {rpcEndpoint.type === RpcEndpointType.Infura
+ ? rpcEndpoint.url.replace('/v3/{infuraProjectId}', '')
+ : rpcEndpoint.url}
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export default NetworkListItem;
diff --git a/ui/components/multichain/dropdown-editor/dropdown-editor.tsx b/ui/components/multichain/dropdown-editor/dropdown-editor.tsx
new file mode 100644
index 000000000000..2bd7b4348e37
--- /dev/null
+++ b/ui/components/multichain/dropdown-editor/dropdown-editor.tsx
@@ -0,0 +1,260 @@
+import React, { ReactNode, useEffect, useRef, useState } from 'react';
+import classnames from 'classnames';
+import {
+ Box,
+ ButtonIcon,
+ ButtonIconSize,
+ Icon,
+ IconName,
+ IconSize,
+ Input,
+ Label,
+ Popover,
+ PopoverPosition,
+ Text,
+} from '../../component-library';
+import {
+ AlignItems,
+ BackgroundColor,
+ BorderColor,
+ BorderRadius,
+ Display,
+ IconColor,
+ JustifyContent,
+ TextColor,
+ TextVariant,
+} from '../../../helpers/constants/design-system';
+import { useI18nContext } from '../../../hooks/useI18nContext';
+import Tooltip from '../../ui/tooltip';
+
+export enum DropdownEditorStyle {
+ /** When open, the dropdown overlays elements that follow */
+ PopoverStyle,
+ /** When open, the dropdown pushes down elements that follow */
+ BoxStyle,
+}
+
+// A dropdown for selecting, adding, and deleting items.
+export const DropdownEditor = - ({
+ title,
+ placeholder,
+ items,
+ selectedItemIndex,
+ addButtonText,
+ error,
+ style,
+ onItemSelected,
+ onItemDeleted,
+ onItemAdd,
+ onDropdownOpened,
+ itemKey,
+ itemIsDeletable = () => true,
+ renderItem,
+ renderTooltip,
+ buttonDataTestId,
+}: {
+ title: string;
+ placeholder: string;
+ items?: Item[];
+ selectedItemIndex?: number;
+ addButtonText: string;
+ error?: boolean;
+ style: DropdownEditorStyle;
+ onItemSelected: (index: number) => void;
+ onItemDeleted: (deletedIndex: number, newSelectedIndex?: number) => void;
+ onItemAdd: () => void;
+ onDropdownOpened?: () => void;
+ itemKey: (item: Item) => string;
+ itemIsDeletable?: (item: Item, items: Item[]) => boolean;
+ renderItem: (item: Item, isList: boolean) => string | ReactNode;
+ renderTooltip: (item: Item, isList: boolean) => string | undefined;
+ buttonDataTestId: string;
+}) => {
+ const t = useI18nContext();
+ const dropdown = useRef(null);
+ const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+
+ const renderDropdownList = () => (
+
+ {items?.map((item, index) => {
+ const row = (
+ {
+ onItemSelected(index);
+ setIsDropdownOpen(false);
+ }}
+ className={classnames('dropdown-editor__item', {
+ 'dropdown-editor__item--selected': index === selectedItemIndex,
+ })}
+ >
+ {index === selectedItemIndex && (
+
+ )}
+ {renderItem(item, true)}
+ {itemIsDeletable(item, items) && (
+ {
+ e.stopPropagation();
+
+ // Determine which item should be selected after deletion
+ let newSelectedIndex;
+ if (selectedItemIndex === undefined || items.length <= 1) {
+ newSelectedIndex = undefined;
+ } else if (index === selectedItemIndex) {
+ newSelectedIndex = 0;
+ } else if (index > selectedItemIndex) {
+ newSelectedIndex = selectedItemIndex;
+ } else if (index < selectedItemIndex) {
+ newSelectedIndex = selectedItemIndex - 1;
+ }
+
+ onItemDeleted(index, newSelectedIndex);
+ }}
+ />
+ )}
+
+ );
+
+ const tooltip = renderTooltip(item, true);
+ return tooltip ? (
+
+ {row}
+
+ ) : (
+ row
+ );
+ })}
+
+
+
+
+ {addButtonText}
+
+
+
+ );
+
+ let borderColor = BorderColor.borderDefault;
+ if (error) {
+ borderColor = BorderColor.errorDefault;
+ } else if (isDropdownOpen) {
+ borderColor = BorderColor.primaryDefault;
+ }
+
+ // Call back in a useEffect so it triggers after the opening has rendered
+ useEffect(() => {
+ if (isDropdownOpen) {
+ onDropdownOpened?.();
+ }
+ }, [isDropdownOpen]);
+
+ const selectedItem = items?.[selectedItemIndex ?? -1];
+ const tooltip = selectedItem ? renderTooltip(selectedItem, false) : undefined;
+
+ const box = (
+ {
+ setIsDropdownOpen(!isDropdownOpen);
+ }}
+ className="dropdown-editor__item-dropdown"
+ display={Display.Flex}
+ alignItems={AlignItems.center}
+ justifyContent={JustifyContent.spaceBetween}
+ borderRadius={BorderRadius.LG}
+ borderColor={borderColor}
+ borderWidth={1}
+ paddingLeft={4}
+ paddingRight={4}
+ ref={dropdown}
+ >
+ {selectedItem ? (
+ renderItem(selectedItem, false)
+ ) : (
+
+ )}
+
+
+ );
+
+ return (
+
+
+ {tooltip ? (
+
+ {box}
+
+ ) : (
+ box
+ )}
+ {style === DropdownEditorStyle.PopoverStyle ? (
+ 0 ? 2 : 0}
+ paddingBottom={items && items.length > 0 ? 2 : 0}
+ paddingLeft={0}
+ matchWidth={true}
+ paddingRight={0}
+ className="dropdown-editor__item-popover"
+ referenceElement={dropdown.current}
+ position={PopoverPosition.Bottom}
+ isOpen={isDropdownOpen}
+ onClickOutside={() => setIsDropdownOpen(false)}
+ >
+ {renderDropdownList()}
+
+ ) : (
+
+ {renderDropdownList()}
+
+ )}
+
+ );
+};
diff --git a/ui/components/multichain/dropdown-editor/index.scss b/ui/components/multichain/dropdown-editor/index.scss
new file mode 100644
index 000000000000..a453085b2211
--- /dev/null
+++ b/ui/components/multichain/dropdown-editor/index.scss
@@ -0,0 +1,38 @@
+.dropdown-editor {
+ &__item-dropdown {
+ cursor: pointer;
+ word-break: break-all;
+ }
+
+ &__item {
+ position: relative;
+ }
+
+ &__item:hover {
+ cursor: pointer;
+ background-color: var(--color-background-default-hover);
+ }
+
+ &__item--selected,
+ &__item--selected:hover {
+ background-color: var(--color-primary-muted);
+ }
+
+ &__item-selected-pill {
+ width: 4px;
+ height: calc(100% - 8px);
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ }
+
+ &__item-popover {
+ z-index: 1;
+ }
+
+ &__item-placeholder {
+ cursor: pointer;
+ pointer-events: none;
+ user-select: none;
+ }
+}
diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss
index 5fda42a8141f..8ab0c6202a5a 100644
--- a/ui/components/multichain/multichain-components.scss
+++ b/ui/components/multichain/multichain-components.scss
@@ -18,10 +18,14 @@
@import 'connected-accounts-menu';
@import 'connected-site-menu';
@import 'create-named-snap-account';
+@import 'dropdown-editor';
@import 'token-list-item';
@import 'network-list-item';
@import 'network-list-item-menu';
@import 'network-list-menu';
+@import 'network-list-menu/add-rpc-url-modal';
+@import 'network-list-menu/add-block-explorer-modal';
+@import 'network-list-menu/select-rpc-url-modal';
@import 'product-tour-popover';
@import 'nft-item';
@import 'badge-status';
diff --git a/ui/components/multichain/network-list-menu/add-block-explorer-modal/add-block-explorer-modal.tsx b/ui/components/multichain/network-list-menu/add-block-explorer-modal/add-block-explorer-modal.tsx
new file mode 100644
index 000000000000..65b98b081921
--- /dev/null
+++ b/ui/components/multichain/network-list-menu/add-block-explorer-modal/add-block-explorer-modal.tsx
@@ -0,0 +1,91 @@
+import React, { useEffect, useState } from 'react';
+import {
+ Box,
+ ButtonPrimary,
+ ButtonPrimarySize,
+ FormTextField,
+ FormTextFieldSize,
+ HelpText,
+ HelpTextSeverity,
+} from '../../../component-library';
+import {
+ BackgroundColor,
+ BlockSize,
+ BorderRadius,
+ Display,
+ FlexDirection,
+ JustifyContent,
+ TextVariant,
+} from '../../../../helpers/constants/design-system';
+import { useI18nContext } from '../../../../hooks/useI18nContext';
+import { isWebUrl } from '../../../../../app/scripts/lib/util';
+
+const AddBlockExplorerModal = ({
+ onAdded,
+}: {
+ onAdded: (url: string) => void;
+}) => {
+ const t = useI18nContext();
+ const [url, setUrl] = useState();
+ const [error, setError] = useState();
+
+ useEffect(() => {
+ if (url && url?.length > 0 && !isWebUrl(url)) {
+ setError(t('urlErrorMsg'));
+ } else {
+ setError(undefined);
+ }
+ }, [url]);
+
+ return (
+
+
+ setUrl(e.target.value)}
+ />
+ {error && (
+ {error}
+ )}
+
+
+ {
+ if (url) {
+ onAdded(url);
+ }
+ }}
+ >
+ {t('addUrl')}
+
+
+
+ );
+};
+
+export default AddBlockExplorerModal;
diff --git a/ui/components/multichain/network-list-menu/add-block-explorer-modal/index.scss b/ui/components/multichain/network-list-menu/add-block-explorer-modal/index.scss
new file mode 100644
index 000000000000..196db3e6a48e
--- /dev/null
+++ b/ui/components/multichain/network-list-menu/add-block-explorer-modal/index.scss
@@ -0,0 +1,9 @@
+.add-block-explorer-modal {
+ overflow-y: auto;
+
+ &__footer {
+ position: sticky;
+ bottom: 0;
+ box-shadow: 0 0 8px 0 var(--color-shadow-default);
+ }
+}
diff --git a/ui/components/multichain/network-list-menu/add-rpc-url-modal/__snapshots__/add-rpc-url-modal.test.tsx.snap b/ui/components/multichain/network-list-menu/add-rpc-url-modal/__snapshots__/add-rpc-url-modal.test.tsx.snap
index 0fa9ff26c6ff..6f151cfe0c0e 100644
--- a/ui/components/multichain/network-list-menu/add-rpc-url-modal/__snapshots__/add-rpc-url-modal.test.tsx.snap
+++ b/ui/components/multichain/network-list-menu/add-rpc-url-modal/__snapshots__/add-rpc-url-modal.test.tsx.snap
@@ -3,36 +3,70 @@
exports[`AddRpcUrlModal should render correctly 1`] = `
-
+
+
+
+
+
-
+
+
`;
diff --git a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.test.tsx b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.test.tsx
index 65b8e889e94b..510ca7d98f05 100644
--- a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.test.tsx
+++ b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.test.tsx
@@ -17,25 +17,19 @@ describe('AddRpcUrlModal', () => {
});
it('should render correctly', () => {
- const { container } = render();
+ const { container } = render( undefined} />);
expect(container).toMatchSnapshot();
});
- it('should render the input field with the correct label', () => {
- render();
- const inputLabel = screen.getByLabelText('additionalRpcUrl');
- expect(inputLabel).toBeInTheDocument();
- });
-
it('should render the "Add URL" button with correct text', () => {
- render();
+ render( undefined} />);
const addButton = screen.getByRole('button', { name: 'addUrl' });
expect(addButton).toBeInTheDocument();
});
it('should call the appropriate function when "Add URL" button is clicked', () => {
const mockAddUrl = jest.fn();
- render();
+ render( null} />);
const addButton = screen.getByRole('button', { name: 'addUrl' });
userEvent.click(addButton);
expect(mockAddUrl).not.toHaveBeenCalled();
diff --git a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx
index 8a9ff53a74b5..b2086aeab2f5 100644
--- a/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx
+++ b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx
@@ -1,42 +1,109 @@
-import React from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import {
Box,
ButtonPrimary,
ButtonPrimarySize,
FormTextField,
+ FormTextFieldSize,
+ HelpText,
+ HelpTextSeverity,
} from '../../../component-library';
import {
+ BackgroundColor,
BlockSize,
+ BorderRadius,
Display,
+ FlexDirection,
+ JustifyContent,
TextVariant,
} from '../../../../helpers/constants/design-system';
import { useI18nContext } from '../../../../hooks/useI18nContext';
+import { isWebUrl } from '../../../../../app/scripts/lib/util';
-const AddRpcUrlModal = () => {
+const AddRpcUrlModal = ({
+ onAdded,
+}: {
+ onAdded: (url: string, name?: string) => void;
+}) => {
const t = useI18nContext();
+ const [url, setUrl] = useState();
+ const [error, setError] = useState();
+ const nameRef = useRef(null);
+
+ useEffect(() => {
+ if (url && !isWebUrl(url)) {
+ setError(isWebUrl(`https://${url}`) ? t('urlErrorMsg') : t('invalidRPC'));
+ } else {
+ setError(undefined);
+ }
+ }, [url]);
+
return (
-
-
+
+
+ setUrl(e.target.value)}
+ />
+ {error && (
+ {error}
+ )}
+
+
- ({})}
>
- {t('addUrl')}
-
+ {
+ if (url && !error && nameRef.current) {
+ onAdded(url, nameRef.current.value || undefined);
+ }
+ }}
+ >
+ {t('addUrl')}
+
+
);
};
diff --git a/ui/components/multichain/network-list-menu/add-rpc-url-modal/index.scss b/ui/components/multichain/network-list-menu/add-rpc-url-modal/index.scss
new file mode 100644
index 000000000000..27d1fe4c20fd
--- /dev/null
+++ b/ui/components/multichain/network-list-menu/add-rpc-url-modal/index.scss
@@ -0,0 +1,9 @@
+.add-rpc-modal {
+ overflow-y: auto;
+
+ &__footer {
+ position: sticky;
+ bottom: 0;
+ box-shadow: 0 0 8px 0 var(--color-shadow-default);
+ }
+}
diff --git a/ui/components/multichain/network-list-menu/rpc-list-item.tsx b/ui/components/multichain/network-list-menu/rpc-list-item.tsx
new file mode 100644
index 000000000000..086f9838d142
--- /dev/null
+++ b/ui/components/multichain/network-list-menu/rpc-list-item.tsx
@@ -0,0 +1,98 @@
+import React from 'react';
+import { infuraProjectId } from '../../../../shared/constants/network';
+import { Box, Text } from '../../component-library';
+import {
+ Display,
+ FlexDirection,
+ BorderStyle,
+ BorderColor,
+ TextColor,
+ TextVariant,
+ BackgroundColor,
+ BlockSize,
+} from '../../../helpers/constants/design-system';
+
+// TODO: Use version from network controller with v21 upgrade
+enum RpcEndpointType {
+ Custom = 'custom',
+ Infura = 'infura',
+}
+
+export const stripKeyFromInfuraUrl = (endpoint: string) => {
+ let modifiedEndpoint = endpoint;
+
+ if (modifiedEndpoint.endsWith('/v3/{infuraProjectId}')) {
+ modifiedEndpoint = modifiedEndpoint.replace('/v3/{infuraProjectId}', '');
+ } else if (modifiedEndpoint.endsWith(`/v3/${infuraProjectId}`)) {
+ modifiedEndpoint = modifiedEndpoint.replace(`/v3/${infuraProjectId}`, '');
+ }
+
+ return modifiedEndpoint;
+};
+
+export const stripProtocol = (endpoint: string) => {
+ const url = new URL(endpoint);
+ return `${url.host}${url.pathname === '/' ? '' : url.pathname}`;
+};
+
+// This components represents an RPC endpoint in a list,
+// currently when selecting or editing endpoints for a network.
+const RpcListItem = ({
+ rpcEndpoint,
+}: {
+ rpcEndpoint: {
+ name?: string;
+ url: string;
+ type: RpcEndpointType;
+ };
+}) => {
+ const { url, type } = rpcEndpoint;
+ const name = type === RpcEndpointType.Infura ? 'Infura' : rpcEndpoint.name;
+
+ const displayEndpoint = (endpoint?: string) =>
+ endpoint ? stripProtocol(stripKeyFromInfuraUrl(endpoint)) : '\u00A0';
+
+ const padding = name ? 2 : 4;
+
+ return (
+
+
+
+ {name || displayEndpoint(url)}
+
+
+ {name && (
+
+
+ {displayEndpoint(url)}
+
+
+ )}
+
+ );
+};
+
+export default RpcListItem;
diff --git a/ui/components/multichain/network-list-menu/select-rpc-url-modal/__snapshots__/select-rpc-url-modal.test.tsx.snap b/ui/components/multichain/network-list-menu/select-rpc-url-modal/__snapshots__/select-rpc-url-modal.test.tsx.snap
new file mode 100644
index 000000000000..ebb23db52186
--- /dev/null
+++ b/ui/components/multichain/network-list-menu/select-rpc-url-modal/__snapshots__/select-rpc-url-modal.test.tsx.snap
@@ -0,0 +1,69 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SelectRpcUrlModal Component renders select rpc url 1`] = `
+
+
+
+
+
+
+
+
+ Ethereum Mainnet
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/ui/components/multichain/network-list-menu/select-rpc-url-modal/index.scss b/ui/components/multichain/network-list-menu/select-rpc-url-modal/index.scss
new file mode 100644
index 000000000000..0f8cce816d4b
--- /dev/null
+++ b/ui/components/multichain/network-list-menu/select-rpc-url-modal/index.scss
@@ -0,0 +1,23 @@
+.select-rpc-url {
+ &__item {
+ position: relative;
+ }
+
+ &__item:hover {
+ cursor: pointer;
+ background-color: var(--color-background-default-hover);
+ }
+
+ &__item--selected,
+ &__item--selected:hover {
+ background-color: var(--color-primary-muted);
+ }
+
+ &__item-selected-pill {
+ width: 4px;
+ height: calc(100% - 8px);
+ position: absolute;
+ top: 4px;
+ left: 4px;
+ }
+}
diff --git a/ui/components/multichain/network-list-menu/select-rpc-url-modal/select-rpc-url-modal.test.tsx b/ui/components/multichain/network-list-menu/select-rpc-url-modal/select-rpc-url-modal.test.tsx
new file mode 100644
index 000000000000..8f3de3d488f9
--- /dev/null
+++ b/ui/components/multichain/network-list-menu/select-rpc-url-modal/select-rpc-url-modal.test.tsx
@@ -0,0 +1,170 @@
+import React from 'react';
+import { fireEvent, screen } from '@testing-library/react';
+import configureMockStore from 'redux-mock-store';
+import { renderWithProvider } from '../../../../../test/lib/render-helpers';
+import {
+ // TODO: Add this API with network controller v21 upgrade
+ // updateNetwork,
+ setActiveNetwork,
+ setEditedNetwork,
+ toggleNetworkMenu,
+} from '../../../../store/actions';
+import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../shared/constants/network';
+import { stripProtocol } from '../rpc-list-item';
+import { SelectRpcUrlModal } from './select-rpc-url-modal'; // Adjust the path as needed
+
+const mockDispatch = jest.fn();
+jest.mock('react-redux', () => ({
+ ...jest.requireActual('react-redux'),
+ useDispatch: () => mockDispatch,
+}));
+
+jest.mock('../../../../store/actions', () => ({
+ // TODO: Add this API with network controller v21 upgrade
+ // updateNetwork: jest.fn(),
+ setActiveNetwork: jest.fn(),
+ setEditedNetwork: jest.fn(),
+ toggleNetworkMenu: jest.fn(),
+}));
+
+const mockStore = configureMockStore();
+const networkConfiguration = {
+ chainId: '0x1',
+ name: 'Ethereum Mainnet',
+ rpcEndpoints: [
+ { url: 'https://mainnet.infura.io/v3/', networkClientId: 'mainnet' },
+ { url: 'https://rpc.flashbots.net/', networkClientId: 'flashbots' },
+ ],
+ defaultRpcEndpointIndex: 0,
+};
+
+const store = mockStore({
+ metamask: {
+ networks: [networkConfiguration],
+ activeNetwork: '0x1',
+ },
+});
+
+describe('SelectRpcUrlModal Component', () => {
+ beforeEach(() => {
+ mockDispatch.mockClear();
+ });
+
+ it('renders select rpc url', () => {
+ const { container } = renderWithProvider(
+ ,
+ store,
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('should render the component correctly with network image and name', () => {
+ const { getByRole, getByText } = renderWithProvider(
+ ,
+ store,
+ );
+
+ const imageSrc =
+ CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[
+ networkConfiguration.chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP
+ ];
+
+ const networkImage = getByRole('img');
+
+ expect(networkImage).toBeInTheDocument();
+ expect(networkImage).toHaveAttribute('src', imageSrc);
+ expect(getByText(networkConfiguration.name)).toBeInTheDocument();
+ });
+
+ it('should render all RPC endpoints and highlight the selected one', () => {
+ const { getByText } = renderWithProvider(
+ ,
+ store,
+ );
+
+ networkConfiguration.rpcEndpoints.forEach((rpcEndpoint) => {
+ expect(getByText(stripProtocol(rpcEndpoint.url))).toBeInTheDocument();
+ });
+
+ const selectedItem = getByText(
+ stripProtocol(networkConfiguration.rpcEndpoints[0].url),
+ ).closest('.select-rpc-url__item');
+
+ expect(selectedItem).toHaveClass('select-rpc-url__item--selected');
+ });
+
+ it('should dispatch the correct actions when an RPC endpoint is clicked', () => {
+ const { getByText } = renderWithProvider(
+ ,
+ store,
+ );
+
+ const rpcEndpoint = getByText(
+ stripProtocol(networkConfiguration.rpcEndpoints[1].url),
+ );
+ fireEvent.click(rpcEndpoint);
+
+ // TODO: Add this API with network controller v21 upgrade
+ // expect(mockDispatch).toHaveBeenCalledWith(
+ // updateNetwork({
+ // ...networkConfiguration,
+ // defaultRpcEndpointIndex: 1,
+ // }),
+ // );
+ expect(mockDispatch).toHaveBeenCalledWith(setActiveNetwork('flashbots'));
+ expect(mockDispatch).toHaveBeenCalledWith(setEditedNetwork());
+ expect(mockDispatch).toHaveBeenCalledWith(toggleNetworkMenu());
+ });
+
+ it('should render the selected indicator correctly for the default RPC', () => {
+ const { container } = renderWithProvider(
+ ,
+ store,
+ );
+
+ const selectedPill = container.querySelector(
+ '.select-rpc-url__item-selected-pill',
+ );
+ expect(selectedPill).toBeInTheDocument();
+ });
+
+ it('should render the modal with a network image', () => {
+ renderWithProvider(
+ ,
+ store,
+ );
+
+ const networkImage = screen.getByRole('img');
+ expect(networkImage).toBeInTheDocument();
+ expect(networkImage).toHaveAttribute(
+ 'src',
+ CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[
+ networkConfiguration.chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP
+ ],
+ );
+ });
+
+ it('should handle click on RPC URL and update the network', () => {
+ renderWithProvider(
+ ,
+ store,
+ );
+
+ fireEvent.click(
+ screen.getByText(stripProtocol(networkConfiguration.rpcEndpoints[1].url)),
+ );
+
+ // TODO: Add this API with network controller v21 upgrade
+ // expect(mockDispatch).toHaveBeenCalledWith(
+ // updateNetwork({
+ // ...networkConfiguration,
+ // defaultRpcEndpointIndex: 1,
+ // }),
+ // );
+ expect(mockDispatch).toHaveBeenCalledWith(
+ setActiveNetwork(networkConfiguration.rpcEndpoints[1].networkClientId),
+ );
+ expect(mockDispatch).toHaveBeenCalledWith(setEditedNetwork());
+ expect(mockDispatch).toHaveBeenCalledWith(toggleNetworkMenu());
+ });
+});
diff --git a/ui/components/multichain/network-list-menu/select-rpc-url-modal/select-rpc-url-modal.tsx b/ui/components/multichain/network-list-menu/select-rpc-url-modal/select-rpc-url-modal.tsx
new file mode 100644
index 000000000000..b16dddf5f86e
--- /dev/null
+++ b/ui/components/multichain/network-list-menu/select-rpc-url-modal/select-rpc-url-modal.tsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import classnames from 'classnames';
+import { useDispatch } from 'react-redux';
+import {
+ AvatarNetwork,
+ AvatarNetworkSize,
+ Box,
+ Text,
+} from '../../../component-library';
+import {
+ AlignItems,
+ BackgroundColor,
+ BorderRadius,
+ Display,
+ TextColor,
+ TextVariant,
+} from '../../../../helpers/constants/design-system';
+import { CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP } from '../../../../../shared/constants/network';
+import {
+ setActiveNetwork,
+ setEditedNetwork,
+ toggleNetworkMenu,
+} from '../../../../store/actions';
+import RpcListItem from '../rpc-list-item';
+
+export const SelectRpcUrlModal = ({
+ networkConfiguration,
+}: {
+ // TODO: `NetworkConfiguration` with network controller v21 upgrade
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ networkConfiguration: any;
+}) => {
+ const dispatch = useDispatch();
+
+ const image =
+ CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[
+ networkConfiguration.chainId as keyof typeof CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP
+ ];
+
+ return (
+
+
+
+ {image && (
+
+ )}
+
+ {networkConfiguration.name}
+
+
+
+
+ {networkConfiguration.rpcEndpoints.map(
+ // TODO: types will be inferred with network controller v21 upgrade
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (rpcEndpoint: any, index: number) => (
+ {
+ // TODO: When this API becomes available with network controller v21 upgrade
+ // dispatch(
+ // updateNetwork({
+ // ...networkConfiguration,
+ // defaultRpcEndpointIndex: index,
+ // }),
+ // );
+ dispatch(setActiveNetwork(rpcEndpoint.networkClientId));
+ dispatch(setEditedNetwork());
+ dispatch(toggleNetworkMenu());
+ }}
+ className={classnames('select-rpc-url__item', {
+ 'select-rpc-url__item--selected':
+ index === networkConfiguration.defaultRpcEndpointIndex,
+ })}
+ >
+ {index === networkConfiguration.defaultRpcEndpointIndex && (
+
+ )}
+
+
+ ),
+ )}
+
+ );
+};
+
+export default SelectRpcUrlModal;
diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts
index a16508c9a45a..d053b32e4e6e 100644
--- a/ui/ducks/app/app.ts
+++ b/ui/ducks/app/app.ts
@@ -99,6 +99,8 @@ type AppState = {
txId: string | null;
accountDetailsAddress: string;
snapsInstallPrivacyWarningShown: boolean;
+ isAddingNewNetwork: boolean;
+ isMultiRpcOnboarding: boolean;
};
type AppSliceState = {
@@ -181,6 +183,8 @@ const initialState: AppState = {
txId: null,
accountDetailsAddress: '',
snapsInstallPrivacyWarningShown: false,
+ isAddingNewNetwork: false,
+ isMultiRpcOnboarding: false,
};
export default function reduceApp(
@@ -583,6 +587,12 @@ export default function reduceApp(
...appState,
customTokenAmount: action.payload,
};
+ case actionConstants.TOGGLE_NETWORK_MENU:
+ return {
+ ...appState,
+ isAddingNewNetwork: Boolean(action.payload?.isAddingNewNetwork),
+ isMultiRpcOnboarding: Boolean(action.payload?.isMultiRpcOnboarding),
+ };
///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
case actionConstants.SHOW_KEYRING_SNAP_REMOVAL_RESULT:
return {
diff --git a/ui/store/actions.ts b/ui/store/actions.ts
index 6bf5ef82ae9f..0c6f61654d09 100644
--- a/ui/store/actions.ts
+++ b/ui/store/actions.ts
@@ -3067,6 +3067,10 @@ export function setSmartTransactionsOptInStatus(
};
}
+export function setShowMultiRpcModal(value: boolean) {
+ return setPreference('showMultiRpcModal', value);
+}
+
export function setAutoLockTimeLimit(value: number | null) {
return setPreference('autoLockTimeLimit', value);
}
@@ -3178,9 +3182,13 @@ export function toggleAccountMenu() {
};
}
-export function toggleNetworkMenu() {
+export function toggleNetworkMenu(payload?: {
+ isAddingNewNetwork: boolean;
+ isMultiRpcOnboarding: boolean;
+}) {
return {
type: actionConstants.TOGGLE_NETWORK_MENU,
+ payload,
};
}
@@ -4058,9 +4066,10 @@ export function setNewNetworkAdded({
export function setEditedNetwork(
payload:
| {
- networkConfigurationId: string;
+ chainId: string;
+ networkConfigurationId?: string;
nickname: string;
- editCompleted: boolean;
+ editCompleted?: boolean;
}
| undefined = undefined,
): PayloadAction