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;
+}