@djangocfg/layouts 2.1.108 → 2.1.110

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 (42) hide show
  1. package/README.md +16 -9
  2. package/package.json +15 -15
  3. package/src/layouts/AuthLayout/AuthLayout.tsx +92 -20
  4. package/src/layouts/AuthLayout/components/index.ts +11 -7
  5. package/src/layouts/AuthLayout/components/oauth/index.ts +0 -1
  6. package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +35 -0
  7. package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +56 -0
  8. package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +22 -0
  9. package/src/layouts/AuthLayout/components/shared/AuthError.tsx +26 -0
  10. package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +47 -0
  11. package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +53 -0
  12. package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +41 -0
  13. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +42 -0
  14. package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +48 -0
  15. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +57 -0
  16. package/src/layouts/AuthLayout/components/shared/index.ts +21 -0
  17. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +171 -0
  18. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +114 -0
  19. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +70 -0
  20. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +24 -0
  21. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +125 -0
  22. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +91 -0
  23. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +92 -0
  24. package/src/layouts/AuthLayout/components/steps/index.ts +6 -0
  25. package/src/layouts/AuthLayout/constants.ts +24 -0
  26. package/src/layouts/AuthLayout/content.ts +78 -0
  27. package/src/layouts/AuthLayout/hooks/index.ts +1 -0
  28. package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +37 -0
  29. package/src/layouts/AuthLayout/index.ts +9 -5
  30. package/src/layouts/AuthLayout/styles/auth.css +578 -0
  31. package/src/layouts/PrivateLayout/PrivateLayout.tsx +13 -1
  32. package/src/layouts/PrivateLayout/components/PrivateSidebar.tsx +59 -46
  33. package/src/layouts/PrivateLayout/index.ts +1 -1
  34. package/src/layouts/ProfileLayout/ProfileLayout.tsx +2 -2
  35. package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +2 -2
  36. package/src/layouts/AuthLayout/components/AuthHelp.tsx +0 -114
  37. package/src/layouts/AuthLayout/components/AuthSuccess.tsx +0 -101
  38. package/src/layouts/AuthLayout/components/IdentifierForm.tsx +0 -322
  39. package/src/layouts/AuthLayout/components/OTPForm.tsx +0 -174
  40. package/src/layouts/AuthLayout/components/TwoFactorForm.tsx +0 -140
  41. package/src/layouts/AuthLayout/components/TwoFactorSetup.tsx +0 -286
  42. package/src/layouts/AuthLayout/components/oauth/OAuthProviders.tsx +0 -56
