All components

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.

Open in the catalogue

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

registry/new-york/blocks/auth/auth-recovery-codes.tsx
"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>
  );
}

Similar components