diff --git a/.changeset/forty-ladybugs-tie.md b/.changeset/forty-ladybugs-tie.md
new file mode 100644
index 00000000000..19092ffee6b
--- /dev/null
+++ b/.changeset/forty-ladybugs-tie.md
@@ -0,0 +1,6 @@
+---
+'@clerk/clerk-js': patch
+'@clerk/types': patch
+---
+
+Introduce `` component to be used within Commerce cancel subscription flow.
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index 50d50a6ce75..9f3ef3f9d76 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -1,9 +1,9 @@
{
"files": [
- { "path": "./dist/clerk.js", "maxSize": "575kB" },
+ { "path": "./dist/clerk.js", "maxSize": "576kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "78kB" },
{ "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
- { "path": "./dist/ui-common*.js", "maxSize": "93KB" },
+ { "path": "./dist/ui-common*.js", "maxSize": "94KB" },
{ "path": "./dist/vendors*.js", "maxSize": "30KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
diff --git a/packages/clerk-js/src/ui/components/PricingTable/PlanDetailDrawer.tsx b/packages/clerk-js/src/ui/components/PricingTable/PlanDetailDrawer.tsx
index abf44033e4f..258c7c7eb57 100644
--- a/packages/clerk-js/src/ui/components/PricingTable/PlanDetailDrawer.tsx
+++ b/packages/clerk-js/src/ui/components/PricingTable/PlanDetailDrawer.tsx
@@ -1,8 +1,8 @@
import type { __experimental_CommercePlanResource } from '@clerk/types';
import * as React from 'react';
-import { Alert, Box, Button, Col, Flex, Heading, Text } from '../../customizables';
-import { Drawer } from '../../elements';
+import { Box, Button, descriptors, Heading, Text } from '../../customizables';
+import { Alert, Drawer } from '../../elements';
import type { PlanPeriod } from './PlanCard';
import { PlanCardFeaturesList, PlanCardHeader } from './PlanCard';
@@ -27,10 +27,26 @@ export function PlanDetailDrawer({
planPeriod,
setPlanPeriod,
}: PlanDetailDrawerProps) {
+ const [showConfirmation, setShowConfirmation] = React.useState(false);
+ const [isSubmitting, setIsSubmitting] = React.useState(false);
+ const [hasError, setHasError] = React.useState(false);
if (!plan) {
return null;
}
const hasFeatures = plan.features.length > 0;
+ const cancelSubscription = async () => {
+ setHasError(false);
+ setIsSubmitting(true);
+
+ // TODO(@COMMERCE): we need to get a handle on the subscription object in order to cancel it,
+ // but this method doesn't exist yet.
+ //
+ // await subscription.cancel().then(() => {
+ // setIsSubmitting(false);
+ // handleClose();
+ // }).catch(() => { setHasError(true); setIsSubmitting(false); });
+ };
+
return (
}
/>
+
{hasFeatures ? (
) : null}
- setIsOpen(false)}
- />
-
-
- );
-}
-const CancelFooter = ({ plan }: { plan: __experimental_CommercePlanResource; handleClose: () => void }) => {
- // const { __experimental_commerce } = useClerk();
- const [showConfirmation, setShowConfirmation] = React.useState(false);
- const [isSubmitting, setIsSubmitting] = React.useState(false);
- const [hasError, setHasError] = React.useState(false);
-
- const cancelSubscription = async () => {
- setHasError(false);
- setIsSubmitting(true);
-
- // TODO: we need to get a handle on the subscription object in order to cancel it,
- // but this method doesn't exist yet.
- //
- // await subscription.cancel().then(() => {
- // setIsSubmitting(false);
- // handleClose();
- // }).catch(() => { setHasError(true); setIsSubmitting(false); });
- };
-
- // TODO: remove when we can hook up cancel button
- // return null;
-
- return (
-
- {showConfirmation ? (
-
- Cancel {plan.name} Subscription?
-
- You can keep using “{plan.name}” features until [DATE], after which you will no longer have
- access.
-
- {hasError && (
- There was a problem canceling your subscription, please try again.
- )}
-
+
+
+
+
+ {!isSubmitting && (
+
+ )}
- )}
-
-
-
- ) : (
-
- )}
-
+
+ {/* TODO(@COMMERCE): needs localization */}
+ Cancel {plan.name} Subscription?
+
+
+ {/* TODO(@COMMERCE): needs localization */}
+ You can keep using “{plan.name}” features until [DATE], after which you will no longer have
+ access.
+
+ {hasError && (
+ // TODO(@COMMERCE): needs localization
+ There was a problem canceling your subscription, please try again.
+ )}
+
+
+
);
-};
+}
diff --git a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts
index c6ac9d01884..529457b7db0 100644
--- a/packages/clerk-js/src/ui/customizables/elementDescriptors.ts
+++ b/packages/clerk-js/src/ui/customizables/elementDescriptors.ts
@@ -99,6 +99,11 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
'drawerBody',
'drawerFooter',
'drawerClose',
+ 'drawerConfirmationBackdrop',
+ 'drawerConfirmationRoot',
+ 'drawerConfirmationTitle',
+ 'drawerConfirmationDescription',
+ 'drawerConfirmationActions',
'formHeader',
'formHeaderTitle',
diff --git a/packages/clerk-js/src/ui/elements/Drawer.tsx b/packages/clerk-js/src/ui/elements/Drawer.tsx
index 574bfc82505..1616200d74d 100644
--- a/packages/clerk-js/src/ui/elements/Drawer.tsx
+++ b/packages/clerk-js/src/ui/elements/Drawer.tsx
@@ -14,7 +14,7 @@ import {
import * as React from 'react';
import { transitionDurationValues, transitionTiming } from '../../ui/foundations/transitions';
-import { Box, descriptors, Flex, Heading, Icon, useAppearance } from '../customizables';
+import { Box, descriptors, Flex, Heading, Icon, Span, useAppearance } from '../customizables';
import { usePrefersReducedMotion } from '../hooks';
import { useScrollLock } from '../hooks/useScrollLock';
import { Close as CloseIcon } from '../icons';
@@ -402,6 +402,120 @@ const Close = React.forwardRef((_, ref) => {
Close.displayName = 'Drawer.Close';
+/* -------------------------------------------------------------------------------------------------
+ * Drawer.Confirmation
+ * -----------------------------------------------------------------------------------------------*/
+
+interface ConfirmationProps {
+ open: boolean;
+ onOpenChange: (open: boolean) => void;
+ children: React.ReactNode;
+ actionsSlot: React.ReactNode;
+}
+
+const Confirmation = React.forwardRef(
+ ({ open, onOpenChange, children, actionsSlot }, ref) => {
+ const prefersReducedMotion = usePrefersReducedMotion();
+ const { animations: layoutAnimations } = useAppearance().parsedLayout;
+ const isMotionSafe = !prefersReducedMotion && layoutAnimations === true;
+
+ const { refs, context } = useFloating({
+ open,
+ onOpenChange,
+ transform: false,
+ strategy: 'absolute',
+ });
+
+ const mergedRefs = useMergeRefs([ref, refs.setFloating]);
+
+ const { styles: overlayTransitionStyles } = useTransitionStyles(context, {
+ initial: { opacity: 0 },
+ open: { opacity: 1 },
+ close: { opacity: 0 },
+ common: {
+ transitionProperty: 'opacity',
+ transitionTimingFunction: transitionTiming.bezier,
+ },
+ duration: transitionDurationValues.drawer,
+ });
+
+ const { isMounted, styles: modalTransitionStyles } = useTransitionStyles(context, {
+ initial: { transform: 'translate3D(0, 100%, 0)' },
+ open: { transform: 'translate3D(0, 0, 0)' },
+ close: { transform: 'translate3D(0, 100%, 0)' },
+ common: {
+ transitionProperty: 'transform',
+ transitionTimingFunction: transitionTiming.bezier,
+ },
+ duration: isMotionSafe ? transitionDurationValues.drawer : 0,
+ });
+
+ const { getFloatingProps } = useInteractions([useClick(context), useDismiss(context), useRole(context)]);
+
+ if (!isMounted) return null;
+
+ return (
+ <>
+ ({
+ position: 'absolute',
+ inset: 0,
+ backgroundImage: `linear-gradient(to bottom, ${colors.setAlpha(t.colors.$colorBackground, 0.28)}, ${t.colors.$colorBackground})`,
+ })}
+ />
+
+
+ ({
+ display: 'flex',
+ flexDirection: 'column',
+ rowGap: t.space.$6,
+ outline: 'none',
+ willChange: 'transform',
+ position: 'absolute',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ background: common.mergedColorsBackground(
+ colors.setAlpha(t.colors.$colorBackground, 1),
+ t.colors.$neutralAlpha50,
+ ),
+ padding: t.space.$4,
+ borderStartStartRadius: t.radii.$md,
+ borderStartEndRadius: t.radii.$md,
+ boxShadow: `0 0 0 1px ${t.colors.$neutralAlpha100}`,
+ })}
+ >
+ {children}
+
+
+ {actionsSlot}
+
+
+
+ >
+ );
+ },
+);
+
+Confirmation.displayName = 'Drawer.Confirmation';
+
export const Drawer = {
Root,
Overlay,
@@ -409,5 +523,6 @@ export const Drawer = {
Header,
Body,
Footer,
+ Confirmation,
Close,
};
diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts
index 49bfe60287f..3802a0b1b03 100644
--- a/packages/types/src/appearance.ts
+++ b/packages/types/src/appearance.ts
@@ -217,6 +217,11 @@ export type ElementsConfig = {
drawerBody: WithOptions;
drawerFooter: WithOptions;
drawerClose: WithOptions;
+ drawerConfirmationBackdrop: WithOptions;
+ drawerConfirmationRoot: WithOptions;
+ drawerConfirmationTitle: WithOptions;
+ drawerConfirmationDescription: WithOptions;
+ drawerConfirmationActions: WithOptions;
formHeader: WithOptions;
formHeaderTitle: WithOptions;