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,
+ };
+}