From 53527e23e61d43260c2904749fffba4e892637ff Mon Sep 17 00:00:00 2001 From: salimtb Date: Wed, 26 Jun 2024 15:16:03 +0200 Subject: [PATCH 1/7] feat: improve add network fields checkers --- app/components/Nav/App/index.js | 9 + .../__snapshots__/index.test.tsx.snap | 2 +- .../NetworksSettings/NetworkSettings/index.js | 192 +++++++++++++++-- .../NetworkSettings/index.test.tsx | 202 +++++++++++++++++- app/constants/navigation/Routes.ts | 1 + locales/languages/en.json | 8 + 6 files changed, 397 insertions(+), 17 deletions(-) diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index 2ca02dd680e..0fa896eb094 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -119,6 +119,7 @@ import NFTAutoDetectionModal from '../../../../app/components/Views/NFTAutoDetec import NftOptions from '../../../components/Views/NftOptions'; import ShowTokenIdSheet from '../../../components/Views/ShowTokenIdSheet'; import OriginSpamModal from '../../Views/OriginSpamModal/OriginSpamModal'; +import { isNetworkUiRedesignEnabled } from '../../../util/networks'; ///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps) import { SnapsExecutionWebView } from '../../../lib/snaps'; ///: END:ONLY_INCLUDE_IF @@ -924,6 +925,14 @@ const App = ({ userLoggedIn }) => { component={AddNetworkFlow} options={{ animationEnabled: true }} /> + {isNetworkUiRedesignEnabled ? ( + + ) : null} + - + `; diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js index ad606a6149e..fec31f3d83f 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js @@ -78,6 +78,11 @@ import Routes from '../../../../../constants/navigation/Routes'; import { selectUseSafeChainsListValidation } from '../../../../../../app/selectors/preferencesController'; import withIsOriginalNativeToken from './withIsOriginalNativeToken'; import { compose } from 'redux'; +import Icon, { + IconColor, + IconName, + IconSize, +} from '../../../../../component-library/components/Icons/Icon'; const createStyles = (colors) => StyleSheet.create({ @@ -143,6 +148,10 @@ const createStyles = (colors) => flexGrow: 1, flexShrink: 1, }, + newWarningContainer: { + flexGrow: 1, + flexShrink: 1, + }, label: { fontSize: 14, paddingVertical: 12, @@ -163,6 +172,18 @@ const createStyles = (colors) => color: colors.text.default, ...fontStyles.normal, }, + messageWarning: { + paddingVertical: 2, + fontSize: 14, + color: colors.warning.default, + ...typography.sBodyMD, + }, + suggestionButton: { + color: colors.text.default, + paddingLeft: 2, + paddingRight: 4, + marginTop: 4, + }, inlineWarning: { paddingVertical: 2, fontSize: 14, @@ -252,7 +273,7 @@ const allNetworksblockExplorerUrl = (networkName) => /** * Main view for app configurations */ -class NetworkSettings extends PureComponent { +export class NetworkSettings extends PureComponent { static propTypes = { /** * Network configurations @@ -523,11 +544,31 @@ class NetworkSettings extends PureComponent { return true; }; + checkIfChainIdExists = async (chainId) => { + const checkCustomNetworks = Object.values( + this.props.networkConfigurations, + ).filter((item) => item.chainId === toHex(chainId)); + + if (isNetworkUiRedesignEnabled && checkCustomNetworks.length > 0) { + return true; + } + return false; + }; + checkIfNetworkExists = async (rpcUrl) => { const checkCustomNetworks = Object.values( this.props.networkConfigurations, ).filter((item) => item.rpcUrl === rpcUrl); + if (checkCustomNetworks.length > 0) { + if (isNetworkUiRedesignEnabled) { + this.setState({ + warningRpcUrl: strings( + 'app_settings.url_associated_to_another_chain_id', + ), + }); + return checkCustomNetworks; + } this.setState({ warningRpcUrl: strings('app_settings.network_exists') }); return checkCustomNetworks; } @@ -689,6 +730,14 @@ class NetworkSettings extends PureComponent { } if (isNetworkExists.length > 0) { + if (isNetworkUiRedesignEnabled) { + return this.setState({ + validatedRpcURL: false, + warningRpcUrl: strings( + 'app_settings.url_associated_to_another_chain_id', + ), + }); + } return this.setState({ validatedRpcURL: true, warningRpcUrl: strings('app_settings.network_exists'), @@ -712,8 +761,26 @@ class NetworkSettings extends PureComponent { /** * Validates that chain id is a valid integer number, setting a warningChainId if is invalid */ - validateChainId = () => { - const { chainId } = this.state; + validateChainId = async () => { + const { chainId, rpcUrl, editable } = this.state; + + const isChainIdExists = await this.checkIfChainIdExists(chainId); + const isNetworkExists = await this.checkIfNetworkExists(rpcUrl); + + if ( + isChainIdExists && + isNetworkExists.length > 0 && + isNetworkUiRedesignEnabled && + !editable + ) { + return this.setState({ + validateChainId: false, + warningChainId: strings( + 'app_settings.chain_id_associated_with_another_network', + ), + }); + } + if (!chainId) { return this.setState({ warningChainId: strings('app_settings.chain_id_required'), @@ -753,6 +820,29 @@ class NetworkSettings extends PureComponent { }); } + let endpointChainId; + let providerError; + try { + endpointChainId = await jsonRpcRequest(rpcUrl, 'eth_chainId'); + } catch (err) { + Logger.error(err, 'Failed to fetch the chainId from the endpoint.'); + providerError = err; + } + + if (providerError || typeof endpointChainId !== 'string') { + return this.setState({ + validatedRpcURL: false, + warningRpcUrl: strings('app_settings.unMatched_chain'), + }); + } + + if (endpointChainId !== toHex(chainId)) { + return this.setState({ + validatedRpcURL: false, + warningChainId: strings('app_settings.unMatched_chain_name'), + }); + } + this.validateRpcAndChainId(); this.setState({ warningChainId: undefined, validatedChainId: true }); }; @@ -976,6 +1066,17 @@ class NetworkSettings extends PureComponent { navigation.goBack(); }; + goToNetworkEdit = () => { + const { rpcUrl } = this.state; + const { navigation } = this.props; + navigation.goBack(); + navigation.navigate(Routes.EDIT_NETWORK, { + network: rpcUrl, + shouldNetworkSwitchPopToWallet: false, + shouldShowPopularNetworks: false, + }); + }; + showNetworkModal = (networkConfiguration) => { this.setState({ showPopularNetworkModal: true, @@ -1075,6 +1176,65 @@ class NetworkSettings extends PureComponent { const shouldNetworkSwitchPopToWallet = route.params?.shouldNetworkSwitchPopToWallet ?? true; + const renderWarningChainId = () => { + const CHAIN_LIST_URL = 'https://chainid.network/'; + const containerStyle = isNetworkUiRedesignEnabled + ? styles.newWarningContainer + : styles.warningContainer; + + if (warningChainId) { + if (warningChainId === strings('app_settings.unMatched_chain_name')) { + return ( + + {warningChainId} + + + {strings('app_settings.find_the_right_one')}{' '} + Linking.openURL(CHAIN_LIST_URL)} + > + chainid.network{' '} + + + + + + ); + } + if ( + warningChainId === + strings('app_settings.chain_id_associated_with_another_network') + ) { + return ( + + + {strings( + 'app_settings.chain_id_associated_with_another_network', + )}{' '} + this.goToNetworkEdit()} + > + {strings('app_settings.edit_original_network')} + + + + ); + } + return ( + + {warningChainId} + + ); + } + return null; + }; + const renderWarningSymbol = () => { const { validatedSymbol } = this.state; if (warningSymbol) { @@ -1135,7 +1295,11 @@ class NetworkSettings extends PureComponent { {!networkTypeOrRpcUrl ? ( ) : null} @@ -1163,8 +1327,11 @@ class NetworkSettings extends PureComponent { /> {warningName ? ( + + {strings('wallet.incorrect_network_name_warning')} + - {strings('wallet.chain_id_currently_used')}{' '} + {strings('wallet.suggested_name')}{' '} { @@ -1172,8 +1339,7 @@ class NetworkSettings extends PureComponent { }} > {warningName} - {' '} - {strings('asset_details.network').toLowerCase()} + ) : null} @@ -1197,7 +1363,11 @@ class NetworkSettings extends PureComponent { /> {warningRpcUrl && ( {warningRpcUrl} @@ -1223,11 +1393,7 @@ class NetworkSettings extends PureComponent { testID={NetworksViewSelectorsIDs.CHAIN_INPUT} keyboardAppearance={themeAppearance} /> - {warningChainId ? ( - - {warningChainId} - - ) : null} + {renderWarningChainId()} {strings('app_settings.network_symbol_label')} diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx index 9b26c525ae2..26572bf7a89 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx @@ -1,11 +1,20 @@ +// NetworkSettings.test.js or NetworkSettings.test.tsx + import React from 'react'; import { shallow } from 'enzyme'; -import NetworkSettings from './'; +import { NetworkSettings } from './'; // Import the undecorated component import configureMockStore from 'redux-mock-store'; import { Provider } from 'react-redux'; +import { ThemeContext, mockTheme } from '../../../../../../app/util/theme'; import { backgroundState } from '../../../../../util/test/initial-root-state'; +import { NETWORKS_CHAIN_ID } from '../../../../../constants/network'; +import { getEtherscanBaseUrl } from '../../../../../util/etherscan'; +import Networks, { getAllNetworks } from '../../../../../util/networks'; +import { strings } from '../../../../../../locales/i18n'; +jest.useFakeTimers(); const mockStore = configureMockStore(); + const initialState = { engine: { backgroundState, @@ -14,15 +23,202 @@ const initialState = { networkOnboardedState: { '1': true }, }, }; + const store = mockStore(initialState); +const SAMPLE_NETWORKSETTINGS_PROPS = { + route: { params: {} }, + navigation: { setOptions: jest.fn(), navigate: jest.fn(), goBack: jest.fn() }, +}; + describe('NetworkSettings', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let wrapper: any; + + beforeEach(() => { + wrapper = shallow( + + + + + , + ) + .find(NetworkSettings) + .dive(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + it('should render correctly', () => { - const wrapper = shallow( + const component = shallow( , ); - expect(wrapper).toMatchSnapshot(); + + expect(component).toMatchSnapshot(); + }); + + it('should return an empty string if the mainnet configuration is not found', () => { + const newProps = { + ...SAMPLE_NETWORKSETTINGS_PROPS, + networkConfigurations: { + '4': { + chainId: '4', + rpcUrl: 'https://rinkeby.infura.io/v3/YOUR-PROJECT-ID', + }, + }, + }; + + wrapper = shallow( + + + , + ) + .find(NetworkSettings) + .dive(); + + const instance = wrapper.instance(); + const rpcUrl = instance.getCustomMainnetRPCURL(); + expect(rpcUrl).toBe(''); + }); + it('should update state and call getCurrentState on RPC URL change', async () => { + const getCurrentStateSpy = jest.spyOn( + wrapper.instance(), + 'getCurrentState', + ); + + await wrapper.instance().onRpcUrlChange('http://localhost:8545'); + + expect(wrapper.state('rpcUrl')).toBe('http://localhost:8545'); + expect(getCurrentStateSpy).toHaveBeenCalled(); + }); + + it('should initialize state correctly when networkTypeOrRpcUrl is provided', () => { + const SAMPLE_NETWORKSETTINGS_PROPS_2 = { + route: { params: { networkTypeOrRpcUrl: true } }, + navigation: { + setOptions: jest.fn(), + navigate: jest.fn(), + goBack: jest.fn(), + }, + }; + + const wrapper2 = shallow( + + + , + ) + .find(NetworkSettings) + .dive(); + + const instance = wrapper2.instance() as NetworkSettings; + instance.componentDidMount(); + + expect(wrapper2.state('blockExplorerUrl')).toBe(undefined); + expect(wrapper2.state('nickname')).toBe(undefined); + expect(wrapper2.state('chainId')).toBe(undefined); + expect(wrapper2.state('rpcUrl')).toBe(undefined); + }); + + it('should update state and call getCurrentState on nickname change', async () => { + const getCurrentStateSpy = jest.spyOn( + wrapper.instance(), + 'getCurrentState', + ); + + await wrapper.instance().onNicknameChange('Localhost'); + + expect(wrapper.state('nickname')).toBe('Localhost'); + expect(getCurrentStateSpy).toHaveBeenCalled(); + }); + + it('should initialize state correctly on componentDidMount', () => { + const instance = wrapper.instance(); + instance.componentDidMount(); + + expect(wrapper.state('rpcUrl')).toBe(undefined); + expect(wrapper.state('blockExplorerUrl')).toBe(undefined); + expect(wrapper.state('nickname')).toBe(undefined); + expect(wrapper.state('chainId')).toBe(undefined); + expect(wrapper.state('ticker')).toBe(undefined); + expect(wrapper.state('editable')).toBe(undefined); + expect(wrapper.state('addMode')).toBe(true); + expect(wrapper.state('warningRpcUrl')).toBe(undefined); + expect(wrapper.state('warningChainId')).toBe(undefined); + expect(wrapper.state('warningSymbol')).toBe(undefined); + expect(wrapper.state('validatedRpcURL')).toBe(true); + expect(wrapper.state('validatedChainId')).toBe(true); + expect(wrapper.state('validatedSymbol')).toBe(true); + expect(wrapper.state('initialState')).toBe(undefined); + expect(wrapper.state('enableAction')).toBe(false); + expect(wrapper.state('inputWidth')).toEqual({ width: '99%' }); + expect(wrapper.state('showPopularNetworkModal')).toBe(false); + expect(wrapper.state('popularNetwork')).toEqual({}); + expect(wrapper.state('showWarningModal')).toBe(false); + expect(wrapper.state('showNetworkDetailsModal')).toBe(false); + expect(wrapper.state('isNameFieldFocused')).toBe(false); + expect(wrapper.state('isSymbolFieldFocused')).toBe(false); + expect(wrapper.state('networkList')).toEqual([]); + }); + + it('should add RPC URL correctly', async () => { + wrapper.setState({ + rpcUrl: 'http://localhost:8545', + chainId: '0x1', + ticker: 'ETH', + nickname: 'Localhost', + }); + + await wrapper.instance().addRpcUrl(); + + expect(wrapper.state('rpcUrl')).toBe('http://localhost:8545'); + }); + + it('should validate RPC URL and Chain ID combination', async () => { + wrapper.setState({ rpcUrl: 'http://localhost:8545', chainId: '0x1' }); + + await wrapper.instance().validateRpcAndChainId(); + + expect(wrapper.state('validatedRpcURL')).toBe(true); + expect(wrapper.state('validatedChainId')).toBe(true); + }); + + it('should update state and call getCurrentState on block explorer URL change', async () => { + const getCurrentStateSpy = jest.spyOn( + wrapper.instance(), + 'getCurrentState', + ); + + await wrapper.instance().onBlockExplorerUrlChange('https://etherscan.io'); + + expect(wrapper.state('blockExplorerUrl')).toBe('https://etherscan.io'); + expect(getCurrentStateSpy).toHaveBeenCalled(); + }); + + it('should update state and call getCurrentState on ticker change', async () => { + const getCurrentStateSpy = jest.spyOn( + wrapper.instance(), + 'getCurrentState', + ); + + await wrapper.instance().onTickerChange('ETH'); + + expect(wrapper.state('ticker')).toBe('ETH'); + expect(getCurrentStateSpy).toHaveBeenCalled(); + }); + + it('should update state and call getCurrentState on Chain ID change', async () => { + const getCurrentStateSpy = jest.spyOn( + wrapper.instance(), + 'getCurrentState', + ); + + await wrapper.instance().onChainIDChange('0x1'); + + expect(wrapper.state('chainId')).toBe('0x1'); + expect(getCurrentStateSpy).toHaveBeenCalled(); }); }); diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts index 628cf28496e..834cf692a35 100644 --- a/app/constants/navigation/Routes.ts +++ b/app/constants/navigation/Routes.ts @@ -124,6 +124,7 @@ const Routes = { WALLET_RESET_NEEDED: 'WalletResetNeeded', }, ADD_NETWORK: 'AddNetwork', + EDIT_NETWORK: 'EditNetwork', SWAPS: 'Swaps', LOCK_SCREEN: 'LockScreen', NOTIFICATIONS: { diff --git a/locales/languages/en.json b/locales/languages/en.json index a86a6b93add..187374500ee 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -451,6 +451,8 @@ "use_the_currency_symbol": "uses the currency symbol", "use_correct_symbol": "Make sure you’re using the correct symbol before continuing", "chain_id_currently_used": "This Chain ID is currently used by the", + "incorrect_network_name_warning": "According to our records, the network name may not correctly match this chain ID.", + "suggested_name": "Suggested name:", "network_check_validation_desc": "reduces your chances of connecting to a malicious or incorrect network.", "cant_verify_custom_network_warning": "We can’t verify custom networks. To avoid malicious providers from recording your network activity, only add networks you trust.", "nfts_autodetection_cta": "Turn on NFT detection in Settings", @@ -949,6 +951,12 @@ "popular": "Popular", "delete": "Delete", "network_exists": "This network has already been added.", + "unMatched_chain": "According to our records, this URL does not match a known provider for this chain ID.", + "unMatched_chain_name": "This chain ID doesn’t match the network name.", + "url_associated_to_another_chain_id": "This URL is associated with another chain ID.", + "chain_id_associated_with_another_network": "The information you have entered is associated with an existing chain ID. Update your information or", + "edit_original_network": "edit the original network", + "find_the_right_one": "Find the right one on:", "delete_metrics_title": "Delete MetaMetrics data", "delete_metrics_description_part_one": "This will delete historical", "delete_metrics_description_part_two": "MetaMetrics", From d2269d945d30ce456e0ed9d5cc866a06006a9910 Mon Sep 17 00:00:00 2001 From: salimtb Date: Thu, 8 Aug 2024 00:15:32 +0200 Subject: [PATCH 2/7] fix: fix test coverage --- .../NetworksSettings/NetworkSettings/index.js | 105 +++++----- .../NetworkSettings/index.test.tsx | 197 ++++++++++++++++-- 2 files changed, 235 insertions(+), 67 deletions(-) diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js index fec31f3d83f..51ea6a71021 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js @@ -545,14 +545,18 @@ export class NetworkSettings extends PureComponent { }; checkIfChainIdExists = async (chainId) => { - const checkCustomNetworks = Object.values( - this.props.networkConfigurations, - ).filter((item) => item.chainId === toHex(chainId)); + const { networkConfigurations } = this.props; - if (isNetworkUiRedesignEnabled && checkCustomNetworks.length > 0) { - return true; - } - return false; + // Convert the chainId to hex format + const hexChainId = toHex(chainId); + + // Check if any network configuration matches the given chainId + const chainIdExists = Object.values(networkConfigurations).some( + (item) => item.chainId === hexChainId, + ); + + // Return true if the chainId exists and the UI redesign is enabled, otherwise false + return isNetworkUiRedesignEnabled && chainIdExists; }; checkIfNetworkExists = async (rpcUrl) => { @@ -1277,6 +1281,51 @@ export class NetworkSettings extends PureComponent { return null; }; + const renderButtons = () => { + if (addMode || editable) { + return ( + + {editable ? ( + +