From 79bcd6dc30d445a2185846d3b03d8ec2651b8102 Mon Sep 17 00:00:00 2001
From: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
Date: Mon, 3 Nov 2025 11:11:16 +0000
Subject: [PATCH] feat: welcome to organizations modal after creation (#24823)
## What does this PR do?
Adds a welcome modal for new organizations that appears after organization creation. The modal showcases key features of the Organizations plan and provides a better onboarding experience.
## Visual Demo (For contributors especially)
#### Image Demo:

## Mandatory Tasks (DO NOT REMOVE)
- [x] I have self-reviewed the code.
- [x] I have updated the developer docs in /docs if this PR makes changes that would require a documentation change. N/A
- [x] I confirm automated tests are in place that prove my fix is effective or that my feature works.
## How should this be tested?
1. Create a new organization through either:
- The onboarding flow
- The settings/organizations/new page
- The organization creation form
2. After successful creation and redirect, verify the welcome modal appears showing organization features.
3. Verify the modal can be closed by:
- Clicking the "Continue" button
- Clicking outside the modal
- Pressing ESC key
4. Verify the modal doesn't reappear after being closed (query param and session storage should be cleared).
## Checklist
- I have read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md)
- My code follows the style guidelines of this project
- I have commented my code, particularly in hard-to-understand areas
- I have checked if my changes generate no new warnings
---
.../onboarding/hooks/useSubmitOnboarding.ts | 14 +-
.../new/_components/OnboardMembersView.tsx | 5 +-
.../new/_components/PaymentStatusView.tsx | 2 +-
apps/web/public/static/locales/en/common.json | 9 +
.../WelcomeToOrganizationsModal.tsx | 168 ++++++++++++++++++
.../ee/organizations/hooks/useWelcomeModal.ts | 51 ++++++
packages/features/shell/DynamicModals.tsx | 22 +++
packages/features/shell/Shell.tsx | 2 +
8 files changed, 262 insertions(+), 11 deletions(-)
create mode 100644 packages/features/ee/organizations/components/WelcomeToOrganizationsModal.tsx
create mode 100644 packages/features/ee/organizations/hooks/useWelcomeModal.ts
create mode 100644 packages/features/shell/DynamicModals.tsx
diff --git a/apps/web/modules/onboarding/hooks/useSubmitOnboarding.ts b/apps/web/modules/onboarding/hooks/useSubmitOnboarding.ts
index 0e5db227b7..d26ae08f29 100644
--- a/apps/web/modules/onboarding/hooks/useSubmitOnboarding.ts
+++ b/apps/web/modules/onboarding/hooks/useSubmitOnboarding.ts
@@ -1,6 +1,7 @@
import { useRouter } from "next/navigation";
import { useState } from "react";
+import { setShowNewOrgModalFlag } from "@calcom/features/ee/organizations/hooks/useWelcomeModal";
import { useFlagMap } from "@calcom/features/flags/context/provider";
import { CreationSource } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
@@ -25,14 +26,8 @@ export const useSubmitOnboarding = () => {
setError(null);
try {
- const {
- selectedPlan,
- organizationDetails,
- organizationBrand,
- teams,
- inviteRole,
- resetOnboarding,
- } = store;
+ const { selectedPlan, organizationDetails, organizationBrand, teams, inviteRole, resetOnboarding } =
+ store;
if (selectedPlan !== "organization") {
throw new Error("Only organization plan is currently supported");
@@ -83,7 +78,8 @@ export const useSubmitOnboarding = () => {
// No checkout URL means billing is disabled (self-hosted flow)
// Organization has already been created by the backend
showToast("Organization created successfully!", "success");
- // TODO: after this redirect we need to hard refresh the page to see org
+ // Set flag to show welcome modal after personal onboarding redirect
+ setShowNewOrgModalFlag();
skipToPersonal(resetOnboarding);
} catch (err) {
const errorMessage = err instanceof Error ? err.message : "Failed to create organization";
diff --git a/apps/web/modules/settings/organizations/new/_components/OnboardMembersView.tsx b/apps/web/modules/settings/organizations/new/_components/OnboardMembersView.tsx
index 5ec8541438..42c421d2c8 100644
--- a/apps/web/modules/settings/organizations/new/_components/OnboardMembersView.tsx
+++ b/apps/web/modules/settings/organizations/new/_components/OnboardMembersView.tsx
@@ -5,6 +5,7 @@ import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
+import { setShowNewOrgModalFlag } from "@calcom/features/ee/organizations/hooks/useWelcomeModal";
import { useOnboarding } from "@calcom/features/ee/organizations/lib/onboardingStore";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc";
@@ -52,7 +53,9 @@ const useOrgCreation = () => {
await utils.viewer.organizations.listCurrent.invalidate();
await session.update();
reset();
- window.location.href = `${window.location.origin}/settings/organizations/profile`;
+ // Set flag to show welcome modal (using both query param and sessionStorage for reliability)
+ setShowNewOrgModalFlag();
+ window.location.href = `${window.location.origin}/settings/organizations/profile?newOrganizationModal=true`;
} else {
// Unexpected state
setServerErrorMessage("Unexpected response from server");
diff --git a/apps/web/modules/settings/organizations/new/_components/PaymentStatusView.tsx b/apps/web/modules/settings/organizations/new/_components/PaymentStatusView.tsx
index 2cedf3f3ab..52092ae2dd 100644
--- a/apps/web/modules/settings/organizations/new/_components/PaymentStatusView.tsx
+++ b/apps/web/modules/settings/organizations/new/_components/PaymentStatusView.tsx
@@ -28,7 +28,7 @@ const PaymentStatusView = () => {
if (organization) {
setOrganizationCreated(true);
// Organization is created, redirect to next step
- router.push(`/settings/organizations`);
+ router.push(`/settings/organizations?newOrganizationModal=true`);
}
}, [organization, router, useOnboardingStore]);
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index fc9b3fd001..6d78041b08 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -2527,6 +2527,15 @@
"resolve": "Resolve",
"no_organization_slug": "There was an error creating teams for this organization. Missing URL slug.",
"copy_link_org": "Copy link to organization",
+ "welcome_to_organizations": "Welcome to Organizations!",
+ "organizations_welcome_description": "We're excited to connect your bookers with your team members. With your Teams's plan you get:",
+ "1_parent_team_unlimited_subteams": "1 parent team and unlimited sub-teams",
+ "organization_workflows": "Organization workflows",
+ "custom_subdomain": "Yourcompany.cal.com subdomain",
+ "instant_meetings": "Cal.com instant meetings",
+ "collective_round_robin_events": "Collective & Round-robin events",
+ "routing_forms": "Routing forms",
+ "team_workflows": "Team workflows",
"404_the_org": "The organization",
"404_the_team": "The team",
"404_claim_entity_org": "Claim your subdomain for your organization",
diff --git a/packages/features/ee/organizations/components/WelcomeToOrganizationsModal.tsx b/packages/features/ee/organizations/components/WelcomeToOrganizationsModal.tsx
new file mode 100644
index 0000000000..d4c6e027b1
--- /dev/null
+++ b/packages/features/ee/organizations/components/WelcomeToOrganizationsModal.tsx
@@ -0,0 +1,168 @@
+"use client";
+
+import { motion } from "framer-motion";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Button } from "@calcom/ui/components/button";
+import { Dialog, DialogContent } from "@calcom/ui/components/dialog";
+import { Icon, type IconName } from "@calcom/ui/components/icon";
+import { Logo } from "@calcom/ui/components/logo";
+
+import { useWelcomeModal } from "../hooks/useWelcomeModal";
+
+const features = [
+ "1_parent_team_unlimited_subteams",
+ "organization_workflows",
+ "custom_subdomain",
+ "instant_meetings",
+ "collective_round_robin_events",
+ "routing_forms",
+ "team_workflows",
+];
+
+export function WelcomeToOrganizationsModal() {
+ const { t } = useLocale();
+ const { isOpen, closeModal } = useWelcomeModal();
+
+ const SMALL = { outer: 32, icon: 16 };
+ const LARGE = { outer: 48, icon: 24 };
+ const RINGS = [60, 95, 130]; // Ring radii in px
+ const RING_STROKE = 1;
+
+ return (
+
+ );
+}
diff --git a/packages/features/ee/organizations/hooks/useWelcomeModal.ts b/packages/features/ee/organizations/hooks/useWelcomeModal.ts
new file mode 100644
index 0000000000..4ab1ef7f4c
--- /dev/null
+++ b/packages/features/ee/organizations/hooks/useWelcomeModal.ts
@@ -0,0 +1,51 @@
+import { parseAsBoolean, useQueryState } from "nuqs";
+import { useEffect, useState } from "react";
+
+const STORAGE_KEY = "showNewOrgModal";
+
+export function useWelcomeModal() {
+ const [newOrganizationModal, setNewOrganizationModal] = useQueryState(
+ "newOrganizationModal",
+ parseAsBoolean.withDefault(false)
+ );
+
+ const [isOpen, setIsOpen] = useState(false);
+
+ useEffect(() => {
+ // Check query param first
+ if (newOrganizationModal) {
+ setIsOpen(true);
+ return;
+ }
+
+ // Check sessionStorage as fallback (for cases where we redirect through personal onboarding)
+ if (typeof window !== "undefined" && sessionStorage.getItem(STORAGE_KEY) === "true") {
+ setIsOpen(true);
+ }
+ }, [newOrganizationModal]);
+
+ const closeModal = () => {
+ setIsOpen(false);
+ // Remove the query param from URL
+ setNewOrganizationModal(null);
+ // Also clear sessionStorage
+ if (typeof window !== "undefined") {
+ sessionStorage.removeItem(STORAGE_KEY);
+ }
+ };
+
+ return {
+ isOpen,
+ closeModal,
+ };
+}
+
+/**
+ * Helper function to set the flag that triggers the welcome modal.
+ * Use this before redirecting to ensure the modal shows after navigation.
+ */
+export function setShowNewOrgModalFlag() {
+ if (typeof window !== "undefined") {
+ sessionStorage.setItem(STORAGE_KEY, "true");
+ }
+}
diff --git a/packages/features/shell/DynamicModals.tsx b/packages/features/shell/DynamicModals.tsx
new file mode 100644
index 0000000000..a80f9c7383
--- /dev/null
+++ b/packages/features/shell/DynamicModals.tsx
@@ -0,0 +1,22 @@
+"use client";
+
+import { WelcomeToOrganizationsModal } from "@calcom/features/ee/organizations/components/WelcomeToOrganizationsModal";
+
+/**
+ * Container for all query-param driven modals that should appear globally across the app.
+ * This keeps the Shell component clean and provides a centralized place for dynamic modals.
+ *
+ * We can probably also use this for thinks like the T&C and Privacy Policy modals. That @marketing are discussing
+ *
+ * To add a new modal:
+ * 1. Create the modal component with its own useQueryState hook
+ * 2. Import and add it here
+ */
+export function DynamicModals() {
+ return (
+ <>
+