diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js
index 2ca02dd680e..52b8b5d1f7f 100644
--- a/app/components/Nav/App/index.js
+++ b/app/components/Nav/App/index.js
@@ -122,6 +122,7 @@ import OriginSpamModal from '../../Views/OriginSpamModal/OriginSpamModal';
///: BEGIN:ONLY_INCLUDE_IF(preinstalled-snaps,external-snaps)
import { SnapsExecutionWebView } from '../../../lib/snaps';
///: END:ONLY_INCLUDE_IF
+import isNetworkUiRedesignEnabled from '../../../util/networks/isNetworkUiRedesignEnabled';
const clearStackNavigatorOptions = {
headerShown: false,
@@ -924,6 +925,14 @@ const App = ({ userLoggedIn }) => {
component={AddNetworkFlow}
options={{ animationEnabled: true }}
/>
+ {isNetworkUiRedesignEnabled() ? (
+
+ ) : null}
+
{
- isNetworkUiRedesignEnabled && setIsSearchFieldFocused(true);
+ isNetworkUiRedesignEnabled() && setIsSearchFieldFocused(true);
},
onBlur: () => {
- isNetworkUiRedesignEnabled && setIsSearchFieldFocused(false);
+ isNetworkUiRedesignEnabled() && setIsSearchFieldFocused(false);
},
}
: {};
- const inputStylesWhichAreFeatureFlagged = !isNetworkUiRedesignEnabled
+ const inputStylesWhichAreFeatureFlagged = !isNetworkUiRedesignEnabled()
? styles.input
: isSearchFieldFocused
? styles.input
: styles.unfocusedInput;
- const containerInputStylesWhichAreFeatureFlagged = !isNetworkUiRedesignEnabled
- ? styles.inputWrapper
- : isSearchFieldFocused
- ? styles.focusedInputWrapper
- : styles.inputWrapper;
+ const containerInputStylesWhichAreFeatureFlagged =
+ !isNetworkUiRedesignEnabled()
+ ? styles.inputWrapper
+ : isSearchFieldFocused
+ ? styles.focusedInputWrapper
+ : styles.inputWrapper;
return (
diff --git a/app/components/Views/NetworkSelector/NetworkSelector.styles.ts b/app/components/Views/NetworkSelector/NetworkSelector.styles.ts
index 868d5445cd6..80bfbddbcfa 100644
--- a/app/components/Views/NetworkSelector/NetworkSelector.styles.ts
+++ b/app/components/Views/NetworkSelector/NetworkSelector.styles.ts
@@ -2,8 +2,8 @@
import Device from '../../../util/device';
import { StyleSheet } from 'react-native';
import { fontStyles } from '../../../styles/common';
-import { isNetworkUiRedesignEnabled } from '../../../util/networks';
import { Colors } from '../../../util/theme/models';
+import { isNetworkUiRedesignEnabled } from '../../../util/networks/isNetworkUiRedesignEnabled';
/**
* Style sheet function for NetworkSelector screen.
@@ -15,7 +15,7 @@ const createStyles = (colors: Colors) =>
marginHorizontal: 16,
marginBottom: Device.isAndroid()
? 16
- : isNetworkUiRedesignEnabled
+ : isNetworkUiRedesignEnabled()
? 12
: 0,
},
@@ -83,7 +83,7 @@ const createStyles = (colors: Colors) =>
marginTop: 1,
},
networkListContainer: {
- height: isNetworkUiRedesignEnabled ? '100%' : undefined,
+ height: isNetworkUiRedesignEnabled() ? '100%' : undefined,
},
networkIcon: {
width: 20,
diff --git a/app/components/Views/NetworkSelector/NetworkSelector.tsx b/app/components/Views/NetworkSelector/NetworkSelector.tsx
index 3a2d2431d9c..80b6d194a79 100644
--- a/app/components/Views/NetworkSelector/NetworkSelector.tsx
+++ b/app/components/Views/NetworkSelector/NetworkSelector.tsx
@@ -32,7 +32,6 @@ import Networks, {
getDecimalChainId,
isTestNet,
getNetworkImageSource,
- isNetworkUiRedesignEnabled,
} from '../../../util/networks';
import {
LINEA_MAINNET,
@@ -78,6 +77,7 @@ import { ButtonsAlignment } from '../../../component-library/components/BottomSh
import { ButtonProps } from '../../../component-library/components/Buttons/Button/Button.types';
import BottomSheetFooter from '../../../component-library/components/BottomSheets/BottomSheetFooter/BottomSheetFooter';
import { ExtendedNetwork } from '../Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.types';
+import { isNetworkUiRedesignEnabled } from '../../../util/networks/isNetworkUiRedesignEnabled';
const NetworkSelector = () => {
const [showPopularNetworkModal, setShowPopularNetworkModal] = useState(false);
@@ -95,14 +95,14 @@ const NetworkSelector = () => {
const providerConfig: ProviderConfig = useSelector(selectProviderConfig);
const networkConfigurations = useSelector(selectNetworkConfigurations);
- const avatarSize = isNetworkUiRedesignEnabled ? AvatarSize.Sm : undefined;
- const modalTitle = isNetworkUiRedesignEnabled
+ const avatarSize = isNetworkUiRedesignEnabled() ? AvatarSize.Sm : undefined;
+ const modalTitle = isNetworkUiRedesignEnabled()
? 'networks.additional_network_information_title'
: 'networks.network_warning_title';
- const modalDescription = isNetworkUiRedesignEnabled
+ const modalDescription = isNetworkUiRedesignEnabled()
? 'networks.additonial_network_information_desc'
: 'networks.network_warning_desc';
- const buttonLabelAddNetwork = isNetworkUiRedesignEnabled
+ const buttonLabelAddNetwork = isNetworkUiRedesignEnabled()
? 'app_settings.network_add_custom_network'
: 'app_settings.network_add_network';
const [showConfirmDeleteModal, setShowConfirmDeleteModal] = useState({
@@ -269,9 +269,9 @@ const NetworkSelector = () => {
const renderMainnet = () => {
const { name: mainnetName, chainId } = Networks.mainnet;
- if (isNetworkUiRedesignEnabled && isNoSearchResults(MAINNET)) return null;
+ if (isNetworkUiRedesignEnabled() && isNoSearchResults(MAINNET)) return null;
- if (isNetworkUiRedesignEnabled) {
+ if (isNetworkUiRedesignEnabled()) {
return (
{
const renderLineaMainnet = () => {
const { name: lineaMainnetName, chainId } = Networks['linea-mainnet'];
- if (isNetworkUiRedesignEnabled && isNoSearchResults('linea-mainnet'))
+ if (isNetworkUiRedesignEnabled() && isNoSearchResults('linea-mainnet'))
return null;
- if (isNetworkUiRedesignEnabled) {
+ if (isNetworkUiRedesignEnabled()) {
return (
{
if (!chainId) return null;
const { name } = { name: nickname || rpcUrl };
- if (isNetworkUiRedesignEnabled && isNoSearchResults(name)) return null;
+ if (isNetworkUiRedesignEnabled() && isNoSearchResults(name))
+ return null;
//@ts-expect-error - The utils/network file is still JS and this function expects a networkType, and should be optional
const image = getNetworkImageSource({ chainId: chainId?.toString() });
- if (isNetworkUiRedesignEnabled) {
+ if (isNetworkUiRedesignEnabled()) {
return (
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { name, imageSource, chainId } = (Networks as any)[networkType];
- if (isNetworkUiRedesignEnabled && isNoSearchResults(name)) return null;
+ if (isNetworkUiRedesignEnabled() && isNoSearchResults(name)) return null;
- if (isNetworkUiRedesignEnabled) {
+ if (isNetworkUiRedesignEnabled()) {
return (
{
const renderAdditonalNetworks = () => {
let filteredNetworks;
- if (isNetworkUiRedesignEnabled && searchString.length > 0)
+ if (isNetworkUiRedesignEnabled() && searchString.length > 0)
filteredNetworks = PopularList.filter(({ nickname }) =>
nickname.toLowerCase().includes(searchString.toLowerCase()),
);
@@ -615,7 +616,7 @@ const NetworkSelector = () => {
<>
- {isNetworkUiRedesignEnabled && (
+ {isNetworkUiRedesignEnabled() && (
{
/>
)}
- {isNetworkUiRedesignEnabled &&
+ {isNetworkUiRedesignEnabled() &&
searchString.length === 0 &&
renderEnabledNetworksTitle()}
{renderMainnet()}
{renderLineaMainnet()}
{renderRpcNetworks()}
- {isNetworkUiRedesignEnabled &&
+ {isNetworkUiRedesignEnabled() &&
searchString.length === 0 &&
renderPopularNetworksTitle()}
- {isNetworkUiRedesignEnabled && renderAdditonalNetworks()}
+ {isNetworkUiRedesignEnabled() && renderAdditonalNetworks()}
{searchString.length === 0 && renderTestNetworksSwitch()}
{showTestNetworks && renderOtherNetworks()}
@@ -656,7 +657,7 @@ const NetworkSelector = () => {
return (
- {isNetworkUiRedesignEnabled ? (
+ {isNetworkUiRedesignEnabled() ? (
{renderBottomSheetContent()}
diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.tsx b/app/components/Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.tsx
index 57902d7a092..50120794cfa 100644
--- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.tsx
+++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.tsx
@@ -17,7 +17,7 @@ import {
} from '../../../../../../selectors/networkController';
import AvatarNetwork from '../../../../../../component-library/components/Avatars/Avatar/variants/AvatarNetwork';
import { AvatarSize } from '../../../../../../component-library/components/Avatars/Avatar';
-import { isNetworkUiRedesignEnabled } from '../../../../../../util/networks';
+import { isNetworkUiRedesignEnabled } from '../../../../../../util/networks/isNetworkUiRedesignEnabled';
const CustomNetwork = ({
isNetworkModalVisible,
@@ -98,7 +98,7 @@ const CustomNetwork = ({
}
/>
| | | |
-
+
{networkConfiguration.nickname}
diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/__snapshots__/index.test.tsx.snap b/app/components/Views/Settings/NetworksSettings/NetworkSettings/__snapshots__/index.test.tsx.snap
index c3228995d25..ba068aa9447 100644
--- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/__snapshots__/index.test.tsx.snap
+++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/__snapshots__/index.test.tsx.snap
@@ -27,6 +27,68 @@ exports[`NetworkSettings should render correctly 1`] = `
}
}
>
-
+
+
+`;
+
+exports[`NetworkSettings should render the component correctly when isNetworkUiRedesignEnabled is false 1`] = `
+
+
+
+`;
+
+exports[`NetworkSettings should render the component correctly when isNetworkUiRedesignEnabled is true 1`] = `
+
+
`;
diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
index ad606a6149e..b61e57079bb 100644
--- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
+++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js
@@ -20,7 +20,6 @@ import Networks, {
isprivateConnection,
getAllNetworks,
getIsNetworkOnboarded,
- isNetworkUiRedesignEnabled,
} from '../../../../../util/networks';
import { getEtherscanBaseUrl } from '../../../../../util/etherscan';
import Engine from '../../../../../core/Engine';
@@ -78,6 +77,12 @@ 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';
+import { isNetworkUiRedesignEnabled } from '../../../../../util/networks/isNetworkUiRedesignEnabled';
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
@@ -321,6 +342,8 @@ class NetworkSettings extends PureComponent {
showNetworkDetailsModal: false,
isNameFieldFocused: false,
isSymbolFieldFocused: false,
+ isRpcUrlFieldFocused: false,
+ isChainIdFieldFocused: false,
networkList: [],
};
@@ -523,12 +546,34 @@ class NetworkSettings extends PureComponent {
return true;
};
+ checkIfChainIdExists = async (chainId) => {
+ const { networkConfigurations } = this.props;
+
+ // 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) => {
const checkCustomNetworks = Object.values(
this.props.networkConfigurations,
).filter((item) => item.rpcUrl === rpcUrl);
+
if (checkCustomNetworks.length > 0) {
- this.setState({ warningRpcUrl: strings('app_settings.network_exists') });
+ if (!isNetworkUiRedesignEnabled()) {
+ this.setState({
+ warningRpcUrl: strings('app_settings.network_exists'),
+ });
+ return checkCustomNetworks;
+ }
+
return checkCustomNetworks;
}
const defaultNetworks = getAllNetworks().map((item) => Networks[item]);
@@ -689,6 +734,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 +765,38 @@ 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: true,
+ warningChainId: strings(
+ 'app_settings.chain_id_associated_with_another_network',
+ ),
+ });
+ }
+
+ if (
+ isChainIdExists &&
+ isNetworkExists.length === 0 &&
+ isNetworkUiRedesignEnabled() &&
+ !editable
+ ) {
+ return this.setState({
+ validateChainId: true,
+ warningChainId: strings('app_settings.network_already_exist'),
+ });
+ }
+
if (!chainId) {
return this.setState({
warningChainId: strings('app_settings.chain_id_required'),
@@ -753,6 +836,37 @@ 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') &&
+ isNetworkUiRedesignEnabled()
+ ) {
+ return this.setState({
+ validatedRpcURL: false,
+ warningRpcUrl: strings('app_settings.unMatched_chain'),
+ });
+ }
+
+ if (endpointChainId !== toHex(chainId)) {
+ if (isNetworkUiRedesignEnabled()) {
+ return this.setState({
+ warningRpcUrl: strings(
+ 'app_settings.url_associated_to_another_chain_id',
+ ),
+ validatedRpcURL: false,
+ warningChainId: strings('app_settings.unMatched_chain_name'),
+ });
+ }
+ }
+
this.validateRpcAndChainId();
this.setState({ warningChainId: undefined, validatedChainId: true });
};
@@ -846,6 +960,13 @@ class NetworkSettings extends PureComponent {
*/
disabledByChainId = () => {
const { chainId, validatedChainId, warningChainId } = this.state;
+
+ if (isNetworkUiRedesignEnabled()) {
+ return (
+ !chainId ||
+ (chainId && (!validatedChainId || warningChainId !== undefined))
+ );
+ }
if (!chainId) return true;
return validatedChainId && !!warningChainId;
};
@@ -926,6 +1047,22 @@ class NetworkSettings extends PureComponent {
this.setState({ isSymbolFieldFocused: false });
};
+ onRpcUrlFocused = () => {
+ this.setState({ isRpcUrlFieldFocused: true });
+ };
+
+ onRpcUrlBlur = () => {
+ this.setState({ isRpcUrlFieldFocused: false });
+ };
+
+ onChainIdFocused = () => {
+ this.setState({ isChainIdFieldFocused: true });
+ };
+
+ onChainIdBlur = () => {
+ this.setState({ isChainIdFieldFocused: false });
+ };
+
jumpToRpcURL = () => {
const { current } = this.inputRpcURL;
current && current.focus();
@@ -976,6 +1113,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,
@@ -1005,6 +1153,8 @@ class NetworkSettings extends PureComponent {
inputWidth,
isNameFieldFocused,
isSymbolFieldFocused,
+ isRpcUrlFieldFocused,
+ isChainIdFieldFocused,
} = this.state;
const { route } = this.props;
const isCustomMainnet = route.params?.isCustomMainnet;
@@ -1050,6 +1200,26 @@ class NetworkSettings extends PureComponent {
isCustomMainnet ? styles.onboardingInput : undefined,
];
+ const inputErrorRpcStyle = [
+ warningRpcUrl
+ ? isRpcUrlFieldFocused
+ ? styles.inputWithFocus
+ : styles.inputWithError
+ : styles.input,
+ inputWidth,
+ isCustomMainnet ? styles.onboardingInput : undefined,
+ ];
+
+ const inputChainIdStyle = [
+ warningChainId
+ ? isChainIdFieldFocused
+ ? styles.inputWithFocus
+ : styles.inputWithError
+ : styles.input,
+ inputWidth,
+ isCustomMainnet ? styles.onboardingInput : undefined,
+ ];
+
const isRPCEditable = isCustomMainnet || editable;
const isActionDisabled =
!enableAction ||
@@ -1075,6 +1245,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) {
@@ -1117,6 +1346,51 @@ class NetworkSettings extends PureComponent {
return null;
};
+ const renderButtons = () => {
+ if (addMode || editable) {
+ return (
+
+ {editable ? (
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+ );
+ }
+ return null;
+ };
+
return this.state.showNetworkDetailsModal ? (
{!networkTypeOrRpcUrl ? (
) : null}
@@ -1163,8 +1441,11 @@ class NetworkSettings extends PureComponent {
/>
{warningName ? (
+
+ {strings('wallet.incorrect_network_name_warning')}
+
- {strings('wallet.chain_id_currently_used')}{' '}
+ {strings('wallet.suggested_name')}{' '}
{
@@ -1172,8 +1453,7 @@ class NetworkSettings extends PureComponent {
}}
>
{warningName}
- {' '}
- {strings('asset_details.network').toLowerCase()}
+
) : null}
@@ -1182,13 +1462,17 @@ class NetworkSettings extends PureComponent {
{
+ this.validateRpcUrl();
+ this.onRpcUrlBlur();
+ }}
+ onFocus={this.onRpcUrlFocused}
placeholder={strings('app_settings.network_rpc_placeholder')}
placeholderTextColor={colors.text.muted}
onSubmitEditing={this.jumpToChainId}
@@ -1197,7 +1481,11 @@ class NetworkSettings extends PureComponent {
/>
{warningRpcUrl && (
{warningRpcUrl}
@@ -1209,13 +1497,17 @@ class NetworkSettings extends PureComponent {
{
+ this.validateChainId();
+ this.onChainIdBlur();
+ }}
+ onFocus={this.onChainIdFocused}
placeholder={strings('app_settings.network_chain_id_placeholder')}
placeholderTextColor={colors.text.muted}
onSubmitEditing={this.jumpToSymbol}
@@ -1223,11 +1515,7 @@ class NetworkSettings extends PureComponent {
testID={NetworksViewSelectorsIDs.CHAIN_INPUT}
keyboardAppearance={themeAppearance}
/>
- {warningChainId ? (
-
- {warningChainId}
-
- ) : null}
+ {renderWarningChainId()}
{strings('app_settings.network_symbol_label')}
@@ -1286,47 +1574,7 @@ class NetworkSettings extends PureComponent {
testID={CustomDefaultNetworkIDs.USE_THIS_NETWORK_BUTTON_ID}
/>
) : (
- (addMode || editable) && (
-
- {editable ? (
-
-
-
-
- ) : (
-
-
-
- )}
-
- )
+ renderButtons()
)}
@@ -1402,7 +1650,7 @@ class NetworkSettings extends PureComponent {
testID={NetworksViewSelectorsIDs.CONTAINER}
>
- {(isNetworkUiRedesignEnabled && !shouldShowPopularNetworks) ||
+ {(isNetworkUiRedesignEnabled() && !shouldShowPopularNetworks) ||
networkTypeOrRpcUrl ? (
this.customNetwork(networkTypeOrRpcUrl)
) : (
diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx
index 9b26c525ae2..f82d968d18d 100644
--- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx
+++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.test.tsx
@@ -1,11 +1,21 @@
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';
+// eslint-disable-next-line import/no-namespace
+import * as networkUtils from '../../../../../util/networks/isNetworkUiRedesignEnabled';
+// Mock the entire module
+jest.mock('../../../../../util/networks/isNetworkUiRedesignEnabled', () => ({
+ isNetworkUiRedesignEnabled: jest.fn(),
+}));
+
+jest.useFakeTimers();
const mockStore = configureMockStore();
+
const initialState = {
engine: {
backgroundState,
@@ -14,15 +24,504 @@ const initialState = {
networkOnboardedState: { '1': true },
},
};
+
const store = mockStore(initialState);
+const SAMPLE_NETWORKSETTINGS_PROPS = {
+ route: { params: {} },
+ networkConfigurations: {
+ chainId: '0x1',
+ rpcUrl: 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID',
+ nickname: 'Ethereum mainnet',
+ rpcPrefs: {
+ blockExplorerUrl: 'https://etherscan.io',
+ },
+ ticker: 'ETH',
+ },
+ navigation: { setOptions: jest.fn(), navigate: jest.fn(), goBack: jest.fn() },
+ matchedChainNetwork: {
+ safeChainsList: [
+ {
+ name: 'Ethereum Mainnet',
+ chain: 'ETH',
+ icon: 'ethereum',
+ rpc: [
+ 'https://mainnet.infura.io/v3/${INFURA_API_KEY}',
+ 'wss://mainnet.infura.io/ws/v3/${INFURA_API_KEY}',
+ 'https://api.mycryptoapi.com/eth',
+ 'https://cloudflare-eth.com',
+ 'https://ethereum-rpc.publicnode.com',
+ 'wss://ethereum-rpc.publicnode.com',
+ 'https://mainnet.gateway.tenderly.co',
+ 'wss://mainnet.gateway.tenderly.co',
+ 'https://rpc.blocknative.com/boost',
+ 'https://rpc.flashbots.net',
+ 'https://rpc.flashbots.net/fast',
+ 'https://rpc.mevblocker.io',
+ 'https://rpc.mevblocker.io/fast',
+ 'https://rpc.mevblocker.io/noreverts',
+ 'https://rpc.mevblocker.io/fullprivacy',
+ 'https://eth.drpc.org',
+ 'wss://eth.drpc.org',
+ ],
+ features: [
+ {
+ name: 'EIP155',
+ },
+ {
+ name: 'EIP1559',
+ },
+ ],
+ faucets: [],
+ nativeCurrency: {
+ name: 'Ether',
+ symbol: 'ETH',
+ decimals: 18,
+ },
+ infoURL: 'https://ethereum.org',
+ shortName: 'eth',
+ chainId: 1,
+ networkId: 1,
+ slip44: 60,
+ ens: {
+ registry: '0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e',
+ },
+ explorers: [
+ {
+ name: 'etherscan',
+ url: 'https://etherscan.io',
+ standard: 'EIP3091',
+ },
+ {
+ name: 'blockscout',
+ url: 'https://eth.blockscout.com',
+ icon: 'blockscout',
+ standard: 'EIP3091',
+ },
+ {
+ name: 'dexguru',
+ url: 'https://ethereum.dex.guru',
+ icon: 'dexguru',
+ standard: 'EIP3091',
+ },
+ ],
+ },
+ ],
+ },
+};
+
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(component).toMatchSnapshot();
+ });
+
+ it('should render the component correctly when isNetworkUiRedesignEnabled is true', () => {
+ (networkUtils.isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(
+ () => true,
+ );
+
+ const component = shallow(
+
+
+ ,
+ );
+
+ expect(component).toMatchSnapshot();
+ expect(networkUtils.isNetworkUiRedesignEnabled()).toBe(true);
+ });
+
+ it('should render the component correctly when isNetworkUiRedesignEnabled is false', () => {
+ (networkUtils.isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(
+ () => false,
+ );
+
+ const component = shallow(
,
);
- expect(wrapper).toMatchSnapshot();
+
+ expect(component).toMatchSnapshot();
+ expect(networkUtils.isNetworkUiRedesignEnabled()).toBe(false);
+ });
+
+ 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: { network: 'mainnet' },
+ },
+ navigation: {
+ setOptions: jest.fn(),
+ navigate: jest.fn(),
+ goBack: jest.fn(),
+ },
+ networkConfigurations: {
+ '0x1': {
+ chainId: '0x1',
+ rpcUrl: 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID',
+ nickname: 'Ethereum mainnet',
+ rpcPrefs: {
+ blockExplorerUrl: 'https://etherscan.io',
+ },
+ ticker: 'ETH',
+ },
+ },
+ };
+
+ const wrapper2 = shallow(
+
+
+ ,
+ )
+ .find(NetworkSettings)
+ .dive();
+
+ const instance = wrapper2.instance() as NetworkSettings;
+ instance.componentDidMount();
+
+ expect(wrapper2.state('blockExplorerUrl')).toBe('https://etherscan.io');
+ expect(wrapper2.state('nickname')).toBe('Ethereum Main Network');
+ expect(wrapper2.state('chainId')).toBe('0x1');
+ expect(wrapper2.state('rpcUrl')).toBe('https://mainnet.infura.io/v3/');
+ });
+
+ it('should initialize state correctly when networkTypeOrRpcUrl is provided and isCustomMainnet is true', () => {
+ const SAMPLE_NETWORKSETTINGS_PROPS_2 = {
+ route: {
+ params: { network: 'mainnet', isCustomMainnet: true },
+ },
+ navigation: {
+ setOptions: jest.fn(),
+ navigate: jest.fn(),
+ goBack: jest.fn(),
+ },
+ networkConfigurations: {
+ '0x1': {
+ chainId: '0x1',
+ rpcUrl: 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID',
+ nickname: 'Ethereum mainnet',
+ rpcPrefs: {
+ blockExplorerUrl: 'https://etherscan.io',
+ },
+ ticker: 'ETH',
+ },
+ },
+ };
+
+ const wrapperComponent = shallow(
+
+
+ ,
+ )
+ .find(NetworkSettings)
+ .dive();
+
+ const instance = wrapperComponent.instance() as NetworkSettings;
+ instance.componentDidMount();
+
+ expect(wrapperComponent.state('blockExplorerUrl')).toBe(
+ 'https://etherscan.io',
+ );
+ expect(wrapperComponent.state('nickname')).toBe('Ethereum Main Custom');
+ expect(wrapperComponent.state('chainId')).toBe('0x1');
+ expect(wrapperComponent.state('rpcUrl')).toBe(
+ 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID',
+ );
+ });
+
+ it('should initialize state correctly when networkTypeOrRpcUrl is provided and allNetworks is not found', () => {
+ const SAMPLE_NETWORKSETTINGS_PROPS_2 = {
+ route: {
+ params: { network: 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID' },
+ },
+ navigation: {
+ setOptions: jest.fn(),
+ navigate: jest.fn(),
+ goBack: jest.fn(),
+ },
+ networkConfigurations: {
+ '0x1': {
+ chainId: '0x1',
+ rpcUrl: 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID',
+ nickname: 'Ethereum mainnet',
+ rpcPrefs: {
+ blockExplorerUrl: 'https://etherscan.io',
+ },
+ ticker: 'ETH',
+ },
+ },
+ };
+
+ const wrapper2 = shallow(
+
+
+ ,
+ )
+ .find(NetworkSettings)
+ .dive();
+
+ const instance = wrapper2.instance() as NetworkSettings;
+ instance.componentDidMount();
+
+ expect(wrapper2.state('blockExplorerUrl')).toBe('https://etherscan.io');
+ expect(wrapper2.state('nickname')).toBe('Ethereum mainnet');
+ expect(wrapper2.state('chainId')).toBe('0x1');
+ expect(wrapper2.state('rpcUrl')).toBe(
+ 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID',
+ );
+ });
+
+ 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();
+ });
+
+ it('should update isNameFieldFocused state on name input focus and blur', () => {
+ const instance = wrapper.instance();
+
+ instance.onNameFocused();
+ expect(wrapper.state('isNameFieldFocused')).toBe(true);
+
+ instance.onNameBlur();
+ expect(wrapper.state('isNameFieldFocused')).toBe(false);
+ });
+
+ it('should update isSymbolFieldFocused state on symbol input focus and blur', () => {
+ const instance = wrapper.instance();
+
+ instance.onSymbolFocused();
+ expect(wrapper.state('isSymbolFieldFocused')).toBe(true);
+
+ instance.onSymbolBlur();
+ expect(wrapper.state('isSymbolFieldFocused')).toBe(false);
+ });
+
+ it('should update isRpcUrlFieldFocused state on RPC URL input focus and blur', () => {
+ const instance = wrapper.instance();
+
+ instance.onRpcUrlFocused();
+ expect(wrapper.state('isRpcUrlFieldFocused')).toBe(true);
+
+ instance.onRpcUrlBlur();
+ expect(wrapper.state('isRpcUrlFieldFocused')).toBe(false);
+ });
+
+ it('should update isChainIdFieldFocused state on chain ID input focus and blur', () => {
+ const instance = wrapper.instance();
+
+ instance.onChainIdFocused();
+ expect(wrapper.state('isChainIdFieldFocused')).toBe(true);
+
+ instance.onChainIdBlur();
+ expect(wrapper.state('isChainIdFieldFocused')).toBe(false);
+ });
+
+ describe('getDecimalChainId', () => {
+ let wrapperTest;
+ // Do not need to mock entire Engine. Only need subset of data for testing purposes.
+ // TODO: Replace "any" with type
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let instanceTest: any;
+
+ beforeEach(() => {
+ wrapperTest = shallow(
+
+
+
+
+ ,
+ )
+ .find(NetworkSettings)
+ .dive();
+
+ instanceTest = wrapperTest.instance();
+ });
+
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return the chainId as is if it is falsy', () => {
+ expect(instanceTest.getDecimalChainId(null)).toBe(null);
+ expect(instanceTest.getDecimalChainId(undefined)).toBe(undefined);
+ });
+
+ it('should return the chainId as is if it is not a string', () => {
+ expect(instanceTest.getDecimalChainId(123)).toBe(123);
+ });
+
+ it('should return the chainId as is if it does not start with 0x', () => {
+ expect(instanceTest.getDecimalChainId('123')).toBe('123');
+ expect(instanceTest.getDecimalChainId('abc')).toBe('abc');
+ });
+
+ it('should convert hex chainId to decimal string', () => {
+ expect(instanceTest.getDecimalChainId('0x1')).toBe('1');
+ expect(instanceTest.getDecimalChainId('0xa')).toBe('10');
+ expect(instanceTest.getDecimalChainId('0x64')).toBe('100');
+ expect(instanceTest.getDecimalChainId('0x12c')).toBe('300');
+ });
+
+ it('should handle edge cases for hex chainId conversion', () => {
+ expect(instanceTest.getDecimalChainId('0x0')).toBe('0');
+ expect(instanceTest.getDecimalChainId('0xff')).toBe('255');
+ expect(instanceTest.getDecimalChainId('0x7fffffffffffffff')).toBe(
+ '9223372036854776000',
+ );
+ });
});
});
diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/styles.ts b/app/components/Views/Settings/NetworksSettings/NetworkSettings/styles.ts
index 1e978a7ab38..d6606f0ca3d 100644
--- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/styles.ts
+++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/styles.ts
@@ -1,5 +1,5 @@
+import isNetworkUiRedesignEnabled from '../../../../../util/networks/isNetworkUiRedesignEnabled';
import { StyleSheet } from 'react-native';
-import { isNetworkUiRedesignEnabled } from '../../../../../util/networks';
const createStyles = () =>
StyleSheet.create({
@@ -12,7 +12,7 @@ const createStyles = () =>
popularNetworkImage: {
width: 20,
height: 20,
- marginRight: isNetworkUiRedesignEnabled ? 20 : 10,
+ marginRight: isNetworkUiRedesignEnabled() ? 20 : 10,
borderRadius: 10,
},
popularWrapper: {
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/app/util/networks/index.js b/app/util/networks/index.js
index ab4ef0c010f..070dfbe48f8 100644
--- a/app/util/networks/index.js
+++ b/app/util/networks/index.js
@@ -573,8 +573,5 @@ export const deprecatedGetNetworkId = async () => {
});
};
-export const isNetworkUiRedesignEnabled =
- process.env.MM_NETWORK_UI_REDESIGN_ENABLED === '1';
-
export const isMutichainVersion1Enabled =
process.env.MM_MULTICHAIN_V1_ENABLED === '1';
diff --git a/app/util/networks/isNetworkUiRedesignEnabled.ts b/app/util/networks/isNetworkUiRedesignEnabled.ts
new file mode 100644
index 00000000000..e6d1667314d
--- /dev/null
+++ b/app/util/networks/isNetworkUiRedesignEnabled.ts
@@ -0,0 +1,4 @@
+export const isNetworkUiRedesignEnabled = () =>
+ process.env.MM_NETWORK_UI_REDESIGN_ENABLED === '1';
+
+export default isNetworkUiRedesignEnabled;
diff --git a/locales/languages/en.json b/locales/languages/en.json
index a86a6b93add..c9b053605e3 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,13 @@
"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",
+ "network_already_exist": "You already have a network with the same chain ID or RPC URL. Enter a new chain ID or RPC URL",
+ "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",