Repository: MetaMask/metamask-mobile
Pull request #13847: feat: bridge input screen
Author: login: infiniteflower
Labels: name: team-swaps-deprecated
description: DEPRECATED: please use "team-swaps-and-bridge" instead
name: team-swaps-and-bridge
description: Swaps and Bridge team
name: release-7.43.0
description: Issue or pull request that will be included in release 7.43.0
Created: 2025-03-04T21:21:01Z | Merged: 2025-03-11T20:01:59Z

<!--
Please submit this PR as a draft initially.
Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once.
-->

## **Description**

<!--
Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions:
1. What is the reason for the change?
2. What is the improvement/solution?
-->

This PR:
1. Implements the Bridge input screen where a user can enter a source token amount
2. Shows a fiat value for the amount they entered
3. Shows token icon and network
4. Validates that there is sufficient balance

It does not:

1. fetch quotes
5. allow the user to select tokens
6. allow the user to select networks
7. allow the user to switch between token amount and fiat amount inputs

Everything is hidden behind a hard coded env flag, so nothing will be changed in production.

## **Related issues**

Fixes:

## **Manual testing steps**

1. Add `export MM_BRIDGE_UI_ENABLED="true"` to `.js.env`
8. Go to the Bridge page

## **Screenshots/Recordings**

<!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. -->

### **Before**

<!-- [screenshots/recordings] -->

### **After**

<!-- [screenshots/recordings] -->

<img width="321" alt="Screenshot 2025-03-07 at 18 24 37" src="https://github.com/user-attachments/assets/5f4bcc10-b9ac-4beb-a830-e4c603382330" />


## **Pre-merge author checklist**

