Skip to content

Commit

Permalink
Merge branch 'main' into android_footer_fix
Browse files Browse the repository at this point in the history
  • Loading branch information
MarioAslau authored Feb 19, 2025
2 parents e3e2066 + de4d322 commit 9298c39
Show file tree
Hide file tree
Showing 99 changed files with 4,828 additions and 534 deletions.
4 changes: 4 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ e2e/specs/identity @MetaMask/identity
ses.cjs @MetaMask/supply-chain
patches/react-native+0.*.patch @MetaMask/supply-chain

# Portfolio Team
app/components/hooks/useTokenSearchDiscovery @MetaMask/portfolio
app/core/Engine/controllers/TokenSearchDiscoveryController @MetaMask/portfolio

# Snaps Team
**/snaps/** @MetaMask/snaps-devs
**/Snaps/** @MetaMask/snaps-devs
Expand Down
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,8 @@ android {
applicationId "io.metamask"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionName "7.39.0"
versionCode 1544
versionName "7.40.0"
versionCode 1553
testBuildType System.getProperty('testBuildType', 'debug')
missingDimensionStrategy 'react-native-camera', 'general'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
Expand Down
12 changes: 12 additions & 0 deletions app/components/Nav/Main/MainNavigator.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import NftDetailsFullImage from '../../Views/NftDetails/NFtDetailsFullImage';
import AccountPermissions from '../../../components/Views/AccountPermissions';
import { AccountPermissionsScreens } from '../../../components/Views/AccountPermissions/AccountPermissions.types';
import { StakeModalStack, StakeScreenStack } from '../../UI/Stake/routes';
import BridgeView from '../../UI/Bridge';

const Stack = createStackNavigator();
const Tab = createBottomTabNavigator();
Expand Down Expand Up @@ -713,6 +714,16 @@ const Swaps = () => (
</Stack.Navigator>
);

const Bridge = () => (
<Stack.Navigator>
<Stack.Screen
name="BridgeView"
component={BridgeView}
options={BridgeView.navigationOptions}
/>
</Stack.Navigator>
);

const SetPasswordFlow = () => (
<Stack.Navigator>
<Stack.Screen
Expand Down Expand Up @@ -817,6 +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="StakeScreens" component={StakeScreenStack} />
<Stack.Screen
name="StakeModals"
Expand Down
121 changes: 121 additions & 0 deletions app/components/UI/AssetOverview/AssetOverview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import * as networks from '../../../util/networks';
// eslint-disable-next-line import/no-namespace
import * as transactions from '../../../util/transactions';
import { mockNetworkState } from '../../../util/test/network';
import Engine from '../../../core/Engine';
import Routes from '../../../constants/navigation/Routes';

const MOCK_CHAIN_ID = '0x1';

Expand Down Expand Up @@ -106,6 +108,9 @@ jest.mock('../../../core/Engine', () => ({
.mockReturnValue(mockNetworkConfiguration),
setActiveNetwork: jest.fn().mockResolvedValue(undefined),
},
MultichainNetworkController: {
setActiveNetwork: jest.fn().mockResolvedValue(undefined),
},
},
}));

Expand Down Expand Up @@ -338,4 +343,120 @@ describe('AssetOverview', () => {
const buyButton = queryByTestId(TokenOverviewSelectorsIDs.BUY_BUTTON);
expect(buyButton).toBeNull();
});

describe('Portfolio view network switching', () => {
beforeEach(() => {
jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(true);
jest.useFakeTimers();
// Reset mocks before each test
jest.clearAllMocks();
});

afterEach(() => {
jest.useRealTimers();
});

it('should switch networks before sending when on different chain', async () => {
const differentChainAsset = {
...asset,
chainId: '0x89', // Different chain (Polygon)
};

const { getByTestId } = renderWithProvider(
<AssetOverview
asset={differentChainAsset}
displayBuyButton
displaySwapsButton
swapsIsLive
/>,
{ state: mockInitialState },
);

const sendButton = getByTestId('token-send-button');
await fireEvent.press(sendButton);

// Wait for all promises to resolve
await Promise.resolve();

expect(navigate).toHaveBeenCalledWith(Routes.WALLET.HOME, {
screen: Routes.WALLET.TAB_STACK_FLOW,
params: {
screen: Routes.WALLET_VIEW,
},
});
});

it('should switch networks before swapping when on different chain', async () => {
const differentChainAsset = {
...asset,
chainId: '0x89', // Different chain (Polygon)
};

const { getByTestId } = renderWithProvider(
<AssetOverview
asset={differentChainAsset}
displayBuyButton
displaySwapsButton
swapsIsLive
/>,
{ state: mockInitialState },
);

const swapButton = getByTestId('token-swap-button');
await fireEvent.press(swapButton);

// Wait for all promises to resolve
await Promise.resolve();

expect(navigate).toHaveBeenCalledWith(Routes.WALLET.HOME, {
screen: Routes.WALLET.TAB_STACK_FLOW,
params: {
screen: Routes.WALLET_VIEW,
},
});

expect(
Engine.context.NetworkController.getNetworkConfigurationByChainId,
).toHaveBeenCalledWith('0x89');

// Fast-forward timers to trigger the swap navigation
jest.advanceTimersByTime(500);

expect(navigate).toHaveBeenCalledWith('Swaps', {
screen: 'SwapsAmountView',
params: {
sourceToken: differentChainAsset.address,
sourcePage: 'MainView',
chainId: '0x89',
},
});
});

it('should not switch networks when on same chain', async () => {
const sameChainAsset = {
...asset,
chainId: MOCK_CHAIN_ID, // Same chain as current
};

const { getByTestId } = renderWithProvider(
<AssetOverview
asset={sameChainAsset}
displayBuyButton
displaySwapsButton
swapsIsLive
/>,
{ state: mockInitialState },
);

const sendButton = getByTestId('token-send-button');
await fireEvent.press(sendButton);

// Wait for all promises to resolve
await Promise.resolve();

expect(
Engine.context.MultichainNetworkController.setActiveNetwork,
).not.toHaveBeenCalled();
});
});
});
42 changes: 31 additions & 11 deletions app/components/UI/AssetOverview/AssetOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ const AssetOverview: React.FC<AssetOverviewProps> = ({
const primaryCurrency = useSelector(
(state: RootState) => state.settings.primaryCurrency,
);
const goToBridge = useGoToBridge('TokenDetails');
const selectedAddress = useSelector(
selectSelectedInternalAccountFormattedAddress,
);
Expand Down Expand Up @@ -176,6 +175,23 @@ const AssetOverview: React.FC<AssetOverviewProps> = ({
});
}, [navigation, asset.address, asset.chainId]);

const handleBridgeNavigation = useCallback(() => {
navigation.navigate('Bridge', {
screen: 'BridgeView',
params: {
sourceToken: asset.address,
sourcePage: 'MainView',
chainId: asset.chainId,
},
});
}, [navigation, asset.address, asset.chainId]);

const goToPortfolioBridge = useGoToBridge('TokenDetails');

const goToBridge = process.env.MM_BRIDGE_UI_ENABLED === 'true'
? handleBridgeNavigation
: goToPortfolioBridge;

const onSend = async () => {
if (isPortfolioViewEnabled()) {
navigation.navigate(Routes.WALLET.HOME, {
Expand All @@ -186,7 +202,8 @@ const AssetOverview: React.FC<AssetOverviewProps> = ({
});

if (asset.chainId !== selectedChainId) {
const { NetworkController } = Engine.context;
const { NetworkController, MultichainNetworkController } =
Engine.context;
const networkConfiguration =
NetworkController.getNetworkConfigurationByChainId(
asset.chainId as Hex,
Expand All @@ -197,7 +214,9 @@ const AssetOverview: React.FC<AssetOverviewProps> = ({
networkConfiguration.defaultRpcEndpointIndex
]?.networkClientId;

await NetworkController.setActiveNetwork(networkClientId as string);
await MultichainNetworkController.setActiveNetwork(
networkClientId as string,
);
}
}
if ((asset.isETH || asset.isNative) && ticker) {
Expand All @@ -217,7 +236,8 @@ const AssetOverview: React.FC<AssetOverviewProps> = ({
},
});
if (asset.chainId !== selectedChainId) {
const { NetworkController } = Engine.context;
const { NetworkController, MultichainNetworkController } =
Engine.context;
const networkConfiguration =
NetworkController.getNetworkConfigurationByChainId(
asset.chainId as Hex,
Expand All @@ -228,13 +248,13 @@ const AssetOverview: React.FC<AssetOverviewProps> = ({
networkConfiguration.defaultRpcEndpointIndex
]?.networkClientId;

NetworkController.setActiveNetwork(networkClientId as string).then(
() => {
setTimeout(() => {
handleSwapNavigation();
}, 500);
},
);
MultichainNetworkController.setActiveNetwork(
networkClientId as string,
).then(() => {
setTimeout(() => {
handleSwapNavigation();
}, 500);
});
} else {
handleSwapNavigation();
}
Expand Down
36 changes: 36 additions & 0 deletions app/components/UI/Bridge/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
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;
47 changes: 47 additions & 0 deletions app/components/UI/Bridge/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import renderWithProvider, {
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: () => ({}),
};
});

jest.mock('../../../core/Engine', () => ({
context: {
SwapsController: {
fetchAggregatorMetadataWithCache: jest.fn(),
fetchTopAssetsWithCache: jest.fn(),
fetchTokenWithCache: jest.fn(),
},
},
}));

const mockInitialState: DeepPartial<RootState> = {
engine: {
backgroundState: {
...backgroundState,
},
},
};

describe('BridgeView', () => {
it('renders', async () => {
const { getByText } = renderWithProvider(<BridgeView />, {
state: mockInitialState,
});
expect(getByText('Bridge')).toBeDefined();
});
});
Loading

0 comments on commit 9298c39

Please sign in to comment.