dx(frontend): make preview deploys work + minor improvements (#11329)

## Changes 🏗️

Make sure we can login on preview deployments generated by Vercel to
test Front-end changes. As of now, the Cloudflare CAPTCHA verification
fails, we don't need to have it active there.

### Minor improvements

<img width="1599" height="755" alt="Screenshot 2025-11-06 at 16 18 10"
src="https://github.com/user-attachments/assets/0a3fb1f3-2d4d-49fe-885f-10f141dc0ce4"
/>

Prevent the following build error:
```
15:58:01.507
     at j (.next/server/app/(no-navbar)/onboarding/reset/page.js:1:5125)
15:58:01.507
     at <unknown> (.next/server/chunks/5826.js:2:14221)
15:58:01.507
     at b.handleCallbackErrors (.next/server/chunks/5826.js:43:43068)
15:58:01.507
     at <unknown> (.next/server/chunks/5826.js:2:14194) {
15:58:01.507
   description: "Route /onboarding/reset couldn't be rendered statically because it used `cookies`. See more info here: https://nextjs.org/docs/messages/dynamic-server-error",
15:58:01.507
   digest: 'DYNAMIC_SERVER_USAGE'
15:58:01.507
 }
 ```
by making the reset onboarding route a client one. I made a new component, `<LoadingSpinner />`, and that page will show it while onboarding it's being reset.

## 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] You can login/signup on the app and use it in the preview URL generated by Vercel
This commit is contained in:
Ubbe
2025-11-07 21:03:57 +07:00
committed by GitHub
parent 5559d978d7
commit dfed092869
7 changed files with 172 additions and 11 deletions

View File

@@ -1,7 +0,0 @@
import { postV1ResetOnboardingProgress } from "@/app/api/__generated__/endpoints/onboarding/onboarding";
import { redirect } from "next/navigation";
export default async function OnboardingResetPage() {
await postV1ResetOnboardingProgress();
redirect("/onboarding/1-welcome");
}

View File

@@ -0,0 +1,32 @@
"use client";
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 { useEffect } from "react";
export default function OnboardingResetPage() {
const { toast } = useToast();
useEffect(() => {
postV1ResetOnboardingProgress()
.then(() => {
toast({
title: "Onboarding reset successfully",
description: "You can now start the onboarding process again",
variant: "success",
});
redirect("/onboarding/1-welcome");
})
.catch(() => {
toast({
title: "Failed to reset onboarding",
description: "Please try again later",
variant: "destructive",
});
});
}, []);
return <LoadingSpinner cover />;
}

View File

@@ -1,6 +1,7 @@
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";
@@ -26,7 +27,7 @@ export async function POST(request: Request) {
// Verify Turnstile token if provided
const captchaOk = await verifyTurnstileToken(turnstileToken ?? "", "login");
if (!captchaOk) {
if (!captchaOk && !environment.isVercelPreview()) {
return NextResponse.json(
{ error: "CAPTCHA verification failed. Please try again." },
{ status: 400 },

View File

@@ -1,8 +1,9 @@
import { NextResponse } from "next/server";
import * as Sentry from "@sentry/nextjs";
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";
@@ -31,7 +32,7 @@ export async function POST(request: Request) {
"signup",
);
if (!captchaOk) {
if (!captchaOk && !environment.isVercelPreview()) {
return NextResponse.json(
{ error: "CAPTCHA verification failed. Please try again." },
{ status: 400 },

View File

@@ -0,0 +1,86 @@
import type { Meta, StoryObj } from "@storybook/nextjs";
import { LoadingSpinner } from "./LoadingSpinner";
const meta: Meta<typeof LoadingSpinner> = {
title: "Atoms/LoadingSpinner",
component: LoadingSpinner,
tags: ["autodocs"],
parameters: {
layout: "centered",
docs: {
description: {
component:
"Animated loading indicator using the Phosphor CircleNotch icon. Provide a `size` prop or custom classes to fit different contexts.",
},
},
},
argTypes: {
size: {
control: "select",
options: ["small", "medium", "large"],
description: "Spinner size preset",
},
className: {
control: "text",
description: "Additional CSS classes to customize color or layout",
},
},
args: {
size: "medium",
className: "text-indigo-500",
role: "status",
"aria-label": "loading",
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const Small: Story = {
args: {
size: "small",
},
};
export const Large: Story = {
args: {
size: "large",
},
};
export const CustomColor: Story = {
args: {
className: "text-emerald-500",
},
};
export const Cover: Story = {
args: {
cover: true,
},
};
export const AllSizes: Story = {
render: renderAllSizes,
};
function renderAllSizes() {
return (
<div className="flex items-center gap-8 text-indigo-500">
<div className="flex flex-col items-center gap-2">
<LoadingSpinner size="small" aria-label="loading-small" />
<span className="text-xs capitalize text-zinc-500">Small</span>
</div>
<div className="flex flex-col items-center gap-2">
<LoadingSpinner size="medium" aria-label="loading-medium" />
<span className="text-xs capitalize text-zinc-500">Medium</span>
</div>
<div className="flex flex-col items-center gap-2">
<LoadingSpinner size="large" aria-label="loading-large" />
<span className="text-xs capitalize text-zinc-500">Large</span>
</div>
</div>
);
}

View File

@@ -0,0 +1,43 @@
import { cn } from "@/lib/utils";
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
import React from "react";
const sizeClassNameMap = {
small: "h-4 w-4",
medium: "h-6 w-6",
large: "h-10 w-10",
} as const;
type SpinnerSize = keyof typeof sizeClassNameMap;
type LoadingSpinnerProps = {
size?: SpinnerSize;
className?: string;
cover?: boolean;
} & React.ComponentPropsWithoutRef<typeof CircleNotchIcon>;
export function LoadingSpinner(props: LoadingSpinnerProps) {
const { size = "medium", className, cover = false, ...restProps } = props;
const spinner = (
<CircleNotchIcon
className={cn(
"animate-spin text-inherit",
sizeClassNameMap[size],
className,
)}
weight="bold"
{...restProps}
/>
);
if (cover) {
return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
{spinner}
</div>
);
}
return spinner;
}

View File

@@ -84,6 +84,10 @@ function isClientSide() {
return typeof window !== "undefined";
}
function isVercelPreview() {
return process.env.VERCEL_ENV === "preview";
}
function isCAPTCHAEnabled() {
return process.env.NEXT_PUBLIC_TURNSTILE === "enabled";
}
@@ -110,6 +114,7 @@ export const environment = {
isDev,
isCloud,
isLocal,
isVercelPreview,
isCAPTCHAEnabled,
areFeatureFlagsEnabled,
};