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 @@
+
+
+
+
+