You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

303 lines
9.0 KiB

"use client";
import { PhoneNumberUtil } from "google-libphonenumber";
import { resolveCountryName } from "@/data/countries";
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 ||
parsed.city ||
parsed.countryCode)
) {
cachedRegion = parsed;
return parsed;
}
}
} catch {}
}
return null;
}
export function setStoredUserGeoRegion(region: UserGeoRegion) {
const current =
cachedRegion ||
(typeof window !== "undefined" ? getStoredUserGeoRegion() : null);
const merged: UserGeoRegion = {
...(current || {}),
...region,
};
cachedRegion = merged;
if (typeof window !== "undefined") {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(merged));
if (merged.phoneCode) {
localStorage.setItem(PHONE_STORAGE_KEY, merged.phoneCode);
}
} catch {}
}
listeners.forEach((fn) => {
fn(merged);
});
}
function resolvePhoneCodeFromCountryCode(isoCode?: string): string | undefined {
if (!isoCode) return undefined;
try {
const callingCode = phoneUtil.getCountryCodeForRegion(
isoCode.toUpperCase(),
);
if (callingCode) {
return `+${callingCode}`;
}
} catch {}
return undefined;
}
function getFallbackGeoRegion(): UserGeoRegion {
const existing = getStoredUserGeoRegion();
if (
existing &&
(existing.country ||
existing.phoneCode ||
existing.countryCode ||
existing.city)
) {
console.log(
"[GEO_BRIDGE_LOG] 📦 Preserving existing stored user geo region:",
JSON.stringify(existing),
);
return existing;
}
console.log("[GEO_BRIDGE_LOG] ⚠️ Using default fallback region (+44)...");
const defaultRegion: UserGeoRegion = {
phoneCode: "+44",
};
setStoredUserGeoRegion(defaultRegion);
return defaultRegion;
}
/**
* Fetch geo region strictly via Flutter bridge action protocol ('get_location').
* No direct backend HTTP requests are made.
*/
function fetchFlutterBridgeGeoRegion(): Promise<UserGeoRegion> {
return new Promise<UserGeoRegion>((resolve) => {
let resolved = false;
let unsubscribe: (() => void) | undefined;
let timer: ReturnType<typeof setTimeout> | null = null;
const finish = (region: UserGeoRegion) => {
if (resolved) return;
resolved = true;
if (timer) {
clearTimeout(timer);
timer = null;
}
if (unsubscribe) {
try {
unsubscribe();
} catch {}
unsubscribe = undefined;
}
resolve(region);
};
console.log(
"[GEO_BRIDGE_LOG] 🌉 Registering Flutter response listener for get_location...",
);
// 1) Register a listener with window.addFlutterResponseListener
if (
typeof window !== "undefined" &&
typeof window.addFlutterResponseListener === "function"
) {
unsubscribe = window.addFlutterResponseListener(
(event: FlutterResponseEvent) => {
if (!event) return;
const action = event.action?.toLowerCase();
// 2) Listens for event.action === "get_location"
if (action === "get_location") {
console.log(
"[GEO_BRIDGE_LOG] 📥 Flutter get_location response received:",
JSON.stringify(event),
);
const data = (event.data || (event as any).payload) as
| {
ip?: string;
country?: string;
country_code?: string;
city?: string;
}
| undefined;
if (
event.success &&
data &&
(data.country || data.country_code || data.city || data.ip)
) {
const rawCountry = data.country || data.country_code || "";
const isoCode =
data.country_code ||
(rawCountry && rawCountry.trim().length === 2
? rawCountry.trim().toUpperCase()
: undefined);
const phoneCode = resolvePhoneCodeFromCountryCode(isoCode);
const countryName =
resolveCountryName(data.country || isoCode, "en") ||
data.country;
const countryFa =
resolveCountryName(data.country || isoCode, "fa") ||
countryName;
console.log(
`[GEO_BRIDGE_LOG] 🌍 Resolved from Flutter bridge: FA=${countryFa} | EN=${countryName} | Code=${isoCode} | City=${data.city || "None"} | IP=${data.ip || "None"} | Phone=${phoneCode || "+44"}`,
);
const region: UserGeoRegion = {
ip: data.ip,
city: data.city,
country: countryName,
countryCode: isoCode,
phoneCode: phoneCode || "+44",
};
setStoredUserGeoRegion(region);
finish(region);
} else {
console.warn(
"[GEO_BRIDGE_LOG] ⚠️ Flutter get_location unsuccessful or empty, falling back to local cached default",
);
finish(getFallbackGeoRegion());
}
}
},
);
}
// 3) Posts message: window.HabibApp.postMessage(JSON.stringify({ action: "get_location" }))
try {
if (typeof window !== "undefined" && window.HabibApp?.postMessage) {
console.log(
"[GEO_BRIDGE_LOG] 📤 Posting { action: 'get_location' } to window.HabibApp...",
);
window.HabibApp.postMessage(JSON.stringify({ action: "get_location" }));
} else if (
typeof window !== "undefined" &&
typeof (window as any).sendToFlutter === "function"
) {
console.log(
"[GEO_BRIDGE_LOG] 📤 Sending get_location via sendToFlutter...",
);
(window as any).sendToFlutter("get_location");
}
} catch (e) {
console.error(
"[GEO_BRIDGE_LOG] ❌ Failed to post get_location message:",
e,
);
}
// 4) Has a timeout fallback of 4000ms: if no response from Flutter, fallback to cached / default region (no HTTP)
timer = setTimeout(() => {
if (!resolved) {
console.warn(
"[GEO_BRIDGE_LOG] ⏱️ Flutter bridge get_location timed out after 4000ms, using fallback region...",
);
finish(getFallbackGeoRegion());
}
}, 4000);
});
}
/**
* Get user geo region.
* Uses Flutter bridge action ('get_location') exclusively for location detection.
* Never performs direct HTTP requests.
*/
export function getUserGeoRegion(force = false): Promise<UserGeoRegion> {
// If !force: Check cachedRegion or getStoredUserGeoRegion(). If present and has location, return it immediately.
if (!force) {
const existing = cachedRegion || getStoredUserGeoRegion();
if (
existing &&
(existing.country || existing.countryCode || existing.city)
) {
console.log(
"[GEO_BRIDGE_LOG] 📦 Returning cached/stored user geo region:",
JSON.stringify(existing),
);
return Promise.resolve(existing);
}
} else {
cachedRegion = null;
geoRegionPromise = null;
}
if (geoRegionPromise) {
return geoRegionPromise;
}
// Check if running inside Flutter Webview: typeof window !== "undefined" && window.HabibApp?.postMessage
const isFlutter =
typeof window !== "undefined" &&
(Boolean(window.HabibApp?.postMessage) ||
typeof (window as any).sendToFlutter === "function");
if (isFlutter) {
console.log(
"[GEO_BRIDGE_LOG] 📱 Inside Flutter WebView detected, requesting location via Flutter bridge action 'get_location'",
);
geoRegionPromise = fetchFlutterBridgeGeoRegion().finally(() => {
geoRegionPromise = null;
});
} else {
console.log(
"[GEO_BRIDGE_LOG] 🌐 Standard browser / non-Flutter environment: resolving from cached storage or default fallback (no HTTP calls)",
);
const fallback = getFallbackGeoRegion();
geoRegionPromise = Promise.resolve(fallback);
}
return geoRegionPromise;
}