@djangocfg/layouts 2.1.579 → 2.1.581

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.
Files changed (30) hide show
  1. package/package.json +12 -12
  2. package/src/layouts/AuthLayout/AuthLayout.tsx +25 -7
  3. package/src/layouts/AuthLayout/README.md +16 -12
  4. package/src/layouts/AuthLayout/components/index.ts +0 -2
  5. package/src/layouts/AuthLayout/components/oauth/OAuthCallback.tsx +8 -9
  6. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +1 -1
  7. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +1 -1
  8. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +10 -2
  9. package/src/layouts/AuthLayout/components/steps/index.ts +0 -3
  10. package/src/layouts/AuthLayout/constants.ts +0 -4
  11. package/src/layouts/AuthLayout/index.ts +0 -3
  12. package/src/layouts/AuthLayout/styles/auth.css +0 -110
  13. package/src/layouts/AuthLayout/types.ts +5 -2
  14. package/src/layouts/SettingsLayout/components/TwoFactorSection/TwoFactorSection.tsx +2 -2
  15. package/src/layouts/_components/locale-switcher/LocaleSwitcherDropdown.tsx +2 -1
  16. package/src/layouts/_components/locale-switcher/LocaleSwitcherTrigger.tsx +4 -3
  17. package/src/layouts/_components/locale-switcher/iconFlag.ts +5 -0
  18. package/src/snippets/TwoFactor/TwoFactorChallengeDialog.tsx +188 -0
  19. package/src/snippets/TwoFactor/TwoFactorSetup.tsx +209 -0
  20. package/src/snippets/TwoFactor/index.ts +2 -0
  21. package/src/snippets/index.ts +1 -0
  22. package/src/testing/MockAuthFormProvider.tsx +7 -41
  23. package/src/testing/index.ts +4 -2
  24. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +0 -86
  25. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +0 -36
  26. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +0 -141
  27. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +0 -145
  28. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +0 -133
  29. package/src/layouts/AuthLayout/hooks/index.ts +0 -1
  30. package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +0 -51
@@ -1,141 +0,0 @@
1
- 'use client';
2
-
3
- import { Check, Copy } from 'lucide-react';
4
- import React, { memo, useMemo, useState } from 'react';
5
- import { QRCodeSVG } from 'qrcode.react';
6
-
7
- import { useAppT } from '@djangocfg/i18n';
8
- import {
9
- Collapsible,
10
- CollapsibleContent,
11
- CollapsibleTrigger,
12
- } from '@djangocfg/ui-core/components';
13
-
14
- import { AUTH } from '../../../constants';
15
- import { useCopyToClipboard } from '../../../hooks';
16
- import {
17
- AuthButton,
18
- AuthContainer,
19
- AuthError,
20
- AuthHeader,
21
- AuthLink,
22
- AuthOTPInput,
23
- } from '../../shared';
24
-
25
- interface SetupQRCodeProps {
26
- provisioningUri: string;
27
- secret: string;
28
- isLoading: boolean;
29
- error?: string | null;
30
- onConfirm: (code: string) => Promise<unknown>;
31
- onSkip?: () => void;
32
- }
33
-
34
- /**
35
- * SetupQRCode - QR code scanning step for 2FA setup.
36
- *
37
- * Memoised: re-renders only when `provisioningUri`, `secret`, `isLoading`,
38
- * `error`, `onConfirm` or `onSkip` change. `onConfirm` and `onSkip` are
39
- * compared by reference — callers should stabilise them with useCallback.
40
- */
41
- function SetupQRCodeRaw({
42
- provisioningUri,
43
- secret,
44
- isLoading,
45
- error,
46
- onConfirm,
47
- onSkip,
48
- }: SetupQRCodeProps) {
49
- const [confirmCode, setConfirmCode] = useState('');
50
- const { copied, copy } = useCopyToClipboard();
51
- const t = useAppT();
52
- const content = useMemo(() => ({
53
- title: t('layouts.auth.setup.qrCode.title'),
54
- subtitle: t('layouts.auth.setup.qrCode.subtitle'),
55
- manualEntry: t('layouts.auth.setup.qrCode.manualEntry'),
56
- confirmPrompt: t('layouts.auth.setup.qrCode.confirmPrompt'),
57
- button: t('layouts.auth.setup.qrCode.button'),
58
- skip: t('layouts.auth.setup.qrCode.skip'),
59
- copied: t('layouts.auth.setup.complete.copied'),
60
- copy: 'Copy',
61
- }), [t]);
62
-
63
- const handleSubmit = async (e: React.FormEvent) => {
64
- e.preventDefault();
65
- await onConfirm(confirmCode);
66
- };
67
-
68
- const handleCopySecret = () => {
69
- copy(secret);
70
- };
71
-
72
- return (
73
- <AuthContainer step="2fa-setup">
74
- <AuthHeader title={content.title} subtitle={content.subtitle} />
75
-
76
- <form onSubmit={handleSubmit} className="auth-form-group">
77
- <div className="auth-qr-container">
78
- <div className="auth-qr">
79
- <QRCodeSVG
80
- value={provisioningUri}
81
- size={AUTH.QR_CODE_SIZE}
82
- level="M"
83
- marginSize={0}
84
- />
85
- </div>
86
- </div>
87
-
88
- <Collapsible className="auth-collapsible-centered">
89
- <CollapsibleTrigger asChild>
90
- <AuthLink>{content.manualEntry}</AuthLink>
91
- </CollapsibleTrigger>
92
- <CollapsibleContent>
93
- <div className="auth-secret-container">
94
- <code className="auth-secret">{secret}</code>
95
- <AuthLink onClick={handleCopySecret}>
96
- {copied ? (
97
- <>
98
- <Check className="w-4 h-4" />
99
- {content.copied}
100
- </>
101
- ) : (
102
- <>
103
- <Copy className="w-4 h-4" />
104
- {content.copy}
105
- </>
106
- )}
107
- </AuthLink>
108
- </div>
109
- </CollapsibleContent>
110
- </Collapsible>
111
-
112
- <p className="auth-instruction">{content.confirmPrompt}</p>
113
-
114
- <AuthOTPInput
115
- value={confirmCode}
116
- onChange={setConfirmCode}
117
- disabled={isLoading}
118
- />
119
-
120
- <AuthError message={error} />
121
-
122
- <AuthButton
123
- loading={isLoading}
124
- disabled={confirmCode.length !== AUTH.TOTP_LENGTH}
125
- >
126
- {content.button}
127
- </AuthButton>
128
-
129
- {onSkip && (
130
- <div className="auth-actions">
131
- <AuthLink onClick={onSkip} disabled={isLoading}>
132
- {content.skip}
133
- </AuthLink>
134
- </div>
135
- )}
136
- </form>
137
- </AuthContainer>
138
- );
139
- }
140
-
141
- export const SetupQRCode = memo(SetupQRCodeRaw);
@@ -1,145 +0,0 @@
1
- 'use client';
2
-
3
- import React, { memo } from 'react';
4
-
5
- import { useTwoFactorSetup } from '@djangocfg/api/auth';
6
-
7
- import { useAuthFormContext } from '../../../context';
8
- import { SetupComplete } from './SetupComplete';
9
- import { SetupLoading } from './SetupLoading';
10
- import { SetupQRCode } from './SetupQRCode';
11
-
12
- export interface SetupStepProps {
13
- onComplete?: () => void;
14
- onSkip?: () => void;
15
- }
16
-
17
- /**
18
- * SetupStep - Orchestrator for 2FA setup flow (requires AuthFormContext).
19
- * For use outside AuthLayout use SetupStepStandalone.
20
- *
21
- * Memoised: re-renders only when `onComplete` or `onSkip` references
22
- * change. Callers should stabilise these callbacks with useCallback.
23
- */
24
- function SetupStepRaw({ onComplete, onSkip }: SetupStepProps) {
25
- const { setStep } = useAuthFormContext();
26
-
27
- const handleComplete = () => {
28
- onComplete?.();
29
- setStep('success');
30
- };
31
-
32
- const handleSkip = () => {
33
- onSkip?.();
34
- setStep('success');
35
- };
36
-
37
- const {
38
- isLoading,
39
- error,
40
- setupData,
41
- backupCodes,
42
- backupCodesWarning,
43
- setupStep,
44
- startSetup,
45
- confirmSetup,
46
- } = useTwoFactorSetup({
47
- onComplete: handleComplete,
48
- onError: () => {},
49
- });
50
-
51
- // Start setup on mount
52
- React.useEffect(() => {
53
- if (setupStep === 'idle') {
54
- startSetup();
55
- }
56
- }, [setupStep, startSetup]);
57
-
58
- // Loading state
59
- if (isLoading && !setupData) {
60
- return <SetupLoading />;
61
- }
62
-
63
- // Complete - show backup codes
64
- if (setupStep === 'complete' && backupCodes) {
65
- return (
66
- <SetupComplete
67
- backupCodes={backupCodes}
68
- backupCodesWarning={backupCodesWarning}
69
- onDone={handleComplete}
70
- />
71
- );
72
- }
73
-
74
- // Setup - show QR code
75
- if (setupData) {
76
- return (
77
- <SetupQRCode
78
- provisioningUri={setupData.provisioningUri}
79
- secret={setupData.secret}
80
- isLoading={isLoading}
81
- error={error}
82
- onConfirm={confirmSetup}
83
- onSkip={onSkip ? handleSkip : undefined}
84
- />
85
- );
86
- }
87
-
88
- // Fallback to loading
89
- return <SetupLoading />;
90
- }
91
-
92
- export const SetupStep = memo(SetupStepRaw);
93
-
94
- /**
95
- * SetupStepStandalone — same flow as SetupStep but without AuthFormContext.
96
- * Use this inside ProfileLayout or any page outside AuthLayout.
97
- *
98
- * Memoised: re-renders only when `onComplete` or `onSkip` references
99
- * change. Callers should stabilise these callbacks with useCallback.
100
- */
101
- function SetupStepStandaloneRaw({ onComplete, onSkip }: SetupStepProps) {
102
- const {
103
- isLoading,
104
- error,
105
- setupData,
106
- backupCodes,
107
- backupCodesWarning,
108
- setupStep,
109
- startSetup,
110
- confirmSetup,
111
- } = useTwoFactorSetup({ onComplete, onError: () => {} });
112
-
113
- React.useEffect(() => {
114
- if (setupStep === 'idle') startSetup();
115
- }, [setupStep, startSetup]);
116
-
117
- if (isLoading && !setupData) return <SetupLoading />;
118
-
119
- if (setupStep === 'complete' && backupCodes) {
120
- return (
121
- <SetupComplete
122
- backupCodes={backupCodes}
123
- backupCodesWarning={backupCodesWarning}
124
- onDone={() => onComplete?.()}
125
- />
126
- );
127
- }
128
-
129
- if (setupData) {
130
- return (
131
- <SetupQRCode
132
- provisioningUri={setupData.provisioningUri}
133
- secret={setupData.secret}
134
- isLoading={isLoading}
135
- error={error}
136
- onConfirm={confirmSetup}
137
- onSkip={onSkip}
138
- />
139
- );
140
- }
141
-
142
- return <SetupLoading />;
143
- }
144
-
145
- export const SetupStepStandalone = memo(SetupStepStandaloneRaw);
@@ -1,133 +0,0 @@
1
- 'use client';
2
-
3
- import React, { memo, useMemo } from 'react';
4
-
5
- import { useAppT } from '@djangocfg/i18n';
6
-
7
- import { AUTH } from '../../constants';
8
- import { useAuthFormContext } from '../../context';
9
- import {
10
- AuthButton,
11
- AuthContainer,
12
- AuthError,
13
- AuthHeader,
14
- AuthLink,
15
- AuthOTPInput,
16
- } from '../shared';
17
-
18
- /**
19
- * TwoFactorStep - Apple-style 2FA verification.
20
- *
21
- * Minimal design with:
22
- * - TOTP code input (default)
23
- * - Backup code input (alternate)
24
- * - Clean toggle between modes
25
- *
26
- * Memoised: no props; reads from context. Prevents re-renders when
27
- * AuthContent switches steps or sibling branches update.
28
- */
29
- function TwoFactorStepRaw() {
30
- const {
31
- twoFactorCode,
32
- useBackupCode,
33
- error,
34
- is2FALoading,
35
- twoFactorWarning,
36
- twoFactorAttemptsRemaining,
37
- setTwoFactorCode,
38
- handle2FASubmit,
39
- handleUseBackupCode,
40
- handleUseTOTP,
41
- handleBackToIdentifier,
42
- } = useAuthFormContext();
43
-
44
- // Translations
45
- const t = useAppT();
46
- const content = useMemo(() => ({
47
- title: t('layouts.auth.twoFactor.title'),
48
- subtitle: {
49
- totp: t('layouts.auth.twoFactor.subtitle.totp'),
50
- backup: t('layouts.auth.twoFactor.subtitle.backup'),
51
- },
52
- placeholder: {
53
- backup: t('layouts.auth.twoFactor.placeholder.backup'),
54
- },
55
- button: t('layouts.auth.twoFactor.button'),
56
- toggle: {
57
- toBackup: t('layouts.auth.twoFactor.toggle.toBackup'),
58
- toTotp: t('layouts.auth.twoFactor.toggle.toTotp'),
59
- },
60
- changeEmail: t('layouts.auth.otp.changeEmail'),
61
- }), [t]);
62
-
63
- const subtitle = useBackupCode ? content.subtitle.backup : content.subtitle.totp;
64
-
65
- return (
66
- <AuthContainer step="2fa">
67
- <div className="auth-2fa-box">
68
- <div className="auth-2fa-shield" aria-hidden="true">
69
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.75" strokeLinecap="round" strokeLinejoin="round">
70
- <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" />
71
- </svg>
72
- </div>
73
-
74
- <AuthHeader title={content.title} subtitle={subtitle} />
75
-
76
- <form onSubmit={handle2FASubmit} className="auth-form-group">
77
- {useBackupCode ? (
78
- <input
79
- type="text"
80
- value={twoFactorCode}
81
- onChange={(e) => setTwoFactorCode(e.target.value.toUpperCase())}
82
- placeholder={content.placeholder.backup}
83
- disabled={is2FALoading}
84
- maxLength={AUTH.BACKUP_CODE_MAX_LENGTH}
85
- autoComplete="off"
86
- autoFocus
87
- className="auth-input"
88
- />
89
- ) : (
90
- <AuthOTPInput
91
- value={twoFactorCode}
92
- onChange={setTwoFactorCode}
93
- disabled={is2FALoading}
94
- />
95
- )}
96
-
97
- {twoFactorWarning && (
98
- <div className="auth-dev-notice">{twoFactorWarning}</div>
99
- )}
100
-
101
- <AuthError message={error} />
102
-
103
- {twoFactorAttemptsRemaining !== null && twoFactorAttemptsRemaining <= 3 && (
104
- <div className="auth-dev-notice">
105
- {twoFactorAttemptsRemaining} attempt{twoFactorAttemptsRemaining !== 1 ? 's' : ''} remaining
106
- </div>
107
- )}
108
-
109
- <AuthButton
110
- loading={is2FALoading}
111
- disabled={!useBackupCode && twoFactorCode.length !== AUTH.TOTP_LENGTH}
112
- >
113
- {content.button}
114
- </AuthButton>
115
-
116
- <div className="auth-actions">
117
- <AuthLink
118
- onClick={useBackupCode ? handleUseTOTP : handleUseBackupCode}
119
- disabled={is2FALoading}
120
- >
121
- {useBackupCode ? content.toggle.toTotp : content.toggle.toBackup}
122
- </AuthLink>
123
- <AuthLink onClick={handleBackToIdentifier} disabled={is2FALoading}>
124
- {content.changeEmail}
125
- </AuthLink>
126
- </div>
127
- </form>
128
- </div>
129
- </AuthContainer>
130
- );
131
- }
132
-
133
- export const TwoFactorStep = memo(TwoFactorStepRaw);
@@ -1 +0,0 @@
1
- export { useCopyToClipboard } from './useCopyToClipboard';
@@ -1,51 +0,0 @@
1
- 'use client';
2
-
3
- import { useCallback, useEffect, useRef, useState } from 'react';
4
-
5
- import { AUTH } from '../constants';
6
-
7
- interface UseCopyToClipboardResult {
8
- copied: boolean;
9
- copy: (text: string) => Promise<void>;
10
- }
11
-
12
- /**
13
- * useCopyToClipboard - Copy text with temporary feedback state
14
- *
15
- * @param duration - How long to show "copied" state (default: 2000ms)
16
- */
17
- export const useCopyToClipboard = (
18
- duration = AUTH.COPY_FEEDBACK_DURATION
19
- ): UseCopyToClipboardResult => {
20
- const [copied, setCopied] = useState(false);
21
- // One timer, cleared on unmount. Copying twice in quick succession stacked
22
- // two, so the first cleared the flag while the second copy was still fresh.
23
- const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
24
-
25
- useEffect(
26
- () => () => {
27
- if (timerRef.current) clearTimeout(timerRef.current);
28
- },
29
- []
30
- );
31
-
32
- const copy = useCallback(
33
- async (text: string) => {
34
- try {
35
- await navigator.clipboard.writeText(text);
36
- setCopied(true);
37
- if (timerRef.current) clearTimeout(timerRef.current);
38
- timerRef.current = setTimeout(() => {
39
- timerRef.current = null;
40
- setCopied(false);
41
- }, duration);
42
- } catch {
43
- // Clipboard API not available
44
- console.warn('Clipboard API not available');
45
- }
46
- },
47
- [duration]
48
- );
49
-
50
- return { copied, copy };
51
- };