refactor(settings-passkeys): ux improvements
This commit is contained in:
parent
5362151f9f
commit
97c70ea65a
2 changed files with 73 additions and 71 deletions
|
|
@ -51,14 +51,15 @@ export function LoginPage({ error }: LoginPageProps = {}) {
|
||||||
const errorDescription = errorCode ? getLoginErrorDescription(errorCode) : null;
|
const errorDescription = errorCode ? getLoginErrorDescription(errorCode) : null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
|
||||||
!PublicKeyCredential.isConditionalMediationAvailable ||
|
|
||||||
!PublicKeyCredential.isConditionalMediationAvailable()
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const autoSignIn = async () => {
|
const autoSignIn = async () => {
|
||||||
|
if (
|
||||||
|
typeof PublicKeyCredential === "undefined" ||
|
||||||
|
!PublicKeyCredential.isConditionalMediationAvailable ||
|
||||||
|
!(await PublicKeyCredential.isConditionalMediationAvailable())
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await authClient.signIn.passkey({
|
await authClient.signIn.passkey({
|
||||||
autoFill: true,
|
autoFill: true,
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||||
import { Fingerprint, Plus, Trash2, Pencil } from "lucide-react";
|
import { Fingerprint, Plus, Trash2, Pencil } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { Button } from "~/client/components/ui/button";
|
import { Button } from "~/client/components/ui/button";
|
||||||
|
|
@ -27,6 +27,7 @@ import { Label } from "~/client/components/ui/label";
|
||||||
import { authClient } from "~/client/lib/auth-client";
|
import { authClient } from "~/client/lib/auth-client";
|
||||||
import { logger } from "~/client/lib/logger";
|
import { logger } from "~/client/lib/logger";
|
||||||
import { useTimeFormat } from "~/client/lib/datetime";
|
import { useTimeFormat } from "~/client/lib/datetime";
|
||||||
|
import { cn } from "~/client/lib/utils";
|
||||||
|
|
||||||
type PasskeyEntry = {
|
type PasskeyEntry = {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -37,8 +38,16 @@ type PasskeyEntry = {
|
||||||
|
|
||||||
export function PasskeysSection() {
|
export function PasskeysSection() {
|
||||||
const { formatDateTime } = useTimeFormat();
|
const { formatDateTime } = useTimeFormat();
|
||||||
const { data: passkeys, isPending } = authClient.useListPasskeys();
|
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 [addDialogOpen, setAddDialogOpen] = useState(false);
|
||||||
const [newPasskeyName, setNewPasskeyName] = useState("");
|
const [newPasskeyName, setNewPasskeyName] = useState("");
|
||||||
|
|
||||||
|
|
@ -84,12 +93,12 @@ export function PasskeysSection() {
|
||||||
|
|
||||||
const deletePasskeyMutation = useMutation({
|
const deletePasskeyMutation = useMutation({
|
||||||
mutationFn: async (id: string) => {
|
mutationFn: async (id: string) => {
|
||||||
const { error } = await authClient.$fetch("/passkey/delete-passkey", {
|
const { error } = await authClient.passkey.deletePasskey({ id });
|
||||||
method: "POST",
|
|
||||||
body: { id },
|
|
||||||
});
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
},
|
},
|
||||||
|
onMutate: () => {
|
||||||
|
setDeletePasskeyOpen(false);
|
||||||
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Passkey deleted");
|
toast.success("Passkey deleted");
|
||||||
setDeleteTarget(null);
|
setDeleteTarget(null);
|
||||||
|
|
@ -100,13 +109,13 @@ export function PasskeysSection() {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleAddPasskey = (e: React.FormEvent) => {
|
const handleAddPasskey = (e: React.ChangeEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const name = newPasskeyName.trim() || undefined;
|
const name = newPasskeyName.trim() || undefined;
|
||||||
addPasskeyMutation.mutate(name);
|
addPasskeyMutation.mutate(name);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRename = (e: React.FormEvent) => {
|
const handleRename = (e: React.ChangeEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!renameTarget) return;
|
if (!renameTarget) return;
|
||||||
const name = renameValue.trim();
|
const name = renameValue.trim();
|
||||||
|
|
@ -122,8 +131,6 @@ export function PasskeysSection() {
|
||||||
deletePasskeyMutation.mutate(deleteTarget.id);
|
deletePasskeyMutation.mutate(deleteTarget.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
const list = (passkeys ?? []) as PasskeyEntry[];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="border-t border-border/50 bg-card-header p-6">
|
<div className="border-t border-border/50 bg-card-header p-6">
|
||||||
|
|
@ -148,51 +155,52 @@ export function PasskeysSection() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isPending ? (
|
<p className={cn("text-sm text-muted-foreground", { hidden: !isPending })}>Loading passkeys...</p>
|
||||||
<p className="text-sm text-muted-foreground">Loading passkeys...</p>
|
<p className={cn("text-sm text-muted-foreground", { hidden: passkeys && passkeys.length > 0 })}>
|
||||||
) : list.length === 0 ? (
|
No passkeys yet. Add one to enable passwordless sign-in.
|
||||||
<p className="text-sm text-muted-foreground">
|
</p>
|
||||||
No passkeys yet. Add one to enable passwordless sign-in.
|
<ul
|
||||||
</p>
|
className={cn("divide-y divide-border/50 rounded-md border border-border/50", {
|
||||||
) : (
|
hidden: passkeys?.length === 0,
|
||||||
<ul className="divide-y divide-border/50 rounded-md border border-border/50">
|
})}
|
||||||
{list.map((p) => (
|
>
|
||||||
<li key={p.id} className="flex items-center justify-between gap-4 p-3">
|
{passkeys?.map((p) => (
|
||||||
<div className="min-w-0 flex-1">
|
<li key={p.id} className="flex items-center justify-between gap-4 p-3">
|
||||||
<p className="text-sm font-medium truncate">
|
<div className="min-w-0 flex-1">
|
||||||
{p.name?.trim() || "Unnamed passkey"}
|
<p className="text-sm font-medium truncate">{p.name?.trim() || "Unnamed passkey"}</p>
|
||||||
</p>
|
<p className="text-xs text-muted-foreground">
|
||||||
<p className="text-xs text-muted-foreground">
|
Added {formatDateTime(new Date(p.createdAt))}
|
||||||
Added {formatDateTime(new Date(p.createdAt))}
|
</p>
|
||||||
</p>
|
</div>
|
||||||
</div>
|
<div className="flex gap-2">
|
||||||
<div className="flex gap-2">
|
<Button
|
||||||
<Button
|
variant="outline"
|
||||||
variant="outline"
|
size="sm"
|
||||||
size="sm"
|
aria-label={`Rename passkey ${p.name?.trim() || "Unnamed passkey"}`}
|
||||||
aria-label={`Rename passkey ${p.name?.trim() || "Unnamed passkey"}`}
|
title={`Rename passkey ${p.name?.trim() || "Unnamed passkey"}`}
|
||||||
title={`Rename passkey ${p.name?.trim() || "Unnamed passkey"}`}
|
onClick={() => {
|
||||||
onClick={() => {
|
setRenameTarget(p);
|
||||||
setRenameTarget(p);
|
setRenameValue(p.name ?? "");
|
||||||
setRenameValue(p.name ?? "");
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Pencil className="h-4 w-4" />
|
||||||
<Pencil className="h-4 w-4" />
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
variant="destructive"
|
||||||
variant="destructive"
|
size="sm"
|
||||||
size="sm"
|
aria-label={`Delete passkey ${p.name?.trim() || "Unnamed passkey"}`}
|
||||||
aria-label={`Delete passkey ${p.name?.trim() || "Unnamed passkey"}`}
|
title={`Delete passkey ${p.name?.trim() || "Unnamed passkey"}`}
|
||||||
title={`Delete passkey ${p.name?.trim() || "Unnamed passkey"}`}
|
onClick={() => {
|
||||||
onClick={() => setDeleteTarget(p)}
|
setDeleteTarget(p);
|
||||||
>
|
setDeletePasskeyOpen(true);
|
||||||
<Trash2 className="h-4 w-4" />
|
}}
|
||||||
</Button>
|
>
|
||||||
</div>
|
<Trash2 className="h-4 w-4" />
|
||||||
</li>
|
</Button>
|
||||||
))}
|
</div>
|
||||||
</ul>
|
</li>
|
||||||
)}
|
))}
|
||||||
|
</ul>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|
@ -218,7 +226,6 @@ export function PasskeysSection() {
|
||||||
value={newPasskeyName}
|
value={newPasskeyName}
|
||||||
onChange={(e) => setNewPasskeyName(e.target.value)}
|
onChange={(e) => setNewPasskeyName(e.target.value)}
|
||||||
placeholder="My Laptop"
|
placeholder="My Laptop"
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -257,7 +264,6 @@ export function PasskeysSection() {
|
||||||
id="passkey-rename"
|
id="passkey-rename"
|
||||||
value={renameValue}
|
value={renameValue}
|
||||||
onChange={(e) => setRenameValue(e.target.value)}
|
onChange={(e) => setRenameValue(e.target.value)}
|
||||||
autoFocus
|
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -274,12 +280,7 @@ export function PasskeysSection() {
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<AlertDialog
|
<AlertDialog open={deletePasskeyOpen} onOpenChange={setDeletePasskeyOpen}>
|
||||||
open={Boolean(deleteTarget)}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
if (!open) setDeleteTarget(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
<AlertDialogTitle>Delete passkey?</AlertDialogTitle>
|
<AlertDialogTitle>Delete passkey?</AlertDialogTitle>
|
||||||
|
|
@ -293,7 +294,7 @@ export function PasskeysSection() {
|
||||||
<AlertDialogAction
|
<AlertDialogAction
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
void handleDelete();
|
handleDelete();
|
||||||
}}
|
}}
|
||||||
disabled={deletePasskeyMutation.isPending}
|
disabled={deletePasskeyMutation.isPending}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue