import { useState, useCallback } from 'react'; import { isWebAuthnAvailable, preparePublicKeyCreationOptions, serializeRegistrationCredential, } from '../utils/webauthn'; const usePasskeys = ({ api, notifyApiError, setStatusMessage, token }) => { const [passkeys, setPasskeys] = useState([]); const [passkeysSupported, setPasskeysSupported] = useState(null); const [passkeysLoading, setPasskeysLoading] = useState(false); const [registeringPasskey, setRegisteringPasskey] = useState(false); const [revokingPasskeyId, setRevokingPasskeyId] = useState(null); const refreshPasskeys = useCallback(async () => { if (!token) { return; } setPasskeysLoading(true); try { const { data } = await api.get('/profile/passkeys'); setPasskeys(Array.isArray(data) ? data : []); setPasskeysSupported(true); } catch (error) { const status = error?.response?.status; if (status === 400 || status === 404) { setPasskeysSupported(false); setPasskeys([]); } else { notifyApiError(error, 'Failed to load passkeys.'); } } finally { setPasskeysLoading(false); } }, [api, notifyApiError, token]); const registerPasskey = useCallback( async ({ nickname } = {}) => { if (!isWebAuthnAvailable()) { setPasskeysSupported(false); setStatusMessage('Passkeys are not supported in this browser.', 'error'); return { ok: false, reason: 'unsupported' }; } if (registeringPasskey) { return { ok: false, reason: 'busy' }; } setRegisteringPasskey(true); try { const { data } = await api.post('/auth/passkeys/register/start', {}); const challengeId = data?.challengeId || data?.challenge_id; const publicKeyOptions = data?.publicKey || data?.public_key || data?.challenge?.publicKey || data?.publicKeyCredentialCreationOptions; if (!challengeId || !publicKeyOptions) { throw new Error('Invalid passkey challenge response.'); } const publicKey = preparePublicKeyCreationOptions({ publicKey: publicKeyOptions }); const credential = await navigator.credentials.create({ publicKey }); if (!credential) { return { ok: false, reason: 'cancelled' }; } const serialized = serializeRegistrationCredential(credential); const payload = { challengeId, credential: serialized, }; const trimmedNickname = nickname?.trim(); if (trimmedNickname) { payload.nickname = trimmedNickname; } await api.post('/auth/passkeys/register/finish', payload); await refreshPasskeys(); setPasskeysSupported(true); setStatusMessage('Passkey registered.', 'success'); return { ok: true }; } catch (error) { if (error?.name === 'NotAllowedError') { setStatusMessage('Passkey registration cancelled.', 'info'); return { ok: false, reason: 'cancelled' }; } const status = error?.response?.status; if (status === 400 || status === 404) { setPasskeysSupported(false); } const message = error?.response?.data?.error || 'Failed to register passkey.'; notifyApiError(error, message); return { ok: false, reason: 'error', message }; } finally { setRegisteringPasskey(false); } }, [api, notifyApiError, refreshPasskeys, registeringPasskey, setStatusMessage], ); const revokePasskey = useCallback( async (passkeyId, reason) => { if (!passkeyId) { return { ok: false, reason: 'missing-id' }; } setRevokingPasskeyId(passkeyId); try { const query = reason ? `?reason=${encodeURIComponent(reason)}` : ''; await api.delete(`/profile/passkeys/${passkeyId}${query}`); await refreshPasskeys(); setStatusMessage('Passkey revoked.', 'success'); return { ok: true }; } catch (error) { const message = error?.response?.data?.error || 'Failed to revoke passkey.'; notifyApiError(error, message); return { ok: false, reason: 'error', message }; } finally { setRevokingPasskeyId(null); } }, [api, notifyApiError, refreshPasskeys, setStatusMessage], ); return { passkeys, passkeysSupported, passkeysLoading, registeringPasskey, revokingPasskeyId, refreshPasskeys, registerPasskey, revokePasskey, }; }; export default usePasskeys;