Skip to content

Commit 20f3ae5

Browse files
authored
feat: rewards activity compatible with predict and deposit musd (#22636)
## **Description** This PR makes it so that the activity grid in rewards can identify predict and deposit musd type events. ## **Changelog** CHANGELOG entry: feat rewards activity grid predict and deposit musd ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/RWDS-697 ## **Screenshots/Recordings** ### **After** <img width="469" height="154" alt="image" src="https://github.com/user-attachments/assets/ae81b968-8f99-4392-9704-7e2b20b4df66" /> <img width="544" height="448" alt="image" src="https://github.com/user-attachments/assets/e874d717-ba16-4488-ac87-7c7bf71cfd4c" /> ## **Pre-merge author checklist** - [x] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds Rewards Activity support for PREDICT and MUSD_DEPOSIT events, including UI details, date formatting utilities, types, i18n strings, and comprehensive tests. > > - **Rewards Activity UI**: > - Add `MUSD_DEPOSIT` handling in `ActivityDetailsSheet` via new `MusdDepositEventDetails` component. > - Ensure `PREDICT` events render without description in `ActivityEventRow`. > - **Utils**: > - Update `getEventDetails` to support `PREDICT` and `MUSD_DEPOSIT` (with formatted "For {{date}}" detail and icons). > - Add `formatUTCDate` and `formatRewardsMusdDepositPayloadDate` in `utils/formatUtils`. > - **Types**: > - Introduce `MusdDepositEventPayload` and extend `PointsEventDto` union with `PREDICT` and `MUSD_DEPOSIT`. > - **i18n**: > - Add strings for prediction, mUSD deposit, and deposit-period/date labels in `locales/languages/en.json`. > - **Tests**: > - New/expanded tests for `ActivityEventRow`, `ActivityDetailsSheet`, `MusdDepositEventDetails`, `eventDetailsUtils`, and `formatUtils`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 99d004a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8bb23bf commit 20f3ae5

File tree

11 files changed

+1042
-3
lines changed

11 files changed

+1042
-3
lines changed

app/components/UI/Rewards/components/Tabs/ActivityTab/ActivityEventRow.test.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,33 @@ import { getEventDetails } from '../../../utils/eventDetailsUtils';
88
import { IconName } from '@metamask/design-system-react-native';
99
import TEST_ADDRESS from '../../../../../../constants/address';
1010
import { useActivityDetailsConfirmAction } from '../../../hooks/useActivityDetailsConfirmAction';
11+
import { REWARDS_VIEW_SELECTORS } from '../../../Views/RewardsView.constants';
1112

1213
// Mock the utility functions
1314
jest.mock('../../../utils/formatUtils', () => ({
1415
formatRewardsDate: jest.fn(),
1516
formatNumber: jest
1617
.fn()
1718
.mockImplementation((value) => value?.toString() || '0'),
19+
formatRewardsMusdDepositPayloadDate: jest.fn(
20+
(isoDate: string | undefined) => {
21+
// Mock implementation that matches the real implementation behavior
22+
if (
23+
!isoDate ||
24+
typeof isoDate !== 'string' ||
25+
!/^\d{4}-\d{2}-\d{2}$/.test(isoDate)
26+
) {
27+
return null;
28+
}
29+
const date = new Date(`${isoDate}T00:00:00Z`);
30+
return new Intl.DateTimeFormat('en-US', {
31+
year: 'numeric',
32+
month: 'short',
33+
day: 'numeric',
34+
timeZone: 'UTC',
35+
}).format(date);
36+
},
37+
),
1838
}));
1939

2040
jest.mock('../../../utils/eventDetailsUtils', () => ({
@@ -194,6 +214,34 @@ describe('ActivityEventRow', () => {
194214
...overrides,
195215
} as PointsEventDto;
196216

217+
case 'PREDICT':
218+
return {
219+
id: 'predict-event-1',
220+
timestamp: new Date('2025-09-15T10:30:00.000Z'),
221+
type: 'PREDICT' as const,
222+
value: 20,
223+
bonus: null,
224+
accountAddress: '0x069060A475c76C77427CcC8CbD7eCB0B293f5beD',
225+
payload: null,
226+
updatedAt: new Date('2025-09-15T10:30:00.000Z'),
227+
...overrides,
228+
} as PointsEventDto;
229+
230+
case 'MUSD_DEPOSIT':
231+
return {
232+
id: 'musd-deposit-event-1',
233+
timestamp: new Date('2025-11-11T10:30:00.000Z'),
234+
type: 'MUSD_DEPOSIT' as const,
235+
value: 10,
236+
bonus: null,
237+
accountAddress: '0x069060A475c76C77427CcC8CbD7eCB0B293f5beD',
238+
payload: {
239+
date: '2025-11-11',
240+
},
241+
updatedAt: new Date('2025-11-11T10:30:00.000Z'),
242+
...overrides,
243+
} as PointsEventDto;
244+
197245
default:
198246
throw new Error(`Unsupported event type: ${eventType}`);
199247
}
@@ -516,6 +564,30 @@ describe('ActivityEventRow', () => {
516564
expect(getByText('+50%')).toBeOnTheScreen();
517565
expect(mockGetEventDetails).toHaveBeenCalledWith(event, TEST_ADDRESS);
518566
});
567+
568+
it('renders PREDICT event without description', () => {
569+
// Arrange
570+
const event = createMockEvent({ type: 'PREDICT' });
571+
mockGetEventDetails.mockReturnValue({
572+
title: 'Predict',
573+
details: undefined,
574+
icon: IconName.Speedometer,
575+
});
576+
577+
// Act
578+
const { getByText, getByTestId } = render(
579+
<ActivityEventRow event={event} accountName={TEST_ADDRESS} />,
580+
);
581+
582+
// Assert
583+
expect(getByText('Predict')).toBeOnTheScreen();
584+
expect(getByText('+20')).toBeOnTheScreen();
585+
const detailsElement = getByTestId(
586+
`${REWARDS_VIEW_SELECTORS.ACTIVITY_EVENT_ROW_DETAILS}-${undefined}`,
587+
);
588+
expect(detailsElement.props.children).toBeUndefined();
589+
expect(detailsElement).toHaveTextContent('');
590+
});
519591
});
520592

521593
describe('edge cases', () => {

app/components/UI/Rewards/components/Tabs/ActivityTab/EventDetails/ActivityDetailsSheet.test.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ jest.mock('../../../../../../../../locales/i18n', () => ({
3737
'rewards.events.points_base': 'Base',
3838
'rewards.events.points_boost': 'Boost',
3939
'rewards.events.points_total': 'Total',
40+
'rewards.events.for_deposit_period': 'For deposit period',
4041
};
4142
return t[key] || key;
4243
}),
@@ -46,6 +47,25 @@ jest.mock('../../../../../../../../locales/i18n', () => ({
4647
jest.mock('../../../../utils/formatUtils', () => ({
4748
formatRewardsDate: jest.fn(() => 'Sep 9, 2025'),
4849
formatNumber: jest.fn((n: number) => n.toString()),
50+
formatRewardsMusdDepositPayloadDate: jest.fn(
51+
(isoDate: string | undefined) => {
52+
// Mock implementation that matches the real implementation behavior
53+
if (
54+
!isoDate ||
55+
typeof isoDate !== 'string' ||
56+
!/^\d{4}-\d{2}-\d{2}$/.test(isoDate)
57+
) {
58+
return null;
59+
}
60+
const date = new Date(`${isoDate}T00:00:00Z`);
61+
return new Intl.DateTimeFormat('en-US', {
62+
year: 'numeric',
63+
month: 'short',
64+
day: 'numeric',
65+
timeZone: 'UTC',
66+
}).format(date);
67+
},
68+
),
4969
}));
5070

5171
// Mock eventDetailsUtils
@@ -140,6 +160,29 @@ describe('ActivityDetailsSheet', () => {
140160
expect(screen.getByText('43.25 USDC')).toBeTruthy();
141161
});
142162

163+
it('renders MusdDepositEventDetails for MUSD_DEPOSIT event type', () => {
164+
const musdDepositEvent: Extract<
165+
PointsEventDto,
166+
{ type: 'MUSD_DEPOSIT' }
167+
> = {
168+
...baseEvent,
169+
type: 'MUSD_DEPOSIT',
170+
payload: {
171+
date: '2025-11-11',
172+
},
173+
};
174+
175+
render(
176+
<ActivityDetailsSheet event={musdDepositEvent} accountName="Primary" />,
177+
);
178+
179+
// Verify GenericEventDetails content is rendered (base component)
180+
expect(screen.getByText('Details')).toBeTruthy();
181+
// Verify MusdDepositEventDetails specific content
182+
expect(screen.getByText('For deposit period')).toBeTruthy();
183+
expect(screen.getByText('Nov 11, 2025')).toBeTruthy();
184+
});
185+
143186
it('renders GenericEventDetails for other event types', () => {
144187
const genericEvent: PointsEventDto = {
145188
...baseEvent,

app/components/UI/Rewards/components/Tabs/ActivityTab/EventDetails/ActivityDetailsSheet.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { getEventDetails } from '../../../../utils/eventDetailsUtils';
88
import { GenericEventDetails } from './GenericEventDetails';
99
import { SwapEventDetails } from './SwapEventDetails';
1010
import { CardEventDetails } from './CardEventDetails';
11+
import { MusdDepositEventDetails } from './MusdDepositEventDetails';
1112
import { PointsEventDto } from '../../../../../../../core/Engine/controllers/rewards-controller/types';
1213

1314
interface ActivityDetailsSheetProps {
@@ -26,6 +27,10 @@ export const ActivityDetailsSheet: React.FC<ActivityDetailsSheetProps> = ({
2627
return <SwapEventDetails event={event} accountName={accountName} />;
2728
case 'CARD':
2829
return <CardEventDetails event={event} accountName={accountName} />;
30+
case 'MUSD_DEPOSIT':
31+
return (
32+
<MusdDepositEventDetails event={event} accountName={accountName} />
33+
);
2934
default:
3035
return <GenericEventDetails event={event} accountName={accountName} />;
3136
}
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import React from 'react';
2+
import { render, screen } from '@testing-library/react-native';
3+
import { useSelector } from 'react-redux';
4+
import { MusdDepositEventDetails } from './MusdDepositEventDetails';
5+
import { AvatarAccountType } from '../../../../../../../component-library/components/Avatars/Avatar';
6+
import TEST_ADDRESS from '../../../../../../../constants/address';
7+
import { PointsEventDto } from '../../../../../../../core/Engine/controllers/rewards-controller/types';
8+
import { formatRewardsMusdDepositPayloadDate } from '../../../../utils/formatUtils';
9+
10+
// Mock react-redux
11+
jest.mock('react-redux', () => ({
12+
useSelector: jest.fn(),
13+
}));
14+
const mockUseSelector = useSelector as jest.MockedFunction<typeof useSelector>;
15+
16+
// Mock i18n strings used by GenericEventDetails and MusdDepositEventDetails
17+
jest.mock('../../../../../../../../locales/i18n', () => ({
18+
strings: jest.fn((key: string) => {
19+
const t: Record<string, string> = {
20+
'rewards.events.details': 'Details',
21+
'rewards.events.date': 'Date',
22+
'rewards.events.account': 'Account',
23+
'rewards.events.points': 'Points',
24+
'rewards.events.points_base': 'Base',
25+
'rewards.events.points_boost': 'Boost',
26+
'rewards.events.points_total': 'Total',
27+
'rewards.events.for_deposit_period': 'For deposit period',
28+
};
29+
return t[key] || key;
30+
}),
31+
}));
32+
33+
// Mock format utils used by GenericEventDetails and MusdDepositEventDetails
34+
jest.mock('../../../../utils/formatUtils', () => ({
35+
formatRewardsDate: jest.fn(() => 'Sep 9, 2025'),
36+
formatNumber: jest.fn((n: number) => n.toString()),
37+
formatRewardsMusdDepositPayloadDate: jest.fn(
38+
(isoDate: string | undefined) => {
39+
// Mock implementation that matches the real implementation behavior
40+
if (
41+
!isoDate ||
42+
typeof isoDate !== 'string' ||
43+
!/^\d{4}-\d{2}-\d{2}$/.test(isoDate)
44+
) {
45+
return null;
46+
}
47+
const date = new Date(`${isoDate}T00:00:00Z`);
48+
return new Intl.DateTimeFormat('en-US', {
49+
year: 'numeric',
50+
month: 'short',
51+
day: 'numeric',
52+
timeZone: 'UTC',
53+
}).format(date);
54+
},
55+
),
56+
}));
57+
58+
// Mock SVG used in the component to avoid native rendering issues
59+
jest.mock(
60+
'../../../../../../../images/rewards/metamask-rewards-points.svg',
61+
() => 'SvgMock',
62+
);
63+
64+
describe('MusdDepositEventDetails', () => {
65+
beforeEach(() => {
66+
jest.clearAllMocks();
67+
mockUseSelector.mockReturnValue(AvatarAccountType.JazzIcon);
68+
});
69+
70+
const baseMusdDepositEvent: Extract<
71+
PointsEventDto,
72+
{ type: 'MUSD_DEPOSIT' }
73+
> = {
74+
id: 'musd-deposit-1',
75+
timestamp: new Date('2025-09-09T09:09:33.000Z'),
76+
type: 'MUSD_DEPOSIT',
77+
value: 100,
78+
bonus: null,
79+
accountAddress: TEST_ADDRESS,
80+
updatedAt: new Date('2025-09-09T09:09:33.000Z'),
81+
payload: {
82+
date: '2025-11-11',
83+
},
84+
};
85+
86+
it('renders deposit period row when payload.date exists', () => {
87+
render(
88+
<MusdDepositEventDetails
89+
event={baseMusdDepositEvent}
90+
accountName="Primary"
91+
/>,
92+
);
93+
94+
// Verify GenericEventDetails header is rendered
95+
expect(screen.getByText('Details')).toBeTruthy();
96+
97+
// Verify deposit period label and formatted date are displayed
98+
expect(screen.getByText('For deposit period')).toBeTruthy();
99+
expect(screen.getByText('Nov 11, 2025')).toBeTruthy();
100+
});
101+
102+
it('does not render deposit period row when payload is null', () => {
103+
const eventWithoutPayload: Extract<
104+
PointsEventDto,
105+
{ type: 'MUSD_DEPOSIT' }
106+
> = {
107+
...baseMusdDepositEvent,
108+
payload: null,
109+
};
110+
111+
render(
112+
<MusdDepositEventDetails
113+
event={eventWithoutPayload}
114+
accountName="Primary"
115+
/>,
116+
);
117+
118+
// Verify GenericEventDetails content is rendered
119+
expect(screen.getByText('Details')).toBeTruthy();
120+
121+
// Verify deposit period row is not displayed when payload is null
122+
expect(screen.queryByText('For deposit period')).toBeNull();
123+
});
124+
125+
it('renders base points and total correctly', () => {
126+
const eventWithBonus: Extract<PointsEventDto, { type: 'MUSD_DEPOSIT' }> = {
127+
...baseMusdDepositEvent,
128+
value: 500,
129+
bonus: { bonusPoints: 100, bips: 0, bonuses: [] },
130+
};
131+
132+
render(
133+
<MusdDepositEventDetails event={eventWithBonus} accountName="Primary" />,
134+
);
135+
136+
// Verify points section from GenericEventDetails
137+
expect(screen.getByText('Points')).toBeTruthy();
138+
139+
// Base = value - bonus = 500 - 100 = 400
140+
expect(screen.getByText('Base')).toBeTruthy();
141+
expect(screen.getByText('400')).toBeTruthy();
142+
143+
// Boost
144+
expect(screen.getByText('Boost')).toBeTruthy();
145+
expect(screen.getByText('100')).toBeTruthy();
146+
147+
// Total
148+
expect(screen.getByText('Total')).toBeTruthy();
149+
expect(screen.getByText('500')).toBeTruthy();
150+
});
151+
152+
it('displays account name when provided', () => {
153+
render(
154+
<MusdDepositEventDetails
155+
event={baseMusdDepositEvent}
156+
accountName="Deposit Account"
157+
/>,
158+
);
159+
160+
// Verify account name is displayed
161+
expect(screen.getByText('Account')).toBeTruthy();
162+
expect(screen.getByText('Deposit Account')).toBeTruthy();
163+
});
164+
165+
it('calls formatRewardsMusdDepositPayloadDate with correct ISO date string', () => {
166+
const mockFormatRewardsMusdDepositPayloadDate =
167+
formatRewardsMusdDepositPayloadDate as jest.MockedFunction<
168+
typeof formatRewardsMusdDepositPayloadDate
169+
>;
170+
mockFormatRewardsMusdDepositPayloadDate.mockReturnValue('Dec 25, 2025');
171+
172+
const eventWithDate: Extract<PointsEventDto, { type: 'MUSD_DEPOSIT' }> = {
173+
...baseMusdDepositEvent,
174+
payload: {
175+
date: '2025-12-25',
176+
},
177+
};
178+
179+
render(
180+
<MusdDepositEventDetails event={eventWithDate} accountName="Primary" />,
181+
);
182+
183+
// Verify the formatter was called with the correct ISO date string
184+
expect(mockFormatRewardsMusdDepositPayloadDate).toHaveBeenCalledWith(
185+
'2025-12-25',
186+
);
187+
188+
// Verify the formatted date is displayed
189+
expect(screen.getByText('Dec 25, 2025')).toBeTruthy();
190+
});
191+
});

0 commit comments

Comments
 (0)