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; +};