import { useState } from "react"; import { useMutation, useQuery } from "@tanstack/react-query"; 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"; import { cn } from "~/client/lib/utils"; type PasskeyEntry = { id: string; name?: string | null; createdAt: Date | string; deviceType?: string; }; export function PasskeysSection() { const { formatDateTime } = useTimeFormat(); const { data: passkeys, isPending } = useQuery({ queryKey: ["passkeys"], queryFn: async () => { const { data, error } = await authClient.passkey.listUserPasskeys(); if (error) throw error; return data; }, }); const [deletePasskeyOpen, setDeletePasskeyOpen] = useState(false); const [addDialogOpen, setAddDialogOpen] = useState(false); const [newPasskeyName, setNewPasskeyName] = useState(""); const [renameTarget, setRenameTarget] = useState(null); const [renameValue, setRenameValue] = useState(""); const [deleteTarget, setDeleteTarget] = useState(null); const addPasskeyMutation = useMutation({ mutationFn: async (name: string | undefined) => { const { error } = await authClient.passkey.addPasskey({ name }); if (error) throw error; }, onSuccess: () => { toast.success("Passkey added"); setAddDialogOpen(false); setNewPasskeyName(""); }, onError: (error: Error) => { logger.error(error); toast.error("Failed to add passkey", { description: error.message }); }, }); const renamePasskeyMutation = useMutation({ mutationFn: async ({ id, name }: { id: string; name: string }) => { const { error } = await authClient.$fetch("/passkey/update-passkey", { method: "POST", body: { id, name }, }); if (error) throw error; }, onSuccess: () => { toast.success("Passkey renamed"); setRenameTarget(null); setRenameValue(""); }, onError: (error: Error) => { logger.error(error); toast.error("Failed to rename passkey", { description: error.message }); }, }); const deletePasskeyMutation = useMutation({ mutationFn: async (id: string) => { const { error } = await authClient.passkey.deletePasskey({ id }); if (error) throw error; }, onMutate: () => { setDeletePasskeyOpen(false); }, onSuccess: () => { toast.success("Passkey deleted"); setDeleteTarget(null); }, onError: (error: Error) => { logger.error(error); toast.error("Failed to delete passkey", { description: error.message }); }, }); const handleAddPasskey = (e: React.ChangeEvent) => { e.preventDefault(); const name = newPasskeyName.trim() || undefined; addPasskeyMutation.mutate(name); }; const handleRename = (e: React.ChangeEvent) => { e.preventDefault(); if (!renameTarget) return; const name = renameValue.trim(); if (!name) { toast.error("Name is required"); return; } renamePasskeyMutation.mutate({ id: renameTarget.id, name }); }; const handleDelete = () => { if (!deleteTarget) return; deletePasskeyMutation.mutate(deleteTarget.id); }; 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.

Loading passkeys...

0 })}> No passkeys yet. Add one to enable passwordless sign-in.

    {passkeys?.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" />
{ if (!open) { setRenameTarget(null); setRenameValue(""); } }} >
Rename passkey Choose a name to recognize this passkey.
setRenameValue(e.target.value)} required />
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(); handleDelete(); }} disabled={deletePasskeyMutation.isPending} > Delete ); }