import { useQuery } from '@tanstack/react-query'; import { KeyRoundIcon, LockIcon } from 'lucide-react'; import { Button } from '#/components/atoms/button.jsx'; import { Panel } from '#/components/atoms/panel.jsx'; import { RowIcon } from '#/components/atoms/row-icon.jsx'; import { ErrorNotice } from '#/components/molecules/error-notice.jsx'; import { ListRow } from '#/components/molecules/list-row.jsx'; import { Section } from '#/components/molecules/section.jsx'; import { get, post } from '#/lib/api.js'; import { b64, unb64 } from '#/lib/base64.js'; import { day } from '#/lib/format.js'; import { queryKeys, useApiMutation } from '#/lib/query.js'; export function SignInMethods({ did }) { const { data, error } = useQuery({ queryKey: queryKeys.passkeys, queryFn: () => get('/passkeys'), }); const action = useApiMutation([queryKeys.passkeys]); const add = (name) => action.run(async () => { if (!window.PublicKeyCredential) { throw new Error('This browser has no passkey support.'); } const credential = await navigator.credentials.create({ publicKey: { challenge: unb64(data.challenge), rp: { id: data.rpId, name: window.location.host }, user: { id: new TextEncoder().encode(did), name: data.accountName, displayName: data.accountName, }, pubKeyCredParams: [ { type: 'public-key', alg: -7 }, { type: 'public-key', alg: -257 }, ], authenticatorSelection: { residentKey: 'preferred', userVerification: 'preferred', }, excludeCredentials: data.passkeys.map((passkey) => ({ type: 'public-key', id: unb64(passkey.credentialId), })), timeout: 120000, }, }); const response = await fetch('/account/passkeys/register', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, credentialId: b64(credential.rawId), publicKey: b64(credential.response.getPublicKey()), algorithm: credential.response.getPublicKeyAlgorithm(), clientDataJSON: b64(credential.response.clientDataJSON), }), }); const body = await response.json(); if (!response.ok) throw new Error(body.error); }); const remove = (credentialId) => action.run(async () => { await post('/passkeys/revoke', { credentialId }); }); return ( <>
{data?.passkeys?.map((passkey) => ( } title={passkey.name} description={`Passkey · ${ passkey.lastUsedAt ? `used ${day(passkey.lastUsedAt)}` : `added ${day(passkey.createdAt)}` }`} actions={ } /> ))} } title="Account password" description="Backup way to sign in" />
); }