Skip to content
Closed
6 changes: 6 additions & 0 deletions app/components/UI/Rewards/RewardsNavigator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import OndoCampaignDetailsView from './Views/OndoCampaignDetailsView';
import SeasonOneCampaignDetailsView from './Views/SeasonOneCampaignDetailsView';
import CampaignMechanicsView from './Views/CampaignMechanicsView';
import MusdCalculatorView from './Views/MusdCalculatorView';
import OndoLeaderboardView from './Views/OndoLeaderboardView';
import { useSelector } from 'react-redux';
import { selectRewardsSubscriptionId } from '../../../selectors/rewards';
import { selectIsRewardsVersionBlocked } from '../../../reducers/rewards/selectors';
Expand Down Expand Up @@ -112,6 +113,11 @@ const RewardsNavigator: React.FC = () => {
component={MusdCalculatorView}
options={{ headerShown: false }}
/>
<Stack.Screen
name={Routes.REWARDS_ONDO_CAMPAIGN_LEADERBOARD}
component={OndoLeaderboardView}
options={{ headerShown: false }}
/>
</>
) : null}
</Stack.Navigator>
Expand Down
146 changes: 146 additions & 0 deletions app/components/UI/Rewards/Views/OndoCampaignDetailsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,18 @@ jest.mock('../components/Campaigns/CampaignHowItWorks', () => {
};
});

jest.mock('../components/Campaigns/OndoLeaderboardPosition', () => {
const ReactActual = jest.requireActual('react');
const { View } = jest.requireActual('react-native');
return {
__esModule: true,
default: () =>
ReactActual.createElement(View, {
testID: 'ondo-leaderboard-position',
}),
};
});

jest.mock('../components/Campaigns/CampaignOptInSheet', () => {
const ReactActual = jest.requireActual('react');
const { View } = jest.requireActual('react-native');
Expand All @@ -136,6 +148,18 @@ jest.mock('../components/Campaigns/CampaignOptInSheet', () => {
};
});

jest.mock('../components/Campaigns/RewardsCampaignPortfolio', () => {
const ReactActual = jest.requireActual('react');
const { View } = jest.requireActual('react-native');
return {
__esModule: true,
default: ({ campaignId: _campaignId }: { campaignId: string }) =>
ReactActual.createElement(View, {
testID: 'rewards-campaign-portfolio',
}),
};
});

jest.mock('../components/RewardsErrorBanner', () => {
const ReactActual = jest.requireActual('react');
const { View, Text, Pressable } = jest.requireActual('react-native');
Expand Down Expand Up @@ -342,6 +366,36 @@ describe('OndoCampaignDetailsView', () => {
const { queryByTestId } = render(<OndoCampaignDetailsView />);
expect(queryByTestId('campaign-how-it-works')).toBeNull();
});

it('renders OndoLeaderboardPosition when participant is opted in', () => {
mockUseRewardCampaigns.mockReturnValue({
...hookDefaults,
campaigns: [createTestCampaign()],
});
mockUseGetCampaignParticipantStatus.mockReturnValue({
status: { optedIn: true, participantCount: 1 },
isLoading: false,
hasError: false,
refetch: jest.fn(),
});
const { getByTestId } = render(<OndoCampaignDetailsView />);
expect(getByTestId('ondo-leaderboard-position')).toBeDefined();
});

it('does not render OndoLeaderboardPosition when participant is not opted in', () => {
mockUseRewardCampaigns.mockReturnValue({
...hookDefaults,
campaigns: [createTestCampaign()],
});
mockUseGetCampaignParticipantStatus.mockReturnValue({
status: { optedIn: false, participantCount: 0 },
isLoading: false,
hasError: false,
refetch: jest.fn(),
});
const { queryByTestId } = render(<OndoCampaignDetailsView />);
expect(queryByTestId('ondo-leaderboard-position')).toBeNull();
});
});

describe('opt-in CTA', () => {
Expand Down Expand Up @@ -459,6 +513,98 @@ describe('OndoCampaignDetailsView', () => {
});
});

describe('leaderboard position', () => {
it('shows OndoLeaderboardPosition when participant is opted in', () => {
mockUseRewardCampaigns.mockReturnValue({
...hookDefaults,
campaigns: [createTestCampaign()],
});
mockUseGetCampaignParticipantStatus.mockReturnValue({
status: { optedIn: true, participantCount: 1 },
isLoading: false,
hasError: false,
refetch: jest.fn(),
});
const { getByTestId } = render(<OndoCampaignDetailsView />);
expect(getByTestId('ondo-leaderboard-position')).toBeDefined();
});

it('does not show OndoLeaderboardPosition when not opted in and campaign is active', () => {
mockUseRewardCampaigns.mockReturnValue({
...hookDefaults,
campaigns: [createTestCampaign()],
});
const { queryByTestId } = render(<OndoCampaignDetailsView />);
expect(queryByTestId('ondo-leaderboard-position')).toBeNull();
});

it('shows OndoLeaderboardPosition when not opted in but campaign is complete', () => {
const lastMonth = new Date();
lastMonth.setMonth(lastMonth.getMonth() - 1);
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);

mockUseRewardCampaigns.mockReturnValue({
...hookDefaults,
campaigns: [
createTestCampaign({
startDate: lastMonth.toISOString(),
endDate: yesterday.toISOString(),
}),
],
});
const { getByTestId } = render(<OndoCampaignDetailsView />);
expect(getByTestId('ondo-leaderboard-position')).toBeDefined();
});
});

