import { useState } from "react"; import { Fingerprint, Loader2, CheckCircle, AlertCircle } from "lucide-react"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { api, ApiError } from "@/lib/api"; import { isWebAuthnSupported, createRegistrationCredential, formatRegistrationCredential, WebAuthnRegistrationOptions, } from "@/lib/webauthn"; interface AddPasskeyWizardProps { open: boolean; onOpenChange: (open: boolean) => void; onSuccess?: (passkey: { id: string; name: string }) => void; } type WizardStep = "name" | "registering" | "success" | "error"; export function AddPasskeyWizard({ open, onOpenChange, onSuccess }: AddPasskeyWizardProps) { const [step, setStep] = useState("name"); const [passkeyName, setPasskeyName] = useState(""); const [error, setError] = useState(null); const [credentialId, setCredentialId] = useState(null); const handleStartRegistration = async () => { if (!passkeyName.trim()) return; if (!isWebAuthnSupported()) { setError("WebAuthn is not supported in this browser. Please use a modern browser."); setStep("error"); return; } setStep("registering"); setError(null); try { // Step 1: Get registration options from server const options = await api.webauthn.beginRegistration() as unknown as WebAuthnRegistrationOptions; // Step 2: Create credential using browser WebAuthn API const credential = await createRegistrationCredential(options); // Step 3: Format and send credential to server with name const formattedCredential = formatRegistrationCredential(credential); const result = await api.webauthn.completeRegistration(formattedCredential, passkeyName.trim()); setCredentialId(result.credential_id); setStep("success"); // Notify parent after a short delay setTimeout(() => { onSuccess?.({ id: result.credential_id, name: passkeyName.trim() }); }, 1500); } catch (err) { console.error("Passkey registration failed:", err); if (err instanceof ApiError) { setError(err.message); } else if (err instanceof DOMException) { // Handle WebAuthn-specific errors switch (err.name) { case "NotAllowedError": setError("Registration was cancelled or timed out. Please try again."); break; case "InvalidStateError": setError("This authenticator is already registered."); break; case "NotSupportedError": setError("Your device doesn't support the required authentication method."); break; default: setError(err.message || "Failed to register passkey"); } } else { setError(err instanceof Error ? err.message : "Failed to register passkey"); } setStep("error"); } }; const handleClose = () => { onOpenChange(false); // Reset state after dialog closes setTimeout(() => { setStep("name"); setPasskeyName(""); setError(null); setCredentialId(null); }, 200); }; const handleRetry = () => { setStep("name"); setError(null); }; return ( Add Passkey {step === "name" && "Register a passkey for passwordless sign-in"} {step === "registering" && "Follow your browser's prompts to register"} {step === "success" && "Passkey registered successfully"} {step === "error" && "Registration failed"}
{step === "name" && (
setPasskeyName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && passkeyName.trim()) { handleStartRegistration(); } }} />

Give this passkey a name to help you identify it later

What happens next

  • • Your browser will prompt you to authenticate
  • • Use Touch ID, Face ID, or your security key
  • • The passkey will be stored on this device
)} {step === "registering" && (

Waiting for authentication

Follow the prompts from your browser or device

)} {step === "success" && (

Passkey added

"{passkeyName}" is now registered and ready to use

)} {step === "error" && (

Registration failed

{error || "Unable to register passkey. Please try again."}

)}
); }