Skip to content

Commit

Permalink
feat: multi chain asset list (#12431)
Browse files Browse the repository at this point in the history
## **Description**

This PR introduces the Unified Asset List feature to MetaMask Mobile,
providing users with a consolidated view of their assets across all
supported blockchain networks. This enhancement improves the user
experience by eliminating the need to switch between networks to view or
manage assets, making asset management more intuitive and efficient.

We will followup with a PR to fix TS feedback we had!
(Not only TS issues but also
[this](#12431 (comment)
and
[this](#12431 (comment)))

## **Related issues**

Fixes: #12462 

## **Manual testing steps**

Build using `PORTFOLIO_VIEW` flag

```
PORTFOLIO_VIEW=true yarn watch
yarn start:ios 
yarn start:android
```

1. Go to the wallet page
2. Select all network on the network filter
3. Check the list of assets
4. Click on each asset with the network filter on "All Networks" and
"Current Network"
5. Test send/swap flows with testnet networks to confirm everything
still works
6. Importing all tokens should work when "All Networks" filter is on
7. Importing networks for a specific network should work when the
"Current Network" filter is on
8. Aggregated balance should chance according to the network filter

## **Screenshots/Recordings**

| Before  | After |
|:---:|:---:|

|![before](https://github.com/user-attachments/assets/449bd3ef-1f69-4cf9-bb93-f5a28838e11b)|![after](https://github.com/user-attachments/assets/26209026-6863-4085-85ed-d541ca4fa720)|

### **Before**

<img
src="https://github.com/user-attachments/assets/4f3aa5f5-920c-4ba6-8f13-0ce5bef4735f"
width="350" alt="before_screenshot">

### **After**

<img
src="https://github.com/user-attachments/assets/41e421b2-5b87-4933-a88e-159a45a15114"
width="350" alt="after_screenshot">

## **Pre-merge author checklist**

- [x] 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).
- [x] I've completed the PR template to the best of my ability
- [x] I’ve included tests if applicable
- [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format
if applicable
- [x] 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**

- [x] I've manually tested the PR (e.g. pull and build branch, run the
app, test code being changed).
- [x] 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.

---------

Co-authored-by: salimtb <[email protected]>
Co-authored-by: sahar-fehri <[email protected]>
  • Loading branch information
3 people authored Dec 10, 2024
1 parent 0f46383 commit 800d116
Show file tree
Hide file tree
Showing 84 changed files with 7,385 additions and 488 deletions.
7 changes: 7 additions & 0 deletions app/components/UI/AccountApproval/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ const mockInitialState = {
},
},
},
TokensController: {
allTokens: {
'0x1': {
'0xc4966c0d659d99699bfd7eb54d8fafee40e4a756': [],
},
},
},
},
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,13 @@ const mockInitialState: DeepPartial<RootState> = {
},
},
TokenBalancesController: {
tokenBalances: { },
tokenBalances: {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': {
'0x5': {
'0x326836cc6cd09B5aa59B81A7F72F25FcC0136b95': '0x2b46',
},
},
},
},
AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
},
Expand Down
86 changes: 78 additions & 8 deletions app/components/UI/AssetOverview/AssetOverview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ import {
MOCK_ADDRESS_2,
} from '../../../util/test/accountsControllerTestUtils';
import { createBuyNavigationDetails } from '../Ramp/routes/utils';
import { getDecimalChainId } from '../../../util/networks';
import {
getDecimalChainId,
isPortfolioViewEnabled,
} from '../../../util/networks';
import { TokenOverviewSelectorsIDs } from '../../../../e2e/selectors/wallet/TokenOverview.selectors';
// eslint-disable-next-line import/no-namespace
import * as networks from '../../../util/networks';

const MOCK_CHAIN_ID = '0x1';

Expand Down Expand Up @@ -43,6 +48,15 @@ const mockInitialState = {
},
} as const,
},
CurrencyRateController: {
conversionRate: {
ETH: {
conversionDate: 1732572535.47,
conversionRate: 3432.53,
usdConversionRate: 3432.53,
},
},
},
},
settings: {
primaryCurrency: 'ETH',
Expand All @@ -51,6 +65,15 @@ const mockInitialState = {

const mockNavigate = jest.fn();
const navigate = mockNavigate;
const mockNetworkConfiguration = {
rpcEndpoints: [
{
networkClientId: 'mockNetworkClientId',
},
],
defaultRpcEndpointIndex: 0,
};

jest.mock('@react-navigation/native', () => {
const actualNav = jest.requireActual('@react-navigation/native');
return {
Expand All @@ -72,9 +95,21 @@ jest.mock('../../hooks/useStyles', () => ({
}),
}));

jest.mock('../../../core/Engine', () => ({
context: {
NetworkController: {
getNetworkConfigurationByChainId: jest
.fn()
.mockReturnValue(mockNetworkConfiguration),
setActiveNetwork: jest.fn().mockResolvedValue(undefined),
},
},
}));

const asset = {
balance: '400',
balanceFiat: '1500',
chainId: MOCK_CHAIN_ID,
logo: 'https://upload.wikimedia.org/wikipedia/commons/0/05/Ethereum_logo_2014.svg',
symbol: 'ETH',
name: 'Ethereum',
Expand All @@ -87,6 +122,10 @@ const asset = {
};

describe('AssetOverview', () => {
beforeEach(() => {
jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(false);
});

it('should render correctly', async () => {
const container = renderWithProvider(
<AssetOverview asset={asset} displayBuyButton displaySwapsButton />,
Expand All @@ -95,6 +134,16 @@ describe('AssetOverview', () => {
expect(container).toMatchSnapshot();
});

it('should render correctly when portfolio view is enabled', async () => {
jest.spyOn(networks, 'isPortfolioViewEnabled').mockReturnValue(true);

const container = renderWithProvider(
<AssetOverview asset={asset} displayBuyButton displaySwapsButton />,
{ state: mockInitialState },
);
expect(container).toMatchSnapshot();
});

it('should handle buy button press', async () => {
const { getByTestId } = renderWithProvider(
<AssetOverview asset={asset} displayBuyButton displaySwapsButton />,
Expand Down Expand Up @@ -133,13 +182,34 @@ describe('AssetOverview', () => {
const swapButton = getByTestId('token-swap-button');
fireEvent.press(swapButton);

expect(navigate).toHaveBeenCalledWith('Swaps', {
params: {
sourcePage: 'MainView',
sourceToken: asset.address,
},
screen: 'SwapsAmountView',
});
if (isPortfolioViewEnabled()) {
expect(navigate).toHaveBeenCalledTimes(3);
expect(navigate).toHaveBeenNthCalledWith(1, 'RampBuy', {
screen: 'GetStarted',
params: {
address: asset.address,
chainId: getDecimalChainId(MOCK_CHAIN_ID),
},
});
expect(navigate).toHaveBeenNthCalledWith(2, 'SendFlowView', {});
expect(navigate).toHaveBeenNthCalledWith(3, 'Swaps', {
screen: 'SwapsAmountView',
params: {
sourcePage: 'MainView',
address: asset.address,
chainId: MOCK_CHAIN_ID,
},
});
} else {
expect(navigate).toHaveBeenCalledWith('Swaps', {
screen: 'SwapsAmountView',
params: {
sourcePage: 'MainView',
sourceToken: asset.address,
chainId: '0x1',
},
});
}
});

it('should not render swap button if displaySwapsButton is false', async () => {
Expand Down
Loading

0 comments on commit 800d116

Please sign in to comment.