Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(clerk-js): Introduce <Drawer.Confirmation /> component #5376

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/forty-ladybugs-tie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': patch
'@clerk/types': patch
---

Introduce `<Drawer.Confirmation />` component to be used within Commerce cancel subscription flow.
4 changes: 2 additions & 2 deletions packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -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" },
Expand Down
165 changes: 83 additions & 82 deletions packages/clerk-js/src/ui/components/PricingTable/PlanDetailDrawer.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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 (
<Drawer.Root
open={isOpen}
Expand Down Expand Up @@ -58,6 +74,7 @@ export function PlanDetailDrawer({
closeSlot={<Drawer.Close />}
/>
</Drawer.Header>

{hasFeatures ? (
<Drawer.Body>
<Box
Expand All @@ -69,92 +86,76 @@ export function PlanDetailDrawer({
</Box>
</Drawer.Body>
) : null}
<CancelFooter
plan={plan}
handleClose={() => setIsOpen(false)}
/>
</Drawer.Content>
</Drawer.Root>
);
}

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 (
<Drawer.Footer>
{showConfirmation ? (
<Col gap={8}>
<Heading textVariant='h3'>Cancel {plan.name} Subscription?</Heading>
<Text colorScheme='secondary'>
You can keep using &ldquo;{plan.name}&rdquo; features until [DATE], after which you will no longer have
access.
</Text>
{hasError && (
<Alert colorScheme='danger'>There was a problem canceling your subscription, please try again.</Alert>
)}
<Flex
gap={3}
justify='end'
<Drawer.Footer>
<Button
variant='bordered'
colorScheme='secondary'
size='sm'
textVariant='buttonLarge'
block
onClick={() => setShowConfirmation(true)}
>
{!isSubmitting && (
{/* TODO(@COMMERCE): needs localization */}
Cancel Subscription
</Button>
</Drawer.Footer>

<Drawer.Confirmation
open={showConfirmation}
onOpenChange={setShowConfirmation}
actionsSlot={
<>
{!isSubmitting && (
<Button
variant='ghost'
size='sm'
textVariant='buttonLarge'
onClick={() => {
setHasError(false);
setShowConfirmation(false);
}}
>
{/* TODO(@COMMERCE): needs localization */}
Keep Subscription
</Button>
)}
<Button
variant='ghost'
variant='solid'
colorScheme='danger'
size='sm'
textVariant='buttonLarge'
onClick={() => {
setHasError(false);
setShowConfirmation(false);
}}
isLoading={isSubmitting}
onClick={cancelSubscription}
>
Keep Subscription
{/* TODO(@COMMERCE): needs localization */}
Cancel Subscription
</Button>
)}
<Button
variant='solid'
colorScheme='danger'
size='sm'
textVariant='buttonLarge'
isLoading={isSubmitting}
onClick={cancelSubscription}
>
Cancel Subscription
</Button>
</Flex>
</Col>
) : (
<Button
variant='bordered'
colorScheme='secondary'
size='sm'
textVariant='buttonLarge'
sx={{
width: '100%',
}}
onClick={() => setShowConfirmation(true)}
</>
}
>
Cancel Subscription
</Button>
)}
</Drawer.Footer>
<Heading
elementDescriptor={descriptors.drawerConfirmationTitle}
as='h2'
textVariant='h3'
>
{/* TODO(@COMMERCE): needs localization */}
Cancel {plan.name} Subscription?
</Heading>
<Text
elementDescriptor={descriptors.drawerConfirmationDescription}
colorScheme='secondary'
>
{/* TODO(@COMMERCE): needs localization */}
You can keep using &ldquo;{plan.name}&rdquo; features until [DATE], after which you will no longer have
access.
</Text>
{hasError && (
// TODO(@COMMERCE): needs localization
<Alert colorScheme='danger'>There was a problem canceling your subscription, please try again.</Alert>
)}
</Drawer.Confirmation>
</Drawer.Content>
</Drawer.Root>
);
};
}
5 changes: 5 additions & 0 deletions packages/clerk-js/src/ui/customizables/elementDescriptors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ export const APPEARANCE_KEYS = containsAllElementsConfigKeys([
'drawerBody',
'drawerFooter',
'drawerClose',
'drawerConfirmationBackdrop',
'drawerConfirmationRoot',
'drawerConfirmationTitle',
'drawerConfirmationDescription',
'drawerConfirmationActions',

'formHeader',
'formHeaderTitle',
Expand Down
117 changes: 116 additions & 1 deletion packages/clerk-js/src/ui/elements/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -402,12 +402,127 @@ const Close = React.forwardRef<HTMLButtonElement>((_, 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<HTMLDivElement, ConfirmationProps>(
({ 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 (
<>
<Span
elementDescriptor={descriptors.drawerConfirmationBackdrop}
style={overlayTransitionStyles}
sx={t => ({
position: 'absolute',
inset: 0,
backgroundImage: `linear-gradient(to bottom, ${colors.setAlpha(t.colors.$colorBackground, 0.28)}, ${t.colors.$colorBackground})`,
})}
/>

<FloatingFocusManager
context={context}
modal
outsideElementsInert
initialFocus={refs.floating}
visuallyHiddenDismiss
>
<Box
ref={mergedRefs}
elementDescriptor={descriptors.drawerConfirmationRoot}
style={modalTransitionStyles}
{...getFloatingProps()}
sx={t => ({
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}

<Flex
elementDescriptor={descriptors.drawerConfirmationActions}
gap={3}
justify='end'
>
{actionsSlot}
</Flex>
</Box>
</FloatingFocusManager>
</>
);
},
);

Confirmation.displayName = 'Drawer.Confirmation';

export const Drawer = {
Root,
Overlay,
Content,
Header,
Body,
Footer,
Confirmation,
Close,
};
5 changes: 5 additions & 0 deletions packages/types/src/appearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<never, ErrorState>;
formHeaderTitle: WithOptions<never, ErrorState>;
Expand Down