"use client"; import getStripe from "@calcom/app-store/stripepayment/lib/client"; import { getPremiumPlanPriceValue } from "@calcom/app-store/stripepayment/lib/utils"; import { fetchSignup, hasCheckoutSession, isAccountUnderReview, isUserAlreadyExistsError, } from "@calcom/features/auth/signup/lib/fetchSignup"; import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail"; import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains"; import ServerTrans from "@calcom/lib/components/ServerTrans"; import { APP_NAME, CLOUDFLARE_SITE_ID, IS_CALCOM, URL_PROTOCOL_REGEX, WEBAPP_URL, WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL, WEBSITE_URL, } from "@calcom/lib/constants"; import { isENVDev } from "@calcom/lib/env"; import { fetchUsername } from "@calcom/lib/fetchUsername"; import { pushGTMEvent } from "@calcom/lib/gtm"; import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; import { useDebounce } from "@calcom/lib/hooks/useDebounce"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { INVALID_CLOUDFLARE_TOKEN_ERROR } from "@calcom/lib/server/checkCfTurnstileToken"; import { IS_EUROPE } from "@calcom/lib/timezoneConstants"; import { signupSchema as apiSignupSchema } from "@calcom/prisma/zod-utils"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import classNames from "@calcom/ui/classNames"; import { Alert } from "@calcom/ui/components/alert"; import { Button } from "@calcom/ui/components/button"; import { CheckboxField, Form, PasswordField, SelectField, TextField } from "@calcom/ui/components/form"; import { Icon } from "@calcom/ui/components/icon"; import { showToast } from "@calcom/ui/components/toast"; import { InfoIcon, ShieldCheckIcon, StarIcon } from "@coss/ui/icons"; import { Analytics as DubAnalytics } from "@dub/analytics/react"; import { zodResolver } from "@hookform/resolvers/zod"; import type { getServerSideProps } from "@lib/signup/getServerSideProps"; import dynamic from "next/dynamic"; import Link from "next/link"; import { useRouter } from "next/navigation"; import Script from "next/script"; import { signIn } from "next-auth/react"; import posthog from "posthog-js"; import { useEffect, useState } from "react"; import type { SubmitHandler } from "react-hook-form"; import { useForm, useFormContext } from "react-hook-form"; import { Toaster } from "sonner"; import { z } from "zod"; const signupSchema = apiSignupSchema.extend({ apiError: z.string().optional(), // Needed to display API errors doesn't get passed to the API cfToken: z.string().optional(), }); const TurnstileCaptcha = dynamic(() => import("@calcom/web/modules/auth/components/Turnstile"), { ssr: false, }); type FormValues = z.infer; export type SignupProps = inferSSRProps; const FEATURES = [ { title: "connect_all_calendars", description: "connect_all_calendars_description", i18nOptions: { appName: APP_NAME, }, icon: "calendar-heart" as const, }, { title: "set_availability", description: "set_availbility_description", icon: "users" as const, }, { title: "share_a_link_or_embed", description: "share_a_link_or_embed_description", icon: "link-2" as const, i18nOptions: { appName: APP_NAME, }, }, ]; function truncateDomain(domain: string) { const maxLength = 25; const cleanDomain = domain.replace(URL_PROTOCOL_REGEX, ""); if (cleanDomain.length <= maxLength) { return cleanDomain; } return `${cleanDomain.substring(0, maxLength - 3)}.../`; } function UsernameField({ username, setPremium, premium, setUsernameTaken, orgSlug, usernameTaken, disabled, ...props }: React.ComponentProps & { username: string; setPremium: (value: boolean) => void; premium: boolean; usernameTaken: boolean; orgSlug?: string; setUsernameTaken: (value: boolean) => void; }) { const { t } = useLocale(); const { register, formState } = useFormContext(); const debouncedUsername = useDebounce(username, 600); useEffect(() => { if (formState.isSubmitting || formState.isSubmitSuccessful) return; async function checkUsername() { // If the username can't be changed, there is no point in doing the username availability check if (disabled) return; if (!debouncedUsername) { setPremium(false); setUsernameTaken(false); return; } fetchUsername(debouncedUsername, orgSlug ?? null).then(({ data }) => { setPremium(data.premium); setUsernameTaken(!data.available); }); } checkUsername(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedUsername, disabled, orgSlug, formState.isSubmitting, formState.isSubmitSuccessful]); return (
{(!formState.isSubmitting || !formState.isSubmitted) && (
{usernameTaken ? (

{t("already_in_use_error")}

) : premium ? (

{t("premium_username", { price: getPremiumPlanPriceValue(), interpolation: { escapeValue: false }, })}

) : null}
)}
); } function addOrUpdateQueryParam(url: string, key: string, value: string) { const separator = url.includes("?") ? "&" : "?"; const param = `${key}=${encodeURIComponent(value)}`; return `${url}${separator}${param}`; } export default function Signup({ prepopulateFormValues, token, orgSlug, isGoogleLoginEnabled, isOutlookLoginEnabled, isSAMLLoginEnabled, orgAutoAcceptEmail, redirectUrl, emailVerificationEnabled, onboardingV3Enabled, }: SignupProps) { const isOrgInviteByLink = orgSlug && !prepopulateFormValues?.username; const [isSamlSignup, setIsSamlSignup] = useState(false); const [premiumUsername, setPremiumUsername] = useState(false); const [usernameTaken, setUsernameTaken] = useState(false); const [isGoogleLoading, setIsGoogleLoading] = useState(false); const [isMicrosoftLoading, setIsMicrosoftLoading] = useState(false); const [accountUnderReview, setAccountUnderReview] = useState(false); const [displayEmailForm, setDisplayEmailForm] = useState(token); const [turnstileKey, setTurnstileKey] = useState(0); const searchParams = useCompatSearchParams(); const { t, i18n } = useLocale(); const router = useRouter(); const formMethods = useForm({ resolver: zodResolver(signupSchema), defaultValues: prepopulateFormValues satisfies FormValues, mode: "onTouched", }); const { register, watch, formState: { isSubmitting, errors, isSubmitSuccessful }, } = formMethods; useEffect(() => { if (redirectUrl) { localStorage.setItem("onBoardingRedirect", redirectUrl); } }, [redirectUrl]); const [userConsentToCookie, setUserConsentToCookie] = useState(false); // No need to be checked for user to proceed function handleConsentChange(consent: boolean) { setUserConsentToCookie(!consent); } const loadingSubmitState = isSubmitSuccessful || isSubmitting; const displayBackButton = token ? false : displayEmailForm; const isPlatformUser = redirectUrl?.includes("platform") && redirectUrl?.includes("new"); const signUp: SubmitHandler = async (_data) => { const { cfToken, ...data } = _data; posthog.capture("signup_form_submitted", { has_token: !!token, is_org_invite: isOrgInviteByLink, org_slug: orgSlug, is_premium_username: premiumUsername, username_taken: usernameTaken, }); try { const result = await fetchSignup( { ...data, language: i18n.language, token, }, cfToken ); if (!result.ok) { if (isUserAlreadyExistsError(result)) { showToast(t("account_already_exists_please_login"), "warning"); const callbackUrl = token ? `/teams?token=${token}` : "/event-types"; setTimeout(() => { router.push(`/auth/login?callbackUrl=${encodeURIComponent(callbackUrl)}`); }, 3000); return; } if (hasCheckoutSession(result)) { const stripe = await getStripe(); if (stripe) { const { error } = await stripe.redirectToCheckout({ sessionId: result.error.checkoutSessionId, }); if (error) console.warn(error.message); } return; } throw new Error(result.error.message); } if (isAccountUnderReview(result)) { setAccountUnderReview(true); return; } if (process.env.NEXT_PUBLIC_GTM_ID) { pushGTMEvent("create_account", { email: data.email, user: data.username, lang: data.language }); } const gettingStartedPath = onboardingV3Enabled ? "onboarding/getting-started" : "getting-started"; const verifyOrGettingStarted = emailVerificationEnabled ? "auth/verify-email" : gettingStartedPath; const gettingStartedWithPlatform = "settings/platform/new"; const constructCallBackIfUrlPresent = () => { if (isOrgInviteByLink) { return `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`; } return addOrUpdateQueryParam(`${WEBAPP_URL}/${searchParams.get("callbackUrl")}`, "from", "signup"); }; const constructCallBackIfUrlNotPresent = () => { if (isPlatformUser) { return `${WEBAPP_URL}/${gettingStartedWithPlatform}?from=signup`; } return `${WEBAPP_URL}/${verifyOrGettingStarted}?from=signup`; }; const constructCallBackUrl = () => { const callbackUrlSearchParams = searchParams?.get("callbackUrl"); return callbackUrlSearchParams ? constructCallBackIfUrlPresent() : constructCallBackIfUrlNotPresent(); }; await signIn<"credentials">("credentials", { ...data, callbackUrl: constructCallBackUrl(), }); } catch (err) { setTurnstileKey((k) => k + 1); formMethods.setValue("cfToken", undefined); const errorMessage = err instanceof Error ? err.message : t("unexpected_error_try_again"); if (errorMessage === INVALID_CLOUDFLARE_TOKEN_ERROR) { return; } posthog.capture("signup_form_submit_error", { has_token: !!token, is_org_invite: isOrgInviteByLink, org_slug: orgSlug, is_premium_username: premiumUsername, error_message: errorMessage, }); formMethods.setError("apiError", { message: errorMessage }); } }; return ( <> {IS_CALCOM && (!IS_EUROPE || userConsentToCookie) ? ( <> {process.env.NEXT_PUBLIC_GTM_ID && ( <> {/* biome-ignore lint/security/noDangerouslySetInnerHtml: GTM script injection */}