Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
efe2312
feat: add batch sell button to trade wallet actions
infiniteflower May 4, 2026
9097477
chore: move feature flag to .js.env
infiniteflower May 4, 2026
8887995
chore: use proper icon for wallet actions
infiniteflower May 5, 2026
2922c23
chore: update frame icon
infiniteflower May 5, 2026
4bafac9
feat: add high rate alert modal
infiniteflower May 6, 2026
9031f5c
chore: add high rate modal route
infiniteflower May 6, 2026
afa6ee9
chore: bump bridge controller
infiniteflower May 6, 2026
06bc074
feat: good first pass of batch sell
infiniteflower May 6, 2026
bb17cd4
chore: rename multiswap to batchsell
infiniteflower May 6, 2026
c9e4ecc
feat: show percent price change in batch sell
infiniteflower May 6, 2026
e97fd8b
feat: show current token price
infiniteflower May 6, 2026
40f23d6
chore: update styles
infiniteflower May 6, 2026
0ab4aaf
feat: allow changing sort direction
infiniteflower May 6, 2026
6c0601f
fix: filter out zero balance tokens
infiniteflower May 6, 2026
3cad25c
feat: implement BatchSellEmptyState component and update BatchSellTok…
infiniteflower May 6, 2026
c13cb7e
fix: tsc issues
infiniteflower May 6, 2026
87c8c28
chore: clean up types
infiniteflower May 6, 2026
c64d66b
chore: tests
infiniteflower May 6, 2026
c5bc80e
fix: styling
infiniteflower May 7, 2026
25e2ab1
chore: use official Merge icon
infiniteflower May 7, 2026
3fab4c7
chore: use bottom sheet from design system package
infiniteflower May 7, 2026
bef7002
chore: pull avatar token into variasble
infiniteflower May 12, 2026
b5fb119
chore: yarn
infiniteflower May 12, 2026
d095c9a
chore: add env var to buids.yml
infiniteflower May 12, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React from 'react';
import { Image } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
import {
Box,
BoxAlignItems,
BoxJustifyContent,
Button,
ButtonSize,
ButtonVariant,
Text,
TextColor,
TextVariant,
} from '@metamask/design-system-react-native';
import { strings } from '../../../../../../locales/i18n';
import emptyStateDefiLight from '../../../../../images/empty-state-defi-light.png';
import { BatchSellTokenSelectSelectorsIDs } from './BatchSellTokenSelect.testIds';

interface BatchSellEmptyStateProps {
onExploreTokensPress: () => void;
}

