Skip to content

feat: add asset selector #14958

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

Merged
merged 86 commits into from
May 7, 2025
Merged
Show file tree
Hide file tree
Changes from 83 commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
b23f666
feat: add snap ui selector files
hmalik88 Apr 25, 2025
5580929
feat: add asset selector component mapper
hmalik88 Apr 25, 2025
a3a5752
feat: add multichain balances selector
hmalik88 Apr 25, 2025
53aed51
test: add multichain balances selector test file
hmalik88 Apr 25, 2025
b894e15
feat: add getMemoizedInternalAccountByAddress selector
hmalik88 Apr 25, 2025
d2ae8d6
Merge branch 'main' into hm/add-asset-selector
hmalik88 Apr 25, 2025
74a3e5c
feat: add token image assets
hmalik88 Apr 28, 2025
c7ce5a9
feat: update textfield styles to accept custom input styles
hmalik88 Apr 28, 2025
39f0246
feat: add inputStyle prop to textfield and update prop type
hmalik88 Apr 28, 2025
a3a07c8
feat: adjust component logic to mobile
hmalik88 Apr 28, 2025
4bcdfcb
feat: add useSnapAssetDisplay hook
hmalik88 Apr 28, 2025
5cf0a6f
feat: pass inputStyle prop to textfield
hmalik88 Apr 28, 2025
9297fab
feat: add param to indicate parent container flex direction
hmalik88 Apr 28, 2025
3758b15
feat: add asset selector mapping logic to field
hmalik88 Apr 28, 2025
75c9750
feat: add asset selector to component mappping
hmalik88 Apr 28, 2025
1c897ed
feat: update types to include parentIsFlexRow
hmalik88 Apr 28, 2025
1b8ac18
feat: add asset selector to safe component list
hmalik88 Apr 28, 2025
fe85d3e
feat: update selector styles, update option type and selected option …
hmalik88 Apr 28, 2025
c025b46
feat: add asset selector translation
hmalik88 Apr 28, 2025
eb64e52
feat: add chain ids maps, token image paths, chain id to token image …
hmalik88 Apr 28, 2025
61ec635
feat: add token image getter function
hmalik88 Apr 28, 2025
2acbc37
Merge branch 'main' into hm/add-asset-selector
hmalik88 Apr 28, 2025
e3c1a6d
refactor: use strings directly for translation
hmalik88 Apr 29, 2025
9e90ce0
chore: update types to allow for jpg and jpeg imports
hmalik88 Apr 29, 2025
89191fd
fix: import image paths
hmalik88 Apr 29, 2025
c3006b5
chore: update function return type
hmalik88 Apr 29, 2025
22ee1fe
Merge branch 'main' into hm/add-asset-selector
hmalik88 Apr 29, 2025
33c44eb
chore: add token types
hmalik88 Apr 29, 2025
fb92ead
fix: use correct import for token type and use correct getter for tok…
hmalik88 Apr 29, 2025
1c008bc
Revert "feat: add token image assets"
hmalik88 Apr 29, 2025
fb805ee
Revert "feat: add chain ids maps, token image paths, chain id to toke…
hmalik88 Apr 29, 2025
b932f59
chore: remove unused imports and variables
hmalik88 Apr 29, 2025
e37cd1c
chore: remove unused image getter
hmalik88 Apr 29, 2025
ab5c74e
chore: undo jpg and jpeg type declarations
hmalik88 Apr 29, 2025
bc32860
chore: remove inputStyle prop
hmalik88 Apr 30, 2025
8efa6f0
refactor: use style prop for input styling
hmalik88 Apr 30, 2025
8e4f921
fix: update styling
hmalik88 Apr 30, 2025
4bd34ef
fix: use correct params in useSnapAssetDisplay
hmalik88 Apr 30, 2025
b0b55b8
fix: pass correct style prop from field component to asset selector
hmalik88 Apr 30, 2025
4322229
chore: remove unused import
hmalik88 Apr 30, 2025
8e1670d
chore: update prop type to include isParentFlexRow
hmalik88 Apr 30, 2025
a4c65e9
fix: make balance required in token type
hmalik88 Apr 30, 2025
c98c074
Merge branch 'main' into hm/add-asset-selector
hmalik88 Apr 30, 2025
ba83399
fix: update styling to get it closer to design
hmalik88 May 2, 2025
4d5e366
Merge branch 'main' into hm/add-asset-selector
hmalik88 May 2, 2025
de553f6
chore: remove unused vars
hmalik88 May 2, 2025
5b0e011
Merge branch 'main' into hm/add-asset-selector
hmalik88 May 2, 2025
eb272a0
fix: styling
hmalik88 May 5, 2025
bf67cd7
Merge branch 'main' into hm/add-asset-selector
hmalik88 May 5, 2025
ff3f18a
chore: add comment explaining parentSize
hmalik88 May 5, 2025
8fea18b
fix: change comment in badge wrapper types file
hmalik88 May 5, 2025
9104aea
chore: add comment explaining display logic
hmalik88 May 5, 2025
9d0ebf5
chore: add comment explaining style change for snap ui input
hmalik88 May 5, 2025
2be94aa
chore: add comment explaining isParentFlexRow prop
hmalik88 May 5, 2025
f881fee
chore: add comments explaining cloning elements to pass the context prop
hmalik88 May 5, 2025
efa35ed
fix: update styling of badge element to be consistent with other usag…
hmalik88 May 5, 2025
8c41659
fix: use the primary muted as background color for selected option
hmalik88 May 5, 2025
aa434d4
refactor: remove asset types file and write simple token interface in…
hmalik88 May 5, 2025
922ce0b
refactor: rename isParentFlexRow to compact at the snap ui selector l…
hmalik88 May 5, 2025
e0d438a
chore: remove unused import
hmalik88 May 5, 2025
f82c2bc
test: add multichainBalancesController test
hmalik88 May 5, 2025
6b53aca
Merge branch 'main' into hm/add-asset-selector
hmalik88 May 5, 2025
b131430
test: creat asset selector test file
hmalik88 May 6, 2025
9aab267
Merge branch 'main' into hm/add-asset-selector
hmalik88 May 6, 2025
804caa6
chore: add test ids and update key to use toString to account for non…
hmalik88 May 7, 2025
b257277
fix: move backgroundState spread after defined state
hmalik88 May 7, 2025
d2e2105
test: add tests and create snapshot
hmalik88 May 7, 2025
c19f778
Merge branch 'main' into hm/add-asset-selector
hmalik88 May 7, 2025
2ffc82c
chore: remove extraneous files
hmalik88 May 7, 2025
fb4c9a3
fix: update snapshots
hmalik88 May 7, 2025
e169d1c
fix: update input snapshot
hmalik88 May 7, 2025
9ab0e6c
fix: use index for key
hmalik88 May 7, 2025
567bedc
refactor: address review comments
hmalik88 May 7, 2025
1f73907
Merge branch 'main' into hm/add-asset-selector
hmalik88 May 7, 2025
b8ded61
chore: actually remove unnecessary files
hmalik88 May 7, 2025
a769886
refactor: update translation structure
hmalik88 May 7, 2025
c956448
style: remove comma
hmalik88 May 7, 2025
c8ad6ab
refactor: use badge component for badge element
hmalik88 May 7, 2025
236f01c
Merge branch 'main' into hm/add-asset-selector
hmalik88 May 7, 2025
f2b729e
chore: update snapshot
hmalik88 May 7, 2025
e01bc78
fix: add missing style properties
hmalik88 May 7, 2025
2794bb4
style: apply prettier
hmalik88 May 7, 2025
9dba241
Merge branch 'main' into hm/add-asset-selector
hmalik88 May 7, 2025
a5a721a
chore: update snapshot again
hmalik88 May 7, 2025
16a6b50
fix: add extra check to make sure selectedOptionComponent is defined
hmalik88 May 7, 2025
36cda02
Merge branch 'main' into hm/add-asset-selector
hmalik88 May 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions app/components/Snaps/SnapUIAssetSelector/SnapUIAssetSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import React, { FunctionComponent } from 'react';
import { CaipAccountId, CaipChainId } from '@metamask/utils';
import { SnapUISelector } from '../SnapUISelector/SnapUISelector';
import { strings } from '../../../../locales/i18n';
import { SnapUIAsset, useSnapAssetSelectorData } from './useSnapAssetDisplay';
import { Box } from '../../UI/Box/Box';
import { AlignItems, FlexDirection } from '../../UI/Box/box.types';
import BadgeWrapper, {
BadgePosition,
} from '../../../component-library/components/Badges/BadgeWrapper';
import { AvatarSize } from '../../../component-library/components/Avatars/Avatar';
import AvatarToken from '../../../component-library/components/Avatars/Avatar/variants/AvatarToken';
import Text, {
TextColor,
TextVariant,
} from '../../../component-library/components/Texts/Text';
import { ImageSourcePropType, ViewStyle } from 'react-native';
import Badge, {
BadgeVariant,
} from '../../../component-library/components/Badges/Badge';

/**
*
* @param props - The component props.
* @param props.icon - The asset icon.
* @param props.symbol - The asset symbol.
* @param props.name - The asset name.
* @param props.balance - The asset balance.
* @param props.fiat - The asset balance in fiat.
* @param props.networkName - The network name.
* @param props.networkIcon - The network icon.
* @param props.compact - Whether the selector is compact.
* @param props.context - The rendering context ('inline' or 'modal').
* @returns The Asset Selector option.
*/
const SnapUIAssetSelectorOption: FunctionComponent<
SnapUIAsset & {
style?: ViewStyle;
compact?: boolean;
context?: 'inline' | 'modal';
}
> = ({
icon,
symbol,
name,
balance,
fiat,
networkName,
networkIcon,
compact = false,
context = 'modal',
}) => (
<Box
alignItems={AlignItems.center}
flexDirection={FlexDirection.Row}
gap={16}
// eslint-disable-next-line react-native/no-inline-styles
style={{ overflow: 'hidden', flex: 1 }}
>
{/* eslint-disable-next-line react-native/no-inline-styles */}
<Box alignItems={AlignItems.center}>
<BadgeWrapper
badgeElement={
<Badge
variant={BadgeVariant.Network}
imageSource={networkIcon as ImageSourcePropType}
name={networkName}
/>
}
badgePosition={BadgePosition.BottomRight}
>
<AvatarToken size={AvatarSize.Sm} imageSource={{ uri: icon }} />
</BadgeWrapper>
</Box>
<Box
flexDirection={FlexDirection.Column}
// eslint-disable-next-line react-native/no-inline-styles
style={{ overflow: 'hidden', flex: 1 }}
>
<Text
variant={TextVariant.BodyMDMedium}
numberOfLines={1}
ellipsizeMode="tail"
>
{name}
</Text>
<Text
color={TextColor.Alternative}
variant={TextVariant.BodySM}
ellipsizeMode="tail"
numberOfLines={1}
>
{networkName}
</Text>
</Box>
<Box
flexDirection={FlexDirection.Column}
// We hide the balance and fiat in inline mode when the asset selector has a sibling element
// eslint-disable-next-line react-native/no-inline-styles
style={{
marginLeft: 'auto',
marginRight: 8,
...(context === 'inline' && compact ? { display: 'none' } : {}),
}}
alignItems={AlignItems.flexEnd}
>
<Text variant={TextVariant.BodySMMedium}>
{balance} {symbol}
</Text>
<Text color={TextColor.Alternative} variant={TextVariant.BodySM}>
{fiat}
</Text>
</Box>
</Box>
);

