Skip to content

Commit ee8bcf3

Browse files
committed
Formatting Recovery Code
1 parent 07d1019 commit ee8bcf3

File tree

2 files changed

+67
-30
lines changed

2 files changed

+67
-30
lines changed

app/Providers/FortifyServiceProvider.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ public function register(): void
2525
public function boot(): void
2626
{
2727
Fortify::twoFactorChallengeView(fn () => Inertia::render('auth/two-factor-challenge'));
28-
2928
Fortify::confirmPasswordView(fn () => Inertia::render('auth/confirm-password'));
3029

3130
RateLimiter::for('two-factor', function (Request $request) {

resources/js/components/two-factor-recovery-codes.tsx

Lines changed: 67 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,88 +2,126 @@ import { Button } from '@/components/ui/button';
22
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
33
import { regenerateRecoveryCodes } from '@/routes/two-factor';
44
import { Form } from '@inertiajs/react';
5-
import { Eye, EyeOff, LockKeyhole, RefreshCw } from 'lucide-react';
6-
import { useEffect, useRef, useState } from 'react';
5+
import { Eye, EyeOff, LockKeyhole, LucideIcon, RefreshCw } from 'lucide-react';
6+
import { useCallback, useEffect, useRef, useState } from 'react';
77

88
interface TwoFactorRecoveryCodesProps {
99
recoveryCodesList: string[];
1010
fetchRecoveryCodes: () => Promise<void>;
1111
}
1212

13-
export default function TwoFactorRecoveryCodes({ recoveryCodesList, fetchRecoveryCodes }: TwoFactorRecoveryCodesProps) {
14-
const [isRecoveryCodesVisible, setIsRecoveryCodesVisible] = useState<boolean>(false);
15-
const recoveryCodeSectionRef = useRef<HTMLDivElement | null>(null);
13+
export default function TwoFactorRecoveryCodes({
14+
recoveryCodesList,
15+
fetchRecoveryCodes,
16+
}: TwoFactorRecoveryCodesProps) {
17+
const [isCodesVisible, setIsCodesVisible] = useState<boolean>(false);
18+
const codesSectionRef = useRef<HTMLDivElement | null>(null);
1619

17-
const toggleRecoveryCodesVisibility = async () => {
18-
if (!isRecoveryCodesVisible && !recoveryCodesList.length) {
20+
const toggleCodesVisibility = useCallback(async () => {
21+
if (!isCodesVisible && !recoveryCodesList.length) {
1922
await fetchRecoveryCodes();
2023
}
2124

22-
setIsRecoveryCodesVisible(!isRecoveryCodesVisible);
25+
setIsCodesVisible(!isCodesVisible);
2326

24-
if (!isRecoveryCodesVisible) {
27+
if (!isCodesVisible) {
2528
setTimeout(() => {
26-
recoveryCodeSectionRef.current?.scrollIntoView({ behavior: 'smooth' });
29+
codesSectionRef.current?.scrollIntoView({
30+
behavior: 'smooth',
31+
block: 'nearest'
32+
});
2733
});
2834
}
29-
};
35+
}, [isCodesVisible, recoveryCodesList.length, fetchRecoveryCodes]);
3036

3137
useEffect(() => {
3238
if (!recoveryCodesList.length) {
3339
fetchRecoveryCodes();
3440
}
3541
}, [recoveryCodesList.length, fetchRecoveryCodes]);
3642

43+
const IconComponent: LucideIcon = isCodesVisible ? EyeOff : Eye;
44+
3745
return (
3846
<Card>
3947
<CardHeader>
4048
<CardTitle className="flex gap-3">
41-
<LockKeyhole className="size-4" />
49+
<LockKeyhole className="size-4" aria-hidden="true" />
4250
2FA Recovery Codes
4351
</CardTitle>
4452
<CardDescription>
45-
Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.
53+
Recovery codes let you regain access if you lose your 2FA device. Store them in a secure password manager.
4654
</CardDescription>
4755
</CardHeader>
4856
<CardContent>
4957
<div className="flex flex-col gap-3 select-none sm:flex-row sm:items-center sm:justify-between">
50-
<Button onClick={toggleRecoveryCodesVisibility} className="w-fit">
51-
{isRecoveryCodesVisible ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
52-
{isRecoveryCodesVisible ? 'Hide' : 'View'} Recovery Codes
58+
<Button
59+
onClick={toggleCodesVisibility}
60+
className="w-fit"
61+
aria-expanded={isCodesVisible}
62+
aria-controls="recovery-codes-section"
63+
>
64+
<IconComponent className="size-4" aria-hidden="true" />
65+
{isCodesVisible ? 'Hide' : 'View'} Recovery Codes
5366
</Button>
5467

55-
{isRecoveryCodesVisible && (
68+
{isCodesVisible && (
5669
<Form {...regenerateRecoveryCodes.form()} options={{ preserveScroll: true }} onSuccess={fetchRecoveryCodes}>
5770
{({ processing }) => (
58-
<Button variant="secondary" type="submit" disabled={processing}>
59-
<RefreshCw className={`mr-2 size-4 ${processing ? 'animate-spin' : ''}`} />
71+
<Button
72+
variant="secondary"
73+
type="submit"
74+
disabled={processing}
75+
aria-describedby="regenerate-warning"
76+
>
77+
<RefreshCw
78+
className={`mr-2 size-4 ${processing ? 'animate-spin' : ''}`}
79+
aria-hidden="true"
80+
/>
6081
{processing ? 'Regenerating...' : 'Regenerate Codes'}
6182
</Button>
6283
)}
6384
</Form>
6485
)}
6586
</div>
6687
<div
88+
id="recovery-codes-section"
6789
className={`relative overflow-hidden transition-all duration-300 ${
68-
isRecoveryCodesVisible ? 'h-auto opacity-100' : 'h-0 opacity-0'
90+
isCodesVisible ? 'h-auto opacity-100' : 'h-0 opacity-0'
6991
}`}
92+
aria-hidden={!isCodesVisible}
7093
>
7194
<div className="mt-3 space-y-3">
72-
<div ref={recoveryCodeSectionRef} className="grid gap-1 rounded-lg bg-muted p-4 font-mono text-sm">
95+
<div
96+
ref={codesSectionRef}
97+
className="grid gap-1 rounded-lg bg-muted p-4 font-mono text-sm"
98+
role="list"
99+
aria-label="Recovery codes"
100+
>
73101
{!recoveryCodesList.length ? (
74-
<div className="space-y-2">
75-
{Array.from({ length: 8 }, (_, n) => (
76-
<div key={n} className="h-4 animate-pulse rounded bg-muted-foreground/20" />
102+
<div className="space-y-2" aria-label="Loading recovery codes">
103+
{Array.from({ length: 8 }, (_, index) => (
104+
<div
105+
key={index}
106+
className="h-4 animate-pulse rounded bg-muted-foreground/20"
107+
aria-hidden="true"
108+
/>
77109
))}
78110
</div>
79111
) : (
80-
recoveryCodesList.map((code, index) => <div key={index}>{code}</div>)
112+
recoveryCodesList.map((code, index) => (
113+
<div key={index} role="listitem" className="select-text">
114+
{code}
115+
</div>
116+
))
81117
)}
82118
</div>
83-
<p className="text-xs text-muted-foreground select-none">
84-
Each can be used once to access your account and will be removed after use. If you need more, click{' '}
85-
<span className="font-bold">Regenerate Codes</span> above.
86-
</p>
119+
<div className="text-xs text-muted-foreground select-none">
120+
<p id="regenerate-warning">
121+
Each recovery code can be used once to access your account and will be removed after use.
122+
If you need more, click <span className="font-bold">Regenerate Codes</span> above.
123+
</p>
124+
</div>
87125
</div>
88126
</div>
89127
</CardContent>

0 commit comments

Comments
 (0)