diff --git a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx
index 6ec82c4f34..a46b0502df 100644
--- a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx
+++ b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx
@@ -245,7 +245,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
)}
{(usernameIsAvailable || premiumUsername) && currentUsername !== inputUsernameValue && (
-
+
)}
diff --git a/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx b/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx
index 14a70abe69..0221287e71 100644
--- a/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx
+++ b/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx
@@ -149,7 +149,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
{markAsError &&
Username is already taken
}
{usernameIsAvailable && currentUsername !== inputUsernameValue && (
-
+
)}
diff --git a/apps/web/lib/queries/event-types/get-event-types.ts b/apps/web/lib/queries/event-types/get-event-types.ts
deleted file mode 100644
index 97b79b6192..0000000000
--- a/apps/web/lib/queries/event-types/get-event-types.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { EventType } from "@prisma/client";
-
-import * as fetch from "@lib/core/http/fetch-wrapper";
-
-type GetEventsResponse = { message: string; data: EventType[] };
-const getEventTypes = async () => {
- const response = await fetch.get
("/api/event-type");
- return response.data;
-};
-
-export default getEventTypes;
diff --git a/apps/web/pages/api/event-type/index.ts b/apps/web/pages/api/event-type/index.ts
deleted file mode 100644
index ea3bd0fe81..0000000000
--- a/apps/web/pages/api/event-type/index.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { NextApiRequest, NextApiResponse } from "next";
-
-import prisma from "@calcom/prisma";
-
-import { getSession } from "@lib/auth";
-
-export default async function handler(req: NextApiRequest, res: NextApiResponse) {
- const session = await getSession({ req: req });
-
- if (!session) {
- res.status(401).json({ message: "Not authenticated" });
- return;
- }
-
- if (!session.user?.id) {
- console.error("Session is missing a user id");
- return res.status(500).json({ message: "Something went wrong" });
- }
-
- if (req.method === "GET") {
- const user = await prisma.user.findUnique({
- where: {
- id: session.user.id,
- },
- select: {
- id: true,
- eventTypes: {
- where: {
- team: null,
- },
- select: {
- id: true,
- title: true,
- description: true,
- length: true,
- schedulingType: true,
- slug: true,
- hidden: true,
- metadata: true,
- },
- },
- },
- });
-
- return res.status(200).json({ message: "Events.", data: user?.eventTypes });
- }
-}
diff --git a/apps/web/pages/getting-started.tsx b/apps/web/pages/getting-started.tsx
index 82773f7c03..68b9864ee7 100644
--- a/apps/web/pages/getting-started.tsx
+++ b/apps/web/pages/getting-started.tsx
@@ -12,14 +12,16 @@ import { useForm } from "react-hook-form";
import * as z from "zod";
import getApps from "@calcom/app-store/utils";
-import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/CalendarManager";
import dayjs from "@calcom/dayjs";
+import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
import { DOCS_URL } from "@calcom/lib/constants";
import { fetchUsername } from "@calcom/lib/fetchUsername";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import showToast from "@calcom/lib/notification";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
-import { isBrowserLocale24h } from "@calcom/lib/timeFormat";
import prisma from "@calcom/prisma";
import { trpc } from "@calcom/trpc/react";
+import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { Icon } from "@calcom/ui/Icon";
@@ -27,8 +29,6 @@ import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
import { Form } from "@calcom/ui/form/fields";
import { getSession } from "@lib/auth";
-import { DEFAULT_SCHEDULE } from "@lib/availability";
-import { useLocale } from "@lib/hooks/useLocale";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { Schedule as ScheduleType } from "@lib/types/schedule";
@@ -36,8 +36,9 @@ import { ClientSuspense } from "@components/ClientSuspense";
import Loader from "@components/Loader";
import Schedule from "@components/availability/Schedule";
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
+import { UsernameAvailability } from "@components/ui/UsernameAvailability";
-import getEventTypes from "../lib/queries/event-types/get-event-types";
+import { TRPCClientErrorLike } from "@trpc/client";
type ScheduleFormValues = {
schedule: ScheduleType;
@@ -47,20 +48,36 @@ let mutationComplete: ((err: Error | null) => void) | null;
export default function Onboarding(props: inferSSRProps) {
const { t } = useLocale();
+ const { user } = props;
const router = useRouter();
+ const utils = trpc.useContext();
const telemetry = useTelemetry();
+ const [hasErrors, setHasErrors] = useState(false);
+ const [errorMessage, setErrorMessage] = useState("");
+ const { data: eventTypes } = trpc.useQuery(["viewer.eventTypes.list"]);
+ const onSuccessMutation = async () => {
+ showToast(t("your_user_profile_updated_successfully"), "success");
+ setHasErrors(false); // dismiss any open errors
+ await utils.invalidateQueries(["viewer.me"]);
+ };
+ const onErrorMutation = (error: TRPCClientErrorLike) => {
+ setHasErrors(true);
+ setErrorMessage(error.message);
+ document?.getElementsByTagName("main")[0]?.scrollTo({ top: 0, behavior: "smooth" });
+ };
const mutation = trpc.useMutation("viewer.updateProfile", {
onSuccess: async () => {
setSubmitting(true);
setEnteredName(nameRef.current?.value || "");
+ setInputUsernameValue(usernameRef.current?.value || "");
if (mutationComplete) {
mutationComplete(null);
mutationComplete = null;
}
setSubmitting(false);
},
- onError: (err) => {
+ onError: (err: TRPCClientErrorLike) => {
setError(new Error(err.message));
if (mutationComplete) {
mutationComplete(new Error(err.message));
@@ -90,6 +107,9 @@ export default function Onboarding(props: inferSSRProps {
- const res = await fetch(`/api/user/${props.user.id}`, {
+ const res = await fetch(`/api/user/${user.id}`, {
method: "PATCH",
body: JSON.stringify({ data: { ...data } }),
headers: {
@@ -112,44 +132,29 @@ export default function Onboarding(props: inferSSRProps {
- throw new Error(err.message);
+ onError: (error: TRPCClientErrorLike) => {
+ throw new Error(error.message);
},
});
/** Name */
const nameRef = useRef(null);
+ /** Username */
+ const usernameRef = useRef(null!);
const bioRef = useRef(null);
/** End Name */
/** TimeZone */
- const [selectedTimeZone, setSelectedTimeZone] = useState(props.user.timeZone ?? dayjs.tz.guess());
+ const [selectedTimeZone, setSelectedTimeZone] = useState(user.timeZone ?? dayjs.tz.guess());
/** End TimeZone */
/** Onboarding Steps */
- const [currentStep, setCurrentStep] = useState(0);
- const detectStep = () => {
- // Always set timezone if new user
- let step = 0;
-
- const hasConfigureCalendar = props.integrations.some((integration) => integration.credential !== null);
- if (hasConfigureCalendar) {
- step = 2;
- }
-
- const hasSchedules = props.schedules && props.schedules.length > 0;
- if (hasSchedules) {
- step = 3;
- }
-
- setCurrentStep(step);
- };
-
+ const [currentStep, setCurrentStep] = useState(props.initialStep);
const handleConfirmStep = async () => {
try {
setSubmitting(true);
@@ -205,22 +210,15 @@ export default function Onboarding(props: inferSSRProps {
setSubmitting(true);
- if (!props.eventTypes || props.eventTypes.length === 0) {
- const eventTypes = await getEventTypes();
- if (eventTypes.length === 0) {
- await Promise.all(
- DEFAULT_EVENT_TYPES.map(async (event) => {
- return createEventType.mutate(event);
- })
- );
- }
+ if (eventTypes?.length === 0) {
+ await Promise.all(
+ DEFAULT_EVENT_TYPES.map(async (event) => {
+ return createEventType.mutate(event);
+ })
+ );
}
- // Write default timeformat to localStorage
- const browserTimeFormat = isBrowserLocale24h() ? 24 : 12;
-
await updateUser({
completedOnboarding: true,
- timeFormat: browserTimeFormat,
});
setSubmitting(false);
@@ -301,7 +299,7 @@ export default function Onboarding(props: inferSSRProps
+ {hasErrors && }
+
{
formMethods.setValue("token", e.target.value);
@@ -323,7 +323,7 @@ export default function Onboarding(props: inferSSRProps
@@ -641,12 +654,41 @@ export async function getServerSideProps(context: NextPageContext) {
timeZone: true,
identityProvider: true,
completedOnboarding: true,
+ weekStart: true,
+ hideBranding: true,
+ theme: true,
+ plan: true,
+ brandColor: true,
+ darkBrandColor: true,
+ metadata: true,
+ timeFormat: true,
+ allowDynamicBooking: true,
selectedCalendars: {
select: {
externalId: true,
integration: true,
},
},
+ credentials: {
+ where: {
+ userId: session.user.id,
+ },
+ select: {
+ id: true,
+ type: true,
+ key: true,
+ userId: true,
+ appId: true,
+ },
+ },
+ schedules: {
+ where: {
+ userId: session.user.id,
+ },
+ select: {
+ id: true,
+ },
+ },
},
});
if (!user) {
@@ -662,59 +704,18 @@ export async function getServerSideProps(context: NextPageContext) {
};
}
- const credentials = await prisma.credential.findMany({
- where: {
- userId: user.id,
- },
- select: {
- id: true,
- type: true,
- key: true,
- userId: true,
- appId: true,
- },
- });
-
- const integrations = getApps(credentials)
+ const integrations = getApps(user.credentials)
.filter((item) => item.type.endsWith("_calendar"))
.map((item) => omit(item, "key"));
-
- // get user's credentials + their connected integrations
- const calendarCredentials = getCalendarCredentials(credentials, user.id);
- // get all the connected integrations' calendars (from third party)
- const connectedCalendars = await getConnectedCalendars(calendarCredentials, user.selectedCalendars);
-
- const eventTypes = await prisma.eventType.findMany({
- where: {
- userId: user.id,
- },
- select: {
- id: true,
- title: true,
- slug: true,
- description: true,
- length: true,
- hidden: true,
- },
- });
-
- const schedules = await prisma.schedule.findMany({
- where: {
- userId: user.id,
- },
- select: {
- id: true,
- },
- });
+ const { schedules } = user;
+ const hasConfigureCalendar = integrations.some((integration) => integration.credential !== null);
+ const hasSchedules = schedules && schedules.length > 0;
return {
props: {
session,
user,
- integrations,
- connectedCalendars,
- eventTypes,
- schedules,
+ initialStep: hasSchedules ? (hasConfigureCalendar ? 2 : 3) : 0,
},
};
}
diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma
index 752857ef33..5ebd5b72ee 100644
--- a/packages/prisma/schema.prisma
+++ b/packages/prisma/schema.prisma
@@ -75,6 +75,7 @@ model EventType {
currency String @default("usd")
slotInterval Int?
metadata Json?
+ /// @zod.custom(imports.successRedirectUrl)
successRedirectUrl String?
workflows WorkflowsOnEventTypes[]
diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts
index b97800d353..edfe7f6142 100644
--- a/packages/prisma/zod-utils.ts
+++ b/packages/prisma/zod-utils.ts
@@ -111,3 +111,23 @@ export const userMetadata = z
intentUsername: z.string().optional(),
})
.nullable();
+
+/**
+ * Ensures that it is a valid HTTP URL
+ * It automatically avoids
+ * - XSS attempts through javascript:alert('hi')
+ * - mailto: links
+ */
+export function assertValidUrl(url: string | null | undefined) {
+ if (!url) return false;
+ return url.startsWith("http://") && url.startsWith("https://");
+}
+
+export const successRedirectUrl = z
+ .string()
+ .url()
+ .refine(assertValidUrl, {
+ path: ["successRedirectUrl"],
+ message: "Invalid URL",
+ })
+ .nullish();
diff --git a/packages/trpc/server/routers/viewer/eventTypes.tsx b/packages/trpc/server/routers/viewer/eventTypes.tsx
index 7c920b34c4..bca1ae3dba 100644
--- a/packages/trpc/server/routers/viewer/eventTypes.tsx
+++ b/packages/trpc/server/routers/viewer/eventTypes.tsx
@@ -17,22 +17,6 @@ function isPeriodType(keyInput: string): keyInput is PeriodType {
return Object.keys(PeriodType).includes(keyInput);
}
-/**
- * Ensures that it is a valid HTTP URL
- * It automatically avoids
- * - XSS attempts through javascript:alert('hi')
- * - mailto: links
- */
-function assertValidUrl(url: string | null | undefined) {
- if (!url) {
- return;
- }
-
- if (!url.startsWith("http://") && !url.startsWith("https://")) {
- throw new TRPCError({ code: "PARSE_ERROR", message: "Invalid URL" });
- }
-}
-
function handlePeriodType(periodType: string | undefined): PeriodType | undefined {
if (typeof periodType !== "string") return undefined;
const passedPeriodType = periodType.toUpperCase();
@@ -102,9 +86,20 @@ const EventTypeUpdateInput = _EventTypeModel
export const eventTypesRouter = createProtectedRouter()
.query("list", {
async resolve({ ctx }) {
- return await ctx.prisma.webhook.findMany({
+ return await ctx.prisma.eventType.findMany({
where: {
userId: ctx.user.id,
+ team: null,
+ },
+ select: {
+ id: true,
+ title: true,
+ description: true,
+ length: true,
+ schedulingType: true,
+ slug: true,
+ hidden: true,
+ metadata: true,
},
});
},
@@ -269,7 +264,6 @@ export const eventTypesRouter = createProtectedRouter()
hashedLink,
...rest
} = input;
- assertValidUrl(input.successRedirectUrl);
const data: Prisma.EventTypeUpdateInput = rest;
data.locations = locations ?? undefined;
if (periodType) {