/**
* The props for the SnapUIAssetSelector.
*/
interface SnapUIAssetSelectorProps {
name: string;
addresses: CaipAccountId[];
chainIds?: CaipChainId[];
disabled?: boolean;
form?: string;
label?: string;
error?: string;
style?: Record<string, ViewStyle>;
compact?: boolean;
}

/**
* The SnapUIAssetSelector component.
*
* @param props - The component props.
* @param props.addresses - The addresses to get the assets for.
* @param props.chainIds - The chainIds to filter the assets by.
* @param props.disabled - Whether the selector is disabled.
* @param props.compact - Whether the selector is compact.
* @returns The AssetSelector component.
*/
export const SnapUIAssetSelector: FunctionComponent<
SnapUIAssetSelectorProps
> = ({ addresses, chainIds, disabled, style, compact, ...props }) => {
const assets = useSnapAssetSelectorData({ addresses, chainIds });

const options = assets.map(({ address, name, symbol }) => ({
key: 'asset',
value: { asset: address, name, symbol },
disabled: false,
}));

const optionComponents = assets.map((asset, index) => (
<SnapUIAssetSelectorOption {...asset} key={index} compact={compact} />
));

return (
<SnapUISelector
title={strings('snap_ui.asset_selector.title')}
options={options}
optionComponents={optionComponents}
disabled={disabled || assets.length === 0}
style={style}
compact={compact}
{...props}
/>
);
};
157 changes: 157 additions & 0 deletions app/components/Snaps/SnapUIAssetSelector/useSnapAssetDisplay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import {
CaipAccountId,
CaipAssetType,
CaipChainId,
KnownCaipNamespace,
parseCaipAccountId,
parseCaipChainId,
} from '@metamask/utils';
import { useSelector } from 'react-redux';
import { getMemoizedInternalAccountByAddress } from '../../../selectors/accountsController';
import { selectMultichainTokenListForAccountId } from '../../../selectors/multichain';
import I18n from '../../../../locales/i18n';
import { formatWithThreshold } from '../../../util/assets';
import { selectNetworkConfigurations } from '../../../selectors/networkController';
import { AllowedBridgeChainIds, NETWORK_TO_SHORT_NETWORK_NAME_MAP } from '../../../constants/bridge';
import { selectCurrentCurrency } from '../../../selectors/currencyRateController';
import { RootState } from '../../../reducers';
import { getNonEvmNetworkImageSourceByChainId } from '../../../util/networks/customNetworks';

