diff --git a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
index f669d85de4..bc7eb90c3f 100644
--- a/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
+++ b/services/web/app/coffee/Features/Collaborators/CollaboratorsEmailHandler.coffee
@@ -11,27 +11,6 @@ module.exports = CollaboratorsEmailHandler =
"user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
].join("&")
- notifyUserOfProjectShare: (project_id, email, callback)->
- Project
- .findOne(_id: project_id )
- .select("name owner_ref")
- .populate('owner_ref')
- .exec (err, project)->
- emailOptions =
- to: email
- replyTo: project.owner_ref.email
- project:
- name: project.name
- url: "#{Settings.siteUrl}/project/#{project._id}?" + [
- "project_name=#{encodeURIComponent(project.name)}"
- "user_first_name=#{encodeURIComponent(project.owner_ref.first_name)}"
- "new_email=#{encodeURIComponent(email)}"
- "r=#{project.owner_ref.referal_id}" # Referal
- "rs=ci" # referral source = collaborator invite
- ].join("&")
- owner: project.owner_ref
- EmailHandler.sendEmail "projectSharedWithYou", emailOptions, callback
-
notifyUserOfProjectInvite: (project_id, email, invite, callback)->
Project
.findOne(_id: project_id )
diff --git a/services/web/app/coffee/Features/Email/EmailBuilder.coffee b/services/web/app/coffee/Features/Email/EmailBuilder.coffee
index 5ceece048e..70d11e219b 100644
--- a/services/web/app/coffee/Features/Email/EmailBuilder.coffee
+++ b/services/web/app/coffee/Features/Email/EmailBuilder.coffee
@@ -7,10 +7,18 @@ settings = require("settings-sharelatex")
templates = {}
+
templates.registered =
subject: _.template "Activate your #{settings.appName} Account"
layout: PersonalEmailLayout
type: "notification"
+ plainTextTemplate: _.template """
+Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".
+
+Click here to set your password and log in: <%= setNewPasswordUrl %>
+
+If you have any questions or problems, please contact #{settings.adminEmail}
+"""
compiledTemplate: _.template """
Congratulations, you've just had an account created for you on #{settings.appName} with the email address "<%= to %>".
@@ -19,10 +27,24 @@ templates.registered =
If you have any questions or problems, please contact #{settings.adminEmail}.
"""
+
templates.canceledSubscription =
subject: _.template "ShareLaTeX thoughts"
layout: PersonalEmailLayout
type:"lifecycle"
+ plainTextTemplate: _.template """
+Hi <%= first_name %>,
+
+I'm sorry to see you cancelled your ShareLaTeX premium account. Would you mind giving me some advice on what the site is lacking at the moment via this survey?:
+
+https://sharelatex.typeform.com/to/f5lBiZ
+
+Thank you in advance.
+
+Henry
+
+ShareLaTeX Co-founder
+"""
compiledTemplate: _.template '''
Hi <%= first_name %>,
@@ -36,10 +58,26 @@ ShareLaTeX Co-founder
'''
+
templates.passwordResetRequested =
subject: _.template "Password Reset - #{settings.appName}"
layout: NotificationEmailLayout
type:"notification"
+ plainTextTemplate: _.template """
+Password Reset
+
+We got a request to reset your #{settings.appName} password.
+
+Click this link to reset your password: <%= setNewPasswordUrl %>
+
+If you ignore this message, your password won't be changed.
+
+If you didn't request a password reset, let us know.
+
+Thank you
+
+#{settings.appName} - <%= siteUrl %>
+"""
compiledTemplate: _.template """
Password Reset
@@ -66,26 +104,6 @@ If you didn't request a password reset, let us know.
#{settings.appName}
"""
-templates.projectSharedWithYou =
- subject: _.template "<%= owner.email %> wants to share <%= project.name %> with you"
- layout: NotificationEmailLayout
- type:"notification"
- compiledTemplate: _.template """
-Hi, <%= owner.email %> wants to share '<%= project.name %>' with you
-
-
-
- Thank you
- #{settings.appName}
-"""
templates.projectInvite =
subject: _.template "<%= project.name %> - shared by <%= owner.email %>"
@@ -113,10 +131,20 @@ Thank you
#{settings.appName}
"""
+
templates.completeJoinGroupAccount =
subject: _.template "Verify Email to join <%= group_name %> group"
layout: NotificationEmailLayout
type:"notification"
+ plainTextTemplate: _.template """
+Hi, please verify your email to join the <%= group_name %> and get your free premium account
+
+Click this link to verify now: <%= completeJoinUrl %>
+
+Thank You
+
+#{settings.appName} - <%= siteUrl %>
+"""
compiledTemplate: _.template """
Hi, please verify your email to join the <%= group_name %> and get your free premium account
@@ -134,6 +162,7 @@ templates.completeJoinGroupAccount =
#{settings.appName}
"""
+
module.exports =
templates: templates
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
index 9b812d8d17..51b3db8f56 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionController.coffee
@@ -8,6 +8,7 @@ Settings = require 'settings-sharelatex'
logger = require('logger-sharelatex')
GeoIpLookup = require("../../infrastructure/GeoIpLookup")
SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
+UserGetter = require "../User/UserGetter"
module.exports = SubscriptionController =
@@ -21,14 +22,25 @@ module.exports = SubscriptionController =
if req.query.v?
viewName = "#{viewName}_#{req.query.v}"
logger.log viewName:viewName, "showing plans page"
+ currentUser = null
GeoIpLookup.getCurrencyCode req.query?.ip || req.ip, (err, recomendedCurrency)->
return next(err) if err?
- res.render viewName,
- title: "plans_and_pricing"
- plans: plans
- baseUrl: baseUrl
- gaExperiments: Settings.gaExperiments.plansPage
- recomendedCurrency:recomendedCurrency
+ render = () ->
+ res.render viewName,
+ title: "plans_and_pricing"
+ plans: plans
+ baseUrl: baseUrl
+ gaExperiments: Settings.gaExperiments.plansPage
+ recomendedCurrency:recomendedCurrency
+ shouldABTestPlans: currentUser == null or (currentUser?.signUpDate? and currentUser.signUpDate >= (new Date('2016-10-27')))
+ user_id = AuthenticationController.getLoggedInUserId(req)
+ if user_id?
+ UserGetter.getUser user_id, {signUpDate: 1}, (err, user) ->
+ return next(err) if err?
+ currentUser = user
+ render()
+ else
+ render()
#get to show the recurly.js page
paymentPage: (req, res, next) ->
diff --git a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
index 44c31c8d60..8e2fc2032e 100644
--- a/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
+++ b/services/web/app/coffee/Features/Subscription/SubscriptionViewModelBuilder.coffee
@@ -44,7 +44,7 @@ module.exports =
allPlans = {}
plans.forEach (plan)->
allPlans[plan.planCode] = plan
-
+
result =
allPlans: allPlans
@@ -54,7 +54,7 @@ module.exports =
result.studentAccounts = _.filter plans, (plan)->
plan.planCode.indexOf("student") != -1
-
+
result.groupMonthlyPlans = _.filter plans, (plan)->
plan.groupPlan and !plan.annual
@@ -68,4 +68,3 @@ module.exports =
!plan.groupPlan and plan.annual and plan.planCode.indexOf("student") == -1
return result
-
diff --git a/services/web/app/coffee/Features/User/UserController.coffee b/services/web/app/coffee/Features/User/UserController.coffee
index 389de1a0f2..47c093d590 100644
--- a/services/web/app/coffee/Features/User/UserController.coffee
+++ b/services/web/app/coffee/Features/User/UserController.coffee
@@ -15,12 +15,26 @@ settings = require "settings-sharelatex"
module.exports = UserController =
- deleteUser: (req, res)->
+ tryDeleteUser: (req, res, next) ->
user_id = AuthenticationController.getLoggedInUserId(req)
- UserDeleter.deleteUser user_id, (err)->
- if !err?
+ password = req.body.password
+ logger.log {user_id}, "trying to delete user account"
+ if !password? or password == ''
+ logger.err {user_id}, 'no password supplied for attempt to delete account'
+ return res.sendStatus(403)
+ AuthenticationManager.authenticate {_id: user_id}, password, (err, user) ->
+ if err?
+ logger.err {user_id}, 'error authenticating during attempt to delete account'
+ return next(err)
+ if !user
+ logger.err {user_id}, 'auth failed during attempt to delete account'
+ return res.sendStatus(403)
+ UserDeleter.deleteUser user_id, (err) ->
+ if err?
+ logger.err {user_id}, "error while deleting user account"
+ return next(err)
req.session?.destroy()
- res.sendStatus(200)
+ res.sendStatus(200)
unsubscribe: (req, res)->
user_id = AuthenticationController.getLoggedInUserId(req)
@@ -143,7 +157,7 @@ module.exports = UserController =
type:'success'
text:'Your password has been changed'
else
- logger.log user: user, "current password wrong"
+ logger.log user_id: user_id, "current password wrong"
res.send
message:
type:'error'
diff --git a/services/web/app/coffee/infrastructure/LoggerSerializers.coffee b/services/web/app/coffee/infrastructure/LoggerSerializers.coffee
index f496b8cdad..7bd90c3bf5 100644
--- a/services/web/app/coffee/infrastructure/LoggerSerializers.coffee
+++ b/services/web/app/coffee/infrastructure/LoggerSerializers.coffee
@@ -1,5 +1,7 @@
module.exports =
user: (user) ->
+ if !user?
+ return null
if !user._id?
user = {_id : user}
return {
@@ -10,6 +12,8 @@ module.exports =
}
project: (project) ->
+ if !project?
+ return null
if !project._id?
project = {_id: project}
return {
diff --git a/services/web/app/coffee/router.coffee b/services/web/app/coffee/router.coffee
index 56dd8d821b..a027f6359e 100644
--- a/services/web/app/coffee/router.coffee
+++ b/services/web/app/coffee/router.coffee
@@ -92,7 +92,7 @@ module.exports = class Router
webRouter.post '/user/sessions/clear', AuthenticationController.requireLogin(), UserController.clearSessions
webRouter.delete '/user/newsletter/unsubscribe', AuthenticationController.requireLogin(), UserController.unsubscribe
- webRouter.delete '/user', AuthenticationController.requireLogin(), UserController.deleteUser
+ webRouter.post '/user/delete', AuthenticationController.requireLogin(), UserController.tryDeleteUser
webRouter.get '/user/personal_info', AuthenticationController.requireLogin(), UserInfoController.getLoggedInUsersPersonalInfo
apiRouter.get '/user/:user_id/personal_info', AuthenticationController.httpAuth, UserInfoController.getPersonalInfo
diff --git a/services/web/app/views/layout.jade b/services/web/app/views/layout.jade
index 7f5cfd891f..8f4d1263db 100644
--- a/services/web/app/views/layout.jade
+++ b/services/web/app/views/layout.jade
@@ -51,8 +51,6 @@ html(itemscope, itemtype='http://schema.org/Product')
script(type="text/javascript").
window.csrfToken = "#{csrfToken}";
- block scripts
-
script(src=buildJsPath("libs/jquery-1.11.1.min.js", {fingerprint:false}))
script(type="text/javascript").
var noCdnKey = "nocdn=true"
@@ -61,6 +59,9 @@ html(itemscope, itemtype='http://schema.org/Product')
if (cdnBlocked && !noCdnAlreadyInUrl && navigator.userAgent.indexOf("Googlebot") == -1) {
window.location.search += '&'+noCdnKey;
}
+
+ block scripts
+
script(src=buildJsPath("libs/angular-1.3.15.min.js", {fingerprint:false}))
script.
diff --git a/services/web/app/views/project/editor/history.jade b/services/web/app/views/project/editor/history.jade
index 9cf756c344..3799b22363 100644
--- a/services/web/app/views/project/editor/history.jade
+++ b/services/web/app/views/project/editor/history.jade
@@ -1,41 +1,85 @@
div#history(ng-show="ui.view == 'history'")
span(ng-controller="HistoryPremiumPopup")
- .upgrade-prompt(ng-show="!project.features.versioning")
- .message(ng-show="project.owner._id == user.id")
- p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
- p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
- ul.list-unstyled
- li
- i.fa.fa-check
- | #{translate("unlimited_projects")}
-
- li
- i.fa.fa-check
- | #{translate("collabs_per_proj", {collabcount:'Multiple'})}
-
- li
- i.fa.fa-check
- | #{translate("full_doc_history")}
-
- li
- i.fa.fa-check
- | #{translate("sync_to_dropbox")}
+ .upgrade-prompt(ng-if="project.features.versioning === false && ui.view === 'history'")
+
+ div(sixpack-switch="teaser-history")
+ .message(
+ sixpack-default
+ ng-show="project.owner._id == user.id"
+ )
+ p.text-center: strong #{translate("upgrade_to_get_feature", {feature:"full Project History"})}
+ p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
+ ul.list-unstyled
+ li
+ i.fa.fa-check
+ | #{translate("unlimited_projects")}
+
+ li
+ i.fa.fa-check
+ | #{translate("collabs_per_proj", {collabcount:'Multiple'})}
+
+ li
+ i.fa.fa-check
+ | #{translate("full_doc_history")}
+
+ li
+ i.fa.fa-check
+ | #{translate("sync_to_dropbox")}
- li
- i.fa.fa-check
- | #{translate("sync_to_github")}
+ li
+ i.fa.fa-check
+ | #{translate("sync_to_github")}
- li
- i.fa.fa-check
- |#{translate("compile_larger_projects")}
+ li
+ i.fa.fa-check
+ |#{translate("compile_larger_projects")}
+ p.text-center(ng-controller="FreeTrialModalController")
+ a.btn.btn-success(
+ href
+ ng-class="buttonClass"
+ ng-click="startFreeTrial('history')"
+ sixpack-convert="teaser-history"
+ ) #{translate("start_free_trial")}
- p.text-center(ng-controller="FreeTrialModalController")
- a.btn.btn-success(
- href
- ng-class="buttonClass"
- ng-click="startFreeTrial('history')"
- ) #{translate("start_free_trial")}
+ .message.message-wider(
+ sixpack-when="focused"
+ ng-show="project.owner._id == user.id"
+ )
+ header.message-header
+ h3 History
+ .message-body
+ h4.teaser-title See who changed what. Go back to previous versions.
+ img.teaser-img(
+ src="/img/teasers/history/teaser-history.png"
+ alt="History"
+ )
+ p.text-center.small(ng-show="startedFreeTrial") #{translate("refresh_page_after_starting_free_trial")}
+ .row
+ .col-md-8.col-md-offset-2
+ ul.list-unstyled
+ li
+ i.fa.fa-check
+ | Catch up with your collaborators changes
+
+ li
+ i.fa.fa-check
+ | See changes over any time period
+
+ li
+ i.fa.fa-check
+ | Revert your documents to previous versions
+
+ li
+ i.fa.fa-check
+ | Restore deleted files
+ p.text-center(ng-controller="FreeTrialModalController")
+ a.btn.btn-success(
+ href
+ ng-class="buttonClass"
+ ng-click="startFreeTrial('history')"
+ sixpack-convert="teaser-history"
+ ) Try it for free
.message(ng-show="project.owner._id != user.id")
p #{translate("ask_proj_owner_to_upgrade_for_history")}
diff --git a/services/web/app/views/subscriptions/dashboard.jade b/services/web/app/views/subscriptions/dashboard.jade
index 34493e5a8c..175c8d1e4f 100644
--- a/services/web/app/views/subscriptions/dashboard.jade
+++ b/services/web/app/views/subscriptions/dashboard.jade
@@ -12,8 +12,8 @@ block scripts
mixin printPlan(plan)
-if (!plan.hideFromUsers)
- tr(ng-controller="ChangePlanFormController")
- td(ng-init="plan=#{JSON.stringify(plan)}")
+ tr(ng-controller="ChangePlanFormController", ng-init="plan=#{JSON.stringify(plan)}", ng-show="shouldShowPlan(plan.planCode)")
+ td
strong #{plan.name}
td {{refreshPrice(plan.planCode)}}
-if (plan.annual)
@@ -46,8 +46,8 @@ block content
|
| #{translate("your_billing_details_were_saved")}
.card(ng-if="view == 'overview'")
- .page-header
- h1 #{translate("your_subscription")}
+ .page-header(x-current-plan="#{subscription.planCode}")
+ h1 #{translate("your_subscription")}
- if (subscription && user._id+'' == subscription.admin_id+'')
case subscription.state
@@ -56,7 +56,8 @@ block content
when "active"
p !{translate("currently_subscribed_to_plan", {planName:"" + subscription.name + ""})}
- a(href, ng-click="changePlan = true") !{translate("change_plan")}.
+ span(ng-show="!isNextGenPlan")
+ a(href, ng-click="changePlan = true") !{translate("change_plan")}.
p !{translate("next_payment_of_x_collectected_on_y", {paymentAmmount:"" + subscription.price + "", collectionDate:"" + subscription.nextPaymentDueAt + ""})}
p.pull-right
p
diff --git a/services/web/app/views/subscriptions/plans.jade b/services/web/app/views/subscriptions/plans.jade
index 9db21fa3a6..d87f601889 100644
--- a/services/web/app/views/subscriptions/plans.jade
+++ b/services/web/app/views/subscriptions/plans.jade
@@ -3,6 +3,7 @@ block scripts
script(type='text/javascript').
window.recomendedCurrency = '#{recomendedCurrency}'
window.abCurrencyFlag = '#{abCurrencyFlag}'
+ window.shouldABTestPlans = #{shouldABTestPlans || false}
script(type='text/javascript').
(function() {var s=document.createElement('script'); s.type='text/javascript';s.async=true;
@@ -56,133 +57,144 @@ block content
ng-click="changeCurreny(currency)"
) {{currency}} ({{value['symbol']}})
- .row(ng-cloak)
- .col-md-10.col-md-offset-1
- .row
- .card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
- .col-md-4
- .card.card-first
- .card-header
- h2 #{translate("personal")}
- .circle #{translate("free")}
- ul.list-unstyled
- li #{translate("one_collaborator")}
- li
- li
- li
- li
- br
- a.btn.btn-info(
- href="/register"
- style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
- ) #{translate("sign_up_now")}
- .col-md-4
- .card.card-highlighted
- .card-header
- h2 #{translate("collaborator")}
- .circle
- span(ng-if="ui.view == 'monthly'")
- | {{plans[currencyCode]['collaborator']['monthly']}}
- span.small /mo
- span(ng-if="ui.view == 'annual'")
- | {{plans[currencyCode]['collaborator']['annual']}}
- span.small /yr
- ul.list-unstyled
- li
- strong #{translate("collabs_per_proj", {collabcount:10})}
- li #{translate("full_doc_history")}
- li #{translate("sync_to_dropbox")}
- li #{translate("sync_to_github")}
- li
- br
- a.btn.btn-info(
+ div(ng-show="showPlans")
+ .row(ng-cloak)
+ .col-md-10.col-md-offset-1
+ .row
+ .card-group.text-centered(ng-if="ui.view == 'monthly' || ui.view == 'annual'")
+ .col-md-4
+ .card.card-first
+ .card-header
+ h2 #{translate("personal")}
+ .circle #{translate("free")}
+ ul.list-unstyled
+ li #{translate("one_collaborator")}
+ li
+ li
+ li
+ li
+ br
+ a.btn.btn-info(
+ href="/register"
+ style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
+ ) #{translate("sign_up_now")}
+ .col-md-4
+ .card.card-highlighted
+ .card-header
+ h2 #{translate("collaborator")}
+ .circle
+ span(ng-if="ui.view == 'monthly'")
+ | {{plans[currencyCode]['collaborator']['monthly']}}
+ span.small /mo
+ span(ng-if="ui.view == 'annual'")
+ | {{plans[currencyCode]['collaborator']['annual']}}
+ span.small /yr
+ ul.list-unstyled
+ li
+ strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:10})}
+ strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:8})}
+ strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:12})}
+ li #{translate("full_doc_history")}
+ li #{translate("sync_to_dropbox")}
+ li #{translate("sync_to_github")}
+ li
+ br
+ a.btn.btn-info(
+
+ ng-href="#{baseUrl}/user/subscription/new?planCode=collaborator{{ (ui.view == 'annual' ? '-annual' : '') + (plansVariant == 'default' ? planQueryString : '_'+plansVariant)}}¤cy={{currencyCode}}", ng-click="signUpNowClicked('collaborator')"
+ )
+ span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
+ span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
+ .col-md-4
+ .card.card-last
+ .card-header
+ h2 #{translate("professional")}
+ .circle
+ span(ng-if="ui.view == 'monthly'")
+ | {{plans[currencyCode]['professional']['monthly']}}
+ span.small /mo
+ span(ng-if="ui.view == 'annual'")
+ | {{plans[currencyCode]['professional']['annual']}}
+ span.small /yr
+ ul.list-unstyled
+ li
+ strong #{translate("unlimited_collabs")}
+ li #{translate("full_doc_history")}
+ li #{translate("sync_to_dropbox")}
+ li #{translate("sync_to_github")}
+ li
+ br
+ a.btn.btn-info(
+ ng-href="#{baseUrl}/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}¤cy={{currencyCode}}", ng-click="signUpNowClicked('professional')"
+ )
+ span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
+ span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
+
+ .card-group.text-centered(ng-if="ui.view == 'student'")
+ .col-md-4
+ .card.card-first
+ .card-header
+ h2 #{translate("personal")}
+ .circle #{translate("free")}
+ ul.list-unstyled
+ li #{translate("one_collaborator")}
+ li
+ li
+ li
+ li
+ br
+ a.btn.btn-info(
+ href="/register"
+ style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
+ ) #{translate("sign_up_now")}
+
+ .col-md-4
+ .card.card-highlighted
+ .card-header
+ h2 #{translate("student")}
+ .circle
+ span
+ | {{plans[currencyCode]['student']['monthly']}}
+ span.small /mo
+ ul.list-unstyled
+ li
+ strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})}
+ strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})}
+ strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})}
+ li #{translate("full_doc_history")}
+ li #{translate("sync_to_dropbox")}
+ li #{translate("sync_to_github")}
+ li
+ br
+ a.btn.btn-info(
+ ng-href="#{baseUrl}/user/subscription/new?planCode=student{{ plansVariant == 'default' ? planQueryString : '_'+plansVariant }}¤cy={{currencyCode}}",
+ ng-click="signUpNowClicked('student')"
+ ) #{translate("start_free_trial")}
+
+ .col-md-4
+ .card.card-last
+ .card-header
+ h2 #{translate("student")} (#{translate("annual")})
+ .circle
+ span
+ | {{plans[currencyCode]['student']['annual']}}
+ span.small /yr
+ ul.list-unstyled
+ li
+ strong(ng-show="plansVariant == 'default'") #{translate("collabs_per_proj", {collabcount:6})}
+ strong(ng-show="plansVariant == 'heron'") #{translate("collabs_per_proj", {collabcount:4})}
+ strong(ng-show="plansVariant == 'ibis'") #{translate("collabs_per_proj", {collabcount:8})}
+ li #{translate("full_doc_history")}
+ li #{translate("sync_to_dropbox")}
+ li #{translate("sync_to_github")}
+ li
+ br
+ a.btn.btn-info(
+ ng-href="#{baseUrl}/user/subscription/new?planCode=student-annual{{ plansVariant == 'default' ? '' : '_'+plansVariant }}¤cy={{currencyCode}}",
+ ng-click="signUpNowClicked('student')"
+ ) #{translate("buy_now")}
- ng-href="#{baseUrl}/user/subscription/new?planCode=collaborator{{ ui.view == 'annual' && '-annual' || planQueryString}}¤cy={{currencyCode}}", ng-click="signUpNowClicked('collaborator')"
- )
- span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
- span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
- .col-md-4
- .card.card-last
- .card-header
- h2 #{translate("professional")}
- .circle
- span(ng-if="ui.view == 'monthly'")
- | {{plans[currencyCode]['professional']['monthly']}}
- span.small /mo
- span(ng-if="ui.view == 'annual'")
- | {{plans[currencyCode]['professional']['annual']}}
- span.small /yr
- ul.list-unstyled
- li
- strong #{translate("unlimited_collabs")}
- li #{translate("full_doc_history")}
- li #{translate("sync_to_dropbox")}
- li #{translate("sync_to_github")}
- li
- br
- a.btn.btn-info(
- ng-href="#{baseUrl}/user/subscription/new?planCode=professional{{ ui.view == 'annual' && '-annual' || planQueryString}}¤cy={{currencyCode}}", ng-click="signUpNowClicked('professional')"
- )
- span(ng-show="ui.view != 'annual'") #{translate("start_free_trial")}
- span(ng-show="ui.view == 'annual'") #{translate("buy_now")}
- .card-group.text-centered(ng-if="ui.view == 'student'")
- .col-md-4
- .card.card-first
- .card-header
- h2 #{translate("personal")}
- .circle #{translate("free")}
- ul.list-unstyled
- li #{translate("one_collaborator")}
- li
- li
- li
- li
- br
- a.btn.btn-info(
- href="/register"
- style=(getLoggedInUserId() === undefined ? "" : "visibility: hidden")
- ) #{translate("sign_up_now")}
-
- .col-md-4
- .card.card-highlighted
- .card-header
- h2 #{translate("student")}
- .circle
- span
- | {{plans[currencyCode]['student']['monthly']}}
- span.small /mo
- ul.list-unstyled
- li
- strong #{translate("collabs_per_proj", {collabcount:6})}
- li #{translate("full_doc_history")}
- li #{translate("sync_to_dropbox")}
- li #{translate("sync_to_github")}
- li
- br
- a.btn.btn-info(
- ng-href="#{baseUrl}/user/subscription/new?planCode=student{{planQueryString}}¤cy={{currencyCode}}", ng-click="signUpNowClicked('student')"
- ) #{translate("start_free_trial")}
-
- .col-md-4
- .card.card-last
- .card-header
- h2 #{translate("student")} (#{translate("annual")})
- .circle
- span
- | {{plans[currencyCode]['student']['annual']}}
- span.small /yr
- ul.list-unstyled
- li
- strong #{translate("collabs_per_proj", {collabcount:6})}
- li #{translate("full_doc_history")}
- li #{translate("sync_to_dropbox")}
- li #{translate("sync_to_github")}
- li
- br
- a.btn.btn-info(
- ng-href="#{baseUrl}/user/subscription/new?planCode=student-annual¤cy={{currencyCode}}", ng-click="signUpNowClicked('student')"
- ) #{translate("buy_now")}
.row.row-spaced(ng-cloak)
p.text-centered #{translate("choose_plan_works_for_you", {len:'{{trial_len}}'})}
diff --git a/services/web/app/views/subscriptions/successful_subscription.jade b/services/web/app/views/subscriptions/successful_subscription.jade
index 8413f5e036..17ac4f3594 100644
--- a/services/web/app/views/subscriptions/successful_subscription.jade
+++ b/services/web/app/views/subscriptions/successful_subscription.jade
@@ -2,7 +2,7 @@ extends ../layout
block content
.content.content-alt
- .container
+ .container(ng-controller="SuccessfulSubscriptionController")
.row
.col-md-8.col-md-offset-2
.card(ng-cloak)
diff --git a/services/web/app/views/user/settings.jade b/services/web/app/views/user/settings.jade
index d2fa8326d1..eb5bf671fa 100644
--- a/services/web/app/views/user/settings.jade
+++ b/services/web/app/views/user/settings.jade
@@ -150,16 +150,32 @@ block content
script(type='text/ng-template', id='deleteAccountModalTemplate')
.modal-header
h3 #{translate("delete_account")}
- .modal-body
- p !{translate("delete_account_warning_message_2")}
+ div.modal-body#delete-account-modal
+ p !{translate("delete_account_warning_message_3")}
form(novalidate, name="deleteAccountForm")
+ label #{translate('email')}
input.form-control(
type="text",
+ autocomplete="off",
placeholder="",
ng-model="state.deleteText",
focus-on="open",
ng-keyup="checkValidation()"
)
+ label #{translate('password')}
+ input.form-control(
+ type="password",
+ autocomplete="off",
+ placeholder="",
+ ng-model="state.password",
+ ng-keyup="checkValidation()"
+ )
+ div(ng-if="state.error")
+ div.alert.alert-danger
+ | #{translate('generic_something_went_wrong')}
+ div(ng-if="state.invalidCredentials")
+ div.alert.alert-danger
+ | #{translate('email_or_password_wrong_try_again')}
.modal-footer
button.btn.btn-default(
ng-click="cancel()"
diff --git a/services/web/public/coffee/ide.coffee b/services/web/public/coffee/ide.coffee
index 1318857afa..fdb1a2804b 100644
--- a/services/web/public/coffee/ide.coffee
+++ b/services/web/public/coffee/ide.coffee
@@ -67,16 +67,21 @@ define [
pdfLayout: 'sideBySide'
}
$scope.user = window.user
+
+
+ $scope.shouldABTestPlans = false
+ if $scope.user.signUpDate >= '2016-10-27'
+ $scope.shouldABTestPlans = true
+
$scope.settings = window.userSettings
$scope.anonymous = window.anonymous
$scope.chat = {}
-
# Only run the header AB test for newly registered users.
_abTestStartDate = new Date(Date.UTC(2016, 8, 28))
_userSignUpDate = new Date(window.user.signUpDate)
-
+
$scope.shouldABTestHeaderLabels = _userSignUpDate > _abTestStartDate
$scope.headerLabelsABVariant = ""
@@ -93,7 +98,7 @@ define [
# Tracking code.
$scope.$watch "ui.view", (newView, oldView) ->
if newView? and newView != "editor" and newView != "pdf"
- event_tracking.sendMBOnce "ide-open-view-#{ newView }-once"
+ event_tracking.sendMBOnce "ide-open-view-#{ newView }-once"
$scope.$watch "ui.chatOpen", (isOpen) ->
event_tracking.sendMBOnce "ide-open-chat-once" if isOpen
@@ -106,7 +111,7 @@ define [
# End of tracking code.
window._ide = ide
-
+
ide.validFileRegex = '^[^\*\/]*$' # Don't allow * and /
ide.project_id = $scope.project_id = window.project_id
diff --git a/services/web/public/coffee/ide/connection/ConnectionManager.coffee b/services/web/public/coffee/ide/connection/ConnectionManager.coffee
index f3d14a3e3d..811587e5ad 100644
--- a/services/web/public/coffee/ide/connection/ConnectionManager.coffee
+++ b/services/web/public/coffee/ide/connection/ConnectionManager.coffee
@@ -27,6 +27,7 @@ define [], () ->
@connected = false
@userIsInactive = false
+ @gracefullyReconnecting = false
@$scope.connection =
reconnecting: false
@@ -54,6 +55,7 @@ define [], () ->
@ide.socket.on "connect", () =>
sl_console.log "[socket.io connect] Connected"
@connected = true
+ @gracefullyReconnecting = false
@ide.pushEvent("connected")
@$scope.$apply () =>
@@ -81,7 +83,7 @@ define [], () ->
@$scope.$apply () =>
@$scope.connection.reconnecting = false
- if !$scope.connection.forced_disconnect and !@userIsInactive
+ if !$scope.connection.forced_disconnect and !@userIsInactive and !@gracefullyReconnecting
@startAutoReconnectCountdown()
@ide.socket.on 'forceDisconnect', (message) =>
@@ -97,7 +99,11 @@ define [], () ->
setTimeout () ->
location.reload()
, 10 * 1000
-
+
+ @ide.socket.on "reconnectGracefully", () =>
+ sl_console.log "Reconnect gracefully"
+ @reconnectGracefully()
+
joinProject: () ->
sl_console.log "[joinProject] joining..."
@ide.socket.emit 'joinProject', {
@@ -180,3 +186,24 @@ define [], () ->
@$scope.$apply () =>
@$scope.connection.inactive_disconnect = true
+ RECONNECT_GRACEFULLY_RETRY_INTERVAL: 5000 # ms
+ MAX_RECONNECT_GRACEFULLY_INTERVAL: 60 * 5 * 1000 # 5 minutes
+ reconnectGracefully: () ->
+ @reconnectGracefullyStarted ?= new Date()
+ userIsInactive = (new Date() - @lastUserAction) > @RECONNECT_GRACEFULLY_RETRY_INTERVAL
+ maxIntervalReached = (new Date() - @reconnectGracefullyStarted) > @MAX_RECONNECT_GRACEFULLY_INTERVAL
+ if userIsInactive or maxIntervalReached
+ sl_console.log "[reconnectGracefully] User didn't do anything for last 5 seconds, reconnecting"
+ @_reconnectGracefullyNow()
+ else
+ sl_console.log "[reconnectGracefully] User is working, will try again in 5 seconds"
+ setTimeout () =>
+ @reconnectGracefully()
+ , @RECONNECT_GRACEFULLY_RETRY_INTERVAL
+
+ _reconnectGracefullyNow: () ->
+ @gracefullyReconnecting = true
+ @reconnectGracefullyStarted = null
+ # Clear cookie so we don't go to the same backend server
+ $.cookie("SERVERID", "", { expires: -1, path: "/" })
+ @reconnectImmediately()
\ No newline at end of file
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
index 87b525120f..1a573cc91e 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor.coffee
@@ -15,9 +15,13 @@ define [
# set the path for ace workers if using a CDN (from editor.jade)
if window.aceWorkerPath != ""
+ syntaxValidationEnabled = true
ace.config.set('workerPath', "#{window.aceWorkerPath}")
else
- ace.config.setDefaultValue("session", "useWorker", false)
+ syntaxValidationEnabled = false
+
+ # By default, don't use workers - enable them per-session as required
+ ace.config.setDefaultValue("session", "useWorker", false)
# Ace loads its script itself, so we need to hook in to be able to clear
# the cache.
@@ -202,8 +206,12 @@ define [
editor.setReadOnly !!value
scope.$watch "syntaxValidation", (value) ->
- session = editor.getSession()
- session.setOption("useWorker", value);
+ # ignore undefined settings here
+ # only instances of ace with an explicit value should set useWorker
+ # the history instance will have syntaxValidation undefined
+ if value? and syntaxValidationEnabled
+ session = editor.getSession()
+ session.setOption("useWorker", value);
editor.setOption("scrollPastEnd", true)
@@ -223,14 +231,32 @@ define [
# see if we can lookup a suitable mode from ace
# but fall back to text by default
try
- mode = ModeList.getModeForPath(scope.fileName).mode
+ if scope.fileName.match(/\.(Rtex|bbl)$/i)
+ # recognise Rtex and bbl as latex
+ mode = "ace/mode/latex"
+ else if scope.fileName.match(/\.(sty|cls|clo)$/)
+ # recognise some common files as tex
+ mode = "ace/mode/tex"
+ else
+ mode = ModeList.getModeForPath(scope.fileName).mode
+ # we prefer plain_text mode over text mode because ace's
+ # text mode is actually for code and has unwanted
+ # indenting (see wrapMethod in ace edit_session.js)
+ if mode is "ace/mode/text"
+ mode = "ace/mode/plain_text"
catch
- mode = "ace/mode/text"
+ mode = "ace/mode/plain_text"
- editor.setSession(new EditSession(lines, mode))
+ # create our new session
+ session = new EditSession(lines, mode)
- session = editor.getSession()
session.setUseWrapMode(true)
+ # use syntax validation only when explicitly set
+ if scope.syntaxValidation? and syntaxValidationEnabled
+ session.setOption("useWorker", scope.syntaxValidation);
+
+ # now attach session to editor
+ editor.setSession(session)
doc = session.getDocument()
doc.on "change", onChange
diff --git a/services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee b/services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee
index 9c61ce983d..40f1ba7c82 100644
--- a/services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee
+++ b/services/web/public/coffee/ide/editor/directives/aceEditor/undo/UndoManager.coffee
@@ -233,8 +233,16 @@ define [
start = aceDelta.start
if !start?
error = new Error("aceDelta had no start event.")
+ JSONstringifyWithCycles = (o) ->
+ seen = []
+ return JSON.stringify o, (k,v) ->
+ if (typeof v == 'object')
+ if ( !seen.indexOf(v) )
+ return '__cycle__'
+ seen.push(v);
+ return v
Raven?.captureException(error, {
- aceDelta: JSON.stringify(aceDelta)
+ aceDelta: JSONstringifyWithCycles(aceDelta)
})
throw error
linesBefore = docLines.slice(0, start.row)
diff --git a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
index 790f2384a1..c2559b1faf 100644
--- a/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
+++ b/services/web/public/coffee/ide/pdf/controllers/PdfController.coffee
@@ -29,12 +29,12 @@ define [
$scope.$watch "shouldShowLogs", (shouldShow) ->
if shouldShow
- $scope.$applyAsync () ->
+ $scope.$applyAsync () ->
$scope.shouldDropUp = getFilesDropdownTopCoordAsRatio() > 0.65
# log hints tracking
$scope.logHintsNegFeedbackValues = logHintsFeedback.feedbackOpts
-
+
$scope.trackLogHintsLearnMore = () ->
event_tracking.sendMB "logs-hints-learn-more"
@@ -108,7 +108,7 @@ define [
_csrf: window.csrfToken
}, {params: params}
- parseCompileResponse = (response) ->
+ parseCompileResponse = (response) ->
# keep last url
last_pdf_url = $scope.pdf.url
@@ -469,7 +469,7 @@ define [
event_tracking.sendMB "subscription-start-trial", { source }
- window.open("/user/subscription/new?planCode=student_free_trial_7_days")
+ window.open("/user/subscription/new?planCode=#{$scope.startTrialPlanCode}")
$scope.startedFreeTrial = true
App.factory "synctex", ["ide", "$http", "$q", (ide, $http, $q) ->
diff --git a/services/web/public/coffee/ide/references/ReferencesManager.coffee b/services/web/public/coffee/ide/references/ReferencesManager.coffee
index 2f1e95c5b1..c5d7c2348b 100644
--- a/services/web/public/coffee/ide/references/ReferencesManager.coffee
+++ b/services/web/public/coffee/ide/references/ReferencesManager.coffee
@@ -17,8 +17,13 @@ define [
# When we join the project:
# index all references files
# and don't broadcast to all clients
+ @inited = false
@$scope.$on 'project:joined', (e) =>
- @indexAllReferences(false)
+ # We only need to grab the references when the editor first loads,
+ # not on every reconnect
+ if !@inited
+ @inited = true
+ @indexAllReferences(false)
setTimeout(
(self) ->
diff --git a/services/web/public/coffee/main/account-settings.coffee b/services/web/public/coffee/main/account-settings.coffee
index 29ec146051..08226ab399 100644
--- a/services/web/public/coffee/main/account-settings.coffee
+++ b/services/web/public/coffee/main/account-settings.coffee
@@ -29,10 +29,13 @@ define [
App.controller "DeleteAccountModalController", [
"$scope", "$modalInstance", "$timeout", "$http",
($scope, $modalInstance, $timeout, $http) ->
- $scope.state =
+ $scope.state =
isValid : false
deleteText: ""
+ password: ""
inflight: false
+ error: false
+ invalidCredentials: false
$modalInstance.opened.then () ->
$timeout () ->
@@ -40,20 +43,33 @@ define [
, 700
$scope.checkValidation = ->
- $scope.state.isValid = $scope.state.deleteText == $scope.email
+ $scope.state.isValid = $scope.state.deleteText == $scope.email and $scope.state.password.length > 0
$scope.delete = () ->
$scope.state.inflight = true
-
+ $scope.state.error = false
+ $scope.state.invalidCredentials = false
$http({
- method: "DELETE"
- url: "/user"
+ method: "POST"
+ url: "/user/delete"
headers:
"X-CSRF-Token": window.csrfToken
+ "Content-Type": 'application/json'
+ data:
+ password: $scope.state.password
})
.success () ->
$modalInstance.close()
+ $scope.state.inflight = false
+ $scope.state.error = false
+ $scope.state.invalidCredentials = false
window.location = "/"
+ .error (data, status) ->
+ $scope.state.inflight = false
+ if status == 403
+ $scope.state.invalidCredentials = true
+ else
+ $scope.state.error = true
$scope.cancel = () ->
$modalInstance.dismiss('cancel')
diff --git a/services/web/public/coffee/main/account-upgrade.coffee b/services/web/public/coffee/main/account-upgrade.coffee
index be842b6907..6144bea9ef 100644
--- a/services/web/public/coffee/main/account-upgrade.coffee
+++ b/services/web/public/coffee/main/account-upgrade.coffee
@@ -6,14 +6,34 @@ define [
$scope.buttonClass = "btn-primary"
$scope.startFreeTrial = (source, couponCode) ->
- event_tracking.sendMB "subscription-start-trial", { source }
+ plan = 'collaborator_free_trial_7_days'
w = window.open()
- sixpack.convert "track-changes-discount", ->
- sixpack.participate 'in-editor-free-trial-plan', ['student', 'collaborator'], (planName, rawResponse)->
- ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
- url = "/user/subscription/new?planCode=#{planName}_free_trial_7_days&ssp=#{planName == 'collaborator'}"
- if couponCode?
- url = "#{url}&cc=#{couponCode}"
- $scope.startedFreeTrial = true
- w.location = url
+ go = () ->
+ ga?('send', 'event', 'subscription-funnel', 'upgraded-free-trial', source)
+ url = "/user/subscription/new?planCode=#{plan}&ssp=true"
+ if couponCode?
+ url = "#{url}&cc=#{couponCode}"
+ $scope.startedFreeTrial = true
+
+ switch source
+ when "dropbox"
+ sixpack.participate 'teaser-dropbox-text', ['default', 'dropbox-focused'], (variant) ->
+ event_tracking.sendMB "subscription-start-trial", { source, plan, variant }
+
+ when "history"
+ sixpack.participate 'teaser-history', ['default', 'focused'], (variant) ->
+ event_tracking.sendMB "subscription-start-trial", { source, plan, variant }
+
+ else
+ event_tracking.sendMB "subscription-start-trial", { source, plan }
+
+ w.location = url
+
+ if $scope.shouldABTestPlans
+ sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)->
+ if chosenVariation in ['heron', 'ibis']
+ plan = "collaborator_#{chosenVariation}"
+ go()
+ else
+ go()
diff --git a/services/web/public/coffee/main/new-subscription.coffee b/services/web/public/coffee/main/new-subscription.coffee
index d006c6173c..61961cfccb 100644
--- a/services/web/public/coffee/main/new-subscription.coffee
+++ b/services/web/public/coffee/main/new-subscription.coffee
@@ -5,12 +5,15 @@ define [
App.controller "NewSubscriptionController", ($scope, MultiCurrencyPricing, abTestManager, $http, sixpack, event_tracking, ccUtils)->
throw new Error("Recurly API Library Missing.") if typeof recurly is "undefined"
-
+
$scope.currencyCode = MultiCurrencyPricing.currencyCode
$scope.plans = MultiCurrencyPricing.plans
$scope.switchToStudent = ()->
- window.location = "/user/subscription/new?planCode=student_free_trial_7_days¤cy=#{$scope.currencyCode}&cc=#{$scope.data.coupon}"
+ currentPlanCode = window.plan_code
+ planCode = currentPlanCode.replace('collaborator', 'student')
+ event_tracking.sendMB 'subscription-form-switch-to-student', { plan: window.plan_code }
+ window.location = "/user/subscription/new?planCode=#{planCode}¤cy=#{$scope.currencyCode}&cc=#{$scope.data.coupon}"
event_tracking.sendMB "subscription-form", { plan : window.plan_code }
diff --git a/services/web/public/coffee/main/plans.coffee b/services/web/public/coffee/main/plans.coffee
index 4f21c4b5a8..538152bf43 100644
--- a/services/web/public/coffee/main/plans.coffee
+++ b/services/web/public/coffee/main/plans.coffee
@@ -5,12 +5,171 @@ define [
App.factory "MultiCurrencyPricing", () ->
-
+
currencyCode = window.recomendedCurrency
return {
currencyCode:currencyCode
- plans:
+
+ heron:
+ USD:
+ student:
+ monthly: "$6"
+ annual: "$60"
+ collaborator:
+ monthly: "$12"
+ annual: "$144"
+ EUR:
+ student:
+ monthly: "€5"
+ annual: "€50"
+ collaborator:
+ monthly: "€11"
+ annual: "€132"
+ GBP:
+ student:
+ monthly: "£5"
+ annual: "£50"
+ collaborator:
+ monthly: "£10"
+ annual: "£120"
+ SEK:
+ student:
+ monthly: "45 kr"
+ annual: "450 kr"
+ collaborator:
+ monthly: "90 kr"
+ annual: "1080 kr"
+ CAD:
+ student:
+ monthly: "$7"
+ annual: "$70"
+ collaborator:
+ monthly: "$14"
+ annual: "$168"
+ NOK:
+ student:
+ monthly: "45 kr"
+ annual: "450 kr"
+ collaborator:
+ monthly: "90 kr"
+ annual: "1080 kr"
+ DKK:
+ student:
+ monthly: "40 kr"
+ annual: "400 kr"
+ collaborator:
+ monthly: "70 kr"
+ annual: "840 kr"
+ AUD:
+ student:
+ monthly: "$8"
+ annual: "$80"
+ collaborator:
+ monthly: "$15"
+ annual: "$180"
+ NZD:
+ student:
+ monthly: "$8"
+ annual: "$80"
+ collaborator:
+ monthly: "$15"
+ annual: "$180"
+ CHF:
+ student:
+ monthly: "Fr 6"
+ annual: "Fr 60"
+ collaborator:
+ monthly: "Fr 12"
+ annual: "Fr 144"
+ SGD:
+ student:
+ monthly: "$8"
+ annual: "$80"
+ collaborator:
+ monthly: "$16"
+ annual: "$192"
+
+ ibis:
+ USD:
+ student:
+ monthly: "$10"
+ annual: "$100"
+ collaborator:
+ monthly: "$18"
+ annual: "$216"
+ EUR:
+ student:
+ monthly: "€9"
+ annual: "€90"
+ collaborator:
+ monthly: "€17"
+ annual: "€204"
+ GBP:
+ student:
+ monthly: "£7"
+ annual: "£70"
+ collaborator:
+ monthly: "£14"
+ annual: "£168"
+ SEK:
+ student:
+ monthly: "75 kr"
+ annual: "750 kr"
+ collaborator:
+ monthly: "140 kr"
+ annual: "1680 kr"
+ CAD:
+ student:
+ monthly: "$12"
+ annual: "$120"
+ collaborator:
+ monthly: "$22"
+ annual: "$264"
+ NOK:
+ student:
+ monthly: "75 kr"
+ annual: "750 kr"
+ collaborator:
+ monthly: "140 kr"
+ annual: "1680 kr"
+ DKK:
+ student:
+ monthly: "68 kr"
+ annual: "680 kr"
+ collaborator:
+ monthly: "110 kr"
+ annual: "1320 kr"
+ AUD:
+ student:
+ monthly: "$13"
+ annual: "$130"
+ collaborator:
+ monthly: "$22"
+ annual: "$264"
+ NZD:
+ student:
+ monthly: "$14"
+ annual: "$140"
+ collaborator:
+ monthly: "$22"
+ annual: "$264"
+ CHF:
+ student:
+ monthly: "Fr 10"
+ annual: "Fr 100"
+ collaborator:
+ monthly: "Fr 18"
+ annual: "Fr 216"
+ SGD:
+ student:
+ monthly: "$14"
+ annual: "$140"
+ collaborator:
+ monthly: "$25"
+ annual: "$300"
+
+ plans:
USD:
symbol: "$"
student:
@@ -23,7 +182,7 @@ define [
monthly: "$30"
annual: "$360"
- EUR:
+ EUR:
symbol: "€"
student:
monthly: "€7"
@@ -34,7 +193,7 @@ define [
professional:
monthly: "€28"
annual: "€336"
-
+
GBP:
symbol: "£"
student:
@@ -117,7 +276,7 @@ define [
professional:
monthly: "$35"
annual: "$420"
-
+
CHF:
symbol: "Fr"
student:
@@ -141,36 +300,54 @@ define [
professional:
monthly: "$40"
annual: "$480"
+
}
-
+ App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http, sixpack) ->
- App.controller "PlansController", ($scope, $modal, event_tracking, abTestManager, MultiCurrencyPricing, $http) ->
+ $scope.showPlans = false
+
+ $scope.plansVariant = 'default'
+ $scope.shouldABTestPlans = window.shouldABTestPlans
+
+ if $scope.shouldABTestPlans
+ sixpack.participate 'plans-1610', ['default', 'heron', 'ibis'], (chosenVariation, rawResponse)->
+ $scope.plansVariant = chosenVariation
+ event_tracking.sendMB 'plans-page', {plans_variant: chosenVariation}
+ if chosenVariation in ['heron', 'ibis']
+ # overwrite student plans with alternative
+ for currency, _v of $scope.plans
+ $scope.plans[currency]['student'] = MultiCurrencyPricing[chosenVariation][currency]['student']
+ $scope.plans[currency]['collaborator'] = MultiCurrencyPricing[chosenVariation][currency]['collaborator']
+ $scope.showPlans = true
+ else
+ $scope.showPlans = true
$scope.plans = MultiCurrencyPricing.plans
+
$scope.currencyCode = MultiCurrencyPricing.currencyCode
$scope.trial_len = 7
+
$scope.planQueryString = '_free_trial_7_days'
$scope.ui =
view: "monthly"
-
$scope.changeCurreny = (newCurrency)->
$scope.currencyCode = newCurrency
$scope.signUpNowClicked = (plan, annual)->
+ event_tracking.sendMB 'plans-page-start-trial', {plan}
if $scope.ui.view == "annual"
plan = "#{plan}_annual"
-
- event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan
+ event_tracking.send 'subscription-funnel', 'sign_up_now_button', plan
$scope.switchToMonthly = ->
$scope.ui.view = "monthly"
event_tracking.send 'subscription-funnel', 'plans-page', 'monthly-prices'
-
+
$scope.switchToStudent = ->
$scope.ui.view = "student"
event_tracking.send 'subscription-funnel', 'plans-page', 'student-prices'
@@ -178,7 +355,7 @@ define [
$scope.switchToAnnual = ->
$scope.ui.view = "annual"
event_tracking.send 'subscription-funnel', 'plans-page', 'student-prices'
-
+
$scope.openGroupPlanModal = () ->
$modal.open {
templateUrl: "groupPlanModalTemplate"
diff --git a/services/web/public/coffee/main/subscription-dashboard.coffee b/services/web/public/coffee/main/subscription-dashboard.coffee
index 7476d814e1..71bc198e6d 100644
--- a/services/web/public/coffee/main/subscription-dashboard.coffee
+++ b/services/web/public/coffee/main/subscription-dashboard.coffee
@@ -1,15 +1,21 @@
define [
"base"
], (App)->
+
+ App.controller 'SuccessfulSubscriptionController', ($scope, sixpack) ->
+ sixpack.convert 'plans-1610', () ->
+
+
SUBSCRIPTION_URL = "/user/subscription/update"
setupReturly = _.once ->
recurly?.configure window.recurlyApiKey
PRICES = {}
+
App.controller "CurrenyDropdownController", ($scope, MultiCurrencyPricing, $q)->
- $scope.plans = MultiCurrencyPricing.plans
+ # $scope.plans = MultiCurrencyPricing.plans
$scope.currencyCode = MultiCurrencyPricing.currencyCode
$scope.changeCurrency = (newCurrency)->
@@ -31,7 +37,7 @@ define [
$scope.currencyCode = MultiCurrencyPricing.currencyCode
$scope.pricing = MultiCurrencyPricing
- $scope.plans = MultiCurrencyPricing.plans
+ # $scope.plans = MultiCurrencyPricing.plans
$scope.currencySymbol = MultiCurrencyPricing.plans[MultiCurrencyPricing.currencyCode].symbol
$scope.currencyCode = MultiCurrencyPricing.currencyCode
@@ -53,9 +59,9 @@ define [
price = ""
App.controller "ConfirmChangePlanController", ($scope, $modalInstance, $http)->
-
+
$scope.confirmChangePlan = ->
- body =
+ body =
plan_code: $scope.plan.planCode
_csrf : window.csrfToken
@@ -74,7 +80,7 @@ define [
$scope.confirmLeaveGroup = ->
$scope.inflight = true
$http({
- url: "/subscription/group/user",
+ url: "/subscription/group/user",
method: "DELETE",
params: {admin_user_id: $scope.admin_id, _csrf: window.csrfToken}
}).success ->
@@ -87,6 +93,8 @@ define [
App.controller "UserSubscriptionController", ($scope, MultiCurrencyPricing, $http, sixpack, $modal) ->
+ $scope.plans = MultiCurrencyPricing.plans
+
freeTrialEndDate = new Date(subscription?.trial_ends_at)
sevenDaysTime = new Date()
@@ -96,6 +104,16 @@ define [
freeTrialExpiresUnderSevenDays = freeTrialEndDate < sevenDaysTime
$scope.view = 'overview'
+ $scope.getSuffix = (planCode) ->
+ planCode?.match(/(.*?)_(.*)/)?[2] || null
+ $scope.subscriptionSuffix = $scope.getSuffix(window?.subscription?.planCode)
+ if $scope.subscriptionSuffix == 'free_trial_7_days'
+ $scope.subscriptionSuffix = ''
+ $scope.isNextGenPlan = $scope.subscriptionSuffix in ['heron', 'ibis']
+
+ $scope.shouldShowPlan = (planCode) ->
+ $scope.getSuffix(planCode) not in ['heron', 'ibis']
+
isMonthlyCollab = subscription?.planCode?.indexOf("collaborator") != -1 and subscription?.planCode?.indexOf("ann") == -1
stillInFreeTrial = freeTrialInFuture and freeTrialExpiresUnderSevenDays
@@ -118,7 +136,7 @@ define [
$scope.studentPrice = $scope.currencySymbol + (totalPriceExTax + taxAmmount)
$scope.downgradeToStudent = ->
- body =
+ body =
plan_code: 'student'
_csrf : window.csrfToken
$scope.inflight = true
@@ -129,7 +147,7 @@ define [
console.log "something went wrong changing plan"
$scope.cancelSubscription = ->
- body =
+ body =
_csrf : window.csrfToken
$scope.inflight = true
@@ -158,7 +176,7 @@ define [
$scope.exendTrial = ->
- body =
+ body =
_csrf : window.csrfToken
$scope.inflight = true
$http.put("/user/subscription/extend", body)
@@ -166,6 +184,3 @@ define [
location.reload()
.error ->
console.log "something went wrong changing plan"
-
-
-
diff --git a/services/web/public/img/teasers/history/teaser-history.png b/services/web/public/img/teasers/history/teaser-history.png
new file mode 100644
index 0000000000..d03671f1f1
Binary files /dev/null and b/services/web/public/img/teasers/history/teaser-history.png differ
diff --git a/services/web/public/js/ace-1.2.5/mode-latex.js b/services/web/public/js/ace-1.2.5/mode-latex.js
index c87324efe4..f183d7c263 100644
--- a/services/web/public/js/ace-1.2.5/mode-latex.js
+++ b/services/web/public/js/ace-1.2.5/mode-latex.js
@@ -214,18 +214,21 @@ var createLatexWorker = function (session) {
var suppressions = [];
var hints = [];
var changeHandler = null;
+ var docChangePending = false;
+ var firstPass = true;
var worker = new WorkerClient(["ace"], "ace/mode/latex_worker", "LatexWorker");
worker.attachToDocument(doc);
-
- doc.on("change", function () {
+ var docChangeHandler = doc.on("change", function () {
+ docChangePending = true;
if(changeHandler) {
clearTimeout(changeHandler);
changeHandler = null;
}
});
- selection.on("changeCursor", function () {
+ var cursorHandler = selection.on("changeCursor", function () {
+ if (docChangePending) { return; } ;
changeHandler = setTimeout(function () {
updateMarkers({cursorMoveOnly:true});
suppressions = [];
@@ -307,11 +310,20 @@ var createLatexWorker = function (session) {
}
}
if (!cursorMoveOnly || suppressedChanges) {
- session.setAnnotations(annotations);
+ if (firstPass) {
+ if (annotations.length > 0) {
+ var originalAnnotations = session.getAnnotations();
+ session.setAnnotations(originalAnnotations.concat(annotations));
+ };
+ firstPass = false;
+ } else {
+ session.setAnnotations(annotations);
+ }
};
};
worker.on("lint", function(results) {
+ if(docChangePending) { docChangePending = false; };
hints = results.data;
if (hints.length > 100) {
hints = hints.slice(0, 100); // limit to 100 errors
@@ -319,14 +331,22 @@ var createLatexWorker = function (session) {
updateMarkers();
});
worker.on("terminate", function() {
+ if(changeHandler) {
+ clearTimeout(changeHandler);
+ changeHandler = null;
+ }
+ doc.off("change", docChangeHandler);
+ selection.off("changeCursor", cursorHandler);
for (var key in savedRange) {
var range = savedRange[key];
- range.start.detach();
- range.end.detach();
+ if (range.start !== cursorAnchor) { range.start.detach(); }
+ if (range.end !== cursorAnchor) { range.end.detach(); }
session.removeMarker(range.id);
- delete savedRange[key];
}
-
+ savedRange = {};
+ hints = [];
+ suppressions = [];
+ session.clearAnnotations();
});
return worker;
diff --git a/services/web/public/js/ace-1.2.5/worker-latex.js b/services/web/public/js/ace-1.2.5/worker-latex.js
index c933f8db5f..20c377dc83 100644
--- a/services/web/public/js/ace-1.2.5/worker-latex.js
+++ b/services/web/public/js/ace-1.2.5/worker-latex.js
@@ -1755,7 +1755,7 @@ var InterpretTokens = function (TokeniseResult, ErrorReporter) {
};
if (endToken) {
- TokenErrorFromTo(token, endToken, "invalid environment command" + text.substring(token[2], endToken[3] || endToken[2]));
+ TokenErrorFromTo(token, endToken, "invalid environment command " + text.substring(token[2], endToken[3] || endToken[2]));
} else {
TokenError(token, "invalid environment command");
};
diff --git a/services/web/public/stylesheets/app/account-settings.less b/services/web/public/stylesheets/app/account-settings.less
index 23769e055b..c232e36fab 100644
--- a/services/web/public/stylesheets/app/account-settings.less
+++ b/services/web/public/stylesheets/app/account-settings.less
@@ -2,4 +2,11 @@
.alert {
margin-bottom: 0;
}
-}
\ No newline at end of file
+}
+
+#delete-account-modal {
+ .alert {
+ margin-top: 25px;
+ margin-bottom: 4px;
+ }
+}
diff --git a/services/web/public/stylesheets/app/editor.less b/services/web/public/stylesheets/app/editor.less
index e376598b8b..905cf269c5 100644
--- a/services/web/public/stylesheets/app/editor.less
+++ b/services/web/public/stylesheets/app/editor.less
@@ -414,11 +414,13 @@
}
}
+.teaser-title,
.dropbox-teaser-title {
margin-top: 0;
text-align: center;
}
+.teaser-img,
.dropbox-teaser-img {
.img-responsive;
margin-bottom: 5px;
diff --git a/services/web/public/stylesheets/app/editor/history.less b/services/web/public/stylesheets/app/editor/history.less
index cb88be62c1..81af8f989f 100644
--- a/services/web/public/stylesheets/app/editor/history.less
+++ b/services/web/public/stylesheets/app/editor/history.less
@@ -25,6 +25,19 @@
background-color: white;
border-radius: 8px;
}
+ .message-wider {
+ width: 650px;
+ margin-top: 60px;
+ padding: 0;
+ }
+
+ .message-header {
+ .modal-header;
+ }
+
+ .message-body {
+ .modal-body;
+ }
}
.diff-panel {
diff --git a/services/web/test/UnitTests/coffee/Email/EmailBuilderTests.coffee b/services/web/test/UnitTests/coffee/Email/EmailBuilderTests.coffee
index ff51507636..4ee0e0ed05 100644
--- a/services/web/test/UnitTests/coffee/Email/EmailBuilderTests.coffee
+++ b/services/web/test/UnitTests/coffee/Email/EmailBuilderTests.coffee
@@ -18,30 +18,6 @@ describe "EmailBuilder", ->
"settings-sharelatex":@settings
"logger-sharelatex": log:->
- describe "projectSharedWithYou", ->
- beforeEach ->
- @opts =
- to:"bob@bob.com"
- first_name:"bob"
- owner:
- email:"sally@hally.com"
- project:
- url:"http://www.project.com"
- name:"standard project"
- @email = @EmailBuilder.buildEmail("projectSharedWithYou", @opts)
-
- it "should insert the owner email into the template", ->
- @email.html.indexOf(@opts.owner.email).should.not.equal -1
- @email.subject.indexOf(@opts.owner.email).should.not.equal -1
-
- it 'should not have text component', ->
- expect(@email.html?).to.equal true
- expect(@email.text?).to.equal false
-
- it "should not have undefined in it", ->
- @email.html.indexOf("undefined").should.equal -1
- @email.subject.indexOf("undefined").should.equal -1
-
describe "projectInvite", ->
beforeEach ->
@opts =
diff --git a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee
index 1f935e420f..3a43c8988e 100644
--- a/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/Subscription/SubscriptionControllerTests.coffee
@@ -20,7 +20,7 @@ mockSubscriptions =
describe "SubscriptionController sanboxed", ->
beforeEach ->
- @user = {email:"tom@yahoo.com", _id: 'one'}
+ @user = {email:"tom@yahoo.com", _id: 'one', signUpDate: new Date('2000-10-01')}
@activeRecurlySubscription = mockSubscriptions["subscription-123-active"]
@AuthenticationController =
@@ -63,6 +63,8 @@ describe "SubscriptionController sanboxed", ->
getCurrencyCode:sinon.stub()
@SubscriptionDomainHandler =
getDomainLicencePage:sinon.stub()
+ @UserGetter =
+ getUser: sinon.stub().callsArgWith(2, null, @user)
@SubscriptionController = SandboxedModule.require modulePath, requires:
'../Authentication/AuthenticationController': @AuthenticationController
'./SubscriptionHandler': @SubscriptionHandler
@@ -76,6 +78,7 @@ describe "SubscriptionController sanboxed", ->
warn:->
"settings-sharelatex": @settings
"./SubscriptionDomainHandler":@SubscriptionDomainHandler
+ "../User/UserGetter": @UserGetter
@res = new MockResponse()
@@ -92,12 +95,31 @@ describe "SubscriptionController sanboxed", ->
@GeoIpLookup.getCurrencyCode.callsArgWith(1, null, @stubbedCurrencyCode)
@res.callback = done
@SubscriptionController.plansPage(@req, @res)
+ @UserGetter.getUser = sinon.stub().callsArgWith(2, null, @user)
it "should set the recommended currency from the geoiplookup", (done)->
@res.renderedVariables.recomendedCurrency.should.equal(@stubbedCurrencyCode)
@GeoIpLookup.getCurrencyCode.calledWith(@req.ip).should.equal true
done()
+ it 'should fetch the current user', (done) ->
+ @UserGetter.getUser.callCount.should.equal 1
+ done()
+
+ it 'should decide not to AB test the plans', (done) ->
+ @res.renderedVariables.shouldABTestPlans.should.equal false
+ done()
+
+ describe 'when user is not logged in', (done) ->
+
+ beforeEach ->
+ @AuthenticationController.getLoggedInUserId.returns(null)
+
+ it 'should not fetch the current user', (done) ->
+ @UserGetter.getUser.callCount.should.equal 0
+ done()
+
+
describe "editBillingDetailsPage", ->
describe "with a user with a subscription", ->
beforeEach (done) ->
diff --git a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee
index a9c98e02ec..cc1d2190ef 100644
--- a/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee
+++ b/services/web/test/UnitTests/coffee/User/UserControllerTests.coffee
@@ -84,15 +84,81 @@ describe "UserController", ->
sendStatus: sinon.stub()
json: sinon.stub()
@next = sinon.stub()
- describe "deleteUser", ->
- it "should delete the user", (done)->
+ describe 'tryDeleteUser', ->
- @res.sendStatus = (code)=>
- @UserDeleter.deleteUser.calledWith(@user_id)
+ beforeEach ->
+ @req.body.password = 'wat'
+ @AuthenticationController.getLoggedInUserId = sinon.stub().returns(@user._id)
+ @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, @user)
+ @UserDeleter.deleteUser = sinon.stub().callsArgWith(1, null)
+
+ it 'should send 200', (done) ->
+ @res.sendStatus = (code) =>
code.should.equal 200
done()
- @UserController.deleteUser @req, @res
+ @UserController.tryDeleteUser @req, @res, @next
+
+ it 'should try to authenticate user', (done) ->
+ @res.sendStatus = (code) =>
+ @AuthenticationManager.authenticate.callCount.should.equal 1
+ @AuthenticationManager.authenticate.calledWith({_id: @user._id}, @req.body.password).should.equal true
+ done()
+ @UserController.tryDeleteUser @req, @res, @next
+
+ it 'should delete the user', (done) ->
+ @res.sendStatus = (code) =>
+ @UserDeleter.deleteUser.callCount.should.equal 1
+ @UserDeleter.deleteUser.calledWith(@user._id).should.equal true
+ done()
+ @UserController.tryDeleteUser @req, @res, @next
+
+ describe 'when no password is supplied', ->
+
+ beforeEach ->
+ @req.body.password = ''
+
+ it 'should return 403', (done) ->
+ @res.sendStatus = (code) =>
+ code.should.equal 403
+ done()
+ @UserController.tryDeleteUser @req, @res, @next
+
+ describe 'when authenticate produces an error', ->
+
+ beforeEach ->
+ @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, new Error('woops'))
+
+ it 'should call next with an error', (done) ->
+ @next = (err) =>
+ expect(err).to.not.equal null
+ expect(err).to.be.instanceof Error
+ done()
+ @UserController.tryDeleteUser @req, @res, @next
+
+ describe 'when authenticate does not produce a user', ->
+
+ beforeEach ->
+ @AuthenticationManager.authenticate = sinon.stub().callsArgWith(2, null, null)
+
+ it 'should return 403', (done) ->
+ @res.sendStatus = (code) =>
+ code.should.equal 403
+ done()
+ @UserController.tryDeleteUser @req, @res, @next
+
+ describe 'when deleteUser produces an error', ->
+
+ beforeEach ->
+ @UserDeleter.deleteUser = sinon.stub().callsArgWith(1, new Error('woops'))
+
+ it 'should call next with an error', (done) ->
+ @next = (err) =>
+ expect(err).to.not.equal null
+ expect(err).to.be.instanceof Error
+ done()
+ @UserController.tryDeleteUser @req, @res, @next
+
describe "unsubscribe", ->