UNPKG

@convex-dev/better-auth

Version:
190 lines 7.13 kB
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { Component, useCallback, useEffect, useMemo, useState } from "react"; import { Authenticated, ConvexProviderWithAuth, useConvexAuth, useQuery, } from "convex/react"; /** * A wrapper React component which provides a {@link react.ConvexReactClient} * authenticated with Better Auth. * * @public */ export function ConvexBetterAuthProvider({ children, client, authClient, initialToken, }) { const useBetterAuth = useUseAuthFromBetterAuth(authClient, initialToken); useEffect(() => { (async () => { const url = new URL(window.location?.href); const token = url.searchParams.get("ott"); if (token) { const authClientWithCrossDomain = authClient; url.searchParams.delete("ott"); const result = await authClientWithCrossDomain.crossDomain.oneTimeToken.verify({ token, }); const session = result.data?.session; if (session) { await authClient.getSession({ fetchOptions: { headers: { Authorization: `Bearer ${session.token}`, }, }, }); authClientWithCrossDomain.updateSession(); } window.history.replaceState({}, "", url); } })(); }, [authClient]); return (_jsx(ConvexProviderWithAuth, { client: client, useAuth: useBetterAuth, children: _jsx(_Fragment, { children: children }) })); } let initialTokenUsed = false; function useUseAuthFromBetterAuth(authClient, initialToken) { const [cachedToken, setCachedToken] = useState(initialTokenUsed ? (initialToken ?? null) : null); useEffect(() => { if (!initialTokenUsed) { initialTokenUsed = true; } }, []); return useMemo(() => function useAuthFromBetterAuth() { const { data: session, isPending: isSessionPending } = authClient.useSession(); const sessionId = session?.session?.id; useEffect(() => { if (!session && !isSessionPending && cachedToken) { setCachedToken(null); } }, [session, isSessionPending]); const fetchAccessToken = useCallback(async ({ forceRefreshToken = false, } = {}) => { if (cachedToken && !forceRefreshToken) { return cachedToken; } try { const { data } = await authClient.convex.token(); const token = data?.token || null; setCachedToken(token); return token; } catch { setCachedToken(null); return null; } }, // Build a new fetchAccessToken to trigger setAuth() whenever the // session changes. // eslint-disable-next-line react-hooks/exhaustive-deps [sessionId]); return useMemo(() => ({ isLoading: isSessionPending, isAuthenticated: session !== null, fetchAccessToken, }), // eslint-disable-next-line react-hooks/exhaustive-deps [isSessionPending, sessionId, fetchAccessToken]); }, [authClient]); } class ErrorBoundary extends Component { constructor(props) { super(props); this.state = {}; } static defaultProps = { renderFallback: () => null, }; static getDerivedStateFromError(error) { return { error }; } async componentDidCatch(error) { if (this.props.isAuthError(error)) { await this.props.onUnauth(); } } render() { if (this.state.error && this.props.isAuthError(this.state.error)) { return this.props.renderFallback?.(); } return this.props.children; } } // Subscribe to the session validated user to keep this check reactive to // actual user auth state at the provider level (rather than just jwt validity state). const UserSubscription = ({ getAuthUserFn, }) => { useQuery(getAuthUserFn); return null; }; /** * _Experimental_ * * A wrapper React component which provides error handling for auth related errors. * This is typically used to redirect the user to the login page when they are * unauthenticated, and does so reactively based on the getAuthUserFn query. * * @example * ```ts * // convex/auth.ts * export const { getAuthUser } = authComponent.clientApi(); * * // auth-client.tsx * import { AuthBoundary } from "@convex-dev/react"; * import { api } from '../../convex/_generated/api' * import { isAuthError } from '../lib/utils' * * export const ClientAuthBoundary = ({ children }: PropsWithChildren) => { * return ( * <AuthBoundary * onUnauth={() => redirect("/sign-in")} * authClient={authClient} * getAuthUserFn={api.auth.getAuthUser} * isAuthError={isAuthError} * > * <>{children}</> * </AuthBoundary> * ) * ``` * @param props.children - Children to render. * @param props.onUnauth - Function to call when the user is * unauthenticated. Typically a redirect to the login page. * @param props.authClient - Better Auth authClient to use. * @param props.renderFallback - Fallback component to render when the user is * unauthenticated. Defaults to null. Generally not rendered as error handling * is typically a redirect. * @param props.getAuthUserFn - Reference to a Convex query that returns user. * The component provides a query for this via `export const { getAuthUser } = authComponent.clientApi()`. * @param props.isAuthError - Function to check if the error is auth related. */ export const AuthBoundary = ({ children, /** * The function to call when the user is unauthenticated. Typically a redirect * to the login page. */ onUnauth, /** * The Better Auth authClient to use. */ authClient, /** * The fallback to render when the user is unauthenticated. Defaults to null. * Generally not rendered as error handling is typically a redirect. */ renderFallback, /** * The function to call to get the auth user. */ getAuthUserFn, /** * The function to call to check if the error is auth related. */ isAuthError, }) => { const { isAuthenticated, isLoading } = useConvexAuth(); const handleUnauth = useCallback(async () => { // Auth request that will clear cookies if session is invalid await authClient.getSession(); await onUnauth(); }, [onUnauth]); useEffect(() => { void (async () => { if (!isLoading && !isAuthenticated) { await handleUnauth(); } })(); }, [isLoading, isAuthenticated]); return (_jsxs(ErrorBoundary, { onUnauth: handleUnauth, isAuthError: isAuthError, renderFallback: renderFallback, children: [_jsx(Authenticated, { children: _jsx(UserSubscription, { getAuthUserFn: getAuthUserFn }) }), children] })); }; //# sourceMappingURL=index.js.map