Browse Source
refactor(geo): centralize geolocation logic into a unified utility
refactor(geo): centralize geolocation logic into a unified utility
Extract geolocation detection and storage logic from `QuestionBirthplace` and `QuestionPhone` into a new `geo-region.ts` library. This provides a single source of truth for user region data, including city, country, and phone calling codes. - Create `src/lib/geo-region.ts` to manage GeoIP fetching, caching, and subscription-based updates. - Refactor `QuestionBirthplace` to use the new `getUserGeoRegion` utility instead of inline fetch logic. - Refactor `QuestionPhone` to use the unified storage and retrieval mechanism for phone calling codes. - Add unit tests in `ui-config.test.tsx` to verify auto-detection behavior when `enable_geoip` is enabled.master
4 changed files with 319 additions and 225 deletions
-
105src/components/Componentes/question-birthplace.tsx
-
185src/components/Componentes/question-phone.tsx
-
43src/components/Componentes/ui-config.test.tsx
-
211src/lib/geo-region.ts
@ -0,0 +1,211 @@ |
|||
"use client"; |
|||
|
|||
import { PhoneNumberUtil } from "google-libphonenumber"; |
|||
import { http } from "./http"; |
|||
|
|||
export type UserGeoRegion = { |
|||
ip?: string; |
|||
city?: string; |
|||
country?: string; |
|||
countryCode?: string; // e.g. "IR", "US", "GB"
|
|||
phoneCode?: string; // e.g. "+98", "+1", "+44"
|
|||
}; |
|||
|
|||
const phoneUtil = PhoneNumberUtil.getInstance(); |
|||
|
|||
const STORAGE_KEY = "user_geo_region"; |
|||
const PHONE_STORAGE_KEY = "geoIPPhoneCode"; |
|||
|
|||
let cachedRegion: UserGeoRegion | null = null; |
|||
let geoRegionPromise: Promise<UserGeoRegion> | null = null; |
|||
const listeners = new Set<(region: UserGeoRegion) => void>(); |
|||
|
|||
export function resetUserGeoRegionForTesting() { |
|||
cachedRegion = null; |
|||
geoRegionPromise = null; |
|||
listeners.clear(); |
|||
} |
|||
|
|||
export function subscribeToUserGeoRegion( |
|||
fn: (region: UserGeoRegion) => void, |
|||
): () => void { |
|||
listeners.add(fn); |
|||
return () => { |
|||
listeners.delete(fn); |
|||
}; |
|||
} |
|||
|
|||
export function getStoredUserGeoRegion(): UserGeoRegion | null { |
|||
if (cachedRegion) return cachedRegion; |
|||
if (typeof window !== "undefined") { |
|||
try { |
|||
const stored = localStorage.getItem(STORAGE_KEY); |
|||
if (stored) { |
|||
const parsed = JSON.parse(stored) as UserGeoRegion; |
|||
if (parsed && (parsed.country || parsed.phoneCode)) { |
|||
cachedRegion = parsed; |
|||
return parsed; |
|||
} |
|||
} |
|||
} catch {} |
|||
} |
|||
return null; |
|||
} |
|||
|
|||
export function setStoredUserGeoRegion(region: UserGeoRegion) { |
|||
cachedRegion = region; |
|||
if (typeof window !== "undefined") { |
|||
try { |
|||
localStorage.setItem(STORAGE_KEY, JSON.stringify(region)); |
|||
if (region.phoneCode) { |
|||
localStorage.setItem(PHONE_STORAGE_KEY, region.phoneCode); |
|||
} |
|||
} catch {} |
|||
} |
|||
listeners.forEach((fn) => fn(region)); |
|||
} |
|||
|
|||
function resolvePhoneCodeFromCountryCode(isoCode?: string): string | undefined { |
|||
if (!isoCode) return undefined; |
|||
try { |
|||
const callingCode = phoneUtil.getCountryCodeForRegion( |
|||
isoCode.toUpperCase(), |
|||
); |
|||
if (callingCode) { |
|||
return `+${callingCode}`; |
|||
} |
|||
} catch {} |
|||
return undefined; |
|||
} |
|||
|
|||
export function getUserGeoRegion(): Promise<UserGeoRegion> { |
|||
const existing = getStoredUserGeoRegion(); |
|||
if (existing && (existing.city || existing.country)) { |
|||
return Promise.resolve(existing); |
|||
} |
|||
|
|||
if (geoRegionPromise) { |
|||
return geoRegionPromise; |
|||
} |
|||
|
|||
geoRegionPromise = (async () => { |
|||
try { |
|||
// 1. Primary: Habib Backend User Region API (/account/auth/user/region/)
|
|||
try { |
|||
const response = await http.get<{ |
|||
ip?: string; |
|||
country?: string; |
|||
country_code?: string; |
|||
city?: string; |
|||
}>("/account/auth/user/region/", { |
|||
timeout: 3000, |
|||
}); |
|||
|
|||
const data = response.data; |
|||
if (data && (data.country || data.country_code || data.city)) { |
|||
const phoneCode = resolvePhoneCodeFromCountryCode(data.country_code); |
|||
const region: UserGeoRegion = { |
|||
ip: data.ip, |
|||
city: data.city, |
|||
country: data.country, |
|||
countryCode: data.country_code, |
|||
phoneCode: phoneCode || "+44", |
|||
}; |
|||
setStoredUserGeoRegion(region); |
|||
return region; |
|||
} |
|||
} catch { |
|||
// Fallback to secondary geo endpoints
|
|||
} |
|||
|
|||
// 2. Secondary fallback: ipapi.co with 2s timeout
|
|||
const controller = new AbortController(); |
|||
const timeoutId = setTimeout(() => controller.abort(), 2000); |
|||
try { |
|||
const res = await fetch("https://ipapi.co/json/", { |
|||
signal: controller.signal, |
|||
}); |
|||
clearTimeout(timeoutId); |
|||
if (res?.ok) { |
|||
const data = await res.json(); |
|||
if ( |
|||
data && |
|||
(data.country_name || data.city || data.country_calling_code) |
|||
) { |
|||
const rawPhone = data.country_calling_code |
|||
? String(data.country_calling_code).trim() |
|||
: ""; |
|||
const phoneCode = rawPhone.startsWith("+") |
|||
? rawPhone |
|||
: rawPhone |
|||
? `+${rawPhone}` |
|||
: "+44"; |
|||
const region: UserGeoRegion = { |
|||
ip: data.ip, |
|||
city: data.city, |
|||
country: data.country_name, |
|||
countryCode: data.country_code, |
|||
phoneCode, |
|||
}; |
|||
setStoredUserGeoRegion(region); |
|||
return region; |
|||
} |
|||
} |
|||
} catch { |
|||
clearTimeout(timeoutId); |
|||
} |
|||
|
|||
// 3. Tertiary fallback: ipwho.is with 2s timeout
|
|||
const secondaryController = new AbortController(); |
|||
const secondaryTimeoutId = setTimeout( |
|||
() => secondaryController.abort(), |
|||
2000, |
|||
); |
|||
try { |
|||
const res = await fetch("https://ipwho.is/", { |
|||
signal: secondaryController.signal, |
|||
}); |
|||
clearTimeout(secondaryTimeoutId); |
|||
if (res?.ok) { |
|||
const data = await res.json(); |
|||
if (data && (data.country || data.city || data.calling_code)) { |
|||
const rawPhone = data.calling_code |
|||
? String(data.calling_code).trim() |
|||
: ""; |
|||
const phoneCode = rawPhone.startsWith("+") |
|||
? rawPhone |
|||
: rawPhone |
|||
? `+${rawPhone}` |
|||
: "+44"; |
|||
const region: UserGeoRegion = { |
|||
ip: data.ip, |
|||
city: data.city, |
|||
country: data.country, |
|||
countryCode: data.country_code, |
|||
phoneCode, |
|||
}; |
|||
setStoredUserGeoRegion(region); |
|||
return region; |
|||
} |
|||
} |
|||
} catch { |
|||
clearTimeout(secondaryTimeoutId); |
|||
} |
|||
|
|||
// 4. Default fallback
|
|||
const defaultRegion: UserGeoRegion = { |
|||
phoneCode: "+44", |
|||
}; |
|||
setStoredUserGeoRegion(defaultRegion); |
|||
return defaultRegion; |
|||
} catch { |
|||
const defaultRegion: UserGeoRegion = { |
|||
phoneCode: "+44", |
|||
}; |
|||
setStoredUserGeoRegion(defaultRegion); |
|||
return defaultRegion; |
|||
} |
|||
})(); |
|||
|
|||
return geoRegionPromise; |
|||
} |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue