@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
JavaScript
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;
}