Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/sour-lemons-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Update billing resources with trial properties.
7 changes: 7 additions & 0 deletions .changeset/tender-planets-win.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/localizations': minor
'@clerk/clerk-js': minor
'@clerk/types': minor
---

Update PricingTable with trial info.
4 changes: 4 additions & 0 deletions packages/clerk-js/src/core/resources/CommercePlan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export class CommercePlan extends BaseResource implements CommercePlanResource {
slug!: string;
avatarUrl!: string;
features!: CommerceFeature[];
freeTrialDays!: number | null;
freeTrialEnabled!: boolean;

constructor(data: CommercePlanJSON) {
super();
Expand Down Expand Up @@ -56,6 +58,8 @@ export class CommercePlan extends BaseResource implements CommercePlanResource {
this.publiclyVisible = data.publicly_visible;
this.slug = data.slug;
this.avatarUrl = data.avatar_url;
this.freeTrialDays = this.withDefault(data.free_trial_days, null);
this.freeTrialEnabled = this.withDefault(data.free_trial_enabled, false);
this.features = (data.features || []).map(feature => new CommerceFeature(feature));

return this;
Expand Down
5 changes: 5 additions & 0 deletions packages/clerk-js/src/core/resources/CommerceSubscription.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
date: Date;
} | null = null;
subscriptionItems!: CommerceSubscriptionItemResource[];
eligibleForFreeTrial?: boolean;

constructor(data: CommerceSubscriptionJSON) {
super();
Expand All @@ -51,6 +52,7 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr
}
: null;
this.subscriptionItems = (data.subscription_items || []).map(item => new CommerceSubscriptionItem(item));
this.eligibleForFreeTrial = data.eligible_for_free_trial;
return this;
}
}
Expand All @@ -74,6 +76,7 @@ export class CommerceSubscriptionItem extends BaseResource implements CommerceSu
credit?: {
amount: CommerceMoney;
};
freeTrialEndsAt!: Date | null;

