Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/transaction-pay-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add `fiatPayment` transaction state into `transactionData` and `updateFiatPayment` atomic patch action, including defaults initialization and payment-token reset behavior ([#8093](https://github.com/MetaMask/core/pull/8093))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor, is everything after the comma needed?


## [16.1.2]

### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { TransactionMeta } from '@metamask/transaction-controller';
import type { Hex } from '@metamask/utils';

import { TransactionPayController } from '.';
import { updateFiatPayment } from './actions/update-fiat-payment';
import { updatePaymentToken } from './actions/update-payment-token';
import { TransactionPayStrategy } from './constants';
import { getMessengerMock } from './tests/messenger-mock';
Expand All @@ -16,6 +17,7 @@ import { updateQuotes } from './utils/quotes';
import { updateSourceAmounts } from './utils/source-amounts';
import { pollTransactionChanges } from './utils/transaction';

jest.mock('./actions/update-fiat-payment');
jest.mock('./actions/update-payment-token');
jest.mock('./utils/source-amounts');
jest.mock('./utils/quotes');
Expand All @@ -28,6 +30,7 @@ const TOKEN_ADDRESS_MOCK = '0xabc' as Hex;
const CHAIN_ID_MOCK = '0x1' as Hex;

describe('TransactionPayController', () => {
const updateFiatPaymentMock = jest.mocked(updateFiatPayment);
const updatePaymentTokenMock = jest.mocked(updatePaymentToken);
const updateSourceAmountsMock = jest.mocked(updateSourceAmounts);
const updateQuotesMock = jest.mocked(updateQuotes);
Expand Down Expand Up @@ -77,6 +80,48 @@ describe('TransactionPayController', () => {
});
});

describe('updateFiatPayment', () => {
it('calls util', () => {
createController().updateFiatPayment({
amount: '20',
selectedPaymentMethodId: '/payments/debit-credit-card',
transactionId: TRANSACTION_ID_MOCK,
});

expect(updateFiatPaymentMock).toHaveBeenCalledWith(
{
amount: '20',
selectedPaymentMethodId: '/payments/debit-credit-card',
transactionId: TRANSACTION_ID_MOCK,
},
{
messenger,
updateTransactionData: expect.any(Function),
},
);
});

it('is callable via messenger action handler', () => {
createController();

messenger.call('TransactionPayController:updateFiatPayment', {
amount: '15',
transactionId: TRANSACTION_ID_MOCK,
});

expect(updateFiatPaymentMock).toHaveBeenCalledWith(
{
amount: '15',
transactionId: TRANSACTION_ID_MOCK,
},
{
messenger,
updateTransactionData: expect.any(Function),
},
);
});
});

describe('setTransactionConfig', () => {
it('updates isMaxAmount in state', () => {
const controller = createController();
Expand Down Expand Up @@ -271,6 +316,11 @@ describe('TransactionPayController', () => {
expect(
controller.state.transactionData[TRANSACTION_ID_MOCK],
).toStrictEqual({
fiatPayment: {
amount: null,
quickBuyOrderId: null,
selectedPaymentMethodId: null,
},
isLoading: false,
sourceAmounts: [{ sourceAmountHuman: '1.23' }],
tokens: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { TransactionMeta } from '@metamask/transaction-controller';
import type { Draft } from 'immer';
import { noop } from 'lodash';

import { updateFiatPayment } from './actions/update-fiat-payment';
import { updatePaymentToken } from './actions/update-payment-token';
import {
CONTROLLER_NAME,
Expand All @@ -18,6 +19,7 @@ import type {
TransactionPayControllerMessenger,
TransactionPayControllerOptions,
TransactionPayControllerState,
UpdateFiatPaymentRequest,
UpdatePaymentTokenRequest,
} from './types';
import { getStrategyOrder } from './utils/feature-flags';
Expand Down Expand Up @@ -111,6 +113,13 @@ export class TransactionPayController extends BaseController<
});
}

updateFiatPayment(request: UpdateFiatPaymentRequest): void {
updateFiatPayment(request, {
messenger: this.messenger,
updateTransactionData: this.#updateTransactionData.bind(this),
});
}

#removeTransactionData(transactionId: string): void {
this.update((state) => {
delete state.transactionData[transactionId];
Expand All @@ -133,6 +142,11 @@ export class TransactionPayController extends BaseController<

if (!current) {
transactionData[transactionId] = {
fiatPayment: {
amount: null,
quickBuyOrderId: null,
selectedPaymentMethodId: null,
},
isLoading: false,
tokens: [],
};
Expand Down Expand Up @@ -195,6 +209,11 @@ export class TransactionPayController extends BaseController<
'TransactionPayController:updatePaymentToken',
this.updatePaymentToken.bind(this),
);

this.messenger.registerActionHandler(
'TransactionPayController:updateFiatPayment',
this.updateFiatPayment.bind(this),
);
}

#getStrategiesWithFallback(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import type { TransactionMeta } from '@metamask/transaction-controller';
import { noop } from 'lodash';

import { updateFiatPayment } from './update-fiat-payment';
import type { TransactionData } from '../types';
import { getTransaction } from '../utils/transaction';

jest.mock('../utils/transaction');

const TRANSACTION_ID_MOCK = '123-456';
const FROM_MOCK = '0x456';

describe('Update Fiat Payment Action', () => {
const getTransactionMock = jest.mocked(getTransaction);

beforeEach(() => {
jest.resetAllMocks();

getTransactionMock.mockReturnValue({
id: TRANSACTION_ID_MOCK,
txParams: { from: FROM_MOCK },
} as TransactionMeta);
});

it('updates only selected payment method id', () => {
const updateTransactionDataMock = jest.fn();

updateFiatPayment(
{
selectedPaymentMethodId: '/payments/debit-credit-card',
transactionId: TRANSACTION_ID_MOCK,
},
{
messenger: {} as never,
updateTransactionData: updateTransactionDataMock,
},
);

expect(updateTransactionDataMock).toHaveBeenCalledTimes(1);

const transactionDataMock = {
fiatPayment: {
amount: '20',
quickBuyOrderId: '/providers/transak/orders/order-id-1',
selectedPaymentMethodId: '/payments/bank-transfer',
},
} as TransactionData;

updateTransactionDataMock.mock.calls[0][1](transactionDataMock);

expect(transactionDataMock.fiatPayment).toStrictEqual({
amount: '20',
quickBuyOrderId: '/providers/transak/orders/order-id-1',
selectedPaymentMethodId: '/payments/debit-credit-card',
});
});

it('updates amount without resetting other fiat fields', () => {
const updateTransactionDataMock = jest.fn();

updateFiatPayment(
{
amount: '100',
transactionId: TRANSACTION_ID_MOCK,
},
{
messenger: {} as never,
updateTransactionData: updateTransactionDataMock,
},
);

const transactionDataMock = {
fiatPayment: {
amount: '20',
quickBuyOrderId: '/providers/transak/orders/order-id-1',
selectedPaymentMethodId: '/payments/debit-credit-card',
},
} as TransactionData;

updateTransactionDataMock.mock.calls[0][1](transactionDataMock);

expect(transactionDataMock.fiatPayment).toStrictEqual({
amount: '100',
quickBuyOrderId: '/providers/transak/orders/order-id-1',
selectedPaymentMethodId: '/payments/debit-credit-card',
});
});

it('initializes fiat payment state when missing', () => {
const updateTransactionDataMock = jest.fn();

updateFiatPayment(
{
amount: '5',
quickBuyOrderId: '/providers/transak/orders/order-id-2',
transactionId: TRANSACTION_ID_MOCK,
},
{
messenger: {} as never,
updateTransactionData: updateTransactionDataMock,
},
);

const transactionDataMock = {} as TransactionData;
updateTransactionDataMock.mock.calls[0][1](transactionDataMock);

expect(transactionDataMock.fiatPayment).toStrictEqual({
amount: '5',
quickBuyOrderId: '/providers/transak/orders/order-id-2',
selectedPaymentMethodId: null,
});
});

it('supports clearing fields with null values', () => {
const updateTransactionDataMock = jest.fn();

updateFiatPayment(
{
selectedPaymentMethodId: null,
transactionId: TRANSACTION_ID_MOCK,
},
{
messenger: {} as never,
updateTransactionData: updateTransactionDataMock,
},
);

const transactionDataMock = {
fiatPayment: {
amount: '20',
quickBuyOrderId: '/providers/transak/orders/order-id-1',
selectedPaymentMethodId: '/payments/debit-credit-card',
},
} as TransactionData;

updateTransactionDataMock.mock.calls[0][1](transactionDataMock);

expect(transactionDataMock.fiatPayment).toStrictEqual({
amount: '20',
quickBuyOrderId: '/providers/transak/orders/order-id-1',
selectedPaymentMethodId: null,
});
});

it('throws if transaction not found', () => {
getTransactionMock.mockReturnValue(undefined);

expect(() =>
updateFiatPayment(
{
amount: '10',
transactionId: TRANSACTION_ID_MOCK,
},
{
messenger: {} as never,
updateTransactionData: noop,
},
),
).toThrow('Transaction not found');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { createModuleLogger } from '@metamask/utils';
import { pickBy } from 'lodash';

import type { TransactionPayControllerMessenger } from '..';
import { projectLogger } from '../logger';
import type {
UpdateFiatPaymentRequest,
UpdateTransactionDataCallback,
} from '../types';
import { getTransaction } from '../utils/transaction';

const log = createModuleLogger(projectLogger, 'update-fiat-payment');

export type UpdateFiatPaymentOptions = {
messenger: TransactionPayControllerMessenger;
updateTransactionData: UpdateTransactionDataCallback;
};

/**
* Update fiat payment state for a specific transaction.
*
* @param request - Request parameters.
* @param options - Options bag.
*/
export function updateFiatPayment(
request: UpdateFiatPaymentRequest,
options: UpdateFiatPaymentOptions,
): void {
const { transactionId, selectedPaymentMethodId, amount, quickBuyOrderId } =
request;
const { messenger, updateTransactionData } = options;

const transaction = getTransaction(transactionId, messenger);

if (!transaction) {
throw new Error('Transaction not found');
}

log('Updated fiat payment', {
transactionId,
selectedPaymentMethodId,
amount,
quickBuyOrderId,
});

updateTransactionData(transactionId, (data) => {
const currentFiatPayment = data.fiatPayment ?? {
amount: null,
quickBuyOrderId: null,
selectedPaymentMethodId: null,
};

const patch = pickBy(
Copy link
Member

@matthewwalsh0 matthewwalsh0 Mar 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than doing a pickBy which has implicit overwrite behaviour, should we go the callback route like in updateTransactionConfig so it's explicit and totally flexible?

{
amount,
quickBuyOrderId,
selectedPaymentMethodId,
},
(value) => value !== undefined,
) as Partial<typeof currentFiatPayment>;

data.fiatPayment = {
...currentFiatPayment,
...patch,
};
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ describe('Update Payment Token Action', () => {
decimals: 6,
symbol: 'TST',
});

expect(transactionDataMock.fiatPayment).toStrictEqual({
amount: null,
quickBuyOrderId: null,
selectedPaymentMethodId: null,
});
});

it('throws if token info not found', () => {
Expand Down
Loading
Loading