From 2827956164b6cdbd17e414c38528a2a28e2ea47f Mon Sep 17 00:00:00 2001 From: salimtb Date: Tue, 16 Jun 2026 13:52:30 +0200 Subject: [PATCH 01/13] feat(explore-search): add Quick Trade button to each crypto token row Each token row in the Explore search results (both "All" and "Crypto" tabs) now shows a circular flash-icon button that opens the existing QuickBuy sheet pre-loaded for that token. Key changes: - Add optional onQuickTrade prop to TrendingTokenRowItem, rendered as a 36 px dark circle with a flash icon (DS Icon, theme-aware bg) - Thread onQuickTrade through TokenSearchRowItem -> SearchFeedRow (tokens feed only, stocks/perps/etc. are unaffected) - ExploreSearchResults: replace empty BottomSheet with TrendingQuickBuy adapter; FullFeedList in ExploreSearchScreen wired up identically - New TrendingQuickBuy UI-layer adapter maps TrendingAsset to QuickBuyTarget and renders QuickBuy.Root, keeping TrendingView isolated from SocialLeaderboard per ADR-0020 - Analytics: add 'explore_search' to SocialLeaderboardSource and QuickBuySheetSource; fire SOCIAL_QUICK_BUY_SHEET_VIEWED on open with source, caip19, and marketCap; forward analyticsContext to QuickBuy.Root so all downstream events are attributed correctly - Tests: TrendingTokenRowItem button render/press/navigation guards; TrendingQuickBuy target mapping and event firing lifecycle; SearchFeedRow onQuickTrade forwarding and isolation - Locale: add trending.quick_trade = "Quick Buy & Sell" --- .../TrendingQuickBuy.test.tsx | 216 ++++++++++++++++++ .../TrendingQuickBuy/TrendingQuickBuy.tsx | 74 ++++++ .../TrendingTokenRowItem.styles.ts | 15 +- .../TrendingTokenRowItem.test.tsx | 61 +++++ .../TrendingTokenRowItem.tsx | 27 ++- .../analytics/socialLeaderboardEvents.ts | 4 +- .../ExploreSearchScreen.tsx | 44 ++-- .../feeds/tokens/TokenRowItem.tsx | 4 + .../search/ExploreSearchResults.tsx | 29 ++- .../search/SearchFeedRow.test.tsx | 74 +++++- .../TrendingView/search/SearchFeedRow.tsx | 45 ++-- locales/languages/en.json | 1 + 12 files changed, 541 insertions(+), 53 deletions(-) create mode 100644 app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx create mode 100644 app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx diff --git a/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx b/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx new file mode 100644 index 00000000000..45c03f8b8ad --- /dev/null +++ b/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx @@ -0,0 +1,216 @@ +// TrendingQuickBuy — unit tests +// +// Covers: +// - Inert (hidden) when no token is provided +// - Visible when a token is provided +// - Correct QuickBuyTarget mapping from TrendingAsset (ERC-20 + native) +// - SOCIAL_QUICK_BUY_SHEET_VIEWED fired once per open with explore_search source +// - Event NOT re-fired while the same token stays open +// - analyticsContext source forwarded to QuickBuy.Root + +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'; + +// ─── Mocks ─────────────────────────────────────────────────────────────────── + +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) => { + 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 => ({ + 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, +}); + +// ─── Tests ──────────────────────────────────────────────────────────────────── + +describe('TrendingQuickBuy', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + // ── Visibility ────────────────────────────────────────────────────────────── + + it('renders QuickBuy.Root with isVisible=false when token is null', () => { + render(); + + expect(mockQuickBuyRoot).toHaveBeenCalledWith( + expect.objectContaining({ isVisible: false }), + ); + }); + + it('renders QuickBuy.Root with isVisible=true when token is provided', () => { + render(); + + expect(mockQuickBuyRoot).toHaveBeenCalledWith( + expect.objectContaining({ isVisible: true }), + ); + }); + + // ── Target mapping: ERC-20 ─────────────────────────────────────────────────── + + it('maps an ERC-20 TrendingAsset to the correct QuickBuyTarget', () => { + const token = makeToken({ + assetId: 'eip155:1/erc20:0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + symbol: 'SNX', + name: 'Synthetix', + }); + + render(); + + expect(mockQuickBuyRoot).toHaveBeenCalledWith( + expect.objectContaining({ + target: { + chain: 'eip155:1', + tokenAddress: '0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', + tokenSymbol: 'SNX', + tokenName: 'Synthetix', + }, + }), + ); + }); + + // ── Target mapping: native token ───────────────────────────────────────────── + + it('maps a native (slip44) TrendingAsset to the zero address', () => { + const token = makeToken({ + assetId: 'eip155:1/slip44:60', + symbol: 'ETH', + name: 'Ethereum', + }); + + render(); + + expect(mockQuickBuyRoot).toHaveBeenCalledWith( + expect.objectContaining({ + target: expect.objectContaining({ + tokenAddress: '0x0000000000000000000000000000000000000000', + tokenSymbol: 'ETH', + }), + }), + ); + }); + + // ── Analytics context ──────────────────────────────────────────────────────── + + it('passes analyticsContext with source=explore_search to QuickBuy.Root', () => { + render(); + + expect(mockQuickBuyRoot).toHaveBeenCalledWith( + expect.objectContaining({ + analyticsContext: expect.objectContaining({ source: 'explore_search' }), + }), + ); + }); + + // ── Sheet-viewed event ─────────────────────────────────────────────────────── + + it('fires SOCIAL_QUICK_BUY_SHEET_VIEWED when token transitions from null to non-null', () => { + const token = makeToken(); + render(); + + 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(); + expect(mockTrack).not.toHaveBeenCalled(); + }); + + it('does not re-fire the event while the same token stays visible', () => { + const token = makeToken(); + const { rerender } = render( + , + ); + + // Simulate a re-render with the same token (e.g. parent re-renders) + rerender(); + + 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( + , + ); + // Close sheet + rerender(); + // Open with a new token + rerender(); + + expect(mockTrack).toHaveBeenCalledTimes(2); + expect(mockTrack).toHaveBeenNthCalledWith( + 2, + MetaMetricsEvents.SOCIAL_QUICK_BUY_SHEET_VIEWED, + expect.objectContaining({ + [SocialLeaderboardEventProperties.CAIP19]: tokenB.assetId, + }), + ); + }); + + // ── onClose forwarded ──────────────────────────────────────────────────────── + + it('forwards onClose to QuickBuy.Root', () => { + const onClose = jest.fn(); + render(); + + expect(mockQuickBuyRoot).toHaveBeenCalledWith( + expect.objectContaining({ onClose }), + ); + }); +}); diff --git a/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx b/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx new file mode 100644 index 00000000000..e4231b549f1 --- /dev/null +++ b/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx @@ -0,0 +1,74 @@ +import type { TrendingAsset } from '@metamask/assets-controllers'; +import React, { useEffect, useMemo, useRef } from 'react'; +import { QuickBuy } from '../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/quickBuy'; +import { TOP_TRADERS_QUICK_BUY_FEATURES } from '../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/features'; +import type { QuickBuyTarget } from '../../../../Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/types'; +import { NATIVE_SWAPS_TOKEN_ADDRESS } from '../../../../../constants/bridge'; +import { MetaMetricsEvents } from '../../../../../core/Analytics'; +import { + useSocialLeaderboardAnalytics, + SocialLeaderboardEventProperties, +} from '../../../../Views/SocialLeaderboard/analytics'; + +interface TrendingQuickBuyProps { + token: TrendingAsset | null; + onClose: () => void; +} + +/** + * Adapter that maps a `TrendingAsset` into the `QuickBuyTarget` format and + * renders `QuickBuy.Root`. Lives in the UI layer so route directories such as + * `TrendingView` can import it without violating ADR-0020. + * + * Fires `SOCIAL_QUICK_BUY_SHEET_VIEWED` with `source: 'explore_search'` each + * time the sheet is opened so all downstream QuickBuy events are attributed. + */ +const TrendingQuickBuy: React.FC = ({ + token, + onClose, +}) => { + const { track } = useSocialLeaderboardAnalytics(); + const prevTokenRef = useRef(null); + + const target = useMemo((): QuickBuyTarget | null => { + if (!token) return null; + const [caipChainId, assetIdentifier] = token.assetId.split('/'); + if (!caipChainId) return null; + const isNative = assetIdentifier?.startsWith('slip44:'); + const tokenAddress = isNative + ? NATIVE_SWAPS_TOKEN_ADDRESS + : (assetIdentifier?.split(':')[1] ?? ''); + return { + chain: caipChainId as QuickBuyTarget['chain'], + tokenAddress, + tokenSymbol: token.symbol ?? '', + tokenName: token.name ?? '', + }; + }, [token]); + + // Fire sheet-viewed event once per open (when token goes null → non-null). + useEffect(() => { + if (token && !prevTokenRef.current) { + track(MetaMetricsEvents.SOCIAL_QUICK_BUY_SHEET_VIEWED, { + [SocialLeaderboardEventProperties.SOURCE]: 'explore_search', + [SocialLeaderboardEventProperties.CAIP19]: token.assetId, + ...(typeof token.marketCap === 'number' + ? { [SocialLeaderboardEventProperties.MARKET_CAP]: token.marketCap } + : {}), + }); + } + prevTokenRef.current = token; + }, [token, track]); + + return ( + + ); +}; + +export default TrendingQuickBuy; diff --git a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.styles.ts b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.styles.ts index c07be3bc8e0..1fe33fc5f91 100644 --- a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.styles.ts +++ b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.styles.ts @@ -1,8 +1,9 @@ import { StyleSheet } from 'react-native'; import { Theme } from '../../../../../util/theme/models'; -const styleSheet = (_params: { theme: Theme }) => - StyleSheet.create({ +const styleSheet = (params: { theme: Theme }) => { + const { theme } = params; + return StyleSheet.create({ container: { display: 'flex', flexDirection: 'row', @@ -38,6 +39,16 @@ const styleSheet = (_params: { theme: Theme }) => stockBadgeWrapper: { marginTop: 4, }, + quickTradeButton: { + width: 36, + height: 36, + borderRadius: 18, + backgroundColor: theme.colors.background.alternative, + alignItems: 'center', + justifyContent: 'center', + alignSelf: 'center', + }, }); +}; export default styleSheet; diff --git a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.test.tsx b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.test.tsx index fa59067b321..66be994712a 100644 --- a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.test.tsx +++ b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.test.tsx @@ -1815,4 +1815,65 @@ describe('TrendingTokenRowItem', () => { expect(queryByText('Malicious')).toBeNull(); }); }); + + describe('Quick Trade button (onQuickTrade)', () => { + it('does not render the quick trade button when onQuickTrade is not provided', () => { + const token = createMockToken(); + + const { queryByTestId } = renderWithProvider( + , + { state: mockState }, + false, + ); + + expect(queryByTestId('quick-trade-button')).toBeNull(); + }); + + it('renders the quick trade button when onQuickTrade is provided', () => { + const token = createMockToken(); + const onQuickTrade = jest.fn(); + + const { getByTestId } = renderWithProvider( + , + { state: mockState }, + false, + ); + + expect(getByTestId('quick-trade-button')).toBeOnTheScreen(); + }); + + it('calls onQuickTrade with the token when the button is pressed', () => { + const token = createMockToken(); + const onQuickTrade = jest.fn(); + + const { getByTestId } = renderWithProvider( + , + { state: mockState }, + false, + ); + + fireEvent.press(getByTestId('quick-trade-button')); + + expect(onQuickTrade).toHaveBeenCalledTimes(1); + expect(onQuickTrade).toHaveBeenCalledWith(token); + }); + + it('does not trigger row navigation when the quick trade button is pressed', async () => { + const token = createMockToken(); + const onQuickTrade = jest.fn(); + + const { getByTestId } = renderWithProvider( + , + { state: mockState }, + false, + ); + + fireEvent.press(getByTestId('quick-trade-button')); + + await waitFor(() => { + expect(mockNavigate).not.toHaveBeenCalled(); + expect(mockDispatch).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx index abdc7de8400..bfc4a858a25 100644 --- a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx +++ b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx @@ -1,6 +1,6 @@ import React, { useCallback, useMemo } from 'react'; import { getTrendingTokenRowItemTestId } from './TrendingTokenRowItem.testIds'; -import { TouchableOpacity, View } from 'react-native'; +import { Pressable, TouchableOpacity, View } from 'react-native'; import { useSelector } from 'react-redux'; import Text, { TextColor, @@ -8,6 +8,7 @@ import Text, { } from '../../../../../component-library/components/Texts/Text'; import { useStyles } from '../../../../../component-library/hooks'; import styleSheet from './TrendingTokenRowItem.styles'; +import { Icon, IconName, IconSize } from '@metamask/design-system-react-native'; import { TrendingAsset } from '@metamask/assets-controllers'; import TrendingTokenLogo from '../TrendingTokenLogo'; import Badge, { @@ -85,6 +86,8 @@ interface TrendingTokenRowItemProps { * `testID` (and E2E selectors) unique per instance. */ testIdInstanceKey?: string; + /** When provided, shows a circular Quick Trade button on the right of the row. */ + onQuickTrade?: (token: TrendingAsset) => void; } /** @@ -136,6 +139,7 @@ const TrendingTokenRowItem = ({ onPress, onCardPress, testIdInstanceKey, + onQuickTrade, }: TrendingTokenRowItemProps) => { const { styles } = useStyles(styleSheet, {}); const currentCurrency = useSelector(selectCurrentCurrency) || 'usd'; @@ -260,6 +264,27 @@ const TrendingTokenRowItem = ({ ) )} + {onQuickTrade && ( + { + e?.stopPropagation?.(); + onQuickTrade(token); + }} + hitSlop={8} + testID="quick-trade-button" + accessibilityRole="button" + style={({ pressed }) => [ + styles.quickTradeButton, + pressed && { opacity: 0.5 }, + ]} + > + + + )} ); }; diff --git a/app/components/Views/SocialLeaderboard/analytics/socialLeaderboardEvents.ts b/app/components/Views/SocialLeaderboard/analytics/socialLeaderboardEvents.ts index ac684e53028..eea0fbb2b4a 100644 --- a/app/components/Views/SocialLeaderboard/analytics/socialLeaderboardEvents.ts +++ b/app/components/Views/SocialLeaderboard/analytics/socialLeaderboardEvents.ts @@ -86,7 +86,8 @@ export type SocialLeaderboardSource = | 'profile_position' | 'asset_details' | 'market_insights' - | 'security_trust'; + | 'security_trust' + | 'explore_search'; export type LeaderboardScreenViewedSource = Extract< SocialLeaderboardSource, @@ -121,4 +122,5 @@ export type QuickBuySheetSource = Extract< | 'asset_details' | 'market_insights' | 'security_trust' + | 'explore_search' >; diff --git a/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx b/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx index 65c39dc734a..967076c0ac2 100644 --- a/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx +++ b/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx @@ -6,6 +6,8 @@ import React, { useState, } from 'react'; import { ActivityIndicator, Keyboard, Platform } from 'react-native'; +import type { TrendingAsset } from '@metamask/assets-controllers'; +import TrendingQuickBuy from '../../../../UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; @@ -64,6 +66,9 @@ const FullFeedList: React.FC = ({ }) => { const tw = useTailwind(); const flashListRef = useRef>(null); + const [quickTradeToken, setQuickTradeToken] = useState( + null, + ); useEffect(() => { flashListRef.current?.scrollToOffset({ offset: 0, animated: false }); @@ -82,6 +87,8 @@ const FullFeedList: React.FC = ({ resetScrollTracking(); }, [searchQuery, resetScrollTracking]); + const handleQuickTrade = feedId === 'tokens' ? setQuickTradeToken : undefined; + const renderItem: ListRenderItem = useCallback( ({ item, index }) => ( = ({ searchQuery={searchQuery} tabName={tabName} resultCount={resultCount} + onQuickTrade={handleQuickTrade} /> ), - [feedId, searchQuery, tabName, resultCount], + [feedId, searchQuery, tabName, resultCount, handleQuickTrade], ); const keyExtractor = useCallback( @@ -134,20 +142,26 @@ const FullFeedList: React.FC = ({ } return ( - + <> + + setQuickTradeToken(null)} + /> + ); }; diff --git a/app/components/Views/TrendingView/feeds/tokens/TokenRowItem.tsx b/app/components/Views/TrendingView/feeds/tokens/TokenRowItem.tsx index 3cf28243e0b..0749dd8aa94 100644 --- a/app/components/Views/TrendingView/feeds/tokens/TokenRowItem.tsx +++ b/app/components/Views/TrendingView/feeds/tokens/TokenRowItem.tsx @@ -15,6 +15,8 @@ interface TokenRowItemProps { tokenDetailsSource?: TokenDetailsSource; /** Called synchronously before the card's press handler fires. */ onCardPress?: () => void; + /** When provided, shows the Quick Trade button on the row. */ + onQuickTrade?: (token: TrendingAsset) => void; } /** Token row used inside the home tabs. */ @@ -38,12 +40,14 @@ export const TokenSearchRowItem: React.FC = ({ token, index, tokenDetailsSource, + onQuickTrade, }) => ( ); diff --git a/app/components/Views/TrendingView/search/ExploreSearchResults.tsx b/app/components/Views/TrendingView/search/ExploreSearchResults.tsx index d2f6a2c68ba..c906bfddab8 100644 --- a/app/components/Views/TrendingView/search/ExploreSearchResults.tsx +++ b/app/components/Views/TrendingView/search/ExploreSearchResults.tsx @@ -1,4 +1,10 @@ -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { Pressable, StyleSheet } from 'react-native'; import { useSelector } from 'react-redux'; import { @@ -8,10 +14,6 @@ import { TextVariant, TextColor, FontWeight, - Icon, - IconName, - IconSize, - IconColor, BoxFlexDirection, BoxAlignItems, BoxJustifyContent, @@ -35,6 +37,7 @@ import SearchFeedRow, { SearchFeedSkeleton, getItemId } from './SearchFeedRow'; import { MAX_ITEMS_PER_SECTION, getViewMoreLabel } from './viewMoreLabel'; import type { FlatListItem, ListItemHeader } from './searchTypes'; import CryptoMoversPillItem from '../feeds/tokens/CryptoMoversPillItem'; +import TrendingQuickBuy from '../../../UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy'; const POPULAR_ASSETS: TrendingAsset[] = [ { @@ -95,6 +98,10 @@ const ExploreSearchResults: React.FC = ({ [sections], ); + const [quickTradeToken, setQuickTradeToken] = useState( + null, + ); + const { onScrollBeginDrag, resetScrollTracking } = useScrollTracking( 'scrolled', searchQuery, @@ -161,11 +168,6 @@ const ExploreSearchResults: React.FC = ({ > {viewMoreLabel} - )} @@ -242,6 +244,9 @@ const ExploreSearchResults: React.FC = ({ searchQuery={searchQuery} tabName={activeTab} resultCount={totalResultCount} + onQuickTrade={ + item.feedId === 'tokens' ? setQuickTradeToken : undefined + } /> ); }, @@ -346,6 +351,10 @@ const ExploreSearchResults: React.FC = ({ ListFooterComponent={renderFooter} onScrollBeginDrag={onScrollBeginDrag} /> + setQuickTradeToken(null)} + /> ); }; diff --git a/app/components/Views/TrendingView/search/SearchFeedRow.test.tsx b/app/components/Views/TrendingView/search/SearchFeedRow.test.tsx index ece52dcb695..14746182b77 100644 --- a/app/components/Views/TrendingView/search/SearchFeedRow.test.tsx +++ b/app/components/Views/TrendingView/search/SearchFeedRow.test.tsx @@ -31,17 +31,25 @@ jest.mock('./analytics', () => ({ trackExploreSearchEvent: jest.fn(), })); +const mockOnQuickTrade = jest.fn(); + jest.mock('../feeds/tokens/TokenRowItem', () => ({ TokenSearchRowItem: ({ token, tokenDetailsSource, + onQuickTrade, }: { token: TrendingAsset; tokenDetailsSource?: TokenDetailsSource; + onQuickTrade?: (t: TrendingAsset) => void; }) => ( - - {token.assetId} - + onQuickTrade?.(token)} + > + {token.assetId} + ), CryptoMoversSearchRowItem: ({ token }: { token: TrendingAsset }) => ( {token.assetId} @@ -217,6 +225,66 @@ describe('SearchFeedRow', () => { }); }); +describe('onQuickTrade prop', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('forwards onQuickTrade to TokenSearchRowItem for the tokens feed', () => { + const token = { assetId: 'eip155:1/erc20:0xabc' } as TrendingAsset; + + const { getByTestId } = render( + , + ); + + fireEvent.press(getByTestId('stub-token-row')); + expect(mockOnQuickTrade).toHaveBeenCalledWith(token); + }); + + it('does not forward onQuickTrade to TokenSearchRowItem for the stocks feed', () => { + const token = { assetId: 'eip155:1/erc20:0xabc' } as TrendingAsset; + + const { getByTestId } = render( + , + ); + + fireEvent.press(getByTestId('stub-token-row')); + expect(mockOnQuickTrade).not.toHaveBeenCalled(); + }); + + it('renders tokens feed without onQuickTrade when prop is not provided', () => { + const token = { assetId: 'eip155:1/erc20:0xabc' } as TrendingAsset; + + const { getByTestId } = render( + , + ); + + // pressing does not call anything (no handler wired) + fireEvent.press(getByTestId('stub-token-row')); + expect(mockOnQuickTrade).not.toHaveBeenCalled(); + }); +}); + describe('SearchFeedSkeleton', () => { it('uses site skeleton for sites and predictions', () => { const { getByTestId, rerender } = render( diff --git a/app/components/Views/TrendingView/search/SearchFeedRow.tsx b/app/components/Views/TrendingView/search/SearchFeedRow.tsx index c86d32d0253..a774b6abb5a 100644 --- a/app/components/Views/TrendingView/search/SearchFeedRow.tsx +++ b/app/components/Views/TrendingView/search/SearchFeedRow.tsx @@ -21,28 +21,9 @@ interface SearchFeedRowProps { searchQuery: string; tabName: SearchFeedPill; resultCount?: number; + onQuickTrade?: (token: TrendingAsset) => void; } -const renderRow = (feedId: SearchFeedId, item: unknown, index: number) => { - switch (feedId) { - case 'tokens': - case 'stocks': - return ( - - ); - case 'perps': - return ; - case 'predictions': - return ; - case 'sites': - return ; - } -}; - export const getItemId = (feedId: SearchFeedId, item: unknown): string => { switch (feedId) { case 'tokens': @@ -65,6 +46,7 @@ const SearchFeedRow: React.FC = ({ searchQuery, tabName, resultCount, + onQuickTrade, }) => { const searchQueryRef = useRef(searchQuery); searchQueryRef.current = searchQuery; @@ -83,7 +65,28 @@ const SearchFeedRow: React.FC = ({ }); }, [feedId, tabName, item, index]); - return {renderRow(feedId, item, index)}; + const row = (() => { + switch (feedId) { + case 'tokens': + case 'stocks': + return ( + + ); + case 'perps': + return ; + case 'predictions': + return ; + case 'sites': + return ; + } + })(); + + return {row}; }; /** Skeleton row appropriate for a given feed. */ diff --git a/locales/languages/en.json b/locales/languages/en.json index e630ce4e7a7..2b25e0c35e0 100644 --- a/locales/languages/en.json +++ b/locales/languages/en.json @@ -9557,6 +9557,7 @@ "low_to_high": "Low to high", "apply": "Apply", "search_placeholder": "Search tokens, markets and URLs", + "quick_trade": "Quick Buy & Sell", "cancel": "Cancel", "perps": "Perps", "rwa_perps_section": "Perps", From fba483110e06c20a93cb6917b466b93854f68c21 Mon Sep 17 00:00:00 2001 From: salimtb Date: Tue, 16 Jun 2026 14:25:22 +0200 Subject: [PATCH 02/13] feat(explore-search): gate Quick Trade button behind A/B test ASSETS-3380 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flag: assetsASSETS3380AbtestExploreQuickBuy (LaunchDarkly JSON) - control (0–50%): button hidden — status quo - treatment (50–100%): flash button visible on each crypto token row Changes: - New abTestConfig.ts: flag key, variant enum, variant config, and analytics mapping for ASSETS-3380 - ExploreSearchResults + FullFeedList (ExploreSearchScreen): onQuickTrade is only wired when variant.showQuickTradeButton is true; useABTest auto-fires Experiment Viewed once per session - abTestAnalyticsRegistry: register EXPLORE_QUICK_BUY mapping so that SOCIAL_QUICK_BUY_SHEET_VIEWED, AMOUNT_SELECTED, TRADE_SUBMITTED, TRADE_COMPLETED, and DISMISSED are auto-enriched with active_ab_tests - abTestConfig.test.ts: 11 tests covering flag key, control/treatment behaviour, exposure metadata, and all enriched event names --- .../ExploreSearchScreen.tsx | 17 +++- .../search/ExploreSearchResults.tsx | 17 +++- .../TrendingView/search/abTestConfig.test.ts | 95 +++++++++++++++++++ .../Views/TrendingView/search/abTestConfig.ts | 48 ++++++++++ app/util/analytics/abTestAnalyticsRegistry.ts | 2 + 5 files changed, 177 insertions(+), 2 deletions(-) create mode 100644 app/components/Views/TrendingView/search/abTestConfig.test.ts create mode 100644 app/components/Views/TrendingView/search/abTestConfig.ts diff --git a/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx b/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx index 967076c0ac2..c8560afe6b2 100644 --- a/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx +++ b/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx @@ -8,6 +8,12 @@ import React, { import { ActivityIndicator, Keyboard, Platform } from 'react-native'; import type { TrendingAsset } from '@metamask/assets-controllers'; import TrendingQuickBuy from '../../../../UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy'; +import { useABTest } from '../../../../../hooks/useABTest'; +import { + EXPLORE_QUICK_BUY_AB_KEY, + EXPLORE_QUICK_BUY_VARIANTS, + EXPLORE_QUICK_BUY_EXPOSURE_METADATA, +} from '../../search/abTestConfig'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useNavigation } from '@react-navigation/native'; import { useTailwind } from '@metamask/design-system-twrnc-preset'; @@ -70,6 +76,12 @@ const FullFeedList: React.FC = ({ null, ); + const { variant: quickBuyVariant } = useABTest( + EXPLORE_QUICK_BUY_AB_KEY, + EXPLORE_QUICK_BUY_VARIANTS, + EXPLORE_QUICK_BUY_EXPOSURE_METADATA, + ); + useEffect(() => { flashListRef.current?.scrollToOffset({ offset: 0, animated: false }); }, [searchQuery]); @@ -87,7 +99,10 @@ const FullFeedList: React.FC = ({ resetScrollTracking(); }, [searchQuery, resetScrollTracking]); - const handleQuickTrade = feedId === 'tokens' ? setQuickTradeToken : undefined; + const handleQuickTrade = + feedId === 'tokens' && quickBuyVariant.showQuickTradeButton + ? setQuickTradeToken + : undefined; const renderItem: ListRenderItem = useCallback( ({ item, index }) => ( diff --git a/app/components/Views/TrendingView/search/ExploreSearchResults.tsx b/app/components/Views/TrendingView/search/ExploreSearchResults.tsx index c906bfddab8..1446dce7239 100644 --- a/app/components/Views/TrendingView/search/ExploreSearchResults.tsx +++ b/app/components/Views/TrendingView/search/ExploreSearchResults.tsx @@ -38,6 +38,12 @@ import { MAX_ITEMS_PER_SECTION, getViewMoreLabel } from './viewMoreLabel'; import type { FlatListItem, ListItemHeader } from './searchTypes'; import CryptoMoversPillItem from '../feeds/tokens/CryptoMoversPillItem'; import TrendingQuickBuy from '../../../UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy'; +import { useABTest } from '../../../../hooks/useABTest'; +import { + EXPLORE_QUICK_BUY_AB_KEY, + EXPLORE_QUICK_BUY_VARIANTS, + EXPLORE_QUICK_BUY_EXPOSURE_METADATA, +} from './abTestConfig'; const POPULAR_ASSETS: TrendingAsset[] = [ { @@ -102,6 +108,12 @@ const ExploreSearchResults: React.FC = ({ null, ); + const { variant: quickBuyVariant } = useABTest( + EXPLORE_QUICK_BUY_AB_KEY, + EXPLORE_QUICK_BUY_VARIANTS, + EXPLORE_QUICK_BUY_EXPOSURE_METADATA, + ); + const { onScrollBeginDrag, resetScrollTracking } = useScrollTracking( 'scrolled', searchQuery, @@ -245,7 +257,9 @@ const ExploreSearchResults: React.FC = ({ tabName={activeTab} resultCount={totalResultCount} onQuickTrade={ - item.feedId === 'tokens' ? setQuickTradeToken : undefined + item.feedId === 'tokens' && quickBuyVariant.showQuickTradeButton + ? setQuickTradeToken + : undefined } /> ); @@ -256,6 +270,7 @@ const ExploreSearchResults: React.FC = ({ searchQuery, activeTab, totalResultCount, + quickBuyVariant.showQuickTradeButton, ], ); diff --git a/app/components/Views/TrendingView/search/abTestConfig.test.ts b/app/components/Views/TrendingView/search/abTestConfig.test.ts new file mode 100644 index 00000000000..26c7aa09dfd --- /dev/null +++ b/app/components/Views/TrendingView/search/abTestConfig.test.ts @@ -0,0 +1,95 @@ +// abTestConfig — structural tests +// +// Verifies flag key, variant names, and analytics mapping are correctly wired +// so a misconfiguration (e.g. wrong flag key or missing event) is caught +// before shipping to LaunchDarkly. + +import { + EXPLORE_QUICK_BUY_AB_KEY, + EXPLORE_QUICK_BUY_VARIANTS, + EXPLORE_QUICK_BUY_EXPOSURE_METADATA, + EXPLORE_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING, + ExploreQuickBuyVariant, +} from './abTestConfig'; +import { EVENT_NAME } from '../../../../core/Analytics/MetaMetrics.events'; + +describe('EXPLORE_QUICK_BUY_AB_KEY', () => { + it('matches the LaunchDarkly flag name', () => { + expect(EXPLORE_QUICK_BUY_AB_KEY).toBe( + 'assetsASSETS3380AbtestExploreQuickBuy', + ); + }); +}); + +describe('EXPLORE_QUICK_BUY_VARIANTS', () => { + it('control hides the quick trade button', () => { + expect( + EXPLORE_QUICK_BUY_VARIANTS[ExploreQuickBuyVariant.Control] + .showQuickTradeButton, + ).toBe(false); + }); + + it('treatment shows the quick trade button', () => { + expect( + EXPLORE_QUICK_BUY_VARIANTS[ExploreQuickBuyVariant.Treatment] + .showQuickTradeButton, + ).toBe(true); + }); + + it('includes a control variant (required by useABTest)', () => { + expect(ExploreQuickBuyVariant.Control).toBe('control'); + }); +}); + +describe('EXPLORE_QUICK_BUY_EXPOSURE_METADATA', () => { + it('has human-readable variation names for both variants', () => { + expect( + EXPLORE_QUICK_BUY_EXPOSURE_METADATA.variationNames[ + ExploreQuickBuyVariant.Control + ], + ).toBeTruthy(); + expect( + EXPLORE_QUICK_BUY_EXPOSURE_METADATA.variationNames[ + ExploreQuickBuyVariant.Treatment + ], + ).toBeTruthy(); + }); +}); + +describe('EXPLORE_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING', () => { + it('uses the correct flag key', () => { + expect(EXPLORE_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING.flagKey).toBe( + EXPLORE_QUICK_BUY_AB_KEY, + ); + }); + + it('includes all valid variants', () => { + expect(EXPLORE_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING.validVariants).toEqual( + expect.arrayContaining(Object.values(ExploreQuickBuyVariant)), + ); + }); + + it('enriches SOCIAL_QUICK_BUY_SHEET_VIEWED', () => { + expect(EXPLORE_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING.eventNames).toContain( + EVENT_NAME.SOCIAL_QUICK_BUY_SHEET_VIEWED, + ); + }); + + it('enriches SOCIAL_QUICK_BUY_TRADE_SUBMITTED', () => { + expect(EXPLORE_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING.eventNames).toContain( + EVENT_NAME.SOCIAL_QUICK_BUY_TRADE_SUBMITTED, + ); + }); + + it('enriches SOCIAL_QUICK_BUY_TRADE_COMPLETED', () => { + expect(EXPLORE_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING.eventNames).toContain( + EVENT_NAME.SOCIAL_QUICK_BUY_TRADE_COMPLETED, + ); + }); + + it('enriches SOCIAL_QUICK_BUY_DISMISSED', () => { + expect(EXPLORE_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING.eventNames).toContain( + EVENT_NAME.SOCIAL_QUICK_BUY_DISMISSED, + ); + }); +}); diff --git a/app/components/Views/TrendingView/search/abTestConfig.ts b/app/components/Views/TrendingView/search/abTestConfig.ts new file mode 100644 index 00000000000..0f3c7836017 --- /dev/null +++ b/app/components/Views/TrendingView/search/abTestConfig.ts @@ -0,0 +1,48 @@ +import { EVENT_NAME } from '../../../../core/Analytics/MetaMetrics.events'; +import type { ABTestAnalyticsMapping } from '../../../../util/analytics/abTestAnalytics.types'; + +// --- Explore Search Quick Buy A/B Test (ASSETS-3380) --- + +export const EXPLORE_QUICK_BUY_AB_KEY = 'assetsASSETS3380AbtestExploreQuickBuy'; + +export enum ExploreQuickBuyVariant { + Control = 'control', + Treatment = 'treatment', +} + +export const EXPLORE_QUICK_BUY_VARIANTS: Record< + ExploreQuickBuyVariant, + { showQuickTradeButton: boolean } +> = { + [ExploreQuickBuyVariant.Control]: { showQuickTradeButton: false }, + [ExploreQuickBuyVariant.Treatment]: { showQuickTradeButton: true }, +}; + +/** + * Shared exposure metadata so both surfaces (ExploreSearchResults and + * FullFeedList) emit identical `Experiment Viewed` properties. + */ +export const EXPLORE_QUICK_BUY_EXPOSURE_METADATA = { + experimentName: 'Explore Search Quick Buy Button', + variationNames: { + [ExploreQuickBuyVariant.Control]: 'No quick buy button on token rows', + [ExploreQuickBuyVariant.Treatment]: 'Flash quick buy button on token rows', + }, +}; + +/** + * Auto-enriches Quick Buy business events with the active A/B variant so the + * data team can slice trade metrics by experiment arm. + */ +export const EXPLORE_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING: ABTestAnalyticsMapping = + { + flagKey: EXPLORE_QUICK_BUY_AB_KEY, + validVariants: Object.values(ExploreQuickBuyVariant), + eventNames: [ + EVENT_NAME.SOCIAL_QUICK_BUY_SHEET_VIEWED, + EVENT_NAME.SOCIAL_QUICK_BUY_AMOUNT_SELECTED, + EVENT_NAME.SOCIAL_QUICK_BUY_TRADE_SUBMITTED, + EVENT_NAME.SOCIAL_QUICK_BUY_TRADE_COMPLETED, + EVENT_NAME.SOCIAL_QUICK_BUY_DISMISSED, + ], + }; diff --git a/app/util/analytics/abTestAnalyticsRegistry.ts b/app/util/analytics/abTestAnalyticsRegistry.ts index 76c48d7c4e4..7dba864fa0e 100644 --- a/app/util/analytics/abTestAnalyticsRegistry.ts +++ b/app/util/analytics/abTestAnalyticsRegistry.ts @@ -12,6 +12,7 @@ import { import { AMBIENT_PRICE_COLOR_AB_TEST_ANALYTICS_MAPPING } from '../../components/UI/TokenDetails/components/abTestConfig'; import { SOCIAL_AI_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING } from '../../components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/abTestConfig'; import { WHATS_HAPPENING_EXPLORE_AB_TEST_ANALYTICS_MAPPING } from '../../components/Views/TrendingView/abTestConfig'; +import { EXPLORE_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING } from '../../components/Views/TrendingView/search/abTestConfig'; import { ONBOARDING_CHECKLIST_STEPPER_AB_TEST_ANALYTICS_MAPPING } from '../../components/UI/WalletHomeOnboardingSteps/abTestConfig'; export const AB_TEST_ANALYTICS_MAPPINGS: readonly ABTestAnalyticsMapping[] = [ @@ -34,6 +35,7 @@ export const AB_TEST_ANALYTICS_MAPPINGS: readonly ABTestAnalyticsMapping[] = [ // Explore WHATS_HAPPENING_EXPLORE_AB_TEST_ANALYTICS_MAPPING, + EXPLORE_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING, // Token Details AMBIENT_PRICE_COLOR_AB_TEST_ANALYTICS_MAPPING, From 24b591197f24488694ca56377931f5a74b678191 Mon Sep 17 00:00:00 2001 From: salimtb Date: Wed, 17 Jun 2026 10:23:52 +0200 Subject: [PATCH 03/13] fix: clean up --- .../TrendingQuickBuy.test.tsx | 24 ------------------- .../TrendingQuickBuy/TrendingQuickBuy.tsx | 9 ------- 2 files changed, 33 deletions(-) diff --git a/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx b/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx index 45c03f8b8ad..6799c97c722 100644 --- a/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx +++ b/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx @@ -1,13 +1,3 @@ -// TrendingQuickBuy — unit tests -// -// Covers: -// - Inert (hidden) when no token is provided -// - Visible when a token is provided -// - Correct QuickBuyTarget mapping from TrendingAsset (ERC-20 + native) -// - SOCIAL_QUICK_BUY_SHEET_VIEWED fired once per open with explore_search source -// - Event NOT re-fired while the same token stays open -// - analyticsContext source forwarded to QuickBuy.Root - import React from 'react'; import { render } from '@testing-library/react-native'; import type { TrendingAsset } from '@metamask/assets-controllers'; @@ -58,15 +48,11 @@ const makeToken = (overrides: Partial = {}): TrendingAsset => ({ ...overrides, }); -// ─── Tests ──────────────────────────────────────────────────────────────────── - describe('TrendingQuickBuy', () => { beforeEach(() => { jest.clearAllMocks(); }); - // ── Visibility ────────────────────────────────────────────────────────────── - it('renders QuickBuy.Root with isVisible=false when token is null', () => { render(); @@ -83,8 +69,6 @@ describe('TrendingQuickBuy', () => { ); }); - // ── Target mapping: ERC-20 ─────────────────────────────────────────────────── - it('maps an ERC-20 TrendingAsset to the correct QuickBuyTarget', () => { const token = makeToken({ assetId: 'eip155:1/erc20:0xc011a73ee8576fb46f5e1c5751ca3b9fe0af2a6f', @@ -106,8 +90,6 @@ describe('TrendingQuickBuy', () => { ); }); - // ── Target mapping: native token ───────────────────────────────────────────── - it('maps a native (slip44) TrendingAsset to the zero address', () => { const token = makeToken({ assetId: 'eip155:1/slip44:60', @@ -127,8 +109,6 @@ describe('TrendingQuickBuy', () => { ); }); - // ── Analytics context ──────────────────────────────────────────────────────── - it('passes analyticsContext with source=explore_search to QuickBuy.Root', () => { render(); @@ -139,8 +119,6 @@ describe('TrendingQuickBuy', () => { ); }); - // ── Sheet-viewed event ─────────────────────────────────────────────────────── - it('fires SOCIAL_QUICK_BUY_SHEET_VIEWED when token transitions from null to non-null', () => { const token = makeToken(); render(); @@ -203,8 +181,6 @@ describe('TrendingQuickBuy', () => { ); }); - // ── onClose forwarded ──────────────────────────────────────────────────────── - it('forwards onClose to QuickBuy.Root', () => { const onClose = jest.fn(); render(); diff --git a/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx b/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx index e4231b549f1..823b5c5f93a 100644 --- a/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx +++ b/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx @@ -15,14 +15,6 @@ interface TrendingQuickBuyProps { onClose: () => void; } -/** - * Adapter that maps a `TrendingAsset` into the `QuickBuyTarget` format and - * renders `QuickBuy.Root`. Lives in the UI layer so route directories such as - * `TrendingView` can import it without violating ADR-0020. - * - * Fires `SOCIAL_QUICK_BUY_SHEET_VIEWED` with `source: 'explore_search'` each - * time the sheet is opened so all downstream QuickBuy events are attributed. - */ const TrendingQuickBuy: React.FC = ({ token, onClose, @@ -46,7 +38,6 @@ const TrendingQuickBuy: React.FC = ({ }; }, [token]); - // Fire sheet-viewed event once per open (when token goes null → non-null). useEffect(() => { if (token && !prevTokenRef.current) { track(MetaMetricsEvents.SOCIAL_QUICK_BUY_SHEET_VIEWED, { From 4176a0fad86fb52e1066b2eddd795dbc64425158 Mon Sep 17 00:00:00 2001 From: salimtb Date: Wed, 17 Jun 2026 10:46:24 +0200 Subject: [PATCH 04/13] fix: clean up --- .../UI/TokenDetails/components/useAssetVisibility.test.ts | 2 -- .../components/TrendingQuickBuy/TrendingQuickBuy.test.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/app/components/UI/TokenDetails/components/useAssetVisibility.test.ts b/app/components/UI/TokenDetails/components/useAssetVisibility.test.ts index 7b4548b03c3..cababfef295 100644 --- a/app/components/UI/TokenDetails/components/useAssetVisibility.test.ts +++ b/app/components/UI/TokenDetails/components/useAssetVisibility.test.ts @@ -8,8 +8,6 @@ import useAssetVisibility, { } from './useAssetVisibility'; import type { TokenI } from '../../Tokens/types'; -// ─── Mocks ─────────────────────────────────────────────────────────────────── - jest.mock('react-redux', () => ({ useSelector: jest.fn(), })); diff --git a/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx b/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx index 6799c97c722..f4c7525ae89 100644 --- a/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx +++ b/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.test.tsx @@ -5,8 +5,6 @@ import TrendingQuickBuy from './TrendingQuickBuy'; import { MetaMetricsEvents } from '../../../../../core/Analytics'; import { SocialLeaderboardEventProperties } from '../../../../Views/SocialLeaderboard/analytics'; -// ─── Mocks ─────────────────────────────────────────────────────────────────── - const mockTrack = jest.fn(); jest.mock( '../../../../Views/SocialLeaderboard/analytics/useSocialLeaderboardAnalytics', From 19de0267d38f9a19ea182f9b2b69eb14a61dbb63 Mon Sep 17 00:00:00 2001 From: salimtb Date: Wed, 17 Jun 2026 11:08:14 +0200 Subject: [PATCH 05/13] fix: clean up --- .../UI/TokenDetails/components/useAssetVisibility.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/components/UI/TokenDetails/components/useAssetVisibility.test.ts b/app/components/UI/TokenDetails/components/useAssetVisibility.test.ts index cababfef295..7b4548b03c3 100644 --- a/app/components/UI/TokenDetails/components/useAssetVisibility.test.ts +++ b/app/components/UI/TokenDetails/components/useAssetVisibility.test.ts @@ -8,6 +8,8 @@ import useAssetVisibility, { } from './useAssetVisibility'; import type { TokenI } from '../../Tokens/types'; +// ─── Mocks ─────────────────────────────────────────────────────────────────── + jest.mock('react-redux', () => ({ useSelector: jest.fn(), })); From 585d8167797de0d7f5c91c11cfe4b52b2c2f2814 Mon Sep 17 00:00:00 2001 From: salimtb Date: Wed, 17 Jun 2026 11:38:51 +0200 Subject: [PATCH 06/13] fix: fix bottom sheet close --- .../components/QuickBuy/QuickBuyRoot.tsx | 88 +++++++++++-------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuyRoot.tsx b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuyRoot.tsx index 4163df32c22..22f785b806f 100644 --- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuyRoot.tsx +++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuyRoot.tsx @@ -1,5 +1,6 @@ import { BottomSheetDialog, + BottomSheetOverlay, type BottomSheetDialogRef, Box, } from '@metamask/design-system-react-native'; @@ -147,46 +148,57 @@ const QuickBuyRootInner: React.FC = ({ ); return ( - - {isContentReady ? ( - - + bottomSheetRef.current?.onCloseDialog() + : undefined + } + /> + + {isContentReady ? ( + - - {renderActiveScreen(activeScreen, children)} - - - - ) : ( - - - - )} - + + {renderActiveScreen(activeScreen, children)} + + + + ) : ( + + + + )} + + ); }; From 15baed2d0b5a4c4c9b99aefeb52130df48149db5 Mon Sep 17 00:00:00 2001 From: salimtb Date: Wed, 17 Jun 2026 12:20:23 +0200 Subject: [PATCH 07/13] fix: design review comments --- .../TrendingTokenRowItem/TrendingTokenRowItem.styles.ts | 2 +- .../TrendingTokenRowItem/TrendingTokenRowItem.tsx | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.styles.ts b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.styles.ts index 1fe33fc5f91..0dab3cb45be 100644 --- a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.styles.ts +++ b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.styles.ts @@ -43,7 +43,7 @@ const styleSheet = (params: { theme: Theme }) => { width: 36, height: 36, borderRadius: 18, - backgroundColor: theme.colors.background.alternative, + backgroundColor: theme.colors.background.muted, alignItems: 'center', justifyContent: 'center', alignSelf: 'center', diff --git a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx index bfc4a858a25..d378bd52b5e 100644 --- a/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx +++ b/app/components/UI/Trending/components/TrendingTokenRowItem/TrendingTokenRowItem.tsx @@ -8,7 +8,12 @@ import Text, { } from '../../../../../component-library/components/Texts/Text'; import { useStyles } from '../../../../../component-library/hooks'; import styleSheet from './TrendingTokenRowItem.styles'; -import { Icon, IconName, IconSize } from '@metamask/design-system-react-native'; +import { + Icon, + IconColor, + IconName, + IconSize, +} from '@metamask/design-system-react-native'; import { TrendingAsset } from '@metamask/assets-controllers'; import TrendingTokenLogo from '../TrendingTokenLogo'; import Badge, { @@ -37,7 +42,6 @@ import { TokenDetailsSource } from '../../../TokenDetails/constants/constants'; import { useTrendingTokenPress } from '../../hooks/useTrendingTokenPress/useTrendingTokenPress'; import SecurityTrustInlineBadge from '../../../SecurityTrust/components/SecurityTrustInlineBadge/SecurityTrustInlineBadge'; import { selectCurrentCurrency } from '../../../../../selectors/currencyRateController'; - /** * Gets the text color for price percentage change */ From 71b77b93b68925ad3e9ca221d123ac5228aee449 Mon Sep 17 00:00:00 2001 From: salimtb Date: Wed, 17 Jun 2026 12:50:16 +0200 Subject: [PATCH 08/13] fix: fix unit test --- .../components/QuickBuy/QuickBuySheet.test.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuySheet.test.tsx b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuySheet.test.tsx index 5638bd5c46d..02112a8ce90 100644 --- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuySheet.test.tsx +++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuySheet.test.tsx @@ -41,6 +41,10 @@ jest.mock('@metamask/design-system-react-native', () => { return { ...actual, + // Stub the overlay — its Animated fade uses useNativeDriver which isn't + // available in the test environment. + BottomSheetOverlay: () => + ReactMock.createElement(View, { testID: 'mock-bottom-sheet-overlay' }), BottomSheetDialog: ReactMock.forwardRef( ( { From d993515d204456e86bdf33ebbe79bf6c92d33d00 Mon Sep 17 00:00:00 2001 From: salimtb Date: Sun, 21 Jun 2026 23:08:53 +0200 Subject: [PATCH 09/13] feat: extend Quick Buy flash button to all Explore surfaces - Wire Quick Buy to CryptoTab (explore_crypto), RwasTab (explore_rwas), TrendingTokensFullView (explore_trending), and RWATokensFullView (explore_stocks) with correct per-surface analytics source values - Fix TokenRowItem to forward onQuickTrade prop to TrendingTokenRowItem - Make TrendingQuickBuy accept a configurable source prop (defaults to explore_search) and thread it through TokenListPageLayout, TrendingTokensData, and TrendingTokensList - Add explore_crypto/now/rwas/trending/stocks to SocialLeaderboardSource and QuickBuySheetSource types - Extend ABTestAnalyticsMapping with excludeWhenPropertiesMatch (array support) and add array support to injectWhenPropertiesMatch - Scope socialAiTSA612AbtestQuickBuy to exclude all explore_* sources - Scope assetsASSETS3380AbtestExploreQuickBuy to inject for all explore_* sources only --- .../RWATokensFullView/RWATokensFullView.tsx | 15 ++- .../TrendingTokensFullView.tsx | 15 +++ .../TokenListPageLayout.tsx | 8 ++ .../TrendingQuickBuy/TrendingQuickBuy.tsx | 12 +- .../TrendingTokensList/TrendingTokensList.tsx | 8 +- .../components/QuickBuy/abTestConfig.ts | 11 ++ .../analytics/socialLeaderboardEvents.ts | 12 +- .../feeds/tokens/TokenRowItem.tsx | 2 + .../Views/TrendingView/search/abTestConfig.ts | 11 ++ .../Views/TrendingView/tabs/CryptoTab.tsx | 115 ++++++++++------- .../Views/TrendingView/tabs/RwasTab.tsx | 119 ++++++++++-------- app/util/analytics/abTestAnalytics.types.ts | 15 ++- app/util/analytics/enrichWithABTests.ts | 30 ++++- 13 files changed, 265 insertions(+), 108 deletions(-) diff --git a/app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.tsx b/app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.tsx index 450de2d9439..b0a97233889 100644 --- a/app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.tsx +++ b/app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.tsx @@ -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, @@ -10,6 +12,9 @@ import TokenListPageLayout from '../../components/TokenListPageLayout/TokenListP import { RWA_NETWORKS_LIST } from '../../utils/trendingNetworksList'; const RWATokensFullView = () => { + const [quickTradeToken, setQuickTradeToken] = useState( + null, + ); const filters = useTokenListFilters({ timeOption: TimeOption.TwentyFourHours, }); @@ -53,6 +58,14 @@ const RWATokensFullView = () => { allowedNetworks={RWA_NETWORKS_LIST} onLoadMore={loadMore} isLoadingMore={isLoadingMore} + onQuickTrade={setQuickTradeToken} + quickBuyNode={ + setQuickTradeToken(null)} + source="explore_stocks" + /> + } /> ); }; diff --git a/app/components/UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView.tsx b/app/components/UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView.tsx index a3afcab39c4..addfe58988a 100644 --- a/app/components/UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView.tsx +++ b/app/components/UI/Trending/Views/TrendingTokensFullView/TrendingTokensFullView.tsx @@ -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'; @@ -43,6 +44,7 @@ export interface TrendingTokensDataProps { theme: Theme; onLoadMore?: () => void; isLoadingMore?: boolean; + onQuickTrade?: (token: TrendingAsset) => void; search: { searchResults: TrendingAsset[]; @@ -62,6 +64,7 @@ export const TrendingTokensData = (props: TrendingTokensDataProps) => { theme, onLoadMore, isLoadingMore, + onQuickTrade, } = props; const tw = useTailwind(); @@ -95,6 +98,7 @@ export const TrendingTokensData = (props: TrendingTokensDataProps) => { filterContext={filterContext} onLoadMore={onLoadMore} isLoadingMore={isLoadingMore} + onQuickTrade={onQuickTrade} refreshControl={ { const TrendingTokensFullView = () => { const sessionManager = TrendingFeedSessionManager.getInstance(); + const [quickTradeToken, setQuickTradeToken] = useState( + null, + ); const { params } = useRoute< RouteProp<{ TrendingTokensFullView: TrendingTokensFullViewParams }> @@ -259,6 +266,14 @@ const TrendingTokensFullView = () => { selectedTime={filters.selectedTimeOption} /> } + onQuickTrade={setQuickTradeToken} + quickBuyNode={ + setQuickTradeToken(null)} + source="explore_trending" + /> + } /> ); }; diff --git a/app/components/UI/Trending/components/TokenListPageLayout/TokenListPageLayout.tsx b/app/components/UI/Trending/components/TokenListPageLayout/TokenListPageLayout.tsx index 0287b2218fb..fec9aee4cef 100644 --- a/app/components/UI/Trending/components/TokenListPageLayout/TokenListPageLayout.tsx +++ b/app/components/UI/Trending/components/TokenListPageLayout/TokenListPageLayout.tsx @@ -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; } /** @@ -65,6 +69,8 @@ const TokenListPageLayout: React.FC = ({ extraBottomSheets, onLoadMore, isLoadingMore, + onQuickTrade, + quickBuyNode, }) => { const tw = useTailwind(); const theme = useAppThemeFromContext(); @@ -112,6 +118,7 @@ const TokenListPageLayout: React.FC = ({ theme={theme} onLoadMore={onLoadMore} isLoadingMore={isLoadingMore} + onQuickTrade={onQuickTrade} /> = ({ sortDirection={filters.priceChangeSortDirection} /> {extraBottomSheets} + {quickBuyNode} ); }; diff --git a/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx b/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx index 823b5c5f93a..d0a0094e9f4 100644 --- a/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx +++ b/app/components/UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy.tsx @@ -8,16 +8,20 @@ import { MetaMetricsEvents } from '../../../../../core/Analytics'; import { useSocialLeaderboardAnalytics, SocialLeaderboardEventProperties, + type QuickBuySheetSource, } from '../../../../Views/SocialLeaderboard/analytics'; -interface TrendingQuickBuyProps { +export interface TrendingQuickBuyProps { token: TrendingAsset | null; onClose: () => void; + /** Analytics surface identifier. Defaults to `'explore_search'`. */ + source?: QuickBuySheetSource; } const TrendingQuickBuy: React.FC = ({ token, onClose, + source = 'explore_search', }) => { const { track } = useSocialLeaderboardAnalytics(); const prevTokenRef = useRef(null); @@ -41,7 +45,7 @@ const TrendingQuickBuy: React.FC = ({ useEffect(() => { if (token && !prevTokenRef.current) { track(MetaMetricsEvents.SOCIAL_QUICK_BUY_SHEET_VIEWED, { - [SocialLeaderboardEventProperties.SOURCE]: 'explore_search', + [SocialLeaderboardEventProperties.SOURCE]: source, [SocialLeaderboardEventProperties.CAIP19]: token.assetId, ...(typeof token.marketCap === 'number' ? { [SocialLeaderboardEventProperties.MARKET_CAP]: token.marketCap } @@ -49,7 +53,7 @@ const TrendingQuickBuy: React.FC = ({ }); } prevTokenRef.current = token; - }, [token, track]); + }, [token, track, source]); return ( = ({ target={target} onClose={onClose} features={TOP_TRADERS_QUICK_BUY_FEATURES} - analyticsContext={{ source: 'explore_search' }} + analyticsContext={{ source }} /> ); }; diff --git a/app/components/UI/Trending/components/TrendingTokensList/TrendingTokensList.tsx b/app/components/UI/Trending/components/TrendingTokensList/TrendingTokensList.tsx index 08157a72633..08681a815eb 100644 --- a/app/components/UI/Trending/components/TrendingTokensList/TrendingTokensList.tsx +++ b/app/components/UI/Trending/components/TrendingTokensList/TrendingTokensList.tsx @@ -44,6 +44,10 @@ export interface TrendingTokensListProps { * Whether a pagination request is in flight. Shows a spinner in the list footer. */ isLoadingMore?: boolean; + /** + * When provided, shows a Quick Trade flash button on each row. + */ + onQuickTrade?: (token: TrendingAsset) => void; } /** @@ -60,6 +64,7 @@ const TrendingTokensList: React.FC = React.memo( filterContext, onLoadMore, isLoadingMore, + onQuickTrade, }) => { const renderItem = useCallback( ({ item, index }: { item: TrendingAsset; index: number }) => ( @@ -68,9 +73,10 @@ const TrendingTokensList: React.FC = React.memo( selectedTimeOption={selectedTimeOption} position={index} filterContext={filterContext} + onQuickTrade={onQuickTrade} /> ), - [selectedTimeOption, filterContext], + [selectedTimeOption, filterContext, onQuickTrade], ); const keyExtractor = useCallback( diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/abTestConfig.ts b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/abTestConfig.ts index 068ce1d1544..66462edbe66 100644 --- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/abTestConfig.ts +++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/abTestConfig.ts @@ -44,4 +44,15 @@ export const SOCIAL_AI_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING: ABTestAnalyticsMappi EVENT_NAME.SOCIAL_QUICK_BUY_TRADE_SUBMITTED, EVENT_NAME.SOCIAL_QUICK_BUY_TRADE_COMPLETED, ], + // Only applies to Token Details / Market Insights flows — not Explore + excludeWhenPropertiesMatch: { + source: [ + 'explore_search', + 'explore_crypto', + 'explore_now', + 'explore_rwas', + 'explore_trending', + 'explore_stocks', + ] as const, + }, }; diff --git a/app/components/Views/SocialLeaderboard/analytics/socialLeaderboardEvents.ts b/app/components/Views/SocialLeaderboard/analytics/socialLeaderboardEvents.ts index eea0fbb2b4a..870b05202ef 100644 --- a/app/components/Views/SocialLeaderboard/analytics/socialLeaderboardEvents.ts +++ b/app/components/Views/SocialLeaderboard/analytics/socialLeaderboardEvents.ts @@ -87,7 +87,12 @@ export type SocialLeaderboardSource = | 'asset_details' | 'market_insights' | 'security_trust' - | 'explore_search'; + | 'explore_search' + | 'explore_crypto' + | 'explore_now' + | 'explore_rwas' + | 'explore_trending' + | 'explore_stocks'; export type LeaderboardScreenViewedSource = Extract< SocialLeaderboardSource, @@ -123,4 +128,9 @@ export type QuickBuySheetSource = Extract< | 'market_insights' | 'security_trust' | 'explore_search' + | 'explore_crypto' + | 'explore_now' + | 'explore_rwas' + | 'explore_trending' + | 'explore_stocks' >; diff --git a/app/components/Views/TrendingView/feeds/tokens/TokenRowItem.tsx b/app/components/Views/TrendingView/feeds/tokens/TokenRowItem.tsx index 0749dd8aa94..7e4bf4a00fc 100644 --- a/app/components/Views/TrendingView/feeds/tokens/TokenRowItem.tsx +++ b/app/components/Views/TrendingView/feeds/tokens/TokenRowItem.tsx @@ -25,6 +25,7 @@ export const TokenRowItem: React.FC = ({ index, tokenDetailsSource, onCardPress, + onQuickTrade, }) => ( = ({ filterContext={DEFAULT_TOKENS_FILTER_CONTEXT} tokenDetailsSource={tokenDetailsSource} onCardPress={onCardPress} + onQuickTrade={onQuickTrade} /> ); diff --git a/app/components/Views/TrendingView/search/abTestConfig.ts b/app/components/Views/TrendingView/search/abTestConfig.ts index 0f3c7836017..221f2fe41bb 100644 --- a/app/components/Views/TrendingView/search/abTestConfig.ts +++ b/app/components/Views/TrendingView/search/abTestConfig.ts @@ -45,4 +45,15 @@ export const EXPLORE_QUICK_BUY_AB_TEST_ANALYTICS_MAPPING: ABTestAnalyticsMapping EVENT_NAME.SOCIAL_QUICK_BUY_TRADE_COMPLETED, EVENT_NAME.SOCIAL_QUICK_BUY_DISMISSED, ], + // This A/B test applies to all Explore surfaces + injectWhenPropertiesMatch: { + source: [ + 'explore_search', + 'explore_crypto', + 'explore_now', + 'explore_rwas', + 'explore_trending', + 'explore_stocks', + ] as const, + }, }; diff --git a/app/components/Views/TrendingView/tabs/CryptoTab.tsx b/app/components/Views/TrendingView/tabs/CryptoTab.tsx index 55c57b1f4bc..4f752f226f5 100644 --- a/app/components/Views/TrendingView/tabs/CryptoTab.tsx +++ b/app/components/Views/TrendingView/tabs/CryptoTab.tsx @@ -1,4 +1,5 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; +import { View, StyleSheet } from 'react-native'; import { useNavigation, NavigationProp } from '@react-navigation/native'; import { useSelector } from 'react-redux'; import { Box } from '@metamask/design-system-react-native'; @@ -31,6 +32,11 @@ import TileCarousel from '../components/TileCarousel'; import type { TabProps } from '../hooks/useExploreRefresh'; import { trackExploreInteracted } from '../search/analytics'; import { TrendingViewSelectorsIDs } from '../TrendingView.testIds'; +import TrendingQuickBuy from '../../../UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy'; + +const styles = StyleSheet.create({ + container: { flex: 1 }, +}); interface CryptoPerpsBlockProps { refresh: TabProps['refresh']; @@ -93,6 +99,10 @@ const CryptoTab: React.FC = ({ refresh, refreshing, onRefresh }) => { useNavigation>(); const isPerpsEnabled = useSelector(selectPerpsEnabledFlag); + const [quickTradeToken, setQuickTradeToken] = useState( + null, + ); + const tokens = useTokensFeed({ refresh }); const cryptoPredictions = usePredictionsFeed({ variant: 'crypto', @@ -117,6 +127,7 @@ const CryptoTab: React.FC = ({ refresh, refreshing, onRefresh }) => { item_clicked: item.assetId, }) } + onQuickTrade={setQuickTradeToken} /> ), [], @@ -125,53 +136,67 @@ const CryptoTab: React.FC = ({ refresh, refreshing, onRefresh }) => { const showTokens = tokens.isLoading || tokens.data.length > 0; return ( - - {showTokens && ( - - - navigation.navigate(Routes.WALLET.TRENDING_TOKENS_FULL_VIEW) - } - testID="section-header-view-all-tokens" - tabName="Crypto" - sectionName="tokens_trending" - /> - - data={tokens.data} - isLoading={tokens.isLoading} - renderItem={renderTokenItem} - Skeleton={TrendingTokensSkeleton} - idPrefix="tokens" - /> - - )} + + + {showTokens && ( + + + navigation.navigate(Routes.WALLET.TRENDING_TOKENS_FULL_VIEW) + } + testID="section-header-view-all-tokens" + tabName="Crypto" + sectionName="tokens_trending" + /> + + data={tokens.data} + isLoading={tokens.isLoading} + renderItem={renderTokenItem} + Skeleton={TrendingTokensSkeleton} + idPrefix="tokens" + /> + + )} - {isPerpsEnabled && ( - - - navigateToPerpsMarketList(perpsNavigation, 'crypto', sortOptionId) - } - /> - - )} + {isPerpsEnabled && ( + + + navigateToPerpsMarketList( + perpsNavigation, + 'crypto', + sortOptionId, + ) + } + /> + + )} - navigateToExplorePredictionsList(navigation, 'crypto')} + + navigateToExplorePredictionsList(navigation, 'crypto') + } + /> + + + setQuickTradeToken(null)} + source="explore_crypto" /> - + ); }; diff --git a/app/components/Views/TrendingView/tabs/RwasTab.tsx b/app/components/Views/TrendingView/tabs/RwasTab.tsx index 79b067807f7..51d61f890fd 100644 --- a/app/components/Views/TrendingView/tabs/RwasTab.tsx +++ b/app/components/Views/TrendingView/tabs/RwasTab.tsx @@ -1,4 +1,5 @@ -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; +import { View, StyleSheet } from 'react-native'; import { useNavigation, NavigationProp } from '@react-navigation/native'; import { useSelector } from 'react-redux'; import { Box } from '@metamask/design-system-react-native'; @@ -34,6 +35,11 @@ import SectionHeader from '../components/SectionHeader'; import type { TabProps } from '../hooks/useExploreRefresh'; import { trackExploreInteracted } from '../search/analytics'; import { TrendingViewSelectorsIDs } from '../TrendingView.testIds'; +import TrendingQuickBuy from '../../../UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy'; + +const styles = StyleSheet.create({ + container: { flex: 1 }, +}); interface RwaPerpsBlockProps { refresh: TabProps['refresh']; @@ -102,6 +108,10 @@ const RwasTab: React.FC = ({ refresh, refreshing, onRefresh }) => { const isPerpsEnabled = useSelector(selectPerpsEnabledFlag); const isPredictEnabled = useSelector(selectPredictEnabledFlag); + const [quickTradeToken, setQuickTradeToken] = useState( + null, + ); + const politics = usePredictionsFeed({ variant: 'politics', refresh }); const stocks = useStocksFeed({ refresh, @@ -126,6 +136,7 @@ const RwasTab: React.FC = ({ refresh, refreshing, onRefresh }) => { item_clicked: item.assetId, }) } + onQuickTrade={setQuickTradeToken} /> ), [], @@ -134,56 +145,64 @@ const RwasTab: React.FC = ({ refresh, refreshing, onRefresh }) => { const showStocks = stocks.isLoading || stocks.data.length > 0; return ( - - - navigateToExplorePredictionsList(appNavigation, 'politics') - } - isEnabled={isPredictEnabled} - /> + + + + navigateToExplorePredictionsList(appNavigation, 'politics') + } + isEnabled={isPredictEnabled} + /> + + {showStocks && ( + + + appNavigation.navigate(Routes.WALLET.RWA_TOKENS_FULL_VIEW) + } + testID="section-header-view-all-stocks" + tabName="RWAs" + sectionName="stocks" + /> + + data={stocks.data} + isLoading={stocks.isLoading} + renderItem={renderStockItem} + Skeleton={TrendingTokensSkeleton} + idPrefix="stocks" + /> + + )} - {showStocks && ( - - - appNavigation.navigate(Routes.WALLET.RWA_TOKENS_FULL_VIEW) - } - testID="section-header-view-all-stocks" - tabName="RWAs" - sectionName="stocks" - /> - - data={stocks.data} - isLoading={stocks.isLoading} - renderItem={renderStockItem} - Skeleton={TrendingTokensSkeleton} - idPrefix="stocks" - /> - - )} - - {isPerpsEnabled && ( - - - navigateToPerpsMarketList(perpsNavigation, filter, sortOptionId) - } - /> - - )} - + {isPerpsEnabled && ( + + + navigateToPerpsMarketList(perpsNavigation, filter, sortOptionId) + } + /> + + )} + + + setQuickTradeToken(null)} + source="explore_rwas" + /> + ); }; diff --git a/app/util/analytics/abTestAnalytics.types.ts b/app/util/analytics/abTestAnalytics.types.ts index fb1424d513e..3de222f89fb 100644 --- a/app/util/analytics/abTestAnalytics.types.ts +++ b/app/util/analytics/abTestAnalytics.types.ts @@ -11,7 +11,18 @@ export interface ABTestAnalyticsMapping { >; /** * When set, `active_ab_tests` for this flag is only injected if every entry - * matches `event.properties` (strict `===`). Omit for unconditional injection. + * matches `event.properties`. Values may be a scalar or an array of scalars + * (any one matching value satisfies the condition). */ - injectWhenPropertiesMatch?: Readonly>; + injectWhenPropertiesMatch?: Readonly< + Record + >; + /** + * When set, `active_ab_tests` for this flag is NOT injected if any entry + * matches `event.properties`. Values may be a scalar or an array of scalars + * (any one matching value causes exclusion). + */ + excludeWhenPropertiesMatch?: Readonly< + Record + >; } diff --git a/app/util/analytics/enrichWithABTests.ts b/app/util/analytics/enrichWithABTests.ts index 80c02984452..2908368d203 100644 --- a/app/util/analytics/enrichWithABTests.ts +++ b/app/util/analytics/enrichWithABTests.ts @@ -58,9 +58,30 @@ const eventMatchesInjectGate = ( if (!gate || Object.keys(gate).length === 0) { return true; } - return Object.entries(gate).every( - ([propertyKey, expected]) => properties[propertyKey] === expected, - ); + return Object.entries(gate).every(([propertyKey, expected]) => { + const actual = properties[propertyKey]; + if (Array.isArray(expected)) { + return (expected as readonly unknown[]).includes(actual); + } + return actual === expected; + }); +}; + +const eventMatchesExcludeGate = ( + properties: Record, + mapping: ABTestAnalyticsMapping, +): boolean => { + const gate = mapping.excludeWhenPropertiesMatch; + if (!gate || Object.keys(gate).length === 0) { + return false; + } + return Object.entries(gate).some(([propertyKey, excluded]) => { + const actual = properties[propertyKey]; + if (Array.isArray(excluded)) { + return (excluded as readonly unknown[]).includes(actual); + } + return actual === excluded; + }); }; export const getRemoteFeatureFlagsFromState = ( @@ -89,7 +110,8 @@ export const enrichWithABTests = < (mapping) => hasEventName(mapping, event.name) && eventMatchesPropertyRequirements(mapping, event) && - eventMatchesInjectGate(event.properties, mapping), + eventMatchesInjectGate(event.properties, mapping) && + !eventMatchesExcludeGate(event.properties, mapping), ); if (relevantMappings.length === 0) { From fe37cdf10eabed1d42852c1bc0bd4639c482645e Mon Sep 17 00:00:00 2001 From: salimtb Date: Sun, 21 Jun 2026 23:12:07 +0200 Subject: [PATCH 10/13] fix: remove BottomSheetOverlay from QuickBuyRoot Reverts the dismiss-on-outside-press regression introduced in 585d816. The QuickBuy sheet is intentionally overlay-free: no scrim, no background dimming, and the surface below stays interactive. BottomSheetOverlay was blocking all touches on the underlying content and closing the sheet when tapping outside, which is the opposite of the intended behaviour. Also removes the now-unnecessary BottomSheetOverlay stub from QuickBuySheet.test.tsx. --- .../components/QuickBuy/QuickBuyRoot.tsx | 88 ++++++++----------- .../QuickBuy/QuickBuySheet.test.tsx | 4 - 2 files changed, 38 insertions(+), 54 deletions(-) diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuyRoot.tsx b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuyRoot.tsx index 22f785b806f..4163df32c22 100644 --- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuyRoot.tsx +++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuyRoot.tsx @@ -1,6 +1,5 @@ import { BottomSheetDialog, - BottomSheetOverlay, type BottomSheetDialogRef, Box, } from '@metamask/design-system-react-native'; @@ -148,57 +147,46 @@ const QuickBuyRootInner: React.FC = ({ ); return ( - <> - bottomSheetRef.current?.onCloseDialog() - : undefined - } - /> - - {isContentReady ? ( - + {isContentReady ? ( + + - - - {renderActiveScreen(activeScreen, children)} - - - - ) : ( - - - - )} - - + {renderActiveScreen(activeScreen, children)} + + + + ) : ( + + + + )} + ); }; diff --git a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuySheet.test.tsx b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuySheet.test.tsx index 02112a8ce90..5638bd5c46d 100644 --- a/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuySheet.test.tsx +++ b/app/components/Views/SocialLeaderboard/TraderPositionView/components/QuickBuy/QuickBuySheet.test.tsx @@ -41,10 +41,6 @@ jest.mock('@metamask/design-system-react-native', () => { return { ...actual, - // Stub the overlay — its Animated fade uses useNativeDriver which isn't - // available in the test environment. - BottomSheetOverlay: () => - ReactMock.createElement(View, { testID: 'mock-bottom-sheet-overlay' }), BottomSheetDialog: ReactMock.forwardRef( ( { From db2640e7d9a7ec18e9ea75688ca1ef838bbe80ce Mon Sep 17 00:00:00 2001 From: salimtb Date: Mon, 22 Jun 2026 12:17:10 +0200 Subject: [PATCH 11/13] fix: put under ab testing --- .../RWATokensFullView/RWATokensFullView.tsx | 15 ++++++++++++++- .../TrendingTokensFullView.tsx | 15 ++++++++++++++- .../ExploreSearchScreen.tsx | 8 +++++++- .../search/ExploreSearchResults.tsx | 8 +++++++- .../Views/TrendingView/tabs/CryptoTab.tsx | 18 ++++++++++++++++-- .../Views/TrendingView/tabs/RwasTab.tsx | 18 ++++++++++++++++-- 6 files changed, 74 insertions(+), 8 deletions(-) diff --git a/app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.tsx b/app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.tsx index b0a97233889..5f70901f974 100644 --- a/app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.tsx +++ b/app/components/UI/Trending/Views/RWATokensFullView/RWATokensFullView.tsx @@ -10,11 +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( 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, }); @@ -58,7 +69,9 @@ const RWATokensFullView = () => { allowedNetworks={RWA_NETWORKS_LIST} onLoadMore={loadMore} isLoadingMore={isLoadingMore} - onQuickTrade={setQuickTradeToken} + onQuickTrade={ + quickBuyVariant.showQuickTradeButton ? setQuickTradeToken : undefined + } quickBuyNode={ { const [quickTradeToken, setQuickTradeToken] = useState( 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 }> @@ -266,7 +277,9 @@ const TrendingTokensFullView = () => { selectedTime={filters.selectedTimeOption} /> } - onQuickTrade={setQuickTradeToken} + onQuickTrade={ + quickBuyVariant.showQuickTradeButton ? setQuickTradeToken : undefined + } quickBuyNode={ = ({ null, ); - const { variant: quickBuyVariant } = useABTest( + const exploreQuickBuyABTest = useABTest( EXPLORE_QUICK_BUY_AB_KEY, EXPLORE_QUICK_BUY_VARIANTS, EXPLORE_QUICK_BUY_EXPOSURE_METADATA, ); + const { variant: quickBuyVariant } = exploreQuickBuyABTest; + // eslint-disable-next-line no-console + console.log( + `[${EXPLORE_QUICK_BUY_AB_KEY}] ExploreSearchScreen result:`, + exploreQuickBuyABTest, + ); useEffect(() => { flashListRef.current?.scrollToOffset({ offset: 0, animated: false }); diff --git a/app/components/Views/TrendingView/search/ExploreSearchResults.tsx b/app/components/Views/TrendingView/search/ExploreSearchResults.tsx index d6c7e0a7f9c..440e3a7f044 100644 --- a/app/components/Views/TrendingView/search/ExploreSearchResults.tsx +++ b/app/components/Views/TrendingView/search/ExploreSearchResults.tsx @@ -113,11 +113,17 @@ const ExploreSearchResults: React.FC = ({ null, ); - const { variant: quickBuyVariant } = useABTest( + const exploreQuickBuyABTest = useABTest( EXPLORE_QUICK_BUY_AB_KEY, EXPLORE_QUICK_BUY_VARIANTS, EXPLORE_QUICK_BUY_EXPOSURE_METADATA, ); + const { variant: quickBuyVariant } = exploreQuickBuyABTest; + // eslint-disable-next-line no-console + console.log( + `[${EXPLORE_QUICK_BUY_AB_KEY}] ExploreSearchResults result ++++++:`, + exploreQuickBuyABTest, + ); const { onScrollBeginDrag, resetScrollTracking } = useScrollTracking( 'scrolled', diff --git a/app/components/Views/TrendingView/tabs/CryptoTab.tsx b/app/components/Views/TrendingView/tabs/CryptoTab.tsx index 4c263af5ae4..126b2d1a66d 100644 --- a/app/components/Views/TrendingView/tabs/CryptoTab.tsx +++ b/app/components/Views/TrendingView/tabs/CryptoTab.tsx @@ -35,6 +35,12 @@ import type { TabProps } from '../hooks/useExploreRefresh'; import { trackExploreInteracted } from '../search/analytics'; import { TrendingViewSelectorsIDs } from '../TrendingView.testIds'; import TrendingQuickBuy from '../../../UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy'; +import { useABTest } from '../../../../hooks/useABTest'; +import { + EXPLORE_QUICK_BUY_AB_KEY, + EXPLORE_QUICK_BUY_VARIANTS, + EXPLORE_QUICK_BUY_EXPOSURE_METADATA, +} from '../search/abTestConfig'; const styles = StyleSheet.create({ container: { flex: 1 }, @@ -110,6 +116,12 @@ const CryptoTabContent: React.FC = ({ null, ); + const { variant: quickBuyVariant } = useABTest( + EXPLORE_QUICK_BUY_AB_KEY, + EXPLORE_QUICK_BUY_VARIANTS, + EXPLORE_QUICK_BUY_EXPOSURE_METADATA, + ); + const tokens = useTokensFeed({ refresh }); const cryptoPredictions = usePredictionsFeed({ variant: 'crypto', @@ -139,10 +151,12 @@ const CryptoTabContent: React.FC = ({ item_clicked: item.assetId, }) } - onQuickTrade={setQuickTradeToken} + onQuickTrade={ + quickBuyVariant.showQuickTradeButton ? setQuickTradeToken : undefined + } /> ), - [], + [quickBuyVariant.showQuickTradeButton], ); const showTokens = tokens.isLoading || tokens.data.length > 0; diff --git a/app/components/Views/TrendingView/tabs/RwasTab.tsx b/app/components/Views/TrendingView/tabs/RwasTab.tsx index 0cd005976f0..62fd064a4d3 100644 --- a/app/components/Views/TrendingView/tabs/RwasTab.tsx +++ b/app/components/Views/TrendingView/tabs/RwasTab.tsx @@ -44,6 +44,12 @@ import type { TabProps } from '../hooks/useExploreRefresh'; import { trackExploreInteracted } from '../search/analytics'; import { TrendingViewSelectorsIDs } from '../TrendingView.testIds'; import TrendingQuickBuy from '../../../UI/Trending/components/TrendingQuickBuy/TrendingQuickBuy'; +import { useABTest } from '../../../../hooks/useABTest'; +import { + EXPLORE_QUICK_BUY_AB_KEY, + EXPLORE_QUICK_BUY_VARIANTS, + EXPLORE_QUICK_BUY_EXPOSURE_METADATA, +} from '../search/abTestConfig'; const styles = StyleSheet.create({ container: { flex: 1 }, @@ -118,6 +124,12 @@ const RwasTabContent: React.FC = ({ null, ); + const { variant: quickBuyVariant } = useABTest( + EXPLORE_QUICK_BUY_AB_KEY, + EXPLORE_QUICK_BUY_VARIANTS, + EXPLORE_QUICK_BUY_EXPOSURE_METADATA, + ); + const politics = usePredictionsFeed({ variant: 'politics', refresh }); const stocks = useStocksFeed({ refresh, @@ -143,10 +155,12 @@ const RwasTabContent: React.FC = ({ item_clicked: item.assetId, }) } - onQuickTrade={setQuickTradeToken} + onQuickTrade={ + quickBuyVariant.showQuickTradeButton ? setQuickTradeToken : undefined + } /> ), - [], + [quickBuyVariant.showQuickTradeButton], ); const showStocks = stocks.isLoading || stocks.data.length > 0; From 7cd1a2526a2a0ec0a6faaadeb1627b1a003c6abd Mon Sep 17 00:00:00 2001 From: salimtb Date: Mon, 22 Jun 2026 12:23:33 +0200 Subject: [PATCH 12/13] fix: clean up --- .../Views/ExploreSearchScreen/ExploreSearchScreen.tsx | 8 +------- .../Views/TrendingView/search/ExploreSearchResults.tsx | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx b/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx index 835041905b1..c8560afe6b2 100644 --- a/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx +++ b/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx @@ -76,17 +76,11 @@ const FullFeedList: React.FC = ({ null, ); - const exploreQuickBuyABTest = useABTest( + const { variant: quickBuyVariant } = useABTest( EXPLORE_QUICK_BUY_AB_KEY, EXPLORE_QUICK_BUY_VARIANTS, EXPLORE_QUICK_BUY_EXPOSURE_METADATA, ); - const { variant: quickBuyVariant } = exploreQuickBuyABTest; - // eslint-disable-next-line no-console - console.log( - `[${EXPLORE_QUICK_BUY_AB_KEY}] ExploreSearchScreen result:`, - exploreQuickBuyABTest, - ); useEffect(() => { flashListRef.current?.scrollToOffset({ offset: 0, animated: false }); diff --git a/app/components/Views/TrendingView/search/ExploreSearchResults.tsx b/app/components/Views/TrendingView/search/ExploreSearchResults.tsx index 440e3a7f044..d6c7e0a7f9c 100644 --- a/app/components/Views/TrendingView/search/ExploreSearchResults.tsx +++ b/app/components/Views/TrendingView/search/ExploreSearchResults.tsx @@ -113,17 +113,11 @@ const ExploreSearchResults: React.FC = ({ null, ); - const exploreQuickBuyABTest = useABTest( + const { variant: quickBuyVariant } = useABTest( EXPLORE_QUICK_BUY_AB_KEY, EXPLORE_QUICK_BUY_VARIANTS, EXPLORE_QUICK_BUY_EXPOSURE_METADATA, ); - const { variant: quickBuyVariant } = exploreQuickBuyABTest; - // eslint-disable-next-line no-console - console.log( - `[${EXPLORE_QUICK_BUY_AB_KEY}] ExploreSearchResults result ++++++:`, - exploreQuickBuyABTest, - ); const { onScrollBeginDrag, resetScrollTracking } = useScrollTracking( 'scrolled', From f520a31171b1019b7b4b14e0503bb85fa6dadf30 Mon Sep 17 00:00:00 2001 From: salimtb Date: Mon, 22 Jun 2026 12:51:55 +0200 Subject: [PATCH 13/13] fix: put quick buy under ab test --- .../Views/ExploreSearchScreen/ExploreSearchScreen.tsx | 3 ++- .../Views/TrendingView/search/ExploreSearchResults.tsx | 3 ++- .../Views/TrendingView/search/SearchFeedRow.test.tsx | 4 ++-- app/components/Views/TrendingView/search/SearchFeedRow.tsx | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx b/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx index c8560afe6b2..a3783230c91 100644 --- a/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx +++ b/app/components/Views/TrendingView/Views/ExploreSearchScreen/ExploreSearchScreen.tsx @@ -100,7 +100,8 @@ const FullFeedList: React.FC = ({ }, [searchQuery, resetScrollTracking]); const handleQuickTrade = - feedId === 'tokens' && quickBuyVariant.showQuickTradeButton + (feedId === 'tokens' || feedId === 'stocks') && + quickBuyVariant.showQuickTradeButton ? setQuickTradeToken : undefined; diff --git a/app/components/Views/TrendingView/search/ExploreSearchResults.tsx b/app/components/Views/TrendingView/search/ExploreSearchResults.tsx index d6c7e0a7f9c..61161351160 100644 --- a/app/components/Views/TrendingView/search/ExploreSearchResults.tsx +++ b/app/components/Views/TrendingView/search/ExploreSearchResults.tsx @@ -273,7 +273,8 @@ const ExploreSearchResults: React.FC = ({ tabName={activeTab} resultCount={totalResultCount} onQuickTrade={ - item.feedId === 'tokens' && quickBuyVariant.showQuickTradeButton + (item.feedId === 'tokens' || item.feedId === 'stocks') && + quickBuyVariant.showQuickTradeButton ? setQuickTradeToken : undefined } diff --git a/app/components/Views/TrendingView/search/SearchFeedRow.test.tsx b/app/components/Views/TrendingView/search/SearchFeedRow.test.tsx index 14746182b77..6b15ece0e93 100644 --- a/app/components/Views/TrendingView/search/SearchFeedRow.test.tsx +++ b/app/components/Views/TrendingView/search/SearchFeedRow.test.tsx @@ -248,7 +248,7 @@ describe('onQuickTrade prop', () => { expect(mockOnQuickTrade).toHaveBeenCalledWith(token); }); - it('does not forward onQuickTrade to TokenSearchRowItem for the stocks feed', () => { + it('forwards onQuickTrade to TokenSearchRowItem for the stocks feed', () => { const token = { assetId: 'eip155:1/erc20:0xabc' } as TrendingAsset; const { getByTestId } = render( @@ -263,7 +263,7 @@ describe('onQuickTrade prop', () => { ); fireEvent.press(getByTestId('stub-token-row')); - expect(mockOnQuickTrade).not.toHaveBeenCalled(); + expect(mockOnQuickTrade).toHaveBeenCalledWith(token); }); it('renders tokens feed without onQuickTrade when prop is not provided', () => { diff --git a/app/components/Views/TrendingView/search/SearchFeedRow.tsx b/app/components/Views/TrendingView/search/SearchFeedRow.tsx index a774b6abb5a..4e7a46a90ea 100644 --- a/app/components/Views/TrendingView/search/SearchFeedRow.tsx +++ b/app/components/Views/TrendingView/search/SearchFeedRow.tsx @@ -74,7 +74,7 @@ const SearchFeedRow: React.FC = ({ token={item as TrendingAsset} index={index} tokenDetailsSource={TokenDetailsSource.ExploreSearch} - onQuickTrade={feedId === 'tokens' ? onQuickTrade : undefined} + onQuickTrade={onQuickTrade} /> ); case 'perps':