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:
co-authored by
CarinaWolli
Bailey Pumfleet
parent
bb36807689
commit
f2ca2bdc83
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 || <> </>}
|
||||
</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>
|
||||
|
||||
Reference in New Issue
Block a user