Skip to content

Commit b1d9aac

Browse files
authored
feat(astro): Add Billing buttons (#6583)
1 parent 720ca64 commit b1d9aac

13 files changed

Lines changed: 429 additions & 2 deletions

File tree

.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/astro': minor
3+
---
4+
5+
Expose billing buttons as experimental
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
import { SignedIn, __experimental_CheckoutButton as CheckoutButton } from '@clerk/astro/components';
3+
import Layout from '../../layouts/Layout.astro';
4+
---
5+
6+
<Layout title="Checkout Button">
7+
<main>
8+
<SignedIn>
9+
<CheckoutButton
10+
planId='cplan_2wMjqdlza0hTJc4HLCoBwAiExhF'
11+
planPeriod='month'
12+
>
13+
Checkout Now
14+
</CheckoutButton>
15+
</SignedIn>
16+
</main>
17+
</Layout>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
import { PlanDetailsButton } from '@clerk/astro/components';
3+
import Layout from '../../layouts/Layout.astro';
4+
---
5+
6+
<Layout title="Plan Details Button">
7+
<main>
8+
<PlanDetailsButton planId='cplan_2wMjqdlza0hTJc4HLCoBwAiExhF'>
9+
Plan details
10+
</PlanDetailsButton>
11+
</main>
12+
</Layout>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
import { __experimental_SubscriptionDetailsButton as SubscriptionDetailsButton } from '@clerk/astro/components';
3+
import Layout from '../../layouts/Layout.astro';
4+
---
5+
6+
<Layout title="Subscription Details Button">
7+
<main>
8+
<SubscriptionDetailsButton>
9+
Subscription details
10+
</SubscriptionDetailsButton>
11+
</main>
12+
</Layout>

packages/astro/src/astro-components/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export { default as AuthenticateWithRedirectCallback } from './control/Authentic
1212
export { default as SignInButton } from './unstyled/SignInButton.astro';
1313
export { default as SignUpButton } from './unstyled/SignUpButton.astro';
1414
export { default as SignOutButton } from './unstyled/SignOutButton.astro';
15+
export { default as __experimental_SubscriptionDetailsButton } from './unstyled/SubscriptionDetailsButton.astro';
16+
export { default as __experimental_CheckoutButton } from './unstyled/CheckoutButton.astro';
17+
export { default as PlanDetailsButton } from './unstyled/PlanDetailsButton.astro';
1518

1619
/**
1720
* UI Components
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
---
2+
import type { HTMLTag, Polymorphic } from 'astro/types';
3+
import type { __experimental_CheckoutButtonProps } from '@clerk/types';
4+
import type { ButtonProps } from '../../types';
5+
import { addUnstyledAttributeToFirstTag, logAsPropUsageDeprecation } from './utils';
6+
7+
type Props<Tag extends HTMLTag = 'button'> = Polymorphic<ButtonProps<Tag>> &
8+
Omit<__experimental_CheckoutButtonProps, 'onSubscriptionComplete'> & { clickIdentifier?: string };
9+
10+
import { generateSafeId } from '@clerk/astro/internal';
11+
12+
const safeId = generateSafeId();
13+
14+
if ('as' in Astro.props) {
15+
logAsPropUsageDeprecation();
16+
}
17+
18+
const {
19+
as: Tag = 'button',
20+
asChild,
21+
planId,
22+
planPeriod,
23+
for: _for,
24+
newSubscriptionRedirectUrl,
25+
checkoutProps,
26+
clickIdentifier,
27+
...props
28+
} = Astro.props;
29+
30+
const checkoutOptions = {
31+
planId,
32+
planPeriod,
33+
for: _for,
34+
newSubscriptionRedirectUrl,
35+
clickIdentifier,
36+
...checkoutProps,
37+
};
38+
39+
let htmlElement = '';
40+
41+
if (asChild) {
42+
htmlElement = await Astro.slots.render('default');
43+
htmlElement = addUnstyledAttributeToFirstTag(htmlElement, safeId);
44+
}
45+
---
46+
47+
{
48+
asChild ? (
49+
<Fragment set:html={htmlElement} />
50+
) : (
51+
<Tag
52+
{...props}
53+
data-clerk-unstyled-id={safeId}
54+
>
55+
<slot>Checkout</slot>
56+
</Tag>
57+
)
58+
}
59+
60+
<script is:inline define:vars={{ props, checkoutOptions, safeId }}>
61+
const btn = document.querySelector(`[data-clerk-unstyled-id="${safeId}"]`);
62+
63+
btn.addEventListener('click', () => {
64+
const clerk = window.Clerk;
65+
66+
// Authentication checks
67+
if (!clerk.user) {
68+
throw new Error('Ensure that `<CheckoutButton />` is rendered inside a `<SignedIn />` component.');
69+
}
70+
71+
if (!clerk.organization && checkoutOptions.for === 'organization') {
72+
throw new Error('Wrap `<CheckoutButton for="organization" />` with a check for an active organization.');
73+
}
74+
75+
console.log('checkoutOptions', checkoutOptions);
76+
77+
if (checkoutOptions.clickIdentifier) {
78+
const clickEvent = new CustomEvent('clerk:subscription-complete', { detail: checkoutOptions.clickIdentifier });
79+
checkoutOptions.onSubscriptionComplete = () => {
80+
document.dispatchEvent(clickEvent);
81+
};
82+
}
83+
84+
return clerk.__internal_openCheckout(checkoutOptions);
85+
});
86+
</script>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
import type { HTMLTag, Polymorphic } from 'astro/types';
3+
import type { __experimental_PlanDetailsButtonProps } from '@clerk/types';
4+
import type { ButtonProps } from '../../types';
5+
import { addUnstyledAttributeToFirstTag, logAsPropUsageDeprecation } from './utils';
6+
7+
type Props<Tag extends HTMLTag = 'button'> = Polymorphic<ButtonProps<Tag>> & __experimental_PlanDetailsButtonProps;
8+
9+
import { generateSafeId } from '@clerk/astro/internal';
10+
11+
const safeId = generateSafeId();
12+
13+
if ('as' in Astro.props) {
14+
logAsPropUsageDeprecation();
15+
}
16+
17+
const { as: Tag = 'button', asChild, plan, planId, initialPlanPeriod, planDetailsProps, ...props } = Astro.props;
18+
19+
const planDetailsOptions = {
20+
plan,
21+
planId,
22+
initialPlanPeriod,
23+
...planDetailsProps,
24+
};
25+
26+
let htmlElement = '';
27+
28+
if (asChild) {
29+
htmlElement = await Astro.slots.render('default');
30+
htmlElement = addUnstyledAttributeToFirstTag(htmlElement, safeId);
31+
}
32+
---
33+
34+
{
35+
asChild ? (
36+
<Fragment set:html={htmlElement} />
37+
) : (
38+
<Tag
39+
{...props}
40+
data-clerk-unstyled-id={safeId}
41+
>
42+
<slot>Plan details</slot>
43+
</Tag>
44+
)
45+
}
46+
47+
<script is:inline define:vars={{ props, planDetailsOptions, safeId }}>
48+
const btn = document.querySelector(`[data-clerk-unstyled-id="${safeId}"]`);
49+
50+
btn.addEventListener('click', () => {
51+
const clerk = window.Clerk;
52+
53+
return clerk.__internal_openPlanDetails(planDetailsOptions);
54+
});
55+
</script>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
---
2+
import type { __experimental_SubscriptionDetailsButtonProps } from '@clerk/types';
3+
4+
import type { HTMLTag, Polymorphic } from 'astro/types';
5+
import type { ButtonProps } from '../../types';
6+
import { addUnstyledAttributeToFirstTag, logAsPropUsageDeprecation } from './utils';
7+
8+
type Props<Tag extends HTMLTag = 'button'> = Polymorphic<ButtonProps<Tag>> &
9+
Omit<__experimental_SubscriptionDetailsButtonProps, 'onSubscriptionCancel'> & { clickIdentifier?: string };
10+
11+
import { generateSafeId } from '@clerk/astro/internal';
12+
13+
const safeId = generateSafeId();
14+
15+
if ('as' in Astro.props) {
16+
logAsPropUsageDeprecation();
17+
}
18+
19+
const { as: Tag = 'button', asChild, for: _for, clickIdentifier, subscriptionDetailsProps, ...props } = Astro.props;
20+
21+
const subscriptionDetailsOptions = {
22+
for: _for,
23+
clickIdentifier,
24+
...subscriptionDetailsProps,
25+
};
26+
27+
let htmlElement = '';
28+
29+
if (asChild) {
30+
htmlElement = await Astro.slots.render('default');
31+
htmlElement = addUnstyledAttributeToFirstTag(htmlElement, safeId);
32+
}
33+
---
34+
35+
{
36+
asChild ? (
37+
<Fragment set:html={htmlElement} />
38+
) : (
39+
<Tag
40+
{...props}
41+
data-clerk-unstyled-id={safeId}
42+
>
43+
<slot>Subscription details</slot>
44+
</Tag>
45+
)
46+
}
47+
48+
<script is:inline define:vars={{ props, subscriptionDetailsOptions, safeId }}>
49+
const btn = document.querySelector(`[data-clerk-unstyled-id="${safeId}"]`);
50+
51+
btn.addEventListener('click', () => {
52+
const clerk = window.Clerk;
53+
54+
// Authentication checks
55+
if (!clerk.user) {
56+
throw new Error('Ensure that `<SubscriptionDetailsButton />` is rendered inside a `<SignedIn />` component.');
57+
}
58+
59+
if (!clerk.organization && subscriptionDetailsOptions.for === 'organization') {
60+
throw new Error(
61+
'Wrap `<SubscriptionDetailsButton for="organization" />` with a check for an active organization.',
62+
);
63+
}
64+
if (subscriptionDetailsOptions.clickIdentifier) {
65+
const clickEvent = new CustomEvent('clerk:subscription-cancel', {
66+
detail: subscriptionDetailsOptions.clickIdentifier,
67+
});
68+
subscriptionDetailsOptions.onSubscriptionCancel = () => {
69+
document.dispatchEvent(clickEvent);
70+
};
71+
}
72+
73+
return clerk.__internal_openSubscriptionDetails(subscriptionDetailsOptions);
74+
});
75+
</script>
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import type { __experimental_CheckoutButtonProps } from '@clerk/types';
2+
import React from 'react';
3+
4+
import type { WithClerkProp } from './utils';
5+
import { assertSingleChild, normalizeWithDefaultValue, safeExecute, withClerk } from './utils';
6+
7+
export type { __experimental_CheckoutButtonProps as CheckoutButtonProps };
8+
9+
export const CheckoutButton = withClerk(
10+
({ clerk, children, ...props }: WithClerkProp<React.PropsWithChildren<__experimental_CheckoutButtonProps>>) => {
11+
const {
12+
planId,
13+
planPeriod,
14+
for: _for,
15+
onSubscriptionComplete,
16+
newSubscriptionRedirectUrl,
17+
checkoutProps,
18+
...rest
19+
} = props;
20+
21+
// Note: Auth checks are moved to runtime since Astro React components
22+
// don't have access to auth context at render time like Vue/React apps do
23+
24+
children = normalizeWithDefaultValue(children, 'Checkout');
25+
const child = assertSingleChild(children)('CheckoutButton');
26+
27+
const clickHandler = () => {
28+
if (!clerk) {
29+
return;
30+
}
31+
32+
return clerk.__internal_openCheckout({
33+
planId,
34+
planPeriod,
35+
for: _for,
36+
onSubscriptionComplete,
37+
newSubscriptionRedirectUrl,
38+
...checkoutProps,
39+
});
40+
};
41+
42+
const wrappedChildClickHandler: React.MouseEventHandler = e => {
43+
if (child && typeof child === 'object' && 'props' in child) {
44+
void safeExecute(child.props.onClick)(e);
45+
}
46+
return clickHandler();
47+
};
48+
49+
const childProps = { ...rest, onClick: wrappedChildClickHandler };
50+
return React.cloneElement(child as React.ReactElement<unknown>, childProps);
51+
},
52+
'CheckoutButton',
53+
);
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { __experimental_PlanDetailsButtonProps } from '@clerk/types';
2+
import React from 'react';
3+
4+
import type { WithClerkProp } from './utils';
5+
import { assertSingleChild, normalizeWithDefaultValue, safeExecute, withClerk } from './utils';
6+
7+
export type { __experimental_PlanDetailsButtonProps as PlanDetailsButtonProps };
8+
9+
export const PlanDetailsButton = withClerk(
10+
({ clerk, children, ...props }: WithClerkProp<React.PropsWithChildren<__experimental_PlanDetailsButtonProps>>) => {
11+
const { plan, planId, initialPlanPeriod, planDetailsProps, ...rest } = props;
12+
13+
children = normalizeWithDefaultValue(children, 'Plan details');
14+
const child = assertSingleChild(children)('PlanDetailsButton');
15+
16+
const clickHandler = () => {
17+
if (!clerk) {
18+
return;
19+
}
20+
21+
return clerk.__internal_openPlanDetails({
22+
plan,
23+
planId,
24+
initialPlanPeriod,
25+
...planDetailsProps,
26+
} as __experimental_PlanDetailsButtonProps);
27+
};
28+
29+
const wrappedChildClickHandler: React.MouseEventHandler = e => {
30+
if (child && typeof child === 'object' && 'props' in child) {
31+
void safeExecute(child.props.onClick)(e);
32+
}
33+
return clickHandler();
34+
};
35+
36+
const childProps = { ...rest, onClick: wrappedChildClickHandler };
37+
return React.cloneElement(child as React.ReactElement<unknown>, childProps);
38+
},
39+
'PlanDetailsButton',
40+
);

0 commit comments

Comments
 (0)