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

[web] Migrate institution memberships dash to React

GitOrigin-RevId: 75bb8e7d7338418733a836a8e654fe5b0a9fceff
This commit is contained in:
Jessica Lawshe 2023-01-18 08:38:35 -06:00 committed by Copybot
parent dddf9b7042
commit c8ef5e6f65
9 changed files with 279 additions and 1 deletions

View file

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

View file

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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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.'
)
})
})

View file

@ -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,
})
})
})
}
})

View file

@ -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 })
})
})