Skip to content

Commit a04513e

Browse files
committed
feat(clerk-js,shared): Make subscription actions more visible
This PR updates the styling of the action buttons related to Billing subscriptions. Previously, these actions were hidden behind a three-dots menu, which made them hard to find. Now, the buttons are visible outside of the menu, making them much easier to spot.
1 parent 5966383 commit a04513e

File tree

6 files changed

+82
-93
lines changed

6 files changed

+82
-93
lines changed

.changeset/curvy-pianos-wait.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/shared': patch
4+
---
5+
6+
Make subscription actions more visible with inline buttons

integration/tests/pricing-table.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -320,10 +320,11 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
320320

321321
await u.po.page.getByRole('button', { name: 'Manage' }).first().click();
322322
await u.po.subscriptionDetails.waitForMounted();
323-
await u.po.subscriptionDetails.root.locator('.cl-menuButtonEllipsisBordered').click();
324323
await u.po.subscriptionDetails.root.getByText('Cancel free trial').click();
325-
await u.po.subscriptionDetails.root.locator('.cl-drawerConfirmationRoot').waitFor({ state: 'visible' });
326-
await u.po.subscriptionDetails.root.getByRole('button', { name: 'Cancel free trial' }).click();
324+
const confirmationDialog = u.po.subscriptionDetails.root.locator('.cl-drawerConfirmationRoot');
325+
await confirmationDialog.waitFor({ state: 'visible' });
326+
// Click the Cancel free trial button within the confirmation dialog
327+
await confirmationDialog.getByRole('button', { name: 'Cancel free trial' }).click();
327328
await u.po.subscriptionDetails.waitForUnmounted();
328329

329330
await expect(
@@ -552,10 +553,11 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
552553
.getByRole('button', { name: 'Manage' })
553554
.click();
554555
await u.po.subscriptionDetails.waitForMounted();
555-
await u.po.subscriptionDetails.root.locator('.cl-menuButtonEllipsisBordered').click();
556-
await u.po.subscriptionDetails.root.getByText('Cancel subscription').click();
557-
await u.po.subscriptionDetails.root.locator('.cl-drawerConfirmationRoot').waitFor({ state: 'visible' });
558556
await u.po.subscriptionDetails.root.getByText('Cancel subscription').click();
557+
const confirmationDialog = u.po.subscriptionDetails.root.locator('.cl-drawerConfirmationRoot');
558+
await confirmationDialog.waitFor({ state: 'visible' });
559+
// Click the Cancel subscription button within the confirmation dialog
560+
await confirmationDialog.getByText('Cancel subscription').click();
559561
await u.po.subscriptionDetails.waitForUnmounted();
560562

561563
// Verify the Free plan with Upcoming status exists

packages/clerk-js/src/ui/components/SubscriptionDetails/__tests__/SubscriptionDetails.test.tsx

Lines changed: 31 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ describe('SubscriptionDetails', () => {
9797
],
9898
});
9999

100-
const { getByRole, getByText, queryByText, getAllByText, userEvent } = render(
100+
const { getByRole, getByText, queryByText, getAllByText } = render(
101101
<Drawer.Root
102102
open
103103
onOpenChange={() => {}}
@@ -130,12 +130,8 @@ describe('SubscriptionDetails', () => {
130130
expect(queryByText('Ends on')).toBeNull();
131131
});
132132

133-
const menuButton = getByRole('button', { name: /Open menu/i });
134-
expect(menuButton).toBeVisible();
135-
await userEvent.click(menuButton);
136-
137133
await waitFor(() => {
138-
expect(getByText('Switch to annual $100 / year')).toBeVisible();
134+
expect(getByText('Switch to annual')).toBeVisible();
139135
expect(getByText('Cancel subscription')).toBeVisible();
140136
});
141137
});
@@ -204,7 +200,7 @@ describe('SubscriptionDetails', () => {
204200
],
205201
});
206202

207-
const { getByRole, getByText, queryByText, getAllByText, userEvent } = render(
203+
const { getByRole, getByText, queryByText, getAllByText } = render(
208204
<Drawer.Root
209205
open
210206
onOpenChange={() => {}}
@@ -237,12 +233,8 @@ describe('SubscriptionDetails', () => {
237233
expect(queryByText('Ends on')).toBeNull();
238234
});
239235

240-
const menuButton = getByRole('button', { name: /Open menu/i });
241-
expect(menuButton).toBeVisible();
242-
await userEvent.click(menuButton);
243-
244236
await waitFor(() => {
245-
expect(getByText('Switch to monthly $10 / month')).toBeVisible();
237+
expect(getByText('Switch to monthly')).toBeVisible();
246238
expect(getByText('Cancel subscription')).toBeVisible();
247239
});
248240
});
@@ -293,7 +285,7 @@ describe('SubscriptionDetails', () => {
293285
],
294286
});
295287

296-
const { getByRole, getByText, queryByText, queryByRole } = render(
288+
const { getByRole, getByText, queryByText } = render(
297289
<Drawer.Root
298290
open
299291
onOpenChange={() => {}}
@@ -319,7 +311,9 @@ describe('SubscriptionDetails', () => {
319311
expect(queryByText('Monthly')).toBeNull();
320312
expect(queryByText('Next payment on')).toBeNull();
321313
expect(queryByText('Next payment amount')).toBeNull();
322-
expect(queryByRole('button', { name: /Open menu/i })).toBeNull();
314+
315+
expect(queryByText('Cancel subscription')).toBeNull();
316+
expect(queryByText(/Switch to/i)).toBeNull();
323317
});
324318
});
325319

@@ -436,7 +430,7 @@ describe('SubscriptionDetails', () => {
436430
],
437431
});
438432

439-
const { getByRole, getByText, getAllByText, queryByText, getAllByRole, userEvent } = render(
433+
const { getByRole, getByText, getAllByText, queryByText } = render(
440434
<Drawer.Root
441435
open
442436
onOpenChange={() => {}}
@@ -469,20 +463,10 @@ describe('SubscriptionDetails', () => {
469463
expect(getByText('Begins on')).toBeVisible();
470464
});
471465

472-
const [menuButton, upcomingMenuButton] = getAllByRole('button', { name: /Open menu/i });
473-
await userEvent.click(menuButton);
474-
475466
await waitFor(() => {
476-
expect(getByText('Switch to monthly $13 / month')).toBeVisible();
467+
expect(getByText('Switch to monthly')).toBeVisible();
477468
expect(getByText('Resubscribe')).toBeVisible();
478-
expect(queryByText('Cancel subscription')).toBeNull();
479-
});
480-
481-
await userEvent.click(upcomingMenuButton);
482-
483-
await waitFor(() => {
484-
expect(getByText('Switch to annual $90.99 / year')).toBeVisible();
485-
expect(getByText('Cancel subscription')).toBeVisible();
469+
expect(getAllByText('Cancel subscription').length).toBe(1);
486470
});
487471
});
488472

@@ -694,7 +678,7 @@ describe('SubscriptionDetails', () => {
694678
],
695679
});
696680

697-
const { getByRole, getByText, userEvent } = render(
681+
const { getByText, getAllByText, userEvent } = render(
698682
<Drawer.Root
699683
open
700684
onOpenChange={() => {}}
@@ -710,12 +694,9 @@ describe('SubscriptionDetails', () => {
710694
expect(getByText('Active')).toBeVisible();
711695
});
712696

713-
// Open the menu
714-
const menuButton = getByRole('button', { name: /Open menu/i });
715-
await userEvent.click(menuButton);
716-
717-
// Wait for the cancel option to appear and click it
718-
await userEvent.click(getByText('Cancel subscription'));
697+
// Get the inline Cancel subscription button (first one, before confirmation dialog opens)
698+
const cancelButtons = getAllByText('Cancel subscription');
699+
await userEvent.click(cancelButtons[0]);
719700

720701
await waitFor(() => {
721702
expect(getByText('Cancel Monthly Plan Subscription?')).toBeVisible();
@@ -727,7 +708,10 @@ describe('SubscriptionDetails', () => {
727708
expect(getByText('Keep subscription')).toBeVisible();
728709
});
729710

730-
await userEvent.click(getByText('Cancel subscription'));
711+
// Click the Cancel subscription button in the confirmation dialog
712+
// Use getAllByText and select the last one (confirmation dialog button)
713+
const allCancelButtons = getAllByText('Cancel subscription');
714+
await userEvent.click(allCancelButtons[allCancelButtons.length - 1]);
731715

732716
// Assert that the cancelSubscription method was called
733717
await waitFor(() => {
@@ -815,7 +799,7 @@ describe('SubscriptionDetails', () => {
815799
subscriptionItems: [subscription],
816800
});
817801

818-
const { getByRole, getByText, userEvent } = render(
802+
const { getByText, userEvent } = render(
819803
<Drawer.Root
820804
open
821805
onOpenChange={() => {}}
@@ -829,11 +813,6 @@ describe('SubscriptionDetails', () => {
829813
expect(getByText('Annual Plan')).toBeVisible();
830814
});
831815

832-
// Open the menu
833-
const menuButton = getByRole('button', { name: /Open menu/i });
834-
await userEvent.click(menuButton);
835-
836-
// Wait for the Resubscribe option and click it
837816
await userEvent.click(getByText('Resubscribe'));
838817

839818
// Assert resubscribe was called
@@ -920,7 +899,7 @@ describe('SubscriptionDetails', () => {
920899
subscriptionItems: [subscription],
921900
});
922901

923-
const { getByRole, getByText, userEvent } = render(
902+
const { getByText, userEvent } = render(
924903
<Drawer.Root
925904
open
926905
onOpenChange={() => {}}
@@ -934,11 +913,6 @@ describe('SubscriptionDetails', () => {
934913
expect(getByText('Annual Plan')).toBeVisible();
935914
});
936915

937-
// Open the menu
938-
const menuButton = getByRole('button', { name: /Open menu/i });
939-
await userEvent.click(menuButton);
940-
941-
// Wait for the Switch to monthly option and click it
942916
await userEvent.click(getByText(/Switch to monthly/i));
943917

944918
// Assert switchToMonthly was called
@@ -1112,7 +1086,7 @@ describe('SubscriptionDetails', () => {
11121086
],
11131087
});
11141088

1115-
const { getByRole, getByText, getAllByText, queryByText, userEvent } = render(
1089+
const { getByRole, getByText, getAllByText, queryByText } = render(
11161090
<Drawer.Root
11171091
open
11181092
onOpenChange={() => {}}
@@ -1149,11 +1123,7 @@ describe('SubscriptionDetails', () => {
11491123
expect(queryByText('Next payment amount')).toBeNull();
11501124
});
11511125

1152-
// Test the menu shows free trial specific options
1153-
const menuButton = getByRole('button', { name: /Open menu/i });
1154-
expect(menuButton).toBeVisible();
1155-
await userEvent.click(menuButton);
1156-
1126+
// Test the inline button shows free trial specific option
11571127
await waitFor(() => {
11581128
expect(getByText('Cancel free trial')).toBeVisible();
11591129
});
@@ -1228,7 +1198,7 @@ describe('SubscriptionDetails', () => {
12281198
],
12291199
});
12301200

1231-
const { getByRole, getByText, userEvent } = render(
1201+
const { getByText, getAllByText, userEvent } = render(
12321202
<Drawer.Root
12331203
open
12341204
onOpenChange={() => {}}
@@ -1244,12 +1214,9 @@ describe('SubscriptionDetails', () => {
12441214
expect(getByText('Free trial')).toBeVisible();
12451215
});
12461216

1247-
// Open the menu
1248-
const menuButton = getByRole('button', { name: /Open menu/i });
1249-
await userEvent.click(menuButton);
1250-
1251-
// Wait for the cancel option to appear and click it
1252-
await userEvent.click(getByText('Cancel free trial'));
1217+
// Get the inline Cancel free trial button (first one, before confirmation dialog opens)
1218+
const cancelTrialButtons = getAllByText('Cancel free trial');
1219+
await userEvent.click(cancelTrialButtons[0]);
12531220

12541221
await waitFor(() => {
12551222
// Should show free trial specific cancellation dialog
@@ -1262,8 +1229,10 @@ describe('SubscriptionDetails', () => {
12621229
expect(getByText('Keep free trial')).toBeVisible();
12631230
});
12641231

1265-
// Click the cancel button in the dialog
1266-
await userEvent.click(getByText('Cancel free trial'));
1232+
// Click the Cancel free trial button in the confirmation dialog
1233+
// Use getAllByText and select the last one (confirmation dialog button)
1234+
const allCancelTrialButtons = getAllByText('Cancel free trial');
1235+
await userEvent.click(allCancelTrialButtons[allCancelTrialButtons.length - 1]);
12671236

12681237
// Assert that the cancelSubscription method was called
12691238
await waitFor(() => {

packages/clerk-js/src/ui/components/SubscriptionDetails/index.tsx

Lines changed: 33 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,17 +18,10 @@ import { CardAlert } from '@/ui/elements/Card/CardAlert';
1818
import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
1919
import { Drawer, useDrawerContext } from '@/ui/elements/Drawer';
2020
import { LineItems } from '@/ui/elements/LineItems';
21-
import { ThreeDotsMenu } from '@/ui/elements/ThreeDotsMenu';
2221
import { handleError } from '@/ui/utils/errorHandler';
2322
import { formatDate } from '@/ui/utils/formatDate';
2423

25-
import {
26-
normalizeFormatted,
27-
SubscriberTypeContext,
28-
usePlansContext,
29-
useSubscriberTypeContext,
30-
useSubscription,
31-
} from '../../contexts';
24+
import { SubscriberTypeContext, usePlansContext, useSubscriberTypeContext, useSubscription } from '../../contexts';
3225
import type { LocalizationKey } from '../../customizables';
3326
import {
3427
Button,
@@ -408,16 +401,8 @@ const SubscriptionCardActions = ({ subscription }: { subscription: BillingSubscr
408401
? {
409402
label:
410403
subscription.planPeriod === 'month'
411-
? localizationKeys('billing.switchToAnnualWithAnnualPrice', {
412-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
413-
price: normalizeFormatted(subscription.plan.annualFee!.amountFormatted),
414-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
415-
currency: subscription.plan.annualFee!.currencySymbol,
416-
})
417-
: localizationKeys('billing.switchToMonthlyWithPrice', {
418-
price: normalizeFormatted(subscription.plan.fee.amountFormatted),
419-
currency: subscription.plan.fee.currencySymbol,
420-
}),
404+
? localizationKeys('billing.switchToAnnual')
405+
: localizationKeys('billing.switchToMonthly'),
421406
onClick: () => {
422407
openCheckout({
423408
planId: subscription.plan.id,
@@ -471,10 +456,34 @@ const SubscriptionCardActions = ({ subscription }: { subscription: BillingSubscr
471456
}
472457

473458
return (
474-
<ThreeDotsMenu
475-
variant='bordered'
476-
actions={actions}
477-
/>
459+
<Flex
460+
elementDescriptor={descriptors.subscriptionDetailsCardActions}
461+
gap={2}
462+
sx={t => ({
463+
paddingInline: t.space.$3,
464+
paddingBlock: t.space.$3,
465+
borderBlockStartWidth: t.borderWidths.$normal,
466+
borderBlockStartStyle: t.borderStyles.$solid,
467+
borderBlockStartColor: t.colors.$borderAlpha100,
468+
})}
469+
>
470+
{actions.map((action, index) => (
471+
<Button
472+
key={index}
473+
elementDescriptor={
474+
action.isDestructive
475+
? descriptors.subscriptionDetailsCancelButton
476+
: descriptors.subscriptionDetailsActionButton
477+
}
478+
variant={action.isDestructive ? 'ghost' : 'outline'}
479+
colorScheme={action.isDestructive ? 'danger' : undefined}
480+
size='xs'
481+
textVariant='buttonSmall'
482+
onClick={action.onClick}
483+
localizationKey={action.label}
484+
/>
485+
))}
486+
</Flex>
478487
);
479488
};
480489

@@ -539,7 +548,6 @@ const SubscriptionCard = ({ subscription }: { subscription: BillingSubscriptionI
539548

540549
{/* Pricing details */}
541550
<Flex
542-
elementDescriptor={descriptors.subscriptionDetailsCardActions}
543551
justify='between'
544552
align='center'
545553
>
@@ -555,8 +563,6 @@ const SubscriptionCard = ({ subscription }: { subscription: BillingSubscriptionI
555563
{fee.amountFormatted} /{' '}
556564
{t(localizationKeys(`billing.${subscription.planPeriod === 'month' ? 'month' : 'year'}`))}
557565
</Text>
558-
559-
<SubscriptionCardActions subscription={subscription} />
560566
</Flex>
561567
</Col>
562568

@@ -599,6 +605,8 @@ const SubscriptionCard = ({ subscription }: { subscription: BillingSubscriptionI
599605
value={formatDate(subscription.periodStart)}
600606
/>
601607
) : null}
608+
609+
<SubscriptionCardActions subscription={subscription} />
602610
</Col>
603611
);
604612
};

packages/clerk-js/src/ui/customizables/elementDescriptors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,8 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
506506
'subscriptionDetailsCardBody',
507507
'subscriptionDetailsCardFooter',
508508
'subscriptionDetailsCardActions',
509+
'subscriptionDetailsActionButton',
510+
'subscriptionDetailsCancelButton',
509511
'subscriptionDetailsDetailRow',
510512
'subscriptionDetailsDetailRowLabel',
511513
'subscriptionDetailsDetailRowValue',

packages/shared/src/types/appearance.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,8 @@ export type ElementsConfig = {
641641
subscriptionDetailsCardBody: WithOptions;
642642
subscriptionDetailsCardFooter: WithOptions;
643643
subscriptionDetailsCardActions: WithOptions;
644+
subscriptionDetailsActionButton: WithOptions;
645+
subscriptionDetailsCancelButton: WithOptions;
644646
subscriptionDetailsDetailRow: WithOptions;
645647
subscriptionDetailsDetailRowLabel: WithOptions;
646648
subscriptionDetailsDetailRowValue: WithOptions;

0 commit comments

Comments
 (0)