diff --git a/src/components/captcha/captcha.jsx b/src/components/captcha/captcha.jsx new file mode 100644 index 000000000..cb51f4d79 --- /dev/null +++ b/src/components/captcha/captcha.jsx @@ -0,0 +1,76 @@ +const bindAll = require('lodash.bindall'); +const PropTypes = require('prop-types'); +const React = require('react'); + +class Captcha extends React.Component { + constructor (props) { + super(props); + bindAll(this, [ + 'setCaptchaRef', + 'onCaptchaLoad', + 'executeCaptcha' + ]); + + } + componentDidMount () { + if (window.grecaptcha) { + this.onCaptchaLoad(); + } else { + // If grecaptcha doesn't exist on window, we havent loaded the captcha js yet. Load it. + // ReCaptcha calls a callback when the grecatpcha object is usable. That callback + // needs to be global so set it on the window. + window.grecaptchaOnLoad = this.onCaptchaLoad; + // Load Google ReCaptcha script. + const script = document.createElement('script'); + script.async = true; + script.onerror = this.props.onCaptchaError; + script.src = `https://www.recaptcha.net/recaptcha/api.js?onload=grecaptchaOnLoad&render=explicit&hl=${window._locale}`; + document.body.appendChild(script); + } + } + componentWillUnmount () { + window.grecaptchaOnLoad = null; + } + + onCaptchaLoad () { + // Let the owner of this component do some work + // when captcha is done loading (e.g. enabling a button) + this.props.onCaptchaLoad(); + this.grecaptcha = window.grecaptcha; + if (!this.grecaptcha) { + // According to the reCaptcha documentation, this callback shouldn't get + // called unless window.grecaptcha exists. This is just here to be extra defensive. + this.props.onCaptchaError(); + return; + } + this.widgetId = this.grecaptcha.render(this.captchaRef, + { + callback: this.props.onCaptchaSolved, + sitekey: process.env.RECAPTCHA_SITE_KEY + }, + true); + } + setCaptchaRef (ref) { + this.captchaRef = ref; + } + executeCaptcha () { + this.grecaptcha.execute(this.widgetId); + } + render () { + return ( +
+ ); + } +} +Captcha.propTypes = { + onCaptchaError: PropTypes.func, + onCaptchaLoad: PropTypes.func, + onCaptchaSolved: PropTypes.func +}; +module.exports = Captcha; diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx index d73b08d89..48fe7dad7 100644 --- a/src/components/join-flow/email-step.jsx +++ b/src/components/join-flow/email-step.jsx @@ -11,7 +11,7 @@ const JoinFlowStep = require('./join-flow-step.jsx'); const FormikInput = require('../../components/formik-forms/formik-input.jsx'); const FormikCheckbox = require('../../components/formik-forms/formik-checkbox.jsx'); const InfoButton = require('../info-button/info-button.jsx'); - +const Captcha = require('../../components/captcha/captcha.jsx'); require('./join-flow-steps.scss'); class EmailStep extends React.Component { @@ -24,8 +24,8 @@ class EmailStep extends React.Component { 'validateEmailRemotelyWithCache', 'validateForm', 'setCaptchaRef', - 'captchaSolved', - 'onCaptchaLoad' + 'handleCaptchaSolved', + 'handleCaptchaLoad' ]); this.state = { captchaIsLoading: true @@ -40,43 +40,12 @@ class EmailStep extends React.Component { } // automatically start with focus on username field if (this.emailInput) this.emailInput.focus(); - - if (window.grecaptcha) { - this.onCaptchaLoad(); - } else { - // If grecaptcha doesn't exist on window, we havent loaded the captcha js yet. Load it. - // ReCaptcha calls a callback when the grecatpcha object is usable. That callback - // needs to be global so set it on the window. - window.grecaptchaOnLoad = this.onCaptchaLoad; - // Load Google ReCaptcha script. - const script = document.createElement('script'); - script.async = true; - script.onerror = this.props.onCaptchaError; - script.src = `https://www.recaptcha.net/recaptcha/api.js?onload=grecaptchaOnLoad&render=explicit&hl=${window._locale}`; - document.body.appendChild(script); - } - } - componentWillUnmount () { - window.grecaptchaOnLoad = null; } handleSetEmailRef (emailInputRef) { this.emailInput = emailInputRef; } - onCaptchaLoad () { + handleCaptchaLoad () { this.setState({captchaIsLoading: false}); - this.grecaptcha = window.grecaptcha; - if (!this.grecaptcha) { - // According to the reCaptcha documentation, this callback shouldn't get - // called unless window.grecaptcha exists. This is just here to be extra defensive. - this.props.onCaptchaError(); - return; - } - this.widgetId = this.grecaptcha.render(this.captchaRef, - { - callback: this.captchaSolved, - sitekey: process.env.RECAPTCHA_SITE_KEY - }, - true); } // simple function to memoize remote requests for usernames validateEmailRemotelyWithCache (email) { @@ -116,9 +85,9 @@ class EmailStep extends React.Component { // Change set submitting to false so that if the user clicks out of // the captcha, the button is clickable again (instead of a disabled button with a spinner). this.formikBag.setSubmitting(false); - this.grecaptcha.execute(this.widgetId); + this.captchaRef.executeCaptcha(); } - captchaSolved (token) { + handleCaptchaSolved (token) { // Now thatcaptcha is done, we can tell Formik we're submitting. this.formikBag.setSubmitting(true); this.formData['g-recaptcha-response'] = token; @@ -224,12 +193,11 @@ class EmailStep extends React.Component { name="subscribe" /> - ); diff --git a/test/unit/components/captcha.test.jsx b/test/unit/components/captcha.test.jsx new file mode 100644 index 000000000..027f721c6 --- /dev/null +++ b/test/unit/components/captcha.test.jsx @@ -0,0 +1,57 @@ +const React = require('react'); +const enzyme = require('enzyme'); + + +const Captcha = require('../../../src/components/captcha/captcha.jsx'); + +describe('Captcha test', () => { + global.grecaptcha = { + execute: jest.fn(), + render: jest.fn() + }; + + test('Captcha load calls props captchaOnLoad', () => { + const props = { + onCaptchaLoad: jest.fn() + }; + const wrapper = enzyme.shallow(