@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 +17 -0
- package/README.md +84 -33
- package/package.json +1 -1
- package/src/configs/message.config.ts +4 -1
- package/src/graphql/recaptchaConfig.graphql.ts +18 -21
- package/src/index.ts +163 -101
- package/src/lib/_extendConfig.ts +3 -3
- package/src/lib/_storageConfig.ts +3 -3
- package/src/services/recaptcha.service.ts +17 -36
- package/src/tests/setup.ts +5 -0
- package/src/types/recaptcha.types.ts +19 -27
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
|
|
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
|
-
|
|
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
|
-
|
|
9
|
+
## Installation
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
```bash
|
|
12
|
+
npm i @adobe-commerce/recaptcha
|
|
13
|
+
```
|
|
13
14
|
|
|
14
|
-
|
|
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
|
-
|
|
17
|
+
```typescript
|
|
18
|
+
import { setEndpoint, setConfig, initReCaptcha, verifyReCaptcha, enableLogger } from '@adobe-commerce/recaptcha';
|
|
24
19
|
|
|
25
|
-
|
|
20
|
+
// 1. Set the backend GraphQL endpoint
|
|
21
|
+
setEndpoint('https://your-store.com/graphql');
|
|
26
22
|
|
|
27
|
-
|
|
23
|
+
// 2. Fetch and store reCAPTCHA configuration
|
|
24
|
+
await setConfig([{ badgeId: 'generateCustomerToken' }]);
|
|
28
25
|
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
+
Enables or disables debug logging. When enabled, logs are prefixed with `[ReCaptcha]` and include:
|
|
41
74
|
|
|
42
|
-
|
|
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
|
-
|
|
82
|
+
## Supported Form Types
|
|
45
83
|
|
|
46
|
-
|
|
84
|
+
The module queries these form types from the backend:
|
|
47
85
|
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
11
|
+
ReCaptchaResponse,
|
|
12
12
|
PropsFormTypes,
|
|
13
|
-
|
|
13
|
+
ReCaptchaModel,
|
|
14
|
+
ReCaptchaFormConfigResult,
|
|
14
15
|
} from './types/recaptcha.types';
|
|
15
|
-
import {
|
|
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
|
-
|
|
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:
|
|
140
|
+
config: ReCaptchaModel
|
|
49
141
|
): Promise<void | null> {
|
|
50
142
|
if (!config) return;
|
|
51
143
|
|
|
52
144
|
if (config?.badgePosition === 'inline') {
|
|
53
|
-
|
|
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)
|
|
184
|
+
if (!webApiKey) {
|
|
185
|
+
this._log('No website key found, skipping script injection');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
87
188
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
? `${
|
|
99
|
-
: `${
|
|
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<
|
|
206
|
+
async _fetchStoreConfig(): Promise<ReCaptchaResponse | undefined> {
|
|
106
207
|
try {
|
|
107
|
-
const
|
|
108
|
-
|
|
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
|
-
|
|
119
|
-
}
|
|
211
|
+
this._log(`Fetching config for forms: ${formTypes.join(', ')}`);
|
|
120
212
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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<
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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:
|
|
220
|
-
config?.data?.
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
}
|
package/src/lib/_extendConfig.ts
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
* accompanying it.
|
|
8
8
|
*******************************************************************/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import { ReCaptchaModel } from '../types/recaptcha.types';
|
|
11
11
|
import { typeDefaultForm } from '../configs/typeForms.config';
|
|
12
12
|
|
|
13
13
|
export const extendConfig = (
|
|
14
|
-
config:
|
|
14
|
+
config: ReCaptchaModel,
|
|
15
15
|
modifyParams: any[]
|
|
16
|
-
):
|
|
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 {
|
|
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<
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 = (
|
|
40
|
+
export const waitForReCaptcha = () => {
|
|
51
41
|
return new Promise((resolve) => {
|
|
52
42
|
const observer = new MutationObserver((_, obs) => {
|
|
53
|
-
|
|
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:
|
|
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(
|
|
66
|
+
await waitForReCaptcha();
|
|
83
67
|
}
|
|
84
68
|
|
|
85
|
-
|
|
86
|
-
const recaptchaApi = isEnterprise
|
|
87
|
-
? grecaptcha.enterprise
|
|
88
|
-
: grecaptcha;
|
|
69
|
+
const api = isEnterprise ? grecaptcha.enterprise : grecaptcha;
|
|
89
70
|
|
|
90
|
-
return
|
|
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
|
-
|
|
85
|
+
api.render(element.id, {
|
|
105
86
|
sitekey: config.websiteKey as string,
|
|
106
87
|
badge: config.badgePosition,
|
|
107
88
|
size: 'invisible',
|
|
@@ -7,20 +7,17 @@
|
|
|
7
7
|
* accompanying it.
|
|
8
8
|
*******************************************************************/
|
|
9
9
|
|
|
10
|
-
export
|
|
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
|
|
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
|
|
29
|
+
export interface ReCaptchaModifyProps extends ReCaptchaInitProps {
|
|
33
30
|
forms?: PropsFormTypes[];
|
|
34
31
|
}
|
|
35
32
|
|
|
36
|
-
export interface
|
|
33
|
+
export interface ReCaptchaResponse {
|
|
37
34
|
data?: {
|
|
38
|
-
|
|
35
|
+
recaptchaConfig?: ReCaptchaProps | ReCaptchaModifyProps;
|
|
36
|
+
[key: string]: ReCaptchaFormConfigResult | ReCaptchaProps | ReCaptchaModifyProps | undefined;
|
|
39
37
|
};
|
|
40
38
|
errors?: { message: string }[];
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
export
|
|
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
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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 {
|