Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f40c332
manage billing section
sean-brydon Sep 17, 2025
b10fb56
wip billing credits
sean-brydon Sep 17, 2025
5f8243d
WIP
sean-brydon Sep 17, 2025
a244251
WIP
sean-brydon Sep 18, 2025
b719826
Download expense log
sean-brydon Sep 18, 2025
aa6dab5
credit worth
sean-brydon Sep 18, 2025
32a013c
skeleton fixes
sean-brydon Sep 18, 2025
e5b9ab9
add org tip
sean-brydon Sep 18, 2025
ee5a999
add teams tip
sean-brydon Sep 18, 2025
da4d6cc
restore service
sean-brydon Sep 18, 2025
47ccd0e
type check
sean-brydon Sep 18, 2025
460237b
type check
sean-brydon Sep 18, 2025
7586e87
fix types
sean-brydon Sep 18, 2025
ac0d846
additional credits
sean-brydon Sep 18, 2025
a90a13b
fix progress bar
sean-brydon Sep 18, 2025
22d9c56
add dashed prop
sean-brydon Sep 18, 2025
a532d5d
match new designs
sean-brydon Sep 18, 2025
a7806cf
Merge branch 'main' into feat/billing-redesign-settings
sean-brydon Sep 18, 2025
0b24b48
Merge branch 'main' into feat/billing-redesign-settings
sean-brydon Sep 19, 2025
b2a191a
hide area with no monthly credits
sean-brydon Sep 19, 2025
2d8bcc8
fix i18n
sean-brydon Sep 19, 2025
76ad510
show current balance label
sean-brydon Sep 19, 2025
b69403c
Update apps/web/modules/settings/billing/billing-view.tsx
sean-brydon Sep 19, 2025
6c2e24e
spacing + monthly credits not showing additional
sean-brydon Sep 19, 2025
e8158d4
Remove additional credits from monthly calculations
sean-brydon Sep 19, 2025
e9ea0b3
feat: replace add members redirect with invite modal in billing settings
devin-ai-integration[bot] Sep 19, 2025
5f22b0c
Remove redudant vars from method
sean-brydon Sep 19, 2025
65c8cfc
fix type check
sean-brydon Sep 19, 2025
be6e62e
Merge branch 'main' into feat/billing-redesign-settings
anikdhabal Sep 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const Page = async () => {
<SettingsHeader
title={t("billing")}
description={t("manage_billing_description")}
borderInShellHeader={true}>
borderInShellHeader={false}>
<BillingView />
</SettingsHeader>
);
Expand Down
27 changes: 17 additions & 10 deletions apps/web/modules/settings/billing/billing-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,23 +75,30 @@ const BillingView = () => {
}
};

// title={t("view_and_manage_billing_details")}
// description={t("view_and_edit_billing_details")}>
return (
<>
<div className="border-subtle space-y-6 rounded-b-lg border border-t-0 px-6 py-8 text-sm sm:space-y-8">
<CtaRow title={t("view_and_manage_billing_details")} description={t("view_and_edit_billing_details")}>
<Button color="primary" href={billingHref} target="_blank" EndIcon="external-link">
<div className="bg-muted border-muted mt-5 rounded-xl border p-1">
<div className="bg-default border-muted flex rounded-[10px] border px-5 py-4">
<div className="flex w-full flex-col gap-1">
<h3 className="text-emphasis text-sm font-semibold leading-none">{t("manage_billing")}</h3>
<p className="text-subtle text-sm font-medium leading-tight">
{t("view_and_manage_billing_details")}
</p>
</div>
<Button color="primary" href={billingHref} target="_blank" size="sm" EndIcon="external-link">
{t("billing_portal")}
</Button>
Comment on lines +88 to 90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add rel="noopener noreferrer" to external link

Prevents tabnabbing and drops referrer.

-          <Button color="primary" href={billingHref} target="_blank" size="sm" EndIcon="external-link">
+          <Button color="primary" href={billingHref} target="_blank" rel="noopener noreferrer" size="sm" EndIcon="external-link">
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Button color="primary" href={billingHref} target="_blank" size="sm" EndIcon="external-link">
{t("billing_portal")}
</Button>
<Button color="primary" href={billingHref} target="_blank" rel="noopener noreferrer" size="sm" EndIcon="external-link">
{t("billing_portal")}
</Button>
🤖 Prompt for AI Agents
In apps/web/modules/settings/billing/billing-view.tsx around lines 90 to 92, the
external link Button is missing rel="noopener noreferrer"; add rel="noopener
noreferrer" to the Button so the rendered anchor includes it (if the Button
component doesn't forward rel, replace it with an <a> tag or set component="a"
and pass rel, href and target accordingly) to prevent tabnabbing and drop the
referrer.

