diff --git a/app/components/UI/ReusableModal/ReusableModal.types.ts b/app/components/UI/ReusableModal/ReusableModal.types.ts index b3fbc1503a6..24bb9491eef 100644 --- a/app/components/UI/ReusableModal/ReusableModal.types.ts +++ b/app/components/UI/ReusableModal/ReusableModal.types.ts @@ -17,6 +17,14 @@ export interface ReusableModalProps extends ViewProps { * @default true */ isInteractable?: boolean; + + /** + * Determines whether the navigation should revert to the previous path when the modal is closed. + * If set to `true`, closing the modal will trigger the navigation to go back to the previous screen or route. + * If set to `false`, the navigation will remain on the current path when the modal is closed. + * @default true + */ + shouldGoBack?: boolean; } export type ReusableModalPostCallback = () => void; diff --git a/app/components/UI/ReusableModal/index.tsx b/app/components/UI/ReusableModal/index.tsx index 8399975aa04..9033c018cd1 100644 --- a/app/components/UI/ReusableModal/index.tsx +++ b/app/components/UI/ReusableModal/index.tsx @@ -47,7 +47,17 @@ import { export type { ReusableModalRef } from './ReusableModal.types'; const ReusableModal = forwardRef( - ({ children, onDismiss, isInteractable = true, style, ...props }, ref) => { + ( + { + children, + onDismiss, + isInteractable = true, + shouldGoBack = true, + style, + ...props + }, + ref, + ) => { const postCallback = useRef(); const { height: screenHeight } = useWindowDimensions(); const { styles } = useStyles(styleSheet, {}); @@ -66,10 +76,12 @@ const ReusableModal = forwardRef( const onHidden = useCallback(() => { // Sheet is automatically unmounted from the navigation stack. - navigation.goBack(); + if (shouldGoBack) { + navigation.goBack(); + } onDismiss?.(!!postCallback.current); postCallback.current?.(); - }, [navigation, onDismiss]); + }, [navigation, onDismiss, shouldGoBack]); const gestureHandler = useAnimatedGestureHandler< PanGestureHandlerGestureEvent, diff --git a/app/components/Views/NetworkSelector/NetworkSelector.test.tsx b/app/components/Views/NetworkSelector/NetworkSelector.test.tsx index 6dacf921a1a..e7985867b75 100644 --- a/app/components/Views/NetworkSelector/NetworkSelector.test.tsx +++ b/app/components/Views/NetworkSelector/NetworkSelector.test.tsx @@ -185,6 +185,20 @@ describe('Network Selector', () => { expect(toJSON()).toMatchSnapshot(); }); + it('renders correctly when network UI redesign is enabled', () => { + (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true); + const { toJSON } = renderComponent(initialState); + expect(toJSON()).toMatchSnapshot(); + }); + + it('shows popular networks when UI redesign is enabled', () => { + (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true); + const { getByText } = renderComponent(initialState); + + const popularNetworksTitle = getByText('Additional networks'); + expect(popularNetworksTitle).toBeTruthy(); + }); + it('changes network when another network cell is pressed', async () => { (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => false); const { getByText } = renderComponent(initialState); @@ -387,4 +401,62 @@ describe('Network Selector', () => { fireEvent.press(rpcOption); }); }); + + it('filters networks correctly when searching', () => { + (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true); + const { getByPlaceholderText, queryByText } = renderComponent(initialState); + + const searchInput = getByPlaceholderText('Search'); + + // Simulate entering a search term + fireEvent.changeText(searchInput, 'Polygon'); + + // Polygon should appear, but others should not + expect(queryByText('Polygon Mainnet')).toBeTruthy(); + expect(queryByText('Avalanche Mainnet C-Chain')).toBeNull(); + + // Clear search and check if all networks appear + fireEvent.changeText(searchInput, ''); + expect(queryByText('Polygon Mainnet')).toBeTruthy(); + expect(queryByText('Avalanche Mainnet C-Chain')).toBeTruthy(); + }); + + it('shows popular networks when network UI redesign is enabled', () => { + (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true); + const { getByText } = renderComponent(initialState); + + // Check that the additional networks section is rendered + const popularNetworksTitle = getByText('Additional networks'); + expect(popularNetworksTitle).toBeTruthy(); + }); + + it('opens the multi-RPC selection modal correctly', async () => { + (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true); + const { getByText } = renderComponent(initialState); + + const polygonCell = getByText('Polygon Mainnet'); + + // Open the modal + fireEvent.press(polygonCell); + await waitFor(() => { + const rpcOption = getByText('polygon-mainnet.infura.io/v3'); + expect(rpcOption).toBeTruthy(); + }); + }); + + it('toggles test networks visibility when switch is used', () => { + (isNetworkUiRedesignEnabled as jest.Mock).mockImplementation(() => true); + const { getByTestId } = renderComponent(initialState); + const testNetworksSwitch = getByTestId( + NetworkListModalSelectorsIDs.TEST_NET_TOGGLE, + ); + + // Toggle the switch on + fireEvent(testNetworksSwitch, 'onValueChange', true); + expect(setShowTestNetworksSpy).toBeCalledWith(true); + + // Toggle the switch off + fireEvent(testNetworksSwitch, 'onValueChange', false); + expect(setShowTestNetworksSpy).toBeCalledWith(false); + }); }); diff --git a/app/components/Views/NetworkSelector/NetworkSelector.tsx b/app/components/Views/NetworkSelector/NetworkSelector.tsx index 8dbd03768f6..2ae1b3d0f36 100644 --- a/app/components/Views/NetworkSelector/NetworkSelector.tsx +++ b/app/components/Views/NetworkSelector/NetworkSelector.tsx @@ -86,13 +86,13 @@ import BottomSheetFooter from '../../../component-library/components/BottomSheet import { ExtendedNetwork } from '../Settings/NetworksSettings/NetworkSettings/CustomNetworkView/CustomNetwork.types'; import { isNetworkUiRedesignEnabled } from '../../../util/networks/isNetworkUiRedesignEnabled'; import { Hex } from '@metamask/utils'; -import ListItemSelect from '../../../component-library/components/List/ListItemSelect'; import hideProtocolFromUrl from '../../../util/hideProtocolFromUrl'; import { CHAIN_IDS } from '@metamask/transaction-controller'; import { LINEA_DEFAULT_RPC_URL } from '../../../constants/urls'; import { useNetworkInfo } from '../../../selectors/selectedNetworkController'; import { NetworkConfiguration } from '@metamask/network-controller'; import Logger from '../../../util/Logger'; +import RpcSelectionModal from './RpcSelectionModal/RpcSelectionModal'; interface infuraNetwork { name: string; @@ -159,9 +159,15 @@ const NetworkSelector = () => { networkName: '', }); - const [showNetworkMenuModal, setNetworkMenuModal] = useState({ + const [showNetworkMenuModal, setNetworkMenuModal] = useState<{ + isVisible: boolean; + chainId: `0x${string}`; + displayEdit: boolean; + networkTypeOrRpcUrl: string; + isReadOnly: boolean; + }>({ isVisible: false, - chainId: '', + chainId: '0x1', displayEdit: false, networkTypeOrRpcUrl: '', isReadOnly: false, @@ -200,8 +206,15 @@ const NetworkSelector = () => { // Set the active network NetworkController.setActiveNetwork(clientId); + // Redirect to wallet page + navigate(Routes.WALLET.HOME, { + screen: Routes.WALLET.TAB_STACK_FLOW, + params: { + screen: Routes.WALLET_VIEW, + }, + }); }, - [networkConfigurations], + [networkConfigurations, navigate], ); const [showMultiRpcSelectModal, setShowMultiRpcSelectModal] = useState<{ @@ -220,54 +233,6 @@ const NetworkSelector = () => { const deleteModalSheetRef = useRef(null); - // The only possible value types are mainnet, linea-mainnet, sepolia and linea-sepolia - const onNetworkChange = (type: InfuraNetworkType) => { - const { - NetworkController, - CurrencyRateController, - AccountTrackerController, - SelectedNetworkController, - } = Engine.context; - - if (domainIsConnectedDapp && process.env.MULTICHAIN_V1) { - SelectedNetworkController.setNetworkClientIdForDomain(origin, type); - } else { - let ticker = type; - if (type === LINEA_SEPOLIA) { - ticker = TESTNET_TICKER_SYMBOLS.LINEA_SEPOLIA as InfuraNetworkType; - } - if (type === SEPOLIA) { - ticker = TESTNET_TICKER_SYMBOLS.SEPOLIA as InfuraNetworkType; - } - - const networkConfiguration = - NetworkController.getNetworkConfigurationByChainId( - BUILT_IN_NETWORKS[type].chainId, - ); - - const clientId = - networkConfiguration?.rpcEndpoints[ - networkConfiguration.defaultRpcEndpointIndex - ].networkClientId ?? type; - - CurrencyRateController.updateExchangeRate(ticker); - NetworkController.setActiveNetwork(clientId); - AccountTrackerController.refresh(); - - setTimeout(async () => { - await updateIncomingTransactions(); - }, 1000); - } - - sheetRef.current?.onCloseBottomSheet(); - - trackEvent(MetaMetricsEvents.NETWORK_SWITCHED, { - chain_id: getDecimalChainId(selectedChainId), - from_network: selectedNetworkName, - to_network: type, - }); - }; - const onSetRpcTarget = async (networkConfiguration: NetworkConfiguration) => { const { CurrencyRateController, @@ -343,7 +308,7 @@ const NetworkSelector = () => { const closeModal = useCallback(() => { setNetworkMenuModal(() => ({ - chainId: '', + chainId: '0x1', isVisible: false, displayEdit: false, networkTypeOrRpcUrl: '', @@ -384,6 +349,55 @@ const NetworkSelector = () => { Linking.openURL(strings('networks.learn_more_url')); }; + // The only possible value types are mainnet, linea-mainnet, sepolia and linea-sepolia + const onNetworkChange = (type: InfuraNetworkType) => { + const { + NetworkController, + CurrencyRateController, + AccountTrackerController, + SelectedNetworkController, + } = Engine.context; + + if (domainIsConnectedDapp && process.env.MULTICHAIN_V1) { + SelectedNetworkController.setNetworkClientIdForDomain(origin, type); + } else { + let ticker = type; + if (type === LINEA_SEPOLIA) { + ticker = TESTNET_TICKER_SYMBOLS.LINEA_SEPOLIA as InfuraNetworkType; + } + if (type === SEPOLIA) { + ticker = TESTNET_TICKER_SYMBOLS.SEPOLIA as InfuraNetworkType; + } + + const networkConfiguration = + NetworkController.getNetworkConfigurationByChainId( + BUILT_IN_NETWORKS[type].chainId, + ); + + const clientId = + networkConfiguration?.rpcEndpoints[ + networkConfiguration.defaultRpcEndpointIndex + ].networkClientId ?? type; + + CurrencyRateController.updateExchangeRate(ticker); + NetworkController.setActiveNetwork(clientId); + closeRpcModal(); + AccountTrackerController.refresh(); + + setTimeout(async () => { + await updateIncomingTransactions(); + }, 1000); + } + + sheetRef.current?.onCloseBottomSheet(); + + trackEvent(MetaMetricsEvents.NETWORK_SWITCHED, { + chain_id: getDecimalChainId(selectedChainId), + from_network: selectedNetworkName, + to_network: type, + }); + }; + const filterNetworksByName = ( networks: ExtendedNetwork[], networkName: string, @@ -632,9 +646,7 @@ const NetworkSelector = () => { >; const { name, imageSource, chainId } = TypedNetworks[networkType]; - const networkConfiguration = Object.values(networkConfigurations).find( - ({ chainId: networkId }) => networkId === chainId, - ); + const networkConfiguration = networkConfigurations[chainId]; const rpcUrl = networkConfiguration?.rpcEndpoints?.[ @@ -794,10 +806,8 @@ const NetworkSelector = () => { setSearchString(''); }; - const removeRpcUrl = (chainId: string) => { - const networkConfiguration = Object.values(networkConfigurations).find( - (config) => config.chainId === chainId, - ); + const removeRpcUrl = (chainId: `0x${string}`) => { + const networkConfiguration = networkConfigurations[chainId]; if (!networkConfiguration) { throw new Error(`Unable to find network with chain id ${chainId}`); @@ -840,91 +850,6 @@ const NetworkSelector = () => { onPress: () => confirmRemoveRpc(), }; - const renderBottomSheetRpc = useCallback(() => { - let imageSource; - - if (showMultiRpcSelectModal.chainId === CHAIN_IDS.MAINNET) { - imageSource = images.ETHEREUM; - } else if (showMultiRpcSelectModal.chainId === CHAIN_IDS.LINEA_MAINNET) { - imageSource = images['LINEA-MAINNET']; - } else { - //@ts-expect-error - The utils/network file is still JS and this function expects a networkType, and should be optional - imageSource = getNetworkImageSource({ - chainId: showMultiRpcSelectModal?.chainId?.toString(), - }); - } - - if (!showMultiRpcSelectModal.isVisible) return null; - - const chainId = showMultiRpcSelectModal.chainId; - - const rpcEndpoints = - networkConfigurations[chainId as `0x${string}`]?.rpcEndpoints || []; - - return ( - - - - {strings('app_settings.select_rpc_url')}{' '} - - - - {showMultiRpcSelectModal.networkName} - - - - - {rpcEndpoints.map(({ url, networkClientId }, index) => ( - { - onRpcSelect(networkClientId, chainId as `0x${string}`); - closeRpcModal(); - }} - > - - - {hideKeyFromUrl(hideProtocolFromUrl(url))} - - - - ))} - - - ); - }, [ - showMultiRpcSelectModal, - rpcMenuSheetRef, - closeRpcModal, - styles, - networkConfigurations, - onRpcSelect, - ]); - const renderBottomSheetContent = () => ( <> @@ -1020,14 +945,21 @@ const NetworkSelector = () => { actionTitle={strings('app_settings.delete')} iconName={IconName.Trash} onPress={() => removeRpcUrl(showNetworkMenuModal.chainId)} - testID={`delete-network-button-${showNetworkMenuModal.chainId}`} + testID={NetworkListModalSelectorsIDs.DELETE_NETWORK} /> ) : null} ) : null} - {renderBottomSheetRpc()} + {showConfirmDeleteModal.isVisible ? ( ({ + ...jest.requireActual('react-redux'), + useSelector: (fn: (state: unknown) => unknown) => fn(MOCK_STORE_STATE), +})); + +jest.mock('react-native-safe-area-context', () => { + // using disting digits for mock rects to make sure they are not mixed up + const inset = { top: 1, right: 2, bottom: 3, left: 4 }; + const frame = { width: 5, height: 6, x: 7, y: 8 }; + return { + SafeAreaProvider: jest.fn().mockImplementation(({ children }) => children), + SafeAreaConsumer: jest + .fn() + .mockImplementation(({ children }) => children(inset)), + useSafeAreaInsets: jest.fn().mockImplementation(() => inset), + useSafeAreaFrame: jest.fn().mockImplementation(() => frame), + }; +}); + +jest.mock('@react-navigation/native', () => { + const actualReactNavigation = jest.requireActual('@react-navigation/native'); + return { + ...actualReactNavigation, + useNavigation: () => ({ + navigate: jest.fn(), + setOptions: jest.fn(), + goBack: jest.fn(), + reset: jest.fn(), + }), + }; +}); + +describe('RpcSelectionModal', () => { + // Fully mock rpcMenuSheetRef to match the BottomSheetRef type + const mockRpcMenuSheetRef = { + current: { + onOpenBottomSheet: jest.fn(), + onCloseBottomSheet: jest.fn(), + }, + }; + + const defaultProps = { + showMultiRpcSelectModal: { + isVisible: true, + chainId: CHAIN_IDS.MAINNET, + networkName: 'Mainnet', + }, + closeRpcModal: jest.fn(), + onRpcSelect: jest.fn(), + rpcMenuSheetRef: mockRpcMenuSheetRef, + networkConfigurations: MOCK_STORE_STATE.engine.backgroundState + .NetworkController.networkConfigurations as unknown as Record< + string, + NetworkConfiguration + >, + + styles: { + baseHeader: {}, + cellBorder: {}, + rpcMenu: {}, + rpcText: {}, + textCentred: {}, + alternativeText: {}, + }, + }; + + it('should render correctly', () => { + const { toJSON } = renderWithProvider( + , + ); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/app/components/Views/NetworkSelector/RpcSelectionModal/RpcSelectionModal.tsx b/app/components/Views/NetworkSelector/RpcSelectionModal/RpcSelectionModal.tsx new file mode 100644 index 00000000000..2db335337ba --- /dev/null +++ b/app/components/Views/NetworkSelector/RpcSelectionModal/RpcSelectionModal.tsx @@ -0,0 +1,144 @@ +import React, { FC, useCallback } from 'react'; +import { View } from 'react-native'; +import BottomSheet, { + BottomSheetRef, +} from '../../../../component-library/components/BottomSheets/BottomSheet'; +import BottomSheetHeader from '../../../../component-library/components/BottomSheets/BottomSheetHeader'; +import Text from '../../../../component-library/components/Texts/Text/Text'; +import Cell, { + CellVariant, +} from '../../../../component-library/components/Cells/Cell'; +import ListItemSelect from '../../../../component-library/components/List/ListItemSelect'; +import { + AvatarSize, + AvatarVariant, +} from '../../../../component-library/components/Avatars/Avatar'; +import { TextVariant } from '../../../../component-library/components/Texts/Text'; +import Networks, { getNetworkImageSource } from '../../../../util/networks'; +import { strings } from '../../../../../locales/i18n'; +import { CHAIN_IDS } from '@metamask/transaction-controller'; +import images from 'images/image-icons'; +import hideProtocolFromUrl from '../../../../util/hideProtocolFromUrl'; +import hideKeyFromUrl from '../../../..//util/hideKeyFromUrl'; +import { NetworkConfiguration } from '@metamask/network-controller'; + +interface RpcSelectionModalProps { + showMultiRpcSelectModal: { + isVisible: boolean; + chainId: string; + networkName: string; + }; + closeRpcModal: () => void; + onRpcSelect: (networkClientId: string, chainId: `0x${string}`) => void; + rpcMenuSheetRef: React.RefObject; + networkConfigurations: Record; + // TODO: Replace "any" with type + // eslint-disable-next-line @typescript-eslint/no-explicit-any + styles: any; +} + +const RpcSelectionModal: FC = ({ + showMultiRpcSelectModal, + closeRpcModal, + onRpcSelect, + rpcMenuSheetRef, + networkConfigurations, + styles, +}) => { + const renderRpcSelection = useCallback(() => { + let imageSource; + + if (showMultiRpcSelectModal.chainId === CHAIN_IDS.MAINNET) { + imageSource = images.ETHEREUM; + } else if (showMultiRpcSelectModal.chainId === CHAIN_IDS.LINEA_MAINNET) { + imageSource = images['LINEA-MAINNET']; + } else { + //@ts-expect-error - The utils/network file is still JS and this function expects a networkType, and should be optional + imageSource = getNetworkImageSource({ + chainId: showMultiRpcSelectModal?.chainId?.toString(), + }); + } + + if (!showMultiRpcSelectModal.isVisible) return null; + + const chainId = showMultiRpcSelectModal.chainId; + + const rpcEndpoints = + networkConfigurations[chainId as `0x${string}`]?.rpcEndpoints || []; + + return ( + + + + {strings('app_settings.select_rpc_url')}{' '} + + + + {showMultiRpcSelectModal.networkName} + + + + + {rpcEndpoints.map( + ( + { + url, + networkClientId, + }: { url: string; networkClientId: string }, + index: number, + ) => ( + { + onRpcSelect(networkClientId, chainId as `0x${string}`); + closeRpcModal(); + }} + > + + + {hideKeyFromUrl(hideProtocolFromUrl(url))} + + + + ), + )} + + + ); + }, [ + showMultiRpcSelectModal, + rpcMenuSheetRef, + closeRpcModal, + styles, + networkConfigurations, + onRpcSelect, + ]); + + return renderRpcSelection(); +}; + +export default RpcSelectionModal; diff --git a/app/components/Views/NetworkSelector/RpcSelectionModal/__snapshots__/RpcSelectionModal.test.tsx.snap b/app/components/Views/NetworkSelector/RpcSelectionModal/__snapshots__/RpcSelectionModal.test.tsx.snap new file mode 100644 index 00000000000..57b5843993a --- /dev/null +++ b/app/components/Views/NetworkSelector/RpcSelectionModal/__snapshots__/RpcSelectionModal.test.tsx.snap @@ -0,0 +1,379 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RpcSelectionModal should render correctly 1`] = ` + + + + + + + + + + + + + + + + Select RPC URL + + + + + + + + + + Ethereum Main Network + + + + + Mainnet + + + + + + + + + + + + + + + + mainnet.infura.io/v3 + + + + + + + + + + + + +`; diff --git a/app/components/Views/NetworkSelector/RpcSelectionModal/index.tsx b/app/components/Views/NetworkSelector/RpcSelectionModal/index.tsx new file mode 100644 index 00000000000..f42c8e673b4 --- /dev/null +++ b/app/components/Views/NetworkSelector/RpcSelectionModal/index.tsx @@ -0,0 +1 @@ +export { default } from './RpcSelectionModal'; diff --git a/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap b/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap index 8f50b91b23f..5a3ad2dd41e 100644 --- a/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap +++ b/app/components/Views/NetworkSelector/__snapshots__/NetworkSelector.test.tsx.snap @@ -1211,3 +1211,2824 @@ exports[`Network Selector renders correctly 1`] = ` `; + +exports[`Network Selector renders correctly when network UI redesign is enabled 1`] = ` + + + + + + + + + + + + + NETWORK_SELECTOR + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Select a network + + + + + + + + +  + + + + + + + Enabled networks + + + + + + + + + + + + + Ethereum Main Network + + + + + + + + + + + + + + + + + + + + + + + + + + Linea Main Network + + + + https://linea-mainnet.infura.io/v3 + + + + + + + + + + + + + + + + + + + + + + + + + Avalanche Mainnet C-Chain + + + + api.avax.network/ext/bc/C + + + + + + + + + + + + + + + + + + + + + + + + + Polygon Mainnet + + + + polygon-mainnet.infura.io/v3 + + + + + + + + + + + + + + + + + + + + + + + + + Optimism + + + + optimism-mainnet.infura.io/v3 + + + + + + + + + + + + + + + + + + + + + + G + + + + + Gnosis Chain + + + + rpc.gnosischain.com + + + + + + + + + + + + + + + + + Additional networks + + + +  + + + + + + + + + + + + + Arbitrum One + + + + + Add + + + + + + + + + + + + BNB Chain + + + + + Add + + + + + + + + + + + + Base + + + + + Add + + + + + + + + + + + + Palm + + + + + Add + + + + + + + + + + + + zkSync Era Mainnet + + + + + Add + + + + + + + Show test networks + + + + + + + + Add a custom network + + + + + + + + + + + + + + + + +`; diff --git a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js index e2ee56493dc..d7923a58094 100644 --- a/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js +++ b/app/components/Views/Settings/NetworksSettings/NetworkSettings/index.js @@ -78,13 +78,15 @@ import { isNetworkUiRedesignEnabled } from '../../../../../util/networks/isNetwo import Cell, { CellVariant, } from '../../../../../component-library/components/Cells/Cell'; -import BottomSheet from '../../../../../component-library/components/BottomSheets/BottomSheet'; import BottomSheetHeader from '../../../../../component-library/components/BottomSheets/BottomSheetHeader'; import { TextVariant } from '../../../../../component-library/components/Texts/Text'; import ButtonLink from '../../../../../component-library/components/Buttons/Button/variants/ButtonLink'; import ButtonPrimary from '../../../../../component-library/components/Buttons/Button/variants/ButtonPrimary'; import { RpcEndpointType } from '@metamask/network-controller'; import { AvatarVariant } from '../../../../../component-library/components/Avatars/Avatar'; +import ReusableModal from '../../../../../components/UI/ReusableModal'; +import Device from '../../../../../util/device'; +import { ScrollView } from 'react-native-gesture-handler'; const createStyles = (colors) => StyleSheet.create({ @@ -92,16 +94,96 @@ const createStyles = (colors) => paddingHorizontal: 16, }, addRpcButton: { + position: 'absolute', alignSelf: 'center', }, - addRpcNameButton: { + screen: { + flex: 1, + paddingHorizontal: 24, + paddingVertical: 16, + justifyContent: 'space-between', + alignItems: 'center', + backgroundColor: colors.background.default, + }, + container: { + flex: 1, + }, + headerText: { + fontSize: 18, + fontWeight: 'bold', + }, + scrollViewContent: { + paddingBottom: 16, + }, + scrollableBox: { + height: 164, + marginVertical: 10, + justifyContent: 'center', + alignItems: 'center', alignSelf: 'center', + bottom: 64, + }, + footer: { + height: 60, + justifyContent: 'center', + alignItems: 'center', + position: 'absolute', + bottom: 16, + left: 0, + right: 0, + zIndex: 10, // Ensures it stays on top of other components + }, + content: { + justifyContent: 'center', paddingHorizontal: 16, - paddingVertical: 16, - width: '100%', + flexGrow: 1, + }, + addRpcNameButton: { + paddingTop: 32, + alignSelf: 'center', + }, + sheet: { + flexDirection: 'column', + bottom: 0, + top: Device.getDeviceHeight() * 0.5, + backgroundColor: colors.background.default, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + height: Device.getDeviceHeight() * 0.5, + }, + sheetSmall: { + position: 'absolute', + bottom: 0, + top: Device.getDeviceHeight() * 0.7, + backgroundColor: colors.background.default, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + height: Device.getDeviceHeight() * 0.3, + }, + sheetRpcForm: { + position: 'absolute', + bottom: 0, + top: Device.getDeviceHeight() * 0.3, + backgroundColor: colors.background.default, + borderTopLeftRadius: 10, + borderTopRightRadius: 10, + height: Device.getDeviceHeight() * 0.7, + }, + sheetContent: { + flex: 1, + flexShrink: 1, + }, + notch: { + width: 48, + height: 5, + borderRadius: 4, + backgroundColor: colors.border.default, + marginTop: 4, + alignSelf: 'center', }, rpcMenu: { paddingHorizontal: 16, + flex: 1, }, wrapper: { backgroundColor: colors.background.default, @@ -178,7 +260,6 @@ const createStyles = (colors) => }, heading: { fontSize: 16, - paddingVertical: 12, color: colors.text.default, ...fontStyles.bold, }, @@ -401,10 +482,6 @@ export class NetworkSettings extends PureComponent { inputChainId = React.createRef(); inputSymbol = React.createRef(); inputBlockExplorerURL = React.createRef(); - rpcAddMenuSheetRef = React.createRef(); - addBlockExplorerMenuSheetRef = React.createRef(); - rpcAddFormSheetRef = React.createRef(); - blockExplorerAddFormSheetRef = React.createRef(); getOtherNetworks = () => allNetworks.slice(1); @@ -657,8 +734,13 @@ export class NetworkSettings extends PureComponent { checkIfChainIdExists = async (chainId) => { const { networkConfigurations } = this.props; - // Convert the chainId to hex format - const hexChainId = toHex(chainId); + let hexChainId; + try { + // Convert the chainId to hex format + hexChainId = toHex(chainId); + } catch (error) { + hexChainId = null; + } // Check if any network configuration matches the given chainId const chainIdExists = Object.values(networkConfigurations).some( @@ -1340,22 +1422,18 @@ export class NetworkSettings extends PureComponent { openAddRpcForm = () => { this.setState({ showAddRpcForm: { isVisible: true } }); - this.rpcAddFormSheetRef.current?.onOpenBottomSheet(); }; closeAddRpcForm = () => { this.setState({ showAddRpcForm: { isVisible: false } }); - this.rpcAddFormSheetRef.current?.onCloseBottomSheet(); }; openAddBlockExplorerForm = () => { this.setState({ showAddBlockExplorerForm: { isVisible: true } }); - this.blockExplorerAddFormSheetRef.current?.onOpenBottomSheet(); }; closeAddBlockExplorerRpcForm = () => { this.setState({ showAddBlockExplorerForm: { isVisible: false } }); - this.blockExplorerAddFormSheetRef.current?.onCloseBottomSheet(); }; closeRpcModal = () => { @@ -1364,22 +1442,18 @@ export class NetworkSettings extends PureComponent { rpcUrlForm: '', rpcNameForm: '', }); - this.rpcAddMenuSheetRef.current?.onCloseBottomSheet(); }; openRpcModal = () => { this.setState({ showMultiRpcAddModal: { isVisible: true } }); - this.rpcAddMenuSheetRef.current?.onOpenBottomSheet(); }; openBlockExplorerModal = () => { this.setState({ showMultiBlockExplorerAddModal: { isVisible: true } }); - this.addBlockExplorerMenuSheetRef.current?.onOpenBottomSheet(); }; closeBlockExplorerModal = () => { this.setState({ showMultiBlockExplorerAddModal: { isVisible: false } }); - this.addBlockExplorerMenuSheetRef.current?.onCloseBottomSheet(); }; switchToMainnet = () => { @@ -1448,7 +1522,7 @@ export class NetworkSettings extends PureComponent { }); }; - customNetwork = (networkTypeOrRpcUrl) => { + customNetwork = () => { const { rpcUrl, rpcUrls, @@ -1743,17 +1817,7 @@ export class NetworkSettings extends PureComponent { style={styles.wrapper} testID={NetworksViewSelectorsIDs.CONTAINER} > - { - if (this.isAnyModalVisible()) { - this.closeAddBlockExplorerRpcForm(); - this.closeAddRpcForm(); - this.closeBlockExplorerModal(); - this.closeRpcModal(); - } - }} - > + { this.validateChainId(); @@ -1901,6 +1967,7 @@ export class NetworkSettings extends PureComponent { autoCapitalize={'none'} autoCorrect={false} value={ticker} + editable={!this.isAnyModalVisible()} onChangeText={this.onTickerChange} onBlur={() => { this.validateSymbol(); @@ -1974,259 +2041,291 @@ export class NetworkSettings extends PureComponent { {isNetworkUiRedesignEnabled() && showAddRpcForm.isVisible ? ( - - { - this.closeAddRpcForm(); - this.openRpcModal(); - }} - > - - {strings('app_settings.add_rpc_url')} - - - - - {strings('app_settings.network_rpc_url_label')} - - - {warningRpcUrl && ( - - {warningRpcUrl} - - )} - - {strings('app_settings.network_rpc_name_label')} - - - - { - this.onRpcItemAdd(rpcUrlForm, rpcNameForm); - }} - width={ButtonWidthTypes.Full} - labelTextVariant={TextVariant.DisplayMD} - isDisabled={!!warningRpcUrl} - testID={NetworksViewSelectorsIDs.ADD_RPC_BUTTON} + + + { + this.closeAddRpcForm(); + this.openRpcModal(); + }} + > + + {strings('app_settings.add_rpc_url')} + + + + + + {strings('app_settings.network_rpc_url_label')} + + - + {warningRpcUrl && ( + + {warningRpcUrl} + + )} + + {strings('app_settings.network_rpc_name_label')} + + + + { + this.onRpcItemAdd(rpcUrlForm, rpcNameForm); + }} + width={ButtonWidthTypes.Auto} + labelTextVariant={TextVariant.DisplayMD} + isDisabled={!!warningRpcUrl} + testID={NetworksViewSelectorsIDs.ADD_RPC_BUTTON} + /> + + - + ) : null} {isNetworkUiRedesignEnabled() && showAddBlockExplorerForm.isVisible ? ( - - { - this.closeAddBlockExplorerRpcForm(); - this.openBlockExplorerModal(); - }} - > - - {strings('app_settings.add_block_explorer_url')} - - - - - {strings('app_settings.network_block_explorer_label')} - - + + + { + this.closeAddBlockExplorerRpcForm(); + this.openBlockExplorerModal(); + }} + > + + {strings('app_settings.add_block_explorer_url')} + + + + + {strings('app_settings.network_block_explorer_label')} + + + {blockExplorerUrl && !isUrl(blockExplorerUrl) && ( + + + {strings('app_settings.invalid_block_explorer_url')} + + )} - testID={NetworksViewSelectorsIDs.BLOCK_EXPLORER_INPUT} - placeholderTextColor={colors.text.muted} - onSubmitEditing={this.toggleNetworkDetailsModal} - keyboardAppearance={themeAppearance} - /> - {blockExplorerUrl && !isUrl(blockExplorerUrl) && ( - - - {strings('app_settings.invalid_block_explorer_url')} - + + { + this.onBlockExplorerItemAdd(blockExplorerUrl); + }} + width={ButtonWidthTypes.Full} + labelTextVariant={TextVariant.DisplayMD} + isDisabled={!blockExplorerUrl || !isUrl(blockExplorerUrl)} + /> - )} - - { - this.onBlockExplorerItemAdd(blockExplorerUrl); - }} - width={ButtonWidthTypes.Full} - labelTextVariant={TextVariant.DisplayMD} - isDisabled={!blockExplorerUrl || !isUrl(blockExplorerUrl)} - /> - + - + ) : null} + {isNetworkUiRedesignEnabled() && showMultiBlockExplorerAddModal.isVisible ? ( - 0 ? styles.sheet : styles.sheetSmall + } + onDismiss={this.closeBlockExplorerModal} + shouldGoBack={false} > - - - {strings('app_settings.add_block_explorer_url')} - - - - {blockExplorerUrls.map((url) => ( - { - await this.onBlockExplorerUrlChange(url); - }} - showButtonIcon={blockExplorerUrl !== url} - buttonIcon={IconName.Trash} - buttonProps={{ - onButtonClick: () => { - this.onBlockExplorerUrlDelete(url); - }, - }} - avatarProps={{ - variant: AvatarVariant.Network, - }} - /> - ))} - - { - this.openAddBlockExplorerForm(); - this.closeBlockExplorerModal(); - }} - width={ButtonWidthTypes.Auto} - labelTextVariant={TextVariant.DisplayMD} - /> - + {/* Sticky Notch */} + + + {/* Sticky Header */} + + + {strings('app_settings.add_block_explorer_url')} + + + + {/* Scrollable Middle Content */} + + {blockExplorerUrls.length > 0 ? ( + + {blockExplorerUrls.map((url) => ( + { + await this.onBlockExplorerUrlChange(url); + this.closeBlockExplorerModal(); + }} + showButtonIcon={blockExplorerUrl !== url} + buttonIcon={IconName.Trash} + buttonProps={{ + onButtonClick: () => { + this.onBlockExplorerUrlDelete(url); + }, + }} + avatarProps={{ + variant: AvatarVariant.Network, + }} + /> + ))} + + ) : null} + + {/* Add Block Explorer Button */} + + { + this.openAddBlockExplorerForm(); + this.closeBlockExplorerModal(); + }} + width={ButtonWidthTypes.Auto} + labelTextVariant={TextVariant.DisplayMD} + /> + + - + ) : null} + {isNetworkUiRedesignEnabled() && showMultiRpcAddModal.isVisible ? ( - 0 ? styles.sheet : styles.sheetSmall} + onDismiss={this.closeRpcModal} + shouldGoBack={false} > - - - {strings('app_settings.add_rpc_url')} - - - - {rpcUrls.map(({ url, name, type }) => ( - { - await this.onRpcUrlChangeWithName(url, name, type); - this.closeRpcModal(); - }} - showButtonIcon={ - rpcUrl !== url && type !== RpcEndpointType.Infura - } - buttonIcon={IconName.Trash} - buttonProps={{ - onButtonClick: () => { - this.onRpcUrlDelete(url); - }, - }} - onTextClick={async () => { - await this.onRpcUrlChangeWithName(url, name, type); - this.closeRpcModal(); - }} - avatarProps={{ - variant: AvatarVariant.Token, - }} - /> - ))} - - { - this.openAddRpcForm(); - this.closeRpcModal(); - }} - width={ButtonWidthTypes.Auto} - labelTextVariant={TextVariant.DisplayMD} - testID={NetworksViewSelectorsIDs.ADD_RPC_BUTTON} - /> - + + + {/* Sticky Header */} + + + {strings('app_settings.add_rpc_url')} + + + + {/* Scrollable Middle Content */} + + {rpcUrls.length > 0 ? ( + + {rpcUrls.map(({ url, name, type }) => ( + { + await this.onRpcUrlChangeWithName(url, name, type); + this.closeRpcModal(); + }} + showButtonIcon={ + rpcUrl !== url && type !== RpcEndpointType.Infura + } + buttonIcon={IconName.Trash} + buttonProps={{ + onButtonClick: () => { + this.onRpcUrlDelete(url); + }, + }} + onTextClick={async () => { + await this.onRpcUrlChangeWithName(url, name, type); + this.closeRpcModal(); + }} + avatarProps={{ + variant: AvatarVariant.Token, + }} + /> + ))} + + ) : null} + + { + this.openAddRpcForm(); + this.closeRpcModal(); + }} + width={ButtonWidthTypes.Auto} + labelTextVariant={TextVariant.DisplayMD} + testID={NetworksViewSelectorsIDs.ADD_RPC_BUTTON} + /> + + - + ) : null} ); @@ -2302,7 +2401,7 @@ export class NetworkSettings extends PureComponent { {(isNetworkUiRedesignEnabled() && !shouldShowPopularNetworks) || networkTypeOrRpcUrl ? ( - this.customNetwork(networkTypeOrRpcUrl) + this.customNetwork() ) : ( ({ @@ -35,11 +36,36 @@ const initialState = { }, }; +jest.mock('../../../../../core/Engine', () => ({ + context: { + NetworkController: { + setProviderType: jest.fn(), + setActiveNetwork: jest.fn(), + getNetworkClientById: () => ({ + configuration: { + chainId: '0x1', + rpcUrl: 'https://mainnet.infura.io/v3', + ticker: 'ETH', + type: 'custom', + }, + }), + removeNetwork: jest.fn(), + updateNetwork: jest.fn(), + }, + CurrencyRateController: { + updateExchangeRate: jest.fn(), + }, + }, +})); + const store = mockStore(initialState); const SAMPLE_NETWORKSETTINGS_PROPS = { route: { params: {} }, - networkConfigurationsByChainId: { + providerConfig: { + rpcUrl: 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID', + }, + networkConfigurations: { '0x1': { blockExplorerUrls: ['https://etherscan.io'], chainId: '0x1', @@ -61,6 +87,7 @@ const SAMPLE_NETWORKSETTINGS_PROPS = { { name: 'Ethereum Mainnet', chain: 'ETH', + chainId: 1, icon: 'ethereum', rpc: [ 'https://mainnet.infura.io/v3/${INFURA_API_KEY}', @@ -97,7 +124,6 @@ const SAMPLE_NETWORKSETTINGS_PROPS = { }, infoURL: 'https://ethereum.org', shortName: 'eth', - chainId: 1, networkId: 1, slip44: 60, ens: { @@ -123,6 +149,17 @@ const SAMPLE_NETWORKSETTINGS_PROPS = { }, ], }, + { + name: 'Polygon', + chain: 'MATIC', + chainId: 137, + faucets: [], + nativeCurrency: { + name: 'Polygon', + symbol: 'MATIC', + decimals: 18, + }, + }, ], }, }; @@ -446,6 +483,15 @@ describe('NetworkSettings', () => { expect(wrapper.state('validatedChainId')).toBe(true); }); + it('should add RPC URL correctly to POL for polygon', async () => { + wrapper.setState({ rpcUrl: 'http://localhost:8545', chainId: '0x89' }); + + await wrapper.instance().validateRpcAndChainId(); + + expect(wrapper.state('validatedRpcURL')).toBe(true); + expect(wrapper.state('validatedChainId')).toBe(true); + }); + // here it('should update state and call getCurrentState on block explorer URL change', async () => { const newProps = { @@ -606,4 +652,630 @@ describe('NetworkSettings', () => { ); }); }); + + describe('NetworkSettings additional tests', () => { + beforeEach(() => { + wrapper = shallow( + + + , + ) + .find(NetworkSettings) + .dive(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should validate chain ID format and set warning if invalid', async () => { + const instance = wrapper.instance(); + + // Test with an invalid chainId format + await instance.onChainIDChange('invalidChainId'); + await instance.validateChainId(); + + expect(wrapper.state('warningChainId')).toBe( + "Invalid number. Enter a decimal or '0x'-prefixed hexadecimal number.", + ); + }); + + it('should validate chain ID correctly if valid', async () => { + const instance = wrapper.instance(); + + // Test with a valid chainId + await instance.onChainIDChange('0x1'); + await instance.validateChainId(); + + expect(wrapper.state('warningChainId')).toBe(undefined); + }); + + it('should toggle the modal for RPC form correctly', () => { + const instance = wrapper.instance(); + + instance.openAddRpcForm(); + expect(wrapper.state('showAddRpcForm').isVisible).toBe(true); + + instance.closeAddRpcForm(); + expect(wrapper.state('showAddRpcForm').isVisible).toBe(false); + }); + + it('should toggle the modal for Block Explorer form correctly', () => { + const instance = wrapper.instance(); + + instance.openAddBlockExplorerForm(); + expect(wrapper.state('showAddBlockExplorerForm').isVisible).toBe(true); + + instance.closeAddBlockExplorerRpcForm(); + expect(wrapper.state('showAddBlockExplorerForm').isVisible).toBe(false); + }); + + it('should validate RPC URL and set a warning if the format is invalid', async () => { + const instance = wrapper.instance(); + + // Test with an invalid RPC URL + await instance.onRpcUrlChange('invalidUrl'); + await instance.validateRpcUrl('invalidUrl'); + + expect(wrapper.state('warningRpcUrl')).toBe( + 'URIs require the appropriate HTTPS prefix', + ); + }); + + it('should not set warning for a valid RPC URL', async () => { + const instance = wrapper.instance(); + + // Test with a valid RPC URL + await instance.onRpcUrlChange( + 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID', + ); + await instance.validateRpcUrl( + 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID', + ); + + expect(wrapper.state('warningRpcUrl')).toBe(undefined); + }); + + it('should correctly add RPC URL through modal and update state', async () => { + const instance = wrapper.instance(); + + // Open RPC form modal and add a new RPC URL + instance.openAddRpcForm(); + await instance.onRpcItemAdd('https://new-rpc-url.com', 'New RPC'); + + expect(wrapper.state('rpcUrls').length).toBe(1); + expect(wrapper.state('rpcUrls')[0].url).toBe('https://new-rpc-url.com'); + expect(wrapper.state('rpcUrls')[0].name).toBe('New RPC'); + }); + + it('should correctly add Block Explorer URL through modal and update state', async () => { + const instance = wrapper.instance(); + + // Open Block Explorer form modal and add a new URL + instance.openAddBlockExplorerForm(); + await instance.onBlockExplorerItemAdd('https://new-blockexplorer.com'); + + expect(wrapper.state('blockExplorerUrls').length).toBe(1); + expect(wrapper.state('blockExplorerUrls')[0]).toBe( + 'https://new-blockexplorer.com', + ); + }); + + it('should call validateRpcAndChainId when chainId and rpcUrl are set', async () => { + const instance = wrapper.instance(); + const validateRpcAndChainIdSpy = jest.spyOn( + instance, + 'validateRpcAndChainId', + ); + + wrapper.setState({ + rpcUrl: 'http://localhost:8545', + chainId: '0x1', + }); + + await instance.validateRpcAndChainId(); + + expect(validateRpcAndChainIdSpy).toHaveBeenCalled(); + }); + + it('should correctly delete an RPC URL and update state', async () => { + const instance = wrapper.instance(); + + // Add and then delete an RPC URL + await instance.onRpcItemAdd('https://to-delete-url.com', 'RPC to delete'); + expect(wrapper.state('rpcUrls').length).toBe(1); + + await instance.onRpcUrlDelete('https://to-delete-url.com'); + expect(wrapper.state('rpcUrls').length).toBe(0); + }); + + it('should correctly delete a Block Explorer URL and update state', async () => { + const instance = wrapper.instance(); + + // Add and then delete a Block Explorer URL + await instance.onBlockExplorerItemAdd( + 'https://to-delete-blockexplorer.com', + ); + expect(wrapper.state('blockExplorerUrls').length).toBe(1); + + await instance.onBlockExplorerUrlDelete( + 'https://to-delete-blockexplorer.com', + ); + expect(wrapper.state('blockExplorerUrls').length).toBe(0); + }); + + it('should call the navigation method to go back when removeRpcUrl is called', () => { + const instance = wrapper.instance(); + wrapper.setState({ + rpcUrl: 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID', + }); + instance.removeRpcUrl(); + + expect(SAMPLE_NETWORKSETTINGS_PROPS.navigation.goBack).toHaveBeenCalled(); + }); + + it('should disable action button when form is incomplete', async () => { + const instance = wrapper.instance(); + + // Set incomplete form state + wrapper.setState({ + rpcUrl: '', + chainId: '', + nickname: '', + }); + + await instance.addRpcUrl(); + + // The action button should be disabled + expect(wrapper.state('enableAction')).toBe(false); + }); + + it('should enable action button when form is complete', async () => { + const instance = wrapper.instance(); + + // Set complete form state + wrapper.setState({ + rpcUrls: [ + { url: 'http://localhost:8545', type: 'custom', name: 'test' }, + ], + rpcUrl: 'http://localhost:8545', + chainId: '0x1', + nickname: 'Localhost', + ticker: 'ETH', + }); + + await instance.getCurrentState(); + + // The action button should be enabled + expect(wrapper.state('enableAction')).toBe(true); + }); + + it('should validateChainId and set appropriate error messages for invalid chainId formats', async () => { + const instance = wrapper.instance(); + + // Set an invalid chain ID + await instance.onChainIDChange('0xinvalid'); + await instance.validateChainId(); + + expect(wrapper.state('warningChainId')).toBe( + 'Invalid hexadecimal number.', + ); + }); + + it('should handle valid chainId conversion and updating state correctly', async () => { + const instance = wrapper.instance(); + + await instance.onChainIDChange('0x1'); + await instance.validateChainId(); + + expect(wrapper.state('warningChainId')).toBe(undefined); + }); + + it('should call getCurrentState when onNicknameChange is triggered', async () => { + const instance = wrapper.instance(); + const getCurrentStateSpy = jest.spyOn(instance, 'getCurrentState'); + + await instance.onNicknameChange('New Nickname'); + + expect(wrapper.state('nickname')).toBe('New Nickname'); + expect(getCurrentStateSpy).toHaveBeenCalled(); + }); + + it('should not call getCurrentState', async () => { + const instance = wrapper.instance(); + const getCurrentStateSpy = jest.spyOn(instance, 'getCurrentState'); + + await instance.onBlockExplorerItemAdd(''); + + expect(getCurrentStateSpy).not.toHaveBeenCalled(); + }); + + it('should set blockExplorerState', async () => { + const instance = wrapper.instance(); + const getCurrentStateSpy = jest.spyOn(instance, 'getCurrentState'); + + await instance.onBlockExplorerItemAdd('https://etherscan.io'); + + expect(wrapper.state('blockExplorerUrls').length).toBe(1); + expect(getCurrentStateSpy).toHaveBeenCalled(); + }); + + it('should not validate the symbol if useSafeChainsListValidation is false', async () => { + const instance = wrapper.instance(); + + const validSymbol = 'ETH'; + + await instance.validateSymbol(validSymbol); + + expect(instance.state.warningSymbol).toBeUndefined(); // No warning for valid symbol + }); + + it('should validateChainIdOnSubmit', async () => { + const instance = wrapper.instance(); + + const validChainId = '0x38'; + + await instance.validateChainIdOnSubmit( + validChainId, + validChainId, + 'https://bsc-dataseed.binance.org/', + ); + + expect(instance.state.warningChainId).toBeUndefined(); + }); + + it('should set a warning when chainId is not valid', async () => { + const instance = wrapper.instance(); + + const validChainId = '0xInvalidChainId'; + + await instance.validateChainIdOnSubmit(validChainId); + + expect(instance.state.warningChainId).toBe( + 'Could not fetch chain ID. Is your RPC URL correct?', + ); + }); + + it('should return without updating warningName when useSafeChainsListValidation is false', () => { + const instance = wrapper.instance(); + + instance.props.useSafeChainsListValidation = false; // Disable validation + + instance.validateName(); + + // Make sure warningName wasn't updated + expect(instance.state.warningName).toBeUndefined(); + }); + + it('should set warningName to undefined if chainToMatch name is the same as nickname', () => { + const instance = wrapper.instance(); + + const chainToMatch = { name: 'Test Network' }; + + instance.validateName(chainToMatch); + + expect(instance.state.warningName).toBeUndefined(); + }); + + it('should set warningName to undefined when networkList name is the same as nickname', () => { + const instance = wrapper.instance(); + + instance.setState({ + networkList: { + name: 'Test Network', // same as nickname + }, + }); + + instance.validateName(); + + expect(instance.state.warningName).toBeUndefined(); + }); + + it('should update rpcUrl, set validatedRpcURL to false, and call validation methods', async () => { + const instance = wrapper.instance(); + + const validateNameSpy = jest.spyOn(instance, 'validateName'); + const validateChainIdSpy = jest.spyOn(instance, 'validateChainId'); + const validateSymbolSpy = jest.spyOn(instance, 'validateSymbol'); + const getCurrentStateSpy = jest.spyOn(instance, 'getCurrentState'); + + // Mock initial state + instance.setState({ + addMode: true, + }); + + // Call the function + await instance.onRpcUrlChangeWithName( + 'https://example.com', + 'Test Network', + 'Custom', + ); + + // Assert that state was updated + expect(wrapper.state('rpcUrl')).toBe('https://example.com'); + expect(wrapper.state('validatedRpcURL')).toBe(false); + expect(wrapper.state('rpcName')).toBe('Test Network'); + expect(wrapper.state('warningRpcUrl')).toBeUndefined(); + expect(wrapper.state('warningChainId')).toBeUndefined(); + expect(wrapper.state('warningSymbol')).toBeUndefined(); + expect(wrapper.state('warningName')).toBeUndefined(); + + // Assert that the validation methods were called + expect(validateNameSpy).toHaveBeenCalled(); + expect(validateChainIdSpy).toHaveBeenCalled(); + expect(validateSymbolSpy).toHaveBeenCalled(); + expect(getCurrentStateSpy).toHaveBeenCalled(); + }); + + it('should set rpcName to type if name is not provided', async () => { + const instance = wrapper.instance(); + + await instance.onRpcUrlChangeWithName( + 'https://example.com', + null, + 'Custom', + ); + + expect(wrapper.state('rpcName')).toBe('Custom'); + }); + + it('should not call validateChainId if addMode is false', async () => { + const instance = wrapper.instance(); + + const validateChainIdSpy = jest.spyOn(instance, 'validateChainId'); + + // Set addMode to false + instance.setState({ + addMode: false, + }); + + await instance.onRpcUrlChangeWithName( + 'https://example.com', + 'Test Network', + 'Custom', + ); + + // ValidateChainId should not be called + expect(validateChainIdSpy).not.toHaveBeenCalled(); + }); + }); + + describe('NetworkSettings componentDidMount', () => { + it('should correctly initialize state 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': { + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + chainId: '0x1', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: 'Infura', + url: 'https://mainnet.infura.io/v3/', + }, + ], + name: 'Ethereum Main Network', + nativeCurrency: 'ETH', + }, + }, + }; + + // Reinitialize the component with new props + const wrapper2 = shallow( + + + , + ) + .find(NetworkSettings) + .dive(); + + const instance2 = wrapper2.instance(); + + // Simulate component mounting + instance2.componentDidMount?.(); + + // Check if state was initialized correctly + 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 set addMode to true if no networkTypeOrRpcUrl is provided', () => { + const SAMPLE_NETWORKSETTINGS_PROPS_3 = { + route: { + params: {}, + }, + navigation: { + setOptions: jest.fn(), + navigate: jest.fn(), + goBack: jest.fn(), + }, + }; + + // Reinitialize the component without networkTypeOrRpcUrl + const wrapper3 = shallow( + + + , + ) + .find(NetworkSettings) + .dive(); + + const instance3 = wrapper3.instance(); + + // Simulate component mounting + instance3.componentDidMount?.(); + + // Check if state was initialized with addMode set to true + expect(wrapper3.state('addMode')).toBe(true); + }); + + it('should handle cases where the network is custom', () => { + const SAMPLE_NETWORKSETTINGS_PROPS_4 = { + route: { + params: { network: 'https://custom-network.io' }, + }, + navigation: { + setOptions: jest.fn(), + navigate: jest.fn(), + goBack: jest.fn(), + }, + networkConfigurations: { + '0x123': { + blockExplorerUrls: ['https://custom-explorer.io'], + chainId: '0x123', + defaultRpcEndpointIndex: 0, + rpcEndpoints: [ + { + url: 'https://custom-network.io', + type: RpcEndpointType.Custom, + }, + ], + name: 'Custom Network', + nativeCurrency: 'CUST', + }, + }, + }; + + // Reinitialize the component with custom network + const wrapper4 = shallow( + + + , + ) + .find(NetworkSettings) + .dive(); + + const instance4 = wrapper4.instance(); + + // Simulate component mounting + instance4.componentDidMount?.(); + + // Check if state was initialized correctly for the custom network + expect(wrapper4.state('nickname')).toBe('Custom Network'); + expect(wrapper4.state('chainId')).toBe('0x123'); + expect(wrapper4.state('rpcUrl')).toBe('https://custom-network.io'); + }); + + it('should call validateRpcAndChainId when matchedChainNetwork changes', () => { + const instance = wrapper.instance(); + + const validateRpcAndChainIdSpy = jest.spyOn( + wrapper.instance(), + 'validateRpcAndChainId', + ); + const updateNavBarSpy = jest.spyOn(wrapper.instance(), 'updateNavBar'); + + const prevProps = { + matchedChainNetwork: { + id: 'network1', + }, + }; + + // Simulate a prop change + wrapper.setProps({ + matchedChainNetwork: { + id: 'network2', + }, + }); + + instance.componentDidUpdate(prevProps); + + expect(updateNavBarSpy).toHaveBeenCalled(); + expect(validateRpcAndChainIdSpy).toHaveBeenCalled(); + }); + }); + + describe('NetworkSettings - handleNetworkUpdate', () => { + const mockNavigation = { + navigate: jest.fn(), + goBack: jest.fn(), + }; + + const SAMPLE_PROPS = { + route: { + params: { + network: 'mainnet', + }, + }, + navigation: { + setOptions: jest.fn(), + navigate: jest.fn(), + goBack: jest.fn(), + }, + networkConfigurations: { + '0x1': { + blockExplorerUrls: ['https://etherscan.io'], + defaultBlockExplorerUrlIndex: 0, + defaultRpcEndpointIndex: 0, + chainId: '0x1', + rpcEndpoints: [ + { + networkClientId: 'mainnet', + type: 'Infura', + url: 'https://mainnet.infura.io/v3/', + }, + ], + name: 'Ethereum Main Network', + nativeCurrency: 'ETH', + }, + }, + }; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const wrapper4: any = shallow( + + + , + ) + .find(NetworkSettings) + .dive(); + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should update the network if the network exists', async () => { + const instance = wrapper4.instance(); + + await instance.handleNetworkUpdate({ + rpcUrl: 'http://localhost:8080', + rpcUrls: [{ url: 'http://localhost:8080', type: 'custom', name: '' }], + blockExplorerUrls: ['https://etherscan.io'], + isNetworkExists: [], + chainId: '0x1', + navigation: mockNavigation, + }); + + expect( + Engine.context.NetworkController.updateNetwork, + ).toHaveBeenCalledWith( + '0x1', // chainId + expect.objectContaining({ + blockExplorerUrls: ['https://etherscan.io'], + chainId: '0x1', + defaultBlockExplorerUrlIndex: undefined, + defaultRpcEndpointIndex: 0, + name: undefined, + nativeCurrency: undefined, + rpcEndpoints: [ + { name: '', type: 'custom', url: 'http://localhost:8080' }, + ], + }), + { replacementSelectedRpcEndpointIndex: 0 }, + ); + }); + }); }); diff --git a/app/core/__mocks__/MockedEngine.ts b/app/core/__mocks__/MockedEngine.ts index dcdde0ec33c..5d125a599ab 100644 --- a/app/core/__mocks__/MockedEngine.ts +++ b/app/core/__mocks__/MockedEngine.ts @@ -30,6 +30,7 @@ export const mockedEngine = { }, }; }, + removeNetwork: () => ({}), state: { ...mockNetworkState({ chainId: CHAIN_IDS.MAINNET, diff --git a/app/store/migrations/055.test.ts b/app/store/migrations/055.test.ts index 4184ed5027d..77dfd6da315 100644 --- a/app/store/migrations/055.test.ts +++ b/app/store/migrations/055.test.ts @@ -6,7 +6,6 @@ import { PreferencesState } from '@metamask/preferences-controller'; import { SelectedNetworkControllerState } from '@metamask/selected-network-controller'; const version = 55; -const oldVersion = 55; interface EngineState { engine: { @@ -32,7 +31,6 @@ describe(`migration #${version}`, () => { it('captures an exception if the network controller state is not defined', async () => { const oldState = { - meta: { version: oldVersion }, engine: { backgroundState: {} }, }; @@ -48,7 +46,6 @@ describe(`migration #${version}`, () => { it('captures an exception if the network controller state is not an object', async () => { for (const NetworkController of [undefined, null, 1, 'foo']) { const oldState = { - meta: { version: oldVersion }, engine: { backgroundState: { NetworkController } }, }; @@ -65,7 +62,6 @@ describe(`migration #${version}`, () => { it('captures an exception if the transaction controller state is not defined', async () => { const oldState = { - meta: { version: oldVersion }, engine: { backgroundState: { NetworkController: {} } }, }; @@ -131,7 +127,6 @@ describe(`migration #${version}`, () => { }; const oldState = { - meta: { version: oldVersion }, engine: { backgroundState: { NetworkController: { @@ -268,7 +263,6 @@ describe(`migration #${version}`, () => { const randomChainId = '0x123456'; const oldState = { - meta: { version: oldVersion }, engine: { backgroundState: { NetworkController: { @@ -322,7 +316,6 @@ describe(`migration #${version}`, () => { const randomChainId = '0x123456'; const oldState = { - meta: { version: oldVersion }, engine: { backgroundState: { NetworkController: { @@ -352,7 +345,6 @@ describe(`migration #${version}`, () => { it('handles the case where selectedNetworkClientId does not point to any endpoint', async () => { const oldState = { - meta: { version: oldVersion }, engine: { backgroundState: { NetworkController: { diff --git a/app/store/migrations/055.ts b/app/store/migrations/055.ts index 570ff8457d8..187e7f19a0f 100644 --- a/app/store/migrations/055.ts +++ b/app/store/migrations/055.ts @@ -1,8 +1,5 @@ import { captureException } from '@sentry/react-native'; -import { - TransactionControllerState, - TransactionMeta, -} from '@metamask/transaction-controller'; +import { TransactionMeta } from '@metamask/transaction-controller'; import { parse, equal } from 'uri-js'; import { SelectedNetworkControllerState } from '@metamask/selected-network-controller'; import { hasProperty, isObject, RuntimeObject } from '@metamask/utils'; @@ -16,8 +13,7 @@ export const version = 55; * @param networkConfigurations - Existing network configurations. * @returns Updated network configurations including Infura networks. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function addBuiltInInfuraNetworks(networkConfigurations: any[]) { +function addBuiltInInfuraNetworks(networkConfigurations: unknown[]) { return [ { type: 'infura', @@ -74,8 +70,8 @@ export default function migrate(state: unknown) { const networkControllerState = state.engine?.backgroundState?.NetworkController; - const transactionControllerState = state.engine?.backgroundState - ?.TransactionController as TransactionControllerState; + const transactionControllerState = + state.engine?.backgroundState?.TransactionController; const selectedNetworkController = state.engine?.backgroundState ?.SelectedNetworkController as SelectedNetworkControllerState; @@ -381,23 +377,6 @@ export default function migrate(state: unknown) { ); } - // Migrate the user's drag + drop preference order for the network menu - if ( - hasProperty(state.engine.backgroundState, 'NetworkOrderController') && - isObject(state.engine.backgroundState.NetworkOrderController) && - Array.isArray( - state.engine.backgroundState.NetworkOrderController.orderedNetworkList, - ) - ) { - state.engine.backgroundState.NetworkOrderController.orderedNetworkList = [ - ...new Set( - state.engine.backgroundState.NetworkOrderController.orderedNetworkList.map( - (network) => network.networkId, - ), - ), - ].map((networkId) => ({ networkId })); - } - // Return the modified state return state; } diff --git a/e2e/pages/Settings/NetworksView.js b/e2e/pages/Settings/NetworksView.js index 3a06040f7fd..5ab1cdde96d 100644 --- a/e2e/pages/Settings/NetworksView.js +++ b/e2e/pages/Settings/NetworksView.js @@ -20,21 +20,15 @@ class NetworkView { } get addNetworkButtonForm() { - return device.getPlatform() === 'ios' - ? Matchers.getElementByID(NetworkListModalSelectorsIDs.ADD_BUTTON) - : Matchers.getElementByLabel(NetworkListModalSelectorsIDs.ADD_BUTTON); + return Matchers.getElementByID(NetworkListModalSelectorsIDs.ADD_BUTTON); } get addRpcDropDownButton() { - return device.getPlatform() === 'ios' - ? Matchers.getElementByID(NetworksViewSelectorsIDs.ICON_BUTTON_RPC) - : Matchers.getElementByLabel(NetworksViewSelectorsIDs.ICON_BUTTON_RPC); + return Matchers.getElementByID(NetworksViewSelectorsIDs.ICON_BUTTON_RPC); } get addRpcButton() { - return device.getPlatform() === 'ios' - ? Matchers.getElementByID(NetworksViewSelectorsIDs.ADD_RPC_BUTTON) - : Matchers.getElementByLabel(NetworksViewSelectorsIDs.ADD_RPC_BUTTON); + return Matchers.getElementByID(NetworksViewSelectorsIDs.ADD_RPC_BUTTON); } get noMatchingText() { @@ -98,13 +92,9 @@ class NetworkView { } get rpcAddButton() { - return device.getPlatform() === 'android' - ? Matchers.getElementByLabel( - NetworksViewSelectorsIDs.ADD_CUSTOM_NETWORK_BUTTON, - ) - : Matchers.getElementByID( - NetworksViewSelectorsIDs.ADD_CUSTOM_NETWORK_BUTTON, - ); + return Matchers.getElementByID( + NetworksViewSelectorsIDs.ADD_CUSTOM_NETWORK_BUTTON, + ); } get blockExplorer() { diff --git a/e2e/pages/modals/NetworkListModal.js b/e2e/pages/modals/NetworkListModal.js index 2865844a391..c961f0ba603 100644 --- a/e2e/pages/modals/NetworkListModal.js +++ b/e2e/pages/modals/NetworkListModal.js @@ -46,12 +46,18 @@ class NetworkListModal { ); } + get deleteButton() { + return Matchers.getElementByID('delete-network-button'); + } + async getCustomNetwork(network, custom = false) { if (device.getPlatform() === 'android' || !custom) { return Matchers.getElementByText(network); } - return Matchers.getElementByID(NetworkListModalSelectorsIDs.CUSTOM_NETWORK_CELL(network)); + return Matchers.getElementByID( + NetworkListModalSelectorsIDs.CUSTOM_NETWORK_CELL(network), + ); } async tapDeleteButton() { @@ -72,7 +78,7 @@ class NetworkListModal { } async swipeToDismissModal() { - await Gestures.swipe(this.selectNetwork, 'down', 'slow', 0.6); + await Gestures.swipe(this.selectNetwork, 'down', 'slow', 0.9); } async tapTestNetworkSwitch() { @@ -98,6 +104,9 @@ class NetworkListModal { async tapAddNetworkButton() { await Gestures.waitAndTap(this.addPopularNetworkButton); } + async deleteNetwork() { + await Gestures.waitAndTap(this.deleteButton); + } } export default new NetworkListModal(); diff --git a/e2e/selectors/Modals/NetworkListModal.selectors.js b/e2e/selectors/Modals/NetworkListModal.selectors.js index a476f4b3a07..68e890f00d8 100644 --- a/e2e/selectors/Modals/NetworkListModal.selectors.js +++ b/e2e/selectors/Modals/NetworkListModal.selectors.js @@ -9,6 +9,7 @@ export const NetworkListModalSelectorsText = { export const NetworkListModalSelectorsIDs = { SCROLL: 'other-networks-scroll', TEST_NET_TOGGLE: 'test-network-switch-id', + DELETE_NETWORK: 'delete-network-button', OTHER_LIST: 'other-network-name', ADD_BUTTON: 'add-network-button', TOOLTIP: 'popular-networks-information-tooltip', diff --git a/e2e/specs/networks/add-custom-rpc.spec.js b/e2e/specs/networks/add-custom-rpc.spec.js index d45488870b0..a93af0334fd 100644 --- a/e2e/specs/networks/add-custom-rpc.spec.js +++ b/e2e/specs/networks/add-custom-rpc.spec.js @@ -57,7 +57,7 @@ describe(Regression('Custom RPC Tests'), () => { await NetworkView.tapRpcDropDownButton(); await NetworkView.tapAddRpcButton(); - await TestHelpers.delay(2000); + await TestHelpers.delay(200); await NetworkView.typeInRpcUrl('abc'); // Input incorrect RPC URL await Assertions.checkIfVisible(NetworkView.rpcWarningBanner); await NetworkView.clearRpcInputBox(); @@ -65,19 +65,21 @@ describe(Regression('Custom RPC Tests'), () => { await NetworkView.tapAddRpcButton(); + await NetworkView.typeInNetworkSymbol( + `${CustomNetworks.Gnosis.providerConfig.ticker}\n`, + ); + await NetworkView.typeInChainId( CustomNetworks.Gnosis.providerConfig.chainId, ); + await NetworkView.tapChainIDLabel(); // Focus outside of text input field - await NetworkView.typeInNetworkSymbol( - `${CustomNetworks.Gnosis.providerConfig.ticker}\n`, - ); if (device.getPlatform() === 'ios') { await NetworkView.tapChainIDLabel(); // Focus outside of text input field await NetworkView.tapChainIDLabel(); // Focus outside of text input field - await NetworkView.tapRpcNetworkAddButton(); } + await NetworkView.tapRpcNetworkAddButton(); }); it('should switch to Gnosis network', async () => { @@ -176,8 +178,7 @@ describe(Regression('Custom RPC Tests'), () => { } // delete Gnosis network - const deleteButton = Matchers.getElementByID('delete-network-button-0x64'); - await Gestures.waitAndTap(deleteButton); + await NetworkListModal.deleteNetwork(); await TestHelpers.delay(200); diff --git a/e2e/specs/networks/networks-search.spec.js b/e2e/specs/networks/networks-search.spec.js index 42b0ea475b7..c30b24db4ec 100644 --- a/e2e/specs/networks/networks-search.spec.js +++ b/e2e/specs/networks/networks-search.spec.js @@ -44,10 +44,7 @@ describe(Regression('Networks Search'), () => { } // delete avalanche network - const deleteButton = Matchers.getElementByID( - 'delete-network-button-0xa86a', - ); - await Gestures.waitAndTap(deleteButton); + await NetworkListModal.deleteNetwork(); await TestHelpers.delay(2000); await NetworkListModal.tapDeleteButton(); diff --git a/e2e/specs/permission-systems/permission-system-delete-wallet.spec.js b/e2e/specs/permission-systems/permission-system-delete-wallet.spec.js index 53a16cb43ff..85ee625fdfd 100644 --- a/e2e/specs/permission-systems/permission-system-delete-wallet.spec.js +++ b/e2e/specs/permission-systems/permission-system-delete-wallet.spec.js @@ -102,6 +102,7 @@ describe( await Assertions.checkIfVisible(Browser.browserScreenID); await Browser.tapNetworkAvatarButtonOnBrowser(); await Assertions.checkIfNotVisible(ConnectedAccountsModal.title); + await NetworkListModal.scrollToBottomOfNetworkList(); await Assertions.checkIfVisible(NetworkListModal.testNetToggle); }, ); diff --git a/e2e/specs/permission-systems/permission-system-revoking-multiple-accounts.spec.js b/e2e/specs/permission-systems/permission-system-revoking-multiple-accounts.spec.js index 685361d5729..f59d8e7c114 100644 --- a/e2e/specs/permission-systems/permission-system-revoking-multiple-accounts.spec.js +++ b/e2e/specs/permission-systems/permission-system-revoking-multiple-accounts.spec.js @@ -15,57 +15,62 @@ import { Regression } from '../../tags'; const AccountTwoText = 'Account 2'; -describe(Regression('Connecting to multiple dapps and revoking permission on one but staying connected to the other'), () => { - beforeAll(async () => { - jest.setTimeout(150000); - await TestHelpers.reverseServerPort(); - }); +describe( + Regression( + 'Connecting to multiple dapps and revoking permission on one but staying connected to the other', + ), + () => { + beforeAll(async () => { + jest.setTimeout(150000); + await TestHelpers.reverseServerPort(); + }); - it('should connect multiple accounts and revoke them', async () => { - await withFixtures( - { - dapp: true, - fixture: new FixtureBuilder() - .withPermissionControllerConnectedToTestDapp() - .build(), - restartDevice: true, - }, - async () => { - //should navigate to browser - await loginToApp(); - await TabBarComponent.tapBrowser(); - await Assertions.checkIfVisible(Browser.browserScreenID); + it('should connect multiple accounts and revoke them', async () => { + await withFixtures( + { + dapp: true, + fixture: new FixtureBuilder() + .withPermissionControllerConnectedToTestDapp() + .build(), + restartDevice: true, + }, + async () => { + //should navigate to browser + await loginToApp(); + await TabBarComponent.tapBrowser(); + await Assertions.checkIfVisible(Browser.browserScreenID); - //TODO: should re add connecting to an external swap step after detox has been updated + //TODO: should re add connecting to an external swap step after detox has been updated - await Browser.navigateToTestDApp(); - await Browser.tapNetworkAvatarButtonOnBrowser(); - await Assertions.checkIfVisible(ConnectedAccountsModal.title); - await TestHelpers.delay(2000); + await Browser.navigateToTestDApp(); + await Browser.tapNetworkAvatarButtonOnBrowser(); + await Assertions.checkIfVisible(ConnectedAccountsModal.title); + await TestHelpers.delay(2000); - await Assertions.checkIfNotVisible(ToastModal.notificationTitle); - await ConnectedAccountsModal.tapConnectMoreAccountsButton(); - await AccountListView.tapAddAccountButton(); - await AddAccountModal.tapCreateAccount(); - if (device.getPlatform() === 'android') { - await Assertions.checkIfTextIsDisplayed(AccountTwoText); - } - await AccountListView.tapAccountIndex(0); - await AccountListView.tapConnectAccountsButton(); + await Assertions.checkIfNotVisible(ToastModal.notificationTitle); + await ConnectedAccountsModal.tapConnectMoreAccountsButton(); + await AccountListView.tapAddAccountButton(); + await AddAccountModal.tapCreateAccount(); + if (device.getPlatform() === 'android') { + await Assertions.checkIfTextIsDisplayed(AccountTwoText); + } + await AccountListView.tapAccountIndex(0); + await AccountListView.tapConnectAccountsButton(); - // should revoke accounts - await Browser.tapNetworkAvatarButtonOnBrowser(); - await ConnectedAccountsModal.tapPermissionsButton(); - await TestHelpers.delay(1500); - await ConnectedAccountsModal.tapDisconnectAllButton(); - await Assertions.checkIfNotVisible(ToastModal.notificationTitle); + // should revoke accounts + await Browser.tapNetworkAvatarButtonOnBrowser(); + await ConnectedAccountsModal.tapPermissionsButton(); + await TestHelpers.delay(1500); + await ConnectedAccountsModal.tapDisconnectAllButton(); + await Assertions.checkIfNotVisible(ToastModal.notificationTitle); - await Browser.tapNetworkAvatarButtonOnBrowser(); - await Assertions.checkIfNotVisible(ConnectedAccountsModal.title); - await Assertions.checkIfVisible(NetworkListModal.networkScroll); - await NetworkListModal.swipeToDismissModal(); - await Assertions.checkIfNotVisible(NetworkListModal.networkScroll); - }, - ); - }); -}); + await Browser.tapNetworkAvatarButtonOnBrowser(); + await Assertions.checkIfNotVisible(ConnectedAccountsModal.title); + await Assertions.checkIfVisible(NetworkListModal.networkScroll); + await NetworkListModal.swipeToDismissModal(); + await Assertions.checkIfNotVisible(NetworkListModal.networkScroll); + }, + ); + }); + }, +); diff --git a/package.json b/package.json index 1b811292afe..67b20e24b93 100644 --- a/package.json +++ b/package.json @@ -349,6 +349,7 @@ "stream-browserify": "3.0.0", "through2": "3.0.1", "unicode-confusables": "^0.1.1", + "uri-js": "^4.4.1", "url": "0.11.0", "url-parse": "1.5.9", "uuid": "^8.3.2", @@ -484,7 +485,6 @@ "serve-handler": "^6.1.5", "simple-git": "^3.25.0", "typescript": "~5.4.5", - "uri-js": "^4.4.1", "wdio-cucumberjs-json-reporter": "^4.4.3", "webpack": "^5.88.2", "webpack-cli": "^5.1.4",