From a7beee47b7110ec88cb6a72a880735009d9848c4 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 3 Mar 2026 14:50:53 +0100 Subject: [PATCH 1/3] Add fiatPayment into transactionData --- .../transaction-pay-controller/CHANGELOG.md | 4 + .../src/TransactionPayController.test.ts | 50 ++++++ .../src/TransactionPayController.ts | 19 +++ .../src/actions/update-fiat-payment.test.ts | 161 ++++++++++++++++++ .../src/actions/update-fiat-payment.ts | 67 ++++++++ .../src/actions/update-payment-token.test.ts | 6 + .../src/actions/update-payment-token.ts | 5 + .../transaction-pay-controller/src/index.ts | 3 + .../transaction-pay-controller/src/types.ts | 37 ++++ 9 files changed, 352 insertions(+) create mode 100644 packages/transaction-pay-controller/src/actions/update-fiat-payment.test.ts create mode 100644 packages/transaction-pay-controller/src/actions/update-fiat-payment.ts diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index a122ad557a5..eb882d95d1b 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -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 ([#](https://github.com/MetaMask/core/pull/)) + ## [16.1.2] ### Fixed diff --git a/packages/transaction-pay-controller/src/TransactionPayController.test.ts b/packages/transaction-pay-controller/src/TransactionPayController.test.ts index fc6f7faff78..a16bd18f1ea 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.test.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.test.ts @@ -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'; @@ -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'); @@ -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); @@ -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(); @@ -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: [], diff --git a/packages/transaction-pay-controller/src/TransactionPayController.ts b/packages/transaction-pay-controller/src/TransactionPayController.ts index 7b8b2f20ed5..8777475720a 100644 --- a/packages/transaction-pay-controller/src/TransactionPayController.ts +++ b/packages/transaction-pay-controller/src/TransactionPayController.ts @@ -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, @@ -18,6 +19,7 @@ import type { TransactionPayControllerMessenger, TransactionPayControllerOptions, TransactionPayControllerState, + UpdateFiatPaymentRequest, UpdatePaymentTokenRequest, } from './types'; import { getStrategyOrder } from './utils/feature-flags'; @@ -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]; @@ -133,6 +142,11 @@ export class TransactionPayController extends BaseController< if (!current) { transactionData[transactionId] = { + fiatPayment: { + amount: null, + quickBuyOrderId: null, + selectedPaymentMethodId: null, + }, isLoading: false, tokens: [], }; @@ -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( diff --git a/packages/transaction-pay-controller/src/actions/update-fiat-payment.test.ts b/packages/transaction-pay-controller/src/actions/update-fiat-payment.test.ts new file mode 100644 index 00000000000..cd21285236b --- /dev/null +++ b/packages/transaction-pay-controller/src/actions/update-fiat-payment.test.ts @@ -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'); + }); +}); diff --git a/packages/transaction-pay-controller/src/actions/update-fiat-payment.ts b/packages/transaction-pay-controller/src/actions/update-fiat-payment.ts new file mode 100644 index 00000000000..a16b5aebe0e --- /dev/null +++ b/packages/transaction-pay-controller/src/actions/update-fiat-payment.ts @@ -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( + { + amount, + quickBuyOrderId, + selectedPaymentMethodId, + }, + (value) => value !== undefined, + ) as Partial; + + data.fiatPayment = { + ...currentFiatPayment, + ...patch, + }; + }); +} diff --git a/packages/transaction-pay-controller/src/actions/update-payment-token.test.ts b/packages/transaction-pay-controller/src/actions/update-payment-token.test.ts index 759370c66e3..2da8860bfc0 100644 --- a/packages/transaction-pay-controller/src/actions/update-payment-token.test.ts +++ b/packages/transaction-pay-controller/src/actions/update-payment-token.test.ts @@ -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', () => { diff --git a/packages/transaction-pay-controller/src/actions/update-payment-token.ts b/packages/transaction-pay-controller/src/actions/update-payment-token.ts index bd28290fe7f..bd27be33654 100644 --- a/packages/transaction-pay-controller/src/actions/update-payment-token.ts +++ b/packages/transaction-pay-controller/src/actions/update-payment-token.ts @@ -57,6 +57,11 @@ export function updatePaymentToken( updateTransactionData(transactionId, (data) => { data.paymentToken = paymentToken; + data.fiatPayment = { + amount: null, + quickBuyOrderId: null, + selectedPaymentMethodId: null, + }; }); } diff --git a/packages/transaction-pay-controller/src/index.ts b/packages/transaction-pay-controller/src/index.ts index 52426a80c7a..343aa5a3a3e 100644 --- a/packages/transaction-pay-controller/src/index.ts +++ b/packages/transaction-pay-controller/src/index.ts @@ -1,6 +1,7 @@ export type { TransactionConfig, TransactionConfigCallback, + TransactionFiatPayment, TransactionPayControllerActions, TransactionPayControllerEvents, TransactionPayControllerGetDelegationTransactionAction, @@ -9,6 +10,7 @@ export type { TransactionPayControllerMessenger, TransactionPayControllerOptions, TransactionPayControllerSetTransactionConfigAction, + TransactionPayControllerUpdateFiatPaymentAction, TransactionPayControllerState, TransactionPayControllerStateChangeEvent, TransactionPayControllerUpdatePaymentTokenAction, @@ -17,6 +19,7 @@ export type { TransactionPayRequiredToken, TransactionPaySourceAmount, TransactionPayTotals, + UpdateFiatPaymentRequest, UpdatePaymentTokenRequest, } from './types'; export { TransactionPayStrategy } from './constants'; diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 7ea554b7073..5434be532fb 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -83,6 +83,12 @@ export type TransactionPayControllerUpdatePaymentTokenAction = { handler: (request: UpdatePaymentTokenRequest) => void; }; +/** Action to update fiat payment state for a transaction. */ +export type TransactionPayControllerUpdateFiatPaymentAction = { + type: `${typeof CONTROLLER_NAME}:updateFiatPayment`; + handler: (request: UpdateFiatPaymentRequest) => void; +}; + /** Action to update transaction configuration using a callback. */ export type TransactionPayControllerSetTransactionConfigAction = { type: `${typeof CONTROLLER_NAME}:setTransactionConfig`; @@ -115,6 +121,7 @@ export type TransactionPayControllerActions = | TransactionPayControllerGetDelegationTransactionAction | TransactionPayControllerGetStateAction | TransactionPayControllerGetStrategyAction + | TransactionPayControllerUpdateFiatPaymentAction | TransactionPayControllerSetTransactionConfigAction | TransactionPayControllerUpdatePaymentTokenAction; @@ -153,6 +160,9 @@ export type TransactionPayControllerState = { /** State relating to a single transaction. */ export type TransactionData = { + /** Fiat payment method state. */ + fiatPayment?: TransactionFiatPayment; + /** Whether quotes are currently being retrieved. */ isLoading: boolean; @@ -191,6 +201,18 @@ export type TransactionData = { totals?: TransactionPayTotals; }; +/** Fiat payment state stored per transaction. */ +export type TransactionFiatPayment = { + /** Entered fiat amount for the selected payment method. */ + amount: string | null; + + /** Selected fiat payment method ID. */ + selectedPaymentMethodId: string | null; + + /** Quick-buy order ID in normalized format (/providers/{provider}/orders/{id}). */ + quickBuyOrderId: string | null; +}; + /** A token required by a transaction. */ export type TransactionPayRequiredToken = { /** Address of the required token. */ @@ -501,6 +523,21 @@ export type UpdatePaymentTokenRequest = { chainId: Hex; }; +/** Request to update fiat payment state for a transaction. */ +export type UpdateFiatPaymentRequest = { + /** Entered fiat amount. */ + amount?: string | null; + + /** Selected fiat payment method ID. */ + selectedPaymentMethodId?: string | null; + + /** ID of the transaction to update. */ + transactionId: string; + + /** Quick-buy order ID in normalized format (/providers/{provider}/orders/{id}). */ + quickBuyOrderId?: string | null; +}; + /** Callback to convert a transaction to a redeem delegation. */ export type GetDelegationTransactionCallback = ({ transaction, From 7f6259cbe05ce31fa4ad7ca6df43285635be2a90 Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 3 Mar 2026 14:54:21 +0100 Subject: [PATCH 2/3] Update changelog --- packages/transaction-pay-controller/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/CHANGELOG.md b/packages/transaction-pay-controller/CHANGELOG.md index eb882d95d1b..86a1f20b2e6 100644 --- a/packages/transaction-pay-controller/CHANGELOG.md +++ b/packages/transaction-pay-controller/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `fiatPayment` transaction state into `transactionData` and `updateFiatPayment` atomic patch action, including defaults initialization and payment-token reset behavior ([#](https://github.com/MetaMask/core/pull/)) +- 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)) ## [16.1.2] From d8c2b71c745c2db8484844a82b0eba8dfce0bdcf Mon Sep 17 00:00:00 2001 From: Goktug Poyraz Date: Tue, 3 Mar 2026 14:56:25 +0100 Subject: [PATCH 3/3] Fix order --- packages/transaction-pay-controller/src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/transaction-pay-controller/src/types.ts b/packages/transaction-pay-controller/src/types.ts index 5434be532fb..84d9298edd8 100644 --- a/packages/transaction-pay-controller/src/types.ts +++ b/packages/transaction-pay-controller/src/types.ts @@ -121,8 +121,8 @@ export type TransactionPayControllerActions = | TransactionPayControllerGetDelegationTransactionAction | TransactionPayControllerGetStateAction | TransactionPayControllerGetStrategyAction - | TransactionPayControllerUpdateFiatPaymentAction | TransactionPayControllerSetTransactionConfigAction + | TransactionPayControllerUpdateFiatPaymentAction | TransactionPayControllerUpdatePaymentTokenAction; export type TransactionPayControllerEvents =