Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
6 changes: 6 additions & 0 deletions .changeset/brown-news-unite.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

[Billing Beta] Cleanup naming inconsistencies in billing dates.
18 changes: 6 additions & 12 deletions packages/clerk-js/src/core/resources/CommerceSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,10 @@ export class CommerceSubscriptionItem extends BaseResource implements CommerceSu
planPeriod!: CommerceSubscriptionPlanPeriod;
status!: CommerceSubscriptionStatus;
createdAt!: Date;
periodStart!: Date;
periodEnd!: Date | null;
canceledAt!: Date | null;
Comment on lines +65 to +67
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Public API rename: add JSDoc, confirm breaking-change handling

Great move consolidating to Date-based fields. Since these are public properties, please:

  • Add JSDoc to clarify semantics and nullability.
  • Confirm this is tracked as a breaking change in the changeset and docs.
  • Optional: consider temporary runtime getters for the old names to ease migration (types would still be removed).

Apply JSDoc locally:

   createdAt!: Date;
-  periodStart!: Date;
-  periodEnd!: Date | null;
-  canceledAt!: Date | null;
+  /**
+   * Start of the current billing period.
+   */
+  periodStart!: Date;
+  /**
+   * End of the current billing period (null for free plans or when not applicable).
+   */
+  periodEnd!: Date | null;
+  /**
+   * Timestamp when cancellation was requested/effective (null if not canceled).
+   */
+  canceledAt!: Date | null;

If you want temporary runtime back-compat, add in this class (types remain removed):

// Deprecated runtime aliases (optional)
/** @deprecated Use periodStart */
get periodStartDate(): Date { return this.periodStart }
/** @deprecated Use periodEnd */
get periodEndDate(): Date | null { return this.periodEnd }
/** @deprecated Use canceledAt */
get canceledAtDate(): Date | null { return this.canceledAt }

🏁 Script executed:

#!/usr/bin/env bash
set -e

printf "1) CommerceSubscription class definition:\n"
rg -n "class CommerceSubscription" -n packages/clerk-js/src/core/resources/CommerceSubscription.ts || echo "Not found"

printf "\n2) Date fields in CommerceSubscription:\n"
rg -nE "periodStart|periodEnd|canceledAt" -n packages/clerk-js/src/core/resources/CommerceSubscription.ts || echo "No matching fields"

printf "\n3) Changesets mentioning subscription:\n"
if [ -d ".changeset" ]; then
  rg -i "subscription" -n .changeset || echo "No references in .changeset"
else
  echo "No .changeset directory found"
fi

printf "\n4) Documentation mentions:\n"
if [ -d "docs" ]; then
  rg -i "CommerceSubscription" -n docs || echo "No docs references"
else
  echo "No docs directory found"
fi

Length of output: 1125


Add JSDoc, update changeset, and docs for renamed subscription fields

Please apply the following to fully cover this public-API change:

• In packages/clerk-js/src/core/resources/CommerceSubscription.ts (around lines 65–67), add JSDoc to the new Date fields:

  createdAt!: Date;
+ /**
+  * Start of the current billing period.
+  */
  periodStart!: Date;
+ /**
+  * End of the current billing period (null when not applicable, e.g., free plan).
+  */
  periodEnd!: Date | null;
+ /**
+  * Timestamp when cancellation was requested or effective (null if not canceled).
+  */
  canceledAt!: Date | null;

• Ensure you’ve created or updated a .changeset/…md entry under packages/clerk-js that calls out the breaking-change of these property renames and type updates.

• Update any public documentation or API reference to use periodStart, periodEnd, and canceledAt instead of the old fields.

Optional, for smoother migration you can add deprecated runtime getters:

