mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Update unit tests for ActiveSubscription
GitOrigin-RevId: 181f5a097fff2fa31ed11d39b76f40c9a4b4ca31
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { PaidSubscription } from '../../../../../../types/subscription/dashboard/subscription'
|
||||
import { PausedSubscription } from './states/active/paused'
|
||||
import { ActiveSubscriptionNew } from '@/features/subscription/components/dashboard/states/active/active-new'
|
||||
import { ActiveSubscription } from '@/features/subscription/components/dashboard/states/active/active'
|
||||
import { CanceledSubscription } from './states/canceled'
|
||||
import { ExpiredSubscription } from './states/expired'
|
||||
import { useSubscriptionDashboardContext } from '../../context/subscription-dashboard-context'
|
||||
@@ -43,7 +43,7 @@ function PersonalSubscriptionStates({
|
||||
|
||||
if (state === 'active') {
|
||||
// This version handles subscriptions with and without addons
|
||||
return <ActiveSubscriptionNew subscription={subscription} />
|
||||
return <ActiveSubscription subscription={subscription} />
|
||||
} else if (state === 'canceled') {
|
||||
return <CanceledSubscription subscription={subscription} />
|
||||
} else if (state === 'expired') {
|
||||
|
||||
@@ -1,391 +0,0 @@
|
||||
import { useTranslation, Trans } from 'react-i18next'
|
||||
import { PriceExceptions } from '../../../shared/price-exceptions'
|
||||
import { useSubscriptionDashboardContext } from '../../../../context/subscription-dashboard-context'
|
||||
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
|
||||
import { CancelSubscriptionButton } from './cancel-subscription-button'
|
||||
import { CancelSubscription } from './cancel-plan/cancel-subscription'
|
||||
import { TrialEnding } from './trial-ending'
|
||||
import { ChangePlanModal } from './change-plan/modals/change-plan-modal'
|
||||
import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan-modal'
|
||||
import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal'
|
||||
import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal'
|
||||
import { CancelAiAddOnModal } from '@/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import isInFreeTrial from '../../../../util/is-in-free-trial'
|
||||
import AddOns from '@/features/subscription/components/dashboard/states/active/add-ons'
|
||||
import {
|
||||
AI_ADD_ON_CODE,
|
||||
AI_ASSIST_STANDALONE_MONTHLY_PLAN_CODE,
|
||||
isStandaloneAiPlanCode,
|
||||
} from '@/features/subscription/data/add-on-codes'
|
||||
import getMeta from '@/utils/meta'
|
||||
import SubscriptionRemainder from '@/features/subscription/components/dashboard/states/active/subscription-remainder'
|
||||
import { sendMB } from '../../../../../../infrastructure/event-tracking'
|
||||
import PauseSubscriptionModal from '@/features/subscription/components/dashboard/pause-modal'
|
||||
import LoadingSpinner from '@/shared/components/loading-spinner'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { useLocation } from '@/shared/hooks/use-location'
|
||||
import { FlashMessage } from '@/features/subscription/components/dashboard/states/active/flash-message'
|
||||
import Notification from '@/shared/components/notification'
|
||||
|
||||
export function ActiveSubscriptionNew({
|
||||
subscription,
|
||||
}: {
|
||||
subscription: PaidSubscription
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
recurlyLoadError,
|
||||
setModalIdShown,
|
||||
showCancellation,
|
||||
institutionMemberships,
|
||||
memberGroupSubscriptions,
|
||||
getFormattedRenewalDate,
|
||||
} = useSubscriptionDashboardContext()
|
||||
const cancelPauseReq = useAsync()
|
||||
const { isError: isErrorPause } = cancelPauseReq
|
||||
|
||||
if (showCancellation) return <CancelSubscription />
|
||||
|
||||
const onStandalonePlan = isStandaloneAiPlanCode(subscription.planCode)
|
||||
|
||||
let planName
|
||||
if (onStandalonePlan) {
|
||||
planName = 'Overleaf Free'
|
||||
if (institutionMemberships && institutionMemberships.length > 0) {
|
||||
planName = 'Overleaf Professional'
|
||||
}
|
||||
if (memberGroupSubscriptions.length > 0) {
|
||||
if (
|
||||
memberGroupSubscriptions.some(s => s.planLevelName === 'Professional')
|
||||
) {
|
||||
planName = 'Overleaf Professional'
|
||||
} else {
|
||||
planName = 'Overleaf Standard'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
planName = subscription.plan.name
|
||||
}
|
||||
|
||||
const handlePlanChange = () => setModalIdShown('change-plan')
|
||||
const handleCancelClick = (addOnCode: string) => {
|
||||
if (
|
||||
[AI_ASSIST_STANDALONE_MONTHLY_PLAN_CODE, AI_ADD_ON_CODE].includes(
|
||||
addOnCode
|
||||
)
|
||||
) {
|
||||
setModalIdShown('cancel-ai-add-on')
|
||||
}
|
||||
}
|
||||
const hasPendingPause = Boolean(
|
||||
subscription.payment.state === 'active' &&
|
||||
subscription.payment.remainingPauseCycles &&
|
||||
subscription.payment.remainingPauseCycles > 0
|
||||
)
|
||||
|
||||
const isLegacyPlan =
|
||||
subscription.payment.totalLicenses !==
|
||||
subscription.payment.additionalLicenses
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="notification-list">
|
||||
<FlashMessage />
|
||||
|
||||
{isErrorPause && (
|
||||
<Notification
|
||||
type="error"
|
||||
content={t('generic_something_went_wrong')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<h2 className="h3 fw-bold">{t('billing')}</h2>
|
||||
<p className="mb-1">
|
||||
{subscription.plan.annual ? (
|
||||
<Trans
|
||||
i18nKey="billed_annually_at"
|
||||
values={{ price: subscription.payment.displayPrice }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<i />,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="billed_monthly_at"
|
||||
values={{ price: subscription.payment.displayPrice }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<i />,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
<p className="mb-1">
|
||||
<Trans
|
||||
i18nKey="renews_on"
|
||||
values={{ date: subscription.payment.nextPaymentDueDate }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
/>
|
||||
</p>
|
||||
<div>
|
||||
{subscription.payment.billingDetailsLink ? (
|
||||
<>
|
||||
<a
|
||||
href={subscription.payment.accountManagementLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="me-2"
|
||||
>
|
||||
{t('view_invoices')}
|
||||
</a>
|
||||
<a
|
||||
href={subscription.payment.billingDetailsLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{t('view_billing_details')}
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<a
|
||||
href={subscription.payment.accountManagementLink}
|
||||
rel="noreferrer noopener"
|
||||
className="me-2"
|
||||
>
|
||||
{t('view_payment_portal')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<PriceExceptions subscription={subscription} />
|
||||
{!recurlyLoadError && (
|
||||
<p>
|
||||
<i>
|
||||
<SubscriptionRemainder subscription={subscription} hideTime />
|
||||
</i>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<hr />
|
||||
<h2 className="h3 fw-bold">{t('plan')}</h2>
|
||||
<h3 className="h5 mt-0 mb-1 fw-bold">{planName}</h3>
|
||||
{subscription.pendingPlan &&
|
||||
subscription.pendingPlan.name !== subscription.plan.name && (
|
||||
<p className="mb-1">{t('want_change_to_apply_before_plan_end')}</p>
|
||||
)}
|
||||
{isInFreeTrial(subscription.payment.trialEndsAt) &&
|
||||
subscription.payment.trialEndsAtFormatted && (
|
||||
<TrialEnding
|
||||
trialEndsAtFormatted={subscription.payment.trialEndsAtFormatted}
|
||||
className="mb-1"
|
||||
/>
|
||||
)}
|
||||
{subscription.payment.totalLicenses > 0 && (
|
||||
<p className="mb-1">
|
||||
{isLegacyPlan && subscription.payment.additionalLicenses > 0 ? (
|
||||
<Trans
|
||||
i18nKey="plus_x_additional_licenses_for_a_total_of_y_licenses"
|
||||
values={{
|
||||
count: subscription.payment.totalLicenses,
|
||||
additionalLicenses: subscription.payment.additionalLicenses,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="supports_up_to_x_licenses"
|
||||
values={{ count: subscription.payment.totalLicenses }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{hasPendingPause && (
|
||||
<>
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="your_subscription_will_pause_on"
|
||||
values={{
|
||||
planName: subscription.plan.name,
|
||||
pauseDate: subscription.payment.nextPaymentDueAt,
|
||||
reactivationDate: getFormattedRenewalDate(),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
<p>{t('you_can_still_use_your_premium_features')}</p>
|
||||
</>
|
||||
)}
|
||||
{!onStandalonePlan && (
|
||||
<p className="mb-1">
|
||||
{subscription.plan.annual
|
||||
? t('x_price_per_year', {
|
||||
price: subscription.payment.planOnlyDisplayPrice,
|
||||
})
|
||||
: t('x_price_per_month', {
|
||||
price: subscription.payment.planOnlyDisplayPrice,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{!recurlyLoadError && (
|
||||
<PlanActions
|
||||
subscription={subscription}
|
||||
onStandalonePlan={onStandalonePlan}
|
||||
handlePlanChange={handlePlanChange}
|
||||
hasPendingPause={hasPendingPause}
|
||||
cancelPauseReq={cancelPauseReq}
|
||||
/>
|
||||
)}
|
||||
<hr />
|
||||
<AddOns
|
||||
subscription={subscription}
|
||||
onStandalonePlan={onStandalonePlan}
|
||||
handleCancelClick={handleCancelClick}
|
||||
/>
|
||||
|
||||
<ChangePlanModal />
|
||||
<ConfirmChangePlanModal />
|
||||
<KeepCurrentPlanModal />
|
||||
<ChangeToGroupModal />
|
||||
<CancelAiAddOnModal />
|
||||
<PauseSubscriptionModal />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type PlanActionsProps = {
|
||||
subscription: PaidSubscription
|
||||
onStandalonePlan: boolean
|
||||
handlePlanChange: () => void
|
||||
hasPendingPause: boolean
|
||||
cancelPauseReq: ReturnType<typeof useAsync>
|
||||
}
|
||||
|
||||
function PlanActions({
|
||||
subscription,
|
||||
onStandalonePlan,
|
||||
handlePlanChange,
|
||||
hasPendingPause,
|
||||
cancelPauseReq,
|
||||
}: PlanActionsProps) {
|
||||
const { t } = useTranslation()
|
||||
const isSubscriptionEligibleForFlexibleGroupLicensing = getMeta(
|
||||
'ol-canUseFlexibleLicensing'
|
||||
)
|
||||
const location = useLocation()
|
||||
const { runAsync: runAsyncCancelPause, isLoading: isLoadingCancelPause } =
|
||||
cancelPauseReq
|
||||
|
||||
const handleCancelPendingPauseClick = async () => {
|
||||
try {
|
||||
await runAsyncCancelPause(postJSON('/user/subscription/pause/0'))
|
||||
const newUrl = new URL(location.toString())
|
||||
newUrl.searchParams.set('flash', 'unpaused')
|
||||
window.history.replaceState(null, '', newUrl)
|
||||
location.reload()
|
||||
} catch (e) {
|
||||
debugConsole.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
{isSubscriptionEligibleForFlexibleGroupLicensing ? (
|
||||
<FlexibleGroupLicensingActions subscription={subscription} />
|
||||
) : (
|
||||
<>
|
||||
{!hasPendingPause && !subscription.payment.hasPastDueInvoice && (
|
||||
<OLButton variant="secondary" onClick={handlePlanChange}>
|
||||
{t('change_plan')}
|
||||
</OLButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{hasPendingPause && (
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={handleCancelPendingPauseClick}
|
||||
disabled={isLoadingCancelPause}
|
||||
>
|
||||
{isLoadingCancelPause ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
t('unpause_subscription')
|
||||
)}
|
||||
</OLButton>
|
||||
)}
|
||||
{!onStandalonePlan && (
|
||||
<>
|
||||
{' '}
|
||||
<CancelSubscriptionButton />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FlexibleGroupLicensingActions({
|
||||
subscription,
|
||||
}: {
|
||||
subscription: PaidSubscription
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (subscription.pendingPlan || subscription.payment.hasPastDueInvoice) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isProfessionalPlan = subscription.planCode
|
||||
.toLowerCase()
|
||||
.includes('professional')
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isProfessionalPlan && (
|
||||
<>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
href="/user/subscription/group/upgrade-subscription"
|
||||
onClick={() =>
|
||||
sendMB('flex-upgrade', { location: 'upgrade-plan-button' })
|
||||
}
|
||||
>
|
||||
{t('upgrade_plan')}
|
||||
</OLButton>{' '}
|
||||
</>
|
||||
)}
|
||||
{subscription.plan.membersLimitAddOn === 'additional-license' && (
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
href="/user/subscription/group/add-users"
|
||||
onClick={() => sendMB('flex-add-users')}
|
||||
>
|
||||
{t('buy_more_licenses')}
|
||||
</OLButton>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -4,25 +4,32 @@ import { useSubscriptionDashboardContext } from '../../../../context/subscriptio
|
||||
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
|
||||
import { CancelSubscriptionButton } from './cancel-subscription-button'
|
||||
import { CancelSubscription } from './cancel-plan/cancel-subscription'
|
||||
import { PendingPlanChange } from './pending-plan-change'
|
||||
import { TrialEnding } from './trial-ending'
|
||||
import { PendingAdditionalLicenses } from './pending-additional-licenses'
|
||||
import { ContactSupportToChangeGroupPlan } from './contact-support-to-change-group-plan'
|
||||
import SubscriptionRemainder from './subscription-remainder'
|
||||
import isInFreeTrial from '../../../../util/is-in-free-trial'
|
||||
import { ChangePlanModal } from './change-plan/modals/change-plan-modal'
|
||||
import { ConfirmChangePlanModal } from './change-plan/modals/confirm-change-plan-modal'
|
||||
import { KeepCurrentPlanModal } from './change-plan/modals/keep-current-plan-modal'
|
||||
import { ChangeToGroupModal } from './change-plan/modals/change-to-group-modal'
|
||||
import { CancelAiAddOnModal } from '@/features/subscription/components/dashboard/states/active/change-plan/modals/cancel-ai-add-on-modal'
|
||||
import OLButton from '@/shared/components/ol/ol-button'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import PauseSubscriptionModal from '../../pause-modal'
|
||||
import Notification from '@/shared/components/notification'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import { FlashMessage } from './flash-message'
|
||||
import { useLocation } from '@/shared/hooks/use-location'
|
||||
import isInFreeTrial from '../../../../util/is-in-free-trial'
|
||||
import AddOns from '@/features/subscription/components/dashboard/states/active/add-ons'
|
||||
import {
|
||||
AI_ADD_ON_CODE,
|
||||
AI_ASSIST_STANDALONE_MONTHLY_PLAN_CODE,
|
||||
isStandaloneAiPlanCode,
|
||||
} from '@/features/subscription/data/add-on-codes'
|
||||
import getMeta from '@/utils/meta'
|
||||
import SubscriptionRemainder from '@/features/subscription/components/dashboard/states/active/subscription-remainder'
|
||||
import { sendMB } from '../../../../../../infrastructure/event-tracking'
|
||||
import PauseSubscriptionModal from '@/features/subscription/components/dashboard/pause-modal'
|
||||
import LoadingSpinner from '@/shared/components/loading-spinner'
|
||||
import { postJSON } from '@/infrastructure/fetch-json'
|
||||
import { debugConsole } from '@/utils/debugging'
|
||||
import useAsync from '@/shared/hooks/use-async'
|
||||
import { useLocation } from '@/shared/hooks/use-location'
|
||||
import { FlashMessage } from '@/features/subscription/components/dashboard/states/active/flash-message'
|
||||
import Notification from '@/shared/components/notification'
|
||||
import { PendingPlanChange } from './pending-plan-change'
|
||||
|
||||
export function ActiveSubscription({
|
||||
subscription,
|
||||
@@ -34,33 +41,55 @@ export function ActiveSubscription({
|
||||
recurlyLoadError,
|
||||
setModalIdShown,
|
||||
showCancellation,
|
||||
institutionMemberships,
|
||||
memberGroupSubscriptions,
|
||||
getFormattedRenewalDate,
|
||||
} = useSubscriptionDashboardContext()
|
||||
const {
|
||||
isError: isErrorPause,
|
||||
runAsync: runAsyncCancelPause,
|
||||
isLoading: isLoadingCancelPause,
|
||||
} = useAsync()
|
||||
const location = useLocation()
|
||||
const cancelPauseReq = useAsync()
|
||||
const { isError: isErrorPause } = cancelPauseReq
|
||||
|
||||
if (showCancellation) return <CancelSubscription />
|
||||
|
||||
const hasPendingPause =
|
||||
subscription.payment.state === 'active' &&
|
||||
subscription.payment.remainingPauseCycles &&
|
||||
subscription.payment.remainingPauseCycles > 0
|
||||
const onStandalonePlan = isStandaloneAiPlanCode(subscription.planCode)
|
||||
|
||||
const handleCancelPendingPauseClick = async () => {
|
||||
try {
|
||||
await runAsyncCancelPause(postJSON('/user/subscription/pause/0'))
|
||||
const newUrl = new URL(location.toString())
|
||||
newUrl.searchParams.set('flash', 'unpaused')
|
||||
window.history.replaceState(null, '', newUrl)
|
||||
location.reload()
|
||||
} catch (e) {
|
||||
debugConsole.error(e)
|
||||
let planName
|
||||
if (onStandalonePlan) {
|
||||
planName = 'Overleaf Free'
|
||||
if (institutionMemberships && institutionMemberships.length > 0) {
|
||||
planName = 'Overleaf Professional'
|
||||
}
|
||||
if (memberGroupSubscriptions.length > 0) {
|
||||
if (
|
||||
memberGroupSubscriptions.some(s => s.planLevelName === 'Professional')
|
||||
) {
|
||||
planName = 'Overleaf Professional'
|
||||
} else {
|
||||
planName = 'Overleaf Standard'
|
||||
}
|
||||
}
|
||||
} else {
|
||||
planName = subscription.plan.name
|
||||
}
|
||||
|
||||
const handlePlanChange = () => setModalIdShown('change-plan')
|
||||
const handleCancelClick = (addOnCode: string) => {
|
||||
if (
|
||||
[AI_ASSIST_STANDALONE_MONTHLY_PLAN_CODE, AI_ADD_ON_CODE].includes(
|
||||
addOnCode
|
||||
)
|
||||
) {
|
||||
setModalIdShown('cancel-ai-add-on')
|
||||
}
|
||||
}
|
||||
const hasPendingPause = Boolean(
|
||||
subscription.payment.state === 'active' &&
|
||||
subscription.payment.remainingPauseCycles &&
|
||||
subscription.payment.remainingPauseCycles > 0
|
||||
)
|
||||
|
||||
const isLegacyPlan =
|
||||
subscription.payment.totalLicenses !==
|
||||
subscription.payment.additionalLicenses
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -74,66 +103,130 @@ export function ActiveSubscription({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<p>
|
||||
{!hasPendingPause && (
|
||||
<h2 className="h3 fw-bold">{t('billing')}</h2>
|
||||
<p className="mb-1" data-testid="billing-period">
|
||||
{subscription.plan.annual ? (
|
||||
<Trans
|
||||
i18nKey="currently_subscribed_to_plan"
|
||||
values={{
|
||||
planName: subscription.plan.name,
|
||||
}}
|
||||
i18nKey="billed_annually_at"
|
||||
values={{ price: subscription.payment.displayPrice }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<i />,
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="billed_monthly_at"
|
||||
values={{ price: subscription.payment.displayPrice }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<i />,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
{subscription.pendingPlan && (
|
||||
<>
|
||||
{' '}
|
||||
<PendingPlanChange subscription={subscription} />
|
||||
</>
|
||||
)}
|
||||
{!subscription.pendingPlan &&
|
||||
subscription.payment.additionalLicenses > 0 && (
|
||||
<>
|
||||
{' '}
|
||||
<PendingAdditionalLicenses
|
||||
additionalLicenses={subscription.payment.additionalLicenses}
|
||||
totalLicenses={subscription.payment.totalLicenses}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!recurlyLoadError &&
|
||||
!subscription.groupPlan &&
|
||||
!hasPendingPause &&
|
||||
!subscription.payment.hasPastDueInvoice && (
|
||||
<>
|
||||
{' '}
|
||||
<OLButton
|
||||
variant="link"
|
||||
className="btn-inline-link"
|
||||
onClick={() => setModalIdShown('change-plan')}
|
||||
>
|
||||
{t('change_plan')}
|
||||
</OLButton>
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
<p className="mb-1" data-testid="renews-on">
|
||||
<Trans
|
||||
i18nKey="renews_on"
|
||||
values={{ date: subscription.payment.nextPaymentDueDate }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
/>
|
||||
</p>
|
||||
<div>
|
||||
{subscription.payment.billingDetailsLink ? (
|
||||
<>
|
||||
<a
|
||||
href={subscription.payment.accountManagementLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="me-2"
|
||||
>
|
||||
{t('view_invoices')}
|
||||
</a>
|
||||
<a
|
||||
href={subscription.payment.billingDetailsLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{t('view_billing_details')}
|
||||
</a>
|
||||
</>
|
||||
) : (
|
||||
<a
|
||||
href={subscription.payment.accountManagementLink}
|
||||
rel="noreferrer noopener"
|
||||
className="me-2"
|
||||
>
|
||||
{t('view_payment_portal')}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3">
|
||||
<PriceExceptions subscription={subscription} />
|
||||
{!recurlyLoadError && (
|
||||
<p>
|
||||
<i>
|
||||
<SubscriptionRemainder subscription={subscription} hideTime />
|
||||
</i>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<hr />
|
||||
<h2 className="h3 fw-bold">{t('plan')}</h2>
|
||||
<h3 className="h5 mt-0 mb-1 fw-bold">{planName}</h3>
|
||||
{subscription.pendingPlan &&
|
||||
subscription.pendingPlan.name !== subscription.plan.name && (
|
||||
<p>{t('want_change_to_apply_before_plan_end')}</p>
|
||||
<p className="mb-1">{t('want_change_to_apply_before_plan_end')}</p>
|
||||
)}
|
||||
{(!subscription.pendingPlan ||
|
||||
subscription.pendingPlan.name === subscription.plan.name) &&
|
||||
subscription.plan.groupPlan && <ContactSupportToChangeGroupPlan />}
|
||||
{isInFreeTrial(subscription.payment.trialEndsAt) &&
|
||||
subscription.payment.trialEndsAtFormatted && (
|
||||
<TrialEnding
|
||||
trialEndsAtFormatted={subscription.payment.trialEndsAtFormatted}
|
||||
className="mb-1"
|
||||
/>
|
||||
)}
|
||||
{subscription.payment.totalLicenses > 0 && (
|
||||
<p className="mb-1" data-testid="plan-licenses">
|
||||
{isLegacyPlan &&
|
||||
subscription.payment.additionalLicenses > 0 &&
|
||||
!subscription.payment.pendingAdditionalLicenses ? (
|
||||
<Trans
|
||||
i18nKey="plus_x_additional_licenses_for_a_total_of_y_licenses"
|
||||
values={{
|
||||
count: subscription.payment.totalLicenses,
|
||||
additionalLicenses: subscription.payment.additionalLicenses,
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />, <strong />]} // eslint-disable-line react/jsx-key
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="supports_up_to_x_licenses"
|
||||
values={{ count: subscription.payment.totalLicenses }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[<strong />]} // eslint-disable-line react/jsx-key
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{subscription.pendingPlan && (
|
||||
<p className="mb-1" data-testid="pending-plan-change">
|
||||
{' '}
|
||||
<PendingPlanChange subscription={subscription} />
|
||||
</p>
|
||||
)}
|
||||
|
||||
{hasPendingPause && (
|
||||
<>
|
||||
@@ -154,83 +247,155 @@ export function ActiveSubscription({
|
||||
/>
|
||||
</p>
|
||||
<p>{t('you_can_still_use_your_premium_features')}</p>
|
||||
<p>
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={handleCancelPendingPauseClick}
|
||||
disabled={isLoadingCancelPause}
|
||||
>
|
||||
{isLoadingCancelPause ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
t('unpause_subscription')
|
||||
)}
|
||||
</OLButton>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
<p>
|
||||
<Trans
|
||||
i18nKey="next_payment_of_x_collectected_on_y"
|
||||
values={{
|
||||
paymentAmmount: subscription.payment.displayPrice,
|
||||
collectionDate: getFormattedRenewalDate(),
|
||||
}}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
components={[
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
<strong />,
|
||||
]}
|
||||
/>
|
||||
</p>
|
||||
|
||||
<hr />
|
||||
<PriceExceptions subscription={subscription} />
|
||||
<p className="d-inline-flex flex-wrap gap-1">
|
||||
<a
|
||||
href={subscription.payment.billingDetailsLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="btn btn-secondary-info btn-secondary"
|
||||
>
|
||||
{t('update_your_billing_details')}
|
||||
</a>{' '}
|
||||
<a
|
||||
href={subscription.payment.accountManagementLink}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
className="btn btn-secondary-info btn-secondary"
|
||||
>
|
||||
{t('view_your_invoices')}
|
||||
</a>
|
||||
{!recurlyLoadError && (
|
||||
<>
|
||||
{' '}
|
||||
<CancelSubscriptionButton />
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
{!onStandalonePlan && (
|
||||
<p className="mb-1" data-testid="plan-only-price">
|
||||
{subscription.plan.annual
|
||||
? t('x_price_per_year', {
|
||||
price: subscription.payment.planOnlyDisplayPrice,
|
||||
})
|
||||
: t('x_price_per_month', {
|
||||
price: subscription.payment.planOnlyDisplayPrice,
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
{!recurlyLoadError && (
|
||||
<>
|
||||
<br />
|
||||
<p>
|
||||
<i>
|
||||
<SubscriptionRemainder subscription={subscription} />
|
||||
</i>
|
||||
</p>
|
||||
</>
|
||||
<PlanActions
|
||||
subscription={subscription}
|
||||
onStandalonePlan={onStandalonePlan}
|
||||
handlePlanChange={handlePlanChange}
|
||||
hasPendingPause={hasPendingPause}
|
||||
cancelPauseReq={cancelPauseReq}
|
||||
/>
|
||||
)}
|
||||
<hr />
|
||||
<AddOns
|
||||
subscription={subscription}
|
||||
onStandalonePlan={onStandalonePlan}
|
||||
handleCancelClick={handleCancelClick}
|
||||
/>
|
||||
|
||||
<ChangePlanModal />
|
||||
<ConfirmChangePlanModal />
|
||||
<KeepCurrentPlanModal />
|
||||
<ChangeToGroupModal />
|
||||
<CancelAiAddOnModal />
|
||||
<PauseSubscriptionModal />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type PlanActionsProps = {
|
||||
subscription: PaidSubscription
|
||||
onStandalonePlan: boolean
|
||||
handlePlanChange: () => void
|
||||
hasPendingPause: boolean
|
||||
cancelPauseReq: ReturnType<typeof useAsync>
|
||||
}
|
||||
|
||||
function PlanActions({
|
||||
subscription,
|
||||
onStandalonePlan,
|
||||
handlePlanChange,
|
||||
hasPendingPause,
|
||||
cancelPauseReq,
|
||||
}: PlanActionsProps) {
|
||||
const { t } = useTranslation()
|
||||
const isSubscriptionEligibleForFlexibleGroupLicensing = getMeta(
|
||||
'ol-canUseFlexibleLicensing'
|
||||
)
|
||||
const location = useLocation()
|
||||
const { runAsync: runAsyncCancelPause, isLoading: isLoadingCancelPause } =
|
||||
cancelPauseReq
|
||||
|
||||
const handleCancelPendingPauseClick = async () => {
|
||||
try {
|
||||
await runAsyncCancelPause(postJSON('/user/subscription/pause/0'))
|
||||
const newUrl = new URL(location.toString())
|
||||
newUrl.searchParams.set('flash', 'unpaused')
|
||||
window.history.replaceState(null, '', newUrl)
|
||||
location.reload()
|
||||
} catch (e) {
|
||||
debugConsole.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mt-3">
|
||||
{isSubscriptionEligibleForFlexibleGroupLicensing ? (
|
||||
<FlexibleGroupLicensingActions subscription={subscription} />
|
||||
) : (
|
||||
<>
|
||||
{!hasPendingPause && !subscription.payment.hasPastDueInvoice && (
|
||||
<OLButton variant="secondary" onClick={handlePlanChange}>
|
||||
{t('change_plan')}
|
||||
</OLButton>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{hasPendingPause && (
|
||||
<OLButton
|
||||
variant="primary"
|
||||
onClick={handleCancelPendingPauseClick}
|
||||
disabled={isLoadingCancelPause}
|
||||
>
|
||||
{isLoadingCancelPause ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
t('unpause_subscription')
|
||||
)}
|
||||
</OLButton>
|
||||
)}
|
||||
{!onStandalonePlan && (
|
||||
<>
|
||||
{' '}
|
||||
<CancelSubscriptionButton />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FlexibleGroupLicensingActions({
|
||||
subscription,
|
||||
}: {
|
||||
subscription: PaidSubscription
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (subscription.pendingPlan || subscription.payment.hasPastDueInvoice) {
|
||||
return null
|
||||
}
|
||||
|
||||
const isProfessionalPlan = subscription.planCode
|
||||
.toLowerCase()
|
||||
.includes('professional')
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isProfessionalPlan && (
|
||||
<>
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
href="/user/subscription/group/upgrade-subscription"
|
||||
onClick={() =>
|
||||
sendMB('flex-upgrade', { location: 'upgrade-plan-button' })
|
||||
}
|
||||
>
|
||||
{t('upgrade_plan')}
|
||||
</OLButton>{' '}
|
||||
</>
|
||||
)}
|
||||
{subscription.plan.membersLimitAddOn === 'additional-license' && (
|
||||
<OLButton
|
||||
variant="secondary"
|
||||
href="/user/subscription/group/add-users"
|
||||
onClick={() => sendMB('flex-add-users')}
|
||||
>
|
||||
{t('buy_more_licenses')}
|
||||
</OLButton>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export function TrialEnding({
|
||||
className,
|
||||
}: TrialEndingProps) {
|
||||
return (
|
||||
<p className={className}>
|
||||
<p className={className} data-testid="trial-ending">
|
||||
<Trans
|
||||
i18nKey="youre_on_free_trial_which_ends_on"
|
||||
values={{ date: trialEndsAtFormatted }}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function PriceExceptions({ subscription }: PriceExceptionsProps) {
|
||||
{activeCoupons.length > 0 && (
|
||||
<>
|
||||
<i>* {t('coupons_not_included')}:</i>
|
||||
<ul>
|
||||
<ul data-testid="active-coupons">
|
||||
{activeCoupons.map(coupon => (
|
||||
<li key={coupon.code}>
|
||||
<i>{coupon.description || coupon.name}</i>
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import React from 'react'
|
||||
import { ActiveSubscription } from '../../js/features/subscription/components/dashboard/states/active/active'
|
||||
import {
|
||||
annualActiveSubscription,
|
||||
groupActiveSubscription,
|
||||
groupActiveSubscriptionWithPendingLicenseChange,
|
||||
monthlyActiveCollaborator,
|
||||
pendingSubscriptionChange,
|
||||
trialCollaboratorSubscription,
|
||||
trialSubscription,
|
||||
pastDueExpiredSubscription,
|
||||
annualActiveSubscriptionEuro,
|
||||
annualActiveSubscriptionWithAddons,
|
||||
annualActiveSubscriptionWithCoupons,
|
||||
pendingPausedSubscription,
|
||||
groupProfessionalActiveSubscription,
|
||||
} from '../../../test/frontend/features/subscription/fixtures/subscriptions'
|
||||
import { SubscriptionDashboardProvider } from '../../js/features/subscription/context/subscription-dashboard-context'
|
||||
import { SplitTestProvider } from '@/shared/context/split-test-context'
|
||||
import { PaidSubscription } from '@ol-types/subscription/dashboard/subscription'
|
||||
import type { StoryFn } from '@storybook/react'
|
||||
import { setupSubscriptionDashContext } from '../../../test/frontend/features/subscription/helpers/setup-subscription-dash-context'
|
||||
|
||||
export default {
|
||||
title: 'Subscription/ActiveSubscription',
|
||||
component: ActiveSubscription,
|
||||
argTypes: {
|
||||
subscription: { control: 'object' },
|
||||
canUseFlexibleLicensing: { control: 'boolean' },
|
||||
},
|
||||
}
|
||||
|
||||
const Template: StoryFn<{
|
||||
subscription: PaidSubscription
|
||||
canUseFlexibleLicensing: boolean
|
||||
}> = args => {
|
||||
window.metaAttributesCache = window.metaAttributesCache || new Map()
|
||||
// @ts-ignore
|
||||
delete global.recurly
|
||||
setupSubscriptionDashContext({
|
||||
metaTags: [
|
||||
{ name: 'ol-subscription', value: args.subscription },
|
||||
{
|
||||
name: 'ol-canUseFlexibleLicensing',
|
||||
value: args.canUseFlexibleLicensing,
|
||||
},
|
||||
],
|
||||
currencyCode: args.subscription.payment.currency,
|
||||
recurlyNotLoaded: false,
|
||||
queryingRecurly: false,
|
||||
})
|
||||
return (
|
||||
<SplitTestProvider>
|
||||
<SubscriptionDashboardProvider>
|
||||
<ActiveSubscription {...args} />
|
||||
</SubscriptionDashboardProvider>
|
||||
</SplitTestProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export const CollaboratorAnnual = Template.bind({})
|
||||
CollaboratorAnnual.args = {
|
||||
subscription: annualActiveSubscription,
|
||||
canUseFlexibleLicensing:
|
||||
annualActiveSubscription.plan?.canUseFlexibleLicensing,
|
||||
}
|
||||
|
||||
export const CollaboratorMonthly = Template.bind({})
|
||||
CollaboratorMonthly.args = {
|
||||
subscription: monthlyActiveCollaborator,
|
||||
canUseFlexibleLicensing:
|
||||
monthlyActiveCollaborator.plan?.canUseFlexibleLicensing,
|
||||
}
|
||||
|
||||
export const CollaboratorAnnualEuro = Template.bind({})
|
||||
CollaboratorAnnualEuro.args = {
|
||||
subscription: annualActiveSubscriptionEuro,
|
||||
canUseFlexibleLicensing:
|
||||
annualActiveSubscriptionEuro.plan?.canUseFlexibleLicensing,
|
||||
}
|
||||
|
||||
export const CollaboratorTrial = Template.bind({})
|
||||
CollaboratorTrial.args = {
|
||||
subscription: trialCollaboratorSubscription,
|
||||
canUseFlexibleLicensing:
|
||||
trialCollaboratorSubscription.plan?.canUseFlexibleLicensing,
|
||||
}
|
||||
|
||||
export const PersonalTrial = Template.bind({})
|
||||
PersonalTrial.args = {
|
||||
subscription: trialSubscription,
|
||||
canUseFlexibleLicensing: trialSubscription.plan?.canUseFlexibleLicensing,
|
||||
}
|
||||
|
||||
export const GroupStandard = Template.bind({})
|
||||
GroupStandard.args = {
|
||||
subscription: groupActiveSubscription,
|
||||
canUseFlexibleLicensing:
|
||||
groupActiveSubscription.plan?.canUseFlexibleLicensing,
|
||||
}
|
||||
|
||||
export const GroupProfessional = Template.bind({})
|
||||
GroupProfessional.args = {
|
||||
subscription: groupProfessionalActiveSubscription,
|
||||
canUseFlexibleLicensing:
|
||||
groupProfessionalActiveSubscription.plan?.canUseFlexibleLicensing,
|
||||
}
|
||||
|
||||
export const GroupPendingLicenseChange = Template.bind({})
|
||||
GroupPendingLicenseChange.args = {
|
||||
subscription: groupActiveSubscriptionWithPendingLicenseChange,
|
||||
canUseFlexibleLicensing:
|
||||
groupActiveSubscriptionWithPendingLicenseChange.plan
|
||||
?.canUseFlexibleLicensing,
|
||||
}
|
||||
|
||||
export const PastDueExpired = Template.bind({})
|
||||
PastDueExpired.args = {
|
||||
subscription: pastDueExpiredSubscription,
|
||||
canUseFlexibleLicensing:
|
||||
pastDueExpiredSubscription.plan?.canUseFlexibleLicensing,
|
||||
}
|
||||
|
||||
export const Addons = Template.bind({})
|
||||
Addons.args = {
|
||||
subscription: annualActiveSubscriptionWithAddons,
|
||||
canUseFlexibleLicensing:
|
||||
annualActiveSubscriptionWithAddons.plan?.canUseFlexibleLicensing,
|
||||
}
|
||||
|
||||
export const PendingPaused = Template.bind({})
|
||||
PendingPaused.args = {
|
||||
subscription: pendingPausedSubscription,
|
||||
canUseFlexibleLicensing:
|
||||
pendingPausedSubscription.plan?.canUseFlexibleLicensing,
|
||||
}
|
||||
|
||||
export const PendingPlanChange = Template.bind({})
|
||||
PendingPlanChange.args = {
|
||||
subscription: pendingSubscriptionChange,
|
||||
canUseFlexibleLicensing:
|
||||
pendingSubscriptionChange.plan?.canUseFlexibleLicensing,
|
||||
}
|
||||
|
||||
export const Coupons = Template.bind({})
|
||||
Coupons.args = {
|
||||
subscription: annualActiveSubscriptionWithCoupons,
|
||||
canUseFlexibleLicensing:
|
||||
annualActiveSubscriptionWithCoupons.plan?.canUseFlexibleLicensing,
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
import { expect } from 'chai'
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import { fireEvent, screen, waitFor, within } from '@testing-library/react'
|
||||
import * as eventTracking from '@/infrastructure/event-tracking'
|
||||
import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
|
||||
import {
|
||||
annualActiveSubscription,
|
||||
annualActiveSubscriptionEuro,
|
||||
annualActiveSubscriptionWithAddons,
|
||||
annualActiveSubscriptionWithCoupons,
|
||||
groupActiveSubscription,
|
||||
groupActiveSubscriptionWithPendingLicenseChange,
|
||||
groupProfessionalActiveSubscription,
|
||||
monthlyActiveCollaborator,
|
||||
pendingSubscriptionChange,
|
||||
trialCollaboratorSubscription,
|
||||
@@ -37,45 +41,78 @@ describe('<ActiveSubscription />', function () {
|
||||
})
|
||||
|
||||
function expectedInActiveSubscription(subscription: PaidSubscription) {
|
||||
// sentence broken up by bolding
|
||||
screen.getByText('You are currently subscribed to the', { exact: false })
|
||||
screen.getByText(subscription.plan.name, { exact: false })
|
||||
if (subscription.plan.annual) {
|
||||
within(screen.getByTestId('billing-period')).getByText((_, el) =>
|
||||
Boolean(
|
||||
el?.textContent?.includes(
|
||||
`Billed annually at ${subscription.payment.displayPrice}`
|
||||
)
|
||||
)
|
||||
)
|
||||
within(screen.getByTestId('plan-only-price')).getByText((_, el) =>
|
||||
Boolean(
|
||||
el?.textContent?.includes(
|
||||
`${subscription.payment.planOnlyDisplayPrice} per year`
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
within(screen.getByTestId('billing-period')).getByText((_, el) =>
|
||||
Boolean(
|
||||
el?.textContent?.includes(
|
||||
`Billed monthly at ${subscription.payment.displayPrice}`
|
||||
)
|
||||
)
|
||||
)
|
||||
within(screen.getByTestId('plan-only-price')).getByText((_, el) =>
|
||||
Boolean(
|
||||
el?.textContent?.includes(
|
||||
`${subscription.payment.planOnlyDisplayPrice} per month`
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
within(screen.getByTestId('renews-on')).getByText((_, el) =>
|
||||
Boolean(
|
||||
el?.textContent?.includes(
|
||||
`Renews on ${subscription.payment.nextPaymentDueDate}`
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
screen.getByRole('button', { name: 'Change plan' })
|
||||
|
||||
// sentence broken up by bolding
|
||||
screen.getByText('The next payment of', { exact: false })
|
||||
screen.getByText(subscription.payment.displayPrice, {
|
||||
exact: false,
|
||||
})
|
||||
screen.getByText('will be collected on', { exact: false })
|
||||
const dates = screen.getAllByText(subscription.payment.nextPaymentDueAt, {
|
||||
exact: false,
|
||||
})
|
||||
expect(dates.length).to.equal(2)
|
||||
screen.getByRole('heading', { name: subscription.plan.name, level: 3 })
|
||||
|
||||
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' })
|
||||
screen.getByRole('link', { name: 'View invoices' })
|
||||
|
||||
if (subscription.payment.billingDetailsLink) {
|
||||
screen.getByRole('link', { name: 'View billing details' })
|
||||
}
|
||||
}
|
||||
|
||||
it('renders the dash annual active subscription', function () {
|
||||
renderActiveSubscription(annualActiveSubscription)
|
||||
expectedInActiveSubscription(annualActiveSubscription)
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||
expect(button).to.exist
|
||||
})
|
||||
|
||||
it('renders the dash annual active subscription in EUR', function () {
|
||||
renderActiveSubscription(annualActiveSubscriptionEuro)
|
||||
expectedInActiveSubscription(annualActiveSubscriptionEuro)
|
||||
})
|
||||
|
||||
it('shows change plan UI when button clicked', async function () {
|
||||
renderActiveSubscription(annualActiveSubscription)
|
||||
expectedInActiveSubscription(annualActiveSubscription)
|
||||
|
||||
const button = screen.getByRole('button', { name: 'Change plan' })
|
||||
fireEvent.click(button)
|
||||
|
||||
// confirm main dash UI still shown
|
||||
screen.getByText('You are currently subscribed to the', { exact: false })
|
||||
|
||||
await screen.findByRole('heading', { name: 'Change plan' })
|
||||
await waitFor(
|
||||
() =>
|
||||
@@ -86,34 +123,9 @@ describe('<ActiveSubscription />', function () {
|
||||
)
|
||||
})
|
||||
|
||||
it('notes when user is changing plan at end of current plan term', function () {
|
||||
renderActiveSubscription(pendingSubscriptionChange)
|
||||
|
||||
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,
|
||||
})
|
||||
|
||||
screen.getByText(
|
||||
'If you wish this change to apply before the end of your current billing period, please contact us.'
|
||||
)
|
||||
|
||||
expect(screen.queryByRole('link', { name: 'contact Support' })).to.be.null
|
||||
expect(screen.queryByText('if you wish to change your group subscription.'))
|
||||
.to.be.null
|
||||
})
|
||||
|
||||
it('does not show "Change plan" option when past due', function () {
|
||||
// account is likely in expired state, but be sure to not show option if state is still active
|
||||
const activePastDueSubscription = Object.assign(
|
||||
{},
|
||||
JSON.parse(JSON.stringify(annualActiveSubscription))
|
||||
)
|
||||
|
||||
const activePastDueSubscription = cloneDeep(annualActiveSubscription)
|
||||
activePastDueSubscription.payment.hasPastDueInvoice = true
|
||||
|
||||
renderActiveSubscription(activePastDueSubscription)
|
||||
@@ -122,23 +134,41 @@ describe('<ActiveSubscription />', function () {
|
||||
expect(changePlan).to.be.null
|
||||
})
|
||||
|
||||
it('notes when user is changing plan at end of current plan term', function () {
|
||||
renderActiveSubscription(pendingSubscriptionChange)
|
||||
|
||||
expectedInActiveSubscription(pendingSubscriptionChange)
|
||||
|
||||
within(screen.getByTestId('pending-plan-change')).getByText((_, el) =>
|
||||
Boolean(
|
||||
el?.textContent?.includes(
|
||||
`Your plan is changing to ${pendingSubscriptionChange.pendingPlan!.name} at the end of the current billing period`
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
screen.getByText(
|
||||
'If you wish this change to apply before the end of your current billing period, please contact us.'
|
||||
)
|
||||
})
|
||||
|
||||
it('shows the pending license change message when plan change is pending', function () {
|
||||
renderActiveSubscription(groupActiveSubscriptionWithPendingLicenseChange)
|
||||
|
||||
screen.getByText('Your subscription is changing to include', {
|
||||
exact: false,
|
||||
})
|
||||
|
||||
screen.getByText(
|
||||
groupActiveSubscriptionWithPendingLicenseChange.payment
|
||||
.pendingAdditionalLicenses!
|
||||
within(screen.getByTestId('pending-plan-change')).getByText((_, el) =>
|
||||
Boolean(
|
||||
el?.textContent?.includes(
|
||||
`Your subscription is changing to include ${groupActiveSubscriptionWithPendingLicenseChange.payment.pendingAdditionalLicenses} additional license(s) for a total of ${groupActiveSubscriptionWithPendingLicenseChange.payment.pendingTotalLicenses}`
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
screen.getByText('additional license(s) for a total of', { exact: false })
|
||||
|
||||
screen.getByText(
|
||||
groupActiveSubscriptionWithPendingLicenseChange.payment
|
||||
.pendingTotalLicenses!
|
||||
within(screen.getByTestId('plan-licenses')).getByText((_, el) =>
|
||||
Boolean(
|
||||
el?.textContent?.includes(
|
||||
`Supports up to ${groupActiveSubscriptionWithPendingLicenseChange.payment.totalLicenses}`
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
expect(
|
||||
@@ -148,8 +178,8 @@ describe('<ActiveSubscription />', function () {
|
||||
).to.be.null
|
||||
})
|
||||
|
||||
it('shows the pending license change message when plan change is not pending', function () {
|
||||
const subscription = Object.assign({}, groupActiveSubscription)
|
||||
it('for legacy plans shows the pending license change message when plan change is not pending', function () {
|
||||
const subscription = cloneDeep(groupActiveSubscription)
|
||||
subscription.payment.additionalLicenses = 4
|
||||
subscription.payment.totalLicenses =
|
||||
subscription.payment.totalLicenses +
|
||||
@@ -157,42 +187,71 @@ describe('<ActiveSubscription />', function () {
|
||||
|
||||
renderActiveSubscription(subscription)
|
||||
|
||||
screen.getByText('Your subscription includes', {
|
||||
exact: false,
|
||||
})
|
||||
|
||||
screen.getByText(subscription.payment.additionalLicenses)
|
||||
|
||||
screen.getByText('additional license(s) for a total of', { exact: false })
|
||||
|
||||
screen.getByText(subscription.payment.totalLicenses)
|
||||
within(screen.getByTestId('plan-licenses')).getByText((_, el) =>
|
||||
Boolean(
|
||||
el?.textContent?.includes(
|
||||
`Plus ${subscription.payment.additionalLicenses} additional license(s) for a total of ${subscription.payment.totalLicenses}`
|
||||
)
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
it('shows when trial ends and first payment collected and when subscription would become inactive if cancelled', function () {
|
||||
renderActiveSubscription(trialSubscription)
|
||||
screen.getByText('You’re on a free trial which ends on', { exact: false })
|
||||
|
||||
const endDate = screen.getAllByText(
|
||||
trialSubscription.payment.trialEndsAtFormatted!
|
||||
within(screen.getByTestId('trial-ending')).getByText((_, el) =>
|
||||
Boolean(
|
||||
el?.textContent?.includes(
|
||||
`You’re on a free trial which ends on ${trialSubscription.payment.trialEndsAtFormatted}`
|
||||
)
|
||||
)
|
||||
)
|
||||
expect(endDate.length).to.equal(3)
|
||||
})
|
||||
|
||||
it('shows current discounts', function () {
|
||||
const subscriptionWithActiveCoupons = cloneDeep(annualActiveSubscription)
|
||||
subscriptionWithActiveCoupons.payment.activeCoupons = [
|
||||
{
|
||||
name: 'fake coupon name',
|
||||
code: 'fake-coupon',
|
||||
description: '',
|
||||
},
|
||||
]
|
||||
renderActiveSubscription(subscriptionWithActiveCoupons)
|
||||
screen.getByText(
|
||||
/this does not include your current discounts, which will be applied automatically before your next payment/i
|
||||
it('shows correct actions for group plan: professional ', function () {
|
||||
renderActiveSubscription(groupProfessionalActiveSubscription)
|
||||
screen.getByRole('link', { name: /buy more licenses/i })
|
||||
})
|
||||
|
||||
it('shows correct actions for group plan: standard (collaborator)', function () {
|
||||
renderActiveSubscription(groupActiveSubscription)
|
||||
screen.getByRole('link', { name: /upgrade plan/i })
|
||||
screen.getByRole('link', { name: /buy more licenses/i })
|
||||
})
|
||||
|
||||
it('shows add-ons if present', function () {
|
||||
renderActiveSubscription(annualActiveSubscriptionWithAddons)
|
||||
screen.getByText('AI Assist')
|
||||
})
|
||||
|
||||
it('shows empty add-ons message if none present', function () {
|
||||
renderActiveSubscription(annualActiveSubscription)
|
||||
screen.getByText(/You don’t have any add-ons on your account/i)
|
||||
})
|
||||
|
||||
it('shows multiple active coupons', function () {
|
||||
renderActiveSubscription(annualActiveSubscriptionWithCoupons)
|
||||
within(screen.getByTestId('active-coupons')).getByText(
|
||||
'Coupon1 for 10% off',
|
||||
{ exact: false }
|
||||
)
|
||||
screen.getByText(
|
||||
subscriptionWithActiveCoupons.payment.activeCoupons[0].name
|
||||
within(screen.getByTestId('active-coupons')).getByText(
|
||||
'Coupon2 for 15% off',
|
||||
{ exact: false }
|
||||
)
|
||||
})
|
||||
|
||||
it('renders correct hrefs for invoice and billing details links', function () {
|
||||
renderActiveSubscription(annualActiveSubscription)
|
||||
const invoiceLink = screen.getByRole('link', { name: 'View invoices' })
|
||||
expect(invoiceLink.getAttribute('href')).to.equal(
|
||||
annualActiveSubscription.payment.accountManagementLink
|
||||
)
|
||||
const billingLink = screen.getByRole('link', {
|
||||
name: 'View billing details',
|
||||
})
|
||||
expect(billingLink.getAttribute('href')).to.equal(
|
||||
annualActiveSubscription.payment.billingDetailsLink
|
||||
)
|
||||
})
|
||||
|
||||
@@ -523,13 +582,5 @@ describe('<ActiveSubscription />', function () {
|
||||
const changePlan = screen.queryByRole('button', { name: 'Change plan' })
|
||||
expect(changePlan).to.be.null
|
||||
})
|
||||
|
||||
it('shows contact Support message for group plan change requests', function () {
|
||||
renderActiveSubscription(groupActiveSubscription)
|
||||
screen.getByRole('link', { name: 'contact Support' })
|
||||
screen.getByText('if you wish to change your group subscription.', {
|
||||
exact: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -31,6 +31,7 @@ export const annualActiveSubscription: PaidSubscription = {
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
featureDescription: [],
|
||||
canUseFlexibleLicensing: false,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
@@ -48,7 +49,7 @@ export const annualActiveSubscription: PaidSubscription = {
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$199.00',
|
||||
planOnlyDisplayPrice: '',
|
||||
planOnlyDisplayPrice: '$199.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
@@ -57,6 +58,186 @@ export const annualActiveSubscription: PaidSubscription = {
|
||||
},
|
||||
}
|
||||
|
||||
export const annualActiveSubscriptionWithCoupons: PaidSubscription = {
|
||||
manager_ids: ['abc123'],
|
||||
member_ids: [],
|
||||
invited_emails: [],
|
||||
groupPlan: false,
|
||||
membersLimit: 0,
|
||||
_id: 'def456',
|
||||
admin_id: 'abc123',
|
||||
teamInvites: [],
|
||||
planCode: 'collaborator-annual',
|
||||
plan: {
|
||||
planCode: 'collaborator-annual',
|
||||
name: 'Standard (Collaborator) Annual',
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
featureDescription: [],
|
||||
canUseFlexibleLicensing: false,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
billingDetailsLink: '/user/subscription/payment/billing-details',
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [
|
||||
{ name: 'Coupon1', code: 'c1', description: 'Coupon1 for 10% off' },
|
||||
{ name: 'Coupon2', code: 'c2', description: 'Coupon2 for 15% off' },
|
||||
],
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$199.00',
|
||||
planOnlyDisplayPrice: '$199.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
isEligibleForPause: false,
|
||||
isEligibleForDowngradeUpsell: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const pendingPausedSubscription: PaidSubscription = {
|
||||
manager_ids: ['abc123'],
|
||||
member_ids: [],
|
||||
invited_emails: [],
|
||||
groupPlan: false,
|
||||
membersLimit: 0,
|
||||
_id: 'def456',
|
||||
admin_id: 'abc123',
|
||||
teamInvites: [],
|
||||
planCode: 'collaborator-annual',
|
||||
plan: {
|
||||
planCode: 'collaborator-annual',
|
||||
name: 'Standard (Collaborator) Annual',
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
featureDescription: [],
|
||||
canUseFlexibleLicensing: false,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
billingDetailsLink: '/user/subscription/payment/billing-details',
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
remainingPauseCycles: 1,
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$199.00',
|
||||
planOnlyDisplayPrice: '$199.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
isEligibleForPause: false,
|
||||
isEligibleForDowngradeUpsell: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const pausedSubscription: PaidSubscription = {
|
||||
manager_ids: ['abc123'],
|
||||
member_ids: [],
|
||||
invited_emails: [],
|
||||
groupPlan: false,
|
||||
membersLimit: 0,
|
||||
_id: 'def456',
|
||||
admin_id: 'abc123',
|
||||
teamInvites: [],
|
||||
planCode: 'collaborator-annual',
|
||||
plan: {
|
||||
planCode: 'collaborator-annual',
|
||||
name: 'Standard (Collaborator) Annual',
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
featureDescription: [],
|
||||
canUseFlexibleLicensing: false,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
billingDetailsLink: '/user/subscription/payment/billing-details',
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
currency: 'USD',
|
||||
state: 'paused',
|
||||
remainingPauseCycles: 1,
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$199.00',
|
||||
planOnlyDisplayPrice: '$199.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
isEligibleForPause: false,
|
||||
isEligibleForDowngradeUpsell: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const annualActiveSubscriptionWithAddons: PaidSubscription = {
|
||||
manager_ids: ['abc123'],
|
||||
member_ids: [],
|
||||
invited_emails: [],
|
||||
groupPlan: false,
|
||||
membersLimit: 0,
|
||||
_id: 'def456',
|
||||
admin_id: 'abc123',
|
||||
teamInvites: [],
|
||||
planCode: 'collaborator-annual',
|
||||
plan: {
|
||||
planCode: 'collaborator-annual',
|
||||
name: 'Standard (Collaborator) Annual',
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
featureDescription: [],
|
||||
canUseFlexibleLicensing: false,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
billingDetailsLink: '/user/subscription/payment/billing-details',
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 0,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$199.00',
|
||||
planOnlyDisplayPrice: '$199.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {
|
||||
assistant: '$100.00',
|
||||
},
|
||||
isEligibleForGroupPlan: true,
|
||||
isEligibleForPause: false,
|
||||
isEligibleForDowngradeUpsell: false,
|
||||
},
|
||||
addOns: [{ addOnCode: 'assistant', quantity: 1, unitAmountInCents: 10000 }],
|
||||
}
|
||||
|
||||
export const annualActiveSubscriptionEuro: PaidSubscription = {
|
||||
manager_ids: ['abc123'],
|
||||
member_ids: [],
|
||||
@@ -73,6 +254,7 @@ export const annualActiveSubscriptionEuro: PaidSubscription = {
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
featureDescription: [],
|
||||
canUseFlexibleLicensing: false,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0.24,
|
||||
@@ -90,7 +272,7 @@ export const annualActiveSubscriptionEuro: PaidSubscription = {
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '€221.96',
|
||||
planOnlyDisplayPrice: '',
|
||||
planOnlyDisplayPrice: '€221.96',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
@@ -114,6 +296,7 @@ export const annualActiveSubscriptionPro: PaidSubscription = {
|
||||
name: 'Professional',
|
||||
price_in_cents: 4500,
|
||||
featureDescription: [],
|
||||
canUseFlexibleLicensing: false,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
@@ -131,7 +314,7 @@ export const annualActiveSubscriptionPro: PaidSubscription = {
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$42.00',
|
||||
planOnlyDisplayPrice: '',
|
||||
planOnlyDisplayPrice: '$42.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
@@ -156,6 +339,7 @@ export const pastDueExpiredSubscription: PaidSubscription = {
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
featureDescription: [],
|
||||
canUseFlexibleLicensing: false,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
@@ -173,7 +357,7 @@ export const pastDueExpiredSubscription: PaidSubscription = {
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: true,
|
||||
displayPrice: '$199.00',
|
||||
planOnlyDisplayPrice: '',
|
||||
planOnlyDisplayPrice: '$199.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
@@ -198,6 +382,7 @@ export const canceledSubscription: PaidSubscription = {
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
featureDescription: [],
|
||||
canUseFlexibleLicensing: false,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
@@ -215,7 +400,7 @@ export const canceledSubscription: PaidSubscription = {
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$199.00',
|
||||
planOnlyDisplayPrice: '',
|
||||
planOnlyDisplayPrice: '$199.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
@@ -240,6 +425,7 @@ export const pendingSubscriptionChange: PaidSubscription = {
|
||||
price_in_cents: 21900,
|
||||
annual: true,
|
||||
featureDescription: [],
|
||||
canUseFlexibleLicensing: false,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
@@ -257,7 +443,7 @@ export const pendingSubscriptionChange: PaidSubscription = {
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$199.00',
|
||||
planOnlyDisplayPrice: '',
|
||||
planOnlyDisplayPrice: '$199.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
@@ -293,6 +479,7 @@ export const groupActiveSubscription: GroupSubscription = {
|
||||
groupPlan: true,
|
||||
membersLimit: 10,
|
||||
membersLimitAddOn: 'additional-license',
|
||||
canUseFlexibleLicensing: true,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
@@ -310,7 +497,54 @@ export const groupActiveSubscription: GroupSubscription = {
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$1290.00',
|
||||
planOnlyDisplayPrice: '',
|
||||
planOnlyDisplayPrice: '$1290.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
isEligibleForPause: false,
|
||||
isEligibleForDowngradeUpsell: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const groupProfessionalActiveSubscription: GroupSubscription = {
|
||||
manager_ids: ['abc123'],
|
||||
member_ids: ['abc123'],
|
||||
invited_emails: [],
|
||||
groupPlan: true,
|
||||
teamName: 'GAS',
|
||||
membersLimit: 2,
|
||||
_id: 'bcd567',
|
||||
admin_id: 'abc123',
|
||||
teamInvites: [],
|
||||
planCode: 'group_professional_2_enterprise',
|
||||
plan: {
|
||||
planCode: 'group_professional_2_enterprise',
|
||||
name: 'Group Professional Plan (2 licenses)',
|
||||
hideFromUsers: true,
|
||||
price_in_cents: 129000,
|
||||
annual: true,
|
||||
groupPlan: true,
|
||||
membersLimit: 2,
|
||||
membersLimitAddOn: 'additional-license',
|
||||
canUseFlexibleLicensing: true,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
billingDetailsLink: '/user/subscription/payment/billing-details',
|
||||
accountManagementLink: '/user/subscription/payment/account-management',
|
||||
additionalLicenses: 0,
|
||||
totalLicenses: 10,
|
||||
nextPaymentDueAt,
|
||||
nextPaymentDueDate,
|
||||
currency: 'USD',
|
||||
state: 'active',
|
||||
trialEndsAtFormatted: null,
|
||||
trialEndsAt: null,
|
||||
activeCoupons: [],
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$1290.00',
|
||||
planOnlyDisplayPrice: '$1290.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
@@ -340,6 +574,7 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
|
||||
groupPlan: true,
|
||||
membersLimit: 10,
|
||||
membersLimitAddOn: 'additional-license',
|
||||
canUseFlexibleLicensing: true,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
@@ -359,7 +594,7 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
|
||||
displayPrice: '$2967.00',
|
||||
pendingAdditionalLicenses: 13,
|
||||
pendingTotalLicenses: 23,
|
||||
planOnlyDisplayPrice: '',
|
||||
planOnlyDisplayPrice: '$2967.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
@@ -411,7 +646,7 @@ export const trialSubscription: PaidSubscription = {
|
||||
accountEmail: 'fake@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$14.00',
|
||||
planOnlyDisplayPrice: '',
|
||||
planOnlyDisplayPrice: '$14.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
@@ -456,6 +691,7 @@ export const trialCollaboratorSubscription: PaidSubscription = {
|
||||
price_in_cents: 2300,
|
||||
featureDescription: [],
|
||||
hideFromUsers: true,
|
||||
canUseFlexibleLicensing: false,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
@@ -473,7 +709,7 @@ export const trialCollaboratorSubscription: PaidSubscription = {
|
||||
accountEmail: 'foo@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$21.00',
|
||||
planOnlyDisplayPrice: '',
|
||||
planOnlyDisplayPrice: '$21.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
@@ -497,6 +733,7 @@ export const monthlyActiveCollaborator: PaidSubscription = {
|
||||
name: 'Standard (Collaborator)',
|
||||
price_in_cents: 212300900,
|
||||
featureDescription: [],
|
||||
canUseFlexibleLicensing: false,
|
||||
},
|
||||
payment: {
|
||||
taxRate: 0,
|
||||
@@ -514,7 +751,7 @@ export const monthlyActiveCollaborator: PaidSubscription = {
|
||||
accountEmail: 'foo@example.com',
|
||||
hasPastDueInvoice: false,
|
||||
displayPrice: '$21.00',
|
||||
planOnlyDisplayPrice: '',
|
||||
planOnlyDisplayPrice: '$21.00',
|
||||
addOns: [],
|
||||
addOnDisplayPricesWithoutAdditionalLicense: {},
|
||||
isEligibleForGroupPlan: true,
|
||||
|
||||
@@ -8,7 +8,8 @@ import { CurrencyCode } from '../../../../../types/subscription/currency'
|
||||
export function renderActiveSubscription(
|
||||
subscription: PaidSubscription,
|
||||
tags: MetaTag[] = [],
|
||||
currencyCode?: CurrencyCode
|
||||
currencyCode?: CurrencyCode,
|
||||
canUseFlexibleLicensing?: boolean
|
||||
) {
|
||||
renderWithSubscriptionDashContext(
|
||||
<ActiveSubscription subscription={subscription} />,
|
||||
@@ -26,6 +27,13 @@ export function renderActiveSubscription(
|
||||
name: 'ol-recommendedCurrency',
|
||||
value: currencyCode || 'USD',
|
||||
},
|
||||
{
|
||||
name: 'ol-canUseFlexibleLicensing',
|
||||
value:
|
||||
canUseFlexibleLicensing ||
|
||||
subscription.plan?.canUseFlexibleLicensing ||
|
||||
false,
|
||||
},
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { render } from '@testing-library/react'
|
||||
import _ from 'lodash'
|
||||
import { SubscriptionDashboardProvider } from '../../../../../frontend/js/features/subscription/context/subscription-dashboard-context'
|
||||
import { groupPriceByUsageTypeAndSize, plans } from '../fixtures/plans'
|
||||
import fetchMock from 'fetch-mock'
|
||||
import { SplitTestProvider } from '@/shared/context/split-test-context'
|
||||
import { MetaTag } from '@/utils/meta'
|
||||
import { setupSubscriptionDashContext } from './setup-subscription-dash-context'
|
||||
|
||||
export function renderWithSubscriptionDashContext(
|
||||
component: React.ReactElement,
|
||||
@@ -25,62 +25,7 @@ export function renderWithSubscriptionDashContext(
|
||||
</SplitTestProvider>
|
||||
)
|
||||
|
||||
options?.metaTags?.forEach(tag =>
|
||||
window.metaAttributesCache.set(tag!.name, tag!.value)
|
||||
)
|
||||
window.metaAttributesCache.set('ol-user', {})
|
||||
|
||||
if (!options?.recurlyNotLoaded) {
|
||||
// @ts-ignore
|
||||
global.recurly = {
|
||||
configure: () => {},
|
||||
Pricing: {
|
||||
Subscription: () => {
|
||||
return {
|
||||
plan: (planCode: string) => {
|
||||
let plan
|
||||
const isGroupPlan = planCode.includes('group')
|
||||
if (isGroupPlan) {
|
||||
const [, planType, size, usage] = planCode.split('_')
|
||||
const currencyCode = options?.currencyCode || 'USD'
|
||||
plan = _.get(groupPriceByUsageTypeAndSize, [
|
||||
usage,
|
||||
planType,
|
||||
currencyCode,
|
||||
size,
|
||||
])
|
||||
} else {
|
||||
plan = plans.find(p => p.planCode === planCode)
|
||||
}
|
||||
|
||||
const response = {
|
||||
next: {
|
||||
total: plan?.price_in_cents
|
||||
? plan.price_in_cents / 100
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
return {
|
||||
currency: () => {
|
||||
return {
|
||||
catch: () => {
|
||||
return {
|
||||
done: (callback: (response: object) => void) => {
|
||||
if (!options?.queryingRecurly) {
|
||||
return callback(response)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
setupSubscriptionDashContext(options)
|
||||
|
||||
return render(component, {
|
||||
wrapper: SubscriptionDashboardProviderWrapper,
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import _ from 'lodash'
|
||||
import { groupPriceByUsageTypeAndSize, plans } from '../fixtures/plans'
|
||||
import { MetaTag } from '@/utils/meta'
|
||||
|
||||
export function setupSubscriptionDashContext(options?: {
|
||||
metaTags?: MetaTag[]
|
||||
recurlyNotLoaded?: boolean
|
||||
queryingRecurly?: boolean
|
||||
currencyCode?: string
|
||||
}) {
|
||||
options?.metaTags?.forEach(tag =>
|
||||
window.metaAttributesCache.set(tag!.name, tag!.value)
|
||||
)
|
||||
window.metaAttributesCache.set('ol-user', {})
|
||||
if (!options?.recurlyNotLoaded) {
|
||||
// @ts-ignore
|
||||
global.recurly = {
|
||||
configure: () => {},
|
||||
Pricing: {
|
||||
Subscription: () => {
|
||||
return {
|
||||
plan: (planCode: string) => {
|
||||
let plan
|
||||
const isGroupPlan = planCode.includes('group')
|
||||
if (isGroupPlan) {
|
||||
const [, planType, size, usage] = planCode.split('_')
|
||||
const currencyCode = options?.currencyCode || 'USD'
|
||||
plan = _.get(groupPriceByUsageTypeAndSize, [
|
||||
usage,
|
||||
planType,
|
||||
currencyCode,
|
||||
size,
|
||||
])
|
||||
} else {
|
||||
plan = plans.find(p => p.planCode === planCode)
|
||||
}
|
||||
|
||||
const response = {
|
||||
next: {
|
||||
total: plan?.price_in_cents
|
||||
? plan.price_in_cents / 100
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
return {
|
||||
currency: () => {
|
||||
return {
|
||||
catch: () => {
|
||||
return {
|
||||
done: (callback: (response: object) => void) => {
|
||||
if (!options?.queryingRecurly) {
|
||||
return callback(response)
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user