Skip to content

Commit 5601767

Browse files
feat(clerk-js): Pass appearance variables to stripe elements. (#5346)
1 parent ed30d66 commit 5601767

File tree

6 files changed

+225
-5
lines changed

6 files changed

+225
-5
lines changed

.changeset/many-forks-burn.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Pass appearance variables to stripe elements.

packages/clerk-js/bundlewatch.config.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
{ "path": "./dist/waitlist*.js", "maxSize": "1.3KB" },
2121
{ "path": "./dist/keylessPrompt*.js", "maxSize": "5.9KB" },
2222
{ "path": "./dist/pricingTable*.js", "maxSize": "5KB" },
23-
{ "path": "./dist/checkout*.js", "maxSize": "8.8KB" },
23+
{ "path": "./dist/checkout*.js", "maxSize": "9KB" },
2424
{ "path": "./dist/up-billing-page*.js", "maxSize": "1KB" }
2525
]
2626
}

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

+25-4
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,15 @@ import type {
77
ClerkRuntimeError,
88
} from '@clerk/types';
99
import { Elements, PaymentElement, useElements, useStripe } from '@stripe/react-stripe-js';
10-
import type { Stripe } from '@stripe/stripe-js';
10+
import type { Appearance as StripeAppearance, Stripe } from '@stripe/stripe-js';
1111
import { useCallback, useEffect, useMemo, useState } from 'react';
1212

13-
import { Box, Button, Col, descriptors, Flex, Form, Icon, Text } from '../../customizables';
13+
import { Box, Button, Col, descriptors, Flex, Form, Icon, Text, useAppearance } from '../../customizables';
1414
import { Alert, Disclosure, Divider, Drawer, LineItems, Select, SelectButton, SelectOptionList } from '../../elements';
1515
import { useFetch } from '../../hooks';
1616
import { ArrowUpDown, CreditCard } from '../../icons';
1717
import { animations } from '../../styledSystem';
18-
import { handleError } from '../../utils';
18+
import { handleError, normalizeColorString } from '../../utils';
1919

2020
export const CheckoutForm = ({
2121
stripe,
@@ -27,6 +27,27 @@ export const CheckoutForm = ({
2727
onCheckoutComplete: (checkout: __experimental_CommerceCheckoutResource) => void;
2828
}) => {
2929
const { plan, planPeriod, totals } = checkout;
30+
const { colors, fontWeights, fontSizes, radii, space } = useAppearance().parsedInternalTheme;
31+
const elementsAppearance: StripeAppearance = {
32+
variables: {
33+
colorPrimary: normalizeColorString(colors.$primary500),
34+
colorBackground: normalizeColorString(colors.$colorInputBackground),
35+
colorText: normalizeColorString(colors.$colorText),
36+
colorTextSecondary: normalizeColorString(colors.$colorTextSecondary),
37+
colorSuccess: normalizeColorString(colors.$success500),
38+
colorDanger: normalizeColorString(colors.$danger500),
39+
colorWarning: normalizeColorString(colors.$warning500),
40+
fontWeightNormal: fontWeights.$normal.toString(),
41+
fontWeightMedium: fontWeights.$medium.toString(),
42+
fontWeightBold: fontWeights.$bold.toString(),
43+
fontSizeXl: fontSizes.$xl,
44+
fontSizeLg: fontSizes.$lg,
45+
fontSizeSm: fontSizes.$md,
46+
fontSizeXs: fontSizes.$sm,
47+
borderRadius: radii.$md,
48+
spacingUnit: space.$1,
49+
},
50+
};
3051
return (
3152
<Drawer.Body>
3253
<Box
@@ -77,7 +98,7 @@ export const CheckoutForm = ({
7798
{stripe && (
7899
<Elements
79100
stripe={stripe}
80-
options={{ clientSecret: checkout.externalClientSecret }}
101+
options={{ clientSecret: checkout.externalClientSecret, appearance: elementsAppearance }}
81102
>
82103
<CheckoutFormElements
83104
checkout={checkout}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { normalizeColorString } from '../normalizeColorString';
2+
3+
describe('normalizeColorString', () => {
4+
beforeEach(() => {
5+
jest.spyOn(console, 'warn').mockImplementation(() => {}) as jest.Mock;
6+
});
7+
8+
afterEach(() => {
9+
jest.clearAllMocks();
10+
});
11+
12+
// Hex color tests
13+
test('should keep 3-char hex colors unchanged', () => {
14+
expect(normalizeColorString('#123')).toBe('#123');
15+
expect(console.warn).not.toHaveBeenCalled();
16+
});
17+
18+
test('should keep 6-char hex colors unchanged', () => {
19+
expect(normalizeColorString('#123456')).toBe('#123456');
20+
expect(console.warn).not.toHaveBeenCalled();
21+
});
22+
23+
test('should remove alpha from 4-char hex colors', () => {
24+
expect(normalizeColorString('#123F')).toBe('#123');
25+
expect(console.warn).not.toHaveBeenCalled();
26+
});
27+
28+
test('should remove alpha from 8-char hex colors', () => {
29+
expect(normalizeColorString('#12345678')).toBe('#123456');
30+
expect(console.warn).not.toHaveBeenCalled();
31+
});
32+
33+
test('should warn for invalid hex formats but return the original', () => {
34+
expect(normalizeColorString('#12')).toBe('#12');
35+
expect(console.warn).toHaveBeenCalledTimes(1);
36+
37+
(console.warn as jest.Mock).mockClear();
38+
expect(normalizeColorString('#12345')).toBe('#12345');
39+
expect(console.warn).toHaveBeenCalledTimes(1);
40+
});
41+
42+
// RGB color tests
43+
test('should keep rgb format unchanged but normalize whitespace', () => {
44+
expect(normalizeColorString('rgb(255, 0, 0)')).toBe('rgb(255, 0, 0)');
45+
expect(console.warn).not.toHaveBeenCalled();
46+
});
47+
48+
test('should convert rgba to rgb', () => {
49+
expect(normalizeColorString('rgba(255, 0, 0, 0.5)')).toBe('rgb(255, 0, 0)');
50+
expect(console.warn).not.toHaveBeenCalled();
51+
});
52+
53+
test('should handle rgb with whitespace variations', () => {
54+
expect(normalizeColorString('rgb(255,0,0)')).toBe('rgb(255, 0, 0)');
55+
expect(normalizeColorString('rgb( 255 , 0 , 0 )')).toBe('rgb(255, 0, 0)');
56+
expect(console.warn).not.toHaveBeenCalled();
57+
});
58+
59+
// HSL color tests
60+
test('should keep hsl format unchanged but normalize whitespace', () => {
61+
expect(normalizeColorString('hsl(120, 100%, 50%)')).toBe('hsl(120, 100%, 50%)');
62+
expect(console.warn).not.toHaveBeenCalled();
63+
});
64+
65+
test('should convert hsla to hsl', () => {
66+
expect(normalizeColorString('hsla(120, 100%, 50%, 0.8)')).toBe('hsl(120, 100%, 50%)');
67+
expect(console.warn).not.toHaveBeenCalled();
68+
});
69+
70+
test('should handle hsl with whitespace variations', () => {
71+
expect(normalizeColorString('hsl(120,100%,50%)')).toBe('hsl(120, 100%, 50%)');
72+
expect(normalizeColorString('hsl( 120 , 100% , 50% )')).toBe('hsl(120, 100%, 50%)');
73+
expect(console.warn).not.toHaveBeenCalled();
74+
});
75+
76+
// Warning tests for invalid inputs
77+
test('should warn for invalid color formats but return the original', () => {
78+
expect(normalizeColorString('')).toBe('');
79+
expect(console.warn).toHaveBeenCalledTimes(1);
80+
81+
(console.warn as jest.Mock).mockClear();
82+
expect(normalizeColorString('invalid')).toBe('invalid');
83+
expect(console.warn).toHaveBeenCalledTimes(1);
84+
85+
(console.warn as jest.Mock).mockClear();
86+
expect(normalizeColorString('rgb(255,0)')).toBe('rgb(255,0)');
87+
expect(console.warn).toHaveBeenCalledTimes(1);
88+
});
89+
90+
test('should warn for non-string inputs but return the original or empty string', () => {
91+
expect(normalizeColorString(null as any)).toBe('');
92+
expect(console.warn).toHaveBeenCalledTimes(1);
93+
94+
(console.warn as jest.Mock).mockClear();
95+
expect(normalizeColorString(123 as any)).toBe(123 as any);
96+
expect(console.warn).toHaveBeenCalledTimes(1);
97+
});
98+
99+
// Edge cases
100+
test('should handle trimming whitespace', () => {
101+
expect(normalizeColorString(' #123 ')).toBe('#123');
102+
expect(normalizeColorString('\n rgb(255, 0, 0) \t')).toBe('rgb(255, 0, 0)');
103+
expect(console.warn).not.toHaveBeenCalled();
104+
});
105+
});

packages/clerk-js/src/ui/utils/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,4 @@ export * from './colorOptionToHslaScale';
2525
export * from './createCustomMenuItems';
2626
export * from './usernameUtils';
2727
export * from './web3CallbackErrorHandler';
28+
export * from './normalizeColorString';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Normalizes color format strings by removing alpha values if present
3+
* Handles conversions between:
4+
* - Hex: #RGB, #RGBA, #RRGGBB, #RRGGBBAA → #RGB or #RRGGBB
5+
* - RGB: rgb(r, g, b), rgba(r, g, b, a) → rgb(r, g, b)
6+
* - HSL: hsl(h, s%, l%), hsla(h, s%, l%, a) → hsl(h, s%, l%)
7+
*
8+
* @param colorString - The color string to normalize
9+
* @returns The normalized color string without alpha components, or the original string if invalid
10+
*/
11+
export function normalizeColorString(colorString: string): string {
12+
if (!colorString || typeof colorString !== 'string') {
13+
console.warn('Invalid input: color string must be a non-empty string');
14+
return colorString || '';
15+
}
16+
17+
const trimmed = colorString.trim();
18+
19+
// Handle empty strings
20+
if (trimmed === '') {
21+
console.warn('Invalid input: color string cannot be empty');
22+
return '';
23+
}
24+
25+
// Handle hex colors
26+
if (trimmed.startsWith('#')) {
27+
// Validate hex format
28+
if (!/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/.test(trimmed)) {
29+
console.warn(`Invalid hex color format: ${colorString}`);
30+
return trimmed;
31+
}
32+
33+
// #RGBA format (4 chars)
34+
if (trimmed.length === 5) {
35+
return '#' + trimmed.slice(1, 4);
36+
}
37+
// #RRGGBBAA format (9 chars)
38+
if (trimmed.length === 9) {
39+
return '#' + trimmed.slice(1, 7);
40+
}
41+
// Regular hex formats (#RGB, #RRGGBB)
42+
return trimmed;
43+
}
44+
45+
// Handle rgb/rgba
46+
if (/^rgba?\(/.test(trimmed)) {
47+
// Extract and normalize rgb values
48+
const rgbMatch = trimmed.match(/^rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/);
49+
if (rgbMatch) {
50+
// Already in rgb format, normalize whitespace
51+
return `rgb(${rgbMatch[1]}, ${rgbMatch[2]}, ${rgbMatch[3]})`;
52+
}
53+
54+
// Extract and normalize rgba values
55+
const rgbaMatch = trimmed.match(/^rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)\s*\)$/);
56+
if (rgbaMatch) {
57+
// Convert rgba to rgb, normalize whitespace
58+
return `rgb(${rgbaMatch[1]}, ${rgbaMatch[2]}, ${rgbaMatch[3]})`;
59+
}
60+
61+
console.warn(`Invalid RGB/RGBA format: ${colorString}`);
62+
return trimmed;
63+
}
64+
65+
// Handle hsl/hsla
66+
if (/^hsla?\(/.test(trimmed)) {
67+
// Extract and normalize hsl values
68+
const hslMatch = trimmed.match(/^hsl\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*\)$/);
69+
if (hslMatch) {
70+
// Already in hsl format, normalize whitespace
71+
return `hsl(${hslMatch[1]}, ${hslMatch[2]}%, ${hslMatch[3]}%)`;
72+
}
73+
74+
// Extract and normalize hsla values
75+
const hslaMatch = trimmed.match(/^hsla\(\s*(\d+)\s*,\s*(\d+)%\s*,\s*(\d+)%\s*,\s*([\d.]+)\s*\)$/);
76+
if (hslaMatch) {
77+
// Convert hsla to hsl, normalize whitespace
78+
return `hsl(${hslaMatch[1]}, ${hslaMatch[2]}%, ${hslaMatch[3]}%)`;
79+
}
80+
81+
console.warn(`Invalid HSL/HSLA format: ${colorString}`);
82+
return trimmed;
83+
}
84+
85+
// If we reach here, the input is not a recognized color format
86+
console.warn(`Unrecognized color format: ${colorString}`);
87+
return trimmed;
88+
}

0 commit comments

Comments
 (0)