Files
calendar/apps/web/modules/ee/organizations/lib/onboardingStore.ts
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
0774a41119 refactor: move hooks and stores from packages/features to apps/web/modules (#27221)
* refactor: move booking-audit client components from packages/features to apps/web/modules

This is part of the larger effort to move tRPC-dependent UI components from packages/features to apps/web/modules to eliminate circular dependencies.

Changes:
- Move BookingHistory.tsx and BookingHistoryPage.tsx to apps/web/modules/booking-audit/components/
- Update imports in apps/web to use the new location

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: move formbricks client from packages/features to apps/web/modules

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: move hooks and stores from packages/features to apps/web/modules

- Move useAppsData hook from packages/features/apps/hooks to apps/web/modules/apps/hooks
- Move onboardingStore from packages/features/ee/organizations/lib to apps/web/modules/ee/organizations/lib
- Move useWelcomeModal hook from packages/features/ee/organizations/hooks to apps/web/modules/ee/organizations/hooks
- Move useAgentsData hook from packages/features/ee/workflows/hooks to apps/web/modules/ee/workflows/hooks
- Update all import paths in consuming files

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* refactor: move onboardingStore test file to apps/web/modules

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-01-26 13:11:14 +00:00

192 lines
6.2 KiB
TypeScript

import { useSession } from "next-auth/react";
import { useSearchParams, useRouter, usePathname } from "next/navigation";
import { useEffect } from "react";
import { create } from "zustand";
import { persist, StorageValue } from "zustand/middleware";
import { WEBAPP_URL, IS_TEAM_BILLING_ENABLED_CLIENT } from "@calcom/lib/constants";
import { localStorage } from "@calcom/lib/webstorage";
import { BillingPeriod, UserPermissionRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
interface OnboardingAdminStoreState {
billingPeriod?: BillingPeriod;
pricePerSeat?: number | null;
seats?: number | null;
orgOwnerEmail?: string;
}
interface OnboardingUserStoreState {
name: string;
slug: string;
logo?: string | null;
bio?: string | null;
brandColor?: string | null;
bannerUrl?: string | null;
onboardingId: string | null;
invitedMembers: { email: string; name?: string }[];
teams: { id: number; name: string; slug: string | null; isBeingMigrated: boolean }[];
}
interface OnboardingStoreState extends OnboardingAdminStoreState, OnboardingUserStoreState {
// Actions for admin state
setBillingPeriod: (billingPeriod: BillingPeriod) => void;
setPricePerSeat: (price: number | null) => void;
setSeats: (seats: number | null) => void;
setOrgOwnerEmail: (email: string) => void;
// Actions for user state
setName: (name: string) => void;
setSlug: (slug: string) => void;
setLogo: (logo: string) => void;
setBio: (bio: string) => void;
setBrandColor: (brandColor: string) => void;
setBannerUrl: (bannerUrl: string) => void;
addInvitedMember: (member: { email: string; name?: string }) => void;
removeInvitedMember: (email: string) => void;
setOnboardingId: (onboardingId: string) => void;
// Actions for team state
setTeams: (teams: { id: number; name: string; slug: string | null; isBeingMigrated: boolean }[]) => void;
// Reset state
reset: (state?: Partial<OnboardingStoreState>) => void;
}
const initialState: OnboardingAdminStoreState & OnboardingUserStoreState = {
// User state
name: "",
slug: "",
logo: "",
bio: "",
brandColor: "",
bannerUrl: "",
orgOwnerEmail: "",
seats: null,
pricePerSeat: null,
billingPeriod: undefined,
onboardingId: null,
invitedMembers: [],
teams: [],
};
export const useOnboardingStore = create<OnboardingStoreState>()(
persist(
(set, get) => ({
...initialState,
// Admin actions
setBillingPeriod: (billingPeriod) => set({ billingPeriod }),
setPricePerSeat: (pricePerSeat) => set({ pricePerSeat }),
setSeats: (seats) => set({ seats }),
setOrgOwnerEmail: (orgOwnerEmail) => set({ orgOwnerEmail }),
// User actions
setName: (name) => set({ name }),
setSlug: (slug) => set({ slug }),
setLogo: (logo) => set({ logo }),
setBio: (bio) => set({ bio }),
setBrandColor: (brandColor) => set({ brandColor }),
setBannerUrl: (bannerUrl) => set({ bannerUrl }),
setOnboardingId: (onboardingId) => set({ onboardingId }),
addInvitedMember: (member) =>
set((state) => ({
invitedMembers: [...state.invitedMembers, member],
})),
removeInvitedMember: (email) =>
set((state) => ({
invitedMembers: state.invitedMembers.filter((member) => member.email !== email),
})),
// Team actions
setTeams: (teams) => set({ teams }),
reset: (state) => {
if (state) {
set(state);
} else {
// Clear the localStorage entry for this store
if (typeof window !== "undefined") {
localStorage.removeItem("org-creation-onboarding");
}
set(initialState);
}
},
}),
{
name: "org-creation-onboarding",
}
)
);
export const useOnboarding = () => {
const session = useSession();
const router = useRouter();
const path = usePathname();
const isAdmin = session.data?.user?.role === UserPermissionRole.ADMIN;
const isBillingEnabled = process.env.NEXT_PUBLIC_IS_E2E ? false : IS_TEAM_BILLING_ENABLED_CLIENT;
const searchParams = useSearchParams();
const { data: organizationOnboarding, isPending: isLoadingOrgOnboarding } =
trpc.viewer.organizations.getOrganizationOnboarding.useQuery();
const { reset } = useOnboardingStore();
useEffect(() => {
if (isLoadingOrgOnboarding) {
return;
}
// Admin on handover page should never touch the store
// The DB query returns admin's own onboarding, not the one being handed over
if (isAdmin && path?.includes("/handover")) {
return;
}
if (organizationOnboarding?.isComplete) {
reset();
}
if (organizationOnboarding) {
// Admin creating for someone else - don't sync from DB, let them proceed to handover
if (isAdmin && organizationOnboarding?.orgOwnerEmail !== session.data?.user.email) {
return;
}
// Always sync from DB to ensure UI reflects latest changes
reset({
onboardingId: organizationOnboarding.id,
billingPeriod: organizationOnboarding.billingPeriod as BillingPeriod,
pricePerSeat: organizationOnboarding.pricePerSeat,
seats: organizationOnboarding.seats,
orgOwnerEmail: organizationOnboarding.orgOwnerEmail,
name: organizationOnboarding.name,
slug: organizationOnboarding.slug,
bio: organizationOnboarding.bio,
logo: organizationOnboarding.logo,
brandColor: organizationOnboarding.brandColor,
bannerUrl: organizationOnboarding.bannerUrl,
invitedMembers: [],
teams: [],
});
}
// Note: We no longer redirect if onboardingId is missing, as regular users
// don't create the onboarding record until the final step
}, [organizationOnboarding, isLoadingOrgOnboarding, isAdmin, reset, path]);
useEffect(() => {
if (session.status === "loading") {
return;
}
if (!session.data) {
const searchString = !searchParams ? "" : `${searchParams.toString()}`;
router.push(`/auth/login?callbackUrl=${WEBAPP_URL}${path}${searchString ? `?${searchString}` : ""}`);
}
}, [session, router, path, searchParams]);
return {
useOnboardingStore,
isLoadingOrgOnboarding,
dbOnboarding: organizationOnboarding,
isBillingEnabled,
};
};