Skip to content

Commit 55cdda5

Browse files
Merge branch 'main' into codex/headless-plan-pr-links
2 parents 56e50a7 + 8208502 commit 55cdda5

11 files changed

Lines changed: 371 additions & 73 deletions

File tree

app/components/UI/Perps/routes/index.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ import { RouteProp, useNavigation, useRoute } from '@react-navigation/native';
4747
/* eslint-disable-next-line */
4848
import { NavigationContext } from '@react-navigation/core';
4949
import { CONFIRMATION_HEADER_CONFIG } from '../constants/perpsConfig';
50-
import { clearStackNavigatorOptions } from '../../../../constants/navigation/clearStackNavigatorOptions';
50+
import {
51+
clearNativeStackNavigatorOptions,
52+
clearStackNavigatorOptions,
53+
transparentModalScreenOptions,
54+
} from '../../../../constants/navigation/clearStackNavigatorOptions';
5155

5256
const Stack = createNativeStackNavigator<PerpsNavigationParamList>();
5357
const ModalStack = createStackNavigator();
@@ -73,7 +77,7 @@ function getRedesignedConfirmationsHeaderOptions({
7377
title: '',
7478
headerBackVisible: false,
7579
contentStyle: { backgroundColor: 'transparent' },
76-
presentation: 'transparentModal',
80+
...transparentModalScreenOptions,
7781
};
7882
}
7983

@@ -353,9 +357,9 @@ const PerpsScreenStack = () => {
353357
name={Routes.PERPS.TPSL}
354358
component={PerpsTPSLView}
355359
options={{
360+
...transparentModalScreenOptions,
356361
title: strings('perps.tpsl.title'),
357362
headerShown: false,
358-
presentation: 'transparentModal',
359363
}}
360364
/>
361365

@@ -411,12 +415,8 @@ const PerpsScreenStack = () => {
411415
name={Routes.PERPS.MODALS.CLOSE_POSITION_MODALS}
412416
component={PerpsClosePositionBottomSheetStack}
413417
options={{
414-
headerShown: false,
415-
contentStyle: {
416-
backgroundColor: 'transparent',
417-
},
418-
animation: 'none',
419-
presentation: 'transparentModal',
418+
...clearNativeStackNavigatorOptions,
419+
...transparentModalScreenOptions,
420420
}}
421421
/>
422422

@@ -425,12 +425,8 @@ const PerpsScreenStack = () => {
425425
name={Routes.PERPS.MODALS.ROOT}
426426
component={PerpsModalStack}
427427
options={{
428-
headerShown: false,
429-
contentStyle: {
430-
backgroundColor: 'transparent',
431-
},
432-
animation: 'none',
433-
presentation: 'transparentModal',
428+
...clearNativeStackNavigatorOptions,
429+
...transparentModalScreenOptions,
434430
}}
435431
/>
436432

@@ -442,7 +438,7 @@ const PerpsScreenStack = () => {
442438
component={PayWithModal}
443439
options={{
444440
headerShown: false,
445-
presentation: 'transparentModal',
441+
...transparentModalScreenOptions,
446442
}}
447443
/>
448444

app/components/UI/Rewards/hooks/useCampaignOutcomeToast.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,13 @@ function setupDefaults({
146146
subscriptionId = SUBSCRIPTION_ID,
147147
outcome = null,
148148
}: {
149-
campaigns?: ReturnType<typeof makeCompletedCampaign>[];
149+
campaigns?: ReturnType<typeof makeCompletedCampaign>[] | undefined;
150150
dismissed?: Record<string, boolean>;
151151
subscriptionId?: string | null;
152152
outcome?: BaseCampaignParticipantOutcomeDto | null;
153153
} = {}) {
154154
mockUseSelector.mockImplementation((selector) => {
155-
if (selector === selectCampaigns) return campaigns;
155+
if (selector === selectCampaigns) return campaigns ?? [];
156156
if (selector === selectDismissedCampaignOutcomeToasts) return dismissed;
157157
if (selector === selectRewardsSubscriptionId) return subscriptionId;
158158
return undefined;
@@ -193,6 +193,14 @@ describe('useCampaignOutcomeToast', () => {
193193
expect(mockShowToast).not.toHaveBeenCalled();
194194
});
195195

196+
it('campaigns are missing from persisted state', () => {
197+
setupDefaults({ campaigns: undefined });
198+
expect(() =>
199+
renderHook(() => useCampaignOutcomeToast(mockConfig)),
200+
).not.toThrow();
201+
expect(mockShowToast).not.toHaveBeenCalled();
202+
});
203+
196204
it('subscriptionId is missing', () => {
197205
setupDefaults({
198206
subscriptionId: null,

app/constants/navigation/clearStackNavigatorOptions.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { NativeStackNavigationOptions } from '@react-navigation/native-stack';
12
import type { StackNavigationOptions } from '@react-navigation/stack';
23

34
/** Transparent stack with no transition animation; used for modal-style flows. */
@@ -22,3 +23,30 @@ export const clearStackNavigatorOptionsWithTransitionAnimation: StackNavigationO
2223
}),
2324
animationEnabled: false,
2425
};
26+
27+
/**
28+
* Native-stack counterpart to {@link clearStackNavigatorOptions}.
29+
* Use with `createNativeStackNavigator` only (`contentStyle` / `animation`, not `cardStyle` / `animationEnabled`).
30+
*
31+
* Includes `animation: 'none'` — omit this preset on screens where you want the default push/modal animation.
32+
*/
33+
export const clearNativeStackNavigatorOptions: NativeStackNavigationOptions = {
34+
headerShown: false,
35+
contentStyle: {
36+
backgroundColor: 'transparent',
37+
},
38+
animation: 'none',
39+
};
40+
41+
/**
42+
* Per-screen options for overlay-style screens on native stack.
43+
* Replaces the JS-stack `cardStyleInterpolator` trick (overlay opacity 0) — native stack keeps the
44+
* presenting screen mounted and does not dim it when `presentation: 'transparentModal'` is used.
45+
*
46+
* Often spread **after** {@link clearNativeStackNavigatorOptions} for fully static overlays.
47+
* Skip `clearNativeStackNavigatorOptions` when this screen should keep the default stack animation
48+
* (it sets `animation: 'none'`).
49+
*/
50+
export const transparentModalScreenOptions: NativeStackNavigationOptions = {
51+
presentation: 'transparentModal',
52+
};

app/core/Engine/controllers/rewards-controller/RewardsController.ts

Lines changed: 5 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ import {
5252
BASE32_REGEX,
5353
CampaignType,
5454
} from './types';
55+
import {
56+
defaultRewardsControllerState,
57+
getRewardsControllerDefaultState,
58+
} from './defaultState';
5559
import type { RewardsControllerMessenger } from '../../messengers/rewards-controller-messenger';
5660
import {
5761
storeSubscriptionToken,
@@ -308,36 +312,7 @@ const metadata: StateMetadata<RewardsControllerState> = {
308312
},
309313
};
310314

311-
/**
312-
* Get the default state for the RewardsController
313-
*/
314-
export const getRewardsControllerDefaultState = (): RewardsControllerState => ({
315-
activeAccount: null,
316-
accounts: {},
317-
subscriptions: {},
318-
subscriptionBenefits: {},
319-
seasons: {},
320-
subscriptionReferralDetails: {},
321-
seasonStatuses: {},
322-
activeBoosts: {},
323-
unlockedRewards: {},
324-
pointsEvents: {},
325-
offDeviceSubscriptionAccounts: {},
326-
campaigns: {},
327-
campaignParticipantStatus: {},
328-
ondoCampaignLeaderboard: {},
329-
ondoCampaignLeaderboardPositions: {},
330-
ondoCampaignPortfolio: {},
331-
ondoCampaignActivity: {},
332-
ondoCampaignDeposits: {},
333-
perpsTradingCampaignLeaderboard: {},
334-
perpsTradingCampaignLeaderboardPositions: {},
335-
perpsTradingCampaignVolume: {},
336-
pointsEstimateHistory: [],
337-
rewardsEnvUrl: null,
338-
});
339-
340-
export const defaultRewardsControllerState = getRewardsControllerDefaultState();
315+
export { defaultRewardsControllerState, getRewardsControllerDefaultState };
341316

342317
type CacheReader<T> = (
343318
key: string,
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { RewardsControllerState } from './types';
2+
3+
/**
4+
* Get the default state for the RewardsController.
5+
*/
6+
export const getRewardsControllerDefaultState = (): RewardsControllerState => ({
7+
activeAccount: null,
8+
accounts: {},
9+
subscriptions: {},
10+
subscriptionBenefits: {},
11+
seasons: {},
12+
subscriptionReferralDetails: {},
13+
seasonStatuses: {},
14+
activeBoosts: {},
15+
unlockedRewards: {},
16+
pointsEvents: {},
17+
offDeviceSubscriptionAccounts: {},
18+
campaigns: {},
19+
campaignParticipantStatus: {},
20+
ondoCampaignLeaderboard: {},
21+
ondoCampaignLeaderboardPositions: {},
22+
ondoCampaignPortfolio: {},
23+
ondoCampaignActivity: {},
24+
ondoCampaignDeposits: {},
25+
perpsTradingCampaignLeaderboard: {},
26+
perpsTradingCampaignLeaderboardPositions: {},
27+
perpsTradingCampaignVolume: {},
28+
pointsEstimateHistory: [],
29+
rewardsEnvUrl: null,
30+
});
31+
32+
export const defaultRewardsControllerState = getRewardsControllerDefaultState();

app/reducers/rewards/index.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2328,6 +2328,27 @@ describe('rewardsReducer', () => {
23282328
);
23292329
});
23302330

2331+
it('should default persisted season arrays to empty arrays when absent', () => {
2332+
const persistedRewardsStateWithoutFields = {
2333+
...initialState,
2334+
seasonTiers: undefined,
2335+
seasonActivityTypes: undefined,
2336+
seasonWaysToEarn: undefined,
2337+
} as unknown as RewardsState;
2338+
const rehydrateAction = {
2339+
type: 'persist/REHYDRATE',
2340+
payload: {
2341+
rewards: persistedRewardsStateWithoutFields,
2342+
},
2343+
};
2344+
2345+
const state = rewardsReducer(initialState, rehydrateAction);
2346+
2347+
expect(state.seasonTiers).toEqual([]);
2348+
expect(state.seasonActivityTypes).toEqual([]);
2349+
expect(state.seasonWaysToEarn).toEqual([]);
2350+
});
2351+
23312352
it('should preserve all persisted UI state fields', () => {
23322353
// Arrange
23332354
const persistedRewardsState: RewardsState = {
@@ -2681,6 +2702,21 @@ describe('rewardsReducer', () => {
26812702

26822703
expect(state.ondoCampaignActivity).toEqual({});
26832704
});
2705+
2706+
it('should default campaigns to [] when absent from persisted state (upgrade path)', () => {
2707+
const persistedRewardsStateWithoutField = {
2708+
...initialState,
2709+
campaigns: undefined,
2710+
} as unknown as RewardsState;
2711+
const rehydrateAction = {
2712+
type: 'persist/REHYDRATE',
2713+
payload: { rewards: persistedRewardsStateWithoutField },
2714+
};
2715+
2716+
const state = rewardsReducer(initialState, rehydrateAction);
2717+
2718+
expect(state.campaigns).toEqual([]);
2719+
});
26842720
});
26852721

26862722
describe('unknown actions', () => {
@@ -4623,6 +4659,17 @@ describe('setBenefits', () => {
46234659
expect(state.benefits).toEqual(mockBenefitsPayload.benefits);
46244660
});
46254661

4662+
it('sets benefits to empty array when payload benefits are missing', () => {
4663+
const action = setBenefits({
4664+
...mockBenefitsPayload,
4665+
benefits: undefined,
4666+
} as unknown as typeof mockBenefitsPayload);
4667+
4668+
const state = rewardsReducer(initialState, action);
4669+
4670+
expect(state.benefits).toEqual([]);
4671+
});
4672+
46264673
it('replaces existing benefits with new payload benefits', () => {
46274674
const stateWithBenefits: RewardsState = {
46284675
...initialState,

app/reducers/rewards/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -687,7 +687,7 @@ const rewardsSlice = createSlice({
687687
},
688688

689689
setBenefits: (state, action: PayloadAction<SubscriptionBenefitsState>) => {
690-
state.benefits = action.payload.benefits;
690+
state.benefits = action.payload.benefits ?? [];
691691
},
692692

693693
setBenefitsLoading: (state, action: PayloadAction<boolean>) => {
@@ -897,9 +897,10 @@ const rewardsSlice = createSlice({
897897
seasonName: action.payload.rewards.seasonName,
898898
seasonStartDate: action.payload.rewards.seasonStartDate,
899899
seasonEndDate: action.payload.rewards.seasonEndDate,
900-
seasonTiers: action.payload.rewards.seasonTiers,
901-
seasonActivityTypes: action.payload.rewards.seasonActivityTypes,
902-
seasonWaysToEarn: action.payload.rewards.seasonWaysToEarn,
900+
seasonTiers: action.payload.rewards.seasonTiers ?? [],
901+
seasonActivityTypes:
902+
action.payload.rewards.seasonActivityTypes ?? [],
903+
seasonWaysToEarn: action.payload.rewards.seasonWaysToEarn ?? [],
903904
referralCode: action.payload.rewards.referralCode,
904905
refereeCount: action.payload.rewards.refereeCount,
905906
currentTier: action.payload.rewards.currentTier,
@@ -912,7 +913,7 @@ const rewardsSlice = createSlice({
912913
activeBoosts: action.payload.rewards.activeBoosts,
913914
pointsEvents: action.payload.rewards.pointsEvents,
914915
unlockedRewards: action.payload.rewards.unlockedRewards,
915-
campaigns: action.payload.rewards.campaigns,
916+
campaigns: action.payload.rewards.campaigns ?? [],
916917
campaignParticipantStatuses:
917918
action.payload.rewards.campaignParticipantStatuses ?? {},
918919
ondoCampaignLeaderboardPositions:

0 commit comments

Comments
 (0)