/** @deprecated Use periodStart */
get periodStartDate(): Date { return this.periodStart }
/** @deprecated Use periodEnd */
get periodEndDate(): Date | null { return this.periodEnd }
/** @deprecated Use canceledAt */
get canceledAtDate(): Date | null { return this.canceledAt }
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/resources/CommerceSubscription.ts around lines
65–67, the newly renamed Date fields (periodStart, periodEnd, canceledAt) lack
JSDoc and the repo is missing a changeset and documentation updates for this
public-API break; add concise JSDoc comments above each field describing the
value and types (e.g., periodStart: subscription period start Date, periodEnd:
nullable subscription period end Date, canceledAt: nullable cancellation Date),
create or update a .changeset/*.md under packages/clerk-js documenting the
breaking change and migration notes, update public docs/API reference to use
periodStart/periodEnd/canceledAt instead of the old names, and optionally add
deprecated runtime getters (periodStartDate, periodEndDate, canceledAtDate) that
return the new fields to smooth migration.

pastDueAt!: Date | null;
periodStartDate!: Date;
periodEndDate!: Date | null;
canceledAtDate!: Date | null;
periodStart!: number;
periodEnd!: number;
canceledAt!: number | null;
//TODO(@COMMERCE): Why can this be undefined ?
amount?: CommerceMoney;
credit?: {
Expand All @@ -90,16 +87,13 @@ export class CommerceSubscriptionItem extends BaseResource implements CommerceSu
this.plan = new CommercePlan(data.plan);
this.planPeriod = data.plan_period;
this.status = data.status;
this.periodStart = data.period_start;
this.periodEnd = data.period_end;
this.canceledAt = data.canceled_at;

this.createdAt = unixEpochToDate(data.created_at);
this.pastDueAt = data.past_due_at ? unixEpochToDate(data.past_due_at) : null;

this.periodStartDate = unixEpochToDate(data.period_start);
this.periodEndDate = data.period_end ? unixEpochToDate(data.period_end) : null;
this.canceledAtDate = data.canceled_at ? unixEpochToDate(data.canceled_at) : null;
this.periodStart = unixEpochToDate(data.period_start);
this.periodEnd = data.period_end ? unixEpochToDate(data.period_end) : null;
this.canceledAt = data.canceled_at ? unixEpochToDate(data.canceled_at) : null;

this.amount = data.amount ? commerceMoneyFromJSON(data.amount) : undefined;
this.credit = data.credit && data.credit.amount ? { amount: commerceMoneyFromJSON(data.credit.amount) } : undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const PricingTableRoot = (props: PricingTableProps) => {

// don't pay attention to the default plan
const activeSubscription = subscriptionItems?.find(
sub => !sub.canceledAtDate && sub.status === 'active' && !sub.plan.isDefault,
sub => !sub.canceledAt && sub.status === 'active' && !sub.plan.isDefault,
);
if (activeSubscription) {
return activeSubscription.planPeriod;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ function Card(props: CardProps) {
shouldShowFooter = true;
shouldShowFooterNotice = true;
} else if (subscription.status === 'active') {
if (subscription.canceledAtDate) {
if (subscription.canceledAt) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyAmount > 0) {
Expand Down Expand Up @@ -233,7 +233,7 @@ function Card(props: CardProps) {
elementDescriptor={descriptors.pricingTableCardFooterNotice}
variant={isCompact ? 'buttonSmall' : 'buttonLarge'}
localizationKey={localizationKeys('badge__startsAt', {
date: subscription?.periodStartDate,
date: subscription?.periodStart,
})}
colorScheme='secondary'
sx={t => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ describe('SubscriptionDetails', () => {
isDefault: false,
},
createdAt: new Date('2021-01-01'),
periodStartDate: new Date('2021-01-01'),
periodEndDate: new Date('2021-02-01'),
canceledAtDate: null,
periodStart: new Date('2021-01-01'),
periodEnd: new Date('2021-02-01'),
canceledAt: null,
paymentSourceId: 'src_123',
planPeriod: 'month',
status: 'active',
Expand Down Expand Up @@ -161,9 +161,9 @@ describe('SubscriptionDetails', () => {
isDefault: false,
},
createdAt: new Date('2021-01-01'),
periodStartDate: new Date('2021-01-01'),
periodEndDate: new Date('2022-01-01'),
canceledAtDate: null,
periodStart: new Date('2021-01-01'),
periodEnd: new Date('2022-01-01'),
canceledAt: null,
paymentSourceId: 'src_123',
planPeriod: 'annual' as const,
status: 'active' as const,
Expand Down Expand Up @@ -246,9 +246,9 @@ describe('SubscriptionDetails', () => {
isDefault: true,
},
createdAt: new Date('2021-01-01'),
periodStartDate: new Date('2021-01-01'),
periodEndDate: new Date('2021-02-01'),
canceledAtDate: null,
periodStart: new Date('2021-01-01'),
periodEnd: new Date('2021-02-01'),
canceledAt: null,
paymentSourceId: 'src_123',
planPeriod: 'month' as const,
status: 'active' as const,
Expand Down Expand Up @@ -354,9 +354,9 @@ describe('SubscriptionDetails', () => {
id: 'sub_annual',
plan: planAnnual,
createdAt: new Date('2021-01-01'),
periodStartDate: new Date('2021-01-01'),
periodEndDate: new Date('2022-01-01'),
canceledAtDate: new Date('2021-04-01'),
periodStart: new Date('2021-01-01'),
periodEnd: new Date('2022-01-01'),
canceledAt: new Date('2021-04-01'),
paymentSourceId: 'src_annual',
planPeriod: 'annual' as const,
status: 'active' as const,
Expand All @@ -365,9 +365,9 @@ describe('SubscriptionDetails', () => {
id: 'sub_monthly',
plan: planMonthly,
createdAt: new Date('2022-01-01'),
periodStartDate: new Date('2022-02-01'),
periodEndDate: new Date('2022-03-01'),
canceledAtDate: null,
periodStart: new Date('2022-02-01'),
periodEnd: new Date('2022-03-01'),
canceledAt: null,
paymentSourceId: 'src_monthly',
planPeriod: 'month' as const,
status: 'upcoming' as const,
Expand Down Expand Up @@ -486,9 +486,9 @@ describe('SubscriptionDetails', () => {
id: 'test_active',
plan: planMonthly,
createdAt: new Date('2021-01-01'),
periodStartDate: new Date('2021-01-01'),
periodEndDate: new Date('2021-02-01'),
canceledAtDate: new Date('2021-01-03'),
periodStart: new Date('2021-01-01'),
periodEnd: new Date('2021-02-01'),
canceledAt: new Date('2021-01-03'),
paymentSourceId: 'src_free_active',
planPeriod: 'month' as const,
status: 'active' as const,
Expand All @@ -497,8 +497,8 @@ describe('SubscriptionDetails', () => {
id: 'sub_free_upcoming',
plan: planFreeUpcoming,
createdAt: new Date('2021-01-03'),
periodStartDate: new Date('2021-02-01'),
canceledAtDate: null,
periodStart: new Date('2021-02-01'),
canceledAt: null,
paymentSourceId: 'src_free_upcoming',
planPeriod: 'month' as const,
status: 'upcoming' as const,
Expand Down Expand Up @@ -582,9 +582,9 @@ describe('SubscriptionDetails', () => {
isDefault: false,
},
createdAt: new Date('2021-01-01'),
periodStartDate: new Date('2021-01-01'),
periodEndDate: new Date('2021-02-01'),
canceledAtDate: null,
periodStart: new Date('2021-01-01'),
periodEnd: new Date('2021-02-01'),
canceledAt: null,
paymentSourceId: 'src_123',
planPeriod: 'month' as const,
status: 'active' as const,
Expand Down Expand Up @@ -668,9 +668,9 @@ describe('SubscriptionDetails', () => {
id: 'sub_annual',
plan,
createdAt: new Date('2021-01-01'),
periodStartDate: new Date('2021-01-01'),
periodEndDate: new Date('2022-01-01'),
canceledAtDate: new Date('2021-04-01'),
periodStart: new Date('2021-01-01'),
periodEnd: new Date('2022-01-01'),
canceledAt: new Date('2021-04-01'),
paymentSourceId: 'src_annual',
planPeriod: 'annual' as const,
status: 'active' as const,
Expand Down Expand Up @@ -755,9 +755,9 @@ describe('SubscriptionDetails', () => {
id: 'sub_annual',
plan,
createdAt: new Date('2021-01-01'),
periodStartDate: new Date('2021-01-01'),
periodEndDate: new Date('2022-01-01'),
canceledAtDate: null,
periodStart: new Date('2021-01-01'),
periodEnd: new Date('2022-01-01'),
canceledAt: null,
paymentSourceId: 'src_annual',
planPeriod: 'annual' as const,
status: 'active' as const,
Expand Down Expand Up @@ -862,9 +862,9 @@ describe('SubscriptionDetails', () => {
id: 'sub_past_due',
plan,
createdAt: new Date('2021-01-01'),
periodStartDate: new Date('2021-01-01'),
periodEndDate: new Date('2021-02-01'),
canceledAtDate: null,
periodStart: new Date('2021-01-01'),
periodEnd: new Date('2021-02-01'),
canceledAt: null,
paymentSourceId: 'src_123',
planPeriod: 'month' as const,
status: 'past_due' as const,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ const SubscriptionDetailsFooter = withCardStateProvider(() => {
: localizationKeys('commerce.cancelSubscriptionAccessUntil', {
plan: selectedSubscription.plan.name,
// this will always be defined in this state
date: selectedSubscription.periodEndDate as Date,
date: selectedSubscription.periodEnd as Date,
})
}
/>
Expand Down Expand Up @@ -339,8 +339,8 @@ const SubscriptionCardActions = ({ subscription }: { subscription: CommerceSubsc
subscription.planPeriod === 'annual') &&
subscription.status !== 'past_due';
const isFree = isFreePlan(subscription.plan);
const isCancellable = subscription.canceledAtDate === null && !isFree && subscription.status !== 'past_due';
const isReSubscribable = subscription.canceledAtDate !== null && !isFree;
const isCancellable = subscription.canceledAt === null && !isFree && subscription.status !== 'past_due';
const isReSubscribable = subscription.canceledAt !== null && !isFree;

const openCheckout = useCallback(
(params?: __internal_CheckoutProps) => {
Expand Down Expand Up @@ -523,14 +523,14 @@ const SubscriptionCard = ({ subscription }: { subscription: CommerceSubscription
value={formatDate(subscription.createdAt)}
/>
{/* The free plan does not have a period end date */}
{subscription.periodEndDate && (
{subscription.periodEnd && (
<DetailRow
label={
subscription.canceledAtDate
subscription.canceledAt
? localizationKeys('commerce.subscriptionDetails.endsOn')
: localizationKeys('commerce.subscriptionDetails.renewsAt')
}
value={formatDate(subscription.periodEndDate)}
value={formatDate(subscription.periodEnd)}
/>
)}
</>
Expand All @@ -539,7 +539,7 @@ const SubscriptionCard = ({ subscription }: { subscription: CommerceSubscription
{subscription.status === 'upcoming' ? (
<DetailRow
label={localizationKeys('commerce.subscriptionDetails.beginsOn')}
value={formatDate(subscription.periodStartDate)}
value={formatDate(subscription.periodStart)}
/>
) : null}
</Col>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export function SubscriptionsList({
>
{subscription.plan.name}
</Text>
{sortedSubscriptions.length > 1 || !!subscription.canceledAtDate ? (
{sortedSubscriptions.length > 1 || !!subscription.canceledAt ? (
<SubscriptionBadge subscription={subscription} />
) : null}
</Flex>
Expand Down
18 changes: 9 additions & 9 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export const usePlansContext = () => {
// should the default plan be shown as active
const isDefaultPlanImplicitlyActiveOrUpcoming = useMemo(() => {
// are there no subscriptions or are all subscriptions canceled
return subscriptionItems.length === 0 || !subscriptionItems.some(subscription => !subscription.canceledAtDate);
return subscriptionItems.length === 0 || !subscriptionItems.some(subscription => !subscription.canceledAt);
}, [subscriptionItems]);

// return the active or upcoming subscription for a plan if it exists
Expand Down Expand Up @@ -178,7 +178,7 @@ export const usePlansContext = () => {
({ plan, subscription: sub }: { plan?: CommercePlanResource; subscription?: CommerceSubscriptionItemResource }) => {
const subscription = sub ?? (plan ? activeOrUpcomingSubscription(plan) : undefined);

return !subscription || !subscription.canceledAtDate;
return !subscription || !subscription.canceledAt;
},
[activeOrUpcomingSubscription],
);
Expand Down Expand Up @@ -214,7 +214,7 @@ export const usePlansContext = () => {
const getLocalizationKey = () => {
// Handle subscription cases
if (subscription) {
if (_selectedPlanPeriod !== subscription.planPeriod && subscription.canceledAtDate) {
if (_selectedPlanPeriod !== subscription.planPeriod && subscription.canceledAt) {
if (_selectedPlanPeriod === 'month') {
return localizationKeys('commerce.switchToMonthly');
}
Expand All @@ -224,7 +224,7 @@ export const usePlansContext = () => {
}
}

if (subscription.canceledAtDate) {
if (subscription.canceledAt) {
return localizationKeys('commerce.reSubscribe');
}

Expand Down Expand Up @@ -268,14 +268,14 @@ export const usePlansContext = () => {
}

if (subscription.status === 'upcoming') {
return localizationKeys('badge__startsAt', { date: subscription.periodStartDate });
return localizationKeys('badge__startsAt', { date: subscription.periodStart });
}
if (subscription.canceledAtDate) {
if (subscription.canceledAt) {
// @ts-expect-error `periodEndDate` is always defined when `canceledAtDate` exists
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEndDate });
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
}
if (subscription.periodEndDate) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEndDate });
if (subscription.periodEnd) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEnd });
}
Comment on lines +271 to 279
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Remove stale ts-expect-error and add a type-safe guard

The @ts-expect-error comment references old names and hides a legitimate nullable type. Guard on periodEnd instead of suppressing types.

Apply:

-    if (subscription.canceledAt) {
-      // @ts-expect-error `periodEndDate` is always defined when `canceledAtDate` exists
-      return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
-    }
+    if (subscription.canceledAt && subscription.periodEnd) {
+      return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
+    }

This reflects the new invariant and keeps the code type-safe.

📝 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
return localizationKeys('badge__startsAt', { date: subscription.periodStart });
}
if (subscription.canceledAtDate) {
if (subscription.canceledAt) {
// @ts-expect-error `periodEndDate` is always defined when `canceledAtDate` exists
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEndDate });
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
}
if (subscription.periodEndDate) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEndDate });
if (subscription.periodEnd) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEnd });
}
return localizationKeys('badge__startsAt', { date: subscription.periodStart });
}
if (subscription.canceledAt && subscription.periodEnd) {
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
}
if (subscription.periodEnd) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEnd });
}
🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/contexts/components/Plans.tsx around lines 271 to
279, remove the stale "@ts-expect-error" and make the branch type-safe by
guarding on subscription.periodEnd (e.g. change the canceled branch to if
(subscription.canceledAt && subscription.periodEnd)) so you only access
periodEnd when it's defined; update the localization call to use the existing
periodEnd property and delete the obsolete comment.

