"use client"; import { RiAddLine, RiAlertLine, RiDeleteBinLine, RiFingerprintLine, RiLoader4Line, RiPencilLine, } from "@remixicon/react"; import { formatDistanceToNow } from "date-fns"; import { ptBR } from "date-fns/locale"; import { useCallback, useEffect, useState } from "react"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { authClient } from "@/lib/auth/client"; interface Passkey { id: string; name: string | null; deviceType: string; createdAt: Date | null; } export function PasskeysForm() { const [passkeys, setPasskeys] = useState([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [passkeySupported, setPasskeySupported] = useState(false); // Add passkey const [isAddOpen, setIsAddOpen] = useState(false); const [addName, setAddName] = useState(""); const [isAdding, setIsAdding] = useState(false); // Rename passkey const [editingId, setEditingId] = useState(null); const [editName, setEditName] = useState(""); const [isRenaming, setIsRenaming] = useState(false); // Delete passkey const [deleteId, setDeleteId] = useState(null); const [isDeleting, setIsDeleting] = useState(false); const isMutating = isAdding || isRenaming || isDeleting; const fetchPasskeys = useCallback( async (options?: { showLoader?: boolean }) => { const showLoader = options?.showLoader ?? true; if (showLoader) setIsLoading(true); setError(null); try { const { data, error: fetchError } = await authClient.passkey.listUserPasskeys(); if (fetchError) { setError(fetchError.message || "Erro ao carregar passkeys."); return; } setPasskeys( (data ?? []).map((p) => ({ id: p.id, name: p.name, deviceType: p.deviceType, createdAt: p.createdAt ? new Date(p.createdAt) : null, })), ); } catch { setError("Erro ao carregar passkeys."); } finally { if (showLoader) setIsLoading(false); } }, [], ); useEffect(() => { if (typeof window === "undefined") return; setPasskeySupported(typeof PublicKeyCredential !== "undefined"); fetchPasskeys(); }, [fetchPasskeys]); const handleAdd = async () => { if (!passkeySupported) { setError("Passkeys não são suportadas neste navegador/dispositivo."); return; } setIsAdding(true); setError(null); try { const { error: addError } = await authClient.passkey.addPasskey({ name: addName.trim() || undefined, }); if (addError) { setError(addError.message || "Erro ao registrar passkey."); return; } setAddName(""); setIsAddOpen(false); await fetchPasskeys({ showLoader: false }); } catch { setError("Erro ao registrar passkey."); } finally { setIsAdding(false); } }; const handleRename = async (id: string) => { if (!editName.trim()) return; setIsRenaming(true); setError(null); try { const { error: renameError } = await authClient.passkey.updatePasskey({ id, name: editName.trim(), }); if (renameError) { setError(renameError.message || "Erro ao renomear passkey."); return; } setEditingId(null); setEditName(""); await fetchPasskeys({ showLoader: false }); } catch { setError("Erro ao renomear passkey."); } finally { setIsRenaming(false); } }; const handleDelete = async () => { if (!deleteId) return; setIsDeleting(true); setError(null); try { const { error: deleteError } = await authClient.passkey.deletePasskey({ id: deleteId, }); if (deleteError) { setError(deleteError.message || "Erro ao remover passkey."); return; } setDeleteId(null); await fetchPasskeys({ showLoader: false }); } catch { setError("Erro ao remover passkey."); } finally { setIsDeleting(false); } }; const startEditing = (passkey: Passkey) => { setEditingId(passkey.id); setEditName(passkey.name || ""); }; const cancelEditing = () => { setEditingId(null); setEditName(""); }; const deviceTypeLabel = (type: string) => { switch (type) { case "singleDevice": return "Dispositivo único"; case "multiDevice": return "Multi-dispositivo"; default: return type; } }; return (

Suas passkeys

Gerencie suas passkeys para login sem senha.

{ if (!open) { setAddName(""); setError(null); } setIsAddOpen(open); }} > Registrar Passkey Dê um nome para identificar esta passkey (opcional). Em seguida, seu navegador solicitará a confirmação biométrica.
setAddName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); void handleAdd(); } }} disabled={isAdding} />
{error && (
{error}
)}
{error && !isAddOpen && (
{error}
)} {!passkeySupported && !isLoading && (
Este navegador/dispositivo não suporta passkeys.
)} {isLoading ? (
) : passkeys.length === 0 ? (

Nenhuma passkey cadastrada. Adicione uma para login sem senha.

) : (
{passkeys.map((pk) => (
{editingId === pk.id ? (
setEditName(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") handleRename(pk.id); if (e.key === "Escape") cancelEditing(); }} autoFocus disabled={isRenaming} />
) : ( <>
{pk.name || "Passkey sem nome"}

{deviceTypeLabel(pk.deviceType)} {pk.createdAt ? ` · Criada ${formatDistanceToNow(pk.createdAt, { addSuffix: true, locale: ptBR, })}` : " · Data de criação indisponível"}

)}
{editingId !== pk.id && ( )}
))}
)} {/* Delete Confirmation Dialog */} !open && setDeleteId(null)} > Remover passkey? Esta passkey não poderá mais ser usada para login. Esta ação não pode ser desfeita. Cancelar {isDeleting ? "Removendo..." : "Remover"}
); }