subscription-dashboard.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. import _ from 'lodash'
  2. /* global recurly */
  3. /* eslint-disable
  4. camelcase,
  5. max-len,
  6. no-return-assign,
  7. no-unused-vars,
  8. */
  9. // TODO: This file was created by bulk-decaffeinate.
  10. // Fix any style issues and re-enable lint.
  11. /*
  12. * decaffeinate suggestions:
  13. * DS102: Remove unnecessary code created because of implicit returns
  14. * DS104: Avoid inline assignments
  15. * DS204: Change includes calls to have a more natural evaluation order
  16. * DS207: Consider shorter variations of null checks
  17. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  18. */
  19. import App from '../base'
  20. import getMeta from '../utils/meta'
  21. const SUBSCRIPTION_URL = '/user/subscription/update'
  22. const GROUP_PLAN_MODAL_OPTIONS = getMeta('ol-groupPlanModalOptions')
  23. const ensureRecurlyIsSetup = _.once(() => {
  24. if (typeof recurly === 'undefined' || !recurly) {
  25. return false
  26. }
  27. recurly.configure(getMeta('ol-recurlyApiKey'))
  28. return true
  29. })
  30. function getPricePerUser(price, currencySymbol, size) {
  31. let perUserPrice = price / size
  32. if (perUserPrice % 1 !== 0) {
  33. perUserPrice = perUserPrice.toFixed(2)
  34. }
  35. return `${currencySymbol}${perUserPrice}`
  36. }
  37. App.controller('MetricsEmailController', function ($scope, $http) {
  38. $scope.institutionEmailSubscription = function (institutionId) {
  39. const inst = _.find(window.managedInstitutions, function (institution) {
  40. return institution.v1Id === parseInt(institutionId)
  41. })
  42. if (inst.metricsEmail.optedOutUserIds.includes(window.user_id)) {
  43. return 'Subscribe'
  44. } else {
  45. return 'Unsubscribe'
  46. }
  47. }
  48. $scope.changeInstitutionalEmailSubscription = function (institutionId) {
  49. $scope.subscriptionChanging = true
  50. return $http({
  51. method: 'POST',
  52. url: `/institutions/${institutionId}/emailSubscription`,
  53. headers: {
  54. 'X-CSRF-Token': window.csrfToken,
  55. },
  56. }).then(function successCallback(response) {
  57. window.managedInstitutions = _.map(
  58. window.managedInstitutions,
  59. function (institution) {
  60. if (institution.v1Id === parseInt(institutionId)) {
  61. institution.metricsEmail.optedOutUserIds = response.data
  62. }
  63. return institution
  64. }
  65. )
  66. $scope.subscriptionChanging = false
  67. })
  68. }
  69. })
  70. App.factory('RecurlyPricing', function ($q, MultiCurrencyPricing) {
  71. return {
  72. loadDisplayPriceWithTax: function (planCode, currency, taxRate) {
  73. if (!ensureRecurlyIsSetup()) return
  74. const currencySymbol = MultiCurrencyPricing.plans[currency].symbol
  75. const pricing = recurly.Pricing()
  76. return $q(function (resolve, reject) {
  77. pricing
  78. .plan(planCode, { quantity: 1 })
  79. .currency(currency)
  80. .done(function (price) {
  81. const totalPriceExTax = parseFloat(price.next.total)
  82. let taxAmount = totalPriceExTax * taxRate
  83. if (isNaN(taxAmount)) {
  84. taxAmount = 0
  85. }
  86. let total = totalPriceExTax + taxAmount
  87. if (total % 1 !== 0) {
  88. total = total.toFixed(2)
  89. }
  90. resolve({
  91. total: `${currencySymbol}${total}`,
  92. totalValue: total,
  93. subtotal: `${currencySymbol}${totalPriceExTax.toFixed(2)}`,
  94. tax: `${currencySymbol}${taxAmount.toFixed(2)}`,
  95. includesTax: taxAmount !== 0,
  96. })
  97. })
  98. })
  99. },
  100. }
  101. })
  102. App.controller('ChangePlanToGroupFormController', function ($scope, $modal) {
  103. if (!ensureRecurlyIsSetup()) return
  104. const subscription = getMeta('ol-subscription')
  105. const currency = subscription.recurly.currency
  106. const validCurrencies = GROUP_PLAN_MODAL_OPTIONS.currencies.map(
  107. item => item.code
  108. )
  109. if (validCurrencies.includes(currency)) {
  110. $scope.isValidCurrencyForUpgrade = true
  111. }
  112. $scope.openGroupPlanModal = function () {
  113. const planCode = subscription.plan.planCode
  114. $scope.defaultGroupPlan = planCode.includes('professional')
  115. ? 'professional'
  116. : 'collaborator'
  117. $scope.currentPlanCurrency = currency
  118. $modal.open({
  119. templateUrl: 'groupPlanModalUpgradeTemplate',
  120. controller: 'GroupPlansModalUpgradeController',
  121. scope: $scope,
  122. })
  123. }
  124. })
  125. App.controller(
  126. 'GroupPlansModalUpgradeController',
  127. function ($scope, $modal, $location, $http, RecurlyPricing) {
  128. $scope.options = GROUP_PLAN_MODAL_OPTIONS
  129. $scope.groupPlans = getMeta('ol-groupPlans')
  130. const currency = $scope.currentPlanCurrency
  131. // default selected
  132. $scope.selected = {
  133. plan_code: $scope.defaultGroupPlan || 'collaborator',
  134. currency,
  135. size: '10',
  136. usage: 'enterprise',
  137. }
  138. $scope.recalculatePrice = function () {
  139. const subscription = getMeta('ol-subscription')
  140. const { taxRate } = subscription.recurly
  141. const { usage, plan_code, currency, size } = $scope.selected
  142. $scope.discountEligible = size >= 10
  143. const recurlyPricePlaceholder = { total: '...' }
  144. let perUserDisplayPricePlaceholder = '...'
  145. const currencySymbol = $scope.options.currencySymbols[currency]
  146. if (taxRate === 0) {
  147. const basePriceInCents =
  148. $scope.groupPlans[usage][plan_code][currency][size].price_in_cents
  149. const basePriceInUnit = (basePriceInCents / 100).toFixed()
  150. recurlyPricePlaceholder.total = `${currencySymbol}${basePriceInUnit}`
  151. perUserDisplayPricePlaceholder = getPricePerUser(
  152. basePriceInUnit,
  153. currencySymbol,
  154. size
  155. )
  156. }
  157. $scope.recurlyPrice = recurlyPricePlaceholder // Placeholder while we talk to recurly
  158. $scope.perUserDisplayPrice = perUserDisplayPricePlaceholder // Placeholder while we talk to recurly
  159. const recurlyPlanCode = `group_${plan_code}_${size}_${usage}`
  160. RecurlyPricing.loadDisplayPriceWithTax(
  161. recurlyPlanCode,
  162. currency,
  163. taxRate
  164. ).then(price => {
  165. $scope.recurlyPrice = price
  166. $scope.perUserDisplayPrice = getPricePerUser(
  167. price.totalValue,
  168. currencySymbol,
  169. size
  170. )
  171. })
  172. }
  173. $scope.$watch('selected', $scope.recalculatePrice, true)
  174. $scope.recalculatePrice()
  175. $scope.upgrade = function () {
  176. const { plan_code, size, usage } = $scope.selected
  177. const body = {
  178. _csrf: window.csrfToken,
  179. plan_code: `group_${plan_code}_${size}_${usage}`,
  180. }
  181. $scope.inflight = true
  182. $http
  183. .post(`/user/subscription/update`, body)
  184. .then(() => location.reload())
  185. }
  186. }
  187. )
  188. App.controller(
  189. 'ChangePlanFormController',
  190. function ($scope, $modal, RecurlyPricing) {
  191. if (!ensureRecurlyIsSetup()) return
  192. function stripCentsIfZero(displayPrice) {
  193. return displayPrice ? displayPrice.replace(/\.00$/, '') : '...'
  194. }
  195. $scope.changePlan = () =>
  196. $modal.open({
  197. templateUrl: 'confirmChangePlanModalTemplate',
  198. controller: 'ConfirmChangePlanController',
  199. scope: $scope,
  200. })
  201. $scope.cancelPendingPlanChange = () =>
  202. $modal.open({
  203. templateUrl: 'cancelPendingPlanChangeModalTemplate',
  204. controller: 'CancelPendingPlanChangeController',
  205. scope: $scope,
  206. })
  207. $scope.$watch('plan', function (plan) {
  208. if (!plan) return
  209. const planCodesChangingAtTermEnd = getMeta(
  210. 'ol-planCodesChangingAtTermEnd'
  211. )
  212. $scope.planChangesAtTermEnd = false
  213. if (
  214. planCodesChangingAtTermEnd &&
  215. planCodesChangingAtTermEnd.indexOf(plan.planCode) > -1
  216. ) {
  217. $scope.planChangesAtTermEnd = true
  218. }
  219. const planCode = plan.planCode
  220. const subscription = getMeta('ol-subscription')
  221. const { currency, taxRate } = subscription.recurly
  222. if (subscription.recurly.displayPrice) {
  223. if (subscription.pendingPlan?.planCode === planCode) {
  224. $scope.displayPrice = stripCentsIfZero(
  225. subscription.recurly.displayPrice
  226. )
  227. return
  228. }
  229. if (subscription.planCode === planCode) {
  230. if (subscription.pendingPlan) {
  231. $scope.displayPrice = stripCentsIfZero(
  232. subscription.recurly.currentPlanDisplayPrice
  233. )
  234. } else {
  235. $scope.displayPrice = stripCentsIfZero(
  236. subscription.recurly.displayPrice
  237. )
  238. }
  239. return
  240. }
  241. }
  242. $scope.displayPrice = '...' // Placeholder while we talk to recurly
  243. RecurlyPricing.loadDisplayPriceWithTax(planCode, currency, taxRate).then(
  244. recurlyPrice => {
  245. $scope.displayPrice = recurlyPrice.total
  246. }
  247. )
  248. })
  249. }
  250. )
  251. App.controller(
  252. 'ConfirmChangePlanController',
  253. function ($scope, $modalInstance, $http) {
  254. $scope.confirmChangePlan = function () {
  255. const body = {
  256. plan_code: $scope.plan.planCode,
  257. _csrf: window.csrfToken,
  258. }
  259. $scope.genericError = false
  260. $scope.inflight = true
  261. return $http
  262. .post(`${SUBSCRIPTION_URL}?origin=confirmChangePlan`, body)
  263. .then(() => location.reload())
  264. .catch(() => {
  265. $scope.genericError = true
  266. $scope.inflight = false
  267. })
  268. }
  269. return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
  270. }
  271. )
  272. App.controller(
  273. 'CancelPendingPlanChangeController',
  274. function ($scope, $modalInstance, $http) {
  275. $scope.confirmCancelPendingPlanChange = function () {
  276. const body = {
  277. _csrf: window.csrfToken,
  278. }
  279. $scope.genericError = false
  280. $scope.inflight = true
  281. return $http
  282. .post('/user/subscription/cancel-pending', body)
  283. .then(() => location.reload())
  284. .catch(() => {
  285. $scope.genericError = true
  286. $scope.inflight = false
  287. })
  288. }
  289. return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
  290. }
  291. )
  292. App.controller(
  293. 'LeaveGroupModalController',
  294. function ($scope, $modalInstance, $http) {
  295. $scope.confirmLeaveGroup = function () {
  296. $scope.inflight = true
  297. return $http({
  298. url: '/subscription/group/user',
  299. method: 'DELETE',
  300. params: {
  301. subscriptionId: $scope.subscriptionId,
  302. _csrf: window.csrfToken,
  303. },
  304. })
  305. .then(() => location.reload())
  306. .catch(() => console.log('something went wrong changing plan'))
  307. }
  308. return ($scope.cancel = () => $modalInstance.dismiss('cancel'))
  309. }
  310. )
  311. App.controller('GroupMembershipController', function ($scope, $modal) {
  312. $scope.removeSelfFromGroup = function (subscriptionId) {
  313. $scope.subscriptionId = subscriptionId
  314. return $modal.open({
  315. templateUrl: 'LeaveGroupModalTemplate',
  316. controller: 'LeaveGroupModalController',
  317. scope: $scope,
  318. })
  319. }
  320. })
  321. App.controller('RecurlySubscriptionController', function ($scope) {
  322. const recurlyIsSetup = ensureRecurlyIsSetup()
  323. const subscription = getMeta('ol-subscription')
  324. $scope.showChangePlanButton = recurlyIsSetup && !subscription.groupPlan
  325. if (
  326. window.subscription.recurly.account.has_past_due_invoice &&
  327. window.subscription.recurly.account.has_past_due_invoice._ === 'true'
  328. ) {
  329. $scope.showChangePlanButton = false
  330. }
  331. $scope.recurlyLoadError = !recurlyIsSetup
  332. $scope.switchToDefaultView = () => {
  333. $scope.showCancellation = false
  334. $scope.showChangePlan = false
  335. }
  336. $scope.switchToDefaultView()
  337. $scope.switchToCancellationView = () => {
  338. $scope.showCancellation = true
  339. $scope.showChangePlan = false
  340. }
  341. $scope.switchToChangePlanView = () => {
  342. $scope.showCancellation = false
  343. $scope.showChangePlan = true
  344. }
  345. })
  346. App.controller(
  347. 'RecurlyCancellationController',
  348. function ($scope, RecurlyPricing, $http) {
  349. if (!ensureRecurlyIsSetup()) return
  350. const subscription = getMeta('ol-subscription')
  351. const sevenDaysTime = new Date()
  352. sevenDaysTime.setDate(sevenDaysTime.getDate() + 7)
  353. const freeTrialEndDate = new Date(subscription.recurly.trial_ends_at)
  354. const freeTrialInFuture = freeTrialEndDate > new Date()
  355. const freeTrialExpiresUnderSevenDays = freeTrialEndDate < sevenDaysTime
  356. const isMonthlyCollab =
  357. subscription.plan.planCode.indexOf('collaborator') !== -1 &&
  358. subscription.plan.planCode.indexOf('ann') === -1 &&
  359. !subscription.groupPlan
  360. const stillInFreeTrial = freeTrialInFuture && freeTrialExpiresUnderSevenDays
  361. if (isMonthlyCollab && stillInFreeTrial) {
  362. $scope.showExtendFreeTrial = true
  363. } else if (isMonthlyCollab && !stillInFreeTrial) {
  364. $scope.showDowngradeToStudent = true
  365. } else {
  366. $scope.showBasicCancel = true
  367. }
  368. const { currency, taxRate } = subscription.recurly
  369. $scope.studentDisplayPrice = '...' // Placeholder while we talk to recurly
  370. RecurlyPricing.loadDisplayPriceWithTax('student', currency, taxRate).then(
  371. price => {
  372. $scope.studentDisplayPrice = price.total
  373. }
  374. )
  375. $scope.downgradeToStudent = function () {
  376. const body = {
  377. plan_code: 'student',
  378. _csrf: window.csrfToken,
  379. }
  380. $scope.inflight = true
  381. return $http
  382. .post(`${SUBSCRIPTION_URL}?origin=downgradeToStudent`, body)
  383. .then(() => location.reload())
  384. .catch(() => console.log('something went wrong changing plan'))
  385. }
  386. $scope.cancelSubscription = function () {
  387. const body = { _csrf: window.csrfToken }
  388. $scope.inflight = true
  389. return $http
  390. .post('/user/subscription/cancel', body)
  391. .then(() => (location.href = '/user/subscription/canceled'))
  392. .catch(() => console.log('something went wrong changing plan'))
  393. }
  394. $scope.extendTrial = function () {
  395. const body = { _csrf: window.csrfToken }
  396. $scope.inflight = true
  397. return $http
  398. .put('/user/subscription/extend', body)
  399. .then(() => location.reload())
  400. .catch(() => console.log('something went wrong changing plan'))
  401. }
  402. }
  403. )