@djangocfg/layouts 2.1.109 → 2.1.111

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 (53) hide show
  1. package/package.json +16 -14
  2. package/src/components/errors/ErrorBoundary.tsx +12 -6
  3. package/src/components/errors/ErrorLayout.tsx +19 -9
  4. package/src/components/errors/errorConfig.ts +28 -22
  5. package/src/layouts/AuthLayout/AuthLayout.tsx +92 -20
  6. package/src/layouts/AuthLayout/components/index.ts +11 -7
  7. package/src/layouts/AuthLayout/components/oauth/index.ts +0 -1
  8. package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +35 -0
  9. package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +56 -0
  10. package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +22 -0
  11. package/src/layouts/AuthLayout/components/shared/AuthError.tsx +26 -0
  12. package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +47 -0
  13. package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +53 -0
  14. package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +41 -0
  15. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +42 -0
  16. package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +48 -0
  17. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +57 -0
  18. package/src/layouts/AuthLayout/components/shared/index.ts +21 -0
  19. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +171 -0
  20. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +114 -0
  21. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +70 -0
  22. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +24 -0
  23. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +125 -0
  24. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +91 -0
  25. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +92 -0
  26. package/src/layouts/AuthLayout/components/steps/index.ts +6 -0
  27. package/src/layouts/AuthLayout/constants.ts +24 -0
  28. package/src/layouts/AuthLayout/content.ts +78 -0
  29. package/src/layouts/AuthLayout/hooks/index.ts +1 -0
  30. package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +37 -0
  31. package/src/layouts/AuthLayout/index.ts +9 -5
  32. package/src/layouts/AuthLayout/styles/auth.css +578 -0
  33. package/src/layouts/ProfileLayout/ProfileLayout.tsx +130 -58
  34. package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +2 -2
  35. package/src/layouts/PublicLayout/components/PublicMobileDrawer.tsx +12 -4
  36. package/src/layouts/PublicLayout/components/PublicNavigation.tsx +6 -2
  37. package/src/layouts/_components/UserMenu.tsx +14 -6
  38. package/src/snippets/AuthDialog/AuthDialog.tsx +15 -6
  39. package/src/snippets/Breadcrumbs.tsx +19 -8
  40. package/src/snippets/McpChat/components/ChatPanel.tsx +16 -6
  41. package/src/snippets/McpChat/components/ChatSidebar.tsx +20 -8
  42. package/src/snippets/PWAInstall/components/A2HSHint.tsx +23 -10
  43. package/src/snippets/PWAInstall/components/DesktopGuide.tsx +44 -32
  44. package/src/snippets/PWAInstall/components/IOSGuideDrawer.tsx +34 -25
  45. package/src/snippets/PWAInstall/components/IOSGuideModal.tsx +34 -25
  46. package/src/snippets/PushNotifications/components/PushPrompt.tsx +16 -6
  47. package/src/layouts/AuthLayout/components/AuthHelp.tsx +0 -114
  48. package/src/layouts/AuthLayout/components/AuthSuccess.tsx +0 -101
  49. package/src/layouts/AuthLayout/components/IdentifierForm.tsx +0 -322
  50. package/src/layouts/AuthLayout/components/OTPForm.tsx +0 -174
  51. package/src/layouts/AuthLayout/components/TwoFactorForm.tsx +0 -140
  52. package/src/layouts/AuthLayout/components/TwoFactorSetup.tsx +0 -286
  53. package/src/layouts/AuthLayout/components/oauth/OAuthProviders.tsx +0 -56
