import { useState } from "react"; import { Fingerprint, Plus, Trash2, Pencil } from "lucide-react"; import { toast } from "sonner"; import { Button } from "~/client/components/ui/button"; import { CardContent, CardDescription, CardTitle } from "~/client/components/ui/card"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "~/client/components/ui/alert-dialog"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/client/components/ui/dialog"; import { Input } from "~/client/components/ui/input"; import { Label } from "~/client/components/ui/label"; import { authClient } from "~/client/lib/auth-client"; import { logger } from "~/client/lib/logger"; import { useTimeFormat } from "~/client/lib/datetime"; type PasskeyEntry = { id: string; name?: string | null; createdAt: Date | string; deviceType?: string; }; export function PasskeysSection() { const { formatDateTime } = useTimeFormat(); const { data: passkeys, isPending, refetch } = authClient.useListPasskeys(); const [addDialogOpen, setAddDialogOpen] = useState(false); const [newPasskeyName, setNewPasskeyName] = useState(""); const [isAdding, setIsAdding] = useState(false); const [renameTarget, setRenameTarget] = useState(null); const [renameValue, setRenameValue] = useState(""); const [isRenaming, setIsRenaming] = useState(false); const [deleteTarget, setDeleteTarget] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const handleAddPasskey = async (e: React.FormEvent) => { e.preventDefault(); setIsAdding(true); try { const { error } = await authClient.passkey.addPasskey({ name: newPasskeyName.trim() || undefined, }); if (error) { logger.error(error); toast.error("Failed to add passkey", { description: error.message }); return; } toast.success("Passkey added"); setAddDialogOpen(false); setNewPasskeyName(""); await refetch(); } catch (err) { logger.error(err); toast.error("Failed to add passkey", { description: err instanceof Error ? err.message : "Unknown error", }); } finally { setIsAdding(false); } }; const handleRename = async (e: React.FormEvent) => { e.preventDefault(); if (!renameTarget) return; const name = renameValue.trim(); if (!name) { toast.error("Name is required"); return; } setIsRenaming(true); try { const { error } = await authClient.$fetch("/passkey/update-passkey", { method: "POST", body: { id: renameTarget.id, name }, }); if (error) { logger.error(error); toast.error("Failed to rename passkey", { description: error.message }); return; } toast.success("Passkey renamed"); setRenameTarget(null); setRenameValue(""); await refetch(); } catch (err) { logger.error(err); toast.error("Failed to rename passkey", { description: err instanceof Error ? err.message : "Unknown error", }); } finally { setIsRenaming(false); } }; const handleDelete = async () => { if (!deleteTarget) return; setIsDeleting(true); try { const { error } = await authClient.$fetch("/passkey/delete-passkey", { method: "POST", body: { id: deleteTarget.id }, }); if (error) { logger.error(error); toast.error("Failed to delete passkey", { description: error.message }); return; } toast.success("Passkey deleted"); setDeleteTarget(null); await refetch(); } catch (err) { logger.error(err); toast.error("Failed to delete passkey", { description: err instanceof Error ? err.message : "Unknown error", }); } finally { setIsDeleting(false); } }; const list = (passkeys ?? []) as PasskeyEntry[]; return ( <>
Passkeys Sign in faster and more securely with passkeys stored on your device or password manager. You can add more than one.

Passkeys use your device's biometrics or screen lock instead of a password. They are phishing-resistant and cannot be reused across sites.

{isPending ? (

Loading passkeys...

) : list.length === 0 ? (

No passkeys yet. Add one to enable passwordless sign-in.

) : (
    {list.map((p) => (
  • {p.name?.trim() || "Unnamed passkey"}

    Added {formatDateTime(new Date(p.createdAt))}

  • ))}
)}
{ setAddDialogOpen(open); if (!open) setNewPasskeyName(""); }} >
Add a passkey Give this passkey a name so you can recognize it later (e.g. "MacBook Touch ID").
setNewPasskeyName(e.target.value)} placeholder="My Laptop" autoFocus />
{ if (!open) { setRenameTarget(null); setRenameValue(""); } }} >
Rename passkey Choose a name to recognize this passkey.
setRenameValue(e.target.value)} autoFocus required />
{ if (!open) setDeleteTarget(null); }} > Delete passkey? This will remove "{deleteTarget?.name?.trim() || "this passkey"}" from your account. You won't be able to use it to sign in anymore. Cancel { e.preventDefault(); void handleDelete(); }} disabled={isDeleting} > Delete ); }