</CtaRow>
</div>
<BillingCredits />
<div className="border-subtle mt-6 space-y-6 rounded-lg border px-6 py-8 text-sm sm:space-y-8">
<CtaRow title={t("need_anything_else")} description={t("further_billing_help")}>
<Button color="secondary" onClick={onContactSupportClick}>
</div>
<div className="flex items-center justify-between px-4 py-5">
<p className="text-subtle text-sm font-medium leading-tight">{t("need_help")}</p>
<Button color="secondary" size="sm" onClick={onContactSupportClick}>
{t("contact_support")}
</Button>
</CtaRow>
</div>
</div>
<BillingCredits />
</>
);
};
Expand Down
264 changes: 167 additions & 97 deletions apps/web/modules/settings/billing/components/BillingCredits.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ import { downloadAsCsv } from "@calcom/lib/csvUtils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
import { trpc } from "@calcom/trpc/react";
import classNames from "@calcom/ui/classNames";
import { Button } from "@calcom/ui/components/button";
import { Select } from "@calcom/ui/components/form";
import { TextField, Label, InputError } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { ProgressBar } from "@calcom/ui/components/progress-bar";
import { showToast } from "@calcom/ui/components/toast";
import { Tooltip } from "@calcom/ui/components/tooltip";

import { BillingCreditsSkeleton } from "./BillingCreditsSkeleton";

Expand All @@ -29,6 +32,39 @@ type MonthOption = {
endDate: string;
};

type CreditRowProps = {
label: string;
value: number;
isBold?: boolean;
underline?: "dashed" | "solid";
className?: string;
};

const CreditRow = ({ label, value, isBold = false, underline, className = "" }: CreditRowProps) => {
const numberFormatter = new Intl.NumberFormat();
return (
<div
className={classNames(
`my-1 flex justify-between`,
underline === "dashed"
? "border-subtle border-b border-dashed"
: underline === "solid"
? "border-subtle border-b border-solid"
: "mt-1",
className
)}>
<span
className={classNames("text-sm", isBold ? "font-semibold" : "text-subtle font-medium leading-tight")}>
{label}
</span>
<span
className={classNames(`text-sm`, isBold ? "font-semibold" : "text-subtle font-medium leading-tight")}>
{numberFormatter.format(value)}
</span>
</div>
);
};

