Skip to content

Commit 9572bf5

Browse files
authoredMar 17, 2025
refactor(clerk-js): Introduce <Drawer.Confirmation /> component (#5376)
1 parent ecc2b93 commit 9572bf5

File tree

6 files changed

+217
-85
lines changed

6 files changed

+217
-85
lines changed
 

‎.changeset/forty-ladybugs-tie.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/types': patch
4+
---
5+
6+
Introduce `<Drawer.Confirmation />` component to be used within Commerce cancel subscription flow.

‎packages/clerk-js/bundlewatch.config.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
{
22
"files": [
3-
{ "path": "./dist/clerk.js", "maxSize": "575kB" },
3+
{ "path": "./dist/clerk.js", "maxSize": "576kB" },
44
{ "path": "./dist/clerk.browser.js", "maxSize": "78kB" },
55
{ "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
6-
{ "path": "./dist/ui-common*.js", "maxSize": "93KB" },
6+
{ "path": "./dist/ui-common*.js", "maxSize": "94KB" },
77
{ "path": "./dist/vendors*.js", "maxSize": "30KB" },
88
{ "path": "./dist/coinbase*.js", "maxSize": "35.5KB" },
99
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { __experimental_CommercePlanResource } from '@clerk/types';
22
import * as React from 'react';
33

4-
import { Alert, Box, Button, Col, Flex, Heading, Text } from '../../customizables';
5-
import { Drawer } from '../../elements';
4+
import { Box, Button, descriptors, Heading, Text } from '../../customizables';
5+
import { Alert, Drawer } from '../../elements';
66
import type { PlanPeriod } from './PlanCard';
77
import { PlanCardFeaturesList, PlanCardHeader } from './PlanCard';
88

@@ -27,10 +27,26 @@ export function PlanDetailDrawer({
2727
planPeriod,
2828
setPlanPeriod,
2929
}: PlanDetailDrawerProps) {
30+
const [showConfirmation, setShowConfirmation] = React.useState(false);
31+
const [isSubmitting, setIsSubmitting] = React.useState(false);
32+
const [hasError, setHasError] = React.useState(false);
3033
if (!plan) {
3134
return null;
3235
}
3336
const hasFeatures = plan.features.length > 0;
37+
const cancelSubscription = async () => {
38+
setHasError(false);
39+
setIsSubmitting(true);
40+
41+
// TODO(@COMMERCE): we need to get a handle on the subscription object in order to cancel it,
42+
// but this method doesn't exist yet.
43+
//
44+
// await subscription.cancel().then(() => {
45+
// setIsSubmitting(false);
46+
// handleClose();
47+
// }).catch(() => { setHasError(true); setIsSubmitting(false); });
48+
};
49+
3450
return (
3551
<Drawer.Root
3652
open={isOpen}
@@ -58,6 +74,7 @@ export function PlanDetailDrawer({
5874
closeSlot={<Drawer.Close />}
5975
/>
6076
</Drawer.Header>
77+
6178
{hasFeatures ? (
6279
<Drawer.Body>
6380
<Box
@@ -69,92 +86,76 @@ export function PlanDetailDrawer({
6986
</Box>
7087
</Drawer.Body>
7188
) : null}
72-
<CancelFooter
73-
plan={plan}
74-
handleClose={() => setIsOpen(false)}
75-
/>
76-
</Drawer.Content>
77-
</Drawer.Root>
78-
);
79-
}
8089

81-
const CancelFooter = ({ plan }: { plan: __experimental_CommercePlanResource; handleClose: () => void }) => {
82-
// const { __experimental_commerce } = useClerk();
83-
const [showConfirmation, setShowConfirmation] = React.useState(false);
84-
const [isSubmitting, setIsSubmitting] = React.useState(false);
85-
const [hasError, setHasError] = React.useState(false);
86-
87-
const cancelSubscription = async () => {
88-
setHasError(false);
89-
setIsSubmitting(true);
90-
91-
// TODO: we need to get a handle on the subscription object in order to cancel it,
92-
// but this method doesn't exist yet.
93-
//
94-
// await subscription.cancel().then(() => {
95-
// setIsSubmitting(false);
96-
// handleClose();
97-
// }).catch(() => { setHasError(true); setIsSubmitting(false); });
98-
};
99-
100-
// TODO: remove when we can hook up cancel button
101-
// return null;
102-
103-
return (
104-
<Drawer.Footer>
105-
{showConfirmation ? (
106-
<Col gap={8}>
107-
<Heading textVariant='h3'>Cancel {plan.name} Subscription?</Heading>
108-
<Text colorScheme='secondary'>
109-
You can keep using &ldquo;{plan.name}&rdquo; features until [DATE], after which you will no longer have
110-
access.
111-
</Text>
112-
{hasError && (
113-
<Alert colorScheme='danger'>There was a problem canceling your subscription, please try again.</Alert>
114-
)}
115-
<Flex
116-
gap={3}
117-
justify='end'
90+
<Drawer.Footer>
91+
<Button
92+
variant='bordered'
93+
colorScheme='secondary'
94+
size='sm'
95+
textVariant='buttonLarge'
96+
block
97+
onClick={() => setShowConfirmation(true)}
11898
>
119-
{!isSubmitting && (
99+
{/* TODO(@COMMERCE): needs localization */}
100+
Cancel Subscription
101+
</Button>
102+
</Drawer.Footer>
103+
104+
<Drawer.Confirmation
105+
open={showConfirmation}
106+
onOpenChange={setShowConfirmation}
107+
actionsSlot={
108+
<>
109+
{!isSubmitting && (
110+
<Button
111+
variant='ghost'
112+
size='sm'
113+
textVariant='buttonLarge'
114+
onClick={() => {
115+
setHasError(false);
116+
setShowConfirmation(false);
117+
}}
118+
>
119+
{/* TODO(@COMMERCE): needs localization */}
120+
Keep Subscription
121+
</Button>
122+
)}
120123
<Button
121-
variant='ghost'
124+
variant='solid'
125+
colorScheme='danger'
122126
size='sm'
123127
textVariant='buttonLarge'
124-
onClick={() => {
125-
setHasError(false);
126-
setShowConfirmation(false);
127-
}}
128+
isLoading={isSubmitting}
129+
onClick={cancelSubscription}
128130
>
129-
Keep Subscription
131+
{/* TODO(@COMMERCE): needs localization */}
132+
Cancel Subscription
130133
</Button>
131-
)}
132-
<Button
133-
variant='solid'
134-
colorScheme='danger'
135-
size='sm'
136-
textVariant='buttonLarge'
137-
isLoading={isSubmitting}
138-
onClick={cancelSubscription}
139-
>
140-
Cancel Subscription
141-
</Button>
142-
</Flex>
143-
</Col>
144-
) : (
145-
<Button
146-
variant='bordered'
147-
colorScheme='secondary'
148-
size='sm'
149-
textVariant='buttonLarge'
150-
sx={{
151-
width: '100%',
152-
}}
153-
onClick={() => setShowConfirmation(true)}
134+
</>
135+
}
154136
>
155-
Cancel Subscription
156-
</Button>
157-
)}
158-
</Drawer.Footer>
137+
<Heading
138+
elementDescriptor={descriptors.drawerConfirmationTitle}
139+
as='h2'
140+
textVariant='h3'
141+
>
142+
{/* TODO(@COMMERCE): needs localization */}
143+
Cancel {plan.name} Subscription?
144+
</Heading>
145+
<Text
146+
elementDescriptor={descriptors.drawerConfirmationDescription}
147+
colorScheme='secondary'
148+
>
149+
{/* TODO(@COMMERCE): needs localization */}
150+
You can keep using &ldquo;{plan.name}&rdquo; features until [DATE], after which you will no longer have
151+
access.
152+
</Text>
153+
{hasError && (
154+
// TODO(@COMMERCE): needs localization
155+
<Alert colorScheme='danger'>There was a problem canceling your subscription, please try again.</Alert>
156+
)}
157+
</Drawer.Confirmation>
158+
</Drawer.Content>
159+
</Drawer.Root>
159160
);
160-
};
161+
}

‎packages/clerk-js/src/ui/customizables/elementDescriptors.ts

+5
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
9999
'drawerBody',
100100
'drawerFooter',
101101
'drawerClose',
102+
'drawerConfirmationBackdrop',
103+
'drawerConfirmationRoot',
104+
'drawerConfirmationTitle',
105+
'drawerConfirmationDescription',
106+
'drawerConfirmationActions',
102107

103108
'formHeader',
104109
'formHeaderTitle',

‎packages/clerk-js/src/ui/elements/Drawer.tsx

+116-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
import * as React from 'react';
1515

1616
import { transitionDurationValues, transitionTiming } from '../../ui/foundations/transitions';
17-
import { Box, descriptors, Flex, Heading, Icon, useAppearance } from '../customizables';
17+
import { Box, descriptors, Flex, Heading, Icon, Span, useAppearance } from '../customizables';
1818
import { usePrefersReducedMotion } from '../hooks';
1919
import { useScrollLock } from '../hooks/useScrollLock';
2020
import { Close as CloseIcon } from '../icons';
@@ -402,12 +402,127 @@ const Close = React.forwardRef<HTMLButtonElement>((_, ref) => {
402402

403403
Close.displayName = 'Drawer.Close';
404404

405+
/* -------------------------------------------------------------------------------------------------
406+
* Drawer.Confirmation
407+
* -----------------------------------------------------------------------------------------------*/
408+
409+
interface ConfirmationProps {
410+
open: boolean;
411+
onOpenChange: (open: boolean) => void;
412+
children: React.ReactNode;
413+
actionsSlot: React.ReactNode;
414+
}
415+
416+
const Confirmation = React.forwardRef<HTMLDivElement, ConfirmationProps>(
417+
({ open, onOpenChange, children, actionsSlot }, ref) => {
418+
const prefersReducedMotion = usePrefersReducedMotion();
419+
const { animations: layoutAnimations } = useAppearance().parsedLayout;
420+
const isMotionSafe = !prefersReducedMotion && layoutAnimations === true;
421+
422+
const { refs, context } = useFloating({
423+
open,
424+
onOpenChange,
425+
transform: false,
426+
strategy: 'absolute',
427+
});
428+
429+
const mergedRefs = useMergeRefs([ref, refs.setFloating]);
430+
431+
const { styles: overlayTransitionStyles } = useTransitionStyles(context, {
432+
initial: { opacity: 0 },
433+
open: { opacity: 1 },
434+
close: { opacity: 0 },
435+
common: {
436+
transitionProperty: 'opacity',
437+
transitionTimingFunction: transitionTiming.bezier,
438+
},
439+
duration: transitionDurationValues.drawer,
440+
});
441+
442+
const { isMounted, styles: modalTransitionStyles } = useTransitionStyles(context, {
443+
initial: { transform: 'translate3D(0, 100%, 0)' },
444+
open: { transform: 'translate3D(0, 0, 0)' },
445+
close: { transform: 'translate3D(0, 100%, 0)' },
446+
common: {
447+
transitionProperty: 'transform',
448+
transitionTimingFunction: transitionTiming.bezier,
449+
},
450+
duration: isMotionSafe ? transitionDurationValues.drawer : 0,
451+
});
452+
453+
const { getFloatingProps } = useInteractions([useClick(context), useDismiss(context), useRole(context)]);
454+
455+
if (!isMounted) return null;
456+
457+
return (
458+
<>
459+
<Span
460+
elementDescriptor={descriptors.drawerConfirmationBackdrop}
461+
style={overlayTransitionStyles}
462+
sx={t => ({
463+
position: 'absolute',
464+
inset: 0,
465+
backgroundImage: `linear-gradient(to bottom, ${colors.setAlpha(t.colors.$colorBackground, 0.28)}, ${t.colors.$colorBackground})`,
466+
})}
467+
/>
468+
469+
<FloatingFocusManager
470+
context={context}
471+
modal
472+
outsideElementsInert
473+
initialFocus={refs.floating}
474+
visuallyHiddenDismiss
475+
>
476+
<Box
477+
ref={mergedRefs}
478+
elementDescriptor={descriptors.drawerConfirmationRoot}
479+
style={modalTransitionStyles}
480+
{...getFloatingProps()}
481+
sx={t => ({
482+
display: 'flex',
483+
flexDirection: 'column',
484+
rowGap: t.space.$6,
485+
outline: 'none',
486+
willChange: 'transform',
487+
position: 'absolute',
488+
bottom: 0,
489+
left: 0,
490+
right: 0,
491+
background: common.mergedColorsBackground(
492+
colors.setAlpha(t.colors.$colorBackground, 1),
493+
t.colors.$neutralAlpha50,
494+
),
495+
padding: t.space.$4,
496+
borderStartStartRadius: t.radii.$md,
497+
borderStartEndRadius: t.radii.$md,
498+
boxShadow: `0 0 0 1px ${t.colors.$neutralAlpha100}`,
499+
})}
500+
>
501+
{children}
502+
503+
<Flex
504+
elementDescriptor={descriptors.drawerConfirmationActions}
505+
gap={3}
506+
justify='end'
507+
>
508+
{actionsSlot}
509+
</Flex>
510+
</Box>
511+
</FloatingFocusManager>
512+
</>
513+
);
514+
},
515+
);
516+
517+
Confirmation.displayName = 'Drawer.Confirmation';
518+
405519
export const Drawer = {
406520
Root,
407521
Overlay,
408522
Content,
409523
Header,
410524
Body,
411525
Footer,
526+
Confirmation,
412527
Close,
413528
};

‎packages/types/src/appearance.ts

+5
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@ export type ElementsConfig = {
217217
drawerBody: WithOptions;
218218
drawerFooter: WithOptions;
219219
drawerClose: WithOptions;
220+
drawerConfirmationBackdrop: WithOptions;
221+
drawerConfirmationRoot: WithOptions;
222+
drawerConfirmationTitle: WithOptions;
223+
drawerConfirmationDescription: WithOptions;
224+
drawerConfirmationActions: WithOptions;
220225

221226
formHeader: WithOptions<never, ErrorState>;
222227
formHeaderTitle: WithOptions<never, ErrorState>;

0 commit comments

Comments
 (0)
Please sign in to comment.