Skip to content

Commit 326dc71

Browse files
authored
feat: update correct transactiondata.paymentOverride value and pay token for deposit using money account (MetaMask#30614)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until this PR meets the canonical Definition of Ready For Review in `docs/readme/ready-for-review.md`. In short: the template must be materially complete (not just section titles present), all status checks must be currently passing, and the only expected follow-up commits must be reviewer-driven. --> ## **Description** Update correct transactiondata.paymentOverride value and pay token for deposit using money account. ## **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: ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/CONF-1427 ## **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** https://github.com/user-attachments/assets/be07e081-c776-4dfb-acff-b4e8257d8761 ## **Pre-merge author checklist** <!-- Every checklist item must be consciously assessed before marking this PR as "Ready for review". A checked box means you deliberately considered that responsibility, not that you literally performed every action listed. Unchecked boxes are ambiguous: they are not an implicit "N/A" and they are not a silent "skip". See `docs/readme/ready-for-review.md` for the full checklist semantics. --> - [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. #### Performance checks (if applicable) - [ ] I've tested on Android - Ideally on a mid-range device; emulator is acceptable - [ ] I've tested with a power user scenario - Use these [power-user SRPs](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** <!-- Reviewer checklist items follow the same semantics as the author checklist: an unchecked box is ambiguous, a checked box means the reviewer consciously assessed that responsibility. See `docs/readme/ready-for-review.md`. --> - [ ] 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** > Touches confirmation payment selection, balance validation, and TransactionPayController config across deposits; mistakes could block confirms or show wrong funding source, but changes are scoped with broad tests and controller bump. > > **Overview** > This PR wires **Money account** as a first-class pay source for Transaction Pay deposits by persisting `transactionData.paymentOverride` (`PaymentOverride.MoneyAccount`) and keeping UI, token selection, balances, and alerts aligned with that choice. > > **Confirmation UI:** `PayWithRow` branches on the override to a dedicated money-account row (label + optional fiat balance). The pay-with bottom sheet sets the override when the money-account row is tapped and clears it via a shared `useClearPaymentOverride` hook when the user picks crypto, fiat, or Perps—crypto section logic also hides misleading MUSD/MONAD rows and checkmarks while money account owns the selection. > > **Pay token & amounts:** Automatic selection forces **MUSD on MONAD** when the override is active (not in post-quote flows), re-applies on toggle, and `useTransactionCustomAmount` / insufficient-balance alerts use **money account fiat** (`totalFiatRaw`) instead of on-chain pay-token balance, skipping fee/native-gas checks for that source. > > **Infrastructure:** Adds `selectPaymentOverrideByTransactionId`, bumps `@metamask/transaction-pay-controller` to **^22.7.0**, and extends tests across row, sections, alerts, and auto-token hooks. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit e65de07. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 4a41941 commit 326dc71

21 files changed

Lines changed: 931 additions & 68 deletions

app/components/Views/confirmations/components/rows/pay-with-row/pay-with-row.test.tsx

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,29 @@
11
import React from 'react';
2+
import { type PaymentMethod } from '@metamask/ramps-controller';
3+
import { PaymentOverride } from '@metamask/transaction-pay-controller';
4+
import { useNavigation } from '@react-navigation/native';
5+
import { act, fireEvent } from '@testing-library/react-native';
6+
import { merge } from 'lodash';
7+
import { Text as MockText } from 'react-native';
8+
29
import { PayWithRow } from './pay-with-row';
310
import { TokenIconProps } from '../../token-icon';
411
import { useTransactionPayToken } from '../../../hooks/pay/useTransactionPayToken';
512
import { useTransactionPayWithdraw } from '../../../hooks/pay/useTransactionPayWithdraw';
613
import { useTransactionPayRequiredTokens } from '../../../hooks/pay/useTransactionPayData';
714
import { useTransactionPaySelectedFiatPaymentMethod } from '../../../hooks/pay/useTransactionPaySelectedFiatPaymentMethod';
8-
import { type PaymentMethod } from '@metamask/ramps-controller';
9-
import { useNavigation } from '@react-navigation/native';
10-
import { act, fireEvent } from '@testing-library/react-native';
1115
import Routes from '../../../../../../constants/navigation/Routes';
12-
import { Text as MockText } from 'react-native';
1316
import renderWithProvider from '../../../../../../util/test/renderWithProvider';
1417
import { backgroundState } from '../../../../../../util/test/initial-root-state';
1518
import { isHardwareAccount } from '../../../../../../util/address';
1619
import { useConfirmationMetricEvents } from '../../../hooks/metrics/useConfirmationMetricEvents';
1720
import { useTransactionMetadataRequest } from '../../../hooks/transactions/useTransactionMetadataRequest';
1821
import { useParams } from '../../../../../../util/navigation/navUtils';
22+
import useMoneyAccountBalance from '../../../../../UI/Money/hooks/useMoneyAccountBalance';
1923

2024
jest.mock('../../../hooks/transactions/useTransactionMetadataRequest');
2125
jest.mock('../../../../../../util/navigation/navUtils');
26+
jest.mock('../../../../../UI/Money/hooks/useMoneyAccountBalance');
2227
jest.mock('@react-navigation/native', () => ({
2328
...jest.requireActual('@react-navigation/native'),
2429
useNavigation: jest.fn(),
@@ -131,6 +136,11 @@ describe('PayWithRow', () => {
131136
navigate: navigateMock,
132137
} as never);
133138

139+
jest.mocked(useTransactionMetadataRequest).mockReturnValue(undefined);
140+
jest.mocked(useMoneyAccountBalance).mockReturnValue({
141+
totalFiatFormatted: undefined,
142+
} as ReturnType<typeof useMoneyAccountBalance>);
143+
134144
isHardwareAccountMock.mockReturnValue(false);
135145
});
136146

@@ -285,4 +295,85 @@ describe('PayWithRow', () => {
285295
);
286296
});
287297
});
298+
299+
describe('money account mode', () => {
300+
const TRANSACTION_ID_MOCK = 'tx-money-1';
301+
302+
const MONEY_ACCOUNT_STATE = merge({}, STATE_MOCK, {
303+
engine: {
304+
backgroundState: {
305+
TransactionPayController: {
306+
transactionData: {
307+
[TRANSACTION_ID_MOCK]: {
308+
paymentOverride: PaymentOverride.MoneyAccount,
309+
},
310+
},
311+
},
312+
},
313+
},
314+
});
315+
316+
function renderMoneyAccount() {
317+
return renderWithProvider(<PayWithRow />, {
318+
state: MONEY_ACCOUNT_STATE,
319+
});
320+
}
321+
322+
beforeEach(() => {
323+
jest.mocked(useTransactionMetadataRequest).mockReturnValue({
324+
id: TRANSACTION_ID_MOCK,
325+
} as never);
326+
});
327+
328+
it('renders money account label with balance', () => {
329+
jest.mocked(useMoneyAccountBalance).mockReturnValue({
330+
totalFiatFormatted: '$500.00',
331+
} as ReturnType<typeof useMoneyAccountBalance>);
332+
333+
const { getByTestId } = renderMoneyAccount();
334+
335+
expect(getByTestId('pay-with-symbol').props.children).toContain(
336+
'Money account',
337+
);
338+
expect(getByTestId('pay-with-balance')).toBeDefined();
339+
});
340+
341+
it('renders money account label without balance when totalFiatFormatted is undefined', () => {
342+
jest.mocked(useMoneyAccountBalance).mockReturnValue({
343+
totalFiatFormatted: undefined,
344+
} as ReturnType<typeof useMoneyAccountBalance>);
345+
346+
const { getByTestId, queryByTestId } = renderMoneyAccount();
347+
348+
expect(getByTestId('pay-with-symbol').props.children).toContain(
349+
'Money account',
350+
);
351+
expect(queryByTestId('pay-with-balance')).toBeNull();
352+
});
353+
354+
it('renders skeleton when no pay token in money account mode', () => {
355+
jest.mocked(useTransactionPayToken).mockReturnValue({
356+
payToken: undefined,
357+
setPayToken: jest.fn(),
358+
});
359+
360+
const { getByTestId } = renderMoneyAccount();
361+
362+
expect(getByTestId('pay-with-row-skeleton')).toBeDefined();
363+
});
364+
365+
it('navigates to pay-with modal on press', async () => {
366+
jest.mocked(useMoneyAccountBalance).mockReturnValue({
367+
totalFiatFormatted: '$500.00',
368+
} as ReturnType<typeof useMoneyAccountBalance>);
369+
370+
const { getByTestId } = renderMoneyAccount();
371+
372+
await act(() => {
373+
fireEvent.press(getByTestId('pay-with'));
374+
});
375+
376+
expect(navigateMock).toHaveBeenCalled();
377+
});
378+
});
288379
});

app/components/Views/confirmations/components/rows/pay-with-row/pay-with-row.tsx

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import React, { useCallback, useMemo } from 'react';
22
import { useNavigation } from '@react-navigation/native';
3+
import { useSelector } from 'react-redux';
34
import { PaymentType } from '@consensys/on-ramp-sdk';
45
import Routes from '../../../../../../constants/navigation/Routes';
6+
import { RootState } from '../../../../../../reducers';
7+
import { selectPaymentOverrideByTransactionId } from '../../../../../../selectors/transactionPayController';
58
import { TokenIcon, TokenIconVariant } from '../../token-icon';
69
import { useTransactionPayToken } from '../../../hooks/pay/useTransactionPayToken';
710
import { useTransactionPayWithdraw } from '../../../hooks/pay/useTransactionPayWithdraw';
@@ -24,6 +27,7 @@ import {
2427
import { useStyles } from '../../../../../hooks/useStyles';
2528
import styleSheet from './pay-with-row.styles';
2629
import { BigNumber } from 'bignumber.js';
30+
import { PaymentOverride } from '@metamask/transaction-pay-controller';
2731
import { strings } from '../../../../../../../locales/i18n';
2832
import { useTransactionMetadataRequest } from '../../../hooks/transactions/useTransactionMetadataRequest';
2933
import { isHardwareAccount } from '../../../../../../util/address';
@@ -42,6 +46,7 @@ import { useConfirmationMetricEvents } from '../../../hooks/metrics/useConfirmat
4246
import { type PaymentMethod } from '@metamask/ramps-controller';
4347
import { useParams } from '../../../../../../util/navigation/navUtils';
4448
import { SetPayTokenRequest } from '../../../hooks/pay/useAutomaticTransactionPayToken';
49+
import useMoneyAccountBalance from '../../../../../UI/Money/hooks/useMoneyAccountBalance';
4550
import { useConfirmationContext } from '../../../context/confirmation-context';
4651
import { useTheme } from '../../../../../../util/theme';
4752
import { isPayWithBottomSheetEnabled } from '../../../utils/transaction-pay';
@@ -51,6 +56,19 @@ interface PayWithRouteParams {
5156
}
5257

5358
export function PayWithRow() {
59+
const transactionId = useTransactionMetadataRequest()?.id ?? '';
60+
const paymentOverride = useSelector((state: RootState) =>
61+
selectPaymentOverrideByTransactionId(state, transactionId),
62+
);
63+
64+
if (paymentOverride === PaymentOverride.MoneyAccount) {
65+
return <PayWithRowMoneyAccount />;
66+
}
67+
68+
return <PayWithRowInteractive />;
69+
}
70+
71+
function PayWithRowInteractive() {
5472
const navigation = useNavigation();
5573
const { payToken } = useTransactionPayToken();
5674
const { isWithdraw } = useTransactionPayWithdraw();
@@ -248,6 +266,83 @@ function PayWithFiatPaymentMethodRow({
248266
);
249267
}
250268

269+
function PayWithRowMoneyAccount() {
270+
const navigation = useNavigation();
271+
const { payToken } = useTransactionPayToken();
272+
const { styles } = useStyles(styleSheet, {});
273+
const { setConfirmationMetric } = useConfirmationMetricEvents();
274+
const { preferredPaymentToken } = useParams<PayWithRouteParams>({});
275+
const { totalFiatFormatted } = useMoneyAccountBalance();
276+
277+
const handleClick = useCallback(() => {
278+
setConfirmationMetric({
279+
properties: { mm_pay_token_list_opened: true },
280+
});
281+
if (isPayWithBottomSheetEnabled()) {
282+
navigation.navigate(Routes.CONFIRMATION_PAY_WITH_BOTTOM_SHEET, {
283+
preferredPaymentToken,
284+
});
285+
return;
286+
}
287+
navigation.navigate(Routes.CONFIRMATION_PAY_WITH_MODAL);
288+
}, [navigation, preferredPaymentToken, setConfirmationMetric]);
289+
290+
if (!payToken) {
291+
return <PayWithRowSkeleton />;
292+
}
293+
294+
return (
295+
<TouchableOpacity
296+
onPress={handleClick}
297+
testID={ConfirmationRowComponentIDs.PAY_WITH}
298+
>
299+
<Box
300+
flexDirection={FlexDirection.Row}
301+
alignItems={AlignItems.center}
302+
justifyContent={JustifyContent.spaceBetween}
303+
style={styles.container}
304+
>
305+
<Text variant={TextVariant.BodyMd} color={TextColor.TextAlternative}>
306+
{strings('confirm.label.pay_with')}
307+
</Text>
308+
<Box
309+
flexDirection={FlexDirection.Row}
310+
alignItems={AlignItems.center}
311+
gap={8}
312+
>
313+
<TokenIcon
314+
address={payToken.address}
315+
chainId={payToken.chainId}
316+
variant={TokenIconVariant.Row}
317+
/>
318+
<Text
319+
variant={TextVariant.BodyMd}
320+
fontWeight={FontWeight.Medium}
321+
color={TextColor.TextDefault}
322+
testID={TransactionPayComponentIDs.PAY_WITH_SYMBOL}
323+
>
324+
{strings('confirm.pay_with_bottom_sheet.money_account')}
325+
{totalFiatFormatted && (
326+
<Text
327+
variant={TextVariant.BodyMd}
328+
color={TextColor.TextAlternative}
329+
testID={TransactionPayComponentIDs.PAY_WITH_BALANCE}
330+
>
331+
{` (${totalFiatFormatted})`}
332+
</Text>
333+
)}
334+
</Text>
335+
<Icon
336+
name={IconName.ArrowDown}
337+
size={IconSize.Sm}
338+
color={IconColor.Alternative}
339+
/>
340+
</Box>
341+
</Box>
342+
</TouchableOpacity>
343+
);
344+
}
345+
251346
export function PayWithRowSkeleton() {
252347
const { styles } = useStyles(styleSheet, {});
253348

0 commit comments

Comments
 (0)