mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-21 20:47:08 -05:00
Merge pull request #11340 from overleaf/jel-react-personal-subscription-dash
[web] Begin to migrate personal subscription dash commons to React GitOrigin-RevId: 43c096bd72199c8c0a7b40c8fd84a0a12c108217
This commit is contained in:
parent
14039c6729
commit
2499ecadcc
19 changed files with 823 additions and 36 deletions
|
@ -9,6 +9,7 @@
|
|||
"access_denied": "",
|
||||
"access_your_projects_with_git": "",
|
||||
"account_has_been_link_to_institution_account": "",
|
||||
"account_has_past_due_invoice_change_plan_warning": "",
|
||||
"account_not_linked_to_dropbox": "",
|
||||
"account_settings": "",
|
||||
"acct_linked_to_institution_acct_2": "",
|
||||
|
@ -71,6 +72,7 @@
|
|||
"change_or_cancel-or": "",
|
||||
"change_owner": "",
|
||||
"change_password": "",
|
||||
"change_plan": "",
|
||||
"change_primary_email_address_instructions": "",
|
||||
"change_project_owner": "",
|
||||
"chat": "",
|
||||
|
@ -118,10 +120,12 @@
|
|||
"create": "",
|
||||
"create_first_project": "",
|
||||
"create_new_folder": "",
|
||||
"create_new_subscription": "",
|
||||
"create_project_in_github": "",
|
||||
"created_at": "",
|
||||
"creating": "",
|
||||
"current_password": "",
|
||||
"currently_subscribed_to_plan": "",
|
||||
"date_and_owner": "",
|
||||
"delete": "",
|
||||
"delete_account": "",
|
||||
|
@ -431,6 +435,7 @@
|
|||
"new_project": "",
|
||||
"new_to_latex_look_at": "",
|
||||
"newsletter": "",
|
||||
"next_payment_of_x_collectected_on_y": "",
|
||||
"no_existing_password": "",
|
||||
"no_messages": "",
|
||||
"no_new_commits_in_github": "",
|
||||
|
@ -502,6 +507,7 @@
|
|||
"priority_support": "",
|
||||
"privacy_policy": "",
|
||||
"private": "",
|
||||
"problem_with_subscription_contact_us": "",
|
||||
"processing": "",
|
||||
"professional": "",
|
||||
"project": "",
|
||||
|
@ -531,6 +537,7 @@
|
|||
"push_sharelatex_changes_to_github": "",
|
||||
"raw_logs": "",
|
||||
"raw_logs_description": "",
|
||||
"reactivate_subscription": "",
|
||||
"read_only": "",
|
||||
"read_only_token": "",
|
||||
"read_write_token": "",
|
||||
|
@ -652,8 +659,10 @@
|
|||
"stop_on_validation_error": "",
|
||||
"store_your_work": "",
|
||||
"subject": "",
|
||||
"subject_to_additional_vat": "",
|
||||
"submit_title": "",
|
||||
"subscription_admins_cannot_be_deleted": "",
|
||||
"subscription_canceled_and_terminate_on_x": "",
|
||||
"sure_you_want_to_delete": "",
|
||||
"switch_to_editor": "",
|
||||
"switch_to_pdf": "",
|
||||
|
@ -732,6 +741,7 @@
|
|||
"update": "",
|
||||
"update_account_info": "",
|
||||
"update_dropbox_settings": "",
|
||||
"update_your_billing_details": "",
|
||||
"upgrade": "",
|
||||
"upgrade_for_longer_compiles": "",
|
||||
"upgrade_now": "",
|
||||
|
@ -749,6 +759,8 @@
|
|||
"view_all": "",
|
||||
"view_logs": "",
|
||||
"view_pdf": "",
|
||||
"view_your_invoices": "",
|
||||
"want_change_to_apply_before_plan_end": "",
|
||||
"we_cant_find_any_sections_or_subsections_in_this_file": "",
|
||||
"we_logged_you_in": "",
|
||||
"welcome_to_sl": "",
|
||||
|
@ -763,8 +775,10 @@
|
|||
"your_affiliation_is_confirmed": "",
|
||||
"your_browser_does_not_support_this_feature": "",
|
||||
"your_message_to_collaborators": "",
|
||||
"your_plan_is_changing_at_term_end": "",
|
||||
"your_projects": "",
|
||||
"your_subscription": "",
|
||||
"your_subscription_has_expired": "",
|
||||
"zotero_groups_loading_error": "",
|
||||
"zotero_groups_relink": "",
|
||||
"zotero_integration": "",
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { Trans } from 'react-i18next'
|
||||
import { Institution } from '../../../../../../types/institution'
|
||||
import PremiumFeaturesLink from './premium-features-link'
|
||||
|
||||
type InstitutionMembershipsProps = {
|
||||
memberships?: Array<Institution>
|
||||
|
@ -43,6 +44,7 @@ function InstitutionMemberships({ memberships }: InstitutionMembershipsProps) {
|
|||
<hr />
|
||||
</div>
|
||||
))}
|
||||
{memberships.length > 0 && <PremiumFeaturesLink />}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Subscription } from '../../../../../../types/subscription/dashboard/subscription'
|
||||
import { ActiveSubsciption } from './states/active'
|
||||
import { CanceledSubsciption } from './states/canceled'
|
||||
import { ExpiredSubsciption } from './states/expired'
|
||||
|
||||
function PastDueSubscriptionAlert({
|
||||
subscription,
|
||||
}: {
|
||||
subscription: Subscription
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<div className="alert alert-danger" role="alert">
|
||||
{t('account_has_past_due_invoice_change_plan_warning')}{' '}
|
||||
<a
|
||||
href={subscription.recurly.accountManagementLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{t('view_your_invoices')}
|
||||
</a>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function PersonalSubscriptionStates({
|
||||
subscription,
|
||||
state,
|
||||
}: {
|
||||
subscription: Subscription
|
||||
state?: string
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (state === 'active') {
|
||||
return <ActiveSubsciption subscription={subscription} />
|
||||
} else if (state === 'canceled') {
|
||||
return <CanceledSubsciption subscription={subscription} />
|
||||
} else if (state === 'expired') {
|
||||
return <ExpiredSubsciption subscription={subscription} />
|
||||
} else {
|
||||
return <>{t('problem_with_subscription_contact_us')}</>
|
||||
}
|
||||
}
|
||||
|
||||
function PersonalSubscription({
|
||||
subscription,
|
||||
}: {
|
||||
subscription?: Subscription
|
||||
}) {
|
||||
const state = subscription?.recurly?.state
|
||||
|
||||
if (!subscription) return <></>
|
||||
|
||||
return (
|
||||
<>
|
||||
{subscription.recurly.account.has_past_due_invoice._ === 'true' && (
|
||||
<PastDueSubscriptionAlert subscription={subscription} />
|
||||
)}
|
||||
<PersonalSubscriptionStates subscription={subscription} state={state} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PersonalSubscription
|
|
@ -28,18 +28,22 @@ function PremiumFeaturesLink() {
|
|||
|
||||
if (featuresPageVariant === 'new') {
|
||||
return (
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="get_most_subscription_by_checking_features"
|
||||
components={[featuresPageLink]}
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="get_most_subscription_by_checking_premium_features"
|
||||
components={[featuresPageLink]}
|
||||
/>
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { SubscriptionDashboardProvider } from '../../context/subscription-dashboard-context'
|
||||
import useWaitForI18n from '../../../../shared/hooks/use-wait-for-i18n'
|
||||
import SubscriptionDashboard from './subscription-dashboard'
|
||||
|
||||
|
@ -8,7 +9,11 @@ function Root() {
|
|||
return null
|
||||
}
|
||||
|
||||
return <SubscriptionDashboard />
|
||||
return (
|
||||
<SubscriptionDashboardProvider>
|
||||
<SubscriptionDashboard />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default Root
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
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,57 @@
|
|||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { Subscription } from '../../../../../../../types/subscription/dashboard/subscription'
|
||||
import PremiumFeaturesLink from '../premium-features-link'
|
||||
|
||||
export function CanceledSubsciption({
|
||||
subscription,
|
||||
}: {
|
||||
subscription: Subscription
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="currently_subscribed_to_plan"
|
||||
values={{
|
||||
planName: subscription.plan.name,
|
||||
}}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="subscription_canceled_and_terminate_on_x"
|
||||
values={{
|
||||
terminateDate: subscription.recurly.nextPaymentDueAt,
|
||||
}}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
<PremiumFeaturesLink />
|
||||
<p>
|
||||
<a
|
||||
href={subscription.recurly.accountManagementLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="btn btn-secondary-info btn-secondary"
|
||||
>
|
||||
{t('view_your_invoices')}
|
||||
</a>
|
||||
</p>
|
||||
<form action="/user/subscription/reactivate" method="POST">
|
||||
<input type="hidden" name="_csrf" value={window.csrfToken} />
|
||||
<button type="submit" className="btn btn-primary">
|
||||
{t('reactivate_subscription')}
|
||||
</button>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
import { Subscription } from '../../../../../../../types/subscription/dashboard/subscription'
|
||||
|
||||
export function ExpiredSubsciption({
|
||||
subscription,
|
||||
}: {
|
||||
subscription: Subscription
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<p>{t('your_subscription_has_expired')}</p>
|
||||
<p>
|
||||
<a
|
||||
href={subscription.recurly.accountManagementLink}
|
||||
className="btn btn-secondary-info btn-secondary"
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{t('view_your_invoices')}
|
||||
</a>{' '}
|
||||
<a href="/user/subscription/plans" className="btn btn-primary">
|
||||
{t('create_new_subscription')}
|
||||
</a>
|
||||
</p>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -2,12 +2,15 @@ import { useTranslation } from 'react-i18next'
|
|||
import getMeta from '../../../../utils/meta'
|
||||
import InstitutionMemberships from './institution-memberships'
|
||||
import FreePlan from './free-plan'
|
||||
import PremiumFeaturesLink from './premium-features-link'
|
||||
import PersonalSubscription from './personal-subscription'
|
||||
|
||||
function SubscriptionDashboard() {
|
||||
const { t } = useTranslation()
|
||||
const institutionMemberships = getMeta('ol-currentInstitutionsWithLicence')
|
||||
const hasDisplayedSubscription = institutionMemberships?.length > 0
|
||||
const subscription = getMeta('ol-subscription')
|
||||
|
||||
const hasDisplayedSubscription =
|
||||
institutionMemberships?.length > 0 || subscription
|
||||
|
||||
return (
|
||||
<div className="container">
|
||||
|
@ -19,7 +22,8 @@ function SubscriptionDashboard() {
|
|||
</div>
|
||||
|
||||
<InstitutionMemberships memberships={institutionMemberships} />
|
||||
{hasDisplayedSubscription ? <PremiumFeaturesLink /> : <FreePlan />}
|
||||
<PersonalSubscription subscription={subscription} />
|
||||
{!hasDisplayedSubscription && <FreePlan />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
export function PriceExceptions() {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
<i>* {t('subject_to_additional_vat')}</i>
|
||||
</p>
|
||||
{/* TODO: activeCoupons */}
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
import { createContext, ReactNode, useContext, useMemo, useState } from 'react'
|
||||
|
||||
type SubscriptionDashboardContextValue = {
|
||||
showChangePersonalPlan: boolean
|
||||
setShowChangePersonalPlan: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
export const SubscriptionDashboardContext = createContext<
|
||||
SubscriptionDashboardContextValue | undefined
|
||||
>(undefined)
|
||||
|
||||
export function SubscriptionDashboardProvider({
|
||||
children,
|
||||
}: {
|
||||
children: ReactNode
|
||||
}) {
|
||||
const [showChangePersonalPlan, setShowChangePersonalPlan] = useState(false)
|
||||
|
||||
const value = useMemo<SubscriptionDashboardContextValue>(
|
||||
() => ({
|
||||
showChangePersonalPlan,
|
||||
setShowChangePersonalPlan,
|
||||
}),
|
||||
[showChangePersonalPlan, setShowChangePersonalPlan]
|
||||
)
|
||||
|
||||
return (
|
||||
<SubscriptionDashboardContext.Provider value={value}>
|
||||
{children}
|
||||
</SubscriptionDashboardContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export function useSubscriptionDashboardContext() {
|
||||
const context = useContext(SubscriptionDashboardContext)
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
'SubscriptionDashboardContext is only available inside SubscriptionDashboardProvider'
|
||||
)
|
||||
}
|
||||
return context
|
||||
}
|
|
@ -1137,6 +1137,7 @@
|
|||
"quoted_text_in": "Quoted text in",
|
||||
"raw_logs": "Raw logs",
|
||||
"raw_logs_description": "Raw logs from the LaTeX compiler",
|
||||
"reactivate_subscription": "Reactivate your subscription",
|
||||
"read_only": "Read Only",
|
||||
"read_only_token": "Read-Only Token",
|
||||
"read_write_token": "Read-Write Token",
|
||||
|
|
|
@ -55,4 +55,11 @@ describe('<InstitutionMemberships />', function () {
|
|||
'Sorry, something went wrong. Subscription information related to institutional affiliations may not be displayed. Please try again later.'
|
||||
)
|
||||
})
|
||||
|
||||
it('renders the "Get the most out of your" subscription text when a user has a subscription', function () {
|
||||
render(<InstitutionMemberships memberships={memberships} />)
|
||||
screen.getByText('Get the most out of your', {
|
||||
exact: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
import { expect } from 'chai'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import PersonalSubscription from '../../../../../../frontend/js/features/subscription/components/dashboard/personal-subscription'
|
||||
import { SubscriptionDashboardProvider } from '../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
|
||||
import {
|
||||
annualActiveSubscription,
|
||||
canceledSubscription,
|
||||
pastDueExpiredSubscription,
|
||||
} from '../../fixtures/subscriptions'
|
||||
|
||||
describe('<PersonalSubscription />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
describe('no subscription', function () {
|
||||
it('returns empty container', function () {
|
||||
const { container } = render(
|
||||
<SubscriptionDashboardProvider>
|
||||
<PersonalSubscription subscription={undefined} />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
expect(container.firstChild).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
describe('subscription states ', function () {
|
||||
it('renders the active dash', function () {
|
||||
render(
|
||||
<SubscriptionDashboardProvider>
|
||||
<PersonalSubscription subscription={annualActiveSubscription} />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
|
||||
screen.getByText('You are currently subscribed to the', { exact: false })
|
||||
})
|
||||
|
||||
it('renders the canceled dash', function () {
|
||||
render(
|
||||
<SubscriptionDashboardProvider>
|
||||
<PersonalSubscription subscription={canceledSubscription} />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
screen.getByText(
|
||||
'Your subscription has been canceled and will terminate on',
|
||||
{ exact: false }
|
||||
)
|
||||
screen.getByText(canceledSubscription.recurly.nextPaymentDueAt, {
|
||||
exact: false,
|
||||
})
|
||||
|
||||
screen.getByText('No further payments will be taken.', { exact: false })
|
||||
|
||||
screen.getByText(
|
||||
'Get the most out of your Overleaf subscription by checking out the list of',
|
||||
{ exact: false }
|
||||
)
|
||||
|
||||
screen.getByRole('link', { name: 'View Your Invoices' })
|
||||
screen.getByRole('button', { name: 'Reactivate your subscription' })
|
||||
})
|
||||
|
||||
it('renders the expired dash', function () {
|
||||
render(
|
||||
<SubscriptionDashboardProvider>
|
||||
<PersonalSubscription subscription={pastDueExpiredSubscription} />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
screen.getByText('Your subscription has expired.')
|
||||
})
|
||||
|
||||
it('renders error message when an unknown subscription state', function () {
|
||||
const withStateDeleted = Object.assign({}, annualActiveSubscription)
|
||||
withStateDeleted.recurly.state = undefined
|
||||
render(
|
||||
<SubscriptionDashboardProvider>
|
||||
<PersonalSubscription subscription={withStateDeleted} />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
screen.getByText(
|
||||
'There is a problem with your subscription. Please contact us for more information.'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('past due subscription', function () {
|
||||
it('renders error alert', function () {
|
||||
render(
|
||||
<SubscriptionDashboardProvider>
|
||||
<PersonalSubscription subscription={pastDueExpiredSubscription} />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
screen.getByRole('alert')
|
||||
screen.getByText(
|
||||
'Your account currently has a past due invoice. You will not be able to change your plan until this is resolved.',
|
||||
{ exact: false }
|
||||
)
|
||||
const invoiceLinks = screen.getAllByText('View Your Invoices', {
|
||||
exact: false,
|
||||
})
|
||||
expect(invoiceLinks.length).to.equal(2)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,92 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { ActiveSubsciption } from '../../../../../../../frontend/js/features/subscription/components/dashboard/states/active'
|
||||
import { SubscriptionDashboardProvider } from '../../../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
|
||||
import { Subscription } from '../../../../../../../types/subscription/dashboard/subscription'
|
||||
import {
|
||||
annualActiveSubscription,
|
||||
pendingSubscriptionChange,
|
||||
} from '../../../fixtures/subscriptions'
|
||||
|
||||
describe('<ActiveSubscription />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
function expectedInActiveSubscription(subscription: Subscription) {
|
||||
// sentence broken up by bolding
|
||||
screen.getByText('You are currently subscribed to the', { exact: false })
|
||||
screen.getByText(subscription.plan.name, { exact: false })
|
||||
|
||||
screen.getByRole('button', { name: 'Change plan' })
|
||||
|
||||
// sentence broken up by bolding
|
||||
screen.getByText('The next payment of', { exact: false })
|
||||
screen.getByText(annualActiveSubscription.recurly.displayPrice, {
|
||||
exact: false,
|
||||
})
|
||||
screen.getByText('will be collected on', { exact: false })
|
||||
screen.getByText(annualActiveSubscription.recurly.nextPaymentDueAt, {
|
||||
exact: false,
|
||||
})
|
||||
|
||||
// sentence broken up by link
|
||||
screen.getByText(
|
||||
'Get the most out of your Overleaf subscription by checking out the list of',
|
||||
{ exact: false }
|
||||
)
|
||||
|
||||
screen.getByText(
|
||||
'* Prices may be subject to additional VAT, depending on your country.'
|
||||
)
|
||||
|
||||
screen.getByRole('link', { name: 'Update Your Billing Details' })
|
||||
screen.getByRole('link', { name: 'View Your Invoices' })
|
||||
}
|
||||
|
||||
it('renders the dash annual active subscription', function () {
|
||||
render(
|
||||
<SubscriptionDashboardProvider>
|
||||
<ActiveSubsciption subscription={annualActiveSubscription} />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
expectedInActiveSubscription(annualActiveSubscription)
|
||||
})
|
||||
|
||||
it('shows change plan UI when button clicked', function () {
|
||||
render(
|
||||
<SubscriptionDashboardProvider>
|
||||
<ActiveSubsciption subscription={annualActiveSubscription} />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||
fireEvent.click(button)
|
||||
|
||||
// confirm main dash UI UI still shown
|
||||
expectedInActiveSubscription(annualActiveSubscription)
|
||||
|
||||
// TODO: add change plan UI
|
||||
screen.getByText('change subscription placeholder', { exact: false })
|
||||
})
|
||||
|
||||
it('notes when user is changing plan at end of current plan term', function () {
|
||||
render(
|
||||
<SubscriptionDashboardProvider>
|
||||
<ActiveSubsciption subscription={pendingSubscriptionChange} />
|
||||
</SubscriptionDashboardProvider>
|
||||
)
|
||||
|
||||
expectedInActiveSubscription(pendingSubscriptionChange)
|
||||
|
||||
screen.getByText('Your plan is changing to', { exact: false })
|
||||
|
||||
screen.getByText(pendingSubscriptionChange.pendingPlan!.name)
|
||||
screen.getByText(' at the end of the current billing period', {
|
||||
exact: false,
|
||||
})
|
||||
})
|
||||
})
|
|
@ -0,0 +1,21 @@
|
|||
import { render, screen } from '@testing-library/react'
|
||||
import { ExpiredSubsciption } from '../../../../../../../frontend/js/features/subscription/components/dashboard/states/expired'
|
||||
import { pastDueExpiredSubscription } from '../../../fixtures/subscriptions'
|
||||
|
||||
describe('<ExpiredSubsciption />', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
afterEach(function () {
|
||||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
it('renders the invoices link', function () {
|
||||
render(<ExpiredSubsciption subscription={pastDueExpiredSubscription} />)
|
||||
|
||||
screen.getByText('View Your Invoices', {
|
||||
exact: false,
|
||||
})
|
||||
})
|
||||
})
|
|
@ -11,30 +11,6 @@ describe('<SubscriptionDashboard />', function () {
|
|||
window.metaAttributesCache = new Map()
|
||||
})
|
||||
|
||||
describe('Institution affiliation with commons', function () {
|
||||
beforeEach(function () {
|
||||
window.metaAttributesCache.set('ol-currentInstitutionsWithLicence', [
|
||||
{
|
||||
id: 9258,
|
||||
name: 'Test University',
|
||||
commonsAccount: true,
|
||||
isUniversity: true,
|
||||
confirmed: true,
|
||||
ssoBeta: false,
|
||||
ssoEnabled: false,
|
||||
maxConfirmationMonths: 6,
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('renders the "Get the most out of your" subscription text when a user has a subscription', function () {
|
||||
render(<SubscriptionDashboard />)
|
||||
screen.getByText('Get the most out of your', {
|
||||
exact: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Free Plan', function () {
|
||||
it('does not render the "Get the most out of your" subscription text', function () {
|
||||
render(<SubscriptionDashboard />)
|
||||
|
|
|
@ -0,0 +1,173 @@
|
|||
import { Subscription } from '../../../../../types/subscription/dashboard/subscription'
|
||||
const dateformat = require('dateformat')
|
||||
const today = new Date()
|
||||
const oneYearFromToday = new Date().setFullYear(today.getFullYear() + 1)
|
||||
const nextPaymentDueAt = dateformat(oneYearFromToday, 'dS mmmm yyyy')
|
||||
|
||||
export const annualActiveSubscription: Subscription = {
|
||||
manager_ids: ['abc123'],
|
||||
member_ids: [],
|
||||
invited_emails: [],
|
||||
groupPlan: false,
|
||||
membersLimit: 0,
|
||||
_id: 'def456',
|
||||
admin_id: 'abc123',
|
||||
teamInvites: [],
|
||||
planCode: 'collaborator-annual',
|
||||
recurlySubscription_id: 'ghi789',
|
||||
plan: {
|
||||
planCode: 'collaborator-annual',
|
||||
name: 'Standard (Collaborator) Annual',
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
features: {},
|
||||
featureDescription: [],
|
||||
},
|
||||
recurly: {
|
||||
tax: 0,
|
||||
taxRate: 0,
|
||||
billingDetailsLink: '/user/subscription/recurly/billing-details',
|
||||
accountManagementLink: '/user/subscription/recurly/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
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: '$199.00',
|
||||
},
|
||||
}
|
||||
|
||||
export const pastDueExpiredSubscription: Subscription = {
|
||||
manager_ids: ['abc123'],
|
||||
member_ids: [],
|
||||
invited_emails: [],
|
||||
groupPlan: false,
|
||||
membersLimit: 0,
|
||||
_id: 'def456',
|
||||
admin_id: 'abc123',
|
||||
teamInvites: [],
|
||||
planCode: 'collaborator-annual',
|
||||
recurlySubscription_id: 'ghi789',
|
||||
plan: {
|
||||
planCode: 'collaborator-annual',
|
||||
name: 'Standard (Collaborator) Annual',
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
features: {},
|
||||
featureDescription: [],
|
||||
},
|
||||
recurly: {
|
||||
tax: 0,
|
||||
taxRate: 0,
|
||||
billingDetailsLink: '/user/subscription/recurly/billing-details',
|
||||
accountManagementLink: '/user/subscription/recurly/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
currency: 'USD',
|
||||
state: 'expired',
|
||||
trialEndsAtFormatted: null,
|
||||
trial_ends_at: null,
|
||||
activeCoupons: [],
|
||||
account: {
|
||||
has_canceled_subscription: { _: 'false', $: { type: 'boolean' } },
|
||||
has_past_due_invoice: { _: 'true', $: { type: 'boolean' } },
|
||||
},
|
||||
displayPrice: '$199.00',
|
||||
},
|
||||
}
|
||||
|
||||
export const canceledSubscription: Subscription = {
|
||||
manager_ids: ['abc123'],
|
||||
member_ids: [],
|
||||
invited_emails: [],
|
||||
groupPlan: false,
|
||||
membersLimit: 0,
|
||||
_id: 'def456',
|
||||
admin_id: 'abc123',
|
||||
teamInvites: [],
|
||||
planCode: 'collaborator-annual',
|
||||
recurlySubscription_id: 'ghi789',
|
||||
plan: {
|
||||
planCode: 'collaborator-annual',
|
||||
name: 'Standard (Collaborator) Annual',
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
features: {},
|
||||
featureDescription: [],
|
||||
},
|
||||
recurly: {
|
||||
tax: 0,
|
||||
taxRate: 0,
|
||||
billingDetailsLink: '/user/subscription/recurly/billing-details',
|
||||
accountManagementLink: '/user/subscription/recurly/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
currency: 'USD',
|
||||
state: 'canceled',
|
||||
trialEndsAtFormatted: null,
|
||||
trial_ends_at: null,
|
||||
activeCoupons: [],
|
||||
account: {
|
||||
has_canceled_subscription: { _: 'true', $: { type: 'boolean' } },
|
||||
has_past_due_invoice: { _: 'false', $: { type: 'boolean' } },
|
||||
},
|
||||
displayPrice: '$199.00',
|
||||
},
|
||||
}
|
||||
|
||||
export const pendingSubscriptionChange: Subscription = {
|
||||
manager_ids: ['abc123'],
|
||||
member_ids: [],
|
||||
invited_emails: [],
|
||||
groupPlan: false,
|
||||
membersLimit: 0,
|
||||
_id: 'def456',
|
||||
admin_id: 'abc123',
|
||||
teamInvites: [],
|
||||
planCode: 'collaborator-annual',
|
||||
recurlySubscription_id: 'ghi789',
|
||||
plan: {
|
||||
planCode: 'collaborator-annual',
|
||||
name: 'Standard (Collaborator) Annual',
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
features: {},
|
||||
featureDescription: [],
|
||||
},
|
||||
recurly: {
|
||||
tax: 0,
|
||||
taxRate: 0,
|
||||
billingDetailsLink: '/user/subscription/recurly/billing-details',
|
||||
accountManagementLink: '/user/subscription/recurly/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
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: '$199.00',
|
||||
},
|
||||
pendingPlan: {
|
||||
planCode: 'professional-annual',
|
||||
name: 'Professional Annual',
|
||||
price_in_cents: 42900,
|
||||
annual: true,
|
||||
features: {},
|
||||
featureDescription: [],
|
||||
},
|
||||
}
|
61
services/web/types/subscription/dashboard/subscription.ts
Normal file
61
services/web/types/subscription/dashboard/subscription.ts
Normal file
|
@ -0,0 +1,61 @@
|
|||
import { Nullable } from '../../utils'
|
||||
|
||||
type Plan = {
|
||||
planCode: string
|
||||
name: string
|
||||
price_in_cents: number
|
||||
annual?: boolean
|
||||
features: object
|
||||
hideFromUsers?: boolean
|
||||
featureDescription: object[]
|
||||
}
|
||||
|
||||
type SubscriptionState = 'active' | 'canceled' | 'expired'
|
||||
|
||||
export type Subscription = {
|
||||
_id: string
|
||||
admin_id: string
|
||||
manager_ids: string[]
|
||||
member_ids: string[]
|
||||
invited_emails: string[]
|
||||
groupPlan: boolean
|
||||
membersLimit: number
|
||||
teamInvites: object[]
|
||||
planCode: string
|
||||
recurlySubscription_id: string
|
||||
plan: Plan
|
||||
recurly: {
|
||||
tax: number
|
||||
taxRate: number
|
||||
billingDetailsLink: string
|
||||
accountManagementLink: string
|
||||
additionalLicenses: number
|
||||
totalLicenses: number
|
||||
nextPaymentDueAt: string
|
||||
currency: string
|
||||
state?: SubscriptionState
|
||||
trialEndsAtFormatted: Nullable<string>
|
||||
trial_ends_at: Nullable<string>
|
||||
activeCoupons: any[] // TODO: confirm type in array
|
||||
account: {
|
||||
// data via Recurly API
|
||||
has_canceled_subscription: {
|
||||
_: 'false' | 'true'
|
||||
$: {
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
has_past_due_invoice: {
|
||||
_: 'false' | 'true'
|
||||
$: {
|
||||
type: 'boolean'
|
||||
}
|
||||
}
|
||||
}
|
||||
displayPrice: string
|
||||
currentPlanDisplayPrice?: string
|
||||
pendingAdditionalLicenses?: number
|
||||
pendingTotalLicenses?: number
|
||||
}
|
||||
pendingPlan?: Plan
|
||||
}
|
Loading…
Reference in a new issue