Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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,12 +130,8 @@ 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('Switch to annual')).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,12 +233,8 @@ 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('Switch to monthly')).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,10 @@ describe('SubscriptionDetails', () => {
expect(getByText('Begins on')).toBeVisible();
});

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

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

await userEvent.click(upcomingMenuButton);

await waitFor(() => {
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 +678,7 @@ describe('SubscriptionDetails', () => {
],
});

const { getByRole, getByText, userEvent } = render(
const { getByText, getAllByText, userEvent } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand All @@ -710,12 +694,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 +708,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 +799,7 @@ describe('SubscriptionDetails', () => {
subscriptionItems: [subscription],
});

const { getByRole, getByText, userEvent } = render(
const { getByText, userEvent } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand All @@ -829,11 +813,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 +899,7 @@ describe('SubscriptionDetails', () => {
subscriptionItems: [subscription],
});

const { getByRole, getByText, userEvent } = render(
const { getByText, userEvent } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand All @@ -934,11 +913,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 +1086,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 +1123,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 +1198,7 @@ describe('SubscriptionDetails', () => {
],
});

const { getByRole, getByText, userEvent } = render(
const { getByText, getAllByText, userEvent } = render(
<Drawer.Root
open
onOpenChange={() => {}}
Expand All @@ -1244,12 +1214,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 +1229,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
58 changes: 33 additions & 25 deletions packages/clerk-js/src/ui/components/SubscriptionDetails/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,10 @@ 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';

import {
normalizeFormatted,
SubscriberTypeContext,
usePlansContext,
useSubscriberTypeContext,
useSubscription,
} from '../../contexts';
import { SubscriberTypeContext, usePlansContext, useSubscriberTypeContext, useSubscription } from '../../contexts';
import type { LocalizationKey } from '../../customizables';
import {
Button,
Expand Down Expand Up @@ -408,16 +401,8 @@ const SubscriptionCardActions = ({ subscription }: { subscription: BillingSubscr
? {
label:
subscription.planPeriod === 'month'
? localizationKeys('billing.switchToAnnualWithAnnualPrice', {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
price: normalizeFormatted(subscription.plan.annualFee!.amountFormatted),
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
currency: subscription.plan.annualFee!.currencySymbol,
})
: localizationKeys('billing.switchToMonthlyWithPrice', {
price: normalizeFormatted(subscription.plan.fee.amountFormatted),
currency: subscription.plan.fee.currencySymbol,
}),
? localizationKeys('billing.switchToAnnual')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we keep the labels that include the prices ? Originally we didn't had them, and the based of feedback we added them.

If we are deciding to remove them, we should mark those localization keys as deprecated and remove them in the next major, since they are not longer used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Restored prices in switch plans buttons via b507f9a

: localizationKeys('billing.switchToMonthly'),
onClick: () => {
openCheckout({
planId: subscription.plan.id,
Expand Down Expand Up @@ -471,10 +456,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 +548,6 @@ const SubscriptionCard = ({ subscription }: { subscription: BillingSubscriptionI

{/* Pricing details */}
<Flex
elementDescriptor={descriptors.subscriptionDetailsCardActions}
justify='between'
align='center'
>
Expand All @@ -555,8 +563,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 +605,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