mirror of
https://github.com/scratchfoundation/scratch-www.git
synced 2024-11-26 00:58:14 -05:00
Merge pull request #3765 from picklesrus/captcha-component
Move reCaptcha code to a component
This commit is contained in:
commit
7548253b17
4 changed files with 154 additions and 71 deletions
76
src/components/captcha/captcha.jsx
Normal file
76
src/components/captcha/captcha.jsx
Normal file
|
@ -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 (
|
||||
<div
|
||||
className="g-recaptcha"
|
||||
data-badge="bottomright"
|
||||
data-sitekey={process.env.RECAPTCHA_SITE_KEY}
|
||||
data-size="invisible"
|
||||
ref={this.setCaptchaRef}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
Captcha.propTypes = {
|
||||
onCaptchaError: PropTypes.func.isRequired,
|
||||
onCaptchaLoad: PropTypes.func.isRequired,
|
||||
onCaptchaSolved: PropTypes.func.isRequired
|
||||
};
|
||||
module.exports = Captcha;
|
|
@ -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"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="g-recaptcha"
|
||||
data-badge="bottomright"
|
||||
data-sitekey={process.env.RECAPTCHA_SITE_KEY}
|
||||
data-size="invisible"
|
||||
<Captcha
|
||||
ref={this.setCaptchaRef}
|
||||
onCaptchaError={this.props.onCaptchaError}
|
||||
onCaptchaLoad={this.handleCaptchaLoad}
|
||||
onCaptchaSolved={this.handleCaptchaSolved}
|
||||
/>
|
||||
</JoinFlowStep>
|
||||
);
|
||||
|
|
57
test/unit/components/captcha.test.jsx
Normal file
57
test/unit/components/captcha.test.jsx
Normal file
|
@ -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(<Captcha
|
||||
{...props}
|
||||
/>);
|
||||
wrapper.instance().onCaptchaLoad();
|
||||
expect(global.grecaptcha.render).toHaveBeenCalled();
|
||||
expect(props.onCaptchaLoad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Captcha execute calls grecatpcha execute', () => {
|
||||
const props = {
|
||||
onCaptchaLoad: jest.fn()
|
||||
};
|
||||
const wrapper = enzyme.shallow(<Captcha
|
||||
{...props}
|
||||
/>);
|
||||
wrapper.instance().executeCaptcha();
|
||||
expect(global.grecaptcha.execute).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Captcha load calls props captchaOnLoad', () => {
|
||||
const props = {
|
||||
onCaptchaLoad: jest.fn()
|
||||
};
|
||||
const wrapper = enzyme.shallow(<Captcha
|
||||
{...props}
|
||||
/>);
|
||||
wrapper.instance().onCaptchaLoad();
|
||||
expect(global.grecaptcha.render).toHaveBeenCalled();
|
||||
expect(props.onCaptchaLoad).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Captcha renders the div google wants', () => {
|
||||
const props = {
|
||||
onCaptchaLoad: jest.fn()
|
||||
};
|
||||
const wrapper = enzyme.mount(<Captcha
|
||||
{...props}
|
||||
/>);
|
||||
expect(wrapper.find('div.g-recaptcha')).toHaveLength(1);
|
||||
});
|
||||
});
|
|
@ -115,9 +115,9 @@ describe('EmailStep test', () => {
|
|||
const formikBag = {
|
||||
setSubmitting: jest.fn()
|
||||
};
|
||||
global.grecaptcha = {
|
||||
execute: jest.fn(),
|
||||
render: jest.fn()
|
||||
|
||||
const captchaRef = {
|
||||
executeCaptcha: jest.fn()
|
||||
};
|
||||
const formData = {item1: 'thing', item2: 'otherthing'};
|
||||
const intlWrapper = shallowWithIntl(
|
||||
|
@ -125,13 +125,12 @@ describe('EmailStep test', () => {
|
|||
{...defaultProps()}
|
||||
/>);
|
||||
|
||||
|
||||
const emailStepWrapper = intlWrapper.dive();
|
||||
emailStepWrapper.instance().onCaptchaLoad(); // to setup catpcha state
|
||||
emailStepWrapper.instance().setCaptchaRef(captchaRef);
|
||||
emailStepWrapper.instance().handleValidSubmit(formData, formikBag);
|
||||
|
||||
expect(formikBag.setSubmitting).toHaveBeenCalledWith(false);
|
||||
expect(global.grecaptcha.execute).toHaveBeenCalled();
|
||||
expect(captchaRef.executeCaptcha).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('captchaSolved sets token and goes to next step', () => {
|
||||
|
@ -141,9 +140,8 @@ describe('EmailStep test', () => {
|
|||
const formikBag = {
|
||||
setSubmitting: jest.fn()
|
||||
};
|
||||
global.grecaptcha = {
|
||||
execute: jest.fn(),
|
||||
render: jest.fn()
|
||||
const captchaRef = {
|
||||
executeCaptcha: jest.fn()
|
||||
};
|
||||
const formData = {item1: 'thing', item2: 'otherthing'};
|
||||
const intlWrapper = shallowWithIntl(
|
||||
|
@ -154,11 +152,11 @@ describe('EmailStep test', () => {
|
|||
|
||||
const emailStepWrapper = intlWrapper.dive();
|
||||
// Call these to setup captcha.
|
||||
emailStepWrapper.instance().onCaptchaLoad(); // to setup catpcha state
|
||||
emailStepWrapper.instance().setCaptchaRef(captchaRef); // to setup catpcha state
|
||||
emailStepWrapper.instance().handleValidSubmit(formData, formikBag);
|
||||
|
||||
const captchaToken = 'abcd';
|
||||
emailStepWrapper.instance().captchaSolved(captchaToken);
|
||||
emailStepWrapper.instance().handleCaptchaSolved(captchaToken);
|
||||
// Make sure captchaSolved calls onNextStep with formData that has
|
||||
// a captcha token and left everything else in the object in place.
|
||||
expect(props.onNextStep).toHaveBeenCalledWith(
|
||||
|
@ -170,29 +168,13 @@ describe('EmailStep test', () => {
|
|||
expect(formikBag.setSubmitting).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
test('Captcha load error calls error function', () => {
|
||||
const props = {
|
||||
onCaptchaError: jest.fn()
|
||||
};
|
||||
// Set this to null to force an error.
|
||||
global.grecaptcha = null;
|
||||
const intlWrapper = shallowWithIntl(
|
||||
<EmailStep
|
||||
{...defaultProps()}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
const emailStepWrapper = intlWrapper.dive();
|
||||
emailStepWrapper.instance().onCaptchaLoad();
|
||||
expect(props.onCaptchaError).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('Component logs analytics', () => {
|
||||
const sendAnalyticsFn = jest.fn();
|
||||
const onCaptchaError = jest.fn();
|
||||
mountWithIntl(
|
||||
<EmailStep
|
||||
sendAnalytics={sendAnalyticsFn}
|
||||
onCaptchaError={onCaptchaError}
|
||||
/>);
|
||||
expect(sendAnalyticsFn).toHaveBeenCalledWith('join-email');
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue