Merge pull request #11414 from overleaf/jel-react-personal-subscription-dash-pt-2

[web] Continue migration of personal subscription dash commons to React

GitOrigin-RevId: 997716f70c953b088f686d4b0f638705ad838520
This commit is contained in:
Jessica Lawshe 2023-01-26 11:01:54 -06:00 committed by Copybot
parent 2499ecadcc
commit dad1460d71
18 changed files with 696 additions and 120 deletions

View file

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

View file

@ -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": "",

View file

@ -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({
<PastDueSubscriptionAlert subscription={subscription} />
)}
<PersonalSubscriptionStates subscription={subscription} state={state} />
{recurlyLoadError && (
<div className="alert alert-warning" role="alert">
<strong>{t('payment_provider_unreachable_error')}</strong>
</div>
)}
</>
)
}

View file

@ -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 (
<>
<h2>{t('change_plan')}</h2>
<p>
<strong>TODO: change subscription placeholder</strong>
</p>
</>
)
}
export function ActiveSubsciption({
subscription,
}: {
subscription: Subscription
}) {
const { t } = useTranslation()
const { setShowChangePersonalPlan } = useSubscriptionDashboardContext()
return (
<>
<p>
<Trans
i18nKey="currently_subscribed_to_plan"
values={{
planName: subscription.plan.name,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
{subscription.pendingPlan &&
subscription.pendingPlan.name !== subscription.plan.name && (
<>
{' '}
<Trans
i18nKey="your_plan_is_changing_at_term_end"
values={{
pendingPlanName: subscription.pendingPlan.name,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</>
)}{' '}
{/* TODO: pending_additional_licenses */}
{/* TODO: additionalLicenses */}
<button
className="btn-inline-link"
onClick={() => setShowChangePersonalPlan(true)}
>
{t('change_plan')}
</button>
</p>
{subscription.pendingPlan && (
<p>{t('want_change_to_apply_before_plan_end')}</p>
)}
{/* TODO: groupPlan */}
{/* TODO: trialEndsAtFormatted */}
<p>
<Trans
i18nKey="next_payment_of_x_collectected_on_y"
values={{
paymentAmmount: subscription.recurly.displayPrice,
collectionDate: subscription.recurly.nextPaymentDueAt,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</p>
<PremiumFeaturesLink />
<PriceExceptions />
<a
href={subscription.recurly.billingDetailsLink}
target="_blank"
rel="noreferrer noopener"
className="btn btn-secondary-info btn-secondary"
>
{t('update_your_billing_details')}
</a>{' '}
<a
href={subscription.recurly.accountManagementLink}
target="_blank"
rel="noreferrer noopener"
className="btn btn-secondary-info btn-secondary"
>
{t('view_your_invoices')}
</a>{' '}
{/* TODO: cancel button */}
<ChangePlan />
</>
)
}

View file

@ -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 <CancelSubscription />
return (
<>
<p>
<Trans
i18nKey="currently_subscribed_to_plan"
values={{
planName: subscription.plan.name,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
{subscription.pendingPlan && (
<PendingPlanChange subscription={subscription} />
)}
{!subscription.pendingPlan &&
subscription.recurly.additionalLicenses > 0 && (
<>
{' '}
<Trans
i18nKey="additional_licenses"
values={{
additionalLicenses: subscription.recurly.additionalLicenses,
totalLicenses: subscription.recurly.totalLicenses,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</>
)}{' '}
{!recurlyLoadError &&
!subscription.groupPlan &&
subscription.recurly.account.has_past_due_invoice._ !== 'true' && (
<button
className="btn-inline-link"
onClick={() => setShowChangePersonalPlan(true)}
>
{t('change_plan')}
</button>
)}
</p>
{subscription.pendingPlan && (
<p>{t('want_change_to_apply_before_plan_end')}</p>
)}
{/* TODO: groupPlan */}
<TrialEnding subscription={subscription} />
<p>
<Trans
i18nKey="next_payment_of_x_collectected_on_y"
values={{
paymentAmmount: subscription.recurly.displayPrice,
collectionDate: subscription.recurly.nextPaymentDueAt,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</p>
<PremiumFeaturesLink />
<PriceExceptions />
<p>
<a
href={subscription.recurly.billingDetailsLink}
target="_blank"
rel="noreferrer noopener"
className="btn btn-secondary-info btn-secondary"
>
{t('update_your_billing_details')}
</a>{' '}
<a
href={subscription.recurly.accountManagementLink}
target="_blank"
rel="noreferrer noopener"
className="btn btn-secondary-info btn-secondary"
>
{t('view_your_invoices')}
</a>
</p>
{!recurlyLoadError && (
<CancelSubscriptionButton subscription={subscription} />
)}
<ChangePlan />
</>
)
}

View file

@ -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 (
<>
<br />
<p>
<button
className="btn btn-danger"
onClick={handleCancelSubscriptionClick}
>
{t('cancel_your_subscription')}
</button>
</p>
{!stillInATrial && (
<p>
<i>
<Trans
i18nKey="subscription_will_remain_active_until_end_of_billing_period_x"
values={{
terminationDate: subscription.recurly.nextPaymentDueAt,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</i>
</p>
)}
</>
)
}

View file

@ -0,0 +1,15 @@
import { useTranslation } from 'react-i18next'
export function CancelSubscription() {
const { t } = useTranslation()
return (
<div className="text-center">
<p>
<strong>{t('wed_love_you_to_stay')}</strong>
</p>
{/* todo: showExtendFreeTrial */}
{/* todo: showDowngrade */}
{/* todo: showBasicCancel */}
</div>
)
}

View file

@ -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 (
<>
<h2>{t('change_plan')}</h2>
<p>
<strong>TODO: change subscription placeholder</strong>
</p>
</>
)
}

View file

@ -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 && (
<Trans
i18nKey="your_plan_is_changing_at_term_end"
values={{
pendingPlanName: subscription.pendingPlan.name,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
)}
{((subscription.recurly.pendingAdditionalLicenses &&
subscription.recurly.pendingAdditionalLicenses > 0) ||
subscription.recurly.additionalLicenses > 0) && (
<>
{' '}
<Trans
i18nKey="pending_additional_licenses"
values={{
pendingAdditionalLicenses:
subscription.recurly.pendingAdditionalLicenses,
pendingTotalLicenses: subscription.recurly.pendingTotalLicenses,
}}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</>
)}
</>
)
}

View file

@ -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 (
<p>
<Trans
i18nKey="youre_on_free_trial_which_ends_on"
values={{ date: subscription.recurly.trialEndsAtFormatted }}
components={[
// eslint-disable-next-line react/jsx-key
<strong />,
]}
/>
</p>
)
}

View file

@ -1,6 +1,10 @@
import { createContext, ReactNode, useContext, useMemo, useState } from 'react'
type SubscriptionDashboardContextValue = {
recurlyLoadError: boolean
setRecurlyLoadError: React.Dispatch<React.SetStateAction<boolean>>
showCancellation: boolean
setShowCancellation: React.Dispatch<React.SetStateAction<boolean>>
showChangePersonalPlan: boolean
setShowChangePersonalPlan: React.Dispatch<React.SetStateAction<boolean>>
}
@ -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<SubscriptionDashboardContextValue>(
() => ({
recurlyLoadError,
setRecurlyLoadError,
showCancellation,
setShowCancellation,
showChangePersonalPlan,
setShowChangePersonalPlan,
}),
[showChangePersonalPlan, setShowChangePersonalPlan]
[
recurlyLoadError,
setRecurlyLoadError,
showCancellation,
setShowCancellation,
showChangePersonalPlan,
setShowChangePersonalPlan,
]
)
return (

View file

@ -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": "Youre on a free trial which ends on <0>__date__</0>.",
"zh-CN": "Chinese",
"zip_contents_too_large": "Zip contents too large",
"zotero": "Zotero",

View file

@ -11,10 +11,12 @@ import {
describe('<PersonalSubscription />', 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('<PersonalSubscription />', 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(
<SubscriptionDashboardProvider>
@ -105,4 +110,35 @@ describe('<PersonalSubscription />', 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(
<SubscriptionDashboardProvider>
<PersonalSubscription subscription={annualActiveSubscription} />
</SubscriptionDashboardProvider>
)
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(
<SubscriptionDashboardProvider>
<PersonalSubscription subscription={annualActiveSubscription} />
</SubscriptionDashboardProvider>
)
expect(screen.queryByRole('alert')).to.be.null
screen.getByText('Change plan')
})
})
})

View file

@ -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('<ActiveSubscription />', 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('<ActiveSubscription />', 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('<ActiveSubscription />', function () {
exact: false,
})
})
it('does not show "Change plan" option for group plans', function () {
render(
<SubscriptionDashboardProvider>
<ActiveSubsciption subscription={groupActiveSubscription} />
</SubscriptionDashboardProvider>
)
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(
<SubscriptionDashboardProvider>
<ActiveSubsciption subscription={activePastDueSubscription} />
</SubscriptionDashboardProvider>
)
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(
<SubscriptionDashboardProvider>
<ActiveSubsciption
subscription={groupActiveSubscriptionWithPendingLicenseChange}
/>
</SubscriptionDashboardProvider>
)
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(
<SubscriptionDashboardProvider>
<ActiveSubsciption subscription={subscription} />
</SubscriptionDashboardProvider>
)
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(
<SubscriptionDashboardProvider>
<ActiveSubsciption subscription={trialSubscription} />
</SubscriptionDashboardProvider>
)
screen.getByText('Youre 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(
<SubscriptionDashboardProvider>
<ActiveSubsciption subscription={annualActiveSubscription} />
</SubscriptionDashboardProvider>
)
// 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('Wed love you to stay')
})
})

View file

@ -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('<SubscriptionDashboard />', function () {
beforeEach(function () {
@ -13,7 +14,11 @@ describe('<SubscriptionDashboard />', function () {
describe('Free Plan', function () {
it('does not render the "Get the most out of your" subscription text', function () {
render(<SubscriptionDashboard />)
render(
<SubscriptionDashboardProvider>
<SubscriptionDashboard />
</SubscriptionDashboardProvider>
)
const text = screen.queryByText('Get the most out of your', {
exact: false,
})

View file

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

View file

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

View file

@ -31,5 +31,6 @@ declare global {
_reportAcePerf: () => void
MathJax: Record<string, any>
overallThemes: OverallThemeMeta[]
recurly?: object
}
}