Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
@@ -1,4 +1,6 @@
import React, { useCallback } from 'react';
import React, { useCallback, useState } from 'react';
import type { TrendingAsset } from '@metamask/assets-controllers';
import TrendingQuickBuy from '../../components/TrendingQuickBuy/TrendingQuickBuy';
import { strings } from '../../../../../../locales/i18n';
import {
PriceChangeOption,
Expand All @@ -8,8 +10,22 @@ import { useRwaTokens } from '../../hooks/useRwaTokens/useRwaTokens';
import { useTokenListFilters } from '../../hooks/useTokenListFilters/useTokenListFilters';
import TokenListPageLayout from '../../components/TokenListPageLayout/TokenListPageLayout';
import { RWA_NETWORKS_LIST } from '../../utils/trendingNetworksList';
import { useABTest } from '../../../../../hooks/useABTest';
import {
EXPLORE_QUICK_BUY_AB_KEY,
EXPLORE_QUICK_BUY_VARIANTS,
EXPLORE_QUICK_BUY_EXPOSURE_METADATA,
} from '../../../../Views/TrendingView/search/abTestConfig';

const RWATokensFullView = () => {
const [quickTradeToken, setQuickTradeToken] = useState<TrendingAsset | null>(
null,
);
const { variant: quickBuyVariant } = useABTest(
EXPLORE_QUICK_BUY_AB_KEY,
EXPLORE_QUICK_BUY_VARIANTS,
EXPLORE_QUICK_BUY_EXPOSURE_METADATA,
);
const filters = useTokenListFilters({
timeOption: TimeOption.TwentyFourHours,
});
Expand Down Expand Up @@ -53,6 +69,16 @@ const RWATokensFullView = () => {
allowedNetworks={RWA_NETWORKS_LIST}
onLoadMore={loadMore}
isLoadingMore={isLoadingMore}
onQuickTrade={
quickBuyVariant.showQuickTradeButton ? setQuickTradeToken : undefined
}
quickBuyNode={
<TrendingQuickBuy
token={quickTradeToken}
onClose={() => setQuickTradeToken(null)}
source="explore_stocks"
/>
}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useCallback, useMemo, useState } from 'react';
import TrendingQuickBuy from '../../components/TrendingQuickBuy/TrendingQuickBuy';
import { View, RefreshControl } from 'react-native';
import { useRoute, type RouteProp } from '@react-navigation/native';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
Expand Down Expand Up @@ -28,6 +29,12 @@ import { FilterButton } from '../../components/FilterBar/FilterBar';
import TokenListPageLayout from '../../components/TokenListPageLayout/TokenListPageLayout';
import { TRENDING_NETWORKS_LIST } from '../../utils/trendingNetworksList';
import type { Theme } from '../../../../../util/theme/models';
import { useABTest } from '../../../../../hooks/useABTest';
import {
EXPLORE_QUICK_BUY_AB_KEY,
EXPLORE_QUICK_BUY_VARIANTS,
EXPLORE_QUICK_BUY_EXPOSURE_METADATA,
} from '../../../../Views/TrendingView/search/abTestConfig';

export interface TrendingTokensFullViewParams {
initialTimeOption?: TimeOption;
Expand All @@ -43,6 +50,7 @@ export interface TrendingTokensDataProps {
theme: Theme;
onLoadMore?: () => void;
isLoadingMore?: boolean;
onQuickTrade?: (token: TrendingAsset) => void;

search: {
searchResults: TrendingAsset[];
Expand All @@ -62,6 +70,7 @@ export const TrendingTokensData = (props: TrendingTokensDataProps) => {
theme,
onLoadMore,
isLoadingMore,
onQuickTrade,
} = props;

const tw = useTailwind();
Expand Down Expand Up @@ -95,6 +104,7 @@ export const TrendingTokensData = (props: TrendingTokensDataProps) => {
filterContext={filterContext}
onLoadMore={onLoadMore}
isLoadingMore={isLoadingMore}
onQuickTrade={onQuickTrade}
refreshControl={
<RefreshControl
colors={[theme.colors.primary.default]}
Expand All @@ -110,6 +120,14 @@ export const TrendingTokensData = (props: TrendingTokensDataProps) => {

const TrendingTokensFullView = () => {
const sessionManager = TrendingFeedSessionManager.getInstance();
const [quickTradeToken, setQuickTradeToken] = useState<TrendingAsset | null>(
null,
);
const { variant: quickBuyVariant } = useABTest(
EXPLORE_QUICK_BUY_AB_KEY,
EXPLORE_QUICK_BUY_VARIANTS,
EXPLORE_QUICK_BUY_EXPOSURE_METADATA,
);
const { params } =
useRoute<
RouteProp<{ TrendingTokensFullView: TrendingTokensFullViewParams }>
Expand Down Expand Up @@ -259,6 +277,16 @@ const TrendingTokensFullView = () => {
selectedTime={filters.selectedTimeOption}
/>
}
onQuickTrade={
quickBuyVariant.showQuickTradeButton ? setQuickTradeToken : undefined
}
quickBuyNode={
<TrendingQuickBuy
token={quickTradeToken}
onClose={() => setQuickTradeToken(null)}
source="explore_trending"
/>
}
/>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ export interface TokenListPageLayoutProps {
onLoadMore?: () => void;
/** Whether a pagination request is in flight. */
isLoadingMore?: boolean;
/** When provided, shows a Quick Trade flash button on each token row. */
onQuickTrade?: (token: TrendingAsset) => void;
/** Overlay node (e.g. TrendingQuickBuy sheet) rendered outside the scroll area. */
quickBuyNode?: React.ReactNode;
}

/**
Expand All @@ -65,6 +69,8 @@ const TokenListPageLayout: React.FC<TokenListPageLayoutProps> = ({
extraBottomSheets,
onLoadMore,
isLoadingMore,
onQuickTrade,
quickBuyNode,
}) => {
const tw = useTailwind();
const theme = useAppThemeFromContext();
Expand Down Expand Up @@ -112,6 +118,7 @@ const TokenListPageLayout: React.FC<TokenListPageLayoutProps> = ({
theme={theme}
onLoadMore={onLoadMore}
isLoadingMore={isLoadingMore}
onQuickTrade={onQuickTrade}
/>

<TrendingTokenNetworkBottomSheet
Expand All @@ -129,6 +136,7 @@ const TokenListPageLayout: React.FC<TokenListPageLayoutProps> = ({
sortDirection={filters.priceChangeSortDirection}
/>
{extraBottomSheets}
{quickBuyNode}
</SafeAreaView>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import React from 'react';
import { render } from '@testing-library/react-native';
import type { TrendingAsset } from '@metamask/assets-controllers';
import TrendingQuickBuy from './TrendingQuickBuy';
import { MetaMetricsEvents } from '../../../../../core/Analytics';
import { SocialLeaderboardEventProperties } from '../../../../Views/SocialLeaderboard/analytics';

const mockTrack = jest.fn();
jest.mock(
'../../../../Views/SocialLeaderboard/analytics/useSocialLeaderboardAnalytics',
() => ({
useSocialLeaderboardAnalytics: () => ({ track: mockTrack }),
}),
);

const mockQuickBuyRoot = jest.fn();
jest.mock(
'../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuy',
() => ({
QuickBuy: {
Root: (props: Record<string, unknown>) => {
mockQuickBuyRoot(props);
return null;
},
},
}),
);

jest.mock(
'../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/features',
() => ({
TOP_TRADERS_QUICK_BUY_FEATURES: { tradeModes: ['buy', 'sell'] },
}),
);

// ─── Helpers ─────────────────────────────────────────────────────────────────

const makeToken = (overrides: Partial<TrendingAsset> = {}): TrendingAsset => ({
assetId: 'eip155:1/erc20:0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
name: 'USD Coin',
symbol: 'USDC',
decimals: 6,
price: '1.00',
marketCap: 75_000_000_000,
aggregatedUsdVolume: 900_000_000,
...overrides,
});

describe('TrendingQuickBuy', () => {
beforeEach(() => {
jest.clearAllMocks();
});

it('renders QuickBuy.Root with isVisible=false when token is null', () => {
render(<TrendingQuickBuy token={null} onClose={jest.fn()} />);

expect(mockQuickBuyRoot).toHaveBeenCalledWith(
expect.objectContaining({ isVisible: false }),
);
});

it('renders QuickBuy.Root with isVisible=true when token is provided', () => {
render(<TrendingQuickBuy token={makeToken()} onClose={jest.fn()} />);

expect(mockQuickBuyRoot).toHaveBeenCalledWith(
expect.objectContaining({ isVisible: true }),
);
});

it('maps an ERC-20 TrendingAsset to the correct QuickBuyTarget', () => {
const token = makeToken({
assetId: 'eip155:1/erc20:0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
symbol: 'SNX',
name: 'Synthetix',
});

render(<TrendingQuickBuy token={token} onClose={jest.fn()} />);

expect(mockQuickBuyRoot).toHaveBeenCalledWith(
expect.objectContaining({
target: {
chain: 'eip155:1',
tokenAddress: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f',
tokenSymbol: 'SNX',
tokenName: 'Synthetix',
},
}),
);
});

it('maps a native (slip44) TrendingAsset to the zero address', () => {
const token = makeToken({
assetId: 'eip155:1/slip44:60',
symbol: 'ETH',
name: 'Ethereum',
});

render(<TrendingQuickBuy token={token} onClose={jest.fn()} />);

expect(mockQuickBuyRoot).toHaveBeenCalledWith(
expect.objectContaining({
target: expect.objectContaining({
tokenAddress: '0x0000000000000000000000000000000000000000',
tokenSymbol: 'ETH',
}),
}),
);
});

it('passes analyticsContext with source=explore_search to QuickBuy.Root', () => {
render(<TrendingQuickBuy token={makeToken()} onClose={jest.fn()} />);

expect(mockQuickBuyRoot).toHaveBeenCalledWith(
expect.objectContaining({
analyticsContext: expect.objectContaining({ source: 'explore_search' }),
}),
);
});

it('fires SOCIAL_QUICK_BUY_SHEET_VIEWED when token transitions from null to non-null', () => {
const token = makeToken();
render(<TrendingQuickBuy token={token} onClose={jest.fn()} />);

expect(mockTrack).toHaveBeenCalledTimes(1);
expect(mockTrack).toHaveBeenCalledWith(
MetaMetricsEvents.SOCIAL_QUICK_BUY_SHEET_VIEWED,
expect.objectContaining({
[SocialLeaderboardEventProperties.SOURCE]: 'explore_search',
[SocialLeaderboardEventProperties.CAIP19]: token.assetId,
[SocialLeaderboardEventProperties.MARKET_CAP]: token.marketCap,
}),
);
});

it('does not fire SOCIAL_QUICK_BUY_SHEET_VIEWED when token is null', () => {
render(<TrendingQuickBuy token={null} onClose={jest.fn()} />);
expect(mockTrack).not.toHaveBeenCalled();
});

it('does not re-fire the event while the same token stays visible', () => {
const token = makeToken();
const { rerender } = render(
<TrendingQuickBuy token={token} onClose={jest.fn()} />,
);

// Simulate a re-render with the same token (e.g. parent re-renders)
rerender(<TrendingQuickBuy token={token} onClose={jest.fn()} />);

expect(mockTrack).toHaveBeenCalledTimes(1);
});

it('fires the event again when a new token is opened after closing', () => {
const tokenA = makeToken({
assetId: 'eip155:1/erc20:0xaaa',
symbol: 'AAA',
name: 'Token A',
});
const tokenB = makeToken({
assetId: 'eip155:1/erc20:0xbbb',
symbol: 'BBB',
name: 'Token B',
});

const { rerender } = render(
<TrendingQuickBuy token={tokenA} onClose={jest.fn()} />,
);
// Close sheet
rerender(<TrendingQuickBuy token={null} onClose={jest.fn()} />);
// Open with a new token
rerender(<TrendingQuickBuy token={tokenB} onClose={jest.fn()} />);

expect(mockTrack).toHaveBeenCalledTimes(2);
expect(mockTrack).toHaveBeenNthCalledWith(
2,
MetaMetricsEvents.SOCIAL_QUICK_BUY_SHEET_VIEWED,
expect.objectContaining({
[SocialLeaderboardEventProperties.CAIP19]: tokenB.assetId,
}),
);
});

it('forwards onClose to QuickBuy.Root', () => {
const onClose = jest.fn();
render(<TrendingQuickBuy token={makeToken()} onClose={onClose} />);

expect(mockQuickBuyRoot).toHaveBeenCalledWith(
expect.objectContaining({ onClose }),
);
});
});
Loading
Loading