Skip to content

Commit 4e8a2c5

Browse files
authored
chore: update animations for perps (#21241)
## **Description** Consolidating the rewards animations to use this [version of the animation](#20664). Previously we had slightly different implementations in 3 places of the app as well as outdated animations for the Rewards interaction. There will be a follow up PR to remove old hooks/components related to rewards animations ## **Changelog** CHANGELOG entry: Updated rewards animation for Perps ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TMCU-62 ## **Manual testing steps** ```gherkin Feature: my feature name Scenario: user [verb for user action] Given [describe expected initial app state] When user [verb for user action] Then [describe expected outcome] ``` ## **Screenshots/Recordings** `~` ### **Before** https://github.com/user-attachments/assets/85fbfdef-9a05-41bf-88b2-54b6b8efb818 https://github.com/user-attachments/assets/b5cce02e-f850-480c-92da-7557333ef67f https://github.com/user-attachments/assets/861bc4ac-24f2-42e5-bd3d-b897b92a5cfb https://github.com/user-attachments/assets/ac61d439-6ee2-4252-9191-cafe942db10f ### **After** https://github.com/user-attachments/assets/5d2c8d2e-3b8a-432a-a370-c0dc9c9859c3 https://github.com/user-attachments/assets/c6b67945-b5d8-412b-b753-a77aaa9ce917 https://github.com/user-attachments/assets/10433a6a-fda8-4ccc-b711-1d6c876a911d https://github.com/user-attachments/assets/9c941de3-1b37-41de-9255-1484a787d230 ## **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] > Replaces RewardPointsDisplay with the new RewardsAnimations in Perps Order and Close Position views, adds tooltip/error handling, and updates tests accordingly. > > - **Perps UI**: > - **Rewards Row**: Replace `RewardPointsDisplay` with `RewardsAnimations` in `PerpsOrderView` and `PerpsClosePositionView`. > - Map loading/error to `RewardAnimationState`; pass `value`, `bonusBips`, `shouldShow`. > - Add `useTooltipModal` to show points error info via `infoOnPress`. > - **Rewards Component**: > - Update `RewardPointsAnimation` to accept `bonusBips?`, `shouldShow?` and return `null` when `shouldShow` is false. > - **Tests**: > - Add/expand tests for rewards row rendering, loading/error states, bonus bips, and formatted values in both views. > - Update i18n test strings and component tests for `RewardPointsAnimation` behavior and new props. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit f5f73b4. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8969552 commit 4e8a2c5

File tree

6 files changed

+306
-12
lines changed

6 files changed

+306
-12
lines changed

app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.test.tsx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2245,4 +2245,133 @@ describe('PerpsClosePositionView', () => {
22452245
});
22462246
});
22472247
});
2248+
2249+
describe('Rewards Points Row', () => {
2250+
it('should render RewardsAnimations component when rewards are enabled', async () => {
2251+
// Arrange
2252+
usePerpsRewardsMock.mockReturnValue({
2253+
shouldShowRewardsRow: true,
2254+
estimatedPoints: 1000,
2255+
isLoading: false,
2256+
hasError: false,
2257+
bonusBips: 250,
2258+
feeDiscountPercentage: 15,
2259+
isRefresh: false,
2260+
});
2261+
2262+
// Act
2263+
const { getByText } = renderWithProvider(
2264+
<PerpsClosePositionView />,
2265+
{ state: STATE_MOCK },
2266+
true,
2267+
);
2268+
2269+
// Assert
2270+
await waitFor(() => {
2271+
expect(getByText(strings('perps.estimated_points'))).toBeDefined();
2272+
expect(getByText('1,000')).toBeDefined();
2273+
});
2274+
});
2275+
2276+
it('should not render rewards row when shouldShowRewardsRow is false', async () => {
2277+
// Arrange
2278+
usePerpsRewardsMock.mockReturnValue({
2279+
shouldShowRewardsRow: false,
2280+
estimatedPoints: undefined,
2281+
isLoading: false,
2282+
hasError: false,
2283+
bonusBips: undefined,
2284+
feeDiscountPercentage: undefined,
2285+
isRefresh: false,
2286+
});
2287+
2288+
// Act
2289+
const { queryByText } = renderWithProvider(
2290+
<PerpsClosePositionView />,
2291+
{ state: STATE_MOCK },
2292+
true,
2293+
);
2294+
2295+
// Assert
2296+
await waitFor(() => {
2297+
expect(queryByText(strings('perps.estimated_points'))).toBeNull();
2298+
});
2299+
});
2300+
2301+
it('should render RewardsAnimations in loading state', async () => {
2302+
// Arrange
2303+
usePerpsRewardsMock.mockReturnValue({
2304+
shouldShowRewardsRow: true,
2305+
estimatedPoints: 0,
2306+
isLoading: true,
2307+
hasError: false,
2308+
bonusBips: undefined,
2309+
feeDiscountPercentage: undefined,
2310+
isRefresh: false,
2311+
});
2312+
2313+
// Act
2314+
const { getByText } = renderWithProvider(
2315+
<PerpsClosePositionView />,
2316+
{ state: STATE_MOCK },
2317+
true,
2318+
);
2319+
2320+
// Assert
2321+
await waitFor(() => {
2322+
expect(getByText(strings('perps.estimated_points'))).toBeDefined();
2323+
});
2324+
});
2325+
2326+
it('should render RewardsAnimations in error state', async () => {
2327+
// Arrange
2328+
usePerpsRewardsMock.mockReturnValue({
2329+
shouldShowRewardsRow: true,
2330+
estimatedPoints: 0,
2331+
isLoading: false,
2332+
hasError: true,
2333+
bonusBips: undefined,
2334+
feeDiscountPercentage: undefined,
2335+
isRefresh: false,
2336+
});
2337+
2338+
// Act
2339+
const { getByText } = renderWithProvider(
2340+
<PerpsClosePositionView />,
2341+
{ state: STATE_MOCK },
2342+
true,
2343+
);
2344+
2345+
// Assert
2346+
await waitFor(() => {
2347+
expect(getByText(strings('perps.estimated_points'))).toBeDefined();
2348+
});
2349+
});
2350+
2351+
it('should render RewardsAnimations with bonus bips', async () => {
2352+
// Arrange
2353+
usePerpsRewardsMock.mockReturnValue({
2354+
shouldShowRewardsRow: true,
2355+
estimatedPoints: 2500,
2356+
isLoading: false,
2357+
hasError: false,
2358+
bonusBips: 500,
2359+
feeDiscountPercentage: 25,
2360+
isRefresh: false,
2361+
});
2362+
2363+
// Act
2364+
const { getByText } = renderWithProvider(
2365+
<PerpsClosePositionView />,
2366+
{ state: STATE_MOCK },
2367+
true,
2368+
);
2369+
2370+
// Assert
2371+
await waitFor(() => {
2372+
expect(getByText(strings('perps.estimated_points'))).toBeDefined();
2373+
expect(getByText('2,500')).toBeDefined();
2374+
});
2375+
});
2376+
});
22482377
});

