diff --git a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/4-agent/page.tsx b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/4-agent/page.tsx index be874675b1..bd289225b1 100644 --- a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/4-agent/page.tsx +++ b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/4-agent/page.tsx @@ -12,22 +12,22 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context"; import { StoreAgentDetails } from "@/lib/autogpt-server-api"; import { isEmptyOrWhitespace } from "@/lib/utils"; import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider"; -import { finishOnboarding } from "../6-congrats/actions"; +import router from "next/router"; export default function Page() { - const { state, updateState } = useOnboarding(4, "INTEGRATIONS"); + const { state, updateState, completeStep } = useOnboarding(4, "INTEGRATIONS"); const [agents, setAgents] = useState([]); const api = useBackendAPI(); useEffect(() => { api.getOnboardingAgents().then((agents) => { if (agents.length < 2) { - finishOnboarding(); + completeStep("CONGRATS"); + router.push("/onboarding"); } - setAgents(agents); }); - }, [api, setAgents]); + }, []); useEffect(() => { // Deselect agent if it's not in the list of agents diff --git a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/6-congrats/actions.ts b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/6-congrats/actions.ts deleted file mode 100644 index 202bad57bd..0000000000 --- a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/6-congrats/actions.ts +++ /dev/null @@ -1,18 +0,0 @@ -"use server"; -import BackendAPI from "@/lib/autogpt-server-api"; -import { revalidatePath } from "next/cache"; -import { redirect } from "next/navigation"; - -export async function finishOnboarding() { - const api = new BackendAPI(); - const onboarding = await api.getUserOnboarding(); - const listingId = onboarding?.selectedStoreListingVersionId; - if (listingId) { - const libraryAgent = await api.addMarketplaceAgentToLibrary(listingId); - revalidatePath(`/library/agents/${libraryAgent.id}`, "layout"); - redirect(`/library/agents/${libraryAgent.id}`); - } else { - revalidatePath("/library", "layout"); - redirect("/library"); - } -} diff --git a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/6-congrats/page.tsx b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/6-congrats/page.tsx index 0cac60c6f0..6a383460c9 100644 --- a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/6-congrats/page.tsx +++ b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/6-congrats/page.tsx @@ -1,12 +1,13 @@ "use client"; import { useEffect, useRef, useState } from "react"; import { cn } from "@/lib/utils"; -import { finishOnboarding } from "./actions"; +import { useRouter } from "next/navigation"; import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider"; import * as party from "party-js"; export default function Page() { const { completeStep } = useOnboarding(7, "AGENT_INPUT"); + const router = useRouter(); const [showText, setShowText] = useState(false); const [showSubtext, setShowSubtext] = useState(false); const divRef = useRef(null); @@ -31,8 +32,9 @@ export default function Page() { }, 500); const timer2 = setTimeout(() => { + // Mark CONGRATS as complete - /onboarding page will handle adding agent to library and redirect completeStep("CONGRATS"); - finishOnboarding(); + router.push("/onboarding"); }, 3000); return () => { @@ -40,7 +42,7 @@ export default function Page() { clearTimeout(timer1); clearTimeout(timer2); }; - }, []); + }, [completeStep, router]); return (
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 c1e3bf0540..2d47fbfd7b 100644 --- a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/page.tsx +++ b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/page.tsx @@ -1,37 +1,90 @@ -import BackendAPI from "@/lib/autogpt-server-api"; -import { redirect } from "next/navigation"; -import { finishOnboarding } from "./6-congrats/actions"; -import { shouldShowOnboarding } from "@/app/api/helpers"; +"use client"; +import { useEffect } from "react"; +import { useRouter } from "next/navigation"; +import { useBackendAPI } from "@/lib/autogpt-server-api/context"; +import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; -// Force dynamic rendering to avoid static generation issues with cookies -export const dynamic = "force-dynamic"; +export default function OnboardingPage() { + const router = useRouter(); + const api = useBackendAPI(); -export default async function OnboardingPage() { - const api = new BackendAPI(); - const isOnboardingEnabled = await shouldShowOnboarding(); + useEffect(() => { + async function redirectToStep() { + try { + // Check if onboarding is enabled + const isEnabled = await api.isOnboardingEnabled(); + if (!isEnabled) { + router.push("/"); + return; + } - if (!isOnboardingEnabled) { - redirect("/marketplace"); - } + const onboarding = await api.getUserOnboarding(); - const onboarding = await api.getUserOnboarding(); + // Handle completed onboarding + if (onboarding.completedSteps.includes("GET_RESULTS")) { + router.push("/"); + return; + } - // CONGRATS is the last step in intro onboarding - if (onboarding.completedSteps.includes("GET_RESULTS")) - redirect("/marketplace"); - else if (onboarding.completedSteps.includes("CONGRATS")) finishOnboarding(); - else if (onboarding.completedSteps.includes("AGENT_INPUT")) - redirect("/onboarding/5-run"); - else if (onboarding.completedSteps.includes("AGENT_NEW_RUN")) - redirect("/onboarding/5-run"); - else if (onboarding.completedSteps.includes("AGENT_CHOICE")) - redirect("/onboarding/5-run"); - else if (onboarding.completedSteps.includes("INTEGRATIONS")) - redirect("/onboarding/4-agent"); - else if (onboarding.completedSteps.includes("USAGE_REASON")) - redirect("/onboarding/3-services"); - else if (onboarding.completedSteps.includes("WELCOME")) - redirect("/onboarding/2-reason"); + // Handle CONGRATS - add agent to library and redirect + if (onboarding.completedSteps.includes("CONGRATS")) { + if (onboarding.selectedStoreListingVersionId) { + try { + const libraryAgent = await api.addMarketplaceAgentToLibrary( + onboarding.selectedStoreListingVersionId, + ); + router.push(`/library/agents/${libraryAgent.id}`); + } catch (error) { + console.error("Failed to add agent to library:", error); + router.push("/library"); + } + } else { + router.push("/library"); + } + return; + } - redirect("/onboarding/1-welcome"); + // Redirect to appropriate step based on completed steps + if (onboarding.completedSteps.includes("AGENT_INPUT")) { + router.push("/onboarding/5-run"); + return; + } + + if (onboarding.completedSteps.includes("AGENT_NEW_RUN")) { + router.push("/onboarding/5-run"); + return; + } + + if (onboarding.completedSteps.includes("AGENT_CHOICE")) { + router.push("/onboarding/5-run"); + return; + } + + if (onboarding.completedSteps.includes("INTEGRATIONS")) { + router.push("/onboarding/4-agent"); + return; + } + + if (onboarding.completedSteps.includes("USAGE_REASON")) { + router.push("/onboarding/3-services"); + return; + } + + if (onboarding.completedSteps.includes("WELCOME")) { + router.push("/onboarding/2-reason"); + return; + } + + // Default: redirect to first step + router.push("/onboarding/1-welcome"); + } catch (error) { + console.error("Failed to determine onboarding step:", error); + router.push("/"); + } + } + + redirectToStep(); + }, [api, router]); + + return ; } diff --git a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/reset/page.tsx b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/reset/page.tsx index 873c719a06..0113e67c17 100644 --- a/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/reset/page.tsx +++ b/autogpt_platform/frontend/src/app/(no-navbar)/onboarding/reset/page.tsx @@ -2,11 +2,12 @@ import { postV1ResetOnboardingProgress } from "@/app/api/__generated__/endpoints/onboarding/onboarding"; import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner"; import { useToast } from "@/components/molecules/Toast/use-toast"; -import { redirect } from "next/navigation"; +import { useRouter } from "next/navigation"; import { useEffect } from "react"; export default function OnboardingResetPage() { const { toast } = useToast(); + const router = useRouter(); useEffect(() => { postV1ResetOnboardingProgress() @@ -17,7 +18,7 @@ export default function OnboardingResetPage() { variant: "success", }); - redirect("/onboarding/1-welcome"); + router.push("/onboarding"); }) .catch(() => { toast({ @@ -26,7 +27,7 @@ export default function OnboardingResetPage() { variant: "destructive", }); }); - }, []); + }, [toast, router]); return ; } diff --git a/autogpt_platform/frontend/src/app/(platform)/login/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/login/helpers.ts new file mode 100644 index 0000000000..0eb0b1910f --- /dev/null +++ b/autogpt_platform/frontend/src/app/(platform)/login/helpers.ts @@ -0,0 +1,5 @@ +export function computeReturnURL(returnUrl: string | null, result: any) { + return returnUrl + ? returnUrl + : (result?.next as string) || (result?.onboarding ? "/onboarding" : "/"); +} diff --git a/autogpt_platform/frontend/src/app/(platform)/login/page.tsx b/autogpt_platform/frontend/src/app/(platform)/login/page.tsx index 957ac78c93..802ea7f3ba 100644 --- a/autogpt_platform/frontend/src/app/(platform)/login/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/login/page.tsx @@ -20,6 +20,7 @@ export default function LoginPage() { turnstile, captchaKey, isLoading, + isRedirecting, isCloudEnv, isUserLoading, isGoogleLoading, @@ -30,7 +31,7 @@ export default function LoginPage() { handleCloseNotAllowedModal, } = useLoginPage(); - if (isUserLoading || user) { + if (isUserLoading || user || isRedirecting) { return ; } diff --git a/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts b/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts index 3f2c41f92e..b0771b9403 100644 --- a/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts @@ -5,9 +5,10 @@ import { environment } from "@/services/environment"; import { loginFormSchema, LoginProvider } from "@/types/auth"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter, useSearchParams } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import { useForm } from "react-hook-form"; import z from "zod"; +import { computeReturnURL } from "./helpers"; export function useLoginPage() { const { supabase, user, isUserLoading } = useSupabase(); @@ -20,6 +21,7 @@ export function useLoginPage() { const [isLoading, setIsLoading] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [showNotAllowedModal, setShowNotAllowedModal] = useState(false); + const [isRedirecting, setIsRedirecting] = useState(false); const isCloudEnv = environment.isCloud(); const isVercelPreview = process.env.NEXT_PUBLIC_VERCEL_ENV === "preview"; @@ -42,10 +44,6 @@ export function useLoginPage() { turnstile.reset(); }, [turnstile]); - useEffect(() => { - if (user) router.push("/"); - }, [user]); - async function handleProviderLogin(provider: LoginProvider) { setIsGoogleLoading(true); @@ -80,7 +78,10 @@ export function useLoginPage() { } const { url } = await response.json(); - if (url) window.location.href = url as string; + if (url) { + setIsRedirecting(true); + window.location.href = url as string; + } } catch (error) { resetCaptcha(); setIsGoogleLoading(false); @@ -143,11 +144,11 @@ export function useLoginPage() { setFeedback(null); // Prioritize returnUrl from query params over backend's onboarding logic - const next = returnUrl - ? returnUrl - : (result?.next as string) || - (result?.onboarding ? "/onboarding" : "/"); - if (next) router.push(next); + const next = computeReturnURL(returnUrl, result); + if (next) { + setIsRedirecting(true); + router.push(next); + } } catch (error) { toast({ title: @@ -169,6 +170,7 @@ export function useLoginPage() { captchaKey, user, isLoading, + isRedirecting, isCloudEnv, isUserLoading, isGoogleLoading, diff --git a/autogpt_platform/frontend/src/app/(platform)/signup/page.tsx b/autogpt_platform/frontend/src/app/(platform)/signup/page.tsx index dff878436d..97cba646f1 100644 --- a/autogpt_platform/frontend/src/app/(platform)/signup/page.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/signup/page.tsx @@ -32,6 +32,7 @@ export default function SignupPage() { isLoading, isCloudEnv, isUserLoading, + isRedirecting, isGoogleLoading, showNotAllowedModal, isSupabaseAvailable, @@ -40,7 +41,7 @@ export default function SignupPage() { handleCloseNotAllowedModal, } = useSignupPage(); - if (isUserLoading || isLoggedIn) { + if (isUserLoading || isLoggedIn || isRedirecting) { return ; } diff --git a/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts b/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts index b7ec88c3b5..d7302f54a5 100644 --- a/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts +++ b/autogpt_platform/frontend/src/app/(platform)/signup/useSignupPage.ts @@ -3,7 +3,7 @@ import { useSupabase } from "@/lib/supabase/hooks/useSupabase"; import { LoginProvider, signupFormSchema } from "@/types/auth"; import { zodResolver } from "@hookform/resolvers/zod"; import { useRouter } from "next/navigation"; -import { useCallback, useEffect, useState } from "react"; +import { useCallback, useState } from "react"; import { useForm } from "react-hook-form"; import z from "zod"; import { useToast } from "@/components/molecules/Toast/use-toast"; @@ -18,7 +18,7 @@ export function useSignupPage() { const [isLoading, setIsLoading] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [showNotAllowedModal, setShowNotAllowedModal] = useState(false); - + const [isRedirecting, setIsRedirecting] = useState(false); const isCloudEnv = environment.isCloud(); const isVercelPreview = process.env.NEXT_PUBLIC_VERCEL_ENV === "preview"; @@ -43,10 +43,6 @@ export function useSignupPage() { }, }); - useEffect(() => { - if (user) router.push("/"); - }, [user]); - async function handleProviderSignup(provider: LoginProvider) { setIsGoogleLoading(true); @@ -85,8 +81,11 @@ export function useSignupPage() { } const { url } = await response.json(); - if (url) window.location.href = url as string; - setFeedback(null); + if (url) { + setIsRedirecting(true); + setFeedback(null); + window.location.href = url as string; + } } catch (error) { setIsGoogleLoading(false); resetCaptcha(); @@ -160,8 +159,12 @@ export function useSignupPage() { } setFeedback(null); - const next = (result?.next as string) || "/"; - router.push(next); + + const next = result?.next || "/"; + if (next) { + setIsRedirecting(true); + router.push(next); + } } catch (error) { setIsLoading(false); toast({ @@ -183,6 +186,7 @@ export function useSignupPage() { captchaKey, isLoggedIn: !!user, isLoading, + isRedirecting, isCloudEnv, isUserLoading, isGoogleLoading, diff --git a/autogpt_platform/frontend/src/lib/supabase/helpers.ts b/autogpt_platform/frontend/src/lib/supabase/helpers.ts index cf89d96724..a5870f571a 100644 --- a/autogpt_platform/frontend/src/lib/supabase/helpers.ts +++ b/autogpt_platform/frontend/src/lib/supabase/helpers.ts @@ -14,6 +14,8 @@ export const PROTECTED_PAGES = [ export const ADMIN_PAGES = ["/admin"] as const; +export const AUTHENTICATION_PAGES = ["/login", "/signup"] as const; + export function getCookieSettings(): Partial { return { secure: process.env.NODE_ENV === "production", @@ -31,6 +33,12 @@ export function isAdminPage(pathname: string): boolean { return ADMIN_PAGES.some((page) => pathname.startsWith(page)); } +export function isAuthenticationPage(pathname: string): boolean { + return AUTHENTICATION_PAGES.some( + (page) => pathname === page || pathname.startsWith(`${page}/`), + ); +} + export function shouldRedirectOnLogout(pathname: string): boolean { return isProtectedPage(pathname) || isAdminPage(pathname); } diff --git a/autogpt_platform/frontend/src/lib/supabase/middleware.ts b/autogpt_platform/frontend/src/lib/supabase/middleware.ts index 3ac0aeec17..760bb254af 100644 --- a/autogpt_platform/frontend/src/lib/supabase/middleware.ts +++ b/autogpt_platform/frontend/src/lib/supabase/middleware.ts @@ -1,6 +1,11 @@ import { createServerClient } from "@supabase/ssr"; import { NextResponse, type NextRequest } from "next/server"; -import { getCookieSettings, isAdminPage, isProtectedPage } from "./helpers"; +import { + getCookieSettings, + isAdminPage, + isAuthenticationPage, + isProtectedPage, +} from "./helpers"; import { environment } from "@/services/environment"; export async function updateSession(request: NextRequest) { @@ -62,7 +67,13 @@ export async function updateSession(request: NextRequest) { } } - // 2. Check if user is authenticated but lacks admin role when accessing admin pages + // 2. Check if user is authenticated but trying to access unauthenticated pages + if (user && isAuthenticationPage(pathname)) { + url.pathname = "/marketplace"; + return NextResponse.redirect(url); + } + + // 3. Check if user is authenticated but lacks admin role when accessing admin pages if (user && userRole !== "admin" && isAdminPage(pathname)) { url.pathname = "/marketplace"; return NextResponse.redirect(url);