diff --git a/services/web/app/views/subscriptions/dashboard-react.pug b/services/web/app/views/subscriptions/dashboard-react.pug
index 7ddf9e8d05..932b5df240 100644
--- a/services/web/app/views/subscriptions/dashboard-react.pug
+++ b/services/web/app/views/subscriptions/dashboard-react.pug
@@ -3,6 +3,9 @@ extends ../layout-marketing
block entrypointVar
- entrypoint = 'pages/user/subscription/dashboard'
+block head-scripts
+ script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
+
block append meta
meta(name="ol-managedInstitutions", data-type="json", content=managedInstitutions)
meta(name="ol-planCodesChangingAtTermEnd", data-type="json", content=plans.planCodesChangingAtTermEnd)
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 50a95aa28d..25e4fbc628 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -22,6 +22,7 @@
"add_or_remove_project_from_tag": "",
"add_role_and_department": "",
"add_to_folder": "",
+ "additional_licenses": "",
"all_projects": "",
"also": "",
"anyone_with_link_can_edit": "",
@@ -57,6 +58,7 @@
"can_link_your_institution_acct_2": "",
"can_now_relink_dropbox": "",
"cancel": "",
+ "cancel_your_subscription": "",
"cannot_invite_non_user": "",
"cannot_invite_self": "",
"cannot_verify_user_not_robot": "",
@@ -475,6 +477,7 @@
"password": "",
"password_managed_externally": "",
"password_was_detected_on_a_public_list_of_known_compromised_passwords": "",
+ "payment_provider_unreachable_error": "",
"pdf_compile_in_progress_error": "",
"pdf_compile_rate_limit_hit": "",
"pdf_compile_try_again": "",
@@ -484,6 +487,7 @@
"pdf_rendering_error": "",
"pdf_viewer": "",
"pdf_viewer_error": "",
+ "pending_additional_licenses": "",
"plan_tooltip": "",
"please_change_primary_to_remove": "",
"please_check_your_inbox": "",
@@ -663,6 +667,7 @@
"submit_title": "",
"subscription_admins_cannot_be_deleted": "",
"subscription_canceled_and_terminate_on_x": "",
+ "subscription_will_remain_active_until_end_of_billing_period_x": "",
"sure_you_want_to_delete": "",
"switch_to_editor": "",
"switch_to_pdf": "",
@@ -763,6 +768,7 @@
"want_change_to_apply_before_plan_end": "",
"we_cant_find_any_sections_or_subsections_in_this_file": "",
"we_logged_you_in": "",
+ "wed_love_you_to_stay": "",
"welcome_to_sl": "",
"wide": "",
"with_premium_subscription_you_also_get": "",
@@ -779,6 +785,7 @@
"your_projects": "",
"your_subscription": "",
"your_subscription_has_expired": "",
+ "youre_on_free_trial_which_ends_on": "",
"zotero_groups_loading_error": "",
"zotero_groups_relink": "",
"zotero_integration": "",
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
index ca55fee085..fbcea64113 100644
--- a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
+++ b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx
@@ -1,8 +1,10 @@
+import { useEffect } from 'react'
import { useTranslation } from 'react-i18next'
import { Subscription } from '../../../../../../types/subscription/dashboard/subscription'
-import { ActiveSubsciption } from './states/active'
+import { ActiveSubsciption } from './states/active/active'
import { CanceledSubsciption } from './states/canceled'
import { ExpiredSubsciption } from './states/expired'
+import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
function PastDueSubscriptionAlert({
subscription,
@@ -51,9 +53,18 @@ function PersonalSubscription({
}: {
subscription?: Subscription
}) {
+ const { t } = useTranslation()
+ const { recurlyLoadError, setRecurlyLoadError } =
+ useSubscriptionDashboardContext()
const state = subscription?.recurly?.state
- if (!subscription) return <>>
+ useEffect(() => {
+ if (typeof window.recurly === 'undefined' || !window.recurly) {
+ setRecurlyLoadError(true)
+ }
+ })
+
+ if (!subscription) return null
return (
<>
@@ -61,6 +72,11 @@ function PersonalSubscription({
)}
+ {recurlyLoadError && (
+
+ {t('payment_provider_unreachable_error')}
+
+ )}
>
)
}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active.tsx
deleted file mode 100644
index ade818d5e3..0000000000
--- a/services/web/frontend/js/features/subscription/components/dashboard/states/active.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import { useTranslation, Trans } from 'react-i18next'
-import PremiumFeaturesLink from '../premium-features-link'
-import { PriceExceptions } from '../../shared/price-exceptions'
-import { useSubscriptionDashboardContext } from '../../../context/subscription-dashboard-context'
-import { Subscription } from '../../../../../../../types/subscription/dashboard/subscription'
-
-function ChangePlan() {
- const { t } = useTranslation()
- const { showChangePersonalPlan } = useSubscriptionDashboardContext()
-
- if (!showChangePersonalPlan) return null
-
- return (
- <>
- {t('change_plan')}
-
- TODO: change subscription placeholder
-
- >
- )
-}
-
-export function ActiveSubsciption({
- subscription,
-}: {
- subscription: Subscription
-}) {
- const { t } = useTranslation()
- const { setShowChangePersonalPlan } = useSubscriptionDashboardContext()
-
- return (
- <>
-
- ,
- ]}
- />
- {subscription.pendingPlan &&
- subscription.pendingPlan.name !== subscription.plan.name && (
- <>
- {' '}
- ,
- ]}
- />
- >
- )}{' '}
- {/* TODO: pending_additional_licenses */}
- {/* TODO: additionalLicenses */}
-
-
- {subscription.pendingPlan && (
- {t('want_change_to_apply_before_plan_end')}
- )}
- {/* TODO: groupPlan */}
- {/* TODO: trialEndsAtFormatted */}
-
- ,
- // eslint-disable-next-line react/jsx-key
- ,
- ]}
- />
-
-
-
-
- {t('update_your_billing_details')}
- {' '}
-
- {t('view_your_invoices')}
- {' '}
- {/* TODO: cancel button */}
-
- >
- )
-}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx
new file mode 100644
index 0000000000..91b86b5fa8
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx
@@ -0,0 +1,117 @@
+import { useTranslation, Trans } from 'react-i18next'
+import PremiumFeaturesLink from '../../premium-features-link'
+import { PriceExceptions } from '../../../shared/price-exceptions'
+import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
+import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription'
+import { CancelSubscriptionButton } from './cancel-subscription-button'
+import { CancelSubscription } from './cancel-subscription'
+import { PendingPlanChange } from './pending-plan-change'
+import { TrialEnding } from './trial-ending'
+import { ChangePlan } from './change-plan'
+
+export function ActiveSubsciption({
+ subscription,
+}: {
+ subscription: Subscription
+}) {
+ const { t } = useTranslation()
+ const { recurlyLoadError, setShowChangePersonalPlan, showCancellation } =
+ useSubscriptionDashboardContext()
+
+ if (showCancellation) return
+
+ return (
+ <>
+
+ ,
+ ]}
+ />
+ {subscription.pendingPlan && (
+
+ )}
+ {!subscription.pendingPlan &&
+ subscription.recurly.additionalLicenses > 0 && (
+ <>
+ {' '}
+ ,
+ // eslint-disable-next-line react/jsx-key
+ ,
+ ]}
+ />
+ >
+ )}{' '}
+ {!recurlyLoadError &&
+ !subscription.groupPlan &&
+ subscription.recurly.account.has_past_due_invoice._ !== 'true' && (
+
+ )}
+
+ {subscription.pendingPlan && (
+ {t('want_change_to_apply_before_plan_end')}
+ )}
+ {/* TODO: groupPlan */}
+
+
+ ,
+ // eslint-disable-next-line react/jsx-key
+ ,
+ ]}
+ />
+
+
+
+
+
+ {t('update_your_billing_details')}
+ {' '}
+
+ {t('view_your_invoices')}
+
+
+
+ {!recurlyLoadError && (
+
+ )}
+
+
+ >
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx
new file mode 100644
index 0000000000..bb961f2d31
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx
@@ -0,0 +1,56 @@
+import { useTranslation, Trans } from 'react-i18next'
+import * as eventTracking from '../../../../../../infrastructure/event-tracking'
+import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription'
+import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
+
+export function CancelSubscriptionButton({
+ subscription,
+}: {
+ subscription: Subscription
+}) {
+ const { t } = useTranslation()
+ const { recurlyLoadError, setShowCancellation } =
+ useSubscriptionDashboardContext()
+
+ const stillInATrial =
+ subscription.recurly.trialEndsAtFormatted &&
+ subscription.recurly.trial_ends_at &&
+ new Date(subscription.recurly.trial_ends_at).getTime() > Date.now()
+
+ function handleCancelSubscriptionClick() {
+ eventTracking.sendMB('subscription-page-cancel-button-click')
+ setShowCancellation(true)
+ }
+
+ if (recurlyLoadError) return null
+
+ return (
+ <>
+
+
+
+
+ {!stillInATrial && (
+
+
+ ,
+ ]}
+ />
+
+
+ )}
+ >
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription.tsx
new file mode 100644
index 0000000000..eb4ed9d4a9
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription.tsx
@@ -0,0 +1,15 @@
+import { useTranslation } from 'react-i18next'
+
+export function CancelSubscription() {
+ const { t } = useTranslation()
+ return (
+
+
+ {t('wed_love_you_to_stay')}
+
+ {/* todo: showExtendFreeTrial */}
+ {/* todo: showDowngrade */}
+ {/* todo: showBasicCancel */}
+
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan.tsx
new file mode 100644
index 0000000000..f56813a985
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/change-plan.tsx
@@ -0,0 +1,18 @@
+import { useTranslation } from 'react-i18next'
+import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
+
+export function ChangePlan() {
+ const { t } = useTranslation()
+ const { showChangePersonalPlan } = useSubscriptionDashboardContext()
+
+ if (!showChangePersonalPlan) return null
+
+ return (
+ <>
+ {t('change_plan')}
+
+ TODO: change subscription placeholder
+
+ >
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-plan-change.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-plan-change.tsx
new file mode 100644
index 0000000000..00e2a401d0
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-plan-change.tsx
@@ -0,0 +1,49 @@
+import { Trans } from 'react-i18next'
+import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription'
+
+export function PendingPlanChange({
+ subscription,
+}: {
+ subscription: Subscription
+}) {
+ if (!subscription.pendingPlan) return null
+
+ return (
+ <>
+ {subscription.pendingPlan.name !== subscription.plan.name && (
+ ,
+ ]}
+ />
+ )}
+
+ {((subscription.recurly.pendingAdditionalLicenses &&
+ subscription.recurly.pendingAdditionalLicenses > 0) ||
+ subscription.recurly.additionalLicenses > 0) && (
+ <>
+ {' '}
+ ,
+ // eslint-disable-next-line react/jsx-key
+ ,
+ ]}
+ />
+ >
+ )}
+ >
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx
new file mode 100644
index 0000000000..ce3615c9c4
--- /dev/null
+++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active/trial-ending.tsx
@@ -0,0 +1,26 @@
+import { Trans } from 'react-i18next'
+import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription'
+
+export function TrialEnding({ subscription }: { subscription: Subscription }) {
+ if (
+ !subscription.recurly.trialEndsAtFormatted ||
+ !subscription.recurly.trial_ends_at
+ )
+ return null
+
+ const endDate = new Date(subscription.recurly.trial_ends_at)
+ if (endDate.getTime() < Date.now()) return null
+
+ return (
+
+ ,
+ ]}
+ />
+
+ )
+}
diff --git a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx
index 031846f674..04fdcc7629 100644
--- a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx
+++ b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx
@@ -1,6 +1,10 @@
import { createContext, ReactNode, useContext, useMemo, useState } from 'react'
type SubscriptionDashboardContextValue = {
+ recurlyLoadError: boolean
+ setRecurlyLoadError: React.Dispatch>
+ showCancellation: boolean
+ setShowCancellation: React.Dispatch>
showChangePersonalPlan: boolean
setShowChangePersonalPlan: React.Dispatch>
}
@@ -14,14 +18,27 @@ export function SubscriptionDashboardProvider({
}: {
children: ReactNode
}) {
+ const [recurlyLoadError, setRecurlyLoadError] = useState(false)
+ const [showCancellation, setShowCancellation] = useState(false)
const [showChangePersonalPlan, setShowChangePersonalPlan] = useState(false)
const value = useMemo(
() => ({
+ recurlyLoadError,
+ setRecurlyLoadError,
+ showCancellation,
+ setShowCancellation,
showChangePersonalPlan,
setShowChangePersonalPlan,
}),
- [showChangePersonalPlan, setShowChangePersonalPlan]
+ [
+ recurlyLoadError,
+ setRecurlyLoadError,
+ showCancellation,
+ setShowCancellation,
+ showChangePersonalPlan,
+ setShowChangePersonalPlan,
+ ]
)
return (
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 046471831c..a7610b9a0f 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -1657,6 +1657,7 @@
"your_sessions": "Your Sessions",
"your_subscription": "Your Subscription",
"your_subscription_has_expired": "Your subscription has expired.",
+ "youre_on_free_trial_which_ends_on": "You’re on a free trial which ends on <0>__date__0>.",
"zh-CN": "Chinese",
"zip_contents_too_large": "Zip contents too large",
"zotero": "Zotero",
diff --git a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
index b958261624..eab3074c2e 100644
--- a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
+++ b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx
@@ -11,10 +11,12 @@ import {
describe('', function () {
beforeEach(function () {
window.metaAttributesCache = new Map()
+ window.recurly = {}
})
afterEach(function () {
window.metaAttributesCache = new Map()
+ delete window.recurly
})
describe('no subscription', function () {
@@ -74,7 +76,10 @@ describe('', function () {
})
it('renders error message when an unknown subscription state', function () {
- const withStateDeleted = Object.assign({}, annualActiveSubscription)
+ const withStateDeleted = Object.assign(
+ {},
+ JSON.parse(JSON.stringify(annualActiveSubscription))
+ )
withStateDeleted.recurly.state = undefined
render(
@@ -105,4 +110,35 @@ describe('', function () {
expect(invoiceLinks.length).to.equal(2)
})
})
+
+ describe('Recurly JS', function () {
+ const recurlyFailedToLoadText =
+ 'Sorry, there was an error talking to our payment provider. Please try again in a few moments. If you are using any ad or script blocking extensions in your browser, you may need to temporarily disable them.'
+
+ it('shows an alert and hides "Change plan" option when Recurly did not load', function () {
+ delete window.recurly
+ render(
+
+
+
+ )
+
+ screen.getByRole('alert')
+ screen.getByText(recurlyFailedToLoadText)
+
+ expect(screen.queryByText('Change plan')).to.be.null
+ })
+
+ it('should not show an alert and should show "Change plan" option when Recurly did load', function () {
+ render(
+
+
+
+ )
+
+ expect(screen.queryByRole('alert')).to.be.null
+
+ screen.getByText('Change plan')
+ })
+ })
})
diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx
index 3cf878e03c..8e09882e43 100644
--- a/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx
+++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx
@@ -1,19 +1,29 @@
+import { expect } from 'chai'
import { fireEvent, render, screen } from '@testing-library/react'
-import { ActiveSubsciption } from '../../../../../../../frontend/js/features/subscription/components/dashboard/states/active'
+import * as eventTracking from '../../../../../../../frontend/js/infrastructure/event-tracking'
+import { ActiveSubsciption } from '../../../../../../../frontend/js/features/subscription/components/dashboard/states/active/active'
import { SubscriptionDashboardProvider } from '../../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
import { Subscription } from '../../../../../../../types/subscription/dashboard/subscription'
import {
annualActiveSubscription,
+ groupActiveSubscription,
+ groupActiveSubscriptionWithPendingLicenseChange,
pendingSubscriptionChange,
+ trialSubscription,
} from '../../../fixtures/subscriptions'
+import sinon from 'sinon'
describe('', function () {
+ let sendMBSpy: sinon.SinonSpy
+
beforeEach(function () {
window.metaAttributesCache = new Map()
+ sendMBSpy = sinon.spy(eventTracking, 'sendMB')
})
afterEach(function () {
window.metaAttributesCache = new Map()
+ sendMBSpy.restore()
})
function expectedInActiveSubscription(subscription: Subscription) {
@@ -25,13 +35,14 @@ describe('', function () {
// sentence broken up by bolding
screen.getByText('The next payment of', { exact: false })
- screen.getByText(annualActiveSubscription.recurly.displayPrice, {
+ screen.getByText(subscription.recurly.displayPrice, {
exact: false,
})
screen.getByText('will be collected on', { exact: false })
- screen.getByText(annualActiveSubscription.recurly.nextPaymentDueAt, {
+ const dates = screen.getAllByText(subscription.recurly.nextPaymentDueAt, {
exact: false,
})
+ expect(dates.length).to.equal(2)
// sentence broken up by link
screen.getByText(
@@ -89,4 +100,145 @@ describe('', function () {
exact: false,
})
})
+
+ it('does not show "Change plan" option for group plans', function () {
+ render(
+
+
+
+ )
+
+ const changePlan = screen.queryByRole('button', { name: 'Change plan' })
+ expect(changePlan).to.be.null
+ })
+
+ it('does not show "Change plan" option when past due', function () {
+ // account is likely in expired state, but be sure to not show option if state is still active
+ const activePastDueSubscription = Object.assign(
+ {},
+ annualActiveSubscription
+ )
+
+ activePastDueSubscription.recurly.account.has_past_due_invoice._ = 'true'
+
+ render(
+
+
+
+ )
+
+ const changePlan = screen.queryByRole('button', { name: 'Change plan' })
+ expect(changePlan).to.be.null
+ })
+
+ it('shows the pending license change message when plan change is pending', function () {
+ render(
+
+
+
+ )
+
+ screen.getByText('Your subscription is changing to include', {
+ exact: false,
+ })
+
+ if (
+ !groupActiveSubscriptionWithPendingLicenseChange.recurly
+ .pendingAdditionalLicenses
+ ) {
+ throw Error('not expected test data')
+ }
+ screen.getByText(
+ groupActiveSubscriptionWithPendingLicenseChange.recurly
+ .pendingAdditionalLicenses
+ )
+
+ screen.getByText('additional license(s) for a total of', { exact: false })
+
+ if (
+ !groupActiveSubscriptionWithPendingLicenseChange.recurly
+ .pendingTotalLicenses
+ ) {
+ throw Error('not expected test data')
+ }
+ screen.getByText(
+ groupActiveSubscriptionWithPendingLicenseChange.recurly
+ .pendingTotalLicenses
+ )
+ })
+
+ it('shows the pending license change message when plan change is not pending', function () {
+ const subscription = Object.assign({}, groupActiveSubscription)
+ subscription.recurly.additionalLicenses = 4
+ subscription.recurly.totalLicenses =
+ subscription.recurly.totalLicenses +
+ subscription.recurly.additionalLicenses
+
+ render(
+
+
+
+ )
+
+ screen.getByText('Your subscription includes', {
+ exact: false,
+ })
+
+ if (!subscription.recurly.additionalLicenses) {
+ throw Error('not expected test data')
+ }
+ screen.getByText(subscription.recurly.additionalLicenses)
+
+ screen.getByText('additional license(s) for a total of', { exact: false })
+
+ screen.getByText(subscription.recurly.totalLicenses)
+ })
+
+ it('shows when trial ends and first payment collected', function () {
+ render(
+
+
+
+ )
+ screen.getByText('You’re on a free trial which ends on', { exact: false })
+ if (!trialSubscription.recurly.trialEndsAtFormatted) {
+ throw new Error('not expected test data')
+ }
+ const endDate = screen.getAllByText(
+ trialSubscription.recurly.trialEndsAtFormatted
+ )
+ expect(endDate.length).to.equal(2)
+ })
+
+ it('shows cancel UI and sends event', function () {
+ render(
+
+
+
+ )
+ // before button clicked
+ screen.getByText(
+ 'Your subscription will remain active until the end of your billing period',
+ { exact: false }
+ )
+ const dates = screen.getAllByText(
+ annualActiveSubscription.recurly.nextPaymentDueAt,
+ {
+ exact: false,
+ }
+ )
+ expect(dates.length).to.equal(2)
+
+ const button = screen.getByRole('button', {
+ name: 'Cancel Your Subscription',
+ })
+ fireEvent.click(button)
+ expect(sendMBSpy).to.be.calledOnceWith(
+ 'subscription-page-cancel-button-click'
+ )
+
+ screen.getByText('We’d love you to stay')
+ })
})
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
index bca237f99a..6f329e2822 100644
--- 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
@@ -1,6 +1,7 @@
import { expect } from 'chai'
import { render, screen } from '@testing-library/react'
import SubscriptionDashboard from '../../../../../../frontend/js/features/subscription/components/dashboard/subscription-dashboard'
+import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
describe('', function () {
beforeEach(function () {
@@ -13,7 +14,11 @@ describe('', function () {
describe('Free Plan', function () {
it('does not render the "Get the most out of your" subscription text', function () {
- render()
+ render(
+
+
+
+ )
const text = screen.queryByText('Get the most out of your', {
exact: false,
})
diff --git a/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx b/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx
index 2f7fa5e2e7..353e4af699 100644
--- a/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx
+++ b/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx
@@ -3,6 +3,11 @@ const dateformat = require('dateformat')
const today = new Date()
const oneYearFromToday = new Date().setFullYear(today.getFullYear() + 1)
const nextPaymentDueAt = dateformat(oneYearFromToday, 'dS mmmm yyyy')
+const sevenDaysFromToday = new Date().setDate(today.getDate() + 7)
+const sevenDaysFromTodayFormatted = dateformat(
+ sevenDaysFromToday,
+ 'dS mmmm yyyy'
+)
export const annualActiveSubscription: Subscription = {
manager_ids: ['abc123'],
@@ -171,3 +176,163 @@ export const pendingSubscriptionChange: Subscription = {
featureDescription: [],
},
}
+
+export const groupActiveSubscription: Subscription = {
+ manager_ids: ['abc123'],
+ member_ids: ['abc123'],
+ invited_emails: [],
+ groupPlan: true,
+ membersLimit: 10,
+ _id: 'def456',
+ admin_id: 'abc123',
+ teamInvites: [],
+ planCode: 'group_collaborator_10_enterprise',
+ recurlySubscription_id: 'ghi789',
+ plan: {
+ planCode: 'group_collaborator_10_enterprise',
+ name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise',
+ hideFromUsers: true,
+ price_in_cents: 129000,
+ annual: true,
+ features: {},
+ groupPlan: true,
+ membersLimit: 10,
+ membersLimitAddOn: 'additional-license',
+ },
+ recurly: {
+ tax: 0,
+ taxRate: 0,
+ billingDetailsLink: '/user/subscription/recurly/billing-details',
+ accountManagementLink: '/user/subscription/recurly/account-management',
+ additionalLicenses: 0,
+ totalLicenses: 10,
+ nextPaymentDueAt,
+ currency: 'USD',
+ state: 'active',
+ trialEndsAtFormatted: null,
+ trial_ends_at: null,
+ activeCoupons: [],
+ account: {
+ has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
+ has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
+ },
+ displayPrice: '$1290.00',
+ },
+}
+
+export const groupActiveSubscriptionWithPendingLicenseChange: Subscription = {
+ manager_ids: ['abc123'],
+ member_ids: ['abc123'],
+ invited_emails: [],
+ groupPlan: true,
+ membersLimit: 10,
+ _id: 'def456',
+ admin_id: 'abc123',
+ teamInvites: [],
+ planCode: 'group_collaborator_10_enterprise',
+ recurlySubscription_id: 'ghi789',
+ plan: {
+ planCode: 'group_collaborator_10_enterprise',
+ name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise',
+ hideFromUsers: true,
+ price_in_cents: 129000,
+ annual: true,
+ features: {},
+ groupPlan: true,
+ membersLimit: 10,
+ membersLimitAddOn: 'additional-license',
+ },
+ recurly: {
+ tax: 0,
+ taxRate: 0,
+ billingDetailsLink: '/user/subscription/recurly/billing-details',
+ accountManagementLink: '/user/subscription/recurly/account-management',
+ additionalLicenses: 11,
+ totalLicenses: 21,
+ nextPaymentDueAt,
+ currency: 'USD',
+ state: 'active',
+ trialEndsAtFormatted: null,
+ trial_ends_at: null,
+ activeCoupons: [],
+ account: {
+ has_canceled_subscription: {
+ _: 'false',
+ $: {
+ type: 'boolean',
+ },
+ },
+ has_past_due_invoice: {
+ _: 'false',
+ $: {
+ type: 'boolean',
+ },
+ },
+ },
+ displayPrice: '$2967.00',
+ currentPlanDisplayPrice: '$2709.00',
+ pendingAdditionalLicenses: 13,
+ pendingTotalLicenses: 23,
+ },
+ pendingPlan: {
+ planCode: 'group_collaborator_10_enterprise',
+ name: 'Overleaf Standard (Collaborator) - Group Account (10 licenses) - Enterprise',
+ hideFromUsers: true,
+ price_in_cents: 129000,
+ annual: true,
+ features: {},
+ groupPlan: true,
+ membersLimit: 10,
+ membersLimitAddOn: 'additional-license',
+ },
+}
+
+export const trialSubscription: Subscription = {
+ manager_ids: ['abc123'],
+ member_ids: [],
+ invited_emails: [],
+ groupPlan: false,
+ membersLimit: 0,
+ _id: 'def456',
+ admin_id: 'abc123',
+ teamInvites: [],
+ planCode: 'paid-personal_free_trial_7_days',
+ recurlySubscription_id: 'ghi789',
+ plan: {
+ planCode: 'paid-personal_free_trial_7_days',
+ name: 'Personal',
+ price_in_cents: 1500,
+ features: {},
+ featureDescription: [],
+ hideFromUsers: true,
+ },
+ recurly: {
+ tax: 0,
+ taxRate: 0,
+ billingDetailsLink: '/user/subscription/recurly/billing-details',
+ accountManagementLink: '/user/subscription/recurly/account-management',
+ additionalLicenses: 0,
+ totalLicenses: 0,
+ nextPaymentDueAt: sevenDaysFromTodayFormatted,
+ currency: 'USD',
+ state: 'active',
+ trialEndsAtFormatted: sevenDaysFromTodayFormatted,
+ trial_ends_at: new Date(sevenDaysFromToday).toString(),
+ activeCoupons: [],
+ account: {
+ has_canceled_subscription: {
+ _: 'false',
+ $: {
+ type: 'boolean',
+ },
+ },
+ has_past_due_invoice: {
+ _: 'false',
+ $: {
+ type: 'boolean',
+ },
+ },
+ },
+ displayPrice: '$14.00',
+ },
+}
diff --git a/services/web/types/subscription/dashboard/subscription.ts b/services/web/types/subscription/dashboard/subscription.ts
index f0b8dc731e..8d2e5d4810 100644
--- a/services/web/types/subscription/dashboard/subscription.ts
+++ b/services/web/types/subscription/dashboard/subscription.ts
@@ -7,7 +7,10 @@ type Plan = {
annual?: boolean
features: object
hideFromUsers?: boolean
- featureDescription: object[]
+ featureDescription?: object[]
+ groupPlan?: boolean
+ membersLimit?: number
+ membersLimitAddOn?: string
}
type SubscriptionState = 'active' | 'canceled' | 'expired'
diff --git a/services/web/types/window.ts b/services/web/types/window.ts
index 9f02651ea4..f6f4dd255c 100644
--- a/services/web/types/window.ts
+++ b/services/web/types/window.ts
@@ -31,5 +31,6 @@ declare global {
_reportAcePerf: () => void
MathJax: Record
overallThemes: OverallThemeMeta[]
+ recurly?: object
}
}