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:
Jessica Lawshe 2023-01-26 10:48:30 -06:00 committed by Copybot
parent 14039c6729
commit 2499ecadcc
19 changed files with 823 additions and 36 deletions

View file

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

View file

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

View file

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

View file

@ -28,18 +28,22 @@ function PremiumFeaturesLink() {
if (featuresPageVariant === 'new') {
return (
<Trans
i18nKey="get_most_subscription_by_checking_features"
components={[featuresPageLink]}
/>
<p>
<Trans
i18nKey="get_most_subscription_by_checking_features"
components={[featuresPageLink]}
/>
</p>
)
}
return (
<Trans
i18nKey="get_most_subscription_by_checking_premium_features"
components={[featuresPageLink]}
/>
<p>
<Trans
i18nKey="get_most_subscription_by_checking_premium_features"
components={[featuresPageLink]}
/>
</p>
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 */}
</>
)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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: [],
},
}

View 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
}