diff --git a/client/coral-admin/src/containers/Configure/CommentSettings.js b/client/coral-admin/src/containers/Configure/CommentSettings.js index 0ccb5f820..1051bb0ba 100644 --- a/client/coral-admin/src/containers/Configure/CommentSettings.js +++ b/client/coral-admin/src/containers/Configure/CommentSettings.js @@ -32,6 +32,10 @@ const updateModeration = (updateSettings, mod) => () => { updateSettings({moderation}); }; +const updateEmailConfirmation = (updateSettings, verify) => () => { + updateSettings({requireEmailConfirmation: !verify}); +}; + const updateInfoBoxEnable = (updateSettings, infoBox) => () => { const infoBoxEnable = !infoBox; updateSettings({infoBoxEnable}); @@ -67,106 +71,123 @@ const CommentSettings = ({fetchingSettings, title, updateSettings, settingsError return Loading settings...; } + // just putting this here for shorthand below + const on = styles.enabledSetting; + const off = styles.disabledSetting; + return (

{title}

- -
- -
-
+ +
+ +
+
{lang.t('configure.enable-pre-moderation')}

{lang.t('configure.enable-pre-moderation-text')}

-
- -
- -
-
-
{lang.t('configure.comment-count-header')}
-

- {lang.t('configure.comment-count-text-pre')} - - {lang.t('configure.comment-count-text-post')} - { - errors.charCount && - -
- - {lang.t('configure.comment-count-error')} -
- } -

-
-
- -
- -
-
- {lang.t('configure.include-comment-stream')} -

- {lang.t('configure.include-comment-stream-desc')} -

-
-
- -
+ + +
+ +
+
+
{lang.t('configure.require-email-verification')}
+

+ {lang.t('configure.require-email-verification-text')} +

+
+
+ +
+ +
+
+
{lang.t('configure.comment-count-header')}
+

+ {lang.t('configure.comment-count-text-pre')} + + {lang.t('configure.comment-count-text-post')} + { + errors.charCount && + +
+ + {lang.t('configure.comment-count-error')} +
+ } +

+
+
+ +
+ +
+
+ {lang.t('configure.include-comment-stream')} +

+ {lang.t('configure.include-comment-stream-desc')} +

+
+
+
- - -
- {lang.t('configure.closed-comments-desc')} -
- -
+
+
+ +
+ {lang.t('configure.closed-comments-desc')} +
+
- - -
- {lang.t('configure.close-after')} -
- -
- - - - - -
+
+
+ +
+ {lang.t('configure.close-after')} +
+ +
+ + + + +
- +
+
); }; diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index a89b53523..65411066a 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -50,6 +50,8 @@ "configure": { "enable-pre-moderation": "Enable pre-moderation", "enable-pre-moderation-text": "Moderators must approve any comment before it is published.", + "require-email-verification": "Require Email Confirmation", + "require-email-verification-text": "New Users must verify their email before commenting", "include-comment-stream": "Include Comment Stream Description for Readers.", "include-comment-stream-desc": "Write a message to be added to the top of your comment stream. Pose a topic, include community guidelines, etc.", "include-text": "Include your text here.", @@ -155,6 +157,8 @@ "configure": { "enable-pre-moderation": "Habilitar pre-moderación", "enable-pre-moderation-text": "Los moderadores deben aprobar cada comentario antes de que sea publicado.", + "require-email-verification": "Necesita confirmación de correo", + "require-email-verification-text": "Nuevos usuarios deben verificar sus correos antes de comentar", "include-comment-stream": "Incluir la Descripción a un Hilo de Comentario para los y las Lectoras.", "include-comment-stream-desc": "Escribir un mensaje que será agregado a la parte de arriba del tu hilo de comentarios. Por ejemplo, un tema, guias de comunidad, etc.", "include-text": "Incluir tu texto aqui.", diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 2a7057cca..dac503b72 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -5,7 +5,7 @@ import isEqual from 'lodash/isEqual'; import {TabBar, Tab, TabContent, Spinner} from '../../coral-ui'; -const {logout, showSignInDialog} = authActions; +const {logout, showSignInDialog, requestConfirmEmail} = authActions; const {addNotification, clearNotification} = notificationActions; const {fetchAssetSuccess} = assetActions; @@ -191,6 +191,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = dispatch => ({ + requestConfirmEmail: () => dispatch(requestConfirmEmail()), loadAsset: (asset) => dispatch(fetchAssetSuccess(asset)), addNotification: (type, text) => dispatch(addNotification(type, text)), clearNotification: () => dispatch(clearNotification()), diff --git a/client/coral-framework/actions/auth.js b/client/coral-framework/actions/auth.js index 15ee4968b..592fe2be1 100644 --- a/client/coral-framework/actions/auth.js +++ b/client/coral-framework/actions/auth.js @@ -21,6 +21,7 @@ export const cleanState = () => ({type: actions.CLEAN_STATE}); const signInRequest = () => ({type: actions.FETCH_SIGNIN_REQUEST}); const signInSuccess = (user, isAdmin) => ({type: actions.FETCH_SIGNIN_SUCCESS, user, isAdmin}); const signInFailure = error => ({type: actions.FETCH_SIGNIN_FAILURE, error}); +const emailConfirmError = () => ({type: actions.EMAIL_CONFIRM_ERROR}); export const fetchSignIn = (formData) => (dispatch) => { dispatch(signInRequest()); @@ -30,7 +31,18 @@ export const fetchSignIn = (formData) => (dispatch) => { dispatch(signInSuccess(user, isAdmin)); dispatch(hideSignInDialog()); }) - .catch(() => dispatch(signInFailure(lang.t('error.emailPasswordError')))); + .catch(error => { + if (error.metadata) { + + // the user might not have a valid email. prompt the user user re-request the confirmation email + dispatch(signInFailure(lang.t('error.emailNotVerified', error.metadata))); + dispatch(emailConfirmError()); + } else { + + // invalid credentials + dispatch(signInFailure(lang.t('error.emailPasswordError'))); + } + }); }; // Sign In - Facebook @@ -137,3 +149,21 @@ export const checkLogin = () => dispatch => { dispatch(checkLoginFailure(`${error.message}`)); }); }; + +const confirmEmailRequest = () => ({type: actions.CONFIRM_EMAIL_REQUEST}); +const confirmEmailSuccess = () => ({type: actions.CONFIRM_EMAIL_SUCCESS}); +const confirmEmailFailure = () => ({type: actions.CONFIRM_EMAIL_FAILURE}); + +export const requestConfirmEmail = email => dispatch => { + dispatch(confirmEmailRequest()); + return coralApi('/users/resend-confirm', {method: 'POST', body: {email}}) + .then(() => { + dispatch(confirmEmailSuccess()); + }) + .catch(err => { + console.log('failed to send email confirmation', err); + + // email might have already been confirmed + dispatch(confirmEmailFailure()); + }); +}; diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index 5742adf75..1dae348df 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -32,3 +32,8 @@ export const CHECK_LOGIN_SUCCESS = 'CHECK_LOGIN_SUCCESS'; export const CHECK_LOGIN_FAILURE = 'CHECK_LOGIN_FAILURE'; export const CHECK_CSRF_TOKEN = 'CHECK_CSRF_TOKEN'; + +export const EMAIL_CONFIRM_ERROR = 'EMAIL_CONFIRM_ERROR'; +export const CONFIRM_EMAIL_REQUEST = 'CONFIRM_EMAIL_REQUEST'; +export const CONFIRM_EMAIL_SUCCESS = 'CONFIRM_EMAIL_SUCCESS'; +export const CONFIRM_EMAIL_FAILURE = 'CONFIRM_EMAIL_FAILURE'; diff --git a/client/coral-framework/helpers/response.js b/client/coral-framework/helpers/response.js index e4e6e7c37..d95773c04 100644 --- a/client/coral-framework/helpers/response.js +++ b/client/coral-framework/helpers/response.js @@ -34,15 +34,21 @@ const buildOptions = (inputOptions = {}) => { }; const handleResp = res => { - if (res.status === 401) { - throw new Error('Not Authorized to make this request'); - } else if (res.status > 399) { + if (res.status > 399) { return res.json().then(err => { let message = err.message || res.status; + const error = new Error(); + + if (err.error && err.error.metadata && err.error.metadata.message) { + error.metadata = err.error.metadata.message; + } + if (err.error && err.error.translation_key) { message = err.error.translation_key; } - throw new Error(message); + + error.message = message; + throw error; }); } else if (res.status === 204) { return res.text(); diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 36f8e0764..6cb4e97cd 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -11,6 +11,9 @@ const initialState = Map({ error: '', passwordRequestSuccess: null, passwordRequestFailure: null, + emailConfirmationFailure: false, + emailConfirmationLoading: false, + emailConfirmationSuccess: false, successSignUp: false }); @@ -33,6 +36,9 @@ export default function auth (state = initialState, action) { error: '', passwordRequestFailure: null, passwordRequestSuccess: null, + emailConfirmationFailure: false, + emailConfirmationSuccess: false, + emailConfirmationLoading: false, successSignUp: false })); case actions.CHANGE_VIEW : @@ -101,6 +107,16 @@ export default function auth (state = initialState, action) { return state .set('passwordRequestFailure', 'There was an error sending your password reset email. Please try again soon!') .set('passwordRequestSuccess', null); + case actions.EMAIL_CONFIRM_ERROR: + return state + .set('emailConfirmationFailure', true) + .set('emailConfirmationLoading', false); + case actions.CONFIRM_EMAIL_REQUEST: + return state.set('emailConfirmationLoading', true); + case actions.CONFIRM_EMAIL_SUCCESS: + return state + .set('emailConfirmationSuccess', true) + .set('emailConfirmationLoading', false); default : return state; } diff --git a/client/coral-framework/translations.json b/client/coral-framework/translations.json index 5bf9b40a9..07b719f6b 100644 --- a/client/coral-framework/translations.json +++ b/client/coral-framework/translations.json @@ -5,6 +5,7 @@ "contentNotAvailable": "This content is not available", "suspendedAccountMsg": "Your account is currently suspended. This means that you cannot Like, Flag, or write comments. Please contact moderator@fakeurl.com for more information", "error": { + "emailNotVerified": "Email address {0} not verified.", "email": "Not a valid E-Mail", "password": "Password must be at least 8 characters", "displayName": "Display names can contain letters, numbers and _ only", @@ -27,6 +28,7 @@ "contentNotAvailable": "El contenido no se encuentra disponible", "suspendedAccountMsg": "Tu cuenta se encuentra suspendida. Esto significa que no puedes dar Like, Marcar o escribir commentarios. Por favor, contacta moderator@fakeurl for more information", "error": { + "emailNotVerified": "Dirección de correo electrónico {0} no verificada.", "email": "No es un email válido", "password": "La contraseña debe tener por lo menos 8 caracteres", "displayName": "Los nombres pueden contener letras, números y _", diff --git a/client/coral-sign-in/components/SignDialog.js b/client/coral-sign-in/components/SignDialog.js index 6243472b4..0645f110f 100644 --- a/client/coral-sign-in/components/SignDialog.js +++ b/client/coral-sign-in/components/SignDialog.js @@ -17,7 +17,12 @@ const SignDialog = ({open, view, handleClose, offset, ...props}) => ( }}> × {view === 'SIGNIN' && } - {view === 'SIGNUP' && } + { + view === 'SIGNUP' && + } {view === 'FORGOT' && } ); diff --git a/client/coral-sign-in/components/SignInContent.js b/client/coral-sign-in/components/SignInContent.js index de5e77a49..e27185b52 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -1,67 +1,100 @@ -import React from 'react'; -import Button from 'coral-ui/components/Button'; -import FormField from './FormField'; +import React, {PropTypes} from 'react'; import Alert from './Alert'; -import Spinner from 'coral-ui/components/Spinner'; +import {Button, FormField, Spinner, Success} from 'coral-ui'; import styles from './styles.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; const lang = new I18n(translations); -const SignInContent = ({handleChange, formData, ...props}) => ( -
-
-

- {lang.t('signIn.signIn')} -

-
-
- -
-
-

- {lang.t('signIn.or')} -

-
- { props.auth.error && {props.auth.error} } -
- - -
- { - !props.auth.isLoading ? - - : - - } +const SignInContent = ({ + handleChange, + handleChangeEmail, + emailToBeResent, + handleResendConfirmation, + emailConfirmationLoading, + emailConfirmationSuccess, + formData, + ...props +}) => { + + return ( +
+
+

+ {props.auth.emailConfirmationFailure ? lang.t('signIn.emailConfirmCTA') : lang.t('signIn.signIn')} +

+
+
+ +
+
+

+ {lang.t('signIn.or')} +

+
+ { props.auth.error && {props.auth.error} } + { + props.auth.emailConfirmationFailure + ? +

{lang.t('signIn.requestNewConfirmEmail')}

+ + + {emailConfirmationLoading && } + {emailConfirmationSuccess && } + + :
+ + +
+ { + !props.auth.isLoading ? + + : + + } +
+ + } + - - -
-); + ); +}; + +SignInContent.propTypes = { + emailConfirmationLoading: PropTypes.bool.isRequired, + emailConfirmationSuccess: PropTypes.bool.isRequired, + handleResendConfirmation: PropTypes.func.isRequired, + handleChangeEmail: PropTypes.func.isRequired, + emailToBeResent: PropTypes.string.isRequired +}; export default SignInContent; diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index 4144b1d2f..246d9d884 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -1,9 +1,6 @@ import React from 'react'; -import FormField from './FormField'; import Alert from './Alert'; -import Button from 'coral-ui/components/Button'; -import Spinner from 'coral-ui/components/Spinner'; -import Success from 'coral-ui/components/Success'; +import {Button, FormField, Spinner, Success} from 'coral-ui'; import styles from './styles.css'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; diff --git a/client/coral-sign-in/components/styles.css b/client/coral-sign-in/components/styles.css index f1a7bace3..08146f73f 100644 --- a/client/coral-sign-in/components/styles.css +++ b/client/coral-sign-in/components/styles.css @@ -14,28 +14,6 @@ font-size: 1.2em; } -.formField { - margin-top: 15px; -} - -.formField label { - font-size: 1.08em; - font-weight: bold; - margin-bottom: 5px; -} - -.formField input { - width: 100%; - display: block; - border: none; - outline: none; - border: 1px solid rgba(0,0,0,.12); - padding: 10px 6px; - box-sizing: border-box; - border-radius: 2px; - margin: 5px auto; -} - .footer { margin: 20px auto 10px; text-align: center; @@ -150,3 +128,15 @@ input.error{ background-color: 1px solid coral; padding: 10px; } + +.emailConfirmDialog { + margin-top: 15px; +} + +.confirmLabel { + display: block; +} + +.confirmSubmit { + +} diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 9e9ca1f72..2435cdcfd 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -16,6 +16,7 @@ import { hideSignInDialog, fetchSignInFacebook, fetchForgotPassword, + requestConfirmEmail, facebookCallback, invalidForm, validForm, @@ -30,6 +31,7 @@ class SignInContainer extends Component { password: '', confirmPassword: '' }, + emailToBeResent: '', errors: {}, showErrors: false }; @@ -38,9 +40,10 @@ class SignInContainer extends Component { super(props); this.state = this.initialState; this.handleChange = this.handleChange.bind(this); + this.handleChangeEmail = this.handleChangeEmail.bind(this); + this.handleResendConfirmation = this.handleResendConfirmation.bind(this); this.handleSignUp = this.handleSignUp.bind(this); this.handleSignIn = this.handleSignIn.bind(this); - this.handleClose = this.handleClose.bind(this); this.addError = this.addError.bind(this); } @@ -71,6 +74,23 @@ class SignInContainer extends Component { }); } + handleChangeEmail(e) { + const {value} = e.target; + this.setState({emailToBeResent: value}); + } + + handleResendConfirmation(e) { + e.preventDefault(); + this.props.requestConfirmEmail(this.state.emailToBeResent) + .then(() => { + setTimeout(() => { + + // allow success UI to be shown for a second, and then close the modal + this.props.handleClose(); + }, 2500); + }); + } + addError(name, error) { return this.setState(state => ({ errors: { @@ -124,12 +144,9 @@ class SignInContainer extends Component { this.props.fetchSignIn(this.state.formData); } - handleClose() { - this.props.hideSignInDialog(); - } - render() { const {auth, showSignInDialog, noButton, offset} = this.props; + const {emailConfirmationLoading, emailConfirmationSuccess} = auth; return (
{!noButton &&