From 2499ecadccd294a26da9b02d325d9910d57628b6 Mon Sep 17 00:00:00 2001 From: Jessica Lawshe Date: Thu, 26 Jan 2023 10:48:30 -0600 Subject: [PATCH] Merge pull request #11340 from overleaf/jel-react-personal-subscription-dash [web] Begin to migrate personal subscription dash commons to React GitOrigin-RevId: 43c096bd72199c8c0a7b40c8fd84a0a12c108217 --- .../web/frontend/extracted-translations.json | 14 ++ .../dashboard/institution-memberships.tsx | 2 + .../dashboard/personal-subscription.tsx | 68 +++++++ .../dashboard/premium-features-link.tsx | 20 +- .../components/dashboard/root.tsx | 7 +- .../components/dashboard/states/active.tsx | 111 +++++++++++ .../components/dashboard/states/canceled.tsx | 57 ++++++ .../components/dashboard/states/expired.tsx | 28 +++ .../dashboard/subscription-dashboard.tsx | 10 +- .../components/shared/price-exceptions.tsx | 13 ++ .../subscription-dashboard-context.tsx | 42 +++++ services/web/locales/en.json | 1 + .../institution-memberships.test.tsx | 7 + .../dashboard/personal-subscription.test.tsx | 108 +++++++++++ .../dashboard/states/active.test.tsx | 92 ++++++++++ .../dashboard/states/expired.test.tsx | 21 +++ .../dashboard/subscription-dashboard.test.tsx | 24 --- .../subscription/fixtures/subscriptions.tsx | 173 ++++++++++++++++++ .../subscription/dashboard/subscription.ts | 61 ++++++ 19 files changed, 823 insertions(+), 36 deletions(-) create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/active.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/canceled.tsx create mode 100644 services/web/frontend/js/features/subscription/components/dashboard/states/expired.tsx create mode 100644 services/web/frontend/js/features/subscription/components/shared/price-exceptions.tsx create mode 100644 services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx create mode 100644 services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx create mode 100644 services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx create mode 100644 services/web/test/frontend/features/subscription/components/dashboard/states/expired.test.tsx create mode 100644 services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx create mode 100644 services/web/types/subscription/dashboard/subscription.ts diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index f963f6620e..50a95aa28d 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -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": "", diff --git a/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx b/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx index 58336cc681..601a31b5ec 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/institution-memberships.tsx @@ -1,5 +1,6 @@ import { Trans } from 'react-i18next' import { Institution } from '../../../../../../types/institution' +import PremiumFeaturesLink from './premium-features-link' type InstitutionMembershipsProps = { memberships?: Array @@ -43,6 +44,7 @@ function InstitutionMemberships({ memberships }: InstitutionMembershipsProps) {
))} + {memberships.length > 0 && } ) diff --git a/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx new file mode 100644 index 0000000000..ca55fee085 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx @@ -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 ( + <> +
+ {t('account_has_past_due_invoice_change_plan_warning')}{' '} + + {t('view_your_invoices')} + +
+ + ) +} + +function PersonalSubscriptionStates({ + subscription, + state, +}: { + subscription: Subscription + state?: string +}) { + const { t } = useTranslation() + + if (state === 'active') { + return + } else if (state === 'canceled') { + return + } else if (state === 'expired') { + return + } 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' && ( + + )} + + + ) +} + +export default PersonalSubscription diff --git a/services/web/frontend/js/features/subscription/components/dashboard/premium-features-link.tsx b/services/web/frontend/js/features/subscription/components/dashboard/premium-features-link.tsx index ecc60bb7a0..f57c0b6a78 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/premium-features-link.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/premium-features-link.tsx @@ -28,18 +28,22 @@ function PremiumFeaturesLink() { if (featuresPageVariant === 'new') { return ( - +

+ +

) } return ( - +

+ +

) } diff --git a/services/web/frontend/js/features/subscription/components/dashboard/root.tsx b/services/web/frontend/js/features/subscription/components/dashboard/root.tsx index 2fe706e027..e4553066cc 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/root.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/root.tsx @@ -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 + return ( + + + + ) } export default Root diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/active.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/active.tsx new file mode 100644 index 0000000000..ade818d5e3 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/active.tsx @@ -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 ( + <> +

{t('change_plan')}

+

+ TODO: change subscription placeholder +

+ + ) +} + +export function ActiveSubsciption({ + subscription, +}: { + subscription: Subscription +}) { + const { t } = useTranslation() + const { setShowChangePersonalPlan } = useSubscriptionDashboardContext() + + return ( + <> +

+ , + ]} + /> + {subscription.pendingPlan && + subscription.pendingPlan.name !== subscription.plan.name && ( + <> + {' '} + , + ]} + /> + + )}{' '} + {/* TODO: pending_additional_licenses */} + {/* TODO: additionalLicenses */} + +

+ {subscription.pendingPlan && ( +

{t('want_change_to_apply_before_plan_end')}

+ )} + {/* TODO: groupPlan */} + {/* TODO: trialEndsAtFormatted */} +

+ , + // eslint-disable-next-line react/jsx-key + , + ]} + /> +

