Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
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