Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions .changeset/funny-ducks-boil.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Convert date properties from number to Date in CommerceSubscriptionResource
Deprecates fields of type `number`
- subscription.periodStart
- subscription.periodEnd
- subscription.canceledAt
Introduces fields of type `Date`
- subscription.periodStartDate
- subscription.periodEndDate
- subscription.canceledAtDate
- subscription.createdAt
13 changes: 13 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@ import type {
DeletedObjectJSON,
} from '@clerk/types';

import { unixEpochToDate } from '@/utils/date';

import { commerceMoneyFromJSON } from '../../utils';
import { BaseResource, CommercePlan, DeletedObject } from './internal';

Expand All@@ -17,13 +19,18 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
plan!: CommercePlan;
planPeriod!: CommerceSubscriptionPlanPeriod;
status!: CommerceSubscriptionStatus;
createdAt!: Date;
periodStartDate!: Date;
periodEndDate!: Date | null;
canceledAtDate!: Date | null;
periodStart!: number;
periodEnd!: number;
canceledAt!: number | null;
amount?: CommerceMoney;
credit?: {
amount: CommerceMoney;
};

constructor(data: CommerceSubscriptionJSON) {
super();
this.fromJSON(data);
Expand All@@ -42,6 +49,12 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
this.periodStart = data.period_start;
this.periodEnd = data.period_end;
this.canceledAt = data.canceled_at;

this.createdAt = unixEpochToDate(data.created_at);
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;
Comment on lines +53 to +56

Copy link
Copy Markdown
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

Verify the unixEpochToDate utility function

The conversion logic looks correct, but ensure the unixEpochToDate utility function properly handles edge cases.


🏁 Script executed:

#!/bin/bash# Description: Verify the unixEpochToDate utility function exists and examine its implementation# Search for the utility function definition
ast-grep --pattern 'function unixEpochToDate($_) { $$$}'# Also search for arrow function or const declaration
rg -A 10 "unixEpochToDate.*=" src/utils/date

Length of output: 163


I couldn’t locate unixEpochToDate in the initial paths—let’s do a global search to find its definition and usages.


🏁 Script executed:

#!/bin/bash# Search for definition and all usages of unixEpochToDate across the repo
rg -n "unixEpochToDate"

Length of output: 10950


Let’s inspect the implementation and its tests:


🏁 Script executed:

#!/bin/bash# Show the unixEpochToDate implementation
sed -n '1,200p' packages/clerk-js/src/utils/date.ts
# Show tests covering edge cases
sed -n '1,200p' packages/clerk-js/src/utils/__tests__/date.spec.ts

Length of output: 1264


Improve zero-timestamp handling in unixEpochToDate

The current implementation

exportfunctionunixEpochToDate(epochInSeconds?: number): Date{constdate=newDate(epochInSeconds||newDate());returnisValidDate(date) ? date : newDate();}

uses a falsy check (epochInSeconds || …), so passing 0 (the Unix epoch) falls back to Date.now(). To correctly support 0, please:

  • In packages/clerk-js/src/utils/date.ts, update the guard:
    export function unixEpochToDate(epochInSeconds?: number): Date {
  • const date = new Date(epochInSeconds || new Date());
  • const timestamp = epochInSeconds != null ? epochInSeconds : Date.now();
  • const date = new Date(timestamp);
    return isValidDate(date) ? date : new Date();
    }
- In `packages/clerk-js/src/utils/__tests__/date.spec.ts`, add a zero-timestamp test:
```ts
it('correctly handles zero', () => {
const date = unixEpochToDate(0);
expect(date.getTime()).toBe(0);
});

This change ensures unixEpochToDate(0) returns the Unix epoch rather than “now.”

🤖 Prompt for AI Agents
In packages/clerk-js/src/utils/date.ts, update the unixEpochToDate function to
explicitly check for undefined or null instead of using a falsy check, so that
passing 0 returns the correct Unix epoch date. Then, in
packages/clerk-js/src/utils/__tests__/date.spec.ts, add a test case that calls
unixEpochToDate(0) and asserts that the returned date's time is 0 to verify
correct handling of the zero timestamp.


this.amount = data.amount ? commerceMoneyFromJSON(data.amount) : undefined;
this.credit = data.credit && data.credit.amount ? { amount: commerceMoneyFromJSON(data.credit.amount) } : undefined;
return this;
Expand Down
5 changes: 3 additions & 2 deletions packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -210,7 +210,7 @@ const PlanDetailsInternal = ({
{(!plan.isDefault && !isDefaultPlanImplicitlyActiveOrUpcoming) || !subscription ? (
<Drawer.Footer>
{subscription ? (
subscription.canceledAt ? (
subscription.canceledAtDate ? (
<Button
block
textVariant='buttonLarge'
Expand DownExpand Up@@ -318,7 +318,8 @@ const PlanDetailsInternal = ({
? localizationKeys('commerce.cancelSubscriptionNoCharge')
: localizationKeys('commerce.cancelSubscriptionAccessUntil', {
plan: subscription.plan.name,
date: subscription.periodEnd,
// @ts-expect-error this will always be defined in this case.
date: subscription.periodEndDate,
Comment on lines +321 to +322

Copy link
Copy Markdown
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

Verify the TypeScript suppression is justified.

The migration from periodEnd to periodEndDate is consistent with the pattern. However, the @ts-expect-error comment suggests a type issue. Please ensure that the property is indeed always defined in this context or consider updating the type definitions.


🏁 Script executed:

#!/bin/bash# Description: Check the type definition of periodEndDate in CommerceSubscriptionResource# Expected: Verify if periodEndDate is optional or required in the type definition
ast-grep --pattern 'interface CommerceSubscriptionResource { $$$ periodEndDate$_$$$ $$$}'# Also search for any type definitions related to subscription period dates
rg -A 3 -B 3 "periodEndDate.*[?:]"

Length of output: 2751


Fix the TypeScript suppression for periodEndDate
The CommerceSubscriptionResource model declares periodEndDate as Date | null, so subscription.periodEndDate may legitimately be null. Suppressing the error hides a potential runtime issue. Please remove the @ts-expect-error and either:

  • Add an explicit null check before using it, e.g.
    if(!subscription.periodEndDate){// handle missing date…}else{date: subscription.periodEndDate,}
  • Or use a non-null assertion if you’re certain it can’t be null in this context:
    - // @ts-expect-error this will always be defined in this case.- date: subscription.periodEndDate,+ date: subscription.periodEndDate!,

Locations to update:

  • packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx (lines 321–322)
  • Note: the type in packages/types/src/commerce.ts is periodEndDate: Date | null;
🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx at lines 321-322,
remove the @ts-expect-error comment on subscription.periodEndDate and address
its nullable type properly. Add an explicit null check before using
subscription.periodEndDate to handle the case when it is null, or if you are
certain it cannot be null here, replace the suppression with a non-null
assertion operator to ensure type safety without hiding potential runtime
issues.

})
}
/>
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const PricingTableRoot = (props: PricingTableProps) => {

// don't pay attention to the default plan
const activeSubscription = subscriptions?.find(
sub => !sub.canceledAt && sub.status === 'active' && !sub.plan.isDefault,
sub => !sub.canceledAtDate && sub.status === 'active' && !sub.plan.isDefault,
);
if (activeSubscription) {
return activeSubscription.planPeriod;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -143,7 +143,7 @@ function Card(props: CardProps) {
shouldShowFooter = true;
shouldShowFooterNotice = true;
} else if (subscription.status === 'active') {
if (subscription.canceledAt) {
if (subscription.canceledAtDate) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyAmount > 0) {
Expand DownExpand Up@@ -253,7 +253,7 @@ function Card(props: CardProps) {
elementDescriptor={descriptors.pricingTableCardFooterNotice}
variant={isCompact ? 'buttonSmall' : 'buttonLarge'}
localizationKey={localizationKeys('badge__startsAt', {
date: subscription?.periodStart,
date: subscription?.periodStartDate,
})}
colorScheme='secondary'
sx={t => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ export function SubscriptionsList({
>
{subscription.plan.name}
</Text>
{sortedSubscriptions.length > 1 || !!subscription.canceledAt ? (
{sortedSubscriptions.length > 1 || !!subscription.canceledAtDate ? (
<Badge
colorScheme={subscription.status === 'active' ? 'secondary' : 'primary'}
localizationKey={
Expand Down
31 changes: 18 additions & 13 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,9 +98,9 @@ export const useSubscriptions = () => {
if (
isSignedIn &&
defaultFreePlan &&
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAt))
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAtDate))
) {
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAt);
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAtDate);
return [
..._subscriptions.data,
new CommerceSubscription({
Expand All@@ -111,7 +111,8 @@ export const useSubscriptions = () => {
plan_period: 'month',
canceled_at: null,
status: _subscriptions.data.length === 0 ? 'active' : 'upcoming',
period_start: canceledSubscription?.periodEnd || 0,
created_at: canceledSubscription?.periodEndDate?.getTime() || 0,
period_start: canceledSubscription?.periodEndDate?.getTime() || 0,
period_end: 0,
}),
];
Expand DownExpand Up@@ -190,7 +191,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 subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAt);
return subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAtDate);
}, [subscriptions]);

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

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

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

Expand DownExpand Up@@ -332,12 +333,16 @@ export const usePlansContext = () => {

const captionForSubscription = useCallback((subscription: CommerceSubscriptionResource) => {
if (subscription.status === 'upcoming') {
return localizationKeys('badge__startsAt', { date: subscription.periodStart });
} else if (subscription.canceledAt) {
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
} else {
return localizationKeys('badge__renewsAt', { date: subscription.periodEnd });
return localizationKeys('badge__startsAt', { date: subscription.periodStartDate });
}
if (subscription.canceledAtDate) {
// @ts-expect-error `periodEndDate` is always defined when `canceledAtDate` exists
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEndDate });
}
if (subscription.periodEndDate) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEndDate });
}
return;
}, []);

// handle the selection of a plan, either by opening the subscription details or checkout
Expand All@@ -355,7 +360,7 @@ export const usePlansContext = () => {

const portalRoot = getClosestProfileScrollBox(mode, event);

if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAt) {
if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAtDate) {
clerk.__internal_openPlanDetails({
plan,
initialPlanPeriod: planPeriod,
Expand Down
13 changes: 13 additions & 0 deletions packages/types/src/commerce.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -155,8 +155,21 @@ export interface CommerceSubscriptionResource extends ClerkResource {
plan: CommercePlanResource;
planPeriod: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
createdAt: Date;
periodStartDate: Date;
periodEndDate: Date | null;
canceledAtDate: Date | null;
/**
* @deprecated Use `periodStartDate` instead
*/
periodStart: number;
/**
* @deprecated Use `periodEndDate` instead
*/
periodEnd: number;
/**
* @deprecated Use `canceledAtDate` instead
*/
canceledAt: number | null;
amount?: CommerceMoney;
credit?: {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/json.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,6 +701,7 @@ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
plan: CommercePlanJSON;
plan_period: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
created_at: number;
period_start: number;
period_end: number;
canceled_at: number | null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions .changeset/funny-ducks-boil.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Convert date properties from number to Date in CommerceSubscriptionResource
Deprecates fields of type `number`
- subscription.periodStart
- subscription.periodEnd
- subscription.canceledAt
Introduces fields of type `Date`
- subscription.periodStartDate
- subscription.periodEndDate
- subscription.canceledAtDate
- subscription.createdAt
13 changes: 13 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@ import type {
DeletedObjectJSON,
} from '@clerk/types';

import { unixEpochToDate } from '@/utils/date';

import { commerceMoneyFromJSON } from '../../utils';
import { BaseResource, CommercePlan, DeletedObject } from './internal';

Expand All@@ -17,13 +19,18 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
plan!: CommercePlan;
planPeriod!: CommerceSubscriptionPlanPeriod;
status!: CommerceSubscriptionStatus;
createdAt!: Date;
periodStartDate!: Date;
periodEndDate!: Date | null;
canceledAtDate!: Date | null;
periodStart!: number;
periodEnd!: number;
canceledAt!: number | null;
amount?: CommerceMoney;
credit?: {
amount: CommerceMoney;
};

constructor(data: CommerceSubscriptionJSON) {
super();
this.fromJSON(data);
Expand All@@ -42,6 +49,12 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
this.periodStart = data.period_start;
this.periodEnd = data.period_end;
this.canceledAt = data.canceled_at;

this.createdAt = unixEpochToDate(data.created_at);
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;
Comment on lines +53 to +56

Copy link
Copy Markdown
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

Verify the unixEpochToDate utility function

The conversion logic looks correct, but ensure the unixEpochToDate utility function properly handles edge cases.


🏁 Script executed:

#!/bin/bash# Description: Verify the unixEpochToDate utility function exists and examine its implementation# Search for the utility function definition
ast-grep --pattern 'function unixEpochToDate($_) { $$$}'# Also search for arrow function or const declaration
rg -A 10 "unixEpochToDate.*=" src/utils/date

Length of output: 163


I couldn’t locate unixEpochToDate in the initial paths—let’s do a global search to find its definition and usages.


🏁 Script executed:

#!/bin/bash# Search for definition and all usages of unixEpochToDate across the repo
rg -n "unixEpochToDate"

Length of output: 10950


Let’s inspect the implementation and its tests:


🏁 Script executed:

#!/bin/bash# Show the unixEpochToDate implementation
sed -n '1,200p' packages/clerk-js/src/utils/date.ts
# Show tests covering edge cases
sed -n '1,200p' packages/clerk-js/src/utils/__tests__/date.spec.ts

Length of output: 1264


Improve zero-timestamp handling in unixEpochToDate

The current implementation

exportfunctionunixEpochToDate(epochInSeconds?: number): Date{constdate=newDate(epochInSeconds||newDate());returnisValidDate(date) ? date : newDate();}

uses a falsy check (epochInSeconds || …), so passing 0 (the Unix epoch) falls back to Date.now(). To correctly support 0, please:

  • In packages/clerk-js/src/utils/date.ts, update the guard:
    export function unixEpochToDate(epochInSeconds?: number): Date {
  • const date = new Date(epochInSeconds || new Date());
  • const timestamp = epochInSeconds != null ? epochInSeconds : Date.now();
  • const date = new Date(timestamp);
    return isValidDate(date) ? date : new Date();
    }
- In `packages/clerk-js/src/utils/__tests__/date.spec.ts`, add a zero-timestamp test:
```ts
it('correctly handles zero', () => {
const date = unixEpochToDate(0);
expect(date.getTime()).toBe(0);
});

This change ensures unixEpochToDate(0) returns the Unix epoch rather than “now.”

🤖 Prompt for AI Agents
In packages/clerk-js/src/utils/date.ts, update the unixEpochToDate function to
explicitly check for undefined or null instead of using a falsy check, so that
passing 0 returns the correct Unix epoch date. Then, in
packages/clerk-js/src/utils/__tests__/date.spec.ts, add a test case that calls
unixEpochToDate(0) and asserts that the returned date's time is 0 to verify
correct handling of the zero timestamp.


this.amount = data.amount ? commerceMoneyFromJSON(data.amount) : undefined;
this.credit = data.credit && data.credit.amount ? { amount: commerceMoneyFromJSON(data.credit.amount) } : undefined;
return this;
Expand Down
5 changes: 3 additions & 2 deletions packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -210,7 +210,7 @@ const PlanDetailsInternal = ({
{(!plan.isDefault && !isDefaultPlanImplicitlyActiveOrUpcoming) || !subscription ? (
<Drawer.Footer>
{subscription ? (
subscription.canceledAt ? (
subscription.canceledAtDate ? (
<Button
block
textVariant='buttonLarge'
Expand DownExpand Up@@ -318,7 +318,8 @@ const PlanDetailsInternal = ({
? localizationKeys('commerce.cancelSubscriptionNoCharge')
: localizationKeys('commerce.cancelSubscriptionAccessUntil', {
plan: subscription.plan.name,
date: subscription.periodEnd,
// @ts-expect-error this will always be defined in this case.
date: subscription.periodEndDate,
Comment on lines +321 to +322

Copy link
Copy Markdown
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

Verify the TypeScript suppression is justified.

The migration from periodEnd to periodEndDate is consistent with the pattern. However, the @ts-expect-error comment suggests a type issue. Please ensure that the property is indeed always defined in this context or consider updating the type definitions.


🏁 Script executed:

#!/bin/bash# Description: Check the type definition of periodEndDate in CommerceSubscriptionResource# Expected: Verify if periodEndDate is optional or required in the type definition
ast-grep --pattern 'interface CommerceSubscriptionResource { $$$ periodEndDate$_$$$ $$$}'# Also search for any type definitions related to subscription period dates
rg -A 3 -B 3 "periodEndDate.*[?:]"

Length of output: 2751


Fix the TypeScript suppression for periodEndDate
The CommerceSubscriptionResource model declares periodEndDate as Date | null, so subscription.periodEndDate may legitimately be null. Suppressing the error hides a potential runtime issue. Please remove the @ts-expect-error and either:

  • Add an explicit null check before using it, e.g.
    if(!subscription.periodEndDate){// handle missing date…}else{date: subscription.periodEndDate,}
  • Or use a non-null assertion if you’re certain it can’t be null in this context:
    - // @ts-expect-error this will always be defined in this case.- date: subscription.periodEndDate,+ date: subscription.periodEndDate!,

Locations to update:

  • packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx (lines 321–322)
  • Note: the type in packages/types/src/commerce.ts is periodEndDate: Date | null;
🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx at lines 321-322,
remove the @ts-expect-error comment on subscription.periodEndDate and address
its nullable type properly. Add an explicit null check before using
subscription.periodEndDate to handle the case when it is null, or if you are
certain it cannot be null here, replace the suppression with a non-null
assertion operator to ensure type safety without hiding potential runtime
issues.

})
}
/>
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const PricingTableRoot = (props: PricingTableProps) => {

// don't pay attention to the default plan
const activeSubscription = subscriptions?.find(
sub => !sub.canceledAt && sub.status === 'active' && !sub.plan.isDefault,
sub => !sub.canceledAtDate && sub.status === 'active' && !sub.plan.isDefault,
);
if (activeSubscription) {
return activeSubscription.planPeriod;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -143,7 +143,7 @@ function Card(props: CardProps) {
shouldShowFooter = true;
shouldShowFooterNotice = true;
} else if (subscription.status === 'active') {
if (subscription.canceledAt) {
if (subscription.canceledAtDate) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyAmount > 0) {
Expand DownExpand Up@@ -253,7 +253,7 @@ function Card(props: CardProps) {
elementDescriptor={descriptors.pricingTableCardFooterNotice}
variant={isCompact ? 'buttonSmall' : 'buttonLarge'}
localizationKey={localizationKeys('badge__startsAt', {
date: subscription?.periodStart,
date: subscription?.periodStartDate,
})}
colorScheme='secondary'
sx={t => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ export function SubscriptionsList({
>
{subscription.plan.name}
</Text>
{sortedSubscriptions.length > 1 || !!subscription.canceledAt ? (
{sortedSubscriptions.length > 1 || !!subscription.canceledAtDate ? (
<Badge
colorScheme={subscription.status === 'active' ? 'secondary' : 'primary'}
localizationKey={
Expand Down
31 changes: 18 additions & 13 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,9 +98,9 @@ export const useSubscriptions = () => {
if (
isSignedIn &&
defaultFreePlan &&
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAt))
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAtDate))
) {
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAt);
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAtDate);
return [
..._subscriptions.data,
new CommerceSubscription({
Expand All@@ -111,7 +111,8 @@ export const useSubscriptions = () => {
plan_period: 'month',
canceled_at: null,
status: _subscriptions.data.length === 0 ? 'active' : 'upcoming',
period_start: canceledSubscription?.periodEnd || 0,
created_at: canceledSubscription?.periodEndDate?.getTime() || 0,
period_start: canceledSubscription?.periodEndDate?.getTime() || 0,
period_end: 0,
}),
];
Expand DownExpand Up@@ -190,7 +191,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 subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAt);
return subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAtDate);
}, [subscriptions]);

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

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

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

Expand DownExpand Up@@ -332,12 +333,16 @@ export const usePlansContext = () => {

const captionForSubscription = useCallback((subscription: CommerceSubscriptionResource) => {
if (subscription.status === 'upcoming') {
return localizationKeys('badge__startsAt', { date: subscription.periodStart });
} else if (subscription.canceledAt) {
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
} else {
return localizationKeys('badge__renewsAt', { date: subscription.periodEnd });
return localizationKeys('badge__startsAt', { date: subscription.periodStartDate });
}
if (subscription.canceledAtDate) {
// @ts-expect-error `periodEndDate` is always defined when `canceledAtDate` exists
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEndDate });
}
if (subscription.periodEndDate) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEndDate });
}
return;
}, []);

// handle the selection of a plan, either by opening the subscription details or checkout
Expand All@@ -355,7 +360,7 @@ export const usePlansContext = () => {

const portalRoot = getClosestProfileScrollBox(mode, event);

if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAt) {
if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAtDate) {
clerk.__internal_openPlanDetails({
plan,
initialPlanPeriod: planPeriod,
Expand Down
13 changes: 13 additions & 0 deletions packages/types/src/commerce.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -155,8 +155,21 @@ export interface CommerceSubscriptionResource extends ClerkResource {
plan: CommercePlanResource;
planPeriod: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
createdAt: Date;
periodStartDate: Date;
periodEndDate: Date | null;
canceledAtDate: Date | null;
/**
* @deprecated Use `periodStartDate` instead
*/
periodStart: number;
/**
* @deprecated Use `periodEndDate` instead
*/
periodEnd: number;
/**
* @deprecated Use `canceledAtDate` instead
*/
canceledAt: number | null;
amount?: CommerceMoney;
credit?: {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/json.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,6 +701,7 @@ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
plan: CommercePlanJSON;
plan_period: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
created_at: number;
period_start: number;
period_end: number;
canceled_at: number | null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions .changeset/funny-ducks-boil.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Convert date properties from number to Date in CommerceSubscriptionResource
Deprecates fields of type `number`
- subscription.periodStart
- subscription.periodEnd
- subscription.canceledAt
Introduces fields of type `Date`
- subscription.periodStartDate
- subscription.periodEndDate
- subscription.canceledAtDate
- subscription.createdAt
13 changes: 13 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@ import type {
DeletedObjectJSON,
} from '@clerk/types';

import { unixEpochToDate } from '@/utils/date';

import { commerceMoneyFromJSON } from '../../utils';
import { BaseResource, CommercePlan, DeletedObject } from './internal';

Expand All@@ -17,13 +19,18 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
plan!: CommercePlan;
planPeriod!: CommerceSubscriptionPlanPeriod;
status!: CommerceSubscriptionStatus;
createdAt!: Date;
periodStartDate!: Date;
periodEndDate!: Date | null;
canceledAtDate!: Date | null;
periodStart!: number;
periodEnd!: number;
canceledAt!: number | null;
amount?: CommerceMoney;
credit?: {
amount: CommerceMoney;
};

constructor(data: CommerceSubscriptionJSON) {
super();
this.fromJSON(data);
Expand All@@ -42,6 +49,12 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
this.periodStart = data.period_start;
this.periodEnd = data.period_end;
this.canceledAt = data.canceled_at;

this.createdAt = unixEpochToDate(data.created_at);
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;
Comment on lines +53 to +56

Copy link
Copy Markdown
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

Verify the unixEpochToDate utility function

The conversion logic looks correct, but ensure the unixEpochToDate utility function properly handles edge cases.


🏁 Script executed:

#!/bin/bash# Description: Verify the unixEpochToDate utility function exists and examine its implementation# Search for the utility function definition
ast-grep --pattern 'function unixEpochToDate($_) { $$$}'# Also search for arrow function or const declaration
rg -A 10 "unixEpochToDate.*=" src/utils/date

Length of output: 163


I couldn’t locate unixEpochToDate in the initial paths—let’s do a global search to find its definition and usages.


🏁 Script executed:

#!/bin/bash# Search for definition and all usages of unixEpochToDate across the repo
rg -n "unixEpochToDate"

Length of output: 10950


Let’s inspect the implementation and its tests:


🏁 Script executed:

#!/bin/bash# Show the unixEpochToDate implementation
sed -n '1,200p' packages/clerk-js/src/utils/date.ts
# Show tests covering edge cases
sed -n '1,200p' packages/clerk-js/src/utils/__tests__/date.spec.ts

Length of output: 1264


Improve zero-timestamp handling in unixEpochToDate

The current implementation

exportfunctionunixEpochToDate(epochInSeconds?: number): Date{constdate=newDate(epochInSeconds||newDate());returnisValidDate(date) ? date : newDate();}

uses a falsy check (epochInSeconds || …), so passing 0 (the Unix epoch) falls back to Date.now(). To correctly support 0, please:

  • In packages/clerk-js/src/utils/date.ts, update the guard:
    export function unixEpochToDate(epochInSeconds?: number): Date {
  • const date = new Date(epochInSeconds || new Date());
  • const timestamp = epochInSeconds != null ? epochInSeconds : Date.now();
  • const date = new Date(timestamp);
    return isValidDate(date) ? date : new Date();
    }
- In `packages/clerk-js/src/utils/__tests__/date.spec.ts`, add a zero-timestamp test:
```ts
it('correctly handles zero', () => {
const date = unixEpochToDate(0);
expect(date.getTime()).toBe(0);
});

This change ensures unixEpochToDate(0) returns the Unix epoch rather than “now.”

🤖 Prompt for AI Agents
In packages/clerk-js/src/utils/date.ts, update the unixEpochToDate function to
explicitly check for undefined or null instead of using a falsy check, so that
passing 0 returns the correct Unix epoch date. Then, in
packages/clerk-js/src/utils/__tests__/date.spec.ts, add a test case that calls
unixEpochToDate(0) and asserts that the returned date's time is 0 to verify
correct handling of the zero timestamp.


this.amount = data.amount ? commerceMoneyFromJSON(data.amount) : undefined;
this.credit = data.credit && data.credit.amount ? { amount: commerceMoneyFromJSON(data.credit.amount) } : undefined;
return this;
Expand Down
5 changes: 3 additions & 2 deletions packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -210,7 +210,7 @@ const PlanDetailsInternal = ({
{(!plan.isDefault && !isDefaultPlanImplicitlyActiveOrUpcoming) || !subscription ? (
<Drawer.Footer>
{subscription ? (
subscription.canceledAt ? (
subscription.canceledAtDate ? (
<Button
block
textVariant='buttonLarge'
Expand DownExpand Up@@ -318,7 +318,8 @@ const PlanDetailsInternal = ({
? localizationKeys('commerce.cancelSubscriptionNoCharge')
: localizationKeys('commerce.cancelSubscriptionAccessUntil', {
plan: subscription.plan.name,
date: subscription.periodEnd,
// @ts-expect-error this will always be defined in this case.
date: subscription.periodEndDate,
Comment on lines +321 to +322

Copy link
Copy Markdown
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

Verify the TypeScript suppression is justified.

The migration from periodEnd to periodEndDate is consistent with the pattern. However, the @ts-expect-error comment suggests a type issue. Please ensure that the property is indeed always defined in this context or consider updating the type definitions.


🏁 Script executed:

#!/bin/bash# Description: Check the type definition of periodEndDate in CommerceSubscriptionResource# Expected: Verify if periodEndDate is optional or required in the type definition
ast-grep --pattern 'interface CommerceSubscriptionResource { $$$ periodEndDate$_$$$ $$$}'# Also search for any type definitions related to subscription period dates
rg -A 3 -B 3 "periodEndDate.*[?:]"

Length of output: 2751


Fix the TypeScript suppression for periodEndDate
The CommerceSubscriptionResource model declares periodEndDate as Date | null, so subscription.periodEndDate may legitimately be null. Suppressing the error hides a potential runtime issue. Please remove the @ts-expect-error and either:

  • Add an explicit null check before using it, e.g.
    if(!subscription.periodEndDate){// handle missing date…}else{date: subscription.periodEndDate,}
  • Or use a non-null assertion if you’re certain it can’t be null in this context:
    - // @ts-expect-error this will always be defined in this case.- date: subscription.periodEndDate,+ date: subscription.periodEndDate!,

Locations to update:

  • packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx (lines 321–322)
  • Note: the type in packages/types/src/commerce.ts is periodEndDate: Date | null;
🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx at lines 321-322,
remove the @ts-expect-error comment on subscription.periodEndDate and address
its nullable type properly. Add an explicit null check before using
subscription.periodEndDate to handle the case when it is null, or if you are
certain it cannot be null here, replace the suppression with a non-null
assertion operator to ensure type safety without hiding potential runtime
issues.

})
}
/>
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const PricingTableRoot = (props: PricingTableProps) => {

// don't pay attention to the default plan
const activeSubscription = subscriptions?.find(
sub => !sub.canceledAt && sub.status === 'active' && !sub.plan.isDefault,
sub => !sub.canceledAtDate && sub.status === 'active' && !sub.plan.isDefault,
);
if (activeSubscription) {
return activeSubscription.planPeriod;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -143,7 +143,7 @@ function Card(props: CardProps) {
shouldShowFooter = true;
shouldShowFooterNotice = true;
} else if (subscription.status === 'active') {
if (subscription.canceledAt) {
if (subscription.canceledAtDate) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyAmount > 0) {
Expand DownExpand Up@@ -253,7 +253,7 @@ function Card(props: CardProps) {
elementDescriptor={descriptors.pricingTableCardFooterNotice}
variant={isCompact ? 'buttonSmall' : 'buttonLarge'}
localizationKey={localizationKeys('badge__startsAt', {
date: subscription?.periodStart,
date: subscription?.periodStartDate,
})}
colorScheme='secondary'
sx={t => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ export function SubscriptionsList({
>
{subscription.plan.name}
</Text>
{sortedSubscriptions.length > 1 || !!subscription.canceledAt ? (
{sortedSubscriptions.length > 1 || !!subscription.canceledAtDate ? (
<Badge
colorScheme={subscription.status === 'active' ? 'secondary' : 'primary'}
localizationKey={
Expand Down
31 changes: 18 additions & 13 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,9 +98,9 @@ export const useSubscriptions = () => {
if (
isSignedIn &&
defaultFreePlan &&
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAt))
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAtDate))
) {
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAt);
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAtDate);
return [
..._subscriptions.data,
new CommerceSubscription({
Expand All@@ -111,7 +111,8 @@ export const useSubscriptions = () => {
plan_period: 'month',
canceled_at: null,
status: _subscriptions.data.length === 0 ? 'active' : 'upcoming',
period_start: canceledSubscription?.periodEnd || 0,
created_at: canceledSubscription?.periodEndDate?.getTime() || 0,
period_start: canceledSubscription?.periodEndDate?.getTime() || 0,
period_end: 0,
}),
];
Expand DownExpand Up@@ -190,7 +191,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 subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAt);
return subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAtDate);
}, [subscriptions]);

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

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

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

Expand DownExpand Up@@ -332,12 +333,16 @@ export const usePlansContext = () => {

const captionForSubscription = useCallback((subscription: CommerceSubscriptionResource) => {
if (subscription.status === 'upcoming') {
return localizationKeys('badge__startsAt', { date: subscription.periodStart });
} else if (subscription.canceledAt) {
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
} else {
return localizationKeys('badge__renewsAt', { date: subscription.periodEnd });
return localizationKeys('badge__startsAt', { date: subscription.periodStartDate });
}
if (subscription.canceledAtDate) {
// @ts-expect-error `periodEndDate` is always defined when `canceledAtDate` exists
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEndDate });
}
if (subscription.periodEndDate) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEndDate });
}
return;
}, []);

// handle the selection of a plan, either by opening the subscription details or checkout
Expand All@@ -355,7 +360,7 @@ export const usePlansContext = () => {

const portalRoot = getClosestProfileScrollBox(mode, event);

if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAt) {
if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAtDate) {
clerk.__internal_openPlanDetails({
plan,
initialPlanPeriod: planPeriod,
Expand Down
13 changes: 13 additions & 0 deletions packages/types/src/commerce.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -155,8 +155,21 @@ export interface CommerceSubscriptionResource extends ClerkResource {
plan: CommercePlanResource;
planPeriod: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
createdAt: Date;
periodStartDate: Date;
periodEndDate: Date | null;
canceledAtDate: Date | null;
/**
* @deprecated Use `periodStartDate` instead
*/
periodStart: number;
/**
* @deprecated Use `periodEndDate` instead
*/
periodEnd: number;
/**
* @deprecated Use `canceledAtDate` instead
*/
canceledAt: number | null;
amount?: CommerceMoney;
credit?: {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/json.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,6 +701,7 @@ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
plan: CommercePlanJSON;
plan_period: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
created_at: number;
period_start: number;
period_end: number;
canceled_at: number | null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions .changeset/funny-ducks-boil.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Convert date properties from number to Date in CommerceSubscriptionResource
Deprecates fields of type `number`
- subscription.periodStart
- subscription.periodEnd
- subscription.canceledAt
Introduces fields of type `Date`
- subscription.periodStartDate
- subscription.periodEndDate
- subscription.canceledAtDate
- subscription.createdAt
13 changes: 13 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@ import type {
DeletedObjectJSON,
} from '@clerk/types';

import { unixEpochToDate } from '@/utils/date';

import { commerceMoneyFromJSON } from '../../utils';
import { BaseResource, CommercePlan, DeletedObject } from './internal';

Expand All@@ -17,13 +19,18 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
plan!: CommercePlan;
planPeriod!: CommerceSubscriptionPlanPeriod;
status!: CommerceSubscriptionStatus;
createdAt!: Date;
periodStartDate!: Date;
periodEndDate!: Date | null;
canceledAtDate!: Date | null;
periodStart!: number;
periodEnd!: number;
canceledAt!: number | null;
amount?: CommerceMoney;
credit?: {
amount: CommerceMoney;
};

constructor(data: CommerceSubscriptionJSON) {
super();
this.fromJSON(data);
Expand All@@ -42,6 +49,12 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
this.periodStart = data.period_start;
this.periodEnd = data.period_end;
this.canceledAt = data.canceled_at;

this.createdAt = unixEpochToDate(data.created_at);
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;
Comment on lines +53 to +56

Copy link
Copy Markdown
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

Verify the unixEpochToDate utility function

The conversion logic looks correct, but ensure the unixEpochToDate utility function properly handles edge cases.


🏁 Script executed:

#!/bin/bash# Description: Verify the unixEpochToDate utility function exists and examine its implementation# Search for the utility function definition
ast-grep --pattern 'function unixEpochToDate($_) { $$$}'# Also search for arrow function or const declaration
rg -A 10 "unixEpochToDate.*=" src/utils/date

Length of output: 163


I couldn’t locate unixEpochToDate in the initial paths—let’s do a global search to find its definition and usages.


🏁 Script executed:

#!/bin/bash# Search for definition and all usages of unixEpochToDate across the repo
rg -n "unixEpochToDate"

Length of output: 10950


Let’s inspect the implementation and its tests:


🏁 Script executed:

#!/bin/bash# Show the unixEpochToDate implementation
sed -n '1,200p' packages/clerk-js/src/utils/date.ts
# Show tests covering edge cases
sed -n '1,200p' packages/clerk-js/src/utils/__tests__/date.spec.ts

Length of output: 1264


Improve zero-timestamp handling in unixEpochToDate

The current implementation

exportfunctionunixEpochToDate(epochInSeconds?: number): Date{constdate=newDate(epochInSeconds||newDate());returnisValidDate(date) ? date : newDate();}

uses a falsy check (epochInSeconds || …), so passing 0 (the Unix epoch) falls back to Date.now(). To correctly support 0, please:

  • In packages/clerk-js/src/utils/date.ts, update the guard:
    export function unixEpochToDate(epochInSeconds?: number): Date {
  • const date = new Date(epochInSeconds || new Date());
  • const timestamp = epochInSeconds != null ? epochInSeconds : Date.now();
  • const date = new Date(timestamp);
    return isValidDate(date) ? date : new Date();
    }
- In `packages/clerk-js/src/utils/__tests__/date.spec.ts`, add a zero-timestamp test:
```ts
it('correctly handles zero', () => {
const date = unixEpochToDate(0);
expect(date.getTime()).toBe(0);
});

This change ensures unixEpochToDate(0) returns the Unix epoch rather than “now.”

🤖 Prompt for AI Agents
In packages/clerk-js/src/utils/date.ts, update the unixEpochToDate function to
explicitly check for undefined or null instead of using a falsy check, so that
passing 0 returns the correct Unix epoch date. Then, in
packages/clerk-js/src/utils/__tests__/date.spec.ts, add a test case that calls
unixEpochToDate(0) and asserts that the returned date's time is 0 to verify
correct handling of the zero timestamp.


this.amount = data.amount ? commerceMoneyFromJSON(data.amount) : undefined;
this.credit = data.credit && data.credit.amount ? { amount: commerceMoneyFromJSON(data.credit.amount) } : undefined;
return this;
Expand Down
5 changes: 3 additions & 2 deletions packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -210,7 +210,7 @@ const PlanDetailsInternal = ({
{(!plan.isDefault && !isDefaultPlanImplicitlyActiveOrUpcoming) || !subscription ? (
<Drawer.Footer>
{subscription ? (
subscription.canceledAt ? (
subscription.canceledAtDate ? (
<Button
block
textVariant='buttonLarge'
Expand DownExpand Up@@ -318,7 +318,8 @@ const PlanDetailsInternal = ({
? localizationKeys('commerce.cancelSubscriptionNoCharge')
: localizationKeys('commerce.cancelSubscriptionAccessUntil', {
plan: subscription.plan.name,
date: subscription.periodEnd,
// @ts-expect-error this will always be defined in this case.
date: subscription.periodEndDate,
Comment on lines +321 to +322

Copy link
Copy Markdown
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

Verify the TypeScript suppression is justified.

The migration from periodEnd to periodEndDate is consistent with the pattern. However, the @ts-expect-error comment suggests a type issue. Please ensure that the property is indeed always defined in this context or consider updating the type definitions.


🏁 Script executed:

#!/bin/bash# Description: Check the type definition of periodEndDate in CommerceSubscriptionResource# Expected: Verify if periodEndDate is optional or required in the type definition
ast-grep --pattern 'interface CommerceSubscriptionResource { $$$ periodEndDate$_$$$ $$$}'# Also search for any type definitions related to subscription period dates
rg -A 3 -B 3 "periodEndDate.*[?:]"

Length of output: 2751


Fix the TypeScript suppression for periodEndDate
The CommerceSubscriptionResource model declares periodEndDate as Date | null, so subscription.periodEndDate may legitimately be null. Suppressing the error hides a potential runtime issue. Please remove the @ts-expect-error and either:

  • Add an explicit null check before using it, e.g.
    if(!subscription.periodEndDate){// handle missing date…}else{date: subscription.periodEndDate,}
  • Or use a non-null assertion if you’re certain it can’t be null in this context:
    - // @ts-expect-error this will always be defined in this case.- date: subscription.periodEndDate,+ date: subscription.periodEndDate!,

Locations to update:

  • packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx (lines 321–322)
  • Note: the type in packages/types/src/commerce.ts is periodEndDate: Date | null;
🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx at lines 321-322,
remove the @ts-expect-error comment on subscription.periodEndDate and address
its nullable type properly. Add an explicit null check before using
subscription.periodEndDate to handle the case when it is null, or if you are
certain it cannot be null here, replace the suppression with a non-null
assertion operator to ensure type safety without hiding potential runtime
issues.

})
}
/>
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const PricingTableRoot = (props: PricingTableProps) => {

// don't pay attention to the default plan
const activeSubscription = subscriptions?.find(
sub => !sub.canceledAt && sub.status === 'active' && !sub.plan.isDefault,
sub => !sub.canceledAtDate && sub.status === 'active' && !sub.plan.isDefault,
);
if (activeSubscription) {
return activeSubscription.planPeriod;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -143,7 +143,7 @@ function Card(props: CardProps) {
shouldShowFooter = true;
shouldShowFooterNotice = true;
} else if (subscription.status === 'active') {
if (subscription.canceledAt) {
if (subscription.canceledAtDate) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyAmount > 0) {
Expand DownExpand Up@@ -253,7 +253,7 @@ function Card(props: CardProps) {
elementDescriptor={descriptors.pricingTableCardFooterNotice}
variant={isCompact ? 'buttonSmall' : 'buttonLarge'}
localizationKey={localizationKeys('badge__startsAt', {
date: subscription?.periodStart,
date: subscription?.periodStartDate,
})}
colorScheme='secondary'
sx={t => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ export function SubscriptionsList({
>
{subscription.plan.name}
</Text>
{sortedSubscriptions.length > 1 || !!subscription.canceledAt ? (
{sortedSubscriptions.length > 1 || !!subscription.canceledAtDate ? (
<Badge
colorScheme={subscription.status === 'active' ? 'secondary' : 'primary'}
localizationKey={
Expand Down
31 changes: 18 additions & 13 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,9 +98,9 @@ export const useSubscriptions = () => {
if (
isSignedIn &&
defaultFreePlan &&
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAt))
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAtDate))
) {
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAt);
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAtDate);
return [
..._subscriptions.data,
new CommerceSubscription({
Expand All@@ -111,7 +111,8 @@ export const useSubscriptions = () => {
plan_period: 'month',
canceled_at: null,
status: _subscriptions.data.length === 0 ? 'active' : 'upcoming',
period_start: canceledSubscription?.periodEnd || 0,
created_at: canceledSubscription?.periodEndDate?.getTime() || 0,
period_start: canceledSubscription?.periodEndDate?.getTime() || 0,
period_end: 0,
}),
];
Expand DownExpand Up@@ -190,7 +191,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 subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAt);
return subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAtDate);
}, [subscriptions]);

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

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

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

Expand DownExpand Up@@ -332,12 +333,16 @@ export const usePlansContext = () => {

const captionForSubscription = useCallback((subscription: CommerceSubscriptionResource) => {
if (subscription.status === 'upcoming') {
return localizationKeys('badge__startsAt', { date: subscription.periodStart });
} else if (subscription.canceledAt) {
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
} else {
return localizationKeys('badge__renewsAt', { date: subscription.periodEnd });
return localizationKeys('badge__startsAt', { date: subscription.periodStartDate });
}
if (subscription.canceledAtDate) {
// @ts-expect-error `periodEndDate` is always defined when `canceledAtDate` exists
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEndDate });
}
if (subscription.periodEndDate) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEndDate });
}
return;
}, []);

// handle the selection of a plan, either by opening the subscription details or checkout
Expand All@@ -355,7 +360,7 @@ export const usePlansContext = () => {

const portalRoot = getClosestProfileScrollBox(mode, event);

if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAt) {
if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAtDate) {
clerk.__internal_openPlanDetails({
plan,
initialPlanPeriod: planPeriod,
Expand Down
13 changes: 13 additions & 0 deletions packages/types/src/commerce.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -155,8 +155,21 @@ export interface CommerceSubscriptionResource extends ClerkResource {
plan: CommercePlanResource;
planPeriod: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
createdAt: Date;
periodStartDate: Date;
periodEndDate: Date | null;
canceledAtDate: Date | null;
/**
* @deprecated Use `periodStartDate` instead
*/
periodStart: number;
/**
* @deprecated Use `periodEndDate` instead
*/
periodEnd: number;
/**
* @deprecated Use `canceledAtDate` instead
*/
canceledAt: number | null;
amount?: CommerceMoney;
credit?: {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/json.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,6 +701,7 @@ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
plan: CommercePlanJSON;
plan_period: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
created_at: number;
period_start: number;
period_end: number;
canceled_at: number | null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions .changeset/funny-ducks-boil.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Convert date properties from number to Date in CommerceSubscriptionResource
Deprecates fields of type `number`
- subscription.periodStart
- subscription.periodEnd
- subscription.canceledAt
Introduces fields of type `Date`
- subscription.periodStartDate
- subscription.periodEndDate
- subscription.canceledAtDate
- subscription.createdAt
13 changes: 13 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@ import type {
DeletedObjectJSON,
} from '@clerk/types';

import { unixEpochToDate } from '@/utils/date';

import { commerceMoneyFromJSON } from '../../utils';
import { BaseResource, CommercePlan, DeletedObject } from './internal';

Expand All@@ -17,13 +19,18 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
plan!: CommercePlan;
planPeriod!: CommerceSubscriptionPlanPeriod;
status!: CommerceSubscriptionStatus;
createdAt!: Date;
periodStartDate!: Date;
periodEndDate!: Date | null;
canceledAtDate!: Date | null;
periodStart!: number;
periodEnd!: number;
canceledAt!: number | null;
amount?: CommerceMoney;
credit?: {
amount: CommerceMoney;
};

constructor(data: CommerceSubscriptionJSON) {
super();
this.fromJSON(data);
Expand All@@ -42,6 +49,12 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
this.periodStart = data.period_start;
this.periodEnd = data.period_end;
this.canceledAt = data.canceled_at;

this.createdAt = unixEpochToDate(data.created_at);
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;
Comment on lines +53 to +56

Copy link
Copy Markdown
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

Verify the unixEpochToDate utility function

The conversion logic looks correct, but ensure the unixEpochToDate utility function properly handles edge cases.


🏁 Script executed:

#!/bin/bash# Description: Verify the unixEpochToDate utility function exists and examine its implementation# Search for the utility function definition
ast-grep --pattern 'function unixEpochToDate($_) { $$$}'# Also search for arrow function or const declaration
rg -A 10 "unixEpochToDate.*=" src/utils/date

Length of output: 163


I couldn’t locate unixEpochToDate in the initial paths—let’s do a global search to find its definition and usages.


🏁 Script executed:

#!/bin/bash# Search for definition and all usages of unixEpochToDate across the repo
rg -n "unixEpochToDate"

Length of output: 10950


Let’s inspect the implementation and its tests:


🏁 Script executed:

#!/bin/bash# Show the unixEpochToDate implementation
sed -n '1,200p' packages/clerk-js/src/utils/date.ts
# Show tests covering edge cases
sed -n '1,200p' packages/clerk-js/src/utils/__tests__/date.spec.ts

Length of output: 1264


Improve zero-timestamp handling in unixEpochToDate

The current implementation

exportfunctionunixEpochToDate(epochInSeconds?: number): Date{constdate=newDate(epochInSeconds||newDate());returnisValidDate(date) ? date : newDate();}

uses a falsy check (epochInSeconds || …), so passing 0 (the Unix epoch) falls back to Date.now(). To correctly support 0, please:

  • In packages/clerk-js/src/utils/date.ts, update the guard:
    export function unixEpochToDate(epochInSeconds?: number): Date {
  • const date = new Date(epochInSeconds || new Date());
  • const timestamp = epochInSeconds != null ? epochInSeconds : Date.now();
  • const date = new Date(timestamp);
    return isValidDate(date) ? date : new Date();
    }
- In `packages/clerk-js/src/utils/__tests__/date.spec.ts`, add a zero-timestamp test:
```ts
it('correctly handles zero', () => {
const date = unixEpochToDate(0);
expect(date.getTime()).toBe(0);
});

This change ensures unixEpochToDate(0) returns the Unix epoch rather than “now.”

🤖 Prompt for AI Agents
In packages/clerk-js/src/utils/date.ts, update the unixEpochToDate function to
explicitly check for undefined or null instead of using a falsy check, so that
passing 0 returns the correct Unix epoch date. Then, in
packages/clerk-js/src/utils/__tests__/date.spec.ts, add a test case that calls
unixEpochToDate(0) and asserts that the returned date's time is 0 to verify
correct handling of the zero timestamp.


this.amount = data.amount ? commerceMoneyFromJSON(data.amount) : undefined;
this.credit = data.credit && data.credit.amount ? { amount: commerceMoneyFromJSON(data.credit.amount) } : undefined;
return this;
Expand Down
5 changes: 3 additions & 2 deletions packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -210,7 +210,7 @@ const PlanDetailsInternal = ({
{(!plan.isDefault && !isDefaultPlanImplicitlyActiveOrUpcoming) || !subscription ? (
<Drawer.Footer>
{subscription ? (
subscription.canceledAt ? (
subscription.canceledAtDate ? (
<Button
block
textVariant='buttonLarge'
Expand DownExpand Up@@ -318,7 +318,8 @@ const PlanDetailsInternal = ({
? localizationKeys('commerce.cancelSubscriptionNoCharge')
: localizationKeys('commerce.cancelSubscriptionAccessUntil', {
plan: subscription.plan.name,
date: subscription.periodEnd,
// @ts-expect-error this will always be defined in this case.
date: subscription.periodEndDate,
Comment on lines +321 to +322

Copy link
Copy Markdown
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

Verify the TypeScript suppression is justified.

The migration from periodEnd to periodEndDate is consistent with the pattern. However, the @ts-expect-error comment suggests a type issue. Please ensure that the property is indeed always defined in this context or consider updating the type definitions.


🏁 Script executed:

#!/bin/bash# Description: Check the type definition of periodEndDate in CommerceSubscriptionResource# Expected: Verify if periodEndDate is optional or required in the type definition
ast-grep --pattern 'interface CommerceSubscriptionResource { $$$ periodEndDate$_$$$ $$$}'# Also search for any type definitions related to subscription period dates
rg -A 3 -B 3 "periodEndDate.*[?:]"

Length of output: 2751


Fix the TypeScript suppression for periodEndDate
The CommerceSubscriptionResource model declares periodEndDate as Date | null, so subscription.periodEndDate may legitimately be null. Suppressing the error hides a potential runtime issue. Please remove the @ts-expect-error and either:

  • Add an explicit null check before using it, e.g.
    if(!subscription.periodEndDate){// handle missing date…}else{date: subscription.periodEndDate,}
  • Or use a non-null assertion if you’re certain it can’t be null in this context:
    - // @ts-expect-error this will always be defined in this case.- date: subscription.periodEndDate,+ date: subscription.periodEndDate!,

Locations to update:

  • packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx (lines 321–322)
  • Note: the type in packages/types/src/commerce.ts is periodEndDate: Date | null;
🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx at lines 321-322,
remove the @ts-expect-error comment on subscription.periodEndDate and address
its nullable type properly. Add an explicit null check before using
subscription.periodEndDate to handle the case when it is null, or if you are
certain it cannot be null here, replace the suppression with a non-null
assertion operator to ensure type safety without hiding potential runtime
issues.

})
}
/>
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const PricingTableRoot = (props: PricingTableProps) => {

// don't pay attention to the default plan
const activeSubscription = subscriptions?.find(
sub => !sub.canceledAt && sub.status === 'active' && !sub.plan.isDefault,
sub => !sub.canceledAtDate && sub.status === 'active' && !sub.plan.isDefault,
);
if (activeSubscription) {
return activeSubscription.planPeriod;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -143,7 +143,7 @@ function Card(props: CardProps) {
shouldShowFooter = true;
shouldShowFooterNotice = true;
} else if (subscription.status === 'active') {
if (subscription.canceledAt) {
if (subscription.canceledAtDate) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyAmount > 0) {
Expand DownExpand Up@@ -253,7 +253,7 @@ function Card(props: CardProps) {
elementDescriptor={descriptors.pricingTableCardFooterNotice}
variant={isCompact ? 'buttonSmall' : 'buttonLarge'}
localizationKey={localizationKeys('badge__startsAt', {
date: subscription?.periodStart,
date: subscription?.periodStartDate,
})}
colorScheme='secondary'
sx={t => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ export function SubscriptionsList({
>
{subscription.plan.name}
</Text>
{sortedSubscriptions.length > 1 || !!subscription.canceledAt ? (
{sortedSubscriptions.length > 1 || !!subscription.canceledAtDate ? (
<Badge
colorScheme={subscription.status === 'active' ? 'secondary' : 'primary'}
localizationKey={
Expand Down
31 changes: 18 additions & 13 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,9 +98,9 @@ export const useSubscriptions = () => {
if (
isSignedIn &&
defaultFreePlan &&
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAt))
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAtDate))
) {
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAt);
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAtDate);
return [
..._subscriptions.data,
new CommerceSubscription({
Expand All@@ -111,7 +111,8 @@ export const useSubscriptions = () => {
plan_period: 'month',
canceled_at: null,
status: _subscriptions.data.length === 0 ? 'active' : 'upcoming',
period_start: canceledSubscription?.periodEnd || 0,
created_at: canceledSubscription?.periodEndDate?.getTime() || 0,
period_start: canceledSubscription?.periodEndDate?.getTime() || 0,
period_end: 0,
}),
];
Expand DownExpand Up@@ -190,7 +191,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 subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAt);
return subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAtDate);
}, [subscriptions]);

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

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

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

Expand DownExpand Up@@ -332,12 +333,16 @@ export const usePlansContext = () => {

const captionForSubscription = useCallback((subscription: CommerceSubscriptionResource) => {
if (subscription.status === 'upcoming') {
return localizationKeys('badge__startsAt', { date: subscription.periodStart });
} else if (subscription.canceledAt) {
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
} else {
return localizationKeys('badge__renewsAt', { date: subscription.periodEnd });
return localizationKeys('badge__startsAt', { date: subscription.periodStartDate });
}
if (subscription.canceledAtDate) {
// @ts-expect-error `periodEndDate` is always defined when `canceledAtDate` exists
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEndDate });
}
if (subscription.periodEndDate) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEndDate });
}
return;
}, []);

// handle the selection of a plan, either by opening the subscription details or checkout
Expand All@@ -355,7 +360,7 @@ export const usePlansContext = () => {

const portalRoot = getClosestProfileScrollBox(mode, event);

if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAt) {
if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAtDate) {
clerk.__internal_openPlanDetails({
plan,
initialPlanPeriod: planPeriod,
Expand Down
13 changes: 13 additions & 0 deletions packages/types/src/commerce.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -155,8 +155,21 @@ export interface CommerceSubscriptionResource extends ClerkResource {
plan: CommercePlanResource;
planPeriod: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
createdAt: Date;
periodStartDate: Date;
periodEndDate: Date | null;
canceledAtDate: Date | null;
/**
* @deprecated Use `periodStartDate` instead
*/
periodStart: number;
/**
* @deprecated Use `periodEndDate` instead
*/
periodEnd: number;
/**
* @deprecated Use `canceledAtDate` instead
*/
canceledAt: number | null;
amount?: CommerceMoney;
credit?: {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/json.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,6 +701,7 @@ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
plan: CommercePlanJSON;
plan_period: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
created_at: number;
period_start: number;
period_end: number;
canceled_at: number | null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions .changeset/funny-ducks-boil.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Convert date properties from number to Date in CommerceSubscriptionResource
Deprecates fields of type `number`
- subscription.periodStart
- subscription.periodEnd
- subscription.canceledAt
Introduces fields of type `Date`
- subscription.periodStartDate
- subscription.periodEndDate
- subscription.canceledAtDate
- subscription.createdAt
13 changes: 13 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@ import type {
DeletedObjectJSON,
} from '@clerk/types';

import { unixEpochToDate } from '@/utils/date';

import { commerceMoneyFromJSON } from '../../utils';
import { BaseResource, CommercePlan, DeletedObject } from './internal';

Expand All@@ -17,13 +19,18 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
plan!: CommercePlan;
planPeriod!: CommerceSubscriptionPlanPeriod;
status!: CommerceSubscriptionStatus;
createdAt!: Date;
periodStartDate!: Date;
periodEndDate!: Date | null;
canceledAtDate!: Date | null;
periodStart!: number;
periodEnd!: number;
canceledAt!: number | null;
amount?: CommerceMoney;
credit?: {
amount: CommerceMoney;
};

constructor(data: CommerceSubscriptionJSON) {
super();
this.fromJSON(data);
Expand All@@ -42,6 +49,12 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
this.periodStart = data.period_start;
this.periodEnd = data.period_end;
this.canceledAt = data.canceled_at;

this.createdAt = unixEpochToDate(data.created_at);
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;
Comment on lines +53 to +56

Copy link
Copy Markdown
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

Verify the unixEpochToDate utility function

The conversion logic looks correct, but ensure the unixEpochToDate utility function properly handles edge cases.


🏁 Script executed:

#!/bin/bash# Description: Verify the unixEpochToDate utility function exists and examine its implementation# Search for the utility function definition
ast-grep --pattern 'function unixEpochToDate($_) { $$$}'# Also search for arrow function or const declaration
rg -A 10 "unixEpochToDate.*=" src/utils/date

Length of output: 163


I couldn’t locate unixEpochToDate in the initial paths—let’s do a global search to find its definition and usages.


🏁 Script executed:

#!/bin/bash# Search for definition and all usages of unixEpochToDate across the repo
rg -n "unixEpochToDate"

Length of output: 10950


Let’s inspect the implementation and its tests:


🏁 Script executed:

#!/bin/bash# Show the unixEpochToDate implementation
sed -n '1,200p' packages/clerk-js/src/utils/date.ts
# Show tests covering edge cases
sed -n '1,200p' packages/clerk-js/src/utils/__tests__/date.spec.ts

Length of output: 1264


Improve zero-timestamp handling in unixEpochToDate

The current implementation

exportfunctionunixEpochToDate(epochInSeconds?: number): Date{constdate=newDate(epochInSeconds||newDate());returnisValidDate(date) ? date : newDate();}

uses a falsy check (epochInSeconds || …), so passing 0 (the Unix epoch) falls back to Date.now(). To correctly support 0, please:

  • In packages/clerk-js/src/utils/date.ts, update the guard:
    export function unixEpochToDate(epochInSeconds?: number): Date {
  • const date = new Date(epochInSeconds || new Date());
  • const timestamp = epochInSeconds != null ? epochInSeconds : Date.now();
  • const date = new Date(timestamp);
    return isValidDate(date) ? date : new Date();
    }
- In `packages/clerk-js/src/utils/__tests__/date.spec.ts`, add a zero-timestamp test:
```ts
it('correctly handles zero', () => {
const date = unixEpochToDate(0);
expect(date.getTime()).toBe(0);
});

This change ensures unixEpochToDate(0) returns the Unix epoch rather than “now.”

🤖 Prompt for AI Agents
In packages/clerk-js/src/utils/date.ts, update the unixEpochToDate function to
explicitly check for undefined or null instead of using a falsy check, so that
passing 0 returns the correct Unix epoch date. Then, in
packages/clerk-js/src/utils/__tests__/date.spec.ts, add a test case that calls
unixEpochToDate(0) and asserts that the returned date's time is 0 to verify
correct handling of the zero timestamp.


this.amount = data.amount ? commerceMoneyFromJSON(data.amount) : undefined;
this.credit = data.credit && data.credit.amount ? { amount: commerceMoneyFromJSON(data.credit.amount) } : undefined;
return this;
Expand Down
5 changes: 3 additions & 2 deletions packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -210,7 +210,7 @@ const PlanDetailsInternal = ({
{(!plan.isDefault && !isDefaultPlanImplicitlyActiveOrUpcoming) || !subscription ? (
<Drawer.Footer>
{subscription ? (
subscription.canceledAt ? (
subscription.canceledAtDate ? (
<Button
block
textVariant='buttonLarge'
Expand DownExpand Up@@ -318,7 +318,8 @@ const PlanDetailsInternal = ({
? localizationKeys('commerce.cancelSubscriptionNoCharge')
: localizationKeys('commerce.cancelSubscriptionAccessUntil', {
plan: subscription.plan.name,
date: subscription.periodEnd,
// @ts-expect-error this will always be defined in this case.
date: subscription.periodEndDate,
Comment on lines +321 to +322

Copy link
Copy Markdown
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

Verify the TypeScript suppression is justified.

The migration from periodEnd to periodEndDate is consistent with the pattern. However, the @ts-expect-error comment suggests a type issue. Please ensure that the property is indeed always defined in this context or consider updating the type definitions.


🏁 Script executed:

#!/bin/bash# Description: Check the type definition of periodEndDate in CommerceSubscriptionResource# Expected: Verify if periodEndDate is optional or required in the type definition
ast-grep --pattern 'interface CommerceSubscriptionResource { $$$ periodEndDate$_$$$ $$$}'# Also search for any type definitions related to subscription period dates
rg -A 3 -B 3 "periodEndDate.*[?:]"

Length of output: 2751


Fix the TypeScript suppression for periodEndDate
The CommerceSubscriptionResource model declares periodEndDate as Date | null, so subscription.periodEndDate may legitimately be null. Suppressing the error hides a potential runtime issue. Please remove the @ts-expect-error and either:

  • Add an explicit null check before using it, e.g.
    if(!subscription.periodEndDate){// handle missing date…}else{date: subscription.periodEndDate,}
  • Or use a non-null assertion if you’re certain it can’t be null in this context:
    - // @ts-expect-error this will always be defined in this case.- date: subscription.periodEndDate,+ date: subscription.periodEndDate!,

Locations to update:

  • packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx (lines 321–322)
  • Note: the type in packages/types/src/commerce.ts is periodEndDate: Date | null;
🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx at lines 321-322,
remove the @ts-expect-error comment on subscription.periodEndDate and address
its nullable type properly. Add an explicit null check before using
subscription.periodEndDate to handle the case when it is null, or if you are
certain it cannot be null here, replace the suppression with a non-null
assertion operator to ensure type safety without hiding potential runtime
issues.

})
}
/>
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const PricingTableRoot = (props: PricingTableProps) => {

// don't pay attention to the default plan
const activeSubscription = subscriptions?.find(
sub => !sub.canceledAt && sub.status === 'active' && !sub.plan.isDefault,
sub => !sub.canceledAtDate && sub.status === 'active' && !sub.plan.isDefault,
);
if (activeSubscription) {
return activeSubscription.planPeriod;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -143,7 +143,7 @@ function Card(props: CardProps) {
shouldShowFooter = true;
shouldShowFooterNotice = true;
} else if (subscription.status === 'active') {
if (subscription.canceledAt) {
if (subscription.canceledAtDate) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyAmount > 0) {
Expand DownExpand Up@@ -253,7 +253,7 @@ function Card(props: CardProps) {
elementDescriptor={descriptors.pricingTableCardFooterNotice}
variant={isCompact ? 'buttonSmall' : 'buttonLarge'}
localizationKey={localizationKeys('badge__startsAt', {
date: subscription?.periodStart,
date: subscription?.periodStartDate,
})}
colorScheme='secondary'
sx={t => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ export function SubscriptionsList({
>
{subscription.plan.name}
</Text>
{sortedSubscriptions.length > 1 || !!subscription.canceledAt ? (
{sortedSubscriptions.length > 1 || !!subscription.canceledAtDate ? (
<Badge
colorScheme={subscription.status === 'active' ? 'secondary' : 'primary'}
localizationKey={
Expand Down
31 changes: 18 additions & 13 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,9 +98,9 @@ export const useSubscriptions = () => {
if (
isSignedIn &&
defaultFreePlan &&
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAt))
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAtDate))
) {
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAt);
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAtDate);
return [
..._subscriptions.data,
new CommerceSubscription({
Expand All@@ -111,7 +111,8 @@ export const useSubscriptions = () => {
plan_period: 'month',
canceled_at: null,
status: _subscriptions.data.length === 0 ? 'active' : 'upcoming',
period_start: canceledSubscription?.periodEnd || 0,
created_at: canceledSubscription?.periodEndDate?.getTime() || 0,
period_start: canceledSubscription?.periodEndDate?.getTime() || 0,
period_end: 0,
}),
];
Expand DownExpand Up@@ -190,7 +191,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 subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAt);
return subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAtDate);
}, [subscriptions]);

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

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

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

Expand DownExpand Up@@ -332,12 +333,16 @@ export const usePlansContext = () => {

const captionForSubscription = useCallback((subscription: CommerceSubscriptionResource) => {
if (subscription.status === 'upcoming') {
return localizationKeys('badge__startsAt', { date: subscription.periodStart });
} else if (subscription.canceledAt) {
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
} else {
return localizationKeys('badge__renewsAt', { date: subscription.periodEnd });
return localizationKeys('badge__startsAt', { date: subscription.periodStartDate });
}
if (subscription.canceledAtDate) {
// @ts-expect-error `periodEndDate` is always defined when `canceledAtDate` exists
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEndDate });
}
if (subscription.periodEndDate) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEndDate });
}
return;
}, []);

// handle the selection of a plan, either by opening the subscription details or checkout
Expand All@@ -355,7 +360,7 @@ export const usePlansContext = () => {

const portalRoot = getClosestProfileScrollBox(mode, event);

if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAt) {
if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAtDate) {
clerk.__internal_openPlanDetails({
plan,
initialPlanPeriod: planPeriod,
Expand Down
13 changes: 13 additions & 0 deletions packages/types/src/commerce.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -155,8 +155,21 @@ export interface CommerceSubscriptionResource extends ClerkResource {
plan: CommercePlanResource;
planPeriod: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
createdAt: Date;
periodStartDate: Date;
periodEndDate: Date | null;
canceledAtDate: Date | null;
/**
* @deprecated Use `periodStartDate` instead
*/
periodStart: number;
/**
* @deprecated Use `periodEndDate` instead
*/
periodEnd: number;
/**
* @deprecated Use `canceledAtDate` instead
*/
canceledAt: number | null;
amount?: CommerceMoney;
credit?: {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/json.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,6 +701,7 @@ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
plan: CommercePlanJSON;
plan_period: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
created_at: number;
period_start: number;
period_end: number;
canceled_at: number | null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions .changeset/funny-ducks-boil.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Convert date properties from number to Date in CommerceSubscriptionResource
Deprecates fields of type `number`
- subscription.periodStart
- subscription.periodEnd
- subscription.canceledAt
Introduces fields of type `Date`
- subscription.periodStartDate
- subscription.periodEndDate
- subscription.canceledAtDate
- subscription.createdAt
13 changes: 13 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@ import type {
DeletedObjectJSON,
} from '@clerk/types';

import { unixEpochToDate } from '@/utils/date';

import { commerceMoneyFromJSON } from '../../utils';
import { BaseResource, CommercePlan, DeletedObject } from './internal';

Expand All@@ -17,13 +19,18 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
plan!: CommercePlan;
planPeriod!: CommerceSubscriptionPlanPeriod;
status!: CommerceSubscriptionStatus;
createdAt!: Date;
periodStartDate!: Date;
periodEndDate!: Date | null;
canceledAtDate!: Date | null;
periodStart!: number;
periodEnd!: number;
canceledAt!: number | null;
amount?: CommerceMoney;
credit?: {
amount: CommerceMoney;
};

constructor(data: CommerceSubscriptionJSON) {
super();
this.fromJSON(data);
Expand All@@ -42,6 +49,12 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
this.periodStart = data.period_start;
this.periodEnd = data.period_end;
this.canceledAt = data.canceled_at;

this.createdAt = unixEpochToDate(data.created_at);
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;
Comment on lines +53 to +56

Copy link
Copy Markdown
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

Verify the unixEpochToDate utility function

The conversion logic looks correct, but ensure the unixEpochToDate utility function properly handles edge cases.


🏁 Script executed:

#!/bin/bash# Description: Verify the unixEpochToDate utility function exists and examine its implementation# Search for the utility function definition
ast-grep --pattern 'function unixEpochToDate($_) { $$$}'# Also search for arrow function or const declaration
rg -A 10 "unixEpochToDate.*=" src/utils/date

Length of output: 163


I couldn’t locate unixEpochToDate in the initial paths—let’s do a global search to find its definition and usages.


🏁 Script executed:

#!/bin/bash# Search for definition and all usages of unixEpochToDate across the repo
rg -n "unixEpochToDate"

Length of output: 10950


Let’s inspect the implementation and its tests:


🏁 Script executed:

#!/bin/bash# Show the unixEpochToDate implementation
sed -n '1,200p' packages/clerk-js/src/utils/date.ts
# Show tests covering edge cases
sed -n '1,200p' packages/clerk-js/src/utils/__tests__/date.spec.ts

Length of output: 1264


Improve zero-timestamp handling in unixEpochToDate

The current implementation

exportfunctionunixEpochToDate(epochInSeconds?: number): Date{constdate=newDate(epochInSeconds||newDate());returnisValidDate(date) ? date : newDate();}

uses a falsy check (epochInSeconds || …), so passing 0 (the Unix epoch) falls back to Date.now(). To correctly support 0, please:

  • In packages/clerk-js/src/utils/date.ts, update the guard:
    export function unixEpochToDate(epochInSeconds?: number): Date {
  • const date = new Date(epochInSeconds || new Date());
  • const timestamp = epochInSeconds != null ? epochInSeconds : Date.now();
  • const date = new Date(timestamp);
    return isValidDate(date) ? date : new Date();
    }
- In `packages/clerk-js/src/utils/__tests__/date.spec.ts`, add a zero-timestamp test:
```ts
it('correctly handles zero', () => {
const date = unixEpochToDate(0);
expect(date.getTime()).toBe(0);
});

This change ensures unixEpochToDate(0) returns the Unix epoch rather than “now.”

🤖 Prompt for AI Agents
In packages/clerk-js/src/utils/date.ts, update the unixEpochToDate function to
explicitly check for undefined or null instead of using a falsy check, so that
passing 0 returns the correct Unix epoch date. Then, in
packages/clerk-js/src/utils/__tests__/date.spec.ts, add a test case that calls
unixEpochToDate(0) and asserts that the returned date's time is 0 to verify
correct handling of the zero timestamp.


this.amount = data.amount ? commerceMoneyFromJSON(data.amount) : undefined;
this.credit = data.credit && data.credit.amount ? { amount: commerceMoneyFromJSON(data.credit.amount) } : undefined;
return this;
Expand Down
5 changes: 3 additions & 2 deletions packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -210,7 +210,7 @@ const PlanDetailsInternal = ({
{(!plan.isDefault && !isDefaultPlanImplicitlyActiveOrUpcoming) || !subscription ? (
<Drawer.Footer>
{subscription ? (
subscription.canceledAt ? (
subscription.canceledAtDate ? (
<Button
block
textVariant='buttonLarge'
Expand DownExpand Up@@ -318,7 +318,8 @@ const PlanDetailsInternal = ({
? localizationKeys('commerce.cancelSubscriptionNoCharge')
: localizationKeys('commerce.cancelSubscriptionAccessUntil', {
plan: subscription.plan.name,
date: subscription.periodEnd,
// @ts-expect-error this will always be defined in this case.
date: subscription.periodEndDate,
Comment on lines +321 to +322

Copy link
Copy Markdown
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

Verify the TypeScript suppression is justified.

The migration from periodEnd to periodEndDate is consistent with the pattern. However, the @ts-expect-error comment suggests a type issue. Please ensure that the property is indeed always defined in this context or consider updating the type definitions.


🏁 Script executed:

#!/bin/bash# Description: Check the type definition of periodEndDate in CommerceSubscriptionResource# Expected: Verify if periodEndDate is optional or required in the type definition
ast-grep --pattern 'interface CommerceSubscriptionResource { $$$ periodEndDate$_$$$ $$$}'# Also search for any type definitions related to subscription period dates
rg -A 3 -B 3 "periodEndDate.*[?:]"

Length of output: 2751


Fix the TypeScript suppression for periodEndDate
The CommerceSubscriptionResource model declares periodEndDate as Date | null, so subscription.periodEndDate may legitimately be null. Suppressing the error hides a potential runtime issue. Please remove the @ts-expect-error and either:

  • Add an explicit null check before using it, e.g.
    if(!subscription.periodEndDate){// handle missing date…}else{date: subscription.periodEndDate,}
  • Or use a non-null assertion if you’re certain it can’t be null in this context:
    - // @ts-expect-error this will always be defined in this case.- date: subscription.periodEndDate,+ date: subscription.periodEndDate!,

Locations to update:

  • packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx (lines 321–322)
  • Note: the type in packages/types/src/commerce.ts is periodEndDate: Date | null;
🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx at lines 321-322,
remove the @ts-expect-error comment on subscription.periodEndDate and address
its nullable type properly. Add an explicit null check before using
subscription.periodEndDate to handle the case when it is null, or if you are
certain it cannot be null here, replace the suppression with a non-null
assertion operator to ensure type safety without hiding potential runtime
issues.

})
}
/>
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const PricingTableRoot = (props: PricingTableProps) => {

// don't pay attention to the default plan
const activeSubscription = subscriptions?.find(
sub => !sub.canceledAt && sub.status === 'active' && !sub.plan.isDefault,
sub => !sub.canceledAtDate && sub.status === 'active' && !sub.plan.isDefault,
);
if (activeSubscription) {
return activeSubscription.planPeriod;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -143,7 +143,7 @@ function Card(props: CardProps) {
shouldShowFooter = true;
shouldShowFooterNotice = true;
} else if (subscription.status === 'active') {
if (subscription.canceledAt) {
if (subscription.canceledAtDate) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyAmount > 0) {
Expand DownExpand Up@@ -253,7 +253,7 @@ function Card(props: CardProps) {
elementDescriptor={descriptors.pricingTableCardFooterNotice}
variant={isCompact ? 'buttonSmall' : 'buttonLarge'}
localizationKey={localizationKeys('badge__startsAt', {
date: subscription?.periodStart,
date: subscription?.periodStartDate,
})}
colorScheme='secondary'
sx={t => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ export function SubscriptionsList({
>
{subscription.plan.name}
</Text>
{sortedSubscriptions.length > 1 || !!subscription.canceledAt ? (
{sortedSubscriptions.length > 1 || !!subscription.canceledAtDate ? (
<Badge
colorScheme={subscription.status === 'active' ? 'secondary' : 'primary'}
localizationKey={
Expand Down
31 changes: 18 additions & 13 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,9 +98,9 @@ export const useSubscriptions = () => {
if (
isSignedIn &&
defaultFreePlan &&
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAt))
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAtDate))
) {
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAt);
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAtDate);
return [
..._subscriptions.data,
new CommerceSubscription({
Expand All@@ -111,7 +111,8 @@ export const useSubscriptions = () => {
plan_period: 'month',
canceled_at: null,
status: _subscriptions.data.length === 0 ? 'active' : 'upcoming',
period_start: canceledSubscription?.periodEnd || 0,
created_at: canceledSubscription?.periodEndDate?.getTime() || 0,
period_start: canceledSubscription?.periodEndDate?.getTime() || 0,
period_end: 0,
}),
];
Expand DownExpand Up@@ -190,7 +191,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 subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAt);
return subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAtDate);
}, [subscriptions]);

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

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

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

Expand DownExpand Up@@ -332,12 +333,16 @@ export const usePlansContext = () => {

const captionForSubscription = useCallback((subscription: CommerceSubscriptionResource) => {
if (subscription.status === 'upcoming') {
return localizationKeys('badge__startsAt', { date: subscription.periodStart });
} else if (subscription.canceledAt) {
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
} else {
return localizationKeys('badge__renewsAt', { date: subscription.periodEnd });
return localizationKeys('badge__startsAt', { date: subscription.periodStartDate });
}
if (subscription.canceledAtDate) {
// @ts-expect-error `periodEndDate` is always defined when `canceledAtDate` exists
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEndDate });
}
if (subscription.periodEndDate) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEndDate });
}
return;
}, []);

// handle the selection of a plan, either by opening the subscription details or checkout
Expand All@@ -355,7 +360,7 @@ export const usePlansContext = () => {

const portalRoot = getClosestProfileScrollBox(mode, event);

if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAt) {
if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAtDate) {
clerk.__internal_openPlanDetails({
plan,
initialPlanPeriod: planPeriod,
Expand Down
13 changes: 13 additions & 0 deletions packages/types/src/commerce.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -155,8 +155,21 @@ export interface CommerceSubscriptionResource extends ClerkResource {
plan: CommercePlanResource;
planPeriod: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
createdAt: Date;
periodStartDate: Date;
periodEndDate: Date | null;
canceledAtDate: Date | null;
/**
* @deprecated Use `periodStartDate` instead
*/
periodStart: number;
/**
* @deprecated Use `periodEndDate` instead
*/
periodEnd: number;
/**
* @deprecated Use `canceledAtDate` instead
*/
canceledAt: number | null;
amount?: CommerceMoney;
credit?: {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/json.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,6 +701,7 @@ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
plan: CommercePlanJSON;
plan_period: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
created_at: number;
period_start: number;
period_end: number;
canceled_at: number | null;
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
15 changes: 15 additions & 0 deletions .changeset/funny-ducks-boil.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Convert date properties from number to Date in CommerceSubscriptionResource
Deprecates fields of type `number`
- subscription.periodStart
- subscription.periodEnd
- subscription.canceledAt
Introduces fields of type `Date`
- subscription.periodStartDate
- subscription.periodEndDate
- subscription.canceledAtDate
- subscription.createdAt
13 changes: 13 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceSubscription.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -8,6 +8,8 @@ import type {
DeletedObjectJSON,
} from '@clerk/types';

import { unixEpochToDate } from '@/utils/date';

import { commerceMoneyFromJSON } from '../../utils';
import { BaseResource, CommercePlan, DeletedObject } from './internal';

Expand All@@ -17,13 +19,18 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
plan!: CommercePlan;
planPeriod!: CommerceSubscriptionPlanPeriod;
status!: CommerceSubscriptionStatus;
createdAt!: Date;
periodStartDate!: Date;
periodEndDate!: Date | null;
canceledAtDate!: Date | null;
periodStart!: number;
periodEnd!: number;
canceledAt!: number | null;
amount?: CommerceMoney;
credit?: {
amount: CommerceMoney;
};

constructor(data: CommerceSubscriptionJSON) {
super();
this.fromJSON(data);
Expand All@@ -42,6 +49,12 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
this.periodStart = data.period_start;
this.periodEnd = data.period_end;
this.canceledAt = data.canceled_at;

this.createdAt = unixEpochToDate(data.created_at);
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;
Comment on lines +53 to +56

Copy link
Copy Markdown
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

Verify the unixEpochToDate utility function

The conversion logic looks correct, but ensure the unixEpochToDate utility function properly handles edge cases.


🏁 Script executed:

#!/bin/bash# Description: Verify the unixEpochToDate utility function exists and examine its implementation# Search for the utility function definition
ast-grep --pattern 'function unixEpochToDate($_) { $$$}'# Also search for arrow function or const declaration
rg -A 10 "unixEpochToDate.*=" src/utils/date

Length of output: 163


I couldn’t locate unixEpochToDate in the initial paths—let’s do a global search to find its definition and usages.


🏁 Script executed:

#!/bin/bash# Search for definition and all usages of unixEpochToDate across the repo
rg -n "unixEpochToDate"

Length of output: 10950


Let’s inspect the implementation and its tests:


🏁 Script executed:

#!/bin/bash# Show the unixEpochToDate implementation
sed -n '1,200p' packages/clerk-js/src/utils/date.ts
# Show tests covering edge cases
sed -n '1,200p' packages/clerk-js/src/utils/__tests__/date.spec.ts

Length of output: 1264


Improve zero-timestamp handling in unixEpochToDate

The current implementation

exportfunctionunixEpochToDate(epochInSeconds?: number): Date{constdate=newDate(epochInSeconds||newDate());returnisValidDate(date) ? date : newDate();}

uses a falsy check (epochInSeconds || …), so passing 0 (the Unix epoch) falls back to Date.now(). To correctly support 0, please:

  • In packages/clerk-js/src/utils/date.ts, update the guard:
    export function unixEpochToDate(epochInSeconds?: number): Date {
  • const date = new Date(epochInSeconds || new Date());
  • const timestamp = epochInSeconds != null ? epochInSeconds : Date.now();
  • const date = new Date(timestamp);
    return isValidDate(date) ? date : new Date();
    }
- In `packages/clerk-js/src/utils/__tests__/date.spec.ts`, add a zero-timestamp test:
```ts
it('correctly handles zero', () => {
const date = unixEpochToDate(0);
expect(date.getTime()).toBe(0);
});

This change ensures unixEpochToDate(0) returns the Unix epoch rather than “now.”

🤖 Prompt for AI Agents
In packages/clerk-js/src/utils/date.ts, update the unixEpochToDate function to
explicitly check for undefined or null instead of using a falsy check, so that
passing 0 returns the correct Unix epoch date. Then, in
packages/clerk-js/src/utils/__tests__/date.spec.ts, add a test case that calls
unixEpochToDate(0) and asserts that the returned date's time is 0 to verify
correct handling of the zero timestamp.


this.amount = data.amount ? commerceMoneyFromJSON(data.amount) : undefined;
this.credit = data.credit && data.credit.amount ? { amount: commerceMoneyFromJSON(data.credit.amount) } : undefined;
return this;
Expand Down
5 changes: 3 additions & 2 deletions packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -210,7 +210,7 @@ const PlanDetailsInternal = ({
{(!plan.isDefault && !isDefaultPlanImplicitlyActiveOrUpcoming) || !subscription ? (
<Drawer.Footer>
{subscription ? (
subscription.canceledAt ? (
subscription.canceledAtDate ? (
<Button
block
textVariant='buttonLarge'
Expand DownExpand Up@@ -318,7 +318,8 @@ const PlanDetailsInternal = ({
? localizationKeys('commerce.cancelSubscriptionNoCharge')
: localizationKeys('commerce.cancelSubscriptionAccessUntil', {
plan: subscription.plan.name,
date: subscription.periodEnd,
// @ts-expect-error this will always be defined in this case.
date: subscription.periodEndDate,
Comment on lines +321 to +322

Copy link
Copy Markdown
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

Verify the TypeScript suppression is justified.

The migration from periodEnd to periodEndDate is consistent with the pattern. However, the @ts-expect-error comment suggests a type issue. Please ensure that the property is indeed always defined in this context or consider updating the type definitions.


🏁 Script executed:

#!/bin/bash# Description: Check the type definition of periodEndDate in CommerceSubscriptionResource# Expected: Verify if periodEndDate is optional or required in the type definition
ast-grep --pattern 'interface CommerceSubscriptionResource { $$$ periodEndDate$_$$$ $$$}'# Also search for any type definitions related to subscription period dates
rg -A 3 -B 3 "periodEndDate.*[?:]"

Length of output: 2751


Fix the TypeScript suppression for periodEndDate
The CommerceSubscriptionResource model declares periodEndDate as Date | null, so subscription.periodEndDate may legitimately be null. Suppressing the error hides a potential runtime issue. Please remove the @ts-expect-error and either:

  • Add an explicit null check before using it, e.g.
    if(!subscription.periodEndDate){// handle missing date…}else{date: subscription.periodEndDate,}
  • Or use a non-null assertion if you’re certain it can’t be null in this context:
    - // @ts-expect-error this will always be defined in this case.- date: subscription.periodEndDate,+ date: subscription.periodEndDate!,

Locations to update:

  • packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx (lines 321–322)
  • Note: the type in packages/types/src/commerce.ts is periodEndDate: Date | null;
🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx at lines 321-322,
remove the @ts-expect-error comment on subscription.periodEndDate and address
its nullable type properly. Add an explicit null check before using
subscription.periodEndDate to handle the case when it is null, or if you are
certain it cannot be null here, replace the suppression with a non-null
assertion operator to ensure type safety without hiding potential runtime
issues.

})
}
/>
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -24,7 +24,7 @@ const PricingTableRoot = (props: PricingTableProps) => {

// don't pay attention to the default plan
const activeSubscription = subscriptions?.find(
sub => !sub.canceledAt && sub.status === 'active' && !sub.plan.isDefault,
sub => !sub.canceledAtDate && sub.status === 'active' && !sub.plan.isDefault,
);
if (activeSubscription) {
return activeSubscription.planPeriod;
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -143,7 +143,7 @@ function Card(props: CardProps) {
shouldShowFooter = true;
shouldShowFooterNotice = true;
} else if (subscription.status === 'active') {
if (subscription.canceledAt) {
if (subscription.canceledAtDate) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyAmount > 0) {
Expand DownExpand Up@@ -253,7 +253,7 @@ function Card(props: CardProps) {
elementDescriptor={descriptors.pricingTableCardFooterNotice}
variant={isCompact ? 'buttonSmall' : 'buttonLarge'}
localizationKey={localizationKeys('badge__startsAt', {
date: subscription?.periodStart,
date: subscription?.periodStartDate,
})}
colorScheme='secondary'
sx={t => ({
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -125,7 +125,7 @@ export function SubscriptionsList({
>
{subscription.plan.name}
</Text>
{sortedSubscriptions.length > 1 || !!subscription.canceledAt ? (
{sortedSubscriptions.length > 1 || !!subscription.canceledAtDate ? (
<Badge
colorScheme={subscription.status === 'active' ? 'secondary' : 'primary'}
localizationKey={
Expand Down
31 changes: 18 additions & 13 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -98,9 +98,9 @@ export const useSubscriptions = () => {
if (
isSignedIn &&
defaultFreePlan &&
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAt))
(_subscriptions.data.length === 0 || !_subscriptions.data.some(subscription => !subscription.canceledAtDate))
) {
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAt);
const canceledSubscription = _subscriptions.data.find(subscription => subscription.canceledAtDate);
return [
..._subscriptions.data,
new CommerceSubscription({
Expand All@@ -111,7 +111,8 @@ export const useSubscriptions = () => {
plan_period: 'month',
canceled_at: null,
status: _subscriptions.data.length === 0 ? 'active' : 'upcoming',
period_start: canceledSubscription?.periodEnd || 0,
created_at: canceledSubscription?.periodEndDate?.getTime() || 0,
period_start: canceledSubscription?.periodEndDate?.getTime() || 0,
period_end: 0,
}),
];
Expand DownExpand Up@@ -190,7 +191,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 subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAt);
return subscriptions.length === 0 || !subscriptions.some(subscription => !subscription.canceledAtDate);
}, [subscriptions]);

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

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

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

Expand DownExpand Up@@ -332,12 +333,16 @@ export const usePlansContext = () => {

const captionForSubscription = useCallback((subscription: CommerceSubscriptionResource) => {
if (subscription.status === 'upcoming') {
return localizationKeys('badge__startsAt', { date: subscription.periodStart });
} else if (subscription.canceledAt) {
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEnd });
} else {
return localizationKeys('badge__renewsAt', { date: subscription.periodEnd });
return localizationKeys('badge__startsAt', { date: subscription.periodStartDate });
}
if (subscription.canceledAtDate) {
// @ts-expect-error `periodEndDate` is always defined when `canceledAtDate` exists
return localizationKeys('badge__canceledEndsAt', { date: subscription.periodEndDate });
}
if (subscription.periodEndDate) {
return localizationKeys('badge__renewsAt', { date: subscription.periodEndDate });
}
return;
}, []);

// handle the selection of a plan, either by opening the subscription details or checkout
Expand All@@ -355,7 +360,7 @@ export const usePlansContext = () => {

const portalRoot = getClosestProfileScrollBox(mode, event);

if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAt) {
if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAtDate) {
clerk.__internal_openPlanDetails({
plan,
initialPlanPeriod: planPeriod,
Expand Down
13 changes: 13 additions & 0 deletions packages/types/src/commerce.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -155,8 +155,21 @@ export interface CommerceSubscriptionResource extends ClerkResource {
plan: CommercePlanResource;
planPeriod: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
createdAt: Date;
periodStartDate: Date;
periodEndDate: Date | null;
canceledAtDate: Date | null;
/**
* @deprecated Use `periodStartDate` instead
*/
periodStart: number;
/**
* @deprecated Use `periodEndDate` instead
*/
periodEnd: number;
/**
* @deprecated Use `canceledAtDate` instead
*/
canceledAt: number | null;
amount?: CommerceMoney;
credit?: {
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/json.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -701,6 +701,7 @@ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
plan: CommercePlanJSON;
plan_period: CommerceSubscriptionPlanPeriod;
status: CommerceSubscriptionStatus;
created_at: number;
period_start: number;
period_end: number;
canceled_at: number | null;
Expand Down