diff --git a/www/app/(app)/AuthWrapper.tsx b/www/app/(app)/AuthWrapper.tsx new file mode 100644 index 00000000..8af78f81 --- /dev/null +++ b/www/app/(app)/AuthWrapper.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { Flex, Spinner } from "@chakra-ui/react"; +import useAuthReady from "../lib/useAuthReady"; + +export default function AuthWrapper({ + children, +}: { + children: React.ReactNode; +}) { + const { isAuthReady, isLoading } = useAuthReady(); + + // Show spinner while auth is loading + if (isLoading || !isAuthReady) { + return ( + + + + ); + } + + return <>{children}; +} diff --git a/www/app/(app)/layout.tsx b/www/app/(app)/layout.tsx index 053df5a7..8822214b 100644 --- a/www/app/(app)/layout.tsx +++ b/www/app/(app)/layout.tsx @@ -5,6 +5,7 @@ import Image from "next/image"; import About from "../(aboutAndPrivacy)/about"; import Privacy from "../(aboutAndPrivacy)/privacy"; import UserInfo from "../(auth)/userInfo"; +import AuthWrapper from "./AuthWrapper"; import { RECORD_A_MEETING_URL } from "../lib/constants"; export default async function AppLayout({ @@ -90,7 +91,7 @@ export default async function AppLayout({ - {children} + {children} ); } diff --git a/www/app/(app)/transcripts/[transcriptId]/page.tsx b/www/app/(app)/transcripts/[transcriptId]/page.tsx index 0a2dba47..3e55f5cb 100644 --- a/www/app/(app)/transcripts/[transcriptId]/page.tsx +++ b/www/app/(app)/transcripts/[transcriptId]/page.tsx @@ -86,7 +86,7 @@ export default function TranscriptDetails(details: TranscriptDetails) { useActiveTopic={useActiveTopic} waveform={waveform.waveform} media={mp3.media} - mediaDuration={transcript.response.duration} + mediaDuration={transcript.response?.duration} /> ) : !mp3.loading && (waveform.error || mp3.error) ? ( @@ -116,7 +116,7 @@ export default function TranscriptDetails(details: TranscriptDetails) { { transcript.reload(); diff --git a/www/app/(app)/transcripts/useTranscript.ts b/www/app/(app)/transcripts/useTranscript.ts index c26ca89c..7872bd09 100644 --- a/www/app/(app)/transcripts/useTranscript.ts +++ b/www/app/(app)/transcripts/useTranscript.ts @@ -46,6 +46,16 @@ const useTranscript = ( }; } + // Check if data is undefined or null + if (!data) { + return { + response: null, + loading: true, + error: false, + reload: refetch, + }; + } + return { response: data as GetTranscript, loading: false, diff --git a/www/app/lib/api-hooks.ts b/www/app/lib/api-hooks.ts index d2565f09..dd27e131 100644 --- a/www/app/lib/api-hooks.ts +++ b/www/app/lib/api-hooks.ts @@ -4,12 +4,12 @@ import { $api } from "./apiClient"; import { useError } from "../(errors)/errorContext"; import { useQueryClient } from "@tanstack/react-query"; import type { paths } from "../reflector-api"; -import useSessionStatus from "./useSessionStatus"; +import useAuthReady from "./useAuthReady"; // Rooms hooks export function useRoomsList(page: number = 1) { const { setError } = useError(); - const { status } = useSessionStatus(); + const { isAuthReady } = useAuthReady(); return $api.useQuery( "get", @@ -20,9 +20,8 @@ export function useRoomsList(page: number = 1) { }, }, { - // Only fetch when authenticated - // Using direct status check to avoid any derived state issues - enabled: status === "authenticated", + // Only fetch when authentication is fully ready (session + token) + enabled: isAuthReady, }, ); } @@ -38,7 +37,7 @@ export function useTranscriptsSearch( } = {}, ) { const { setError } = useError(); - const { status } = useSessionStatus(); + const { isAuthReady } = useAuthReady(); return $api.useQuery( "get", @@ -55,8 +54,8 @@ export function useTranscriptsSearch( }, }, { - // Only fetch when authenticated - enabled: status === "authenticated", + // Only fetch when authentication is fully ready (session + token) + enabled: isAuthReady, }, ); } @@ -92,7 +91,7 @@ export function useTranscriptProcess() { export function useTranscriptGet(transcriptId: string | null) { const { setError } = useError(); - const { status } = useSessionStatus(); + const { isAuthReady } = useAuthReady(); return $api.useQuery( "get", @@ -106,7 +105,7 @@ export function useTranscriptGet(transcriptId: string | null) { }, { // Only fetch when authenticated and transcriptId is provided - enabled: !!transcriptId && status === "authenticated", + enabled: !!transcriptId && isAuthReady, }, ); } @@ -163,7 +162,7 @@ export function useRoomDelete() { // Zulip hooks - NOTE: These endpoints are not in the OpenAPI spec yet export function useZulipStreams() { const { setError } = useError(); - const { status } = useSessionStatus(); + const { isAuthReady } = useAuthReady(); // @ts-ignore - Zulip endpoint not in OpenAPI spec return $api.useQuery( @@ -172,14 +171,14 @@ export function useZulipStreams() { {}, { // Only fetch when authenticated - enabled: status === "authenticated", + enabled: isAuthReady, }, ); } export function useZulipTopics(streamId: number | null) { const { setError } = useError(); - const { status } = useSessionStatus(); + const { isAuthReady } = useAuthReady(); // @ts-ignore - Zulip endpoint not in OpenAPI spec return $api.useQuery( @@ -188,7 +187,7 @@ export function useZulipTopics(streamId: number | null) { {}, { // Only fetch when authenticated and streamId is provided - enabled: !!streamId && status === "authenticated", + enabled: !!streamId && isAuthReady, }, ); } @@ -262,7 +261,7 @@ export function useTranscriptUploadAudio() { // Transcript queries export function useTranscriptWaveform(transcriptId: string | null) { const { setError } = useError(); - const { status } = useSessionStatus(); + const { isAuthReady } = useAuthReady(); return $api.useQuery( "get", @@ -273,14 +272,14 @@ export function useTranscriptWaveform(transcriptId: string | null) { }, }, { - enabled: !!transcriptId && status === "authenticated", + enabled: !!transcriptId && isAuthReady, }, ); } export function useTranscriptMP3(transcriptId: string | null) { const { setError } = useError(); - const { status } = useSessionStatus(); + const { isAuthReady } = useAuthReady(); return $api.useQuery( "get", @@ -291,14 +290,14 @@ export function useTranscriptMP3(transcriptId: string | null) { }, }, { - enabled: !!transcriptId && status === "authenticated", + enabled: !!transcriptId && isAuthReady, }, ); } export function useTranscriptTopics(transcriptId: string | null) { const { setError } = useError(); - const { status } = useSessionStatus(); + const { isAuthReady } = useAuthReady(); return $api.useQuery( "get", @@ -309,14 +308,14 @@ export function useTranscriptTopics(transcriptId: string | null) { }, }, { - enabled: !!transcriptId && status === "authenticated", + enabled: !!transcriptId && isAuthReady, }, ); } export function useTranscriptTopicsWithWords(transcriptId: string | null) { const { setError } = useError(); - const { status } = useSessionStatus(); + const { isAuthReady } = useAuthReady(); return $api.useQuery( "get", @@ -327,7 +326,7 @@ export function useTranscriptTopicsWithWords(transcriptId: string | null) { }, }, { - enabled: !!transcriptId && status === "authenticated", + enabled: !!transcriptId && isAuthReady, }, ); } @@ -337,7 +336,7 @@ export function useTranscriptTopicsWithWordsPerSpeaker( topicId: string | null, ) { const { setError } = useError(); - const { status } = useSessionStatus(); + const { isAuthReady } = useAuthReady(); return $api.useQuery( "get", @@ -351,7 +350,7 @@ export function useTranscriptTopicsWithWordsPerSpeaker( }, }, { - enabled: !!transcriptId && !!topicId && status === "authenticated", + enabled: !!transcriptId && !!topicId && isAuthReady, }, ); } @@ -359,7 +358,7 @@ export function useTranscriptTopicsWithWordsPerSpeaker( // Participant operations export function useTranscriptParticipants(transcriptId: string | null) { const { setError } = useError(); - const { status } = useSessionStatus(); + const { isAuthReady } = useAuthReady(); return $api.useQuery( "get", @@ -370,7 +369,7 @@ export function useTranscriptParticipants(transcriptId: string | null) { }, }, { - enabled: !!transcriptId && status === "authenticated", + enabled: !!transcriptId && isAuthReady, }, ); } diff --git a/www/app/lib/apiClient.tsx b/www/app/lib/apiClient.tsx index cc1e4ca7..118adcd1 100644 --- a/www/app/lib/apiClient.tsx +++ b/www/app/lib/apiClient.tsx @@ -19,8 +19,12 @@ export const client = createClient({ // Create the React Query client wrapper export const $api = createFetchClient(client); -// Store the current auth token +// Store the current auth token and ready state let currentAuthToken: string | null | undefined = null; +let authConfigured = false; + +// Export function to check if auth is ready +export const isAuthConfigured = () => authConfigured; // Set up authentication middleware once client.use({ @@ -42,6 +46,7 @@ client.use({ // Configure authentication by updating the token export const configureApiAuth = (token: string | null | undefined) => { currentAuthToken = token; + authConfigured = true; }; // Export typed hooks for convenience diff --git a/www/app/lib/useAuthReady.ts b/www/app/lib/useAuthReady.ts new file mode 100644 index 00000000..ac85c808 --- /dev/null +++ b/www/app/lib/useAuthReady.ts @@ -0,0 +1,43 @@ +"use client"; + +import { useState, useEffect } from "react"; +import useSessionStatus from "./useSessionStatus"; +import { isAuthConfigured } from "./apiClient"; + +/** + * Hook to check if authentication is fully ready. + * This ensures both the session is authenticated AND the API client token is configured. + * Prevents race conditions where React Query fires requests before the token is set. + */ +export default function useAuthReady() { + const { status, isAuthenticated } = useSessionStatus(); + const [authReady, setAuthReady] = useState(false); + + useEffect(() => { + // Check if both session is authenticated and token is configured + const checkAuthReady = () => { + const ready = isAuthenticated && isAuthConfigured(); + setAuthReady(ready); + }; + + // Check immediately + checkAuthReady(); + + // Also check periodically for a short time to catch async updates + const interval = setInterval(checkAuthReady, 100); + + // Stop checking after 2 seconds (auth should be ready by then) + const timeout = setTimeout(() => clearInterval(interval), 2000); + + return () => { + clearInterval(interval); + clearTimeout(timeout); + }; + }, [isAuthenticated]); + + return { + isAuthReady: authReady, + isLoading: status === "loading", + isAuthenticated, + }; +}