Skip to content

Commit ea65f20

Browse files
authored
feat(vue): Add Billing buttons (#6680)
1 parent 5e3e12f commit ea65f20

File tree

13 files changed

+260
-16
lines changed

13 files changed

+260
-16
lines changed

.changeset/cold-parks-push.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/vue': minor
3+
---
4+
5+
Expose billing buttons as experimental

integration/templates/vue-vite/src/router.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,22 @@ const routes = [
4747
path: '/user',
4848
component: () => import('./views/Profile.vue'),
4949
},
50+
// Billing button routes
51+
{
52+
name: 'CheckoutBtn',
53+
path: '/billing/checkout-btn',
54+
component: () => import('./views/billing/CheckoutBtn.vue'),
55+
},
56+
{
57+
name: 'PlanDetailsBtn',
58+
path: '/billing/plan-details-btn',
59+
component: () => import('./views/billing/PlanDetailsBtn.vue'),
60+
},
61+
{
62+
name: 'SubscriptionDetailsBtn',
63+
path: '/billing/subscription-details-btn',
64+
component: () => import('./views/billing/SubscriptionDetailsBtn.vue'),
65+
},
5066
];
5167

5268
const router = createRouter({
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<template>
2+
<main>
3+
<SignedIn>
4+
<CheckoutButton
5+
planId="cplan_2wMjqdlza0hTJc4HLCoBwAiExhF"
6+
planPeriod="month"
7+
>
8+
Checkout Now
9+
</CheckoutButton>
10+
</SignedIn>
11+
</main>
12+
</template>
13+
14+
<script setup lang="ts">
15+
import { SignedIn } from '@clerk/vue';
16+
import { CheckoutButton } from '@clerk/vue/experimental';
17+
</script>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<template>
2+
<main>
3+
<PlanDetailsButton planId="cplan_2wMjqdlza0hTJc4HLCoBwAiExhF"> Plan details </PlanDetailsButton>
4+
</main>
5+
</template>
6+
7+
<script setup lang="ts">
8+
import { PlanDetailsButton } from '@clerk/vue/experimental';
9+
</script>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<template>
2+
<main>
3+
<SubscriptionDetailsButton> Subscription details </SubscriptionDetailsButton>
4+
</main>
5+
</template>
6+
7+
<script setup lang="ts">
8+
import { SubscriptionDetailsButton } from '@clerk/vue/experimental';
9+
</script>

integration/tests/pricing-table.test.ts

Lines changed: 5 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,8 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
3232
});
3333

3434
test('renders pricing details of a specific plan', async ({ page, context }) => {
35-
if (!app.name.includes('next')) {
36-
return;
37-
}
35+
test.skip(app.name.includes('astro'), 'Still working on it');
36+
3837
const u = createTestUtils({ app, page, context });
3938
await u.po.page.goToRelative('/billing/plan-details-btn');
4039

@@ -83,9 +82,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
8382
page,
8483
context,
8584
}) => {
86-
if (!app.name.includes('next')) {
87-
return;
88-
}
85+
test.skip(app.name.includes('astro'), 'Still working on it');
8986
const u = createTestUtils({ app, page, context });
9087
await u.po.signIn.goTo();
9188
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
@@ -100,9 +97,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
10097
});
10198

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

139134
test('opens subscription details drawer', async ({ page, context }) => {
140-
if (!app.name.includes('next')) {
141-
return;
142-
}
135+
test.skip(app.name.includes('astro'), 'Still working on it');
143136
const u = createTestUtils({ app, page, context });
144137
await u.po.signIn.goTo();
145138
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });

packages/vue/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@
3030
"types": "./dist/index.d.ts",
3131
"default": "./dist/index.js"
3232
},
33+
"./experimental": {
34+
"types": "./dist/experimental.d.ts",
35+
"default": "./dist/experimental.js"
36+
},
3337
"./internal": {
3438
"types": "./dist/internal.d.ts",
3539
"default": "./dist/internal.js"
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<script setup lang="ts">
2+
import { useAttrs, useSlots } from 'vue';
3+
import type { __experimental_CheckoutButtonProps } from '@clerk/types';
4+
import { useClerk } from '../composables/useClerk';
5+
import { useAuth } from '../composables/useAuth';
6+
import { assertSingleChild, normalizeWithDefaultValue } from '../utils';
7+
8+
const props = defineProps<__experimental_CheckoutButtonProps>();
9+
10+
const clerk = useClerk();
11+
const { userId, orgId } = useAuth();
12+
const slots = useSlots();
13+
const attrs = useAttrs();
14+
15+
// Authentication checks - similar to React implementation
16+
if (userId.value === null) {
17+
throw new Error('Ensure that `<CheckoutButton />` is rendered inside a `<SignedIn />` component.');
18+
}
19+
20+
if (orgId.value === null && props.for === 'organization') {
21+
throw new Error('Wrap `<CheckoutButton for="organization" />` with a check for an active organization.');
22+
}
23+
24+
function getChildComponent() {
25+
const children = normalizeWithDefaultValue(slots.default?.({}), 'Checkout');
26+
return assertSingleChild(children, 'CheckoutButton');
27+
}
28+
29+
function clickHandler() {
30+
if (!clerk.value) {
31+
return;
32+
}
33+
34+
return clerk.value.__internal_openCheckout({
35+
planId: props.planId,
36+
planPeriod: props.planPeriod,
37+
for: props.for,
38+
onSubscriptionComplete: props.onSubscriptionComplete,
39+
newSubscriptionRedirectUrl: props.newSubscriptionRedirectUrl,
40+
...props.checkoutProps,
41+
});
42+
}
43+
</script>
44+
45+
<template>
46+
<component
47+
:is="getChildComponent"
48+
v-bind="attrs"
49+
@click="clickHandler"
50+
>
51+
<slot />
52+
</component>
53+
</template>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<script setup lang="ts">
2+
import { useAttrs, useSlots } from 'vue';
3+
import type { __experimental_PlanDetailsButtonProps } from '@clerk/types';
4+
import { useClerk } from '../composables/useClerk';
5+
import { assertSingleChild, normalizeWithDefaultValue } from '../utils';
6+
7+
const props = defineProps<__experimental_PlanDetailsButtonProps>();
8+
9+
const clerk = useClerk();
10+
const slots = useSlots();
11+
const attrs = useAttrs();
12+
13+
function getChildComponent() {
14+
const children = normalizeWithDefaultValue(slots.default?.({}), 'Plan details');
15+
return assertSingleChild(children, 'PlanDetailsButton');
16+
}
17+
18+
function clickHandler() {
19+
if (!clerk.value) {
20+
return;
21+
}
22+
23+
return clerk.value.__internal_openPlanDetails({
24+
plan: props.plan,
25+
planId: props.planId,
26+
initialPlanPeriod: props.initialPlanPeriod,
27+
...props.planDetailsProps,
28+
} as __experimental_PlanDetailsButtonProps);
29+
}
30+
</script>
31+
32+
<template>
33+
<component
34+
:is="getChildComponent"
35+
v-bind="attrs"
36+
@click="clickHandler"
37+
>
38+
<slot />
39+
</component>
40+
</template>
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<script setup lang="ts">
2+
import { useAttrs, useSlots } from 'vue';
3+
import type { __experimental_SubscriptionDetailsButtonProps } from '@clerk/types';
4+
import { useClerk } from '../composables/useClerk';
5+
import { useAuth } from '../composables/useAuth';
6+
import { assertSingleChild, normalizeWithDefaultValue } from '../utils';
7+
8+
const props = defineProps<__experimental_SubscriptionDetailsButtonProps>();
9+
10+
const clerk = useClerk();
11+
const { userId, orgId } = useAuth();
12+
const slots = useSlots();
13+
const attrs = useAttrs();
14+
15+
// Authentication checks - similar to React implementation
16+
if (userId.value === null) {
17+
throw new Error('Ensure that `<SubscriptionDetailsButton />` is rendered inside a `<SignedIn />` component.');
18+
}
19+
20+
if (orgId.value === null && props.for === 'organization') {
21+
throw new Error('Wrap `<SubscriptionDetailsButton for="organization" />` with a check for an active organization.');
22+
}
23+
24+
function getChildComponent() {
25+
const children = normalizeWithDefaultValue(slots.default?.({}), 'Subscription details');
26+
return assertSingleChild(children, 'SubscriptionDetailsButton');
27+
}
28+
29+
function clickHandler() {
30+
if (!clerk.value) {
31+
return;
32+
}
33+
34+
return clerk.value.__internal_openSubscriptionDetails({
35+
for: props.for,
36+
onSubscriptionCancel: props.onSubscriptionCancel,
37+
...props.subscriptionDetailsProps,
38+
});
39+
}
40+
</script>
41+
42+
<template>
43+
<component
44+
:is="getChildComponent"
45+
v-bind="attrs"
46+
@click="clickHandler"
47+
>
48+
<slot />
49+
</component>
50+
</template>

0 commit comments

Comments
 (0)