Skip to content

Commit 44104e1

Browse files
Amit91848cubic-dev-ai[bot]Udit-takkar
authored
feat: gated features modal (calcom#25296)
* feat: Modal for gated features * use zustand * refactor * Apply suggestions from code review Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * Update packages/ui/components/card/Card.tsx Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * dismiss button fix and url fix * fix: broken redirect to teams * fix: type check and review comments * add learn more tracking back --------- Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
1 parent 191dcc4 commit 44104e1

7 files changed

Lines changed: 354 additions & 167 deletions

File tree

apps/web/public/gated-features/roles_and_permissions.svg

Lines changed: 59 additions & 0 deletions
Loading

apps/web/public/static/locales/en/common.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4093,6 +4093,11 @@
40934093
"onboarding_teams_browser_view_no_teams_title": "No teams yet",
40944094
"onboarding_teams_browser_view_no_teams_description": "Add teams to get started",
40954095
"book_now": "Book now",
4096+
"only_available_on_orgs_plan": "This feature is available on the Organization plan",
4097+
"upgrade_team_to_orgs_with_price": "Upgrade your team to get access to this and much more for $37 per month/user.",
4098+
"attribute_based_routing": "Attribute-based routing",
4099+
"compliance_check": "SOC2, HIPAA, ISO 27001 compliance check",
4100+
"and_more_features": "and more features...",
40964101
"view": "View",
40974102
"booking_response": "Booking Response",
40984103
"list_view": "List view",

packages/features/shell/DynamicModals.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { WelcomeToOrganizationsModal } from "@calcom/features/ee/organizations/components/WelcomeToOrganizationsModal";
44

55
import { WelcomeToCalcomModal } from "./components/WelcomeToCalcomModal";
6+
import { GatedFeaturesModal } from "./components/GatedFeaturesModal";
67

78
/**
89
* Container for all query-param driven modals that should appear globally across the app.
@@ -20,6 +21,7 @@ export function DynamicModals() {
2021
<WelcomeToOrganizationsModal />
2122
<WelcomeToCalcomModal />
2223
{/* Add more query-param driven modals here */}
24+
<GatedFeaturesModal />
2325
</>
2426
);
2527
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useLocale } from "@calcom/lib/hooks/useLocale";
2+
import { Dialog, DialogContent } from "@calcom/ui/components/dialog";
3+
import { Button } from "@calcom/ui/components/button";
4+
5+
import { useGatedFeaturesStore, GatedFeatures } from "../stores/gatedFeaturesStore";
6+
7+
type FeatureContent = {
8+
badgeText: string;
9+
title: string;
10+
image: string;
11+
description: string;
12+
features: string[];
13+
learnMoreUrl: string;
14+
ctaUrl: string;
15+
};
16+
17+
const content: Record<GatedFeatures, FeatureContent> = {
18+
roles_and_permissions: {
19+
badgeText: "roles_and_permissions",
20+
title: "only_available_on_orgs_plan",
21+
image: "/gated-features/roles_and_permissions.svg",
22+
description: "upgrade_team_to_orgs_with_price",
23+
features: [
24+
"1_parent_team_unlimited_subteams",
25+
"attribute_based_routing",
26+
"compliance_check",
27+
"and_more_features",
28+
],
29+
learnMoreUrl: "https://cal.com/enterprise",
30+
ctaUrl: "/settings/organizations/new",
31+
},
32+
};
33+
34+
export function GatedFeaturesModal() {
35+
const { t } = useLocale();
36+
const isOpen = useGatedFeaturesStore((state) => state.isOpen);
37+
const activeFeature = useGatedFeaturesStore((state) => state.activeFeature);
38+
const close = useGatedFeaturesStore((state) => state.close);
39+
40+
const data = activeFeature ? content[activeFeature] : null;
41+
42+
if (!data) {
43+
return null;
44+
}
45+
46+
const { badgeText, description, image, title, features, learnMoreUrl, ctaUrl } = data;
47+
48+
return (
49+
<Dialog open={isOpen} onOpenChange={(open) => !open && close()}>
50+
<DialogContent className="p-0!" style={{ maxWidth: "400px" }}>
51+
<div className="p-6">
52+
<img src={image} alt={t(title)} className="w-full h-36 mb-5" />
53+
<div className="p-1 text-xs bg-emphasis rounded-md w-fit mb-3 leading-3">{t(badgeText)}</div>
54+
55+
<div className="mb-5 flex flex-col gap-2">
56+
<div className="text-xl font-semibold text-emphasis leading-6">{t(title)}</div>
57+
<div className="text-sm leading-5 font-normal text-default">{t(description)}</div>
58+
</div>
59+
<div className="flex flex-col gap-1 ml-6">
60+
<ul className="list-disc">
61+
{features.map((feature) => (
62+
<li className="text-sm text-default font-medium" key={feature}>{t(feature)}</li>
63+
))}
64+
</ul>
65+
</div>
66+
</div>
67+
<div className="w-full bg-muted border-subtle flex items-center justify-between rounded-b-2xl border-t px-6 py-5">
68+
<Button color="minimal" onClick={close}>{t("dismiss")}</Button>
69+
<div className="flex gap-2">
70+
<Button color="secondary" target="_blank" rel="noopener noreferrer" href={learnMoreUrl}>{t("learn_more")}</Button>
71+
<Button color="primary" href={ctaUrl}>{t("upgrade")}</Button>
72+
</div>
73+
</div>
74+
</DialogContent>
75+
</Dialog>
76+
);
77+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { create } from "zustand";
2+
3+
export enum GatedFeatures {
4+
RolesAndPermissions = "roles_and_permissions",
5+
}
6+
7+
type GatedFeaturesStore = {
8+
isOpen: boolean;
9+
activeFeature: GatedFeatures | null;
10+
open: (feature: GatedFeatures) => void;
11+
close: () => void;
12+
};
13+
14+
export const useGatedFeaturesStore = create<GatedFeaturesStore>((set) => ({
15+
isOpen: false,
16+
activeFeature: null,
17+
open: (feature) => set({ isOpen: true, activeFeature: feature }),
18+
close: () => set({ isOpen: false, activeFeature: null }),
19+
}));

0 commit comments

Comments
 (0)