@djangocfg/api 2.1.591 → 2.1.592

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@djangocfg/api",
3
- "version": "2.1.591",
3
+ "version": "2.1.592",
4
4
  "description": "Auto-generated TypeScript API client with React hooks, SWR integration, and Zod validation for Django REST Framework backends",
5
5
  "keywords": [
6
6
  "django",
@@ -91,7 +91,7 @@
91
91
  "zod": "^4.3.6"
92
92
  },
93
93
  "devDependencies": {
94
- "@djangocfg/typescript-config": "^2.1.591",
94
+ "@djangocfg/typescript-config": "^2.1.592",
95
95
  "@testing-library/react": "^16.3.2",
96
96
  "@types/node": "^25.9.5",
97
97
  "@types/react": "19.2.15",
@@ -123,6 +123,15 @@ function defaultBaseUrl(): string {
123
123
  return '';
124
124
  }
125
125
 
126
+ /** The browser's IANA zone; null where Intl cannot tell (e.g. a bare server runtime). */
127
+ function detectTimezone(): string | null {
128
+ try {
129
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || null;
130
+ } catch {
131
+ return null;
132
+ }
133
+ }
134
+
126
135
  /** Default API key fallback from `NEXT_PUBLIC_API_KEY`.
127
136
  *
128
137
  * Both browser and server: if NEXT_PUBLIC_API_KEY is set it is used as a
@@ -140,6 +149,7 @@ function defaultApiKey(): string | null {
140
149
 
141
150
  // ── In-memory overrides (win over storage / env) ───────────────────────────
142
151
  let _localeOverride: string | null = null;
152
+ let _timezoneOverride: string | null = null;
143
153
  let _apiKeyOverride: string | null = null;
144
154
  let _baseUrlOverride: string | null = null;
145
155
  let _withCredentials = true;
@@ -450,6 +460,11 @@ export const auth = {
450
460
  getLocale(): string | null { return _localeOverride ?? detectLocale(); },
451
461
  setLocale(locale: string | null): void { _localeOverride = locale; },
452
462
 
463
+ // ── Time zone ─────────────────────────────────────────────────────
464
+ // IANA name sent as `X-Timezone`. `null` override = follow the browser.
465
+ getTimezone(): string | null { return _timezoneOverride ?? detectTimezone(); },
466
+ setTimezone(timezone: string | null): void { _timezoneOverride = timezone; },
467
+
453
468
  // ── Base URL ──────────────────────────────────────────────────────
454
469
  getBaseUrl(): string {
455
470
  const url = (_baseUrlOverride ?? defaultBaseUrl());
@@ -759,10 +774,8 @@ export function installAuthOnClient(client: HeyClient): void {
759
774
  const apiKey = auth.getApiKey();
760
775
  if (apiKey && !request.headers.has('X-API-Key')) request.headers.set('X-API-Key', apiKey);
761
776
 
762
- try {
763
- const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
764
- if (tz) request.headers.set('X-Timezone', tz);
765
- } catch {}
777
+ const tz = auth.getTimezone();
778
+ if (tz) request.headers.set('X-Timezone', tz);
766
779
  request.headers.set('X-Client-Time', new Date().toISOString());
767
780
 
768
781
  // DPoP proof — sign a fresh per-request proof with the non-extractable key.
@@ -0,0 +1,18 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { auth } from '../../_api/generated/helpers/auth';
4
+
5
+ describe('auth time zone override', () => {
6
+ beforeEach(() => auth.setTimezone(null));
7
+
8
+ it('follows the browser until overridden', () => {
9
+ expect(auth.getTimezone()).toBe(Intl.DateTimeFormat().resolvedOptions().timeZone);
10
+ });
11
+
12
+ it('returns the override and clears back to the browser zone', () => {
13
+ auth.setTimezone('Asia/Seoul');
14
+ expect(auth.getTimezone()).toBe('Asia/Seoul');
15
+ auth.setTimezone(null);
16
+ expect(auth.getTimezone()).toBe(Intl.DateTimeFormat().resolvedOptions().timeZone);
17
+ });
18
+ });
@@ -3,6 +3,9 @@
3
3
  // Session (reactive snapshot from the generated auth store)
4
4
  export { useSession, type SessionSnapshot, type SessionStatus } from './useSession';
5
5
 
6
+ // Time zone preference (X-Timezone override)
7
+ export { useTimezone, type UseTimezoneReturn } from './useTimezone';
8
+
6
9
  // Router hooks
7
10
  export { useCfgRouter } from './useCfgRouter';
8
11
  export { useQueryParams } from './useQueryParams';
@@ -0,0 +1,67 @@
1
+ 'use client';
2
+
3
+ import { useCallback, useSyncExternalStore } from 'react';
4
+
5
+ import { auth } from '../../_api/generated/helpers/auth';
6
+
7
+ /**
8
+ * Time zone preference. The choice lives on the device (localStorage) and is
9
+ * pushed into the generated auth store, which sends it as `X-Timezone` on every
10
+ * request; Django's TimezoneMiddleware then activates it and mirrors it onto
11
+ * the profile. `null` = automatic (follow the browser).
12
+ */
13
+ const STORAGE_KEY = 'djangocfg.timezone';
14
+
15
+ const listeners = new Set<() => void>();
16
+
17
+ function read(): string | null {
18
+ try {
19
+ return localStorage.getItem(STORAGE_KEY) || null;
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ /** Apply the stored choice to the transport before any request goes out. */
26
+ function hydrate(): void {
27
+ if (typeof window === 'undefined') return;
28
+ auth.setTimezone(read());
29
+ }
30
+ hydrate();
31
+
32
+ function write(timezone: string | null): void {
33
+ try {
34
+ if (timezone) localStorage.setItem(STORAGE_KEY, timezone);
35
+ else localStorage.removeItem(STORAGE_KEY);
36
+ } catch {
37
+ /* storage unavailable — the choice then lasts for this page only */
38
+ }
39
+ auth.setTimezone(timezone);
40
+ listeners.forEach((l) => l());
41
+ }
42
+
43
+ function subscribe(listener: () => void): () => void {
44
+ listeners.add(listener);
45
+ return () => listeners.delete(listener);
46
+ }
47
+
48
+ export interface UseTimezoneReturn {
49
+ /** Zone in effect: the explicit choice, else the browser's. `null` only where Intl cannot tell. */
50
+ timezone: string | null;
51
+ /** The explicit choice; `null` = automatic. Feed this to `TimezoneSelect`. */
52
+ preference: string | null;
53
+ isAuto: boolean;
54
+ /** Pick a zone, or `null` for automatic. */
55
+ setTimezone: (timezone: string | null) => void;
56
+ }
57
+
58
+ export function useTimezone(): UseTimezoneReturn {
59
+ const preference = useSyncExternalStore(subscribe, read, () => null);
60
+ const setTimezone = useCallback((tz: string | null) => write(tz), []);
61
+ return {
62
+ timezone: preference ?? auth.getTimezone(),
63
+ preference,
64
+ isAuto: preference === null,
65
+ setTimezone,
66
+ };
67
+ }