fix: onboarding - team invites (#27113)
This commit is contained in:
@@ -25,10 +25,10 @@ const ServerPage = async () => {
|
||||
return redirect("/auth/login");
|
||||
}
|
||||
|
||||
// If user has pending team invites, redirect them directly to personal onboarding
|
||||
// This handles the case where users sign up with an invite token and are redirected here
|
||||
const hasPendingInvite = await MembershipRepository.hasPendingInviteByUserId({ userId: session.user.id });
|
||||
if (hasPendingInvite) {
|
||||
// If user has any team membership (pending or accepted), redirect them directly to personal onboarding
|
||||
// This handles the case where users sign up with an invite token (membership is auto-accepted)
|
||||
const hasTeamMembership = await MembershipRepository.hasAnyTeamMembershipByUserId({ userId: session.user.id });
|
||||
if (hasTeamMembership) {
|
||||
return redirect("/onboarding/personal/settings");
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,12 @@ export function useTeamInvites() {
|
||||
return { isPending: listInvites.isPending, listInvites: listInvites.data };
|
||||
}
|
||||
|
||||
export function useHasTeamMembership() {
|
||||
const { data, isPending } = trpc.viewer.teams.hasTeamMembership.useQuery();
|
||||
|
||||
return { isPending, hasTeamMembership: data?.hasTeamMembership ?? false };
|
||||
}
|
||||
|
||||
export function useHasTeamPlan() {
|
||||
const { data: hasTeamPlan, isPending } = trpc.viewer.teams.hasTeamPlan.useQuery();
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { type IconName } from "@calcom/ui/components/icon";
|
||||
import { RadioAreaGroup } from "@calcom/ui/components/radio";
|
||||
import { useTeamInvites } from "@calcom/web/modules/billing/hooks/useHasPaidPlan";
|
||||
import { useHasTeamMembership } from "@calcom/web/modules/billing/hooks/useHasPaidPlan";
|
||||
|
||||
import { OnboardingCard } from "../components/OnboardingCard";
|
||||
import { OnboardingLayout } from "../components/OnboardingLayout";
|
||||
@@ -30,7 +30,7 @@ export const OnboardingView = ({ userEmail }: OnboardingViewProps) => {
|
||||
const { selectedPlan, setSelectedPlan, resetOnboardingPreservingPlan } = useOnboardingStore();
|
||||
const previousPlanRef = useRef<PlanType | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const { listInvites, isPending: isPendingInvites } = useTeamInvites();
|
||||
const { hasTeamMembership, isPending: isPendingMembership } = useHasTeamMembership();
|
||||
|
||||
// Reset onboarding data when visiting this page, but preserve the selected plan
|
||||
useEffect(() => {
|
||||
@@ -38,15 +38,16 @@ export const OnboardingView = ({ userEmail }: OnboardingViewProps) => {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// If user has pending team invites, redirect them directly to personal onboarding
|
||||
// If user has any team membership (pending or accepted), redirect them directly to personal onboarding
|
||||
// This handles the case where users sign up with an invite token (membership is auto-accepted)
|
||||
useEffect(() => {
|
||||
if (!isPendingInvites && listInvites && listInvites.length > 0) {
|
||||
if (!isPendingMembership && hasTeamMembership) {
|
||||
setSelectedPlan("personal");
|
||||
startTransition(() => {
|
||||
router.push("/onboarding/personal/settings");
|
||||
});
|
||||
}
|
||||
}, [isPendingInvites, listInvites, router, setSelectedPlan]);
|
||||
}, [isPendingMembership, hasTeamMembership, router, setSelectedPlan]);
|
||||
|
||||
// Plan order mapping for determining direction
|
||||
const planOrder: Record<PlanType, number> = {
|
||||
@@ -130,8 +131,8 @@ export const OnboardingView = ({ userEmail }: OnboardingViewProps) => {
|
||||
|
||||
const selectedPlanData = plans.find((plan) => plan.id === selectedPlan);
|
||||
|
||||
// Show loading state while checking for invites or if redirecting
|
||||
if (isPendingInvites || (listInvites && listInvites.length > 0)) {
|
||||
// Show loading state while checking for team membership or if redirecting
|
||||
if (isPendingMembership || hasTeamMembership) {
|
||||
return (
|
||||
<OnboardingLayout userEmail={userEmail}>
|
||||
<OnboardingCard title={t("loading")} subtitle="" />
|
||||
|
||||
@@ -64,9 +64,11 @@ export async function checkOnboardingRedirect(
|
||||
// Determine which onboarding path to use
|
||||
const onboardingV3Enabled = await featuresRepository.checkIfFeatureIsEnabledGlobally("onboarding-v3");
|
||||
|
||||
const hasPendingInvite = await MembershipRepository.hasPendingInviteByUserId({ userId });
|
||||
// Check for any team membership (pending or accepted) to handle users who signed up via invite token
|
||||
// When users sign up with an invite token, the membership is auto-accepted
|
||||
const hasTeamMembership = await MembershipRepository.hasAnyTeamMembershipByUserId({ userId });
|
||||
|
||||
if (hasPendingInvite && onboardingV3Enabled) {
|
||||
if (hasTeamMembership && onboardingV3Enabled) {
|
||||
return "/onboarding/personal/settings";
|
||||
}
|
||||
|
||||
|
||||
@@ -593,4 +593,24 @@ export class MembershipRepository {
|
||||
});
|
||||
return !!pendingInvite;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a user has any team membership (pending or accepted).
|
||||
* Used during onboarding to detect users who signed up via invite token,
|
||||
* where the membership is auto-accepted.
|
||||
*/
|
||||
static async hasAnyTeamMembershipByUserId({ userId }: { userId: number }): Promise<boolean> {
|
||||
const membership = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
team: {
|
||||
isOrganization: false,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
return !!membership;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,10 @@ export const viewerTeamsRouter = router({
|
||||
return handler(opts);
|
||||
}),
|
||||
hasTeamPlan,
|
||||
hasTeamMembership: authedProcedure.query(async (opts) => {
|
||||
const { default: handler } = await import("./hasTeamMembership.handler");
|
||||
return handler(opts);
|
||||
}),
|
||||
listInvites: authedProcedure.query(async (opts) => {
|
||||
const { default: handler } = await import("./listInvites.handler");
|
||||
return handler(opts);
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
|
||||
|
||||
type HasTeamMembershipOptions = {
|
||||
ctx: {
|
||||
user: { id: number };
|
||||
};
|
||||
};
|
||||
|
||||
export const hasTeamMembershipHandler = async ({ ctx }: HasTeamMembershipOptions) => {
|
||||
const userId = ctx.user.id;
|
||||
const hasTeamMembership = await MembershipRepository.hasAnyTeamMembershipByUserId({ userId });
|
||||
return { hasTeamMembership };
|
||||
};
|
||||
|
||||
export default hasTeamMembershipHandler;
|
||||
Reference in New Issue
Block a user