@hcaptcha/react-hcaptcha 1.13.0 → 1.14.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/README.md CHANGED
@@ -154,6 +154,7 @@ return <HCaptcha ref={captchaRef} onLoad={onLoad} sitekey={sitekey} {...props} /
154
154
  |`onVerify`|`token, eKey`|When challenge is completed. The response `token` and an `eKey` (session id) are passed along.|
155
155
  |`onExpire`|-|When the current token expires.|
156
156
  |`onLoad`|-|When the hCaptcha API loads.|
157
+ |`onReady`|-|When the hCaptcha is ready to be used.|
157
158
  |`onOpen`|-|When the user display of a challenge starts.|
158
159
  |`onClose`|-|When the user dismisses a challenge.|
159
160
  |`onChalExpired`|-|When the user display of a challenge times out with no answer.|
package/dist/esm/index.js CHANGED
@@ -1,7 +1,5 @@
1
- import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/objectWithoutPropertiesLoose";
2
1
  import _assertThisInitialized from "@babel/runtime/helpers/assertThisInitialized";
3
2
  import _inheritsLoose from "@babel/runtime/helpers/inheritsLoose";
4
- var _excluded = ["userJourneys"];
5
3
  import * as React from 'react';
6
4
  import { hCaptchaLoader } from '@hcaptcha/loader';
7
5
  import { getFrame, getMountElement } from './utils.js';
@@ -39,6 +37,12 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
39
37
  _this.apiScriptRequested = false;
40
38
  _this.sentryHub = null;
41
39
  _this.captchaId = '';
40
+
41
+ /**
42
+ * Tracks the currently pending async execute() promise.
43
+ * Stores { resolve, reject } so we can cancel on unmount/errors/etc.
44
+ */
45
+ _this._pendingExecute = null;
42
46
  _this.state = {
43
47
  isApiReady: false,
44
48
  isRemoved: false,
@@ -73,6 +77,7 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
73
77
  _proto.componentWillUnmount = function componentWillUnmount() {
74
78
  var hcaptcha = this._hcaptcha;
75
79
  var captchaId = this.captchaId;
80
+ this._cancelPendingExecute('react-component-unmounted');
76
81
  if (!this.isReady()) {
77
82
  return;
78
83
  }
@@ -143,7 +148,7 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
143
148
  scriptSource: scriptSource,
144
149
  secureApi: secureApi,
145
150
  cleanup: cleanup,
146
- userJourneys: userJourneys
151
+ uj: userJourneys !== undefined ? userJourneys : false
147
152
  };
148
153
  hCaptchaLoader(mountParams).then(this.handleOnLoad, this.handleError)["catch"](this.handleError);
149
154
  this.apiScriptRequested = true;
@@ -158,13 +163,6 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
158
163
  // • API is not ready
159
164
  // • Component has already been mounted
160
165
  if (!isApiReady || captchaId) return;
161
-
162
- // It is needed to pass only the props that hCaptcha supports
163
- // React are able to receive userJourneys as prop but hCaptcha not
164
- // hcaptcha expects to have only "uj" parameter to enable user journeys
165
- var _this$props2 = this.props,
166
- userJourneys = _this$props2.userJourneys,
167
- basicProps = _objectWithoutPropertiesLoose(_this$props2, _excluded);
168
166
  var renderParams = Object.assign({
169
167
  "open-callback": this.handleOpen,
170
168
  "close-callback": this.handleClose,
@@ -172,10 +170,9 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
172
170
  "chalexpired-callback": this.handleChallengeExpired,
173
171
  "expired-callback": this.handleExpire,
174
172
  "callback": this.handleSubmit
175
- }, basicProps, {
173
+ }, this.props, {
176
174
  hl: this.props.hl || this.props.languageOverride,
177
- languageOverride: undefined,
178
- uj: userJourneys !== undefined ? userJourneys : false
175
+ languageOverride: undefined
179
176
  });
180
177
  var hcaptcha = this._hcaptcha;
181
178
  //Render hCaptcha widget and provide necessary callbacks - hCaptcha
@@ -195,13 +192,16 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
195
192
  if (!this.isReady()) {
196
193
  return;
197
194
  }
195
+
198
196
  // Reset captcha state, removes stored token and unticks checkbox
199
197
  hcaptcha.reset(captchaId);
198
+ this._cancelPendingExecute('hcaptcha-reset');
200
199
  };
