import { LoaderCircleIcon } from 'lucide-react'; import { useEffect, useRef, useState } from 'react'; import { AppTile } from '#/components/atoms/app-tile.jsx'; import { Avatar } from '#/components/atoms/avatar.jsx'; import { Button } from '#/components/atoms/button.jsx'; import { Panel } from '#/components/atoms/panel.jsx'; import { ErrorNotice } from '#/components/molecules/error-notice.jsx'; import { ScopeSummary } from '#/components/molecules/scope-summary.jsx'; import { TextField } from '#/components/molecules/text-field.jsx'; import { b64, unb64 } from '#/lib/base64.js'; import { clientHost } from '#/lib/format.js'; export function ConsentPage({ clientName, clientId, requestUri, loginHint = '', error = '', permissions = {}, passkey, }) { const [err, setErr] = useState(error); const [busy, setBusy] = useState(false); const [profile, setProfile] = useState(null); const [redirect, setRedirect] = useState(null); const approveButton = useRef(null); useEffect(() => { if (!loginHint) return; let live = true; fetch( `https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=${encodeURIComponent(loginHint)}`, ) .then((response) => (response.ok ? response.json() : null)) .then((data) => { if (live && data) setProfile(data); }) .catch(() => {}); return () => { live = false; }; }, [loginHint]); const identityOnly = !permissions.hasTransitionGeneric && !permissions.hasEmail && !permissions.repo?.length && !permissions.rpc?.length && !permissions.includes?.length && !permissions.blobs?.length && !permissions.spaces?.length; const handle = loginHint.startsWith('did:') ? loginHint : `@${loginHint}`; const initial = loginHint.startsWith('did:') ? '?' : (loginHint[0] || '?').toUpperCase(); // The client's page can take seconds to load. Until it does, the redirect // screen shows the user that the request already has its answer. const returnToClient = (url, action) => { setRedirect({ url, action }); window.location.href = url; }; const submit = async (event) => { event.preventDefault(); const form = event.currentTarget; const action = event.nativeEvent.submitter?.value === 'deny' ? 'deny' : 'approve'; const body = new URLSearchParams(new FormData(form)); body.set('action', action); setErr(''); setBusy(true); try { const response = await fetch('/oauth/authorize', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json', }, body, }); const result = await response.json().catch(() => ({})); if (!response.ok || !result.redirect) throw new Error(result.error || 'That authorization failed.'); returnToClient(result.redirect, action); } catch (caught) { setErr(String(caught?.message || caught)); setBusy(false); } }; const passkeyApprove = async () => { setErr(''); setBusy(true); try { if (!window.PublicKeyCredential) { throw new Error('This browser has no passkey support.'); } const assertion = await navigator.credentials.get({ publicKey: { challenge: unb64(passkey.challenge), rpId: passkey.rpId, userVerification: 'preferred', timeout: 120000, }, }); const response = await fetch('/oauth/authorize/passkey', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ request_uri: requestUri, client_id: clientId, credentialId: b64(assertion.rawId), authenticatorData: b64(assertion.response.authenticatorData), clientDataJSON: b64(assertion.response.clientDataJSON), signature: b64(assertion.response.signature), }), }); const result = await response.json(); if (!response.ok) throw new Error(result.error || 'That passkey was refused.'); returnToClient(result.redirect, 'approve'); } catch (caught) { setErr( caught?.name === 'NotAllowedError' ? 'Cancelled.' : String(caught?.message || caught), ); setBusy(false); } }; if (redirect) { return ( ); } return (
{loginHint && (
{initial}} />
{profile?.displayName || profile?.handle || 'Loading…'}
{profile?.handle ? `@${profile.handle}` : handle}
)}
{clientName}
{clientHost(clientId)}

{identityOnly ? 'This app wants to uniquely identify you through your account.' : 'This app wants to access your account.'}

{ if (event.key !== 'Enter') return; event.preventDefault(); event.currentTarget.form?.requestSubmit(approveButton.current); }} // biome-ignore lint/a11y/noAutofocus: the only field on a dedicated authorize screen autoFocus />
{passkey && ( )}
); } /** * The screen between an answered request and the client's own page. The * browser is already navigating to `url`. The link is for a navigation that * does not arrive. */ function RedirectScreen({ clientName, clientId, url, approved }) { return (
{approved ? 'Authorized' : 'Not authorized'}

Taking you back to {clientName}.

Still here?{' '} Continue to {clientHost(clientId)}

); }