import { authClient } from '@documenso/auth/client'; import type { SessionUser } from '@documenso/auth/server/lib/session/session'; import { trpc } from '@documenso/trpc/client'; import type { TGetOrganisationSessionResponse } from '@documenso/trpc/server/organisation-router/get-organisation-session.types'; import type { Session } from '@prisma/client'; import type React from 'react'; import { createContext, useCallback, useContext, useEffect, useState } from 'react'; import { useLocation } from 'react-router'; import { SKIP_QUERY_BATCH_META } from '../../constants/trpc'; export type AppSession = { session: Session; user: SessionUser; organisations: TGetOrganisationSessionResponse; }; interface SessionProviderProps { children: React.ReactNode; initialSession: AppSession | null; } interface SessionContextValue { sessionData: AppSession | null; refreshSession: () => Promise; } const SessionContext = createContext(null); export const useSession = () => { const context = useContext(SessionContext); if (!context) { throw new Error('useSession must be used within a SessionProvider'); } if (!context.sessionData) { throw new Error('Session not found'); } return { ...context.sessionData, refreshSession: context.refreshSession, }; }; export const useOptionalSession = () => { const context = useContext(SessionContext); if (!context) { throw new Error('useOptionalSession must be used within a SessionProvider'); } return context; }; export const SessionProvider = ({ children, initialSession }: SessionProviderProps) => { const [session, setSession] = useState(initialSession); const location = useLocation(); const refreshSession = useCallback(async () => { const newSession = await authClient.getSession(); if (!newSession.isAuthenticated) { setSession(null); return; } const organisations = await trpc.organisation.internal.getOrganisationSession .query(undefined, SKIP_QUERY_BATCH_META.trpc) .catch((e) => { const errorMessage = typeof e.message === 'string' ? e.message.toLowerCase() : ''; const isNetworkError = errorMessage.includes('networkerror') || errorMessage.includes('failed to fetch'); // If the error is a transient network/abort error (e.g. page refresh while // fetch was in-flight), return null to signal we should skip the state update. if (isNetworkError) { return null; } // Todo: (RR7) Log return []; }); // Skip session update if the organisation fetch was aborted due to a transient // network error (e.g. page refresh while fetch was in-flight). if (organisations === null) { return; } setSession({ session: newSession.session, user: newSession.user, organisations, }); }, []); useEffect(() => { const onFocus = () => { void refreshSession(); }; window.addEventListener('focus', onFocus); return () => { window.removeEventListener('focus', onFocus); }; }, [refreshSession]); /** * Refresh session in background on navigation. */ useEffect(() => { void refreshSession(); }, [location.pathname]); return ( {children} ); };