diff --git a/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx b/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx
index 7d4e4bcb933..dd869dbc7d0 100644
--- a/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx
+++ b/packages/clerk-js/src/ui/components/Checkout/Checkout.tsx
@@ -1,43 +1,25 @@
import type { __experimental_CheckoutProps } from '@clerk/types';
-import { PROFILE_CARD_SCROLLBOX_ID } from '../../constants';
-import { useCheckoutContext, withCoreUserGuard } from '../../contexts';
+import { __experimental_CheckoutContext } from '../../contexts';
import { Flow } from '../../customizables';
import { Drawer } from '../../elements';
-import { Route, Switch } from '../../router';
import { CheckoutPage } from './CheckoutPage';
export const __experimental_Checkout = (props: __experimental_CheckoutProps) => {
return (
-
-
-
-
-
+ <__experimental_CheckoutContext.Provider
+ value={{
+ componentName: 'Checkout',
+ }}
+ >
+
+
+
+
+
);
};
-
-const AuthenticatedRoutes = withCoreUserGuard((props: __experimental_CheckoutProps) => {
- const { mode = 'mounted', isOpen = false, setIsOpen = () => {} } = useCheckoutContext();
-
- return (
-
-
-
-
-
-
-
- );
-});
diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx
index b75f027e55f..437cd1e21be 100644
--- a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx
+++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx
@@ -1,14 +1,13 @@
import type { __experimental_CommerceCheckoutResource } from '@clerk/types';
-import { useCheckoutContext } from '../../contexts';
import { Box, Button, descriptors, Heading, Icon, localizationKeys, Span, Text } from '../../customizables';
-import { Drawer, LineItems } from '../../elements';
+import { Drawer, LineItems, useDrawerContext } from '../../elements';
import { Check } from '../../icons';
const capitalize = (name: string) => name[0].toUpperCase() + name.slice(1);
export const CheckoutComplete = ({ checkout }: { checkout: __experimental_CommerceCheckoutResource }) => {
- const { setIsOpen } = useCheckoutContext();
+ const { setIsOpen } = useDrawerContext();
const handleClose = () => {
if (setIsOpen) {
diff --git a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx
index 016738c435c..58d37abf5cc 100644
--- a/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx
+++ b/packages/clerk-js/src/ui/components/PricingTable/PricingTable.tsx
@@ -8,10 +8,9 @@ import type {
import { useState } from 'react';
import { PROFILE_CARD_SCROLLBOX_ID } from '../../constants';
-import { __experimental_CheckoutContext, usePricingTableContext } from '../../contexts';
+import { usePricingTableContext } from '../../contexts';
import { AppearanceProvider } from '../../customizables';
import { usePlans } from '../../hooks';
-import { __experimental_Checkout } from '../Checkout';
import { PricingTableDefault } from './PricingTableDefault';
import { PricingTableMatrix } from './PricingTableMatrix';
import { SubscriptionDetailDrawer } from './SubscriptionDetailDrawer';
@@ -25,10 +24,8 @@ export const __experimental_PricingTable = (props: __experimental_PricingTablePr
const { plans, subscriptions, revalidate } = usePlans({ subscriberType });
const [planPeriod, setPlanPeriod] = useState<__experimental_CommerceSubscriptionPlanPeriod>('month');
- const [checkoutPlan, setCheckoutPlan] = useState<__experimental_CommercePlanResource>();
const [detailSubscription, setDetailSubscription] = useState<__experimental_CommerceSubscriptionResource>();
- const [showCheckout, setShowCheckout] = useState(false);
const [showSubscriptionDetailDrawer, setShowSubscriptionDetailDrawer] = useState(false);
const selectPlan = (plan: __experimental_CommercePlanResource) => {
@@ -40,8 +37,13 @@ export const __experimental_PricingTable = (props: __experimental_PricingTablePr
setDetailSubscription(activeSubscription);
setShowSubscriptionDetailDrawer(true);
} else {
- setCheckoutPlan(plan);
- setShowCheckout(true);
+ clerk.__internal_openCheckout({
+ planId: plan.id,
+ planPeriod,
+ orgId: subscriberType === 'org' ? organization?.id : undefined,
+ onSubscriptionComplete: onSubscriptionChange,
+ portalId: mode === 'modal' ? PROFILE_CARD_SCROLLBOX_ID : undefined,
+ });
}
};
@@ -74,26 +76,6 @@ export const __experimental_PricingTable = (props: __experimental_PricingTablePr
appearanceKey='checkout'
appearance={props.checkoutProps?.appearance}
>
- <__experimental_CheckoutContext.Provider
- value={{
- componentName: 'Checkout',
- mode,
- isOpen: showCheckout,
- setIsOpen: setShowCheckout,
- }}
- >
- {/*TODO: Used by InvisibleRootBox, can we simplify? */}
-
- {checkoutPlan && (
- <__experimental_Checkout
- planPeriod={planPeriod}
- planId={checkoutPlan.id}
- orgId={subscriberType === 'org' ? organization?.id : undefined}
- onSubscriptionComplete={onSubscriptionChange}
- />
- )}
-
-
;
@@ -71,164 +72,166 @@ export function SubscriptionDetailDrawer({
};
return (
-
-
-
-
- !hasFeatures
- ? {
- flex: 1,
- borderBottomWidth: 0,
- background: t.colors.$colorBackground,
- }
- : null
- }
- >
- }
- />
-
+
+
+
+
+
+ !hasFeatures
+ ? {
+ flex: 1,
+ borderBottomWidth: 0,
+ background: t.colors.$colorBackground,
+ }
+ : null
+ }
+ >
+ }
+ />
+
- {hasFeatures ? (
-
- ({
- display: 'grid',
- rowGap: t.space.$4,
- padding: t.space.$4,
- })}
- >
- {features.map(feature => (
- ({
- display: 'flex',
- alignItems: 'baseline',
- gap: t.space.$3,
- })}
- >
- {feature.avatarUrl ? (
- 24}
- title={feature.name}
- initials={feature.name[0]}
- rounded={false}
- imageUrl={feature.avatarUrl}
- />
- ) : null}
-
- ({
- fontWeight: t.fontWeights.$medium,
- })}
- >
- {feature.name}
-
- {feature.description ? (
+ {hasFeatures ? (
+
+ ({
+ display: 'grid',
+ rowGap: t.space.$4,
+ padding: t.space.$4,
+ })}
+ >
+ {features.map(feature => (
+ ({
+ display: 'flex',
+ alignItems: 'baseline',
+ gap: t.space.$3,
+ })}
+ >
+ {feature.avatarUrl ? (
+ 24}
+ title={feature.name}
+ initials={feature.name[0]}
+ rounded={false}
+ imageUrl={feature.avatarUrl}
+ />
+ ) : null}
+
({
- marginBlockStart: t.space.$0x25,
- fontSize: t.fontSizes.$xs,
+ fontWeight: t.fontWeights.$medium,
})}
>
- {feature.description}
+ {feature.name}
- ) : null}
-
-
- ))}
-
-
- ) : null}
+ {feature.description ? (
+ ({
+ marginBlockStart: t.space.$0x25,
+ fontSize: t.fontSizes.$xs,
+ })}
+ >
+ {feature.description}
+
+ ) : null}
+
+
+ ))}
+
+
+ ) : null}
-
-
-
+
+
+
-
- {!isSubmitting && (
+
+ {!isSubmitting && (
+
+ )}
- )}
-
- >
- }
- >
-
+ }
>
- {/* TODO(@COMMERCE): needs localization */}
- Cancel {subscription.plan.name} Subscription?
-
-
- {/* TODO(@COMMERCE): needs localization */}
- You can keep using “{subscription.plan.name}” features until [DATE], after which you will no
- longer have access.
-
- {cancelError && (
- // TODO(@COMMERCE): needs localization
- {typeof cancelError === 'string' ? cancelError : cancelError.message}
- )}
-
-
-
+
+ {/* TODO(@COMMERCE): needs localization */}
+ Cancel {subscription.plan.name} Subscription?
+
+
+ {/* TODO(@COMMERCE): needs localization */}
+ You can keep using “{subscription.plan.name}” features until [DATE], after which you will no
+ longer have access.
+
+ {cancelError && (
+ // TODO(@COMMERCE): needs localization
+ {typeof cancelError === 'string' ? cancelError : cancelError.message}
+ )}
+
+
+
+
);
}
diff --git a/packages/clerk-js/src/ui/elements/Drawer.tsx b/packages/clerk-js/src/ui/elements/Drawer.tsx
index b7552cdf102..32f0f88b115 100644
--- a/packages/clerk-js/src/ui/elements/Drawer.tsx
+++ b/packages/clerk-js/src/ui/elements/Drawer.tsx
@@ -20,7 +20,7 @@ import { usePrefersReducedMotion } from '../hooks';
import { useScrollLock } from '../hooks/useScrollLock';
import { Close as CloseIcon } from '../icons';
import type { ThemableCssProp } from '../styledSystem';
-import { common, InternalThemeProvider } from '../styledSystem';
+import { common } from '../styledSystem';
import { colors } from '../utils';
import { IconButton } from './IconButton';
@@ -102,21 +102,19 @@ function Root({
]);
return (
-
-
- {children}
-
-
+
+ {children}
+
);
}
@@ -196,7 +194,7 @@ const Content = React.forwardRef(({ children }, re
const prefersReducedMotion = usePrefersReducedMotion();
const { animations: layoutAnimations } = useAppearance().parsedLayout;
const isMotionSafe = !prefersReducedMotion && layoutAnimations === true;
- const { strategy, portalProps, refs, context, getFloatingProps } = useDrawerContext();
+ const { strategy, refs, context, getFloatingProps } = useDrawerContext();
const mergedRefs = useMergeRefs([ref, refs.setFloating]);
const { isMounted, styles: transitionStyles } = useTransitionStyles(context, {
@@ -213,47 +211,45 @@ const Content = React.forwardRef(({ children }, re
if (!isMounted) return null;
return (
-
-
+ ({
+ // Apply the conditional right offset + the spread of the
+ // box shadow to ensure it is fully offscreen before unmounting
+ '--transform-offset':
+ strategy === 'fixed' ? `calc(100% + ${t.space.$3} + ${t.space.$8x75})` : `calc(100% + ${t.space.$8x75})`,
+ willChange: 'transform',
+ position: strategy,
+ insetBlock: strategy === 'fixed' ? t.space.$3 : 0,
+ insetInlineEnd: strategy === 'fixed' ? t.space.$3 : 0,
+ outline: 0,
+ width: t.sizes.$100,
+ backgroundColor: t.colors.$colorBackground,
+ borderStartStartRadius: t.radii.$xl,
+ borderEndStartRadius: t.radii.$xl,
+ borderEndEndRadius: strategy === 'fixed' ? t.radii.$xl : 0,
+ borderStartEndRadius: strategy === 'fixed' ? t.radii.$xl : 0,
+ borderWidth: t.borderWidths.$normal,
+ borderStyle: t.borderStyles.$solid,
+ borderColor: t.colors.$neutralAlpha100,
+ boxShadow: t.shadows.$cardBoxShadow,
+ overflow: 'hidden',
+ zIndex: t.zIndices.$modal,
+ })}
>
- ({
- // Apply the conditional right offset + the spread of the
- // box shadow to ensure it is fully offscreen before unmounting
- '--transform-offset':
- strategy === 'fixed' ? `calc(100% + ${t.space.$3} + ${t.space.$8x75})` : `calc(100% + ${t.space.$8x75})`,
- willChange: 'transform',
- position: strategy,
- insetBlock: strategy === 'fixed' ? t.space.$3 : 0,
- insetInlineEnd: strategy === 'fixed' ? t.space.$3 : 0,
- outline: 0,
- width: t.sizes.$100,
- backgroundColor: t.colors.$colorBackground,
- borderStartStartRadius: t.radii.$xl,
- borderEndStartRadius: t.radii.$xl,
- borderEndEndRadius: strategy === 'fixed' ? t.radii.$xl : 0,
- borderStartEndRadius: strategy === 'fixed' ? t.radii.$xl : 0,
- borderWidth: t.borderWidths.$normal,
- borderStyle: t.borderStyles.$solid,
- borderColor: t.colors.$neutralAlpha100,
- boxShadow: t.shadows.$cardBoxShadow,
- overflow: 'hidden',
- zIndex: t.zIndices.$modal,
- })}
- >
- {children}
-
-
-
+ {children}
+
+
);
});
diff --git a/packages/clerk-js/src/ui/lazyModules/components.ts b/packages/clerk-js/src/ui/lazyModules/components.ts
index 4ce33dc78c6..b26c3f33343 100644
--- a/packages/clerk-js/src/ui/lazyModules/components.ts
+++ b/packages/clerk-js/src/ui/lazyModules/components.ts
@@ -95,6 +95,10 @@ export const PricingTable = lazy(() =>
componentImportPaths.PricingTable().then(module => ({ default: module.__experimental_PricingTable })),
);
+export const Checkout = lazy(() =>
+ componentImportPaths.Checkout().then(module => ({ default: module.__experimental_Checkout })),
+);
+
export const SessionTasks = lazy(() =>
componentImportPaths.SessionTasks().then(module => ({ default: module.SessionTask })),
);
@@ -124,6 +128,7 @@ export const ClerkComponents = {
WaitlistModal,
BlankCaptchaModal,
PricingTable,
+ Checkout,
};
export type ClerkComponentName = keyof typeof ClerkComponents;
diff --git a/packages/clerk-js/src/ui/lazyModules/providers.tsx b/packages/clerk-js/src/ui/lazyModules/providers.tsx
index 61162e7ee32..e99d2542a58 100644
--- a/packages/clerk-js/src/ui/lazyModules/providers.tsx
+++ b/packages/clerk-js/src/ui/lazyModules/providers.tsx
@@ -2,7 +2,7 @@ import { deprecated } from '@clerk/shared/deprecated';
import type { Appearance } from '@clerk/types';
import React, { lazy, Suspense } from 'react';
-import type { FlowMetadata } from '../elements';
+import type { Drawer, FlowMetadata } from '../elements';
import type { ThemableCssProp } from '../styledSystem';
import type { ClerkComponentName } from './components';
import { ClerkComponents } from './components';
@@ -20,6 +20,8 @@ const Portal = lazy(() => import('./../portal').then(m => ({ default: m.Portal }
const VirtualBodyRootPortal = lazy(() => import('./../portal').then(m => ({ default: m.VirtualBodyRootPortal })));
const FlowMetadataProvider = lazy(() => import('./../elements').then(m => ({ default: m.FlowMetadataProvider })));
const Modal = lazy(() => import('./../elements').then(m => ({ default: m.Modal })));
+const DrawerRoot = lazy(() => import('./../elements').then(m => ({ default: m.Drawer.Root })));
+const DrawerOverlay = lazy(() => import('./../elements').then(m => ({ default: m.Drawer.Overlay })));
const OrganizationSwitcherPrefetch = lazy(() =>
import(/* webpackChunkName: "prefetchorganizationlist" */ '../components/prefetch-organization-list').then(m => ({
default: m.OrganizationSwitcherPrefetch,
@@ -131,6 +133,46 @@ export const LazyModalRenderer = (props: LazyModalRendererProps) => {
);
};
+type DrawerProps = Parameters[0];
+
+type LazyDrawerRendererProps = React.PropsWithChildren<
+ {
+ componentName: ClerkComponentName;
+ flowName?: FlowMetadata['flow'];
+ open: DrawerProps['open'];
+ onOpenChange: DrawerProps['onOpenChange'];
+ portalId?: string;
+ } & AppearanceProviderProps
+>;
+
+export const LazyDrawerRenderer = (props: LazyDrawerRendererProps) => {
+ return (
+
+
+
+
+
+
+ {props.children}
+
+
+
+
+
+ );
+};
+
/**
* This component automatically mounts when impersonating, without a user action.
* We want to hotload the /ui dependencies only if we're actually impersonating.
diff --git a/packages/clerk-js/src/ui/types.ts b/packages/clerk-js/src/ui/types.ts
index b915f4240d1..3e65b2d8acd 100644
--- a/packages/clerk-js/src/ui/types.ts
+++ b/packages/clerk-js/src/ui/types.ts
@@ -109,9 +109,6 @@ export type __experimental_PricingTableCtx = __experimental_PricingTableProps &
export type __experimental_CheckoutCtx = __experimental_CheckoutProps & {
componentName: 'Checkout';
- mode?: ComponentMode;
- isOpen?: boolean;
- setIsOpen?: (open: boolean) => void;
};
export type SessionTasksCtx = {
diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts
index e74152e7000..0c5ce92ceb6 100644
--- a/packages/react/src/isomorphicClerk.ts
+++ b/packages/react/src/isomorphicClerk.ts
@@ -2,6 +2,7 @@ import { inBrowser } from '@clerk/shared/browser';
import { loadClerkJsScript } from '@clerk/shared/loadClerkJsScript';
import { handleValueOrFn } from '@clerk/shared/utils';
import type {
+ __experimental_CheckoutProps,
__experimental_CommerceNamespace,
__experimental_PricingTableProps,
__internal_UserVerificationModalProps,
@@ -109,6 +110,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
private preopenOneTap?: null | GoogleOneTapProps = null;
private preopenUserVerification?: null | __internal_UserVerificationProps = null;
private preopenSignIn?: null | SignInProps = null;
+ private preopenCheckout?: null | __experimental_CheckoutProps = null;
private preopenSignUp?: null | SignUpProps = null;
private preopenUserProfile?: null | UserProfileProps = null;
private preopenOrganizationProfile?: null | OrganizationProfileProps = null;
@@ -472,6 +474,10 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
clerkjs.openSignIn(this.preopenSignIn);
}
+ if (this.preopenCheckout !== null) {
+ clerkjs.__internal_openCheckout(this.preopenCheckout);
+ }
+
if (this.preopenSignUp !== null) {
clerkjs.openSignUp(this.preopenSignUp);
}
@@ -650,6 +656,22 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk {
}
};
+ __internal_openCheckout = (props?: __experimental_CheckoutProps) => {
+ if (this.clerkjs && this.#loaded) {
+ this.clerkjs.__internal_openCheckout(props);
+ } else {
+ this.preopenCheckout = props;
+ }
+ };
+
+ __internal_closeCheckout = () => {
+ if (this.clerkjs && this.#loaded) {
+ this.clerkjs.__internal_closeCheckout();
+ } else {
+ this.preopenCheckout = null;
+ }
+ };
+
__internal_openReverification = (props?: __internal_UserVerificationModalProps) => {
if (this.clerkjs && this.#loaded) {
this.clerkjs.__internal_openReverification(props);
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index e4f80529c48..444f5f40853 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -178,6 +178,17 @@ export interface Clerk {
*/
closeSignIn: () => void;
+ /**
+ * Opens the Clerk Checkout component in a drawer.
+ * @param props Optional checkout configuration parameters.
+ */
+ __internal_openCheckout: (props?: __experimental_CheckoutProps) => void;
+
+ /**
+ * Closes the Clerk Checkout drawer.
+ */
+ __internal_closeCheckout: () => void;
+
/**
* Opens the Clerk UserVerification component in a modal.
* @param props Optional user verification configuration parameters.
@@ -1515,6 +1526,7 @@ export type __experimental_CheckoutProps = {
planPeriod?: __experimental_CommerceSubscriptionPlanPeriod;
orgId?: string;
onSubscriptionComplete?: () => void;
+ portalId?: string;
};
export type __experimental_PaymentSourcesProps = {