Files
calendar/apps/web/modules/onboarding/hooks/useCreateTeam.ts
T
sean-brydonGitHubClaude Opus 4.6Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
d80ee83da7 feat: redesign team creation flow to match onboarding (#27698)
* Revert "Revert "feat: redesign team creation flow to match onboarding-v3 design (#26733)""

This re-applies the team creation redesign from PR #26733 which was
previously reverted. Cherry-picked from 2540423ba3.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: check org/username slug when in org context

* fix: use expect(page).toHaveURL() instead of page.waitForURL() in team e2e test

Co-Authored-By: unknown <>

* fix: redirect URL

* fix: flag import

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-06 13:19:39 +00:00

141 lines
4.7 KiB
TypeScript

import { useFlagMap } from "@calcom/features/flags/context/provider";
import { CreationSource, MembershipRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import type { OnboardingState } from "../store/onboarding-store";
import { useOnboardingStore } from "../store/onboarding-store";
type UseCreateTeamOptions = {
redirectBasePath?: string;
skipRedirectAfterInvite?: boolean;
};
export function useCreateTeam(options: UseCreateTeamOptions = {}) {
const { redirectBasePath = "/onboarding/teams", skipRedirectAfterInvite = false } = options;
const router = useRouter();
const [isSubmitting, setIsSubmitting] = useState(false);
const flags = useFlagMap();
const { setTeamId, teamId } = useOnboardingStore();
const createTeamMutation = trpc.viewer.teams.create.useMutation();
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation();
const createTeam = async (store?: OnboardingState) => {
setIsSubmitting(true);
try {
// Get the latest state from the store to ensure we have the most up-to-date values
const currentStore = store || useOnboardingStore.getState();
const { teamDetails, teamBrand } = currentStore;
// Validate team details - if empty, redirect back to team details step
if (!teamDetails.name || !teamDetails.name.trim() || !teamDetails.slug || !teamDetails.slug.trim()) {
router.push(`${redirectBasePath}/details`);
setIsSubmitting(false);
return;
}
// Create the team
const result = await createTeamMutation.mutateAsync({
name: teamDetails.name,
slug: teamDetails.slug,
bio: teamDetails.bio,
logo: teamBrand.logo,
isOnboarding: true,
});
// If there's a checkout URL, redirect to Stripe payment
if (result.url && !result.team) {
window.location.href = result.url;
return;
}
if (result.team) {
// Store the teamId and redirect to invite flow after team creation
setTeamId(result.team.id);
router.push(`${redirectBasePath}/invite/email?teamId=${result.team.id}`);
}
} catch (error) {
console.error("Failed to create team:", error);
throw error;
} finally {
setIsSubmitting(false);
}
};
const inviteMembers = async (
invites: Array<{ email: string; role: "MEMBER" | "ADMIN" }>,
language: string
) => {
if (!teamId) {
throw new Error("Team ID is required to invite members");
}
setIsSubmitting(true);
try {
// Filter and validate invites
const validInvites = invites.filter((invite) => invite.email && invite.email.trim().length > 0);
if (validInvites.length === 0) {
throw new Error("At least one valid email address is required");
}
// Group invites by role and send separate requests for each role
// This is necessary because the schema validation expects array of strings when using bulk invites
const invitesByRole = validInvites.reduce(
(acc, invite) => {
const role = invite.role === "ADMIN" ? MembershipRole.ADMIN : MembershipRole.MEMBER;
if (!acc[role]) {
acc[role] = [];
}
acc[role].push(invite.email.trim().toLowerCase());
return acc;
},
{} as Record<MembershipRole, string[]>
);
// Send invites for each role group
await Promise.all(
Object.entries(invitesByRole).map(([role, emails]) =>
inviteMemberMutation.mutateAsync({
teamId,
usernameOrEmail: emails, // Array of strings, not objects
role: role as MembershipRole,
language,
creationSource: CreationSource.WEBAPP,
})
)
);
// Redirect to personal settings after successful invite (unless caller handles its own redirect)
if (!skipRedirectAfterInvite) {
const gettingStartedPath = flags["onboarding-v3"]
? "/onboarding/personal/settings?fromTeamOnboarding=true"
: "/getting-started";
router.replace(gettingStartedPath);
}
} catch (error) {
console.error("Failed to invite members:", error);
// Extract error message from TRPC error
if (error && typeof error === "object" && "message" in error) {
throw new Error(error.message as string);
}
if (error instanceof Error) {
throw error;
}
throw new Error("Failed to invite members. Please try again.");
} finally {
setIsSubmitting(false);
}
};
return {
createTeam,
inviteMembers,
isSubmitting,
error: createTeamMutation.error || inviteMemberMutation.error,
};
}