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;