Files
calendar/packages/features/ee/organizations/lib/onboardingStore.ts
T
837349e3d3 feat: Organization onboarding improvement - Payment before creation of org but onboarding doesn't require payment (#18990)
* create seed for org upgrade

* migrate about/new pages

* pages refactor + use persistant zustand store

* invited members view + confirm button

* add check to org slug

* check reserved subdomain

* remove quotes from subdomain env var

* intro to creation + billing

* create price

* fix types

* createWithPaymentIntent + permission check on teams

* open stripe link in popup

* intro to tests

* organization price and seat override tests

* move permissions to a new permission service

* update env and permission check

* intro to paid invoice trigger

* dont use subId

* wip

* Get e2e working with migration of teams and members

* fix ts errors

* Get flow working again

* Fix various issues and refactor

* Some more fixes

* Fix wrong page route link

* Platform onboarding fix and moving members of team to org rix

* Platform onboarding fix and moving members of team to org rix

* Fix tests, found a bug

* Get custom price flow working

* Get admin impersonation flow working for a non-existent user

* Fix unit test

* Admin onboarding handover

* fix admin onboarding tests

* fix ts error

* Get updateQuantity working

* More fixes

* fix test

* fix schema name

* Add tests

* Add missing file

* More tests

* Add more tests

* fix mt-2 moving down input into overflow

* fix handover layout removing HOC

* Update PR_TODO.md

* fix ts error due to merge conflict

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmgmail.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2025-03-13 11:17:43 +00:00

175 lines
5.7 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 } from "zustand/middleware";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { UserPermissionRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
enum BillingPeriod {
MONTHLY = "MONTHLY",
ANNUALLY = "ANNUALLY",
}
interface OnboardingAdminStoreState {
billingPeriod?: BillingPeriod;
pricePerSeat?: number | null;
seats?: number | null;
orgOwnerEmail?: string;
}
interface OnboardingUserStoreState {
name: string;
slug: string;
logo?: string | null;
bio?: 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;
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: "",
orgOwnerEmail: "",
seats: null,
pricePerSeat: null,
billingPeriod: undefined,
onboardingId: null,
invitedMembers: [],
teams: [],
};
export const useOnboardingStore = create<OnboardingStoreState>()(
persist(
(set) => ({
...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 }),
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 action
reset: (state) => {
if (state) {
set(state);
} else {
set(initialState);
}
},
}),
{
name: "org-creation-onboarding",
}
)
);
export const useOnboarding = (params?: { step?: "start" | "status" | null }) => {
const session = useSession();
const router = useRouter();
const path = usePathname();
const isAdmin = session.data?.user?.role === UserPermissionRole.ADMIN;
const searchParams = useSearchParams();
const { data: organizationOnboarding, isPending: isLoadingOrgOnboarding } =
trpc.viewer.organizations.getOrganizationOnboarding.useQuery();
const { reset, onboardingId } = useOnboardingStore();
const step = params?.step ?? null;
useEffect(() => {
if (isLoadingOrgOnboarding) {
return;
}
if (organizationOnboarding) {
// Must not keep resetting state on each step change as every step doesn't save at the moment and this would reset user's changes
if (!window.isOrgOnboardingSynced) {
window.isOrgOnboardingSynced = true;
// Must reset with current state of onboarding in DB for the user
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,
});
}
} else {
// First step doesn't require onboardingId
const requireOnboardingId = step !== "start";
// Reset to first step if onboardingId isn't available
if (!onboardingId && requireOnboardingId) {
console.warn("No onboardingId found in store, redirecting to /settings/organizations/new");
router.push("/settings/organizations/new");
}
}
// Admin must start fresh, so that they can create organization with any email and data doesn't mix from different org creations
if (isAdmin) {
reset();
}
}, [organizationOnboarding, isLoadingOrgOnboarding, isAdmin, onboardingId, reset, step, router]);
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 };
};