diff --git a/client/coral-framework/hocs/index.js b/client/coral-framework/hocs/index.js
index cfc41676b..c8b8f6bb6 100644
--- a/client/coral-framework/hocs/index.js
+++ b/client/coral-framework/hocs/index.js
@@ -7,4 +7,8 @@ export { default as excludeIf } from './excludeIf';
export { default as connect } from './connect';
export { default as withMergedSettings } from './withMergedSettings';
export { default as withSignIn } from './withSignIn';
+export { default as withSignUp } from './withSignUp';
export { default as withForgotPassword } from './withForgotPassword';
+export {
+ default as withResendEmailConfirmation,
+} from './withResendEmailConfirmation';
diff --git a/client/coral-framework/hocs/withForgotPassword.js b/client/coral-framework/hocs/withForgotPassword.js
index c8b42b8c5..fb8eb434c 100644
--- a/client/coral-framework/hocs/withForgotPassword.js
+++ b/client/coral-framework/hocs/withForgotPassword.js
@@ -11,6 +11,7 @@ export default hoistStatics(WrappedComponent => {
static contextTypes = {
store: PropTypes.object,
rest: PropTypes.func,
+ pym: PropTypes.object,
};
state = {
@@ -19,9 +20,11 @@ export default hoistStatics(WrappedComponent => {
success: false,
};
- forgotPassword = email => {
+ forgotPassword = (email, redirectUri) => {
+ if (!redirectUri) {
+ redirectUri = this.context.pym.parentUrl || location.href;
+ }
const { rest } = this.context;
- const redirectUri = location.href;
this.setState({ loading: true, error: null, success: false });
rest('/account/password/reset', {
diff --git a/client/coral-framework/hocs/withResendEmailConfirmation.js b/client/coral-framework/hocs/withResendEmailConfirmation.js
new file mode 100644
index 000000000..6b5f8375d
--- /dev/null
+++ b/client/coral-framework/hocs/withResendEmailConfirmation.js
@@ -0,0 +1,65 @@
+import React from 'react';
+import hoistStatics from 'recompose/hoistStatics';
+import PropTypes from 'prop-types';
+import { translateError } from '../utils';
+
+/**
+ * WithResendEmailConfirmaton provides properties `forgotPasssword`, `loading`, `errorMessage`, `success`.
+ */
+export default hoistStatics(WrappedComponent => {
+ class WithResendEmailConfirmaton extends React.Component {
+ static contextTypes = {
+ store: PropTypes.object,
+ rest: PropTypes.func,
+ pym: PropTypes.object,
+ };
+
+ state = {
+ error: null,
+ loading: false,
+ success: false,
+ };
+
+ resendEmailConfirmation = (email, redirectUri) => {
+ if (!redirectUri) {
+ redirectUri = this.context.pym.parentUrl || location.href;
+ }
+ const { rest } = this.context;
+ this.setState({ loading: true, error: null, success: false });
+
+ rest('/users/resend-verify', {
+ method: 'POST',
+ body: { email },
+ headers: { 'X-Pym-Url': redirectUri },
+ })
+ .then(() => {
+ this.setState({ loading: false, error: null, success: true });
+ })
+ .catch(error => {
+ console.error(error);
+ this.setState({ loading: false, error });
+ });
+ };
+
+ getErrorMessage() {
+ if (!this.state.error) {
+ return '';
+ }
+ return translateError(this.state.error);
+ }
+
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ return WithResendEmailConfirmaton;
+});
diff --git a/client/coral-framework/hocs/withSignIn.js b/client/coral-framework/hocs/withSignIn.js
index fa70138f9..53c3ab6d1 100644
--- a/client/coral-framework/hocs/withSignIn.js
+++ b/client/coral-framework/hocs/withSignIn.js
@@ -6,7 +6,13 @@ import { translateError } from '../utils';
import { t } from '../services/i18n';
/**
- * WithSignIn provides properties `signIn`, `loading`, `errorMessage`, `requireRecaptcha`, 'success'.
+ * WithSignIn provides properties
+ * `signIn`
+ * `loading`
+ * `errorMessage`
+ * `requireRecaptcha`
+ * `requireEmailConfirmation`
+ * 'success'
*/
export default hoistStatics(WrappedComponent => {
class WithSignIn extends React.Component {
@@ -20,6 +26,7 @@ export default hoistStatics(WrappedComponent => {
loading: false,
success: false,
requireRecaptcha: false,
+ requireEmailConfirmation: false,
};
signIn = (email, password, recaptchaResponse) => {
@@ -51,6 +58,8 @@ export default hoistStatics(WrappedComponent => {
if (error.translation_key === 'LOGIN_MAXIMUM_EXCEEDED') {
changeSet.requireRecaptcha = !!this.context.store.getState().config
.static.TALK_RECAPTCHA_PUBLIC;
+ } else if (error.translation_key === 'EMAIL_NOT_VERIFIED') {
+ changeSet.requireEmailConfirmation = true;
}
this.setState(changeSet);
});
@@ -73,6 +82,7 @@ export default hoistStatics(WrappedComponent => {
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
requireRecaptcha={this.state.requireRecaptcha}
+ requireEmailConfirmation={this.state.requireEmailConfirmation}
success={this.state.success}
/>
);
diff --git a/client/coral-framework/hocs/withSignUp.js b/client/coral-framework/hocs/withSignUp.js
new file mode 100644
index 000000000..8274b3b8a
--- /dev/null
+++ b/client/coral-framework/hocs/withSignUp.js
@@ -0,0 +1,118 @@
+import React from 'react';
+import hoistStatics from 'recompose/hoistStatics';
+import { compose, gql } from 'react-apollo';
+import PropTypes from 'prop-types';
+import { translateError } from '../utils';
+import validate from '../helpers/validate';
+import errorMsg from 'coral-framework/helpers/error';
+import t from '../services/i18n';
+import withQuery from './withQuery';
+import get from 'lodash/get';
+
+const requiredFields = ['username', 'email', 'password'];
+const allFields = requiredFields;
+
+const QUERY = gql`
+ query TalkFramework_WithSignUpQuery {
+ settings {
+ requireEmailConfirmation
+ }
+ }
+`;
+
+export const withSettingsQuery = withQuery(QUERY);
+
+/**
+ * withSignUp provides properties `signUp`, `loading`, `errorMessage`, `requireEmailVerification`, 'success', 'validate'.
+ */
+const withSignUp = hoistStatics(WrappedComponent => {
+ class WithSignUp extends React.Component {
+ static contextTypes = {
+ store: PropTypes.object,
+ rest: PropTypes.func,
+ pym: PropTypes.object,
+ };
+
+ static propTypes = {
+ root: PropTypes.object.isRequired,
+ };
+
+ state = {
+ error: null,
+ loading: false,
+ success: false,
+ };
+
+ validate = (field, value) => {
+ if (!allFields.includes(field)) {
+ return '';
+ }
+
+ if (requiredFields.includes(field) && !value) {
+ return t('sign_in.required_field');
+ }
+
+ if (field in validate) {
+ return validate[field](value) ? '' : errorMsg[field];
+ }
+
+ return '';
+ };
+
+ signUp = ({ username, email, password }, redirectUri) => {
+ if (!redirectUri) {
+ redirectUri = this.context.pym.parentUrl || location.href;
+ }
+
+ const { rest } = this.context;
+ const params = {
+ method: 'POST',
+ body: {
+ username,
+ email,
+ password,
+ },
+ headers: { 'X-Pym-Url': redirectUri },
+ };
+
+ rest('/users', params)
+ .then(() => {
+ this.setState({ success: true, loading: false, error: null });
+ })
+ .catch(error => {
+ if (!error.status || error.status !== 401) {
+ console.error(error);
+ }
+ const changeSet = { success: false, loading: false, error };
+ this.setState(changeSet);
+ });
+ };
+
+ getErrorMessage() {
+ if (!this.state.error) {
+ return '';
+ }
+ return translateError(this.state.error);
+ }
+
+ render() {
+ return (
+
+ );
+ }
+ }
+
+ return WithSignUp;
+});
+
+export default compose(withSettingsQuery, withSignUp);
diff --git a/plugins/talk-plugin-auth/client/login/components/ForgotPassword.js b/plugins/talk-plugin-auth/client/login/components/ForgotPassword.js
index 84daa04b7..12a309c28 100644
--- a/plugins/talk-plugin-auth/client/login/components/ForgotPassword.js
+++ b/plugins/talk-plugin-auth/client/login/components/ForgotPassword.js
@@ -5,8 +5,6 @@ import { Button, TextField } from 'plugin-api/beta/client/components/ui';
import t from 'coral-framework/services/i18n';
class ForgotPassword extends React.Component {
- state = { value: '' };
-
handleSignUpLink = e => {
e.preventDefault();
this.props.onSignUpLink();
diff --git a/plugins/talk-plugin-auth/client/login/components/Main.js b/plugins/talk-plugin-auth/client/login/components/Main.js
index 1aed0f229..992d7a2fd 100644
--- a/plugins/talk-plugin-auth/client/login/components/Main.js
+++ b/plugins/talk-plugin-auth/client/login/components/Main.js
@@ -3,7 +3,9 @@ import { Dialog } from 'plugin-api/beta/client/components/ui';
import styles from './Main.css';
import PropTypes from 'prop-types';
import SignIn from '../containers/SignIn';
+import SignUp from '../containers/SignUp';
import ForgotPassword from '../containers/ForgotPassword';
+import ResendEmailConfirmation from '../containers/ResendEmailConfirmation';
import * as views from '../enums/views';
const Main = ({ view, onResetView }) => (
@@ -14,7 +16,9 @@ const Main = ({ view, onResetView }) => (
)}
{view === views.SIGN_IN && }
+ {view === views.SIGN_UP && }
{view === views.FORGOT_PASSWORD && }
+ {view === views.RESEND_EMAIL_CONFIRMATION && }
);
diff --git a/plugins/talk-plugin-auth/client/login/components/ResendEmailConfirmation.css b/plugins/talk-plugin-auth/client/login/components/ResendEmailConfirmation.css
new file mode 100644
index 000000000..dc59a9ff1
--- /dev/null
+++ b/plugins/talk-plugin-auth/client/login/components/ResendEmailConfirmation.css
@@ -0,0 +1,15 @@
+.header {
+ margin-bottom: 20px;
+ text-align: center;
+ font-size: 1.2em;
+}
+
+.notVerified {
+ padding: 10px;
+ margin-bottom: 20px;
+ border-radius: 2px;
+ border: solid 1px #dddd00;
+ background: #FFFF9C;
+ color: #777700;
+}
+
diff --git a/plugins/talk-plugin-auth/client/login/components/ResendEmailConfirmation.js b/plugins/talk-plugin-auth/client/login/components/ResendEmailConfirmation.js
new file mode 100644
index 000000000..082492128
--- /dev/null
+++ b/plugins/talk-plugin-auth/client/login/components/ResendEmailConfirmation.js
@@ -0,0 +1,56 @@
+import React from 'react';
+import {
+ Button,
+ Spinner,
+ Success,
+ Alert,
+} from 'plugin-api/beta/client/components/ui';
+import PropTypes from 'prop-types';
+import styles from './ResendEmailConfirmation.css';
+import t from 'coral-framework/services/i18n';
+
+class ResendVerification extends React.Component {
+ handleSubmit = e => {
+ e.preventDefault();
+ this.props.onSubmit();
+ };
+
+ render() {
+ const { email, errorMessage, loading, success } = this.props;
+ return (
+
+
{t('sign_in.email_verify_cta')}
+
+ {errorMessage &&
{errorMessage}}
+
+ {t('error.email_not_verified', email)}
+
+
+ {!loading &&
+ !success && (
+
+ )}
+ {loading && }
+ {success && }
+
+
+ );
+ }
+}
+
+ResendVerification.propTypes = {
+ success: PropTypes.bool.isRequired,
+ loading: PropTypes.bool.isRequired,
+ email: PropTypes.string.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ errorMessage: PropTypes.string.isRequired,
+};
+
+export default ResendVerification;
diff --git a/plugins/talk-plugin-auth/client/login/components/SignUp.css b/plugins/talk-plugin-auth/client/login/components/SignUp.css
new file mode 100644
index 000000000..057713757
--- /dev/null
+++ b/plugins/talk-plugin-auth/client/login/components/SignUp.css
@@ -0,0 +1,49 @@
+.header {
+ margin-bottom: 20px;
+}
+
+.header h1, .separator h1{
+ text-align: center;
+ font-size: 1.2em;
+}
+
+.socialConnections {
+ margin-bottom: 20px;
+}
+
+.separator h1{
+ text-align: center;
+ font-size: 1.2em;
+}
+
+.hint {
+ color: grey;
+ font-weight: 600;
+ padding: 3px 0 16px;
+}
+
+.action {
+ margin-top: 0px;
+}
+
+.signInButton {
+ margin-top: 10px;
+ background-color: #2a2a2a;
+}
+
+.footer {
+ margin: 20px auto 10px;
+ text-align: center;
+}
+
+.footer span {
+ display: block;
+ margin-bottom: 5px;
+}
+
+.footer a {
+ color: #2c69b6;
+ cursor: pointer;
+ margin: 0 5px;
+}
+
diff --git a/plugins/talk-plugin-auth/client/login/components/SignUp.js b/plugins/talk-plugin-auth/client/login/components/SignUp.js
new file mode 100644
index 000000000..014d0db89
--- /dev/null
+++ b/plugins/talk-plugin-auth/client/login/components/SignUp.js
@@ -0,0 +1,168 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+ Button,
+ TextField,
+ Spinner,
+ Success,
+ Alert,
+} from 'plugin-api/beta/client/components/ui';
+import t from 'coral-framework/services/i18n';
+import styles from './SignUp.css';
+
+class SignUp extends React.Component {
+ handleSignInLink = e => {
+ e.preventDefault();
+ this.props.onSignInLink();
+ };
+
+ handleUsernameChange = e => this.props.onUsernameChange(e.target.value);
+ handleEmailChange = e => this.props.onEmailChange(e.target.value);
+ handlePasswordChange = e => this.props.onPasswordChange(e.target.value);
+ handlePasswordRepeatChange = e =>
+ this.props.onPasswordRepeatChange(e.target.value);
+
+ handleSubmit = e => {
+ e.preventDefault();
+ this.props.onSubmit();
+ };
+
+ render() {
+ const {
+ username,
+ email,
+ password,
+ passwordRepeat,
+ usernameError,
+ emailError,
+ passwordError,
+ passwordRepeatError,
+ loading,
+ errorMessage,
+ requireEmailConfirmation,
+ success,
+ } = this.props;
+
+ return (
+
+
+
{t('sign_in.sign_up')}
+
+
+ {errorMessage &&
{errorMessage}}
+ {!success && (
+
+
Social
+
+
{t('sign_in.or')}
+
+
+
+ )}
+ {success && (
+
+
+ {requireEmailConfirmation && (
+
+ {t('sign_in.verify_email')}
+
+
+ {t('sign_in.verify_email2')}
+
+ )}
+
+ )}
+
+
+ );
+ }
+}
+
+SignUp.propTypes = {
+ loading: PropTypes.bool.isRequired,
+ username: PropTypes.string.isRequired,
+ usernameError: PropTypes.string.isRequired,
+ email: PropTypes.string.isRequired,
+ emailError: PropTypes.string.isRequired,
+ password: PropTypes.string.isRequired,
+ passwordError: PropTypes.string.isRequired,
+ passwordRepeat: PropTypes.string.isRequired,
+ passwordRepeatError: PropTypes.string.isRequired,
+ onUsernameChange: PropTypes.func.isRequired,
+ onEmailChange: PropTypes.func.isRequired,
+ onPasswordChange: PropTypes.func.isRequired,
+ onPasswordRepeatChange: PropTypes.func.isRequired,
+ onSignInLink: PropTypes.func.isRequired,
+ onSubmit: PropTypes.func.isRequired,
+ errorMessage: PropTypes.string.isRequired,
+ requireEmailConfirmation: PropTypes.bool.isRequired,
+ success: PropTypes.bool.isRequired,
+};
+
+export default SignUp;
diff --git a/plugins/talk-plugin-auth/client/login/containers/Main.js b/plugins/talk-plugin-auth/client/login/containers/Main.js
index 3391fe1f4..b75030e0b 100644
--- a/plugins/talk-plugin-auth/client/login/containers/Main.js
+++ b/plugins/talk-plugin-auth/client/login/containers/Main.js
@@ -11,6 +11,22 @@ class MainContainer extends React.Component {
this.props.setView(views.SIGN_IN);
};
+ resizeHeight() {
+ setTimeout(() => {
+ const height = document.getElementById('signInDialog').offsetHeight + 100;
+ window.resizeTo(500, height);
+ }, 20);
+ }
+
+ componentDidMount() {
+ this.resizeHeight();
+ }
+
+ componentDidUpdate(prevProps) {
+ if (prevProps.view !== this.props.view) {
+ this.resizeHeight();
+ }
+ }
render() {
return ;
}
diff --git a/plugins/talk-plugin-auth/client/login/containers/ResendEmailConfirmation.js b/plugins/talk-plugin-auth/client/login/containers/ResendEmailConfirmation.js
new file mode 100644
index 000000000..3b4f4b564
--- /dev/null
+++ b/plugins/talk-plugin-auth/client/login/containers/ResendEmailConfirmation.js
@@ -0,0 +1,62 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withResendEmailConfirmation } from 'coral-framework/hocs';
+import { compose } from 'recompose';
+import ResendEmailConfirmaton from '../components/ResendEmailConfirmation';
+import { connect } from 'plugin-api/beta/client/hocs';
+import { bindActionCreators } from 'redux';
+import * as views from '../enums/views';
+import { setView } from '../actions';
+
+class ResendEmailConfirmatonContainer extends Component {
+ handleSubmit = () => {
+ this.props.resendEmailConfirmation(this.props.email);
+ };
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.success) {
+ setTimeout(() => {
+ // allow success UI to be shown for a second, and then close the modal
+ this.props.setView(views.SIGN_IN);
+ }, 2500);
+ }
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+ResendEmailConfirmatonContainer.propTypes = {
+ success: PropTypes.bool.isRequired,
+ loading: PropTypes.bool.isRequired,
+ resendEmailConfirmation: PropTypes.func.isRequired,
+ errorMessage: PropTypes.string.isRequired,
+ setView: PropTypes.func.isRequired,
+ email: PropTypes.string.isRequired,
+};
+
+const mapStateToProps = ({ talkPluginAuth: state }) => ({
+ email: state.email,
+});
+
+const mapDispatchToProps = dispatch =>
+ bindActionCreators(
+ {
+ setView,
+ },
+ dispatch
+ );
+
+export default compose(
+ connect(mapStateToProps, mapDispatchToProps),
+ withResendEmailConfirmation
+)(ResendEmailConfirmatonContainer);
diff --git a/plugins/talk-plugin-auth/client/login/containers/SignIn.js b/plugins/talk-plugin-auth/client/login/containers/SignIn.js
index 7247a4ffe..3b84999fe 100644
--- a/plugins/talk-plugin-auth/client/login/containers/SignIn.js
+++ b/plugins/talk-plugin-auth/client/login/containers/SignIn.js
@@ -34,7 +34,9 @@ class SignInContainer extends Component {
};
componentWillReceiveProps(nextProps) {
- if (nextProps.success) {
+ if (nextProps.requireEmailConfirmation) {
+ this.props.setView(views.RESEND_EMAIL_CONFIRMATION);
+ } else if (nextProps.success) {
window.close();
}
}
@@ -47,8 +49,8 @@ class SignInContainer extends Component {
onPasswordChange={this.props.setPassword}
onForgotPasswordLink={this.handleForgotPasswordLink}
onSignUpLink={this.handleSignUpLink}
- email={this.state.email}
- password={this.state.password}
+ email={this.props.email}
+ password={this.props.password}
errorMessage={this.props.errorMessage}
onRecaptchaVerify={this.handleRecaptchaVerify}
requireRecaptcha={this.props.requireRecaptcha}
@@ -62,6 +64,7 @@ SignInContainer.propTypes = {
signIn: PropTypes.func.isRequired,
errorMessage: PropTypes.string.isRequired,
requireRecaptcha: PropTypes.bool.isRequired,
+ requireEmailConfirmation: PropTypes.bool.isRequired,
loading: PropTypes.bool.isRequired,
success: PropTypes.bool.isRequired,
setView: PropTypes.func.isRequired,
diff --git a/plugins/talk-plugin-auth/client/login/containers/SignUp.js b/plugins/talk-plugin-auth/client/login/containers/SignUp.js
new file mode 100644
index 000000000..5eb5535d0
--- /dev/null
+++ b/plugins/talk-plugin-auth/client/login/containers/SignUp.js
@@ -0,0 +1,126 @@
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withSignUp } from 'coral-framework/hocs';
+import { compose } from 'recompose';
+import SignUp from '../components/SignUp';
+import { connect } from 'plugin-api/beta/client/hocs';
+import { bindActionCreators } from 'redux';
+import * as views from '../enums/views';
+import { setView, setEmail, setPassword } from '../actions';
+import t from 'coral-framework/services/i18n';
+
+class SignUpContainer extends Component {
+ state = {
+ username: '',
+ passwordRepeat: '',
+ usernameError: '',
+ emailError: '',
+ passwordError: '',
+ passwordRepeatError: '',
+ };
+
+ validate = data => {
+ let valid = true;
+ const changes = {};
+ Object.keys(data).forEach(name => {
+ const error = this.props.validate(name, data[name]);
+ if (error) {
+ valid = false;
+ }
+ changes[`${name}Error`] = error;
+ });
+
+ if (data.password !== data.passwordRepeat) {
+ changes['passwordRepeatError'] = t('sign_in.passwords_dont_match');
+ valid = false;
+ }
+
+ this.setState(changes);
+ return valid;
+ };
+
+ handleSubmit = () => {
+ const data = {
+ username: this.state.username,
+ email: this.props.email,
+ password: this.props.password,
+ passwordRepeat: this.state.passwordRepeat,
+ };
+
+ if (this.validate(data)) {
+ this.props.signUp(data);
+ }
+ };
+
+ setUsername = username => this.setState({ username });
+ setPasswordRepeat = passwordRepeat => this.setState({ passwordRepeat });
+
+ handleForgotPasswordLink = () => {
+ this.props.setView(views.FORGOT_PASSWORD);
+ };
+
+ handleSignInLink = () => {
+ this.props.setView(views.SIGN_IN);
+ };
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+SignUpContainer.propTypes = {
+ setView: PropTypes.func.isRequired,
+ email: PropTypes.string.isRequired,
+ password: PropTypes.string.isRequired,
+ setEmail: PropTypes.func.isRequired,
+ setPassword: PropTypes.func.isRequired,
+ signUp: PropTypes.func.isRequired,
+ loading: PropTypes.bool.isRequired,
+ errorMessage: PropTypes.string.isRequired,
+ requireEmailConfirmation: PropTypes.bool.isRequired,
+ success: PropTypes.bool.isRequired,
+ validate: PropTypes.func.isRequired,
+};
+
+const mapStateToProps = ({ talkPluginAuth: state }) => ({
+ email: state.email,
+ password: state.password,
+});
+
+const mapDispatchToProps = dispatch =>
+ bindActionCreators(
+ {
+ setView,
+ setEmail,
+ setPassword,
+ },
+ dispatch
+ );
+
+export default compose(
+ connect(mapStateToProps, mapDispatchToProps),
+ withSignUp
+)(SignUpContainer);
diff --git a/plugins/talk-plugin-auth/client/login/enums/views.js b/plugins/talk-plugin-auth/client/login/enums/views.js
index 4f92f4439..5c6732755 100644
--- a/plugins/talk-plugin-auth/client/login/enums/views.js
+++ b/plugins/talk-plugin-auth/client/login/enums/views.js
@@ -1,4 +1,4 @@
export const SIGN_IN = 'SIGN_IN';
export const FORGOT_PASSWORD = 'FORGOT_PASSWORD';
export const SIGN_UP = 'SIGN_UP';
-export const VERIFY_EMAIL = 'VERIFY_EMAIL';
+export const RESEND_EMAIL_CONFIRMATION = 'RESEND_EMAIL_CONFIRMATION';