- [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable
- [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors.

## **Pre-merge reviewer checklist**

- [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
- [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.



Diff:
diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js
index e2a54e4a37e..a29f155901b 100644
--- a/app/components/Nav/App/index.js
+++ b/app/components/Nav/App/index.js
@@ -145,6 +145,7 @@ import {
 } from '../../../util/trace';
 import getUIStartupSpan from '../../../core/Performance/UIStartup';
 import { Confirm } from '../../Views/confirmations/Confirm';
+import { BridgeTokenSelector } from '../../UI/Bridge/BridgeTokenSelector';
 
 const clearStackNavigatorOptions = {
   headerShown: false,
@@ -523,6 +524,10 @@ const RootModalFlow = () => (
       component={ChangeInSimulationModal}
     />
     <Stack.Screen name={Routes.SHEET.TOOLTIP_MODAL} component={TooltipModal} />
+    <Stack.Screen
+      name={Routes.SHEET.BRIDGE_TOKEN_SELECTOR}
+      component={BridgeTokenSelector}
+    />
   </Stack.Navigator>
 );
 
diff --git a/app/components/Nav/Main/MainNavigator.js b/app/components/Nav/Main/MainNavigator.js
index 21125cc77b9..6e9f5209945 100644
--- a/app/components/Nav/Main/MainNavigator.js
+++ b/app/components/Nav/Main/MainNavigator.js
@@ -828,7 +828,7 @@ const MainNavigator = () => (
       {() => <RampRoutes rampType={RampType.SELL} />}
     </Stack.Screen>
     <Stack.Screen name="Swaps" component={Swaps} />
-    <Stack.Screen name="Bridge" component={Bridge} />
+    <Stack.Screen name={Routes.BRIDGE} component={Bridge} />
     <Stack.Screen name="StakeScreens" component={StakeScreenStack} />
     <Stack.Screen
       name="StakeModals"
diff --git a/app/components/UI/Bridge/BridgeTokenSelector.test.tsx b/app/components/UI/Bridge/BridgeTokenSelector.test.tsx
new file mode 100644
index 00000000000..f23e2226264
--- /dev/null
+++ b/app/components/UI/Bridge/BridgeTokenSelector.test.tsx
@@ -0,0 +1,17 @@
+import { renderScreen } from '../../../util/test/renderWithProvider';
+import { BridgeTokenSelector } from './BridgeTokenSelector';
+import Routes from '../../../constants/navigation/Routes';
+
+describe('BridgeTokenSelector', () => {
+  it('renders', () => {
+    const { getByText } = renderScreen(
+      BridgeTokenSelector,
+      {
+        name: Routes.SHEET.BRIDGE_TOKEN_SELECTOR,
+      },
+    );
+
+    expect(getByText('Tokens')).toBeTruthy();
+    expect(getByText('Token content will go here')).toBeTruthy();
+  });
+});
diff --git a/app/components/UI/Bridge/BridgeTokenSelector.tsx b/app/components/UI/Bridge/BridgeTokenSelector.tsx
new file mode 100644
index 00000000000..92a2e896bdc
--- /dev/null
+++ b/app/components/UI/Bridge/BridgeTokenSelector.tsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { Box } from '../Box/Box';
+import Text, { TextVariant } from '../../../component-library/components/Texts/Text';
+import { useStyles } from '../../../component-library/hooks';
+import { Theme } from '../../../util/theme/models';
+import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader';
+import BottomSheet from '../../../component-library/components/BottomSheets/BottomSheet';
+
+const createStyles = (params: { theme: Theme }) => {
+  const { theme } = params;
+  return StyleSheet.create({
+    content: {
+      padding: 24,
+      backgroundColor: theme.colors.background.default,
+      flex: 1,
+    },
+  });
+};
+
+export const BridgeTokenSelector = () => {
+  const { styles } = useStyles(createStyles, {});
+
+  return (
+    <BottomSheet isFullscreen>
+      <Box style={styles.content}>
+        <BottomSheetHeader>
+          <Text variant={TextVariant.HeadingMD}>Tokens</Text>
+        </BottomSheetHeader>
+        <Box>
+          <Text variant={TextVariant.BodyMD}>Token content will go here</Text>
+        </Box>
+      </Box>
+    </BottomSheet>
+  );
+};
diff --git a/app/components/UI/Bridge/Token.tsx b/app/components/UI/Bridge/Token.tsx
new file mode 100644
index 00000000000..818d4832112
--- /dev/null
+++ b/app/components/UI/Bridge/Token.tsx
@@ -0,0 +1,100 @@
+import React from 'react';
+import { StyleSheet, ImageSourcePropType, TouchableOpacity } from 'react-native';
+import Text from '../../../component-library/components/Texts/Text';
+import { useStyles } from '../../../component-library/hooks';
+import { Theme } from '../../../util/theme/models';
+import { Box } from '../Box/Box';
+import { FlexDirection, AlignItems, JustifyContent } from '../Box/box.types';
+import BadgeWrapper from '../../../component-library/components/Badges/BadgeWrapper';
+import Badge, { BadgeVariant } from '../../../component-library/components/Badges/Badge';
+import { BOTTOM_BADGEWRAPPER_BADGEPOSITION } from '../../../component-library/components/Badges/BadgeWrapper/BadgeWrapper.constants';
+import { useNavigation } from '@react-navigation/native';
+import Routes from '../../../constants/navigation/Routes';
+import TokenIcon from '../Swaps/components/TokenIcon';
+
+interface TokenProps {
+  symbol?: string;
+  iconUrl?: ImageSourcePropType;
+  networkImageSource?: ImageSourcePropType;
+  networkName?: string;
+  testID?: string;
+}
+
+interface StylesParams {
+  theme: Theme;
+}
+
+const createStyles = (params: StylesParams) => {
+  const { theme } = params;
+  const { shadows } = theme;
+  return StyleSheet.create({
+    icon: {
+      width: 32,
+      height: 32,
+      borderRadius: 16,
+    },
+    pillContainer: {
+      backgroundColor: theme.colors.background.default,
+      borderRadius: 100,
+      paddingVertical: 8,
+      paddingHorizontal: 12,
+      ...shadows.size.xs,
+    },
+    tokenSymbol: {
+      color: theme.colors.text.default,
+      fontSize: 24,
+      lineHeight: 28,
+    },
+  });
+};
+
+export const Token: React.FC<TokenProps> = ({
+  symbol,
+  iconUrl,
+  networkImageSource,
+  networkName,
+  testID,
+}) => {
+  const { styles } = useStyles(createStyles, {});
+  const navigation = useNavigation();
+
+  const handlePress = () => {
+    navigation.navigate(Routes.MODAL.ROOT_MODAL_FLOW, {
+      screen: Routes.SHEET.BRIDGE_TOKEN_SELECTOR,
+      params: {},
+    });
+  };
+
+  return (
+    <TouchableOpacity onPress={handlePress} testID={testID}>
+      <Box
+        style={styles.pillContainer}
+        flexDirection={FlexDirection.Row}
+        alignItems={AlignItems.center}
+        justifyContent={JustifyContent.flexEnd}
+        gap={4}
+      >
+        <BadgeWrapper
+          badgePosition={BOTTOM_BADGEWRAPPER_BADGEPOSITION}
+          badgeElement={
+            <Badge
+              variant={BadgeVariant.Network}
+              imageSource={networkImageSource}
+              name={networkName}
+            />
+          }
+        >
+          <TokenIcon
+            symbol={symbol}
+            icon={typeof iconUrl === 'string' ? iconUrl : undefined}
+            style={styles.icon}
+          />
+        </BadgeWrapper>
+
+        <Text style={styles.tokenSymbol}>
+          {symbol}
+        </Text>
+      </Box>
+    </TouchableOpacity>
+  );
+};
diff --git a/app/components/UI/Bridge/TokenInputArea.tsx b/app/components/UI/Bridge/TokenInputArea.tsx
new file mode 100644
index 00000000000..392a13e89b8
--- /dev/null
+++ b/app/components/UI/Bridge/TokenInputArea.tsx
@@ -0,0 +1,115 @@
+import React from 'react';
+import { StyleSheet, ImageSourcePropType } from 'react-native';
+import { useSelector } from 'react-redux';
+import { useStyles } from '../../../component-library/hooks';
+import { Box } from '../Box/Box';
+import Text, { TextColor } from '../../../component-library/components/Texts/Text';
+import Input from '../../../component-library/components/Form/TextField/foundation/Input';
+import { Token } from './Token';
+import { selectCurrentCurrency, selectConversionRate } from '../../../selectors/currencyRateController';
+import { weiToFiat, toWei } from '../../../util/number';
+
+interface TokenInputAreaProps {
+  value?: string;
+  tokenSymbol?: string;
+  tokenAddress?: string;
+  tokenBalance?: string;
+  tokenIconUrl?: ImageSourcePropType;
+  networkImageSource?: ImageSourcePropType;
+  networkName?: string;
+  autoFocus?: boolean;
+  isReadonly?: boolean;
+  testID?: string;
+}
+
+const createStyles = () =>
+  StyleSheet.create({
+    container: {
+      paddingHorizontal: 24,
+    },
+    content: {
+      padding: 16,
+    },
+    row: {
+      flexDirection: 'row',
+      justifyContent: 'space-between',
+      alignItems: 'center',
+    },
+    amountContainer: {
+      flex: 1,
+    },
+    input: {
+      fontSize: 40,
+      borderWidth: 0,
+      lineHeight: 40,
+      height: 40,
+    },
+  });
+
+const formatAddress = (address?: string) => address ? `${address.slice(0, 6)}...${address.slice(-4)}` : undefined;
+
+export const TokenInputArea: React.FC<TokenInputAreaProps> = ({
+  value,
+  tokenSymbol,
+  tokenAddress,
+  tokenBalance,
+  tokenIconUrl,
+  networkImageSource,
+  networkName,
+  autoFocus,
+  isReadonly = false,
+  testID,
+}) => {
+  const { styles } = useStyles(createStyles, {});
+  const currentCurrency = useSelector(selectCurrentCurrency);
+  const conversionRate = useSelector(selectConversionRate);
+
+  const formattedBalance = tokenSymbol && tokenBalance ? `${tokenBalance} ${tokenSymbol}` : undefined;
+  const formattedAddress = tokenAddress ? formatAddress(tokenAddress) : undefined;
+
+  const subtitle = formattedBalance ?? formattedAddress;
+
+  // Convert token amount to fiat value using actual conversion rate
+  const weiAmount = toWei(value ?? '0');
+  const fiatValue = conversionRate
+    ? weiToFiat(weiAmount, conversionRate, currentCurrency)
+    : undefined;
+
+  return (
+    <Box style={styles.container}>
+      <Box style={styles.content} gap={4}>
+        <Box style={styles.row}>
+          <Box style={styles.amountContainer}>
+            <Input
+              value={value}
+              style={styles.input}
+              isReadonly={isReadonly}
+              autoFocus={autoFocus}
+              placeholder="0"
+              testID={`${testID}-input`}
+            />
+          </Box>
+          <Token
+            symbol={tokenSymbol}
+            iconUrl={tokenIconUrl}
+            networkImageSource={networkImageSource}
+            networkName={networkName}
+            testID={testID}
+          />
+        </Box>
+        <Box style={styles.row}>
+          {fiatValue && (
+            <Text color={TextColor.Alternative}>
+              {fiatValue}
+            </Text>
+          )}
+          {subtitle && (
+            <Text color={TextColor.Alternative}>
+              {subtitle}
+            </Text>
+          )}
+        </Box>
+      </Box>
+    </Box>
+  );
+};
diff --git a/app/components/UI/Bridge/index.js b/app/components/UI/Bridge/index.js
deleted file mode 100644
index f6c1285f5a9..00000000000
--- a/app/components/UI/Bridge/index.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import React from 'react';
-import {
-  StyleSheet,
-  View,
-} from 'react-native';
-import ScreenView from '../../Base/ScreenView';
-import { useTheme } from '../../../util/theme';
-import Text from '../../../component-library/components/Texts/Text';
-
-const createStyles = (colors) =>
-  StyleSheet.create({
-    container: { backgroundColor: colors.background.default },
-    content: {
-      flexGrow: 1,
-      justifyContent: 'center',
-    },
-  });
-
-const BridgeView = () => {
-  const { colors } = useTheme();
-  const styles = createStyles(colors);
-
-  return (
-    <ScreenView
-      style={styles.container}
-      contentContainerStyle={styles.screen}
-      keyboardShouldPersistTaps="handled"
-    >
-      <View style={styles.content}>
-        <Text>Bridge</Text>
-      </View>
-    </ScreenView>
-  );
-};
-
-export default BridgeView;
diff --git a/app/components/UI/Bridge/index.test.tsx b/app/components/UI/Bridge/index.test.tsx
index 5cc5a3f0ce0..2e5f3d7afa5 100644
--- a/app/components/UI/Bridge/index.test.tsx
+++ b/app/components/UI/Bridge/index.test.tsx
@@ -1,23 +1,11 @@
-import React from 'react';
-import renderWithProvider, {
-  DeepPartial,
-} from '../../../util/test/renderWithProvider';
+import { renderScreen, DeepPartial } from '../../../util/test/renderWithProvider';
 import { backgroundState } from '../../../util/test/initial-root-state';
 import { RootState } from '../../../reducers';
-import BridgeView from './';
-
-jest.mock('@react-navigation/native', () => {
-  const actualNav = jest.requireActual('@react-navigation/native');
-  return {
-    ...actualNav,
-    useNavigation: () => ({
-      setOptions: jest.fn(),
-      pop: jest.fn(),
-      navigate: jest.fn(),
-    }),
-    useRoute: () => ({}),
-  };
-});
+import BridgeView from '../Bridge';
+import { fireEvent } from '@testing-library/react-native';
+import Routes from '../../../constants/navigation/Routes';
+import { BridgeState } from '../../../core/redux/slices/bridge';
+import { Hex } from '@metamask/utils';
 
 jest.mock('../../../core/Engine', () => ({
   context: {
@@ -37,11 +25,325 @@ const mockInitialState: DeepPartial<RootState> = {
   },
 };
 
+const mockNavigate = jest.fn();
+jest.mock('@react-navigation/native', () => {
+  const actualNav = jest.requireActual('@react-navigation/native');
+  return {
+    ...actualNav,
+    useNavigation: () => ({
+      navigate: mockNavigate,
+      setOptions: jest.fn(),
+    }),
+  };
+});
+
+// Mock useLatestBalance hook
+jest.mock('./useLatestBalance', () => ({
+  useLatestBalance: jest.fn().mockImplementation((token, chainId) => {
+    if (!token.address || !chainId) return undefined;
+
+    // Need to do this due to this error: "The module factory of `jest.mock()` is not allowed to reference any out-of-scope variables.""
+    const actualEthers = jest.requireActual('ethers');
+
+    return {
+      displayBalance: '2.0',
+      atomicBalance: actualEthers.BigNumber.from('2000000000000000000'), // 2 ETH
+    };
+  }),
+}));
+
 describe('BridgeView', () => {
+  const initialState = {
+    engine: {
+      backgroundState: {
+        NetworkController: {
+          selectedNetworkClientId: '1',
+          networkConfigurations: {
+            '0x1': {
+              chainId: '0x1',
+              ticker: 'ETH',
+              nickname: 'Ethereum Mainnet',
+            },
+          },
+          providerConfig: {
+            chainId: '0x1',
+          },
+        },
+        MultichainNetworkController: {
+          isEvmSelected: true,
+          selectedMultichainNetworkChainId: undefined,
+          multichainNetworkConfigurationsByChainId: {},
+        },
+        AccountTrackerController: {
+          accounts: {
+            '0x1234567890123456789012345678901234567890': {
+              balance: '0x0',
+            },
+          },
+        },
+        AccountsController: {
+          internalAccounts: {
+            selectedAccount: 'account1',
+            accounts: {
+              account1: {
+                id: 'account1',
+                address: '0x1234567890123456789012345678901234567890',
+                name: 'Account 1',
+                balance: '0x0',
+              },
+            },
+          },
+        },
+        CurrencyRateController: {
+          currentCurrency: 'USD',
+          currencyRates: {
+            ETH: {
+              conversionRate: 2000,
+            },
+          },
+        },
+        PreferencesController: {
+          ipfsGateway: 'https://dweb.link/ipfs/',
+        },
+      },
+    },
+    bridge: {
+      sourceToken: {
+        symbol: 'ETH',
+        decimals: 18,
+        address: '0x0000000000000000000000000000000000000000',
+        image: 'https://example.com/image.png',
+        chainId: '0x1' as Hex,
+      },
+      destToken: undefined,
+      sourceAmount: undefined,
+      destAmount: undefined,
+      sourceChainId: '0x1' as const,
+      destChainId: undefined,
+    } as BridgeState,
+  };
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
   it('renders', async () => {
-    const { getByText } = renderWithProvider(<BridgeView />, {
-      state: mockInitialState,
+    const { getByText } = renderScreen(
+      BridgeView,
+      {
+        name: Routes.BRIDGE,
+      },
+      { state: mockInitialState },
+    );
+    expect(getByText('Select amount')).toBeDefined();
+  });
+
+  it('should open BridgeTokenSelector when clicking source token', async () => {
+    const { findByText } = renderScreen(
+      BridgeView,
+      {
+        name: Routes.BRIDGE,
+      },
+      { state: initialState },
+    );
+
+    // Find and click the token button
+    const tokenButton = await findByText('ETH');
+    expect(tokenButton).toBeTruthy();
+    fireEvent.press(tokenButton);
+
+    // Verify navigation to BridgeTokenSelector
+    expect(mockNavigate).toHaveBeenCalledWith(Routes.MODAL.ROOT_MODAL_FLOW, {
+      screen: Routes.SHEET.BRIDGE_TOKEN_SELECTOR,
+      params: {},
+    });
+  });
+
+  it('should open BridgeTokenSelector when clicking destination token area', async () => {
+    const { getByTestId } = renderScreen(
+      BridgeView,
+      {
+        name: Routes.BRIDGE,
+      },
+      { state: initialState },
+    );
+
+    // Find and click the destination token area
+    const destTokenArea = getByTestId('dest-token-area');
+    expect(destTokenArea).toBeTruthy();
+
+    fireEvent.press(destTokenArea);
+
+    // Verify navigation to BridgeTokenSelector
+    expect(mockNavigate).toHaveBeenCalledWith(Routes.MODAL.ROOT_MODAL_FLOW, {
+      screen: Routes.SHEET.BRIDGE_TOKEN_SELECTOR,
+      params: {},
+    });
+  });
+
+  it('should update source token amount when typing', () => {
+    const { getByTestId, getByText } = renderScreen(
+      BridgeView,
+      {
+        name: Routes.BRIDGE,
+      },
+      { state: initialState },
+    );
+
+    // Press number buttons to input 1.5
+    fireEvent.press(getByText('9'));
+    fireEvent.press(getByText('.'));
+    fireEvent.press(getByText('5'));
+
+    // Verify the input value is updated
+    const input = getByTestId('source-token-area-input');
+    expect(input.props.value).toBe('9.5');
+  });
+
+  it('should display source token symbol and balance', () => {
+    const stateWithAmount = {
+      ...initialState,
+      bridge: {
+        ...initialState.bridge,
+        sourceAmount: '1.5',
+      },
+    };
+
+    const { getByText, getByTestId } = renderScreen(
+      BridgeView,
+      {
+        name: Routes.BRIDGE,
+      },
+      { state: stateWithAmount },
+    );
+
+    // Verify token symbol is displayed
+    expect(getByText('ETH')).toBeTruthy();
+
+    // Verify token amount is displayed
+    const input = getByTestId('source-token-area-input');
+    expect(input.props.value).toBe('1.5');
+
+    // Verify fiat value is displayed (1.5 ETH * $2000 = $3000)
+    expect(getByText('$3000')).toBeTruthy();
+  });
+
+  describe('Bottom Content', () => {
+    it('should show "Select amount" when no amount is entered', () => {
+      const { getByText } = renderScreen(
+        BridgeView,
+        {
+          name: Routes.BRIDGE,
+        },
+        { state: initialState },
+      );
+
+      expect(getByText('Select amount')).toBeTruthy();
+    });
+
+    it('should show "Select amount" when amount is zero', () => {
+      const stateWithZeroAmount = {
+        ...initialState,
+        bridge: {
+          ...initialState.bridge,
+          sourceAmount: '0',
+        },
+      };
+
+      const { getByText } = renderScreen(
+        BridgeView,
+        {
+          name: Routes.BRIDGE,
+        },
+        { state: stateWithZeroAmount },
+      );
+
+      expect(getByText('Select amount')).toBeTruthy();
+    });
+
+    it('should show "Insufficient balance" when amount exceeds balance', () => {
+      const stateWithHighAmount = {
+        ...initialState,
+        bridge: {
+          ...initialState.bridge,
+          sourceAmount: '3.0', // Balance is mocked at 2.0 ETH
+        },
+      };
+
+      const { getByText } = renderScreen(
+        BridgeView,
+        {
+          name: Routes.BRIDGE,
+        },
+        { state: stateWithHighAmount },
+      );
+
+      expect(getByText('Insufficient balance')).toBeTruthy();
+    });
+
+    it('should show Continue button and Terms link when amount is valid', () => {
+      const stateWithValidAmount = {
+        ...initialState,
+        bridge: {
+          ...initialState.bridge,
+          sourceAmount: '1.0', // Less than balance of 2.0 ETH
+        },
+      };
+
+      const { getByText } = renderScreen(
+        BridgeView,
+        {
+          name: Routes.BRIDGE,
+        },
+        { state: stateWithValidAmount },
+      );
+
+      expect(getByText('Continue')).toBeTruthy();
+      expect(getByText('Terms & Conditions')).toBeTruthy();
+    });
+
+    it('should handle Continue button press', () => {
+      const stateWithValidAmount = {
+        ...initialState,
+        bridge: {
+          ...initialState.bridge,
+          sourceAmount: '1.0',
+        },
+      };
+
+      const { getByText } = renderScreen(
+        BridgeView,
+        {
+          name: Routes.BRIDGE,
+        },
+        { state: stateWithValidAmount },
+      );
+
+      const continueButton = getByText('Continue');
+      fireEvent.press(continueButton);
+    });
+
+    it('should handle Terms & Conditions press', () => {
+      const stateWithValidAmount = {
+        ...initialState,
+        bridge: {
+          ...initialState.bridge,
+          sourceAmount: '1.0',
+        },
+      };
+
+      const { getByText } = renderScreen(
+        BridgeView,
+        {
+          name: Routes.BRIDGE,
+        },
+        { state: stateWithValidAmount },
+      );
+
+      const termsButton = getByText('Terms & Conditions');
+      fireEvent.press(termsButton);
+
+      // TODO: Add expectations once Terms navigation is implemented
     });
-    expect(getByText('Bridge')).toBeDefined();
   });
 });
diff --git a/app/components/UI/Bridge/index.tsx b/app/components/UI/Bridge/index.tsx
new file mode 100644
index 00000000000..584c09851ed
--- /dev/null
+++ b/app/components/UI/Bridge/index.tsx
@@ -0,0 +1,266 @@
+import React, { useEffect, useMemo } from 'react';
+import { StyleSheet, TouchableOpacity } from 'react-native';
+import { useSelector, useDispatch } from 'react-redux';
+import ScreenView from '../../Base/ScreenView';
+import Keypad from '../../Base/Keypad';
+import { TokenInputArea } from './TokenInputArea';
+import Button, { ButtonVariants } from '../../../component-library/components/Buttons/Button';
+import { useStyles } from '../../../component-library/hooks';
+import { Theme } from '../../../util/theme/models';
+import { Box } from '../Box/Box';
+import { FlexDirection, JustifyContent, AlignItems } from '../Box/box.types';
+import Text, { TextColor } from '../../../component-library/components/Texts/Text';
+import Icon, { IconName, IconSize } from '../../../component-library/components/Icons/Icon';
+import images from '../../../images/image-icons';
+import { SupportedCaipChainId } from '@metamask/multichain-network-controller';
+import { Hex } from '@metamask/utils';
+import { isTestNet, getTestNetImageByChainId, isMainnetByChainId, isLineaMainnetByChainId } from '../../../util/networks';
+import { PopularList, UnpopularNetworkList, CustomNetworkImgMapping } from '../../../util/networks/customNetworks';
+import { useLatestBalance } from './useLatestBalance';
+import {
+  selectSourceAmount,
+  selectDestAmount,
+  selectSourceChainId,
+  selectDestChainId,
+  selectSourceToken,
+  selectDestToken,
+  setSourceAmount,
+  resetBridgeState,
+  switchTokens,
+} from '../../../core/redux/slices/bridge';
+import { ethers } from 'ethers';
+import { useNavigation, useRoute } from '@react-navigation/native';
+import { getBridgeNavbar } from '../Navbar';
+import { useTheme } from '../../../util/theme';
+import { strings } from '../../../../locales/i18n';
+
+const getNetworkImage = (chainId: SupportedCaipChainId | Hex) => {
+  if (isTestNet(chainId)) return getTestNetImageByChainId(chainId);
+  if (isMainnetByChainId(chainId)) return images.ETHEREUM;
+  if (isLineaMainnetByChainId(chainId)) return images['LINEA-MAINNET'];
+
+  if (CustomNetworkImgMapping[chainId as Hex]) {
+    return CustomNetworkImgMapping[chainId as Hex];
+  }
+
+  const unpopularNetwork = UnpopularNetworkList.find(
+    (networkConfig) => networkConfig.chainId === chainId,
+  );
+
+  const popularNetwork = PopularList.find(
+    (networkConfig) => networkConfig.chainId === chainId,
+  );
+
+  const network = unpopularNetwork || popularNetwork;
+  if (network) {
+    return network.rpcPrefs.imageSource;
+  }
+
+  return undefined;
+};
+
+const createStyles = (params: { theme: Theme }) => {
+  const { theme } = params;
+  return StyleSheet.create({
+    content: {
+      flexGrow: 1,
+    },
+    screen: {
+      flexGrow: 1,
+    },
+    inputsContainer: {
+      paddingVertical: 12,
+    },
+    buttonContainer: {
+      width: '100%',
+    },
+    button: {
+      width: '100%',
+    },
+    bottomSection: {
+      padding: 24,
+    },
+    arrowContainer: {
+      position: 'relative',
+      alignItems: 'center',
+      height: 1,
+      backgroundColor: theme.colors.border.muted,
+    },
+    arrowCircle: {
+      position: 'absolute',
+      top: -16,
+      backgroundColor: theme.colors.background.alternative,
+      width: 32,
+      height: 32,
+      borderRadius: 16,
+      display: 'flex',
+      alignItems: 'center',
+      justifyContent: 'center',
+    },
+    arrow: {
+      fontSize: 20,
+      color: theme.colors.text.default,
+      lineHeight: 20,
+      height: 20,
+      includeFontPadding: false,
+      textAlignVertical: 'center',
+      paddingTop: 1,
+    },
+  });
+};
+
+// We get here through handleBridgeNavigation in AssetOverview and WalletActions
+const BridgeView = () => {
+  const { styles } = useStyles(createStyles, {});
+  const dispatch = useDispatch();
+  const navigation = useNavigation();
+  const route = useRoute();
+  const { colors } = useTheme();
+
+  // Bridge state from Redux
+  const sourceToken = useSelector(selectSourceToken);
+  const destToken = useSelector(selectDestToken);
+  const sourceAmount = useSelector(selectSourceAmount);
+  const destAmount = useSelector(selectDestAmount);
+  const sourceChainId = useSelector(selectSourceChainId);
+  const destChainId = useSelector(selectDestChainId);
+
+  const sourceBalance = useLatestBalance({
+    address: sourceToken?.address,
+    decimals: sourceToken?.decimals,
+  }, sourceChainId);
+
+  const hasInsufficientBalance = useMemo(() => {
+    if (!sourceAmount || !sourceBalance?.atomicBalance || !sourceToken?.decimals) {
+      return false;
+    }
+
+    const sourceAmountAtomic = ethers.utils.parseUnits(sourceAmount, sourceToken.decimals);
+    return sourceAmountAtomic.gt(sourceBalance.atomicBalance);
+  }, [sourceAmount, sourceBalance?.atomicBalance, sourceToken?.decimals]);
+
+  // Reset bridge state when component unmounts
+  useEffect(() => () => {
+      dispatch(resetBridgeState());
+    }, [dispatch]);
+
+  useEffect(() => {
+    navigation.setOptions(getBridgeNavbar(navigation, route, colors));
+  }, [navigation, route, colors]);
+
+  const handleKeypadChange = ({ value }: { value: string; valueAsNumber: number; pressedKey: string }) => {
+    dispatch(setSourceAmount(value || undefined));
+  };
+
+  const handleContinue = () => {
+    // TODO: Implement bridge transaction with source and destination amounts
+  };
+
+  const handleTermsPress = () => {
+    // TODO: Implement terms and conditions navigation
+  };
+
+  const handleArrowPress = () => {
+    if (destChainId && destToken) {
+      dispatch(switchTokens());
+    }
+  };
+
+  const renderBottomContent = () => {
+    if (!sourceAmount || (sourceToken?.decimals && ethers.utils.parseUnits(sourceAmount, sourceToken.decimals).isZero())) {
+      return (
+        <Text color={TextColor.Alternative}>
+          Select amount
+        </Text>
+      );
+    }
+
+    if (hasInsufficientBalance) {
+      return (
+        <Text color={TextColor.Error}>
+          Insufficient balance
+        </Text>
+      );
+    }
+
+    return (
+      <>
+        <Button
+          variant={ButtonVariants.Primary}
+          label={strings('bridge.continue')}
+          onPress={handleContinue}
+          style={styles.button}
+        />
+        <Button
+          variant={ButtonVariants.Link}
+          label={<Text color={TextColor.Alternative}>{strings('bridge.terms_and_conditions')}</Text>}
+          onPress={handleTermsPress}
+        />
+      </>
+    );
+  };
+
+  return (
+    // Need this to be full height of screen
+    // @ts-expect-error The type is incorrect, this will work
+    <ScreenView contentContainerStyle={styles.screen}>
+      <Box
+        style={styles.content}
+        flexDirection={FlexDirection.Column}
+        justifyContent={JustifyContent.spaceBetween}
+      >
+        <Box style={styles.inputsContainer} gap={8}>
+          <TokenInputArea
+            value={sourceAmount}
+            tokenSymbol={sourceToken?.symbol}
+            tokenBalance={sourceBalance?.displayBalance}
+            tokenIconUrl={sourceToken?.image ? { uri: sourceToken.image } : undefined}
+            tokenAddress={sourceToken?.address}
+            networkImageSource={getNetworkImage(sourceChainId)}
+            autoFocus
+            isReadonly
+            testID="source-token-area"
+          />
+          <Box style={styles.arrowContainer}>
+            <TouchableOpacity
+              onPress={handleArrowPress}
+              disabled={!destChainId || !destToken}
+              style={styles.arrowCircle}
+            >
+              <Text style={styles.arrow}>↓</Text>
+            </TouchableOpacity>
+          </Box>
+          <TokenInputArea
+            value={destAmount}
+            tokenSymbol={destToken?.symbol}
+            tokenAddress={destToken?.address}
+            tokenIconUrl={destToken?.image ? { uri: destToken.image } : undefined}
+            networkImageSource={destChainId ? getNetworkImage(destChainId) : undefined}
+            isReadonly
+            testID="dest-token-area"
+          />
+        </Box>
+        <Box style={styles.bottomSection}>
+          <Keypad
+            value={sourceAmount}
+            onChange={handleKeypadChange}
+            currency={sourceToken?.symbol || 'ETH'}
+            decimals={sourceToken?.decimals || 18}
+            deleteIcon={<Icon name={IconName.ArrowLeft} size={IconSize.Lg} />}
+          />
+          <Box
+            style={styles.buttonContainer}
+            flexDirection={FlexDirection.Column}
+            justifyContent={JustifyContent.center}
+            alignItems={AlignItems.center}
+            gap={12}
+          >
+            {renderBottomContent()}
+          </Box>
+        </Box>
+      </Box>
+    </ScreenView>
+  );
+};
+
+export default BridgeView;
diff --git a/app/components/UI/Bridge/types.ts b/app/components/UI/Bridge/types.ts
new file mode 100644
index 00000000000..ff8cd98cb64
--- /dev/null
+++ b/app/components/UI/Bridge/types.ts
@@ -0,0 +1,9 @@
+import { CaipChainId } from '@metamask/utils';
+
+export interface BridgeToken {
+  address: string;
+  symbol: string;
+  image: string;
+  decimals: number;
+  chainId: CaipChainId;
+}
diff --git a/app/components/UI/Bridge/useLatestBalance.test.tsx b/app/components/UI/Bridge/useLatestBalance.test.tsx
new file mode 100644
index 00000000000..9c17671f05b
--- /dev/null
+++ b/app/components/UI/Bridge/useLatestBalance.test.tsx
@@ -0,0 +1,182 @@
+import { renderHookWithProvider } from '../../../util/test/renderWithProvider';
+import { useLatestBalance } from './useLatestBalance';
+import { getProviderByChainId } from '../../../util/notifications/methods/common';
+import { BigNumber, constants } from 'ethers';
+import { waitFor } from '@testing-library/react-native';
+
+// Mock dependencies
+jest.mock('../../../util/notifications/methods/common', () => ({
+  getProviderByChainId: jest.fn(),
+}));
+
+// Mock ethers contract
+jest.mock('ethers', () => {
+  const actual = jest.requireActual('ethers');
+  return {
+    ...actual,
+    Contract: jest.fn().mockImplementation(() => ({
+      balanceOf: jest.fn().mockResolvedValue(actual.BigNumber.from('1000000')),
+    })),
+  };
+});
+
+describe('useLatestBalance', () => {
+  const mockProvider = {
+    getBalance: jest.fn().mockResolvedValue(BigNumber.from('1000000000000000000')),
+    getNetwork: jest.fn().mockResolvedValue({ chainId: 1 }),
+  };
+
+  const initialState = {
+    engine: {
+      backgroundState: {
+        NetworkController: {
+          selectedNetworkClientId: '1',
+          networkConfigurations: {
+            '0x1': {
+              chainId: '0x1',
+              ticker: 'ETH',
+              nickname: 'Ethereum Mainnet',
+            },
+          },
+          providerConfig: {
+            chainId: '0x1',
+          },
+        },
+        MultichainNetworkController: {
+          isEvmSelected: true,
+          selectedMultichainNetworkChainId: undefined,
+          multichainNetworkConfigurationsByChainId: {},
+        },
+        AccountTrackerController: {
+          accounts: {
+            '0x1234567890123456789012345678901234567890': {
+              balance: '0x0',
+            },
+          },
+        },
+        AccountsController: {
+          internalAccounts: {
+            selectedAccount: 'account1',
+            accounts: {
+              account1: {
+                id: 'account1',
+                address: '0x1234567890123456789012345678901234567890',
+                name: 'Account 1',
+                balance: '0x0',
+              },
+            },
+          },
+        },
+        CurrencyRateController: {
+          currentCurrency: 'USD',
+          currencyRates: {
+            ETH: {
+              conversionRate: 2000,
+            },
+          },
+        },
+        PreferencesController: {
+          ipfsGateway: 'https://dweb.link/ipfs/',
+        },
+      },
+    },
+  };
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+    (getProviderByChainId as jest.Mock).mockReturnValue(mockProvider);
+  });
+
+  it('should fetch native token balance when token address is zero address', async () => {
+    const { result } = renderHookWithProvider(
+      () =>
+        useLatestBalance(
+          {
+            address: constants.AddressZero,
+            decimals: 18,
+          },
+          '0x1',
+        ),
+      { state: initialState },
+    );
+
+    await waitFor(() => {
+      expect(mockProvider.getBalance).toHaveBeenCalledWith('0x1234567890123456789012345678901234567890');
+      expect(result.current).toEqual({
+        displayBalance: '1.0',
+        atomicBalance: BigNumber.from('1000000000000000000'),
+      });
+    });
+  });
+
+  it('should fetch ERC20 token balance', async () => {
+    const { result } = renderHookWithProvider(
+      () =>
+        useLatestBalance(
+          {
+            address: '0x1234567890123456789012345678901234567890',
+            decimals: 6,
+          },
+          '0x1',
+        ),
+      { state: initialState },
+    );
+
+    await waitFor(() => {
+      expect(result.current).toEqual({
+        displayBalance: '1.0',
+        atomicBalance: BigNumber.from('1000000'),
+      });
+    });
+  });
+
+  it('should not fetch balance when chainId is CAIP format', async () => {
+    const { result } = renderHookWithProvider(
+      () =>
+        useLatestBalance(
+          {
+            address: constants.AddressZero,
+            decimals: 18,
+          },
+          'eip155:1',
+        ),
+      { state: initialState },
+    );
+
+    expect(mockProvider.getBalance).not.toHaveBeenCalled();
+    expect(result.current).toBeUndefined();
+  });
+
+  it('should not fetch balance when chainId does not match current chain', async () => {
+    const { result } = renderHookWithProvider(
+      () =>
+        useLatestBalance(
+          {
+            address: constants.AddressZero,
+            decimals: 18,
+          },
+          '0x2',
+        ),
+      { state: initialState },
+    );
+
+    expect(mockProvider.getBalance).not.toHaveBeenCalled();
+    expect(result.current).toBeUndefined();
+  });
+
+  it('should not fetch balance when token address is missing', async () => {
+    const { result } = renderHookWithProvider(
+      () =>
+        useLatestBalance(
+          {
+            decimals: 18,
+          },
+          '0x1',
+        ),
+      { state: initialState },
+    );
+
+    expect(mockProvider.getBalance).not.toHaveBeenCalled();
+    expect(result.current).toBeUndefined();
+  });
+});
diff --git a/app/components/UI/Bridge/useLatestBalance.ts b/app/components/UI/Bridge/useLatestBalance.ts
new file mode 100644
index 00000000000..cf006885c41
--- /dev/null
+++ b/app/components/UI/Bridge/useLatestBalance.ts
@@ -0,0 +1,90 @@
+import { useEffect, useState, useCallback } from 'react';
+import { type Hex, type CaipChainId, isCaipChainId } from '@metamask/utils';
+import { abiERC20 } from '@metamask/metamask-eth-abis';
+import { Web3Provider } from '@ethersproject/providers';
+import { formatUnits, getAddress } from 'ethers/lib/utils';
+import { useSelector } from 'react-redux';
+import { selectSelectedInternalAccountFormattedAddress } from '../../../selectors/accountsController';
+import { selectChainId } from '../../../selectors/networkController';
+import { getProviderByChainId } from '../../../util/notifications/methods/common';
+import { BigNumber, constants, Contract } from 'ethers';
+
+export async function fetchAtomicTokenBalance(
+  address: string,
+  userAddress: string,
+  web3Provider: Web3Provider,
+): Promise<BigNumber> {
+  const tokenContract = new Contract(address, abiERC20, web3Provider);
+  const tokenBalancePromise = tokenContract
+    ? tokenContract.balanceOf(userAddress)
+    : Promise.resolve();
+  return await tokenBalancePromise;
+}
+
+export const fetchAtomicBalance = async (
+  web3Provider: Web3Provider,
+  selectedAddress: string,
+  tokenAddress: string,
+  chainId: Hex,
+): Promise<BigNumber | undefined> => {
+  if (tokenAddress && chainId) {
+    if (tokenAddress === constants.AddressZero) {
+      return await web3Provider.getBalance(getAddress(selectedAddress));
+    }
+    return await fetchAtomicTokenBalance(tokenAddress, selectedAddress, web3Provider);
+  }
+  return undefined;
+};
+
+interface Balance {
+  displayBalance: string;
+  atomicBalance: BigNumber;
+}
+
+/**
+ * A hook that fetches and returns the latest balance for a given token.
+ * @param token - The token object containing address, decimals, and symbol.
+ * @param chainId - The chain ID to be used for fetching the balance. Optional.
+ * @returns An object containing the the balance as a non-atomic decimal string and the atomic balance as a BigNumber.
+ */
+export const useLatestBalance = (
+  token: {
+    address?: string;
+    decimals?: number;
+  },
+  chainId?: Hex | CaipChainId,
+) => {
+  const [balance, setBalance] = useState<Balance | undefined>(undefined);
+  const selectedAddress = useSelector(selectSelectedInternalAccountFormattedAddress);
+  const currentChainId = useSelector(selectChainId);
+
+  const handleFetchAtomicBalance = useCallback(async () => {
+    if (
+      token.address &&
+      token.decimals &&
+      chainId && !isCaipChainId(chainId) &&
+      currentChainId === chainId &&
+      selectedAddress
+    ) {
+      const web3Provider = getProviderByChainId(currentChainId);
+      const atomicBalance = await fetchAtomicBalance(
+        web3Provider,
+        selectedAddress,
+        token.address,
+        chainId,
+      );
+      if (atomicBalance && token.decimals) {
+        setBalance({
+          displayBalance: formatUnits(atomicBalance, token.decimals),
+          atomicBalance,
+        });
+      }
+    }
+  }, [token.address, token.decimals, chainId, currentChainId, selectedAddress]);
+
+  useEffect(() => {
+    handleFetchAtomicBalance();
+  }, [handleFetchAtomicBalance]);
+
+  return balance;
+};
diff --git a/app/components/UI/Navbar/index.js b/app/components/UI/Navbar/index.js
index d8b8339a167..4d42866c8a8 100644
--- a/app/components/UI/Navbar/index.js
+++ b/app/components/UI/Navbar/index.js
@@ -1723,6 +1723,52 @@ export function getSwapsQuotesNavbar(navigation, route, themeColors) {
   };
 }
 
+export function getBridgeNavbar(navigation, route, themeColors) {
+  const innerStyles = StyleSheet.create({
+    headerButtonText: {
+      color: themeColors.primary.default,
+      fontSize: 14,
+      ...fontStyles.normal,
+    },
+    headerStyle: {
+      backgroundColor: themeColors.background.default,
+      shadowColor: importedColors.transparent,
+      elevation: 0,
+    },
+  });
+  const title = route.params?.title ?? 'Swap/Bridge';
+
+  const leftAction = () => navigation.pop();
+
+  return {
+    headerTitle: () => (
+      <NavbarTitle
+        title={title}
+        disableNetwork
+        showSelectedNetwork={false}
+        translate={false}
+      />
+    ),
+    headerLeft: () => (
+      <TouchableOpacity onPress={leftAction} style={styles.backButton}>
+        <Icon name={IconName.ArrowLeft} />
+      </TouchableOpacity>
+    ),
+    headerRight: () => (
+      // eslint-disable-next-line react/jsx-no-bind
+      <TouchableOpacity
+        onPress={() => navigation.dangerouslyGetParent()?.pop()}
+        style={styles.closeButton}
+      >
+        <Text style={innerStyles.headerButtonText}>
+          {strings('navigation.cancel')}
+        </Text>
+      </TouchableOpacity>
+    ),
+    headerStyle: innerStyles.headerStyle,
+  };
+}
+
 export function getFiatOnRampAggNavbar(
   navigation,
   { title, showBack = true, showCancel = true } = {},
diff --git a/app/constants/navigation/Routes.ts b/app/constants/navigation/Routes.ts
index 9f001197002..6615e1a9ab6 100644
--- a/app/constants/navigation/Routes.ts
+++ b/app/constants/navigation/Routes.ts
@@ -115,6 +115,7 @@ const Routes = {
     TOKEN_SORT: 'TokenSort',
     TOKEN_FILTER: 'TokenFilter',
     CHANGE_IN_SIMULATION_MODAL: 'ChangeInSimulationModal',
+    BRIDGE_TOKEN_SELECTOR: 'BridgeTokenSelector',
   },
   BROWSER: {
     HOME: 'BrowserTabHome',
@@ -137,6 +138,7 @@ const Routes = {
   ADD_NETWORK: 'AddNetwork',
   EDIT_NETWORK: 'EditNetwork',
   SWAPS: 'Swaps',
+  BRIDGE: 'Bridge',
   LOCK_SCREEN: 'LockScreen',
   CONFIRM_FLAT_PAGE: 'Confirmation Flat Page',
   CONFIRM_MODAL: 'Confirmation Modal',
diff --git a/app/core/redux/slices/bridge/index.test.ts b/app/core/redux/slices/bridge/index.test.ts
new file mode 100644
index 00000000000..eafa0101388
--- /dev/null
+++ b/app/core/redux/slices/bridge/index.test.ts
@@ -0,0 +1,178 @@
+import reducer, {
+  initialState,
+  setSourceAmount,
+  setDestAmount,
+  setSourceChainId,
+  setDestChainId,
+  resetBridgeState,
+  switchTokens,
+} from '.';
+import { SupportedCaipChainId } from '@metamask/multichain-network-controller';
+import { BridgeToken } from '../../../../components/UI/Bridge/types';
+
+describe('bridge slice', () => {
+  const mockToken: BridgeToken = {
+    address: '0x123',
+    symbol: 'ETH',
+    decimals: 18,
+    image: 'https://example.com/eth.png',
+    chainId: 'eip155:1' as SupportedCaipChainId,
+  };
+
+  const mockDestToken: BridgeToken = {
+    address: '0x456',
+    symbol: 'USDC',
+    decimals: 6,
+    image: 'https://example.com/usdc.png',
+    chainId: 'eip155:2' as SupportedCaipChainId,
+  };
+
+  describe('initial state', () => {
+    it('should have the correct initial state', () => {
+      expect(initialState).toEqual({
+        sourceAmount: undefined,
+        destAmount: undefined,
+        sourceChainId: '0x1',
+        destChainId: undefined,
+        sourceToken: undefined,
+        destToken: undefined,
+      });
+    });
+  });
+
+  describe('setSourceAmount', () => {
+    it('should set the source amount', () => {
+      const amount = '1.5';
+      const action = setSourceAmount(amount);
+      const state = reducer(initialState, action);
+
+      expect(state.sourceAmount).toBe(amount);
+    });
+
+    it('should set source amount to undefined', () => {
+      const action = setSourceAmount(undefined);
+      const state = reducer(initialState, action);
+
+      expect(state.sourceAmount).toBeUndefined();
+    });
+  });
+
+  describe('setDestAmount', () => {
+    it('should set the destination amount', () => {
+      const amount = '100';
+      const action = setDestAmount(amount);
+      const state = reducer(initialState, action);
+
+      expect(state.destAmount).toBe(amount);
+    });
+
+    it('should set dest amount to undefined', () => {
+      const action = setDestAmount(undefined);
+      const state = reducer(initialState, action);
+
+      expect(state.destAmount).toBeUndefined();
+    });
+  });
+
+  describe('setSourceChainId', () => {
+    it('should set the source chain ID', () => {
+      const chainId = 'eip155:2' as SupportedCaipChainId;
+      const action = setSourceChainId(chainId);
+      const state = reducer(initialState, action);
+
+      expect(state.sourceChainId).toBe(chainId);
+    });
+  });
+
+  describe('setDestChainId', () => {
+    it('should set the destination chain ID', () => {
+      const chainId = 'eip155:2' as SupportedCaipChainId;
+      const action = setDestChainId(chainId);
+      const state = reducer(initialState, action);
+
+      expect(state.destChainId).toBe(chainId);
+    });
+
+    it('should set dest chain ID to undefined', () => {
+      const action = setDestChainId(undefined);
+      const state = reducer(initialState, action);
+
+      expect(state.destChainId).toBeUndefined();
+    });
+  });
+
+  describe('resetBridgeState', () => {
+    it('should reset the state to initial state', () => {
+      const state = {
+        ...initialState,
+        sourceAmount: '1.5',
+        destAmount: '100',
+        sourceToken: mockToken,
+        destToken: mockDestToken,
+      };
+
+      const action = resetBridgeState();
+      const newState = reducer(state, action);
+
+      expect(newState).toEqual(initialState);
+    });
+  });
+
+  describe('switchTokens', () => {
+    it('should switch source and destination tokens when both are defined', () => {
+      const state = {
+        ...initialState,
+        sourceToken: mockToken,
+        destToken: mockDestToken,
+        sourceChainId: 'eip155:1' as SupportedCaipChainId,
+        destChainId: 'eip155:2' as SupportedCaipChainId,
+        sourceAmount: '1.5',
+        destAmount: '100',
+      };
+
+      const action = switchTokens();
+      const newState = reducer(state, action);
+
+      expect(newState.sourceToken).toEqual(mockDestToken);
+      expect(newState.destToken).toEqual(mockToken);
+      expect(newState.sourceChainId).toBe('eip155:2');
+      expect(newState.destChainId).toBe('eip155:1');
+      expect(newState.sourceAmount).toBeUndefined();
+      expect(newState.destAmount).toBeUndefined();
+    });
+
+    it('should not switch tokens when destination token is undefined', () => {
+      const state = {
+        ...initialState,
+        sourceToken: mockToken,
+        destToken: undefined,
+        sourceChainId: 'eip155:1' as SupportedCaipChainId,
+        destChainId: undefined,
+        sourceAmount: '1.5',
+        destAmount: undefined,
+      };
+
+      const action = switchTokens();
+      const newState = reducer(state, action);
+
+      expect(newState).toEqual(state);
+    });
+
+    it('should not switch tokens when destination chain is undefined', () => {
+      const state = {
+        ...initialState,
+        sourceToken: mockToken,
+        destToken: mockDestToken,
+        sourceChainId: 'eip155:1' as SupportedCaipChainId,
+        destChainId: undefined,
+        sourceAmount: '1.5',
+        destAmount: '100',
+      };
+
+      const action = switchTokens();
+      const newState = reducer(state, action);
+
+      expect(newState).toEqual(state);
+    });
+  });
+});
diff --git a/app/core/redux/slices/bridge/index.ts b/app/core/redux/slices/bridge/index.ts
new file mode 100644
index 00000000000..12395727817
--- /dev/null
+++ b/app/core/redux/slices/bridge/index.ts
@@ -0,0 +1,136 @@
+import { PayloadAction, createSlice } from '@reduxjs/toolkit';
+import { RootState } from '../../../../reducers';
+import { SupportedCaipChainId } from '@metamask/multichain-network-controller';
+import { Hex, isCaipChainId } from '@metamask/utils';
+import { ethers } from 'ethers';
+import { createSelector } from 'reselect';
+import { selectTokens } from '../../../../selectors/tokensController';
+import { getNativeSwapsToken } from '@metamask/swaps-controller/dist/swapsUtil';
+import { BridgeToken } from '../../../../components/UI/Bridge/types';
+import { selectChainId } from '../../../../selectors/networkController';
+
+export interface BridgeState {
+  sourceAmount: string | undefined;
+  destAmount: string | undefined;
+  sourceChainId: SupportedCaipChainId | Hex;
+  destChainId: SupportedCaipChainId | Hex | undefined;
+  sourceToken: BridgeToken | undefined;
+  destToken: BridgeToken | undefined;
+}
+
+export const initialState: BridgeState = {
+  sourceAmount: undefined,
+  destAmount: undefined,
+  sourceChainId: '0x1',
+  destChainId: undefined,
+  sourceToken: undefined,
+  destToken: undefined,
+};
+
+const name = 'bridge';
+
+const slice = createSlice({
+  name,
+  initialState,
+  reducers: {
+    setSourceAmount: (state, action: PayloadAction<string | undefined>) => {
+      state.sourceAmount = action.payload;
+    },
+    setDestAmount: (state, action: PayloadAction<string | undefined>) => {
+      state.destAmount = action.payload;
+    },
+    setSourceChainId: (state, action: PayloadAction<SupportedCaipChainId | Hex>) => {
+      state.sourceChainId = action.payload;
+    },
+    setDestChainId: (state, action: PayloadAction<SupportedCaipChainId | Hex | undefined>) => {
+      state.destChainId = action.payload;
+    },
+    resetBridgeState: () => initialState,
+    switchTokens: (state) => {
+      // Don't switch if destination chain or token is undefined
+      if (!state.destChainId || !state.destToken) {
+        return;
+      }
+
+      // Switch tokens
+      const tempToken = state.sourceToken;
+      state.sourceToken = state.destToken;
+      state.destToken = tempToken;
+
+      // Switch chain IDs
+      const tempChainId = state.sourceChainId;
+      state.sourceChainId = state.destChainId;
+      state.destChainId = tempChainId;
+
+      // Reset amounts
+      state.sourceAmount = undefined;
+      state.destAmount = undefined;
+    },
+  },
+});
+
+const { actions, reducer } = slice;
+
+export default reducer;
+
+// Base selectors
+const selectBridgeState = (state: RootState) => state[name];
+const selectTokensList = selectTokens;
+
+// Derived selectors using createSelector
+export const selectSourceAmount = createSelector(
+  selectBridgeState,
+  (bridgeState) => bridgeState.sourceAmount,
+);
+
+export const selectDestAmount = createSelector(
+  selectBridgeState,
+  (bridgeState) => bridgeState.destAmount,
+);
+
+export const selectSourceChainId = createSelector(
+  selectChainId,
+  (chainId) => chainId,
+);
+
+export const selectDestChainId = createSelector(
+  selectBridgeState,
+  (bridgeState) => bridgeState.destChainId,
+);
+
+// Combined selectors for related state
+export const selectSourceToken = createSelector(
+  selectBridgeState,
+  selectTokensList,
+  (bridgeState, tokens) => {
+    const { sourceChainId } = bridgeState;
+    const sourceToken = !isCaipChainId(sourceChainId)
+      ? getNativeSwapsToken(sourceChainId)
+      : tokens.find((token) => token.address === ethers.constants.AddressZero);
+
+    if (!sourceToken) return undefined;
+
+    return {
+      address: sourceToken.address,
+      symbol: sourceToken.symbol,
+      image: 'iconUrl' in sourceToken ? sourceToken.iconUrl : '',
+      decimals: sourceToken.decimals,
+      chainId: sourceChainId as SupportedCaipChainId,
+    } as BridgeToken;
+  },
+);
+
+export const selectDestToken = createSelector(
+  selectBridgeState,
+  (bridgeState) => bridgeState.destToken,
+);
+
+// Actions
+export const {
+  setSourceAmount,
+  setDestAmount,
+  setSourceChainId,
+  setDestChainId,
+  resetBridgeState,
+  switchTokens,
+} = actions;
diff --git a/app/reducers/index.ts b/app/reducers/index.ts
index 990bc742032..3c5b47dfe73 100644
--- a/app/reducers/index.ts
+++ b/app/reducers/index.ts
@@ -30,6 +30,7 @@ import transactionMetricsReducer from '../core/redux/slices/transactionMetrics';
 import originThrottlingReducer from '../core/redux/slices/originThrottling';
 import notificationsAccountsProvider from '../core/redux/slices/notifications';
 import stakingReducer from '../core/redux/slices/staking';
+import bridgeReducer from '../core/redux/slices/bridge';
 ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
 import { MultichainSettingsState } from '../actions/multichain/state';
 import multichainReducer from './multichain';
@@ -123,6 +124,7 @@ export interface RootState {
   originThrottling: StateFromReducer<typeof originThrottlingReducer>;
   notifications: StateFromReducer<typeof notificationsAccountsProvider>;
   staking: StateFromReducer<typeof stakingReducer>;
+  bridge: StateFromReducer<typeof bridgeReducer>;
   ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
   multichainSettings: MultichainSettingsState;
   ///: END:ONLY_INCLUDE_IF
@@ -165,6 +167,7 @@ const rootReducer = combineReducers<RootState, any>({
   originThrottling: originThrottlingReducer,
   notifications: notificationsAccountsProvider,
   staking: stakingReducer,
+  bridge: bridgeReducer,
   ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
   multichainSettings: multichainReducer,
   ///: END:ONLY_INCLUDE_IF
diff --git a/app/util/test/initial-root-state.ts b/app/util/test/initial-root-state.ts
index d7e1c2b64da..ae76d7d11a6 100644
--- a/app/util/test/initial-root-state.ts
+++ b/app/util/test/initial-root-state.ts
@@ -5,6 +5,7 @@ import { initialState as initialSecurityState } from '../../reducers/security';
 import { initialState as initialInpageProvider } from '../../core/redux/slices/inpageProvider';
 import { initialState as transactionMetrics } from '../../core/redux/slices/transactionMetrics';
 import { initialState as originThrottling } from '../../core/redux/slices/originThrottling';
+import { initialState as initialBridgeState } from '../../core/redux/slices/bridge';
 import initialBackgroundState from './initial-background-state.json';
 import { userInitialState } from '../../reducers/user';
 import { initialNavigationState } from '../../reducers/navigation';
@@ -53,6 +54,7 @@ const initialRootState: RootState = {
   originThrottling,
   notifications: {},
   staking: initialStakingState,
+  bridge: initialBridgeState,
   ///: BEGIN:ONLY_INCLUDE_IF(keyring-snaps)
   multichainSettings: initialMultichainSettingsState,
   ///: END:ONLY_INCLUDE_IF
diff --git a/locales/languages/en.json b/locales/languages/en.json
index e166add6c1a..a8702a629c6 100644
--- a/locales/languages/en.json
+++ b/locales/languages/en.json
@@ -3717,5 +3717,9 @@
     "title": "Transactions just got smarter",
     "link": "Higher success rates",
     "description": " and MEV protection. Now on by default."
+  },
+  "bridge": {
+    "continue": "Continue",
+    "terms_and_conditions": "Terms & Conditions"
   }
 }
diff --git a/package.json b/package.json
index ec3df528342..728a7f59289 100644
--- a/package.json
+++ b/package.json
@@ -184,6 +184,7 @@
     "@metamask/keyring-snap-client": "^4.0.1",
     "@metamask/logging-controller": "^6.0.4",
     "@metamask/message-signing-snap": "^0.3.3",
+    "@metamask/metamask-eth-abis": "3.1.1",
     "@metamask/multichain-network-controller": "^0.1.1",
     "@metamask/network-controller": "^22.1.0",
     "@metamask/notification-services-controller": "^2.0.0",


Reviews:
user: login: micaelae
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-06T17:02:58Z
user: login: GustavoRSSilva
state: DISMISSED
author_association: NONE
submitted_at: 2025-03-07T15:04:56Z
user: login: micaelae
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-07T16:25:35Z
user: login: bfullam
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T17:25:41Z
user: login: micaelae
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-03-07T17:42:39Z
user: login: bfullam
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:49:30Z
user: login: amitabh94
body: Small question but overall LGTM!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T17:55:07Z
user: login: infiniteflower
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T18:00:18Z
user: login: amitabh94
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T18:06:51Z
user: login: tommasini
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T18:28:57Z
user: login: tommasini
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T18:29:43Z
user: login: infiniteflower
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T18:35:42Z
user: login: infiniteflower
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T18:36:30Z
user: login: infiniteflower
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T18:39:51Z
user: login: amitabh94
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:05:39Z
user: login: infiniteflower
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:09:55Z
user: login: infiniteflower
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:12:13Z
user: login: bfullam
state: DISMISSED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:13:39Z
user: login: infiniteflower
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:15:14Z
user: login: amitabh94
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-11T19:22:16Z

Review comments:
path: app/core/redux/slices/bridge/index.ts
user: login: amitabh94
body: Can we not have this state in the bridge controller state itself? Is there benefit to create this slice rather than get it from controller?
created_at: 2025-03-11T17:54:19Z
author_association: CONTRIBUTOR
path: app/core/redux/slices/bridge/index.ts
user: login: infiniteflower
body: My observation of controllers is the general pattern has been to leave the user inputs to the actual UI layer itself. The controller only cares about the request object that is submitted to the API.

The UI input values may or may not be fully and correctly formed.
created_at: 2025-03-11T18:00:18Z
author_association: CONTRIBUTOR
path: app/core/redux/slices/bridge/index.ts
user: login: amitabh94
body: Do we then want to have local component state rather than global redux state?
created_at: 2025-03-11T18:06:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,36 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { Box } from '../Box/Box';
+import Text, { TextVariant } from '../../../component-library/components/Texts/Text';
+import { useStyles } from '../../../component-library/hooks';
+import { Theme } from '../../../util/theme/models';
+import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader';
+import BottomSheet from '../../../component-library/components/BottomSheets/BottomSheet';
+
+const createStyles = (params: { theme: Theme }) => {
+  const { theme } = params;
+  return StyleSheet.create({
+    content: {
+      padding: 24,
+      backgroundColor: theme.colors.background.default,
+      flex: 1,
+    },
+  });
+};
+
+export const BridgeTokenSelector = () => {
+  const { styles } = useStyles(createStyles, {});
+
+  return (
+    <BottomSheet isFullscreen>
+      <Box style={styles.content}>
+        <BottomSheetHeader>
+          <Text variant={TextVariant.HeadingMD}>Tokens</Text>
+        </BottomSheetHeader>
+        <Box>
+          <Text variant={TextVariant.BodyMD}>Token content will go here</Text>
path: app/components/UI/Bridge/BridgeTokenSelector.tsx
user: login: tommasini
body: Missing translations here
created_at: 2025-03-11T18:28:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,36 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { Box } from '../Box/Box';
+import Text, { TextVariant } from '../../../component-library/components/Texts/Text';
+import { useStyles } from '../../../component-library/hooks';
+import { Theme } from '../../../util/theme/models';
+import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader';
+import BottomSheet from '../../../component-library/components/BottomSheets/BottomSheet';
+
+const createStyles = (params: { theme: Theme }) => {
+  const { theme } = params;
+  return StyleSheet.create({
+    content: {
+      padding: 24,
+      backgroundColor: theme.colors.background.default,
+      flex: 1,
+    },
+  });
+};
+
+export const BridgeTokenSelector = () => {
+  const { styles } = useStyles(createStyles, {});
+
+  return (
+    <BottomSheet isFullscreen>
+      <Box style={styles.content}>
+        <BottomSheetHeader>
+          <Text variant={TextVariant.HeadingMD}>Tokens</Text>
path: app/components/UI/Bridge/BridgeTokenSelector.tsx
user: login: tommasini
body: Not sure if product wants translations here, How is it working on other "Tokens" string placement?
https://github.com/MetaMask/metamask-mobile/blob/9f2713093cbf088814aa4427255d778b0ca62070/locales/languages/en.json#L405
created_at: 2025-03-11T18:29:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,36 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { Box } from '../Box/Box';
+import Text, { TextVariant } from '../../../component-library/components/Texts/Text';
+import { useStyles } from '../../../component-library/hooks';
+import { Theme } from '../../../util/theme/models';
+import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader';
+import BottomSheet from '../../../component-library/components/BottomSheets/BottomSheet';
+
+const createStyles = (params: { theme: Theme }) => {
+  const { theme } = params;
+  return StyleSheet.create({
+    content: {
+      padding: 24,
+      backgroundColor: theme.colors.background.default,
+      flex: 1,
+    },
+  });
+};
+
+export const BridgeTokenSelector = () => {
+  const { styles } = useStyles(createStyles, {});
+
+  return (
+    <BottomSheet isFullscreen>
+      <Box style={styles.content}>
+        <BottomSheetHeader>
+          <Text variant={TextVariant.HeadingMD}>Tokens</Text>
path: app/components/UI/Bridge/BridgeTokenSelector.tsx
user: login: infiniteflower
body: Right, will do the `en.json` stuff thanks
created_at: 2025-03-11T18:35:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,36 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { Box } from '../Box/Box';
+import Text, { TextVariant } from '../../../component-library/components/Texts/Text';
+import { useStyles } from '../../../component-library/hooks';
+import { Theme } from '../../../util/theme/models';
+import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader';
+import BottomSheet from '../../../component-library/components/BottomSheets/BottomSheet';
+
+const createStyles = (params: { theme: Theme }) => {
+  const { theme } = params;
+  return StyleSheet.create({
+    content: {
+      padding: 24,
+      backgroundColor: theme.colors.background.default,
+      flex: 1,
+    },
+  });
+};
+
+export const BridgeTokenSelector = () => {
+  const { styles } = useStyles(createStyles, {});
+
+  return (
+    <BottomSheet isFullscreen>
+      <Box style={styles.content}>
+        <BottomSheetHeader>
+          <Text variant={TextVariant.HeadingMD}>Tokens</Text>
+        </BottomSheetHeader>
+        <Box>
+          <Text variant={TextVariant.BodyMD}>Token content will go here</Text>
path: app/components/UI/Bridge/BridgeTokenSelector.tsx
user: login: infiniteflower
body: This one is just a placeholder screen, we don't need translations here
created_at: 2025-03-11T18:36:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,36 @@
+import React from 'react';
+import { StyleSheet } from 'react-native';
+import { Box } from '../Box/Box';
+import Text, { TextVariant } from '../../../component-library/components/Texts/Text';
+import { useStyles } from '../../../component-library/hooks';
+import { Theme } from '../../../util/theme/models';
+import BottomSheetHeader from '../../../component-library/components/BottomSheets/BottomSheetHeader';
+import BottomSheet from '../../../component-library/components/BottomSheets/BottomSheet';
+
+const createStyles = (params: { theme: Theme }) => {
+  const { theme } = params;
+  return StyleSheet.create({
+    content: {
+      padding: 24,
+      backgroundColor: theme.colors.background.default,
+      flex: 1,
+    },
+  });
+};
+
+export const BridgeTokenSelector = () => {
+  const { styles } = useStyles(createStyles, {});
+
+  return (
+    <BottomSheet isFullscreen>
+      <Box style={styles.content}>
+        <BottomSheetHeader>
+          <Text variant={TextVariant.HeadingMD}>Tokens</Text>
path: app/components/UI/Bridge/BridgeTokenSelector.tsx
user: login: infiniteflower
body: But this page is just a placeholder screen, we don't need translations here
created_at: 2025-03-11T18:39:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -184,6 +184,7 @@
     "@metamask/keyring-snap-client": "^4.0.1",
     "@metamask/logging-controller": "^6.0.4",
     "@metamask/message-signing-snap": "^0.3.3",
+    "@metamask/metamask-eth-abis": "3.1.1",
path: package.json
user: login: amitabh94
body: No change in yarn.lock? I see this package is already included in the resolutions. Do we still need to add this to dependencies?
created_at: 2025-03-11T19:05:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -184,6 +184,7 @@
     "@metamask/keyring-snap-client": "^4.0.1",
     "@metamask/logging-controller": "^6.0.4",
     "@metamask/message-signing-snap": "^0.3.3",
+    "@metamask/metamask-eth-abis": "3.1.1",
path: package.json
user: login: infiniteflower
body: Gotcha, will remove
created_at: 2025-03-11T19:09:55Z
author_association: CONTRIBUTOR
diff_hunk: @@ -184,6 +184,7 @@
     "@metamask/keyring-snap-client": "^4.0.1",
     "@metamask/logging-controller": "^6.0.4",
     "@metamask/message-signing-snap": "^0.3.3",
+    "@metamask/metamask-eth-abis": "3.1.1",
path: package.json
user: login: infiniteflower
body: Done in [e1a7ef0](https://github.com/MetaMask/metamask-mobile/pull/13847/commits/e1a7ef0e83dcb0ce426a5f573f2913cfa86d68e1)
created_at: 2025-03-11T19:12:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -184,6 +184,7 @@
     "@metamask/keyring-snap-client": "^4.0.1",
     "@metamask/logging-controller": "^6.0.4",
     "@metamask/message-signing-snap": "^0.3.3",
+    "@metamask/metamask-eth-abis": "3.1.1",
path: package.json
user: login: infiniteflower
body: Actually the dep check CI job fails without this https://github.com/MetaMask/metamask-mobile/actions/runs/13796251070/job/38588504064?pr=13847, so I'm going to put it back in

I did run `yarn && yarn setup` and no lock file changes
created_at: 2025-03-11T19:15:14Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-03-04T21:21:14Z
body: **CLA Signature Action:** All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-05T16:05:47Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/f686b3b0-6fab-4a0c-99a1-542058f20cff) **Bitrise**

✅✅✅ `pr_smoke_e2e_pipeline` passed on Bitrise! ✅✅✅

Commit hash: 31477745d457db455ed44a11adf3cf6e542d6317
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/f686b3b0-6fab-4a0c-99a1-542058f20cff

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

<!-- BITRISE_TAG -->
<!-- BITRISE_SUCCESS_TAG -->

<!-- 31477745d457db455ed44a11adf3cf6e542d6317 -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-05T22:50:01Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/e1062ec5-c05b-4fba-9505-d9667573e5bf) **Bitrise**

❌❌❌ `pr_smoke_e2e_pipeline` failed on Bitrise! ❌❌❌

Commit hash: 6d4880820657972a70afffd0bf4da7a746428b44
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/e1062ec5-c05b-4fba-9505-d9667573e5bf

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

> [!TIP]
>- Check the [documentation](https://www.notion.so/metamask-consensys/Bitrise-Pipeline-Overview-43159500c43748a389556f0593e8834b#26052f2ea6e24f8c9cfdb57a7522dc1f) if you have any doubts on how to understand the failure on bitrise

<!-- BITRISE_TAG -->
<!-- BITRISE_FAIL_TAG -->

<!-- 6d4880820657972a70afffd0bf4da7a746428b44 -->
author_association: CONTRIBUTOR
user: login: codecov-commenter
created_at: 2025-03-06T01:04:02Z
body: ## [Codecov](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask) Report
Attention: Patch coverage is `67.11409%` with `49 lines` in your changes missing coverage. Please review.
> Project coverage is 63.77%. Comparing base [(`d1d22d7`)](https://app.codecov.io/gh/MetaMask/metamask-mobile/commit/d1d22d72d3871336b1b25eb2de191dea407aae77?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask) to head [(`08f1129`)](https://app.codecov.io/gh/MetaMask/metamask-mobile/commit/08f1129a3001c67073bb6f44de10bdc48521d4b1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask).
> Report is 40 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask) | Patch % | Lines |
|---|---|---|
| [app/components/UI/Bridge/index.tsx](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?src=pr&el=tree&filepath=app%2Fcomponents%2FUI%2FBridge%2Findex.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask#diff-YXBwL2NvbXBvbmVudHMvVUkvQnJpZGdlL2luZGV4LnRzeA==) | 46.29% | [19 Missing and 10 partials :warning: ](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask) |
| [app/components/UI/Bridge/TokenInputArea.tsx](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?src=pr&el=tree&filepath=app%2Fcomponents%2FUI%2FBridge%2FTokenInputArea.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask#diff-YXBwL2NvbXBvbmVudHMvVUkvQnJpZGdlL1Rva2VuSW5wdXRBcmVhLnRzeA==) | 62.50% | [2 Missing and 4 partials :warning: ](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask) |
| [app/components/UI/Bridge/BridgeTokenSelector.tsx](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?src=pr&el=tree&filepath=app%2Fcomponents%2FUI%2FBridge%2FBridgeTokenSelector.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask#diff-YXBwL2NvbXBvbmVudHMvVUkvQnJpZGdlL0JyaWRnZVRva2VuU2VsZWN0b3IudHN4) | 33.33% | [4 Missing :warning: ](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask) |
| [app/components/UI/Bridge/useLatestBalance.ts](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?src=pr&el=tree&filepath=app%2Fcomponents%2FUI%2FBridge%2FuseLatestBalance.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask#diff-YXBwL2NvbXBvbmVudHMvVUkvQnJpZGdlL3VzZUxhdGVzdEJhbGFuY2UudHM=) | 82.60% | [1 Missing and 3 partials :warning: ](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask) |
| [app/core/redux/slices/bridge/index.ts](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?src=pr&el=tree&filepath=app%2Fcore%2Fredux%2Fslices%2Fbridge%2Findex.ts&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask#diff-YXBwL2NvcmUvcmVkdXgvc2xpY2VzL2JyaWRnZS9pbmRleC50cw==) | 89.74% | [1 Missing and 3 partials :warning: ](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask) |
| [app/components/UI/Bridge/Token.tsx](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?src=pr&el=tree&filepath=app%2Fcomponents%2FUI%2FBridge%2FToken.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask#diff-YXBwL2NvbXBvbmVudHMvVUkvQnJpZGdlL1Rva2VuLnRzeA==) | 81.81% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main   #13847      +/-   ##
==========================================
+ Coverage   63.42%   63.77%   +0.34%     
==========================================
  Files        2093     2132      +39     
  Lines       45277    45684     +407     
  Branches     6234     6317      +83     
==========================================
+ Hits        28717    29133     +416     
+ Misses      14676    14642      -34     
- Partials     1884     1909      +25     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/MetaMask/metamask-mobile/pull/13847?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=MetaMask).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-03-06T19:09:44Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/3e4191fd-4474-475f-8799-9888c2ed8311) **Bitrise**

❌❌❌ `pr_smoke_e2e_pipeline` failed on Bitrise! ❌❌❌

Commit hash: f1ee396802f4c2793677a1f688bbb7fe82d4a2af
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/3e4191fd-4474-475f-8799-9888c2ed8311

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

> [!TIP]
>- Check the [documentation](https://www.notion.so/metamask-consensys/Bitrise-Pipeline-Overview-43159500c43748a389556f0593e8834b#26052f2ea6e24f8c9cfdb57a7522dc1f) if you have any doubts on how to understand the failure on bitrise

<!-- BITRISE_TAG -->
<!-- BITRISE_FAIL_TAG -->

<!-- f1ee396802f4c2793677a1f688bbb7fe82d4a2af -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T20:10:23Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/127ee20d-a6e6-4661-a512-ecd80fd62f16) **Bitrise**

✅✅✅ `pr_smoke_e2e_pipeline` passed on Bitrise! ✅✅✅

Commit hash: 3dcda37d6a73f00887e16bfa8b496613854b45ba
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/127ee20d-a6e6-4661-a512-ecd80fd62f16

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

<!-- BITRISE_TAG -->
<!-- BITRISE_SUCCESS_TAG -->

<!-- 3dcda37d6a73f00887e16bfa8b496613854b45ba -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-06T20:56:07Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/5af6d850-2c39-405c-b7b0-43802f83e554) **Bitrise**

✅✅✅ `pr_smoke_e2e_pipeline` passed on Bitrise! ✅✅✅

Commit hash: 9aa406b76c9d957586df67cbc38526814bc09395
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/5af6d850-2c39-405c-b7b0-43802f83e554

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

<!-- BITRISE_TAG -->
<!-- BITRISE_SUCCESS_TAG -->

<!-- 9aa406b76c9d957586df67cbc38526814bc09395 -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-11T14:48:05Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/2ef840ee-3cd6-44f0-a46a-7dd1cc0918ef) **Bitrise**

✅✅✅ `pr_smoke_e2e_pipeline` passed on Bitrise! ✅✅✅

Commit hash: b376a0c080dc59b742c9c3fd59fdb4354749f2f4
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/2ef840ee-3cd6-44f0-a46a-7dd1cc0918ef

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

<!-- BITRISE_TAG -->
<!-- BITRISE_SUCCESS_TAG -->

<!-- b376a0c080dc59b742c9c3fd59fdb4354749f2f4 -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-11T18:48:17Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/00c84f8b-68b3-4274-9350-7c6f12e24410) **Bitrise**

🔄🔄🔄 `pr_smoke_e2e_pipeline` started on Bitrise...🔄🔄🔄

Commit hash: 35c230080c4a679d80f551e4a057ba043e1f93cc
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/00c84f8b-68b3-4274-9350-7c6f12e24410

>[!NOTE]
>- This comment will auto-update when build completes
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request
<!-- BITRISE_TAG -->
<!-- BITRISE_PENDING_TAG -->

<!-- 35c230080c4a679d80f551e4a057ba043e1f93cc -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-11T19:12:53Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/02236924-4d39-4db3-b8ae-27f949864fe0) **Bitrise**

🔄🔄🔄 `pr_smoke_e2e_pipeline` started on Bitrise...🔄🔄🔄

Commit hash: e1a7ef0e83dcb0ce426a5f573f2913cfa86d68e1
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/02236924-4d39-4db3-b8ae-27f949864fe0

>[!NOTE]
>- This comment will auto-update when build completes
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request
<!-- BITRISE_TAG -->
<!-- BITRISE_PENDING_TAG -->

<!-- e1a7ef0e83dcb0ce426a5f573f2913cfa86d68e1 -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-03-11T19:16:31Z
body: ## [<img alt="https://bitrise.io/" src="https://assets-global.website-files.com/5db35de024bb983af1b4e151/5e6f9ccc3e129dfd8a205e4e_Bitrise%20Logo%20-%20Eggplant%20Bg.png" height="20">](https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/3bd74c59-11a7-4b67-92f4-5a410ccd12e8) **Bitrise**

✅✅✅ `pr_smoke_e2e_pipeline` passed on Bitrise! ✅✅✅

Commit hash: 0fe605957327f62716f1598870886648325c0e3c
Build link: https://app.bitrise.io/app/be69d4368ee7e86d/pipelines/3bd74c59-11a7-4b67-92f4-5a410ccd12e8

>[!NOTE]
>- You can kick off another `pr_smoke_e2e_pipeline` on Bitrise by removing and re-applying the `Run Smoke E2E` label on the pull request

<!-- BITRISE_TAG -->
<!-- BITRISE_SUCCESS_TAG -->

<!-- 0fe605957327f62716f1598870886648325c0e3c -->
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-03-11T19:28:18Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=metamask-mobile&pullRequest=13847) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=metamask-mobile&pullRequest=13847&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=metamask-mobile&pullRequest=13847&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=metamask-mobile&pullRequest=13847&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [80.5% Coverage on New Code](https://sonarcloud.io/component_measures?id=metamask-mobile&pullRequest=13847&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=metamask-mobile&pullRequest=13847&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=metamask-mobile&pullRequest=13847)


author_association: NONE