From 9538992eafb868473f8d4b79ab54abe8df6d7430 Mon Sep 17 00:00:00 2001 From: Ubbe Date: Thu, 29 Jan 2026 18:13:28 +0700 Subject: [PATCH] hotfix(frontend): flags copilot redirects (#11878) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes 🏗️ - Refactor homepage redirect logic to always point to `/` - the `/` route handles whether to redirect to `/copilot` or `/library` based on flag - Simplify `useGetFlag` checks - Add `` and `` wrapper components - helpers to do 1 thing or the other, depending on chat enabled/disabled - avoids boilerplate code, checking flagss and redirects mistakes (especially around race conditions with LD init ) ## Checklist 📋 ### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Log in / out of AutoGPT with flag disabled/enabled - [x] Sign up to AutoGPT with flag disabled/enabled - [x] Redirects to homepage always work `/` - [x] Can't access Copilot with disabled flag --- .gitignore | 1 + .../src/app/(no-navbar)/onboarding/page.tsx | 13 ++-- .../src/app/(platform)/auth/callback/route.ts | 14 ++--- .../SessionsList/useSessionsPagination.ts | 4 +- .../src/app/(platform)/copilot/layout.tsx | 11 +++- .../src/app/(platform)/copilot/page.tsx | 12 +--- .../app/(platform)/copilot/useCopilotPage.ts | 31 +--------- .../src/app/(platform)/error/page.tsx | 6 +- .../src/app/(platform)/login/actions.ts | 7 +-- .../src/app/(platform)/login/useLoginPage.ts | 10 +--- .../src/app/(platform)/signup/actions.ts | 7 +-- .../app/(platform)/signup/useSignupPage.ts | 10 +--- .../frontend/src/app/api/helpers.ts | 1 - autogpt_platform/frontend/src/app/page.tsx | 29 +++------ .../layout/Navbar/components/NavbarLink.tsx | 7 +-- .../frontend/src/hooks/useAgentGraph.tsx | 2 +- .../frontend/src/lib/constants.ts | 7 --- .../frontend/src/lib/supabase/helpers.ts | 3 +- .../frontend/src/lib/supabase/middleware.ts | 3 +- .../onboarding/onboarding-provider.tsx | 10 +--- .../src/services/environment/index.ts | 10 +++- .../feature-flags/FeatureFlagPage.tsx | 59 +++++++++++++++++++ .../feature-flags/FeatureFlagRedirect.tsx | 51 ++++++++++++++++ .../feature-flags/feature-flag-provider.tsx | 10 ++-- .../services/feature-flags/use-get-flag.ts | 33 +++-------- classic/frontend/.gitignore | 1 + 26 files changed, 188 insertions(+), 164 deletions(-) create mode 100644 autogpt_platform/frontend/src/services/feature-flags/FeatureFlagPage.tsx create mode 100644 autogpt_platform/frontend/src/services/feature-flags/FeatureFlagRedirect.tsx diff --git a/.gitignore b/.gitignore index dfce8ba810..8660637ae5 100644 --- a/.gitignore +++ b/.gitignore @@ -179,3 +179,4 @@ autogpt_platform/backend/settings.py .test-contents .claude/settings.local.json /autogpt_platform/backend/logs +.next \ No newline at end of file diff --git a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/page.tsx b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/page.tsx index 70d9783ccd..246fe52826 100644 --- a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/page.tsx +++ b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/page.tsx @@ -1,10 +1,9 @@ "use client"; +import { getV1OnboardingState } from "@/app/api/__generated__/endpoints/onboarding/onboarding"; +import { getOnboardingStatus, resolveResponse } from "@/app/api/helpers"; import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; -import { resolveResponse, getOnboardingStatus } from "@/app/api/helpers"; -import { getV1OnboardingState } from "@/app/api/__generated__/endpoints/onboarding/onboarding"; -import { getHomepageRoute } from "@/lib/constants"; export default function OnboardingPage() { const router = useRouter(); @@ -13,12 +12,10 @@ export default function OnboardingPage() { async function redirectToStep() { try { // Check if onboarding is enabled (also gets chat flag for redirect) - const { shouldShowOnboarding, isChatEnabled } = - await getOnboardingStatus(); - const homepageRoute = getHomepageRoute(isChatEnabled); + const { shouldShowOnboarding } = await getOnboardingStatus(); if (!shouldShowOnboarding) { - router.replace(homepageRoute); + router.replace("/"); return; } @@ -26,7 +23,7 @@ export default function OnboardingPage() { // Handle completed onboarding if (onboarding.completedSteps.includes("GET_RESULTS")) { - router.replace(homepageRoute); + router.replace("/"); return; } diff --git a/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts b/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts index 15be137f63..e7e2997d0d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts +++ b/autogpt_platform/frontend/src/app/(platform)/auth/callback/route.ts @@ -1,9 +1,8 @@ -import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; -import { getHomepageRoute } from "@/lib/constants"; -import BackendAPI from "@/lib/autogpt-server-api"; -import { NextResponse } from "next/server"; -import { revalidatePath } from "next/cache"; import { getOnboardingStatus } from "@/app/api/helpers"; +import BackendAPI from "@/lib/autogpt-server-api"; +import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; +import { revalidatePath } from "next/cache"; +import { NextResponse } from "next/server"; // Handle the callback to complete the user session login export async function GET(request: Request) { @@ -27,13 +26,12 @@ export async function GET(request: Request) { await api.createUser(); // Get onboarding status from backend (includes chat flag evaluated for this user) - const { shouldShowOnboarding, isChatEnabled } = - await getOnboardingStatus(); + const { shouldShowOnboarding } = await getOnboardingStatus(); if (shouldShowOnboarding) { next = "/onboarding"; revalidatePath("/onboarding", "layout"); } else { - next = getHomepageRoute(isChatEnabled); + next = "/"; revalidatePath(next, "layout"); } } catch (createUserError) { diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/useSessionsPagination.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/useSessionsPagination.ts index 11ddd937af..61e3e6f37f 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/useSessionsPagination.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/components/CopilotShell/components/SessionsList/useSessionsPagination.ts @@ -73,9 +73,9 @@ export function useSessionsPagination({ enabled }: UseSessionsPaginationArgs) { }; const reset = () => { + // Only reset the offset - keep existing sessions visible during refetch + // The effect will replace sessions when new data arrives at offset 0 setOffset(0); - setAccumulatedSessions([]); - setTotalCount(null); }; return { diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/layout.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/layout.tsx index 89cf72e2ba..876e5accfb 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/layout.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/layout.tsx @@ -1,6 +1,13 @@ -import type { ReactNode } from "react"; +"use client"; +import { FeatureFlagPage } from "@/services/feature-flags/FeatureFlagPage"; +import { Flag } from "@/services/feature-flags/use-get-flag"; +import { type ReactNode } from "react"; import { CopilotShell } from "./components/CopilotShell/CopilotShell"; export default function CopilotLayout({ children }: { children: ReactNode }) { - return {children}; + return ( + + {children} + + ); } diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx b/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx index 104b238895..e9bc018c1b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/page.tsx @@ -14,14 +14,8 @@ export default function CopilotPage() { const isInterruptModalOpen = useCopilotStore((s) => s.isInterruptModalOpen); const confirmInterrupt = useCopilotStore((s) => s.confirmInterrupt); const cancelInterrupt = useCopilotStore((s) => s.cancelInterrupt); - const { - greetingName, - quickActions, - isLoading, - hasSession, - initialPrompt, - isReady, - } = state; + const { greetingName, quickActions, isLoading, hasSession, initialPrompt } = + state; const { handleQuickAction, startChatWithPrompt, @@ -29,8 +23,6 @@ export default function CopilotPage() { handleStreamingChange, } = handlers; - if (!isReady) return null; - if (hasSession) { return (
diff --git a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts index e4713cd24a..9d99f8e7bd 100644 --- a/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/copilot/useCopilotPage.ts @@ -3,18 +3,11 @@ import { postV2CreateSession, } from "@/app/api/__generated__/endpoints/chat/chat"; import { useToast } from "@/components/molecules/Toast/use-toast"; -import { getHomepageRoute } from "@/lib/constants"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { useOnboarding } from "@/providers/onboarding/onboarding-provider"; -import { - Flag, - type FlagValues, - useGetFlag, -} from "@/services/feature-flags/use-get-flag"; import { SessionKey, sessionStorage } from "@/services/storage/session-storage"; import * as Sentry from "@sentry/nextjs"; import { useQueryClient } from "@tanstack/react-query"; -import { useFlags } from "launchdarkly-react-client-sdk"; import { useRouter } from "next/navigation"; import { useEffect } from "react"; import { useCopilotStore } from "./copilot-page-store"; @@ -33,22 +26,6 @@ export function useCopilotPage() { const isCreating = useCopilotStore((s) => s.isCreatingSession); const setIsCreating = useCopilotStore((s) => s.setIsCreatingSession); - // Complete VISIT_COPILOT onboarding step to grant $5 welcome bonus - useEffect(() => { - if (isLoggedIn) { - completeStep("VISIT_COPILOT"); - } - }, [completeStep, isLoggedIn]); - - const isChatEnabled = useGetFlag(Flag.CHAT); - const flags = useFlags(); - const homepageRoute = getHomepageRoute(isChatEnabled); - const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true"; - const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID; - const isLaunchDarklyConfigured = envEnabled && Boolean(clientId); - const isFlagReady = - !isLaunchDarklyConfigured || flags[Flag.CHAT] !== undefined; - const greetingName = getGreetingName(user); const quickActions = getQuickActions(); @@ -58,11 +35,8 @@ export function useCopilotPage() { : undefined; useEffect(() => { - if (!isFlagReady) return; - if (isChatEnabled === false) { - router.replace(homepageRoute); - } - }, [homepageRoute, isChatEnabled, isFlagReady, router]); + if (isLoggedIn) completeStep("VISIT_COPILOT"); + }, [completeStep, isLoggedIn]); async function startChatWithPrompt(prompt: string) { if (!prompt?.trim()) return; @@ -116,7 +90,6 @@ export function useCopilotPage() { isLoading: isUserLoading, hasSession, initialPrompt, - isReady: isFlagReady && isChatEnabled !== false && isLoggedIn, }, handlers: { handleQuickAction, diff --git a/autogpt_platform/frontend/src/app/(platform)/error/page.tsx b/autogpt_platform/frontend/src/app/(platform)/error/page.tsx index b26ca4559b..3cf68178ad 100644 --- a/autogpt_platform/frontend/src/app/(platform)/error/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/error/page.tsx @@ -1,8 +1,6 @@ "use client"; import { ErrorCard } from "@/components/molecules/ErrorCard/ErrorCard"; -import { getHomepageRoute } from "@/lib/constants"; -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; import { useSearchParams } from "next/navigation"; import { Suspense } from "react"; import { getErrorDetails } from "./helpers"; @@ -11,8 +9,6 @@ function ErrorPageContent() { const searchParams = useSearchParams(); const errorMessage = searchParams.get("message"); const errorDetails = getErrorDetails(errorMessage); - const isChatEnabled = useGetFlag(Flag.CHAT); - const homepageRoute = getHomepageRoute(isChatEnabled); function handleRetry() { // Auth-related errors should redirect to login @@ -30,7 +26,7 @@ function ErrorPageContent() { }, 2000); } else { // For server/network errors, go to home - window.location.href = homepageRoute; + window.location.href = "/"; } } diff --git a/autogpt_platform/frontend/src/app/(platform)/login/actions.ts b/autogpt_platform/frontend/src/app/(platform)/login/actions.ts index 447a25a41d..c4867dd123 100644 --- a/autogpt_platform/frontend/src/app/(platform)/login/actions.ts +++ b/autogpt_platform/frontend/src/app/(platform)/login/actions.ts @@ -1,6 +1,5 @@ "use server"; -import { getHomepageRoute } from "@/lib/constants"; import BackendAPI from "@/lib/autogpt-server-api"; import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; import { loginFormSchema } from "@/types/auth"; @@ -38,10 +37,8 @@ export async function login(email: string, password: string) { await api.createUser(); // Get onboarding status from backend (includes chat flag evaluated for this user) - const { shouldShowOnboarding, isChatEnabled } = await getOnboardingStatus(); - const next = shouldShowOnboarding - ? "/onboarding" - : getHomepageRoute(isChatEnabled); + const { shouldShowOnboarding } = await getOnboardingStatus(); + const next = shouldShowOnboarding ? "/onboarding" : "/"; return { success: true, diff --git a/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts b/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts index e64cc1858d..9b81965c31 100644 --- a/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts @@ -1,8 +1,6 @@ import { useToast } from "@/components/molecules/Toast/use-toast"; -import { getHomepageRoute } from "@/lib/constants"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { environment } from "@/services/environment"; -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; import { loginFormSchema, LoginProvider } from "@/types/auth"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter, useSearchParams } from "next/navigation"; @@ -22,17 +20,15 @@ export function useLoginPage() { const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [showNotAllowedModal, setShowNotAllowedModal] = useState(false); const isCloudEnv = environment.isCloud(); - const isChatEnabled = useGetFlag(Flag.CHAT); - const homepageRoute = getHomepageRoute(isChatEnabled); // Get redirect destination from 'next' query parameter const nextUrl = searchParams.get("next"); useEffect(() => { if (isLoggedIn && !isLoggingIn) { - router.push(nextUrl || homepageRoute); + router.push(nextUrl || "/"); } - }, [homepageRoute, isLoggedIn, isLoggingIn, nextUrl, router]); + }, [isLoggedIn, isLoggingIn, nextUrl, router]); const form = useForm>({ resolver: zodResolver(loginFormSchema), @@ -98,7 +94,7 @@ export function useLoginPage() { } // Prefer URL's next parameter, then use backend-determined route - router.replace(nextUrl || result.next || homepageRoute); + router.replace(nextUrl || result.next || "/"); } catch (error) { toast({ title: diff --git a/autogpt_platform/frontend/src/app/(platform)/signup/actions.ts b/autogpt_platform/frontend/src/app/(platform)/signup/actions.ts index 0fbba54b8e..204482dbe9 100644 --- a/autogpt_platform/frontend/src/app/(platform)/signup/actions.ts +++ b/autogpt_platform/frontend/src/app/(platform)/signup/actions.ts @@ -1,6 +1,5 @@ "use server"; -import { getHomepageRoute } from "@/lib/constants"; import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase"; import { signupFormSchema } from "@/types/auth"; import * as Sentry from "@sentry/nextjs"; @@ -59,10 +58,8 @@ export async function signup( } // Get onboarding status from backend (includes chat flag evaluated for this user) - const { shouldShowOnboarding, isChatEnabled } = await getOnboardingStatus(); - const next = shouldShowOnboarding - ? "/onboarding" - : getHomepageRoute(isChatEnabled); + const { shouldShowOnboarding } = await getOnboardingStatus(); + const next = shouldShowOnboarding ? "/onboarding" : "/"; return { success: true, next }; } catch (err) { diff --git a/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts b/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts index 5fa8c2c159..fd78b48735 100644 --- a/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts @@ -1,8 +1,6 @@ import { useToast } from "@/components/molecules/Toast/use-toast"; -import { getHomepageRoute } from "@/lib/constants"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { environment } from "@/services/environment"; -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; import { LoginProvider, signupFormSchema } from "@/types/auth"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter, useSearchParams } from "next/navigation"; @@ -22,17 +20,15 @@ export function useSignupPage() { const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [showNotAllowedModal, setShowNotAllowedModal] = useState(false); const isCloudEnv = environment.isCloud(); - const isChatEnabled = useGetFlag(Flag.CHAT); - const homepageRoute = getHomepageRoute(isChatEnabled); // Get redirect destination from 'next' query parameter const nextUrl = searchParams.get("next"); useEffect(() => { if (isLoggedIn && !isSigningUp) { - router.push(nextUrl || homepageRoute); + router.push(nextUrl || "/"); } - }, [homepageRoute, isLoggedIn, isSigningUp, nextUrl, router]); + }, [isLoggedIn, isSigningUp, nextUrl, router]); const form = useForm>({ resolver: zodResolver(signupFormSchema), @@ -133,7 +129,7 @@ export function useSignupPage() { } // Prefer the URL's next parameter, then result.next (for onboarding), then default - const redirectTo = nextUrl || result.next || homepageRoute; + const redirectTo = nextUrl || result.next || "/"; router.replace(redirectTo); } catch (error) { setIsLoading(false); diff --git a/autogpt_platform/frontend/src/app/api/helpers.ts b/autogpt_platform/frontend/src/app/api/helpers.ts index c2104d231a..226f5fa786 100644 --- a/autogpt_platform/frontend/src/app/api/helpers.ts +++ b/autogpt_platform/frontend/src/app/api/helpers.ts @@ -181,6 +181,5 @@ export async function getOnboardingStatus() { const isCompleted = onboarding.completedSteps.includes("CONGRATS"); return { shouldShowOnboarding: status.is_onboarding_enabled && !isCompleted, - isChatEnabled: status.is_chat_enabled, }; } diff --git a/autogpt_platform/frontend/src/app/page.tsx b/autogpt_platform/frontend/src/app/page.tsx index dbfab49469..31d1e96e48 100644 --- a/autogpt_platform/frontend/src/app/page.tsx +++ b/autogpt_platform/frontend/src/app/page.tsx @@ -1,27 +1,14 @@ "use client"; -import { getHomepageRoute } from "@/lib/constants"; -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; -import { useRouter } from "next/navigation"; -import { useEffect } from "react"; +import { FeatureFlagRedirect } from "@/services/feature-flags/FeatureFlagRedirect"; +import { Flag } from "@/services/feature-flags/use-get-flag"; export default function Page() { - const isChatEnabled = useGetFlag(Flag.CHAT); - const router = useRouter(); - const homepageRoute = getHomepageRoute(isChatEnabled); - const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true"; - const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID; - const isLaunchDarklyConfigured = envEnabled && Boolean(clientId); - const isFlagReady = - !isLaunchDarklyConfigured || typeof isChatEnabled === "boolean"; - - useEffect( - function redirectToHomepage() { - if (!isFlagReady) return; - router.replace(homepageRoute); - }, - [homepageRoute, isFlagReady, router], + return ( + ); - - return null; } diff --git a/autogpt_platform/frontend/src/components/layout/Navbar/components/NavbarLink.tsx b/autogpt_platform/frontend/src/components/layout/Navbar/components/NavbarLink.tsx index eab5a7352f..dff1277384 100644 --- a/autogpt_platform/frontend/src/components/layout/Navbar/components/NavbarLink.tsx +++ b/autogpt_platform/frontend/src/components/layout/Navbar/components/NavbarLink.tsx @@ -1,7 +1,6 @@ "use client"; import { IconLaptop } from "@/components/__legacy__/ui/icons"; -import { getHomepageRoute } from "@/lib/constants"; import { cn } from "@/lib/utils"; import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; import { ListChecksIcon } from "@phosphor-icons/react/dist/ssr"; @@ -24,11 +23,11 @@ interface Props { export function NavbarLink({ name, href }: Props) { const pathname = usePathname(); const isChatEnabled = useGetFlag(Flag.CHAT); - const homepageRoute = getHomepageRoute(isChatEnabled); + const expectedHomeRoute = isChatEnabled ? "/copilot" : "/library"; const isActive = - href === homepageRoute - ? pathname === "/" || pathname.startsWith(homepageRoute) + href === expectedHomeRoute + ? pathname === "/" || pathname.startsWith(expectedHomeRoute) : pathname.includes(href); return ( diff --git a/autogpt_platform/frontend/src/hooks/useAgentGraph.tsx b/autogpt_platform/frontend/src/hooks/useAgentGraph.tsx index 6c097c395e..d422e389dd 100644 --- a/autogpt_platform/frontend/src/hooks/useAgentGraph.tsx +++ b/autogpt_platform/frontend/src/hooks/useAgentGraph.tsx @@ -66,7 +66,7 @@ export default function useAgentGraph( >(null); const [xyNodes, setXYNodes] = useState([]); const [xyEdges, setXYEdges] = useState([]); - const betaBlocks = useGetFlag(Flag.BETA_BLOCKS); + const betaBlocks = useGetFlag(Flag.BETA_BLOCKS) as string[]; // Filter blocks based on beta flags const availableBlocks = useMemo(() => { diff --git a/autogpt_platform/frontend/src/lib/constants.ts b/autogpt_platform/frontend/src/lib/constants.ts index de5aac1670..19365a56ac 100644 --- a/autogpt_platform/frontend/src/lib/constants.ts +++ b/autogpt_platform/frontend/src/lib/constants.ts @@ -11,10 +11,3 @@ export const API_KEY_HEADER_NAME = "X-API-Key"; // Layout export const NAVBAR_HEIGHT_PX = 60; - -// Routes -export function getHomepageRoute(isChatEnabled?: boolean | null): string { - if (isChatEnabled === true) return "/copilot"; - if (isChatEnabled === false) return "/library"; - return "/"; -} diff --git a/autogpt_platform/frontend/src/lib/supabase/helpers.ts b/autogpt_platform/frontend/src/lib/supabase/helpers.ts index 3fd0eacb5f..26f7711bde 100644 --- a/autogpt_platform/frontend/src/lib/supabase/helpers.ts +++ b/autogpt_platform/frontend/src/lib/supabase/helpers.ts @@ -1,4 +1,3 @@ -import { getHomepageRoute } from "@/lib/constants"; import { environment } from "@/services/environment"; import { Key, storage } from "@/services/storage/local-storage"; import { type CookieOptions } from "@supabase/ssr"; @@ -71,7 +70,7 @@ export function getRedirectPath( } if (isAdminPage(path) && userRole !== "admin") { - return getHomepageRoute(); + return "/"; } return null; diff --git a/autogpt_platform/frontend/src/lib/supabase/middleware.ts b/autogpt_platform/frontend/src/lib/supabase/middleware.ts index de8b867ef0..cd1f4a240e 100644 --- a/autogpt_platform/frontend/src/lib/supabase/middleware.ts +++ b/autogpt_platform/frontend/src/lib/supabase/middleware.ts @@ -1,4 +1,3 @@ -import { getHomepageRoute } from "@/lib/constants"; import { environment } from "@/services/environment"; import { createServerClient } from "@supabase/ssr"; import { NextResponse, type NextRequest } from "next/server"; @@ -67,7 +66,7 @@ export async function updateSession(request: NextRequest) { // 2. Check if user is authenticated but lacks admin role when accessing admin pages if (user && userRole !== "admin" && isAdminPage(pathname)) { - url.pathname = getHomepageRoute(); + url.pathname = "/"; return NextResponse.redirect(url); } diff --git a/autogpt_platform/frontend/src/providers/onboarding/onboarding-provider.tsx b/autogpt_platform/frontend/src/providers/onboarding/onboarding-provider.tsx index 1ee4b2b6db..42cb99f187 100644 --- a/autogpt_platform/frontend/src/providers/onboarding/onboarding-provider.tsx +++ b/autogpt_platform/frontend/src/providers/onboarding/onboarding-provider.tsx @@ -23,9 +23,7 @@ import { WebSocketNotification, } from "@/lib/autogpt-server-api"; import { useBackendAPI } from "@/lib/autogpt-server-api/context"; -import { getHomepageRoute } from "@/lib/constants"; import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; -import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import { @@ -104,8 +102,6 @@ export default function OnboardingProvider({ const pathname = usePathname(); const router = useRouter(); const { isLoggedIn } = useSupabase(); - const isChatEnabled = useGetFlag(Flag.CHAT); - const homepageRoute = getHomepageRoute(isChatEnabled); useOnboardingTimezoneDetection(); @@ -150,7 +146,7 @@ export default function OnboardingProvider({ if (isOnOnboardingRoute) { const enabled = await resolveResponse(getV1IsOnboardingEnabled()); if (!enabled) { - router.push(homepageRoute); + router.push("/"); return; } } @@ -162,7 +158,7 @@ export default function OnboardingProvider({ isOnOnboardingRoute && shouldRedirectFromOnboarding(onboarding.completedSteps, pathname) ) { - router.push(homepageRoute); + router.push("/"); } } catch (error) { console.error("Failed to initialize onboarding:", error); @@ -177,7 +173,7 @@ export default function OnboardingProvider({ } initializeOnboarding(); - }, [api, homepageRoute, isOnOnboardingRoute, router, isLoggedIn, pathname]); + }, [api, isOnOnboardingRoute, router, isLoggedIn, pathname]); const handleOnboardingNotification = useCallback( (notification: WebSocketNotification) => { diff --git a/autogpt_platform/frontend/src/services/environment/index.ts b/autogpt_platform/frontend/src/services/environment/index.ts index f19bc417e3..0214dcb3c8 100644 --- a/autogpt_platform/frontend/src/services/environment/index.ts +++ b/autogpt_platform/frontend/src/services/environment/index.ts @@ -83,6 +83,10 @@ function getPostHogCredentials() { }; } +function getLaunchDarklyClientId() { + return process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID; +} + function isProductionBuild() { return process.env.NODE_ENV === "production"; } @@ -120,7 +124,10 @@ function isVercelPreview() { } function areFeatureFlagsEnabled() { - return process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "enabled"; + return ( + process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true" && + Boolean(process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID) + ); } function isPostHogEnabled() { @@ -143,6 +150,7 @@ export const environment = { getSupabaseAnonKey, getPreviewStealingDev, getPostHogCredentials, + getLaunchDarklyClientId, // Assertions isServerSide, isClientSide, diff --git a/autogpt_platform/frontend/src/services/feature-flags/FeatureFlagPage.tsx b/autogpt_platform/frontend/src/services/feature-flags/FeatureFlagPage.tsx new file mode 100644 index 0000000000..eef0691de2 --- /dev/null +++ b/autogpt_platform/frontend/src/services/feature-flags/FeatureFlagPage.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; +import { useLDClient } from "launchdarkly-react-client-sdk"; +import { useRouter } from "next/navigation"; +import { ReactNode, useEffect, useState } from "react"; +import { environment } from "../environment"; +import { Flag, useGetFlag } from "./use-get-flag"; + +interface FeatureFlagRedirectProps { + flag: Flag; + whenDisabled: string; + children: ReactNode; +} + +export function FeatureFlagPage({ + flag, + whenDisabled, + children, +}: FeatureFlagRedirectProps) { + const [isLoading, setIsLoading] = useState(true); + const router = useRouter(); + const flagValue = useGetFlag(flag); + const ldClient = useLDClient(); + const ldEnabled = environment.areFeatureFlagsEnabled(); + const ldReady = Boolean(ldClient); + const flagEnabled = Boolean(flagValue); + + useEffect(() => { + const initialize = async () => { + if (!ldEnabled) { + router.replace(whenDisabled); + setIsLoading(false); + return; + } + + // Wait for LaunchDarkly to initialize when enabled to prevent race conditions + if (ldEnabled && !ldReady) return; + + try { + await ldClient?.waitForInitialization(); + if (!flagEnabled) router.replace(whenDisabled); + } catch (error) { + console.error(error); + router.replace(whenDisabled); + } finally { + setIsLoading(false); + } + }; + + initialize(); + }, [ldReady, flagEnabled]); + + return isLoading || !flagEnabled ? ( + + ) : ( + <>{children} + ); +} diff --git a/autogpt_platform/frontend/src/services/feature-flags/FeatureFlagRedirect.tsx b/autogpt_platform/frontend/src/services/feature-flags/FeatureFlagRedirect.tsx new file mode 100644 index 0000000000..b843b5567c --- /dev/null +++ b/autogpt_platform/frontend/src/services/feature-flags/FeatureFlagRedirect.tsx @@ -0,0 +1,51 @@ +"use client"; + +import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; +import { useLDClient } from "launchdarkly-react-client-sdk"; +import { useRouter } from "next/navigation"; +import { useEffect } from "react"; +import { environment } from "../environment"; +import { Flag, useGetFlag } from "./use-get-flag"; + +interface FeatureFlagRedirectProps { + flag: Flag; + whenEnabled: string; + whenDisabled: string; +} + +export function FeatureFlagRedirect({ + flag, + whenEnabled, + whenDisabled, +}: FeatureFlagRedirectProps) { + const router = useRouter(); + const flagValue = useGetFlag(flag); + const ldEnabled = environment.areFeatureFlagsEnabled(); + const ldClient = useLDClient(); + const ldReady = Boolean(ldClient); + const flagEnabled = Boolean(flagValue); + + useEffect(() => { + const initialize = async () => { + if (!ldEnabled) { + router.replace(whenDisabled); + return; + } + + // Wait for LaunchDarkly to initialize when enabled to prevent race conditions + if (ldEnabled && !ldReady) return; + + try { + await ldClient?.waitForInitialization(); + router.replace(flagEnabled ? whenEnabled : whenDisabled); + } catch (error) { + console.error(error); + router.replace(whenDisabled); + } + }; + + initialize(); + }, [ldReady, flagEnabled]); + + return ; +} diff --git a/autogpt_platform/frontend/src/services/feature-flags/feature-flag-provider.tsx b/autogpt_platform/frontend/src/services/feature-flags/feature-flag-provider.tsx index 47e4bd738a..8b78f4c589 100644 --- a/autogpt_platform/frontend/src/services/feature-flags/feature-flag-provider.tsx +++ b/autogpt_platform/frontend/src/services/feature-flags/feature-flag-provider.tsx @@ -7,14 +7,12 @@ import type { ReactNode } from "react"; import { useMemo } from "react"; import { environment } from "../environment"; -const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID; -const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true"; const LAUNCHDARKLY_INIT_TIMEOUT_MS = 5000; export function LaunchDarklyProvider({ children }: { children: ReactNode }) { const { user, isUserLoading } = useSupabase(); - const isCloud = environment.isCloud(); - const isLaunchDarklyConfigured = isCloud && envEnabled && clientId; + const envEnabled = environment.areFeatureFlagsEnabled(); + const clientId = environment.getLaunchDarklyClientId(); const context = useMemo(() => { if (isUserLoading || !user) { @@ -36,7 +34,7 @@ export function LaunchDarklyProvider({ children }: { children: ReactNode }) { }; }, [user, isUserLoading]); - if (!isLaunchDarklyConfigured) { + if (!envEnabled) { return <>{children}; } @@ -44,7 +42,7 @@ export function LaunchDarklyProvider({ children }: { children: ReactNode }) { (flag: T): FlagValues[T] | null { +type FlagValues = typeof defaultFlags; + +export function useGetFlag(flag: T): FlagValues[T] { const currentFlags = useFlags(); const flagValue = currentFlags[flag]; + const areFlagsEnabled = environment.areFeatureFlagsEnabled(); - const envEnabled = process.env.NEXT_PUBLIC_LAUNCHDARKLY_ENABLED === "true"; - const clientId = process.env.NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_ID; - const isLaunchDarklyConfigured = envEnabled && Boolean(clientId); - - if (!isLaunchDarklyConfigured || isPwMockEnabled) { - return mockFlags[flag]; + if (!areFlagsEnabled || isPwMockEnabled) { + return defaultFlags[flag]; } - return flagValue ?? mockFlags[flag]; + return flagValue ?? defaultFlags[flag]; } diff --git a/classic/frontend/.gitignore b/classic/frontend/.gitignore index 036283f834..eb060615c5 100644 --- a/classic/frontend/.gitignore +++ b/classic/frontend/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +.next/ migrate_working_dir/ # IntelliJ related