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>
This commit is contained in:
co-authored by
coderabbitai[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
parent
1eb6ff3e65
commit
fa35cc5210
@@ -205,6 +205,7 @@ const AddNewTeamsFormChild = ({ teams }: { teams: { id: number; name: string; sl
|
||||
<TextField
|
||||
key={field.id}
|
||||
{...register(`teams.${index}.name`)}
|
||||
data-testid={`team.${index}.name`}
|
||||
label=""
|
||||
addOnClassname="bg-transparent p-0 border-l-0"
|
||||
className={index > 0 ? "mb-2" : ""}
|
||||
|
||||
@@ -34,28 +34,28 @@ const useOrgCreation = () => {
|
||||
const session = useSession();
|
||||
const utils = trpc.useUtils();
|
||||
const [serverErrorMessage, setServerErrorMessage] = useState("");
|
||||
const { useOnboardingStore, isBillingEnabled } = useOnboarding();
|
||||
const { useOnboardingStore } = useOnboarding();
|
||||
const { reset } = useOnboardingStore();
|
||||
|
||||
const checkoutMutation = trpc.viewer.organizations.createWithPaymentIntent.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data.checkoutUrl) {
|
||||
window.location.href = data.checkoutUrl;
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
setServerErrorMessage(t(error.message));
|
||||
},
|
||||
});
|
||||
|
||||
const createOrgMutation = trpc.viewer.organizations.createSelfHosted.useMutation({
|
||||
// Single mutation for all flows (billing, self-hosted, admin)
|
||||
const intentToCreateOrgMutation = trpc.viewer.organizations.intentToCreateOrg.useMutation({
|
||||
onSuccess: async (data) => {
|
||||
if (data.organization) {
|
||||
// Invalidate the organizations query to ensure fresh data on the next page
|
||||
reset({
|
||||
onboardingId: data.organizationOnboardingId,
|
||||
});
|
||||
|
||||
if (data.checkoutUrl) {
|
||||
// Billing enabled - redirect to Stripe
|
||||
window.location.href = data.checkoutUrl;
|
||||
} else if (data.organizationId) {
|
||||
// Self-hosted - org already created, redirect to organizations
|
||||
await utils.viewer.organizations.listCurrent.invalidate();
|
||||
await session.update();
|
||||
reset();
|
||||
window.location.href = `${window.location.origin}/settings/organizations/profile`;
|
||||
} else {
|
||||
// Unexpected state
|
||||
setServerErrorMessage("Unexpected response from server");
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
@@ -63,12 +63,10 @@ const useOrgCreation = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const mutationToUse = isBillingEnabled ? checkoutMutation : createOrgMutation;
|
||||
|
||||
return {
|
||||
mutation: mutationToUse,
|
||||
mutate: mutationToUse.mutate,
|
||||
isPending: mutationToUse.isPending,
|
||||
mutation: intentToCreateOrgMutation,
|
||||
mutate: intentToCreateOrgMutation.mutate,
|
||||
isPending: intentToCreateOrgMutation.isPending,
|
||||
errorMessage: serverErrorMessage,
|
||||
};
|
||||
};
|
||||
@@ -84,7 +82,13 @@ export const AddNewTeamMembersForm = () => {
|
||||
invitedMembers,
|
||||
logo,
|
||||
bio,
|
||||
onboardingId,
|
||||
name,
|
||||
slug,
|
||||
billingPeriod,
|
||||
seats,
|
||||
pricePerSeat,
|
||||
brandColor,
|
||||
bannerUrl,
|
||||
} = useOnboardingStore();
|
||||
const orgCreation = useOrgCreation();
|
||||
|
||||
@@ -155,7 +159,7 @@ export const AddNewTeamMembersForm = () => {
|
||||
placeholder="colleague@company.com"
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" StartIcon="plus" color="secondary">
|
||||
<Button type="submit" StartIcon="plus" color="secondary" data-testid="invite-new-member-button">
|
||||
{t("add")}
|
||||
</Button>
|
||||
</form>
|
||||
@@ -203,22 +207,31 @@ export const AddNewTeamMembersForm = () => {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 mt-6 flex items-center justify-end">
|
||||
<div className="mt-3 flex items-center justify-end">
|
||||
<Button
|
||||
data-testid="publish-button"
|
||||
onClick={() => {
|
||||
if (!onboardingId) {
|
||||
console.error("Org owner email and onboardingId are required", {
|
||||
orgOwnerEmail,
|
||||
onboardingId,
|
||||
});
|
||||
// Submit ALL data to intentToCreateOrg
|
||||
if (!name || !slug || !orgOwnerEmail) {
|
||||
console.error("Required fields missing", { name, slug, orgOwnerEmail });
|
||||
showToast(t("required_fields_missing"), "error");
|
||||
return;
|
||||
}
|
||||
orgCreation.mutation.mutate({
|
||||
|
||||
orgCreation.mutate({
|
||||
name,
|
||||
slug,
|
||||
orgOwnerEmail,
|
||||
seats,
|
||||
pricePerSeat,
|
||||
billingPeriod,
|
||||
creationSource: "WEBAPP" as const,
|
||||
logo,
|
||||
bio,
|
||||
brandColor,
|
||||
bannerUrl,
|
||||
teams,
|
||||
invitedMembers,
|
||||
onboardingId,
|
||||
});
|
||||
}}
|
||||
loading={orgCreation.isPending}>
|
||||
|
||||
@@ -6,8 +6,8 @@ import { useEffect, useState } from "react";
|
||||
import { useOnboarding } from "@calcom/features/ee/organizations/lib/onboardingStore";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
|
||||
const PaymentStatusView = () => {
|
||||
const { t } = useLocale();
|
||||
@@ -15,9 +15,7 @@ const PaymentStatusView = () => {
|
||||
const searchParams = useSearchParams();
|
||||
const paymentStatus = searchParams?.get("paymentStatus");
|
||||
const paymentError = searchParams?.get("error");
|
||||
const { useOnboardingStore } = useOnboarding({
|
||||
step: "status",
|
||||
});
|
||||
const { useOnboardingStore } = useOnboarding();
|
||||
const [organizationCreated, setOrganizationCreated] = useState<boolean>(false);
|
||||
const { name } = useOnboardingStore();
|
||||
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { useOnboarding } from "@calcom/features/ee/organizations/lib/onboardingStore";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert } from "@calcom/ui/components/alert";
|
||||
import { WizardLayout } from "@calcom/ui/components/layout";
|
||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui/components/skeleton";
|
||||
|
||||
export const LayoutWrapper = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<WizardLayout currentStep={1} maxSteps={5}>
|
||||
{children}
|
||||
</WizardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
const ResumeOnboardingView = () => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const onboardingIdParam = searchParams?.get("onboardingId");
|
||||
|
||||
const { dbOnboarding, isLoadingOrgOnboarding, useOnboardingStore } = useOnboarding();
|
||||
const { reset } = useOnboardingStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoadingOrgOnboarding) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!onboardingIdParam) {
|
||||
router.push("/settings/organizations/new");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!dbOnboarding) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dbOnboarding.isComplete) {
|
||||
router.push("/settings/organizations");
|
||||
return;
|
||||
}
|
||||
|
||||
// Load onboarding data into store
|
||||
reset({
|
||||
onboardingId: dbOnboarding.id,
|
||||
name: dbOnboarding.name,
|
||||
slug: dbOnboarding.slug,
|
||||
orgOwnerEmail: dbOnboarding.orgOwnerEmail,
|
||||
billingPeriod: dbOnboarding.billingPeriod,
|
||||
seats: dbOnboarding.seats,
|
||||
pricePerSeat: dbOnboarding.pricePerSeat,
|
||||
logo: dbOnboarding.logo,
|
||||
bio: dbOnboarding.bio,
|
||||
brandColor: dbOnboarding.brandColor,
|
||||
bannerUrl: dbOnboarding.bannerUrl,
|
||||
});
|
||||
|
||||
// Redirect to next step (About page)
|
||||
router.push("/settings/organizations/new/about");
|
||||
}, [dbOnboarding, isLoadingOrgOnboarding, onboardingIdParam, reset, router]);
|
||||
|
||||
if (isLoadingOrgOnboarding) {
|
||||
return (
|
||||
<SkeletonContainer className="space-y-4">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-4 w-3/4" />
|
||||
<SkeletonText className="h-4 w-1/2" />
|
||||
</SkeletonContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (!onboardingIdParam) {
|
||||
return (
|
||||
<Alert
|
||||
data-testid="error"
|
||||
severity="error"
|
||||
title={t("error")}
|
||||
message={t("no_onboarding_id_provided")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!dbOnboarding) {
|
||||
return (
|
||||
<Alert data-testid="error" severity="error" title={t("error")} message={t("onboarding_not_found")} />
|
||||
);
|
||||
}
|
||||
|
||||
if (dbOnboarding.isComplete) {
|
||||
return (
|
||||
<Alert
|
||||
data-testid="error"
|
||||
severity="info"
|
||||
title={t("onboarding_already_complete")}
|
||||
message={t("onboarding_already_complete_description")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Loading state while redirecting
|
||||
return (
|
||||
<SkeletonContainer className="space-y-4">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-4 w-3/4" />
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResumeOnboardingView;
|
||||
Reference in New Issue
Block a user