Skip to content

Commit e1e0fb7

Browse files
authored
fix(frontend): post login/signup/onboarding redirect clash (#11382)
## Changes 🏗️ ### Issue 1: login/signup redirect conflict There are 2 hooks, both on the login and signup pages, that attempt to call `router.push` once a user logs in or is created. The main offender seems to be this hook: ```tsx useEffect(() => { if (user) router.push("/"); }, [user]); ``` Which is in place on both pages to prevent logged-in users from accessing `/login` or `/signup`. What happens is when a user signs up or logs in, if they need onboarding, there is a `router.push` down the line to redirect them there, which conflicts with the one done in this hook. **Solution** I moved the logic from that hook to the `middleware.ts`, which is a better place for it... It won't conflict anymore with onboarding redirects done in those pages ### Issue 2: onboarding server redirects Potential race condition: both the server component and the client `<OnboardingProvider />` perform redirects. The server component redirects happen first, but if onboarding state changes after mount, the provider can redirect again, causing rapid mount/unmount cycles. **Solution** Make all onboarding redirects central in `/onboarding` which is now a client component do in client redirects only and displaying a spinner while it does so. ## 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] Tested locally login/logout/signup and trying to access `/login` and `/signup` being logged in
1 parent a054740 commit e1e0fb7

File tree

12 files changed

+160
-90
lines changed

12 files changed

+160
-90
lines changed

autogpt_platform/frontend/src/app/(no-navbar)/onboarding/4-agent/page.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,22 @@ import { useBackendAPI } from "@/lib/autogpt-server-api/context";
1212
import { StoreAgentDetails } from "@/lib/autogpt-server-api";
1313
import { isEmptyOrWhitespace } from "@/lib/utils";
1414
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
15-
import { finishOnboarding } from "../6-congrats/actions";
15+
import router from "next/router";
1616

1717
export default function Page() {
18-
const { state, updateState } = useOnboarding(4, "INTEGRATIONS");
18+
const { state, updateState, completeStep } = useOnboarding(4, "INTEGRATIONS");
1919
const [agents, setAgents] = useState<StoreAgentDetails[]>([]);
2020
const api = useBackendAPI();
2121

2222
useEffect(() => {
2323
api.getOnboardingAgents().then((agents) => {
2424
if (agents.length < 2) {
25-
finishOnboarding();
25+
completeStep("CONGRATS");
26+
router.push("/onboarding");
2627
}
27-
2828
setAgents(agents);
2929
});
30-
}, [api, setAgents]);
30+
}, []);
3131

3232
useEffect(() => {
3333
// Deselect agent if it's not in the list of agents

autogpt_platform/frontend/src/app/(no-navbar)/onboarding/6-congrats/actions.ts

Lines changed: 0 additions & 18 deletions
This file was deleted.

autogpt_platform/frontend/src/app/(no-navbar)/onboarding/6-congrats/page.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"use client";
22
import { useEffect, useRef, useState } from "react";
33
import { cn } from "@/lib/utils";
4-
import { finishOnboarding } from "./actions";
4+
import { useRouter } from "next/navigation";
55
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
66
import * as party from "party-js";
77

88
export default function Page() {
99
const { completeStep } = useOnboarding(7, "AGENT_INPUT");
10+
const router = useRouter();
1011
const [showText, setShowText] = useState(false);
1112
const [showSubtext, setShowSubtext] = useState(false);
1213
const divRef = useRef(null);
@@ -31,16 +32,17 @@ export default function Page() {
3132
}, 500);
3233

3334
const timer2 = setTimeout(() => {
35+
// Mark CONGRATS as complete - /onboarding page will handle adding agent to library and redirect
3436
completeStep("CONGRATS");
35-
finishOnboarding();
37+
router.push("/onboarding");
3638
}, 3000);
3739

3840
return () => {
3941
clearTimeout(timer0);
4042
clearTimeout(timer1);
4143
clearTimeout(timer2);
4244
};
43-
}, []);
45+
}, [completeStep, router]);
4446

