diff --git a/README.md b/README.md index 00dda1b..49693e3 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,9 @@ return { } catch (error) { console.log(error); } - } + }; + + const handleOpen = () => { + console.log("HCaptcha [onOpen]: The user display of a challenge starts."); + }; + + const handleClose = () => { + console.log("HCaptcha [onClose]: The user dismisses a challenge."); + }; + + const handleChallengeExpired = () => { + console.log("HCaptcha [onChalExpired]: The user display of a challenge times out with no answer."); + }; return (
- undefined} + onOpen={handleOpen} + onClose={handleClose} + onChalExpired={handleChallengeExpired} />
@@ -39,6 +55,9 @@ class ReactDemo extends React.Component { this.handleChange = this.handleChange.bind(this); this.handleReset = this.handleReset.bind(this); this.onVerifyCaptcha = this.onVerifyCaptcha.bind(this); + this.handleOpen = this.handleOpen.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleChallengeExpired = this.handleChallengeExpired.bind(this); // Leave languageOverride unset or null for browser autodetection. // To force a language, use the code: https://hcaptcha.com/docs/languages this.languageOverride = null; // "fr"; @@ -48,7 +67,7 @@ class ReactDemo extends React.Component { this.setState({isVerified: true}); } - onVerifyCaptcha (token) { + onVerifyCaptcha(token) { console.log("Verified: " + token); this.setState({isVerified: true}) } @@ -64,6 +83,18 @@ class ReactDemo extends React.Component { this.setState({isVerified: false}) } + handleOpen() { + console.log("HCaptcha [onOpen]: The user display of a challenge starts."); + } + + handleClose() { + console.log("HCaptcha [onClose]: The user dismisses a challenge."); + } + + handleChallengeExpired() { + console.log("HCaptcha [onChalExpired]: The user display of a challenge times out with no answer."); + } + render() { const { isVerified } = this.state; @@ -85,24 +116,42 @@ class ReactDemo extends React.Component { {!this.state.async ? ( <>
-
-
-
{isVerified && diff --git a/package.json b/package.json index ab5036b..f6295d7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hcaptcha/react-hcaptcha", - "version": "1.0.0", + "version": "1.1.0", "types": "types/index.d.ts", "main": "dist/index.js", "files": [ diff --git a/src/index.js b/src/index.js index 2c77c6b..ac33bd1 100644 --- a/src/index.js +++ b/src/index.js @@ -39,12 +39,16 @@ class HCaptcha extends React.Component { this.renderCaptcha = this.renderCaptcha.bind(this); this.resetCaptcha = this.resetCaptcha.bind(this); this.removeCaptcha = this.removeCaptcha.bind(this); + this.isReady = this.isReady.bind(this); // Event Handlers this.handleOnLoad = this.handleOnLoad.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleExpire = this.handleExpire.bind(this); this.handleError = this.handleError.bind(this); + this.handleOpen = this.handleOpen.bind(this); + this.handleClose = this.handleClose.bind(this); + this.handleChallengeExpired = this.handleChallengeExpired.bind(this); const isApiReady = typeof hcaptcha !== 'undefined'; @@ -88,8 +92,11 @@ class HCaptcha extends React.Component { } componentWillUnmount() { - const { isApiReady, isRemoved, captchaId } = this.state; - if(!isApiReady || isRemoved) return + const { captchaId } = this.state; + + if (!this.isReady()) { + return; + } // Reset any stored variables / timers when unmounting hcaptcha.reset(captchaId); @@ -124,7 +131,10 @@ class HCaptcha extends React.Component { if (!isApiReady) return; const renderParams = Object.assign({ + "open-callback" : this.handleOpen, + "close-callback" : this.handleClose, "error-callback" : this.handleError, + "chalexpired-callback": this.handleChallengeExpired, "expired-callback" : this.handleExpire, "callback" : this.handleSubmit, }, this.props, { @@ -141,17 +151,21 @@ class HCaptcha extends React.Component { } resetCaptcha() { - const { isApiReady, isRemoved, captchaId } = this.state; + const { captchaId } = this.state; - if (!isApiReady || isRemoved) return + if (!this.isReady()) { + return; + } // Reset captcha state, removes stored token and unticks checkbox hcaptcha.reset(captchaId) } removeCaptcha(callback) { - const { isApiReady, isRemoved, captchaId } = this.state; + const { captchaId } = this.state; - if (!isApiReady || isRemoved) return + if (!this.isReady()) { + return; + } this.setState({ isRemoved: true }, () => { hcaptcha.remove(captchaId); @@ -184,9 +198,12 @@ class HCaptcha extends React.Component { handleExpire () { const { onExpire } = this.props; - const { isApiReady, isRemoved, captchaId } = this.state; + const { captchaId } = this.state; + + if (!this.isReady()) { + return; + } - if (!isApiReady || isRemoved) return hcaptcha.reset(captchaId) // If hCaptcha runs into error, reset captcha - hCaptcha if (onExpire) onExpire(); @@ -194,18 +211,52 @@ class HCaptcha extends React.Component { handleError (event) { const { onError } = this.props; - const { isApiReady, isRemoved, captchaId } = this.state; + const { captchaId } = this.state; - if (!isApiReady || isRemoved) return + if (!this.isReady()) { + return; + } hcaptcha.reset(captchaId) // If hCaptcha runs into error, reset captcha - hCaptcha if (onError) onError(event); } + isReady () { + const { isApiReady, isRemoved } = this.state; + + return isApiReady && !isRemoved; + } + + handleOpen () { + if (!this.isReady() || !this.props.onOpen) { + return; + } + + this.props.onOpen(); + } + + handleClose () { + if (!this.isReady() || !this.props.onClose) { + return; + } + + this.props.onClose(); + } + + handleChallengeExpired () { + if (!this.isReady() || !this.props.onChalExpired) { + return; + } + + this.props.onChalExpired(); + } + execute (opts = null) { - const { isApiReady, isRemoved, captchaId } = this.state; + const { captchaId } = this.state; - if (!isApiReady || isRemoved) return; + if (!this.isReady()) { + return; + } if (opts && typeof opts !== "object") { opts = null; diff --git a/tests/hcaptcha.spec.js b/tests/hcaptcha.spec.js index b39a024..0b3b8ef 100644 --- a/tests/hcaptcha.spec.js +++ b/tests/hcaptcha.spec.js @@ -28,6 +28,9 @@ describe("hCaptcha", () => { onError: jest.fn(), onExpire: jest.fn(), onLoad: jest.fn(), + onOpen: jest.fn(), + onClose: jest.fn(), + onChalExpired: jest.fn(), }; window.hcaptcha = getMockedHcaptcha(); instance = ReactTestUtils.renderIntoDocument( @@ -41,6 +44,9 @@ describe("hCaptcha", () => { onError={mockFns.onError} onExpire={mockFns.onExpire} onLoad={mockFns.onLoad} + onOpen={mockFns.onOpen} + onClose={mockFns.onClose} + onChalExpired={mockFns.onChalExpired} />, ); }); @@ -144,6 +150,111 @@ describe("hCaptcha", () => { expect(window.hcaptcha.reset.mock.calls.length).toBe(1); }); + describe('onOpen callback', () => { + afterAll(() => { + jest.restoreAllMocks(); + }); + + it("should be called if the captcha is ready and the callback is provided as a prop", () => { + jest.spyOn(instance, 'isReady').mockImplementation(() => true); + + expect(mockFns.onOpen.mock.calls.length).toBe(0); + instance.handleOpen(); + expect(mockFns.onOpen.mock.calls.length).toBe(1); + }); + + it("should not be called if the captcha is not ready", () => { + jest.spyOn(instance, 'isReady').mockImplementation(() => false); + + expect(mockFns.onOpen.mock.calls.length).toBe(0); + instance.handleOpen(); + expect(mockFns.onOpen.mock.calls.length).toBe(0); + }); + + it("should not be called if not provided as a prop", () => { + instance = ReactTestUtils.renderIntoDocument( + , + ); + jest.spyOn(instance, 'isReady').mockImplementation(() => true); + + expect(mockFns.onOpen.mock.calls.length).toBe(0); + instance.handleOpen(); + expect(mockFns.onOpen.mock.calls.length).toBe(0); + }); + }); + + describe('onClose callback', () => { + afterAll(() => { + jest.restoreAllMocks(); + }); + + it("should be called if the captcha is ready and the callback is provided as a prop", () => { + jest.spyOn(instance, 'isReady').mockImplementation(() => true); + + expect(mockFns.onClose.mock.calls.length).toBe(0); + instance.handleClose(); + expect(mockFns.onClose.mock.calls.length).toBe(1); + }); + + it("should not be called if the captcha is not ready", () => { + jest.spyOn(instance, 'isReady').mockImplementation(() => false); + + expect(mockFns.onClose.mock.calls.length).toBe(0); + instance.handleClose(); + expect(mockFns.onClose.mock.calls.length).toBe(0); + }); + + it("should not be called if not provided as a prop", () => { + instance = ReactTestUtils.renderIntoDocument( + , + ); + jest.spyOn(instance, 'isReady').mockImplementation(() => true); + + expect(mockFns.onClose.mock.calls.length).toBe(0); + instance.handleClose(); + expect(mockFns.onClose.mock.calls.length).toBe(0); + }); + }); + + describe('onChalExpired callback', () => { + afterAll(() => { + jest.restoreAllMocks(); + }); + + it("should be called if the captcha is ready and the callback is provided as a prop", () => { + jest.spyOn(instance, 'isReady').mockImplementation(() => true); + + expect(mockFns.onChalExpired.mock.calls.length).toBe(0); + instance.handleChallengeExpired(); + expect(mockFns.onChalExpired.mock.calls.length).toBe(1); + }); + + it("should not be called if the captcha is not ready", () => { + jest.spyOn(instance, 'isReady').mockImplementation(() => false); + + expect(mockFns.onClose.mock.calls.length).toBe(0); + instance.handleClose(); + expect(mockFns.onClose.mock.calls.length).toBe(0); + }); + + it("should not be called if not provided as a prop", () => { + instance = ReactTestUtils.renderIntoDocument( + , + ); + jest.spyOn(instance, 'isReady').mockImplementation(() => true); + + expect(mockFns.onChalExpired.mock.calls.length).toBe(0); + instance.handleChallengeExpired(); + expect(mockFns.onChalExpired.mock.calls.length).toBe(0); + }); + }); + it("el renders after api loads and a widget id is set", () => { expect(instance.state.captchaId).toBe(MOCK_WIDGET_ID); expect(window.hcaptcha.render.mock.calls.length).toBe(1); diff --git a/types/index.d.ts b/types/index.d.ts index 45ff1b4..e5706c1 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -15,6 +15,9 @@ interface HCaptchaState { interface HCaptchaProps { onExpire?: () => any; + onOpen?: () => any; + onClose?: () => any; + onChalExpired?: () => any; onError?: (event: string) => any; onVerify?: (token: string) => any; onLoad?: () => any;