@djangocfg/layouts 2.1.109 → 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 (38) hide show
  1. package/package.json +14 -14
  2. package/src/layouts/AuthLayout/AuthLayout.tsx +92 -20
  3. package/src/layouts/AuthLayout/components/index.ts +11 -7
  4. package/src/layouts/AuthLayout/components/oauth/index.ts +0 -1
  5. package/src/layouts/AuthLayout/components/shared/AuthButton.tsx +35 -0
  6. package/src/layouts/AuthLayout/components/shared/AuthContainer.tsx +56 -0
  7. package/src/layouts/AuthLayout/components/shared/AuthDivider.tsx +22 -0
  8. package/src/layouts/AuthLayout/components/shared/AuthError.tsx +26 -0
  9. package/src/layouts/AuthLayout/components/shared/AuthFooter.tsx +47 -0
  10. package/src/layouts/AuthLayout/components/shared/AuthHeader.tsx +53 -0
  11. package/src/layouts/AuthLayout/components/shared/AuthLink.tsx +41 -0
  12. package/src/layouts/AuthLayout/components/shared/AuthOTPInput.tsx +42 -0
  13. package/src/layouts/AuthLayout/components/shared/ChannelToggle.tsx +48 -0
  14. package/src/layouts/AuthLayout/components/shared/TermsCheckbox.tsx +57 -0
  15. package/src/layouts/AuthLayout/components/shared/index.ts +21 -0
  16. package/src/layouts/AuthLayout/components/steps/IdentifierStep.tsx +171 -0
  17. package/src/layouts/AuthLayout/components/steps/OTPStep.tsx +114 -0
  18. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupComplete.tsx +70 -0
  19. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupLoading.tsx +24 -0
  20. package/src/layouts/AuthLayout/components/steps/SetupStep/SetupQRCode.tsx +125 -0
  21. package/src/layouts/AuthLayout/components/steps/SetupStep/index.tsx +91 -0
  22. package/src/layouts/AuthLayout/components/steps/TwoFactorStep.tsx +92 -0
  23. package/src/layouts/AuthLayout/components/steps/index.ts +6 -0
  24. package/src/layouts/AuthLayout/constants.ts +24 -0
  25. package/src/layouts/AuthLayout/content.ts +78 -0
  26. package/src/layouts/AuthLayout/hooks/index.ts +1 -0
  27. package/src/layouts/AuthLayout/hooks/useCopyToClipboard.ts +37 -0
  28. package/src/layouts/AuthLayout/index.ts +9 -5
  29. package/src/layouts/AuthLayout/styles/auth.css +578 -0
  30. package/src/layouts/ProfileLayout/ProfileLayout.tsx +2 -2
  31. package/src/layouts/ProfileLayout/components/TwoFactorSection.tsx +2 -2
  32. package/src/layouts/AuthLayout/components/AuthHelp.tsx +0 -114
  33. package/src/layouts/AuthLayout/components/AuthSuccess.tsx +0 -101
  34. package/src/layouts/AuthLayout/components/IdentifierForm.tsx +0 -322
  35. package/src/layouts/AuthLayout/components/OTPForm.tsx +0 -174
  36. package/src/layouts/AuthLayout/components/TwoFactorForm.tsx +0 -140
  37. package/src/layouts/AuthLayout/components/TwoFactorSetup.tsx +0 -286
  38. package/src/layouts/AuthLayout/components/oauth/OAuthProviders.tsx +0 -56
