mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	refactor: fix trial expiration calculations (#1090)
* refactor: fix trial expiration calculations * refactor: count full trial days for warning banner * refactor: fix flaky test
This commit is contained in:
		
							parent
							
								
									67a4f2e67f
								
							
						
					
					
						commit
						f46047f10a
					
				| @ -9,7 +9,7 @@ import { | ||||
|     InstanceState, | ||||
|     InstancePlan, | ||||
| } from 'interfaces/instance'; | ||||
| import { calculateTrialDaysRemaining } from 'utils/billing'; | ||||
| import { hasTrialExpired } 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 trialDaysRemaining = calculateTrialDaysRemaining(instanceStatus); | ||||
|     const trialHasExpired = hasTrialExpired(instanceStatus); | ||||
| 
 | ||||
|     const price = { | ||||
|         [InstancePlan.PRO]: 80, | ||||
| @ -91,11 +91,6 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => { | ||||
|         user: 15, | ||||
|     }; | ||||
| 
 | ||||
|     const statusExpired = | ||||
|         instanceStatus.state === InstanceState.TRIAL && | ||||
|         typeof trialDaysRemaining === 'number' && | ||||
|         trialDaysRemaining <= 0; | ||||
| 
 | ||||
|     const planPrice = price[instanceStatus.plan]; | ||||
|     const seats = instanceStatus.seats ?? 5; | ||||
|     const freeAssigned = Math.min(users.length, seats); | ||||
| @ -135,12 +130,12 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => { | ||||
|                                 show={ | ||||
|                                     <StyledTrialSpan | ||||
|                                         sx={theme => ({ | ||||
|                                             color: statusExpired | ||||
|                                             color: trialHasExpired | ||||
|                                                 ? theme.palette.error.dark | ||||
|                                                 : theme.palette.warning.dark, | ||||
|                                         })} | ||||
|                                     > | ||||
|                                         {statusExpired | ||||
|                                         {trialHasExpired | ||||
|                                             ? 'Trial expired' | ||||
|                                             : instanceStatus.trialExtended | ||||
|                                             ? 'Extended Trial' | ||||
|  | ||||
| @ -9,7 +9,7 @@ import { IInstanceStatus, InstanceState } from 'interfaces/instance'; | ||||
| import { ADMIN } from 'component/providers/AccessProvider/permissions'; | ||||
| import AccessContext from 'contexts/AccessContext'; | ||||
| import useInstanceStatusApi from 'hooks/api/actions/useInstanceStatusApi/useInstanceStatusApi'; | ||||
| import { calculateTrialDaysRemaining } from 'utils/billing'; | ||||
| import { hasTrialExpired } from 'utils/instanceTrial'; | ||||
| import useToast from 'hooks/useToast'; | ||||
| import { formatUnknownError } from 'utils/formatUnknownError'; | ||||
| 
 | ||||
| @ -24,22 +24,16 @@ const TrialDialog: VFC<ITrialDialogProps> = ({ | ||||
| }) => { | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     const navigate = useNavigate(); | ||||
|     const trialDaysRemaining = calculateTrialDaysRemaining(instanceStatus); | ||||
| 
 | ||||
|     const statusExpired = | ||||
|         instanceStatus.state === InstanceState.TRIAL && | ||||
|         typeof trialDaysRemaining === 'number' && | ||||
|         trialDaysRemaining <= 0; | ||||
| 
 | ||||
|     const [dialogOpen, setDialogOpen] = useState(statusExpired); | ||||
|     const trialHasExpired = hasTrialExpired(instanceStatus); | ||||
|     const [dialogOpen, setDialogOpen] = useState(trialHasExpired); | ||||
| 
 | ||||
|     useEffect(() => { | ||||
|         setDialogOpen(statusExpired); | ||||
|         setDialogOpen(trialHasExpired); | ||||
|         const interval = setInterval(() => { | ||||
|             setDialogOpen(statusExpired); | ||||
|             setDialogOpen(trialHasExpired); | ||||
|         }, 60000); | ||||
|         return () => clearInterval(interval); | ||||
|     }, [statusExpired]); | ||||
|     }, [trialHasExpired]); | ||||
| 
 | ||||
|     if (hasAccess(ADMIN)) { | ||||
|         return ( | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| import { styled, Button, Typography } from '@mui/material'; | ||||
| import { IInstanceStatus, InstanceState } from 'interfaces/instance'; | ||||
| import { IInstanceStatus } from 'interfaces/instance'; | ||||
| import { INSTANCE_STATUS_BAR_ID } from 'utils/testIds'; | ||||
| import { InfoOutlined, WarningAmber } from '@mui/icons-material'; | ||||
| import { useNavigate } from 'react-router-dom'; | ||||
| @ -7,7 +7,10 @@ import { useContext } from 'react'; | ||||
| import AccessContext from 'contexts/AccessContext'; | ||||
| import { ADMIN } from 'component/providers/AccessProvider/permissions'; | ||||
| import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; | ||||
| import { calculateTrialDaysRemaining } from 'utils/billing'; | ||||
| import { | ||||
|     hasTrialExpired, | ||||
|     formatTrialExpirationWarning, | ||||
| } from 'utils/instanceTrial'; | ||||
| 
 | ||||
| const StyledWarningBar = styled('aside')(({ theme }) => ({ | ||||
|     position: 'relative', | ||||
| @ -59,14 +62,10 @@ export const InstanceStatusBar = ({ | ||||
|     instanceStatus, | ||||
| }: IInstanceStatusBarProps) => { | ||||
|     const { hasAccess } = useContext(AccessContext); | ||||
|     const trialHasExpired = hasTrialExpired(instanceStatus); | ||||
|     const trialExpirationWarning = formatTrialExpirationWarning(instanceStatus); | ||||
| 
 | ||||
|     const trialDaysRemaining = calculateTrialDaysRemaining(instanceStatus); | ||||
| 
 | ||||
|     if ( | ||||
|         instanceStatus.state === InstanceState.TRIAL && | ||||
|         typeof trialDaysRemaining === 'number' && | ||||
|         trialDaysRemaining <= 0 | ||||
|     ) { | ||||
|     if (trialHasExpired) { | ||||
|         return ( | ||||
|             <StyledWarningBar data-testid={INSTANCE_STATUS_BAR_ID}> | ||||
|                 <StyledWarningIcon /> | ||||
| @ -87,11 +86,7 @@ export const InstanceStatusBar = ({ | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     if ( | ||||
|         instanceStatus.state === InstanceState.TRIAL && | ||||
|         typeof trialDaysRemaining === 'number' && | ||||
|         trialDaysRemaining <= 10 | ||||
|     ) { | ||||
|     if (trialExpirationWarning) { | ||||
|         return ( | ||||
|             <StyledInfoBar data-testid={INSTANCE_STATUS_BAR_ID}> | ||||
|                 <StyledInfoIcon /> | ||||
| @ -101,7 +96,7 @@ export const InstanceStatusBar = ({ | ||||
|                     })} | ||||
|                 > | ||||
|                     <strong>Heads up!</strong> You have{' '} | ||||
|                     <strong>{trialDaysRemaining} days</strong> left of your free{' '} | ||||
|                     <strong>{trialExpirationWarning}</strong> left of your free{' '} | ||||
|                     {instanceStatus.plan} trial. | ||||
|                 </Typography> | ||||
|                 <ConditionallyRender | ||||
|  | ||||
| @ -65,8 +65,7 @@ exports[`InstanceStatusBar should warn when the trial is about to expire 1`] = ` | ||||
|      You have | ||||
|       | ||||
|     <strong> | ||||
|       4 | ||||
|        days | ||||
|       4 days | ||||
|     </strong> | ||||
|      left of your free | ||||
|       | ||||
|  | ||||
| @ -1,10 +0,0 @@ | ||||
| import { differenceInDays, parseISO } from 'date-fns'; | ||||
| import { IInstanceStatus } from 'interfaces/instance'; | ||||
| 
 | ||||
| export const calculateTrialDaysRemaining = ( | ||||
|     instanceStatus?: IInstanceStatus | ||||
| ): number | undefined => { | ||||
|     return instanceStatus?.trialExpiry | ||||
|         ? differenceInDays(parseISO(instanceStatus.trialExpiry), new Date()) | ||||
|         : undefined; | ||||
| }; | ||||
							
								
								
									
										66
									
								
								frontend/src/utils/instanceTrial.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								frontend/src/utils/instanceTrial.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,66 @@ | ||||
| import { | ||||
|     hasTrialExpired, | ||||
|     formatTrialExpirationWarning, | ||||
| } from 'utils/instanceTrial'; | ||||
| import { InstancePlan, InstanceState } from 'interfaces/instance'; | ||||
| import { subHours, addHours, addMinutes, subMinutes } 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', () => { | ||||
|     expect( | ||||
|         hasTrialExpired({ | ||||
|             plan: InstancePlan.UNKNOWN, | ||||
|             state: InstanceState.TRIAL, | ||||
|             trialExpiry: subHours(new Date(), 2).toISOString(), | ||||
|         }) | ||||
|     ).toEqual(true); | ||||
|     expect( | ||||
|         hasTrialExpired({ | ||||
|             plan: InstancePlan.UNKNOWN, | ||||
|             state: InstanceState.TRIAL, | ||||
|             trialExpiry: addHours(new Date(), 2).toISOString(), | ||||
|         }) | ||||
|     ).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'); | ||||
| }); | ||||
							
								
								
									
										48
									
								
								frontend/src/utils/instanceTrial.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								frontend/src/utils/instanceTrial.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| import { parseISO, formatDistanceToNowStrict, isPast } from 'date-fns'; | ||||
| import { IInstanceStatus, InstanceState } from 'interfaces/instance'; | ||||
| import differenceInDays from 'date-fns/differenceInDays'; | ||||
| 
 | ||||
| const TRIAL_EXPIRATION_WARNING_DAYS_THRESHOLD = 10; | ||||
| 
 | ||||
| export const hasTrialExpired = ( | ||||
|     instanceStatus: IInstanceStatus | undefined | ||||
| ): boolean => { | ||||
|     const trialExpiry = parseTrialExpiryDate(instanceStatus); | ||||
| 
 | ||||
|     if (!trialExpiry) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     return isPast(trialExpiry); | ||||
| }; | ||||
| 
 | ||||
| export const formatTrialExpirationWarning = ( | ||||
|     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 ( | ||||
|         instanceStatus && | ||||
|         instanceStatus.state === InstanceState.TRIAL && | ||||
|         instanceStatus.trialExpiry | ||||
|     ) { | ||||
|         return parseISO(instanceStatus.trialExpiry); | ||||
|     } | ||||
| }; | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user