Skip to content

Commit bde228d

Browse files
octoperaeliox
andauthored
refactor(clerk-js): Improve switching between plans flows (#5883)
Co-authored-by: Keiran Flanigan <keiran@aeliox.com>
1 parent 45eb6eb commit bde228d

File tree

4 files changed

+116
-32
lines changed

4 files changed

+116
-32
lines changed

.changeset/little-cases-knock.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Improvements of flows for switching between plans

packages/clerk-js/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{ "path": "./dist/clerk.browser.js", "maxSize": "68.3KB" },
55
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "110KB" },
66
{ "path": "./dist/clerk.headless*.js", "maxSize": "52KB" },
7-
{ "path": "./dist/ui-common*.js", "maxSize": "104.1KB" },
7+
{ "path": "./dist/ui-common*.js", "maxSize": "104.20KB" },
88
{ "path": "./dist/vendors*.js", "maxSize": "39.5KB" },
99
{ "path": "./dist/coinbase*.js", "maxSize": "38KB" },
1010
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },

packages/clerk-js/src/ui/components/PricingTable/PricingTableDefault.tsx

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,7 @@ function Card(props: CardProps) {
109109
const canManageBilling = useProtect(
110110
has => has({ permission: 'org:sys_billing:manage' }) || subscriberType === 'user',
111111
);
112-
113-
const { buttonPropsForPlan, upcomingSubscriptionsExist, activeOrUpcomingSubscription } = usePlansContext();
112+
const { buttonPropsForPlan, activeOrUpcomingSubscriptionBasedOnPlanPeriod } = usePlansContext();
114113

115114
const showPlanDetails = (event?: React.MouseEvent<HTMLElement>) => {
116115
const portalRoot = getClosestProfileScrollBox(mode, event);
@@ -123,21 +122,38 @@ function Card(props: CardProps) {
123122
});
124123
};
125124

126-
const subscription = activeOrUpcomingSubscription(plan);
127-
const hasFeatures = plan.features.length > 0;
125+
const subscription = React.useMemo(
126+
() => activeOrUpcomingSubscriptionBasedOnPlanPeriod(plan, planPeriod),
127+
[plan, planPeriod, activeOrUpcomingSubscriptionBasedOnPlanPeriod],
128+
);
128129
const isPlanActive = subscription?.status === 'active';
130+
const hasFeatures = plan.features.length > 0;
129131
const showStatusRow = !!subscription;
130-
const isEligibleForSwitchToAnnual = plan.annualMonthlyAmount > 0 && planPeriod === 'annual';
131132

132-
const shouldShowFooter =
133-
!subscription ||
134-
subscription?.status === 'upcoming' ||
135-
subscription?.canceledAt ||
136-
(planPeriod !== subscription?.planPeriod && !plan.isDefault && isEligibleForSwitchToAnnual);
137-
const shouldShowFooterNotice =
138-
subscription?.status === 'upcoming' && (planPeriod === subscription.planPeriod || plan.isDefault);
133+
let shouldShowFooter = false;
134+
let shouldShowFooterNotice = false;
139135

140-
const planPeriodSameAsSelectedPlanPeriod = !upcomingSubscriptionsExist && subscription?.planPeriod === planPeriod;
136+
if (!subscription) {
137+
shouldShowFooter = true;
138+
shouldShowFooterNotice = false;
139+
} else if (subscription.status === 'upcoming') {
140+
shouldShowFooter = true;
141+
shouldShowFooterNotice = true;
142+
} else if (subscription.status === 'active') {
143+
if (subscription.canceledAt) {
144+
shouldShowFooter = true;
145+
shouldShowFooterNotice = false;
146+
} else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyAmount > 0) {
147+
shouldShowFooter = true;
148+
shouldShowFooterNotice = false;
149+
} else {
150+
shouldShowFooter = false;
151+
shouldShowFooterNotice = false;
152+
}
153+
} else {
154+
shouldShowFooter = false;
155+
shouldShowFooterNotice = false;
156+
}
141157

142158
return (
143159
<Box
@@ -226,7 +242,6 @@ function Card(props: CardProps) {
226242
borderTopWidth: t.borderWidths.$normal,
227243
borderTopStyle: t.borderStyles.$solid,
228244
borderTopColor: t.colors.$neutralAlpha100,
229-
background: planPeriodSameAsSelectedPlanPeriod && hasFeatures ? t.colors.$colorBackground : undefined,
230245
order: ctaPosition === 'top' ? -1 : undefined,
231246
})}
232247
>

packages/clerk-js/src/ui/contexts/components/Plans.tsx

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,40 @@ export const usePlansContext = () => {
176176
[ctx.subscriptions],
177177
);
178178