export function BatchSellEmptyState({
onExploreTokensPress,
}: BatchSellEmptyStateProps) {

Check warning on line 26 in app/components/UI/Bridge/Views/BatchSellTokenSelect/BatchSellEmptyState.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=metamask-mobile&issues=AZ3_QrnFAfQsKCaoqDUV&open=AZ3_QrnFAfQsKCaoqDUV&pullRequest=29690
const tw = useTailwind();

return (
<SafeAreaView style={tw.style('flex-1 bg-default')} edges={['bottom']}>
<Box
testID={BatchSellTokenSelectSelectorsIDs.EMPTY_STATE}
alignItems={BoxAlignItems.Center}
justifyContent={BoxJustifyContent.Center}
gap={3}
twClassName="flex-1 px-4 py-4"
>
<Image
source={emptyStateDefiLight}
resizeMode="contain"
style={tw.style('h-[72px] w-[72px]')}
/>
<Box alignItems={BoxAlignItems.Center} gap={3} twClassName="px-4">
<Text
variant={TextVariant.BodyMd}
color={TextColor.TextAlternative}
twClassName="w-[240px] text-center"
>
{strings('bridge.batch_sell_empty_state_description')}
</Text>
<Button
variant={ButtonVariant.Secondary}
size={ButtonSize.Lg}
onPress={onExploreTokensPress}
twClassName="self-center"
testID={BatchSellTokenSelectSelectorsIDs.EXPLORE_TOKENS_BUTTON}
>
{strings('bridge.explore_tokens')}
</Button>
</Box>
</Box>
</SafeAreaView>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import React, { useMemo } from 'react';
import { ImageSourcePropType, StyleSheet, View } from 'react-native';
import { useSelector } from 'react-redux';
import {
Checkbox,
FontWeight,
Text,
TextColor as DSTextColor,
TextVariant as DSTextVariant,
} from '@metamask/design-system-react-native';
import { getNativeTokenAddress } from '@metamask/assets-controllers';
import {
formatChainIdToHex,
isNativeAddress,
} from '@metamask/bridge-controller';
import { Hex } from '@metamask/utils';
import {
TextColor as ComponentLibraryTextColor,
TextVariant as ComponentLibraryTextVariant,
} from '../../../../../component-library/components/Texts/Text';
import { RootState } from '../../../../../reducers';
import {
selectCurrencyRates,
selectCurrentCurrency,
} from '../../../../../selectors/currencyRateController';
import { selectNativeCurrencyByChainId } from '../../../../../selectors/networkController';
import { selectTokenMarketData } from '../../../../../selectors/tokenRatesController';
import { safeToChecksumAddress } from '../../../../../util/address';
import { strings } from '../../../../../../locales/i18n';
import { formatPriceWithSubscriptNotation } from '../../../Predict/utils/format';
import { useTokenPricePercentageChange } from '../../../Tokens/hooks/useTokenPricePercentageChange';
import { TokenSelectorItem } from '../../components/TokenSelectorItem';
import { BridgeToken } from '../../types';

const styles = StyleSheet.create({
secondaryRow: {
flexDirection: 'row',
alignItems: 'center',
flexShrink: 1,
minWidth: 0,
},
priceText: {
flexShrink: 1,
minWidth: 0,
},
tokenBalance: {
paddingHorizontal: 0,
textAlign: 'right',
},
});

function getPricePercentChangeText(
pricePercentChange: number | undefined,
): string | undefined {
if (
pricePercentChange === undefined ||
!Number.isFinite(pricePercentChange)
) {
return undefined;
}

return `${pricePercentChange >= 0 ? '+' : ''}${pricePercentChange.toFixed(
2,
)}%`;
}

function getPricePercentChangeTextColor(
pricePercentChange: number,
): DSTextColor {
if (pricePercentChange > 0) {
return DSTextColor.SuccessDefault;
}

if (pricePercentChange < 0) {
return DSTextColor.ErrorDefault;
}

return DSTextColor.TextAlternative;
}

function getTokenPriceInFiat({
token,
chainId,
isNative,
tokenMarketData,
currencyRates,
nativeCurrency,
}: {
token: BridgeToken;
chainId: Hex;
isNative: boolean;
tokenMarketData: ReturnType<typeof selectTokenMarketData>;
currencyRates: ReturnType<typeof selectCurrencyRates>;
nativeCurrency?: string;
}): number | undefined {
const addressToUse = isNative
? getNativeTokenAddress(chainId)
: safeToChecksumAddress(token.address);
const marketPriceInNative =
tokenMarketData?.[chainId]?.[addressToUse as Hex]?.price;

if (marketPriceInNative != null) {
const nativeToFiatRate = nativeCurrency
? currencyRates?.[nativeCurrency]?.conversionRate
: undefined;

return nativeToFiatRate
? marketPriceInNative * nativeToFiatRate
: undefined;
}

const nativePriceInFiat = isNative
? currencyRates?.[token.symbol]?.conversionRate
: undefined;

return nativePriceInFiat ?? undefined;
}

interface BatchSellTokenRowProps {
token: BridgeToken;
isSelected: boolean;
networkName?: string;
networkImageSource?: ImageSourcePropType;
onTokenPress: (token: BridgeToken) => void;
}

export function BatchSellTokenRow({
token,
isSelected,
networkName,
networkImageSource,
onTokenPress,
}: BatchSellTokenRowProps) {

Check warning on line 133 in app/components/UI/Bridge/Views/BatchSellTokenSelect/BatchSellTokenRow.tsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Mark the props of the component as read-only.

See more on https://sonarcloud.io/project/issues?id=metamask-mobile&issues=AZ3_QrmlAfQsKCaoqDUT&open=AZ3_QrmlAfQsKCaoqDUT&pullRequest=29690
const chainId = formatChainIdToHex(token.chainId);
const isNative = isNativeAddress(token.address);
const tokenMarketData = useSelector(selectTokenMarketData);
const currencyRates = useSelector(selectCurrencyRates);
const currentCurrency = useSelector(selectCurrentCurrency);
const nativeCurrency = useSelector((state: RootState) =>
selectNativeCurrencyByChainId(state, chainId),
);
const pricePercentChange = useTokenPricePercentageChange({
address: token.address,
chainId,
isNative,
});
const tokenPriceInFiat = useMemo(
() =>
getTokenPriceInFiat({
token,
chainId,
isNative,
tokenMarketData,
currencyRates,
nativeCurrency,
}),
[token, chainId, isNative, tokenMarketData, currencyRates, nativeCurrency],
);
const tokenPriceText =
tokenPriceInFiat !== undefined
? formatPriceWithSubscriptNotation(tokenPriceInFiat, currentCurrency)
: undefined;
const pricePercentChangeText = getPricePercentChangeText(pricePercentChange);
const pricePercentChangeTextColor =
pricePercentChangeText && pricePercentChange !== undefined
? getPricePercentChangeTextColor(pricePercentChange)
: undefined;
const secondaryRowContent =
tokenPriceText || pricePercentChangeText ? (
<View style={styles.secondaryRow}>
{tokenPriceText && (
<Text
variant={DSTextVariant.BodySm}
fontWeight={FontWeight.Medium}
color={DSTextColor.TextAlternative}
numberOfLines={1}
style={styles.priceText}
>
{tokenPriceText}
</Text>
)}
{tokenPriceText && pricePercentChangeText && (
<Text
variant={DSTextVariant.BodySm}
fontWeight={FontWeight.Medium}
color={DSTextColor.TextAlternative}
>
{' • '}
</Text>
)}
{pricePercentChangeText && pricePercentChangeTextColor && (
<Text
variant={DSTextVariant.BodySm}
fontWeight={FontWeight.Medium}
color={pricePercentChangeTextColor}
numberOfLines={1}
>
{pricePercentChangeText}
</Text>
)}
</View>
) : undefined;

return (
<TokenSelectorItem
token={token}
onPress={onTokenPress}
networkName={networkName}
networkImageSource={networkImageSource}
isSelected={isSelected}
shouldChangeSelectedStyle={false}
shouldShowNetworkIcon={false}
secondaryRowContent={secondaryRowContent}
tokenBalanceTextProps={{
textVariant: ComponentLibraryTextVariant.BodySMMedium,
textColor: ComponentLibraryTextColor.Alternative,
textStyle: styles.tokenBalance,
}}
>
<Checkbox
isSelected={isSelected}
onChange={() => onTokenPress(token)}
accessibilityLabel={`${token.symbol} ${strings(
'bridge.batch_sell_checkbox_label',
)}`}
/>
</TokenSelectorItem>
);
}
Loading
Loading