diff --git a/apps/web/app/(use-page-wrapper)/(main-nav)/event-types/page.tsx b/apps/web/app/(use-page-wrapper)/(main-nav)/event-types/page.tsx index 2a8187a624..e74d324a19 100644 --- a/apps/web/app/(use-page-wrapper)/(main-nav)/event-types/page.tsx +++ b/apps/web/app/(use-page-wrapper)/(main-nav)/event-types/page.tsx @@ -6,6 +6,7 @@ import { unstable_cache } from "next/cache"; import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; +import { checkOnboardingRedirect } from "@calcom/features/auth/lib/onboardingUtils"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery"; import { eventTypesRouter } from "@calcom/trpc/server/routers/viewer/eventTypes/_router"; @@ -53,6 +54,17 @@ const Page = async ({ searchParams }: PageProps) => { return redirect("/auth/login"); } + // Check if user needs onboarding and redirect before fetching event types data + // Use organizationId from session profile if available to avoid extra query + const organizationId = session.user.profile?.organizationId ?? null; + const onboardingPath = await checkOnboardingRedirect(session.user.id, { + checkEmailVerification: true, + organizationId, + }); + if (onboardingPath) { + return redirect(onboardingPath); + } + const t = await getTranslate(); const filters = getTeamsFiltersFromQuery(_searchParams); const userEventGroupsData = await getCachedEventGroups(_headers, _cookies, filters); diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 95d99df6d9..f9731ef029 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,6 +1,7 @@ import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; +import { checkOnboardingRedirect } from "@calcom/features/auth/lib/onboardingUtils"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import { buildLegacyRequest } from "@lib/buildLegacyCtx"; @@ -11,6 +12,17 @@ const RedirectPage = async () => { if (!session?.user?.id) { redirect("/auth/login"); } + + // Check if user needs onboarding and redirect before going to event-types + const organizationId = session.user.profile?.organizationId ?? null; + const onboardingPath = await checkOnboardingRedirect(session.user.id, { + checkEmailVerification: true, + organizationId, + }); + if (onboardingPath) { + redirect(onboardingPath); + } + redirect("/event-types"); }; diff --git a/packages/features/auth/lib/onboardingUtils.ts b/packages/features/auth/lib/onboardingUtils.ts new file mode 100644 index 0000000000..80d092e68f --- /dev/null +++ b/packages/features/auth/lib/onboardingUtils.ts @@ -0,0 +1,68 @@ +import dayjs from "@calcom/dayjs"; +import { FeaturesRepository } from "@calcom/features/flags/features.repository"; +import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository"; +import { UserRepository } from "@calcom/features/users/repositories/UserRepository"; +import { prisma } from "@calcom/prisma"; + +const ONBOARDING_INTRODUCED_AT = dayjs("September 1 2021").toISOString(); + +/** + * Checks if a user needs onboarding on the server side. + * Returns the onboarding path if redirect is needed, null otherwise. + * + * @param userId - The user ID to check + * @param options - Optional configuration + * @param options.checkEmailVerification - Whether to check if email verification is required + * @param options.organizationId - Optional organizationId from session (to avoid extra query) + */ +export async function checkOnboardingRedirect( + userId: number, + options?: { + checkEmailVerification?: boolean; + organizationId?: number | null; + } +): Promise { + // Query user data needed for onboarding check using UserRepository + const userRepository = new UserRepository(prisma); + const user = await userRepository.findById({ id: userId }); + + if (!user) { + return null; + } + + // Use provided organizationId or query it via ProfileRepository + let organizationId: number | null; + if (options?.organizationId !== undefined) { + organizationId = options.organizationId; + } else { + const profile = await ProfileRepository.findFirstForUserId({ userId }); + organizationId = profile?.organizationId ?? null; + } + + // Check if user should be shown onboarding + const shouldShowOnboarding = + !user.completedOnboarding && !organizationId && dayjs(user.createdDate).isAfter(ONBOARDING_INTRODUCED_AT); + + if (!shouldShowOnboarding) { + return null; + } + + // Check email verification if needed + const featuresRepository = new FeaturesRepository(prisma); + + if (options?.checkEmailVerification) { + const emailVerificationEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally( + "email-verification" + ); + + if (!user.emailVerified && user.identityProvider === "CAL" && emailVerificationEnabled) { + // User needs email verification, redirect to verification page + return "/auth/verify-email"; + } + } + + // Determine which onboarding path to use + const onboardingV3Enabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("onboarding-v3"); + + return onboardingV3Enabled ? "/onboarding/getting-started" : "/getting-started"; +}