constructor(data: CommerceSubscriptionItemJSON) {
super();
Expand Down Expand Up @@ -103,6 +106,8 @@ export class CommerceSubscriptionItem extends BaseResource implements CommerceSu

this.amount = data.amount ? commerceMoneyFromJSON(data.amount) : undefined;
this.credit = data.credit && data.credit.amount ? { amount: commerceMoneyFromJSON(data.credit.amount) } : undefined;

this.freeTrialEndsAt = data.free_trial_ends_at ? unixEpochToDate(data.free_trial_ends_at) : null;
return this;
Comment thread
panteliselef marked this conversation as resolved.
Outdated
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@ function Card(props: CardProps) {
} else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyAmount > 0) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else if (plan.freeTrialEnabled && subscription.freeTrialEndsAt !== null) {
shouldShowFooter = true;
shouldShowFooterNotice = true;
} else {
shouldShowFooter = false;
shouldShowFooterNotice = false;
Expand Down Expand Up @@ -232,9 +235,13 @@ function Card(props: CardProps) {
<Text
elementDescriptor={descriptors.pricingTableCardFooterNotice}
variant={isCompact ? 'buttonSmall' : 'buttonLarge'}
localizationKey={localizationKeys('badge__startsAt', {
date: subscription?.periodStartDate,
})}
localizationKey={
plan.freeTrialEnabled && subscription.freeTrialEndsAt !== null
? localizationKeys('badge__trialEndsAt', {
date: subscription?.freeTrialEndsAt,
})
: localizationKeys('badge__startsAt', { date: subscription?.periodStartDate })
}
colorScheme='secondary'
sx={t => ({
paddingBlock: t.space.$1x5,
Expand Down
17 changes: 13 additions & 4 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ export const usePlansContext = () => {
return false;
}, [clerk, subscriberType]);

const { subscriptionItems, revalidate: revalidateSubscriptions } = useSubscription();
const { subscriptionItems, revalidate: revalidateSubscriptions, data: topLevelSubscription } = useSubscription();

// Invalidates cache but does not fetch immediately
const { data: plans, revalidate: revalidatePlans } = usePlans({ mode: 'cache' });
Expand Down Expand Up @@ -187,6 +187,7 @@ export const usePlansContext = () => {
const buttonPropsForPlan = useCallback(
({
plan,
// TODO(@COMMERCE): This needs to be removed.
subscription: sub,
Comment thread
panteliselef marked this conversation as resolved.
isCompact = false,
selectedPlanPeriod = 'annual',
Expand All @@ -211,6 +212,13 @@ export const usePlansContext = () => {

const isEligibleForSwitchToAnnual = (plan?.annualMonthlyAmount ?? 0) > 0;

const freeTrialOr = (localizationKey: LocalizationKey): LocalizationKey => {
if (plan?.freeTrialEnabled && topLevelSubscription?.eligibleForFreeTrial) {
return localizationKeys('commerce.startFreeTrial', { days: plan.freeTrialDays ?? 0 });
}
return localizationKey;
};

Comment thread
coderabbitai[bot] marked this conversation as resolved.
const getLocalizationKey = () => {
// Handle subscription cases
if (subscription) {
Expand Down Expand Up @@ -246,20 +254,21 @@ export const usePlansContext = () => {
// Handle non-subscription cases
const hasNonDefaultSubscriptions =
subscriptionItems.filter(subscription => !subscription.plan.isDefault).length > 0;

return hasNonDefaultSubscriptions
? localizationKeys('commerce.switchPlan')
: localizationKeys('commerce.subscribe');
: freeTrialOr(localizationKeys('commerce.subscribe'));
};

return {
localizationKey: getLocalizationKey(),
localizationKey: freeTrialOr(getLocalizationKey()),
variant: isCompact ? 'bordered' : 'solid',
colorScheme: isCompact ? 'secondary' : 'primary',
isDisabled: !canManageBilling,
disabled: !canManageBilling,
};
Comment thread
panteliselef marked this conversation as resolved.
},
[activeOrUpcomingSubscriptionWithPlanPeriod, canManageBilling, subscriptionItems],
[activeOrUpcomingSubscriptionWithPlanPeriod, canManageBilling, subscriptionItems, topLevelSubscription],
);

const captionForSubscription = useCallback((subscription: CommerceSubscriptionItemResource) => {
Expand Down
2 changes: 2 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export const enUS: LocalizationResource = {
badge__default: 'Default',
badge__endsAt: "Ends {{ date | shortDate('en-US') }}",
badge__expired: 'Expired',
badge__trialEndsAt: "Trial ends {{ date | shortDate('en-US') }}",
Comment thread
panteliselef marked this conversation as resolved.
Outdated
badge__otherImpersonatorDevice: 'Other impersonator device',
badge__pastDueAt: "Past due {{ date | shortDate('en-US') }}",
badge__pastDuePlan: 'Past due',
Expand Down Expand Up @@ -140,6 +141,7 @@ export const enUS: LocalizationResource = {
},
subtotal: 'Subtotal',
switchPlan: 'Switch to this plan',
startFreeTrial: 'Start {{days}}-day free trial',
switchToAnnual: 'Switch to annual',
switchToAnnualWithAnnualPrice: 'Switch to annual {{currency}}{{price}} / year',
switchToMonthly: 'Switch to monthly',
Expand Down
47 changes: 47 additions & 0 deletions packages/types/src/commerce.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,24 @@ export interface CommercePlanResource extends ClerkResource {
* ```
*/
features: CommerceFeatureResource[];
/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
* @example
* ```tsx
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
freeTrialDays: number | null;
/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
* @example
* ```tsx
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
freeTrialEnabled: boolean;
__internal_toSnapshot: () => CommercePlanJSONSnapshot;
}

Expand Down Expand Up @@ -1106,6 +1124,25 @@ export interface CommerceSubscriptionItemResource extends ClerkResource {
* ```
*/
cancel: (params: CancelSubscriptionParams) => Promise<DeletedObjectResource>;
// /**
// * @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
// * It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
// * @example
// * ```tsx
// * <ClerkProvider clerkJsVersion="x.x.x" />
// * ```
// */
// isFreeTrial: boolean;

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
* @example
* ```tsx
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
freeTrialEndsAt: Date | null;
}

/**
Expand Down Expand Up @@ -1215,6 +1252,16 @@ export interface CommerceSubscriptionResource extends ClerkResource {
* ```
*/
updatedAt: Date | null;

/**
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change.
* It is advised to pin the SDK version and the clerk-js version to a specific version to avoid breaking changes.
* @example
* ```tsx
* <ClerkProvider clerkJsVersion="x.x.x" />
* ```
*/
eligibleForFreeTrial?: boolean;
}

/**
Expand Down
6 changes: 6 additions & 0 deletions packages/types/src/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,8 @@ export interface CommercePlanJSON extends ClerkResourceJSON {
slug: string;
avatar_url: string;
features: CommerceFeatureJSON[];
free_trial_days?: number | null;
free_trial_enabled?: boolean;
}

/**
Expand Down Expand Up @@ -780,6 +782,9 @@ export interface CommerceSubscriptionItemJSON extends ClerkResourceJSON {
period_end: number;
canceled_at: number | null;
past_due_at: number | null;
// is_free_trial: boolean;
// TODO(@COMMERCE): Remove optional after GA.
free_trial_ends_at?: number | null;
}

/**
Expand Down Expand Up @@ -809,6 +814,7 @@ export interface CommerceSubscriptionJSON extends ClerkResourceJSON {
updated_at: number | null;
past_due_at: number | null;
subscription_items: CommerceSubscriptionItemJSON[] | null;
eligible_for_free_trial?: boolean;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions packages/types/src/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export type __internal_LocalizationResource = {
badge__pastDuePlan: LocalizationValue;
badge__startsAt: LocalizationValue<'date'>;
badge__pastDueAt: LocalizationValue<'date'>;
badge__trialEndsAt: LocalizationValue<'date'>;
badge__endsAt: LocalizationValue;
badge__expired: LocalizationValue;
badge__canceledEndsAt: LocalizationValue<'date'>;
Expand All @@ -174,6 +175,7 @@ export type __internal_LocalizationResource = {
keepSubscription: LocalizationValue;
reSubscribe: LocalizationValue;
subscribe: LocalizationValue;
startFreeTrial: LocalizationValue<'days'>;
switchPlan: LocalizationValue;
switchToMonthly: LocalizationValue;
switchToAnnual: LocalizationValue;
Expand Down
Loading