From c5ea75cecc56738a61888553ecfbacdd62950062 Mon Sep 17 00:00:00 2001 From: Nico Date: Mon, 4 May 2026 11:31:19 +0200 Subject: [PATCH] refactor(passkeys): use TanStack mutations for passkey CRUD operations --- .../settings/components/passkeys-section.tsx | 162 +++++++++--------- 1 file changed, 78 insertions(+), 84 deletions(-) diff --git a/app/client/modules/settings/components/passkeys-section.tsx b/app/client/modules/settings/components/passkeys-section.tsx index 24a58f54..1a4f3938 100644 --- a/app/client/modules/settings/components/passkeys-section.tsx +++ b/app/client/modules/settings/components/passkeys-section.tsx @@ -1,4 +1,5 @@ import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; import { Fingerprint, Plus, Trash2, Pencil } from "lucide-react"; import { toast } from "sonner"; import { Button } from "~/client/components/ui/button"; @@ -36,46 +37,76 @@ type PasskeyEntry = { export function PasskeysSection() { const { formatDateTime } = useTimeFormat(); - const { data: passkeys, isPending, refetch } = authClient.useListPasskeys(); + const { data: passkeys, isPending } = 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; - } + 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(""); - await refetch(); - } catch (err) { - logger.error(err); - toast.error("Failed to add passkey", { - description: err instanceof Error ? err.message : "Unknown error", + }, + 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 }, }); - } finally { - setIsAdding(false); - } + 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.$fetch("/passkey/delete-passkey", { + method: "POST", + body: { id }, + }); + if (error) throw error; + }, + 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.FormEvent) => { + e.preventDefault(); + const name = newPasskeyName.trim() || undefined; + addPasskeyMutation.mutate(name); }; - const handleRename = async (e: React.FormEvent) => { + const handleRename = (e: React.FormEvent) => { e.preventDefault(); if (!renameTarget) return; const name = renameValue.trim(); @@ -83,55 +114,12 @@ export function PasskeysSection() { 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); - } + renamePasskeyMutation.mutate({ id: renameTarget.id, name }); }; - const handleDelete = async () => { + const handleDelete = () => { 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); - } + deletePasskeyMutation.mutate(deleteTarget.id); }; const list = (passkeys ?? []) as PasskeyEntry[]; @@ -144,15 +132,15 @@ export function PasskeysSection() { Passkeys - Sign in faster and more securely with passkeys stored on your device or password manager. You can add more - than one. + 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. + Passkeys use your device's biometrics or screen lock instead of a password. They are + phishing-resistant and cannot be reused across sites.

- @@ -272,7 +266,7 @@ export function PasskeysSection() { - @@ -290,18 +284,18 @@ export function PasskeysSection() { 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. + This will remove "{deleteTarget?.name?.trim() || "this passkey"}" from your account. You + won't be able to use it to sign in anymore. - Cancel + Cancel { e.preventDefault(); void handleDelete(); }} - disabled={isDeleting} + disabled={deletePasskeyMutation.isPending} > Delete