Skip to content

Commit

Permalink
Refactor the captcha properties to exist directly under the appearance
Browse files Browse the repository at this point in the history
  • Loading branch information
anagstef committed Feb 26, 2025
1 parent 6008795 commit 69b7ff0
Show file tree
Hide file tree
Showing 9 changed files with 62 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .changeset/fluffy-hairs-thank.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
'@clerk/types': minor
---

Introduce layout configuration options for the appearance of the Turnstile widget
Introduce the `captcha` appearance property for the CAPTCHA widget
5 changes: 0 additions & 5 deletions .changeset/small-grapes-hunt.md

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { Card, withCardStateProvider } from '../../elements';
import { Route, Switch } from '../../router';

const BlankCard = withCardStateProvider(() => {
const { parsedLayout } = useAppearance();
const { parsedCaptcha } = useAppearance();
const { locale } = useLocalizations();
const captchaTheme = parsedLayout?.captchaTheme;
const captchaSize = parsedLayout?.captchaSize;
const captchaLanguage = parsedLayout?.captchaLanguage || locale;
const captchaTheme = parsedCaptcha?.theme;
const captchaSize = parsedCaptcha?.size;
const captchaLanguage = parsedCaptcha?.language || locale;

return (
<Card.Root>
Expand Down
28 changes: 22 additions & 6 deletions packages/clerk-js/src/ui/customizables/parseAppearance.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { fastDeepMergeAndReplace } from '@clerk/shared/utils';
import type { Appearance, DeepPartial, Elements, Layout, Theme } from '@clerk/types';
import type { Appearance, CaptchaAppearanceOptions, DeepPartial, Elements, Layout, Theme } from '@clerk/types';

import { createInternalTheme, defaultInternalTheme } from '../foundations';
import { polishedAppearance } from '../polishedAppearance';
Expand All @@ -16,8 +16,12 @@ import {
export type ParsedElements = Elements[];
export type ParsedInternalTheme = InternalTheme;
export type ParsedLayout = Required<Layout>;
export type ParsedCaptcha = Required<CaptchaAppearanceOptions>;

type PublicAppearanceTopLevelKey = keyof Omit<Appearance, 'baseTheme' | 'elements' | 'layout' | 'variables'>;
type PublicAppearanceTopLevelKey = keyof Omit<
Appearance,
'baseTheme' | 'elements' | 'layout' | 'variables' | 'captcha'
>;

export type AppearanceCascade = {
globalAppearance?: Appearance;
Expand All @@ -29,6 +33,7 @@ export type ParsedAppearance = {
parsedElements: ParsedElements;
parsedInternalTheme: ParsedInternalTheme;
parsedLayout: ParsedLayout;
parsedCaptcha: ParsedCaptcha;
};

const defaultLayout: ParsedLayout = {
Expand All @@ -44,9 +49,12 @@ const defaultLayout: ParsedLayout = {
shimmer: true,
animations: true,
unsafe_disableDevelopmentModeWarnings: false,
captchaTheme: 'auto',
captchaSize: 'normal',
captchaLanguage: '',
};

const defaultCaptchaOptions: ParsedCaptcha = {
theme: 'auto',
size: 'normal',
language: '',
};

/**
Expand All @@ -66,6 +74,7 @@ export const parseAppearance = (cascade: AppearanceCascade): ParsedAppearance =>

const parsedInternalTheme = parseVariables(appearanceList);
const parsedLayout = parseLayout(appearanceList);
const parsedCaptcha = parseCaptcha(appearanceList);

if (
!appearanceList.find(a => {
Expand All @@ -86,7 +95,7 @@ export const parseAppearance = (cascade: AppearanceCascade): ParsedAppearance =>
return res;
}),
);
return { parsedElements, parsedInternalTheme, parsedLayout };
return { parsedElements, parsedInternalTheme, parsedLayout, parsedCaptcha };
};

const expand = (theme: Theme | undefined, cascade: any[]) => {
Expand All @@ -109,6 +118,13 @@ const parseLayout = (appearanceList: Appearance[]) => {
return { ...defaultLayout, ...appearanceList.reduce((acc, appearance) => ({ ...acc, ...appearance.layout }), {}) };
};

const parseCaptcha = (appearanceList: Appearance[]) => {
return {
...defaultCaptchaOptions,
...appearanceList.reduce((acc, appearance) => ({ ...acc, ...appearance.captcha }), {}),
};
};

const parseVariables = (appearances: Appearance[]) => {
const res = {} as DeepPartial<InternalTheme>;
fastDeepMergeAndReplace({ ...defaultInternalTheme }, res);
Expand Down
8 changes: 4 additions & 4 deletions packages/clerk-js/src/ui/elements/CaptchaElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ export const CaptchaElement = () => {
const maxHeightValueRef = useRef('0');
const minHeightValueRef = useRef('unset');
const marginBottomValueRef = useRef('unset');
const { parsedLayout } = useAppearance();
const { parsedCaptcha } = useAppearance();
const { locale } = useLocalizations();
const captchaTheme = parsedLayout?.captchaTheme;
const captchaSize = parsedLayout?.captchaSize;
const captchaLanguage = parsedLayout?.captchaLanguage || locale;
const captchaTheme = parsedCaptcha?.theme;
const captchaSize = parsedCaptcha?.size;
const captchaLanguage = parsedCaptcha?.language || locale;

useEffect(() => {
if (!elementRef.current) return;
Expand Down
34 changes: 20 additions & 14 deletions packages/clerk-js/src/utils/captcha/turnstile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { waitForElement } from '@clerk/shared/dom';
import { loadScript } from '@clerk/shared/loadScript';
import type { CaptchaWidgetType, Layout } from '@clerk/types';
import type { CaptchaAppearanceOptions, CaptchaWidgetType } from '@clerk/types';

import { CAPTCHA_ELEMENT_ID, CAPTCHA_INVISIBLE_CLASSNAME } from './constants';
import type { CaptchaOptions } from './types';
Expand All @@ -9,6 +9,12 @@ import type { CaptchaOptions } from './types';
// CF docs: https://developers.cloudflare.com/turnstile/get-started/client-side-rendering/#disable-implicit-rendering
const CLOUDFLARE_TURNSTILE_ORIGINAL_URL = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';

type CaptchaAttributes = {
theme?: RenderOptions['theme'];
language?: RenderOptions['language'];
size: RenderOptions['size'];
};

interface RenderOptions {
/**
* Every widget has a sitekey. This sitekey is associated with the corresponding widget configuration and is created upon the widget creation.
Expand Down Expand Up @@ -63,19 +69,19 @@ interface RenderOptions {
* The default is auto, which respects the user preference. This can be forced to light or dark by setting the theme accordingly.
* @default 'auto'
*/
theme?: Layout['captchaTheme'];
theme?: CaptchaAppearanceOptions['theme'];
/**
* The widget size. Can take the following values: normal, flexible, compact.
* @default 'normal'
*/
size?: Layout['captchaSize'];
size?: CaptchaAppearanceOptions['size'];
/**
* Language to display, must be either: auto (default) to use the language that the visitor has chosen,
* or an ISO 639-1 two-letter language code (e.g. en) or language and country code (e.g. en-US).
* Refer to the list of supported languages for more information.
* https://developers.cloudflare.com/turnstile/reference/supported-languages
*/
language?: string;
language?: CaptchaAppearanceOptions['language'];
/**
* A custom value that can be used to differentiate widgets under the same sitekey
* in analytics and which is returned upon validation. This can only contain up to
Expand Down Expand Up @@ -127,10 +133,10 @@ async function loadCaptchaFromCloudflareURL() {
}
}

function getCaptchaAttibutesFromElemenet(element: HTMLElement) {
const theme = element.getAttribute('data-cl-theme');
const language = element.getAttribute('data-cl-language');
const size = element.getAttribute('data-cl-size');
function getCaptchaAttibutesFromElemenet(element: HTMLElement): CaptchaAttributes {
const theme = (element.getAttribute('data-cl-theme') as RenderOptions['theme']) || undefined;
const language = (element.getAttribute('data-cl-language') as RenderOptions['language']) || undefined;
const size = (element.getAttribute('data-cl-size') as RenderOptions['size']) || undefined;

return { theme, language, size };
}
Expand Down Expand Up @@ -170,9 +176,9 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => {
const modalContainderEl = await waitForElement(modalContainerQuerySelector);
if (modalContainderEl) {
const { theme, language, size } = getCaptchaAttibutesFromElemenet(modalContainderEl);
captchaTheme = theme as RenderOptions['theme'];
captchaLanguage = language as RenderOptions['language'];
captchaSize = size as RenderOptions['size'];
captchaTheme = theme;
captchaLanguage = language;
captchaSize = size;
}
}

Expand All @@ -184,9 +190,9 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => {
widgetContainerQuerySelector = `#${CAPTCHA_ELEMENT_ID}`;
visibleDiv.style.maxHeight = '0'; // This is to prevent the layout shift when the render method is called
const { theme, language, size } = getCaptchaAttibutesFromElemenet(visibleDiv);
captchaTheme = theme as RenderOptions['theme'];
captchaLanguage = language as RenderOptions['language'];
captchaSize = size as RenderOptions['size'];
captchaTheme = theme;
captchaLanguage = language;
captchaSize = size;
} else {
console.error(
'Cannot initialize Smart CAPTCHA widget because the `clerk-captcha` DOM element was not found; falling back to Invisible CAPTCHA widget. If you are using a custom flow, visit https://clerk.com/docs/custom-flows/bot-sign-up-protection for instructions',
Expand Down
3 changes: 0 additions & 3 deletions packages/themes/src/themes/dark.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,4 @@ export const dark = experimental_createTheme({
'--cl-screen': '#111111',
},
},
layout: {
captchaTheme: 'dark',
},
});
3 changes: 0 additions & 3 deletions packages/themes/src/themes/shadesOfPurple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,4 @@ export const shadesOfPurple = experimental_createTheme({
colorInputText: '#a1fdfe',
colorShimmer: 'rgba(161,253,254,0.36)',
},
layout: {
captchaTheme: 'dark',
},
});
14 changes: 11 additions & 3 deletions packages/types/src/appearance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -551,6 +551,12 @@ export type Theme = {
* Eg: `formButtonPrimary__loading: { backgroundColor: 'gray' }`
*/
elements?: Elements;
/**
* The appearance of the CAPTCHA widget.
* This will be used to style the CAPTCHA widget.
* Eg: `theme: 'dark'`
*/
captcha?: CaptchaAppearanceOptions;
};

export type Layout = {
Expand Down Expand Up @@ -630,22 +636,24 @@ export type Layout = {
* @default false
*/
unsafe_disableDevelopmentModeWarnings?: boolean;
};

export type CaptchaAppearanceOptions = {
/**
* The widget theme. Can take the following values: light, dark, auto.
* @default 'auto'
*/
captchaTheme?: 'auto' | 'light' | 'dark';
theme?: 'auto' | 'light' | 'dark';
/**
* The widget size. Can take the following values: normal, flexible, compact.
* @default 'normal'
*/
captchaSize?: 'normal' | 'flexible' | 'compact';
size?: 'normal' | 'flexible' | 'compact';
/**
* Language to display, must be either: auto (default) to use the language that the visitor has chosen, or an ISO 639-1 two-letter language code (e.g. en) or language and country code (e.g. en-US).
* Refer to the list of supported languages for more information: https://developers.cloudflare.com/turnstile/reference/supported-languages
*/
captchaLanguage?: string;
language?: string;
};

export type SignInTheme = Theme;
Expand Down

0 comments on commit 69b7ff0

Please sign in to comment.