"use client"; import { subdomainSuffix } from "@calcom/features/ee/organizations/lib/orgDomains"; import { isCompanyEmail } from "@calcom/features/ee/organizations/lib/utils"; import { IS_SELF_HOSTED } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import slugify from "@calcom/lib/slugify"; import { BillingPeriod, CreationSource, UserPermissionRole } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc/react"; import type { Ensure } from "@calcom/types/utils"; import classNames from "@calcom/ui/classNames"; import { Alert } from "@calcom/ui/components/alert"; import { Button } from "@calcom/ui/components/button"; import { Form, Label, TextField, ToggleGroup } from "@calcom/ui/components/form"; import { RadioAreaGroup as RadioArea } from "@calcom/ui/components/radio"; import { useOnboarding } from "@calcom/web/modules/ee/organizations/lib/onboardingStore"; import { useRouter } from "next/navigation"; import type { SessionContextValue } from "next-auth/react"; import { useSession } from "next-auth/react"; import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; function extractDomainFromEmail(email: string) { const match = email.match(/^(?:.*?:\/\/)?.*?([\w-]*(?:\.\w{2,}|\.\w{2,}\.\w{2}))(?:[/?#:]|$)/); const out = (match && match[1]) ?? ""; return out.split(".")[0]; } export const CreateANewOrganizationForm = () => { const session = useSession(); const { isLoadingOrgOnboarding } = useOnboarding(); if (!session.data || isLoadingOrgOnboarding) { return null; } return ; }; const CreateANewOrganizationFormChild = ({ session }: { session: Ensure }) => { const { t } = useLocale(); const router = useRouter(); const [serverErrorMessage, setServerErrorMessage] = useState(null); const isAdmin = session.data.user.role === UserPermissionRole.ADMIN; // Let self-hosters create an organization with their own email. Hosted's Admin already has an organization for their email const defaultOrgOwnerEmail = (!isAdmin || IS_SELF_HOSTED ? session.data.user.email : null) ?? ""; const { useOnboardingStore, isBillingEnabled } = useOnboarding(); const { slug, name, orgOwnerEmail, billingPeriod, pricePerSeat, seats, onboardingId, reset } = useOnboardingStore(); // For non-admin users, always use the current session email to prevent stale cached email issues. // This handles the case where a user changes their email in settings and returns to org creation. const effectiveOrgOwnerEmail = !isAdmin ? defaultOrgOwnerEmail : orgOwnerEmail || defaultOrgOwnerEmail; const userHasCompanyEmail = isAdmin || isCompanyEmail(effectiveOrgOwnerEmail); const newOrganizationFormMethods = useForm<{ name: string; seats: number | null; billingPeriod: BillingPeriod; pricePerSeat: number | null; slug: string; orgOwnerEmail: string; }>({ defaultValues: { billingPeriod: billingPeriod ?? BillingPeriod.MONTHLY, slug: slug ?? (!isAdmin ? deriveSlugFromEmail(effectiveOrgOwnerEmail) : undefined), orgOwnerEmail: effectiveOrgOwnerEmail, name: name ?? (!isAdmin ? deriveOrgNameFromEmail(effectiveOrgOwnerEmail) : undefined), seats: seats ?? null, pricePerSeat: pricePerSeat ?? null, }, }); const intentToCreateOrgMutation = trpc.viewer.organizations.intentToCreateOrg.useMutation({ onSuccess: async (data) => { reset({ onboardingId: data.organizationOnboardingId, billingPeriod: data.billingPeriod, pricePerSeat: data.pricePerSeat ?? null, seats: data.seats ?? null, orgOwnerEmail: data.orgOwnerEmail, name: data.name, slug: data.slug, }); // Small delay to ensure Zustand persist middleware has time to write to localStorage await new Promise((resolve) => setTimeout(resolve, 50)); if (data.handoverUrl) { // Admin handover flow - redirect to handover page router.push("/settings/organizations/new/handover"); } else if (data.organizationId) { // Self-hosted flow - org already created, redirect to organizations list router.push("/settings/organizations"); } else { // Regular flow - continue to next step router.push("/settings/organizations/new/about"); } }, onError: (err) => { if (err.message === "organization_url_taken") { newOrganizationFormMethods.setError("slug", { type: "custom", message: t("url_taken") }); } else if (err.message === "domain_taken_team" || err.message === "domain_taken_project") { newOrganizationFormMethods.setError("slug", { type: "custom", message: t("problem_registering_domain"), }); } else { setServerErrorMessage(t(err.message)); } }, }); const needToCreateOnboarding = !onboardingId; if (!userHasCompanyEmail) { return (
{t("update_email_address")} } />
); } return ( <>
{ if (!needToCreateOnboarding) { // Resuming existing onboarding - just navigate to next step router.push("/settings/organizations/new/about"); } else { // Check if this is admin handover flow based on the submitted form value const isAdminHandoverFlow = isAdmin && v.orgOwnerEmail !== session.data.user.email; if (isAdminHandoverFlow) { // Admin creating for someone else - submit immediately with just Step 1 data if (!intentToCreateOrgMutation.isPending) { setServerErrorMessage(null); intentToCreateOrgMutation.mutate({ ...v, creationSource: CreationSource.WEBAPP }); } } else { // Regular user or admin creating for self - store locally and continue reset({ billingPeriod: v.billingPeriod, pricePerSeat: v.pricePerSeat, seats: v.seats, orgOwnerEmail: v.orgOwnerEmail, name: v.name, slug: v.slug, }); router.push("/settings/organizations/new/about"); } } }}>
{serverErrorMessage && (
)} {isBillingEnabled && isAdmin && (
( <> { if ([BillingPeriod.ANNUALLY, BillingPeriod.MONTHLY].includes(e)) { onChange(e); } }} options={[ { value: "MONTHLY", label: "Monthly", }, { value: "ANNUALLY", label: "Annually", }, ]} /> )} />
)} (
{ const email = e?.target.value; newOrganizationFormMethods.setValue("orgOwnerEmail", email.trim()); if (newOrganizationFormMethods.getValues("slug") === "") { const slug = deriveSlugFromEmail(email); newOrganizationFormMethods.setValue("slug", slug); } const name = deriveOrgNameFromEmail(email); newOrganizationFormMethods.setValue("name", name); }} autoComplete="off" />
)} />
( <> { newOrganizationFormMethods.setValue("name", e?.target.value.trim()); if (newOrganizationFormMethods.formState.touchedFields["slug"] === undefined) { newOrganizationFormMethods.setValue("slug", slugify(e?.target.value)); } }} autoComplete="off" /> )} />
( { newOrganizationFormMethods.setValue("slug", slugify(e?.target.value), { shouldTouch: true, }); newOrganizationFormMethods.clearErrors("slug"); }} /> )} />
{isBillingEnabled && isAdmin && ( <>
(
{ onChange(+e.target.value); }} autoComplete="off" />
)} />
(
{ onChange(+e.target.value); }} autoComplete="off" />
)} />
)} {/* This radio group does nothing - its just for visual purposes */} {isBillingEnabled && !isAdmin && ( <>

Upgrade to Organizations

{t("teams")}

{t("your_current_plan")}

{t("organization")} {pricePerSeat && seats ? (

{`$${pricePerSeat} per user per month ${ billingPeriod === BillingPeriod.ANNUALLY ? "(billed annually)" : "" }`}

) : (

{t("organization_price_per_user_month")}

)}
)}
); }; export function deriveSlugFromEmail(email: string) { const domain = extractDomainFromEmail(email); return domain; } export function deriveOrgNameFromEmail(email: string) { const domain = extractDomainFromEmail(email); return domain.charAt(0).toUpperCase() + domain.slice(1); }