Auth Recovery Codes
Auth Recovery Codes is a React and Tailwind CSS data component from HextaUI, licensed MIT. Copy it and paste it into your project.
HextaUI
MIT
data
What it is
Auth Recovery Codes is a React and Tailwind CSS data component from HextaUI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
No packages beyond the project baseline
Licence
This component comes from HextaUI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Auth Recovery Codes
"use client";
import {
AlertTriangle,
CheckCircle2,
Copy,
Download,
Eye,
EyeOff,
Loader2,
RefreshCw,
Shield,
} from "lucide-react";
import { useCallback, useState } from "react";
import { cn } from "@/lib/utils";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/registry/new-york/ui/alert";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/registry/new-york/ui/alert-dialog";
import { Button } from "@/registry/new-york/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/registry/new-york/ui/card";
export interface AuthRecoveryCodesProps {
codes?: string[];
onGenerate?: () => void;
onRegenerate?: () => void;
className?: string;
isLoading?: boolean;
errors?: {
general?: string;
};
}
function downloadCodes(codes: string[]): void {
if (codes.length === 0) return;
const content = codes.join("\n");
const blob = new Blob([content], { type: "text/plain" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "recovery-codes.txt";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
interface ErrorAlertProps {
message: string;
}
function ErrorAlert({ message }: ErrorAlertProps) {
return (
<div
aria-live="polite"
className="rounded-lg border border-destructive/50 bg-destructive/10 p-3 text-destructive text-sm"
role="alert"
>
{message}
</div>
);
}
interface EmptyStateProps {
className?: string;
}
function EmptyState({ className }: EmptyStateProps) {
return (
<Card className={cn("w-full max-w-sm shadow-xs", className)}>
<CardHeader>
<CardTitle className="flex items-center gap-2">
Recovery codes
</CardTitle>
<CardDescription>
Backup codes for two-factor authentication
</CardDescription>
</CardHeader>
<CardContent>
<div className="flex flex-col items-center justify-center py-8 text-center">
<p className="text-muted-foreground text-sm">
No recovery codes available. Generate codes to get started.
</p>
</div>
</CardContent>
</Card>
);
}
interface EmptyStateWithGenerateProps {
isLoading: boolean;
onGenerate?: () => void;
}
function EmptyStateWithGenerate({
isLoading,
onGenerate,
}: EmptyStateWithGenerateProps) {
return (
<div className="flex flex-col items-center justify-center gap-4 rounded-lg border bg-muted/50 p-8 text-center">
<div className="flex size-12 items-center justify-center rounded-full bg-muted">
<Shield aria-hidden="true" className="size-6 text-muted-foreground" />
</div>
<div className="flex flex-col gap-2">
<p className="font-medium text-sm">No recovery codes generated</p>
<p className="text-muted-foreground text-xs">
Generate recovery codes to use as a backup for two-factor
authentication
</p>
</div>
{onGenerate && (
<Button
aria-busy={isLoading}
className="min-h-[44px] touch-manipulation"
data-loading={isLoading}
disabled={isLoading}
onClick={onGenerate}
type="button"
>
{isLoading ? (
<>
<Loader2 aria-hidden="true" className="size-4 animate-spin" />
Generating…
</>
) : (
<>
<RefreshCw aria-hidden="true" className="size-4" />
Generate recovery codes
</>
)}
</Button>
)}
</div>
);
}
interface RecoveryCodeItemProps {
code: string;
copied: boolean;
index: number;
onCopy: (code: string, index: number) => void;
showCode: boolean;
}
function RecoveryCodeItem({
code,
copied,
index,
onCopy,
showCode,
}: RecoveryCodeItemProps) {
if (showCode) {
return (
<div className="flex items-center justify-between gap-2 rounded-md border bg-muted/50 py-0.5 pl-2 font-mono text-sm">
<span className="flex-1">{code}</span>
<Button
aria-label={`Copy recovery code ${index + 1}`}
className="min-h-[10px] min-w-[10px] touch-manipulation"
onClick={() => onCopy(code, index)}
size="icon-sm"
type="button"
variant="ghost"
>
{copied ? (
<CheckCircle2
aria-hidden="true"
className="size-3.5 text-primary"
/>
) : (
<Copy aria-hidden="true" className="size-3.5" />
)}
</Button>
</div>
);
}
return (
<div className="flex items-center justify-between gap-2 rounded-md border bg-muted/50 p-2">
<span className="font-mono text-sm">
{Array.from({ length: 8 })
.map(() => "•")
.join("")}
</span>
<Copy aria-hidden="true" className="size-3.5 text-muted-foreground" />
</div>
);
}
interface RecoveryCodesListProps {
codes: string[];
copiedIndex: number | null;
onCopyCode: (code: string, index: number) => void;
showCodes: boolean;
}
function RecoveryCodesList({
codes,
copiedIndex,
onCopyCode,
showCodes,
}: RecoveryCodesListProps) {
return (
<div className="grid grid-cols-1 gap-2 rounded-lg border bg-background p-4 sm:grid-cols-2">
{codes.map((code, index) => (
<RecoveryCodeItem
code={code}
copied={copiedIndex === index}
index={index}
key={index}
onCopy={onCopyCode}
showCode={showCodes}
/>
))}
</div>
);
}
interface RecoveryCodesActionsProps {
codes: string[];
isRegenerating: boolean;
onCopyAll: () => void;
onDownload: () => void;
onRegenerate?: () => void;
}
function RecoveryCodesActions({
codes,
isRegenerating,
onCopyAll,
onDownload,
onRegenerate,
}: RecoveryCodesActionsProps) {
return (
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
<Button
className="min-h-[44px] w-full touch-manipulation sm:w-auto"
onClick={onCopyAll}
type="button"
variant="outline"
>
<Copy aria-hidden="true" className="size-4" />
Copy all codes
</Button>
<Button
className="min-h-[44px] w-full touch-manipulation sm:w-auto"
onClick={onDownload}
type="button"
variant="outline"
>
<Download aria-hidden="true" className="size-4" />
Download codes
</Button>
{onRegenerate && (
<RegenerateDialog
isRegenerating={isRegenerating}
onRegenerate={onRegenerate}
/>
)}
</div>
);
}
interface RegenerateDialogProps {
isRegenerating: boolean;
onRegenerate: () => void;
}
function RegenerateDialog({
isRegenerating,
onRegenerate,
}: RegenerateDialogProps) {
return (
<AlertDialog>
<AlertDialogTrigger asChild>
<Button
aria-busy={isRegenerating}
className="min-h-[44px] w-full touch-manipulation sm:w-auto"
data-loading={isRegenerating}
disabled={isRegenerating}
type="button"
variant="outline"
>
{isRegenerating ? (
<>
<Loader2 aria-hidden="true" className="size-4 animate-spin" />
Regenerating…
</>
) : (
<>
<RefreshCw aria-hidden="true" className="size-4" />
Regenerate codes
</>
)}
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Regenerate recovery codes?</AlertDialogTitle>
<AlertDialogDescription>
This will invalidate all existing recovery codes and generate new
ones. Make sure to save the new codes in a secure location. This
action cannot be undone.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={onRegenerate}
>
Regenerate codes
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
export default function AuthRecoveryCodes({
codes = [],
onGenerate,
onRegenerate,
className,
isLoading = false,
errors,
}: AuthRecoveryCodesProps) {
const [showCodes, setShowCodes] = useState(false);
const [copiedIndex, setCopiedIndex] = useState<number | null>(null);
const [isRegenerating, setIsRegenerating] = useState(false);
const handleCopyCode = useCallback(async (code: string, index: number) => {
try {
await navigator.clipboard.writeText(code);
setCopiedIndex(index);
setTimeout(() => setCopiedIndex(null), 2000);
} catch {}
}, []);
const handleCopyAll = useCallback(async () => {
if (codes.length === 0) return;
try {
await navigator.clipboard.writeText(codes.join("\n"));
} catch {}
}, [codes]);
const handleDownload = useCallback(() => {
downloadCodes(codes);
}, [codes]);
const handleRegenerate = useCallback(async () => {
setIsRegenerating(true);
try {
await onRegenerate?.();
setShowCodes(true);
} finally {
setIsRegenerating(false);
}
}, [onRegenerate]);
const handleToggleShowCodes = useCallback(() => {
setShowCodes((prev) => !prev);
}, []);
const generalError = errors?.general;
if (codes.length === 0 && !onGenerate) {
return <EmptyState className={className} />;
}
return (
<Card className={cn("w-full max-w-sm shadow-xs", className)}>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex flex-col gap-2">
<CardTitle className="flex items-center gap-2">
<Shield aria-hidden="true" className="size-5" />
Recovery codes
</CardTitle>
<CardDescription>
Save these codes in a safe place. You can use them to access your
account if you lose your authenticator device.
</CardDescription>
</div>
{codes.length === 0 && onGenerate && (
<Button
aria-busy={isLoading}
className="min-h-[44px] touch-manipulation"
data-loading={isLoading}
disabled={isLoading}
onClick={onGenerate}
type="button"
>
{isLoading ? (
<>
<Loader2 aria-hidden="true" className="size-4 animate-spin" />
Generating…
</>
) : (
<>
<RefreshCw aria-hidden="true" className="size-4" />
Generate codes
</>
)}
</Button>
)}
</div>
</CardHeader>
<CardContent>
<div className="flex flex-col gap-6">
{generalError && <ErrorAlert message={generalError} />}
{codes.length > 0 ? (
<>
<Alert variant="destructive">
<AlertTriangle aria-hidden="true" />
<AlertTitle>Save your recovery codes</AlertTitle>
<AlertDescription>
Keep these codes somewhere safe. Each code works only once.
</AlertDescription>
</Alert>
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
<h3 className="font-medium text-sm">Your recovery codes</h3>
<Button
className="flex min-h-[44px] w-full touch-manipulation items-center sm:w-auto"
onClick={handleToggleShowCodes}
type="button"
variant="ghost"
>
{showCodes ? (
<>
<EyeOff aria-hidden="true" className="size-4" />
Hide codes
</>
) : (
<>
<Eye aria-hidden="true" className="size-4" />
Show codes
</>
)}
</Button>
</div>
<RecoveryCodesList
codes={codes}
copiedIndex={copiedIndex}
onCopyCode={handleCopyCode}
showCodes={showCodes}
/>
<RecoveryCodesActions
codes={codes}
isRegenerating={isRegenerating}
onCopyAll={handleCopyAll}
onDownload={handleDownload}
onRegenerate={onRegenerate ? handleRegenerate : undefined}
/>
</div>
</>
) : (
<EmptyStateWithGenerate
isLoading={isLoading}
onGenerate={onGenerate}
/>
)}
</div>
</CardContent>
</Card>
);
}