diff --git a/.changeset/legal-ants-flow.md b/.changeset/legal-ants-flow.md
new file mode 100644
index 00000000000..6e309217c8d
--- /dev/null
+++ b/.changeset/legal-ants-flow.md
@@ -0,0 +1,5 @@
+---
+'@clerk/clerk-js': patch
+---
+
+Add class `cl-planDetails-root` to the parent div element that containes the plan details drawer.
diff --git a/.changeset/little-seals-hang.md b/.changeset/little-seals-hang.md
new file mode 100644
index 00000000000..3f287927aee
--- /dev/null
+++ b/.changeset/little-seals-hang.md
@@ -0,0 +1,5 @@
+---
+'@clerk/testing': patch
+---
+
+Add `planDetails` to the page details object.
diff --git a/integration/templates/next-app-router/src/app/billing/checkout-btn/page.tsx b/integration/templates/next-app-router/src/app/billing/checkout-btn/page.tsx
new file mode 100644
index 00000000000..4904d056e95
--- /dev/null
+++ b/integration/templates/next-app-router/src/app/billing/checkout-btn/page.tsx
@@ -0,0 +1,17 @@
+import { SignedIn } from '@clerk/nextjs';
+import { CheckoutButton } from '@clerk/nextjs/experimental';
+
+export default function Home() {
+ return (
+
+
+
+ Checkout Now
+
+
+
+ );
+}
diff --git a/integration/templates/next-app-router/src/app/billing/plan-details-btn/page.tsx b/integration/templates/next-app-router/src/app/billing/plan-details-btn/page.tsx
new file mode 100644
index 00000000000..ef0009520b9
--- /dev/null
+++ b/integration/templates/next-app-router/src/app/billing/plan-details-btn/page.tsx
@@ -0,0 +1,9 @@
+import { PlanDetailsButton } from '@clerk/nextjs/experimental';
+
+export default function Home() {
+ return (
+
+ Plan details
+
+ );
+}
diff --git a/integration/templates/next-app-router/src/app/billing/subscription-details-btn/page.tsx b/integration/templates/next-app-router/src/app/billing/subscription-details-btn/page.tsx
new file mode 100644
index 00000000000..c6122bae841
--- /dev/null
+++ b/integration/templates/next-app-router/src/app/billing/subscription-details-btn/page.tsx
@@ -0,0 +1,9 @@
+import { SubscriptionDetailsButton } from '@clerk/nextjs/experimental';
+
+export default function Home() {
+ return (
+
+ Subscription details
+
+ );
+}
diff --git a/integration/templates/next-app-router/tsconfig.json b/integration/templates/next-app-router/tsconfig.json
index 0c7555fa765..eb0b41d94d5 100644
--- a/integration/templates/next-app-router/tsconfig.json
+++ b/integration/templates/next-app-router/tsconfig.json
@@ -9,7 +9,7 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
- "moduleResolution": "node",
+ "moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
diff --git a/integration/tests/pricing-table.test.ts b/integration/tests/pricing-table.test.ts
index 5538294589e..783ff3e7ada 100644
--- a/integration/tests/pricing-table.test.ts
+++ b/integration/tests/pricing-table.test.ts
@@ -28,6 +28,29 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
await expect(u.po.page.getByRole('heading', { name: 'Pro' })).toBeVisible();
});
+ test('renders pricing details of a specific plan', async ({ page, context }) => {
+ if (!app.name.includes('next')) {
+ return;
+ }
+
+ const u = createTestUtils({ app, page, context });
+ await u.po.page.goToRelative('/billing/plan-details-btn');
+
+ await u.po.page.getByRole('button', { name: 'Plan details' }).click();
+
+ await u.po.planDetails.waitForMounted();
+ const { root } = u.po.planDetails;
+ await expect(root.getByRole('heading', { name: 'Plus' })).toBeVisible();
+ await expect(root.getByText('$9.99')).toBeVisible();
+ await expect(root.getByText('This is the Plus plan!')).toBeVisible();
+ await expect(root.getByText('Feature One')).toBeVisible();
+ await expect(root.getByText('First feature')).toBeVisible();
+ await expect(root.getByText('Feature Two')).toBeVisible();
+ await expect(root.getByText('Second feature')).toBeVisible();
+ await expect(root.getByText('Feature Three')).toBeVisible();
+ await expect(root.getByText('Third feature')).toBeVisible();
+ });
+
test('when signed out, clicking subscribe button navigates to sign in page', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.page.goToRelative('/pricing-table');
@@ -39,6 +62,10 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
});
test('when signed in, clicking get started button opens checkout drawer', async ({ page, context }) => {
+ if (!app.name.includes('next')) {
+ return;
+ }
+
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
@@ -64,6 +91,21 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
await expect(u.po.page.getByText('Checkout')).toBeVisible();
});
+ test('when signed in, clicking checkout button open checkout drawer', async ({ page, context }) => {
+ if (!app.name.includes('next')) {
+ return;
+ }
+ const u = createTestUtils({ app, page, context });
+ await u.po.signIn.goTo();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.page.goToRelative('/billing/checkout-btn');
+
+ await u.po.page.getByRole('button', { name: 'Checkout Now' }).click();
+ await u.po.checkout.waitForMounted();
+ await u.po.page.getByText(/^Checkout$/).click();
+ await u.po.checkout.fillTestCard();
+ });
+
test('can subscribe to a plan', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
@@ -86,6 +128,25 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
}
});
+ test('Displays subscription details drawer', async ({ page, context }) => {
+ if (!app.name.includes('next')) {
+ return;
+ }
+ const u = createTestUtils({ app, page, context });
+ await u.po.signIn.goTo();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
+ await u.po.page.goToRelative('/billing/subscription-details-btn');
+
+ await u.po.page.getByRole('button', { name: 'Subscription details' }).click();
+
+ await u.po.subscriptionDetails.waitForMounted();
+ await expect(u.po.page.getByText('Plus')).toBeVisible();
+ await expect(u.po.page.getByText('Subscription details')).toBeVisible();
+ await expect(u.po.page.getByText('Current billing cycle')).toBeVisible();
+ await expect(u.po.page.getByText('Next payment on')).toBeVisible();
+ await expect(u.po.page.getByText('Next payment amount')).toBeVisible();
+ });
+
test('can upgrade to a new plan with saved card', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
diff --git a/packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx b/packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx
index 4a9ec723545..d32194fd744 100644
--- a/packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx
+++ b/packages/clerk-js/src/ui/components/Plans/PlanDetails.tsx
@@ -20,6 +20,7 @@ import {
Col,
descriptors,
Flex,
+ Flow,
Heading,
localizationKeys,
Span,
@@ -30,9 +31,13 @@ import {
export const PlanDetails = (props: __internal_PlanDetailsProps) => {
return (
-
-
-
+
+
+
+
+
+
+
);
};
diff --git a/packages/clerk-js/src/ui/elements/contexts/index.tsx b/packages/clerk-js/src/ui/elements/contexts/index.tsx
index 5d43d9f63ff..84aa3211f39 100644
--- a/packages/clerk-js/src/ui/elements/contexts/index.tsx
+++ b/packages/clerk-js/src/ui/elements/contexts/index.tsx
@@ -100,6 +100,7 @@ export type FlowMetadata = {
| 'apiKeys'
| 'oauthConsent'
| 'subscriptionDetails'
+ | 'subscriptionDetails'
| 'taskChooseOrganization';
part?:
| 'start'
diff --git a/packages/testing/src/playwright/unstable/page-objects/index.ts b/packages/testing/src/playwright/unstable/page-objects/index.ts
index 8b408d10548..698a91b5677 100644
--- a/packages/testing/src/playwright/unstable/page-objects/index.ts
+++ b/packages/testing/src/playwright/unstable/page-objects/index.ts
@@ -8,6 +8,7 @@ import { createExpectPageObject } from './expect';
import { createImpersonationPageObject } from './impersonation';
import { createKeylessPopoverPageObject } from './keylessPopover';
import { createOrganizationSwitcherComponentPageObject } from './organizationSwitcher';
+import { createPlanDetailsPageObject } from './planDetails';
import { createPricingTablePageObject } from './pricingTable';
import { createSessionTaskComponentPageObject } from './sessionTask';
import { createSignInComponentPageObject } from './signIn';
@@ -50,5 +51,6 @@ export const createPageObjects = ({
waitlist: createWaitlistComponentPageObject(testArgs),
apiKeys: createAPIKeysComponentPageObject(testArgs),
subscriptionDetails: createSubscriptionDetailsPageObject(testArgs),
+ planDetails: createPlanDetailsPageObject(testArgs),
};
};
diff --git a/packages/testing/src/playwright/unstable/page-objects/planDetails.ts b/packages/testing/src/playwright/unstable/page-objects/planDetails.ts
new file mode 100644
index 00000000000..29b8c6ca706
--- /dev/null
+++ b/packages/testing/src/playwright/unstable/page-objects/planDetails.ts
@@ -0,0 +1,14 @@
+import type { EnhancedPage } from './app';
+import { common } from './common';
+
+export const createPlanDetailsPageObject = (testArgs: { page: EnhancedPage }) => {
+ const { page } = testArgs;
+ const self = {
+ ...common(testArgs),
+ waitForMounted: (selector = '.cl-planDetails-root') => {
+ return page.waitForSelector(selector, { state: 'attached' });
+ },
+ root: page.locator('.cl-planDetails-root'),
+ };
+ return self;
+};