@@ -0,0 +1,47 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ export interface AuthFooterProps {
6
+ termsUrl?: string;
7
+ privacyUrl?: string;
8
+ supportUrl?: string;
9
+ className?: string;
10
+ }
11
+
12
+ /**
13
+ * AuthFooter - Minimal link row for terms, privacy, and support
14
+ */
15
+ export const AuthFooter: React.FC<AuthFooterProps> = ({
16
+ termsUrl,
17
+ privacyUrl,
18
+ supportUrl,
19
+ className = '',
20
+ }) => {
21
+ const links = [
22
+ termsUrl && { href: termsUrl, label: 'Terms' },
23
+ privacyUrl && { href: privacyUrl, label: 'Privacy' },
24
+ supportUrl && { href: supportUrl, label: 'Help' },
25
+ ].filter(Boolean) as { href: string; label: string }[];
26
+
27
+ if (links.length === 0) {
28
+ return null;
29
+ }
30
+
31
+ return (
32
+ <div className={`auth-footer ${className}`}>
33
+ {links.map((link, index) => (
34
+ <React.Fragment key={link.href}>
35
+ {index > 0 && <span className="auth-footer-dot">&middot;</span>}
36
+ <a
37
+ href={link.href}
38
+ target="_blank"
39
+ rel="noopener noreferrer"
40
+ >
41
+ {link.label}
42
+ </a>
43
+ </React.Fragment>
44
+ ))}
45
+ </div>
46
+ );
47
+ };
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ export interface AuthHeaderProps {
6
+ logo?: string;
7
+ title: string;
8
+ subtitle?: string;
9
+ identifier?: string;
10
+ className?: string;
11
+ }
12
+
13
+ /**
14
+ * AuthHeader - Apple-style header with logo, title, and subtitle
15
+ *
16
+ * Features:
17
+ * - Optional logo with scale animation
18
+ * - Large, bold title
19
+ * - Muted subtitle
20
+ * - Optional highlighted identifier
21
+ */
22
+ export const AuthHeader: React.FC<AuthHeaderProps> = ({
23
+ logo,
24
+ title,
25
+ subtitle,
26
+ identifier,
27
+ className = '',
28
+ }) => {
29
+ return (
30
+ <div className={`auth-header ${className}`}>
31
+ {logo && (
32
+ <img
33
+ src={logo}
34
+ alt=""
35
+ className="auth-logo"
36
+ aria-hidden="true"
37
+ />
38
+ )}
39
+ <h1 className="auth-title">{title}</h1>
40
+ {subtitle && (
41
+ <p className="auth-subtitle">
42
+ {subtitle}
43
+ {identifier && (
44
+ <>
45
+ <br />
46
+ <span className="auth-identifier">{identifier}</span>
47
+ </>
48
+ )}
49
+ </p>
50
+ )}
51
+ </div>
52
+ );
53
+ };
@@ -0,0 +1,41 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ export interface AuthLinkProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
6
+ children: React.ReactNode;
7
+ href?: string;
8
+ }
9
+
10
+ /**
11
+ * AuthLink - Text link styled button/anchor
12
+ */
13
+ export const AuthLink: React.FC<AuthLinkProps> = ({
14
+ children,
15
+ href,
16
+ className = '',
17
+ ...props
18
+ }) => {
19
+ if (href) {
20
+ return (
21
+ <a
22
+ href={href}
23
+ className={`auth-link ${className}`}
24
+ target="_blank"
25
+ rel="noopener noreferrer"
26
+ >
27
+ {children}
28
+ </a>
29
+ );
30
+ }
31
+
32
+ return (
33
+ <button
34
+ type="button"
35
+ className={`auth-link ${className}`}
36
+ {...props}
37
+ >
38
+ {children}
39
+ </button>
40
+ );
41
+ };
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import { OTPInput } from '@djangocfg/ui-core/components';
6
+
7
+ import { AUTH } from '../../constants';
8
+
9
+ export interface AuthOTPInputProps {
10
+ value: string;
11
+ onChange: (value: string) => void;
12
+ onComplete?: (value: string) => void;
13
+ disabled?: boolean;
14
+ autoFocus?: boolean;
15
+ }
16
+
17
+ /**
18
+ * AuthOTPInput - Reusable OTP input with consistent styling
19
+ *
20
+ * Used in: OTPStep, TwoFactorStep, SetupStep
21
+ */
22
+ export const AuthOTPInput: React.FC<AuthOTPInputProps> = ({
23
+ value,
24
+ onChange,
25
+ onComplete,
26
+ disabled = false,
27
+ autoFocus = true,
28
+ }) => (
29
+ <div className="auth-otp-container auth-otp-wrapper">
30
+ <OTPInput
31
+ length={AUTH.OTP_LENGTH}
32
+ validationMode="numeric"
33
+ pasteBehavior="clean"
34
+ value={value}
35
+ onChange={onChange}
36
+ onComplete={onComplete}
37
+ disabled={disabled}
38
+ autoFocus={autoFocus}
39
+ size="lg"
40
+ />
41
+ </div>
42
+ );
@@ -0,0 +1,48 @@
1
+ 'use client';
2
+
3
+ import { Mail, Phone } from 'lucide-react';
4
+ import React from 'react';
5
+
6
+ import type { AuthChannel } from '../../types';
7
+
8
+ export interface ChannelToggleProps {
9
+ channel: AuthChannel;
10
+ onChange: (channel: AuthChannel) => void;
11
+ disabled?: boolean;
12
+ className?: string;
13
+ }
14
+
15
+ /**
16
+ * ChannelToggle - Apple-style segmented control for email/phone
17
+ */
18
+ export const ChannelToggle: React.FC<ChannelToggleProps> = ({
19
+ channel,
20
+ onChange,
21
+ disabled = false,
22
+ className = '',
23
+ }) => {
24
+ return (
25
+ <div className={`auth-channel-toggle ${className}`}>
26
+ <button
27
+ type="button"
28
+ className="auth-channel-option"
29
+ data-active={channel === 'email'}
30
+ onClick={() => onChange('email')}
31
+ disabled={disabled}
32
+ >
33
+ <Mail />
34
+ Email
35
+ </button>
36
+ <button
37
+ type="button"
38
+ className="auth-channel-option"
39
+ data-active={channel === 'phone'}
40
+ onClick={() => onChange('phone')}
41
+ disabled={disabled}
42
+ >
43
+ <Phone />
44
+ Phone
45
+ </button>
46
+ </div>
47
+ );
48
+ };
@@ -0,0 +1,57 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import { Checkbox } from '@djangocfg/ui-core/components';
6
+
7
+ export interface TermsCheckboxProps {
8
+ checked: boolean;
9
+ onChange: (checked: boolean) => void;
10
+ termsUrl?: string;
11
+ privacyUrl?: string;
12
+ disabled?: boolean;
13
+ className?: string;
14
+ }
15
+
16
+ /**
17
+ * TermsCheckbox - Compact terms acceptance checkbox
18
+ */
19
+ export const TermsCheckbox: React.FC<TermsCheckboxProps> = ({
20
+ checked,
21
+ onChange,
22
+ termsUrl,
23
+ privacyUrl,
24
+ disabled = false,
25
+ className = '',
26
+ }) => {
27
+ // Don't render if no links provided
28
+ if (!termsUrl && !privacyUrl) {
29
+ return null;
30
+ }
31
+
32
+ return (
33
+ <div className={`auth-terms ${className}`}>
34
+ <Checkbox
35
+ id="auth-terms"
36
+ checked={checked}
37
+ onCheckedChange={onChange}
38
+ disabled={disabled}
39
+ className="auth-terms-checkbox"
40
+ />
41
+ <label htmlFor="auth-terms">
42
+ I agree to the{' '}
43
+ {termsUrl && (
44
+ <a href={termsUrl} target="_blank" rel="noopener noreferrer">
45
+ Terms
46
+ </a>
47
+ )}
48
+ {termsUrl && privacyUrl && ' and '}
49
+ {privacyUrl && (
50
+ <a href={privacyUrl} target="_blank" rel="noopener noreferrer">
51
+ Privacy Policy
52
+ </a>
53
+ )}
54
+ </label>
55
+ </div>
56
+ );
57
+ };
@@ -0,0 +1,21 @@
1
+ export { AuthContainer } from './AuthContainer';
2
+ export { AuthHeader } from './AuthHeader';
3
+ export { AuthFooter } from './AuthFooter';
4
+ export { AuthDivider } from './AuthDivider';
5
+ export { AuthError } from './AuthError';
6
+ export { AuthButton } from './AuthButton';
7
+ export { AuthLink } from './AuthLink';
8
+ export { AuthOTPInput } from './AuthOTPInput';
9
+ export { ChannelToggle } from './ChannelToggle';
10
+ export { TermsCheckbox } from './TermsCheckbox';
11
+
12
+ export type { AuthContainerProps } from './AuthContainer';
13
+ export type { AuthHeaderProps } from './AuthHeader';
14
+ export type { AuthFooterProps } from './AuthFooter';
15
+ export type { AuthDividerProps } from './AuthDivider';
16
+ export type { AuthErrorProps } from './AuthError';
17
+ export type { AuthButtonProps } from './AuthButton';
18
+ export type { AuthLinkProps } from './AuthLink';
19
+ export type { AuthOTPInputProps } from './AuthOTPInput';
20
+ export type { ChannelToggleProps } from './ChannelToggle';
21
+ export type { TermsCheckboxProps } from './TermsCheckbox';
@@ -0,0 +1,171 @@
1
+ 'use client';
2
+
3
+ import { Github } from 'lucide-react';
4
+ import React, { useEffect } from 'react';
5
+
6
+ import { useGithubAuth } from '@djangocfg/api/auth';
7
+ import { Input, PhoneInput } from '@djangocfg/ui-core/components';
8
+
9
+ import { AUTH_CONTENT } from '../../content';
10
+ import { useAuthFormContext } from '../../context';
11
+ import {
12
+ AuthButton,
13
+ AuthContainer,
14
+ AuthDivider,
15
+ AuthError,
16
+ AuthFooter,
17
+ AuthHeader,
18
+ ChannelToggle,
19
+ TermsCheckbox,
20
+ } from '../shared';
21
+
22
+ /**
23
+ * IdentifierStep - Apple-style email/phone input step
24
+ *
25
+ * Clean, minimal design with:
26
+ * - Optional logo
27
+ * - Channel toggle (email/phone)
28
+ * - Single input field
29
+ * - Terms checkbox
30
+ * - OAuth options
31
+ */
32
+ export const IdentifierStep: React.FC = () => {
33
+ const {
34
+ identifier,
35
+ channel,
36
+ isLoading,
37
+ acceptedTerms,
38
+ error,
39
+ logoUrl,
40
+ termsUrl,
41
+ privacyUrl,
42
+ supportUrl,
43
+ enablePhoneAuth,
44
+ enableGithubAuth,
45
+ sourceUrl,
46
+ setIdentifier,
47
+ setChannel,
48
+ setAcceptedTerms,
49
+ setError,
50
+ handleIdentifierSubmit,
51
+ detectChannelFromIdentifier,
52
+ validateIdentifier,
53
+ } = useAuthFormContext();
54
+
55
+ // GitHub OAuth
56
+ const { isLoading: isGithubLoading, startGithubAuth } = useGithubAuth({
57
+ sourceUrl,
58
+ onError: setError,
59
+ });
60
+
61
+ // Force email if phone disabled
62
+ useEffect(() => {
63
+ if (!enablePhoneAuth && channel === 'phone') {
64
+ setChannel('email');
65
+ if (identifier && detectChannelFromIdentifier(identifier) === 'phone') {
66
+ setIdentifier('');
67
+ }
68
+ }
69
+ }, [enablePhoneAuth, channel, identifier, setChannel, setIdentifier, detectChannelFromIdentifier]);
70
+
71
+ // Handle identifier change with auto-detection
72
+ const handleChange = (value: string) => {
73
+ setIdentifier(value);
74
+ const detected = detectChannelFromIdentifier(value);
75
+ if (detected && detected !== channel) {
76
+ if (detected === 'phone' && !enablePhoneAuth) return;
77
+ setChannel(detected);
78
+ }
79
+ };
80
+
81
+ // Handle channel switch
82
+ const handleChannelChange = (newChannel: 'email' | 'phone') => {
83
+ if (newChannel === 'phone' && !enablePhoneAuth) return;
84
+ setChannel(newChannel);
85
+ if (identifier && !validateIdentifier(identifier, newChannel)) {
86
+ setIdentifier('');
87
+ }
88
+ };
89
+
90
+ const content = AUTH_CONTENT.identifier;
91
+ const subtitle = channel === 'phone' ? content.subtitle.phone : content.subtitle.email;
92
+ const hasTerms = Boolean(termsUrl || privacyUrl);
93
+
94
+ return (
95
+ <AuthContainer step="identifier">
96
+ <AuthHeader logo={logoUrl} title={content.title} subtitle={subtitle} />
97
+
98
+ <form onSubmit={handleIdentifierSubmit} className="auth-form-group">
99
+ {enablePhoneAuth && (
100
+ <ChannelToggle
101
+ channel={channel}
102
+ onChange={handleChannelChange}
103
+ disabled={isLoading}
104
+ />
105
+ )}
106
+
107
+ {channel === 'phone' ? (
108
+ <div className="auth-phone-input">
109
+ <PhoneInput
110
+ value={identifier}
111
+ onChange={(value) => handleChange(value || '')}
112
+ disabled={isLoading}
113
+ placeholder={content.placeholder.phone}
114
+ defaultCountry="US"
115
+ className="auth-input"
116
+ />
117
+ </div>
118
+ ) : (
119
+ <Input
120
+ type="email"
121
+ value={identifier}
122
+ onChange={(e) => handleChange(e.target.value)}
123
+ placeholder={content.placeholder.email}
124
+ disabled={isLoading}
125
+ required
126
+ autoFocus
127
+ className="auth-input"
128
+ />
129
+ )}
130
+
131
+ <TermsCheckbox
132
+ checked={acceptedTerms}
133
+ onChange={setAcceptedTerms}
134
+ termsUrl={termsUrl}
135
+ privacyUrl={privacyUrl}
136
+ disabled={isLoading}
137
+ />
138
+
139
+ <AuthError message={error} />
140
+
141
+ <AuthButton
142
+ loading={isLoading}
143
+ disabled={!identifier || (hasTerms && !acceptedTerms)}
144
+ >
145
+ {content.button}
146
+ </AuthButton>
147
+ </form>
148
+
149
+ {enableGithubAuth && (
150
+ <>
151
+ <AuthDivider />
152
+ <AuthButton
153
+ type="button"
154
+ variant="secondary"
155
+ loading={isGithubLoading}
156
+ onClick={startGithubAuth}
157
+ >
158
+ <Github className="w-5 h-5" />
159
+ {content.oauth.github}
160
+ </AuthButton>
161
+ </>
162
+ )}
163
+
164
+ <AuthFooter
165
+ termsUrl={termsUrl}
166
+ privacyUrl={privacyUrl}
167
+ supportUrl={supportUrl}
168
+ />
169
+ </AuthContainer>
170
+ );
171
+ };
@@ -0,0 +1,114 @@
1
+ 'use client';
2
+
3
+ import React, { useCallback, useState } from 'react';
4
+
5
+ import { config } from '../../../../utils';
6
+ import { AUTH } from '../../constants';
7
+ import { AUTH_CONTENT } from '../../content';
8
+ import { useAuthFormContext } from '../../context';
9
+ import {
10
+ AuthButton,
11
+ AuthContainer,
12
+ AuthError,
13
+ AuthHeader,
14
+ AuthLink,
15
+ AuthOTPInput,
16
+ } from '../shared';
17
+
18
+ type SubmitState = 'idle' | 'submitting';
19
+
20
+ /**
21
+ * OTPStep - Apple-style OTP verification
22
+ *
23
+ * Minimal design focused on the OTP input:
24
+ * - Clear title
25
+ * - Masked identifier for privacy
26
+ * - Large OTP input
27
+ * - Text links for actions
28
+ */
29
+ export const OTPStep: React.FC = () => {
30
+ const {
31
+ identifier,
32
+ channel,
33
+ otp,
34
+ isLoading,
35
+ error,
36
+ setOtp,
37
+ handleOTPSubmit,
38
+ handleResendOTP,
39
+ handleBackToIdentifier,
40
+ isAutoSubmittingFromUrl,
41
+ } = useAuthFormContext();
42
+
43
+ const [submitState, setSubmitState] = useState<SubmitState>('idle');
44
+
45
+ // Mask identifier for privacy
46
+ const maskedIdentifier = React.useMemo(() => {
47
+ if (channel === 'phone') {
48
+ return identifier.slice(-4).padStart(identifier.length, '*');
49
+ }
50
+ const [local, domain] = identifier.split('@');
51
+ if (!local || !domain) return identifier;
52
+ return `${local[0]}${'*'.repeat(Math.min(local.length - 1, 5))}@${domain}`;
53
+ }, [identifier, channel]);
54
+
55
+ // Auto-submit on complete (state machine approach)
56
+ const handleOTPComplete = useCallback(
57
+ async (completedValue: string) => {
58
+ if (submitState !== 'idle' || isLoading || isAutoSubmittingFromUrl.current) return;
59
+ if (completedValue.length !== AUTH.OTP_LENGTH) return;
60
+
61
+ setSubmitState('submitting');
62
+ const fakeEvent = { preventDefault: () => {} } as React.FormEvent;
63
+
64
+ setTimeout(async () => {
65
+ try {
66
+ await handleOTPSubmit(fakeEvent);
67
+ } finally {
68
+ setSubmitState('idle');
69
+ }
70
+ }, AUTH.AUTO_SUBMIT_DELAY);
71
+ },
72
+ [handleOTPSubmit, isLoading, isAutoSubmittingFromUrl, submitState]
73
+ );
74
+
75
+ const content = AUTH_CONTENT.otp;
76
+
77
+ return (
78
+ <AuthContainer step="otp">
79
+ <AuthHeader
80
+ title={content.title}
81
+ subtitle={content.subtitle}
82
+ identifier={maskedIdentifier}
83
+ />
84
+
85
+ <form onSubmit={handleOTPSubmit} className="auth-form-group">
86
+ <AuthOTPInput
87
+ value={otp}
88
+ onChange={setOtp}
89
+ onComplete={handleOTPComplete}
90
+ disabled={isLoading}
91
+ />
92
+
93
+ {config.isDevelopment && (
94
+ <div className="auth-dev-notice">{AUTH_CONTENT.dev.anyCodeWorks}</div>
95
+ )}
96
+
97
+ <AuthError message={error} />
98
+
99
+ <AuthButton loading={isLoading} disabled={otp.length < AUTH.OTP_LENGTH}>
100
+ {content.button}
101
+ </AuthButton>
102
+
103
+ <div className="auth-actions">
104
+ <AuthLink onClick={handleResendOTP} disabled={isLoading}>
105
+ {content.resend}
106
+ </AuthLink>
107
+ <AuthLink onClick={handleBackToIdentifier} disabled={isLoading}>
108
+ {content.changeIdentifier(channel)}
109
+ </AuthLink>
110
+ </div>
111
+ </form>
112
+ </AuthContainer>
113
+ );
114
+ };
@@ -0,0 +1,70 @@
1
+ 'use client';
2
+
3
+ import { Check, Copy } from 'lucide-react';
4
+ import React from 'react';
5
+
6
+ import { AUTH_CONTENT } from '../../../content';
7
+ import { useCopyToClipboard } from '../../../hooks';
8
+ import { AuthButton, AuthContainer, AuthHeader } from '../../shared';
9
+
10
+ interface SetupCompleteProps {
11
+ backupCodes: string[];
12
+ backupCodesWarning?: string;
13
+ onDone: () => void;
14
+ }
15
+
16
+ /**
17
+ * SetupComplete - Backup codes display after 2FA setup
18
+ */
19
+ export const SetupComplete: React.FC<SetupCompleteProps> = ({
20
+ backupCodes,
21
+ backupCodesWarning,
22
+ onDone,
23
+ }) => {
24
+ const { copied, copy } = useCopyToClipboard();
25
+ const content = AUTH_CONTENT.setup.complete;
26
+
27
+ const handleCopyAll = () => {
28
+ copy(backupCodes.join('\n'));
29
+ };
30
+
31
+ return (
32
+ <AuthContainer step="2fa-setup">
33
+ <AuthHeader title={content.title} subtitle={content.subtitle} />
34
+
35
+ <div className="auth-form-group">
36
+ {backupCodesWarning && (
37
+ <div className="auth-dev-notice">{backupCodesWarning}</div>
38
+ )}
39
+
40
+ <div className="auth-backup-codes">
41
+ {backupCodes.map((code, index) => (
42
+ <div key={index} className="auth-backup-code">
43
+ {code}
44
+ </div>
45
+ ))}
46
+ </div>
47
+
48
+ <p className="auth-instruction">{content.instruction}</p>
49
+
50
+ <AuthButton type="button" variant="secondary" onClick={handleCopyAll}>
51
+ {copied ? (
52
+ <>
53
+ <Check className="w-4 h-4" />
54
+ {content.copied}
55
+ </>
56
+ ) : (
57
+ <>
58
+ <Copy className="w-4 h-4" />
59
+ {content.copyAll}
60
+ </>
61
+ )}
62
+ </AuthButton>
63
+
64
+ <AuthButton type="button" onClick={onDone}>
65
+ {content.done}
66
+ </AuthButton>
67
+ </div>
68
+ </AuthContainer>
69
+ );
70
+ };
@@ -0,0 +1,24 @@
1
+ 'use client';
2
+
3
+ import React from 'react';
4
+
5
+ import { AUTH_CONTENT } from '../../../content';
6
+ import { AuthButton, AuthContainer, AuthHeader } from '../../shared';
7
+
8
+ /**
9
+ * SetupLoading - Loading state for 2FA setup
10
+ */
11
+ export const SetupLoading: React.FC = () => {
12
+ const content = AUTH_CONTENT.setup.loading;
13
+
14
+ return (
15
+ <AuthContainer step="2fa-setup">
16
+ <AuthHeader title={content.title} />
17
+ <div className="auth-form-group">
18
+ <AuthButton loading disabled>
19
+ {content.button}
20
+ </AuthButton>
21
+ </div>
22
+ </AuthContainer>
23
+ );
24
+ };