chore: Onboarding path service plus redirects (#24679)

* i18n

* WIP icon stuff

* More icon work

* More gradient tuning

* Mvoe plan-icon to planicon.tsx

* Fix cubic suggestion

* Fix darkmode icon and gradients

* Fix type error

* Onboarding path service

* Update usages of onboarding path and getting started hook
This commit is contained in:
sean-brydon
2025-10-28 11:10:25 +00:00
committed by GitHub
parent 3418a25a85
commit fa884e8516
10 changed files with 72 additions and 16 deletions
+4 -1
View File
@@ -4,6 +4,7 @@ import { z } from "zod";
import dayjs from "@calcom/dayjs";
import { OrganizationRepository } from "@calcom/features/ee/organizations/repositories/OrganizationRepository";
import { StripeBillingService } from "@calcom/features/ee/billing/stripe-billing-service";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { IS_STRIPE_ENABLED } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
@@ -172,7 +173,9 @@ export async function handler(req: NextApiRequest, res: NextApiResponse) {
await moveUserToMatchingOrg({ email: user.email });
return res.redirect(`${WEBAPP_URL}/${hasCompletedOnboarding ? "/event-types" : "/getting-started"}`);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath(prisma);
return res.redirect(`${WEBAPP_URL}${hasCompletedOnboarding ? "/event-types" : gettingStartedPath}`);
}
export async function cleanUpVerificationTokens(id: number) {
+5 -2
View File
@@ -11,6 +11,7 @@ import useEmailVerifyCheck from "@calcom/trpc/react/hooks/useEmailVerifyCheck";
import { showToast } from "@calcom/ui/components/toast";
import { Button } from "@calcom/ui/components/button";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { useFlagMap } from "@calcom/features/flags/context/provider";
function VerifyEmailPage() {
const { data } = useEmailVerifyCheck();
@@ -18,13 +19,15 @@ function VerifyEmailPage() {
const router = useRouter();
const { t, isLocaleReady } = useLocale();
const mutation = trpc.viewer.auth.resendVerifyEmail.useMutation();
const flags = useFlagMap();
useEffect(() => {
if (data?.isVerified) {
router.replace("/getting-started");
const gettingStartedPath = flags["onboarding-v3"] ? "/onboarding/getting-started" : "/getting-started";
router.replace(gettingStartedPath);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data?.isVerified]);
}, [data?.isVerified, flags]);
if (!isLocaleReady) {
return null;
}
@@ -1,6 +1,7 @@
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useFlagMap } from "@calcom/features/flags/context/provider";
import { trpc } from "@calcom/trpc/react";
import type { OnboardingState } from "../store/onboarding-store";
@@ -8,6 +9,7 @@ import type { OnboardingState } from "../store/onboarding-store";
export function useCreateTeam() {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const flags = useFlagMap();
const createTeamMutation = trpc.viewer.teams.create.useMutation();
@@ -31,7 +33,11 @@ export function useCreateTeam() {
}
if (result.team) {
router.push("/getting-started");
// Not sure we need this flag check - keeping it here for safe keeping as this is called only from v3 onboarding flow
const gettingStartedPath = flags["onboarding-v3"]
? "/onboarding/getting-started"
: "/getting-started";
router.push(gettingStartedPath);
}
} catch (error) {
console.error("Failed to create team:", error);
@@ -4,6 +4,7 @@ import { useState } from "react";
import { CreationSource } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { showToast } from "@calcom/ui/components/toast";
import { useFlagMap } from "@calcom/features/flags/context/provider";
import type { OnboardingState } from "../store/onboarding-store";
@@ -11,6 +12,7 @@ export const useSubmitOnboarding = () => {
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const flags = useFlagMap();
const intentToCreateOrg = trpc.viewer.organizations.intentToCreateOrg.useMutation();
@@ -80,7 +82,8 @@ export const useSubmitOnboarding = () => {
showToast("Organization created successfully!", "success");
// TODO: after this redirect we need to hard refresh the page to see org
resetOnboarding();
router.push("/getting-started");
const gettingStartedPath = flags["onboarding-v3"] ? "/onboarding/getting-started" : "/getting-started";
router.push(gettingStartedPath);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to create organization";
setError(errorMessage);
@@ -7,6 +7,7 @@ import stripe from "@calcom/features/ee/payments/server/stripe";
import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { ssoTenantProduct } from "@calcom/features/ee/sso/lib/sso";
import { checkUsername } from "@calcom/features/profile/lib/checkUsername";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
@@ -18,7 +19,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
const providerParam = asStringOrNull(context.query.provider);
const emailParam = asStringOrNull(context.query.email);
const usernameParam = asStringOrNull(context.query.username);
const successDestination = `/getting-started${usernameParam ? `?username=${usernameParam}` : ""}`;
const successDestination = await OnboardingPathService.getGettingStartedPathWithParams(
prisma,
usernameParam ? { username: usernameParam } : undefined
);
if (!providerParam) {
throw new Error(`File is not named sso/[provider]`);
}
@@ -42,9 +42,10 @@ export function useRedirectToOnboardingIfNeeded() {
useEffect(() => {
if (canRedirect) {
router.replace("/getting-started");
const gettingStartedPath = flags["onboarding-v3"] ? "/onboarding/getting-started" : "/getting-started";
router.replace(gettingStartedPath);
}
}, [canRedirect, router]);
}, [canRedirect, router, flags]);
return {
isLoading,
@@ -7,6 +7,7 @@ import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepos
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
import { createAProfileForAnExistingUser } from "@calcom/features/profile/lib/createAProfileForAnExistingUser";
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { deleteDomain } from "@calcom/lib/domainManager/organization";
import logger from "@calcom/lib/logger";
@@ -76,7 +77,7 @@ export class TeamService {
if (!existingToken) throw new TRPCError({ code: "NOT_FOUND", message: "Invite token not found" });
return {
token: existingToken.token,
inviteLink: TeamService.buildInviteLink(existingToken.token, isOrganizationOrATeamInOrganization),
inviteLink: await TeamService.buildInviteLink(existingToken.token, isOrganizationOrATeamInOrganization),
};
}
@@ -93,14 +94,18 @@ export class TeamService {
return {
token,
inviteLink: TeamService.buildInviteLink(token, isOrganizationOrATeamInOrganization),
inviteLink: await TeamService.buildInviteLink(token, isOrganizationOrATeamInOrganization),
};
}
private static buildInviteLink(token: string, isOrgContext: boolean): string {
private static async buildInviteLink(token: string, isOrgContext: boolean): Promise<string> {
const teamInviteLink = `${WEBAPP_URL}/teams?token=${token}`;
const orgInviteLink = `${WEBAPP_URL}/signup?token=${token}&callbackUrl=/getting-started`;
return isOrgContext ? orgInviteLink : teamInviteLink;
if (!isOrgContext) {
return teamInviteLink;
}
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath(prisma);
const orgInviteLink = `${WEBAPP_URL}/signup?token=${token}&callbackUrl=${gettingStartedPath}`;
return orgInviteLink;
}
/**
* Deletes a team and all its associated data in a safe, transactional order.
@@ -0,0 +1,24 @@
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
import type { PrismaClient } from "@calcom/prisma";
export class OnboardingPathService {
static async getGettingStartedPath(prisma: PrismaClient): Promise<string> {
const featuresRepository = new FeaturesRepository(prisma);
const onboardingV3Enabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("onboarding-v3");
return onboardingV3Enabled ? "/onboarding/getting-started" : "/getting-started";
}
static async getGettingStartedPathWithParams(
prisma: PrismaClient,
queryParams?: Record<string, string>
): Promise<string> {
const basePath = await OnboardingPathService.getGettingStartedPath(prisma);
if (!queryParams || Object.keys(queryParams).length === 0) {
return basePath;
}
const params = new URLSearchParams(queryParams);
return `${basePath}?${params.toString()}`;
}
}
@@ -9,6 +9,7 @@ import { PermissionCheckService } from "@calcom/features/pbac/services/permissio
import { createAProfileForAnExistingUser } from "@calcom/features/profile/lib/createAProfileForAnExistingUser";
import { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { ENABLE_PROFILE_SWITCHER, WEBAPP_URL } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
@@ -503,12 +504,13 @@ export async function sendSignupToOrganizationEmail({
}) {
try {
const verificationToken = await createVerificationToken(usernameOrEmail, teamId);
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath(prisma);
await sendTeamInviteEmail({
language: translation,
from: inviterName || `${team.name}'s admin`,
to: usernameOrEmail,
teamName: team.name,
joinLink: `${WEBAPP_URL}/signup?token=${verificationToken.token}&callbackUrl=/getting-started`,
joinLink: `${WEBAPP_URL}/signup?token=${verificationToken.token}&callbackUrl=${gettingStartedPath}`,
isCalcomMember: false,
isOrg: isOrg,
parentTeamName: team?.parent?.name,
@@ -717,7 +719,8 @@ export const sendExistingUserTeamInviteEmails = async ({
if (!user.completedOnboarding && !user.password?.hash && user.identityProvider === "CAL") {
const verificationToken = await createVerificationToken(user.email, teamId);
inviteTeamOptions.joinLink = `${WEBAPP_URL}/signup?token=${verificationToken.token}&callbackUrl=/getting-started`;
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath(prisma);
inviteTeamOptions.joinLink = `${WEBAPP_URL}/signup?token=${verificationToken.token}&callbackUrl=${gettingStartedPath}`;
inviteTeamOptions.isCalcomMember = false;
} else if (!isAutoJoin) {
let verificationToken = await prisma.verificationToken.findFirst({
@@ -1,5 +1,6 @@
import { sendTeamInviteEmail } from "@calcom/emails";
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
import { OnboardingPathService } from "@calcom/features/onboarding/lib/onboarding-path.service";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getTranslation } from "@calcom/lib/server/i18n";
import { VerificationTokenRepository } from "@calcom/lib/server/repository/verificationToken";
@@ -51,7 +52,8 @@ export const resendInvitationHandler = async ({ ctx, input }: InviteMemberOption
if (user?.completedOnboarding) {
inviteTeamOptions.joinLink = `${WEBAPP_URL}/teams?token=${verificationToken.token}&autoAccept=true`;
} else {
inviteTeamOptions.joinLink = `${WEBAPP_URL}/signup?token=${verificationToken.token}&callbackUrl=/getting-started`;
const gettingStartedPath = await OnboardingPathService.getGettingStartedPath(prisma);
inviteTeamOptions.joinLink = `${WEBAPP_URL}/signup?token=${verificationToken.token}&callbackUrl=${gettingStartedPath}`;
inviteTeamOptions.isCalcomMember = false;
}
} catch (error) {