@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 +15 -15
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +49 -2
- package/src/layouts/AuthLayout/context.tsx +8 -0
- package/src/layouts/AuthLayout/styles/auth.css +24 -0
- package/src/layouts/AuthLayout/types.ts +29 -1
- package/src/testing/MockAuthFormProvider.tsx +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
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.
|
|
93
|
-
"@djangocfg/api": "^2.1.
|
|
94
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
95
|
-
"@djangocfg/devtools": "^2.1.
|
|
96
|
-
"@djangocfg/i18n": "^2.1.
|
|
97
|
-
"@djangocfg/ui-core": "^2.1.
|
|
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.
|
|
128
|
-
"@djangocfg/api": "^2.1.
|
|
129
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
130
|
-
"@djangocfg/devtools": "^2.1.
|
|
131
|
-
"@djangocfg/i18n": "^2.1.
|
|
132
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
133
|
-
"@djangocfg/ui-core": "^2.1.
|
|
134
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
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
|
-
|
|
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>;
|