Skip to content
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
6 changes: 6 additions & 0 deletions .changeset/curvy-pianos-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
---

Make subscription actions more visible with inline buttons
14 changes: 8 additions & 6 deletions integration/tests/pricing-table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,11 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl

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

await expect(
Expand Down Expand Up @@ -552,10 +553,11 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
.getByRole('button', { name: 'Manage' })
.click();
await u.po.subscriptionDetails.waitForMounted();
await u.po.subscriptionDetails.root.locator('.cl-menuButtonEllipsisBordered').click();
await u.po.subscriptionDetails.root.getByText('Cancel subscription').click();
await u.po.subscriptionDetails.root.locator('.cl-drawerConfirmationRoot').waitFor({ state: 'visible' });
await u.po.subscriptionDetails.root.getByText('Cancel subscription').click();
const confirmationDialog = u.po.subscriptionDetails.root.locator('.cl-drawerConfirmationRoot');
await confirmationDialog.waitFor({ state: 'visible' });
// Click the Cancel subscription button within the confirmation dialog
await confirmationDialog.getByText('Cancel subscription').click();
await u.po.subscriptionDetails.waitForUnmounted();

// Verify the Free plan with Upcoming status exists
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "123KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "65KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "117.1KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "120.1KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "122KB" },
{ "path": "./dist/vendors*.js", "maxSize": "47KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "38KB" },
{ "path": "./dist/stripe-vendors*.js", "maxSize": "1KB" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ describe('SubscriptionDetails', () => {
],
});

const { getByRole, getByText, queryByText, getAllByText, userEvent } = render(
const { getByRole, getByText, queryByText, getAllByText } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand Down Expand Up @@ -130,10 +130,6 @@ describe('SubscriptionDetails', () => {
expect(queryByText('Ends on')).toBeNull();
});

const menuButton = getByRole('button', { name: /Open menu/i });
expect(menuButton).toBeVisible();
await userEvent.click(menuButton);

await waitFor(() => {
expect(getByText('Switch to annual $100 / year')).toBeVisible();
expect(getByText('Cancel subscription')).toBeVisible();
Expand Down Expand Up @@ -204,7 +200,7 @@ describe('SubscriptionDetails', () => {
],
});

const { getByRole, getByText, queryByText, getAllByText, userEvent } = render(
const { getByRole, getByText, queryByText, getAllByText } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand Down Expand Up @@ -237,10 +233,6 @@ describe('SubscriptionDetails', () => {
expect(queryByText('Ends on')).toBeNull();
});

const menuButton = getByRole('button', { name: /Open menu/i });
expect(menuButton).toBeVisible();
await userEvent.click(menuButton);

await waitFor(() => {
expect(getByText('Switch to monthly $10 / month')).toBeVisible();
expect(getByText('Cancel subscription')).toBeVisible();
Expand Down Expand Up @@ -293,7 +285,7 @@ describe('SubscriptionDetails', () => {
],
});

const { getByRole, getByText, queryByText, queryByRole } = render(
const { getByRole, getByText, queryByText } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand All @@ -319,7 +311,9 @@ describe('SubscriptionDetails', () => {
expect(queryByText('Monthly')).toBeNull();
expect(queryByText('Next payment on')).toBeNull();
expect(queryByText('Next payment amount')).toBeNull();
expect(queryByRole('button', { name: /Open menu/i })).toBeNull();

expect(queryByText('Cancel subscription')).toBeNull();
expect(queryByText(/Switch to/i)).toBeNull();
});
});

Expand Down Expand Up @@ -436,7 +430,7 @@ describe('SubscriptionDetails', () => {
],
});

const { getByRole, getByText, getAllByText, queryByText, getAllByRole, userEvent } = render(
const { getByRole, getByText, getAllByText, queryByText } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand Down Expand Up @@ -469,20 +463,13 @@ describe('SubscriptionDetails', () => {
expect(getByText('Begins on')).toBeVisible();
});

const [menuButton, upcomingMenuButton] = getAllByRole('button', { name: /Open menu/i });
await userEvent.click(menuButton);

await waitFor(() => {
// Active (canceled) annual subscription buttons
expect(getByText('Switch to monthly $13 / month')).toBeVisible();
expect(getByText('Resubscribe')).toBeVisible();
expect(queryByText('Cancel subscription')).toBeNull();
});

await userEvent.click(upcomingMenuButton);

await waitFor(() => {
// Upcoming monthly subscription buttons
expect(getByText('Switch to annual $90.99 / year')).toBeVisible();
expect(getByText('Cancel subscription')).toBeVisible();
expect(getAllByText('Cancel subscription').length).toBe(1);
});
});

Expand Down Expand Up @@ -694,7 +681,7 @@ describe('SubscriptionDetails', () => {
],
});

const { getByRole, getByText, userEvent } = render(
const { getByText, getAllByText, userEvent } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand All @@ -710,12 +697,9 @@ describe('SubscriptionDetails', () => {
expect(getByText('Active')).toBeVisible();
});

// Open the menu
const menuButton = getByRole('button', { name: /Open menu/i });
await userEvent.click(menuButton);

// Wait for the cancel option to appear and click it
await userEvent.click(getByText('Cancel subscription'));
// Get the inline Cancel subscription button (first one, before confirmation dialog opens)
const cancelButtons = getAllByText('Cancel subscription');
await userEvent.click(cancelButtons[0]);

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

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

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

const { getByRole, getByText, userEvent } = render(
const { getByText, userEvent } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand All @@ -829,11 +816,6 @@ describe('SubscriptionDetails', () => {
expect(getByText('Annual Plan')).toBeVisible();
});

// Open the menu
const menuButton = getByRole('button', { name: /Open menu/i });
await userEvent.click(menuButton);

// Wait for the Resubscribe option and click it
await userEvent.click(getByText('Resubscribe'));

// Assert resubscribe was called
Expand Down Expand Up @@ -920,7 +902,7 @@ describe('SubscriptionDetails', () => {
subscriptionItems: [subscription],
});

const { getByRole, getByText, userEvent } = render(
const { getByText, userEvent } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand All @@ -934,11 +916,6 @@ describe('SubscriptionDetails', () => {
expect(getByText('Annual Plan')).toBeVisible();
});

// Open the menu
const menuButton = getByRole('button', { name: /Open menu/i });
await userEvent.click(menuButton);

// Wait for the Switch to monthly option and click it
await userEvent.click(getByText(/Switch to monthly/i));

// Assert switchToMonthly was called
Expand Down Expand Up @@ -1112,7 +1089,7 @@ describe('SubscriptionDetails', () => {
],
});

const { getByRole, getByText, getAllByText, queryByText, userEvent } = render(
const { getByRole, getByText, getAllByText, queryByText } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand Down Expand Up @@ -1149,11 +1126,7 @@ describe('SubscriptionDetails', () => {
expect(queryByText('Next payment amount')).toBeNull();
});

// Test the menu shows free trial specific options
const menuButton = getByRole('button', { name: /Open menu/i });
expect(menuButton).toBeVisible();
await userEvent.click(menuButton);

// Test the inline button shows free trial specific option
await waitFor(() => {
expect(getByText('Cancel free trial')).toBeVisible();
});
Expand Down Expand Up @@ -1228,7 +1201,7 @@ describe('SubscriptionDetails', () => {
],
});

const { getByRole, getByText, userEvent } = render(
const { getByText, getAllByText, userEvent } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand All @@ -1244,12 +1217,9 @@ describe('SubscriptionDetails', () => {
expect(getByText('Free trial')).toBeVisible();
});

// Open the menu
const menuButton = getByRole('button', { name: /Open menu/i });
await userEvent.click(menuButton);

// Wait for the cancel option to appear and click it
await userEvent.click(getByText('Cancel free trial'));
// Get the inline Cancel free trial button (first one, before confirmation dialog opens)
const cancelTrialButtons = getAllByText('Cancel free trial');
await userEvent.click(cancelTrialButtons[0]);

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

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

// Assert that the cancelSubscription method was called
await waitFor(() => {
Expand Down
38 changes: 30 additions & 8 deletions packages/clerk-js/src/ui/components/SubscriptionDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { CardAlert } from '@/ui/elements/Card/CardAlert';
import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
import { Drawer, useDrawerContext } from '@/ui/elements/Drawer';
import { LineItems } from '@/ui/elements/LineItems';
import { ThreeDotsMenu } from '@/ui/elements/ThreeDotsMenu';
import { handleError } from '@/ui/utils/errorHandler';
import { formatDate } from '@/ui/utils/formatDate';

Expand Down Expand Up @@ -471,10 +470,34 @@ const SubscriptionCardActions = ({ subscription }: { subscription: BillingSubscr
}

return (
<ThreeDotsMenu
variant='bordered'
actions={actions}
/>
<Flex
elementDescriptor={descriptors.subscriptionDetailsCardActions}
gap={2}
sx={t => ({
paddingInline: t.space.$3,
paddingBlock: t.space.$3,
borderBlockStartWidth: t.borderWidths.$normal,
borderBlockStartStyle: t.borderStyles.$solid,
borderBlockStartColor: t.colors.$borderAlpha100,
})}
>
{actions.map((action, index) => (
<Button
key={index}
elementDescriptor={
action.isDestructive
? descriptors.subscriptionDetailsCancelButton
: descriptors.subscriptionDetailsActionButton
}
variant={action.isDestructive ? 'ghost' : 'outline'}
colorScheme={action.isDestructive ? 'danger' : undefined}
size='xs'
textVariant='buttonSmall'
onClick={action.onClick}
localizationKey={action.label}
/>
))}
</Flex>
);
};

Expand Down Expand Up @@ -539,7 +562,6 @@ const SubscriptionCard = ({ subscription }: { subscription: BillingSubscriptionI

{/* Pricing details */}
<Flex
elementDescriptor={descriptors.subscriptionDetailsCardActions}
justify='between'
align='center'
>
Expand All @@ -555,8 +577,6 @@ const SubscriptionCard = ({ subscription }: { subscription: BillingSubscriptionI
{fee.amountFormatted} /{' '}
{t(localizationKeys(`billing.${subscription.planPeriod === 'month' ? 'month' : 'year'}`))}
</Text>

<SubscriptionCardActions subscription={subscription} />
</Flex>
</Col>

Expand Down Expand Up @@ -599,6 +619,8 @@ const SubscriptionCard = ({ subscription }: { subscription: BillingSubscriptionI
value={formatDate(subscription.periodStart)}
/>
) : null}

<SubscriptionCardActions subscription={subscription} />
</Col>
);
};
Expand Down
2 changes: 2 additions & 0 deletions packages/clerk-js/src/ui/customizables/elementDescriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,8 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
'subscriptionDetailsCardBody',
'subscriptionDetailsCardFooter',
'subscriptionDetailsCardActions',
'subscriptionDetailsActionButton',
'subscriptionDetailsCancelButton',
'subscriptionDetailsDetailRow',
'subscriptionDetailsDetailRowLabel',
'subscriptionDetailsDetailRowValue',
Expand Down
2 changes: 2 additions & 0 deletions packages/shared/src/types/appearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,8 @@ export type ElementsConfig = {
subscriptionDetailsCardBody: WithOptions;
subscriptionDetailsCardFooter: WithOptions;
subscriptionDetailsCardActions: WithOptions;
subscriptionDetailsActionButton: WithOptions;
subscriptionDetailsCancelButton: WithOptions;
subscriptionDetailsDetailRow: WithOptions;
subscriptionDetailsDetailRowLabel: WithOptions;
subscriptionDetailsDetailRowValue: WithOptions;
Expand Down
Loading