Skip to content

Commit d8f59a6

Browse files
feat(clerk-js,shared): Make subscription actions more visible with inline buttons (#7255)
1 parent 38015fd commit d8f59a6

File tree

7 files changed

+79
-73
lines changed

7 files changed

+79
-73
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/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "123KB" },
77
{ "path": "./dist/clerk.headless*.js", "maxSize": "65KB" },
88
{ "path": "./dist/ui-common*.js", "maxSize": "117.1KB" },
9-
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "120.1KB" },
9+
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "122KB" },
1010
{ "path": "./dist/vendors*.js", "maxSize": "47KB" },
1111
{ "path": "./dist/coinbase*.js", "maxSize": "38KB" },
1212
{ "path": "./dist/stripe-vendors*.js", "maxSize": "1KB" },

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

Lines changed: 30 additions & 58 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,10 +130,6 @@ 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(() => {
138134
expect(getByText('Switch to annual $100 / year')).toBeVisible();
139135
expect(getByText('Cancel subscription')).toBeVisible();
@@ -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,10 +233,6 @@ 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(() => {
245237
expect(getByText('Switch to monthly $10 / month')).toBeVisible();
246238
expect(getByText('Cancel subscription')).toBeVisible();
@@ -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,13 @@ 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(() => {
467+
// Active (canceled) annual subscription buttons
476468
expect(getByText('Switch to monthly $13 / month')).toBeVisible();
477469
expect(getByText('Resubscribe')).toBeVisible();
478-
expect(queryByText('Cancel subscription')).toBeNull();
479-
});
480-
481-
await userEvent.click(upcomingMenuButton);
482-
483-
await waitFor(() => {
470+
// Upcoming monthly subscription buttons
484471
expect(getByText('Switch to annual $90.99 / year')).toBeVisible();
485-
expect(getByText('Cancel subscription')).toBeVisible();
472+
expect(getAllByText('Cancel subscription').length).toBe(1);
486473
});
487474
});
488475

@@ -694,7 +681,7 @@ describe('SubscriptionDetails', () => {
694681
],
695682
});
696683

697-
const { getByRole, getByText, userEvent } = render(
684+
const { getByText, getAllByText, userEvent } = render(
698685
<Drawer.Root
699686
open
700687
onOpenChange={() => {}}
@@ -710,12 +697,9 @@ describe('SubscriptionDetails', () => {
710697
expect(getByText('Active')).toBeVisible();
711698
});
712699

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'));
700+
// Get the inline Cancel subscription button (first one, before confirmation dialog opens)
701+
const cancelButtons = getAllByText('Cancel subscription');
702+
await userEvent.click(cancelButtons[0]);
719703

720704
await waitFor(() => {
721705
expect(getByText('Cancel Monthly Plan Subscription?')).toBeVisible();
@@ -727,7 +711,10 @@ describe('SubscriptionDetails', () => {
727711
expect(getByText('Keep subscription')).toBeVisible();
728712
});
729713

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

732719
// Assert that the cancelSubscription method was called
733720
await waitFor(() => {
@@ -815,7 +802,7 @@ describe('SubscriptionDetails', () => {
815802
subscriptionItems: [subscription],
816803
});
817804

818-
const { getByRole, getByText, userEvent } = render(
805+
const { getByText, userEvent } = render(
819806
<Drawer.Root
820807
open
821808
onOpenChange={() => {}}
@@ -829,11 +816,6 @@ describe('SubscriptionDetails', () => {
829816
expect(getByText('Annual Plan')).toBeVisible();
830817
});
831818

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
837819
await userEvent.click(getByText('Resubscribe'));
838820

839821
// Assert resubscribe was called
@@ -920,7 +902,7 @@ describe('SubscriptionDetails', () => {
920902
subscriptionItems: [subscription],
921903
});
922904

923-
const { getByRole, getByText, userEvent } = render(
905+
const { getByText, userEvent } = render(
924906
<Drawer.Root
925907
open
926908
onOpenChange={() => {}}
@@ -934,11 +916,6 @@ describe('SubscriptionDetails', () => {
934916
expect(getByText('Annual Plan')).toBeVisible();
935917
});
936918

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
942919
await userEvent.click(getByText(/Switch to monthly/i));
943920

944921
// Assert switchToMonthly was called
@@ -1112,7 +1089,7 @@ describe('SubscriptionDetails', () => {
11121089
],
11131090
});
11141091

1115-
const { getByRole, getByText, getAllByText, queryByText, userEvent } = render(
1092+
const { getByRole, getByText, getAllByText, queryByText } = render(
11161093
<Drawer.Root
11171094
open
11181095
onOpenChange={() => {}}
@@ -1149,11 +1126,7 @@ describe('SubscriptionDetails', () => {
11491126
expect(queryByText('Next payment amount')).toBeNull();
11501127
});
11511128

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-
1129+
// Test the inline button shows free trial specific option
11571130
await waitFor(() => {
11581131
expect(getByText('Cancel free trial')).toBeVisible();
11591132
});
@@ -1228,7 +1201,7 @@ describe('SubscriptionDetails', () => {
12281201
],
12291202
});
12301203

1231-
const { getByRole, getByText, userEvent } = render(
1204+
const { getByText, getAllByText, userEvent } = render(
12321205
<Drawer.Root
12331206
open
12341207
onOpenChange={() => {}}
@@ -1244,12 +1217,9 @@ describe('SubscriptionDetails', () => {
12441217
expect(getByText('Free trial')).toBeVisible();
12451218
});
12461219

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'));
1220+
// Get the inline Cancel free trial button (first one, before confirmation dialog opens)
1221+
const cancelTrialButtons = getAllByText('Cancel free trial');
1222+
await userEvent.click(cancelTrialButtons[0]);
12531223

12541224
await waitFor(() => {
12551225
// Should show free trial specific cancellation dialog
@@ -1262,8 +1232,10 @@ describe('SubscriptionDetails', () => {
12621232
expect(getByText('Keep free trial')).toBeVisible();
12631233
});
12641234

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

12681240
// Assert that the cancelSubscription method was called
12691241
await waitFor(() => {

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

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ 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

@@ -472,10 +471,34 @@ const SubscriptionCardActions = ({ subscription }: { subscription: BillingSubscr
472471
}
473472

474473
return (
475-
<ThreeDotsMenu
476-
variant='bordered'
477-
actions={actions}
478-
/>
474+
<Flex
475+
elementDescriptor={descriptors.subscriptionDetailsCardActions}
476+
gap={2}
477+
sx={t => ({
478+
paddingInline: t.space.$3,
479+
paddingBlock: t.space.$3,
480+
borderBlockStartWidth: t.borderWidths.$normal,
481+
borderBlockStartStyle: t.borderStyles.$solid,
482+
borderBlockStartColor: t.colors.$borderAlpha100,
483+
})}
484+
>
485+
{actions.map((action, index) => (
486+
<Button
487+
key={index}
488+
elementDescriptor={
489+
action.isDestructive
490+
? descriptors.subscriptionDetailsCancelButton
491+
: descriptors.subscriptionDetailsActionButton
492+
}
493+
variant={action.isDestructive ? 'ghost' : 'outline'}
494+
colorScheme={action.isDestructive ? 'danger' : undefined}
495+
size='xs'
496+
textVariant='buttonSmall'
497+
onClick={action.onClick}
498+
localizationKey={action.label}
499+
/>
500+
))}
501+
</Flex>
479502
);
480503
};
481504

@@ -540,7 +563,6 @@ const SubscriptionCard = ({ subscription }: { subscription: BillingSubscriptionI
540563

541564
{/* Pricing details */}
542565
<Flex
543-
elementDescriptor={descriptors.subscriptionDetailsCardActions}
544566
justify='between'
545567
align='center'
546568
>
@@ -556,8 +578,6 @@ const SubscriptionCard = ({ subscription }: { subscription: BillingSubscriptionI
556578
{fee.amountFormatted} /{' '}
557579
{t(localizationKeys(`billing.${subscription.planPeriod === 'month' ? 'month' : 'year'}`))}
558580
</Text>
559-
560-
<SubscriptionCardActions subscription={subscription} />
561581
</Flex>
562582
</Col>
563583

@@ -600,6 +620,8 @@ const SubscriptionCard = ({ subscription }: { subscription: BillingSubscriptionI
600620
value={formatDate(subscription.periodStart)}
601621
/>
602622
) : null}
623+
624+
<SubscriptionCardActions subscription={subscription} />
603625
</Col>
604626
);
605627
};

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)