From ef61a8a81eb54531134fee06e185bd869a90ec5e Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Mon, 17 Feb 2025 17:51:57 +0200 Subject: [PATCH 01/12] feat(clerk-js): Introduce layout configuration options for Turnstile widget appearance --- .changeset/fluffy-hairs-thank.md | 6 +++ .../ui/components/BlankCaptchaModal/index.tsx | 15 ++++++- .../src/ui/customizables/parseAppearance.ts | 3 ++ .../src/ui/elements/CaptchaElement.tsx | 10 ++++- .../clerk-js/src/utils/captcha/turnstile.ts | 45 ++++++++++++++++++- packages/types/src/appearance.ts | 16 +++++++ 6 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 .changeset/fluffy-hairs-thank.md diff --git a/.changeset/fluffy-hairs-thank.md b/.changeset/fluffy-hairs-thank.md new file mode 100644 index 00000000000..9fa6413ce1a --- /dev/null +++ b/.changeset/fluffy-hairs-thank.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': minor +'@clerk/types': minor +--- + +Introduce layout configuration options for the appearance of the Turnstile widget diff --git a/packages/clerk-js/src/ui/components/BlankCaptchaModal/index.tsx b/packages/clerk-js/src/ui/components/BlankCaptchaModal/index.tsx index 2e6c240f34c..065c74ae10a 100644 --- a/packages/clerk-js/src/ui/components/BlankCaptchaModal/index.tsx +++ b/packages/clerk-js/src/ui/components/BlankCaptchaModal/index.tsx @@ -1,12 +1,23 @@ -import { Flow } from '../../customizables'; +import { Flow, useAppearance, useLocalizations } from '../../customizables'; import { Card, withCardStateProvider } from '../../elements'; import { Route, Switch } from '../../router'; const BlankCard = withCardStateProvider(() => { + const { parsedLayout } = useAppearance(); + const { locale } = useLocalizations(); + const captchaTheme = parsedLayout?.captchaTheme; + const captchaSize = parsedLayout?.captchaSize; + const captchaLanguage = parsedLayout?.captchaLanguage || locale; + return ( -
+
); diff --git a/packages/clerk-js/src/ui/customizables/parseAppearance.ts b/packages/clerk-js/src/ui/customizables/parseAppearance.ts index 1c5f32c5526..a3c2d945778 100644 --- a/packages/clerk-js/src/ui/customizables/parseAppearance.ts +++ b/packages/clerk-js/src/ui/customizables/parseAppearance.ts @@ -44,6 +44,9 @@ const defaultLayout: ParsedLayout = { shimmer: true, animations: true, unsafe_disableDevelopmentModeWarnings: false, + captchaTheme: 'light', + captchaSize: 'normal', + captchaLanguage: 'en-US', }; /** diff --git a/packages/clerk-js/src/ui/elements/CaptchaElement.tsx b/packages/clerk-js/src/ui/elements/CaptchaElement.tsx index 7a5a5408f20..a5d64303e7c 100644 --- a/packages/clerk-js/src/ui/elements/CaptchaElement.tsx +++ b/packages/clerk-js/src/ui/elements/CaptchaElement.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef } from 'react'; import { CAPTCHA_ELEMENT_ID } from '../../utils/captcha'; -import { Box } from '../customizables'; +import { Box, useAppearance, useLocalizations } from '../customizables'; /** * This component uses a MutationObserver to listen for DOM changes made by our Turnstile logic, @@ -14,6 +14,11 @@ export const CaptchaElement = () => { const maxHeightValueRef = useRef('0'); const minHeightValueRef = useRef('unset'); const marginBottomValueRef = useRef('unset'); + const { parsedLayout } = useAppearance(); + const { locale } = useLocalizations(); + const captchaTheme = parsedLayout?.captchaTheme; + const captchaSize = parsedLayout?.captchaSize; + const captchaLanguage = parsedLayout?.captchaLanguage || locale; useEffect(() => { if (!elementRef.current) return; @@ -48,6 +53,9 @@ export const CaptchaElement = () => { minHeight: minHeightValueRef.current, marginBottom: marginBottomValueRef.current, }} + data-cl-theme={captchaTheme} + data-cl-size={captchaSize} + data-cl-language={captchaLanguage} /> ); }; diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index 1d2724cb07d..0f421fa41df 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -1,6 +1,6 @@ import { waitForElement } from '@clerk/shared/dom'; import { loadScript } from '@clerk/shared/loadScript'; -import type { CaptchaWidgetType } from '@clerk/types'; +import type { CaptchaWidgetType, Layout } from '@clerk/types'; import { CAPTCHA_ELEMENT_ID, CAPTCHA_INVISIBLE_CLASSNAME } from './constants'; import type { CaptchaOptions } from './types'; @@ -58,6 +58,23 @@ interface RenderOptions { * @default 'always' */ appearance?: 'always' | 'execute' | 'interaction-only'; + /** + * The widget theme. Can take the following values: light, dark, auto. + * The default is auto, which respects the user preference. This can be forced to light or dark by setting the theme accordingly. + */ + theme?: Layout['captchaTheme']; + /** + * The widget size. Can take the following values: normal, flexible, compact. + * The default is normal. + */ + size?: Layout['captchaSize']; + /** + * 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; } interface Turnstile { @@ -103,6 +120,14 @@ 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'); + + return { theme, language, size }; +} + /* * How this function works: * The widgetType is either 'invisible' or 'smart'. @@ -119,6 +144,9 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { let captchaToken = ''; let id = ''; let turnstileSiteKey = siteKey; + let captchaTheme: RenderOptions['theme']; + let captchaSize: RenderOptions['size']; + let captchaLanguage: RenderOptions['language']; let retries = 0; let widgetContainerQuerySelector: string | undefined; // The backend uses this to determine which Turnstile site-key was used in order to verify the token @@ -132,7 +160,13 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { captchaWidgetType = widgetType; widgetContainerQuerySelector = modalContainerQuerySelector; await openModal?.(); - await waitForElement(modalContainerQuerySelector); + 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']; + } } // smart widget with container provided by user @@ -142,6 +176,10 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { captchaWidgetType = 'smart'; 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']; } 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', @@ -166,6 +204,9 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { const id = captcha.render(widgetContainerQuerySelector, { sitekey: turnstileSiteKey, appearance: 'interaction-only', + theme: captchaTheme || 'auto', + size: captchaSize || 'normal', + language: captchaLanguage || 'en-US', retry: 'never', 'refresh-expired': 'auto', callback: function (token: string) { diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index 73d2e73b487..7aeea0ea892 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -630,6 +630,22 @@ export type Layout = { * @default false */ unsafe_disableDevelopmentModeWarnings?: boolean; + + /** + * The widget theme. Can take the following values: light, dark, auto. + * The default is light. + */ + captchaTheme?: 'auto' | 'light' | 'dark'; + /** + * The widget size. Can take the following values: normal, flexible, compact. + * The default is normal. + */ + captchaSize?: '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; }; export type SignInTheme = Theme; From 414dfb446002be6fdafd7e394b321163789b4294 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Tue, 18 Feb 2025 16:35:52 +0200 Subject: [PATCH 02/12] Use dark captcha for the dark and shadesOfPurple themes --- .changeset/small-grapes-hunt.md | 5 +++++ packages/themes/src/themes/dark.ts | 3 +++ packages/themes/src/themes/shadesOfPurple.ts | 3 +++ 3 files changed, 11 insertions(+) create mode 100644 .changeset/small-grapes-hunt.md diff --git a/.changeset/small-grapes-hunt.md b/.changeset/small-grapes-hunt.md new file mode 100644 index 00000000000..892080b31c9 --- /dev/null +++ b/.changeset/small-grapes-hunt.md @@ -0,0 +1,5 @@ +--- +'@clerk/themes': minor +--- + +Use dark theme for Turnstile in dark and shadesOfPurple themes. diff --git a/packages/themes/src/themes/dark.ts b/packages/themes/src/themes/dark.ts index 59f1f1cb7c2..af6a61e572e 100644 --- a/packages/themes/src/themes/dark.ts +++ b/packages/themes/src/themes/dark.ts @@ -21,4 +21,7 @@ export const dark = experimental_createTheme({ '--cl-screen': '#111111', }, }, + layout: { + captchaTheme: 'dark', + }, }); diff --git a/packages/themes/src/themes/shadesOfPurple.ts b/packages/themes/src/themes/shadesOfPurple.ts index 7099a2639e8..82ab6c74b39 100644 --- a/packages/themes/src/themes/shadesOfPurple.ts +++ b/packages/themes/src/themes/shadesOfPurple.ts @@ -10,4 +10,7 @@ export const shadesOfPurple = experimental_createTheme({ colorInputText: '#a1fdfe', colorShimmer: 'rgba(161,253,254,0.36)', }, + layout: { + captchaTheme: 'dark', + }, }); From 21ef19a6f9d3b79b318a504ae88caec3bab4a62a Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Wed, 19 Feb 2025 11:23:05 +0200 Subject: [PATCH 03/12] fix default captcha language --- packages/clerk-js/src/ui/customizables/parseAppearance.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/ui/customizables/parseAppearance.ts b/packages/clerk-js/src/ui/customizables/parseAppearance.ts index a3c2d945778..6039c7f1698 100644 --- a/packages/clerk-js/src/ui/customizables/parseAppearance.ts +++ b/packages/clerk-js/src/ui/customizables/parseAppearance.ts @@ -46,7 +46,7 @@ const defaultLayout: ParsedLayout = { unsafe_disableDevelopmentModeWarnings: false, captchaTheme: 'light', captchaSize: 'normal', - captchaLanguage: 'en-US', + captchaLanguage: '', }; /** From 521afc34f7a26b005d51398301957b70880c37bd Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Wed, 19 Feb 2025 11:53:59 +0200 Subject: [PATCH 04/12] use auto for language default --- packages/clerk-js/src/utils/captcha/turnstile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index 0f421fa41df..f2c84d0938c 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -206,7 +206,7 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { appearance: 'interaction-only', theme: captchaTheme || 'auto', size: captchaSize || 'normal', - language: captchaLanguage || 'en-US', + language: captchaLanguage || 'auto', retry: 'never', 'refresh-expired': 'auto', callback: function (token: string) { From 5ec09c5d7478333f5e769858268e6cdcd167c26b Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Thu, 20 Feb 2025 18:07:49 +0200 Subject: [PATCH 05/12] Update packages/clerk-js/src/utils/captcha/turnstile.ts Co-authored-by: Alex Carpenter --- packages/clerk-js/src/utils/captcha/turnstile.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index f2c84d0938c..eb3025dadde 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -61,6 +61,7 @@ interface RenderOptions { /** * The widget theme. Can take the following values: light, dark, auto. * 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']; /** From 08167d644f803d8a3a3441123b25ff1b82d3ba92 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Thu, 20 Feb 2025 18:07:55 +0200 Subject: [PATCH 06/12] Update packages/clerk-js/src/utils/captcha/turnstile.ts Co-authored-by: Alex Carpenter --- packages/clerk-js/src/utils/captcha/turnstile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index eb3025dadde..bee81865853 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -66,7 +66,7 @@ interface RenderOptions { theme?: Layout['captchaTheme']; /** * The widget size. Can take the following values: normal, flexible, compact. - * The default is normal. + * @default 'normal' */ size?: Layout['captchaSize']; /** From 080cbd94c10dc8f6df048d334e0d7b9deefc1c0c Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Thu, 20 Feb 2025 18:10:58 +0200 Subject: [PATCH 07/12] change default theme to 'auto' --- packages/clerk-js/src/ui/customizables/parseAppearance.ts | 2 +- packages/types/src/appearance.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/ui/customizables/parseAppearance.ts b/packages/clerk-js/src/ui/customizables/parseAppearance.ts index 6039c7f1698..009cef6ddca 100644 --- a/packages/clerk-js/src/ui/customizables/parseAppearance.ts +++ b/packages/clerk-js/src/ui/customizables/parseAppearance.ts @@ -44,7 +44,7 @@ const defaultLayout: ParsedLayout = { shimmer: true, animations: true, unsafe_disableDevelopmentModeWarnings: false, - captchaTheme: 'light', + captchaTheme: 'auto', captchaSize: 'normal', captchaLanguage: '', }; diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index 7aeea0ea892..73fa9e69e0f 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -633,12 +633,12 @@ export type Layout = { /** * The widget theme. Can take the following values: light, dark, auto. - * The default is light. + * @default 'auto' */ captchaTheme?: 'auto' | 'light' | 'dark'; /** * The widget size. Can take the following values: normal, flexible, compact. - * The default is normal. + * @default 'normal' */ captchaSize?: 'normal' | 'flexible' | 'compact'; /** From 55b6a53ea9182a3e26175582e658115054a591f4 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Thu, 20 Feb 2025 18:22:39 +0200 Subject: [PATCH 08/12] Adjust min-height of Turnstile widget if captcha size is `compact` --- packages/clerk-js/src/utils/captcha/turnstile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index bee81865853..01cbf238a19 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -227,7 +227,7 @@ export const getTurnstileToken = async (opts: CaptchaOptions) => { // We set the min-height to the height of the Turnstile widget // because the widget initially does a small layout shift // and then expands to the correct height - visibleWidget.style.minHeight = '68px'; + visibleWidget.style.minHeight = captchaSize === 'compact' ? '140px' : '68px'; visibleWidget.style.marginBottom = '1.5rem'; } } From 69b7ff0fc3d0e0057dd84065dab2f23eb137f573 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Wed, 26 Feb 2025 11:36:03 +0200 Subject: [PATCH 09/12] Refactor the captcha properties to exist directly under the appearance --- .changeset/fluffy-hairs-thank.md | 2 +- .changeset/small-grapes-hunt.md | 5 --- .../ui/components/BlankCaptchaModal/index.tsx | 8 ++--- .../src/ui/customizables/parseAppearance.ts | 28 +++++++++++---- .../src/ui/elements/CaptchaElement.tsx | 8 ++--- .../clerk-js/src/utils/captcha/turnstile.ts | 34 +++++++++++-------- packages/themes/src/themes/dark.ts | 3 -- packages/themes/src/themes/shadesOfPurple.ts | 3 -- packages/types/src/appearance.ts | 14 ++++++-- 9 files changed, 62 insertions(+), 43 deletions(-) delete mode 100644 .changeset/small-grapes-hunt.md diff --git a/.changeset/fluffy-hairs-thank.md b/.changeset/fluffy-hairs-thank.md index 9fa6413ce1a..e661f0c6496 100644 --- a/.changeset/fluffy-hairs-thank.md +++ b/.changeset/fluffy-hairs-thank.md @@ -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 diff --git a/.changeset/small-grapes-hunt.md b/.changeset/small-grapes-hunt.md deleted file mode 100644 index 892080b31c9..00000000000 --- a/.changeset/small-grapes-hunt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@clerk/themes': minor ---- - -Use dark theme for Turnstile in dark and shadesOfPurple themes. diff --git a/packages/clerk-js/src/ui/components/BlankCaptchaModal/index.tsx b/packages/clerk-js/src/ui/components/BlankCaptchaModal/index.tsx index 065c74ae10a..a1f35ba4b40 100644 --- a/packages/clerk-js/src/ui/components/BlankCaptchaModal/index.tsx +++ b/packages/clerk-js/src/ui/components/BlankCaptchaModal/index.tsx @@ -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 ( diff --git a/packages/clerk-js/src/ui/customizables/parseAppearance.ts b/packages/clerk-js/src/ui/customizables/parseAppearance.ts index 009cef6ddca..f01f8b4cb6a 100644 --- a/packages/clerk-js/src/ui/customizables/parseAppearance.ts +++ b/packages/clerk-js/src/ui/customizables/parseAppearance.ts @@ -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'; @@ -16,8 +16,12 @@ import { export type ParsedElements = Elements[]; export type ParsedInternalTheme = InternalTheme; export type ParsedLayout = Required; +export type ParsedCaptcha = Required; -type PublicAppearanceTopLevelKey = keyof Omit; +type PublicAppearanceTopLevelKey = keyof Omit< + Appearance, + 'baseTheme' | 'elements' | 'layout' | 'variables' | 'captcha' +>; export type AppearanceCascade = { globalAppearance?: Appearance; @@ -29,6 +33,7 @@ export type ParsedAppearance = { parsedElements: ParsedElements; parsedInternalTheme: ParsedInternalTheme; parsedLayout: ParsedLayout; + parsedCaptcha: ParsedCaptcha; }; const defaultLayout: ParsedLayout = { @@ -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: '', }; /** @@ -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 => { @@ -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[]) => { @@ -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; fastDeepMergeAndReplace({ ...defaultInternalTheme }, res); diff --git a/packages/clerk-js/src/ui/elements/CaptchaElement.tsx b/packages/clerk-js/src/ui/elements/CaptchaElement.tsx index a5d64303e7c..29e040c79fc 100644 --- a/packages/clerk-js/src/ui/elements/CaptchaElement.tsx +++ b/packages/clerk-js/src/ui/elements/CaptchaElement.tsx @@ -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; diff --git a/packages/clerk-js/src/utils/captcha/turnstile.ts b/packages/clerk-js/src/utils/captcha/turnstile.ts index 2688902c329..80c0df70526 100644 --- a/packages/clerk-js/src/utils/captcha/turnstile.ts +++ b/packages/clerk-js/src/utils/captcha/turnstile.ts @@ -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'; @@ -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. @@ -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 @@ -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 }; } @@ -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; } } @@ -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', diff --git a/packages/themes/src/themes/dark.ts b/packages/themes/src/themes/dark.ts index af6a61e572e..59f1f1cb7c2 100644 --- a/packages/themes/src/themes/dark.ts +++ b/packages/themes/src/themes/dark.ts @@ -21,7 +21,4 @@ export const dark = experimental_createTheme({ '--cl-screen': '#111111', }, }, - layout: { - captchaTheme: 'dark', - }, }); diff --git a/packages/themes/src/themes/shadesOfPurple.ts b/packages/themes/src/themes/shadesOfPurple.ts index 82ab6c74b39..7099a2639e8 100644 --- a/packages/themes/src/themes/shadesOfPurple.ts +++ b/packages/themes/src/themes/shadesOfPurple.ts @@ -10,7 +10,4 @@ export const shadesOfPurple = experimental_createTheme({ colorInputText: '#a1fdfe', colorShimmer: 'rgba(161,253,254,0.36)', }, - layout: { - captchaTheme: 'dark', - }, }); diff --git a/packages/types/src/appearance.ts b/packages/types/src/appearance.ts index 73fa9e69e0f..e086a68f729 100644 --- a/packages/types/src/appearance.ts +++ b/packages/types/src/appearance.ts @@ -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 = { @@ -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; From 2db9a5078000e53ad5dfd8d1044ad0d82922acd6 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Wed, 26 Feb 2025 11:55:14 +0200 Subject: [PATCH 10/12] add tests for parsedCaptcha --- .../__tests__/parseAppearance.test.tsx | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) diff --git a/packages/clerk-js/src/ui/customizables/__tests__/parseAppearance.test.tsx b/packages/clerk-js/src/ui/customizables/__tests__/parseAppearance.test.tsx index 9976b0fd17b..bc3982c82b0 100644 --- a/packages/clerk-js/src/ui/customizables/__tests__/parseAppearance.test.tsx +++ b/packages/clerk-js/src/ui/customizables/__tests__/parseAppearance.test.tsx @@ -380,3 +380,87 @@ describe('AppearanceProvider layout flows', () => { expect(result.current.parsedElements[0]['alert'].backgroundColor).toBe(themeBColor); }); }); + +describe('AppearanceProvider captcha', () => { + it('sets the parsedCaptcha correctly from the globalAppearance prop', () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useAppearance(), { wrapper }); + expect(result.current.parsedCaptcha.theme).toBe('dark'); + expect(result.current.parsedCaptcha.size).toBe('compact'); + expect(result.current.parsedCaptcha.language).toBe('el-GR'); + }); + + it('sets the parsedCaptcha correctly from the appearance prop', () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useAppearance(), { wrapper }); + expect(result.current.parsedCaptcha.theme).toBe('dark'); + expect(result.current.parsedCaptcha.size).toBe('compact'); + expect(result.current.parsedCaptcha.language).toBe('el-GR'); + }); + + it('sets the parsedLayout correctly from the globalAppearance and appearance prop', () => { + const wrapper = ({ children }) => ( + + {children} + + ); + + const { result } = renderHook(() => useAppearance(), { wrapper }); + expect(result.current.parsedCaptcha.theme).toBe('dark'); + expect(result.current.parsedCaptcha.size).toBe('compact'); + expect(result.current.parsedCaptcha.language).toBe('el-GR'); + }); + + it('uses the default values when no captcha property is passed', () => { + const wrapper = ({ children }) => {children}; + + const { result } = renderHook(() => useAppearance(), { wrapper }); + expect(result.current.parsedCaptcha.theme).toBe('auto'); + expect(result.current.parsedCaptcha.size).toBe('normal'); + expect(result.current.parsedCaptcha.language).toBe(''); + }); +}); From 8bf643b4d2cd13899d9d6da0e624bacf538f7325 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Wed, 26 Feb 2025 12:18:04 +0200 Subject: [PATCH 11/12] increase bundlewatch maxSize --- packages/clerk-js/bundlewatch.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index fe0ea8fb266..fd73a9957e9 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -2,7 +2,7 @@ "files": [ { "path": "./dist/clerk.js", "maxSize": "560kB" }, { "path": "./dist/clerk.browser.js", "maxSize": "75kB" }, - { "path": "./dist/clerk.headless.js", "maxSize": "48.2KB" }, + { "path": "./dist/clerk.headless.js", "maxSize": "48.3KB" }, { "path": "./dist/ui-common*.js", "maxSize": "89KB" }, { "path": "./dist/vendors*.js", "maxSize": "25KB" }, { "path": "./dist/coinbase*.js", "maxSize": "35.5KB" }, From 4a482c7758fdcd73fe4c4192c7be565e5c63ac13 Mon Sep 17 00:00:00 2001 From: Stefanos Anagnostou Date: Wed, 26 Feb 2025 15:06:09 +0200 Subject: [PATCH 12/12] Update .changeset/fluffy-hairs-thank.md --- .changeset/fluffy-hairs-thank.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/fluffy-hairs-thank.md b/.changeset/fluffy-hairs-thank.md index e661f0c6496..24f4eb8be20 100644 --- a/.changeset/fluffy-hairs-thank.md +++ b/.changeset/fluffy-hairs-thank.md @@ -3,4 +3,4 @@ '@clerk/types': minor --- -Introduce the `captcha` appearance property for the CAPTCHA widget +Introduce the `appearance.captcha` property for the CAPTCHA widget