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
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;
|
|
}
|