Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cold-parks-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/vue': minor
---

Expose billing buttons as experimental
16 changes: 16 additions & 0 deletions integration/templates/vue-vite/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,22 @@ const routes = [
path: '/user',
component: () => import('./views/Profile.vue'),
},
// Billing button routes
{
name: 'CheckoutBtn',
path: '/billing/checkout-btn',
component: () => import('./views/billing/CheckoutBtn.vue'),
},
{
name: 'PlanDetailsBtn',
path: '/billing/plan-details-btn',
component: () => import('./views/billing/PlanDetailsBtn.vue'),
},
{
name: 'SubscriptionDetailsBtn',
path: '/billing/subscription-details-btn',
component: () => import('./views/billing/SubscriptionDetailsBtn.vue'),
},
];

const router = createRouter({
Expand Down
17 changes: 17 additions & 0 deletions integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<template>
<main>
<SignedIn>
<CheckoutButton
planId="cplan_2wMjqdlza0hTJc4HLCoBwAiExhF"
planPeriod="month"
>
Checkout Now
</CheckoutButton>
</SignedIn>
</main>
</template>

<script setup lang="ts">
import { SignedIn } from '@clerk/vue';
import { CheckoutButton } from '@clerk/vue/experimental';
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<main>
<PlanDetailsButton planId="cplan_2wMjqdlza0hTJc4HLCoBwAiExhF"> Plan details </PlanDetailsButton>
</main>
</template>

<script setup lang="ts">
import { PlanDetailsButton } from '@clerk/vue/experimental';
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<template>
<main>
<SubscriptionDetailsButton> Subscription details </SubscriptionDetailsButton>
</main>
</template>

<script setup lang="ts">
import { SubscriptionDetailsButton } from '@clerk/vue/experimental';
</script>
17 changes: 5 additions & 12 deletions integration/tests/pricing-table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
});

test('renders pricing details of a specific plan', async ({ page, context }) => {
if (!app.name.includes('next')) {
return;
}
test.skip(app.name.includes('astro'), 'Still working on it');

const u = createTestUtils({ app, page, context });
await u.po.page.goToRelative('/billing/plan-details-btn');

Expand Down Expand Up @@ -83,9 +82,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
page,
context,
}) => {
if (!app.name.includes('next')) {
return;
}
test.skip(app.name.includes('astro'), 'Still working on it');
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
Expand All @@ -100,9 +97,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
});

test('when signed in, clicking checkout button open checkout drawer', async ({ page, context }) => {
if (!app.name.includes('next')) {
return;
}
test.skip(app.name.includes('astro'), 'Still working on it');
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
Expand Down Expand Up @@ -137,9 +132,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
});

test('opens subscription details drawer', async ({ page, context }) => {
if (!app.name.includes('next')) {
return;
}
test.skip(app.name.includes('astro'), 'Still working on it');
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
Expand Down
4 changes: 4 additions & 0 deletions packages/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./experimental": {
"types": "./dist/experimental.d.ts",
"default": "./dist/experimental.js"
},
"./internal": {
"types": "./dist/internal.d.ts",
"default": "./dist/internal.js"
Expand Down
53 changes: 53 additions & 0 deletions packages/vue/src/components/CheckoutButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script setup lang="ts">
import { useAttrs, useSlots } from 'vue';
import type { __experimental_CheckoutButtonProps } from '@clerk/types';
import { useClerk } from '../composables/useClerk';
import { useAuth } from '../composables/useAuth';
import { assertSingleChild, normalizeWithDefaultValue } from '../utils';

const props = defineProps<__experimental_CheckoutButtonProps>();

const clerk = useClerk();
const { userId, orgId } = useAuth();
const slots = useSlots();
const attrs = useAttrs();

// Authentication checks - similar to React implementation
if (userId.value === null) {
throw new Error('Ensure that `<CheckoutButton />` is rendered inside a `<SignedIn />` component.');
}

if (orgId.value === null && props.for === 'organization') {
throw new Error('Wrap `<CheckoutButton for="organization" />` with a check for an active organization.');
}

function getChildComponent() {
const children = normalizeWithDefaultValue(slots.default?.({}), 'Checkout');
return assertSingleChild(children, 'CheckoutButton');
}

function clickHandler() {
if (!clerk.value) {
return;
}

return clerk.value.__internal_openCheckout({
planId: props.planId,
planPeriod: props.planPeriod,
for: props.for,
onSubscriptionComplete: props.onSubscriptionComplete,
newSubscriptionRedirectUrl: props.newSubscriptionRedirectUrl,
...props.checkoutProps,
});
}
</script>

<template>
<component
:is="getChildComponent"
v-bind="attrs"
@click="clickHandler"
>
<slot />
</component>
</template>
40 changes: 40 additions & 0 deletions packages/vue/src/components/PlanDetailsButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script setup lang="ts">
import { useAttrs, useSlots } from 'vue';
import type { __experimental_PlanDetailsButtonProps } from '@clerk/types';
import { useClerk } from '../composables/useClerk';
import { assertSingleChild, normalizeWithDefaultValue } from '../utils';

const props = defineProps<__experimental_PlanDetailsButtonProps>();

const clerk = useClerk();
const slots = useSlots();
const attrs = useAttrs();

function getChildComponent() {
const children = normalizeWithDefaultValue(slots.default?.({}), 'Plan details');
return assertSingleChild(children, 'PlanDetailsButton');
}

function clickHandler() {
if (!clerk.value) {
return;
}

return clerk.value.__internal_openPlanDetails({
plan: props.plan,
planId: props.planId,
initialPlanPeriod: props.initialPlanPeriod,
...props.planDetailsProps,
} as __experimental_PlanDetailsButtonProps);
}
</script>

<template>
<component
:is="getChildComponent"
v-bind="attrs"
@click="clickHandler"
>
<slot />
</component>
</template>
50 changes: 50 additions & 0 deletions packages/vue/src/components/SubscriptionDetailsButton.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<script setup lang="ts">
import { useAttrs, useSlots } from 'vue';
import type { __experimental_SubscriptionDetailsButtonProps } from '@clerk/types';
import { useClerk } from '../composables/useClerk';
import { useAuth } from '../composables/useAuth';
import { assertSingleChild, normalizeWithDefaultValue } from '../utils';

const props = defineProps<__experimental_SubscriptionDetailsButtonProps>();

const clerk = useClerk();
const { userId, orgId } = useAuth();
const slots = useSlots();
const attrs = useAttrs();

// Authentication checks - similar to React implementation
if (userId.value === null) {
throw new Error('Ensure that `<SubscriptionDetailsButton />` is rendered inside a `<SignedIn />` component.');
}

if (orgId.value === null && props.for === 'organization') {
throw new Error('Wrap `<SubscriptionDetailsButton for="organization" />` with a check for an active organization.');
}

function getChildComponent() {
const children = normalizeWithDefaultValue(slots.default?.({}), 'Subscription details');
return assertSingleChild(children, 'SubscriptionDetailsButton');
}

function clickHandler() {
if (!clerk.value) {
return;
}

return clerk.value.__internal_openSubscriptionDetails({
for: props.for,
onSubscriptionCancel: props.onSubscriptionCancel,
...props.subscriptionDetailsProps,
});
}
</script>

<template>
<component
:is="getChildComponent"
v-bind="attrs"
@click="clickHandler"
>
<slot />
</component>
</template>
41 changes: 41 additions & 0 deletions packages/vue/src/experimental.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* @experimental
* These components and their prop types are unstable and may change in future releases.
* They are part of Clerk's Billing feature which is available under public beta.
*/
export { default as SubscriptionDetailsButton } from './components/SubscriptionDetailsButton.vue';

/**
* @experimental
* These components and their prop types are unstable and may change in future releases.
* They are part of Clerk's Billing feature which is available under public beta.
*/
export { default as CheckoutButton } from './components/CheckoutButton.vue';

/**
* @experimental
* These components and their prop types are unstable and may change in future releases.
* They are part of Clerk's Billing feature which is available under public beta.
*/
export { default as PlanDetailsButton } from './components/PlanDetailsButton.vue';

export type {
/**
* @experimental
* These components and their prop types are unstable and may change in future releases.
* They are part of Clerk's Billing feature which is available under public beta.
*/
__experimental_SubscriptionDetailsButtonProps as SubscriptionDetailsButtonProps,
/**
* @experimental
* These components and their prop types are unstable and may change in future releases.
* They are part of Clerk's Billing feature which is available under public beta.
*/
__experimental_CheckoutButtonProps as CheckoutButtonProps,
/**
* @experimental
* These components and their prop types are unstable and may change in future releases.
* They are part of Clerk's Billing feature which is available under public beta.
*/
__experimental_PlanDetailsButtonProps as PlanDetailsButtonProps,
} from '@clerk/types';
9 changes: 8 additions & 1 deletion packages/vue/src/utils/childrenUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,14 @@ import { h, Text, type VNode } from 'vue';
import { errorThrower } from '../errors/errorThrower';
import { multipleChildrenInButtonComponent } from '../errors/messages';

type ButtonName = 'SignInButton' | 'SignUpButton' | 'SignOutButton' | 'SignInWithMetamaskButton';
type ButtonName =
| 'SignInButton'
| 'SignUpButton'
| 'SignOutButton'
| 'SignInWithMetamaskButton'
| 'SubscriptionDetailsButton'
| 'CheckoutButton'
| 'PlanDetailsButton';

export const normalizeWithDefaultValue = (slotContent: VNode[] | undefined, defaultValue: string) => {
// Render a button with the default value if no slot content is provided
Expand Down
6 changes: 3 additions & 3 deletions packages/vue/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,21 @@ type EsbuildPlugin = NonNullable<Options['esbuildPlugins']>[number];
export default defineConfig(() => {
return {
clean: true,
entry: ['./src/index.ts', './src/internal.ts', './src/errors.ts'],
entry: ['./src/index.ts', './src/experimental.ts', './src/internal.ts', './src/errors.ts'],
format: ['esm'],
bundle: true,
sourcemap: true,
minify: false,
dts: false,
esbuildPlugins: [
// Adds .vue files support
vuePlugin() as EsbuildPlugin,
vuePlugin(),
// Automatically generates runtime props from TypeScript types/interfaces for all
// control and UI components, adding them to Vue components during build via
// Object.defineProperty
autoPropsPlugin({
include: ['**/*.ts'],
}) as EsbuildPlugin,
}),
],
define: {
PACKAGE_NAME: `"${name}"`,
Expand Down