All components

Auth Two Factor Verify

Auth Two Factor Verify is a React and Tailwind CSS component from HextaUI, licensed MIT. Copy it and paste it into your project.

HextaUI MIT other

What it is

Auth Two Factor Verify is a React and Tailwind CSS 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 Two Factor Verify

registry/new-york/blocks/auth/auth-two-factor-verify.tsx
"use client";

import { Loader2, RefreshCw, ShieldCheck } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
import { cn } from "@/lib/utils";
import { Button } from "@/registry/new-york/ui/button";
import {
  Card,
  CardContent,
  CardDescription,
  CardHeader,
  CardTitle,
} from "@/registry/new-york/ui/card";
import {
  Field,
  FieldContent,
  FieldError,
  FieldLabel,
} from "@/registry/new-york/ui/field";
import {
  InputGroup,
  InputGroupAddon,
  InputGroupInput,
} from "@/registry/new-york/ui/input-group";
import {
  InputOTP,
  InputOTPGroup,
  InputOTPSlot,
} from "@/registry/new-york/ui/input-otp";
import { Separator } from "@/registry/new-york/ui/separator";

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>
  );
}

function Header() {
  return (
    <>
      <CardTitle className="flex items-center gap-2">
        Verify two-factor authentication
      </CardTitle>
      <CardDescription>
        Enter the 6-digit code from your authenticator app
      </CardDescription>
    </>
  );
}

interface RecoveryCodeFieldProps {
  value: string;
  onChange: (value: string) => void;
  error?: string;
}

function RecoveryCodeField({ value, onChange, error }: RecoveryCodeFieldProps) {
  return (
    <Field data-invalid={!!error}>
      <FieldLabel htmlFor="recovery-code">
        Recovery code
        <span aria-label="required" className="text-destructive">
          *
        </span>
      </FieldLabel>
      <FieldContent>
        <InputGroup aria-invalid={!!error}>
          <InputGroupAddon>
            <ShieldCheck aria-hidden="true" className="size-4" />
          </InputGroupAddon>
          <InputGroupInput
            aria-describedby={error ? "recovery-code-error" : undefined}
            aria-invalid={!!error}
            autoComplete="one-time-code"
            id="recovery-code"
            name="recoveryCode"
            onChange={(e) => onChange(e.target.value)}
            placeholder="Enter recovery code…"
            required
            type="text"
            value={value}
          />
        </InputGroup>
        {error && <FieldError id="recovery-code-error">{error}</FieldError>}
      </FieldContent>
    </Field>
  );
}

interface VerifyRecoveryCodeButtonProps {
  isLoading: boolean;
  disabled: boolean;
}

function VerifyRecoveryCodeButton({
  isLoading,
  disabled,
}: VerifyRecoveryCodeButtonProps) {
  return (
    <Button
      aria-busy={isLoading}
      className="min-h-[44px] w-full touch-manipulation sm:min-h-[32px] sm:w-auto"
      data-loading={isLoading}
      disabled={disabled}
      type="submit"
    >
      {isLoading ? (
        <>
          <Loader2 aria-hidden="true" className="size-4 animate-spin" />
          Verifying…
        </>
      ) : (
        <>
          <ShieldCheck aria-hidden="true" className="size-4" />
          Verify recovery code
        </>
      )}
    </Button>
  );
}

interface SwitchToAuthenticatorButtonProps {
  onSwitch: () => void;
}

function SwitchToAuthenticatorButton({
  onSwitch,
}: SwitchToAuthenticatorButtonProps) {
  return (
    <Button
      className="min-h-[44px] w-full touch-manipulation sm:min-h-[32px] sm:w-auto"
      onClick={onSwitch}
      type="button"
      variant="outline"
    >
      Use authenticator code
    </Button>
  );
}

interface RecoveryCodeFormProps {
  recoveryCode: string;
  onRecoveryCodeChange: (value: string) => void;
  error?: string;
  isLoading: boolean;
  onSubmit: (e: React.FormEvent) => void;
  onSwitchToAuthenticator: () => void;
}

function RecoveryCodeForm({
  recoveryCode,
  onRecoveryCodeChange,
  error,
  isLoading,
  onSubmit,
  onSwitchToAuthenticator,
}: RecoveryCodeFormProps) {
  return (
    <form className="flex flex-col gap-4" onSubmit={onSubmit}>
      <RecoveryCodeField
        error={error}
        onChange={onRecoveryCodeChange}
        value={recoveryCode}
      />
      <div className="flex flex-col gap-2 sm:flex-row">
        <VerifyRecoveryCodeButton
          disabled={isLoading || !recoveryCode.trim()}
          isLoading={isLoading}
        />
        <SwitchToAuthenticatorButton onSwitch={onSwitchToAuthenticator} />
      </div>
    </form>
  );
}

interface ResendButtonProps {
  cooldown: number;
  isLoading: boolean;
  onResend: () => void;
}

function ResendButton({ cooldown, isLoading, onResend }: ResendButtonProps) {
  return (
    <button
      className="min-h-[32px] touch-manipulation self-start rounded-sm hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 sm:self-auto"
      disabled={cooldown > 0 || isLoading}
      onClick={onResend}
      type="button"
    >
      {cooldown > 0 ? (
        `Resend in ${cooldown}s`
      ) : (
        <span className="flex items-center gap-1">
          <RefreshCw aria-hidden="true" className="size-3" />
          Resend code
        </span>
      )}
    </button>
  );
}

