diff --git a/.changeset/curvy-pianos-wait.md b/.changeset/curvy-pianos-wait.md new file mode 100644 index 00000000000..1f56148b64a --- /dev/null +++ b/.changeset/curvy-pianos-wait.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +--- + +Make subscription actions more visible with inline buttons diff --git a/integration/tests/pricing-table.test.ts b/integration/tests/pricing-table.test.ts index 80560e487aa..bdc61418b6c 100644 --- a/integration/tests/pricing-table.test.ts +++ b/integration/tests/pricing-table.test.ts @@ -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( @@ -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 diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 4b3ceae4b8c..509aa6dfaba 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -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" }, diff --git a/packages/clerk-js/src/ui/components/SubscriptionDetails/__tests__/SubscriptionDetails.test.tsx b/packages/clerk-js/src/ui/components/SubscriptionDetails/__tests__/SubscriptionDetails.test.tsx index 2c646d0452c..2003a7cf50c 100644 --- a/packages/clerk-js/src/ui/components/SubscriptionDetails/__tests__/SubscriptionDetails.test.tsx +++ b/packages/clerk-js/src/ui/components/SubscriptionDetails/__tests__/SubscriptionDetails.test.tsx @@ -97,7 +97,7 @@ describe('SubscriptionDetails', () => { ], }); - const { getByRole, getByText, queryByText, getAllByText, userEvent } = render( + const { getByRole, getByText, queryByText, getAllByText } = render( {}} @@ -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(); @@ -204,7 +200,7 @@ describe('SubscriptionDetails', () => { ], }); - const { getByRole, getByText, queryByText, getAllByText, userEvent } = render( + const { getByRole, getByText, queryByText, getAllByText } = render( {}} @@ -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(); @@ -293,7 +285,7 @@ describe('SubscriptionDetails', () => { ], }); - const { getByRole, getByText, queryByText, queryByRole } = render( + const { getByRole, getByText, queryByText } = render( {}} @@ -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(); }); }); @@ -436,7 +430,7 @@ describe('SubscriptionDetails', () => { ], }); - const { getByRole, getByText, getAllByText, queryByText, getAllByRole, userEvent } = render( + const { getByRole, getByText, getAllByText, queryByText } = render( {}} @@ -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); }); }); @@ -694,7 +681,7 @@ describe('SubscriptionDetails', () => { ], }); - const { getByRole, getByText, userEvent } = render( + const { getByText, getAllByText, userEvent } = render( {}} @@ -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(); @@ -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(() => { @@ -815,7 +802,7 @@ describe('SubscriptionDetails', () => { subscriptionItems: [subscription], }); - const { getByRole, getByText, userEvent } = render( + const { getByText, userEvent } = render( {}} @@ -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 @@ -920,7 +902,7 @@ describe('SubscriptionDetails', () => { subscriptionItems: [subscription], }); - const { getByRole, getByText, userEvent } = render( + const { getByText, userEvent } = render( {}} @@ -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 @@ -1112,7 +1089,7 @@ describe('SubscriptionDetails', () => { ], }); - const { getByRole, getByText, getAllByText, queryByText, userEvent } = render( + const { getByRole, getByText, getAllByText, queryByText } = render( {}} @@ -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(); }); @@ -1228,7 +1201,7 @@ describe('SubscriptionDetails', () => { ], }); - const { getByRole, getByText, userEvent } = render( + const { getByText, getAllByText, userEvent } = render( {}} @@ -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 @@ -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(() => { diff --git a/packages/clerk-js/src/ui/components/SubscriptionDetails/index.tsx b/packages/clerk-js/src/ui/components/SubscriptionDetails/index.tsx index 730d50c4b49..b0b8a670e8a 100644 --- a/packages/clerk-js/src/ui/components/SubscriptionDetails/index.tsx +++ b/packages/clerk-js/src/ui/components/SubscriptionDetails/index.tsx @@ -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'; @@ -471,10 +470,34 @@ const SubscriptionCardActions = ({ subscription }: { subscription: BillingSubscr } return ( - + ({ + paddingInline: t.space.$3, + paddingBlock: t.space.$3, + borderBlockStartWidth: t.borderWidths.$normal, + borderBlockStartStyle: t.borderStyles.$solid, + borderBlockStartColor: t.colors.$borderAlpha100, + })} + > + {actions.map((action, index) => ( +