From 3daba3b9075adc29e63523fb81c8db4d679212f0 Mon Sep 17 00:00:00 2001 From: picklesrus Date: Tue, 20 Aug 2019 17:05:43 -0400 Subject: [PATCH 1/9] Initial work for captcha in new join flow. TODOS: - handle error states - Setup keys for different environments. - Make sure remote validators are run before captcha. --- src/components/join-flow/email-step.jsx | 41 ++++++++++++++++++++++--- src/components/join-flow/join-flow.jsx | 10 ++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx index 98836f1e7..0f16d9992 100644 --- a/src/components/join-flow/email-step.jsx +++ b/src/components/join-flow/email-step.jsx @@ -19,13 +19,30 @@ class EmailStep extends React.Component { bindAll(this, [ 'handleSetEmailRef', 'handleValidSubmit', - 'validateEmail', - 'validateForm' + 'validateEmailIfPresent', + 'validateForm', + 'setCaptchaRef', + 'captchaSolved' ]); } + componentDidMount () { // automatically start with focus on username field if (this.emailInput) this.emailInput.focus(); + this.grecaptcha = window.grecaptcha; + if (!this.grecaptcha) { + // Captcha doesn't exist on the window. There must have been a + // problem downloading the script. There isn't much we can do about it though. + // TODO: put up the error screen when we have it. + return; + } + // TODO: Add in error callback for this once we have an error screen. + this.widgetId = this.grecaptcha.render(this.captchaRef, + { + callback: this.captchaSolved, + sitekey: '' + }, + true); } handleSetEmailRef (emailInputRef) { this.emailInput = emailInputRef; @@ -42,8 +59,17 @@ class EmailStep extends React.Component { return {}; } handleValidSubmit (formData, formikBag) { - formikBag.setSubmitting(false); - this.props.onNextStep(formData); + this.formData = formData; + this.formikBag = formikBag; + this.grecaptcha.execute(this.widgetId); + } + captchaSolved (token) { + this.formData['g-recaptcha-response'] = token; + this.formikBag.setSubmitting(false); + this.props.onNextStep(this.formData); + } + setCaptchaRef (ref) { + this.captchaRef = ref; } render () { return ( @@ -116,6 +142,13 @@ class EmailStep extends React.Component { name="subscribe" /> +
); }} diff --git a/src/components/join-flow/join-flow.jsx b/src/components/join-flow/join-flow.jsx index f743dc088..2a55ff536 100644 --- a/src/components/join-flow/join-flow.jsx +++ b/src/components/join-flow/join-flow.jsx @@ -29,6 +29,16 @@ class JoinFlow extends React.Component { step: 0 }; } + + componentDidMount () { + // Load Google Captcha script so that it is ready to go when we get to + // the last step. + const script = document.createElement('script'); + script.src = `https://www.recaptcha.net/recaptcha/api.js?render=explicit&hl=${window._locale}`; + script.async = true; + document.body.appendChild(script); + } + handleAdvanceStep (formData) { formData = formData || {}; this.setState({ From 9d4d2f3c670d1511f607d5c0877d8e6f24106f57 Mon Sep 17 00:00:00 2001 From: picklesrus Date: Tue, 20 Aug 2019 17:37:52 -0400 Subject: [PATCH 2/9] Put the captcha badge in the bottom right. --- src/components/join-flow/email-step.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx index 0f16d9992..30db18a5d 100644 --- a/src/components/join-flow/email-step.jsx +++ b/src/components/join-flow/email-step.jsx @@ -144,7 +144,7 @@ class EmailStep extends React.Component {
Date: Wed, 21 Aug 2019 13:59:49 -0400 Subject: [PATCH 3/9] Maybe setup captcha site keys properly? --- .travis.yml | 5 +++++ src/components/join-flow/email-step.jsx | 4 ++-- webpack.config.js | 2 ++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index aaf414d4e..b60270840 100644 --- a/.travis.yml +++ b/.travis.yml @@ -33,6 +33,11 @@ env: - CLOUDDATA_HOST_VAR=CLOUDDATA_HOST_$TRAVIS_BRANCH - CLOUDDATA_HOST=${!CLOUDDATA_HOST_VAR} - CLOUDDATA_HOST=${CLOUDDATA_HOST:-$CLOUDDATA_HOST_STAGING} + - RECAPTCHA_SITE_KEY_master=6LeRbUwUAAAAAFYhKgk3G9OKWqE_OJ7Z-7VTUCbl + - RECAPTCHA_SITE_KEY_STAGING=6LfukK4UAAAAAFR44yoZMhv8fj6xh-PMiIxwryG3 + - RECAPTCHA_SITE=RECAPTCHA_SITE_KEY_$TRAVIS_BRANCH + - RECAPTCHA_SITE_KEY=${!RECAPTCHA_SITE_KEY_VAR} + - RECAPTCHA_SITE_KEY=${RECAPTCHA_SITE_KEY:-$RECAPTCHA_SITE_KEY_STAGING} - ROOT_URL_master=https://scratch.mit.edu - ROOT_URL_STAGING=https://scratch.ly - ROOT_URL_VAR=ROOT_URL_$TRAVIS_BRANCH diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx index 30db18a5d..4e2b28d57 100644 --- a/src/components/join-flow/email-step.jsx +++ b/src/components/join-flow/email-step.jsx @@ -40,7 +40,7 @@ class EmailStep extends React.Component { this.widgetId = this.grecaptcha.render(this.captchaRef, { callback: this.captchaSolved, - sitekey: '' + sitekey: process.env.RECAPTCHA_SITE_KEY }, true); } @@ -145,7 +145,7 @@ class EmailStep extends React.Component {
diff --git a/webpack.config.js b/webpack.config.js index 3118a4bb5..a2fe4fcbc 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -173,6 +173,8 @@ module.exports = { new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"' + (process.env.NODE_ENV || 'development') + '"', 'process.env.API_HOST': '"' + (process.env.API_HOST || 'https://api.scratch.mit.edu') + '"', + 'process.env.RECAPTCHA_SITE_KEY': '"' + + (process.env.RECAPTCHA_SITE_KEY || '6Lf6kK4UAAAAABKTyvdSqgcSVASEnMrCquiAkjVW') + '"', 'process.env.ASSET_HOST': '"' + (process.env.ASSET_HOST || 'https://assets.scratch.mit.edu') + '"', 'process.env.BACKPACK_HOST': '"' + (process.env.BACKPACK_HOST || 'https://backpack.scratch.mit.edu') + '"', 'process.env.CLOUDDATA_HOST': '"' + (process.env.CLOUDDATA_HOST || 'clouddata.scratch.mit.edu') + '"', From f83fa4390c1cdc5d746dc24d70facb66428ed4aa Mon Sep 17 00:00:00 2001 From: picklesrus Date: Tue, 27 Aug 2019 15:42:33 -0400 Subject: [PATCH 4/9] - Undo bad merge changing name of validateEmail function - Move loading of capcha js to final step - add in onload and onerror callbacks for catpcha js loading --- src/components/join-flow/email-step.jsx | 15 ++++++++------- src/components/join-flow/join-flow.jsx | 9 --------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx index 4e2b28d57..b5c5d9ce0 100644 --- a/src/components/join-flow/email-step.jsx +++ b/src/components/join-flow/email-step.jsx @@ -19,10 +19,12 @@ class EmailStep extends React.Component { bindAll(this, [ 'handleSetEmailRef', 'handleValidSubmit', - 'validateEmailIfPresent', + 'validateEmail', 'validateForm', 'setCaptchaRef', - 'captchaSolved' + 'captchaSolved', + 'onCaptchaLoad', + 'onCaptchaError' ]); } @@ -31,12 +33,11 @@ class EmailStep extends React.Component { if (this.emailInput) this.emailInput.focus(); this.grecaptcha = window.grecaptcha; if (!this.grecaptcha) { - // Captcha doesn't exist on the window. There must have been a - // problem downloading the script. There isn't much we can do about it though. - // TODO: put up the error screen when we have it. - return; + // According to the reCaptcha documentation, this callback shouldn't get + // called unless window.grecaptcha exists. This is just here to be extra defensive. + // TODO: Put up the error screen when we have one. } - // TODO: Add in error callback for this once we have an error screen. + // TODO: Add in error callback for render once we have an error screen. this.widgetId = this.grecaptcha.render(this.captchaRef, { callback: this.captchaSolved, diff --git a/src/components/join-flow/join-flow.jsx b/src/components/join-flow/join-flow.jsx index 2a55ff536..d6867a7da 100644 --- a/src/components/join-flow/join-flow.jsx +++ b/src/components/join-flow/join-flow.jsx @@ -30,15 +30,6 @@ class JoinFlow extends React.Component { }; } - componentDidMount () { - // Load Google Captcha script so that it is ready to go when we get to - // the last step. - const script = document.createElement('script'); - script.src = `https://www.recaptcha.net/recaptcha/api.js?render=explicit&hl=${window._locale}`; - script.async = true; - document.body.appendChild(script); - } - handleAdvanceStep (formData) { formData = formData || {}; this.setState({ From b8b76f69ba123d52e92d9acb181c7aada313a66a Mon Sep 17 00:00:00 2001 From: picklesrus Date: Tue, 27 Aug 2019 15:51:48 -0400 Subject: [PATCH 5/9] Make sure the "create account" button is clickable until captcha is actually solved. --- src/components/join-flow/email-step.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx index b5c5d9ce0..33125f8b8 100644 --- a/src/components/join-flow/email-step.jsx +++ b/src/components/join-flow/email-step.jsx @@ -62,11 +62,15 @@ class EmailStep extends React.Component { handleValidSubmit (formData, formikBag) { this.formData = formData; this.formikBag = formikBag; + // 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); } captchaSolved (token) { + // Now thatcaptcha is done, we can tell Formik we're submitting. + this.formikBag.setSubmitting(true); this.formData['g-recaptcha-response'] = token; - this.formikBag.setSubmitting(false); this.props.onNextStep(this.formData); } setCaptchaRef (ref) { From 350f6e6bab219cb93f07bf294a41e92e4b33873e Mon Sep 17 00:00:00 2001 From: picklesrus Date: Tue, 27 Aug 2019 16:29:54 -0400 Subject: [PATCH 6/9] Make it so Create Account button is not clickable until captcha js loads. --- src/components/join-flow/email-step.jsx | 26 ++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx index 33125f8b8..d93a21a53 100644 --- a/src/components/join-flow/email-step.jsx +++ b/src/components/join-flow/email-step.jsx @@ -26,11 +26,35 @@ class EmailStep extends React.Component { 'onCaptchaLoad', 'onCaptchaError' ]); + this.state = { + captchaIsLoading: true + }; } componentDidMount () { +<<<<<<< HEAD // automatically start with focus on username field if (this.emailInput) this.emailInput.focus(); +======= + // 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.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; + } + onCaptchaError () { + // TODO send user to error step once we have one. + } + onCaptchaLoad () { + this.setState({captchaIsLoading: false}); +>>>>>>> Make it so Create Account button is not clickable until captcha js loads. this.grecaptcha = window.grecaptcha; if (!this.grecaptcha) { // According to the reCaptcha documentation, this callback shouldn't get @@ -119,7 +143,7 @@ class EmailStep extends React.Component { innerClassName="join-flow-inner-email-step" nextButton={this.props.intl.formatMessage({id: 'registration.createAccount'})} title={this.props.intl.formatMessage({id: 'registration.emailStepTitle'})} - waiting={isSubmitting} + waiting={isSubmitting || this.state.captchaIsLoading} onSubmit={handleSubmit} > Date: Tue, 27 Aug 2019 16:36:10 -0400 Subject: [PATCH 7/9] Only load the captcha js if it hasn't been loaded yet. --- src/components/join-flow/email-step.jsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx index d93a21a53..c550e3de6 100644 --- a/src/components/join-flow/email-step.jsx +++ b/src/components/join-flow/email-step.jsx @@ -32,6 +32,7 @@ class EmailStep extends React.Component { } componentDidMount () { +<<<<<<< HEAD <<<<<<< HEAD // automatically start with focus on username field if (this.emailInput) this.emailInput.focus(); @@ -45,6 +46,20 @@ class EmailStep extends React.Component { script.onerror = this.onCaptchaError; script.src = `https://www.recaptcha.net/recaptcha/api.js?onload=grecaptchaOnLoad&render=explicit&hl=${window._locale}`; document.body.appendChild(script); +======= + // If grecaptcha doesn't exist on window, we havent loaded the captcha js yet. Load it. + if (!window.grecaptcha) { + // 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.onCaptchaError; + script.src = `https://www.recaptcha.net/recaptcha/api.js?onload=grecaptchaOnLoad&render=explicit&hl=${window._locale}`; + document.body.appendChild(script); + } +>>>>>>> Only load the captcha js if it hasn't been loaded yet. } componentWillUnmount () { window.grecaptchaOnLoad = null; From c4d6e3dcef31ef90fdecd06e027616f07e417d5f Mon Sep 17 00:00:00 2001 From: picklesrus Date: Tue, 27 Aug 2019 16:38:38 -0400 Subject: [PATCH 8/9] space fixes --- src/components/join-flow/email-step.jsx | 2 +- src/components/join-flow/join-flow.jsx | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx index c550e3de6..77f5d7e54 100644 --- a/src/components/join-flow/email-step.jsx +++ b/src/components/join-flow/email-step.jsx @@ -107,7 +107,7 @@ class EmailStep extends React.Component { this.grecaptcha.execute(this.widgetId); } captchaSolved (token) { - // Now thatcaptcha is done, we can tell Formik we're submitting. + // Now that captcha is done, we can tell Formik we're submitting. this.formikBag.setSubmitting(true); this.formData['g-recaptcha-response'] = token; this.props.onNextStep(this.formData); diff --git a/src/components/join-flow/join-flow.jsx b/src/components/join-flow/join-flow.jsx index d6867a7da..f743dc088 100644 --- a/src/components/join-flow/join-flow.jsx +++ b/src/components/join-flow/join-flow.jsx @@ -29,7 +29,6 @@ class JoinFlow extends React.Component { step: 0 }; } - handleAdvanceStep (formData) { formData = formData || {}; this.setState({ From 3c716af744eb0a18ea2c0fbfa619b62ada50e997 Mon Sep 17 00:00:00 2001 From: picklesrus Date: Thu, 29 Aug 2019 11:33:50 -0400 Subject: [PATCH 9/9] Fix merge conflicts. --- src/components/join-flow/email-step.jsx | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/src/components/join-flow/email-step.jsx b/src/components/join-flow/email-step.jsx index 77f5d7e54..88a24c97a 100644 --- a/src/components/join-flow/email-step.jsx +++ b/src/components/join-flow/email-step.jsx @@ -32,21 +32,9 @@ class EmailStep extends React.Component { } componentDidMount () { -<<<<<<< HEAD -<<<<<<< HEAD // automatically start with focus on username field if (this.emailInput) this.emailInput.focus(); -======= - // 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.onCaptchaError; - script.src = `https://www.recaptcha.net/recaptcha/api.js?onload=grecaptchaOnLoad&render=explicit&hl=${window._locale}`; - document.body.appendChild(script); -======= + // If grecaptcha doesn't exist on window, we havent loaded the captcha js yet. Load it. if (!window.grecaptcha) { // ReCaptcha calls a callback when the grecatpcha object is usable. That callback @@ -59,17 +47,18 @@ class EmailStep extends React.Component { script.src = `https://www.recaptcha.net/recaptcha/api.js?onload=grecaptchaOnLoad&render=explicit&hl=${window._locale}`; document.body.appendChild(script); } ->>>>>>> Only load the captcha js if it hasn't been loaded yet. } componentWillUnmount () { window.grecaptchaOnLoad = null; } + handleSetEmailRef (emailInputRef) { + this.emailInput = emailInputRef; + } onCaptchaError () { // TODO send user to error step once we have one. } onCaptchaLoad () { this.setState({captchaIsLoading: false}); ->>>>>>> Make it so Create Account button is not clickable until captcha js loads. this.grecaptcha = window.grecaptcha; if (!this.grecaptcha) { // According to the reCaptcha documentation, this callback shouldn't get @@ -84,9 +73,6 @@ class EmailStep extends React.Component { }, true); } - handleSetEmailRef (emailInputRef) { - this.emailInput = emailInputRef; - } validateEmail (email) { if (!email) return this.props.intl.formatMessage({id: 'general.required'}); const isValidLocally = emailValidator.validate(email); @@ -107,7 +93,7 @@ class EmailStep extends React.Component { this.grecaptcha.execute(this.widgetId); } captchaSolved (token) { - // Now that captcha is done, we can tell Formik we're submitting. + // Now thatcaptcha is done, we can tell Formik we're submitting. this.formikBag.setSubmitting(true); this.formData['g-recaptcha-response'] = token; this.props.onNextStep(this.formData);