diff --git a/.changeset/purple-cities-study.md b/.changeset/purple-cities-study.md new file mode 100644 index 00000000000..c7a40b91e51 --- /dev/null +++ b/.changeset/purple-cities-study.md @@ -0,0 +1,5 @@ +--- +'@clerk/clerk-js': patch +--- + +Add entry animations to CheckoutComplete component to smooth our the transition between checking out to successful state. diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 47b4a4fdd85..d24606d3c2d 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -21,7 +21,7 @@ { "path": "./dist/waitlist*.js", "maxSize": "1.3KB" }, { "path": "./dist/keylessPrompt*.js", "maxSize": "6.5KB" }, { "path": "./dist/pricingTable*.js", "maxSize": "4.02KB" }, - { "path": "./dist/checkout*.js", "maxSize": "5.3KB" }, + { "path": "./dist/checkout*.js", "maxSize": "5.75KB" }, { "path": "./dist/paymentSources*.js", "maxSize": "8.9KB" }, { "path": "./dist/up-billing-page*.js", "maxSize": "2.4KB" }, { "path": "./dist/op-billing-page*.js", "maxSize": "2.4KB" }, diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx index 664d8116d31..b45caa4b186 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx @@ -1,13 +1,19 @@ import type { __experimental_CommerceCheckoutResource } from '@clerk/types'; -import { Box, Button, descriptors, Heading, Icon, localizationKeys, Span, Text } from '../../customizables'; +import { Box, Button, descriptors, Heading, localizationKeys, Span, Text } from '../../customizables'; import { Drawer, LineItems, useDrawerContext } from '../../elements'; -import { Check } from '../../icons'; +import { transitionDurationValues, transitionTiming } from '../../foundations/transitions'; +import { animations } from '../../styledSystem'; import { formatDate } from '../../utils'; - const capitalize = (name: string) => name[0].toUpperCase() + name.slice(1); -export const CheckoutComplete = ({ checkout }: { checkout: __experimental_CommerceCheckoutResource }) => { +export const CheckoutComplete = ({ + checkout, + isMotionSafe, +}: { + checkout: __experimental_CommerceCheckoutResource; + isMotionSafe: boolean; +}) => { const { setIsOpen } = useDrawerContext(); const handleClose = () => { @@ -29,11 +35,40 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer width: '100%', padding: t.space.$4, flexShrink: 0, + transformOrigin: 'bottom center', + animationName: 'scaleIn', + animationDuration: `${transitionDurationValues.slowest}ms`, + animationTimingFunction: transitionTiming.bezier, + animationFillMode: 'forwards', + opacity: 0, + '@keyframes scaleIn': { + '0%': { + filter: 'blur(10px)', + transform: 'scale(0.85)', + opacity: 0, + }, + '100%': { + filter: 'blur(0px)', + transform: 'scale(1)', + opacity: 1, + }, + }, + ...(!isMotionSafe && { + animation: 'none', + opacity: 1, + }), })} > - - - + {[1, 0.75, 0.5].map((scale, index, array) => { + return ( + + ); + })} ({ @@ -55,15 +90,48 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer }, })} > - ({ @@ -83,11 +151,55 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer ? localizationKeys('__experimental_commerce.checkout.title__paymentSuccessful') : localizationKeys('__experimental_commerce.checkout.title__subscriptionSuccessful') } + sx={{ + opacity: 0, + animationName: 'slideUp', + animationDuration: `${transitionDurationValues.slowest}ms`, + animationTimingFunction: transitionTiming.bezier, + animationFillMode: 'forwards', + '@keyframes slideUp': { + '0%': { + transform: 'translateY(30px)', + opacity: 0, + }, + '100%': { + transform: 'translateY(0)', + opacity: 1, + }, + }, + ...(!isMotionSafe && { + opacity: 1, + animation: 'none', + }), + }} /> ({ textAlign: 'center', paddingInline: t.space.$8, marginBlockStart: t.space.$2 })} + sx={t => ({ + textAlign: 'center', + paddingInline: t.space.$8, + marginBlockStart: t.space.$2, + opacity: 0, + animationName: 'slideUp', + animationDuration: `${transitionDurationValues.slowest * 1.5}ms`, + animationTimingFunction: transitionTiming.bezier, + animationFillMode: 'forwards', + '@keyframes slideUp': { + '0%': { + transform: 'translateY(30px)', + opacity: 0, + }, + '100%': { + transform: 'translateY(0)', + opacity: 1, + }, + }, + ...(!isMotionSafe && { + opacity: 1, + animation: 'none', + }), + })} localizationKey={ checkout.subscription?.status === 'active' ? localizationKeys('__experimental_commerce.checkout.description__paymentSuccessful') @@ -97,10 +209,25 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer - ({ rowGap: t.space.$4, + animationName: 'footerSlideInUp', + animationDuration: `${transitionDurationValues.drawer}ms`, + animationTimingFunction: transitionTiming.bezier, + '@keyframes footerSlideInUp': { + '0%': { + transform: 'translateY(100%)', + opacity: 0, + }, + '100%': { + transform: 'translateY(0)', + opacity: 1, + }, + }, + ...(!isMotionSafe && { + animation: 'none', + }), })} > @@ -142,11 +269,18 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer function Ring({ scale, + index, + isMotionSafe, }: { /** * Number between 0-1 */ scale: number; + /** + * Index of the ring (0-2) + */ + index: number; + isMotionSafe: boolean; }) { return ( ); diff --git a/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx b/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx index 428b6faff75..50681015b23 100644 --- a/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx +++ b/packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx @@ -5,9 +5,9 @@ import type { } from '@clerk/types'; import { useEffect } from 'react'; -import { Alert, Box, Flex, localizationKeys, Spinner, useLocalizations } from '../../customizables'; +import { Alert, Box, Flex, localizationKeys, Spinner, useAppearance, useLocalizations } from '../../customizables'; import { Drawer, useDrawerContext } from '../../elements'; -import { useCheckout } from '../../hooks'; +import { useCheckout, usePrefersReducedMotion } from '../../hooks'; import { EmailForm } from '../UserProfile/EmailForm'; import { CheckoutComplete } from './CheckoutComplete'; import { CheckoutForm } from './CheckoutForm'; @@ -16,6 +16,9 @@ export const CheckoutPage = (props: __experimental_CheckoutProps) => { const { translateError } = useLocalizations(); const { planId, planPeriod, subscriberType, onSubscriptionComplete } = props; const { setIsOpen, isOpen } = useDrawerContext(); + const prefersReducedMotion = usePrefersReducedMotion(); + const { animations: layoutAnimations } = useAppearance().parsedLayout; + const isMotionSafe = !prefersReducedMotion && layoutAnimations === true; const { checkout, isLoading, invalidate, revalidate, updateCheckout, errors } = useCheckout({ planId, @@ -49,7 +52,12 @@ export const CheckoutPage = (props: __experimental_CheckoutProps) => { if (checkout) { if (checkout?.status === 'completed') { - return ; + return ( + + ); } return ( diff --git a/packages/clerk-js/src/ui/elements/Drawer.tsx b/packages/clerk-js/src/ui/elements/Drawer.tsx index b0800e62795..0542c6c66b6 100644 --- a/packages/clerk-js/src/ui/elements/Drawer.tsx +++ b/packages/clerk-js/src/ui/elements/Drawer.tsx @@ -313,11 +313,11 @@ const Header = React.forwardRef(({ title, children, * Drawer.Body * -----------------------------------------------------------------------------------------------*/ -interface BodyProps { +interface BodyProps extends React.HTMLAttributes { children: React.ReactNode; } -const Body = React.forwardRef(({ children }, ref) => { +const Body = React.forwardRef(({ children, ...props }, ref) => { return ( (({ children }, ref) => overflowY: 'auto', overflowX: 'hidden', }} + {...props} > {children} @@ -339,12 +340,12 @@ const Body = React.forwardRef(({ children }, ref) => * Drawer.Footer * -----------------------------------------------------------------------------------------------*/ -interface FooterProps { +interface FooterProps extends React.HTMLAttributes { children?: React.ReactNode; sx?: ThemableCssProp; } -const Footer = React.forwardRef(({ children, sx }, ref) => { +const Footer = React.forwardRef(({ children, sx, ...props }, ref) => { return ( (({ children, sx }, }), sx, ]} + {...props} > {children}