Skip to content

Commit 0e04956

Browse files
committed
refactor(ramps-controller): store full region objects in userRegion
1 parent 4a1b947 commit 0e04956

File tree

2 files changed

+216
-36
lines changed

2 files changed

+216
-36
lines changed

packages/ramps-controller/src/RampsController.ts

Lines changed: 215 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {
1212
Eligibility,
1313
TokensResponse,
1414
Provider,
15+
State,
1516
} from './RampsService';
1617
import 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
*/
5271
export 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
});

packages/ramps-controller/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type {
66
RampsControllerState,
77
RampsControllerStateChangeEvent,
88
RampsControllerOptions,
9+
UserRegion,
910
} from './RampsController';
1011
export {
1112
RampsController,

0 commit comments

Comments
 (0)