Files
calendar/apps/web/modules/billing/components/UpgradePlanDialog.tsx
T

304 lines
11 KiB
TypeScript

"use client";
import { BILLING_PLANS, BILLING_PRICING } from "@calcom/features/ee/billing/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "@calcom/ui/components/icon";
import { Alert, AlertDescription, AlertTitle } from "@coss/ui/components/alert";
import { Badge } from "@coss/ui/components/badge";
import { Button } from "@coss/ui/components/button";
import { Card, CardPanel } from "@coss/ui/components/card";
import {
Dialog,
DialogClose,
DialogHeader,
DialogPanel,
DialogPopup,
DialogTitle,
DialogTrigger,
} from "@coss/ui/components/dialog";
import { Toggle, ToggleGroup } from "@coss/ui/components/toggle-group";
import Link from "next/link";
import posthog from "posthog-js";
import { useState } from "react";
type BillingPeriodToggle = "annual" | "monthly";
interface PlanFeature {
text: string;
}
interface PlanColumnProps {
name: string;
badge?: string;
price: string;
priceSubtext: string;
description: string;
features: PlanFeature[];
buttonText: string;
buttonHref: string;
buttonTarget?: string;
primaryButton?: boolean;
onCtaClick?: () => void;
}
function PlanColumn({
name,
badge,
price,
priceSubtext,
description,
features,
buttonText,
buttonHref,
buttonTarget,
primaryButton,
onCtaClick,
}: PlanColumnProps): JSX.Element {
return (
<Card className="flex-1 gap-0 rounded-xl border-subtle p-4 py-0">
<CardPanel className="px-0">
<div className="flex items-center gap-2">
<h3 className="font-medium text-sm text-emphasis">{name}</h3>
{badge && (
<Badge variant="outline" className="rounded-lg py-0.5 px-2 h-fit!">
{badge}
</Badge>
)}
</div>
<p className="font-cal mt-2 leading-none font-semibold text-2xl text-emphasis">{price}</p>
<p className="mt-2 leading-none font-medium text-sm text-subtle h-4">{priceSubtext}</p>
<Button
className="mt-4 w-full"
variant={primaryButton ? "default" : "outline"}
onClick={onCtaClick}
render={
<Link
href={buttonHref}
target={buttonTarget}
rel={buttonTarget === "_blank" ? "noopener noreferrer" : undefined}
/>
}>
<Icon name="circle-arrow-up" />
<span>{buttonText}</span>
</Button>
<p className="mt-4 text-sm text-subtle">{description}</p>
<ul className="mt-3 space-y-2">
{features.map((feature) => (
<li key={feature.text} className="flex items-start gap-2 text-sm">
<Icon name="dot" className="relative top-0.5 h-4 w-4 shrink-0 text-default" />
<span className="text-default">{feature.text}</span>
</li>
))}
</ul>
</CardPanel>
</Card>
);
}
export type UpgradePlanDialogProps = {
tracking: string;
target: "team" | "organization";
info?: {
title: string;
description: string;
};
children: React.ReactNode;
};
export function UpgradePlanDialog({ tracking, target, info, children }: UpgradePlanDialogProps): JSX.Element {
const { t } = useLocale();
const [billingPeriod, setBillingPeriod] = useState<"annual" | "monthly">("annual");
const { data: activeTeamPlan } = trpc.viewer.teams.hasActiveTeamPlan.useQuery();
const formatCents = (cents: number) => {
const dollars = cents / 100;
return `$${cents % 100 === 0 ? dollars : dollars.toFixed(2)}`;
};
const teamPrice = formatCents(BILLING_PRICING[BILLING_PLANS.TEAMS][billingPeriod]);
const orgPrice = formatCents(BILLING_PRICING[BILLING_PLANS.ORGANIZATIONS][billingPeriod]);
const currentPlanPricingKey = activeTeamPlan?.billingPeriod === "ANNUALLY" ? "annual" : "monthly";
const currentTeamPrice = formatCents(BILLING_PRICING[BILLING_PLANS.TEAMS][currentPlanPricingKey]);
const bpParam = billingPeriod === "annual" ? "a" : "m";
const teamHref = `/settings/teams/new?bp=${bpParam}`;
const organizationHref = `/onboarding/organization/details?migrate=true&bp=${bpParam}`;
const teamFeatures: PlanFeature[] = [
{ text: t("upgrade_feature_round_robin") },
{ text: t("upgrade_feature_collective_events") },
{ text: t("routing_forms") },
{ text: t("upgrade_feature_workflows") },
{ text: t("upgrade_feature_insights") },
{ text: t("upgrade_feature_remove_branding") },
];
const orgFeatures: PlanFeature[] = [
{ text: t("upgrade_feature_everything_in_team") },
{ text: t("unlimited_teams") },
{ text: t("upgrade_feature_verified_domain") },
{ text: t("upgrade_feature_directory_sync") },
{ text: t("upgrade_feature_sso") },
{ text: t("upgrade_feature_admin_panel") },
];
const enterpriseFeatures: PlanFeature[] = [
{ text: t("upgrade_feature_everything_in_org") },
{ text: t("upgrade_feature_dedicated_support") },
{ text: t("upgrade_feature_custom_sla") },
{ text: t("upgrade_feature_custom_integrations") },
{ text: t("upgrade_feature_compliance") },
];
return (
<Dialog>
<DialogTrigger render={children as React.ReactElement} />
<DialogPopup className="max-w-3xl" showCloseButton={false} bottomStickOnMobile={false}>
<DialogHeader>
<div className="flex flex-wrap items-center justify-between gap-2">
<DialogTitle>{t("upgrade_dialog_title")}</DialogTitle>
<div className="flex items-center gap-1 max-sm:order-last">
<ToggleGroup
value={[billingPeriod]}
onValueChange={(value): void => {
if (value.length > 0) {
const newPeriod = value[0] as BillingPeriodToggle;
setBillingPeriod(newPeriod);
posthog.capture("upgrade_plan_dialog_billing_period_changed", {
source: tracking,
target,
billingPeriod: newPeriod,
});
}
}}
className="rounded-lg bg-muted p-1"
size="sm">
<Toggle
value="annual"
className="gap-1 rounded-md data-pressed:bg-default data-pressed:shadow-sm">
{t("upgrade_billing_annual")}
<Badge variant="info" size="sm">
{t("discount_25")}
</Badge>
</Toggle>
<Toggle
value="monthly"
className="ml-1 rounded-md data-pressed:bg-default data-pressed:shadow-sm">
{t("monthly")}
</Toggle>
</ToggleGroup>
<DialogClose render={<Button size="icon" variant="ghost" />}>
<Icon name="x" />
</DialogClose>
</div>
{info && (
<Alert variant="info" className="w-full">
<AlertTitle>{info.title}</AlertTitle>
<AlertDescription>{info.description}</AlertDescription>
</Alert>
)}
</div>
</DialogHeader>
<DialogPanel>
<div className="mt-3 overflow-x-auto">
<div className="flex gap-2 sm:gap-4 max-sm:grid max-sm:grid-flow-col max-sm:auto-cols-fr max-sm:w-max">
{target === "team" && (
<PlanColumn
name={t("team")}
badge={t("upgrade_badge_free_trial")}
price={teamPrice}
priceSubtext={t("upgrade_price_per_month_user")}
description={t("upgrade_plan_team_tagline")}
features={teamFeatures}
buttonText={t("upgrade_cta_teams")}
buttonHref={teamHref}
primaryButton={target === "team"}
onCtaClick={() =>
posthog.capture("upgrade_plan_dialog_cta_clicked", {
source: tracking,
plan: "team",
target,
billingPeriod,
})
}
/>
)}
<PlanColumn
name={t("organization")}
badge={t("upgrade_badge_free_trial")}
price={orgPrice}
priceSubtext={t("upgrade_price_per_month_user")}
description={t("upgrade_plan_org_tagline")}
features={orgFeatures}
buttonText={t("upgrade_cta_orgs")}
buttonHref={organizationHref}
primaryButton={target === "organization"}
onCtaClick={() =>
posthog.capture("upgrade_plan_dialog_cta_clicked", {
source: tracking,
plan: "organization",
target,
billingPeriod,
})
}
/>
<PlanColumn
name={t("enterprise")}
price={t("custom")}
priceSubtext=""
description={t("upgrade_plan_enterprise_tagline")}
features={enterpriseFeatures}
buttonText={t("upgrade_cta_enterprise")}
buttonHref="https://cal.com/sales"
buttonTarget="_blank"
onCtaClick={() =>
posthog.capture("upgrade_plan_dialog_cta_clicked", {
source: tracking,
plan: "enterprise",
target,
billingPeriod,
})
}
/>
</div>
</div>
<Card className="bg-muted mt-4 py-3 px-4 flex-row justify-between items-center">
{target === "team" && (
<div>
<p className="font-medium text-sm text-emphasis">{t("individual")}</p>
<p className="font-cal mt-1 font-semibold text-emphasis text-2xl leading-none">{t("free")}</p>
</div>
)}
{target === "organization" && (
<div>
<p className="font-medium text-sm text-emphasis">{t("team")}</p>
<p className="mt-1">
<span className="font-cal font-semibold text-emphasis text-2xl leading-none">
{currentTeamPrice}
</span>
<span className="ml-1 leading-none font-medium text-sm text-subtle">
{t("upgrade_price_per_month_user")}
</span>
</p>
</div>
)}
<Badge variant="outline" size="lg" className="rounded-lg py-1.5 px-2.5 h-fit!">
<span className="text-emphasis opacity-50 leading-none">{t("upgrade_badge_current_plan")}</span>
</Badge>
</Card>
</DialogPanel>
</DialogPopup>
</Dialog>
);
}