Просмотр исходного кода

Interstitial Payment Page UI + Route (#8305)

GitOrigin-RevId: a31f7094db819e0dad4ff3f09c17544d40260dd4
M Fahru 4 лет назад
Родитель
Сommit
73cf4116b6

+ 24 - 0
services/web/app/src/Features/Subscription/SubscriptionController.js

@@ -21,6 +21,7 @@ const { expressify } = require('../../util/promises')
 const OError = require('@overleaf/o-error')
 const SplitTestHandler = require('../SplitTests/SplitTestHandler')
 const SubscriptionHelper = require('./SubscriptionHelper')
+const interstitialPaymentConfig = require('./interstitialPaymentConfig')
 
 const groupPlanModalOptions = Settings.groupPlanModalOptions
 const validGroupPlanModalOptions = {
@@ -225,6 +226,28 @@ async function userSubscriptionPage(req, res) {
   res.render('subscriptions/dashboard', data)
 }
 
+async function interstitialPaymentPage(req, res) {
+  const user = SessionManager.getSessionUser(req.session)
+  const { currencyCode: recommendedCurrency } =
+    await GeoIpLookup.promises.getCurrencyCode(
+      (req.query ? req.query.ip : undefined) || req.ip
+    )
+
+  const hasSubscription =
+    await LimitationsManager.promises.userHasV1OrV2Subscription(user)
+
+  if (hasSubscription) {
+    res.redirect('/user/subscription?hasSubscription=true')
+  } else {
+    res.render('subscriptions/interstitial-payment', {
+      title: 'subscribe',
+      itm_content: req.query && req.query.itm_content,
+      recommendedCurrency,
+      interstitialPaymentConfig,
+    })
+  }
+}
+
 function createSubscription(req, res, next) {
   const user = SessionManager.getSessionUser(req.session)
   const recurlyTokenIds = {
@@ -560,6 +583,7 @@ module.exports = {
   plansPage: expressify(plansPage),
   paymentPage: expressify(paymentPage),
   userSubscriptionPage: expressify(userSubscriptionPage),
+  interstitialPaymentPage: expressify(interstitialPaymentPage),
   createSubscription,
   successfulSubscription,
   cancelSubscription,

+ 6 - 0
services/web/app/src/Features/Subscription/SubscriptionRouter.js

@@ -25,6 +25,12 @@ module.exports = {
       SubscriptionController.paymentPage
     )
 
+    webRouter.get(
+      '/user/subscription/interstitial-payment',
+      AuthenticationController.requireLogin(),
+      SubscriptionController.interstitialPaymentPage
+    )
+
     webRouter.get(
       '/user/subscription/thank-you',
       AuthenticationController.requireLogin(),

+ 217 - 0
services/web/app/src/Features/Subscription/interstitialPaymentConfig.js

@@ -0,0 +1,217 @@
+const config = {
+  tableHead: {
+    individual_personal: {},
+    individual_collaborator: {},
+    individual_professional: {},
+    student_student: {
+      showExtraContent: true,
+    },
+  },
+  highlightedColumn: {
+    index: 1,
+    text: {
+      monthly: 'MOST POPULAR',
+      annual: 'MOST POPULAR',
+    },
+  },
+  showStudentsOnlyLabel: true,
+  features: [
+    {
+      divider: false,
+      items: [
+        {
+          feature: 'number_of_users',
+          info: 'number_of_users_info',
+          value: 'str',
+          plans: {
+            personal: '1 user',
+            collaborator: '1 user',
+            professional: '1 user',
+            student: '1 user',
+          },
+        },
+        {
+          feature: 'max_collab_per_project',
+          info: 'max_collab_per_project_info',
+          value: 'str',
+          plans: {
+            personal: 'You + 1',
+            collaborator: 'You + 10',
+            professional: 'Unlimited',
+            student: 'You + 6',
+          },
+        },
+      ],
+    },
+    {
+      divider: true,
+      dividerLabel: 'you_and_collaborators_get_access_to',
+      dividerInfo: 'you_and_collaborators_get_access_to_info',
+      items: [
+        {
+          feature: 'compile_timeout_short',
+          info: 'compile_timeout_short_info',
+          value: 'str',
+          plans: {
+            personal: '4 minutes',
+            collaborator: '4 minutes',
+            professional: '4 minutes',
+            student: '4 minutes',
+          },
+        },
+        {
+          feature: 'realtime_track_changes',
+          info: 'realtime_track_changes_info_v2',
+          value: 'bool',
+          plans: {
+            personal: false,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+        {
+          feature: 'full_doc_history',
+          info: 'full_doc_history_info_v2',
+          value: 'bool',
+          plans: {
+            personal: true,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+        {
+          feature: 'reference_search',
+          info: 'reference_search_info_v2',
+          value: 'bool',
+          plans: {
+            personal: true,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+        {
+          feature: 'git_integration_lowercase',
+          info: 'git_integration_lowercase_info',
+          value: 'bool',
+          plans: {
+            personal: true,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+      ],
+    },
+    {
+      divider: true,
+      dividerLabel: 'you_get_access_to',
+      dividerInfo: 'you_get_access_to_info',
+      items: [
+        {
+          feature: 'powerful_latex_editor_and_realtime_collaboration',
+          info: 'powerful_latex_editor_and_realtime_collaboration_info',
+          value: 'bool',
+          plans: {
+            personal: true,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+        {
+          feature: 'unlimited_projects',
+          info: 'unlimited_projects_info',
+          value: 'bool',
+          plans: {
+            personal: true,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+        {
+          feature: 'thousands_templates',
+          info: 'hundreds_templates_info',
+          value: 'bool',
+          plans: {
+            personal: true,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+        {
+          feature: 'symbol_palette',
+          info: 'symbol_palette_info',
+          value: 'bool',
+          plans: {
+            personal: true,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+        {
+          feature: 'github_only_integration_lowercase',
+          info: 'github_only_integration_lowercase_info',
+          value: 'bool',
+          plans: {
+            personal: true,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+        {
+          feature: 'dropbox_integration_lowercase',
+          info: 'dropbox_integration_info',
+          value: 'bool',
+          plans: {
+            personal: true,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+        {
+          feature: 'mendeley_integration_lowercase',
+          info: 'mendeley_integration_lowercase_info',
+          value: 'bool',
+          plans: {
+            personal: true,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+        {
+          feature: 'zotero_integration_lowercase',
+          info: 'zotero_integration_lowercase_info',
+          value: 'bool',
+          plans: {
+            personal: true,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+        {
+          feature: 'priority_support',
+          info: 'priority_support_info',
+          value: 'bool',
+          plans: {
+            personal: true,
+            collaborator: true,
+            professional: true,
+            student: true,
+          },
+        },
+      ],
+    },
+  ],
+}
+
+module.exports = config

+ 18 - 12
services/web/app/src/Features/Subscription/plansV2Config.js

@@ -2,12 +2,12 @@ const plansV2Features = require('./plansV2Features')
 
 const config = {
   individual: {
-    tableHead: [
-      'individual_free',
-      'individual_personal',
-      'individual_collaborator',
-      'individual_professional',
-    ],
+    tableHead: {
+      individual_free: {},
+      individual_personal: {},
+      individual_collaborator: {},
+      individual_professional: {},
+    },
     features: plansV2Features.individual,
     highlightedColumn: {
       index: 2,
@@ -18,11 +18,11 @@ const config = {
     },
   },
   group: {
-    tableHead: [
-      'group_collaborator',
-      'group_professional',
-      'group_organization',
-    ],
+    tableHead: {
+      group_collaborator: {},
+      group_professional: {},
+      group_organization: {},
+    },
     features: plansV2Features.group,
     highlightedColumn: {
       index: 1,
@@ -32,7 +32,13 @@ const config = {
     },
   },
   student: {
-    tableHead: ['student_free', 'student_student', 'student_university'],
+    tableHead: {
+      student_free: {},
+      student_student: {
+        showExtraContent: false,
+      },
+      student_university: {},
+    },
     features: plansV2Features.student,
     highlightedColumn: {
       index: 1,

+ 52 - 0
services/web/app/views/subscriptions/interstitial-payment.pug

@@ -0,0 +1,52 @@
+extends ../layout-marketing
+
+include ./plans-marketing/_mixins
+include ./plans-marketing/_tables
+include ./plans-marketing/v2/_mixins
+
+block vars
+	- entrypoint = 'pages/user/subscription/plans-v2/plans-v2-main'
+
+block append meta
+	meta(name="ol-recommendedCurrency" content=recommendedCurrency)
+	meta(name="ol-itm_content" content=itm_content)
+
+block content
+	main.content.content-alt#main-content
+		.content-page
+			.plans
+				.container
+					.row
+						.col-md-12
+							.page-header.centered.plans-header.text-centered.top-page-header
+								h1.text-capitalize #{translate('choose_your_plan')}
+
+					//- TODO: add analytics by adding 2 arguments in the mixin below
+					+monthly_annual_switch()
+
+					.row.plans-v2-table-sticky-header.plans-v2-table-sticky-header-individual.sticky(data-ol-plans-v2-table-sticky-header='individual')
+						.plans-v2-table-sticky-header-item
+							span #{translate("personal")}
+						.plans-v2-table-sticky-header-item.plans-v2-table-sticky-header-item-highlighted
+							span #{translate("standard")}
+						.plans-v2-table-sticky-header-item
+							span #{translate("professional")}
+						.plans-v2-table-sticky-header-item
+							span #{translate("student")}
+
+					.row.plans-v2-table-container(data-ol-plans-v2-table-container='monthly')
+						.col-sm-12
+							.row
+								table.card.plans-v2-table.plans-v2-table-individual
+									+plans_v2_table('monthly', interstitialPaymentConfig)
+
+					.row.plans-v2-table-container(hidden data-ol-plans-v2-table-container='annual')
+						.col-sm-12
+							.row
+								table.card.plans-v2-table.plans-v2-table-individual
+									+plans_v2_table('annual', interstitialPaymentConfig)
+
+					//- sticky header on mobile will be "hidden" (by removing its sticky position) if it reaches this div
+					.invisible(aria-hidden="true" data-ol-plans-v2-table-sticky-header-stop)
+
+	!= moduleIncludes("contactModalGeneral-marketing", locals)

+ 9 - 4
services/web/app/views/subscriptions/plans-marketing/v2/_mixins.pug

@@ -2,12 +2,14 @@ mixin plans_v2_table(period, config)
 	tr
 		th
 		- for (var i = 0; i < 4; i++)
+			- var tableHeadKey = Object.keys(config.tableHead)[i]
+			- var tableHeadOptions = Object.values(config.tableHead)[i]
 			th(
 				class=(i === config.highlightedColumn.index ? 'plans-v2-table-green-highlighted' : (i === config.highlightedColumn.index - 1 ? 'plans-v2-table-cell-before-highlighted-column' : ''))
 			)
 				if (i === config.highlightedColumn.index)
 					p.plans-v2-table-green-highlighted-text !{config.highlightedColumn.text[period]}
-				case config.tableHead[i]
+				case tableHeadKey
 					when 'individual_free'
 						+table_head_individual_free(period)
 					when 'individual_personal'
@@ -25,14 +27,14 @@ mixin plans_v2_table(period, config)
 					when 'student_free'
 						+table_head_student_free(period)
 					when 'student_student'
-						+table_head_student_student(period)
+						+table_head_student_student(period, tableHeadOptions.showExtraContent)
 					when 'student_university'
 						+table_head_student_university(period)
 
 	for featuresPerSection in config.features
 		if featuresPerSection.divider
 			tr.plans-v2-table-divider
-				td(colspan=config.tableHead.length + 1)
+				td(colspan=Object.keys(config.tableHead).length + 1)
 					div
 						b.plans-v2-table-divider-label #{translate(featuresPerSection.dividerLabel)}
 						//- will only appear on screen width >= 768px (using CSS)
@@ -220,7 +222,7 @@ mixin table_head_student_free(period)
 		.plans-v2-table-btn-buy-container-desktop
 			+btn_buy_student_free()
 
-mixin table_head_student_student(period)
+mixin table_head_student_student(period, showExtraContent)
 	div.plans-v2-table-th-content
 		p.plans-v2-table-th-content-title #{translate("student")}
 		+table_head_price('student', period)
@@ -229,6 +231,9 @@ mixin table_head_student_student(period)
 		ul.plans-v2-table-th-content-benefit
 			li !{translate("x_collaborators_per_project", {collaboratorsCount: '6'})}
 			li #{translate("all_premium_features")}
+			if showExtraContent
+				li #{translate("for_students_only")}
+
 		.plans-v2-table-btn-buy-container-desktop
 			+btn_buy_student_student(period)
 

+ 7 - 1
services/web/frontend/js/features/plans/group-plan-modal/index.js

@@ -131,7 +131,13 @@ document
     })
   })
 
-updateGroupModalPlanPricing()
+const isGroupPlanModalAvailable = document.querySelector(
+  '[data-ol-group-plan-modal]'
+)
+
+if (isGroupPlanModalAvailable) {
+  updateGroupModalPlanPricing()
+}
 
 if (window.location.hash === '#groups') {
   showGroupPlanModal()

+ 7 - 1
services/web/frontend/js/features/plans/plans-v2-group-plan-modal.js

@@ -63,4 +63,10 @@ document
   .querySelectorAll('[data-ol-group-plan-form] input')
   .forEach(el => el.addEventListener('change', changePlansV2MainPageGroupData))
 
-hideCurrencyPicker()
+const isGroupPlanModalAvailable = document.querySelector(
+  '[data-ol-group-plan-modal]'
+)
+
+if (isGroupPlanModalAvailable) {
+  hideCurrencyPicker()
+}

+ 1 - 0
services/web/locales/en.json

@@ -898,6 +898,7 @@
   "for_enterprise": "For enterprise",
   "for_universities": "For universities",
   "for_students": "For students",
+  "for_students_only": "For students only",
   "get_involved": "Get involved",
   "become_an_advisor": "Become an __appName__ advisor",
   "participate_in_user_research": "Participate in user research",

+ 33 - 0
services/web/test/unit/src/Subscription/SubscriptionControllerTests.js

@@ -220,6 +220,39 @@ describe('SubscriptionController', function () {
     })
   })
 
+  describe('interstitialPaymentPage', function () {
+    beforeEach(function () {
+      this.req.ip = '1234.3123.3131.333 313.133.445.666 653.5345.5345.534'
+      this.GeoIpLookup.promises.getCurrencyCode.resolves({
+        currencyCode: this.stubbedCurrencyCode,
+      })
+    })
+
+    describe('with a user without subscription', function () {
+      it('should render the interstitial payment page', function (done) {
+        this.res.render = (page, opts) => {
+          page.should.equal('subscriptions/interstitial-payment')
+          done()
+        }
+        this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
+      })
+    })
+
+    describe('with a user with subscription', function () {
+      it('should redirect to the subscription dashboard', function (done) {
+        this.PlansLocator.findLocalPlanInSettings.returns({})
+        this.LimitationsManager.promises.userHasV1OrV2Subscription.resolves(
+          true
+        )
+        this.res.redirect = url => {
+          url.should.equal('/user/subscription?hasSubscription=true')
+          done()
+        }
+        this.SubscriptionController.interstitialPaymentPage(this.req, this.res)
+      })
+    })
+  })
+
   describe('paymentPage', function () {
     beforeEach(function () {
       this.req.headers = {}