const getMonthOptions = (): MonthOption[] => {
const options: MonthOption[] = [];
const minDate = dayjs.utc("2025-05-01");
Expand Down Expand Up @@ -71,6 +107,7 @@ export default function BillingCredits() {

const params = useParamsWithFallback();
const orgId = session.data?.user?.org?.id;
const orgSlug = session.data?.user?.org?.slug;

const parsedTeamId = Number(params.id);
const teamId: number | undefined = Number.isFinite(parsedTeamId)
Expand Down Expand Up @@ -132,117 +169,150 @@ export default function BillingCredits() {
buyCreditsMutation.mutate({ quantity: data.quantity, teamId });
};

const teamCreditsPercentageUsed =
creditsData.credits.totalMonthlyCredits > 0
? (creditsData.credits.totalRemainingMonthlyCredits / creditsData.credits.totalMonthlyCredits) * 100
: 0;
const totalCredits =
(creditsData.credits.totalCreditsForMonth ?? 0) ||
creditsData.credits.totalMonthlyCredits + creditsData.credits.additionalCredits;
const totalUsed =
(creditsData.credits.totalCreditsUsedThisMonth ?? 0) ||
totalCredits - (creditsData.credits.totalRemainingCreditsForMonth ?? 0);

const teamCreditsPercentageUsed = totalCredits > 0 ? (totalUsed / totalCredits) * 100 : 0;
const numberFormatter = new Intl.NumberFormat();

return (
<div className="border-subtle mt-8 space-y-6 rounded-lg border px-6 py-6 pb-6 text-sm sm:space-y-8">
<div>
<h2 className="text-base font-semibold">{t("credits")}</h2>
<ServerTrans
t={t}
i18nKey="view_and_manage_credits_description"
components={[
<Link
key="Credit System"
className="underline underline-offset-2"
target="_blank"
href="https://cal.com/help/billing-and-usage/messaging-credits">
Learn more
</Link>,
]}
/>
<div className="-mx-6 mt-6">
<hr className="border-subtle" />
<>
<div className="bg-muted border-muted mt-5 rounded-xl border p-1">
<div className="flex flex-col gap-1 px-4 py-5">
<h2 className="text-default text-base font-semibold leading-none">{t("credits")}</h2>
<p className="text-subtle text-sm font-medium leading-tight">{t("view_and_manage_credits")}</p>
</div>
<div className="mt-6">
{creditsData.credits.totalMonthlyCredits > 0 ? (
<div className="mb-4">
<Label>{t("monthly_credits")}</Label>
<ProgressBar
color="green"
percentageValue={teamCreditsPercentageUsed}
label={`${Math.max(0, Math.round(teamCreditsPercentageUsed))}%`}
/>
<div className="text-subtle">
<div>
{t("total_credits", {
totalCredits: creditsData.credits.totalMonthlyCredits,
})}
<div className="bg-default border-muted flex w-full rounded-[10px] border px-5 py-4">
<div className="w-full">
{totalCredits > 0 ? (
<div className="mb-4">
<CreditRow
label={t("monthly_credits")}
value={creditsData.credits.totalMonthlyCredits ?? 0}
isBold={true}
underline="dashed"
/>
<CreditRow label={t("credits_used")} value={totalUsed} underline="solid" />
<CreditRow
label={t("total_credits_remaining")}
value={creditsData.credits.totalRemainingCreditsForMonth}
/>
<div className="mt-4">
<ProgressBar color="green" percentageValue={100 - teamCreditsPercentageUsed} />
</div>
<div>
{t("remaining_credits", {
remainingCredits: creditsData.credits.totalRemainingMonthlyCredits,
})}
{/*750 credits per tip*/}
<div className="mt-4 flex flex-1 items-center justify-between">
<p className="text-subtle text-sm font-medium leading-tight">
{orgSlug ? t("credits_per_tip_org") : t("credits_per_tip_teams")}
</p>
<Button
href={
orgSlug
? `/settings/organizations/${orgSlug}/members`
: `/settings/teams/${teamId}/members`
}
size="sm"
color="secondary">
{t("add_members_no_elipsis")}
</Button>
</div>
</div>
) : (
<></>
)}
<div className="-mx-5 mt-5">
<hr className="border-subtle" />
</div>
) : (
<></>
)}
<Label>
{creditsData.credits.totalMonthlyCredits ? t("additional_credits") : t("available_credits")}
</Label>
<div className="mt-2 text-sm">{creditsData.credits.additionalCredits}</div>
<div className="-mx-6 mb-6 mt-6">
<hr className="border-subtle mb-3 mt-3" />
</div>
<form onSubmit={handleSubmit(onSubmit)} className="flex">
<div className="-mb-1 mr-auto">
<Label>{t("buy_additional_credits")}</Label>
<div className="flex flex-col">
<TextField
required
type="number"
{...register("quantity", {
required: t("error_required_field"),
min: { value: 50, message: t("minimum_of_credits_required") },
valueAsNumber: true,
})}
label=""
containerClassName="w-60"
onChange={(e) => setValue("quantity", Number(e.target.value))}
min={50}
addOnSuffix={<>{t("credits")}</>}
/>
{/*Auto Top-Up goes here when we have it*/}
{/*<div className="-mx-5 mt-5">
<hr className="border-subtle" />
</div>*/}
{/*Additional Credits*/}
<form onSubmit={handleSubmit(onSubmit)} className="mt-4 flex">
<div className="-mb-1 mr-auto w-full">
<div className="flex justify-between">
<div className="flex gap-1">
<Label>{t("additional_credits")}</Label>
<Tooltip content={t("view_additional_credits_expense_tip")}>
<Icon name="info" className="text-muted-foreground mt-0.5 h-3 w-3" />
</Tooltip>
</div>
<p className="text-sm font-semibold leading-none">
{numberFormatter.format(creditsData.credits.additionalCredits)}
</p>
</div>
<div className="flex w-full items-center gap-2">
<TextField
required
type="number"
{...register("quantity", {
required: t("error_required_field"),
min: { value: 50, message: t("minimum_of_credits_required") },
valueAsNumber: true,
})}
label=""
containerClassName="w-full -mt-1"
size="sm"
onChange={(e) => setValue("quantity", Number(e.target.value))}
min={50}
addOnSuffix={<>{t("credits")}</>}
/>
<Button color="secondary" target="_blank" size="sm" type="submit" data-testid="buy-credits">
{t("buy")}
</Button>
</div>
{errors.quantity && <InputError message={errors.quantity.message ?? t("invalid_input")} />}
</div>
</form>
<div className="-mx-5 mt-5">
<hr className="border-subtle" />
</div>
<div className="mt-auto">
<Button
color="primary"
target="_blank"
EndIcon="external-link"
type="submit"
data-testid="buy-credits">
{t("buy_credits")}
</Button>
</div>
</form>
<div className="-mx-6 mb-6 mt-6">
<hr className="border-subtle mb-3 mt-3" />
</div>
<div className="flex">
<div className="mr-auto">
<Label className="mb-4">{t("download_expense_log")}</Label>
<div className="mt-2 flex flex-col">
<Select
options={monthOptions}
value={selectedMonth}
onChange={(option) => option && setSelectedMonth(option)}
/>
{/*Download Expense Log*/}
<div className="mt-4 flex">
<div className="mr-auto w-full">
<Label className="mb-4">{t("download_expense_log")}</Label>
<div className="mr-2 mt-1">
<Select
size="sm"
className="w-full"
innerClassNames={{
control: "font-medium text-emphasis",
}}
options={monthOptions}
value={selectedMonth}
onChange={(option) => option && setSelectedMonth(option)}
/>
</div>
</div>
<div className="mt-auto">
<Button onClick={handleDownload} loading={isDownloading} color="secondary" size="sm">
{t("download")}
</Button>
</div>
</div>
<div className="mt-auto">
<Button onClick={handleDownload} loading={isDownloading} StartIcon="file-down">
{t("download")}
</Button>
</div>
</div>
</div>
{/*Credit Worth Section*/}
<div className="text-subtle px-5 py-4 text-sm font-medium leading-tight">
<ServerTrans
t={t}
i18nKey="credit_worth_description"
components={[
<Link
key="Credit System"
className="underline underline-offset-2"
target="_blank"
href="https://cal.com/help/billing-and-usage/messaging-credits">
Learn more
</Link>,
]}
/>
Comment on lines +296 to +308
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Add rel for external link security and localize link label

target="_blank" should include rel="noopener noreferrer". Also replace literal "Learn more" with a localized string.

           <ServerTrans
             t={t}
             i18nKey="credit_worth_description"
             components={[
               <Link
                 key="Credit System"
                 className="underline underline-offset-2"
-                target="_blank"
-                href="https://cal.com/help/billing-and-usage/messaging-credits">
-                Learn more
+                target="_blank"
+                rel="noopener noreferrer"
+                href="https://cal.com/help/billing-and-usage/messaging-credits">
+                {t("learn_more")}
               </Link>,
             ]}
           />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<ServerTrans
t={t}
i18nKey="credit_worth_description"
components={[
<Link
key="Credit System"
className="underline underline-offset-2"
target="_blank"
href="https://cal.com/help/billing-and-usage/messaging-credits">
Learn more
</Link>,
]}
/>
<ServerTrans
t={t}
i18nKey="credit_worth_description"
components={[
<Link
key="Credit System"
className="underline underline-offset-2"
target="_blank"
rel="noopener noreferrer"
href="https://cal.com/help/billing-and-usage/messaging-credits">
{t("learn_more")}
</Link>,
]}
/>
🤖 Prompt for AI Agents
In apps/web/modules/settings/billing/components/BillingCredits.tsx around lines
293 to 305, the external Link opens in a new tab but is missing rel="noopener
noreferrer" and uses a hardcoded "Learn more" label; update the Link props to
include rel="noopener noreferrer" for security and replace the literal label
with a localized string via the existing i18n function (e.g. use t('learn_more')
or the appropriate translation key) so the link text is localized.

</div>
</div>
</div>
</>
);
}
Loading
Loading