import { createContext, useCallback, useContext, useEffect, useState, } from 'react'; import { canReview, currentSession, grantCurrent, onSessionEnded, renewGrant, signOut as revoke, } from '#/lib/oauth.js'; /** * @typedef {Object} SessionContextValue * @property {import('#/lib/oauth.js').Session|null} session * @property {(value: import('#/lib/oauth.js').Session|null) => void} setSession * @property {() => Promise} endSession * @property {boolean} reviewer - whether the granted scope permits a review */ const SessionContext = createContext( /** @type {SessionContextValue} */ (/** @type {unknown} */ ({})), ); export function SessionProvider({ children }) { const [session, setSession] = useState(() => currentSession()); const endSession = useCallback(async () => { await revoke(session); setSession(null); }, [session]); // A refusal from the server, rather than a sign-out the visitor asked for. useEffect(() => { onSessionEnded(() => setSession(null)); }, []); // The app's scope grows as it does, and a session granted before a growth // lacks the new piece. Rather than a message, the visitor goes back // through consent and returns to this page: the consent screen itself is // the ask, and it names exactly what changed. Never from the callback, // which is the renewal finishing; once per tab, so a server granting less // than asked cannot loop the page. useEffect(() => { if (!session || grantCurrent(session)) return; if (window.location.pathname === '/callback') return; renewGrant( session, window.location.pathname + window.location.search + window.location.hash, ); }, [session]); return ( {children} ); } export function useSession() { return useContext(SessionContext); }