@djangocfg/layouts 2.1.476 → 2.1.478

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/layouts",
3
- "version": "2.1.476",
3
+ "version": "2.1.478",
4
4
  "description": "Simple, straightforward layout components for Next.js - import and use with props",
5
5
  "keywords": [
6
6
  "layouts",
@@ -89,12 +89,12 @@
89
89
  "check": "tsc --noEmit"
90
90
  },
91
91
  "peerDependencies": {
92
- "@djangocfg/analytics": "^2.1.476",
93
- "@djangocfg/api": "^2.1.476",
94
- "@djangocfg/centrifugo": "^2.1.476",
95
- "@djangocfg/devtools": "^2.1.476",
96
- "@djangocfg/i18n": "^2.1.476",
97
- "@djangocfg/ui-core": "^2.1.476",
92
+ "@djangocfg/analytics": "^2.1.478",
93
+ "@djangocfg/api": "^2.1.478",
94
+ "@djangocfg/centrifugo": "^2.1.478",
95
+ "@djangocfg/devtools": "^2.1.478",
96
+ "@djangocfg/i18n": "^2.1.478",
97
+ "@djangocfg/ui-core": "^2.1.478",
98
98
  "@hookform/resolvers": "^5.2.2",
99
99
  "consola": "^3.4.2",
100
100
  "lucide-react": "^0.545.0",
@@ -124,14 +124,14 @@
124
124
  "uuid": "^11.1.1"
125
125
  },
