Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/app/api/geolocation/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { NextResponse } from "next/server";

async function getHandler() {
const headersList = await headers();
const country = headersList.get("x-vercel-ip-country") || "Unknown";
const country = headersList.get("cf-ipcountry") || headersList.get("x-vercel-ip-country") || "Unknown";
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

const response = NextResponse.json({ country });
response.headers.set("Cache-Control", "public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400");
Expand Down
88 changes: 85 additions & 3 deletions apps/web/components/phone-input/PhoneInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,88 @@ function BasePhoneInputWeb({
);
}

// Maps IANA timezone to ISO 3166-1 alpha-2 country code (lowercase).
// navigator.language region is unreliable — browser language ≠ user location.
// Timezone is a much better proxy for physical location.
const TIMEZONE_COUNTRY_MAP: Record<string, string> = {
"Asia/Kolkata": "in",
"Asia/Calcutta": "in",
"America/New_York": "us",
"America/Chicago": "us",
"America/Denver": "us",
"America/Los_Angeles": "us",
"America/Anchorage": "us",
"Pacific/Honolulu": "us",
"Europe/London": "gb",
"Europe/Paris": "fr",
"Europe/Berlin": "de",
"Europe/Madrid": "es",
"Europe/Rome": "it",
"Europe/Amsterdam": "nl",
"Europe/Brussels": "be",
"Europe/Zurich": "ch",
"Europe/Vienna": "at",
"Europe/Stockholm": "se",
"Europe/Oslo": "no",
"Europe/Copenhagen": "dk",
"Europe/Helsinki": "fi",
"Europe/Warsaw": "pl",
"Europe/Prague": "cz",
"Europe/Bucharest": "ro",
"Europe/Athens": "gr",
"Europe/Istanbul": "tr",
"Europe/Moscow": "ru",
"Europe/Lisbon": "pt",
"Europe/Dublin": "ie",
"Asia/Tokyo": "jp",
"Asia/Shanghai": "cn",
"Asia/Hong_Kong": "hk",
"Asia/Singapore": "sg",
"Asia/Seoul": "kr",
"Asia/Taipei": "tw",
"Asia/Bangkok": "th",
"Asia/Jakarta": "id",
"Asia/Manila": "ph",
"Asia/Kuala_Lumpur": "my",
"Asia/Dubai": "ae",
"Asia/Riyadh": "sa",
"Asia/Karachi": "pk",
"Asia/Dhaka": "bd",
"Asia/Colombo": "lk",
"Asia/Kathmandu": "np",
"Asia/Ho_Chi_Minh": "vn",
"Australia/Sydney": "au",
"Australia/Melbourne": "au",
"Australia/Perth": "au",
"Pacific/Auckland": "nz",
"America/Toronto": "ca",
"America/Vancouver": "ca",
"America/Mexico_City": "mx",
"America/Sao_Paulo": "br",
"America/Argentina/Buenos_Aires": "ar",
"America/Santiago": "cl",
"America/Bogota": "co",
"America/Lima": "pe",
"Africa/Cairo": "eg",
"Africa/Lagos": "ng",
"Africa/Johannesburg": "za",
"Africa/Nairobi": "ke",
"Africa/Casablanca": "ma",
"Asia/Jerusalem": "il",
"Asia/Beirut": "lb",
"Asia/Baghdad": "iq",
"Asia/Tehran": "ir",
};

function getCountryFromTimezone(): string | undefined {
try {
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
return TIMEZONE_COUNTRY_MAP[tz];
} catch {
return undefined;
}
}

const useDefaultCountry = () => {
const defaultPhoneCountryFromStore = useBookerStore((state) => state.defaultPhoneCountry);
const [defaultCountry, setDefaultCountry] = useState<CountryCode>(defaultPhoneCountryFromStore || "us");
Expand All @@ -179,9 +261,9 @@ const useDefaultCountry = () => {
if (isSupportedCountry(data?.countryCode)) {
setDefaultCountry(data.countryCode.toLowerCase() as CountryCode);
} else {
const navCountry = navigator.language.split("-")[1]?.toUpperCase();
if (navCountry && isSupportedCountry(navCountry)) {
setDefaultCountry(navCountry.toLowerCase() as CountryCode);
const tzCountry = getCountryFromTimezone();
if (tzCountry && isSupportedCountry(tzCountry.toUpperCase())) {
setDefaultCountry(tzCountry as CountryCode);
} else {
setDefaultCountry("us");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ type CountryCodeOptions = {
export const countryCodeHandler = async ({ ctx }: CountryCodeOptions) => {
const { req } = ctx;

const countryCode: string | string[] = req?.headers?.["x-vercel-ip-country"] ?? "";
const countryCode: string | string[] =
req?.headers?.["cf-ipcountry"] || req?.headers?.["x-vercel-ip-country"] || "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Empty-string fallback makes the timezone detection unreachable in PhoneInput.tsx.

The countryCodeHandler returns "" when neither header is present. In PhoneInput.tsx (context snippet 1, line 257), the consumer guard is:

if (!data?.countryCode) {
  return;          // empty string is falsy → returns here
}
// …
} else {
  const tzCountry = getCountryFromTimezone();   // ← never reached

!"" is true, so the effect returns early and the timezone-detection branch (getCountryFromTimezone()) is never reached for self-hosted deployments without either header. That is precisely the use-case this PR is intended to fix.

geolocation/route.ts still returns "Unknown" as its no-header fallback; using the same sentinel here restores the intended flow (truthy → isSupportedCountry("Unknown") is falseelse branch executes):

🐛 Proposed fix
  const countryCode: string | string[] =
-   req?.headers?.["cf-ipcountry"] || req?.headers?.["x-vercel-ip-country"] || "";
+   req?.headers?.["cf-ipcountry"] || req?.headers?.["x-vercel-ip-country"] || "Unknown";

Note: Cloudflare uses "XX" for clients without country-code data and "T1" for Tor-network clients. Both are already handled correctly (truthy, isSupportedCountry returns false → triggers the timezone fallback), so no extra handling is needed for those values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/trpc/server/routers/publicViewer/countryCode.handler.ts` around
lines 10 - 11, The handler currently falls back to an empty string which is
falsy and prevents the timezone-detection branch in PhoneInput.tsx from running;
change the fallback in the countryCode extraction to the same sentinel used
elsewhere (e.g. "Unknown") so the value is truthy and allows
isSupportedCountry/getCountryFromTimezone to behave as intended—update the line
that defines countryCode in countryCode.handler (the const countryCode: string |
string[] = ...) to use "Unknown" instead of "" as the final fallback.

return { countryCode: Array.isArray(countryCode) ? countryCode[0] : countryCode };
};

Expand Down
Loading