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/Cargo.toml b/Cargo.toml
index 7547f0d..33a190f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,7 +22,7 @@ oxc = { version = "0.13.5", features = ["transformer", "codegen"] }
swc = "0.276.0"
swc_common = "0.33.26"
swc_ecma_ast = "0.113.7"
-swc_ecma_parser = "0.144.2"
+swc_ecma_parser = { version = "0.144.2", features = ["typescript"] }
swc_ecma_transforms_react = "0.184.1"
swc_ecma_transforms_typescript = "0.189.1"
swc_ecma_visit = "0.99.1"
@@ -30,7 +30,7 @@ swc_ecma_visit = "0.99.1"
num_cpus = "1.16.0"
criterion2 = { version = "0.10.0", default-features = false }
rayon = "1.10.0"
-mimalloc = "0.1.41"
+mimalloc = "0.1.42"
[features]
codspeed = ["criterion2/codspeed"]
diff --git a/benches/transformer.rs b/benches/transformer.rs
index d4c69ea..16f1a5d 100644
--- a/benches/transformer.rs
+++ b/benches/transformer.rs
@@ -11,23 +11,23 @@ trait TheBencher {
const ID: &'static str;
- fn run(source: &str) -> Self::RunOutput;
+ fn run(path: &Path, source: &str) -> Self::RunOutput;
- fn bench(g: &mut BenchmarkGroup<'_, WallTime>, source: &str) {
+ fn bench(g: &mut BenchmarkGroup<'_, WallTime>, path: &Path, source: &str) {
let cpus = num_cpus::get_physical();
let id = BenchmarkId::new(Self::ID, "single-thread");
- g.bench_with_input(id, &source, |b, source| b.iter(|| Self::run(source)));
+ g.bench_with_input(id, &source, |b, source| b.iter(|| Self::run(path, source)));
let id = BenchmarkId::new(Self::ID, "no-drop");
g.bench_with_input(id, &source, |b, source| {
- b.iter_with_large_drop(|| Self::run(source))
+ b.iter_with_large_drop(|| Self::run(path, source))
});
let id = BenchmarkId::new(Self::ID, "parallel");
g.bench_with_input(id, &source, |b, source| {
b.iter(|| {
(0..cpus).into_par_iter().for_each(|_| {
- Self::run(source);
+ Self::run(path, source);
});
})
});
@@ -41,7 +41,7 @@ impl TheBencher for OxcBencher {
const ID: &'static str = "oxc";
- fn run(source_text: &str) -> Self::RunOutput {
+ fn run(path: &Path, source_text: &str) -> Self::RunOutput {
use oxc::{
allocator::Allocator,
codegen::{Codegen, CodegenOptions},
@@ -51,7 +51,7 @@ impl TheBencher for OxcBencher {
};
let allocator = Allocator::default();
- let source_type = SourceType::default();
+ let source_type = SourceType::from_path(path).unwrap();
{
let ret = Parser::new(&allocator, source_text, source_type).parse();
let trivias = ret.trivias;
@@ -86,11 +86,11 @@ impl TheBencher for SwcBencher {
const ID: &'static str = "swc";
- fn run(source: &str) -> Self::RunOutput {
+ fn run(path: &Path, source: &str) -> Self::RunOutput {
use std::sync::Arc;
use swc::{Compiler, PrintArgs, SwcComments};
use swc_common::{chain, source_map::SourceMap, sync::Lrc, Mark, GLOBALS};
- use swc_ecma_parser::{Parser, StringInput, Syntax};
+ use swc_ecma_parser::{EsConfig, Parser, StringInput, Syntax, TsConfig};
use swc_ecma_transforms_react::{react, Options};
use swc_ecma_transforms_typescript::strip;
use swc_ecma_visit::FoldWith;
@@ -98,10 +98,18 @@ impl TheBencher for SwcBencher {
let cm = Lrc::new(SourceMap::new(swc_common::FilePathMapping::empty()));
let compiler = Compiler::new(Arc::clone(&cm));
let comments = SwcComments::default();
+ let syntax = match path.extension().unwrap().to_str().unwrap() {
+ "js" => Syntax::Es(EsConfig::default()),
+ "tsx" => Syntax::Typescript(TsConfig {
+ tsx: true,
+ ..TsConfig::default()
+ }),
+ _ => panic!("need to define syntax for swc"),
+ };
GLOBALS.set(&Default::default(), || {
let program = Parser::new(
- Syntax::Es(Default::default()),
+ syntax,
StringInput::new(source, Default::default(), Default::default()),
Some(&comments),
)
@@ -132,13 +140,15 @@ 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 path = Path::new("files").join(filename);
+ let source = std::fs::read_to_string(&path).unwrap();
+ let mut g = c.benchmark_group(filename);
+ OxcBencher::bench(&mut g, &path, &source);
+ SwcBencher::bench(&mut g, &path, &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 = (
+
+