/**
* An asset for the SnapUIAssetSelector.
*/
export interface SnapUIAsset {
icon: string;
symbol: string;
name: string;
balance: string;
fiat: string;
chainId: CaipChainId;
address: CaipAssetType;
networkName: string;
networkIcon?: string;
}

interface TokenWithFiatAmount {
image: string;
symbol: string;
name: string;
balance: string;
secondary: string;
chainId: CaipChainId;
address: CaipAssetType;
}

/**
* The parameters for the hook.
*
* @param addresses - The addresses to get the assets for.
* This is a list of the same address but for different chains.
* @param chainIds - The chainIds to filter the assets by.
*/
interface UseSnapAssetSelectorDataParams {
addresses: CaipAccountId[];
chainIds?: CaipChainId[];
}

/**
* Gets the assets from state and format them for the SnapUIAssetSelector.
*
* @param params - The parameters for the hook.
* @param params.addresses - The addresses to get the assets for.
* This is a list of the same address but for different chains.
* @param params.chainIds - The chainIds to filter the assets by.
* @returns The formatted assets.
*/
export const useSnapAssetSelectorData = ({
addresses,
chainIds,
}: UseSnapAssetSelectorDataParams) => {
const currentCurrency = useSelector(selectCurrentCurrency);
const locale = I18n.locale;

const parsedAccounts = addresses.map(parseCaipAccountId);

const account = useSelector((state) =>
getMemoizedInternalAccountByAddress(state, parsedAccounts[0].address),
);
const networks = useSelector(selectNetworkConfigurations);

const assets = useSelector((state: RootState) =>
selectMultichainTokenListForAccountId(state, account?.id)
) as TokenWithFiatAmount[];

/**
* Formats a fiat balance.
*
* @param balance - The balance to format.
* @returns The formatted balance.
*/
const formatFiatBalance = (balance: number | null = 0) =>
formatWithThreshold(balance, 0.01, locale, {
style: 'currency',
currency: currentCurrency.toUpperCase(),
});

/**
* Formats an asset balance.
*
* @param balance - The balance to format.
* @returns The formatted balance.
*/
const formatAssetBalance = (balance: string) => {
const parsedBalance = parseFloat(balance);
return formatWithThreshold(parsedBalance, 0.00001, locale, {
minimumFractionDigits: 0,
maximumFractionDigits: 5,
});
};

/**
* Formats a non-EVM asset for the SnapUIAssetSelector.
*
* @param asset - The asset to format.
* @returns The formatted asset.
*/
const formatAsset = (asset: TokenWithFiatAmount) => {
const networkName =
NETWORK_TO_SHORT_NETWORK_NAME_MAP[
asset.chainId as AllowedBridgeChainIds
] ?? networks[asset.chainId]?.name;

return {
icon: asset.image,
symbol: asset.symbol,
name: asset.name,
balance: formatAssetBalance(asset.balance),
networkName,
networkIcon: getNonEvmNetworkImageSourceByChainId(asset.chainId as CaipChainId),
fiat: formatFiatBalance(Number(asset.secondary)),
chainId: asset.chainId as CaipChainId,
address: asset.address as CaipAssetType,
};
};

// Filter the chain IDs to only include the requested ones.
const requestedChainIds = parsedAccounts
.map((chainId) => chainId)
.filter(({ chainId }) => (chainIds ? chainIds?.includes(chainId) : true));

// Format the assets
const formattedAssets: SnapUIAsset[] = assets.map(formatAsset);

// Filter the assets by the requested chain IDs
const filteredAssets = formattedAssets.filter((asset) =>
requestedChainIds.some(({ chainId, chain: { namespace, reference } }) => {
// Handles the "eip155:0" case
if (namespace === KnownCaipNamespace.Eip155 && reference === '0') {
const { namespace: assetNamepace } = parseCaipChainId(asset.chainId);
return assetNamepace === namespace;
}

return chainId === asset.chainId;
}),
);

return filteredAssets;
};
3 changes: 3 additions & 0 deletions app/components/Snaps/SnapUIInput/SnapUIInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export const SnapUIInput = ({
onChangeText={handleChange}
autoCapitalize="none"
autoCorrect={false}
// We set a max height of 58px and let the input grow to fill the rest of the height next to a taller sibling element.
// eslint-disable-next-line react-native/no-inline-styles
style={{ maxHeight: 58, flexGrow: 1 }}
/>
{error && (
// eslint-disable-next-line react-native/no-inline-styles
Expand Down
Loading
Loading