Skip to content

Commit 47470c0

Browse files
feat(clerk-js): Add transition animations into checkout complete state (#5802)
1 parent 0384d60 commit 47470c0

File tree

5 files changed

+182
-23
lines changed

5 files changed

+182
-23
lines changed

.changeset/purple-cities-study.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Add entry animations to CheckoutComplete component to smooth our the transition between checking out to successful state.

packages/clerk-js/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
2222
{ "path": "./dist/keylessPrompt*.js", "maxSize": "6.5KB" },
2323
{ "path": "./dist/pricingTable*.js", "maxSize": "4.02KB" },
24-
{ "path": "./dist/checkout*.js", "maxSize": "5.3KB" },
24+
{ "path": "./dist/checkout*.js", "maxSize": "5.75KB" },
2525
{ "path": "./dist/paymentSources*.js", "maxSize": "8.9KB" },
2626
{ "path": "./dist/up-billing-page*.js", "maxSize": "2.4KB" },
2727
{ "path": "./dist/op-billing-page*.js", "maxSize": "2.4KB" },

packages/clerk-js/src/ui/components/Checkout/CheckoutComplete.tsx

Lines changed: 159 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
11
import type { __experimental_CommerceCheckoutResource } from '@clerk/types';
22

3-
import { Box, Button, descriptors, Heading, Icon, localizationKeys, Span, Text } from '../../customizables';
3+
import { Box, Button, descriptors, Heading, localizationKeys, Span, Text } from '../../customizables';
44
import { Drawer, LineItems, useDrawerContext } from '../../elements';
5-
import { Check } from '../../icons';
5+
import { transitionDurationValues, transitionTiming } from '../../foundations/transitions';
6+
import { animations } from '../../styledSystem';
67
import { formatDate } from '../../utils';
7-
88
const capitalize = (name: string) => name[0].toUpperCase() + name.slice(1);
99

10-
export const CheckoutComplete = ({ checkout }: { checkout: __experimental_CommerceCheckoutResource }) => {
10+
export const CheckoutComplete = ({
11+
checkout,
12+
isMotionSafe,
13+
}: {
14+
checkout: __experimental_CommerceCheckoutResource;
15+
isMotionSafe: boolean;
16+
}) => {
1117
const { setIsOpen } = useDrawerContext();
1218

1319
const handleClose = () => {
@@ -29,11 +35,40 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer
2935
width: '100%',
3036
padding: t.space.$4,
3137
flexShrink: 0,
38+
transformOrigin: 'bottom center',
39+
animationName: 'scaleIn',
40+
animationDuration: `${transitionDurationValues.slowest}ms`,
41+
animationTimingFunction: transitionTiming.bezier,
42+
animationFillMode: 'forwards',
43+
opacity: 0,
44+
'@keyframes scaleIn': {
45+
'0%': {
46+
filter: 'blur(10px)',
47+
transform: 'scale(0.85)',
48+
opacity: 0,
49+
},
50+
'100%': {
51+
filter: 'blur(0px)',
52+
transform: 'scale(1)',
53+
opacity: 1,
54+
},
55+
},
56+
...(!isMotionSafe && {
57+
animation: 'none',
58+
opacity: 1,
59+
}),
3260
})}
3361
>
34-
<Ring scale={1} />
35-
<Ring scale={0.75} />
36-
<Ring scale={0.5} />
62+
{[1, 0.75, 0.5].map((scale, index, array) => {
63+
return (
64+
<Ring
65+
key={scale}
66+
scale={scale}
67+
index={array.length - 1 - index}
68+
isMotionSafe={isMotionSafe}
69+
/>
70+
);
71+
})}
3772
<Box
3873
elementDescriptor={descriptors.checkoutSuccessBadge}
3974
sx={t => ({
@@ -55,15 +90,48 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer
5590
},
5691
})}
5792
>
58-
<Icon
59-
icon={Check}
60-
colorScheme='neutral'
61-
sx={{
93+
<svg
94+
fill='none'
95+
viewBox='0 0 10 10'
96+
aria-hidden='true'
97+
style={{
6298
position: 'relative',
6399
margin: 'auto',
100+
width: '1rem',
101+
height: '1rem',
64102
}}
65-
aria-hidden
66-
/>
103+
>
104+
<path
105+
d='m1 6 3 3 5-8'
106+
stroke='currentColor'
107+
strokeWidth='1.25'
108+
strokeLinecap='round'
109+
strokeLinejoin='round'
110+
strokeDasharray='1'
111+
pathLength='1'
112+
style={{
113+
strokeDashoffset: '1',
114+
animationName: 'check',
115+
animationDuration: `${transitionDurationValues.drawer}ms`,
116+
animationTimingFunction: transitionTiming.bezier,
117+
animationFillMode: 'forwards',
118+
animationDelay: `${transitionDurationValues.slow}ms`,
119+
...(!isMotionSafe && {
120+
strokeDashoffset: '0',
121+
animation: 'none',
122+
}),
123+
}}
124+
/>
125+
</svg>
126+
<style>{`
127+
@keyframes check {
128+
0% {
129+
stroke-dashoffset: 1;
130+
}
131+
100% {
132+
stroke-dashoffset: 0;
133+
}
134+
`}</style>
67135
</Box>
68136
<Span
69137
sx={t => ({
@@ -83,11 +151,55 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer
83151
? localizationKeys('__experimental_commerce.checkout.title__paymentSuccessful')
84152
: localizationKeys('__experimental_commerce.checkout.title__subscriptionSuccessful')
85153
}
154+
sx={{
155+
opacity: 0,
156+
animationName: 'slideUp',
157+
animationDuration: `${transitionDurationValues.slowest}ms`,
158+
animationTimingFunction: transitionTiming.bezier,
159+
animationFillMode: 'forwards',
160+
'@keyframes slideUp': {
161+
'0%': {
162+
transform: 'translateY(30px)',
163+
opacity: 0,
164+
},
165+
'100%': {
166+
transform: 'translateY(0)',
167+
opacity: 1,
168+
},
169+
},
170+
...(!isMotionSafe && {
171+
opacity: 1,
172+
animation: 'none',
173+
}),
174+
}}
86175
/>
87176
<Text
88177
elementDescriptor={descriptors.checkoutSuccessDescription}
89178
colorScheme='secondary'
90-
sx={t => ({ textAlign: 'center', paddingInline: t.space.$8, marginBlockStart: t.space.$2 })}
179+
sx={t => ({
180+
textAlign: 'center',
181+
paddingInline: t.space.$8,
182+
marginBlockStart: t.space.$2,
183+
opacity: 0,
184+
animationName: 'slideUp',
185+
animationDuration: `${transitionDurationValues.slowest * 1.5}ms`,
186+
animationTimingFunction: transitionTiming.bezier,
187+
animationFillMode: 'forwards',
188+
'@keyframes slideUp': {
189+
'0%': {
190+
transform: 'translateY(30px)',
191+
opacity: 0,
192+
},
193+
'100%': {
194+
transform: 'translateY(0)',
195+
opacity: 1,
196+
},
197+
},
198+
...(!isMotionSafe && {
199+
opacity: 1,
200+
animation: 'none',
201+
}),
202+
})}
91203
localizationKey={
92204
checkout.subscription?.status === 'active'
93205
? localizationKeys('__experimental_commerce.checkout.description__paymentSuccessful')
@@ -97,10 +209,25 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer
97209
</Span>
98210
</Span>
99211
</Drawer.Body>
100-
101212
<Drawer.Footer
102213
sx={t => ({
103214
rowGap: t.space.$4,
215+
animationName: 'footerSlideInUp',
216+
animationDuration: `${transitionDurationValues.drawer}ms`,
217+
animationTimingFunction: transitionTiming.bezier,
218+
'@keyframes footerSlideInUp': {
219+
'0%': {
220+
transform: 'translateY(100%)',
221+
opacity: 0,
222+
},
223+
'100%': {
224+
transform: 'translateY(0)',
225+
opacity: 1,
226+
},
227+
},
228+
...(!isMotionSafe && {
229+
animation: 'none',
230+
}),
104231
})}
105232
>
106233
<LineItems.Root>
@@ -142,11 +269,18 @@ export const CheckoutComplete = ({ checkout }: { checkout: __experimental_Commer
142269

143270
function Ring({
144271
scale,
272+
index,
273+
isMotionSafe,
145274
}: {
146275
/**
147276
* Number between 0-1
148277
*/
149278
scale: number;
279+
/**
280+
* Index of the ring (0-2)
281+
*/
282+
index: number;
283+
isMotionSafe: boolean;
150284
}) {
151285
return (
152286
<Span
@@ -161,6 +295,16 @@ function Ring({
161295
borderColor: t.colors.$neutralAlpha200,
162296
borderRadius: t.radii.$circle,
163297
maskImage: `linear-gradient(to bottom, transparent 15%, black, transparent 85%)`,
298+
opacity: 0,
299+
animationName: animations.fadeIn,
300+
animationDuration: `${transitionDurationValues.slow}ms`,
301+
animationTimingFunction: transitionTiming.bezier,
302+
animationFillMode: 'forwards',
303+
animationDelay: `${index * transitionDurationValues.slow}ms`,
304+
...(!isMotionSafe && {
305+
animation: 'none',
306+
opacity: 1,
307+
}),
164308
})}
165309
/>
166310
);

packages/clerk-js/src/ui/components/Checkout/CheckoutPage.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import type {
55
} from '@clerk/types';
66
import { useEffect } from 'react';
77

8-
import { Alert, Box, Flex, localizationKeys, Spinner, useLocalizations } from '../../customizables';
8+
import { Alert, Box, Flex, localizationKeys, Spinner, useAppearance, useLocalizations } from '../../customizables';
99
import { Drawer, useDrawerContext } from '../../elements';
10-
import { useCheckout } from '../../hooks';
10+
import { useCheckout, usePrefersReducedMotion } from '../../hooks';
1111
import { EmailForm } from '../UserProfile/EmailForm';
1212
import { CheckoutComplete } from './CheckoutComplete';
1313
import { CheckoutForm } from './CheckoutForm';
@@ -16,6 +16,9 @@ export const CheckoutPage = (props: __experimental_CheckoutProps) => {
1616
const { translateError } = useLocalizations();
1717
const { planId, planPeriod, subscriberType, onSubscriptionComplete } = props;
1818
const { setIsOpen, isOpen } = useDrawerContext();
19+
const prefersReducedMotion = usePrefersReducedMotion();
20+
const { animations: layoutAnimations } = useAppearance().parsedLayout;
21+
const isMotionSafe = !prefersReducedMotion && layoutAnimations === true;
1922

2023
const { checkout, isLoading, invalidate, revalidate, updateCheckout, errors } = useCheckout({
2124
planId,
@@ -49,7 +52,12 @@ export const CheckoutPage = (props: __experimental_CheckoutProps) => {
4952

5053
if (checkout) {
5154
if (checkout?.status === 'completed') {
52-
return <CheckoutComplete checkout={checkout} />;
55+
return (
56+
<CheckoutComplete
57+
checkout={checkout}
58+
isMotionSafe={isMotionSafe}
59+
/>
60+
);
5361
}
5462

5563
return (

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -313,11 +313,11 @@ const Header = React.forwardRef<HTMLDivElement, HeaderProps>(({ title, children,
313313
* Drawer.Body
314314
* -----------------------------------------------------------------------------------------------*/
315315

316-
interface BodyProps {
316+
interface BodyProps extends React.HTMLAttributes<HTMLDivElement> {
317317
children: React.ReactNode;
318318
}
319319

320-
const Body = React.forwardRef<HTMLDivElement, BodyProps>(({ children }, ref) => {
320+
const Body = React.forwardRef<HTMLDivElement, BodyProps>(({ children, ...props }, ref) => {
321321
return (
322322
<Box
323323
ref={ref}
@@ -329,6 +329,7 @@ const Body = React.forwardRef<HTMLDivElement, BodyProps>(({ children }, ref) =>
329329
overflowY: 'auto',
330330
overflowX: 'hidden',
331331
}}
332+
{...props}
332333
>
333334
{children}
334335
</Box>
@@ -339,12 +340,12 @@ const Body = React.forwardRef<HTMLDivElement, BodyProps>(({ children }, ref) =>
339340
* Drawer.Footer
340341
* -----------------------------------------------------------------------------------------------*/
341342

342-
interface FooterProps {
343+
interface FooterProps extends React.HTMLAttributes<HTMLDivElement> {
343344
children?: React.ReactNode;
344345
sx?: ThemableCssProp;
345346
}
346347

347-
const Footer = React.forwardRef<HTMLDivElement, FooterProps>(({ children, sx }, ref) => {
348+
const Footer = React.forwardRef<HTMLDivElement, FooterProps>(({ children, sx, ...props }, ref) => {
348349
return (
349350
<Box
350351
ref={ref}
@@ -367,6 +368,7 @@ const Footer = React.forwardRef<HTMLDivElement, FooterProps>(({ children, sx },
367368
}),
368369
sx,
369370
]}
371+
{...props}
370372
>
371373
{children}
372374
</Box>

0 commit comments

Comments
 (0)