feat: remove setup-availability step in onboarding flow (#17799)

* feat: make the onboarding flow 3-step for google signups

* fix

* refactor

* rename

* improve tests

* remove test tag

* fix test and type error

* fix

* feat: remove setup-availability step in onboarding flow

* update test

* revert unneeded change

* add safety

* update create schedule options
This commit is contained in:
Benny Joo
2024-12-10 14:30:23 -07:00
committed by GitHub
parent 370345da22
commit a466e8eef7
3 changed files with 31 additions and 119 deletions
@@ -1,88 +0,0 @@
import { useForm } from "react-hook-form";
import { Schedule } from "@calcom/features/schedules";
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { TRPCClientErrorLike } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Form, Icon } from "@calcom/ui";
interface ISetupAvailabilityProps {
nextStep: () => void;
defaultScheduleId?: number | null;
}
const SetupAvailability = (props: ISetupAvailabilityProps) => {
const { defaultScheduleId } = props;
const { t } = useLocale();
const { nextStep } = props;
const scheduleId = defaultScheduleId === null ? undefined : defaultScheduleId;
const queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
{ scheduleId: defaultScheduleId ?? undefined },
{
enabled: !!scheduleId,
}
);
const availabilityForm = useForm({
defaultValues: {
schedule: queryAvailability?.data?.availability || DEFAULT_SCHEDULE,
},
});
const mutationOptions = {
onError: (error: TRPCClientErrorLike<AppRouter>) => {
throw new Error(error.message);
},
onSuccess: () => {
nextStep();
},
};
const createSchedule = trpc.viewer.availability.schedule.create.useMutation(mutationOptions);
const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions);
return (
<Form
form={availabilityForm}
handleSubmit={async (values) => {
try {
if (defaultScheduleId) {
await updateSchedule.mutate({
scheduleId: defaultScheduleId,
name: t("default_schedule_name"),
...values,
});
} else {
await createSchedule.mutate({
name: t("default_schedule_name"),
...values,
});
}
} catch (error) {
if (error instanceof Error) {
// setError(error);
// @TODO: log error
}
}
}}>
<div className="bg-default dark:text-inverted text-emphasis border-subtle w-full rounded-md border dark:bg-opacity-5">
<Schedule control={availabilityForm.control} name="schedule" weekStart={1} />
</div>
<div>
<Button
data-testid="save-availability"
type="submit"
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
loading={availabilityForm.formState.isSubmitting}
disabled={availabilityForm.formState.isSubmitting}>
{t("next_step_text")} <Icon name="arrow-right" className="ml-2 h-4 w-4 self-center" />
</Button>
</div>
</Form>
);
};
export { SetupAvailability };
@@ -4,30 +4,32 @@ import { signOut } from "next-auth/react";
import type { TFunction } from "next-i18next";
import Head from "next/head";
import { usePathname, useRouter } from "next/navigation";
import { Suspense } from "react";
import { Suspense, useEffect } from "react";
import { Toaster } from "react-hot-toast";
import { z } from "zod";
import { classNames } from "@calcom/lib";
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { IdentityProvider } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc";
import type { TRPCClientErrorLike } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Button, StepCard, Steps } from "@calcom/ui";
import { Button, showToast, StepCard, Steps } from "@calcom/ui";
import { Icon } from "@calcom/ui";
import type { getServerSideProps } from "@lib/getting-started/[[...step]]/getServerSideProps";
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
import { ConnectedVideoStep } from "@components/getting-started/steps-views/ConnectedVideoStep";
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
import UserProfile from "@components/getting-started/steps-views/UserProfile";
import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
const INITIAL_STEP = "user-settings";
const BASE_STEPS = ["user-settings", "setup-availability", "user-profile"] as const;
const BASE_STEPS = ["user-settings", "user-profile"] as const;
const EXTRA_STEPS = ["connected-calendar", "connected-video"] as const;
type StepType = (typeof BASE_STEPS)[number] | (typeof EXTRA_STEPS)[number];
@@ -41,13 +43,6 @@ const getStepsAndHeadersForUser = (identityProvider: IdentityProvider, t: TFunct
title: t("welcome_to_cal_header", { appName: APP_NAME }),
subtitle: [t("we_just_need_basic_info"), t("edit_form_later_subtitle")],
},
{
title: t("set_availability"),
subtitle: [
t("set_availability_getting_started_subtitle_1"),
t("set_availability_getting_started_subtitle_2"),
],
},
{
title: t("nearly_there"),
subtitle: [t("nearly_there_instructions")],
@@ -86,9 +81,7 @@ const getStepsAndHeadersForUser = (identityProvider: IdentityProvider, t: TFunct
const stepRouteSchema = z.object({
step: z
.array(
z.enum(["user-settings", "setup-availability", "user-profile", "connected-calendar", "connected-video"])
)
.array(z.enum(["user-settings", "user-profile", "connected-calendar", "connected-video"]))
.default([INITIAL_STEP]),
from: z.string().optional(),
});
@@ -97,10 +90,15 @@ export type PageProps = inferSSRProps<typeof getServerSideProps>;
const OnboardingPage = (props: PageProps) => {
const pathname = usePathname();
const params = useParamsWithFallback();
const router = useRouter();
const [user] = trpc.viewer.me.useSuspenseQuery();
const { t } = useLocale();
const [user] = trpc.viewer.me.useSuspenseQuery();
const scheduleMutationOptions = {
onError: (error: TRPCClientErrorLike<AppRouter>) => {
showToast(error.message, "error");
},
};
const createSchedule = trpc.viewer.availability.schedule.create.useMutation(scheduleMutationOptions);
const result = stepRouteSchema.safeParse({
...params,
@@ -137,6 +135,20 @@ const OnboardingPage = (props: PageProps) => {
router.push(`/getting-started/${stepTransform(newStep)}`);
};
useEffect(() => {
if (!user || user.completedOnboarding) {
return;
}
const createDefaultSchedule = async () => {
await createSchedule.mutateAsync({
name: t("default_schedule_name"),
...DEFAULT_SCHEDULE,
});
};
createDefaultSchedule();
}, []);
return (
<div
className={classNames(
@@ -178,9 +190,6 @@ const OnboardingPage = (props: PageProps) => {
{currentStep === "connected-video" && <ConnectedVideoStep nextStep={goToNextStep} />}
{currentStep === "setup-availability" && (
<SetupAvailability nextStep={goToNextStep} defaultScheduleId={user.defaultScheduleId} />
)}
{currentStep === "user-profile" && <UserProfile />}
</Suspense>
</StepCard>
+3 -12
View File
@@ -41,7 +41,7 @@ test.describe("Onboarding", () => {
await page.locator("button[type=submit]").click();
if (identityProvider === IdentityProvider.GOOGLE) {
await expect(page).toHaveURL(/.*setup-availability/);
await expect(page).toHaveURL(/.*user-profile/);
} else {
await expect(page).toHaveURL(/.*connected-calendar/);
}
@@ -66,20 +66,11 @@ test.describe("Onboarding", () => {
// tests skip button, we don't want to test entire flow.
await page.locator("button[data-testid=skip-step]").click();
await expect(page).toHaveURL(/.*setup-availability/);
await expect(page).toHaveURL(/.*user-profile/);
});
}
await test.step("step 4 - Setup Availability", async () => {
const isDisabled = await page.locator("button[data-testid=save-availability]").isDisabled();
await expect(isDisabled).toBe(false);
// same here, skip this step.
await page.locator("button[data-testid=save-availability]").click();
await expect(page).toHaveURL(/.*user-profile/);
});
await test.step("step 5- User Profile", async () => {
await test.step("step 4 - User Profile", async () => {
await page.locator("button[type=submit]").click();
// should redirect to /event-types after onboarding