Skip to content

Commit b7340f7

Browse files
authored
feat: add upgrade banners for teams and organizations (calcom#27650)
1 parent c197de1 commit b7340f7

74 files changed

Lines changed: 1833 additions & 499 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.

apps/web/app/(use-page-wrapper)/(main-nav)/teams/page.tsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
1-
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
1+
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
2+
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
23
import type { PageProps as ServerPageProps } from "app/_types";
34
import { _generateMetadata, getTranslate } from "app/_utils";
5+
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
46
import { cookies, headers } from "next/headers";
57
import { redirect } from "next/navigation";
6-
7-
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
8-
9-
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
10-
118
import { ServerTeamsListing } from "./server-page";
129

1310
export const generateMetadata = async () =>
@@ -37,10 +34,14 @@ const ServerPage = async ({ searchParams: _searchParams }: ServerPageProps) => {
3734
}
3835

3936
const t = await getTranslate();
40-
const { Main, CTA } = await ServerTeamsListing({ searchParams, session });
37+
const { Main, CTA, showHeader } = await ServerTeamsListing({ searchParams, session });
4138

4239
return (
43-
<ShellMainAppDir CTA={CTA} heading={t("teams")} subtitle={t("create_manage_teams_collaborative")}>
40+
<ShellMainAppDir
41+
CTA={showHeader ? CTA : null}
42+
heading={showHeader ? t("teams") : undefined}
43+
subtitle={showHeader ? t("create_manage_teams_collaborative") : undefined}
44+
flexChildrenContainer={!showHeader}>
4445
{Main}
4546
</ShellMainAppDir>
4647
);

apps/web/app/(use-page-wrapper)/(main-nav)/teams/server-page.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
1-
import type { SearchParams } from "app/_types";
2-
import type { Session } from "next-auth";
3-
import { unstable_cache } from "next/cache";
4-
51
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
62
import { TeamService } from "@calcom/features/ee/teams/services/teamService";
73
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
84
import { ErrorWithCode } from "@calcom/lib/errors";
95
import prisma from "@calcom/prisma";
106
import { MembershipRole } from "@calcom/prisma/enums";
11-
7+
import type { SearchParams } from "app/_types";
8+
import { unstable_cache } from "next/cache";
9+
import type { Session } from "next-auth";
1210
import { TeamsListing } from "~/ee/teams/components/TeamsListing";
13-
1411
import { TeamsCTA } from "./CTA";
1512

1613
const getCachedTeams = unstable_cache(
@@ -31,7 +28,11 @@ export const ServerTeamsListing = async ({
3128
}: {
3229
searchParams: SearchParams;
3330
session: Session;
34-
}) => {
31+
}): Promise<{
32+
Main: JSX.Element;
33+
CTA: JSX.Element | null;
34+
showHeader: boolean;
35+
}> => {
3536
const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token;
3637
const autoAccept = Array.isArray(searchParams?.autoAccept)
3738
? searchParams.autoAccept[0]
@@ -60,6 +61,15 @@ export const ServerTeamsListing = async ({
6061
const userProfile = session?.user?.profile;
6162
const orgId = userProfile?.organizationId ?? session?.user.org?.id;
6263

64+
// Filter to get accepted non-organization teams (same logic as TeamsListing)
65+
const acceptedTeams = teams.filter((m) => m.accepted && !m.isOrganization);
66+
67+
// Check if user has a team plan (any accepted team with a slug)
68+
const hasTeamPlan = teams.some((team) => team.accepted === true && team.slug !== null);
69+
70+
// Show header unless we're showing the upgrade banner (no teams and no team plan)
71+
const showHeader = acceptedTeams.length > 0 || hasTeamPlan;
72+
6373
const permissionCheckService = new PermissionCheckService();
6474
const canCreateTeam = orgId
6575
? await permissionCheckService.checkPermission({
@@ -84,5 +94,6 @@ export const ServerTeamsListing = async ({
8494
/>
8595
),
8696
CTA: !orgId || canCreateTeam ? <TeamsCTA /> : null,
97+
showHeader,
8798
};
8899
};

apps/web/app/(use-page-wrapper)/apps/routing-forms/forms/[[...pages]]/Forms.tsx

Lines changed: 5 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@ import type { RoutingFormWithResponseCount } from "@calcom/app-store/routing-for
55
import { FilterResults } from "~/filters/components/FilterResults";
66
import { TeamsFilter } from "~/filters/components/TeamsFilter";
77
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
8-
import { WEBAPP_URL } from "@calcom/lib/constants";
98
import { useLocale } from "@calcom/lib/hooks/useLocale";
109
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
1110
import { MembershipRole } from "@calcom/prisma/enums";
1211
import { trpc } from "@calcom/trpc/react";
1312
import { ArrowButton } from "@calcom/ui/components/arrow-button";
1413
import { Badge } from "@calcom/ui/components/badge";
15-
import { Button } from "@calcom/ui/components/button";
1614
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
1715
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
1816
import { List, ListLinkItem } from "@calcom/ui/components/list";
@@ -26,24 +24,15 @@ import {
2624
FormActionsDropdown,
2725
FormActionsProvider,
2826
} from "@calcom/web/components/apps/routing-forms/FormActions";
29-
import {
30-
ChartBarIcon,
31-
CircleCheckIcon,
32-
DownloadIcon,
33-
FileTextIcon,
34-
MailIcon,
35-
ShuffleIcon,
36-
} from "@coss/ui/icons";
3727
import { useAutoAnimate } from "@formkit/auto-animate/react";
3828
import posthog from "posthog-js";
3929
import { useEffect, useState } from "react";
4030
import { useFormContext } from "react-hook-form";
41-
import { useHasPaidPlan } from "~/billing/hooks/useHasPaidPlan";
31+
import { useHasPaidPlan, useHasTeamPlan } from "~/billing/hooks/useHasPaidPlan";
4232
import LicenseRequired from "~/ee/common/components/LicenseRequired";
4333
import { CreateButtonWithTeamsList } from "~/ee/teams/components/createButton/CreateButtonWithTeamsList";
4434
import SkeletonLoaderTeamList from "~/ee/teams/components/SkeletonloaderTeamList";
4535
import { ShellMain } from "~/shell/Shell";
46-
import { UpgradeTip } from "~/shell/UpgradeTip";
4736

4837
function NewFormButton({ setNewFormDialogState }: { setNewFormDialogState: SetNewFormDialogState }) {
4938
const { t } = useLocale();
@@ -66,6 +55,7 @@ function NewFormButton({ setNewFormDialogState }: { setNewFormDialogState: SetNe
6655
export default function RoutingForms({ appUrl }: { appUrl: string }) {
6756
const { t } = useLocale();
6857
const { hasPaidPlan } = useHasPaidPlan();
58+
const { hasTeamPlan, isPending: isPendingTeamPlan } = useHasTeamPlan();
6959
const routerQuery = useRouterQuery();
7060
const hookForm = useFormContext<RoutingFormWithResponseCount>();
7161
const utils = trpc.useUtils();
@@ -95,38 +85,6 @@ export default function RoutingForms({ appUrl }: { appUrl: string }) {
9585
const [newFormDialogState, setNewFormDialogState] = useState<NewFormDialogState>(null);
9686

9787
const forms = queryRes.data?.filtered;
98-
const features = [
99-
{
100-
icon: <FileTextIcon className="h-5 w-5 text-orange-500" />,
101-
title: t("create_your_first_form"),
102-
description: t("create_your_first_form_description"),
103-
},
104-
{
105-
icon: <ShuffleIcon className="h-5 w-5 text-lime-500" />,
106-
title: t("create_your_first_route"),
107-
description: t("route_to_the_right_person"),
108-
},
109-
{
110-
icon: <ChartBarIcon className="h-5 w-5 text-blue-500" />,
111-
title: t("reporting"),
112-
description: t("reporting_feature"),
113-
},
114-
{
115-
icon: <CircleCheckIcon className="h-5 w-5 text-teal-500" />,
116-
title: t("test_routing_form"),
117-
description: t("test_preview_description"),
118-
},
119-
{
120-
icon: <MailIcon className="h-5 w-5 text-yellow-500" />,
121-
title: t("routing_forms_send_email_owner"),
122-
description: t("routing_forms_send_email_owner_description"),
123-
},
124-
{
125-
icon: <DownloadIcon className="h-5 w-5 text-violet-500" />,
126-
title: t("download_responses"),
127-
description: t("download_responses_description"),
128-
},
129-
];
13088

13189
async function moveRoutingForm(index: number, increment: 1 | -1) {
13290
const types = forms?.map((type) => {
@@ -162,25 +120,8 @@ export default function RoutingForms({ appUrl }: { appUrl: string }) {
162120
) : null
163121
}
164122
subtitle={t("routing_forms_description")}>
165-
<UpgradeTip
166-
plan="team"
167-
title={t("routing_that_grows_with_you")}
168-
description={t("routing_forms_upgrade_description")}
169-
features={features}
170-
background="/tips/routing-forms"
171-
isParentLoading={<SkeletonLoaderTeamList />}
172-
buttons={
173-
<div className="stack-y-2 rtl:space-x-reverse sm:space-x-2">
174-
<ButtonGroup>
175-
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
176-
{t("get_started")}
177-
</Button>
178-
<Button color="minimal" href="https://cal.com/routing" target="_blank">
179-
{t("learn_more")}
180-
</Button>
181-
</ButtonGroup>
182-
</div>
183-
}>
123+
{isPendingTeamPlan && <SkeletonLoaderTeamList />}
124+
{!isPendingTeamPlan && hasTeamPlan && (
184125
<FormActionsProvider
185126
appUrl={appUrl}
186127
newFormDialogState={newFormDialogState}
@@ -340,7 +281,7 @@ export default function RoutingForms({ appUrl }: { appUrl: string }) {
340281
</FilterResults>
341282
</div>
342283
</FormActionsProvider>
343-
</UpgradeTip>
284+
)}
344285
</ShellMain>
345286
</LicenseRequired>
346287
);

apps/web/app/(use-page-wrapper)/apps/routing-forms/forms/[[...pages]]/page.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
2+
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
3+
import { FullscreenUpgradeBannerForRoutingFormPage } from "@calcom/web/modules/billing/upgrade-banners/FullscreenUpgradeBannerForRoutingFormPage";
4+
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
15
import { _generateMetadata } from "app/_utils";
2-
6+
import { cookies, headers } from "next/headers";
37
import Forms from "./Forms";
48

59
export const generateMetadata = async ({ params }: { params: Promise<{ pages: string[] }> }) => {
@@ -15,6 +19,14 @@ export const generateMetadata = async ({ params }: { params: Promise<{ pages: st
1519
};
1620

1721
const ServerPage = async () => {
22+
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
23+
const userId = session?.user?.id;
24+
const hasTeamPlan = userId && (await MembershipRepository.hasAnyAcceptedMembershipByUserId(userId));
25+
26+
if (!hasTeamPlan) {
27+
return <FullscreenUpgradeBannerForRoutingFormPage />;
28+
}
29+
1830
return <Forms appUrl="/routing" />;
1931
};
2032

apps/web/app/(use-page-wrapper)/insights/call-history/page.tsx

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { _generateMetadata } from "app/_utils";
2-
1+
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
2+
import { _generateMetadata, getTranslate } from "app/_utils";
3+
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
34
import InsightsCallHistoryPage from "~/insights/views/insights-call-history-view";
5+
import Shell from "~/shell/Shell";
46

57
import { checkInsightsPagePermission } from "../checkInsightsPagePermission";
68

@@ -16,5 +18,16 @@ export const generateMetadata = async () =>
1618
export default async function Page() {
1719
await checkInsightsPagePermission();
1820

19-
return <InsightsCallHistoryPage />;
21+
const t = await getTranslate();
22+
23+
return (
24+
<Shell withoutMain={true}>
25+
<ShellMainAppDir
26+
heading={t("insights")}
27+
subtitle={t("insights_subtitle")}
28+
actions={<div className={`flex items-center gap-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
29+
<InsightsCallHistoryPage />
30+
</ShellMainAppDir>
31+
</Shell>
32+
);
2033
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { MembershipRepository } from "@calcom/features/membership/repositories/MembershipRepository";
2+
import { FullscreenUpgradeBannerForInsightsPage } from "@calcom/web/modules/billing/upgrade-banners/FullscreenUpgradeBannerForInsightsPage";
3+
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
4+
import Shell from "~/shell/Shell";
5+
6+
export async function getInsightsUpgradeBanner(userId: number) {
7+
const hasMembership = await MembershipRepository.hasAnyAcceptedMembershipByUserId(userId);
8+
9+
if (hasMembership) return null;
10+
11+
return (
12+
<Shell withoutMain={true}>
13+
<ShellMainAppDir>
14+
<FullscreenUpgradeBannerForInsightsPage />
15+
</ShellMainAppDir>
16+
</Shell>
17+
);
18+
}

apps/web/app/(use-page-wrapper)/insights/layout.tsx

Lines changed: 0 additions & 23 deletions
This file was deleted.
Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import { _generateMetadata } from "app/_utils";
2-
1+
import { CTA_CONTAINER_CLASS_NAME } from "@calcom/features/data-table/lib/utils";
2+
import { UserRepository } from "@calcom/features/users/repositories/UserRepository";
33
import prisma from "@calcom/prisma";
4-
4+
import { _generateMetadata, getTranslate } from "app/_utils";
5+
import { ShellMainAppDir } from "app/(use-page-wrapper)/(main-nav)/ShellMainAppDir";
56
import InsightsPage from "~/insights/views/insights-view";
7+
import Shell from "~/shell/Shell";
68

79
import { checkInsightsPagePermission } from "./checkInsightsPagePermission";
10+
import { getInsightsUpgradeBanner } from "./getInsightsUpgradeBanner";
811

912
export const generateMetadata = async () =>
1013
await _generateMetadata(
@@ -15,17 +18,26 @@ export const generateMetadata = async () =>
1518
"/insights"
1619
);
1720

18-
const ServerPage = async () => {
21+
export default async function Page() {
1922
const session = await checkInsightsPagePermission();
2023

21-
const { timeZone } = await prisma.user.findUniqueOrThrow({
22-
where: { id: session?.user.id ?? -1 },
23-
select: {
24-
timeZone: true,
25-
},
26-
});
24+
const banner = await getInsightsUpgradeBanner(session.user.id);
25+
if (banner) return banner;
2726

28-
return <InsightsPage timeZone={timeZone} />;
29-
};
27+
const t = await getTranslate();
3028

31-
export default ServerPage;
29+
const userRepository = new UserRepository(prisma);
30+
const user = await userRepository.getTimeZoneAndDefaultScheduleId({ userId: session.user.id });
31+
const timeZone = user?.timeZone ?? "UTC";
32+
33+
return (
34+
<Shell withoutMain={true}>
35+
<ShellMainAppDir
36+
heading={t("insights")}
37+
subtitle={t("insights_subtitle")}
38+
actions={<div className={`flex items-center gap-2 ${CTA_CONTAINER_CLASS_NAME}`} />}>
39+
<InsightsPage timeZone={timeZone} />
40+
</ShellMainAppDir>
41+
</Shell>
42+
);
43+
}

0 commit comments

Comments
 (0)