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"
|
UserGetter = require "../User/UserGetter"
|
||||||
FeaturesUpdater = require './FeaturesUpdater'
|
FeaturesUpdater = require './FeaturesUpdater'
|
||||||
planFeatures = require './planFeatures'
|
planFeatures = require './planFeatures'
|
||||||
|
GroupPlansData = require './GroupPlansData'
|
||||||
|
|
||||||
module.exports = SubscriptionController =
|
module.exports = SubscriptionController =
|
||||||
|
|
||||||
|
@ -31,6 +32,7 @@ module.exports = SubscriptionController =
|
||||||
gaExperiments: Settings.gaExperiments.plansPage
|
gaExperiments: Settings.gaExperiments.plansPage
|
||||||
recomendedCurrency:recomendedCurrency
|
recomendedCurrency:recomendedCurrency
|
||||||
planFeatures: planFeatures
|
planFeatures: planFeatures
|
||||||
|
groupPlans: GroupPlansData
|
||||||
user_id = AuthenticationController.getLoggedInUserId(req)
|
user_id = AuthenticationController.getLoggedInUserId(req)
|
||||||
if user_id?
|
if user_id?
|
||||||
UserGetter.getUser user_id, {signUpDate: 1}, (err, user) ->
|
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
|
.modal-header
|
||||||
h3 #{translate("group_plan_enquiry")}
|
h3 #{translate("group_plan_enquiry")}
|
||||||
.modal-body
|
.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",
|
data-toggle="dropdown",
|
||||||
dropdown-toggle
|
dropdown-toggle
|
||||||
)
|
)
|
||||||
| {{currencyCode}} ({{plans[currencyCode]['symbol']}})
|
| {{currencyCode}} ({{allCurrencies[currencyCode]['symbol']}})
|
||||||
span.caret
|
span.caret
|
||||||
ul.dropdown-menu(role="menu")
|
ul.dropdown-menu(role="menu")
|
||||||
li(ng-repeat="(currency, value) in plans")
|
li(ng-repeat="(currency, value) in availableCurrencies")
|
||||||
a(
|
a(
|
||||||
ng-click="changeCurrency(currency)",
|
ng-click="changeCurrency(currency)",
|
||||||
) {{currency}} ({{value['symbol']}})
|
) {{currency}} ({{value['symbol']}})
|
||||||
|
@ -44,11 +44,11 @@ block content
|
||||||
span !{translate("first_few_days_free", {trialLen:'{{trialLength}}'})}
|
span !{translate("first_few_days_free", {trialLen:'{{trialLength}}'})}
|
||||||
span(ng-if="discountMonths && discountRate") - {{discountMonths}} #{translate("month")}s {{discountRate}}% Off
|
span(ng-if="discountMonths && discountRate") - {{discountMonths}} #{translate("month")}s {{discountRate}}% Off
|
||||||
div(ng-if="price")
|
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("month")}
|
||||||
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
|
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
|
||||||
div(ng-if="normalPrice")
|
div(ng-if="normalPrice")
|
||||||
span.small Normally {{plans[currencyCode]['symbol']}}{{normalPrice}}
|
span.small Normally {{availableCurrencies[currencyCode]['symbol']}}{{normalPrice}}
|
||||||
.row
|
.row
|
||||||
div()
|
div()
|
||||||
.col-md-12()
|
.col-md-12()
|
||||||
|
@ -188,8 +188,8 @@ block content
|
||||||
div.price-breakdown(ng-if="price.next.tax !== '0.00'")
|
div.price-breakdown(ng-if="price.next.tax !== '0.00'")
|
||||||
hr.thin
|
hr.thin
|
||||||
span Total:
|
span Total:
|
||||||
strong {{plans[currencyCode]['symbol']}}{{price.next.total}}
|
strong {{availableCurrencies[currencyCode]['symbol']}}{{price.next.total}}
|
||||||
span ({{plans[currencyCode]['symbol']}}{{price.next.subtotal}} + {{plans[currencyCode]['symbol']}}{{price.next.tax}} tax)
|
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("month")}
|
||||||
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
|
span(ng-if="!monthlyBilling") #{translate("every")} #{translate("year")}
|
||||||
hr.thin
|
hr.thin
|
||||||
|
|
|
@ -8,8 +8,9 @@ block vars
|
||||||
|
|
||||||
block scripts
|
block scripts
|
||||||
script(type='text/javascript').
|
script(type='text/javascript').
|
||||||
window.recomendedCurrency = '#{recomendedCurrency}'
|
window.recomendedCurrency = '#{recomendedCurrency}';
|
||||||
window.abCurrencyFlag = '#{abCurrencyFlag}'
|
window.abCurrencyFlag = '#{abCurrencyFlag}';
|
||||||
|
window.groupPlans = !{JSON.stringify(groupPlans)};
|
||||||
|
|
||||||
block content
|
block content
|
||||||
.content.content-alt.content-page
|
.content.content-alt.content-page
|
||||||
|
@ -94,7 +95,7 @@ block content
|
||||||
br
|
br
|
||||||
br
|
br
|
||||||
a.btn.btn-default(
|
a.btn.btn-default(
|
||||||
href
|
href="#groups"
|
||||||
ng-click="openGroupPlanModal()"
|
ng-click="openGroupPlanModal()"
|
||||||
) #{translate('find_out_more')}
|
) #{translate('find_out_more')}
|
||||||
|
|
||||||
|
@ -131,3 +132,4 @@ block content
|
||||||
.row.row-spaced
|
.row.row-spaced
|
||||||
|
|
||||||
include _modal_group_inquiry
|
include _modal_group_inquiry
|
||||||
|
include _modal_group_purchase
|
||||||
|
|
|
@ -220,6 +220,9 @@ module.exports = settings =
|
||||||
templates: true
|
templates: true
|
||||||
trackChanges: true
|
trackChanges: true
|
||||||
|
|
||||||
|
features:
|
||||||
|
personal: defaultFeatures
|
||||||
|
|
||||||
plans: plans = [{
|
plans: plans = [{
|
||||||
planCode: "personal"
|
planCode: "personal"
|
||||||
name: "Personal"
|
name: "Personal"
|
||||||
|
|
|
@ -1,19 +1,9 @@
|
||||||
/* eslint-disable
|
/* eslint-disable
|
||||||
camelcase,
|
camelcase,
|
||||||
max-len,
|
max-len,
|
||||||
no-return-assign,
|
no-return-assign
|
||||||
no-undef,
|
|
||||||
no-unused-vars,
|
|
||||||
*/
|
*/
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
/* global recurly,_,define */
|
||||||
// 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
|
|
||||||
*/
|
|
||||||
define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
||||||
App.controller('NewSubscriptionController', function(
|
App.controller('NewSubscriptionController', function(
|
||||||
$scope,
|
$scope,
|
||||||
|
@ -22,13 +12,13 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
||||||
event_tracking,
|
event_tracking,
|
||||||
ccUtils
|
ccUtils
|
||||||
) {
|
) {
|
||||||
let inputHasError, isFormValid, setPaymentMethod
|
|
||||||
if (typeof recurly === 'undefined') {
|
if (typeof recurly === 'undefined') {
|
||||||
throw new Error('Recurly API Library Missing.')
|
throw new Error('Recurly API Library Missing.')
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.currencyCode = MultiCurrencyPricing.currencyCode
|
$scope.currencyCode = MultiCurrencyPricing.currencyCode
|
||||||
$scope.plans = MultiCurrencyPricing.plans
|
$scope.allCurrencies = MultiCurrencyPricing.plans
|
||||||
|
$scope.availableCurrencies = {}
|
||||||
$scope.planCode = window.plan_code
|
$scope.planCode = window.plan_code
|
||||||
|
|
||||||
$scope.switchToStudent = function() {
|
$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', {
|
event_tracking.sendMB('subscription-form-switch-to-student', {
|
||||||
plan: window.plan_code
|
plan: window.plan_code
|
||||||
})
|
})
|
||||||
return (window.location = `/user/subscription/new?planCode=${planCode}¤cy=${
|
window.location = `/user/subscription/new?planCode=${planCode}¤cy=${
|
||||||
$scope.currencyCode
|
$scope.currencyCode
|
||||||
}&cc=${$scope.data.coupon}`)
|
}&cc=${$scope.data.coupon}`
|
||||||
}
|
}
|
||||||
|
|
||||||
event_tracking.sendMB('subscription-form', { plan: window.plan_code })
|
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()
|
const pricing = recurly.Pricing()
|
||||||
window.pricing = pricing
|
window.pricing = pricing
|
||||||
|
|
||||||
const initialPricing = pricing
|
pricing
|
||||||
.plan(window.plan_code, { quantity: 1 })
|
.plan(window.plan_code, { quantity: 1 })
|
||||||
.address({ country: $scope.data.country })
|
.address({ country: $scope.data.country })
|
||||||
.tax({ tax_code: 'digital', vat_number: '' })
|
.tax({ tax_code: 'digital', vat_number: '' })
|
||||||
|
@ -96,58 +86,41 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
||||||
pricing.on('change', () => {
|
pricing.on('change', () => {
|
||||||
$scope.planName = pricing.items.plan.name
|
$scope.planName = pricing.items.plan.name
|
||||||
$scope.price = pricing.price
|
$scope.price = pricing.price
|
||||||
$scope.trialLength =
|
if (pricing.items.plan.trial) {
|
||||||
pricing.items.plan.trial != null
|
$scope.trialLength = pricing.items.plan.trial.length
|
||||||
? pricing.items.plan.trial.length
|
}
|
||||||
: undefined
|
|
||||||
$scope.monthlyBilling = pricing.items.plan.period.length === 1
|
$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 (
|
if (
|
||||||
__guard__(
|
pricing.items &&
|
||||||
__guard__(
|
pricing.items.coupon &&
|
||||||
pricing.items != null ? pricing.items.coupon : undefined,
|
pricing.items.coupon.discount &&
|
||||||
x1 => x1.discount
|
pricing.items.coupon.discount.type === 'percent'
|
||||||
),
|
|
||||||
x => x.type
|
|
||||||
) === 'percent'
|
|
||||||
) {
|
) {
|
||||||
const basePrice = parseInt(pricing.price.base.plan.unit)
|
const basePrice = parseInt(pricing.price.base.plan.unit)
|
||||||
$scope.normalPrice = basePrice
|
$scope.normalPrice = basePrice
|
||||||
if (
|
if (
|
||||||
pricing.items.coupon.applies_for_months > 0 &&
|
pricing.items.coupon.applies_for_months > 0 &&
|
||||||
__guard__(
|
pricing.items.coupon.discount.rate &&
|
||||||
pricing.items.coupon != null
|
pricing.items.coupon.applies_for_months
|
||||||
? pricing.items.coupon.discount
|
|
||||||
: undefined,
|
|
||||||
x2 => x2.rate
|
|
||||||
) &&
|
|
||||||
(pricing.items.coupon != null
|
|
||||||
? pricing.items.coupon.applies_for_months
|
|
||||||
: undefined) != null
|
|
||||||
) {
|
) {
|
||||||
$scope.discountMonths =
|
$scope.discountMonths = pricing.items.coupon.applies_for_months
|
||||||
pricing.items.coupon != null
|
$scope.discountRate = pricing.items.coupon.discount.rate * 100
|
||||||
? pricing.items.coupon.applies_for_months
|
|
||||||
: undefined
|
|
||||||
$scope.discountRate =
|
|
||||||
__guard__(
|
|
||||||
pricing.items.coupon != null
|
|
||||||
? pricing.items.coupon.discount
|
|
||||||
: undefined,
|
|
||||||
x3 => x3.rate
|
|
||||||
) * 100
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (pricing.price.taxes[0] && pricing.price.taxes[0].rate) {
|
||||||
__guard__(
|
|
||||||
pricing.price != null ? pricing.price.taxes[0] : undefined,
|
|
||||||
x4 => x4.rate
|
|
||||||
) != null
|
|
||||||
) {
|
|
||||||
$scope.normalPrice += basePrice * 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()
|
$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()
|
return pricing.currency(newCurrency).done()
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.inputHasError = inputHasError = function(formItem) {
|
$scope.inputHasError = function(formItem) {
|
||||||
if (formItem == null) {
|
if (formItem == null) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -170,7 +143,7 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
||||||
return formItem.$touched && formItem.$invalid
|
return formItem.$touched && formItem.$invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.isFormValid = isFormValid = function(form) {
|
$scope.isFormValid = function(form) {
|
||||||
if ($scope.paymentMethod.value === 'paypal') {
|
if ($scope.paymentMethod.value === 'paypal') {
|
||||||
return $scope.data.country !== ''
|
return $scope.data.country !== ''
|
||||||
} else {
|
} else {
|
||||||
|
@ -181,10 +154,10 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
||||||
$scope.updateCountry = () =>
|
$scope.updateCountry = () =>
|
||||||
pricing.address({ country: $scope.data.country }).done()
|
pricing.address({ country: $scope.data.country }).done()
|
||||||
|
|
||||||
$scope.setPaymentMethod = setPaymentMethod = function(method) {
|
$scope.setPaymentMethod = function(method) {
|
||||||
$scope.paymentMethod.value = method
|
$scope.paymentMethod.value = method
|
||||||
$scope.validation.errorFields = {}
|
$scope.validation.errorFields = {}
|
||||||
return ($scope.genericError = '')
|
$scope.genericError = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
const completeSubscription = function(err, recurly_token_id) {
|
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)
|
event_tracking.sendMB('subscription-error', err)
|
||||||
// We may or may not be in a digest loop here depending on
|
// We may or may not be in a digest loop here depending on
|
||||||
// whether recurly could do validation locally, so do it async
|
// whether recurly could do validation locally, so do it async
|
||||||
return $scope.$evalAsync(function() {
|
$scope.$evalAsync(function() {
|
||||||
$scope.processing = false
|
$scope.processing = false
|
||||||
$scope.genericError = err.message
|
$scope.genericError = err.message
|
||||||
return _.each(
|
_.each(
|
||||||
err.fields,
|
err.fields,
|
||||||
field => ($scope.validation.errorFields[field] = true)
|
field => ($scope.validation.errorFields[field] = true)
|
||||||
)
|
)
|
||||||
|
@ -208,11 +181,8 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
||||||
subscriptionDetails: {
|
subscriptionDetails: {
|
||||||
currencyCode: pricing.items.currency,
|
currencyCode: pricing.items.currency,
|
||||||
plan_code: pricing.items.plan.code,
|
plan_code: pricing.items.plan.code,
|
||||||
coupon_code:
|
coupon_code: pricing.items.coupon ? pricing.items.coupon.code : '',
|
||||||
__guard__(
|
|
||||||
pricing.items != null ? pricing.items.coupon : undefined,
|
|
||||||
x => x.code
|
|
||||||
) || '',
|
|
||||||
isPaypal: $scope.paymentMethod.value === 'paypal',
|
isPaypal: $scope.paymentMethod.value === 'paypal',
|
||||||
address: {
|
address: {
|
||||||
address1: $scope.data.address1,
|
address1: $scope.data.address1,
|
||||||
|
@ -235,12 +205,11 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
||||||
.post('/user/subscription/create', postData)
|
.post('/user/subscription/create', postData)
|
||||||
.then(function() {
|
.then(function() {
|
||||||
event_tracking.sendMB('subscription-submission-success')
|
event_tracking.sendMB('subscription-submission-success')
|
||||||
return (window.location.href = '/user/subscription/thank-you')
|
window.location.href = '/user/subscription/thank-you'
|
||||||
})
|
})
|
||||||
.catch(function() {
|
.catch(function() {
|
||||||
$scope.processing = false
|
$scope.processing = false
|
||||||
return ($scope.genericError =
|
$scope.genericError = 'Something went wrong processing the request'
|
||||||
'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: 'AF', name: 'Afghanistan' },
|
||||||
{ code: 'AL', name: 'Albania' },
|
{ code: 'AL', name: 'Albania' },
|
||||||
{ code: 'DZ', name: 'Algeria' },
|
{ code: 'DZ', name: 'Algeria' },
|
||||||
|
@ -507,10 +476,5 @@ define(['base', 'directives/creditCards', 'libs/recurly-4.8.5'], App =>
|
||||||
{ code: 'YE', name: 'Yemen' },
|
{ code: 'YE', name: 'Yemen' },
|
||||||
{ code: 'ZM', name: 'Zambia' },
|
{ code: 'ZM', name: 'Zambia' },
|
||||||
{ code: 'AX', name: 'Åland Islandscode:' }
|
{ code: 'AX', name: 'Åland Islandscode:' }
|
||||||
])
|
]
|
||||||
}))
|
}))
|
||||||
function __guard__(value, transform) {
|
|
||||||
return typeof value !== 'undefined' && value !== null
|
|
||||||
? transform(value)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,16 +1,8 @@
|
||||||
/* eslint-disable
|
/* eslint-disable
|
||||||
camelcase,
|
camelcase,
|
||||||
max-len,
|
max-len
|
||||||
no-return-assign,
|
|
||||||
no-undef,
|
|
||||||
*/
|
*/
|
||||||
// TODO: This file was created by bulk-decaffeinate.
|
/* global define,history */
|
||||||
// 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
|
|
||||||
*/
|
|
||||||
define(['base', 'libs/recurly-4.8.5'], function(App, recurly) {
|
define(['base', 'libs/recurly-4.8.5'], function(App, recurly) {
|
||||||
App.factory('MultiCurrencyPricing', function() {
|
App.factory('MultiCurrencyPricing', function() {
|
||||||
const currencyCode = window.recomendedCurrency
|
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,
|
$scope,
|
||||||
$modal,
|
$modal,
|
||||||
event_tracking,
|
event_tracking,
|
||||||
MultiCurrencyPricing,
|
MultiCurrencyPricing,
|
||||||
$http,
|
$http,
|
||||||
$filter,
|
$filter,
|
||||||
ipCookie
|
ipCookie,
|
||||||
|
$location
|
||||||
) {
|
) {
|
||||||
let switchEvent
|
let switchEvent
|
||||||
$scope.showPlans = true
|
$scope.showPlans = true
|
||||||
|
@ -221,7 +214,7 @@ define(['base', 'libs/recurly-4.8.5'], function(App, recurly) {
|
||||||
|
|
||||||
$scope.changeCurreny = function(e, newCurrency) {
|
$scope.changeCurreny = function(e, newCurrency) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
return ($scope.currencyCode = newCurrency)
|
$scope.currencyCode = newCurrency
|
||||||
}
|
}
|
||||||
|
|
||||||
// because ternary logic in angular bindings is hard
|
// 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)
|
plan = eventLabel(plan, location)
|
||||||
event_tracking.sendMB('plans-page-start-trial')
|
event_tracking.sendMB('plans-page-start-trial')
|
||||||
return event_tracking.send(
|
event_tracking.send('subscription-funnel', 'sign_up_now_button', plan)
|
||||||
'subscription-funnel',
|
|
||||||
'sign_up_now_button',
|
|
||||||
plan
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.switchToMonthly = function(e, location) {
|
$scope.switchToMonthly = function(e, location) {
|
||||||
const uiView = 'monthly'
|
const uiView = 'monthly'
|
||||||
switchEvent(e, uiView + '-prices', location)
|
switchEvent(e, uiView + '-prices', location)
|
||||||
return ($scope.ui.view = uiView)
|
$scope.ui.view = uiView
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.switchToStudent = function(e, location) {
|
$scope.switchToStudent = function(e, location) {
|
||||||
const uiView = 'student'
|
const uiView = 'student'
|
||||||
switchEvent(e, uiView + '-prices', location)
|
switchEvent(e, uiView + '-prices', location)
|
||||||
return ($scope.ui.view = uiView)
|
$scope.ui.view = uiView
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.switchToAnnual = function(e, location) {
|
$scope.switchToAnnual = function(e, location) {
|
||||||
const uiView = 'annual'
|
const uiView = 'annual'
|
||||||
switchEvent(e, uiView + '-prices', location)
|
switchEvent(e, uiView + '-prices', location)
|
||||||
return ($scope.ui.view = uiView)
|
$scope.ui.view = uiView
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.openGroupPlanModal = function() {
|
$scope.openGroupPlanModal = function() {
|
||||||
$modal.open({
|
history.replaceState(
|
||||||
templateUrl: 'groupPlanModalTemplate'
|
null,
|
||||||
})
|
document.title,
|
||||||
return event_tracking.send(
|
window.location.pathname + '#groups'
|
||||||
|
)
|
||||||
|
$modal
|
||||||
|
.open({
|
||||||
|
templateUrl: 'groupPlanModalPurchaseTemplate',
|
||||||
|
controller: 'GroupPlansModalPurchaseController'
|
||||||
|
})
|
||||||
|
.result.finally(() =>
|
||||||
|
history.replaceState(null, document.title, window.location.pathname)
|
||||||
|
)
|
||||||
|
event_tracking.send(
|
||||||
'subscription-funnel',
|
'subscription-funnel',
|
||||||
'plans-page',
|
'plans-page',
|
||||||
'group-inquiry-potential'
|
'group-inquiry-potential'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if ($location.hash() === 'groups') {
|
||||||
|
$scope.openGroupPlanModal()
|
||||||
|
}
|
||||||
|
|
||||||
var eventLabel = (label, location) => label
|
var eventLabel = (label, location) => label
|
||||||
|
|
||||||
return (switchEvent = function(e, label, location) {
|
switchEvent = function(e, label, location) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const gaLabel = eventLabel(label, location)
|
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') {
|
if ($scope.subscriptionSuffix === 'free_trial_7_days') {
|
||||||
$scope.subscriptionSuffix = ''
|
$scope.subscriptionSuffix = ''
|
||||||
}
|
}
|
||||||
$scope.isNextGenPlan = ['heron', 'ibis'].includes($scope.subscriptionSuffix)
|
$scope.isNextGenPlan =
|
||||||
|
['heron', 'ibis'].includes($scope.subscriptionSuffix) ||
|
||||||
|
subscription.groupPlan
|
||||||
|
|
||||||
$scope.shouldShowPlan = function(planCode) {
|
$scope.shouldShowPlan = function(planCode) {
|
||||||
let needle
|
let needle
|
||||||
|
@ -201,7 +203,8 @@ define(['base'], function(App) {
|
||||||
? subscription.planCode
|
? subscription.planCode
|
||||||
: undefined,
|
: undefined,
|
||||||
x2 => x2.indexOf('ann')
|
x2 => x2.indexOf('ann')
|
||||||
) === -1
|
) === -1 &&
|
||||||
|
!subscription.groupPlan
|
||||||
const stillInFreeTrial = freeTrialInFuture && freeTrialExpiresUnderSevenDays
|
const stillInFreeTrial = freeTrialInFuture && freeTrialExpiresUnderSevenDays
|
||||||
|
|
||||||
if (isMonthlyCollab && stillInFreeTrial) {
|
if (isMonthlyCollab && stillInFreeTrial) {
|
||||||
|
|
|
@ -62,7 +62,7 @@
|
||||||
.circle {
|
.circle {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
padding: 38px 18px;
|
padding: 46px 18px;
|
||||||
margin: 0 auto @line-height-computed;
|
margin: 0 auto @line-height-computed;
|
||||||
text-shadow: 0 -1px 1px darken(@link-color, 10%);
|
text-shadow: 0 -1px 1px darken(@link-color, 10%);
|
||||||
width: 120px;
|
width: 120px;
|
||||||
|
@ -71,11 +71,20 @@
|
||||||
background-color: @brand-secondary;
|
background-color: @brand-secondary;
|
||||||
color: white;
|
color: white;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
line-height: 1;
|
||||||
span.small {
|
span.small {
|
||||||
color: rgba(255, 255, 255, 0.85);
|
color: rgba(255, 255, 255, 0.85);
|
||||||
font-size: @font-size-base * .8;
|
font-size: @font-size-base * .8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.circle-lg {
|
||||||
|
width: 150px;
|
||||||
|
height: 150px;
|
||||||
|
padding-top: 50px;
|
||||||
|
}
|
||||||
|
.circle-subtext {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
.circle-img {
|
.circle-img {
|
||||||
float: right;
|
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
|
"../User/UserGetter": @UserGetter
|
||||||
"./RecurlyWrapper": @RecurlyWrapper = {}
|
"./RecurlyWrapper": @RecurlyWrapper = {}
|
||||||
"./FeaturesUpdater": @FeaturesUpdater = {}
|
"./FeaturesUpdater": @FeaturesUpdater = {}
|
||||||
|
"./GroupPlansData": @GroupPlansData = {}
|
||||||
|
|
||||||
|
|
||||||
@res = new MockResponse()
|
@res = new MockResponse()
|
||||||
|
@ -135,6 +136,8 @@ describe "SubscriptionController", ->
|
||||||
"../User/UserGetter": @UserGetter
|
"../User/UserGetter": @UserGetter
|
||||||
"./RecurlyWrapper": @RecurlyWrapper = {}
|
"./RecurlyWrapper": @RecurlyWrapper = {}
|
||||||
"./FeaturesUpdater": @FeaturesUpdater = {}
|
"./FeaturesUpdater": @FeaturesUpdater = {}
|
||||||
|
"./GroupPlansData": @GroupPlansData
|
||||||
|
|
||||||
@SubscriptionController.plansPage(@req, @res)
|
@SubscriptionController.plansPage(@req, @res)
|
||||||
|
|
||||||
it 'should not fetch the current user', (done) ->
|
it 'should not fetch the current user', (done) ->
|
||||||
|
|
Loading…
Reference in a new issue