UNPKG

@getmocha/users-service

Version:

An API client, Hono middleware, and a React provider for the Mocha Users Service

147 lines (146 loc) 5.42 kB
import { jsx as _jsx } from "react/jsx-runtime"; import { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'; const AuthContext = createContext(null); /** * A React context provider that manages authentication state and related actions. * Install this at the top of the React component tree to provide authentication * and user management functionality. This is needed for the `useAuth` hook to work. * * This will always fetch the `user` object on mount. * * @example * import { AuthProvider } from '@getmocha/users-service/react'; * * // React Router example * export default function App() { * return ( * <AuthProvider> * <Router> * <Routes> * <Route path="/" element={<HomePage />} /> * <Route path="/auth/callback" element={<AuthCallbackPage />} /> * </Routes> * </Router> * </AuthProvider> * ); * } */ export function AuthProvider({ children }) { const [user, setUser] = useState(null); // Use these to dedup requests. This is mostly for avoiding multiple // calls from useEffects in dev, which could cause wonky behavior with // the loading states or problems when exchanging code for session token. const userRef = useRef(null); const exchangeRef = useRef(null); const [isPending, setIsPending] = useState(true); const [isFetching, setIsFetching] = useState(false); const fetchUser = useCallback(async () => { if (userRef.current) return userRef.current; userRef.current = (async () => { setIsFetching(true); try { const response = await fetch('/api/users/me'); if (!response.ok) { throw new Error(`Failed to fetch user: API responded with HTTP status ${response.status}`); } const user = await response.json(); setUser(user); } catch (error) { throw error; } finally { setIsFetching(false); userRef.current = null; } })(); return userRef.current; }, []); const logout = useCallback(async () => { try { setUser(null); await fetch('/api/logout'); } catch (error) { console.error('Failed to logout:', error); } }, []); const redirectToLogin = useCallback(async () => { try { const response = await fetch('/api/oauth/google/redirect_url'); if (!response.ok) { throw new Error(`Failed to get login redirect URL: API responded with HTTP status ${response.status}`); } const { redirectUrl } = await response.json(); window.location.href = redirectUrl; } catch (error) { console.error(error); } }, []); const exchangeCodeForSessionToken = useCallback(() => { // Ensure we only exchange the code once. In dev, useEffect will run // twice, so we need to reuse this promise to avoid multiple exchanges // which would otherwise result in a failed request. The failed request // sometimes causes the entire flow to break. if (exchangeRef.current) return exchangeRef.current; const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); if (!code) { throw new Error('Cannot exchange code for session token: no code provided in the URL search params.'); } exchangeRef.current = (async (code) => { try { const response = await fetch('/api/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ code }), }); if (!response.ok) { throw new Error(`Failed to exchange code for session token: API responded with HTTP status ${response.status}`); } // Refetch user after successful code exchange to populate user state await fetchUser(); } catch (error) { console.error(error); } finally { // exchangeRef is not set back to null on purpose. // We only expect it to run once per full page load. // If it's called more than once, it's either useEffect // on page load in dev or a bug. } })(code); return exchangeRef.current; }, [fetchUser]); useEffect(() => { fetchUser().then(() => setIsPending(false), () => setIsPending(false)); }, []); const contextValue = { user, isPending, isFetching, fetchUser, redirectToLogin, exchangeCodeForSessionToken, logout, }; return _jsx(AuthContext.Provider, { value: contextValue, children: children }); } /** * A React hook that provides the AuthContextValue. * @example * const { user } = useAuth(); */ export function useAuth() { const context = useContext(AuthContext); if (!context) { throw new Error('useAuth must be used within a AuthProvider'); } return context; }