From 24add32acb13a3f776b0b6a14124c8cb0341c51d Mon Sep 17 00:00:00 2001 From: George Weiler Date: Thu, 29 Jan 2026 13:01:21 -0700 Subject: [PATCH 1/8] feat: adds loading state to ramps controller --- .../ramps-controller/src/RampsController.ts | 231 ++++++++++++++++-- packages/ramps-controller/src/RequestCache.ts | 12 + packages/ramps-controller/src/index.ts | 1 + 3 files changed, 230 insertions(+), 14 deletions(-) diff --git a/packages/ramps-controller/src/RampsController.ts b/packages/ramps-controller/src/RampsController.ts index c9953fbdfaf..903e3827a83 100644 --- a/packages/ramps-controller/src/RampsController.ts +++ b/packages/ramps-controller/src/RampsController.ts @@ -29,6 +29,7 @@ import type { RequestState, ExecuteRequestOptions, PendingRequest, + ResourceType, } from './RequestCache'; import { DEFAULT_REQUEST_CACHE_TTL, @@ -119,6 +120,46 @@ export type RampsControllerState = { * This stores loading, success, and error states for API requests. */ requests: RequestCacheType; + /** + * Whether user region is currently being fetched. + */ + userRegionLoading: boolean; + /** + * Error message if the user region fetch failed, or null. + */ + userRegionError: string | null; + /** + * Whether countries are currently being fetched. + */ + countriesLoading: boolean; + /** + * Error message if the countries fetch failed, or null. + */ + countriesError: string | null; + /** + * Whether providers are currently being fetched. + */ + providersLoading: boolean; + /** + * Error message if the providers fetch failed, or null. + */ + providersError: string | null; + /** + * Whether tokens are currently being fetched. + */ + tokensLoading: boolean; + /** + * Error message if the tokens fetch failed, or null. + */ + tokensError: string | null; + /** + * Whether payment methods are currently being fetched. + */ + paymentMethodsLoading: boolean; + /** + * Error message if the payment methods fetch failed, or null. + */ + paymentMethodsError: string | null; }; /** @@ -179,6 +220,66 @@ const rampsControllerMetadata = { includeInStateLogs: false, usedInUi: true, }, + userRegionLoading: { + persist: false, + includeInDebugSnapshot: true, + includeInStateLogs: false, + usedInUi: true, + }, + userRegionError: { + persist: false, + includeInDebugSnapshot: true, + includeInStateLogs: false, + usedInUi: true, + }, + countriesLoading: { + persist: false, + includeInDebugSnapshot: true, + includeInStateLogs: false, + usedInUi: true, + }, + countriesError: { + persist: false, + includeInDebugSnapshot: true, + includeInStateLogs: false, + usedInUi: true, + }, + providersLoading: { + persist: false, + includeInDebugSnapshot: true, + includeInStateLogs: false, + usedInUi: true, + }, + providersError: { + persist: false, + includeInDebugSnapshot: true, + includeInStateLogs: false, + usedInUi: true, + }, + tokensLoading: { + persist: false, + includeInDebugSnapshot: true, + includeInStateLogs: false, + usedInUi: true, + }, + tokensError: { + persist: false, + includeInDebugSnapshot: true, + includeInStateLogs: false, + usedInUi: true, + }, + paymentMethodsLoading: { + persist: false, + includeInDebugSnapshot: true, + includeInStateLogs: false, + usedInUi: true, + }, + paymentMethodsError: { + persist: false, + includeInDebugSnapshot: true, + includeInStateLogs: false, + usedInUi: true, + }, } satisfies StateMetadata; /** @@ -200,6 +301,16 @@ export function getDefaultRampsControllerState(): RampsControllerState { paymentMethods: [], selectedPaymentMethod: null, requests: {}, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, }; } @@ -427,10 +538,16 @@ export class RampsController extends BaseController< // Create abort controller for this request const abortController = new AbortController(); const lastFetchedAt = Date.now(); + const { resourceType } = options ?? {}; // Update state to loading this.#updateRequestState(cacheKey, createLoadingState()); + // Set resource-level loading state (only on cache miss) + if (resourceType) { + this.#setResourceLoading(resourceType, true); + } + // Create the fetch promise const promise = (async (): Promise => { try { @@ -445,6 +562,12 @@ export class RampsController extends BaseController< cacheKey, createSuccessState(data as Json, lastFetchedAt), ); + + // Clear error on success + if (resourceType) { + this.#setResourceError(resourceType, null); + } + return data; } catch (error) { // Don't update state if aborted @@ -452,12 +575,18 @@ export class RampsController extends BaseController< throw error; } - const errorMessage = (error as Error)?.message; + const errorMessage = (error as Error)?.message ?? 'Unknown error'; this.#updateRequestState( cacheKey, - createErrorState(errorMessage ?? 'Unknown error', lastFetchedAt), + createErrorState(errorMessage, lastFetchedAt), ); + + // Set resource-level error + if (resourceType) { + this.#setResourceError(resourceType, errorMessage); + } + throw error; } finally { // Only delete if this is still our entry (not replaced by a new request) @@ -465,6 +594,11 @@ export class RampsController extends BaseController< if (currentPending?.abortController === abortController) { this.#pendingRequests.delete(cacheKey); } + + // Clear resource-level loading state + if (resourceType) { + this.#setResourceLoading(resourceType, false); + } } })(); @@ -518,6 +652,62 @@ export class RampsController extends BaseController< }); } + /** + * Sets the loading state for a resource type. + * + * @param resourceType - The type of resource. + * @param loading - Whether the resource is loading. + */ + #setResourceLoading(resourceType: ResourceType, loading: boolean): void { + this.update((state) => { + switch (resourceType) { + case 'userRegion': + state.userRegionLoading = loading; + break; + case 'countries': + state.countriesLoading = loading; + break; + case 'providers': + state.providersLoading = loading; + break; + case 'tokens': + state.tokensLoading = loading; + break; + case 'paymentMethods': + state.paymentMethodsLoading = loading; + break; + } + }); + } + + /** + * Sets the error state for a resource type. + * + * @param resourceType - The type of resource. + * @param error - The error message, or null to clear. + */ + #setResourceError(resourceType: ResourceType, error: string | null): void { + this.update((state) => { + switch (resourceType) { + case 'userRegion': + state.userRegionError = error; + break; + case 'countries': + state.countriesError = error; + break; + case 'providers': + state.providersError = error; + break; + case 'tokens': + state.tokensError = error; + break; + case 'paymentMethods': + state.paymentMethodsError = error; + break; + } + }); + } + /** * Gets the state of a specific cached request. * @@ -709,18 +899,31 @@ export class RampsController extends BaseController< * @returns Promise that resolves when initialization is complete. */ async init(options?: ExecuteRequestOptions): Promise { - await this.getCountries(options); + this.#setResourceLoading('userRegion', true); - let regionCode = this.state.userRegion?.regionCode; - regionCode ??= await this.messenger.call('RampsService:getGeolocation'); + try { + await this.getCountries(options); - if (!regionCode) { - throw new Error( - 'Failed to fetch geolocation. Cannot initialize controller without valid region information.', + let regionCode = this.state.userRegion?.regionCode; + regionCode ??= await this.messenger.call('RampsService:getGeolocation'); + + if (!regionCode) { + throw new Error( + 'Failed to fetch geolocation. Cannot initialize controller without valid region information.', + ); + } + + await this.setUserRegion(regionCode, options); + this.#setResourceError('userRegion', null); + } catch (error) { + this.#setResourceError( + 'userRegion', + (error as Error)?.message ?? 'Unknown error', ); + throw error; + } finally { + this.#setResourceLoading('userRegion', false); } - - await this.setUserRegion(regionCode, options); } hydrateState(options?: ExecuteRequestOptions): void { @@ -751,7 +954,7 @@ export class RampsController extends BaseController< async () => { return this.messenger.call('RampsService:getCountries'); }, - options, + { ...options, resourceType: 'countries' }, ); this.update((state) => { @@ -805,7 +1008,7 @@ export class RampsController extends BaseController< }, ); }, - options, + { ...options, resourceType: 'tokens' }, ); this.update((state) => { @@ -924,7 +1127,7 @@ export class RampsController extends BaseController< }, ); }, - options, + { ...options, resourceType: 'providers' }, ); this.update((state) => { @@ -996,7 +1199,7 @@ export class RampsController extends BaseController< provider: providerToUse, }); }, - options, + { ...options, resourceType: 'paymentMethods' }, ); this.update((state) => { diff --git a/packages/ramps-controller/src/RequestCache.ts b/packages/ramps-controller/src/RequestCache.ts index 7abcea71727..0e2cf9a0208 100644 --- a/packages/ramps-controller/src/RequestCache.ts +++ b/packages/ramps-controller/src/RequestCache.ts @@ -1,5 +1,15 @@ import type { Json } from '@metamask/utils'; +/** + * Types of resources that can have loading/error states. + */ +export type ResourceType = + | 'userRegion' + | 'countries' + | 'providers' + | 'tokens' + | 'paymentMethods'; + /** * Status of a cached request. */ @@ -135,6 +145,8 @@ export type ExecuteRequestOptions = { forceRefresh?: boolean; /** Custom TTL for this request in milliseconds */ ttl?: number; + /** Resource type to update loading/error states for */ + resourceType?: ResourceType; }; /** diff --git a/packages/ramps-controller/src/index.ts b/packages/ramps-controller/src/index.ts index 7649f417d90..2c741e3f3bc 100644 --- a/packages/ramps-controller/src/index.ts +++ b/packages/ramps-controller/src/index.ts @@ -43,6 +43,7 @@ export type { RequestState, ExecuteRequestOptions, PendingRequest, + ResourceType, } from './RequestCache'; export { RequestStatus, From dbdbdfcfe9be27ddfca219f4dc0cd9ef534c5f2a Mon Sep 17 00:00:00 2001 From: George Weiler Date: Thu, 29 Jan 2026 13:15:42 -0700 Subject: [PATCH 2/8] feat: adds loading and error state to ramps controller --- .../src/RampsController.test.ts | 278 ++++-------------- .../ramps-controller/src/RampsController.ts | 121 ++------ 2 files changed, 76 insertions(+), 323 deletions(-) diff --git a/packages/ramps-controller/src/RampsController.test.ts b/packages/ramps-controller/src/RampsController.test.ts index bf304f041ab..d390a575f41 100644 --- a/packages/ramps-controller/src/RampsController.test.ts +++ b/packages/ramps-controller/src/RampsController.test.ts @@ -33,14 +33,24 @@ describe('RampsController', () => { expect(controller.state).toMatchInlineSnapshot(` Object { "countries": Array [], + "countriesError": null, + "countriesLoading": false, "paymentMethods": Array [], + "paymentMethodsError": null, + "paymentMethodsLoading": false, "providers": Array [], + "providersError": null, + "providersLoading": false, "requests": Object {}, "selectedPaymentMethod": null, "selectedProvider": null, "selectedToken": null, "tokens": null, + "tokensError": null, + "tokensLoading": false, "userRegion": null, + "userRegionError": null, + "userRegionLoading": false, } `); }); @@ -67,14 +77,24 @@ describe('RampsController', () => { expect(controller.state).toMatchInlineSnapshot(` Object { "countries": Array [], + "countriesError": null, + "countriesLoading": false, "paymentMethods": Array [], + "paymentMethodsError": null, + "paymentMethodsLoading": false, "providers": Array [], + "providersError": null, + "providersLoading": false, "requests": Object {}, "selectedPaymentMethod": null, "selectedProvider": null, "selectedToken": null, "tokens": null, + "tokensError": null, + "tokensLoading": false, "userRegion": null, + "userRegionError": null, + "userRegionLoading": false, } `); }); @@ -382,14 +402,24 @@ describe('RampsController', () => { ).toMatchInlineSnapshot(` Object { "countries": Array [], + "countriesError": null, + "countriesLoading": false, "paymentMethods": Array [], + "paymentMethodsError": null, + "paymentMethodsLoading": false, "providers": Array [], + "providersError": null, + "providersLoading": false, "requests": Object {}, "selectedPaymentMethod": null, "selectedProvider": null, "selectedToken": null, "tokens": null, + "tokensError": null, + "tokensLoading": false, "userRegion": null, + "userRegionError": null, + "userRegionLoading": false, } `); }); @@ -448,14 +478,24 @@ describe('RampsController', () => { ).toMatchInlineSnapshot(` Object { "countries": Array [], + "countriesError": null, + "countriesLoading": false, "paymentMethods": Array [], + "paymentMethodsError": null, + "paymentMethodsLoading": false, "providers": Array [], + "providersError": null, + "providersLoading": false, "requests": Object {}, "selectedPaymentMethod": null, "selectedProvider": null, "selectedToken": null, "tokens": null, + "tokensError": null, + "tokensLoading": false, "userRegion": null, + "userRegionError": null, + "userRegionLoading": false, } `); }); @@ -817,146 +857,6 @@ describe('RampsController', () => { }); }); - describe('sync trigger methods', () => { - describe('triggerSetUserRegion', () => { - it('triggers set user region and returns void', async () => { - await withController( - { - options: { - state: { - countries: createMockCountries(), - }, - }, - }, - async ({ controller, rootMessenger }) => { - rootMessenger.registerActionHandler( - 'RampsService:getTokens', - async () => ({ topTokens: [], allTokens: [] }), - ); - rootMessenger.registerActionHandler( - 'RampsService:getProviders', - async () => ({ providers: [] }), - ); - - const result = controller.triggerSetUserRegion('us-ca'); - expect(result).toBeUndefined(); - - await new Promise((resolve) => setTimeout(resolve, 10)); - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); - }, - ); - }); - - it('does not throw when set fails', async () => { - await withController(async ({ controller }) => { - expect(() => controller.triggerSetUserRegion('us-ca')).not.toThrow(); - }); - }); - }); - - describe('triggerGetCountries', () => { - it('triggers get countries and returns void', async () => { - await withController(async ({ controller, rootMessenger }) => { - rootMessenger.registerActionHandler( - 'RampsService:getCountries', - async () => createMockCountries(), - ); - - const result = controller.triggerGetCountries(); - expect(result).toBeUndefined(); - }); - }); - - it('does not throw when fetch fails', async () => { - await withController(async ({ controller, rootMessenger }) => { - rootMessenger.registerActionHandler( - 'RampsService:getCountries', - async () => { - throw new Error('countries failed'); - }, - ); - - expect(() => controller.triggerGetCountries()).not.toThrow(); - }); - }); - }); - - describe('triggerGetTokens', () => { - it('triggers get tokens and returns void', async () => { - await withController( - { options: { state: { userRegion: createMockUserRegion('us-ca') } } }, - async ({ controller, rootMessenger }) => { - rootMessenger.registerActionHandler( - 'RampsService:getTokens', - async () => ({ topTokens: [], allTokens: [] }), - ); - - const result = controller.triggerGetTokens(); - expect(result).toBeUndefined(); - - await new Promise((resolve) => setTimeout(resolve, 10)); - expect(controller.state.tokens).toStrictEqual({ - topTokens: [], - allTokens: [], - }); - }, - ); - }); - - it('does not throw when fetch fails', async () => { - await withController( - { options: { state: { userRegion: createMockUserRegion('us-ca') } } }, - async ({ controller, rootMessenger }) => { - rootMessenger.registerActionHandler( - 'RampsService:getTokens', - async () => { - throw new Error('tokens failed'); - }, - ); - - expect(() => controller.triggerGetTokens()).not.toThrow(); - }, - ); - }); - }); - - describe('triggerGetProviders', () => { - it('triggers get providers and returns void', async () => { - await withController( - { options: { state: { userRegion: createMockUserRegion('us-ca') } } }, - async ({ controller, rootMessenger }) => { - rootMessenger.registerActionHandler( - 'RampsService:getProviders', - async () => ({ providers: [] }), - ); - - const result = controller.triggerGetProviders(); - expect(result).toBeUndefined(); - - await new Promise((resolve) => setTimeout(resolve, 10)); - expect(controller.state.providers).toStrictEqual([]); - }, - ); - }); - - it('does not throw when fetch fails', async () => { - await withController( - { options: { state: { userRegion: createMockUserRegion('us-ca') } } }, - async ({ controller, rootMessenger }) => { - rootMessenger.registerActionHandler( - 'RampsService:getProviders', - async () => { - throw new Error('providers failed'); - }, - ); - - expect(() => controller.triggerGetProviders()).not.toThrow(); - }, - ); - }); - }); - }); - describe('getCountries', () => { const mockCountries: Country[] = [ { @@ -1188,6 +1088,22 @@ describe('RampsController', () => { ); }); }); + + it('sets userRegionError to Unknown error when error has no message', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => { + throw { code: 'ERR_NO_MESSAGE' }; + }, + ); + + await expect(controller.init()).rejects.toMatchObject({ + code: 'ERR_NO_MESSAGE', + }); + expect(controller.state.userRegionError).toBe('Unknown error'); + }); + }); }); describe('hydrateState', () => { @@ -3501,88 +3417,6 @@ describe('RampsController', () => { }); }); - describe('triggerGetPaymentMethods', () => { - const mockPaymentMethod: PaymentMethod = { - id: '/payments/debit-credit-card', - paymentType: 'debit-credit-card', - name: 'Debit or Credit', - score: 90, - icon: 'card', - }; - - const mockPaymentMethodsResponse: PaymentMethodsResponse = { - payments: [mockPaymentMethod], - }; - - const mockSelectedToken: RampsToken = { - assetId: 'eip155:1/slip44:60', - chainId: 'eip155:1', - name: 'Ethereum', - symbol: 'ETH', - decimals: 18, - iconUrl: 'https://example.com/eth.png', - tokenSupported: true, - }; - - const mockSelectedProvider: Provider = { - id: '/providers/stripe', - name: 'Stripe', - environmentType: 'PRODUCTION', - description: 'Stripe payment provider', - hqAddress: '123 Test St', - links: [], - logos: { - light: '/assets/stripe_light.png', - dark: '/assets/stripe_dark.png', - height: 24, - width: 77, - }, - }; - - it('calls getPaymentMethods without throwing', async () => { - await withController( - { - options: { - state: { - userRegion: createMockUserRegion('us-ca'), - selectedToken: mockSelectedToken, - selectedProvider: mockSelectedProvider, - }, - }, - }, - async ({ controller, rootMessenger }) => { - rootMessenger.registerActionHandler( - 'RampsService:getPaymentMethods', - async () => mockPaymentMethodsResponse, - ); - - controller.triggerGetPaymentMethods('us-ca', { - assetId: 'eip155:1/slip44:60', - provider: '/providers/stripe', - }); - - await new Promise((resolve) => setTimeout(resolve, 0)); - - expect(controller.state.paymentMethods).toStrictEqual([ - mockPaymentMethod, - ]); - }, - ); - }); - - it('does not throw when getPaymentMethods fails', async () => { - await withController(async ({ controller }) => { - expect(() => { - controller.triggerGetPaymentMethods('us-ca', { - assetId: 'eip155:1/slip44:60', - provider: '/providers/stripe', - }); - }).not.toThrow(); - - await new Promise((resolve) => setTimeout(resolve, 0)); - }); - }); - }); }); /** diff --git a/packages/ramps-controller/src/RampsController.ts b/packages/ramps-controller/src/RampsController.ts index 903e3827a83..38bf9775f46 100644 --- a/packages/ramps-controller/src/RampsController.ts +++ b/packages/ramps-controller/src/RampsController.ts @@ -652,6 +652,16 @@ export class RampsController extends BaseController< }); } + /** + * Executes a promise without awaiting, swallowing errors. + * Errors are stored in state via executeRequest. + * + * @param promise - The promise to execute. + */ + #fireAndForget(promise: Promise): void { + promise.catch((_error: unknown) => undefined); + } + /** * Sets the loading state for a resource type. * @@ -820,12 +830,11 @@ export class RampsController extends BaseController< state.userRegion = userRegion; }); - // Only trigger fetches if region changed or if data is missing if (regionChanged || !this.state.tokens) { - this.triggerGetTokens(userRegion.regionCode, 'buy', options); + this.#fireAndForget(this.getTokens(userRegion.regionCode, 'buy', options)); } if (regionChanged || this.state.providers.length === 0) { - this.triggerGetProviders(userRegion.regionCode, options); + this.#fireAndForget(this.getProviders(userRegion.regionCode, options)); } return userRegion; @@ -880,12 +889,9 @@ export class RampsController extends BaseController< state.selectedPaymentMethod = null; }); - // fetch payment methods for the new provider - // this is needed because you can change providers without changing the token - // (getPaymentMethods will use state as its default) - this.triggerGetPaymentMethods(regionCode, { - provider: provider.id, - }); + this.#fireAndForget( + this.getPaymentMethods(regionCode, { provider: provider.id }), + ); } /** @@ -934,8 +940,8 @@ export class RampsController extends BaseController< ); } - this.triggerGetTokens(regionCode, 'buy', options); - this.triggerGetProviders(regionCode, options); + this.#fireAndForget(this.getTokens(regionCode, 'buy', options)); + this.#fireAndForget(this.getProviders(regionCode, options)); } /** @@ -1070,9 +1076,9 @@ export class RampsController extends BaseController< state.selectedPaymentMethod = null; }); - this.triggerGetPaymentMethods(regionCode, { - assetId: token.assetId, - }); + this.#fireAndForget( + this.getPaymentMethods(regionCode, { assetId: token.assetId }), + ); } /** @@ -1264,91 +1270,4 @@ export class RampsController extends BaseController< }); } - // ============================================================ - // Sync Trigger Methods - // These fire-and-forget methods are for use in React effects. - // Errors are stored in state and available via selectors. - // ============================================================ - - /** - * Triggers setting the user region without throwing. - * - * @param region - The region code to set (e.g., "US-CA"). - * @param options - Options for cache behavior. - */ - triggerSetUserRegion(region: string, options?: ExecuteRequestOptions): void { - this.setUserRegion(region, options).catch(() => { - // Error stored in state - }); - } - - /** - * Triggers fetching countries without throwing. - * - * @param options - Options for cache behavior. - */ - triggerGetCountries(options?: ExecuteRequestOptions): void { - this.getCountries(options).catch(() => { - // Error stored in state - }); - } - - /** - * Triggers fetching tokens without throwing. - * - * @param region - The region code. If not provided, uses userRegion from state. - * @param action - The ramp action type ('buy' or 'sell'). - * @param options - Options for cache behavior. - */ - triggerGetTokens( - region?: string, - action: 'buy' | 'sell' = 'buy', - options?: ExecuteRequestOptions, - ): void { - this.getTokens(region, action, options).catch(() => { - // Error stored in state - }); - } - - /** - * Triggers fetching providers without throwing. - * - * @param region - The region code. If not provided, uses userRegion from state. - * @param options - Options for cache behavior and query filters. - */ - triggerGetProviders( - region?: string, - options?: ExecuteRequestOptions & { - provider?: string | string[]; - crypto?: string | string[]; - fiat?: string | string[]; - payments?: string | string[]; - }, - ): void { - this.getProviders(region, options).catch(() => { - // Error stored in state - }); - } - - /** - * Triggers fetching payment methods without throwing. - * - * @param region - User's region code (e.g., "us", "fr", "us-ny"). - * @param options - Query parameters for filtering payment methods. - * @param options.fiat - Fiat currency code. If not provided, uses userRegion currency. - * @param options.assetId - CAIP-19 cryptocurrency identifier. - * @param options.provider - Provider ID path. - */ - triggerGetPaymentMethods( - region?: string, - options?: ExecuteRequestOptions & { - fiat?: string; - assetId?: string; - provider?: string; - }, - ): void { - this.getPaymentMethods(region, options).catch(() => { - // Error stored in state - }); - } } From f9e4f089dea8ca9d0e7af814bd25826af160341b Mon Sep 17 00:00:00 2001 From: George Weiler Date: Thu, 29 Jan 2026 13:53:08 -0700 Subject: [PATCH 3/8] chore: changelog update --- packages/ramps-controller/CHANGELOG.md | 1 + .../ramps-controller/src/selectors.test.ts | 180 ++++++++++++++++++ 2 files changed, 181 insertions(+) diff --git a/packages/ramps-controller/CHANGELOG.md b/packages/ramps-controller/CHANGELOG.md index 72b29347f15..ba07f053a7a 100644 --- a/packages/ramps-controller/CHANGELOG.md +++ b/packages/ramps-controller/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add quotes functionality to RampsController ([#7747](https://github.com/MetaMask/core/pull/7747)) +- Add `quotesLoading` and `quotesError` state properties for quotes resource loading/error tracking ([#7779](https://github.com/MetaMask/core/pull/7779)) ## [5.0.0] diff --git a/packages/ramps-controller/src/selectors.test.ts b/packages/ramps-controller/src/selectors.test.ts index 30d21ada5eb..eaf033e59a3 100644 --- a/packages/ramps-controller/src/selectors.test.ts +++ b/packages/ramps-controller/src/selectors.test.ts @@ -33,6 +33,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': loadingRequest, }, @@ -69,6 +81,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': successRequest, }, @@ -108,6 +132,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': errorRequest, }, @@ -143,6 +179,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: {}, }, }; @@ -201,6 +249,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': successRequest, }, @@ -232,6 +292,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': successRequest1, }, @@ -252,6 +324,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': successRequest2, }, @@ -284,6 +368,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': successRequest, }, @@ -319,6 +415,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getData:[]': successRequest, }, @@ -353,6 +461,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': loadingRequest, }, @@ -375,6 +495,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': successRequest, }, @@ -405,6 +537,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': successRequest, }, @@ -426,6 +570,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': errorRequest, }, @@ -462,6 +618,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': createSuccessState( ['ETH'], @@ -502,6 +670,18 @@ describe('createRequestSelector', () => { paymentMethods: [], selectedPaymentMethod: null, quotes: null, + userRegionLoading: false, + userRegionError: null, + countriesLoading: false, + countriesError: null, + providersLoading: false, + providersError: null, + tokensLoading: false, + tokensError: null, + paymentMethodsLoading: false, + paymentMethodsError: null, + quotesLoading: false, + quotesError: null, requests: { 'getCryptoCurrencies:["US"]': createSuccessState( ['ETH'], From 1dfb0447d0a420982bf05007f89250f8d148702b Mon Sep 17 00:00:00 2001 From: George Weiler Date: Thu, 29 Jan 2026 14:01:52 -0700 Subject: [PATCH 4/8] chore: lint and 100 test coverage --- .../ramps-controller/src/RampsController.test.ts | 7 ++++++- packages/ramps-controller/src/RampsController.ts | 12 ++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/packages/ramps-controller/src/RampsController.test.ts b/packages/ramps-controller/src/RampsController.test.ts index 4df702512dc..5d78d6f5a0a 100644 --- a/packages/ramps-controller/src/RampsController.test.ts +++ b/packages/ramps-controller/src/RampsController.test.ts @@ -1106,10 +1106,15 @@ describe('RampsController', () => { it('sets userRegionError to Unknown error when error has no message', async () => { await withController(async ({ controller, rootMessenger }) => { + const errorWithoutMessage = Object.assign(new Error(), { + code: 'ERR_NO_MESSAGE', + message: undefined, + }) as Error & { code: string }; + rootMessenger.registerActionHandler( 'RampsService:getCountries', async () => { - throw { code: 'ERR_NO_MESSAGE' }; + throw errorWithoutMessage; }, ); diff --git a/packages/ramps-controller/src/RampsController.ts b/packages/ramps-controller/src/RampsController.ts index 829592484a7..b38c64fc7fa 100644 --- a/packages/ramps-controller/src/RampsController.ts +++ b/packages/ramps-controller/src/RampsController.ts @@ -704,7 +704,7 @@ export class RampsController extends BaseController< * * @param promise - The promise to execute. */ - #fireAndForget(promise: Promise): void { + #fireAndForget(promise: Promise): void { promise.catch((_error: unknown) => undefined); } @@ -735,6 +735,9 @@ export class RampsController extends BaseController< case 'quotes': state.quotesLoading = loading; break; + /* istanbul ignore next: exhaustive switch */ + default: + break; } }); } @@ -766,6 +769,9 @@ export class RampsController extends BaseController< case 'quotes': state.quotesError = error; break; + /* istanbul ignore next: exhaustive switch */ + default: + break; } }); } @@ -884,7 +890,9 @@ export class RampsController extends BaseController< }); if (regionChanged || !this.state.tokens) { - this.#fireAndForget(this.getTokens(userRegion.regionCode, 'buy', options)); + this.#fireAndForget( + this.getTokens(userRegion.regionCode, 'buy', options), + ); } if (regionChanged || this.state.providers.length === 0) { this.#fireAndForget(this.getProviders(userRegion.regionCode, options)); From b00fa9bd0d8686d62babe9eaf48d9ac2d699abb2 Mon Sep 17 00:00:00 2001 From: George Weiler Date: Thu, 29 Jan 2026 15:01:37 -0700 Subject: [PATCH 5/8] feat: update ramp controller state to use nested resource objects --- .../ramps-controller/src/RampsController.ts | 415 +++++++----------- packages/ramps-controller/src/index.ts | 3 + 2 files changed, 154 insertions(+), 264 deletions(-) diff --git a/packages/ramps-controller/src/RampsController.ts b/packages/ramps-controller/src/RampsController.ts index b38c64fc7fa..27b5b623564 100644 --- a/packages/ramps-controller/src/RampsController.ts +++ b/packages/ramps-controller/src/RampsController.ts @@ -82,107 +82,69 @@ export type UserRegion = { }; /** - * Describes the shape of the state object for {@link RampsController}. + * Generic type for resource state that bundles data with loading/error states. + * @template TData - The type of the resource data + * @template TSelected - The type of the selected item (defaults to null for resources without selection) */ -export type RampsControllerState = { +export type ResourceState = { /** - * The user's selected region with full country and state objects. - * Initially set via geolocation fetch, but can be manually changed by the user. - * Once set (either via geolocation or manual selection), it will not be overwritten - * by subsequent geolocation fetches. + * The resource data. */ - userRegion: UserRegion | null; + data: TData; /** - * The user's selected provider. - * Can be manually set by the user. + * The currently selected item, or null if none selected. */ - selectedProvider: Provider | null; + selected: TSelected; /** - * List of countries available for ramp actions. + * Whether the resource is currently being fetched. */ - countries: Country[]; + isLoading: boolean; /** - * List of providers available for the current region. + * Error message if the fetch failed, or null. */ - providers: Provider[]; + error: string | null; +}; + +/** + * Describes the shape of the state object for {@link RampsController}. + */ +export type RampsControllerState = { + /** + * The user's region state with data, loading, and error. + * Data contains the full country and state objects. + * Initially set via geolocation fetch, but can be manually changed by the user. + */ + userRegion: ResourceState; /** - * Tokens fetched for the current region and action. - * Contains topTokens and allTokens arrays. + * Countries resource state with data, loading, and error. + * Data contains the list of countries available for ramp actions. */ - tokens: TokensResponse | null; + countries: ResourceState; /** - * The user's selected token. - * When set, automatically fetches and sets payment methods for that token. + * Providers resource state with data, selected, loading, and error. + * Data contains the list of providers available for the current region. */ - selectedToken: RampsToken | null; + providers: ResourceState; /** - * Payment methods available for the current context. - * Filtered by region, fiat, asset, and provider. + * Tokens resource state with data, selected, loading, and error. + * Data contains topTokens and allTokens arrays. */ - paymentMethods: PaymentMethod[]; + tokens: ResourceState; /** - * The user's selected payment method. - * Can be manually set by the user. + * Payment methods resource state with data, selected, loading, and error. + * Data contains payment methods filtered by region, fiat, asset, and provider. */ - selectedPaymentMethod: PaymentMethod | null; + paymentMethods: ResourceState; /** - * Quotes fetched for the current context. - * Contains quotes from multiple providers for the given parameters. + * Quotes resource state with data, loading, and error. + * Data contains quotes from multiple providers for the given parameters. */ - quotes: QuotesResponse | null; + quotes: ResourceState; /** * Cache of request states, keyed by cache key. * This stores loading, success, and error states for API requests. */ requests: RequestCacheType; - /** - * Whether user region is currently being fetched. - */ - userRegionLoading: boolean; - /** - * Error message if the user region fetch failed, or null. - */ - userRegionError: string | null; - /** - * Whether countries are currently being fetched. - */ - countriesLoading: boolean; - /** - * Error message if the countries fetch failed, or null. - */ - countriesError: string | null; - /** - * Whether providers are currently being fetched. - */ - providersLoading: boolean; - /** - * Error message if the providers fetch failed, or null. - */ - providersError: string | null; - /** - * Whether tokens are currently being fetched. - */ - tokensLoading: boolean; - /** - * Error message if the tokens fetch failed, or null. - */ - tokensError: string | null; - /** - * Whether payment methods are currently being fetched. - */ - paymentMethodsLoading: boolean; - /** - * Error message if the payment methods fetch failed, or null. - */ - paymentMethodsError: string | null; - /** - * Whether quotes are currently being fetched. - */ - quotesLoading: boolean; - /** - * Error message if the quotes fetch failed, or null. - */ - quotesError: string | null; }; /** @@ -195,12 +157,6 @@ const rampsControllerMetadata = { includeInStateLogs: true, usedInUi: true, }, - selectedProvider: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: true, - usedInUi: true, - }, countries: { persist: true, includeInDebugSnapshot: true, @@ -219,24 +175,12 @@ const rampsControllerMetadata = { includeInStateLogs: true, usedInUi: true, }, - selectedToken: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: true, - usedInUi: true, - }, paymentMethods: { persist: false, includeInDebugSnapshot: true, includeInStateLogs: true, usedInUi: true, }, - selectedPaymentMethod: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: true, - usedInUi: true, - }, quotes: { persist: false, includeInDebugSnapshot: true, @@ -249,80 +193,29 @@ const rampsControllerMetadata = { includeInStateLogs: false, usedInUi: true, }, - userRegionLoading: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: false, - usedInUi: true, - }, - userRegionError: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: false, - usedInUi: true, - }, - countriesLoading: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: false, - usedInUi: true, - }, - countriesError: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: false, - usedInUi: true, - }, - providersLoading: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: false, - usedInUi: true, - }, - providersError: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: false, - usedInUi: true, - }, - tokensLoading: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: false, - usedInUi: true, - }, - tokensError: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: false, - usedInUi: true, - }, - paymentMethodsLoading: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: false, - usedInUi: true, - }, - paymentMethodsError: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: false, - usedInUi: true, - }, - quotesLoading: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: false, - usedInUi: true, - }, - quotesError: { - persist: false, - includeInDebugSnapshot: true, - includeInStateLogs: false, - usedInUi: true, - }, } satisfies StateMetadata; +/** + * Creates a default resource state object. + * + * @template TData - The type of the resource data. + * @template TSelected - The type of the selected item. + * @param data - The initial data value. + * @param selected - The initial selected value. + * @returns A ResourceState object with default loading and error values. + */ +function createDefaultResourceState( + data: TData, + selected: TSelected = null as TSelected, +): ResourceState { + return { + data, + selected, + isLoading: false, + error: null, + }; +} + /** * Constructs the default {@link RampsController} state. This allows * consumers to provide a partial state object when initializing the controller @@ -333,28 +226,22 @@ const rampsControllerMetadata = { */ export function getDefaultRampsControllerState(): RampsControllerState { return { - userRegion: null, - selectedProvider: null, - countries: [], - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, + userRegion: createDefaultResourceState(null), + countries: createDefaultResourceState([]), + providers: createDefaultResourceState( + [], + null, + ), + tokens: createDefaultResourceState( + null, + null, + ), + paymentMethods: createDefaultResourceState< + PaymentMethod[], + PaymentMethod | null + >([], null), + quotes: createDefaultResourceState(null), requests: {}, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, }; } @@ -687,14 +574,14 @@ export class RampsController extends BaseController< #cleanupState(): void { this.update((state) => { - state.userRegion = null; - state.selectedProvider = null; - state.selectedToken = null; - state.tokens = null; - state.providers = []; - state.paymentMethods = []; - state.selectedPaymentMethod = null; - state.quotes = null; + state.userRegion.data = null; + state.providers.selected = null; + state.tokens.selected = null; + state.tokens.data = null; + state.providers.data = []; + state.paymentMethods.data = []; + state.paymentMethods.selected = null; + state.quotes.data = null; }); } @@ -718,22 +605,22 @@ export class RampsController extends BaseController< this.update((state) => { switch (resourceType) { case 'userRegion': - state.userRegionLoading = loading; + state.userRegion.isLoading = loading; break; case 'countries': - state.countriesLoading = loading; + state.countries.isLoading = loading; break; case 'providers': - state.providersLoading = loading; + state.providers.isLoading = loading; break; case 'tokens': - state.tokensLoading = loading; + state.tokens.isLoading = loading; break; case 'paymentMethods': - state.paymentMethodsLoading = loading; + state.paymentMethods.isLoading = loading; break; case 'quotes': - state.quotesLoading = loading; + state.quotes.isLoading = loading; break; /* istanbul ignore next: exhaustive switch */ default: @@ -752,22 +639,22 @@ export class RampsController extends BaseController< this.update((state) => { switch (resourceType) { case 'userRegion': - state.userRegionError = error; + state.userRegion.error = error; break; case 'countries': - state.countriesError = error; + state.countries.error = error; break; case 'providers': - state.providersError = error; + state.providers.error = error; break; case 'tokens': - state.tokensError = error; + state.tokens.error = error; break; case 'paymentMethods': - state.paymentMethodsError = error; + state.paymentMethods.error = error; break; case 'quotes': - state.quotesError = error; + state.quotes.error = error; break; /* istanbul ignore next: exhaustive switch */ default: @@ -854,15 +741,15 @@ export class RampsController extends BaseController< const normalizedRegion = region.toLowerCase().trim(); try { - const { countries } = this.state; - if (!countries || countries.length === 0) { + const countriesData = this.state.countries.data; + if (!countriesData || countriesData.length === 0) { this.#cleanupState(); throw new Error( 'No countries found. Cannot set user region without valid country information.', ); } - const userRegion = findRegionFromCode(normalizedRegion, countries); + const userRegion = findRegionFromCode(normalizedRegion, countriesData); if (!userRegion) { this.#cleanupState(); @@ -873,28 +760,28 @@ export class RampsController extends BaseController< // Only cleanup state if region is actually changing const regionChanged = - normalizedRegion !== this.state.userRegion?.regionCode; + normalizedRegion !== this.state.userRegion.data?.regionCode; // Set the new region atomically with cleanup to avoid intermediate null state this.update((state) => { if (regionChanged) { - state.selectedProvider = null; - state.selectedToken = null; - state.tokens = null; - state.providers = []; - state.paymentMethods = []; - state.selectedPaymentMethod = null; - state.quotes = null; + state.providers.selected = null; + state.tokens.selected = null; + state.tokens.data = null; + state.providers.data = []; + state.paymentMethods.data = []; + state.paymentMethods.selected = null; + state.quotes.data = null; } - state.userRegion = userRegion; + state.userRegion.data = userRegion; }); - if (regionChanged || !this.state.tokens) { + if (regionChanged || !this.state.tokens.data) { this.#fireAndForget( this.getTokens(userRegion.regionCode, 'buy', options), ); } - if (regionChanged || this.state.providers.length === 0) { + if (regionChanged || this.state.providers.data.length === 0) { this.#fireAndForget(this.getProviders(userRegion.regionCode, options)); } @@ -916,21 +803,21 @@ export class RampsController extends BaseController< setSelectedProvider(providerId: string | null): void { if (providerId === null) { this.update((state) => { - state.selectedProvider = null; - state.paymentMethods = []; - state.selectedPaymentMethod = null; + state.providers.selected = null; + state.paymentMethods.data = []; + state.paymentMethods.selected = null; }); return; } - const regionCode = this.state.userRegion?.regionCode; + const regionCode = this.state.userRegion.data?.regionCode; if (!regionCode) { throw new Error( 'Region is required. Cannot set selected provider without valid region information.', ); } - const { providers } = this.state; + const providers = this.state.providers.data; if (!providers || providers.length === 0) { throw new Error( 'Providers not loaded. Cannot set selected provider before providers are fetched.', @@ -945,9 +832,9 @@ export class RampsController extends BaseController< } this.update((state) => { - state.selectedProvider = provider; - state.paymentMethods = []; - state.selectedPaymentMethod = null; + state.providers.selected = provider; + state.paymentMethods.data = []; + state.paymentMethods.selected = null; }); this.#fireAndForget( @@ -971,7 +858,7 @@ export class RampsController extends BaseController< try { await this.getCountries(options); - let regionCode = this.state.userRegion?.regionCode; + let regionCode = this.state.userRegion.data?.regionCode; regionCode ??= await this.messenger.call('RampsService:getGeolocation'); if (!regionCode) { @@ -994,7 +881,7 @@ export class RampsController extends BaseController< } hydrateState(options?: ExecuteRequestOptions): void { - const regionCode = this.state.userRegion?.regionCode; + const regionCode = this.state.userRegion.data?.regionCode; if (!regionCode) { throw new Error( 'Region code is required. Cannot hydrate state without valid region information.', @@ -1025,7 +912,7 @@ export class RampsController extends BaseController< ); this.update((state) => { - state.countries = countries; + state.countries.data = countries; }); return countries; @@ -1048,7 +935,7 @@ export class RampsController extends BaseController< provider?: string | string[]; }, ): Promise { - const regionToUse = region ?? this.state.userRegion?.regionCode; + const regionToUse = region ?? this.state.userRegion.data?.regionCode; if (!regionToUse) { throw new Error( @@ -1079,10 +966,10 @@ export class RampsController extends BaseController< ); this.update((state) => { - const userRegionCode = state.userRegion?.regionCode; + const userRegionCode = state.userRegion.data?.regionCode; if (userRegionCode === undefined || userRegionCode === normalizedRegion) { - state.tokens = tokens; + state.tokens.data = tokens; } }); @@ -1100,21 +987,21 @@ export class RampsController extends BaseController< setSelectedToken(assetId?: string): void { if (!assetId) { this.update((state) => { - state.selectedToken = null; - state.paymentMethods = []; - state.selectedPaymentMethod = null; + state.tokens.selected = null; + state.paymentMethods.data = []; + state.paymentMethods.selected = null; }); return; } - const regionCode = this.state.userRegion?.regionCode; + const regionCode = this.state.userRegion.data?.regionCode; if (!regionCode) { throw new Error( 'Region is required. Cannot set selected token without valid region information.', ); } - const { tokens } = this.state; + const tokens = this.state.tokens.data; if (!tokens) { throw new Error( 'Tokens not loaded. Cannot set selected token before tokens are fetched.', @@ -1132,9 +1019,9 @@ export class RampsController extends BaseController< } this.update((state) => { - state.selectedToken = token; - state.paymentMethods = []; - state.selectedPaymentMethod = null; + state.tokens.selected = token; + state.paymentMethods.data = []; + state.paymentMethods.selected = null; }); this.#fireAndForget( @@ -1163,7 +1050,7 @@ export class RampsController extends BaseController< payments?: string | string[]; }, ): Promise<{ providers: Provider[] }> { - const regionToUse = region ?? this.state.userRegion?.regionCode; + const regionToUse = region ?? this.state.userRegion.data?.regionCode; if (!regionToUse) { throw new Error( @@ -1198,10 +1085,10 @@ export class RampsController extends BaseController< ); this.update((state) => { - const userRegionCode = state.userRegion?.regionCode; + const userRegionCode = state.userRegion.data?.regionCode; if (userRegionCode === undefined || userRegionCode === normalizedRegion) { - state.providers = providers; + state.providers.data = providers; } }); @@ -1227,13 +1114,13 @@ export class RampsController extends BaseController< provider?: string; }, ): Promise { - const regionCode = region ?? this.state.userRegion?.regionCode ?? null; + const regionCode = region ?? this.state.userRegion.data?.regionCode ?? null; const fiatToUse = - options?.fiat ?? this.state.userRegion?.country?.currency ?? null; + options?.fiat ?? this.state.userRegion.data?.country?.currency ?? null; const assetIdToUse = - options?.assetId ?? this.state.selectedToken?.assetId ?? ''; + options?.assetId ?? this.state.tokens.selected?.assetId ?? ''; const providerToUse = - options?.provider ?? this.state.selectedProvider?.id ?? ''; + options?.provider ?? this.state.providers.selected?.id ?? ''; if (!regionCode) { throw new Error( @@ -1270,8 +1157,8 @@ export class RampsController extends BaseController< ); this.update((state) => { - const currentAssetId = state.selectedToken?.assetId ?? ''; - const currentProviderId = state.selectedProvider?.id ?? ''; + const currentAssetId = state.tokens.selected?.assetId ?? ''; + const currentProviderId = state.providers.selected?.id ?? ''; const tokenSelectionUnchanged = assetIdToUse === currentAssetId; const providerSelectionUnchanged = providerToUse === currentProviderId; @@ -1280,14 +1167,14 @@ export class RampsController extends BaseController< // ex: if the user rapidly changes the token or provider, the in-flight payment methods might not be valid // so this check will ensure that the payment methods are still valid for the token and provider that were requested if (tokenSelectionUnchanged && providerSelectionUnchanged) { - state.paymentMethods = response.payments; + state.paymentMethods.data = response.payments; // this will auto-select the first payment method if the selected payment method is not in the new payment methods const currentSelectionStillValid = response.payments.some( - (pm: PaymentMethod) => pm.id === state.selectedPaymentMethod?.id, + (pm: PaymentMethod) => pm.id === state.paymentMethods.selected?.id, ); if (!currentSelectionStillValid) { - state.selectedPaymentMethod = response.payments[0] ?? null; + state.paymentMethods.selected = response.payments[0] ?? null; } } }); @@ -1305,12 +1192,12 @@ export class RampsController extends BaseController< setSelectedPaymentMethod(paymentMethodId?: string): void { if (!paymentMethodId) { this.update((state) => { - state.selectedPaymentMethod = null; + state.paymentMethods.selected = null; }); return; } - const { paymentMethods } = this.state; + const paymentMethods = this.state.paymentMethods.data; if (!paymentMethods || paymentMethods.length === 0) { throw new Error( 'Payment methods not loaded. Cannot set selected payment method before payment methods are fetched.', @@ -1327,7 +1214,7 @@ export class RampsController extends BaseController< } this.update((state) => { - state.selectedPaymentMethod = paymentMethod; + state.paymentMethods.selected = paymentMethod; }); } @@ -1362,11 +1249,11 @@ export class RampsController extends BaseController< forceRefresh?: boolean; ttl?: number; }): Promise { - const regionToUse = options.region ?? this.state.userRegion?.regionCode; - const fiatToUse = options.fiat ?? this.state.userRegion?.country?.currency; + const regionToUse = options.region ?? this.state.userRegion.data?.regionCode; + const fiatToUse = options.fiat ?? this.state.userRegion.data?.country?.currency; const paymentMethodsToUse = options.paymentMethods ?? - this.state.paymentMethods.map((pm: PaymentMethod) => pm.id); + this.state.paymentMethods.data.map((pm: PaymentMethod) => pm.id); const action = options.action ?? 'buy'; if (!regionToUse) { @@ -1439,10 +1326,10 @@ export class RampsController extends BaseController< ); this.update((state) => { - const userRegionCode = state.userRegion?.regionCode; + const userRegionCode = state.userRegion.data?.regionCode; if (userRegionCode === undefined || userRegionCode === normalizedRegion) { - state.quotes = response; + state.quotes.data = response; } }); diff --git a/packages/ramps-controller/src/index.ts b/packages/ramps-controller/src/index.ts index 5d7a84d9d94..00089017be8 100644 --- a/packages/ramps-controller/src/index.ts +++ b/packages/ramps-controller/src/index.ts @@ -7,6 +7,7 @@ export type { RampsControllerStateChangeEvent, RampsControllerOptions, UserRegion, + ResourceState, } from './RampsController'; export { RampsController, @@ -34,6 +35,8 @@ export type { QuoteCustomAction, QuotesResponse, GetQuotesParams, + RampsToken, + TokensResponse, } from './RampsService'; export { RampsService, From 1f4cfbe81889aeddb316f87da85ffcd0d2039fca Mon Sep 17 00:00:00 2001 From: George Weiler Date: Thu, 29 Jan 2026 15:18:25 -0700 Subject: [PATCH 6/8] chore: controller test update --- .../src/RampsController.test.ts | 677 +++++++++--------- 1 file changed, 355 insertions(+), 322 deletions(-) diff --git a/packages/ramps-controller/src/RampsController.test.ts b/packages/ramps-controller/src/RampsController.test.ts index 5d78d6f5a0a..2d347d6fcc6 100644 --- a/packages/ramps-controller/src/RampsController.test.ts +++ b/packages/ramps-controller/src/RampsController.test.ts @@ -35,28 +35,43 @@ describe('RampsController', () => { await withController(({ controller }) => { expect(controller.state).toMatchInlineSnapshot(` Object { - "countries": Array [], - "countriesError": null, - "countriesLoading": false, - "paymentMethods": Array [], - "paymentMethodsError": null, - "paymentMethodsLoading": false, - "providers": Array [], - "providersError": null, - "providersLoading": false, - "quotes": null, - "quotesError": null, - "quotesLoading": false, + "countries": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "paymentMethods": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "providers": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "quotes": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, "requests": Object {}, - "selectedPaymentMethod": null, - "selectedProvider": null, - "selectedToken": null, - "tokens": null, - "tokensError": null, - "tokensLoading": false, - "userRegion": null, - "userRegionError": null, - "userRegionLoading": false, + "tokens": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, + "userRegion": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, } `); }); @@ -64,15 +79,15 @@ describe('RampsController', () => { it('accepts initial state', async () => { const givenState = { - userRegion: createMockUserRegion('us-ca'), + userRegion: createResourceState(createMockUserRegion('us-ca')), }; await withController( { options: { state: givenState } }, ({ controller }) => { - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); - expect(controller.state.selectedProvider).toBeNull(); - expect(controller.state.tokens).toBeNull(); + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); + expect(controller.state.providers.selected).toBeNull(); + expect(controller.state.tokens.data).toBeNull(); expect(controller.state.requests).toStrictEqual({}); }, ); @@ -82,28 +97,43 @@ describe('RampsController', () => { await withController({ options: { state: {} } }, ({ controller }) => { expect(controller.state).toMatchInlineSnapshot(` Object { - "countries": Array [], - "countriesError": null, - "countriesLoading": false, - "paymentMethods": Array [], - "paymentMethodsError": null, - "paymentMethodsLoading": false, - "providers": Array [], - "providersError": null, - "providersLoading": false, - "quotes": null, - "quotesError": null, - "quotesLoading": false, + "countries": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "paymentMethods": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "providers": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "quotes": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, "requests": Object {}, - "selectedPaymentMethod": null, - "selectedProvider": null, - "selectedToken": null, - "tokens": null, - "tokensError": null, - "tokensLoading": false, - "userRegion": null, - "userRegionError": null, - "userRegionLoading": false, + "tokens": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, + "userRegion": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, } `); }); @@ -111,7 +141,7 @@ describe('RampsController', () => { it('always resets requests cache on initialization', async () => { const givenState = { - userRegion: createMockUserRegion('us-ca'), + userRegion: createResourceState(createMockUserRegion('us-ca')), requests: { someKey: { status: RequestStatus.SUCCESS, @@ -176,12 +206,12 @@ describe('RampsController', () => { async (_regionCode: string) => ({ providers: mockProviders }), ); - expect(controller.state.providers).toStrictEqual([]); + expect(controller.state.providers.data).toStrictEqual([]); const result = await controller.getProviders('us-ca'); expect(result.providers).toStrictEqual(mockProviders); - expect(controller.state.providers).toStrictEqual(mockProviders); + expect(controller.state.providers.data).toStrictEqual(mockProviders); }); }); @@ -242,7 +272,7 @@ describe('RampsController', () => { it('uses userRegion from state when region is not provided', async () => { await withController( - { options: { state: { userRegion: createMockUserRegion('fr') } } }, + { options: { state: { userRegion: createResourceState(createMockUserRegion('fr')) } } }, async ({ controller, rootMessenger }) => { let receivedRegion: string | undefined; rootMessenger.registerActionHandler( @@ -262,7 +292,7 @@ describe('RampsController', () => { it('prefers provided region over userRegion in state', async () => { await withController( - { options: { state: { userRegion: createMockUserRegion('fr') } } }, + { options: { state: { userRegion: createResourceState(createMockUserRegion('fr')) } } }, async ({ controller, rootMessenger }) => { let receivedRegion: string | undefined; rootMessenger.registerActionHandler( @@ -282,7 +312,7 @@ describe('RampsController', () => { it('updates providers when userRegion matches the requested region', async () => { await withController( - { options: { state: { userRegion: createMockUserRegion('us-ca') } } }, + { options: { state: { userRegion: createResourceState(createMockUserRegion('us-ca')) } } }, async ({ controller, rootMessenger }) => { rootMessenger.registerActionHandler( 'RampsService:getProviders', @@ -292,12 +322,12 @@ describe('RampsController', () => { }, ); - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); - expect(controller.state.providers).toStrictEqual([]); + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); + expect(controller.state.providers.data).toStrictEqual([]); await controller.getProviders('US-ca'); - expect(controller.state.providers).toStrictEqual(mockProviders); + expect(controller.state.providers.data).toStrictEqual(mockProviders); }, ); }); @@ -324,8 +354,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - providers: existingProviders, + userRegion: createResourceState(createMockUserRegion('us-ca')), + providers: createResourceState(existingProviders, null), }, }, }, @@ -338,12 +368,12 @@ describe('RampsController', () => { }, ); - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); - expect(controller.state.providers).toStrictEqual(existingProviders); + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); + expect(controller.state.providers.data).toStrictEqual(existingProviders); await controller.getProviders('fr'); - expect(controller.state.providers).toStrictEqual(existingProviders); + expect(controller.state.providers.data).toStrictEqual(existingProviders); }, ); }); @@ -908,7 +938,7 @@ describe('RampsController', () => { async () => mockCountries, ); - expect(controller.state.countries).toStrictEqual([]); + expect(controller.state.countries.data).toStrictEqual([]); const countries = await controller.getCountries(); @@ -947,7 +977,7 @@ describe('RampsController', () => { }, ] `); - expect(controller.state.countries).toStrictEqual(mockCountries); + expect(controller.state.countries.data).toStrictEqual(mockCountries); }); }); }); @@ -966,8 +996,8 @@ describe('RampsController', () => { await controller.init(); - expect(controller.state.countries).toStrictEqual(createMockCountries()); - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); + expect(controller.state.countries.data).toStrictEqual(createMockCountries()); + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); }); }); @@ -977,7 +1007,7 @@ describe('RampsController', () => { { options: { state: { - userRegion: existingRegion, + userRegion: createResourceState(existingRegion), }, }, }, @@ -989,10 +1019,10 @@ describe('RampsController', () => { await controller.init(); - expect(controller.state.countries).toStrictEqual( + expect(controller.state.countries.data).toStrictEqual( createMockCountries(), ); - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); }, ); }); @@ -1037,11 +1067,10 @@ describe('RampsController', () => { { options: { state: { - countries: createMockCountries(), - userRegion: createMockUserRegion('us-ca'), - tokens: mockTokens, - providers: mockProviders, - selectedProvider: mockSelectedProvider, + countries: createResourceState(createMockCountries()), + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(mockTokens, null), + providers: createResourceState(mockProviders, mockSelectedProvider), }, }, }, @@ -1062,10 +1091,10 @@ describe('RampsController', () => { await controller.init(); // Verify persisted state is preserved - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); - expect(controller.state.tokens).toStrictEqual(mockTokens); - expect(controller.state.providers).toStrictEqual(mockProviders); - expect(controller.state.selectedProvider).toStrictEqual( + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); + expect(controller.state.tokens.data).toStrictEqual(mockTokens); + expect(controller.state.providers.data).toStrictEqual(mockProviders); + expect(controller.state.providers.selected).toStrictEqual( mockSelectedProvider, ); }, @@ -1121,7 +1150,7 @@ describe('RampsController', () => { await expect(controller.init()).rejects.toMatchObject({ code: 'ERR_NO_MESSAGE', }); - expect(controller.state.userRegionError).toBe('Unknown error'); + expect(controller.state.userRegion.error).toBe('Unknown error'); }); }); }); @@ -1132,7 +1161,7 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), + userRegion: createResourceState(createMockUserRegion('us-ca')), }, }, }, @@ -1180,7 +1209,7 @@ describe('RampsController', () => { { options: { state: { - countries: createMockCountries(), + countries: createResourceState(createMockCountries()), }, }, }, @@ -1196,9 +1225,9 @@ describe('RampsController', () => { await controller.setUserRegion('US-CA'); - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); - expect(controller.state.userRegion?.country.isoCode).toBe('US'); - expect(controller.state.userRegion?.state?.stateId).toBe('CA'); + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); + expect(controller.state.userRegion.data?.country.isoCode).toBe('US'); + expect(controller.state.userRegion.data?.state?.stateId).toBe('CA'); }, ); }); @@ -1231,7 +1260,7 @@ describe('RampsController', () => { { options: { state: { - countries: createMockCountries(), + countries: createResourceState(createMockCountries()), }, }, }, @@ -1261,22 +1290,22 @@ describe('RampsController', () => { await controller.getPaymentMethods('us-ca'); controller.setSelectedPaymentMethod(mockPaymentMethod.id); - expect(controller.state.tokens).toStrictEqual(mockTokens); - expect(controller.state.providers).toStrictEqual(mockProviders); - expect(controller.state.paymentMethods).toStrictEqual([ + expect(controller.state.tokens.data).toStrictEqual(mockTokens); + expect(controller.state.providers.data).toStrictEqual(mockProviders); + expect(controller.state.paymentMethods.data).toStrictEqual([ mockPaymentMethod, ]); - expect(controller.state.selectedPaymentMethod).toStrictEqual( + expect(controller.state.paymentMethods.selected).toStrictEqual( mockPaymentMethod, ); providersToReturn = []; await controller.setUserRegion('FR'); await new Promise((resolve) => setTimeout(resolve, 50)); - expect(controller.state.tokens).toStrictEqual(mockTokens); - expect(controller.state.providers).toStrictEqual([]); - expect(controller.state.paymentMethods).toStrictEqual([]); - expect(controller.state.selectedPaymentMethod).toBeNull(); + expect(controller.state.tokens.data).toStrictEqual(mockTokens); + expect(controller.state.providers.data).toStrictEqual([]); + expect(controller.state.paymentMethods.data).toStrictEqual([]); + expect(controller.state.paymentMethods.selected).toBeNull(); }, ); }); @@ -1321,11 +1350,10 @@ describe('RampsController', () => { { options: { state: { - countries: createMockCountries(), - userRegion: createMockUserRegion('us-ca'), - tokens: mockTokens, - providers: mockProviders, - selectedProvider: mockSelectedProvider, + countries: createResourceState(createMockCountries()), + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(mockTokens, null), + providers: createResourceState(mockProviders, mockSelectedProvider), }, }, }, @@ -1343,10 +1371,10 @@ describe('RampsController', () => { await controller.setUserRegion('US-ca'); // Verify persisted state is preserved - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); - expect(controller.state.tokens).toStrictEqual(mockTokens); - expect(controller.state.providers).toStrictEqual(mockProviders); - expect(controller.state.selectedProvider).toStrictEqual( + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); + expect(controller.state.tokens.data).toStrictEqual(mockTokens); + expect(controller.state.providers.data).toStrictEqual(mockProviders); + expect(controller.state.providers.selected).toStrictEqual( mockSelectedProvider, ); }, @@ -1402,12 +1430,10 @@ describe('RampsController', () => { { options: { state: { - countries: createMockCountries(), - userRegion: createMockUserRegion('us-ca'), - tokens: mockTokens, - providers: mockProviders, - selectedProvider: mockSelectedProvider, - selectedToken: mockSelectedToken, + countries: createResourceState(createMockCountries()), + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(mockTokens, mockSelectedToken), + providers: createResourceState(mockProviders, mockSelectedProvider), }, }, }, @@ -1425,11 +1451,11 @@ describe('RampsController', () => { await controller.setUserRegion('FR'); // Verify persisted state is cleared - expect(controller.state.userRegion?.regionCode).toBe('fr'); - expect(controller.state.tokens).toBeNull(); - expect(controller.state.providers).toStrictEqual([]); - expect(controller.state.selectedProvider).toBeNull(); - expect(controller.state.selectedToken).toBeNull(); + expect(controller.state.userRegion.data?.regionCode).toBe('fr'); + expect(controller.state.tokens.data).toBeNull(); + expect(controller.state.providers.data).toStrictEqual([]); + expect(controller.state.providers.selected).toBeNull(); + expect(controller.state.tokens.selected).toBeNull(); }, ); }); @@ -1458,7 +1484,7 @@ describe('RampsController', () => { { options: { state: { - countries: countriesWithId, + countries: createResourceState(countriesWithId), }, }, }, @@ -1474,8 +1500,8 @@ describe('RampsController', () => { await controller.setUserRegion('us-ca'); - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); - expect(controller.state.userRegion?.country.name).toBe( + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); + expect(controller.state.userRegion.data?.country.name).toBe( 'United States', ); }, @@ -1499,7 +1525,7 @@ describe('RampsController', () => { { options: { state: { - countries: countriesWithId, + countries: createResourceState(countriesWithId), }, }, }, @@ -1515,8 +1541,8 @@ describe('RampsController', () => { await controller.setUserRegion('fr'); - expect(controller.state.userRegion?.regionCode).toBe('fr'); - expect(controller.state.userRegion?.country.name).toBe('France'); + expect(controller.state.userRegion.data?.regionCode).toBe('fr'); + expect(controller.state.userRegion.data?.country.name).toBe('France'); }, ); }); @@ -1545,7 +1571,7 @@ describe('RampsController', () => { { options: { state: { - countries: countriesWithId, + countries: createResourceState(countriesWithId), }, }, }, @@ -1561,8 +1587,8 @@ describe('RampsController', () => { await controller.setUserRegion('us-ca'); - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); - expect(controller.state.userRegion?.country.name).toBe( + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); + expect(controller.state.userRegion.data?.country.name).toBe( 'United States', ); }, @@ -1585,7 +1611,7 @@ describe('RampsController', () => { { options: { state: { - countries, + countries: createResourceState(countries), }, }, }, @@ -1594,7 +1620,7 @@ describe('RampsController', () => { 'Region "xx" not found in countries data', ); - expect(controller.state.userRegion).toBeNull(); + expect(controller.state.userRegion.data).toBeNull(); }, ); }); @@ -1605,8 +1631,8 @@ describe('RampsController', () => { 'No countries found. Cannot set user region without valid country information.', ); - expect(controller.state.userRegion).toBeNull(); - expect(controller.state.tokens).toBeNull(); + expect(controller.state.userRegion.data).toBeNull(); + expect(controller.state.tokens.data).toBeNull(); }); }); @@ -1615,8 +1641,8 @@ describe('RampsController', () => { { options: { state: { - countries: [], - userRegion: createMockUserRegion('us-ca'), + countries: createResourceState([]), + userRegion: createResourceState(createMockUserRegion('us-ca')), }, }, }, @@ -1625,8 +1651,8 @@ describe('RampsController', () => { 'No countries found. Cannot set user region without valid country information.', ); - expect(controller.state.userRegion).toBeNull(); - expect(controller.state.tokens).toBeNull(); + expect(controller.state.userRegion.data).toBeNull(); + expect(controller.state.tokens.data).toBeNull(); }, ); }); @@ -1654,7 +1680,7 @@ describe('RampsController', () => { { options: { state: { - countries: countriesWithStateId, + countries: createResourceState(countriesWithStateId), }, }, }, @@ -1670,9 +1696,9 @@ describe('RampsController', () => { await controller.setUserRegion('us-ny'); - expect(controller.state.userRegion?.regionCode).toBe('us-ny'); - expect(controller.state.userRegion?.country.isoCode).toBe('US'); - expect(controller.state.userRegion?.state?.name).toBe('New York'); + expect(controller.state.userRegion.data?.regionCode).toBe('us-ny'); + expect(controller.state.userRegion.data?.country.isoCode).toBe('US'); + expect(controller.state.userRegion.data?.state?.name).toBe('New York'); }, ); }); @@ -1700,7 +1726,7 @@ describe('RampsController', () => { { options: { state: { - countries: countriesWithStateId, + countries: createResourceState(countriesWithStateId), }, }, }, @@ -1716,9 +1742,9 @@ describe('RampsController', () => { await controller.setUserRegion('us-ca'); - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); - expect(controller.state.userRegion?.country.isoCode).toBe('US'); - expect(controller.state.userRegion?.state?.name).toBe('California'); + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); + expect(controller.state.userRegion.data?.country.isoCode).toBe('US'); + expect(controller.state.userRegion.data?.state?.name).toBe('California'); }, ); }); @@ -1751,7 +1777,7 @@ describe('RampsController', () => { { options: { state: { - countries: countriesWithStates, + countries: createResourceState(countriesWithStates), }, }, }, @@ -1767,9 +1793,9 @@ describe('RampsController', () => { await controller.setUserRegion('us-xx'); - expect(controller.state.userRegion?.regionCode).toBe('us-xx'); - expect(controller.state.userRegion?.country.isoCode).toBe('US'); - expect(controller.state.userRegion?.state).toBeNull(); + expect(controller.state.userRegion.data?.regionCode).toBe('us-xx'); + expect(controller.state.userRegion.data?.country.isoCode).toBe('US'); + expect(controller.state.userRegion.data?.state).toBeNull(); }, ); }); @@ -1809,8 +1835,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - providers: [mockProvider], + userRegion: createResourceState(createMockUserRegion('us-ca')), + providers: createResourceState([mockProvider], null), }, }, }, @@ -1820,11 +1846,11 @@ describe('RampsController', () => { async () => ({ payments: [] }), ); - expect(controller.state.selectedProvider).toBeNull(); + expect(controller.state.providers.selected).toBeNull(); controller.setSelectedProvider(mockProvider.id); - expect(controller.state.selectedProvider).toStrictEqual(mockProvider); + expect(controller.state.providers.selected).toStrictEqual(mockProvider); }, ); }); @@ -1842,28 +1868,26 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - providers: [mockProvider], - selectedProvider: mockProvider, - paymentMethods: [mockPaymentMethod], - selectedPaymentMethod: mockPaymentMethod, + userRegion: createResourceState(createMockUserRegion('us-ca')), + providers: createResourceState([mockProvider], mockProvider), + paymentMethods: createResourceState([mockPaymentMethod], mockPaymentMethod), }, }, }, ({ controller }) => { - expect(controller.state.selectedProvider).toStrictEqual(mockProvider); - expect(controller.state.paymentMethods).toStrictEqual([ + expect(controller.state.providers.selected).toStrictEqual(mockProvider); + expect(controller.state.paymentMethods.data).toStrictEqual([ mockPaymentMethod, ]); - expect(controller.state.selectedPaymentMethod).toStrictEqual( + expect(controller.state.paymentMethods.selected).toStrictEqual( mockPaymentMethod, ); controller.setSelectedProvider(null); - expect(controller.state.selectedProvider).toBeNull(); - expect(controller.state.paymentMethods).toStrictEqual([]); - expect(controller.state.selectedPaymentMethod).toBeNull(); + expect(controller.state.providers.selected).toBeNull(); + expect(controller.state.paymentMethods.data).toStrictEqual([]); + expect(controller.state.paymentMethods.selected).toBeNull(); }, ); }); @@ -1873,7 +1897,7 @@ describe('RampsController', () => { { options: { state: { - providers: [mockProvider], + providers: createResourceState([mockProvider], null), }, }, }, @@ -1892,7 +1916,7 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), + userRegion: createResourceState(createMockUserRegion('us-ca')), }, }, }, @@ -1911,8 +1935,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - providers: [mockProvider], + userRegion: createResourceState(createMockUserRegion('us-ca')), + providers: createResourceState([mockProvider], null), }, }, }, @@ -1945,11 +1969,9 @@ describe('RampsController', () => { { options: { state: { - selectedProvider: mockProvider, - userRegion: createMockUserRegion('us-ca'), - providers: [mockProvider, newProvider], - paymentMethods: [existingPaymentMethod], - selectedPaymentMethod: existingPaymentMethod, + userRegion: createResourceState(createMockUserRegion('us-ca')), + providers: createResourceState([mockProvider, newProvider], mockProvider), + paymentMethods: createResourceState([existingPaymentMethod], existingPaymentMethod), }, }, }, @@ -1959,21 +1981,21 @@ describe('RampsController', () => { async () => ({ payments: [] }), ); - expect(controller.state.paymentMethods).toStrictEqual([ + expect(controller.state.paymentMethods.data).toStrictEqual([ existingPaymentMethod, ]); - expect(controller.state.selectedPaymentMethod).toStrictEqual( + expect(controller.state.paymentMethods.selected).toStrictEqual( existingPaymentMethod, ); controller.setSelectedProvider(newProvider.id); - expect(controller.state.selectedProvider).toStrictEqual(newProvider); - expect(controller.state.selectedProvider?.id).toBe( + expect(controller.state.providers.selected).toStrictEqual(newProvider); + expect(controller.state.providers.selected?.id).toBe( '/providers/ramp-network-staging', ); - expect(controller.state.paymentMethods).toStrictEqual([]); - expect(controller.state.selectedPaymentMethod).toBeNull(); + expect(controller.state.paymentMethods.data).toStrictEqual([]); + expect(controller.state.paymentMethods.selected).toBeNull(); }, ); }); @@ -2008,8 +2030,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - tokens: mockTokensResponse, + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(mockTokensResponse, null), }, }, }, @@ -2019,11 +2041,11 @@ describe('RampsController', () => { async () => ({ payments: [] }), ); - expect(controller.state.selectedToken).toBeNull(); + expect(controller.state.tokens.selected).toBeNull(); controller.setSelectedToken(mockToken.assetId); - expect(controller.state.selectedToken).toStrictEqual(mockToken); + expect(controller.state.tokens.selected).toStrictEqual(mockToken); }, ); }); @@ -2033,24 +2055,22 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - tokens: mockTokensResponse, - selectedToken: mockToken, - paymentMethods: [mockPaymentMethod], - selectedPaymentMethod: mockPaymentMethod, + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(mockTokensResponse, mockToken), + paymentMethods: createResourceState([mockPaymentMethod], mockPaymentMethod), }, }, }, ({ controller }) => { - expect(controller.state.selectedToken).toStrictEqual(mockToken); - expect(controller.state.paymentMethods).toHaveLength(1); - expect(controller.state.selectedPaymentMethod).not.toBeNull(); + expect(controller.state.tokens.selected).toStrictEqual(mockToken); + expect(controller.state.paymentMethods.data).toHaveLength(1); + expect(controller.state.paymentMethods.selected).not.toBeNull(); controller.setSelectedToken(undefined); - expect(controller.state.selectedToken).toBeNull(); - expect(controller.state.paymentMethods).toStrictEqual([]); - expect(controller.state.selectedPaymentMethod).toBeNull(); + expect(controller.state.tokens.selected).toBeNull(); + expect(controller.state.paymentMethods.data).toStrictEqual([]); + expect(controller.state.paymentMethods.selected).toBeNull(); }, ); }); @@ -2060,7 +2080,7 @@ describe('RampsController', () => { { options: { state: { - tokens: mockTokensResponse, + tokens: createResourceState(mockTokensResponse, null), }, }, }, @@ -2077,7 +2097,7 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), + userRegion: createResourceState(createMockUserRegion('us-ca')), }, }, }, @@ -2094,8 +2114,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - tokens: mockTokensResponse, + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(mockTokensResponse, null), }, }, }, @@ -2114,8 +2134,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - tokens: mockTokensResponse, + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(mockTokensResponse, null), }, }, }, @@ -2159,11 +2179,9 @@ describe('RampsController', () => { { options: { state: { - selectedToken: mockToken, - userRegion: createMockUserRegion('us-ca'), - tokens: tokensWithBoth, - paymentMethods: [mockPaymentMethod], - selectedPaymentMethod: mockPaymentMethod, + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(tokensWithBoth, mockToken), + paymentMethods: createResourceState([mockPaymentMethod], mockPaymentMethod), }, }, }, @@ -2173,18 +2191,18 @@ describe('RampsController', () => { async () => ({ payments: [] }), ); - expect(controller.state.paymentMethods).toStrictEqual([ + expect(controller.state.paymentMethods.data).toStrictEqual([ mockPaymentMethod, ]); - expect(controller.state.selectedPaymentMethod).toStrictEqual( + expect(controller.state.paymentMethods.selected).toStrictEqual( mockPaymentMethod, ); controller.setSelectedToken(newToken.assetId); - expect(controller.state.selectedToken).toStrictEqual(newToken); - expect(controller.state.paymentMethods).toStrictEqual([]); - expect(controller.state.selectedPaymentMethod).toBeNull(); + expect(controller.state.tokens.selected).toStrictEqual(newToken); + expect(controller.state.paymentMethods.data).toStrictEqual([]); + expect(controller.state.paymentMethods.selected).toBeNull(); }, ); }); @@ -2236,7 +2254,7 @@ describe('RampsController', () => { ) => mockTokens, ); - expect(controller.state.tokens).toBeNull(); + expect(controller.state.tokens.data).toBeNull(); const tokens = await controller.getTokens('us-ca', 'buy'); @@ -2275,7 +2293,7 @@ describe('RampsController', () => { ], } `); - expect(controller.state.tokens).toStrictEqual(mockTokens); + expect(controller.state.tokens.data).toStrictEqual(mockTokens); }); }); @@ -2388,7 +2406,7 @@ describe('RampsController', () => { it('uses userRegion from state when region is not provided', async () => { await withController( - { options: { state: { userRegion: createMockUserRegion('fr') } } }, + { options: { state: { userRegion: createResourceState(createMockUserRegion('fr')) } } }, async ({ controller, rootMessenger }) => { let receivedRegion: string | undefined; rootMessenger.registerActionHandler( @@ -2420,7 +2438,7 @@ describe('RampsController', () => { it('prefers provided region over userRegion in state', async () => { await withController( - { options: { state: { userRegion: createMockUserRegion('fr') } } }, + { options: { state: { userRegion: createResourceState(createMockUserRegion('fr')) } } }, async ({ controller, rootMessenger }) => { let receivedRegion: string | undefined; rootMessenger.registerActionHandler( @@ -2444,7 +2462,7 @@ describe('RampsController', () => { it('updates tokens when userRegion matches the requested region', async () => { await withController( - { options: { state: { userRegion: createMockUserRegion('us-ca') } } }, + { options: { state: { userRegion: createResourceState(createMockUserRegion('us-ca')) } } }, async ({ controller, rootMessenger }) => { rootMessenger.registerActionHandler( 'RampsService:getTokens', @@ -2458,12 +2476,12 @@ describe('RampsController', () => { }, ); - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); - expect(controller.state.tokens).toBeNull(); + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); + expect(controller.state.tokens.data).toBeNull(); await controller.getTokens('US-ca'); - expect(controller.state.tokens).toStrictEqual(mockTokens); + expect(controller.state.tokens.data).toStrictEqual(mockTokens); }, ); }); @@ -2498,8 +2516,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - tokens: existingTokens, + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(existingTokens, null), }, }, }, @@ -2516,12 +2534,12 @@ describe('RampsController', () => { }, ); - expect(controller.state.userRegion?.regionCode).toBe('us-ca'); - expect(controller.state.tokens).toStrictEqual(existingTokens); + expect(controller.state.userRegion.data?.regionCode).toBe('us-ca'); + expect(controller.state.tokens.data).toStrictEqual(existingTokens); await controller.getTokens('fr'); - expect(controller.state.tokens).toStrictEqual(existingTokens); + expect(controller.state.tokens.data).toStrictEqual(existingTokens); }, ); }); @@ -2643,11 +2661,10 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - selectedPaymentMethod: mockPaymentMethod1, - paymentMethods: [mockPaymentMethod1, mockPaymentMethod2], - selectedToken: mockSelectedToken, - selectedProvider: mockSelectedProvider, + userRegion: createResourceState(createMockUserRegion('us-ca')), + paymentMethods: createResourceState([mockPaymentMethod1, mockPaymentMethod2], mockPaymentMethod1), + tokens: createResourceState(null, mockSelectedToken), + providers: createResourceState([], mockSelectedProvider), }, }, }, @@ -2657,7 +2674,7 @@ describe('RampsController', () => { async () => mockPaymentMethodsResponse, ); - expect(controller.state.selectedPaymentMethod).toStrictEqual( + expect(controller.state.paymentMethods.selected).toStrictEqual( mockPaymentMethod1, ); @@ -2666,10 +2683,10 @@ describe('RampsController', () => { provider: '/providers/stripe', }); - expect(controller.state.selectedPaymentMethod).toStrictEqual( + expect(controller.state.paymentMethods.selected).toStrictEqual( mockPaymentMethod1, ); - expect(controller.state.paymentMethods).toStrictEqual([ + expect(controller.state.paymentMethods.data).toStrictEqual([ mockPaymentMethod1, mockPaymentMethod2, ]); @@ -2690,11 +2707,10 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - selectedPaymentMethod: removedPaymentMethod, - paymentMethods: [removedPaymentMethod], - selectedToken: mockSelectedToken, - selectedProvider: mockSelectedProvider, + userRegion: createResourceState(createMockUserRegion('us-ca')), + paymentMethods: createResourceState([removedPaymentMethod], removedPaymentMethod), + tokens: createResourceState(null, mockSelectedToken), + providers: createResourceState([], mockSelectedProvider), }, }, }, @@ -2704,7 +2720,7 @@ describe('RampsController', () => { async () => mockPaymentMethodsResponse, ); - expect(controller.state.selectedPaymentMethod).toStrictEqual( + expect(controller.state.paymentMethods.selected).toStrictEqual( removedPaymentMethod, ); @@ -2713,10 +2729,10 @@ describe('RampsController', () => { provider: '/providers/stripe', }); - expect(controller.state.selectedPaymentMethod).toStrictEqual( + expect(controller.state.paymentMethods.selected).toStrictEqual( mockPaymentMethod1, ); - expect(controller.state.paymentMethods).toStrictEqual([ + expect(controller.state.paymentMethods.data).toStrictEqual([ mockPaymentMethod1, mockPaymentMethod2, ]); @@ -2729,11 +2745,10 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - selectedPaymentMethod: null, - paymentMethods: [], - selectedToken: mockSelectedToken, - selectedProvider: mockSelectedProvider, + userRegion: createResourceState(createMockUserRegion('us-ca')), + paymentMethods: createResourceState([], null), + tokens: createResourceState(null, mockSelectedToken), + providers: createResourceState([], mockSelectedProvider), }, }, }, @@ -2743,17 +2758,17 @@ describe('RampsController', () => { async () => mockPaymentMethodsResponse, ); - expect(controller.state.selectedPaymentMethod).toBeNull(); + expect(controller.state.paymentMethods.selected).toBeNull(); await controller.getPaymentMethods('us-ca', { assetId: 'eip155:1/slip44:60', provider: '/providers/stripe', }); - expect(controller.state.selectedPaymentMethod).toStrictEqual( + expect(controller.state.paymentMethods.selected).toStrictEqual( mockPaymentMethod1, ); - expect(controller.state.paymentMethods).toStrictEqual([ + expect(controller.state.paymentMethods.data).toStrictEqual([ mockPaymentMethod1, mockPaymentMethod2, ]); @@ -2766,9 +2781,9 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - selectedToken: mockSelectedToken, - selectedProvider: mockSelectedProvider, + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(null, mockSelectedToken), + providers: createResourceState([], mockSelectedProvider), }, }, }, @@ -2778,14 +2793,14 @@ describe('RampsController', () => { async () => mockPaymentMethodsResponse, ); - expect(controller.state.paymentMethods).toStrictEqual([]); + expect(controller.state.paymentMethods.data).toStrictEqual([]); await controller.getPaymentMethods('us-ca', { assetId: 'eip155:1/slip44:60', provider: '/providers/stripe', }); - expect(controller.state.paymentMethods).toStrictEqual([ + expect(controller.state.paymentMethods.data).toStrictEqual([ mockPaymentMethod1, mockPaymentMethod2, ]); @@ -2798,7 +2813,7 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), + userRegion: createResourceState(createMockUserRegion('us-ca')), }, }, }, @@ -2845,7 +2860,7 @@ describe('RampsController', () => { { options: { state: { - userRegion: regionWithoutCurrency, + userRegion: createResourceState(regionWithoutCurrency), }, }, }, @@ -2877,8 +2892,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - selectedToken: mockToken, + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(null, mockToken), }, }, }, @@ -2926,8 +2941,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - selectedProvider: testProvider, + userRegion: createResourceState(createMockUserRegion('us-ca')), + providers: createResourceState([], testProvider), }, }, }, @@ -2960,7 +2975,7 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('fr'), + userRegion: createResourceState(createMockUserRegion('fr')), }, }, }, @@ -3002,11 +3017,10 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - selectedPaymentMethod: removedPaymentMethod, - paymentMethods: [removedPaymentMethod], - selectedToken: mockSelectedToken, - selectedProvider: mockSelectedProvider, + userRegion: createResourceState(createMockUserRegion('us-ca')), + paymentMethods: createResourceState([removedPaymentMethod], removedPaymentMethod), + tokens: createResourceState(null, mockSelectedToken), + providers: createResourceState([], mockSelectedProvider), }, }, }, @@ -3021,8 +3035,8 @@ describe('RampsController', () => { provider: '/providers/stripe', }); - expect(controller.state.selectedPaymentMethod).toBeNull(); - expect(controller.state.paymentMethods).toStrictEqual([]); + expect(controller.state.paymentMethods.selected).toBeNull(); + expect(controller.state.paymentMethods.data).toStrictEqual([]); }, ); }); @@ -3045,9 +3059,9 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - selectedToken: null, - selectedProvider: null, + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(null, null), + providers: createResourceState([], null), }, }, }, @@ -3121,14 +3135,16 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - selectedToken: tokenA, - selectedProvider: null, - paymentMethods: [], - tokens: { - topTokens: [tokenA, tokenB], - allTokens: [tokenA, tokenB], - }, + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState( + { + topTokens: [tokenA, tokenB], + allTokens: [tokenA, tokenB], + }, + tokenA, + ), + providers: createResourceState([], null), + paymentMethods: createResourceState([], null), }, }, }, @@ -3169,8 +3185,8 @@ describe('RampsController', () => { resolveTokenARequest({ payments: paymentMethodsForTokenA }); await tokenAPaymentMethodsPromise; - expect(controller.state.selectedToken).toStrictEqual(tokenB); - expect(controller.state.paymentMethods).toStrictEqual( + expect(controller.state.tokens.selected).toStrictEqual(tokenB); + expect(controller.state.paymentMethods.data).toStrictEqual( paymentMethodsForTokenB, ); expect(callCount).toBe(2); @@ -3233,11 +3249,10 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - selectedToken: null, - selectedProvider: providerA, - paymentMethods: [], - providers: [providerA, providerB], + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(null, null), + providers: createResourceState([providerA, providerB], providerA), + paymentMethods: createResourceState([], null), }, }, }, @@ -3278,8 +3293,8 @@ describe('RampsController', () => { resolveProviderARequest({ payments: paymentMethodsForProviderA }); await providerAPaymentMethodsPromise; - expect(controller.state.selectedProvider).toStrictEqual(providerB); - expect(controller.state.paymentMethods).toStrictEqual( + expect(controller.state.providers.selected).toStrictEqual(providerB); + expect(controller.state.paymentMethods.data).toStrictEqual( paymentMethodsForProviderB, ); expect(callCount).toBe(2); @@ -3327,10 +3342,10 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us-ca'), - selectedToken: token, - selectedProvider: provider, - paymentMethods: [], + userRegion: createResourceState(createMockUserRegion('us-ca')), + tokens: createResourceState(null, token), + providers: createResourceState([], provider), + paymentMethods: createResourceState([], null), }, }, }, @@ -3345,9 +3360,9 @@ describe('RampsController', () => { provider: provider.id, }); - expect(controller.state.selectedToken).toStrictEqual(token); - expect(controller.state.selectedProvider).toStrictEqual(provider); - expect(controller.state.paymentMethods).toStrictEqual( + expect(controller.state.tokens.selected).toStrictEqual(token); + expect(controller.state.providers.selected).toStrictEqual(provider); + expect(controller.state.paymentMethods.data).toStrictEqual( newPaymentMethods, ); }, @@ -3369,16 +3384,16 @@ describe('RampsController', () => { { options: { state: { - paymentMethods: [mockPaymentMethod], + paymentMethods: createResourceState([mockPaymentMethod], null), }, }, }, ({ controller }) => { - expect(controller.state.selectedPaymentMethod).toBeNull(); + expect(controller.state.paymentMethods.selected).toBeNull(); controller.setSelectedPaymentMethod(mockPaymentMethod.id); - expect(controller.state.selectedPaymentMethod).toStrictEqual( + expect(controller.state.paymentMethods.selected).toStrictEqual( mockPaymentMethod, ); }, @@ -3390,19 +3405,18 @@ describe('RampsController', () => { { options: { state: { - selectedPaymentMethod: mockPaymentMethod, - paymentMethods: [mockPaymentMethod], + paymentMethods: createResourceState([mockPaymentMethod], mockPaymentMethod), }, }, }, ({ controller }) => { - expect(controller.state.selectedPaymentMethod).toStrictEqual( + expect(controller.state.paymentMethods.selected).toStrictEqual( mockPaymentMethod, ); controller.setSelectedPaymentMethod(undefined); - expect(controller.state.selectedPaymentMethod).toBeNull(); + expect(controller.state.paymentMethods.selected).toBeNull(); }, ); }); @@ -3422,7 +3436,7 @@ describe('RampsController', () => { { options: { state: { - paymentMethods: [mockPaymentMethod], + paymentMethods: createResourceState([mockPaymentMethod], null), }, }, }, @@ -3473,8 +3487,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us'), - paymentMethods: [ + userRegion: createResourceState(createMockUserRegion('us')), + paymentMethods: createResourceState([ { id: '/payments/debit-credit-card', paymentType: 'debit-credit-card', @@ -3482,7 +3496,7 @@ describe('RampsController', () => { score: 90, icon: 'card', }, - ], + ], null), }, }, }, @@ -3492,7 +3506,7 @@ describe('RampsController', () => { async () => mockQuotesResponse, ); - expect(controller.state.quotes).toBeNull(); + expect(controller.state.quotes.data).toBeNull(); const result = await controller.getQuotes({ assetId: 'eip155:1/slip44:60', @@ -3502,7 +3516,7 @@ describe('RampsController', () => { expect(result.success).toHaveLength(1); expect(result.success[0]?.provider).toBe('/providers/moonpay'); - expect(controller.state.quotes).toStrictEqual(mockQuotesResponse); + expect(controller.state.quotes.data).toStrictEqual(mockQuotesResponse); }, ); }); @@ -3512,8 +3526,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us'), - paymentMethods: [ + userRegion: createResourceState(createMockUserRegion('us')), + paymentMethods: createResourceState([ { id: '/payments/debit-credit-card', paymentType: 'debit-credit-card', @@ -3521,7 +3535,7 @@ describe('RampsController', () => { score: 90, icon: 'card', }, - ], + ], null), }, }, }, @@ -3562,7 +3576,7 @@ describe('RampsController', () => { { options: { state: { - userRegion: { + userRegion: createResourceState({ country: { isoCode: 'US', name: 'United States', @@ -3573,7 +3587,7 @@ describe('RampsController', () => { }, state: null, regionCode: 'us', - }, + }), }, }, }, @@ -3595,8 +3609,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us'), - paymentMethods: [], + userRegion: createResourceState(createMockUserRegion('us')), + paymentMethods: createResourceState([], null), }, }, }, @@ -3617,8 +3631,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us'), - paymentMethods: [ + userRegion: createResourceState(createMockUserRegion('us')), + paymentMethods: createResourceState([ { id: '/payments/debit-credit-card', paymentType: 'debit-credit-card', @@ -3626,7 +3640,7 @@ describe('RampsController', () => { score: 90, icon: 'card', }, - ], + ], null), }, }, }, @@ -3663,8 +3677,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us'), - paymentMethods: [ + userRegion: createResourceState(createMockUserRegion('us')), + paymentMethods: createResourceState([ { id: '/payments/debit-credit-card', paymentType: 'debit-credit-card', @@ -3672,7 +3686,7 @@ describe('RampsController', () => { score: 90, icon: 'card', }, - ], + ], null), }, }, }, @@ -3701,8 +3715,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us'), - paymentMethods: [ + userRegion: createResourceState(createMockUserRegion('us')), + paymentMethods: createResourceState([ { id: '/payments/debit-credit-card', paymentType: 'debit-credit-card', @@ -3710,7 +3724,7 @@ describe('RampsController', () => { score: 90, icon: 'card', }, - ], + ], null), }, }, }, @@ -3739,8 +3753,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us'), - paymentMethods: [ + userRegion: createResourceState(createMockUserRegion('us')), + paymentMethods: createResourceState([ { id: '/payments/debit-credit-card', paymentType: 'debit-credit-card', @@ -3748,7 +3762,7 @@ describe('RampsController', () => { score: 90, icon: 'card', }, - ], + ], null), }, }, }, @@ -3805,8 +3819,8 @@ describe('RampsController', () => { { options: { state: { - userRegion: createMockUserRegion('us'), - countries: [ + userRegion: createResourceState(createMockUserRegion('us')), + countries: createResourceState([ { isoCode: 'US', flag: '🇺🇸', @@ -3823,8 +3837,8 @@ describe('RampsController', () => { currency: 'EUR', supported: { buy: true, sell: true }, }, - ], - paymentMethods: [ + ]), + paymentMethods: createResourceState([ { id: '/payments/debit-credit-card', paymentType: 'debit-credit-card', @@ -3832,7 +3846,7 @@ describe('RampsController', () => { score: 90, icon: 'card', }, - ], + ], null), }, }, }, @@ -3867,7 +3881,7 @@ describe('RampsController', () => { await quotesPromise; // Quotes should not be updated because region changed - expect(controller.state.quotes).toBeNull(); + expect(controller.state.quotes.data).toBeNull(); }, ); }); @@ -4012,6 +4026,25 @@ function createMockCountries(): Country[] { ]; } +/** + * Creates a ResourceState object for testing. + * + * @param data - The resource data. + * @param selected - The selected item (optional). + * @returns A ResourceState object. + */ +function createResourceState( + data: TData, + selected: TSelected = null as TSelected, +) { + return { + data, + selected, + isLoading: false, + error: null, + }; +} + /** * The type of the messenger populated with all external actions and events * required by the controller under test. From a0d8a41b462f186051c9b629d867cb025e05bb89 Mon Sep 17 00:00:00 2001 From: George Weiler Date: Thu, 29 Jan 2026 15:26:35 -0700 Subject: [PATCH 7/8] chore: controller test update --- .../src/RampsController.test.ts | 180 ++++++++++++------ 1 file changed, 126 insertions(+), 54 deletions(-) diff --git a/packages/ramps-controller/src/RampsController.test.ts b/packages/ramps-controller/src/RampsController.test.ts index 2d347d6fcc6..0fc28d8d29b 100644 --- a/packages/ramps-controller/src/RampsController.test.ts +++ b/packages/ramps-controller/src/RampsController.test.ts @@ -440,28 +440,43 @@ describe('RampsController', () => { ), ).toMatchInlineSnapshot(` Object { - "countries": Array [], - "countriesError": null, - "countriesLoading": false, - "paymentMethods": Array [], - "paymentMethodsError": null, - "paymentMethodsLoading": false, - "providers": Array [], - "providersError": null, - "providersLoading": false, - "quotes": null, - "quotesError": null, - "quotesLoading": false, + "countries": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "paymentMethods": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "providers": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "quotes": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, "requests": Object {}, - "selectedPaymentMethod": null, - "selectedProvider": null, - "selectedToken": null, - "tokens": null, - "tokensError": null, - "tokensLoading": false, - "userRegion": null, - "userRegionError": null, - "userRegionLoading": false, + "tokens": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, + "userRegion": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, } `); }); @@ -477,14 +492,36 @@ describe('RampsController', () => { ), ).toMatchInlineSnapshot(` Object { - "countries": Array [], - "paymentMethods": Array [], - "providers": Array [], - "selectedPaymentMethod": null, - "selectedProvider": null, - "selectedToken": null, - "tokens": null, - "userRegion": null, + "countries": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "paymentMethods": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "providers": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "tokens": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, + "userRegion": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, } `); }); @@ -500,10 +537,30 @@ describe('RampsController', () => { ), ).toMatchInlineSnapshot(` Object { - "countries": Array [], - "providers": Array [], - "tokens": null, - "userRegion": null, + "countries": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "providers": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "tokens": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, + "userRegion": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, } `); }); @@ -519,28 +576,43 @@ describe('RampsController', () => { ), ).toMatchInlineSnapshot(` Object { - "countries": Array [], - "countriesError": null, - "countriesLoading": false, - "paymentMethods": Array [], - "paymentMethodsError": null, - "paymentMethodsLoading": false, - "providers": Array [], - "providersError": null, - "providersLoading": false, - "quotes": null, - "quotesError": null, - "quotesLoading": false, + "countries": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "paymentMethods": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "providers": Object { + "data": Array [], + "error": null, + "isLoading": false, + "selected": null, + }, + "quotes": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, "requests": Object {}, - "selectedPaymentMethod": null, - "selectedProvider": null, - "selectedToken": null, - "tokens": null, - "tokensError": null, - "tokensLoading": false, - "userRegion": null, - "userRegionError": null, - "userRegionLoading": false, + "tokens": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, + "userRegion": Object { + "data": null, + "error": null, + "isLoading": false, + "selected": null, + }, } `); }); From 9b4cf0899c7b8c073ae432787c69d4d812b8b1dd Mon Sep 17 00:00:00 2001 From: George Weiler Date: Thu, 29 Jan 2026 15:34:52 -0700 Subject: [PATCH 8/8] chore: selectors test update --- .../ramps-controller/src/selectors.test.ts | 402 +++--------------- 1 file changed, 56 insertions(+), 346 deletions(-) diff --git a/packages/ramps-controller/src/selectors.test.ts b/packages/ramps-controller/src/selectors.test.ts index eaf033e59a3..6a99cab073b 100644 --- a/packages/ramps-controller/src/selectors.test.ts +++ b/packages/ramps-controller/src/selectors.test.ts @@ -10,6 +10,33 @@ type TestRootState = { ramps: RampsControllerState; }; +function createDefaultResourceState( + data: TData, + selected: TSelected = null as TSelected, +) { + return { + data, + selected, + isLoading: false, + error: null, + }; +} + +function createMockRampsState( + overrides: Partial = {}, +): RampsControllerState { + return { + userRegion: createDefaultResourceState(null), + countries: createDefaultResourceState([]), + providers: createDefaultResourceState([], null), + tokens: createDefaultResourceState(null, null), + paymentMethods: createDefaultResourceState([], null), + quotes: createDefaultResourceState(null), + requests: {}, + ...overrides, + }; +} + describe('createRequestSelector', () => { const getState = (state: TestRootState): RampsControllerState => state.ramps; @@ -23,32 +50,11 @@ describe('createRequestSelector', () => { const loadingRequest = createLoadingState(); const state: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': loadingRequest, }, - }, + }), }; const result = selector(state); @@ -71,32 +77,11 @@ describe('createRequestSelector', () => { const successRequest = createSuccessState(['ETH', 'BTC'], Date.now()); const state: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': successRequest, }, - }, + }), }; const result = selector(state); @@ -122,32 +107,11 @@ describe('createRequestSelector', () => { const errorRequest = createErrorState('Network error', Date.now()); const state: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': errorRequest, }, - }, + }), }; const result = selector(state); @@ -169,30 +133,7 @@ describe('createRequestSelector', () => { ); const state: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, - requests: {}, - }, + ramps: createMockRampsState(), }; const result = selector(state); @@ -239,32 +180,11 @@ describe('createRequestSelector', () => { const successRequest = createSuccessState(['ETH', 'BTC'], Date.now()); const state: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': successRequest, }, - }, + }), }; const result1 = selector(state); @@ -282,64 +202,22 @@ describe('createRequestSelector', () => { const successRequest1 = createSuccessState(['ETH'], Date.now()); const state1: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': successRequest1, }, - }, + }), }; const result1 = selector(state1); const successRequest2 = createSuccessState(['ETH', 'BTC'], Date.now()); const state2: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': successRequest2, }, - }, + }), }; const result2 = selector(state2); @@ -358,32 +236,11 @@ describe('createRequestSelector', () => { const largeArray = Array.from({ length: 1000 }, (_, i) => `item-${i}`); const successRequest = createSuccessState(largeArray, Date.now()); const state: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': successRequest, }, - }, + }), }; const result1 = selector(state); @@ -405,32 +262,11 @@ describe('createRequestSelector', () => { }; const successRequest = createSuccessState(complexData, Date.now()); const state: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getData:[]': successRequest, }, - }, + }), }; const result1 = selector(state); @@ -451,32 +287,11 @@ describe('createRequestSelector', () => { const loadingRequest = createLoadingState(); const loadingState: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': loadingRequest, }, - }, + }), }; const loadingResult = selector(loadingState); @@ -485,32 +300,11 @@ describe('createRequestSelector', () => { const successRequest = createSuccessState(['ETH'], Date.now()); const successState: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': successRequest, }, - }, + }), }; const successResult = selector(successState); @@ -527,32 +321,11 @@ describe('createRequestSelector', () => { const successRequest = createSuccessState(['ETH'], Date.now()); const successState: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': successRequest, }, - }, + }), }; const successResult = selector(successState); @@ -560,32 +333,11 @@ describe('createRequestSelector', () => { const errorRequest = createErrorState('Failed to fetch', Date.now()); const errorState: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': errorRequest, }, - }, + }), }; const errorResult = selector(errorState); @@ -608,28 +360,7 @@ describe('createRequestSelector', () => { ); const state: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': createSuccessState( ['ETH'], @@ -637,7 +368,7 @@ describe('createRequestSelector', () => { ), 'getPrice:["US"]': createSuccessState(100, Date.now()), }, - }, + }), }; const result1 = selector1(state); @@ -660,28 +391,7 @@ describe('createRequestSelector', () => { ); const state: TestRootState = { - ramps: { - userRegion: null, - countries: [], - selectedProvider: null, - providers: [], - tokens: null, - selectedToken: null, - paymentMethods: [], - selectedPaymentMethod: null, - quotes: null, - userRegionLoading: false, - userRegionError: null, - countriesLoading: false, - countriesError: null, - providersLoading: false, - providersError: null, - tokensLoading: false, - tokensError: null, - paymentMethodsLoading: false, - paymentMethodsError: null, - quotesLoading: false, - quotesError: null, + ramps: createMockRampsState({ requests: { 'getCryptoCurrencies:["US"]': createSuccessState( ['ETH'], @@ -692,7 +402,7 @@ describe('createRequestSelector', () => { Date.now(), ), }, - }, + }), }; const result1 = selector1(state);