mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #1107 from sharelatex/ja-purchase-groups
Purchase group/team accounts directly via app GitOrigin-RevId: 1a502878753de77758fb431f45a6366f199f1cb0
This commit is contained in:
parent
f1c8dcdf1e
commit
140f97eb20
15 changed files with 464 additions and 117 deletions
|
@ -0,0 +1,37 @@
|
|||
Settings = require 'settings-sharelatex'
|
||||
fs = require('fs')
|
||||
|
||||
# The groups.json file encodes the various group plan options we provide, and
|
||||
# is used in the app the render the appropriate dialog in the plans page, and
|
||||
# to generate the appropriate entries in the Settings.plans array.
|
||||
# It is also used by scripts/recurly/sync_recurly.rb, which will make sure
|
||||
# Recurly has a plan configured for all the groups, and that the prices are
|
||||
# up to date with the data in groups.json.
|
||||
data = fs.readFileSync(__dirname + '/../../../templates/plans/groups.json')
|
||||
groups = JSON.parse(data.toString())
|
||||
|
||||
capitalize = (string) ->
|
||||
string.charAt(0).toUpperCase() + string.slice(1);
|
||||
|
||||
# With group accounts in Recurly, we end up with a lot of plans to manage.
|
||||
# Rather than hand coding them in the settings file, and then needing to keep
|
||||
# that data in sync with the data in groups.json, we can auto generate the
|
||||
# group plan entries and append them to Settings.plans at boot time. This is not
|
||||
# a particularly clean pattern, since it's a little surprising that settings
|
||||
# are modified at boot-time, but I think it's a better option than trying to
|
||||
# keep two sources of data in sync.
|
||||
for usage, plan_data of groups
|
||||
for plan_code, currency_data of plan_data
|
||||
for currency, price_data of currency_data
|
||||
for size, price of price_data
|
||||
Settings.plans.push {
|
||||
planCode: "group_#{plan_code}_#{size}_#{usage}",
|
||||
name: "#{Settings.appName} #{capitalize(plan_code)} - Group Account (#{size} licenses) - #{capitalize(usage)}",
|
||||
hideFromUsers: true,
|
||||
annual: true
|
||||
features: Settings.features[plan_code]
|
||||
groupPlan: true
|
||||
membersLimit: parseInt(size)
|
||||
}
|
||||
|
||||
module.exports = groups
|
|
@ -11,6 +11,7 @@ SubscriptionDomainHandler = require("./SubscriptionDomainHandler")
|
|||
UserGetter = require "../User/UserGetter"
|
||||
FeaturesUpdater = require './FeaturesUpdater'
|
||||
planFeatures = require './planFeatures'
|
||||
GroupPlansData = require './GroupPlansData'
|
||||
|
||||
module.exports = SubscriptionController =
|
||||
|
||||
|
@ -31,6 +32,7 @@ module.exports = SubscriptionController =
|
|||
gaExperiments: Settings.gaExperiments.plansPage
|
||||
recomendedCurrency:recomendedCurrency
|
||||
planFeatures: planFeatures
|
||||
groupPlans: GroupPlansData
|
||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||
if user_id?
|
||||
UserGetter.getUser user_id, {signUpDate: 1}, (err, user) ->
|
||||
|
|
122
services/web/app/templates/plans/groups.json
Normal file
122
services/web/app/templates/plans/groups.json
Normal file
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"enterprise": {
|
||||
"collaborator": {
|
||||
"USD": {
|
||||
"2": 252,
|
||||
"3": 376,
|
||||
"4": 495,
|
||||
"5": 615,
|
||||
"10": 1170,
|
||||
"20": 2160,
|
||||
"50": 4950
|
||||
},
|
||||
"EUR": {
|
||||
"2": 235,
|
||||
"3": 352,
|
||||
"4": 468,
|
||||
"5": 584,
|
||||
"10": 1090,
|
||||
"20": 2015,
|
||||
"50": 4620
|
||||
},
|
||||
"GBP": {
|
||||
"2": 198,
|
||||
"3": 296,
|
||||
"4": 394,
|
||||
"5": 492,
|
||||
"10": 935,
|
||||
"20": 1730,
|
||||
"50": 3960
|
||||
}
|
||||
},
|
||||
"professional": {
|
||||
"USD": {
|
||||
"2": 504,
|
||||
"3": 752,
|
||||
"4": 990,
|
||||
"5": 1230,
|
||||
"10": 2340,
|
||||
"20": 4320,
|
||||
"50": 9900
|
||||
},
|
||||
"EUR": {
|
||||
"2": 470,
|
||||
"3": 704,
|
||||
"4": 936,
|
||||
"5": 1168,
|
||||
"10": 2185,
|
||||
"20": 4030,
|
||||
"50": 9240
|
||||
},
|
||||
"GBP": {
|
||||
"2": 396,
|
||||
"3": 592,
|
||||
"4": 788,
|
||||
"5": 984,
|
||||
"10": 1870,
|
||||
"20": 3455,
|
||||
"50": 7920
|
||||
}
|
||||
}
|
||||
},
|
||||
"educational": {
|
||||
"collaborator": {
|
||||
"USD": {
|
||||
"2": 252,
|
||||
"3": 376,
|
||||
"4": 495,
|
||||
"5": 615,
|
||||
"10": 695,
|
||||
"20": 1295,
|
||||
"50": 2970
|
||||
},
|
||||
"EUR": {
|
||||
"2": 235,
|
||||
"3": 352,
|
||||
"4": 468,
|
||||
"5": 584,
|
||||
"10": 655,
|
||||
"20": 1210,
|
||||
"50": 2770
|
||||
},
|
||||
"GBP": {
|
||||
"2": 198,
|
||||
"3": 296,
|
||||
"4": 394,
|
||||
"5": 492,
|
||||
"10": 560,
|
||||
"20": 1035,
|
||||
"50": 2375
|
||||
}
|
||||
},
|
||||
"professional": {
|
||||
"USD": {
|
||||
"2": 504,
|
||||
"3": 752,
|
||||
"4": 990,
|
||||
"5": 1230,
|
||||
"10": 1390,
|
||||
"20": 2590,
|
||||
"50": 5940
|
||||
},
|
||||
"EUR": {
|
||||
"2": 470,
|
||||
"3": 704,
|
||||
"4": 936,
|
||||
"5": 1168,
|
||||
"10": 1310,
|
||||
"20": 2420,
|
||||
"50": 5545
|
||||
},
|
||||
"GBP": {
|
||||
"2": 396,
|
||||
"3": 592,
|
||||
"4": 788,
|
||||
"5": 984,
|
||||
"10": 1125,
|
||||
"20": 2075,
|
||||
"50": 4750
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
script(type="text/ng-template", id="groupPlanModalTemplate")
|
||||
script(type="text/ng-template", id="groupPlanModalInquiryTemplate")
|
||||
.modal-header
|
||||
h3 #{translate("group_plan_enquiry")}
|
||||
.modal-body
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
script(type="text/ng-template", id="groupPlanModalPurchaseTemplate")
|
||||
.modal-header
|
||||
h3 Save 30% or more with a group license
|
||||
.modal-body.plans
|
||||
.container-fluid
|
||||
.row
|
||||
.col-md-6.text-center
|
||||
.circle.circle-lg
|
||||
| {{ displayPrice }}
|
||||
span.small / year
|
||||
br
|
||||
span.circle-subtext For {{ selected.size }} users
|
||||
ul.list-unstyled
|
||||
li Each user will have access to:
|
||||
li
|
||||
li(ng-if="selected.plan_code == 'collaborator'")
|
||||
strong #{translate("collabs_per_proj", {collabcount:10})}
|
||||
li(ng-if="selected.plan_code == 'professional'")
|
||||
strong #{translate("unlimited_collabs")}
|
||||
+features_premium
|
||||
.col-md-6
|
||||
form.form
|
||||
.form-group
|
||||
label(for='plan_code')
|
||||
| Plan
|
||||
select.form-control(id="plan_code", ng-model="selected.plan_code")
|
||||
option(ng-repeat="plan_code in options.plan_codes", value="{{plan_code.code}}") {{ plan_code.display }}
|
||||
.form-group
|
||||
label(for='size')
|
||||
| Number of users
|
||||
select.form-control(id="size", ng-model="selected.size")
|
||||
option(ng-repeat="size in options.sizes", value="{{size}}") {{ size }}
|
||||
.form-group
|
||||
label(for='currency')
|
||||
| Currency
|
||||
select.form-control(id="currency", ng-model="selected.currency")
|
||||
option(ng-repeat="currency in options.currencies", value="{{currency.code}}") {{ currency.display }}
|
||||
.form-group
|
||||
label(for='usage')
|
||||
| Usage
|
||||
select.form-control(id="usage", ng-model="selected.usage")
|
||||
option(ng-repeat="usage in options.usages", value="{{usage.code}}") {{ usage.display }}
|
||||
p.small.text-center.row-spaced-small(ng-show="selected.usage == 'educational'")
|
||||
| Save an additional 40% on groups of 10 or more with our educational discount
|
||||
.modal-footer
|
||||
.text-center
|
||||
button.btn.btn-primary.btn-lg(ng-click="purchase()") Purchase Now
|
||||
br
|
||||
| or
|
||||
br
|
||||
a(href, ng-click="payByInvoice()") Pay by invoice or VAT invoice
|
||||
|
|
@ -26,10 +26,10 @@ block content
|
|||
data-toggle="dropdown",
|
||||
dropdown-toggle
|
||||
)
|
||||
| {{currencyCode}} ({{plans[currencyCode]['symbol']}})
|
||||
| {{currencyCode}} ({{allCurrencies[currencyCode]['symbol']}})
|
||||
span.caret
|
||||
ul.dropdown-menu(role="menu")
|
||||
li(ng-repeat="(currency, value) in plans")
|
||||
li(ng-repeat="(currency, value) in availableCurrencies")
|
||||
a(
|
||||
ng-click="changeCurrency(currency)",
|
||||
) {{currency}} ({{value['symbol']}})
|
||||
|
@ -44,11 +44,11 @@ block content
|
|||
span !{translate("first_few_days_free", {trialLen:'{{trialLength}}'})}
|
||||
span(ng-if="discountMonths && discountRate") - {{discountMonths}} #{translate("month")}s {{discountRate}}% Off
|
||||
div(ng-if="price")
|
||||
strong {{plans[currencyCode]['symbol']}}{{price.next.total}}
|
||||
strong {{availableCurrencies[currencyCode]['symbol']}}{{price.next.total}}
|
||||
span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
|
||||
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
|
||||
div(ng-if="normalPrice")
|
||||
span.small Normally {{plans[currencyCode]['symbol']}}{{normalPrice}}
|
||||
span.small Normally {{availableCurrencies[currencyCode]['symbol']}}{{normalPrice}}
|
||||
.row
|
||||
div()
|
||||
.col-md-12()
|
||||
|
@ -188,8 +188,8 @@ block content
|
|||
div.price-breakdown(ng-if="price.next.tax !== '0.00'")
|
||||
hr.thin
|
||||
span Total:
|
||||
strong {{plans[currencyCode]['symbol']}}{{price.next.total}}
|
||||
span ({{plans[currencyCode]['symbol']}}{{price.next.subtotal}} + {{plans[currencyCode]['symbol']}}{{price.next.tax}} tax)
|
||||
strong {{availableCurrencies[currencyCode]['symbol']}}{{price.next.total}}
|
||||
span ({{availableCurrencies[currencyCode]['symbol']}}{{price.next.subtotal}} + {{availableCurrencies[currencyCode]['symbol']}}{{price.next.tax}} tax)
|
||||
span(ng-if="monthlyBilling") #{translate("every")} #{translate("month")}
|
||||
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
|
||||
hr.thin
|
||||
|
|
|
@ -8,8 +8,9 @@ block vars
|
|||
|
||||
block scripts
|
||||
script(type='text/javascript').
|
||||
window.recomendedCurrency = '#{recomendedCurrency}'
|
||||
window.abCurrencyFlag = '#{abCurrencyFlag}'
|
||||
window.recomendedCurrency = '#{recomendedCurrency}';
|
||||
window.abCurrencyFlag = '#{abCurrencyFlag}';
|
||||
window.groupPlans = !{JSON.stringify(groupPlans)};
|
||||
|
||||
block content
|
||||
.content.content-alt.content-page
|
||||
|
@ -94,7 +95,7 @@ block content
|
|||
br
|
||||
br
|
||||
a.btn.btn-default(
|
||||
href
|
||||
href="#groups"
|
||||
ng-click="openGroupPlanModal()"
|
||||
) #{translate('find_out_more')}
|
||||
|
||||
|
@ -131,3 +132,4 @@ block content
|
|||
.row.row-spaced
|
||||
|
||||
include _modal_group_inquiry
|
||||
include _modal_group_purchase
|
||||
|
|
|
@ -220,6 +220,9 @@ module.exports = settings =
|
|||
templates: true
|
||||
trackChanges: true
|
||||
|
||||
features:
|
||||
personal: defaultFeatures
|
||||
|
||||
plans: plans = [{
|
||||
planCode: "personal"
|
||||
name: "Personal"
|
||||
|
|
|
@ -1,19 +1,9 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-undef,
|
||||
no-unused-vars,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS103: Rewrite code to no longer use __guard__
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
no-return-assign
|
||||
*/
|
||||
/* global recurly,_,define */
|
||||
define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
||||
App.controller('NewSubscriptionController', function(
|
||||
$scope,
|
||||
|
@ -22,13 +12,13 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
|||
event_tracking,
|
||||
ccUtils
|
||||
) {
|
||||
let inputHasError, isFormValid, setPaymentMethod
|
||||
if (typeof recurly === 'undefined') {
|
||||
throw new Error('Recurly API Library Missing.')
|
||||
}
|
||||
|
||||
$scope.currencyCode = MultiCurrencyPricing.currencyCode
|
||||
$scope.plans = MultiCurrencyPricing.plans
|
||||
$scope.allCurrencies = MultiCurrencyPricing.plans
|
||||
$scope.availableCurrencies = {}
|
||||
$scope.planCode = window.plan_code
|
||||
|
||||
$scope.switchToStudent = function() {
|
||||
|
@ -37,9 +27,9 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
|||
event_tracking.sendMB('subscription-form-switch-to-student', {
|
||||
plan: window.plan_code
|
||||
})
|
||||
return (window.location = `/user/subscription/new?planCode=${planCode}¤cy=${
|
||||
window.location = `/user/subscription/new?planCode=${planCode}¤cy=${
|
||||
$scope.currencyCode
|
||||
}&cc=${$scope.data.coupon}`)
|
||||
}&cc=${$scope.data.coupon}`
|
||||
}
|
||||
|
||||
event_tracking.sendMB('subscription-form', { plan: window.plan_code })
|
||||
|
@ -85,7 +75,7 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
|||
const pricing = recurly.Pricing()
|
||||
window.pricing = pricing
|
||||
|
||||
const initialPricing = pricing
|
||||
pricing
|
||||
.plan(window.plan_code, { quantity: 1 })
|
||||
.address({ country: $scope.data.country })
|
||||
.tax({ tax_code: 'digital', vat_number: '' })
|
||||
|
@ -96,58 +86,41 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
|||
pricing.on('change', () => {
|
||||
$scope.planName = pricing.items.plan.name
|
||||
$scope.price = pricing.price
|
||||
$scope.trialLength =
|
||||
pricing.items.plan.trial != null
|
||||
? pricing.items.plan.trial.length
|
||||
: undefined
|
||||
if (pricing.items.plan.trial) {
|
||||
$scope.trialLength = pricing.items.plan.trial.length
|
||||
}
|
||||
$scope.monthlyBilling = pricing.items.plan.period.length === 1
|
||||
|
||||
$scope.availableCurrencies = {}
|
||||
for (let currencyCode in pricing.items.plan.price) {
|
||||
if (MultiCurrencyPricing.plans[currencyCode]) {
|
||||
$scope.availableCurrencies[currencyCode] =
|
||||
MultiCurrencyPricing.plans[currencyCode]
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
__guard__(
|
||||
__guard__(
|
||||
pricing.items != null ? pricing.items.coupon : undefined,
|
||||
x1 => x1.discount
|
||||
),
|
||||
x => x.type
|
||||
) === 'percent'
|
||||
pricing.items &&
|
||||
pricing.items.coupon &&
|
||||
pricing.items.coupon.discount &&
|
||||
pricing.items.coupon.discount.type === 'percent'
|
||||
) {
|
||||
const basePrice = parseInt(pricing.price.base.plan.unit)
|
||||
$scope.normalPrice = basePrice
|
||||
if (
|
||||
pricing.items.coupon.applies_for_months > 0 &&
|
||||
__guard__(
|
||||
pricing.items.coupon != null
|
||||
? pricing.items.coupon.discount
|
||||
: undefined,
|
||||
x2 => x2.rate
|
||||
) &&
|
||||
(pricing.items.coupon != null
|
||||
? pricing.items.coupon.applies_for_months
|
||||
: undefined) != null
|
||||
pricing.items.coupon.discount.rate &&
|
||||
pricing.items.coupon.applies_for_months
|
||||
) {
|
||||
$scope.discountMonths =
|
||||
pricing.items.coupon != null
|
||||
? pricing.items.coupon.applies_for_months
|
||||
: undefined
|
||||
$scope.discountRate =
|
||||
__guard__(
|
||||
pricing.items.coupon != null
|
||||
? pricing.items.coupon.discount
|
||||
: undefined,
|
||||
x3 => x3.rate
|
||||
) * 100
|
||||
$scope.discountMonths = pricing.items.coupon.applies_for_months
|
||||
$scope.discountRate = pricing.items.coupon.discount.rate * 100
|
||||
}
|
||||
|
||||
if (
|
||||
__guard__(
|
||||
pricing.price != null ? pricing.price.taxes[0] : undefined,
|
||||
x4 => x4.rate
|
||||
) != null
|
||||
) {
|
||||
if (pricing.price.taxes[0] && pricing.price.taxes[0].rate) {
|
||||
$scope.normalPrice += basePrice * pricing.price.taxes[0].rate
|
||||
}
|
||||
}
|
||||
return $scope.$apply()
|
||||
$scope.$apply()
|
||||
})
|
||||
|
||||
$scope.applyCoupon = () => pricing.coupon($scope.data.coupon).done()
|
||||
|
@ -162,7 +135,7 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
|||
return pricing.currency(newCurrency).done()
|
||||
}
|
||||
|
||||
$scope.inputHasError = inputHasError = function(formItem) {
|
||||
$scope.inputHasError = function(formItem) {
|
||||
if (formItem == null) {
|
||||
return false
|
||||
}
|
||||
|
@ -170,7 +143,7 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
|||
return formItem.$touched && formItem.$invalid
|
||||
}
|
||||
|
||||
$scope.isFormValid = isFormValid = function(form) {
|
||||
$scope.isFormValid = function(form) {
|
||||
if ($scope.paymentMethod.value === 'paypal') {
|
||||
return $scope.data.country !== ''
|
||||
} else {
|
||||
|
@ -181,10 +154,10 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
|||
$scope.updateCountry = () =>
|
||||
pricing.address({ country: $scope.data.country }).done()
|
||||
|
||||
$scope.setPaymentMethod = setPaymentMethod = function(method) {
|
||||
$scope.setPaymentMethod = function(method) {
|
||||
$scope.paymentMethod.value = method
|
||||
$scope.validation.errorFields = {}
|
||||
return ($scope.genericError = '')
|
||||
$scope.genericError = ''
|
||||
}
|
||||
|
||||
const completeSubscription = function(err, recurly_token_id) {
|
||||
|
@ -193,10 +166,10 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
|||
event_tracking.sendMB('subscription-error', err)
|
||||
// We may or may not be in a digest loop here depending on
|
||||
// whether recurly could do validation locally, so do it async
|
||||
return $scope.$evalAsync(function() {
|
||||
$scope.$evalAsync(function() {
|
||||
$scope.processing = false
|
||||
$scope.genericError = err.message
|
||||
return _.each(
|
||||
_.each(
|
||||
err.fields,
|
||||
field => ($scope.validation.errorFields[field] = true)
|
||||
)
|
||||
|
@ -208,11 +181,8 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
|||
subscriptionDetails: {
|
||||
currencyCode: pricing.items.currency,
|
||||
plan_code: pricing.items.plan.code,
|
||||
coupon_code:
|
||||
__guard__(
|
||||
pricing.items != null ? pricing.items.coupon : undefined,
|
||||
x => x.code
|
||||
) || '',
|
||||
coupon_code: pricing.items.coupon ? pricing.items.coupon.code : '',
|
||||
|
||||
isPaypal: $scope.paymentMethod.value === 'paypal',
|
||||
address: {
|
||||
address1: $scope.data.address1,
|
||||
|
@ -235,12 +205,11 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
|||
.post('/user/subscription/create', postData)
|
||||
.then(function() {
|
||||
event_tracking.sendMB('subscription-submission-success')
|
||||
return (window.location.href = '/user/subscription/thank-you')
|
||||
window.location.href = '/user/subscription/thank-you'
|
||||
})
|
||||
.catch(function() {
|
||||
$scope.processing = false
|
||||
return ($scope.genericError =
|
||||
'Something went wrong processing the request')
|
||||
$scope.genericError = 'Something went wrong processing the request'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -255,7 +224,7 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
|||
}
|
||||
}
|
||||
|
||||
return ($scope.countries = [
|
||||
$scope.countries = [
|
||||
{ code: 'AF', name: 'Afghanistan' },
|
||||
{ code: 'AL', name: 'Albania' },
|
||||
{ code: 'DZ', name: 'Algeria' },
|
||||
|
@ -507,10 +476,5 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
|||
{ code: 'YE', name: 'Yemen' },
|
||||
{ code: 'ZM', name: 'Zambia' },
|
||||
{ code: 'AX', name: 'Åland Islandscode:' }
|
||||
])
|
||||
]
|
||||
}))
|
||||
function __guard__(value, transform) {
|
||||
return typeof value !== 'undefined' && value !== null
|
||||
? transform(value)
|
||||
: undefined
|
||||
}
|
||||
|
|
|
@ -1,16 +1,8 @@
|
|||
/* eslint-disable
|
||||
camelcase,
|
||||
max-len,
|
||||
no-return-assign,
|
||||
no-undef,
|
||||
*/
|
||||
// TODO: This file was created by bulk-decaffeinate.
|
||||
// Fix any style issues and re-enable lint.
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
max-len
|
||||
*/
|
||||
/* global define,history */
|
||||
define(['base', 'libs/recurly-4.8.5'], function(App, recurly) {
|
||||
App.factory('MultiCurrencyPricing', function() {
|
||||
const currencyCode = window.recomendedCurrency
|
||||
|
@ -197,14 +189,15 @@ define(['base', 'libs/recurly-4.8.5'], function(App, recurly) {
|
|||
}
|
||||
})
|
||||
|
||||
return App.controller('PlansController', function(
|
||||
App.controller('PlansController', function(
|
||||
$scope,
|
||||
$modal,
|
||||
event_tracking,
|
||||
MultiCurrencyPricing,
|
||||
$http,
|
||||
$filter,
|
||||
ipCookie
|
||||
ipCookie,
|
||||
$location
|
||||
) {
|
||||
let switchEvent
|
||||
$scope.showPlans = true
|
||||
|
@ -221,7 +214,7 @@ define(['base', 'libs/recurly-4.8.5'], function(App, recurly) {
|
|||
|
||||
$scope.changeCurreny = function(e, newCurrency) {
|
||||
e.preventDefault()
|
||||
return ($scope.currencyCode = newCurrency)
|
||||
$scope.currencyCode = newCurrency
|
||||
}
|
||||
|
||||
// because ternary logic in angular bindings is hard
|
||||
|
@ -240,48 +233,139 @@ define(['base', 'libs/recurly-4.8.5'], function(App, recurly) {
|
|||
}
|
||||
plan = eventLabel(plan, location)
|
||||
event_tracking.sendMB('plans-page-start-trial')
|
||||
return event_tracking.send(
|
||||
'subscription-funnel',
|
||||
'sign_up_now_button',
|
||||
plan
|
||||
)
|
||||
event_tracking.send('subscription-funnel', 'sign_up_now_button', plan)
|
||||
}
|
||||
|
||||
$scope.switchToMonthly = function(e, location) {
|
||||
const uiView = 'monthly'
|
||||
switchEvent(e, uiView + '-prices', location)
|
||||
return ($scope.ui.view = uiView)
|
||||
$scope.ui.view = uiView
|
||||
}
|
||||
|
||||
$scope.switchToStudent = function(e, location) {
|
||||
const uiView = 'student'
|
||||
switchEvent(e, uiView + '-prices', location)
|
||||
return ($scope.ui.view = uiView)
|
||||
$scope.ui.view = uiView
|
||||
}
|
||||
|
||||
$scope.switchToAnnual = function(e, location) {
|
||||
const uiView = 'annual'
|
||||
switchEvent(e, uiView + '-prices', location)
|
||||
return ($scope.ui.view = uiView)
|
||||
$scope.ui.view = uiView
|
||||
}
|
||||
|
||||
$scope.openGroupPlanModal = function() {
|
||||
$modal.open({
|
||||
templateUrl: 'groupPlanModalTemplate'
|
||||
history.replaceState(
|
||||
null,
|
||||
document.title,
|
||||
window.location.pathname + '#groups'
|
||||
)
|
||||
$modal
|
||||
.open({
|
||||
templateUrl: 'groupPlanModalPurchaseTemplate',
|
||||
controller: 'GroupPlansModalPurchaseController'
|
||||
})
|
||||
return event_tracking.send(
|
||||
.result.finally(() =>
|
||||
history.replaceState(null, document.title, window.location.pathname)
|
||||
)
|
||||
event_tracking.send(
|
||||
'subscription-funnel',
|
||||
'plans-page',
|
||||
'group-inquiry-potential'
|
||||
)
|
||||
}
|
||||
if ($location.hash() === 'groups') {
|
||||
$scope.openGroupPlanModal()
|
||||
}
|
||||
|
||||
var eventLabel = (label, location) => label
|
||||
|
||||
return (switchEvent = function(e, label, location) {
|
||||
switchEvent = function(e, label, location) {
|
||||
e.preventDefault()
|
||||
const gaLabel = eventLabel(label, location)
|
||||
return event_tracking.send('subscription-funnel', 'plans-page', gaLabel)
|
||||
})
|
||||
event_tracking.send('subscription-funnel', 'plans-page', gaLabel)
|
||||
}
|
||||
})
|
||||
|
||||
App.controller('GroupPlansModalPurchaseController', function($scope, $modal) {
|
||||
$scope.options = {
|
||||
plan_codes: [
|
||||
{
|
||||
display: 'Collaborator',
|
||||
code: 'collaborator'
|
||||
},
|
||||
{
|
||||
display: 'Professional',
|
||||
code: 'professional'
|
||||
}
|
||||
],
|
||||
currencies: [
|
||||
{
|
||||
display: 'USD ($)',
|
||||
code: 'USD'
|
||||
},
|
||||
{
|
||||
display: 'GBP (£)',
|
||||
code: 'GBP'
|
||||
},
|
||||
{
|
||||
display: 'EUR (€)',
|
||||
code: 'EUR'
|
||||
}
|
||||
],
|
||||
currencySymbols: {
|
||||
USD: '$',
|
||||
EUR: '€',
|
||||
GBP: '£'
|
||||
},
|
||||
sizes: [2, 3, 4, 5, 10, 20, 50],
|
||||
usages: [
|
||||
{
|
||||
display: 'Enterprise',
|
||||
code: 'enterprise'
|
||||
},
|
||||
{
|
||||
display: 'Educational',
|
||||
code: 'educational'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
$scope.prices = window.groupPlans
|
||||
|
||||
let currency = 'USD'
|
||||
if (['USD', 'GBP', 'EUR'].includes(window.recomendedCurrency)) {
|
||||
currency = window.recomendedCurrency
|
||||
}
|
||||
|
||||
$scope.selected = {
|
||||
plan_code: 'collaborator',
|
||||
currency,
|
||||
size: '10',
|
||||
usage: 'educational'
|
||||
}
|
||||
|
||||
$scope.recalculatePrice = function() {
|
||||
let { usage, plan_code, currency, size } = $scope.selected
|
||||
const price = $scope.prices[usage][plan_code][currency][size]
|
||||
const currencySymbol = $scope.options.currencySymbols[currency]
|
||||
$scope.displayPrice = `${currencySymbol}${price}`
|
||||
}
|
||||
|
||||
$scope.$watch('selected', $scope.recalculatePrice, true)
|
||||
$scope.recalculatePrice()
|
||||
|
||||
$scope.purchase = function() {
|
||||
let { plan_code, size, usage, currency } = $scope.selected
|
||||
plan_code = `group_${plan_code}_${size}_${usage}`
|
||||
window.location = `/user/subscription/new?planCode=${plan_code}¤cy=${currency}`
|
||||
}
|
||||
|
||||
$scope.payByInvoice = function() {
|
||||
$modal.open({
|
||||
templateUrl: 'groupPlanModalInquiryTemplate'
|
||||
})
|
||||
$scope.$close()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -179,7 +179,9 @@ define(['base'], function(App) {
|
|||
if ($scope.subscriptionSuffix === 'free_trial_7_days') {
|
||||
$scope.subscriptionSuffix = ''
|
||||
}
|
||||
$scope.isNextGenPlan = ['heron', 'ibis'].includes($scope.subscriptionSuffix)
|
||||
$scope.isNextGenPlan =
|
||||
['heron', 'ibis'].includes($scope.subscriptionSuffix) ||
|
||||
subscription.groupPlan
|
||||
|
||||
$scope.shouldShowPlan = function(planCode) {
|
||||
let needle
|
||||
|
@ -201,7 +203,8 @@ define(['base'], function(App) {
|
|||
? subscription.planCode
|
||||
: undefined,
|
||||
x2 => x2.indexOf('ann')
|
||||
) === -1
|
||||
) === -1 &&
|
||||
!subscription.groupPlan
|
||||
const stillInFreeTrial = freeTrialInFuture && freeTrialExpiresUnderSevenDays
|
||||
|
||||
if (isMonthlyCollab && stillInFreeTrial) {
|
||||
|
|
|
@ -62,7 +62,7 @@
|
|||
.circle {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 700;
|
||||
padding: 38px 18px;
|
||||
padding: 46px 18px;
|
||||
margin: 0 auto @line-height-computed;
|
||||
text-shadow: 0 -1px 1px darken(@link-color, 10%);
|
||||
width: 120px;
|
||||
|
@ -71,11 +71,20 @@
|
|||
background-color: @brand-secondary;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
line-height: 1;
|
||||
span.small {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: @font-size-base * .8;
|
||||
}
|
||||
}
|
||||
.circle-lg {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
padding-top: 50px;
|
||||
}
|
||||
.circle-subtext {
|
||||
font-size: 1rem;
|
||||
}
|
||||
.circle-img {
|
||||
float: right;
|
||||
}
|
||||
|
|
4
services/web/scripts/recurly/Gemfile
Normal file
4
services/web/scripts/recurly/Gemfile
Normal file
|
@ -0,0 +1,4 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
gem 'recurly'
|
||||
gem 'json'
|
62
services/web/scripts/recurly/sync_recurly.rb
Normal file
62
services/web/scripts/recurly/sync_recurly.rb
Normal file
|
@ -0,0 +1,62 @@
|
|||
require 'rubygems'
|
||||
require 'recurly'
|
||||
require 'json'
|
||||
|
||||
if ENV['RECURLY_SUBDOMAIN']
|
||||
Recurly.subdomain = ENV['RECURLY_SUBDOMAIN']
|
||||
else
|
||||
print "Defaulting to sharelatex-sandbox. Set RECURLY_SUBDOMAIN environment variable to override\n"
|
||||
Recurly.subdomain = "sharelatex-sandbox"
|
||||
end
|
||||
|
||||
if ENV['RECURLY_API_KEY']
|
||||
Recurly.api_key = ENV['RECURLY_API_KEY']
|
||||
else
|
||||
print "Please set RECURLY_API_KEY environment variable\n"
|
||||
exit 1
|
||||
end
|
||||
|
||||
file = File.read('../../app/templates/plans/groups.json')
|
||||
groups = JSON.parse(file)
|
||||
# data format: groups[usage][plan_code][currency][size] = price
|
||||
|
||||
PLANS = {}
|
||||
groups.each do |usage, data|
|
||||
data.each do |plan_code, data|
|
||||
data.each do |currency, data|
|
||||
data.each do |size, price|
|
||||
full_plan_code = "group_#{plan_code}_#{size}_#{usage}"
|
||||
plan = PLANS[full_plan_code] ||= {
|
||||
plan_code: full_plan_code,
|
||||
name: "Overleaf #{plan_code.capitalize} - Group Account (#{size} licenses) - #{usage.capitalize}",
|
||||
unit_amount_in_cents: {},
|
||||
plan_interval_length: 12,
|
||||
plan_interval_unit: 'months'
|
||||
}
|
||||
plan[:unit_amount_in_cents][currency] = price * 100
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
PLANS.each do |plan_code, plan|
|
||||
print "Syncing #{plan_code}...\n"
|
||||
print "#{plan}\n"
|
||||
begin
|
||||
recurly_plan = Recurly::Plan.find(plan_code)
|
||||
rescue Recurly::Resource::NotFound => e
|
||||
recurly_plan = nil
|
||||
end
|
||||
|
||||
if recurly_plan.nil?
|
||||
print "No plan found, creating...\n"
|
||||
Recurly::Plan.create(plan)
|
||||
else
|
||||
print "Existing plan found, updating...\n"
|
||||
plan.each do |key, value|
|
||||
recurly_plan[key] = value
|
||||
recurly_plan.save
|
||||
end
|
||||
end
|
||||
print "Done!\n"
|
||||
end
|
|
@ -77,6 +77,7 @@ describe "SubscriptionController", ->
|
|||
"../User/UserGetter": @UserGetter
|
||||
"./RecurlyWrapper": @RecurlyWrapper = {}
|
||||
"./FeaturesUpdater": @FeaturesUpdater = {}
|
||||
"./GroupPlansData": @GroupPlansData = {}
|
||||
|
||||
|
||||
@res = new MockResponse()
|
||||
|
@ -135,6 +136,8 @@ describe "SubscriptionController", ->
|
|||
"../User/UserGetter": @UserGetter
|
||||
"./RecurlyWrapper": @RecurlyWrapper = {}
|
||||
"./FeaturesUpdater": @FeaturesUpdater = {}
|
||||
"./GroupPlansData": @GroupPlansData
|
||||
|
||||
@SubscriptionController.plansPage(@req, @res)
|
||||
|
||||
it 'should not fetch the current user', (done) ->
|
||||
|
|
Loading…
Reference in a new issue