Explorar el Código

Merge pull request #11885 from overleaf/ab-personal-sub-custom-react

[web] Display a message for custom personal subscriptions in React dash

GitOrigin-RevId: c70986ffacfdfdeccd3675385e2e5dba1a2ad61f
Alexandre Bourdin hace 3 años
padre
commit
d2fb800174

+ 1 - 1
services/web/app/views/subscriptions/dashboard-react.pug

@@ -7,6 +7,7 @@ block head-scripts
 	script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
 	script(type="text/javascript", nonce=scriptNonce, src="https://js.recurly.com/v4/recurly.js")
 
 
 block append meta
 block append meta
+	meta(name="ol-subscription" data-type="json" content=personalSubscription)
 	meta(name="ol-managedGroupSubscriptions" data-type="json" content=managedGroupSubscriptions)
 	meta(name="ol-managedGroupSubscriptions" data-type="json" content=managedGroupSubscriptions)
 	meta(name="ol-memberGroupSubscriptions" data-type="json" content=memberGroupSubscriptions)
 	meta(name="ol-memberGroupSubscriptions" data-type="json" content=memberGroupSubscriptions)
 	meta(name="ol-managedInstitutions" data-type="json" content=managedInstitutions)
 	meta(name="ol-managedInstitutions" data-type="json" content=managedInstitutions)
@@ -17,7 +18,6 @@ block append meta
 	meta(name="ol-plans", data-type="json" content=plans)
 	meta(name="ol-plans", data-type="json" content=plans)
 	if (personalSubscription && personalSubscription.recurly)
 	if (personalSubscription && personalSubscription.recurly)
 		meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
 		meta(name="ol-recurlyApiKey" content=settings.apis.recurly.publicKey)
-		meta(name="ol-subscription" data-type="json" content=personalSubscription)
 		meta(name="ol-recommendedCurrency" content=personalSubscription.recurly.currency)
 		meta(name="ol-recommendedCurrency" content=personalSubscription.recurly.currency)
 		meta(name="ol-groupPlans" data-type="json" content=groupPlans)
 		meta(name="ol-groupPlans" data-type="json" content=groupPlans)
 
 

+ 1 - 0
services/web/frontend/extracted-translations.json

@@ -564,6 +564,7 @@
   "please_compile_pdf_before_word_count": "",
   "please_compile_pdf_before_word_count": "",
   "please_confirm_email": "",
   "please_confirm_email": "",
   "please_confirm_your_email_before_making_it_default": "",
   "please_confirm_your_email_before_making_it_default": "",
+  "please_contact_support_to_makes_change_to_your_plan": "",
   "please_get_in_touch": "",
   "please_get_in_touch": "",
   "please_link_before_making_primary": "",
   "please_link_before_making_primary": "",
   "please_reconfirm_institutional_email": "",
   "please_reconfirm_institutional_email": "",

+ 19 - 6
services/web/frontend/js/features/subscription/components/dashboard/personal-subscription.tsx

@@ -1,5 +1,5 @@
-import { useTranslation } from 'react-i18next'
-import { Subscription } from '../../../../../../types/subscription/dashboard/subscription'
+import { Trans, useTranslation } from 'react-i18next'
+import { RecurlySubscription } from '../../../../../../types/subscription/dashboard/subscription'
 import { ActiveSubscription } from './states/active/active'
 import { ActiveSubscription } from './states/active/active'
 import { CanceledSubscription } from './states/canceled'
 import { CanceledSubscription } from './states/canceled'
 import { ExpiredSubscription } from './states/expired'
 import { ExpiredSubscription } from './states/expired'
