feat(07.1-04): add useUserTimezone client hook
- New lib/hooks/use-user-timezone.ts exporting useUserTimezone() and formatInUserTimezone()
- Reads user.timezone from Better Auth useSession() additionalField (Plan 01)
- Validates against Intl.supportedValuesOf('timeZone') with safe fallback to NEXT_PUBLIC_DEFAULT_TIMEZONE || 'UTC'
- Pure formatInUserTimezone helper safe to call inside loops (not a hook)
- Resolves TZ-04
This commit is contained in:
parent
3a3564fc91
commit
2ac2db7a23
1 changed files with 61 additions and 0 deletions
61
lib/hooks/use-user-timezone.ts
Normal file
61
lib/hooks/use-user-timezone.ts
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useSession } from "@/lib/auth-client";
|
||||||
|
|
||||||
|
// Public-readable default (Next.js exposes NEXT_PUBLIC_* to the browser).
|
||||||
|
// Operators can set this in .env.local to match the server-side
|
||||||
|
// DEFAULT_TIMEZONE. If unset, both server and client default to 'UTC'.
|
||||||
|
function getClientDefaultTimezone(): string {
|
||||||
|
return process.env.NEXT_PUBLIC_DEFAULT_TIMEZONE || "UTC";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidIanaTimezone(tz: unknown): tz is string {
|
||||||
|
if (typeof tz !== "string" || tz.length === 0 || tz.length > 64) return false;
|
||||||
|
try {
|
||||||
|
return Intl.supportedValuesOf("timeZone").includes(tz);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useUserTimezone — TZ-04.
|
||||||
|
*
|
||||||
|
* Returns the calling user's IANA timezone string, sourced from the
|
||||||
|
* Better Auth additionalField on `useSession()`. Falls back to
|
||||||
|
* `process.env.NEXT_PUBLIC_DEFAULT_TIMEZONE || 'UTC'` while the session
|
||||||
|
* is loading, when the field is missing, or when the stored value is
|
||||||
|
* not a recognized IANA zone.
|
||||||
|
*
|
||||||
|
* This is the ONLY supported way to read the user's tz on the client.
|
||||||
|
* Do NOT call `Intl.DateTimeFormat()` with a hardcoded zone or rely on
|
||||||
|
* the browser's local zone — the user may have travelled or set a
|
||||||
|
* preference that differs from the device.
|
||||||
|
*/
|
||||||
|
export function useUserTimezone(): string {
|
||||||
|
const { data } = useSession();
|
||||||
|
// Better Auth additionalField is typed `string` post-Plan-01; defence
|
||||||
|
// in depth: validate before returning.
|
||||||
|
const raw = (data?.user as { timezone?: unknown } | undefined)?.timezone;
|
||||||
|
if (isValidIanaTimezone(raw)) return raw;
|
||||||
|
return getClientDefaultTimezone();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* formatInUserTimezone — convenience wrapper for the common case of
|
||||||
|
* "format an ISO string in the user's tz". Equivalent to
|
||||||
|
* `new Date(iso).toLocaleString(locale, { ...options, timeZone: tz })`.
|
||||||
|
*
|
||||||
|
* Pass `tz` from `useUserTimezone()` and the same options object you'd
|
||||||
|
* pass to toLocaleString / toLocaleDateString — this helper keeps
|
||||||
|
* existing format strings working without rewrites.
|
||||||
|
*/
|
||||||
|
export function formatInUserTimezone(
|
||||||
|
input: string | number | Date,
|
||||||
|
tz: string,
|
||||||
|
options?: Intl.DateTimeFormatOptions,
|
||||||
|
locale: string = "en-US",
|
||||||
|
): string {
|
||||||
|
const date = input instanceof Date ? input : new Date(input);
|
||||||
|
return date.toLocaleString(locale, { ...options, timeZone: tz });
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue