diff --git a/packages/ramps-controller/CHANGELOG.md b/packages/ramps-controller/CHANGELOG.md index 44131aa439f..5444a504222 100644 --- a/packages/ramps-controller/CHANGELOG.md +++ b/packages/ramps-controller/CHANGELOG.md @@ -9,9 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `doNotUpdateState` option to `ExecuteRequestOptions` to allow external consumers to use controller methods without updating state ([#7708](https://github.com/MetaMask/core/pull/7708)) - Add `hydrateState()` method to fetch providers and tokens for user region ([#7707](https://github.com/MetaMask/core/pull/7707)) - Add `countries` state to RampsController with 24 hour TTL caching ([#7707](https://github.com/MetaMask/core/pull/7707)) -- Add `SupportedActions` type for `{ buy: boolean; sell: boolean }` support info +- Add `SupportedActions` type for `{ buy: boolean; sell: boolean }` support info ([#7707](https://github.com/MetaMask/core/pull/7707)) ### Changed diff --git a/packages/ramps-controller/src/RampsController.test.ts b/packages/ramps-controller/src/RampsController.test.ts index 0a3158f618f..fb5e807f9e6 100644 --- a/packages/ramps-controller/src/RampsController.test.ts +++ b/packages/ramps-controller/src/RampsController.test.ts @@ -23,7 +23,7 @@ import type { RampsServiceGetProvidersAction, RampsServiceGetPaymentMethodsAction, } from './RampsService-method-action-types'; -import { RequestStatus } from './RequestCache'; +import { RequestStatus, createCacheKey } from './RequestCache'; describe('RampsController', () => { describe('constructor', () => { @@ -365,6 +365,24 @@ describe('RampsController', () => { ); }); }); + + it('does not update state when doNotUpdateState is true', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getProviders', + async () => ({ providers: mockProviders }), + ); + + expect(controller.state.providers).toStrictEqual([]); + + const result = await controller.getProviders('us', { + doNotUpdateState: true, + }); + + expect(result.providers).toStrictEqual(mockProviders); + expect(controller.state.providers).toStrictEqual([]); + }); + }); }); describe('metadata', () => { @@ -1028,6 +1046,42 @@ describe('RampsController', () => { expect(callCount).toBe(1); }); }); + + it('does not update state when doNotUpdateState is true', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => mockCountries, + ); + + expect(controller.state.countries).toStrictEqual([]); + + const countries = await controller.getCountries({ + doNotUpdateState: true, + }); + + expect(countries).toStrictEqual(mockCountries); + expect(controller.state.countries).toStrictEqual([]); + }); + }); + + it('still updates request cache when doNotUpdateState is true', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => mockCountries, + ); + + await controller.getCountries({ doNotUpdateState: true }); + + const cacheKey = createCacheKey('getCountries', []); + const requestState = controller.getRequestState(cacheKey); + + expect(requestState).toBeDefined(); + expect(requestState?.status).toBe(RequestStatus.SUCCESS); + expect(requestState?.data).toStrictEqual(mockCountries); + }); + }); }); describe('init', () => { @@ -2338,6 +2392,24 @@ describe('RampsController', () => { expect(callCount).toBe(2); }); }); + + it('does not update state when doNotUpdateState is true', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getTokens', + async () => mockTokens, + ); + + expect(controller.state.tokens).toBeNull(); + + const tokens = await controller.getTokens('us', 'buy', { + doNotUpdateState: true, + }); + + expect(tokens).toStrictEqual(mockTokens); + expect(controller.state.tokens).toBeNull(); + }); + }); }); describe('getPaymentMethods', () => { @@ -2589,6 +2661,38 @@ describe('RampsController', () => { }, ); }); + + it('does not update state when doNotUpdateState is true', async () => { + await withController( + { + options: { + state: { + userRegion: createMockUserRegion('us'), + }, + }, + }, + async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getPaymentMethods', + async () => mockPaymentMethodsResponse, + ); + + expect(controller.state.paymentMethods).toStrictEqual([]); + + const response = await controller.getPaymentMethods({ + assetId: 'eip155:1/slip44:60', + provider: '/providers/stripe', + doNotUpdateState: true, + }); + + expect(response.payments).toStrictEqual([ + mockPaymentMethod1, + mockPaymentMethod2, + ]); + expect(controller.state.paymentMethods).toStrictEqual([]); + }, + ); + }); }); describe('setSelectedPaymentMethod', () => { diff --git a/packages/ramps-controller/src/RampsController.ts b/packages/ramps-controller/src/RampsController.ts index 1ea76631fbd..615121f7a5e 100644 --- a/packages/ramps-controller/src/RampsController.ts +++ b/packages/ramps-controller/src/RampsController.ts @@ -699,9 +699,11 @@ export class RampsController extends BaseController< options, ); - this.update((state) => { - state.countries = countries; - }); + if (!options?.doNotUpdateState) { + this.update((state) => { + state.countries = countries; + }); + } return countries; } @@ -753,13 +755,18 @@ export class RampsController extends BaseController< options, ); - this.update((state) => { - const userRegionCode = state.userRegion?.regionCode; + if (!options?.doNotUpdateState) { + this.update((state) => { + const userRegionCode = state.userRegion?.regionCode; - if (userRegionCode === undefined || userRegionCode === normalizedRegion) { - state.tokens = tokens; - } - }); + if ( + userRegionCode === undefined || + userRegionCode === normalizedRegion + ) { + state.tokens = tokens; + } + }); + } return tokens; } @@ -819,13 +826,18 @@ export class RampsController extends BaseController< options, ); - this.update((state) => { - const userRegionCode = state.userRegion?.regionCode; + if (!options?.doNotUpdateState) { + this.update((state) => { + const userRegionCode = state.userRegion?.regionCode; - if (userRegionCode === undefined || userRegionCode === normalizedRegion) { - state.providers = providers; - } - }); + if ( + userRegionCode === undefined || + userRegionCode === normalizedRegion + ) { + state.providers = providers; + } + }); + } return { providers }; } @@ -841,6 +853,7 @@ export class RampsController extends BaseController< * @param options.provider - Provider ID path. * @param options.forceRefresh - Whether to bypass cache. * @param options.ttl - Custom TTL for this request. + * @param options.doNotUpdateState - If true, skip updating controller state (but still update request cache for deduplication). * @returns The payment methods response containing payments array. */ async getPaymentMethods(options: { @@ -850,6 +863,7 @@ export class RampsController extends BaseController< provider: string; forceRefresh?: boolean; ttl?: number; + doNotUpdateState?: boolean; }): Promise { const regionToUse = options.region ?? this.state.userRegion?.regionCode; const fiatToUse = options.fiat ?? this.state.userRegion?.country?.currency; @@ -885,22 +899,28 @@ export class RampsController extends BaseController< provider: options.provider, }); }, - { forceRefresh: options.forceRefresh, ttl: options.ttl }, + { + forceRefresh: options.forceRefresh, + ttl: options.ttl, + doNotUpdateState: options.doNotUpdateState, + }, ); - this.update((state) => { - state.paymentMethods = response.payments; - // Only clear selected payment method if it's no longer in the new list - // This preserves the selection when cached data is returned (same context) - if ( - state.selectedPaymentMethod && - !response.payments.some( - (pm: PaymentMethod) => pm.id === state.selectedPaymentMethod?.id, - ) - ) { - state.selectedPaymentMethod = null; - } - }); + if (!options?.doNotUpdateState) { + this.update((state) => { + state.paymentMethods = response.payments; + // Only clear selected payment method if it's no longer in the new list + // This preserves the selection when cached data is returned (same context) + if ( + state.selectedPaymentMethod && + !response.payments.some( + (pm: PaymentMethod) => pm.id === state.selectedPaymentMethod?.id, + ) + ) { + state.selectedPaymentMethod = null; + } + }); + } return response; } @@ -992,6 +1012,7 @@ export class RampsController extends BaseController< * @param options.provider - Provider ID path. * @param options.forceRefresh - Whether to bypass cache. * @param options.ttl - Custom TTL for this request. + * @param options.doNotUpdateState - If true, skip updating controller state. */ triggerGetPaymentMethods(options: { region?: string; @@ -1000,6 +1021,7 @@ export class RampsController extends BaseController< provider: string; forceRefresh?: boolean; ttl?: number; + doNotUpdateState?: boolean; }): void { this.getPaymentMethods(options).catch(() => { // Error stored in state diff --git a/packages/ramps-controller/src/RequestCache.ts b/packages/ramps-controller/src/RequestCache.ts index 7abcea71727..7c2030d7303 100644 --- a/packages/ramps-controller/src/RequestCache.ts +++ b/packages/ramps-controller/src/RequestCache.ts @@ -135,6 +135,8 @@ export type ExecuteRequestOptions = { forceRefresh?: boolean; /** Custom TTL for this request in milliseconds */ ttl?: number; + /** If true, skip updating controller state (but still update request cache for deduplication) */ + doNotUpdateState?: boolean; }; /**