fix: add badge for disabled workflow actions and templates in trial mode (#19675)

* add badge for trial mode

* fix tooltip issue

* fix typo

* add inactive team plan badge

* change to one salesforce request

* clean up

* fix type error

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
This commit is contained in:
Carina Wollendorfer
2025-03-10 15:34:29 +01:00
committed by GitHub
co-authored by CarinaWolli Bailey Pumfleet
parent bb36807689
commit f2ca2bdc83
10 changed files with 61 additions and 33 deletions
@@ -3001,6 +3001,10 @@
"ooo_team_redirect_infinite_not_allowed": "There is already a reverse forwarding between these users.",
"previous_ooo_empty_title": "No previous OOO found",
"previous": "Previous",
"trial_mode": "Trial mode",
"inactive_team_plan": "Inactive team plan",
"inactive_team_plan_description": "Your team plan is inactive. Check your subcription or reach out to customer support",
"limited_access_trial_mode": "Limited access during trial. Feature available after trial ends.",
"uid": "UID",
"link": "Link",
"tip_username_plus": "Tip: You can a '+' between usernames: cal.com/anna+brian to make a dynamic group meeting",
@@ -1,3 +1,5 @@
import type Stripe from "stripe";
export interface BillingService {
checkoutSessionIsPaid(paymentId: string): Promise<boolean>;
handleSubscriptionCancel(subscriptionId: string): Promise<void>;
@@ -7,5 +9,5 @@ export interface BillingService {
subscriptionItemId: string;
membershipCount: number;
}): Promise<void>;
checkIfTeamHasActivePlan(subscriptionId: string): Promise<boolean>;
getSubscriptionStatus(subscriptionId: string): Promise<Stripe.Subscription.Status | null>;
}
@@ -30,10 +30,10 @@ export class StripeBillingService implements BillingService {
const checkoutSession = await this.stripe.checkout.sessions.retrieve(paymentId);
return checkoutSession.payment_status === "paid";
}
async checkIfTeamHasActivePlan(subscriptionId: string) {
async getSubscriptionStatus(subscriptionId: string) {
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId);
if (!subscription || !subscription.status) return false;
if (!subscription || !subscription.status) return null;
return subscription.status === "active" || subscription.status === "past_due";
return subscription.status;
}
}
@@ -161,10 +161,10 @@ export class InternalTeamBilling implements TeamBilling {
paymentRequired: false,
};
}
/** Used to check if the current team plan is active */
async checkIfTeamHasActivePlan() {
/** Returns the subscription status (active, past_due, trialing, ...) */
async getSubscriptionStatus() {
const { subscriptionId } = this.team.metadata;
if (!subscriptionId) return false;
return await billing.checkIfTeamHasActivePlan(subscriptionId);
if (!subscriptionId) return null;
return await billing.getSubscriptionStatus(subscriptionId);
}
}
+3 -3
View File
@@ -39,12 +39,12 @@ export function useHasEnterprisePlan() {
return { isPending, hasTeamPlan: hasTeamPlan?.hasTeamPlan };
}
export function useHasActiveTeamPlan(teamId?: number) {
if (IS_SELF_HOSTED) return { isPending: false, hasActiveTeamPlan: true };
export function useHasActiveTeamPlan() {
if (IS_SELF_HOSTED) return { isPending: false, hasActiveTeamPlan: true, isTrial: false };
const { data, isPending } = trpc.viewer.teams.hasActiveTeamPlan.useQuery();
return { isPending, hasActiveTeamPlan: !!data };
return { isPending, hasActiveTeamPlan: !!data?.isActive, isTrial: !!data?.isTrial };
}
export default useHasPaidPlan;
@@ -10,7 +10,8 @@ type HasActiveTeamPlanOptions = {
};
export const hasActiveTeamPlanHandler = async ({ ctx }: HasActiveTeamPlanOptions) => {
if (IS_SELF_HOSTED) return true;
if (IS_SELF_HOSTED) return { isActive: true, isTrial: false };
const teams = await prisma.team.findMany({
where: {
members: {
@@ -22,18 +23,23 @@ export const hasActiveTeamPlanHandler = async ({ ctx }: HasActiveTeamPlanOptions
},
});
if (!teams.length) return false;
if (!teams.length) return { isActive: false, isTrial: false };
let isTrial = false;
// check if user has at least on membership with an active plan
for (const team of teams) {
const teamBillingService = new InternalTeamBilling(team);
const isPlanActive = await teamBillingService.checkIfTeamHasActivePlan();
if (isPlanActive) {
return true;
const subscriptionStatus = await teamBillingService.getSubscriptionStatus();
if (subscriptionStatus === "active" || subscriptionStatus === "past_due") {
return { isActive: true, isTrial: false };
}
if (subscriptionStatus === "trialing") {
isTrial = true;
}
}
return false;
return { isActive: false, isTrial };
};
export default hasActiveTeamPlanHandler;
@@ -20,9 +20,9 @@ export const getWorkflowActionOptionsHandler = async ({ ctx }: GetWorkflowAction
const isCurrentUsernamePremium =
user && hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false;
let isTeamsPlan = false;
let teamsPlan = { isActive: false, isTrial: false };
if (!isCurrentUsernamePremium) {
isTeamsPlan = await hasActiveTeamPlanHandler({ ctx });
teamsPlan = await hasActiveTeamPlanHandler({ ctx });
}
const hasOrgsPlan = !!user.profile?.organizationId;
@@ -30,7 +30,7 @@ export const getWorkflowActionOptionsHandler = async ({ ctx }: GetWorkflowAction
const t = await getTranslation(ctx.user.locale, "common");
return getWorkflowActionOptions(
t,
IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan,
IS_SELF_HOSTED || isCurrentUsernamePremium || teamsPlan.isActive,
IS_SELF_HOSTED || hasOrgsPlan
);
};
@@ -80,14 +80,11 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
const isCurrentUsernamePremium = hasKeyInMetadata(user, "isPremium") ? !!user.metadata.isPremium : false;
let isTeamsPlan = false;
let teamsPlan = { isActive: false, isTrial: false };
if (!isCurrentUsernamePremium) {
isTeamsPlan = await hasActiveTeamPlanHandler({
ctx,
});
teamsPlan = await hasActiveTeamPlanHandler({ ctx });
}
const hasPaidPlan = IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan;
const hasPaidPlan = IS_SELF_HOSTED || isCurrentUsernamePremium || teamsPlan.isActive;
let newActiveOn: number[] = [];
let removedActiveOnIds: number[] = [];
@@ -1,21 +1,36 @@
import Link from "next/link";
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useHasPaidPlan, useHasActiveTeamPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Tooltip } from "../tooltip";
import { Badge } from "./Badge";
export const UpgradeTeamsBadge = function UpgradeTeamsBadge() {
export const UpgradeTeamsBadge = function UpgradeTeamsBadge({
checkForActiveStatus,
}: {
checkForActiveStatus?: boolean;
}) {
const { t } = useLocale();
const { hasPaidPlan } = useHasPaidPlan();
const { hasActiveTeamPlan, isTrial } = useHasActiveTeamPlan();
if (hasPaidPlan) return null;
if (hasPaidPlan) {
if (!checkForActiveStatus || hasActiveTeamPlan) return null;
}
const badgeString = isTrial ? t("trial_mode") : hasPaidPlan ? t("inactive_team_plan") : t("upgrade");
const tooltipString = isTrial
? t("limited_access_trial_mode")
: hasPaidPlan
? t("inactive_team_plan_description")
: t("upgrade_to_enable_feature");
return (
<Tooltip content={t("upgrade_to_enable_feature")}>
<Link href="/teams">
<Badge variant="gray">{t("upgrade")}</Badge>
<Tooltip content={tooltipString}>
<Link href={!hasPaidPlan ? "/teams" : ""}>
<Badge variant="gray">{badgeString}</Badge>
</Link>
</Tooltip>
);
@@ -47,7 +47,11 @@ export const OptionComponent = <
<span className="mr-auto" data-testid={`select-option-${(props as unknown as ExtendedOption).value}`}>
{props.label || <>&nbsp;</>}
</span>
{(props.data as unknown as ExtendedOption).needsTeamsUpgrade ? <UpgradeTeamsBadge /> : <></>}
{(props.data as unknown as ExtendedOption).needsTeamsUpgrade ? (
<UpgradeTeamsBadge checkForActiveStatus={true} />
) : (
<></>
)}
{props.isSelected && <Icon name="check" className="ml-2 h-4 w-4" />}
</div>
</reactSelectComponents.Option>