Skip to content

Commit

Permalink
Merge pull request #205 from hCaptcha/feat/loader
Browse files Browse the repository at this point in the history
feat(loader): Use @hcaptcha/loader package for loading script
  • Loading branch information
faris-imi authored Nov 9, 2023
2 parents d259497 + b7c989a commit 78126ef
Show file tree
Hide file tree
Showing 13 changed files with 169 additions and 5,554 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ return <HCaptcha ref={captchaRef} onLoad={onLoad} sitekey={sitekey} {...props} /
|`imghost`|String|No|`-`|See enterprise docs.|
|`reportapi`|String|No|`-`|See enterprise docs.|
|`sentry`|String|No|`-`|See enterprise docs.|
| `cleanup` | Boolean | No | `true` | Remove script tag after setup.|
|`custom`|Boolean|No|`-`|See enterprise docs.|
|`loadAsync`|Boolean|No|`true`|Set if the script should be loaded asynchronously.|
|`scriptLocation`|Element|No|`document.head`| Location of where to append the script tag. Make sure to add it to an area that will persist to prevent loading multiple times in the same document view. Note: If `null` is provided, the `document.head` will be used.|
Expand Down
3 changes: 2 additions & 1 deletion demo/app/examples/AsyncExample.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,10 +64,11 @@ export function AsyncExample() {
onClose={handleClose}
onError={handleError}
onChalExpired={handleChallengeExpired}
sentry={false}
/>
<button onClick={executeCaptcha}>Execute asynchronously</button>
<button onClick={getRespKey}>Get Response Key</button>
<button onClick={getResponse}>Get Response</button>
</div>
);
}
}
3 changes: 2 additions & 1 deletion demo/app/examples/ClassExample.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export class ClassExample extends React.Component {
onClose={this.handleClose}
onError={this.handleError}
onChalExpired={this.handleChallengeExpired}
sentry={false}
/>
{isVerified &&
<div>
Expand All @@ -84,4 +85,4 @@ export class ClassExample extends React.Component {
</div>
);
}
}
}
3 changes: 2 additions & 1 deletion demo/app/examples/FrameExample.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function FrameExample({ document }) {
onChalExpired={handleChallengeExpired}
scriptLocation={document.head}
challenge-container={document.body}
sentry={false}
/>
);
}
}
17 changes: 14 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hcaptcha/react-hcaptcha",
"version": "1.8.1",
"version": "1.9.0",
"types": "types/index.d.ts",
"main": "dist/index.js",
"module": "dist/esm/index.js",
Expand Down Expand Up @@ -58,6 +58,7 @@
"webpack-dev-server": "^4.13.1"
},
"dependencies": {
"@babel/runtime": "^7.17.9"
"@babel/runtime": "^7.17.9",
"@hcaptcha/loader": "^1.0.8"
}
}
13 changes: 13 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export const scopeTag = {
key: 'source',
value: '@hCaptcha/react'
}

export const breadcrumbMessages = {
mounted: 'hCaptcha component mounted',
expired: 'hCaptcha expired',
unmounted: 'hCaptcha component unmounted',
reset: 'hCaptcha reset',
removed: 'hCaptcha removed'

}
135 changes: 68 additions & 67 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,8 @@
import * as React from 'react';
import { generateQuery, getFrame, getMountElement } from './utils.js';
import { hCaptchaLoader, initSentry } from '@hcaptcha/loader';

const SCRIPT_ID = 'hcaptcha-api-script-id';
const HCAPTCHA_LOAD_FN_NAME = 'hcaptchaOnLoad';

// Prevent loading API script multiple times
const scripts = [];

// Generate hCaptcha API script
const mountCaptchaScript = (params = {}) => {
const element = getMountElement(params.scriptLocation);
delete params.scriptLocation;

const frame = getFrame(element);
const script = scripts.find(({ scope }) => scope === frame.window);

if (frame.document.getElementById(SCRIPT_ID) && script) {
// API was already requested
return script.promise;
}

const promise = new Promise((resolve, reject) => {
// Create global onload callback
frame.window[HCAPTCHA_LOAD_FN_NAME] = resolve;

const domain = params.apihost || "https://js.hcaptcha.com";
delete params.apihost;

const script = frame.document.createElement("script");
script.id = SCRIPT_ID;
script.src = `${domain}/1/api.js?render=explicit&onload=${HCAPTCHA_LOAD_FN_NAME}`;

script.async = params.loadAsync !== undefined? params.loadAsync : true;
delete params.loadAsync;

script.onerror = (event) => reject('script-error');

const query = generateQuery(params);
script.src += query !== ""? `&${query}` : "";

element.appendChild(script);
});

scripts.push({ promise, scope: frame.window });

return promise;
};
import { getFrame, getMountElement } from './utils.js';
import { breadcrumbMessages, scopeTag } from "./constants";


