@adobe-commerce/recaptcha 1.1.0-alpha-202603171457 → 1.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # @adobe-commerce/recaptcha
2
+
3
+ ## 1.1.0-beta.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d054150: ### Summary
8
+
9
+ - Replaces the `recaptchaV3Config` GraphQL query with `recaptchaFormConfig`, which supports both reCAPTCHA v3 and Enterprise
10
+ - Queries all form types in a single request using GraphQL aliases and normalizes the per-form response into the existing config shape
11
+ - Loads `enterprise.js` or `api.js` based on the detected `re_captcha_type` from the backend
12
+ - Uses `grecaptcha.enterprise.*` namespace for token generation and badge rendering when Enterprise is configured
13
+ - Skips forms with empty `website_key` (enabled but not usable)
14
+ - Warns and disables reCAPTCHA entirely if mixed types (v3 + Enterprise) are detected across forms
15
+ - Adds `[ReCaptcha]`-prefixed debug logging throughout the lifecycle (gated behind `enableLogger`)
16
+ - Renames all `V3`-suffixed types to generic names (`ReCaptchaModel`, `ReCaptchaResponse`, etc.)
17
+ - Updates README with Enterprise support documentation
package/README.md CHANGED
@@ -2,54 +2,105 @@
2
2
 
3
3
  ## Purpose
4
4
 
5
- This functionality is designed to prevent CAPTCHAs. It provides methods for detecting and bypassing CAPTCHAs, improving user experience and automating interactions with web services.
6
- `
5
+ This module integrates Google reCAPTCHA into Adobe Commerce storefronts. It supports both **reCAPTCHA v3** and **reCAPTCHA Enterprise**, providing methods for configuring, initializing, and verifying reCAPTCHA on protected forms.
7
6
 
8
- ## Methods
7
+ The module queries the `recaptchaFormConfig` GraphQL endpoint to fetch per-form configuration, automatically detecting which reCAPTCHA type (v3 or Enterprise) is configured and loading the appropriate script.
9
8
 
10
- ### Private Methods
9
+ ## Installation
11
10
 
12
- Private methods are used within the class and are not accessible externally.
11
+ ```bash
12
+ npm i @adobe-commerce/recaptcha
13
+ ```
13
14
 
14
- - `_updateBadgePosition(currentForm, config);`
15
- - Responsible for changing the widget's position if it needs to be placed inline.
16
- - `_addRecaptchaScript();`
17
- - Adds a script to the page.
18
- - `_fetchStoreConfig();`
19
- - Requests configuration from the backend.
20
- - `_loadConfig();`
21
- - Responsible for loading the config from Session Storage.
15
+ ## Quick Start
22
16
 
23
- ### Public Methods
17
+ ```typescript
18
+ import { setEndpoint, setConfig, initReCaptcha, verifyReCaptcha, enableLogger } from '@adobe-commerce/recaptcha';
24
19
 
25
- `import {setEndpoint, setConfig, initReCaptcha, verifyReCaptcha } from "@adobe-commerce/recaptcha"`
20
+ // 1. Set the backend GraphQL endpoint
21
+ setEndpoint('https://your-store.com/graphql');
26
22
 
27
- Public methods are available for use when interacting with the functionality.
23
+ // 2. Fetch and store reCAPTCHA configuration
24
+ await setConfig([{ badgeId: 'generateCustomerToken' }]);
28
25
 
29
- - `setEndpoint(url : string);`
30
- - It sets the URL from which the reCAPTCHA settings will be fetched.
31
- - `setConfig(configList : [{ badgeId: 'badgeId'}]);`
32
- - Initializes the configuration, accepting a URL and a set of parameters. The set of parameters is necessary for customizing form settings. Init on top lvl application.
33
- - `initReCaptcha();`
34
- - Initializes reCAPTCHA and adds a script to the website.
35
- - `verifyReCaptcha();`
36
- - If the method is present, it returns a token.
26
+ // 3. Initialize reCAPTCHA (loads script, renders badges)
27
+ initReCaptcha();
37
28
 
