Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 25 additions & 21 deletions app/components/Nav/Main/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
} from '../../hooks/useNetworksByNamespace/useNetworksByNamespace';
import { useNetworkSelection } from '../../hooks/useNetworkSelection/useNetworkSelection';
import { useIsOnBridgeRoute } from '../../UI/Bridge/hooks/useIsOnBridgeRoute';
import { consumeSuppressedNetworkAddedToast } from '../../../util/networks/networkToastSuppression';

const Stack = createStackNavigator();

Expand Down Expand Up @@ -167,7 +168,7 @@
} else {
props.setInfuraAvailabilityNotBlocked();
}
// eslint-disable-next-line react-hooks/exhaustive-deps

Check warning on line 171 in app/components/Nav/Main/index.js

View workflow job for this annotation

GitHub Actions / Run `lint`

React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior
}, [
props.navigation,
props.providerType,
Expand Down Expand Up @@ -298,11 +299,9 @@
);

// Emit network addition/deletion toast if network list changes
// Bridge routes are skipped as they interfere with bridge UI
if (
previousNetworkValues.length &&
currentNetworkValues.length !== previousNetworkValues.length &&
!isOnBridgeRoute
currentNetworkValues.length !== previousNetworkValues.length
) {
// Find the newly added network by comparing chainIds
const newNetwork = currentNetworkValues.find(
Expand All @@ -318,24 +317,29 @@
),
);

toastRef?.current?.showToast({
variant: ToastVariants.Plain,
labelOptions: [
{
label: `${
(newNetwork?.name || deletedNetwork?.name) ??
strings('asset_details.network')
} `,
isBold: true,
},
{
label: deletedNetwork
? strings('toast.network_removed')
: strings('toast.network_added'),
},
],
networkImageSource: networkImage,
});
const shouldShowNetworkAddedToast =
newNetwork && !consumeSuppressedNetworkAddedToast(newNetwork?.chainId);
const shouldShowToast = shouldShowNetworkAddedToast || deletedNetwork;
if (shouldShowToast) {
toastRef?.current?.showToast({
variant: ToastVariants.Plain,
labelOptions: [
{
label: `${
(newNetwork?.name || deletedNetwork?.name) ??
strings('asset_details.network')
} `,
isBold: true,
},
{
label: deletedNetwork
? strings('toast.network_removed')
: strings('toast.network_added'),
},
],
networkImageSource: networkImage,
});
}
}
previousNetworkConfigurations.current = networkConfigurations;
}, [isOnBridgeRoute, networkConfigurations, networkImage, toastRef]);
Expand Down Expand Up @@ -378,7 +382,7 @@
removeConnectionStatusListener.current &&
removeConnectionStatusListener.current();
};
// eslint-disable-next-line react-hooks/exhaustive-deps

Check warning on line 385 in app/components/Nav/Main/index.js

View workflow job for this annotation

GitHub Actions / Run `lint`

React Compiler has skipped optimizing this component because one or more React ESLint rules were disabled. React Compiler only works when your components follow all the rules of React, disabling them may result in unexpected or incorrect behavior
}, [connectionChangeHandler]);

const openDeprecatedNetworksArticle = () => {
Expand Down
2 changes: 2 additions & 0 deletions app/components/UI/Bridge/hooks/useTokenSelection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Hex } from '@metamask/utils';
import Engine from '../../../../core/Engine';
import { selectNetworkConfigurations } from '../../../../selectors/networkController';
import { PopularList } from '../../../../util/networks/customNetworks';
import { suppressNextNetworkAddedToast } from '../../../../util/networks/networkToastSuppression';