interface OTPFieldWithResendProps {
  value: string;
  onChange: (value: string) => void;
  error?: string;
  isLoading: boolean;
  cooldown: number;
  onResend?: () => void;
}

function OTPFieldWithResend({
  value,
  onChange,
  error,
  isLoading,
  cooldown,
  onResend,
}: OTPFieldWithResendProps) {
  return (
    <Field data-invalid={!!error}>
      <FieldLabel htmlFor="2fa-code">
        Authentication code
        <span aria-label="required" className="text-destructive">
          *
        </span>
      </FieldLabel>
      <FieldContent>
        <InputOTP
          aria-describedby={error ? "2fa-code-error" : undefined}
          aria-invalid={!!error}
          disabled={isLoading}
          id="2fa-code"
          maxLength={6}
          onChange={onChange}
          value={value}
        >
          <InputOTPGroup>
            <InputOTPSlot index={0} />
            <InputOTPSlot index={1} />
            <InputOTPSlot index={2} />
            <InputOTPSlot index={3} />
            <InputOTPSlot index={4} />
            <InputOTPSlot index={5} />
          </InputOTPGroup>
        </InputOTP>
        {error && <FieldError id="2fa-code-error">{error}</FieldError>}
        <div className="flex flex-col gap-2 text-muted-foreground text-xs sm:flex-row sm:items-center sm:justify-between">
          <span>Enter the 6-digit code from your app</span>
          {onResend && (
            <ResendButton
              cooldown={cooldown}
              isLoading={isLoading}
              onResend={onResend}
            />
          )}
        </div>
      </FieldContent>
    </Field>
  );
}

function EmailSeparator() {
  return (
    <div className="relative">
      <Separator />
      <div className="absolute inset-0 flex items-center justify-center">
        <span className="bg-card px-2 text-muted-foreground text-xs">Or</span>
      </div>
    </div>
  );
}

interface RecoveryCodeToggleButtonProps {
  onToggle: () => void;
}

function RecoveryCodeToggleButton({ onToggle }: RecoveryCodeToggleButtonProps) {
  return (
    <Button
      className="min-h-[44px] w-full touch-manipulation sm:min-h-[32px]"
      onClick={onToggle}
      type="button"
      variant="ghost"
    >
      Use recovery code instead
    </Button>
  );
}

export interface AuthTwoFactorVerifyProps {
  onSubmit?: (code: string) => void;
  onRecoveryCode?: (code: string) => void;
  onResend?: () => void;
  className?: string;
  isLoading?: boolean;
  resendCooldown?: number; // seconds
  errors?: {
    code?: string;
    recoveryCode?: string;
    general?: string;
  };
  autoSubmit?: boolean;
}

export default function AuthTwoFactorVerify({
  onSubmit,
  onRecoveryCode,
  onResend,
  className,
  isLoading = false,
  resendCooldown = 60,
  errors,
  autoSubmit = true,
}: AuthTwoFactorVerifyProps) {
  const [code, setCode] = useState("");
  const [recoveryCode, setRecoveryCode] = useState("");
  const [useRecoveryCode, setUseRecoveryCode] = useState(false);
  const [cooldown, setCooldown] = useState(0);

  useEffect(() => {
    if (cooldown > 0) {
      const timer = setTimeout(() => {
        setCooldown((prev) => prev - 1);
      }, 1000);
      return () => clearTimeout(timer);
    }
  }, [cooldown]);

  useEffect(() => {
    if (autoSubmit && code.length === 6 && !isLoading && !useRecoveryCode) {
      onSubmit?.(code);
    }
  }, [code, autoSubmit, isLoading, useRecoveryCode, onSubmit]);

  const handleResend = useCallback(async () => {
    if (cooldown > 0) return;
    await onResend?.();
    setCooldown(resendCooldown);
  }, [cooldown, onResend, resendCooldown]);

  const handleRecoveryCodeSubmit = useCallback(
    (e: React.FormEvent) => {
      e.preventDefault();
      if (recoveryCode.trim()) {
        onRecoveryCode?.(recoveryCode.trim());
      }
    },
    [recoveryCode, onRecoveryCode]
  );

  const handleSwitchToAuthenticator = useCallback(() => {
    setUseRecoveryCode(false);
    setRecoveryCode("");
  }, []);

  const handleSwitchToRecoveryCode = useCallback(() => {
    setUseRecoveryCode(true);
  }, []);

  return (
    <Card className={cn("w-full max-w-sm shadow-xs", className)}>
      <CardHeader>
        <Header />
      </CardHeader>
      <CardContent>
        <div className="flex flex-col gap-6">
          {errors?.general && <ErrorAlert message={errors.general} />}

          {useRecoveryCode ? (
            <RecoveryCodeForm
              error={errors?.recoveryCode}
              isLoading={isLoading}
              onRecoveryCodeChange={setRecoveryCode}
              onSubmit={handleRecoveryCodeSubmit}
              onSwitchToAuthenticator={handleSwitchToAuthenticator}
              recoveryCode={recoveryCode}
            />
          ) : (
            <div className="flex flex-col gap-4">
              <OTPFieldWithResend
                cooldown={cooldown}
                error={errors?.code}
                isLoading={isLoading}
                onChange={setCode}
                onResend={onResend}
                value={code}
              />

              {onRecoveryCode && (
                <>
                  <EmailSeparator />
                  <RecoveryCodeToggleButton
                    onToggle={handleSwitchToRecoveryCode}
                  />
                </>
              )}
            </div>
          )}
        </div>
      </CardContent>
    </Card>
  );
}

Similar components