fix(frontend): onboarding fixes... (2)

This commit is contained in:
Lluis Agusti
2025-11-15 00:58:13 +07:00
parent 81d61a0c94
commit 9977144b3d
10 changed files with 216 additions and 217 deletions

View File

@@ -1,13 +1,15 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import { cn } from "@/lib/utils";
import { useRouter } from "next/navigation";
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
import * as party from "party-js";
import { useEffect, useRef, useState } from "react";
import { useOnboarding } from "../../../../providers/onboarding/onboarding-provider";
export default function Page() {
const { completeStep } = useOnboarding(7, "AGENT_INPUT");
const router = useRouter();
const api = useBackendAPI();
const [showText, setShowText] = useState(false);
const [showSubtext, setShowSubtext] = useState(false);
const divRef = useRef(null);
@@ -31,10 +33,28 @@ export default function Page() {
setShowSubtext(true);
}, 500);
const timer2 = setTimeout(() => {
// Mark CONGRATS as complete - /onboarding page will handle adding agent to library and redirect
const timer2 = setTimeout(async () => {
completeStep("CONGRATS");
router.push("/onboarding");
try {
const onboarding = await api.getUserOnboarding();
if (onboarding?.selectedStoreListingVersionId) {
try {
const libraryAgent = await api.addMarketplaceAgentToLibrary(
onboarding.selectedStoreListingVersionId,
);
router.replace(`/library/agents/${libraryAgent.id}`);
} catch (error) {
console.error("Failed to add agent to library:", error);
router.replace("/library");
}
} else {
router.replace("/library");
}
} catch (error) {
console.error("Failed to get onboarding data:", error);
router.replace("/library");
}
}, 3000);
return () => {
@@ -42,7 +62,7 @@ export default function Page() {
clearTimeout(timer1);
clearTimeout(timer2);
};
}, [completeStep, router]);
}, [completeStep, router, api]);
return (
<div className="flex h-screen w-screen flex-col items-center justify-center bg-violet-100">

View File

@@ -26,24 +26,6 @@ export default function OnboardingPage() {
return;
}
// 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.replace(`/library/agents/${libraryAgent.id}`);
} catch (error) {
console.error("Failed to add agent to library:", error);
router.replace("/library");
}
} else {
router.replace("/library");
}
return;
}
// Redirect to appropriate step based on completed steps
if (onboarding.completedSteps.includes("AGENT_INPUT")) {
router.push("/onboarding/5-run");

View File

@@ -0,0 +1,67 @@
"use server";
import BackendAPI from "@/lib/autogpt-server-api";
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { verifyTurnstileToken } from "@/lib/turnstile";
import { environment } from "@/services/environment";
import { loginFormSchema } from "@/types/auth";
import * as Sentry from "@sentry/nextjs";
import { shouldShowOnboarding } from "../../api/helpers";
export async function login(
email: string,
password: string,
turnstileToken?: string,
) {
try {
const parsed = loginFormSchema.safeParse({ email, password });
if (!parsed.success) {
return {
success: false,
error: "Invalid email or password",
};
}
const captchaOk = await verifyTurnstileToken(turnstileToken ?? "", "login");
if (!captchaOk && !environment.isVercelPreview()) {
return {
success: false,
error: "CAPTCHA verification failed. Please try again.",
};
}
const supabase = await getServerSupabase();
if (!supabase) {
return {
success: false,
error: "Authentication service unavailable",
};
}
const { error } = await supabase.auth.signInWithPassword(parsed.data);
if (error) {
return {
success: false,
error: error.message,
};
}
const api = new BackendAPI();
await api.createUser();
const onboarding = await shouldShowOnboarding();
return {
success: true,
onboarding,
next: onboarding ? "/onboarding" : "/",
};
} catch (err) {
Sentry.captureException(err);
return {
success: false,
error: "Failed to login. Please try again.",
};
}
}

View File

@@ -20,6 +20,7 @@ export default function LoginPage() {
turnstile,
captchaKey,
isLoading,
isGoogleLoading,
isCloudEnv,
isUserLoading,
showNotAllowedModal,
@@ -100,6 +101,7 @@ export default function LoginPage() {
<Button
variant="primary"
loading={isLoading}
disabled={isGoogleLoading}
type="submit"
className="mt-6 w-full"
>
@@ -109,7 +111,7 @@ export default function LoginPage() {
{isCloudEnv ? (
<GoogleOAuthButton
onClick={() => handleProviderLogin("google")}
isLoading={isLoading}
isLoading={isGoogleLoading}
disabled={isLoading}
/>
) : null}

View File

@@ -8,6 +8,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { useCallback, useState } from "react";
import { useForm } from "react-hook-form";
import z from "zod";
import { login as loginAction } from "./actions";
import { computeReturnURL } from "./helpers";
export function useLoginPage() {
@@ -19,6 +20,7 @@ export function useLoginPage() {
const returnUrl = searchParams.get("returnUrl");
const { toast } = useToast();
const [isLoading, setIsLoading] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
const isCloudEnv = environment.isCloud();
const isVercelPreview = process.env.NEXT_PUBLIC_VERCEL_ENV === "preview";
@@ -43,7 +45,7 @@ export function useLoginPage() {
}, [turnstile]);
async function handleProviderLogin(provider: LoginProvider) {
setIsLoading(true);
setIsGoogleLoading(true);
if (isCloudEnv && !turnstile.verified && !isVercelPreview) {
toast({
@@ -51,7 +53,7 @@ export function useLoginPage() {
variant: "info",
});
setIsLoading(false);
setIsGoogleLoading(false);
resetCaptcha();
return;
}
@@ -72,7 +74,7 @@ export function useLoginPage() {
if (url) window.location.href = url as string;
} catch (error) {
resetCaptcha();
setIsLoading(false);
setIsGoogleLoading(false);
setFeedback(
error instanceof Error ? error.message : "Failed to start OAuth flow",
);
@@ -105,20 +107,14 @@ export function useLoginPage() {
}
try {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: data.email,
password: data.password,
turnstileToken: turnstile.token,
}),
});
const result = await loginAction(
data.email,
data.password,
turnstile.token ?? undefined,
);
const result = await response.json();
if (!response.ok) {
throw new Error(result?.error || "Login failed");
if (!result.success) {
throw new Error(result.error || "Login failed");
}
// Prioritize returnUrl from query params over backend's onboarding logic
@@ -145,6 +141,7 @@ export function useLoginPage() {
captchaKey,
user,
isLoading,
isGoogleLoading,
isCloudEnv,
isUserLoading,
showNotAllowedModal,

View File

@@ -0,0 +1,86 @@
"use server";
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { verifyTurnstileToken } from "@/lib/turnstile";
import { environment } from "@/services/environment";
import { signupFormSchema } from "@/types/auth";
import * as Sentry from "@sentry/nextjs";
import { isWaitlistError, logWaitlistError } from "../../api/auth/utils";
import { shouldShowOnboarding } from "../../api/helpers";
export async function signup(
email: string,
password: string,
confirmPassword: string,
agreeToTerms: boolean,
turnstileToken?: string,
) {
try {
const parsed = signupFormSchema.safeParse({
email,
password,
confirmPassword,
agreeToTerms,
});
if (!parsed.success) {
return {
success: false,
error: "Invalid signup payload",
};
}
const captchaOk = await verifyTurnstileToken(
turnstileToken ?? "",
"signup",
);
if (!captchaOk && !environment.isVercelPreview()) {
return {
success: false,
error: "CAPTCHA verification failed. Please try again.",
};
}
const supabase = await getServerSupabase();
if (!supabase) {
return {
success: false,
error: "Authentication service unavailable",
};
}
const { data, error } = await supabase.auth.signUp(parsed.data);
if (error) {
if (isWaitlistError(error?.code, error?.message)) {
logWaitlistError("Signup", error.message);
return { success: false, error: "not_allowed" };
}
if ((error as any).code === "user_already_exists") {
return { success: false, error: "user_already_exists" };
}
return {
success: false,
error: error.message,
};
}
if (data.session) {
await supabase.auth.setSession(data.session);
}
const isOnboardingEnabled = await shouldShowOnboarding();
const next = isOnboardingEnabled ? "/onboarding" : "/";
return { success: true, next };
} catch (err) {
Sentry.captureException(err);
return {
success: false,
error: "Failed to sign up. Please try again.",
};
}
}

View File

@@ -30,6 +30,7 @@ export default function SignupPage() {
captchaKey,
isLoggedIn,
isLoading,
isGoogleLoading,
isCloudEnv,
isUserLoading,
showNotAllowedModal,
@@ -177,6 +178,7 @@ export default function SignupPage() {
<Button
variant="primary"
loading={isLoading}
disabled={isGoogleLoading}
type="submit"
className="mt-6 w-full"
>
@@ -187,7 +189,7 @@ export default function SignupPage() {
{isCloudEnv ? (
<GoogleOAuthButton
onClick={() => handleProviderSignup("google")}
isLoading={isLoading}
isLoading={isGoogleLoading}
disabled={isLoading}
/>
) : null}

View File

@@ -8,6 +8,7 @@ import { useRouter } from "next/navigation";
import { useCallback, useState } from "react";
import { useForm } from "react-hook-form";
import z from "zod";
import { signup as signupAction } from "./actions";
export function useSignupPage() {
const { supabase, user, isUserLoading } = useSupabase();
@@ -16,8 +17,8 @@ export function useSignupPage() {
const { toast } = useToast();
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
const [showNotAllowedModal, setShowNotAllowedModal] = useState(false);
const [isCreatingAccount, setIsCreatingAccount] = useState(false);
const isCloudEnv = environment.isCloud();
const isVercelPreview = process.env.NEXT_PUBLIC_VERCEL_ENV === "preview";
@@ -43,21 +44,19 @@ export function useSignupPage() {
});
async function handleProviderSignup(provider: LoginProvider) {
setIsLoading(true);
setIsGoogleLoading(true);
if (isCloudEnv && !turnstile.verified && !isVercelPreview) {
toast({
title: "Please complete the CAPTCHA challenge.",
variant: "default",
});
setIsLoading(false);
setIsGoogleLoading(false);
resetCaptcha();
return;
}
try {
setIsCreatingAccount(true);
const response = await fetch("/api/auth/provider", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -78,7 +77,7 @@ export function useSignupPage() {
const { url } = await response.json();
if (url) window.location.href = url as string;
} catch (error) {
setIsLoading(false);
setIsGoogleLoading(false);
resetCaptcha();
toast({
title:
@@ -114,34 +113,29 @@ export function useSignupPage() {
}
try {
const response = await fetch("/api/auth/signup", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
email: data.email,
password: data.password,
confirmPassword: data.confirmPassword,
agreeToTerms: data.agreeToTerms,
turnstileToken: turnstile.token,
}),
});
const result = await signupAction(
data.email,
data.password,
data.confirmPassword,
data.agreeToTerms,
turnstile.token ?? undefined,
);
const result = await response.json();
setIsLoading(false);
if (!response.ok) {
if (result?.error === "user_already_exists") {
if (!result.success) {
if (result.error === "user_already_exists") {
setFeedback("User with this email already exists");
turnstile.reset();
return;
}
if (result?.error === "not_allowed") {
if (result.error === "not_allowed") {
setShowNotAllowedModal(true);
return;
}
toast({
title: result?.error || "Signup failed",
title: result.error || "Signup failed",
variant: "destructive",
});
resetCaptcha();
@@ -149,11 +143,10 @@ export function useSignupPage() {
return;
}
const next = result?.next || "/";
const next = result.next || "/";
if (next) router.replace(next);
} catch (error) {
setIsLoading(false);
setIsCreatingAccount(false);
toast({
title:
error instanceof Error
@@ -173,7 +166,7 @@ export function useSignupPage() {
captchaKey,
isLoggedIn: !!user,
isLoading,
isCreatingAccount,
isGoogleLoading,
isCloudEnv,
isUserLoading,
showNotAllowedModal,

View File

@@ -1,67 +0,0 @@
import BackendAPI from "@/lib/autogpt-server-api";
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { verifyTurnstileToken } from "@/lib/turnstile";
import { environment } from "@/services/environment";
import { loginFormSchema } from "@/types/auth";
import * as Sentry from "@sentry/nextjs";
import { NextResponse } from "next/server";
import { shouldShowOnboarding } from "../../helpers";
export async function POST(request: Request) {
try {
const body = await request.json();
const parsed = loginFormSchema.safeParse({
email: body?.email,
password: body?.password,
});
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid email or password" },
{ status: 400 },
);
}
const turnstileToken: string | undefined = body?.turnstileToken;
// Verify Turnstile token if provided
const captchaOk = await verifyTurnstileToken(turnstileToken ?? "", "login");
if (!captchaOk && !environment.isVercelPreview()) {
return NextResponse.json(
{ error: "CAPTCHA verification failed. Please try again." },
{ status: 400 },
);
}
const supabase = await getServerSupabase();
if (!supabase) {
return NextResponse.json(
{ error: "Authentication service unavailable" },
{ status: 500 },
);
}
const { error } = await supabase.auth.signInWithPassword(parsed.data);
if (error) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
const api = new BackendAPI();
await api.createUser();
const onboarding = await shouldShowOnboarding();
return NextResponse.json({
success: true,
onboarding,
next: onboarding ? "/onboarding" : "/",
});
} catch (err) {
Sentry.captureException(err);
return NextResponse.json(
{ error: "Failed to login. Please try again." },
{ status: 500 },
);
}
}

View File

@@ -1,83 +0,0 @@
import { getServerSupabase } from "@/lib/supabase/server/getServerSupabase";
import { verifyTurnstileToken } from "@/lib/turnstile";
import { environment } from "@/services/environment";
import { signupFormSchema } from "@/types/auth";
import * as Sentry from "@sentry/nextjs";
import { NextResponse } from "next/server";
import { shouldShowOnboarding } from "../../helpers";
import { isWaitlistError, logWaitlistError } from "../utils";
export async function POST(request: Request) {
try {
const body = await request.json();
const parsed = signupFormSchema.safeParse({
email: body?.email,
password: body?.password,
confirmPassword: body?.confirmPassword,
agreeToTerms: body?.agreeToTerms,
});
if (!parsed.success) {
return NextResponse.json(
{ error: "Invalid signup payload" },
{ status: 400 },
);
}
const turnstileToken: string | undefined = body?.turnstileToken;
const captchaOk = await verifyTurnstileToken(
turnstileToken ?? "",
"signup",
);
if (!captchaOk && !environment.isVercelPreview()) {
return NextResponse.json(
{ error: "CAPTCHA verification failed. Please try again." },
{ status: 400 },
);
}
const supabase = await getServerSupabase();
if (!supabase) {
return NextResponse.json(
{ error: "Authentication service unavailable" },
{ status: 500 },
);
}
const { data, error } = await supabase.auth.signUp(parsed.data);
if (error) {
if (isWaitlistError(error?.code, error?.message)) {
logWaitlistError("Signup", error.message);
return NextResponse.json({ error: "not_allowed" }, { status: 403 });
}
if ((error as any).code === "user_already_exists") {
return NextResponse.json(
{ error: "user_already_exists" },
{ status: 409 },
);
}
return NextResponse.json({ error: error.message }, { status: 400 });
}
if (data.session) {
await supabase.auth.setSession(data.session);
}
const isOnboardingEnabled = await shouldShowOnboarding();
const next = isOnboardingEnabled ? "/onboarding" : "/";
return NextResponse.json({ success: true, next });
} catch (err) {
Sentry.captureException(err);
return NextResponse.json(
{ error: "Failed to sign up. Please try again." },
{ status: 500 },
);
}
}