Skip to content

Commit d9f265f

Browse files
authoredApr 17, 2024··
feat(clerk-js): Fallback to invisible CAPTCHA if the element is not found (#3191)
* feat(clerk-js): Fallback to invisible CAPTCHA if the element to render to is not found in the DOM * fix(clerk-js): Fix error wording * fix(clerk-js): Add comment to explain the captcha fallback mechanism * fix(clerk-js): Use the invisible key when DOM node is missing
1 parent beac05f commit d9f265f

File tree

6 files changed

+51
-14
lines changed

6 files changed

+51
-14
lines changed
 

‎.changeset/itchy-onions-rush.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
'@clerk/types': patch
4+
---
5+
6+
Fallback to invisible CAPTCHA if the element to render to is not found in the DOM

‎packages/clerk-js/src/core/resources/DisplayConfig.ts

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource
2121
branded!: boolean;
2222
captchaPublicKey: string | null = null;
2323
captchaWidgetType: CaptchaWidgetType = null;
24+
captchaPublicKeyInvisible: string | null = null;
2425
homeUrl!: string;
2526
instanceEnvironmentType!: string;
2627
faviconImageUrl!: string;
@@ -68,6 +69,7 @@ export class DisplayConfig extends BaseResource implements DisplayConfigResource
6869
this.branded = data.branded;
6970
this.captchaPublicKey = data.captcha_public_key;
7071
this.captchaWidgetType = data.captcha_widget_type;
72+
this.captchaPublicKeyInvisible = data.captcha_public_key_invisible;
7173
this.supportEmail = data.support_email || '';
7274
this.clerkJSVersion = data.clerk_js_version;
7375
this.organizationProfileUrl = data.organization_profile_url;

‎packages/clerk-js/src/core/resources/SignUp.ts

+7-3
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,19 @@ export class SignUp extends BaseResource implements SignUpResource {
6868

6969
create = async (params: SignUpCreateParams): Promise<SignUpResource> => {
7070
const paramsWithCaptcha: Record<string, unknown> = params;
71-
const { captchaSiteKey, canUseCaptcha, captchaURL, captchaWidgetType } = retrieveCaptchaInfo(SignUp.clerk);
71+
const { captchaSiteKey, canUseCaptcha, captchaURL, captchaWidgetType, captchaPublicKeyInvisible } =
72+
retrieveCaptchaInfo(SignUp.clerk);
7273

73-
if (canUseCaptcha && captchaSiteKey && captchaURL) {
74+
if (canUseCaptcha && captchaSiteKey && captchaURL && captchaPublicKeyInvisible) {
7475
try {
75-
paramsWithCaptcha.captchaToken = await getCaptchaToken({
76+
const { captchaToken, captchaWidgetTypeUsed } = await getCaptchaToken({
7677
siteKey: captchaSiteKey,
7778
widgetType: captchaWidgetType,
79+
invisibleSiteKey: captchaPublicKeyInvisible,
7880
scriptUrl: captchaURL,
7981
});
82+
paramsWithCaptcha.captchaToken = captchaToken;
83+
paramsWithCaptcha.captchaWidgetType = captchaWidgetTypeUsed;
8084
} catch (e) {
8185
if (e.captchaError) {
8286
paramsWithCaptcha.captchaError = e.captchaError;

‎packages/clerk-js/src/utils/captcha.ts

+32-10
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ interface RenderOptions {
3636
* @param errorCode string
3737
*/
3838
'error-callback'?: (errorCode: string) => void;
39+
/**
40+
* A JavaScript callback invoked when a given client/browser is not supported by the widget.
41+
*/
42+
'unsupported-callback'?: () => boolean;
3943
/**
4044
* Appearance controls when the widget is visible.
4145
* It can be always (default), execute, or interaction-only.
@@ -80,32 +84,46 @@ export async function loadCaptcha(url: string) {
8084
return window.turnstile;
8185
}
8286

87+
/*
88+
* How this function works:
89+
* The widgetType is either 'invisible' or 'smart'.
90+
* - If the widgetType is 'invisible', the captcha widget is rendered in a hidden div at the bottom of the body.
91+
* - If the widgetType is 'smart', the captcha widget is rendered in a div with the id 'clerk-captcha'. If the div does
92+
* not exist, the invisibleSiteKey is used as a fallback and the widget is rendered in a hidden div at the bottom of the body.
93+
*/
8394
export const getCaptchaToken = async (captchaOptions: {
8495
siteKey: string;
8596
scriptUrl: string;
8697
widgetType: CaptchaWidgetType;
98+
invisibleSiteKey: string;
8799
}) => {
88-
const { siteKey: sitekey, scriptUrl, widgetType } = captchaOptions;
100+
const { siteKey, scriptUrl, widgetType, invisibleSiteKey } = captchaOptions;
89101
let captchaToken = '',
90102
id = '';
91-
const invisibleWidget = !widgetType || widgetType === 'invisible';
103+
let invisibleWidget = !widgetType || widgetType === 'invisible';
104+
let turnstileSiteKey = siteKey;
92105

93106
let widgetDiv: HTMLElement | null = null;
94107

95-
if (invisibleWidget) {
108+
const createInvisibleDOMElement = () => {
96109
const div = document.createElement('div');
97110
div.classList.add(CAPTCHA_INVISIBLE_CLASSNAME);
98111
document.body.appendChild(div);
99-
widgetDiv = div;
112+
return div;
113+
};
114+
115+
if (invisibleWidget) {
116+
widgetDiv = createInvisibleDOMElement();
100117
} else {
101118
const visibleDiv = document.getElementById(CAPTCHA_ELEMENT_ID);
102119
if (visibleDiv) {
103120
visibleDiv.style.display = 'block';
104121
widgetDiv = visibleDiv;
105122
} else {
106-
throw {
107-
captchaError: 'Element to render the captcha not found',
108-
};
123+
console.error('Captcha DOM element not found. Using invisible captcha widget.');
124+
widgetDiv = createInvisibleDOMElement();
125+
invisibleWidget = true;
126+
turnstileSiteKey = invisibleSiteKey;
109127
}
110128
}
111129

@@ -117,8 +135,8 @@ export const getCaptchaToken = async (captchaOptions: {
117135
return new Promise((resolve, reject) => {
118136
try {
119137
const id = captcha.render(invisibleWidget ? `.${CAPTCHA_INVISIBLE_CLASSNAME}` : `#${CAPTCHA_ELEMENT_ID}`, {
120-
sitekey,
121-
appearance: widgetType === 'always_visible' ? 'always' : 'interaction-only',
138+
sitekey: turnstileSiteKey,
139+
appearance: 'interaction-only',
122140
retry: 'never',
123141
'refresh-expired': 'auto',
124142
callback: function (token: string) {
@@ -139,6 +157,10 @@ export const getCaptchaToken = async (captchaOptions: {
139157
}
140158
reject([errorCodes.join(','), id]);
141159
},
160+
'unsupported-callback': function () {
161+
reject(['This browser is not supported by the CAPTCHA.', id]);
162+
return true;
163+
},
142164
});
143165
} catch (e) {
144166
/**
@@ -171,5 +193,5 @@ export const getCaptchaToken = async (captchaOptions: {
171193
}
172194
}
173195

174-
return captchaToken;
196+
return { captchaToken, captchaWidgetTypeUsed: invisibleWidget ? 'invisible' : 'smart' };
175197
};

‎packages/clerk-js/src/utils/retrieveCaptchaInfo.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const retrieveCaptchaInfo = (clerk: Clerk) => {
77
return {
88
captchaSiteKey: _environment ? _environment.displayConfig.captchaPublicKey : null,
99
captchaWidgetType: _environment ? _environment.displayConfig.captchaWidgetType : null,
10+
captchaPublicKeyInvisible: _environment ? _environment.displayConfig.captchaPublicKeyInvisible : null,
1011
canUseCaptcha: _environment
1112
? _environment.userSettings.signUp.captcha_enabled &&
1213
clerk.isStandardBrowser &&

‎packages/types/src/displayConfig.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { DisplayThemeJSON } from './json';
22
import type { ClerkResource } from './resource';
33

44
export type PreferredSignInStrategy = 'password' | 'otp';
5-
export type CaptchaWidgetType = 'smart' | 'always_visible' | 'invisible' | null;
5+
export type CaptchaWidgetType = 'smart' | 'invisible' | null;
66

77
export interface DisplayConfigJSON {
88
object: 'display_config';
@@ -16,6 +16,7 @@ export interface DisplayConfigJSON {
1616
branded: boolean;
1717
captcha_public_key: string | null;
1818
captcha_widget_type: CaptchaWidgetType;
19+
captcha_public_key_invisible: string | null;
1920
home_url: string;
2021
instance_environment_type: string;
2122
logo_image_url: string;
@@ -46,6 +47,7 @@ export interface DisplayConfigResource extends ClerkResource {
4647
branded: boolean;
4748
captchaPublicKey: string | null;
4849
captchaWidgetType: CaptchaWidgetType;
50+
captchaPublicKeyInvisible: string | null;
4951
homeUrl: string;
5052
instanceEnvironmentType: string;
5153
logoImageUrl: string;

0 commit comments

Comments
 (0)
Please sign in to comment.