@@ -1,101 +0,0 @@
1
- /**
2
- * Auth Success Component
3
- *
4
- * Full-screen success layout shown after successful authentication.
5
- * Displays a centered logo with a subtle animation, then redirects.
6
- */
7
-
8
- 'use client';
9
-
10
- import React, { useEffect, useState } from 'react';
11
-
12
- import { useCfgRouter } from '@djangocfg/api/auth';
13
-
14
- import { useAuthFormContext } from '../context';
15
-
16
- export interface AuthSuccessProps {
17
- className?: string;
18
- /** Delay before redirect in ms (default: 1500) */
19
- redirectDelay?: number;
20
- }
21
-
22
- export const AuthSuccess: React.FC<AuthSuccessProps> = ({ className, redirectDelay = 1500 }) => {
23
- const { logoUrl, redirectUrl } = useAuthFormContext();
24
- const router = useCfgRouter();
25
- const [isVisible, setIsVisible] = useState(false);
26
-
27
- useEffect(() => {
28
- // Trigger animation after mount
29
- const animTimer = setTimeout(() => setIsVisible(true), 50);
30
-
31
- // Redirect after delay
32
- const redirectTimer = setTimeout(() => {
33
- const finalUrl = redirectUrl || '/dashboard';
34
- router.hardPush(finalUrl);
35
- }, redirectDelay);
36
-
37
- return () => {
38
- clearTimeout(animTimer);
39
- clearTimeout(redirectTimer);
40
- };
41
- }, [redirectUrl, redirectDelay, router]);
42
-
43
- if (!logoUrl) {
44
- // Fallback: simple checkmark if no logo provided
45
- return (
46
- <div className={`fixed inset-0 flex items-center justify-center bg-background z-50 ${className || ''}`}>
47
- <div
48
- className={`transition-all duration-700 ease-out ${
49
- isVisible ? 'opacity-100 scale-100' : 'opacity-0 scale-95'
50
- }`}
51
- >
52
- <div className="w-24 h-24 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
53
- <svg
54
- className="w-12 h-12 text-green-600 dark:text-green-400"
55
- fill="none"
56
- stroke="currentColor"
57
- viewBox="0 0 24 24"
58
- >
59
- <path
60
- strokeLinecap="round"
61
- strokeLinejoin="round"
62
- strokeWidth={2}
63
- d="M5 13l4 4L19 7"
64
- />
65
- </svg>
66
- </div>
67
- </div>
68
- </div>
69
- );
70
- }
71
-
72
- return (
73
- <div className={`fixed inset-0 flex items-center justify-center bg-background z-50 ${className || ''}`}>
74
- <div
75
- className={`transition-all duration-700 ease-out ${
76
- isVisible ? 'opacity-100 scale-100' : 'opacity-0 scale-90'
77
- }`}
78
- >
79
- {/* Logo container with max size and animation */}
80
- <div className="relative">
81
- {/* Subtle glow effect */}
82
- <div
83
- className={`absolute inset-0 blur-3xl transition-opacity duration-1000 ${
84
- isVisible ? 'opacity-20' : 'opacity-0'
85
- }`}
86
- style={{
87
- background: 'radial-gradient(circle, currentColor 0%, transparent 70%)',
88
- }}
89
- />
90
-
91
- {/* Logo image */}
92
- <img
93
- src={logoUrl}
94
- alt="Success"
95
- className="relative w-32 h-32 sm:w-40 sm:h-40 md:w-48 md:h-48 object-contain"
96
- />
97
- </div>
98
- </div>
99
- </div>
100
- );
101
- };
@@ -1,322 +0,0 @@
1
- 'use client';
2
-
3
- import { Mail, Phone, Send, User } from 'lucide-react';
4
- import React, { useEffect, useState } from 'react';
5
-
6
- import {
7
- Button, Card, CardContent, CardDescription, CardHeader, CardTitle, Checkbox, Input, Label,
8
- PhoneInput, Tabs, TabsContent, TabsList, TabsTrigger
9
- } from '@djangocfg/ui-core/components';
10
-
11
- import { useAuthFormContext } from '../context';
12
- import { AuthHelp } from './AuthHelp';
13
- import { OAuthProviders } from './oauth';
14
-
15
- export const IdentifierForm: React.FC = () => {
16
- const {
17
- identifier,
18
- channel,
19
- isLoading,
20
- acceptedTerms,
21
- termsUrl,
22
- privacyUrl,
23
- enablePhoneAuth,
24
- setIdentifier,
25
- setChannel,
26
- setAcceptedTerms,
27
- handleIdentifierSubmit,
28
- detectChannelFromIdentifier,
29
- validateIdentifier,
30
- error,
31
- } = useAuthFormContext();
32
-
33
- const [localChannel, setLocalChannel] = useState<'email' | 'phone'>(channel);
34
-
35
- // Sync localChannel with channel from context (for localStorage updates)
36
- useEffect(() => {
37
- setLocalChannel(channel);
38
- }, [channel]);
39
-
40
- // Force email channel if phone auth is disabled
41
- useEffect(() => {
42
- if (!enablePhoneAuth && localChannel === 'phone') {
43
- setLocalChannel('email');
44
- setChannel('email');
45
- // Clear identifier if it's a phone number
46
- if (identifier && detectChannelFromIdentifier(identifier) === 'phone') {
47
- setIdentifier('');
48
- }
49
- }
50
- }, [
51
- enablePhoneAuth,
52
- localChannel,
53
- identifier,
54
- setChannel,
55
- setIdentifier,
56
- detectChannelFromIdentifier,
57
- ]);
58
-
59
- // Handle identifier change with auto-detection
60
- const handleIdentifierChange = (value: string) => {
61
- setIdentifier(value);
62
-
63
- // Auto-detect channel if user is typing (only if phone auth is enabled)
64
- const detectedChannel = detectChannelFromIdentifier(value);
65
- if (detectedChannel && detectedChannel !== localChannel) {
66
- // Only switch to phone if phone auth is enabled
67
- if (detectedChannel === 'phone' && !enablePhoneAuth) {
68
- return; // Don't switch to phone channel if disabled
69
- }
70
- setLocalChannel(detectedChannel);
71
- setChannel(detectedChannel);
72
- }
73
- };
74
-
75
- // Handle manual channel switch
76
- const handleChannelChange = (newChannel: 'email' | 'phone') => {
77
- // Prevent switching to phone if phone auth is disabled
78
- if (newChannel === 'phone' && !enablePhoneAuth) {
79
- return;
80
- }
81
-
82
- setLocalChannel(newChannel);
83
- setChannel(newChannel);
84
- // Clear identifier when switching channels
85
- if (identifier && !validateIdentifier(identifier, newChannel)) {
86
- setIdentifier('');
87
- }
88
- };
89
-
90
- const getChannelDescription = () => {
91
- return localChannel === 'phone'
92
- ? 'Enter your phone number to receive a verification code via SMS'
93
- : 'Enter your email address to receive a verification code';
94
- };
95
-
96
- // Check if we have any links for terms/privacy - if not, we don't show the checkbox
97
- const hasAnyLinks = Boolean(termsUrl || privacyUrl);
98
-
99
- return (
100
- <Card className="w-full max-w-md mx-auto shadow-lg border border-border bg-card/50 backdrop-blur-sm">
101
- <CardHeader className="text-center pb-6">
102
- <div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-4">
103
- <User className="w-6 h-6 text-primary" />
104
- </div>
105
- <CardTitle className="text-xl font-semibold">Sign In</CardTitle>
106
- <CardDescription className="text-muted-foreground">
107
- {getChannelDescription()}
108
- </CardDescription>
109
- </CardHeader>
110
- <CardContent className="space-y-6">
111
- {enablePhoneAuth ? (
112
- <Tabs
113
- value={localChannel}
114
- onValueChange={(value) => handleChannelChange(value as 'email' | 'phone')}
115
- >
116
- {/* Channel Selection Tabs */}
117
- <TabsList className="grid w-full grid-cols-2">
118
- <TabsTrigger value="email" className="flex items-center gap-2">
119
- <Mail className="w-4 h-4" />
120
- Email
121
- </TabsTrigger>
122
- <TabsTrigger value="phone" className="flex items-center gap-2">
123
- <Phone className="w-4 h-4" />
124
- Phone
125
- </TabsTrigger>
126
- </TabsList>
127
-
128
- <form onSubmit={handleIdentifierSubmit} className="space-y-6 mt-6">
129
- <TabsContent value="email" className="space-y-3 mt-0">
130
- <Label
131
- htmlFor="identifier"
132
- className="text-sm font-medium text-foreground flex items-center gap-2"
133
- >
134
- <Mail className="w-4 h-4" />
135
- Email Address
136
- </Label>
137
- <Input
138
- id="identifier"
139
- type="email"
140
- placeholder="Enter your email address"
141
- value={identifier}
142
- onChange={(e) => handleIdentifierChange(e.target.value)}
143
- disabled={isLoading}
144
- required
145
- className="h-11 text-base"
146
- />
147
- </TabsContent>
148
-
149
- <TabsContent value="phone" className="space-y-3 mt-0">
150
- <Label
151
- htmlFor="phone-identifier"
152
- className="text-sm font-medium text-foreground flex items-center gap-2"
153
- >
154
- <Phone className="w-4 h-4" />
155
- Phone Number
156
- </Label>
157
- <PhoneInput
158
- value={identifier}
159
- onChange={(value) => handleIdentifierChange(value || '')}
160
- disabled={isLoading}
161
- placeholder="Enter your phone number"
162
- defaultCountry="US"
163
- className="h-11 text-base"
164
- />
165
- </TabsContent>
166
-
167
- {/* Terms and Conditions - only show if we have links */}
168
- {hasAnyLinks && (
169
- <div className="flex items-start gap-3">
170
- <Checkbox
171
- id="terms"
172
- checked={acceptedTerms}
173
- onCheckedChange={setAcceptedTerms}
174
- disabled={isLoading}
175
- className="mt-1"
176
- />
177
- <div className="text-sm text-muted-foreground leading-5">
178
- <Label htmlFor="terms" className="cursor-pointer">
179
- I agree to the{' '}
180
- {termsUrl && (
181
- <>
182
- <a
183
- href={termsUrl}
184
- target="_blank"
185
- rel="noopener noreferrer"
186
- className="text-primary hover:underline font-medium"
187
- >
188
- Terms of Service
189
- </a>
190
- {privacyUrl && <>{' '}and{' '}</>}
191
- </>
192
- )}
193
- {privacyUrl && (
194
- <a
195
- href={privacyUrl}
196
- target="_blank"
197
- rel="noopener noreferrer"
198
- className="text-primary hover:underline font-medium"
199
- >
200
- Privacy Policy
201
- </a>
202
- )}
203
- </Label>
204
- </div>
205
- </div>
206
- )}
207
-
208
- {/* Error Message */}
209
- {error && (
210
- <div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
211
- {error}
212
- </div>
213
- )}
214
-
215
- {/* Submit Button */}
216
- <Button
217
- type="submit"
218
- size="lg"
219
- className="w-full"
220
- disabled={!identifier || (hasAnyLinks && !acceptedTerms)}
221
- loading={isLoading}
222
- >
223
- <Send className="w-4 h-4" />
224
- Send verification code
225
- </Button>
226
- </form>
227
- </Tabs>
228
- ) : (
229
- <form onSubmit={handleIdentifierSubmit} className="space-y-6 mt-6">
230
- {/* Email-only input when phone auth is disabled */}
231
- <div className="space-y-3">
232
- <Label
233
- htmlFor="email-only"
234
- className="text-sm font-medium text-foreground flex items-center gap-2"
235
- >
236
- <Mail className="w-4 h-4" />
237
- Email Address
238
- </Label>
239
- <Input
240
- id="email-only"
241
- type="email"
242
- placeholder="Enter your email address"
243
- value={identifier}
244
- onChange={(e) => handleIdentifierChange(e.target.value)}
245
- disabled={isLoading}
246
- required
247
- className="h-11 text-base"
248
- />
249
- </div>
250
-
251
- {/* Terms and Conditions - only show if we have links */}
252
- {hasAnyLinks && (
253
- <div className="flex items-start gap-3">
254
- <Checkbox
255
- id="terms-email"
256
- checked={acceptedTerms}
257
- onCheckedChange={setAcceptedTerms}
258
- disabled={isLoading}
259
- className="mt-1"
260
- />
261
- <div className="text-sm text-muted-foreground leading-5">
262
- <Label htmlFor="terms-email" className="cursor-pointer">
263
- I agree to the{' '}
264
- {termsUrl && (
265
- <>
266
- <a
267
- href={termsUrl}
268
- target="_blank"
269
- rel="noopener noreferrer"
270
- className="text-primary hover:underline font-medium"
271
- >
272
- Terms of Service
273
- </a>
274
- {privacyUrl && <>{' '}and{' '}</>}
275
- </>
276
- )}
277
- {privacyUrl && (
278
- <a
279
- href={privacyUrl}
280
- target="_blank"
281
- rel="noopener noreferrer"
282
- className="text-primary hover:underline font-medium"
283
- >
284
- Privacy Policy
285
- </a>
286
- )}
287
- </Label>
288
- </div>
289
- </div>
290
- )}
291
-
292
- {/* Error Message */}
293
- {error && (
294
- <div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
295
- {error}
296
- </div>
297
- )}
298
-
299
- {/* Submit Button */}
300
- <Button
301
- type="submit"
302
- size="lg"
303
- className="w-full"
304
- disabled={!identifier || (hasAnyLinks && !acceptedTerms)}
305
- loading={isLoading}
306
- >
307
- <Send className="w-4 h-4" />
308
- Send verification code
309
- </Button>
310
- </form>
311
- )}
312
-
313
- {/* OAuth Providers (GitHub, etc.) */}
314
- <OAuthProviders />
315
-
316
- {/* Help Section */}
317
- <AuthHelp />
318
- </CardContent>
319
- </Card>
320
- );
321
- };
322
-
@@ -1,174 +0,0 @@
1
- 'use client';
2
-
3
- import { ArrowLeft, Mail, MessageCircle, RotateCw, ShieldCheck } from 'lucide-react';
4
- import React, { useCallback, useRef } from 'react';
5
-
6
- import {
7
- Button, Card, CardContent, CardDescription, CardHeader, CardTitle, OTPInput
8
- } from '@djangocfg/ui-core/components';
9
-
10
- import { config } from '../../../utils';
11
- import { useAuthFormContext } from '../context';
12
- import { AuthHelp } from './AuthHelp';
13
-
14
- export const OTPForm: React.FC = () => {
15
- const {
16
- identifier,
17
- channel,
18
- otp,
19
- isLoading,
20
- error,
21
- supportUrl,
22
- setOtp,
23
- handleOTPSubmit,
24
- handleResendOTP,
25
- handleBackToIdentifier,
26
- isAutoSubmittingFromUrl,
27
- } = useAuthFormContext();
28
-
29
- // Ref to track if auto-submit is in progress to prevent duplicate submissions
30
- const isAutoSubmittingRef = useRef(false);
31
-
32
- // Handle auto-submit when OTP is complete (after paste or last digit entry)
33
- // Note: useAutoAuth already handles auto-submit from URL, this handles manual input/paste
34
- const handleOTPComplete = useCallback((completedValue: string) => {
35
- // Prevent duplicate submissions - check local ref, isLoading, and URL auto-submit ref
36
- if (isAutoSubmittingRef.current || isLoading || isAutoSubmittingFromUrl.current) return;
37
-
38
- if (completedValue.length === 6) {
39
- isAutoSubmittingRef.current = true;
40
-
41
- // Create a fake form event for handleOTPSubmit
42
- const fakeEvent = {
43
- preventDefault: () => {},
44
- } as React.FormEvent;
45
-
46
- // Small delay to ensure state is updated, then submit
47
- // Reset ref after submit completes (isLoading will handle preventing re-submits)
48
- setTimeout(async () => {
49
- try {
50
- await handleOTPSubmit(fakeEvent);
51
- } finally {
52
- isAutoSubmittingRef.current = false;
53
- }
54
- }, 100);
55
- }
56
- }, [handleOTPSubmit, isLoading, isAutoSubmittingFromUrl]);
57
-
58
- const getChannelIcon = () => {
59
- return channel === 'phone' ? (
60
- <div className="flex items-center justify-center">
61
- <MessageCircle className="w-5 h-5 text-primary" />
62
- </div>
63
- ) : (
64
- <Mail className="w-5 h-5 text-primary" />
65
- );
66
- };
67
-
68
- const getChannelTitle = () => {
69
- return channel === 'phone' ? 'Verify Your Phone' : 'Verify Your Email';
70
- };
71
-
72
- const getChannelDescription = () => {
73
- const channelName = channel === 'phone' ? 'phone number' : 'email address';
74
- const method = channel === 'phone' ? 'WhatsApp/SMS' : 'email';
75
- return `We've sent a 6-digit verification code to your ${channelName} via ${method}`;
76
- };
77
-
78
- return (
79
- <Card className="w-full max-w-md mx-auto shadow-lg border border-border bg-card/50 backdrop-blur-sm">
80
- <CardHeader className="text-center pb-6">
81
- <div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-4">
82
- {getChannelIcon()}
83
- </div>
84
- <CardTitle className="text-xl font-semibold">{getChannelTitle()}</CardTitle>
85
- <CardDescription className="text-muted-foreground">
86
- {getChannelDescription()}
87
- <br />
88
- <span className="font-medium text-foreground">{identifier}</span>
89
- </CardDescription>
90
- </CardHeader>
91
- <CardContent className="space-y-6">
92
- <form onSubmit={handleOTPSubmit} className="space-y-6">
93
- <div className="space-y-3">
94
- <label className="text-sm font-medium text-foreground text-center block">
95
- Enter verification code
96
- </label>
97
- <div className="flex justify-center">
98
- <OTPInput
99
- length={6}
100
- validationMode="numeric"
101
- pasteBehavior="clean"
102
- value={otp}
103
- onChange={setOtp}
104
- onComplete={handleOTPComplete}
105
- disabled={isLoading}
106
- autoFocus={true}
107
- autoSubmit={false}
108
- size="lg"
109
- />
110
- </div>
111
-
112
- {/* Development Mode Notice */}
113
- {config.isDevelopment && (
114
- <div className="text-xs text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/30 p-2 rounded-md border border-amber-200 dark:border-amber-800 text-center">
115
- 🔧 Dev Mode: Any OTP code works
116
- </div>
117
- )}
118
- </div>
119
-
120
- <div className="space-y-4">
121
- <Button
122
- type="submit"
123
- size="lg"
124
- className="w-full"
125
- disabled={otp.length < 6}
126
- loading={isLoading}
127
- >
128
- <ShieldCheck className="w-5 h-5" />
129
- Verify Code
130
- </Button>
131
-
132
- <div className="flex gap-3">
133
- <Button
134
- type="button"
135
- variant="outline"
136
- onClick={handleBackToIdentifier}
137
- disabled={isLoading}
138
- className="flex-1"
139
- >
140
- <ArrowLeft className="w-4 h-4" />
141
- Back
142
- </Button>
143
-
144
- <Button
145
- type="button"
146
- variant="outline"
147
- onClick={handleResendOTP}
148
- disabled={isLoading}
149
- className="flex-1"
150
- >
151
- <RotateCw className="w-4 h-4" />
152
- Resend
153
- </Button>
154
- </div>
155
- </div>
156
- </form>
157
-
158
- {/* Error Message */}
159
- {error && (
160
- <div className="text-sm text-destructive bg-destructive/10 p-3 rounded-md border border-destructive/20">
161
- {error}
162
- </div>
163
- )}
164
-
165
- {supportUrl && (
166
- <div className="mt-4">
167
- <AuthHelp />
168
- </div>
169
- )}
170
- </CardContent>
171
- </Card>
172
- );
173
- };
174
-