mirror of
https://github.com/overleaf/overleaf.git
synced 2025-04-05 06:09:20 +00:00
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:
parent
2499ecadcc
commit
dad1460d71
18 changed files with 696 additions and 120 deletions
|
@ -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)
|
||||
|
|
|
@ -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": "",
|
||||
|
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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 />
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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 />,
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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('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(
|
||||
<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('We’d love you to stay')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -31,5 +31,6 @@ declare global {
|
|||
_reportAcePerf: () => void
|
||||
MathJax: Record<string, any>
|
||||
overallThemes: OverallThemeMeta[]
|
||||
recurly?: object
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue