Skip to content

Commit d7b351d

Browse files
committed
Add fallback to selectors and rehydrate actions
1 parent a7d19d3 commit d7b351d

8 files changed

Lines changed: 288 additions & 59 deletions

File tree

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: 32 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 = {
@@ -4638,6 +4659,17 @@ describe('setBenefits', () => {
46384659
expect(state.benefits).toEqual(mockBenefitsPayload.benefits);
46394660
});
46404661

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+
46414673
it('replaces existing benefits with new payload benefits', () => {
46424674
const stateWithBenefits: RewardsState = {
46434675
...initialState,

app/reducers/rewards/index.ts

Lines changed: 5 additions & 4 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,

app/reducers/rewards/selectors.test.ts

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
selectBulkLinkFailedAccounts,
4747
selectBulkLinkWasInterrupted,
4848
selectBulkLinkAccountProgress,
49+
selectBenefits,
4950
selectCampaigns,
5051
selectCampaignsLoading,
5152
selectCampaignsError,
@@ -85,6 +86,7 @@ import {
8586
SeasonWayToEarnDto,
8687
PointsEventDto,
8788
OndoGmActivityEntryDto,
89+
SubscriptionBenefitDto,
8890
} from '../../core/Engine/controllers/rewards-controller/types';
8991
import { RootState } from '..';
9092
import { RewardsState, AccountOptInBannerInfoStatus } from '.';
@@ -550,6 +552,16 @@ describe('Rewards selectors', () => {
550552
expect(result.current).toEqual([]);
551553
});
552554

555+
it('returns empty array when season tiers are undefined', () => {
556+
const mockState = {
557+
rewards: { seasonTiers: undefined },
558+
} as unknown as RootState;
559+
mockedUseSelector.mockImplementation((selector) => selector(mockState));
560+
561+
const { result } = renderHook(() => useSelector(selectSeasonTiers));
562+
expect(result.current).toEqual([]);
563+
});
564+
553565
it('returns season tiers when set', () => {
554566
const mockTiers: SeasonTierDto[] = [
555567
{
@@ -605,6 +617,18 @@ describe('Rewards selectors', () => {
605617
expect(result.current).toEqual([]);
606618
});
607619

620+
it('returns empty array when season activity types are undefined', () => {
621+
const mockState = {
622+
rewards: { seasonActivityTypes: undefined },
623+
} as unknown as RootState;
624+
mockedUseSelector.mockImplementation((selector) => selector(mockState));
625+
626+
const { result } = renderHook(() =>
627+
useSelector(selectSeasonActivityTypes),
628+
);
629+
expect(result.current).toEqual([]);
630+
});
631+
608632
it('returns season activity types when set', () => {
609633
const mockActivityTypes: SeasonActivityTypeDto[] = [
610634
{
@@ -639,6 +663,16 @@ describe('Rewards selectors', () => {
639663
expect(result.current).toEqual([]);
640664
});
641665

666+
it('returns empty array when season ways to earn are undefined', () => {
667+
const mockState = {
668+
rewards: { seasonWaysToEarn: undefined },
669+
} as unknown as RootState;
670+
mockedUseSelector.mockImplementation((selector) => selector(mockState));
671+
672+
const { result } = renderHook(() => useSelector(selectSeasonWaysToEarn));
673+
expect(result.current).toEqual([]);
674+
});
675+
642676
it('returns season ways to earn when set', () => {
643677
const mockWaysToEarn: SeasonWayToEarnDto[] = [
644678
{
@@ -1033,6 +1067,18 @@ describe('Rewards selectors', () => {
10331067
expect(result.current).toHaveLength(0);
10341068
});
10351069

1070+
it('returns empty array when account banner config is undefined', () => {
1071+
const mockState = {
1072+
rewards: { hideCurrentAccountNotOptedInBanner: undefined },
1073+
} as unknown as RootState;
1074+
mockedUseSelector.mockImplementation((selector) => selector(mockState));
1075+
1076+
const { result } = renderHook(() =>
1077+
useSelector(selectHideCurrentAccountNotOptedInBannerArray),
1078+
);
1079+
expect(result.current).toEqual([]);
1080+
});
1081+
10361082
it('returns single account configuration when set', () => {
10371083
const mockAccountConfig: AccountOptInBannerInfoStatus = {
10381084
accountGroupId: 'keyring:wallet1/1',
@@ -3120,6 +3166,37 @@ describe('Rewards selectors', () => {
31203166
showUpcomingDate: false,
31213167
};
31223168

3169+
describe('selectBenefits', () => {
3170+
const mockBenefit: SubscriptionBenefitDto = {
3171+
id: 101,
3172+
longTitle: 'Premium Access',
3173+
shortDescription: 'Get premium perks',
3174+
longDescription: 'Unlock premium partner benefits.',
3175+
thumbnail: 'https://example.com/benefits/premium.png',
3176+
validFrom: '2026-01-01T00:00:00.000Z',
3177+
validTo: '2026-12-31T00:00:00.000Z',
3178+
actionDate: '2026-06-01T00:00:00.000Z',
3179+
url: 'https://example.com/claim',
3180+
chain: 'ethereum',
3181+
type: {
3182+
id: 1,
3183+
name: 'Partner',
3184+
},
3185+
};
3186+
3187+
it('returns empty array when benefits are undefined', () => {
3188+
const state = createMockRootState({
3189+
benefits: undefined as unknown as SubscriptionBenefitDto[],
3190+
});
3191+
expect(selectBenefits(state)).toEqual([]);
3192+
});
3193+
3194+
it('returns benefits when they exist', () => {
3195+
const state = createMockRootState({ benefits: [mockBenefit] });
3196+
expect(selectBenefits(state)).toEqual([mockBenefit]);
3197+
});
3198+
});
3199+
31233200
describe('selectCampaigns', () => {
31243201
it('returns empty array when campaigns is empty', () => {
31253202
const mockState = { rewards: { campaigns: [] } };
@@ -3878,6 +3955,16 @@ describe('Rewards selectors', () => {
38783955
expect(selectDismissedCampaignOutcomeToasts(state)).toEqual({});
38793956
});
38803957

3958+
it('returns empty object when dismissed toasts are undefined', () => {
3959+
const state = createMockRootState({
3960+
dismissedCampaignOutcomeToasts: undefined as unknown as Record<
3961+
string,
3962+
boolean
3963+
>,
3964+
});
3965+
expect(selectDismissedCampaignOutcomeToasts(state)).toEqual({});
3966+
});
3967+
38813968
it('returns the dismissed toasts map', () => {
38823969
const dismissed = {
38833970
'campaign-1:sub-1:winner': true,

app/reducers/rewards/selectors.ts

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import { createSelector } from 'reselect';
2-
import { RootState } from '..';
2+
import type { RootState } from '..';
3+
import { initialState } from '.';
34
import { RewardsTab, OnboardingStep } from './types';
45
import { hasMinimumRequiredVersion } from '../../util/remoteFeatureFlag';
5-
import {
6-
CampaignDto,
7-
SubscriptionBenefitDto,
8-
} from '../../core/Engine/controllers/rewards-controller/types.ts';
96

107
export const selectActiveTab = (state: RootState): RewardsTab =>
118
state.rewards.activeTab;
@@ -52,14 +49,20 @@ export const selectSeasonStartDate = (state: RootState) =>
5249
export const selectSeasonEndDate = (state: RootState) =>
5350
state.rewards.seasonEndDate;
5451

55-
export const selectSeasonTiers = (state: RootState) =>
56-
state.rewards.seasonTiers;
52+
export const selectSeasonTiers = (
53+
state: RootState,
54+
): RootState['rewards']['seasonTiers'] =>
55+
state.rewards.seasonTiers ?? initialState.seasonTiers;
5756

58-
export const selectSeasonActivityTypes = (state: RootState) =>
59-
state.rewards.seasonActivityTypes;
57+
export const selectSeasonActivityTypes = (
58+
state: RootState,
59+
): RootState['rewards']['seasonActivityTypes'] =>
60+
state.rewards.seasonActivityTypes ?? initialState.seasonActivityTypes;
6061

61-
export const selectSeasonWaysToEarn = (state: RootState) =>
62-
state.rewards.seasonWaysToEarn;
62+
export const selectSeasonWaysToEarn = (
63+
state: RootState,
64+
): RootState['rewards']['seasonWaysToEarn'] =>
65+
state.rewards.seasonWaysToEarn ?? initialState.seasonWaysToEarn;
6366

6467
export const selectOnboardingActiveStep = (state: RootState): OnboardingStep =>
6568
state.rewards.onboardingActiveStep;
@@ -93,7 +96,9 @@ export const selectHideUnlinkedAccountsBanner = (state: RootState) =>
9396

9497
export const selectHideCurrentAccountNotOptedInBannerArray = (
9598
state: RootState,
96-
) => state.rewards.hideCurrentAccountNotOptedInBanner;
99+
): RootState['rewards']['hideCurrentAccountNotOptedInBanner'] =>
100+
state.rewards.hideCurrentAccountNotOptedInBanner ??
101+
initialState.hideCurrentAccountNotOptedInBanner;
97102

98103
export const selectActiveBoosts = (state: RootState) =>
99104
state.rewards.activeBoosts;
@@ -155,16 +160,19 @@ export const selectBulkLinkAccountProgress = (state: RootState) => {
155160
};
156161

157162
// Benefits selectors
158-
export const selectBenefits = (state: RootState): SubscriptionBenefitDto[] =>
159-
state.rewards.benefits;
163+
export const selectBenefits = (
164+
state: RootState,
165+
): RootState['rewards']['benefits'] =>
166+
state.rewards.benefits ?? initialState.benefits;
160167

161168
export const selectBenefitsLoading = (state: RootState): boolean =>
162169
state.rewards.benefitsLoading;
163170

164171
// Campaigns selectors
165-
const EMPTY_CAMPAIGNS: CampaignDto[] = [];
166-
export const selectCampaigns = (state: RootState): CampaignDto[] =>
167-
state.rewards.campaigns ?? EMPTY_CAMPAIGNS;
172+
export const selectCampaigns = (
173+
state: RootState,
174+
): RootState['rewards']['campaigns'] =>
175+
state.rewards.campaigns ?? initialState.campaigns;
168176

169177
export const selectCampaignById = (campaignId: string) => (state: RootState) =>
170178
state.rewards.campaigns?.find((c) => c.id === campaignId) ?? null;
@@ -322,8 +330,11 @@ export const selectOndoCampaignDepositsError = (state: RootState) =>
322330
export const selectPendingDeeplink = (state: RootState) =>
323331
state.rewards.pendingDeeplink;
324332

325-
export const selectDismissedCampaignOutcomeToasts = (state: RootState) =>
326-
state.rewards.dismissedCampaignOutcomeToasts;
333+
export const selectDismissedCampaignOutcomeToasts = (
334+
state: RootState,
335+
): RootState['rewards']['dismissedCampaignOutcomeToasts'] =>
336+
state.rewards.dismissedCampaignOutcomeToasts ??
337+
initialState.dismissedCampaignOutcomeToasts;
327338

328339
// Perps Trading Campaign leaderboard selectors
329340
export const selectPerpsTradingCampaignLeaderboard = (state: RootState) =>

0 commit comments

Comments
 (0)