From f2ca2bdc8305c71d1158ff49bba94bc26dd1f705 Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Mon, 10 Mar 2025 15:34:29 +0100 Subject: [PATCH] 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 Co-authored-by: Bailey Pumfleet --- apps/web/public/static/locales/en/common.json | 4 +++ .../features/ee/billing/billing-service.ts | 4 ++- .../ee/billing/stripe-billling-service.ts | 6 ++--- .../ee/billing/teams/internal-team-billing.ts | 8 +++--- packages/lib/hooks/useHasPaidPlan.ts | 6 ++--- .../viewer/teams/hasActiveTeamPlan.handler.ts | 18 ++++++++----- .../getWorkflowActionOptions.handler.ts | 6 ++--- .../viewer/workflows/update.handler.ts | 9 +++---- .../ui/components/badge/UpgradeTeamsBadge.tsx | 27 ++++++++++++++----- .../ui/components/form/select/components.tsx | 6 ++++- 10 files changed, 61 insertions(+), 33 deletions(-) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 07b90ea1bf..cdcb3001b3 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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", diff --git a/packages/features/ee/billing/billing-service.ts b/packages/features/ee/billing/billing-service.ts index 63c785f843..8c88de7b3d 100644 --- a/packages/features/ee/billing/billing-service.ts +++ b/packages/features/ee/billing/billing-service.ts @@ -1,3 +1,5 @@ +import type Stripe from "stripe"; + export interface BillingService { checkoutSessionIsPaid(paymentId: string): Promise; handleSubscriptionCancel(subscriptionId: string): Promise; @@ -7,5 +9,5 @@ export interface BillingService { subscriptionItemId: string; membershipCount: number; }): Promise; - checkIfTeamHasActivePlan(subscriptionId: string): Promise; + getSubscriptionStatus(subscriptionId: string): Promise; } diff --git a/packages/features/ee/billing/stripe-billling-service.ts b/packages/features/ee/billing/stripe-billling-service.ts index 1604a1ade1..b7bc123968 100644 --- a/packages/features/ee/billing/stripe-billling-service.ts +++ b/packages/features/ee/billing/stripe-billling-service.ts @@ -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; } } diff --git a/packages/features/ee/billing/teams/internal-team-billing.ts b/packages/features/ee/billing/teams/internal-team-billing.ts index 18beff9ead..aba14c881d 100644 --- a/packages/features/ee/billing/teams/internal-team-billing.ts +++ b/packages/features/ee/billing/teams/internal-team-billing.ts @@ -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); } } diff --git a/packages/lib/hooks/useHasPaidPlan.ts b/packages/lib/hooks/useHasPaidPlan.ts index ca37686ea0..a3496789e1 100644 --- a/packages/lib/hooks/useHasPaidPlan.ts +++ b/packages/lib/hooks/useHasPaidPlan.ts @@ -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; diff --git a/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts b/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts index 8b817b2e22..7ea6bbff9c 100644 --- a/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts +++ b/packages/trpc/server/routers/viewer/teams/hasActiveTeamPlan.handler.ts @@ -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; diff --git a/packages/trpc/server/routers/viewer/workflows/getWorkflowActionOptions.handler.ts b/packages/trpc/server/routers/viewer/workflows/getWorkflowActionOptions.handler.ts index 1cd8591615..f5efc44c91 100644 --- a/packages/trpc/server/routers/viewer/workflows/getWorkflowActionOptions.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/getWorkflowActionOptions.handler.ts @@ -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 ); }; diff --git a/packages/trpc/server/routers/viewer/workflows/update.handler.ts b/packages/trpc/server/routers/viewer/workflows/update.handler.ts index 3ed9d91b48..96fa837021 100755 --- a/packages/trpc/server/routers/viewer/workflows/update.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/update.handler.ts @@ -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[] = []; diff --git a/packages/ui/components/badge/UpgradeTeamsBadge.tsx b/packages/ui/components/badge/UpgradeTeamsBadge.tsx index 6bc2335b7b..f461a979aa 100644 --- a/packages/ui/components/badge/UpgradeTeamsBadge.tsx +++ b/packages/ui/components/badge/UpgradeTeamsBadge.tsx @@ -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 ( - - - {t("upgrade")} + + + {badgeString} ); diff --git a/packages/ui/components/form/select/components.tsx b/packages/ui/components/form/select/components.tsx index 267c384d56..99060b382f 100644 --- a/packages/ui/components/form/select/components.tsx +++ b/packages/ui/components/form/select/components.tsx @@ -47,7 +47,11 @@ export const OptionComponent = < {props.label || <> } - {(props.data as unknown as ExtendedOption).needsTeamsUpgrade ? : <>} + {(props.data as unknown as ExtendedOption).needsTeamsUpgrade ? ( + + ) : ( + <> + )} {props.isSelected && }