Skip to content

Commit

Permalink
feat: PortfolioView (#28593)
Browse files Browse the repository at this point in the history
## **Description**

This consolidates the changes from a series of 3 Multichain Asset List
PRs that built on each other:

1. Product code (feature branch):
#28386
2. Unit tests: #28451
3. e2e tests: #28524

We created separate branches for rapid iteration and isolated testing.
The code is now cleaner and stable enough for review and merge into
develop, gated by the `PORTFOLIO_VIEW` feature flag.

We will introduce another PR to remove this feature flag when we are
ready to ship it.

[![Open in GitHub
Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/28593?quickstart=1)

## **Related issues**

Fixes:
https://github.com/orgs/MetaMask/projects/85/views/35?pane=issue&itemId=82217837

## **Manual testing steps**

`PORTFOLIO_VIEW=1 yarn webpack --watch`

1. View tokens across all networks in one unified list.
2. Filter tokens by selected network
3. Crosschain navigation:
- Token detail pages update to display data from the appropriate
network.
- Send/Swap actions automatically adjust the selected network for user
convenience.
- Ensure that network switch is functional, and sends/swaps happen on
correct chain.
    
Known caveats:
1. POL native token market data not populating. Will be addressed here:
#28584 and
MetaMask/core#4952
2. Native token swapping on different network than selected network
swaps incorrect token:
#28587
3. Multichain token detection experimental draft:
#28380
    

## **Screenshots/Recordings**


https://github.com/user-attachments/assets/79e7fd2d-9908-4c7a-8134-089cbe6593cc


https://github.com/user-attachments/assets/dfb4a54f-a8ae-48a4-a9e7-50327f56054a

## **Pre-merge author checklist**

- [ ] I've followed [MetaMask Contributor
Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask
Extension Coding
Standards](https://github.com/MetaMask/metamask-extension/blob/develop/.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-extension/blob/develop/.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.

---------

Co-authored-by: Jonathan Bursztyn <[email protected]>
Co-authored-by: chloeYue <[email protected]>
Co-authored-by: seaona <[email protected]>
Co-authored-by: Monte Lai <[email protected]>
Co-authored-by: Charly Chevalier <[email protected]>
Co-authored-by: Pedro Figueiredo <[email protected]>
Co-authored-by: MetaMask Bot <[email protected]>
Co-authored-by: NidhiKJha <[email protected]>
Co-authored-by: sahar-fehri <[email protected]>
  • Loading branch information
10 people authored Nov 21, 2024
1 parent 9f8d61b commit a04b34b
Show file tree
Hide file tree
Showing 52 changed files with 1,306 additions and 498 deletions.
8 changes: 6 additions & 2 deletions app/_locales/en/messages.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions shared/constants/network.ts
Original file line number Diff line number Diff line change
Expand Up @@ -816,6 +816,9 @@ export const CHAIN_ID_TO_ETHERS_NETWORK_NAME_MAP = {
export const CHAIN_ID_TOKEN_IMAGE_MAP = {
[CHAIN_IDS.MAINNET]: ETH_TOKEN_IMAGE_URL,
[CHAIN_IDS.TEST_ETH]: TEST_ETH_TOKEN_IMAGE_URL,
[CHAIN_IDS.ARBITRUM]: ETH_TOKEN_IMAGE_URL,
[CHAIN_IDS.BASE]: ETH_TOKEN_IMAGE_URL,
[CHAIN_IDS.LINEA_MAINNET]: ETH_TOKEN_IMAGE_URL,
[CHAIN_IDS.BSC]: BNB_TOKEN_IMAGE_URL,
[CHAIN_IDS.POLYGON]: POL_TOKEN_IMAGE_URL,
[CHAIN_IDS.AVALANCHE]: AVAX_TOKEN_IMAGE_URL,
Expand Down
5 changes: 4 additions & 1 deletion test/data/mock-state.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
}
},
"metamask": {
"allTokens": {},
"tokenBalances": {},
"use4ByteResolution": true,
"ipfsGateway": "dweb.link",
"dismissSeedBackUpReminder": false,
Expand Down Expand Up @@ -384,7 +386,8 @@
"key": "tokenFiatAmount",
"order": "dsc",
"sortCallback": "stringNumeric"
}
},
"tokenNetworkFilter": {}
},
"ensResolutionsByAddress": {},
"isAccountMenuOpen": false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,6 @@
},
"TxController": {
"methodData": "object",
"submitHistory": "object",
"transactions": "object",
"lastFetchedBlockNumbers": "object",
"submitHistory": "object"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@
"isRedesignedConfirmationsDeveloperEnabled": "boolean",
"showConfirmationAdvancedDetails": false,
"tokenSortConfig": "object",
"tokenNetworkFilter": {},
"showMultiRpcModal": "boolean",
"shouldShowAggregatedBalancePopover": "boolean",
"tokenNetworkFilter": {}
Expand Down
10 changes: 5 additions & 5 deletions test/e2e/tests/privacy-mode/privacy-mode.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe('Privacy Mode', function () {
async ({ driver }) => {
async function checkForHeaderValue(value) {
const balanceElement = await driver.findElement(
'[data-testid="eth-overview__primary-currency"] .currency-display-component__text',
'[data-testid="account-value-and-suffix"]',
);
const surveyText = await balanceElement.getText();
assert.equal(
Expand All @@ -30,15 +30,15 @@ describe('Privacy Mode', function () {

async function checkForTokenValue(value) {
const balanceElement = await driver.findElement(
'[data-testid="multichain-token-list-item-secondary-value"]',
'[data-testid="multichain-token-list-item-value"]',
);
const surveyText = await balanceElement.getText();
assert.equal(surveyText, value, `Token balance should be "${value}"`);
}

async function checkForPrivacy() {
await checkForHeaderValue('••••••');
await checkForTokenValue('•••••••••');
await checkForTokenValue('••••••');
}

async function checkForNoPrivacy() {
Expand All @@ -48,7 +48,7 @@ describe('Privacy Mode', function () {

async function togglePrivacy() {
const balanceElement = await driver.findElement(
'[data-testid="eth-overview__primary-currency"] .currency-display-component__text',
'[data-testid="account-value-and-suffix"]',
);
const initialText = await balanceElement.getText();

Expand Down Expand Up @@ -81,7 +81,7 @@ describe('Privacy Mode', function () {

async function togglePrivacy() {
const balanceElement = await driver.findElement(
'[data-testid="eth-overview__primary-currency"] .currency-display-component__text',
'[data-testid="account-value-and-suffix"]',
);
const initialText = await balanceElement.getText();

Expand Down
14 changes: 4 additions & 10 deletions test/e2e/tests/tokens/token-sort.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,8 @@ describe('Token List', function () {

assert.ok(tokenSymbolsBeforeSorting[0].includes('Ethereum'));

await await driver.clickElement(
'[data-testid="sort-by-popover-toggle"]',
);
await await driver.clickElement('[data-testid="sortByAlphabetically"]');
await driver.clickElement('[data-testid="sort-by-popover-toggle"]');
await driver.clickElement('[data-testid="sortByAlphabetically"]');

await driver.delay(regularDelayMs);
const tokenListAfterSortingAlphabetically = await driver.findElements(
Expand All @@ -85,12 +83,8 @@ describe('Token List', function () {
tokenListSymbolsAfterSortingAlphabetically[0].includes('ABC'),
);

await await driver.clickElement(
'[data-testid="sort-by-popover-toggle"]',
);
await await driver.clickElement(
'[data-testid="sortByDecliningBalance"]',
);
await driver.clickElement('[data-testid="sort-by-popover-toggle"]');
await driver.clickElement('[data-testid="sortByDecliningBalance"]');

await driver.delay(regularDelayMs);
const tokenListBeforeSortingByDecliningBalance =
Expand Down
1 change: 1 addition & 0 deletions test/jest/mock-store.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ export const createSwapsMockStore = () => {
preferences: {
showFiatInTestnets: true,
smartTransactionsOptInStatus: true,
tokenNetworkFilter: {},
showMultiRpcModal: false,
},
transactions: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,13 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => {
}, [currentNetwork.chainId, TEST_CHAINS]);

const allOpts: Record<string, boolean> = {};
Object.keys(allNetworks).forEach((chainId) => {
Object.keys(allNetworks || {}).forEach((chainId) => {
allOpts[chainId] = true;
});

const allNetworksFilterShown =
Object.keys(tokenNetworkFilter).length !== Object.keys(allOpts).length;
Object.keys(tokenNetworkFilter || {}).length !==
Object.keys(allOpts || {}).length;

useEffect(() => {
if (isTestNetwork) {
Expand All @@ -86,7 +87,7 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => {
// We need to set the default filter for all users to be all included networks, rather than defaulting to empty object
// This effect is to unblock and derisk in the short-term
useEffect(() => {
if (Object.keys(tokenNetworkFilter).length === 0) {
if (Object.keys(tokenNetworkFilter || {}).length === 0) {
dispatch(setTokenNetworkFilter(allOpts));
}
}, []);
Expand Down Expand Up @@ -162,7 +163,7 @@ const AssetListControlBar = ({ showTokensLinks }: AssetListControlBarProps) => {
>
{process.env.PORTFOLIO_VIEW && (
<ButtonBase
data-testid="network-filter"
data-testid="sort-by-networks"
variant={TextVariant.bodyMdMedium}
className="asset-list-control-bar__button asset-list-control-bar__network_control"
onClick={toggleNetworkFilterPopover}
Expand Down
10 changes: 10 additions & 0 deletions ui/components/app/assets/asset-list/asset-list.ramps-card.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,16 @@ const render = (
[selectedInternalAccount.address]: { balance },
},
},
preferences: {
tokenNetworkFilter: {},
tokenSortConfig: {
key: 'token-sort-key',
order: 'dsc',
sortCallback: 'stringNumeric',
},
},
allTokens: {},
tokenBalances: {},
},
};
const store = configureStore(state);
Expand Down
4 changes: 3 additions & 1 deletion ui/components/app/assets/asset-list/asset-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,9 @@ const AssetList = ({ onClickAsset, showTokensLinks }: AssetListProps) => {
)}
<AssetListControlBar showTokensLinks={shouldShowTokensLinks} />
<TokenList
nativeToken={<NativeToken onClickAsset={onClickAsset} />}
// nativeToken is still needed to avoid breaking flask build's support for bitcoin
// TODO: refactor this to no longer be needed for non-evm chains
nativeToken={!isEvm && <NativeToken onClickAsset={onClickAsset} />}
onTokenClick={(chainId: string, tokenAddress: string) => {
onClickAsset(chainId, tokenAddress);
trackEvent({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,14 @@ import {
} from '../../../../../selectors/multichain';
import { getPreferences } from '../../../../../selectors';
import { TokenListItem } from '../../../../multichain';
import { useIsOriginalNativeTokenSymbol } from '../../../../../hooks/useIsOriginalNativeTokenSymbol';
import { AssetListProps } from '../asset-list';
import { useNativeTokenBalance } from './use-native-token-balance';

const NativeToken = ({ onClickAsset }: AssetListProps) => {
const nativeCurrency = useSelector(getMultichainNativeCurrency);
const isMainnet = useSelector(getMultichainIsMainnet);
const { chainId, ticker, type, rpcUrl } = useSelector(
getMultichainCurrentNetwork,
);
const { chainId } = useSelector(getMultichainCurrentNetwork);
const { privacyMode } = useSelector(getPreferences);
const isOriginalNativeSymbol = useIsOriginalNativeTokenSymbol(
chainId,
ticker,
type,
rpcUrl,
);
const balance = useSelector(getMultichainSelectedAccountCachedBalance);
const balanceIsLoading = !balance;

Expand All @@ -50,7 +41,6 @@ const NativeToken = ({ onClickAsset }: AssetListProps) => {
tokenSymbol={symbol}
secondary={secondary}
tokenImage={balanceIsLoading ? null : primaryTokenImage}
isOriginalTokenSymbol={isOriginalNativeSymbol}
isNativeCurrency
isStakeable={isStakeable}
showPercentage
Expand Down
Loading

0 comments on commit a04b34b

Please sign in to comment.