4547
return (
4648
<div className="flex h-screen w-screen flex-col items-center justify-center bg-violet-100">
Lines changed: 89 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,90 @@
1-
import BackendAPI from "@/lib/autogpt-server-api";
2-
import { redirect } from "next/navigation";
3-
import { finishOnboarding } from "./6-congrats/actions";
4-
import { shouldShowOnboarding } from "@/app/api/helpers";
5-
6-
// Force dynamic rendering to avoid static generation issues with cookies
7-
export const dynamic = "force-dynamic";
8-
9-
export default async function OnboardingPage() {
10-
const api = new BackendAPI();
11-
const isOnboardingEnabled = await shouldShowOnboarding();
12-
13-
if (!isOnboardingEnabled) {
14-
redirect("/marketplace");
15-
}
16-
17-
const onboarding = await api.getUserOnboarding();
18-
19-
// CONGRATS is the last step in intro onboarding
20-
if (onboarding.completedSteps.includes("GET_RESULTS"))
21-
redirect("/marketplace");
22-
else if (onboarding.completedSteps.includes("CONGRATS")) finishOnboarding();
23-
else if (onboarding.completedSteps.includes("AGENT_INPUT"))
24-
redirect("/onboarding/5-run");
25-
else if (onboarding.completedSteps.includes("AGENT_NEW_RUN"))
26-
redirect("/onboarding/5-run");
27-
else if (onboarding.completedSteps.includes("AGENT_CHOICE"))
28-
redirect("/onboarding/5-run");
29-
else if (onboarding.completedSteps.includes("INTEGRATIONS"))
30-
redirect("/onboarding/4-agent");
31-
else if (onboarding.completedSteps.includes("USAGE_REASON"))
32-
redirect("/onboarding/3-services");
33-
else if (onboarding.completedSteps.includes("WELCOME"))
34-
redirect("/onboarding/2-reason");
35-
36-
redirect("/onboarding/1-welcome");
1+
"use client";
2+
import { useEffect } from "react";
3+
import { useRouter } from "next/navigation";
4+
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
5+
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
6+
7+
export default function OnboardingPage() {
8+
const router = useRouter();
9+
const api = useBackendAPI();
10+
11+
useEffect(() => {
12+
async function redirectToStep() {
13+
try {
14+
// Check if onboarding is enabled
15+
const isEnabled = await api.isOnboardingEnabled();
16+
if (!isEnabled) {
17+
router.push("/");
18+
return;
19+
}
20+
21+
const onboarding = await api.getUserOnboarding();
22+
23+
// Handle completed onboarding
24+
if (onboarding.completedSteps.includes("GET_RESULTS")) {
25+
router.push("/");
26+
return;
27+
}
28+
29+
// Handle CONGRATS - add agent to library and redirect
30+
if (onboarding.completedSteps.includes("CONGRATS")) {
31+
if (onboarding.selectedStoreListingVersionId) {
32+
try {
33+
const libraryAgent = await api.addMarketplaceAgentToLibrary(
34+
onboarding.selectedStoreListingVersionId,
35+
);
36+
router.push(`/library/agents/${libraryAgent.id}`);
37+
} catch (error) {
38+
console.error("Failed to add agent to library:", error);
39+
router.push("/library");
40+
}
41+
} else {
42+
router.push("/library");
43+
}
44+
return;
45+
}
46+
47+
// Redirect to appropriate step based on completed steps
48+
if (onboarding.completedSteps.includes("AGENT_INPUT")) {
49+
router.push("/onboarding/5-run");
50+
return;
51+
}
52+
53+
if (onboarding.completedSteps.includes("AGENT_NEW_RUN")) {
54+
router.push("/onboarding/5-run");
55+
return;
56+
}
57+
58+
if (onboarding.completedSteps.includes("AGENT_CHOICE")) {
59+
router.push("/onboarding/5-run");
60+
return;
61+
}
62+
63+
if (onboarding.completedSteps.includes("INTEGRATIONS")) {
64+
router.push("/onboarding/4-agent");
65+
return;
66+
}
67+
68+
if (onboarding.completedSteps.includes("USAGE_REASON")) {
69+
router.push("/onboarding/3-services");
70+
return;
71+
}
72+
73+
if (onboarding.completedSteps.includes("WELCOME")) {
74+
router.push("/onboarding/2-reason");
75+
return;
76+
}
77+
78+
// Default: redirect to first step
79+
router.push("/onboarding/1-welcome");
80+
} catch (error) {
81+
console.error("Failed to determine onboarding step:", error);
82+
router.push("/");
83+
}
84+
}
85+
86+
redirectToStep();
87+
}, [api, router]);
88+
89+
return <LoadingSpinner size="large" cover />;
3790
}

autogpt_platform/frontend/src/app/(no-navbar)/onboarding/reset/page.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
import { postV1ResetOnboardingProgress } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
33
import { LoadingSpinner } from "@/components/atoms/LoadingSpinner/LoadingSpinner";
44
import { useToast } from "@/components/molecules/Toast/use-toast";
5-
import { redirect } from "next/navigation";
5+
import { useRouter } from "next/navigation";
66
import { useEffect } from "react";
77

88
export default function OnboardingResetPage() {
99
const { toast } = useToast();
10+
const router = useRouter();
1011

1112
useEffect(() => {
1213
postV1ResetOnboardingProgress()
@@ -17,7 +18,7 @@ export default function OnboardingResetPage() {
1718
variant: "success",
1819
});
1920

20-
redirect("/onboarding/1-welcome");
21+
router.push("/onboarding");
2122
})
2223
.catch(() => {
2324
toast({
@@ -26,7 +27,7 @@ export default function OnboardingResetPage() {
2627
variant: "destructive",
2728
});
2829
});
29-
}, []);
30+
}, [toast, router]);
3031

