Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
S
scratch-www
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Analytics
Analytics
Repository
Value Stream
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Commits
Open sidebar
xpstem
scratch-www
Commits
7548253b
Unverified
Commit
7548253b
authored
Mar 27, 2020
by
picklesrus
Committed by
GitHub
Mar 27, 2020
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #3765 from picklesrus/captcha-component
Move reCaptcha code to a component
parents
54d56b9d
18740693
Changes
4
Show whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
154 additions
and
71 deletions
+154
-71
src/components/captcha/captcha.jsx
src/components/captcha/captcha.jsx
+76
-0
src/components/join-flow/email-step.jsx
src/components/join-flow/email-step.jsx
+10
-42
test/unit/components/captcha.test.jsx
test/unit/components/captcha.test.jsx
+57
-0
test/unit/components/email-step.test.jsx
test/unit/components/email-step.test.jsx
+11
-29
No files found.
src/components/captcha/captcha.jsx
0 → 100644
View file @
7548253b
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
;
src/components/join-flow/email-step.jsx
View file @
7548253b
...
...
@@ -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
'
,
'
c
aptchaSolved
'
,
'
on
CaptchaLoad
'
'
handleC
aptchaSolved
'
,
'
handle
CaptchaLoad
'
]);
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
;
}
on
CaptchaLoad
()
{
handle
CaptchaLoad
()
{
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
(
);
}
c
aptchaSolved
(
token
)
{
handleC
aptchaSolved
(
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
>
);
...
...
test/unit/components/captcha.test.jsx
0 → 100644
View file @
7548253b
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
);
});
});
test/unit/components/email-step.test.jsx
View file @
7548253b
...
...
@@ -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
().
c
aptchaSolved
(
captchaToken
);
emailStepWrapper
.
instance
().
handleC
aptchaSolved
(
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
'
);
});
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment