@djangocfg/layouts 2.1.113 → 2.1.115
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 +14 -14
- package/src/layouts/AppLayout/index.ts +1 -1
- package/src/layouts/AuthLayout/AuthLayout.tsx +5 -2
- package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +9 -3
- package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +13 -4
- package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +11 -3
- package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +14 -5
- package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +20 -3
- package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +19 -6
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +12 -3
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +8 -3
- package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +15 -5
- package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +20 -3
- package/src/layouts/AuthLayout/content.ts +0 -78
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@djangocfg/layouts",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.115",
|
|
4
4
|
"description": "Simple, straightforward layout components for Next.js - import and use with props",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"layouts",
|
|
@@ -74,12 +74,12 @@
|
|
|
74
74
|
"check": "tsc --noEmit"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@djangocfg/api": "^2.1.
|
|
78
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
79
|
-
"@djangocfg/i18n": "^2.1.
|
|
80
|
-
"@djangocfg/ui-core": "^2.1.
|
|
81
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
82
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
77
|
+
"@djangocfg/api": "^2.1.115",
|
|
78
|
+
"@djangocfg/centrifugo": "^2.1.115",
|
|
79
|
+
"@djangocfg/i18n": "^2.1.115",
|
|
80
|
+
"@djangocfg/ui-core": "^2.1.115",
|
|
81
|
+
"@djangocfg/ui-nextjs": "^2.1.115",
|
|
82
|
+
"@djangocfg/ui-tools": "^2.1.115",
|
|
83
83
|
"@hookform/resolvers": "^5.2.2",
|
|
84
84
|
"consola": "^3.4.2",
|
|
85
85
|
"lucide-react": "^0.545.0",
|
|
@@ -102,13 +102,13 @@
|
|
|
102
102
|
"uuid": "^11.1.0"
|
|
103
103
|
},
|
|
104
104
|
"devDependencies": {
|
|
105
|
-
"@djangocfg/api": "^2.1.
|
|
106
|
-
"@djangocfg/i18n": "^2.1.
|
|
107
|
-
"@djangocfg/centrifugo": "^2.1.
|
|
108
|
-
"@djangocfg/typescript-config": "^2.1.
|
|
109
|
-
"@djangocfg/ui-core": "^2.1.
|
|
110
|
-
"@djangocfg/ui-nextjs": "^2.1.
|
|
111
|
-
"@djangocfg/ui-tools": "^2.1.
|
|
105
|
+
"@djangocfg/api": "^2.1.115",
|
|
106
|
+
"@djangocfg/i18n": "^2.1.115",
|
|
107
|
+
"@djangocfg/centrifugo": "^2.1.115",
|
|
108
|
+
"@djangocfg/typescript-config": "^2.1.115",
|
|
109
|
+
"@djangocfg/ui-core": "^2.1.115",
|
|
110
|
+
"@djangocfg/ui-nextjs": "^2.1.115",
|
|
111
|
+
"@djangocfg/ui-tools": "^2.1.115",
|
|
112
112
|
"@types/node": "^24.7.2",
|
|
113
113
|
"@types/react": "^19.1.0",
|
|
114
114
|
"@types/react-dom": "^19.1.0",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
export { AppLayout } from './AppLayout';
|
|
6
|
-
export type { AppLayoutProps, LayoutMode } from './AppLayout';
|
|
6
|
+
export type { AppLayoutProps, LayoutMode, I18nLayoutConfig } from './AppLayout';
|
|
7
7
|
|
|
8
8
|
export { BaseApp } from './BaseApp';
|
|
9
9
|
export type { BaseAppProps } from './BaseApp';
|
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
import React, { useEffect, useState } from 'react';
|
|
11
11
|
|
|
12
12
|
import { useCfgRouter } from '@djangocfg/api/auth';
|
|
13
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
13
14
|
|
|
14
15
|
import { Suspense } from '../../components';
|
|
15
16
|
import { OAuthCallback } from './components/oauth';
|
|
16
17
|
import { IdentifierStep, OTPStep, SetupStep, TwoFactorStep } from './components/steps';
|
|
17
18
|
import { AUTH } from './constants';
|
|
18
|
-
import { AUTH_CONTENT } from './content';
|
|
19
19
|
import { AuthFormProvider, useAuthFormContext } from './context';
|
|
20
20
|
|
|
21
21
|
import './styles/auth.css';
|
|
@@ -101,8 +101,11 @@ const AuthSuccess: React.FC<AuthSuccessInlineProps> = ({
|
|
|
101
101
|
redirectDelay = AUTH.REDIRECT_DELAY,
|
|
102
102
|
}) => {
|
|
103
103
|
const router = useCfgRouter();
|
|
104
|
+
const t = useTypedT<I18nTranslations>();
|
|
104
105
|
const [isVisible, setIsVisible] = useState(false);
|
|
105
106
|
|
|
107
|
+
const successMessage = React.useMemo(() => t('layouts.auth.success.message'), [t]);
|
|
108
|
+
|
|
106
109
|
useEffect(() => {
|
|
107
110
|
const animTimer = setTimeout(() => setIsVisible(true), AUTH.ANIMATION_START_DELAY);
|
|
108
111
|
const redirectTimer = setTimeout(() => {
|
|
@@ -150,7 +153,7 @@ const AuthSuccess: React.FC<AuthSuccessInlineProps> = ({
|
|
|
150
153
|
className="auth-success-text"
|
|
151
154
|
style={{ opacity: isVisible ? 1 : 0 }}
|
|
152
155
|
>
|
|
153
|
-
{
|
|
156
|
+
{successMessage}
|
|
154
157
|
</p>
|
|
155
158
|
</div>
|
|
156
159
|
</div>
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
4
6
|
|
|
5
7
|
export interface AuthDividerProps {
|
|
6
8
|
text?: string;
|
|
@@ -11,12 +13,16 @@ export interface AuthDividerProps {
|
|
|
11
13
|
* AuthDivider - Minimal "or" divider
|
|
12
14
|
*/
|
|
13
15
|
export const AuthDivider: React.FC<AuthDividerProps> = ({
|
|
14
|
-
text
|
|
16
|
+
text,
|
|
15
17
|
className = '',
|
|
16
18
|
}) => {
|
|
19
|
+
const t = useTypedT<I18nTranslations>();
|
|
20
|
+
const defaultText = useMemo(() => t('layouts.auth.divider.or'), [t]);
|
|
21
|
+
const dividerText = text ?? defaultText;
|
|
22
|
+
|
|
17
23
|
return (
|
|
18
24
|
<div className={`auth-divider ${className}`}>
|
|
19
|
-
{
|
|
25
|
+
{dividerText}
|
|
20
26
|
</div>
|
|
21
27
|
);
|
|
22
28
|
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
4
6
|
|
|
5
7
|
export interface AuthFooterProps {
|
|
6
8
|
termsUrl?: string;
|
|
@@ -18,10 +20,17 @@ export const AuthFooter: React.FC<AuthFooterProps> = ({
|
|
|
18
20
|
supportUrl,
|
|
19
21
|
className = '',
|
|
20
22
|
}) => {
|
|
23
|
+
const t = useTypedT<I18nTranslations>();
|
|
24
|
+
const labels = useMemo(() => ({
|
|
25
|
+
terms: t('layouts.auth.footer.terms'),
|
|
26
|
+
privacy: t('layouts.auth.footer.privacy'),
|
|
27
|
+
support: t('layouts.auth.footer.support'),
|
|
28
|
+
}), [t]);
|
|
29
|
+
|
|
21
30
|
const links = [
|
|
22
|
-
termsUrl && { href: termsUrl, label:
|
|
23
|
-
privacyUrl && { href: privacyUrl, label:
|
|
24
|
-
supportUrl && { href: supportUrl, label:
|
|
31
|
+
termsUrl && { href: termsUrl, label: labels.terms },
|
|
32
|
+
privacyUrl && { href: privacyUrl, label: labels.privacy },
|
|
33
|
+
supportUrl && { href: supportUrl, label: labels.support },
|
|
25
34
|
].filter(Boolean) as { href: string; label: string }[];
|
|
26
35
|
|
|
27
36
|
if (links.length === 0) {
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Mail, Phone } from 'lucide-react';
|
|
4
|
-
import React from 'react';
|
|
4
|
+
import React, { useMemo } from 'react';
|
|
5
|
+
|
|
6
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
5
7
|
|
|
6
8
|
import type { AuthChannel } from '../../types';
|
|
7
9
|
|
|
@@ -21,6 +23,12 @@ export const ChannelToggle: React.FC<ChannelToggleProps> = ({
|
|
|
21
23
|
disabled = false,
|
|
22
24
|
className = '',
|
|
23
25
|
}) => {
|
|
26
|
+
const t = useTypedT<I18nTranslations>();
|
|
27
|
+
const labels = useMemo(() => ({
|
|
28
|
+
email: t('layouts.auth.identifier.channel.email'),
|
|
29
|
+
phone: t('layouts.auth.identifier.channel.phone'),
|
|
30
|
+
}), [t]);
|
|
31
|
+
|
|
24
32
|
return (
|
|
25
33
|
<div className={`auth-channel-toggle ${className}`}>
|
|
26
34
|
<button
|
|
@@ -31,7 +39,7 @@ export const ChannelToggle: React.FC<ChannelToggleProps> = ({
|
|
|
31
39
|
disabled={disabled}
|
|
32
40
|
>
|
|
33
41
|
<Mail />
|
|
34
|
-
|
|
42
|
+
{labels.email}
|
|
35
43
|
</button>
|
|
36
44
|
<button
|
|
37
45
|
type="button"
|
|
@@ -41,7 +49,7 @@ export const ChannelToggle: React.FC<ChannelToggleProps> = ({
|
|
|
41
49
|
disabled={disabled}
|
|
42
50
|
>
|
|
43
51
|
<Phone />
|
|
44
|
-
|
|
52
|
+
{labels.phone}
|
|
45
53
|
</button>
|
|
46
54
|
</div>
|
|
47
55
|
);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
4
|
|
|
5
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
5
6
|
import { Checkbox } from '@djangocfg/ui-core/components';
|
|
6
7
|
|
|
7
8
|
export interface TermsCheckboxProps {
|
|
@@ -24,6 +25,14 @@ export const TermsCheckbox: React.FC<TermsCheckboxProps> = ({
|
|
|
24
25
|
disabled = false,
|
|
25
26
|
className = '',
|
|
26
27
|
}) => {
|
|
28
|
+
const t = useTypedT<I18nTranslations>();
|
|
29
|
+
const labels = useMemo(() => ({
|
|
30
|
+
agree: t('layouts.auth.terms.agree'),
|
|
31
|
+
and: t('layouts.auth.terms.and'),
|
|
32
|
+
terms: t('layouts.auth.terms.terms'),
|
|
33
|
+
privacy: t('layouts.auth.terms.privacy'),
|
|
34
|
+
}), [t]);
|
|
35
|
+
|
|
27
36
|
// Don't render if no links provided
|
|
28
37
|
if (!termsUrl && !privacyUrl) {
|
|
29
38
|
return null;
|
|
@@ -39,16 +48,16 @@ export const TermsCheckbox: React.FC<TermsCheckboxProps> = ({
|
|
|
39
48
|
className="auth-terms-checkbox"
|
|
40
49
|
/>
|
|
41
50
|
<label htmlFor="auth-terms">
|
|
42
|
-
|
|
51
|
+
{labels.agree}{' '}
|
|
43
52
|
{termsUrl && (
|
|
44
53
|
<a href={termsUrl} target="_blank" rel="noopener noreferrer">
|
|
45
|
-
|
|
54
|
+
{labels.terms}
|
|
46
55
|
</a>
|
|
47
56
|
)}
|
|
48
|
-
{termsUrl && privacyUrl &&
|
|
57
|
+
{termsUrl && privacyUrl && ` ${labels.and} `}
|
|
49
58
|
{privacyUrl && (
|
|
50
59
|
<a href={privacyUrl} target="_blank" rel="noopener noreferrer">
|
|
51
|
-
|
|
60
|
+
{labels.privacy}
|
|
52
61
|
</a>
|
|
53
62
|
)}
|
|
54
63
|
</label>
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Github } from 'lucide-react';
|
|
4
|
-
import React, { useEffect } from 'react';
|
|
4
|
+
import React, { useEffect, useMemo } from 'react';
|
|
5
5
|
|
|
6
6
|
import { useGithubAuth } from '@djangocfg/api/auth';
|
|
7
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
7
8
|
import { Input, PhoneInput } from '@djangocfg/ui-core/components';
|
|
8
9
|
|
|
9
|
-
import { AUTH_CONTENT } from '../../content';
|
|
10
10
|
import { useAuthFormContext } from '../../context';
|
|
11
11
|
import {
|
|
12
12
|
AuthButton,
|
|
@@ -52,6 +52,24 @@ export const IdentifierStep: React.FC = () => {
|
|
|
52
52
|
validateIdentifier,
|
|
53
53
|
} = useAuthFormContext();
|
|
54
54
|
|
|
55
|
+
// Translations
|
|
56
|
+
const t = useTypedT<I18nTranslations>();
|
|
57
|
+
const content = useMemo(() => ({
|
|
58
|
+
title: t('layouts.auth.identifier.title'),
|
|
59
|
+
subtitle: {
|
|
60
|
+
email: t('layouts.auth.identifier.subtitle.email'),
|
|
61
|
+
phone: t('layouts.auth.identifier.subtitle.phone'),
|
|
62
|
+
},
|
|
63
|
+
placeholder: {
|
|
64
|
+
email: t('layouts.auth.identifier.placeholder.email'),
|
|
65
|
+
phone: t('layouts.auth.identifier.placeholder.phone'),
|
|
66
|
+
},
|
|
67
|
+
button: t('layouts.auth.identifier.button'),
|
|
68
|
+
oauth: {
|
|
69
|
+
github: t('layouts.auth.identifier.oauth.github'),
|
|
70
|
+
},
|
|
71
|
+
}), [t]);
|
|
72
|
+
|
|
55
73
|
// GitHub OAuth
|
|
56
74
|
const { isLoading: isGithubLoading, startGithubAuth } = useGithubAuth({
|
|
57
75
|
sourceUrl,
|
|
@@ -87,7 +105,6 @@ export const IdentifierStep: React.FC = () => {
|
|
|
87
105
|
}
|
|
88
106
|
};
|
|
89
107
|
|
|
90
|
-
const content = AUTH_CONTENT.identifier;
|
|
91
108
|
const subtitle = channel === 'phone' ? content.subtitle.phone : content.subtitle.email;
|
|
92
109
|
const hasTerms = Boolean(termsUrl || privacyUrl);
|
|
93
110
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React, { useCallback, useState } from 'react';
|
|
3
|
+
import React, { useCallback, useMemo, useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
4
6
|
|
|
5
7
|
import { config } from '../../../../utils';
|
|
6
8
|
import { AUTH } from '../../constants';
|
|
7
|
-
import { AUTH_CONTENT } from '../../content';
|
|
8
9
|
import { useAuthFormContext } from '../../context';
|
|
9
10
|
import {
|
|
10
11
|
AuthButton,
|
|
@@ -42,8 +43,20 @@ export const OTPStep: React.FC = () => {
|
|
|
42
43
|
|
|
43
44
|
const [submitState, setSubmitState] = useState<SubmitState>('idle');
|
|
44
45
|
|
|
46
|
+
// Translations
|
|
47
|
+
const t = useTypedT<I18nTranslations>();
|
|
48
|
+
const content = useMemo(() => ({
|
|
49
|
+
title: t('layouts.auth.otp.title'),
|
|
50
|
+
subtitle: t('layouts.auth.otp.subtitle'),
|
|
51
|
+
button: t('layouts.auth.otp.button'),
|
|
52
|
+
resend: t('layouts.auth.otp.resend'),
|
|
53
|
+
changeEmail: t('layouts.auth.otp.changeEmail'),
|
|
54
|
+
changePhone: t('layouts.auth.otp.changePhone'),
|
|
55
|
+
devMode: t('layouts.auth.dev.anyCodeWorks'),
|
|
56
|
+
}), [t]);
|
|
57
|
+
|
|
45
58
|
// Mask identifier for privacy
|
|
46
|
-
const maskedIdentifier =
|
|
59
|
+
const maskedIdentifier = useMemo(() => {
|
|
47
60
|
if (channel === 'phone') {
|
|
48
61
|
return identifier.slice(-4).padStart(identifier.length, '*');
|
|
49
62
|
}
|
|
@@ -72,7 +85,7 @@ export const OTPStep: React.FC = () => {
|
|
|
72
85
|
[handleOTPSubmit, isLoading, isAutoSubmittingFromUrl, submitState]
|
|
73
86
|
);
|
|
74
87
|
|
|
75
|
-
const
|
|
88
|
+
const changeIdentifierLabel = channel === 'phone' ? content.changePhone : content.changeEmail;
|
|
76
89
|
|
|
77
90
|
return (
|
|
78
91
|
<AuthContainer step="otp">
|
|
@@ -91,7 +104,7 @@ export const OTPStep: React.FC = () => {
|
|
|
91
104
|
/>
|
|
92
105
|
|
|
93
106
|
{config.isDevelopment && (
|
|
94
|
-
<div className="auth-dev-notice">{
|
|
107
|
+
<div className="auth-dev-notice">{content.devMode}</div>
|
|
95
108
|
)}
|
|
96
109
|
|
|
97
110
|
<AuthError message={error} />
|
|
@@ -105,7 +118,7 @@ export const OTPStep: React.FC = () => {
|
|
|
105
118
|
{content.resend}
|
|
106
119
|
</AuthLink>
|
|
107
120
|
<AuthLink onClick={handleBackToIdentifier} disabled={isLoading}>
|
|
108
|
-
{
|
|
121
|
+
{changeIdentifierLabel}
|
|
109
122
|
</AuthLink>
|
|
110
123
|
</div>
|
|
111
124
|
</form>
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Check, Copy } from 'lucide-react';
|
|
4
|
-
import React from 'react';
|
|
4
|
+
import React, { useMemo } from 'react';
|
|
5
|
+
|
|
6
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
5
7
|
|
|
6
|
-
import { AUTH_CONTENT } from '../../../content';
|
|
7
8
|
import { useCopyToClipboard } from '../../../hooks';
|
|
8
9
|
import { AuthButton, AuthContainer, AuthHeader } from '../../shared';
|
|
9
10
|
|
|
@@ -22,7 +23,15 @@ export const SetupComplete: React.FC<SetupCompleteProps> = ({
|
|
|
22
23
|
onDone,
|
|
23
24
|
}) => {
|
|
24
25
|
const { copied, copy } = useCopyToClipboard();
|
|
25
|
-
const
|
|
26
|
+
const t = useTypedT<I18nTranslations>();
|
|
27
|
+
const content = useMemo(() => ({
|
|
28
|
+
title: t('layouts.auth.setup.complete.title'),
|
|
29
|
+
subtitle: t('layouts.auth.setup.complete.subtitle'),
|
|
30
|
+
instruction: t('layouts.auth.setup.complete.instruction'),
|
|
31
|
+
copyAll: t('layouts.auth.setup.complete.copyAll'),
|
|
32
|
+
copied: t('layouts.auth.setup.complete.copied'),
|
|
33
|
+
done: t('layouts.auth.setup.complete.done'),
|
|
34
|
+
}), [t]);
|
|
26
35
|
|
|
27
36
|
const handleCopyAll = () => {
|
|
28
37
|
copy(backupCodes.join('\n'));
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
4
6
|
|
|
5
|
-
import { AUTH_CONTENT } from '../../../content';
|
|
6
7
|
import { AuthButton, AuthContainer, AuthHeader } from '../../shared';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* SetupLoading - Loading state for 2FA setup
|
|
10
11
|
*/
|
|
11
12
|
export const SetupLoading: React.FC = () => {
|
|
12
|
-
const
|
|
13
|
+
const t = useTypedT<I18nTranslations>();
|
|
14
|
+
const content = useMemo(() => ({
|
|
15
|
+
title: t('layouts.auth.setup.loading.title'),
|
|
16
|
+
button: t('layouts.auth.setup.loading.button'),
|
|
17
|
+
}), [t]);
|
|
13
18
|
|
|
14
19
|
return (
|
|
15
20
|
<AuthContainer step="2fa-setup">
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Check, Copy } from 'lucide-react';
|
|
4
|
-
import React, { useState } from 'react';
|
|
4
|
+
import React, { useMemo, useState } from 'react';
|
|
5
5
|
import { QRCodeSVG } from 'qrcode.react';
|
|
6
6
|
|
|
7
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
7
8
|
import {
|
|
8
9
|
Collapsible,
|
|
9
10
|
CollapsibleContent,
|
|
@@ -11,7 +12,6 @@ import {
|
|
|
11
12
|
} from '@djangocfg/ui-core/components';
|
|
12
13
|
|
|
13
14
|
import { AUTH } from '../../../constants';
|
|
14
|
-
import { AUTH_CONTENT } from '../../../content';
|
|
15
15
|
import { useCopyToClipboard } from '../../../hooks';
|
|
16
16
|
import {
|
|
17
17
|
AuthButton,
|
|
@@ -44,7 +44,17 @@ export const SetupQRCode: React.FC<SetupQRCodeProps> = ({
|
|
|
44
44
|
}) => {
|
|
45
45
|
const [confirmCode, setConfirmCode] = useState('');
|
|
46
46
|
const { copied, copy } = useCopyToClipboard();
|
|
47
|
-
const
|
|
47
|
+
const t = useTypedT<I18nTranslations>();
|
|
48
|
+
const content = useMemo(() => ({
|
|
49
|
+
title: t('layouts.auth.setup.qrCode.title'),
|
|
50
|
+
subtitle: t('layouts.auth.setup.qrCode.subtitle'),
|
|
51
|
+
manualEntry: t('layouts.auth.setup.qrCode.manualEntry'),
|
|
52
|
+
confirmPrompt: t('layouts.auth.setup.qrCode.confirmPrompt'),
|
|
53
|
+
button: t('layouts.auth.setup.qrCode.button'),
|
|
54
|
+
skip: t('layouts.auth.setup.qrCode.skip'),
|
|
55
|
+
copied: t('layouts.auth.setup.complete.copied'),
|
|
56
|
+
copy: 'Copy',
|
|
57
|
+
}), [t]);
|
|
48
58
|
|
|
49
59
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
50
60
|
e.preventDefault();
|
|
@@ -82,12 +92,12 @@ export const SetupQRCode: React.FC<SetupQRCodeProps> = ({
|
|
|
82
92
|
{copied ? (
|
|
83
93
|
<>
|
|
84
94
|
<Check className="w-4 h-4" />
|
|
85
|
-
{
|
|
95
|
+
{content.copied}
|
|
86
96
|
</>
|
|
87
97
|
) : (
|
|
88
98
|
<>
|
|
89
99
|
<Copy className="w-4 h-4" />
|
|
90
|
-
|
|
100
|
+
{content.copy}
|
|
91
101
|
</>
|
|
92
102
|
)}
|
|
93
103
|
</AuthLink>
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import React from 'react';
|
|
3
|
+
import React, { useMemo } from 'react';
|
|
4
4
|
|
|
5
|
+
import { useTypedT, type I18nTranslations } from '@djangocfg/i18n';
|
|
5
6
|
import { Input } from '@djangocfg/ui-core/components';
|
|
6
7
|
|
|
7
8
|
import { AUTH } from '../../constants';
|
|
8
|
-
import { AUTH_CONTENT } from '../../content';
|
|
9
9
|
import { useAuthFormContext } from '../../context';
|
|
10
10
|
import {
|
|
11
11
|
AuthButton,
|
|
@@ -37,7 +37,24 @@ export const TwoFactorStep: React.FC = () => {
|
|
|
37
37
|
handleUseTOTP,
|
|
38
38
|
} = useAuthFormContext();
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
// Translations
|
|
41
|
+
const t = useTypedT<I18nTranslations>();
|
|
42
|
+
const content = useMemo(() => ({
|
|
43
|
+
title: t('layouts.auth.twoFactor.title'),
|
|
44
|
+
subtitle: {
|
|
45
|
+
totp: t('layouts.auth.twoFactor.subtitle.totp'),
|
|
46
|
+
backup: t('layouts.auth.twoFactor.subtitle.backup'),
|
|
47
|
+
},
|
|
48
|
+
placeholder: {
|
|
49
|
+
backup: t('layouts.auth.twoFactor.placeholder.backup'),
|
|
50
|
+
},
|
|
51
|
+
button: t('layouts.auth.twoFactor.button'),
|
|
52
|
+
toggle: {
|
|
53
|
+
toBackup: t('layouts.auth.twoFactor.toggle.toBackup'),
|
|
54
|
+
toTotp: t('layouts.auth.twoFactor.toggle.toTotp'),
|
|
55
|
+
},
|
|
56
|
+
}), [t]);
|
|
57
|
+
|
|
41
58
|
const subtitle = useBackupCode ? content.subtitle.backup : content.subtitle.totp;
|
|
42
59
|
|
|
43
60
|
return (
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AuthLayout Content
|
|
3
|
-
*
|
|
4
|
-
* All UI strings centralized for easy updates and i18n.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
export const AUTH_CONTENT = {
|
|
8
|
-
identifier: {
|
|
9
|
-
title: 'Sign in',
|
|
10
|
-
subtitle: {
|
|
11
|
-
email: 'Enter your email to continue',
|
|
12
|
-
phone: 'Enter your phone number to continue',
|
|
13
|
-
},
|
|
14
|
-
placeholder: {
|
|
15
|
-
email: 'email@example.com',
|
|
16
|
-
phone: 'Enter your phone number',
|
|
17
|
-
},
|
|
18
|
-
button: 'Continue',
|
|
19
|
-
oauth: {
|
|
20
|
-
github: 'Continue with GitHub',
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
|
|
24
|
-
otp: {
|
|
25
|
-
title: 'Enter code',
|
|
26
|
-
subtitle: 'We sent a code to',
|
|
27
|
-
button: 'Verify',
|
|
28
|
-
resend: 'Resend code',
|
|
29
|
-
changeIdentifier: (channel: string) => `Change ${channel}`,
|
|
30
|
-
},
|
|
31
|
-
|
|
32
|
-
twoFactor: {
|
|
33
|
-
title: 'Two-factor authentication',
|
|
34
|
-
subtitle: {
|
|
35
|
-
totp: 'Enter the 6-digit code from your authenticator',
|
|
36
|
-
backup: 'Enter one of your backup codes',
|
|
37
|
-
},
|
|
38
|
-
placeholder: {
|
|
39
|
-
backup: 'XXXXXXXX',
|
|
40
|
-
},
|
|
41
|
-
button: 'Verify',
|
|
42
|
-
toggle: {
|
|
43
|
-
toBackup: 'Use backup code',
|
|
44
|
-
toTotp: 'Use authenticator app',
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
|
|
48
|
-
setup: {
|
|
49
|
-
loading: {
|
|
50
|
-
title: 'Setting up...',
|
|
51
|
-
button: 'Loading',
|
|
52
|
-
},
|
|
53
|
-
qrCode: {
|
|
54
|
-
title: 'Set up 2FA',
|
|
55
|
-
subtitle: 'Scan this QR code with your authenticator app',
|
|
56
|
-
manualEntry: "Can't scan? Enter manually",
|
|
57
|
-
confirmPrompt: 'Enter the 6-digit code to confirm',
|
|
58
|
-
button: 'Enable 2FA',
|
|
59
|
-
skip: 'Skip for now',
|
|
60
|
-
},
|
|
61
|
-
complete: {
|
|
62
|
-
title: '2FA enabled',
|
|
63
|
-
subtitle: 'Save these backup codes securely',
|
|
64
|
-
instruction: 'Each code can only be used once',
|
|
65
|
-
copyAll: 'Copy all codes',
|
|
66
|
-
copied: 'Copied',
|
|
67
|
-
done: 'Done',
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
|
|
71
|
-
success: {
|
|
72
|
-
message: 'Signed in successfully',
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
dev: {
|
|
76
|
-
anyCodeWorks: 'Dev Mode: Any code works',
|
|
77
|
-
},
|
|
78
|
-
} as const;
|