diff --git a/packages/ramps-controller/src/RampsController.test.ts b/packages/ramps-controller/src/RampsController.test.ts index c2197bba703..e75c8b5391a 100644 --- a/packages/ramps-controller/src/RampsController.test.ts +++ b/packages/ramps-controller/src/RampsController.test.ts @@ -8,7 +8,11 @@ import type { import type { RampsControllerMessenger } from './RampsController'; import { RampsController } from './RampsController'; -import type { RampsServiceGetGeolocationAction } from './RampsService-method-action-types'; +import type { Country } from './RampsService'; +import type { + RampsServiceGetGeolocationAction, + RampsServiceGetCountriesAction, +} from './RampsService-method-action-types'; import { RequestStatus, createCacheKey } from './RequestCache'; describe('RampsController', () => { @@ -502,6 +506,323 @@ describe('RampsController', () => { }); }); }); + + describe('getCountries', () => { + const mockCountries: Country[] = [ + { + isoCode: 'US', + flag: 'πŸ‡ΊπŸ‡Έ', + name: 'United States of America', + phone: { + prefix: '+1', + placeholder: '(555) 123-4567', + template: '(XXX) XXX-XXXX', + }, + currency: 'USD', + supported: true, + recommended: true, + unsupportedStates: ['ny'], + transakSupported: true, + }, + { + isoCode: 'AT', + flag: 'πŸ‡¦πŸ‡Ή', + name: 'Austria', + phone: { + prefix: '+43', + placeholder: '660 1234567', + template: 'XXX XXXXXXX', + }, + currency: 'EUR', + supported: true, + transakSupported: true, + }, + ]; + + it('fetches countries from the service', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => mockCountries, + ); + + const countries = await controller.getCountries('deposit'); + + expect(countries).toMatchInlineSnapshot(` + Array [ + Object { + "currency": "USD", + "flag": "πŸ‡ΊπŸ‡Έ", + "isoCode": "US", + "name": "United States of America", + "phone": Object { + "placeholder": "(555) 123-4567", + "prefix": "+1", + "template": "(XXX) XXX-XXXX", + }, + "recommended": true, + "supported": true, + "transakSupported": true, + "unsupportedStates": Array [ + "ny", + ], + }, + Object { + "currency": "EUR", + "flag": "πŸ‡¦πŸ‡Ή", + "isoCode": "AT", + "name": "Austria", + "phone": Object { + "placeholder": "660 1234567", + "prefix": "+43", + "template": "XXX XXXXXXX", + }, + "supported": true, + "transakSupported": true, + }, + ] + `); + }); + }); + + it('caches countries response', async () => { + await withController(async ({ controller, rootMessenger }) => { + let callCount = 0; + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => { + callCount += 1; + return mockCountries; + }, + ); + + await controller.getCountries('deposit'); + await controller.getCountries('deposit'); + + expect(callCount).toBe(1); + }); + }); + }); + + describe('getRegionEligibility', () => { + const mockCountries: Country[] = [ + { + isoCode: 'US', + flag: 'πŸ‡ΊπŸ‡Έ', + name: 'United States of America', + phone: { + prefix: '+1', + placeholder: '(555) 123-4567', + template: '(XXX) XXX-XXXX', + }, + currency: 'USD', + supported: true, + unsupportedStates: ['ny'], + }, + { + isoCode: 'AT', + flag: 'πŸ‡¦πŸ‡Ή', + name: 'Austria', + phone: { + prefix: '+43', + placeholder: '660 1234567', + template: 'XXX XXXXXXX', + }, + currency: 'EUR', + supported: true, + }, + { + isoCode: 'RU', + flag: 'πŸ‡·πŸ‡Ί', + name: 'Russia', + phone: { + prefix: '+7', + placeholder: '999 123-45-67', + template: 'XXX XXX-XX-XX', + }, + currency: 'RUB', + supported: false, + }, + ]; + + it('fetches geolocation and returns eligibility when geolocation is null', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getGeolocation', + async () => 'AT', + ); + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => mockCountries, + ); + + expect(controller.state.geolocation).toBeNull(); + + const eligible = await controller.getRegionEligibility('deposit'); + + expect(controller.state.geolocation).toBe('AT'); + expect(eligible).toBe(true); + }); + }); + + it('fetches geolocation and returns false for unsupported region when geolocation is null', async () => { + await withController(async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getGeolocation', + async () => 'RU', + ); + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => mockCountries, + ); + + expect(controller.state.geolocation).toBeNull(); + + const eligible = await controller.getRegionEligibility('deposit'); + + expect(controller.state.geolocation).toBe('RU'); + expect(eligible).toBe(false); + }); + }); + + it('only fetches geolocation once when already set', async () => { + await withController(async ({ controller, rootMessenger }) => { + let geolocationCallCount = 0; + rootMessenger.registerActionHandler( + 'RampsService:getGeolocation', + async () => { + geolocationCallCount += 1; + return 'AT'; + }, + ); + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => mockCountries, + ); + + await controller.getRegionEligibility('deposit'); + await controller.getRegionEligibility('deposit'); + + expect(geolocationCallCount).toBe(1); + }); + }); + + it('returns true for a supported country', async () => { + await withController( + { options: { state: { geolocation: 'AT' } } }, + async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => mockCountries, + ); + + const eligible = await controller.getRegionEligibility('deposit'); + + expect(eligible).toBe(true); + }, + ); + }); + + it('returns true for a supported US state', async () => { + await withController( + { options: { state: { geolocation: 'US-TX' } } }, + async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => mockCountries, + ); + + const eligible = await controller.getRegionEligibility('deposit'); + + expect(eligible).toBe(true); + }, + ); + }); + + it('returns false for an unsupported US state', async () => { + await withController( + { options: { state: { geolocation: 'US-NY' } } }, + async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => mockCountries, + ); + + const eligible = await controller.getRegionEligibility('deposit'); + + expect(eligible).toBe(false); + }, + ); + }); + + it('returns false for a country not in the list', async () => { + await withController( + { options: { state: { geolocation: 'XX' } } }, + async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => mockCountries, + ); + + const eligible = await controller.getRegionEligibility('deposit'); + + expect(eligible).toBe(false); + }, + ); + }); + + it('returns false for a country that is not supported', async () => { + await withController( + { options: { state: { geolocation: 'RU' } } }, + async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => mockCountries, + ); + + const eligible = await controller.getRegionEligibility('deposit'); + + expect(eligible).toBe(false); + }, + ); + }); + + it('is case-insensitive for state codes', async () => { + await withController( + { options: { state: { geolocation: 'US-ny' } } }, + async ({ controller, rootMessenger }) => { + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async () => mockCountries, + ); + + const eligible = await controller.getRegionEligibility('deposit'); + + expect(eligible).toBe(false); + }, + ); + }); + + it('passes action parameter to getCountries', async () => { + await withController( + { options: { state: { geolocation: 'AT' } } }, + async ({ controller, rootMessenger }) => { + let receivedAction: string | undefined; + rootMessenger.registerActionHandler( + 'RampsService:getCountries', + async (action) => { + receivedAction = action; + return mockCountries; + }, + ); + + await controller.getRegionEligibility('deposit'); + + expect(receivedAction).toBe('deposit'); + }, + ); + }); + }); }); /** @@ -510,7 +831,9 @@ describe('RampsController', () => { */ type RootMessenger = Messenger< MockAnyNamespace, - MessengerActions | RampsServiceGetGeolocationAction, + | MessengerActions + | RampsServiceGetGeolocationAction + | RampsServiceGetCountriesAction, MessengerEvents >; @@ -554,7 +877,7 @@ function getMessenger(rootMessenger: RootMessenger): RampsControllerMessenger { }); rootMessenger.delegate({ messenger, - actions: ['RampsService:getGeolocation'], + actions: ['RampsService:getGeolocation', 'RampsService:getCountries'], }); return messenger; } diff --git a/packages/ramps-controller/src/RampsController.ts b/packages/ramps-controller/src/RampsController.ts index 9b72f9dfdbb..7dbc657bbd1 100644 --- a/packages/ramps-controller/src/RampsController.ts +++ b/packages/ramps-controller/src/RampsController.ts @@ -7,7 +7,11 @@ import { BaseController } from '@metamask/base-controller'; import type { Messenger } from '@metamask/messenger'; import type { Json } from '@metamask/utils'; -import type { RampsServiceGetGeolocationAction } from './RampsService-method-action-types'; +import type { Country } from './RampsService'; +import type { + RampsServiceGetGeolocationAction, + RampsServiceGetCountriesAction, +} from './RampsService-method-action-types'; import type { RequestCache as RequestCacheType, RequestState, @@ -101,7 +105,9 @@ export type RampsControllerActions = RampsControllerGetStateAction; /** * Actions from other messengers that {@link RampsController} calls. */ -type AllowedActions = RampsServiceGetGeolocationAction; +type AllowedActions = + | RampsServiceGetGeolocationAction + | RampsServiceGetCountriesAction; /** * Published when the state of {@link RampsController} changes. @@ -393,4 +399,70 @@ export class RampsController extends BaseController< return geolocation; } + + /** + * Fetches the list of supported countries for a given ramp action. + * + * @param action - The ramp action type + * @param options - Options for cache behavior. + * @returns An array of countries with their eligibility information. + */ + async getCountries( + action: 'deposit', + options?: ExecuteRequestOptions, + ): Promise { + const cacheKey = createCacheKey('getCountries', [action]); + + return this.executeRequest( + cacheKey, + async () => { + return this.messenger.call('RampsService:getCountries', action); + }, + options, + ); + } + + /** + * Determines if the user's current region is eligible for ramps. + * Checks the user's geolocation against the list of supported countries. + * + * @param action - The ramp action type ('deposit' or 'withdraw'). + * @param options - Options for cache behavior. + * @returns True if the user's region is eligible for ramps, false otherwise. + */ + async getRegionEligibility( + action: 'deposit', + options?: ExecuteRequestOptions, + ): Promise { + const { geolocation } = this.state; + + if (!geolocation) { + await this.updateGeolocation(options); + return this.getRegionEligibility(action, options); + } + + const countries = await this.getCountries(action, options); + + const countryCode = geolocation.split('-')[0]; + const stateCode = geolocation.split('-')[1]?.toLowerCase(); + + const country = countries.find( + (entry) => entry.isoCode.toUpperCase() === countryCode?.toUpperCase(), + ); + + if (!country?.supported) { + return false; + } + + if ( + stateCode && + country.unsupportedStates?.some( + (state) => state.toLowerCase() === stateCode, + ) + ) { + return false; + } + + return true; + } } diff --git a/packages/ramps-controller/src/RampsService-method-action-types.ts b/packages/ramps-controller/src/RampsService-method-action-types.ts index c6a0dffbd75..152c4258b52 100644 --- a/packages/ramps-controller/src/RampsService-method-action-types.ts +++ b/packages/ramps-controller/src/RampsService-method-action-types.ts @@ -16,7 +16,20 @@ export type RampsServiceGetGeolocationAction = { handler: RampsService['getGeolocation']; }; +/** + * Makes a request to the cached API to retrieve the list of supported countries. + * + * @param action - The ramp action type ('deposit' or 'withdraw'). + * @returns An array of countries with their eligibility information. + */ +export type RampsServiceGetCountriesAction = { + type: `RampsService:getCountries`; + handler: RampsService['getCountries']; +}; + /** * Union of all RampsService action types. */ -export type RampsServiceMethodActions = RampsServiceGetGeolocationAction; +export type RampsServiceMethodActions = + | RampsServiceGetGeolocationAction + | RampsServiceGetCountriesAction; diff --git a/packages/ramps-controller/src/RampsService.test.ts b/packages/ramps-controller/src/RampsService.test.ts index 31fda495ca9..46c36279997 100644 --- a/packages/ramps-controller/src/RampsService.test.ts +++ b/packages/ramps-controller/src/RampsService.test.ts @@ -51,8 +51,10 @@ describe('RampsService', () => { expect(geolocationResponse).toBe('US-TX'); }); - it('uses localhost URL when environment is Development', async () => { - nock('http://localhost:3000').get('/geolocation').reply(200, 'US-TX'); + it('uses staging URL when environment is Development', async () => { + nock('https://on-ramp.uat-api.cx.metamask.io') + .get('/geolocation') + .reply(200, 'US-TX'); const { rootMessenger } = getService({ options: { environment: RampsEnvironment.Development }, }); @@ -64,13 +66,6 @@ describe('RampsService', () => { expect(geolocationResponse).toBe('US-TX'); }); - it('throws if the environment is invalid', () => { - expect(() => - getService({ - options: { environment: 'invalid' as RampsEnvironment }, - }), - ).toThrow('Invalid environment: invalid'); - }); it('throws if the API returns an empty response', async () => { nock('https://on-ramp.uat-api.cx.metamask.io') @@ -139,6 +134,292 @@ describe('RampsService', () => { expect(geolocationResponse).toBe('US-TX'); }); }); + + describe('RampsService:getCountries', () => { + const mockCountriesResponse = [ + { + isoCode: 'US', + flag: 'πŸ‡ΊπŸ‡Έ', + name: 'United States of America', + phone: { + prefix: '+1', + placeholder: '(555) 123-4567', + template: '(XXX) XXX-XXXX', + }, + currency: 'USD', + supported: true, + recommended: true, + unsupportedStates: ['ny'], + transakSupported: true, + }, + { + isoCode: 'AT', + flag: 'πŸ‡¦πŸ‡Ή', + name: 'Austria', + phone: { + prefix: '+43', + placeholder: '660 1234567', + template: 'XXX XXXXXXX', + }, + currency: 'EUR', + supported: true, + transakSupported: true, + }, + ]; + + it('returns the countries from the cache API', async () => { + nock('https://on-ramp-cache.uat-api.cx.metamask.io') + .get('/regions/countries') + .query({ action: 'deposit', sdk: '2.1.6', context: 'mobile-ios' }) + .reply(200, mockCountriesResponse); + const { rootMessenger } = getService(); + + const countriesResponse = await rootMessenger.call( + 'RampsService:getCountries', + 'deposit', + ); + + expect(countriesResponse).toMatchInlineSnapshot(` + Array [ + Object { + "currency": "USD", + "flag": "πŸ‡ΊπŸ‡Έ", + "isoCode": "US", + "name": "United States of America", + "phone": Object { + "placeholder": "(555) 123-4567", + "prefix": "+1", + "template": "(XXX) XXX-XXXX", + }, + "recommended": true, + "supported": true, + "transakSupported": true, + "unsupportedStates": Array [ + "ny", + ], + }, + Object { + "currency": "EUR", + "flag": "πŸ‡¦πŸ‡Ή", + "isoCode": "AT", + "name": "Austria", + "phone": Object { + "placeholder": "660 1234567", + "prefix": "+43", + "template": "XXX XXXXXXX", + }, + "supported": true, + "transakSupported": true, + }, + ] + `); + }); + + it('uses the production cache URL when environment is Production', async () => { + nock('https://on-ramp-cache.api.cx.metamask.io') + .get('/regions/countries') + .query({ action: 'deposit', sdk: '2.1.6', context: 'mobile-ios' }) + .reply(200, mockCountriesResponse); + const { rootMessenger } = getService({ + options: { environment: RampsEnvironment.Production }, + }); + + const countriesResponse = await rootMessenger.call( + 'RampsService:getCountries', + 'deposit', + ); + + expect(countriesResponse).toMatchInlineSnapshot(` + Array [ + Object { + "currency": "USD", + "flag": "πŸ‡ΊπŸ‡Έ", + "isoCode": "US", + "name": "United States of America", + "phone": Object { + "placeholder": "(555) 123-4567", + "prefix": "+1", + "template": "(XXX) XXX-XXXX", + }, + "recommended": true, + "supported": true, + "transakSupported": true, + "unsupportedStates": Array [ + "ny", + ], + }, + Object { + "currency": "EUR", + "flag": "πŸ‡¦πŸ‡Ή", + "isoCode": "AT", + "name": "Austria", + "phone": Object { + "placeholder": "660 1234567", + "prefix": "+43", + "template": "XXX XXXXXXX", + }, + "supported": true, + "transakSupported": true, + }, + ] + `); + }); + + it('uses staging cache URL when environment is Development', async () => { + nock('https://on-ramp-cache.uat-api.cx.metamask.io') + .get('/regions/countries') + .query({ action: 'deposit', sdk: '2.1.6', context: 'mobile-ios' }) + .reply(200, mockCountriesResponse); + const { rootMessenger } = getService({ + options: { environment: RampsEnvironment.Development }, + }); + + const countriesResponse = await rootMessenger.call( + 'RampsService:getCountries', + 'deposit', + ); + + expect(countriesResponse).toMatchInlineSnapshot(` + Array [ + Object { + "currency": "USD", + "flag": "πŸ‡ΊπŸ‡Έ", + "isoCode": "US", + "name": "United States of America", + "phone": Object { + "placeholder": "(555) 123-4567", + "prefix": "+1", + "template": "(XXX) XXX-XXXX", + }, + "recommended": true, + "supported": true, + "transakSupported": true, + "unsupportedStates": Array [ + "ny", + ], + }, + Object { + "currency": "EUR", + "flag": "πŸ‡¦πŸ‡Ή", + "isoCode": "AT", + "name": "Austria", + "phone": Object { + "placeholder": "660 1234567", + "prefix": "+43", + "template": "XXX XXXXXXX", + }, + "supported": true, + "transakSupported": true, + }, + ] + `); + }); + + it('passes the action parameter correctly', async () => { + nock('https://on-ramp-cache.uat-api.cx.metamask.io') + .get('/regions/countries') + .query({ action: 'withdraw', sdk: '2.1.6', context: 'mobile-ios' }) + .reply(200, mockCountriesResponse); + const { rootMessenger } = getService(); + + const countriesResponse = await rootMessenger.call( + 'RampsService:getCountries', + 'withdraw', + ); + + expect(countriesResponse).toMatchInlineSnapshot(` + Array [ + Object { + "currency": "USD", + "flag": "πŸ‡ΊπŸ‡Έ", + "isoCode": "US", + "name": "United States of America", + "phone": Object { + "placeholder": "(555) 123-4567", + "prefix": "+1", + "template": "(XXX) XXX-XXXX", + }, + "recommended": true, + "supported": true, + "transakSupported": true, + "unsupportedStates": Array [ + "ny", + ], + }, + Object { + "currency": "EUR", + "flag": "πŸ‡¦πŸ‡Ή", + "isoCode": "AT", + "name": "Austria", + "phone": Object { + "placeholder": "660 1234567", + "prefix": "+43", + "template": "XXX XXXXXXX", + }, + "supported": true, + "transakSupported": true, + }, + ] + `); + }); + + it('throws if the API returns an error', async () => { + nock('https://on-ramp-cache.uat-api.cx.metamask.io') + .get('/regions/countries') + .query({ action: 'deposit', sdk: '2.1.6', context: 'mobile-ios' }) + .times(4) + .reply(500); + const { service, rootMessenger } = getService(); + service.onRetry(() => { + clock.nextAsync().catch(() => undefined); + }); + + await expect( + rootMessenger.call('RampsService:getCountries', 'deposit'), + ).rejects.toThrow( + "Fetching 'https://on-ramp-cache.uat-api.cx.metamask.io/regions/countries?action=deposit&sdk=2.1.6&context=mobile-ios' failed with status '500'", + ); + }); + }); + + describe('getCountries', () => { + it('does the same thing as the messenger action', async () => { + const mockCountries = [ + { + isoCode: 'US', + flag: 'πŸ‡ΊπŸ‡Έ', + name: 'United States', + phone: { prefix: '+1', placeholder: '', template: '' }, + currency: 'USD', + supported: true, + }, + ]; + nock('https://on-ramp-cache.uat-api.cx.metamask.io') + .get('/regions/countries') + .query({ action: 'deposit', sdk: '2.1.6', context: 'mobile-ios' }) + .reply(200, mockCountries); + const { service } = getService(); + + const countriesResponse = await service.getCountries('deposit'); + + expect(countriesResponse).toMatchInlineSnapshot(` + Array [ + Object { + "currency": "USD", + "flag": "πŸ‡ΊπŸ‡Έ", + "isoCode": "US", + "name": "United States", + "phone": Object { + "placeholder": "", + "prefix": "+1", + "template": "", + }, + "supported": true, + }, + ] + `); + }); + }); }); /** diff --git a/packages/ramps-controller/src/RampsService.ts b/packages/ramps-controller/src/RampsService.ts index 3d0d88ab187..5ccbf3cdedc 100644 --- a/packages/ramps-controller/src/RampsService.ts +++ b/packages/ramps-controller/src/RampsService.ts @@ -7,6 +7,30 @@ import type { Messenger } from '@metamask/messenger'; import type { RampsServiceMethodActions } from './RampsService-method-action-types'; +/** + * Represents phone number information for a country. + */ +export type CountryPhone = { + prefix: string; + placeholder: string; + template: string; +}; + +/** + * Represents a country returned from the regions/countries API. + */ +export type Country = { + isoCode: string; + flag: string; + name: string; + phone: CountryPhone; + currency: string; + supported: boolean; + recommended?: boolean; + unsupportedStates?: string[]; + transakSupported?: boolean; +}; + // === GENERAL === /** @@ -24,9 +48,18 @@ export enum RampsEnvironment { Development = 'development', } +/** + * The type of ramps API service. + * Determines which base URL to use (cache vs standard). + */ +export enum RampsApiService { + Regions = 'regions', + Orders = 'orders', +} + // === MESSENGER === -const MESSENGER_EXPOSED_METHODS = ['getGeolocation'] as const; +const MESSENGER_EXPOSED_METHODS = ['getGeolocation', 'getCountries'] as const; /** * Actions that {@link RampsService} exposes to other consumers. @@ -61,19 +94,25 @@ export type RampsServiceMessenger = Messenger< // === SERVICE DEFINITION === /** - * Gets the base URL for API requests based on the environment. + * Gets the base URL for API requests based on the environment and service type. + * The Regions service uses a cache URL, while other services use the standard URL. * * @param environment - The environment to use. + * @param service - The API service type (determines if cache URL is used). * @returns The base URL for API requests. */ -function getBaseUrl(environment: RampsEnvironment): string { +function getBaseUrl( + environment: RampsEnvironment, + service: RampsApiService, +): string { + const cache = service === RampsApiService.Regions ? '-cache' : ''; + switch (environment) { case RampsEnvironment.Production: - return 'https://on-ramp.api.cx.metamask.io'; + return `https://on-ramp${cache}.api.cx.metamask.io`; case RampsEnvironment.Staging: - return 'https://on-ramp.uat-api.cx.metamask.io'; case RampsEnvironment.Development: - return 'http://localhost:3000'; + return `https://on-ramp${cache}.uat-api.cx.metamask.io`; default: throw new Error(`Invalid environment: ${String(environment)}`); } @@ -146,9 +185,9 @@ export class RampsService { readonly #policy: ServicePolicy; /** - * The base URL for API requests. + * The environment used for API requests. */ - readonly #baseUrl: string; + readonly #environment: RampsEnvironment; /** * Constructs a new RampsService object. @@ -178,7 +217,7 @@ export class RampsService { this.#messenger = messenger; this.#fetch = fetchFunction; this.#policy = createServicePolicy(policyOptions); - this.#baseUrl = getBaseUrl(environment); + this.#environment = environment; this.#messenger.registerMethodActionHandlers( this, @@ -248,7 +287,8 @@ export class RampsService { */ async getGeolocation(): Promise { const responseData = await this.#policy.execute(async () => { - const url = new URL('geolocation', this.#baseUrl); + const baseUrl = getBaseUrl(this.#environment, RampsApiService.Orders); + const url = new URL('geolocation', baseUrl); const localResponse = await this.#fetch(url); if (!localResponse.ok) { throw new HttpError( @@ -270,4 +310,33 @@ export class RampsService { throw new Error('Malformed response received from geolocation API'); } + + /** + * Makes a request to the cached API to retrieve the list of supported countries. + * + * @param action - The ramp action type ('deposit' or 'withdraw'). + * @returns An array of countries with their eligibility information. + */ + async getCountries( + action: 'deposit' | 'withdraw' = 'deposit', + ): Promise { + const responseData = await this.#policy.execute(async () => { + const baseUrl = getBaseUrl(this.#environment, RampsApiService.Regions); + const url = new URL('regions/countries', baseUrl); + url.searchParams.set('action', action); + url.searchParams.set('sdk', '2.1.6'); + url.searchParams.set('context', 'mobile-ios'); + + const localResponse = await this.#fetch(url); + if (!localResponse.ok) { + throw new HttpError( + localResponse.status, + `Fetching '${url.toString()}' failed with status '${localResponse.status}'`, + ); + } + return localResponse.json() as Promise; + }); + + return responseData; + } } diff --git a/packages/ramps-controller/src/index.ts b/packages/ramps-controller/src/index.ts index 78527ba470b..7610db0a1d6 100644 --- a/packages/ramps-controller/src/index.ts +++ b/packages/ramps-controller/src/index.ts @@ -15,9 +15,14 @@ export type { RampsServiceActions, RampsServiceEvents, RampsServiceMessenger, + Country, + CountryPhone, } from './RampsService'; -export { RampsService, RampsEnvironment } from './RampsService'; -export type { RampsServiceGetGeolocationAction } from './RampsService-method-action-types'; +export { RampsService, RampsEnvironment, RampsApiService } from './RampsService'; +export type { + RampsServiceGetGeolocationAction, + RampsServiceGetCountriesAction, +} from './RampsService-method-action-types'; export type { RequestCache, RequestState,