Parcourir la source

Merge pull request #11231 from overleaf/jel-react-subscriptions-dash-commons

[web] Migrate institution memberships dash to React

GitOrigin-RevId: 75bb8e7d7338418733a836a8e654fe5b0a9fceff
Jessica Lawshe il y a 3 ans
Parent
commit
c8ef5e6f65

+ 1 - 0
services/web/app/views/subscriptions/dashboard-react.pug

@@ -6,6 +6,7 @@ block entrypointVar
 block append meta
 	meta(name="ol-managedInstitutions", data-type="json", content=managedInstitutions)
 	meta(name="ol-planCodesChangingAtTermEnd", data-type="json", content=plans.planCodesChangingAtTermEnd)
+	meta(name="ol-currentInstitutionsWithLicence", data-type="json" content=currentInstitutionsWithLicence)
 	if (personalSubscription && personalSubscription.recurly)
 		meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
 		meta(name="ol-subscription" data-type="json" content=personalSubscription)

+ 4 - 0
services/web/frontend/extracted-translations.json

@@ -252,6 +252,8 @@
   "generic_linked_file_compile_error": "",
   "generic_something_went_wrong": "",
   "get_collaborative_benefits": "",
+  "get_most_subscription_by_checking_features": "",
+  "get_most_subscription_by_checking_premium_features": "",
   "git": "",
   "git_bridge_modal_description": "",
   "github_commit_message_placeholder": "",
@@ -745,12 +747,14 @@
   "word_count": "",
   "work_offline": "",
   "work_with_non_overleaf_users": "",
+  "you_are_on_x_plan_as_a_confirmed_member_of_institution_y": "",
   "you_can_now_log_in_sso": "",
   "you_dont_have_any_repositories": "",
   "your_affiliation_is_confirmed": "",
   "your_browser_does_not_support_this_feature": "",
   "your_message_to_collaborators": "",
   "your_projects": "",
+  "your_subscription": "",
   "zotero_groups_loading_error": "",
   "zotero_groups_relink": "",
   "zotero_integration": "",

+ 51 - 0
services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx

@@ -0,0 +1,51 @@
+import { Trans } from 'react-i18next'
+import { Institution } from '../../../../../../types/institution'
+
+type InstitutionMembershipsProps = {
+  memberships?: Array<Institution>
+}
+
+function InstitutionMemberships({ memberships }: InstitutionMembershipsProps) {
+  // memberships is undefined when data failed to load. If user has no memberships, then an empty array is returned
+
+  if (!memberships) {
+    return (
+      <div className="alert alert-warning">
+        <p>
+          Sorry, something went wrong. Subscription information related to
+          institutional affiliations may not be displayed. Please try again
+          later.
+        </p>
+      </div>
+    )
+  }
+
+  return (
+    <>
+      <div>
+        {memberships.map((institution: Institution) => (
+          <div key={`${institution.id}`}>
+            <Trans
+              i18nKey="you_are_on_x_plan_as_a_confirmed_member_of_institution_y"
+              values={{
+                planName: 'Professional',
+                institutionName: institution.name || '',
+              }}
+              components={[
+                // eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
+                <a href="/user/subscription/plans" rel="noopener" />,
+                // eslint-disable-next-line react/jsx-key
+                <strong />,
+                // eslint-disable-next-line react/jsx-key
+                <strong />,
+              ]}
+            />
+            <hr />
+          </div>
+        ))}
+      </div>
+    </>
+  )
+}
+
+export default InstitutionMemberships

+ 46 - 0
services/web/frontend/js/features/subscription/components/dashboard/premium-features-link.tsx

@@ -0,0 +1,46 @@
+import { Trans } from 'react-i18next'
+import getMeta from '../../../../utils/meta'
+import * as eventTracking from '../../../../infrastructure/event-tracking'
+
+function PremiumFeaturesLink() {
+  const featuresPageVariant =
+    getMeta('ol-splitTestVariants')?.['features-page'] || 'default'
+
+  function handleLinkClick() {
+    eventTracking.sendMB('features-page-link', {
+      splitTest: 'features-page',
+      splitTestVariant: featuresPageVariant,
+    })
+  }
+
+  const featuresPageLink = (
+    // translation adds content
+    // eslint-disable-next-line jsx-a11y/anchor-has-content
+    <a
+      href={
+        featuresPageVariant === 'new'
+          ? '/about/features-overview'
+          : '/learn/how-to/Overleaf_premium_features'
+      }
+      onClick={handleLinkClick}
+    />
+  )
+
+  if (featuresPageVariant === 'new') {
+    return (
+      <Trans
+        i18nKey="get_most_subscription_by_checking_features"
+        components={[featuresPageLink]}
+      />
+    )
+  }
+
+  return (
+    <Trans
+      i18nKey="get_most_subscription_by_checking_premium_features"
+      components={[featuresPageLink]}
+    />
+  )
+}
+
+export default PremiumFeaturesLink

+ 2 - 1
services/web/frontend/js/features/subscription/components/dashboard/root.tsx

@@ -1,4 +1,5 @@
 import useWaitForI18n from '../../../../shared/hooks/use-wait-for-i18n'
+import SubscriptionDashboard from './subscription-dashboard'
 
 function Root() {
   const { isReady } = useWaitForI18n()
@@ -7,7 +8,7 @@ function Root() {
     return null
   }
 
-  return <h2>React Subscription Dashboard</h2>
+  return <SubscriptionDashboard />
 }
 
 export default Root

+ 29 - 0
services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx

@@ -0,0 +1,29 @@
+import { useTranslation } from 'react-i18next'
+import getMeta from '../../../../utils/meta'
+import InstitutionMemberships from './institution-memberships'
+import PremiumFeaturesLink from './premium-features-link'
+
+function SubscriptionDashboard() {
+  const { t } = useTranslation()
+  const institutionMemberships = getMeta('ol-currentInstitutionsWithLicence')
+  const hasDisplayedSubscription = institutionMemberships?.length > 0
+
+  return (
+    <div className="container">
+      <div className="row">
+        <div className="col-md-8 col-md-offset-2">
+          <div className="card">
+            <div className="page-header">
+              <h1>{t('your_subscription')}</h1>
+            </div>
+
+            <InstitutionMemberships memberships={institutionMemberships} />
+            {hasDisplayedSubscription ? <PremiumFeaturesLink /> : <></>}
+          </div>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default SubscriptionDashboard

+ 58 - 0
services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx

@@ -0,0 +1,58 @@
+import { expect } from 'chai'
+import { render, screen } from '@testing-library/react'
+import InstitutionMemberships from '../../../../../../frontend/js/features/subscription/components/dashboard/institution-memberships'
+
+const memberships = [
+  {
+    id: 9258,
+    name: 'Test University',
+    commonsAccount: true,
+    isUniversity: true,
+    confirmed: true,
+    ssoBeta: false,
+    ssoEnabled: false,
+    maxConfirmationMonths: 6,
+  },
+  {
+    id: 9259,
+    name: 'Example Institution',
+    commonsAccount: true,
+    isUniversity: true,
+    confirmed: true,
+    ssoBeta: false,
+    ssoEnabled: true,
+    maxConfirmationMonths: 12,
+  },
+]
+
+describe('<InstitutionMemberships />', function () {
+  beforeEach(function () {
+    window.metaAttributesCache = new Map()
+  })
+
+  afterEach(function () {
+    window.metaAttributesCache = new Map()
+  })
+
+  it('renders all insitutions with license', function () {
+    render(<InstitutionMemberships memberships={memberships} />)
+
+    const elements = screen.getAllByText('You are on our', {
+      exact: false,
+    })
+    expect(elements.length).to.equal(2)
+    expect(elements[0].textContent).to.equal(
+      'You are on our Professional plan as a confirmed member of Test University'
+    )
+    expect(elements[1].textContent).to.equal(
+      'You are on our Professional plan as a confirmed member of Example Institution'
+    )
+  })
+
+  it('renders error message when failed to check commons licenses', function () {
+    render(<InstitutionMemberships memberships={undefined} />)
+    screen.getByText(
+      'Sorry, something went wrong. Subscription information related to institutional affiliations may not be displayed. Please try again later.'
+    )
+  })
+})

+ 59 - 0
services/web/test/frontend/features/subscription/components/dashboard/premium-features-link.test.tsx

@@ -0,0 +1,59 @@
+import { expect } from 'chai'
+import sinon from 'sinon'
+import { fireEvent, render, screen, within } from '@testing-library/react'
+import * as eventTracking from '../../../../../../frontend/js/infrastructure/event-tracking'
+import PremiumFeaturesLink from '../../../../../../frontend/js/features/subscription/components/dashboard/premium-features-link'
+
+describe('<PremiumFeaturesLink />', function () {
+  const originalLocation = window.location
+
+  let sendMBSpy: sinon.SinonSpy
+
+  const variants = [
+    { name: 'default', link: '/learn/how-to/Overleaf_premium_features' },
+    { name: 'new', link: '/about/features-overview' },
+  ]
+
+  beforeEach(function () {
+    window.metaAttributesCache = new Map()
+    sendMBSpy = sinon.spy(eventTracking, 'sendMB')
+  })
+
+  afterEach(function () {
+    window.metaAttributesCache = new Map()
+    sendMBSpy.restore()
+    Object.defineProperty(window, 'location', {
+      value: originalLocation,
+    })
+  })
+
+  for (const variant of variants) {
+    describe(`${variant.name} variant`, function () {
+      beforeEach(function () {
+        window.metaAttributesCache.set('ol-splitTestVariants', {
+          'features-page': variant.name,
+        })
+      })
+      afterEach(function () {
+        window.metaAttributesCache.delete('ol-splitTestVariants')
+      })
+
+      it('renders the premium features link and sends analytics event', function () {
+        render(<PremiumFeaturesLink />)
+        const premiumText = screen.getByText('Get the most out of your', {
+          exact: false,
+        })
+        const link = within(premiumText).getByRole('link')
+
+        fireEvent.click(link)
+
+        expect(sendMBSpy).to.be.calledOnce
+        expect(sendMBSpy).calledWith('features-page-link', {
+          splitTest: 'features-page',
+          splitTestVariant: variant.name,
+          page: originalLocation.pathname,
+        })
+      })
+    })
+  }
+})

+ 29 - 0
services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx

@@ -0,0 +1,29 @@
+import { render, screen } from '@testing-library/react'
+import SubscriptionDashboard from '../../../../../../frontend/js/features/subscription/components/dashboard/subscription-dashboard'
+
+describe('<SubscriptionDashboard />', function () {
+  beforeEach(function () {
+    window.metaAttributesCache = new Map()
+    window.metaAttributesCache.set('ol-currentInstitutionsWithLicence', [
+      {
+        id: 9258,
+        name: 'Test University',
+        commonsAccount: true,
+        isUniversity: true,
+        confirmed: true,
+        ssoBeta: false,
+        ssoEnabled: false,
+        maxConfirmationMonths: 6,
+      },
+    ])
+  })
+
+  afterEach(function () {
+    window.metaAttributesCache = new Map()
+  })
+
+  it('renders the premium features text when a user has a subscription', function () {
+    render(<SubscriptionDashboard />)
+    screen.getByText('Get the most out of your', { exact: false })
+  })
+})