From 80737758fdc475b37fa032ce82f119eff7f246b7 Mon Sep 17 00:00:00 2001
From: Boshen
Date: Sat, 8 Jun 2024 21:25:56 +0800
Subject: [PATCH] add cal.com.tsx
---
.github/workflows/benchmark.yml | 15 +-
benches/transformer.rs | 15 +-
files/cal.com.tsx | 30591 +++++++++++++++++++++++++
typescript.js => files/typescript.js | 0
4 files changed, 30607 insertions(+), 14 deletions(-)
create mode 100644 files/cal.com.tsx
rename typescript.js => files/typescript.js (100%)
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index 9d8c250..7b1131b 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -16,20 +16,21 @@ on:
- 'Cargo.lock'
- 'rust-toolchain.toml'
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
+ cancel-in-progress: ${{ github.ref_name != 'main' }}
+
jobs:
benchmark:
name: Benchmark
runs-on: ubuntu-latest
steps:
- - name: Checkout Branch
- uses: actions/checkout@v4
+ - uses: taiki-e/checkout-action@v1
- - name: Setup rust toolchain, cache and cargo-codspeed binary
- uses: moonrepo/setup-rust@v1
+ - uses: Boshen/setup-rust@main
with:
- channel: stable
- cache-target: release
- bins: cargo-codspeed
+ save-cache: ${{ github.ref_name == 'main' }}
+ tools: cargo-codspeed
- name: Build Benchmark
run: cargo codspeed build --features codspeed
diff --git a/benches/transformer.rs b/benches/transformer.rs
index d4c69ea..4722f0c 100644
--- a/benches/transformer.rs
+++ b/benches/transformer.rs
@@ -132,13 +132,14 @@ impl TheBencher for SwcBencher {
}
fn transformer_benchmark(c: &mut Criterion) {
- let filename = "typescript.js";
- let source = std::fs::read_to_string(filename).unwrap();
-
- let mut g = c.benchmark_group(filename);
- OxcBencher::bench(&mut g, &source);
- SwcBencher::bench(&mut g, &source);
- g.finish();
+ let filenames = ["typescript.js", "cal.com.tsx"];
+ for filename in filenames {
+ let source = std::fs::read_to_string(Path::new("files").join(filename)).unwrap();
+ let mut g = c.benchmark_group(filename);
+ OxcBencher::bench(&mut g, &source);
+ SwcBencher::bench(&mut g, &source);
+ g.finish();
+ }
}
criterion_group!(transformer, transformer_benchmark);
diff --git a/files/cal.com.tsx b/files/cal.com.tsx
new file mode 100644
index 0000000..7112f68
--- /dev/null
+++ b/files/cal.com.tsx
@@ -0,0 +1,30591 @@
+// adds tooltip context to all stories
+import { TooltipProvider } from "@radix-ui/react-tooltip";
+import type { Preview } from "@storybook/react";
+import React from "react";
+import { I18nextProvider } from "react-i18next";
+
+import type { EmbedThemeConfig } from "@calcom/embed-core/src/types";
+// adds trpc context to all stories (esp. booker)
+import { StorybookTrpcProvider } from "@calcom/ui";
+
+import "../styles/globals.css";
+import "../styles/storybook-styles.css";
+import i18n from "./i18next";
+
+const preview: Preview = {
+ parameters: {
+ actions: { argTypesRegex: "^on[A-Z].*" },
+
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/,
+ },
+ },
+
+ globals: {
+ locale: "en",
+ locales: {
+ en: "English",
+ fr: "Français",
+ },
+ },
+
+ i18n,
+
+ nextjs: {
+ appDirectory: true,
+ },
+ },
+
+ decorators: [
+ (Story) => (
+
+
+
+
+
+
+
+
+
+ ),
+ ],
+};
+
+export default preview;
+
+declare global {
+ interface Window {
+ getEmbedNamespace: () => string | null;
+ getEmbedTheme: () => EmbedThemeConfig | null;
+ }
+}
+
+window.getEmbedNamespace = () => {
+ const url = new URL(document.URL);
+ const namespace = url.searchParams.get("embed");
+ return namespace;
+};
+
+window.getEmbedTheme = () => {
+ return "auto";
+};
+export const Title = ({
+ title,
+ suffix,
+ subtitle,
+ offset,
+}: {
+ title: string;
+ suffix?: string;
+ subtitle?: string;
+ offset?: boolean;
+}) => {
+ return (
+
+
+ {title}
+ {suffix && {suffix} }
+
+ {subtitle &&
{subtitle}
}
+
+ );
+};
+export const Note = ({ children }: { children: React.ReactNode }) => (
+
+);
+import { classNames } from "@calcom/lib";
+
+interface ExampleProps {
+ children: React.ReactNode;
+ title: string;
+ isFullWidth?: boolean;
+}
+export const Example = ({ children, title, isFullWidth = false }: ExampleProps) => {
+ return (
+
+ );
+};
+
+interface ExamplesProps {
+ children: React.ReactNode;
+ title: string;
+ footnote?: React.ReactNode;
+ dark?: boolean;
+}
+
+export const Examples = ({ children, title, footnote = null, dark }: ExamplesProps) => {
+ return (
+
+
{title}
+
{children}
+ {!!footnote &&
{footnote}
}
+
+ );
+};
+import { ArgsTable } from "@storybook/addon-docs";
+import type { SortType } from "@storybook/blocks";
+import type { PropDescriptor } from "@storybook/preview-api";
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any -- ignore storybook addon types component as any so we have to do
+type Component = any;
+type BaseProps = {
+ include?: PropDescriptor;
+ exclude?: PropDescriptor;
+ sort?: SortType;
+};
+
+type OfProps = BaseProps & {
+ of: "." | "^" | Component;
+};
+
+export function CustomArgsTable({ of, sort }: OfProps) {
+ return (
+
+ );
+}
+import type { ReactElement, ReactNode } from "react";
+import React from "react";
+
+import { classNames } from "@calcom/lib";
+
+export function VariantsTable({
+ children,
+ titles,
+ isDark,
+ columnMinWidth = 150,
+}: {
+ children: ReactElement | ReactElement[];
+ titles: string[];
+ isDark?: boolean;
+ // Mainly useful on mobile, so components don't get squeesed
+ columnMinWidth?: number;
+}) {
+ const columns = React.Children.toArray(children) as ReactElement[];
+ return (
+
+
+
+
+ {columns.map((column) => (
+
+
+ {column.props.variant}
+
+ {React.Children.count(column.props.children) &&
+ React.Children.map(column.props.children, (cell) => (
+
+ {cell}
+
+ ))}
+
+ ))}
+
+
+ {!isDark && (
+
+
+ {children}
+
+
+ )}
+
+ );
+}
+
+interface RowProps {
+ variant: string;
+ children: ReactNode;
+}
+
+/**
+ * There are two reasons we have this "empty" wrapper component:
+ * 1. In order to have an isolate group per variant, which we iterate through in the table component.
+ * 2. To have a way to pass the variant.
+ */
+export function VariantRow({ children }: RowProps) {
+ return <>{children}>;
+}
+
+export function RowTitles({ titles }: { titles: string[] }) {
+ return (
+
+
+ {titles.map((title) => (
+
+ {title}
+
+ ))}
+
+ );
+}
+import dynamic from "next/dynamic";
+import type { SwaggerUI } from "swagger-ui-react";
+
+import { SnippedGenerator, requestSnippets } from "@lib/snippets";
+
+const SwaggerUIDynamic: SwaggerUI & { url: string } = dynamic(() => import("swagger-ui-react"), {
+ ssr: false,
+});
+
+export default function APIDocs() {
+ return (
+
+ );
+}
+import "highlight.js/styles/default.css";
+import "swagger-ui-react/swagger-ui.css";
+
+import "../styles/globals.css";
+
+function MyApp({ Component, pageProps }) {
+ return ;
+}
+
+export default MyApp;
+import { type TFunction } from "i18next";
+import i18next from "i18next";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+import { headers } from "next/headers";
+
+import { constructGenericImage } from "@calcom/lib/OgImages";
+import { IS_CALCOM, WEBAPP_URL, APP_NAME, SEO_IMG_OGIMG } from "@calcom/lib/constants";
+//@ts-expect-error no type definitions
+import config from "@calcom/web/next-i18next.config";
+
+import { preparePageMetadata } from "@lib/metadata";
+
+const create = async (locale: string, ns: string) => {
+ const { _nextI18Next } = await serverSideTranslations(locale, [ns], config);
+
+ const _i18n = i18next.createInstance();
+ _i18n.init({
+ lng: locale,
+ resources: _nextI18Next?.initialI18nStore,
+ fallbackLng: _nextI18Next?.userConfig?.i18n.defaultLocale,
+ });
+ return _i18n;
+};
+
+const getFixedT = async (locale: string, ns: string) => {
+ const i18n = await create(locale, ns);
+
+ return i18n.getFixedT(locale, ns);
+};
+
+export const _generateMetadata = async (
+ getTitle: (t: TFunction) => string,
+ getDescription: (t: TFunction) => string
+) => {
+ const h = headers();
+ const canonical = h.get("x-pathname") ?? "";
+ const locale = h.get("x-locale") ?? "en";
+
+ const t = await getFixedT(locale, "common");
+
+ const title = getTitle(t);
+ const description = getDescription(t);
+
+ const metadataBase = new URL(IS_CALCOM ? "https://cal.com" : WEBAPP_URL);
+
+ const image =
+ SEO_IMG_OGIMG +
+ constructGenericImage({
+ title,
+ description,
+ });
+
+ return preparePageMetadata({
+ title,
+ canonical,
+ image,
+ description,
+ siteName: APP_NAME,
+ metadataBase,
+ });
+};
+import type { GetStaticProps, GetStaticPropsContext } from "next";
+import { notFound, redirect } from "next/navigation";
+
+export const withAppDirSsg =
+ >(getStaticProps: GetStaticProps) =>
+ async (context: GetStaticPropsContext) => {
+ const ssgResponse = await getStaticProps(context);
+
+ if ("redirect" in ssgResponse) {
+ redirect(ssgResponse.redirect.destination);
+ }
+
+ if ("notFound" in ssgResponse) {
+ notFound();
+ }
+
+ const props = await Promise.resolve(ssgResponse.props);
+
+ return {
+ ...ssgResponse.props,
+ // includes dehydratedState required for future page trpcPropvider
+ ...("trpcState" in props && { dehydratedState: props.trpcState }),
+ };
+ };
+import type { GetServerSideProps, GetServerSidePropsContext } from "next";
+import { notFound, redirect } from "next/navigation";
+
+export const withAppDirSsr =
+ >(getServerSideProps: GetServerSideProps) =>
+ async (context: GetServerSidePropsContext) => {
+ const ssrResponse = await getServerSideProps(context);
+
+ if ("redirect" in ssrResponse) {
+ redirect(ssrResponse.redirect.destination);
+ }
+ if ("notFound" in ssrResponse) {
+ notFound();
+ }
+
+ const props = await Promise.resolve(ssrResponse.props);
+
+ return {
+ ...props,
+ // includes dehydratedState required for future page trpcPropvider
+ ...("trpcState" in props && { dehydratedState: props.trpcState }),
+ };
+ };
+import { type DehydratedState, QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { HydrateClient } from "app/_trpc/HydrateClient";
+import { trpc } from "app/_trpc/client";
+import { useState } from "react";
+import superjson from "superjson";
+
+import { httpBatchLink, httpLink, loggerLink, splitLink } from "@calcom/trpc/client";
+import { ENDPOINTS } from "@calcom/trpc/react/shared";
+
+export type Endpoint = (typeof ENDPOINTS)[number];
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+const resolveEndpoint = (links: any) => {
+ // TODO: Update our trpc routes so they are more clear.
+ // This function parses paths like the following and maps them
+ // to the correct API endpoints.
+ // - viewer.me - 2 segment paths like this are for logged in requests
+ // - viewer.public.i18n - 3 segments paths can be public or authed
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return (ctx: any) => {
+ const parts = ctx.op.path.split(".");
+ let endpoint;
+ let path = "";
+ if (parts.length == 2) {
+ endpoint = parts[0] as keyof typeof links;
+ path = parts[1];
+ } else {
+ endpoint = parts[1] as keyof typeof links;
+ path = parts.splice(2, parts.length - 2).join(".");
+ }
+ return links[endpoint]({ ...ctx, op: { ...ctx.op, path } });
+ };
+};
+
+export const TrpcProvider: React.FC<{ children: React.ReactNode; dehydratedState?: DehydratedState }> = ({
+ children,
+ dehydratedState,
+}) => {
+ const [queryClient] = useState(
+ () =>
+ new QueryClient({
+ defaultOptions: { queries: { staleTime: 5000 } },
+ })
+ );
+ const url =
+ typeof window !== "undefined"
+ ? "/api/trpc"
+ : process.env.VERCEL_URL
+ ? `https://${process.env.VERCEL_URL}/api/trpc`
+ : `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/trpc`;
+
+ const [trpcClient] = useState(() =>
+ trpc.createClient({
+ links: [
+ // adds pretty logs to your console in development and logs errors in production
+ loggerLink({
+ enabled: (opts) =>
+ !!process.env.NEXT_PUBLIC_DEBUG || (opts.direction === "down" && opts.result instanceof Error),
+ }),
+ splitLink({
+ // check for context property `skipBatch`
+ condition: (op) => !!op.context.skipBatch,
+ // when condition is true, use normal request
+ true: (runtime) => {
+ const links = Object.fromEntries(
+ ENDPOINTS.map((endpoint) => [
+ endpoint,
+ httpLink({
+ url: `${url}/${endpoint}`,
+ })(runtime),
+ ])
+ );
+ return resolveEndpoint(links);
+ },
+ // when condition is false, use batch request
+ false: (runtime) => {
+ const links = Object.fromEntries(
+ ENDPOINTS.map((endpoint) => [
+ endpoint,
+ httpBatchLink({
+ url: `${url}/${endpoint}`,
+ })(runtime),
+ ])
+ );
+ return resolveEndpoint(links);
+ },
+ }),
+ ],
+ transformer: superjson,
+ })
+ );
+
+ return (
+
+
+ {dehydratedState ? {children} : children}
+
+
+ );
+};
+"use client";
+
+import { createHydrateClient } from "app/_trpc/createHydrateClient";
+import superjson from "superjson";
+
+export const HydrateClient = createHydrateClient({
+ transformer: superjson,
+});
+"use client";
+
+import { type DehydratedState, HydrationBoundary } from "@tanstack/react-query";
+import { useMemo } from "react";
+
+type DataTransformer = {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ serialize: (data: any) => any;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ deserialize: (data: any) => any;
+};
+
+export function createHydrateClient(opts: { transformer: DataTransformer }) {
+ return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) {
+ const { state, children } = props;
+
+ const transformedState: DehydratedState = useMemo(() => {
+ if (opts.transformer) {
+ return opts.transformer.deserialize(state);
+ }
+ return state;
+ }, [state]);
+
+ return {children} ;
+ };
+}
+import type { LayoutProps, PageProps } from "app/_types";
+import { type GetServerSidePropsContext } from "next";
+import { cookies, headers } from "next/headers";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+
+import PageWrapper from "@components/PageWrapperAppDir";
+
+type WithLayoutParams> = {
+ getLayout: ((page: React.ReactElement) => React.ReactNode) | null;
+ Page?: (props: T) => React.ReactElement | null;
+ getData?: (arg: GetServerSidePropsContext) => Promise;
+ isBookingPage?: boolean;
+};
+
+export function WithLayout>({
+ getLayout,
+ getData,
+ Page,
+ isBookingPage,
+}: WithLayoutParams) {
+ return async (p: P extends "P" ? PageProps : LayoutProps) => {
+ const h = headers();
+ const nonce = h.get("x-nonce") ?? undefined;
+ let props = {} as T;
+
+ if ("searchParams" in p && getData) {
+ props = (await getData(buildLegacyCtx(h, cookies(), p.params, p.searchParams))) ?? ({} as T);
+ }
+
+ const children = "children" in p ? p.children : null;
+
+ return (
+
+ {Page ? : children}
+
+ );
+ };
+}
+import { dir } from "i18next";
+import { Inter } from "next/font/google";
+import localFont from "next/font/local";
+import { headers, cookies } from "next/headers";
+import Script from "next/script";
+import React from "react";
+
+import { getLocale } from "@calcom/features/auth/lib/getLocale";
+import { IS_PRODUCTION } from "@calcom/lib/constants";
+
+import { prepareRootMetadata } from "@lib/metadata";
+
+import "../styles/globals.css";
+
+const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
+const calFont = localFont({
+ src: "../fonts/CalSans-SemiBold.woff2",
+ variable: "--font-cal",
+ preload: true,
+ display: "block",
+});
+
+export const generateMetadata = () =>
+ prepareRootMetadata({
+ twitterCreator: "@calcom",
+ twitterSite: "@calcom",
+ robots: {
+ index: false,
+ follow: false,
+ },
+ });
+
+const getInitialProps = async (url: string) => {
+ const { pathname, searchParams } = new URL(url);
+
+ const isEmbed = pathname.endsWith("/embed") || (searchParams?.get("embedType") ?? null) !== null;
+ const embedColorScheme = searchParams?.get("ui.color-scheme");
+
+ const req = { headers: headers(), cookies: cookies() };
+ const newLocale = await getLocale(req);
+ const direction = dir(newLocale);
+
+ return { isEmbed, embedColorScheme, locale: newLocale, direction };
+};
+
+const getFallbackProps = () => ({
+ locale: "en",
+ direction: "ltr",
+ isEmbed: false,
+ embedColorScheme: false,
+});
+
+export default async function RootLayout({ children }: { children: React.ReactNode }) {
+ const h = headers();
+
+ const fullUrl = h.get("x-url") ?? "";
+ const nonce = h.get("x-csp") ?? "";
+
+ const isSSG = !fullUrl;
+
+ const { locale, direction, isEmbed, embedColorScheme } = isSSG
+ ? getFallbackProps()
+ : await getInitialProps(fullUrl);
+
+ return (
+
+
+ {!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
+ // eslint-disable-next-line @next/next/no-sync-scripts
+
+ )}
+
+
+
+ {children}
+
+
+ );
+}
+"use client";
+
+/**
+ * Typescript class based component for custom-error
+ * @link https://nextjs.org/docs/advanced-features/custom-error-page
+ */
+import type { NextPage } from "next";
+import type { ErrorProps } from "next/error";
+import React from "react";
+
+import { HttpError } from "@calcom/lib/http-error";
+import logger from "@calcom/lib/logger";
+import { redactError } from "@calcom/lib/redactError";
+
+import { ErrorPage } from "@components/error/error-page";
+
+type NextError = Error & { digest?: string };
+
+// Ref: https://nextjs.org/docs/app/api-reference/file-conventions/error#props
+export type DefaultErrorProps = {
+ error: NextError;
+ reset: () => void; // A function to reset the error boundary
+};
+
+type AugmentedError = NextError | HttpError | null;
+
+type CustomErrorProps = {
+ err?: AugmentedError;
+ statusCode?: number;
+ message?: string;
+} & Omit;
+
+const log = logger.getSubLogger({ prefix: ["[error]"] });
+
+const CustomError: NextPage = (props) => {
+ const { error } = props;
+ let errorObject: CustomErrorProps = {
+ message: error.message,
+ err: error,
+ };
+
+ if (error instanceof HttpError) {
+ const redactedError = redactError(error);
+ errorObject = {
+ statusCode: error.statusCode,
+ title: redactedError.name,
+ message: redactedError.message,
+ err: {
+ ...redactedError,
+ ...error,
+ },
+ };
+ }
+
+ // `error.digest` property contains an automatically generated hash of the error that can be used to match the corresponding error in server-side logs
+ log.debug(`${error?.toString() ?? JSON.stringify(error)}`);
+ log.info("errorObject: ", errorObject);
+
+ return (
+
+ );
+};
+
+export default CustomError;
+import LegacyPage from "@pages/insights/index";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/MainLayoutAppDir";
+
+import { getServerSideProps } from "@lib/insights/getServerSideProps";
+import { type inferSSRProps } from "@lib/types/inferSSRProps";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ () => "Insights",
+ (t) => t("insights_subtitle")
+ );
+
+const getData = withAppDirSsr>(getServerSideProps);
+
+export default WithLayout({ getLayout, getData, Page: LegacyPage });
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/organizations/pages/settings/general";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("general"),
+ (t) => t("general_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/organizations/pages/settings/appearance";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("appearance"),
+ (t) => t("appearance_org_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/organizations/pages/settings/profile";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("profile"),
+ (t) => t("profile_org_description")
+ );
+
+export default Page;
+import LegacyPage, { getServerSideProps, LayoutWrapperAppDir } from "@pages/settings/organizations/new/index";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { type inferSSRProps } from "@lib/types/inferSSRProps";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("set_up_your_organization"),
+ (t) => t("organizations_description")
+ );
+
+export default WithLayout({
+ getLayout: LayoutWrapperAppDir,
+ Page: LegacyPage,
+ getData: withAppDirSsr>(getServerSideProps),
+});
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/teams/pages/team-appearance-view";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("booking_appearance"),
+ (t) => t("appearance_team_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("profile"),
+ (t) => t("profile_team_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("team_members"),
+ (t) => t("members_team_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/organizations/pages/settings/members";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("organization_members"),
+ (t) => t("organization_description")
+ );
+
+export default Page;
+import LegacyPage, {
+ buildWrappedOnboardTeamMembersPage,
+} from "@pages/settings/organizations/[id]/onboard-members";
+import { type Params } from "app/_types";
+import { _generateMetadata } from "app/_utils";
+import { headers } from "next/headers";
+
+import PageWrapper from "@components/PageWrapperAppDir";
+
+type PageProps = Readonly<{
+ params: Params;
+}>;
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("invite_organization_admins"),
+ (t) => t("invite_organization_admins_description")
+ );
+
+const Page = ({ params }: PageProps) => {
+ const h = headers();
+ const nonce = h.get("x-nonce") ?? undefined;
+
+ return (
+ buildWrappedOnboardTeamMembersPage(params.id, page)}
+ requiresLicense={false}
+ nonce={nonce}
+ themeBasis={null}>
+
+
+ );
+};
+
+export default Page;
+import LegacyPage, { WrappedAboutOrganizationPage } from "@pages/settings/organizations/[id]/about";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("about_your_organization"),
+ (t) => t("about_your_organization_description")
+ );
+
+export default WithLayout({ Page: LegacyPage, getLayout: WrappedAboutOrganizationPage });
+import LegacyPage, { WrapperAddNewTeamsPage } from "@pages/settings/organizations/[id]/add-teams";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("create_your_teams"),
+ (t) => t("create_your_teams_description")
+ );
+
+export default WithLayout({ Page: LegacyPage, getLayout: WrapperAddNewTeamsPage });
+import LegacyPage, { WrappedSetPasswordPage } from "@pages/settings/organizations/[id]/set-password";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("set_a_password"),
+ (t) => t("set_a_password_description")
+ );
+
+export default WithLayout({ Page: LegacyPage, getLayout: WrappedSetPasswordPage });
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import Page from "@pages/settings/billing/index";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("billing"),
+ (t) => t("manage_billing_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import Page from "@pages/settings/security/password";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("password"),
+ (t) => t("password_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import Page from "@pages/settings/security/two-factor-auth";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("two_factor_auth"),
+ (t) => t("add_an_extra_layer_of_security")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/sso/page/user-sso-view";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("sso_configuration"),
+ (t) => t("sso_configuration_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import Page from "@pages/settings/security/impersonation";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("impersonation"),
+ (t) => t("impersonation_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout })<"L">;
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("organizations"),
+ (t) => t("orgs_page_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+export default WithLayout({ getLayout: null })<"L">;
+import Page from "@pages/settings/admin/oAuth/oAuthView";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ () => "OAuth",
+ () => "Add new OAuth Clients"
+ );
+
+export default Page;
+import LegacyPage from "@pages/settings/admin/oAuth/index";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ () => "OAuth",
+ () => "Add new OAuth Clients"
+ );
+
+export default WithLayout({ getLayout, Page: LegacyPage })<"P">;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
+
+export default WithLayout({ getLayout })<"L">;
+import Page from "@pages/settings/admin/flags";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ () => "Feature Flags",
+ () => "Here you can toggle your Cal.com instance features."
+ );
+
+export default Page;
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import Page from "@calcom/features/ee/users/pages/users-add-view";
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ () => "Add new user",
+ () => "Here you can add a new user."
+ );
+
+export default WithLayout({ getLayout, Page })<"P">;
+import { type Params } from "app/_types";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { z } from "zod";
+
+import Page from "@calcom/features/ee/users/pages/users-edit-view";
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+const userIdSchema = z.object({ id: z.coerce.number() });
+
+export const generateMetadata = async ({ params }: { params: Params }) => {
+ const input = userIdSchema.safeParse(params);
+ if (!input.success) {
+ return await _generateMetadata(
+ () => "",
+ () => "Here you can edit a current user."
+ );
+ }
+
+ const userId = input.data.id;
+ const { trpc } = await import("@calcom/trpc");
+ const [data] = trpc.viewer.users.get.useSuspenseQuery({ userId });
+ const { user } = data;
+ const title = `Editing user: ${user.username}`;
+
+ return await _generateMetadata(
+ () => title,
+ () => "Here you can edit a current user."
+ );
+};
+
+export default WithLayout({ getLayout, Page })<"P">;
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import Page from "@calcom/features/ee/users/pages/users-listing-view";
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ () => "Users",
+ () => "A list of all the users in your account including their name, title, email and role."
+ );
+
+export default WithLayout({ getLayout, Page })<"P">;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
+
+export default WithLayout({ getLayout })<"L">;
+import Page from "@pages/settings/admin/impersonation";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("admin"),
+ (t) => t("impersonation")
+ );
+
+export default Page;
+import LegacyPage from "@pages/settings/admin/index";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ () => "Admin",
+ () => "admin_description"
+ );
+
+export default WithLayout({ getLayout, Page: LegacyPage })<"P">;
+import Page from "@pages/settings/admin/apps/[category]";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("apps"),
+ (t) => t("admin_apps_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir";
+
+export default WithLayout({ getLayout })<"L">;
+import Page from "@pages/settings/admin/apps/index";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("apps"),
+ (t) => t("admin_apps_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import Page from "@pages/settings/my-account/calendars";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("calendars"),
+ (t) => t("calendars_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import Page from "@pages/settings/my-account/general";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("general"),
+ (t) => t("general_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import Page from "@pages/settings/my-account/appearance";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("appearance"),
+ (t) => t("appearance_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import Page from "@pages/settings/my-account/conferencing";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("conferencing"),
+ (t) => t("conferencing_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout });
+import Page from "@pages/settings/my-account/profile";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("profile"),
+ (t) => t("profile_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout })<"L">;
+import LegacyPage, { LayoutWrapper } from "@pages/settings/teams/new/index";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("create_new_team"),
+ (t) => t("create_new_team_description")
+ );
+
+export default WithLayout({ Page: LegacyPage, getLayout: LayoutWrapper })<"P">;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout })<"L">;
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/teams/pages/team-appearance-view";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("booking_appearance"),
+ (t) => t("appearance_team_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout })<"L">;
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/sso/page/teams-sso-view";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("sso_configuration"),
+ (t) => t("sso_configuration_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout })<"L">;
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/teams/pages/team-profile-view";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("profile"),
+ (t) => t("profile_team_description")
+ );
+
+export default Page;
+import LegacyPage, { GetLayout } from "@pages/settings/teams/[id]/onboard-members";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("add_team_members"),
+ (t) => t("add_team_members_description")
+ );
+
+export default WithLayout({ Page: LegacyPage, getLayout: GetLayout })<"P">;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout })<"L">;
+import { _generateMetadata } from "app/_utils";
+
+import Page from "@calcom/features/ee/teams/pages/team-members-view";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("team_members"),
+ (t) => t("members_team_description")
+ );
+
+export default Page;
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir";
+
+export default WithLayout({ getLayout })<"L">;
+import Page from "@pages/settings/billing/index";
+import { _generateMetadata } from "app/_utils";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("billing"),
+ (t) => t("team_billing_description")
+ );
+
+export default Page;
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import MeetingEnded from "~/videos/views/videos-meeting-ended-single-view";
+import { getServerSideProps } from "~/videos/views/videos-meeting-ended-single-view.getServerSideProps";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ () => "Meeting Unavailable",
+ () => "Meeting Unavailable"
+ );
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({ getData, Page: MeetingEnded, getLayout: null })<"P">;
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import type { PageProps } from "app/_types";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import prisma, { bookingMinimalSelect } from "@calcom/prisma";
+
+import MeetingNotStarted from "~/videos/views/videos-meeting-not-started-single-view";
+import { getServerSideProps } from "~/videos/views/videos-meeting-not-started-single-view.getServerSideProps";
+
+export const generateMetadata = async ({ params }: PageProps) => {
+ const booking = await prisma.booking.findUnique({
+ where: {
+ uid: typeof params?.uid === "string" ? params.uid : "",
+ },
+ select: bookingMinimalSelect,
+ });
+
+ return await _generateMetadata(
+ (t) => t("this_meeting_has_not_started_yet"),
+ () => booking?.title ?? ""
+ );
+};
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({ getData, Page: MeetingNotStarted, getLayout: null })<"P">;
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { APP_NAME } from "@calcom/lib/constants";
+
+import VideosSingleView from "~/videos/views/videos-single-view";
+import { getServerSideProps, type PageProps } from "~/videos/views/videos-single-view.getServerSideProps";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ () => `${APP_NAME} Video`,
+ (t) => t("quick_video_meeting")
+ );
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({ getData, Page: VideosSingleView, getLayout: null })<"P">;
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import type { GetServerSidePropsContext } from "next";
+
+import { ssrInit } from "@server/lib/ssr";
+
+import NoMeetingFound from "~/videos/views/videos-no-meeting-found-single-view";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("no_meeting_found"),
+ (t) => t("no_meeting_found")
+ );
+
+// ssr was added by Intuita, legacy page does not have it
+const getData = async (context: GetServerSidePropsContext) => {
+ const ssr = await ssrInit(context);
+
+ return {
+ dehydratedState: ssr.dehydrate(),
+ };
+};
+
+export default WithLayout({ getData, Page: NoMeetingFound, getLayout: null })<"P">;
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/MainLayoutAppDir";
+
+import EnterprisePage from "@components/EnterprisePage";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("create_your_org"),
+ (t) => t("create_your_org_description")
+ );
+
+export default WithLayout({ getLayout, Page: EnterprisePage })<"P">;
+import { withAppDirSsg } from "app/WithAppDirSsg";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import type { InferGetStaticPropsType } from "next";
+
+import { getLayout } from "@calcom/features/MainLayoutAppDir";
+import { APP_NAME } from "@calcom/lib/constants";
+
+import { validStatuses } from "~/bookings/lib/validStatuses";
+import Page from "~/bookings/views/bookings-listing-view";
+import { getStaticProps } from "~/bookings/views/bookings-listing-view.getStaticProps";
+
+type Y = InferGetStaticPropsType;
+const getData = withAppDirSsg(getStaticProps);
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => `${APP_NAME} | ${t("bookings")}`,
+ () => ""
+ );
+
+export const generateStaticParams = async () => {
+ return validStatuses.map((status) => ({ status }));
+};
+
+export default WithLayout({ getLayout, getData, Page })<"P">;
+
+export const dynamic = "force-static";
+import VerifyPage from "@pages/auth/verify";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { WithLayout } from "app/layoutHOC";
+
+import { getServerSideProps } from "@server/lib/auth/verify/getServerSideProps";
+
+export default WithLayout({
+ getLayout: null,
+ Page: VerifyPage,
+ getData: withAppDirSsr(getServerSideProps),
+})<"P">;
+import Setup from "@pages/auth/setup";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { WithLayout } from "app/layoutHOC";
+import type { InferGetServerSidePropsType } from "next";
+
+import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
+
+export default WithLayout({
+ getLayout: null,
+ Page: Setup,
+ getData: withAppDirSsr>(getServerSideProps),
+})<"P">;
+import Logout from "@pages/auth/logout";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { WithLayout } from "app/layoutHOC";
+
+import { getServerSideProps } from "@server/lib/auth/logout/getServerSideProps";
+
+export default WithLayout({ getLayout: null, Page: Logout, getData: withAppDirSsr(getServerSideProps) })<"P">;
+import VerifyEmailPage from "@pages/auth/verify-email";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ (t) => t("check_your_email"),
+ (t) => t("check_your_email")
+ );
+};
+
+export default WithLayout({
+ getLayout: null,
+ Page: VerifyEmailPage,
+})<"P">;
+import DirectSSOLogin from "@pages/auth/sso/direct";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { WithLayout } from "app/layoutHOC";
+
+import { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps";
+
+export default WithLayout({
+ getLayout: null,
+ Page: DirectSSOLogin,
+ getData: withAppDirSsr(getServerSideProps),
+})<"P">;
+import Provider from "@pages/auth/sso/[provider]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { WithLayout } from "app/layoutHOC";
+import type { InferGetServerSidePropsType } from "next";
+
+import { getServerSideProps } from "@server/lib/auth/sso/[provider]/getServerSideProps";
+
+export default WithLayout({
+ getLayout: null,
+ Page: Provider,
+ getData: withAppDirSsr>(getServerSideProps),
+})<"P">;
+import signin from "@pages/auth/signin";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { WithLayout } from "app/layoutHOC";
+import type { InferGetServerSidePropsType } from "next";
+
+import { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps";
+
+export default WithLayout({
+ getLayout: null,
+ Page: signin,
+ // @ts-expect-error TODO: fix this
+ getData: withAppDirSsr>(getServerSideProps),
+})<"P">;
+import SetNewUserPassword from "@pages/auth/forgot-password/[id]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { getServerSideProps } from "@server/lib/forgot-password/[id]/getServerSideProps";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ (t) => t("reset_password"),
+ (t) => t("change_your_password")
+ );
+};
+
+export default WithLayout({
+ getLayout: null,
+ Page: SetNewUserPassword,
+ getData: withAppDirSsr(getServerSideProps),
+})<"P">;
+import ForgotPassword from "@pages/auth/forgot-password";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { getServerSideProps } from "@server/lib/forgot-password/getServerSideProps";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ (t) => t("reset_password"),
+ (t) => t("change_your_password")
+ );
+};
+
+export default WithLayout({
+ getLayout: null,
+ Page: ForgotPassword,
+ getData: withAppDirSsr(getServerSideProps),
+})<"P">;
+import Login from "@pages/auth/login";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import type { InferGetServerSidePropsType } from "next";
+
+import { APP_NAME } from "@calcom/lib/constants";
+
+import { getServerSideProps } from "@server/lib/auth/login/getServerSideProps";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ (t) => `${t("login")} | ${APP_NAME}`,
+ (t) => t("login")
+ );
+};
+
+export default WithLayout({
+ getLayout: null,
+ Page: Login,
+ getData: withAppDirSsr>(getServerSideProps),
+})<"P">;
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import withEmbedSsrAppDir from "app/WithEmbedSSR";
+import { WithLayout } from "app/layoutHOC";
+
+import OldPage from "~/bookings/views/bookings-single-view";
+import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
+
+const getData = withAppDirSsr(getServerSideProps);
+
+const getEmbedData = withEmbedSsrAppDir(getData);
+
+export default WithLayout({ getLayout: null, getData: getEmbedData, Page: OldPage });
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import type { Params, SearchParams } from "app/_types";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { type GetServerSidePropsContext } from "next";
+import { cookies, headers } from "next/headers";
+
+import { BookingStatus } from "@calcom/prisma/enums";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+
+import OldPage from "~/bookings/views/bookings-single-view";
+import { getServerSideProps, type PageProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Params;
+ searchParams: SearchParams;
+}) => {
+ const { bookingInfo, eventType, recurringBookings } = await getData(
+ buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
+ );
+ const needsConfirmation = bookingInfo.status === BookingStatus.PENDING && eventType.requiresConfirmation;
+
+ return await _generateMetadata(
+ (t) =>
+ t(`booking_${needsConfirmation ? "submitted" : "confirmed"}${recurringBookings ? "_recurring" : ""}`),
+ (t) =>
+ t(`booking_${needsConfirmation ? "submitted" : "confirmed"}${recurringBookings ? "_recurring" : ""}`)
+ );
+};
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({ getLayout: null, getData, Page: OldPage });
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import type { Params, SearchParams } from "app/_types";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { cookies, headers } from "next/headers";
+
+import { APP_NAME } from "@calcom/lib/constants";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+
+import EventTypePageWrapper from "~/event-types/views/event-types-single-view";
+import { getServerSideProps } from "~/event-types/views/event-types-single-view.getServerSideProps";
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Params;
+ searchParams: SearchParams;
+}) => {
+ const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
+ const { eventType } = await getData(legacyCtx);
+
+ return await _generateMetadata(
+ (t) => `${eventType.title} | ${t("event_type")} | ${APP_NAME}`,
+ () => ""
+ );
+};
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({ getLayout: null, getData, Page: EventTypePageWrapper })<"P">;
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/MainLayoutAppDir";
+
+import EventTypes from "~/event-types/views/event-types-listing-view";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("event_types_page_title"),
+ (t) => t("event_types_page_subtitle")
+ );
+
+export default WithLayout({ getLayout, Page: EventTypes })<"P">;
+import { type PageProps } from "@pages/team/[slug]";
+import EmbedPage from "@pages/team/[slug]/embed";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import withEmbedSsrAppDir from "app/WithEmbedSSR";
+import type { Params, SearchParams } from "app/_types";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { type GetServerSidePropsContext } from "next";
+import { cookies, headers } from "next/headers";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Params;
+ searchParams: SearchParams;
+}) => {
+ const props = await getData(
+ buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
+ );
+ const teamName = props.team.name || "Nameless Team";
+
+ return await _generateMetadata(
+ () => teamName,
+ () => teamName
+ );
+};
+
+const getEmbedData = withEmbedSsrAppDir(getData);
+
+export default WithLayout({
+ Page: EmbedPage,
+ getData: getEmbedData,
+ getLayout: null,
+ isBookingPage: true,
+})<"P">;
+import TypePage, { type PageProps } from "@pages/team/[slug]/[type]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import type { Params, SearchParams } from "app/_types";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { type GetServerSidePropsContext } from "next";
+import { cookies, headers } from "next/headers";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Params;
+ searchParams: SearchParams;
+}) => {
+ const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
+ const props = await getData(legacyCtx as unknown as GetServerSidePropsContext);
+ const { entity, user, slug, booking } = props;
+ const { trpc } = await import("@calcom/trpc");
+ const { data: event } = trpc.viewer.public.event.useQuery(
+ { username: user, eventSlug: slug, isTeamEvent: false, org: entity.orgSlug ?? null },
+ { refetchOnWindowFocus: false }
+ );
+
+ const profileName = event?.profile?.name ?? "";
+ const title = event?.title ?? "";
+
+ return await _generateMetadata(
+ (t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
+ (t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
+ );
+};
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({
+ Page: TypePage,
+ getData,
+ getLayout: null,
+ isBookingPage: true,
+})<"P">;
+import TeamPage, { type PageProps } from "@pages/team/[slug]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import type { Params, SearchParams } from "app/_types";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { type GetServerSidePropsContext } from "next";
+import { cookies, headers } from "next/headers";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Params;
+ searchParams: SearchParams;
+}) => {
+ const props = await getData(
+ buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
+ );
+ const teamName = props.team.name || "Nameless Team";
+
+ return await _generateMetadata(
+ () => teamName,
+ () => teamName
+ );
+};
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({
+ Page: TeamPage,
+ getData,
+ getLayout: null,
+ isBookingPage: true,
+})<"P">;
+import { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
+import Page from "@pages/org/[orgSlug]/[user]/embed";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import withEmbedSsrAppDir from "app/WithEmbedSSR";
+import { WithLayout } from "app/layoutHOC";
+
+const getData = withAppDirSsr(getServerSideProps);
+
+const getEmbedData = withEmbedSsrAppDir(getData);
+
+export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
+import { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
+import Page from "@pages/org/[orgSlug]/[user]/[type]/embed";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import withEmbedSsrAppDir from "app/WithEmbedSSR";
+import { WithLayout } from "app/layoutHOC";
+
+import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
+
+const getData = withAppDirSsr(getServerSideProps);
+const getEmbedData = withEmbedSsrAppDir(getData);
+
+export default WithLayout({ getLayout: null, getData: getEmbedData, isBookingPage: true, Page });
+import Page, { type PageProps } from "@pages/org/[orgSlug]/[user]/[type]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { WithLayout } from "app/layoutHOC";
+
+import { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({ getLayout: null, getData, isBookingPage: true, Page });
+import Page, { getServerSideProps, type PageProps } from "@pages/org/[orgSlug]/[user]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { WithLayout } from "app/layoutHOC";
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({ getLayout: null, getData, isBookingPage: true, Page });
+import TeamPage, { type PageProps } from "@pages/team/[slug]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import type { Params, SearchParams } from "app/_types";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { type GetServerSidePropsContext } from "next";
+import { cookies, headers } from "next/headers";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Params;
+ searchParams: SearchParams;
+}) => {
+ const props = await getData(
+ buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
+ );
+ const teamName = props.team.name || "Nameless Team";
+
+ return await _generateMetadata(
+ () => teamName,
+ () => teamName
+ );
+};
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({
+ Page: TeamPage,
+ getData,
+ getLayout: null,
+ isBookingPage: true,
+})<"P">;
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import PaymentPage from "@calcom/features/ee/payments/components/PaymentPage";
+import { getServerSideProps, type PaymentPageProps } from "@calcom/features/ee/payments/pages/payment";
+import { APP_NAME } from "@calcom/lib/constants";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ // the title does not contain the eventName as in the legacy page
+ (t) => `${t("payment")} | ${APP_NAME}`,
+ () => ""
+ );
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({ getLayout: null, getData, Page: PaymentPage });
+import LegacyPage, { type SignupProps } from "@pages/signup";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { getServerSideProps } from "@lib/signup/getServerSideProps";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("sign_up"),
+ (t) => t("sign_up")
+ );
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({
+ Page: LegacyPage,
+ getLayout: null,
+ getData,
+})<"P">;
+import LegacyPage, { getStaticProps } from "@pages/workflows/[workflow]";
+import { withAppDirSsg } from "app/WithAppDirSsg";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { type GetServerSidePropsContext } from "next";
+import { headers, cookies } from "next/headers";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Record;
+ searchParams: { [key: string]: string | string[] | undefined };
+}) => {
+ const { workflow } = await getData(
+ buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
+ );
+ return await _generateMetadata(
+ () => workflow ?? "Untitled",
+ () => ""
+ );
+};
+
+const getData = withAppDirSsg(getStaticProps);
+
+export const generateStaticParams = () => [];
+
+// @ts-expect-error TODO: fix this
+export default WithLayout({ getLayout: null, getData, Page: LegacyPage })<"P">;
+export const dynamic = "force-static";
+// generate segments on demand
+export const dynamicParams = true;
+export const revalidate = 10;
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/MainLayoutAppDir";
+import LegacyPage from "@calcom/features/ee/workflows/pages/index";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("workflows"),
+ (t) => t("workflows_to_automate_notifications")
+ );
+
+export default WithLayout({ getLayout, Page: LegacyPage })<"P">;
+import LegacyPage from "@pages/connect-and-join";
+import { WithLayout } from "app/layoutHOC";
+
+export default WithLayout({ getLayout: null, Page: LegacyPage })<"P">;
+import Page from "@pages/more";
+import { WithLayout } from "app/layoutHOC";
+
+export default WithLayout({ getLayout: null, Page })<"P">;
+import Page from "@pages/getting-started/[[...step]]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { WithLayout } from "app/layoutHOC";
+
+import { getServerSideProps } from "@lib/getting-started/[[...step]]/getServerSideProps";
+
+export default WithLayout({ getLayout: null, getData: withAppDirSsr(getServerSideProps), Page });
+import LegacyPage, { type PageProps, getServerSideProps } from "@pages/team/[slug]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import withEmbedSsrAppDir from "app/WithEmbedSSR";
+import { WithLayout } from "app/layoutHOC";
+
+const getData = withAppDirSsr(getServerSideProps);
+const getEmbedData = withEmbedSsrAppDir(getData);
+
+export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
+import LegacyPage, { getServerSideProps } from "@pages/team/[slug]/[type]/embed";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import withEmbedSsrAppDir from "app/WithEmbedSSR";
+import { WithLayout } from "app/layoutHOC";
+
+const getData = withAppDirSsr(getServerSideProps);
+const getEmbedData = withEmbedSsrAppDir(getData);
+
+// @ts-expect-error Type is missing the following properties from type: entity, duration, booking, away, and 7 more.
+export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
+import LegacyPage, { type PageProps, getServerSideProps } from "@pages/team/[slug]/[type]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { type GetServerSidePropsContext } from "next";
+import { cookies, headers } from "next/headers";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Record;
+ searchParams: { [key: string]: string | string[] | undefined };
+}) => {
+ const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
+ const props = await getData(legacyCtx as unknown as GetServerSidePropsContext);
+ const { entity, user, slug, booking } = props;
+ const { trpc } = await import("@calcom/trpc");
+ const { data: event } = trpc.viewer.public.event.useQuery(
+ { username: user, eventSlug: slug, isTeamEvent: false, org: entity.orgSlug ?? null },
+ { refetchOnWindowFocus: false }
+ );
+
+ const profileName = event?.profile?.name ?? "";
+ const title = event?.title ?? "";
+
+ return await _generateMetadata(
+ (t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
+ (t) => `${booking?.uid ? t("reschedule") : ""} ${title}`
+ );
+};
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({
+ Page: LegacyPage,
+ getData,
+ getLayout: null,
+ isBookingPage: true,
+})<"P">;
+import LegacyPage, { getServerSideProps, type PageProps } from "@pages/team/[slug]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { type GetServerSidePropsContext } from "next";
+import { cookies, headers } from "next/headers";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Record;
+ searchParams: { [key: string]: string | string[] | undefined };
+}) => {
+ const props = await getData(
+ buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
+ );
+ const teamName = props.team.name || "Nameless Team";
+
+ return await _generateMetadata(
+ () => teamName,
+ () => teamName
+ );
+};
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({
+ Page: LegacyPage,
+ getData,
+ getLayout: null,
+ isBookingPage: true,
+})<"P">;
+import Page from "@pages/teams/index";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/MainLayoutAppDir";
+
+import { getServerSideProps } from "@lib/teams/getServerSideProps";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => t("teams"),
+ (t) => t("create_manage_teams_collaborative")
+ );
+
+export default WithLayout({ getData: withAppDirSsr(getServerSideProps), getLayout, Page })<"P">;
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import withEmbedSsrAppDir from "app/WithEmbedSSR";
+import { WithLayout } from "app/layoutHOC";
+
+import LegacyPage from "~/users/views/users-public-view";
+import { getServerSideProps, type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
+
+const getData = withAppDirSsr(getServerSideProps);
+
+const getEmbedData = withEmbedSsrAppDir(getData);
+
+export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import withEmbedSsrAppDir from "app/WithEmbedSSR";
+import { WithLayout } from "app/layoutHOC";
+
+import LegacyPage from "~/users/views/users-type-public-view";
+import { getServerSideProps, type PageProps } from "~/users/views/users-type-public-view.getServerSideProps";
+
+const getData = withAppDirSsr(getServerSideProps);
+
+const getEmbedData = withEmbedSsrAppDir(getData);
+
+export default WithLayout({ getLayout: null, getData: getEmbedData, Page: LegacyPage })<"P">;
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { type GetServerSidePropsContext } from "next";
+import { headers, cookies } from "next/headers";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+
+import LegacyPage from "~/users/views/users-type-public-view";
+import { getServerSideProps, type PageProps } from "~/users/views/users-type-public-view.getServerSideProps";
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Record;
+ searchParams: { [key: string]: string | string[] | undefined };
+}) => {
+ const props = await getData(
+ buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
+ );
+
+ const { eventData, booking, user, slug } = props;
+ const rescheduleUid = booking?.uid;
+ const { trpc } = await import("@calcom/trpc");
+ const { data: event } = trpc.viewer.public.event.useQuery(
+ { username: user, eventSlug: slug, isTeamEvent: false, org: eventData.entity.orgSlug ?? null },
+ { refetchOnWindowFocus: false }
+ );
+
+ const profileName = event?.profile?.name ?? "";
+ const title = event?.title ?? "";
+
+ return await _generateMetadata(
+ (t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
+ (t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
+ );
+};
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({
+ getData,
+ Page: LegacyPage,
+ getLayout: null,
+})<"P">;
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { type GetServerSidePropsContext } from "next";
+import { headers, cookies } from "next/headers";
+
+import { getLayout } from "@calcom/features/MainLayoutAppDir";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+
+import LegacyPage from "~/users/views/users-public-view";
+import { getServerSideProps, type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Record;
+ searchParams: { [key: string]: string | string[] | undefined };
+}) => {
+ const props = await getData(
+ buildLegacyCtx(headers(), cookies(), params, searchParams) as unknown as GetServerSidePropsContext
+ );
+
+ const { profile, markdownStrippedBio } = props;
+ return await _generateMetadata(
+ () => profile.name,
+ () => markdownStrippedBio
+ );
+};
+
+const getData = withAppDirSsr(getServerSideProps);
+export default WithLayout({ getLayout, getData, Page: LegacyPage })<"P">;
+import { redirect } from "next/navigation";
+
+const getPageProps = () => {
+ return redirect(`/apps/routing-forms/forms`);
+};
+const Page = () => {
+ getPageProps();
+
+ return null;
+};
+
+export default Page;
+import { type SearchParams } from "app/_types";
+import { type GetServerSidePropsContext } from "next";
+import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
+import { cookies, headers } from "next/headers";
+import { redirect } from "next/navigation";
+import z from "zod";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+
+const paramsSchema = z
+ .object({
+ pages: z.array(z.string()),
+ })
+ .catch({
+ pages: [],
+ });
+
+const getPageProps = async (context: GetServerSidePropsContext) => {
+ const { pages } = paramsSchema.parse(context.params);
+
+ return redirect(`/apps/routing-forms/${pages.length ? pages.join("/") : ""}`);
+};
+
+type PageProps = Readonly<{
+ params: Params;
+ searchParams: SearchParams;
+}>;
+
+const Page = async ({ params, searchParams }: PageProps) => {
+ const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
+ await getPageProps(legacyCtx as unknown as GetServerSidePropsContext);
+
+ return null;
+};
+
+export default Page;
+import LegacyPage from "@pages/maintenance";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { APP_NAME } from "@calcom/lib/constants";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ (t) => `${t("under_maintenance")} | ${APP_NAME}`,
+ (t) => t("under_maintenance_description", { appName: APP_NAME })
+ );
+
+export default WithLayout({ getLayout: null, Page: LegacyPage })<"P">;
+import Page from "@pages/apps/installed/[category]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { APP_NAME } from "@calcom/lib/constants";
+
+import { getServerSideProps } from "@lib/apps/installed/[category]/getServerSideProps";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ (t) => `${t("installed_apps")} | ${APP_NAME}`,
+ (t) => t("manage_your_connected_apps")
+ );
+};
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({ getLayout: null, getData, Page });
+import AppsPage from "@pages/apps";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { getLayout } from "@calcom/features/MainLayoutAppDir";
+import { APP_NAME } from "@calcom/lib/constants";
+
+import { getServerSideProps } from "@lib/apps/getServerSideProps";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ () => `Apps | ${APP_NAME}`,
+ () => ""
+ );
+};
+
+export default WithLayout({ getLayout, getData: withAppDirSsr(getServerSideProps), Page: AppsPage });
+import CategoryPage, { type PageProps } from "@pages/apps/categories/[category]";
+import { Prisma } from "@prisma/client";
+import { withAppDirSsg } from "app/WithAppDirSsg";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { APP_NAME } from "@calcom/lib/constants";
+import prisma from "@calcom/prisma";
+import { AppCategories } from "@calcom/prisma/enums";
+
+import { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ () => `${APP_NAME} | ${APP_NAME}`,
+ () => ""
+ );
+};
+
+export const generateStaticParams = async () => {
+ const paths = Object.keys(AppCategories);
+
+ try {
+ await prisma.$queryRaw`SELECT 1`;
+ } catch (e: unknown) {
+ if (e instanceof Prisma.PrismaClientInitializationError) {
+ // Database is not available at build time. Make sure we fall back to building these pages on demand
+ return [];
+ } else {
+ throw e;
+ }
+ }
+
+ return paths.map((category) => ({ category }));
+};
+
+const getData = withAppDirSsg(getStaticProps);
+
+export default WithLayout({ getData, Page: CategoryPage, getLayout: null })<"P">;
+export const dynamic = "force-static";
+import Page from "@pages/apps/categories/index";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+
+import { APP_NAME } from "@calcom/lib/constants";
+
+import { getServerSideProps } from "@lib/apps/categories/getServerSideProps";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ () => `Categories | ${APP_NAME}`,
+ () => ""
+ );
+};
+
+export default WithLayout({ getData: withAppDirSsr(getServerSideProps), Page, getLayout: null })<"P">;
+import Page from "@pages/apps/[slug]/setup";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import type { InferGetServerSidePropsType } from "next";
+
+import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
+import { APP_NAME } from "@calcom/lib/constants";
+
+export const generateMetadata = async ({ params }: { params: Record }) => {
+ return await _generateMetadata(
+ () => `${params.slug} | ${APP_NAME}`,
+ () => ""
+ );
+};
+
+type T = InferGetServerSidePropsType;
+
+const getData = withAppDirSsr(getServerSideProps);
+
+export default WithLayout({ getLayout: null, Page, getData });
+import LegacyPage, { getLayout } from "@pages/apps/[slug]/[...pages]";
+import type { PageProps, SearchParams } from "app/_types";
+import { _generateMetadata } from "app/_utils";
+import type { GetServerSidePropsContext } from "next";
+import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
+import { cookies, headers } from "next/headers";
+import { notFound, redirect } from "next/navigation";
+import z from "zod";
+
+import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
+import RoutingFormsRoutingConfig, {
+ serverSidePropsConfig,
+} from "@calcom/app-store/routing-forms/pages/app-routing.config";
+import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { APP_NAME } from "@calcom/lib/constants";
+import prisma from "@calcom/prisma";
+import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
+
+import type { AppProps } from "@lib/app-providers";
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+
+import PageWrapper from "@components/PageWrapperAppDir";
+
+import { ssrInit } from "@server/lib/ssr";
+
+type AppPageType = {
+ getServerSideProps: AppGetServerSideProps;
+ // A component than can accept any properties
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ default: ((props: any) => JSX.Element) &
+ Pick;
+};
+
+type Found = {
+ notFound: false;
+ Component: AppPageType["default"];
+ getServerSideProps: AppPageType["getServerSideProps"];
+};
+
+const AppsRouting = {
+ "routing-forms": RoutingFormsRoutingConfig,
+ typeform: TypeformRoutingConfig,
+};
+
+const paramsSchema = z.object({
+ slug: z.string(),
+ pages: z.array(z.string()),
+});
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Params;
+ searchParams: SearchParams;
+}) => {
+ const p = paramsSchema.safeParse(params);
+
+ if (!p.success) {
+ return notFound();
+ }
+
+ const mainPage = p.data.pages[0];
+
+ if (mainPage === "forms") {
+ return await _generateMetadata(
+ () => `Forms | ${APP_NAME}`,
+ () => ""
+ );
+ }
+
+ const legacyContext = buildLegacyCtx(
+ headers(),
+ cookies(),
+ params,
+ searchParams
+ ) as unknown as GetServerSidePropsContext;
+ const { form } = await getPageProps(legacyContext);
+
+ return await _generateMetadata(
+ () => `${form.name} | ${APP_NAME}`,
+ () => form.description
+ );
+};
+
+function getRoute(appName: string, pages: string[]) {
+ const routingConfig = AppsRouting[appName as keyof typeof AppsRouting] as Record;
+
+ if (!routingConfig) {
+ notFound();
+ }
+
+ const mainPage = pages[0];
+ const appPage = routingConfig.layoutHandler || (routingConfig[mainPage] as AppPageType);
+
+ const getServerSidePropsHandler = serverSidePropsConfig[mainPage];
+
+ if (!appPage) {
+ notFound();
+ }
+
+ return {
+ notFound: false,
+ Component: appPage.default,
+ ...appPage,
+ getServerSideProps: getServerSidePropsHandler,
+ } as Found;
+}
+
+const getPageProps = async ({ params, query, req }: GetServerSidePropsContext) => {
+ const p = paramsSchema.safeParse(params);
+
+ if (!p.success) {
+ return notFound();
+ }
+
+ const { slug: appName, pages } = p.data;
+
+ const route = getRoute(appName, pages);
+
+ if (route.notFound) {
+ return route;
+ }
+
+ if (route.getServerSideProps) {
+ // TODO: Document somewhere that right now it is just a convention that filename should have appPages in it's name.
+ // appPages is actually hardcoded here and no matter the fileName the same variable would be used.
+ // We can write some validation logic later on that ensures that [...appPages].tsx file exists
+ params!.appPages = pages.slice(1);
+
+ const ctx = { req, params, query };
+
+ const session = await getServerSession({ req });
+ const user = session?.user;
+ const app = await getAppWithMetadata({ slug: appName });
+
+ if (!app) {
+ notFound();
+ }
+
+ const result = await route.getServerSideProps(
+ {
+ ...ctx,
+ params: {
+ ...ctx.params,
+ appPages: pages.slice(1),
+ },
+ } as GetServerSidePropsContext<{
+ slug: string;
+ pages: string[];
+ appPages: string[];
+ }>,
+ prisma,
+ user,
+ ssrInit
+ );
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ //@ts-ignore
+
+ if (result.notFound) {
+ notFound();
+ }
+
+ if (result.redirect) {
+ redirect(result.redirect.destination);
+ }
+
+ return {
+ appName,
+ appUrl: app.simplePath || `/apps/${appName}`,
+ ...result.props,
+ };
+ } else {
+ return {
+ appName,
+ };
+ }
+};
+
+export default async function Page({ params, searchParams }: PageProps) {
+ const h = headers();
+ const nonce = h.get("x-nonce") ?? undefined;
+
+ const legacyContext = buildLegacyCtx(
+ h,
+ cookies(),
+ params,
+ searchParams
+ ) as unknown as GetServerSidePropsContext;
+ const props = await getPageProps(legacyContext);
+ return (
+
+
+
+ );
+}
+import Page from "@pages/apps/[slug]/index";
+import { Prisma } from "@prisma/client";
+import { withAppDirSsg } from "app/WithAppDirSsg";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import type { InferGetStaticPropsType } from "next";
+import { cookies, headers } from "next/headers";
+
+import { APP_NAME } from "@calcom/lib/constants";
+import prisma from "@calcom/prisma";
+
+import { getStaticProps } from "@lib/apps/[slug]/getStaticProps";
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+
+type Y = InferGetStaticPropsType;
+const getData = withAppDirSsg(getStaticProps);
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Record;
+ searchParams: { [key: string]: string | string[] | undefined };
+}) => {
+ const legacyContext = buildLegacyCtx(headers(), cookies(), params, searchParams);
+ const res = await getData(legacyContext);
+
+ return await _generateMetadata(
+ () => `${res?.data.name} | ${APP_NAME}`,
+ () => res?.data.description ?? ""
+ );
+};
+
+export const generateStaticParams = async () => {
+ try {
+ const appStore = await prisma.app.findMany({ select: { slug: true } });
+ return appStore.map(({ slug }) => ({ slug }));
+ } catch (e: unknown) {
+ if (e instanceof Prisma.PrismaClientInitializationError) {
+ // Database is not available at build time, but that's ok – we fall back to resolving paths on demand
+ } else {
+ throw e;
+ }
+ }
+
+ return [];
+};
+
+export default WithLayout({ getLayout: null, Page, getData });
+
+export const dynamic = "force-static";
+import LegacyPage from "@pages/d/[link]/[slug]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import type { Params, SearchParams } from "app/_types";
+import { _generateMetadata } from "app/_utils";
+import { WithLayout } from "app/layoutHOC";
+import { cookies, headers } from "next/headers";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+import { getServerSideProps } from "@lib/d/[link]/[slug]/getServerSideProps";
+
+export const generateMetadata = async ({
+ params,
+ searchParams,
+}: {
+ params: Params;
+ searchParams: SearchParams;
+}) => {
+ const pageProps = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
+
+ const { entity, booking, user, slug, isTeamEvent } = pageProps;
+ const rescheduleUid = booking?.uid;
+ const { trpc } = await import("@calcom/trpc");
+ const { data: event } = trpc.viewer.public.event.useQuery(
+ { username: user ?? "", eventSlug: slug ?? "", isTeamEvent, org: entity.orgSlug ?? null },
+ { refetchOnWindowFocus: false }
+ );
+ const profileName = event?.profile?.name ?? "";
+ const title = event?.title ?? "";
+ return await _generateMetadata(
+ (t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
+ (t) => `${rescheduleUid ? t("reschedule") : ""} ${title}`
+ );
+};
+
+const getData = withAppDirSsr(getServerSideProps);
+export default WithLayout({ getLayout: null, Page: LegacyPage, getData })<"P">;
+import { getServerSideProps } from "@pages/reschedule/[uid]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import type { SearchParams } from "app/_types";
+import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
+import { cookies, headers } from "next/headers";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+import withEmbedSsr from "@lib/withEmbedSsr";
+
+type PageProps = Readonly<{
+ params: Params;
+ searchParams: SearchParams;
+}>;
+
+const Page = async ({ params, searchParams }: PageProps) => {
+ const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
+ await withAppDirSsr(withEmbedSsr(getServerSideProps))(legacyCtx);
+
+ return null;
+};
+
+export default Page;
+import { getServerSideProps } from "@pages/reschedule/[uid]";
+import { withAppDirSsr } from "app/WithAppDirSsr";
+import type { SearchParams } from "app/_types";
+import { _generateMetadata } from "app/_utils";
+import type { Params } from "next/dist/shared/lib/router/utils/route-matcher";
+import { headers, cookies } from "next/headers";
+
+import { buildLegacyCtx } from "@lib/buildLegacyCtx";
+
+export const generateMetadata = async () =>
+ await _generateMetadata(
+ () => "",
+ () => ""
+ );
+
+type PageProps = Readonly<{
+ params: Params;
+ searchParams: SearchParams;
+}>;
+
+const getData = withAppDirSsr(getServerSideProps);
+
+const Page = async ({ params, searchParams }: PageProps) => {
+ const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
+
+ await getData(legacyCtx);
+
+ return null;
+};
+
+export default Page;
+import type { GetServerSidePropsContext } from "next";
+import { isNotFoundError } from "next/dist/client/components/not-found";
+import { getURLFromRedirectError, isRedirectError } from "next/dist/client/components/redirect";
+import { notFound, redirect } from "next/navigation";
+
+import { WEBAPP_URL } from "@calcom/lib/constants";
+
+export type EmbedProps = {
+ isEmbed?: boolean;
+};
+
+export default function withEmbedSsrAppDir>(
+ getData: (context: GetServerSidePropsContext) => Promise
+) {
+ return async (context: GetServerSidePropsContext): Promise => {
+ const { embed, layout } = context.query;
+
+ try {
+ const props = await getData(context);
+
+ return {
+ ...props,
+ isEmbed: true,
+ };
+ } catch (e) {
+ if (isRedirectError(e)) {
+ const destinationUrl = getURLFromRedirectError(e);
+ let urlPrefix = "";
+
+ // Get the URL parsed from URL so that we can reliably read pathname and searchParams from it.
+ const destinationUrlObj = new URL(destinationUrl, WEBAPP_URL);
+
+ // If it's a complete URL, use the origin as the prefix to ensure we redirect to the same domain.
+ if (destinationUrl.search(/^(http:|https:).*/) !== -1) {
+ urlPrefix = destinationUrlObj.origin;
+ } else {
+ // Don't use any prefix for relative URLs to ensure we stay on the same domain
+ urlPrefix = "";
+ }
+
+ const destinationQueryStr = destinationUrlObj.searchParams.toString();
+ // Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
+ const newDestinationUrl = `${urlPrefix}${destinationUrlObj.pathname}/embed?${
+ destinationQueryStr ? `${destinationQueryStr}&` : ""
+ }layout=${layout}&embed=${embed}`;
+
+ redirect(newDestinationUrl);
+ }
+
+ if (isNotFoundError(e)) {
+ notFound();
+ }
+
+ throw e;
+ }
+ };
+}
+"use client";
+
+import { type NextPage } from "next";
+
+import CustomError, { type DefaultErrorProps } from "./error";
+
+export const GlobalError: NextPage = (props) => {
+ return (
+
+
+
+
+
+ );
+};
+
+export default GlobalError;
+import NotFoundPage from "@pages/404";
+import { WithLayout } from "app/layoutHOC";
+import type { GetStaticPropsContext } from "next";
+
+import { getTranslations } from "@server/lib/getTranslations";
+
+const getData = async (context: GetStaticPropsContext) => {
+ const i18n = await getTranslations(context);
+
+ return {
+ i18n,
+ };
+};
+
+export const dynamic = "force-static";
+
+export default WithLayout({ getLayout: null, getData, Page: NotFoundPage });
+import type { GetServerSidePropsContext } from "next";
+
+export const getServerSideProps = async (_context: GetServerSidePropsContext) => {
+ const EMAIL_FROM = process.env.EMAIL_FROM;
+
+ return {
+ props: {
+ EMAIL_FROM,
+ },
+ };
+};
+import type { GetServerSidePropsContext } from "next";
+
+import { ssrInit } from "@server/lib/ssr";
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const ssr = await ssrInit(context);
+ // Deleting old cookie manually, remove this code after all existing cookies have expired
+ context.res?.setHeader(
+ "Set-Cookie",
+ "next-auth.session-token=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;"
+ );
+
+ return {
+ props: {
+ trpcState: ssr.dehydrate(),
+ query: context.query,
+ },
+ };
+}
+import { samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
+
+export async function getServerSideProps() {
+ return {
+ props: {
+ samlTenantID,
+ samlProductID,
+ },
+ };
+}
+import type { GetServerSidePropsContext } from "next";
+
+import { getPremiumMonthlyPlanPriceId } from "@calcom/app-store/stripepayment/lib/utils";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
+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 { IS_PREMIUM_USERNAME_ENABLED } from "@calcom/lib/constants";
+import { checkUsername } from "@calcom/lib/server/checkUsername";
+import prisma from "@calcom/prisma";
+
+import { asStringOrNull } from "@lib/asStringOrNull";
+
+import { ssrInit } from "@server/lib/ssr";
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ // get query params and typecast them to string
+ // (would be even better to assert them instead of typecasting)
+ 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}` : ""}`;
+ if (!providerParam) {
+ throw new Error(`File is not named sso/[provider]`);
+ }
+
+ const { req } = context;
+
+ const session = await getServerSession({ req });
+ const ssr = await ssrInit(context);
+ const { currentOrgDomain } = orgDomainConfig(context.req);
+
+ if (session) {
+ // Validating if username is Premium, while this is true an email its required for stripe user confirmation
+ if (usernameParam && session.user.email) {
+ const availability = await checkUsername(usernameParam, currentOrgDomain);
+ if (availability.available && availability.premium && IS_PREMIUM_USERNAME_ENABLED) {
+ const stripePremiumUrl = await getStripePremiumUsernameUrl({
+ userEmail: session.user.email,
+ username: usernameParam,
+ successDestination,
+ });
+ if (stripePremiumUrl) {
+ return {
+ redirect: {
+ destination: stripePremiumUrl,
+ permanent: false,
+ },
+ };
+ }
+ }
+ }
+
+ return {
+ redirect: {
+ destination: successDestination,
+ permanent: false,
+ },
+ };
+ }
+
+ let error: string | null = null;
+
+ let tenant = samlTenantID;
+ let product = samlProductID;
+
+ if (providerParam === "saml" && hostedCal) {
+ if (!emailParam) {
+ error = "Email not provided";
+ } else {
+ try {
+ const ret = await ssoTenantProduct(prisma, emailParam);
+ tenant = ret.tenant;
+ product = ret.product;
+ } catch (e) {
+ if (e instanceof Error) {
+ error = e.message;
+ }
+ }
+ }
+ }
+
+ if (error) {
+ return {
+ redirect: {
+ destination: `/auth/error?error=${error}`,
+ permanent: false,
+ },
+ };
+ }
+
+ return {
+ props: {
+ trpcState: ssr.dehydrate(),
+ provider: providerParam,
+ isSAMLLoginEnabled,
+ hostedCal,
+ tenant,
+ product,
+ error,
+ },
+ };
+};
+
+type GetStripePremiumUsernameUrl = {
+ userEmail: string;
+ username: string;
+ successDestination: string;
+};
+
+const getStripePremiumUsernameUrl = async ({
+ userEmail,
+ username,
+ successDestination,
+}: GetStripePremiumUsernameUrl): Promise => {
+ // @TODO: probably want to check if stripe user email already exists? or not
+ const customer = await stripe.customers.create({
+ email: userEmail,
+ metadata: {
+ email: userEmail,
+ username,
+ },
+ });
+
+ const checkoutSession = await stripe.checkout.sessions.create({
+ mode: "subscription",
+ payment_method_types: ["card"],
+ customer: customer.id,
+ line_items: [
+ {
+ price: getPremiumMonthlyPlanPriceId(),
+ quantity: 1,
+ },
+ ],
+ success_url: `${process.env.NEXT_PUBLIC_WEBAPP_URL}${successDestination}&session_id={CHECKOUT_SESSION_ID}`,
+ cancel_url: process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com",
+ allow_promotion_codes: true,
+ });
+
+ return checkoutSession.url;
+};
+import type { GetServerSidePropsContext } from "next";
+import { getProviders, getCsrfToken } from "next-auth/react";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const { req } = context;
+
+ const session = await getServerSession({ req });
+ const csrfToken = await getCsrfToken(context);
+ const providers = await getProviders();
+ if (session) {
+ return {
+ redirect: { destination: "/" },
+ };
+ }
+ return {
+ props: {
+ csrfToken,
+ providers,
+ },
+ };
+}
+import { jwtVerify } from "jose";
+import type { GetServerSidePropsContext } from "next";
+import { getCsrfToken } from "next-auth/react";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
+import { WEBSITE_URL } from "@calcom/lib/constants";
+import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
+import prisma from "@calcom/prisma";
+
+import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
+import { ssrInit } from "@server/lib/ssr";
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const { req, res, query } = context;
+
+ const session = await getServerSession({ req, res });
+ const ssr = await ssrInit(context);
+
+ const verifyJwt = (jwt: string) => {
+ const secret = new TextEncoder().encode(process.env.CALENDSO_ENCRYPTION_KEY);
+
+ return jwtVerify(jwt, secret, {
+ issuer: WEBSITE_URL,
+ audience: `${WEBSITE_URL}/auth/login`,
+ algorithms: ["HS256"],
+ });
+ };
+
+ let totpEmail = null;
+ if (context.query.totp) {
+ try {
+ const decryptedJwt = await verifyJwt(context.query.totp as string);
+ if (decryptedJwt.payload) {
+ totpEmail = decryptedJwt.payload.email as string;
+ } else {
+ return {
+ redirect: {
+ destination: "/auth/error?error=JWT%20Invalid%20Payload",
+ permanent: false,
+ },
+ };
+ }
+ } catch (e) {
+ return {
+ redirect: {
+ destination: "/auth/error?error=Invalid%20JWT%3A%20Please%20try%20again",
+ permanent: false,
+ },
+ };
+ }
+ }
+
+ if (session) {
+ const { callbackUrl } = query;
+
+ if (callbackUrl) {
+ try {
+ const destination = getSafeRedirectUrl(callbackUrl as string);
+ if (destination) {
+ return {
+ redirect: {
+ destination,
+ permanent: false,
+ },
+ };
+ }
+ } catch (e) {
+ console.warn(e);
+ }
+ }
+
+ return {
+ redirect: {
+ destination: "/",
+ permanent: false,
+ },
+ };
+ }
+
+ const userCount = await prisma.user.count();
+ if (userCount === 0) {
+ // Proceed to new onboarding to create first admin user
+ return {
+ redirect: {
+ destination: "/auth/setup",
+ permanent: false,
+ },
+ };
+ }
+ return {
+ props: {
+ csrfToken: await getCsrfToken(context),
+ trpcState: ssr.dehydrate(),
+ isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
+ isSAMLLoginEnabled,
+ samlTenantID,
+ samlProductID,
+ totpEmail,
+ },
+ };
+}
+import type { GetServerSidePropsContext } from "next";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
+import prisma from "@calcom/prisma";
+import { UserPermissionRole } from "@calcom/prisma/enums";
+
+import { ssrInit } from "@server/lib/ssr";
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const { req } = context;
+
+ const ssr = await ssrInit(context);
+ const userCount = await prisma.user.count();
+
+ const session = await getServerSession({ req });
+
+ if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
+ return {
+ redirect: {
+ destination: `/404`,
+ permanent: false,
+ },
+ };
+ }
+
+ const deploymentKey = await prisma.deployment.findUnique({
+ where: { id: 1 },
+ select: { licenseKey: true },
+ });
+
+ // Check existant CALCOM_LICENSE_KEY env var and acccount for it
+ if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey?.licenseKey) {
+ await prisma.deployment.upsert({
+ where: { id: 1 },
+ update: {
+ licenseKey: process.env.CALCOM_LICENSE_KEY,
+ agreedLicenseAt: new Date(),
+ },
+ create: {
+ licenseKey: process.env.CALCOM_LICENSE_KEY,
+ agreedLicenseAt: new Date(),
+ },
+ });
+ }
+
+ const isFreeLicense = (await getDeploymentKey(prisma)) === "";
+
+ return {
+ props: {
+ trpcState: ssr.dehydrate(),
+ isFreeLicense,
+ userCount,
+ },
+ };
+}
+import type { GetServerSidePropsContext } from "next";
+import { getCsrfToken } from "next-auth/react";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+
+import { getLocale } from "@calcom/features/auth/lib/getLocale";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const { req, res } = context;
+
+ const session = await getServerSession({ req });
+
+ // @TODO res will not be available in future pages (app dir)
+ if (session) {
+ res.writeHead(302, { Location: "/" });
+ res.end();
+ return { props: {} };
+ }
+ const locale = await getLocale(context.req);
+
+ return {
+ props: {
+ csrfToken: await getCsrfToken(context),
+ ...(await serverSideTranslations(locale, ["common"])),
+ },
+ };
+}
+import type { GetServerSidePropsContext } from "next";
+import { getCsrfToken } from "next-auth/react";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+
+import { getLocale } from "@calcom/features/auth/lib/getLocale";
+import prisma from "@calcom/prisma";
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const id = context.params?.id as string;
+
+ let resetPasswordRequest = await prisma.resetPasswordRequest.findFirst({
+ where: {
+ id,
+ expires: {
+ gt: new Date(),
+ },
+ },
+ select: {
+ email: true,
+ },
+ });
+ try {
+ resetPasswordRequest &&
+ (await prisma.user.findUniqueOrThrow({ where: { email: resetPasswordRequest.email } }));
+ } catch (e) {
+ resetPasswordRequest = null;
+ }
+ const locale = await getLocale(context.req);
+ return {
+ props: {
+ isRequestExpired: !resetPasswordRequest,
+ requestId: id,
+ csrfToken: await getCsrfToken({ req: context.req }),
+ ...(await serverSideTranslations(locale, ["common"])),
+ },
+ };
+}
+import React from "react";
+import "react-calendar/dist/Calendar.css";
+import "react-date-picker/dist/DatePicker.css";
+import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
+
+import { Calendar } from "@calcom/ui/components/icon";
+
+import classNames from "@lib/classNames";
+
+type Props = {
+ date: Date;
+ onDatesChange?: ((date: Date) => void) | undefined;
+ className?: string;
+ disabled?: boolean;
+ minDate?: Date;
+};
+
+export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }: Props) => {
+ return (
+ }
+ value={date}
+ minDate={minDate}
+ disabled={disabled}
+ onChange={onDatesChange}
+ />
+ );
+};
+import type { GroupBase, Props, SingleValue } from "react-select";
+import { components } from "react-select";
+
+import type { EventLocationType } from "@calcom/app-store/locations";
+import { classNames } from "@calcom/lib";
+import invertLogoOnDark from "@calcom/lib/invertLogoOnDark";
+import { Select } from "@calcom/ui";
+
+export type LocationOption = {
+ label: string;
+ value: EventLocationType["type"];
+ icon?: string;
+ disabled?: boolean;
+ address?: string;
+ credentialId?: number;
+ teamName?: string;
+};
+
+export type SingleValueLocationOption = SingleValue;
+
+export type GroupOptionType = GroupBase;
+
+const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
+ return (
+
+ {icon &&
}
+
{label}
+
+ );
+};
+
+export default function LocationSelect(props: Props) {
+ return (
+
+ name="location"
+ id="location-select"
+ components={{
+ Option: (props) => {
+ return (
+
+
+
+ );
+ },
+ SingleValue: (props) => (
+
+
+
+ ),
+ }}
+ formatOptionLabel={(e) => (
+
+ {e.icon && (
+
+ )}
+
{e.label}
+
+ )}
+ formatGroupLabel={(e) => {e.label}
}
+ {...props}
+ />
+ );
+}
+import classNames from "classnames";
+import type { InputHTMLAttributes, ReactNode } from "react";
+import React, { forwardRef } from "react";
+
+type Props = InputHTMLAttributes & {
+ label?: ReactNode;
+};
+
+const MinutesField = forwardRef(({ label, ...rest }, ref) => {
+ return (
+
+ {!!label && (
+
+
+ {label}
+
+
+ )}
+
+
+ );
+});
+
+MinutesField.displayName = "MinutesField";
+
+export default MinutesField;
+import React from "react";
+import type { Props } from "react-select";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Avatar } from "@calcom/ui";
+import { X } from "@calcom/ui/components/icon";
+
+import Select from "@components/ui/form/Select";
+
+type CheckedSelectOption = {
+ avatar: string;
+ label: string;
+ value: string;
+ disabled?: boolean;
+};
+
+export const CheckedSelect = ({
+ options = [],
+ value = [],
+ ...props
+}: Omit, "value" | "onChange"> & {
+ value?: readonly CheckedSelectOption[];
+ onChange: (value: readonly CheckedSelectOption[]) => void;
+}) => {
+ const { t } = useLocale();
+ return (
+ <>
+
+ {value.map((option) => (
+
+
+ {option.label}
+
props.onChange(value.filter((item) => item.value !== option.value))}
+ className="text-subtle float-right mt-0.5 h-5 w-5 cursor-pointer"
+ />
+
+ ))}
+ >
+ );
+};
+
+export default CheckedSelect;
+import React, { useCallback, useEffect, useState } from "react";
+import type { GroupBase, Props, InputProps, SingleValue, MultiValue } from "react-select";
+import ReactSelect, { components } from "react-select";
+
+import classNames from "@calcom/lib/classNames";
+import { useGetTheme } from "@calcom/lib/hooks/useTheme";
+
+export type SelectProps<
+ Option,
+ IsMulti extends boolean = false,
+ Group extends GroupBase = GroupBase
+> = Props ;
+
+export const InputComponent = >({
+ inputClassName,
+ ...props
+}: InputProps ) => {
+ return (
+
+ );
+};
+
+function Select<
+ Option,
+ IsMulti extends boolean = false,
+ Group extends GroupBase = GroupBase
+>({ className, ...props }: SelectProps ) {
+ const [mounted, setMounted] = useState(false);
+ const { resolvedTheme, forcedTheme } = useGetTheme();
+ const hasDarkTheme = !forcedTheme && resolvedTheme === "dark";
+ const darkThemeColors = {
+ /** Dark Theme starts */
+ //primary - Border when selected and Selected Option background
+ primary: "rgb(41 41 41 / var(--tw-border-opacity))",
+
+ neutral0: "rgb(62 62 62 / var(--tw-bg-opacity))",
+ // Down Arrow hover color
+ neutral5: "white",
+
+ neutral10: "rgb(41 41 41 / var(--tw-border-opacity))",
+
+ // neutral20 - border color + down arrow default color
+ neutral20: "rgb(41 41 41 / var(--tw-border-opacity))",
+
+ // neutral30 - hover border color
+ neutral30: "rgb(41 41 41 / var(--tw-border-opacity))",
+
+ neutral40: "white",
+
+ danger: "white",
+
+ // Cross button in multiselect
+ dangerLight: "rgb(41 41 41 / var(--tw-border-opacity))",
+
+ // neutral50 - MultiSelect - "Select Text" color
+ neutral50: "white",
+
+ // neutral60 - Down Arrow color
+ neutral60: "white",
+
+ neutral70: "red",
+
+ // neutral80 - Selected option
+ neutral80: "white",
+
+ neutral90: "blue",
+
+ primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))",
+ primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
+ /** Dark Theme ends */
+ };
+
+ useEffect(() => {
+ setMounted(true);
+ }, []);
+
+ // Till we know in JS the theme is ready, we can't render react-select as it would render with light theme instead
+ if (!mounted) {
+ return ;
+ }
+
+ return (
+ ({
+ ...theme,
+ borderRadius: 6,
+ colors: {
+ ...theme.colors,
+ ...(hasDarkTheme
+ ? darkThemeColors
+ : {
+ /** Light Theme starts */
+ primary: "var(--brand-color)",
+ primary50: "rgba(209 , 213, 219, var(--tw-bg-opacity))",
+ primary25: "rgba(244, 245, 246, var(--tw-bg-opacity))",
+ /** Light Theme Ends */
+ }),
+ },
+ })}
+ styles={{
+ option: (provided, state) => ({
+ ...provided,
+ color: state.isSelected ? "var(--brand-text-color)" : "black",
+ ":active": {
+ backgroundColor: state.isSelected ? "" : "var(--brand-color)",
+ color: "var(--brand-text-color)",
+ },
+ }),
+ }}
+ components={{
+ ...components,
+ IndicatorSeparator: () => null,
+ Input: InputComponent,
+ }}
+ className={classNames("border-0 text-sm", className)}
+ {...props}
+ />
+ );
+}
+
+export function SelectWithValidation<
+ Option extends { label: string; value: string },
+ isMulti extends boolean = false,
+ Group extends GroupBase = GroupBase
+>({
+ required = false,
+ onChange,
+ value,
+ ...remainingProps
+}: SelectProps & { required?: boolean }) {
+ const [hiddenInputValue, _setHiddenInputValue] = useState(() => {
+ if (value instanceof Array || !value) {
+ return;
+ }
+ return value.value || "";
+ });
+
+ const setHiddenInputValue = useCallback((value: MultiValue | SingleValue ) => {
+ let hiddenInputValue = "";
+ if (value instanceof Array) {
+ hiddenInputValue = value.map((val) => val.value).join(",");
+ } else {
+ hiddenInputValue = value?.value || "";
+ }
+ _setHiddenInputValue(hiddenInputValue);
+ }, []);
+
+ useEffect(() => {
+ if (!value) {
+ return;
+ }
+ setHiddenInputValue(value);
+ }, [value, setHiddenInputValue]);
+
+ return (
+
+ {
+ setHiddenInputValue(value);
+ if (onChange) {
+ onChange(value, ...remainingArgs);
+ }
+ }}
+ />
+ {required && (
+ {}}
+ // TODO:Not able to get focus to work
+ // onFocus={() => selectRef.current?.focus()}
+ required={required}
+ />
+ )}
+
+ );
+}
+export default Select;
+import type { InputHTMLAttributes } from "react";
+import React, { forwardRef } from "react";
+
+import classNames from "@calcom/lib/classNames";
+import { InfoBadge } from "@calcom/ui";
+
+type Props = InputHTMLAttributes & {
+ label?: React.ReactNode;
+ description: string;
+ descriptionAsLabel?: boolean;
+ informationIconText?: string;
+};
+
+const CheckboxField = forwardRef(
+ ({ label, description, informationIconText, ...rest }, ref) => {
+ const descriptionAsLabel = !label || rest.descriptionAsLabel;
+ return (
+
+ {label && (
+
+ {React.createElement(
+ descriptionAsLabel ? "div" : "label",
+ {
+ className: "flex text-sm font-medium text-default",
+ ...(!descriptionAsLabel
+ ? {
+ htmlFor: rest.id,
+ }
+ : {}),
+ },
+ label
+ )}
+
+ )}
+
+
+ {React.createElement(
+ descriptionAsLabel ? "label" : "div",
+ {
+ className: classNames(
+ "relative flex items-start",
+ descriptionAsLabel ? "text-default" : "text-emphasis"
+ ),
+ },
+ <>
+
+
+
+
{description}
+ >
+ )}
+ {informationIconText &&
}
+
+
+
+ );
+ }
+);
+
+CheckboxField.displayName = "CheckboxField";
+
+export default CheckboxField;
+export default function SettingInputContainer({
+ Input,
+ Icon,
+ label,
+ htmlFor,
+}: {
+ Input: React.ReactNode;
+ Icon: (props: React.SVGProps) => JSX.Element | null;
+ label: string;
+ htmlFor?: string;
+}) {
+ return (
+
+
+
+
+
+ {label}
+
+
+
{Input}
+
+
+ );
+}
+import dynamic from "next/dynamic";
+import { useSearchParams } from "next/navigation";
+import { useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+
+import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
+import { CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
+import type { TRPCClientErrorLike } from "@calcom/trpc/client";
+import { trpc } from "@calcom/trpc/react";
+import type { AppRouter } from "@calcom/trpc/server/routers/_app";
+
+import useRouterQuery from "@lib/hooks/useRouterQuery";
+
+interface UsernameAvailabilityFieldProps {
+ onSuccessMutation?: () => void;
+ onErrorMutation?: (error: TRPCClientErrorLike) => void;
+}
+
+export const getUsernameAvailabilityComponent = (isPremium: boolean) => {
+ if (isPremium)
+ return dynamic(() => import("./PremiumTextfield").then((m) => m.PremiumTextfield), { ssr: false });
+ return dynamic(() => import("./UsernameTextfield").then((m) => m.UsernameTextfield), { ssr: false });
+};
+
+export const UsernameAvailabilityField = ({
+ onSuccessMutation,
+ onErrorMutation,
+}: UsernameAvailabilityFieldProps) => {
+ const searchParams = useSearchParams();
+ const [user] = trpc.viewer.me.useSuspenseQuery();
+ const [currentUsernameState, setCurrentUsernameState] = useState(user.username || "");
+ const { username: usernameFromQuery, setQuery: setUsernameFromQuery } = useRouterQuery("username");
+ const { username: currentUsername, setQuery: setCurrentUsername } =
+ searchParams?.get("username") && user.username === null
+ ? { username: usernameFromQuery, setQuery: setUsernameFromQuery }
+ : { username: currentUsernameState || "", setQuery: setCurrentUsernameState };
+ const formMethods = useForm({
+ defaultValues: {
+ username: currentUsername,
+ },
+ });
+
+ const UsernameAvailability = getUsernameAvailabilityComponent(!IS_SELF_HOSTED && !user.organization?.id);
+ const orgBranding = useOrgBranding();
+
+ const usernamePrefix = orgBranding
+ ? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
+ : `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
+
+ return (
+ {
+ return (
+
+ );
+ }}
+ />
+ );
+};
+import classNames from "classnames";
+// eslint-disable-next-line no-restricted-imports
+import { debounce, noop } from "lodash";
+import { useSession } from "next-auth/react";
+import type { RefCallback } from "react";
+import { useEffect, useMemo, useState } from "react";
+
+import { fetchUsername } from "@calcom/lib/fetchUsername";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { TRPCClientErrorLike } from "@calcom/trpc/client";
+import { trpc } from "@calcom/trpc/react";
+import type { AppRouter } from "@calcom/trpc/server/routers/_app";
+import { Button, Dialog, DialogClose, DialogContent, TextField, DialogFooter } from "@calcom/ui";
+import { Check, Edit2 } from "@calcom/ui/components/icon";
+
+interface ICustomUsernameProps {
+ currentUsername: string | undefined;
+ setCurrentUsername?: (newUsername: string) => void;
+ inputUsernameValue: string | undefined;
+ usernameRef: RefCallback;
+ setInputUsernameValue: (value: string) => void;
+ onSuccessMutation?: () => void;
+ onErrorMutation?: (error: TRPCClientErrorLike) => void;
+}
+
+const UsernameTextfield = (props: ICustomUsernameProps & Partial>) => {
+ const { t } = useLocale();
+ const { update } = useSession();
+
+ const {
+ currentUsername,
+ setCurrentUsername = noop,
+ inputUsernameValue,
+ setInputUsernameValue,
+ usernameRef,
+ onSuccessMutation,
+ onErrorMutation,
+ ...rest
+ } = props;
+ const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
+ const [markAsError, setMarkAsError] = useState(false);
+ const [openDialogSaveUsername, setOpenDialogSaveUsername] = useState(false);
+
+ const debouncedApiCall = useMemo(
+ () =>
+ debounce(async (username) => {
+ // TODO: Support orgSlug
+ const { data } = await fetchUsername(username, null);
+ setMarkAsError(!data.available);
+ setUsernameIsAvailable(data.available);
+ }, 150),
+ []
+ );
+
+ useEffect(() => {
+ if (!inputUsernameValue) {
+ debouncedApiCall.cancel();
+ setUsernameIsAvailable(false);
+ setMarkAsError(false);
+ return;
+ }
+
+ if (currentUsername !== inputUsernameValue) {
+ debouncedApiCall(inputUsernameValue);
+ } else {
+ setUsernameIsAvailable(false);
+ }
+ }, [inputUsernameValue, debouncedApiCall, currentUsername]);
+
+ const updateUsernameMutation = trpc.viewer.updateProfile.useMutation({
+ onSuccess: async () => {
+ onSuccessMutation && (await onSuccessMutation());
+ setOpenDialogSaveUsername(false);
+ setCurrentUsername(inputUsernameValue);
+ await update({ username: inputUsernameValue });
+ },
+ onError: (error) => {
+ onErrorMutation && onErrorMutation(error);
+ },
+ });
+
+ const ActionButtons = () => {
+ return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
+
+ setOpenDialogSaveUsername(true)}
+ data-testid="update-username-btn">
+ {t("update")}
+
+ {
+ if (currentUsername) {
+ setInputUsernameValue(currentUsername);
+ }
+ }}>
+ {t("cancel")}
+
+
+ ) : (
+ <>>
+ );
+ };
+
+ const updateUsername = async () => {
+ updateUsernameMutation.mutate({
+ username: inputUsernameValue,
+ });
+ };
+
+ return (
+
+
+
+
{
+ event.preventDefault();
+ setInputUsernameValue(event.target.value);
+ }}
+ data-testid="username-input"
+ {...rest}
+ />
+ {currentUsername !== inputUsernameValue && (
+
+
+ {usernameIsAvailable ? : <>>}
+
+
+ )}
+
+
+
+ {markAsError &&
{t("username_already_taken")}
}
+
+ {usernameIsAvailable && currentUsername !== inputUsernameValue && (
+
+ )}
+
+
+
+
+
+
+
{t("current_username")}
+
+ {currentUsername}
+
+
+
+
+ {t("new_username")}
+
+
{inputUsernameValue}
+
+
+
+
+
+
+
+ {t("save")}
+
+
+ setOpenDialogSaveUsername(false)}>
+ {t("cancel")}
+
+
+
+
+
+ );
+};
+
+export { UsernameTextfield };
+import classNames from "classnames";
+// eslint-disable-next-line no-restricted-imports
+import { debounce, noop } from "lodash";
+import { useSession } from "next-auth/react";
+import { usePathname, useRouter, useSearchParams } from "next/navigation";
+import type { RefCallback } from "react";
+import { useEffect, useMemo, useState } from "react";
+
+import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
+import { WEBAPP_URL } from "@calcom/lib/constants";
+import { fetchUsername } from "@calcom/lib/fetchUsername";
+import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { TRPCClientErrorLike } from "@calcom/trpc/client";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import { trpc } from "@calcom/trpc/react";
+import type { AppRouter } from "@calcom/trpc/server/routers/_app";
+import { Button, Dialog, DialogClose, DialogContent, DialogFooter, Input, Label } from "@calcom/ui";
+import { Check, Edit2, ExternalLink, Star as StarSolid } from "@calcom/ui/components/icon";
+
+export enum UsernameChangeStatusEnum {
+ UPGRADE = "UPGRADE",
+}
+
+interface ICustomUsernameProps {
+ currentUsername: string | undefined;
+ setCurrentUsername?: (newUsername: string) => void;
+ inputUsernameValue: string | undefined;
+ usernameRef: RefCallback;
+ setInputUsernameValue: (value: string) => void;
+ onSuccessMutation?: () => void;
+ onErrorMutation?: (error: TRPCClientErrorLike) => void;
+ readonly?: boolean;
+}
+
+const obtainNewUsernameChangeCondition = ({
+ userIsPremium,
+ isNewUsernamePremium,
+}: {
+ userIsPremium: boolean;
+ isNewUsernamePremium: boolean;
+ stripeCustomer: RouterOutputs["viewer"]["stripeCustomer"] | undefined;
+}) => {
+ if (!userIsPremium && isNewUsernamePremium) {
+ return UsernameChangeStatusEnum.UPGRADE;
+ }
+};
+
+const PremiumTextfield = (props: ICustomUsernameProps) => {
+ const searchParams = useSearchParams();
+ const pathname = usePathname();
+ const router = useRouter();
+ const { t } = useLocale();
+ const { update } = useSession();
+ const {
+ currentUsername,
+ setCurrentUsername = noop,
+ inputUsernameValue,
+ setInputUsernameValue,
+ usernameRef,
+ onSuccessMutation,
+ onErrorMutation,
+ readonly: disabled,
+ } = props;
+ const [user] = trpc.viewer.me.useSuspenseQuery();
+ const [usernameIsAvailable, setUsernameIsAvailable] = useState(false);
+ const [markAsError, setMarkAsError] = useState(false);
+ const recentAttemptPaymentStatus = searchParams?.get("recentAttemptPaymentStatus");
+ const [openDialogSaveUsername, setOpenDialogSaveUsername] = useState(false);
+ const { data: stripeCustomer } = trpc.viewer.stripeCustomer.useQuery();
+ const isCurrentUsernamePremium =
+ user && user.metadata && hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false;
+ const [isInputUsernamePremium, setIsInputUsernamePremium] = useState(false);
+ const debouncedApiCall = useMemo(
+ () =>
+ debounce(async (username: string) => {
+ // TODO: Support orgSlug
+ const { data } = await fetchUsername(username, null);
+ setMarkAsError(!data.available && !!currentUsername && username !== currentUsername);
+ setIsInputUsernamePremium(data.premium);
+ setUsernameIsAvailable(data.available);
+ }, 150),
+ [currentUsername]
+ );
+
+ useEffect(() => {
+ // Use the current username or if it's not set, use the one available from stripe
+ setInputUsernameValue(currentUsername || stripeCustomer?.username || "");
+ }, [setInputUsernameValue, currentUsername, stripeCustomer?.username]);
+
+ useEffect(() => {
+ if (!inputUsernameValue) {
+ debouncedApiCall.cancel();
+ return;
+ }
+ debouncedApiCall(inputUsernameValue);
+ }, [debouncedApiCall, inputUsernameValue]);
+
+ const updateUsername = trpc.viewer.updateProfile.useMutation({
+ onSuccess: async () => {
+ onSuccessMutation && (await onSuccessMutation());
+ await update({ username: inputUsernameValue });
+ setOpenDialogSaveUsername(false);
+ },
+ onError: (error) => {
+ onErrorMutation && onErrorMutation(error);
+ },
+ });
+
+ // when current username isn't set - Go to stripe to check what username he wanted to buy and was it a premium and was it paid for
+ const paymentRequired = !currentUsername && stripeCustomer?.isPremium;
+
+ const usernameChangeCondition = obtainNewUsernameChangeCondition({
+ userIsPremium: isCurrentUsernamePremium,
+ isNewUsernamePremium: isInputUsernamePremium,
+ stripeCustomer,
+ });
+
+ const usernameFromStripe = stripeCustomer?.username;
+
+ const paymentLink = `/api/integrations/stripepayment/subscription?intentUsername=${
+ inputUsernameValue || usernameFromStripe
+ }&action=${usernameChangeCondition}&callbackUrl=${WEBAPP_URL}${pathname}`;
+
+ const ActionButtons = () => {
+ if (paymentRequired) {
+ return (
+
+
+ {t("Reserve")}
+
+
+ );
+ }
+ if ((usernameIsAvailable || isInputUsernamePremium) && currentUsername !== inputUsernameValue) {
+ return (
+
+ setOpenDialogSaveUsername(true)}
+ data-testid="update-username-btn">
+ {t("update")}
+
+ {
+ if (currentUsername) {
+ setInputUsernameValue(currentUsername);
+ }
+ }}>
+ {t("cancel")}
+
+
+ );
+ }
+ return <>>;
+ };
+
+ const saveUsername = () => {
+ if (usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE) {
+ updateUsername.mutate({
+ username: inputUsernameValue,
+ });
+ setCurrentUsername(inputUsernameValue);
+ }
+ };
+
+ let paymentMsg = !currentUsername ? (
+
+ You need to reserve your premium username for {getPremiumPlanPriceValue()}
+
+ ) : null;
+
+ if (recentAttemptPaymentStatus && recentAttemptPaymentStatus !== "paid") {
+ paymentMsg = (
+
+ Your payment could not be completed. Your username is still not reserved
+
+ );
+ }
+
+ return (
+
+
+ {t("username")}
+
+
+
+ {process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
+
+
+
+
{
+ event.preventDefault();
+ // Reset payment status
+ const _searchParams = new URLSearchParams(searchParams ?? undefined);
+ _searchParams.delete("paymentStatus");
+ if (searchParams?.toString() !== _searchParams.toString()) {
+ router.replace(`${pathname}?${_searchParams.toString()}`);
+ }
+ setInputUsernameValue(event.target.value);
+ }}
+ data-testid="username-input"
+ />
+
+
+ {isInputUsernamePremium ? : <>>}
+ {!isInputUsernamePremium && usernameIsAvailable ? (
+
+ ) : (
+ <>>
+ )}
+
+
+
+
+ {(usernameIsAvailable || isInputUsernamePremium) && currentUsername !== inputUsernameValue && (
+
+ )}
+
+ {paymentMsg}
+ {markAsError &&
Username is already taken
}
+
+
+
+ {usernameChangeCondition && usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
+ {t("change_username_standard_to_premium")}
+ )}
+ >
+ }>
+
+
+
+
+
{t("current_username")}
+
+ {currentUsername}
+
+
+
+
+ {t("new_username")}
+
+
{inputUsernameValue}
+
+
+
+
+
+
+ {/* redirect to checkout */}
+ {usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
+
+ <>
+ {t("go_to_stripe_billing")}
+ >
+
+ )}
+ {/* Normal save */}
+ {usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE && (
+ {
+ saveUsername();
+ }}>
+ {t("save")}
+
+ )}
+ setOpenDialogSaveUsername(false)}>
+ {t("cancel")}
+
+
+
+
+
+ );
+};
+
+export { PremiumTextfield };
+import classNames from "classnames";
+import { useState } from "react";
+import type { ControllerRenderProps } from "react-hook-form";
+
+import { Edit2 } from "@calcom/ui/components/icon";
+
+const EditableHeading = function EditableHeading({
+ value,
+ onChange,
+ isReady,
+ ...passThroughProps
+}: {
+ isReady?: boolean;
+} & Omit &
+ ControllerRenderProps) {
+ const [isEditing, setIsEditing] = useState(false);
+ const enableEditing = () => setIsEditing(true);
+ return (
+
+
+
+ {value}
+ {!isEditing && isReady && (
+
+ )}
+ {
+ setIsEditing(true);
+ passThroughProps.onFocus && passThroughProps.onFocus(e);
+ }}
+ onBlur={(e) => {
+ setIsEditing(false);
+ passThroughProps.onBlur && passThroughProps.onBlur(e);
+ }}
+ onChange={(e) => onChange && onChange(e.target.value)}
+ />
+
+
+
+ );
+};
+
+export default EditableHeading;
+import React from "react";
+
+import type { SVGComponent } from "@lib/types/SVGComponent";
+
+interface LinkIconButtonProps extends React.ButtonHTMLAttributes {
+ Icon: SVGComponent;
+}
+
+export default function LinkIconButton(props: LinkIconButtonProps) {
+ return (
+
+
+
+ {props.children}
+
+
+ );
+}
+import classNames from "classnames";
+
+import { HeadSeo, Logo } from "@calcom/ui";
+
+import Loader from "@components/Loader";
+
+interface Props {
+ title: string;
+ description: string;
+ footerText?: React.ReactNode | string;
+ showLogo?: boolean;
+ heading?: string;
+ loading?: boolean;
+}
+
+export default function AuthContainer(props: React.PropsWithChildren) {
+ return (
+
+
+ {props.showLogo &&
}
+
+
+ {props.heading &&
{props.heading} }
+
+ {props.loading && (
+
+
+
+ )}
+
+
+ {props.children}
+
+
{props.footerText}
+
+
+ );
+}
+import classNames from "classnames";
+import type { PropsWithChildren } from "react";
+import React from "react";
+
+import { Dialog, DialogContent } from "@calcom/ui";
+
+export default function ModalContainer(
+ props: PropsWithChildren<{
+ wide?: boolean;
+ scroll?: boolean;
+ noPadding?: boolean;
+ isOpen: boolean;
+ onExit: () => void;
+ }>
+) {
+ return (
+
+
+
+
+ {props.children}
+
+
+
+
+ );
+}
+import type { BaseSyntheticEvent } from "react";
+import React, { useEffect, useState } from "react";
+import { useForm } from "react-hook-form";
+
+import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
+import { useCallbackRef } from "@calcom/lib/hooks/useCallbackRef";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, Dialog, DialogContent, DialogFooter, Form, PasswordField, showToast } from "@calcom/ui";
+
+import TwoFactor from "@components/auth/TwoFactor";
+
+import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
+
+interface EnableTwoFactorModalProps {
+ open: boolean;
+ onOpenChange: () => void;
+
+ /**
+ * Called when the user closes the modal without disabling two-factor auth
+ */
+ onCancel: () => void;
+
+ /**
+ * Called when the user enables two-factor auth
+ */
+ onEnable: () => void;
+}
+
+enum SetupStep {
+ ConfirmPassword,
+ DisplayBackupCodes,
+ DisplayQrCode,
+ EnterTotpCode,
+}
+
+const WithStep = ({
+ step,
+ current,
+ children,
+}: {
+ step: SetupStep;
+ current: SetupStep;
+ children: JSX.Element;
+}) => {
+ return step === current ? children : null;
+};
+
+interface EnableTwoFactorValues {
+ totpCode: string;
+}
+
+const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: EnableTwoFactorModalProps) => {
+ const { t } = useLocale();
+ const form = useForm();
+
+ const setupDescriptions = {
+ [SetupStep.ConfirmPassword]: t("2fa_confirm_current_password"),
+ [SetupStep.DisplayBackupCodes]: t("backup_code_instructions"),
+ [SetupStep.DisplayQrCode]: t("2fa_scan_image_or_use_code"),
+ [SetupStep.EnterTotpCode]: t("2fa_enter_six_digit_code"),
+ };
+ const [step, setStep] = useState(SetupStep.ConfirmPassword);
+ const [password, setPassword] = useState("");
+ const [backupCodes, setBackupCodes] = useState([]);
+ const [backupCodesUrl, setBackupCodesUrl] = useState("");
+ const [dataUri, setDataUri] = useState("");
+ const [secret, setSecret] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [errorMessage, setErrorMessage] = useState(null);
+
+ const resetState = () => {
+ setPassword("");
+ setErrorMessage(null);
+ setStep(SetupStep.ConfirmPassword);
+ };
+
+ async function handleSetup(e: React.FormEvent) {
+ e.preventDefault();
+
+ if (isSubmitting) {
+ return;
+ }
+
+ setIsSubmitting(true);
+ setErrorMessage(null);
+
+ try {
+ const response = await TwoFactorAuthAPI.setup(password);
+ const body = await response.json();
+
+ if (response.status === 200) {
+ setBackupCodes(body.backupCodes);
+
+ // create backup codes download url
+ const textBlob = new Blob([body.backupCodes.map(formatBackupCode).join("\n")], {
+ type: "text/plain",
+ });
+ if (backupCodesUrl) URL.revokeObjectURL(backupCodesUrl);
+ setBackupCodesUrl(URL.createObjectURL(textBlob));
+
+ setDataUri(body.dataUri);
+ setSecret(body.secret);
+ setStep(SetupStep.DisplayQrCode);
+ return;
+ }
+
+ if (body.error === ErrorCode.IncorrectPassword) {
+ setErrorMessage(t("incorrect_password"));
+ } else {
+ setErrorMessage(t("something_went_wrong"));
+ }
+ } catch (e) {
+ setErrorMessage(t("something_went_wrong"));
+ console.error(t("error_enabling_2fa"), e);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ async function handleEnable({ totpCode }: EnableTwoFactorValues, e: BaseSyntheticEvent | undefined) {
+ e?.preventDefault();
+
+ if (isSubmitting) {
+ return;
+ }
+
+ setIsSubmitting(true);
+ setErrorMessage(null);
+
+ try {
+ const response = await TwoFactorAuthAPI.enable(totpCode);
+ const body = await response.json();
+
+ if (response.status === 200) {
+ setStep(SetupStep.DisplayBackupCodes);
+ return;
+ }
+
+ if (body.error === ErrorCode.IncorrectTwoFactorCode) {
+ setErrorMessage(`${t("code_is_incorrect")} ${t("please_try_again")}`);
+ } else {
+ setErrorMessage(t("something_went_wrong"));
+ }
+ } catch (e) {
+ setErrorMessage(t("something_went_wrong"));
+ console.error(t("error_enabling_2fa"), e);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ const handleEnableRef = useCallbackRef(handleEnable);
+
+ const totpCode = form.watch("totpCode");
+
+ // auto submit 2FA if all inputs have a value
+ useEffect(() => {
+ if (totpCode?.trim().length === 6) {
+ form.handleSubmit(handleEnableRef.current)();
+ }
+ }, [form, handleEnableRef, totpCode]);
+
+ const formatBackupCode = (code: string) => `${code.slice(0, 5)}-${code.slice(5, 10)}`;
+
+ return (
+
+
+
+
+
+
+ <>
+
+ {
+ // eslint-disable-next-line @next/next/no-img-element
+
+ }
+
+
+ {secret}
+
+ >
+
+
+ <>
+
+ {backupCodes.map((code) => (
+
{formatBackupCode(code)}
+ ))}
+
+ >
+
+
+
+
+ );
+};
+
+export default EnableTwoFactorModal;
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+
+import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, Dialog, DialogContent, DialogFooter, Form, PasswordField } from "@calcom/ui";
+
+import BackupCode from "@components/auth/BackupCode";
+import TwoFactor from "@components/auth/TwoFactor";
+
+import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
+
+interface DisableTwoFactorAuthModalProps {
+ open: boolean;
+ onOpenChange: () => void;
+ disablePassword?: boolean;
+ /** Called when the user closes the modal without disabling two-factor auth */
+ onCancel: () => void;
+ /** Called when the user disables two-factor auth */
+ onDisable: () => void;
+}
+
+interface DisableTwoFactorValues {
+ backupCode: string;
+ totpCode: string;
+ password: string;
+}
+
+const DisableTwoFactorAuthModal = ({
+ onDisable,
+ onCancel,
+ disablePassword,
+ open,
+ onOpenChange,
+}: DisableTwoFactorAuthModalProps) => {
+ const [isDisabling, setIsDisabling] = useState(false);
+ const [errorMessage, setErrorMessage] = useState(null);
+ const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false);
+ const { t } = useLocale();
+
+ const form = useForm();
+
+ const resetForm = (clearPassword = true) => {
+ if (clearPassword) form.setValue("password", "");
+ form.setValue("backupCode", "");
+ form.setValue("totpCode", "");
+ setErrorMessage(null);
+ };
+
+ async function handleDisable({ password, totpCode, backupCode }: DisableTwoFactorValues) {
+ if (isDisabling) {
+ return;
+ }
+ setIsDisabling(true);
+ setErrorMessage(null);
+
+ try {
+ const response = await TwoFactorAuthAPI.disable(password, totpCode, backupCode);
+ if (response.status === 200) {
+ setTwoFactorLostAccess(false);
+ resetForm();
+ onDisable();
+ return;
+ }
+
+ const body = await response.json();
+ if (body.error === ErrorCode.IncorrectPassword) {
+ setErrorMessage(t("incorrect_password"));
+ } else if (body.error === ErrorCode.SecondFactorRequired) {
+ setErrorMessage(t("2fa_required"));
+ } else if (body.error === ErrorCode.IncorrectTwoFactorCode) {
+ setErrorMessage(t("incorrect_2fa"));
+ } else if (body.error === ErrorCode.IncorrectBackupCode) {
+ setErrorMessage(t("incorrect_backup_code"));
+ } else if (body.error === ErrorCode.MissingBackupCodes) {
+ setErrorMessage(t("missing_backup_codes"));
+ } else {
+ setErrorMessage(t("something_went_wrong"));
+ }
+ } catch (e) {
+ setErrorMessage(t("something_went_wrong"));
+ console.error(t("error_disabling_2fa"), e);
+ } finally {
+ setIsDisabling(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default DisableTwoFactorAuthModal;
+"use client";
+
+import { ShellMain } from "@calcom/features/shell/Shell";
+import { UpgradeTip } from "@calcom/features/tips";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, ButtonGroup } from "@calcom/ui";
+import { BarChart, CreditCard, Globe, Lock, Paintbrush, Users } from "@calcom/ui/components/icon";
+
+export default function EnterprisePage() {
+ const { t } = useLocale();
+
+ const features = [
+ {
+ icon: ,
+ title: t("branded_subdomain"),
+ description: t("branded_subdomain_description"),
+ },
+ {
+ icon: ,
+ title: t("org_insights"),
+ description: t("org_insights_description"),
+ },
+ {
+ icon: ,
+ title: t("extensive_whitelabeling"),
+ description: t("extensive_whitelabeling_description"),
+ },
+ {
+ icon: ,
+ title: t("unlimited_teams"),
+ description: t("unlimited_teams_description"),
+ },
+ {
+ icon: ,
+ title: t("unified_billing"),
+ description: t("unified_billing_description"),
+ },
+ {
+ icon: ,
+ title: t("advanced_managed_events"),
+ description: t("advanced_managed_events_description"),
+ },
+ ];
+ return (
+
+
+
+
+
+ {t("contact_sales")}
+
+
+ {t("learn_more")}
+
+
+
+ }>
+ <>Create Org>
+
+
+
+ );
+}
+/**
+ * @deprecated Use custom Skeletons instead
+ **/
+export { Loader as default } from "@calcom/ui";
+import type { BaseSyntheticEvent } from "react";
+import React, { useEffect, useState } from "react";
+import { useForm } from "react-hook-form";
+
+import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
+import { useCallbackRef } from "@calcom/lib/hooks/useCallbackRef";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, Dialog, DialogContent, Form } from "@calcom/ui";
+
+import TwoFactor from "@components/auth/TwoFactor";
+
+import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
+import TwoFactorModalHeader from "./TwoFactorModalHeader";
+
+interface EnableTwoFactorModalProps {
+ /**
+ * Called when the user closes the modal without disabling two-factor auth
+ */
+ onCancel: () => void;
+
+ /**
+ * Called when the user enables two-factor auth
+ */
+ onEnable: () => void;
+}
+
+enum SetupStep {
+ ConfirmPassword,
+ DisplayQrCode,
+ EnterTotpCode,
+}
+
+const WithStep = ({
+ step,
+ current,
+ children,
+}: {
+ step: SetupStep;
+ current: SetupStep;
+ children: JSX.Element;
+}) => {
+ return step === current ? children : null;
+};
+
+interface EnableTwoFactorValues {
+ totpCode: string;
+}
+
+const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps) => {
+ const { t } = useLocale();
+ const form = useForm();
+
+ const setupDescriptions = {
+ [SetupStep.ConfirmPassword]: t("2fa_confirm_current_password"),
+ [SetupStep.DisplayQrCode]: t("2fa_scan_image_or_use_code"),
+ [SetupStep.EnterTotpCode]: t("2fa_enter_six_digit_code"),
+ };
+ const [step, setStep] = useState(SetupStep.ConfirmPassword);
+ const [password, setPassword] = useState("");
+ const [dataUri, setDataUri] = useState("");
+ const [secret, setSecret] = useState("");
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [errorMessage, setErrorMessage] = useState(null);
+
+ async function handleSetup(e: React.FormEvent) {
+ e.preventDefault();
+
+ if (isSubmitting) {
+ return;
+ }
+
+ setIsSubmitting(true);
+ setErrorMessage(null);
+
+ try {
+ const response = await TwoFactorAuthAPI.setup(password);
+ const body = await response.json();
+
+ if (response.status === 200) {
+ setDataUri(body.dataUri);
+ setSecret(body.secret);
+ setStep(SetupStep.DisplayQrCode);
+ return;
+ }
+
+ if (body.error === ErrorCode.IncorrectPassword) {
+ setErrorMessage(t("incorrect_password"));
+ } else {
+ setErrorMessage(t("something_went_wrong"));
+ }
+ } catch (e) {
+ setErrorMessage(t("something_went_wrong"));
+ console.error(t("error_enabling_2fa"), e);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ async function handleEnable({ totpCode }: EnableTwoFactorValues, e: BaseSyntheticEvent | undefined) {
+ e?.preventDefault();
+
+ if (isSubmitting) {
+ return;
+ }
+
+ setIsSubmitting(true);
+ setErrorMessage(null);
+
+ try {
+ const response = await TwoFactorAuthAPI.enable(totpCode);
+ const body = await response.json();
+
+ if (response.status === 200) {
+ onEnable();
+ return;
+ }
+
+ if (body.error === ErrorCode.IncorrectTwoFactorCode) {
+ setErrorMessage(`${t("code_is_incorrect")} ${t("please_try_again")}`);
+ } else {
+ setErrorMessage(t("something_went_wrong"));
+ }
+ } catch (e) {
+ setErrorMessage(t("something_went_wrong"));
+ console.error(t("error_enabling_2fa"), e);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ const handleEnableRef = useCallbackRef(handleEnable);
+
+ const totpCode = form.watch("totpCode");
+
+ // auto submit 2FA if all inputs have a value
+ useEffect(() => {
+ if (totpCode?.trim().length === 6) {
+ form.handleSubmit(handleEnableRef.current)();
+ }
+ }, [form, handleEnableRef, totpCode]);
+
+ return (
+
+
+
+
+
+
+
+
+ <>
+
+ {
+ // eslint-disable-next-line @next/next/no-img-element
+
+ }
+
+ {secret}
+ >
+
+
+
+
+ );
+};
+
+export default EnableTwoFactorModal;
+import { useState } from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Badge, Button } from "@calcom/ui";
+
+import DisableTwoFactorModal from "./DisableTwoFactorModal";
+import EnableTwoFactorModal from "./EnableTwoFactorModal";
+
+const TwoFactorAuthSection = ({ twoFactorEnabled }: { twoFactorEnabled: boolean }) => {
+ const [enabled, setEnabled] = useState(twoFactorEnabled);
+ const [enableModalOpen, setEnableModalOpen] = useState(false);
+ const [disableModalOpen, setDisableModalOpen] = useState(false);
+ const { t } = useLocale();
+
+ return (
+ <>
+
+
+
+
{t("2fa")}
+
+ {enabled ? t("enabled") : t("disabled")}
+
+
+
{t("add_an_extra_layer_of_security")}
+
+
+ (enabled ? setDisableModalOpen(true) : setEnableModalOpen(true))}>
+ {enabled ? t("disable") : t("enable")}
+
+
+
+ {enableModalOpen && (
+ {
+ setEnabled(true);
+ setEnableModalOpen(false);
+ }}
+ onCancel={() => setEnableModalOpen(false)}
+ />
+ )}
+
+ {disableModalOpen && (
+ {
+ setEnabled(false);
+ setDisableModalOpen(false);
+ }}
+ onCancel={() => setDisableModalOpen(false)}
+ />
+ )}
+ >
+ );
+};
+
+export default TwoFactorAuthSection;
+import { Shield } from "@calcom/ui/components/icon";
+
+const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
+ return (
+
+
+
+
+
+
+ {title}
+
+
{description}
+
+
+ );
+};
+
+export default TwoFactorModalHeader;
+import type { SyntheticEvent } from "react";
+import { useState } from "react";
+
+import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, showToast } from "@calcom/ui";
+
+const ChangePasswordSection = () => {
+ const [oldPassword, setOldPassword] = useState("");
+ const [newPassword, setNewPassword] = useState("");
+ const [errorMessage, setErrorMessage] = useState(null);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const { t, isLocaleReady } = useLocale();
+ // hold display until the locale is loaded
+ if (!isLocaleReady) {
+ return null;
+ }
+
+ const errorMessages: { [key: string]: string } = {
+ [ErrorCode.IncorrectPassword]: t("current_incorrect_password"),
+ [ErrorCode.NewPasswordMatchesOld]: t("new_password_matches_old_password"),
+ };
+
+ async function changePasswordHandler(e: SyntheticEvent) {
+ e.preventDefault();
+
+ if (isSubmitting) {
+ return;
+ }
+
+ setIsSubmitting(true);
+ setErrorMessage(null);
+
+ try {
+ const response = await fetch("/api/auth/changepw", {
+ method: "PATCH",
+ body: JSON.stringify({ oldPassword, newPassword }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (response.status === 200) {
+ setOldPassword("");
+ setNewPassword("");
+ showToast(t("password_has_been_changed"), "success");
+ return;
+ }
+
+ const body = await response.json();
+ setErrorMessage(errorMessages[body.error] || `${t("something_went_wrong")}${t("please_try_again")}`);
+ } catch (err) {
+ console.error(t("error_changing_password"), err);
+ setErrorMessage(`${t("something_went_wrong")}${t("please_try_again")}`);
+ } finally {
+ setIsSubmitting(false);
+ }
+ }
+
+ return (
+ <>
+
+ >
+ );
+};
+
+export default ChangePasswordSection;
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Badge, Button, showToast } from "@calcom/ui";
+
+const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonation: boolean }) => {
+ const utils = trpc.useContext();
+
+ const { t } = useLocale();
+
+ const mutation = trpc.viewer.updateProfile.useMutation({
+ onSuccess: async () => {
+ showToast(t("your_user_profile_updated_successfully"), "success");
+ await utils.viewer.me.invalidate();
+ },
+ });
+
+ return (
+ <>
+
+
+
+
+ {t("user_impersonation_heading")}
+
+
+ {!disableImpersonation ? t("enabled") : t("disabled")}
+
+
+
{t("user_impersonation_description")}
+
+
+
+ !disableImpersonation
+ ? mutation.mutate({ disableImpersonation: true })
+ : mutation.mutate({ disableImpersonation: false })
+ }>
+ {!disableImpersonation ? t("disable") : t("enable")}
+
+
+
+ >
+ );
+};
+
+export default DisableUserImpersonation;
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+
+import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, Dialog, DialogContent, Form, PasswordField } from "@calcom/ui";
+
+import TwoFactor from "@components/auth/TwoFactor";
+
+import TwoFactorAuthAPI from "./TwoFactorAuthAPI";
+import TwoFactorModalHeader from "./TwoFactorModalHeader";
+
+interface DisableTwoFactorAuthModalProps {
+ /** Called when the user closes the modal without disabling two-factor auth */
+ onCancel: () => void;
+ /** Called when the user disables two-factor auth */
+ onDisable: () => void;
+}
+
+interface DisableTwoFactorValues {
+ totpCode: string;
+ password: string;
+}
+
+const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuthModalProps) => {
+ const [isDisabling, setIsDisabling] = useState(false);
+ const [errorMessage, setErrorMessage] = useState(null);
+ const { t } = useLocale();
+ const form = useForm();
+ async function handleDisable({ totpCode, password }: DisableTwoFactorValues) {
+ if (isDisabling) {
+ return;
+ }
+ setIsDisabling(true);
+ setErrorMessage(null);
+
+ try {
+ const response = await TwoFactorAuthAPI.disable(password, totpCode);
+ if (response.status === 200) {
+ onDisable();
+ return;
+ }
+
+ const body = await response.json();
+ if (body.error === ErrorCode.IncorrectPassword) {
+ setErrorMessage(t("incorrect_password"));
+ }
+ if (body.error === ErrorCode.SecondFactorRequired) {
+ setErrorMessage(t("2fa_required"));
+ }
+ if (body.error === ErrorCode.IncorrectTwoFactorCode) {
+ setErrorMessage(t("incorrect_2fa"));
+ } else {
+ setErrorMessage(t("something_went_wrong"));
+ }
+ } catch (e) {
+ setErrorMessage(t("something_went_wrong"));
+ console.error(t("error_disabling_2fa"), e);
+ } finally {
+ setIsDisabling(false);
+ }
+ }
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default DisableTwoFactorAuthModal;
+import React, { useEffect, useState } from "react";
+import useDigitInput from "react-digit-input";
+import { useFormContext } from "react-hook-form";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Label, Input } from "@calcom/ui";
+
+export default function TwoFactor({ center = true, autoFocus = true }) {
+ const [value, onChange] = useState("");
+ const { t } = useLocale();
+ const methods = useFormContext();
+
+ const digits = useDigitInput({
+ acceptedCharacters: /^[0-9]$/,
+ length: 6,
+ value,
+ onChange,
+ });
+
+ useEffect(() => {
+ if (value) methods.setValue("totpCode", value);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [value]);
+
+ const className = "h-12 w-12 !text-xl text-center";
+
+ return (
+
+
{t("2fa_code")}
+
+
{t("2fa_enabled_instructions")}
+
+
+
+
+ {digits.map((digit, index) => (
+
+ ))}
+
+
+ );
+}
+import React from "react";
+import { useFormContext } from "react-hook-form";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Label, TextField } from "@calcom/ui";
+
+export default function TwoFactor({ center = true }) {
+ const { t } = useLocale();
+ const methods = useFormContext();
+
+ return (
+
+
{t("backup_code")}
+
+
{t("backup_code_instructions")}
+
+
+
+ );
+}
+"use client";
+
+import { useSession } from "next-auth/react";
+import { usePathname, useRouter } from "next/navigation";
+import type { ComponentProps } from "react";
+import React, { useEffect } from "react";
+
+import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
+import type Shell from "@calcom/features/shell/Shell";
+import { UserPermissionRole } from "@calcom/prisma/enums";
+import { ErrorBoundary } from "@calcom/ui";
+
+export default function AdminLayout({
+ children,
+ ...rest
+}: { children: React.ReactNode } & ComponentProps) {
+ const pathname = usePathname();
+ const session = useSession();
+ const router = useRouter();
+
+ // Force redirect on component level
+ useEffect(() => {
+ if (session.data && session.data.user.role !== UserPermissionRole.ADMIN) {
+ router.replace("/settings/my-account/profile");
+ }
+ }, [session, router]);
+
+ const isAppsPage = pathname?.startsWith("/settings/admin/apps");
+ return (
+
+
+
*]:flex-1"}>
+ {children}
+
+
+
+ );
+}
+
+export const getLayout = (page: React.ReactElement) => {page} ;
+import { useSession } from "next-auth/react";
+import { usePathname, useRouter } from "next/navigation";
+import type { ComponentProps } from "react";
+import React, { useEffect } from "react";
+
+import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
+import type Shell from "@calcom/features/shell/Shell";
+import { UserPermissionRole } from "@calcom/prisma/enums";
+import { ErrorBoundary } from "@calcom/ui";
+
+export default function AdminLayout({
+ children,
+ ...rest
+}: { children: React.ReactNode } & ComponentProps) {
+ const pathname = usePathname();
+ const session = useSession();
+ const router = useRouter();
+
+ // Force redirect on component level
+ useEffect(() => {
+ if (session.data && session.data.user.role !== UserPermissionRole.ADMIN) {
+ router.replace("/settings/my-account/profile");
+ }
+ }, [session, router]);
+
+ const isAppsPage = pathname?.startsWith("/settings/admin/apps");
+ return (
+
+
+
*]:flex-1"}>
+ {children}
+
+
+
+ );
+}
+
+export const getLayout = (page: React.ReactElement) => {page} ;
+import React from "react";
+
+import { SkeletonText } from "@calcom/ui";
+
+function SkeletonLoader() {
+ return (
+
+ );
+}
+
+export default SkeletonLoader;
+
+function SkeletonItem() {
+ return (
+
+
+
+
+ );
+}
+import Link from "next/link";
+import { useState } from "react";
+
+import type { EventLocationType, getEventLocationValue } from "@calcom/app-store/locations";
+import {
+ getEventLocationType,
+ getSuccessPageLocationMessage,
+ guessEventLocationType,
+} from "@calcom/app-store/locations";
+import dayjs from "@calcom/dayjs";
+// TODO: Use browser locale, implement Intl in Dayjs maybe?
+import "@calcom/dayjs/locales";
+import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
+import classNames from "@calcom/lib/classNames";
+import { formatTime } from "@calcom/lib/date-fns";
+import getPaymentAppData from "@calcom/lib/getPaymentAppData";
+import { useBookerUrl } from "@calcom/lib/hooks/useBookerUrl";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
+import { BookingStatus } from "@calcom/prisma/enums";
+import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
+import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
+import { trpc } from "@calcom/trpc/react";
+import type { ActionType } from "@calcom/ui";
+import {
+ Badge,
+ Button,
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogFooter,
+ MeetingTimeInTimezones,
+ showToast,
+ TableActions,
+ TextAreaField,
+ Tooltip,
+} from "@calcom/ui";
+import { Ban, Check, Clock, CreditCard, MapPin, RefreshCcw, Send, X } from "@calcom/ui/components/icon";
+
+import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
+import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
+import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
+
+type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
+
+type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
+
+type BookingItemProps = BookingItem & {
+ listingStatus: BookingListingStatus;
+ recurringInfo: RouterOutputs["viewer"]["bookings"]["get"]["recurringInfo"][number] | undefined;
+ loggedInUser: {
+ userId: number | undefined;
+ userTimeZone: string | undefined;
+ userTimeFormat: number | null | undefined;
+ userEmail: string | undefined;
+ };
+};
+
+function BookingListItem(booking: BookingItemProps) {
+ const bookerUrl = useBookerUrl();
+ const { userId, userTimeZone, userTimeFormat, userEmail } = booking.loggedInUser;
+
+ const {
+ t,
+ i18n: { language },
+ } = useLocale();
+ const utils = trpc.useContext();
+ const [rejectionReason, setRejectionReason] = useState("");
+ const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
+ const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
+ const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState(false);
+ const cardCharged = booking?.payment[0]?.success;
+ const mutation = trpc.viewer.bookings.confirm.useMutation({
+ onSuccess: (data) => {
+ if (data?.status === BookingStatus.REJECTED) {
+ setRejectionDialogIsOpen(false);
+ showToast(t("booking_rejection_success"), "success");
+ } else {
+ showToast(t("booking_confirmation_success"), "success");
+ }
+ utils.viewer.bookings.invalidate();
+ },
+ onError: () => {
+ showToast(t("booking_confirmation_failed"), "error");
+ utils.viewer.bookings.invalidate();
+ },
+ });
+
+ const isUpcoming = new Date(booking.endTime) >= new Date();
+ const isPast = new Date(booking.endTime) < new Date();
+ const isCancelled = booking.status === BookingStatus.CANCELLED;
+ const isConfirmed = booking.status === BookingStatus.ACCEPTED;
+ const isRejected = booking.status === BookingStatus.REJECTED;
+ const isPending = booking.status === BookingStatus.PENDING;
+ const isRecurring = booking.recurringEventId !== null;
+ const isTabRecurring = booking.listingStatus === "recurring";
+ const isTabUnconfirmed = booking.listingStatus === "unconfirmed";
+
+ const paymentAppData = getPaymentAppData(booking.eventType);
+
+ const location = booking.location as ReturnType;
+ const locationVideoCallUrl = bookingMetadataSchema.parse(booking?.metadata || {})?.videoCallUrl;
+
+ const locationToDisplay = getSuccessPageLocationMessage(
+ locationVideoCallUrl ? locationVideoCallUrl : location,
+ t,
+ booking.status
+ );
+ const provider = guessEventLocationType(location);
+
+ const bookingConfirm = async (confirm: boolean) => {
+ let body = {
+ bookingId: booking.id,
+ confirmed: confirm,
+ reason: rejectionReason,
+ };
+ /**
+ * Only pass down the recurring event id when we need to confirm the entire series, which happens in
+ * the "Recurring" tab and "Unconfirmed" tab, to support confirming discretionally in the "Recurring" tab.
+ */
+ if ((isTabRecurring || isTabUnconfirmed) && isRecurring) {
+ body = Object.assign({}, body, { recurringEventId: booking.recurringEventId });
+ }
+ mutation.mutate(body);
+ };
+
+ const getSeatReferenceUid = () => {
+ if (!booking.seatsReferences[0]) {
+ return undefined;
+ }
+ return booking.seatsReferences[0].referenceUid;
+ };
+
+ const pendingActions: ActionType[] = [
+ {
+ id: "reject",
+ label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("reject_all") : t("reject"),
+ onClick: () => {
+ setRejectionDialogIsOpen(true);
+ },
+ icon: Ban,
+ disabled: mutation.isPending,
+ },
+ // For bookings with payment, only confirm if the booking is paid for
+ ...((isPending && !paymentAppData.enabled) ||
+ (paymentAppData.enabled && !!paymentAppData.price && booking.paid)
+ ? [
+ {
+ id: "confirm",
+ bookingId: booking.id,
+ label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
+ onClick: () => {
+ bookingConfirm(true);
+ },
+ icon: Check,
+ disabled: mutation.isPending,
+ },
+ ]
+ : []),
+ ];
+
+ let bookedActions: ActionType[] = [
+ {
+ id: "cancel",
+ label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
+ /* When cancelling we need to let the UI and the API know if the intention is to
+ cancel all remaining bookings or just that booking instance. */
+ href: `/booking/${booking.uid}?cancel=true${
+ isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
+ }${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
+ `,
+ icon: X,
+ },
+ {
+ id: "edit_booking",
+ label: t("edit"),
+ actions: [
+ {
+ id: "reschedule",
+ icon: Clock,
+ label: t("reschedule_booking"),
+ href: `${bookerUrl}/reschedule/${booking.uid}${
+ booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
+ }`,
+ },
+ {
+ id: "reschedule_request",
+ icon: Send,
+ iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
+ label: t("send_reschedule_request"),
+ onClick: () => {
+ setIsOpenRescheduleDialog(true);
+ },
+ },
+ {
+ id: "change_location",
+ label: t("edit_location"),
+ onClick: () => {
+ setIsOpenLocationDialog(true);
+ },
+ icon: MapPin,
+ },
+ ],
+ },
+ ];
+
+ const chargeCardActions: ActionType[] = [
+ {
+ id: "charge_card",
+ label: cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee"),
+ disabled: cardCharged,
+ onClick: () => {
+ setChargeCardDialogIsOpen(true);
+ },
+ icon: CreditCard,
+ },
+ ];
+
+ if (isTabRecurring && isRecurring) {
+ bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
+ }
+
+ if (isPast && isPending && !isConfirmed) {
+ bookedActions = bookedActions.filter((action) => action.id !== "cancel");
+ }
+
+ const RequestSentMessage = () => {
+ return (
+
+ {t("reschedule_request_sent")}
+
+ );
+ };
+
+ const startTime = dayjs(booking.startTime)
+ .tz(userTimeZone)
+ .locale(language)
+ .format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
+ const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
+ const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
+ const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
+ onSuccess: () => {
+ showToast(t("location_updated"), "success");
+ setIsOpenLocationDialog(false);
+ utils.viewer.bookings.invalidate();
+ },
+ });
+
+ const saveLocation = (
+ newLocationType: EventLocationType["type"],
+ details: {
+ [key: string]: string;
+ }
+ ) => {
+ let newLocation = newLocationType as string;
+ const eventLocationType = getEventLocationType(newLocationType);
+ if (eventLocationType?.organizerInputType) {
+ newLocation = details[Object.keys(details)[0]];
+ }
+ setLocationMutation.mutate({ bookingId: booking.id, newLocation, details });
+ };
+
+ // Getting accepted recurring dates to show
+ const recurringDates = booking.recurringInfo?.bookings[BookingStatus.ACCEPTED]
+ .concat(booking.recurringInfo?.bookings[BookingStatus.CANCELLED])
+ .concat(booking.recurringInfo?.bookings[BookingStatus.PENDING])
+ .sort((date1: Date, date2: Date) => date1.getTime() - date2.getTime());
+
+ const buildBookingLink = () => {
+ const urlSearchParams = new URLSearchParams({
+ allRemainingBookings: isTabRecurring.toString(),
+ });
+ if (booking.attendees[0]) urlSearchParams.set("email", booking.attendees[0].email);
+ return `/booking/${booking.uid}?${urlSearchParams.toString()}`;
+ };
+
+ const bookingLink = buildBookingLink();
+
+ const title = booking.title;
+
+ const showViewRecordingsButton = !!(booking.isRecorded && isPast && isConfirmed);
+ const showCheckRecordingButton =
+ isPast &&
+ isConfirmed &&
+ !booking.isRecorded &&
+ (!booking.location || booking.location === "integrations:daily" || booking?.location?.trim() === "");
+
+ const showRecordingActions: ActionType[] = [
+ {
+ id: "view_recordings",
+ label: showCheckRecordingButton ? t("check_for_recordings") : t("view_recordings"),
+ onClick: () => {
+ setViewRecordingsDialogIsOpen(true);
+ },
+ color: showCheckRecordingButton ? "secondary" : "primary",
+ disabled: mutation.isPending,
+ },
+ ];
+
+ return (
+ <>
+
+
+ {booking.paid && booking.payment[0] && (
+
+ )}
+ {(showViewRecordingsButton || showCheckRecordingButton) && (
+
+ )}
+ {/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
+
+
+
+
+ {t("rejection_reason")}
+ (Optional)
+ >
+ }
+ value={rejectionReason}
+ onChange={(e) => setRejectionReason(e.target.value)}
+ />
+
+
+
+
+ {
+ bookingConfirm(false);
+ }}>
+ {t("rejection_confirmation")}
+
+
+
+
+
+
+
+
+
+
{startTime}
+
+ {formatTime(booking.startTime, userTimeFormat, userTimeZone)} -{" "}
+ {formatTime(booking.endTime, userTimeFormat, userTimeZone)}
+
+
+ {!isPending && (
+
+ )}
+ {isPending && (
+
+ {t("unconfirmed")}
+
+ )}
+ {booking.eventType?.team && (
+
+ {booking.eventType.team.name}
+
+ )}
+ {booking.paid && !booking.payment[0] ? (
+
+ {t("error_collecting_card")}
+
+ ) : booking.paid ? (
+
+ {booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
+
+ ) : null}
+ {recurringDates !== undefined && (
+
+
+
+ )}
+
+
+
+
+
+ {/* Time and Badges for mobile */}
+
+
+
{startTime}
+
+ {formatTime(booking.startTime, userTimeFormat, userTimeZone)} -{" "}
+ {formatTime(booking.endTime, userTimeFormat, userTimeZone)}
+
+
+
+
+ {isPending && (
+
+ {t("unconfirmed")}
+
+ )}
+ {booking.eventType?.team && (
+
+ {booking.eventType.team.name}
+
+ )}
+ {!!booking?.eventType?.price && !booking.paid && (
+
+ {t("pending_payment")}
+
+ )}
+ {recurringDates !== undefined && (
+
+
+
+ )}
+
+
+
+
+ {title}
+
+
+ {paymentAppData.enabled && !booking.paid && booking.payment.length && (
+
+ {t("pending_payment")}
+
+ )}
+
+ {booking.description && (
+
+ "{booking.description}"
+
+ )}
+ {booking.attendees.length !== 0 && (
+
+ )}
+ {isCancelled && booking.rescheduled && (
+
+
+
+ )}
+
+
+
+
+ {isUpcoming && !isCancelled ? (
+ <>
+ {isPending && userId === booking.user?.id && }
+ {isConfirmed && }
+ {isRejected && {t("rejected")}
}
+ >
+ ) : null}
+ {isPast && isPending && !isConfirmed ? : null}
+ {(showViewRecordingsButton || showCheckRecordingButton) && (
+
+ )}
+ {isCancelled && booking.rescheduled && (
+
+
+
+ )}
+ {booking.status === "ACCEPTED" && booking.paid && booking.payment[0]?.paymentOption === "HOLD" && (
+
+ )}
+
+
+ >
+ );
+}
+
+interface RecurringBookingsTooltipProps {
+ booking: BookingItemProps;
+ recurringDates: Date[];
+ userTimeZone: string | undefined;
+ userTimeFormat: number | null | undefined;
+}
+
+const RecurringBookingsTooltip = ({
+ booking,
+ recurringDates,
+ userTimeZone,
+ userTimeFormat,
+}: RecurringBookingsTooltipProps) => {
+ const {
+ t,
+ i18n: { language },
+ } = useLocale();
+ const now = new Date();
+ const recurringCount = recurringDates.filter((recurringDate) => {
+ return (
+ recurringDate >= now &&
+ !booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
+ .map((date) => date.toString())
+ .includes(recurringDate.toString())
+ );
+ }).length;
+
+ return (
+ (booking.recurringInfo &&
+ booking.eventType?.recurringEvent?.freq &&
+ (booking.listingStatus === "recurring" ||
+ booking.listingStatus === "unconfirmed" ||
+ booking.listingStatus === "cancelled") && (
+
+
+
{
+ const pastOrCancelled =
+ aDate < now ||
+ booking.recurringInfo?.bookings[BookingStatus.CANCELLED]
+ .map((date) => date.toString())
+ .includes(aDate.toString());
+ return (
+
+ {formatTime(aDate, userTimeFormat, userTimeZone)}
+ {" - "}
+ {dayjs(aDate).locale(language).format("D MMMM YYYY")}
+
+ );
+ })}>
+
+
+
+ {booking.status === BookingStatus.ACCEPTED
+ ? `${t("event_remaining_other", {
+ count: recurringCount,
+ })}`
+ : getEveryFreqFor({
+ t,
+ recurringEvent: booking.eventType.recurringEvent,
+ recurringCount: booking.recurringInfo.count,
+ })}
+
+
+
+
+
+ )) ||
+ null
+ );
+};
+
+interface UserProps {
+ id: number;
+ name: string | null;
+ email: string;
+}
+
+const FirstAttendee = ({
+ user,
+ currentEmail,
+}: {
+ user: UserProps;
+ currentEmail: string | null | undefined;
+}) => {
+ const { t } = useLocale();
+ return user.email === currentEmail ? (
+ {t("you")}
+ ) : (
+ e.stopPropagation()}>
+ {user.name}
+
+ );
+};
+
+type AttendeeProps = {
+ name?: string;
+ email: string;
+};
+
+const Attendee = ({ email, name }: AttendeeProps) => {
+ return (
+ e.stopPropagation()}>
+ {name || email}
+
+ );
+};
+
+const DisplayAttendees = ({
+ attendees,
+ user,
+ currentEmail,
+}: {
+ attendees: AttendeeProps[];
+ user: UserProps | null;
+ currentEmail?: string | null;
+}) => {
+ const { t } = useLocale();
+ return (
+
+ {user &&
}
+ {attendees.length > 1 ?
, :
{t("and")} }
+
+ {attendees.length > 1 && (
+ <>
+
{t("and")}
+ {attendees.length > 2 ? (
+
(
+
+
+
+ ))}>
+ {t("plus_more", { count: attendees.length - 1 })}
+
+ ) : (
+
+ )}
+ >
+ )}
+
+ );
+};
+
+export default BookingListItem;
+import { useRouter } from "next/navigation";
+import { useCallback, useState } from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
+import type { RecurringEvent } from "@calcom/types/Calendar";
+import { Button, TextArea } from "@calcom/ui";
+import { X } from "@calcom/ui/components/icon";
+
+type Props = {
+ booking: {
+ title?: string;
+ uid?: string;
+ id?: number;
+ };
+ profile: {
+ name: string | null;
+ slug: string | null;
+ };
+ recurringEvent: RecurringEvent | null;
+ team?: string | null;
+ setIsCancellationMode: (value: boolean) => void;
+ theme: string | null;
+ allRemainingBookings: boolean;
+ seatReferenceUid?: string;
+};
+
+export default function CancelBooking(props: Props) {
+ const [cancellationReason, setCancellationReason] = useState("");
+ const { t } = useLocale();
+ const router = useRouter();
+ const { booking, allRemainingBookings, seatReferenceUid } = props;
+ const [loading, setLoading] = useState(false);
+ const telemetry = useTelemetry();
+ const [error, setError] = useState(booking ? null : t("booking_already_cancelled"));
+
+ const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
+ if (node !== null) {
+ node.scrollIntoView({ behavior: "smooth" });
+ node.focus();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return (
+ <>
+ {error && (
+
+
+
+
+
+
+ {error}
+
+
+
+ )}
+ {!error && (
+
+
{t("cancellation_reason")}
+
+ )}
+ >
+ );
+}
+import * as RadioGroup from "@radix-ui/react-radio-group";
+import classNames from "classnames";
+import Link from "next/link";
+import { useState } from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+
+const ChooseLicense = (
+ props: {
+ value: string;
+ onChange: (value: string) => void;
+ onSubmit: (value: string) => void;
+ } & Omit
+) => {
+ const { value: initialValue = "FREE", onChange, onSubmit, ...rest } = props;
+ const [value, setValue] = useState(initialValue);
+ const { t } = useLocale();
+
+ return (
+
+ );
+};
+
+export default ChooseLicense;
+import { zodResolver } from "@hookform/resolvers/zod";
+// eslint-disable-next-line no-restricted-imports
+import { noop } from "lodash";
+import { useCallback, useState } from "react";
+import { Controller, FormProvider, useForm, useFormState } from "react-hook-form";
+import * as z from "zod";
+
+import { classNames } from "@calcom/lib";
+import { CONSOLE_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
+import { trpc } from "@calcom/trpc/react";
+import { Button, TextField } from "@calcom/ui";
+import { Check, ExternalLink, Loader } from "@calcom/ui/components/icon";
+
+type EnterpriseLicenseFormValues = {
+ licenseKey: string;
+};
+
+const makeSchemaLicenseKey = (args: { callback: (valid: boolean) => void; onSuccessValidate: () => void }) =>
+ z.object({
+ licenseKey: z
+ .string()
+ .uuid({
+ message: "License key must follow UUID format: 8-4-4-4-12",
+ })
+ .superRefine(async (data, ctx) => {
+ const parse = z.string().uuid().safeParse(data);
+ if (parse.success) {
+ args.callback(true);
+ const response = await fetch(`${CONSOLE_URL}/api/license?key=${data}`);
+ args.callback(false);
+ const json = await response.json();
+ if (!json.valid) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `License key ${json.message.toLowerCase()}`,
+ });
+ } else {
+ args.onSuccessValidate();
+ }
+ }
+ }),
+ });
+
+const EnterpriseLicense = (
+ props: {
+ licenseKey?: string;
+ initialValue?: Partial;
+ onSuccessValidate: () => void;
+ onSubmit: (value: EnterpriseLicenseFormValues) => void;
+ onSuccess?: (
+ data: RouterOutputs["viewer"]["deploymentSetup"]["update"],
+ variables: RouterInputs["viewer"]["deploymentSetup"]["update"]
+ ) => void;
+ } & Omit
+) => {
+ const { onSubmit, onSuccess = noop, onSuccessValidate = noop, ...rest } = props;
+ const { t } = useLocale();
+ const [checkLicenseLoading, setCheckLicenseLoading] = useState(false);
+ const mutation = trpc.viewer.deploymentSetup.update.useMutation({
+ onSuccess,
+ });
+
+ const schemaLicenseKey = useCallback(
+ () =>
+ makeSchemaLicenseKey({
+ callback: setCheckLicenseLoading,
+ onSuccessValidate,
+ }),
+ [setCheckLicenseLoading, onSuccessValidate]
+ );
+
+ const formMethods = useForm({
+ defaultValues: {
+ licenseKey: props.licenseKey || "",
+ },
+ resolver: zodResolver(schemaLicenseKey()),
+ });
+
+ const handleSubmit = formMethods.handleSubmit((values) => {
+ onSubmit(values);
+ setCheckLicenseLoading(false);
+ mutation.mutate(values);
+ });
+
+ const { isDirty, errors } = useFormState(formMethods);
+
+ return (
+
+
+
+ );
+};
+
+export default EnterpriseLicense;
+import { zodResolver } from "@hookform/resolvers/zod";
+import classNames from "classnames";
+import { signIn } from "next-auth/react";
+import React from "react";
+import { Controller, FormProvider, useForm } from "react-hook-form";
+import * as z from "zod";
+
+import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
+import { WEBSITE_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
+import { UserCheck } from "@calcom/ui/components/icon";
+
+export const AdminUserContainer = (props: React.ComponentProps & { userCount: number }) => {
+ const { t } = useLocale();
+ if (props.userCount > 0)
+ return (
+
+ );
+ return ;
+};
+
+export const AdminUser = (props: { onSubmit: () => void; onError: () => void; onSuccess: () => void }) => {
+ const { t } = useLocale();
+
+ const formSchema = z.object({
+ username: z
+ .string()
+ .refine((val) => val.trim().length >= 1, { message: t("at_least_characters", { count: 1 }) }),
+ email_address: z.string().email({ message: t("enter_valid_email") }),
+ full_name: z.string().min(3, t("at_least_characters", { count: 3 })),
+ password: z.string().superRefine((data, ctx) => {
+ const isStrict = true;
+ const result = isPasswordValid(data, true, isStrict);
+ Object.keys(result).map((key: string) => {
+ if (!result[key as keyof typeof result]) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: [key],
+ message: key,
+ });
+ }
+ });
+ }),
+ });
+
+ type formSchemaType = z.infer;
+
+ const formMethods = useForm({
+ mode: "onChange",
+ resolver: zodResolver(formSchema),
+ });
+
+ const onError = () => {
+ props.onError();
+ };
+
+ const onSubmit = formMethods.handleSubmit(async (data) => {
+ props.onSubmit();
+ const response = await fetch("/api/auth/setup", {
+ method: "POST",
+ body: JSON.stringify({
+ username: data.username.trim(),
+ full_name: data.full_name,
+ email_address: data.email_address.toLowerCase(),
+ password: data.password,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ if (response.status === 200) {
+ await signIn("credentials", {
+ redirect: false,
+ callbackUrl: "/",
+ email: data.email_address.toLowerCase(),
+ password: data.password,
+ });
+ props.onSuccess();
+ } else {
+ props.onError();
+ }
+ }, onError);
+
+ const longWebsiteUrl = WEBSITE_URL.length > 30;
+
+ return (
+
+
+
+ );
+};
+import { useRouter } from "next/navigation";
+import type { Dispatch, SetStateAction } from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Check } from "@calcom/ui/components/icon";
+
+const StepDone = (props: {
+ currentStep: number;
+ nextStepPath: string;
+ setIsPending: Dispatch>;
+}) => {
+ const router = useRouter();
+ const { t } = useLocale();
+
+ return (
+
+ );
+};
+
+export default StepDone;
+import { CALCOM_VERSION } from "@calcom/lib/constants";
+import { trpc } from "@calcom/trpc/react";
+
+export function useViewerI18n(locale: string) {
+ return trpc.viewer.public.i18n.useQuery(
+ { locale, CalComVersion: CALCOM_VERSION },
+ {
+ /**
+ * i18n should never be clubbed with other queries, so that it's caching can be managed independently.
+ **/
+ trpc: {
+ context: { skipBatch: true },
+ },
+ }
+ );
+}
+import type { Attendee, Booking, User } from "@prisma/client";
+import type { FC } from "react";
+import { useMemo } from "react";
+import { JsonLd } from "react-schemaorg";
+import type { EventReservation, Person, ReservationStatusType } from "schema-dts";
+
+type EventSchemaUser = Pick;
+type EventSchemaAttendee = Pick;
+
+interface EventReservationSchemaInterface {
+ reservationId: Booking["uid"];
+ eventName: Booking["title"];
+ startTime: Booking["startTime"];
+ endTime: Booking["endTime"];
+ organizer: EventSchemaUser | null;
+ attendees: EventSchemaAttendee[];
+ location: Booking["location"];
+ description: Booking["description"];
+ status: Booking["status"];
+}
+
+const EventReservationSchema: FC = ({
+ reservationId,
+ eventName,
+ startTime,
+ endTime,
+ organizer,
+ attendees,
+ location,
+ description,
+ status,
+}) => {
+ const reservationStatus = useMemo(() => {
+ switch (status) {
+ case "ACCEPTED":
+ return "ReservationConfirmed";
+ case "REJECTED":
+ case "CANCELLED":
+ return "ReservationCancelled";
+ case "PENDING":
+ return "ReservationPending";
+ default:
+ return "ReservationHold";
+ }
+ }, [status]);
+
+ return (
+
+ item={{
+ "@context": "https://schema.org",
+ "@type": "EventReservation",
+ reservationId,
+ reservationStatus,
+ reservationFor: {
+ "@type": "Event",
+ name: eventName,
+ startDate: startTime.toString(),
+ endDate: endTime.toString(),
+ organizer: organizer
+ ? ({ "@type": "Person", name: organizer.name, email: organizer.email } as Person)
+ : undefined,
+ attendee: attendees?.map(
+ (person) => ({ "@type": "Person", name: person.name, email: person.email } as Person)
+ ),
+ location: location || undefined,
+ description: description || undefined,
+ },
+ }}
+ />
+ );
+};
+
+export default EventReservationSchema;
+import { ErrorMessage } from "@hookform/error-message";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { isValidPhoneNumber } from "libphonenumber-js";
+import { Trans } from "next-i18next";
+import Link from "next/link";
+import { useEffect } from "react";
+import { Controller, useForm, useWatch, useFormContext } from "react-hook-form";
+import { z } from "zod";
+
+import type { EventLocationType, LocationObject } from "@calcom/app-store/locations";
+import {
+ getEventLocationType,
+ getHumanReadableLocationValue,
+ getMessageForOrganizer,
+ LocationType,
+ OrganizerDefaultConferencingAppType,
+} from "@calcom/app-store/locations";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import { trpc } from "@calcom/trpc/react";
+import { Input } from "@calcom/ui";
+import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
+import { MapPin } from "@calcom/ui/components/icon";
+
+import { QueryCell } from "@lib/QueryCell";
+
+import CheckboxField from "@components/ui/form/CheckboxField";
+import type { LocationOption } from "@components/ui/form/LocationSelect";
+import LocationSelect from "@components/ui/form/LocationSelect";
+
+type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
+
+interface ISetLocationDialog {
+ saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
+ selection?: LocationOption;
+ booking?: BookingItem;
+ defaultValues?: LocationObject[];
+ setShowLocationModal: React.Dispatch>;
+ isOpenDialog: boolean;
+ setSelectedLocation?: (param: LocationOption | undefined) => void;
+ setEditingLocationType?: (param: string) => void;
+ teamId?: number;
+}
+
+const LocationInput = (props: {
+ eventLocationType: EventLocationType;
+ locationFormMethods: ReturnType;
+ id: string;
+ required: boolean;
+ placeholder: string;
+ className?: string;
+ defaultValue?: string;
+}): JSX.Element | null => {
+ const { eventLocationType, locationFormMethods, ...remainingProps } = props;
+ const { control } = useFormContext() as typeof locationFormMethods;
+ if (eventLocationType?.organizerInputType === "text") {
+ return (
+
+ );
+ } else if (eventLocationType?.organizerInputType === "phone") {
+ const { defaultValue, ...rest } = remainingProps;
+
+ return (
+ {
+ return ;
+ }}
+ />
+ );
+ }
+ return null;
+};
+
+export const EditLocationDialog = (props: ISetLocationDialog) => {
+ const {
+ saveLocation,
+ selection,
+ booking,
+ setShowLocationModal,
+ isOpenDialog,
+ defaultValues,
+ setSelectedLocation,
+ setEditingLocationType,
+ teamId,
+ } = props;
+ const { t } = useLocale();
+ const locationsQuery = trpc.viewer.locationOptions.useQuery({ teamId });
+
+ useEffect(() => {
+ if (selection) {
+ locationFormMethods.setValue("locationType", selection?.value);
+ if (selection?.address) {
+ locationFormMethods.setValue("locationAddress", selection?.address);
+ }
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [selection]);
+
+ const locationFormSchema = z.object({
+ locationType: z.string(),
+ phone: z.string().optional().nullable(),
+ locationAddress: z.string().optional(),
+ credentialId: z.number().optional(),
+ teamName: z.string().optional(),
+ locationLink: z
+ .string()
+ .optional()
+ .superRefine((val, ctx) => {
+ if (
+ eventLocationType &&
+ !eventLocationType.default &&
+ eventLocationType.linkType === "static" &&
+ eventLocationType.urlRegExp
+ ) {
+ const valid = z.string().regex(new RegExp(eventLocationType.urlRegExp)).safeParse(val).success;
+ if (!valid) {
+ const sampleUrl = eventLocationType.organizerInputPlaceholder;
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `Invalid URL for ${eventLocationType.label}. ${
+ sampleUrl ? `Sample URL: ${sampleUrl}` : ""
+ }`,
+ });
+ }
+ return;
+ }
+
+ const valid = z.string().url().optional().safeParse(val).success;
+ if (!valid) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ message: `Invalid URL`,
+ });
+ }
+ return;
+ }),
+ displayLocationPublicly: z.boolean().optional(),
+ locationPhoneNumber: z
+ .string()
+ .nullable()
+ .refine((val) => {
+ if (val === null) return false;
+ return isValidPhoneNumber(val);
+ })
+ .optional(),
+ });
+
+ const locationFormMethods = useForm({
+ mode: "onSubmit",
+ resolver: zodResolver(locationFormSchema),
+ });
+
+ const selectedLocation = useWatch({
+ control: locationFormMethods.control,
+ name: "locationType",
+ });
+
+ const selectedAddrValue = useWatch({
+ control: locationFormMethods.control,
+ name: "locationAddress",
+ });
+
+ const eventLocationType = getEventLocationType(selectedLocation);
+
+ const defaultLocation = defaultValues?.find(
+ (location: { type: EventLocationType["type"]; address?: string }) => {
+ if (location.type === LocationType.InPerson) {
+ return location.type === eventLocationType?.type && location.address === selectedAddrValue;
+ } else {
+ return location.type === eventLocationType?.type;
+ }
+ }
+ );
+
+ const LocationOptions = (() => {
+ if (eventLocationType && eventLocationType.organizerInputType && LocationInput) {
+ if (!eventLocationType.variable) {
+ console.error("eventLocationType.variable can't be undefined");
+ return null;
+ }
+
+ return (
+
+
+ {t(eventLocationType.messageForOrganizer || "")}
+
+
+
+
+
+ {!booking && (
+
+ (
+
+ locationFormMethods.setValue("displayLocationPublicly", e.target.checked)
+ }
+ informationIconText={t("display_location_info_badge")}
+ />
+ )}
+ />
+
+ )}
+
+ );
+ } else {
+ return {getMessageForOrganizer(selectedLocation, t)}
;
+ }
+ })();
+
+ return (
+ setShowLocationModal(open)}>
+
+
+
+
+
+
+
+
+ {t("edit_location")}
+
+ {!booking && (
+
+
+ Can't find the right video app? Visit our
+
+ App Store
+
+ .
+
+
+ )}
+
+
+
+ {booking && (
+ <>
+
{t("current_location")}:
+
+ {getHumanReadableLocationValue(booking.location, t)}
+
+ >
+ )}
+
+
+
+
+
+ );
+};
+import type { Dispatch, SetStateAction } from "react";
+import { useState } from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import {
+ Button,
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ showToast,
+ TextArea,
+} from "@calcom/ui";
+import { Clock } from "@calcom/ui/components/icon";
+
+interface IRescheduleDialog {
+ isOpenDialog: boolean;
+ setIsOpenDialog: Dispatch>;
+ bookingUId: string;
+}
+
+export const RescheduleDialog = (props: IRescheduleDialog) => {
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+ const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
+ const [rescheduleReason, setRescheduleReason] = useState("");
+
+ const { mutate: rescheduleApi, isPending } = trpc.viewer.bookings.requestReschedule.useMutation({
+ async onSuccess() {
+ showToast(t("reschedule_request_sent"), "success");
+ setIsOpenDialog(false);
+ await utils.viewer.bookings.invalidate();
+ },
+ onError() {
+ showToast(t("unexpected_error_try_again"), "error");
+ // @TODO: notify sentry
+ },
+ });
+
+ return (
+
+
+
+
+
+
+
+
+
{t("reschedule_modal_description")}
+
+ {t("reason_for_reschedule_request")}
+ (Optional)
+
+
+
+
+
+ );
+};
+import { useState } from "react";
+import type { Dispatch, SetStateAction } from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import {
+ Button,
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ showToast,
+} from "@calcom/ui";
+import { CreditCard, AlertTriangle } from "@calcom/ui/components/icon";
+
+interface IRescheduleDialog {
+ isOpenDialog: boolean;
+ setIsOpenDialog: Dispatch>;
+ bookingId: number;
+ paymentAmount: number;
+ paymentCurrency: string;
+}
+
+export const ChargeCardDialog = (props: IRescheduleDialog) => {
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+ const { isOpenDialog, setIsOpenDialog, bookingId } = props;
+ const [chargeError, setChargeError] = useState(false);
+ const chargeCardMutation = trpc.viewer.payments.chargeCard.useMutation({
+ onSuccess: () => {
+ utils.viewer.bookings.invalidate();
+ setIsOpenDialog(false);
+ showToast("Charge successful", "success");
+ },
+ onError: () => {
+ setChargeError(true);
+ },
+ });
+
+ const currencyStringParams = {
+ amount: props.paymentAmount / 100.0,
+ formatParams: { amount: { currency: props.paymentCurrency } },
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
{t("charge_card_dialog_body", currencyStringParams)}
+
+ {chargeError && (
+
+
+
{t("error_charging_card")}
+
+ )}
+
+
+
+
+ chargeCardMutation.mutate({
+ bookingId,
+ })
+ }>
+ {t("charge_attendee", currencyStringParams)}
+
+
+
+
+
+
+ );
+};
+import type { ReactNode } from "react";
+
+import { Badge } from "@calcom/ui";
+
+function pluralize(opts: { num: number; plural: string; singular: string }) {
+ if (opts.num === 0) {
+ return opts.singular;
+ }
+ return opts.singular;
+}
+
+export default function SubHeadingTitleWithConnections(props: { title: ReactNode; numConnections?: number }) {
+ const num = props.numConnections;
+ return (
+ <>
+ {props.title}
+ {num ? (
+
+ {num}{" "}
+ {pluralize({
+ num,
+ singular: "connection",
+ plural: "connections",
+ })}
+
+ ) : null}
+ >
+ );
+}
+import { DefaultSeo } from "next-seo";
+import { Inter } from "next/font/google";
+import localFont from "next/font/local";
+import Head from "next/head";
+import Script from "next/script";
+
+import "@calcom/embed-core/src/embed-iframe";
+import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
+import { IS_CALCOM, WEBAPP_URL } from "@calcom/lib/constants";
+import { buildCanonical } from "@calcom/lib/next-seo.config";
+
+import type { AppProps } from "@lib/app-providers";
+import AppProviders from "@lib/app-providers";
+import { seoConfig } from "@lib/config/next-seo.config";
+
+export interface CalPageWrapper {
+ (props?: AppProps): JSX.Element;
+ PageWrapper?: AppProps["Component"]["PageWrapper"];
+}
+
+const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
+const calFont = localFont({
+ src: "../fonts/CalSans-SemiBold.woff2",
+ variable: "--font-cal",
+ preload: true,
+ display: "swap",
+});
+
+function PageWrapper(props: AppProps) {
+ const { Component, pageProps, err, router } = props;
+ let pageStatus = "200";
+
+ if (router.pathname === "/404") {
+ pageStatus = "404";
+ } else if (router.pathname === "/500") {
+ pageStatus = "500";
+ }
+
+ // On client side don't let nonce creep into DOM
+ // It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
+ // See https://github.com/kentcdodds/nonce-hydration-issues
+ // Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
+ const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce;
+ const providerProps = {
+ ...props,
+ pageProps: {
+ ...props.pageProps,
+ nonce,
+ },
+ };
+ // Use the layout defined at the page level, if available
+ const getLayout = Component.getLayout ?? ((page) => page);
+
+ const path = router.asPath;
+
+ return (
+
+
+
+
+
+
+
+
+ {getLayout(
+ Component.requiresLicense ? (
+
+
+
+ ) : (
+
+ )
+ )}
+
+ );
+}
+
+export default PageWrapper;
+import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { RouterInputs } from "@calcom/trpc/react";
+import { trpc } from "@calcom/trpc/react";
+
+interface ICreateEventsOnCalendarSelectProps {
+ calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
+}
+
+const CreateEventsOnCalendarSelect = (props: ICreateEventsOnCalendarSelectProps) => {
+ const { calendar } = props;
+ const { t } = useLocale();
+ const mutation = trpc.viewer.setDestinationCalendar.useMutation();
+
+ return (
+ <>
+
+
+
+ {t("create_events_on")}
+
+
+ {
+ mutation.mutate(calendar);
+ }}
+ hidePlaceholder
+ />
+
+
+
+ >
+ );
+};
+
+export { CreateEventsOnCalendarSelect };
+import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
+
+interface IConnectedCalendarItem {
+ name: string;
+ logo: string;
+ externalId?: string;
+ integrationType: string;
+ calendars?: {
+ primary: true | null;
+ isSelected: boolean;
+ credentialId: number;
+ name?: string | undefined;
+ readOnly?: boolean | undefined;
+ userId?: number | undefined;
+ integration?: string | undefined;
+ externalId: string;
+ }[];
+}
+
+const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
+ const { name, logo, externalId, calendars, integrationType } = prop;
+
+ return (
+ <>
+
+
+
+
+ {name}
+ {/* Temporarily removed till we use it on another place */}
+ {/*
+ {t("default")}
+ */}
+
+
+
+ {externalId}{" "}
+
+
+
+ {/* Temporarily removed */}
+ {/*
+ {t("edit")}
+ */}
+
+
+
+
+ {calendars?.map((calendar, i) => (
+
+ ))}
+
+
+ >
+ );
+};
+
+export { ConnectedCalendarItem };
+import { SkeletonAvatar, SkeletonText, SkeletonButton } from "@calcom/ui";
+
+export function StepConnectionLoader() {
+ return (
+
+ {Array.from({ length: 4 }).map((_item, index) => {
+ return (
+
+
+
+
+
+ );
+ })}
+
+ );
+}
+import { InstallAppButtonWithoutPlanCheck } from "@calcom/app-store/components";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { App } from "@calcom/types/App";
+import { Button } from "@calcom/ui";
+
+interface IAppConnectionItem {
+ title: string;
+ description?: string;
+ logo: string;
+ type: App["type"];
+ installed?: boolean;
+}
+
+const AppConnectionItem = (props: IAppConnectionItem) => {
+ const { title, logo, type, installed } = props;
+ const { t } = useLocale();
+ return (
+
+
+
+
{title}
+
+
(
+ {
+ // Save cookie key to return url step
+ document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
+ buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
+ }}>
+ {installed ? t("installed") : t("connect")}
+
+ )}
+ />
+
+ );
+};
+
+export { AppConnectionItem };
+import { useRouter } from "next/navigation";
+import type { FormEvent } from "react";
+import { useRef, useState } from "react";
+import { useForm } from "react-hook-form";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { md } from "@calcom/lib/markdownIt";
+import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
+import turndown from "@calcom/lib/turndownService";
+import { trpc } from "@calcom/trpc/react";
+import type { Ensure } from "@calcom/types/utils";
+import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
+import { UserAvatar } from "@calcom/ui";
+import { ArrowRight } from "@calcom/ui/components/icon";
+
+type FormData = {
+ bio: string;
+};
+
+const UserProfile = () => {
+ const [user] = trpc.viewer.me.useSuspenseQuery();
+ const { t } = useLocale();
+ const avatarRef = useRef(null);
+ const { setValue, handleSubmit, getValues } = useForm({
+ defaultValues: { bio: user?.bio || "" },
+ });
+
+ const { data: eventTypes } = trpc.viewer.eventTypes.list.useQuery();
+ const [imageSrc, setImageSrc] = useState(user?.avatar || "");
+ const utils = trpc.useContext();
+ const router = useRouter();
+ const createEventType = trpc.viewer.eventTypes.create.useMutation();
+ const telemetry = useTelemetry();
+ const [firstRender, setFirstRender] = useState(true);
+
+ const mutation = trpc.viewer.updateProfile.useMutation({
+ onSuccess: async (_data, context) => {
+ if (context.avatar) {
+ showToast(t("your_user_profile_updated_successfully"), "success");
+ await utils.viewer.me.refetch();
+ } else {
+ try {
+ if (eventTypes?.length === 0) {
+ await Promise.all(
+ DEFAULT_EVENT_TYPES.map(async (event) => {
+ return createEventType.mutate(event);
+ })
+ );
+ }
+ } catch (error) {
+ console.error(error);
+ }
+
+ await utils.viewer.me.refetch();
+ router.push("/");
+ }
+ },
+ onError: () => {
+ showToast(t("problem_saving_user_profile"), "error");
+ },
+ });
+ const onSubmit = handleSubmit((data: { bio: string }) => {
+ const { bio } = data;
+
+ telemetry.event(telemetryEventTypes.onboardingFinished);
+
+ mutation.mutate({
+ bio,
+ completedOnboarding: true,
+ });
+ });
+
+ async function updateProfileHandler(event: FormEvent) {
+ event.preventDefault();
+ const enteredAvatar = avatarRef.current?.value;
+ mutation.mutate({
+ avatar: enteredAvatar,
+ });
+ }
+
+ const DEFAULT_EVENT_TYPES = [
+ {
+ title: t("15min_meeting"),
+ slug: "15min",
+ length: 15,
+ },
+ {
+ title: t("30min_meeting"),
+ slug: "30min",
+ length: 30,
+ },
+ {
+ title: t("secret_meeting"),
+ slug: "secret",
+ length: 15,
+ hidden: true,
+ },
+ ];
+
+ const organization =
+ user.organization && user.organization.id
+ ? {
+ ...(user.organization as Ensure),
+ slug: user.organization.slug || null,
+ requestedSlug: user.organization.metadata?.requestedSlug || null,
+ }
+ : null;
+ return (
+
+ );
+};
+
+export default UserProfile;
+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 } from "@calcom/ui";
+import { ArrowRight } from "@calcom/ui/components/icon";
+
+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) => {
+ 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 (
+
+ );
+};
+
+export { SetupAvailability };
+import { zodResolver } from "@hookform/resolvers/zod";
+import { useEffect } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import dayjs from "@calcom/dayjs";
+import { useTimePreferences } from "@calcom/features/bookings/lib";
+import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
+import { trpc } from "@calcom/trpc/react";
+import { Button, TimezoneSelect, Input } from "@calcom/ui";
+import { ArrowRight } from "@calcom/ui/components/icon";
+
+import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
+
+interface IUserSettingsProps {
+ nextStep: () => void;
+ hideUsername?: boolean;
+}
+
+const UserSettings = (props: IUserSettingsProps) => {
+ const { nextStep } = props;
+ const [user] = trpc.viewer.me.useSuspenseQuery();
+ const { t } = useLocale();
+ const { setTimezone: setSelectedTimeZone, timezone: selectedTimeZone } = useTimePreferences();
+ const telemetry = useTelemetry();
+ const userSettingsSchema = z.object({
+ name: z
+ .string()
+ .min(1)
+ .max(FULL_NAME_LENGTH_MAX_LIMIT, {
+ message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
+ }),
+ });
+ const {
+ register,
+ handleSubmit,
+ formState: { errors },
+ } = useForm>({
+ defaultValues: {
+ name: user?.name || "",
+ },
+ reValidateMode: "onChange",
+ resolver: zodResolver(userSettingsSchema),
+ });
+
+ useEffect(() => {
+ telemetry.event(telemetryEventTypes.onboardingStarted);
+ }, [telemetry]);
+
+ const utils = trpc.useContext();
+ const onSuccess = async () => {
+ await utils.viewer.me.invalidate();
+ nextStep();
+ };
+ const mutation = trpc.viewer.updateProfile.useMutation({
+ onSuccess: onSuccess,
+ });
+
+ const onSubmit = handleSubmit((data) => {
+ mutation.mutate({
+ name: data.name,
+ timeZone: selectedTimeZone,
+ });
+ });
+
+ return (
+
+ );
+};
+
+export { UserSettings };
+import classNames from "@calcom/lib/classNames";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { List } from "@calcom/ui";
+import { ArrowRight } from "@calcom/ui/components/icon";
+
+import { AppConnectionItem } from "../components/AppConnectionItem";
+import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
+import { CreateEventsOnCalendarSelect } from "../components/CreateEventsOnCalendarSelect";
+import { StepConnectionLoader } from "../components/StepConnectionLoader";
+
+interface IConnectCalendarsProps {
+ nextStep: () => void;
+}
+
+const ConnectedCalendars = (props: IConnectCalendarsProps) => {
+ const { nextStep } = props;
+ const queryConnectedCalendars = trpc.viewer.connectedCalendars.useQuery({ onboarding: true });
+ const { t } = useLocale();
+ const queryIntegrations = trpc.viewer.integrations.useQuery({
+ variant: "calendar",
+ onlyInstalled: false,
+ sortByMostPopular: true,
+ });
+
+ const firstCalendar = queryConnectedCalendars.data?.connectedCalendars.find(
+ (item) => item.calendars && item.calendars?.length > 0
+ );
+ const disabledNextButton = firstCalendar === undefined;
+ const destinationCalendar = queryConnectedCalendars.data?.destinationCalendar;
+ return (
+ <>
+ {/* Already connected calendars */}
+ {!queryConnectedCalendars.isPending &&
+ firstCalendar &&
+ firstCalendar.integration &&
+ firstCalendar.integration.title &&
+ firstCalendar.integration.logo && (
+ <>
+
+ 0
+ ? firstCalendar.calendars[0].externalId
+ : ""
+ }
+ calendars={firstCalendar.calendars}
+ integrationType={firstCalendar.integration.type}
+ />
+
+ {/* Create event on selected calendar */}
+
+ {t("connect_calendars_from_app_store")}
+ >
+ )}
+
+ {/* Connect calendars list */}
+ {firstCalendar === undefined && queryIntegrations.data && queryIntegrations.data.items.length > 0 && (
+
+ {queryIntegrations.data &&
+ queryIntegrations.data.items.map((item) => (
+
+ {item.title && item.logo && (
+
+ )}
+
+ ))}
+
+ )}
+
+ {queryIntegrations.isPending && }
+
+ nextStep()}
+ disabled={disabledNextButton}>
+ {firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
+
+
+ >
+ );
+};
+
+export { ConnectedCalendars };
+import classNames from "@calcom/lib/classNames";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { List } from "@calcom/ui";
+import { ArrowRight } from "@calcom/ui/components/icon";
+
+import { AppConnectionItem } from "../components/AppConnectionItem";
+import { StepConnectionLoader } from "../components/StepConnectionLoader";
+
+interface ConnectedAppStepProps {
+ nextStep: () => void;
+}
+
+const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
+ const { nextStep } = props;
+ const { data: queryConnectedVideoApps, isPending } = trpc.viewer.integrations.useQuery({
+ variant: "conferencing",
+ onlyInstalled: false,
+ sortByMostPopular: true,
+ });
+ const { t } = useLocale();
+
+ const hasAnyInstalledVideoApps = queryConnectedVideoApps?.items.some(
+ (item) => item.userCredentialIds.length > 0
+ );
+
+ return (
+ <>
+ {!isPending && (
+
+ {queryConnectedVideoApps?.items &&
+ queryConnectedVideoApps?.items.map((item) => {
+ if (item.slug === "daily-video") return null; // we dont want to show daily here as it is installed by default
+ return (
+
+ {item.name && item.logo && (
+ 0}
+ />
+ )}
+
+ );
+ })}
+
+ )}
+
+ {isPending && }
+ nextStep()}>
+ {t("next_step_text")}
+
+
+ >
+ );
+};
+
+export { ConnectedVideoStep };
+import Link from "next/link";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
+import { md } from "@calcom/lib/markdownIt";
+import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
+import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
+import { UserAvatar } from "@calcom/ui";
+
+type TeamType = Omit, "inviteToken">;
+type MembersType = TeamType["members"];
+type MemberType = Pick & {
+ safeBio: string | null;
+ bookerUrl: string;
+};
+
+const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
+ const routerQuery = useRouterQuery();
+ const { t } = useLocale();
+ const isBioEmpty = !member.bio || !member.bio.replace("
", "").length;
+
+ // We don't want to forward orgSlug and user which are route params to the next route
+ const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = routerQuery;
+
+ return (
+
+
+
+
+ {member.name}
+
+ {!isBioEmpty ? (
+ <>
+
+ >
+ ) : (
+ t("user_from_team", { user: member.name, team: teamName })
+ )}
+
+
+
+
+ );
+};
+
+const Members = ({ members, teamName }: { members: MemberType[]; teamName: string | null }) => {
+ if (!members || members.length === 0) {
+ return null;
+ }
+
+ return (
+
+ {members.map((member) => {
+ return member.username !== null && ;
+ })}
+
+ );
+};
+
+const Team = ({ members, teamName }: { members: MemberType[]; teamName: string | null }) => {
+ return (
+
+
+
+ );
+};
+
+export default Team;
+import { useState } from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { X } from "@calcom/ui/components/icon";
+
+export default function AddToHomescreen() {
+ const { t } = useLocale();
+ const [closeBanner, setCloseBanner] = useState(false);
+
+ if (typeof window !== "undefined") {
+ if (window.matchMedia("(display-mode: standalone)").matches) {
+ return null;
+ }
+ }
+ return !closeBanner ? (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t("add_to_homescreen")}
+
+
+
+
+ setCloseBanner(true)}
+ type="button"
+ className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
+ {t("dismiss")}
+
+
+
+
+
+
+
+ ) : null;
+}
+import React from "react";
+
+import { Button, SkeletonText } from "@calcom/ui";
+import { MoreHorizontal } from "@calcom/ui/components/icon";
+
+import classNames from "@lib/classNames";
+
+function SkeletonLoader() {
+ return (
+
+ );
+}
+
+export default SkeletonLoader;
+
+function SkeletonItem() {
+ return (
+
+
+
+ );
+}
+
+export const SelectSkeletonLoader = ({ className }: { className?: string }) => {
+ return (
+
+
+
+ );
+};
+import dynamic from "next/dynamic";
+import Link from "next/link";
+import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
+import { useEffect, useState } from "react";
+import { Controller, useFormContext } from "react-hook-form";
+import short from "short-uuid";
+import { v5 as uuidv5 } from "uuid";
+import type { z } from "zod";
+
+import type { EventNameObjectType } from "@calcom/core/event";
+import { getEventName } from "@calcom/core/event";
+import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
+import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
+import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
+import {
+ allowDisablingAttendeeConfirmationEmails,
+ allowDisablingHostConfirmationEmails,
+} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
+import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
+import type { EditableSchema } from "@calcom/features/form-builder/schema";
+import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
+import { classNames } from "@calcom/lib";
+import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
+import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { Prisma } from "@calcom/prisma/client";
+import { trpc } from "@calcom/trpc/react";
+import {
+ Alert,
+ Button,
+ CheckboxField,
+ Label,
+ SettingsToggle,
+ showToast,
+ TextField,
+ Tooltip,
+} from "@calcom/ui";
+import { Copy, Edit, Info } from "@calcom/ui/components/icon";
+
+import RequiresConfirmationController from "./RequiresConfirmationController";
+
+const CustomEventTypeModal = dynamic(() => import("@components/eventtype/CustomEventTypeModal"));
+
+const generateHashedLink = (id: number) => {
+ const translator = short();
+ const seed = `${id}:${new Date().getTime()}`;
+ const uid = translator.fromUUID(uuidv5(seed, uuidv5.URL));
+ return uid;
+};
+
+export const EventAdvancedTab = ({ eventType, team }: Pick) => {
+ const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
+ const { data: user } = trpc.viewer.me.useQuery();
+ const formMethods = useFormContext();
+ const { t } = useLocale();
+
+ const [showEventNameTip, setShowEventNameTip] = useState(false);
+ const [hashedLinkVisible, setHashedLinkVisible] = useState(!!formMethods.getValues("hashedLink"));
+ const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!formMethods.getValues("successRedirectUrl"));
+ const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
+
+ const bookingFields: Prisma.JsonObject = {};
+
+ const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow);
+ const selectedThemeIsDark =
+ user?.theme === "dark" ||
+ (!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
+
+ formMethods.getValues().bookingFields.forEach(({ name }) => {
+ bookingFields[name] = `${name} input`;
+ });
+
+ const eventNameObject: EventNameObjectType = {
+ attendeeName: t("scheduler"),
+ eventType: formMethods.getValues("title"),
+ eventName: formMethods.getValues("eventName"),
+ host: formMethods.getValues("users")[0]?.name || "Nameless",
+ bookingFields: bookingFields,
+ t,
+ };
+
+ const [requiresConfirmation, setRequiresConfirmation] = useState(
+ formMethods.getValues("requiresConfirmation")
+ );
+ const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
+ const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
+ const noShowFeeEnabled =
+ formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
+ formMethods.getValues("metadata")?.apps?.stripe?.paymentOption === "HOLD";
+
+ useEffect(() => {
+ !hashedUrl && setHashedUrl(generateHashedLink(formMethods.getValues("users")[0]?.id ?? team?.id));
+ }, [formMethods.getValues("users"), hashedUrl, team?.id]);
+
+ const toggleGuests = (enabled: boolean) => {
+ const bookingFields = formMethods.getValues("bookingFields");
+ formMethods.setValue(
+ "bookingFields",
+ bookingFields.map((field) => {
+ if (field.name === "guests") {
+ return {
+ ...field,
+ hidden: !enabled,
+ editable: (!enabled ? "system-but-hidden" : "system-but-optional") as z.infer<
+ typeof EditableSchema
+ >,
+ };
+ }
+ return field;
+ })
+ );
+ };
+
+ const { shouldLockDisableProps } = useLockedFieldsManager(
+ formMethods.getValues(),
+ t("locked_fields_admin_description"),
+ t("locked_fields_member_description")
+ );
+ const eventNamePlaceholder = getEventName({
+ ...eventNameObject,
+ eventName: formMethods.watch("eventName"),
+ });
+
+ const successRedirectUrlLocked = shouldLockDisableProps("successRedirectUrl");
+ const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
+
+ const closeEventNameTip = () => setShowEventNameTip(false);
+
+ return (
+
+ {/**
+ * Only display calendar selector if user has connected calendars AND if it's not
+ * a team event. Since we don't have logic to handle each attendee calendar (for now).
+ * This will fallback to each user selected destination calendar.
+ */}
+
+ {!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && (
+
+
+
+ {t("add_to_calendar")}
+
+
+ {t("add_another_calendar")}
+
+
+
(
+
+ )}
+ />
+ {t("select_which_cal")}
+
+ )}
+
+ setShowEventNameTip((old) => !old)}>
+
+
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
(
+ onChange(e)}
+ />
+ )}
+ />
+
+ (
+ onChange(e)}
+ />
+ )}
+ />
+
+ (
+ <>
+ {
+ setRedirectUrlVisible(e);
+ onChange(e ? value : "");
+ }}>
+
+
+
+ {t("redirect_url_warning")}
+
+
+
+ >
+ )}
+ />
+
+
+
+
+ }
+ {...shouldLockDisableProps("hashedLinkCheck")}
+ description={t("private_link_description", { appName: APP_NAME })}
+ checked={hashedLinkVisible}
+ onCheckedChange={(e) => {
+ formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
+ setHashedLinkVisible(e);
+ }}>
+
+ {!IS_VISUAL_REGRESSION_TESTING && (
+
+ {
+ navigator.clipboard.writeText(placeholderHashedLink);
+ if (formMethods.getValues("hashedLink")) {
+ showToast(t("private_link_copied"), "success");
+ } else {
+ showToast(t("enabled_after_update_description"), "warning");
+ }
+ }}>
+
+
+
+ }
+ />
+ )}
+
+
+
+ (
+ <>
+ {
+ // Enabling seats will disable guests and requiring confirmation until fully supported
+ if (e) {
+ toggleGuests(false);
+ formMethods.setValue("requiresConfirmation", false);
+ setRequiresConfirmation(false);
+ formMethods.setValue("metadata.multipleDuration", undefined);
+ formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2);
+ } else {
+ formMethods.setValue("seatsPerTimeSlot", null);
+ toggleGuests(true);
+ }
+ onChange(e);
+ }}>
+
+
(
+
+
{t("seats")}>}
+ onChange={(e) => {
+ onChange(Math.abs(Number(e.target.value)));
+ }}
+ data-testid="seats-per-time-slot"
+ />
+
+ (
+ onChange(e)}
+ checked={value}
+ />
+ )}
+ />
+
+
+ (
+ onChange(e)}
+ checked={value}
+ />
+ )}
+ />
+
+
+ )}
+ />
+
+
+ {noShowFeeEnabled && }
+ >
+ )}
+ />
+ (
+ onChange(e)}
+ data-testid="lock-timezone-toggle"
+ />
+ )}
+ />
+ {allowDisablingAttendeeConfirmationEmails(workflows) && (
+ (
+ <>
+ onChange(e)}
+ />
+ >
+ )}
+ />
+ )}
+ {allowDisablingHostConfirmationEmails(workflows) && (
+ (
+ <>
+ onChange(e)}
+ />
+ >
+ )}
+ />
+ )}
+ {showEventNameTip && (
+ formMethods.setValue("eventName", val)}
+ defaultValue={formMethods.getValues("eventName")}
+ placeHolder={eventNamePlaceholder}
+ event={eventNameObject}
+ />
+ )}
+
+ );
+};
+export type EventTypeDescriptionSafeProps = {
+ eventType: { description: string | null; descriptionAsSafeHTML: string | null };
+};
+
+export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
+ const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
+ if (eventType.description)
+ props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML || "" };
+ return
;
+};
+
+export default EventTypeDescriptionSafeHTML;
+export { default } from "@calcom/features/ee/workflows/components/EventWorkflowsTab";
+import { useAutoAnimate } from "@formkit/auto-animate/react";
+import * as RadioGroup from "@radix-ui/react-radio-group";
+import type { FormValues } from "pages/event-types/[type]";
+import type { Key } from "react";
+import React, { useEffect, useState } from "react";
+import type { UseFormRegisterReturn } from "react-hook-form";
+import { Controller, useFormContext } from "react-hook-form";
+import type { SingleValue } from "react-select";
+
+import { classNames } from "@calcom/lib";
+import type { DurationType } from "@calcom/lib/convertToNewDurationType";
+import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
+import findDurationType from "@calcom/lib/findDurationType";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { ascendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimit";
+import type { PeriodType } from "@calcom/prisma/enums";
+import type { IntervalLimit } from "@calcom/types/Calendar";
+import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
+import { Plus, Trash2 } from "@calcom/ui/components/icon";
+
+const MinimumBookingNoticeInput = React.forwardRef<
+ HTMLInputElement,
+ Omit, "ref">
+>(function MinimumBookingNoticeInput({ ...passThroughProps }, ref) {
+ const { t } = useLocale();
+ const { setValue, getValues } = useFormContext();
+ const durationTypeOptions: {
+ value: DurationType;
+ label: string;
+ }[] = [
+ {
+ label: t("minutes"),
+ value: "minutes",
+ },
+ {
+ label: t("hours"),
+ value: "hours",
+ },
+ {
+ label: t("days"),
+ value: "days",
+ },
+ ];
+
+ const [minimumBookingNoticeDisplayValues, setMinimumBookingNoticeDisplayValues] = useState<{
+ type: DurationType;
+ value: number;
+ }>({
+ type: findDurationType(getValues(passThroughProps.name)),
+ value: convertToNewDurationType(
+ "minutes",
+ findDurationType(getValues(passThroughProps.name)),
+ getValues(passThroughProps.name)
+ ),
+ });
+ // keep hidden field in sync with minimumBookingNoticeDisplayValues
+ useEffect(() => {
+ setValue(
+ passThroughProps.name,
+ convertToNewDurationType(
+ minimumBookingNoticeDisplayValues.type,
+ "minutes",
+ minimumBookingNoticeDisplayValues.value
+ )
+ );
+ }, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
+
+ return (
+
+
+
+ setMinimumBookingNoticeDisplayValues({
+ ...minimumBookingNoticeDisplayValues,
+ value: parseInt(e.target.value || "0", 10),
+ })
+ }
+ label={t("minimum_booking_notice")}
+ type="number"
+ placeholder="0"
+ min={0}
+ className="mb-0 h-9 rounded-[4px] ltr:mr-2 rtl:ml-2"
+ />
+
+
+
option.value === minimumBookingNoticeDisplayValues.type
+ )}
+ onChange={(input) => {
+ if (input) {
+ setMinimumBookingNoticeDisplayValues({
+ ...minimumBookingNoticeDisplayValues,
+ type: input.value,
+ });
+ }
+ }}
+ options={durationTypeOptions}
+ />
+
+ );
+});
+
+export const EventLimitsTab = () => {
+ const { t, i18n } = useLocale();
+ const formMethods = useFormContext();
+
+ const PERIOD_TYPES = [
+ {
+ type: "ROLLING" as const,
+ suffix: t("into_the_future"),
+ },
+ {
+ type: "RANGE" as const,
+ prefix: t("within_date_range"),
+ },
+ {
+ type: "UNLIMITED" as const,
+ prefix: t("indefinitely_into_future"),
+ },
+ ];
+
+ const watchPeriodType = formMethods.watch("periodType");
+
+ const optionsPeriod = [
+ { value: 0, label: t("business_days") },
+ { value: 1, label: t("calendar_days") },
+ ];
+
+ const [offsetToggle, setOffsetToggle] = useState(formMethods.getValues("offsetStart") > 0);
+
+ // Preview how the offset will affect start times
+ const watchOffsetStartValue = formMethods.watch("offsetStart");
+ const offsetOriginalTime = new Date();
+ offsetOriginalTime.setHours(9, 0, 0, 0);
+ const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + watchOffsetStartValue * 60 * 1000);
+
+ return (
+
+
+
+
+ {t("before_event")}
+ {
+ const beforeBufferOptions = [
+ {
+ label: t("event_buffer_default"),
+ value: 0,
+ },
+ ...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
+ label: `${minutes} ${t("minutes")}`,
+ value: minutes,
+ })),
+ ];
+ return (
+ {
+ if (val) onChange(val.value);
+ }}
+ defaultValue={
+ beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
+ }
+ options={beforeBufferOptions}
+ />
+ );
+ }}
+ />
+
+
+ {t("after_event")}
+ {
+ const afterBufferOptions = [
+ {
+ label: t("event_buffer_default"),
+ value: 0,
+ },
+ ...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
+ label: `${minutes} ${t("minutes")}`,
+ value: minutes,
+ })),
+ ];
+ return (
+ {
+ if (val) onChange(val.value);
+ }}
+ defaultValue={
+ afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
+ }
+ options={afterBufferOptions}
+ />
+ );
+ }}
+ />
+
+
+
+
+ {t("minimum_booking_notice")}
+
+
+
+ {t("slot_interval")}
+ {
+ const slotIntervalOptions = [
+ {
+ label: t("slot_interval_default"),
+ value: -1,
+ },
+ ...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
+ label: `${minutes} ${t("minutes")}`,
+ value: minutes,
+ })),
+ ];
+ return (
+ {
+ formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
+ }}
+ defaultValue={
+ slotIntervalOptions.find(
+ (option) => option.value === formMethods.getValues("slotInterval")
+ ) || slotIntervalOptions[0]
+ }
+ options={slotIntervalOptions}
+ />
+ );
+ }}
+ />
+
+
+
+
{
+ const isChecked = Object.keys(value ?? {}).length > 0;
+ return (
+ {
+ if (active) {
+ formMethods.setValue("bookingLimits", {
+ PER_DAY: 1,
+ });
+ } else {
+ formMethods.setValue("bookingLimits", {});
+ }
+ }}
+ switchContainerClassName={classNames(
+ "border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
+ isChecked && "rounded-b-none"
+ )}
+ childrenClassName="lg:ml-0">
+
+
+
+
+ );
+ }}
+ />
+ {
+ const isChecked = value;
+ return (
+ {
+ formMethods.setValue("onlyShowFirstAvailableSlot", active ?? false);
+ }}
+ switchContainerClassName={classNames(
+ "border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
+ isChecked && "rounded-b-none"
+ )}
+ />
+ );
+ }}
+ />
+ {
+ const isChecked = Object.keys(value ?? {}).length > 0;
+ return (
+ {
+ if (active) {
+ formMethods.setValue("durationLimits", {
+ PER_DAY: 60,
+ });
+ } else {
+ formMethods.setValue("durationLimits", {});
+ }
+ }}>
+
+
+
+
+ );
+ }}
+ />
+ {
+ const isChecked = value && value !== "UNLIMITED";
+
+ return (
+ formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
+
+
formMethods.setValue("periodType", val as PeriodType)}>
+ {PERIOD_TYPES.map((period) => {
+ if (period.type === "UNLIMITED") return null;
+ return (
+
+
+
+
+
+ {period.prefix ?
{period.prefix} : null}
+ {period.type === "ROLLING" && (
+
+
+
+ formMethods.setValue("periodCountCalendarDays", opt?.value === 1 ? "1" : "0")
+ }
+ name="periodCoundCalendarDays"
+ value={optionsPeriod.find((opt) => {
+ opt.value ===
+ (formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0);
+ })}
+ defaultValue={optionsPeriod.find(
+ (opt) =>
+ opt.value ===
+ (formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0)
+ )}
+ />
+
+ )}
+ {period.type === "RANGE" && (
+
+ (
+ {
+ formMethods.setValue("periodDates", {
+ startDate,
+ endDate,
+ });
+ }}
+ />
+ )}
+ />
+
+ )}
+ {period.suffix ?
{period.suffix} : null}
+
+ );
+ })}
+
+
+
+ );
+ }}
+ />
+ {
+ setOffsetToggle(active);
+ if (!active) {
+ formMethods.setValue("offsetStart", 0);
+ }
+ }}>
+
+ Number(value) })}
+ addOnSuffix={<>{t("minutes")}>}
+ hint={t("offset_start_description", {
+ originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
+ adjustedTime: offsetAdjustedTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
+ })}
+ />
+
+
+
+ );
+};
+
+type IntervalLimitsKey = keyof IntervalLimit;
+
+const INTERVAL_LIMIT_OPTIONS = ascendingLimitKeys.map((key) => ({
+ value: key as keyof IntervalLimit,
+ label: `Per ${intervalLimitKeyToUnit(key)}`,
+}));
+
+type IntervalLimitItemProps = {
+ key: Key;
+ limitKey: IntervalLimitsKey;
+ step: number;
+ value: number;
+ textFieldSuffix?: string;
+ disabled?: boolean;
+ selectOptions: { value: keyof IntervalLimit; label: string }[];
+ hasDeleteButton?: boolean;
+ onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
+ onLimitChange: (intervalLimitsKey: IntervalLimitsKey, limit: number) => void;
+ onIntervalSelect: (interval: SingleValue<{ value: keyof IntervalLimit; label: string }>) => void;
+};
+
+const IntervalLimitItem = ({
+ limitKey,
+ step,
+ value,
+ textFieldSuffix,
+ selectOptions,
+ hasDeleteButton,
+ disabled,
+ onDelete,
+ onLimitChange,
+ onIntervalSelect,
+}: IntervalLimitItemProps) => {
+ return (
+
+ onLimitChange(limitKey, parseInt(e.target.value || "0", 10))}
+ />
+ option.value === limitKey)}
+ onChange={onIntervalSelect}
+ className="w-36"
+ />
+ {hasDeleteButton && !disabled && (
+ onDelete(limitKey)}
+ />
+ )}
+
+ );
+};
+
+type IntervalLimitsManagerProps = {
+ propertyName: K;
+ defaultLimit: number;
+ step: number;
+ textFieldSuffix?: string;
+ disabled?: boolean;
+};
+
+const IntervalLimitsManager = ({
+ propertyName,
+ defaultLimit,
+ step,
+ textFieldSuffix,
+ disabled,
+}: IntervalLimitsManagerProps) => {
+ const { watch, setValue, control } = useFormContext();
+ const watchIntervalLimits = watch(propertyName);
+ const { t } = useLocale();
+
+ const [animateRef] = useAutoAnimate();
+
+ return (
+ {
+ const currentIntervalLimits = value;
+
+ const addLimit = () => {
+ if (!currentIntervalLimits || !watchIntervalLimits) return;
+ const currentKeys = Object.keys(watchIntervalLimits);
+
+ const [rest] = Object.values(INTERVAL_LIMIT_OPTIONS).filter(
+ (option) => !currentKeys.includes(option.value)
+ );
+ if (!rest || !currentKeys.length) return;
+ //currentDurationLimits is always defined so can be casted
+ // @ts-expect-error FIXME Fix these typings
+ setValue(propertyName, {
+ ...watchIntervalLimits,
+ [rest.value]: defaultLimit,
+ });
+ };
+
+ return (
+
+ {currentIntervalLimits &&
+ watchIntervalLimits &&
+ Object.entries(currentIntervalLimits)
+ .sort(([limitKeyA], [limitKeyB]) => {
+ return (
+ ascendingLimitKeys.indexOf(limitKeyA as IntervalLimitsKey) -
+ ascendingLimitKeys.indexOf(limitKeyB as IntervalLimitsKey)
+ );
+ })
+ .map(([key, value]) => {
+ const limitKey = key as IntervalLimitsKey;
+ return (
+ 1}
+ selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
+ (option) => !Object.keys(currentIntervalLimits).includes(option.value)
+ )}
+ onLimitChange={(intervalLimitKey, val) =>
+ // @ts-expect-error FIXME Fix these typings
+ setValue(`${propertyName}.${intervalLimitKey}`, val)
+ }
+ onDelete={(intervalLimitKey) => {
+ const current = currentIntervalLimits;
+ delete current[intervalLimitKey];
+ onChange(current);
+ }}
+ onIntervalSelect={(interval) => {
+ const current = currentIntervalLimits;
+ const currentValue = watchIntervalLimits[limitKey];
+
+ // Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
+ delete current[limitKey];
+ const newData = {
+ ...current,
+ // Set limit to new selected value (in the example above this means we set the limit to per_week here).
+ [interval?.value as IntervalLimitsKey]: currentValue,
+ };
+ onChange(newData);
+ }}
+ />
+ );
+ })}
+ {currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && !disabled && (
+
+ {t("add_limit")}
+
+ )}
+
+ );
+ }}
+ />
+ );
+};
+import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
+import { Clock, User } from "@calcom/ui/components/icon";
+
+function SkeletonLoader() {
+ return (
+
+
+
+
+ );
+}
+
+export default SkeletonLoader;
+
+function SkeletonItem() {
+ return (
+
+
+
+ );
+}
+import type { Webhook } from "@prisma/client";
+import { Webhook as TbWebhook } from "lucide-react";
+import { Trans } from "next-i18next";
+import Link from "next/link";
+import type { EventTypeSetupProps } from "pages/event-types/[type]";
+import { useState } from "react";
+
+import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
+import { WebhookForm } from "@calcom/features/webhooks/components";
+import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
+import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
+import { subscriberUrlReserved } from "@calcom/features/webhooks/lib/subscriberUrlReserved";
+import { APP_NAME } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
+import { Plus, Lock } from "@calcom/ui/components/icon";
+
+export const EventWebhooksTab = ({ eventType }: Pick) => {
+ const { t } = useLocale();
+
+ const utils = trpc.useContext();
+
+ const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ eventTypeId: eventType.id });
+
+ const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({
+ variant: "other",
+ onlyInstalled: true,
+ });
+
+ const [createModalOpen, setCreateModalOpen] = useState(false);
+ const [editModalOpen, setEditModalOpen] = useState(false);
+ const [webhookToEdit, setWebhookToEdit] = useState();
+
+ const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
+ async onSuccess() {
+ setEditModalOpen(false);
+ showToast(t("webhook_updated_successfully"), "success");
+ await utils.viewer.webhook.list.invalidate();
+ await utils.viewer.eventTypes.get.invalidate();
+ },
+ onError(error) {
+ showToast(`${error.message}`, "error");
+ },
+ });
+
+ const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
+ async onSuccess() {
+ setCreateModalOpen(false);
+ showToast(t("webhook_created_successfully"), "success");
+ await utils.viewer.webhook.list.invalidate();
+ await utils.viewer.eventTypes.get.invalidate();
+ },
+ onError(error) {
+ showToast(`${error.message}`, "error");
+ },
+ });
+
+ const onCreateWebhook = async (values: WebhookFormSubmitData) => {
+ if (
+ subscriberUrlReserved({
+ subscriberUrl: values.subscriberUrl,
+ id: values.id,
+ webhooks,
+ eventTypeId: eventType.id,
+ })
+ ) {
+ showToast(t("webhook_subscriber_url_reserved"), "error");
+ return;
+ }
+
+ if (!values.payloadTemplate) {
+ values.payloadTemplate = null;
+ }
+
+ createWebhookMutation.mutate({
+ subscriberUrl: values.subscriberUrl,
+ eventTriggers: values.eventTriggers,
+ active: values.active,
+ payloadTemplate: values.payloadTemplate,
+ secret: values.secret,
+ eventTypeId: eventType.id,
+ });
+ };
+
+ const NewWebhookButton = () => {
+ const { t } = useLocale();
+ return (
+ setCreateModalOpen(true)}>
+ {t("new_webhook")}
+
+ );
+ };
+
+ const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
+ eventType,
+ t("locked_fields_admin_description"),
+ t("locked_fields_member_description")
+ );
+ const webhookLockedStatus = shouldLockDisableProps("webhooks");
+
+ return (
+
+ {webhooks && !isLoading && (
+ <>
+
+
+ <>
+ {isManagedEventType && (
+
+ )}
+ {webhooks.length ? (
+ <>
+
+
{t("webhooks")}
+
+ {t("add_webhook_description", { appName: APP_NAME })}
+
+
+
+ {webhooks.map((webhook, index) => {
+ return (
+ {
+ setEditModalOpen(true);
+ setWebhookToEdit(webhook);
+ }}
+ />
+ );
+ })}
+
+
+
+
+ If you wish to edit or manage your web hooks, please head over to
+
+ webhooks settings
+
+
+
+
+ >
+ ) : (
+
+ {t("locked_by_admin")}
+
+ ) : (
+
+ )
+ }
+ />
+ )}
+ >
+
+
+
+ {/* New webhook dialog */}
+
!isOpen && setCreateModalOpen(false)}>
+
+ setCreateModalOpen(false)}
+ apps={installedApps?.items.map((app) => app.slug)}
+ />
+
+
+ {/* Edit webhook dialog */}
+
!isOpen && setEditModalOpen(false)}>
+
+ app.slug)}
+ onCancel={() => setEditModalOpen(false)}
+ onSubmit={(values: WebhookFormSubmitData) => {
+ if (
+ subscriberUrlReserved({
+ subscriberUrl: values.subscriberUrl,
+ id: webhookToEdit?.id,
+ webhooks,
+ eventTypeId: eventType.id,
+ })
+ ) {
+ showToast(t("webhook_subscriber_url_reserved"), "error");
+ return;
+ }
+
+ if (values.changeSecret) {
+ values.secret = values.newSecret.length ? values.newSecret : null;
+ }
+
+ if (!values.payloadTemplate) {
+ values.payloadTemplate = null;
+ }
+
+ editWebhookMutation.mutate({
+ id: webhookToEdit?.id || "",
+ subscriberUrl: values.subscriberUrl,
+ eventTriggers: values.eventTriggers,
+ active: values.active,
+ payloadTemplate: values.payloadTemplate,
+ secret: values.secret,
+ eventTypeId: webhookToEdit?.eventTypeId || undefined,
+ });
+ }}
+ />
+
+
+ >
+ )}
+
+ );
+};
+import { Trans } from "next-i18next";
+
+import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { ConfirmationDialogContent, Dialog } from "@calcom/ui";
+
+interface ManagedEventDialogProps {
+ slugExistsChildrenDialogOpen: ChildrenEventType[];
+ slug: string;
+ onOpenChange: () => void;
+ isPending: boolean;
+ onConfirm: (e: { preventDefault: () => void }) => void;
+}
+
+export default function ManagedEventDialog(props: ManagedEventDialogProps) {
+ const { t } = useLocale();
+ const { slugExistsChildrenDialogOpen, slug, onOpenChange, isPending, onConfirm } = props;
+
+ return (
+ 0} onOpenChange={onOpenChange}>
+
+
+ ch.owner.name)
+ .slice(0, -1)
+ .join(", ")} ${
+ slugExistsChildrenDialogOpen.length > 1 ? t("and") : ""
+ } ${slugExistsChildrenDialogOpen.map((ch) => ch.owner.name).slice(-1)}`,
+ slug,
+ }}
+ count={slugExistsChildrenDialogOpen.length}
+ />
+
{" "}
+ {t("managed_event_dialog_clarification")}
+
+
+ );
+}
+import type { Webhook } from "@prisma/client";
+import { useSession } from "next-auth/react";
+import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
+import { useState } from "react";
+import { useFormContext } from "react-hook-form";
+
+import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
+import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
+import { WebhookForm } from "@calcom/features/webhooks/components";
+import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
+import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
+import { subscriberUrlReserved } from "@calcom/features/webhooks/lib/subscriberUrlReserved";
+import { classNames } from "@calcom/lib";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { WebhookTriggerEvents } from "@calcom/prisma/enums";
+import { trpc } from "@calcom/trpc/react";
+import { Alert, Button, EmptyScreen, SettingsToggle, Dialog, DialogContent, showToast } from "@calcom/ui";
+import { PhoneCall, Plus, Lock, Webhook as TbWebhook } from "@calcom/ui/components/icon";
+
+type InstantEventControllerProps = {
+ eventType: EventTypeSetup;
+ paymentEnabled: boolean;
+ isTeamEvent: boolean;
+};
+
+export default function InstantEventController({
+ eventType,
+ paymentEnabled,
+ isTeamEvent,
+}: InstantEventControllerProps) {
+ const { t } = useLocale();
+ const session = useSession();
+ const [instantEventState, setInstantEventState] = useState(eventType?.isInstantEvent ?? false);
+ const formMethods = useFormContext();
+
+ const { shouldLockDisableProps } = useLockedFieldsManager(
+ eventType,
+ t("locked_fields_admin_description"),
+ t("locked_fields_member_description")
+ );
+
+ const instantLocked = shouldLockDisableProps("isInstantEvent");
+
+ const isOrg = !!session.data?.user?.org?.id;
+
+ if (session.status === "loading") return <>>;
+
+ return (
+
+
+ {!isOrg || !isTeamEvent ? (
+
{t("upgrade")}}
+ />
+ ) : (
+
+ {paymentEnabled ? (
+
+ ) : (
+ <>
+
+
{
+ if (!e) {
+ formMethods.setValue("isInstantEvent", false);
+ setInstantEventState(false);
+ } else {
+ formMethods.setValue("isInstantEvent", true);
+ setInstantEventState(true);
+ }
+ }}>
+
+ {instantEventState && }
+
+
+ >
+ )}
+
+ )}
+
+
+ );
+}
+
+const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) => {
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+ const { data: webhooks } = trpc.viewer.webhook.list.useQuery({
+ eventTypeId: eventType.id,
+ eventTriggers: [WebhookTriggerEvents.INSTANT_MEETING],
+ });
+ const { data: installedApps, isPending } = trpc.viewer.integrations.useQuery({
+ variant: "other",
+ onlyInstalled: true,
+ });
+
+ const [createModalOpen, setCreateModalOpen] = useState(false);
+ const [editModalOpen, setEditModalOpen] = useState(false);
+ const [webhookToEdit, setWebhookToEdit] = useState();
+
+ const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
+ async onSuccess() {
+ setEditModalOpen(false);
+ await utils.viewer.webhook.list.invalidate();
+ showToast(t("webhook_updated_successfully"), "success");
+ },
+ onError(error) {
+ showToast(`${error.message}`, "error");
+ },
+ });
+
+ const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
+ async onSuccess() {
+ showToast(t("webhook_created_successfully"), "success");
+ await utils.viewer.webhook.list.invalidate();
+ setCreateModalOpen(false);
+ },
+ onError(error) {
+ showToast(`${error.message}`, "error");
+ },
+ });
+
+ const onCreateWebhook = async (values: WebhookFormSubmitData) => {
+ if (
+ subscriberUrlReserved({
+ subscriberUrl: values.subscriberUrl,
+ id: values.id,
+ webhooks,
+ eventTypeId: eventType.id,
+ })
+ ) {
+ showToast(t("webhook_subscriber_url_reserved"), "error");
+ return;
+ }
+
+ if (!values.payloadTemplate) {
+ values.payloadTemplate = null;
+ }
+
+ createWebhookMutation.mutate({
+ subscriberUrl: values.subscriberUrl,
+ eventTriggers: values.eventTriggers,
+ active: values.active,
+ payloadTemplate: values.payloadTemplate,
+ secret: values.secret,
+ eventTypeId: eventType.id,
+ });
+ };
+
+ const NewWebhookButton = () => {
+ const { t } = useLocale();
+ return (
+ setCreateModalOpen(true)}>
+ {t("new_webhook")}
+
+ );
+ };
+
+ const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
+ eventType,
+ t("locked_fields_admin_description"),
+ t("locked_fields_member_description")
+ );
+ const webhookLockedStatus = shouldLockDisableProps("webhooks");
+
+ return (
+
+ {webhooks && !isPending && (
+ <>
+
+ {webhooks.length ? (
+ <>
+
+ {webhooks.map((webhook, index) => {
+ return (
+ {
+ setEditModalOpen(true);
+ setWebhookToEdit(webhook);
+ }}
+ />
+ );
+ })}
+
+
+ {t("warning_payment_instant_meeting_event")}
+
+ >
+ ) : (
+ <>
+
+ {t("warning_payment_instant_meeting_event")}
+
+
+ {t("locked_by_admin")}
+
+ ) : (
+
+ )
+ }
+ />
+ >
+ )}
+
+
+ {/* New webhook dialog */}
+
!isOpen && setCreateModalOpen(false)}>
+
+ setCreateModalOpen(false)}
+ apps={installedApps?.items.map((app) => app.slug)}
+ selectOnlyInstantMeetingOption={true}
+ />
+
+
+ {/* Edit webhook dialog */}
+
!isOpen && setEditModalOpen(false)}>
+
+ app.slug)}
+ onCancel={() => setEditModalOpen(false)}
+ onSubmit={(values: WebhookFormSubmitData) => {
+ if (
+ subscriberUrlReserved({
+ subscriberUrl: values.subscriberUrl,
+ id: webhookToEdit?.id,
+ webhooks,
+ eventTypeId: eventType.id,
+ })
+ ) {
+ showToast(t("webhook_subscriber_url_reserved"), "error");
+ return;
+ }
+
+ if (values.changeSecret) {
+ values.secret = values.newSecret.length ? values.newSecret : null;
+ }
+
+ if (!values.payloadTemplate) {
+ values.payloadTemplate = null;
+ }
+
+ editWebhookMutation.mutate({
+ id: webhookToEdit?.id || "",
+ subscriberUrl: values.subscriberUrl,
+ eventTriggers: values.eventTriggers,
+ active: values.active,
+ payloadTemplate: values.payloadTemplate,
+ secret: values.secret,
+ eventTypeId: webhookToEdit?.eventTypeId || undefined,
+ });
+ }}
+ />
+
+
+ >
+ )}
+
+ );
+};
+import { Trans } from "next-i18next";
+import Link from "next/link";
+import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
+import { useFormContext } from "react-hook-form";
+
+import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
+import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
+import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
+import type { EventTypeAppsList } from "@calcom/app-store/utils";
+import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Button, EmptyScreen, Alert } from "@calcom/ui";
+import { Grid, Lock } from "@calcom/ui/components/icon";
+
+export type EventType = Pick["eventType"] &
+ EventTypeAppCardComponentProps["eventType"];
+
+export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
+ const { t } = useLocale();
+ const { data: eventTypeApps, isPending } = trpc.viewer.integrations.useQuery({
+ extendsFeature: "EventType",
+ teamId: eventType.team?.id || eventType.parent?.teamId,
+ });
+
+ const formMethods = useFormContext();
+ const installedApps =
+ eventTypeApps?.items.filter((app) => app.userCredentialIds.length || app.teams.length) || [];
+ const notInstalledApps =
+ eventTypeApps?.items.filter((app) => !app.userCredentialIds.length && !app.teams.length) || [];
+ const allAppsData = formMethods.watch("metadata")?.apps || {};
+
+ const setAllAppsData = (_allAppsData: typeof allAppsData) => {
+ formMethods.setValue("metadata", {
+ ...formMethods.getValues("metadata"),
+ apps: _allAppsData,
+ });
+ };
+
+ const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => {
+ return function (key) {
+ const appData = allAppsData[appId as keyof typeof allAppsData] || {};
+ if (key) {
+ return appData[key as keyof typeof appData];
+ }
+ return appData;
+ };
+ };
+
+ const eventTypeFormMetadata = formMethods.getValues("metadata");
+
+ const getAppDataSetter = (
+ appId: EventTypeAppsList,
+ appCategories: string[],
+ credentialId?: number
+ ): SetAppData => {
+ return function (key, value) {
+ // Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
+ const allAppsDataFromForm = formMethods.getValues("metadata")?.apps || {};
+ const appData = allAppsDataFromForm[appId];
+ setAllAppsData({
+ ...allAppsDataFromForm,
+ [appId]: {
+ ...appData,
+ [key]: value,
+ credentialId,
+ appCategories,
+ },
+ });
+ };
+ };
+
+ const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
+ eventType,
+ t("locked_fields_admin_description"),
+ t("locked_fields_member_description")
+ );
+
+ const appsWithTeamCredentials = eventTypeApps?.items.filter((app) => app.teams.length) || [];
+ const cardsForAppsWithTeams = appsWithTeamCredentials.map((app) => {
+ const appCards = [];
+
+ if (app.userCredentialIds.length) {
+ appCards.push(
+
+ );
+ }
+
+ for (const team of app.teams) {
+ if (team) {
+ appCards.push(
+
+ );
+ }
+ }
+ return appCards;
+ });
+
+ return (
+ <>
+
+
+ {isManagedEventType && (
+
+ )}
+ {!isPending && !installedApps?.length ? (
+
+ {t("locked_by_admin")}
+
+ ) : (
+
+ {t("empty_installed_apps_button")}{" "}
+
+ )
+ }
+ />
+ ) : null}
+ {cardsForAppsWithTeams.map((apps) => apps.map((cards) => cards))}
+ {installedApps.map((app) => {
+ if (!app.teams.length)
+ return (
+
+ );
+ })}
+
+
+ {!shouldLockDisableProps("apps").disabled && (
+
+ {!isPending && notInstalledApps?.length ? (
+ <>
+
+ {t("available_apps_lower_case")}
+
+
+
+ View popular apps below and explore more in our
+
+ App Store
+
+
+
+ >
+ ) : null}
+
+ {notInstalledApps?.map((app) => (
+
+ ))}
+
+
+ )}
+ >
+ );
+};
+import * as RadioGroup from "@radix-ui/react-radio-group";
+import type { UnitTypeLongPlural } from "dayjs";
+import { Trans } from "next-i18next";
+import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
+import type { Dispatch, SetStateAction } from "react";
+import { useEffect, useState } from "react";
+import { Controller, useFormContext } from "react-hook-form";
+import type z from "zod";
+
+import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
+import { classNames } from "@calcom/lib";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
+import { Input, SettingsToggle, RadioField, Select } from "@calcom/ui";
+
+type RequiresConfirmationControllerProps = {
+ metadata: z.infer;
+ requiresConfirmation: boolean;
+ onRequiresConfirmation: Dispatch>;
+ seatsEnabled: boolean;
+ eventType: EventTypeSetup;
+};
+
+export default function RequiresConfirmationController({
+ metadata,
+ eventType,
+ requiresConfirmation,
+ onRequiresConfirmation,
+ seatsEnabled,
+}: RequiresConfirmationControllerProps) {
+ const { t } = useLocale();
+ const [requiresConfirmationSetup, setRequiresConfirmationSetup] = useState(
+ metadata?.requiresConfirmationThreshold
+ );
+ const defaultRequiresConfirmationSetup = { time: 30, unit: "minutes" as UnitTypeLongPlural };
+ const formMethods = useFormContext();
+
+ useEffect(() => {
+ if (!requiresConfirmation) {
+ formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [requiresConfirmation]);
+
+ const { shouldLockDisableProps } = useLockedFieldsManager(
+ eventType,
+ t("locked_fields_admin_description"),
+ t("locked_fields_member_description")
+ );
+ const requiresConfirmationLockedProps = shouldLockDisableProps("requiresConfirmation");
+
+ const options = [
+ { label: t("minute_timeUnit"), value: "minutes" },
+ { label: t("hour_timeUnit"), value: "hours" },
+ ];
+
+ const defaultValue = options.find(
+ (opt) =>
+ opt.value === (metadata?.requiresConfirmationThreshold?.unit ?? defaultRequiresConfirmationSetup.unit)
+ );
+
+ return (
+
+
+
(
+ {
+ formMethods.setValue("requiresConfirmation", val);
+ onRequiresConfirmation(val);
+ }}>
+
+
{
+ if (val === "always") {
+ formMethods.setValue("requiresConfirmation", true);
+ onRequiresConfirmation(true);
+ formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
+ setRequiresConfirmationSetup(undefined);
+ } else if (val === "notice") {
+ formMethods.setValue("requiresConfirmation", true);
+ onRequiresConfirmation(true);
+ formMethods.setValue(
+ "metadata.requiresConfirmationThreshold",
+ requiresConfirmationSetup || defaultRequiresConfirmationSetup
+ );
+ }
+ }}>
+
+ {(requiresConfirmationSetup === undefined ||
+ !requiresConfirmationLockedProps.disabled) && (
+
+ )}
+ {(requiresConfirmationSetup !== undefined ||
+ !requiresConfirmationLockedProps.disabled) && (
+
+
+ {
+ const val = Number(evt.target?.value);
+ setRequiresConfirmationSetup({
+ unit:
+ requiresConfirmationSetup?.unit ??
+ defaultRequiresConfirmationSetup.unit,
+ time: val,
+ });
+ formMethods.setValue(
+ "metadata.requiresConfirmationThreshold.time",
+ val
+ );
+ }}
+ className="border-default !m-0 block w-16 rounded-r-none border-r-0 text-sm [appearance:textfield] focus:z-10 focus:border-r"
+ defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
+ />
+
+ {
+ setRequiresConfirmationSetup({
+ time:
+ requiresConfirmationSetup?.time ??
+ defaultRequiresConfirmationSetup.time,
+ unit: opt?.value as UnitTypeLongPlural,
+ });
+ formMethods.setValue(
+ "metadata.requiresConfirmationThreshold.unit",
+ opt?.value as UnitTypeLongPlural
+ );
+ }}
+ defaultValue={defaultValue}
+ />
+
+
+ ),
+ }}
+ />
+ >
+ }
+ id="notice"
+ value="notice"
+ />
+ )}
+
+
+
+
+ )}
+ />
+
+
+ );
+}
+import type { EventTypeSetupProps } from "pages/event-types/[type]";
+
+import getPaymentAppData from "@calcom/lib/getPaymentAppData";
+
+import RecurringEventController from "./RecurringEventController";
+
+export const EventRecurringTab = ({ eventType }: Pick) => {
+ const paymentAppData = getPaymentAppData(eventType);
+
+ const requirePayment = paymentAppData.price > 0;
+
+ return ;
+};
+import type { FC } from "react";
+import type { SubmitHandler } from "react-hook-form";
+import { FormProvider } from "react-hook-form";
+import { useForm, useFormContext } from "react-hook-form";
+
+import type { EventNameObjectType } from "@calcom/core/event";
+import { getEventName } from "@calcom/core/event";
+import { validateCustomEventName } from "@calcom/core/event";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, Dialog, DialogClose, DialogFooter, DialogContent, TextField } from "@calcom/ui";
+
+interface FormValues {
+ customEventName: string;
+}
+
+interface CustomEventTypeModalFormProps {
+ placeHolder: string;
+ close: () => void;
+ setValue: (value: string) => void;
+ event: EventNameObjectType;
+ defaultValue: string;
+}
+
+const CustomEventTypeModalForm: FC = (props) => {
+ const { t } = useLocale();
+ const { placeHolder, close, setValue, event } = props;
+ const { register, handleSubmit, watch, getValues } = useFormContext();
+ const onSubmit: SubmitHandler = (data) => {
+ setValue(data.customEventName);
+ close();
+ };
+
+ // const customEventName = watch("customEventName");
+ const previewText = getEventName({ ...event, eventName: watch("customEventName") });
+ const placeHolder_ = watch("customEventName") === "" ? previewText : placeHolder;
+
+ return (
+
+ );
+};
+
+interface CustomEventTypeModalProps {
+ placeHolder: string;
+ defaultValue: string;
+ close: () => void;
+ setValue: (value: string) => void;
+ event: EventNameObjectType;
+}
+
+const CustomEventTypeModal: FC = (props) => {
+ const { t } = useLocale();
+
+ const { defaultValue, placeHolder, close, setValue, event } = props;
+
+ const methods = useForm({
+ defaultValues: {
+ customEventName: defaultValue,
+ },
+ });
+
+ return (
+
+
+
+
+
+
+ {t("cancel")}
+
+
+ {t("create")}
+
+
+
+
+ );
+};
+
+export default CustomEventTypeModal;
+import type { EventTypeSetupProps } from "pages/event-types/[type]";
+
+import getPaymentAppData from "@calcom/lib/getPaymentAppData";
+
+import InstantEventController from "./InstantEventController";
+
+export const EventInstantTab = ({
+ eventType,
+ isTeamEvent,
+}: Pick & { isTeamEvent: boolean }) => {
+ const paymentAppData = getPaymentAppData(eventType);
+
+ const requirePayment = paymentAppData.price > 0;
+
+ return (
+
+ );
+};
+import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
+import { useState } from "react";
+import { useFormContext } from "react-hook-form";
+
+import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
+import { classNames } from "@calcom/lib";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Frequency } from "@calcom/prisma/zod-utils";
+import type { RecurringEvent } from "@calcom/types/Calendar";
+import { Alert, Select, SettingsToggle, TextField } from "@calcom/ui";
+
+type RecurringEventControllerProps = {
+ eventType: EventTypeSetup;
+ paymentEnabled: boolean;
+};
+
+export default function RecurringEventController({
+ eventType,
+ paymentEnabled,
+}: RecurringEventControllerProps) {
+ const { t } = useLocale();
+ const formMethods = useFormContext();
+ const [recurringEventState, setRecurringEventState] = useState(
+ formMethods.getValues("recurringEvent")
+ );
+ /* Just yearly-0, monthly-1 and weekly-2 */
+ const recurringEventFreqOptions = Object.entries(Frequency)
+ .filter(([key, value]) => isNaN(Number(key)) && Number(value) < 3)
+ .map(([key, value]) => ({
+ label: t(`${key.toString().toLowerCase()}`, { count: recurringEventState?.interval }),
+ value: value.toString(),
+ }));
+
+ const { shouldLockDisableProps } = useLockedFieldsManager(
+ eventType,
+ t("locked_fields_admin_description"),
+ t("locked_fields_member_description")
+ );
+
+ const recurringLocked = shouldLockDisableProps("recurringEvent");
+
+ return (
+
+
+ {paymentEnabled ? (
+
+ ) : (
+ <>
+
+
{
+ if (!e) {
+ formMethods.setValue("recurringEvent", null);
+ setRecurringEventState(null);
+ } else {
+ const newVal = eventType.recurringEvent || {
+ interval: 1,
+ count: 12,
+ freq: Frequency.WEEKLY,
+ };
+ formMethods.setValue("recurringEvent", newVal);
+ setRecurringEventState(newVal);
+ }
+ }}>
+
+ {recurringEventState && (
+
+
+
{t("repeats_every")}
+
{
+ const newVal = {
+ ...recurringEventState,
+ interval: parseInt(event?.target.value),
+ };
+ formMethods.setValue("recurringEvent", newVal);
+ setRecurringEventState(newVal);
+ }}
+ />
+ {
+ const newVal = {
+ ...recurringEventState,
+ freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
+ };
+ formMethods.setValue("recurringEvent", newVal);
+ setRecurringEventState(newVal);
+ }}
+ />
+
+
+
{t("for_a_maximum_of")}
+
{
+ const newVal = {
+ ...recurringEventState,
+ count: parseInt(event?.target.value),
+ };
+ formMethods.setValue("recurringEvent", newVal);
+ setRecurringEventState(newVal);
+ }}
+ />
+
+ {t("events", {
+ count: recurringEventState.count,
+ })}
+
+
+
+ )}
+
+
+ >
+ )}
+
+
+ );
+}
+import { Webhook as TbWebhook } from "lucide-react";
+import type { TFunction } from "next-i18next";
+import { Trans } from "next-i18next";
+import { useRouter } from "next/navigation";
+import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
+import { useMemo, useState, Suspense } from "react";
+import type { UseFormReturn } from "react-hook-form";
+
+import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
+import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
+import Shell from "@calcom/features/shell/Shell";
+import { classNames } from "@calcom/lib";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { HttpError } from "@calcom/lib/http-error";
+import { SchedulingType } from "@calcom/prisma/enums";
+import { trpc, TRPCClientError } from "@calcom/trpc/react";
+import {
+ Button,
+ ButtonGroup,
+ ConfirmationDialogContent,
+ Dialog,
+ DropdownMenuSeparator,
+ Dropdown,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownItem,
+ DropdownMenuTrigger,
+ HorizontalTabs,
+ Label,
+ showToast,
+ Skeleton,
+ Switch,
+ Tooltip,
+ VerticalDivider,
+ VerticalTabs,
+} from "@calcom/ui";
+import {
+ Link as LinkIcon,
+ Calendar,
+ Clock,
+ Sliders,
+ Repeat,
+ Grid,
+ Zap,
+ Users,
+ ExternalLink,
+ Code,
+ Trash,
+ PhoneCall,
+ MoreHorizontal,
+ Loader,
+} from "@calcom/ui/components/icon";
+
+import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
+
+type Props = {
+ children: React.ReactNode;
+ eventType: EventTypeSetupProps["eventType"];
+ currentUserMembership: EventTypeSetupProps["currentUserMembership"];
+ team: EventTypeSetupProps["team"];
+ disableBorder?: boolean;
+ enabledAppsNumber: number;
+ installedAppsNumber: number;
+ enabledWorkflowsNumber: number;
+ formMethods: UseFormReturn;
+ isUpdateMutationLoading?: boolean;
+ availability?: AvailabilityOption;
+ isUserOrganizationAdmin: boolean;
+ bookerUrl: string;
+ activeWebhooksNumber: number;
+};
+
+type getNavigationProps = {
+ t: TFunction;
+ length: number;
+ id: number;
+ multipleDuration?: EventTypeSetupProps["eventType"]["metadata"]["multipleDuration"];
+ enabledAppsNumber: number;
+ enabledWorkflowsNumber: number;
+ installedAppsNumber: number;
+ availability: AvailabilityOption | undefined;
+};
+
+function getNavigation({
+ length,
+ id,
+ multipleDuration,
+ t,
+ enabledAppsNumber,
+ installedAppsNumber,
+ enabledWorkflowsNumber,
+}: getNavigationProps) {
+ const duration = multipleDuration?.map((duration) => ` ${duration}`) || length;
+
+ return [
+ {
+ name: "event_setup_tab_title",
+ href: `/event-types/${id}?tabName=setup`,
+ icon: LinkIcon,
+ info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
+ },
+ {
+ name: "event_limit_tab_title",
+ href: `/event-types/${id}?tabName=limits`,
+ icon: Clock,
+ info: `event_limit_tab_description`,
+ },
+ {
+ name: "event_advanced_tab_title",
+ href: `/event-types/${id}?tabName=advanced`,
+ icon: Sliders,
+ info: `event_advanced_tab_description`,
+ },
+ {
+ name: "recurring",
+ href: `/event-types/${id}?tabName=recurring`,
+ icon: Repeat,
+ info: `recurring_event_tab_description`,
+ },
+ {
+ name: "apps",
+ href: `/event-types/${id}?tabName=apps`,
+ icon: Grid,
+ //TODO: Handle proper translation with count handling
+ info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
+ },
+ {
+ name: "workflows",
+ href: `/event-types/${id}?tabName=workflows`,
+ icon: Zap,
+ info: `${enabledWorkflowsNumber} ${t("active")}`,
+ },
+ ];
+}
+
+function EventTypeSingleLayout({
+ children,
+ eventType,
+ currentUserMembership,
+ team,
+ disableBorder,
+ enabledAppsNumber,
+ installedAppsNumber,
+ enabledWorkflowsNumber,
+ isUpdateMutationLoading,
+ formMethods,
+ availability,
+ isUserOrganizationAdmin,
+ bookerUrl,
+ activeWebhooksNumber,
+}: Props) {
+ const utils = trpc.useContext();
+ const { t } = useLocale();
+ const router = useRouter();
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+
+ const hasPermsToDelete =
+ currentUserMembership?.role !== "MEMBER" ||
+ !currentUserMembership ||
+ formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
+ isUserOrganizationAdmin;
+
+ const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
+ onSuccess: async () => {
+ await utils.viewer.eventTypes.invalidate();
+ showToast(t("event_type_deleted_successfully"), "success");
+ router.push("/event-types");
+ setDeleteDialogOpen(false);
+ },
+ onError: (err) => {
+ if (err instanceof HttpError) {
+ const message = `${err.statusCode}: ${err.message}`;
+ showToast(message, "error");
+ setDeleteDialogOpen(false);
+ } else if (err instanceof TRPCClientError) {
+ showToast(err.message, "error");
+ }
+ },
+ });
+
+ const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
+ formMethods.getValues(),
+ t("locked_fields_admin_description"),
+ t("locked_fields_member_description")
+ );
+
+ const length = formMethods.watch("length");
+ const multipleDuration = formMethods.watch("metadata")?.multipleDuration;
+
+ const watchSchedulingType = formMethods.watch("schedulingType");
+ const watchChildrenCount = formMethods.watch("children").length;
+ // Define tab navigation here
+ const EventTypeTabs = useMemo(() => {
+ const navigation = getNavigation({
+ t,
+ length,
+ multipleDuration,
+ id: formMethods.getValues("id"),
+ enabledAppsNumber,
+ installedAppsNumber,
+ enabledWorkflowsNumber,
+ availability,
+ });
+
+ navigation.splice(1, 0, {
+ name: "availability",
+ href: `/event-types/${formMethods.getValues("id")}?tabName=availability`,
+ icon: Calendar,
+ info:
+ isManagedEventType || isChildrenManagedEventType
+ ? formMethods.getValues("schedule") === null
+ ? "members_default_schedule"
+ : isChildrenManagedEventType
+ ? `${
+ formMethods.getValues("scheduleName")
+ ? `${formMethods.getValues("scheduleName")} - ${t("managed")}`
+ : `default_schedule_name`
+ }`
+ : formMethods.getValues("scheduleName") ?? `default_schedule_name`
+ : formMethods.getValues("scheduleName") ?? `default_schedule_name`,
+ });
+ // If there is a team put this navigation item within the tabs
+ if (team) {
+ navigation.splice(2, 0, {
+ name: "assignment",
+ href: `/event-types/${formMethods.getValues("id")}?tabName=team`,
+ icon: Users,
+ info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
+ isManagedEventType ? ` - ${t("number_member", { count: watchChildrenCount || 0 })}` : ""
+ }`,
+ });
+ }
+ const showWebhooks = !(isManagedEventType || isChildrenManagedEventType);
+ if (showWebhooks) {
+ if (team) {
+ navigation.push({
+ name: "instant_tab_title",
+ href: `/event-types/${eventType.id}?tabName=instant`,
+ icon: PhoneCall,
+ info: `instant_event_tab_description`,
+ });
+ }
+ navigation.push({
+ name: "webhooks",
+ href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
+ icon: TbWebhook,
+ info: `${activeWebhooksNumber} ${t("active")}`,
+ });
+ }
+ return navigation;
+ }, [
+ t,
+ enabledAppsNumber,
+ installedAppsNumber,
+ enabledWorkflowsNumber,
+ availability,
+ isManagedEventType,
+ isChildrenManagedEventType,
+ team,
+ length,
+ multipleDuration,
+ formMethods.getValues("id"),
+ watchSchedulingType,
+ watchChildrenCount,
+ activeWebhooksNumber,
+ ]);
+
+ const permalink = `${bookerUrl}/${
+ team ? `${!team.parentId ? "team/" : ""}${team.slug}` : formMethods.getValues("users")[0].username
+ }/${eventType.slug}`;
+
+ const embedLink = `${
+ team ? `team/${team.slug}` : formMethods.getValues("users")[0].username
+ }/${formMethods.getValues("slug")}`;
+ const isManagedEvent = formMethods.getValues("schedulingType") === SchedulingType.MANAGED ? "_managed" : "";
+ // const title = formMethods.watch("title");
+ return (
+
+ {!formMethods.getValues("metadata")?.managedEventConfig && (
+ <>
+
+ {formMethods.watch("hidden") && (
+
+ {t("hidden")}
+
+ )}
+
+
+ {
+ formMethods.setValue("hidden", !e);
+ }}
+ />
+
+
+
+
+ >
+ )}
+
+ {/* TODO: Figure out why combined isnt working - works in storybook */}
+
+ {!isManagedEventType && (
+ <>
+ {/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
+
+
+
+
+ {
+ navigator.clipboard.writeText(permalink);
+ showToast("Link copied!", "success");
+ }}
+ />
+
+ >
+ )}
+ {!isChildrenManagedEventType && (
+ setDeleteDialogOpen(true)}
+ />
+ )}
+
+
+
+
+
+
+
+
+
+
+
+ {t("preview")}
+
+
+
+ {
+ navigator.clipboard.writeText(permalink);
+ showToast("Link copied!", "success");
+ }}>
+ {t("copy_link")}
+
+
+
+ setDeleteDialogOpen(true)}>
+ {t("delete")}
+
+
+
+
+
+ {formMethods.watch("hidden") ? t("show_eventtype_on_profile") : t("hide_from_profile")}
+
+ {
+ formMethods.setValue("hidden", !e);
+ }}
+ />
+
+
+
+
+
+ {t("save")}
+
+
+ }>
+ }>
+
+
+
+ {
+ e.preventDefault();
+ deleteMutation.mutate({ id: formMethods.getValues("id") });
+ }}>
+
+ , ul:
}}>
+
+ Members assigned to this event type will also have their event types deleted.
+
+ Anyone who they've shared their link with will no longer be able to book using it.
+
+
+
+
+
+
+
+
+ );
+}
+
+export { EventTypeSingleLayout };
+import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
+import { useState, memo, useEffect } from "react";
+import { Controller, useFormContext } from "react-hook-form";
+import type { OptionProps, SingleValueProps } from "react-select";
+import { components } from "react-select";
+
+import dayjs from "@calcom/dayjs";
+import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
+import classNames from "@calcom/lib/classNames";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { weekdayNames } from "@calcom/lib/weekday";
+import { SchedulingType } from "@calcom/prisma/enums";
+import { trpc } from "@calcom/trpc/react";
+import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
+import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
+import { ExternalLink, Globe } from "@calcom/ui/components/icon";
+
+import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
+
+export type AvailabilityOption = {
+ label: string;
+ value: number;
+ isDefault: boolean;
+ isManaged?: boolean;
+};
+
+const Option = ({ ...props }: OptionProps) => {
+ const { label, isDefault, isManaged = false } = props.data;
+ const { t } = useLocale();
+ return (
+
+ {label}
+ {isDefault && (
+
+ {t("default")}
+
+ )}
+ {isManaged && (
+
+ {t("managed")}
+
+ )}
+
+ );
+};
+
+const SingleValue = ({ ...props }: SingleValueProps) => {
+ const { label, isDefault, isManaged = false } = props.data;
+ const { t } = useLocale();
+ return (
+
+ {label}
+ {isDefault && (
+
+ {t("default")}
+
+ )}
+ {isManaged && (
+
+ {t("managed")}
+
+ )}
+
+ );
+};
+
+const format = (date: Date, hour12: boolean) =>
+ Intl.DateTimeFormat(undefined, {
+ hour: "numeric",
+ minute: "numeric",
+ hourCycle: hour12 ? "h12" : "h24",
+ }).format(new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss")));
+
+const EventTypeScheduleDetails = memo(
+ ({
+ isManagedEventType,
+ selectedScheduleValue,
+ }: {
+ isManagedEventType: boolean;
+ selectedScheduleValue: AvailabilityOption | undefined;
+ }) => {
+ const { data: loggedInUser } = useMeQuery();
+ const timeFormat = loggedInUser?.timeFormat;
+ const { t, i18n } = useLocale();
+ const { watch } = useFormContext();
+
+ const scheduleId = watch("schedule");
+ const { isPending, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
+ {
+ scheduleId:
+ scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
+ isManagedEventType,
+ },
+ { enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
+ );
+
+ const filterDays = (dayNum: number) =>
+ schedule?.schedule.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
+
+ return (
+
+
+
+ {weekdayNames(i18n.language, 1, "long").map((day, index) => {
+ const isAvailable = !!filterDays(index).length;
+ return (
+
+
+ {day}
+
+ {isPending ? (
+
+ ) : isAvailable ? (
+
+ {filterDays(index).map((dayRange, i) => (
+
+
+ {format(dayRange.startTime, timeFormat === 12)}
+
+
-
+
{format(dayRange.endTime, timeFormat === 12)}
+
+ ))}
+
+ ) : (
+ {t("unavailable")}
+ )}
+
+ );
+ })}
+
+
+
+
+
+ {schedule?.timeZone || }
+
+ {!!schedule?.id && !schedule.isManaged && !schedule.readOnly && (
+
+ {t("edit_availability")}
+
+ )}
+
+
+ );
+ }
+);
+
+EventTypeScheduleDetails.displayName = "EventTypeScheduleDetails";
+
+const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
+ const { t } = useLocale();
+ const { shouldLockIndicator, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
+ eventType,
+ t("locked_fields_admin_description"),
+ t("locked_fields_member_description")
+ );
+ const { watch, setValue, getValues } = useFormContext();
+ const watchSchedule = watch("schedule");
+ const [options, setOptions] = useState([]);
+
+ const { data, isPending } = trpc.viewer.availability.list.useQuery(undefined);
+
+ useEffect(
+ function refactorMeWithoutEffect() {
+ if (!data) {
+ return;
+ }
+ const schedules = data.schedules;
+
+ const options = schedules.map((schedule) => ({
+ value: schedule.id,
+ label: schedule.name,
+ isDefault: schedule.isDefault,
+ isManaged: false,
+ }));
+
+ // We are showing a managed event for a team admin, so adding the option to let members choose their schedule
+ if (isManagedEventType) {
+ options.push({
+ value: 0,
+ label: t("members_default_schedule"),
+ isDefault: false,
+ isManaged: false,
+ });
+ }
+
+ // We are showing a managed event for a member and team owner selected their own schedule, so adding
+ // the managed schedule option
+ if (
+ isChildrenManagedEventType &&
+ watchSchedule &&
+ !schedules.find((schedule) => schedule.id === watchSchedule)
+ ) {
+ options.push({
+ value: watchSchedule,
+ label: eventType.scheduleName ?? t("default_schedule_name"),
+ isDefault: false,
+ isManaged: false,
+ });
+ }
+ // We push the selected schedule from the event type if it's not part of the list response. This happens if the user is an admin but not the schedule owner.
+ else if (eventType.schedule && !schedules.find((schedule) => schedule.id === eventType.schedule)) {
+ options.push({
+ value: eventType.schedule,
+ label: eventType.scheduleName ?? t("default_schedule_name"),
+ isDefault: false,
+ isManaged: false,
+ });
+ }
+
+ setOptions(options);
+
+ const scheduleId = getValues("schedule");
+ const value = options.find((option) =>
+ scheduleId
+ ? option.value === scheduleId
+ : isManagedEventType
+ ? option.value === 0
+ : option.value === schedules.find((schedule) => schedule.isDefault)?.id
+ );
+
+ setValue("availability", value);
+ },
+ [data]
+ );
+ const availabilityValue = watch("availability");
+
+ useEffect(() => {
+ if (!availabilityValue?.value) return;
+ setValue("schedule", availabilityValue.value);
+ }, [availabilityValue, setValue]);
+
+ return (
+
+
+
+ {t("availability")}
+ {shouldLockIndicator("availability")}
+
+ {isPending && }
+ {!isPending && (
+ {
+ return (
+ {
+ field.onChange(selected?.value || null);
+ if (selected?.value) setValue("availability", selected);
+ }}
+ className="block w-full min-w-0 flex-1 rounded-sm text-sm"
+ value={availabilityValue}
+ components={{ Option, SingleValue }}
+ isMulti={false}
+ />
+ );
+ }}
+ />
+ )}
+
+ {availabilityValue?.value !== 0 ? (
+
+ ) : (
+ isManagedEventType && (
+
{t("members_default_schedule_description")}
+ )
+ )}
+
+ );
+};
+
+const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSetup }) => {
+ const { t } = useLocale();
+ const { setValue } = useFormContext();
+ return (
+ (
+ {
+ onChange(!checked);
+ if (!checked) {
+ setValue("schedule", null);
+ }
+ }}
+ title={t("choose_common_schedule_team_event")}
+ description={t("choose_common_schedule_team_event_description")}>
+
+
+ )}
+ />
+ );
+};
+
+export const EventAvailabilityTab = ({
+ eventType,
+ isTeamEvent,
+}: {
+ eventType: EventTypeSetup;
+ isTeamEvent: boolean;
+}) => {
+ return isTeamEvent && eventType.schedulingType !== SchedulingType.MANAGED ? (
+
+ ) : (
+
+ );
+};
+import { Trans } from "next-i18next";
+import Link from "next/link";
+import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
+import { useEffect, useRef, useState } from "react";
+import type { ComponentProps, Dispatch, SetStateAction } from "react";
+import { Controller, useFormContext, useWatch } from "react-hook-form";
+import type { Options } from "react-select";
+
+import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
+import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
+import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { SchedulingType } from "@calcom/prisma/enums";
+import { Label, Select, SettingsToggle } from "@calcom/ui";
+
+interface IUserToValue {
+ id: number | null;
+ name: string | null;
+ username: string | null;
+ avatar: string;
+ email: string;
+}
+
+const mapUserToValue = ({ id, name, username, avatar, email }: IUserToValue, pendingString: string) => ({
+ value: `${id || ""}`,
+ label: `${name || email || ""}${!username ? ` (${pendingString})` : ""}`,
+ avatar,
+ email,
+});
+
+export const mapMemberToChildrenOption = (
+ member: EventTypeSetupProps["teamMembers"][number],
+ slug: string,
+ pendingString: string
+) => {
+ return {
+ slug,
+ hidden: false,
+ created: false,
+ owner: {
+ id: member.id,
+ name: member.name ?? "",
+ email: member.email,
+ username: member.username ?? "",
+ membership: member.membership,
+ eventTypeSlugs: member.eventTypes ?? [],
+ avatar: member.avatar,
+ },
+ value: `${member.id ?? ""}`,
+ label: `${member.name || member.email || ""}${!member.username ? ` (${pendingString})` : ""}`,
+ };
+};
+
+const sortByLabel = (a: ReturnType, b: ReturnType) => {
+ if (a.label < b.label) {
+ return -1;
+ }
+ if (a.label > b.label) {
+ return 1;
+ }
+ return 0;
+};
+
+const ChildrenEventTypesList = ({
+ options = [],
+ value,
+ onChange,
+ ...rest
+}: {
+ value: ReturnType[];
+ onChange?: (options: ReturnType[]) => void;
+ options?: Options>;
+} & Omit>, "onChange" | "value">) => {
+ const { t } = useLocale();
+ return (
+
+
+ {t("assign_to")}
+ {
+ onChange &&
+ onChange(
+ options.map((option) => ({
+ ...option,
+ }))
+ );
+ }}
+ value={value}
+ options={options.filter((opt) => !value.find((val) => val.owner.id.toString() === opt.value))}
+ controlShouldRenderValue={false}
+ {...rest}
+ />
+
+
+ );
+};
+
+const AssignAllTeamMembers = ({
+ assignAllTeamMembers,
+ setAssignAllTeamMembers,
+ onActive,
+ onInactive,
+}: {
+ assignAllTeamMembers: boolean;
+ setAssignAllTeamMembers: Dispatch>;
+ onActive: () => void;
+ onInactive?: () => void;
+}) => {
+ const { t } = useLocale();
+ const formMethods = useFormContext();
+
+ return (
+
+ name="assignAllTeamMembers"
+ render={() => (
+ {
+ formMethods.setValue("assignAllTeamMembers", active);
+ setAssignAllTeamMembers(active);
+ if (active) {
+ onActive();
+ } else if (!!onInactive) {
+ onInactive();
+ }
+ }}
+ toggleSwitchAtTheEnd
+ />
+ )}
+ />
+ );
+};
+
+const CheckedHostField = ({
+ labelText,
+ placeholder,
+ options = [],
+ isFixed,
+ value,
+ onChange,
+ helperText,
+ ...rest
+}: {
+ labelText?: string;
+ placeholder: string;
+ isFixed: boolean;
+ value: { isFixed: boolean; userId: number }[];
+ onChange?: (options: { isFixed: boolean; userId: number }[]) => void;
+ options?: Options;
+ helperText?: React.ReactNode | string;
+} & Omit>, "onChange" | "value">) => {
+ return (
+
+
+ {labelText ?
{labelText} : <>>}
+
!!value.find((host) => host.userId.toString() === option.value)}
+ onChange={(options) => {
+ onChange &&
+ onChange(
+ options.map((option) => ({
+ isFixed,
+ userId: parseInt(option.value, 10),
+ }))
+ );
+ }}
+ value={(value || [])
+ .filter(({ isFixed: _isFixed }) => isFixed === _isFixed)
+ .map(
+ (host) =>
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ options.find((member) => member.value === host.userId.toString())!
+ )
+ .filter(Boolean)}
+ controlShouldRenderValue={false}
+ options={options}
+ placeholder={placeholder}
+ {...rest}
+ />
+ {helperText && {helperText}
}
+
+
+ );
+};
+
+const FixedHostHelper = (
+
+ Add anyone who needs to attend the event.
+
+ Learn more
+
+
+);
+
+const RoundRobinHosts = ({
+ teamMembers,
+ value,
+ onChange,
+ assignAllTeamMembers,
+ setAssignAllTeamMembers,
+}: {
+ value: { isFixed: boolean; userId: number }[];
+ onChange: (hosts: { isFixed: boolean; userId: number }[]) => void;
+ teamMembers: {
+ value: string;
+ label: string;
+ avatar: string;
+ email: string;
+ }[];
+ assignAllTeamMembers: boolean;
+ setAssignAllTeamMembers: Dispatch>;
+}) => {
+ const { t } = useLocale();
+ const formMethods = useFormContext();
+
+ return (
+
+
+
{
+ onChange([...value.filter(({ isFixed }) => !isFixed), ...changeValue]);
+ }}
+ value={value}
+ placeholder={t("add_fixed_hosts")}
+ labelText={t("fixed_hosts")}
+ helperText={FixedHostHelper}
+ />
+
+
+
{t("round_robin_hosts")}
+
+
+ formMethods.setValue(
+ "hosts",
+ teamMembers.map((teamMember) => ({
+ isFixed: false,
+ userId: parseInt(teamMember.value, 10),
+ }))
+ )
+ }
+ onInactive={() => formMethods.setValue("hosts", [])}
+ />
+
+ {assignAllTeamMembers ? (
+
+ ) : (
+
+ onChange([...value.filter(({ isFixed }) => isFixed), ...changeValue])
+ }
+ value={value}
+ isFixed={false}
+ placeholder={t("add_attendees")}
+ helperText={t("round_robin_helper")}
+ />
+ )}
+
+
+
+ );
+};
+
+const ChildrenEventTypes = ({
+ childrenEventTypeOptions,
+ assignAllTeamMembers,
+ setAssignAllTeamMembers,
+}: {
+ childrenEventTypeOptions: ReturnType[];
+ assignAllTeamMembers: boolean;
+ setAssignAllTeamMembers: Dispatch>;
+}) => {
+ const formMethods = useFormContext();
+ return (
+
+
+
formMethods.setValue("children", childrenEventTypeOptions)}
+ />
+ {!assignAllTeamMembers ? (
+
+ name="children"
+ render={({ field: { onChange, value } }) => (
+
+ )}
+ />
+ ) : (
+ <>>
+ )}
+
+
+ );
+};
+
+const Hosts = ({
+ teamMembers,
+ assignAllTeamMembers,
+ setAssignAllTeamMembers,
+}: {
+ teamMembers: {
+ value: string;
+ label: string;
+ avatar: string;
+ email: string;
+ }[];
+ assignAllTeamMembers: boolean;
+ setAssignAllTeamMembers: Dispatch>;
+}) => {
+ const { t } = useLocale();
+ const {
+ control,
+ resetField,
+ getValues,
+ formState: { submitCount },
+ } = useFormContext();
+ const schedulingType = useWatch({
+ control,
+ name: "schedulingType",
+ });
+ const initialValue = useRef<{
+ hosts: FormValues["hosts"];
+ schedulingType: SchedulingType | null;
+ submitCount: number;
+ } | null>(null);
+
+ useEffect(() => {
+ // Handles init & out of date initial value after submission.
+ if (!initialValue.current || initialValue.current?.submitCount !== submitCount) {
+ initialValue.current = { hosts: getValues("hosts"), schedulingType, submitCount };
+ return;
+ }
+ resetField("hosts", {
+ defaultValue: initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
+ });
+ }, [schedulingType, resetField, getValues, submitCount]);
+
+ const formMethods = useFormContext();
+ return (
+
+ name="hosts"
+ render={({ field: { onChange, value } }) => {
+ const schedulingTypeRender = {
+ COLLECTIVE: (
+
+
+
+ formMethods.setValue(
+ "hosts",
+ teamMembers.map((teamMember) => ({
+ isFixed: true,
+ userId: parseInt(teamMember.value, 10),
+ }))
+ )
+ }
+ onInactive={() => formMethods.setValue("hosts", [])}
+ />
+ {assignAllTeamMembers ? (
+ <>>
+ ) : (
+
+ )}
+
+
+ ),
+ ROUND_ROBIN: (
+ <>
+
+ {/*{t("hosts")}>}
+ />*/}
+ >
+ ),
+ MANAGED: <>>,
+ };
+ return !!schedulingType ? schedulingTypeRender[schedulingType] : <>>;
+ }}
+ />
+ );
+};
+
+export const EventTeamTab = ({
+ team,
+ teamMembers,
+ eventType,
+}: Pick) => {
+ const { t } = useLocale();
+
+ const schedulingTypeOptions: {
+ value: SchedulingType;
+ label: string;
+ // description: string;
+ }[] = [
+ {
+ value: "COLLECTIVE",
+ label: t("collective"),
+ // description: t("collective_description"),
+ },
+ {
+ value: "ROUND_ROBIN",
+ label: t("round_robin"),
+ // description: t("round_robin_description"),
+ },
+ ];
+ const pendingMembers = (member: (typeof teamMembers)[number]) =>
+ !!eventType.team?.parentId || !!member.username;
+ const teamMembersOptions = teamMembers
+ .filter(pendingMembers)
+ .map((member) => mapUserToValue(member, t("pending")));
+ const childrenEventTypeOptions = teamMembers.filter(pendingMembers).map((member) => {
+ return mapMemberToChildrenOption(
+ { ...member, eventTypes: member.eventTypes.filter((et) => et !== eventType.slug) },
+ eventType.slug,
+ t("pending")
+ );
+ });
+ const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
+ const formMethods = useFormContext();
+ const [assignAllTeamMembers, setAssignAllTeamMembers] = useState(
+ formMethods.getValues("assignAllTeamMembers") ?? false
+ );
+
+ return (
+
+ {team && !isManagedEventType && (
+
+
+ {t("scheduling_type")}
+
+ name="schedulingType"
+ render={({ field: { value, onChange } }) => (
+ opt.value === value)}
+ className="w-full"
+ onChange={(val) => {
+ onChange(val?.value);
+ formMethods.setValue("assignAllTeamMembers", false);
+ setAssignAllTeamMembers(false);
+ }}
+ />
+ )}
+ />
+
+
+
+ )}
+ {team && isManagedEventType && (
+
+ )}
+
+ );
+};
+import { useAutoAnimate } from "@formkit/auto-animate/react";
+import { ErrorMessage } from "@hookform/error-message";
+import { Trans } from "next-i18next";
+import Link from "next/link";
+import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
+import { useEffect, useState } from "react";
+import { Controller, useFormContext, useFieldArray } from "react-hook-form";
+import type { MultiValue } from "react-select";
+
+import type { EventLocationType } from "@calcom/app-store/locations";
+import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
+import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
+import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
+import { CAL_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { md } from "@calcom/lib/markdownIt";
+import { slugify } from "@calcom/lib/slugify";
+import turndown from "@calcom/lib/turndownService";
+import {
+ Label,
+ Select,
+ SettingsToggle,
+ Skeleton,
+ TextField,
+ Editor,
+ SkeletonContainer,
+ SkeletonText,
+ Input,
+ PhoneInput,
+ Button,
+ showToast,
+} from "@calcom/ui";
+import { Plus, X, Check, CornerDownRight } from "@calcom/ui/components/icon";
+
+import CheckboxField from "@components/ui/form/CheckboxField";
+import type { SingleValueLocationOption } from "@components/ui/form/LocationSelect";
+import LocationSelect from "@components/ui/form/LocationSelect";
+
+const getLocationFromType = (
+ type: EventLocationType["type"],
+ locationOptions: Pick["locationOptions"]
+) => {
+ for (const locationOption of locationOptions) {
+ const option = locationOption.options.find((option) => option.value === type);
+ if (option) {
+ return option;
+ }
+ }
+};
+
+const getLocationInfo = ({
+ eventType,
+ locationOptions,
+}: Pick) => {
+ const locationAvailable =
+ eventType.locations &&
+ eventType.locations.length > 0 &&
+ locationOptions.some((op) => op.options.find((opt) => opt.value === eventType.locations[0].type));
+ const locationDetails = eventType.locations &&
+ eventType.locations.length > 0 &&
+ !locationAvailable && {
+ slug: eventType.locations[0].type.replace("integrations:", "").replace(":", "-").replace("_video", ""),
+ name: eventType.locations[0].type
+ .replace("integrations:", "")
+ .replace(":", " ")
+ .replace("_video", "")
+ .split(" ")
+ .map((word) => word[0].toUpperCase() + word.slice(1))
+ .join(" "),
+ };
+ return { locationAvailable, locationDetails };
+};
+
+const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
+ const formMethods = useFormContext();
+ const [mounted, setIsMounted] = useState(false);
+ const { t } = useLocale();
+ const [firstRender, setFirstRender] = useState(true);
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ return mounted ? (
+ md.render(formMethods.getValues("description") || "")}
+ setText={(value: string) => formMethods.setValue("description", turndown(value))}
+ excludedToolbarItems={["blockType"]}
+ placeholder={t("quick_video_meeting")}
+ editable={isEditable}
+ firstRender={firstRender}
+ setFirstRender={setFirstRender}
+ />
+ ) : (
+
+
+
+ );
+};
+
+export const EventSetupTab = (
+ props: Pick<
+ EventTypeSetupProps,
+ "eventType" | "locationOptions" | "team" | "teamMembers" | "destinationCalendar"
+ >
+) => {
+ const { t } = useLocale();
+ const formMethods = useFormContext();
+ const { eventType, team, destinationCalendar } = props;
+ const [multipleDuration, setMultipleDuration] = useState(
+ formMethods.getValues("metadata")?.multipleDuration
+ );
+ const orgBranding = useOrgBranding();
+ const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
+
+ const locationOptions = props.locationOptions.map((locationOption) => {
+ const options = locationOption.options.filter((option) => {
+ // Skip "Organizer's Default App" for non-team members
+ return !team ? option.label !== t("organizer_default_conferencing_app") : true;
+ });
+
+ return {
+ ...locationOption,
+ options,
+ };
+ });
+
+ const multipleDurationOptions = [
+ 5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 150, 180, 240, 480,
+ ].map((mins) => ({
+ value: mins,
+ label: t("multiple_duration_mins", { count: mins }),
+ }));
+
+ const [selectedMultipleDuration, setSelectedMultipleDuration] = useState<
+ MultiValue<{
+ value: number;
+ label: string;
+ }>
+ >(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
+ const [defaultDuration, setDefaultDuration] = useState(
+ selectedMultipleDuration.find((opt) => opt.value === formMethods.getValues("length")) ?? null
+ );
+
+ const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
+ useLockedFieldsManager(
+ formMethods.getValues(),
+ t("locked_fields_admin_description"),
+ t("locked_fields_member_description")
+ );
+
+ const Locations = () => {
+ const { t } = useLocale();
+ const {
+ fields: locationFields,
+ append,
+ remove,
+ update: updateLocationField,
+ } = useFieldArray({
+ control: formMethods.control,
+ name: "locations",
+ });
+
+ const [animationRef] = useAutoAnimate();
+
+ const validLocations = formMethods.getValues("locations").filter((location) => {
+ const eventLocation = getEventLocationType(location.type);
+ if (!eventLocation) {
+ // It's possible that the location app in use got uninstalled.
+ return false;
+ }
+ return true;
+ });
+
+ const defaultValue = isManagedEventType
+ ? locationOptions.find((op) => op.label === t("default"))?.options[0]
+ : undefined;
+
+ const { locationDetails, locationAvailable } = getLocationInfo(props);
+
+ const LocationInput = (props: {
+ eventLocationType: EventLocationType;
+ defaultValue?: string;
+ index: number;
+ }) => {
+ const { eventLocationType, index, ...remainingProps } = props;
+
+ if (eventLocationType?.organizerInputType === "text") {
+ const { defaultValue, ...rest } = remainingProps;
+
+ return (
+ {
+ return (
+
+ );
+ }}
+ />
+ );
+ } else if (eventLocationType?.organizerInputType === "phone") {
+ const { defaultValue, ...rest } = remainingProps;
+
+ return (
+ {
+ return (
+
+ );
+ }}
+ />
+ );
+ }
+ return null;
+ };
+
+ const [showEmptyLocationSelect, setShowEmptyLocationSelect] = useState(false);
+ const [selectedNewOption, setSelectedNewOption] = useState(null);
+
+ return (
+
+
+ {locationFields.map((field, index) => {
+ const eventLocationType = getEventLocationType(field.type);
+ const defaultLocation = field;
+
+ const option = getLocationFromType(field.type, locationOptions);
+
+ return (
+
+
+
{
+ if (e?.value) {
+ const newLocationType = e.value;
+ const eventLocationType = getEventLocationType(newLocationType);
+ if (!eventLocationType) {
+ return;
+ }
+ const canAddLocation =
+ eventLocationType.organizerInputType ||
+ !validLocations.find((location) => location.type === newLocationType);
+
+ if (canAddLocation) {
+ updateLocationField(index, {
+ type: newLocationType,
+ ...(e.credentialId && {
+ credentialId: e.credentialId,
+ teamName: e.teamName,
+ }),
+ });
+ } else {
+ updateLocationField(index, {
+ type: field.type,
+ ...(field.credentialId && {
+ credentialId: field.credentialId,
+ teamName: field.teamName,
+ }),
+ });
+ showToast(t("location_already_exists"), "warning");
+ }
+ }
+ }}
+ />
+ remove(index)}
+ aria-label={t("remove")}>
+
+
+
+
+
+
+ {eventLocationType?.organizerInputType && (
+
+
+
+ {
+ const fieldValues = formMethods.getValues("locations")[index];
+ updateLocationField(index, {
+ ...fieldValues,
+ displayLocationPublicly: e.target.checked,
+ });
+ }}
+ informationIconText={t("display_location_info_badge")}
+ />
+
+
+ )}
+
+ );
+ })}
+ {(validLocations.length === 0 || showEmptyLocationSelect) && (
+
+ {
+ if (e?.value) {
+ const newLocationType = e.value;
+ const eventLocationType = getEventLocationType(newLocationType);
+ if (!eventLocationType) {
+ return;
+ }
+
+ const canAppendLocation =
+ eventLocationType.organizerInputType ||
+ !validLocations.find((location) => location.type === newLocationType);
+
+ if (canAppendLocation) {
+ append({
+ type: newLocationType,
+ ...(e.credentialId && {
+ credentialId: e.credentialId,
+ teamName: e.teamName,
+ }),
+ });
+ setSelectedNewOption(e);
+ } else {
+ showToast(t("location_already_exists"), "warning");
+ setSelectedNewOption(null);
+ }
+ }
+ }}
+ />
+
+ )}
+ {validLocations.some(
+ (location) =>
+ location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
+ ) && (
+
+
+
+
+
+
+ The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
+ Change it{" "}
+
+ here.
+ {" "}
+
+
+
+ )}
+ {isChildrenManagedEventType && !locationAvailable && locationDetails && (
+
+ {t("app_not_connected", { appName: locationDetails.name })}{" "}
+
+ {t("connect_now")}
+
+
+ )}
+ {validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && (
+
+ setShowEmptyLocationSelect(true)}>
+ {t("add_location")}
+
+
+ )}
+
+
+
+ Can't find the right video app? Visit our
+
+ App Store
+
+ .
+
+
+
+ );
+ };
+
+ const lengthLockedProps = shouldLockDisableProps("length");
+ const descriptionLockedProps = shouldLockDisableProps("description");
+ const urlPrefix = orgBranding
+ ? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
+ : `${CAL_URL?.replace(/^(https?:|)\/\//, "")}`;
+
+ return (
+
+
+
+
+
+
+ {t("description")}
+ {shouldLockIndicator("description")}
+
+
+
+
+ {urlPrefix}/
+ {!isManagedEventType
+ ? team
+ ? (orgBranding ? "" : "team/") + team.slug
+ : formMethods.getValues("users")[0].username
+ : t("username_placeholder")}
+ /
+ >
+ }
+ {...formMethods.register("slug", {
+ setValueAs: (v) => slugify(v),
+ })}
+ />
+
+
+ {multipleDuration ? (
+
+
+
+ {t("available_durations")}
+
+ {
+ let newOptions = [...options];
+ newOptions = newOptions.sort((a, b) => {
+ return a?.value - b?.value;
+ });
+ const values = newOptions.map((opt) => opt.value);
+ setMultipleDuration(values);
+ setSelectedMultipleDuration(newOptions);
+ if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
+ if (newOptions.length > 0) {
+ setDefaultDuration(newOptions[0]);
+ formMethods.setValue("length", newOptions[0].value);
+ } else {
+ setDefaultDuration(null);
+ }
+ }
+ if (newOptions.length === 1 && defaultDuration === null) {
+ setDefaultDuration(newOptions[0]);
+ formMethods.setValue("length", newOptions[0].value);
+ }
+ formMethods.setValue("metadata.multipleDuration", values);
+ }}
+ />
+
+
+
+ {t("default_duration")}
+ {shouldLockIndicator("length")}
+
+ t("default_duration_no_options")}
+ options={selectedMultipleDuration}
+ onChange={(option) => {
+ setDefaultDuration(
+ selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
+ );
+ if (option) formMethods.setValue("length", option.value);
+ }}
+ />
+
+
+ ) : (
+
{t("minutes")}>}
+ min={1}
+ />
+ )}
+ {!lengthLockedProps.disabled && (
+
+ {
+ if (multipleDuration !== undefined) {
+ setMultipleDuration(undefined);
+ setSelectedMultipleDuration([]);
+ setDefaultDuration(null);
+ formMethods.setValue("metadata.multipleDuration", undefined);
+ formMethods.setValue("length", eventType.length);
+ } else {
+ setMultipleDuration([]);
+ formMethods.setValue("metadata.multipleDuration", []);
+ formMethods.setValue("length", 0);
+ }
+ }}
+ />
+
+ )}
+
+
+
+
+
+ {t("location")}
+ {shouldLockIndicator("locations")}
+
+
+ } />
+
+
+
+
+ );
+};
+import React from "react";
+
+import { HttpError } from "@calcom/lib/http-error";
+
+type Props = {
+ statusCode?: number | null;
+ error?: Error | HttpError | null;
+ message?: string;
+ /** Display debugging information */
+ displayDebug?: boolean;
+ children?: never;
+};
+
+const defaultProps = {
+ displayDebug: false,
+};
+
+const ErrorDebugPanel: React.FC<{ error: Props["error"]; children?: never }> = (props) => {
+ const { error: e } = props;
+
+ const debugMap = [
+ ["error.message", e?.message],
+ ["error.name", e?.name],
+ ["error.class", e instanceof Error ? e.constructor.name : undefined],
+ ["http.url", e instanceof HttpError ? e.url : undefined],
+ ["http.status", e instanceof HttpError ? e.statusCode : undefined],
+ ["http.cause", e instanceof HttpError ? e.cause?.message : undefined],
+ ["error.stack", e instanceof Error ? e.stack : undefined],
+ ];
+
+ return (
+
+
+
+ {debugMap.map(([key, value]) => {
+ if (value !== undefined) {
+ return (
+
+
{key}
+ {value}
+
+ );
+ }
+ })}
+
+
+
+ );
+};
+
+export const ErrorPage: React.FC = (props) => {
+ const { message, statusCode, error, displayDebug } = { ...defaultProps, ...props };
+
+ return (
+ <>
+
+
+
+
{statusCode}
+
+ {message}
+
+
+
+ {displayDebug && (
+
+
+
+ )}
+
+ >
+ );
+};
+import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
+import Shell from "@calcom/features/shell/Shell";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { HeadSeo } from "@calcom/ui";
+
+import type { AppPageProps } from "./AppPage";
+import { AppPage } from "./AppPage";
+
+const ShellHeading = () => {
+ const { t } = useLocale();
+ return {t("app_store")} ;
+};
+
+export default function WrappedApp(props: AppPageProps) {
+ return (
+ } backPath="/apps" withoutSeo>
+
+ {props.licenseRequired ? (
+
+
+
+ ) : (
+
+ )}
+
+ );
+}
+import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
+import { doesAppSupportTeamInstall } from "@calcom/app-store/utils";
+import { Spinner } from "@calcom/features/calendars/weeklyview/components/spinner/Spinner";
+import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
+import { CAL_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import type { AppFrontendPayload } from "@calcom/types/App";
+import type { ButtonProps } from "@calcom/ui";
+import {
+ Avatar,
+ Button,
+ Dropdown,
+ DropdownItem,
+ DropdownMenuContent,
+ DropdownMenuLabel,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ showToast,
+} from "@calcom/ui";
+
+export const InstallAppButtonChild = ({
+ userAdminTeams,
+ addAppMutationInput,
+ appCategories,
+ multiInstall,
+ credentials,
+ concurrentMeetings,
+ paid,
+ ...props
+}: {
+ userAdminTeams?: UserAdminTeams;
+ addAppMutationInput: { type: AppFrontendPayload["type"]; variant: string; slug: string };
+ appCategories: string[];
+ multiInstall?: boolean;
+ credentials?: RouterOutputs["viewer"]["appCredentialsByType"]["credentials"];
+ concurrentMeetings?: boolean;
+ paid?: AppFrontendPayload["paid"];
+} & ButtonProps) => {
+ const { t } = useLocale();
+
+ const mutation = useAddAppMutation(null, {
+ onSuccess: (data) => {
+ if (data?.setupPending) return;
+ showToast(t("app_successfully_installed"), "success");
+ },
+ onError: (error) => {
+ if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
+ },
+ });
+ const shouldDisableInstallation = !multiInstall ? !!(credentials && credentials.length) : false;
+
+ // Paid apps don't support team installs at the moment
+ // Also, cal.ai(the only paid app at the moment) doesn't support team install either
+ if (paid) {
+ return (
+
+ {paid.trial ? t("start_paid_trial") : t("subscribe")}
+
+ );
+ }
+
+ if (
+ !userAdminTeams?.length ||
+ !doesAppSupportTeamInstall({ appCategories, concurrentMeetings, isPaid: !!paid })
+ ) {
+ return (
+
+ {multiInstall ? t("install_another") : t("install_app")}
+
+ );
+ }
+
+ return (
+
+
+
+ {multiInstall ? t("install_another") : t("install_app")}
+
+
+
+ {
+ if (mutation.isPending) event.preventDefault();
+ }}>
+ {mutation.isPending && (
+
+
+
+ )}
+ {t("install_app_on")}
+ {userAdminTeams.map((team) => {
+ const isInstalled =
+ credentials &&
+ credentials.some((credential) =>
+ credential?.teamId ? credential?.teamId === team.id : credential.userId === team.id
+ );
+
+ return (
+ (
+
+ )}
+ onClick={() => {
+ mutation.mutate(
+ team.isUser ? addAppMutationInput : { ...addAppMutationInput, teamId: team.id }
+ );
+ }}>
+
+ {t(team.name)} {isInstalled && `(${t("installed")})`}
+
+
+ );
+ })}
+
+
+
+ );
+};
+import Link from "next/link";
+import type { IframeHTMLAttributes } from "react";
+import React, { useState, useEffect } from "react";
+
+import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
+import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
+import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
+import classNames from "@calcom/lib/classNames";
+import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import type { App as AppType } from "@calcom/types/App";
+import { Badge, Button, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
+import { BookOpen, Check, ExternalLink, File, Flag, Mail, Shield } from "@calcom/ui/components/icon";
+
+import { InstallAppButtonChild } from "./InstallAppButtonChild";
+
+export type AppPageProps = {
+ name: string;
+ description: AppType["description"];
+ type: AppType["type"];
+ isGlobal?: AppType["isGlobal"];
+ logo: string;
+ slug: string;
+ variant: string;
+ body: React.ReactNode;
+ categories: string[];
+ author: string;
+ pro?: boolean;
+ price?: number;
+ commission?: number;
+ feeType?: AppType["feeType"];
+ docs?: string;
+ website?: string;
+ email: string; // required
+ tos?: string;
+ privacy?: string;
+ licenseRequired: AppType["licenseRequired"];
+ teamsPlanRequired: AppType["teamsPlanRequired"];
+ descriptionItems?: Array }>;
+ isTemplate?: boolean;
+ disableInstall?: boolean;
+ dependencies?: string[];
+ concurrentMeetings: AppType["concurrentMeetings"];
+ paid?: AppType["paid"];
+};
+
+export const AppPage = ({
+ name,
+ type,
+ logo,
+ slug,
+ variant,
+ body,
+ categories,
+ author,
+ price = 0,
+ commission,
+ isGlobal = false,
+ feeType,
+ docs,
+ website,
+ email,
+ tos,
+ privacy,
+ teamsPlanRequired,
+ descriptionItems,
+ isTemplate,
+ dependencies,
+ concurrentMeetings,
+ paid,
+}: AppPageProps) => {
+ const { t, i18n } = useLocale();
+ const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
+
+ const mutation = useAddAppMutation(null, {
+ onSuccess: (data) => {
+ if (data?.setupPending) return;
+ showToast(t("app_successfully_installed"), "success");
+ },
+ onError: (error) => {
+ if (error instanceof Error) showToast(error.message || t("app_could_not_be_installed"), "error");
+ },
+ });
+
+ const priceInDollar = Intl.NumberFormat("en-US", {
+ style: "currency",
+ currency: "USD",
+ useGrouping: false,
+ }).format(price);
+
+ const [existingCredentials, setExistingCredentials] = useState([]);
+ const [showDisconnectIntegration, setShowDisconnectIntegration] = useState(false);
+
+ const appDbQuery = trpc.viewer.appCredentialsByType.useQuery({ appType: type });
+
+ useEffect(
+ function refactorMeWithoutEffect() {
+ const data = appDbQuery.data;
+
+ const credentialsCount = data?.credentials.length || 0;
+ setShowDisconnectIntegration(
+ data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
+ );
+ setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
+ },
+ [appDbQuery.data]
+ );
+
+ const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
+ enabled: !!dependencies,
+ });
+
+ const disableInstall =
+ dependencyData.data && dependencyData.data.some((dependency) => !dependency.installed);
+
+ // const disableInstall = requiresGCal && !gCalInstalled.data;
+
+ // variant not other allows, an app to be shown in calendar category without requiring an actual calendar connection e.g. vimcal
+ // Such apps, can only be installed once.
+ const allowedMultipleInstalls = categories.indexOf("calendar") > -1 && variant !== "other";
+
+ return (
+
+ {hasDescriptionItems && (
+
+ {descriptionItems ? (
+ descriptionItems.map((descriptionItem, index) =>
+ typeof descriptionItem === "object" ? (
+
+
+
+ ) : (
+
+ )
+ )
+ ) : (
+
+ )}
+
+ )}
+
+
+
+
+
+
{name}
+
+
+
+ {categories[0]}
+ {" "}
+ {paid && (
+ <>
+
+ {Intl.NumberFormat(i18n.language, {
+ style: "currency",
+ currency: "USD",
+ useGrouping: false,
+ maximumFractionDigits: 0,
+ }).format(paid.priceInUsd)}
+ /{t("month")}
+
+ >
+ )}
+ •{" "}
+
+ {t("published_by", { author })}
+
+
+ {isTemplate && (
+
+ Template - Available in Dev Environment only for testing
+
+ )}
+
+
+ {!appDbQuery.isPending ? (
+ isGlobal ||
+ (existingCredentials.length > 0 && allowedMultipleInstalls ? (
+
+
+ {existingCredentials.length > 0
+ ? t("active_install", { count: existingCredentials.length })
+ : t("default")}
+
+ {!isGlobal && (
+ {
+ if (useDefaultComponent) {
+ props = {
+ ...props,
+ onClick: () => {
+ mutation.mutate({ type, variant, slug });
+ },
+ loading: mutation.isPending,
+ };
+ }
+ return (
+
+ );
+ }}
+ />
+ )}
+
+ ) : showDisconnectIntegration ? (
+
{
+ appDbQuery.refetch();
+ }}
+ />
+ ) : (
+ {
+ if (useDefaultComponent) {
+ props = {
+ ...props,
+ onClick: () => {
+ mutation.mutate({ type, variant, slug });
+ },
+ loading: mutation.isPending,
+ };
+ }
+ return (
+
+ );
+ }}
+ />
+ ))
+ ) : (
+
+ )}
+
+ {dependencies &&
+ (!dependencyData.isPending ? (
+
+ ) : (
+
+ ))}
+
+ {price !== 0 && !paid && (
+
+ {feeType === "usage-based" ? `${commission}% + ${priceInDollar}/booking` : priceInDollar}
+ {feeType === "monthly" && `/${t("month")}`}
+
+ )}
+
+
+ {body}
+
+ {!paid && (
+ <>
+ {t("pricing")}
+
+ {teamsPlanRequired ? (
+ t("teams_plan_required")
+ ) : price === 0 ? (
+ t("free_to_use_apps")
+ ) : (
+ <>
+ {Intl.NumberFormat(i18n.language, {
+ style: "currency",
+ currency: "USD",
+ useGrouping: false,
+ }).format(price)}
+ {feeType === "monthly" && `/${t("month")}`}
+ >
+ )}
+
+ >
+ )}
+
+ {t("contact")}
+
+
+
+ {t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
+
+
+ {t("report_app")}
+
+
+
+ );
+};
+import Link from "next/link";
+import { Fragment } from "react";
+
+import { InstallAppButton } from "@calcom/app-store/components";
+import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
+import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
+import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import {
+ Alert,
+ Button,
+ EmptyScreen,
+ List,
+ AppSkeletonLoader as SkeletonLoader,
+ ShellSubHeading,
+ Label,
+} from "@calcom/ui";
+import { Calendar } from "@calcom/ui/components/icon";
+
+import { QueryCell } from "@lib/QueryCell";
+
+import AppListCard from "@components/AppListCard";
+import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector";
+import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
+
+type Props = {
+ onChanged: () => unknown | Promise;
+ fromOnboarding?: boolean;
+ destinationCalendarId?: string;
+ isPending?: boolean;
+};
+
+function CalendarList(props: Props) {
+ const { t } = useLocale();
+ const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: false });
+
+ return (
+ (
+
+ {data.items.map((item) => (
+ (
+
+ {t("connect")}
+
+ )}
+ onChanged={() => props.onChanged()}
+ />
+ }
+ />
+ ))}
+
+ )}
+ />
+ );
+}
+
+// todo: @hariom extract this into packages/apps-store as "GeneralAppSettings"
+function ConnectedCalendarsList(props: Props) {
+ const { t } = useLocale();
+ const query = trpc.viewer.connectedCalendars.useQuery(undefined, {
+ suspense: true,
+ refetchOnWindowFocus: false,
+ });
+ const { fromOnboarding, isPending } = props;
+ return (
+ null}
+ success={({ data }) => {
+ if (!data.connectedCalendars.length) {
+ return null;
+ }
+
+ return (
+
+
+
+
+
+ {t("check_for_conflicts")}
+
+
{t("select_calendars")}
+
+
+ {!!data.connectedCalendars.length && (
+
+ )}
+
+
+
+
+ {data.connectedCalendars.map((item) => (
+
+ {item.calendars ? (
+
+
+
+ }>
+
+ {!fromOnboarding && (
+ <>
+
{t("toggle_calendars_conflict")}
+
+ {item.calendars.map((cal) => (
+
+ ))}
+
+ >
+ )}
+
+
+ ) : (
+
+ {item.integration.name}:{" "}
+ {t("calendar_error")}
+
+ }
+ iconClassName="h-10 w-10 ml-2 mr-1 mt-0.5"
+ actions={
+
+
+
+ }
+ />
+ )}
+
+ ))}
+
+
+ );
+ }}
+ />
+ );
+}
+
+export function CalendarListContainer(props: { heading?: boolean; fromOnboarding?: boolean }) {
+ const { t } = useLocale();
+ const { heading = true, fromOnboarding } = props;
+ const utils = trpc.useContext();
+ const onChanged = () =>
+ Promise.allSettled([
+ utils.viewer.integrations.invalidate(
+ { variant: "calendar", onlyInstalled: true },
+ {
+ exact: true,
+ }
+ ),
+ utils.viewer.connectedCalendars.invalidate(),
+ ]);
+ const query = trpc.viewer.connectedCalendars.useQuery();
+ const installedCalendars = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
+ const mutation = trpc.viewer.setDestinationCalendar.useMutation({
+ onSuccess: () => {
+ utils.viewer.connectedCalendars.invalidate();
+ },
+ });
+ return (
+ }
+ success={({ data }) => {
+ return (
+ <>
+ {!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
+ <>
+ {heading && (
+ <>
+
+
+
+ {t("add_to_calendar")}
+
+
+
+ {t("add_to_calendar_description")}
+
+
+
+
+
+ {t("add_events_to")}
+
+
+
+
+
+
+ >
+ )}
+ >
+ ) : fromOnboarding ? (
+ <>
+ {!!query.data?.connectedCalendars.length && (
+ }
+ />
+ )}
+
+ >
+ ) : (
+
+ {t(`connect_calendar_apps`)}
+
+ }
+ />
+ )}
+ >
+ );
+ }}
+ />
+ );
+}
+import type { FunctionComponent, SVGProps } from "react";
+
+import { InstallAppButton } from "@calcom/app-store/components";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import {
+ Button,
+ Dropdown,
+ DropdownMenuContent,
+ DropdownMenuTrigger,
+ DropdownMenuItem,
+ DropdownItem,
+} from "@calcom/ui";
+import { Plus } from "@calcom/ui/components/icon";
+
+import { QueryCell } from "@lib/QueryCell";
+
+interface AdditionalCalendarSelectorProps {
+ isPending?: boolean;
+}
+
+const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorProps): JSX.Element | null => {
+ const { t } = useLocale();
+ const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
+
+ return (
+ {
+ const options = data.items.map((item) => ({
+ label: item.name,
+ slug: item.slug,
+ image: item.logo,
+ type: item.type,
+ }));
+ options.push({
+ label: "Add new calendars",
+ slug: "add-new",
+ image: "",
+ type: "new_other",
+ });
+ return (
+
+
+
+ {t("add")}
+
+
+
+ {options.map((data) => (
+
+ {data.slug === "add-new" ? (
+
+ {t("install_new_calendar_app")}
+
+ ) : (
+ {
+ const props = { ...installProps } as FunctionComponent>;
+ return (
+
+
+ {data.image && }
+ {`${t("add")} ${data.label}`}
+
+
+ );
+ }}
+ />
+ )}
+
+ ))}
+
+
+ );
+ }}
+ />
+ );
+};
+
+export default AdditionalCalendarSelector;
+import { useSession } from "next-auth/react";
+import { useRouter } from "next/navigation";
+import type { ComponentProps } from "react";
+import React from "react";
+
+import { ShellMain } from "@calcom/features/shell/Shell";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { EmptyScreen } from "@calcom/ui";
+import { AlertCircle } from "@calcom/ui/components/icon";
+
+type AppsLayoutProps = {
+ children: React.ReactNode;
+ actions?: (className?: string) => JSX.Element;
+ emptyStore?: boolean;
+} & Omit, "actions">;
+
+export default function AppsLayout({ children, actions, emptyStore, ...rest }: AppsLayoutProps) {
+ const { t } = useLocale();
+ const session = useSession();
+ const router = useRouter();
+ const isAdmin = session.data?.user.role === "ADMIN";
+
+ if (session.status === "loading") return <>>;
+ return (
+
+
+
+ {emptyStore ? (
+ router.push("/settings/admin/apps/calendar")}
+ />
+ ) : (
+ <>{children}>
+ )}
+
+
+
+ );
+}
+import type { ComponentProps } from "react";
+import React from "react";
+
+import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
+import Shell from "@calcom/features/shell/Shell";
+
+export default function InstalledAppsLayout({
+ children,
+ ...rest
+}: { children: React.ReactNode } & ComponentProps) {
+ return (
+
+
+ {children}
+
+
+ );
+}
+export const getLayout = (page: React.ReactElement) => {page} ;
+import { useCallback, useState } from "react";
+
+import { AppSettings } from "@calcom/app-store/_components/AppSettings";
+import { InstallAppButton } from "@calcom/app-store/components";
+import { getEventLocationTypeFromApp, type EventLocationType } from "@calcom/app-store/locations";
+import type { CredentialOwner } from "@calcom/app-store/types";
+import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
+import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { AppCategories } from "@calcom/prisma/enums";
+import { trpc, type RouterOutputs } from "@calcom/trpc";
+import type { App } from "@calcom/types/App";
+import {
+ Dropdown,
+ DropdownItem,
+ DropdownMenuContent,
+ DropdownMenuTrigger,
+ List,
+ showToast,
+ Button,
+ DropdownMenuItem,
+ Alert,
+} from "@calcom/ui";
+import { MoreHorizontal, Trash, Video } from "@calcom/ui/components/icon";
+
+import AppListCard from "@components/AppListCard";
+
+interface AppListProps {
+ variant?: AppCategories;
+ data: RouterOutputs["viewer"]["integrations"];
+ handleDisconnect: (credentialId: number) => void;
+ listClassName?: string;
+}
+
+export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppListProps) => {
+ const { data: defaultConferencingApp } = trpc.viewer.getUsersDefaultConferencingApp.useQuery();
+ const utils = trpc.useContext();
+ const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
+ const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>(
+ undefined
+ );
+
+ const onSuccessCallback = useCallback(() => {
+ setBulkUpdateModal(true);
+ showToast("Default app updated successfully", "success");
+ }, []);
+
+ const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({
+ onSuccess: () => {
+ showToast("Default app updated successfully", "success");
+ utils.viewer.getUsersDefaultConferencingApp.invalidate();
+ },
+ onError: (error) => {
+ showToast(`Error: ${error.message}`, "error");
+ },
+ });
+
+ const ChildAppCard = ({
+ item,
+ }: {
+ item: RouterOutputs["viewer"]["integrations"]["items"][number] & {
+ credentialOwner?: CredentialOwner;
+ };
+ }) => {
+ const appSlug = item?.slug;
+ const appIsDefault =
+ appSlug === defaultConferencingApp?.appSlug ||
+ (appSlug === "daily-video" && !defaultConferencingApp?.appSlug);
+ return (
+ 0 : false}
+ credentialOwner={item?.credentialOwner}
+ actions={
+ !item.credentialOwner?.readOnly ? (
+
+
+
+
+
+
+ {!appIsDefault && variant === "conferencing" && !item.credentialOwner?.teamId && (
+
+ {
+ const locationType = getEventLocationTypeFromApp(item?.locationOption?.value ?? "");
+ if (locationType?.linkType === "static") {
+ setLocationType({ ...locationType, slug: appSlug });
+ } else {
+ updateDefaultAppMutation.mutate({
+ appSlug,
+ });
+ setBulkUpdateModal(true);
+ }
+ }}>
+ {t("set_as_default")}
+
+
+ )}
+
+
+
+
+ ) : null
+ }>
+
+
+ );
+ };
+
+ const appsWithTeamCredentials = data.items.filter((app) => app.teams.length);
+ const cardsForAppsWithTeams = appsWithTeamCredentials.map((app) => {
+ const appCards = [];
+
+ if (app.userCredentialIds.length) {
+ appCards.push( );
+ }
+ for (const team of app.teams) {
+ if (team) {
+ appCards.push(
+
+ );
+ }
+ }
+ return appCards;
+ });
+
+ const { t } = useLocale();
+ return (
+ <>
+
+ {cardsForAppsWithTeams.map((apps) => apps.map((cards) => cards))}
+ {data.items
+ .filter((item) => item.invalidCredentialIds)
+ .map((item) => {
+ if (!item.teams.length) return ;
+ })}
+
+ {locationType && (
+ setLocationType(undefined)}
+ onSuccess={onSuccessCallback}
+ />
+ )}
+
+ {bulkUpdateModal && (
+
+ )}
+ >
+ );
+};
+
+function ConnectOrDisconnectIntegrationMenuItem(props: {
+ credentialId: number;
+ type: App["type"];
+ isGlobal?: boolean;
+ installed?: boolean;
+ invalidCredentialIds?: number[];
+ teamId?: number;
+ handleDisconnect: (credentialId: number, teamId?: number) => void;
+}) {
+ const { type, credentialId, isGlobal, installed, handleDisconnect, teamId } = props;
+ const { t } = useLocale();
+
+ const utils = trpc.useContext();
+ const handleOpenChange = () => {
+ utils.viewer.integrations.invalidate();
+ };
+
+ if (credentialId || type === "stripe_payment" || isGlobal) {
+ return (
+
+ handleDisconnect(credentialId, teamId)}
+ disabled={isGlobal}
+ StartIcon={Trash}>
+ {t("remove_app")}
+
+
+ );
+ }
+
+ if (!installed) {
+ return (
+
+ );
+ }
+
+ return (
+ (
+
+ {t("install")}
+
+ )}
+ onChanged={handleOpenChange}
+ />
+ );
+}
+import { usePathname, useRouter } from "next/navigation";
+import type { ReactNode } from "react";
+import { useEffect, useRef, useState } from "react";
+import { z } from "zod";
+
+import type { CredentialOwner } from "@calcom/app-store/types";
+import classNames from "@calcom/lib/classNames";
+import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
+import { Badge, ListItemText, Avatar } from "@calcom/ui";
+import { AlertCircle } from "@calcom/ui/components/icon";
+
+type ShouldHighlight =
+ | {
+ slug: string;
+ shouldHighlight: true;
+ }
+ | {
+ shouldHighlight?: never;
+ slug?: never;
+ };
+
+type AppListCardProps = {
+ logo?: string;
+ title: string;
+ description: string;
+ actions?: ReactNode;
+ isDefault?: boolean;
+ isTemplate?: boolean;
+ invalidCredential?: boolean;
+ children?: ReactNode;
+ credentialOwner?: CredentialOwner;
+ className?: string;
+} & ShouldHighlight;
+
+const schema = z.object({ hl: z.string().optional() });
+
+export default function AppListCard(props: AppListCardProps) {
+ const { t } = useLocale();
+ const {
+ logo,
+ title,
+ description,
+ actions,
+ isDefault,
+ slug,
+ shouldHighlight,
+ isTemplate,
+ invalidCredential,
+ children,
+ credentialOwner,
+ className,
+ } = props;
+ const {
+ data: { hl },
+ } = useTypedQuery(schema);
+ const router = useRouter();
+ const [highlight, setHighlight] = useState(shouldHighlight && hl === slug);
+ const timeoutRef = useRef(null);
+ const searchParams = useCompatSearchParams();
+ const pathname = usePathname();
+
+ useEffect(() => {
+ if (shouldHighlight && highlight && searchParams !== null && pathname !== null) {
+ timeoutRef.current = setTimeout(() => {
+ const _searchParams = new URLSearchParams(searchParams);
+ _searchParams.delete("hl");
+ _searchParams.delete("category"); // this comes from params, not from search params
+
+ setHighlight(false);
+
+ const stringifiedSearchParams = _searchParams.toString();
+
+ router.replace(`${pathname}${stringifiedSearchParams !== "" ? `?${stringifiedSearchParams}` : ""}`);
+ }, 3000);
+ }
+ return () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ timeoutRef.current = null;
+ }
+ };
+ }, [highlight, pathname, router, searchParams, shouldHighlight]);
+
+ return (
+
+
+ {logo ? (
+
+ ) : null}
+
+
+
{title}
+
+ {isDefault && {t("default")} }
+ {isTemplate && Template }
+
+
+
{description}
+ {invalidCredential && (
+
+
+
+ {t("invalid_credential")}
+
+
+ )}
+
+ {credentialOwner && (
+
+
+
+
+ {credentialOwner.name}
+
+
+
+ )}
+
+ {actions}
+
+ {children &&
{children}
}
+
+ );
+}
+"use client";
+
+import { type DehydratedState } from "@tanstack/react-query";
+import type { SSRConfig } from "next-i18next";
+// import I18nLanguageHandler from "@components/I18nLanguageHandler";
+import { usePathname } from "next/navigation";
+import Script from "next/script";
+import type { ReactNode } from "react";
+
+import "@calcom/embed-core/src/embed-iframe";
+import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
+
+import type { AppProps } from "@lib/app-providers-app-dir";
+import AppProviders from "@lib/app-providers-app-dir";
+
+export interface CalPageWrapper {
+ (props?: AppProps): JSX.Element;
+ PageWrapper?: AppProps["Component"]["PageWrapper"];
+}
+
+export type PageWrapperProps = Readonly<{
+ getLayout: ((page: React.ReactElement) => ReactNode) | null;
+ children: React.ReactNode;
+ requiresLicense: boolean;
+ nonce: string | undefined;
+ themeBasis: string | null;
+ dehydratedState?: DehydratedState;
+ isThemeSupported?: boolean;
+ isBookingPage?: boolean;
+ i18n?: SSRConfig;
+}>;
+
+function PageWrapper(props: PageWrapperProps) {
+ const pathname = usePathname();
+ let pageStatus = "200";
+
+ if (pathname === "/404") {
+ pageStatus = "404";
+ } else if (pathname === "/500") {
+ pageStatus = "500";
+ }
+
+ // On client side don't let nonce creep into DOM
+ // It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
+ // See https://github.com/kentcdodds/nonce-hydration-issues
+ // Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
+ const nonce = typeof window !== "undefined" ? (props.nonce ? "" : undefined) : props.nonce;
+ const providerProps: PageWrapperProps = {
+ ...props,
+ nonce,
+ };
+
+ const getLayout: (page: React.ReactElement) => ReactNode = props.getLayout ?? ((page) => page);
+
+ return (
+
+ {/* */}
+ <>
+
+ {getLayout(
+ props.requiresLicense ? {props.children} : <>{props.children}>
+ )}
+ >
+
+ );
+}
+
+export default PageWrapper;
+import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
+
+// If feature flag is disabled, return not found on getServerSideProps
+export const getServerSideProps = async () => {
+ const prisma = await import("@calcom/prisma").then((mod) => mod.default);
+ const flags = await getFeatureFlagMap(prisma);
+
+ if (flags.insights === false) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ return { props: {} };
+};
+import type { GetServerSidePropsContext } from "next";
+
+import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const prisma = await import("@calcom/prisma").then((mod) => mod.default);
+ const flags = await getFeatureFlagMap(prisma);
+ // Check if organizations are enabled
+ if (flags["organizations"] !== true) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ const querySlug = context.query.slug as string;
+
+ return {
+ props: {
+ querySlug: querySlug ?? null,
+ },
+ };
+};
+import type {
+ QueryObserverPendingResult,
+ QueryObserverRefetchErrorResult,
+ QueryObserverSuccessResult,
+ QueryObserverLoadingErrorResult,
+ UseQueryResult,
+} from "@tanstack/react-query";
+import type { ReactNode } from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Alert, Loader } from "@calcom/ui";
+
+type ErrorLike = {
+ message: string;
+};
+type JSXElementOrNull = JSX.Element | null;
+
+interface QueryCellOptionsBase {
+ query: UseQueryResult;
+ customLoader?: ReactNode;
+ error?: (
+ query: QueryObserverLoadingErrorResult | QueryObserverRefetchErrorResult
+ ) => JSXElementOrNull;
+ loading?: (query: QueryObserverPendingResult | null) => JSXElementOrNull;
+}
+
+interface QueryCellOptionsNoEmpty
+ extends QueryCellOptionsBase {
+ success: (query: QueryObserverSuccessResult) => JSXElementOrNull;
+}
+
+interface QueryCellOptionsWithEmpty
+ extends QueryCellOptionsBase {
+ success: (query: QueryObserverSuccessResult, TError>) => JSXElementOrNull;
+ /**
+ * If there's no data (`null`, `undefined`, or `[]`), render this component
+ */
+ empty: (query: QueryObserverSuccessResult) => JSXElementOrNull;
+}
+
+export function QueryCell(
+ opts: QueryCellOptionsWithEmpty
+): JSXElementOrNull;
+export function QueryCell(
+ opts: QueryCellOptionsNoEmpty
+): JSXElementOrNull;
+/** @deprecated Use `trpc.useQuery` instead. */
+export function QueryCell(
+ opts: QueryCellOptionsNoEmpty | QueryCellOptionsWithEmpty
+) {
+ const { query } = opts;
+ const { isLocaleReady } = useLocale();
+ const StatusLoader = opts.customLoader || ; // Fixes edge case where this can return null form query cell
+
+ if (!isLocaleReady) {
+ return opts.loading?.(query.status === "pending" ? query : null) ?? StatusLoader;
+ }
+ if (query.status === "pending") {
+ return opts.loading?.(query) ?? StatusLoader;
+ }
+
+ if (query.status === "success") {
+ if ("empty" in opts && (query.data == null || (Array.isArray(query.data) && query.data.length === 0))) {
+ return opts.empty(query);
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return opts.success(query as any);
+ }
+
+ if (query.status === "error") {
+ return (
+ opts.error?.(query) ?? (
+
+ )
+ );
+ }
+
+ // impossible state
+ return null;
+}
+import { TooltipProvider } from "@radix-ui/react-tooltip";
+import { dir } from "i18next";
+import type { Session } from "next-auth";
+import { SessionProvider, useSession } from "next-auth/react";
+import { EventCollectionProvider } from "next-collect/client";
+import type { SSRConfig } from "next-i18next";
+import { appWithTranslation } from "next-i18next";
+import { ThemeProvider } from "next-themes";
+import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
+import type { ParsedUrlQuery } from "querystring";
+import type { PropsWithChildren, ReactNode } from "react";
+import { useEffect } from "react";
+
+import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
+import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
+import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
+import { FeatureProvider } from "@calcom/features/flags/context/provider";
+import { useFlags } from "@calcom/features/flags/hooks";
+import { MetaProvider } from "@calcom/ui";
+
+import useIsBookingPage from "@lib/hooks/useIsBookingPage";
+import type { WithLocaleProps } from "@lib/withLocale";
+import type { WithNonceProps } from "@lib/withNonce";
+
+import { useViewerI18n } from "@components/I18nLanguageHandler";
+
+const I18nextAdapter = appWithTranslation<
+ NextJsAppProps & {
+ children: React.ReactNode;
+ }
+>(({ children }) => <>{children}>);
+
+// Workaround for https://github.com/vercel/next.js/issues/8592
+export type AppProps = Omit<
+ NextAppProps<
+ WithLocaleProps<
+ WithNonceProps<{
+ themeBasis?: string;
+ session: Session;
+ i18n?: SSRConfig;
+ }>
+ >
+ >,
+ "Component"
+> & {
+ Component: NextAppProps["Component"] & {
+ requiresLicense?: boolean;
+ isThemeSupported?: boolean;
+ isBookingPage?: boolean | ((arg: { router: NextAppProps["router"] }) => boolean);
+ getLayout?: (page: React.ReactElement) => ReactNode;
+ PageWrapper?: (props: AppProps) => JSX.Element;
+ };
+
+ /** Will be defined only is there was an error */
+ err?: Error;
+};
+
+type AppPropsWithChildren = AppProps & {
+ children: ReactNode;
+};
+
+const getEmbedNamespace = (query: ParsedUrlQuery) => {
+ // Mostly embed query param should be available on server. Use that there.
+ // Use the most reliable detection on client
+ return typeof window !== "undefined" ? window.getEmbedNamespace() : (query.embed as string) || null;
+};
+
+// We dont need to pass nonce to the i18n provider - this was causing x2-x3 re-renders on a hard refresh
+type AppPropsWithoutNonce = Omit & {
+ pageProps: Omit;
+};
+
+const CustomI18nextProvider = (props: AppPropsWithoutNonce) => {
+ /**
+ * i18n should never be clubbed with other queries, so that it's caching can be managed independently.
+ **/
+
+ const session = useSession();
+ const locale = session?.data?.user.locale ?? props.pageProps.newLocale;
+
+ useEffect(() => {
+ try {
+ // @ts-expect-error TS2790: The operand of a 'delete' operator must be optional.
+ delete window.document.documentElement["lang"];
+
+ window.document.documentElement.lang = locale;
+
+ // Next.js writes the locale to the same attribute
+ // https://github.com/vercel/next.js/blob/1609da2d9552fed48ab45969bdc5631230c6d356/packages/next/src/shared/lib/router/router.ts#L1786
+ // which can result in a race condition
+ // this property descriptor ensures this never happens
+ Object.defineProperty(window.document.documentElement, "lang", {
+ configurable: true,
+ // value: locale,
+ set: function (this) {
+ // empty setter on purpose
+ },
+ get: function () {
+ return locale;
+ },
+ });
+ } catch (error) {
+ console.error(error);
+
+ window.document.documentElement.lang = locale;
+ }
+
+ window.document.dir = dir(locale);
+ }, [locale]);
+
+ const clientViewerI18n = useViewerI18n(locale);
+ const i18n = clientViewerI18n.data?.i18n ?? props.pageProps.i18n;
+
+ const passedProps = {
+ ...props,
+ pageProps: {
+ ...props.pageProps,
+
+ ...i18n,
+ },
+ };
+
+ return ;
+};
+
+const enum ThemeSupport {
+ // e.g. Login Page
+ None = "none",
+ // Entire App except Booking Pages
+ App = "systemOnly",
+ // Booking Pages(including Routing Forms)
+ Booking = "userConfigured",
+}
+
+type CalcomThemeProps = PropsWithChildren<
+ Pick &
+ Pick &
+ Pick
+>;
+const CalcomThemeProvider = (props: CalcomThemeProps) => {
+ // Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
+ // One such example is our Embeds Demo and Testing page at http://localhost:3100
+ // Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
+ const embedNamespace = getEmbedNamespace(props.router.query);
+ const isEmbedMode = typeof embedNamespace === "string";
+
+ return (
+
+ {/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
+ {/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
+ {typeof window !== "undefined" && !isEmbedMode && (
+
+ )}
+ {props.children}
+
+ );
+};
+
+/**
+ * The most important job for this fn is to generate correct storageKey for theme persistenc.
+ * `storageKey` is important because that key is listened for changes(using [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event) and any pages opened will change it's theme based on that(as part of next-themes implementation).
+ * Choosing the right storageKey avoids theme flickering caused by another page using different theme
+ * So, we handle all the cases here namely,
+ * - Both Booking Pages, /free/30min and /pro/30min but configured with different themes but being operated together.
+ * - Embeds using different namespace. They can be completely themed different on the same page.
+ * - Embeds using the same namespace but showing different cal.com links with different themes
+ * - Embeds using the same namespace and showing same cal.com links with different themes(Different theme is possible for same cal.com link in case of embed because of theme config available in embed)
+ * - App has different theme then Booking Pages.
+ *
+ * All the above cases have one thing in common, which is the origin and thus localStorage is shared and thus `storageKey` is critical to avoid theme flickering.
+ *
+ * Some things to note:
+ * - There is a side effect of so many factors in `storageKey` that many localStorage keys will be created if a user goes through all these scenarios(e.g like booking a lot of different users)
+ * - Some might recommend disabling localStorage persistence but that doesn't give good UX as then we would default to light theme always for a few seconds before switching to dark theme(if that's the user's preference).
+ * - We can't disable [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event handling as well because changing theme in one tab won't change the theme without refresh in other tabs. That's again a bad UX
+ * - Theme flickering becomes infinitely ongoing in case of embeds because of the browser's delay in processing `storage` event within iframes. Consider two embeds simulatenously opened with pages A and B. Note the timeline and keep in mind that it happened
+ * because 'setItem(A)' and 'Receives storageEvent(A)' allowed executing setItem(B) in b/w because of the delay.
+ * - t1 -> setItem(A) & Fires storageEvent(A) - On Page A) - Current State(A)
+ * - t2 -> setItem(B) & Fires storageEvent(B) - On Page B) - Current State(B)
+ * - t3 -> Receives storageEvent(A) & thus setItem(A) & thus fires storageEvent(A) (On Page B) - Current State(A)
+ * - t4 -> Receives storageEvent(B) & thus setItem(B) & thus fires storageEvent(B) (On Page A) - Current State(B)
+ * - ... and so on ...
+ */
+function getThemeProviderProps({
+ props,
+ isEmbedMode,
+ embedNamespace,
+}: {
+ props: Omit;
+ isEmbedMode: boolean;
+ embedNamespace: string | null;
+}) {
+ const isBookingPage = (() => {
+ if (typeof props.isBookingPage === "function") {
+ return props.isBookingPage({ router: props.router });
+ }
+ return props.isBookingPage;
+ })();
+
+ const themeSupport = isBookingPage
+ ? ThemeSupport.Booking
+ : // if isThemeSupported is explicitly false, we don't use theme there
+ props.isThemeSupported === false
+ ? ThemeSupport.None
+ : ThemeSupport.App;
+
+ const isBookingPageThemeSupportRequired = themeSupport === ThemeSupport.Booking;
+ const themeBasis = props.themeBasis;
+
+ if ((isBookingPageThemeSupportRequired || isEmbedMode) && !themeBasis) {
+ console.warn(
+ "`themeBasis` is required for booking page theme support. Not providing it will cause theme flicker."
+ );
+ }
+
+ const appearanceIdSuffix = themeBasis ? `:${themeBasis}` : "";
+ const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
+ let embedExplicitlySetThemeSuffix = "";
+
+ if (typeof window !== "undefined") {
+ const embedTheme = window.getEmbedTheme();
+ if (embedTheme) {
+ embedExplicitlySetThemeSuffix = `:${embedTheme}`;
+ }
+ }
+
+ const storageKey = isEmbedMode
+ ? // Same Namespace, Same Organizer but different themes would still work seamless and not cause theme flicker
+ // Even though it's recommended to use different namespaces when you want to theme differently on the same page but if the embeds are on different pages, the problem can still arise
+ `embed-theme-${embedNamespace}${appearanceIdSuffix}${embedExplicitlySetThemeSuffix}`
+ : themeSupport === ThemeSupport.App
+ ? "app-theme"
+ : isBookingPageThemeSupportRequired
+ ? `booking-theme${appearanceIdSuffix}`
+ : undefined;
+
+ return {
+ storageKey,
+ forcedTheme,
+ themeSupport,
+ nonce: props.nonce,
+ enableColorScheme: false,
+ enableSystem: themeSupport !== ThemeSupport.None,
+ // next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
+ // This is how login to dashboard soft navigation changes theme from light to dark
+ key: storageKey,
+ attribute: "class",
+ };
+}
+
+function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
+ const flags = useFlags();
+ return {children} ;
+}
+
+function useOrgBrandingValues() {
+ const session = useSession();
+ return session?.data?.user.org;
+}
+
+function OrgBrandProvider({ children }: { children: React.ReactNode }) {
+ const orgBrand = useOrgBrandingValues();
+ return {children} ;
+}
+
+const AppProviders = (props: AppPropsWithChildren) => {
+ // No need to have intercom on public pages - Good for Page Performance
+ const isBookingPage = useIsBookingPage();
+ const { pageProps, ...rest } = props;
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { nonce, ...restPageProps } = pageProps;
+ const propsWithoutNonce = {
+ pageProps: {
+ ...restPageProps,
+ },
+ ...rest,
+ };
+
+ const RemainingProviders = (
+
+
+
+
+ {/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
+
+
+
+ {props.children}
+
+
+
+
+
+
+
+ );
+
+ if (isBookingPage) {
+ return RemainingProviders;
+ }
+
+ return (
+
+ {RemainingProviders}
+
+ );
+};
+
+export default AppProviders;
+import type { SearchParams } from "app/_types";
+import { type Params } from "app/_types";
+import type { GetServerSidePropsContext } from "next";
+import { type ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
+import { type ReadonlyRequestCookies } from "next/dist/server/web/spec-extension/adapters/request-cookies";
+
+const createProxifiedObject = (object: Record) =>
+ new Proxy(object, {
+ set: () => {
+ throw new Error("You are trying to modify 'headers' or 'cookies', which is not supported in app dir");
+ },
+ });
+
+const buildLegacyHeaders = (headers: ReadonlyHeaders) => {
+ const headersObject = Object.fromEntries(headers.entries());
+
+ return createProxifiedObject(headersObject);
+};
+
+const buildLegacyCookies = (cookies: ReadonlyRequestCookies) => {
+ const cookiesObject = cookies.getAll().reduce>((acc, { name, value }) => {
+ acc[name] = value;
+ return acc;
+ }, {});
+
+ return createProxifiedObject(cookiesObject);
+};
+
+export const buildLegacyCtx = (
+ headers: ReadonlyHeaders,
+ cookies: ReadonlyRequestCookies,
+ params: Params,
+ searchParams: SearchParams
+) => {
+ return {
+ query: { ...searchParams, ...params },
+ params,
+ req: { headers: buildLegacyHeaders(headers), cookies: buildLegacyCookies(cookies) },
+ res: new Proxy(Object.create(null), {
+ // const { req, res } = ctx - valid
+ // res.anything - throw
+ get() {
+ throw new Error(
+ "You are trying to access the 'res' property of the context, which is not supported in app dir"
+ );
+ },
+ }),
+ } as unknown as GetServerSidePropsContext;
+};
+import type { GetServerSidePropsContext } from "next";
+import z from "zod";
+
+import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
+import slugify from "@calcom/lib/slugify";
+import prisma from "@calcom/prisma";
+
+import { getServerSideProps as GSSTeamTypePage } from "@lib/team/[slug]/[type]/getServerSideProps";
+
+import { getServerSideProps as GSSUserTypePage } from "~/users/views/users-type-public-view.getServerSideProps";
+
+const paramsSchema = z.object({
+ orgSlug: z.string().transform((s) => slugify(s)),
+ user: z.string(),
+ type: z.string().transform((s) => slugify(s)),
+});
+
+export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
+ const { user: teamOrUserSlugOrDynamicGroup, orgSlug, type } = paramsSchema.parse(ctx.params);
+ const team = await prisma.team.findFirst({
+ where: {
+ slug: slugify(teamOrUserSlugOrDynamicGroup),
+ parentId: {
+ not: null,
+ },
+ parent: getSlugOrRequestedSlug(orgSlug),
+ },
+ select: {
+ id: true,
+ },
+ });
+
+ if (team) {
+ const params = { slug: teamOrUserSlugOrDynamicGroup, type };
+ return GSSTeamTypePage({
+ ...ctx,
+ params: {
+ ...ctx.params,
+ ...params,
+ },
+ query: {
+ ...ctx.query,
+ ...params,
+ },
+ });
+ }
+ const params = { user: teamOrUserSlugOrDynamicGroup, type };
+ return GSSUserTypePage({
+ ...ctx,
+ params: {
+ ...ctx.params,
+ ...params,
+ },
+ query: {
+ ...ctx.query,
+ ...params,
+ },
+ });
+};
+import { getServerSideProps as GSSUserPage } from "@pages/[user]";
+import { getServerSideProps as GSSTeamPage } from "@pages/team/[slug]";
+import type { GetServerSidePropsContext } from "next";
+
+import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
+import prisma from "@calcom/prisma";
+
+export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
+ const team = await prisma.team.findFirst({
+ where: {
+ slug: ctx.query.user as string,
+ parentId: {
+ not: null,
+ },
+ parent: getSlugOrRequestedSlug(ctx.query.orgSlug as string),
+ },
+ select: {
+ id: true,
+ },
+ });
+ if (team) {
+ return GSSTeamPage({ ...ctx, query: { slug: ctx.query.user } });
+ }
+ return GSSUserPage({ ...ctx, query: { user: ctx.query.user, redirect: ctx.query.redirect } });
+};
+import type { GetServerSidePropsContext } from "next";
+import { z } from "zod";
+
+import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
+import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername";
+import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
+import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
+import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
+import slugify from "@calcom/lib/slugify";
+import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
+
+import { IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
+import { ssrInit } from "@server/lib/ssr";
+
+const checkValidEmail = (email: string) => z.string().email().safeParse(email).success;
+
+const querySchema = z.object({
+ username: z
+ .string()
+ .optional()
+ .transform((val) => val || ""),
+ email: z.string().email().optional(),
+});
+
+export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
+ const prisma = await import("@calcom/prisma").then((mod) => mod.default);
+ const flags = await getFeatureFlagMap(prisma);
+ const ssr = await ssrInit(ctx);
+ const token = z.string().optional().parse(ctx.query.token);
+
+ const props = {
+ isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
+ isSAMLLoginEnabled,
+ trpcState: ssr.dehydrate(),
+ prepopulateFormValues: undefined,
+ };
+
+ // username + email prepopulated from query params
+ const { username: preFillusername, email: prefilEmail } = querySchema.parse(ctx.query);
+
+ if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || flags["disable-signup"]) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ // no token given, treat as a normal signup without verification token
+ if (!token) {
+ return {
+ props: JSON.parse(
+ JSON.stringify({
+ ...props,
+ prepopulateFormValues: {
+ username: preFillusername || null,
+ email: prefilEmail || null,
+ },
+ })
+ ),
+ };
+ }
+
+ const verificationToken = await prisma.verificationToken.findUnique({
+ where: {
+ token,
+ },
+ include: {
+ team: {
+ select: {
+ metadata: true,
+ parentId: true,
+ parent: {
+ select: {
+ slug: true,
+ metadata: true,
+ },
+ },
+ slug: true,
+ },
+ },
+ },
+ });
+
+ if (!verificationToken || verificationToken.expires < new Date()) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ const existingUser = await prisma.user.findFirst({
+ where: {
+ AND: [
+ {
+ email: verificationToken?.identifier,
+ },
+ {
+ emailVerified: {
+ not: null,
+ },
+ },
+ ],
+ },
+ });
+
+ if (existingUser) {
+ return {
+ redirect: {
+ permanent: false,
+ destination: `/auth/login?callbackUrl=${WEBAPP_URL}/${ctx.query.callbackUrl}`,
+ },
+ };
+ }
+
+ const guessUsernameFromEmail = (email: string) => {
+ const [username] = email.split("@");
+ return username;
+ };
+
+ let username = guessUsernameFromEmail(verificationToken.identifier);
+
+ const tokenTeam = {
+ ...verificationToken?.team,
+ metadata: teamMetadataSchema.parse(verificationToken?.team?.metadata),
+ };
+
+ const isATeamInOrganization = tokenTeam?.parentId !== null;
+ const isOrganization = tokenTeam.metadata?.isOrganization;
+ // Detect if the team is an org by either the metadata flag or if it has a parent team
+ const isOrganizationOrATeamInOrganization = isOrganization || isATeamInOrganization;
+ // If we are dealing with an org, the slug may come from the team itself or its parent
+ const orgSlug = isOrganizationOrATeamInOrganization
+ ? tokenTeam.metadata?.requestedSlug || tokenTeam.parent?.slug || tokenTeam.slug
+ : null;
+
+ // Org context shouldn't check if a username is premium
+ if (!IS_SELF_HOSTED && !isOrganizationOrATeamInOrganization) {
+ // Im not sure we actually hit this because of next redirects signup to website repo - but just in case this is pretty cool :)
+ const { available, suggestion } = await checkPremiumUsername(username);
+
+ username = available ? username : suggestion || username;
+ }
+
+ const isValidEmail = checkValidEmail(verificationToken.identifier);
+ const isOrgInviteByLink = isOrganizationOrATeamInOrganization && !isValidEmail;
+ const parentMetaDataForSubteam = tokenTeam?.parent?.metadata
+ ? teamMetadataSchema.parse(tokenTeam.parent.metadata)
+ : null;
+
+ return {
+ props: {
+ ...props,
+ token,
+ prepopulateFormValues: !isOrgInviteByLink
+ ? {
+ email: verificationToken.identifier,
+ username: isOrganizationOrATeamInOrganization
+ ? getOrgUsernameFromEmail(
+ verificationToken.identifier,
+ (isOrganization
+ ? tokenTeam.metadata?.orgAutoAcceptEmail
+ : parentMetaDataForSubteam?.orgAutoAcceptEmail) || ""
+ )
+ : slugify(username),
+ }
+ : null,
+ orgSlug,
+ orgAutoAcceptEmail: isOrgInviteByLink
+ ? tokenTeam?.metadata?.orgAutoAcceptEmail ?? parentMetaDataForSubteam?.orgAutoAcceptEmail ?? null
+ : null,
+ },
+ };
+};
+/** @deprecated use zod instead */
+export function asStringOrNull(str: unknown) {
+ return typeof str === "string" ? str : null;
+}
+
+/** @deprecated use zod instead */
+export function asStringOrUndefined(str: unknown) {
+ return typeof str === "string" ? str : undefined;
+}
+
+/** @deprecated use zod instead */
+export function asNumberOrUndefined(str: unknown) {
+ return typeof str === "string" ? parseInt(str) : undefined;
+}
+
+/** @deprecated use zod instead */
+export function asNumberOrThrow(str: unknown) {
+ return parseInt(asStringOrThrow(str));
+}
+
+/** @deprecated use zod instead */
+export function asStringOrThrow(str: unknown): string {
+ if (typeof str !== "string") {
+ throw new Error(`Expected "string" - got ${typeof str}`);
+ }
+ return str;
+}
+import type { GetStaticProps } from "next";
+import { z } from "zod";
+
+const querySchema = z.object({
+ workflow: z.string(),
+});
+
+export const getStaticProps: GetStaticProps = (ctx) => {
+ const params = querySchema.safeParse(ctx.params);
+ console.log("Built workflow page:", params);
+ if (!params.success) return { notFound: true };
+
+ return {
+ props: {
+ workflow: params.data.workflow,
+ },
+ revalidate: 10, // seconds
+ };
+};
+import type { GetServerSideProps } from "next";
+
+import { csp } from "@lib/csp";
+
+export type WithNonceProps> = T & {
+ nonce?: string;
+};
+
+/**
+ * Make any getServerSideProps fn return the nonce so that it can be used by Components in the page to add any script tag.
+ * Note that if the Components are not adding any script tag then this is not needed. Even in absence of this, Document.getInitialProps would be able to generate nonce itself which it needs to add script tags common to all pages
+ * There is no harm in wrapping a `getServerSideProps` fn with this even if it doesn't add any script tag.
+ */
+export default function withNonce>(
+ getServerSideProps: GetServerSideProps
+): GetServerSideProps> {
+ return async (context) => {
+ const ssrResponse = await getServerSideProps(context);
+
+ if (!("props" in ssrResponse)) {
+ return ssrResponse;
+ }
+
+ const { nonce } = csp(context.req, context.res);
+
+ // Skip nonce property if it's not available instead of setting it to undefined because undefined can't be serialized.
+ const nonceProps = nonce
+ ? {
+ nonce,
+ }
+ : null;
+
+ // Helps in debugging that withNonce was used but a valid nonce couldn't be set
+ context.res.setHeader("x-csp", nonce ? "ssr" : "false");
+
+ return {
+ ...ssrResponse,
+ props: {
+ ...ssrResponse.props,
+ ...nonceProps,
+ },
+ };
+ };
+}
+export type WithLocaleProps> = T & {
+ newLocale: string;
+};
+import type { GetServerSidePropsContext } from "next";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+
+import { getLocale } from "@calcom/features/auth/lib/getLocale";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import prisma from "@calcom/prisma";
+
+import { ssrInit } from "@server/lib/ssr";
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const { req } = context;
+
+ const session = await getServerSession({ req });
+
+ if (!session?.user?.id) {
+ return { redirect: { permanent: false, destination: "/auth/login" } };
+ }
+
+ const ssr = await ssrInit(context);
+
+ await ssr.viewer.me.prefetch();
+
+ const user = await prisma.user.findUnique({
+ where: {
+ id: session.user.id,
+ },
+ select: {
+ completedOnboarding: true,
+ teams: {
+ select: {
+ accepted: true,
+ team: {
+ select: {
+ id: true,
+ name: true,
+ logo: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ if (!user) {
+ throw new Error("User from session not found");
+ }
+
+ if (user.completedOnboarding) {
+ return { redirect: { permanent: false, destination: "/event-types" } };
+ }
+ const locale = await getLocale(context.req);
+ return {
+ props: {
+ ...(await serverSideTranslations(locale, ["common"])),
+ trpcState: ssr.dehydrate(),
+ hasPendingInvites: user.teams.find((team) => team.accepted === false) ?? false,
+ },
+ };
+};
+import type { GetServerSidePropsContext } from "next";
+import { z } from "zod";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
+import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
+import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
+import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
+import slugify from "@calcom/lib/slugify";
+import prisma from "@calcom/prisma";
+import { RedirectType } from "@calcom/prisma/client";
+
+import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
+
+const paramsSchema = z.object({
+ type: z.string().transform((s) => slugify(s)),
+ slug: z.string().transform((s) => slugify(s)),
+});
+
+// Booker page fetches a tiny bit of data server side:
+// 1. Check if team exists, to show 404
+// 2. If rescheduling, get the booking details
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const session = await getServerSession(context);
+ const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
+ const { rescheduleUid, duration: queryDuration, isInstantMeeting: queryIsInstantMeeting } = context.query;
+ const { ssrInit } = await import("@server/lib/ssr");
+ const ssr = await ssrInit(context);
+ const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
+ const isOrgContext = currentOrgDomain && isValidOrgDomain;
+
+ if (!isOrgContext) {
+ const redirect = await getTemporaryOrgRedirect({
+ slugs: teamSlug,
+ redirectType: RedirectType.Team,
+ eventTypeSlug: meetingSlug,
+ currentQuery: context.query,
+ });
+
+ if (redirect) {
+ return redirect;
+ }
+ }
+
+ const team = await prisma.team.findFirst({
+ where: {
+ ...getSlugOrRequestedSlug(teamSlug),
+ parent: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
+ },
+ select: {
+ id: true,
+ hideBranding: true,
+ },
+ });
+
+ if (!team) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ let booking: GetBookingType | null = null;
+ if (rescheduleUid) {
+ booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
+ }
+
+ const org = isValidOrgDomain ? currentOrgDomain : null;
+ // We use this to both prefetch the query on the server,
+ // as well as to check if the event exist, so we c an show a 404 otherwise.
+ const eventData = await ssr.viewer.public.event.fetch({
+ username: teamSlug,
+ eventSlug: meetingSlug,
+ isTeamEvent: true,
+ org,
+ });
+
+ if (!eventData) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ return {
+ props: {
+ entity: eventData.entity,
+ duration: getMultipleDurationValue(
+ eventData.metadata?.multipleDuration,
+ queryDuration,
+ eventData.length
+ ),
+ booking,
+ away: false,
+ user: teamSlug,
+ teamId: team.id,
+ slug: meetingSlug,
+ trpcState: ssr.dehydrate(),
+ isBrandingHidden: team?.hideBranding,
+ isInstantMeeting: eventData.isInstantEvent && queryIsInstantMeeting ? true : false,
+ themeBasis: null,
+ },
+ };
+};
+import type { GetServerSidePropsContext } from "next";
+
+import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
+import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
+import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
+import logger from "@calcom/lib/logger";
+import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
+import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
+import slugify from "@calcom/lib/slugify";
+import { stripMarkdown } from "@calcom/lib/stripMarkdown";
+import prisma from "@calcom/prisma";
+import { RedirectType } from "@calcom/prisma/client";
+import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
+
+import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
+
+import { ssrInit } from "@server/lib/ssr";
+
+const log = logger.getSubLogger({ prefix: ["team/[slug]"] });
+
+const getTheLastArrayElement = (value: ReadonlyArray | string | undefined): string | undefined => {
+ if (value === undefined || typeof value === "string") {
+ return value;
+ }
+
+ return value.at(-1);
+};
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const slug = getTheLastArrayElement(context.query.slug) ?? getTheLastArrayElement(context.query.orgSlug);
+
+ const { isValidOrgDomain, currentOrgDomain } = orgDomainConfig(
+ context.req,
+ context.params?.orgSlug ?? context.query?.orgSlug
+ );
+ const isOrgContext = isValidOrgDomain && currentOrgDomain;
+
+ // Provided by Rewrite from next.config.js
+ const isOrgProfile = context.query?.isOrgProfile === "1";
+ const flags = await getFeatureFlagMap(prisma);
+ const isOrganizationFeatureEnabled = flags["organizations"];
+
+ log.debug("getServerSideProps", {
+ isOrgProfile,
+ isOrganizationFeatureEnabled,
+ isValidOrgDomain,
+ currentOrgDomain,
+ });
+
+ const team = await getTeamWithMembers({
+ slug: slugify(slug ?? ""),
+ orgSlug: currentOrgDomain,
+ isTeamView: true,
+ isOrgView: isValidOrgDomain && isOrgProfile,
+ });
+
+ if (!isOrgContext && slug) {
+ const redirect = await getTemporaryOrgRedirect({
+ slugs: slug,
+ redirectType: RedirectType.Team,
+ eventTypeSlug: null,
+ currentQuery: context.query,
+ });
+
+ if (redirect) {
+ return redirect;
+ }
+ }
+
+ const ssr = await ssrInit(context);
+ const metadata = teamMetadataSchema.parse(team?.metadata ?? {});
+
+ // Taking care of sub-teams and orgs
+ if (
+ (!isValidOrgDomain && team?.parent) ||
+ (!isValidOrgDomain && !!metadata?.isOrganization) ||
+ !isOrganizationFeatureEnabled
+ ) {
+ return { notFound: true } as const;
+ }
+
+ if (!team || (team.parent && !team.parent.slug)) {
+ const unpublishedTeam = await prisma.team.findFirst({
+ where: {
+ ...(team?.parent
+ ? { id: team.parent.id }
+ : {
+ metadata: {
+ path: ["requestedSlug"],
+ equals: slug,
+ },
+ }),
+ },
+ });
+
+ if (!unpublishedTeam) return { notFound: true } as const;
+
+ return {
+ props: {
+ isUnpublished: true,
+ team: { ...unpublishedTeam, createdAt: null },
+ trpcState: ssr.dehydrate(),
+ },
+ } as const;
+ }
+
+ team.eventTypes =
+ team.eventTypes?.map((type) => ({
+ ...type,
+ users: type.users.map((user) => ({
+ ...user,
+ avatar: `/${user.username}/avatar.png`,
+ })),
+ descriptionAsSafeHTML: markdownToSafeHTML(type.description),
+ })) ?? null;
+
+ const safeBio = markdownToSafeHTML(team.bio) || "";
+
+ const members = !team.isPrivate
+ ? team.members.map((member) => {
+ return {
+ name: member.name,
+ id: member.id,
+ bio: member.bio,
+ subteams: member.subteams,
+ username: member.username,
+ accepted: member.accepted,
+ organizationId: member.organizationId,
+ safeBio: markdownToSafeHTML(member.bio || ""),
+ bookerUrl: getBookerBaseUrlSync(member.organization?.slug || ""),
+ };
+ })
+ : [];
+
+ const markdownStrippedBio = stripMarkdown(team?.bio || "");
+
+ const { inviteToken: _inviteToken, ...serializableTeam } = team;
+
+ return {
+ props: {
+ team: { ...serializableTeam, safeBio, members, metadata },
+ themeBasis: serializableTeam.slug,
+ trpcState: ssr.dehydrate(),
+ markdownStrippedBio,
+ isValidOrgDomain,
+ currentOrgDomain,
+ },
+ } as const;
+};
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+
+export function useToggleQuery(name: string) {
+ const searchParams = useCompatSearchParams();
+
+ return {
+ isOn: searchParams?.get(name) === "1",
+ };
+}
+// returns query object same as ctx.query but for app dir
+export const getQuery = (url: string, params: Record) => {
+ if (!url.length) {
+ return params;
+ }
+
+ const { searchParams } = new URL(url);
+ const searchParamsObj = Object.fromEntries(searchParams.entries());
+
+ return { ...searchParamsObj, ...params };
+};
+import type { GetServerSidePropsContext } from "next";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+
+import { ssrInit } from "@server/lib/ssr";
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const ssr = await ssrInit(context);
+ await ssr.viewer.me.prefetch();
+ const session = await getServerSession({ req: context.req, res: context.res });
+ const token = Array.isArray(context.query?.token) ? context.query.token[0] : context.query?.token;
+
+ const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}` : null;
+
+ if (!session) {
+ return {
+ redirect: {
+ destination: callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}` : "/auth/login",
+ permanent: false,
+ },
+ };
+ }
+
+ return { props: { trpcState: ssr.dehydrate() } };
+};
+import { TooltipProvider } from "@radix-ui/react-tooltip";
+import { TrpcProvider } from "app/_trpc/trpc-provider";
+import { dir } from "i18next";
+import type { Session } from "next-auth";
+import { SessionProvider, useSession } from "next-auth/react";
+import { EventCollectionProvider } from "next-collect/client";
+import { appWithTranslation, type SSRConfig } from "next-i18next";
+import { ThemeProvider } from "next-themes";
+import type { AppProps as NextAppProps } from "next/app";
+import type { ReadonlyURLSearchParams } from "next/navigation";
+import { useSearchParams } from "next/navigation";
+import { useEffect, type ReactNode } from "react";
+
+import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
+import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
+import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
+import { FeatureProvider } from "@calcom/features/flags/context/provider";
+import { useFlags } from "@calcom/features/flags/hooks";
+import { MetaProvider } from "@calcom/ui";
+
+import useIsBookingPage from "@lib/hooks/useIsBookingPage";
+import type { WithNonceProps } from "@lib/withNonce";
+
+import { useViewerI18n } from "@components/I18nLanguageHandler";
+import type { PageWrapperProps } from "@components/PageWrapperAppDir";
+
+// Workaround for https://github.com/vercel/next.js/issues/8592
+export type AppProps = Omit<
+ NextAppProps<
+ WithNonceProps<{
+ themeBasis?: string;
+ session: Session;
+ }>
+ >,
+ "Component"
+> & {
+ Component: NextAppProps["Component"] & {
+ requiresLicense?: boolean;
+ isThemeSupported?: boolean;
+ isBookingPage?: boolean | ((arg: { router: NextAppProps["router"] }) => boolean);
+ getLayout?: (page: React.ReactElement) => ReactNode;
+ PageWrapper?: (props: AppProps) => JSX.Element;
+ };
+
+ /** Will be defined only is there was an error */
+ err?: Error;
+};
+
+const getEmbedNamespace = (searchParams: ReadonlyURLSearchParams) => {
+ // Mostly embed query param should be available on server. Use that there.
+ // Use the most reliable detection on client
+ return typeof window !== "undefined" ? window.getEmbedNamespace() : searchParams.get("embed") ?? null;
+};
+
+// @ts-expect-error appWithTranslation expects AppProps
+const AppWithTranslationHoc = appWithTranslation(({ children }) => <>{children}>);
+
+const CustomI18nextProvider = (props: { children: React.ReactElement; i18n?: SSRConfig }) => {
+ /**
+ * i18n should never be clubbed with other queries, so that it's caching can be managed independently.
+ **/
+ // @TODO
+
+ const session = useSession();
+
+ // window.document.documentElement.lang can be empty in some cases, for instance when we rendering GlobalError (not-found) page.
+ const locale =
+ session?.data?.user.locale ?? typeof window !== "undefined"
+ ? window.document.documentElement.lang || "en"
+ : "en";
+
+ useEffect(() => {
+ try {
+ // @ts-expect-error TS2790: The operand of a 'delete' operator must be optional.
+ delete window.document.documentElement["lang"];
+
+ window.document.documentElement.lang = locale;
+
+ // Next.js writes the locale to the same attribute
+ // https://github.com/vercel/next.js/blob/1609da2d9552fed48ab45969bdc5631230c6d356/packages/next/src/shared/lib/router/router.ts#L1786
+ // which can result in a race condition
+ // this property descriptor ensures this never happens
+ Object.defineProperty(window.document.documentElement, "lang", {
+ configurable: true,
+ // value: locale,
+ set: function (this) {
+ // empty setter on purpose
+ },
+ get: function () {
+ return locale;
+ },
+ });
+ } catch (error) {
+ console.error(error);
+
+ window.document.documentElement.lang = locale;
+ }
+ window.document.dir = dir(locale);
+ }, [locale]);
+
+ const clientViewerI18n = useViewerI18n(locale);
+ const i18n = clientViewerI18n.data?.i18n ?? props.i18n;
+
+ return (
+ // @ts-expect-error AppWithTranslationHoc expects AppProps
+
+ {props.children}
+
+ );
+};
+
+const enum ThemeSupport {
+ // e.g. Login Page
+ None = "none",
+ // Entire App except Booking Pages
+ App = "systemOnly",
+ // Booking Pages(including Routing Forms)
+ Booking = "userConfigured",
+}
+
+type CalcomThemeProps = Readonly<{
+ isBookingPage: boolean;
+ themeBasis: string | null;
+ nonce: string | undefined;
+ isThemeSupported: boolean;
+ children: React.ReactNode;
+}>;
+
+const CalcomThemeProvider = (props: CalcomThemeProps) => {
+ // Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
+ // One such example is our Embeds Demo and Testing page at http://localhost:3100
+ // Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
+
+ const searchParams = useSearchParams();
+ const embedNamespace = searchParams ? getEmbedNamespace(searchParams) : null;
+ const isEmbedMode = typeof embedNamespace === "string";
+
+ return (
+
+ {/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
+ {/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
+ {typeof window !== "undefined" && !isEmbedMode && (
+
+ )}
+ {props.children}
+
+ );
+};
+
+/**
+ * The most important job for this fn is to generate correct storageKey for theme persistenc.
+ * `storageKey` is important because that key is listened for changes(using [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event) and any pages opened will change it's theme based on that(as part of next-themes implementation).
+ * Choosing the right storageKey avoids theme flickering caused by another page using different theme
+ * So, we handle all the cases here namely,
+ * - Both Booking Pages, /free/30min and /pro/30min but configured with different themes but being operated together.
+ * - Embeds using different namespace. They can be completely themed different on the same page.
+ * - Embeds using the same namespace but showing different cal.com links with different themes
+ * - Embeds using the same namespace and showing same cal.com links with different themes(Different theme is possible for same cal.com link in case of embed because of theme config available in embed)
+ * - App has different theme then Booking Pages.
+ *
+ * All the above cases have one thing in common, which is the origin and thus localStorage is shared and thus `storageKey` is critical to avoid theme flickering.
+ *
+ * Some things to note:
+ * - There is a side effect of so many factors in `storageKey` that many localStorage keys will be created if a user goes through all these scenarios(e.g like booking a lot of different users)
+ * - Some might recommend disabling localStorage persistence but that doesn't give good UX as then we would default to light theme always for a few seconds before switching to dark theme(if that's the user's preference).
+ * - We can't disable [`storage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/storage_event) event handling as well because changing theme in one tab won't change the theme without refresh in other tabs. That's again a bad UX
+ * - Theme flickering becomes infinitely ongoing in case of embeds because of the browser's delay in processing `storage` event within iframes. Consider two embeds simulatenously opened with pages A and B. Note the timeline and keep in mind that it happened
+ * because 'setItem(A)' and 'Receives storageEvent(A)' allowed executing setItem(B) in b/w because of the delay.
+ * - t1 -> setItem(A) & Fires storageEvent(A) - On Page A) - Current State(A)
+ * - t2 -> setItem(B) & Fires storageEvent(B) - On Page B) - Current State(B)
+ * - t3 -> Receives storageEvent(A) & thus setItem(A) & thus fires storageEvent(A) (On Page B) - Current State(A)
+ * - t4 -> Receives storageEvent(B) & thus setItem(B) & thus fires storageEvent(B) (On Page A) - Current State(B)
+ * - ... and so on ...
+ */
+function getThemeProviderProps(props: {
+ isBookingPage: boolean;
+ themeBasis: string | null;
+ nonce: string | undefined;
+ isEmbedMode: boolean;
+ embedNamespace: string | null;
+ isThemeSupported: boolean;
+}) {
+ const themeSupport = props.isBookingPage
+ ? ThemeSupport.Booking
+ : // if isThemeSupported is explicitly false, we don't use theme there
+ props.isThemeSupported === false
+ ? ThemeSupport.None
+ : ThemeSupport.App;
+
+ const isBookingPageThemeSupportRequired = themeSupport === ThemeSupport.Booking;
+
+ if ((isBookingPageThemeSupportRequired || props.isEmbedMode) && !props.themeBasis) {
+ console.warn(
+ "`themeBasis` is required for booking page theme support. Not providing it will cause theme flicker."
+ );
+ }
+
+ const appearanceIdSuffix = props.themeBasis ? `:${props.themeBasis}` : "";
+ const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
+ let embedExplicitlySetThemeSuffix = "";
+
+ if (typeof window !== "undefined") {
+ const embedTheme = window.getEmbedTheme();
+ if (embedTheme) {
+ embedExplicitlySetThemeSuffix = `:${embedTheme}`;
+ }
+ }
+
+ const storageKey = props.isEmbedMode
+ ? // Same Namespace, Same Organizer but different themes would still work seamless and not cause theme flicker
+ // Even though it's recommended to use different namespaces when you want to theme differently on the same page but if the embeds are on different pages, the problem can still arise
+ `embed-theme-${props.embedNamespace}${appearanceIdSuffix}${embedExplicitlySetThemeSuffix}`
+ : themeSupport === ThemeSupport.App
+ ? "app-theme"
+ : isBookingPageThemeSupportRequired
+ ? `booking-theme${appearanceIdSuffix}`
+ : undefined;
+
+ return {
+ storageKey,
+ forcedTheme,
+ themeSupport,
+ nonce: props.nonce,
+ enableColorScheme: false,
+ enableSystem: themeSupport !== ThemeSupport.None,
+ // next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
+ // This is how login to dashboard soft navigation changes theme from light to dark
+ key: storageKey,
+ attribute: "class",
+ };
+}
+
+function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
+ const flags = useFlags();
+ return {children} ;
+}
+
+function useOrgBrandingValues() {
+ const session = useSession();
+ return session?.data?.user.org;
+}
+
+function OrgBrandProvider({ children }: { children: React.ReactNode }) {
+ const orgBrand = useOrgBrandingValues();
+ return {children} ;
+}
+
+const AppProviders = (props: PageWrapperProps) => {
+ // No need to have intercom on public pages - Good for Page Performance
+ const isBookingPage = useIsBookingPage();
+
+ const RemainingProviders = (
+
+
+
+
+
+ {/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
+
+
+
+ {props.children}
+
+
+
+
+
+
+
+
+ );
+
+ if (isBookingPage) {
+ return RemainingProviders;
+ }
+
+ return (
+
+ {RemainingProviders}
+
+ );
+};
+
+export default AppProviders;
+import type { GetServerSidePropsContext } from "next";
+import { z } from "zod";
+
+import { AppCategories } from "@calcom/prisma/enums";
+
+export type querySchemaType = z.infer;
+
+export const querySchema = z.object({
+ category: z.nativeEnum(AppCategories),
+});
+
+export async function getServerSideProps(ctx: GetServerSidePropsContext) {
+ // get return-to cookie and redirect if needed
+ const { cookies } = ctx.req;
+
+ const returnTo = cookies["return-to"];
+
+ if (cookies && returnTo) {
+ ctx.res.setHeader("Set-Cookie", "return-to=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
+ const redirect = {
+ redirect: {
+ destination: `${returnTo}`,
+ permanent: false,
+ },
+ } as const;
+
+ return redirect;
+ }
+
+ const params = querySchema.safeParse(ctx.params);
+
+ if (!params.success) {
+ const notFound = { notFound: true } as const;
+
+ return notFound;
+ }
+
+ return {
+ props: {
+ category: params.data.category,
+ },
+ };
+}
+export async function getServerSideProps() {
+ return { redirect: { permanent: false, destination: "/apps/installed/calendar" } };
+}
+import type { GetServerSidePropsContext } from "next";
+
+import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import getUserAdminTeams from "@calcom/features/ee/teams/lib/getUserAdminTeams";
+import type { UserAdminTeams } from "@calcom/features/ee/teams/lib/getUserAdminTeams";
+import type { AppCategories } from "@calcom/prisma/enums";
+
+import { ssrInit } from "@server/lib/ssr";
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const { req } = context;
+
+ const ssr = await ssrInit(context);
+
+ const session = await getServerSession({ req });
+
+ let appStore, userAdminTeams: UserAdminTeams;
+ if (session?.user?.id) {
+ userAdminTeams = await getUserAdminTeams({ userId: session.user.id, getUserInfo: true });
+ appStore = await getAppRegistryWithCredentials(session.user.id, userAdminTeams);
+ } else {
+ appStore = await getAppRegistry();
+ userAdminTeams = [];
+ }
+
+ const categoryQuery = appStore.map(({ categories }) => ({
+ categories: categories || [],
+ }));
+ const categories = categoryQuery.reduce((c, app) => {
+ for (const category of app.categories) {
+ c[category] = c[category] ? c[category] + 1 : 1;
+ }
+ return c;
+ }, {} as Record);
+
+ return {
+ props: {
+ categories: Object.entries(categories)
+ .map(([name, count]): { name: AppCategories; count: number } => ({
+ name: name as AppCategories,
+ count,
+ }))
+ .sort(function (a, b) {
+ return b.count - a.count;
+ }),
+ appStore,
+ userAdminTeams,
+ trpcState: ssr.dehydrate(),
+ },
+ };
+};
+import type { GetStaticPropsContext } from "next";
+
+import { getAppRegistry } from "@calcom/app-store/_appRegistry";
+import prisma from "@calcom/prisma";
+import type { AppCategories } from "@calcom/prisma/enums";
+
+export const getStaticProps = async (context: GetStaticPropsContext) => {
+ const category = context.params?.category as AppCategories;
+
+ const appQuery = await prisma.app.findMany({
+ where: {
+ categories: {
+ has: category,
+ },
+ },
+ select: {
+ slug: true,
+ },
+ });
+
+ const dbAppsSlugs = appQuery.map((category) => category.slug);
+
+ const appStore = await getAppRegistry();
+
+ const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug));
+ return {
+ props: {
+ apps,
+ },
+ };
+};
+import type { GetServerSidePropsContext } from "next";
+
+import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+
+import { ssrInit } from "@server/lib/ssr";
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const { req } = context;
+
+ const ssr = await ssrInit(context);
+
+ const session = await getServerSession({ req });
+
+ let appStore;
+ if (session?.user?.id) {
+ appStore = await getAppRegistryWithCredentials(session.user.id);
+ } else {
+ appStore = await getAppRegistry();
+ }
+
+ const categories = appStore.reduce((c, app) => {
+ for (const category of app.categories) {
+ c[category] = c[category] ? c[category] + 1 : 1;
+ }
+ return c;
+ }, {} as Record);
+
+ return {
+ props: {
+ categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
+ trpcState: ssr.dehydrate(),
+ },
+ };
+};
+import fs from "fs";
+import matter from "gray-matter";
+import MarkdownIt from "markdown-it";
+import type { GetStaticPropsContext } from "next";
+import path from "path";
+import { z } from "zod";
+
+import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
+import { getAppAssetFullPath } from "@calcom/app-store/getAppAssetFullPath";
+import { IS_PRODUCTION } from "@calcom/lib/constants";
+import prisma from "@calcom/prisma";
+
+const md = new MarkdownIt("default", { html: true, breaks: true });
+
+export const sourceSchema = z.object({
+ content: z.string(),
+ data: z.object({
+ description: z.string().optional(),
+ items: z
+ .array(
+ z.union([
+ z.string(),
+ z.object({
+ iframe: z.object({ src: z.string() }),
+ }),
+ ])
+ )
+ .optional(),
+ }),
+});
+
+export const getStaticProps = async (ctx: GetStaticPropsContext) => {
+ if (typeof ctx.params?.slug !== "string") return { notFound: true } as const;
+
+ const appMeta = await getAppWithMetadata({
+ slug: ctx.params?.slug,
+ });
+
+ const appFromDb = await prisma.app.findUnique({
+ where: { slug: ctx.params.slug.toLowerCase() },
+ });
+
+ const isAppAvailableInFileSystem = appMeta;
+ const isAppDisabled = isAppAvailableInFileSystem && (!appFromDb || !appFromDb.enabled);
+
+ if (!IS_PRODUCTION && isAppDisabled) {
+ return {
+ props: {
+ isAppDisabled: true as const,
+ data: {
+ ...appMeta,
+ },
+ },
+ };
+ }
+
+ if (!appFromDb || !appMeta || isAppDisabled) return { notFound: true } as const;
+
+ const isTemplate = appMeta.isTemplate;
+ const appDirname = path.join(isTemplate ? "templates" : "", appFromDb.dirName);
+ const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
+ const postFilePath = path.join(README_PATH);
+ let source = "";
+
+ try {
+ source = fs.readFileSync(postFilePath).toString();
+ source = source.replace(/{DESCRIPTION}/g, appMeta.description);
+ } catch (error) {
+ /* If the app doesn't have a README we fallback to the package description */
+ console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
+ source = appMeta.description;
+ }
+
+ const result = matter(source);
+ const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
+ if (data.items) {
+ data.items = data.items.map((item) => {
+ if (typeof item === "string") {
+ return getAppAssetFullPath(item, {
+ dirName: appMeta.dirName,
+ isTemplate: appMeta.isTemplate,
+ });
+ }
+ return item;
+ });
+ }
+ return {
+ props: {
+ isAppDisabled: false as const,
+ source: { content, data },
+ data: appMeta,
+ },
+ };
+};
+import type { GetServerSidePropsContext } from "next";
+import { z } from "zod";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { getBookingForReschedule, getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
+import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
+import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
+import slugify from "@calcom/lib/slugify";
+import prisma from "@calcom/prisma";
+
+import type { inferSSRProps } from "@lib/types/inferSSRProps";
+import type { EmbedProps } from "@lib/withEmbedSsr";
+
+export type PageProps = inferSSRProps & EmbedProps;
+
+async function getUserPageProps(context: GetServerSidePropsContext) {
+ const session = await getServerSession(context);
+ const { link, slug } = paramsSchema.parse(context.params);
+ const { rescheduleUid, duration: queryDuration } = context.query;
+ const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req);
+ const org = isValidOrgDomain ? currentOrgDomain : null;
+
+ const { ssrInit } = await import("@server/lib/ssr");
+ const ssr = await ssrInit(context);
+
+ const hashedLink = await prisma.hashedLink.findUnique({
+ where: {
+ link,
+ },
+ select: {
+ eventTypeId: true,
+ eventType: {
+ select: {
+ users: {
+ select: {
+ username: true,
+ },
+ },
+ team: {
+ select: {
+ id: true,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ const username = hashedLink?.eventType.users[0]?.username;
+
+ const notFound = {
+ notFound: true,
+ } as const;
+
+ if (!hashedLink || !username) {
+ return notFound;
+ }
+
+ const user = await prisma.user.findFirst({
+ where: {
+ username,
+ organization: isValidOrgDomain
+ ? {
+ slug: currentOrgDomain,
+ }
+ : null,
+ },
+ select: {
+ away: true,
+ hideBranding: true,
+ },
+ });
+
+ if (!user) {
+ return notFound;
+ }
+
+ let booking: GetBookingType | null = null;
+ if (rescheduleUid) {
+ booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
+ }
+
+ const isTeamEvent = !!hashedLink.eventType?.team?.id;
+
+ // We use this to both prefetch the query on the server,
+ // as well as to check if the event exist, so we c an show a 404 otherwise.
+ const eventData = await ssr.viewer.public.event.fetch({ username, eventSlug: slug, isTeamEvent, org });
+
+ if (!eventData) {
+ return notFound;
+ }
+
+ return {
+ props: {
+ entity: eventData.entity,
+ duration: getMultipleDurationValue(
+ eventData.metadata?.multipleDuration,
+ queryDuration,
+ eventData.length
+ ),
+ booking,
+ away: user?.away,
+ user: username,
+ slug,
+ trpcState: ssr.dehydrate(),
+ isBrandingHidden: user?.hideBranding,
+ // Sending the team event from the server, because this template file
+ // is reused for both team and user events.
+ isTeamEvent,
+ hashedLink: link,
+ },
+ };
+}
+
+const paramsSchema = z.object({ link: z.string(), slug: z.string().transform((s) => slugify(s)) });
+
+// Booker page fetches a tiny bit of data server side, to determine early
+// whether the page should show an away state or dynamic booking not allowed.
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ return await getUserPageProps(context);
+};
+import type { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from "next";
+
+import { WEBAPP_URL } from "@calcom/lib/constants";
+
+export type EmbedProps = {
+ isEmbed?: boolean;
+};
+
+export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
+ return async (context: GetServerSidePropsContext): Promise> => {
+ const ssrResponse = await getServerSideProps(context);
+ const embed = context.query.embed;
+ const layout = context.query.layout;
+
+ if ("redirect" in ssrResponse) {
+ const destinationUrl = ssrResponse.redirect.destination;
+ let urlPrefix = "";
+
+ // Get the URL parsed from URL so that we can reliably read pathname and searchParams from it.
+ const destinationUrlObj = new URL(ssrResponse.redirect.destination, WEBAPP_URL);
+
+ // If it's a complete URL, use the origin as the prefix to ensure we redirect to the same domain.
+ if (destinationUrl.search(/^(http:|https:).*/) !== -1) {
+ urlPrefix = destinationUrlObj.origin;
+ } else {
+ // Don't use any prefix for relative URLs to ensure we stay on the same domain
+ urlPrefix = "";
+ }
+
+ const destinationQueryStr = destinationUrlObj.searchParams.toString();
+ // Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
+ const newDestinationUrl = `${urlPrefix}${destinationUrlObj.pathname}/embed?${
+ destinationQueryStr ? `${destinationQueryStr}&` : ""
+ }layout=${layout}&embed=${embed}`;
+ return {
+ ...ssrResponse,
+ redirect: {
+ ...ssrResponse.redirect,
+ destination: newDestinationUrl,
+ },
+ };
+ }
+
+ if (!("props" in ssrResponse)) {
+ return ssrResponse;
+ }
+ return {
+ ...ssrResponse,
+ props: {
+ ...ssrResponse.props,
+ isEmbed: true,
+ },
+ };
+ };
+}
+export function isBrandingHidden(hideBrandingSetting: boolean, hasPaidPlan: boolean) {
+ return hasPaidPlan && hideBrandingSetting;
+}
+"use client";
+
+import { useAutoAnimate } from "@formkit/auto-animate/react";
+import { Fragment, useState } from "react";
+import { z } from "zod";
+
+import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
+import dayjs from "@calcom/dayjs";
+import { FilterToggle } from "@calcom/features/bookings/components/FilterToggle";
+import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer";
+import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery";
+import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
+import { ShellMain } from "@calcom/features/shell/Shell";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import { trpc } from "@calcom/trpc/react";
+import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
+import { Alert, Button, EmptyScreen, HorizontalTabs } from "@calcom/ui";
+import { Calendar } from "@calcom/ui/components/icon";
+
+import { useInViewObserver } from "@lib/hooks/useInViewObserver";
+import useMeQuery from "@lib/hooks/useMeQuery";
+
+import BookingListItem from "@components/booking/BookingListItem";
+import SkeletonLoader from "@components/booking/SkeletonLoader";
+
+import { validStatuses } from "~/bookings/lib/validStatuses";
+
+type BookingListingStatus = z.infer>["status"];
+type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
+
+type RecurringInfo = {
+ recurringEventId: string | null;
+ count: number;
+ firstDate: Date | null;
+ bookings: { [key: string]: Date[] };
+};
+
+const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
+ {
+ name: "upcoming",
+ href: "/bookings/upcoming",
+ },
+ {
+ name: "unconfirmed",
+ href: "/bookings/unconfirmed",
+ },
+ {
+ name: "recurring",
+ href: "/bookings/recurring",
+ },
+ {
+ name: "past",
+ href: "/bookings/past",
+ },
+ {
+ name: "cancelled",
+ href: "/bookings/cancelled",
+ },
+];
+
+const descriptionByStatus: Record, string> = {
+ upcoming: "upcoming_bookings",
+ recurring: "recurring_bookings",
+ past: "past_bookings",
+ cancelled: "cancelled_bookings",
+ unconfirmed: "unconfirmed_bookings",
+};
+
+const querySchema = z.object({
+ status: z.enum(validStatuses),
+});
+
+export default function Bookings() {
+ const params = useParamsWithFallback();
+ const { data: filterQuery } = useFilterQuery();
+ const { status } = params ? querySchema.parse(params) : { status: "upcoming" as const };
+ const { t } = useLocale();
+ const user = useMeQuery().data;
+ const [isFiltersVisible, setIsFiltersVisible] = useState(false);
+
+ const query = trpc.viewer.bookings.get.useInfiniteQuery(
+ {
+ limit: 10,
+ filters: {
+ ...filterQuery,
+ status: filterQuery.status ?? status,
+ },
+ },
+ {
+ // first render has status `undefined`
+ enabled: true,
+ getNextPageParam: (lastPage) => lastPage.nextCursor,
+ }
+ );
+
+ // Animate page (tab) transitions to look smoothing
+
+ const buttonInView = useInViewObserver(() => {
+ if (!query.isFetching && query.hasNextPage && query.status === "success") {
+ query.fetchNextPage();
+ }
+ });
+
+ const isEmpty = !query.data?.pages[0]?.bookings.length;
+
+ const shownBookings: Record = {};
+ const filterBookings = (booking: BookingOutput) => {
+ if (status === "recurring" || status == "unconfirmed" || status === "cancelled") {
+ if (!booking.recurringEventId) {
+ return true;
+ }
+ if (
+ shownBookings[booking.recurringEventId] !== undefined &&
+ shownBookings[booking.recurringEventId].length > 0
+ ) {
+ shownBookings[booking.recurringEventId].push(booking);
+ return false;
+ }
+ shownBookings[booking.recurringEventId] = [booking];
+ } else if (status === "upcoming") {
+ return (
+ dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") !==
+ dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
+ );
+ }
+ return true;
+ };
+
+ let recurringInfoToday: RecurringInfo | undefined;
+
+ const bookingsToday =
+ query.data?.pages.map((page) =>
+ page.bookings.filter((booking: BookingOutput) => {
+ recurringInfoToday = page.recurringInfo.find(
+ (info) => info.recurringEventId === booking.recurringEventId
+ );
+
+ return (
+ dayjs(booking.startTime).tz(user?.timeZone).format("YYYY-MM-DD") ===
+ dayjs().tz(user?.timeZone).format("YYYY-MM-DD")
+ );
+ })
+ )[0] || [];
+
+ const [animationParentRef] = useAutoAnimate();
+
+ return (
+
+
+
+
+
+
+
+
+
+ {query.status === "error" && (
+
+ )}
+ {(query.status === "pending" || query.isPaused) &&
}
+ {query.status === "success" && !isEmpty && (
+ <>
+ {!!bookingsToday.length && status === "upcoming" && (
+
+
+
{t("today")}
+
+
+
+
+ {bookingsToday.map((booking: BookingOutput) => (
+
+ ))}
+
+
+
+
+
+ )}
+
+
+
+
+ {query.data.pages.map((page, index) => (
+
+ {page.bookings.filter(filterBookings).map((booking: BookingOutput) => {
+ const recurringInfo = page.recurringInfo.find(
+ (info) => info.recurringEventId === booking.recurringEventId
+ );
+ return (
+
+ );
+ })}
+
+ ))}
+
+
+
+
+ query.fetchNextPage()}>
+ {query.hasNextPage ? t("load_more_results") : t("no_more_results")}
+
+
+
+ >
+ )}
+ {query.status === "success" && isEmpty && (
+
+
+
+ )}
+
+
+
+
+ );
+}
+import { type GetStaticProps } from "next";
+import { z } from "zod";
+
+import { getTranslations } from "@server/lib/getTranslations";
+
+import { validStatuses } from "~/bookings/lib/validStatuses";
+
+const querySchema = z.object({
+ status: z.enum(validStatuses),
+});
+
+export const getStaticProps: GetStaticProps = async (ctx) => {
+ const params = querySchema.safeParse(ctx.params);
+ const i18n = await getTranslations(ctx);
+
+ if (!params.success) return { notFound: true };
+
+ return {
+ props: {
+ status: params.data.status,
+ i18n,
+ },
+ };
+};
+"use client";
+
+import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
+import classNames from "classnames";
+import { createEvent } from "ics";
+import { useSession } from "next-auth/react";
+import Link from "next/link";
+import { usePathname, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import { RRule } from "rrule";
+import { z } from "zod";
+
+import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
+import type { getEventLocationValue } from "@calcom/app-store/locations";
+import { getSuccessPageLocationMessage, guessEventLocationType } from "@calcom/app-store/locations";
+import { getEventTypeAppData } from "@calcom/app-store/utils";
+import type { nameObjectSchema } from "@calcom/core/event";
+import { getEventName } from "@calcom/core/event";
+import type { ConfigType } from "@calcom/dayjs";
+import dayjs from "@calcom/dayjs";
+import {
+ sdkActionManager,
+ useEmbedNonStylesConfig,
+ useIsBackgroundTransparent,
+ useIsEmbed,
+} from "@calcom/embed-core/embed-iframe";
+import { Price } from "@calcom/features/bookings/components/event-meta/Price";
+import { SMS_REMINDER_NUMBER_FIELD, SystemField } from "@calcom/features/bookings/lib/SystemField";
+import { APP_NAME } from "@calcom/lib/constants";
+import {
+ formatToLocalizedDate,
+ formatToLocalizedTime,
+ formatToLocalizedTimezone,
+} from "@calcom/lib/date-fns";
+import useGetBrandingColours from "@calcom/lib/getBrandColours";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
+import useTheme from "@calcom/lib/hooks/useTheme";
+import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
+import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
+import { localStorage } from "@calcom/lib/webstorage";
+import { BookingStatus } from "@calcom/prisma/enums";
+import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
+import { Alert, Badge, Button, EmailInput, HeadSeo, useCalcomTheme } from "@calcom/ui";
+import { AlertCircle, Calendar, Check, ChevronLeft, ExternalLink, X } from "@calcom/ui/components/icon";
+
+import { timeZone } from "@lib/clock";
+
+import PageWrapper from "@components/PageWrapper";
+import CancelBooking from "@components/booking/CancelBooking";
+import EventReservationSchema from "@components/schemas/EventReservationSchema";
+
+import type { PageProps } from "./bookings-single-view.getServerSideProps";
+
+const stringToBoolean = z
+ .string()
+ .optional()
+ .transform((val) => val === "true");
+
+const querySchema = z.object({
+ uid: z.string(),
+ email: z.string().optional(),
+ eventTypeSlug: z.string().optional(),
+ cancel: stringToBoolean,
+ allRemainingBookings: stringToBoolean,
+ changes: stringToBoolean,
+ reschedule: stringToBoolean,
+ isSuccessBookingPage: stringToBoolean,
+ formerTime: z.string().optional(),
+ seatReferenceUid: z.string().optional(),
+});
+
+const useBrandColors = ({
+ brandColor,
+ darkBrandColor,
+}: {
+ brandColor?: string | null;
+ darkBrandColor?: string | null;
+}) => {
+ const brandTheme = useGetBrandingColours({
+ lightVal: brandColor,
+ darkVal: darkBrandColor,
+ });
+ useCalcomTheme(brandTheme);
+};
+
+export default function Success(props: PageProps) {
+ const { t } = useLocale();
+ const router = useRouter();
+ const routerQuery = useRouterQuery();
+ const pathname = usePathname();
+ const searchParams = useCompatSearchParams();
+ const { eventType, bookingInfo, requiresLoginToUpdate } = props;
+ const {
+ allRemainingBookings,
+ isSuccessBookingPage,
+ cancel: isCancellationMode,
+ formerTime,
+ email,
+ seatReferenceUid,
+ } = querySchema.parse(routerQuery);
+
+ const attendeeTimeZone = bookingInfo?.attendees.find((attendee) => attendee.email === email)?.timeZone;
+
+ const tz = props.tz ? props.tz : isSuccessBookingPage && attendeeTimeZone ? attendeeTimeZone : timeZone();
+
+ const location = bookingInfo.location as ReturnType;
+ let rescheduleLocation: string | undefined;
+ if (
+ typeof bookingInfo.responses?.location === "object" &&
+ "optionValue" in bookingInfo.responses.location
+ ) {
+ rescheduleLocation = bookingInfo.responses.location.optionValue;
+ }
+
+ const locationVideoCallUrl: string | undefined = bookingMetadataSchema.parse(
+ bookingInfo?.metadata || {}
+ )?.videoCallUrl;
+
+ const status = bookingInfo?.status;
+ const reschedule = bookingInfo.status === BookingStatus.ACCEPTED;
+ const cancellationReason = bookingInfo.cancellationReason || bookingInfo.rejectionReason;
+
+ const attendees = bookingInfo?.attendees;
+
+ const isGmail = !!attendees.find((attendee) => attendee.email.includes("gmail.com"));
+
+ const [is24h, setIs24h] = useState(
+ props?.userTimeFormat ? props.userTimeFormat === 24 : isBrowserLocale24h()
+ );
+ const { data: session } = useSession();
+
+ const [date, setDate] = useState(dayjs.utc(bookingInfo.startTime));
+
+ const isBackgroundTransparent = useIsBackgroundTransparent();
+ const isEmbed = useIsEmbed();
+ const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
+ const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
+ const [calculatedDuration, setCalculatedDuration] = useState(undefined);
+
+ function setIsCancellationMode(value: boolean) {
+ const _searchParams = new URLSearchParams(searchParams ?? undefined);
+
+ if (value) {
+ _searchParams.set("cancel", "true");
+ } else {
+ if (_searchParams.get("cancel")) {
+ _searchParams.delete("cancel");
+ }
+ }
+
+ router.replace(`${pathname}?${_searchParams.toString()}`);
+ }
+
+ let evtName = eventType.eventName;
+ if (eventType.isDynamic && bookingInfo.responses?.title) {
+ evtName = bookingInfo.responses.title as string;
+ }
+ const eventNameObject = {
+ attendeeName: bookingInfo.responses.name as z.infer | string,
+ eventType: eventType.title,
+ eventName: evtName,
+ host: props.profile.name || "Nameless",
+ location: location,
+ bookingFields: bookingInfo.responses,
+ t,
+ };
+
+ const giphyAppData = getEventTypeAppData(eventType, "giphy");
+ const giphyImage = giphyAppData?.thankYouPage;
+
+ const eventName = getEventName(eventNameObject, true);
+ // Confirmation can be needed in two cases as of now
+ // - Event Type has require confirmation option enabled always
+ // - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
+ // - It's a paid event and payment is pending.
+ const needsConfirmation = bookingInfo.status === BookingStatus.PENDING && eventType.requiresConfirmation;
+ const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
+ const isLoggedIn = session?.user;
+ const isCancelled =
+ status === "CANCELLED" ||
+ status === "REJECTED" ||
+ (!!seatReferenceUid &&
+ !bookingInfo.seatsReferences.some((reference) => reference.referenceUid === seatReferenceUid));
+
+ // const telemetry = useTelemetry();
+ /* useEffect(() => {
+ if (top !== window) {
+ //page_view will be collected automatically by _middleware.ts
+ telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/booking"));
+ }
+ }, [telemetry]); */
+
+ useEffect(() => {
+ const users = eventType.users;
+ if (!sdkActionManager) return;
+ // TODO: We should probably make it consistent with Webhook payload. Some data is not available here, as and when requirement comes we can add
+ sdkActionManager.fire("bookingSuccessful", {
+ booking: bookingInfo,
+ eventType,
+ date: date.toString(),
+ duration: calculatedDuration,
+ organizer: {
+ name: users[0].name || "Nameless",
+ email: users[0].email || "Email-less",
+ timeZone: users[0].timeZone,
+ },
+ confirmed: !needsConfirmation,
+ // TODO: Add payment details
+ });
+ setDate(
+ date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess() || "Europe/London")
+ );
+ setIs24h(props?.userTimeFormat ? props.userTimeFormat === 24 : !!getIs24hClockFromLocalStorage());
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [eventType, needsConfirmation]);
+
+ useEffect(() => {
+ setCalculatedDuration(dayjs(bookingInfo.endTime).diff(dayjs(bookingInfo.startTime), "minutes"));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ function eventLink(): string {
+ const optional: { location?: string } = {};
+ if (locationVideoCallUrl) {
+ optional["location"] = locationVideoCallUrl;
+ }
+
+ const event = createEvent({
+ start: [
+ date.toDate().getUTCFullYear(),
+ (date.toDate().getUTCMonth() as number) + 1,
+ date.toDate().getUTCDate(),
+ date.toDate().getUTCHours(),
+ date.toDate().getUTCMinutes(),
+ ],
+ startInputType: "utc",
+ title: eventName,
+ description: eventType.description ? eventType.description : undefined,
+ /** formatted to required type of description ^ */
+ duration: {
+ minutes: calculatedDuration,
+ },
+ ...optional,
+ });
+
+ if (event.error) {
+ throw event.error;
+ }
+
+ return encodeURIComponent(event.value ? event.value : false);
+ }
+
+ function getTitle(): string {
+ const titleSuffix = props.recurringBookings ? "_recurring" : "";
+ if (isCancelled) {
+ return "";
+ }
+ if (needsConfirmation) {
+ if (props.profile.name !== null) {
+ return t(`user_needs_to_confirm_or_reject_booking${titleSuffix}`, {
+ user: props.profile.name,
+ });
+ }
+ return t(`needs_to_be_confirmed_or_rejected${titleSuffix}`);
+ }
+ return t(`emailed_you_and_attendees${titleSuffix}`);
+ }
+
+ // This is a weird case where the same route can be opened in booking flow as a success page or as a booking detail page from the app
+ // As Booking Page it has to support configured theme, but as booking detail page it should not do any change. Let Shell.tsx handle it.
+ useTheme(isSuccessBookingPage ? props.profile.theme : "system");
+ useBrandColors({
+ brandColor: props.profile.brandColor,
+ darkBrandColor: props.profile.darkBrandColor,
+ });
+ const title = t(
+ `booking_${needsConfirmation ? "submitted" : "confirmed"}${props.recurringBookings ? "_recurring" : ""}`
+ );
+
+ const locationToDisplay = getSuccessPageLocationMessage(
+ locationVideoCallUrl ? locationVideoCallUrl : location,
+ t,
+ bookingInfo.status
+ );
+
+ const rescheduleLocationToDisplay = getSuccessPageLocationMessage(
+ rescheduleLocation ?? "",
+ t,
+ bookingInfo.status
+ );
+
+ const providerName = guessEventLocationType(location)?.label;
+ const rescheduleProviderName = guessEventLocationType(rescheduleLocation)?.label;
+
+ return (
+
+ {!isEmbed && (
+
+ )}
+ {isLoggedIn && !isEmbed && (
+
+
+ {t("back_to_bookings")}
+
+
+ )}
+
+
+
+
+
+
+
+
+ {giphyImage && !needsConfirmation && !isCancelled && (
+ // eslint-disable-next-line @next/next/no-img-element
+
+ )}
+ {!giphyImage && !needsConfirmation && !isCancelled && (
+
+ )}
+ {needsConfirmation && !isCancelled &&
}
+ {isCancelled &&
}
+
+
+
+ {needsConfirmation && !isCancelled
+ ? props.recurringBookings
+ ? t("booking_submitted_recurring")
+ : t("booking_submitted")
+ : isCancelled
+ ? seatReferenceUid
+ ? t("no_longer_attending")
+ : t("event_cancelled")
+ : props.recurringBookings
+ ? t("meeting_is_scheduled_recurring")
+ : t("meeting_is_scheduled")}
+
+
+ {props.paymentStatus &&
+ (bookingInfo.status === BookingStatus.CANCELLED ||
+ bookingInfo.status === BookingStatus.REJECTED) && (
+
+ {!props.paymentStatus.success &&
+ !props.paymentStatus.refunded &&
+ t("booking_with_payment_cancelled")}
+ {props.paymentStatus.success &&
+ !props.paymentStatus.refunded &&
+ t("booking_with_payment_cancelled_already_paid")}
+ {props.paymentStatus.refunded && t("booking_with_payment_cancelled_refunded")}
+
+ )}
+
+
+ {(isCancelled || reschedule) && cancellationReason && (
+ <>
+
+ {isCancelled ? t("reason") : t("reschedule_reason")}
+
+
{cancellationReason}
+ >
+ )}
+
{t("what")}
+
+ {eventName}
+
+
{t("when")}
+
+ {reschedule && !!formerTime && (
+
+
+
+ )}
+
+
+ {(bookingInfo?.user || bookingInfo?.attendees) && (
+ <>
+
{t("who")}
+
+ {bookingInfo?.user && (
+
+
+
+ {bookingInfo.user.name}
+
+ {t("Host")}
+
+
{bookingInfo.user.email}
+
+ )}
+ {bookingInfo?.attendees.map((attendee) => (
+
+ {attendee.name && (
+
{attendee.name}
+ )}
+
{attendee.email}
+
+ ))}
+
+ >
+ )}
+ {locationToDisplay && !isCancelled && (
+ <>
+
{t("where")}
+
+ {!rescheduleLocation || locationToDisplay === rescheduleLocationToDisplay ? (
+
+ ) : (
+ <>
+ {!!formerTime && (
+
+ )}
+
+
+ >
+ )}
+
+ >
+ )}
+ {props.paymentStatus && (
+ <>
+
+ {props.paymentStatus.paymentOption === "HOLD"
+ ? t("complete_your_booking")
+ : t("payment")}
+
+
+ >
+ )}
+ {bookingInfo?.description && (
+ <>
+
{t("additional_notes")}
+
+
{bookingInfo.description}
+
+ >
+ )}
+
+
+ {Object.entries(bookingInfo.responses).map(([name, response]) => {
+ const field = eventType.bookingFields.find((field) => field.name === name);
+ // We show location in the "where" section
+ // We show Booker Name, Emails and guests in Who section
+ // We show notes in additional notes section
+ // We show rescheduleReason at the top
+ if (!field) return null;
+ const isSystemField = SystemField.safeParse(field.name);
+ // SMS_REMINDER_NUMBER_FIELD is a system field but doesn't have a dedicated place in the UI. So, it would be shown through the following responses list
+ if (isSystemField.success && field.name !== SMS_REMINDER_NUMBER_FIELD) return null;
+
+ const label = field.label || t(field.defaultLabel || "");
+
+ return (
+ <>
+
{label}
+
+ {field.type === "boolean" ? (response ? t("yes") : t("no")) : response.toString()}
+
+ >
+ );
+ })}
+
+
+ {requiresLoginToUpdate && (
+ <>
+
+
+ {t("need_to_make_a_change")}
+ {/* Login button but redirect to here */}
+
+
+
+ {t("login")}
+
+
+
+
+ >
+ )}
+ {!requiresLoginToUpdate &&
+ (!needsConfirmation || !userIsOwner) &&
+ !isCancelled &&
+ (!isCancellationMode ? (
+ <>
+
+
+ {t("need_to_make_a_change")}
+
+ <>
+ {!props.recurringBookings && (
+
+
+
+ {t("reschedule")}
+
+
+ {t("or_lowercase")}
+
+ )}
+
+ setIsCancellationMode(true)}>
+ {t("cancel")}
+
+ >
+
+ >
+ ) : (
+ <>
+
+
+ >
+ ))}
+ {userIsOwner &&
+ !needsConfirmation &&
+ !isCancellationMode &&
+ !isCancelled &&
+ !!calculatedDuration && (
+ <>
+
+
+
+ {t("add_to_calendar")}
+
+
+
+
+ Google
+
+
+
+
+
+ Microsoft Outlook
+
+
+
+
+
+ Microsoft Office
+
+
+
+
+
+ {t("other")}
+
+
+
+
+
+ >
+ )}
+
+ {session === null && !(userIsOwner || props.hideBranding) && (
+ <>
+
+
+ >
+ )}
+
+ {isGmail && (
+
+ {t("google_new_spam_policy")}
+
+
+ {t("resolve")}
+
+
+
+ }
+ CustomIcon={AlertCircle}
+ customIconColor="text-attention dark:text-orange-200"
+ />
+ )}
+
+
+
+
+
+ );
+}
+
+const DisplayLocation = ({
+ locationToDisplay,
+ providerName,
+ className,
+}: {
+ locationToDisplay: string;
+ providerName?: string;
+ className?: string;
+}) =>
+ locationToDisplay.startsWith("http") ? (
+
+ {providerName || "Link"}
+
+
+ ) : (
+ {locationToDisplay}
+ );
+
+Success.isBookingPage = true;
+Success.PageWrapper = PageWrapper;
+
+type RecurringBookingsProps = {
+ eventType: PageProps["eventType"];
+ recurringBookings: PageProps["recurringBookings"];
+ date: dayjs.Dayjs;
+ duration: number | undefined;
+ is24h: boolean;
+ allRemainingBookings: boolean;
+ isCancelled: boolean;
+ tz: string;
+};
+
+function RecurringBookings({
+ eventType,
+ recurringBookings,
+ duration,
+ date,
+ allRemainingBookings,
+ is24h,
+ isCancelled,
+ tz,
+}: RecurringBookingsProps) {
+ const [moreEventsVisible, setMoreEventsVisible] = useState(false);
+ const {
+ t,
+ i18n: { language },
+ } = useLocale();
+ const recurringBookingsSorted = recurringBookings
+ ? recurringBookings.sort((a: ConfigType, b: ConfigType) => (dayjs(a).isAfter(dayjs(b)) ? 1 : -1))
+ : null;
+
+ if (!duration) return null;
+
+ if (recurringBookingsSorted && allRemainingBookings) {
+ return (
+ <>
+ {eventType.recurringEvent?.count && (
+
+ {getEveryFreqFor({
+ t,
+ recurringEvent: eventType.recurringEvent,
+ recurringCount: recurringBookings?.length ?? undefined,
+ })}
+
+ )}
+ {eventType.recurringEvent?.count &&
+ recurringBookingsSorted.slice(0, 4).map((dateStr: string, idx: number) => (
+
+ {formatToLocalizedDate(dayjs.tz(dateStr, tz), language, "full", tz)}
+
+ {formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h, tz)} -{" "}
+ {formatToLocalizedTime(dayjs(dateStr).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
+
+ ({formatToLocalizedTimezone(dayjs(dateStr), language, tz)})
+
+
+ ))}
+ {recurringBookingsSorted.length > 4 && (
+ setMoreEventsVisible(!moreEventsVisible)}>
+
+ + {t("plus_more", { count: recurringBookingsSorted.length - 4 })}
+
+
+ {eventType.recurringEvent?.count &&
+ recurringBookingsSorted.slice(4).map((dateStr: string, idx: number) => (
+
+ {formatToLocalizedDate(dayjs.tz(dateStr, tz), language, "full", tz)}
+
+ {formatToLocalizedTime(dayjs(dateStr), language, undefined, !is24h, tz)} -{" "}
+ {formatToLocalizedTime(
+ dayjs(dateStr).add(duration, "m"),
+ language,
+ undefined,
+ !is24h,
+ tz
+ )}{" "}
+
+ ({formatToLocalizedTimezone(dayjs(dateStr), language, tz)})
+
+
+ ))}
+
+
+ )}
+ >
+ );
+ }
+
+ return (
+
+ {formatToLocalizedDate(date, language, "full", tz)}
+
+ {formatToLocalizedTime(date, language, undefined, !is24h, tz)} -{" "}
+ {formatToLocalizedTime(dayjs(date).add(duration, "m"), language, undefined, !is24h, tz)}{" "}
+ ({formatToLocalizedTimezone(date, language, tz)})
+
+ );
+}
+import type { GetServerSidePropsContext } from "next";
+import { z } from "zod";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { getBookingWithResponses } from "@calcom/features/bookings/lib/get-booking";
+import { parseRecurringEvent } from "@calcom/lib";
+import { getDefaultEvent } from "@calcom/lib/defaultEvents";
+import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
+import prisma from "@calcom/prisma";
+import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
+
+import type { inferSSRProps } from "@lib/types/inferSSRProps";
+
+import { ssrInit } from "@server/lib/ssr";
+
+const stringToBoolean = z
+ .string()
+ .optional()
+ .transform((val) => val === "true");
+
+const querySchema = z.object({
+ uid: z.string(),
+ email: z.string().optional(),
+ eventTypeSlug: z.string().optional(),
+ cancel: stringToBoolean,
+ allRemainingBookings: stringToBoolean,
+ changes: stringToBoolean,
+ reschedule: stringToBoolean,
+ isSuccessBookingPage: stringToBoolean,
+ formerTime: z.string().optional(),
+ seatReferenceUid: z.string().optional(),
+});
+
+export type PageProps = inferSSRProps;
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ // this is needed to prevent bundling of lib/booking to the client bundle
+ // usually functions that are used in getServerSideProps are tree shaken from client bundle
+ // but not in case when they are exported. So we have to dynamically load them, or to copy paste them to the /future/page.
+
+ const { getRecurringBookings, handleSeatsEventTypeOnBooking, getEventTypesFromDB } = await import(
+ "@lib/booking"
+ );
+
+ const ssr = await ssrInit(context);
+ const session = await getServerSession(context);
+ let tz: string | null = null;
+ let userTimeFormat: number | null = null;
+ let requiresLoginToUpdate = false;
+ if (session) {
+ const user = await ssr.viewer.me.fetch();
+ tz = user.timeZone;
+ userTimeFormat = user.timeFormat;
+ }
+
+ const parsedQuery = querySchema.safeParse(context.query);
+
+ if (!parsedQuery.success) return { notFound: true } as const;
+ const { eventTypeSlug } = parsedQuery.data;
+ let { uid, seatReferenceUid } = parsedQuery.data;
+
+ const maybeBookingUidFromSeat = await maybeGetBookingUidFromSeat(prisma, uid);
+ if (maybeBookingUidFromSeat.uid) uid = maybeBookingUidFromSeat.uid;
+ if (maybeBookingUidFromSeat.seatReferenceUid) seatReferenceUid = maybeBookingUidFromSeat.seatReferenceUid;
+ const bookingInfoRaw = await prisma.booking.findFirst({
+ where: {
+ uid: uid,
+ },
+ select: {
+ title: true,
+ id: true,
+ uid: true,
+ description: true,
+ customInputs: true,
+ smsReminderNumber: true,
+ recurringEventId: true,
+ startTime: true,
+ endTime: true,
+ location: true,
+ status: true,
+ metadata: true,
+ cancellationReason: true,
+ responses: true,
+ rejectionReason: true,
+ user: {
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ username: true,
+ timeZone: true,
+ },
+ },
+ attendees: {
+ select: {
+ name: true,
+ email: true,
+ timeZone: true,
+ },
+ },
+ eventTypeId: true,
+ eventType: {
+ select: {
+ eventName: true,
+ slug: true,
+ timeZone: true,
+ },
+ },
+ seatsReferences: {
+ select: {
+ referenceUid: true,
+ },
+ },
+ },
+ });
+ if (!bookingInfoRaw) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ const eventTypeRaw = !bookingInfoRaw.eventTypeId
+ ? getDefaultEvent(eventTypeSlug || "")
+ : await getEventTypesFromDB(bookingInfoRaw.eventTypeId);
+ if (!eventTypeRaw) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ if (eventTypeRaw.seatsPerTimeSlot && !seatReferenceUid && !session) {
+ requiresLoginToUpdate = true;
+ }
+
+ const bookingInfo = getBookingWithResponses(bookingInfoRaw);
+ // @NOTE: had to do this because Server side cant return [Object objects]
+ // probably fixable with json.stringify -> json.parse
+ bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
+ bookingInfo["endTime"] = (bookingInfo?.endTime as Date)?.toISOString() as unknown as Date;
+
+ eventTypeRaw.users = !!eventTypeRaw.hosts?.length
+ ? eventTypeRaw.hosts.map((host) => host.user)
+ : eventTypeRaw.users;
+
+ if (!eventTypeRaw.users.length) {
+ if (!eventTypeRaw.owner)
+ return {
+ notFound: true,
+ } as const;
+ eventTypeRaw.users.push({
+ ...eventTypeRaw.owner,
+ });
+ }
+
+ const eventType = {
+ ...eventTypeRaw,
+ periodStartDate: eventTypeRaw.periodStartDate?.toString() ?? null,
+ periodEndDate: eventTypeRaw.periodEndDate?.toString() ?? null,
+ metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata),
+ recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent),
+ customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
+ };
+
+ const profile = {
+ name: eventType.team?.name || eventType.users[0]?.name || null,
+ email: eventType.team ? null : eventType.users[0].email || null,
+ theme: (!eventType.team?.name && eventType.users[0]?.theme) || null,
+ brandColor: eventType.team ? null : eventType.users[0].brandColor || null,
+ darkBrandColor: eventType.team ? null : eventType.users[0].darkBrandColor || null,
+ slug: eventType.team?.slug || eventType.users[0]?.username || null,
+ };
+
+ if (bookingInfo !== null && eventType.seatsPerTimeSlot) {
+ await handleSeatsEventTypeOnBooking(
+ eventType,
+ bookingInfo,
+ seatReferenceUid,
+ session?.user.id === eventType.userId
+ );
+ }
+
+ const payment = await prisma.payment.findFirst({
+ where: {
+ bookingId: bookingInfo.id,
+ },
+ select: {
+ success: true,
+ refunded: true,
+ currency: true,
+ amount: true,
+ paymentOption: true,
+ },
+ });
+
+ return {
+ props: {
+ themeBasis: eventType.team ? eventType.team.slug : eventType.users[0]?.username,
+ hideBranding: eventType.team ? eventType.team.hideBranding : eventType.users[0].hideBranding,
+ profile,
+ eventType,
+ recurringBookings: await getRecurringBookings(bookingInfo.recurringEventId),
+ trpcState: ssr.dehydrate(),
+ dynamicEventName: bookingInfo?.eventType?.eventName || "",
+ bookingInfo,
+ paymentStatus: payment,
+ ...(tz && { tz }),
+ userTimeFormat,
+ requiresLoginToUpdate,
+ },
+ };
+}
+import type { GetServerSidePropsContext } from "next";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+
+import { asStringOrThrow } from "@lib/asStringOrNull";
+import type { inferSSRProps } from "@lib/types/inferSSRProps";
+
+import { ssrInit } from "@server/lib/ssr";
+
+export type PageProps = inferSSRProps;
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const { req, res, query } = context;
+
+ const session = await getServerSession({ req, res });
+
+ const typeParam = parseInt(asStringOrThrow(query.type));
+ const ssr = await ssrInit(context);
+
+ if (Number.isNaN(typeParam)) {
+ const notFound = {
+ notFound: true,
+ } as const;
+
+ return notFound;
+ }
+
+ if (!session?.user?.id) {
+ const redirect = {
+ redirect: {
+ permanent: false,
+ destination: "/auth/login",
+ },
+ } as const;
+ return redirect;
+ }
+
+ await ssr.viewer.eventTypes.get.prefetch({ id: typeParam });
+
+ const { eventType } = await ssr.viewer.eventTypes.get.fetch({ id: typeParam });
+
+ return {
+ props: {
+ eventType,
+ type: typeParam,
+ trpcState: ssr.dehydrate(),
+ },
+ };
+};
+"use client";
+
+/* eslint-disable @typescript-eslint/no-empty-function */
+import { useAutoAnimate } from "@formkit/auto-animate/react";
+import { zodResolver } from "@hookform/resolvers/zod";
+import { isValidPhoneNumber } from "libphonenumber-js";
+import dynamic from "next/dynamic";
+import { useEffect, useMemo, useState } from "react";
+import { useForm } from "react-hook-form";
+import { z } from "zod";
+
+import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps";
+import { getEventLocationType } from "@calcom/app-store/locations";
+import { validateCustomEventName } from "@calcom/core/event";
+import type { EventLocationType } from "@calcom/core/location";
+import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
+import { validateIntervalLimitOrder } from "@calcom/lib";
+import { CAL_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
+import { HttpError } from "@calcom/lib/http-error";
+import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
+import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
+import type { Prisma } from "@calcom/prisma/client";
+import type { PeriodType, SchedulingType } from "@calcom/prisma/enums";
+import type {
+ BookerLayoutSettings,
+ customInputSchema,
+ EventTypeMetaDataSchema,
+} from "@calcom/prisma/zod-utils";
+import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import { trpc } from "@calcom/trpc/react";
+import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
+import { Form, showToast } from "@calcom/ui";
+
+import type { AppProps } from "@lib/app-providers";
+
+import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
+import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayout";
+
+import { type PageProps } from "~/event-types/views/event-types-single-view.getServerSideProps";
+
+// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings;
+const EventSetupTab = dynamic(() =>
+ import("@components/eventtype/EventSetupTab").then((mod) => mod.EventSetupTab)
+);
+
+const EventAvailabilityTab = dynamic(() =>
+ import("@components/eventtype/EventAvailabilityTab").then((mod) => mod.EventAvailabilityTab)
+);
+
+const EventTeamTab = dynamic(() =>
+ import("@components/eventtype/EventTeamTab").then((mod) => mod.EventTeamTab)
+);
+
+const EventLimitsTab = dynamic(() =>
+ import("@components/eventtype/EventLimitsTab").then((mod) => mod.EventLimitsTab)
+);
+
+const EventAdvancedTab = dynamic(() =>
+ import("@components/eventtype/EventAdvancedTab").then((mod) => mod.EventAdvancedTab)
+);
+
+const EventInstantTab = dynamic(() =>
+ import("@components/eventtype/EventInstantTab").then((mod) => mod.EventInstantTab)
+);
+
+const EventRecurringTab = dynamic(() =>
+ import("@components/eventtype/EventRecurringTab").then((mod) => mod.EventRecurringTab)
+);
+
+const EventAppsTab = dynamic(() =>
+ import("@components/eventtype/EventAppsTab").then((mod) => mod.EventAppsTab)
+);
+
+const EventWorkflowsTab = dynamic(() => import("@components/eventtype/EventWorkfowsTab"));
+
+const EventWebhooksTab = dynamic(() =>
+ import("@components/eventtype/EventWebhooksTab").then((mod) => mod.EventWebhooksTab)
+);
+
+const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
+
+export type FormValues = {
+ id: number;
+ title: string;
+ eventTitle: string;
+ eventName: string;
+ slug: string;
+ isInstantEvent: boolean;
+ length: number;
+ offsetStart: number;
+ description: string;
+ disableGuests: boolean;
+ lockTimeZoneToggleOnBookingPage: boolean;
+ requiresConfirmation: boolean;
+ requiresBookerEmailVerification: boolean;
+ recurringEvent: RecurringEvent | null;
+ schedulingType: SchedulingType | null;
+ hidden: boolean;
+ hideCalendarNotes: boolean;
+ hashedLink: string | undefined;
+ locations: {
+ type: EventLocationType["type"];
+ address?: string;
+ attendeeAddress?: string;
+ link?: string;
+ hostPhoneNumber?: string;
+ displayLocationPublicly?: boolean;
+ phone?: string;
+ hostDefault?: string;
+ credentialId?: number;
+ teamName?: string;
+ }[];
+ customInputs: CustomInputParsed[];
+ schedule: number | null;
+ periodType: PeriodType;
+ periodDays: number;
+ periodCountCalendarDays: "1" | "0";
+ periodDates: { startDate: Date; endDate: Date };
+ seatsPerTimeSlot: number | null;
+ seatsShowAttendees: boolean | null;
+ seatsShowAvailabilityCount: boolean | null;
+ seatsPerTimeSlotEnabled: boolean;
+ scheduleName: string;
+ minimumBookingNotice: number;
+ minimumBookingNoticeInDurationType: number;
+ beforeEventBuffer: number;
+ afterEventBuffer: number;
+ slotInterval: number | null;
+ metadata: z.infer;
+ destinationCalendar: {
+ integration: string;
+ externalId: string;
+ };
+ successRedirectUrl: string;
+ durationLimits?: IntervalLimit;
+ bookingLimits?: IntervalLimit;
+ onlyShowFirstAvailableSlot: boolean;
+ children: ChildrenEventType[];
+ hosts: { userId: number; isFixed: boolean }[];
+ bookingFields: z.infer;
+ availability?: AvailabilityOption;
+ bookerLayouts: BookerLayoutSettings;
+ multipleDurationEnabled: boolean;
+ users: EventTypeSetup["users"];
+ assignAllTeamMembers: boolean;
+};
+
+export type CustomInputParsed = typeof customInputSchema._output;
+
+const querySchema = z.object({
+ tabName: z
+ .enum([
+ "setup",
+ "availability",
+ "apps",
+ "limits",
+ "instant",
+ "recurring",
+ "team",
+ "advanced",
+ "workflows",
+ "webhooks",
+ ])
+ .optional()
+ .default("setup"),
+});
+
+export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
+export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"];
+
+const EventTypePage = (props: EventTypeSetupProps) => {
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+ const telemetry = useTelemetry();
+ const {
+ data: { tabName },
+ } = useTypedQuery(querySchema);
+
+ const { data: eventTypeApps } = trpc.viewer.integrations.useQuery({
+ extendsFeature: "EventType",
+ teamId: props.eventType.team?.id || props.eventType.parent?.teamId,
+ onlyInstalled: true,
+ });
+
+ const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
+ const [animationParentRef] = useAutoAnimate();
+ const updateMutation = trpc.viewer.eventTypes.update.useMutation({
+ onSuccess: async () => {
+ formMethods.setValue(
+ "children",
+ formMethods.getValues().children.map((child) => ({
+ ...child,
+ created: true,
+ }))
+ );
+ formMethods.setValue("assignAllTeamMembers", formMethods.getValues("assignAllTeamMembers") || false);
+ showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
+ },
+ async onSettled() {
+ await utils.viewer.eventTypes.get.invalidate();
+ },
+ onError: (err) => {
+ let message = "";
+ if (err instanceof HttpError) {
+ const message = `${err.statusCode}: ${err.message}`;
+ showToast(message, "error");
+ }
+
+ if (err.data?.code === "UNAUTHORIZED") {
+ message = `${err.data.code}: ${t("error_event_type_unauthorized_update")}`;
+ }
+
+ if (err.data?.code === "PARSE_ERROR" || err.data?.code === "BAD_REQUEST") {
+ message = `${err.data.code}: ${t(err.message)}`;
+ }
+
+ if (err.data?.code === "INTERNAL_SERVER_ERROR") {
+ message = t("unexpected_error_try_again");
+ }
+
+ showToast(message ? t(message) : t(err.message), "error");
+ },
+ });
+
+ const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
+ startDate: new Date(eventType.periodStartDate || Date.now()),
+ endDate: new Date(eventType.periodEndDate || Date.now()),
+ });
+
+ const metadata = eventType.metadata;
+ // fallback to !!eventType.schedule when 'useHostSchedulesForTeamEvent' is undefined
+ if (!!team && metadata !== null) {
+ metadata.config = {
+ ...metadata.config,
+ useHostSchedulesForTeamEvent:
+ typeof eventType.metadata?.config?.useHostSchedulesForTeamEvent !== "undefined"
+ ? eventType.metadata?.config?.useHostSchedulesForTeamEvent === true
+ : !!eventType.schedule,
+ };
+ } else {
+ // Make sure non-team events NEVER have this config key;
+ delete metadata?.config?.useHostSchedulesForTeamEvent;
+ }
+
+ const bookingFields: Prisma.JsonObject = {};
+
+ eventType.bookingFields.forEach(({ name }) => {
+ bookingFields[name] = name;
+ });
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const defaultValues: any = useMemo(() => {
+ return {
+ title: eventType.title,
+ id: eventType.id,
+ slug: eventType.slug,
+ afterEventBuffer: eventType.afterEventBuffer,
+ beforeEventBuffer: eventType.beforeEventBuffer,
+ eventName: eventType.eventName || "",
+ scheduleName: eventType.scheduleName,
+ periodDays: eventType.periodDays || 30,
+ requiresBookerEmailVerification: eventType.requiresBookerEmailVerification,
+ seatsPerTimeSlot: eventType.seatsPerTimeSlot,
+ seatsShowAttendees: eventType.seatsShowAttendees,
+ seatsShowAvailabilityCount: eventType.seatsShowAvailabilityCount,
+ lockTimeZoneToggleOnBookingPage: eventType.lockTimeZoneToggleOnBookingPage,
+ locations: eventType.locations || [],
+ destinationCalendar: eventType.destinationCalendar,
+ recurringEvent: eventType.recurringEvent || null,
+ isInstantEvent: eventType.isInstantEvent,
+ description: eventType.description ?? undefined,
+ schedule: eventType.schedule || undefined,
+ bookingLimits: eventType.bookingLimits || undefined,
+ onlyShowFirstAvailableSlot: eventType.onlyShowFirstAvailableSlot || undefined,
+ durationLimits: eventType.durationLimits || undefined,
+ length: eventType.length,
+ hidden: eventType.hidden,
+ hashedLink: eventType.hashedLink?.link || undefined,
+ periodDates: {
+ startDate: periodDates.startDate,
+ endDate: periodDates.endDate,
+ },
+ hideCalendarNotes: eventType.hideCalendarNotes,
+ offsetStart: eventType.offsetStart,
+ bookingFields: eventType.bookingFields,
+ periodType: eventType.periodType,
+ periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
+ schedulingType: eventType.schedulingType,
+ requiresConfirmation: eventType.requiresConfirmation,
+ slotInterval: eventType.slotInterval,
+ minimumBookingNotice: eventType.minimumBookingNotice,
+ metadata,
+ hosts: eventType.hosts,
+ successRedirectUrl: eventType.successRedirectUrl || "",
+ users: eventType.users,
+ children: eventType.children.map((ch) => ({
+ ...ch,
+ created: true,
+ owner: {
+ ...ch.owner,
+ eventTypeSlugs:
+ eventType.team?.members
+ .find((mem) => mem.user.id === ch.owner.id)
+ ?.user.eventTypes.map((evTy) => evTy.slug)
+ .filter((slug) => slug !== eventType.slug) ?? [],
+ },
+ })),
+ seatsPerTimeSlotEnabled: eventType.seatsPerTimeSlot,
+ assignAllTeamMembers: eventType.assignAllTeamMembers,
+ };
+ }, [eventType, periodDates, metadata]);
+
+ const formMethods = useForm({
+ defaultValues,
+ resolver: zodResolver(
+ z
+ .object({
+ // Length if string, is converted to a number or it can be a number
+ // Make it optional because it's not submitted from all tabs of the page
+ eventName: z
+ .string()
+ .refine(
+ (val) =>
+ validateCustomEventName(val, t("invalid_event_name_variables"), bookingFields) === true,
+ {
+ message: t("invalid_event_name_variables"),
+ }
+ )
+ .optional(),
+ length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
+ offsetStart: z.union([z.string().transform((val) => +val), z.number()]).optional(),
+ bookingFields: eventTypeBookingFields,
+ locations: z
+ .array(
+ z
+ .object({
+ type: z.string(),
+ address: z.string().optional(),
+ link: z.string().url().optional(),
+ phone: z
+ .string()
+ .refine((val) => isValidPhoneNumber(val))
+ .optional(),
+ hostPhoneNumber: z
+ .string()
+ .refine((val) => isValidPhoneNumber(val))
+ .optional(),
+ displayLocationPublicly: z.boolean().optional(),
+ credentialId: z.number().optional(),
+ teamName: z.string().optional(),
+ })
+ .passthrough()
+ .superRefine((val, ctx) => {
+ if (val?.link) {
+ const link = val.link;
+ const eventLocationType = getEventLocationType(val.type);
+ if (
+ eventLocationType &&
+ !eventLocationType.default &&
+ eventLocationType.linkType === "static" &&
+ eventLocationType.urlRegExp
+ ) {
+ const valid = z
+ .string()
+ .regex(new RegExp(eventLocationType.urlRegExp))
+ .safeParse(link).success;
+
+ if (!valid) {
+ const sampleUrl = eventLocationType.organizerInputPlaceholder;
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: [eventLocationType?.defaultValueVariable ?? "link"],
+ message: t("invalid_url_error_message", {
+ label: eventLocationType.label,
+ sampleUrl: sampleUrl ?? "https://cal.com",
+ }),
+ });
+ }
+ return;
+ }
+
+ const valid = z.string().url().optional().safeParse(link).success;
+
+ if (!valid) {
+ ctx.addIssue({
+ code: z.ZodIssueCode.custom,
+ path: [eventLocationType?.defaultValueVariable ?? "link"],
+ message: `Invalid URL`,
+ });
+ }
+ }
+ return;
+ })
+ )
+ .optional(),
+ })
+ // TODO: Add schema for other fields later.
+ .passthrough()
+ ),
+ });
+
+ useEffect(() => {
+ if (!formMethods.formState.isDirty) {
+ //TODO: What's the best way to sync the form with backend
+ formMethods.setValue("bookingFields", defaultValues.bookingFields);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [defaultValues]);
+
+ const appsMetadata = formMethods.getValues("metadata")?.apps;
+ const availability = formMethods.watch("availability");
+ let numberOfActiveApps = 0;
+
+ if (appsMetadata) {
+ numberOfActiveApps = Object.entries(appsMetadata).filter(
+ ([appId, appData]) =>
+ eventTypeApps?.items.find((app) => app.slug === appId)?.isInstalled && appData.enabled
+ ).length;
+ }
+
+ const permalink = `${CAL_URL}/${team ? `team/${team.slug}` : eventType.users[0].username}/${
+ eventType.slug
+ }`;
+
+ const tabMap = {
+ setup: (
+
+ ),
+ availability: ,
+ team: ,
+ limits: ,
+ advanced: ,
+ instant: ,
+ recurring: ,
+ apps: ,
+ workflows: (
+ workflowOnEventType.workflow)}
+ />
+ ),
+ webhooks: ,
+ } as const;
+
+ const handleSubmit = async (values: FormValues) => {
+ const {
+ periodDates,
+ periodCountCalendarDays,
+ beforeEventBuffer,
+ afterEventBuffer,
+ seatsPerTimeSlot,
+ seatsShowAttendees,
+ seatsShowAvailabilityCount,
+ bookingLimits,
+ onlyShowFirstAvailableSlot,
+ durationLimits,
+ recurringEvent,
+ locations,
+ metadata,
+ customInputs,
+ children,
+ assignAllTeamMembers,
+ // We don't need to send send these values to the backend
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ seatsPerTimeSlotEnabled,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ minimumBookingNoticeInDurationType,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ bookerLayouts,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ multipleDurationEnabled,
+ length,
+ ...input
+ } = values;
+
+ if (!Number(length)) throw new Error(t("event_setup_length_error"));
+
+ if (bookingLimits) {
+ const isValid = validateIntervalLimitOrder(bookingLimits);
+ if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
+ }
+
+ if (durationLimits) {
+ const isValid = validateIntervalLimitOrder(durationLimits);
+ if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
+ }
+
+ const layoutError = validateBookerLayouts(metadata?.bookerLayouts || null);
+ if (layoutError) throw new Error(t(layoutError));
+
+ if (metadata?.multipleDuration !== undefined) {
+ if (metadata?.multipleDuration.length < 1) {
+ throw new Error(t("event_setup_multiple_duration_error"));
+ } else {
+ if (!length && !metadata?.multipleDuration?.includes(length)) {
+ throw new Error(t("event_setup_multiple_duration_default_error"));
+ }
+ }
+ }
+
+ // Prevent two payment apps to be enabled
+ // Ok to cast type here because this metadata will be updated as the event type metadata
+ if (checkForMultiplePaymentApps(metadata as z.infer))
+ throw new Error(t("event_setup_multiple_payment_apps_error"));
+
+ if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
+ throw new Error(t("seats_and_no_show_fee_error"));
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const { availability, users, scheduleName, ...rest } = input;
+ updateMutation.mutate({
+ ...rest,
+ length,
+ locations,
+ recurringEvent,
+ periodStartDate: periodDates.startDate,
+ periodEndDate: periodDates.endDate,
+ periodCountCalendarDays: periodCountCalendarDays === "1",
+ id: eventType.id,
+ beforeEventBuffer,
+ afterEventBuffer,
+ bookingLimits,
+ onlyShowFirstAvailableSlot,
+ durationLimits,
+ seatsPerTimeSlot,
+ seatsShowAttendees,
+ seatsShowAvailabilityCount,
+ metadata,
+ customInputs,
+ children,
+ assignAllTeamMembers,
+ });
+ };
+
+ const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState([]);
+ const slug = formMethods.watch("slug") ?? eventType.slug;
+
+ // Optional prerender all tabs after 300 ms on mount
+ useEffect(() => {
+ const timeout = setTimeout(() => {
+ const Components = [
+ EventSetupTab,
+ EventAvailabilityTab,
+ EventTeamTab,
+ EventLimitsTab,
+ EventAdvancedTab,
+ EventInstantTab,
+ EventRecurringTab,
+ EventAppsTab,
+ EventWorkflowsTab,
+ EventWebhooksTab,
+ ];
+
+ Components.forEach((C) => {
+ // @ts-expect-error Property 'render' does not exist on type 'ComponentClass
+ C.render.preload();
+ });
+ }, 300);
+
+ return () => {
+ clearTimeout(timeout);
+ };
+ }, []);
+ return (
+ <>
+ webhook.active).length}
+ team={team}
+ availability={availability}
+ isUpdateMutationLoading={updateMutation.isPending}
+ formMethods={formMethods}
+ // disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
+ disableBorder={true}
+ currentUserMembership={currentUserMembership}
+ bookerUrl={eventType.bookerUrl}
+ isUserOrganizationAdmin={props.isUserOrganizationAdmin}>
+
+
+
+ {slugExistsChildrenDialogOpen.length ? (
+ {
+ setSlugExistsChildrenDialogOpen([]);
+ }}
+ slug={slug}
+ onConfirm={(e: { preventDefault: () => void }) => {
+ e.preventDefault();
+ handleSubmit(formMethods.getValues());
+ telemetry.event(telemetryEventTypes.slugReplacementAction);
+ setSlugExistsChildrenDialogOpen([]);
+ }}
+ />
+ ) : null}
+ >
+ );
+};
+
+const EventTypePageWrapper: React.FC & {
+ PageWrapper?: AppProps["Component"]["PageWrapper"];
+ getLayout?: AppProps["Component"]["getLayout"];
+} = (props) => {
+ const { data } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
+
+ if (!data) return null;
+ return ;
+};
+
+export default EventTypePageWrapper;
+"use client";
+
+import { useAutoAnimate } from "@formkit/auto-animate/react";
+import { Trans } from "next-i18next";
+import Link from "next/link";
+import { usePathname, useRouter } from "next/navigation";
+import type { FC } from "react";
+import { memo, useEffect, useState } from "react";
+import { z } from "zod";
+
+import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
+import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
+import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
+import { EventTypeDescription } from "@calcom/features/eventtypes/components";
+import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
+import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
+import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
+import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
+import { ShellMain } from "@calcom/features/shell/Shell";
+import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
+import { CAL_URL } from "@calcom/lib/constants";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
+import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
+import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
+import { HttpError } from "@calcom/lib/http-error";
+import { SchedulingType } from "@calcom/prisma/enums";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import { trpc, TRPCClientError } from "@calcom/trpc/react";
+import {
+ Alert,
+ Avatar,
+ Badge,
+ Button,
+ ButtonGroup,
+ ConfirmationDialogContent,
+ CreateButton,
+ Dialog,
+ Dropdown,
+ DropdownItem,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuPortal,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+ EmptyScreen,
+ HeadSeo,
+ HorizontalTabs,
+ Label,
+ showToast,
+ Skeleton,
+ Switch,
+ Tooltip,
+ ArrowButton,
+ UserAvatarGroup,
+} from "@calcom/ui";
+import {
+ Clipboard,
+ Code,
+ Copy,
+ Edit,
+ Edit2,
+ ExternalLink,
+ Link as LinkIcon,
+ MoreHorizontal,
+ Trash,
+ Upload,
+ Users,
+ VenetianMask,
+} from "@calcom/ui/components/icon";
+
+import type { AppProps } from "@lib/app-providers";
+import useMeQuery from "@lib/hooks/useMeQuery";
+
+import SkeletonLoader from "@components/eventtype/SkeletonLoader";
+
+type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
+type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
+type GetByViewerResponse = RouterOutputs["viewer"]["eventTypes"]["getByViewer"] | undefined;
+
+interface EventTypeListHeadingProps {
+ profile: EventTypeGroupProfile;
+ membershipCount: number;
+ teamId?: number | null;
+ bookerUrl: string;
+}
+
+type EventTypeGroup = EventTypeGroups[number];
+type EventType = EventTypeGroup["eventTypes"][number];
+
+interface EventTypeListProps {
+ group: EventTypeGroup;
+ groupIndex: number;
+ readOnly: boolean;
+ bookerUrl: string | null;
+ types: EventType[];
+}
+
+interface MobileTeamsTabProps {
+ eventTypeGroups: EventTypeGroups;
+}
+
+const querySchema = z.object({
+ teamId: z.nullable(z.coerce.number()).optional().default(null),
+});
+
+const MobileTeamsTab: FC = (props) => {
+ const { eventTypeGroups } = props;
+ const orgBranding = useOrgBranding();
+ const tabs = eventTypeGroups.map((item) => ({
+ name: item.profile.name ?? "",
+ href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types?noTeam",
+ avatar: orgBranding
+ ? `${orgBranding.fullDomain}${item.teamId ? "/team" : ""}/${item.profile.slug}/avatar.png`
+ : item.profile.image ?? `${WEBAPP_URL + (item.teamId && "/team")}/${item.profile.slug}/avatar.png`,
+ }));
+ const { data } = useTypedQuery(querySchema);
+ const events = eventTypeGroups.filter((item) => item.teamId === data.teamId);
+
+ return (
+
+
+ {events.length > 0 ? (
+
+ ) : (
+
+ )}
+
+ );
+};
+
+const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => {
+ const { t } = useLocale();
+
+ const content = () => (
+
+
+ {type.title}
+
+ {group.profile.slug ? (
+
+ {`/${
+ type.schedulingType !== SchedulingType.MANAGED ? group.profile.slug : t("username_placeholder")
+ }/${type.slug}`}
+
+ ) : null}
+ {readOnly && (
+
+ {t("readonly")}
+
+ )}
+
+ );
+
+ return readOnly ? (
+
+ {content()}
+
+
+ ) : (
+
+
+
+ {type.title}
+
+ {group.profile.slug ? (
+
+ {`/${group.profile.slug}/${type.slug}`}
+
+ ) : null}
+ {readOnly && (
+
+ {t("readonly")}
+
+ )}
+
+
+
+ );
+};
+
+const MemoizedItem = memo(Item);
+
+export const EventTypeList = ({
+ group,
+ groupIndex,
+ readOnly,
+ types,
+ bookerUrl,
+}: EventTypeListProps): JSX.Element => {
+ const { t } = useLocale();
+ const router = useRouter();
+ const pathname = usePathname();
+ const searchParams = useCompatSearchParams();
+ const [parent] = useAutoAnimate();
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
+ const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
+ const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState(
+ null
+ );
+ const utils = trpc.useContext();
+ const mutation = trpc.viewer.eventTypeOrder.useMutation({
+ onError: async (err) => {
+ console.error(err.message);
+ await utils.viewer.eventTypes.getByViewer.cancel();
+ // REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
+ await utils.viewer.eventTypes.invalidate();
+ },
+ onSettled: () => {
+ // REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
+ utils.viewer.eventTypes.invalidate();
+ },
+ });
+
+ const setHiddenMutation = trpc.viewer.eventTypes.update.useMutation({
+ onMutate: async ({ id }) => {
+ await utils.viewer.eventTypes.getByViewer.cancel();
+ const previousValue = utils.viewer.eventTypes.getByViewer.getData();
+ if (previousValue) {
+ const newList = [...types];
+ const itemIndex = newList.findIndex((item) => item.id === id);
+ if (itemIndex !== -1 && newList[itemIndex]) {
+ newList[itemIndex].hidden = !newList[itemIndex].hidden;
+ }
+ utils.viewer.eventTypes.getByViewer.setData(undefined, {
+ ...previousValue,
+ eventTypeGroups: [
+ ...previousValue.eventTypeGroups.slice(0, groupIndex),
+ { ...group, eventTypes: newList },
+ ...previousValue.eventTypeGroups.slice(groupIndex + 1),
+ ],
+ });
+ }
+ return { previousValue };
+ },
+ onError: async (err, _, context) => {
+ if (context?.previousValue) {
+ utils.viewer.eventTypes.getByViewer.setData(undefined, context.previousValue);
+ }
+ console.error(err.message);
+ },
+ onSettled: () => {
+ // REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
+ utils.viewer.eventTypes.invalidate();
+ },
+ });
+
+ async function moveEventType(index: number, increment: 1 | -1) {
+ const newList = [...types];
+
+ const type = types[index];
+ const tmp = types[index + increment];
+ if (tmp) {
+ newList[index] = tmp;
+ newList[index + increment] = type;
+ }
+
+ await utils.viewer.eventTypes.getByViewer.cancel();
+
+ const previousValue = utils.viewer.eventTypes.getByViewer.getData();
+ if (previousValue) {
+ utils.viewer.eventTypes.getByViewer.setData(undefined, {
+ ...previousValue,
+ eventTypeGroups: [
+ ...previousValue.eventTypeGroups.slice(0, groupIndex),
+ { ...group, eventTypes: newList },
+ ...previousValue.eventTypeGroups.slice(groupIndex + 1),
+ ],
+ });
+ }
+
+ mutation.mutate({
+ ids: newList.map((type) => type.id),
+ });
+ }
+
+ async function deleteEventTypeHandler(id: number) {
+ const payload = { id };
+ deleteMutation.mutate(payload);
+ }
+
+ // inject selection data into url for correct router history
+ const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => {
+ const newSearchParams = new URLSearchParams(searchParams ?? undefined);
+ function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) {
+ if (value) newSearchParams.set(key, value.toString());
+ if (value === null) newSearchParams.delete(key);
+ }
+ setParamsIfDefined("dialog", "duplicate");
+ setParamsIfDefined("title", eventType.title);
+ setParamsIfDefined("description", eventType.description);
+ setParamsIfDefined("slug", eventType.slug);
+ setParamsIfDefined("id", eventType.id);
+ setParamsIfDefined("length", eventType.length);
+ setParamsIfDefined("pageSlug", group.profile.slug);
+ router.push(`${pathname}?${newSearchParams.toString()}`);
+ };
+
+ const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
+ onSuccess: () => {
+ showToast(t("event_type_deleted_successfully"), "success");
+ setDeleteDialogOpen(false);
+ },
+ onMutate: async ({ id }) => {
+ await utils.viewer.eventTypes.getByViewer.cancel();
+ const previousValue = utils.viewer.eventTypes.getByViewer.getData();
+ if (previousValue) {
+ const newList = types.filter((item) => item.id !== id);
+
+ utils.viewer.eventTypes.getByViewer.setData(undefined, {
+ ...previousValue,
+ eventTypeGroups: [
+ ...previousValue.eventTypeGroups.slice(0, groupIndex),
+ { ...group, eventTypes: newList },
+ ...previousValue.eventTypeGroups.slice(groupIndex + 1),
+ ],
+ });
+ }
+ return { previousValue };
+ },
+ onError: (err, _, context) => {
+ if (context?.previousValue) {
+ utils.viewer.eventTypes.getByViewer.setData(undefined, context.previousValue);
+ }
+ if (err instanceof HttpError) {
+ const message = `${err.statusCode}: ${err.message}`;
+ showToast(message, "error");
+ setDeleteDialogOpen(false);
+ } else if (err instanceof TRPCClientError) {
+ showToast(err.message, "error");
+ }
+ },
+ onSettled: () => {
+ // REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
+ utils.viewer.eventTypes.invalidate();
+ },
+ });
+
+ const [isNativeShare, setNativeShare] = useState(true);
+
+ useEffect(() => {
+ if (!navigator.share) {
+ setNativeShare(false);
+ }
+ }, []);
+
+ if (!types.length) {
+ return group.teamId ? (
+
+ ) : (
+
+ );
+ }
+
+ const firstItem = types[0];
+ const lastItem = types[types.length - 1];
+ const isManagedEventPrefix = () => {
+ return deleteDialogTypeSchedulingType === SchedulingType.MANAGED ? "_managed" : "";
+ };
+ return (
+
+
+ {types.map((type, index) => {
+ const embedLink = `${group.profile.slug}/${type.slug}`;
+ const calLink = `${bookerUrl}/${embedLink}`;
+ const isPrivateURLEnabled = type.hashedLink?.link;
+ const placeholderHashedLink = `${CAL_URL}/d/${type.hashedLink?.link}/${type.slug}`;
+ const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
+ const isChildrenManagedEventType =
+ type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
+ return (
+
+
+
+ {!(firstItem && firstItem.id === type.id) && (
+
moveEventType(index, -1)} arrowDirection="up" />
+ )}
+
+ {!(lastItem && lastItem.id === type.id) && (
+ moveEventType(index, 1)} arrowDirection="down" />
+ )}
+
+
+
+ {type.team && !isManagedEventType && (
+
+ )}
+ {isManagedEventType && type?.children && type.children?.length > 0 && (
+
ch.users) ?? []}
+ />
+ )}
+
+ {!isManagedEventType && (
+ <>
+ {type.hidden &&
{t("hidden")} }
+
+
+ {
+ setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
+ }}
+ />
+
+
+ >
+ )}
+
+
+ {!isManagedEventType && (
+ <>
+
+
+
+
+
+ {
+ showToast(t("link_copied"), "success");
+ navigator.clipboard.writeText(calLink);
+ }}
+ />
+
+
+ {isPrivateURLEnabled && (
+
+ {
+ showToast(t("private_link_copied"), "success");
+ navigator.clipboard.writeText(placeholderHashedLink);
+ }}
+ />
+
+ )}
+ >
+ )}
+
+
+
+
+
+ {!readOnly && (
+
+ router.push(`/event-types/${type.id}`)}>
+ {t("edit")}
+
+
+ )}
+ {!isManagedEventType && !isChildrenManagedEventType && (
+ <>
+
+ openDuplicateModal(type, group)}>
+ {t("duplicate")}
+
+
+ >
+ )}
+ {!isManagedEventType && (
+
+
+ {t("embed")}
+
+
+ )}
+ {/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
+ {(group.metadata?.readOnly === false || group.metadata.readOnly === null) &&
+ !isChildrenManagedEventType && (
+ <>
+
+
+ {
+ setDeleteDialogOpen(true);
+ setDeleteDialogTypeId(type.id);
+ setDeleteDialogSchedulingType(type.schedulingType);
+ }}
+ StartIcon={Trash}
+ className="w-full rounded-none">
+ {t("delete")}
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {!isManagedEventType && (
+ <>
+
+
+ {t("preview")}
+
+
+
+ {
+ navigator.clipboard.writeText(calLink);
+ showToast(t("link_copied"), "success");
+ }}
+ StartIcon={Clipboard}
+ className="w-full rounded-none text-left">
+ {t("copy_link")}
+
+
+ >
+ )}
+ {isNativeShare ? (
+
+ {
+ navigator
+ .share({
+ title: t("share"),
+ text: t("share_event", { appName: APP_NAME }),
+ url: calLink,
+ })
+ .then(() => showToast(t("link_shared"), "success"))
+ .catch(() => showToast(t("failed"), "error"));
+ }}
+ StartIcon={Upload}
+ className="w-full rounded-none">
+ {t("share")}
+
+
+ ) : null}
+ {!readOnly && (
+
+ router.push(`/event-types/${type.id}`)}
+ StartIcon={Edit}
+ className="w-full rounded-none">
+ {t("edit")}
+
+
+ )}
+ {!isManagedEventType && !isChildrenManagedEventType && (
+
+ openDuplicateModal(type, group)}
+ StartIcon={Copy}
+ data-testid={`event-type-duplicate-${type.id}`}>
+ {t("duplicate")}
+
+
+ )}
+ {/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
+ {(group.metadata?.readOnly === false || group.metadata.readOnly === null) &&
+ !isChildrenManagedEventType && (
+ <>
+
+ {
+ setDeleteDialogOpen(true);
+ setDeleteDialogTypeId(type.id);
+ setDeleteDialogSchedulingType(type.schedulingType);
+ }}
+ StartIcon={Trash}
+ className="w-full rounded-none">
+ {t("delete")}
+
+
+ >
+ )}
+
+ {!isManagedEventType && (
+
+
+ {type.hidden ? t("show_eventtype_on_profile") : t("hide_from_profile")}
+
+ {
+ setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
+ }}
+ />
+
+ )}
+
+
+
+
+
+
+ );
+ })}
+
+
+ {
+ e.preventDefault();
+ deleteEventTypeHandler(deleteDialogTypeId);
+ }}>
+
+ , ul:
}}>
+
+ Members assigned to this event type will also have their event types deleted.
+
+ Anyone who they've shared their link with will no longer be able to book using it.
+
+
+
+
+
+
+
+ );
+};
+
+const EventTypeListHeading = ({
+ profile,
+ membershipCount,
+ teamId,
+ bookerUrl,
+}: EventTypeListHeadingProps): JSX.Element => {
+ const { t } = useLocale();
+ const router = useRouter();
+
+ const publishTeamMutation = trpc.viewer.teams.publish.useMutation({
+ onSuccess(data) {
+ router.push(data.url);
+ },
+ onError: (error) => {
+ showToast(error.message, "error");
+ },
+ });
+
+ return (
+
+
+
+
+ {profile?.name || ""}
+
+ {membershipCount && teamId && (
+
+
+
+
+ {membershipCount}
+
+
+
+ )}
+ {profile?.slug && (
+
+ {`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
+
+ )}
+
+ {!profile?.slug && !!teamId && (
+
publishTeamMutation.mutate({ teamId })}>
+
+ {t("upgrade")}
+
+
+ )}
+
+ );
+};
+
+const CreateFirstEventTypeView = ({ slug }: { slug: string }) => {
+ const { t } = useLocale();
+
+ return (
+
+ {t("create")}
+
+ }
+ />
+ );
+};
+
+const CTA = ({ data }: { data: GetByViewerResponse }) => {
+ const { t } = useLocale();
+
+ if (!data) return null;
+
+ const profileOptions = data.profiles
+ .filter((profile) => !profile.readOnly)
+ .map((profile) => {
+ return {
+ teamId: profile.teamId,
+ label: profile.name || profile.slug,
+ image: profile.image,
+ membershipRole: profile.membershipRole,
+ slug: profile.slug,
+ };
+ });
+
+ return (
+ }
+ />
+ );
+};
+
+const Actions = () => {
+ return (
+
+
+
+ );
+};
+
+const EmptyEventTypeList = ({ group }: { group: EventTypeGroup }) => {
+ const { t } = useLocale();
+ return (
+ <>
+
+ {t("create")}
+
+ }
+ />
+ >
+ );
+};
+
+const Main = ({
+ status,
+ errorMessage,
+ data,
+ filters,
+}: {
+ status: string;
+ data: GetByViewerResponse;
+ errorMessage?: string;
+ filters: ReturnType;
+}) => {
+ const isMobile = useMediaQuery("(max-width: 768px)");
+ const searchParams = useCompatSearchParams();
+ const orgBranding = useOrgBranding();
+
+ if (!data || status === "pending") {
+ return ;
+ }
+
+ if (status === "error") {
+ return ;
+ }
+
+ const isFilteredByOnlyOneItem =
+ (filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) && data.eventTypeGroups.length === 1;
+ return (
+ <>
+ {data.eventTypeGroups.length > 1 || isFilteredByOnlyOneItem ? (
+ <>
+ {isMobile ? (
+
+ ) : (
+ data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
+
+
+
+ {group.eventTypes.length ? (
+
+ ) : group.teamId ? (
+
+ ) : (
+
+ )}
+
+ ))
+ )}
+ >
+ ) : (
+ data.eventTypeGroups.length === 1 && (
+
+ )
+ )}
+ {data.eventTypeGroups.length === 0 && }
+
+ {searchParams?.get("dialog") === "duplicate" && }
+ >
+ );
+};
+
+const EventTypesPage: React.FC & {
+ PageWrapper?: AppProps["Component"]["PageWrapper"];
+ getLayout?: AppProps["Component"]["getLayout"];
+} = () => {
+ const { t } = useLocale();
+ const searchParams = useCompatSearchParams();
+ const { open } = useIntercom();
+ const { data: user } = useMeQuery();
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const [showProfileBanner, setShowProfileBanner] = useState(false);
+ const orgBranding = useOrgBranding();
+ const routerQuery = useRouterQuery();
+ const filters = getTeamsFiltersFromQuery(routerQuery);
+
+ // TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
+ const { data, status, error } = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters }, {
+ refetchOnWindowFocus: false,
+ gcTime: 1 * 60 * 60 * 1000,
+ staleTime: 1 * 60 * 60 * 1000,
+ });
+
+ useEffect(() => {
+ if (searchParams?.get("openIntercom") === "true") {
+ open();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ setShowProfileBanner(
+ !!orgBranding && !document.cookie.includes("calcom-profile-banner=1") && !user?.completedOnboarding
+ );
+ }, [orgBranding, user]);
+
+ return (
+ }
+ CTA={ }>
+
+
+
+ );
+};
+
+export default EventTypesPage;
+"use client";
+
+import dayjs from "@calcom/dayjs";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
+import { Button, HeadSeo, EmptyScreen } from "@calcom/ui";
+import { ArrowRight, Calendar, Clock } from "@calcom/ui/components/icon";
+
+import { type PageProps } from "./videos-meeting-not-started-single-view.getServerSideProps";
+
+export default function MeetingNotStarted(props: PageProps) {
+ const { t } = useLocale();
+ return (
+ <>
+
+
+
+ {props.booking.title}
+
+
+ {dayjs(props.booking.startTime).format(`${detectBrowserTimeFormat}, dddd DD MMMM YYYY`)}
+
+ >
+ }
+ buttonRaw={
+
+ {t("go_back")}
+
+ }
+ />
+
+ >
+ );
+}
+"use client";
+
+import DailyIframe from "@daily-co/daily-js";
+import Head from "next/head";
+import { useState, useEffect, useRef } from "react";
+
+import dayjs from "@calcom/dayjs";
+import classNames from "@calcom/lib/classNames";
+import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
+import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-fns";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
+import { ChevronRight } from "@calcom/ui/components/icon";
+
+import { type PageProps } from "./videos-single-view.getServerSideProps";
+
+export default function JoinCall(props: PageProps) {
+ const { t } = useLocale();
+ const { meetingUrl, meetingPassword, booking } = props;
+
+ useEffect(() => {
+ const callFrame = DailyIframe.createFrame({
+ theme: {
+ colors: {
+ accent: "#FFF",
+ accentText: "#111111",
+ background: "#111111",
+ backgroundAccent: "#111111",
+ baseText: "#FFF",
+ border: "#292929",
+ mainAreaBg: "#111111",
+ mainAreaBgAccent: "#1A1A1A",
+ mainAreaText: "#FFF",
+ supportiveText: "#FFF",
+ },
+ },
+ showLeaveButton: true,
+ iframeStyle: {
+ position: "fixed",
+ width: "100%",
+ height: "100%",
+ },
+ url: meetingUrl,
+ ...(typeof meetingPassword === "string" && { token: meetingPassword }),
+ });
+ callFrame.join();
+ return () => {
+ callFrame.destroy();
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const title = `${APP_NAME} Video`;
+ return (
+ <>
+
+ {title}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {booking?.user?.organization?.calVideoLogo ? (
+
+ ) : (
+
+ )}
+
+
+ >
+ );
+}
+
+interface ProgressBarProps {
+ startTime: string;
+ endTime: string;
+}
+
+function ProgressBar(props: ProgressBarProps) {
+ const { t } = useLocale();
+ const { startTime, endTime } = props;
+ const currentTime = dayjs().second(0).millisecond(0);
+ const startingTime = dayjs(startTime).second(0).millisecond(0);
+ const isPast = currentTime.isAfter(startingTime);
+ const currentDifference = dayjs().diff(startingTime, "minutes");
+ const startDuration = dayjs(endTime).diff(startingTime, "minutes");
+ const [duration, setDuration] = useState(() => {
+ if (currentDifference >= 0 && isPast) {
+ return startDuration - currentDifference;
+ } else {
+ return startDuration;
+ }
+ });
+
+ const timeoutRef = useRef(null);
+ const intervalRef = useRef(null);
+
+ useEffect(() => {
+ const now = dayjs();
+ const remainingMilliseconds = (60 - now.get("seconds")) * 1000 - now.get("milliseconds");
+
+ timeoutRef.current = setTimeout(() => {
+ const past = dayjs().isAfter(startingTime);
+
+ if (past) {
+ setDuration((prev) => prev - 1);
+ }
+
+ intervalRef.current = setInterval(() => {
+ if (dayjs().isAfter(startingTime)) {
+ setDuration((prev) => prev - 1);
+ }
+ }, 60000);
+ }, remainingMilliseconds);
+
+ return () => {
+ if (timeoutRef.current) {
+ clearTimeout(timeoutRef.current);
+ timeoutRef.current = null;
+ }
+ if (intervalRef.current) {
+ clearInterval(intervalRef.current);
+ intervalRef.current = null;
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const prev = startDuration - duration;
+ const percentage = prev * (100 / startDuration);
+ return (
+
+
+ {duration} {t("minutes")}
+
+
+
+ );
+}
+
+interface VideoMeetingInfo {
+ booking: PageProps["booking"];
+}
+
+export function VideoMeetingInfo(props: VideoMeetingInfo) {
+ const [open, setOpen] = useState(false);
+ const { booking } = props;
+ const { t } = useLocale();
+
+ const endTime = new Date(booking.endTime);
+ const startTime = new Date(booking.startTime);
+
+ return (
+ <>
+
+
+ {t("what")}:
+ {booking.title}
+ {t("invitee_timezone")}:
+ {booking.user?.timeZone}
+ {t("when")}:
+
+ {formatToLocalizedDate(startTime)}
+ {formatToLocalizedTime(startTime)}
+
+ {t("time_left")}
+
+
+ {t("who")}:
+
+ {booking?.user?.name} - {t("organizer")}:{" "}
+ {booking?.user?.email}
+
+
+ {booking.attendees.length
+ ? booking.attendees.map((attendee) => (
+
+ {attendee.name} – {attendee.email}
+
+ ))
+ : null}
+
+ {booking.description && (
+ <>
+ {t("description")}:
+
+
+ >
+ )}
+
+
+ setOpen(!open)}>
+
+
+
+
+ >
+ );
+}
+"use client";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, EmptyScreen, HeadSeo } from "@calcom/ui";
+import { X, ArrowRight } from "@calcom/ui/components/icon";
+
+export default function NoMeetingFound() {
+ const { t } = useLocale();
+
+ return (
+ <>
+
+
+
+ {t("go_back_home")}
+
+ }
+ />
+
+ >
+ );
+}
+"use client";
+
+import dayjs from "@calcom/dayjs";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
+import { Button, HeadSeo } from "@calcom/ui";
+import { ArrowRight, Calendar, X } from "@calcom/ui/components/icon";
+
+import { type PageProps } from "./videos-meeting-ended-single-view.getServerSideProps";
+
+export default function MeetingUnavailable(props: PageProps) {
+ const { t } = useLocale();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ This meeting is in the past.
+
+
+
+
+ {props.booking.title}
+
+
+
+ {dayjs(props.booking.startTime).format(`${detectBrowserTimeFormat}, dddd DD MMMM YYYY`)}
+
+
+
+
+
+
+ {t("go_back")}
+
+
+
+
+
+
+
+
+
+ );
+}
+import type { GetServerSidePropsContext } from "next";
+
+import prisma, { bookingMinimalSelect } from "@calcom/prisma";
+import type { inferSSRProps } from "@calcom/types/inferSSRProps";
+
+export type PageProps = inferSSRProps;
+
+// change the type
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const booking = await prisma.booking.findUnique({
+ where: {
+ uid: context.query.uid as string,
+ },
+ select: bookingMinimalSelect,
+ });
+
+ if (!booking) {
+ const redirect = {
+ redirect: {
+ destination: "/video/no-meeting-found",
+ permanent: false,
+ },
+ } as const;
+
+ return redirect;
+ }
+
+ const bookingObj = Object.assign({}, booking, {
+ startTime: booking.startTime.toString(),
+ endTime: booking.endTime.toString(),
+ });
+
+ return {
+ props: {
+ booking: bookingObj,
+ },
+ };
+}
+import type { GetServerSidePropsContext } from "next";
+
+import prisma, { bookingMinimalSelect } from "@calcom/prisma";
+
+import { type inferSSRProps } from "@lib/types/inferSSRProps";
+
+export type PageProps = inferSSRProps;
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const booking = await prisma.booking.findUnique({
+ where: {
+ uid: context.query.uid as string,
+ },
+ select: {
+ ...bookingMinimalSelect,
+ uid: true,
+ user: {
+ select: {
+ credentials: true,
+ },
+ },
+ references: {
+ select: {
+ uid: true,
+ type: true,
+ meetingUrl: true,
+ },
+ },
+ },
+ });
+
+ if (!booking) {
+ const redirect = {
+ redirect: {
+ destination: "/video/no-meeting-found",
+ permanent: false,
+ },
+ } as const;
+
+ return redirect;
+ }
+
+ const bookingObj = Object.assign({}, booking, {
+ startTime: booking.startTime.toString(),
+ endTime: booking.endTime.toString(),
+ });
+
+ return {
+ props: {
+ booking: bookingObj,
+ },
+ };
+}
+import MarkdownIt from "markdown-it";
+import type { GetServerSidePropsContext } from "next";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { getCalVideoReference } from "@calcom/features/get-cal-video-reference";
+import prisma, { bookingMinimalSelect } from "@calcom/prisma";
+
+import { type inferSSRProps } from "@lib/types/inferSSRProps";
+
+import { ssrInit } from "@server/lib/ssr";
+
+export type PageProps = inferSSRProps;
+
+const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const { req } = context;
+
+ const ssr = await ssrInit(context);
+
+ const booking = await prisma.booking.findUnique({
+ where: {
+ uid: context.query.uid as string,
+ },
+ select: {
+ ...bookingMinimalSelect,
+ uid: true,
+ description: true,
+ isRecorded: true,
+ user: {
+ select: {
+ id: true,
+ timeZone: true,
+ name: true,
+ email: true,
+ organization: {
+ select: {
+ calVideoLogo: true,
+ },
+ },
+ },
+ },
+ references: {
+ select: {
+ uid: true,
+ type: true,
+ meetingUrl: true,
+ meetingPassword: true,
+ },
+ where: {
+ type: "daily_video",
+ },
+ },
+ },
+ });
+
+ if (!booking || booking.references.length === 0 || !booking.references[0].meetingUrl) {
+ return {
+ redirect: {
+ destination: "/video/no-meeting-found",
+ permanent: false,
+ },
+ };
+ }
+
+ //daily.co calls have a 60 minute exit buffer when a user enters a call when it's not available it will trigger the modals
+ const now = new Date();
+ const exitDate = new Date(now.getTime() - 60 * 60 * 1000);
+
+ //find out if the meeting is in the past
+ const isPast = booking?.endTime <= exitDate;
+ if (isPast) {
+ return {
+ redirect: {
+ destination: `/video/meeting-ended/${booking?.uid}`,
+ permanent: false,
+ },
+ };
+ }
+
+ const bookingObj = Object.assign({}, booking, {
+ startTime: booking.startTime.toString(),
+ endTime: booking.endTime.toString(),
+ });
+
+ const session = await getServerSession({ req });
+
+ // set meetingPassword to null for guests
+ if (session?.user.id !== bookingObj.user?.id) {
+ bookingObj.references.forEach((bookRef) => {
+ bookRef.meetingPassword = null;
+ });
+ }
+ const videoReference = getCalVideoReference(bookingObj.references);
+
+ return {
+ props: {
+ meetingUrl: videoReference.meetingUrl ?? "",
+ ...(typeof videoReference.meetingPassword === "string" && {
+ meetingPassword: videoReference.meetingPassword,
+ }),
+ booking: {
+ ...bookingObj,
+ ...(bookingObj.description && { description: md.render(bookingObj.description) }),
+ },
+ trpcState: ssr.dehydrate(),
+ },
+ };
+}
+import type { DehydratedState } from "@tanstack/react-query";
+import type { GetServerSideProps } from "next";
+import { encode } from "querystring";
+import type { z } from "zod";
+
+import { handleUserRedirection } from "@calcom/features/booking-redirect/handle-user";
+import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
+import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
+import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
+import { getUsernameList } from "@calcom/lib/defaultEvents";
+import logger from "@calcom/lib/logger";
+import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
+import { stripMarkdown } from "@calcom/lib/stripMarkdown";
+import prisma from "@calcom/prisma";
+import { RedirectType, type EventType, type User } from "@calcom/prisma/client";
+import { baseEventTypeSelect } from "@calcom/prisma/selects";
+import { EventTypeMetaDataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils";
+
+import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
+import type { EmbedProps } from "@lib/withEmbedSsr";
+
+import { ssrInit } from "@server/lib/ssr";
+
+export type UserPageProps = {
+ trpcState: DehydratedState;
+ profile: {
+ name: string;
+ image: string;
+ theme: string | null;
+ brandColor: string;
+ darkBrandColor: string;
+ organization: {
+ requestedSlug: string | null;
+ slug: string | null;
+ id: number | null;
+ };
+ allowSEOIndexing: boolean;
+ username: string | null;
+ };
+ users: Pick[];
+ themeBasis: string | null;
+ markdownStrippedBio: string;
+ safeBio: string;
+ entity: {
+ isUnpublished?: boolean;
+ orgSlug?: string | null;
+ name?: string | null;
+ };
+ eventTypes: ({
+ descriptionAsSafeHTML: string;
+ metadata: z.infer;
+ } & Pick<
+ EventType,
+ | "id"
+ | "title"
+ | "slug"
+ | "length"
+ | "hidden"
+ | "lockTimeZoneToggleOnBookingPage"
+ | "requiresConfirmation"
+ | "requiresBookerEmailVerification"
+ | "price"
+ | "currency"
+ | "recurringEvent"
+ >)[];
+} & EmbedProps;
+
+export const getEventTypesWithHiddenFromDB = async (userId: number) => {
+ const eventTypes = await prisma.eventType.findMany({
+ where: {
+ AND: [
+ {
+ teamId: null,
+ },
+ {
+ OR: [
+ {
+ userId,
+ },
+ {
+ users: {
+ some: {
+ id: userId,
+ },
+ },
+ },
+ ],
+ },
+ ],
+ },
+ orderBy: [
+ {
+ position: "desc",
+ },
+ {
+ id: "asc",
+ },
+ ],
+ select: {
+ ...baseEventTypeSelect,
+ metadata: true,
+ },
+ });
+ // map and filter metadata, exclude eventType entirely when faulty metadata is found.
+ // report error to exception so we don't lose the error.
+ return eventTypes.reduce((eventTypes, eventType) => {
+ const parsedMetadata = EventTypeMetaDataSchema.safeParse(eventType.metadata);
+ if (!parsedMetadata.success) {
+ logger.error(parsedMetadata.error);
+ return eventTypes;
+ }
+ eventTypes.push({
+ ...eventType,
+ metadata: parsedMetadata.data,
+ });
+ return eventTypes;
+ }, []);
+};
+
+export const getServerSideProps: GetServerSideProps = async (context) => {
+ const ssr = await ssrInit(context);
+ const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
+ const usernameList = getUsernameList(context.query.user as string);
+ const isOrgContext = isValidOrgDomain && currentOrgDomain;
+ const dataFetchStart = Date.now();
+ let outOfOffice = false;
+ const isDynamicGroup = usernameList.length > 1;
+ if (usernameList.length === 1) {
+ const result = await handleUserRedirection({ username: usernameList[0] });
+ if (result && result.outOfOffice) {
+ outOfOffice = true;
+ }
+ if (result && result.redirect?.destination) {
+ return result;
+ }
+ }
+
+ if (!isOrgContext) {
+ const redirect = await getTemporaryOrgRedirect({
+ slugs: usernameList,
+ redirectType: RedirectType.User,
+ eventTypeSlug: null,
+ currentQuery: context.query,
+ });
+
+ if (redirect) {
+ return redirect;
+ }
+ }
+
+ const usersWithoutAvatar = await prisma.user.findMany({
+ where: {
+ username: {
+ in: usernameList,
+ },
+ organization: isOrgContext ? getSlugOrRequestedSlug(currentOrgDomain) : null,
+ },
+ select: {
+ id: true,
+ username: true,
+ email: true,
+ name: true,
+ bio: true,
+ metadata: true,
+ brandColor: true,
+ darkBrandColor: true,
+ avatarUrl: true,
+ organizationId: true,
+ organization: {
+ select: {
+ slug: true,
+ name: true,
+ metadata: true,
+ },
+ },
+ theme: true,
+ away: true,
+ verified: true,
+ allowDynamicBooking: true,
+ allowSEOIndexing: true,
+ },
+ });
+
+ const users = usersWithoutAvatar.map((user) => ({
+ ...user,
+ organization: {
+ ...user.organization,
+ metadata: user.organization?.metadata ? teamMetadataSchema.parse(user.organization.metadata) : null,
+ },
+ avatar: `/${user.username}/avatar.png`,
+ }));
+
+ if (isDynamicGroup) {
+ const destinationUrl = `/${usernameList.join("+")}/dynamic`;
+ logger.debug(`Dynamic group detected, redirecting to ${destinationUrl}`);
+ return {
+ redirect: {
+ permanent: false,
+ destination: destinationUrl,
+ },
+ } as {
+ redirect: {
+ permanent: false;
+ destination: string;
+ };
+ };
+ }
+
+ if (!users.length || (!isValidOrgDomain && !users.some((user) => user.organizationId === null))) {
+ return {
+ notFound: true,
+ } as {
+ notFound: true;
+ };
+ }
+
+ const [user] = users; //to be used when dealing with single user, not dynamic group
+
+ const profile = {
+ name: user.name || user.username || "",
+ image: user.avatar,
+ theme: user.theme,
+ brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
+ avatarUrl: user.avatarUrl,
+ darkBrandColor: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
+ allowSEOIndexing: user.allowSEOIndexing ?? true,
+ username: user.username,
+ organization: {
+ id: user.organizationId,
+ slug: user.organization?.slug ?? null,
+ requestedSlug: user.organization?.metadata?.requestedSlug ?? null,
+ },
+ };
+
+ const eventTypesWithHidden = await getEventTypesWithHiddenFromDB(user.id);
+ const dataFetchEnd = Date.now();
+ if (context.query.log === "1") {
+ context.res.setHeader("X-Data-Fetch-Time", `${dataFetchEnd - dataFetchStart}ms`);
+ }
+ const eventTypesRaw = eventTypesWithHidden.filter((evt) => !evt.hidden);
+
+ const eventTypes = eventTypesRaw.map((eventType) => ({
+ ...eventType,
+ metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
+ descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
+ }));
+
+ // if profile only has one public event-type, redirect to it
+ if (eventTypes.length === 1 && context.query.redirect !== "false" && !outOfOffice) {
+ // Redirect but don't change the URL
+ const urlDestination = `/${user.username}/${eventTypes[0].slug}`;
+ const { query } = context;
+ const urlQuery = new URLSearchParams(encode(query));
+
+ return {
+ redirect: {
+ permanent: false,
+ destination: `${urlDestination}?${urlQuery}`,
+ },
+ };
+ }
+
+ const safeBio = markdownToSafeHTML(user.bio) || "";
+
+ const markdownStrippedBio = stripMarkdown(user?.bio || "");
+ const org = usersWithoutAvatar[0].organization;
+
+ return {
+ props: {
+ users: users.map((user) => ({
+ name: user.name,
+ username: user.username,
+ bio: user.bio,
+ avatarUrl: user.avatarUrl,
+ away: usernameList.length === 1 ? outOfOffice : user.away,
+ verified: user.verified,
+ })),
+ entity: {
+ isUnpublished: org?.slug === null,
+ orgSlug: currentOrgDomain,
+ name: org?.name ?? null,
+ },
+ eventTypes,
+ safeBio,
+ profile,
+ // Dynamic group has no theme preference right now. It uses system theme.
+ themeBasis: user.username,
+ trpcState: ssr.dehydrate(),
+ markdownStrippedBio,
+ },
+ };
+};
+import { type GetServerSidePropsContext } from "next";
+import { z } from "zod";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { handleTypeRedirection } from "@calcom/features/booking-redirect/handle-type";
+import { getBookingForReschedule, getBookingForSeatedEvent } from "@calcom/features/bookings/lib/get-booking";
+import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
+import { orgDomainConfig, userOrgQuery } from "@calcom/features/ee/organizations/lib/orgDomains";
+import { getUsernameList } from "@calcom/lib/defaultEvents";
+import slugify from "@calcom/lib/slugify";
+import prisma from "@calcom/prisma";
+import { RedirectType } from "@calcom/prisma/client";
+
+import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect";
+import { type inferSSRProps } from "@lib/types/inferSSRProps";
+import { type EmbedProps } from "@lib/withEmbedSsr";
+
+export type PageProps = inferSSRProps & EmbedProps;
+
+async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
+ const session = await getServerSession(context);
+ const { user: usernames, type: slug } = paramsSchema.parse(context.params);
+ const { rescheduleUid, bookingUid } = context.query;
+
+ const { ssrInit } = await import("@server/lib/ssr");
+ const ssr = await ssrInit(context);
+ const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
+ const org = isValidOrgDomain ? currentOrgDomain : null;
+ if (!org) {
+ const redirect = await getTemporaryOrgRedirect({
+ slugs: usernames,
+ redirectType: RedirectType.User,
+ eventTypeSlug: slug,
+ currentQuery: context.query,
+ });
+
+ if (redirect) {
+ return redirect;
+ }
+ }
+
+ const users = await prisma.user.findMany({
+ where: {
+ username: {
+ in: usernames,
+ },
+ organization: isValidOrgDomain
+ ? {
+ slug: currentOrgDomain,
+ }
+ : null,
+ },
+ select: {
+ allowDynamicBooking: true,
+ },
+ });
+
+ if (!users.length) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ let booking: GetBookingType | null = null;
+ if (rescheduleUid) {
+ booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
+ } else if (bookingUid) {
+ booking = await getBookingForSeatedEvent(`${bookingUid}`);
+ }
+
+ // We use this to both prefetch the query on the server,
+ // as well as to check if the event exist, so we c an show a 404 otherwise.
+ const eventData = await ssr.viewer.public.event.fetch({
+ username: usernames.join("+"),
+ eventSlug: slug,
+ org,
+ });
+
+ if (!eventData) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ return {
+ props: {
+ eventData: {
+ entity: eventData.entity,
+ length: eventData.length,
+ metadata: {
+ ...eventData.metadata,
+ multipleDuration: [15, 30, 60],
+ },
+ },
+ booking,
+ user: usernames.join("+"),
+ slug,
+ away: false,
+ trpcState: ssr.dehydrate(),
+ isBrandingHidden: false,
+ isSEOIndexable: true,
+ themeBasis: null,
+ bookingUid: bookingUid ? `${bookingUid}` : null,
+ rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
+ },
+ };
+}
+
+async function getUserPageProps(context: GetServerSidePropsContext) {
+ const session = await getServerSession(context);
+ const { user: usernames, type: slug } = paramsSchema.parse(context.params);
+ const username = usernames[0];
+ const { rescheduleUid, bookingUid } = context.query;
+ const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
+ let outOfOffice = false;
+ const isOrgContext = currentOrgDomain && isValidOrgDomain;
+ if (!isOrgContext) {
+ const redirect = await getTemporaryOrgRedirect({
+ slugs: usernames,
+ redirectType: RedirectType.User,
+ eventTypeSlug: slug,
+ currentQuery: context.query,
+ });
+
+ if (redirect) {
+ return redirect;
+ }
+ }
+
+ const { ssrInit } = await import("@server/lib/ssr");
+ const ssr = await ssrInit(context);
+ const user = await prisma.user.findFirst({
+ where: {
+ username,
+ organization: userOrgQuery(context.req, context.params?.orgSlug),
+ },
+ select: {
+ id: true,
+ hideBranding: true,
+ allowSEOIndexing: true,
+ },
+ });
+
+ if (!user) {
+ return {
+ notFound: true,
+ } as const;
+ }
+ // If user is found, quickly verify bookingRedirects
+ const result = await handleTypeRedirection({
+ userId: user.id,
+ username,
+ slug,
+ });
+ if (result && result.outOfOffice) {
+ outOfOffice = true;
+ }
+ if (result && result.redirect?.destination) {
+ return result;
+ }
+
+ let booking: GetBookingType | null = null;
+ if (rescheduleUid) {
+ booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
+ } else if (bookingUid) {
+ booking = await getBookingForSeatedEvent(`${bookingUid}`);
+ }
+
+ const org = isValidOrgDomain ? currentOrgDomain : null;
+ // We use this to both prefetch the query on the server,
+ // as well as to check if the event exist, so we can show a 404 otherwise.
+ const eventData = await ssr.viewer.public.event.fetch({
+ username,
+ eventSlug: slug,
+ org,
+ });
+
+ if (!eventData) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ return {
+ props: {
+ booking,
+ eventData: {
+ entity: eventData.entity,
+ length: eventData.length,
+ metadata: eventData.metadata,
+ },
+ away: outOfOffice,
+ user: username,
+ slug,
+ trpcState: ssr.dehydrate(),
+ isBrandingHidden: user?.hideBranding,
+ isSEOIndexable: user?.allowSEOIndexing,
+ themeBasis: username,
+ bookingUid: bookingUid ? `${bookingUid}` : null,
+ rescheduleUid: rescheduleUid ? `${rescheduleUid}` : null,
+ },
+ };
+}
+
+const paramsSchema = z.object({
+ type: z.string().transform((s) => slugify(s)),
+ user: z.string().transform((s) => getUsernameList(s)),
+});
+
+// Booker page fetches a tiny bit of data server side, to determine early
+// whether the page should show an away state or dynamic booking not allowed.
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const { user } = paramsSchema.parse(context.params);
+ const isDynamicGroup = user.length > 1;
+
+ return isDynamicGroup ? await getDynamicGroupPageProps(context) : await getUserPageProps(context);
+};
+"use client";
+
+import classNames from "classnames";
+import type { InferGetServerSidePropsType } from "next";
+import Link from "next/link";
+import { useSearchParams } from "next/navigation";
+import { Toaster } from "react-hot-toast";
+
+import {
+ sdkActionManager,
+ useEmbedNonStylesConfig,
+ useEmbedStyles,
+ useIsEmbed,
+} from "@calcom/embed-core/embed-iframe";
+import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
+import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
+import useTheme from "@calcom/lib/hooks/useTheme";
+import { HeadSeo, UnpublishedEntity, UserAvatar } from "@calcom/ui";
+import { ArrowRight, Verified } from "@calcom/ui/components/icon";
+
+import { type getServerSideProps } from "./users-public-view.getServerSideProps";
+
+export function UserPage(props: InferGetServerSidePropsType) {
+ const { users, profile, eventTypes, markdownStrippedBio, entity } = props;
+ const searchParams = useSearchParams();
+
+ const [user] = users; //To be used when we only have a single user, not dynamic group
+ useTheme(profile.theme);
+ const { t } = useLocale();
+
+ const isBioEmpty = !user.bio || !user.bio.replace("
", "").length;
+
+ const isEmbed = useIsEmbed(props.isEmbed);
+ const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
+ const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
+ const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
+ const {
+ // So it doesn't display in the Link (and make tests fail)
+ user: _user,
+ orgSlug: _orgSlug,
+ redirect: _redirect,
+ ...query
+ } = useRouterQuery();
+
+ const isRedirect = searchParams?.get("redirected") === "true" || false;
+ const fromUserNameRedirected = searchParams?.get("username") || "";
+ /*
+ const telemetry = useTelemetry();
+ useEffect(() => {
+ if (top !== window) {
+ //page_view will be collected automatically by _middleware.ts
+ telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
+ }
+ }, [telemetry, router.asPath]); */
+
+ if (entity?.isUnpublished) {
+ return (
+
+
+
+ );
+ }
+
+ const isEventListEmpty = eventTypes.length === 0;
+
+ return (
+ <>
+
+
+
+
+ {isRedirect && (
+
+
+ {t("user_redirect_title", {
+ username: fromUserNameRedirected,
+ })}{" "}
+ 🏝️
+
+
+ {t("user_redirect_description", {
+ profile: {
+ username: user.username,
+ },
+ username: fromUserNameRedirected,
+ })}{" "}
+ 😄
+
+
+ )}
+
+
+
+ {profile.name}
+ {user.verified && (
+
+ )}
+
+ {!isBioEmpty && (
+ <>
+
+ >
+ )}
+
+
+
+ {user.away ? (
+
+
+
😴{` ${t("user_away")}`}
+
{t("user_away_description") as string}
+
+
+ ) : (
+ eventTypes.map((type) => (
+
+
+ {/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
+
+
{
+ sdkActionManager?.fire("eventTypeSelected", {
+ eventType: type,
+ });
+ }}
+ data-testid="event-type-link">
+
+
{type.title}
+
+
+
+
+
+ ))
+ )}
+
+
+ {isEventListEmpty && }
+
+
+
+ >
+ );
+}
+
+export default UserPage;
+"use client";
+
+import { useSearchParams } from "next/navigation";
+
+import { Booker } from "@calcom/atoms";
+import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
+import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
+
+import { type PageProps } from "./users-type-public-view.getServerSideProps";
+
+export const getMultipleDurationValue = (
+ multipleDurationConfig: number[] | undefined,
+ queryDuration: string | string[] | null | undefined,
+ defaultValue: number
+) => {
+ if (!multipleDurationConfig) return null;
+ if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration);
+ return defaultValue;
+};
+
+export default function Type({
+ slug,
+ user,
+ isEmbed,
+ booking,
+ away,
+ isBrandingHidden,
+ isSEOIndexable,
+ rescheduleUid,
+ eventData,
+}: PageProps) {
+ const searchParams = useSearchParams();
+
+ return (
+
+
+
+
+ );
+}
+import type { GetServerSidePropsContext } from "next";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+
+function RedirectPage() {
+ return;
+}
+
+export async function getServerSideProps({ req, res }: GetServerSidePropsContext) {
+ const session = await getServerSession({ req, res });
+
+ if (!session?.user?.id) {
+ return { redirect: { permanent: false, destination: "/auth/login" } };
+ }
+
+ return { redirect: { permanent: false, destination: "/event-types" } };
+}
+
+export default RedirectPage;
+"use client";
+
+import { getLayout } from "@calcom/features/MainLayout";
+import {
+ AverageEventDurationChart,
+ BookingKPICards,
+ BookingStatusLineChart,
+ LeastBookedTeamMembersTable,
+ MostBookedTeamMembersTable,
+ PopularEventsTable,
+} from "@calcom/features/insights/components";
+import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
+import { Filters } from "@calcom/features/insights/filters";
+import { ShellMain } from "@calcom/features/shell/Shell";
+import { UpgradeTip } from "@calcom/features/tips";
+import { WEBAPP_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc";
+import { Button, ButtonGroup } from "@calcom/ui";
+import { RefreshCcw, UserPlus, Users } from "@calcom/ui/components/icon";
+
+import { getServerSideProps } from "@lib/insights/getServerSideProps";
+
+import PageWrapper from "@components/PageWrapper";
+
+export default function InsightsPage() {
+ const { t } = useLocale();
+ const { data: user } = trpc.viewer.me.useQuery();
+
+ const features = [
+ {
+ icon: ,
+ title: t("view_bookings_across"),
+ description: t("view_bookings_across_description"),
+ },
+ {
+ icon: ,
+ title: t("identify_booking_trends"),
+ description: t("identify_booking_trends_description"),
+ },
+ {
+ icon: ,
+ title: t("spot_popular_event_types"),
+ description: t("spot_popular_event_types_description"),
+ },
+ ];
+
+ return (
+
+
+
+
+
+ {t("create_team")}
+
+
+ {t("learn_more")}
+
+
+
+ }>
+ {!user ? (
+ <>>
+ ) : (
+
+
+
+
+
+ )}
+
+
+
+ );
+}
+
+InsightsPage.PageWrapper = PageWrapper;
+InsightsPage.getLayout = getLayout;
+
+export { getServerSideProps };
+import MembersView from "@calcom/features/ee/organizations/pages/settings/members";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = MembersView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import OrgProfileView from "@calcom/features/ee/organizations/pages/settings/profile";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = OrgProfileView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+"use client";
+
+import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
+import { CreateANewOrganizationForm } from "@calcom/features/ee/organizations/components";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { WizardLayout, Meta, WizardLayoutAppDir } from "@calcom/ui";
+
+import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
+import type { inferSSRProps } from "@lib/types/inferSSRProps";
+
+import PageWrapper from "@components/PageWrapper";
+
+const CreateNewOrganizationPage = ({ querySlug }: inferSSRProps) => {
+ const { t } = useLocale();
+ return (
+
+
+
+
+ );
+};
+const LayoutWrapper = (page: React.ReactElement) => {
+ return (
+
+ {page}
+
+ );
+};
+
+export const LayoutWrapperAppDir = (page: React.ReactElement) => {
+ return (
+
+ {page}
+
+ );
+};
+
+CreateNewOrganizationPage.getLayout = LayoutWrapper;
+CreateNewOrganizationPage.PageWrapper = PageWrapper;
+
+export default CreateNewOrganizationPage;
+
+export { getServerSideProps };
+import TeamMembersView from "@calcom/features/ee/organizations/pages/settings/other-team-members-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = TeamMembersView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import OtherTeamProfileView from "@calcom/features/ee/organizations/pages/settings/other-team-profile-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = OtherTeamProfileView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import TeamAppearenceView from "@calcom/features/ee/teams/pages/team-appearance-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = TeamAppearenceView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import OrgAppearanceView from "@calcom/features/ee/organizations/pages/settings/appearance";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = OrgAppearanceView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+import BillingPage from "../../settings/billing/index";
+
+const Page = BillingPage as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+"use client";
+
+import { redirect, useRouter } from "next/navigation";
+
+import { AddNewTeamsForm } from "@calcom/features/ee/organizations/components";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Meta, WizardLayout } from "@calcom/ui";
+import { WizardLayoutAppDir } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+export { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
+
+const AddNewTeamsPage = () => {
+ const { t } = useLocale();
+ return (
+ <>
+
+
+ >
+ );
+};
+
+AddNewTeamsPage.getLayout = (page: React.ReactElement) => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const router = useRouter();
+
+ return (
+ {
+ router.push(`/event-types`);
+ }}>
+ {page}
+
+ );
+};
+
+AddNewTeamsPage.PageWrapper = PageWrapper;
+
+export const WrapperAddNewTeamsPage = (page: React.ReactElement) => {
+ return (
+ {
+ redirect(`/event-types`);
+ }}>
+ {page}
+
+ );
+};
+
+export default AddNewTeamsPage;
+"use client";
+
+import { redirect, useRouter } from "next/navigation";
+
+import AddNewTeamMembers from "@calcom/features/ee/teams/components/AddNewTeamMembers";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Meta, WizardLayout, WizardLayoutAppDir } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+export { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
+
+const OnboardTeamMembersPage = () => {
+ const { t } = useLocale();
+
+ return (
+ <>
+
+
+ >
+ );
+};
+
+OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const router = useRouter();
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const query = useCompatSearchParams();
+
+ return (
+ {
+ router.push(`/settings/organizations/${query.get("id")}/add-teams`);
+ }}>
+ {page}
+
+ );
+};
+
+export const buildWrappedOnboardTeamMembersPage = (
+ id: string | string[] | undefined,
+ page: React.ReactElement
+) => {
+ return (
+ {
+ redirect(`/settings/organizations/${id}/add-teams`);
+ }}>
+ {page}
+
+ );
+};
+
+OnboardTeamMembersPage.PageWrapper = PageWrapper;
+
+export default OnboardTeamMembersPage;
+"use client";
+
+import { SetPasswordForm } from "@calcom/features/ee/organizations/components";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Meta, WizardLayout, WizardLayoutAppDir } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+export { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
+
+const SetPasswordPage = () => {
+ const { t } = useLocale();
+ return (
+ <>
+
+
+ >
+ );
+};
+const LayoutWrapper = (page: React.ReactElement) => {
+ return (
+
+ {page}
+
+ );
+};
+
+export const WrappedSetPasswordPage = (page: React.ReactElement) => {
+ return (
+
+ {page}
+
+ );
+};
+
+SetPasswordPage.getLayout = LayoutWrapper;
+SetPasswordPage.PageWrapper = PageWrapper;
+
+export default SetPasswordPage;
+"use client";
+
+import { AboutOrganizationForm } from "@calcom/features/ee/organizations/components";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Meta, WizardLayout, WizardLayoutAppDir } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+export { getServerSideProps } from "@calcom/features/ee/organizations/pages/organization";
+
+const AboutOrganizationPage = () => {
+ const { t } = useLocale();
+ return (
+ <>
+
+
+ >
+ );
+};
+export const LayoutWrapper = (page: React.ReactElement) => {
+ return (
+
+ {page}
+
+ );
+};
+
+export const WrappedAboutOrganizationPage = (page: React.ReactElement) => {
+ return (
+
+ {page}
+
+ );
+};
+
+AboutOrganizationPage.getLayout = LayoutWrapper;
+AboutOrganizationPage.PageWrapper = PageWrapper;
+
+export default AboutOrganizationPage;
+import OrgGeneralView from "@calcom/features/ee/organizations/pages/settings/general";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = OrgGeneralView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import WeebhooksView from "@calcom/features/webhooks/pages/webhooks-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = WeebhooksView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import WeebhookNewView from "@calcom/features/webhooks/pages/webhook-new-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = WeebhookNewView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import WebhookEditView from "@calcom/features/webhooks/pages/webhook-edit-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = WebhookEditView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import { useState } from "react";
+
+import type { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
+import LicenseRequired from "@calcom/ee/common/components/LicenseRequired";
+import ApiKeyDialogForm from "@calcom/features/ee/api-keys/components/ApiKeyDialogForm";
+import ApiKeyListItem from "@calcom/features/ee/api-keys/components/ApiKeyListItem";
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
+import { APP_NAME } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import {
+ Button,
+ Dialog,
+ DialogContent,
+ EmptyScreen,
+ Meta,
+ SkeletonContainer,
+ SkeletonText,
+} from "@calcom/ui";
+import { Link as LinkIcon, Plus } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+
+const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+const ApiKeysView = () => {
+ const { t } = useLocale();
+
+ const { data, isPending } = trpc.viewer.apiKeys.list.useQuery();
+
+ const [apiKeyModal, setApiKeyModal] = useState(false);
+ const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | undefined>(
+ undefined
+ );
+
+ const NewApiKeyButton = () => {
+ return (
+ {
+ setApiKeyToEdit(undefined);
+ setApiKeyModal(true);
+ }}>
+ {t("add")}
+
+ );
+ };
+
+ if (isPending || !data) {
+ return (
+
+ );
+ }
+
+ return (
+ <>
+ }
+ borderInShellHeader={true}
+ />
+
+
+
+ {data?.length ? (
+ <>
+
+ {data.map((apiKey, index) => (
+
{
+ setApiKeyToEdit(apiKey);
+ setApiKeyModal(true);
+ }}
+ />
+ ))}
+
+ >
+ ) : (
+
}
+ />
+ )}
+
+
+
+
+
+ setApiKeyModal(false)} defaultValues={apiKeyToEdit} />
+
+
+ >
+ );
+};
+
+ApiKeysView.getLayout = getLayout;
+ApiKeysView.PageWrapper = PageWrapper;
+
+export default ApiKeysView;
+"use client";
+
+import { useState } from "react";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import { Meta, showToast, SettingsToggle, SkeletonContainer, SkeletonText } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
+ return (
+
+
+
+
+
+
+ );
+};
+
+const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"] }) => {
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+ const [disableImpersonation, setDisableImpersonation] = useState(
+ user?.disableImpersonation
+ );
+
+ const mutation = trpc.viewer.updateProfile.useMutation({
+ onSuccess: () => {
+ showToast(t("profile_updated_successfully"), "success");
+ },
+ onSettled: () => {
+ utils.viewer.me.invalidate();
+ },
+ onMutate: async ({ disableImpersonation }) => {
+ await utils.viewer.me.cancel();
+ const previousValue = utils.viewer.me.getData();
+
+ setDisableImpersonation(disableImpersonation);
+
+ return { previousValue };
+ },
+ onError: (error, variables, context) => {
+ if (context?.previousValue) {
+ utils.viewer.me.setData(undefined, context.previousValue);
+ setDisableImpersonation(context.previousValue?.disableImpersonation);
+ }
+ showToast(`${t("error")}, ${error.message}`, "error");
+ },
+ });
+
+ return (
+ <>
+
+
+ {
+ mutation.mutate({ disableImpersonation: !checked });
+ }}
+ switchContainerClassName="rounded-t-none border-t-0"
+ disabled={mutation.isPending}
+ />
+
+ >
+ );
+};
+
+const ProfileImpersonationViewWrapper = () => {
+ const { data: user, isPending } = trpc.viewer.me.useQuery();
+ const { t } = useLocale();
+
+ if (isPending || !user)
+ return ;
+
+ return ;
+};
+
+ProfileImpersonationViewWrapper.getLayout = getLayout;
+ProfileImpersonationViewWrapper.PageWrapper = PageWrapper;
+
+export default ProfileImpersonationViewWrapper;
+import UserSSOView from "@calcom/features/ee/sso/page/user-sso-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = UserSSOView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+"use client";
+
+import { useState } from "react";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import {
+ Badge,
+ Meta,
+ SkeletonButton,
+ SkeletonContainer,
+ SkeletonText,
+ Alert,
+ SettingsToggle,
+} from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
+import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
+
+const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
+ return (
+
+
+
+
+ );
+};
+
+const TwoFactorAuthView = () => {
+ const utils = trpc.useContext();
+
+ const { t } = useLocale();
+ const { data: user, isPending } = trpc.viewer.me.useQuery();
+
+ const [enableModalOpen, setEnableModalOpen] = useState(false);
+ const [disableModalOpen, setDisableModalOpen] = useState(false);
+
+ if (isPending)
+ return ;
+
+ const isCalProvider = user?.identityProvider === "CAL";
+ const canSetupTwoFactor = !isCalProvider && !user?.twoFactorEnabled;
+ return (
+ <>
+
+ {canSetupTwoFactor && }
+
+ user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
+ }
+ Badge={
+
+ {user?.twoFactorEnabled ? t("enabled") : t("disabled")}
+
+ }
+ switchContainerClassName="rounded-t-none border-t-0"
+ />
+
+ setEnableModalOpen(!enableModalOpen)}
+ onEnable={() => {
+ setEnableModalOpen(false);
+ utils.viewer.me.invalidate();
+ }}
+ onCancel={() => {
+ setEnableModalOpen(false);
+ }}
+ />
+
+ setDisableModalOpen(!disableModalOpen)}
+ onDisable={() => {
+ setDisableModalOpen(false);
+ utils.viewer.me.invalidate();
+ }}
+ onCancel={() => {
+ setDisableModalOpen(false);
+ }}
+ />
+ >
+ );
+};
+
+TwoFactorAuthView.getLayout = getLayout;
+TwoFactorAuthView.PageWrapper = PageWrapper;
+
+export default TwoFactorAuthView;
+"use client";
+
+import { signOut, useSession } from "next-auth/react";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+
+import { identityProviderNameMap } from "@calcom/features/auth/lib/identityProviderNameMap";
+import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
+import { classNames } from "@calcom/lib";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { IdentityProvider } from "@calcom/prisma/enums";
+import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import { trpc } from "@calcom/trpc/react";
+import {
+ Alert,
+ Button,
+ Form,
+ Meta,
+ PasswordField,
+ Select,
+ SettingsToggle,
+ showToast,
+ SkeletonButton,
+ SkeletonContainer,
+ SkeletonText,
+} from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+type ChangePasswordSessionFormValues = {
+ oldPassword: string;
+ newPassword: string;
+ sessionTimeout?: number;
+ apiError: string;
+};
+
+interface PasswordViewProps {
+ user: RouterOutputs["viewer"]["me"];
+}
+
+const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const PasswordView = ({ user }: PasswordViewProps) => {
+ const { data } = useSession();
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+ const metadata = userMetadataSchema.safeParse(user?.metadata);
+ const initialSessionTimeout = metadata.success ? metadata.data?.sessionTimeout : undefined;
+
+ const [sessionTimeout, setSessionTimeout] = useState(initialSessionTimeout);
+
+ const sessionMutation = trpc.viewer.updateProfile.useMutation({
+ onSuccess: (data) => {
+ showToast(t("session_timeout_changed"), "success");
+ formMethods.reset(formMethods.getValues());
+ setSessionTimeout(data.metadata?.sessionTimeout);
+ },
+ onSettled: () => {
+ utils.viewer.me.invalidate();
+ },
+ onMutate: async () => {
+ await utils.viewer.me.cancel();
+ const previousValue = await utils.viewer.me.getData();
+ const previousMetadata = userMetadataSchema.safeParse(previousValue?.metadata);
+
+ if (previousValue && sessionTimeout && previousMetadata.success) {
+ utils.viewer.me.setData(undefined, {
+ ...previousValue,
+ metadata: { ...previousMetadata?.data, sessionTimeout: sessionTimeout },
+ });
+ return { previousValue };
+ }
+ },
+ onError: (error, _, context) => {
+ if (context?.previousValue) {
+ utils.viewer.me.setData(undefined, context.previousValue);
+ }
+ showToast(`${t("session_timeout_change_error")}, ${error.message}`, "error");
+ },
+ });
+ const passwordMutation = trpc.viewer.auth.changePassword.useMutation({
+ onSuccess: () => {
+ showToast(t("password_has_been_changed"), "success");
+ formMethods.resetField("oldPassword");
+ formMethods.resetField("newPassword");
+
+ if (data?.user.role === "INACTIVE_ADMIN") {
+ /*
+ AdminPasswordBanner component relies on the role returned from the session.
+ Next-Auth doesn't provide a way to revalidate the session cookie,
+ so this a workaround to hide the banner after updating the password.
+ discussion: https://github.com/nextauthjs/next-auth/discussions/4229
+ */
+ signOut({ callbackUrl: "/auth/login" });
+ }
+ },
+ onError: (error) => {
+ showToast(`${t("error_updating_password")}, ${t(error.message)}`, "error");
+
+ formMethods.setError("apiError", {
+ message: t(error.message),
+ type: "custom",
+ });
+ },
+ });
+
+ const formMethods = useForm({
+ defaultValues: {
+ oldPassword: "",
+ newPassword: "",
+ },
+ });
+
+ const handleSubmit = (values: ChangePasswordSessionFormValues) => {
+ const { oldPassword, newPassword } = values;
+
+ if (!oldPassword.length) {
+ formMethods.setError(
+ "oldPassword",
+ { type: "required", message: t("error_required_field") },
+ { shouldFocus: true }
+ );
+ }
+ if (!newPassword.length) {
+ formMethods.setError(
+ "newPassword",
+ { type: "required", message: t("error_required_field") },
+ { shouldFocus: true }
+ );
+ }
+
+ if (oldPassword && newPassword) {
+ passwordMutation.mutate({ oldPassword, newPassword });
+ }
+ };
+
+ const timeoutOptions = [5, 10, 15].map((mins) => ({
+ label: t("multiple_duration_mins", { count: mins }),
+ value: mins,
+ }));
+
+ const isDisabled = formMethods.formState.isSubmitting || !formMethods.formState.isDirty;
+
+ const passwordMinLength = data?.user.role === "USER" ? 7 : 15;
+ const isUser = data?.user.role === "USER";
+
+ return (
+ <>
+
+ {user && user.identityProvider !== IdentityProvider.CAL ? (
+
+
+ {t("account_managed_by_identity_provider", {
+ provider: identityProviderNameMap[user.identityProvider],
+ })}
+
+
+
+ {t("account_managed_by_identity_provider_description", {
+ provider: identityProviderNameMap[user.identityProvider],
+ })}
+
+
+ ) : (
+
+ )}
+ >
+ );
+};
+
+const PasswordViewWrapper = () => {
+ const { data: user, isPending } = trpc.viewer.me.useQuery();
+ const { t } = useLocale();
+ if (isPending || !user)
+ return ;
+
+ return ;
+};
+
+PasswordViewWrapper.getLayout = getLayout;
+PasswordViewWrapper.PageWrapper = PageWrapper;
+
+export default PasswordViewWrapper;
+"use client";
+
+import { Meta } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+import { getLayout } from "@components/auth/layouts/AdminLayout";
+
+function AdminAppsView() {
+ return (
+ <>
+
+ Admin index
+ >
+ );
+}
+
+AdminAppsView.getLayout = getLayout;
+AdminAppsView.PageWrapper = PageWrapper;
+
+export default AdminAppsView;
+import AdminOrgsPage from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = AdminOrgsPage as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import OrgEditView from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgEditPage";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = OrgEditView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import { zodResolver } from "@hookform/resolvers/zod";
+import type { GetServerSidePropsContext } from "next";
+import { getSession } from "next-auth/react";
+import type { TFunction } from "next-i18next";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import z from "zod";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { UserPermissionRole } from "@calcom/prisma/enums";
+import { getStringAsNumberRequiredSchema } from "@calcom/prisma/zod-utils";
+import { Button, Form, Meta, TextField, showToast } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+import { getLayout } from "./_OrgMigrationLayout";
+
+function Wrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
+
+const enum State {
+ IDLE,
+ LOADING,
+ SUCCESS,
+ ERROR,
+}
+
+export const getFormSchema = (t: TFunction) =>
+ z.object({
+ targetOrgId: z.union([getStringAsNumberRequiredSchema(t), z.number()]),
+ teamId: z.union([getStringAsNumberRequiredSchema(t), z.number()]),
+ });
+
+export default function RemoveTeamFromOrg() {
+ const [state, setState] = useState(State.IDLE);
+ const { t } = useLocale();
+ const formSchema = getFormSchema(t);
+ const form = useForm({
+ mode: "onSubmit",
+ resolver: zodResolver(formSchema),
+ });
+
+ const register = form.register;
+
+ return (
+
+ {/* Due to some reason auth from website doesn't work if /api endpoint is used. Spent a lot of time and in the end went with submitting data to the same page, because you can't do POST request to a page in Next.js, doing a GET request */}
+
+
+ );
+}
+
+export async function getServerSideProps(ctx: GetServerSidePropsContext) {
+ const session = await getSession(ctx);
+ if (!session || !session.user) {
+ return {
+ redirect: {
+ destination: "/login",
+ permanent: false,
+ },
+ };
+ }
+
+ const isAdmin = session.user.role === UserPermissionRole.ADMIN;
+ if (!isAdmin) {
+ return {
+ redirect: {
+ destination: "/",
+ permanent: false,
+ },
+ };
+ }
+ return {
+ props: {
+ ...(await serverSideTranslations(ctx.locale || "en", ["common"])),
+ },
+ };
+}
+
+RemoveTeamFromOrg.PageWrapper = PageWrapper;
+RemoveTeamFromOrg.getLayout = getLayout;
+import { getLayout as getSettingsLayout } from "@calcom/features/settings/layouts/SettingsLayout";
+import { HorizontalTabs } from "@calcom/ui";
+
+export default function OrgMigrationLayout({ children }: { children: React.ReactElement }) {
+ return getSettingsLayout(
+
+
+ {children}
+
+ );
+}
+export const getLayout = (page: React.ReactElement) => {
+ return {page} ;
+};
+import { zodResolver } from "@hookform/resolvers/zod";
+import type { GetServerSidePropsContext } from "next";
+import { getSession } from "next-auth/react";
+import type { TFunction } from "next-i18next";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+import z from "zod";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { UserPermissionRole } from "@calcom/prisma/enums";
+import { getStringAsNumberRequiredSchema } from "@calcom/prisma/zod-utils";
+import { Button, TextField, Meta, showToast, Form } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+import { getLayout } from "./_OrgMigrationLayout";
+
+function Wrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
+
+const enum State {
+ IDLE,
+ LOADING,
+ SUCCESS,
+ ERROR,
+}
+
+export const getFormSchema = (t: TFunction) =>
+ z.object({
+ userId: z.union([getStringAsNumberRequiredSchema(t), z.number()]),
+ targetOrgId: z.union([getStringAsNumberRequiredSchema(t), z.number()]),
+ });
+
+export default function RemoveUserFromOrg() {
+ const [state, setState] = useState(State.IDLE);
+ const { t } = useLocale();
+ const formSchema = getFormSchema(t);
+ const form = useForm({
+ mode: "onSubmit",
+ resolver: zodResolver(formSchema),
+ });
+ const register = form.register;
+ return (
+
+ {/* Due to some reason auth from website doesn't work if /api endpoint is used. Spent a lot of time and in the end went with submitting data to the same page, because you can't do POST request to a page in Next.js, doing a GET request */}
+
+
+ );
+}
+
+export async function getServerSideProps(ctx: GetServerSidePropsContext) {
+ const session = await getSession(ctx);
+ if (!session || !session.user) {
+ return {
+ redirect: {
+ destination: "/login",
+ permanent: false,
+ },
+ };
+ }
+
+ const isAdmin = session.user.role === UserPermissionRole.ADMIN;
+ if (!isAdmin) {
+ return {
+ redirect: {
+ destination: "/",
+ permanent: false,
+ },
+ };
+ }
+ return {
+ props: {
+ ...(await serverSideTranslations(ctx.locale || "en", ["common"])),
+ },
+ };
+}
+
+RemoveUserFromOrg.PageWrapper = PageWrapper;
+RemoveUserFromOrg.getLayout = getLayout;
+import { zodResolver } from "@hookform/resolvers/zod";
+import type { GetServerSidePropsContext } from "next";
+import { getSession } from "next-auth/react";
+import type { TFunction } from "next-i18next";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+import { useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { UserPermissionRole } from "@calcom/prisma/enums";
+import { getStringAsNumberRequiredSchema } from "@calcom/prisma/zod-utils";
+import { Button, Form, Meta, SelectField, TextField, showToast } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+import { getLayout } from "./_OrgMigrationLayout";
+
+export const getFormSchema = (t: TFunction) => {
+ return z.object({
+ teamId: z.number().or(getStringAsNumberRequiredSchema(t)),
+ targetOrgId: z.number().or(getStringAsNumberRequiredSchema(t)),
+ moveMembers: z.boolean(),
+ teamSlugInOrganization: z.string(),
+ });
+};
+
+function Wrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
+
+const enum State {
+ IDLE,
+ LOADING,
+ SUCCESS,
+ ERROR,
+}
+
+export default function MoveTeamToOrg() {
+ const [state, setState] = useState(State.IDLE);
+ const moveUsersOptions = [
+ {
+ label: "No",
+ value: "false",
+ },
+ {
+ label: "Yes",
+ value: "true",
+ },
+ ];
+ const { t } = useLocale();
+ const formSchema = getFormSchema(t);
+ const formMethods = useForm({
+ mode: "onSubmit",
+ resolver: zodResolver(formSchema),
+ });
+
+ const { register, watch } = formMethods;
+ const moveMembers = watch("moveMembers");
+ return (
+
+
+
+ );
+}
+
+export async function getServerSideProps(ctx: GetServerSidePropsContext) {
+ const session = await getSession(ctx);
+ if (!session || !session.user) {
+ return {
+ redirect: {
+ destination: "/login",
+ permanent: false,
+ },
+ };
+ }
+
+ const isAdmin = session.user.role === UserPermissionRole.ADMIN;
+ if (!isAdmin) {
+ return {
+ redirect: {
+ destination: "/",
+ permanent: false,
+ },
+ };
+ }
+ return {
+ props: {
+ error: null,
+ migrated: null,
+ userId: session.user.id,
+ ...(await serverSideTranslations(ctx.locale || "en", ["common"])),
+ username: session.user.username,
+ },
+ };
+}
+
+MoveTeamToOrg.PageWrapper = PageWrapper;
+MoveTeamToOrg.getLayout = getLayout;
+/**
+ * It could be an admin feature to move a user to an organization but because it's a temporary thing before mono-user orgs are implemented, it's not right to spend time on it.
+ * Plus, we need to do it only for cal.com and not provide as a feature to our self hosters.
+ */
+import { zodResolver } from "@hookform/resolvers/zod";
+import type { GetServerSidePropsContext } from "next";
+import { getSession } from "next-auth/react";
+import type { TFunction } from "next-i18next";
+import { serverSideTranslations } from "next-i18next/serverSideTranslations";
+import { useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import z from "zod";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { MembershipRole } from "@calcom/prisma/client";
+import { UserPermissionRole } from "@calcom/prisma/enums";
+import { getStringAsNumberRequiredSchema } from "@calcom/prisma/zod-utils";
+import { Button, Form, Meta, SelectField, TextField, showToast } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+import { getLayout } from "./_OrgMigrationLayout";
+
+function Wrapper({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ );
+}
+
+export const getFormSchema = (t: TFunction) =>
+ z.object({
+ userId: z.union([z.string().pipe(z.coerce.number()), z.number()]).optional(),
+ userName: z.string().optional(),
+ targetOrgId: z.union([getStringAsNumberRequiredSchema(t), z.number()]),
+ targetOrgUsername: z.string().min(1, t("error_required_field")),
+ shouldMoveTeams: z.boolean(),
+ targetOrgRole: z.union([
+ z.literal(MembershipRole.ADMIN),
+ z.literal(MembershipRole.MEMBER),
+ z.literal(MembershipRole.OWNER),
+ ]),
+ });
+
+const enum State {
+ IDLE,
+ LOADING,
+ SUCCESS,
+ ERROR,
+}
+export default function MoveUserToOrg() {
+ const [state, setState] = useState(State.IDLE);
+
+ const roles = Object.values(MembershipRole).map((role) => ({
+ label: role,
+ value: role,
+ }));
+
+ const moveTeamsOptions = [
+ {
+ label: "Yes",
+ value: "true",
+ },
+ {
+ label: "No",
+ value: "false",
+ },
+ ];
+ const { t } = useLocale();
+ const formSchema = getFormSchema(t);
+ const form = useForm({
+ mode: "onSubmit",
+ resolver: zodResolver(formSchema),
+ });
+
+ const shouldMoveTeams = form.watch("shouldMoveTeams");
+ const register = form.register;
+ return (
+
+ {/* Due to some reason auth from website doesn't work if /api endpoint is used. Spent a lot of time and in the end went with submitting data to the same page, because you can't do POST request to a page in Next.js, doing a GET request */}
+
+
+ );
+}
+
+export async function getServerSideProps(ctx: GetServerSidePropsContext) {
+ const session = await getSession(ctx);
+ if (!session || !session.user) {
+ return {
+ redirect: {
+ destination: "/login",
+ permanent: false,
+ },
+ };
+ }
+
+ const isAdmin = session.user.role === UserPermissionRole.ADMIN;
+ if (!isAdmin) {
+ return {
+ redirect: {
+ destination: "/",
+ permanent: false,
+ },
+ };
+ }
+ return {
+ props: {
+ ...(await serverSideTranslations(ctx.locale || "en", ["common"])),
+ },
+ };
+}
+
+MoveUserToOrg.PageWrapper = PageWrapper;
+MoveUserToOrg.getLayout = getLayout;
+"use client";
+
+import PageWrapper from "@components/PageWrapper";
+import { getLayout } from "@components/auth/layouts/AdminLayout";
+
+import OAuthView from "./oAuthView";
+
+const OAuthPage = () => ;
+
+OAuthPage.getLayout = getLayout;
+OAuthPage.PageWrapper = PageWrapper;
+
+export default OAuthPage;
+"use client";
+
+import { useState } from "react";
+import { useForm } from "react-hook-form";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc";
+import { Meta, Form, Button, TextField, showToast, Tooltip, ImageUploader, Avatar } from "@calcom/ui";
+import { Clipboard } from "@calcom/ui/components/icon";
+import { Plus } from "@calcom/ui/components/icon";
+
+type FormValues = {
+ name: string;
+ redirectUri: string;
+ logo: string;
+};
+
+export default function OAuthView() {
+ const oAuthForm = useForm();
+ const [clientSecret, setClientSecret] = useState("");
+ const [clientId, setClientId] = useState("");
+ const [logo, setLogo] = useState("");
+ const { t } = useLocale();
+
+ const mutation = trpc.viewer.oAuth.addClient.useMutation({
+ onSuccess: async (data) => {
+ setClientSecret(data.clientSecret);
+ setClientId(data.clientId);
+ showToast(`Successfully added ${data.name} as new client`, "success");
+ },
+ onError: (error) => {
+ showToast(`Adding clientfailed: ${error.message}`, "error");
+ },
+ });
+
+ return (
+
+
+ {!clientId ? (
+
+ ) : (
+
+
{oAuthForm.getValues("name")}
+
Client Id
+
+
+ {" "}
+ {clientId}
+
+
+ {
+ navigator.clipboard.writeText(clientId);
+ showToast("Client ID copied!", "success");
+ }}
+ type="button"
+ className="rounded-l-none text-base"
+ StartIcon={Clipboard}>
+ {t("copy")}
+
+
+
+ {clientSecret ? (
+ <>
+
Client Secret
+
+
+ {" "}
+ {clientSecret}
+
+
+ {
+ navigator.clipboard.writeText(clientSecret);
+ setClientSecret("");
+ showToast("Client secret copied!", "success");
+ }}
+ type="button"
+ className="rounded-l-none text-base"
+ StartIcon={Clipboard}>
+ {t("copy")}
+
+
+
+
{t("copy_client_secret_info")}
+ >
+ ) : (
+ <>>
+ )}
+
{
+ setClientId("");
+ setLogo("");
+ oAuthForm.reset();
+ }}
+ className="mt-5">
+ {t("add_new_client")}
+
+
+ )}
+
+ );
+}
+"use client";
+
+import { signIn } from "next-auth/react";
+import { useRef } from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, Meta, TextField } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+import { getLayout } from "@components/auth/layouts/AdminLayout";
+
+function AdminView() {
+ const { t } = useLocale();
+ const usernameRef = useRef(null);
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+AdminView.getLayout = getLayout;
+AdminView.PageWrapper = PageWrapper;
+
+export default AdminView;
+import UsersListingView from "@calcom/features/ee/users/pages/users-listing-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = UsersListingView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import UsersAddView from "@calcom/features/ee/users/pages/users-add-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = UsersAddView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import UsersEditView from "@calcom/features/ee/users/pages/users-edit-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = UsersEditView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+"use client";
+export { default } from "./[category]";
+"use client";
+
+import AdminAppsList from "@calcom/features/apps/AdminAppsList";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Meta } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+import { getLayout } from "@components/auth/layouts/AdminLayout";
+
+function AdminAppsView() {
+ const { t } = useLocale();
+ return (
+ <>
+
+
+ >
+ );
+}
+
+AdminAppsView.getLayout = getLayout;
+AdminAppsView.PageWrapper = PageWrapper;
+
+export default AdminAppsView;
+"use client";
+
+import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view";
+
+import PageWrapper from "@components/PageWrapper";
+import { getLayout } from "@components/auth/layouts/AdminLayout";
+
+const FlagsPage = () => ;
+
+FlagsPage.getLayout = getLayout;
+FlagsPage.PageWrapper = PageWrapper;
+
+export default FlagsPage;
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { signOut, useSession } from "next-auth/react";
+import type { BaseSyntheticEvent } from "react";
+import React, { useRef, useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import { z } from "zod";
+
+import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
+import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
+import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
+import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { md } from "@calcom/lib/markdownIt";
+import turndown from "@calcom/lib/turndownService";
+import { IdentityProvider } from "@calcom/prisma/enums";
+import type { TRPCClientErrorLike } from "@calcom/trpc/client";
+import { trpc } from "@calcom/trpc/react";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import type { AppRouter } from "@calcom/trpc/server/routers/_app";
+import type { Ensure } from "@calcom/types/utils";
+import {
+ Alert,
+ Button,
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogFooter,
+ DialogTrigger,
+ Editor,
+ Form,
+ ImageUploader,
+ Label,
+ Meta,
+ PasswordField,
+ showToast,
+ SkeletonAvatar,
+ SkeletonButton,
+ SkeletonContainer,
+ SkeletonText,
+ TextField,
+} from "@calcom/ui";
+import { UserAvatar } from "@calcom/ui";
+import { AlertTriangle, Trash2 } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+import TwoFactor from "@components/auth/TwoFactor";
+import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
+
+const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
+ return (
+
+
+
+
+ );
+};
+
+interface DeleteAccountValues {
+ totpCode: string;
+}
+
+type FormValues = {
+ username: string;
+ avatar: string;
+ name: string;
+ email: string;
+ bio: string;
+};
+
+const ProfileView = () => {
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+ const { update } = useSession();
+ const { data: user, isPending } = trpc.viewer.me.useQuery();
+
+ const { data: avatarData } = trpc.viewer.avatar.useQuery(undefined, {
+ enabled: !isPending && !user?.avatarUrl,
+ });
+
+ const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
+ onSuccess: async (res) => {
+ await update(res);
+ showToast(t("settings_updated_successfully"), "success");
+
+ // signout user only in case of password reset
+ if (res.signOutUser && tempFormValues && res.passwordReset) {
+ showToast(t("password_reset_email", { email: tempFormValues.email }), "success");
+ await signOut({ callbackUrl: "/auth/logout?passReset=true" });
+ } else {
+ utils.viewer.me.invalidate();
+ utils.viewer.avatar.invalidate();
+ utils.viewer.shouldVerifyEmail.invalidate();
+ }
+
+ setConfirmAuthEmailChangeWarningDialogOpen(false);
+ setTempFormValues(null);
+ },
+ onError: (e) => {
+ switch (e.message) {
+ // TODO: Add error codes.
+ case "email_already_used":
+ {
+ showToast(t(e.message), "error");
+ }
+ return;
+ default:
+ showToast(t("error_updating_settings"), "error");
+ }
+ },
+ });
+
+ const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false);
+ const [tempFormValues, setTempFormValues] = useState(null);
+ const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState("");
+ const [confirmAuthEmailChangeWarningDialogOpen, setConfirmAuthEmailChangeWarningDialogOpen] =
+ useState(false);
+
+ const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
+ const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
+ const [deleteErrorMessage, setDeleteErrorMessage] = useState("");
+ const form = useForm();
+
+ const onDeleteMeSuccessMutation = async () => {
+ await utils.viewer.me.invalidate();
+ showToast(t("Your account was deleted"), "success");
+
+ setHasDeleteErrors(false); // dismiss any open errors
+ if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
+ signOut({ callbackUrl: "/auth/logout?survey=true" });
+ } else {
+ signOut({ callbackUrl: "/auth/logout" });
+ }
+ };
+
+ const confirmPasswordMutation = trpc.viewer.auth.verifyPassword.useMutation({
+ onSuccess() {
+ if (tempFormValues) updateProfileMutation.mutate(tempFormValues);
+ setConfirmPasswordOpen(false);
+ },
+ onError() {
+ setConfirmPasswordDeleteErrorMessage(t("incorrect_password"));
+ },
+ });
+
+ const onDeleteMeErrorMutation = (error: TRPCClientErrorLike) => {
+ setHasDeleteErrors(true);
+ setDeleteErrorMessage(errorMessages[error.message]);
+ };
+ const deleteMeMutation = trpc.viewer.deleteMe.useMutation({
+ onSuccess: onDeleteMeSuccessMutation,
+ onError: onDeleteMeErrorMutation,
+ async onSettled() {
+ await utils.viewer.me.invalidate();
+ },
+ });
+ const deleteMeWithoutPasswordMutation = trpc.viewer.deleteMeWithoutPassword.useMutation({
+ onSuccess: onDeleteMeSuccessMutation,
+ onError: onDeleteMeErrorMutation,
+ async onSettled() {
+ await utils.viewer.me.invalidate();
+ },
+ });
+
+ const isCALIdentityProvider = user?.identityProvider === IdentityProvider.CAL;
+
+ const onConfirmPassword = (e: Event | React.MouseEvent) => {
+ e.preventDefault();
+
+ const password = passwordRef.current.value;
+ confirmPasswordMutation.mutate({ passwordInput: password });
+ };
+
+ const onConfirmAuthEmailChange = (e: Event | React.MouseEvent) => {
+ e.preventDefault();
+
+ if (tempFormValues) updateProfileMutation.mutate(tempFormValues);
+ };
+
+ const onConfirmButton = (e: Event | React.MouseEvent) => {
+ e.preventDefault();
+ if (isCALIdentityProvider) {
+ const totpCode = form.getValues("totpCode");
+ const password = passwordRef.current.value;
+ deleteMeMutation.mutate({ password, totpCode });
+ } else {
+ deleteMeWithoutPasswordMutation.mutate();
+ }
+ };
+
+ const onConfirm = ({ totpCode }: DeleteAccountValues, e: BaseSyntheticEvent | undefined) => {
+ e?.preventDefault();
+ if (isCALIdentityProvider) {
+ const password = passwordRef.current.value;
+ deleteMeMutation.mutate({ password, totpCode });
+ } else {
+ deleteMeWithoutPasswordMutation.mutate();
+ }
+ };
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const passwordRef = useRef(null!);
+
+ const errorMessages: { [key: string]: string } = {
+ [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
+ [ErrorCode.IncorrectPassword]: `${t("incorrect_password")} ${t("please_try_again")}`,
+ [ErrorCode.UserNotFound]: t("no_account_exists"),
+ [ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`,
+ [ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
+ [ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
+ };
+
+ if (isPending || !user) {
+ return (
+
+ );
+ }
+
+ const defaultValues = {
+ username: user.username || "",
+ avatar: getUserAvatarUrl(user),
+ name: user.name || "",
+ email: user.email || "",
+ bio: user.bio || "",
+ };
+
+ return (
+ <>
+
+ {
+ if (values.email !== user.email && isCALIdentityProvider) {
+ setTempFormValues(values);
+ setConfirmPasswordOpen(true);
+ } else if (values.email !== user.email && !isCALIdentityProvider) {
+ setTempFormValues(values);
+ // Opens a dialog warning the change
+ setConfirmAuthEmailChangeWarningDialogOpen(true);
+ } else {
+ updateProfileMutation.mutate(values);
+ }
+ }}
+ extraField={
+
+ {
+ showToast(t("settings_updated_successfully"), "success");
+ await utils.viewer.me.invalidate();
+ }}
+ onErrorMutation={() => {
+ showToast(t("error_updating_settings"), "error");
+ }}
+ />
+
+ }
+ />
+
+
+
{t("danger_zone")}
+
{t("account_deletion_cannot_be_undone")}
+
+ {/* Delete account Dialog */}
+
+
+
+
+ {t("delete_account")}
+
+
+
+
+ <>
+
+
{t("delete_account_confirmation_message")}
+ {isCALIdentityProvider && (
+
+ )}
+
+ {user?.twoFactorEnabled && isCALIdentityProvider && (
+
+ )}
+
+ {hasDeleteErrors &&
}
+
+
+
+ onConfirmButton(e)}>
+ {t("delete_my_account")}
+
+
+ >
+
+
+
+ {/* If changing email, confirm password */}
+
+
+
+
+
+ {confirmPasswordErrorMessage &&
}
+
+
+ onConfirmPassword(e)}>
+ {t("confirm")}
+
+
+
+
+
+
+ {/* If changing email from !CAL Login */}
+
+
+
+ onConfirmAuthEmailChange(e)}>
+ {t("confirm")}
+
+
+
+
+
+ >
+ );
+};
+
+const ProfileForm = ({
+ defaultValues,
+ onSubmit,
+ extraField,
+ isPending = false,
+ isFallbackImg,
+ user,
+ userOrganization,
+}: {
+ defaultValues: FormValues;
+ onSubmit: (values: FormValues) => void;
+ extraField?: React.ReactNode;
+ isPending: boolean;
+ isFallbackImg: boolean;
+ user: RouterOutputs["viewer"]["me"];
+ userOrganization: RouterOutputs["viewer"]["me"]["organization"];
+}) => {
+ const { t } = useLocale();
+ const [firstRender, setFirstRender] = useState(true);
+
+ const profileFormSchema = z.object({
+ username: z.string(),
+ avatar: z.string(),
+ name: z
+ .string()
+ .trim()
+ .min(1, t("you_need_to_add_a_name"))
+ .max(FULL_NAME_LENGTH_MAX_LIMIT, {
+ message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
+ }),
+ email: z.string().email(),
+ bio: z.string(),
+ });
+
+ const formMethods = useForm({
+ defaultValues,
+ resolver: zodResolver(profileFormSchema),
+ });
+
+ const {
+ formState: { isSubmitting, isDirty },
+ } = formMethods;
+
+ const isDisabled = isSubmitting || !isDirty;
+ return (
+
+ );
+};
+
+ProfileView.getLayout = getLayout;
+ProfileView.PageWrapper = PageWrapper;
+
+export default ProfileView;
+import { Trash2 } from "lucide-react";
+import React, { useState } from "react";
+import { Controller, useForm, useFormState } from "react-hook-form";
+
+import dayjs from "@calcom/dayjs";
+import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
+import { ShellMain } from "@calcom/features/shell/Shell";
+import { useHasTeamPlan } from "@calcom/lib/hooks/useHasPaidPlan";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
+import {
+ Button,
+ Meta,
+ showToast,
+ Select,
+ SkeletonText,
+ UpgradeTeamsBadge,
+ Switch,
+ DateRangePicker,
+} from "@calcom/ui";
+import { TableNew, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+export type BookingRedirectForm = {
+ dateRange: { startDate: Date; endDate: Date };
+ offset: number;
+ toTeamUserId: number | null;
+};
+
+const OutOfOfficeSection = () => {
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+
+ const [profileRedirect, setProfileRedirect] = useState(false);
+ const [selectedMember, setSelectedMember] = useState<{ label: string; value: number | null } | null>(null);
+
+ const [dateRange] = useState<{ startDate: Date; endDate: Date }>({
+ startDate: dayjs().startOf("d").toDate(),
+ endDate: dayjs().add(1, "d").endOf("d").toDate(),
+ });
+
+ const { handleSubmit, setValue, getValues, control } = useForm({
+ defaultValues: {
+ dateRange: {
+ startDate: dateRange.startDate,
+ endDate: dateRange.endDate,
+ },
+ offset: dayjs().utcOffset(),
+ toTeamUserId: null,
+ },
+ });
+
+ const createOutOfOfficeEntry = trpc.viewer.outOfOfficeCreate.useMutation({
+ onSuccess: () => {
+ showToast(t("success_entry_created"), "success");
+ utils.viewer.outOfOfficeEntriesList.invalidate();
+ setProfileRedirect(false);
+ },
+ onError: (error) => {
+ showToast(t(error.message), "error");
+ },
+ });
+
+ const { hasTeamPlan } = useHasTeamPlan();
+ const { data: listMembers } = trpc.viewer.teams.listMembers.useQuery({});
+ const me = useMeQuery();
+ const memberListOptions: {
+ value: number | null;
+ label: string;
+ }[] =
+ listMembers
+ ?.filter((member) => me?.data?.id !== member.id)
+ .map((member) => ({
+ value: member.id || null,
+ label: member.name || "",
+ })) || [];
+
+ return (
+ <>
+
+ >
+ );
+};
+
+const OutOfOfficeEntriesList = () => {
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+ const { data, isPending } = trpc.viewer.outOfOfficeEntriesList.useQuery();
+ const deleteOutOfOfficeEntryMutation = trpc.viewer.outOfOfficeEntryDelete.useMutation({
+ onSuccess: () => {
+ showToast(t("success_deleted_entry_out_of_office"), "success");
+ utils.viewer.outOfOfficeEntriesList.invalidate();
+ useFormState;
+ },
+ onError: () => {
+ showToast(`An error ocurred`, "error");
+ },
+ });
+ if (data === null || data?.length === 0 || data === undefined) return null;
+ return (
+
+
+
+
+ {t("time_range")}
+ {t("redirect_to")}
+
+ {t("action")}
+
+
+
+ {data?.map((item) => (
+
+
+
+ {dayjs.utc(item.start).format("ll")} - {dayjs.utc(item.end).format("ll")}
+
+
+
+ {item.toUser?.username || "N/A"}
+
+
+ {
+ deleteOutOfOfficeEntryMutation.mutate({ outOfOfficeUid: item.uuid });
+ }}
+ />
+
+
+ ))}
+ {isPending && (
+
+ {new Array(6).fill(0).map((_, index) => (
+
+
+
+ ))}
+
+ )}
+
+ {!isPending && (data === undefined || data.length === 0) && (
+
+
+ {t("no_redirects_found")}
+
+
+ )}
+
+
+
+ );
+};
+
+const OutOfOfficePage = () => {
+ const { t } = useLocale();
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
+
+OutOfOfficePage.getLayout = getLayout;
+OutOfOfficePage.PageWrapper = PageWrapper;
+
+export default OutOfOfficePage;
+"use client";
+
+import { useReducer } from "react";
+
+import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Button, EmptyScreen, Meta, SkeletonContainer, SkeletonText } from "@calcom/ui";
+import { Calendar, Plus } from "@calcom/ui/components/icon";
+
+import { QueryCell } from "@lib/QueryCell";
+
+import PageWrapper from "@components/PageWrapper";
+import { AppList } from "@components/apps/AppList";
+
+const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
+ return (
+
+
+
+
+
+
+
+ );
+};
+
+const AddConferencingButton = () => {
+ const { t } = useLocale();
+
+ return (
+
+ {t("add")}
+
+ );
+};
+
+type ModalState = {
+ isOpen: boolean;
+ credentialId: null | number;
+};
+
+const ConferencingLayout = () => {
+ const { t } = useLocale();
+
+ const [modal, updateModal] = useReducer(
+ (data: ModalState, partialData: Partial) => ({ ...data, ...partialData }),
+ {
+ isOpen: false,
+ credentialId: null,
+ }
+ );
+
+ const query = trpc.viewer.integrations.useQuery({
+ variant: "conferencing",
+ onlyInstalled: true,
+ });
+
+ const handleModelClose = () => {
+ updateModal({ isOpen: false, credentialId: null });
+ };
+
+ const handleDisconnect = (credentialId: number) => {
+ updateModal({ isOpen: true, credentialId });
+ };
+
+ return (
+ <>
+
+
}
+ borderInShellHeader={true}
+ />
+
+ }
+ success={({ data }) => {
+ console.log(data);
+ if (!data.items.length) {
+ return (
+
+ {t("connect_conference_apps")}
+
+ }
+ />
+ );
+ }
+ return (
+
+ );
+ }}
+ />
+
+
+ >
+ );
+};
+
+ConferencingLayout.getLayout = getLayout;
+ConferencingLayout.PageWrapper = PageWrapper;
+
+export default ConferencingLayout;
+"use client";
+
+import { useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+import type { z } from "zod";
+
+import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
+import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
+import ThemeLabel from "@calcom/features/settings/ThemeLabel";
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
+import { APP_NAME } from "@calcom/lib/constants";
+import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants";
+import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours";
+import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
+import type { userMetadata } from "@calcom/prisma/zod-utils";
+import { trpc } from "@calcom/trpc/react";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import {
+ Alert,
+ Button,
+ ColorPicker,
+ Form,
+ Meta,
+ showToast,
+ SkeletonButton,
+ SkeletonContainer,
+ SkeletonText,
+ SettingsToggle,
+ UpgradeTeamsBadge,
+} from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const AppearanceView = ({
+ user,
+ hasPaidPlan,
+}: {
+ user: RouterOutputs["viewer"]["me"];
+ hasPaidPlan: boolean;
+}) => {
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+ const [darkModeError, setDarkModeError] = useState(false);
+ const [lightModeError, setLightModeError] = useState(false);
+ const [isCustomBrandColorChecked, setIsCustomBranColorChecked] = useState(
+ user?.brandColor !== DEFAULT_LIGHT_BRAND_COLOR || user?.darkBrandColor !== DEFAULT_DARK_BRAND_COLOR
+ );
+ const [hideBrandingValue, setHideBrandingValue] = useState(user?.hideBranding ?? false);
+
+ const userThemeFormMethods = useForm({
+ defaultValues: {
+ theme: user.theme,
+ },
+ });
+
+ const {
+ formState: { isSubmitting: isUserThemeSubmitting, isDirty: isUserThemeDirty },
+ reset: resetUserThemeReset,
+ } = userThemeFormMethods;
+
+ const bookerLayoutFormMethods = useForm({
+ defaultValues: {
+ metadata: user.metadata as z.infer,
+ },
+ });
+
+ const {
+ formState: { isSubmitting: isBookerLayoutFormSubmitting, isDirty: isBookerLayoutFormDirty },
+ reset: resetBookerLayoutThemeReset,
+ } = bookerLayoutFormMethods;
+
+ const DEFAULT_BRAND_COLOURS = {
+ light: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
+ dark: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR,
+ };
+
+ const brandColorsFormMethods = useForm({
+ defaultValues: {
+ brandColor: DEFAULT_BRAND_COLOURS.light,
+ darkBrandColor: DEFAULT_BRAND_COLOURS.dark,
+ },
+ });
+
+ const {
+ formState: { isSubmitting: isBrandColorsFormSubmitting, isDirty: isBrandColorsFormDirty },
+ reset: resetBrandColorsThemeReset,
+ } = brandColorsFormMethods;
+
+ const selectedTheme = userThemeFormMethods.watch("theme");
+ const selectedThemeIsDark =
+ selectedTheme === "dark" ||
+ (selectedTheme === "" &&
+ typeof document !== "undefined" &&
+ document.documentElement.classList.contains("dark"));
+
+ const mutation = trpc.viewer.updateProfile.useMutation({
+ onSuccess: async (data) => {
+ await utils.viewer.me.invalidate();
+ showToast(t("settings_updated_successfully"), "success");
+ resetBrandColorsThemeReset({ brandColor: data.brandColor, darkBrandColor: data.darkBrandColor });
+ resetBookerLayoutThemeReset({ metadata: data.metadata });
+ resetUserThemeReset({ theme: data.theme });
+ },
+ onError: (error) => {
+ if (error.message) {
+ showToast(error.message, "error");
+ } else {
+ showToast(t("error_updating_settings"), "error");
+ }
+ },
+ });
+
+ return (
+
+
+
+
+
{t("theme")}
+
{t("theme_applies_note")}
+
+
+
+
+
+
+
+
+ {/* TODO future PR to preview brandColors */}
+ {/*
window.open(`${WEBAPP_URL}/${user.username}/${user.eventTypes[0].title}`, "_blank")}>
+ Preview
+ */}
+
+
}
+ onCheckedChange={(checked) => {
+ setHideBrandingValue(checked);
+ mutation.mutate({ hideBranding: checked });
+ }}
+ switchContainerClassName="mt-6"
+ />
+
+ );
+};
+
+const AppearanceViewWrapper = () => {
+ const { data: user, isPending } = trpc.viewer.me.useQuery();
+ const { isPending: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan();
+
+ const { t } = useLocale();
+
+ if (isPending || isTeamPlanStatusLoading || !user)
+ return ;
+
+ return ;
+};
+
+AppearanceViewWrapper.getLayout = getLayout;
+AppearanceViewWrapper.PageWrapper = PageWrapper;
+
+export default AppearanceViewWrapper;
+"use client";
+
+import { Fragment } from "react";
+
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, Meta, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui";
+import { Plus } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+import { CalendarListContainer } from "@components/apps/CalendarListContainer";
+
+const SkeletonLoader = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const AddCalendarButton = () => {
+ const { t } = useLocale();
+
+ return (
+ <>
+
+ {t("add_calendar")}
+
+ >
+ );
+};
+
+const CalendarsView = () => {
+ const { t } = useLocale();
+
+ return (
+ <>
+ }
+ borderInShellHeader={false}
+ />
+
+
+
+ >
+ );
+};
+
+CalendarsView.getLayout = getLayout;
+CalendarsView.PageWrapper = PageWrapper;
+
+export default CalendarsView;
+"use client";
+
+import { useSession } from "next-auth/react";
+import { useState } from "react";
+import { Controller, useForm } from "react-hook-form";
+
+import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { localeOptions } from "@calcom/lib/i18n";
+import { nameOfDay } from "@calcom/lib/weekday";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import { trpc } from "@calcom/trpc/react";
+import {
+ Button,
+ Form,
+ Label,
+ Meta,
+ Select,
+ showToast,
+ SkeletonButton,
+ SkeletonContainer,
+ SkeletonText,
+ TimezoneSelect,
+ SettingsToggle,
+} from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+interface GeneralViewProps {
+ localeProp: string;
+ user: RouterOutputs["viewer"]["me"];
+}
+
+const GeneralQueryView = () => {
+ const { t } = useLocale();
+
+ const { data: user, isPending } = trpc.viewer.me.useQuery();
+ if (isPending) return ;
+ if (!user) {
+ throw new Error(t("something_went_wrong"));
+ }
+ return ;
+};
+
+const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
+ const utils = trpc.useContext();
+ const { t } = useLocale();
+ const { update } = useSession();
+ const [isUpdateBtnLoading, setIsUpdateBtnLoading] = useState(false);
+
+ const mutation = trpc.viewer.updateProfile.useMutation({
+ onSuccess: async (res) => {
+ await utils.viewer.me.invalidate();
+ reset(getValues());
+ showToast(t("settings_updated_successfully"), "success");
+ await update(res);
+
+ if (res.locale) {
+ window.calNewLocale = res.locale;
+ }
+ },
+ onError: () => {
+ showToast(t("error_updating_settings"), "error");
+ },
+ onSettled: async () => {
+ await utils.viewer.me.invalidate();
+ setIsUpdateBtnLoading(false);
+ },
+ });
+
+ const timeFormatOptions = [
+ { value: 12, label: t("12_hour") },
+ { value: 24, label: t("24_hour") },
+ ];
+
+ const weekStartOptions = [
+ { value: "Sunday", label: nameOfDay(localeProp, 0) },
+ { value: "Monday", label: nameOfDay(localeProp, 1) },
+ { value: "Tuesday", label: nameOfDay(localeProp, 2) },
+ { value: "Wednesday", label: nameOfDay(localeProp, 3) },
+ { value: "Thursday", label: nameOfDay(localeProp, 4) },
+ { value: "Friday", label: nameOfDay(localeProp, 5) },
+ { value: "Saturday", label: nameOfDay(localeProp, 6) },
+ ];
+
+ const formMethods = useForm({
+ defaultValues: {
+ locale: {
+ value: localeProp || "",
+ label: localeOptions.find((option) => option.value === localeProp)?.label || "",
+ },
+ timeZone: user.timeZone || "",
+ timeFormat: {
+ value: user.timeFormat || 12,
+ label: timeFormatOptions.find((option) => option.value === user.timeFormat)?.label || 12,
+ },
+ weekStart: {
+ value: user.weekStart,
+ label: nameOfDay(localeProp, user.weekStart === "Sunday" ? 0 : 1),
+ },
+ },
+ });
+ const {
+ formState: { isDirty, isSubmitting },
+ reset,
+ getValues,
+ } = formMethods;
+ const isDisabled = isSubmitting || !isDirty;
+
+ const [isAllowDynamicBookingChecked, setIsAllowDynamicBookingChecked] = useState(
+ !!user.allowDynamicBooking
+ );
+ const [isAllowSEOIndexingChecked, setIsAllowSEOIndexingChecked] = useState(!!user.allowSEOIndexing);
+ const [isReceiveMonthlyDigestEmailChecked, setIsReceiveMonthlyDigestEmailChecked] = useState(
+ !!user.receiveMonthlyDigestEmail
+ );
+
+ return (
+
+
+
+
{
+ setIsAllowDynamicBookingChecked(checked);
+ mutation.mutate({ allowDynamicBooking: checked });
+ }}
+ switchContainerClassName="mt-6"
+ />
+
+ {
+ setIsAllowSEOIndexingChecked(checked);
+ mutation.mutate({ allowSEOIndexing: checked });
+ }}
+ switchContainerClassName="mt-6"
+ />
+
+ {
+ setIsReceiveMonthlyDigestEmailChecked(checked);
+ mutation.mutate({ receiveMonthlyDigestEmail: checked });
+ }}
+ switchContainerClassName="mt-6"
+ />
+
+ );
+};
+
+GeneralQueryView.getLayout = getLayout;
+GeneralQueryView.PageWrapper = PageWrapper;
+
+export default GeneralQueryView;
+"use client";
+
+import Head from "next/head";
+
+import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { WizardLayout } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+const CreateNewTeamPage = () => {
+ const { t } = useLocale();
+ return (
+ <>
+
+ {t("create_new_team")}
+
+
+
+ >
+ );
+};
+export const LayoutWrapper = (page: React.ReactElement) => {
+ return (
+
+ {page}
+
+ );
+};
+
+CreateNewTeamPage.getLayout = LayoutWrapper;
+CreateNewTeamPage.PageWrapper = PageWrapper;
+
+export default CreateNewTeamPage;
+"use client";
+
+import Head from "next/head";
+
+import AddNewTeamMembers from "@calcom/features/ee/teams/components/AddNewTeamMembers";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { WizardLayout } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+const OnboardTeamMembersPage = () => {
+ const { t } = useLocale();
+ return (
+ <>
+
+ {t("add_team_members")}
+
+
+
+ >
+ );
+};
+
+export const GetLayout = (page: React.ReactElement) => (
+
+ {page}
+
+);
+
+OnboardTeamMembersPage.getLayout = GetLayout;
+OnboardTeamMembersPage.PageWrapper = PageWrapper;
+
+export default OnboardTeamMembersPage;
+import TeamMembersView from "@calcom/features/ee/teams/pages/team-members-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = TeamMembersView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import TeamProfileView from "@calcom/features/ee/teams/pages/team-profile-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = TeamProfileView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import TeamSSOView from "@calcom/features/ee/sso/page/teams-sso-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = TeamSSOView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import TeamAppearenceView from "@calcom/features/ee/teams/pages/team-appearance-view";
+
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+const Page = TeamAppearenceView as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+import type { CalPageWrapper } from "@components/PageWrapper";
+import PageWrapper from "@components/PageWrapper";
+
+import BillingPage from "../../billing";
+
+const Page = BillingPage as CalPageWrapper;
+Page.PageWrapper = PageWrapper;
+
+export default Page;
+"use client";
+
+import { usePathname } from "next/navigation";
+
+import { useIntercom } from "@calcom/features/ee/support/lib/intercom/useIntercom";
+import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
+import { classNames } from "@calcom/lib";
+import { WEBAPP_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, Meta } from "@calcom/ui";
+import { ExternalLink } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+
+interface CtaRowProps {
+ title: string;
+ description: string;
+ children: React.ReactNode;
+ className?: string;
+}
+
+const CtaRow = ({ title, description, className, children }: CtaRowProps) => {
+ return (
+ <>
+
+
+
{title}
+
{description}
+
+ {children}
+
+ >
+ );
+};
+
+const BillingView = () => {
+ const pathname = usePathname();
+ const { t } = useLocale();
+ const { open } = useIntercom();
+ const returnTo = pathname;
+ const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`;
+
+ const onContactSupportClick = async () => {
+ await open();
+ };
+
+ return (
+ <>
+
+
+
+
+ {t("billing_portal")}
+
+
+
+
+
+
+
+ {t("contact_support")}
+
+
+
+ >
+ );
+};
+
+BillingView.getLayout = getLayout;
+BillingView.PageWrapper = PageWrapper;
+
+export default BillingView;
+import PageWrapper, { type CalPageWrapper } from "@components/PageWrapper";
+
+import MeetingEnded from "~/videos/views/videos-meeting-ended-single-view";
+
+export {
+ getServerSideProps,
+ type PageProps,
+} from "~/videos/views/videos-meeting-ended-single-view.getServerSideProps";
+
+const MeetingEndedPage = MeetingEnded as unknown as CalPageWrapper;
+
+MeetingEndedPage.PageWrapper = PageWrapper;
+
+export default MeetingEndedPage;
+import PageWrapper, { type CalPageWrapper } from "@components/PageWrapper";
+
+import VideosSingleView from "~/videos/views/videos-single-view";
+
+export { getServerSideProps, type PageProps } from "~/videos/views/videos-single-view.getServerSideProps";
+
+const VideosSinglePage = VideosSingleView as unknown as CalPageWrapper;
+
+VideosSinglePage.PageWrapper = PageWrapper;
+
+export default VideosSinglePage;
+import PageWrapper, { type CalPageWrapper } from "@components/PageWrapper";
+
+import MeetingNotStarted from "~/videos/views/videos-meeting-not-started-single-view";
+
+export {
+ getServerSideProps,
+ type PageProps,
+} from "~/videos/views/videos-meeting-not-started-single-view.getServerSideProps";
+
+const MeetingNotStartedPage = MeetingNotStarted as unknown as CalPageWrapper;
+
+MeetingNotStartedPage.PageWrapper = PageWrapper;
+
+export default MeetingNotStartedPage;
+import PageWrapper, { type CalPageWrapper } from "@components/PageWrapper";
+
+import NoMeetingFound from "~/videos/views/videos-no-meeting-found-single-view";
+
+const NoMeetingFoundPage = NoMeetingFound as unknown as CalPageWrapper;
+
+NoMeetingFoundPage.PageWrapper = PageWrapper;
+
+export default NoMeetingFoundPage;
+"use client";
+
+import Head from "next/head";
+
+import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+export default function MaintenancePage() {
+ const { t, isLocaleReady } = useLocale();
+ if (!isLocaleReady) return null;
+ return (
+
+
+
+ {t("under_maintenance")} | {APP_NAME}
+
+
+
+
+
{t("under_maintenance")}
+
+ {t("under_maintenance_description", { appName: APP_NAME })}
+
+
{t("contact_support")}
+
+
+ );
+}
+
+MaintenancePage.PageWrapper = PageWrapper;
+"use client";
+
+import type { GetStaticPropsContext } from "next";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useEffect, useState } from "react";
+
+import {
+ getOrgDomainConfigFromHostname,
+ subdomainSuffix,
+} from "@calcom/features/ee/organizations/lib/orgDomains";
+import { DOCS_URL, IS_CALCOM, JOIN_DISCORD, WEBSITE_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { HeadSeo } from "@calcom/ui";
+import { BookOpen, Check, ChevronRight, FileText, Shield } from "@calcom/ui/components/icon";
+import { Discord } from "@calcom/ui/components/icon/Discord";
+
+import PageWrapper from "@components/PageWrapper";
+
+import { getTranslations } from "@server/lib/getTranslations";
+
+enum pageType {
+ ORG = "org",
+ TEAM = "team",
+ USER = "user",
+ OTHER = "other",
+}
+
+export default function Custom404() {
+ const pathname = usePathname();
+ const { t } = useLocale();
+ const [username, setUsername] = useState("");
+ const [currentPageType, setCurrentPageType] = useState(pageType.USER);
+
+ const links = [
+ {
+ title: "Enterprise",
+ description: "Learn more about organizations and subdomains in our enterprise plan.",
+ icon: Shield,
+ href: `${WEBSITE_URL}/enterprise`,
+ },
+ {
+ title: t("documentation"),
+ description: t("documentation_description"),
+ icon: FileText,
+ href: DOCS_URL,
+ },
+ {
+ title: t("blog"),
+ description: t("blog_description"),
+ icon: BookOpen,
+ href: `${WEBSITE_URL}/blog`,
+ },
+ ];
+
+ const [url, setUrl] = useState(`${WEBSITE_URL}/signup`);
+ useEffect(() => {
+ const { isValidOrgDomain, currentOrgDomain } = getOrgDomainConfigFromHostname({
+ hostname: window.location.host,
+ });
+
+ const [routerUsername] = pathname?.replace("%20", "-").split(/[?#]/) ?? [];
+ if (routerUsername && (!isValidOrgDomain || !currentOrgDomain)) {
+ const splitPath = routerUsername.split("/");
+ if (splitPath[1] === "team" && splitPath.length === 3) {
+ // Accessing a non-existent team
+ setUsername(splitPath[2]);
+ setCurrentPageType(pageType.TEAM);
+ setUrl(
+ `${WEBSITE_URL}/signup?callbackUrl=settings/teams/new%3Fslug%3D${splitPath[2].replace("/", "")}`
+ );
+ } else {
+ setUsername(routerUsername);
+ setUrl(`${WEBSITE_URL}/signup?username=${routerUsername.replace("/", "")}`);
+ }
+ } else {
+ setUsername(currentOrgDomain ?? "");
+ setCurrentPageType(pageType.ORG);
+ setUrl(
+ `${WEBSITE_URL}/signup?callbackUrl=settings/organizations/new%3Fslug%3D${
+ currentOrgDomain?.replace("/", "") ?? ""
+ }`
+ );
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ const isSuccessPage = pathname?.startsWith("/booking");
+ const isSubpage = pathname?.includes("/", 2) || isSuccessPage;
+
+ /**
+ * If we're on 404 and the route is insights it means it is disabled
+ * TODO: Abstract this for all disabled features
+ **/
+ const isInsights = pathname?.startsWith("/insights");
+ if (isInsights) {
+ return (
+ <>
+
+
+
+
+
{t("error_404")}
+
+ Feature is currently disabled
+
+
+
+
+
+ {t("or_go_back_home")}
+ →
+
+
+
+
+
+ >
+ );
+ }
+
+ if (!username) return null;
+
+ return (
+ <>
+
+
+
+
+
+ {((!isSubpage && IS_CALCOM) ||
+ currentPageType === pageType.ORG ||
+ currentPageType === pageType.TEAM) && (
+
+ )}
+
+ {t("popular_pages")}
+
+
+
+
+ {t("or_go_back_home")}
+ →
+
+
+
+
+
+ >
+ );
+}
+
+Custom404.PageWrapper = PageWrapper;
+
+export const getStaticProps = async (context: GetStaticPropsContext) => {
+ const i18n = await getTranslations(context);
+
+ return {
+ props: {
+ i18n,
+ },
+ };
+};
+"use client";
+
+import { getLayout } from "@calcom/features/MainLayout";
+
+import EnterprisePage from "@components/EnterprisePage";
+import PageWrapper from "@components/PageWrapper";
+
+const ProxifiedEnterprisePage = new Proxy<{
+ (): JSX.Element;
+ PageWrapper?: typeof PageWrapper;
+ getLayout?: typeof getLayout;
+}>(EnterprisePage, {});
+
+ProxifiedEnterprisePage.PageWrapper = PageWrapper;
+ProxifiedEnterprisePage.getLayout = getLayout;
+
+export default ProxifiedEnterprisePage;
+import type { GetStaticPaths } from "next";
+
+import { getLayout } from "@calcom/features/MainLayout";
+
+import PageWrapper from "@components/PageWrapper";
+
+import { validStatuses } from "~/bookings/lib/validStatuses";
+import BookingsListingView from "~/bookings/views/bookings-listing-view";
+
+export { getStaticProps } from "~/bookings/views/bookings-listing-view.getStaticProps";
+
+const BookingsListingPage = new Proxy<{
+ (): JSX.Element;
+ PageWrapper?: typeof PageWrapper;
+ getLayout?: typeof getLayout;
+}>(BookingsListingView, {});
+
+BookingsListingPage.PageWrapper = PageWrapper;
+BookingsListingPage.getLayout = getLayout;
+
+export const getStaticPaths: GetStaticPaths = () => {
+ return {
+ paths: validStatuses.map((status) => ({
+ params: { status },
+ locale: "en",
+ })),
+ fallback: "blocking",
+ };
+};
+
+export default BookingsListingPage;
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import classNames from "classnames";
+import { signIn } from "next-auth/react";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState, useEffect } from "react";
+import { FormProvider, useForm } from "react-hook-form";
+import { FaGoogle } from "react-icons/fa";
+import { z } from "zod";
+
+import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
+import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
+import { WEBAPP_URL, WEBSITE_URL, HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
+import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
+import { trpc } from "@calcom/trpc/react";
+import { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
+import { ArrowLeft, Lock } from "@calcom/ui/components/icon";
+
+import type { inferSSRProps } from "@lib/types/inferSSRProps";
+import type { WithNonceProps } from "@lib/withNonce";
+
+import AddToHomescreen from "@components/AddToHomescreen";
+import PageWrapper from "@components/PageWrapper";
+import BackupCode from "@components/auth/BackupCode";
+import TwoFactor from "@components/auth/TwoFactor";
+import AuthContainer from "@components/ui/AuthContainer";
+
+import { getServerSideProps } from "@server/lib/auth/login/getServerSideProps";
+
+interface LoginValues {
+ email: string;
+ password: string;
+ totpCode: string;
+ backupCode: string;
+ csrfToken: string;
+}
+export default function Login({
+ csrfToken,
+ isGoogleLoginEnabled,
+ isSAMLLoginEnabled,
+ samlTenantID,
+ samlProductID,
+ totpEmail,
+}: // eslint-disable-next-line @typescript-eslint/ban-types
+inferSSRProps & WithNonceProps<{}>) {
+ const searchParams = useCompatSearchParams();
+ const { t } = useLocale();
+ const router = useRouter();
+ const formSchema = z
+ .object({
+ email: z
+ .string()
+ .min(1, `${t("error_required_field")}`)
+ .email(`${t("enter_valid_email")}`),
+ password: !!totpEmail ? z.literal("") : z.string().min(1, `${t("error_required_field")}`),
+ })
+ // Passthrough other fields like totpCode
+ .passthrough();
+ const methods = useForm({ resolver: zodResolver(formSchema) });
+ const { register, formState } = methods;
+ const [twoFactorRequired, setTwoFactorRequired] = useState(!!totpEmail || false);
+ const [twoFactorLostAccess, setTwoFactorLostAccess] = useState(false);
+ const [errorMessage, setErrorMessage] = useState(null);
+
+ const errorMessages: { [key: string]: string } = {
+ // [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
+ // Don't leak information about whether an email is registered or not
+ [ErrorCode.IncorrectEmailPassword]: t("incorrect_email_password"),
+ [ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`,
+ [ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
+ [ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
+ };
+
+ const telemetry = useTelemetry();
+
+ let callbackUrl = searchParams?.get("callbackUrl") || "";
+
+ if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1);
+
+ // If not absolute URL, make it absolute
+ if (!/^https?:\/\//.test(callbackUrl)) {
+ callbackUrl = `${WEBAPP_URL}/${callbackUrl}`;
+ }
+
+ const safeCallbackUrl = getSafeRedirectUrl(callbackUrl);
+
+ callbackUrl = safeCallbackUrl || "";
+
+ const LoginFooter = (
+
+ {t("dont_have_an_account")}
+
+ );
+
+ const TwoFactorFooter = (
+ <>
+ {
+ if (twoFactorLostAccess) {
+ setTwoFactorLostAccess(false);
+ methods.setValue("backupCode", "");
+ } else {
+ setTwoFactorRequired(false);
+ methods.setValue("totpCode", "");
+ }
+ setErrorMessage(null);
+ }}
+ StartIcon={ArrowLeft}
+ color="minimal">
+ {t("go_back")}
+
+ {!twoFactorLostAccess ? (
+ {
+ setTwoFactorLostAccess(true);
+ setErrorMessage(null);
+ methods.setValue("totpCode", "");
+ }}
+ StartIcon={Lock}
+ color="minimal">
+ {t("lost_access")}
+
+ ) : null}
+ >
+ );
+
+ const ExternalTotpFooter = (
+ {
+ window.location.replace("/");
+ }}
+ color="minimal">
+ {t("cancel")}
+
+ );
+
+ const onSubmit = async (values: LoginValues) => {
+ setErrorMessage(null);
+ telemetry.event(telemetryEventTypes.login, collectPageParameters());
+ const res = await signIn<"credentials">("credentials", {
+ ...values,
+ callbackUrl,
+ redirect: false,
+ });
+ if (!res) setErrorMessage(errorMessages[ErrorCode.InternalServerError]);
+ // we're logged in! let's do a hard refresh to the desired url
+ else if (!res.error) router.push(callbackUrl);
+ else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true);
+ else if (res.error === ErrorCode.IncorrectBackupCode) setErrorMessage(t("incorrect_backup_code"));
+ else if (res.error === ErrorCode.MissingBackupCodes) setErrorMessage(t("missing_backup_codes"));
+ // fallback if error not found
+ else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
+ };
+
+ const { data, isPending, error } = trpc.viewer.public.ssoConnections.useQuery();
+
+ useEffect(
+ function refactorMeWithoutEffect() {
+ if (error) {
+ setErrorMessage(error.message);
+ }
+ },
+ [error]
+ );
+
+ const displaySSOLogin = HOSTED_CAL_FEATURES
+ ? true
+ : isSAMLLoginEnabled && !isPending && data?.connectionExists;
+
+ return (
+
+
+
+
+ {!twoFactorRequired && (
+ <>
+ {(isGoogleLoginEnabled || displaySSOLogin) && }
+
+ {isGoogleLoginEnabled && (
+ {
+ e.preventDefault();
+ await signIn("google");
+ }}>
+ {t("signin_with_google")}
+
+ )}
+ {displaySSOLogin && (
+
+ )}
+
+ >
+ )}
+
+
+
+
+ );
+}
+
+export { getServerSideProps };
+
+Login.PageWrapper = PageWrapper;
+"use client";
+
+import { usePathname, useRouter } from "next/navigation";
+import { useState } from "react";
+
+import AdminAppsList from "@calcom/features/apps/AdminAppsList";
+import { APP_NAME } from "@calcom/lib/constants";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { inferSSRProps } from "@calcom/types/inferSSRProps";
+import { Meta, WizardForm } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
+import ChooseLicense from "@components/setup/ChooseLicense";
+import EnterpriseLicense from "@components/setup/EnterpriseLicense";
+
+import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
+
+function useSetStep() {
+ const router = useRouter();
+ const searchParams = useCompatSearchParams();
+ const pathname = usePathname();
+ const setStep = (newStep = 1) => {
+ const _searchParams = new URLSearchParams(searchParams ?? undefined);
+ _searchParams.set("step", newStep.toString());
+ router.replace(`${pathname}?${_searchParams.toString()}`);
+ };
+ return setStep;
+}
+
+export function Setup(props: inferSSRProps) {
+ const { t } = useLocale();
+ const router = useRouter();
+ const [value, setValue] = useState(props.isFreeLicense ? "FREE" : "EE");
+ const isFreeLicense = value === "FREE";
+ const [isEnabledEE, setIsEnabledEE] = useState(!props.isFreeLicense);
+ const setStep = useSetStep();
+
+ const steps: React.ComponentProps["steps"] = [
+ {
+ title: t("administrator_user"),
+ description: t("lets_create_first_administrator_user"),
+ content: (setIsPending) => (
+ {
+ setIsPending(true);
+ }}
+ onSuccess={() => {
+ setStep(2);
+ }}
+ onError={() => {
+ setIsPending(false);
+ }}
+ userCount={props.userCount}
+ />
+ ),
+ },
+ {
+ title: t("choose_a_license"),
+ description: t("choose_license_description"),
+ content: (setIsPending) => {
+ return (
+ {
+ setIsPending(true);
+ setStep(3);
+ }}
+ />
+ );
+ },
+ },
+ ];
+
+ if (!isFreeLicense) {
+ steps.push({
+ title: t("step_enterprise_license"),
+ description: t("step_enterprise_license_description"),
+ content: (setIsPending) => {
+ const currentStep = 3;
+ return (
+ {
+ setIsPending(true);
+ }}
+ onSuccess={() => {
+ setStep(currentStep + 1);
+ }}
+ onSuccessValidate={() => {
+ setIsEnabledEE(true);
+ }}
+ />
+ );
+ },
+ isEnabled: isEnabledEE,
+ });
+ }
+
+ steps.push({
+ title: t("enable_apps"),
+ description: t("enable_apps_description", { appName: APP_NAME }),
+ contentClassname: "!pb-0 mb-[-1px]",
+ content: (setIsPending) => {
+ const currentStep = isFreeLicense ? 3 : 4;
+ return (
+ {
+ setIsPending(true);
+ router.replace("/");
+ }}
+ />
+ );
+ },
+ });
+
+ return (
+ <>
+
+
+ t("current_step_of_total", { currentStep, maxSteps })}
+ />
+
+ >
+ );
+}
+
+Setup.isThemeSupported = false;
+Setup.PageWrapper = PageWrapper;
+export default Setup;
+
+export { getServerSideProps };
+/* eslint-disable react-hooks/exhaustive-deps */
+import { useSession } from "next-auth/react";
+import { useRouter } from "next/navigation";
+import { useState, useEffect } from "react";
+
+import { APP_NAME } from "@calcom/lib/constants";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Avatar, Button, Select } from "@calcom/ui";
+import { Plus, Info } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+
+export default function Authorize() {
+ const { t } = useLocale();
+ const { status } = useSession();
+
+ const router = useRouter();
+ const searchParams = useCompatSearchParams();
+
+ const client_id = (searchParams?.get("client_id") as string) || "";
+ const state = searchParams?.get("state") as string;
+ const scope = searchParams?.get("scope") as string;
+
+ const queryString = searchParams?.toString();
+
+ const [selectedAccount, setSelectedAccount] = useState<{ value: string; label: string } | null>();
+ const scopes = scope ? scope.toString().split(",") : [];
+
+ const { data: client, isPending: isPendingGetClient } = trpc.viewer.oAuth.getClient.useQuery(
+ {
+ clientId: client_id as string,
+ },
+ {
+ enabled: status !== "loading",
+ }
+ );
+
+ const { data, isPending: isPendingProfiles } = trpc.viewer.teamsAndUserProfilesQuery.useQuery();
+
+ const generateAuthCodeMutation = trpc.viewer.oAuth.generateAuthCode.useMutation({
+ onSuccess: (data) => {
+ window.location.href = `${client?.redirectUri}?code=${data.authorizationCode}&state=${state}`;
+ },
+ });
+
+ const mappedProfiles = data
+ ? data
+ .filter((profile) => !profile.readOnly)
+ .map((profile) => ({
+ label: profile.name || profile.slug || "",
+ value: profile.slug || "",
+ }))
+ : [];
+
+ useEffect(() => {
+ if (mappedProfiles.length > 0) {
+ setSelectedAccount(mappedProfiles[0]);
+ }
+ }, [isPendingProfiles]);
+
+ useEffect(() => {
+ if (status === "unauthenticated") {
+ const urlSearchParams = new URLSearchParams({
+ callbackUrl: `auth/oauth2/authorize?${queryString}`,
+ });
+ router.replace(`/auth/login?${urlSearchParams.toString()}`);
+ }
+ }, [status]);
+
+ const isPending = isPendingGetClient || isPendingProfiles || status !== "authenticated";
+
+ if (isPending) {
+ return <>>;
+ }
+
+ if (!client) {
+ return {t("unauthorized")}
;
+ }
+
+ return (
+
+
+
+
}
+ className="items-center"
+ imageSrc={client.logo}
+ size="lg"
+ />
+
+
+
+
+
+
+
+
+
+ {t("access_cal_account", { clientName: client.name, appName: APP_NAME })}
+
+
{t("select_account_team")}
+
{
+ setSelectedAccount(value);
+ }}
+ className="w-52"
+ defaultValue={selectedAccount || mappedProfiles[0]}
+ options={mappedProfiles}
+ />
+ {t("allow_client_to", { clientName: client.name })}
+
+
+ ✓ {" "}
+ {t("associate_with_cal_account", { clientName: client.name })}
+
+
+ ✓ {t("see_personal_info")}
+
+
+ ✓ {t("see_primary_email_address")}
+
+
+ ✓ {t("connect_installed_apps")}
+
+
+ ✓ {t("access_event_type")}
+
+
+ ✓ {t("access_availability")}
+
+
+ ✓ {t("access_bookings")}
+
+
+
+
+
+
+
+
+ {t("allow_client_to_do", { clientName: client.name })}
+
+
{t("oauth_access_information", { appName: APP_NAME })}
{" "}
+
+
+
+
+ {
+ window.location.href = `${client.redirectUri}`;
+ }}>
+ {t("go_back")}
+
+ {
+ generateAuthCodeMutation.mutate({
+ clientId: client_id as string,
+ scopes,
+ teamSlug: selectedAccount?.value.startsWith("team/")
+ ? selectedAccount?.value.substring(5)
+ : undefined, // team account starts with /team/
+ });
+ }}
+ data-testid="allow-button">
+ {t("allow")}
+
+
+
+
+ );
+}
+
+Authorize.PageWrapper = PageWrapper;
+"use client";
+
+import { signOut, useSession } from "next-auth/react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+
+import { WEBSITE_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button } from "@calcom/ui";
+import { Check } from "@calcom/ui/components/icon";
+
+import type { inferSSRProps } from "@lib/types/inferSSRProps";
+
+import PageWrapper from "@components/PageWrapper";
+import AuthContainer from "@components/ui/AuthContainer";
+
+import { getServerSideProps } from "@server/lib/auth/logout/getServerSideProps";
+
+type Props = inferSSRProps;
+
+export function Logout(props: Props) {
+ const [btnLoading, setBtnLoading] = useState(false);
+ const { status } = useSession();
+ if (status === "authenticated") signOut({ redirect: false });
+ const router = useRouter();
+ useEffect(() => {
+ if (props.query?.survey === "true") {
+ router.push(`${WEBSITE_URL}/cancellation`);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [props.query?.survey]);
+ const { t } = useLocale();
+
+ const message = () => {
+ if (props.query?.passReset === "true") return "reset_your_password";
+ if (props.query?.emailChange === "true") return "email_change";
+ return "hope_to_see_you_soon";
+ };
+
+ const navigateToLogin = () => {
+ setBtnLoading(true);
+ router.push("/auth/login");
+ };
+
+ return (
+
+
+
+
+
+
+
+ {t("youve_been_logged_out")}
+
+
+
+
+
+ {t("go_back_login")}
+
+
+ );
+}
+
+Logout.PageWrapper = PageWrapper;
+export default Logout;
+
+export { getServerSideProps };
+import { signIn } from "next-auth/react";
+import { useEffect } from "react";
+
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+
+import PageWrapper from "@components/PageWrapper";
+
+// To handle the IdP initiated login flow callback
+export default function Page() {
+ const searchParams = useCompatSearchParams();
+
+ useEffect(() => {
+ const code = searchParams?.get("code");
+
+ signIn("saml-idp", {
+ callbackUrl: "/",
+ code,
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return null;
+}
+Page.PageWrapper = PageWrapper;
+"use client";
+
+import { signIn } from "next-auth/react";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+
+import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
+
+import type { inferSSRProps } from "@lib/types/inferSSRProps";
+
+import PageWrapper from "@components/PageWrapper";
+
+import type { getServerSideProps } from "@server/lib/auth/sso/direct/getServerSideProps";
+
+// This page is used to initiate the SAML authentication flow by redirecting to the SAML provider.
+// Accessible only on self-hosted Cal.com instances.
+export default function Page({ samlTenantID, samlProductID }: inferSSRProps) {
+ const router = useRouter();
+
+ useEffect(() => {
+ if (HOSTED_CAL_FEATURES) {
+ router.push("/auth/login");
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ useEffect(() => {
+ // Initiate SAML authentication flow
+ signIn(
+ "saml",
+ {
+ callbackUrl: "/",
+ },
+ { tenant: samlTenantID, product: samlProductID }
+ );
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ return null;
+}
+
+export { getServerSideProps };
+
+Page.PageWrapper = PageWrapper;
+"use client";
+
+import { signIn } from "next-auth/react";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+
+import type { inferSSRProps } from "@lib/types/inferSSRProps";
+
+import PageWrapper from "@components/PageWrapper";
+
+import { getServerSideProps } from "@server/lib/auth/sso/[provider]/getServerSideProps";
+
+export type SSOProviderPageProps = inferSSRProps;
+
+export default function Provider(props: SSOProviderPageProps) {
+ const searchParams = useCompatSearchParams();
+ const router = useRouter();
+
+ useEffect(() => {
+ if (props.provider === "saml") {
+ const email = searchParams?.get("email");
+
+ if (!email) {
+ router.push(`/auth/error?error=Email not provided`);
+ return;
+ }
+
+ if (!props.isSAMLLoginEnabled) {
+ router.push(`/auth/error?error=SAML login not enabled`);
+ return;
+ }
+
+ signIn("saml", {}, { tenant: props.tenant, product: props.product });
+ } else {
+ signIn(props.provider);
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+ return null;
+}
+
+Provider.PageWrapper = PageWrapper;
+
+export { getServerSideProps };
+export default function NewUserPage() {
+ if (typeof window !== "undefined") {
+ window.location.assign(process.env.NEXT_PUBLIC_WEBAPP_URL || "https://app.cal.com");
+ }
+ return null;
+}
+"use client";
+
+// eslint-disable-next-line no-restricted-imports
+import { debounce } from "lodash";
+import Link from "next/link";
+import type { CSSProperties, SyntheticEvent } from "react";
+import React from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, EmailField } from "@calcom/ui";
+
+import { type inferSSRProps } from "@lib/types/inferSSRProps";
+
+import PageWrapper from "@components/PageWrapper";
+import AuthContainer from "@components/ui/AuthContainer";
+
+import { getServerSideProps } from "@server/lib/forgot-password/getServerSideProps";
+
+export default function ForgotPassword(props: inferSSRProps) {
+ const csrfToken = "csrfToken" in props ? (props.csrfToken as string) : undefined;
+ const { t } = useLocale();
+ const [loading, setLoading] = React.useState(false);
+ const [error, setError] = React.useState<{ message: string } | null>(null);
+ const [success, setSuccess] = React.useState(false);
+ const [email, setEmail] = React.useState("");
+
+ const handleChange = (e: SyntheticEvent) => {
+ const target = e.target as typeof e.target & { value: string };
+ setEmail(target.value);
+ };
+
+ const submitForgotPasswordRequest = async ({ email }: { email: string }) => {
+ try {
+ const res = await fetch("/api/auth/forgot-password", {
+ method: "POST",
+ body: JSON.stringify({ email }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+
+ const json = await res.json();
+ if (!res.ok) {
+ setError(json);
+ } else {
+ setSuccess(true);
+ }
+
+ return json;
+ } catch (reason) {
+ setError({ message: t("unexpected_error_try_again") });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const debouncedHandleSubmitPasswordRequest = debounce(submitForgotPasswordRequest, 250);
+
+ const handleSubmit = async (e: SyntheticEvent) => {
+ e.preventDefault();
+
+ if (!email) {
+ return;
+ }
+
+ if (loading) {
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+ setSuccess(false);
+
+ await debouncedHandleSubmitPasswordRequest({ email });
+ };
+
+ const Success = () => {
+ return (
+
+
{t("password_reset_email", { email })}
+
{t("password_reset_leading")}
+ {error &&
{error.message}
}
+
+ {t("back_to_signin")}
+
+
+ );
+ };
+
+ return (
+
+
+ {t("back_to_signin")}
+
+ >
+ )
+ }>
+ {success && }
+ {!success && (
+ <>
+ {error &&
{error.message}
}
+
+ >
+ )}
+
+ );
+}
+
+ForgotPassword.PageWrapper = PageWrapper;
+
+export { getServerSideProps };
+"use client";
+
+import Link from "next/link";
+import type { CSSProperties } from "react";
+import { useForm } from "react-hook-form";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, PasswordField, Form } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+import AuthContainer from "@components/ui/AuthContainer";
+
+import { getServerSideProps } from "@server/lib/forgot-password/[id]/getServerSideProps";
+
+type Props = {
+ requestId: string;
+ isRequestExpired: boolean;
+ csrfToken: string | undefined;
+};
+
+export default function Page({ requestId, isRequestExpired, csrfToken }: Props) {
+ const { t } = useLocale();
+ const formMethods = useForm<{ new_password: string }>();
+ const success = formMethods.formState.isSubmitSuccessful;
+ const loading = formMethods.formState.isSubmitting;
+ const passwordValue = formMethods.watch("new_password");
+ const isEmpty = passwordValue?.length === 0;
+
+ const submitChangePassword = async ({ password, requestId }: { password: string; requestId: string }) => {
+ const res = await fetch("/api/auth/reset-password", {
+ method: "POST",
+ body: JSON.stringify({ requestId, password }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ const json = await res.json();
+ if (!res.ok) return formMethods.setError("new_password", { type: "server", message: json.message });
+ };
+
+ const Success = () => {
+ return (
+ <>
+
+
+
+ {t("password_updated")}
+
+
+
+ {t("login")}
+
+
+ >
+ );
+ };
+
+ const Expired = () => {
+ return (
+ <>
+
+
+
{t("whoops")}
+ {t("request_is_expired")}
+
+
{t("request_is_expired_instructions")}
+
+
+ {t("try_again")}
+
+
+
+ >
+ );
+ };
+
+ return (
+
+ {isRequestExpired && }
+ {!isRequestExpired && !success && (
+ <>
+
+ >
+ )}
+ {!isRequestExpired && success && (
+ <>
+
+ >
+ )}
+
+ );
+}
+
+Page.PageWrapper = PageWrapper;
+export { getServerSideProps };
+import type { GetStaticPropsContext } from "next";
+import Link from "next/link";
+import z from "zod";
+
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button } from "@calcom/ui";
+import { X } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+import AuthContainer from "@components/ui/AuthContainer";
+
+import { getTranslations } from "@server/lib/getTranslations";
+
+const querySchema = z.object({
+ error: z.string().optional(),
+});
+
+export default function Error() {
+ const { t } = useLocale();
+ const searchParams = useCompatSearchParams();
+ const { error } = querySchema.parse(searchParams);
+ const isTokenVerificationError = error?.toLowerCase() === "verification";
+ const errorMsg = isTokenVerificationError ? t("token_invalid_expired") : t("error_during_login");
+
+ return (
+
+
+
+
+ {t("go_back_login")}
+
+
+
+ );
+}
+
+Error.PageWrapper = PageWrapper;
+
+export const getStaticProps = async (context: GetStaticPropsContext) => {
+ const i18n = await getTranslations(context);
+
+ return {
+ props: {
+ i18n,
+ },
+ };
+};
+"use client";
+
+import { motion } from "framer-motion";
+import { signIn } from "next-auth/react";
+import Head from "next/head";
+import { usePathname, useRouter } from "next/navigation";
+import { useEffect, useRef, useState } from "react";
+import z from "zod";
+
+import { classNames } from "@calcom/lib";
+import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
+import { trpc } from "@calcom/trpc/react";
+import type { inferSSRProps } from "@calcom/types/inferSSRProps";
+import { Button, showToast } from "@calcom/ui";
+import { AlertTriangle, ExternalLink, MailOpen } from "@calcom/ui/components/icon";
+
+import Loader from "@components/Loader";
+import PageWrapper from "@components/PageWrapper";
+
+import { getServerSideProps } from "@server/lib/auth/verify/getServerSideProps";
+
+async function sendVerificationLogin(email: string, username: string) {
+ await signIn("email", {
+ email: email.toLowerCase(),
+ username: username.toLowerCase(),
+ redirect: false,
+ callbackUrl: WEBAPP_URL || "https://app.cal.com",
+ })
+ .then(() => {
+ showToast("Verification email sent", "success");
+ })
+ .catch((err) => {
+ showToast(err, "error");
+ });
+}
+
+function useSendFirstVerificationLogin({
+ email,
+ username,
+}: {
+ email: string | undefined;
+ username: string | undefined;
+}) {
+ const sent = useRef(false);
+ useEffect(() => {
+ if (!email || !username || sent.current) {
+ return;
+ }
+ (async () => {
+ await sendVerificationLogin(email, username);
+ sent.current = true;
+ })();
+ }, [email, username]);
+}
+
+const querySchema = z.object({
+ stripeCustomerId: z.string().optional(),
+ sessionId: z.string().optional(),
+ t: z.string().optional(),
+});
+
+const PaymentFailedIcon = () => (
+
+);
+
+const PaymentSuccess = () => (
+
+);
+
+const MailOpenIcon = () => (
+
+
+
+);
+
+export default function Verify(props: inferSSRProps) {
+ const searchParams = useCompatSearchParams();
+ const pathname = usePathname();
+ const router = useRouter();
+ const routerQuery = useRouterQuery();
+ const { t, sessionId, stripeCustomerId } = querySchema.parse(routerQuery);
+ const [secondsLeft, setSecondsLeft] = useState(30);
+ const { data } = trpc.viewer.public.stripeCheckoutSession.useQuery(
+ {
+ stripeCustomerId,
+ checkoutSessionId: sessionId,
+ },
+ {
+ enabled: !!stripeCustomerId || !!sessionId,
+ staleTime: Infinity,
+ }
+ );
+ useSendFirstVerificationLogin({ email: data?.customer?.email, username: data?.customer?.username });
+ // @note: check for t=timestamp and apply disabled state and secondsLeft accordingly
+ // to avoid refresh to skip waiting 30 seconds to re-send email
+ useEffect(() => {
+ const lastSent = new Date(parseInt(`${t}`));
+ // @note: This double round() looks ugly but it's the only way I came up to get the time difference in seconds
+ const difference = Math.round(Math.round(new Date().getTime() - lastSent.getTime()) / 1000);
+ if (difference < 30) {
+ // If less than 30 seconds, set the seconds left to 30 - difference
+ setSecondsLeft(30 - difference);
+ } else {
+ // else set the seconds left to 0 and disabled false
+ setSecondsLeft(0);
+ }
+ }, [t]);
+ // @note: here we make sure each second is decremented if disabled up to 0.
+ useEffect(() => {
+ if (secondsLeft > 0) {
+ const interval = setInterval(() => {
+ if (secondsLeft > 0) {
+ setSecondsLeft(secondsLeft - 1);
+ }
+ }, 1000);
+ return () => clearInterval(interval);
+ }
+ }, [secondsLeft]);
+
+ if (!data) {
+ // Loading state
+ return ;
+ }
+ const { valid, hasPaymentFailed, customer } = data;
+ if (!valid) {
+ throw new Error("Invalid session or customer id");
+ }
+
+ if (!stripeCustomerId && !sessionId) {
+ return Invalid Link
;
+ }
+
+ return (
+
+
+
+ {/* @note: Ternary can look ugly ant his might be extracted later but I think at 3 it's not yet worth
+ it or too hard to read. */}
+ {hasPaymentFailed
+ ? "Your payment failed"
+ : sessionId
+ ? "Payment successful!"
+ : `Verify your email | ${APP_NAME}`}
+
+
+
+
+ {hasPaymentFailed ?
: sessionId ?
:
}
+
+ {hasPaymentFailed
+ ? "Your payment failed"
+ : sessionId
+ ? "Payment successful!"
+ : "Check your Inbox"}
+
+ {hasPaymentFailed && (
+
Your account has been created, but your premium has not been reserved.
+ )}
+
+ We have sent an email to {customer?.email} with a link to activate your account.{" "}
+ {hasPaymentFailed &&
+ "Once you activate your account you will be able to try purchase your premium username again or select a different one."}
+
+
+
+ Open in Gmail
+
+
+
+
+
Don’t seen an email?
+
0 ? "text-muted" : "underline underline-offset-2 hover:font-normal"
+ )}
+ disabled={secondsLeft > 0}
+ onClick={async (e) => {
+ if (!customer) {
+ return;
+ }
+ e.preventDefault();
+ setSecondsLeft(30);
+ // Update query params with t:timestamp, shallow: true doesn't re-render the page
+ const _searchParams = new URLSearchParams(searchParams?.toString());
+ _searchParams.set("t", `${Date.now()}`);
+ router.replace(`${pathname}?${_searchParams.toString()}`);
+ return await sendVerificationLogin(customer.email, customer.username);
+ }}>
+ {secondsLeft > 0 ? `Resend in ${secondsLeft} seconds` : "Resend"}
+
+
+
+
+ );
+}
+
+export { getServerSideProps };
+Verify.PageWrapper = PageWrapper;
+"use client";
+
+import { MailOpenIcon } from "lucide-react";
+import { useSession } from "next-auth/react";
+import { useRouter } from "next/navigation";
+import { useEffect } from "react";
+
+import { APP_NAME } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc";
+import useEmailVerifyCheck from "@calcom/trpc/react/hooks/useEmailVerifyCheck";
+import { Button, EmptyScreen, showToast } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+function VerifyEmailPage() {
+ const { data } = useEmailVerifyCheck();
+ const { data: session } = useSession();
+ const router = useRouter();
+ const { t, isLocaleReady } = useLocale();
+ const mutation = trpc.viewer.auth.resendVerifyEmail.useMutation();
+
+ useEffect(() => {
+ if (data?.isVerified) {
+ router.replace("/getting-started");
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [data?.isVerified]);
+ if (!isLocaleReady) {
+ return null;
+ }
+ return (
+
+
+
+ {
+ showToast("Send email", "success");
+ mutation.mutate();
+ }}>
+ Resend Email
+
+ }
+ />
+
+
+
+ );
+}
+
+export default VerifyEmailPage;
+
+VerifyEmailPage.PageWrapper = PageWrapper;
+"use client";
+
+import type { getProviders } from "next-auth/react";
+import { signIn } from "next-auth/react";
+
+import { Button } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+import { getServerSideProps } from "@server/lib/auth/signin/getServerSideProps";
+
+function signin({ providers }: { providers: Awaited> }) {
+ if (!providers) {
+ return null;
+ }
+
+ return (
+
+ {Object.values(providers).map((provider) => {
+ return (
+
+ signIn(provider.id)}>Sign in with {provider.name}
+
+ );
+ })}
+
+ );
+}
+
+signin.PageWrapper = PageWrapper;
+
+export default signin;
+
+export { getServerSideProps };
+export { default } from "~/bookings/views/bookings-single-view";
+export { getServerSideProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
+import withEmbedSsr from "@lib/withEmbedSsr";
+
+import { getServerSideProps as _getServerSideProps } from "~/bookings/views/bookings-single-view.getServerSideProps";
+
+export { default } from "~/bookings/views/bookings-single-view";
+
+export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+import { getLayout } from "@calcom/features/MainLayout";
+
+import PageWrapper from "@components/PageWrapper";
+
+import EventTypesPage from "~/event-types/views/event-types-listing-view";
+
+EventTypesPage.getLayout = getLayout;
+EventTypesPage.PageWrapper = PageWrapper;
+
+export default EventTypesPage;
+import PageWrapper from "@components/PageWrapper";
+
+import EventTypePageWrapper from "~/event-types/views/event-types-single-view";
+import { getServerSideProps } from "~/event-types/views/event-types-single-view.getServerSideProps";
+
+export type {
+ CustomInputParsed,
+ EventTypeSetup,
+ EventTypeSetupProps,
+ FormValues,
+} from "~/event-types/views/event-types-single-view";
+
+EventTypePageWrapper.PageWrapper = PageWrapper;
+
+export { getServerSideProps };
+export default EventTypePageWrapper;
+import type { GetServerSidePropsContext } from "next";
+import { z } from "zod";
+
+import { Booker } from "@calcom/atoms";
+import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
+import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
+import { getMultipleDurationValue } from "@calcom/features/bookings/lib/get-booking";
+import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains";
+import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
+import slugify from "@calcom/lib/slugify";
+import prisma from "@calcom/prisma";
+
+import type { inferSSRProps } from "@lib/types/inferSSRProps";
+import type { EmbedProps } from "@lib/withEmbedSsr";
+
+import PageWrapper from "@components/PageWrapper";
+
+export type PageProps = inferSSRProps & EmbedProps;
+
+export default function Type({
+ slug,
+ user,
+ booking,
+ away,
+ isEmbed,
+ isBrandingHidden,
+ entity,
+ duration,
+}: PageProps) {
+ return (
+
+
+
+
+ );
+}
+
+const paramsSchema = z.object({
+ type: z.string().transform((s) => slugify(s)),
+ slug: z.string().transform((s) => slugify(s)),
+});
+
+Type.PageWrapper = PageWrapper;
+Type.isBookingPage = true;
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params);
+ const { duration: queryDuration } = context.query;
+ const { ssrInit } = await import("@server/lib/ssr");
+ const ssr = await ssrInit(context);
+ const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug);
+
+ const team = await prisma.team.findFirst({
+ where: {
+ ...getSlugOrRequestedSlug(teamSlug),
+ parent: isValidOrgDomain && currentOrgDomain ? getSlugOrRequestedSlug(currentOrgDomain) : null,
+ },
+ select: {
+ id: true,
+ hideBranding: true,
+ },
+ });
+
+ if (!team) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ const org = isValidOrgDomain ? currentOrgDomain : null;
+
+ const eventData = await ssr.viewer.public.event.fetch({
+ username: teamSlug,
+ eventSlug: meetingSlug,
+ isTeamEvent: true,
+ org,
+ });
+
+ if (!eventData || !org) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ return {
+ props: {
+ entity: eventData.entity,
+ duration: getMultipleDurationValue(
+ eventData.metadata?.multipleDuration,
+ queryDuration,
+ eventData.length
+ ),
+ booking: null,
+ away: false,
+ user: teamSlug,
+ teamId: team.id,
+ slug: meetingSlug,
+ trpcState: ssr.dehydrate(),
+ isBrandingHidden: team?.hideBranding,
+ themeBasis: null,
+ },
+ };
+};
+import withEmbedSsr from "@lib/withEmbedSsr";
+
+import { getServerSideProps as _getServerSideProps } from "./index";
+
+export { default } from "./index";
+
+export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+"use client";
+
+import PageWrapper from "@components/PageWrapper";
+
+import UserPage from "~/users/views/users-public-view";
+import { type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
+
+import type { PageProps as TeamPageProps } from "../../../team/[slug]";
+import TeamPage from "../../../team/[slug]";
+
+export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/getServerSideProps";
+
+export type PageProps = UserPageProps | TeamPageProps;
+
+export default function Page(props: PageProps) {
+ if ((props as TeamPageProps)?.team) return ;
+ return ;
+}
+
+Page.isBookingPage = true;
+Page.PageWrapper = PageWrapper;
+"use client";
+
+import PageWrapper from "@components/PageWrapper";
+
+import UserTypePage from "~/users/views/users-type-public-view";
+import type { PageProps as UserTypePageProps } from "~/users/views/users-type-public-view.getServerSideProps";
+
+import type { PageProps as TeamTypePageProps } from "../../../../team/[slug]/[type]";
+import TeamTypePage from "../../../../team/[slug]/[type]";
+
+export { getServerSideProps } from "@lib/org/[orgSlug]/[user]/[type]/getServerSideProps";
+
+export type PageProps = UserTypePageProps | TeamTypePageProps;
+
+export default function Page(props: PageProps) {
+ if ((props as TeamTypePageProps)?.teamId) return ;
+ return ;
+}
+
+Page.PageWrapper = PageWrapper;
+Page.isBookingPage = true;
+"use client";
+
+import withEmbedSsr from "@lib/withEmbedSsr";
+
+import { getServerSideProps as _getServerSideProps } from ".";
+
+export { default, type PageProps } from ".";
+
+export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+"use client";
+
+import withEmbedSsr from "@lib/withEmbedSsr";
+
+import { getServerSideProps as _getServerSideProps } from "../[user]";
+
+export { default } from "../[user]";
+
+export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+/**
+ * Typescript class based component for custom-error
+ * @link https://nextjs.org/docs/advanced-features/custom-error-page
+ */
+import type { NextPage, NextPageContext } from "next";
+import type { ErrorProps } from "next/error";
+import NextError from "next/error";
+import React from "react";
+
+import { getErrorFromUnknown } from "@calcom/lib/errors";
+import { HttpError } from "@calcom/lib/http-error";
+import logger from "@calcom/lib/logger";
+import { redactError } from "@calcom/lib/redactError";
+
+import { ErrorPage } from "@components/error/error-page";
+
+// Adds HttpException to the list of possible error types.
+type AugmentedError = (NonNullable & HttpError) | null;
+type CustomErrorProps = {
+ err?: AugmentedError;
+ message?: string;
+ hasGetInitialPropsRun?: boolean;
+} & Omit;
+
+type AugmentedNextPageContext = Omit & {
+ err: AugmentedError;
+};
+
+const log = logger.getSubLogger({ prefix: ["[error]"] });
+
+const CustomError: NextPage = (props) => {
+ const { statusCode, err, message, hasGetInitialPropsRun } = props;
+
+ if (!hasGetInitialPropsRun && err) {
+ // getInitialProps is not called in case of
+ // https://github.com/vercel/next.js/issues/8592. As a workaround, we pass
+ // err via _app.tsx so it can be captured
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ const e = getErrorFromUnknown(err);
+ // can be captured here
+ // e.g. Sentry.captureException(e);
+ }
+ return ;
+};
+
+/**
+ * Partially adapted from the example in
+ * https://github.com/vercel/next.js/tree/canary/examples/with-sentry
+ */
+CustomError.getInitialProps = async (ctx: AugmentedNextPageContext) => {
+ const { res, err, asPath } = ctx;
+ const errorInitialProps = (await NextError.getInitialProps({
+ res,
+ err,
+ } as NextPageContext)) as CustomErrorProps;
+
+ // Workaround for https://github.com/vercel/next.js/issues/8592, mark when
+ // getInitialProps has run
+ errorInitialProps.hasGetInitialPropsRun = true;
+
+ // If a HttpError message, let's override defaults
+ if (err instanceof HttpError) {
+ const redactedError = redactError(err);
+ errorInitialProps.statusCode = err.statusCode;
+ errorInitialProps.title = redactedError.name;
+ errorInitialProps.message = redactedError.message;
+ errorInitialProps.err = {
+ ...redactedError,
+ url: err.url,
+ statusCode: err.statusCode,
+ cause: err.cause,
+ method: err.method,
+ };
+ }
+
+ if (res) {
+ // Running on the server, the response object is available.
+ //
+ // Next.js will pass an err on the server if a page's `getInitialProps`
+ // threw or returned a Promise that rejected
+
+ // Overrides http status code if present in errorInitialProps
+ res.statusCode = errorInitialProps.statusCode;
+
+ log.debug(`server side logged this: ${err?.toString() ?? JSON.stringify(err)}`);
+ log.info("return props, ", errorInitialProps);
+
+ return errorInitialProps;
+ } else {
+ // Running on the client (browser).
+ //
+ // Next.js will provide an err if:
+ //
+ // - a page's `getInitialProps` threw or returned a Promise that rejected
+ // - an exception was thrown somewhere in the React lifecycle (render,
+ // componentDidMount, etc) that was caught by Next.js's React Error
+ // Boundary. Read more about what types of exceptions are caught by Error
+ // Boundaries: https://reactjs.org/docs/error-boundaries.html
+ if (err) {
+ log.info("client side logged this", err);
+ return errorInitialProps;
+ }
+ }
+
+ // If this point is reached, getInitialProps was called without any
+ // information about what the error might be. This is unexpected and may
+ // indicate a bug introduced in Next.js
+ new Error(`_error.tsx getInitialProps missing data at path: ${asPath}`);
+
+ return errorInitialProps;
+};
+
+export default CustomError;
+import PaymentPage from "@calcom/features/ee/payments/components/PaymentPage";
+import { getServerSideProps } from "@calcom/features/ee/payments/pages/payment";
+import type { inferSSRProps } from "@calcom/types/inferSSRProps";
+
+import PageWrapper from "@components/PageWrapper";
+
+export default function Payment(props: inferSSRProps) {
+ return ;
+}
+Payment.PageWrapper = PageWrapper;
+export { getServerSideProps };
+import Head from "next/head";
+
+import { APP_NAME } from "@calcom/lib/constants";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button, showToast } from "@calcom/ui";
+import { Copy } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+
+export default function Error500() {
+ const searchParams = useCompatSearchParams();
+ const { t } = useLocale();
+
+ return (
+
+
+
Something unexpected occurred | {APP_NAME}
+
+
+
+
500
+
It's not you, it's us.
+
{t("something_went_wrong_on_our_end")}
+ {searchParams?.get("error") && (
+
+
+ {t("please_provide_following_text_to_suppport")}:
+
+
+ {searchParams?.get("error")}
+
+ {
+ navigator.clipboard.writeText(searchParams?.get("error") as string);
+ showToast("Link copied!", "success");
+ }}>
+ {t("copy")}
+
+
+
+ )}
+
{t("contact_support")}
+
+ {t("go_back")}
+
+
+
+ );
+}
+
+Error500.PageWrapper = PageWrapper;
+import Workflows from "@calcom/features/ee/workflows/pages/index";
+
+import PageWrapper from "@components/PageWrapper";
+import type { CalPageWrapper } from "@components/PageWrapper";
+
+const WorkflowsPage = Workflows as CalPageWrapper;
+WorkflowsPage.PageWrapper = PageWrapper;
+
+export default WorkflowsPage;
+"use client";
+
+import type { GetStaticPaths } from "next";
+
+import Workflow from "@calcom/features/ee/workflows/pages/workflow";
+
+import { getStaticProps } from "@lib/workflows/[workflow]/getStaticProps";
+
+import PageWrapper from "@components/PageWrapper";
+import type { CalPageWrapper } from "@components/PageWrapper";
+
+export const getStaticPaths: GetStaticPaths = () => {
+ return {
+ paths: [],
+ fallback: "blocking",
+ };
+};
+
+const WorkflowsPage = Workflow as CalPageWrapper;
+WorkflowsPage.PageWrapper = PageWrapper;
+
+export default WorkflowsPage;
+export { getStaticProps };
+import type { IncomingMessage } from "http";
+import { dir } from "i18next";
+import type { NextPageContext } from "next";
+import type { DocumentContext, DocumentProps } from "next/document";
+import Document, { Head, Html, Main, NextScript } from "next/document";
+import { z } from "zod";
+
+import { IS_PRODUCTION } from "@calcom/lib/constants";
+
+import { csp } from "@lib/csp";
+
+type Props = Record & DocumentProps & { newLocale: string };
+function setHeader(ctx: NextPageContext, name: string, value: string) {
+ try {
+ ctx.res?.setHeader(name, value);
+ } catch (e) {
+ // Getting "Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client" when revalidate calendar chache
+ console.log(`Error setting header ${name}=${value} for ${ctx.asPath || "unknown asPath"}`, e);
+ }
+}
+class MyDocument extends Document {
+ static async getInitialProps(ctx: DocumentContext) {
+ const { nonce } = csp(ctx.req || null, ctx.res || null);
+ if (!process.env.CSP_POLICY) {
+ setHeader(ctx, "x-csp", "not-opted-in");
+ } else if (!ctx.res?.getHeader("x-csp")) {
+ // If x-csp not set by gSSP, then it's initialPropsOnly
+ setHeader(ctx, "x-csp", "initialPropsOnly");
+ }
+
+ const getLocaleModule = ctx.req ? await import("@calcom/features/auth/lib/getLocale") : null;
+
+ const newLocale =
+ ctx.req && getLocaleModule
+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ await getLocaleModule.getLocale(ctx.req as IncomingMessage & { cookies: Record })
+ : "en";
+
+ const asPath = ctx.asPath || "";
+ // Use a dummy URL as default so that URL parsing works for relative URLs as well. We care about searchParams and pathname only
+ const parsedUrl = new URL(asPath, "https://dummyurl");
+ const isEmbedSnippetGeneratorPath = parsedUrl.pathname.startsWith("/event-types");
+ // FIXME: Revisit this logic to remove embedType query param check completely. Ideally, /embed should always be there at the end of the URL. Test properly and then remove it.
+ const isEmbed =
+ (parsedUrl.pathname.endsWith("/embed") || parsedUrl.searchParams.get("embedType") !== null) &&
+ !isEmbedSnippetGeneratorPath;
+ const embedColorScheme = parsedUrl.searchParams.get("ui.color-scheme");
+ const initialProps = await Document.getInitialProps(ctx);
+ return { isEmbed, embedColorScheme, nonce, ...initialProps, newLocale };
+ }
+
+ render() {
+ const { isEmbed, embedColorScheme } = this.props;
+ const newLocale = this.props.newLocale || "en";
+ const newDir = dir(newLocale);
+
+ const nonceParsed = z.string().safeParse(this.props.nonce);
+ const nonce = nonceParsed.success ? nonceParsed.data : "";
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
+ // eslint-disable-next-line @next/next/no-sync-scripts
+
+ )}
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default MyDocument;
+"use client";
+
+import { useSession } from "next-auth/react";
+import { Trans } from "next-i18next";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+
+import { getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
+import { WEBAPP_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc";
+import { TRPCClientError } from "@calcom/trpc/react";
+import { Button, EmptyScreen, Alert } from "@calcom/ui";
+import { PhoneCall } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+
+function ConnectAndJoin() {
+ const { t } = useLocale();
+ const router = useRouter();
+ const token = getQueryParam("token");
+ const [meetingUrl, setMeetingUrl] = useState(null);
+ const [errorMessage, setErrorMessage] = useState();
+
+ const session = useSession();
+ const isUserPartOfOrg = session.status === "authenticated" && !!session.data.user?.org;
+
+ const mutation = trpc.viewer.connectAndJoin.useMutation({
+ onSuccess: (res) => {
+ if (res.meetingUrl && !res.isBookingAlreadyAcceptedBySomeoneElse) {
+ router.push(res.meetingUrl);
+ } else if (res.isBookingAlreadyAcceptedBySomeoneElse && res.meetingUrl) {
+ setMeetingUrl(res.meetingUrl);
+ }
+ },
+ onError: (err) => {
+ console.log("err", err, err instanceof TRPCClientError);
+ if (err instanceof TRPCClientError) {
+ setErrorMessage(t(err.message));
+ } else {
+ setErrorMessage(t("something_went_wrong"));
+ }
+ },
+ });
+
+ if (session.status === "loading") return {t("loading")}
;
+
+ if (!token) return {t("token_not_found")}
;
+
+ return (
+
+ {session ? (
+
+ {meetingUrl ? (
+
+
+ Some other host already accepted the meeting. Do you still want to join?
+
+ Continue to Meeting
+
+
+
+ ) : (
+ {
+ mutation.mutate({ token });
+ }}>
+ {t("join_meeting")}
+
+ )}
+ {errorMessage && }
+
+ }
+ />
+ ) : (
+ {t("you_must_be_logged_in_to", { url: WEBAPP_URL })}
+ )}
+
+ );
+}
+
+ConnectAndJoin.requiresLicense = true;
+ConnectAndJoin.PageWrapper = PageWrapper;
+
+export default ConnectAndJoin;
+"use client";
+
+import { zodResolver } from "@hookform/resolvers/zod";
+import { CalendarHeart, Info, Link2, ShieldCheckIcon, StarIcon, Users } from "lucide-react";
+import { signIn } from "next-auth/react";
+import { Trans } from "next-i18next";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { useState, useEffect } from "react";
+import type { SubmitHandler } from "react-hook-form";
+import { useForm, useFormContext } from "react-hook-form";
+import { Toaster } from "react-hot-toast";
+import { z } from "zod";
+
+import getStripe from "@calcom/app-store/stripepayment/lib/client";
+import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils";
+import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
+import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
+import { useFlagMap } from "@calcom/features/flags/context/provider";
+import { classNames } from "@calcom/lib";
+import { APP_NAME, URL_PROTOCOL_REGEX, IS_CALCOM, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
+import { fetchUsername } from "@calcom/lib/fetchUsername";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useDebounce } from "@calcom/lib/hooks/useDebounce";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
+import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils";
+import type { inferSSRProps } from "@calcom/types/inferSSRProps";
+import { Button, HeadSeo, PasswordField, TextField, Form, Alert, showToast } from "@calcom/ui";
+
+import { getServerSideProps } from "@lib/signup/getServerSideProps";
+
+import PageWrapper from "@components/PageWrapper";
+
+const signupSchema = apiSignupSchema.extend({
+ apiError: z.string().optional(), // Needed to display API errors doesnt get passed to the API
+});
+
+type FormValues = z.infer;
+
+export type SignupProps = inferSSRProps;
+
+const FEATURES = [
+ {
+ title: "connect_all_calendars",
+ description: "connect_all_calendars_description",
+ i18nOptions: {
+ appName: APP_NAME,
+ },
+ icon: CalendarHeart,
+ },
+ {
+ title: "set_availability",
+ description: "set_availbility_description",
+ icon: Users,
+ },
+ {
+ title: "share_a_link_or_embed",
+ description: "share_a_link_or_embed_description",
+ icon: Link2,
+ i18nOptions: {
+ appName: APP_NAME,
+ },
+ },
+];
+
+function UsernameField({
+ username,
+ setPremium,
+ premium,
+ setUsernameTaken,
+ orgSlug,
+ usernameTaken,
+ disabled,
+ ...props
+}: React.ComponentProps & {
+ username: string;
+ setPremium: (value: boolean) => void;
+ premium: boolean;
+ usernameTaken: boolean;
+ orgSlug?: string;
+ setUsernameTaken: (value: boolean) => void;
+}) {
+ const { t } = useLocale();
+ const { register, formState } = useFormContext();
+ const debouncedUsername = useDebounce(username, 600);
+
+ useEffect(() => {
+ if (formState.isSubmitting || formState.isSubmitSuccessful) return;
+
+ async function checkUsername() {
+ // If the username can't be changed, there is no point in doing the username availability check
+ if (disabled) return;
+ if (!debouncedUsername) {
+ setPremium(false);
+ setUsernameTaken(false);
+ return;
+ }
+ fetchUsername(debouncedUsername, orgSlug ?? null).then(({ data }) => {
+ setPremium(data.premium);
+ setUsernameTaken(!data.available);
+ });
+ }
+ checkUsername();
+ }, [
+ debouncedUsername,
+ setPremium,
+ disabled,
+ orgSlug,
+ setUsernameTaken,
+ formState.isSubmitting,
+ formState.isSubmitSuccessful,
+ ]);
+
+ return (
+
+
+ {(!formState.isSubmitting || !formState.isSubmitted) && (
+
+
+ {usernameTaken ? (
+
+
+
{t("already_in_use_error")}
+
+ ) : premium ? (
+
+
+
+ {t("premium_username", {
+ price: getPremiumPlanPriceValue(),
+ })}
+
+
+ ) : null}
+
+
+ )}
+
+ );
+}
+
+function addOrUpdateQueryParam(url: string, key: string, value: string) {
+ const separator = url.includes("?") ? "&" : "?";
+ const param = `${key}=${encodeURIComponent(value)}`;
+ return `${url}${separator}${param}`;
+}
+
+export default function Signup({
+ prepopulateFormValues,
+ token,
+ orgSlug,
+ isGoogleLoginEnabled,
+ isSAMLLoginEnabled,
+ orgAutoAcceptEmail,
+}: SignupProps) {
+ const [premiumUsername, setPremiumUsername] = useState(false);
+ const [usernameTaken, setUsernameTaken] = useState(false);
+ const [isGoogleLoading, setIsGoogleLoading] = useState(false);
+
+ const searchParams = useCompatSearchParams();
+ const telemetry = useTelemetry();
+ const { t, i18n } = useLocale();
+ const router = useRouter();
+ const flags = useFlagMap();
+ const formMethods = useForm({
+ resolver: zodResolver(signupSchema),
+ defaultValues: prepopulateFormValues satisfies FormValues,
+ mode: "onChange",
+ });
+ const {
+ register,
+ watch,
+ formState: { isSubmitting, errors, isSubmitSuccessful },
+ } = formMethods;
+
+ const loadingSubmitState = isSubmitSuccessful || isSubmitting;
+
+ const handleErrorsAndStripe = async (resp: Response) => {
+ if (!resp.ok) {
+ const err = await resp.json();
+ if (err.checkoutSessionId) {
+ const stripe = await getStripe();
+ if (stripe) {
+ console.log("Redirecting to stripe checkout");
+ const { error } = await stripe.redirectToCheckout({
+ sessionId: err.checkoutSessionId,
+ });
+ console.warn(error.message);
+ }
+ } else {
+ throw new Error(err.message);
+ }
+ }
+ };
+
+ const isOrgInviteByLink = orgSlug && !prepopulateFormValues?.username;
+
+ const signUp: SubmitHandler = async (data) => {
+ await fetch("/api/auth/signup", {
+ body: JSON.stringify({
+ ...data,
+ language: i18n.language,
+ token,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ },
+ method: "POST",
+ })
+ .then(handleErrorsAndStripe)
+ .then(async () => {
+ telemetry.event(telemetryEventTypes.signup, collectPageParameters());
+ const verifyOrGettingStarted = flags["email-verification"] ? "auth/verify-email" : "getting-started";
+ const callBackUrl = `${
+ searchParams?.get("callbackUrl")
+ ? isOrgInviteByLink
+ ? `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`
+ : addOrUpdateQueryParam(`${WEBAPP_URL}/${searchParams.get("callbackUrl")}`, "from", "signup")
+ : `${WEBAPP_URL}/${verifyOrGettingStarted}?from=signup`
+ }`;
+
+ await signIn<"credentials">("credentials", {
+ ...data,
+ callbackUrl: callBackUrl,
+ });
+ })
+ .catch((err) => {
+ formMethods.setError("apiError", { message: err.message });
+ });
+ };
+
+ return (
+
+
+
+ {/* Left side */}
+
+ {/* Header */}
+ {errors.apiError && (
+
+ )}
+
+
+ {IS_CALCOM ? t("create_your_calcom_account") : t("create_your_account")}
+
+ {IS_CALCOM ? (
+
{t("cal_signup_description")}
+ ) : (
+
+ {t("calcom_explained", {
+ appName: APP_NAME,
+ })}
+
+ )}
+
+ {/* Form Container */}
+
+
+ {/* Continue with Social Logins - Only for non-invite links */}
+ {token || (!isGoogleLoginEnabled && !isSAMLLoginEnabled) ? null : (
+
+
+
+
+ {t("or_continue_with")}
+
+
+
+
+ )}
+ {/* Social Logins - Only for non-invite links*/}
+ {!token && (
+
+ {isGoogleLoginEnabled ? (
+
(
+ <>
+
+ >
+ )}
+ className={classNames(
+ "w-full justify-center rounded-md text-center",
+ formMethods.formState.errors.username ? "opacity-50" : ""
+ )}
+ onClick={async () => {
+ setIsGoogleLoading(true);
+ const username = formMethods.getValues("username");
+ const baseUrl = process.env.NEXT_PUBLIC_WEBAPP_URL;
+ const GOOGLE_AUTH_URL = `${baseUrl}/auth/sso/google`;
+ if (username) {
+ // If username is present we save it in query params to check for premium
+ const searchQueryParams = new URLSearchParams();
+ searchQueryParams.set("username", username);
+ localStorage.setItem("username", username);
+ router.push(`${GOOGLE_AUTH_URL}?${searchQueryParams.toString()}`);
+ return;
+ }
+ router.push(GOOGLE_AUTH_URL);
+ }}>
+ Google
+
+ ) : null}
+ {isSAMLLoginEnabled ? (
+
{
+ if (!formMethods.getValues("username")) {
+ formMethods.trigger("username");
+ }
+ if (!formMethods.getValues("email")) {
+ formMethods.trigger("email");
+
+ return;
+ }
+ const username = formMethods.getValues("username");
+ if (!username) {
+ showToast("error", t("username_required"));
+ return;
+ }
+ localStorage.setItem("username", username);
+ const sp = new URLSearchParams();
+ // @NOTE: don't remove username query param as it's required right now for stripe payment page
+ sp.set("username", username);
+ sp.set("email", formMethods.getValues("email"));
+ router.push(
+ `${process.env.NEXT_PUBLIC_WEBAPP_URL}/auth/sso/saml` + `?${sp.toString()}`
+ );
+ }}>
+
+ {t("saml_sso")}
+
+ ) : null}
+
+ )}
+
+ {/* Already have an account & T&C */}
+
+
+
+
{t("already_have_account")}
+
+ {t("sign_in")}
+
+
+
+
+ By proceeding, you agree to our{" "}
+
+ Terms
+ {" "}
+ and{" "}
+
+ Privacy Policy
+
+ .
+
+
+
+
+
+
+ {IS_CALCOM && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+ {FEATURES.map((feature) => (
+ <>
+
+
+
+ {t(feature.title)}
+
+
+
+ {t(
+ feature.description,
+ feature.i18nOptions && {
+ ...feature.i18nOptions,
+ }
+ )}
+
+
+
+ >
+ ))}
+
+
+
+
+
+ );
+}
+
+export { getServerSideProps };
+
+Signup.PageWrapper = PageWrapper;
+"use client";
+
+import Head from "next/head";
+import { usePathname, useRouter } from "next/navigation";
+import { Suspense } from "react";
+import { z } from "zod";
+
+import { classNames } from "@calcom/lib";
+import { APP_NAME } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
+import { trpc } from "@calcom/trpc";
+import { Button, StepCard, Steps } from "@calcom/ui";
+import { Loader } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+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";
+
+export { getServerSideProps } from "@lib/getting-started/[[...step]]/getServerSideProps";
+
+const INITIAL_STEP = "user-settings";
+const steps = [
+ "user-settings",
+ "connected-calendar",
+ "connected-video",
+ "setup-availability",
+ "user-profile",
+] as const;
+
+const stepTransform = (step: (typeof steps)[number]) => {
+ const stepIndex = steps.indexOf(step);
+ if (stepIndex > -1) {
+ return steps[stepIndex];
+ }
+ return INITIAL_STEP;
+};
+
+const stepRouteSchema = z.object({
+ step: z.array(z.enum(steps)).default([INITIAL_STEP]),
+ from: z.string().optional(),
+});
+
+// TODO: Refactor how steps work to be contained in one array/object. Currently we have steps,initalsteps,headers etc. These can all be in one place
+const OnboardingPage = () => {
+ const pathname = usePathname();
+ const params = useParamsWithFallback();
+
+ const router = useRouter();
+ const [user] = trpc.viewer.me.useSuspenseQuery();
+ const { t } = useLocale();
+
+ const result = stepRouteSchema.safeParse({
+ ...params,
+ step: Array.isArray(params.step) ? params.step : [params.step],
+ });
+
+ const currentStep = result.success ? result.data.step[0] : INITIAL_STEP;
+ const from = result.success ? result.data.from : "";
+ const headers = [
+ {
+ title: `${t("welcome_to_cal_header", { appName: APP_NAME })}`,
+ subtitle: [`${t("we_just_need_basic_info")}`, `${t("edit_form_later_subtitle")}`],
+ },
+ {
+ title: `${t("connect_your_calendar")}`,
+ subtitle: [`${t("connect_your_calendar_instructions")}`],
+ skipText: `${t("connect_calendar_later")}`,
+ },
+ {
+ title: `${t("connect_your_video_app")}`,
+ subtitle: [`${t("connect_your_video_app_instructions")}`],
+ skipText: `${t("set_up_later")}`,
+ },
+ {
+ 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")}`],
+ },
+ ];
+
+ // TODO: Add this in when we have solved the ability to move to tokens accept invite and note invitedto
+ // Ability to accept other pending invites if any (low priority)
+ // if (props.hasPendingInvites) {
+ // headers.unshift(
+ // props.hasPendingInvites && {
+ // title: `${t("email_no_user_invite_heading", { appName: APP_NAME })}`,
+ // subtitle: [], // TODO: come up with some subtitle text here
+ // }
+ // );
+ // }
+
+ const goToIndex = (index: number) => {
+ const newStep = steps[index];
+ router.push(`/getting-started/${stepTransform(newStep)}`);
+ };
+
+ const currentStepIndex = steps.indexOf(currentStep);
+
+ return (
+
+
+
{`${APP_NAME} - ${t("getting_started")}`}
+
+
+
+
+
+
+
+
+ }>
+ {currentStep === "user-settings" && (
+ goToIndex(1)} hideUsername={from === "signup"} />
+ )}
+ {currentStep === "connected-calendar" && goToIndex(2)} />}
+
+ {currentStep === "connected-video" && goToIndex(3)} />}
+
+ {currentStep === "setup-availability" && (
+ goToIndex(4)}
+ defaultScheduleId={user.defaultScheduleId}
+ />
+ )}
+ {currentStep === "user-profile" && }
+
+
+
+ {headers[currentStepIndex]?.skipText && (
+
+ {
+ event.preventDefault();
+ goToIndex(currentStepIndex + 1);
+ }}
+ className="mt-8 cursor-pointer px-4 py-2 font-sans text-sm font-medium">
+ {headers[currentStepIndex]?.skipText}
+
+
+ )}
+
+
+
+
+ );
+};
+
+OnboardingPage.PageWrapper = PageWrapper;
+
+export default OnboardingPage;
+"use client";
+
+// This route is reachable by
+// 1. /team/[slug]
+// 2. / (when on org domain e.g. http://calcom.cal.com/. This is through a rewrite from next.config.js)
+// Also the getServerSideProps and default export are reused by
+// 1. org/[orgSlug]/team/[slug]
+// 2. org/[orgSlug]/[user]/[type]
+import classNames from "classnames";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useEffect } from "react";
+
+import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
+import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
+import { WEBAPP_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
+import useTheme from "@calcom/lib/hooks/useTheme";
+import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
+import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
+import { Avatar, Button, HeadSeo, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
+import { ArrowRight } from "@calcom/ui/components/icon";
+
+import { useToggleQuery } from "@lib/hooks/useToggleQuery";
+import { getServerSideProps } from "@lib/team/[slug]/getServerSideProps";
+import type { inferSSRProps } from "@lib/types/inferSSRProps";
+
+import PageWrapper from "@components/PageWrapper";
+import Team from "@components/team/screens/Team";
+
+export { getServerSideProps };
+
+export type PageProps = inferSSRProps;
+function TeamPage({
+ team,
+ isUnpublished,
+ markdownStrippedBio,
+ isValidOrgDomain,
+ currentOrgDomain,
+}: PageProps) {
+ useTheme(team.theme);
+ const routerQuery = useRouterQuery();
+ const pathname = usePathname();
+ const showMembers = useToggleQuery("members");
+ const { t } = useLocale();
+ const isEmbed = useIsEmbed();
+ const telemetry = useTelemetry();
+ const teamName = team.name || "Nameless Team";
+ const isBioEmpty = !team.bio || !team.bio.replace("
", "").length;
+ const metadata = teamMetadataSchema.parse(team.metadata);
+
+ useEffect(() => {
+ telemetry.event(
+ telemetryEventTypes.pageView,
+ collectPageParameters("/team/[slug]", { isTeamBooking: true })
+ );
+ }, [telemetry, pathname]);
+
+ if (isUnpublished) {
+ const slug = team.slug || metadata?.requestedSlug;
+ return (
+
+
+
+ );
+ }
+
+ // slug is a route parameter, we don't want to forward it to the next route
+ const { slug: _slug, orgSlug: _orgSlug, user: _user, ...queryParamsToForward } = routerQuery;
+
+ const EventTypes = ({ eventTypes }: { eventTypes: NonNullable<(typeof team)["eventTypes"]> }) => (
+
+ {eventTypes.map((type, index) => (
+
+
+
{
+ sdkActionManager?.fire("eventTypeSelected", {
+ eventType: type,
+ });
+ }}
+ data-testid="event-type-link"
+ className="flex justify-between">
+
+
+
+
+
+
+
+ ))}
+
+ );
+
+ const SubTeams = () =>
+ team.children.length ? (
+
+ {team.children.map((ch, i) => {
+ const memberCount = team.members.filter(
+ (mem) => mem.subteams?.includes(ch.slug) && mem.accepted
+ ).length;
+ return (
+
+
+
+
+ {ch.name}
+
+ {t("number_member", {
+ count: memberCount,
+ })}
+
+
+
+ mem.subteams?.includes(ch.slug) && mem.accepted)}
+ />
+
+
+ );
+ })}
+
+ ) : (
+
+
+
+
{` ${t("org_no_teams_yet")}`}
+
{t("org_no_teams_yet_description")}
+
+
+
+ );
+
+ return (
+ <>
+
+
+
+
+
+ {team.parent && `${team.parent.name} `}
+ {teamName}
+
+ {!isBioEmpty && (
+ <>
+
+ >
+ )}
+
+ {metadata?.isOrganization ? (
+
+ ) : (
+ <>
+ {(showMembers.isOn || !team.eventTypes?.length) &&
+ (team.isPrivate ? (
+
+
+ {t("you_cannot_see_team_members")}
+
+
+ ) : (
+
+ ))}
+ {!showMembers.isOn && team.eventTypes && team.eventTypes.length > 0 && (
+
+
+
+ {/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
+ {!team.hideBookATeamMember && !team.isPrivate && (
+
+
+
+
+
+ {t("book_a_team_member")}
+
+
+
+ )}
+
+ )}
+ >
+ )}
+
+ >
+ );
+}
+
+TeamPage.isBookingPage = true;
+TeamPage.PageWrapper = PageWrapper;
+
+export default TeamPage;
+"use client";
+
+import withEmbedSsr from "@lib/withEmbedSsr";
+
+import { getServerSideProps as _getServerSideProps } from "../[type]";
+
+export { default, type PageProps } from "../[type]";
+
+export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+"use client";
+
+import { Booker } from "@calcom/atoms";
+import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
+import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
+
+import { getServerSideProps } from "@lib/team/[slug]/[type]/getServerSideProps";
+import type { inferSSRProps } from "@lib/types/inferSSRProps";
+import type { EmbedProps } from "@lib/withEmbedSsr";
+
+import PageWrapper from "@components/PageWrapper";
+
+export type PageProps = inferSSRProps & EmbedProps;
+
+export { getServerSideProps };
+
+export default function Type({
+ slug,
+ user,
+ booking,
+ away,
+ isEmbed,
+ isBrandingHidden,
+ entity,
+ duration,
+ isInstantMeeting,
+}: PageProps) {
+ return (
+
+
+
+
+ );
+}
+
+Type.PageWrapper = PageWrapper;
+Type.isBookingPage = true;
+"use client";
+
+import withEmbedSsr from "@lib/withEmbedSsr";
+
+import { getServerSideProps as _getServerSideProps } from "../../team/[slug]";
+
+export { default } from "../../team/[slug]";
+
+export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+"use client";
+
+import { useRouter } from "next/navigation";
+
+import Shell from "@calcom/features/shell/Shell";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import { trpc } from "@calcom/trpc/react";
+import { Button, EmptyScreen } from "@calcom/ui";
+import { showToast } from "@calcom/ui";
+import { ArrowUpCircle, CheckCircle } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+
+export type OrgUpgradeBannerProps = {
+ data: RouterOutputs["viewer"]["getUserTopBanners"]["orgUpgradeBanner"];
+};
+
+export default function UpgradePage() {
+ const { t } = useLocale();
+
+ const router = useRouter();
+ const publishOrgMutation = trpc.viewer.organizations.publish.useMutation({
+ onSuccess(data) {
+ router.push(data.url);
+ },
+ onError: (error) => {
+ showToast(error.message, "error");
+ },
+ });
+
+ const doesUserHaveOrgToUpgrade = trpc.viewer.organizations.checkIfOrgNeedsUpgrade.useQuery();
+
+ return (
+
+
+ {doesUserHaveOrgToUpgrade.data ? (
+ {
+ publishOrgMutation.mutate();
+ }}>
+ {t("upgrade")}
+
+ }
+ />
+ ) : (
+ {t("contact_support")}}
+ />
+ )}
+
+
+ );
+}
+UpgradePage.PageWrapper = PageWrapper;
+"use client";
+
+import { getLayout } from "@calcom/features/MainLayout";
+import { TeamsListing } from "@calcom/features/ee/teams/components";
+import { ShellMain } from "@calcom/features/shell/Shell";
+import { WEBAPP_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Button } from "@calcom/ui";
+import { Plus } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+
+export { getServerSideProps } from "@lib/teams/getServerSideProps";
+
+function Teams() {
+ const { t } = useLocale();
+ const [user] = trpc.viewer.me.useSuspenseQuery();
+
+ return (
+
+ {t("new")}
+
+ )
+ }>
+
+
+ );
+}
+
+Teams.requiresLicense = false;
+Teams.PageWrapper = PageWrapper;
+Teams.getLayout = getLayout;
+export default Teams;
+import NextAuth from "next-auth";
+
+import { AUTH_OPTIONS } from "@calcom/features/auth/lib/next-auth-options";
+
+export default NextAuth(AUTH_OPTIONS);
+import { ImageResponse } from "@vercel/og";
+import type { NextApiRequest } from "next";
+import type { SatoriOptions } from "satori";
+import { z } from "zod";
+
+import { Meeting, App, Generic } from "@calcom/lib/OgImages";
+
+const calFont = fetch(new URL("../../../../public/fonts/cal.ttf", import.meta.url)).then((res) =>
+ res.arrayBuffer()
+);
+
+const interFont = fetch(new URL("../../../../public/fonts/Inter-Regular.ttf", import.meta.url)).then((res) =>
+ res.arrayBuffer()
+);
+
+const interFontMedium = fetch(new URL("../../../../public/fonts/Inter-Medium.ttf", import.meta.url)).then(
+ (res) => res.arrayBuffer()
+);
+
+export const config = {
+ runtime: "edge",
+};
+
+const meetingSchema = z.object({
+ imageType: z.literal("meeting"),
+ title: z.string(),
+ names: z.string().array(),
+ usernames: z.string().array(),
+ meetingProfileName: z.string(),
+ meetingImage: z.string().nullable().optional(),
+});
+
+const appSchema = z.object({
+ imageType: z.literal("app"),
+ name: z.string(),
+ description: z.string(),
+ slug: z.string(),
+});
+
+const genericSchema = z.object({
+ imageType: z.literal("generic"),
+ title: z.string(),
+ description: z.string(),
+});
+
+export default async function handler(req: NextApiRequest) {
+ const { searchParams } = new URL(`${req.url}`);
+ const imageType = searchParams.get("type");
+
+ const [calFontData, interFontData, interFontMediumData] = await Promise.all([
+ calFont,
+ interFont,
+ interFontMedium,
+ ]);
+ const ogConfig = {
+ width: 1200,
+ height: 630,
+ fonts: [
+ { name: "inter", data: interFontData, weight: 400 },
+ { name: "inter", data: interFontMediumData, weight: 500 },
+ { name: "cal", data: calFontData, weight: 400 },
+ { name: "cal", data: calFontData, weight: 600 },
+ ] as SatoriOptions["fonts"],
+ };
+
+ switch (imageType) {
+ case "meeting": {
+ const { names, usernames, title, meetingProfileName, meetingImage } = meetingSchema.parse({
+ names: searchParams.getAll("names"),
+ usernames: searchParams.getAll("usernames"),
+ title: searchParams.get("title"),
+ meetingProfileName: searchParams.get("meetingProfileName"),
+ meetingImage: searchParams.get("meetingImage"),
+ imageType,
+ });
+
+ const img = new ImageResponse(
+ (
+ ({ name, username: usernames[index] }))}
+ />
+ ),
+ ogConfig
+ ) as { body: Buffer };
+
+ return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
+ }
+ case "app": {
+ const { name, description, slug } = appSchema.parse({
+ name: searchParams.get("name"),
+ description: searchParams.get("description"),
+ slug: searchParams.get("slug"),
+ imageType,
+ });
+ const img = new ImageResponse( , ogConfig) as {
+ body: Buffer;
+ };
+
+ return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
+ }
+
+ case "generic": {
+ const { title, description } = genericSchema.parse({
+ title: searchParams.get("title"),
+ description: searchParams.get("description"),
+ imageType,
+ });
+
+ const img = new ImageResponse( , ogConfig) as {
+ body: Buffer;
+ };
+
+ return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
+ }
+
+ default:
+ return new Response("What you're looking for is not here..", { status: 404 });
+ }
+}
+import { useAutoAnimate } from "@formkit/auto-animate/react";
+import Link from "next/link";
+import { useRouter, usePathname } from "next/navigation";
+import { useCallback } from "react";
+
+import { getLayout } from "@calcom/features/MainLayout";
+import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
+import { ShellMain } from "@calcom/features/shell/Shell";
+import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { HttpError } from "@calcom/lib/http-error";
+import type { RouterOutputs } from "@calcom/trpc/react";
+import { trpc } from "@calcom/trpc/react";
+import { EmptyScreen, showToast, ToggleGroup } from "@calcom/ui";
+import { Clock } from "@calcom/ui/components/icon";
+
+import { QueryCell } from "@lib/QueryCell";
+
+import PageWrapper from "@components/PageWrapper";
+import SkeletonLoader from "@components/availability/SkeletonLoader";
+
+export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
+ const { t } = useLocale();
+ const utils = trpc.useContext();
+
+ const meQuery = trpc.viewer.me.useQuery();
+
+ const router = useRouter();
+
+ const deleteMutation = trpc.viewer.availability.schedule.delete.useMutation({
+ onMutate: async ({ scheduleId }) => {
+ await utils.viewer.availability.list.cancel();
+ const previousValue = utils.viewer.availability.list.getData();
+ if (previousValue) {
+ const filteredValue = previousValue.schedules.filter(({ id }) => id !== scheduleId);
+ utils.viewer.availability.list.setData(undefined, { ...previousValue, schedules: filteredValue });
+ }
+
+ return { previousValue };
+ },
+
+ onError: (err, variables, context) => {
+ if (context?.previousValue) {
+ utils.viewer.availability.list.setData(undefined, context.previousValue);
+ }
+ if (err instanceof HttpError) {
+ const message = `${err.statusCode}: ${err.message}`;
+ showToast(message, "error");
+ }
+ },
+ onSettled: () => {
+ utils.viewer.availability.list.invalidate();
+ },
+ onSuccess: () => {
+ showToast(t("schedule_deleted_successfully"), "success");
+ },
+ });
+
+ const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
+ onSuccess: async ({ schedule }) => {
+ await utils.viewer.availability.list.invalidate();
+ showToast(
+ t("availability_updated_successfully", {
+ scheduleName: schedule.name,
+ }),
+ "success"
+ );
+ },
+ onError: (err) => {
+ if (err instanceof HttpError) {
+ const message = `${err.statusCode}: ${err.message}`;
+ showToast(message, "error");
+ }
+ },
+ });
+
+ const duplicateMutation = trpc.viewer.availability.schedule.duplicate.useMutation({
+ onSuccess: async ({ schedule }) => {
+ await router.push(`/availability/${schedule.id}`);
+ showToast(t("schedule_created_successfully", { scheduleName: schedule.name }), "success");
+ },
+ onError: (err) => {
+ if (err instanceof HttpError) {
+ const message = `${err.statusCode}: ${err.message}`;
+ showToast(message, "error");
+ }
+ },
+ });
+
+ // Adds smooth delete button - item fades and old item slides into place
+
+ const [animationParentRef] = useAutoAnimate();
+
+ return (
+ <>
+ {schedules.length === 0 ? (
+
+ }
+ />
+
+ ) : (
+ <>
+
+
+ {schedules.map((schedule) => (
+
+ ))}
+
+
+
+ {t("temporarily_out_of_office")}{" "}
+
+ {t("add_a_redirect")}
+
+
+ >
+ )}
+ >
+ );
+}
+
+function AvailabilityListWithQuery() {
+ const query = trpc.viewer.availability.list.useQuery();
+
+ return (
+ }
+ customLoader={ }
+ />
+ );
+}
+
+export default function AvailabilityPage() {
+ const { t } = useLocale();
+ const searchParams = useCompatSearchParams();
+ const router = useRouter();
+ const pathname = usePathname();
+
+ // Get a new searchParams string by merging the current
+ // searchParams with a provided key/value pair
+ const createQueryString = useCallback(
+ (name: string, value: string) => {
+ const params = new URLSearchParams(searchParams ?? undefined);
+ params.set(name, value);
+
+ return params.toString();
+ },
+ [searchParams]
+ );
+ return (
+
+
+ {
+ if (!value) return;
+ router.push(`${pathname}?${createQueryString("type", value)}`);
+ }}
+ options={[
+ { value: "mine", label: t("my_availability") },
+ { value: "team", label: t("team_availability") },
+ ]}
+ />
+
+
+ }>
+ {searchParams?.get("type") === "team" ? : }
+
+
+ );
+}
+
+AvailabilityPage.getLayout = getLayout;
+
+AvailabilityPage.PageWrapper = PageWrapper;
+import { Troubleshooter } from "@calcom/features/troubleshooter/Troubleshooter";
+import { getLayout } from "@calcom/features/troubleshooter/layout";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { HeadSeo } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+function TroubleshooterPage() {
+ const { t } = useLocale();
+ return (
+ <>
+
+
+ >
+ );
+}
+
+TroubleshooterPage.getLayout = getLayout;
+TroubleshooterPage.PageWrapper = PageWrapper;
+export default TroubleshooterPage;
+import { useRouter } from "next/navigation";
+import { useState } from "react";
+import { Controller, useFieldArray, useForm } from "react-hook-form";
+
+import dayjs from "@calcom/dayjs";
+import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
+import Schedule from "@calcom/features/schedules/components/Schedule";
+import Shell from "@calcom/features/shell/Shell";
+import { classNames } from "@calcom/lib";
+import { availabilityAsString } from "@calcom/lib/availability";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { HttpError } from "@calcom/lib/http-error";
+import { trpc } from "@calcom/trpc/react";
+import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
+import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
+import {
+ Button,
+ ConfirmationDialogContent,
+ Dialog,
+ DialogTrigger,
+ Form,
+ Label,
+ showToast,
+ Skeleton,
+ SkeletonText,
+ Switch,
+ TimezoneSelect,
+ Tooltip,
+ VerticalDivider,
+} from "@calcom/ui";
+import { Info, MoreVertical, ArrowLeft, Plus, Trash } from "@calcom/ui/components/icon";
+
+import PageWrapper from "@components/PageWrapper";
+import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
+import EditableHeading from "@components/ui/EditableHeading";
+
+type AvailabilityFormValues = {
+ name: string;
+ schedule: ScheduleType;
+ dateOverrides: { ranges: TimeRange[] }[];
+ timeZone: string;
+ isDefault: boolean;
+};
+
+const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
+ const { remove, append, replace, fields } = useFieldArray({
+ name: "dateOverrides",
+ });
+ const excludedDates = fields.map((field) => dayjs(field.ranges[0].start).utc().format("YYYY-MM-DD"));
+ const { t } = useLocale();
+ return (
+
+
+ {t("date_overrides")}{" "}
+
+
+
+
+
+
+
{t("date_overrides_subtitle")}
+
+
+ ranges.forEach((range) => append({ ranges: [range] }))}
+ Trigger={
+
+ {t("add_an_override")}
+
+ }
+ />
+
+
+ );
+};
+
+export default function Availability() {
+ const searchParams = useCompatSearchParams();
+ const { t, i18n } = useLocale();
+ const router = useRouter();
+ const utils = trpc.useContext();
+ const me = useMeQuery();
+ const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
+ const fromEventType = searchParams?.get("fromEventType");
+ const { timeFormat } = me.data || { timeFormat: null };
+ const [openSidebar, setOpenSidebar] = useState(false);
+ const { data: schedule, isPending } = trpc.viewer.availability.schedule.get.useQuery(
+ { scheduleId },
+ {
+ enabled: !!scheduleId,
+ }
+ );
+
+ const form = useForm({
+ values: schedule && {
+ ...schedule,
+ schedule: schedule?.availability || [],
+ },
+ });
+ const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
+ onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
+ if (prevDefaultId && currentDefaultId) {
+ // check weather the default schedule has been changed by comparing previous default schedule id and current default schedule id.
+ if (prevDefaultId !== currentDefaultId) {
+ // if not equal, invalidate previous default schedule id and refetch previous default schedule id.
+ utils.viewer.availability.schedule.get.invalidate({ scheduleId: prevDefaultId });
+ utils.viewer.availability.schedule.get.refetch({ scheduleId: prevDefaultId });
+ }
+ }
+ utils.viewer.availability.schedule.get.invalidate({ scheduleId: data.schedule.id });
+ utils.viewer.availability.list.invalidate();
+ showToast(
+ t("availability_updated_successfully", {
+ scheduleName: data.schedule.name,
+ }),
+ "success"
+ );
+ },
+ onError: (err) => {
+ if (err instanceof HttpError) {
+ const message = `${err.statusCode}: ${err.message}`;
+ showToast(message, "error");
+ }
+ },
+ });
+
+ const deleteMutation = trpc.viewer.availability.schedule.delete.useMutation({
+ onError: (err) => {
+ if (err instanceof HttpError) {
+ const message = `${err.statusCode}: ${err.message}`;
+ showToast(message, "error");
+ }
+ },
+ onSettled: () => {
+ utils.viewer.availability.list.invalidate();
+ },
+ onSuccess: () => {
+ showToast(t("schedule_deleted_successfully"), "success");
+ router.push("/availability");
+ },
+ });
+
+ return (
+ (
+
+ )}
+ />
+ }
+ subtitle={
+ schedule ? (
+ schedule.schedule
+ .filter((availability) => !!availability.days.length)
+ .map((availability) => (
+
+ {availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
+
+
+ ))
+ ) : (
+
+ )
+ }
+ CTA={
+
+
+
+ {t("set_to_default")}
+
+ {
+ form.setValue("isDefault", e);
+ }}
+ />
+
+
+
+
+
+
+
+ {
+ scheduleId && deleteMutation.mutate({ scheduleId });
+ }}>
+ {t("delete_schedule_description")}
+
+
+
+
+
+
+
setOpenSidebar(false)} />
+ {t("availability_settings")}
+
+
+
+
+ {
+ scheduleId && deleteMutation.mutate({ scheduleId });
+ setOpenSidebar(false);
+ }}>
+ {t("delete_schedule_description")}
+
+
+
+
+ {t("name")}
+ (
+
+ )}
+ />
+
+
+
+ {t("set_to_default")}
+
+ {
+ form.setValue("isDefault", e);
+ }}
+ />
+
+
+
+
+
+
+ {t("timezone")}
+
+
+ value ? (
+ onChange(timezone.value)}
+ />
+ ) : (
+
+ )
+ }
+ />
+
+
+
+
+ {t("something_doesnt_look_right")}
+
+
+
+ {t("launch_troubleshooter")}
+
+
+
+
+
+
+
+
+
+ {t("save")}
+
+
setOpenSidebar(true)}
+ />
+
+ }>
+
+
+ );
+}
+
+Availability.PageWrapper = PageWrapper;
+import { type AppProps } from "@lib/app-providers";
+
+import PageWrapper, { type CalPageWrapper } from "@components/PageWrapper";
+
+import User from "~/users/views/users-public-view";
+
+export { getServerSideProps, type UserPageProps } from "~/users/views/users-public-view.getServerSideProps";
+
+const UserPage = User as unknown as CalPageWrapper & {
+ isBookingPage: AppProps["Component"]["isBookingPage"];
+};
+
+UserPage.isBookingPage = true;
+UserPage.PageWrapper = PageWrapper;
+
+export default UserPage;
+import { type AppProps } from "@lib/app-providers";
+
+import PageWrapper, { type CalPageWrapper } from "@components/PageWrapper";
+
+import TypePage from "~/users/views/users-type-public-view";
+
+export { getServerSideProps } from "~/users/views/users-type-public-view.getServerSideProps";
+
+const Type = TypePage as unknown as CalPageWrapper & {
+ isBookingPage: AppProps["Component"]["isBookingPage"];
+};
+
+Type.isBookingPage = true;
+Type.PageWrapper = PageWrapper;
+
+export default Type;
+import { type AppProps } from "@lib/app-providers";
+import withEmbedSsr from "@lib/withEmbedSsr";
+
+import PageWrapper, { type CalPageWrapper } from "@components/PageWrapper";
+
+import TypePage from "~/users/views/users-type-public-view";
+import { getServerSideProps as _getServerSideProps } from "~/users/views/users-type-public-view.getServerSideProps";
+
+export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+
+const Type = TypePage as unknown as CalPageWrapper & {
+ isBookingPage: AppProps["Component"]["isBookingPage"];
+};
+
+Type.isBookingPage = true;
+Type.PageWrapper = PageWrapper;
+
+export default Type;
+import { type AppProps } from "@lib/app-providers";
+import withEmbedSsr from "@lib/withEmbedSsr";
+
+import PageWrapper, { type CalPageWrapper } from "@components/PageWrapper";
+
+import User from "~/users/views/users-public-view";
+import { getServerSideProps as _getServerSideProps } from "~/users/views/users-public-view.getServerSideProps";
+
+const UserPage = User as unknown as CalPageWrapper & {
+ isBookingPage: AppProps["Component"]["isBookingPage"];
+};
+
+UserPage.isBookingPage = true;
+UserPage.PageWrapper = PageWrapper;
+
+export default UserPage;
+
+export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+export default function RoutingFormsIndex() {
+ return null;
+}
+
+export const getServerSideProps = () => {
+ return {
+ redirect: {
+ destination: `/apps/routing-forms/forms`,
+ permanent: false,
+ },
+ };
+};
+import type { GetServerSidePropsContext } from "next";
+import z from "zod";
+
+const paramsSchema = z
+ .object({
+ pages: z.array(z.string()),
+ })
+ .catch({
+ pages: [],
+ });
+
+export default function RoutingForms() {
+ return null;
+}
+
+export const getServerSideProps = (context: GetServerSidePropsContext) => {
+ const { pages } = paramsSchema.parse(context.params);
+
+ return {
+ redirect: {
+ destination: `/apps/routing-forms/${pages.length ? pages.join("/") : ""}`,
+ permanent: false,
+ },
+ };
+};
+"use client";
+
+import type { ChangeEventHandler } from "react";
+import { useState } from "react";
+
+import { getLayout } from "@calcom/features/MainLayout";
+import { classNames } from "@calcom/lib";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { inferSSRProps } from "@calcom/types/inferSSRProps";
+import type { HorizontalTabItemProps } from "@calcom/ui";
+import {
+ AllApps,
+ AppStoreCategories,
+ HorizontalTabs,
+ TextField,
+ PopularAppsSlider,
+ RecentAppsSlider,
+} from "@calcom/ui";
+import { Search } from "@calcom/ui/components/icon";
+
+import { getServerSideProps } from "@lib/apps/getServerSideProps";
+
+import PageWrapper from "@components/PageWrapper";
+import AppsLayout from "@components/apps/layouts/AppsLayout";
+
+const tabs: HorizontalTabItemProps[] = [
+ {
+ name: "app_store",
+ href: "/apps",
+ },
+ {
+ name: "installed_apps",
+ href: "/apps/installed",
+ },
+];
+
+function AppsSearch({
+ onChange,
+ className,
+}: {
+ onChange: ChangeEventHandler;
+ className?: string;
+}) {
+ const { t } = useLocale();
+ return (
+ }
+ addOnClassname="!border-muted"
+ containerClassName={classNames("focus:!ring-offset-0 m-1", className)}
+ type="search"
+ autoComplete="false"
+ onChange={onChange}
+ placeholder={t("search")}
+ />
+ );
+}
+
+export default function Apps({
+ categories,
+ appStore,
+ userAdminTeams,
+}: Omit, "trpcState">) {
+ const { t } = useLocale();
+ const [searchText, setSearchText] = useState(undefined);
+
+ return (
+ (
+
+
+
+
+
+
setSearchText(e.target.value)} />
+
+
+ )}
+ headerClassName="sm:hidden lg:block hidden"
+ emptyStore={!appStore.length}>
+
+ {!searchText && (
+ <>
+
+
+
+ >
+ )}
+
category.name)}
+ userAdminTeams={userAdminTeams}
+ />
+
+
+ );
+}
+
+export { getServerSideProps };
+
+Apps.PageWrapper = PageWrapper;
+Apps.getLayout = getLayout;
+export { getServerSideProps } from "@lib/apps/installed/getServerSideProps";
+
+function RedirectPage() {
+ return;
+}
+
+export default RedirectPage;
+"use client";
+
+import { useReducer } from "react";
+
+import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { AppCategories } from "@calcom/prisma/enums";
+import { trpc } from "@calcom/trpc/react";
+import { Button, EmptyScreen, AppSkeletonLoader as SkeletonLoader, ShellSubHeading } from "@calcom/ui";
+import type { LucideIcon } from "@calcom/ui/components/icon";
+import {
+ BarChart,
+ Calendar,
+ Contact,
+ CreditCard,
+ Grid,
+ Mail,
+ Plus,
+ Share2,
+ Video,
+} from "@calcom/ui/components/icon";
+
+import { QueryCell } from "@lib/QueryCell";
+import type { querySchemaType } from "@lib/apps/installed/[category]/getServerSideProps";
+import { getServerSideProps } from "@lib/apps/installed/[category]/getServerSideProps";
+
+import PageWrapper from "@components/PageWrapper";
+import { AppList } from "@components/apps/AppList";
+import { CalendarListContainer } from "@components/apps/CalendarListContainer";
+import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
+
+interface IntegrationsContainerProps {
+ variant?: AppCategories;
+ exclude?: AppCategories[];
+ handleDisconnect: (credentialId: number) => void;
+}
+
+const IntegrationsContainer = ({
+ variant,
+ exclude,
+ handleDisconnect,
+}: IntegrationsContainerProps): JSX.Element => {
+ const { t } = useLocale();
+ const query = trpc.viewer.integrations.useQuery({
+ variant,
+ exclude,
+ onlyInstalled: true,
+ includeTeamInstalledApps: true,
+ });
+
+ // TODO: Refactor and reuse getAppCategories?
+ const emptyIcon: Record = {
+ calendar: Calendar,
+ conferencing: Video,
+ automation: Share2,
+ analytics: BarChart,
+ payment: CreditCard,
+ other: Grid,
+ web3: CreditCard, // deprecated
+ video: Video, // deprecated
+ messaging: Mail,
+ crm: Contact,
+ };
+
+ return (
+ }
+ success={({ data }) => {
+ if (!data.items.length) {
+ return (
+
+ {t(`connect_${variant || "other"}_apps`)}
+
+ }
+ />
+ );
+ }
+ return (
+
+
+ {t("add")}
+
+ }
+ />
+
+
+
+ );
+ }}
+ />
+ );
+};
+
+type ModalState = {
+ isOpen: boolean;
+ credentialId: null | number;
+ teamId?: number;
+};
+
+export default function InstalledApps() {
+ const searchParams = useCompatSearchParams();
+ const { t } = useLocale();
+ const category = searchParams?.get("category") as querySchemaType["category"];
+ const categoryList: AppCategories[] = Object.values(AppCategories).filter((category) => {
+ // Exclude calendar and other from categoryList, we handle those slightly differently below
+ return !(category in { other: null, calendar: null });
+ });
+
+ const [data, updateData] = useReducer(
+ (data: ModalState, partialData: Partial) => ({ ...data, ...partialData }),
+ {
+ isOpen: false,
+ credentialId: null,
+ }
+ );
+
+ const handleModelClose = () => {
+ updateData({ isOpen: false, credentialId: null });
+ };
+
+ const handleDisconnect = (credentialId: number, teamId?: number) => {
+ updateData({ isOpen: true, credentialId, teamId });
+ };
+
+ return (
+ <>
+
+ {categoryList.includes(category) && (
+
+ )}
+ {category === "calendar" && }
+ {category === "other" && (
+
+ )}
+
+
+ >
+ );
+}
+
+export { getServerSideProps };
+
+InstalledApps.PageWrapper = PageWrapper;
+"use client";
+
+import Link from "next/link";
+
+import Shell from "@calcom/features/shell/Shell";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import type { inferSSRProps } from "@calcom/types/inferSSRProps";
+import { SkeletonText } from "@calcom/ui";
+import { ArrowLeft, ArrowRight } from "@calcom/ui/components/icon";
+
+import { getServerSideProps } from "@lib/apps/categories/getServerSideProps";
+
+import PageWrapper from "@components/PageWrapper";
+
+export default function Apps({ categories }: Omit, "trpcState">) {
+ const { t, isLocaleReady } = useLocale();
+
+ return (
+
+
+
+
+ {isLocaleReady ? t("app_store") :
}{" "}
+
+
+
+
+ {categories.map((category) => (
+
+
+
{category.name}
+
+ {t("number_apps", { count: category.count })}{" "}
+
+
+
+
+ ))}
+
+
+
+ );
+}
+
+Apps.PageWrapper = PageWrapper;
+
+export { getServerSideProps };
+"use client";
+
+import { Prisma } from "@prisma/client";
+import type { InferGetStaticPropsType } from "next";
+import Link from "next/link";
+
+import Shell from "@calcom/features/shell/Shell";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import prisma from "@calcom/prisma";
+import { AppCategories } from "@calcom/prisma/enums";
+import { AppCard, SkeletonText } from "@calcom/ui";
+
+import { getStaticProps } from "@lib/apps/categories/[category]/getStaticProps";
+
+import PageWrapper from "@components/PageWrapper";
+
+export type PageProps = InferGetStaticPropsType;
+export default function Apps({ apps }: PageProps) {
+ const searchParams = useCompatSearchParams();
+ const { t, isLocaleReady } = useLocale();
+ const category = searchParams?.get("category");
+
+ return (
+ <>
+
+
+ {isLocaleReady ? t("app_store") : }{" "}
+
+ {category && (
+
+ /
+ {t("category_apps", { category: category[0].toUpperCase() + category?.slice(1) })}
+
+ )}
+ >
+ }>
+
+
+ {apps
+ .sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
+ .map((app) => {
+ return
;
+ })}
+
+
+
+ >
+ );
+}
+
+Apps.PageWrapper = PageWrapper;
+
+export const getStaticPaths = async () => {
+ const paths = Object.keys(AppCategories);
+
+ try {
+ await prisma.$queryRaw`SELECT 1`;
+ } catch (e: unknown) {
+ if (e instanceof Prisma.PrismaClientInitializationError) {
+ // Database is not available at build time. Make sure we fall back to building these pages on demand
+ return {
+ paths: [],
+ fallback: "blocking",
+ };
+ } else {
+ throw e;
+ }
+ }
+
+ return {
+ paths: paths.map((category) => ({ params: { category } })),
+ fallback: false,
+ };
+};
+
+export { getStaticProps };
+"use client";
+
+import { Prisma } from "@prisma/client";
+import MarkdownIt from "markdown-it";
+import type { GetStaticPaths } from "next";
+import Link from "next/link";
+
+import { IS_PRODUCTION } from "@calcom/lib/constants";
+import prisma from "@calcom/prisma";
+
+import { getStaticProps } from "@lib/apps/[slug]/getStaticProps";
+import type { inferSSRProps } from "@lib/types/inferSSRProps";
+
+import PageWrapper from "@components/PageWrapper";
+import App from "@components/apps/App";
+
+const md = new MarkdownIt("default", { html: true, breaks: true });
+
+function SingleAppPage(props: inferSSRProps) {
+ // If it's not production environment, it would be a better idea to inform that the App is disabled.
+ if (props.isAppDisabled) {
+ if (!IS_PRODUCTION) {
+ // TODO: Improve disabled App UI. This is just a placeholder.
+ return (
+
+ This App seems to be disabled. If you are an admin, you can enable this app from{" "}
+
+ here
+
+
+ );
+ }
+
+ // Disabled App should give 404 any ways in production.
+ return null;
+ }
+
+ const { source, data } = props;
+ return (
+
+
+ >
+ }
+ />
+ );
+}
+
+export const getStaticPaths: GetStaticPaths<{ slug: string }> = async () => {
+ let paths: { params: { slug: string } }[] = [];
+
+ try {
+ const appStore = await prisma.app.findMany({ select: { slug: true } });
+ paths = appStore.map(({ slug }) => ({ params: { slug } }));
+ } catch (e: unknown) {
+ if (e instanceof Prisma.PrismaClientInitializationError) {
+ // Database is not available at build time, but that's ok – we fall back to resolving paths on demand
+ } else {
+ throw e;
+ }
+ }
+
+ return {
+ paths,
+ fallback: "blocking",
+ };
+};
+
+export { getStaticProps };
+
+SingleAppPage.PageWrapper = PageWrapper;
+
+export default SingleAppPage;
+"use client";
+
+import type { InferGetServerSidePropsType } from "next";
+import { useSession } from "next-auth/react";
+import { useRouter } from "next/navigation";
+
+import { AppSetupPage } from "@calcom/app-store/_pages/setup";
+import { getServerSideProps } from "@calcom/app-store/_pages/setup/_getServerSideProps";
+import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
+import { HeadSeo } from "@calcom/ui";
+
+import PageWrapper from "@components/PageWrapper";
+
+export default function SetupInformation(props: InferGetServerSidePropsType) {
+ const searchParams = useCompatSearchParams();
+ const router = useRouter();
+ const slug = searchParams?.get("slug") as string;
+ const { status } = useSession();
+
+ if (status === "loading") {
+ return
;
+ }
+
+ if (status === "unauthenticated") {
+ const urlSearchParams = new URLSearchParams({
+ callbackUrl: `/apps/${slug}/setup`,
+ });
+ router.replace(`/auth/login?${urlSearchParams.toString()}`);
+ }
+
+ return (
+ <>
+ {/* So that the set up page does not get indexed by search engines */}
+
+
+ >
+ );
+}
+
+SetupInformation.PageWrapper = PageWrapper;
+
+export { getServerSideProps };
+"use client";
+
+import type { GetServerSidePropsContext } from "next";
+
+import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
+import RoutingFormsRoutingConfig from "@calcom/app-store/routing-forms/pages/app-routing.config";
+import TypeformRoutingConfig from "@calcom/app-store/typeform/pages/app-routing.config";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
+import prisma from "@calcom/prisma";
+import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps";
+
+import type { AppProps } from "@lib/app-providers";
+
+import PageWrapper from "@components/PageWrapper";
+
+import { ssrInit } from "@server/lib/ssr";
+
+type AppPageType = {
+ getServerSideProps: AppGetServerSideProps;
+ // A component than can accept any properties
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ default: ((props: any) => JSX.Element) &
+ Pick;
+};
+
+type Found = {
+ notFound: false;
+ Component: AppPageType["default"];
+ getServerSideProps: AppPageType["getServerSideProps"];
+};
+
+type NotFound = {
+ notFound: true;
+};
+
+// TODO: It is a candidate for apps.*.generated.*
+const AppsRouting = {
+ "routing-forms": RoutingFormsRoutingConfig,
+ typeform: TypeformRoutingConfig,
+};
+
+function getRoute(appName: string, pages: string[]) {
+ const routingConfig = AppsRouting[appName as keyof typeof AppsRouting] as Record;
+
+ if (!routingConfig) {
+ return {
+ notFound: true,
+ } as NotFound;
+ }
+ const mainPage = pages[0];
+ const appPage = routingConfig.layoutHandler || (routingConfig[mainPage] as AppPageType);
+ if (!appPage) {
+ return {
+ notFound: true,
+ } as NotFound;
+ }
+ return { notFound: false, Component: appPage.default, ...appPage } as Found;
+}
+
+const AppPage: AppPageType["default"] = function AppPage(props) {
+ const appName = props.appName;
+ const params = useParamsWithFallback();
+ const pages = Array.isArray(params.pages) ? params.pages : params.pages?.split("/") ?? [];
+ const route = getRoute(appName, pages);
+
+ const componentProps = {
+ ...props,
+ pages: pages.slice(1),
+ };
+
+ if (!route || route.notFound) {
+ throw new Error("Route can't be undefined");
+ }
+ return ;
+};
+
+AppPage.isBookingPage = ({ router }) => {
+ const route = getRoute(router.query.slug as string, router.query.pages as string[]);
+ if (route.notFound) {
+ return false;
+ }
+ const isBookingPage = route.Component.isBookingPage;
+ if (typeof isBookingPage === "function") {
+ return isBookingPage({ router });
+ }
+
+ return !!isBookingPage;
+};
+
+export const getLayout: NonNullable<(typeof AppPage)["getLayout"]> = (page) => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const { slug, pages } = useParamsWithFallback();
+ const route = getRoute(slug as string, pages as string[]);
+
+ if (route.notFound) {
+ return null;
+ }
+ if (!route.Component.getLayout) {
+ return page;
+ }
+ return route.Component.getLayout(page);
+};
+
+AppPage.PageWrapper = PageWrapper;
+AppPage.getLayout = getLayout;
+
+export default AppPage;
+
+export async function getServerSideProps(
+ context: GetServerSidePropsContext<{
+ slug: string;
+ pages: string[];
+ appPages?: string[];
+ }>
+) {
+ const { params, req, res } = context;
+ if (!params) {
+ return {
+ notFound: true,
+ };
+ }
+ const appName = params.slug;
+ const pages = params.pages;
+ const route = getRoute(appName, pages);
+ if (route.notFound) {
+ return route;
+ }
+ if (route.getServerSideProps) {
+ // TODO: Document somewhere that right now it is just a convention that filename should have appPages in it's name.
+ // appPages is actually hardcoded here and no matter the fileName the same variable would be used.
+ // We can write some validation logic later on that ensures that [...appPages].tsx file exists
+ params.appPages = pages.slice(1);
+ const session = await getServerSession({ req, res });
+ const user = session?.user;
+ const app = await getAppWithMetadata({ slug: appName });
+ if (!app) {
+ return {
+ notFound: true,
+ };
+ }
+
+ const result = await route.getServerSideProps(
+ context as GetServerSidePropsContext<{
+ slug: string;
+ pages: string[];
+ appPages: string[];
+ }>,
+ prisma,
+ user,
+ ssrInit
+ );
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ //@ts-ignore
+ if (result.notFound) {
+ return {
+ notFound: true,
+ };
+ }
+ if (result.redirect) {
+ return {
+ redirect: result.redirect,
+ };
+ }
+ return {
+ props: {
+ appName,
+ appUrl: app.simplePath || `/apps/${appName}`,
+ ...result.props,
+ },
+ };
+ } else {
+ return {
+ props: {
+ appName,
+ },
+ };
+ }
+}
+"use client";
+
+import { Booker } from "@calcom/atoms";
+import { getBookerWrapperClasses } from "@calcom/features/bookings/Booker/utils/getBookerWrapperClasses";
+import { BookerSeo } from "@calcom/features/bookings/components/BookerSeo";
+
+import { getServerSideProps, type PageProps } from "@lib/d/[link]/[slug]/getServerSideProps";
+
+import PageWrapper from "@components/PageWrapper";
+
+export default function Type({
+ slug,
+ isEmbed,
+ user,
+ booking,
+ away,
+ isBrandingHidden,
+ isTeamEvent,
+ entity,
+ duration,
+ hashedLink,
+}: PageProps) {
+ return (
+
+
+
+
+ );
+}
+
+export { getServerSideProps };
+
+Type.PageWrapper = PageWrapper;
+Type.isBookingPage = true;
+import type { IncomingMessage } from "http";
+import type { AppContextType } from "next/dist/shared/lib/utils";
+import React from "react";
+
+import { trpc } from "@calcom/trpc/react";
+
+import type { AppProps } from "@lib/app-providers";
+
+import "../styles/globals.css";
+
+function MyApp(props: AppProps) {
+ const { Component, pageProps } = props;
+
+ if (Component.PageWrapper !== undefined) return Component.PageWrapper(props);
+ return ;
+}
+
+declare global {
+ interface Window {
+ calNewLocale: string;
+ }
+}
+
+MyApp.getInitialProps = async (ctx: AppContextType) => {
+ const { req } = ctx.ctx;
+
+ let newLocale = "en";
+
+ if (req) {
+ const { getLocale } = await import("@calcom/features/auth/lib/getLocale");
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ newLocale = await getLocale(req as IncomingMessage & { cookies: Record });
+ } else if (typeof window !== "undefined" && window.calNewLocale) {
+ newLocale = window.calNewLocale;
+ }
+
+ return {
+ pageProps: {
+ newLocale,
+ },
+ };
+};
+
+const WrappedMyApp = trpc.withTRPC(MyApp);
+
+export default WrappedMyApp;
+// page can be a server component
+import type { GetServerSidePropsContext } from "next";
+import { URLSearchParams } from "url";
+import { z } from "zod";
+
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { getDefaultEvent } from "@calcom/lib/defaultEvents";
+import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
+import prisma, { bookingMinimalSelect } from "@calcom/prisma";
+import { BookingStatus } from "@calcom/prisma/client";
+
+export default function Type() {
+ // Just redirect to the schedule page to reschedule it.
+ return null;
+}
+
+export async function getServerSideProps(context: GetServerSidePropsContext) {
+ const session = await getServerSession(context);
+
+ const { uid: bookingUid, seatReferenceUid } = z
+ .object({ uid: z.string(), seatReferenceUid: z.string().optional() })
+ .parse(context.query);
+
+ const { uid, seatReferenceUid: maybeSeatReferenceUid } = await maybeGetBookingUidFromSeat(
+ prisma,
+ bookingUid
+ );
+ const booking = await prisma.booking.findUnique({
+ where: {
+ uid,
+ },
+ select: {
+ ...bookingMinimalSelect,
+ eventType: {
+ select: {
+ users: {
+ select: {
+ username: true,
+ },
+ },
+ slug: true,
+ team: {
+ select: {
+ slug: true,
+ },
+ },
+ seatsPerTimeSlot: true,
+ userId: true,
+ owner: {
+ select: {
+ id: true,
+ },
+ },
+ hosts: {
+ select: {
+ user: {
+ select: {
+ id: true,
+ },
+ },
+ },
+ },
+ },
+ },
+ dynamicEventSlugRef: true,
+ dynamicGroupSlugRef: true,
+ user: true,
+ status: true,
+ },
+ });
+ const dynamicEventSlugRef = booking?.dynamicEventSlugRef || "";
+
+ if (!booking) {
+ return {
+ notFound: true,
+ } as const;
+ }
+
+ // If booking is already CANCELLED or REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details.
+ // A booking that has been rescheduled to a new booking will also have a status of CANCELLED
+ if (booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED) {
+ return {
+ redirect: {
+ destination: `/booking/${uid}`,
+ permanent: false,
+ },
+ };
+ }
+
+ if (!booking?.eventType && !booking?.dynamicEventSlugRef) {
+ // TODO: Show something in UI to let user know that this booking is not rescheduleable
+ return {
+ notFound: true,
+ } as {
+ notFound: true;
+ };
+ }
+
+ // if booking event type is for a seated event and no seat reference uid is provided, throw not found
+ if (booking?.eventType?.seatsPerTimeSlot && !maybeSeatReferenceUid) {
+ const userId = session?.user?.id;
+
+ if (!userId && !seatReferenceUid) {
+ return {
+ redirect: {
+ destination: `/auth/login?callbackUrl=/reschedule/${bookingUid}`,
+ permanent: false,
+ },
+ };
+ }
+ const userIsHost = booking?.eventType.hosts.find((host) => {
+ if (host.user.id === userId) return true;
+ });
+
+ const userIsOwnerOfEventType = booking?.eventType.owner?.id === userId;
+
+ if (!userIsHost && !userIsOwnerOfEventType) {
+ return {
+ notFound: true,
+ } as {
+ notFound: true;
+ };
+ }
+ }
+
+ const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
+
+ const eventPage = `${
+ eventType.team
+ ? `team/${eventType.team.slug}`
+ : dynamicEventSlugRef
+ ? booking.dynamicGroupSlugRef
+ : booking.user?.username || "rick" /* This shouldn't happen */
+ }/${eventType?.slug}`;
+ const destinationUrl = new URLSearchParams();
+
+ destinationUrl.set("rescheduleUid", seatReferenceUid || bookingUid);
+
+ return {
+ redirect: {
+ destination: `/${eventPage}?${destinationUrl.toString()}${
+ eventType.seatsPerTimeSlot ? "&bookingUid=null" : ""
+ }`,
+ permanent: false,
+ },
+ };
+}
+"use client";
+
+import withEmbedSsr from "@lib/withEmbedSsr";
+
+import { getServerSideProps as _getServerSideProps } from "../[uid]";
+
+export { default } from "../[uid]";
+
+export const getServerSideProps = withEmbedSsr(_getServerSideProps);
+"use client";
+
+import Shell, { MobileNavigationMoreItems } from "@calcom/features/shell/Shell";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+
+import PageWrapper from "@components/PageWrapper";
+
+export default function MorePage() {
+ const { t } = useLocale();
+ return (
+
+
+
+
{t("more_page_footer")}
+
+
+ );
+}
+MorePage.PageWrapper = PageWrapper;
diff --git a/typescript.js b/files/typescript.js
similarity index 100%
rename from typescript.js
rename to files/typescript.js