diff --git a/.changeset/twelve-ideas-exist.md b/.changeset/twelve-ideas-exist.md new file mode 100644 index 00000000000..e62484fd21a --- /dev/null +++ b/.changeset/twelve-ideas-exist.md @@ -0,0 +1,6 @@ +--- +'@clerk/nuxt': minor +'@clerk/vue': minor +--- + +Introducing `` diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index f9e863ca42d..8b48e960265 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -42,8 +42,12 @@ export const createLongRunningApps = () => { config: next.appRouter, env: envs.withSessionTasks, }, - { id: 'next.appRouter.withBillingStaging', config: next.appRouter, env: envs.withBillingStaging }, - { id: 'next.appRouter.withBilling', config: next.appRouter, env: envs.withBilling }, + { id: 'withBillingStaging.next.appRouter', config: next.appRouter, env: envs.withBillingStaging }, + { id: 'withBilling.next.appRouter', config: next.appRouter, env: envs.withBilling }, + { id: 'withBillingStaging.vue.vite', config: vue.vite, env: envs.withBillingStaging }, + { id: 'withBilling.vue.vite', config: vue.vite, env: envs.withBilling }, + // { id: 'withBillingStaging.nuxt.node', config: nuxt.node, env: envs.withBillingStaging }, + // { id: 'withBilling.nuxt.node', config: nuxt.node, env: envs.withBilling }, { id: 'next.appRouter.withLegalConsent', config: next.appRouter, diff --git a/integration/templates/nuxt-node/pages/pricing-table.vue b/integration/templates/nuxt-node/pages/pricing-table.vue new file mode 100644 index 00000000000..e119727a0bd --- /dev/null +++ b/integration/templates/nuxt-node/pages/pricing-table.vue @@ -0,0 +1,3 @@ + diff --git a/integration/templates/vue-vite/src/router.ts b/integration/templates/vue-vite/src/router.ts index db90e9a319a..c598b2b0bff 100644 --- a/integration/templates/vue-vite/src/router.ts +++ b/integration/templates/vue-vite/src/router.ts @@ -36,6 +36,17 @@ const routes = [ path: '/custom-pages/organization-profile', component: () => import('./views/custom-pages/OrganizationProfile.vue'), }, + { + name: 'PricingTable', + path: '/pricing-table', + component: () => import('./views/PricingTable.vue'), + }, + // This was added for billing tests + { + name: 'User', + path: '/user', + component: () => import('./views/Profile.vue'), + }, ]; const router = createRouter({ diff --git a/integration/templates/vue-vite/src/views/PricingTable.vue b/integration/templates/vue-vite/src/views/PricingTable.vue new file mode 100644 index 00000000000..780e4f421d6 --- /dev/null +++ b/integration/templates/vue-vite/src/views/PricingTable.vue @@ -0,0 +1,7 @@ + + + diff --git a/integration/testUtils/index.ts b/integration/testUtils/index.ts index a02fec69200..09dde2d7660 100644 --- a/integration/testUtils/index.ts +++ b/integration/testUtils/index.ts @@ -1,12 +1,11 @@ import { createClerkClient as backendCreateClerkClient } from '@clerk/backend'; -import { createPageObjects, createAppPageObject, type EnhancedPage } from '@clerk/testing/playwright/unstable'; +import { createAppPageObject, createPageObjects, type EnhancedPage } from '@clerk/testing/playwright/unstable'; import type { Browser, BrowserContext, Page } from '@playwright/test'; import type { Application } from '../models/application'; import { createEmailService } from './emailService'; import { createInvitationService } from './invitationsService'; import { createOrganizationsService } from './organizationsService'; - import type { FakeOrganization, FakeUser } from './usersService'; import { createUserService } from './usersService'; diff --git a/integration/tests/pricing-table.test.ts b/integration/tests/pricing-table.test.ts index 12fdf508b7c..3d203aa37cb 100644 --- a/integration/tests/pricing-table.test.ts +++ b/integration/tests/pricing-table.test.ts @@ -23,12 +23,17 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl test('renders pricing table with plans', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.po.page.goToRelative('/pricing-table'); + + await u.po.page.locator('.cl-pricingTable-root').waitFor({ state: 'attached' }); + await expect(u.po.page.getByRole('heading', { name: 'Pro' })).toBeVisible(); }); test('when signed out, clicking get started button navigates to sign in page', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.po.page.goToRelative('/pricing-table'); + await u.po.page.locator('.cl-pricingTable-root').waitFor({ state: 'attached' }); + await u.po.page.getByText('Get started').first().click(); await u.po.signIn.waitForMounted(); await expect(u.po.page.getByText('Checkout')).toBeHidden(); @@ -39,8 +44,10 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl await u.po.signIn.goTo(); await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); await u.po.page.goToRelative('/pricing-table'); + await u.po.page.locator('.cl-pricingTable-root').waitFor({ state: 'attached' }); await u.po.page.getByText('Get started').first().click(); - await expect(u.po.page.getByText('Checkout')).toBeVisible(); + await u.po.page.locator('.cl-checkout-root').waitFor({ state: 'attached' }); + await expect(u.po.page.getByText(/Checkout/i)).toBeVisible(); }); test('can subscribe to a plan', async ({ page, context }) => { @@ -48,9 +55,12 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl await u.po.signIn.goTo(); await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); await u.po.page.goToRelative('/pricing-table'); + await u.po.page.locator('.cl-pricingTable-root').waitFor({ state: 'attached' }); // We have two plans, so subscribe to the first one await u.po.page.getByText('Get started').first().click(); + await u.po.page.locator('.cl-checkout-root').waitFor({ state: 'attached' }); + // Stripe uses multiple iframes, so we need to find the correct one const frame = u.po.page.frameLocator('iframe[src*="elements-inner-payment"]'); await frame.getByLabel('Card number').fill('4242424242424242'); @@ -67,7 +77,10 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl await u.po.signIn.goTo(); await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); await u.po.page.goToRelative('/pricing-table'); + await u.po.page.locator('.cl-pricingTable-root').waitFor({ state: 'attached' }); + await u.po.page.getByText('Manage subscription').click(); + await u.po.page.getByRole('button', { name: 'Cancel subscription' }).click(); await u.po.page.getByRole('alertdialog').getByRole('button', { name: 'Cancel subscription' }).click(); await expect(u.po.page.getByRole('button', { name: 'Re-subscribe' }).first()).toBeVisible(); diff --git a/package.json b/package.json index 434bded6a01..0a459374786 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,7 @@ "test:integration:ap-flows": "pnpm test:integration:base --grep @ap-flows", "test:integration:astro": "E2E_APP_ID=astro.* pnpm test:integration:base --grep @astro", "test:integration:base": "pnpm playwright test --config integration/playwright.config.ts", - "test:integration:billing": "E2E_APP_ID=next.appRouter.withBilling pnpm test:integration:base --grep @billing", + "test:integration:billing": "E2E_APP_ID=withBilling.* pnpm test:integration:base --grep @billing", "test:integration:cleanup": "pnpm playwright test --config integration/playwright.cleanup.config.ts", "test:integration:deployment:nextjs": "pnpm playwright test --config integration/playwright.deployments.config.ts", "test:integration:elements": "E2E_APP_ID=elements.* pnpm test:integration:base --grep @elements", diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index ad9ad12aa38..96d3f0fd67e 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -162,6 +162,7 @@ export default defineNuxtModule({ 'SignedIn', 'SignedOut', 'Waitlist', + 'experimental_PricingTable', ]; components.forEach(component => { void addComponent({ diff --git a/packages/nuxt/src/runtime/components/index.ts b/packages/nuxt/src/runtime/components/index.ts index 12d0e860a8c..db7d8caa513 100644 --- a/packages/nuxt/src/runtime/components/index.ts +++ b/packages/nuxt/src/runtime/components/index.ts @@ -27,4 +27,5 @@ export { SignUpButton, SignOutButton, SignInWithMetamaskButton, + experimental_PricingTable, } from '@clerk/vue'; diff --git a/packages/vue/src/components/index.ts b/packages/vue/src/components/index.ts index 0d0d7740c84..f9af337103a 100644 --- a/packages/vue/src/components/index.ts +++ b/packages/vue/src/components/index.ts @@ -4,6 +4,7 @@ export { default as GoogleOneTap } from './ui-components/GoogleOneTap.vue'; export { default as Waitlist } from './ui-components/Waitlist.vue'; export { default as CreateOrganization } from './ui-components/CreateOrganization.vue'; export { default as OrganizationList } from './ui-components/OrganizationList.vue'; +export { default as experimental_PricingTable } from './ui-components/PricingTable.vue'; export { UserProfile } from './ui-components/UserProfile'; export { OrganizationProfile } from './ui-components/OrganizationProfile'; export { OrganizationSwitcher } from './ui-components/OrganizationSwitcher'; diff --git a/packages/vue/src/components/ui-components/PricingTable.vue b/packages/vue/src/components/ui-components/PricingTable.vue new file mode 100644 index 00000000000..339dc0c2642 --- /dev/null +++ b/packages/vue/src/components/ui-components/PricingTable.vue @@ -0,0 +1,17 @@ + + +