diff --git a/demo/app/examples/AsyncExample.jsx b/demo/app/examples/AsyncExample.jsx index 812c3ba..5908ee9 100644 --- a/demo/app/examples/AsyncExample.jsx +++ b/demo/app/examples/AsyncExample.jsx @@ -1,4 +1,4 @@ -import React, { useRef } from 'react'; +import React, { useRef, useEffect } from 'react'; import HCaptcha from '../../../src/index.js'; @@ -17,6 +17,10 @@ export function AsyncExample() { } }; + useEffect(() => { + executeCaptcha(); + }, []); + const getResponse = () => { try { const res = captchaRef.current.getResponse(); diff --git a/package.json b/package.json index 0222243..cec41e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hcaptcha/react-hcaptcha", - "version": "1.10.3", + "version": "1.11.0", "types": "types/index.d.ts", "main": "dist/index.js", "module": "dist/esm/index.js", diff --git a/src/index.js b/src/index.js index cf44118..8b53e40 100644 --- a/src/index.js +++ b/src/index.js @@ -22,6 +22,7 @@ class HCaptcha extends React.Component { this.resetCaptcha = this.resetCaptcha.bind(this); this.removeCaptcha = this.removeCaptcha.bind(this); this.isReady = this.isReady.bind(this); + this._onReady = null; // Event Handlers this.loadCaptcha = this.loadCaptcha.bind(this); @@ -191,6 +192,7 @@ class HCaptcha extends React.Component { this.setState({ isRemoved: false, captchaId }, () => { onReady && onReady(); + this._onReady && this._onReady(captchaId); }); } @@ -230,7 +232,7 @@ class HCaptcha extends React.Component { }); } - handleOnLoad () { + handleOnLoad () { this.setState({ isApiReady: true }, () => { try { const element = getMountElement(this.props.scriptLocation); @@ -327,22 +329,41 @@ class HCaptcha extends React.Component { } execute (opts = null) { + + opts = typeof opts === 'object' ? opts : null; + try { const { captchaId } = this.state; const hcaptcha = this._hcaptcha; - - + if (!this.isReady()) { - return; - } - - if (opts && typeof opts !== "object") { - opts = null; + const onReady = new Promise((resolve, reject) => { + + this._onReady = (id) => { + try { + const hcaptcha = this._hcaptcha; + + if (opts && opts.async) { + hcaptcha.execute(id, opts).then(resolve).catch(reject); + } else { + resolve(hcaptcha.execute(id, opts)); + } + } catch (e) { + reject(e); + } + }; + }); + + return opts?.async ? onReady : null; } - + return hcaptcha.execute(captchaId, opts); } catch (error) { - this.sentryHub.captureException(error); + this.sentryHub.captureException(error); + if (opts && opts.async) { + return Promise.reject(error); + } + return null; } } diff --git a/tests/hcaptcha.spec.js b/tests/hcaptcha.spec.js index d935da5..8a965b3 100644 --- a/tests/hcaptcha.spec.js +++ b/tests/hcaptcha.spec.js @@ -88,6 +88,38 @@ describe("hCaptcha", () => { expect(window.hcaptcha.execute).toBeCalledWith(MOCK_WIDGET_ID, null); }); + it("stores and calls execute after hCaptcha onload is executed", async () => { + jest.spyOn(instance, 'isReady').mockReturnValueOnce(false); + instance.execute(); + expect(window.hcaptcha.execute.mock.calls.length).toBe(0); + await instance._onReady(MOCK_WIDGET_ID); + expect(window.hcaptcha.execute.mock.calls.length).toBe(1); + expect(window.hcaptcha.execute).toBeCalledWith(MOCK_WIDGET_ID, null); + }); + + it("stores the execute command and calls it after hCaptcha onload is executed", async () => { + jest.spyOn(instance, 'isReady').mockReturnValueOnce(false); + + const onLoad = jest.fn(() => { + expect(instance.state.captchaId).toBe(MOCK_WIDGET_ID); + }); + + instance = ReactTestUtils.renderIntoDocument( + , + ); + expect(window.hcaptcha.execute.mock.calls.length).toBe(0); + + instance.execute(); + instance.handleOnLoad(); + + expect(window.hcaptcha.execute.mock.calls.length).toBe(1); + expect(window.hcaptcha.execute).toBeCalledWith(MOCK_WIDGET_ID, null); + }); + it("can execute synchronously with async: false", () => { expect(window.hcaptcha.execute.mock.calls.length).toBe(0); instance.execute({ async: false });