describe('campaign portfolio', () => {
it('renders campaign portfolio when opted in and campaign type is ONDO_HOLDING', () => {
mockUseRewardCampaigns.mockReturnValue({
...hookDefaults,
campaigns: [createTestCampaign({ type: CampaignType.ONDO_HOLDING })],
});
mockUseGetCampaignParticipantStatus.mockReturnValue({
status: { optedIn: true, participantCount: 1 },
isLoading: false,
hasError: false,
refetch: jest.fn(),
});
const { getByTestId } = render(<CampaignDetailsView />);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests reference undefined CampaignDetailsView component

High Severity

The three new "campaign portfolio" tests render <CampaignDetailsView />, but only OndoCampaignDetailsView is imported in this file. CampaignDetailsView is not defined or imported anywhere in the test, so all three tests will fail. The rest of the test file correctly uses <OndoCampaignDetailsView />.

Additional Locations (2)
Fix in Cursor Fix in Web

expect(getByTestId('rewards-campaign-portfolio')).toBeDefined();
});

it('does not render campaign portfolio when not opted in', () => {
mockUseRewardCampaigns.mockReturnValue({
...hookDefaults,
campaigns: [createTestCampaign({ type: CampaignType.ONDO_HOLDING })],
});
mockUseGetCampaignParticipantStatus.mockReturnValue({
status: { optedIn: false, participantCount: 0 },
isLoading: false,
hasError: false,
refetch: jest.fn(),
});
const { queryByTestId } = render(<CampaignDetailsView />);
expect(queryByTestId('rewards-campaign-portfolio')).toBeNull();
});

it('does not render campaign portfolio when campaign type is not ONDO_HOLDING', () => {
mockUseRewardCampaigns.mockReturnValue({
...hookDefaults,
campaigns: [createTestCampaign({ type: 'OTHER_TYPE' as CampaignType })],
});
mockUseGetCampaignParticipantStatus.mockReturnValue({
status: { optedIn: true, participantCount: 1 },
isLoading: false,
hasError: false,
refetch: jest.fn(),
});
const { queryByTestId } = render(<CampaignDetailsView />);
expect(queryByTestId('rewards-campaign-portfolio')).toBeNull();
});
});

describe('navigation', () => {
it('navigates back when the back button is pressed', () => {
mockUseRewardCampaigns.mockReturnValue({
Expand Down
23 changes: 22 additions & 1 deletion app/components/UI/Rewards/Views/OndoCampaignDetailsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import HeaderCompactStandard from '../../../../component-library/components-temp
import ErrorBoundary from '../../../Views/ErrorBoundary';
import CampaignStatus from '../components/Campaigns/CampaignStatus';
import CampaignHowItWorks from '../components/Campaigns/CampaignHowItWorks';
import OndoLeaderboardPosition from '../components/Campaigns/OndoLeaderboardPosition';
import CampaignJoinCTA from '../components/Campaigns/CampaignJoinCTA';
import { getCampaignStatus } from '../components/Campaigns/CampaignTile.utils';
import RewardsCampaignPortfolio from '../components/Campaigns/RewardsCampaignPortfolio';
import RewardsErrorBanner from '../components/RewardsErrorBanner';
import { CampaignType } from '../../../../core/Engine/controllers/rewards-controller/types';
import { useGetCampaignParticipantStatus } from '../hooks/useGetCampaignParticipantStatus';
import { useRewardCampaigns } from '../hooks/useRewardCampaigns';
import { strings } from '../../../../../locales/i18n';
Expand Down Expand Up @@ -49,6 +52,8 @@ const OndoCampaignDetailsView: React.FC = () => {
}
}, [campaign, navigation]);

const isOptedIn = participantStatus?.status?.optedIn === true;

return (
<ErrorBoundary navigation={navigation} view="OndoCampaignDetailsView">
<SafeAreaView
Expand Down Expand Up @@ -106,7 +111,8 @@ const OndoCampaignDetailsView: React.FC = () => {
{campaign && (
<>
<CampaignStatus campaign={campaign} />
{campaign.details?.howItWorks && (

{campaign.details?.howItWorks && !isOptedIn && (
<>
<Box twClassName="border-b border-border-muted" />
<Box twClassName="px-4 py-4">
Expand All @@ -116,6 +122,21 @@ const OndoCampaignDetailsView: React.FC = () => {
</Box>
</>
)}

{/* Position summary - shown when opted in, or when campaign is complete regardless of opt-in */}
{(isOptedIn || getCampaignStatus(campaign) === 'complete') && (
<>
<Box twClassName="border-b border-border-muted" />
<Box twClassName="px-4 py-4">
<OndoLeaderboardPosition campaignId={campaignId} />
</Box>
</>
)}

{isOptedIn &&
campaign.type === CampaignType.ONDO_HOLDING && (
<RewardsCampaignPortfolio campaignId={campaignId} />
)}
</>
)}
</ScrollView>
Expand Down
Loading
Loading