38
- ## Installation
29
+ // 4. Get a token when submitting a protected form
30
+ const token = await verifyReCaptcha();
31
+ ```
32
+
33
+ ## Public Methods
34
+
35
+ ### `setEndpoint(url: string)`
36
+
37
+ Sets the GraphQL endpoint URL used to fetch reCAPTCHA configuration.
38
+
39
+ ### `setConfig(configList: PropsFormTypes[])`
40
+
41
+ Fetches reCAPTCHA configuration from the backend via `recaptchaFormConfig` and stores it in session storage.
42
+
43
+ - Queries all known form types and determines which are enabled
44
+ - Normalizes the response into a unified config shape
45
+ - Detects the reCAPTCHA type (v3 or Enterprise) from the `re_captcha_type` field
46
+ - Skips forms with empty `website_key`
47
+ - Warns and disables reCAPTCHA if mixed types (both v3 and Enterprise) are detected across forms
48
+
49
+ ```typescript
50
+ await setConfig([
51
+ { badgeId: 'generateCustomerToken' },
52
+ { badgeId: 'contactUs' },
53
+ ]);
54
+ ```
55
+
56
+ ### `initReCaptcha(lazyLoadTimeout?: number)`
57
+
58
+ Initializes reCAPTCHA by loading the appropriate Google script and rendering badges.
59
+
60
+ - Loads `enterprise.js` for Enterprise or `api.js` for v3
61
+ - For inline badge position, renders invisible widgets into DOM elements matching each form's `badgeId`
62
+ - Default lazy load timeout: 3000ms
63
+
64
+ ### `verifyReCaptcha(): Promise<string | undefined>`
65
+
66
+ Generates and returns a reCAPTCHA token for form submission.
67
+
68
+ - Uses `grecaptcha.enterprise.execute()` for Enterprise or `grecaptcha.execute()` for v3
69
+ - Returns `undefined` if reCAPTCHA is not enabled or not configured
70
+
71
+ ### `enableLogger(enabled: boolean)`
39
72
 
40
- To install this functionality, follow these steps:
73
+ Enables or disables debug logging. When enabled, logs are prefixed with `[ReCaptcha]` and include:
41
74
 
42
- 1. npm i: `@adobe-commerce/recaptcha`
75
+ - Config fetch and normalization details
76
+ - Which reCAPTCHA type is detected and which forms are enabled
77
+ - Script loading URLs
78
+ - Badge rendering activity
79
+ - Token generation attempts
80
+ - Warnings for skipped forms (missing keys) or mixed type configurations
43
81
 
44
- 2. [ setEndpoint ] - Use this function at the top level to pass the backend URL.
82
+ ## Supported Form Types
45
83
 
46
- 3. [ setConfig ] - Also use this function at the top level to pass your custom configurations if you plan to use your custom form.
84
+ The module queries these form types from the backend:
47
85
 
48
- 4. [ initReCaptcha ] - Call the function on the page where Dropins is integrated, or immediately after setEndpoint or setConfig. Adds a script to the website.
86
+ | Form Type | Badge ID |
87
+ |---|---|
88
+ | `PLACE_ORDER` | `placeOrder` |
89
+ | `CONTACT` | `contactUs` |
90
+ | `CUSTOMER_LOGIN` | `generateCustomerToken` |
91
+ | `CUSTOMER_FORGOT_PASSWORD` | `requestPasswordResetEmail` |
92
+ | `CUSTOMER_CREATE` | `createCustomerV2` |
93
+ | `CUSTOMER_EDIT` | `updateCustomerV2` |
94
+ | `NEWSLETTER` | `subscribeEmailToNewsletter` |
95
+ | `PRODUCT_REVIEW` | `createProductReview` |
96
+ | `SENDFRIEND` | `SENDFRIEND` |
97
+ | `BRAINTREE` | `BRAINTREE` |
49
98
 
50
- 5. [ verifyReCaptcha ] This function serves as an example in either the API method or your form submission handler. It returns a token, which can then be initialized in the headers upon receipt.
99
+ ## Mixed Type Configuration
51
100
 
101
+ If the Commerce Admin has some forms configured with reCAPTCHA v3 and others with Enterprise, the module will:
52
102
 
53
- ## Summary
103
+ 1. Log a warning with instructions to update the Commerce Admin configuration
104
+ 2. Disable reCAPTCHA entirely (returns no config, no script loaded)
54
105
 
55
- This functionality provides methods for preventing and solving CAPTCHAs, enhancing automation and interaction with websites. Using it will help simplify processes related to CAPTCHA.
106
+ All forms must use the same reCAPTCHA type. Configure this in the Commerce Admin under **Stores > Configuration > Security > Google reCAPTCHA Storefront**.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/recaptcha",
3
- "version": "1.1.0-alpha-202603171457",
3
+ "version": "1.1.0-beta.0",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Module allows to efficiently verify that users are humans, not bots or spammers",
6
6
  "engines": {
@@ -13,5 +13,8 @@ export const recaptchaMessage = {
13
13
  failedGetStorageConfig: 'Configuration could not be loaded.',
14
14
  failedExecutionRecaptcha: 'Recaptcha execution failed',
15
15
  failedInitializing: 'An error occurred while initializing ReCaptcha:',
16
- failedEnterpriseDetection: 'Failed to detect reCAPTCHA type, defaulting to V3',
16
+ mixedTypesWarning:
17
+ 'Multiple reCAPTCHA types detected across forms (both v3 and Enterprise). ' +
18
+ 'This configuration is not supported — all forms must use the same reCAPTCHA type. ' +
19
+ 'Please update your configuration in Commerce Admin '
17
20
  };
@@ -6,32 +6,29 @@
6
6
  * file in accordance with the terms of the Adobe license agreement
7
7
  * accompanying it.
8
8
  *******************************************************************/
9
-
10
- export const RECAPTCHA_CONFIGURATION_V3 = `query {
11
- recaptchaV3Config {
12
- is_enabled
13
- website_key
14
- minimum_score
15
- badge_position
16
- language_code
17
- failure_message
18
- forms
19
- theme
20
- }
21
- }`;
22
-
23
- export const RECAPTCHA_FORM_CONFIGURATION = `query {
24
- recaptchaFormConfig(formType: PLACE_ORDER) {
9
+ const RECAPTCHA_FORM_CONFIG_FIELDS = `
25
10
  is_enabled
26
11
  configurations {
27
- re_captcha_type
28
12
  website_key
29
- theme
30
13
  badge_position
31
14
  language_code
32
15
  minimum_score
16
+ re_captcha_type
33
17
  technical_failure_message
34
18
  validation_failure_message
35
- }
36
- }
37
- }`;
19
+ theme
20
+ }`;
21
+
22
+ export const buildRecaptchaFormConfigQuery = (
23
+ formTypes: string[]
24
+ ): string => {
25
+ const queries = formTypes
26
+ .map(
27
+ (formType) =>
28
+ ` ${formType}: recaptchaFormConfig(formType: ${formType}) {${RECAPTCHA_FORM_CONFIG_FIELDS}
29
+ }`
30
+ )
31
+ .join('\n');
32
+
33
+ return `query {\n${queries}\n}`;
34
+ };
package/src/index.ts CHANGED
@@ -8,11 +8,16 @@
8
8
  *******************************************************************/
9
9
 
10
10
  import {
11
- ReCaptchaV3Response,
11
+ ReCaptchaResponse,
12
12
  PropsFormTypes,
13
- ReCaptchaV3Model,
13
+ ReCaptchaModel,
14
+ ReCaptchaFormConfigResult,
14
15
  } from './types/recaptcha.types';
15
- import { recaptchaMessage, recaptchaBadgeSelector } from './configs';
16
+ import {
17
+ recaptchaMessage,
18
+ recaptchaBadgeSelector,
19
+ typeDefaultForm,
20
+ } from './configs';
16
21
  import {
17
22
  extendConfig,
18
23
  setConfigStorage,
@@ -24,33 +29,126 @@ import {
24
29
  getRecaptchaToken,
25
30
  verifyReCaptchaLoad,
26
31
  } from './services/recaptcha.service';
27
- import {
28
- RECAPTCHA_CONFIGURATION_V3,
29
- RECAPTCHA_FORM_CONFIGURATION,
30
- } from './graphql/recaptchaConfig.graphql';
32
+ import { buildRecaptchaFormConfigQuery } from './graphql/recaptchaConfig.graphql';
31
33
 
32
34
  import { FetchGraphQL } from '@adobe-commerce/fetch-graphql';
33
35
 
34
36
  export const recaptchaFetchApi = new FetchGraphQL().getMethods();
35
37
 
38
+ const RECAPTCHA_V3_SCRIPT_URL = 'https://www.google.com/recaptcha/api.js';
39
+ const RECAPTCHA_ENTERPRISE_SCRIPT_URL = 'https://www.google.com/recaptcha/enterprise.js';
40
+
41
+ const LOG_PREFIX = '[ReCaptcha]';
42
+
36
43
  export class RecaptchaModule {
37
44
  _enableReCAPTCHA: boolean = false;
38
- _recaptchaBackendEndpoint: string =
39
- recaptchaFetchApi.getConfig()?.endpoint || '';
40
- _recaptchaScriptUrl: string = 'https://www.google.com/recaptcha/api.js';
41
- _recaptchaEnterpriseScriptUrl: string =
42
- 'https://www.google.com/recaptcha/enterprise.js';
45
+ _recaptchaBackendEndpoint: string = recaptchaFetchApi.getConfig()?.endpoint || '';
46
+ _recaptchaScriptUrl: string = RECAPTCHA_V3_SCRIPT_URL;
43
47
  _configStorageKey: string = 'recaptchaConfig';
44
48
  _logger: boolean = false;
45
49
 
50
+ _log(...args: any[]) {
51
+ this._logger && console.log(LOG_PREFIX, ...args);
52
+ }
53
+
54
+ _normalizeFormConfigResponse(
55
+ response: any,
56
+ formTypes: string[]
57
+ ): ReCaptchaResponse | undefined {
58
+ const data = response?.data;
59
+ if (!data) return undefined;
60
+
61
+ const enabledForms: string[] = [];
62
+ let globalConfig: ReCaptchaFormConfigResult | null = null;
63
+ const detectedTypes = new Set<string>();
64
+
65
+ for (const formType of formTypes) {
66
+ const formData = data[formType];
67
+
68
+ if (!formData?.is_enabled) continue;
69
+
70
+ // configurations may be an array or a single object
71
+ const config = Array.isArray(formData.configurations)
72
+ ? formData.configurations[0]
73
+ : formData.configurations;
74
+
75
+ if (!config) continue;
76
+
77
+ // Track all reCAPTCHA types across enabled forms, regardless of website_key
78
+ if (config.re_captcha_type) {
79
+ detectedTypes.add(config.re_captcha_type);
80
+ }
81
+
82
+ // Skip forms with empty website_key — enabled but not usable
83
+ if (!config.website_key) {
84
+ this._log(
85
+ `Skipping ${formType}: enabled but missing website_key`
86
+ );
87
+ continue;
88
+ }
89
+
90
+ enabledForms.push(formType);
91
+
92
+ if (!globalConfig) {
93
+ globalConfig = config;
94
+ }
95
+ }
96
+
97
+ // Abort if mixed reCAPTCHA types detected across enabled forms
98
+ if (detectedTypes.size > 1) {
99
+ console.warn(
100
+ LOG_PREFIX,
101
+ recaptchaMessage.mixedTypesWarning,
102
+ `\nDetected: ${[...detectedTypes].join(', ')}`
103
+ );
104
+ return undefined;
105
+ }
106
+
107
+ if (!globalConfig || enabledForms.length === 0) {
108
+ this._log('No enabled forms found in recaptchaFormConfig response');
109
+ return undefined;
110
+ }
111
+
112
+ // Map ReCaptchaConfiguration fields to the existing recaptchaConfig shape
113
+ const isEnterprise =
114
+ globalConfig.re_captcha_type === 'RECAPTCHA_ENTERPRISE';
115
+
116
+ this._log(
117
+ `Using reCAPTCHA ${isEnterprise ? 'Enterprise' : 'v3'}`,
118
+ `| Enabled forms: ${enabledForms.join(', ')}`
119
+ );
120
+
121
+ return {
122
+ data: {
123
+ recaptchaConfig: {
124
+ is_enabled: true,
125
+ website_key: globalConfig.website_key,
126
+ badge_position: globalConfig.badge_position,
127
+ language_code: globalConfig.language_code,
128
+ minimum_score: globalConfig.minimum_score,
129
+ failure_message: globalConfig.validation_failure_message,
130
+ theme: globalConfig.theme,
131
+ forms: enabledForms,
132
+ recaptcha_type: isEnterprise ? 'enterprise' : 'v3',
133
+ },
134
+ },
135
+ };
136
+ }
137
+
46
138
  async _updateBadgePosition(
47
139
  badgeId: string,
48
- config: ReCaptchaV3Model
140
+ config: ReCaptchaModel
49
141
  ): Promise<void | null> {
50
142
  if (!config) return;
51
143
 
52
144
  if (config?.badgePosition === 'inline') {
53
- await verifyReCaptchaLoad(badgeId, config, this._logger);
145
+ this._log(`Rendering inline badge for: ${badgeId}`);
146
+ await verifyReCaptchaLoad(
147
+ badgeId,
148
+ config,
149
+ this._logger,
150
+ config.recaptchaType === 'enterprise'
151
+ );
54
152
  } else {
55
153
  const isBadgeLoaded = await checkRecaptchaBadge();
56
154
 
@@ -83,102 +181,53 @@ export class RecaptchaModule {
83
181
  const isBadgeGlobal = config.badgePosition === 'inline';
84
182
  const languageCode = config.languageCode;
85
183
 
86
- if (!webApiKey) return;
184
+ if (!webApiKey) {
185
+ this._log('No website key found, skipping script injection');
186
+ return;
187
+ }
87
188
 
88
- // Enterprise uses a different script; V3 uses the standard api.js
89
- const scriptBaseUrl =
90
- config.reCaptchaType === 'RECAPTCHA_ENTERPRISE'
91
- ? this._recaptchaEnterpriseScriptUrl
189
+ const scriptUrl =
190
+ config.recaptchaType === 'enterprise'
191
+ ? RECAPTCHA_ENTERPRISE_SCRIPT_URL
92
192
  : this._recaptchaScriptUrl;
93
193
 
94
194
  const script = document.createElement('script');
95
195
  script.setAttribute('id', 'recaptchaId');
96
196
  script.defer = true;
97
197
  script.src = isBadgeGlobal
98
- ? `${scriptBaseUrl}?render=${webApiKey}&badge=none&hl=${languageCode}`
99
- : `${scriptBaseUrl}?render=${webApiKey}&badge=${config.badgePosition}&hl=${languageCode}`;
198
+ ? `${scriptUrl}?render=${webApiKey}&badge=none&hl=${languageCode}`
199
+ : `${scriptUrl}?render=${webApiKey}&badge=${config.badgePosition}&hl=${languageCode}`;
100
200
 
201
+ this._log(`Loading script: ${script.src}`);
101
202
  document.head.appendChild(script);
102
203
  }
103
204
  }
104
205
 
105
- async _fetchStoreConfig(): Promise<ReCaptchaV3Response | undefined> {
206
+ async _fetchStoreConfig(): Promise<ReCaptchaResponse | undefined> {
106
207
  try {
107
- const response = await recaptchaFetchApi.fetchGraphQl(
108
- RECAPTCHA_CONFIGURATION_V3,
109
- {
110
- method: 'GET',
111
- cache: 'force-cache',
112
- }
113
- );
114
-
115
- if (response?.errors?.length) {
116
- this._logger && console.error(response.errors[0].message);
208
+ const formTypes = Object.keys(typeDefaultForm);
209
+ const query = buildRecaptchaFormConfigQuery(formTypes);
117
210
 
118
- return;
119
- }
211
+ this._log(`Fetching config for forms: ${formTypes.join(', ')}`);
120
212
 
121
- return response;
122
- } catch (error) {
123
- this._logger && console.error(`${recaptchaMessage.failedFetch}:`, error);
124
- }
125
- }
126
-
127
- /**
128
- * Fetches config from recaptchaFormConfig (new API). Used when backend
129
- * is configured with reCAPTCHA Enterprise. Normalizes the nested
130
- * response to match recaptchaV3Config shape for downstream pipeline.
131
- * Returns undefined if query fails (older backend) or config is invalid.
132
- */
133
- async _fetchFormConfig(): Promise<ReCaptchaV3Response | undefined> {
134
- try {
135
- const response = await recaptchaFetchApi.fetchGraphQl(
136
- RECAPTCHA_FORM_CONFIGURATION,
137
- {
138
- method: 'GET',
139
- cache: 'force-cache',
140
- }
141
- );
213
+ const response = await recaptchaFetchApi.fetchGraphQl(query, {
214
+ method: 'GET',
215
+ cache: 'force-cache',
216
+ });
142
217
 
143
218
  if (response?.errors?.length) {
144
219
  this._logger && console.error(response.errors[0].message);
145
- return;
146
- }
147
220
 
148
- const formConfig = response?.data?.recaptchaFormConfig;
149
- if (!formConfig?.is_enabled || !formConfig?.configurations?.length) {
150
221
  return;
151
222
  }
152
223
 
153
- const config = formConfig.configurations[0];
154
-
155
- // Normalize to recaptchaV3Config shape so convertKeysToCamelCase
156
- // and extendConfig work unchanged
157
- return {
158
- data: {
159
- recaptchaV3Config: {
160
- is_enabled: formConfig.is_enabled,
161
- website_key: config.website_key,
162
- minimum_score: config.minimum_score,
163
- badge_position: config.badge_position,
164
- language_code: config.language_code,
165
- failure_message:
166
- config.technical_failure_message ||
167
- config.validation_failure_message,
168
- theme: config.theme,
169
- re_captcha_type: config.re_captcha_type,
170
- forms: [],
171
- },
172
- },
173
- };
224
+ return this._normalizeFormConfigResponse(response, formTypes);
174
225
  } catch (error) {
175
- this._logger &&
176
- console.error(recaptchaMessage.failedEnterpriseDetection, error);
177
- return;
226
+ this._logger && console.error(`${recaptchaMessage.failedFetch}:`, error);
178
227
  }
179
228
  }
180
229
 
181
- async _loadConfig(): Promise<ReCaptchaV3Model | null> {
230
+ async _loadConfig(): Promise<ReCaptchaModel | null> {
182
231
  const config = await getConfigStorage(this._configStorageKey);
183
232
 
184
233
  if (!config) {
@@ -201,28 +250,27 @@ export class RecaptchaModule {
201
250
 
202
251
  async setConfig(configList: PropsFormTypes[]) {
203
252
  try {
204
- // Try new API first. Use it only for Enterprise; V3 or
205
- // failure falls back to old API (preserves forms list, backward compat).
206
- const formConfig = await this._fetchFormConfig();
207
- const isEnterprise =
208
- formConfig?.data?.recaptchaV3Config?.re_captcha_type ===
209
- 'RECAPTCHA_ENTERPRISE';
210
- const config = isEnterprise
211
- ? formConfig
212
- : await this._fetchStoreConfig();
213
-
214
- if (!config?.data?.recaptchaV3Config) {
253
+ const config = await this._fetchStoreConfig();
254
+
255
+ if (!config?.data?.recaptchaConfig) {
256
+ this._log('No reCAPTCHA config returned, clearing storage');
215
257
  sessionStorage.removeItem(this._configStorageKey);
216
258
  return;
217
259
  }
218
260
 
219
- const transformConfig: ReCaptchaV3Model = convertKeysToCamelCase(
220
- config?.data?.recaptchaV3Config
261
+ const transformConfig: ReCaptchaModel = convertKeysToCamelCase(
262
+ config?.data?.recaptchaConfig
221
263
  );
222
264
 
223
265
  const extendedRecaptchaConfig = extendConfig(transformConfig, configList);
224
266
 
225
267
  if (extendedRecaptchaConfig) {
268
+ this._log('Config stored:', {
269
+ type: extendedRecaptchaConfig.recaptchaType ?? 'v3',
270
+ enabled: extendedRecaptchaConfig.isEnabled,
271
+ badge: extendedRecaptchaConfig.badgePosition,
272
+ forms: extendedRecaptchaConfig.forms,
273
+ });
226
274
  setConfigStorage(
227
275
  this._configStorageKey,
228
276
  extendedRecaptchaConfig,
@@ -245,9 +293,14 @@ export class RecaptchaModule {
245
293
  const config = await this._loadConfig();
246
294
 
247
295
  if (!config?.forms || !config.isEnabled) {
296
+ this._log('reCAPTCHA disabled or no forms configured, skipping init');
248
297
  return;
249
298
  }
250
299
 
300
+ this._log(
301
+ `Initializing reCAPTCHA (type: ${config.recaptchaType ?? 'v3'}, badge: ${config.badgePosition})`
302
+ );
303
+
251
304
  await this._addRecaptchaScript();
252
305
 
253
306
  if (config.badgePosition === 'inline') {
@@ -272,13 +325,22 @@ export class RecaptchaModule {
272
325
  const config = await this._loadConfig();
273
326
 
274
327
  if (!config?.forms || !config.websiteKey || !config.isEnabled) {
328
+ this._log('verifyReCaptcha skipped: reCAPTCHA not enabled or missing config');
275
329
  return undefined;
276
330
  }
277
331
 
278
- // Enterprise calls grecaptcha.enterprise.execute(); V3 calls grecaptcha.execute()
279
- const isEnterprise =
280
- config.reCaptchaType === 'RECAPTCHA_ENTERPRISE';
281
- return await getRecaptchaToken(config.websiteKey, isEnterprise);
332
+ this._log(
333
+ `Requesting token (type: ${config.recaptchaType ?? 'v3'})`
334
+ );
335
+
336
+ const token = await getRecaptchaToken(
337
+ config.websiteKey,
338
+ config.recaptchaType === 'enterprise'
339
+ );
340
+
341
+ this._log('Token obtained successfully');
342
+
343
+ return token;
282
344
  } catch (error) {
283
345
  this._logger && console.error(error);
284
346
  }
@@ -7,13 +7,13 @@
7
7
  * accompanying it.
8
8
  *******************************************************************/
9
9
 
10
- import { ReCaptchaV3Model } from '../types/recaptcha.types';
10
+ import { ReCaptchaModel } from '../types/recaptcha.types';
11
11
  import { typeDefaultForm } from '../configs/typeForms.config';
12
12
 
13
13
  export const extendConfig = (
14
- config: ReCaptchaV3Model,
14
+ config: ReCaptchaModel,
15
15
  modifyParams: any[]
16
- ): ReCaptchaV3Model | undefined => {
16
+ ): ReCaptchaModel | undefined => {
17
17
  if (config && config.forms) {
18
18
  const modifyForm = config.forms.concat(modifyParams).map((el) => {
19
19
  if (typeof el !== 'string') return { ...el, enabledBadgePlace: false };
@@ -8,13 +8,13 @@
8
8
  *******************************************************************/
9
9
 
10
10
  import { recaptchaMessage } from '../configs';
11
- import { ReCaptchaV3Model } from '../types/recaptcha.types';
11
+ import { ReCaptchaModel } from '../types/recaptcha.types';
12
12
 
13
13
  const getConfigStorage = async (
14
14
  storageKey: string,
15
15
  retries = 1,
16
16
  delay = 1000
17
- ): Promise<ReCaptchaV3Model | null> => {
17
+ ): Promise<ReCaptchaModel | null> => {
18
18
  const storedConfig = sessionStorage.getItem(storageKey);
19
19
 
20
20
  if (storedConfig !== null) {
@@ -30,7 +30,7 @@ const getConfigStorage = async (
30
30
 
31
31
  const setConfigStorage = (
32
32
  storageKey: string,
33
- config: ReCaptchaV3Model,
33
+ config: ReCaptchaModel,
34
34
  logger: boolean
35
35
  ) => {
36
36
  if (!storageKey || !config.websiteKey) return null;
@@ -10,15 +10,10 @@
10
10
  import { recaptchaMessage } from '../configs';
11
11
  import {
12
12
  MutationObserverInit,
13
- ReCaptchaV3Model,
13
+ ReCaptchaModel,
14
14
  } from '../types/recaptcha.types';
15
15
  const { failedExecutionRecaptcha } = recaptchaMessage;
16
16
 
17
- /**
18
- * Retrieves a reCAPTCHA token.
19
- * @param websiteKey - The reCAPTCHA site key
20
- * @param isEnterprise - If true, uses grecaptcha.enterprise.execute() for Enterprise
21
- */
22
17
  export const getRecaptchaToken = async (
23
18
  websiteKey: string,
24
19
  isEnterprise = false
@@ -27,19 +22,14 @@ export const getRecaptchaToken = async (
27
22
  return Promise.reject(failedExecutionRecaptcha);
28
23
  }
29
24
 
30
- if (isEnterprise && !window.grecaptcha.enterprise) {
31
- return Promise.reject(failedExecutionRecaptcha);
32
- }
33
-
34
25
  try {
35
- // Enterprise uses grecaptcha.enterprise.execute() vs grecaptcha.execute() for V3
36
- const executor = isEnterprise
37
- ? window.grecaptcha.enterprise
38
- : window.grecaptcha;
39
-
40
- const token = await executor.execute(websiteKey, {
41
- action: 'click',
42
- });
26
+ const token = isEnterprise
27
+ ? await window.grecaptcha.enterprise.execute(websiteKey, {
28
+ action: 'click',
29
+ })
30
+ : await window.grecaptcha.execute(websiteKey, {
31
+ action: 'click',
32
+ });
43
33
 
44
34
  return token;
45
35
  } catch (error) {
@@ -47,15 +37,10 @@ export const getRecaptchaToken = async (
47
37
  }
48
38
  };
49
39
 
50
- export const waitForReCaptcha = (isEnterprise = false) => {
40
+ export const waitForReCaptcha = () => {
51
41
  return new Promise((resolve) => {
52
42
  const observer = new MutationObserver((_, obs) => {
53
- // Enterprise script populates grecaptcha.enterprise; V3 populates grecaptcha directly
54
- const isReady = isEnterprise
55
- ? window.grecaptcha?.enterprise
56
- : window.grecaptcha;
57
-
58
- if (isReady) {
43
+ if (window.grecaptcha) {
59
44
  obs.disconnect();
60
45
  resolve(true);
61
46
  }
@@ -73,21 +58,17 @@ export const waitForReCaptcha = (isEnterprise = false) => {
73
58
 
74
59
  export const verifyReCaptchaLoad = async (
75
60
  badgeId: string,
76
- config: ReCaptchaV3Model,
77
- logger: boolean
61
+ config: ReCaptchaModel,
62
+ logger: boolean,
63
+ isEnterprise = false
78
64
  ): Promise<void> => {
79
- const isEnterprise = config.reCaptchaType === 'RECAPTCHA_ENTERPRISE';
80
-
81
65
  if (!window.grecaptcha) {
82
- await waitForReCaptcha(isEnterprise);
66
+ await waitForReCaptcha();
83
67
  }
84
68
 
85
- // Select the correct API namespace: Enterprise uses grecaptcha.enterprise for ready()/render()
86
- const recaptchaApi = isEnterprise
87
- ? grecaptcha.enterprise
88
- : grecaptcha;
69
+ const api = isEnterprise ? grecaptcha.enterprise : grecaptcha;
89
70
 
90
- return recaptchaApi.ready(() => {
71
+ return api.ready(() => {
91
72
  const badgeContainers = document.querySelectorAll(`#${badgeId}`);
92
73
 
93
74
  if (!badgeContainers.length) return;
@@ -101,7 +82,7 @@ export const verifyReCaptchaLoad = async (
101
82
  badgeContainers.forEach((element) => {
102
83
  if (element.innerHTML === '') {
103
84
  try {
104
- recaptchaApi.render(element.id, {
85
+ api.render(element.id, {
105
86
  sitekey: config.websiteKey as string,
106
87
  badge: config.badgePosition,
107
88
  size: 'invisible',
@@ -0,0 +1,5 @@
1
+ // Suppress console.log and console.warn output during tests to keep shell clean.
2
+ // Using direct override instead of jest.spyOn so restoreAllMocks() won't undo it.
3
+ const noop = () => {};
4
+ console.log = noop;
5
+ console.warn = noop;
@@ -7,20 +7,17 @@
7
7
  * accompanying it.
8
8
  *******************************************************************/
9
9
 
10
- export type ReCaptchaType = 'RECAPTCHA_V3' | 'RECAPTCHA_ENTERPRISE';
11
-
12
- export interface ReCaptchaV3InitProps {
10
+ export interface ReCaptchaInitProps {
13
11
  is_enabled?: boolean;
14
12
  website_key?: string;
15
13
  minimum_score?: number;
16
14
  badge_position?: string;
17
15
  language_code?: string;
18
16
  failure_message?: string;
19
- re_captcha_type?: string;
20
17
  theme: string;
21
18
  }
22
19
 
23
- export interface ReCaptchaV3Props extends ReCaptchaV3InitProps {
20
+ export interface ReCaptchaProps extends ReCaptchaInitProps {
24
21
  forms?: string[];
25
22
  }
26
23
 
@@ -29,46 +26,41 @@ export interface PropsFormTypes {
29
26
  enabledBadgePlace?: boolean;
30
27
  }
31
28
 
32
- export interface ReCaptchaV3ModifyProps extends ReCaptchaV3InitProps {
29
+ export interface ReCaptchaModifyProps extends ReCaptchaInitProps {
33
30
  forms?: PropsFormTypes[];
34
31
  }
35
32
 
36
- export interface ReCaptchaV3Response {
33
+ export interface ReCaptchaResponse {
37
34
  data?: {
38
- recaptchaV3Config?: ReCaptchaV3Props | ReCaptchaV3ModifyProps;
35
+ recaptchaConfig?: ReCaptchaProps | ReCaptchaModifyProps;
36
+ [key: string]: ReCaptchaFormConfigResult | ReCaptchaProps | ReCaptchaModifyProps | undefined;
39
37
  };
40
38
  errors?: { message: string }[];
41
39
  }
42
40
 
43
- export interface ReCaptchaV3Model {
41
+ export type ReCaptchaType = 'v3' | 'enterprise';
42
+
43
+ export interface ReCaptchaModel {
44
44
  isEnabled?: boolean;
45
45
  websiteKey?: string;
46
46
  minimumScore?: number;
47
47
  badgePosition?: string;
48
48
  languageCode?: string;
49
49
  failureMessage?: string;
50
- reCaptchaType?: ReCaptchaType;
51
50
  theme?: string;
52
51
  forms?: PropsFormTypes[] | string[];
52
+ recaptchaType?: ReCaptchaType;
53
53
  }
54
54
 
55
- export interface ReCaptchaFormConfigResponse {
56
- data?: {
57
- recaptchaFormConfig?: {
58
- is_enabled?: boolean;
59
- configurations?: {
60
- re_captcha_type?: string;
61
- website_key?: string;
62
- theme?: string;
63
- badge_position?: string;
64
- language_code?: string;
65
- minimum_score?: number;
66
- technical_failure_message?: string;
67
- validation_failure_message?: string;
68
- }[];
69
- };
70
- };
71
- errors?: { message: string }[];
55
+ export interface ReCaptchaFormConfigResult {
56
+ website_key?: string;
57
+ badge_position?: string;
58
+ language_code?: string;
59
+ minimum_score?: number;
60
+ re_captcha_type?: string;
61
+ technical_failure_message?: string;
62
+ validation_failure_message?: string;
63
+ theme?: string;
72
64
  }
73
65
 
74
66
  export interface MutationObserverInit {