Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(4057): replace asset avatars with network avatars #13910

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Original file line number Diff line number Diff line change
@@ -34,6 +34,12 @@ const styleSheet = (params: {
textStyle: {
marginLeft: OVERFLOWTEXTMARGIN_BY_AVATARSIZE[size],
},
avatarGroup: {
borderRadius: 4,
borderWidth: 2,
height: 16,
width: 16,
},
});
};

Original file line number Diff line number Diff line change
@@ -72,4 +72,82 @@ describe('AvatarGroup', () => {
spaceBetweenAvatars,
);
});
it('should render the last maxStackedAvatars avatars when renderOverflowCounter is false', () => {
const avatarPropsList = [
...SAMPLE_AVATARGROUP_PROPS.avatarPropsList,

{
variant: 'Account',
type: 'JazzIcon',
accountAddress: '0xLastAddress1',
},
{
variant: 'Account',
type: 'JazzIcon',
accountAddress: '0xLastAddress2',
},
{
variant: 'Account',
type: 'JazzIcon',
accountAddress: '0xLastAddress3',
},
];

const maxStackedAvatars = 4;

const { getAllByTestId, queryByTestId } = renderComponent({
avatarPropsList,
maxStackedAvatars,
renderOverflowCounter: false,
});

const overflowCounter = queryByTestId(AVATARGROUP_OVERFLOWCOUNTER_TESTID);
expect(overflowCounter).toBeNull();

const avatars = getAllByTestId(AVATARGROUP_AVATAR_TESTID);

expect(avatars.length).toBe(maxStackedAvatars);
const lastAvatarProps = avatarPropsList.slice(-maxStackedAvatars);

expect(avatars.length).toBe(lastAvatarProps.length);
});

