Skip to content

Commit e1c851c

Browse files
authored
fix(rewards): leaderboard split view (#29500)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** Fix split view when user is ranked beyond 20 <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **Afte <img width="1179" height="2556" alt="Simulator Screenshot - E2E Test - 2026-04-29 at 11 42 57" src="https://github.com/user-attachments/assets/cb3087a4-dcbf-451c-a323-b536daed590f" /> <img width="1179" height="2556" alt="Simulator Screenshot - E2E Test - 2026-04-29 at 11 43 00" src="https://github.com/user-attachments/assets/6f5a814c-e004-47f4-a062-f9c0c163e725" /> <img width="1179" height="2556" alt="Simulator Screenshot - E2E Test - 2026-04-29 at 11 49 13" src="https://github.com/user-attachments/assets/dbdb97a4-3f38-4663-9767-654e165229eb" /> <img width="1179" height="2556" alt="Simulator Screenshot - E2E Test - 2026-04-29 at 11 49 19" src="https://github.com/user-attachments/assets/58a80e5d-507b-41e7-9dfe-88d38def0d30" /> <img width="1179" height="2556" alt="Simulator Screenshot - E2E Test - 2026-04-29 at 11 50 03" src="https://github.com/user-attachments/assets/ad5d52fa-16dd-4d52-bdd7-06813d0c4099" /> <img width="1179" height="2556" alt="Simulator Screenshot - E2E Test - 2026-04-29 at 11 50 10" src="https://github.com/user-attachments/assets/acb03dfa-4b6c-4183-ae04-71ab72dc9c9e" /> r** <!-- [screenshots/recordings] --> ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [x] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [x] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [x] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Small, UI-only logic changes (list slicing and stat formatting) with added test coverage; low chance of impacting core rewards data or flows. > > **Overview** > Fixes Ondo rewards leaderboard *split view* rendering when the current user is just beyond the first page of results. > > `OndoLeaderboard` now dynamically chooses how many “top” rows to show above the neighbor separator (preview stays at 3; full view shows 18 for ranks 21–22, otherwise 20), with new tests covering these edge ranks. Separately, `OndoCampaignStatsView` clamps displayed qualified days to the required maximum so the “days held” stat never exceeds the campaign requirement. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 5594d61. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent d2f250f commit e1c851c

3 files changed

Lines changed: 124 additions & 4 deletions

File tree

app/components/UI/Rewards/Views/OndoCampaignStatsView.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,10 @@ const OndoCampaignStatsView: React.FC = () => {
156156
: formatTierDisplayName(leaderboardPosition.projectedTier);
157157

158158
const daysHeldValue = leaderboardPosition
159-
? `${leaderboardPosition.qualifiedDays}/${ONDO_GM_REQUIRED_QUALIFIED_DAYS}`
159+
? `${Math.min(
160+
leaderboardPosition.qualifiedDays,
161+
ONDO_GM_REQUIRED_QUALIFIED_DAYS,
162+
)}/${ONDO_GM_REQUIRED_QUALIFIED_DAYS}`
160163
: '-';
161164

162165
const tierMinDeposit = useMemo(

app/components/UI/Rewards/components/Campaigns/OndoLeaderboard.test.tsx

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,14 @@ describe('OndoLeaderboard', () => {
439439
}),
440440
);
441441

442+
const twentyFiveEntries = Array.from({ length: 25 }, (_, i) =>
443+
createMockEntry({
444+
rank: i + 1,
445+
referralCode: `T${String(i + 1).padStart(3, '0')}`,
446+
rateOfReturn: 0.12,
447+
}),
448+
);
449+
442450
it('renders normally when no userPosition is provided', () => {
443451
const { queryByTestId } = render(
444452
<OndoLeaderboard
@@ -622,6 +630,99 @@ describe('OndoLeaderboard', () => {
622630
getByTestId(CAMPAIGN_LEADERBOARD_TEST_IDS.NEIGHBOR_SEPARATOR),
623631
).toBeDefined();
624632
});
633+
634+
it('in full view renders 18 top rows when user rank is 21', () => {
635+
const { getByTestId, queryByTestId } = render(
636+
<OndoLeaderboard
637+
{...defaultProps}
638+
entries={twentyFiveEntries}
639+
selectedTier="STARTER"
640+
userPosition={{
641+
projectedTier: 'STARTER',
642+
rank: 21,
643+
neighbors: [
644+
createMockEntry({ rank: 20, referralCode: 'N20' }),
645+
createMockEntry({ rank: 21, referralCode: 'USER' }),
646+
createMockEntry({ rank: 22, referralCode: 'N22' }),
647+
],
648+
}}
649+
currentUserReferralCode="USER"
650+
/>,
651+
);
652+
653+
expect(
654+
getByTestId(`${CAMPAIGN_LEADERBOARD_TEST_IDS.ENTRY_ROW}-18`),
655+
).toBeDefined();
656+
expect(
657+
queryByTestId(`${CAMPAIGN_LEADERBOARD_TEST_IDS.ENTRY_ROW}-19`),
658+
).toBeNull();
659+
expect(
660+
getByTestId(CAMPAIGN_LEADERBOARD_TEST_IDS.NEIGHBOR_SEPARATOR),
661+
).toBeDefined();
662+
expect(
663+
getByTestId(`${CAMPAIGN_LEADERBOARD_TEST_IDS.ENTRY_ROW}-20`),
664+
).toBeDefined();
665+
});
666+
667+
it('in full view renders 18 top rows when user rank is 22', () => {
668+
const { getByTestId, queryByTestId } = render(
669+
<OndoLeaderboard
670+
{...defaultProps}
671+
entries={twentyFiveEntries}
672+
selectedTier="STARTER"
673+
userPosition={{
674+
projectedTier: 'STARTER',
675+
rank: 22,
676+
neighbors: [
677+
createMockEntry({ rank: 21, referralCode: 'N21' }),
678+
createMockEntry({ rank: 22, referralCode: 'USER' }),
679+
createMockEntry({ rank: 23, referralCode: 'N23' }),
680+
],
681+
}}
682+
currentUserReferralCode="USER"
683+
/>,
684+
);
685+
686+
expect(
687+
getByTestId(`${CAMPAIGN_LEADERBOARD_TEST_IDS.ENTRY_ROW}-18`),
688+
).toBeDefined();
689+
expect(
690+
queryByTestId(`${CAMPAIGN_LEADERBOARD_TEST_IDS.ENTRY_ROW}-19`),
691+
).toBeNull();
692+
expect(
693+
getByTestId(CAMPAIGN_LEADERBOARD_TEST_IDS.NEIGHBOR_SEPARATOR),
694+
).toBeDefined();
695+
});
696+
697+
it('in full view renders 20 top rows when user rank is 23 (not 21 or 22)', () => {
698+
const { getByTestId, queryByTestId } = render(
699+
<OndoLeaderboard
700+
{...defaultProps}
701+
entries={twentyFiveEntries}
702+
selectedTier="STARTER"
703+
userPosition={{
704+
projectedTier: 'STARTER',
705+
rank: 23,
706+
neighbors: [
707+
createMockEntry({ rank: 22, referralCode: 'N22' }),
708+
createMockEntry({ rank: 23, referralCode: 'USER' }),
709+
createMockEntry({ rank: 24, referralCode: 'N24' }),
710+
],
711+
}}
712+
currentUserReferralCode="USER"
713+
/>,
714+
);
715+
716+
expect(
717+
getByTestId(`${CAMPAIGN_LEADERBOARD_TEST_IDS.ENTRY_ROW}-20`),
718+
).toBeDefined();
719+
expect(
720+
queryByTestId(`${CAMPAIGN_LEADERBOARD_TEST_IDS.ENTRY_ROW}-21`),
721+
).toBeNull();
722+
expect(
723+
getByTestId(CAMPAIGN_LEADERBOARD_TEST_IDS.NEIGHBOR_SEPARATOR),
724+
).toBeDefined();
725+
});
625726
});
626727

627728
describe('pending tag', () => {

app/components/UI/Rewards/components/Campaigns/OndoLeaderboard.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ export const CAMPAIGN_LEADERBOARD_TEST_IDS = {
4444
} as const;
4545

4646
const MAX_ENTRIES_LIMIT = 20;
47-
const SPLIT_VIEW_TOP_COUNT = 3;
47+
const SPLIT_VIEW_TOP_COUNT_PREVIEW = 3;
48+
/** Ranks just below the first page: show one fewer top rows to keep split view from crowding the neighbor block. */
49+
const FULL_SPLIT_TOP_REDUCED_AT_RANKS: readonly number[] = [21, 22];
4850

4951
interface UserPosition {
5052
projectedTier: string;
@@ -229,6 +231,20 @@ const OndoLeaderboard: React.FC<CampaignLeaderboardProps> = ({
229231
? maxEntries
230232
: MAX_ENTRIES_LIMIT;
231233

234+
/** Top rows above the neighbor separator in split view (preview: 3; full: 18 for rank 21–22, else 20). */
235+
const splitViewTopCount = useMemo(() => {
236+
if (isPreview) {
237+
return SPLIT_VIEW_TOP_COUNT_PREVIEW;
238+
}
239+
const rank = userPosition?.rank;
240+
if (rank == null) {
241+
return MAX_ENTRIES_LIMIT;
242+
}
243+
return FULL_SPLIT_TOP_REDUCED_AT_RANKS.includes(rank)
244+
? MAX_ENTRIES_LIMIT - 2
245+
: MAX_ENTRIES_LIMIT;
246+
}, [isPreview, userPosition?.rank]);
247+
232248
const showSplitView = useMemo(() => {
233249
if (!userPosition) return false;
234250
return (
@@ -240,10 +256,10 @@ const OndoLeaderboard: React.FC<CampaignLeaderboardProps> = ({
240256

241257
const visibleEntries = useMemo(() => {
242258
if (showSplitView) {
243-
return entries.slice(0, SPLIT_VIEW_TOP_COUNT);
259+
return entries.slice(0, splitViewTopCount);
244260
}
245261
return entries.slice(0, effectiveMaxEntries);
246-
}, [entries, effectiveMaxEntries, showSplitView]);
262+
}, [showSplitView, entries, effectiveMaxEntries, splitViewTopCount]);
247263

248264
const selectedTierLabel = selectedTier
249265
? formatTierDisplayName(selectedTier)

0 commit comments

Comments
 (0)