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

-
-
-
- - - View Project - - -
-
-
-

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", ->