Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
6 changes: 6 additions & 0 deletions .changeset/cold-parks-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/astro': minor
'@clerk/vue': minor
---

Expose billing buttons as experimental
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
import { SignedIn, __experimental_CheckoutButton as CheckoutButton } from '@clerk/astro/components';
import Layout from '../../layouts/Layout.astro';
---

<Layout title="Checkout Button">
<main>
<SignedIn>
<CheckoutButton
planId='cplan_2wMjqdlza0hTJc4HLCoBwAiExhF'
planPeriod='month'
>
Checkout Now
</CheckoutButton>
</SignedIn>
</main>
</Layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
import { __experimental_PlanDetailsButton as PlanDetailsButton } from '@clerk/astro/components';
import Layout from '../../layouts/Layout.astro';
---

<Layout title="Plan Details Button">
<main>
<PlanDetailsButton planId='cplan_2wMjqdlza0hTJc4HLCoBwAiExhF'>
Plan details
</PlanDetailsButton>
</main>
</Layout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
import { __experimental_SubscriptionDetailsButton as SubscriptionDetailsButton } from '@clerk/astro/components';
import Layout from '../../layouts/Layout.astro';
---

<Layout title="Subscription Details Button">
<main>
<SubscriptionDetailsButton>
Subscription details
</SubscriptionDetailsButton>
</main>
</Layout>
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>
16 changes: 3 additions & 13 deletions integration/tests/pricing-table.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ 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 @@ -62,10 +60,6 @@ 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 });
Expand All @@ -92,9 +86,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 @@ -129,9 +121,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withBilling] })('pricing tabl
});

