import { headers } from "next/headers"; export interface LocationData { country?: string; countryCode?: string; currency?: string; city?: string; ip?: string; } /** * Gets the user's location based on their IP address or browser language. * Priority: 1) Cloudflare/Vercel headers → 2) IP API → 3) Accept-Language */ export async function getClientLocation(): Promise { const headerList = await headers(); // 0. Dev override: check for a query param or env var // During local dev you can set NEXT_PUBLIC_DEV_COUNTRY_CODE=AU to test if (process.env.NEXT_PUBLIC_DEV_COUNTRY_CODE) { return { countryCode: process.env.NEXT_PUBLIC_DEV_COUNTRY_CODE.toLowerCase(), ip: "dev-override", }; } // 1. Try Geolocation Headers first (Vercel/Cloudflare edge network) const vercelCountry = headerList.get("x-vercel-ip-country"); const cfCountry = headerList.get("cf-ipcountry"); const countryCodeFromHeaders = vercelCountry || cfCountry; if (countryCodeFromHeaders && countryCodeFromHeaders !== "XX") { return { countryCode: countryCodeFromHeaders.toLowerCase(), ip: headerList.get("x-forwarded-for")?.split(",")[0].trim() || "0.0.0.0", }; } // 2. Try IP-based geolocation const ip = headerList.get("x-forwarded-for")?.split(",")[0].trim() || headerList.get("x-real-ip") || ""; // In local dev, IP is usually ::1 or 127.0.0.1 — try IP API anyway // (ip-api.com handles localhost and returns the caller's actual public IP) if (ip) { try { // ip-api.com supports HTTP for localhost dev — it returns the // caller's public IP when called from a private IP const protocol = process.env.NODE_ENV === "production" ? "https" : "http"; const apiUrl = `${protocol}://ip-api.com/json/${ip === "::1" || ip === "127.0.0.1" ? "" : ip}?fields=status,message,country,countryCode,city,query`; const res = await fetch(apiUrl, { next: { revalidate: 3600 } }); if (res.ok) { const data = await res.json(); if (data.status === "success" && data.countryCode) { return { country: data.country, countryCode: data.countryCode.toLowerCase(), city: data.city, ip: data.query, }; } } } catch (error) { console.error("IP-API lookup failed:", error); } } // 3. Fallback: Accept-Language header — parse ALL locales, not just the first // Format: en-GB,en-US;q=0.9,en;q=0.8 // We prefer the FIRST locale that has a country code (e.g. "en-GB" not just "en") const acceptLanguage = headerList.get("accept-language"); if (acceptLanguage) { const locales = acceptLanguage.split(",").map((l) => l.trim().split(";")[0]); for (const locale of locales) { const parts = locale.split("-"); if (parts.length >= 2) { // Try to get country code (e.g. "en-GB" → "gb") const cc = parts[parts.length - 1]; if (cc && cc.length === 2) { return { countryCode: cc.toLowerCase(), ip: ip || "0.0.0.0", }; } } } } return null; }