class HCaptcha extends React.Component {
Expand Down Expand Up @@ -78,6 +35,7 @@ class HCaptcha extends React.Component {

this.ref = React.createRef();
this.apiScriptRequested = false;
this.sentryHub = null;

this.state = {
isApiReady: false,
Expand All @@ -94,6 +52,13 @@ class HCaptcha extends React.Component {

const isApiReady = typeof this._hcaptcha !== 'undefined';

this.sentryHub = initSentry(this.props.sentry, scopeTag);

this.sentryHub.addBreadcrumb({
category: scopeTag.value,
message: breadcrumbMessages.mounted,
});

/*
* Check if hCaptcha has already been loaded,
* If Yes, render the captcha
Expand Down Expand Up @@ -126,6 +91,11 @@ class HCaptcha extends React.Component {
// Reset any stored variables / timers when unmounting
hcaptcha.reset(captchaId);
hcaptcha.remove(captchaId);

this.sentryHub.addBreadcrumb({
category: scopeTag.value,
message: breadcrumbMessages.unmounted,
});
}

shouldComponentUpdate(nextProps, nextState) {
Expand Down Expand Up @@ -168,26 +138,30 @@ class HCaptcha extends React.Component {
sentry,
custom,
loadAsync,
scriptLocation
scriptLocation,
cleanup = true,
} = this.props;
const mountParams = {
render: 'explicit',
apihost,
assethost,
endpoint,
hl,
host,
imghost,
recaptchacompat: reCaptchaCompat === false? "off" : null,
recaptchacompat: reCaptchaCompat === false? 'off' : null,
reportapi,
sentry,
custom,
loadAsync,
scriptLocation,
cleanup
};

mountCaptchaScript(mountParams)
.then(this.handleOnLoad)
.catch(this.handleError);
hCaptchaLoader(mountParams)
.then(this.handleOnLoad, this.handleError)
.catch(this.handleError);

this.apiScriptRequested = true;
}

Expand Down Expand Up @@ -225,6 +199,11 @@ class HCaptcha extends React.Component {
}
// Reset captcha state, removes stored token and unticks checkbox
hcaptcha.reset(captchaId)

this.sentryHub.addBreadcrumb({
category: scopeTag.value,
message: breadcrumbMessages.reset,
});
}

removeCaptcha(callback) {
Expand All @@ -239,21 +218,33 @@ class HCaptcha extends React.Component {
hcaptcha.remove(captchaId);
callback && callback()
});


this.sentryHub.addBreadcrumb({
category: scopeTag.value,
message: breadcrumbMessages.removed,
});
}

handleOnLoad () {
handleOnLoad () {
this.setState({ isApiReady: true }, () => {
const element = getMountElement(this.props.scriptLocation);
const frame = getFrame(element);
try {
const element = getMountElement(this.props.scriptLocation);
const frame = getFrame(element);

this._hcaptcha = frame.window.hcaptcha;

this._hcaptcha = frame.window.hcaptcha;

// render captcha and wait for captcha id
this.renderCaptcha(() => {
// render captcha and wait for captcha id
this.renderCaptcha(() => {
// trigger onLoad if it exists

const { onLoad } = this.props;
if (onLoad) onLoad();
});
});
} catch (error) {
this.sentryHub.captureException(error);
}
});
}

Expand Down Expand Up @@ -281,6 +272,11 @@ class HCaptcha extends React.Component {
hcaptcha.reset(captchaId) // If hCaptcha runs into error, reset captcha - hCaptcha

if (onExpire) onExpire();

this.sentryHub.addBreadcrumb({
category: scopeTag.value,
message: breadcrumbMessages.expired,
});
}

handleError (event) {
Expand Down Expand Up @@ -327,18 +323,23 @@ class HCaptcha extends React.Component {
}

execute (opts = null) {
const { captchaId } = this.state;
const hcaptcha = this._hcaptcha;
try {
const { captchaId } = this.state;
const hcaptcha = this._hcaptcha;

if (!this.isReady()) {
return;
}

if (opts && typeof opts !== "object") {
opts = null;
}
if (!this.isReady()) {
return;
}

return hcaptcha.execute(captchaId, opts);
if (opts && typeof opts !== "object") {
opts = null;
}

return hcaptcha.execute(captchaId, opts);
} catch (error) {
this.sentryHub.captureException(error);
}
}

setData (data) {
Expand Down
11 changes: 1 addition & 10 deletions src/utils.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
function generateQuery(params) {
return Object.entries(params)
.filter(([key, value]) => value || value === false)
.map(([key, value]) => {
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
}).join("&");
};

function getFrame(element) {
const doc = (element && element.ownerDocument) || document;
const win = doc.defaultView || doc.parentWindow || window;
Expand All @@ -18,7 +10,6 @@ function getMountElement(element) {
}

export {
generateQuery,
getFrame,
getMountElement
};
};
Loading

0 comments on commit 78126ef

Please sign in to comment.