Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Upgrade to nextjs 15 #249

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
16 changes: 8 additions & 8 deletions apps/unsubscriber/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,19 @@
},
"devDependencies": {
"@types/dotenv": "^8.2.3",
"@types/node": "22.7.9",
"playwright": "^1.48.1",
"tsx": "^4.19.1",
"@types/node": "22.8.4",
"playwright": "^1.48.2",
"tsx": "^4.19.2",
"typescript": "5.6.3"
},
"dependencies": {
"@ai-sdk/amazon-bedrock": "^0.0.30",
"@ai-sdk/anthropic": "^0.0.51",
"@ai-sdk/google": "^0.0.52",
"@ai-sdk/openai": "^0.0.68",
"@ai-sdk/amazon-bedrock": "^0.0.33",
"@ai-sdk/anthropic": "^0.0.53",
"@ai-sdk/google": "^0.0.55",
"@ai-sdk/openai": "^0.0.70",
"@fastify/cors": "^10.0.1",
"@t3-oss/env-core": "^0.11.1",
"ai": "^3.4.18",
"ai": "^3.4.27",
"dotenv": "^16.4.5",
"fastify": "^5.0.0",
"zod": "^3.23.8"
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(app)/automation/BulkRunRules.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function BulkRunRules() {
const [startDate, setStartDate] = useState<Date | undefined>();
const [endDate, setEndDate] = useState<Date | undefined>();

const abortRef = useRef<() => void>();
const abortRef = useRef<() => void>(undefined);

return (
<div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
import { use } from "react";

import useSWR from "swr";
import groupBy from "lodash/groupBy";
Expand All @@ -9,11 +10,10 @@ import { LoadingContent } from "@/components/LoadingContent";

export const dynamic = "force-dynamic";

export default function RuleExamplesPage({
params,
}: {
params: { groupId: string };
export default function RuleExamplesPage(props: {
params: Promise<{ groupId: string }>;
}) {
const params = use(props.params);
const { data, isLoading, error } = useSWR<GroupEmailsResponse>(
`/api/user/group/${params.groupId}/messages`,
);
Expand Down
6 changes: 5 additions & 1 deletion apps/web/app/(app)/automation/group/[groupId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
"use client";
import { use } from "react";

import { useRouter } from "next/navigation";
import { ViewGroup } from "@/app/(app)/automation/group/ViewGroup";
import { Container } from "@/components/Container";

export default function GroupPage({ params }: { params: { groupId: string } }) {
export default function GroupPage(props: {
params: Promise<{ groupId: string }>;
}) {
const params = use(props.params);
const router = useRouter();

return (
Expand Down
8 changes: 4 additions & 4 deletions apps/web/app/(app)/automation/rule/[ruleId]/examples/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
import { use } from "react";

import Link from "next/link";
import useSWR from "swr";
Expand All @@ -11,11 +12,10 @@ import { LoadingContent } from "@/components/LoadingContent";

export const dynamic = "force-dynamic";

export default function RuleExamplesPage({
params,
}: {
params: { ruleId: string };
export default function RuleExamplesPage(props: {
params: Promise<{ ruleId: string }>;
}) {
const params = use(props.params);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Add error handling for use hook in the following files:

  • apps/web/app/(app)/automation/group/[groupId]/page.tsx
  • apps/web/app/(app)/automation/rule/[ruleId]/examples/page.tsx
  • apps/web/app/(app)/automation/group/[groupId]/examples/page.tsx
🔗 Analysis chain

Consider adding error handling for use hook.

The use hook is correctly applied to unwrap the Promise-based params. However, consider adding error handling to manage potential Promise rejection scenarios. This could be achieved by wrapping the component in an error boundary or using a try-catch block.

To ensure this change is consistent across the codebase, let's check for similar usage:

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Check for consistent usage of the `use` hook with Promise-based params
rg -A 3 "const params = use\(props\.params\)"

Length of output: 1164

const { data, isLoading, error } = useSWR<ExamplesResponse>(
`/api/user/rules/${params.ruleId}/example`,
);
Expand Down
11 changes: 5 additions & 6 deletions apps/web/app/(app)/automation/rule/[ruleId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@ import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { RuleForm } from "@/app/(app)/automation/RuleForm";
import { TopSection } from "@/components/TopSection";

export default async function RulePage({
params,
searchParams,
}: {
params: { ruleId: string };
searchParams: { new: string };
export default async function RulePage(props: {
params: Promise<{ ruleId: string }>;
searchParams: Promise<{ new: string }>;
}) {
const searchParams = await props.searchParams;
const params = await props.params;
const session = await auth();
if (!session?.user) redirect("/login");

Expand Down
7 changes: 3 additions & 4 deletions apps/web/app/(app)/automation/rule/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import { RuleForm } from "@/app/(app)/automation/RuleForm";
import { examples } from "@/app/(app)/automation/create/examples";
import type { RuleType } from "@prisma/client";

export default function CreateRulePage({
searchParams,
}: {
searchParams: { example?: string; groupId?: string; tab?: RuleType };
export default async function CreateRulePage(props: {
searchParams: Promise<{ example?: string; groupId?: string; tab?: RuleType }>;
}) {
const searchParams = await props.searchParams;
const rule =
searchParams.example &&
examples[Number.parseInt(searchParams.example)].rule;
Expand Down
5 changes: 4 additions & 1 deletion apps/web/app/(app)/compose/ComposeEmailFormLazy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import { Loading } from "@/components/Loading";

// keep bundle size down by importing dynamically on use
export const ComposeEmailFormLazy = dynamic(
() => import("./ComposeEmailForm").then((mod) => mod.ComposeEmailForm),
() =>
import("./ComposeEmailForm").then((mod) => ({
default: mod.ComposeEmailForm,
})),
{
loading: () => <Loading />,
},
Expand Down
9 changes: 4 additions & 5 deletions apps/web/app/(app)/license/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useCallback } from "react";
import { useCallback, use } from "react";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Cannot use the use() hook in client components

The use() hook is intended for server components to unwrap promises during rendering. Since this is a client component ("use client";), using use() is not supported and will result in an error. To handle asynchronous data in client components, you should ensure that searchParams is resolved before being passed in or manage the data fetching within the component using effects.

Apply this diff to fix the issue:

- import { useCallback, use } from "react";
+ import { useCallback } from "react";

...

- export default function LicensePage(props: {
-   searchParams: Promise<{ "license-key"?: string }>;
+ export default function LicensePage(props: {
+   searchParams: { "license-key"?: string };
  }) {

...

-   const searchParams = use(props.searchParams);
+   const { searchParams } = props;

Also applies to: 15-16, 18-18

import { type SubmitHandler, useForm } from "react-hook-form";
import { Button } from "@/components/Button";
import { Input } from "@/components/Input";
Expand All @@ -12,11 +12,10 @@ import { useUser } from "@/hooks/useUser";

type Inputs = { licenseKey: string };

export default function LicensePage({
searchParams,
}: {
searchParams: { "license-key"?: string };
export default function LicensePage(props: {
searchParams: Promise<{ "license-key"?: string }>;
}) {
const searchParams = use(props.searchParams);
const licenseKey = searchParams["license-key"];

const { data } = useUser();
Expand Down
9 changes: 4 additions & 5 deletions apps/web/app/(app)/mail/page.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useCallback, useEffect } from "react";
import { useCallback, useEffect, use } from "react";
import useSWRInfinite from "swr/infinite";
import { useSetAtom } from "jotai";
import { List } from "@/components/email-list/EmailList";
Expand All @@ -12,11 +12,10 @@ import { BetaBanner } from "@/app/(app)/mail/BetaBanner";
import { ClientOnly } from "@/components/ClientOnly";
import { PermissionsCheck } from "@/app/(app)/PermissionsCheck";

export default function Mail({
searchParams,
}: {
searchParams: { type?: string };
export default function Mail(props: {
searchParams: Promise<{ type?: string }>;
}) {
const searchParams = use(props.searchParams);
const query: ThreadsQuery = searchParams.type
? { type: searchParams.type }
: {};
Expand Down
7 changes: 3 additions & 4 deletions apps/web/app/(app)/onboarding/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,10 @@ import { LoadStats } from "@/providers/StatLoaderProvider";

export const maxDuration = 120;

export default function OnboardingPage({
searchParams,
}: {
searchParams: { step?: string };
export default async function OnboardingPage(props: {
searchParams: Promise<{ step?: string }>;
}) {
const searchParams = await props.searchParams;
const step = searchParams.step
? Number.parseInt(searchParams.step)
: undefined;
Expand Down
10 changes: 6 additions & 4 deletions apps/web/app/(app)/simple/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import { getMessage } from "@/utils/gmail/message";

export const dynamic = "force-dynamic";

export default async function SimplePage({
searchParams: { pageToken, type = "IMPORTANT" },
}: {
searchParams: { pageToken?: string; type?: string };
export default async function SimplePage(props: {
searchParams: Promise<{ pageToken?: string; type?: string }>;
Comment on lines +18 to +19
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Incorrect handling of searchParams as a Promise

In Next.js, searchParams is a plain object and does not need to be awaited. Declaring searchParams as a Promise and using await is unnecessary and may cause unexpected behavior.

Apply this diff to correct the function signature and usage:

-export default async function SimplePage(props: {
-  searchParams: Promise<{ pageToken?: string; type?: string }>;
-}) {
-  const searchParams = await props.searchParams;
+export default async function SimplePage({ searchParams }: {
+  searchParams: { pageToken?: string; type?: string };
+}) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default async function SimplePage(props: {
searchParams: Promise<{ pageToken?: string; type?: string }>;
export default async function SimplePage({ searchParams }: {
searchParams: { pageToken?: string; type?: string };
}) {

}) {
const searchParams = await props.searchParams;

const { pageToken, type = "IMPORTANT" } = searchParams;

const session = await auth();
const email = session?.user.email;
if (!email) throw new Error("Not authenticated");
Expand Down
7 changes: 3 additions & 4 deletions apps/web/app/(landing)/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ export const metadata: Metadata = {
alternates: { canonical: "/login" },
};

export default async function AuthenticationPage({
searchParams,
}: {
searchParams?: Record<string, string>;
export default async function AuthenticationPage(props: {
searchParams?: Promise<Record<string, string>>;
}) {
const searchParams = await props.searchParams;
const session = await auth();
if (session?.user.email && !searchParams?.error) {
if (searchParams?.next) {
Expand Down
7 changes: 3 additions & 4 deletions apps/web/app/(landing)/welcome/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@ export const metadata: Metadata = {
alternates: { canonical: "/welcome" },
};

export default async function WelcomePage({
searchParams,
}: {
searchParams: { question?: string; force?: boolean };
export default async function WelcomePage(props: {
searchParams: Promise<{ question?: string; force?: boolean }>;
}) {
const searchParams = await props.searchParams;
const session = await auth();

if (!session?.user.email) redirect("/login");
Expand Down
2 changes: 1 addition & 1 deletion apps/web/app/(landing)/welcome/utms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { cookies } from "next/headers";
import prisma from "@/utils/prisma";

async function storeUtms(userId: string) {
const cookieStore = cookies();
const cookieStore = await cookies();
const utmCampaign = cookieStore.get("utm_campaign");
const utmMedium = cookieStore.get("utm_medium");
const utmSource = cookieStore.get("utm_source");
Expand Down
35 changes: 0 additions & 35 deletions apps/web/app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1 @@
export { GET, POST } from "./auth";
// export const runtime = "edge" // optional

// This code was used in the past to reask for consent when signing in with Google.
// This doesn't happen often, but I'm keeping it here for now just in case we decide to put it back.
// This code worked with Next Auth v4. We've since moved to v5.

// import NextAuth from "next-auth";
// import { authOptions, getAuthOptions } from "@/utils/auth";

// export const dynamic = "force-dynamic";

// // https://next-auth.js.org/configuration/initialization#advanced-initialization
// async function handler(
// request: Request,
// context: { params?: { nextauth?: string[] } }
// ) {
// let authOpts = authOptions;

// if (
// request.method === "POST" &&
// context.params?.nextauth?.[0] === "signin" &&
// context.params.nextauth[1] === "google"
// ) {
// const clonedRequest = request.clone();
// const formData = await clonedRequest.formData();
// const requestConsent = formData.get("consent") === "true";

// authOpts = getAuthOptions({ consent: requestConsent });
// }

// // can remove `as any` once this is fixed: https://github.com/nextauthjs/next-auth/issues/8120
// return await NextAuth(request as any, context as any, authOpts);
// }

// export { handler as GET, handler as POST };
17 changes: 11 additions & 6 deletions apps/web/app/api/user/complete-registration/route.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type NextRequest, NextResponse } from "next/server";
import { cookies, headers } from "next/headers";
import { cookies, headers, type UnsafeUnwrappedHeaders } from "next/headers";
import { auth } from "@/app/api/auth/[...nextauth]/auth";
import { withError } from "@/utils/middleware";
import { sendCompleteRegistrationEvent } from "@/utils/fb";
Expand All @@ -14,11 +14,11 @@ export const POST = withError(async (_request: NextRequest) => {
if (!session?.user.email)
return NextResponse.json({ error: "Not authenticated" });

const eventSourceUrl = headers().get("referer");
const userAgent = headers().get("user-agent");
const eventSourceUrl = (await headers()).get("referer");
const userAgent = (await headers()).get("user-agent");
const ip = getIp();

const c = cookies();
const c = await cookies();

const fbc = c.get("_fbc")?.value;
const fbp = c.get("_fbp")?.value;
Expand All @@ -44,13 +44,18 @@ export const POST = withError(async (_request: NextRequest) => {

function getIp() {
const FALLBACK_IP_ADDRESS = "0.0.0.0";
const forwardedFor = headers().get("x-forwarded-for");
const forwardedFor = (headers() as unknown as UnsafeUnwrappedHeaders).get(
"x-forwarded-for",
);

if (forwardedFor) {
return forwardedFor.split(",")[0] ?? FALLBACK_IP_ADDRESS;
}

return headers().get("x-real-ip") ?? FALLBACK_IP_ADDRESS;
return (
(headers() as unknown as UnsafeUnwrappedHeaders).get("x-real-ip") ??
FALLBACK_IP_ADDRESS
);
}

async function storePosthogSignupEvent(userId: string, email: string) {
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/api/v1/group/[groupId]/emails/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import {

export async function GET(
request: NextRequest,
{ params }: { params: { groupId: string } },
props: { params: Promise<{ groupId: string }> },
) {
const params = await props.params;
const { groupId } = params;
const { searchParams } = new URL(request.url);
const queryResult = groupEmailsQuerySchema.safeParse(
Expand Down
8 changes: 5 additions & 3 deletions apps/web/app/blog/post/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,14 @@ export async function generateStaticParams() {
}

type Props = {
params: { slug: string };
params: Promise<{ slug: string }>;
};

export async function generateMetadata(
{ params }: Props,
props: Props,
parent: ResolvingMetadata,
) {
const params = await props.params;
const post = await sanityFetch<PostType | undefined>({
query: postQuery,
params,
Expand Down Expand Up @@ -62,7 +63,8 @@ export async function generateMetadata(

// Multiple versions of this page will be statically generated
// using the `params` returned by `generateStaticParams`
export default async function Page({ params }: Props) {
export default async function Page(props: Props) {
const params = await props.params;
const post = await sanityFetch<PostType>({ query: postQuery, params });

return <Post post={post} />;
Expand Down
5 changes: 2 additions & 3 deletions apps/web/instrumentation.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
/* eslint-disable no-process-env */
import * as Sentry from "@sentry/nextjs";
import { env } from "process";

export function register() {
if (process.env.NEXT_RUNTIME === "nodejs") {
// this is your Sentry.init call from `sentry.server.config.js|ts`
Sentry.init({
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
Expand All @@ -19,7 +18,7 @@ export function register() {
// This is your Sentry.init call from `sentry.edge.config.js|ts`
if (process.env.NEXT_RUNTIME === "edge") {
Sentry.init({
dsn: env.NEXT_PUBLIC_SENTRY_DSN,
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Adjust this value in production, or use tracesSampler for greater control
tracesSampleRate: 1,
// Setting this option to true will print useful information to the console while you're setting up Sentry.
Expand Down
Loading