@@ -12,6 +12,7 @@ import type {
1212 Eligibility ,
1313 TokensResponse ,
1414 Provider ,
15+ State ,
1516} from './RampsService' ;
1617import type {
1718 RampsServiceGetGeolocationAction ,
@@ -46,15 +47,33 @@ export const controllerName = 'RampsController';
4647
4748// === STATE ===
4849
50+ /**
51+ * Represents the user's selected region with full country and state objects.
52+ */
53+ export type UserRegion = {
54+ /**
55+ * The country object for the selected region.
56+ */
57+ country : Country ;
58+ /**
59+ * The state object if a state was selected, null if only country was selected.
60+ */
61+ state : State | null ;
62+ /**
63+ * The region code string (e.g., "us-ut" or "fr") used for API calls.
64+ */
65+ regionCode : string ;
66+ } ;
67+
4968/**
5069 * Describes the shape of the state object for {@link RampsController}.
5170 */
5271export type RampsControllerState = {
5372 /**
54- * The user's selected region code (e.g., "US-CA") .
73+ * The user's selected region with full country and state objects .
5574 * Initially set via geolocation fetch, but can be manually changed by the user.
5675 */
57- userRegion : string | null ;
76+ userRegion : UserRegion | null ;
5877 /**
5978 * The user's preferred provider.
6079 * Can be manually set by the user.
@@ -196,6 +215,70 @@ export type RampsControllerOptions = {
196215 requestCacheMaxSize ?: number ;
197216} ;
198217
218+ // === HELPER FUNCTIONS ===
219+
220+ /**
221+ * Finds a country and state from a region code string.
222+ *
223+ * @param regionCode - The region code (e.g., "us-ca" or "fr").
224+ * @param countries - Array of countries to search.
225+ * @returns UserRegion object with country and state, or null if not found.
226+ */
227+ function findRegionFromCode (
228+ regionCode : string ,
229+ countries : Country [ ] ,
230+ ) : UserRegion | null {
231+ const normalizedCode = regionCode . toLowerCase ( ) . trim ( ) ;
232+ const parts = normalizedCode . split ( '-' ) ;
233+ const countryCode = parts [ 0 ] ;
234+ const stateCode = parts [ 1 ] ;
235+
236+ const country = countries . find ( ( c ) => {
237+ if ( c . isoCode ?. toLowerCase ( ) === countryCode ) {
238+ return true ;
239+ }
240+ if ( c . id ) {
241+ const id = c . id . toLowerCase ( ) ;
242+ if ( id . startsWith ( '/regions/' ) ) {
243+ const extractedCode = id . replace ( '/regions/' , '' ) . split ( '/' ) [ 0 ] ;
244+ return extractedCode === countryCode ;
245+ }
246+ return id === countryCode || id . endsWith ( `/${ countryCode } ` ) ;
247+ }
248+ return false ;
249+ } ) ;
250+
251+ if ( ! country ) {
252+ return null ;
253+ }
254+
255+ let state : State | null = null ;
256+ if ( stateCode && country . states ) {
257+ state =
258+ country . states . find ( ( s ) => {
259+ if ( s . stateId ?. toLowerCase ( ) === stateCode ) {
260+ return true ;
261+ }
262+ if ( s . id ) {
263+ const stateId = s . id . toLowerCase ( ) ;
264+ if (
265+ stateId . includes ( `-${ stateCode } ` ) ||
266+ stateId . endsWith ( `/${ stateCode } ` )
267+ ) {
268+ return true ;
269+ }
270+ }
271+ return false ;
272+ } ) || null ;
273+ }
274+
275+ return {
276+ country,
277+ state,
278+ regionCode : normalizedCode ,
279+ } ;
280+ }
281+
199282// === CONTROLLER DEFINITION ===
200283
201284/**
@@ -418,18 +501,34 @@ export class RampsController extends BaseController<
418501 } ) ;
419502 }
420503
504+ /**
505+ * Gets countries data from cache or fetches it if not available.
506+ *
507+ * @param action - The ramp action type ('buy' or 'sell').
508+ * @param options - Options for cache behavior.
509+ * @returns Array of countries.
510+ */
511+ async #getCountriesData(
512+ action : 'buy' | 'sell' = 'buy' ,
513+ options ?: ExecuteRequestOptions ,
514+ ) : Promise < Country [ ] > {
515+ return this . getCountries ( action , options ) ;
516+ }
517+
421518 /**
422519 * Updates the user's region by fetching geolocation and eligibility.
423520 * This method calls the RampsService to get the geolocation,
424521 * then automatically fetches eligibility for that region.
425522 *
426523 * @param options - Options for cache behavior.
427- * @returns The user region string .
524+ * @returns The user region object .
428525 */
429- async updateUserRegion ( options ?: ExecuteRequestOptions ) : Promise < string > {
526+ async updateUserRegion (
527+ options ?: ExecuteRequestOptions ,
528+ ) : Promise < UserRegion | null > {
430529 const cacheKey = createCacheKey ( 'updateUserRegion' , [ ] ) ;
431530
432- const userRegion = await this . executeRequest (
531+ const regionCode = await this . executeRequest (
433532 cacheKey ,
434533 async ( ) => {
435534 const result = await this . messenger . call ( 'RampsService:getGeolocation' ) ;
@@ -438,30 +537,76 @@ export class RampsController extends BaseController<
438537 options ,
439538 ) ;
440539
441- const normalizedRegion = userRegion
442- ? userRegion . toLowerCase ( ) . trim ( )
443- : userRegion ;
540+ if ( ! regionCode ) {
541+ this . update ( ( state ) => {
542+ state . userRegion = null ;
543+ state . tokens = null ;
544+ } ) ;
545+ return null ;
546+ }
444547
445- this . update ( ( state ) => {
446- state . userRegion = normalizedRegion ;
447- state . tokens = null ;
448- } ) ;
548+ const normalizedRegion = regionCode . toLowerCase ( ) . trim ( ) ;
449549
450- if ( normalizedRegion ) {
451- try {
452- await this . updateEligibility ( normalizedRegion , options ) ;
453- } catch {
550+ try {
551+ const countries = await this . #getCountriesData( 'buy' , options ) ;
552+ const userRegion = findRegionFromCode ( normalizedRegion , countries ) ;
553+
554+ if ( userRegion ) {
454555 this . update ( ( state ) => {
455- const currentUserRegion = state . userRegion ?. toLowerCase ( ) . trim ( ) ;
456- if ( currentUserRegion === normalizedRegion ) {
457- state . eligibility = null ;
458- state . tokens = null ;
459- }
556+ state . userRegion = userRegion ;
557+ state . tokens = null ;
460558 } ) ;
559+
560+ try {
561+ await this . updateEligibility ( userRegion . regionCode , options ) ;
562+ } catch {
563+ this . update ( ( state ) => {
564+ if ( state . userRegion ?. regionCode === userRegion . regionCode ) {
565+ state . eligibility = null ;
566+ state . tokens = null ;
567+ }
568+ } ) ;
569+ }
570+
571+ return userRegion ;
461572 }
573+ } catch {
574+ // If countries fetch fails, fall back to storing just the region code
575+ // This maintains backward compatibility
576+ }
577+
578+ // Fallback: store as region code only if countries not available
579+ // This shouldn't happen in normal flow, but handles edge cases
580+ const fallbackRegion : UserRegion = {
581+ country : {
582+ isoCode : normalizedRegion . split ( '-' ) [ 0 ] . toUpperCase ( ) ,
583+ flag : '🏳️' ,
584+ name : normalizedRegion ,
585+ phone : { prefix : '' , placeholder : '' , template : '' } ,
586+ currency : '' ,
587+ supported : false ,
588+ } ,
589+ state : null ,
590+ regionCode : normalizedRegion ,
591+ } ;
592+
593+ this . update ( ( state ) => {
594+ state . userRegion = fallbackRegion ;
595+ state . tokens = null ;
596+ } ) ;
597+
598+ try {
599+ await this . updateEligibility ( normalizedRegion , options ) ;
600+ } catch {
601+ this . update ( ( state ) => {
602+ if ( state . userRegion ?. regionCode === normalizedRegion ) {
603+ state . eligibility = null ;
604+ state . tokens = null ;
605+ }
606+ } ) ;
462607 }
463608
464- return normalizedRegion ;
609+ return fallbackRegion ;
465610 }
466611
467612 /**
@@ -478,22 +623,56 @@ export class RampsController extends BaseController<
478623 ) : Promise < Eligibility > {
479624 const normalizedRegion = region . toLowerCase ( ) . trim ( ) ;
480625
626+ try {
627+ const countries = await this . #getCountriesData( 'buy' , options ) ;
628+ const userRegion = findRegionFromCode ( normalizedRegion , countries ) ;
629+
630+ if ( userRegion ) {
631+ this . update ( ( state ) => {
632+ state . userRegion = userRegion ;
633+ state . tokens = null ;
634+ } ) ;
635+
636+ try {
637+ return await this . updateEligibility ( userRegion . regionCode , options ) ;
638+ } catch ( error ) {
639+ this . update ( ( state ) => {
640+ if ( state . userRegion ?. regionCode === userRegion . regionCode ) {
641+ state . eligibility = null ;
642+ state . tokens = null ;
643+ }
644+ } ) ;
645+ throw error ;
646+ }
647+ }
648+ } catch {
649+ // If countries fetch fails, fall back to storing just the region code
650+ }
651+
652+ // Fallback: store as region code only if countries not available
653+ const fallbackRegion : UserRegion = {
654+ country : {
655+ isoCode : normalizedRegion . split ( '-' ) [ 0 ] . toUpperCase ( ) ,
656+ flag : '🏳️' ,
657+ name : normalizedRegion ,
658+ phone : { prefix : '' , placeholder : '' , template : '' } ,
659+ currency : '' ,
660+ supported : false ,
661+ } ,
662+ state : null ,
663+ regionCode : normalizedRegion ,
664+ } ;
665+
481666 this . update ( ( state ) => {
482- state . userRegion = normalizedRegion ;
667+ state . userRegion = fallbackRegion ;
483668 state . tokens = null ;
484669 } ) ;
485670
486671 try {
487672 return await this . updateEligibility ( normalizedRegion , options ) ;
488673 } catch ( error ) {
489- // Eligibility fetch failed, but user region was successfully set.
490- // Don't let eligibility errors prevent user region state from being updated.
491- // Clear eligibility state to avoid showing stale data from a previous location.
492- // Only clear if the region still matches to avoid race conditions where a newer
493- // region change has already succeeded.
494674 this . update ( ( state ) => {
495- const currentUserRegion = state . userRegion ?. toLowerCase ( ) . trim ( ) ;
496- if ( currentUserRegion === normalizedRegion ) {
675+ if ( state . userRegion ?. regionCode === normalizedRegion ) {
497676 state . eligibility = null ;
498677 state . tokens = null ;
499678 }
@@ -531,7 +710,7 @@ export class RampsController extends BaseController<
531710
532711 if ( userRegion ) {
533712 try {
534- await this . getTokens ( userRegion , 'buy' , options ) ;
713+ await this . getTokens ( userRegion . regionCode , 'buy' , options ) ;
535714 } catch {
536715 // Token fetch failed - error state will be available via selectors
537716 }
@@ -564,9 +743,9 @@ export class RampsController extends BaseController<
564743 ) ;
565744
566745 this . update ( ( state ) => {
567- const userRegion = state . userRegion ?. toLowerCase ( ) . trim ( ) ;
746+ const userRegionCode = state . userRegion ?. regionCode ;
568747
569- if ( userRegion === undefined || userRegion === normalizedIsoCode ) {
748+ if ( userRegionCode === undefined || userRegionCode === normalizedIsoCode ) {
570749 state . eligibility = eligibility ;
571750 }
572751 } ) ;
@@ -610,7 +789,7 @@ export class RampsController extends BaseController<
610789 action : 'buy' | 'sell' = 'buy' ,
611790 options ?: ExecuteRequestOptions ,
612791 ) : Promise < TokensResponse > {
613- const regionToUse = region ?? this . state . userRegion ;
792+ const regionToUse = region ?? this . state . userRegion ?. regionCode ;
614793
615794 if ( ! regionToUse ) {
616795 throw new Error (
@@ -634,9 +813,9 @@ export class RampsController extends BaseController<
634813 ) ;
635814
636815 this . update ( ( state ) => {
637- const userRegion = state . userRegion ?. toLowerCase ( ) . trim ( ) ;
816+ const userRegionCode = state . userRegion ?. regionCode ;
638817
639- if ( userRegion === undefined || userRegion === normalizedRegion ) {
818+ if ( userRegionCode === undefined || userRegionCode === normalizedRegion ) {
640819 state . tokens = tokens ;
641820 }
642821 } ) ;
0 commit comments