+ + + + {t('update_your_billing_details')} + {' '} + + {t('view_your_invoices')} + {' '} + {/* TODO: cancel button */} + + + ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/canceled.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/canceled.tsx new file mode 100644 index 0000000000..99aec25527 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/canceled.tsx @@ -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 ( + <> +

+ , + ]} + /> +

+

+ , + ]} + /> +

+ +

+ + {t('view_your_invoices')} + +

+
+ + +
+ + ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/states/expired.tsx b/services/web/frontend/js/features/subscription/components/dashboard/states/expired.tsx new file mode 100644 index 0000000000..ed8130fbc0 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/dashboard/states/expired.tsx @@ -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 ( + <> +

{t('your_subscription_has_expired')}

+

+ + {t('view_your_invoices')} + {' '} + + {t('create_new_subscription')} + +

+ + ) +} diff --git a/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx b/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx index 577b42210e..4f7818303e 100644 --- a/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx +++ b/services/web/frontend/js/features/subscription/components/dashboard/subscription-dashboard.tsx @@ -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 (
@@ -19,7 +22,8 @@ function SubscriptionDashboard() {
- {hasDisplayedSubscription ? : } + + {!hasDisplayedSubscription && } diff --git a/services/web/frontend/js/features/subscription/components/shared/price-exceptions.tsx b/services/web/frontend/js/features/subscription/components/shared/price-exceptions.tsx new file mode 100644 index 0000000000..17a2841682 --- /dev/null +++ b/services/web/frontend/js/features/subscription/components/shared/price-exceptions.tsx @@ -0,0 +1,13 @@ +import { useTranslation } from 'react-i18next' + +export function PriceExceptions() { + const { t } = useTranslation() + return ( + <> +

+ * {t('subject_to_additional_vat')} +

+ {/* TODO: activeCoupons */} + + ) +} diff --git a/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx new file mode 100644 index 0000000000..031846f674 --- /dev/null +++ b/services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx @@ -0,0 +1,42 @@ +import { createContext, ReactNode, useContext, useMemo, useState } from 'react' + +type SubscriptionDashboardContextValue = { + showChangePersonalPlan: boolean + setShowChangePersonalPlan: React.Dispatch> +} + +export const SubscriptionDashboardContext = createContext< + SubscriptionDashboardContextValue | undefined +>(undefined) + +export function SubscriptionDashboardProvider({ + children, +}: { + children: ReactNode +}) { + const [showChangePersonalPlan, setShowChangePersonalPlan] = useState(false) + + const value = useMemo( + () => ({ + showChangePersonalPlan, + setShowChangePersonalPlan, + }), + [showChangePersonalPlan, setShowChangePersonalPlan] + ) + + return ( + + {children} + + ) +} + +export function useSubscriptionDashboardContext() { + const context = useContext(SubscriptionDashboardContext) + if (!context) { + throw new Error( + 'SubscriptionDashboardContext is only available inside SubscriptionDashboardProvider' + ) + } + return context +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 04718ee38b..046471831c 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -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", diff --git a/services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx index ecdf4b9337..8b76fac67b 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/institution-memberships.test.tsx @@ -55,4 +55,11 @@ describe('', 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() + screen.getByText('Get the most out of your', { + exact: false, + }) + }) }) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx new file mode 100644 index 0000000000..b958261624 --- /dev/null +++ b/services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx @@ -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('', 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( + + + + ) + expect(container.firstChild).to.be.null + }) + }) + + describe('subscription states ', function () { + it('renders the active dash', function () { + render( + + + + ) + + screen.getByText('You are currently subscribed to the', { exact: false }) + }) + + it('renders the canceled dash', function () { + render( + + + + ) + 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( + + + + ) + 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( + + + + ) + 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( + + + + ) + 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) + }) + }) +}) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx new file mode 100644 index 0000000000..3cf878e03c --- /dev/null +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/active.test.tsx @@ -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('', 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( + + + + ) + expectedInActiveSubscription(annualActiveSubscription) + }) + + it('shows change plan UI when button clicked', function () { + render( + + + + ) + + 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( + + + + ) + + 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, + }) + }) +}) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/states/expired.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/states/expired.test.tsx new file mode 100644 index 0000000000..1fb4838817 --- /dev/null +++ b/services/web/test/frontend/features/subscription/components/dashboard/states/expired.test.tsx @@ -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('', function () { + beforeEach(function () { + window.metaAttributesCache = new Map() + }) + + afterEach(function () { + window.metaAttributesCache = new Map() + }) + + it('renders the invoices link', function () { + render() + + screen.getByText('View Your Invoices', { + exact: false, + }) + }) +}) diff --git a/services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx b/services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx index da8a1d8064..bca237f99a 100644 --- a/services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx +++ b/services/web/test/frontend/features/subscription/components/dashboard/subscription-dashboard.test.tsx @@ -11,30 +11,6 @@ describe('', 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() - 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() diff --git a/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx b/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx new file mode 100644 index 0000000000..2f7fa5e2e7 --- /dev/null +++ b/services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx @@ -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: [], + }, +} diff --git a/services/web/types/subscription/dashboard/subscription.ts b/services/web/types/subscription/dashboard/subscription.ts new file mode 100644 index 0000000000..f0b8dc731e --- /dev/null +++ b/services/web/types/subscription/dashboard/subscription.ts @@ -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 + trial_ends_at: Nullable + 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 +}