mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #7742 from overleaf/ta-email-preferences-page
Create Email Preferences Page GitOrigin-RevId: 371a62e8423e5cbebff83e61bf35a8b3b638c398
This commit is contained in:
parent
b9d060ca34
commit
f0ac0f3e7a
15 changed files with 236 additions and 14 deletions
|
@ -476,7 +476,7 @@ templates.userOnboardingEmail = NoCTAEmailTemplate({
|
|||
)
|
||||
const userSettingsLink = EmailMessageHelper.displayLink(
|
||||
'here',
|
||||
`${settings.siteUrl}/user/settings`,
|
||||
`${settings.siteUrl}/user/email-preferences`,
|
||||
isPlainText
|
||||
)
|
||||
const onboardingSurveyLink = EmailMessageHelper.displayLink(
|
||||
|
|
|
@ -8,6 +8,7 @@ const OError = require('@overleaf/o-error')
|
|||
const provider = getProvider()
|
||||
|
||||
module.exports = {
|
||||
subscribed: callbackify(provider.subscribed),
|
||||
subscribe: callbackify(provider.subscribe),
|
||||
unsubscribe: callbackify(provider.unsubscribe),
|
||||
changeEmail: callbackify(provider.changeEmail),
|
||||
|
@ -39,11 +40,27 @@ function makeMailchimpProvider() {
|
|||
const MAILCHIMP_LIST_ID = Settings.mailchimp.list_id
|
||||
|
||||
return {
|
||||
subscribed,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
changeEmail,
|
||||
}
|
||||
|
||||
async function subscribed(user) {
|
||||
try {
|
||||
const path = getSubscriberPath(user.email)
|
||||
const result = await mailchimp.get(path)
|
||||
return result?.status === 'subscribed'
|
||||
} catch (err) {
|
||||
if (err.status === 404) {
|
||||
return false
|
||||
}
|
||||
throw OError.tag(err, 'error getting newsletter subscriptions status', {
|
||||
userId: user._id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async function subscribe(user) {
|
||||
try {
|
||||
const path = getSubscriberPath(user.email)
|
||||
|
@ -194,11 +211,20 @@ function makeMailchimpProvider() {
|
|||
|
||||
function makeNullProvider() {
|
||||
return {
|
||||
subscribed,
|
||||
subscribe,
|
||||
unsubscribe,
|
||||
changeEmail,
|
||||
}
|
||||
|
||||
async function subscribed(user) {
|
||||
logger.info(
|
||||
{ user },
|
||||
'Not checking user because no newsletter provider is configured'
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
async function subscribe(user) {
|
||||
logger.info(
|
||||
{ user },
|
||||
|
|
|
@ -98,7 +98,7 @@ EmailBuilder.templates.trialOnboarding = EmailBuilder.NoCTAEmailTemplate({
|
|||
|
||||
const unsubscribe = EmailMessageHelper.displayLink(
|
||||
'here',
|
||||
`${settings.siteUrl}/user/settings`,
|
||||
`${settings.siteUrl}/user/email-preferences`,
|
||||
isPlainText
|
||||
)
|
||||
|
||||
|
|
|
@ -261,6 +261,24 @@ const UserController = {
|
|||
)
|
||||
},
|
||||
|
||||
subscribe(req, res, next) {
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
UserGetter.getUser(userId, (err, user) => {
|
||||
if (err != null) {
|
||||
return next(err)
|
||||
}
|
||||
NewsletterManager.subscribe(user, err => {
|
||||
if (err != null) {
|
||||
OError.tag(err, 'error subscribing to newsletter')
|
||||
return next(err)
|
||||
}
|
||||
return res.json({
|
||||
message: req.i18n.translate('thanks_settings_updated'),
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
unsubscribe(req, res, next) {
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
UserGetter.getUser(userId, (err, user) => {
|
||||
|
@ -269,12 +287,12 @@ const UserController = {
|
|||
}
|
||||
NewsletterManager.unsubscribe(user, err => {
|
||||
if (err != null) {
|
||||
logger.warn(
|
||||
{ err, user },
|
||||
'Failed to unsubscribe user from newsletter'
|
||||
)
|
||||
OError.tag(err, 'error unsubscribing to newsletter')
|
||||
return next(err)
|
||||
}
|
||||
res.sendStatus(200)
|
||||
return res.json({
|
||||
message: req.i18n.translate('thanks_settings_updated'),
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
|
|
@ -5,6 +5,7 @@ const logger = require('@overleaf/logger')
|
|||
const Settings = require('@overleaf/settings')
|
||||
const AuthenticationController = require('../Authentication/AuthenticationController')
|
||||
const SessionManager = require('../Authentication/SessionManager')
|
||||
const NewsletterManager = require('../Newsletter/NewsletterManager')
|
||||
const _ = require('lodash')
|
||||
const { expressify } = require('../../util/promises')
|
||||
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
|
||||
|
@ -211,6 +212,25 @@ const UserPagesController = {
|
|||
)
|
||||
},
|
||||
|
||||
emailPreferencesPage(req, res, next) {
|
||||
const userId = SessionManager.getLoggedInUserId(req.session)
|
||||
UserGetter.getUser(userId, (err, user) => {
|
||||
if (err != null) {
|
||||
return next(err)
|
||||
}
|
||||
NewsletterManager.subscribed(user, (err, subscribed) => {
|
||||
if (err != null) {
|
||||
OError.tag(err, 'error getting newsletter subscription status')
|
||||
return next(err)
|
||||
}
|
||||
res.render('user/email-preferences', {
|
||||
title: 'newsletter_info_title',
|
||||
subscribed,
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
_restructureThirdPartyIds(user) {
|
||||
// 3rd party identifiers are an array of objects
|
||||
// this turn them into a single object, which
|
||||
|
|
|
@ -264,11 +264,31 @@ function initialize(webRouter, privateApiRouter, publicApiRouter) {
|
|||
UserController.clearSessions
|
||||
)
|
||||
|
||||
// deprecated
|
||||
webRouter.delete(
|
||||
'/user/newsletter/unsubscribe',
|
||||
AuthenticationController.requireLogin(),
|
||||
UserController.unsubscribe
|
||||
)
|
||||
|
||||
webRouter.post(
|
||||
'/user/newsletter/unsubscribe',
|
||||
AuthenticationController.requireLogin(),
|
||||
UserController.unsubscribe
|
||||
)
|
||||
|
||||
webRouter.post(
|
||||
'/user/newsletter/subscribe',
|
||||
AuthenticationController.requireLogin(),
|
||||
UserController.subscribe
|
||||
)
|
||||
|
||||
webRouter.get(
|
||||
'/user/email-preferences',
|
||||
AuthenticationController.requireLogin(),
|
||||
UserPagesController.emailPreferencesPage
|
||||
)
|
||||
|
||||
webRouter.post(
|
||||
'/user/delete',
|
||||
RateLimiterMiddleware.rateLimit({
|
||||
|
|
|
@ -6,7 +6,7 @@ block content
|
|||
.row
|
||||
.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
|
||||
.card
|
||||
.page-header.text-centered
|
||||
.page-header
|
||||
h1
|
||||
| #{translate("sharelatex_beta_program")}
|
||||
.beta-opt-in
|
||||
|
@ -49,8 +49,6 @@ block content
|
|||
)
|
||||
span(data-ol-inflight="idle") #{translate("beta_program_opt_out_action")}
|
||||
span(hidden data-ol-inflight="pending") #{translate("processing")}…
|
||||
.form-group
|
||||
a(href="/project").btn.btn-link.btn-sm #{translate("back_to_your_projects")}
|
||||
else
|
||||
form(
|
||||
data-ol-regular-form
|
||||
|
@ -65,5 +63,7 @@ block content
|
|||
)
|
||||
span(data-ol-inflight="idle") #{translate("beta_program_opt_in_action")}
|
||||
span(hidden data-ol-inflight="pending") #{translate("joining")}…
|
||||
.form-group
|
||||
a(href="/project").btn.btn-link.btn-sm #{translate("back_to_your_projects")}
|
||||
.page-separator
|
||||
a.btn.btn-default.text-capitalize(href='/user/settings') #{translate('back_to_account_settings')}
|
||||
|
|
||||
a.btn.btn-default.text-capitalize(href='/project') #{translate('back_to_your_projects')}
|
||||
|
|
47
services/web/app/views/user/email-preferences.pug
Normal file
47
services/web/app/views/user/email-preferences.pug
Normal file
|
@ -0,0 +1,47 @@
|
|||
extends ../layout-marketing
|
||||
|
||||
block content
|
||||
main.content.content-alt#main-content
|
||||
.container
|
||||
.row
|
||||
.col-md-10.col-md-offset-1.col-lg-8.col-lg-offset-2
|
||||
.card
|
||||
.page-header
|
||||
h1 #{translate("newsletter_info_title")}
|
||||
|
||||
p #{translate("newsletter_info_summary")}
|
||||
|
||||
- var submitAction
|
||||
if subscribed
|
||||
- submitAction = '/user/newsletter/unsubscribe'
|
||||
p !{translate("newsletter_info_subscribed", {}, ['strong'])}
|
||||
else
|
||||
- submitAction = '/user/newsletter/subscribe'
|
||||
p !{translate("newsletter_info_unsubscribed", {}, ['strong'])}
|
||||
|
||||
form(
|
||||
data-ol-async-form
|
||||
data-ol-reload-on-success
|
||||
name="newsletterForm"
|
||||
action=submitAction
|
||||
method="POST"
|
||||
)
|
||||
input(name='_csrf', type='hidden', value=csrfToken)
|
||||
+formMessages()
|
||||
p.actions.text-center
|
||||
if subscribed
|
||||
button.btn-danger.btn(type='submit', data-ol-disabled-inflight)
|
||||
span(data-ol-inflight="idle") #{translate("unsubscribe")}
|
||||
span(hidden data-ol-inflight="pending") #{translate("saving")}…
|
||||
else
|
||||
button.btn-primary.btn(type='submit', data-ol-disabled-inflight)
|
||||
span(data-ol-inflight="idle") #{translate("subscribe")}
|
||||
span(hidden data-ol-inflight="pending") #{translate("saving")}…
|
||||
|
||||
if subscribed
|
||||
p #{translate("newsletter_info_note")}
|
||||
|
||||
.page-separator
|
||||
a.btn.btn-default.text-capitalize(href='/user/settings') #{translate('back_to_account_settings')}
|
||||
|
|
||||
a.btn.btn-default.text-capitalize(href='/project') #{translate('back_to_your_projects')}
|
|
@ -67,3 +67,7 @@ block content
|
|||
|
||||
p.text-success.text-center
|
||||
| #{translate('clear_sessions_success')}
|
||||
.page-separator
|
||||
a.btn.btn-default.text-capitalize(href='/user/settings') #{translate('back_to_account_settings')}
|
||||
|
|
||||
a.btn.btn-default.text-capitalize(href='/project') #{translate('back_to_your_projects')}
|
||||
|
|
|
@ -57,6 +57,12 @@ function formSubmitHelper(formEl) {
|
|||
})
|
||||
}
|
||||
|
||||
// Handle reloads
|
||||
if (formEl.hasAttribute('data-ol-reload-on-success')) {
|
||||
window.setTimeout(window.location.reload.bind(window.location), 1000)
|
||||
return
|
||||
}
|
||||
|
||||
// Let the user re-submit the form.
|
||||
formEl.dispatchEvent(new Event('idle'))
|
||||
} catch (error) {
|
||||
|
|
|
@ -211,6 +211,7 @@ cite {
|
|||
// Page header
|
||||
// -------------------------
|
||||
|
||||
.page-separator,
|
||||
.page-header {
|
||||
padding-bottom: ((@line-height-computed / 2) - 1);
|
||||
margin: (@line-height-computed) 0 @line-height-computed;
|
||||
|
|
|
@ -922,6 +922,11 @@
|
|||
"newsletter": "Newsletter",
|
||||
"manage_newsletter": "Manage Your Newsletter Preferences",
|
||||
"newsletter_info_and_unsubscribe": "Every few months we send a newsletter out summarizing the new features available. If you would prefer not to receive this email then you can unsubscribe at any time:",
|
||||
"newsletter_info_title": "Newsletter Preferences",
|
||||
"newsletter_info_summary": "Every few months we send a newsletter out summarizing the new features available.",
|
||||
"newsletter_info_subscribed": "You are currently <0>subscribed</0> to the __appName__ newsletter. If you would prefer not to receive this email then you can unsubscribe at any time.",
|
||||
"newsletter_info_unsubscribed": "You are currently <0>unsubscribed</0> to the __appName__ newsletter.",
|
||||
"newsletter_info_note": "Please note: you will still receive important emails, such as project invites and security notifications (password resets, account linking, etc).",
|
||||
"unsubscribed": "Unsubscribed",
|
||||
"unsubscribing": "Unsubscribing",
|
||||
"unsubscribe": "Unsubscribe",
|
||||
|
@ -1145,6 +1150,7 @@
|
|||
"add_your_first_group_member_now": "Add your first group members now",
|
||||
"thanks_for_subscribing_you_help_sl": "Thank you for subscribing to the __planName__ plan. It’s support from people like yourself that allows __appName__ to continue to grow and improve.",
|
||||
"back_to_your_projects": "Back to your projects",
|
||||
"back_to_account_settings": "Back to account settings",
|
||||
"goes_straight_to_our_inboxes": "It goes straight to both our inboxes",
|
||||
"need_anything_contact_us_at": "If there is anything you ever need please feel free to contact us directly at",
|
||||
"regards": "Regards",
|
||||
|
|
|
@ -13,6 +13,7 @@ describe('NewsletterManager', function () {
|
|||
},
|
||||
}
|
||||
this.mailchimp = {
|
||||
get: sinon.stub(),
|
||||
put: sinon.stub(),
|
||||
patch: sinon.stub(),
|
||||
delete: sinon.stub(),
|
||||
|
@ -42,6 +43,30 @@ describe('NewsletterManager', function () {
|
|||
this.emailHash = 'c02f60ed0ef51818186274e406c9a48f'
|
||||
})
|
||||
|
||||
describe('subscribed', function () {
|
||||
it('calls Mailchimp to get the user status', async function () {
|
||||
await this.NewsletterManager.subscribed(this.user)
|
||||
expect(this.mailchimp.get).to.have.been.calledWith(
|
||||
`/lists/list_id/members/${this.emailHash}`
|
||||
)
|
||||
})
|
||||
|
||||
it('returns true when subscribed', async function () {
|
||||
this.mailchimp.get.resolves({ status: 'subscribed' })
|
||||
|
||||
const subscribed = await this.NewsletterManager.subscribed(this.user)
|
||||
expect(subscribed).to.be.true
|
||||
})
|
||||
|
||||
it('returns false on 404', async function () {
|
||||
const err = new Error()
|
||||
err.status = 404
|
||||
this.mailchimp.get.rejects(err)
|
||||
const subscribed = await this.NewsletterManager.subscribed(this.user)
|
||||
expect(subscribed).to.be.false
|
||||
})
|
||||
})
|
||||
|
||||
describe('subscribe', function () {
|
||||
it('calls Mailchimp to subscribe the user', async function () {
|
||||
await this.NewsletterManager.subscribe(this.user)
|
||||
|
|
|
@ -42,7 +42,10 @@ describe('UserController', function () {
|
|||
promises: { getUser: sinon.stub().resolves(this.user) },
|
||||
}
|
||||
this.User = { findById: sinon.stub().callsArgWith(1, null, this.user) }
|
||||
this.NewsLetterManager = { unsubscribe: sinon.stub().callsArgWith(1) }
|
||||
this.NewsLetterManager = {
|
||||
subscribe: sinon.stub().yields(),
|
||||
unsubscribe: sinon.stub().yields(),
|
||||
}
|
||||
this.AuthenticationController = {
|
||||
establishUserSession: sinon.stub().callsArg(2),
|
||||
}
|
||||
|
@ -288,9 +291,23 @@ describe('UserController', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('subscribe', function () {
|
||||
it('should send the user to subscribe', function (done) {
|
||||
this.res.json = data => {
|
||||
expect(data.message).to.equal('thanks_settings_updated')
|
||||
this.NewsLetterManager.subscribe
|
||||
.calledWith(this.user)
|
||||
.should.equal(true)
|
||||
done()
|
||||
}
|
||||
this.UserController.subscribe(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('unsubscribe', function () {
|
||||
it('should send the user to unsubscribe', function (done) {
|
||||
this.res.sendStatus = () => {
|
||||
this.res.json = data => {
|
||||
expect(data.message).to.equal('thanks_settings_updated')
|
||||
this.NewsLetterManager.unsubscribe
|
||||
.calledWith(this.user)
|
||||
.should.equal(true)
|
||||
|
|
|
@ -60,6 +60,9 @@ describe('UserPagesController', function () {
|
|||
getLoggedInUserId: sinon.stub().returns(this.user._id),
|
||||
getSessionUser: sinon.stub().returns(this.user),
|
||||
}
|
||||
this.NewsletterManager = {
|
||||
subscribed: sinon.stub().yields(),
|
||||
}
|
||||
this.AuthenticationController = {
|
||||
_getRedirectFromSession: sinon.stub(),
|
||||
setRedirectInSession: sinon.stub(),
|
||||
|
@ -74,6 +77,7 @@ describe('UserPagesController', function () {
|
|||
'@overleaf/settings': this.settings,
|
||||
'./UserGetter': this.UserGetter,
|
||||
'./UserSessionsManager': this.UserSessionsManager,
|
||||
'../Newsletter/NewsletterManager': this.NewsletterManager,
|
||||
'../Errors/ErrorController': this.ErrorController,
|
||||
'../Authentication/AuthenticationController':
|
||||
this.AuthenticationController,
|
||||
|
@ -225,6 +229,34 @@ describe('UserPagesController', function () {
|
|||
})
|
||||
})
|
||||
|
||||
describe('emailPreferencesPage', function () {
|
||||
beforeEach(function () {
|
||||
this.UserGetter.getUser = sinon.stub().yields(null, this.user)
|
||||
})
|
||||
|
||||
it('render page with subscribed status', function (done) {
|
||||
this.NewsletterManager.subscribed.yields(null, true)
|
||||
this.res.render = function (page, data) {
|
||||
page.should.equal('user/email-preferences')
|
||||
data.title.should.equal('newsletter_info_title')
|
||||
data.subscribed.should.equal(true)
|
||||
return done()
|
||||
}
|
||||
return this.UserPagesController.emailPreferencesPage(this.req, this.res)
|
||||
})
|
||||
|
||||
it('render page with unsubscribed status', function (done) {
|
||||
this.NewsletterManager.subscribed.yields(null, false)
|
||||
this.res.render = function (page, data) {
|
||||
page.should.equal('user/email-preferences')
|
||||
data.title.should.equal('newsletter_info_title')
|
||||
data.subscribed.should.equal(false)
|
||||
return done()
|
||||
}
|
||||
return this.UserPagesController.emailPreferencesPage(this.req, this.res)
|
||||
})
|
||||
})
|
||||
|
||||
describe('settingsPage', function () {
|
||||
beforeEach(function () {
|
||||
this.request.get = sinon
|
||||
|
|
Loading…
Reference in a new issue