Skip to content

Commit e3a3c5f

Browse files
authored
feat(money): show Paid by MetaMask on sponsored mUSD conversions (#30120)
## **Description** When MM Pay returns a fully sponsored mUSD conversion quote (network, provider, and MetaMask fees all zero), the confirmation screen now surfaces the sponsorship explicitly: the "Transaction fee" row renders a green check + "Paid by MetaMask" label instead of "$0", and the redundant fee-breakdown tooltip is hidden. Polishes the same screen to match design: 8px row gap, "Confirm" CTA, icon-primary header (i), the Total row is hidden for mUSD conversion, and the screen overlays the bottom tab bar when launched from Money Home / Potential Earnings. ## **Changelog** CHANGELOG entry: Added a "Paid by MetaMask" treatment on the mUSD conversion confirmation screen when MetaMask fully sponsors the network, provider, and gas fees. ## **Related issues** Fixes: [MUSD-794](https://consensyssoftware.atlassian.net/browse/MUSD-794) ## **Manual testing steps** ```gherkin Feature: Paid by MetaMask on sponsored mUSD conversion Scenario: user converts a small amount of USDC to mUSD Given the user is on Money Home with a USDC balance And the conversion quote returns zero network, provider, and MetaMask fees When the user taps a token's "Get 3% mUSD bonus" entry, enters an amount, and waits for the quote Then the confirmation screen overlays the bottom tab bar And the Transaction fee row shows a green check + "Paid by MetaMask" with no tooltip And the Total row is hidden And the Claimable bonus row shows 3% And the primary CTA reads "Confirm" ``` ## **Screenshots/Recordings** ### **After** <img width="1206" height="2622" alt="image" src="https://github.com/user-attachments/assets/f42b67d0-060e-4b55-9368-1a712687f3b3" /> <img width="1206" height="2622" alt="image" src="https://github.com/user-attachments/assets/431f37a2-713a-41e3-b709-bb96c724d943" /> <img width="1206" height="2622" alt="image" src="https://github.com/user-attachments/assets/4f4c7d23-4be1-45b9-ac3e-b5f7e5fca652" /> ## **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. #### Performance checks (if applicable) - [ ] I've tested on Android - [ ] I've tested with a power user scenario - [ ] I've instrumented key operations with Sentry traces for production performance metrics ## **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. [MUSD-794]: https://consensyssoftware.atlassian.net/browse/MUSD-794?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Updates the transaction confirmation fee/total presentation for `musdConversion` and adjusts navigation/tooltip behavior, which could affect conversion confirmation UX and fee transparency if edge cases (missing fee fields/quotes) are mishandled. > > **Overview** > mUSD conversion confirmations now explicitly show sponsorship: when MM Pay returns quotes and *all* fee components are zero, the **Transaction fee** row renders a green check + `Paid by MetaMask` label and suppresses the fee-breakdown tooltip via the new `useIsPaidByMetaMask` hook. > > The confirmation screen is further aligned to the sponsored design by hiding the `TotalRow` for `musdConversion`, changing the CTA label to `earn.musd_conversion.confirm`, and replacing the old `useTooltipModal` navbar info flow with an inline `TooltipModal` (`TooltipNode`) rendered by `MusdConversionInfo` (including updated tooltip copy and analytics for the terms link). > > Navigation into redesigned confirmations from the Earn stack is adjusted to present as a `card`, and Money conversion initiation calls drop the `navigationStack` parameter. Tests, smoke specs, and English strings are updated accordingly. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 7c32351. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 7ab53fd commit e3a3c5f

17 files changed

Lines changed: 464 additions & 136 deletions

File tree

app/components/UI/Earn/hooks/useMusdConversionNavbar.test.tsx

Lines changed: 36 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import { useMusdConversionNavbar } from './useMusdConversionNavbar';
66
import useNavbar from '../../../Views/confirmations/hooks/ui/useNavbar';
77
import { strings } from '../../../../../locales/i18n';
88
import { NavbarOverrides } from '../../../Views/confirmations/components/UI/navbar/navbar';
9-
import useTooltipModal from '../../../hooks/useTooltipModal';
109
import { MUSD_CONVERSION_APY } from '../constants/musd';
1110
import AppConstants from '../../../../core/AppConstants';
1211
import { MetaMetricsEvents } from '../../../../core/Analytics';
@@ -30,17 +29,10 @@ jest.mock('../../../../../locales/i18n', () => ({
3029

3130
jest.mock('../../../Views/confirmations/hooks/ui/useNavbar');
3231

33-
jest.mock('../../../hooks/useTooltipModal');
34-
3532
const mockUseNavbar = useNavbar as jest.MockedFunction<typeof useNavbar>;
3633
const mockStrings = strings as jest.MockedFunction<typeof strings>;
37-
const mockUseTooltipModal = useTooltipModal as jest.MockedFunction<
38-
typeof useTooltipModal
39-
>;
4034

4135
describe('useMusdConversionNavbar', () => {
42-
const mockOpenTooltipModal = jest.fn();
43-
4436
beforeEach(() => {
4537
jest.clearAllMocks();
4638

@@ -49,10 +41,6 @@ describe('useMusdConversionNavbar', () => {
4941
mockCreateEventBuilder.mockImplementation(() => ({
5042
addProperties: mockAddProperties,
5143
}));
52-
53-
mockUseTooltipModal.mockReturnValue({
54-
openTooltipModal: mockOpenTooltipModal,
55-
});
5644
});
5745

5846
afterEach(() => {
@@ -78,6 +66,12 @@ describe('useMusdConversionNavbar', () => {
7866
);
7967
});
8068

69+
it('returns a TooltipNode element', () => {
70+
const { result } = renderHook(() => useMusdConversionNavbar());
71+
72+
expect(React.isValidElement(result.current.TooltipNode)).toBe(true);
73+
});
74+
8175
it('provides headerTitle override that renders the heading', () => {
8276
let capturedOverrides: NavbarOverrides | undefined;
8377
mockUseNavbar.mockImplementation((_title, _addBackButton, overrides) => {
@@ -162,23 +156,27 @@ describe('useMusdConversionNavbar', () => {
162156
capturedOverrides = overrides;
163157
});
164158

165-
renderHook(() => useMusdConversionNavbar());
159+
const Harness = () => {
160+
const { TooltipNode } = useMusdConversionNavbar();
161+
const HeaderRight = capturedOverrides?.headerRight as React.FC;
162+
return (
163+
<>
164+
<HeaderRight />
165+
{TooltipNode}
166+
</>
167+
);
168+
};
166169

167-
const HeaderRight = capturedOverrides?.headerRight as React.FC;
168-
const { getByTestId } = render(<HeaderRight />);
170+
const { getByTestId } = render(<Harness />);
169171

170172
fireEvent.press(getByTestId('button-icon'));
171173

172-
expect(mockOpenTooltipModal).toHaveBeenCalledTimes(1);
173-
expect(mockOpenTooltipModal).toHaveBeenCalledWith(
174-
'earn.musd_conversion.convert_and_get_percentage_bonus',
175-
expect.any(Object),
176-
'earn.musd_conversion.powered_by_relay',
177-
'earn.musd_conversion.ok',
178-
);
174+
expect(
175+
getByTestId('musd-conversion-navbar-tooltip-terms-link'),
176+
).toBeOnTheScreen();
179177
});
180178

181-
it('opens bonus terms of use when "Terms apply" is pressed in tooltip content', () => {
179+
it('opens bonus terms of use and tracks event when "Terms apply" is pressed', () => {
182180
const openUrlSpy = jest
183181
.spyOn(Linking, 'openURL')
184182
.mockResolvedValueOnce(undefined);
@@ -188,48 +186,21 @@ describe('useMusdConversionNavbar', () => {
188186
capturedOverrides = overrides;
189187
});
190188

191-
renderHook(() => useMusdConversionNavbar());
192-
193-
const HeaderRight = capturedOverrides?.headerRight as React.FC;
194-
const { getByTestId } = render(<HeaderRight />);
195-
196-
fireEvent.press(getByTestId('button-icon'));
197-
198-
const tooltipBody = mockOpenTooltipModal.mock
199-
.calls[0][1] as React.ReactElement;
200-
const { getByText } = render(tooltipBody);
201-
202-
fireEvent.press(getByText('earn.musd_conversion.education.terms_apply'));
203-
204-
expect(openUrlSpy).toHaveBeenCalledTimes(1);
205-
expect(openUrlSpy).toHaveBeenCalledWith(
206-
AppConstants.URLS.MUSD_CONVERSION_BONUS_TERMS_OF_USE,
207-
);
208-
});
209-
210-
it('tracks MUSD_BONUS_TERMS_OF_USE_PRESSED event when "Terms apply" is pressed in tooltip content', () => {
211-
let capturedOverrides: NavbarOverrides | undefined;
212-
mockUseNavbar.mockImplementation((_title, _addBackButton, overrides) => {
213-
capturedOverrides = overrides;
214-
});
215-
216-
renderHook(() => useMusdConversionNavbar());
189+
const Harness = () => {
190+
const { TooltipNode } = useMusdConversionNavbar();
191+
const HeaderRight = capturedOverrides?.headerRight as React.FC;
192+
return (
193+
<>
194+
<HeaderRight />
195+
{TooltipNode}
196+
</>
197+
);
198+
};
217199

218-
const HeaderRight = capturedOverrides?.headerRight as React.FC;
219-
const { getByTestId } = render(<HeaderRight />);
200+
const { getByTestId } = render(<Harness />);
220201

221202
fireEvent.press(getByTestId('button-icon'));
222-
223-
const tooltipBody = mockOpenTooltipModal.mock
224-
.calls[0][1] as React.ReactElement;
225-
const { getByText } = render(tooltipBody);
226-
227-
mockTrackEvent.mockClear();
228-
mockCreateEventBuilder.mockClear();
229-
mockAddProperties.mockClear();
230-
mockBuild.mockClear();
231-
232-
fireEvent.press(getByText('earn.musd_conversion.education.terms_apply'));
203+
fireEvent.press(getByTestId('musd-conversion-navbar-tooltip-terms-link'));
233204

234205
expect(mockCreateEventBuilder).toHaveBeenCalledWith(
235206
MetaMetricsEvents.MUSD_BONUS_TERMS_OF_USE_PRESSED,
@@ -239,5 +210,8 @@ describe('useMusdConversionNavbar', () => {
239210
url: AppConstants.URLS.MUSD_CONVERSION_BONUS_TERMS_OF_USE,
240211
});
241212
expect(mockTrackEvent).toHaveBeenCalledWith({ name: 'mock-built-event' });
213+
expect(openUrlSpy).toHaveBeenCalledWith(
214+
AppConstants.URLS.MUSD_CONVERSION_BONUS_TERMS_OF_USE,
215+
);
242216
});
243217
});

app/components/UI/Earn/hooks/useMusdConversionNavbar.tsx

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useMemo } from 'react';
1+
import React, { useCallback, useMemo, useState } from 'react';
22
import { View, StyleSheet, Linking } from 'react-native';
33
import Text, {
44
TextVariant,
@@ -12,7 +12,7 @@ import {
1212
IconName,
1313
} from '@metamask/design-system-react-native';
1414
import useNavbar from '../../../Views/confirmations/hooks/ui/useNavbar';
15-
import useTooltipModal from '../../../hooks/useTooltipModal';
15+
import { TooltipModal } from '../../../Views/confirmations/components/UI/Tooltip/Tooltip';
1616
import AppConstants from '../../../../core/AppConstants';
1717
import { useAnalytics } from '../../../hooks/useAnalytics/useAnalytics';
1818
import { MetaMetricsEvents } from '../../../../core/Analytics';
@@ -42,7 +42,7 @@ const styles = StyleSheet.create({
4242
*
4343
*/
4444
export function useMusdConversionNavbar() {
45-
const { openTooltipModal } = useTooltipModal();
45+
const [tooltipOpen, setTooltipOpen] = useState(false);
4646

4747
const { trackEvent, createEventBuilder } = useAnalytics();
4848

@@ -85,33 +85,15 @@ export function useMusdConversionNavbar() {
8585
Linking.openURL(AppConstants.URLS.MUSD_CONVERSION_BONUS_TERMS_OF_USE);
8686
}, [createEventBuilder, trackEvent]);
8787

88-
const onInfoPress = useCallback(() => {
89-
openTooltipModal(
90-
strings('earn.musd_conversion.convert_and_get_percentage_bonus', {
91-
percentage: MUSD_CONVERSION_APY,
92-
}),
93-
<Text variant={TextVariant.BodyMD}>
94-
{strings('earn.musd_conversion.education.description', {
95-
percentage: MUSD_CONVERSION_APY,
96-
})}{' '}
97-
<Text variant={TextVariant.BodyMD}>
98-
<Text onPress={handleTermsOfUsePressed} style={styles.termsText}>
99-
{strings('earn.musd_conversion.education.terms_apply')}
100-
</Text>
101-
</Text>
102-
</Text>,
103-
strings('earn.musd_conversion.powered_by_relay'),
104-
strings('earn.musd_conversion.ok'),
105-
);
106-
}, [handleTermsOfUsePressed, openTooltipModal]);
88+
const onInfoPress = useCallback(() => setTooltipOpen(true), []);
10789

10890
const renderHeaderRight = useCallback(
10991
() => (
11092
<View style={styles.headerRight}>
11193
<ButtonIcon
11294
iconName={IconName.Info}
11395
size={ButtonIconSize.Md}
114-
iconProps={{ color: IconColor.IconAlternative }}
96+
iconProps={{ color: IconColor.IconDefault }}
11597
onPress={onInfoPress}
11698
/>
11799
</View>
@@ -135,4 +117,32 @@ export function useMusdConversionNavbar() {
135117
true,
136118
overrides,
137119
);
120+
121+
const TooltipNode = (
122+
<TooltipModal
123+
open={tooltipOpen}
124+
setOpen={setTooltipOpen}
125+
content={
126+
<Text variant={TextVariant.BodyMD}>
127+
{strings('earn.musd_conversion.convert_tooltip_description', {
128+
percentage: MUSD_CONVERSION_APY,
129+
})}{' '}
130+
<Text
131+
variant={TextVariant.BodyMD}
132+
style={styles.termsText}
133+
onPress={handleTermsOfUsePressed}
134+
testID="musd-conversion-navbar-tooltip-terms-link"
135+
>
136+
{strings('earn.musd_conversion.education.terms_apply')}
137+
</Text>
138+
</Text>
139+
}
140+
title={strings('earn.musd_conversion.convert_and_get_percentage_bonus', {
141+
percentage: MUSD_CONVERSION_APY,
142+
})}
143+
tooltipTestId="musd-conversion-navbar-tooltip"
144+
/>
145+
);
146+
147+
return { TooltipNode };
138148
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ const EarnScreenStack = () => {
2929
<Stack.Screen
3030
name={Routes.FULL_SCREEN_CONFIRMATIONS.REDESIGNED_CONFIRMATIONS}
3131
component={Confirm}
32-
options={emptyNavHeaderOptions}
32+
options={{ ...emptyNavHeaderOptions, presentation: 'card' }}
3333
/>
3434
<Stack.Screen
3535
name={Routes.EARN.MUSD.CONVERSION_EDUCATION}

app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.test.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -887,7 +887,6 @@ describe('MoneyHomeView', () => {
887887
preferredPaymentToken: expect.objectContaining({
888888
address: mockConversionTokens[0].address,
889889
}),
890-
navigationStack: Routes.MONEY.ROOT,
891890
}),
892891
);
893892
});

app/components/UI/Money/Views/MoneyHomeView/MoneyHomeView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,6 @@ const MoneyHomeView = () => {
178178
address: token.address as Hex,
179179
chainId: token.chainId as Hex,
180180
},
181-
navigationStack: Routes.MONEY.ROOT,
182181
});
183182
} catch (error) {
184183
Logger.error(error as Error, {

app/components/UI/Money/Views/MoneyPotentialEarningsView/MoneyPotentialEarningsView.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,6 @@ const MoneyPotentialEarningsView = () => {
7979
address: defaultToken.address as Hex,
8080
chainId: defaultToken.chainId as Hex,
8181
},
82-
navigationStack: Routes.MONEY.ROOT,
8382
});
8483
} catch (error) {
8584
Logger.error(error as Error, {
@@ -97,7 +96,6 @@ const MoneyPotentialEarningsView = () => {
9796
address: token.address as Hex,
9897
chainId: token.chainId as Hex,
9998
},
100-
navigationStack: Routes.MONEY.ROOT,
10199
});
102100
} catch (error) {
103101
Logger.error(error as Error, {

app/components/Views/confirmations/components/info/custom-amount-info/custom-amount-info.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ function useButtonLabel() {
434434
}
435435

436436
if (hasTransactionType(transaction, [TransactionType.musdConversion])) {
437-
return strings('earn.musd_conversion.convert');
437+
return strings('earn.musd_conversion.confirm');
438438
}
439439

440440
return strings('confirm.deposit_edit_amount_done');

app/components/Views/confirmations/components/info/musd-conversion-info/musd-conversion-info.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ describe('MusdConversionInfo', () => {
5353
chainId: '0x1' as Hex,
5454
};
5555
mockUseParams.mockReturnValue(mockParams);
56-
mockUseMusdConversionNavbar.mockReturnValue(undefined);
56+
mockUseMusdConversionNavbar.mockReturnValue({ TooltipNode: <></> });
5757
});
5858

5959
afterEach(() => {

app/components/Views/confirmations/components/info/musd-conversion-info/musd-conversion-info.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export const MusdConversionInfo = () => {
3737
);
3838
}
3939

40-
useMusdConversionNavbar();
40+
const { TooltipNode } = useMusdConversionNavbar();
4141

4242
const { startQuoteTrace } = useMusdConversionQuoteTrace();
4343

@@ -60,11 +60,14 @@ export const MusdConversionInfo = () => {
6060
});
6161

6262
return (
63-
<CustomAmountInfo
64-
preferredToken={preferredPaymentToken}
65-
hidePayTokenAmount
66-
hasMax
67-
onAmountSubmit={startQuoteTrace}
68-
/>
63+
<>
64+
<CustomAmountInfo
65+
preferredToken={preferredPaymentToken}
66+
hidePayTokenAmount
67+
hasMax
68+
onAmountSubmit={startQuoteTrace}
69+
/>
70+
{TooltipNode}
71+
</>
6972
);
7073
};

0 commit comments

Comments
 (0)