diff --git a/packages/ramps-controller/CHANGELOG.md b/packages/ramps-controller/CHANGELOG.md index 1a9103c53b4..5d449b245bb 100644 --- a/packages/ramps-controller/CHANGELOG.md +++ b/packages/ramps-controller/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `status` field to `ResourceState` to distinguish between uninitialized and empty-fetched states ([#8116](https://github.com/MetaMask/core/pull/8116)) + ## [10.2.0] ### Fixed diff --git a/packages/ramps-controller/src/RampsController.test.ts b/packages/ramps-controller/src/RampsController.test.ts index 9d745fe7011..e619af05794 100644 --- a/packages/ramps-controller/src/RampsController.test.ts +++ b/packages/ramps-controller/src/RampsController.test.ts @@ -92,6 +92,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "nativeProviders": { "transak": { @@ -100,6 +101,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "isAuthenticated": false, "kycRequirement": { @@ -107,12 +109,14 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "userDetails": { "data": null, "error": null, "isLoading": false, "selected": null, + "status": "idle", }, }, }, @@ -122,12 +126,14 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "providers": { "data": [], "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "requests": {}, "tokens": { @@ -135,6 +141,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "userRegion": null, } @@ -167,6 +174,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "nativeProviders": { "transak": { @@ -175,6 +183,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "isAuthenticated": false, "kycRequirement": { @@ -182,12 +191,14 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "userDetails": { "data": null, "error": null, "isLoading": false, "selected": null, + "status": "idle", }, }, }, @@ -197,12 +208,14 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "providers": { "data": [], "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "requests": {}, "tokens": { @@ -210,6 +223,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "userRegion": null, } @@ -743,6 +757,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "nativeProviders": { "transak": { @@ -751,6 +766,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "isAuthenticated": false, "kycRequirement": { @@ -758,12 +774,14 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "userDetails": { "data": null, "error": null, "isLoading": false, "selected": null, + "status": "idle", }, }, }, @@ -773,12 +791,14 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "providers": { "data": [], "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "requests": {}, "tokens": { @@ -786,6 +806,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "userRegion": null, } @@ -808,6 +829,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "orders": [], "paymentMethods": { @@ -815,18 +837,21 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "providers": { "data": [], "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "tokens": { "data": null, "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "userRegion": null, } @@ -849,6 +874,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "orders": [], "providers": { @@ -856,12 +882,14 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "tokens": { "data": null, "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "userRegion": null, } @@ -884,6 +912,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "nativeProviders": { "transak": { @@ -892,6 +921,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "isAuthenticated": false, "kycRequirement": { @@ -899,12 +929,14 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "userDetails": { "data": null, "error": null, "isLoading": false, "selected": null, + "status": "idle", }, }, }, @@ -914,12 +946,14 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "providers": { "data": [], "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "requests": {}, "tokens": { @@ -927,6 +961,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "userRegion": null, } @@ -1080,6 +1115,61 @@ describe('RampsController', () => { }); }); + it('sets status to idle when the last concurrent request is stale (last-write-wins)', async () => { + await withController(async ({ controller }) => { + let resolveFirst: (value: string) => void; + let resolveSecond: (value: string) => void; + + // Request A is "current" (its result should be applied) + // Request B is "stale" (isResultCurrent returns false) + const fetcherA = async (): Promise => + new Promise((resolve) => { + resolveFirst = resolve; + }); + const fetcherB = async (): Promise => + new Promise((resolve) => { + resolveSecond = resolve; + }); + + let isACurrentValue = false; + const promiseA = controller.executeRequest( + 'providers-key-a2', + fetcherA, + { + resourceType: 'providers', + isResultCurrent: () => isACurrentValue, + }, + ); + const promiseB = controller.executeRequest( + 'providers-key-b2', + fetcherB, + { + resourceType: 'providers', + isResultCurrent: () => false, + }, + ); + + expect(controller.state.providers.status).toBe(RequestStatus.LOADING); + + // Resolve A first while it's current, count goes from 2 to 1 so status is not written yet + isACurrentValue = true; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + resolveFirst!('result-a'); + await promiseA; + + // Status not updated yet since count is still 1 + expect(controller.state.providers.isLoading).toBe(true); + + // Resolve B last while stale β†’ terminalStatus is IDLE (last-write-wins) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + resolveSecond!('result-b'); + await promiseB; + + expect(controller.state.providers.isLoading).toBe(false); + expect(controller.state.providers.status).toBe(RequestStatus.IDLE); + }); + }); + it('clears resource loading when ref-count hits zero even if map was cleared (defensive)', async () => { await withController(async ({ controller }) => { let resolveFetcher: (value: string) => void; @@ -1888,6 +1978,102 @@ describe('RampsController', () => { ); }); + it('does not inherit stale terminal status after region change aborts a successful request', async () => { + const mockTokens: TokensResponse = { topTokens: [], allTokens: [] }; + + await withController( + { + options: { + state: { + countries: createResourceState(createMockCountries()), + userRegion: { + regionCode: 'us-ca', + state: { + stateId: 'CA', + name: 'California', + supported: { buy: true, sell: true }, + }, + country: { + isoCode: 'US', + name: 'United States of America', + flag: 'πŸ‡ΊπŸ‡Έ', + currency: 'USD', + phone: { prefix: '+1', placeholder: '', template: '' }, + supported: { buy: true, sell: true }, + }, + }, + }, + }, + }, + async ({ controller, rootMessenger }) => { + // Step 1: Launch two concurrent executeRequest calls for 'providers'. + // Request A will succeed (isResultCurrent=true), recording SUCCESS in + // #resourceTerminalStatus. Request B hangs (will be aborted). + let resolveA: (value: string) => void; + + const promiseA = controller.executeRequest( + 'providers-stale-a', + async () => + new Promise((resolve) => { + resolveA = resolve; + }), + { resourceType: 'providers', isResultCurrent: () => true }, + ); + + // Request B hangs β€” will be aborted by the region change below + const promiseB = controller + .executeRequest( + 'providers-stale-b', + async (signal) => + new Promise((_resolve, reject) => { + signal.addEventListener('abort', () => + reject(new Error('aborted')), + ); + }), + { resourceType: 'providers', isResultCurrent: () => true }, + ) + // eslint-disable-next-line no-empty-function -- swallow expected abort rejection + .catch(() => {}); + + // Step 2: Resolve A successfully while B is still in-flight. + // count goes 2β†’1, SUCCESS is recorded in #resourceTerminalStatus but + // not committed yet (count > 0 when A finishes). + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + resolveA!('result-a'); + await promiseA; + + expect(controller.state.providers.isLoading).toBe(true); + + // Step 3: Change region. This calls #abortDependentRequests (aborts B) + // and #clearPendingResourceCountForDependentResources (clears count). + // The fix also clears #resourceTerminalStatus for dependent resources. + // B's finally block sees signal.aborted=true and skips cleanup, so + // without the fix the stale SUCCESS remains in #resourceTerminalStatus. + rootMessenger.registerActionHandler( + 'RampsService:getTokens', + async () => mockTokens, + ); + rootMessenger.registerActionHandler( + 'RampsService:getProviders', + async () => { + throw new Error('FR providers fetch failed'); + }, + ); + + await controller.setUserRegion('FR'); + await promiseB; + + // Wait for the fire-and-forget refetches to settle + await new Promise((resolve) => setTimeout(resolve, 50)); + + // Step 4: Without the fix, the stale SUCCESS from request A prevented + // ERROR from being recorded when the FR request failed, so status + // would be 'success'. With the fix it must be 'error'. + expect(controller.state.providers.status).toBe(RequestStatus.ERROR); + }, + ); + }); + it('does not clear persisted state when setting the same region', async () => { const mockTokens: TokensResponse = { topTokens: [], @@ -4977,7 +5163,7 @@ describe('RampsController', () => { }); }); - it('returns null when service returns BuyWidget with null url', async () => { + it('returns null when service returns BuyWidget with undefined url', async () => { await withController(async ({ controller, rootMessenger }) => { const quote: Quote = { provider: '/providers/transak-staging', @@ -4993,7 +5179,7 @@ describe('RampsController', () => { rootMessenger.registerActionHandler( 'RampsService:getBuyWidgetUrl', async () => ({ - url: null, + url: undefined as unknown as string, browser: 'APP_BROWSER' as const, orderId: null, }), @@ -5004,6 +5190,34 @@ describe('RampsController', () => { expect(widgetUrl).toBeNull(); }); }); + + it('returns empty string when service returns BuyWidget with empty url', async () => { + await withController(async ({ controller, rootMessenger }) => { + const quote: Quote = { + provider: '/providers/transak-staging', + quote: { + amountIn: 100, + amountOut: '0.05', + paymentMethod: '/payments/debit-credit-card', + buyURL: + 'https://on-ramp.uat-api.cx.metamask.io/providers/transak-staging/buy-widget', + }, + }; + + rootMessenger.registerActionHandler( + 'RampsService:getBuyWidgetUrl', + async () => ({ + url: '', + browser: 'APP_BROWSER' as const, + orderId: null, + }), + ); + + const widgetUrl = await controller.getWidgetUrl(quote); + + expect(widgetUrl).toBe(''); + }); + }); }); describe('destroy', () => { @@ -5336,7 +5550,10 @@ describe('RampsController', () => { const pendingOrder = createMockOrder({ providerOrderId: 'poll-1', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', }); controller.addOrder(pendingOrder); @@ -5404,7 +5621,10 @@ describe('RampsController', () => { const pendingOrder = createMockOrder({ providerOrderId: 'status-change-1', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', }); controller.addOrder(pendingOrder); @@ -5441,7 +5661,10 @@ describe('RampsController', () => { const pendingOrder = createMockOrder({ providerOrderId: 'no-change-1', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', }); controller.addOrder(pendingOrder); @@ -5471,7 +5694,10 @@ describe('RampsController', () => { const pendingOrder = createMockOrder({ providerOrderId: 'terminal-1', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', }); controller.addOrder(pendingOrder); @@ -5501,7 +5727,10 @@ describe('RampsController', () => { const pendingOrder = createMockOrder({ providerOrderId: 'unknown-1', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', }); controller.addOrder(pendingOrder); @@ -5531,7 +5760,10 @@ describe('RampsController', () => { const pendingOrder = createMockOrder({ providerOrderId: 'error-1', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', }); controller.addOrder(pendingOrder); @@ -5581,7 +5813,10 @@ describe('RampsController', () => { const orderNoId = createMockOrder({ providerOrderId: '', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', }); controller.addOrder(orderNoId); @@ -5603,7 +5838,10 @@ describe('RampsController', () => { const orderNoWallet = createMockOrder({ providerOrderId: 'no-wallet-1', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '', }); controller.addOrder(orderNoWallet); @@ -5625,7 +5863,10 @@ describe('RampsController', () => { const order = createMockOrder({ providerOrderId: 'strip-prefix-1', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', }); controller.addOrder(order); @@ -5654,7 +5895,10 @@ describe('RampsController', () => { const order = createMockOrder({ providerOrderId: 'backoff-1', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', }); controller.addOrder(order); @@ -5684,7 +5928,10 @@ describe('RampsController', () => { const order = createMockOrder({ providerOrderId: 'poll-min-1', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', pollingSecondsMinimum: 120, }); @@ -5754,7 +6001,10 @@ describe('RampsController', () => { const completedOrder = createMockOrder({ providerOrderId: 'completed-1', status: RampsOrderStatus.Completed, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', }); controller.addOrder(completedOrder); @@ -5776,7 +6026,10 @@ describe('RampsController', () => { const order = createMockOrder({ providerOrderId: 'reset-err-1', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', }); controller.addOrder(order); @@ -5812,7 +6065,10 @@ describe('RampsController', () => { const pendingOrder = createMockOrder({ providerOrderId: 'race-1', status: RampsOrderStatus.Pending, - provider: { id: '/providers/transak', name: 'Transak' }, + provider: createMockProvider({ + id: '/providers/transak', + name: 'Transak', + }), walletAddress: '0xabc', }); controller.addOrder(pendingOrder); @@ -5975,6 +6231,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "isAuthenticated": false, "kycRequirement": { @@ -5982,12 +6239,14 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "idle", }, "userDetails": { "data": null, "error": null, "isLoading": false, "selected": null, + "status": "idle", }, } `); @@ -6191,6 +6450,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "success", } `); }); @@ -6213,6 +6473,9 @@ describe('RampsController', () => { expect( controller.state.nativeProviders.transak.userDetails.error, ).toBe('Auth failed'); + expect( + controller.state.nativeProviders.transak.userDetails.status, + ).toBe(RequestStatus.ERROR); }); }); @@ -6333,6 +6596,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "success", } `); }); @@ -6361,6 +6625,9 @@ describe('RampsController', () => { expect(controller.state.nativeProviders.transak.buyQuote.error).toBe( 'Quote failed', ); + expect(controller.state.nativeProviders.transak.buyQuote.status).toBe( + RequestStatus.ERROR, + ); }); }); @@ -6415,6 +6682,7 @@ describe('RampsController', () => { "error": null, "isLoading": false, "selected": null, + "status": "success", } `); }); @@ -6437,6 +6705,9 @@ describe('RampsController', () => { expect( controller.state.nativeProviders.transak.kycRequirement.error, ).toBe('KYC failed'); + expect( + controller.state.nativeProviders.transak.kycRequirement.status, + ).toBe(RequestStatus.ERROR); }); }); @@ -7196,6 +7467,7 @@ function createResourceState( selected, isLoading: false, error: null, + status: RequestStatus.IDLE, }; } @@ -7244,14 +7516,27 @@ function createMockDepositOrder(): TransakDepositOrder { }; } +function createMockProvider(overrides: Partial = {}): Provider { + return { + id: '/providers/test-provider', + name: 'Test Provider', + environmentType: 'production', + description: 'Test provider description', + hqAddress: '123 Test St', + links: [], + logos: { light: '', dark: '', height: 32, width: 32 }, + ...overrides, + }; +} + function createMockOrder(overrides: Partial = {}): RampsOrder { return { id: '/providers/transak-staging/orders/abc-123', isOnlyLink: false, - provider: { + provider: createMockProvider({ id: '/providers/transak-staging', name: 'Transak (Staging)', - }, + }), success: true, cryptoAmount: 0.05, fiatAmount: 100, diff --git a/packages/ramps-controller/src/RampsController.ts b/packages/ramps-controller/src/RampsController.ts index dde0bb9b645..e9b5d299265 100644 --- a/packages/ramps-controller/src/RampsController.ts +++ b/packages/ramps-controller/src/RampsController.ts @@ -197,6 +197,11 @@ export type ResourceState = { * Error message if the fetch failed, or null. */ error: string | null; + /** + * The current status of the resource: 'idle' | 'loading' | 'success' | 'error'. + * Distinguishes between never-fetched ('idle') and successfully-fetched-empty ('success'). + */ + status: `${RequestStatus}`; }; /** @@ -338,6 +343,7 @@ function createDefaultResourceState( selected, isLoading: false, error: null, + status: RequestStatus.IDLE, }; } @@ -402,6 +408,7 @@ function resetResource( resource.selected = def.selected; resource.isLoading = def.isLoading; resource.error = def.error; + resource.status = def.status; } /** @@ -827,12 +834,17 @@ export class RampsController extends BaseController< const count = this.#pendingResourceCount.get(resourceType) ?? 0; this.#pendingResourceCount.set(resourceType, count + 1); if (count === 0) { - this.#setResourceLoading(resourceType, true); + this.#setResourceLoadingAndStatus( + resourceType, + true, + RequestStatus.LOADING, + ); } } // Create the fetch promise const promise = (async (): Promise => { + let terminalStatus: RequestStatus = RequestStatus.IDLE; try { const data = await fetcher(abortController.signal); @@ -850,6 +862,7 @@ export class RampsController extends BaseController< !options?.isResultCurrent || options.isResultCurrent(); if (isCurrent) { this.#setResourceError(resourceType, null); + terminalStatus = RequestStatus.SUCCESS; } } return data; @@ -868,6 +881,7 @@ export class RampsController extends BaseController< !options?.isResultCurrent || options.isResultCurrent(); if (isCurrent) { this.#setResourceError(resourceType, errorMessage); + terminalStatus = RequestStatus.ERROR; } } throw error; @@ -885,7 +899,11 @@ export class RampsController extends BaseController< const next = Math.max(0, count - 1); if (next === 0) { this.#pendingResourceCount.delete(resourceType); - this.#setResourceLoading(resourceType, false); + this.#setResourceLoadingAndStatus( + resourceType, + false, + terminalStatus, + ); } else { this.#pendingResourceCount.set(resourceType, next); } @@ -986,45 +1004,52 @@ export class RampsController extends BaseController< } /** - * Updates a single field (isLoading or error) on a resource state. - * All resources share the same ResourceState structure, so we use - * dynamic property access to avoid duplicating switch statements. + * Updates one or more fields on a resource state atomically in a single + * `this.update()` call. All resources share the same ResourceState structure, + * so we use dynamic property access to avoid duplicating switch statements. * * @param resourceType - The type of resource. - * @param field - The field to update ('isLoading' or 'error'). - * @param value - The value to set. + * @param fields - An object mapping field names to their new values. */ - #updateResourceField( + #updateResourceFields( resourceType: ResourceType, - field: 'isLoading' | 'error', - value: boolean | string | null, + fields: Partial< + Record<'isLoading' | 'error' | 'status', boolean | string | null> + >, ): void { this.update((state) => { const resource = state[resourceType]; if (resource) { - (resource as Record)[field] = value; + for (const [field, value] of Object.entries(fields)) { + (resource as Record)[field] = value; + } } }); } /** - * Sets the loading state for a resource type. + * Sets the error state for a resource type. * * @param resourceType - The type of resource. - * @param loading - Whether the resource is loading. + * @param error - The error message, or null to clear. */ - #setResourceLoading(resourceType: ResourceType, loading: boolean): void { - this.#updateResourceField(resourceType, 'isLoading', loading); + #setResourceError(resourceType: ResourceType, error: string | null): void { + this.#updateResourceFields(resourceType, { error }); } /** - * Sets the error state for a resource type. + * Sets the loading state and status for a resource type atomically. * * @param resourceType - The type of resource. - * @param error - The error message, or null to clear. + * @param loading - Whether the resource is loading. + * @param status - The status to set ('idle' | 'loading' | 'success' | 'error'). */ - #setResourceError(resourceType: ResourceType, error: string | null): void { - this.#updateResourceField(resourceType, 'error', error); + #setResourceLoadingAndStatus( + resourceType: ResourceType, + loading: boolean, + status: `${RequestStatus}`, + ): void { + this.#updateResourceFields(resourceType, { isLoading: loading, status }); } /** @@ -2072,6 +2097,7 @@ export class RampsController extends BaseController< this.update((state) => { state.nativeProviders.transak.userDetails.isLoading = true; state.nativeProviders.transak.userDetails.error = null; + state.nativeProviders.transak.userDetails.status = RequestStatus.LOADING; }); try { const details = await this.messenger.call( @@ -2080,6 +2106,8 @@ export class RampsController extends BaseController< this.update((state) => { state.nativeProviders.transak.userDetails.data = details; state.nativeProviders.transak.userDetails.isLoading = false; + state.nativeProviders.transak.userDetails.status = + RequestStatus.SUCCESS; }); return details; } catch (error) { @@ -2088,6 +2116,7 @@ export class RampsController extends BaseController< this.update((state) => { state.nativeProviders.transak.userDetails.isLoading = false; state.nativeProviders.transak.userDetails.error = errorMessage; + state.nativeProviders.transak.userDetails.status = RequestStatus.ERROR; }); throw error; } @@ -2114,6 +2143,7 @@ export class RampsController extends BaseController< this.update((state) => { state.nativeProviders.transak.buyQuote.isLoading = true; state.nativeProviders.transak.buyQuote.error = null; + state.nativeProviders.transak.buyQuote.status = RequestStatus.LOADING; }); try { const quote = await this.messenger.call( @@ -2127,6 +2157,7 @@ export class RampsController extends BaseController< this.update((state) => { state.nativeProviders.transak.buyQuote.data = quote; state.nativeProviders.transak.buyQuote.isLoading = false; + state.nativeProviders.transak.buyQuote.status = RequestStatus.SUCCESS; }); return quote; } catch (error) { @@ -2134,6 +2165,7 @@ export class RampsController extends BaseController< this.update((state) => { state.nativeProviders.transak.buyQuote.isLoading = false; state.nativeProviders.transak.buyQuote.error = errorMessage; + state.nativeProviders.transak.buyQuote.status = RequestStatus.ERROR; }); throw error; } @@ -2152,6 +2184,8 @@ export class RampsController extends BaseController< this.update((state) => { state.nativeProviders.transak.kycRequirement.isLoading = true; state.nativeProviders.transak.kycRequirement.error = null; + state.nativeProviders.transak.kycRequirement.status = + RequestStatus.LOADING; }); try { const requirement = await this.messenger.call( @@ -2161,6 +2195,8 @@ export class RampsController extends BaseController< this.update((state) => { state.nativeProviders.transak.kycRequirement.data = requirement; state.nativeProviders.transak.kycRequirement.isLoading = false; + state.nativeProviders.transak.kycRequirement.status = + RequestStatus.SUCCESS; }); return requirement; } catch (error) { @@ -2169,6 +2205,8 @@ export class RampsController extends BaseController< this.update((state) => { state.nativeProviders.transak.kycRequirement.isLoading = false; state.nativeProviders.transak.kycRequirement.error = errorMessage; + state.nativeProviders.transak.kycRequirement.status = + RequestStatus.ERROR; }); throw error; } diff --git a/packages/ramps-controller/src/selectors.test.ts b/packages/ramps-controller/src/selectors.test.ts index 459ad64dedf..2a9d1e0d66b 100644 --- a/packages/ramps-controller/src/selectors.test.ts +++ b/packages/ramps-controller/src/selectors.test.ts @@ -3,6 +3,7 @@ import { createLoadingState, createSuccessState, createErrorState, + RequestStatus, } from './RequestCache'; import { createRequestSelector } from './selectors'; @@ -18,12 +19,14 @@ function createDefaultResourceState( selected: TSelected; isLoading: boolean; error: null; + status: `${RequestStatus}`; } { return { data, selected, isLoading: false, error: null, + status: RequestStatus.IDLE, }; }