/**
* Hook to manage token selection logic for Bridge token selector
Expand Down Expand Up @@ -69,6 +70,7 @@ export const useTokenSelection = (type: TokenSelectorType) => {
try {
const hexChainId = toHex(popularNetwork.chainId) as Hex;
const { blockExplorerUrl } = popularNetwork.rpcPrefs;
suppressNextNetworkAddedToast(popularNetwork.chainId);
await Engine.context.NetworkController.addNetwork({
chainId: hexChainId,
blockExplorerUrls: blockExplorerUrl ? [blockExplorerUrl] : [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { handleSwapUrl } from '../handleSwapUrl';
import NavigationService from '../../../../NavigationService';
import { BridgeViewMode } from '../../../../../components/UI/Bridge/types';
import { fetchAssetMetadata } from '../../../../../components/UI/Bridge/hooks/useAssetMetadata/utils';
import { CHAIN_IDS } from '@metamask/transaction-controller';
import { resetSuppressedNetworkAddedToasts } from '../../../../../util/networks/networkToastSuppression';

jest.mock('../../../../NavigationService', () => ({
navigation: {
Expand All @@ -24,14 +26,11 @@ jest.mock(
jest.mock('../../../../Engine', () => ({
context: {
NetworkController: {
getNetworkConfigurationByChainId: jest.fn().mockReturnValue({
rpcEndpoints: [
{
networkClientId: 'mainnetNetworkClientId',
},
],
defaultRpcEndpointIndex: 0,
}),
getNetworkConfigurationByChainId: jest.fn(),
addNetwork: jest.fn(),
},
NetworkEnablementController: {
enableNetwork: jest.fn(),
},
MultichainNetworkController: {
state: {
Expand All @@ -41,8 +40,32 @@ jest.mock('../../../../Engine', () => ({
},
}));

const mockEngine = jest.requireMock('../../../../Engine') as {
context: {
NetworkController: {
getNetworkConfigurationByChainId: jest.Mock;
addNetwork: jest.Mock;
};
NetworkEnablementController: {
enableNetwork: jest.Mock;
};
};
};
const mockNavigate = NavigationService.navigation.navigate as jest.Mock;
const mockFetchAssetMetadata = fetchAssetMetadata as jest.Mock;
const mockGetNetworkConfigurationByChainId =
mockEngine.context.NetworkController.getNetworkConfigurationByChainId;
const mockAddNetwork = mockEngine.context.NetworkController.addNetwork;
const mockEnableNetwork =
mockEngine.context.NetworkEnablementController.enableNetwork;
const availableNetworkConfig = {
rpcEndpoints: [
{
networkClientId: 'mainnetNetworkClientId',
},
],
defaultRpcEndpointIndex: 0,
};

describe('handleSwapUrl', () => {
const expectedSourceToken = {
Expand All @@ -64,12 +87,17 @@ describe('handleSwapUrl', () => {

beforeEach(() => {
jest.clearAllMocks();
resetSuppressedNetworkAddedToasts();
mockGetNetworkConfigurationByChainId.mockReturnValue(
availableNetworkConfig,
);
// Mock fetchAssetMetadata to return token data based on the address
mockFetchAssetMetadata.mockImplementation(async (address) => {
const assetId = String(address);
// Parse the address from CAIP format if needed
const tokenAddress = address.includes('/')
? address.split(':')[2]
: address;
const tokenAddress = assetId.includes('/')
? (assetId.split(':').at(-1) ?? assetId)
: assetId;

if (
tokenAddress.toLowerCase() ===
Expand Down Expand Up @@ -119,6 +147,7 @@ describe('handleSwapUrl', () => {
location: 'Main View',
},
});
expect(mockEnableNetwork).toHaveBeenCalledWith(CHAIN_IDS.MAINNET);
});

it('navigates to Bridge view with partial parameters (only source token)', async () => {
Expand All @@ -140,6 +169,83 @@ describe('handleSwapUrl', () => {
});
});

it('adds and enables supported missing EVM source networks before navigating', async () => {
const expectedOptimismSourceToken = {
address: '0x1111111111111111111111111111111111111111',
chainId: CHAIN_IDS.OPTIMISM,
decimals: 6,
name: 'Optimism USDC',
symbol: 'USDC',
image: 'https://example.com/op-usdc.png',
};
const expectedOptimismDestToken = {
address: '0x2222222222222222222222222222222222222222',
chainId: CHAIN_IDS.OPTIMISM,
decimals: 6,
name: 'Optimism USDT',
symbol: 'USDT',
image: 'https://example.com/op-usdt.png',
};

mockFetchAssetMetadata.mockImplementation(async (address) => {
const assetId = String(address);
const tokenAddress = assetId.includes('/')
? (assetId.split(':').at(-1) ?? assetId)
: assetId;

if (
tokenAddress.toLowerCase() ===
'0x1111111111111111111111111111111111111111'
) {
return {
...expectedOptimismSourceToken,
assetId: 'eip155:10/erc20:0x1111111111111111111111111111111111111111',
};
}

if (
tokenAddress.toLowerCase() ===
'0x2222222222222222222222222222222222222222'
) {
return {
...expectedOptimismDestToken,
assetId: 'eip155:10/erc20:0x2222222222222222222222222222222222222222',
};
}

return undefined;
});

mockGetNetworkConfigurationByChainId
.mockReturnValueOnce(undefined)
.mockReturnValue(availableNetworkConfig);

const swapPath =
'from=eip155:10/erc20:0x1111111111111111111111111111111111111111&to=eip155:10/erc20:0x2222222222222222222222222222222222222222&amount=1000000';

await handleSwapUrl({ swapPath });

expect(mockAddNetwork).toHaveBeenCalledWith(
expect.objectContaining({
chainId: CHAIN_IDS.OPTIMISM,
name: 'OP',
nativeCurrency: 'ETH',
}),
);
expect(mockEnableNetwork).toHaveBeenCalledWith(CHAIN_IDS.OPTIMISM);
expect(mockNavigate).toHaveBeenCalledWith('Bridge', {
screen: 'BridgeView',
params: {
sourceToken: expectedOptimismSourceToken,
destToken: expectedOptimismDestToken,
sourceAmount: '1.0',
sourcePage: 'deeplink',
bridgeViewMode: BridgeViewMode.Unified,
location: 'Main View',
},
});
});

it('navigates to Bridge view with partial parameters (only dest token)', async () => {
const swapPath =
'to=eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7';
Expand Down Expand Up @@ -200,6 +306,37 @@ describe('handleSwapUrl', () => {
});
});

it('falls back when the source chain is configured but not swap supported', async () => {
mockFetchAssetMetadata.mockResolvedValueOnce({
address: '0x3333333333333333333333333333333333333333',
chainId: '0x1234',
symbol: 'TEST',
name: 'Unsupported Token',
decimals: 18,
image: 'https://example.com/test.png',
assetId: 'eip155:4660/erc20:0x3333333333333333333333333333333333333333',
});

const swapPath =
'from=eip155:4660/erc20:0x3333333333333333333333333333333333333333';

await handleSwapUrl({ swapPath });

expect(mockEnableNetwork).not.toHaveBeenCalledWith('0x1234');
expect(mockAddNetwork).not.toHaveBeenCalled();
expect(mockNavigate).toHaveBeenCalledWith('Bridge', {
screen: 'BridgeView',
params: {
sourceToken: undefined,
destToken: undefined,
sourceAmount: undefined,
sourcePage: 'deeplink',
bridgeViewMode: BridgeViewMode.Unified,
location: 'Main View',
},
});
});

it('handles invalid amount format and navigates with fallback', async () => {
const swapPath =
'from=eip155:1/erc20:0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48&to=eip155:1/erc20:0xdAC17F958D2ee523a2206206994597C13D831ec7&amount=invalid';
Expand Down
Loading
Loading