diff --git a/apps/web/modules/signup-view.tsx b/apps/web/modules/signup-view.tsx index 20e17e03e9353b..cad50e2a6af46d 100644 --- a/apps/web/modules/signup-view.tsx +++ b/apps/web/modules/signup-view.tsx @@ -1,865 +1,865 @@ -"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 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, - setPremium, - setUsernameTaken, - ]); - - 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, - orgAutoAcceptEmail, - redirectUrl, - emailVerificationEnabled, - onboardingV3Enabled, -}: SignupProps) { - const isOrgInviteByLink = orgSlug && !prepopulateFormValues?.username; - 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 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 constructCallBackIfUrlPresent = () => { - if (isOrgInviteByLink) { - return `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`; - } - return addOrUpdateQueryParam(`${WEBAPP_URL}/${searchParams.get("callbackUrl")}`, "from", "signup"); - }; - - const constructCallBackIfUrlNotPresent = () => { - 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 */} -