Skip to content

Commit fa35cc5

Browse files
sean-brydoncoderabbitai[bot]hariombalhara
authored
chore: organization onboarding refactor (calcom#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>
1 parent 1eb6ff3 commit fa35cc5

48 files changed

Lines changed: 4959 additions & 1813 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { _generateMetadata } from "app/_utils";
2+
3+
import ResumeOnboardingPage, { LayoutWrapper } from "~/settings/organizations/new/resume-view";
4+
5+
export const generateMetadata = async () =>
6+
await _generateMetadata(
7+
(t) => t("resume_onboarding"),
8+
(t) => t("resume_onboarding_description"),
9+
undefined,
10+
undefined,
11+
"/settings/organizations/new/resume"
12+
);
13+
14+
const ServerPage = async () => {
15+
return (
16+
<LayoutWrapper>
17+
<ResumeOnboardingPage />
18+
</LayoutWrapper>
19+
);
20+
};
21+
22+
export default ServerPage;

apps/web/modules/settings/organizations/new/_components/AddNewTeamsForm.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ const AddNewTeamsFormChild = ({ teams }: { teams: { id: number; name: string; sl
205205
<TextField
206206
key={field.id}
207207
{...register(`teams.${index}.name`)}
208+
data-testid={`team.${index}.name`}
208209
label=""
209210
addOnClassname="bg-transparent p-0 border-l-0"
210211
className={index > 0 ? "mb-2" : ""}

apps/web/modules/settings/organizations/new/_components/OnboardMembersView.tsx

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -34,41 +34,39 @@ const useOrgCreation = () => {
3434
const session = useSession();
3535
const utils = trpc.useUtils();
3636
const [serverErrorMessage, setServerErrorMessage] = useState("");
37-
const { useOnboardingStore, isBillingEnabled } = useOnboarding();
37+
const { useOnboardingStore } = useOnboarding();
3838
const { reset } = useOnboardingStore();
3939

40-
const checkoutMutation = trpc.viewer.organizations.createWithPaymentIntent.useMutation({
41-
onSuccess: (data) => {
40+
// Single mutation for all flows (billing, self-hosted, admin)
41+
const intentToCreateOrgMutation = trpc.viewer.organizations.intentToCreateOrg.useMutation({
42+
onSuccess: async (data) => {
43+
reset({
44+
onboardingId: data.organizationOnboardingId,
45+
});
46+
4247
if (data.checkoutUrl) {
48+
// Billing enabled - redirect to Stripe
4349
window.location.href = data.checkoutUrl;
44-
}
45-
},
46-
onError: (error) => {
47-
setServerErrorMessage(t(error.message));
48-
},
49-
});
50-
51-
const createOrgMutation = trpc.viewer.organizations.createSelfHosted.useMutation({
52-
onSuccess: async (data) => {
53-
if (data.organization) {
54-
// Invalidate the organizations query to ensure fresh data on the next page
50+
} else if (data.organizationId) {
51+
// Self-hosted - org already created, redirect to organizations
5552
await utils.viewer.organizations.listCurrent.invalidate();
5653
await session.update();
5754
reset();
5855
window.location.href = `${window.location.origin}/settings/organizations/profile`;
56+
} else {
57+
// Unexpected state
58+
setServerErrorMessage("Unexpected response from server");
5959
}
6060
},
6161
onError: (error) => {
6262
setServerErrorMessage(t(error.message));
6363
},
6464
});
6565

66-
const mutationToUse = isBillingEnabled ? checkoutMutation : createOrgMutation;
67-
6866
return {
69-
mutation: mutationToUse,
70-
mutate: mutationToUse.mutate,
71-
isPending: mutationToUse.isPending,
67+
mutation: intentToCreateOrgMutation,
68+
mutate: intentToCreateOrgMutation.mutate,
69+
isPending: intentToCreateOrgMutation.isPending,
7270
errorMessage: serverErrorMessage,
7371
};
7472
};
@@ -84,7 +82,13 @@ export const AddNewTeamMembersForm = () => {
8482
invitedMembers,
8583
logo,
8684
bio,
87-
onboardingId,
85+
name,
86+
slug,
87+
billingPeriod,
88+
seats,
89+
pricePerSeat,
90+
brandColor,
91+
bannerUrl,
8892
} = useOnboardingStore();
8993
const orgCreation = useOrgCreation();
9094

@@ -155,7 +159,7 @@ export const AddNewTeamMembersForm = () => {
155159
placeholder="colleague@company.com"
156160
/>
157161
</div>
158-
<Button type="submit" StartIcon="plus" color="secondary">
162+
<Button type="submit" StartIcon="plus" color="secondary" data-testid="invite-new-member-button">
159163
{t("add")}
160164
</Button>
161165
</form>
@@ -203,22 +207,31 @@ export const AddNewTeamMembersForm = () => {
203207
)}
204208
</div>
205209

206-
<div className="mt-3 mt-6 flex items-center justify-end">
210+
<div className="mt-3 flex items-center justify-end">
207211
<Button
212+
data-testid="publish-button"
208213
onClick={() => {
209-
if (!onboardingId) {
210-
console.error("Org owner email and onboardingId are required", {
211-
orgOwnerEmail,
212-
onboardingId,
213-
});
214+
// Submit ALL data to intentToCreateOrg
215+
if (!name || !slug || !orgOwnerEmail) {
216+
console.error("Required fields missing", { name, slug, orgOwnerEmail });
217+
showToast(t("required_fields_missing"), "error");
214218
return;
215219
}
216-
orgCreation.mutation.mutate({
220+
221+
orgCreation.mutate({
222+
name,
223+
slug,
224+
orgOwnerEmail,
225+
seats,
226+
pricePerSeat,
227+
billingPeriod,
228+
creationSource: "WEBAPP" as const,
217229
logo,
218230
bio,
231+
brandColor,
232+
bannerUrl,
219233
teams,
220234
invitedMembers,
221-
onboardingId,
222235
});
223236
}}
224237
loading={orgCreation.isPending}>

apps/web/modules/settings/organizations/new/_components/PaymentStatusView.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,16 @@ import { useEffect, useState } from "react";
66
import { useOnboarding } from "@calcom/features/ee/organizations/lib/onboardingStore";
77
import { useLocale } from "@calcom/lib/hooks/useLocale";
88
import { trpc } from "@calcom/trpc";
9-
import { Icon } from "@calcom/ui/components/icon";
109
import { Button } from "@calcom/ui/components/button";
10+
import { Icon } from "@calcom/ui/components/icon";
1111

1212
const PaymentStatusView = () => {
1313
const { t } = useLocale();
1414
const router = useRouter();
1515
const searchParams = useSearchParams();
1616
const paymentStatus = searchParams?.get("paymentStatus");
1717
const paymentError = searchParams?.get("error");
18-
const { useOnboardingStore } = useOnboarding({
19-
step: "status",
20-
});
18+
const { useOnboardingStore } = useOnboarding();
2119
const [organizationCreated, setOrganizationCreated] = useState<boolean>(false);
2220
const { name } = useOnboardingStore();
2321

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
"use client";
2+
3+
import { useRouter, useSearchParams } from "next/navigation";
4+
import { useEffect } from "react";
5+
6+
import { useOnboarding } from "@calcom/features/ee/organizations/lib/onboardingStore";
7+
import { useLocale } from "@calcom/lib/hooks/useLocale";
8+
import { Alert } from "@calcom/ui/components/alert";
9+
import { WizardLayout } from "@calcom/ui/components/layout";
10+
import { SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
11+
12+
export const LayoutWrapper = ({ children }: { children: React.ReactNode }) => {
13+
return (
14+
<WizardLayout currentStep={1} maxSteps={5}>
15+
{children}
16+
</WizardLayout>
17+
);
18+
};
19+
20+
const ResumeOnboardingView = () => {
21+
const { t } = useLocale();
22+
const router = useRouter();
23+
const searchParams = useSearchParams();
24+
const onboardingIdParam = searchParams?.get("onboardingId");
25+
26+
const { dbOnboarding, isLoadingOrgOnboarding, useOnboardingStore } = useOnboarding();
27+
const { reset } = useOnboardingStore();
28+
29+
useEffect(() => {
30+
if (isLoadingOrgOnboarding) {
31+
return;
32+
}
33+
34+
if (!onboardingIdParam) {
35+
router.push("/settings/organizations/new");
36+
return;
37+
}
38+
39+
if (!dbOnboarding) {
40+
return;
41+
}
42+
43+
if (dbOnboarding.isComplete) {
44+
router.push("/settings/organizations");
45+
return;
46+
}
47+
48+
// Load onboarding data into store
49+
reset({
50+
onboardingId: dbOnboarding.id,
51+
name: dbOnboarding.name,
52+
slug: dbOnboarding.slug,
53+
orgOwnerEmail: dbOnboarding.orgOwnerEmail,
54+
billingPeriod: dbOnboarding.billingPeriod,
55+
seats: dbOnboarding.seats,
56+
pricePerSeat: dbOnboarding.pricePerSeat,
57+
logo: dbOnboarding.logo,
58+
bio: dbOnboarding.bio,
59+
brandColor: dbOnboarding.brandColor,
60+
bannerUrl: dbOnboarding.bannerUrl,
61+
});
62+
63+
// Redirect to next step (About page)
64+
router.push("/settings/organizations/new/about");
65+
}, [dbOnboarding, isLoadingOrgOnboarding, onboardingIdParam, reset, router]);
66+
67+
if (isLoadingOrgOnboarding) {
68+
return (
69+
<SkeletonContainer className="space-y-4">
70+
<SkeletonText className="h-8 w-full" />
71+
<SkeletonText className="h-4 w-3/4" />
72+
<SkeletonText className="h-4 w-1/2" />
73+
</SkeletonContainer>
74+
);
75+
}
76+
77+
if (!onboardingIdParam) {
78+
return (
79+
<Alert
80+
data-testid="error"
81+
severity="error"
82+
title={t("error")}
83+
message={t("no_onboarding_id_provided")}
84+
/>
85+
);
86+
}
87+
88+
if (!dbOnboarding) {
89+
return (
90+
<Alert data-testid="error" severity="error" title={t("error")} message={t("onboarding_not_found")} />
91+
);
92+
}
93+
94+
if (dbOnboarding.isComplete) {
95+
return (
96+
<Alert
97+
data-testid="error"
98+
severity="info"
99+
title={t("onboarding_already_complete")}
100+
message={t("onboarding_already_complete_description")}
101+
/>
102+
);
103+
}
104+
105+
// Loading state while redirecting
106+
return (
107+
<SkeletonContainer className="space-y-4">
108+
<SkeletonText className="h-8 w-full" />
109+
<SkeletonText className="h-4 w-3/4" />
110+
</SkeletonContainer>
111+
);
112+
};
113+
114+
export default ResumeOnboardingView;

0 commit comments

Comments
 (0)