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:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user