126
126
  "devDependencies": {
127
- "@djangocfg/analytics": "^2.1.476",
128
- "@djangocfg/api": "^2.1.476",
129
- "@djangocfg/centrifugo": "^2.1.476",
130
- "@djangocfg/devtools": "^2.1.476",
131
- "@djangocfg/i18n": "^2.1.476",
132
- "@djangocfg/typescript-config": "^2.1.476",
133
- "@djangocfg/ui-core": "^2.1.476",
134
- "@djangocfg/ui-tools": "^2.1.476",
127
+ "@djangocfg/analytics": "^2.1.478",
128
+ "@djangocfg/api": "^2.1.478",
129
+ "@djangocfg/centrifugo": "^2.1.478",
130
+ "@djangocfg/devtools": "^2.1.478",
131
+ "@djangocfg/i18n": "^2.1.478",
132
+ "@djangocfg/typescript-config": "^2.1.478",
133
+ "@djangocfg/ui-core": "^2.1.478",
134
+ "@djangocfg/ui-tools": "^2.1.478",
135
135
  "@types/node": "^25.9.5",
136
136
  "@types/react": "19.2.15",
137
137
  "@types/react-dom": "19.2.3",
@@ -1,11 +1,11 @@
1
1
  'use client';
2
2
 
3
3
  import { Github } from 'lucide-react';
4
- import React, { memo, useMemo } from 'react';
4
+ import React, { memo, useEffect, useMemo, useRef } from 'react';
5
5
 
6
6
  import { useGithubAuth } from '@djangocfg/api/auth';
7
7
  import { useAppT } from '@djangocfg/i18n';
8
- import { Input } from '@djangocfg/ui-core/components';
8
+ import { Checkbox, Input } from '@djangocfg/ui-core/components';
9
9
 
10
10
  import { useAuthLayoutContext } from '../../AuthLayout';
11
11
  import { useAuthFormContext } from '../../context';
@@ -47,11 +47,40 @@ function IdentifierStepRaw() {
47
47
  privacyUrl,
48
48
  enableGithubAuth,
49
49
  sourceUrl,
50
+ marketingConsent,
51
+ marketingConsentConfig,
50
52
  setIdentifier,
53
+ setMarketingConsent,
51
54
  setError,
52
55
  handleIdentifierSubmit,
53
56
  } = useAuthFormContext();
54
57
 
58
+ // Derived consent values — computed before the JSX return.
59
+ const consentEnabled = Boolean(marketingConsentConfig?.enabled);
60
+ const consentChecked = marketingConsent === true;
61
+ const consentDefaultChecked = marketingConsentConfig?.defaultChecked;
62
+
63
+ // Once the user toggles the checkbox, no default may overwrite their choice.
64
+ const consentTouchedRef = useRef(false);
65
+
66
+ // Default checked-state resolution: explicit `defaultChecked` (typically the
67
+ // server-decided consent policy) wins; the timezone heuristic is only the
68
+ // fallback while it is undefined (policy loading/unavailable). The policy
69
+ // usually arrives async after mount, so a late explicit default still
70
+ // replaces the fallback — but never a user's own choice.
71
+ useEffect(() => {
72
+ if (!consentEnabled || consentTouchedRef.current) return;
73
+ if (consentDefaultChecked !== undefined) {
74
+ if (marketingConsent !== consentDefaultChecked) setMarketingConsent(consentDefaultChecked);
75
+ return;
76
+ }
77
+ if (marketingConsent !== null) return;
78
+ // Jurisdiction heuristic v1: European timezones default to UNCHECKED
79
+ // (opt-in regimes), everywhere else defaults to checked.
80
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? '';
81
+ setMarketingConsent(!timeZone.startsWith('Europe/'));
82
+ }, [consentEnabled, consentDefaultChecked, marketingConsent, setMarketingConsent]);
83
+
55
84
  // Translations
56
85
  const t = useAppT();
57
86
  const content = useMemo(() => ({
@@ -95,6 +124,24 @@ function IdentifierStepRaw() {
95
124
  className="auth-input"
96
125
  />
97
126
 
127
+ {consentEnabled && (
128
+ <label className="auth-marketing-consent">
129
+ <Checkbox
130
+ checked={consentChecked}
131
+ onCheckedChange={(checked) => {
132
+ consentTouchedRef.current = true;
133
+ setMarketingConsent(checked === true);
134
+ }}
135
+ disabled={isLoading}
136
+ className="auth-marketing-consent-checkbox"
137
+ />
138
+ {/* Label copy comes from the consuming app — no product wording here. */}
139
+ <span className="auth-marketing-consent-label">
140
+ {marketingConsentConfig?.label}
141
+ </span>
142
+ </label>
143
+ )}
144
+
98
145
  <AuthError message={error} />
99
146
 
100
147
  <AuthButton
@@ -27,6 +27,7 @@ export const AuthFormProvider: React.FC<AuthLayoutProps> = ({
27
27
  enableGithubAuth = false,
28
28
  logo,
29
29
  redirectUrl,
30
+ marketingConsent,
30
31
  onIdentifierSuccess,
31
32
  onOTPSuccess,
32
33
  onError,
@@ -44,6 +45,10 @@ export const AuthFormProvider: React.FC<AuthLayoutProps> = ({
44
45
  sourceUrl,
45
46
  redirectUrl,
46
47
  requireTermsAcceptance,
48
+ // Presence of this option is what makes the OTP request carry consent fields.
49
+ marketingConsent: marketingConsent?.enabled
50
+ ? { disclosureVersion: marketingConsent.disclosureVersion }
51
+ : undefined,
47
52
  });
48
53
 
49
54
  const value: AuthFormContextType = useMemo(
@@ -57,6 +62,8 @@ export const AuthFormProvider: React.FC<AuthLayoutProps> = ({
57
62
  enableGithubAuth,
58
63
  logo,
59
64
  redirectUrl,
65
+ // Renamed: `marketingConsent` (the name) is the checkbox STATE from the form hook.
66
+ marketingConsentConfig: marketingConsent,
60
67
  }),
61
68
  // eslint-disable-next-line react-hooks/exhaustive-deps
62
69
  [
@@ -68,6 +75,7 @@ export const AuthFormProvider: React.FC<AuthLayoutProps> = ({
68
75
  enableGithubAuth,
69
76
  logo,
70
77
  redirectUrl,
78
+ marketingConsent,
71
79
  ]
72
80
  );
73
81
 
@@ -404,6 +404,30 @@
404
404
  color: var(--foreground);
405
405
  }
406
406
 
407
+ /* ===== MARKETING CONSENT (opt-in checkbox on the identifier step) ===== */
408
+
409
+ .auth-marketing-consent {
410
+ display: flex;
411
+ align-items: flex-start;
412
+ gap: 0.625rem;
413
+ text-align: left;
414
+ font-size: 0.75rem;
415
+ line-height: 1.5;
416
+ color: var(--muted-foreground);
417
+ cursor: pointer;
418
+ user-select: none;
419
+ }
420
+
421
+ .auth-marketing-consent-checkbox {
422
+ flex-shrink: 0;
423
+ /* Optically align the box with the first line of the label copy. */
424
+ margin-top: 0.0625rem;
425
+ }
426
+
427
+ .auth-marketing-consent-label {
428
+ cursor: pointer;
429
+ }
430
+
407
431
  /* ===== REMEMBER ME ===== */
408
432
 
409
433
  .auth-remember {
@@ -29,6 +29,27 @@ export type { AuthBackgroundConfig, AuthShellVariant, AuthShellContextValue } fr
29
29
  // Layout Config — UI-specific configuration for AuthLayout
30
30
  // ─────────────────────────────────────────────────────────────────────────────
31
31
 
32
+ /**
33
+ * Marketing-consent checkbox on the identifier step.
34
+ *
35
+ * The label copy comes from the consuming app (its own i18n) — the shared
36
+ * packages ship no product-specific wording.
37
+ */
38
+ export interface AuthMarketingConsentConfig {
39
+ /** Render the checkbox and send consent fields with the OTP request. */
40
+ enabled: boolean;
41
+ /** Version tag of the disclosure copy, sent as `consent_disclosure_version`. */
42
+ disclosureVersion: string;
43
+ /** Checkbox label — arrives from the app, not from @djangocfg/i18n. */
44
+ label: React.ReactNode;
45
+ /**
46
+ * Explicit default checked-state (e.g. server-decided from the consent
47
+ * policy endpoint). Wins over the built-in timezone heuristic, which
48
+ * remains the fallback while this is undefined (policy loading/failed).
49
+ */
50
+ defaultChecked?: boolean;
51
+ }
52
+
32
53
  export interface AuthLayoutConfig {
33
54
  /** Support page URL */
34
55
  supportUrl?: string;
@@ -45,13 +66,20 @@ export interface AuthLayoutConfig {
45
66
  logo?: React.ReactNode;
46
67
  /** URL to redirect after successful auth (default: /dashboard) */
47
68
  redirectUrl?: string;
69
+ /** Optional marketing-consent checkbox on the identifier step. Absent ⇒ no checkbox, OTP body unchanged. */
70
+ marketingConsent?: AuthMarketingConsentConfig;
48
71
  }
49
72
 
50
73
  // ─────────────────────────────────────────────────────────────────────────────
51
74
  // Context — form state + layout config merged for step components
52
75
  // ─────────────────────────────────────────────────────────────────────────────
53
76
 
54
- export interface AuthFormContextType extends AuthFormReturn, AuthLayoutConfig {}
77
+ // `marketingConsent` is omitted from the config side of the merge: the name is
78
+ // taken by the checkbox *state* (boolean | null) from AuthFormReturn, so the
79
+ // prop object is re-exposed as `marketingConsentConfig`.
80
+ export interface AuthFormContextType extends AuthFormReturn, Omit<AuthLayoutConfig, 'marketingConsent'> {
81
+ marketingConsentConfig?: AuthMarketingConsentConfig;
82
+ }
55
83
 
56
84
  // ─────────────────────────────────────────────────────────────────────────────
57
85
  // Component Props
@@ -57,6 +57,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
57
57
  enableGithubAuth = false,
58
58
  logo,
59
59
  redirectUrl = '/dashboard',
60
+ marketingConsent: marketingConsentConfig,
60
61
 
61
62
  // Initial state
62
63
  step: initialStep = 'identifier',
@@ -80,6 +81,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
80
81
  const [otp, setOtp] = useState(initialOtp);
81
82
  const [isLoading, setIsLoading] = useState(initialLoading);
82
83
  const [acceptedTerms, setAcceptedTerms] = useState(initialAcceptedTerms);
84
+ const [marketingConsent, setMarketingConsent] = useState<boolean | null>(null);
83
85
  const [error, setError] = useState(initialError);
84
86
  const [twoFactorSessionId, setTwoFactorSessionId] = useState<string | null>(initialTwoFactorSessionId);
85
87
  const [shouldPrompt2FA, setShouldPrompt2FA] = useState(initialShouldPrompt2FA);
@@ -101,6 +103,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
101
103
  otp,
102
104
  isLoading,
103
105
  acceptedTerms,
106
+ marketingConsent,
104
107
  step,
105
108
  error,
106
109
  twoFactorSessionId,
@@ -116,6 +119,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
116
119
  setIdentifier,
117
120
  setOtp,
118
121
  setAcceptedTerms,
122
+ setMarketingConsent,
119
123
  setError,
120
124
  clearError: () => setError(''),
121
125
  setStep,
@@ -159,12 +163,14 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
159
163
  enableGithubAuth,
160
164
  logo,
161
165
  redirectUrl,
166
+ marketingConsentConfig,
162
167
  };
163
168
  }, [
164
169
  identifier,
165
170
  otp,
166
171
  isLoading,
167
172
  acceptedTerms,
173
+ marketingConsent,
168
174
  step,
169
175
  error,
170
176
  twoFactorSessionId,
@@ -183,6 +189,7 @@ export const MockAuthFormProvider: React.FC<MockAuthFormProviderProps> = ({
183
189
  enableGithubAuth,
184
190
  logo,
185
191
  redirectUrl,
192
+ marketingConsentConfig,
186
193
  ]);
187
194
 
188
195
  return <AuthFormContext.Provider value={value}>{children}</AuthFormContext.Provider>;