Skip to content

Commit 340209c

Browse files
VGR-GITclaude
andauthored
refactor(rewards): lift portfolio and leaderboard position hook calls to parent views (#28049)
## **Description** This PR refactors how `OndoCampaignDetailsView` decides which sections to render, replacing ad-hoc inline conditions with a single `useMemo` that computes five boolean flags. It also replaces `CampaignEntriesClosedBanner` with `RewardsInfoBanner` for a unified visual treatment. ### Other changes - `OndoLeaderboard`: new `showTitle` prop (default `true`) — parent can suppress the section title when rendering inline. `computedAt` timestamp moves to sit alongside the tier-tabs row instead of a standalone header row. - `OndoLeaderboardPosition`: new `showTitle` (default `false`) and `computedAt` props — full leaderboard view opts in to a header with a "last updated" timestamp. - `OndoPortfolio`: skeleton now shows when loading with an empty positions array (not just when portfolio is `null`). - `CampaignTile`: Enter Now badge is gated behind `isOptinAllowed(campaign)` so it disappears once the deposit cutoff has passed. - `useGetOndoLeaderboard` is now called unconditionally with `campaignId` instead of a computed conditional ID, since all render paths need leaderboard data. ## **Changelog** CHANGELOG entry: null ## **Screenshots/Recordings** Phase 1 - Opt in guidance <img width="1444" height="132" alt="image" src="https://github.com/user-attachments/assets/78fff8c4-ea9b-4292-9876-1f80c66b29ad" /> <img width="722" height="1229" alt="image" src="https://github.com/user-attachments/assets/e36e6278-baa4-47f4-8477-ec92c58f5288" /> --- Phase 1 - Opted in & Position Guidance & Leaderboard <img width="1585" height="210" alt="image" src="https://github.com/user-attachments/assets/d5c58171-9933-4485-89f3-f9832b2d16f4" /> <img width="947" height="1774" alt="Screenshot from 2026-03-30 14-46-51" src="https://github.com/user-attachments/assets/84792f98-96b5-476f-8c2c-2c409cd86260" /> --- Phase 1 - Opted in & at least one position <img width="1585" height="246" alt="image" src="https://github.com/user-attachments/assets/9026da48-ee64-4ea0-8d2c-78d42802da3d" /> <img width="934" height="1778" alt="Screenshot from 2026-03-30 14-35-38" src="https://github.com/user-attachments/assets/c4c7a81e-ad75-435f-8f60-8db979f282d4" /> --- Phase 2 - Not Opted in & cut off date reached <img width="1203" height="244" alt="image" src="https://github.com/user-attachments/assets/3e52588d-2f4a-4e89-a824-f494612442f5" /> <img width="934" height="1778" alt="Screenshot from 2026-03-30 14-41-17" src="https://github.com/user-attachments/assets/6103fd3a-9372-4fa7-becb-f9392517a780" /> --- Phase 2 - Opted in & cut off date reached & No positions <img width="1523" height="398" alt="image" src="https://github.com/user-attachments/assets/22bddf1b-7db4-4e26-86a1-03c046ca8912" /> <img width="934" height="1778" alt="Screenshot from 2026-03-30 14-41-17" src="https://github.com/user-attachments/assets/6103fd3a-9372-4fa7-becb-f9392517a780" /> --- Phase 2 - Opted in & cut off date reached & positions <img width="934" height="1778" alt="Screenshot from 2026-03-30 14-35-38" src="https://github.com/user-attachments/assets/c4c7a81e-ad75-435f-8f60-8db979f282d4" /> --- Completed - Not opted in or opted in and no positions <img width="1523" height="240" alt="image" src="https://github.com/user-attachments/assets/db179fdb-2e0e-4902-a5bf-80be2a7305c8" /> <img width="941" height="1811" alt="Screenshot from 2026-03-30 13-39-10" src="https://github.com/user-attachments/assets/bcfad4e3-1dca-47ce-8ad6-e9a83ec69fbd" /> --- Completed - opted in and at least one position <img width="1444" height="298" alt="image" src="https://github.com/user-attachments/assets/41cb2e04-ca8a-4157-b3c5-5b3e0c9b7e9f" /> <img width="941" height="1811" alt="Screenshot from 2026-03-30 13-50-02" src="https://github.com/user-attachments/assets/71652bc4-f6cb-477f-bfe7-60d2fc236449" /> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Refactors the Ondo rewards screens’ section-gating and data-fetch paths (leaderboard/portfolio/position), which can affect what users see and when network calls happen, but does not touch auth or core transaction flows. > > **Overview** > Improves Ondo rewards screens by **lifting `useGetOndoPortfolioPosition` and `useGetOndoLeaderboardPosition` calls into the parent views** and passing data/loading/error state down into `OndoPortfolio` and `OndoLeaderboardPosition`, removing their internal hook/selector coupling. > > `OndoCampaignDetailsView` now computes a single set of boolean flags to control *How it works*, portfolio, leaderboard, and position sections, replaces the old entries-closed banner with a unified `RewardsInfoBanner`, and fetches leaderboard data unconditionally for the campaign. `OndoLeaderboardView` similarly wires in the position hook and renders leaderboard/position components without duplicated titles. > > UI behavior tweaks: `CampaignTile` hides the “Enter now” label when opt-in is closed, `OndoLeaderboard` adds `showTitle` and adjusts where the “updated at” timestamp renders (including single-tier), and `OndoPortfolio` shows a skeleton when loading with an empty positions array. Copy updates add new competition-closed strings and rename the portfolio title in `en.json`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9477e99. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3f3d69b commit 340209c

15 files changed

Lines changed: 729 additions & 594 deletions

app/components/UI/Rewards/Views/OndoCampaignDetailsView.test.tsx

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import {
1111
import { useRewardCampaigns } from '../hooks/useRewardCampaigns';
1212
import { useGetCampaignParticipantStatus } from '../hooks/useGetCampaignParticipantStatus';
1313
import { useGetOndoLeaderboard } from '../hooks/useGetOndoLeaderboard';
14+
import { useGetOndoLeaderboardPosition } from '../hooks/useGetOndoLeaderboardPosition';
15+
import { useGetOndoPortfolioPosition } from '../hooks/useGetOndoPortfolioPosition';
1416
import Routes from '../../../../constants/navigation/Routes';
1517

1618
const mockGoBack = jest.fn();
@@ -164,14 +166,14 @@ jest.mock('../components/Campaigns/OndoPortfolio', () => {
164166
};
165167
});
166168

167-
jest.mock('../components/Campaigns/CampaignEntriesClosedBanner', () => {
169+
jest.mock('../components/RewardsInfoBanner', () => {
168170
const ReactActual = jest.requireActual('react');
169171
const { View } = jest.requireActual('react-native');
170172
return {
171173
__esModule: true,
172174
default: () =>
173175
ReactActual.createElement(View, {
174-
testID: 'campaign-entries-closed-banner',
176+
testID: 'competition-ended-banner',
175177
}),
176178
};
177179
});
@@ -236,15 +238,29 @@ const mockUseGetOndoLeaderboard = useGetOndoLeaderboard as jest.MockedFunction<
236238
typeof useGetOndoLeaderboard
237239
>;
238240

241+
jest.mock('../hooks/useGetOndoLeaderboardPosition');
242+
const mockUseGetOndoLeaderboardPosition =
243+
useGetOndoLeaderboardPosition as jest.MockedFunction<
244+
typeof useGetOndoLeaderboardPosition
245+
>;
246+
247+
jest.mock('../hooks/useGetOndoPortfolioPosition');
248+
const mockUseGetOndoPortfolioPosition =
249+
useGetOndoPortfolioPosition as jest.MockedFunction<
250+
typeof useGetOndoPortfolioPosition
251+
>;
252+
239253
jest.mock('../../../../../locales/i18n', () => ({
240254
strings: (key: string) => {
241255
const translations: Record<string, string> = {
242256
'rewards.campaigns_view.error_title': 'Unable to load',
243257
'rewards.campaigns_view.error_description': 'Please try again.',
244258
'rewards.campaigns_view.retry_button': 'Retry',
245259
'rewards.campaign_details.join_campaign': 'Join Campaign',
246-
'rewards.campaign_details.entries_closed_title': 'Entries closed',
247-
'rewards.campaign_details.entries_closed_description': 'Missed window',
260+
'rewards.campaign_details.competition_closed_title':
261+
'Competition no longer open',
262+
'rewards.campaign_details.competition_closed_description':
263+
'Entries are now closed',
248264
};
249265
return translations[key] || key;
250266
},
@@ -307,6 +323,20 @@ describe('OndoCampaignDetailsView', () => {
307323
setSelectedTier: jest.fn(),
308324
refetch: jest.fn(),
309325
});
326+
mockUseGetOndoLeaderboardPosition.mockReturnValue({
327+
position: null,
328+
isLoading: false,
329+
hasError: false,
330+
hasFetched: false,
331+
refetch: jest.fn(),
332+
});
333+
mockUseGetOndoPortfolioPosition.mockReturnValue({
334+
portfolio: null,
335+
isLoading: false,
336+
hasError: false,
337+
hasFetched: false,
338+
refetch: jest.fn(),
339+
});
310340
});
311341

312342
it('renders the container', () => {
@@ -463,7 +493,7 @@ describe('OndoCampaignDetailsView', () => {
463493
expect(queryByTestId('campaign-how-it-works')).toBeNull();
464494
});
465495

466-
it('renders OndoLeaderboardPosition when participant is opted in', () => {
496+
it('renders OndoLeaderboardPosition when participant is opted in with positions', () => {
467497
mockUseRewardCampaigns.mockReturnValue({
468498
...hookDefaults,
469499
campaigns: [createTestCampaign()],
@@ -474,6 +504,13 @@ describe('OndoCampaignDetailsView', () => {
474504
hasError: false,
475505
refetch: jest.fn(),
476506
});
507+
mockUseGetOndoPortfolioPosition.mockReturnValue({
508+
portfolio: { positions: [{}], summary: {}, computedAt: '' } as never,
509+
isLoading: false,
510+
hasError: false,
511+
hasFetched: true,
512+
refetch: jest.fn(),
513+
});
477514
const { getByTestId } = render(<OndoCampaignDetailsView />);
478515
expect(getByTestId('ondo-leaderboard-position')).toBeDefined();
479516
});
@@ -620,7 +657,7 @@ describe('OndoCampaignDetailsView', () => {
620657
});
621658
});
622659

623-
describe('entries-closed banner', () => {
660+
describe('competition ended banner', () => {
624661
it('shows the banner when campaign is active, past cutoff, and user is not opted in', () => {
625662
mockUseRewardCampaigns.mockReturnValue({
626663
...hookDefaults,
@@ -634,7 +671,26 @@ describe('OndoCampaignDetailsView', () => {
634671
],
635672
});
636673
const { getByTestId } = render(<OndoCampaignDetailsView />);
637-
expect(getByTestId('campaign-entries-closed-banner')).toBeDefined();
674+
expect(getByTestId('competition-ended-banner')).toBeDefined();
675+
});
676+
677+
it('shows the banner when campaign is complete and user is not opted in', () => {
678+
const lastMonth = new Date();
679+
lastMonth.setMonth(lastMonth.getMonth() - 1);
680+
const yesterday = new Date();
681+
yesterday.setDate(yesterday.getDate() - 1);
682+
683+
mockUseRewardCampaigns.mockReturnValue({
684+
...hookDefaults,
685+
campaigns: [
686+
createTestCampaign({
687+
startDate: lastMonth.toISOString(),
688+
endDate: yesterday.toISOString(),
689+
}),
690+
],
691+
});
692+
const { getByTestId } = render(<OndoCampaignDetailsView />);
693+
expect(getByTestId('competition-ended-banner')).toBeDefined();
638694
});
639695

640696
it('does not show the banner when entries are still open', () => {
@@ -643,10 +699,10 @@ describe('OndoCampaignDetailsView', () => {
643699
campaigns: [createTestCampaign()],
644700
});
645701
const { queryByTestId } = render(<OndoCampaignDetailsView />);
646-
expect(queryByTestId('campaign-entries-closed-banner')).toBeNull();
702+
expect(queryByTestId('competition-ended-banner')).toBeNull();
647703
});
648704

649-
it('does not show the banner when the user is opted in', () => {
705+
it('does not show the banner when the user is opted in with portfolio fetching', () => {
650706
mockUseRewardCampaigns.mockReturnValue({
651707
...hookDefaults,
652708
campaigns: [
@@ -664,11 +720,12 @@ describe('OndoCampaignDetailsView', () => {
664720
hasError: false,
665721
refetch: jest.fn(),
666722
});
723+
// Portfolio not yet fetched — banner should be hidden while data loads
667724
const { queryByTestId } = render(<OndoCampaignDetailsView />);
668-
expect(queryByTestId('campaign-entries-closed-banner')).toBeNull();
725+
expect(queryByTestId('competition-ended-banner')).toBeNull();
669726
});
670727

671-
it('does not show the banner while participant status is loading', () => {
728+
it('does not show the banner while participant status is loading (entries closed)', () => {
672729
mockUseRewardCampaigns.mockReturnValue({
673730
...hookDefaults,
674731
campaigns: [
@@ -687,12 +744,12 @@ describe('OndoCampaignDetailsView', () => {
687744
refetch: jest.fn(),
688745
});
689746
const { queryByTestId } = render(<OndoCampaignDetailsView />);
690-
expect(queryByTestId('campaign-entries-closed-banner')).toBeNull();
747+
expect(queryByTestId('competition-ended-banner')).toBeNull();
691748
});
692749
});
693750

694751
describe('leaderboard position', () => {
695-
it('shows OndoLeaderboardPosition when participant is opted in', () => {
752+
it('shows OndoLeaderboardPosition when participant is opted in with positions', () => {
696753
mockUseRewardCampaigns.mockReturnValue({
697754
...hookDefaults,
698755
campaigns: [createTestCampaign()],
@@ -703,6 +760,13 @@ describe('OndoCampaignDetailsView', () => {
703760
hasError: false,
704761
refetch: jest.fn(),
705762
});
763+
mockUseGetOndoPortfolioPosition.mockReturnValue({
764+
portfolio: { positions: [{}], summary: {}, computedAt: '' } as never,
765+
isLoading: false,
766+
hasError: false,
767+
hasFetched: true,
768+
refetch: jest.fn(),
769+
});
706770
const { getByTestId } = render(<OndoCampaignDetailsView />);
707771
expect(getByTestId('ondo-leaderboard-position')).toBeDefined();
708772
});
@@ -808,7 +872,7 @@ describe('OndoCampaignDetailsView', () => {
808872
expect(queryByText('rewards.ondo_campaign_leaderboard.title')).toBeNull();
809873
});
810874

811-
it('shows leaderboard section header when opted in', () => {
875+
it('shows leaderboard section header when opted in with positions', () => {
812876
mockUseRewardCampaigns.mockReturnValue({
813877
...hookDefaults,
814878
campaigns: [createTestCampaign()],
@@ -819,6 +883,13 @@ describe('OndoCampaignDetailsView', () => {
819883
hasError: false,
820884
refetch: jest.fn(),
821885
});
886+
mockUseGetOndoPortfolioPosition.mockReturnValue({
887+
portfolio: { positions: [{}], summary: {}, computedAt: '' } as never,
888+
isLoading: false,
889+
hasError: false,
890+
hasFetched: true,
891+
refetch: jest.fn(),
892+
});
822893
const { getByText } = render(<OndoCampaignDetailsView />);
823894
expect(
824895
getByText('rewards.ondo_campaign_leaderboard.title'),

0 commit comments

Comments
 (0)