Skip to content
Open
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
3731883
chore: adds the initial setup for leaderboard
zone-live Mar 27, 2026
56fe30f
chore: review points
zone-live Mar 27, 2026
b315bbc
chore: tests update
zone-live Mar 27, 2026
60c8de0
chore: update
zone-live Mar 27, 2026
e615d55
chore: UI update
zone-live Mar 30, 2026
434c01b
Merge branch 'main' into TSA-114-traders-list-leaderboard
zone-live Mar 30, 2026
84d9e79
chore: button foolow | following
zone-live Mar 30, 2026
8d75c90
chore: integrates core controller and service in the Top Traders feature
zone-live Mar 31, 2026
154509c
chore: api url
zone-live Mar 31, 2026
0f5f7d0
Merge branch 'main' into TSA-114-traders-list-leaderboard
zone-live Mar 31, 2026
0d54e09
chore: mock useQuery
zone-live Mar 31, 2026
8f74d86
Merge branch 'main' into TSA-114-traders-list-leaderboard
zone-live Mar 31, 2026
7de904b
chore: lock update
zone-live Mar 31, 2026
dac5f62
chore: adds mocked data
zone-live Mar 31, 2026
e9a1a06
chore: adds mocked data
zone-live Mar 31, 2026
f5cb2b0
chore: calls the Social Dev API
zone-live Apr 1, 2026
0529476
Merge branch 'main' into TSA-114-traders-list-leaderboard
zone-live Apr 1, 2026
360ecef
chore: update test
zone-live Apr 1, 2026
8f4558a
chore: update initial-background-state.json
zone-live Apr 1, 2026
7422558
chore: update snapshots
zone-live Apr 1, 2026
955b334
chore: fix addCommas
zone-live Apr 1, 2026
0764134
chore: update and clean up
zone-live Apr 1, 2026
3d3ddd5
chore: update and clean up
zone-live Apr 1, 2026
bbe85b2
Merge branch 'main' into TSA-114-traders-list-leaderboard
zone-live Apr 1, 2026
7d94c98
chore: update api url
zone-live Apr 1, 2026
cbd6b28
Merge branch 'TSA-114-traders-list-leaderboard' of github.com:MetaMas…
zone-live Apr 1, 2026
86ea0af
chore: test update
zone-live Apr 1, 2026
2c77bd3
Merge branch 'main' into TSA-114-traders-list-leaderboard
zone-live Apr 1, 2026
40d3e3f
Merge branch 'main' into TSA-114-traders-list-leaderboard
zone-live Apr 2, 2026
6c07b42
chore: update preview package
zone-live Apr 2, 2026
63af6e2
chore: update formatPnl
zone-live Apr 2, 2026
a1bfd8d
chore: test update
zone-live Apr 2, 2026
e77a8b6
chore: review updates
zone-live Apr 2, 2026
d3d82f5
chore: logger fix and preview package update
zone-live Apr 2, 2026
82d18a2
chore: dedupe
zone-live Apr 2, 2026
9a904d9
Merge branch 'main' into TSA-114-traders-list-leaderboard
zone-live Apr 2, 2026
5554c47
chore: remove preview package and add correct one
zone-live Apr 2, 2026
e2b056d
Merge branch 'main' into TSA-114-traders-list-leaderboard
zone-live Apr 13, 2026
5f9424f
chore: review update and lint fix
zone-live Apr 13, 2026
5881e59
chore: review update
zone-live Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/components/Views/Homepage/Homepage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,16 @@ jest.mock('../../../selectors/featureFlagController/socialLeaderboard', () => ({
selectSocialLeaderboardEnabled: jest.fn(() => false),
}));

jest.mock('./Sections/TopTraders/hooks', () => ({
useTopTraders: jest.fn(() => ({
traders: [],
isLoading: false,
error: null,
refresh: jest.fn().mockResolvedValue(undefined),
toggleFollow: jest.fn(),
})),
}));

/** Shape of first argument to useHomeViewedEvent (for asserting in tests). */
interface UseHomeViewedEventParamsSnapshot {
sectionName?: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,32 @@ import TopTradersSection from './TopTradersSection';
import Routes from '../../../../../constants/navigation/Routes';
import { SectionRefreshHandle } from '../../types';

const mockRefetch = jest.fn().mockResolvedValue(undefined);
const mockNavigate = jest.fn();

const mockTraders = [
{
id: 'trader-1',
rank: 1,
username: 'alice',
percentageChange: 96.2,
pnlValue: 963000,
isFollowing: false,
},
];

const mockUseTopTraders = jest.fn((_options?: unknown) => ({
traders: mockTraders,
isLoading: false,
error: null,
refresh: mockRefetch,
toggleFollow: jest.fn(),
}));

jest.mock('./hooks', () => ({
useTopTraders: (args: unknown) => mockUseTopTraders(args),
}));

jest.mock('@react-navigation/native', () => {
const actual = jest.requireActual('@react-navigation/native');
return {
Expand Down Expand Up @@ -47,6 +71,39 @@ describe('TopTradersSection', () => {
beforeEach(() => {
jest.clearAllMocks();
mockSelectSocialLeaderboardEnabled.mockImplementation(() => true);
mockUseTopTraders.mockReturnValue({
traders: mockTraders,
isLoading: false,
error: null,
refresh: mockRefetch,
toggleFollow: jest.fn(),
});
});

it('returns null when the API returns no traders', () => {
mockUseTopTraders.mockReturnValue({
traders: [],
isLoading: false,
error: null,
refresh: mockRefetch,
toggleFollow: jest.fn(),
});
renderWithProvider(<TopTradersSection {...defaultProps} />);
expect(screen.queryByTestId('homepage-top-traders-carousel')).toBeNull();
});

it('renders skeletons while loading even when traders is empty', () => {
mockUseTopTraders.mockReturnValue({
traders: [],
isLoading: true,
error: null,
refresh: mockRefetch,
toggleFollow: jest.fn(),
});
renderWithProvider(<TopTradersSection {...defaultProps} />);
expect(
screen.getByTestId('homepage-top-traders-carousel'),
).toBeOnTheScreen();
});

it('returns null when the feature flag is disabled', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import React, {
useImperativeHandle,
useRef,
} from 'react';
import { ScrollView, View } from 'react-native';
import { View } from 'react-native';
import { ScrollView } from 'react-native-gesture-handler';
import { useNavigation } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
import { Box } from '@metamask/design-system-react-native';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
import SectionHeader from '../../../../../component-library/components-temp/SectionHeader';
import { SectionRefreshHandle } from '../../types';
import { selectSocialLeaderboardEnabled } from '../../../../../selectors/featureFlagController/socialLeaderboard';
Expand All @@ -17,51 +18,65 @@ import Routes from '../../../../../constants/navigation/Routes';
import useHomeViewedEvent, {
HomeSectionNames,
} from '../../hooks/useHomeViewedEvent';
import { TopTraderCard, TopTraderCardSkeleton } from './components';
import { useTopTraders } from './hooks';

const HOME_TRADER_LIMIT = 3;
const SKELETON_KEYS = Array.from(
{ length: HOME_TRADER_LIMIT },
(_, i) => `home-trader-skeleton-${i}`,
);

interface TopTradersSectionProps {
sectionIndex: number;
totalSectionsLoaded: number;
}

/**
* TopTradersSection Social leaderboard section on the homepage.
* TopTradersSection -- Social leaderboard entry point on the homepage.
*
* Shows a horizontal carousel of top-performing traders.
* Currently renders an empty placeholder carousel while the data layer is being built.
* Renders a section header plus a horizontally scrollable row of the
* top 3 trader cards. Tapping the header chevron navigates to the
* full TopTradersView.
*/
const TopTradersSection = forwardRef<
SectionRefreshHandle,
TopTradersSectionProps
>(({ sectionIndex, totalSectionsLoaded }, ref) => {
const sectionViewRef = useRef<View>(null);
const tw = useTailwind();
const navigation = useNavigation();
const tw = useTailwind();
const isEnabled = useSelector(selectSocialLeaderboardEnabled);
const title = strings('homepage.sections.top_traders');

const { traders, isLoading, refresh, toggleFollow } = useTopTraders({
limit: HOME_TRADER_LIMIT,
enabled: isEnabled,
});

useImperativeHandle(
ref,
() => ({
refresh: async () => undefined,
refresh,
}),
[],
[refresh],
);

const { onLayout } = useHomeViewedEvent({
sectionRef: sectionViewRef,
isLoading: false,
isLoading,
sectionName: HomeSectionNames.TOP_TRADERS,
sectionIndex,
totalSectionsLoaded,
isEmpty: true,
itemCount: 0,
isEmpty: traders.length === 0,
itemCount: traders.length,
});

const handleViewAll = useCallback(() => {
navigation.navigate(Routes.SOCIAL_LEADERBOARD.VIEW as never);
}, [navigation]);

if (!isEnabled) {
if (!isEnabled || (!isLoading && traders.length === 0)) {
return null;
}

Expand All @@ -73,12 +88,23 @@ const TopTradersSection = forwardRef<
>
<Box gap={3}>
<SectionHeader title={title} onPress={handleViewAll} />

<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={tw.style('px-4 gap-2.5')}
contentContainerStyle={tw.style('px-4 gap-3 pb-2')}
testID="homepage-top-traders-carousel"
/>
>
{isLoading
? SKELETON_KEYS.map((key) => <TopTraderCardSkeleton key={key} />)
: traders.map((trader) => (
<TopTraderCard
key={trader.id}
trader={trader}
onFollowPress={toggleFollow}
/>
))}
</ScrollView>
</Box>
</View>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import { TouchableOpacity, View } from 'react-native';
import { useTailwind } from '@metamask/design-system-twrnc-preset';
import {
Text,
Icon,
IconName,
IconColor,
IconSize,
} from '@metamask/design-system-react-native';
import { strings } from '../../../../../../../locales/i18n';
import type { NetworkFilterSelection } from '../types';

export interface NetworkFilterButtonProps {
/** Currently selected network label (null = All networks) */
selectedNetwork: NetworkFilterSelection;
/** Called when the user taps the button */
onPress: () => void;
testID?: string;
}

/**
* NetworkFilterButton — compact dropdown-style button for filtering by network.
*
* Follows the same visual pattern as the FilterButton in the Trending section.
* Tapping opens the TrendingTokenNetworkBottomSheet managed by the parent.
*/
const NetworkFilterButton: React.FC<NetworkFilterButtonProps> = ({
selectedNetwork,
onPress,
testID,
}) => {
const tw = useTailwind();

const label = selectedNetwork ?? strings('social_leaderboard.all_networks');

return (
<TouchableOpacity
testID={testID ?? 'top-traders-network-filter-button'}
onPress={onPress}
style={tw.style('self-start rounded-lg bg-muted py-2 px-3')}
activeOpacity={0.2}
>
<View style={tw`flex-row items-center justify-center gap-1`}>
<Text
twClassName="text-[14px] font-semibold text-default"
numberOfLines={1}
>
{label}
</Text>
<Icon
name={IconName.ArrowDown}
color={IconColor.IconAlternative}
size={IconSize.Xs}
/>
</View>
</TouchableOpacity>
);
};

export default NetworkFilterButton;
Loading
Loading