diff --git a/services/web/app/views/subscriptions/dashboard-react.pug b/services/web/app/views/subscriptions/dashboard-react.pug index 211fbf2a62..7ddf9e8d05 100644 --- a/services/web/app/views/subscriptions/dashboard-react.pug +++ b/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) diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index a2770ed53c..6fd48b1acc 100644 --- a/services/web/frontend/extracted-translations.json +++ b/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": "", diff --git a/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx b/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx new file mode 100644 index 0000000000..58336cc681 --- /dev/null +++ b/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 +} + +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 ( +
+

+ Sorry, something went wrong. Subscription information related to + institutional affiliations may not be displayed. Please try again + later. +

+
+ ) + } + + return ( + <> +
+ {memberships.map((institution: Institution) => ( +
+ , + // eslint-disable-next-line react/jsx-key + , + // eslint-disable-next-line react/jsx-key + , + ]} + /> +
+
+ ))} +
+ + ) +} + +export default InstitutionMemberships diff --git a/services/web/frontend/js/features/subscription/components/dashboard/premium-features-link.tsx b/services/web/frontend/js/features/subscription/components/dashboard/premium-features-link.tsx new file mode 100644 index 0000000000..ecc60bb7a0 --- /dev/null +++ b/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 + + ) + + if (featuresPageVariant === 'new') { + return ( + + ) + } + + return ( + + ) +} + +export default PremiumFeaturesLink diff --git a/services/web/frontend/js/features/subscription/components/dashboard/root.tsx b/services/web/frontend/js/features/subscription/components/dashboard/root.tsx index 1287e4efd1..2fe706e027 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/root.tsx +++ b/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

React Subscription Dashboard

+ return } export default Root diff --git a/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx b/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx new file mode 100644 index 0000000000..5b829d47ec --- /dev/null +++ b/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 ( +
+
+
+
+
+

{t('your_subscription')}

+
+ + + {hasDisplayedSubscription ? : <>} +
+
+
+
+ ) +} + +export default SubscriptionDashboard diff --git a/services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx new file mode 100644 index 0000000000..ecdf4b9337 --- /dev/null +++ b/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('', function () { + beforeEach(function () { + window.metaAttributesCache = new Map() + }) + + afterEach(function () { + window.metaAttributesCache = new Map() + }) + + it('renders all insitutions with license', function () { + render() + + 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() + screen.getByText( + 'Sorry, something went wrong. Subscription information related to institutional affiliations may not be displayed. Please try again later.' + ) + }) +}) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/premium-features-link.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/premium-features-link.test.tsx new file mode 100644 index 0000000000..5f53d077a3 --- /dev/null +++ b/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('', 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() + 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, + }) + }) + }) + } +}) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx new file mode 100644 index 0000000000..7fe8e92402 --- /dev/null +++ b/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('', 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() + screen.getByText('Get the most out of your', { exact: false }) + }) +})