From 43f7a44c254ba741a9b3421ca397689f75c8506b Mon Sep 17 00:00:00 2001 From: Filip Sekulic Date: Thu, 30 Jun 2022 18:19:07 +0200 Subject: [PATCH] Adding popular custom network integration (#14557) * Initial push * Refactored the code * Additional code * Removed the unused message * Added a tooltip * Fixed tests * Lint fix * Added style to a tooltip * Fix e2e test failure * Lint fix and code revert * Fix e2e test * Fixed paddings * Fixed paddings * CSS fix * Minified svg files * Applied requested changes * Fixed theme issue * Code revert * Added back overridden code * Icon problem fixed * Lint fix * Replaced H3 with H4 * Added unit test * Added breadcrumbs * Added const props for networks * Lint fix * Lint fix * Added toggle button for showing the custom network list and resolved few issues * Fixed routes * Refactored a piece of code * Enabled searching for the newly created option * Fixed unit test * Updated theme --- app/_locales/en/messages.json | 26 ++ app/images/fantom-opera.svg | 1 + app/images/harmony-one.svg | 1 + app/images/info-fox.svg | 1 + app/scripts/controllers/preferences.js | 12 + app/scripts/metamask-controller.js | 44 +- shared/constants/network.js | 109 ++++- ui/components/app/add-network/add-network.js | 390 ++++++++++++------ .../app/add-network/add-network.test.js | 60 +++ ui/components/app/add-network/index.scss | 56 ++- .../app/dropdowns/network-dropdown.js | 11 +- .../metamask-template-renderer.js | 3 + .../safe-component-list.js | 5 + ui/components/ui/chip/chip.js | 19 +- ui/components/ui/chip/chip.scss | 8 + .../ui/definition-list/definition-list.js | 2 + ui/ducks/app/app.js | 10 + ui/helpers/constants/routes.js | 5 + ui/helpers/constants/settings.js | 7 + ui/helpers/utils/settings-search.test.js | 2 +- ui/pages/confirmation/confirmation.js | 53 ++- .../templates/add-ethereum-chain.js | 95 ++++- ui/pages/home/home.component.js | 55 ++- ui/pages/home/home.container.js | 14 +- ui/pages/home/index.scss | 14 + .../experimental-tab.component.js | 42 ++ .../experimental-tab.container.js | 5 + .../networks-form/networks-form.js | 1 - .../networks-tab-subheader.js | 21 +- .../networks-tab-subheader.test.js | 5 +- .../settings/networks-tab/networks-tab.js | 21 +- ui/pages/settings/settings.component.js | 9 +- ui/pages/settings/settings.container.js | 7 + ui/selectors/selectors.js | 10 + ui/store/actionConstants.js | 1 + ui/store/actions.js | 39 ++ 36 files changed, 972 insertions(+), 192 deletions(-) create mode 100644 app/images/fantom-opera.svg create mode 100644 app/images/harmony-one.svg create mode 100644 app/images/info-fox.svg create mode 100644 ui/components/app/add-network/add-network.test.js diff --git a/app/_locales/en/messages.json b/app/_locales/en/messages.json index 1bc9b2944..e7fe44c60 100644 --- a/app/_locales/en/messages.json +++ b/app/_locales/en/messages.json @@ -157,6 +157,9 @@ "addMemo": { "message": "Add memo" }, + "addMoreNetworks": { + "message": "add more networks manually" + }, "addNetwork": { "message": "Add Network" }, @@ -1954,6 +1957,9 @@ "network": { "message": "Network:" }, + "networkAddedSuccessfully": { + "message": "Network added successfully!" + }, "networkDetails": { "message": "Network Details" }, @@ -2891,6 +2897,12 @@ "showAdvancedGasInlineDescription": { "message": "Select this to show gas price and limit controls directly on the send and confirm screens." }, + "showCustomNetworkList": { + "message": "Show Custom Network List" + }, + "showCustomNetworkListDescription": { + "message": "Select this to show a list of networks with prefilled details when adding a new network." + }, "showFiatConversionInTestnets": { "message": "Show Conversion on test networks" }, @@ -3000,6 +3012,9 @@ "snapsToggle": { "message": "A snap will only run if it is enabled" }, + "someNetworksMayPoseSecurity": { + "message": "Some networks may pose security and/or privacy risks. Understand the risks before adding & using a network." + }, "somethingWentWrong": { "message": "Oops! Something went wrong." }, @@ -3562,6 +3577,10 @@ "switchNetworks": { "message": "Switch Networks" }, + "switchToNetwork": { + "message": "Switch to $1", + "description": "$1 represents the custom network that has previously been added" + }, "switchToThisAccount": { "message": "Switch to this account" }, @@ -4008,6 +4027,9 @@ "walletCreationSuccessTitle": { "message": "Wallet creation successful" }, + "wantToAddThisNetwork": { + "message": "Want to add this network?" + }, "warning": { "message": "Warning" }, @@ -4070,6 +4092,10 @@ "yesLetsTry": { "message": "Yes, let's try" }, + "youHaveAddedAll": { + "message": "You've added all the popular networks. You can discover more networks $1 Or you can $2", + "description": "$1 is a link with the text 'here' and $2 is a button with the text 'add more networks manually'" + }, "youNeedToAllowCameraAccess": { "message": "You need to allow camera access to use this feature." }, diff --git a/app/images/fantom-opera.svg b/app/images/fantom-opera.svg new file mode 100644 index 000000000..02297ee3a --- /dev/null +++ b/app/images/fantom-opera.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/harmony-one.svg b/app/images/harmony-one.svg new file mode 100644 index 000000000..e8466d96d --- /dev/null +++ b/app/images/harmony-one.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/images/info-fox.svg b/app/images/info-fox.svg new file mode 100644 index 000000000..57660c1fe --- /dev/null +++ b/app/images/info-fox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/scripts/controllers/preferences.js b/app/scripts/controllers/preferences.js index 706260b24..9b9c16d11 100644 --- a/app/scripts/controllers/preferences.js +++ b/app/scripts/controllers/preferences.js @@ -69,6 +69,7 @@ export default class PreferencesController { ? LEDGER_TRANSPORT_TYPES.WEBHID : LEDGER_TRANSPORT_TYPES.U2F, theme: 'light', + customNetworkListEnabled: false, ...opts.initState, }; @@ -179,6 +180,17 @@ export default class PreferencesController { this.store.updateState({ theme: val }); } + /** + * Setter for the `customNetworkListEnabled` property + * + * @param customNetworkListEnabled + */ + setCustomNetworkListEnabled(customNetworkListEnabled) { + this.store.updateState({ + customNetworkListEnabled, + }); + } + /** * Add new methodData to state, to avoid requesting this information again through Infura * diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index d8f79b6a7..d01d277e7 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -1572,7 +1572,8 @@ export default class MetamaskController extends EventEmitter { setCustomRpc: this.setCustomRpc.bind(this), updateAndSetCustomRpc: this.updateAndSetCustomRpc.bind(this), delCustomRpc: this.delCustomRpc.bind(this), - + addCustomNetwork: this.addCustomNetwork.bind(this), + requestUserApproval: this.requestUserApproval.bind(this), // PreferencesController setSelectedAddress: preferencesController.setSelectedAddress.bind( preferencesController, @@ -1609,7 +1610,9 @@ export default class MetamaskController extends EventEmitter { preferencesController, ), setTheme: preferencesController.setTheme.bind(preferencesController), - + setCustomNetworkListEnabled: preferencesController.setCustomNetworkListEnabled.bind( + preferencesController, + ), // AssetsContractController getTokenStandardAndDetails: this.getTokenStandardAndDetails.bind(this), @@ -2026,6 +2029,43 @@ export default class MetamaskController extends EventEmitter { } } + async requestUserApproval(customRpc, originIsMetaMask) { + try { + await this.approvalController.addAndShowApprovalRequest({ + origin: 'metamask', + type: 'wallet_addEthereumChain', + requestData: { + chainId: customRpc.chainId, + blockExplorerUrl: customRpc.rpcPrefs.blockExplorerUrl, + chainName: customRpc.nickname, + rpcUrl: customRpc.rpcUrl, + ticker: customRpc.ticker, + imageUrl: customRpc.rpcPrefs.imageUrl, + }, + }); + } catch (error) { + if ( + !(originIsMetaMask && error.message === 'User rejected the request.') + ) { + throw error; + } + } + } + + async addCustomNetwork(customRpc) { + const { chainId, chainName, rpcUrl, ticker, blockExplorerUrl } = customRpc; + + await this.preferencesController.addToFrequentRpcList( + rpcUrl, + chainId, + ticker, + chainName, + { + blockExplorerUrl, + }, + ); + } + /** * Create a new Vault and restore an existent keyring. * diff --git a/shared/constants/network.js b/shared/constants/network.js index 11f7803a6..ce8fa7166 100644 --- a/shared/constants/network.js +++ b/shared/constants/network.js @@ -28,6 +28,9 @@ export const POLYGON_CHAIN_ID = '0x89'; export const AVALANCHE_CHAIN_ID = '0xa86a'; export const FANTOM_CHAIN_ID = '0xfa'; export const CELO_CHAIN_ID = '0xa4ec'; +export const ARBITRUM_CHAIN_ID = '0xa4b1'; +export const HARMONY_CHAIN_ID = '0x63564c40'; +export const PALM_CHAIN_ID = '0x2a15c308d'; /** * The largest possible chain ID we can handle. @@ -43,7 +46,14 @@ export const GOERLI_DISPLAY_NAME = 'Goerli'; export const LOCALHOST_DISPLAY_NAME = 'Localhost 8545'; export const BSC_DISPLAY_NAME = 'Binance Smart Chain'; export const POLYGON_DISPLAY_NAME = 'Polygon'; -export const AVALANCHE_DISPLAY_NAME = 'Avalanche'; +export const AVALANCHE_DISPLAY_NAME = 'Avalanche Network C-Chain'; +export const ARBITRUM_DISPLAY_NAME = 'Arbitrum One'; +export const BNB_DISPLAY_NAME = + 'BNB Smart Chain (previously Binance Smart Chain Mainnet)'; +export const OPTIMISM_DISPLAY_NAME = 'Optimism'; +export const FANTOM_DISPLAY_NAME = 'Fantom Opera'; +export const HARMONY_DISPLAY_NAME = 'Harmony Mainnet Shard 0'; +export const PALM_DISPLAY_NAME = 'Palm'; const infuraProjectId = process.env.INFURA_PROJECT_ID; export const getRpcUrl = ({ network, excludeProjectId = false }) => @@ -64,12 +74,20 @@ export const MATIC_SYMBOL = 'MATIC'; export const AVALANCHE_SYMBOL = 'AVAX'; export const FANTOM_SYMBOL = 'FTM'; export const CELO_SYMBOL = 'CELO'; +export const ARBITRUM_SYMBOL = 'AETH'; +export const HARMONY_SYMBOL = 'ONE'; +export const PALM_SYMBOL = 'PALM'; export const ETH_TOKEN_IMAGE_URL = './images/eth_logo.svg'; export const TEST_ETH_TOKEN_IMAGE_URL = './images/black-eth-logo.svg'; export const BNB_TOKEN_IMAGE_URL = './images/bnb.png'; export const MATIC_TOKEN_IMAGE_URL = './images/matic-token.png'; export const AVAX_TOKEN_IMAGE_URL = './images/avax-token.png'; +export const AETH_TOKEN_IMAGE_URL = './images/arbitrum.svg'; +export const FTM_TOKEN_IMAGE_URL = './images/fantom-opera.svg'; +export const HARMONY_ONE_TOKEN_IMAGE_URL = './images/harmony-one.svg'; +export const OPTIMISM_TOKEN_IMAGE_URL = './images/optimism.svg'; +export const PALM_TOKEN_IMAGE_URL = './images/palm.svg'; export const INFURA_PROVIDER_TYPES = [ROPSTEN, RINKEBY, KOVAN, MAINNET, GOERLI]; @@ -166,6 +184,12 @@ export const CHAIN_ID_TO_NETWORK_IMAGE_URL_MAP = { [AVALANCHE_CHAIN_ID]: AVAX_TOKEN_IMAGE_URL, [BSC_CHAIN_ID]: BNB_TOKEN_IMAGE_URL, [POLYGON_CHAIN_ID]: MATIC_TOKEN_IMAGE_URL, + [ARBITRUM_CHAIN_ID]: AETH_TOKEN_IMAGE_URL, + [BSC_CHAIN_ID]: BNB_TOKEN_IMAGE_URL, + [FANTOM_CHAIN_ID]: FTM_TOKEN_IMAGE_URL, + [HARMONY_CHAIN_ID]: HARMONY_ONE_TOKEN_IMAGE_URL, + [OPTIMISM_CHAIN_ID]: OPTIMISM_TOKEN_IMAGE_URL, + [PALM_CHAIN_ID]: PALM_TOKEN_IMAGE_URL, }; export const CHAIN_ID_TO_NETWORK_ID_MAP = Object.values( @@ -309,3 +333,86 @@ export const BUYABLE_CHAINS_MAP = { }, }, }; + +export const FEATURED_RPCS = [ + { + chainId: ARBITRUM_CHAIN_ID, + nickname: ARBITRUM_DISPLAY_NAME, + rpcUrl: `https://arbitrum-mainnet.infura.io/v3/${infuraProjectId}`, + ticker: ARBITRUM_SYMBOL, + rpcPrefs: { + blockExplorerUrl: 'https://explorer.arbitrum.io', + imageUrl: AETH_TOKEN_IMAGE_URL, + }, + }, + { + chainId: AVALANCHE_CHAIN_ID, + nickname: AVALANCHE_DISPLAY_NAME, + rpcUrl: 'https://api.avax.network/ext/bc/C/rpc', + ticker: AVALANCHE_SYMBOL, + rpcPrefs: { + blockExplorerUrl: 'https://snowtrace.io/', + imageUrl: AVAX_TOKEN_IMAGE_URL, + }, + }, + { + chainId: BSC_CHAIN_ID, + nickname: BNB_DISPLAY_NAME, + rpcUrl: 'https://bsc-dataseed.binance.org/', + ticker: BNB_SYMBOL, + rpcPrefs: { + blockExplorerUrl: 'https://bscscan.com/', + imageUrl: BNB_TOKEN_IMAGE_URL, + }, + }, + { + chainId: FANTOM_CHAIN_ID, + nickname: FANTOM_DISPLAY_NAME, + rpcUrl: 'https://rpc.ftm.tools/', + ticker: FANTOM_SYMBOL, + rpcPrefs: { + blockExplorerUrl: 'https://ftmscan.com/', + imageUrl: FTM_TOKEN_IMAGE_URL, + }, + }, + { + chainId: HARMONY_CHAIN_ID, + nickname: HARMONY_DISPLAY_NAME, + rpcUrl: 'https://api.harmony.one/', + ticker: HARMONY_SYMBOL, + rpcPrefs: { + blockExplorerUrl: 'https://explorer.harmony.one/', + imageUrl: HARMONY_ONE_TOKEN_IMAGE_URL, + }, + }, + { + chainId: OPTIMISM_CHAIN_ID, + nickname: OPTIMISM_DISPLAY_NAME, + rpcUrl: `https://optimism-mainnet.infura.io/v3/${infuraProjectId}`, + ticker: ETH_SYMBOL, + rpcPrefs: { + blockExplorerUrl: 'https://optimistic.etherscan.io/', + imageUrl: OPTIMISM_TOKEN_IMAGE_URL, + }, + }, + { + chainId: PALM_CHAIN_ID, + nickname: PALM_DISPLAY_NAME, + rpcUrl: `https://palm-mainnet.infura.io/v3/${infuraProjectId}`, + ticker: PALM_SYMBOL, + rpcPrefs: { + blockExplorerUrl: 'https://explorer.palm.io/', + imageUrl: PALM_TOKEN_IMAGE_URL, + }, + }, + { + chainId: POLYGON_CHAIN_ID, + nickname: `${POLYGON_DISPLAY_NAME} ${capitalize(MAINNET)}`, + rpcUrl: `https://polygon-mainnet.infura.io/v3/${infuraProjectId}`, + ticker: MATIC_SYMBOL, + rpcPrefs: { + blockExplorerUrl: 'https://polygonscan.com/', + imageUrl: MATIC_TOKEN_IMAGE_URL, + }, + }, +]; diff --git a/ui/components/app/add-network/add-network.js b/ui/components/app/add-network/add-network.js index c453eedf5..3ebf2d293 100644 --- a/ui/components/app/add-network/add-network.js +++ b/ui/components/app/add-network/add-network.js @@ -1,168 +1,286 @@ -import React, { useContext } from 'react'; -import { useSelector } from 'react-redux'; -import PropTypes from 'prop-types'; +import React, { useContext, useEffect, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { useHistory } from 'react-router-dom'; import { I18nContext } from '../../../contexts/i18n'; import Box from '../../ui/box'; import Typography from '../../ui/typography'; import { ALIGN_ITEMS, - BLOCK_SIZES, COLORS, DISPLAY, FLEX_DIRECTION, FONT_WEIGHT, TYPOGRAPHY, JUSTIFY_CONTENT, + SIZES, } from '../../../helpers/constants/design-system'; import Button from '../../ui/button'; -import IconCaretLeft from '../../ui/icon/icon-caret-left'; import Tooltip from '../../ui/tooltip'; import IconWithFallback from '../../ui/icon-with-fallback'; import IconBorder from '../../ui/icon-border'; -import { getTheme } from '../../../selectors'; -import { THEME_TYPE } from '../../../pages/settings/experimental-tab/experimental-tab.constant'; +import { + getFrequentRpcListDetail, + getUnapprovedConfirmations, +} from '../../../selectors'; -const AddNetwork = ({ - onBackClick, - onAddNetworkClick, - onAddNetworkManuallyClick, - featuredRPCS, -}) => { +import { + ENVIRONMENT_TYPE_FULLSCREEN, + ENVIRONMENT_TYPE_POPUP, + MESSAGE_TYPE, +} from '../../../../shared/constants/app'; +import { requestUserApproval } from '../../../store/actions'; +import Popover from '../../ui/popover'; +import ConfirmationPage from '../../../pages/confirmation/confirmation'; +import { FEATURED_RPCS } from '../../../../shared/constants/network'; +import { ADD_NETWORK_ROUTE } from '../../../helpers/constants/routes'; +import { getEnvironmentType } from '../../../../app/scripts/lib/util'; + +const AddNetwork = () => { const t = useContext(I18nContext); - const theme = useSelector(getTheme); + const dispatch = useDispatch(); + const history = useHistory(); + const frequentRpcList = useSelector(getFrequentRpcListDetail); + + const frequentRpcListChainIds = Object.values(frequentRpcList).map( + (net) => net.chainId, + ); const infuraRegex = /infura.io/u; - const nets = featuredRPCS - .sort((a, b) => (a.ticker > b.ticker ? 1 : -1)) - .slice(0, 8); + const nets = FEATURED_RPCS.sort((a, b) => + a.ticker > b.ticker ? 1 : -1, + ).slice(0, FEATURED_RPCS.length); + + const notFrequentRpcNetworks = nets.filter( + (net) => frequentRpcListChainIds.indexOf(net.chainId) === -1, + ); + const unapprovedConfirmations = useSelector(getUnapprovedConfirmations); + const [showPopover, setShowPopover] = useState(false); + + useEffect(() => { + const anAddNetworkConfirmationFromMetaMaskExists = unapprovedConfirmations?.find( + (confirmation) => { + return ( + confirmation.origin === 'metamask' && + confirmation.type === MESSAGE_TYPE.ADD_ETHEREUM_CHAIN + ); + }, + ); + if (!showPopover && anAddNetworkConfirmationFromMetaMaskExists) { + setShowPopover(true); + } + + if (showPopover && !anAddNetworkConfirmationFromMetaMaskExists) { + setShowPopover(false); + } + }, [unapprovedConfirmations, showPopover]); return ( - - - - - {t('addNetwork')} - - - - + {Object.keys(notFrequentRpcNetworks).length === 0 ? ( + - {t('addFromAListOfPopularNetworks')} - - - {t('popularCustomNetworks')} - - {nets.map((item, index) => ( - - - - - - - {item.nickname} + + + + + + {t('youHaveAddedAll', [ + + {t('here')}. + , + , + ])} + + + + ) : ( + + {getEnvironmentType() === ENVIRONMENT_TYPE_FULLSCREEN && ( + + + {t('networks')} + + {' > '} + + {t('addANetwork')} - - { - // Warning for the networks that doesn't use infura.io as the RPC - !infuraRegex.test(item.rpcUrl) && ( - - {t('addNetworkTooltipWarning', [ - - {t('learnMoreUpperCase')} - , - ])} - - } - trigger="mouseenter" - theme={theme === THEME_TYPE.DEFAULT ? 'light' : 'dark'} - > - - - ) - } - - + + + + + + + + + {item.nickname} + + + + + { + // Warning for the networks that doesn't use infura.io as the RPC + !infuraRegex.test(item.rpcUrl) && ( + + {t('addNetworkTooltipWarning', [ + + {t('learnMoreUpperCase')} + , + ])} + + } + trigger="mouseenter" + > + + + ) + } + + + + ))} - ))} - - - - - + + + + + )} + {showPopover && ( + + + + )} + ); }; -AddNetwork.propTypes = { - onBackClick: PropTypes.func, - onAddNetworkClick: PropTypes.func, - onAddNetworkManuallyClick: PropTypes.func, - featuredRPCS: PropTypes.array, -}; - export default AddNetwork; diff --git a/ui/components/app/add-network/add-network.test.js b/ui/components/app/add-network/add-network.test.js new file mode 100644 index 000000000..d0272608f --- /dev/null +++ b/ui/components/app/add-network/add-network.test.js @@ -0,0 +1,60 @@ +import React from 'react'; +import { screen } from '@testing-library/react'; +import { renderWithProvider } from '../../../../test/jest'; +import configureStore from '../../../store/store'; +import mockState from '../../../../test/data/mock-state.json'; +import AddNetwork from './add-network'; + +jest.mock('../../../selectors', () => ({ + getFrequentRpcListDetail: () => ({ + frequentRpcList: [ + { + chainId: '0x539', + nickname: 'Localhost 8545', + rpcPrefs: {}, + rpcUrl: 'http://localhost:8545', + ticker: 'ETH', + }, + { + chainId: '0xA4B1', + nickname: 'Arbitrum One', + rpcPrefs: { blockExplorerUrl: 'https://explorer.arbitrum.io' }, + rpcUrl: + 'https://arbitrum-mainnet.infura.io/v3/7e127583378c4732a858df2550aff333', + ticker: 'AETH', + }, + ], + }), + getUnapprovedConfirmations: jest.fn(), + getTheme: () => 'light', +})); + +const render = () => { + const store = configureStore({ + metamask: { + ...mockState.metamask, + }, + }); + return renderWithProvider(, store); +}; + +describe('AddNetwork', () => { + it('should show Add from a list.. text', () => { + render(); + expect( + screen.getByText( + 'Add from a list of popular networks or add a network manually. Only interact with the entities you trust.', + ), + ).toBeInTheDocument(); + }); + + it('should show Popular custom networks text', () => { + render(); + expect(screen.getByText('Popular custom networks')).toBeInTheDocument(); + }); + + it('should show Arbitrum One network nickname', () => { + render(); + expect(screen.getByText('Arbitrum One')).toBeInTheDocument(); + }); +}); diff --git a/ui/components/app/add-network/index.scss b/ui/components/app/add-network/index.scss index 6da064bf5..4c7d9f909 100644 --- a/ui/components/app/add-network/index.scss +++ b/ui/components/app/add-network/index.scss @@ -1,10 +1,36 @@ .add-network { + &__networks-container { + padding-inline-end: 24px; + + @media screen and (max-width: $break-small) { + padding: 0; + } + } + &__header { border-bottom: 1px solid var(--color-border-default); - &__back-icon { - margin-left: 24px; - margin-right: 16px; + @media screen and (max-width: 575px) { + padding-inline-start: 24px; + padding-inline-end: 24px; + } + + &__subtitle { + margin-inline-start: 10px; + margin-inline-end: 10px; + } + } + + &__main-container { + @media screen and (max-width: 575px) { + padding-inline-start: 24px; + padding-inline-end: 24px; + } + } + + &__list-of-networks { + @media screen and (min-width: $break-large) { + width: 75%; } } @@ -23,19 +49,25 @@ &__add-icon { color: var(--color-text-alternative); - margin-left: auto; - margin-right: 0; + margin-inline-start: auto; + margin-inline-end: 0; cursor: pointer; } &__add-button.button { color: var(--color-primary-default); font-size: $font-size-h7; - margin-left: 24px; + margin-inline-start: 24px; } &__footer { border-top: 1px solid var(--color-border-muted); + width: 100%; + padding-bottom: 8px; + + @media screen and (max-width: 575px) { + padding-inline-start: 24px !important; + } & .btn-link { display: initial; @@ -51,6 +83,14 @@ color: var(--color-text-alternative); } } + + &__edge-case-box { + border: 1px solid var(--color-border-muted); + + &__link { + color: var(--color-info-default); + display: inline; + padding: 0; + } + } } - - diff --git a/ui/components/app/dropdowns/network-dropdown.js b/ui/components/app/dropdowns/network-dropdown.js index 33ac6af44..54af45b12 100644 --- a/ui/components/app/dropdowns/network-dropdown.js +++ b/ui/components/app/dropdowns/network-dropdown.js @@ -20,6 +20,7 @@ import { ENVIRONMENT_TYPE_POPUP } from '../../../../shared/constants/app'; import { EVENT } from '../../../../shared/constants/metametrics'; import { ADD_NETWORK_ROUTE, + ADD_POPULAR_CUSTOM_NETWORK, ADVANCED_ROUTE, } from '../../../helpers/constants/routes'; import IconCheck from '../../ui/icon/icon-check'; @@ -49,6 +50,7 @@ function mapStateToProps(state) { frequentRpcListDetail: state.metamask.frequentRpcListDetail || [], networkDropdownOpen: state.appState.networkDropdownOpen, showTestnetMessageInDropdown: state.metamask.showTestnetMessageInDropdown, + addPopularNetworkFeatureToggledOn: state.metamask.customNetworkListEnabled, }; } @@ -101,6 +103,7 @@ class NetworkDropdown extends Component { showTestnetMessageInDropdown: PropTypes.bool.isRequired, hideTestNetMessage: PropTypes.func.isRequired, history: PropTypes.object, + addPopularNetworkFeatureToggledOn: PropTypes.bool, }; handleClick(newProviderType) { @@ -129,10 +132,12 @@ class NetworkDropdown extends Component { + + + + )} ); } diff --git a/ui/pages/home/home.container.js b/ui/pages/home/home.container.js index 09254e101..1c7f5dfed 100644 --- a/ui/pages/home/home.container.js +++ b/ui/pages/home/home.container.js @@ -37,11 +37,16 @@ import { setNewNetworkAdded, setNewCollectibleAddedMessage, setNewTokensImported, + setRpcTarget, ///: BEGIN:ONLY_INCLUDE_IN(flask) removeSnapError, ///: END:ONLY_INCLUDE_IN } from '../../store/actions'; -import { setThreeBoxLastUpdated, hideWhatsNewPopup } from '../../ducks/app/app'; +import { + setThreeBoxLastUpdated, + hideWhatsNewPopup, + setNewCustomNetworkAdded, +} from '../../ducks/app/app'; import { getWeb3ShimUsageAlertEnabledness } from '../../ducks/metamask/metamask'; import { getSwapsFeatureIsLive } from '../../ducks/swaps/swaps'; import { getEnvironmentType } from '../../../app/scripts/lib/util'; @@ -138,6 +143,7 @@ const mapStateToProps = (state) => { isSigningQRHardwareTransaction, newCollectibleAddedMessage: getNewCollectibleAddedMessage(state), newTokensImported: getNewTokensImported(state), + newCustomNetworkAdded: appState.newCustomNetworkAdded, }; }; @@ -180,6 +186,12 @@ const mapDispatchToProps = (dispatch) => ({ setNewTokensImported: (newTokens) => { dispatch(setNewTokensImported(newTokens)); }, + setNewCustomNetworkAdded: () => { + dispatch(setNewCustomNetworkAdded({})); + }, + setRpcTarget: (rpcUrl, chainId, ticker, nickname) => { + dispatch(setRpcTarget(rpcUrl, chainId, ticker, nickname)); + }, }); export default compose( diff --git a/ui/pages/home/index.scss b/ui/pages/home/index.scss index dbecff153..11154b246 100644 --- a/ui/pages/home/index.scss +++ b/ui/pages/home/index.scss @@ -207,4 +207,18 @@ margin-inline-start: 32px; } } + + &__new-network-added { + border-radius: 10px; + text-align: center; + + &__check-circle { + color: var(--color-success-default); + margin-top: 20px; + } + + &__switch-to-button { + margin-bottom: 16px; + } + } } diff --git a/ui/pages/settings/experimental-tab/experimental-tab.component.js b/ui/pages/settings/experimental-tab/experimental-tab.component.js index dc30b54b2..d3f26068f 100644 --- a/ui/pages/settings/experimental-tab/experimental-tab.component.js +++ b/ui/pages/settings/experimental-tab/experimental-tab.component.js @@ -26,6 +26,8 @@ export default class ExperimentalTab extends PureComponent { setEIP1559V2Enabled: PropTypes.func, theme: PropTypes.string, setTheme: PropTypes.func, + customNetworkListEnabled: PropTypes.bool, + setCustomNetworkListEnabled: PropTypes.func, }; settingsRefs = Array( @@ -284,6 +286,45 @@ export default class ExperimentalTab extends PureComponent { ); } + renderCustomNetworkListToggle() { + const { t } = this.context; + const { + customNetworkListEnabled, + setCustomNetworkListEnabled, + } = this.props; + + return ( +
+
+ {t('showCustomNetworkList')} +
+ {t('showCustomNetworkListDescription')} +
+
+
+
+ { + this.context.trackEvent({ + category: EVENT.CATEGORIES.SETTINGS, + event: 'Enabled/Disable CustomNetworkList', + properties: { + action: 'Enabled/Disable CustomNetworkList', + legacy_event: true, + }, + }); + setCustomNetworkListEnabled(!value); + }} + offLabel={t('off')} + onLabel={t('on')} + /> +
+
+
+ ); + } + render() { return (
@@ -295,6 +336,7 @@ export default class ExperimentalTab extends PureComponent { {this.renderCollectibleDetectionToggle()} {this.renderEIP1559V2EnabledToggle()} {this.renderTheme()} + {this.renderCustomNetworkListToggle()}
); } diff --git a/ui/pages/settings/experimental-tab/experimental-tab.container.js b/ui/pages/settings/experimental-tab/experimental-tab.container.js index 1fb4124ee..244de6198 100644 --- a/ui/pages/settings/experimental-tab/experimental-tab.container.js +++ b/ui/pages/settings/experimental-tab/experimental-tab.container.js @@ -7,6 +7,7 @@ import { setOpenSeaEnabled, setEIP1559V2Enabled, setTheme, + setCustomNetworkListEnabled, } from '../../../store/actions'; import { getUseTokenDetection, @@ -14,6 +15,7 @@ import { getOpenSeaEnabled, getEIP1559V2Enabled, getTheme, + getIsCustomNetworkListEnabled, } from '../../../selectors'; import ExperimentalTab from './experimental-tab.component'; @@ -26,6 +28,7 @@ const mapStateToProps = (state) => { openSeaEnabled: getOpenSeaEnabled(state), eip1559V2Enabled: getEIP1559V2Enabled(state), theme: getTheme(state), + customNetworkListEnabled: getIsCustomNetworkListEnabled(state), }; }; @@ -40,6 +43,8 @@ const mapDispatchToProps = (dispatch) => { setOpenSeaEnabled: (val) => dispatch(setOpenSeaEnabled(val)), setEIP1559V2Enabled: (val) => dispatch(setEIP1559V2Enabled(val)), setTheme: (val) => dispatch(setTheme(val)), + setCustomNetworkListEnabled: (val) => + dispatch(setCustomNetworkListEnabled(val)), }; }; 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 ef846c638..22c28a170 100644 --- a/ui/pages/settings/networks-tab/networks-form/networks-form.js +++ b/ui/pages/settings/networks-tab/networks-form/networks-form.js @@ -522,7 +522,6 @@ const NetworksForm = ({ onConfirm: () => { resetForm(); dispatch(setSelectedSettingsRpcUrl('')); - history.push(NETWORKS_ROUTE); }, }), ); diff --git a/ui/pages/settings/networks-tab/networks-tab-subheader/networks-tab-subheader.js b/ui/pages/settings/networks-tab/networks-tab-subheader/networks-tab-subheader.js index 1420e3082..1fad5fdc4 100644 --- a/ui/pages/settings/networks-tab/networks-tab-subheader/networks-tab-subheader.js +++ b/ui/pages/settings/networks-tab/networks-tab-subheader/networks-tab-subheader.js @@ -1,18 +1,31 @@ import React from 'react'; import PropTypes from 'prop-types'; import { useHistory } from 'react-router-dom'; +import { useSelector } from 'react-redux'; import { useI18nContext } from '../../../../hooks/useI18nContext'; -import { ADD_NETWORK_ROUTE } from '../../../../helpers/constants/routes'; +import { + ADD_NETWORK_ROUTE, + ADD_POPULAR_CUSTOM_NETWORK, +} from '../../../../helpers/constants/routes'; import Button from '../../../../components/ui/button'; +import { getIsCustomNetworkListEnabled } from '../../../../selectors'; const NetworksFormSubheader = ({ addNewNetwork }) => { const t = useI18nContext(); const history = useHistory(); + const addPopularNetworkFeatureToggledOn = useSelector( + getIsCustomNetworkListEnabled, + ); + return addNewNetwork ? (
{t('networks')} + {' > '} +
{t('addANetwork')}
{' > '} -
{t('addANetwork')}
+
+ {t('addANetworkManually')} +
) : (
@@ -22,7 +35,9 @@ const NetworksFormSubheader = ({ addNewNetwork }) => { type="primary" onClick={(event) => { event.preventDefault(); - history.push(ADD_NETWORK_ROUTE); + addPopularNetworkFeatureToggledOn + ? history.push(ADD_POPULAR_CUSTOM_NETWORK) + : history.push(ADD_NETWORK_ROUTE); }} > {t('addANetwork')} diff --git a/ui/pages/settings/networks-tab/networks-tab-subheader/networks-tab-subheader.test.js b/ui/pages/settings/networks-tab/networks-tab-subheader/networks-tab-subheader.test.js index 099261049..44b5b1768 100644 --- a/ui/pages/settings/networks-tab/networks-tab-subheader/networks-tab-subheader.test.js +++ b/ui/pages/settings/networks-tab/networks-tab-subheader/networks-tab-subheader.test.js @@ -1,5 +1,6 @@ import React from 'react'; import configureMockStore from 'redux-mock-store'; +import { waitFor } from '@testing-library/react'; import { renderWithProvider } from '../../../../../test/jest/rendering'; import NetworksTabSubheader from '.'; @@ -36,11 +37,11 @@ describe('NetworksTabSubheader Component', () => { expect(getByRole('button', { text: 'Add a network' })).toBeDefined(); }); it('should render add network form subheader correctly', () => { - const { queryByText } = renderComponent({ + const { queryByText, getAllByText } = renderComponent({ addNewNetwork: true, }); expect(queryByText('Networks')).toBeInTheDocument(); - expect(queryByText('>')).toBeInTheDocument(); + waitFor(() => expect(getAllByText('>')).toBeInTheDocument()); expect(queryByText('Add a network')).toBeInTheDocument(); }); }); diff --git a/ui/pages/settings/networks-tab/networks-tab.js b/ui/pages/settings/networks-tab/networks-tab.js index c73787566..35814d419 100644 --- a/ui/pages/settings/networks-tab/networks-tab.js +++ b/ui/pages/settings/networks-tab/networks-tab.js @@ -1,11 +1,12 @@ import React, { useEffect } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { useLocation } from 'react-router-dom'; +import { useLocation, useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; import { useI18nContext } from '../../../hooks/useI18nContext'; import { ADD_NETWORK_ROUTE, + ADD_POPULAR_CUSTOM_NETWORK, NETWORKS_FORM_ROUTE, } from '../../../helpers/constants/routes'; import { setSelectedSettingsRpcUrl } from '../../../store/actions'; @@ -14,6 +15,7 @@ import { getEnvironmentType } from '../../../../app/scripts/lib/util'; import { ENVIRONMENT_TYPE_FULLSCREEN } from '../../../../shared/constants/app'; import { getFrequentRpcListDetail, + getIsCustomNetworkListEnabled, getNetworksTabSelectedRpcUrl, getProvider, } from '../../../selectors'; @@ -36,6 +38,7 @@ const NetworksTab = ({ addNewNetwork }) => { const t = useI18nContext(); const dispatch = useDispatch(); const { pathname } = useLocation(); + const history = useHistory(); const environmentType = getEnvironmentType(); const isFullScreen = environmentType === ENVIRONMENT_TYPE_FULLSCREEN; @@ -45,6 +48,9 @@ const NetworksTab = ({ addNewNetwork }) => { const frequentRpcListDetail = useSelector(getFrequentRpcListDetail); const provider = useSelector(getProvider); const networksTabSelectedRpcUrl = useSelector(getNetworksTabSelectedRpcUrl); + const addPopularNetworkFeatureToggledOn = useSelector( + getIsCustomNetworkListEnabled, + ); const frequentRpcNetworkListDetails = frequentRpcListDetail.map((rpc) => { return { @@ -118,9 +124,16 @@ const NetworksTab = ({ addNewNetwork }) => {