import { useState } from 'react'; import { useLogto } from '@logto/react'; import { errorMessage } from '../../api/client'; import { Alert, Button, Card, Input, useToast, } from '@cameleer/design-system'; import { useAccountMfaStatus, useAccountPasskeyList, useAccountRenamePasskey, useAccountDeletePasskey, } from '../../api/account-hooks'; import { registerPasskey } from '../../api/logto-account-api'; import styles from '../../styles/platform.module.css'; export function PasskeyNudgeBanner() { const { data: status } = useAccountMfaStatus(); const [dismissed, setDismissed] = useState(false); const lastDismissed = localStorage.getItem('passkey_nudge_dismissed'); const recentlyDismissed = lastDismissed && (Date.now() - Number(lastDismissed)) < 30 * 24 * 60 * 60 * 1000; if (dismissed || recentlyDismissed || !status || status.passkeyEnrolled) return null; function handleDismiss() { localStorage.setItem('passkey_nudge_dismissed', String(Date.now())); setDismissed(true); } return ( Use your fingerprint, face, or security key instead of typing a code every time. Not now ); } export function PasskeySection() { const { toast } = useToast(); const { getAccessToken } = useLogto(); const { data: passkeys, isLoading, refetch } = useAccountPasskeyList(); const renamePasskey = useAccountRenamePasskey(); const deletePasskey = useAccountDeletePasskey(); const [editingId, setEditingId] = useState(null); const [editName, setEditName] = useState(''); const [confirmDeleteId, setConfirmDeleteId] = useState(null); const [registering, setRegistering] = useState(false); function parseAgent(agent: string | null): string { if (!agent) return 'Unknown device'; if (agent.includes('Chrome')) return agent.includes('Windows') ? 'Chrome on Windows' : agent.includes('Mac') ? 'Chrome on macOS' : agent.includes('Android') ? 'Chrome on Android' : 'Chrome'; if (agent.includes('Safari') && !agent.includes('Chrome')) return agent.includes('iPhone') ? 'Safari on iPhone' : 'Safari on macOS'; if (agent.includes('Firefox')) return 'Firefox'; if (agent.includes('Edge')) return 'Edge'; return 'Browser'; } function startRename(id: string, currentName: string | null) { setEditingId(id); setEditName(currentName ?? ''); } async function handleRename(id: string) { try { await renamePasskey.mutateAsync({ id, name: editName }); setEditingId(null); toast({ title: 'Passkey renamed', variant: 'success' }); } catch (err) { toast({ title: 'Failed to rename passkey', description: errorMessage(err), variant: 'error' }); } } async function handleRegister() { setRegistering(true); try { await registerPasskey(async () => { const token = await getAccessToken(); if (!token) throw new Error('Not authenticated'); return token; }); await refetch(); toast({ title: 'Passkey registered', variant: 'success' }); } catch (err) { // User cancelled the WebAuthn prompt — not an error if (err instanceof Error && err.name === 'NotAllowedError') return; toast({ title: 'Passkey registration failed', description: errorMessage(err), variant: 'error' }); } finally { setRegistering(false); } } async function handleDelete(id: string) { try { await deletePasskey.mutateAsync(id); setConfirmDeleteId(null); toast({ title: 'Passkey removed', variant: 'success' }); } catch (err) { toast({ title: 'Failed to remove passkey', description: errorMessage(err), variant: 'error' }); } } if (isLoading) return null; const credentials = passkeys ?? []; return ( Use your fingerprint, face, or security key to sign in faster. Register passkey {credentials.length === 0 ? ( No passkeys registered yet. ) : ( {credentials.map((pk) => ( {editingId === pk.id ? ( setEditName(e.target.value)} placeholder="Passkey name" style={{ maxWidth: 200 }} /> handleRename(pk.id)} loading={renamePasskey.isPending}>Save setEditingId(null)}>Cancel ) : ( <> {pk.name || 'Unnamed passkey'} {parseAgent(pk.agent)} · Added {pk.createdAt ? new Date(pk.createdAt).toLocaleDateString() : 'unknown'} > )} {editingId !== pk.id && ( startRename(pk.id, pk.name)}>Rename {confirmDeleteId === pk.id ? ( <> handleDelete(pk.id)} loading={deletePasskey.isPending}>Confirm setConfirmDeleteId(null)}>Cancel > ) : ( setConfirmDeleteId(pk.id)}>Remove )} )} ))} )} ); }
Use your fingerprint, face, or security key instead of typing a code every time.
Use your fingerprint, face, or security key to sign in faster.
No passkeys registered yet.