diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 53961ff86b35..81a479338908 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -311,6 +311,9 @@ "message": "Can’t find a token? You can manually add any token by pasting its address. Token contract addresses can be found on $1", "description": "$1 is a blockchain explorer for a specific network, e.g. Etherscan for Ethereum" }, + "addUrl": { + "message": "Add URL" + }, "addingCustomNetwork": { "message": "Adding Network" }, @@ -320,6 +323,9 @@ "additionalNetworks": { "message": "Additional networks" }, + "additionalRpcUrl": { + "message": "Additional RPC URL" + }, "address": { "message": "Address" }, @@ -966,6 +972,9 @@ "confirmConnectionTitle": { "message": "Confirm connection to $1" }, + "confirmDeletion": { + "message": "Confirm deletion" + }, "confirmFieldPaymaster": { "message": "Fee paid by" }, @@ -978,6 +987,9 @@ "confirmRecoveryPhrase": { "message": "Confirm Secret Recovery Phrase" }, + "confirmRpcUrlDeletionMessage": { + "message": "Are you sure you want to delete the RPC URL? Your information will not be saved for this network." + }, "confirmTitleDescContractInteractionTransaction": { "message": "Only confirm this transaction if you fully understand the content and trust the requesting site." }, @@ -1457,6 +1469,9 @@ "message": "Delete $1 network?", "description": "$1 represents the name of the network" }, + "deleteRpcUrl": { + "message": "Delete RPC URL" + }, "deposit": { "message": "Deposit" }, diff --git a/ui/components/app/modals/confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal.tsx b/ui/components/app/modals/confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal.tsx new file mode 100644 index 000000000000..3eb9a9323048 --- /dev/null +++ b/ui/components/app/modals/confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useDispatch } from 'react-redux'; +import { + BlockSize, + Display, +} from '../../../../helpers/constants/design-system'; +import { + Box, + ButtonPrimary, + ButtonPrimarySize, + ButtonSecondary, + ButtonSecondarySize, + Modal, + ModalBody, + ModalContent, + ModalHeader, + ModalOverlay, +} from '../../../component-library'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { + hideModal, + setEditedNetwork, + toggleNetworkMenu, +} from '../../../../store/actions'; + +const ConfirmDeleteRpcUrlModal = () => { + const t = useI18nContext(); + const dispatch = useDispatch(); + return ( + { + dispatch(setEditedNetwork()); + dispatch(hideModal()); + }} + > + + + {t('confirmDeletion')} + + {t('confirmRpcUrlDeletionMessage')} + + { + dispatch(hideModal()); + dispatch(toggleNetworkMenu()); + }} + > + {t('back')} + + { + console.log('TODO: Delete RPc URL'); + }} + > + {t('deleteRpcUrl')} + + + + + + ); +}; + +export default ConfirmDeleteRpcUrlModal; diff --git a/ui/components/app/modals/modal.js b/ui/components/app/modals/modal.js index f3eb1a950a40..9d546e5de74d 100644 --- a/ui/components/app/modals/modal.js +++ b/ui/components/app/modals/modal.js @@ -38,6 +38,7 @@ import TransactionAlreadyConfirmed from './transaction-already-confirmed'; // Metamask Notifications import ConfirmTurnOffProfileSyncing from './confirm-turn-off-profile-syncing'; import TurnOnMetamaskNotifications from './turn-on-metamask-notifications/turn-on-metamask-notifications'; +import ConfirmDeleteRpcUrlModal from './confirm-delete-rpc-url-modal/confirm-delete-rpc-url-modal'; const modalContainerBaseStyle = { transform: 'translate3d(-50%, 0, 0px)', @@ -230,6 +231,19 @@ const MODALS = { }, }, + CONFIRM_DELETE_RPC_URL: { + contents: , + mobileModalStyle: { + ...modalContainerMobileStyle, + }, + laptopModalStyle: { + ...modalContainerLaptopStyle, + }, + contentStyle: { + borderRadius: '8px', + }, + }, + EDIT_APPROVAL_PERMISSION: { contents: , mobileModalStyle: { 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 new file mode 100644 index 000000000000..8e4269928bc8 --- /dev/null +++ b/ui/components/multichain/network-list-menu/add-rpc-url-modal/add-rpc-url-modal.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { + Box, + ButtonPrimary, + ButtonPrimarySize, + FormTextField, +} from '../../../component-library'; +import { + BlockSize, + Display, + TextVariant, +} from '../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; + +const AddRpcUrlModal = () => { + const t = useI18nContext(); + + return ( + + + + + {t('addUrl')} + + + ); +}; + +export default AddRpcUrlModal; 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 c481b163f8a0..3a1f279103d4 100644 --- a/ui/components/multichain/network-list-menu/network-list-menu.js +++ b/ui/components/multichain/network-list-menu/network-list-menu.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect, useState } from 'react'; +import React, { useContext, useEffect, useMemo, useState } from 'react'; import PropTypes from 'prop-types'; import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'; import { useDispatch, useSelector } from 'react-redux'; @@ -15,6 +15,7 @@ import { toggleNetworkMenu, updateNetworksList, setNetworkClientIdForDomain, + setEditedNetwork, } from '../../../store/actions'; import { FEATURED_RPCS, @@ -32,6 +33,7 @@ import { getOriginOfCurrentTab, getUseRequestQueue, getNetworkConfigurations, + getEditedNetwork, } from '../../../selectors'; import ToggleButton from '../../ui/toggle-button'; import { @@ -70,6 +72,7 @@ import { getLocalNetworkMenuRedesignFeatureFlag } from '../../../helpers/utils/f import AddNetworkModal from '../../../pages/onboarding-flow/add-network-modal'; import PopularNetworkList from './popular-network-list/popular-network-list'; import NetworkListSearch from './network-list-search/network-list-search'; +import AddRpcUrlModal from './add-rpc-url-modal/add-rpc-url-modal'; const ACTION_MODES = { // Displays the search box and network list @@ -78,14 +81,13 @@ const ACTION_MODES = { ADD: 'add', // Displays the Edit form EDIT: 'edit', + // Displays the page for adding an additional RPC URL + ADD_RPC: 'add_rpc', }; 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); @@ -114,6 +116,19 @@ export const NetworkListMenu = ({ onClose }) => { const orderedNetworksList = useSelector(getOrderedNetworksList); + const editedNetwork = useSelector(getEditedNetwork); + + const [actionMode, setActionMode] = useState( + editedNetwork ? ACTION_MODES.EDIT : ACTION_MODES.LIST, + ); + + const networkToEdit = useMemo(() => { + const network = [...nonTestNetworks, ...testNetworks].find( + (n) => n.id === editedNetwork?.networkConfigurationId, + ); + return network ? { ...network, label: network.nickname } : undefined; + }, [editedNetwork, nonTestNetworks, testNetworks]); + const networkConfigurationChainIds = Object.values(networkConfigurations).map( (net) => net.chainId, ); @@ -259,12 +274,12 @@ export const NetworkListMenu = ({ onClose }) => { const getOnEditCallback = (network) => { return () => { - const networkToUse = { - ...network, - label: network.nickname, - }; - setModalTitle(network.nickname); - setNetworkToEdit(networkToUse); + dispatch( + setEditedNetwork({ + networkConfigurationId: network.id, + nickname: network.nickname, + }), + ); setActionMode(ACTION_MODES.EDIT); }; }; @@ -518,7 +533,6 @@ export const NetworkListMenu = ({ onClose }) => { category: MetaMetricsEventCategory.Network, }); setActionMode(ACTION_MODES.ADD); - setModalTitle(t('addCustomNetwork')); }} > {t('addNetwork')} @@ -528,20 +542,38 @@ export const NetworkListMenu = ({ onClose }) => { ); } else if (actionMode === ACTION_MODES.ADD) { return ; + } else if (actionMode === ACTION_MODES.EDIT) { + return ( + setActionMode(ACTION_MODES.ADD_RPC)} + /> + ); + } else if (actionMode === ACTION_MODES.ADD_RPC) { + return ; } - return ( - - ); + return null; // Unreachable, but satisfies linter }; - const headerAdditionalProps = - actionMode === ACTION_MODES.LIST - ? {} - : { onBack: () => setActionMode(ACTION_MODES.LIST) }; + // Modal back button + let onBack; + if (actionMode === ACTION_MODES.EDIT || actionMode === ACTION_MODES.ADD) { + onBack = () => setActionMode(ACTION_MODES.LIST); + } else if (actionMode === ACTION_MODES.ADD_RPC) { + onBack = () => setActionMode(ACTION_MODES.EDIT); + } + + // Modal title + let title; + if (actionMode === ACTION_MODES.LIST) { + title = t('networkMenuHeading'); + } else if (actionMode === ACTION_MODES.ADD) { + title = t('addCustomNetwork'); + } else { + title = editedNetwork.nickname; + } return ( @@ -560,9 +592,9 @@ export const NetworkListMenu = ({ onClose }) => { paddingRight={4} paddingBottom={6} onClose={onClose} - {...headerAdditionalProps} + onBack={onBack} > - {modalTitle} + {title} {renderListNetworks()} diff --git a/ui/ducks/app/app.ts b/ui/ducks/app/app.ts index dc5fa3ba64f6..a16508c9a45a 100644 --- a/ui/ducks/app/app.ts +++ b/ui/ducks/app/app.ts @@ -82,7 +82,13 @@ type AppState = { newNftAddedMessage: string; removeNftMessage: string; newNetworkAddedName: string; - editedNetwork: string; + editedNetwork: + | { + networkConfigurationId: string; + nickname: string; + editCompleted: boolean; + } + | undefined; newNetworkAddedConfigurationId: string; selectedNetworkConfigurationId: string; sendInputCurrencySwitched: boolean; @@ -163,7 +169,7 @@ const initialState: AppState = { newNftAddedMessage: '', removeNftMessage: '', newNetworkAddedName: '', - editedNetwork: '', + editedNetwork: undefined, newNetworkAddedConfigurationId: '', selectedNetworkConfigurationId: '', sendInputCurrencySwitched: false, @@ -489,10 +495,9 @@ export default function reduceApp( }; } case actionConstants.SET_EDIT_NETWORK: { - const { nickname } = action.payload; return { ...appState, - editedNetwork: nickname, + editedNetwork: action.payload, }; } case actionConstants.SET_NEW_TOKENS_IMPORTED: diff --git a/ui/pages/home/home.component.js b/ui/pages/home/home.component.js index 3a3dfc7ed753..d2b22a8e9164 100644 --- a/ui/pages/home/home.component.js +++ b/ui/pages/home/home.component.js @@ -80,6 +80,7 @@ import { ///: END:ONLY_INCLUDE_IF } from '../../../shared/lib/ui-utils'; import { AccountOverview } from '../../components/multichain/account-overview'; +import { setEditedNetwork } from '../../store/actions'; ///: BEGIN:ONLY_INCLUDE_IF(build-beta) import BetaHomeFooter from './beta/beta-home-footer.component'; ///: END:ONLY_INCLUDE_IF @@ -185,7 +186,7 @@ export default class Home extends PureComponent { showOutdatedBrowserWarning: PropTypes.bool.isRequired, setOutdatedBrowserWarningLastShown: PropTypes.func.isRequired, newNetworkAddedName: PropTypes.string, - editedNetwork: PropTypes.string, + editedNetwork: PropTypes.object, // This prop is used in the `shouldCloseNotificationPopup` function // eslint-disable-next-line react/no-unused-prop-types isSigningQRHardwareTransaction: PropTypes.bool.isRequired, @@ -496,7 +497,7 @@ export default class Home extends PureComponent { setRemoveNftMessage(''); setNewTokensImported(''); // Added this so we dnt see the notif if user does not close it setNewTokensImportedError(''); - clearEditedNetwork({}); + setEditedNetwork(); }; const autoHideDelay = 5 * SECOND; @@ -603,7 +604,7 @@ export default class Home extends PureComponent { } /> ) : null} - {editedNetwork ? ( + {editedNetwork?.editCompleted ? ( - {t('newNetworkEdited', [editedNetwork])} + {t('newNetworkEdited', [editedNetwork.nickname])} { dispatch(setNewNetworkAdded({})); }, clearEditedNetwork: () => { - dispatch(setEditedNetwork({})); + dispatch(setEditedNetwork()); }, setActiveNetwork: (networkConfigurationId) => { dispatch(setActiveNetwork(networkConfigurationId)); diff --git a/ui/pages/onboarding-flow/add-network-modal/index.js b/ui/pages/onboarding-flow/add-network-modal/index.js index c031739b68b6..b35785e4f2ac 100644 --- a/ui/pages/onboarding-flow/add-network-modal/index.js +++ b/ui/pages/onboarding-flow/add-network-modal/index.js @@ -19,6 +19,7 @@ export default function AddNetworkModal({ isNewNetworkFlow = false, addNewNetwork = true, networkToEdit = null, + onRpcUrlAdd, }) { const dispatch = useDispatch(); const t = useI18nContext(); @@ -50,6 +51,7 @@ export default function AddNetworkModal({ networksToRender={[]} cancelCallback={closeCallback} submitCallback={closeCallback} + onRpcUrlAdd={onRpcUrlAdd} isNewNetworkFlow={isNewNetworkFlow} {...additionalProps} /> @@ -62,6 +64,7 @@ AddNetworkModal.propTypes = { isNewNetworkFlow: PropTypes.bool, addNewNetwork: PropTypes.bool, networkToEdit: PropTypes.object, + onRpcUrlAdd: PropTypes.func, }; AddNetworkModal.defaultProps = { diff --git a/ui/pages/routes/routes.component.js b/ui/pages/routes/routes.component.js index bec55f91a15e..9fe51d046eec 100644 --- a/ui/pages/routes/routes.component.js +++ b/ui/pages/routes/routes.component.js @@ -209,6 +209,7 @@ export default class Routes extends Component { newPrivacyPolicyToastShownDate: PropTypes.number, setSurveyLinkLastClickedOrClosed: PropTypes.func.isRequired, setNewPrivacyPolicyToastShownDate: PropTypes.func.isRequired, + clearEditedNetwork: PropTypes.func.isRequired, setNewPrivacyPolicyToastClickedOrClosed: PropTypes.func.isRequired, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) isShowKeyringSnapRemovalResultModal: PropTypes.bool.isRequired, @@ -804,6 +805,7 @@ export default class Routes extends Component { switchedNetworkDetails, clearSwitchedNetworkDetails, networkMenuRedesign, + clearEditedNetwork, ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) isShowKeyringSnapRemovalResultModal, hideShowKeyringSnapRemovalResultModal, @@ -886,7 +888,12 @@ export default class Routes extends Component { toggleAccountMenu()} /> ) : null} {isNetworkMenuOpen ? ( - toggleNetworkMenu()} /> + { + toggleNetworkMenu(); + clearEditedNetwork(); + }} + /> ) : null} {networkMenuRedesign ? : null} {accountDetailsAddress ? ( diff --git a/ui/pages/routes/routes.container.js b/ui/pages/routes/routes.container.js index d4334e00b4de..da6d62636d5f 100644 --- a/ui/pages/routes/routes.container.js +++ b/ui/pages/routes/routes.container.js @@ -52,6 +52,7 @@ import { ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) hideKeyringRemovalResultModal, ///: END:ONLY_INCLUDE_IF + setEditedNetwork, } from '../../store/actions'; import { pageChanged } from '../../ducks/history/history'; import { prepareToLeaveSwaps } from '../../ducks/swaps/swaps'; @@ -176,6 +177,7 @@ function mapDispatchToProps(dispatch) { dispatch(setNewPrivacyPolicyToastClickedOrClosed()), setNewPrivacyPolicyToastShownDate: (date) => dispatch(setNewPrivacyPolicyToastShownDate(date)), + clearEditedNetwork: () => dispatch(setEditedNetwork()), ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps) hideShowKeyringSnapRemovalResultModal: () => dispatch(hideKeyringRemovalResultModal()), diff --git a/ui/pages/settings/networks-tab/index.scss b/ui/pages/settings/networks-tab/index.scss index 99a4840aead0..86e792d988c4 100644 --- a/ui/pages/settings/networks-tab/index.scss +++ b/ui/pages/settings/networks-tab/index.scss @@ -7,10 +7,12 @@ &__rpc-dropdown { cursor: pointer; + word-break: break-all; } &__rpc-item { position: relative; + word-break: break-all; } &__rpc-item:hover { 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 298a063f98b6..c5bdaeeb1fc5 100644 --- a/ui/pages/settings/networks-tab/networks-form/networks-form.js +++ b/ui/pages/settings/networks-tab/networks-form/networks-form.js @@ -120,6 +120,7 @@ const NetworksForm = ({ selectedNetwork, cancelCallback, submitCallback, + onRpcUrlAdd, }) => { const t = useI18nContext(); const dispatch = useDispatch(); @@ -795,7 +796,13 @@ const NetworksForm = ({ }, }); if (networkMenuRedesign) { - dispatch(setEditedNetwork({ nickname: networkName })); + dispatch( + setEditedNetwork({ + networkConfigurationId, + nickname: networkName, + editCompleted: true, + }), + ); } } @@ -925,8 +932,12 @@ const NetworksForm = ({ ))} ) : null} + {networkMenuRedesign ? ( - + ) : ( { +import { showModal, toggleNetworkMenu } from '../../../../store/actions'; + +export const RpcUrlEditor = ({ + currentRpcUrl, + onRpcUrlAdd, +}: { + currentRpcUrl: string; + onRpcUrlAdd: () => void; +}) => { // TODO: real endpoints const dummyRpcUrls = [ currentRpcUrl, - 'https://dummy.mainnet.public.blastapi.io', - 'https://dummy.io/v3/blockchain/node/dummy', + 'https://mainnet.public.blastapi.io', + 'https://infura.foo.bar.baz/123456789', ]; const t = useI18nContext(); + const dispatch = useDispatch(); const rpcDropdown = useRef(null); - const [isOpen, setIsOpen] = useState(false); + const [isDropdownOpen, setIsDropdownOpen] = useState(false); const [currentRpcEndpoint, setCurrentRpcEndpoint] = useState(currentRpcUrl); return ( @@ -48,7 +58,7 @@ export const RpcUrlEditor = ({ currentRpcUrl }: { currentRpcUrl: string }) => { {t('defaultRpcUrl')} setIsOpen(!isOpen)} + onClick={() => setIsDropdownOpen(!isDropdownOpen)} className="networks-tab__rpc-dropdown" display={Display.Flex} justifyContent={JustifyContent.spaceBetween} @@ -60,7 +70,7 @@ export const RpcUrlEditor = ({ currentRpcUrl }: { currentRpcUrl: string }) => { > {currentRpcEndpoint} @@ -69,19 +79,24 @@ export const RpcUrlEditor = ({ currentRpcUrl }: { currentRpcUrl: string }) => { paddingTop={2} paddingBottom={2} paddingLeft={0} + matchWidth={true} paddingRight={0} className="networks-tab__rpc-popover" referenceElement={rpcDropdown.current} position={PopoverPosition.Bottom} - isOpen={isOpen} + isOpen={isDropdownOpen} > {dummyRpcUrls.map((rpcEndpoint) => ( setCurrentRpcEndpoint(rpcEndpoint)} + onClick={() => { + setCurrentRpcEndpoint(rpcEndpoint); + setIsDropdownOpen(false); + }} className={classnames('networks-tab__rpc-item', { 'networks-tab__rpc-item--selected': rpcEndpoint === currentRpcEndpoint, @@ -103,19 +118,25 @@ export const RpcUrlEditor = ({ currentRpcUrl }: { currentRpcUrl: string }) => { {rpcEndpoint} alert('TODO: delete confirmation modal')} + onClick={(e: React.MouseEvent) => { + e.stopPropagation(); + dispatch(toggleNetworkMenu()); + dispatch( + showModal({ + name: 'CONFIRM_DELETE_RPC_URL', + }), + ); + }} /> ))} alert('TODO: add RPC modal')} + onClick={onRpcUrlAdd} padding={4} display={Display.Flex} alignItems={AlignItems.center} diff --git a/ui/selectors/selectors.js b/ui/selectors/selectors.js index f613015efb7c..24809b75fe52 100644 --- a/ui/selectors/selectors.js +++ b/ui/selectors/selectors.js @@ -2032,6 +2032,10 @@ export function getNewNetworkAdded(state) { return state.appState.newNetworkAddedName; } +/** + * @param state + * @returns {{ networkConfigurationId: string; nickname: string; editCompleted: boolean} | undefined} + */ export function getEditedNetwork(state) { return state.appState.editedNetwork; } diff --git a/ui/store/actions.test.js b/ui/store/actions.test.js index 813153d33ca3..aa161ba781a1 100644 --- a/ui/store/actions.test.js +++ b/ui/store/actions.test.js @@ -1395,6 +1395,8 @@ describe('Actions', () => { const newNetworkAddedDetails = { nickname: 'test-chain', + networkConfigurationId: 'testNetworkConfigurationId', + editCompleted: true, }; store.dispatch(actions.setEditedNetwork(newNetworkAddedDetails)); diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 4a75d4828212..186e59ccb8df 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -4139,16 +4139,16 @@ export function setNewNetworkAdded({ }; } -export function setEditedNetwork({ - nickname, -}: { - networkConfigurationId: string; - nickname: string; -}): PayloadAction { - return { - type: actionConstants.SET_EDIT_NETWORK, - payload: { nickname }, - }; +export function setEditedNetwork( + payload: + | { + networkConfigurationId: string; + nickname: string; + editCompleted: boolean; + } + | undefined = undefined, +): PayloadAction { + return { type: actionConstants.SET_EDIT_NETWORK, payload }; } export function setNewNftAddedMessage(