mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-17 13:46:47 +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,
|
InstanceState,
|
||||||
InstancePlan,
|
InstancePlan,
|
||||||
} from 'interfaces/instance';
|
} from 'interfaces/instance';
|
||||||
import { hasTrialExpired } from 'utils/instanceTrial';
|
import { trialHasExpired, isTrialInstance } from 'utils/instanceTrial';
|
||||||
import { GridRow } from 'component/common/GridRow/GridRow';
|
import { GridRow } from 'component/common/GridRow/GridRow';
|
||||||
import { GridCol } from 'component/common/GridCol/GridCol';
|
import { GridCol } from 'component/common/GridCol/GridCol';
|
||||||
import { GridColLink } from './GridColLink/GridColLink';
|
import { GridColLink } from './GridColLink/GridColLink';
|
||||||
@ -81,7 +81,7 @@ interface IBillingPlanProps {
|
|||||||
|
|
||||||
export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
const trialHasExpired = hasTrialExpired(instanceStatus);
|
const expired = trialHasExpired(instanceStatus);
|
||||||
|
|
||||||
const price = {
|
const price = {
|
||||||
[InstancePlan.PRO]: 80,
|
[InstancePlan.PRO]: 80,
|
||||||
@ -124,18 +124,16 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
|||||||
{instanceStatus.plan}
|
{instanceStatus.plan}
|
||||||
</StyledPlanSpan>
|
</StyledPlanSpan>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={
|
condition={isTrialInstance(instanceStatus)}
|
||||||
instanceStatus.state === InstanceState.TRIAL
|
|
||||||
}
|
|
||||||
show={
|
show={
|
||||||
<StyledTrialSpan
|
<StyledTrialSpan
|
||||||
sx={theme => ({
|
sx={theme => ({
|
||||||
color: trialHasExpired
|
color: expired
|
||||||
? theme.palette.error.dark
|
? theme.palette.error.dark
|
||||||
: theme.palette.warning.dark,
|
: theme.palette.warning.dark,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{trialHasExpired
|
{expired
|
||||||
? 'Trial expired'
|
? 'Trial expired'
|
||||||
: instanceStatus.trialExtended
|
: instanceStatus.trialExtended
|
||||||
? 'Extended Trial'
|
? 'Extended Trial'
|
||||||
|
@ -5,11 +5,11 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
|||||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||||
import { Typography } from '@mui/material';
|
import { Typography } from '@mui/material';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
import useInstanceStatusApi from 'hooks/api/actions/useInstanceStatusApi/useInstanceStatusApi';
|
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 useToast from 'hooks/useToast';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
|
||||||
@ -24,16 +24,16 @@ const TrialDialog: VFC<ITrialDialogProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const trialHasExpired = hasTrialExpired(instanceStatus);
|
const expired = trialHasExpired(instanceStatus);
|
||||||
const [dialogOpen, setDialogOpen] = useState(trialHasExpired);
|
const [dialogOpen, setDialogOpen] = useState(expired);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setDialogOpen(trialHasExpired);
|
setDialogOpen(expired);
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setDialogOpen(trialHasExpired);
|
setDialogOpen(expired);
|
||||||
}, 60000);
|
}, 60000);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [trialHasExpired]);
|
}, [expired]);
|
||||||
|
|
||||||
if (hasAccess(ADMIN)) {
|
if (hasAccess(ADMIN)) {
|
||||||
return (
|
return (
|
||||||
@ -41,9 +41,9 @@ const TrialDialog: VFC<ITrialDialogProps> = ({
|
|||||||
open={dialogOpen}
|
open={dialogOpen}
|
||||||
primaryButtonText="Upgrade trial"
|
primaryButtonText="Upgrade trial"
|
||||||
secondaryButtonText={
|
secondaryButtonText={
|
||||||
instanceStatus?.trialExtended
|
canExtendTrial(instanceStatus)
|
||||||
? 'Remind me later'
|
? 'Extend trial (5 days)'
|
||||||
: 'Extend trial (5 days)'
|
: 'Remind me later'
|
||||||
}
|
}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/admin/billing');
|
navigate('/admin/billing');
|
||||||
@ -92,16 +92,11 @@ export const InstanceStatus: FC = ({ children }) => {
|
|||||||
const { setToastApiError } = useToast();
|
const { setToastApiError } = useToast();
|
||||||
|
|
||||||
const onExtendTrial = async () => {
|
const onExtendTrial = async () => {
|
||||||
if (
|
try {
|
||||||
instanceStatus?.state === InstanceState.TRIAL &&
|
await extendTrial();
|
||||||
!instanceStatus?.trialExtended
|
await refetchInstanceStatus();
|
||||||
) {
|
} catch (error: unknown) {
|
||||||
try {
|
setToastApiError(formatUnknownError(error));
|
||||||
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 { InstancePlan, InstanceState } from 'interfaces/instance';
|
||||||
import { render } from 'utils/testRenderer';
|
import { render } from 'utils/testRenderer';
|
||||||
import { screen } from '@testing-library/react';
|
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 { INSTANCE_STATUS_BAR_ID } from 'utils/testIds';
|
||||||
import { UNKNOWN_INSTANCE_STATUS } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
import { UNKNOWN_INSTANCE_STATUS } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
||||||
|
|
||||||
@ -14,7 +14,22 @@ test('InstanceStatusBar should be hidden by default', async () => {
|
|||||||
).not.toBeInTheDocument();
|
).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(
|
render(
|
||||||
<InstanceStatusBar
|
<InstanceStatusBar
|
||||||
instanceStatus={{
|
instanceStatus={{
|
||||||
@ -25,9 +40,8 @@ test('InstanceStatusBar should be hidden when the trial is far from expired', as
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(
|
expect(screen.getByTestId(INSTANCE_STATUS_BAR_ID)).toBeInTheDocument();
|
||||||
screen.queryByTestId(INSTANCE_STATUS_BAR_ID)
|
expect(await screen.findByTestId(INSTANCE_STATUS_BAR_ID)).toMatchSnapshot();
|
||||||
).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('InstanceStatusBar should warn when the trial is about to expire', async () => {
|
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();
|
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(
|
render(
|
||||||
<InstanceStatusBar
|
<InstanceStatusBar
|
||||||
instanceStatus={{
|
instanceStatus={{
|
||||||
plan: InstancePlan.PRO,
|
plan: InstancePlan.PRO,
|
||||||
state: InstanceState.TRIAL,
|
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 { useContext } from 'react';
|
||||||
import AccessContext from 'contexts/AccessContext';
|
import AccessContext from 'contexts/AccessContext';
|
||||||
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import {
|
import {
|
||||||
hasTrialExpired,
|
trialHasExpired,
|
||||||
formatTrialExpirationWarning,
|
trialExpiresSoon,
|
||||||
|
isTrialInstance,
|
||||||
} from 'utils/instanceTrial';
|
} from 'utils/instanceTrial';
|
||||||
|
import { formatDistanceToNowStrict, parseISO } from 'date-fns';
|
||||||
|
|
||||||
const StyledWarningBar = styled('aside')(({ theme }) => ({
|
const StyledWarningBar = styled('aside')(({ theme }) => ({
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
@ -61,58 +62,75 @@ interface IInstanceStatusBarProps {
|
|||||||
export const InstanceStatusBar = ({
|
export const InstanceStatusBar = ({
|
||||||
instanceStatus,
|
instanceStatus,
|
||||||
}: IInstanceStatusBarProps) => {
|
}: IInstanceStatusBarProps) => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
if (trialHasExpired(instanceStatus)) {
|
||||||
const trialHasExpired = hasTrialExpired(instanceStatus);
|
return <StatusBarExpired instanceStatus={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 (trialExpirationWarning) {
|
if (trialExpiresSoon(instanceStatus)) {
|
||||||
return (
|
return <StatusBarExpiresSoon instanceStatus={instanceStatus} />;
|
||||||
<StyledInfoBar data-testid={INSTANCE_STATUS_BAR_ID}>
|
}
|
||||||
<StyledInfoIcon />
|
|
||||||
<Typography
|
if (isTrialInstance(instanceStatus)) {
|
||||||
sx={theme => ({
|
return <StatusBarExpiresLater instanceStatus={instanceStatus} />;
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
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();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
if (!hasAccess(ADMIN)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledButton
|
<StyledButton
|
||||||
onClick={() => navigate('/admin/billing')}
|
onClick={() => navigate('/admin/billing')}
|
||||||
|
@ -1,5 +1,45 @@
|
|||||||
// Vitest Snapshot v1
|
// 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`] = `
|
exports[`InstanceStatusBar should warn when the trial has expired 1`] = `
|
||||||
<aside
|
<aside
|
||||||
class="mui-jmsogz"
|
class="mui-jmsogz"
|
||||||
@ -27,12 +67,12 @@ exports[`InstanceStatusBar should warn when the trial has expired 1`] = `
|
|||||||
</strong>
|
</strong>
|
||||||
Your free
|
Your free
|
||||||
Pro
|
Pro
|
||||||
|
trial has expired.
|
||||||
trial has expired.
|
|
||||||
<strong>
|
<strong>
|
||||||
Upgrade trial
|
Upgrade trial
|
||||||
</strong>
|
</strong>
|
||||||
otherwise your
|
otherwise your
|
||||||
|
|
||||||
<strong>
|
<strong>
|
||||||
account will be deleted.
|
account will be deleted.
|
||||||
</strong>
|
</strong>
|
||||||
@ -74,3 +114,73 @@ exports[`InstanceStatusBar should warn when the trial is about to expire 1`] = `
|
|||||||
</p>
|
</p>
|
||||||
</aside>
|
</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 {
|
import {
|
||||||
hasTrialExpired,
|
trialHasExpired,
|
||||||
formatTrialExpirationWarning,
|
canExtendTrial,
|
||||||
|
trialExpiresSoon,
|
||||||
} from 'utils/instanceTrial';
|
} from 'utils/instanceTrial';
|
||||||
import { InstancePlan, InstanceState } from 'interfaces/instance';
|
import { InstancePlan, InstanceState } from 'interfaces/instance';
|
||||||
import { subHours, addHours, addMinutes, subMinutes } from 'date-fns';
|
import { subHours, addHours, addDays } from 'date-fns';
|
||||||
|
|
||||||
test.each([
|
test('trialHasExpired', () => {
|
||||||
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', () => {
|
|
||||||
expect(
|
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,
|
plan: InstancePlan.UNKNOWN,
|
||||||
state: InstanceState.TRIAL,
|
state: InstanceState.TRIAL,
|
||||||
trialExpiry: subHours(new Date(), 2).toISOString(),
|
trialExpiry: subHours(new Date(), 2).toISOString(),
|
||||||
})
|
})
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
expect(
|
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,
|
plan: InstancePlan.UNKNOWN,
|
||||||
state: InstanceState.TRIAL,
|
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);
|
).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 { IInstanceStatus, InstanceState } from 'interfaces/instance';
|
||||||
import differenceInDays from 'date-fns/differenceInDays';
|
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
|
instanceStatus: IInstanceStatus | undefined
|
||||||
): boolean => {
|
): 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 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
|
instanceStatus: IInstanceStatus | undefined
|
||||||
): string | undefined => {
|
): boolean => {
|
||||||
const trialExpiry = parseTrialExpiryDate(instanceStatus);
|
return Boolean(
|
||||||
|
|
||||||
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 (
|
|
||||||
instanceStatus &&
|
instanceStatus &&
|
||||||
instanceStatus.state === InstanceState.TRIAL &&
|
instanceStatus.state === InstanceState.EXPIRED &&
|
||||||
instanceStatus.trialExpiry
|
!instanceStatus.trialExtended
|
||||||
) {
|
);
|
||||||
return parseISO(instanceStatus.trialExpiry);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user