@@ -8,7 +8,7 @@ import { useSubscriptionDashboardContext } from '../../context/subscription-dash
 function PastDueSubscriptionAlert({
 function PastDueSubscriptionAlert({
   subscription,
   subscription,
 }: {
 }: {
-  subscription: Subscription
+  subscription: RecurlySubscription
 }) {
 }) {
   const { t } = useTranslation()
   const { t } = useTranslation()
   return (
   return (
@@ -30,10 +30,10 @@ function PastDueSubscriptionAlert({
 function PersonalSubscriptionStates({
 function PersonalSubscriptionStates({
   subscription,
   subscription,
 }: {
 }: {
-  subscription: Subscription
+  subscription: RecurlySubscription
 }) {
 }) {
   const { t } = useTranslation()
   const { t } = useTranslation()
-  const state = subscription?.recurly?.state
+  const state = subscription?.recurly.state
 
 
   if (state === 'active') {
   if (state === 'active') {
     return <ActiveSubscription subscription={subscription} />
     return <ActiveSubscription subscription={subscription} />
@@ -53,13 +53,26 @@ function PersonalSubscription() {
 
 
   if (!personalSubscription) return null
   if (!personalSubscription) return null
 
 
+  if (!('recurly' in personalSubscription)) {
+    return (
+      <p>
+        <Trans
+          i18nKey="please_contact_support_to_makes_change_to_your_plan"
+          components={[<a href="/contact" />]} // eslint-disable-line react/jsx-key, jsx-a11y/anchor-has-content
+        />
+      </p>
+    )
+  }
+
   return (
   return (
     <>
     <>
       {personalSubscription.recurly.account.has_past_due_invoice._ ===
       {personalSubscription.recurly.account.has_past_due_invoice._ ===
         'true' && (
         'true' && (
         <PastDueSubscriptionAlert subscription={personalSubscription} />
         <PastDueSubscriptionAlert subscription={personalSubscription} />
       )}
       )}
-      <PersonalSubscriptionStates subscription={personalSubscription} />
+      <PersonalSubscriptionStates
+        subscription={personalSubscription as RecurlySubscription}
+      />
       {recurlyLoadError && (
       {recurlyLoadError && (
         <div className="alert alert-warning" role="alert">
         <div className="alert alert-warning" role="alert">
           <strong>{t('payment_provider_unreachable_error')}</strong>
           <strong>{t('payment_provider_unreachable_error')}</strong>

+ 2 - 2
services/web/frontend/js/features/subscription/components/dashboard/states/active/active.tsx

@@ -2,7 +2,7 @@ import { useTranslation, Trans } from 'react-i18next'
 import PremiumFeaturesLink from '../../premium-features-link'
 import PremiumFeaturesLink from '../../premium-features-link'
 import { PriceExceptions } from '../../../shared/price-exceptions'
 import { PriceExceptions } from '../../../shared/price-exceptions'
 import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
-import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription'
+import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
 import { CancelSubscriptionButton } from './cancel-subscription-button'
 import { CancelSubscriptionButton } from './cancel-subscription-button'
 import { CancelSubscription } from './cancel-subscription'
 import { CancelSubscription } from './cancel-subscription'
 import { PendingPlanChange } from './pending-plan-change'
 import { PendingPlanChange } from './pending-plan-change'
@@ -14,7 +14,7 @@ import { ContactSupportToChangeGroupPlan } from './contact-support-to-change-gro
 export function ActiveSubscription({
 export function ActiveSubscription({
   subscription,
   subscription,
 }: {
 }: {
-  subscription: Subscription
+  subscription: RecurlySubscription
 }) {
 }) {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const { recurlyLoadError, setShowChangePersonalPlan, showCancellation } =
   const { recurlyLoadError, setShowChangePersonalPlan, showCancellation } =

+ 2 - 2
services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx

@@ -1,12 +1,12 @@
 import { useTranslation, Trans } from 'react-i18next'
 import { useTranslation, Trans } from 'react-i18next'
 import * as eventTracking from '../../../../../../infrastructure/event-tracking'
 import * as eventTracking from '../../../../../../infrastructure/event-tracking'
-import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription'
+import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
 import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
 import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
 
 
 export function CancelSubscriptionButton({
 export function CancelSubscriptionButton({
   subscription,
   subscription,
 }: {
 }: {
-  subscription: Subscription
+  subscription: RecurlySubscription
 }) {
 }) {
   const { t } = useTranslation()
   const { t } = useTranslation()
   const { recurlyLoadError, setShowCancellation } =
   const { recurlyLoadError, setShowCancellation } =

+ 2 - 2
services/web/frontend/js/features/subscription/components/dashboard/states/active/pending-plan-change.tsx

@@ -1,10 +1,10 @@
 import { Trans } from 'react-i18next'
 import { Trans } from 'react-i18next'
-import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription'
+import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
 
 
 export function PendingPlanChange({
 export function PendingPlanChange({
   subscription,
   subscription,
 }: {
 }: {
-  subscription: Subscription
+  subscription: RecurlySubscription
 }) {
 }) {
   if (!subscription.pendingPlan) return null
   if (!subscription.pendingPlan) return null
 
 

+ 2 - 2
services/web/frontend/js/features/subscription/components/dashboard/states/canceled.tsx

@@ -1,11 +1,11 @@
 import { useTranslation, Trans } from 'react-i18next'
 import { useTranslation, Trans } from 'react-i18next'
-import { Subscription } from '../../../../../../../types/subscription/dashboard/subscription'
+import { RecurlySubscription } from '../../../../../../../types/subscription/dashboard/subscription'
 import PremiumFeaturesLink from '../premium-features-link'
 import PremiumFeaturesLink from '../premium-features-link'
 
 
 export function CanceledSubscription({
 export function CanceledSubscription({
   subscription,
   subscription,
 }: {
 }: {
-  subscription: Subscription
+  subscription: RecurlySubscription
 }) {
 }) {
   const { t } = useTranslation()
   const { t } = useTranslation()
 
 

+ 3 - 2
services/web/frontend/js/features/subscription/components/dashboard/states/expired.tsx

@@ -1,12 +1,13 @@
 import { useTranslation } from 'react-i18next'
 import { useTranslation } from 'react-i18next'
-import { Subscription } from '../../../../../../../types/subscription/dashboard/subscription'
+import { RecurlySubscription } from '../../../../../../../types/subscription/dashboard/subscription'
 
 
 export function ExpiredSubscription({
 export function ExpiredSubscription({
   subscription,
   subscription,
 }: {
 }: {
-  subscription: Subscription
+  subscription: RecurlySubscription
 }) {
 }) {
   const { t } = useTranslation()
   const { t } = useTranslation()
+
   return (
   return (
     <>
     <>
       <p>{t('your_subscription_has_expired')}</p>
       <p>{t('your_subscription_has_expired')}</p>

+ 10 - 5
services/web/frontend/js/features/subscription/context/subscription-dashboard-context.tsx

@@ -8,9 +8,10 @@ import {
   useState,
   useState,
 } from 'react'
 } from 'react'
 import {
 import {
+  CustomSubscription,
   ManagedGroupSubscription,
   ManagedGroupSubscription,
   MemberGroupSubscription,
   MemberGroupSubscription,
-  Subscription,
+  RecurlySubscription,
 } from '../../../../../types/subscription/dashboard/subscription'
 } from '../../../../../types/subscription/dashboard/subscription'
 import {
 import {
   Plan,
   Plan,
@@ -46,7 +47,7 @@ type SubscriptionDashboardContextValue = {
   managedPublishers: ManagedPublisher[]
   managedPublishers: ManagedPublisher[]
   updateManagedInstitution: (institution: ManagedInstitution) => void
   updateManagedInstitution: (institution: ManagedInstitution) => void
   modalIdShown?: SubscriptionDashModalIds
   modalIdShown?: SubscriptionDashModalIds
-  personalSubscription?: Subscription
+  personalSubscription?: RecurlySubscription | CustomSubscription
   hasSubscription: boolean
   hasSubscription: boolean
   plans: Plan[]
   plans: Plan[]
   planCodeToChangeTo?: string
   planCodeToChangeTo?: string
@@ -111,7 +112,7 @@ export function SubscriptionDashboardProvider({
   const institutionMemberships: Institution[] = getMeta(
   const institutionMemberships: Institution[] = getMeta(
     'ol-currentInstitutionsWithLicence'
     'ol-currentInstitutionsWithLicence'
   )
   )
-  const personalSubscription: Subscription = getMeta('ol-subscription')
+  const personalSubscription = getMeta('ol-subscription')
   const managedGroupSubscriptions: ManagedGroupSubscription[] = getMeta(
   const managedGroupSubscriptions: ManagedGroupSubscription[] = getMeta(
     'ol-managedGroupSubscriptions'
     'ol-managedGroupSubscriptions'
   )
   )
@@ -143,7 +144,11 @@ export function SubscriptionDashboardProvider({
   }, [recurlyApiKey, setRecurlyLoadError])
   }, [recurlyApiKey, setRecurlyLoadError])
 
 
   useEffect(() => {
   useEffect(() => {
-    if (isRecurlyLoaded() && plansWithoutDisplayPrice && personalSubscription) {
+    if (
+      isRecurlyLoaded() &&
+      plansWithoutDisplayPrice &&
+      personalSubscription?.recurly
+    ) {
       const { currency, taxRate } = personalSubscription.recurly
       const { currency, taxRate } = personalSubscription.recurly
       const fetchPlansDisplayPrices = async () => {
       const fetchPlansDisplayPrices = async () => {
         for (const plan of plansWithoutDisplayPrice) {
         for (const plan of plansWithoutDisplayPrice) {
@@ -173,7 +178,7 @@ export function SubscriptionDashboardProvider({
       groupPlanToChangeToCode &&
       groupPlanToChangeToCode &&
       groupPlanToChangeToSize &&
       groupPlanToChangeToSize &&
       groupPlanToChangeToUsage &&
       groupPlanToChangeToUsage &&
-      personalSubscription
+      personalSubscription?.recurly
     ) {
     ) {
       setQueryingGroupPlanToChangeToPrice(true)
       setQueryingGroupPlanToChangeToPrice(true)
 
 

+ 1 - 0
services/web/locales/en.json

@@ -1090,6 +1090,7 @@
   "please_compile_pdf_before_word_count": "Please compile your project before performing a word count",
   "please_compile_pdf_before_word_count": "Please compile your project before performing a word count",
   "please_confirm_email": "Please confirm your email __emailAddress__ by clicking on the link in the confirmation email ",
   "please_confirm_email": "Please confirm your email __emailAddress__ by clicking on the link in the confirmation email ",
   "please_confirm_your_email_before_making_it_default": "Please confirm your email before making it the primary.",
   "please_confirm_your_email_before_making_it_default": "Please confirm your email before making it the primary.",
+  "please_contact_support_to_makes_change_to_your_plan": "Please <0>contact support</0> to make changes to your plan",
   "please_enter_email": "Please enter your email address",
   "please_enter_email": "Please enter your email address",
   "please_get_in_touch": "Please get in touch",
   "please_get_in_touch": "Please get in touch",
   "please_link_before_making_primary": "Please confirm your email by linking to your institutional account before making it the primary email.",
   "please_link_before_making_primary": "Please confirm your email by linking to your institutional account before making it the primary email.",

+ 14 - 1
services/web/test/frontend/features/subscription/components/dashboard/personal-subscription.test.tsx

@@ -4,6 +4,7 @@ import PersonalSubscription from '../../../../../../frontend/js/features/subscri
 import {
 import {
   annualActiveSubscription,
   annualActiveSubscription,
   canceledSubscription,
   canceledSubscription,
+  customSubscription,
   pastDueExpiredSubscription,
   pastDueExpiredSubscription,
 } from '../../fixtures/subscriptions'
 } from '../../fixtures/subscriptions'
 import {
 import {
@@ -25,6 +26,18 @@ describe('<PersonalSubscription />', function () {
     })
     })
   })
   })
 
 
+  describe('custom subscription', function () {
+    it('displays contact support message', function () {
+      renderWithSubscriptionDashContext(<PersonalSubscription />, {
+        metaTags: [{ name: 'ol-subscription', value: customSubscription }],
+      })
+
+      screen.getByText('Please', { exact: false })
+      screen.getByText('contact support', { exact: false })
+      screen.getByText('to make changes to your plan', { exact: false })
+    })
+  })
+
   describe('subscription states  ', function () {
   describe('subscription states  ', function () {
     it('renders the active dash', function () {
     it('renders the active dash', function () {
       renderWithSubscriptionDashContext(<PersonalSubscription />, {
       renderWithSubscriptionDashContext(<PersonalSubscription />, {
@@ -44,7 +57,7 @@ describe('<PersonalSubscription />', function () {
         'Your subscription has been canceled and will terminate on',
         'Your subscription has been canceled and will terminate on',
         { exact: false }
         { exact: false }
       )
       )
-      screen.getByText(canceledSubscription.recurly.nextPaymentDueAt, {
+      screen.getByText(canceledSubscription.recurly!.nextPaymentDueAt, {
         exact: false,
         exact: false,
       })
       })
 
 

+ 2 - 2
services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx

@@ -1,7 +1,7 @@
 import { expect } from 'chai'
 import { expect } from 'chai'
 import { fireEvent, screen } from '@testing-library/react'
 import { fireEvent, screen } from '@testing-library/react'
 import * as eventTracking from '../../../../../../../../frontend/js/infrastructure/event-tracking'
 import * as eventTracking from '../../../../../../../../frontend/js/infrastructure/event-tracking'
-import { Subscription } from '../../../../../../../../types/subscription/dashboard/subscription'
+import { RecurlySubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
 import {
 import {
   annualActiveSubscription,
   annualActiveSubscription,
   groupActiveSubscription,
   groupActiveSubscription,
@@ -26,7 +26,7 @@ describe('<ActiveSubscription />', function () {
     sendMBSpy.restore()
     sendMBSpy.restore()
   })
   })
 
 
-  function expectedInActiveSubscription(subscription: Subscription) {
+  function expectedInActiveSubscription(subscription: RecurlySubscription) {
     // sentence broken up by bolding
     // sentence broken up by bolding
     screen.getByText('You are currently subscribed to the', { exact: false })
     screen.getByText('You are currently subscribed to the', { exact: false })
     screen.getByText(subscription.plan.name, { exact: false })
     screen.getByText(subscription.plan.name, { exact: false })

+ 30 - 8
services/web/test/frontend/features/subscription/fixtures/subscriptions.tsx

@@ -1,6 +1,7 @@
 import {
 import {
+  CustomSubscription,
   GroupSubscription,
   GroupSubscription,
-  Subscription,
+  RecurlySubscription,
 } from '../../../../../types/subscription/dashboard/subscription'
 } from '../../../../../types/subscription/dashboard/subscription'
 const dateformat = require('dateformat')
 const dateformat = require('dateformat')
 const today = new Date()
 const today = new Date()
@@ -12,7 +13,7 @@ const sevenDaysFromTodayFormatted = dateformat(
   'dS mmmm yyyy'
   'dS mmmm yyyy'
 )
 )
 
 
-export const annualActiveSubscription: Subscription = {
+export const annualActiveSubscription: RecurlySubscription = {
   manager_ids: ['abc123'],
   manager_ids: ['abc123'],
   member_ids: [],
   member_ids: [],
   invited_emails: [],
   invited_emails: [],
@@ -51,7 +52,7 @@ export const annualActiveSubscription: Subscription = {
   },
   },
 }
 }
 
 
-export const annualActiveSubscriptionEuro: Subscription = {
+export const annualActiveSubscriptionEuro: RecurlySubscription = {
   manager_ids: ['abc123'],
   manager_ids: ['abc123'],
   member_ids: [],
   member_ids: [],
   invited_emails: [],
   invited_emails: [],
@@ -90,7 +91,7 @@ export const annualActiveSubscriptionEuro: Subscription = {
   },
   },
 }
 }
 
 
-export const annualActiveSubscriptionPro: Subscription = {
+export const annualActiveSubscriptionPro: RecurlySubscription = {
   manager_ids: ['abc123'],
   manager_ids: ['abc123'],
   member_ids: [],
   member_ids: [],
   invited_emails: [],
   invited_emails: [],
@@ -128,7 +129,7 @@ export const annualActiveSubscriptionPro: Subscription = {
   },
   },
 }
 }
 
 
-export const pastDueExpiredSubscription: Subscription = {
+export const pastDueExpiredSubscription: RecurlySubscription = {
   manager_ids: ['abc123'],
   manager_ids: ['abc123'],
   member_ids: [],
   member_ids: [],
   invited_emails: [],
   invited_emails: [],
@@ -167,7 +168,7 @@ export const pastDueExpiredSubscription: Subscription = {
   },
   },
 }
 }
 
 
-export const canceledSubscription: Subscription = {
+export const canceledSubscription: RecurlySubscription = {
   manager_ids: ['abc123'],
   manager_ids: ['abc123'],
   member_ids: [],
   member_ids: [],
   invited_emails: [],
   invited_emails: [],
@@ -206,7 +207,7 @@ export const canceledSubscription: Subscription = {
   },
   },
 }
 }
 
 
-export const pendingSubscriptionChange: Subscription = {
+export const pendingSubscriptionChange: RecurlySubscription = {
   manager_ids: ['abc123'],
   manager_ids: ['abc123'],
   member_ids: [],
   member_ids: [],
   invited_emails: [],
   invited_emails: [],
@@ -362,7 +363,7 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
     },
     },
   }
   }
 
 
-export const trialSubscription: Subscription = {
+export const trialSubscription: RecurlySubscription = {
   manager_ids: ['abc123'],
   manager_ids: ['abc123'],
   member_ids: [],
   member_ids: [],
   invited_emails: [],
   invited_emails: [],
@@ -410,3 +411,24 @@ export const trialSubscription: Subscription = {
     displayPrice: '$14.00',
     displayPrice: '$14.00',
   },
   },
 }
 }
+
+export const customSubscription: CustomSubscription = {
+  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,
+    featureDescription: [],
+  },
+  customAccount: true,
+}

+ 2 - 2
services/web/test/frontend/features/subscription/helpers/render-active-subscription.tsx

@@ -1,10 +1,10 @@
 import { ActiveSubscription } from '../../../../../frontend/js/features/subscription/components/dashboard/states/active/active'
 import { ActiveSubscription } from '../../../../../frontend/js/features/subscription/components/dashboard/states/active/active'
-import { Subscription } from '../../../../../types/subscription/dashboard/subscription'
+import { RecurlySubscription } from '../../../../../types/subscription/dashboard/subscription'
 import { groupPlans, plans } from '../fixtures/plans'
 import { groupPlans, plans } from '../fixtures/plans'
 import { renderWithSubscriptionDashContext } from './render-with-subscription-dash-context'
 import { renderWithSubscriptionDashContext } from './render-with-subscription-dash-context'
 
 
 export function renderActiveSubscription(
 export function renderActiveSubscription(
-  subscription: Subscription,
+  subscription: RecurlySubscription,
   tags: { name: string; value: string | object | Array<object> }[] = [],
   tags: { name: string; value: string | object | Array<object> }[] = [],
   currencyCode?: string
   currencyCode?: string
 ) {
 ) {

+ 44 - 35
services/web/types/subscription/dashboard/subscription.ts

@@ -5,6 +5,40 @@ import { User } from '../../../types/user'
 
 
 type SubscriptionState = 'active' | 'canceled' | 'expired'
 type SubscriptionState = 'active' | 'canceled' | 'expired'
 
 
+type Recurly = {
+  tax: number
+  taxRate: number
+  billingDetailsLink: string
+  accountManagementLink: string
+  additionalLicenses: number
+  totalLicenses: number
+  nextPaymentDueAt: string
+  currency: CurrencyCode
+  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
+}
+
 export type Subscription = {
 export type Subscription = {
   _id: string
   _id: string
   admin_id: string
   admin_id: string
@@ -17,44 +51,19 @@ export type Subscription = {
   planCode: string
   planCode: string
   recurlySubscription_id: string
   recurlySubscription_id: string
   plan: Plan
   plan: Plan
-  recurly: {
-    tax: number
-    taxRate: number
-    billingDetailsLink: string
-    accountManagementLink: string
-    additionalLicenses: number
-    totalLicenses: number
-    nextPaymentDueAt: string
-    currency: CurrencyCode
-    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
   pendingPlan?: Plan
 }
 }
 
 
-export type GroupSubscription = Subscription & {
-  teamName?: string
+export type RecurlySubscription = Subscription & {
+  recurly: Recurly
+}
+
+export type CustomSubscription = Subscription & {
+  customAccount: boolean
+}
+
+export type GroupSubscription = RecurlySubscription & {
+  teamName: string
   teamNotice?: string
   teamNotice?: string
 }
 }