it('should render the first maxStackedAvatars avatars when renderOverflowCounter is true', () => {
const avatarPropsList = [
{
variant: 'Account',
type: 'JazzIcon',
accountAddress: '0xFirstAddress1',
},
{
variant: 'Account',
type: 'JazzIcon',
accountAddress: '0xFirstAddress2',
},
{
variant: 'Account',
type: 'JazzIcon',
accountAddress: '0xFirstAddress3',
},
...SAMPLE_AVATARGROUP_PROPS.avatarPropsList,
];

const maxStackedAvatars = 3;

const { getAllByTestId, getByTestId } = renderComponent({
avatarPropsList,
maxStackedAvatars,
renderOverflowCounter: true,
});

const overflowCounter = getByTestId(AVATARGROUP_OVERFLOWCOUNTER_TESTID);
expect(overflowCounter).toBeDefined();

const avatars = getAllByTestId(AVATARGROUP_AVATAR_TESTID);

expect(avatars.length).toBe(maxStackedAvatars);

const overflowCounterNumber = avatarPropsList.length - maxStackedAvatars;
expect(overflowCounter.props.children).toBe(`+${overflowCounterNumber}`);
});
});
Original file line number Diff line number Diff line change
@@ -27,51 +27,55 @@ const AvatarGroup = ({
maxStackedAvatars = DEFAULT_AVATARGROUP_MAXSTACKEDAVATARS,
includesBorder = true,
spaceBetweenAvatars,
renderOverflowCounter = true,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is introduced to optionally avoid rendering the overflow counter to match the Figma

with prop without prop
with without

style,
}: AvatarGroupProps) => {
const overflowCounter = avatarPropsList.length - maxStackedAvatars;
const avatarNegativeSpacing =
spaceBetweenAvatars || SPACEBETWEENAVATARS_BY_AVATARSIZE[size];
const shouldRenderOverflowCounter = overflowCounter > 0;

const doesOverflowCountExist = overflowCounter > 0;
const { styles } = useStyles(styleSheet, {
size,
style,
});

const renderAvatarList = useCallback(
() =>
avatarPropsList.slice(0, maxStackedAvatars).map((avatarProps, index) => {
const marginLeft = index === 0 ? 0 : avatarNegativeSpacing;
const renderAvatarList = useCallback(() => {
const avatarsToRender = renderOverflowCounter
? avatarPropsList.slice(0, maxStackedAvatars)
: avatarPropsList.slice(-maxStackedAvatars);
return avatarsToRender.map((avatarProps, index) => {
const marginLeft = index === 0 ? 0 : avatarNegativeSpacing;

return (
<View
key={`avatar-${index}`}
testID={`${AVATARGROUP_AVATAR_CONTAINER_TESTID}-${index}`}
style={{ marginLeft }}
>
<Avatar
{...avatarProps}
size={size}
includesBorder={includesBorder}
testID={AVATARGROUP_AVATAR_TESTID}
/>
</View>
);
}),
[
avatarPropsList,
avatarNegativeSpacing,
includesBorder,
maxStackedAvatars,
size,
],
);
return (
<View
key={`avatar-${index}`}
testID={`${AVATARGROUP_AVATAR_CONTAINER_TESTID}-${index}`}
style={{ marginLeft }}
>
<Avatar
{...avatarProps}
size={size}
includesBorder={includesBorder}
testID={AVATARGROUP_AVATAR_TESTID}
style={styles.avatarGroup}
/>
</View>
);
});
}, [
avatarPropsList,
avatarNegativeSpacing,
includesBorder,
maxStackedAvatars,
renderOverflowCounter,
size,
styles.avatarGroup,
]);

return (
<View style={styles.base}>
{renderAvatarList()}
{shouldRenderOverflowCounter && (
{renderOverflowCounter && doesOverflowCountExist && (
<Text
variant={TEXTVARIANT_BY_AVATARSIZE[size]}
color={DEFAULT_AVATARGROUP_COUNTER_TEXTCOLOR}
Original file line number Diff line number Diff line change
@@ -62,6 +62,11 @@ export interface AvatarGroupProps extends ViewProps {
* - Please refer to the constants file for the mappings.
*/
spaceBetweenAvatars?: number;
/**
* Optional boolean to render the overflow counter.
* @default true
*/
renderOverflowCounter?: boolean;
}
export interface AvatarGroupStyleSheetVars
extends Pick<AvatarGroupProps, 'style'> {
Original file line number Diff line number Diff line change
@@ -22,7 +22,8 @@ exports[`AvatarGroup should render AvatarGroup component 1`] = `
{
"alignItems": "center",
"backgroundColor": "#ffffff",
"borderRadius": 8,
"borderRadius": 4,
"borderWidth": 2,
"height": 16,
"justifyContent": "center",
"overflow": "hidden",
@@ -57,7 +58,8 @@ exports[`AvatarGroup should render AvatarGroup component 1`] = `
style={
{
"backgroundColor": "#ffffff",
"borderRadius": 8,
"borderRadius": 4,
"borderWidth": 2,
"height": 16,
"overflow": "hidden",
"width": 16,
@@ -96,7 +98,8 @@ exports[`AvatarGroup should render AvatarGroup component 1`] = `
style={
{
"backgroundColor": "#ffffff",
"borderRadius": 8,
"borderRadius": 4,
"borderWidth": 2,
"height": 16,
"overflow": "hidden",
"width": 16,
@@ -135,7 +138,8 @@ exports[`AvatarGroup should render AvatarGroup component 1`] = `
style={
{
"backgroundColor": "#ffffff",
"borderRadius": 8,
"borderRadius": 4,
"borderWidth": 2,
"height": 16,
"overflow": "hidden",
"width": 16,
26 changes: 6 additions & 20 deletions app/components/UI/AccountSelectorList/AccountSelector.test.tsx
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import {
import { mockNetworkState } from '../../../util/test/network';
import { CHAIN_IDS } from '@metamask/transaction-controller';
import { AccountSelectorListProps } from './AccountSelectorList.types';
import { AVATARGROUP_AVATAR_CONTAINER_TESTID } from '../../../component-library/components/Avatars/AvatarGroup/AvatarGroup.constants';

// eslint-disable-next-line import/no-namespace
import * as Utils from '../../hooks/useAccounts/utils';
@@ -159,12 +160,10 @@ describe('AccountSelectorList', () => {
`${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${PERSONAL_ACCOUNT}`,
);

expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined();
expect(
within(businessAccountItem).getByText(regex.usd(3200)),
).toBeDefined();

expect(within(personalAccountItem).getByText(regex.eth(2))).toBeDefined();
expect(
within(personalAccountItem).getByText(regex.usd(6400)),
).toBeDefined();
@@ -199,7 +198,6 @@ describe('AccountSelectorList', () => {
`${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`,
);

expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined();
expect(
within(businessAccountItem).getByText(regex.usd(3200)),
).toBeDefined();
@@ -267,33 +265,21 @@ describe('AccountSelectorList', () => {
`${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`,
);

expect(within(businessAccountItem).getByText(regex.eth(1))).toBeDefined();
expect(
within(businessAccountItem).getByText(regex.usd(3200)),
).toBeDefined();

expect(within(businessAccountItem).queryByText('••••••')).toBeNull();
});
});
it('Text is hidden when privacy mode is on', async () => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is removed because we are no longer showing the selected network token balance, instead it shows the network avatar icons. See the before and after screenshots

const state = {
...initialState,
privacyMode: true,
};

const { queryByTestId } = renderComponent(state);
it('should render AvatarGroup', async () => {
const { queryByTestId } = renderComponent(initialState);

await waitFor(() => {
const businessAccountItem = queryByTestId(
`${AccountListBottomSheetSelectorsIDs.ACCOUNT_BALANCE_BY_ADDRESS_TEST_ID}-${BUSINESS_ACCOUNT}`,
const avatarGroup = queryByTestId(
`${AVATARGROUP_AVATAR_CONTAINER_TESTID}-0`,
);

expect(within(businessAccountItem).queryByText(regex.eth(1))).toBeNull();
expect(
within(businessAccountItem).queryByText(regex.usd(3200)),
).toBeNull();

expect(within(businessAccountItem).getByText('••••••')).toBeDefined();
expect(avatarGroup).toBeDefined();
});
});
});
Original file line number Diff line number Diff line change
@@ -15,7 +15,12 @@ const styleSheet = () =>
alignItems: 'flex-end',
flexDirection: 'column',
},
balanceLabel: { textAlign: 'right' },
balanceLabel: {
textAlign: 'right',
},
networkTokensContainer: {
marginTop: 4,
},
});

export default styleSheet;
121 changes: 96 additions & 25 deletions app/components/UI/AccountSelectorList/AccountSelectorList.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
// Third party dependencies.
import React, { useCallback, useRef } from 'react';
import { Alert, ListRenderItem, View, ViewStyle } from 'react-native';
import React, { useCallback, useRef, useMemo } from 'react';
import {
Alert,
ListRenderItem,
View,
ViewStyle,
ImageSourcePropType,
} from 'react-native';
import { FlatList } from 'react-native-gesture-handler';
import { useSelector } from 'react-redux';
import { useNavigation } from '@react-navigation/native';
@@ -13,11 +19,11 @@ import Cell, {
} from '../../../component-library/components/Cells/Cell';
import { InternalAccount } from '@metamask/keyring-internal-api';
import { useStyles } from '../../../component-library/hooks';
import { TextColor } from '../../../component-library/components/Texts/Text';
import SensitiveText, {
SensitiveTextLength,
} from '../../../component-library/components/Texts/SensitiveText';
import AvatarGroup from '../../../component-library/components/Avatars/AvatarGroup';
import { AvatarNetworkProps } from '../../../component-library/components/Avatars/Avatar/variants/AvatarNetwork/AvatarNetwork.types';
import {
formatAddress,
getLabelTextByAddress,
@@ -37,6 +43,15 @@ import { AccountSelectorListProps } from './AccountSelectorList.types';
import styleSheet from './AccountSelectorList.styles';
import { AccountListBottomSheetSelectorsIDs } from '../../../../e2e/selectors/wallet/AccountListBottomSheet.selectors';
import { WalletViewSelectorsIDs } from '../../../../e2e/selectors/wallet/WalletView.selectors';
import { getNetworkImageSource } from '../../../util/networks';
import { PopularList } from '../../../util/networks/customNetworks';
import { useMultiAccountChainBalances } from '../../hooks/useMultiAccountChainBalances';
interface AvatarNetworksInfoProps extends AvatarNetworkProps {
name: string;
imageSource: ImageSourcePropType;
chainId: string;
totalFiatBalance: number | undefined;
}

const AccountSelectorList = ({
onSelectAccount,
@@ -71,13 +86,19 @@ const AccountSelectorList = ({
);

const internalAccounts = useSelector(selectInternalAccounts);

const multichainBalances = useMultiAccountChainBalances();
const getKeyExtractor = ({ address }: Account) => address;

const renderAccountBalances = useCallback(
({ fiatBalance, tokens }: Assets, address: string) => {
(
{ fiatBalance }: Assets,
address: string,
networksInfo: AvatarNetworksInfoProps[],
) => {
const fiatBalanceStrSplit = fiatBalance.split('\n');
const fiatBalanceAmount = fiatBalanceStrSplit[0] || '';
const tokenTicker = fiatBalanceStrSplit[1] || '';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer shows the token balance based on selected network. See before and after screenshots


return (
<View
style={styles.balancesContainer}
@@ -90,28 +111,68 @@ const AccountSelectorList = ({
>
{fiatBalanceAmount}
</SensitiveText>
<SensitiveText
length={SensitiveTextLength.Short}
style={styles.balanceLabel}
isHidden={privacyMode}
color={privacyMode ? TextColor.Alternative : TextColor.Default}
>
{tokenTicker}
</SensitiveText>
{tokens && (
<AvatarGroup
avatarPropsList={tokens.map((tokenObj) => ({
...tokenObj,
variant: AvatarVariant.Token,
}))}
/>

{networksInfo && (
<View style={styles.networkTokensContainer}>
<AvatarGroup
avatarPropsList={networksInfo.map(
(networkInfo: AvatarNetworksInfoProps, index: number) => ({
...networkInfo,
variant: AvatarVariant.Network,
imageSource: networkInfo.imageSource,
testID: `avatar-group-${index}`,
}),
)}
maxStackedAvatars={4}
renderOverflowCounter={false}
/>
</View>
)}
</View>
);
},
[styles.balancesContainer, styles.balanceLabel, privacyMode],
[
styles.balancesContainer,
styles.balanceLabel,
styles.networkTokensContainer,
privacyMode,
],
);

const accountsWithNetworkInfo = useMemo(() => {
if (!accounts || !Array.isArray(accounts)) {
return [];
}

return accounts.map((account) => {
const accountBalances =
multichainBalances?.[account?.address?.toLowerCase()] || {};
const chainIds = Object.keys(accountBalances);

const networksInfo = chainIds
.map((chainId) => {
const networkBalanceInfo = accountBalances[chainId];
if (!networkBalanceInfo) return null;
const networkInfo = PopularList.find((n) => n.chainId === chainId);

return {
name: networkInfo?.nickname || `Chain ${chainId}`,
//@ts-expect-error - The utils/network file is still JS and this function expects a networkType, and should be optional
imageSource: getNetworkImageSource({
chainId: chainId.toString(),
}),
chainId,
totalFiatBalance: networkBalanceInfo?.totalFiatBalance ?? 0,
};
})
.filter((item): item is NonNullable<typeof item> => item !== null)
.filter((network) => network.totalFiatBalance > 0)
.sort((a, b) => a.totalFiatBalance - b.totalFiatBalance);

return { ...account, networksInfo };
});
}, [accounts, multichainBalances]);

const onLongPress = useCallback(
({
address,
@@ -193,9 +254,19 @@ const AccountSelectorList = ({
[navigate, internalAccounts],
);

const renderAccountItem: ListRenderItem<Account> = useCallback(
const renderAccountItem: ListRenderItem<
Account & { networksInfo: AvatarNetworksInfoProps[] }
> = useCallback(
({
item: { name, address, assets, type, isSelected, balanceError },
item: {
name,
address,
assets,
type,
isSelected,
balanceError,
networksInfo,
},
index,
}) => {
const shortAddress = formatAddress(address, 'short');
@@ -256,7 +327,7 @@ const AccountSelectorList = ({
}}
>
{renderRightAccessory?.(address, accountName) ||
(assets && renderAccountBalances(assets, address))}
(assets && renderAccountBalances(assets, address, networksInfo))}
</Cell>
);
},
@@ -298,7 +369,7 @@ const AccountSelectorList = ({
<FlatList
ref={accountListRef}
onContentSizeChange={onContentSizeChanged}
data={accounts}
data={accountsWithNetworkInfo}
keyExtractor={getKeyExtractor}
renderItem={renderAccountItem}
// Increasing number of items at initial render fixes scroll issue.
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = `
"balanceError": undefined,
"isSelected": true,
"name": "Account 1",
"networksInfo": [],
"type": "HD Key Tree",
"yOffset": 0,
},
@@ -26,6 +27,7 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = `
"balanceError": undefined,
"isSelected": false,
"name": "Account 2",
"networksInfo": [],
"type": "HD Key Tree",
"yOffset": 78,
},
@@ -359,22 +361,22 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = `
>
$3200.00
</Text>
<Text
accessibilityRole="text"
<View
style={
{
"color": "#141618",
"fontFamily": "EuclidCircularB-Regular",
"fontSize": 14,
"fontWeight": "400",
"letterSpacing": 0,
"lineHeight": 22,
"textAlign": "right",
"marginTop": 4,
}
}
>
1 ETH
</Text>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
}
}
/>
</View>
</View>
</View>
</View>
@@ -759,22 +761,22 @@ exports[`AccountSelectorList renders all accounts with balances 1`] = `
>
$6400.00
</Text>
<Text
accessibilityRole="text"
<View
style={
{
"color": "#141618",
"fontFamily": "EuclidCircularB-Regular",
"fontSize": 14,
"fontWeight": "400",
"letterSpacing": 0,
"lineHeight": 22,
"textAlign": "right",
"marginTop": 4,
}
}
>
2 ETH
</Text>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
}
}
/>
</View>
</View>
</View>
</View>
@@ -842,6 +844,7 @@ exports[`AccountSelectorList renders all accounts with right accessory 1`] = `
"balanceError": undefined,
"isSelected": true,
"name": "Account 1",
"networksInfo": [],
"type": "HD Key Tree",
"yOffset": 0,
},
@@ -854,6 +857,7 @@ exports[`AccountSelectorList renders all accounts with right accessory 1`] = `
"balanceError": undefined,
"isSelected": false,
"name": "Account 2",
"networksInfo": [],
"type": "HD Key Tree",
"yOffset": 78,
},
@@ -1570,6 +1574,7 @@ exports[`AccountSelectorList renders correctly 1`] = `
"balanceError": undefined,
"isSelected": true,
"name": "Account 1",
"networksInfo": [],
"type": "HD Key Tree",
"yOffset": 0,
},
@@ -1582,6 +1587,7 @@ exports[`AccountSelectorList renders correctly 1`] = `
"balanceError": undefined,
"isSelected": false,
"name": "Account 2",
"networksInfo": [],
"type": "HD Key Tree",
"yOffset": 78,
},
@@ -1915,22 +1921,22 @@ exports[`AccountSelectorList renders correctly 1`] = `
>
$3200.00
</Text>
<Text
accessibilityRole="text"
<View
style={
{
"color": "#141618",
"fontFamily": "EuclidCircularB-Regular",
"fontSize": 14,
"fontWeight": "400",
"letterSpacing": 0,
"lineHeight": 22,
"textAlign": "right",
"marginTop": 4,
}
}
>
1 ETH
</Text>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
}
}
/>
</View>
</View>
</View>
</View>
@@ -2315,22 +2321,22 @@ exports[`AccountSelectorList renders correctly 1`] = `
>
$6400.00
</Text>
<Text
accessibilityRole="text"
<View
style={
{
"color": "#141618",
"fontFamily": "EuclidCircularB-Regular",
"fontSize": 14,
"fontWeight": "400",
"letterSpacing": 0,
"lineHeight": 22,
"textAlign": "right",
"marginTop": 4,
}
}
>
2 ETH
</Text>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
}
}
/>
</View>
</View>
</View>
</View>
@@ -2398,6 +2404,7 @@ exports[`AccountSelectorList should render all accounts but only the balance for
"balanceError": undefined,
"isSelected": true,
"name": "Account 1",
"networksInfo": [],
"type": "HD Key Tree",
"yOffset": 0,
},
@@ -2407,6 +2414,7 @@ exports[`AccountSelectorList should render all accounts but only the balance for
"balanceError": undefined,
"isSelected": false,
"name": "Account 2",
"networksInfo": [],
"type": "HD Key Tree",
"yOffset": 78,
},
@@ -2624,22 +2632,22 @@ exports[`AccountSelectorList should render all accounts but only the balance for
>
$3200.00
</Text>
<Text
accessibilityRole="text"
<View
style={
{
"color": "#141618",
"fontFamily": "EuclidCircularB-Regular",
"fontSize": 14,
"fontWeight": "400",
"letterSpacing": 0,
"lineHeight": 22,
"textAlign": "right",
"marginTop": 4,
}
}
>
1 ETH
</Text>
<View
style={
{
"alignItems": "center",
"flexDirection": "row",
}
}
/>
</View>
</View>
</View>
</View>
Original file line number Diff line number Diff line change
@@ -189,6 +189,7 @@ exports[`AccountConnectMultiSelector renders correctly 1`] = `
"balance": "0x1",
"isSelected": false,
"name": "Account 1",
"networksInfo": [],
"type": "Simple Key Pair",
"yOffset": 0,
},
@@ -197,6 +198,7 @@ exports[`AccountConnectMultiSelector renders correctly 1`] = `
"balance": "0x2",
"isSelected": false,
"name": "Account 2",
"networksInfo": [],
"type": "Simple Key Pair",
"yOffset": 0,
},
Original file line number Diff line number Diff line change
@@ -550,8 +550,8 @@ exports[`AccountConnect renders correctly 1`] = `
"alignItems": "center",
"backgroundColor": "#ffffff",
"borderColor": "#ffffff",
"borderRadius": 8,
"borderWidth": 1.5,
"borderRadius": 4,
"borderWidth": 2,
"height": 16,
"justifyContent": "center",
"overflow": "hidden",
@@ -588,8 +588,8 @@ exports[`AccountConnect renders correctly 1`] = `
"alignItems": "center",
"backgroundColor": "#ffffff",
"borderColor": "#ffffff",
"borderRadius": 8,
"borderWidth": 1.5,
"borderRadius": 4,
"borderWidth": 2,
"height": 16,
"justifyContent": "center",
"overflow": "hidden",
@@ -626,8 +626,8 @@ exports[`AccountConnect renders correctly 1`] = `
"alignItems": "center",
"backgroundColor": "#ffffff",
"borderColor": "#ffffff",
"borderRadius": 8,
"borderWidth": 1.5,
"borderRadius": 4,
"borderWidth": 2,
"height": 16,
"justifyContent": "center",
"overflow": "hidden",
@@ -664,8 +664,8 @@ exports[`AccountConnect renders correctly 1`] = `
"alignItems": "center",
"backgroundColor": "#ffffff",
"borderColor": "#ffffff",
"borderRadius": 8,
"borderWidth": 1.5,
"borderRadius": 4,
"borderWidth": 2,
"height": 16,
"justifyContent": "center",
"overflow": "hidden",
206 changes: 183 additions & 23 deletions app/components/Views/AccountSelector/AccountSelector.test.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,102 @@
// Third party dependencies.
import React from 'react';
import { screen } from '@testing-library/react-native';
import AccountSelector from './AccountSelector';
import {
NetworkConfiguration,
NetworkStatus,
RpcEndpointType,
} from '@metamask/network-controller';
import { KeyringTypes } from '@metamask/keyring-controller';
import { Hex } from '@metamask/utils';

// External dependencies.
import { renderScreen } from '../../../util/test/renderWithProvider';
import { AccountListBottomSheetSelectorsIDs } from '../../../../e2e/selectors/wallet/AccountListBottomSheet.selectors';
import Routes from '../../../constants/navigation/Routes';
import {
MOCK_ACCOUNTS_CONTROLLER_STATE,
MOCK_ADDRESS_1,
MOCK_ADDRESS_2,
} from '../../../util/test/accountsControllerTestUtils';
import { NETWORK_CHAIN_ID } from '../../../util/networks/customNetworks';

// Internal dependencies.
import {
AccountSelectorParams,
AccountSelectorProps,
} from './AccountSelector.types';
import {
MOCK_ACCOUNTS_CONTROLLER_STATE,
expectedUuid2,
} from '../../../util/test/accountsControllerTestUtils';
import AccountSelector from './AccountSelector';

const mockAccounts = [
{
address: '0xc4966c0d659d99699bfd7eb54d8fafee40e4a756',
address: MOCK_ADDRESS_1,
balance: '0x0',
name: 'Account 1',
type: KeyringTypes.hd,
yOffset: 0,
isSelected: false,
assets: {
fiatBalance: '$0\nETH',
},
balanceError: undefined,
},
{
address: '0x2B5634C42055806a59e9107ED44D43c426E58258',
address: MOCK_ADDRESS_2,
balance: '0x0',
name: 'Account 2',
type: KeyringTypes.hd,
yOffset: 78,
isSelected: true,
assets: {
fiatBalance: '$0.01\nETH',
},
balanceError: undefined,
},
];

const MOCK_TOKEN_ADDRESS_1 = '0x378afc9a77b47a30';
const MOCK_TOKEN_ADDRESS_2 = '0x2f18e6';
const MOCK_TOKEN_ADDRESS_3 = '0x5d512b2498936';
const MOCK_TOKEN_ADDRESS_4 = '0x0D1E753a25eBda689453309112904807625bEFBe';

const mockEnsByAccountAddress = {
'0xc4966c0d659d99699bfd7eb54d8fafee40e4a756': 'test.eth',
[MOCK_ADDRESS_1]: 'test_1.eth',
[MOCK_ADDRESS_2]: 'test_2.eth',
};

const mockNetworkConfigurations: Record<Hex, NetworkConfiguration> = {
[NETWORK_CHAIN_ID.MAINNET]: {
chainId: NETWORK_CHAIN_ID.MAINNET,
nativeCurrency: 'ETH',
name: 'Ethereum Mainnet',
defaultBlockExplorerUrlIndex: 0,
defaultRpcEndpointIndex: 0,
rpcEndpoints: [
{
networkClientId: 'infura-mainnet',
type: RpcEndpointType.Custom,
url: 'https://mainnet.infura.io/v3/{infuraProjectId}',
name: 'Infura',
},
],
blockExplorerUrls: ['https://etherscan.io'],
},
[NETWORK_CHAIN_ID.POLYGON]: {
chainId: NETWORK_CHAIN_ID.POLYGON,
nativeCurrency: 'MATIC',
name: 'Polygon',
defaultBlockExplorerUrlIndex: 0,
defaultRpcEndpointIndex: 0,
rpcEndpoints: [
{
networkClientId: 'custom-network',
type: RpcEndpointType.Custom,
url: 'https://polygon-rpc.com',
name: 'Polygon RPC',
},
],
blockExplorerUrls: ['https://polygonscan.com'],
},
};

const mockInitialState = {
@@ -37,30 +106,92 @@ const mockInitialState = {
keyrings: [
{
type: 'HD Key Tree',
accounts: [
'0xc4966c0d659d99699bfd7eb54d8fafee40e4a756',
'0x2B5634C42055806a59e9107ED44D43c426E58258',
],
accounts: [MOCK_ADDRESS_1, MOCK_ADDRESS_2],
},
],
},
AccountsController: {
...MOCK_ACCOUNTS_CONTROLLER_STATE,
internalAccounts: {
...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts,
accounts: {
...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts.accounts,
[expectedUuid2]: {
...MOCK_ACCOUNTS_CONTROLLER_STATE.internalAccounts.accounts[
expectedUuid2
],
methods: [],
MultichainNetworkController: {
isEvmSelected: true,
selectedMultichainNetworkChainId: undefined,
multichainNetworkConfigurationsByChainId: {},
},
TokenRatesController: {
contractExchangeRates: {},
marketData: {
'0x1': {
[MOCK_TOKEN_ADDRESS_1]: {
price: 3000,
},
[MOCK_TOKEN_ADDRESS_2]: {
price: 1000,
},
},
'0x89': {
[MOCK_TOKEN_ADDRESS_3]: {
price: 5000,
},
[MOCK_TOKEN_ADDRESS_4]: {
price: 2000,
},
},
},
},
AccountsController: MOCK_ACCOUNTS_CONTROLLER_STATE,
AccountTrackerController: {
accountsByChainId: {
[NETWORK_CHAIN_ID.MAINNET]: {},
[NETWORK_CHAIN_ID.POLYGON]: {},
},
accounts: {
[MOCK_ADDRESS_1]: {
balance: '0x0',
stakedBalance: '0x0',
},
[MOCK_ADDRESS_2]: {
balance: '0x5',
},
},
},
NetworkController: {
selectedNetworkClientId: 'custom-network',
networkConfigurationsByChainId: mockNetworkConfigurations,
networksMetadata: {
'custom-network': {
status: NetworkStatus.Available,
EIPS: {},
},
},
},
TokenBalancesController: {
tokenBalances: {
[MOCK_ADDRESS_1.toLowerCase() as Hex]: {
[NETWORK_CHAIN_ID.MAINNET]: {
[MOCK_TOKEN_ADDRESS_1]: '0x378afc9a77b47a30' as Hex,
[MOCK_TOKEN_ADDRESS_2]: '0x2f18e6' as Hex,
},
[NETWORK_CHAIN_ID.POLYGON]: {
[MOCK_TOKEN_ADDRESS_3]: '0x5d512b2498936' as Hex,
[MOCK_TOKEN_ADDRESS_4]: '0x5d512b2498936' as Hex,
},
},
[MOCK_ADDRESS_2.toLowerCase() as Hex]: {
[NETWORK_CHAIN_ID.MAINNET]: {
[MOCK_TOKEN_ADDRESS_1]: '0x378afc9a77b47a30' as Hex,
[MOCK_TOKEN_ADDRESS_2]: '0x2f18e6' as Hex,
},
[NETWORK_CHAIN_ID.POLYGON]: {
[MOCK_TOKEN_ADDRESS_3]: '0x5d512b2498936' as Hex,
[MOCK_TOKEN_ADDRESS_4]: '0x5d512b2498936' as Hex,
},
},
},
},
PreferencesController: {
privacyMode: false,
tokenNetworkFilter: {
[NETWORK_CHAIN_ID.MAINNET]: 'true',
[NETWORK_CHAIN_ID.POLYGON]: 'true',
},
},
},
},
@@ -72,6 +203,35 @@ const mockInitialState = {
},
};

jest.mock('../../../components/hooks/useMultiAccountChainBalances', () => ({
useMultiAccountChainBalances: jest.fn().mockReturnValue({
['0xC4955C0d639D99699Bfd7Ec54d9FaFEe40e4D272'.toLowerCase()]: {
['0x1' as Hex]: {
totalNativeFiatBalance: 100,
totalImportedTokenFiatBalance: 50,
totalFiatBalance: 150,
},
['0x89' as Hex]: {
totalNativeFiatBalance: 20,
totalImportedTokenFiatBalance: 10,
totalFiatBalance: 30,
},
},
['0xC4966c0D659D99699BFD7EB54D8fafEE40e4a756'.toLowerCase()]: {
['0x1' as Hex]: {
totalNativeFiatBalance: 200,
totalImportedTokenFiatBalance: 100,
totalFiatBalance: 300,
},
['0x89' as Hex]: {
totalNativeFiatBalance: 40,
totalImportedTokenFiatBalance: 20,
totalFiatBalance: 60,
},
},
}),
}));

// Mock the Redux dispatch
const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
1 change: 1 addition & 0 deletions app/components/Views/AccountSelector/AccountSelector.tsx
Original file line number Diff line number Diff line change
@@ -58,6 +58,7 @@ const AccountSelector = ({ route }: AccountSelectorProps) => {
const { accounts, ensByAccountAddress } = useAccounts({
checkBalanceError,
isLoading: reloadAccounts,
shouldAggregateAcrossChains: true,
});
const [screen, setScreen] = useState<AccountSelectorScreens>(
navigateToAddAccountActions ?? AccountSelectorScreens.AccountSelector,
Original file line number Diff line number Diff line change
@@ -461,6 +461,7 @@ exports[`AccountSelector should render correctly 1`] = `
</View>
<RCTScrollView
collapsable={false}
data={[]}
getItem={[Function]}
getItemCount={[Function]}
initialNumToRender={999}
7 changes: 6 additions & 1 deletion app/components/hooks/useAccounts/useAccounts.ts
Original file line number Diff line number Diff line change
@@ -44,11 +44,16 @@ import { getAccountBalances } from './utils';
/**
* Hook that returns both wallet accounts and ens name information.
*
* @param checkBalanceErrorFn - Function to check balance error.
* @param isLoading - Whether the accounts are loading.
* @param shouldAggregateAcrossChains - Whether to aggregate across chains.
*
* @returns Object that contains both wallet accounts and ens name information.
*/
const useAccounts = ({
checkBalanceError: checkBalanceErrorFn,
isLoading = false,
shouldAggregateAcrossChains = false,
}: UseAccountsParams = {}): UseAccounts => {
const isMountedRef = useRef(false);
const [accounts, setAccounts] = useState<Account[]>([]);
@@ -73,7 +78,7 @@ const useAccounts = ({
);
const formattedTokensWithBalancesPerChain = useGetFormattedTokensPerChain(
internalAccounts,
!isTokenNetworkFilterEqualCurrentNetwork,
shouldAggregateAcrossChains || !isTokenNetworkFilterEqualCurrentNetwork,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This arg is added to allow the opting out of the popular network filter. Going forward we should always show the aggregated balance of each account in the app/components/UI/AccountSelectorList/AccountSelectorList.tsx. The network filter should not affect the AccountSelectorList component and should only affect the token list

allChainIDs,
);
const totalFiatBalancesCrossChain = useGetTotalFiatBalanceCrossChains(
5 changes: 5 additions & 0 deletions app/components/hooks/useAccounts/useAccounts.types.ts
Original file line number Diff line number Diff line change
@@ -72,6 +72,11 @@ export interface UseAccountsParams {
* @default false
*/
isLoading?: boolean;
/**
* Optional boolean that indicates if the accounts should be aggregated across chains.
* @default true
*/
shouldAggregateAcrossChains?: boolean;
}

/**
6 changes: 3 additions & 3 deletions app/components/hooks/useGetFormattedTokensPerChain.tsx
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@ import { InternalAccount } from '@metamask/keyring-internal-api';
import { isTestNet } from '../../util/networks';
import { selectShowFiatInTestnets } from '../../selectors/settings';

interface AllTokens {
export interface AllTokens {
[chainId: string]: {
[tokenAddress: string]: Token[];
};
@@ -34,13 +34,13 @@ export interface TokensWithBalances {
tokenBalanceFiat: number;
}

interface AddressMapping {
export interface AddressMapping {
[chainId: string]: {
[tokenAddress: string]: string;
};
}

interface TokenBalancesMapping {
export interface TokenBalancesMapping {
[address: string]: AddressMapping;
}

610 changes: 610 additions & 0 deletions app/components/hooks/useMultiAccountChainBalances/index.test.ts

Large diffs are not rendered by default.

137 changes: 137 additions & 0 deletions app/components/hooks/useMultiAccountChainBalances/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Third party dependencies
import { useSelector } from 'react-redux';
import { Token } from '@metamask/assets-controllers';
import { hexToBN, toChecksumHexAddress } from '@metamask/controller-utils';

// External dependencies
import { selectAllTokens } from '../../../selectors/tokensController';
import { selectTokenMarketData } from '../../../selectors/tokenRatesController';
import { selectAllTokenBalances } from '../../../selectors/tokenBalancesController';
import { selectAccountsByChainId } from '../../../selectors/accountTrackerController';
import { selectNetworkConfigurations } from '../../../selectors/networkController';
import {
selectCurrentCurrency,
selectCurrencyRates,
} from '../../../selectors/currencyRateController';
import { selectShowFiatInTestnets } from '../../../selectors/settings';
import {
MarketDataMapping,
TokenBalancesMapping,
AllTokens,
} from '../../hooks/useGetFormattedTokensPerChain';
import {
balanceToFiatNumber,
renderFromTokenMinimalUnit,
toHexadecimal,
weiToFiatNumber,
} from '../../../util/number';
import { isTestNet } from '../../../util/networks';

// Internal dependencies
import { ChainFiatBalances } from './index.types';

/**
* Hook to view portfolio balance data for each chain for all accounts.
*
* @returns Portfolio balance data for each chain for all accounts
*/
export const useMultiAccountChainBalances = (): ChainFiatBalances => {
const allTokenBalances: TokenBalancesMapping = useSelector(
selectAllTokenBalances,
);
const importedTokens: AllTokens = useSelector(selectAllTokens);
const allNetworks: Record<
string,
{
name: string;
nativeCurrency: string;
}
> = useSelector(selectNetworkConfigurations);
const marketData: MarketDataMapping = useSelector(selectTokenMarketData);
const currentCurrency = useSelector(selectCurrentCurrency);
const currencyRates = useSelector(selectCurrencyRates);
const accountsByChainId = useSelector(selectAccountsByChainId);
const showFiatOnTestnets = useSelector(selectShowFiatInTestnets);

const result: ChainFiatBalances = {};
for (const [accountAddress, tokenBalances] of Object.entries(
allTokenBalances,
)) {
let currentChainId = '';

result[accountAddress] = {};
for (const [chainId, tokenBalance] of Object.entries(tokenBalances)) {
if (isTestNet(chainId) && showFiatOnTestnets) {
return {};
}

currentChainId = chainId;
result[accountAddress][chainId] = {
totalNativeFiatBalance: 0,
totalImportedTokenFiatBalance: 0,
totalFiatBalance: 0,
};

// Calculate the imported token balance
const tokens: Token[] = importedTokens?.[chainId]?.[accountAddress] ?? [];
const matchedChainSymbol = allNetworks[chainId].nativeCurrency;
const conversionRate =
currencyRates?.[matchedChainSymbol]?.conversionRate ?? 0;
const tokenExchangeRates = marketData?.[toHexadecimal(chainId)];
const decimalsToShow = (currentCurrency === 'usd' && 2) || undefined;

for (const token of tokens) {
const hexBalance = tokenBalance?.[token.address] ?? '0x0';

const decimalBalance = renderFromTokenMinimalUnit(
hexBalance,
token.decimals,
);
const exchangeRate = tokenExchangeRates?.[token.address]?.price;

const tokenBalanceFiat = balanceToFiatNumber(
decimalBalance,
conversionRate,
exchangeRate,
decimalsToShow,
);

result[accountAddress][chainId].totalImportedTokenFiatBalance +=
tokenBalanceFiat;

// Calculate the native token balance
const balanceBN = hexToBN(
accountsByChainId[toHexadecimal(chainId)][
toChecksumHexAddress(accountAddress)
].balance,
);

const stakedBalanceBN = hexToBN(
accountsByChainId[toHexadecimal(chainId)][
toChecksumHexAddress(accountAddress)
].stakedBalance || '0x00',
);
const totalAccountBalance = balanceBN
.add(stakedBalanceBN)
.toString('hex');
const ethFiat = weiToFiatNumber(
totalAccountBalance,
conversionRate,
decimalsToShow,
);

result[accountAddress][chainId].totalNativeFiatBalance = ethFiat;
}

const currentChainNativeFiatBalance =
result[accountAddress][currentChainId].totalNativeFiatBalance;
const currentChainImportedTokenFiatBalance =
result[accountAddress][currentChainId].totalImportedTokenFiatBalance;

result[accountAddress][currentChainId].totalFiatBalance =
currentChainNativeFiatBalance + currentChainImportedTokenFiatBalance;
}
}

return result;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export interface ChainFiatBalances {
[address: string]: {
[chainId: string]: {
totalNativeFiatBalance: number;
totalImportedTokenFiatBalance: number;
totalFiatBalance: number;
};
};
}
Original file line number Diff line number Diff line change
@@ -31,7 +31,7 @@ import { selectIsEvmNetworkSelected } from '../../../selectors/multichainNetwork
import I18n from 'i18n-js';

/**
* Hook to manage portfolio balance data across chains.
* Hook to manage portfolio balance data across chains for a single account.
*
* @returns Portfolio balance data
*/

Unchanged files with check annotations Beta

<Svg width={width} height={height} viewBox="0 0 401 376" fill="none">
<Path
d="M383.205 167.402C382.77 167.402 382.335 167.287 381.958 167.055L348.55 147.294C347.738 146.802 347.274 145.877 347.361 144.951C347.477 143.996 348.115 143.215 349.014 142.926L397.183 127.273C398.082 126.984 399.068 127.244 399.706 127.939C400.344 128.633 400.518 129.646 400.17 130.514L385.409 165.927C385.148 166.563 384.597 167.084 383.93 167.287C383.698 167.344 383.466 167.402 383.205 167.402ZM355.626 145.848L382.016 161.471L393.674 133.494L355.626 145.848Z"
fill="#75C4FD"

Check warning on line 54 in app/components/UI/Swaps/components/LoadingAnimation/ShapesBackgroundAnimation.tsx

GitHub Actions / scripts (lint)

'#75C4FD' Hex color values are not allowed. Consider using design tokens instead. For support reach out to the design system team #metamask-design-system on Slack
/>
<Path
d="M317.736 91.7651C317.359 91.7651 316.953 91.7362 316.576 91.6783C311.704 91.0418 308.282 86.5862 308.891 81.7256C309.21 79.3821 310.399 77.27 312.284 75.8234C314.169 74.3768 316.518 73.7403 318.867 74.0585C321.216 74.3768 323.333 75.563 324.783 77.4436C326.233 79.3242 326.871 81.6677 326.552 84.0112C325.943 88.5247 322.115 91.7651 317.736 91.7651ZM317.707 76.7492C316.344 76.7492 315.039 77.1832 313.966 78.0223C312.661 79.0349 311.82 80.4815 311.617 82.1017C311.182 85.4579 313.56 88.5536 316.924 89.0165C320.288 89.4505 323.391 87.0781 323.855 83.7219C324.058 82.1017 323.623 80.4815 322.637 79.1795C321.622 77.8776 320.172 77.0386 318.548 76.836C318.258 76.7492 317.968 76.7492 317.707 76.7492Z"
fill="#86E29B"

Check warning on line 58 in app/components/UI/Swaps/components/LoadingAnimation/ShapesBackgroundAnimation.tsx

GitHub Actions / scripts (lint)

'#86E29B' Hex color values are not allowed. Consider using design tokens instead. For support reach out to the design system team #metamask-design-system on Slack
/>
<Path
d="M46.8495 93.5556C46.7045 93.5556 46.5595 93.5556 46.4145 93.5556C43.7756 93.4399 41.3396 92.3116 39.5416 90.3731C35.8587 86.3804 36.1197 80.1021 40.1216 76.4277C44.1236 72.7533 50.4165 73.0137 54.0994 77.0063C55.8974 78.9448 56.8254 81.4619 56.7094 84.0948C56.5934 86.7276 55.4624 89.1579 53.5195 90.9517C51.6925 92.6587 49.3435 93.5556 46.8495 93.5556ZM46.8205 76.5723C45.0806 76.5723 43.3696 77.1799 41.9776 78.453C39.0776 81.1147 38.9036 85.6282 41.5716 88.5214C42.8766 89.9102 44.6166 90.7492 46.5306 90.8071C48.4445 90.8939 50.2425 90.2284 51.6635 88.9265C53.0555 87.6245 53.8965 85.8886 53.9544 83.979C54.0414 82.0695 53.3745 80.2757 52.0695 78.858C50.6775 77.3535 48.7635 76.5723 46.8205 76.5723Z"
fill="#FFB0EB"

Check warning on line 62 in app/components/UI/Swaps/components/LoadingAnimation/ShapesBackgroundAnimation.tsx

GitHub Actions / scripts (lint)

'#FFB0EB' Hex color values are not allowed. Consider using design tokens instead. For support reach out to the design system team #metamask-design-system on Slack
/>
<Path
d="M272.757 111.15C274.614 111.15 276.121 109.647 276.121 107.794C276.121 105.94 274.614 104.438 272.757 104.438C270.899 104.438 269.393 105.94 269.393 107.794C269.393 109.647 270.899 111.15 272.757 111.15Z"
/>
<Path
d="M60.506 283.551C60.013 283.551 59.52 283.406 59.114 283.146C58.389 282.683 57.954 281.902 57.954 281.063L57.287 252.853C57.258 251.899 57.78 251.002 58.621 250.539C59.462 250.076 60.506 250.134 61.289 250.655L87.6207 268.188C88.4037 268.708 88.8386 269.634 88.7516 270.589C88.6646 271.544 88.0557 272.354 87.1857 272.73L61.521 283.406C61.173 283.493 60.825 283.551 60.506 283.551ZM62.536 257.656L63 277.157L80.7478 269.779L62.536 257.656Z"
fill="#FFB0EB"

Check warning on line 94 in app/components/UI/Swaps/components/LoadingAnimation/ShapesBackgroundAnimation.tsx

GitHub Actions / scripts (lint)

'#FFB0EB' Hex color values are not allowed. Consider using design tokens instead. For support reach out to the design system team #metamask-design-system on Slack
/>
<Path
d="M227.109 375.419C226.587 375.419 226.065 375.246 225.63 374.927L203.126 357.857C202.401 357.308 202.024 356.382 202.198 355.485C202.372 354.588 203.01 353.836 203.88 353.575L234.04 343.97C234.91 343.709 235.838 343.912 236.505 344.548C237.143 345.185 237.404 346.111 237.143 346.979L229.487 373.654C229.255 374.436 228.675 375.043 227.892 375.304C227.631 375.39 227.37 375.419 227.109 375.419ZM209.854 356.816L225.746 368.881L231.14 350.046L209.854 356.816Z"
/>
<Path
d="M343.904 258.709C343.817 258.709 343.759 258.709 343.672 258.709C334.595 258.593 327.287 251.1 327.403 242.044C327.519 232.988 335.001 225.697 344.107 225.813C353.183 225.929 360.491 233.422 360.375 242.478C360.259 251.476 352.893 258.709 343.904 258.709ZM343.904 230.095C337.263 230.095 331.782 235.447 331.695 242.102C331.608 248.814 337.002 254.34 343.73 254.427C350.458 254.543 355.996 249.132 356.083 242.42C356.17 235.708 350.776 230.182 344.049 230.095C344.02 230.095 343.962 230.095 343.904 230.095Z"
fill="#86E29B"

Check warning on line 106 in app/components/UI/Swaps/components/LoadingAnimation/ShapesBackgroundAnimation.tsx

GitHub Actions / scripts (lint)

'#86E29B' Hex color values are not allowed. Consider using design tokens instead. For support reach out to the design system team #metamask-design-system on Slack
/>
<Path
d="M79.4075 190.631C79.3205 190.631 79.2625 190.631 79.1755 190.631C70.0986 190.516 62.7906 183.022 62.9066 173.966C63.0226 164.91 70.5046 157.62 79.6105 157.735C88.6874 157.851 95.9953 165.344 95.8793 174.4C95.7633 183.398 88.3974 190.631 79.4075 190.631ZM79.4075 162.017C72.7665 162.017 67.2856 167.37 67.1986 174.024C67.1116 180.736 72.5056 186.263 79.2335 186.349C85.9614 186.465 91.5003 181.055 91.5873 174.342C91.6743 167.63 86.2804 162.104 79.5525 162.017C79.5235 162.017 79.4655 162.017 79.4075 162.017Z"
fill="#86E29B"

Check warning on line 110 in app/components/UI/Swaps/components/LoadingAnimation/ShapesBackgroundAnimation.tsx

GitHub Actions / scripts (lint)

'#86E29B' Hex color values are not allowed. Consider using design tokens instead. For support reach out to the design system team #metamask-design-system on Slack
/>
<Defs>
<LinearGradient
y2="107.806"
gradientUnits="userSpaceOnUse"
>
<Stop stopColor="#75C3FC" />

Check warning on line 121 in app/components/UI/Swaps/components/LoadingAnimation/ShapesBackgroundAnimation.tsx

GitHub Actions / scripts (lint)

'#75C3FC' Hex color values are not allowed. Consider using design tokens instead. For support reach out to the design system team #metamask-design-system on Slack
<Stop offset="1" stopColor="#75C3FC" />

Check warning on line 122 in app/components/UI/Swaps/components/LoadingAnimation/ShapesBackgroundAnimation.tsx

GitHub Actions / scripts (lint)

'#75C3FC' Hex color values are not allowed. Consider using design tokens instead. For support reach out to the design system team #metamask-design-system on Slack
</LinearGradient>
<LinearGradient
id="paint1_linear"
y2="201.861"
gradientUnits="userSpaceOnUse"
>
<Stop stopColor="#75C3FC" />

Check warning on line 132 in app/components/UI/Swaps/components/LoadingAnimation/ShapesBackgroundAnimation.tsx

GitHub Actions / scripts (lint)

'#75C3FC' Hex color values are not allowed. Consider using design tokens instead. For support reach out to the design system team #metamask-design-system on Slack
<Stop offset="1" stopColor="#75C3FC" />

Check warning on line 133 in app/components/UI/Swaps/components/LoadingAnimation/ShapesBackgroundAnimation.tsx

GitHub Actions / scripts (lint)

'#75C3FC' Hex color values are not allowed. Consider using design tokens instead. For support reach out to the design system team #metamask-design-system on Slack
</LinearGradient>
<LinearGradient
id="paint2_linear"