@@ -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
+ };
@@ -0,0 +1,125 @@
1
+ 'use client';
2
+
3
+ import { Check, Copy } from 'lucide-react';
4
+ import React, { useState } from 'react';
5
+ import { QRCodeSVG } from 'qrcode.react';
6
+
7
+ import {
8
+ Collapsible,
9
+ CollapsibleContent,
10
+ CollapsibleTrigger,
11
+ } from '@djangocfg/ui-core/components';
12
+
13
+ import { AUTH } from '../../../constants';
14
+ import { AUTH_CONTENT } from '../../../content';
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;
30
+ onConfirm: (code: string) => Promise<unknown>;
31
+ onSkip?: () => void;
32
+ }
33
+
34
+ /**
35
+ * SetupQRCode - QR code scanning step for 2FA setup
36
+ */
37
+ export const SetupQRCode: React.FC<SetupQRCodeProps> = ({
38
+ provisioningUri,
39
+ secret,
40
+ isLoading,
41
+ error,
42
+ onConfirm,
43
+ onSkip,
44
+ }) => {
45
+ const [confirmCode, setConfirmCode] = useState('');
46
+ const { copied, copy } = useCopyToClipboard();
47
+ const content = AUTH_CONTENT.setup.qrCode;
48
+
49
+ const handleSubmit = async (e: React.FormEvent) => {
50
+ e.preventDefault();
51
+ await onConfirm(confirmCode);
52
+ };
53
+
54
+ const handleCopySecret = () => {
55
+ copy(secret);
56
+ };
57
+
58
+ return (
59
+ <AuthContainer step="2fa-setup">
60
+ <AuthHeader title={content.title} subtitle={content.subtitle} />
61
+
62
+ <form onSubmit={handleSubmit} className="auth-form-group">
63
+ <div className="auth-qr-container">
64
+ <div className="auth-qr">
65
+ <QRCodeSVG
66
+ value={provisioningUri}
67
+ size={AUTH.QR_CODE_SIZE}
68
+ level="M"
69
+ marginSize={0}
70
+ />
71
+ </div>
72
+ </div>
73
+
74
+ <Collapsible>
75
+ <CollapsibleTrigger asChild>
76
+ <AuthLink>{content.manualEntry}</AuthLink>
77
+ </CollapsibleTrigger>
78
+ <CollapsibleContent>
79
+ <div className="auth-secret-container">
80
+ <code className="auth-secret">{secret}</code>
81
+ <AuthLink onClick={handleCopySecret}>
82
+ {copied ? (
83
+ <>
84
+ <Check className="w-4 h-4" />
85
+ {AUTH_CONTENT.setup.complete.copied}
86
+ </>
87
+ ) : (
88
+ <>
89
+ <Copy className="w-4 h-4" />
90
+ Copy
91
+ </>
92
+ )}
93
+ </AuthLink>
94
+ </div>
95
+ </CollapsibleContent>
96
+ </Collapsible>
97
+
98
+ <p className="auth-instruction">{content.confirmPrompt}</p>
99
+
100
+ <AuthOTPInput
101
+ value={confirmCode}
102
+ onChange={setConfirmCode}
103
+ disabled={isLoading}
104
+ />
105
+
106
+ <AuthError message={error} />
107
+
108
+ <AuthButton
109
+ loading={isLoading}
110
+ disabled={confirmCode.length !== AUTH.OTP_LENGTH}
111
+ >
112
+ {content.button}
113
+ </AuthButton>
114
+
115
+ {onSkip && (
116
+ <div className="auth-actions">
117
+ <AuthLink onClick={onSkip} disabled={isLoading}>
118
+ {content.skip}
119
+ </AuthLink>
120
+ </div>
121
+ )}
122
+ </form>
123
+ </AuthContainer>
124
+ );
125
+ };
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+
3
+ import React 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
19
+ *
20
+ * Delegates rendering to focused sub-components:
21
+ * - SetupLoading: Initial loading state
22
+ * - SetupQRCode: QR code scanning
23
+ * - SetupComplete: Backup codes display
24
+ */
25
+ export const SetupStep: React.FC<SetupStepProps> = ({ onComplete, onSkip }) => {
26
+ const { setStep } = useAuthFormContext();
27
+
28
+ const handleComplete = () => {
29
+ onComplete?.();
30
+ setStep('success');
31
+ };
32
+
33
+ const handleSkip = () => {
34
+ onSkip?.();
35
+ setStep('success');
36
+ };
37
+
38
+ const {
39
+ isLoading,
40
+ error,
41
+ setupData,
42
+ backupCodes,
43
+ backupCodesWarning,
44
+ setupStep,
45
+ startSetup,
46
+ confirmSetup,
47
+ } = useTwoFactorSetup({
48
+ onComplete: handleComplete,
49
+ onError: () => {},
50
+ });
51
+
52
+ // Start setup on mount
53
+ React.useEffect(() => {
54
+ if (setupStep === 'idle') {
55
+ startSetup();
56
+ }
57
+ }, [setupStep, startSetup]);
58
+
59
+ // Loading state
60
+ if (isLoading && !setupData) {
61
+ return <SetupLoading />;
62
+ }
63
+
64
+ // Complete - show backup codes
65
+ if (setupStep === 'complete' && backupCodes) {
66
+ return (
67
+ <SetupComplete
68
+ backupCodes={backupCodes}
69
+ backupCodesWarning={backupCodesWarning}
70
+ onDone={handleComplete}
71
+ />
72
+ );
73
+ }
74
+
75
+ // Setup - show QR code
76
+ if (setupData) {
77
+ return (
78
+ <SetupQRCode
79
+ provisioningUri={setupData.provisioningUri}
80
+ secret={setupData.secret}
81
+ isLoading={isLoading}
82
+ error={error}
83
+ onConfirm={confirmSetup}
84
+ onSkip={onSkip ? handleSkip : undefined}
85
+ />
86
+ );
87
+ }
88
+
89
+ // Fallback to loading
90
+ return <SetupLoading />;
91
+ };