-
Notifications
You must be signed in to change notification settings - Fork 371
chore(clerk-js,types): Update checkout flow to support free trials #6494
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. |
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. |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -27,6 +27,7 @@ export class CommerceSubscription extends BaseResource implements CommerceSubscr | |||||||||||||||||||||||||||||||||||||||||||||||||
date: Date; | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | null = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
subscriptionItems!: CommerceSubscriptionItemResource[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||
eligibleForFreeTrial?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+30
to
31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainAlign typing and default for eligibleForFreeTrial. Currently typed as optional and directly assigned, which can propagate undefined. Consider one of:
Option A: - eligibleForFreeTrial?: boolean;
+ eligibleForFreeTrial!: boolean;
...
- this.eligibleForFreeTrial = data.eligible_for_free_trial;
+ this.eligibleForFreeTrial = this.withDefault(data.eligible_for_free_trial, false); To gauge impact, search call sites: Also applies to: 55-56 🏁 Script executed: #!/bin/bash
set -euo pipefail
rg -n "eligibleForFreeTrial" packages Length of output: 494 Align eligibleForFreeTrial typing and default to false To ensure consumers always get a boolean (never
This change makes 📝 Committable suggestion
Suggested change
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||
constructor(data: CommerceSubscriptionJSON) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
super(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -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; | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -74,6 +76,7 @@ export class CommerceSubscriptionItem extends BaseResource implements CommerceSu | |||||||||||||||||||||||||||||||||||||||||||||||||
credit?: { | ||||||||||||||||||||||||||||||||||||||||||||||||||
amount: CommerceMoney; | ||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||
freeTrialEndsAt!: Date | null; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
constructor(data: CommerceSubscriptionItemJSON) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
super(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -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 on lines
+110
to
111
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Use nullish check when mapping timestamps. Avoid truthy checks for numeric timestamps; use a nullish check to prevent edge-case misclassification. - this.freeTrialEndsAt = data.free_trial_ends_at ? unixEpochToDate(data.free_trial_ends_at) : null;
+ this.freeTrialEndsAt =
+ data.free_trial_ends_at != null ? unixEpochToDate(data.free_trial_ends_at) : null; 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -17,6 +17,7 @@ import { Box, Button, Col, descriptors, Flex, Form, localizationKeys, Text } fro | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { ChevronUpDown, InformationCircle } from '../../icons'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import * as AddPaymentSource from '../PaymentSources/AddPaymentSource'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { PaymentSourceRow } from '../PaymentSources/PaymentSourceRow'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
import { SubscriptionBadge } from '../Subscriptions/badge'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
type PaymentMethodSource = 'existing' | 'new'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -25,7 +26,7 @@ const capitalize = (name: string) => name[0].toUpperCase() + name.slice(1); | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
export const CheckoutForm = withCardStateProvider(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { checkout } = useCheckout(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { id, plan, totals, isImmediatePlanChange, planPeriod } = checkout; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { id, plan, totals, isImmediatePlanChange, planPeriod, freeTrialEndsAt } = checkout; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (!id) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -51,6 +52,11 @@ export const CheckoutForm = withCardStateProvider(() => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<LineItems.Title | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
title={plan.name} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
description={planPeriod === 'annual' ? localizationKeys('commerce.billedAnnually') : undefined} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
badge={ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
plan.freeTrialEnabled && freeTrialEndsAt ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<SubscriptionBadge subscription={{ status: 'free_trial' }} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) : null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<LineItems.Description | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
prefix={planPeriod === 'annual' ? 'x12' : undefined} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -85,6 +91,20 @@ export const CheckoutForm = withCardStateProvider(() => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<LineItems.Description text={`${totals.pastDue?.currencySymbol}${totals.pastDue?.amountFormatted}`} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</LineItems.Group> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{freeTrialEndsAt && plan.freeTrialDays && ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<LineItems.Group variant='tertiary'> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<LineItems.Title | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
title={localizationKeys('commerce.checkout.totalDueAfterTrial', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
days: plan.freeTrialDays, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
})} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<LineItems.Description | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
text={`${totals.grandTotal?.currencySymbol}${totals.grandTotal?.amountFormatted}`} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</LineItems.Group> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+95
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Handle missing The label always injects Guard the value or fall back to 🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<LineItems.Group borderTop> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<LineItems.Title title={localizationKeys('commerce.totalDueToday')} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<LineItems.Description text={`${totals.totalDueNow.currencySymbol}${totals.totalDueNow.amountFormatted}`} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -276,15 +296,32 @@ export const PayWithTestPaymentSource = () => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const AddPaymentSourceForCheckout = withCardStateProvider(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { addPaymentSourceAndPay } = useCheckoutMutations(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const useSubmitLabel = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { checkout } = useCheckout(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { status, totals } = checkout; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { status, freeTrialEndsAt, totals } = checkout; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (status === 'needs_initialization') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
throw new Error('Clerk: Invalid state'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (freeTrialEndsAt) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return localizationKeys('commerce.startFreeTrial'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if (totals.totalDueNow.amount > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return localizationKeys('commerce.pay', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
amount: `${totals.totalDueNow.currencySymbol}${totals.totalDueNow.amountFormatted}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return localizationKeys('commerce.subscribe'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+299
to
+318
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion
Throwing when - if (status === 'needs_initialization') {
- throw new Error('Clerk: Invalid state');
- }
+ if (status === 'needs_initialization') {
+ return localizationKeys('formButtonPrimary'); // fallback label
+ } Avoids unhandled exceptions in edge cases. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const AddPaymentSourceForCheckout = withCardStateProvider(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { addPaymentSourceAndPay } = useCheckoutMutations(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const submitLabel = useSubmitLabel(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { checkout } = useCheckout(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<AddPaymentSource.Root | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
onSuccess={addPaymentSourceAndPay} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -294,15 +331,7 @@ const AddPaymentSourceForCheckout = withCardStateProvider(() => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<PayWithTestPaymentSource /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</DevOnly> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{totals.totalDueNow.amount > 0 ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<AddPaymentSource.FormButton | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
text={localizationKeys('commerce.pay', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
amount: `${totals.totalDueNow.currencySymbol}${totals.totalDueNow.amountFormatted}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
})} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) : ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<AddPaymentSource.FormButton text={localizationKeys('commerce.subscribe')} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<AddPaymentSource.FormButton text={submitLabel} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</AddPaymentSource.Root> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -315,8 +344,9 @@ const ExistingPaymentSourceForm = withCardStateProvider( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
totalDueNow: CommerceMoney; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
paymentSources: CommercePaymentSourceResource[]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const submitLabel = useSubmitLabel(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { checkout } = useCheckout(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { paymentSource } = checkout; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { paymentSource, freeTrialEndsAt } = checkout; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { payWithExistingPaymentSource } = useCheckoutMutations(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const card = useCardState(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -338,6 +368,8 @@ const ExistingPaymentSourceForm = withCardStateProvider( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, [paymentSources]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const isSchedulePayment = totalDueNow.amount > 0 && !freeTrialEndsAt; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Form | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
onSubmit={payWithExistingPaymentSource} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -347,7 +379,7 @@ const ExistingPaymentSourceForm = withCardStateProvider( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
rowGap: t.space.$4, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
})} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{totalDueNow.amount > 0 ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
{isSchedulePayment ? ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Select | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
elementId='paymentSource' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
options={options} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -397,17 +429,8 @@ const ExistingPaymentSourceForm = withCardStateProvider( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
width: '100%', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
isLoading={card.isLoading} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<Text | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
localizationKey={ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
totalDueNow.amount > 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
? localizationKeys('commerce.pay', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
amount: `${totalDueNow.currencySymbol}${totalDueNow.amountFormatted}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
: localizationKeys('commerce.subscribe') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
localizationKey={submitLabel} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</Form> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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; | ||||||||||||||||||||||
Comment on lines
+150
to
+152
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Guard against past trials: only show notice if trial end is in the future. Apply: - } else if (plan.freeTrialEnabled && subscription.freeTrialEndsAt !== null) {
+ } else if (
+ plan.freeTrialEnabled &&
+ subscription.freeTrialEndsAt !== null &&
+ subscription.freeTrialEndsAt.getTime() > Date.now()
+ ) {
shouldShowFooter = true;
shouldShowFooterNotice = true; 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||
} else { | ||||||||||||||||||||||
shouldShowFooter = false; | ||||||||||||||||||||||
shouldShowFooterNotice = false; | ||||||||||||||||||||||
|
@@ -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, | ||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,19 +7,21 @@ const keys = { | |
active: 'badge__activePlan', | ||
upcoming: 'badge__upcomingPlan', | ||
past_due: 'badge__pastDuePlan', | ||
free_trial: 'badge__freeTrial', | ||
}; | ||
|
||
const colors = { | ||
active: 'secondary', | ||
upcoming: 'primary', | ||
past_due: 'warning', | ||
free_trial: 'secondary', | ||
}; | ||
|
||
export const SubscriptionBadge = ({ | ||
export const SubscriptionBadge = <T extends { status: CommerceSubscriptionItemResource['status'] }>({ | ||
subscription, | ||
elementDescriptor, | ||
}: { | ||
subscription: CommerceSubscriptionItemResource; | ||
subscription: T | { status: 'free_trial' }; | ||
elementDescriptor?: ElementDescriptor; | ||
Comment on lines
+20
to
25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Inconsistent typing for The component now unions Prefer updating This restores compile-time safety and removes the need for the union / ignores. 🤖 Prompt for AI Agents
|
||
}) => { | ||
return ( | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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' }); | ||||||||||||||||||||||||||||||||
|
@@ -187,6 +187,7 @@ export const usePlansContext = () => { | |||||||||||||||||||||||||||||||
const buttonPropsForPlan = useCallback( | ||||||||||||||||||||||||||||||||
({ | ||||||||||||||||||||||||||||||||
plan, | ||||||||||||||||||||||||||||||||
// TODO(@COMMERCE): This needs to be removed. | ||||||||||||||||||||||||||||||||
subscription: sub, | ||||||||||||||||||||||||||||||||
isCompact = false, | ||||||||||||||||||||||||||||||||
selectedPlanPeriod = 'annual', | ||||||||||||||||||||||||||||||||
|
@@ -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', { days: plan.freeTrialDays ?? 0 }); | ||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||
return localizationKey; | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
Comment on lines
+215
to
+221
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Guard against zero / null trial days
- return localizationKeys('commerce.startFreeTrial__days', { days: plan.freeTrialDays ?? 0 });
+const days = plan.freeTrialDays ?? 0;
+return days > 0
+ ? localizationKeys('commerce.startFreeTrial__days', { days })
+ : localizationKeys('commerce.startFreeTrial'); This keeps the copy natural when the duration is unknown or disabled. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||
const getLocalizationKey = () => { | ||||||||||||||||||||||||||||||||
// Handle subscription cases | ||||||||||||||||||||||||||||||||
if (subscription) { | ||||||||||||||||||||||||||||||||
|
@@ -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, | ||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||
[activeOrUpcomingSubscriptionWithPlanPeriod, canManageBilling, subscriptionItems], | ||||||||||||||||||||||||||||||||
[activeOrUpcomingSubscriptionWithPlanPeriod, canManageBilling, subscriptionItems, topLevelSubscription], | ||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||
const captionForSubscription = useCallback((subscription: CommerceSubscriptionItemResource) => { | ||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -84,9 +84,10 @@ interface TitleProps { | |
title?: string | LocalizationKey; | ||
description?: string | LocalizationKey; | ||
icon?: React.ComponentType; | ||
badge?: React.ReactNode; | ||
} | ||
Comment on lines
+87
to
88
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainBadge slot theming/localization contract; add JSDoc (and consider a theming wrapper). Good addition. To align with our UI guidelines:
Apply JSDoc on the prop (Lines 87-88): interface TitleProps {
title?: string | LocalizationKey;
description?: string | LocalizationKey;
icon?: React.ComponentType;
- badge?: React.ReactNode;
+ /**
+ * Optional badge element rendered next to the title.
+ * Caller is responsible for localization and theming of the badge contents (e.g. pass <SubscriptionBadge />).
+ */
+ badge?: React.ReactNode;
} Optional theming slot wrapper (Lines 124-125) — requires adding a corresponding descriptor (e.g. lineItemsTitleBadge) to descriptors before use: - {badge}
+ {/* wrap to expose a stable theming hook */}
+ {badge ? (
+ <Span elementDescriptor={descriptors.lineItemsTitleBadge}>
+ {badge}
+ </Span>
+ ) : null} To ensure consistency, verify the SubscriptionBadge component uses elementDescriptor and localized strings: Also applies to: 90-90, 124-125 🏁 Script executed: #!/bin/bash
set -euo pipefail
# Verify the badge implementation uses elementDescriptor and localization
fd badge.tsx packages | xargs -I {} sh -c 'echo "----- {}"; sed -n "1,200p" "{}" | rg -n "elementDescriptor|localizationKey|localizationKeys" -n || true' Length of output: 1468 Document The • At Line 87–88 ( interface TitleProps {
title?: string | LocalizationKey;
description?: string | LocalizationKey;
icon?: React.ComponentType;
- badge?: React.ReactNode;
+ /**
+ * Optional badge element rendered next to the title.
+ * The caller is responsible for providing a localized and themed badge
+ * (e.g. `<SubscriptionBadge elementDescriptor={descriptors.lineItemsTitleBadge} />`).
+ */
+ badge?: React.ReactNode;
} • At the render site (around Line 124–125), wrap - {badge}
+ {badge ? (
+ <Span elementDescriptor={descriptors.lineItemsTitleBadge}>
+ {badge}
+ </Span>
+ ) : null} This ensures consumers can target 🤖 Prompt for AI Agents
|
||
|
||
const Title = React.forwardRef<HTMLTableCellElement, TitleProps>(({ title, description, icon }, ref) => { | ||
const Title = React.forwardRef<HTMLTableCellElement, TitleProps>(({ title, description, icon, badge = null }, ref) => { | ||
const context = React.useContext(GroupContext); | ||
if (!context) { | ||
throw new Error('LineItems.Title must be used within LineItems.Group'); | ||
|
@@ -120,6 +121,7 @@ const Title = React.forwardRef<HTMLTableCellElement, TitleProps>(({ title, descr | |
/> | ||
) : null} | ||
<Span localizationKey={title} /> | ||
{badge} | ||
</Span> | ||
) : null} | ||
{description ? ( | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Snapshot parity: include free trial fields in __internal_toSnapshot.
Downstream consumers that rely on snapshots will miss these new fields. Include them (and update the corresponding snapshot type if needed).
To confirm types and call sites:
Also applies to: 68-91
🏁 Script executed:
Length of output: 1029
Add free trial fields to
__internal_toSnapshot
for snapshot parityWe verified that the
CommercePlanJSONSnapshot
type already includesso no type updates are needed. However, the two new properties must be
exported in the snapshot method:
• File: packages/clerk-js/src/core/resources/CommercePlan.ts
• Method:
public __internal_toSnapshot(): CommercePlanJSONSnapshot
Apply this diff:
🤖 Prompt for AI Agents