3132
return <LoadingSpinner cover />;
3233
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function computeReturnURL(returnUrl: string | null, result: any) {
2+
return returnUrl
3+
? returnUrl
4+
: (result?.next as string) || (result?.onboarding ? "/onboarding" : "/");
5+
}

autogpt_platform/frontend/src/app/(platform)/login/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export default function LoginPage() {
2020
turnstile,
2121
captchaKey,
2222
isLoading,
23+
isRedirecting,
2324
isCloudEnv,
2425
isUserLoading,
2526
isGoogleLoading,
@@ -30,7 +31,7 @@ export default function LoginPage() {
3031
handleCloseNotAllowedModal,
3132
} = useLoginPage();
3233

33-
if (isUserLoading || user) {
34+
if (isUserLoading || user || isRedirecting) {
3435
return <LoadingLogin />;
3536
}
3637

autogpt_platform/frontend/src/app/(platform)/login/useLoginPage.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@ import { environment } from "@/services/environment";
55
import { loginFormSchema, LoginProvider } from "@/types/auth";
66
import { zodResolver } from "@hookform/resolvers/zod";
77
import { useRouter, useSearchParams } from "next/navigation";
8-
import { useCallback, useEffect, useState } from "react";
8+
import { useCallback, useState } from "react";
99
import { useForm } from "react-hook-form";
1010
import z from "zod";
11+
import { computeReturnURL } from "./helpers";
1112

1213
export function useLoginPage() {
1314
const { supabase, user, isUserLoading } = useSupabase();
@@ -20,6 +21,7 @@ export function useLoginPage() {
2021
const [isLoading, setIsLoading] = useState(false);
2122
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
2223
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
24+
const [isRedirecting, setIsRedirecting] = useState(false);
2325
const isCloudEnv = environment.isCloud();
2426
const isVercelPreview = process.env.NEXT_PUBLIC_VERCEL_ENV === "preview";
2527

@@ -42,10 +44,6 @@ export function useLoginPage() {
4244
turnstile.reset();
4345
}, [turnstile]);
4446

45-
useEffect(() => {
46-
if (user) router.push("/");
47-
}, [user]);
48-
4947
async function handleProviderLogin(provider: LoginProvider) {
5048
setIsGoogleLoading(true);
5149

@@ -80,7 +78,10 @@ export function useLoginPage() {
8078
}
8179

8280
const { url } = await response.json();
83-
if (url) window.location.href = url as string;
81+
if (url) {
82+
setIsRedirecting(true);
83+
window.location.href = url as string;
84+
}
8485
} catch (error) {
8586
resetCaptcha();
8687
setIsGoogleLoading(false);
@@ -143,11 +144,11 @@ export function useLoginPage() {
143144
setFeedback(null);
144145

145146
// Prioritize returnUrl from query params over backend's onboarding logic
146-
const next = returnUrl
147-
? returnUrl
148-
: (result?.next as string) ||
149-
(result?.onboarding ? "/onboarding" : "/");
150-
if (next) router.push(next);
147+
const next = computeReturnURL(returnUrl, result);
148+
if (next) {
149+
setIsRedirecting(true);
150+
router.push(next);
151+
}
151152
} catch (error) {
152153
toast({
153154
title:
@@ -169,6 +170,7 @@ export function useLoginPage() {
169170
captchaKey,
170171
user,
171172
isLoading,
173+
isRedirecting,
172174
isCloudEnv,
173175
isUserLoading,
174176
isGoogleLoading,

autogpt_platform/frontend/src/app/(platform)/signup/page.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export default function SignupPage() {
3232
isLoading,
3333
isCloudEnv,
3434
isUserLoading,
35+
isRedirecting,
3536
isGoogleLoading,
3637
showNotAllowedModal,
3738
isSupabaseAvailable,
@@ -40,7 +41,7 @@ export default function SignupPage() {
4041
handleCloseNotAllowedModal,
4142
} = useSignupPage();
4243

43-
if (isUserLoading || isLoggedIn) {
44+
if (isUserLoading || isLoggedIn || isRedirecting) {
4445
return <LoadingSignup />;
4546
}
4647

0 commit comments

Comments
 (0)