Skip to content
Open
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
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 { 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 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,55 @@
---
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);
});
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
---
import type { __experimental_SubscriptionDetailsButtonProps } from '@clerk/types';

import type { HTMLTag, Polymorphic } from 'astro/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,
};
Comment on lines +28 to +32
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Make top-level props take precedence over nested subscriptionDetailsProps

Currently subscriptionDetailsProps can override for/onSubscriptionCancel, which is surprising. Prefer explicit top-level props to win.

Apply:

-const subscriptionDetailsOptions = {
-  for: _for,
-  onSubscriptionCancel,
-  ...subscriptionDetailsProps,
-};
+const subscriptionDetailsOptions = {
+  ...subscriptionDetailsProps,
+  for: _for,
+  onSubscriptionCancel,
+};

Verification (consistency with sibling Astro buttons):


🏁 Script executed:

#!/bin/bash
# Inspect options precedence across related unstyled Astro buttons
rg -n -C2 --type=astro 'checkoutOptions|planDetailsOptions|subscriptionDetailsOptions' packages/astro/src/astro-components/unstyled

Length of output: 163


🏁 Script executed:

#!/bin/bash
# Inspect options precedence across related unstyled Astro buttons
rg -n -C2 'checkoutOptions' packages/astro/src/astro-components/unstyled -g '*.astro'
rg -n -C2 'planDetailsOptions' packages/astro/src/astro-components/unstyled -g '*.astro'
rg -n -C2 'subscriptionDetailsOptions' packages/astro/src/astro-components/unstyled -g '*.astro'

Length of output: 5783


🏁 Script executed:

#!/bin/bash
sed -n '1,60p' packages/astro/src/astro-components/unstyled/CheckoutButton.astro
echo '---'
sed -n '1,60p' packages/astro/src/astro-components/unstyled/PlanDetailsButton.astro

Length of output: 2932


Align prop precedence across all unstyled Astro buttons

To ensure top-level props always override nested ones, we need to reverse the spread order in all three buttons—currently each one spreads nested props after the explicit fields, allowing nested props to win. Update as follows:

• packages/astro/src/astro-components/unstyled/CheckoutButton.astro
Replace:

const checkoutOptions = {
  planId,
  planPeriod,
  for: _for,
  onSubscriptionComplete,
  newSubscriptionRedirectUrl,
-  ...checkoutProps,
};
+  ...checkoutProps,
+  planId,
+  planPeriod,
+  for: _for,
+  onSubscriptionComplete,
+  newSubscriptionRedirectUrl,
};

• packages/astro/src/astro-components/unstyled/PlanDetailsButton.astro
Replace:

const planDetailsOptions = {
  plan,
  planId,
  initialPlanPeriod,
-  ...planDetailsProps,
};
+  ...planDetailsProps,
+  plan,
+  planId,
+  initialPlanPeriod,
};

• packages/astro/src/astro-components/unstyled/SubscriptionDetailsButton.astro
Replace:

const subscriptionDetailsOptions = {
  for: _for,
  onSubscriptionCancel,
-  ...subscriptionDetailsProps,
};
+  ...subscriptionDetailsProps,
+  for: _for,
+  onSubscriptionCancel,
};

This guarantees explicit for, planId, etc., always take precedence over any overrides passed in via *Props.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const subscriptionDetailsOptions = {
for: _for,
onSubscriptionCancel,
...subscriptionDetailsProps,
};
const subscriptionDetailsOptions = {
...subscriptionDetailsProps,
for: _for,
onSubscriptionCancel,
};
🤖 Prompt for AI Agents
In packages/astro/src/astro-components/unstyled/SubscriptionDetailsButton.astro
around lines 28-32 (and also apply the same change to
packages/astro/src/astro-components/unstyled/CheckoutButton.astro and
packages/astro/src/astro-components/unstyled/PlanDetailsButton.astro), the
object spread currently places the nested props last so they win; reverse the
spread order so nested props are spread first and then explicit fields follow
(i.e., move ...subscriptionDetailsProps before the explicit for and
onSubscriptionCancel entries) so top-level explicit props take precedence over
the *Props overrides.


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);
});
</script>
Loading
Loading