return;
}, []);
Expand Down
18 changes: 3 additions & 15 deletions packages/types/src/commerce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,7 @@ export interface CommerceSubscriptionItemResource extends ClerkResource {
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
periodStartDate: Date;
periodStart: Date;
/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
Expand All @@ -1047,7 +1047,7 @@ export interface CommerceSubscriptionItemResource extends ClerkResource {
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
periodEndDate: Date | null;
periodEnd: Date | null;
/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
Expand All @@ -1056,19 +1056,7 @@ export interface CommerceSubscriptionItemResource extends ClerkResource {
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
canceledAtDate: Date | null;
/**
* @deprecated Use `periodStartDate` instead
*/
periodStart: number;
/**
* @deprecated Use `periodEndDate` instead
*/
periodEnd: number;
/**
* @deprecated Use `canceledAtDate` instead
*/
canceledAt: number | null;
canceledAt: Date | null;
/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
Expand Down
5 changes: 4 additions & 1 deletion packages/types/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -777,7 +777,10 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON {
status: CommerceSubscriptionStatus;
created_at: number;
period_start: number;
period_end: number;
/**
* Period end is `null` for subscription items that are on the free plan.
*/
period_end: number | null;
canceled_at: number | null;
past_due_at: number | null;
}
Expand Down
Loading