More refactoring

This commit is contained in:
Chi Vinh Le
2018-02-13 00:54:15 +01:00
parent 4b6a3b5b9d
commit 142c5b32f1
16 changed files with 706 additions and 9 deletions
+4
View File
@@ -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';
@@ -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', {
@@ -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 (
<WrappedComponent
{...this.props}
resendEmailConfirmation={this.resendEmailConfirmation}
success={this.state.success}
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
/>
);
}
}
return WithResendEmailConfirmaton;
});
+11 -1
View File
@@ -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}
/>
);
+118
View File
@@ -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 (
<WrappedComponent
{...this.props}
signUp={this.signUp}
loading={this.state.loading}
errorMessage={this.getErrorMessage()}
requireEmailConfirmation={
!!get(this.props, 'root.settings.requireEmailConfirmation')
}
success={this.state.success}
validate={this.validate}
/>
);
}
}
return WithSignUp;
});
export default compose(withSettingsQuery, withSignUp);
@@ -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();
@@ -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 }) => (
</span>
)}
{view === views.SIGN_IN && <SignIn />}
{view === views.SIGN_UP && <SignUp />}
{view === views.FORGOT_PASSWORD && <ForgotPassword />}
{view === views.RESEND_EMAIL_CONFIRMATION && <ResendEmailConfirmation />}
</Dialog>
);
@@ -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;
}
@@ -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 (
<div className="talk-resend-verification">
<h1 className={styles.header}>{t('sign_in.email_verify_cta')}</h1>
{errorMessage && <Alert>{errorMessage}</Alert>}
<div className={styles.notVerified}>
{t('error.email_not_verified', email)}
</div>
<div>
{!loading &&
!success && (
<Button
id="resendConfirmEmail"
cStyle="black"
onClick={this.handleSubmit}
full
>
{t('sign_in.request_new_verify_email')}
</Button>
)}
{loading && <Spinner />}
{success && <Success />}
</div>
</div>
);
}
}
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;
@@ -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;
}
@@ -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 (
<div>
<div className={styles.header}>
<h1>{t('sign_in.sign_up')}</h1>
</div>
{errorMessage && <Alert>{errorMessage}</Alert>}
{!success && (
<div>
<div className={styles.socialConnections}>Social</div>
<div className={styles.separator}>
<h1>{t('sign_in.or')}</h1>
</div>
<form onSubmit={this.handleSubmit}>
<TextField
id="email"
type="email"
label={t('sign_in.email')}
value={email}
style={{ fontSize: 16 }}
showErrors={!!emailError}
errorMsg={emailError}
onChange={this.handleEmailChange}
/>
<TextField
id="username"
type="text"
label={t('sign_in.username')}
value={username}
style={{ fontSize: 16 }}
showErrors={!!usernameError}
errorMsg={usernameError}
onChange={this.handleUsernameChange}
/>
<TextField
id="password"
type="password"
label={t('sign_in.password')}
value={password}
style={{ fontSize: 16 }}
showErrors={!!passwordError}
errorMsg={passwordError}
onChange={this.handlePasswordChange}
minLength="8"
/>
{passwordError && (
<span className={styles.hint}>
{' '}
Password must be at least 8 characters.{' '}
</span>
)}
<TextField
id="confirmPassword"
type="password"
label={t('sign_in.confirm_password')}
value={passwordRepeat}
style={{ fontSize: 16 }}
showErrors={!!passwordRepeatError}
errorMsg={passwordRepeatError}
onChange={this.handlePasswordRepeatChange}
minLength="8"
/>
<div className={styles.action}>
<Button
type="submit"
cStyle="black"
id="coralSignUpButton"
className={styles.signInButton}
full
>
{t('sign_in.sign_up')}
</Button>
{loading && <Spinner />}
</div>
</form>
</div>
)}
{success && (
<div>
<Success />
{requireEmailConfirmation && (
<p>
{t('sign_in.verify_email')}
<br />
<br />
{t('sign_in.verify_email2')}
</p>
)}
</div>
)}
<div className={styles.footer}>
{t('sign_in.already_have_an_account')}{' '}
<a id="coralSignInViewTrigger" onClick={this.handleSignInLink}>
{t('sign_in.sign_in')}
</a>
</div>
</div>
);
}
}
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;
@@ -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 <Main onResetView={this.resetView} view={this.props.view} />;
}
@@ -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 (
<ResendEmailConfirmaton
onSubmit={this.handleSubmit}
email={this.props.email}
errorMessage={this.props.errorMessage}
success={this.props.success}
loading={this.props.loading}
/>
);
}
}
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);
@@ -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,
@@ -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 (
<SignUp
onSubmit={this.handleSubmit}
onUsernameChange={this.setUsername}
onEmailChange={this.props.setEmail}
onPasswordChange={this.props.setPassword}
onPasswordRepeatChange={this.setPasswordRepeat}
onForgotPasswordLink={this.handleForgotPasswordLink}
onSignInLink={this.handleSignInLink}
username={this.state.username}
email={this.props.email}
password={this.props.password}
passwordRepeat={this.state.passwordRepeat}
errorMessage={this.props.errorMessage}
onRecaptchaVerify={this.handleRecaptchaVerify}
requireEmailConfirmation={this.props.requireEmailConfirmation}
loading={this.props.loading}
success={this.props.success}
usernameError={this.state.usernameError}
emailError={this.state.emailError}
passwordError={this.state.passwordError}
passwordRepeatError={this.state.passwordRepeatError}
/>
);
}
}
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);
@@ -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';