diff --git a/services/web/app/src/Features/Subscription/RecurlyWrapper.js b/services/web/app/src/Features/Subscription/RecurlyWrapper.js
index eac574ca0d..8fa219b963 100644
--- a/services/web/app/src/Features/Subscription/RecurlyWrapper.js
+++ b/services/web/app/src/Features/Subscription/RecurlyWrapper.js
@@ -574,6 +574,27 @@ module.exports = RecurlyWrapper = {
)
},
+ updateAccountEmailAddress(accountId, newEmail, callback) {
+ const data = {
+ email: newEmail
+ }
+ const requestBody = RecurlyWrapper._buildXml('account', data)
+
+ RecurlyWrapper.apiRequest(
+ {
+ url: `accounts/${accountId}`,
+ method: 'PUT',
+ body: requestBody
+ },
+ (error, response, body) => {
+ if (error != null) {
+ return callback(error)
+ }
+ RecurlyWrapper._parseAccountXml(body, callback)
+ }
+ )
+ },
+
getAccountActiveCoupons(accountId, callback) {
return RecurlyWrapper.apiRequest(
{
diff --git a/services/web/app/src/Features/Subscription/SubscriptionController.js b/services/web/app/src/Features/Subscription/SubscriptionController.js
index 149f1af274..ff6e5dee0e 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionController.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionController.js
@@ -326,6 +326,18 @@ module.exports = SubscriptionController = {
)
},
+ updateAccountEmailAddress(req, res, next) {
+ const user = AuthenticationController.getSessionUser(req)
+ RecurlyWrapper.updateAccountEmailAddress(user._id, user.email, function(
+ error
+ ) {
+ if (error) {
+ return next(new HttpErrors.InternalServerError({}).withCause(error))
+ }
+ res.sendStatus(200)
+ })
+ },
+
reactivateSubscription(req, res, next) {
const user = AuthenticationController.getSessionUser(req)
logger.log({ user_id: user._id }, 'reactivating subscription')
diff --git a/services/web/app/src/Features/Subscription/SubscriptionRouter.js b/services/web/app/src/Features/Subscription/SubscriptionRouter.js
index 7ccf8b3d07..f4ea226071 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionRouter.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionRouter.js
@@ -127,6 +127,12 @@ module.exports = {
SubscriptionController.processUpgradeToAnnualPlan
)
+ webRouter.post(
+ '/user/subscription/account/email',
+ AuthenticationController.requireLogin(),
+ SubscriptionController.updateAccountEmailAddress
+ )
+
// Currently used in acceptance tests only, as a way to trigger the syncing logic
return publicApiRouter.post(
'/user/:user_id/features/sync',
diff --git a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
index 5d0496100a..77741ca458 100644
--- a/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
+++ b/services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js
@@ -223,7 +223,8 @@ module.exports = {
: undefined
),
trial_ends_at: recurlySubscription.trial_ends_at,
- activeCoupons: recurlyCoupons
+ activeCoupons: recurlyCoupons,
+ account: recurlySubscription.account
}
}
diff --git a/services/web/app/views/subscriptions/dashboard/_personal_subscription.pug b/services/web/app/views/subscriptions/dashboard/_personal_subscription.pug
index 487e3184a9..e0a7501d92 100644
--- a/services/web/app/views/subscriptions/dashboard/_personal_subscription.pug
+++ b/services/web/app/views/subscriptions/dashboard/_personal_subscription.pug
@@ -1,6 +1,7 @@
if (personalSubscription.recurly)
include ./_personal_subscription_recurly
+ include ./_personal_subscription_recurly_sync_email
else
include ./_personal_subscription_custom
-hr
\ No newline at end of file
+hr
diff --git a/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly_sync_email.pug b/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly_sync_email.pug
new file mode 100644
index 0000000000..2a69d2c1ec
--- /dev/null
+++ b/services/web/app/views/subscriptions/dashboard/_personal_subscription_recurly_sync_email.pug
@@ -0,0 +1,18 @@
+-if (user.email !== personalSubscription.recurly.account.email)
+ div
+ hr
+ form(async-form="updateAccountEmailAddress", name="updateAccountEmailAddress", action='/user/subscription/account/email', method="POST")
+ input(name='_csrf', type='hidden', value=csrfToken)
+ .form-group
+ form-messages(for="updateAccountEmailAddress")
+ .alert.alert-success(ng-show="updateAccountEmailAddress.response.success")
+ | #{translate('recurly_email_updated')}
+ div(ng-hide="updateAccountEmailAddress.response.success")
+ p !{translate("recurly_email_update_needed", { recurlyEmail: "" + personalSubscription.recurly.account.email + "", userEmail: "" + user.email + "" })}
+ .actions
+ button.btn-primary.btn(
+ type='submit',
+ ng-disabled="updateAccountEmailAddress.inflight"
+ )
+ span(ng-show="!updateAccountEmailAddress.inflight") #{translate("update")}
+ span(ng-show="updateAccountEmailAddress.inflight") #{translate("updating")}...
diff --git a/services/web/test/acceptance/src/RecurlySubscriptionUpdateTests.js b/services/web/test/acceptance/src/RecurlySubscriptionUpdateTests.js
new file mode 100644
index 0000000000..eb3b4e2016
--- /dev/null
+++ b/services/web/test/acceptance/src/RecurlySubscriptionUpdateTests.js
@@ -0,0 +1,43 @@
+const { expect } = require('chai')
+const async = require('async')
+const User = require('./helpers/User')
+const RecurlySubscription = require('./helpers/RecurlySubscription')
+require('./helpers/MockV1Api')
+
+describe('Subscriptions', function() {
+ describe('update', function() {
+ beforeEach(function(done) {
+ this.recurlyUser = new User()
+ async.series(
+ [
+ cb => this.recurlyUser.ensureUserExists(cb),
+ cb => {
+ this.recurlySubscription = new RecurlySubscription({
+ adminId: this.recurlyUser._id,
+ account: {
+ email: 'stale-recurly@email.com'
+ }
+ })
+ this.recurlySubscription.ensureExists(cb)
+ },
+ cb => this.recurlyUser.login(cb)
+ ],
+ done
+ )
+ })
+
+ it('updates the email address for the account', function(done) {
+ let url = '/user/subscription/account/email'
+
+ this.recurlyUser.request.post({ url }, (error, { statusCode }) => {
+ if (error) {
+ return done(error)
+ }
+ expect(statusCode).to.equal(200)
+ // the actual email update is not tested as the mocked Recurly API
+ // doesn't handle it
+ done()
+ })
+ })
+ })
+})
diff --git a/services/web/test/acceptance/src/SubscriptionDashboardTests.js b/services/web/test/acceptance/src/SubscriptionDashboardTests.js
index 84476d4cde..9fae81e9f2 100644
--- a/services/web/test/acceptance/src/SubscriptionDashboardTests.js
+++ b/services/web/test/acceptance/src/SubscriptionDashboardTests.js
@@ -62,7 +62,8 @@ describe('Subscriptions', function() {
describe('when the user has a subscription with recurly', function() {
beforeEach(function(done) {
MockRecurlyApi.accounts['mock-account-id'] = this.accounts = {
- hosted_login_token: 'mock-login-token'
+ hosted_login_token: 'mock-login-token',
+ email: 'mock@email.com'
}
MockRecurlyApi.subscriptions[
'mock-subscription-id'
@@ -138,7 +139,12 @@ describe('Subscriptions', function() {
tax: 100,
taxRate: 0.2,
trial_ends_at: new Date(2018, 6, 7),
- trialEndsAtFormatted: '7th July 2018'
+ trialEndsAtFormatted: '7th July 2018',
+ account: {
+ account_code: 'mock-account-id',
+ email: 'mock@email.com',
+ hosted_login_token: 'mock-login-token'
+ }
})
})
@@ -176,6 +182,12 @@ describe('Subscriptions', function() {
}
)
})
+
+ it('should return Recurly account email', function() {
+ expect(this.data.personalSubscription.recurly.account.email).to.equal(
+ 'mock@email.com'
+ )
+ })
})
describe('when the user has a subscription without recurly', function() {
diff --git a/services/web/test/acceptance/src/helpers/MockRecurlyApi.js b/services/web/test/acceptance/src/helpers/MockRecurlyApi.js
index 9a67d13e22..723ecb626a 100644
--- a/services/web/test/acceptance/src/helpers/MockRecurlyApi.js
+++ b/services/web/test/acceptance/src/helpers/MockRecurlyApi.js
@@ -14,6 +14,7 @@ let MockRecurlyApi
const express = require('express')
const app = express()
const bodyParser = require('body-parser')
+const SubscriptionController = require('../../../../app/src/Features/Subscription/SubscriptionController')
app.use(bodyParser.json())
@@ -69,11 +70,30 @@ module.exports = MockRecurlyApi = {
${req.params.id}
${account.hosted_login_token}
+ ${account.email}
\
`)
}
})
+ app.put(
+ '/accounts/:id',
+ SubscriptionController.recurlyNotificationParser, // required to parse XML requests
+ (req, res, next) => {
+ const account = this.accounts[req.params.id]
+ if (account == null) {
+ return res.status(404).end()
+ } else {
+ return res.send(`\
+
+ ${req.params.id}
+ ${account.email}
+\
+`)
+ }
+ }
+ )
+
app.get('/coupons/:code', (req, res, next) => {
const coupon = this.coupons[req.params.code]
if (coupon == null) {
diff --git a/services/web/test/acceptance/src/helpers/RecurlySubscription.js b/services/web/test/acceptance/src/helpers/RecurlySubscription.js
index f0df698a20..dc33509e08 100644
--- a/services/web/test/acceptance/src/helpers/RecurlySubscription.js
+++ b/services/web/test/acceptance/src/helpers/RecurlySubscription.js
@@ -10,6 +10,9 @@ class RecurlySubscription {
this.uuid = ObjectId().toString()
this.accountId = this.subscription.admin_id.toString()
this.state = options.state || 'active'
+ this.account = {
+ email: options.account && options.account.email
+ }
}
ensureExists(callback) {
@@ -22,7 +25,10 @@ class RecurlySubscription {
account_id: this.accountId,
state: this.state
})
- MockRecurlyApi.addAccount({ id: this.accountId })
+ MockRecurlyApi.addAccount({
+ id: this.accountId,
+ email: this.account.email
+ })
callback()
})
}
diff --git a/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js b/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js
index 6b3265dd38..25651dd654 100644
--- a/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js
+++ b/services/web/test/unit/src/Subscription/RecurlyWrapperTests.js
@@ -257,6 +257,49 @@ describe('RecurlyWrapper', function() {
})
})
+ describe('updateAccountEmailAddress', function() {
+ beforeEach(function(done) {
+ this.recurlyAccountId = 'account-id-123'
+ this.newEmail = 'example@overleaf.com'
+ this.apiRequest = sinon
+ .stub(this.RecurlyWrapper, 'apiRequest')
+ .callsFake((options, callback) => {
+ this.requestOptions = options
+ callback(null, {}, fixtures['accounts/104'])
+ })
+
+ this.RecurlyWrapper.updateAccountEmailAddress(
+ this.recurlyAccountId,
+ this.newEmail,
+ (error, recurlyAccount) => {
+ this.recurlyAccount = recurlyAccount
+ done()
+ }
+ )
+ })
+
+ afterEach(function() {
+ return this.RecurlyWrapper.apiRequest.restore()
+ })
+
+ it('sends correct XML', function() {
+ this.apiRequest.called.should.equal(true)
+ const { body } = this.apiRequest.lastCall.args[0]
+ expect(body).to.equal(`\
+
+ example@overleaf.com
+\
+`)
+ this.requestOptions.url.should.equal(`accounts/${this.recurlyAccountId}`)
+ this.requestOptions.method.should.equal('PUT')
+ })
+
+ it('should return the updated account', function() {
+ should.exist(this.recurlyAccount)
+ this.recurlyAccount.account_code.should.equal('104')
+ })
+ })
+
describe('updateSubscription', function() {
beforeEach(function(done) {
this.recurlySubscriptionId = 'subscription-id-123'
diff --git a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js
index de22cfb2a8..a768655559 100644
--- a/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js
+++ b/services/web/test/unit/src/Subscription/SubscriptionControllerTests.js
@@ -115,7 +115,9 @@ describe('SubscriptionController', function() {
},
'settings-sharelatex': this.settings,
'../User/UserGetter': this.UserGetter,
- './RecurlyWrapper': (this.RecurlyWrapper = {}),
+ './RecurlyWrapper': (this.RecurlyWrapper = {
+ updateAccountEmailAddress: sinon.stub().yields()
+ }),
'./FeaturesUpdater': (this.FeaturesUpdater = {}),
'./GroupPlansData': (this.GroupPlansData = {}),
'./V1SubscriptionManager': (this.V1SubscriptionManager = {}),
@@ -477,6 +479,28 @@ describe('SubscriptionController', function() {
})
})
+ describe('updateAccountEmailAddress via put', function() {
+ beforeEach(function(done) {
+ this.res = {
+ sendStatus() {
+ return done()
+ }
+ }
+ sinon.spy(this.res, 'sendStatus')
+ this.SubscriptionController.updateAccountEmailAddress(this.req, this.res)
+ })
+
+ it('should send the user and subscriptionId to RecurlyWrapper', function() {
+ this.RecurlyWrapper.updateAccountEmailAddress
+ .calledWith(this.user._id, this.user.email)
+ .should.equal(true)
+ })
+
+ it('shouldrespond with 200', function() {
+ this.res.sendStatus.calledWith(200).should.equal(true)
+ })
+ })
+
describe('reactivateSubscription', function() {
beforeEach(function(done) {
this.res = {