201
200
  _proto.removeCaptcha = function removeCaptcha(callback) {
202
201
  var _this5 = this;
203
202
  var hcaptcha = this._hcaptcha;
204
203
  var captchaId = this.captchaId;
204
+ this._cancelPendingExecute('hcaptcha-removed');
205
205
  if (!this.isReady()) {
206
206
  return;
207
207
  }
@@ -267,6 +267,20 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
267
267
  isApiReady = _this$state.isApiReady,
268
268
  isRemoved = _this$state.isRemoved;
269
269
  return isApiReady && !isRemoved;
270
+ }
271
+
272
+ /**
273
+ * Cancel any pending async execute() promise
274
+ * Called when the component unmounts, errors occur, resets, etc.
275
+ */;
276
+ _proto._cancelPendingExecute = function _cancelPendingExecute(reason) {
277
+ if (!this._pendingExecute) {
278
+ return;
279
+ }
280
+ var pending = this._pendingExecute;
281
+ this._pendingExecute = null;
282
+ var error = new Error(reason);
283
+ pending.reject(error);
270
284
  };
271
285
  _proto.handleOpen = function handleOpen() {
272
286
  if (!this.isReady() || !this.props.onOpen) {
@@ -295,25 +309,71 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
295
309
  try {
296
310
  var hcaptcha = this._hcaptcha;
297
311
  var captchaId = this.captchaId;
312
+
313
+ // Is an async execute and there's already 1 pending, cancel the old one.
314
+ if (opts && opts.async && this._pendingExecute) {
315
+ this._cancelPendingExecute('hcaptcha-execute-replaced');
316
+ }
298
317
  if (!this.isReady()) {
299
- var _opts;
300
- var onReady = new Promise(function (resolve, reject) {
301
- _this7._onReady = function (id) {
302
- try {
303
- var _hcaptcha = _this7._hcaptcha;
304
- if (opts && opts.async) {
305
- _hcaptcha.execute(id, opts).then(resolve)["catch"](reject);
306
- } else {
307
- resolve(_hcaptcha.execute(id, opts));
318
+ if (opts && opts.async) {
319
+ return new Promise(function (resolve, reject) {
320
+ _this7._pendingExecute = {
321
+ resolve: resolve,
322
+ reject: reject
323
+ };
324
+ _this7._onReady = function (id) {
325
+ if (!_this7._pendingExecute) {
326
+ return;
327
+ }
328
+ try {
329
+ var _result = hcaptcha.execute(id, opts);
330
+ if (_result && typeof _result.then === 'function') {
331
+ _result.then(function (val) {
332
+ _this7._pendingExecute = null;
333
+ resolve(val);
334
+ })["catch"](function (err) {
335
+ _this7._pendingExecute = null;
336
+ reject(err);
337
+ });
338
+ } else {
339
+ _this7._pendingExecute = null;
340
+ reject(new Error('hcaptcha-execute-no-promise'));
341
+ }
342
+ } catch (e) {
343
+ _this7._pendingExecute = null;
344
+ reject(e);
308
345
  }
309
- } catch (e) {
310
- reject(e);
311
- }
346
+ };
347
+ });
348
+ } else {
349
+ // Non-async: don't return a promise.
350
+ this._onReady = function (id) {
351
+ hcaptcha.execute(id, opts);
352
+ };
353
+ return null;
354
+ }
355
+ }
356
+
357
+ // hCaptcha is ready, execute directly.
358
+ var result = hcaptcha.execute(captchaId, opts);
359
+
360
+ // If it's async execute, track it.
361
+ if (opts && opts.async && result && typeof result.then === 'function') {
362
+ return new Promise(function (resolve, reject) {
363
+ _this7._pendingExecute = {
364
+ resolve: resolve,
365
+ reject: reject
312
366
  };
367
+ result.then(function (val) {
368
+ _this7._pendingExecute = null;
369
+ resolve(val);
370
+ })["catch"](function (err) {
371
+ _this7._pendingExecute = null;
372
+ reject(err);
373
+ });
313
374
  });
314
- return (_opts = opts) != null && _opts.async ? onReady : null;
315
375
  }
316
- return hcaptcha.execute(captchaId, opts);
376
+ return result;
317
377
  } catch (error) {
318
378
  if (opts && opts.async) {
319
379
  return Promise.reject(error);
@@ -324,6 +384,7 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
324
384
  _proto.close = function close() {
325
385
  var hcaptcha = this._hcaptcha;
326
386
  var captchaId = this.captchaId;
387
+ this._cancelPendingExecute('hcaptcha-closed');
327
388
  if (!this.isReady()) {
328
389
  return;
329
390
  }
package/dist/index.js CHANGED
@@ -7,7 +7,6 @@ Object.defineProperty(exports, "__esModule", {
7
7
  });
8
8
  exports["default"] = void 0;
9
9
  var _typeof2 = _interopRequireDefault(require("@babel/runtime/helpers/typeof"));
10
- var _objectWithoutProperties2 = _interopRequireDefault(require("@babel/runtime/helpers/objectWithoutProperties"));
11
10
  var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime/helpers/classCallCheck"));
12
11
  var _createClass2 = _interopRequireDefault(require("@babel/runtime/helpers/createClass"));
13
12
  var _possibleConstructorReturn2 = _interopRequireDefault(require("@babel/runtime/helpers/possibleConstructorReturn"));
@@ -17,7 +16,6 @@ var _inherits2 = _interopRequireDefault(require("@babel/runtime/helpers/inherits
17
16
  var React = _interopRequireWildcard(require("react"));
18
17
  var _loader = require("@hcaptcha/loader");
19
18
  var _utils = require("./utils.js");
20
- var _excluded = ["userJourneys"];
21
19
  function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function _getRequireWildcardCache(e) { return e ? t : r; })(e); }
22
20
  function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != _typeof3(e) && "function" != typeof e) return { "default": e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n["default"] = e, t && t.set(e, n), n; }
23
21
  function _callSuper(t, o, e) { return o = (0, _getPrototypeOf2["default"])(o), (0, _possibleConstructorReturn2["default"])(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], (0, _getPrototypeOf2["default"])(t).constructor) : o.apply(t, e)); }
@@ -57,6 +55,12 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
57
55
  _this.apiScriptRequested = false;
58
56
  _this.sentryHub = null;
59
57
  _this.captchaId = '';
58
+
59
+ /**
60
+ * Tracks the currently pending async execute() promise.
61
+ * Stores { resolve, reject } so we can cancel on unmount/errors/etc.
62
+ */
63
+ _this._pendingExecute = null;
60
64
  _this.state = {
61
65
  isApiReady: false,
62
66
  isRemoved: false,
@@ -94,6 +98,7 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
94
98
  value: function componentWillUnmount() {
95
99
  var hcaptcha = this._hcaptcha;
96
100
  var captchaId = this.captchaId;
101
+ this._cancelPendingExecute('react-component-unmounted');
97
102
  if (!this.isReady()) {
98
103
  return;
99
104
  }
@@ -170,7 +175,7 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
170
175
  scriptSource: scriptSource,
171
176
  secureApi: secureApi,
172
177
  cleanup: cleanup,
173
- userJourneys: userJourneys
178
+ uj: userJourneys !== undefined ? userJourneys : false
174
179
  };
175
180
  (0, _loader.hCaptchaLoader)(mountParams).then(this.handleOnLoad, this.handleError)["catch"](this.handleError);
176
181
  this.apiScriptRequested = true;
@@ -187,13 +192,6 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
187
192
  // • API is not ready
188
193
  // • Component has already been mounted
189
194
  if (!isApiReady || captchaId) return;
190
-
191
- // It is needed to pass only the props that hCaptcha supports
192
- // React are able to receive userJourneys as prop but hCaptcha not
193
- // hcaptcha expects to have only "uj" parameter to enable user journeys
194
- var _this$props2 = this.props,
195
- userJourneys = _this$props2.userJourneys,
196
- basicProps = (0, _objectWithoutProperties2["default"])(_this$props2, _excluded);
197
195
  var renderParams = Object.assign({
198
196
  "open-callback": this.handleOpen,
199
197
  "close-callback": this.handleClose,
@@ -201,10 +199,9 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
201
199
  "chalexpired-callback": this.handleChallengeExpired,
202
200
  "expired-callback": this.handleExpire,
203
201
  "callback": this.handleSubmit
204
- }, basicProps, {
202
+ }, this.props, {
205
203
  hl: this.props.hl || this.props.languageOverride,
206
- languageOverride: undefined,
207
- uj: userJourneys !== undefined ? userJourneys : false
204
+ languageOverride: undefined
208
205
  });
209
206
  var hcaptcha = this._hcaptcha;
210
207
  //Render hCaptcha widget and provide necessary callbacks - hCaptcha
@@ -226,8 +223,10 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
226
223
  if (!this.isReady()) {
227
224
  return;
228
225
  }
226
+
229
227
  // Reset captcha state, removes stored token and unticks checkbox
230
228
  hcaptcha.reset(captchaId);
229
+ this._cancelPendingExecute('hcaptcha-reset');
231
230
  }
232
231
  }, {
233
232
  key: "removeCaptcha",
@@ -235,6 +234,7 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
235
234
  var _this5 = this;
236
235
  var hcaptcha = this._hcaptcha;
237
236
  var captchaId = this.captchaId;
237
+ this._cancelPendingExecute('hcaptcha-removed');
238
238
  if (!this.isReady()) {
239
239
  return;
240
240
  }
@@ -311,6 +311,22 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
311
311
  isRemoved = _this$state.isRemoved;
312
312
  return isApiReady && !isRemoved;
313
313
  }
314
+
315
+ /**
316
+ * Cancel any pending async execute() promise
317
+ * Called when the component unmounts, errors occur, resets, etc.
318
+ */
319
+ }, {
320
+ key: "_cancelPendingExecute",
321
+ value: function _cancelPendingExecute(reason) {
322
+ if (!this._pendingExecute) {
323
+ return;
324
+ }
325
+ var pending = this._pendingExecute;
326
+ this._pendingExecute = null;
327
+ var error = new Error(reason);
328
+ pending.reject(error);
329
+ }
314
330
  }, {
315
331
  key: "handleOpen",
316
332
  value: function handleOpen() {
@@ -344,25 +360,71 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
344
360
  try {
345
361
  var hcaptcha = this._hcaptcha;
346
362
  var captchaId = this.captchaId;
363
+
364
+ // Is an async execute and there's already 1 pending, cancel the old one.
365
+ if (opts && opts.async && this._pendingExecute) {
366
+ this._cancelPendingExecute('hcaptcha-execute-replaced');
367
+ }
347
368
  if (!this.isReady()) {
348
- var _opts;
349
- var onReady = new Promise(function (resolve, reject) {
350
- _this7._onReady = function (id) {
351
- try {
352
- var _hcaptcha = _this7._hcaptcha;
353
- if (opts && opts.async) {
354
- _hcaptcha.execute(id, opts).then(resolve)["catch"](reject);
355
- } else {
356
- resolve(_hcaptcha.execute(id, opts));
369
+ if (opts && opts.async) {
370
+ return new Promise(function (resolve, reject) {
371
+ _this7._pendingExecute = {
372
+ resolve: resolve,
373
+ reject: reject
374
+ };
375
+ _this7._onReady = function (id) {
376
+ if (!_this7._pendingExecute) {
377
+ return;
357
378
  }
358
- } catch (e) {
359
- reject(e);
360
- }
379
+ try {
380
+ var _result = hcaptcha.execute(id, opts);
381
+ if (_result && typeof _result.then === 'function') {
382
+ _result.then(function (val) {
383
+ _this7._pendingExecute = null;
384
+ resolve(val);
385
+ })["catch"](function (err) {
386
+ _this7._pendingExecute = null;
387
+ reject(err);
388
+ });
389
+ } else {
390
+ _this7._pendingExecute = null;
391
+ reject(new Error('hcaptcha-execute-no-promise'));
392
+ }
393
+ } catch (e) {
394
+ _this7._pendingExecute = null;
395
+ reject(e);
396
+ }
397
+ };
398
+ });
399
+ } else {
400
+ // Non-async: don't return a promise.
401
+ this._onReady = function (id) {
402
+ hcaptcha.execute(id, opts);
403
+ };
404
+ return null;
405
+ }
406
+ }
407
+
408
+ // hCaptcha is ready, execute directly.
409
+ var result = hcaptcha.execute(captchaId, opts);
410
+
411
+ // If it's async execute, track it.
412
+ if (opts && opts.async && result && typeof result.then === 'function') {
413
+ return new Promise(function (resolve, reject) {
414
+ _this7._pendingExecute = {
415
+ resolve: resolve,
416
+ reject: reject
361
417
  };
418
+ result.then(function (val) {
419
+ _this7._pendingExecute = null;
420
+ resolve(val);
421
+ })["catch"](function (err) {
422
+ _this7._pendingExecute = null;
423
+ reject(err);
424
+ });
362
425
  });
363
- return (_opts = opts) !== null && _opts !== void 0 && _opts.async ? onReady : null;
364
426
  }
365
- return hcaptcha.execute(captchaId, opts);
427
+ return result;
366
428
  } catch (error) {
367
429
  if (opts && opts.async) {
368
430
  return Promise.reject(error);
@@ -375,6 +437,7 @@ var HCaptcha = /*#__PURE__*/function (_React$Component) {
375
437
  value: function close() {
376
438
  var hcaptcha = this._hcaptcha;
377
439
  var captchaId = this.captchaId;
440
+ this._cancelPendingExecute('hcaptcha-closed');
378
441
  if (!this.isReady()) {
379
442
  return;
380
443
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hcaptcha/react-hcaptcha",
3
- "version": "1.13.0",
3
+ "version": "1.14.0",
4
4
  "types": "types/index.d.ts",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/esm/index.js",
@@ -60,6 +60,6 @@
60
60
  },
61
61
  "dependencies": {
62
62
  "@babel/runtime": "^7.17.9",
63
- "@hcaptcha/loader": "^2.0.1"
63
+ "@hcaptcha/loader": "^2.2.0"
64
64
  }
65
65
  }
package/src/index.js CHANGED
@@ -38,6 +38,12 @@ class HCaptcha extends React.Component {
38
38
  this.sentryHub = null;
39
39
  this.captchaId = '';
40
40
 
41
+ /**
42
+ * Tracks the currently pending async execute() promise.
43
+ * Stores { resolve, reject } so we can cancel on unmount/errors/etc.
44
+ */
45
+ this._pendingExecute = null;
46
+
41
47
  this.state = {
42
48
  isApiReady: false,
43
49
  isRemoved: false,
@@ -77,6 +83,8 @@ class HCaptcha extends React.Component {
77
83
  const hcaptcha = this._hcaptcha;
78
84
  const captchaId = this.captchaId;
79
85
 
86
+ this._cancelPendingExecute('react-component-unmounted');
87
+
80
88
  if (!this.isReady()) {
81
89
  return;
82
90
  }
@@ -132,6 +140,7 @@ class HCaptcha extends React.Component {
132
140
  cleanup = true,
133
141
  userJourneys,
134
142
  } = this.props;
143
+
135
144
  const mountParams = {
136
145
  render: 'explicit',
137
146
  apihost,
@@ -149,9 +158,9 @@ class HCaptcha extends React.Component {
149
158
  scriptSource,
150
159
  secureApi,
151
160
  cleanup,
152
- userJourneys
161
+ uj: userJourneys !== undefined ? userJourneys : false,
153
162
  };
154
-
163
+
155
164
  hCaptchaLoader(mountParams)
156
165
  .then(this.handleOnLoad, this.handleError)
157
166
  .catch(this.handleError);
@@ -168,11 +177,6 @@ class HCaptcha extends React.Component {
168
177
  // • API is not ready
169
178
  // • Component has already been mounted
170
179
  if (!isApiReady || captchaId) return;
171
-
172
- // It is needed to pass only the props that hCaptcha supports
173
- // React are able to receive userJourneys as prop but hCaptcha not
174
- // hcaptcha expects to have only "uj" parameter to enable user journeys
175
- const { userJourneys, ...basicProps } = this.props;
176
180
 
177
181
  const renderParams = Object.assign({
178
182
  "open-callback" : this.handleOpen,
@@ -181,10 +185,9 @@ class HCaptcha extends React.Component {
181
185
  "chalexpired-callback": this.handleChallengeExpired,
182
186
  "expired-callback" : this.handleExpire,
183
187
  "callback" : this.handleSubmit,
184
- }, basicProps, {
188
+ }, this.props, {
185
189
  hl: this.props.hl || this.props.languageOverride,
186
- languageOverride: undefined,
187
- uj: userJourneys !== undefined ? userJourneys : false,
190
+ languageOverride: undefined
188
191
  });
189
192
 
190
193
  const hcaptcha = this._hcaptcha;
@@ -206,14 +209,19 @@ class HCaptcha extends React.Component {
206
209
  if (!this.isReady()) {
207
210
  return;
208
211
  }
212
+
209
213
  // Reset captcha state, removes stored token and unticks checkbox
210
214
  hcaptcha.reset(captchaId)
215
+
216
+ this._cancelPendingExecute('hcaptcha-reset');
211
217
  }
212
218
 
213
219
  removeCaptcha(callback) {
214
220
  const hcaptcha = this._hcaptcha;
215
221
  const captchaId = this.captchaId;
216
222
 
223
+ this._cancelPendingExecute('hcaptcha-removed');
224
+
217
225
  if (!this.isReady()) {
218
226
  return;
219
227
  }
@@ -222,6 +230,7 @@ class HCaptcha extends React.Component {
222
230
  this.captchaId = '';
223
231
 
224
232
  hcaptcha.remove(captchaId);
233
+
225
234
  callback && callback()
226
235
  });
227
236
  }
@@ -290,6 +299,22 @@ class HCaptcha extends React.Component {
290
299
  return isApiReady && !isRemoved;
291
300
  }
292
301
 
302
+ /**
303
+ * Cancel any pending async execute() promise
304
+ * Called when the component unmounts, errors occur, resets, etc.
305
+ */
306
+ _cancelPendingExecute(reason) {
307
+ if (!this._pendingExecute) {
308
+ return;
309
+ }
310
+
311
+ const pending = this._pendingExecute;
312
+ this._pendingExecute = null;
313
+
314
+ const error = new Error(reason);
315
+ pending.reject(error);
316
+ }
317
+
293
318
  handleOpen () {
294
319
  if (!this.isReady() || !this.props.onOpen) {
295
320
  return;
@@ -314,7 +339,7 @@ class HCaptcha extends React.Component {
314
339
  this.props.onChalExpired();
315
340
  }
316
341
 
317
- execute (opts = null) {
342
+ execute(opts = null) {
318
343
 
319
344
  opts = typeof opts === 'object' ? opts : null;
320
345
 
@@ -322,28 +347,75 @@ class HCaptcha extends React.Component {
322
347
  const hcaptcha = this._hcaptcha;
323
348
  const captchaId = this.captchaId;
324
349
 
350
+ // Is an async execute and there's already 1 pending, cancel the old one.
351
+ if (opts && opts.async && this._pendingExecute) {
352
+ this._cancelPendingExecute('hcaptcha-execute-replaced');
353
+ }
354
+
325
355
  if (!this.isReady()) {
326
- const onReady = new Promise((resolve, reject) => {
356
+ if (opts && opts.async) {
357
+ return new Promise((resolve, reject) => {
358
+ this._pendingExecute = { resolve, reject };
327
359
 
328
- this._onReady = (id) => {
329
- try {
330
- const hcaptcha = this._hcaptcha;
360
+ this._onReady = (id) => {
361
+ if (!this._pendingExecute) {
362
+ return;
363
+ }
331
364
 
332
- if (opts && opts.async) {
333
- hcaptcha.execute(id, opts).then(resolve).catch(reject);
334
- } else {
335
- resolve(hcaptcha.execute(id, opts));
365
+ try {
366
+ const result = hcaptcha.execute(id, opts);
367
+
368
+ if (result && typeof result.then === 'function') {
369
+ result
370
+ .then((val) => {
371
+ this._pendingExecute = null;
372
+ resolve(val);
373
+ })
374
+ .catch((err) => {
375
+ this._pendingExecute = null;
376
+ reject(err);
377
+ });
378
+ } else {
379
+ this._pendingExecute = null;
380
+ reject(new Error('hcaptcha-execute-no-promise'));
381
+ }
382
+ } catch (e) {
383
+ this._pendingExecute = null;
384
+ reject(e);
336
385
  }
337
- } catch (e) {
338
- reject(e);
339
- }
386
+ };
387
+ });
388
+ } else {
389
+ // Non-async: don't return a promise.
390
+ this._onReady = (id) => {
391
+ hcaptcha.execute(id, opts);
340
392
  };
341
- });
393
+
394
+ return null;
395
+ }
396
+ }
342
397
 
343
- return opts?.async ? onReady : null;
398
+ // hCaptcha is ready, execute directly.
399
+ const result = hcaptcha.execute(captchaId, opts);
400
+
401
+ // If it's async execute, track it.
402
+ if (opts && opts.async && result && typeof result.then === 'function') {
403
+ return new Promise((resolve, reject) => {
404
+ this._pendingExecute = { resolve, reject };
405
+
406
+ result
407
+ .then((val) => {
408
+ this._pendingExecute = null;
409
+ resolve(val);
410
+ })
411
+ .catch((err) => {
412
+ this._pendingExecute = null;
413
+ reject(err);
414
+ });
415
+ });
344
416
  }
345
417
 
346
- return hcaptcha.execute(captchaId, opts);
418
+ return result;
347
419
  } catch (error) {
348
420
  if (opts && opts.async) {
349
421
  return Promise.reject(error);
@@ -356,6 +428,8 @@ class HCaptcha extends React.Component {
356
428
  const hcaptcha = this._hcaptcha;
357
429
  const captchaId = this.captchaId;
358
430
 
431
+ this._cancelPendingExecute('hcaptcha-closed');
432
+
359
433
  if (!this.isReady()) {
360
434
  return;
361
435
  }