Skip to content

Commit bcf7f71

Browse files
authored
feat: Update musd calculator (MetaMask#27949)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> This PR provides some minor fixes and improvements to the mUSD calculator: - Guard against / set default value for a variable - Use swap navigation instead of deeplink for reduced friction - Move the disclaimer to the bottom of the page content ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: n/a ## **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** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <img width="1170" height="2532" alt="Simulator Screenshot - iPhone 16e - 2026-03-25 at 16 09 28" src="https://github.com/user-attachments/assets/918eeaf3-0f22-41fb-aabc-a96013925e08" /> ## **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 - [ ] 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** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] 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] > **Medium Risk** > Medium risk because it changes the mUSD swap action from a deeplink to in-app swap navigation and adjusts rewards state rehydration/selectors to tolerate missing campaign status data, which could affect routing and campaign UI behavior if misconfigured. > > **Overview** > Updates the mUSD calculator header to use a new localized `rewards.musd.page_title` string and tweaks layout by moving the disclaimer to the bottom with updated styling. > > Replaces the Swap button’s deeplink (`link.metamask.io/swap`) with in-app swap navigation via `useSwapBridgeNavigation`, preconfiguring ETH→mUSD tokens, and updates tests accordingly. > > Hardens Rewards redux state handling by defaulting `campaignParticipantStatuses` to `{}` on rehydrate and making related selectors null-safe, preventing crashes when the field is missing from persisted payloads. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit fc1814e. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 8077384 commit bcf7f71

20 files changed

Lines changed: 73 additions & 27 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const MusdCalculatorView: React.FC = () => {
1818
style={tw.style('flex-1 bg-default')}
1919
>
2020
<HeaderCompactStandard
21-
title={strings('rewards.musd.title')}
21+
title={strings('rewards.musd.page_title')}
2222
onBack={() => navigation.goBack()}
2323
/>
2424
<MusdCalculatorTab />

app/components/UI/Rewards/components/Tabs/MusdCalculatorTab/MusdCalculatorTab.test.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import React from 'react';
22
import { render, fireEvent, waitFor } from '@testing-library/react-native';
33
import MusdCalculatorTab from './MusdCalculatorTab';
44

5+
const mockGoToSwaps = jest.fn();
6+
57
jest.mock('react-redux', () => ({
68
useSelector: jest.fn(() => 'usd'),
9+
useDispatch: jest.fn(() => jest.fn()),
710
}));
811

912
jest.mock('../../../../../../../locales/i18n', () => ({
@@ -14,6 +17,11 @@ jest.mock('../../../../../../core/DeeplinkManager', () => ({
1417
handleDeeplink: jest.fn(),
1518
}));
1619

20+
jest.mock('../../../../Bridge/hooks/useSwapBridgeNavigation', () => ({
21+
useSwapBridgeNavigation: () => ({ goToSwaps: mockGoToSwaps }),
22+
SwapBridgeNavigationLocation: { Rewards: 'Rewards' },
23+
}));
24+
1725
jest.mock('../../../../../../util/theme', () => {
1826
const { mockTheme } = jest.requireActual('../../../../../../util/theme');
1927
return {
@@ -54,17 +62,11 @@ describe('MusdCalculatorTab', () => {
5462
});
5563
});
5664

57-
it('calls handleDeeplink with swap URL when Swap button is pressed', () => {
58-
const { handleDeeplink } = jest.requireMock(
59-
'../../../../../../core/DeeplinkManager',
60-
);
61-
65+
it('navigates to swap screen when Swap button is pressed', () => {
6266
const { getByText } = render(<MusdCalculatorTab />);
6367
fireEvent.press(getByText('rewards.musd.swap_button'));
6468

65-
expect(handleDeeplink).toHaveBeenCalledWith({
66-
uri: expect.stringContaining('link.metamask.io/swap'),
67-
});
69+
expect(mockGoToSwaps).toHaveBeenCalled();
6870
});
6971

7072
it('updates input value when amount changes', () => {

app/components/UI/Rewards/components/Tabs/MusdCalculatorTab/MusdCalculatorTab.tsx

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ import React, { useCallback, useMemo, useState } from 'react';
22
import { ScrollView } from 'react-native';
33
import {
44
Box,
5-
Icon,
6-
IconName,
7-
IconSize,
85
Text,
6+
TextColor,
97
TextVariant,
108
Button,
119
ButtonVariant,
@@ -18,17 +16,40 @@ import { KeyValueRowStubs } from '../../../../../../component-library/components
1816
import { handleDeeplink } from '../../../../../../core/DeeplinkManager';
1917
import useFiatFormatter from '../../../../SimulationDetails/FiatDisplay/useFiatFormatter';
2018
import { BigNumber } from 'bignumber.js';
19+
import { EthScope } from '@metamask/keyring-api';
20+
import Routes from '../../../../../../constants/navigation/Routes';
21+
import {
22+
SwapBridgeNavigationLocation,
23+
useSwapBridgeNavigation,
24+
} from '../../../../Bridge/hooks/useSwapBridgeNavigation';
25+
import { BridgeToken } from '../../../../Bridge/types';
26+
import {
27+
MUSD_TOKEN,
28+
MUSD_TOKEN_ADDRESS,
29+
} from '../../../../Earn/constants/musd';
30+
import { getNativeSourceToken } from '../../../../Bridge/utils/tokenUtils';
2131

2232
const ANNUAL_BONUS_RATE = 0.03;
2333
const BUY_MUSD_URL =
2434
'https://link.metamask.io/buy?address=0xaca92e438df0b2401ff60da7e4337b687a2435da&amount=100&chainid=1&sig_params=address%2Camount%2Cchainid%2Cutm_source&utm_source=rewards&sig=SdHOoh_QvT1bs8B6g-qCyLH5mUEczYzeOfAv9SNRm4CKjR6uBnUp4e1-Vcojb39fWWScBrui2GLftNlJKQlrAQ';
25-
const SWAP_MUSD_URL =
26-
'https://link.metamask.io/swap?from=eip155%3A1%2Fslip44%3A60&sig_params=from%2Cto%2Cutm_source&to=eip155%3A1%2Ferc20%3A0xacA92E438df0B2401fF60dA7E4337B687a2435DA&utm_source=rewards&sig=mCsMuZB-omwEg9WvGOwA8nuc8NvXj7uFfC_Pn-6Nmwlce2GF356tbZbHIgzYHWhmLb4kvUKMTpj4eb0yUrle1Q';
35+
36+
const MUSD_DEST_TOKEN: BridgeToken = {
37+
address: MUSD_TOKEN_ADDRESS,
38+
symbol: MUSD_TOKEN.symbol,
39+
name: MUSD_TOKEN.name,
40+
decimals: MUSD_TOKEN.decimals,
41+
chainId: '0x1',
42+
};
2743

2844
const MusdCalculatorTab: React.FC = () => {
2945
const tw = useTailwind();
3046
const [musdAmount, setMusdAmount] = useState('1000');
3147

48+
const ethSourceToken = useMemo(
49+
() => getNativeSourceToken(EthScope.Mainnet),
50+
[],
51+
);
52+
3253
const musdCalculations = useMemo(() => {
3354
const amount = parseFloat(musdAmount) || 0;
3455
const annualizedBonus = amount * ANNUAL_BONUS_RATE;
@@ -59,9 +80,16 @@ const MusdCalculatorTab: React.FC = () => {
5980
handleDeeplink({ uri: BUY_MUSD_URL });
6081
}, []);
6182

83+
const { goToSwaps } = useSwapBridgeNavigation({
84+
location: SwapBridgeNavigationLocation.Rewards,
85+
sourcePage: Routes.REWARDS_DASHBOARD,
86+
sourceToken: ethSourceToken,
87+
destToken: MUSD_DEST_TOKEN,
88+
});
89+
6290
const handleSwapMusd = useCallback(() => {
63-
handleDeeplink({ uri: SWAP_MUSD_URL });
64-
}, []);
91+
goToSwaps();
92+
}, [goToSwaps]);
6593

6694
return (
6795
<ScrollView
@@ -132,14 +160,6 @@ const MusdCalculatorTab: React.FC = () => {
132160
</Box>
133161
</Box>
134162

135-
{/* Disclaimer */}
136-
<Box twClassName="flex-row gap-2 items-center">
137-
<Icon name={IconName.Info} size={IconSize.Sm} />
138-
<Text variant={TextVariant.BodySm} twClassName="flex-1">
139-
{strings('rewards.musd.disclaimer')}
140-
</Text>
141-
</Box>
142-
143163
{/* Action Buttons */}
144164
<Box twClassName="gap-3">
145165
<Button
@@ -159,6 +179,15 @@ const MusdCalculatorTab: React.FC = () => {
159179
{strings('rewards.musd.swap_button')}
160180
</Button>
161181
</Box>
182+
183+
{/* Disclaimer */}
184+
<Text
185+
variant={TextVariant.BodySm}
186+
color={TextColor.TextAlternative}
187+
twClassName="text-center"
188+
>
189+
{strings('rewards.musd.disclaimer')}
190+
</Text>
162191
</ScrollView>
163192
);
164193
};

app/reducers/rewards/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ const rewardsSlice = createSlice({
688688
unlockedRewards: action.payload.rewards.unlockedRewards,
689689
campaigns: action.payload.rewards.campaigns,
690690
campaignParticipantStatuses:
691-
action.payload.rewards.campaignParticipantStatuses,
691+
action.payload.rewards.campaignParticipantStatuses ?? {},
692692
ondoCampaignLeaderboardPositions:
693693
action.payload.rewards.ondoCampaignLeaderboardPositions ?? {},
694694
hideUnlinkedAccountsBanner:

app/reducers/rewards/selectors.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -172,13 +172,13 @@ export const selectCampaignParticipantStatuses = (state: RootState) =>
172172
export const selectCampaignParticipantStatusById =
173173
(campaignId: string | undefined) => (state: RootState) =>
174174
campaignId
175-
? (state.rewards.campaignParticipantStatuses[campaignId] ?? null)
175+
? (state.rewards.campaignParticipantStatuses?.[campaignId] ?? null)
176176
: null;
177177

178178
export const selectCampaignParticipantCount =
179179
(campaignId: string | undefined) => (state: RootState) =>
180180
campaignId
181-
? (state.rewards.campaignParticipantStatuses[campaignId]
181+
? (state.rewards.campaignParticipantStatuses?.[campaignId]
182182
?.participantCount ?? null)
183183
: null;
184184

locales/languages/de.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7872,6 +7872,7 @@
78727872
"active_boosts_title": "Aktive Boosts",
78737873
"season_1": "Saison 1",
78747874
"musd": {
7875+
"page_title": "mUSD",
78757876
"title": "mUSD-Bonus-Rechner",
78767877
"description": "Finden Sie heraus, wie viel Sie durch die Konvertierung Ihrer Stablecoins in mUSD verdienen können.",
78777878
"amount_label": "Konvertierter Betrag",

locales/languages/el.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7872,6 +7872,7 @@
78727872
"active_boosts_title": "Ενεργές ενισχύσεις",
78737873
"season_1": "Περίοδος 1",
78747874
"musd": {
7875+
"page_title": "mUSD",
78757876
"title": "Εκτίμηση του μπόνους σε mUSD",
78767877
"description": "Δείτε πόσα μπορείτε να κερδίσετε μετατρέποντας τα stablecoins σας σε mUSD.",
78777878
"amount_label": "Το ποσό μετατράπηκε",

locales/languages/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7878,6 +7878,7 @@
78787878
"active_boosts_title": "Active boosts",
78797879
"season_1": "Season 1",
78807880
"musd": {
7881+
"page_title": "mUSD",
78817882
"title": "mUSD bonus calculator",
78827883
"description": "See how much you could earn by converting your stablecoins to mUSD.",
78837884
"amount_label": "Amount converted",

locales/languages/es.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7872,6 +7872,7 @@
78727872
"active_boosts_title": "Potenciadores activos",
78737873
"season_1": "Temporada 1",
78747874
"musd": {
7875+
"page_title": "mUSD",
78757876
"title": "Calculadora del bono en mUSD",
78767877
"description": "Descubre cuánto podrías ganar al convertir tus monedas estables a mUSD.",
78777878
"amount_label": "Monto convertido",

locales/languages/fr.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7872,6 +7872,7 @@
78727872
"active_boosts_title": "Boosts actifs",
78737873
"season_1": "Saison 1",
78747874
"musd": {
7875+
"page_title": "mUSD",
78757876
"title": "Calculateur du bonus en mUSD",
78767877
"description": "Découvrez combien vous pourriez gagner en convertissant vos stablecoins en mUSD.",
78777878
"amount_label": "Montant converti",

0 commit comments

Comments
 (0)