From 4ff3d6bc9bf665f2bec11d4fb4c5dd5b74355a0d Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 10 Jun 2024 20:39:20 -0500 Subject: [PATCH 01/11] Network Menu Redesign: Add 3-dot menu --- .../multichain/network-list-item/index.scss | 12 --- .../network-list-item/network-list-item.js | 94 +++++++++++++++++-- 2 files changed, 85 insertions(+), 21 deletions(-) diff --git a/ui/components/multichain/network-list-item/index.scss b/ui/components/multichain/network-list-item/index.scss index a7a1e6aa158e..ae68ea52dc3c 100644 --- a/ui/components/multichain/network-list-item/index.scss +++ b/ui/components/multichain/network-list-item/index.scss @@ -14,14 +14,6 @@ color: inherit; } - &:hover, - &:focus, - &:focus-within { - .multichain-network-list-item__delete { - visibility: visible; - } - } - &__network-name { width: 100%; flex: 1; @@ -44,8 +36,4 @@ top: 4px; left: 4px; } - - &__delete { - visibility: hidden; - } } diff --git a/ui/components/multichain/network-list-item/network-list-item.js b/ui/components/multichain/network-list-item/network-list-item.js index 6fe9336f434a..0783d1e50d84 100644 --- a/ui/components/multichain/network-list-item/network-list-item.js +++ b/ui/components/multichain/network-list-item/network-list-item.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import { @@ -10,19 +10,23 @@ import { Display, IconColor, JustifyContent, - Size, TextColor, } from '../../../helpers/constants/design-system'; import { AvatarNetwork, Box, ButtonIcon, + ButtonIconSize, IconName, + Popover, + PopoverPosition, + PopoverRole, Text, } from '../../component-library'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getAvatarNetworkColor } from '../../../helpers/utils/accounts'; import Tooltip from '../../ui/tooltip/tooltip'; +import { MenuItem } from '../../ui/menu'; const MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP = 20; @@ -33,9 +37,36 @@ export const NetworkListItem = ({ focus = true, onClick, onDeleteClick, + onEditClick, }) => { const t = useI18nContext(); const networkRef = useRef(); + const menuRef = useRef(null); + + const [networkOptionsMenuOpen, setNetworkOptionsMenuOpen] = useState(false); + + // Handle click outside of the popover to close it + const popoverDialogRef = useRef(null); + + const handleClickOutside = useCallback( + (event) => { + if ( + popoverDialogRef?.current && + !popoverDialogRef.current.contains(event.target) + ) { + setNetworkOptionsMenuOpen(false); + } + }, + [setNetworkOptionsMenuOpen], + ); + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [handleClickOutside]); useEffect(() => { if (networkRef.current && focus) { @@ -103,19 +134,60 @@ export const NetworkListItem = ({ )} - {onDeleteClick ? ( + {onDeleteClick || onEditClick ? ( { + e.preventDefault(); e.stopPropagation(); - onDeleteClick(); + + console.log(`Opening menu for ${name}`); + setNetworkOptionsMenuOpen(true); }} + size={ButtonIconSize.Sm} /> ) : null} + setNetworkOptionsMenuOpen(false)} + role={PopoverRole.Dialog} + position={PopoverPosition.Bottom} + offset={[0, 0]} + > + {onEditClick ? ( + { + e.stopPropagation(); + + // Pass network info? + onEditClick(); + }} + data-testid="network-list-item-options-edit" + > + {t('edit')} + + ) : null} + {onDeleteClick ? ( + { + e.stopPropagation(); + + // Pass network info? + onDeleteClick(); + }} + data-testid="network-list-item-options-delete" + color={TextColor.errorDefault} + > + {t('delete')} + + ) : null} + ); }; @@ -141,6 +213,10 @@ NetworkListItem.propTypes = { * Executes when the delete icon is clicked */ onDeleteClick: PropTypes.func, + /** + * Executes when the edit icon is clicked + */ + onEditClick: PropTypes.func, /** * Represents if the network item should be keyboard selected */ From adb9efab0852fbd4fc3841c67f90766f4e25a89a Mon Sep 17 00:00:00 2001 From: salimtb Date: Wed, 12 Jun 2024 19:24:58 +0200 Subject: [PATCH 02/11] fix: resolve conflicts --- .../network-list-menu/network-list-menu.js | 402 ++++++++++-------- 1 file changed, 214 insertions(+), 188 deletions(-) diff --git a/ui/components/multichain/network-list-menu/network-list-menu.js b/ui/components/multichain/network-list-menu/network-list-menu.js index 6c276e326062..52c0d729a23a 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.js @@ -162,7 +162,6 @@ export const NetworkListMenu = ({ onClose }) => { }, [dispatch, currentlyOnTestNetwork]); const [searchQuery, setSearchQuery] = useState(''); - const [focusSearch, setFocusSearch] = useState(false); const onboardedInThisUISession = useSelector(getOnboardedInThisUISession); const showNetworkBanner = useSelector(getShowNetworkBanner); const showBanner = @@ -196,9 +195,9 @@ export const NetworkListMenu = ({ onClose }) => { ? items : [...notExistingNetworkConfigurations]; - let searchTestNetworkResults = [...testNetworks]; + const isSearching = searchQuery !== ''; - if (focusSearch && searchQuery !== '') { + if (isSearching) { const fuse = new Fuse(searchResults, { threshold: 0.2, location: 0, @@ -218,25 +217,11 @@ export const NetworkListMenu = ({ onClose }) => { keys: ['nickname', 'chainId', 'ticker'], }); - const fuseForTestsNetworks = new Fuse(searchTestNetworkResults, { - threshold: 0.2, - location: 0, - distance: 100, - maxPatternLength: 32, - minMatchCharLength: 1, - shouldSort: true, - keys: ['nickname', 'chainId', 'ticker'], - }); - fuse.setCollection(searchResults); fuseForPopularNetworks.setCollection(searchAddNetworkResults); - fuseForTestsNetworks.setCollection(searchTestNetworkResults); - const fuseResults = fuse.search(searchQuery); const fuseForPopularNetworksResults = fuseForPopularNetworks.search(searchQuery); - const fuseForTestsNetworksResults = - fuseForTestsNetworks.search(searchQuery); searchResults = searchResults.filter((network) => fuseResults.includes(network), @@ -244,65 +229,25 @@ export const NetworkListMenu = ({ onClose }) => { searchAddNetworkResults = searchAddNetworkResults.filter((network) => fuseForPopularNetworksResults.includes(network), ); - searchTestNetworkResults = searchTestNetworkResults.filter((network) => - fuseForTestsNetworksResults.includes(network), - ); } - const generateNetworkListItem = ({ - network, - isCurrentNetwork, - canDeleteNetwork, - }) => { - return ( - { - dispatch(toggleNetworkMenu()); - if (network.providerType) { - dispatch(setProviderType(network.providerType)); - } else { - dispatch(setActiveNetwork(network.id)); - } - - // If presently on a dapp, communicate a change to - // the dapp via silent switchEthereumChain that the - // network has changed due to user action - if (useRequestQueue && selectedTabOrigin) { - setNetworkClientIdForDomain(selectedTabOrigin, network.id); - } + const getOnDeleteCallback = (networkId) => { + return () => { + dispatch(toggleNetworkMenu()); + dispatch( + showModal({ + name: 'CONFIRM_DELETE_NETWORK', + target: networkId, + onConfirm: () => undefined, + }), + ); + }; + }; - trackEvent({ - event: MetaMetricsEventName.NavNetworkSwitched, - category: MetaMetricsEventCategory.Network, - properties: { - location: 'Network Menu', - chain_id: currentChainId, - from_network: currentChainId, - to_network: network.chainId, - }, - }); - }} - onDeleteClick={ - canDeleteNetwork - ? () => { - dispatch(toggleNetworkMenu()); - dispatch( - showModal({ - name: 'CONFIRM_DELETE_NETWORK', - target: network.id, - onConfirm: () => undefined, - }), - ); - } - : null - } - /> - ); + const getOnEditCallback = (networkId) => { + return () => { + console.log('Get onEditCallback for: ', networkId); + }; }; const generateMenuItems = (desiredNetworks) => { @@ -314,11 +259,45 @@ export const NetworkListMenu = ({ onClose }) => { const canDeleteNetwork = isUnlocked && !isCurrentNetwork && network.removable; - return generateNetworkListItem({ - network, - isCurrentNetwork, - canDeleteNetwork, - }); + return ( + { + dispatch(toggleNetworkMenu()); + if (network.providerType) { + dispatch(setProviderType(network.providerType)); + } else { + dispatch(setActiveNetwork(network.id)); + } + trackEvent({ + event: MetaMetricsEventName.NavNetworkSwitched, + category: MetaMetricsEventCategory.Network, + properties: { + location: 'Network Menu', + chain_id: currentChainId, + from_network: currentChainId, + to_network: network.chainId, + }, + }); + }} + onDeleteClick={ + canDeleteNetwork + ? () => { + dispatch(toggleNetworkMenu()); + getOnDeleteCallback(network.id); + } + : null + } + onEditClick={() => { + dispatch(toggleNetworkMenu()); + getOnEditCallback(network.id); + }} + /> + ); }); }; @@ -364,121 +343,168 @@ export const NetworkListMenu = ({ onClose }) => { - {showBanner ? ( - - drag-and-drop - - } - onClose={() => hideNetworkBanner()} - description={t('dragAndDropBanner')} - /> - ) : null} + - - {t('enabledNetworks')} - - {searchResults.length === 0 && focusSearch ? ( - - {t('noNetworksFound')} - - ) : ( - - - {(provided) => ( - - {searchResults.map((network, index) => { - const isCurrentNetwork = - currentNetwork.id === network.id; - - const canDeleteNetwork = - isUnlocked && - !isCurrentNetwork && - network.removable; - - const networkListItem = generateNetworkListItem({ - network, - isCurrentNetwork, - canDeleteNetwork, - }); - - return ( - - {(providedDrag) => ( - - {networkListItem} - - )} - - ); - })} - {provided.placeholder} - - )} - - - )} - {networkMenuRedesign ? ( - + drag-and-drop + + } + onClose={() => hideNetworkBanner()} + description={t('dragAndDropBanner')} /> ) : null} - - {t('showTestnetNetworks')} - - - {showTestNetworks || currentlyOnTestNetwork ? ( - - {generateMenuItems(searchTestNetworkResults)} + + {searchResults.length === 0 && isSearching ? ( + + {t('noNetworksFound')} + + ) : ( + + + {(provided) => ( + + {searchResults.map((network, index) => { + const isCurrentNetwork = + currentNetwork.id === network.id; + + const canDeleteNetwork = + isUnlocked && + !isCurrentNetwork && + network.removable; + + return ( + + {(providedDrag) => ( + + { + dispatch(toggleNetworkMenu()); + if (network.providerType) { + dispatch( + setProviderType( + network.providerType, + ), + ); + } else { + dispatch( + setActiveNetwork(network.id), + ); + } + + // If presently on a dapp, communicate a change to + // the dapp via silent switchEthereumChain that the + // network has changed due to user action + if ( + useRequestQueue && + selectedTabOrigin + ) { + setNetworkClientIdForDomain( + selectedTabOrigin, + network.id, + ); + } + + trackEvent({ + event: + MetaMetricsEventName.NavNetworkSwitched, + category: + MetaMetricsEventCategory.Network, + properties: { + location: 'Network Menu', + chain_id: currentChainId, + from_network: currentChainId, + to_network: network.chainId, + }, + }); + }} + onDeleteClick={ + canDeleteNetwork + ? () => { + dispatch(toggleNetworkMenu()); + getOnDeleteCallback(network.id); + } + : null + } + onEditClick={() => { + dispatch(toggleNetworkMenu()); + getOnEditCallback(network.id); + }} + /> + + )} + + ); + })} + {provided.placeholder} + + )} + + + )} + {networkMenuRedesign ? ( + + ) : null} + + {t('showTestnetNetworks')} + - ) : null} + {showTestNetworks || currentlyOnTestNetwork ? ( + + {generateMenuItems(testNetworks)} + + ) : null} + + { ) : ( - + )} From 04a4c0287acb513b7dc2aae225b845e6378d1aa1 Mon Sep 17 00:00:00 2001 From: David Walsh Date: Mon, 10 Jun 2024 21:21:34 -0500 Subject: [PATCH 03/11] Add NetworkListItemMenu --- app/_locales/en/messages.json | 3 + .../multichain/multichain-components.scss | 1 + .../network-list-item-menu/index.js | 1 + .../network-list-item-menu/index.scss | 8 + .../network-list-item-menu.js | 152 ++++++ .../network-list-item.test.js.snap | 7 +- .../network-list-item/network-list-item.js | 86 +--- .../network-list-item.test.js | 10 +- .../network-list-menu/network-list-menu.js | 441 +++++++++--------- ui/helpers/utils/network-helper.test.ts | 55 ++- ui/helpers/utils/network-helper.ts | 16 + .../add-network-modal/index.js | 24 +- .../networks-form/networks-form.js | 32 +- 13 files changed, 530 insertions(+), 306 deletions(-) create mode 100644 ui/components/multichain/network-list-item-menu/index.js create mode 100644 ui/components/multichain/network-list-item-menu/index.scss create mode 100644 ui/components/multichain/network-list-item-menu/network-list-item-menu.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index be8313a1fb4a..78f2f87ecb29 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2885,6 +2885,9 @@ "networkNameZkSyncEra": { "message": "zkSync Era" }, + "networkOptions": { + "message": "Network options" + }, "networkProvider": { "message": "Network provider" }, diff --git a/ui/components/multichain/multichain-components.scss b/ui/components/multichain/multichain-components.scss index 8917088b40b4..c4b043c69b1f 100644 --- a/ui/components/multichain/multichain-components.scss +++ b/ui/components/multichain/multichain-components.scss @@ -19,6 +19,7 @@ @import 'connected-site-menu'; @import 'token-list-item'; @import 'network-list-item'; +@import 'network-list-item-menu'; @import 'network-list-menu'; @import 'product-tour-popover'; @import 'nft-item'; diff --git a/ui/components/multichain/network-list-item-menu/index.js b/ui/components/multichain/network-list-item-menu/index.js new file mode 100644 index 000000000000..b21bbd464bd7 --- /dev/null +++ b/ui/components/multichain/network-list-item-menu/index.js @@ -0,0 +1 @@ +export { NetworkListItemMenu } from './network-list-item-menu'; diff --git a/ui/components/multichain/network-list-item-menu/index.scss b/ui/components/multichain/network-list-item-menu/index.scss new file mode 100644 index 000000000000..62685b151700 --- /dev/null +++ b/ui/components/multichain/network-list-item-menu/index.scss @@ -0,0 +1,8 @@ +@use "design-system"; + +.multichain-network-list-item-menu__popover { + z-index: design-system.$popover-in-modal-z-index; + overflow: hidden; + min-width: 225px; + max-width: 225px; +} diff --git a/ui/components/multichain/network-list-item-menu/network-list-item-menu.js b/ui/components/multichain/network-list-item-menu/network-list-item-menu.js new file mode 100644 index 000000000000..6618cee57610 --- /dev/null +++ b/ui/components/multichain/network-list-item-menu/network-list-item-menu.js @@ -0,0 +1,152 @@ +import React, { useCallback, useEffect, useRef } from 'react'; +import PropTypes from 'prop-types'; +import { useI18nContext } from '../../../hooks/useI18nContext'; +import { + IconName, + ModalFocus, + Popover, + PopoverPosition, + PopoverRole, + Text, +} from '../../component-library'; +import { MenuItem } from '../../ui/menu'; +import { IconColor, TextColor } from '../../../helpers/constants/design-system'; + +export const NetworkListItemMenu = ({ + anchorElement, + onClose, + onEditClick, + onDeleteClick, + isOpen, +}) => { + const t = useI18nContext(); + + // Handle Tab key press for accessibility inside the popover and will close the popover on the last MenuItem + const lastItemRef = useRef(null); + const accountDetailsItemRef = useRef(null); + const removeAccountItemRef = useRef(null); + const removeJWTItemRef = useRef(null); + + // Checks the MenuItems from the bottom to top to set lastItemRef on the last MenuItem that is not disabled + useEffect(() => { + if (removeJWTItemRef.current) { + lastItemRef.current = removeJWTItemRef.current; + } else if (removeAccountItemRef.current) { + lastItemRef.current = removeAccountItemRef.current; + } else { + lastItemRef.current = accountDetailsItemRef.current; + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + removeJWTItemRef.current, + removeAccountItemRef.current, + accountDetailsItemRef.current, + ]); + + const handleKeyDown = useCallback( + (event) => { + if (event.key === 'Tab' && event.target === lastItemRef.current) { + // If Tab is pressed at the last item to close popover and focus to next element in DOM + onClose(); + } + }, + [onClose], + ); + + // Handle click outside of the popover to close it + const popoverDialogRef = useRef(null); + + const handleClickOutside = useCallback( + (event) => { + if ( + popoverDialogRef?.current && + !popoverDialogRef.current.contains(event.target) + ) { + onClose(); + } + }, + [onClose], + ); + + useEffect(() => { + document.addEventListener('mousedown', handleClickOutside); + + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [handleClickOutside]); + + return ( + + +
+ {onEditClick ? ( + { + e.stopPropagation(); + + // Pass network info? + onEditClick(); + }} + data-testid="network-list-item-options-edit" + > + {t('edit')} + + ) : null} + {onDeleteClick ? ( + { + e.stopPropagation(); + + // Pass network info? + onDeleteClick(); + }} + data-testid="network-list-item-options-delete" + > + {t('delete')} + + ) : null} +
+
+
+ ); +}; + +NetworkListItemMenu.propTypes = { + /** + * Element that the menu should display next to + */ + anchorElement: PropTypes.instanceOf(window.Element), + /** + * Function that executes when the menu is closed + */ + onClose: PropTypes.func.isRequired, + /** + * Function that executes when the Edit menu item is clicked + */ + onEditClick: PropTypes.func, + /** + * Function that executes when the Delete menu item is closed + */ + onDeleteClick: PropTypes.func, + /** + * Represents if the menu is open or not + * + * @type {boolean} + */ + isOpen: PropTypes.bool.isRequired, +}; diff --git a/ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap b/ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap index df225a439aed..4d3f902a14de 100644 --- a/ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap +++ b/ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap @@ -26,12 +26,13 @@ exports[`NetworkListItem renders properly 1`] = `

diff --git a/ui/components/multichain/network-list-item/network-list-item.js b/ui/components/multichain/network-list-item/network-list-item.js index 0783d1e50d84..74cd19314729 100644 --- a/ui/components/multichain/network-list-item/network-list-item.js +++ b/ui/components/multichain/network-list-item/network-list-item.js @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; import { @@ -8,7 +8,6 @@ import { BorderRadius, Color, Display, - IconColor, JustifyContent, TextColor, } from '../../../helpers/constants/design-system'; @@ -18,15 +17,12 @@ import { ButtonIcon, ButtonIconSize, IconName, - Popover, - PopoverPosition, - PopoverRole, Text, } from '../../component-library'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { getAvatarNetworkColor } from '../../../helpers/utils/accounts'; import Tooltip from '../../ui/tooltip/tooltip'; -import { MenuItem } from '../../ui/menu'; +import { NetworkListItemMenu } from '../network-list-item-menu'; const MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP = 20; @@ -41,32 +37,14 @@ export const NetworkListItem = ({ }) => { const t = useI18nContext(); const networkRef = useRef(); - const menuRef = useRef(null); - const [networkOptionsMenuOpen, setNetworkOptionsMenuOpen] = useState(false); - - // Handle click outside of the popover to close it - const popoverDialogRef = useRef(null); - - const handleClickOutside = useCallback( - (event) => { - if ( - popoverDialogRef?.current && - !popoverDialogRef.current.contains(event.target) - ) { - setNetworkOptionsMenuOpen(false); - } - }, - [setNetworkOptionsMenuOpen], - ); - - useEffect(() => { - document.addEventListener('mousedown', handleClickOutside); + const [networkListItemMenuElement, setNetworkListItemMenuElement] = + useState(); + const setNetworkListItemMenuRef = (ref) => { + setNetworkListItemMenuElement(ref); + }; - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [handleClickOutside]); + const [networkOptionsMenuOpen, setNetworkOptionsMenuOpen] = useState(false); useEffect(() => { if (networkRef.current && focus) { @@ -137,57 +115,23 @@ export const NetworkListItem = ({ {onDeleteClick || onEditClick ? ( { - e.preventDefault(); e.stopPropagation(); - - console.log(`Opening menu for ${name}`); setNetworkOptionsMenuOpen(true); }} size={ButtonIconSize.Sm} /> ) : null} - setNetworkOptionsMenuOpen(false)} - role={PopoverRole.Dialog} - position={PopoverPosition.Bottom} - offset={[0, 0]} - > - {onEditClick ? ( - { - e.stopPropagation(); - - // Pass network info? - onEditClick(); - }} - data-testid="network-list-item-options-edit" - > - {t('edit')} - - ) : null} - {onDeleteClick ? ( - { - e.stopPropagation(); - - // Pass network info? - onDeleteClick(); - }} - data-testid="network-list-item-options-delete" - color={TextColor.errorDefault} - > - {t('delete')} - - ) : null} - + /> ); }; diff --git a/ui/components/multichain/network-list-item/network-list-item.test.js b/ui/components/multichain/network-list-item/network-list-item.test.js index 4c4215d02b10..994e77bc18b7 100644 --- a/ui/components/multichain/network-list-item/network-list-item.test.js +++ b/ui/components/multichain/network-list-item/network-list-item.test.js @@ -65,16 +65,18 @@ describe('NetworkListItem', () => { it('executes onDeleteClick when the delete button is clicked', () => { const onDeleteClick = jest.fn(); const onClick = jest.fn(); - const { container } = render( + + const { getByTestId } = render( , ); - fireEvent.click( - container.querySelector('.multichain-network-list-item__delete'), - ); + + fireEvent.click(getByTestId('network-list-item-options-button')); + + fireEvent.click(getByTestId('network-list-item-options-delete')); expect(onDeleteClick).toHaveBeenCalledTimes(1); expect(onClick).toHaveBeenCalledTimes(0); }); diff --git a/ui/components/multichain/network-list-menu/network-list-menu.js b/ui/components/multichain/network-list-menu/network-list-menu.js index 52c0d729a23a..5b3f2dadbc9f 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.js @@ -84,6 +84,8 @@ export const NetworkListMenu = ({ onClose }) => { const t = useI18nContext(); const [actionMode, setActionMode] = useState(ACTION_MODES.LIST); + const [modalTitle, setModalTitle] = useState(t('networkMenuHeading')); + const [networkToEdit, setNetworkToEdit] = useState(null); const nonTestNetworks = useSelector(getNonTestNetworks); const testNetworks = useSelector(getTestNetworks); const showTestNetworks = useSelector(getShowTestNetworks); @@ -110,13 +112,6 @@ export const NetworkListMenu = ({ onClose }) => { const isUnlocked = useSelector(getIsUnlocked); - let title = t('networkMenuHeading'); - if (actionMode === ACTION_MODES.ADD) { - title = t('addCustomNetwork'); - } else if (actionMode === ACTION_MODES.EDIT) { - title = currentNetwork.nickname; - } - const orderedNetworksList = useSelector(getOrderedNetworksList); const networkConfigurationChainIds = Object.values(networkConfigurations).map( @@ -244,9 +239,15 @@ export const NetworkListMenu = ({ onClose }) => { }; }; - const getOnEditCallback = (networkId) => { + const getOnEditCallback = (network) => { return () => { - console.log('Get onEditCallback for: ', networkId); + const networkToUse = { + ...network, + label: network.nickname, + }; + setModalTitle(network.nickname); + setNetworkToEdit(networkToUse); + setActionMode(ACTION_MODES.EDIT); }; }; @@ -285,17 +286,9 @@ export const NetworkListMenu = ({ onClose }) => { }); }} onDeleteClick={ - canDeleteNetwork - ? () => { - dispatch(toggleNetworkMenu()); - getOnDeleteCallback(network.id); - } - : null + canDeleteNetwork ? getOnDeleteCallback(network.id) : null } - onEditClick={() => { - dispatch(toggleNetworkMenu()); - getOnEditCallback(network.id); - }} + onEditClick={getOnEditCallback(network)} /> ); }); @@ -312,6 +305,211 @@ export const NetworkListMenu = ({ onClose }) => { } }; + const renderListNetworks = () => { + if (actionMode === ACTION_MODES.LIST) { + return ( + <> + + + + {showBanner ? ( + + drag-and-drop + + } + onClose={() => hideNetworkBanner()} + description={t('dragAndDropBanner')} + /> + ) : null} + + {searchResults.length === 0 && isSearching ? ( + + {t('noNetworksFound')} + + ) : ( + + + {(provided) => ( + + {searchResults.map((network, index) => { + const isCurrentNetwork = + currentNetwork.id === network.id; + + const canDeleteNetwork = + isUnlocked && + !isCurrentNetwork && + network.removable; + + return ( + + {(providedDrag) => ( + + { + dispatch(toggleNetworkMenu()); + if (network.providerType) { + dispatch( + setProviderType(network.providerType), + ); + } else { + dispatch(setActiveNetwork(network.id)); + } + + // If presently on a dapp, communicate a change to + // the dapp via silent switchEthereumChain that the + // network has changed due to user action + if ( + useRequestQueue && + selectedTabOrigin + ) { + setNetworkClientIdForDomain( + selectedTabOrigin, + network.id, + ); + } + + trackEvent({ + event: + MetaMetricsEventName.NavNetworkSwitched, + category: + MetaMetricsEventCategory.Network, + properties: { + location: 'Network Menu', + chain_id: currentChainId, + from_network: currentChainId, + to_network: network.chainId, + }, + }); + }} + onDeleteClick={ + canDeleteNetwork + ? getOnDeleteCallback(network.id) + : null + } + onEditClick={getOnEditCallback(network)} + /> + + )} + + ); + })} + {provided.placeholder} + + )} + + + )} + {networkMenuRedesign ? ( + + ) : null} + + {t('showTestnetNetworks')} + + + {showTestNetworks || currentlyOnTestNetwork ? ( + + {generateMenuItems(testNetworks)} + + ) : null} + + + + + { + if (!networkMenuRedesign) { + if (isFullScreen) { + if (completedOnboarding) { + history.push(ADD_POPULAR_CUSTOM_NETWORK); + } else { + dispatch(showModal({ name: 'ONBOARDING_ADD_NETWORK' })); + } + } else { + global.platform.openExtensionInBrowser( + ADD_POPULAR_CUSTOM_NETWORK, + ); + } + dispatch(toggleNetworkMenu()); + return; + } + trackEvent({ + event: MetaMetricsEventName.AddNetworkButtonClick, + category: MetaMetricsEventCategory.Network, + }); + setActionMode(ACTION_MODES.ADD); + setModalTitle(t('addCustomNetwork')); + }} + > + {t('addNetwork')} + + + + ); + } else if (actionMode === ACTION_MODES.ADD) { + return ; + } + return ( + + ); + }; + const headerAdditionalProps = actionMode === ACTION_MODES.LIST ? {} @@ -336,210 +534,9 @@ export const NetworkListMenu = ({ onClose }) => { onClose={onClose} {...headerAdditionalProps} > - {title} + {modalTitle} - {actionMode === ACTION_MODES.LIST ? ( - <> - - - - {showBanner ? ( - - drag-and-drop - - } - onClose={() => hideNetworkBanner()} - description={t('dragAndDropBanner')} - /> - ) : null} - - {searchResults.length === 0 && isSearching ? ( - - {t('noNetworksFound')} - - ) : ( - - - {(provided) => ( - - {searchResults.map((network, index) => { - const isCurrentNetwork = - currentNetwork.id === network.id; - - const canDeleteNetwork = - isUnlocked && - !isCurrentNetwork && - network.removable; - - return ( - - {(providedDrag) => ( - - { - dispatch(toggleNetworkMenu()); - if (network.providerType) { - dispatch( - setProviderType( - network.providerType, - ), - ); - } else { - dispatch( - setActiveNetwork(network.id), - ); - } - - // If presently on a dapp, communicate a change to - // the dapp via silent switchEthereumChain that the - // network has changed due to user action - if ( - useRequestQueue && - selectedTabOrigin - ) { - setNetworkClientIdForDomain( - selectedTabOrigin, - network.id, - ); - } - - trackEvent({ - event: - MetaMetricsEventName.NavNetworkSwitched, - category: - MetaMetricsEventCategory.Network, - properties: { - location: 'Network Menu', - chain_id: currentChainId, - from_network: currentChainId, - to_network: network.chainId, - }, - }); - }} - onDeleteClick={ - canDeleteNetwork - ? () => { - dispatch(toggleNetworkMenu()); - getOnDeleteCallback(network.id); - } - : null - } - onEditClick={() => { - dispatch(toggleNetworkMenu()); - getOnEditCallback(network.id); - }} - /> - - )} - - ); - })} - {provided.placeholder} - - )} - - - )} - {networkMenuRedesign ? ( - - ) : null} - - {t('showTestnetNetworks')} - - - {showTestNetworks || currentlyOnTestNetwork ? ( - - {generateMenuItems(testNetworks)} - - ) : null} - - - - - { - if (!networkMenuRedesign) { - if (isFullScreen) { - if (completedOnboarding) { - history.push(ADD_POPULAR_CUSTOM_NETWORK); - } else { - dispatch(showModal({ name: 'ONBOARDING_ADD_NETWORK' })); - } - } else { - global.platform.openExtensionInBrowser( - ADD_POPULAR_CUSTOM_NETWORK, - ); - } - dispatch(toggleNetworkMenu()); - return; - } - trackEvent({ - event: MetaMetricsEventName.AddNetworkButtonClick, - category: MetaMetricsEventCategory.Network, - }); - setActionMode(ACTION_MODES.ADD); - }} - > - {t('addNetwork')} - - - - ) : ( - - )} + {renderListNetworks()} ); diff --git a/ui/helpers/utils/network-helper.test.ts b/ui/helpers/utils/network-helper.test.ts index fef6711dbd93..188cd235953a 100644 --- a/ui/helpers/utils/network-helper.test.ts +++ b/ui/helpers/utils/network-helper.test.ts @@ -1,4 +1,8 @@ -import { getMatchedChain, getMatchedSymbols } from './network-helper'; +import { + getMatchedChain, + getMatchedNames, + getMatchedSymbols, +} from './network-helper'; describe('netwotkHelper', () => { describe('getMatchedChain', () => { @@ -76,4 +80,53 @@ describe('netwotkHelper', () => { expect(result).toEqual([]); }); }); + + describe('getMatchedName', () => { + it('should return an array of symbols that match the given decimalChainId', () => { + const chains = [ + { + chainId: '1', + name: 'Ethereum', + nativeCurrency: { symbol: 'ETH', name: 'Ethereum' }, + }, + { + chainId: '3', + name: 'tEthereum', + nativeCurrency: { symbol: 'tETH', name: 'tEthereum' }, + }, + { + chainId: '1', + name: 'WEthereum', + nativeCurrency: { symbol: 'WETH', name: 'WEthereum' }, + }, + ]; + const decimalChainId = '1'; + const expected = ['Ethereum', 'WEthereum']; + + const result = getMatchedNames(decimalChainId, chains); + + expect(result).toEqual(expect.arrayContaining(expected)); + expect(result.length).toBe(expected.length); + }); + + it('should return an empty array if no symbols match the given decimalChainId', () => { + const chains = [ + { + chainId: '1', + name: 'Ethereum', + nativeCurrency: { symbol: 'ETH', name: 'Ethereum' }, + }, + { + chainId: '3', + name: 'tEthereum', + nativeCurrency: { symbol: 'tETH', name: 'tEthereum' }, + }, + ]; + const decimalChainId = '2'; // No matching chainId + + const result = getMatchedNames(decimalChainId, chains); + + expect(result).toEqual([]); + }); + }); }); diff --git a/ui/helpers/utils/network-helper.ts b/ui/helpers/utils/network-helper.ts index 840dc8dfbf8a..960f2979cc0e 100644 --- a/ui/helpers/utils/network-helper.ts +++ b/ui/helpers/utils/network-helper.ts @@ -26,3 +26,19 @@ export const getMatchedSymbols = ( return accumulator; }, []); }; + +export const getMatchedNames = ( + decimalChainId: string, + safeChainsList: { + chainId: string; + name: string; + nativeCurrency: { symbol: string; name: string }; + }[], +): string[] => { + return safeChainsList.reduce((accumulator, currentNetwork) => { + if (currentNetwork.chainId.toString() === decimalChainId) { + accumulator.push(currentNetwork?.name); + } + return accumulator; + }, []); +}; diff --git a/ui/pages/onboarding-flow/add-network-modal/index.js b/ui/pages/onboarding-flow/add-network-modal/index.js index 9ee4f9d80c51..c031739b68b6 100644 --- a/ui/pages/onboarding-flow/add-network-modal/index.js +++ b/ui/pages/onboarding-flow/add-network-modal/index.js @@ -12,16 +12,24 @@ import { TypographyVariant, FONT_WEIGHT, } from '../../../helpers/constants/design-system'; - import NetworksForm from '../../settings/networks-tab/networks-form/networks-form'; -export default function AddNetworkModal({ showHeader = true }) { +export default function AddNetworkModal({ + showHeader = false, + isNewNetworkFlow = false, + addNewNetwork = true, + networkToEdit = null, +}) { const dispatch = useDispatch(); const t = useI18nContext(); const closeCallback = () => dispatch(hideModal({ name: 'ONBOARDING_ADD_NETWORK' })); + const additionalProps = networkToEdit + ? { selectedNetwork: networkToEdit } + : {}; + return ( <> {showHeader ? ( @@ -36,12 +44,14 @@ export default function AddNetworkModal({ showHeader = true }) { ) : null} ); @@ -49,8 +59,14 @@ export default function AddNetworkModal({ showHeader = true }) { AddNetworkModal.propTypes = { showHeader: PropTypes.bool, + isNewNetworkFlow: PropTypes.bool, + addNewNetwork: PropTypes.bool, + networkToEdit: PropTypes.object, }; AddNetworkModal.defaultProps = { - showHeader: true, + showHeader: false, + isNewNetworkFlow: false, + addNewNetwork: true, + networkToEdit: null, }; diff --git a/ui/pages/settings/networks-tab/networks-form/networks-form.js b/ui/pages/settings/networks-tab/networks-form/networks-form.js index e66481453d32..54018aa0fe1e 100644 --- a/ui/pages/settings/networks-tab/networks-form/networks-form.js +++ b/ui/pages/settings/networks-tab/networks-form/networks-form.js @@ -10,6 +10,8 @@ import React, { useState, } from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { ORIGIN_METAMASK } from '@metamask/approval-controller'; +import { ApprovalType } from '@metamask/controller-utils'; import { isWebUrl } from '../../../../../app/scripts/lib/util'; import { MetaMetricsEventCategory, @@ -18,6 +20,7 @@ import { } from '../../../../../shared/constants/metametrics'; import { BUILT_IN_NETWORKS, + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP, CHAIN_IDS, CHAINLIST_CURRENCY_SYMBOLS_MAP_NETWORK_COLLISION, FEATURED_RPCS, @@ -40,6 +43,7 @@ import { usePrevious } from '../../../../hooks/usePrevious'; import { useSafeChainsListValidationSelector } from '../../../../selectors'; import { editAndSetNetworkConfiguration, + requestUserApproval, setNewNetworkAdded, setSelectedNetworkConfigurationId, showDeprecatedNetworkModal, @@ -617,6 +621,30 @@ const NetworksForm = ({ const onSubmit = async () => { setIsSubmitting(true); + if (networkMenuRedesign && addNewNetwork) { + dispatch(toggleNetworkMenu()); + await dispatch( + requestUserApproval({ + origin: ORIGIN_METAMASK, + type: ApprovalType.AddEthereumChain, + requestData: { + chainId: prefixChainId(chainId), + rpcUrl, + ticker, + imageUrl: + CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP[prefixChainId(chainId)] ?? '', + chainName: networkName, + rpcPrefs: { + ...rpcPrefs, + blockExplorerUrl: blockExplorerUrl || rpcPrefs?.blockExplorerUrl, + }, + referrer: ORIGIN_METAMASK, + source: MetaMetricsNetworkEventSource.NewAddNetworkFlow, + }, + }), + ); + return; + } try { const formChainId = chainId.trim().toLowerCase(); const prefixedChainId = prefixChainId(formChainId); @@ -890,7 +918,9 @@ const NetworksForm = ({ disabled={isSubmitDisabled} onClick={() => { onSubmit(); - dispatch(toggleNetworkMenu()); + if (!networkMenuRedesign || !addNewNetwork) { + dispatch(toggleNetworkMenu()); + } }} size={ButtonPrimarySize.Lg} width={BlockSize.Full} From 13afd6e7f8b1bbe2b5fb59e1ccf8950c37921898 Mon Sep 17 00:00:00 2001 From: salimtb Date: Thu, 13 Jun 2024 15:09:30 +0200 Subject: [PATCH 04/11] feat: add toast after edit --- app/_locales/en/messages.json | 3 +++ ui/ducks/app/app.ts | 9 +++++++ ui/pages/home/home.component.js | 26 +++++++++++++++++++ ui/pages/home/home.container.js | 6 +++++ .../networks-form/networks-form.js | 4 +++ ui/selectors/selectors.js | 4 +++ ui/selectors/selectors.test.js | 14 ++++++++++ ui/store/actionConstants.ts | 1 + ui/store/actions.test.js | 19 ++++++++++++++ ui/store/actions.ts | 12 +++++++++ 10 files changed, 98 insertions(+) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 78f2f87ecb29..989f66071c41 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -2956,6 +2956,9 @@ "newNetworkAdded": { "message": "“$1” was successfully added!" }, + "newNetworkEdited": { + "message": "“$1” was successfully edited!" + }, "newNftAddedMessage": { "message": "NFT was successfully added!" }, diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index 5c63ac25dcb7..db1e67f662c9 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -82,6 +82,7 @@ type AppState = { newNftAddedMessage: string; removeNftMessage: string; newNetworkAddedName: string; + editedNetwork: string; newNetworkAddedConfigurationId: string; selectedNetworkConfigurationId: string; sendInputCurrencySwitched: boolean; @@ -164,6 +165,7 @@ const initialState: AppState = { newNftAddedMessage: '', removeNftMessage: '', newNetworkAddedName: '', + editedNetwork: '', newNetworkAddedConfigurationId: '', selectedNetworkConfigurationId: '', sendInputCurrencySwitched: false, @@ -490,6 +492,13 @@ export default function reduceApp( newNetworkAddedConfigurationId: networkConfigurationId, }; } + case actionConstants.SET_EDIT_NETWORK: { + const { nickname } = action.payload; + return { + ...appState, + editedNetwork: nickname, + }; + } case actionConstants.SET_NEW_TOKENS_IMPORTED: return { ...appState, diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index aba0f4f57d59..00dc74464006 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -181,6 +181,7 @@ export default class Home extends PureComponent { showOutdatedBrowserWarning: PropTypes.bool.isRequired, setOutdatedBrowserWarningLastShown: PropTypes.func.isRequired, newNetworkAddedName: PropTypes.string, + editedNetwork: PropTypes.string, // This prop is used in the `shouldCloseNotificationPopup` function // eslint-disable-next-line react/no-unused-prop-types isSigningQRHardwareTransaction: PropTypes.bool.isRequired, @@ -194,6 +195,7 @@ export default class Home extends PureComponent { setNewTokensImported: PropTypes.func.isRequired, setNewTokensImportedError: PropTypes.func.isRequired, clearNewNetworkAdded: PropTypes.func, + clearEditedNetwork: PropTypes.func, setActiveNetwork: PropTypes.func, // eslint-disable-next-line react/no-unused-prop-types setTokenAutodetectModal: PropTypes.func, @@ -469,6 +471,7 @@ export default class Home extends PureComponent { newNftAddedMessage, setNewNftAddedMessage, newNetworkAddedName, + editedNetwork, removeNftMessage, setRemoveNftMessage, newTokensImported, @@ -477,6 +480,7 @@ export default class Home extends PureComponent { setNewTokensImportedError, newNetworkAddedConfigurationId, clearNewNetworkAdded, + clearEditedNetwork, setActiveNetwork, } = this.props; @@ -591,6 +595,28 @@ export default class Home extends PureComponent { } /> ) : null} + {console.log('IM HERE ***********', editedNetwork)} + {editedNetwork ? ( + + + + {t('newNetworkEdited', [editedNetwork])} + + clearEditedNetwork()} + className="home__new-network-notification-close" + /> + + } + /> + ) : null} {newTokensImported ? ( { getIsBrowserDeprecated() && getShowOutdatedBrowserWarning(state), seedPhraseBackedUp, newNetworkAddedName: getNewNetworkAdded(state), + editedNetwork: getEditedNetwork(state), isSigningQRHardwareTransaction: getIsSigningQRHardwareTransaction(state), newNftAddedMessage: getNewNftAddedMessage(state), removeNftMessage: getRemoveNftMessage(state), @@ -266,6 +269,9 @@ const mapDispatchToProps = (dispatch) => { clearNewNetworkAdded: () => { dispatch(setNewNetworkAdded({})); }, + clearEditedNetwork: () => { + dispatch(setEditedNetwork({})); + }, setActiveNetwork: (networkConfigurationId) => { dispatch(setActiveNetwork(networkConfigurationId)); }, diff --git a/ui/pages/settings/networks-tab/networks-form/networks-form.js b/ui/pages/settings/networks-tab/networks-form/networks-form.js index 54018aa0fe1e..50ca91c31a18 100644 --- a/ui/pages/settings/networks-tab/networks-form/networks-form.js +++ b/ui/pages/settings/networks-tab/networks-form/networks-form.js @@ -44,6 +44,7 @@ import { useSafeChainsListValidationSelector } from '../../../../selectors'; import { editAndSetNetworkConfiguration, requestUserApproval, + setEditedNetwork, setNewNetworkAdded, setSelectedNetworkConfigurationId, showDeprecatedNetworkModal, @@ -704,6 +705,9 @@ const NetworksForm = ({ token_symbol: ticker, }, }); + if (networkMenuRedesign) { + dispatch(setEditedNetwork({ nickname: networkName })); + } } if ( diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index 55c9576d1bce..d484ce026ab0 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2036,6 +2036,10 @@ export function getNewNetworkAdded(state) { return state.appState.newNetworkAddedName; } +export function getEditedNetwork(state) { + return state.appState.editedNetwork; +} + export function getNetworksTabSelectedNetworkConfigurationId(state) { return state.appState.selectedNetworkConfigurationId; } diff --git a/ui/selectors/selectors.test.js b/ui/selectors/selectors.test.js index f40a8497a86b..1287aff781b6 100644 --- a/ui/selectors/selectors.test.js +++ b/ui/selectors/selectors.test.js @@ -510,6 +510,20 @@ describe('Selectors', () => { }); }); + describe('#getEditedNetwork', () => { + it('returns undefined if getEditedNetwork is undefined', () => { + expect(selectors.getNewNetworkAdded({ appState: {} })).toBeUndefined(); + }); + + it('returns getEditedNetwork', () => { + expect( + selectors.getEditedNetwork({ + appState: { editedNetwork: 'test-chain' }, + }), + ).toStrictEqual('test-chain'); + }); + }); + describe('#getRpcPrefsForCurrentProvider', () => { it('returns an empty object if state.metamask.providerConfig is empty', () => { expect( diff --git a/ui/store/actionConstants.ts b/ui/store/actionConstants.ts index 658e5b30c296..251196f70a73 100644 --- a/ui/store/actionConstants.ts +++ b/ui/store/actionConstants.ts @@ -118,6 +118,7 @@ export const SET_SHOW_TOKEN_AUTO_DETECT_MODAL_UPGRADE = export const SET_SELECTED_NETWORK_CONFIGURATION_ID = 'SET_SELECTED_NETWORK_CONFIGURATION_ID'; export const SET_NEW_NETWORK_ADDED = 'SET_NEW_NETWORK_ADDED'; +export const SET_EDIT_NETWORK = 'SET_EDIT_NETWORK'; export const SET_NEW_NFT_ADDED_MESSAGE = 'SET_NEW_NFT_ADDED_MESSAGE'; export const SET_REMOVE_NFT_MESSAGE = 'SET_REMOVE_NFT_MESSAGE'; diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index d153d1888018..813153d33ca3 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -1389,6 +1389,25 @@ describe('Actions', () => { }); }); + describe('#setEditedNetwork', () => { + it('sets appState.setEditedNetwork to provided value', async () => { + const store = mockStore(); + + const newNetworkAddedDetails = { + nickname: 'test-chain', + }; + + store.dispatch(actions.setEditedNetwork(newNetworkAddedDetails)); + + const resultantActions = store.getActions(); + + expect(resultantActions[0]).toStrictEqual({ + type: 'SET_EDIT_NETWORK', + payload: newNetworkAddedDetails, + }); + }); + }); + describe('#addToAddressBook', () => { it('calls setAddressBook', async () => { const store = mockStore(); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index c57776fd45ea..cc0dd1507bc3 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4149,6 +4149,18 @@ export function setNewNetworkAdded({ }; } +export function setEditedNetwork({ + nickname, +}: { + networkConfigurationId: string; + nickname: string; +}): PayloadAction { + return { + type: actionConstants.SET_EDIT_NETWORK, + payload: { nickname }, + }; +} + export function setNewNftAddedMessage( newNftAddedMessage: string, ): PayloadAction { From ab714b544c39810e7351e7282b4e5a6e51dd484b Mon Sep 17 00:00:00 2001 From: salimtb Date: Thu, 13 Jun 2024 15:12:49 +0200 Subject: [PATCH 05/11] feat: add feature flag for menu --- .../network-list-item/network-list-item.js | 53 ++++++++++++++----- ui/pages/home/home.component.js | 5 +- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/ui/components/multichain/network-list-item/network-list-item.js b/ui/components/multichain/network-list-item/network-list-item.js index 74cd19314729..4e76258584bc 100644 --- a/ui/components/multichain/network-list-item/network-list-item.js +++ b/ui/components/multichain/network-list-item/network-list-item.js @@ -1,6 +1,7 @@ import React, { useEffect, useRef, useState } from 'react'; import classnames from 'classnames'; import PropTypes from 'prop-types'; +import { useSelector } from 'react-redux'; import { AlignItems, BackgroundColor, @@ -10,6 +11,8 @@ import { Display, JustifyContent, TextColor, + Size, + IconColor, } from '../../../helpers/constants/design-system'; import { AvatarNetwork, @@ -23,6 +26,7 @@ import { useI18nContext } from '../../../hooks/useI18nContext'; import { getAvatarNetworkColor } from '../../../helpers/utils/accounts'; import Tooltip from '../../ui/tooltip/tooltip'; import { NetworkListItemMenu } from '../network-list-item-menu'; +import { getLocalNetworkMenuRedesignFeatureFlag } from '../../../helpers/utils/feature-flags'; const MAXIMUM_CHARACTERS_WITHOUT_TOOLTIP = 20; @@ -43,9 +47,42 @@ export const NetworkListItem = ({ const setNetworkListItemMenuRef = (ref) => { setNetworkListItemMenuElement(ref); }; - const [networkOptionsMenuOpen, setNetworkOptionsMenuOpen] = useState(false); + const networkMenuRedesign = useSelector( + getLocalNetworkMenuRedesignFeatureFlag, + ); + + const renderButton = () => { + if (networkMenuRedesign) { + return onDeleteClick || onEditClick ? ( + { + e.stopPropagation(); + setNetworkOptionsMenuOpen(true); + }} + size={ButtonIconSize.Sm} + /> + ) : null; + } + return onDeleteClick ? ( + { + e.stopPropagation(); + onDeleteClick(); + }} + /> + ) : null; + }; useEffect(() => { if (networkRef.current && focus) { networkRef.current.focus(); @@ -112,19 +149,7 @@ export const NetworkListItem = ({ )} - {onDeleteClick || onEditClick ? ( - { - e.stopPropagation(); - setNetworkOptionsMenuOpen(true); - }} - size={ButtonIconSize.Sm} - /> - ) : null} + {renderButton()} ) : null} - {console.log('IM HERE ***********', editedNetwork)} {editedNetwork ? ( From d22da9ff6af3e40566cb3a6deb48f8070763e24b Mon Sep 17 00:00:00 2001 From: salimtb Date: Thu, 20 Jun 2024 11:16:27 +0200 Subject: [PATCH 06/11] fix: fix suggested name form --- .../networks-form/networks-form.js | 118 +++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/ui/pages/settings/networks-tab/networks-form/networks-form.js b/ui/pages/settings/networks-tab/networks-form/networks-form.js index 50ca91c31a18..00d967c43cc0 100644 --- a/ui/pages/settings/networks-tab/networks-form/networks-form.js +++ b/ui/pages/settings/networks-tab/networks-form/networks-form.js @@ -73,6 +73,7 @@ import { } from '../../../../helpers/constants/design-system'; import { getMatchedChain, + getMatchedNames, getMatchedSymbols, } from '../../../../helpers/utils/network-helper'; import { getLocalNetworkMenuRedesignFeatureFlag } from '../../../../helpers/utils/feature-flags'; @@ -123,6 +124,7 @@ const NetworksForm = ({ const t = useI18nContext(); const dispatch = useDispatch(); const DEFAULT_SUGGESTED_TICKER = []; + const DEFAULT_SUGGESTED_NAME = []; const { label, labelKey, viewOnly, rpcPrefs } = selectedNetwork; const selectedNetworkName = label || (labelKey && t(getNetworkLabelKey(labelKey))); @@ -144,6 +146,7 @@ const NetworksForm = ({ ); const [isEditing, setIsEditing] = useState(Boolean(addNewNetwork)); const [previousNetwork, setPreviousNetwork] = useState(selectedNetwork); + const [suggestedNames, setSuggestedNames] = useState(DEFAULT_SUGGESTED_NAME); const trackEvent = useContext(MetaMetricsContext); @@ -205,6 +208,7 @@ const NetworksForm = ({ setErrors({}); setWarnings({}); setSuggestedTicker([]); + setSuggestedNames([]); setIsSubmitting(false); setIsEditing(false); setPreviousNetwork(selectedNetwork); @@ -326,6 +330,33 @@ const NetworksForm = ({ setSuggestedTicker([...matchedSymbol]); }, []); + const autoSuggestName = useCallback((formChainId) => { + const decimalChainId = getDisplayChainId(formChainId); + if (decimalChainId.trim() === '' || safeChainsList.current.length === 0) { + setSuggestedNames([]); + return; + } + const matchedChain = safeChainsList.current?.find( + (chain) => chain.chainId.toString() === decimalChainId, + ); + + const matchedNames = safeChainsList.current?.reduce( + (accumulator, currentNetwork) => { + if (currentNetwork.chainId.toString() === decimalChainId) { + accumulator.push(currentNetwork?.name); + } + return accumulator; + }, + [], + ); + + if (matchedChain === undefined) { + setSuggestedNames([]); + return; + } + setSuggestedNames([...matchedNames]); + }, []); + const hasErrors = () => { return Object.keys(errors).some((key) => { const error = errors[key]; @@ -466,6 +497,7 @@ const NetworksForm = ({ }; } autoSuggestTicker(formChainId); + autoSuggestName(formChainId); return null; }, [rpcUrl, networksToRender, t], @@ -528,6 +560,57 @@ const NetworksForm = ({ [t], ); + const validateNetworkName = useCallback( + async (formChainId, formName) => { + let warningKey; + let warningMessage; + const decimalChainId = getDisplayChainId(formChainId); + + if (!decimalChainId || !formName) { + setSuggestedNames([]); + return null; + } + + if (safeChainsList.current.length === 0) { + warningKey = 'failedToFetchTickerSymbolData'; + warningMessage = t('failedToFetchTickerSymbolData'); + } else { + const matchedChain = getMatchedChain( + decimalChainId, + safeChainsList.current, + ); + + const matchedNames = getMatchedNames( + decimalChainId, + safeChainsList.current, + ); + setSuggestedNames([...matchedNames]); + + if (matchedChain === undefined) { + warningKey = 'failedToFetchTickerSymbolData'; + warningMessage = t('failedToFetchTickerSymbolData'); + } else if ( + !matchedNames.some( + (name) => name.toLowerCase() === formName.toLowerCase(), + ) + ) { + warningKey = 'wrongNetworkName'; + warningMessage = t('wrongNetworkName'); + } + } + + if (warningKey) { + return { + key: warningKey, + msg: warningMessage, + }; + } + + return null; + }, + [t], + ); + const validateRPCUrl = useCallback( (url) => { const [ @@ -585,6 +668,7 @@ const NetworksForm = ({ const { error: chainIdError, warning: chainIdWarning } = (await validateChainId(chainId)) || {}; const tickerWarning = await validateTickerSymbol(chainId, ticker); + const nameWarning = await validateNetworkName(chainId, networkName); const blockExplorerError = validateBlockExplorerURL(blockExplorerUrl); const rpcUrlError = validateRPCUrl(rpcUrl); setErrors({ @@ -597,6 +681,7 @@ const NetworksForm = ({ ...warnings, chainId: chainIdWarning, ticker: tickerWarning, + networkName: nameWarning, }); } @@ -609,6 +694,7 @@ const NetworksForm = ({ ticker, blockExplorerUrl, viewOnly, + networkName, label, previousRpcUrl, previousChainId, @@ -618,6 +704,7 @@ const NetworksForm = ({ validateChainId, validateTickerSymbol, validateRPCUrl, + validateNetworkName, ]); const onSubmit = async () => { @@ -806,7 +893,36 @@ const NetworksForm = ({ disabled={viewOnly} dataTestId="network-form-network-name" /> - {window.metamaskFeatureFlags?.networkMenuRedesign ? ( + {suggestedNames && + !suggestedNames.some( + (nameSuggested) => nameSuggested === networkName, + ) ? ( + + {t('suggestedTokenName')} + {suggestedNames.map((suggestedName, i) => ( + { + setNetworkName(suggestedName); + }} + paddingLeft={1} + paddingRight={1} + style={{ verticalAlign: 'baseline' }} + key={i} + > + {suggestedName} + + ))} + + ) : null} + {networkMenuRedesign ? ( ) : ( Date: Thu, 20 Jun 2024 11:40:01 +0200 Subject: [PATCH 07/11] fix: fix toast css --- ui/pages/home/home.component.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index d92f92476073..d43616dc230d 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -85,7 +85,6 @@ import BetaHomeFooter from './beta/beta-home-footer.component'; ///: END:ONLY_INCLUDE_IF ///: BEGIN:ONLY_INCLUDE_IF(build-flask) import FlaskHomeFooter from './flask/flask-home-footer.component'; -import { setEditedNetwork } from '../../store/actions'; ///: END:ONLY_INCLUDE_IF function shouldCloseNotificationPopup({ @@ -490,7 +489,7 @@ export default class Home extends PureComponent { setRemoveNftMessage(''); setNewTokensImported(''); // Added this so we dnt see the notif if user does not close it setNewTokensImportedError(''); - setEditedNetwork({}); + clearEditedNetwork({}); }; const autoHideDelay = 5 * SECOND; @@ -600,7 +599,7 @@ export default class Home extends PureComponent { {editedNetwork ? ( Date: Thu, 20 Jun 2024 11:45:25 +0200 Subject: [PATCH 08/11] fix: fix PR comments --- .../network-list-item-menu.js | 37 ++----------------- 1 file changed, 3 insertions(+), 34 deletions(-) diff --git a/ui/components/multichain/network-list-item-menu/network-list-item-menu.js b/ui/components/multichain/network-list-item-menu/network-list-item-menu.js index 6618cee57610..52a579513ffa 100644 --- a/ui/components/multichain/network-list-item-menu/network-list-item-menu.js +++ b/ui/components/multichain/network-list-item-menu/network-list-item-menu.js @@ -29,9 +29,7 @@ export const NetworkListItemMenu = ({ // Checks the MenuItems from the bottom to top to set lastItemRef on the last MenuItem that is not disabled useEffect(() => { - if (removeJWTItemRef.current) { - lastItemRef.current = removeJWTItemRef.current; - } else if (removeAccountItemRef.current) { + if (removeAccountItemRef.current) { lastItemRef.current = removeAccountItemRef.current; } else { lastItemRef.current = accountDetailsItemRef.current; @@ -43,42 +41,13 @@ export const NetworkListItemMenu = ({ accountDetailsItemRef.current, ]); - const handleKeyDown = useCallback( - (event) => { - if (event.key === 'Tab' && event.target === lastItemRef.current) { - // If Tab is pressed at the last item to close popover and focus to next element in DOM - onClose(); - } - }, - [onClose], - ); - // Handle click outside of the popover to close it const popoverDialogRef = useRef(null); - const handleClickOutside = useCallback( - (event) => { - if ( - popoverDialogRef?.current && - !popoverDialogRef.current.contains(event.target) - ) { - onClose(); - } - }, - [onClose], - ); - - useEffect(() => { - document.addEventListener('mousedown', handleClickOutside); - - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [handleClickOutside]); - return ( -
+
{onEditClick ? ( Date: Thu, 20 Jun 2024 12:01:42 +0200 Subject: [PATCH 09/11] fix: fix suggested name display --- app/_locales/en/messages.json | 3 +++ ui/pages/settings/networks-tab/networks-form/networks-form.js | 2 ++ 2 files changed, 5 insertions(+) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 989f66071c41..6307c797e954 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -5246,6 +5246,9 @@ "message": "Suggested by $1", "description": "$1 is the snap name" }, + "suggestedTokenName": { + "message": "Suggested name:" + }, "suggestedTokenSymbol": { "message": "Suggested ticker symbol:" }, diff --git a/ui/pages/settings/networks-tab/networks-form/networks-form.js b/ui/pages/settings/networks-tab/networks-form/networks-form.js index 00d967c43cc0..28d89a8b39ab 100644 --- a/ui/pages/settings/networks-tab/networks-form/networks-form.js +++ b/ui/pages/settings/networks-tab/networks-form/networks-form.js @@ -894,6 +894,7 @@ const NetworksForm = ({ dataTestId="network-form-network-name" /> {suggestedNames && + suggestedNames.length > 0 && !suggestedNames.some( (nameSuggested) => nameSuggested === networkName, ) ? ( @@ -955,6 +956,7 @@ const NetworksForm = ({ data-testid="network-form-ticker" helpText={ suggestedTicker && + suggestedTicker.length > 0 && !suggestedTicker.some( (symbolSuggested) => symbolSuggested === ticker, ) ? ( From 2ef606ab31558b7aff4ce9ebac41c06050c8ab77 Mon Sep 17 00:00:00 2001 From: salimtb Date: Thu, 20 Jun 2024 13:23:02 +0200 Subject: [PATCH 10/11] fix: fix linter + tests --- .../network-list-item-menu.js | 2 +- .../network-list-item.test.js.snap | 7 ++- .../network-list-item.test.js | 44 +++++++++++++++ .../network-list-menu/network-list-menu.js | 35 +++++++++--- .../add-network-modal.test.js.snap | 54 ++++++------------- 5 files changed, 91 insertions(+), 51 deletions(-) diff --git a/ui/components/multichain/network-list-item-menu/network-list-item-menu.js b/ui/components/multichain/network-list-item-menu/network-list-item-menu.js index 52a579513ffa..a1a0f9ad6632 100644 --- a/ui/components/multichain/network-list-item-menu/network-list-item-menu.js +++ b/ui/components/multichain/network-list-item-menu/network-list-item-menu.js @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import PropTypes from 'prop-types'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { diff --git a/ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap b/ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap index 4d3f902a14de..df225a439aed 100644 --- a/ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap +++ b/ui/components/multichain/network-list-item/__snapshots__/network-list-item.test.js.snap @@ -26,13 +26,12 @@ exports[`NetworkListItem renders properly 1`] = `

diff --git a/ui/components/multichain/network-list-item/network-list-item.test.js b/ui/components/multichain/network-list-item/network-list-item.test.js index 994e77bc18b7..323e432e0e33 100644 --- a/ui/components/multichain/network-list-item/network-list-item.test.js +++ b/ui/components/multichain/network-list-item/network-list-item.test.js @@ -1,10 +1,12 @@ /* eslint-disable jest/require-top-level-describe */ import React from 'react'; import { fireEvent, render } from '@testing-library/react'; +import { useSelector } from 'react-redux'; import { MATIC_TOKEN_IMAGE_URL, POLYGON_DISPLAY_NAME, } from '../../../../shared/constants/network'; +import { getLocalNetworkMenuRedesignFeatureFlag } from '../../../helpers/utils/feature-flags'; import { NetworkListItem } from '.'; const DEFAULT_PROPS = { @@ -15,13 +17,35 @@ const DEFAULT_PROPS = { onDeleteClick: () => undefined, }; +jest.mock('react-redux', () => ({ + ...jest.requireActual('react-redux'), + useSelector: jest.fn(), +})); + +const generateUseSelectorRouter = (opts) => (selector) => { + if (selector === getLocalNetworkMenuRedesignFeatureFlag) { + return opts.networkMenuRedesign ?? false; + } + return undefined; +}; + describe('NetworkListItem', () => { it('renders properly', () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + networkMenuRedesign: false, + }), + ); const { container } = render(); expect(container).toMatchSnapshot(); }); it('does not render the delete icon when no onDeleteClick is clicked', () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + networkMenuRedesign: false, + }), + ); const { container } = render( , ); @@ -31,6 +55,11 @@ describe('NetworkListItem', () => { }); it('shows as selected when selected', () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + networkMenuRedesign: false, + }), + ); const { container } = render( , ); @@ -42,6 +71,11 @@ describe('NetworkListItem', () => { }); it('renders a tooltip when the network name is very long', () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + networkMenuRedesign: false, + }), + ); const { container } = render( { }); it('executes onClick when the item is clicked', () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + networkMenuRedesign: false, + }), + ); const onClick = jest.fn(); const { container } = render( , @@ -63,6 +102,11 @@ describe('NetworkListItem', () => { }); it('executes onDeleteClick when the delete button is clicked', () => { + useSelector.mockImplementation( + generateUseSelectorRouter({ + networkMenuRedesign: true, + }), + ); const onDeleteClick = jest.fn(); const onClick = jest.fn(); diff --git a/ui/components/multichain/network-list-menu/network-list-menu.js b/ui/components/multichain/network-list-menu/network-list-menu.js index 5b3f2dadbc9f..2df12ec8605d 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.js @@ -157,6 +157,7 @@ export const NetworkListMenu = ({ onClose }) => { }, [dispatch, currentlyOnTestNetwork]); const [searchQuery, setSearchQuery] = useState(''); + const [focusSearch, setFocusSearch] = useState(false); const onboardedInThisUISession = useSelector(getOnboardedInThisUISession); const showNetworkBanner = useSelector(getShowNetworkBanner); const showBanner = @@ -190,9 +191,9 @@ export const NetworkListMenu = ({ onClose }) => { ? items : [...notExistingNetworkConfigurations]; - const isSearching = searchQuery !== ''; + let searchTestNetworkResults = [...testNetworks]; - if (isSearching) { + if (focusSearch && searchQuery !== '') { const fuse = new Fuse(searchResults, { threshold: 0.2, location: 0, @@ -212,11 +213,25 @@ export const NetworkListMenu = ({ onClose }) => { keys: ['nickname', 'chainId', 'ticker'], }); + const fuseForTestsNetworks = new Fuse(searchTestNetworkResults, { + threshold: 0.2, + location: 0, + distance: 100, + maxPatternLength: 32, + minMatchCharLength: 1, + shouldSort: true, + keys: ['nickname', 'chainId', 'ticker'], + }); + fuse.setCollection(searchResults); fuseForPopularNetworks.setCollection(searchAddNetworkResults); + fuseForTestsNetworks.setCollection(searchTestNetworkResults); + const fuseResults = fuse.search(searchQuery); const fuseForPopularNetworksResults = fuseForPopularNetworks.search(searchQuery); + const fuseForTestsNetworksResults = + fuseForTestsNetworks.search(searchQuery); searchResults = searchResults.filter((network) => fuseResults.includes(network), @@ -224,6 +239,9 @@ export const NetworkListMenu = ({ onClose }) => { searchAddNetworkResults = searchAddNetworkResults.filter((network) => fuseForPopularNetworksResults.includes(network), ); + searchTestNetworkResults = searchTestNetworkResults.filter((network) => + fuseForTestsNetworksResults.includes(network), + ); } const getOnDeleteCallback = (networkId) => { @@ -265,8 +283,8 @@ export const NetworkListMenu = ({ onClose }) => { name={network.nickname} iconSrc={network?.rpcPrefs?.imageUrl} key={network.id} - selected={isCurrentNetwork} - focus={isCurrentNetwork && !isSearching} + selected={isCurrentNetwork && !focusSearch} + focus={isCurrentNetwork && !focusSearch} onClick={() => { dispatch(toggleNetworkMenu()); if (network.providerType) { @@ -312,6 +330,7 @@ export const NetworkListMenu = ({ onClose }) => { @@ -339,7 +358,7 @@ export const NetworkListMenu = ({ onClose }) => { /> ) : null} - {searchResults.length === 0 && isSearching ? ( + {searchResults.length === 0 && focusSearch ? ( { name={network.nickname} iconSrc={network?.rpcPrefs?.imageUrl} key={network.id} - selected={isCurrentNetwork} - focus={isCurrentNetwork && !isSearching} + selected={isCurrentNetwork && !focusSearch} + focus={isCurrentNetwork && !focusSearch} onClick={() => { dispatch(toggleNetworkMenu()); if (network.providerType) { @@ -458,7 +477,7 @@ export const NetworkListMenu = ({ onClose }) => { {showTestNetworks || currentlyOnTestNetwork ? ( - {generateMenuItems(testNetworks)} + {generateMenuItems(searchTestNetworkResults)} ) : null} diff --git a/ui/pages/onboarding-flow/add-network-modal/__snapshots__/add-network-modal.test.js.snap b/ui/pages/onboarding-flow/add-network-modal/__snapshots__/add-network-modal.test.js.snap index e422b5d8216b..05128f7c75ca 100644 --- a/ui/pages/onboarding-flow/add-network-modal/__snapshots__/add-network-modal.test.js.snap +++ b/ui/pages/onboarding-flow/add-network-modal/__snapshots__/add-network-modal.test.js.snap @@ -67,36 +67,26 @@ exports[`Add Network Modal should render 1`] = ` +

+ Default RPC URL +

-
-
- - Suggested ticker symbol: - -
`; - -exports[`Add Network Modal should render 2`] = `
`; From 667e58a533146ab25b0dd3d9192e8f81104ef94b Mon Sep 17 00:00:00 2001 From: salimtb Date: Thu, 20 Jun 2024 14:33:49 +0200 Subject: [PATCH 11/11] fix: add enabled network section --- app/_locales/en/messages.json | 3 +++ .../network-list-item-menu.js | 27 ++----------------- .../network-list-menu/network-list-menu.js | 9 +++++++ .../networks-form/networks-form.js | 4 ++- 4 files changed, 17 insertions(+), 26 deletions(-) diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 6307c797e954..ed694b49e987 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -6356,6 +6356,9 @@ "whatsThis": { "message": "What's this?" }, + "wrongNetworkName": { + "message": "According to our records, the network name may not correctly match this chain ID." + }, "xOfYPending": { "message": "$1 of $2 pending", "description": "$1 and $2 are intended to be two numbers, where $2 is a total number of pending confirmations, and $1 is a count towards that total" diff --git a/ui/components/multichain/network-list-item-menu/network-list-item-menu.js b/ui/components/multichain/network-list-item-menu/network-list-item-menu.js index a1a0f9ad6632..51a77c851a72 100644 --- a/ui/components/multichain/network-list-item-menu/network-list-item-menu.js +++ b/ui/components/multichain/network-list-item-menu/network-list-item-menu.js @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { @@ -21,29 +21,6 @@ export const NetworkListItemMenu = ({ }) => { const t = useI18nContext(); - // Handle Tab key press for accessibility inside the popover and will close the popover on the last MenuItem - const lastItemRef = useRef(null); - const accountDetailsItemRef = useRef(null); - const removeAccountItemRef = useRef(null); - const removeJWTItemRef = useRef(null); - - // Checks the MenuItems from the bottom to top to set lastItemRef on the last MenuItem that is not disabled - useEffect(() => { - if (removeAccountItemRef.current) { - lastItemRef.current = removeAccountItemRef.current; - } else { - lastItemRef.current = accountDetailsItemRef.current; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - removeJWTItemRef.current, - removeAccountItemRef.current, - accountDetailsItemRef.current, - ]); - - // Handle click outside of the popover to close it - const popoverDialogRef = useRef(null); - return ( -
+
{onEditClick ? ( { /> ) : null} + + {t('enabledNetworks')} + {searchResults.length === 0 && focusSearch ? ( name.toLowerCase() === formName.toLowerCase(), + (name) => name?.toLowerCase() === formName.toLowerCase(), ) ) { warningKey = 'wrongNetworkName'; @@ -671,12 +671,14 @@ const NetworksForm = ({ const nameWarning = await validateNetworkName(chainId, networkName); const blockExplorerError = validateBlockExplorerURL(blockExplorerUrl); const rpcUrlError = validateRPCUrl(rpcUrl); + setErrors({ ...errors, blockExplorerUrl: blockExplorerError, rpcUrl: rpcUrlError, chainId: chainIdError, }); + setWarnings({ ...warnings, chainId: chainIdWarning,