diff --git a/packages/component-helper-service/src/services/User.js b/packages/component-helper-service/src/services/User.js index 97821f164f7fe0af918f88c97c28c7163024721a..39ad3356cfb10e2ca78660403187bed70094f564 100644 --- a/packages/component-helper-service/src/services/User.js +++ b/packages/component-helper-service/src/services/User.js @@ -28,6 +28,12 @@ class User { handlingEditor: role === 'handlingEditor', invitationToken: role === 'reviewer' ? uuid.v4() : '', isActive: true, + notifications: { + email: { + system: true, + user: true, + }, + }, } let newUser = new UserModel(userBody) diff --git a/packages/component-mail-service/src/Mail.js b/packages/component-mail-service/src/Mail.js index cb3181bd26ca3b95305fdf8222022b01b3789a22..0c8c9319d0d3ef16e67e2c2c53f6db21e78a94bc 100644 --- a/packages/component-mail-service/src/Mail.js +++ b/packages/component-mail-service/src/Mail.js @@ -6,6 +6,7 @@ const confirmSignUp = config.get('confirm-signup.url') const resetPath = config.get('invite-reset-password.url') const resetPasswordPath = config.get('invite-reviewer.url') const forgotPath = config.get('forgot-password.url') +const unsubscribeSlug = config.get('unsubscribe.url') module.exports = { sendSimpleEmail: async ({ @@ -18,6 +19,13 @@ module.exports = { let subject, textBody let emailTemplate = 'simpleCTA' const replacements = {} + replacements.unsubscribeLink = helpers.createUrl( + dashboardUrl, + unsubscribeSlug, + { + id: user.id, + }, + ) switch (emailType) { case 'assign-handling-editor': subject = 'Hindawi Handling Editor Invitation' @@ -28,6 +36,7 @@ module.exports = { replacements.previewText = 'An Editor in Chief has assigned you' replacements.buttonText = 'VIEW DASHBOARD' replacements.url = dashboardUrl + textBody = `${replacements.headline} ${replacements.paragraph} ${ replacements.url } ${replacements.buttonText}` @@ -236,6 +245,9 @@ module.exports = { previewText: 'invitation from Hindawi', intro: `Dear ${user.firstName} ${user.lastName}`, manuscriptText: '', + unsubscribeLink: helpers.createUrl(baseUrl, unsubscribeSlug, { + id: user.id, + }), } let textBody switch (emailType) { @@ -306,8 +318,8 @@ module.exports = { return Email.send(mailData) }, sendNotificationEmail: async ({ - toEmail, user, + toEmail, emailType, meta = { privateNote: '' }, }) => { @@ -324,9 +336,12 @@ module.exports = { : '' const replacements = { detailsUrl, - beforeAnchor: '', - afterAnchor: '', hasLink: true, + afterAnchor: '', + beforeAnchor: '', + unsubscribeLink: helpers.createUrl(meta.baseUrl, unsubscribeSlug, { + id: user.id, + }), } switch (emailType) { case 'unassign-reviewer': diff --git a/packages/component-mail-service/src/helpers/helpers.js b/packages/component-mail-service/src/helpers/helpers.js index 6f6a01f113ca5124039f23e26d3d49316163f0b2..12b4dafaebc92d85c55059e28d5974969f2dff3b 100644 --- a/packages/component-mail-service/src/helpers/helpers.js +++ b/packages/component-mail-service/src/helpers/helpers.js @@ -9,7 +9,7 @@ const createUrl = (baseUrl, slug, queryParams = null) => const getEmailBody = (emailType, replacements) => { handlePartial('header', replacements) - handlePartial('footer') + handlePartial('footer', replacements) handlePartial('mainButton', replacements) handlePartial('mainBody', replacements) @@ -18,7 +18,7 @@ const getEmailBody = (emailType, replacements) => { const getNotificationBody = (emailType, replacements) => { handlePartial('notificationHeader', replacements) - handlePartial('footer') + handlePartial('footer', replacements) handlePartial('signature', replacements) if (replacements.detailsUrl !== undefined) handlePartial('manuscriptDetailsLink', replacements) @@ -29,7 +29,7 @@ const getNotificationBody = (emailType, replacements) => { const getInvitationBody = (emailType, replacements) => { handlePartial('invitationHeader', replacements) - handlePartial('footer') + handlePartial('footer', replacements) handlePartial('invitationUpperContent', replacements) handlePartial('invitationButtons', replacements) handlePartial('manuscriptData', replacements) @@ -117,10 +117,10 @@ const getExpectedDate = (timestamp, daysExpected) => { } module.exports = { + getBody, createUrl, getEmailBody, getExpectedDate, getNotificationBody, getInvitationBody, - getBody, } diff --git a/packages/component-mail-service/src/templates/partials/footer.hbs b/packages/component-mail-service/src/templates/partials/footer.hbs index 4b93ccf593422d3e04aa70665ac05fa03a65af0d..355088275373e0378176b00ca6a9007cc2d54a62 100644 --- a/packages/component-mail-service/src/templates/partials/footer.hbs +++ b/packages/component-mail-service/src/templates/partials/footer.hbs @@ -10,7 +10,7 @@ </p> </div> <p style="font-family:Arial, Helvetica, sans-serif;font-size:12px;line-height:20px"> - <a class="Unsubscribe--unsubscribeLink" href="[Unsubscribe]">Unsubscribe</a> + <a class="Unsubscribe--unsubscribeLink" href="{{ unsubscribeLink }}">Unsubscribe</a> </p> </div> </td> diff --git a/packages/component-user-manager/src/Users.js b/packages/component-user-manager/src/Users.js index 95d064990c554472413277282e82986111fc8609..38c40169905ab9f328657e7721bb0a98d65f369e 100644 --- a/packages/component-user-manager/src/Users.js +++ b/packages/component-user-manager/src/Users.js @@ -125,6 +125,44 @@ const Users = app => { require('./routes/users/changePassword')(app.locals.models), ) + /** + * @api {patch} /api/users/subscriptions Change user's email subscription flag + * @apiGroup Users + * @apiParamExample {json} Body + * { + * "id": "a6184463-b17a-42f8-b02b-ae1d755cdc6b", + * "subscribe": true, + * } + * @apiSuccessExample {json} Success + * HTTP/1.1 200 OK + * { + * "id": "a6184463-b17a-42f8-b02b-ae1d755cdc6b", + * "type": "user", + * "admin": false, + * "email": "email@example.com", + * "teams": [], + * "username": "email@example.com", + * "fragments": [], + * "collections": [], + * "isConfirmed": true, + * "editorInChief": false, + * "handlingEditor": false, + * "notifications": { + * "email": { + * "system": true, + * "user": true + * } + * } + * } + * @apiErrorExample {json} Reset password errors + * HTTP/1.1 400 Bad Request + * HTTP/1.1 404 Not Found + */ + app.patch( + '/api/users/subscribe', + require('./routes/users/subscribe')(app.locals.models), + ) + // register ORCID authentication strategy orcidRoutes(app) } diff --git a/packages/component-user-manager/src/routes/users/subscribe.js b/packages/component-user-manager/src/routes/users/subscribe.js new file mode 100644 index 0000000000000000000000000000000000000000..fa90c8481012b0c8e045842764c508d8b0332d48 --- /dev/null +++ b/packages/component-user-manager/src/routes/users/subscribe.js @@ -0,0 +1,23 @@ +const { set } = require('lodash') +const { services } = require('pubsweet-component-helper-service') + +module.exports = models => async (req, res) => { + const { subscribe, id } = req.body + + if (!services.checkForUndefinedParams(subscribe, id)) + return res.status(400).json({ error: 'Missing required params.' }) + + let user + try { + user = await models.User.find(id) + set(user, 'notifications.email.user', subscribe) + user = await user.save() + + return res.status(200).json({ user }) + } catch (e) { + const notFoundError = await services.handleNotFoundError(e, 'User') + return res.status(notFoundError.status).json({ + error: notFoundError.message, + }) + } +} diff --git a/packages/components-faraday/src/components/Admin/AdminUsers.js b/packages/components-faraday/src/components/Admin/AdminUsers.js index ecaf18ab7d11af51e6ef304bd2f252b78a111dc0..2fe5afe3a8274416b3895fe1bbd60f3d4c7fd6e6 100644 --- a/packages/components-faraday/src/components/Admin/AdminUsers.js +++ b/packages/components-faraday/src/components/Admin/AdminUsers.js @@ -142,10 +142,10 @@ export default compose( setPage(p => (p > 0 ? p - 1 : p)) }, getStatusLabel: () => ({ isConfirmed, isActive = true }) => () => { - if (isConfirmed) { - return isActive ? 'Active' : 'Inactive' + if (!isActive) { + return 'Inactive' } - return 'Invited' + return isConfirmed ? 'Active' : 'Invited' }, toggleUserStatus: ({ dispatch, diff --git a/packages/components-faraday/src/components/Admin/utils.js b/packages/components-faraday/src/components/Admin/utils.js index 99934cd10843f0d3332497abf7c6223c9b6ca63d..2cb2c8023b37cc29e41b5972151dda42cc3ffe03 100644 --- a/packages/components-faraday/src/components/Admin/utils.js +++ b/packages/components-faraday/src/components/Admin/utils.js @@ -31,6 +31,12 @@ export const setAdmin = values => { password: 'defaultpass', editorInChief: newValues.role === 'editorInChief', handlingEditor: newValues.role === 'handlingEditor', + notifications: { + email: { + system: true, + user: true, + }, + }, } } diff --git a/packages/components-faraday/src/components/UserProfile/AccountDetails.js b/packages/components-faraday/src/components/UserProfile/AccountDetails.js index 768c8a46ecafb78fb2b7ab543e18792365d92b03..81080db096d193b2d48cac5a881ce79fe5eee5a2 100644 --- a/packages/components-faraday/src/components/UserProfile/AccountDetails.js +++ b/packages/components-faraday/src/components/UserProfile/AccountDetails.js @@ -29,7 +29,7 @@ export default compose( withJournal, withState('isEdit', 'setEdit', false), withHandlers({ - setEditMode: ({ setEdit }) => value => setEdit(value), + setEditMode: ({ setEdit }) => value => () => setEdit(value), }), )(AccountDetails) diff --git a/packages/components-faraday/src/components/UserProfile/AccountDetailsCard.js b/packages/components-faraday/src/components/UserProfile/AccountDetailsCard.js index 22dadc2aa731f7a0406cc81ff7e968b736bc709e..7e085536a808b582f8faa40dfbfc2fb5a1602e95 100644 --- a/packages/components-faraday/src/components/UserProfile/AccountDetailsCard.js +++ b/packages/components-faraday/src/components/UserProfile/AccountDetailsCard.js @@ -3,10 +3,10 @@ import React, { Fragment } from 'react' import { Row, RowItem, - LabelHeader, + LinkText, LabelTitle, + LabelHeader, DefaultText, - LinkText, } from '../UIComponents/FormItems' import { getUserTitle } from '../utils' @@ -48,7 +48,7 @@ const AccountDetailsCard = ({ </Row> <Row noMargin> <RowItem> - <LinkText onClick={() => setEditMode(true)}>Edit details</LinkText> + <LinkText onClick={setEditMode(true)}>Edit details</LinkText> <LinkText onClick={() => history.push('/profile/change-password')}> Change Password </LinkText> diff --git a/packages/components-faraday/src/components/UserProfile/AccountDetailsEdit.js b/packages/components-faraday/src/components/UserProfile/AccountDetailsEdit.js index f247552c6e71b68f61cbda82f37ee768f8fb20ee..a88c42c6e5d004d6642e21faa60c45940dde1505 100644 --- a/packages/components-faraday/src/components/UserProfile/AccountDetailsEdit.js +++ b/packages/components-faraday/src/components/UserProfile/AccountDetailsEdit.js @@ -11,7 +11,7 @@ const AccountDetailsEdit = ({ journal, user, setEditMode, handleSubmit }) => ( <Root onSubmit={handleSubmit}> <EditUserForm journal={journal} title="Edit account details" user={user} /> <Row> - <Button onClick={() => setEditMode(false)}>Cancel</Button> + <Button onClick={setEditMode(false)}>Cancel</Button> <Button primary type="submit"> Save </Button> diff --git a/packages/components-faraday/src/components/UserProfile/EmailNotifications.js b/packages/components-faraday/src/components/UserProfile/EmailNotifications.js index 853c24047d9ff22f56f13d0ffa96eb1ff9044961..025b39f98420336584e02dbbd0e5865204302840 100644 --- a/packages/components-faraday/src/components/UserProfile/EmailNotifications.js +++ b/packages/components-faraday/src/components/UserProfile/EmailNotifications.js @@ -1,9 +1,14 @@ import React from 'react' import styled from 'styled-components' +import { compose, withHandlers } from 'recompose' +import { + withModal, + ConfirmationModal, +} from 'pubsweet-component-modal/src/components' import { Row, RowItem, LabelHeader, LinkText } from '../UIComponents/FormItems' -const EmailNotifications = ({ subscribed = '' }) => ( +const EmailNotifications = ({ subscribed = '', subscribe, unsubscribe }) => ( <Root> <Row noMargin> <RowItem> @@ -13,20 +18,58 @@ const EmailNotifications = ({ subscribed = '' }) => ( {!subscribed ? ( <Row noMargin> <RowItem> - <LinkText>Re-subscribe</LinkText> + <LinkText onClick={subscribe}>Re-subscribe</LinkText> </RowItem> </Row> ) : ( <Row noMargin> <RowItem> - <LinkText>Unsubscribe</LinkText> + <LinkText onClick={unsubscribe}>Unsubscribe</LinkText> </RowItem> </Row> )} </Root> ) -export default EmailNotifications +export default compose( + withModal(props => ({ + modalComponent: ConfirmationModal, + })), + withHandlers({ + subscribe: ({ + userId, + showModal, + hideModal, + changeEmailSubscription, + }) => () => { + showModal({ + title: 'Subscribe to emails', + subtitle: 'Are you sure you want to subscribe to emails?', + onConfirm: () => { + changeEmailSubscription(userId, false) + hideModal() + }, + onCancel: hideModal, + }) + }, + unsubscribe: ({ + userId, + showModal, + hideModal, + changeEmailSubscription, + }) => () => { + showModal({ + title: 'Unsubscribe from emails', + subtitle: 'Are you sure you want to unsubscribe from emails?', + onConfirm: () => { + changeEmailSubscription(userId, true) + hideModal() + }, + onCancel: hideModal, + }) + }, + }), +)(EmailNotifications) // #region styles const Root = styled.div` diff --git a/packages/components-faraday/src/components/UserProfile/Unsubscribe.js b/packages/components-faraday/src/components/UserProfile/Unsubscribe.js new file mode 100644 index 0000000000000000000000000000000000000000..9e1232cdb0ecab224cfd4965a3c4a7718bb7c17c --- /dev/null +++ b/packages/components-faraday/src/components/UserProfile/Unsubscribe.js @@ -0,0 +1,63 @@ +import React from 'react' +import { connect } from 'react-redux' +import { Button } from '@pubsweet/ui' +import styled from 'styled-components' +import { th } from '@pubsweet/ui-toolkit' +import { compose, lifecycle, withState } from 'recompose' + +import { parseSearchParams } from '../utils' +import { changeEmailSubscription } from '../../redux/users' + +const Unsubscribe = ({ message, history }) => ( + <Root> + <Title>{message}</Title> + <Button onClick={() => history.replace('/')} primary> + Go to Dashboard + </Button> + </Root> +) + +const confirmMessage = `You have successfully unsubscribed from emails. To resubscribe go the your profile.` +const errorMessage = `Something went wrong with your account confirmation. Please try again.` + +export default compose( + connect(null, { changeEmailSubscription }), + withState('message', 'setConfirmMessage', 'Loading...'), + lifecycle({ + componentDidMount() { + const { + location, + setConfirmMessage, + changeEmailSubscription, + } = this.props + const { id } = parseSearchParams(location.search) + changeEmailSubscription(id, false) + .then(() => { + setConfirmMessage(confirmMessage) + }) + .catch(() => { + setConfirmMessage(errorMessage) + }) + }, + }), +)(Unsubscribe) + +// #region styled components +const Root = styled.div` + color: ${th('colorText')}; + margin: 0 auto; + text-align: center; + width: 70vw; + + a { + color: ${th('colorText')}; + } +` + +const Title = styled.div` + color: ${th('colorPrimary')}; + font-size: ${th('fontSizeHeading5')}; + font-family: ${th('fontHeading')}; + margin: 10px auto; +` +// #endregion diff --git a/packages/components-faraday/src/components/UserProfile/UserProfilePage.js b/packages/components-faraday/src/components/UserProfile/UserProfilePage.js index 462abc4c48ed0be8bc05cbab2e7a147cc640b537..7e4e7cbd7ca651475ac08983c561ff580f27607c 100644 --- a/packages/components-faraday/src/components/UserProfile/UserProfilePage.js +++ b/packages/components-faraday/src/components/UserProfile/UserProfilePage.js @@ -6,11 +6,12 @@ import styled from 'styled-components' import { selectCurrentUser } from 'xpub-selectors' import { BreadcrumbsHeader } from 'pubsweet-components-faraday/src/components' -import AccountDetails from './AccountDetails' import LinkOrcID from './LinkOrcID' +import AccountDetails from './AccountDetails' import EmailNotifications from './EmailNotifications' +import { changeEmailSubscription } from '../../redux/users' -const UserProfilePage = ({ history, user }) => ( +const UserProfilePage = ({ history, user, changeEmailSubscription }) => ( <Root> <BreadcrumbsHeader history={history} @@ -20,15 +21,22 @@ const UserProfilePage = ({ history, user }) => ( underlined /> <AccountDetails history={history} user={user} /> - <EmailNotifications subscribed={get(user, 'subscription')} /> + <EmailNotifications + changeEmailSubscription={changeEmailSubscription} + subscribed={get(user, 'notifications.email.user')} + userId={get(user, 'id')} + /> <LinkOrcID id={get(user, 'id')} orcid={get(user, 'orcid')} /> </Root> ) export default compose( - connect(state => ({ - user: selectCurrentUser(state), - })), + connect( + state => ({ + user: selectCurrentUser(state), + }), + { changeEmailSubscription }, + ), )(UserProfilePage) // #region styles diff --git a/packages/components-faraday/src/components/UserProfile/index.js b/packages/components-faraday/src/components/UserProfile/index.js new file mode 100644 index 0000000000000000000000000000000000000000..9d8f501405da336f0374b73693d3183c961399c0 --- /dev/null +++ b/packages/components-faraday/src/components/UserProfile/index.js @@ -0,0 +1 @@ +export { default as Unsubscribe } from './Unsubscribe' diff --git a/packages/components-faraday/src/redux/users.js b/packages/components-faraday/src/redux/users.js index a4e6d1a12984cde20989701eadbeddae1728779b..460f254083804ddddadef2baf826d461cd88f0d5 100644 --- a/packages/components-faraday/src/redux/users.js +++ b/packages/components-faraday/src/redux/users.js @@ -1,5 +1,6 @@ import { get } from 'lodash' -import { create } from 'pubsweet-client/src/helpers/api' +import { actions } from 'pubsweet-client' +import { create, update } from 'pubsweet-client/src/helpers/api' const LOGIN_SUCCESS = 'LOGIN_SUCCESS' @@ -20,3 +21,9 @@ export const confirmUser = (userId, confirmationToken) => dispatch => localStorage.setItem('token', user.token) return dispatch(loginSuccess(user)) }) + +export const changeEmailSubscription = (id, subscribe = true) => dispatch => + update(`/users/subscribe`, { + id, + subscribe, + }).then(() => dispatch(actions.getCurrentUser())) diff --git a/packages/xpub-faraday/app/routes.js b/packages/xpub-faraday/app/routes.js index 826ed981b7f01bdab32619dac5de12f1d87e7124..e06874ce684fa21817fa7e679c940b824f5abc92 100644 --- a/packages/xpub-faraday/app/routes.js +++ b/packages/xpub-faraday/app/routes.js @@ -29,6 +29,8 @@ import { SignUpInvitationPage, } from 'pubsweet-components-faraday/src/components/SignUp' +import { Unsubscribe } from 'pubsweet-components-faraday/src/components/UserProfile' + import FaradayApp from './FaradayApp' const PrivateRoute = ({ component: Component, ...rest }) => ( @@ -84,6 +86,7 @@ const Routes = () => ( path="/forgot-password" /> <Route component={ConfirmAccount} exact path="/confirm-signup" /> + <Route component={Unsubscribe} exact path="/unsubscribe" /> <PrivateRoute component={DashboardPage} exact path="/" /> <PrivateRoute component={UserProfilePage} exact path="/profile" /> <PrivateRoute diff --git a/packages/xpub-faraday/config/default.js b/packages/xpub-faraday/config/default.js index 37367502a5d21c4d8aeb99f715a44c9e8ec16614..22d48a7d70e81b486f47841d157e79f137882789 100644 --- a/packages/xpub-faraday/config/default.js +++ b/packages/xpub-faraday/config/default.js @@ -83,6 +83,9 @@ module.exports = { 'confirm-signup': { url: process.env.PUBSWEET_CONFIRM_SIGNUP_URL || '/confirm-signup', }, + unsubscribe: { + url: process.env.PUBSWEET_UNSUBSCRIBE_URL || '/unsubscribe', + }, roles: { global: ['admin', 'editorInChief', 'author', 'handlingEditor'], collection: ['handlingEditor', 'reviewer', 'author'], diff --git a/packages/xpub-faraday/config/validations.js b/packages/xpub-faraday/config/validations.js index 61e603fe92ae1d12ffedecae79d2774207df71ed..c9e88be1e2e28bd24489ebb4e49d45f146184f86 100644 --- a/packages/xpub-faraday/config/validations.js +++ b/packages/xpub-faraday/config/validations.js @@ -131,6 +131,12 @@ module.exports = { confirmationToken: Joi.string().allow(''), agreeTC: Joi.boolean(), isActive: Joi.boolean().default(true), + notifications: Joi.object({ + email: Joi.object({ + system: Joi.boolean().default(true), + user: Joi.boolean().default(true), + }), + }), }, team: { group: Joi.string(),