Skip to content

Commit bebb6d8

Browse files
feat(clerk-js,types,clerk-react): Add internal open/close checkout methods (#5481)
1 parent e1ec52b commit bebb6d8

File tree

14 files changed

+386
-263
lines changed

14 files changed

+386
-263
lines changed

.changeset/fancy-lies-sell.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/clerk-react': patch
4+
'@clerk/types': patch
5+
---
6+
7+
Introduce `clerk.__internal_openCheckout()` and `clerk.__internal_closeCheckout()` methods and remove `<Checkout />` from within the `<PricingTable />` component.

packages/clerk-js/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
{ "path": "./dist/clerk.browser.js", "maxSize": "81KB" },
55
{ "path": "./dist/clerk.headless*.js", "maxSize": "55KB" },
66
{ "path": "./dist/ui-common*.js", "maxSize": "96KB" },
7-
{ "path": "./dist/vendors*.js", "maxSize": "30KB" },
7+
{ "path": "./dist/vendors*.js", "maxSize": "35KB" },
88
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
99
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
1010
{ "path": "./dist/impersonationfab*.js", "maxSize": "5KB" },

packages/clerk-js/src/core/clerk.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import { addClerkPrefix, isAbsoluteUrl, stripScheme } from '@clerk/shared/url';
1414
import { allSettled, handleValueOrFn, noop } from '@clerk/shared/utils';
1515
import type {
16+
__experimental_CheckoutProps,
1617
__experimental_CommerceNamespace,
1718
__experimental_PricingTableProps,
1819
__internal_ComponentNavigationContext,
@@ -520,6 +521,18 @@ export class Clerk implements ClerkInterface {
520521
void this.#componentControls.ensureMounted().then(controls => controls.closeModal('signIn'));
521522
};
522523

524+
public __internal_openCheckout = (props?: __experimental_CheckoutProps): void => {
525+
this.assertComponentsReady(this.#componentControls);
526+
void this.#componentControls
527+
.ensureMounted({ preloadHint: 'Checkout' })
528+
.then(controls => controls.openDrawer('checkout', props || {}));
529+
};
530+
531+
public __internal_closeCheckout = (): void => {
532+
this.assertComponentsReady(this.#componentControls);
533+
void this.#componentControls.ensureMounted().then(controls => controls.closeDrawer('checkout'));
534+
};
535+
523536
public __internal_openReverification = (props?: __internal_UserVerificationModalProps): void => {
524537
this.assertComponentsReady(this.#componentControls);
525538
if (noUserExists(this)) {

packages/clerk-js/src/ui/Components.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useSafeLayoutEffect } from '@clerk/shared/react';
22
import { createDeferredPromise } from '@clerk/shared/utils';
33
import type {
4+
__experimental_CheckoutProps,
45
__internal_UserVerificationProps,
56
Appearance,
67
Clerk,
@@ -24,6 +25,7 @@ import { useClerkModalStateParams } from './hooks/useClerkModalStateParams';
2425
import type { ClerkComponentName } from './lazyModules/components';
2526
import {
2627
BlankCaptchaModal,
28+
Checkout,
2729
CreateOrganizationModal,
2830
ImpersonationFab,
2931
KeylessPrompt,
@@ -37,6 +39,7 @@ import {
3739
} from './lazyModules/components';
3840
import {
3941
LazyComponentRenderer,
42+
LazyDrawerRenderer,
4043
LazyImpersonationFabProvider,
4144
LazyModalRenderer,
4245
LazyOneTapRenderer,
@@ -99,6 +102,16 @@ export type ComponentControls = {
99102
notify?: boolean;
100103
},
101104
) => void;
105+
openDrawer: <T extends 'checkout'>(
106+
drawer: T,
107+
props: T extends 'checkout' ? __experimental_CheckoutProps : never,
108+
) => void;
109+
closeDrawer: (
110+
drawer: 'checkout',
111+
options?: {
112+
notify?: boolean;
113+
},
114+
) => void;
102115
prefetch: (component: 'organizationSwitcher') => void;
103116
// Special case, as the impersonation fab mounts automatically
104117
mountImpersonationFab: () => void;
@@ -131,6 +144,10 @@ interface ComponentsState {
131144
blankCaptchaModal: null;
132145
organizationSwitcherPrefetch: boolean;
133146
waitlistModal: null | WaitlistProps;
147+
checkoutDrawer: {
148+
open: false;
149+
props: null | __experimental_CheckoutProps;
150+
};
134151
nodes: Map<HTMLDivElement, HtmlNodeOptions>;
135152
impersonationFab: boolean;
136153
}
@@ -212,6 +229,10 @@ const Components = (props: ComponentsProps) => {
212229
organizationSwitcherPrefetch: false,
213230
waitlistModal: null,
214231
blankCaptchaModal: null,
232+
checkoutDrawer: {
233+
open: false,
234+
props: null,
235+
},
215236
nodes: new Map(),
216237
impersonationFab: false,
217238
});
@@ -226,6 +247,7 @@ const Components = (props: ComponentsProps) => {
226247
createOrganizationModal,
227248
waitlistModal,
228249
blankCaptchaModal,
250+
checkoutDrawer,
229251
nodes,
230252
} = state;
231253

@@ -322,6 +344,26 @@ const Components = (props: ComponentsProps) => {
322344
setState(s => ({ ...s, impersonationFab: true }));
323345
};
324346

347+
componentsControls.openDrawer = (name, props) => {
348+
setState(s => ({
349+
...s,
350+
[`${name}Drawer`]: {
351+
open: true,
352+
props,
353+
},
354+
}));
355+
};
356+
357+
componentsControls.closeDrawer = name => {
358+
setState(s => ({
359+
...s,
360+
[`${name}Drawer`]: {
361+
...s[`${name}Drawer`],
362+
open: false,
363+
},
364+
}));
365+
};
366+
325367
componentsControls.prefetch = component => {
326368
setState(s => ({ ...s, [`${component}Prefetch`]: true }));
327369
};
@@ -481,6 +523,26 @@ const Components = (props: ComponentsProps) => {
481523
</LazyModalRenderer>
482524
);
483525

526+
const mountedCheckoutDrawer = checkoutDrawer.props && (
527+
<LazyDrawerRenderer
528+
globalAppearance={state.appearance}
529+
appearanceKey={'checkout' as any}
530+
componentAppearance={{}}
531+
flowName={'checkout'}
532+
open={checkoutDrawer.open}
533+
onOpenChange={() => componentsControls.closeDrawer('checkout')}
534+
componentName={'Checkout'}
535+
portalId={checkoutDrawer.props.portalId}
536+
>
537+
<Checkout
538+
planId={checkoutDrawer.props.planId}
539+
planPeriod={checkoutDrawer.props.planPeriod}
540+
orgId={checkoutDrawer.props.orgId}
541+
onSubscriptionComplete={checkoutDrawer.props.onSubscriptionComplete}
542+
/>
543+
</LazyDrawerRenderer>
544+
);
545+
484546
return (
485547
<Suspense fallback={''}>
486548
<LazyProviders
@@ -511,6 +573,7 @@ const Components = (props: ComponentsProps) => {
511573
{createOrganizationModal && mountedCreateOrganizationModal}
512574
{waitlistModal && mountedWaitlistModal}
513575
{blankCaptchaModal && mountedBlankCaptchaModal}
576+
{mountedCheckoutDrawer}
514577

515578
{state.impersonationFab && (
516579
<LazyImpersonationFabProvider globalAppearance={state.appearance}>
Lines changed: 11 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,25 @@
11
import type { __experimental_CheckoutProps } from '@clerk/types';
22

3-
import { PROFILE_CARD_SCROLLBOX_ID } from '../../constants';
4-
import { useCheckoutContext, withCoreUserGuard } from '../../contexts';
3+
import { __experimental_CheckoutContext } from '../../contexts';
54
import { Flow } from '../../customizables';
65
import { Drawer } from '../../elements';
7-
import { Route, Switch } from '../../router';
86
import { CheckoutPage } from './CheckoutPage';
97

108
export const __experimental_Checkout = (props: __experimental_CheckoutProps) => {
119
return (
1210
<Flow.Root flow='checkout'>
1311
<Flow.Part>
14-
<Switch>
15-
<Route>
16-
<AuthenticatedRoutes {...props} />
17-
</Route>
18-
</Switch>
12+
<__experimental_CheckoutContext.Provider
13+
value={{
14+
componentName: 'Checkout',
15+
}}
16+
>
17+
<Drawer.Content>
18+
<Drawer.Header title='Checkout' />
19+
<CheckoutPage {...props} />
20+
</Drawer.Content>
21+
</__experimental_CheckoutContext.Provider>
1922
</Flow.Part>
2023
</Flow.Root>
2124
);
2225
};
23-
24-
const AuthenticatedRoutes = withCoreUserGuard((props: __experimental_CheckoutProps) => {
25-
const { mode = 'mounted', isOpen = false, setIsOpen = () => {} } = useCheckoutContext();
26-
27-
return (
28-
<Drawer.Root
29-
open={isOpen}
30-
onOpenChange={setIsOpen}
31-
strategy={mode === 'mounted' ? 'fixed' : 'absolute'}
32-
portalProps={{
33-
id: mode === 'modal' ? PROFILE_CARD_SCROLLBOX_ID : undefined,
34-
}}
35-
>
36-
<Drawer.Overlay />
37-
<Drawer.Content>
38-
<Drawer.Header title='Checkout' />
39-
<CheckoutPage {...props} />
40-
</Drawer.Content>
41-
</Drawer.Root>
42-
);
43-
});

packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import type { __experimental_CommerceCheckoutResource } from '@clerk/types';
22

3-
import { useCheckoutContext } from '../../contexts';
43
import { Box, Button, descriptors, Heading, Icon, localizationKeys, Span, Text } from '../../customizables';
5-
import { Drawer, LineItems } from '../../elements';
4+
import { Drawer, LineItems, useDrawerContext } from '../../elements';
65
import { Check } from '../../icons';
76

87
const capitalize = (name: string) => name[0].toUpperCase() + name.slice(1);
98

109
export const CheckoutComplete = ({ checkout }: { checkout: __experimental_CommerceCheckoutResource }) => {
11-
const { setIsOpen } = useCheckoutContext();
10+
const { setIsOpen } = useDrawerContext();
1211

1312
const handleClose = () => {
1413
if (setIsOpen) {

packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@ import type {
88
import { useState } from 'react';
99

1010
import { PROFILE_CARD_SCROLLBOX_ID } from '../../constants';
11-
import { __experimental_CheckoutContext, usePricingTableContext } from '../../contexts';
11+
import { usePricingTableContext } from '../../contexts';
1212
import { AppearanceProvider } from '../../customizables';
1313
import { usePlans } from '../../hooks';
14-
import { __experimental_Checkout } from '../Checkout';
1514
import { PricingTableDefault } from './PricingTableDefault';
1615
import { PricingTableMatrix } from './PricingTableMatrix';
1716
import { SubscriptionDetailDrawer } from './SubscriptionDetailDrawer';
@@ -25,10 +24,8 @@ export const __experimental_PricingTable = (props: __experimental_PricingTablePr
2524
const { plans, subscriptions, revalidate } = usePlans({ subscriberType });
2625

2726
const [planPeriod, setPlanPeriod] = useState<__experimental_CommerceSubscriptionPlanPeriod>('month');
28-
const [checkoutPlan, setCheckoutPlan] = useState<__experimental_CommercePlanResource>();
2927
const [detailSubscription, setDetailSubscription] = useState<__experimental_CommerceSubscriptionResource>();
3028

31-
const [showCheckout, setShowCheckout] = useState(false);
3229
const [showSubscriptionDetailDrawer, setShowSubscriptionDetailDrawer] = useState(false);
3330

3431
const selectPlan = (plan: __experimental_CommercePlanResource) => {
@@ -40,8 +37,13 @@ export const __experimental_PricingTable = (props: __experimental_PricingTablePr
4037
setDetailSubscription(activeSubscription);
4138
setShowSubscriptionDetailDrawer(true);
4239
} else {
43-
setCheckoutPlan(plan);
44-
setShowCheckout(true);
40+
clerk.__internal_openCheckout({
41+
planId: plan.id,
42+
planPeriod,
43+
orgId: subscriberType === 'org' ? organization?.id : undefined,
44+
onSubscriptionComplete: onSubscriptionChange,
45+
portalId: mode === 'modal' ? PROFILE_CARD_SCROLLBOX_ID : undefined,
46+
});
4547
}
4648
};
4749

@@ -74,26 +76,6 @@ export const __experimental_PricingTable = (props: __experimental_PricingTablePr
7476
appearanceKey='checkout'
7577
appearance={props.checkoutProps?.appearance}
7678
>
77-
<__experimental_CheckoutContext.Provider
78-
value={{
79-
componentName: 'Checkout',
80-
mode,
81-
isOpen: showCheckout,
82-
setIsOpen: setShowCheckout,
83-
}}
84-
>
85-
{/*TODO: Used by InvisibleRootBox, can we simplify? */}
86-
<div>
87-
{checkoutPlan && (
88-
<__experimental_Checkout
89-
planPeriod={planPeriod}
90-
planId={checkoutPlan.id}
91-
orgId={subscriberType === 'org' ? organization?.id : undefined}
92-
onSubscriptionComplete={onSubscriptionChange}
93-
/>
94-
)}
95-
</div>
96-
</__experimental_CheckoutContext.Provider>
9779
<SubscriptionDetailDrawer
9880
isOpen={showSubscriptionDetailDrawer}
9981
setIsOpen={setShowSubscriptionDetailDrawer}

0 commit comments

Comments
 (0)