From fc3815a15098b885647d7364b420b51e53a54081 Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Tue, 3 Feb 2026 10:02:14 +0100 Subject: [PATCH] fix: show personal credits even when user is on teams plan (#27511) * show personal credits even if on team plan * show personal credits even when on teams plan * fix learn more link --------- Co-authored-by: CarinaWolli --- .../billing/components/BillingCredits.tsx | 211 ++++++++++++------ .../viewer/credits/getAllCredits.handler.ts | 14 +- 2 files changed, 147 insertions(+), 78 deletions(-) diff --git a/apps/web/modules/settings/billing/components/BillingCredits.tsx b/apps/web/modules/settings/billing/components/BillingCredits.tsx index 3abc6a015b..4fb3287015 100644 --- a/apps/web/modules/settings/billing/components/BillingCredits.tsx +++ b/apps/web/modules/settings/billing/components/BillingCredits.tsx @@ -1,14 +1,12 @@ "use client"; import { useSession } from "next-auth/react"; -import Link from "next/link"; import { useRouter, usePathname } from "next/navigation"; import { useState, useMemo } from "react"; import { useForm } from "react-hook-form"; import dayjs from "@calcom/dayjs"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; -import ServerTrans from "@calcom/lib/components/ServerTrans"; import { IS_SMS_CREDITS_ENABLED } from "@calcom/lib/constants"; import { downloadAsCsv } from "@calcom/lib/csvUtils"; import { useLocale } from "@calcom/lib/hooks/useLocale"; @@ -24,6 +22,7 @@ import { showToast } from "@calcom/ui/components/toast"; import { Tooltip } from "@calcom/ui/components/tooltip"; import { MemberInvitationModalWithoutMembers } from "~/ee/teams/components/MemberInvitationModal"; +import { LearnMoreLink } from "~/event-types/components/LearnMoreLink"; import { BillingCreditsSkeleton } from "./BillingCreditsSkeleton"; @@ -42,7 +41,13 @@ type CreditRowProps = { className?: string; }; -const CreditRow = ({ label, value, isBold = false, underline, className = "" }: CreditRowProps) => { +const CreditRow = ({ + label, + value, + isBold = false, + underline, + className = "", +}: CreditRowProps) => { const numberFormatter = new Intl.NumberFormat(); return (
+ )} + > + className={classNames( + "text-sm", + isBold ? "font-semibold" : "font-medium leading-tight text-subtle" + )} + > {label} + className={classNames( + `text-sm`, + isBold ? "font-semibold" : "font-medium leading-tight text-subtle" + )} + > {numberFormatter.format(value)}
@@ -73,7 +87,10 @@ const getMonthOptions = (): MonthOption[] => { let date = dayjs.utc(); let count = 0; - while ((date.isAfter(minDate) || date.isSame(minDate, "month")) && count < 12) { + while ( + (date.isAfter(minDate) || date.isSame(minDate, "month")) && + count < 12 + ) { const startDate = date.startOf("month"); const endDate = date.endOf("month"); options.push({ @@ -96,9 +113,12 @@ export default function BillingCredits() { const session = useSession(); const orgBranding = useOrgBranding(); const monthOptions = useMemo(() => getMonthOptions(), []); - const [selectedMonth, setSelectedMonth] = useState(monthOptions[0]); + const [selectedMonth, setSelectedMonth] = useState( + monthOptions[0] + ); const [isDownloading, setIsDownloading] = useState(false); - const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false); + const [showMemberInvitationModal, setShowMemberInvitationModal] = + useState(false); const utils = trpc.useUtils(); const { @@ -122,14 +142,26 @@ export default function BillingCredits() { const tokens = (pathname ?? "").split("/").filter(Boolean); const settingsIndex = tokens.indexOf("settings"); const isOrgScopedPath = - settingsIndex >= 0 && ["organizations", "teams"].includes(tokens[settingsIndex + 1]); + settingsIndex >= 0 && + ["organizations", "teams"].includes(tokens[settingsIndex + 1]); - const shouldRender = IS_SMS_CREDITS_ENABLED && !(orgId && !isOrgScopedPath && !orgBranding?.slug); + const shouldRender = + IS_SMS_CREDITS_ENABLED && + !(orgId && !isOrgScopedPath && !orgBranding?.slug); - const { data: creditsData, isLoading } = trpc.viewer.credits.getAllCredits.useQuery( - { teamId }, - { enabled: shouldRender } + const { data: creditsData, isLoading } = + trpc.viewer.credits.getAllCredits.useQuery( + { teamId }, + { enabled: shouldRender } + ); + + const { data: teamPlanData } = trpc.viewer.teams.hasTeamPlan.useQuery( + undefined, + { + enabled: shouldRender && !teamId, + } ); + const isUserInTeam = teamPlanData?.hasTeamPlan ?? false; if (!shouldRender) return null; @@ -153,7 +185,9 @@ export default function BillingCredits() { endDate: selectedMonth.endDate, }); if (result?.csvData) { - const filename = `credit-expense-log-${selectedMonth.value.toLowerCase().replace(" ", "-")}.csv`; + const filename = `credit-expense-log-${selectedMonth.value + .toLowerCase() + .replace(" ", "-")}.csv`; downloadAsCsv(result.csvData, filename); } else { showToast(t("error_downloading_expense_log"), "error"); @@ -168,6 +202,11 @@ export default function BillingCredits() { if (isLoading && teamId) return ; if (!creditsData) return null; + // For personal billing page: hide credits section if user is part of a team and has no personal credits + if (!teamId && isUserInTeam && creditsData.credits.additionalCredits <= 0) { + return null; + } + const onSubmit = (data: { quantity: number }) => { buyCreditsMutation.mutate({ quantity: data.quantity, teamId }); }; @@ -175,15 +214,20 @@ export default function BillingCredits() { const totalCredits = creditsData.credits.totalMonthlyCredits ?? 0; const totalUsed = creditsData.credits.totalCreditsUsedThisMonth ?? 0; - const teamCreditsPercentageUsed = totalCredits > 0 ? (totalUsed / totalCredits) * 100 : 0; + const teamCreditsPercentageUsed = + totalCredits > 0 ? (totalUsed / totalCredits) * 100 : 0; const numberFormatter = new Intl.NumberFormat(); return ( <> -
+
-

{t("credits")}

-

{t("view_and_manage_credits")}

+

+ {t("credits")} +

+

+ {t("view_and_manage_credits")} +

@@ -196,20 +240,33 @@ export default function BillingCredits() { isBold={true} underline="dashed" /> - +
- +
{/*750 credits per tip*/} -
-

- {orgSlug ? t("credits_per_tip_org") : t("credits_per_tip_teams")} +

+

+ {orgSlug + ? t("credits_per_tip_org") + : t("credits_per_tip_teams")}

-
@@ -227,51 +284,76 @@ export default function BillingCredits() {
*/} {/*Additional Credits*/} -
-
+
+
-
+

- {t("current_balance")}{" "} - {numberFormatter.format(creditsData.credits.additionalCredits)} + + {t("current_balance")} + {" "} + {numberFormatter.format( + creditsData.credits.additionalCredits + )}

- +
-
- setValue("quantity", Number(e.target.value))} - min={50} - addOnSuffix={<>{t("credits")}} + {/* Users who are part of a team cannot buy personal credits */} + {teamId || !isUserInTeam ? ( + + + setValue("quantity", Number(e.target.value)) + } + min={50} + addOnSuffix={<>{t("credits")}} + /> + + + ) : null} + {errors.quantity && ( + - -
- {errors.quantity && } + )}
- +

{/*Download Expense Log*/} -
+
-
+