Skip to content

Commit 1b37ec5

Browse files
VGR-GITclaude
andcommitted
feat(rewards): implement campaign end winner UX per RWDS-1168
- Add useOndoCampaignWinnerCode hook (subscription-scoped, returns claim code + loading state) replacing direct useSelector calls in the winning view - Remove misleading prizeDisplay (currentUsdValue is portfolio value, not a prize amount); winning view now shows: You won → rank → rate of return - Fix mailto subject to include the claim code: "Ondo campaign prize claim - {code}" - useMaybeShowCampaignEndToast: winner path no longer dispatches markCampaignEndToastShown so the campaignWon toast re-appears on every app open (session-only dismissal); loser path unchanged (persisted) - OndoCampaignDetailsView: add useFocusEffect auto-navigation to OndoCampaignWinningView for winners on every campaign page session open - Fix test mocks for OndoCampaignWinningView and useMaybeShowCampaignEndToast to break transitive Engine import chains that prevented tests from running Co-authored-by: VGR-GIT <vangulckrik@gmail.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 675efed commit 1b37ec5

18 files changed

Lines changed: 749 additions & 173 deletions

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

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ import React, {
55
useRef,
66
useState,
77
} from 'react';
8+
89
import { Pressable, ScrollView } from 'react-native';
910
import { useSelector } from 'react-redux';
1011
import { selectReferralCode } from '../../../../reducers/rewards/selectors';
11-
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
12+
import {
13+
useFocusEffect,
14+
useNavigation,
15+
useRoute,
16+
RouteProp,
17+
} from '@react-navigation/native';
1218
import {
1319
Box,
1420
BoxAlignItems,
@@ -57,6 +63,7 @@ import {
5763
isCampaignIneligible,
5864
} from '../utils/ondoCampaignConstants';
5965
import useTrackRewardsPageView from '../hooks/useTrackRewardsPageView';
66+
import { isOndoCampaignWinner } from '../hooks/useMaybeShowCampaignEndToast';
6067
import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics';
6168
import { MetaMetricsEvents } from '../../../../core/Analytics';
6269

@@ -168,6 +175,42 @@ const OndoCampaignDetailsView: React.FC = () => {
168175
isOptedIn && hasPositions ? effectiveCampaignId || undefined : undefined,
169176
);
170177

178+
const isWinner = useMemo(
179+
() => isOndoCampaignWinner(leaderboardPosition),
180+
[leaderboardPosition],
181+
);
182+
183+
const navigateToWinningView = useCallback(() => {
184+
navigation.navigate(Routes.REWARDS_ONDO_CAMPAIGN_WINNING_VIEW, {
185+
campaignId: effectiveCampaignId,
186+
campaignName: campaign?.name ?? '',
187+
});
188+
}, [navigation, effectiveCampaignId, campaign]);
189+
190+
const hasPresentedWinningViewRef = useRef(false);
191+
192+
useEffect(() => {
193+
hasPresentedWinningViewRef.current = false;
194+
}, [effectiveCampaignId]);
195+
196+
useFocusEffect(
197+
useCallback(() => {
198+
if (
199+
!hasPresentedWinningViewRef.current &&
200+
campaign &&
201+
getCampaignStatus(campaign) === 'complete' &&
202+
isWinner &&
203+
effectiveCampaignId
204+
) {
205+
hasPresentedWinningViewRef.current = true;
206+
navigation.navigate(Routes.REWARDS_ONDO_CAMPAIGN_WINNING_VIEW, {
207+
campaignId: effectiveCampaignId,
208+
campaignName: campaign.name ?? '',
209+
});
210+
}
211+
}, [campaign, isWinner, effectiveCampaignId, navigation]),
212+
);
213+
171214
const {
172215
leaderboard,
173216
selectedTier,
@@ -321,7 +364,10 @@ const OndoCampaignDetailsView: React.FC = () => {
321364
onPress={() =>
322365
navigation.navigate(
323366
Routes.REWARDS_ONDO_CAMPAIGN_STATS,
324-
{ campaignId: effectiveCampaignId },
367+
{
368+
campaignId: effectiveCampaignId,
369+
campaignName: campaign?.name ?? '',
370+
},
325371
)
326372
}
327373
>
@@ -355,6 +401,9 @@ const OndoCampaignDetailsView: React.FC = () => {
355401
}}
356402
tierMinDeposit={tierMinDeposit}
357403
isIneligible={notEligibleForCampaign}
404+
isWinner={isWinner}
405+
campaignName={campaign?.name ?? ''}
406+
onWinnerBannerPress={navigateToWinningView}
358407
/>
359408
</Box>
360409
</>

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

Lines changed: 46 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo } from 'react';
1+
import React, { useCallback, useMemo } from 'react';
22
import { ScrollView } from 'react-native';
33
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
44
import { useSelector } from 'react-redux';
@@ -14,6 +14,7 @@ import {
1414
TextColor,
1515
TextVariant,
1616
} from '@metamask/design-system-react-native';
17+
import OndoWinnerBanner from '../components/Campaigns/OndoWinnerBanner';
1718
import { getCampaignMechanicsButtonProps } from '../utils/campaignHeaderUtils';
1819
import { useTailwind } from '@metamask/design-system-twrnc-preset';
1920
import { SafeAreaView } from 'react-native-safe-area-context';
@@ -42,11 +43,12 @@ import { getCampaignStatus } from '../components/Campaigns/CampaignTile.utils';
4243
import Routes from '../../../../constants/navigation/Routes';
4344
import useTrackRewardsPageView from '../hooks/useTrackRewardsPageView';
4445
import { selectCampaignById } from '../../../../reducers/rewards/selectors';
46+
import { isOndoCampaignWinner } from '../hooks/useMaybeShowCampaignEndToast';
4547

4648
// ParamListBase requires an index signature, which interfaces don't support
4749
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
4850
type OndoCampaignStatsRouteParams = {
49-
OndoCampaignStats: { campaignId: string };
51+
OndoCampaignStats: { campaignId: string; campaignName?: string };
5052
};
5153

5254
export const ONDO_CAMPAIGN_STATS_VIEW_TEST_IDS = {
@@ -66,7 +68,7 @@ const OndoCampaignStatsView: React.FC = () => {
6668
const navigation = useNavigation();
6769
const route =
6870
useRoute<RouteProp<OndoCampaignStatsRouteParams, 'OndoCampaignStats'>>();
69-
const { campaignId } = route.params;
71+
const { campaignId, campaignName: routeCampaignName } = route.params;
7072

7173
const selectCampaign = useMemo(
7274
() => selectCampaignById(campaignId),
@@ -182,6 +184,15 @@ const OndoCampaignStatsView: React.FC = () => {
182184
daysRemaining > 0 &&
183185
tierMinDeposit != null;
184186

187+
const isWinner = isOndoCampaignWinner(leaderboardPosition);
188+
189+
const navigateToWinningView = useCallback(() => {
190+
navigation.navigate(Routes.REWARDS_ONDO_CAMPAIGN_WINNING_VIEW, {
191+
campaignId,
192+
campaignName: campaign?.name ?? '',
193+
});
194+
}, [navigation, campaignId, campaign]);
195+
185196
return (
186197
<ErrorBoundary navigation={navigation} view="OndoCampaignStatsView">
187198
<SafeAreaView
@@ -285,8 +296,16 @@ const OndoCampaignStatsView: React.FC = () => {
285296
</Box>
286297
)}
287298

299+
{/* ── Winning banner ── */}
300+
{isWinner && (
301+
<OndoWinnerBanner
302+
campaignName={campaign?.name ?? routeCampaignName ?? ''}
303+
onPress={navigateToWinningView}
304+
/>
305+
)}
306+
288307
{/* ── Qualify for rank card (static) ── */}
289-
{showQualifyCard && (
308+
{!isWinner && showQualifyCard && (
290309
<Box twClassName="bg-muted rounded-xl p-4 mt-2 gap-2">
291310
<Text
292311
variant={TextVariant.BodyMd}
@@ -312,28 +331,31 @@ const OndoCampaignStatsView: React.FC = () => {
312331
)}
313332

314333
{/* ── You're qualified card ── */}
315-
{!isIneligible && isQualified && tierMinDeposit != null && (
316-
<Box twClassName="bg-muted rounded-xl p-4 mt-2 gap-2">
317-
<Text
318-
variant={TextVariant.BodyMd}
319-
fontWeight={FontWeight.Medium}
320-
>
321-
{strings('rewards.ondo_campaign_stats.qualified_title')}
322-
</Text>
323-
<Text
324-
variant={TextVariant.BodySm}
325-
color={TextColor.TextAlternative}
326-
>
327-
{strings(
328-
'rewards.ondo_campaign_stats.qualified_description',
329-
{ minNetDeposit: formatUsd(tierMinDeposit) },
330-
)}
331-
</Text>
332-
</Box>
333-
)}
334+
{!isWinner &&
335+
!isIneligible &&
336+
isQualified &&
337+
tierMinDeposit != null && (
338+
<Box twClassName="bg-muted rounded-xl p-4 mt-2 gap-2">
339+
<Text
340+
variant={TextVariant.BodyMd}
341+
fontWeight={FontWeight.Medium}
342+
>
343+
{strings('rewards.ondo_campaign_stats.qualified_title')}
344+
</Text>
345+
<Text
346+
variant={TextVariant.BodySm}
347+
color={TextColor.TextAlternative}
348+
>
349+
{strings(
350+
'rewards.ondo_campaign_stats.qualified_description',
351+
{ minNetDeposit: formatUsd(tierMinDeposit) },
352+
)}
353+
</Text>
354+
</Box>
355+
)}
334356

335357
{/* ── Not eligible banner ── */}
336-
{isIneligible && (
358+
{!isWinner && isIneligible && (
337359
<Box
338360
twClassName="bg-muted rounded-xl p-4 mt-2 gap-2"
339361
testID={CAMPAIGN_STATS_SUMMARY_TEST_IDS.NOT_ELIGIBLE_BANNER}

0 commit comments

Comments
 (0)