mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-26 01:17:00 +02:00
refactor: fix handling of expired/churned trial states (#1107)
This commit is contained in:
parent
9522c59674
commit
583d636144
@ -9,7 +9,7 @@ import {
|
||||
InstanceState,
|
||||
InstancePlan,
|
||||
} from 'interfaces/instance';
|
||||
import { hasTrialExpired } from 'utils/instanceTrial';
|
||||
import { trialHasExpired, isTrialInstance } from 'utils/instanceTrial';
|
||||
import { GridRow } from 'component/common/GridRow/GridRow';
|
||||
import { GridCol } from 'component/common/GridCol/GridCol';
|
||||
import { GridColLink } from './GridColLink/GridColLink';
|
||||
@ -81,7 +81,7 @@ interface IBillingPlanProps {
|
||||
|
||||
export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
||||
const { users } = useUsers();
|
||||
const trialHasExpired = hasTrialExpired(instanceStatus);
|
||||
const expired = trialHasExpired(instanceStatus);
|
||||
|
||||
const price = {
|
||||
[InstancePlan.PRO]: 80,
|
||||
@ -124,18 +124,16 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
||||
{instanceStatus.plan}
|
||||
</StyledPlanSpan>
|
||||
<ConditionallyRender
|
||||
condition={
|
||||
instanceStatus.state === InstanceState.TRIAL
|
||||
}
|
||||
condition={isTrialInstance(instanceStatus)}
|
||||
show={
|
||||
<StyledTrialSpan
|
||||
sx={theme => ({
|
||||
color: trialHasExpired
|
||||
color: expired
|
||||
? theme.palette.error.dark
|
||||
: theme.palette.warning.dark,
|
||||
})}
|
||||
>
|
||||
{trialHasExpired
|
||||
{expired
|
||||
? 'Trial expired'
|
||||
: instanceStatus.trialExtended
|
||||
? 'Extended Trial'
|
||||
|
@ -5,11 +5,11 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import { Typography } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { IInstanceStatus, InstanceState } from 'interfaces/instance';
|
||||
import { IInstanceStatus } from 'interfaces/instance';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import AccessContext from 'contexts/AccessContext';
|
||||
import useInstanceStatusApi from 'hooks/api/actions/useInstanceStatusApi/useInstanceStatusApi';
|
||||
import { hasTrialExpired } from 'utils/instanceTrial';
|
||||
import { trialHasExpired, canExtendTrial } from 'utils/instanceTrial';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
|
||||
@ -24,16 +24,16 @@ const TrialDialog: VFC<ITrialDialogProps> = ({
|
||||
}) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const navigate = useNavigate();
|
||||
const trialHasExpired = hasTrialExpired(instanceStatus);
|
||||
const [dialogOpen, setDialogOpen] = useState(trialHasExpired);
|
||||
const expired = trialHasExpired(instanceStatus);
|
||||
const [dialogOpen, setDialogOpen] = useState(expired);
|
||||
|
||||
useEffect(() => {
|
||||
setDialogOpen(trialHasExpired);
|
||||
setDialogOpen(expired);
|
||||
const interval = setInterval(() => {
|
||||
setDialogOpen(trialHasExpired);
|
||||
setDialogOpen(expired);
|
||||
}, 60000);
|
||||
return () => clearInterval(interval);
|
||||
}, [trialHasExpired]);
|
||||
}, [expired]);
|
||||
|
||||
if (hasAccess(ADMIN)) {
|
||||
return (
|
||||
@ -41,9 +41,9 @@ const TrialDialog: VFC<ITrialDialogProps> = ({
|
||||
open={dialogOpen}
|
||||
primaryButtonText="Upgrade trial"
|
||||
secondaryButtonText={
|
||||
instanceStatus?.trialExtended
|
||||
? 'Remind me later'
|
||||
: 'Extend trial (5 days)'
|
||||
canExtendTrial(instanceStatus)
|
||||
? 'Extend trial (5 days)'
|
||||
: 'Remind me later'
|
||||
}
|
||||
onClick={() => {
|
||||
navigate('/admin/billing');
|
||||
@ -92,16 +92,11 @@ export const InstanceStatus: FC = ({ children }) => {
|
||||
const { setToastApiError } = useToast();
|
||||
|
||||
const onExtendTrial = async () => {
|
||||
if (
|
||||
instanceStatus?.state === InstanceState.TRIAL &&
|
||||
!instanceStatus?.trialExtended
|
||||
) {
|
||||
try {
|
||||
await extendTrial();
|
||||
await refetchInstanceStatus();
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
try {
|
||||
await extendTrial();
|
||||
await refetchInstanceStatus();
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -2,7 +2,7 @@ import { InstanceStatusBar } from 'component/common/InstanceStatus/InstanceStatu
|
||||
import { InstancePlan, InstanceState } from 'interfaces/instance';
|
||||
import { render } from 'utils/testRenderer';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { addDays } from 'date-fns';
|
||||
import { addDays, subDays } from 'date-fns';
|
||||
import { INSTANCE_STATUS_BAR_ID } from 'utils/testIds';
|
||||
import { UNKNOWN_INSTANCE_STATUS } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
||||
|
||||
@ -14,7 +14,22 @@ test('InstanceStatusBar should be hidden by default', async () => {
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('InstanceStatusBar should be hidden when the trial is far from expired', async () => {
|
||||
test('InstanceStatusBar should be hidden when state is active', async () => {
|
||||
render(
|
||||
<InstanceStatusBar
|
||||
instanceStatus={{
|
||||
plan: InstancePlan.PRO,
|
||||
state: InstanceState.ACTIVE,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId(INSTANCE_STATUS_BAR_ID)
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('InstanceStatusBar should warn when the trial is far from expired', async () => {
|
||||
render(
|
||||
<InstanceStatusBar
|
||||
instanceStatus={{
|
||||
@ -25,9 +40,8 @@ test('InstanceStatusBar should be hidden when the trial is far from expired', as
|
||||
/>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen.queryByTestId(INSTANCE_STATUS_BAR_ID)
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByTestId(INSTANCE_STATUS_BAR_ID)).toBeInTheDocument();
|
||||
expect(await screen.findByTestId(INSTANCE_STATUS_BAR_ID)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('InstanceStatusBar should warn when the trial is about to expire', async () => {
|
||||
@ -45,13 +59,41 @@ test('InstanceStatusBar should warn when the trial is about to expire', async ()
|
||||
expect(await screen.findByTestId(INSTANCE_STATUS_BAR_ID)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('InstanceStatusBar should warn when the trial has expired', async () => {
|
||||
test('InstanceStatusBar should warn when trialExpiry has passed', async () => {
|
||||
render(
|
||||
<InstanceStatusBar
|
||||
instanceStatus={{
|
||||
plan: InstancePlan.PRO,
|
||||
state: InstanceState.TRIAL,
|
||||
trialExpiry: new Date().toISOString(),
|
||||
trialExpiry: subDays(new Date(), 1).toISOString(),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId(INSTANCE_STATUS_BAR_ID)).toBeInTheDocument();
|
||||
expect(await screen.findByTestId(INSTANCE_STATUS_BAR_ID)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('InstanceStatusBar should warn when the trial has expired', async () => {
|
||||
render(
|
||||
<InstanceStatusBar
|
||||
instanceStatus={{
|
||||
plan: InstancePlan.PRO,
|
||||
state: InstanceState.EXPIRED,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.getByTestId(INSTANCE_STATUS_BAR_ID)).toBeInTheDocument();
|
||||
expect(await screen.findByTestId(INSTANCE_STATUS_BAR_ID)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('InstanceStatusBar should warn when the trial has churned', async () => {
|
||||
render(
|
||||
<InstanceStatusBar
|
||||
instanceStatus={{
|
||||
plan: InstancePlan.PRO,
|
||||
state: InstanceState.CHURNED,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -6,11 +6,12 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useContext } from 'react';
|
||||
import AccessContext from 'contexts/AccessContext';
|
||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import {
|
||||
hasTrialExpired,
|
||||
formatTrialExpirationWarning,
|
||||
trialHasExpired,
|
||||
trialExpiresSoon,
|
||||
isTrialInstance,
|
||||
} from 'utils/instanceTrial';
|
||||
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
|
||||
|
||||
const StyledWarningBar = styled('aside')(({ theme }) => ({
|
||||
position: 'relative',
|
||||
@ -61,58 +62,75 @@ interface IInstanceStatusBarProps {
|
||||
export const InstanceStatusBar = ({
|
||||
instanceStatus,
|
||||
}: IInstanceStatusBarProps) => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const trialHasExpired = hasTrialExpired(instanceStatus);
|
||||
const trialExpirationWarning = formatTrialExpirationWarning(instanceStatus);
|
||||
|
||||
if (trialHasExpired) {
|
||||
return (
|
||||
<StyledWarningBar data-testid={INSTANCE_STATUS_BAR_ID}>
|
||||
<StyledWarningIcon />
|
||||
<Typography
|
||||
sx={theme => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
})}
|
||||
>
|
||||
<strong>Warning!</strong> Your free {instanceStatus.plan}{' '}
|
||||
trial has expired. <strong>Upgrade trial</strong> otherwise
|
||||
your <strong>account will be deleted.</strong>
|
||||
</Typography>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(ADMIN)}
|
||||
show={<UpgradeButton />}
|
||||
/>
|
||||
</StyledWarningBar>
|
||||
);
|
||||
if (trialHasExpired(instanceStatus)) {
|
||||
return <StatusBarExpired instanceStatus={instanceStatus} />;
|
||||
}
|
||||
|
||||
if (trialExpirationWarning) {
|
||||
return (
|
||||
<StyledInfoBar data-testid={INSTANCE_STATUS_BAR_ID}>
|
||||
<StyledInfoIcon />
|
||||
<Typography
|
||||
sx={theme => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
})}
|
||||
>
|
||||
<strong>Heads up!</strong> You have{' '}
|
||||
<strong>{trialExpirationWarning}</strong> left of your free{' '}
|
||||
{instanceStatus.plan} trial.
|
||||
</Typography>
|
||||
<ConditionallyRender
|
||||
condition={hasAccess(ADMIN)}
|
||||
show={<UpgradeButton />}
|
||||
/>
|
||||
</StyledInfoBar>
|
||||
);
|
||||
if (trialExpiresSoon(instanceStatus)) {
|
||||
return <StatusBarExpiresSoon instanceStatus={instanceStatus} />;
|
||||
}
|
||||
|
||||
if (isTrialInstance(instanceStatus)) {
|
||||
return <StatusBarExpiresLater instanceStatus={instanceStatus} />;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const UpgradeButton = () => {
|
||||
const StatusBarExpired = ({ instanceStatus }: IInstanceStatusBarProps) => {
|
||||
return (
|
||||
<StyledWarningBar data-testid={INSTANCE_STATUS_BAR_ID}>
|
||||
<StyledWarningIcon />
|
||||
<Typography sx={theme => ({ fontSize: theme.fontSizes.smallBody })}>
|
||||
<strong>Warning!</strong> Your free {instanceStatus.plan} trial
|
||||
has expired. <strong>Upgrade trial</strong> otherwise your{' '}
|
||||
<strong>account will be deleted.</strong>
|
||||
</Typography>
|
||||
<BillingLink />
|
||||
</StyledWarningBar>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusBarExpiresSoon = ({ instanceStatus }: IInstanceStatusBarProps) => {
|
||||
const timeRemaining = formatDistanceToNowStrict(
|
||||
parseISO(instanceStatus.trialExpiry!),
|
||||
{ roundingMethod: 'floor' }
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledInfoBar data-testid={INSTANCE_STATUS_BAR_ID}>
|
||||
<StyledInfoIcon />
|
||||
<Typography sx={theme => ({ fontSize: theme.fontSizes.smallBody })}>
|
||||
<strong>Heads up!</strong> You have{' '}
|
||||
<strong>{timeRemaining}</strong> left of your free{' '}
|
||||
{instanceStatus.plan} trial.
|
||||
</Typography>
|
||||
<BillingLink />
|
||||
</StyledInfoBar>
|
||||
);
|
||||
};
|
||||
|
||||
const StatusBarExpiresLater = ({ instanceStatus }: IInstanceStatusBarProps) => {
|
||||
return (
|
||||
<StyledInfoBar data-testid={INSTANCE_STATUS_BAR_ID}>
|
||||
<StyledInfoIcon />
|
||||
<Typography sx={theme => ({ fontSize: theme.fontSizes.smallBody })}>
|
||||
<strong>Heads up!</strong> You're currently on a free{' '}
|
||||
{instanceStatus.plan} trial account.
|
||||
</Typography>
|
||||
<BillingLink />
|
||||
</StyledInfoBar>
|
||||
);
|
||||
};
|
||||
|
||||
const BillingLink = () => {
|
||||
const { hasAccess } = useContext(AccessContext);
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (!hasAccess(ADMIN)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledButton
|
||||
onClick={() => navigate('/admin/billing')}
|
||||
|
@ -1,5 +1,45 @@
|
||||
// Vitest Snapshot v1
|
||||
|
||||
exports[`InstanceStatusBar should warn when the trial has churned 1`] = `
|
||||
<aside
|
||||
class="mui-jmsogz"
|
||||
data-testid="INSTANCE_STATUS_BAR_ID"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium mui-prk1jy-MuiSvgIcon-root"
|
||||
data-testid="WarningAmberIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 5.99 19.53 19H4.47L12 5.99M12 2 1 21h22L12 2z"
|
||||
/>
|
||||
<path
|
||||
d="M13 16h-2v2h2zm0-6h-2v5h2z"
|
||||
/>
|
||||
</svg>
|
||||
<p
|
||||
class="MuiTypography-root MuiTypography-body1 mui-rviqjc-MuiTypography-root"
|
||||
>
|
||||
<strong>
|
||||
Warning!
|
||||
</strong>
|
||||
Your free
|
||||
Pro
|
||||
trial has expired.
|
||||
<strong>
|
||||
Upgrade trial
|
||||
</strong>
|
||||
otherwise your
|
||||
|
||||
<strong>
|
||||
account will be deleted.
|
||||
</strong>
|
||||
</p>
|
||||
</aside>
|
||||
`;
|
||||
|
||||
exports[`InstanceStatusBar should warn when the trial has expired 1`] = `
|
||||
<aside
|
||||
class="mui-jmsogz"
|
||||
@ -27,12 +67,12 @@ exports[`InstanceStatusBar should warn when the trial has expired 1`] = `
|
||||
</strong>
|
||||
Your free
|
||||
Pro
|
||||
|
||||
trial has expired.
|
||||
trial has expired.
|
||||
<strong>
|
||||
Upgrade trial
|
||||
</strong>
|
||||
otherwise your
|
||||
otherwise your
|
||||
|
||||
<strong>
|
||||
account will be deleted.
|
||||
</strong>
|
||||
@ -74,3 +114,73 @@ exports[`InstanceStatusBar should warn when the trial is about to expire 1`] = `
|
||||
</p>
|
||||
</aside>
|
||||
`;
|
||||
|
||||
exports[`InstanceStatusBar should warn when the trial is far from expired 1`] = `
|
||||
<aside
|
||||
class="mui-yx2rkt"
|
||||
data-testid="INSTANCE_STATUS_BAR_ID"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium mui-cle2im-MuiSvgIcon-root"
|
||||
data-testid="InfoOutlinedIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M11 7h2v2h-2zm0 4h2v6h-2zm1-9C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z"
|
||||
/>
|
||||
</svg>
|
||||
<p
|
||||
class="MuiTypography-root MuiTypography-body1 mui-rviqjc-MuiTypography-root"
|
||||
>
|
||||
<strong>
|
||||
Heads up!
|
||||
</strong>
|
||||
You're currently on a free
|
||||
|
||||
Pro
|
||||
trial account.
|
||||
</p>
|
||||
</aside>
|
||||
`;
|
||||
|
||||
exports[`InstanceStatusBar should warn when trialExpiry has passed 1`] = `
|
||||
<aside
|
||||
class="mui-jmsogz"
|
||||
data-testid="INSTANCE_STATUS_BAR_ID"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium mui-prk1jy-MuiSvgIcon-root"
|
||||
data-testid="WarningAmberIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="M12 5.99 19.53 19H4.47L12 5.99M12 2 1 21h22L12 2z"
|
||||
/>
|
||||
<path
|
||||
d="M13 16h-2v2h2zm0-6h-2v5h2z"
|
||||
/>
|
||||
</svg>
|
||||
<p
|
||||
class="MuiTypography-root MuiTypography-body1 mui-rviqjc-MuiTypography-root"
|
||||
>
|
||||
<strong>
|
||||
Warning!
|
||||
</strong>
|
||||
Your free
|
||||
Pro
|
||||
trial has expired.
|
||||
<strong>
|
||||
Upgrade trial
|
||||
</strong>
|
||||
otherwise your
|
||||
|
||||
<strong>
|
||||
account will be deleted.
|
||||
</strong>
|
||||
</p>
|
||||
</aside>
|
||||
`;
|
||||
|
@ -1,66 +1,81 @@
|
||||
import {
|
||||
hasTrialExpired,
|
||||
formatTrialExpirationWarning,
|
||||
trialHasExpired,
|
||||
canExtendTrial,
|
||||
trialExpiresSoon,
|
||||
} from 'utils/instanceTrial';
|
||||
import { InstancePlan, InstanceState } from 'interfaces/instance';
|
||||
import { subHours, addHours, addMinutes, subMinutes } from 'date-fns';
|
||||
import { subHours, addHours, addDays } from 'date-fns';
|
||||
|
||||
test.each([
|
||||
undefined,
|
||||
{ plan: InstancePlan.UNKNOWN },
|
||||
{ plan: InstancePlan.UNKNOWN, state: InstanceState.ACTIVE },
|
||||
{ plan: InstancePlan.UNKNOWN, state: InstanceState.TRIAL },
|
||||
{ plan: InstancePlan.COMPANY, state: InstanceState.TRIAL },
|
||||
{ plan: InstancePlan.PRO, state: InstanceState.TRIAL },
|
||||
])('unknown trial states should not count as expired', input => {
|
||||
expect(hasTrialExpired(input)).toEqual(false);
|
||||
expect(formatTrialExpirationWarning(input)).toEqual(undefined);
|
||||
});
|
||||
|
||||
test('hasTrialExpired', () => {
|
||||
test('trialHasExpired', () => {
|
||||
expect(
|
||||
hasTrialExpired({
|
||||
trialHasExpired({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.UNASSIGNED,
|
||||
})
|
||||
).toEqual(false);
|
||||
expect(
|
||||
trialHasExpired({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.ACTIVE,
|
||||
})
|
||||
).toEqual(false);
|
||||
expect(
|
||||
trialHasExpired({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.TRIAL,
|
||||
trialExpiry: addHours(new Date(), 2).toISOString(),
|
||||
})
|
||||
).toEqual(false);
|
||||
expect(
|
||||
trialHasExpired({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.TRIAL,
|
||||
trialExpiry: subHours(new Date(), 2).toISOString(),
|
||||
})
|
||||
).toEqual(true);
|
||||
expect(
|
||||
hasTrialExpired({
|
||||
trialHasExpired({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.EXPIRED,
|
||||
})
|
||||
).toEqual(true);
|
||||
expect(
|
||||
trialHasExpired({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.CHURNED,
|
||||
})
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('trialExpiresSoon', () => {
|
||||
expect(
|
||||
trialExpiresSoon({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.TRIAL,
|
||||
trialExpiry: addHours(new Date(), 2).toISOString(),
|
||||
trialExpiry: addDays(new Date(), 12).toISOString(),
|
||||
})
|
||||
).toEqual(false);
|
||||
expect(
|
||||
trialExpiresSoon({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.TRIAL,
|
||||
trialExpiry: addDays(new Date(), 8).toISOString(),
|
||||
})
|
||||
).toEqual(true);
|
||||
});
|
||||
|
||||
test('canExtendTrial', () => {
|
||||
expect(
|
||||
canExtendTrial({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.EXPIRED,
|
||||
})
|
||||
).toEqual(true);
|
||||
expect(
|
||||
canExtendTrial({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.EXPIRED,
|
||||
trialExtended: 1,
|
||||
})
|
||||
).toEqual(false);
|
||||
});
|
||||
|
||||
test('formatTrialExpirationWarning', () => {
|
||||
expect(
|
||||
formatTrialExpirationWarning({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.TRIAL,
|
||||
trialExpiry: subMinutes(new Date(), 1).toISOString(),
|
||||
})
|
||||
).toEqual(undefined);
|
||||
expect(
|
||||
formatTrialExpirationWarning({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.TRIAL,
|
||||
trialExpiry: addMinutes(new Date(), 23 * 60 + 1).toISOString(),
|
||||
})
|
||||
).toEqual('23 hours');
|
||||
expect(
|
||||
formatTrialExpirationWarning({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.TRIAL,
|
||||
trialExpiry: addHours(new Date(), 25).toISOString(),
|
||||
})
|
||||
).toEqual('1 day');
|
||||
expect(
|
||||
formatTrialExpirationWarning({
|
||||
plan: InstancePlan.UNKNOWN,
|
||||
state: InstanceState.TRIAL,
|
||||
trialExpiry: addHours(new Date(), 24 * 11 - 1).toISOString(),
|
||||
})
|
||||
).toEqual('10 days');
|
||||
});
|
||||
|
@ -1,48 +1,62 @@
|
||||
import { parseISO, formatDistanceToNowStrict, isPast } from 'date-fns';
|
||||
import { parseISO, isPast } from 'date-fns';
|
||||
import { IInstanceStatus, InstanceState } from 'interfaces/instance';
|
||||
import differenceInDays from 'date-fns/differenceInDays';
|
||||
|
||||
const TRIAL_EXPIRATION_WARNING_DAYS_THRESHOLD = 10;
|
||||
const TRIAL_EXPIRES_SOON_DAYS_THRESHOLD = 10;
|
||||
|
||||
export const hasTrialExpired = (
|
||||
export const isTrialInstance = (
|
||||
instanceStatus: IInstanceStatus | undefined
|
||||
): boolean => {
|
||||
const trialExpiry = parseTrialExpiryDate(instanceStatus);
|
||||
return (
|
||||
instanceStatus?.state === InstanceState.TRIAL ||
|
||||
instanceStatus?.state === InstanceState.EXPIRED ||
|
||||
instanceStatus?.state === InstanceState.CHURNED
|
||||
);
|
||||
};
|
||||
|
||||
if (!trialExpiry) {
|
||||
export const trialHasExpired = (
|
||||
instanceStatus: IInstanceStatus | undefined
|
||||
): boolean => {
|
||||
if (
|
||||
instanceStatus?.state === InstanceState.EXPIRED ||
|
||||
instanceStatus?.state === InstanceState.CHURNED
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
instanceStatus?.state === InstanceState.TRIAL &&
|
||||
instanceStatus?.trialExpiry
|
||||
) {
|
||||
return isPast(parseISO(instanceStatus.trialExpiry));
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
export const trialExpiresSoon = (
|
||||
instanceStatus: IInstanceStatus | undefined
|
||||
) => {
|
||||
if (
|
||||
!instanceStatus ||
|
||||
instanceStatus.state !== InstanceState.TRIAL ||
|
||||
!instanceStatus.trialExpiry
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isPast(trialExpiry);
|
||||
return (
|
||||
differenceInDays(parseISO(instanceStatus.trialExpiry), new Date()) <=
|
||||
TRIAL_EXPIRES_SOON_DAYS_THRESHOLD
|
||||
);
|
||||
};
|
||||
|
||||
export const formatTrialExpirationWarning = (
|
||||
export const canExtendTrial = (
|
||||
instanceStatus: IInstanceStatus | undefined
|
||||
): string | undefined => {
|
||||
const trialExpiry = parseTrialExpiryDate(instanceStatus);
|
||||
|
||||
if (!trialExpiry || isPast(trialExpiry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
differenceInDays(trialExpiry, new Date()) <=
|
||||
TRIAL_EXPIRATION_WARNING_DAYS_THRESHOLD
|
||||
) {
|
||||
return formatDistanceToNowStrict(trialExpiry, {
|
||||
roundingMethod: 'floor',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const parseTrialExpiryDate = (
|
||||
instanceStatus: IInstanceStatus | undefined
|
||||
): Date | undefined => {
|
||||
if (
|
||||
): boolean => {
|
||||
return Boolean(
|
||||
instanceStatus &&
|
||||
instanceStatus.state === InstanceState.TRIAL &&
|
||||
instanceStatus.trialExpiry
|
||||
) {
|
||||
return parseISO(instanceStatus.trialExpiry);
|
||||
}
|
||||
instanceStatus.state === InstanceState.EXPIRED &&
|
||||
!instanceStatus.trialExtended
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user