app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,15 @@ import { MetaMetricsEvents } from '../../../../hooks/useMetrics';
7070
import { TraceName } from '../../../../../util/trace';
7171
import PerpsOrderHeader from '../../components/PerpsOrderHeader';
7272
import PerpsFeesDisplay from '../../components/PerpsFeesDisplay';
73-
import RewardPointsDisplay from '../../components/RewardPointsDisplay';
7473
import PerpsAmountDisplay from '../../components/PerpsAmountDisplay';
7574
import PerpsBottomSheetTooltip from '../../components/PerpsBottomSheetTooltip';
7675
import { PerpsTooltipContentKey } from '../../components/PerpsBottomSheetTooltip/PerpsBottomSheetTooltip.types';
7776
import PerpsLimitPriceBottomSheet from '../../components/PerpsLimitPriceBottomSheet';
7877
import PerpsSlider from '../../components/PerpsSlider/PerpsSlider';
78+
import useTooltipModal from '../../../../../components/hooks/useTooltipModal';
79+
import RewardsAnimations, {
80+
RewardAnimationState,
81+
} from '../../../Rewards/components/RewardPointsAnimation';
7982

8083
const PerpsClosePositionView: React.FC = () => {
8184
const theme = useTheme();
@@ -88,6 +91,7 @@ const PerpsClosePositionView: React.FC = () => {
8891
const inputMethodRef = useRef<InputMethod>('default');
8992

9093
const { showToast, PerpsToastOptions } = usePerpsToasts();
94+
const { openTooltipModal } = useTooltipModal();
9195

9296
// Track screen load performance with unified hook (immediate measurement)
9397
usePerpsMeasurement({
@@ -571,13 +575,23 @@ const PerpsClosePositionView: React.FC = () => {
571575
</TouchableOpacity>
572576
</View>
573577
<View style={styles.summaryValue}>
574-
<RewardPointsDisplay
575-
estimatedPoints={rewardsState.estimatedPoints}
578+
<RewardsAnimations
579+
value={rewardsState.estimatedPoints ?? 0}
576580
bonusBips={rewardsState.bonusBips}
577-
isLoading={rewardsState.isLoading}
578-
hasError={rewardsState.hasError}
579581
shouldShow={rewardsState.shouldShowRewardsRow}
580-
isRefresh={rewardsState.isRefresh}
582+
infoOnPress={() =>
583+
openTooltipModal(
584+
strings('perps.points_error'),
585+
strings('perps.points_error_content'),
586+
)
587+
}
588+
state={
589+
rewardsState.isLoading
590+
? RewardAnimationState.Loading
591+
: rewardsState.hasError
592+
? RewardAnimationState.ErrorState
593+
: RewardAnimationState.Idle
594+
}
581595
/>
582596
</View>
583597
</View>

app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.test.tsx

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ jest.mock('../../../../../../locales/i18n', () => ({
101101
'perps.tpsl.below': 'below',
102102
'perps.tpsl.above': 'above',
103103
'perps.points': 'Points',
104+
'perps.estimated_points': 'perps.estimated_points',
105+
'perps.points_error': 'Points Error',
106+
'perps.points_error_content': 'Unable to load points at this time',
104107
};
105108

106109
if (params && translations[key]) {
@@ -1953,6 +1956,122 @@ describe('PerpsOrderView', () => {
19531956
// Assert - Points text and tooltip should be present
19541957
expect(screen.getByText('perps.estimated_points')).toBeTruthy();
19551958
});
1959+
1960+
it('should render RewardsAnimations component with correct props when rewards shown', async () => {
1961+
// Arrange - Enable rewards with specific values
1962+
(useSelector as jest.Mock).mockImplementation((selector) => {
1963+
if (selector === selectRewardsEnabledFlag) {
1964+
return true;
1965+
}
1966+
return undefined;
1967+
});
1968+
1969+
(usePerpsRewards as jest.Mock).mockReturnValue({
1970+
shouldShowRewardsRow: true,
1971+
estimatedPoints: 1000,
1972+
isLoading: false,
1973+
hasError: false,
1974+
bonusBips: 250,
1975+
feeDiscountPercentage: 15,
1976+
isRefresh: false,
1977+
});
1978+
1979+
// Act
1980+
render(<PerpsOrderView />, { wrapper: TestWrapper });
1981+
1982+
// Assert - Verify the rewards row and animation component render
1983+
await waitFor(() => {
1984+
expect(screen.getByText('perps.estimated_points')).toBeTruthy();
1985+
// The RewardsAnimations component should display the formatted points value
1986+
expect(screen.getByText('1,000')).toBeTruthy();
1987+
});
1988+
});
1989+
1990+
it('should render RewardsAnimations in loading state', async () => {
1991+
// Arrange - Enable rewards in loading state
1992+
(useSelector as jest.Mock).mockImplementation((selector) => {
1993+
if (selector === selectRewardsEnabledFlag) {
1994+
return true;
1995+
}
1996+
return undefined;
1997+
});
1998+
1999+
(usePerpsRewards as jest.Mock).mockReturnValue({
2000+
shouldShowRewardsRow: true,
2001+
estimatedPoints: 0,
2002+
isLoading: true,
2003+
hasError: false,
2004+
bonusBips: undefined,
2005+
feeDiscountPercentage: undefined,
2006+
isRefresh: false,
2007+
});
2008+
2009+
// Act
2010+
render(<PerpsOrderView />, { wrapper: TestWrapper });
2011+
2012+
// Assert - Verify the rewards row renders even in loading state
2013+
await waitFor(() => {
2014+
expect(screen.getByText('perps.estimated_points')).toBeTruthy();
2015+
});
2016+
});
2017+
2018+
it('should render RewardsAnimations in error state', async () => {
2019+
// Arrange - Enable rewards in error state
2020+
(useSelector as jest.Mock).mockImplementation((selector) => {
2021+
if (selector === selectRewardsEnabledFlag) {
2022+
return true;
2023+
}
2024+
return undefined;
2025+
});
2026+
2027+
(usePerpsRewards as jest.Mock).mockReturnValue({
2028+
shouldShowRewardsRow: true,
2029+
estimatedPoints: 0,
2030+
isLoading: false,
2031+
hasError: true,
2032+
bonusBips: undefined,
2033+
feeDiscountPercentage: undefined,
2034+
isRefresh: false,
2035+
});
2036+
2037+
// Act
2038+
render(<PerpsOrderView />, { wrapper: TestWrapper });
2039+
2040+
// Assert - Verify the rewards row renders in error state
2041+
await waitFor(() => {
2042+
expect(screen.getByText('perps.estimated_points')).toBeTruthy();
2043+
// RewardsAnimations component renders with hasError state
2044+
});
2045+
});
2046+
2047+
it('should render RewardsAnimations with bonus bips when provided', async () => {
2048+
// Arrange - Enable rewards with bonus
2049+
(useSelector as jest.Mock).mockImplementation((selector) => {
2050+
if (selector === selectRewardsEnabledFlag) {
2051+
return true;
2052+
}
2053+
return undefined;
2054+
});
2055+
2056+
(usePerpsRewards as jest.Mock).mockReturnValue({
2057+
shouldShowRewardsRow: true,
2058+
estimatedPoints: 2500,
2059+
isLoading: false,
2060+
hasError: false,
2061+
bonusBips: 500, // 5% bonus
2062+
feeDiscountPercentage: 25,
2063+
isRefresh: false,
2064+
});
2065+
2066+
// Act
2067+
render(<PerpsOrderView />, { wrapper: TestWrapper });
2068+
2069+
// Assert - Verify the rewards row renders with bonus
2070+
await waitFor(() => {
2071+
expect(screen.getByText('perps.estimated_points')).toBeTruthy();
2072+
expect(screen.getByText('2,500')).toBeTruthy();
2073+
});
2074+
});
19562075
});
19572076

19582077
describe('Info icon tooltip interactions', () => {

app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ import PerpsOrderHeader from '../../components/PerpsOrderHeader';
5252
import PerpsOrderTypeBottomSheet from '../../components/PerpsOrderTypeBottomSheet';
5353
import PerpsSlider from '../../components/PerpsSlider';
5454
import PerpsTPSLBottomSheet from '../../components/PerpsTPSLBottomSheet';
55-
import RewardPointsDisplay from '../../components/RewardPointsDisplay';
55+
import RewardsAnimations, {
56+
RewardAnimationState,
57+
} from '../../../Rewards/components/RewardPointsAnimation';
5658
import {
5759
PerpsEventProperties,
5860
PerpsEventValues,
@@ -104,6 +106,7 @@ import {
104106
import createStyles from './PerpsOrderView.styles';
105107
import { willFlipPosition } from '../../utils/orderUtils';
106108
import { BigNumber } from 'bignumber.js';
109+
import useTooltipModal from '../../../../../components/hooks/useTooltipModal';
107110

108111
// Navigation params interface
109112
interface OrderRouteParams {
@@ -142,6 +145,7 @@ const PerpsOrderViewContentBase: React.FC = () => {
142145
useState<PerpsTooltipContentKey | null>(null);
143146

144147
const { track } = usePerpsEventTracking();
148+
const { openTooltipModal } = useTooltipModal();
145149

146150
// Ref to access current orderType in callbacks
147151
const orderTypeRef = useRef<OrderType>('market');
@@ -1089,13 +1093,23 @@ const PerpsOrderViewContentBase: React.FC = () => {
10891093
</TouchableOpacity>
10901094
</View>
10911095
<View style={styles.pointsRightContainer}>
1092-
<RewardPointsDisplay
1093-
estimatedPoints={rewardsState.estimatedPoints}
1096+
<RewardsAnimations
1097+
value={rewardsState.estimatedPoints ?? 0}
10941098
bonusBips={rewardsState.bonusBips}
1095-
isLoading={rewardsState.isLoading}
1096-
hasError={rewardsState.hasError}
10971099
shouldShow={rewardsState.shouldShowRewardsRow}
1098-
isRefresh={rewardsState.isRefresh}
1100+
infoOnPress={() =>
1101+
openTooltipModal(
1102+
strings('perps.points_error'),
1103+
strings('perps.points_error_content'),
1104+
)
1105+
}
1106+
state={
1107+
rewardsState.isLoading
1108+
? RewardAnimationState.Loading
1109+
: rewardsState.hasError
1110+
? RewardAnimationState.ErrorState
1111+
: RewardAnimationState.Idle
1112+
}
10991113
/>
11001114
</View>
11011115
</View>

app/components/UI/Rewards/components/RewardPointsAnimation/index.test.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,15 @@ describe('RewardPointsAnimation', () => {
6969
});
7070

7171
describe('basic rendering', () => {
72+
it('renders null when shouldShow is false', () => {
73+
const { toJSON } = render(
74+
<RewardPointsAnimation {...defaultProps} shouldShow={false} />,
75+
);
76+
77+
// Component should return null and render nothing
78+
expect(toJSON()).toBeNull();
79+
});
80+
7281
it('renders with default props', () => {
7382
const { getByText } = render(<RewardPointsAnimation {...defaultProps} />);
7483

0 commit comments

Comments
 (0)