Files
calendar/packages/features/ee/organizations/lib/onboardingStore.ts
T
sean-brydonGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>Hariom Balhara
fa35cc5210 chore: organization onboarding refactor (#24381)
* feat: redirect to new onboarding flow

* Getting started

* Brand details

* Preview organization brands

* Orgs team pages

* Invite team steps

* Move to global zustand store

* Few darkmdoe fixes

* Wip onboarding + stripe flow

* Default plan state

Server Action for gettting slug satus of org

* Remove onboardingId

* Confirmation prompt

* Update old onboarding flow handlers to handle new fields

* update onboarding hook

* Filter out organization section for none -company emails

* Match placeholders to users domain

* Drop migration

* Wip new onboarding intent

* WIP flow for self-hosted. Same service call just split logic

* WIP

* Add TODO

* Use onboarding user type instead of trpc session

* WIP

* WIP

* pass role and team name from onboarding to save in schema

* Add test to ensure role + name + team are persisted into onboarding table

* migrate roles to enum values

* Update ENUM

* Fix type error

* Redirect if flag is disabled

* Remove web

* WIP

* WIP

* Fix migration

* Fix calls

* User onboarding User types instead of trpc session

* Fix factory tests

* Fix flow for self hoste

* Type error

* More type fixes

* Fix handler tests

* Fix enum return type being different

* Use consistant types across the oganization stuff

* Fix

* Use TEAM_BILLING for e2e test

* Refactor is not company email and add tests

* Fix

* Fix

* Refactor flow to submit after form complete

* Fix flow with billing disabled

* Fix tests

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Rename and move test files

* WIP

* Fix types

* Update repo paths + tests

* Move to service folder

* Fix tests

* Fix types

* Remove old test files

* Restore lock

* Fix path

* Fix tests with new paths and factory logic

* Fix updaetdAt

* WIP onboardingID isolation

* Fix e2e test

* verify test

* Code rabbit

* Rename SelfHostedOnboardongService -> SelfHostedOrganizationOnboardingService

* Fix stores

* Fix type error

* Fix types

* remove tsignore

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* NITS

* Add the logic to auto complete admin org when billing enabled

* Fix store being weird

* We need to return the parsed value

* fixes

* sync from db always

* Add onboardingSgtore tests

* fix test

* remove step and status

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2025-10-18 14:13:30 +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,
};
};