@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
@@ -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
-
@@ -1,140 +0,0 @@
1
- 'use client';
2
-
3
- import { KeyRound, Loader2, ShieldCheck } from 'lucide-react';
4
- import React from 'react';
5
-
6
- import {
7
- Alert,
8
- AlertDescription,
9
- Button,
10
- Card,
11
- CardContent,
12
- CardDescription,
13
- CardFooter,
14
- CardHeader,
15
- CardTitle,
16
- OTPInput,
17
- Input,
18
- } from '@djangocfg/ui-core/components';
19
-
20
- import { useAuthFormContext } from '../context';
21
-
22
- /**
23
- * Two-Factor Authentication Form
24
- *
25
- * Displays TOTP code input or backup code input based on user selection.
26
- * Used after OTP/OAuth verification when user has 2FA enabled.
27
- */
28
- export const TwoFactorForm: React.FC = () => {
29
- const {
30
- twoFactorCode,
31
- useBackupCode,
32
- error,
33
- is2FALoading,
34
- twoFactorWarning,
35
- setTwoFactorCode,
36
- handle2FASubmit,
37
- handleUseBackupCode,
38
- handleUseTOTP,
39
- } = useAuthFormContext();
40
-
41
- return (
42
- <Card className="w-full">
43
- <CardHeader className="space-y-1 text-center">
44
- <div className="mx-auto w-12 h-12 bg-primary/10 rounded-full flex items-center justify-center mb-2">
45
- <ShieldCheck className="w-6 h-6 text-primary" />
46
- </div>
47
- <CardTitle className="text-2xl">Two-Factor Authentication</CardTitle>
48
- <CardDescription>
49
- {useBackupCode
50
- ? 'Enter one of your backup recovery codes'
51
- : 'Enter the 6-digit code from your authenticator app'}
52
- </CardDescription>
53
- </CardHeader>
54
-
55
- <form onSubmit={handle2FASubmit}>
56
- <CardContent className="space-y-4">
57
- {/* Error Alert */}
58
- {error && (
59
- <Alert variant="destructive">
60
- <AlertDescription>{error}</AlertDescription>
61
- </Alert>
62
- )}
63
-
64
- {/* Warning Alert (e.g., low backup codes) */}
65
- {twoFactorWarning && (
66
- <Alert>
67
- <AlertDescription>{twoFactorWarning}</AlertDescription>
68
- </Alert>
69
- )}
70
-
71
- {/* TOTP Code Input */}
72
- {!useBackupCode && (
73
- <div className="flex justify-center">
74
- <OTPInput
75
- length={6}
76
- validationMode="numeric"
77
- pasteBehavior="clean"
78
- value={twoFactorCode}
79
- onChange={setTwoFactorCode}
80
- disabled={is2FALoading}
81
- autoFocus={true}
82
- size="lg"
83
- />
84
- </div>
85
- )}
86
-
87
- {/* Backup Code Input */}
88
- {useBackupCode && (
89
- <div className="space-y-2">
90
- <Input
91
- type="text"
92
- placeholder="Enter backup code"
93
- value={twoFactorCode}
94
- onChange={(e) => setTwoFactorCode(e.target.value.toUpperCase())}
95
- disabled={is2FALoading}
96
- className="text-center font-mono text-lg tracking-widest"
97
- maxLength={12}
98
- autoComplete="off"
99
- />
100
- <p className="text-xs text-muted-foreground text-center">
101
- Backup codes are 8 characters, letters and numbers
102
- </p>
103
- </div>
104
- )}
105
- </CardContent>
106
-
107
- <CardFooter className="flex flex-col space-y-3">
108
- <Button
109
- type="submit"
110
- className="w-full"
111
- disabled={is2FALoading || (!useBackupCode && twoFactorCode.length !== 6)}
112
- >
113
- {is2FALoading ? (
114
- <>
115
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
116
- Verifying...
117
- </>
118
- ) : (
119
- 'Verify'
120
- )}
121
- </Button>
122
-
123
- {/* Toggle between TOTP and Backup Code */}
124
- <Button
125
- type="button"
126
- variant="ghost"
127
- className="w-full text-sm"
128
- onClick={useBackupCode ? handleUseTOTP : handleUseBackupCode}
129
- disabled={is2FALoading}
130
- >
131
- <KeyRound className="mr-2 h-4 w-4" />
132
- {useBackupCode
133
- ? 'Use authenticator app instead'
134
- : "Can't access your authenticator? Use a backup code"}
135
- </Button>
136
- </CardFooter>
137
- </form>
138
- </Card>
139
- );
140
- };