diff --git a/README.md b/README.md index 6c3f90997..f47fbd2f6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Talk [![CircleCI](https://circleci.com/gh/coralproject/talk.svg?style=svg)](https://circleci.com/gh/coralproject/talk) -A commenting platform from [The Coral Project](https://coralproject.net). +A commenting platform from [The Coral Project](https://coralproject.net). Talk enters a closed beta in March 2017, but you can download the code for our alpha here. [Read more about Talk here.](https://coralproject.net/products/talk.html) ## Contributing to Talk diff --git a/circle.yml b/circle.yml index f9ac9ea41..bae929df7 100644 --- a/circle.yml +++ b/circle.yml @@ -35,7 +35,7 @@ test: deployment: release: - tag: /[0-9]+(\.[0-9]+)*/ + tag: /v[0-9]+(\.[0-9]+)*/ commands: - bash ./scripts/deploy.sh diff --git a/client/coral-admin/src/containers/Configure/EmbedLink.js b/client/coral-admin/src/containers/Configure/EmbedLink.js index 105b531c5..413f421d7 100644 --- a/client/coral-admin/src/containers/Configure/EmbedLink.js +++ b/client/coral-admin/src/containers/Configure/EmbedLink.js @@ -25,8 +25,24 @@ class EmbedLink extends Component { } render () { - const embedText = `
`; - + const location = window.location; + const talkBaseUrl = [ + location.protocol, + '//', + location.hostname, + location.port ? (`:${ window.location.port}`) : '' + ].join(''); + const coralJsUrl = [talkBaseUrl, '/embed.js'].join(''); + const nonce = String(Math.random()).slice(2); + const streamElementId = `coral_talk_${nonce}`; + const embedText = ` +
+ + `.trim(); return (

{this.props.title}

diff --git a/client/coral-admin/src/translations.json b/client/coral-admin/src/translations.json index a4256848e..6dffeae42 100644 --- a/client/coral-admin/src/translations.json +++ b/client/coral-admin/src/translations.json @@ -50,7 +50,7 @@ "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": "Require Email Verification", "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.", diff --git a/client/coral-embed-stream/src/Embed.js b/client/coral-embed-stream/src/Embed.js index 550fc5fc1..9eea38bb4 100644 --- a/client/coral-embed-stream/src/Embed.js +++ b/client/coral-embed-stream/src/Embed.js @@ -20,6 +20,7 @@ import CommentBox from 'coral-plugin-commentbox/CommentBox'; import UserBox from 'coral-sign-in/components/UserBox'; import SignInContainer from 'coral-sign-in/containers/SignInContainer'; import SuspendedAccount from 'coral-framework/components/SuspendedAccount'; +import ChangeDisplayNameContainer from '../../coral-sign-in/containers/ChangeDisplayNameContainer'; import SettingsContainer from 'coral-settings/containers/SettingsContainer'; import RestrictedContent from 'coral-framework/components/RestrictedContent'; import ConfigureStreamContainer from 'coral-configure/containers/ConfigureStreamContainer'; @@ -130,7 +131,8 @@ class Embed extends Component {
:

{asset.settings.closedMessage}

} - {!loggedIn && } + {!loggedIn && } + {loggedIn && user && } tag + * (including copypasta dependencies like pym.js), but later there will be a + * build step and this code may use import statements + */ + +// using umd.js (https://github.com/umdjs/umd/blob/master/templates/returnExports.js) +(function (root, factory) { + /* eslint-disable */ + if (typeof define === 'function' && define.amd) { + + // AMD. Register as an anonymous module. + define([], factory); + } else if (typeof module === 'object' && module.exports) { + + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = factory(); + } else { + + // Browser globals (root is window) + root.Coral = factory(); + } + /* eslint-enable */ +}(this, function () { + + // This function should return value of window.Coral + var pym = requirePym(); + var Coral = {}; + var Talk = Coral.Talk = {}; + + /** + * Render a Talk stream + * @param {HTMLElement} el - Element to render the stream in + * @param {Object} opts - Configuration options for talk + * @param {String} opts.talk - Talk base URL + * @param {String} [opts.title] - Title of Stream (rendered in iframe) + * @param {String} [opts.asset] - parent Asset ID or URL. Comments in the + * stream will replies to this asset + */ + Talk.render = function (el, opts) { + if ( ! el) { + throw new Error('Please provide Coral.Talk.render() the HTMLElement you want to render Talk in.'); + } + if (typeof el !== 'object') { + throw new Error('Coral.Talk.render() expected HTMLElement but got ' + el + ' (' + typeof el + ')'); + } + opts = opts || {}; + + // @todo infer this URL without explicit user input (if possible, may have to be added at build/render time of this script) + if (! opts.talk) { + throw new Error('Coral.Talk.render() expects opts.talk as the Talk Base URL'); + } + + // ensure el has an id, as pym can't directly accept the HTMLElement + if ( ! el.id) {el.id = '_' + String(Math.random());} + var asset = opts.asset || window.location; + var pymParent = new pym.Parent( + el.id, + buildStreamIframeUrl(opts.talk, asset), + { + title: opts.title, + asset_url: asset, + id: el.id + '_iframe', + name: el.id + '_iframe' + } + ); + + configurePymParent(pymParent, asset); + }; + + return Coral; + + // build the URL to load in the pym iframe + function buildStreamIframeUrl(talkBaseUrl, asset) { + var iframeUrl = [ + talkBaseUrl, + (talkBaseUrl.match(/\/$/) ? '' : '/'), // make sure no double-'/' if opts.talk already ends with '/' + 'embed/stream?asset_url=', + encodeURIComponent(asset) + ].join(''); + return iframeUrl; + } + + // Set up postMessage listeners/handlers on the pymParent + // e.g. to resize the iframe, and navigate the host page + function configurePymParent(pymParent, assetUrl) { + var notificationOffset = 200; + var ready = false; + + // Resize parent iframe height when child height changes + pymParent.onMessage('height', function(height) { + + // TODO: In local testing, this is firing nonstop. Maybe there's a bug on the inside? + // Or it's by design of pym... but that's very wasteful of CPU and DOM reflows (jank) + pymParent.el.querySelector('iframe').height = height + 'px'; + }); + + // Helps child show notifications at the right scrollTop + pymParent.onMessage('getPosition', function() { + var position = viewport().height + document.body.scrollTop; + + if (position > notificationOffset) { + position = position - notificationOffset; + } + + pymParent.sendMessage('position', position); + }); + + // Tell child when parent's DOMContentLoaded + pymParent.onMessage('childReady', function () { + var interval = setInterval(function () { + if (ready) { + window.clearInterval(interval); + + // @todo - It's weird to me that this is sent here in addition to the iframe URL. Could it just be in one place? + pymParent.sendMessage('DOMContentLoaded', assetUrl); + } + }, 100); + }); + + // When end-user clicks link in iframe, open it in parent context + pymParent.onMessage('navigate', function (url) { + window.open(url, '_blank').focus(); + }); + + // wait till images and other iframes are loaded before scrolling the page. + // or do we want to be more aggressive and scroll when we hit DOM ready? + document.addEventListener('DOMContentLoaded', function () { + ready = true; + }); + + // get dimensions of viewport + function viewport() { + var e = window, a = 'inner'; + if ( !( 'innerWidth' in window ) ){ + a = 'client'; + e = document.documentElement || document.body; + } + return { + width : e[a + 'Width'], + height : e[a + 'Height'] + }; + } + } + + // return a reference to pym.js + function requirePym() { + var pym; + + // fake AMD `define` so that the pym.js copypasta doesn't create a global + function define(createPym) { + pym = createPym(); + } + define.amd = true; + + /* eslint-disable */ + /*! pym.js - v1.1.2 - 2016-10-25 */ + !function(a){"function"==typeof define&&define.amd?define(a):"undefined"!=typeof module&&module.exports?module.exports=a():window.pym=a.call(this)}(function(){var a="xPYMx",b={},c=function(a){var b=new RegExp("[\\?&]"+a.replace(/[\[]/,"\\[").replace(/[\]]/,"\\]")+"=([^&#]*)"),c=b.exec(location.search);return null===c?"":decodeURIComponent(c[1].replace(/\+/g," "))},d=function(a,b){if("*"===b.xdomain||a.origin.match(new RegExp(b.xdomain+"$")))return!0},e=function(b,c,d){var e=["pym",b,c,d];return e.join(a)},f=function(b){var c=["pym",b,"(\\S+)","(.*)"];return new RegExp("^"+c.join(a)+"$")},g=function(){for(var a=b.autoInitInstances.length,c=a-1;c>=0;c--){var d=b.autoInitInstances[c];d.el.getElementsByTagName("iframe").length&&d.el.getElementsByTagName("iframe")[0].contentWindow||b.autoInitInstances.splice(c,1)}};return b.autoInitInstances=[],b.autoInit=function(){var a=document.querySelectorAll("[data-pym-src]:not([data-pym-auto-initialized])"),c=a.length;g();for(var d=0;d-1&&(b=this.url.substring(c,this.url.length),this.url=this.url.substring(0,c)),this.url.indexOf("?")<0?this.url+="?":this.url+="&",this.iframe.src=this.url+"initialWidth="+a+"&childId="+this.id+"&parentTitle="+encodeURIComponent(document.title)+"&parentUrl="+encodeURIComponent(window.location.href)+b,this.iframe.setAttribute("width","100%"),this.iframe.setAttribute("scrolling","no"),this.iframe.setAttribute("marginheight","0"),this.iframe.setAttribute("frameborder","0"),this.settings.title&&this.iframe.setAttribute("title",this.settings.title),void 0!==this.settings.allowfullscreen&&this.settings.allowfullscreen!==!1&&this.iframe.setAttribute("allowfullscreen",""),void 0!==this.settings.sandbox&&"string"==typeof this.settings.sandbox&&this.iframe.setAttribute("sandbox",this.settings.sandbox),this.settings.id&&(document.getElementById(this.settings.id)||this.iframe.setAttribute("id",this.settings.id)),this.settings.name&&this.iframe.setAttribute("name",this.settings.name);this.el.firstChild;)this.el.removeChild(this.el.firstChild);this.el.appendChild(this.iframe),window.addEventListener("resize",this._onResize)},this._onResize=function(){this.sendWidth()}.bind(this),this._fire=function(a,b){if(a in this.messageHandlers)for(var c=0;c ({type: actions.SHOW_SIGNIN_DIALOG, offset}); export const hideSignInDialog = () => ({type: actions.HIDE_SIGNIN_DIALOG}); +export const createDisplayNameRequest = () => ({type: actions.CREATE_DISPLAYNAME_REQUEST}); +export const showCreateDisplayNameDialog = () => ({type: actions.SHOW_CREATEDISPLAYNAME_DIALOG}); +export const hideCreateDisplayNameDialog = () => ({type: actions.HIDE_CREATEDISPLAYNAME_DIALOG}); + +const createDisplayNameSuccess = () => ({type: actions.CREATEDISPLAYNAME_SUCCESS}); +const createDisplayNameFailure = error => ({type: actions.CREATEDISPLAYNAME_FAILURE, error}); + +export const updateDisplayName = displayName => ({type: actions.UPDATE_DISPLAYNAME, displayName}); + +export const createDisplayName = (userId, formData) => dispatch => { + dispatch(createDisplayNameRequest()); + coralApi(`/users/${userId}/displayname`, {method: 'POST', body: formData}) + .then((user) => { + dispatch(createDisplayNameSuccess()); + dispatch(hideCreateDisplayNameDialog()); + dispatch(updateDisplayName(user)); + }) + .catch(error => { + dispatch(createDisplayNameFailure(lang.t(`error.${error.message}`))); + }); +}; + export const changeView = view => dispatch => dispatch({ type: actions.CHANGE_VIEW, @@ -21,7 +43,6 @@ 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()); @@ -36,7 +57,6 @@ export const fetchSignIn = (formData) => (dispatch) => { // 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 @@ -60,6 +80,19 @@ export const fetchSignInFacebook = () => dispatch => { ); }; +// Sign Up Facebook + +const signUpFacebookRequest = () => ({type: actions.FETCH_SIGNUP_FACEBOOK_REQUEST}); + +export const fetchSignUpFacebook = () => dispatch => { + dispatch(signUpFacebookRequest()); + window.open( + `${base}/auth/facebook`, + 'Continue with Facebook', + 'menubar=0,resizable=0,width=500,height=500,top=200,left=500' + ); +}; + export const facebookCallback = (err, data) => dispatch => { if (err) { signInFacebookFailure(err); @@ -69,6 +102,7 @@ export const facebookCallback = (err, data) => dispatch => { const user = JSON.parse(data); dispatch(signInFacebookSuccess(user)); dispatch(hideSignInDialog()); + dispatch(showCreateDisplayNameDialog()); } catch (err) { dispatch(signInFacebookFailure(err)); return; @@ -81,15 +115,12 @@ const signUpRequest = () => ({type: actions.FETCH_SIGNUP_REQUEST}); const signUpSuccess = user => ({type: actions.FETCH_SIGNUP_SUCCESS, user}); const signUpFailure = error => ({type: actions.FETCH_SIGNUP_FAILURE, error}); -export const fetchSignUp = formData => (dispatch) => { +export const fetchSignUp = (formData, redirectUri) => (dispatch) => { dispatch(signUpRequest()); - coralApi('/users', {method: 'POST', body: formData}) + coralApi('/users', {method: 'POST', body: formData, headers: {'X-Pym-Url': redirectUri}}) .then(({user}) => { dispatch(signUpSuccess(user)); - setTimeout(() =>{ - dispatch(changeView('SIGNIN')); - }, 3000); }) .catch(error => { dispatch(signUpFailure(lang.t(`error.${error.message}`))); @@ -150,20 +181,20 @@ export const checkLogin = () => dispatch => { }); }; -const confirmEmailRequest = () => ({type: actions.CONFIRM_EMAIL_REQUEST}); -const confirmEmailSuccess = () => ({type: actions.CONFIRM_EMAIL_SUCCESS}); -const confirmEmailFailure = () => ({type: actions.CONFIRM_EMAIL_FAILURE}); +const verifyEmailRequest = () => ({type: actions.VERIFY_EMAIL_REQUEST}); +const verifyEmailSuccess = () => ({type: actions.VERIFY_EMAIL_SUCCESS}); +const verifyEmailFailure = () => ({type: actions.VERIFY_EMAIL_FAILURE}); -export const requestConfirmEmail = email => dispatch => { - dispatch(confirmEmailRequest()); - return coralApi('/users/resend-confirm', {method: 'POST', body: {email}}) +export const requestConfirmEmail = (email, redirectUri) => dispatch => { + dispatch(verifyEmailRequest()); + return coralApi('/users/resend-verify', {method: 'POST', body: {email}, headers: {'X-Pym-Url': redirectUri}}) .then(() => { - dispatch(confirmEmailSuccess()); + dispatch(verifyEmailSuccess()); }) .catch(err => { - console.log('failed to send email confirmation', err); + console.log('failed to send email verification', err); - // email might have already been confirmed - dispatch(confirmEmailFailure()); + // email might have already been verifyed + dispatch(verifyEmailFailure()); }); }; diff --git a/client/coral-framework/constants/auth.js b/client/coral-framework/constants/auth.js index 1dae348df..b6ad3d3a1 100644 --- a/client/coral-framework/constants/auth.js +++ b/client/coral-framework/constants/auth.js @@ -4,6 +4,13 @@ export const CLEAN_STATE = 'CLEAN_STATE'; export const SHOW_SIGNIN_DIALOG = 'SHOW_SIGNIN_DIALOG'; export const HIDE_SIGNIN_DIALOG = 'HIDE_SIGNIN_DIALOG'; +export const CREATE_DISPLAYNAME_REQUEST = 'CREATE_DISPLAYNAME_REQUEST'; +export const CREATEDISPLAYNAME_SUCCESS = 'CREATEDISPLAYNAME_SUCCESS'; +export const CREATEDISPLAYNAME_FAILURE = 'CREATEDISPLAYNAME_FAILURE'; +export const CREATE_DISPLAYNAME = 'CREATE_DISPLAYNAME'; +export const SHOW_CREATEDISPLAYNAME_DIALOG = 'SHOW_CREATEDISPLAYNAME_DIALOG'; +export const HIDE_CREATEDISPLAYNAME_DIALOG = 'HIDE_CREATEDISPLAYNAME_DIALOG'; + export const FETCH_SIGNUP_REQUEST = 'FETCH_SIGNUP_REQUEST'; export const FETCH_SIGNUP_FAILURE = 'FETCH_SIGNUP_FAILURE'; export const FETCH_SIGNUP_SUCCESS = 'FETCH_SIGNUP_SUCCESS'; @@ -16,6 +23,7 @@ export const FETCH_SIGNIN_FACEBOOK_REQUEST = 'FETCH_SIGNIN_FACEBOOK_REQUEST'; export const FETCH_SIGNIN_FACEBOOK_FAILURE = 'FETCH_SIGNIN_FACEBOOK_FAILURE'; export const FETCH_SIGNIN_FACEBOOK_SUCCESS = 'FETCH_SIGNIN_FACEBOOK_SUCCESS'; +export const FETCH_SIGNUP_FACEBOOK_REQUEST = 'FETCH_SIGNUP_FACEBOOK_REQUEST'; export const FETCH_FORGOT_PASSWORD_REQUEST = 'FETCH_FORGOT_PASSWORD_REQUEST'; export const FETCH_FORGOT_PASSWORD_SUCCESS = 'FETCH_FORGOT_PASSWORD_SUCCESS'; export const FETCH_FORGOT_PASSWORD_FAILURE = 'FETCH_FORGOT_PASSWORD_FAILURE'; @@ -33,7 +41,7 @@ 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'; +export const VERIFY_EMAIL_REQUEST = 'VERIFY_EMAIL_REQUEST'; +export const VERIFY_EMAIL_SUCCESS = 'VERIFY_EMAIL_SUCCESS'; +export const VERIFY_EMAIL_FAILURE = 'VERIFY_EMAIL_FAILURE'; +export const UPDATE_DISPLAYNAME = 'UPDATE_DISPLAYNAME'; diff --git a/client/coral-framework/constants/user.js b/client/coral-framework/constants/user.js index 9c6f508fe..57f4e706b 100644 --- a/client/coral-framework/constants/user.js +++ b/client/coral-framework/constants/user.js @@ -5,3 +5,4 @@ export const COMMENTS_BY_USER_REQUEST = 'COMMENTS_BY_USER_REQUEST'; export const COMMENTS_BY_USER_SUCCESS = 'COMMENTS_BY_USER_SUCCESS'; export const COMMENTS_BY_USER_FAILURE = 'COMMENTS_BY_USER_FAILURE'; export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS'; +export const UPDATE_DISPLAYNAME = 'UPDATE_DISPLAYNAME'; diff --git a/client/coral-framework/helpers/response.js b/client/coral-framework/helpers/response.js index d95773c04..64803faef 100644 --- a/client/coral-framework/helpers/response.js +++ b/client/coral-framework/helpers/response.js @@ -14,7 +14,8 @@ const buildOptions = (inputOptions = {}) => { _csrf: csurfDOM ? csurfDOM.content : false }; - const options = Object.assign({}, defaultOptions, inputOptions); + let options = Object.assign({}, defaultOptions, inputOptions); + options.headers = Object.assign({}, defaultOptions.headers, inputOptions.headers); if (options._csrf) { switch (options.method.toLowerCase()) { diff --git a/client/coral-framework/reducers/auth.js b/client/coral-framework/reducers/auth.js index 6cb4e97cd..da68db043 100644 --- a/client/coral-framework/reducers/auth.js +++ b/client/coral-framework/reducers/auth.js @@ -7,14 +7,16 @@ const initialState = Map({ isAdmin: false, user: null, showSignInDialog: false, + showCreateDisplayNameDialog: false, view: 'SIGNIN', error: '', passwordRequestSuccess: null, passwordRequestFailure: null, - emailConfirmationFailure: false, - emailConfirmationLoading: false, - emailConfirmationSuccess: false, - successSignUp: false + emailVerificationFailure: false, + emailVerificationLoading: false, + emailVerificationSuccess: false, + successSignUp: false, + fromSignUp: false }); const purge = user => { @@ -36,11 +38,26 @@ export default function auth (state = initialState, action) { error: '', passwordRequestFailure: null, passwordRequestSuccess: null, - emailConfirmationFailure: false, - emailConfirmationSuccess: false, - emailConfirmationLoading: false, + emailVerificationFailure: false, + emailVerificationSuccess: false, + emailVerificationLoading: false, successSignUp: false })); + case actions.SHOW_CREATEDISPLAYNAME_DIALOG : + return state + .set('showCreateDisplayNameDialog', true); + case actions.HIDE_CREATEDISPLAYNAME_DIALOG : + return state.merge(Map({ + showCreateDisplayNameDialog: false + })); + case actions.CREATEDISPLAYNAME_SUCCESS : + return state.merge(Map({ + showCreateDisplayNameDialog: false, + error: '' + })); + case actions.CREATEDISPLAYNAME_FAILURE : + return state + .set('error', action.error); case actions.CHANGE_VIEW : return state .set('error', '') @@ -72,6 +89,12 @@ export default function auth (state = initialState, action) { .set('isLoading', false) .set('error', action.error) .set('user', null); + case actions.FETCH_SIGNUP_FACEBOOK_REQUEST: + return state + .set('fromSignUp', true); + case actions.FETCH_SIGNIN_FACEBOOK_REQUEST: + return state + .set('fromSignUp', false); case actions.FETCH_SIGNIN_FACEBOOK_SUCCESS: return state .set('user', purge(action.user)) @@ -107,16 +130,19 @@ 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: + case actions.UPDATE_DISPLAYNAME: return state - .set('emailConfirmationFailure', true) - .set('emailConfirmationLoading', false); - case actions.CONFIRM_EMAIL_REQUEST: - return state.set('emailConfirmationLoading', true); - case actions.CONFIRM_EMAIL_SUCCESS: + .set('user', purge(action.displayName)); + case actions.VERIFY_EMAIL_FAILURE: return state - .set('emailConfirmationSuccess', true) - .set('emailConfirmationLoading', false); + .set('emailVerificationFailure', true) + .set('emailVerificationLoading', false); + case actions.VERIFY_EMAIL_REQUEST: + return state.set('emailVerificationLoading', true); + case actions.VERIFY_EMAIL_SUCCESS: + return state + .set('emailVerificationSuccess', true) + .set('emailVerificationLoading', false); default : return state; } diff --git a/client/coral-sign-in/components/CreateDisplayNameDialog.js b/client/coral-sign-in/components/CreateDisplayNameDialog.js new file mode 100644 index 000000000..4d8ef9fb3 --- /dev/null +++ b/client/coral-sign-in/components/CreateDisplayNameDialog.js @@ -0,0 +1,48 @@ +import React from 'react'; +import FormField from 'coral-ui/components/FormField'; +import Alert from './Alert'; +import Button from 'coral-ui/components/Button'; +import {Dialog} 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 CreateDisplayNameDialog = ({open, handleClose, offset, formData, handleSubmitDisplayName, handleChange, ...props}) => ( + + × +
+
+

+ {lang.t('createdisplay.writeyourusername')} +

+
+
+ + { props.auth.error && {props.auth.error} } +
+ + { props.errors.displayName && {lang.t('createdisplay.specialCharacters')} } +
+ +
+ +
+
+
+); + +export default CreateDisplayNameDialog; diff --git a/client/coral-sign-in/components/SignDialog.js b/client/coral-sign-in/components/SignDialog.js index 0645f110f..6243472b4 100644 --- a/client/coral-sign-in/components/SignDialog.js +++ b/client/coral-sign-in/components/SignDialog.js @@ -17,12 +17,7 @@ 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 e27185b52..c7a5dc80d 100644 --- a/client/coral-sign-in/components/SignInContent.js +++ b/client/coral-sign-in/components/SignInContent.js @@ -10,35 +10,28 @@ const SignInContent = ({ handleChange, handleChangeEmail, emailToBeResent, - handleResendConfirmation, - emailConfirmationLoading, - emailConfirmationSuccess, + handleResendVerification, + emailVerificationLoading, + emailVerificationSuccess, formData, - ...props + changeView, + handleSignIn, + auth, + fetchSignInFacebook }) => { return (

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

-
- -
-
-

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

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

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

+ auth.emailVerificationFailure + ? +

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

- {emailConfirmationLoading && } - {emailConfirmationSuccess && } + {emailVerificationLoading && } + {emailVerificationSuccess && } - :
- - -
- { - !props.auth.isLoading ? - - : - - } + :
+
+
- +
+

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

+
+
+ + +
+ { + !auth.isLoading ? + + : + + } +
+ +
}
- props.changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')} + changeView('FORGOT')}>{lang.t('signIn.forgotYourPass')} {lang.t('signIn.needAnAccount')} - props.changeView('SIGNUP')} id='coralRegister'> + changeView('SIGNUP')} id='coralRegister'> {lang.t('signIn.register')} @@ -90,9 +95,17 @@ const SignInContent = ({ }; SignInContent.propTypes = { - emailConfirmationLoading: PropTypes.bool.isRequired, - emailConfirmationSuccess: PropTypes.bool.isRequired, - handleResendConfirmation: PropTypes.func.isRequired, + auth: PropTypes.shape({ + isLoading: PropTypes.bool.isRequired, + error: PropTypes.object, + emailVerificationFailure: PropTypes.bool + }).isRequired, + fetchSignInFacebook: PropTypes.func.isRequired, + handleSignIn: PropTypes.func.isRequired, + changeView: PropTypes.func.isRequired, + emailVerificationLoading: PropTypes.bool.isRequired, + emailVerificationSuccess: PropTypes.bool.isRequired, + handleResendVerification: PropTypes.func.isRequired, handleChangeEmail: PropTypes.func.isRequired, emailToBeResent: PropTypes.string.isRequired }; diff --git a/client/coral-sign-in/components/SignUpContent.js b/client/coral-sign-in/components/SignUpContent.js index 246d9d884..03c083070 100644 --- a/client/coral-sign-in/components/SignUpContent.js +++ b/client/coral-sign-in/components/SignUpContent.js @@ -1,4 +1,4 @@ -import React from 'react'; +import React, {PropTypes} from 'react'; import Alert from './Alert'; import {Button, FormField, Spinner, Success} from 'coral-ui'; import styles from './styles.css'; @@ -6,83 +6,148 @@ import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; const lang = new I18n(translations); -const SignUpContent = ({handleChange, formData, ...props}) => ( -
-
-

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

-
-
- -
-
-

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

-
- { props.auth.error && {props.auth.error} } -
- - - - { props.errors.password && Password must be at least 8 characters. } - -
- { !props.auth.isLoading && !props.auth.successSignUp && ( - - )} - { props.auth.isLoading && } - { !props.auth.isLoading && props.auth.successSignUp && } + +
+ + { auth.error && {auth.error} } + { beforeSignup && +
+
+ +
+
+

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

+
+ + + + + { errors.password && Password must be at least 8 characters. } + +
+ + { auth.isLoading && } +
+ +
+ } + { + successfulSignup && +
+ + { + emailVerificationEnabled && +

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

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

+ } +
+ } +
+ + {lang.t('signIn.alreadyHaveAnAccount')} + changeView('SIGNIN')}> + {lang.t('signIn.signIn')} + + +
- -
- - {lang.t('signIn.alreadyHaveAnAccount')} - props.changeView('SIGNIN')}> - {lang.t('signIn.signIn')} - - -
-
-); + ); + } +} export default SignUpContent; diff --git a/client/coral-sign-in/containers/ChangeDisplayNameContainer.js b/client/coral-sign-in/containers/ChangeDisplayNameContainer.js new file mode 100644 index 000000000..bc7fe8267 --- /dev/null +++ b/client/coral-sign-in/containers/ChangeDisplayNameContainer.js @@ -0,0 +1,135 @@ +import React, {Component} from 'react'; +import {connect} from 'react-redux'; + +import validate from 'coral-framework/helpers/validate'; +import errorMsj from 'coral-framework/helpers/error'; + +import CreateDisplayNameDialog from '../components/CreateDisplayNameDialog'; + +import I18n from 'coral-framework/modules/i18n/i18n'; +import translations from '../translations'; +const lang = new I18n(translations); + +import { + showCreateDisplayNameDialog, + hideCreateDisplayNameDialog, + invalidForm, + validForm, + createDisplayName +} from '../../coral-framework/actions/auth'; + +class ChangeDisplayNameContainer extends Component { + initialState = { + formData: { + displayName: '', + }, + errors: {}, + showErrors: false + }; + + constructor(props) { + super(props); + this.state = this.initialState; + this.handleChange = this.handleChange.bind(this); + this.handleSubmitDisplayName = this.handleSubmitDisplayName.bind(this); + this.handleClose = this.handleClose.bind(this); + this.addError = this.addError.bind(this); + } + + handleChange(e) { + const {name, value} = e.target; + this.setState(state => ({ + ...state, + formData: { + ...state.formData, + [name]: value + } + }), () => { + this.validation(name, value); + }); + } + + addError(name, error) { + return this.setState(state => ({ + errors: { + ...state.errors, + [name]: error + } + })); + } + + validation(name, value) { + const {addError} = this; + + if (!value.length) { + addError(name, lang.t('createdisplay.requiredField')); + } else if (!validate[name](value)) { + addError(name, errorMsj[name]); + } else { + const { [name]: prop, ...errors } = this.state.errors; // eslint-disable-line + // Removes Error + this.setState(state => ({...state, errors})); + } + } + + isCompleted() { + const {formData} = this.state; + return !Object.keys(formData).filter(prop => !formData[prop].length).length; + } + + displayErrors(show = true) { + this.setState({showErrors: show}); + } + + handleSubmitDisplayName(e) { + e.preventDefault(); + const {errors} = this.state; + const {validForm, invalidForm} = this.props; + this.displayErrors(); + if (this.isCompleted() && !Object.keys(errors).length) { + this.props.createDisplayName(this.props.user.id, this.state.formData); + validForm(); + } else { + invalidForm(lang.t('createdisplay.checkTheForm')); + } + } + + handleClose() { + this.props.hideCreateDisplayNameDialog(); + } + + render() { + const {loggedIn, auth, offset} = this.props; + return ( +
+ +
+ ); + } +} + +const mapStateToProps = state => ({ + auth: state.auth.toJS() +}); + +const mapDispatchToProps = dispatch => ({ + createDisplayName: (userid, formData) => dispatch(createDisplayName(userid, formData)), + showCreateDisplayNameDialog: () => dispatch(showCreateDisplayNameDialog()), + hideCreateDisplayNameDialog: () => dispatch(hideCreateDisplayNameDialog()), + invalidForm: error => dispatch(invalidForm(error)), + validForm: () => dispatch(validForm()) +}); + +export default connect( + mapStateToProps, + mapDispatchToProps +)(ChangeDisplayNameContainer); diff --git a/client/coral-sign-in/containers/SignInContainer.js b/client/coral-sign-in/containers/SignInContainer.js index 2435cdcfd..c92fe024d 100644 --- a/client/coral-sign-in/containers/SignInContainer.js +++ b/client/coral-sign-in/containers/SignInContainer.js @@ -1,4 +1,4 @@ -import React, {Component} from 'react'; +import React, {Component, PropTypes} from 'react'; import {connect} from 'react-redux'; import SignDialog from '../components/SignDialog'; import Button from 'coral-ui/components/Button'; @@ -6,6 +6,7 @@ import validate from 'coral-framework/helpers/validate'; import errorMsj from 'coral-framework/helpers/error'; import I18n from 'coral-framework/modules/i18n/i18n'; import translations from '../translations'; +import {pym} from 'coral-framework'; const lang = new I18n(translations); import { @@ -15,6 +16,7 @@ import { showSignInDialog, hideSignInDialog, fetchSignInFacebook, + fetchSignUpFacebook, fetchForgotPassword, requestConfirmEmail, facebookCallback, @@ -41,12 +43,16 @@ class SignInContainer extends Component { this.state = this.initialState; this.handleChange = this.handleChange.bind(this); this.handleChangeEmail = this.handleChangeEmail.bind(this); - this.handleResendConfirmation = this.handleResendConfirmation.bind(this); + this.handleResendVerification = this.handleResendVerification.bind(this); this.handleSignUp = this.handleSignUp.bind(this); this.handleSignIn = this.handleSignIn.bind(this); this.addError = this.addError.bind(this); } + static propTypes = { + requireEmailConfirmation: PropTypes.bool.isRequired + } + componentWillMount () { this.props.checkLogin(); } @@ -79,9 +85,9 @@ class SignInContainer extends Component { this.setState({emailToBeResent: value}); } - handleResendConfirmation(e) { + handleResendVerification(e) { e.preventDefault(); - this.props.requestConfirmEmail(this.state.emailToBeResent) + this.props.requestConfirmEmail(this.state.emailToBeResent, pym.parentUrl || location.href) .then(() => { setTimeout(() => { @@ -132,7 +138,7 @@ class SignInContainer extends Component { const {fetchSignUp, validForm, invalidForm} = this.props; this.displayErrors(); if (this.isCompleted() && !Object.keys(errors).length) { - fetchSignUp(this.state.formData); + fetchSignUp(this.state.formData, pym.parentUrl || location.href); validForm(); } else { invalidForm(lang.t('signIn.checkTheForm')); @@ -145,8 +151,9 @@ class SignInContainer extends Component { } render() { - const {auth, showSignInDialog, noButton, offset} = this.props; - const {emailConfirmationLoading, emailConfirmationSuccess} = auth; + const {auth, showSignInDialog, noButton, offset, requireEmailConfirmation} = this.props; + const {emailVerificationLoading, emailVerificationSuccess} = auth; + return (
{!noButton &&