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
3 changes: 2 additions & 1 deletion packages/ramps-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
106 changes: 105 additions & 1 deletion packages/ramps-controller/src/RampsController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down
80 changes: 51 additions & 29 deletions packages/ramps-controller/src/RampsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 };
}
Expand All @@ -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: {
Expand All @@ -850,6 +863,7 @@ export class RampsController extends BaseController<
provider: string;
forceRefresh?: boolean;
ttl?: number;
doNotUpdateState?: boolean;
}): Promise<PaymentMethodsResponse> {
const regionToUse = options.region ?? this.state.userRegion?.regionCode;
const fiatToUse = options.fiat ?? this.state.userRegion?.country?.currency;
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/ramps-controller/src/RequestCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down
Loading