import { useState } from '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 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.

); } export function PasskeySection() { const { toast } = useToast(); const { data: passkeys, isLoading } = useAccountPasskeyList(); const renamePasskey = useAccountRenamePasskey(); const deletePasskey = useAccountDeletePasskey(); const [editingId, setEditingId] = useState(null); const [editName, setEditName] = useState(''); const [confirmDeleteId, setConfirmDeleteId] = useState(null); 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 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.

{credentials.length === 0 ? (

No passkeys registered. Passkeys can be registered during sign-in when prompted.

) : (
{credentials.map((pk) => (
{editingId === pk.id ? (
setEditName(e.target.value)} placeholder="Passkey name" style={{ maxWidth: 200 }} />
) : ( <>
{pk.name || 'Unnamed passkey'}
{parseAgent(pk.agent)} · Added {pk.createdAt ? new Date(pk.createdAt).toLocaleDateString() : 'unknown'}
)}
{editingId !== pk.id && (
{confirmDeleteId === pk.id ? ( <> ) : ( )}
)}
))}
)}
); }