diff --git a/src/components/auth/actions.js b/src/components/auth/actions.js index 6f4cb84..a1ad175 100644 --- a/src/components/auth/actions.js +++ b/src/components/auth/actions.js @@ -103,9 +103,9 @@ export function activate({key = ''}) { ); } -export function resendActivation({email = ''}) { +export function resendActivation({email = '', captcha}) { return wrapInLoader((dispatch) => - signup.resendActivation({email}) + signup.resendActivation({email, captcha}) .then((resp) => { dispatch(updateUser({ email diff --git a/src/components/auth/resendActivation/ResendActivationBody.jsx b/src/components/auth/resendActivation/ResendActivationBody.jsx index 029c94b..126a028 100644 --- a/src/components/auth/resendActivation/ResendActivationBody.jsx +++ b/src/components/auth/resendActivation/ResendActivationBody.jsx @@ -2,7 +2,7 @@ import React from 'react'; import { FormattedMessage as Message } from 'react-intl'; -import { Input } from 'components/ui/form'; +import { Input, Captcha } from 'components/ui/form'; import registerMessages from 'components/auth/register/Register.intl.json'; import BaseAuthBody from 'components/auth/BaseAuthBody'; @@ -25,16 +25,16 @@ export default class ResendActivation extends BaseAuthBody { -
- -
+ + + ); } diff --git a/src/components/ui/form/Captcha.jsx b/src/components/ui/form/Captcha.jsx new file mode 100644 index 0000000..c9bd0c9 --- /dev/null +++ b/src/components/ui/form/Captcha.jsx @@ -0,0 +1,38 @@ +import React, { PropTypes } from 'react'; + +import captcha from 'services/captcha'; +import { skins, SKIN_DARK } from 'components/ui'; + +import styles from './form.scss'; +import FormInputComponent from './FormInputComponent'; + +export default class Captcha extends FormInputComponent { + static displayName = 'Captcha'; + + static propTypes = { + skin: PropTypes.oneOf(skins) + }; + + static defaultProps = { + skin: SKIN_DARK + }; + + componentDidMount() { + captcha.render(this.el, { + skin: this.props.skin, + onSetCode: this.setCode + }); + } + + render() { + return ( +
+ ); + } + + getValue() { + return this.state && this.state.code; + } + + setCode = (code) => this.setState({code}); +} diff --git a/src/components/ui/form/form.scss b/src/components/ui/form/form.scss index 0cedf25..46e708e 100644 --- a/src/components/ui/form/form.scss +++ b/src/components/ui/form/form.scss @@ -295,6 +295,15 @@ } } +.captcha { + width: 302px; + height: 76px; + // minimum captcha width is 302px, which can not be changed + // using transform to scale down to 296px + transform-origin: 0; + transform: scaleX(0.98); +} + /** * Form validation */ diff --git a/src/components/ui/form/index.js b/src/components/ui/form/index.js index 5de4a7c..b73ffce 100644 --- a/src/components/ui/form/index.js +++ b/src/components/ui/form/index.js @@ -5,6 +5,7 @@ import Button from './Button'; import Form from './Form'; import FormModel from './FormModel'; import Dropdown from './Dropdown'; +import Captcha from './Captcha'; import FormError from './FormError'; export { @@ -15,5 +16,6 @@ export { Form, FormModel, Dropdown, + Captcha, FormError }; diff --git a/src/components/user/actions.js b/src/components/user/actions.js index b91f922..e02fb72 100644 --- a/src/components/user/actions.js +++ b/src/components/user/actions.js @@ -1,6 +1,7 @@ import { routeActions } from 'react-router-redux'; import request from 'services/request'; +import captcha from 'services/captcha'; import accounts from 'services/api/accounts'; import authentication from 'services/api/authentication'; import { setLocale } from 'components/i18n/actions'; @@ -27,6 +28,9 @@ export function changeLang(lang) { accounts.changeLang(lang); } + // TODO: probably should be moved from here, because it is side effect + captcha.setLang(lang); + dispatch({ type: CHANGE_LANG, payload: { diff --git a/src/components/user/factory.js b/src/components/user/factory.js index bcf9846..9449e7b 100644 --- a/src/components/user/factory.js +++ b/src/components/user/factory.js @@ -3,7 +3,7 @@ import { authenticate, changeLang } from 'components/user/actions'; /** * Initializes User state with the fresh data * - * @param {Object} store - redux store + * @param {object} store - redux store * * @return {Promise} a promise, that resolves in User state */ diff --git a/src/functions.js b/src/functions.js index a2aa7c7..e595903 100644 --- a/src/functions.js +++ b/src/functions.js @@ -18,3 +18,25 @@ export function omit(obj, keys) { return newObj; } + +/** + * Asynchronously loads script + * + * @param {string} src + * + * @return {Promise} + */ +export function loadScript(src) { + const script = document.createElement('script'); + + script.async = true; + script.defer = true; + script.src = src; + + return new Promise((resolve, reject) => { + script.onlaod = resolve; + script.onerror = reject; + + document.body.appendChild(script); + }); +} diff --git a/src/index.js b/src/index.js index a6da188..d925712 100644 --- a/src/index.js +++ b/src/index.js @@ -11,6 +11,9 @@ import { IntlProvider } from 'components/i18n'; import routesFactory from 'routes'; import storeFactory from 'storeFactory'; import bsodFactory from 'components/ui/bsod/factory'; +import captcha from 'services/captcha'; + +captcha.setApiKey('6LdUZiYTAAAAAEjDGi9kEu0MRKYHYWskPFNXSYOV'); // TODO const store = storeFactory(); diff --git a/src/services/api/signup.js b/src/services/api/signup.js index ce3069b..4c4c5dd 100644 --- a/src/services/api/signup.js +++ b/src/services/api/signup.js @@ -22,10 +22,10 @@ export default { ); }, - resendActivation({email = ''}) { + resendActivation({email = '', captcha}) { return request.post( '/api/signup/repeat-message', - {email} + {email, captcha} ); } }; diff --git a/src/services/captcha.js b/src/services/captcha.js new file mode 100644 index 0000000..61e9de6 --- /dev/null +++ b/src/services/captcha.js @@ -0,0 +1,59 @@ +import { loadScript } from 'functions'; + +let readyPromise; +let lang = 'en'; +let sitekey; + +export default { + /** + * @param {DOMNode|string} el - dom node or id of element where to render captcha + * @param {string} options.skin - skin color (dark|light) + * @param {function} options.onSetCode - the callback, that will be called with + * captcha verification code, after user successfully solves captcha + * + * @return {Promise} + */ + render(el, {skin: theme, onSetCode: callback}) { + if (!sitekey) { + throw new Error('Site key is required to render captcha'); + } + + return loadApi().then(() => + window.grecaptcha.render(el, { + sitekey, + theme, + callback + }) + ); + }, + + /** + * @param {stirng} newLang + * + * @see https://developers.google.com/recaptcha/docs/language + */ + setLang(newLang) { + lang = newLang; + }, + + /** + * @param {string} apiKey + * + * @see http://www.google.com/recaptcha/admin + */ + setApiKey(apiKey) { + sitekey = apiKey; + } +}; + +function loadApi() { + if (!readyPromise) { + readyPromise = new Promise((resolve) => { + window.onReCaptchaReady = resolve; + }); + + loadScript(`https://www.google.com/recaptcha/api.js?onload=onReCaptchaReady&render=explicit&hl=${lang}`); + } + + return readyPromise; +}