test('Displays 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
3 changes: 3 additions & 0 deletions packages/astro/src/astro-components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export { default as AuthenticateWithRedirectCallback } from './control/Authentic
export { default as SignInButton } from './unstyled/SignInButton.astro';
export { default as SignUpButton } from './unstyled/SignUpButton.astro';
export { default as SignOutButton } from './unstyled/SignOutButton.astro';
export { default as __experimental_SubscriptionDetailsButton } from './unstyled/SubscriptionDetailsButton.astro';
export { default as __experimental_CheckoutButton } from './unstyled/CheckoutButton.astro';
export { default as __experimental_PlanDetailsButton } from './unstyled/PlanDetailsButton.astro';

/**
* UI Components
Expand Down
76 changes: 76 additions & 0 deletions packages/astro/src/astro-components/unstyled/CheckoutButton.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types';
import type { __experimental_CheckoutButtonProps } from '@clerk/types';
import type { ButtonProps } from '../../types';
import { addUnstyledAttributeToFirstTag, logAsPropUsageDeprecation } from './utils';
type Props<Tag extends HTMLTag = 'button'> = Polymorphic<ButtonProps<Tag>> & __experimental_CheckoutButtonProps;
import { generateSafeId } from '@clerk/astro/internal';
const safeId = generateSafeId();
if ('as' in Astro.props) {
logAsPropUsageDeprecation();
}
const {
as: Tag = 'button',
asChild,
planId,
planPeriod,
for: _for,
onSubscriptionComplete,
newSubscriptionRedirectUrl,
checkoutProps,
...props
} = Astro.props;
const checkoutOptions = {
planId,
planPeriod,
for: _for,
onSubscriptionComplete,
newSubscriptionRedirectUrl,
...checkoutProps,
};
let htmlElement = '';
if (asChild) {
htmlElement = await Astro.slots.render('default');
htmlElement = addUnstyledAttributeToFirstTag(htmlElement, safeId);
}
---

{
asChild ? (
<Fragment set:html={htmlElement} />
) : (
<Tag
{...props}
data-clerk-unstyled-id={safeId}
>
<slot>Checkout</slot>
</Tag>
)
}

<script is:inline define:vars={{ props, checkoutOptions, safeId }}>
const btn = document.querySelector(`[data-clerk-unstyled-id="${safeId}"]`);

btn.addEventListener('click', () => {
const clerk = window.Clerk;

// Authentication checks
if (!clerk.user) {
throw new Error('Ensure that `<CheckoutButton />` is rendered inside a `<SignedIn />` component.');
}

if (!clerk.organization && checkoutOptions.for === 'organization') {
throw new Error('Wrap `<CheckoutButton for="organization" />` with a check for an active organization.');
}

return clerk.__internal_openCheckout(checkoutOptions);
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types';
import type { __experimental_PlanDetailsButtonProps } from '@clerk/types';
import type { ButtonProps } from '../../types';
import { addUnstyledAttributeToFirstTag, logAsPropUsageDeprecation } from './utils';
type Props<Tag extends HTMLTag = 'button'> = Polymorphic<ButtonProps<Tag>> & __experimental_PlanDetailsButtonProps;
import { generateSafeId } from '@clerk/astro/internal';
const safeId = generateSafeId();
if ('as' in Astro.props) {
logAsPropUsageDeprecation();
}
const {
as: Tag = 'button',
asChild,
plan,
planId,
initialPlanPeriod,
planDetailsProps,
...props
} = Astro.props;
const planDetailsOptions = {
plan,
planId,
initialPlanPeriod,
...planDetailsProps,
};
let htmlElement = '';
if (asChild) {
htmlElement = await Astro.slots.render('default');
htmlElement = addUnstyledAttributeToFirstTag(htmlElement, safeId);
}
---

{
asChild ? (
<Fragment set:html={htmlElement} />
) : (
<Tag
{...props}
data-clerk-unstyled-id={safeId}
>
<slot>Plan details</slot>
</Tag>
)
}

<script is:inline define:vars={{ props, planDetailsOptions, safeId }}>
const btn = document.querySelector(`[data-clerk-unstyled-id="${safeId}"]`);

btn.addEventListener('click', () => {
const clerk = window.Clerk;

return clerk.__internal_openPlanDetails(planDetailsOptions);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto with the onSubscriptionComplete event https://github.com/clerk/javascript/pull/6583/files#r2333981663

});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
import type { HTMLTag, Polymorphic } from 'astro/types';
import type { __experimental_SubscriptionDetailsButtonProps } from '@clerk/types';
import type { ButtonProps } from '../../types';
import { addUnstyledAttributeToFirstTag, logAsPropUsageDeprecation } from './utils';
type Props<Tag extends HTMLTag = 'button'> = Polymorphic<ButtonProps<Tag>> & __experimental_SubscriptionDetailsButtonProps;
import { generateSafeId } from '@clerk/astro/internal';
const safeId = generateSafeId();
if ('as' in Astro.props) {
logAsPropUsageDeprecation();
}
const {
as: Tag = 'button',
asChild,
for: _for,
subscriptionDetailsProps,
onSubscriptionCancel,
...props
} = Astro.props;
const subscriptionDetailsOptions = {
for: _for,
onSubscriptionCancel,
...subscriptionDetailsProps,
};
let htmlElement = '';
if (asChild) {
htmlElement = await Astro.slots.render('default');
htmlElement = addUnstyledAttributeToFirstTag(htmlElement, safeId);
}
---

{
asChild ? (
<Fragment set:html={htmlElement} />
) : (
<Tag
{...props}
data-clerk-unstyled-id={safeId}
>
<slot>Subscription details</slot>
</Tag>
)
}

<script is:inline define:vars={{ props, subscriptionDetailsOptions, safeId }}>
const btn = document.querySelector(`[data-clerk-unstyled-id="${safeId}"]`);

btn.addEventListener('click', () => {
const clerk = window.Clerk;

// Authentication checks
if (!clerk.user) {
throw new Error('Ensure that `<SubscriptionDetailsButton />` is rendered inside a `<SignedIn />` component.');
}

if (!clerk.organization && subscriptionDetailsOptions.for === 'organization') {
throw new Error(
'Wrap `<SubscriptionDetailsButton for="organization" />` with a check for an active organization.',
);
}

return clerk.__internal_openSubscriptionDetails(subscriptionDetailsOptions);
Copy link
Member

@wobsoriano wobsoriano Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we can't pass a function as prop here, the onSubscriptionCancel prop is unusable, but we can use a custom event like this

const customEvent = new CustomEvent('clerk:subscription-cancel');
clerk.__internal_openSubscriptionDetails({
  ...subscriptionDetailsOptions,
  onSubscriptionCancel: () => document.dispatchEvent(customEvent),
});

and in the parent component, they can listen to it

<script>
  document.addEventListener('clerk:subscription-cancel', (e) => {
     //...
  })
</script>

We do this pattern in our <UserButton> custom menu items in Astro

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handled here

});
</script>
Loading
Loading