179+
// returns all subscriptions for a plan that are active or upcoming
180+
const activeAndUpcomingSubscriptions = useCallback(
181+
(plan: CommercePlanResource) => {
182+
return ctx.subscriptions.filter(subscription => subscription.plan.id === plan.id);
183+
},
184+
[ctx.subscriptions],
185+
);
186+
187+
// return the active or upcoming subscription for a plan based on the plan period, if there is no subscription for the plan period, return the first subscription
188+
const activeOrUpcomingSubscriptionWithPlanPeriod = useCallback(
189+
(plan: CommercePlanResource, planPeriod: CommerceSubscriptionPlanPeriod = 'month') => {
190+
const plansSubscriptions = activeAndUpcomingSubscriptions(plan);
191+
// Handle multiple subscriptions for the same plan
192+
if (plansSubscriptions.length > 1) {
193+
const subscriptionBaseOnPanPeriod = plansSubscriptions.find(subscription => {
194+
return subscription.planPeriod === planPeriod;
195+
});
196+
197+
if (subscriptionBaseOnPanPeriod) {
198+
return subscriptionBaseOnPanPeriod;
199+
}
200+
201+
return plansSubscriptions[0];
202+
}
203+
204+
if (plansSubscriptions.length === 1) {
205+
return plansSubscriptions[0];
206+
}
207+
208+
return undefined;
209+
},
210+
[activeAndUpcomingSubscriptions],
211+
);
212+
179213
const canManageSubscription = useCallback(
180214
({ plan, subscription: sub }: { plan?: CommercePlanResource; subscription?: CommerceSubscriptionResource }) => {
181215
const subscription = sub ?? (plan ? activeOrUpcomingSubscription(plan) : undefined);
@@ -212,36 +246,64 @@ export const usePlansContext = () => {
212246
isDisabled: boolean;
213247
disabled: boolean;
214248
} => {
215-
const subscription = sub ?? (plan ? activeOrUpcomingSubscription(plan) : undefined);
249+
const subscription =
250+
sub ?? (plan ? activeOrUpcomingSubscriptionWithPlanPeriod(plan, selectedPlanPeriod) : undefined);
216251
let _selectedPlanPeriod = selectedPlanPeriod;
217252
if (_selectedPlanPeriod === 'annual' && sub?.plan.annualMonthlyAmount === 0) {
218253
_selectedPlanPeriod = 'month';
219254
}
220255

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

258+
const getLocalizationKey = () => {
259+
// Handle subscription cases
260+
if (subscription) {
261+
if (_selectedPlanPeriod !== subscription.planPeriod && subscription.canceledAt) {
262+
if (_selectedPlanPeriod === 'month') {
263+
return localizationKeys('commerce.switchToMonthly');
264+
}
265+
266+
if (isEligibleForSwitchToAnnual) {
267+
return localizationKeys('commerce.switchToAnnual');
268+
}
269+
}
270+
271+
if (subscription.canceledAt) {
272+
return localizationKeys('commerce.reSubscribe');
273+
}
274+
275+
if (_selectedPlanPeriod !== subscription.planPeriod) {
276+
if (_selectedPlanPeriod === 'month') {
277+
return localizationKeys('commerce.switchToMonthly');
278+
}
279+
280+
if (isEligibleForSwitchToAnnual) {
281+
return localizationKeys('commerce.switchToAnnual');
282+
}
283+
284+
return localizationKeys('commerce.manageSubscription');
285+
}
286+
287+
return localizationKeys('commerce.manageSubscription');
288+
}
289+
290+
// Handle non-subscription cases
291+
const hasNonDefaultSubscriptions =
292+
ctx.subscriptions.filter(subscription => !subscription.plan.isDefault).length > 0;
293+
return hasNonDefaultSubscriptions
294+
? localizationKeys('commerce.switchPlan')
295+
: localizationKeys('commerce.subscribe');
296+
};
297+
223298
return {
224-
localizationKey: subscription
225-
? subscription.canceledAt
226-
? localizationKeys('commerce.reSubscribe')
227-
: selectedPlanPeriod !== subscription.planPeriod
228-
? selectedPlanPeriod === 'month'
229-
? localizationKeys('commerce.switchToMonthly')
230-
: isEligibleForSwitchToAnnual
231-
? localizationKeys('commerce.switchToAnnual')
232-
: localizationKeys('commerce.manageSubscription')
233-
: localizationKeys('commerce.manageSubscription')
234-
: // If there are no active or grace period subscriptions, show the get started button
235-
ctx.subscriptions.filter(subscription => !subscription.plan.isDefault).length > 0
236-
? localizationKeys('commerce.switchPlan')
237-
: localizationKeys('commerce.subscribe'),
299+
localizationKey: getLocalizationKey(),
238300
variant: isCompact ? 'bordered' : 'solid',
239301
colorScheme: isCompact ? 'secondary' : 'primary',
240302
isDisabled: !canManageBilling,
241303
disabled: !canManageBilling,
242304
};
243305
},
244-
[activeOrUpcomingSubscription, canManageBilling, ctx.subscriptions],
306+
[activeOrUpcomingSubscriptionWithPlanPeriod, canManageBilling, ctx.subscriptions],
245307
);
246308

247309
const captionForSubscription = useCallback((subscription: CommerceSubscriptionResource) => {
@@ -261,7 +323,7 @@ export const usePlansContext = () => {
261323

262324
const portalRoot = getClosestProfileScrollBox(mode, event);
263325

264-
if (subscription && !subscription.canceledAt) {
326+
if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAt) {
265327
clerk.__internal_openPlanDetails({
266328
plan,
267329
subscriberType,
@@ -303,6 +365,8 @@ export const usePlansContext = () => {
303365
...ctx,
304366
componentName,
305367
activeOrUpcomingSubscription,
368+
activeAndUpcomingSubscriptions,
369+
activeOrUpcomingSubscriptionBasedOnPlanPeriod: activeOrUpcomingSubscriptionWithPlanPeriod,
306370
isDefaultPlanImplicitlyActiveOrUpcoming,
307371
handleSelectPlan,
308372
buttonPropsForPlan,

0 commit comments

Comments
 (0)