From a65ea474bed81949e429ff8052c49fd9f7fa42e4 Mon Sep 17 00:00:00 2001 From: Andrey Polischuk Date: Thu, 9 Jun 2022 00:59:57 +0300 Subject: [PATCH 1/4] trigger an error if the api script is not loaded --- src/index.js | 67 ++++++++++++++++++++++++++---------------- tests/hcaptcha.spec.js | 22 +++++++++++++- 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/src/index.js b/src/index.js index 989f054..0d9aa2d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,34 +1,45 @@ const React = require('react'); const { generateQuery } = require("./utils.js"); - // Create script to init hCaptcha -let onLoadListeners = []; -let apiScriptRequested = false; - -// Generate hCaptcha API Script +const SCRIPT_ID = 'hcaptcha-api-script-id'; +const HCAPTCHA_LOAD_FN_NAME = 'hcaptchaOnLoad'; + +// Prevent loading API script multiple times +let resolveFn; +let rejectFn; +const mountPromise = new Promise((resolve, reject) => { + resolveFn = resolve; + rejectFn = reject; +}); + +// Generate hCaptcha API script const mountCaptchaScript = (params={}) => { - apiScriptRequested = true; + if (document.getElementById(SCRIPT_ID)) { + // API was already requested + return mountPromise; + } + // Create global onload callback - window.hcaptchaOnLoad = () => { - // Iterate over onload listeners, call each listener - onLoadListeners = onLoadListeners.filter(listener => { - listener(); - return false; - }); - }; + window[HCAPTCHA_LOAD_FN_NAME] = resolveFn; const domain = params.apihost || "https://js.hcaptcha.com"; delete params.apihost; const script = document.createElement("script"); - script.src = `${domain}/1/api.js?render=explicit&onload=hcaptchaOnLoad`; + script.id = SCRIPT_ID; + script.src = `${domain}/1/api.js?render=explicit&onload=${HCAPTCHA_LOAD_FN_NAME}`; script.async = true; + script.onerror = (event) => { + console.error('Failed to load api: ' + script.src, event); + rejectFn(new Error('Failed to load api')); + } const query = generateQuery(params); script.src += query !== ""? `&${query}` : ""; document.head.appendChild(script); -} + return mountPromise; +}; class HCaptcha extends React.Component { @@ -43,6 +54,7 @@ class HCaptcha extends React.Component { // Event Handlers this.handleOnLoad = this.handleOnLoad.bind(this); + this.handleOnLoadingError = this.handleOnLoadingError.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleExpire = this.handleExpire.bind(this); this.handleError = this.handleError.bind(this); @@ -62,17 +74,12 @@ class HCaptcha extends React.Component { } } - componentDidMount () { //Once captcha is mounted intialize hCaptcha - hCaptcha + componentDidMount () { // Once captcha is mounted intialize hCaptcha - hCaptcha const { apihost, assethost, endpoint, host, imghost, languageOverride:hl, reCaptchaCompat, reportapi, sentry, custom } = this.props; const { isApiReady } = this.state; - if (!isApiReady) { //Check if hCaptcha has already been loaded, if not create script tag and wait to render captcha - if (apiScriptRequested) { - return; - } - - // Only create the script tag once, use a global variable to track - mountCaptchaScript({ + if (!isApiReady) { // Check if hCaptcha has already been loaded, if not create script tag and wait to render captcha + const mountParams = { apihost, assethost, endpoint, @@ -83,10 +90,12 @@ class HCaptcha extends React.Component { reportapi, sentry, custom - }); + }; - // Add onload callback to global onload listeners - onLoadListeners.push(this.handleOnLoad); + // Only create the script tag once, use a global promise to track + mountCaptchaScript(mountParams) + .then(this.handleOnLoad) + .catch(this.handleOnLoadingError); } else { this.renderCaptcha(); } @@ -186,6 +195,12 @@ class HCaptcha extends React.Component { }); } + handleOnLoadingError (event) { + const { onError } = this.props; + + if (onError) onError(event); + } + handleSubmit (event) { const { onVerify } = this.props; const { isRemoved, captchaId } = this.state; diff --git a/tests/hcaptcha.spec.js b/tests/hcaptcha.spec.js index b32750f..3a8de47 100644 --- a/tests/hcaptcha.spec.js +++ b/tests/hcaptcha.spec.js @@ -337,7 +337,7 @@ describe("hCaptcha", () => { }); - describe("Query parameter", () => { + describe("Mount hCaptcha API script", () => { beforeEach(() => { // Setup hCaptcha as undefined to load script @@ -492,5 +492,25 @@ describe("hCaptcha", () => { const script = document.querySelector("head > script"); expect(script.src).toContain("custom=true"); }); + + it("emits error when script is failed", async () => { + const onError = jest.fn(); + + instance = ReactTestUtils.renderIntoDocument(); + + const script = document.querySelector("head > script"); + expect(onError.mock.calls.length).toBe(0); + + script.onerror('api-script-failed'); + + // simulate microtask + await Promise.reject().catch(() => null) + + expect(onError.mock.calls.length).toBe(1); + expect(onError.mock.calls[0][0]).toEqual(new Error('Failed to load api')); + }); }); }); From 19609aa3e5a217073252789f843dc90c2320526d Mon Sep 17 00:00:00 2001 From: Andrey Polischuk Date: Fri, 24 Jun 2022 09:04:43 +0300 Subject: [PATCH 2/4] fix script error message --- src/index.js | 2 +- tests/hcaptcha.spec.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 0d9aa2d..0cfbde4 100644 --- a/src/index.js +++ b/src/index.js @@ -31,7 +31,7 @@ const mountCaptchaScript = (params={}) => { script.async = true; script.onerror = (event) => { console.error('Failed to load api: ' + script.src, event); - rejectFn(new Error('Failed to load api')); + rejectFn('script-error'); } const query = generateQuery(params); diff --git a/tests/hcaptcha.spec.js b/tests/hcaptcha.spec.js index 3a8de47..1822f76 100644 --- a/tests/hcaptcha.spec.js +++ b/tests/hcaptcha.spec.js @@ -504,13 +504,13 @@ describe("hCaptcha", () => { const script = document.querySelector("head > script"); expect(onError.mock.calls.length).toBe(0); - script.onerror('api-script-failed'); + script.onerror(new Error('loading failed')); // simulate microtask await Promise.reject().catch(() => null) expect(onError.mock.calls.length).toBe(1); - expect(onError.mock.calls[0][0]).toEqual(new Error('Failed to load api')); + expect(onError.mock.calls[0][0]).toEqual('script-error'); }); }); }); From a21441e74520e5e6bc9c7caefe3a844de4f019e6 Mon Sep 17 00:00:00 2001 From: Andrey Polischuk Date: Fri, 24 Jun 2022 09:05:25 +0300 Subject: [PATCH 3/4] use handleError to script errors --- src/index.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/index.js b/src/index.js index 0cfbde4..aa2bd6a 100644 --- a/src/index.js +++ b/src/index.js @@ -54,7 +54,6 @@ class HCaptcha extends React.Component { // Event Handlers this.handleOnLoad = this.handleOnLoad.bind(this); - this.handleOnLoadingError = this.handleOnLoadingError.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleExpire = this.handleExpire.bind(this); this.handleError = this.handleError.bind(this); @@ -95,7 +94,7 @@ class HCaptcha extends React.Component { // Only create the script tag once, use a global promise to track mountCaptchaScript(mountParams) .then(this.handleOnLoad) - .catch(this.handleOnLoadingError); + .catch(this.handleError); } else { this.renderCaptcha(); } @@ -195,12 +194,6 @@ class HCaptcha extends React.Component { }); } - handleOnLoadingError (event) { - const { onError } = this.props; - - if (onError) onError(event); - } - handleSubmit (event) { const { onVerify } = this.props; const { isRemoved, captchaId } = this.state; @@ -229,11 +222,11 @@ class HCaptcha extends React.Component { const { onError } = this.props; const { captchaId } = this.state; - if (!this.isReady()) { - return; + if (this.isReady()) { + // If hCaptcha runs into error, reset captcha - hCaptcha + hcaptcha.reset(captchaId); } - hcaptcha.reset(captchaId) // If hCaptcha runs into error, reset captcha - hCaptcha if (onError) onError(event); } From 7535cf617a3a6ab300d4c7bb4887f317cd293fea Mon Sep 17 00:00:00 2001 From: Andrey Polischuk Date: Sat, 25 Jun 2022 17:58:57 +0300 Subject: [PATCH 4/4] remove extraneous log an error to console --- src/index.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index aa2bd6a..bf51ebb 100644 --- a/src/index.js +++ b/src/index.js @@ -29,10 +29,7 @@ const mountCaptchaScript = (params={}) => { script.id = SCRIPT_ID; script.src = `${domain}/1/api.js?render=explicit&onload=${HCAPTCHA_LOAD_FN_NAME}`; script.async = true; - script.onerror = (event) => { - console.error('Failed to load api: ' + script.src, event); - rejectFn('script-error'); - } + script.onerror = (event) => rejectFn('script-error'); const query = generateQuery(params); script.src += query !== ""? `&${query}` : "";