Skip to content

refactor(clerk-js): Improve switching between plans flows #5883

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

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
5 changes: 5 additions & 0 deletions .changeset/little-cases-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Improvements of flows for switching between plans
2 changes: 1 addition & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{ "path": "./dist/clerk.browser.js", "maxSize": "68.3KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "110KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "52KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "104.1KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "104.20KB" },
{ "path": "./dist/vendors*.js", "maxSize": "39.5KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "38KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,7 @@ function Card(props: CardProps) {
const canManageBilling = useProtect(
has => has({ permission: 'org:sys_billing:manage' }) || subscriberType === 'user',
);

const { buttonPropsForPlan, upcomingSubscriptionsExist, activeOrUpcomingSubscription } = usePlansContext();
const { buttonPropsForPlan, activeOrUpcomingSubscriptionBasedOnPlanPeriod } = usePlansContext();

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

const subscription = activeOrUpcomingSubscription(plan);
const hasFeatures = plan.features.length > 0;
const subscription = React.useMemo(
() => activeOrUpcomingSubscriptionBasedOnPlanPeriod(plan, planPeriod),
[plan, planPeriod, activeOrUpcomingSubscriptionBasedOnPlanPeriod],
);
const isPlanActive = subscription?.status === 'active';
const hasFeatures = plan.features.length > 0;
const showStatusRow = !!subscription;
const isEligibleForSwitchToAnnual = plan.annualMonthlyAmount > 0 && planPeriod === 'annual';

const shouldShowFooter =
!subscription ||
subscription?.status === 'upcoming' ||
subscription?.canceledAt ||
(planPeriod !== subscription?.planPeriod && !plan.isDefault && isEligibleForSwitchToAnnual);
const shouldShowFooterNotice =
subscription?.status === 'upcoming' && (planPeriod === subscription.planPeriod || plan.isDefault);
let shouldShowFooter = false;
let shouldShowFooterNotice = false;

const planPeriodSameAsSelectedPlanPeriod = !upcomingSubscriptionsExist && subscription?.planPeriod === planPeriod;
if (!subscription) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else if (subscription.status === 'upcoming') {
shouldShowFooter = true;
shouldShowFooterNotice = true;
} else if (subscription.status === 'active') {
if (subscription.canceledAt) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else if (planPeriod !== subscription.planPeriod && plan.annualMonthlyAmount > 0) {
shouldShowFooter = true;
shouldShowFooterNotice = false;
} else {
shouldShowFooter = false;
shouldShowFooterNotice = false;
}
} else {
shouldShowFooter = false;
shouldShowFooterNotice = false;
}

return (
<Box
Expand Down Expand Up @@ -226,7 +242,6 @@ function Card(props: CardProps) {
borderTopWidth: t.borderWidths.$normal,
borderTopStyle: t.borderStyles.$solid,
borderTopColor: t.colors.$neutralAlpha100,
background: planPeriodSameAsSelectedPlanPeriod && hasFeatures ? t.colors.$colorBackground : undefined,
order: ctaPosition === 'top' ? -1 : undefined,
})}
>
Expand Down
98 changes: 81 additions & 17 deletions packages/clerk-js/src/ui/contexts/components/Plans.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,40 @@ export const usePlansContext = () => {
[ctx.subscriptions],
);

// returns all subscriptions for a plan that are active or upcoming
const activeAndUpcomingSubscriptions = useCallback(
(plan: CommercePlanResource) => {
return ctx.subscriptions.filter(subscription => subscription.plan.id === plan.id);
},
[ctx.subscriptions],
);

// 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
const activeOrUpcomingSubscriptionWithPlanPeriod = useCallback(
(plan: CommercePlanResource, planPeriod: CommerceSubscriptionPlanPeriod = 'month') => {
const plansSubscriptions = activeAndUpcomingSubscriptions(plan);
// Handle multiple subscriptions for the same plan
if (plansSubscriptions.length > 1) {
const subscriptionBaseOnPanPeriod = plansSubscriptions.find(subscription => {
return subscription.planPeriod === planPeriod;
});

if (subscriptionBaseOnPanPeriod) {
return subscriptionBaseOnPanPeriod;
}

return plansSubscriptions[0];
}

if (plansSubscriptions.length === 1) {
return plansSubscriptions[0];
}

return undefined;
},
[activeAndUpcomingSubscriptions],
);

const canManageSubscription = useCallback(
({ plan, subscription: sub }: { plan?: CommercePlanResource; subscription?: CommerceSubscriptionResource }) => {
const subscription = sub ?? (plan ? activeOrUpcomingSubscription(plan) : undefined);
Expand Down Expand Up @@ -212,36 +246,64 @@ export const usePlansContext = () => {
isDisabled: boolean;
disabled: boolean;
} => {
const subscription = sub ?? (plan ? activeOrUpcomingSubscription(plan) : undefined);
const subscription =
sub ?? (plan ? activeOrUpcomingSubscriptionWithPlanPeriod(plan, selectedPlanPeriod) : undefined);
let _selectedPlanPeriod = selectedPlanPeriod;
if (_selectedPlanPeriod === 'annual' && sub?.plan.annualMonthlyAmount === 0) {
_selectedPlanPeriod = 'month';
}

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

const getLocalizationKey = () => {
// Handle subscription cases
if (subscription) {
if (_selectedPlanPeriod !== subscription.planPeriod && subscription.canceledAt) {
if (_selectedPlanPeriod === 'month') {
return localizationKeys('commerce.switchToMonthly');
}

if (isEligibleForSwitchToAnnual) {
return localizationKeys('commerce.switchToAnnual');
}
}

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

if (_selectedPlanPeriod !== subscription.planPeriod) {
if (_selectedPlanPeriod === 'month') {
return localizationKeys('commerce.switchToMonthly');
}

if (isEligibleForSwitchToAnnual) {
return localizationKeys('commerce.switchToAnnual');
}

return localizationKeys('commerce.manageSubscription');
}

return localizationKeys('commerce.manageSubscription');
}

// Handle non-subscription cases
const hasNonDefaultSubscriptions =
ctx.subscriptions.filter(subscription => !subscription.plan.isDefault).length > 0;
return hasNonDefaultSubscriptions
? localizationKeys('commerce.switchPlan')
: localizationKeys('commerce.subscribe');
};

return {
localizationKey: subscription
? subscription.canceledAt
? localizationKeys('commerce.reSubscribe')
: selectedPlanPeriod !== subscription.planPeriod
? selectedPlanPeriod === 'month'
? localizationKeys('commerce.switchToMonthly')
: isEligibleForSwitchToAnnual
? localizationKeys('commerce.switchToAnnual')
: localizationKeys('commerce.manageSubscription')
: localizationKeys('commerce.manageSubscription')
: // If there are no active or grace period subscriptions, show the get started button
ctx.subscriptions.filter(subscription => !subscription.plan.isDefault).length > 0
? localizationKeys('commerce.switchPlan')
: localizationKeys('commerce.subscribe'),
localizationKey: getLocalizationKey(),
variant: isCompact ? 'bordered' : 'solid',
colorScheme: isCompact ? 'secondary' : 'primary',
isDisabled: !canManageBilling,
disabled: !canManageBilling,
};
},
[activeOrUpcomingSubscription, canManageBilling, ctx.subscriptions],
[activeOrUpcomingSubscriptionWithPlanPeriod, canManageBilling, ctx.subscriptions],
);

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

const portalRoot = getClosestProfileScrollBox(mode, event);

if (subscription && !subscription.canceledAt) {
if (subscription && subscription.planPeriod === planPeriod && !subscription.canceledAt) {
clerk.__internal_openPlanDetails({
plan,
subscriberType,
Expand Down Expand Up @@ -303,6 +365,8 @@ export const usePlansContext = () => {
...ctx,
componentName,
activeOrUpcomingSubscription,
activeAndUpcomingSubscriptions,
activeOrUpcomingSubscriptionBasedOnPlanPeriod: activeOrUpcomingSubscriptionWithPlanPeriod,
isDefaultPlanImplicitlyActiveOrUpcoming,
handleSelectPlan,
buttonPropsForPlan,
Expand Down