mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-04 13:48:56 +02:00
chore: PAYG billing (#8743)
https://linear.app/unleash/issue/CTO-95/unleash-billing-page-for-enterprise-payg Adds support for PAYG in Unleash's billing page. Includes some refactoring, like splitting Pro and PAYG into different details components. We're now also relying on shared billing-related constants (see `BillingPlan.tsx`). This should make it much easier to change any of these values in the future. I already changed a few that were static / wrongly relying on instanceStatus.seats (we decided we're not doing that for now). 
This commit is contained in:
parent
54444a395c
commit
395a4b6be3
@ -10,13 +10,8 @@ import { BillingHistory } from './BillingHistory/BillingHistory';
|
|||||||
import useInvoices from 'hooks/api/getters/useInvoices/useInvoices';
|
import useInvoices from 'hooks/api/getters/useInvoices/useInvoices';
|
||||||
|
|
||||||
export const Billing = () => {
|
export const Billing = () => {
|
||||||
const {
|
const { isBilling, refetchInstanceStatus, refresh, loading } =
|
||||||
instanceStatus,
|
useInstanceStatus();
|
||||||
isBilling,
|
|
||||||
refetchInstanceStatus,
|
|
||||||
refresh,
|
|
||||||
loading,
|
|
||||||
} = useInstanceStatus();
|
|
||||||
const { invoices } = useInvoices();
|
const { invoices } = useInvoices();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -35,9 +30,7 @@ export const Billing = () => {
|
|||||||
show={
|
show={
|
||||||
<PermissionGuard permissions={ADMIN}>
|
<PermissionGuard permissions={ADMIN}>
|
||||||
<>
|
<>
|
||||||
<BillingDashboard
|
<BillingDashboard />
|
||||||
instanceStatus={instanceStatus!}
|
|
||||||
/>
|
|
||||||
<BillingHistory data={invoices} />
|
<BillingHistory data={invoices} />
|
||||||
</>
|
</>
|
||||||
</PermissionGuard>
|
</PermissionGuard>
|
||||||
|
@ -1,20 +1,12 @@
|
|||||||
import { Grid } from '@mui/material';
|
import { Grid } from '@mui/material';
|
||||||
import type { IInstanceStatus } from 'interfaces/instance';
|
|
||||||
import type { VFC } from 'react';
|
|
||||||
import { BillingInformation } from './BillingInformation/BillingInformation';
|
import { BillingInformation } from './BillingInformation/BillingInformation';
|
||||||
import { BillingPlan } from './BillingPlan/BillingPlan';
|
import { BillingPlan } from './BillingPlan/BillingPlan';
|
||||||
|
|
||||||
interface IBillingDashboardProps {
|
export const BillingDashboard = () => {
|
||||||
instanceStatus: IInstanceStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BillingDashboard: VFC<IBillingDashboardProps> = ({
|
|
||||||
instanceStatus,
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={4}>
|
<Grid container spacing={4}>
|
||||||
<BillingInformation instanceStatus={instanceStatus} />
|
<BillingInformation />
|
||||||
<BillingPlan instanceStatus={instanceStatus} />
|
<BillingPlan />
|
||||||
</Grid>
|
</Grid>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import type { FC } from 'react';
|
|
||||||
import { Alert, Divider, Grid, styled, Typography } from '@mui/material';
|
import { Alert, Divider, Grid, styled, Typography } from '@mui/material';
|
||||||
import { BillingInformationButton } from './BillingInformationButton/BillingInformationButton';
|
import { BillingInformationButton } from './BillingInformationButton/BillingInformationButton';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { type IInstanceStatus, InstanceState } from 'interfaces/instance';
|
import { InstanceState } from 'interfaces/instance';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
||||||
|
|
||||||
const StyledInfoBox = styled('aside')(({ theme }) => ({
|
const StyledInfoBox = styled('aside')(({ theme }) => ({
|
||||||
padding: theme.spacing(4),
|
padding: theme.spacing(4),
|
||||||
@ -28,13 +29,22 @@ const StyledDivider = styled(Divider)(({ theme }) => ({
|
|||||||
margin: `${theme.spacing(2.5)} 0`,
|
margin: `${theme.spacing(2.5)} 0`,
|
||||||
borderColor: theme.palette.divider,
|
borderColor: theme.palette.divider,
|
||||||
}));
|
}));
|
||||||
interface IBillingInformationProps {
|
|
||||||
instanceStatus: IInstanceStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BillingInformation: FC<IBillingInformationProps> = ({
|
export const BillingInformation = () => {
|
||||||
instanceStatus,
|
const { instanceStatus } = useInstanceStatus();
|
||||||
}) => {
|
const {
|
||||||
|
uiConfig: { billing },
|
||||||
|
} = useUiConfig();
|
||||||
|
const isPAYG = billing === 'pay-as-you-go';
|
||||||
|
|
||||||
|
if (!instanceStatus)
|
||||||
|
return (
|
||||||
|
<Grid item xs={12} md={5}>
|
||||||
|
<StyledInfoBox data-loading sx={{ flex: 1, height: '400px' }} />
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
|
||||||
|
const plan = `${instanceStatus.plan}${isPAYG ? ' Pay-as-You-Go' : ''}`;
|
||||||
const inactive = instanceStatus.state !== InstanceState.ACTIVE;
|
const inactive = instanceStatus.state !== InstanceState.ACTIVE;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -58,7 +68,9 @@ export const BillingInformation: FC<IBillingInformationProps> = ({
|
|||||||
</StyledInfoLabel>
|
</StyledInfoLabel>
|
||||||
<StyledDivider />
|
<StyledDivider />
|
||||||
<StyledInfoLabel>
|
<StyledInfoLabel>
|
||||||
<a href='mailto:support@getunleash.io?subject=PRO plan clarifications'>
|
<a
|
||||||
|
href={`mailto:support@getunleash.io?subject=${plan} plan clarifications`}
|
||||||
|
>
|
||||||
Get in touch with us
|
Get in touch with us
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
for any clarification
|
for any clarification
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
import { type IInstanceStatus, InstancePlan } from 'interfaces/instance';
|
||||||
|
import { BillingDetailsPro } from './BillingDetailsPro';
|
||||||
|
import { BillingDetailsPAYG } from './BillingDetailsPAYG';
|
||||||
|
|
||||||
|
interface IBillingDetailsProps {
|
||||||
|
instanceStatus: IInstanceStatus;
|
||||||
|
isPAYG: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BillingDetails = ({
|
||||||
|
instanceStatus,
|
||||||
|
isPAYG,
|
||||||
|
}: IBillingDetailsProps) => {
|
||||||
|
if (isPAYG) {
|
||||||
|
return <BillingDetailsPAYG instanceStatus={instanceStatus} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (instanceStatus.plan === InstancePlan.PRO) {
|
||||||
|
return <BillingDetailsPro instanceStatus={instanceStatus} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
@ -0,0 +1,103 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Divider, Grid, styled, Typography } from '@mui/material';
|
||||||
|
import { GridRow } from 'component/common/GridRow/GridRow';
|
||||||
|
import { GridCol } from 'component/common/GridCol/GridCol';
|
||||||
|
import { GridColLink } from './GridColLink/GridColLink';
|
||||||
|
import type { IInstanceStatus } from 'interfaces/instance';
|
||||||
|
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||||
|
import {
|
||||||
|
BILLING_PAYG_DEFAULT_MINIMUM_SEATS,
|
||||||
|
BILLING_PAYG_USER_PRICE,
|
||||||
|
} from './BillingPlan';
|
||||||
|
|
||||||
|
const StyledInfoLabel = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||||
|
margin: `${theme.spacing(3)} 0`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IBillingDetailsPAYGProps {
|
||||||
|
instanceStatus: IInstanceStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BillingDetailsPAYG = ({
|
||||||
|
instanceStatus,
|
||||||
|
}: IBillingDetailsPAYGProps) => {
|
||||||
|
const { users, loading } = useUsers();
|
||||||
|
|
||||||
|
const eligibleUsers = users.filter((user) => user.email);
|
||||||
|
|
||||||
|
const minSeats =
|
||||||
|
instanceStatus.minSeats ?? BILLING_PAYG_DEFAULT_MINIMUM_SEATS;
|
||||||
|
|
||||||
|
const billableUsers = Math.max(eligibleUsers.length, minSeats);
|
||||||
|
const usersCost = BILLING_PAYG_USER_PRICE * billableUsers;
|
||||||
|
|
||||||
|
const totalCost = usersCost;
|
||||||
|
|
||||||
|
if (loading) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid container>
|
||||||
|
<GridRow
|
||||||
|
sx={(theme) => ({
|
||||||
|
marginBottom: theme.spacing(1.5),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<GridCol vertical>
|
||||||
|
<Typography>
|
||||||
|
<strong>Paid members</strong>
|
||||||
|
<GridColLink>
|
||||||
|
<Link to='/admin/users'>
|
||||||
|
{eligibleUsers.length} assigned of{' '}
|
||||||
|
{minSeats} minimum
|
||||||
|
</Link>
|
||||||
|
</GridColLink>
|
||||||
|
</Typography>
|
||||||
|
<StyledInfoLabel>
|
||||||
|
${BILLING_PAYG_USER_PRICE}/month per paid member
|
||||||
|
</StyledInfoLabel>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol>
|
||||||
|
<Typography
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontSize: theme.fontSizes.mainHeader,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${usersCost.toFixed(2)}
|
||||||
|
</Typography>
|
||||||
|
</GridCol>
|
||||||
|
</GridRow>
|
||||||
|
</Grid>
|
||||||
|
<StyledDivider />
|
||||||
|
<Grid container>
|
||||||
|
<GridRow>
|
||||||
|
<GridCol>
|
||||||
|
<Typography
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontWeight: theme.fontWeight.bold,
|
||||||
|
fontSize: theme.fontSizes.mainHeader,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Total
|
||||||
|
</Typography>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol>
|
||||||
|
<Typography
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontWeight: theme.fontWeight.bold,
|
||||||
|
fontSize: '2rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${totalCost.toFixed(2)}
|
||||||
|
</Typography>
|
||||||
|
</GridCol>
|
||||||
|
</GridRow>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,193 @@
|
|||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { Divider, Grid, styled, Typography } from '@mui/material';
|
||||||
|
import CheckIcon from '@mui/icons-material/Check';
|
||||||
|
import { GridRow } from 'component/common/GridRow/GridRow';
|
||||||
|
import { GridCol } from 'component/common/GridCol/GridCol';
|
||||||
|
import { GridColLink } from './GridColLink/GridColLink';
|
||||||
|
import type { IInstanceStatus } from 'interfaces/instance';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||||
|
import { useTrafficDataEstimation } from 'hooks/useTrafficData';
|
||||||
|
import {
|
||||||
|
BILLING_INCLUDED_REQUESTS,
|
||||||
|
BILLING_PLAN_PRICES,
|
||||||
|
BILLING_PRO_DEFAULT_INCLUDED_SEATS,
|
||||||
|
BILLING_PRO_USER_PRICE,
|
||||||
|
} from './BillingPlan';
|
||||||
|
import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
||||||
|
|
||||||
|
const StyledInfoLabel = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledCheckIcon = styled(CheckIcon)(({ theme }) => ({
|
||||||
|
fontSize: '1rem',
|
||||||
|
marginRight: theme.spacing(1),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledDivider = styled(Divider)(({ theme }) => ({
|
||||||
|
margin: `${theme.spacing(3)} 0`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
interface IBillingDetailsProProps {
|
||||||
|
instanceStatus: IInstanceStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BillingDetailsPro = ({
|
||||||
|
instanceStatus,
|
||||||
|
}: IBillingDetailsProProps) => {
|
||||||
|
const { users, loading } = useUsers();
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentPeriod,
|
||||||
|
toChartData,
|
||||||
|
toTrafficUsageSum,
|
||||||
|
endpointsInfo,
|
||||||
|
getDayLabels,
|
||||||
|
calculateOverageCost,
|
||||||
|
} = useTrafficDataEstimation();
|
||||||
|
|
||||||
|
const eligibleUsers = users.filter((user) => user.email);
|
||||||
|
|
||||||
|
const planPrice = BILLING_PLAN_PRICES[instanceStatus.plan];
|
||||||
|
const seats = BILLING_PRO_DEFAULT_INCLUDED_SEATS;
|
||||||
|
|
||||||
|
const freeAssigned = Math.min(eligibleUsers.length, seats);
|
||||||
|
const paidAssigned = eligibleUsers.length - freeAssigned;
|
||||||
|
const paidAssignedPrice = BILLING_PRO_USER_PRICE * paidAssigned;
|
||||||
|
const includedTraffic = BILLING_INCLUDED_REQUESTS;
|
||||||
|
const traffic = useInstanceTrafficMetrics(currentPeriod.key);
|
||||||
|
|
||||||
|
const overageCost = useMemo(() => {
|
||||||
|
if (!includedTraffic) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const trafficData = toChartData(
|
||||||
|
getDayLabels(currentPeriod.dayCount),
|
||||||
|
traffic,
|
||||||
|
endpointsInfo,
|
||||||
|
);
|
||||||
|
const totalTraffic = toTrafficUsageSum(trafficData);
|
||||||
|
return calculateOverageCost(totalTraffic, includedTraffic);
|
||||||
|
}, [includedTraffic, traffic, currentPeriod, endpointsInfo]);
|
||||||
|
|
||||||
|
const totalCost = planPrice + paidAssignedPrice + overageCost;
|
||||||
|
|
||||||
|
if (loading) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Grid container>
|
||||||
|
<GridRow
|
||||||
|
sx={(theme) => ({
|
||||||
|
marginBottom: theme.spacing(1.5),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<GridCol vertical>
|
||||||
|
<Typography>
|
||||||
|
<strong>Included members</strong>
|
||||||
|
<GridColLink>
|
||||||
|
<Link to='/admin/users'>
|
||||||
|
{freeAssigned} of {seats} assigned
|
||||||
|
</Link>
|
||||||
|
</GridColLink>
|
||||||
|
</Typography>
|
||||||
|
<StyledInfoLabel>
|
||||||
|
You have {seats} team members included in your PRO
|
||||||
|
plan
|
||||||
|
</StyledInfoLabel>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol>
|
||||||
|
<StyledCheckIcon />
|
||||||
|
<Typography variant='body2'>included</Typography>
|
||||||
|
</GridCol>
|
||||||
|
</GridRow>
|
||||||
|
<GridRow
|
||||||
|
sx={(theme) => ({
|
||||||
|
marginBottom: theme.spacing(1.5),
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<GridCol vertical>
|
||||||
|
<Typography>
|
||||||
|
<strong>Paid members</strong>
|
||||||
|
<GridColLink>
|
||||||
|
<Link to='/admin/users'>
|
||||||
|
{paidAssigned} assigned
|
||||||
|
</Link>
|
||||||
|
</GridColLink>
|
||||||
|
</Typography>
|
||||||
|
<StyledInfoLabel>
|
||||||
|
${BILLING_PRO_USER_PRICE}/month per paid member
|
||||||
|
</StyledInfoLabel>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol>
|
||||||
|
<Typography
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontSize: theme.fontSizes.mainHeader,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${paidAssignedPrice.toFixed(2)}
|
||||||
|
</Typography>
|
||||||
|
</GridCol>
|
||||||
|
</GridRow>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={overageCost > 0}
|
||||||
|
show={
|
||||||
|
<GridRow>
|
||||||
|
<GridCol vertical>
|
||||||
|
<Typography>
|
||||||
|
<strong>Accrued traffic charges</strong>
|
||||||
|
<GridColLink>
|
||||||
|
<Link to='/admin/network/data-usage'>
|
||||||
|
view details
|
||||||
|
</Link>
|
||||||
|
</GridColLink>
|
||||||
|
</Typography>
|
||||||
|
<StyledInfoLabel>
|
||||||
|
$5 dollar per 1 million started above
|
||||||
|
included data
|
||||||
|
</StyledInfoLabel>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol>
|
||||||
|
<Typography
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontSize: theme.fontSizes.mainHeader,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${overageCost.toFixed(2)}
|
||||||
|
</Typography>
|
||||||
|
</GridCol>
|
||||||
|
</GridRow>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Grid>
|
||||||
|
<StyledDivider />
|
||||||
|
<Grid container>
|
||||||
|
<GridRow>
|
||||||
|
<GridCol>
|
||||||
|
<Typography
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontWeight: theme.fontWeight.bold,
|
||||||
|
fontSize: theme.fontSizes.mainHeader,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Total
|
||||||
|
</Typography>
|
||||||
|
</GridCol>
|
||||||
|
<GridCol>
|
||||||
|
<Typography
|
||||||
|
sx={(theme) => ({
|
||||||
|
fontWeight: theme.fontWeight.bold,
|
||||||
|
fontSize: '2rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
${totalCost.toFixed(2)}
|
||||||
|
</Typography>
|
||||||
|
</GridCol>
|
||||||
|
</GridRow>
|
||||||
|
</Grid>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,23 +1,23 @@
|
|||||||
import type { FC } from 'react';
|
import { Alert, Grid, styled } from '@mui/material';
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { Alert, Divider, Grid, styled, Typography } from '@mui/material';
|
|
||||||
import { Link } from 'react-router-dom';
|
|
||||||
import CheckIcon from '@mui/icons-material/Check';
|
|
||||||
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import {
|
import { InstanceState, InstancePlan } from 'interfaces/instance';
|
||||||
type IInstanceStatus,
|
|
||||||
InstanceState,
|
|
||||||
InstancePlan,
|
|
||||||
} from 'interfaces/instance';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { trialHasExpired, isTrialInstance } 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 { Badge } from 'component/common/Badge/Badge';
|
import { Badge } from 'component/common/Badge/Badge';
|
||||||
import { GridColLink } from './GridColLink/GridColLink';
|
import { BillingDetails } from './BillingDetails';
|
||||||
import { useTrafficDataEstimation } from 'hooks/useTrafficData';
|
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
||||||
import { useInstanceTrafficMetrics } from 'hooks/api/getters/useInstanceTrafficMetrics/useInstanceTrafficMetrics';
|
|
||||||
|
export const BILLING_PLAN_PRICES: Record<string, number> = {
|
||||||
|
[InstancePlan.PRO]: 80,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BILLING_PAYG_USER_PRICE = 75;
|
||||||
|
export const BILLING_PAYG_DEFAULT_MINIMUM_SEATS = 5;
|
||||||
|
export const BILLING_PRO_USER_PRICE = 15;
|
||||||
|
export const BILLING_PRO_DEFAULT_INCLUDED_SEATS = 5;
|
||||||
|
export const BILLING_INCLUDED_REQUESTS = 53_000_000;
|
||||||
|
|
||||||
const StyledPlanBox = styled('aside')(({ theme }) => ({
|
const StyledPlanBox = styled('aside')(({ theme }) => ({
|
||||||
padding: theme.spacing(2.5),
|
padding: theme.spacing(2.5),
|
||||||
@ -29,20 +29,19 @@ const StyledPlanBox = styled('aside')(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledInfoLabel = styled(Typography)(({ theme }) => ({
|
|
||||||
fontSize: theme.fontSizes.smallBody,
|
|
||||||
color: theme.palette.text.secondary,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledPlanSpan = styled('span')(({ theme }) => ({
|
const StyledPlanSpan = styled('span')(({ theme }) => ({
|
||||||
fontSize: '3.25rem',
|
fontSize: '3.25rem',
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
color: theme.palette.primary.main,
|
color: theme.palette.primary.main,
|
||||||
fontWeight: 800,
|
fontWeight: 800,
|
||||||
|
marginRight: theme.spacing(1.5),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const StyledPAYGSpan = styled('span')(({ theme }) => ({
|
||||||
|
fontWeight: theme.fontWeight.bold,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledTrialSpan = styled('span')(({ theme }) => ({
|
const StyledTrialSpan = styled('span')(({ theme }) => ({
|
||||||
marginLeft: theme.spacing(1.5),
|
|
||||||
fontWeight: theme.fontWeight.bold,
|
fontWeight: theme.fontWeight.bold,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -61,74 +60,26 @@ const StyledAlert = styled(Alert)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledCheckIcon = styled(CheckIcon)(({ theme }) => ({
|
export const BillingPlan = () => {
|
||||||
fontSize: '1rem',
|
|
||||||
marginRight: theme.spacing(1),
|
|
||||||
}));
|
|
||||||
|
|
||||||
const StyledDivider = styled(Divider)(({ theme }) => ({
|
|
||||||
margin: `${theme.spacing(3)} 0`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface IBillingPlanProps {
|
|
||||||
instanceStatus: IInstanceStatus;
|
|
||||||
}
|
|
||||||
|
|
||||||
const proPlanIncludedRequests = 53_000_000;
|
|
||||||
|
|
||||||
export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
|
||||||
const { users, loading } = useUsers();
|
|
||||||
const expired = trialHasExpired(instanceStatus);
|
|
||||||
const { isPro } = useUiConfig();
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
currentPeriod,
|
uiConfig: { billing },
|
||||||
toChartData,
|
} = useUiConfig();
|
||||||
toTrafficUsageSum,
|
const { instanceStatus } = useInstanceStatus();
|
||||||
endpointsInfo,
|
|
||||||
getDayLabels,
|
|
||||||
calculateOverageCost,
|
|
||||||
} = useTrafficDataEstimation();
|
|
||||||
|
|
||||||
const eligibleUsers = users.filter((user: any) => user.email);
|
const isPAYG = billing === 'pay-as-you-go';
|
||||||
|
|
||||||
const price = {
|
if (!instanceStatus)
|
||||||
[InstancePlan.PRO]: 80,
|
return (
|
||||||
[InstancePlan.COMPANY]: 0,
|
<Grid item xs={12} md={7}>
|
||||||
[InstancePlan.TEAM]: 0,
|
<StyledPlanBox data-loading sx={{ flex: 1, height: '400px' }} />
|
||||||
[InstancePlan.ENTERPRISE]: 0,
|
</Grid>
|
||||||
[InstancePlan.UNKNOWN]: 0,
|
|
||||||
user: 15,
|
|
||||||
};
|
|
||||||
|
|
||||||
const planPrice = price[instanceStatus.plan];
|
|
||||||
const seats = instanceStatus.seats ?? 5;
|
|
||||||
|
|
||||||
const freeAssigned = Math.min(eligibleUsers.length, seats);
|
|
||||||
const paidAssigned = eligibleUsers.length - freeAssigned;
|
|
||||||
const paidAssignedPrice = price.user * paidAssigned;
|
|
||||||
const includedTraffic = isPro() ? proPlanIncludedRequests : 0;
|
|
||||||
const traffic = useInstanceTrafficMetrics(currentPeriod.key);
|
|
||||||
|
|
||||||
const overageCost = useMemo(() => {
|
|
||||||
if (!includedTraffic) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const trafficData = toChartData(
|
|
||||||
getDayLabels(currentPeriod.dayCount),
|
|
||||||
traffic,
|
|
||||||
endpointsInfo,
|
|
||||||
);
|
);
|
||||||
const totalTraffic = toTrafficUsageSum(trafficData);
|
|
||||||
return calculateOverageCost(totalTraffic, includedTraffic);
|
|
||||||
}, [includedTraffic, traffic, currentPeriod, endpointsInfo]);
|
|
||||||
|
|
||||||
const totalCost = planPrice + paidAssignedPrice + overageCost;
|
|
||||||
|
|
||||||
|
const expired = trialHasExpired(instanceStatus);
|
||||||
|
const planPrice = BILLING_PLAN_PRICES[instanceStatus.plan] ?? 0;
|
||||||
|
const plan = `${instanceStatus.plan}${isPAYG ? ' Pay-as-You-Go' : ''}`;
|
||||||
const inactive = instanceStatus.state !== InstanceState.ACTIVE;
|
const inactive = instanceStatus.state !== InstanceState.ACTIVE;
|
||||||
|
|
||||||
if (loading) return null;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid item xs={12} md={7}>
|
<Grid item xs={12} md={7}>
|
||||||
<StyledPlanBox>
|
<StyledPlanBox>
|
||||||
@ -139,7 +90,9 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
|||||||
After you have sent your billing information, your
|
After you have sent your billing information, your
|
||||||
instance will be upgraded - you don't have to do
|
instance will be upgraded - you don't have to do
|
||||||
anything.{' '}
|
anything.{' '}
|
||||||
<a href='mailto:support@getunleash.io?subject=PRO plan clarifications'>
|
<a
|
||||||
|
href={`mailto:support@getunleash.io?subject=${plan} plan clarifications`}
|
||||||
|
>
|
||||||
Get in touch with us
|
Get in touch with us
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
for any clarification
|
for any clarification
|
||||||
@ -147,10 +100,11 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Badge color='success'>Current plan</Badge>
|
<Badge color='success'>Current plan</Badge>
|
||||||
<Grid container>
|
<Grid
|
||||||
<GridRow
|
container
|
||||||
sx={(theme) => ({ marginBottom: theme.spacing(3) })}
|
sx={(theme) => ({ marginBottom: theme.spacing(3) })}
|
||||||
>
|
>
|
||||||
|
<GridRow>
|
||||||
<GridCol>
|
<GridCol>
|
||||||
<StyledPlanSpan>
|
<StyledPlanSpan>
|
||||||
{instanceStatus.plan}
|
{instanceStatus.plan}
|
||||||
@ -185,134 +139,18 @@ export const BillingPlan: FC<IBillingPlanProps> = ({ instanceStatus }) => {
|
|||||||
/>
|
/>
|
||||||
</GridCol>
|
</GridCol>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
</Grid>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={Boolean(
|
|
||||||
instanceStatus.plan === InstancePlan.PRO,
|
|
||||||
)}
|
|
||||||
show={
|
|
||||||
<>
|
|
||||||
<Grid container>
|
|
||||||
<GridRow
|
|
||||||
sx={(theme) => ({
|
|
||||||
marginBottom: theme.spacing(1.5),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<GridCol vertical>
|
|
||||||
<Typography>
|
|
||||||
<strong>Included members</strong>
|
|
||||||
<GridColLink>
|
|
||||||
<Link to='/admin/users'>
|
|
||||||
{freeAssigned} of 5 assigned
|
|
||||||
</Link>
|
|
||||||
</GridColLink>
|
|
||||||
</Typography>
|
|
||||||
<StyledInfoLabel>
|
|
||||||
You have 5 team members included in
|
|
||||||
your PRO plan
|
|
||||||
</StyledInfoLabel>
|
|
||||||
</GridCol>
|
|
||||||
<GridCol>
|
|
||||||
<StyledCheckIcon />
|
|
||||||
<Typography variant='body2'>
|
|
||||||
included
|
|
||||||
</Typography>
|
|
||||||
</GridCol>
|
|
||||||
</GridRow>
|
|
||||||
<GridRow
|
|
||||||
sx={(theme) => ({
|
|
||||||
marginBottom: theme.spacing(1.5),
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<GridCol vertical>
|
|
||||||
<Typography>
|
|
||||||
<strong>Paid members</strong>
|
|
||||||
<GridColLink>
|
|
||||||
<Link to='/admin/users'>
|
|
||||||
{paidAssigned} assigned
|
|
||||||
</Link>
|
|
||||||
</GridColLink>
|
|
||||||
</Typography>
|
|
||||||
<StyledInfoLabel>
|
|
||||||
$15/month per paid member
|
|
||||||
</StyledInfoLabel>
|
|
||||||
</GridCol>
|
|
||||||
<GridCol>
|
|
||||||
<Typography
|
|
||||||
sx={(theme) => ({
|
|
||||||
fontSize:
|
|
||||||
theme.fontSizes.mainHeader,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
${paidAssignedPrice.toFixed(2)}
|
|
||||||
</Typography>
|
|
||||||
</GridCol>
|
|
||||||
</GridRow>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={overageCost > 0}
|
|
||||||
show={
|
|
||||||
<GridRow>
|
<GridRow>
|
||||||
<GridCol vertical>
|
<ConditionallyRender
|
||||||
<Typography>
|
condition={isPAYG}
|
||||||
<strong>
|
show={
|
||||||
Accrued traffic charges
|
<StyledPAYGSpan>Pay-as-You-Go</StyledPAYGSpan>
|
||||||
</strong>
|
|
||||||
<GridColLink>
|
|
||||||
<Link to='/admin/network/data-usage'>
|
|
||||||
view details
|
|
||||||
</Link>
|
|
||||||
</GridColLink>
|
|
||||||
</Typography>
|
|
||||||
<StyledInfoLabel>
|
|
||||||
$5 dollar per 1 million
|
|
||||||
started above included data
|
|
||||||
</StyledInfoLabel>
|
|
||||||
</GridCol>
|
|
||||||
<GridCol>
|
|
||||||
<Typography
|
|
||||||
sx={(theme) => ({
|
|
||||||
fontSize:
|
|
||||||
theme.fontSizes
|
|
||||||
.mainHeader,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
${overageCost.toFixed(2)}
|
|
||||||
</Typography>
|
|
||||||
</GridCol>
|
|
||||||
</GridRow>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Grid>
|
|
||||||
<StyledDivider />
|
|
||||||
<Grid container>
|
|
||||||
<GridRow>
|
|
||||||
<GridCol>
|
|
||||||
<Typography
|
|
||||||
sx={(theme) => ({
|
|
||||||
fontWeight:
|
|
||||||
theme.fontWeight.bold,
|
|
||||||
fontSize:
|
|
||||||
theme.fontSizes.mainHeader,
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
Total
|
|
||||||
</Typography>
|
|
||||||
</GridCol>
|
|
||||||
<GridCol>
|
|
||||||
<Typography
|
|
||||||
sx={(theme) => ({
|
|
||||||
fontWeight:
|
|
||||||
theme.fontWeight.bold,
|
|
||||||
fontSize: '2rem',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
${totalCost.toFixed(2)}
|
|
||||||
</Typography>
|
|
||||||
</GridCol>
|
|
||||||
</GridRow>
|
</GridRow>
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
<BillingDetails
|
||||||
}
|
instanceStatus={instanceStatus}
|
||||||
|
isPAYG={isPAYG}
|
||||||
/>
|
/>
|
||||||
</StyledPlanBox>
|
</StyledPlanBox>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -2,6 +2,7 @@ import type { VFC } from 'react';
|
|||||||
import { Alert } from '@mui/material';
|
import { Alert } from '@mui/material';
|
||||||
import { useUsersPlan } from 'hooks/useUsersPlan';
|
import { useUsersPlan } from 'hooks/useUsersPlan';
|
||||||
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||||
|
import { BILLING_PRO_USER_PRICE } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||||
|
|
||||||
export const SeatCostWarning: VFC = () => {
|
export const SeatCostWarning: VFC = () => {
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
@ -19,7 +20,8 @@ export const SeatCostWarning: VFC = () => {
|
|||||||
<p>
|
<p>
|
||||||
<strong>Heads up!</strong> You are exceeding your allocated free
|
<strong>Heads up!</strong> You are exceeding your allocated free
|
||||||
members included in your plan ({planUsers.length} of {seats}).
|
members included in your plan ({planUsers.length} of {seats}).
|
||||||
Creating this user will add <strong>$15/month</strong> to your
|
Creating this user will add{' '}
|
||||||
|
<strong>${BILLING_PRO_USER_PRICE}/month</strong> to your
|
||||||
invoice, starting with your next payment.
|
invoice, starting with your next payment.
|
||||||
</p>
|
</p>
|
||||||
</Alert>
|
</Alert>
|
||||||
|
@ -4,6 +4,13 @@ import GitHub from '@mui/icons-material/GitHub';
|
|||||||
import Launch from '@mui/icons-material/Launch';
|
import Launch from '@mui/icons-material/Launch';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import {
|
||||||
|
BILLING_PAYG_DEFAULT_MINIMUM_SEATS,
|
||||||
|
BILLING_PAYG_USER_PRICE,
|
||||||
|
BILLING_PLAN_PRICES,
|
||||||
|
BILLING_PRO_DEFAULT_INCLUDED_SEATS,
|
||||||
|
} from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||||
|
import { InstancePlan } from 'interfaces/instance';
|
||||||
|
|
||||||
const StyledDemoDialog = styled(DemoDialog)(({ theme }) => ({
|
const StyledDemoDialog = styled(DemoDialog)(({ theme }) => ({
|
||||||
'& .MuiDialog-paper': {
|
'& .MuiDialog-paper': {
|
||||||
@ -132,10 +139,11 @@ export const DemoDialogPlans = ({ open, onClose }: IDemoDialogPlansProps) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant='h6' fontWeight='normal'>
|
<Typography variant='h6' fontWeight='normal'>
|
||||||
$75 per user/month
|
${BILLING_PAYG_USER_PRICE} per user/month
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body2'>
|
<Typography variant='body2'>
|
||||||
5 users minimum
|
{BILLING_PAYG_DEFAULT_MINIMUM_SEATS} users
|
||||||
|
minimum
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@ -166,10 +174,11 @@ export const DemoDialogPlans = ({ open, onClose }: IDemoDialogPlansProps) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant='h6' fontWeight='normal'>
|
<Typography variant='h6' fontWeight='normal'>
|
||||||
$80/month
|
${BILLING_PLAN_PRICES[InstancePlan.PRO]}/month
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body2'>
|
<Typography variant='body2'>
|
||||||
includes 5 seats
|
includes {BILLING_PRO_DEFAULT_INCLUDED_SEATS}{' '}
|
||||||
|
seats
|
||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
import { Box, Card, styled, Typography } from '@mui/material';
|
import { Box, Card, styled, Typography } from '@mui/material';
|
||||||
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
|
import EnvironmentIcon from 'component/common/EnvironmentIcon/EnvironmentIcon';
|
||||||
|
import { BILLING_PRO_DEFAULT_INCLUDED_SEATS } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||||
|
|
||||||
type OrderEnvironmentsDialogPricingProps = {
|
type OrderEnvironmentsDialogPricingProps = {
|
||||||
pricingOptions: Array<{ environments: number; price: number }>;
|
pricingOptions: Array<{ environments: number; price: number }>;
|
||||||
@ -61,8 +62,9 @@ export const OrderEnvironmentsDialogPricing: FC<
|
|||||||
))}
|
))}
|
||||||
<StyledExtraText>
|
<StyledExtraText>
|
||||||
<Typography variant='body2' color='white'>
|
<Typography variant='body2' color='white'>
|
||||||
With Pro, there is a minimum of 5 users, meaning an additional
|
With Pro, there is a minimum of{' '}
|
||||||
environment will cost at least $50 per month.
|
{BILLING_PRO_DEFAULT_INCLUDED_SEATS} users, meaning an
|
||||||
|
additional environment will cost at least $50 per month.
|
||||||
</Typography>
|
</Typography>
|
||||||
</StyledExtraText>
|
</StyledExtraText>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
@ -2,19 +2,16 @@ import { testServerRoute, testServerSetup } from '../../../../utils/testServer';
|
|||||||
import { screen } from '@testing-library/react';
|
import { screen } from '@testing-library/react';
|
||||||
import { render } from 'utils/testRenderer';
|
import { render } from 'utils/testRenderer';
|
||||||
import { UserSeats } from './UserSeats';
|
import { UserSeats } from './UserSeats';
|
||||||
|
import { BILLING_PRO_DEFAULT_INCLUDED_SEATS } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||||
|
|
||||||
const server = testServerSetup();
|
const server = testServerSetup();
|
||||||
const user1 = {};
|
const user1 = {};
|
||||||
const user2 = {};
|
const user2 = {};
|
||||||
|
|
||||||
const setupApiWithSeats = (seats: number | undefined) => {
|
const setupApi = () => {
|
||||||
testServerRoute(server, '/api/admin/user-admin', {
|
testServerRoute(server, '/api/admin/user-admin', {
|
||||||
users: [user1, user2],
|
users: [user1, user2],
|
||||||
});
|
});
|
||||||
testServerRoute(server, '/api/instance/status', {
|
|
||||||
plan: 'Enterprise',
|
|
||||||
seats,
|
|
||||||
});
|
|
||||||
testServerRoute(server, '/api/admin/ui-config', {
|
testServerRoute(server, '/api/admin/ui-config', {
|
||||||
flags: {
|
flags: {
|
||||||
UNLEASH_CLOUD: true,
|
UNLEASH_CLOUD: true,
|
||||||
@ -23,18 +20,12 @@ const setupApiWithSeats = (seats: number | undefined) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
test('User seats display when seats are available', async () => {
|
test('User seats display when seats are available', async () => {
|
||||||
setupApiWithSeats(20);
|
setupApi();
|
||||||
|
|
||||||
render(<UserSeats />);
|
render(<UserSeats />);
|
||||||
|
|
||||||
await screen.findByText('User seats');
|
await screen.findByText('User seats');
|
||||||
await screen.findByText('2/20 seats used');
|
await screen.findByText(
|
||||||
});
|
`2/${BILLING_PRO_DEFAULT_INCLUDED_SEATS} seats used`,
|
||||||
|
);
|
||||||
test('User seats does not display when seats are not available', async () => {
|
|
||||||
setupApiWithSeats(undefined);
|
|
||||||
|
|
||||||
render(<UserSeats />);
|
|
||||||
|
|
||||||
expect(screen.queryByText('User seats')).not.toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
@ -2,7 +2,7 @@ import LicenseIcon from '@mui/icons-material/ReceiptLongOutlined';
|
|||||||
import { Box, styled, Typography } from '@mui/material';
|
import { Box, styled, Typography } from '@mui/material';
|
||||||
import LinearProgress from '@mui/material/LinearProgress';
|
import LinearProgress from '@mui/material/LinearProgress';
|
||||||
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
import { useUsers } from 'hooks/api/getters/useUsers/useUsers';
|
||||||
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
import { BILLING_PRO_DEFAULT_INCLUDED_SEATS } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||||
|
|
||||||
const SeatsUsageBar = styled(LinearProgress)(({ theme }) => ({
|
const SeatsUsageBar = styled(LinearProgress)(({ theme }) => ({
|
||||||
marginTop: theme.spacing(0.5),
|
marginTop: theme.spacing(0.5),
|
||||||
@ -32,8 +32,7 @@ const SeatsUsageText = styled(Box)(({ theme }) => ({
|
|||||||
|
|
||||||
export const UserSeats = () => {
|
export const UserSeats = () => {
|
||||||
const { users } = useUsers();
|
const { users } = useUsers();
|
||||||
const { instanceStatus } = useInstanceStatus();
|
const seats = BILLING_PRO_DEFAULT_INCLUDED_SEATS;
|
||||||
const seats = instanceStatus?.seats;
|
|
||||||
|
|
||||||
if (typeof seats === 'number') {
|
if (typeof seats === 'number') {
|
||||||
const percentageSeats = Math.floor((users.length / seats) * 100);
|
const percentageSeats = Math.floor((users.length / seats) * 100);
|
||||||
|
@ -37,7 +37,9 @@ export const useInstanceStatus = (): IUseInstanceStatusOutput => {
|
|||||||
instanceStatus: data,
|
instanceStatus: data,
|
||||||
refetchInstanceStatus: refetch,
|
refetchInstanceStatus: refetch,
|
||||||
refresh,
|
refresh,
|
||||||
isBilling: billingPlans.includes(data?.plan ?? InstancePlan.UNKNOWN),
|
isBilling:
|
||||||
|
uiConfig.billing === 'pay-as-you-go' ||
|
||||||
|
billingPlans.includes(data?.plan ?? InstancePlan.UNKNOWN),
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
};
|
};
|
||||||
|
@ -2,7 +2,7 @@ import type { IUser } from 'interfaces/user';
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useInstanceStatus } from './api/getters/useInstanceStatus/useInstanceStatus';
|
import { useInstanceStatus } from './api/getters/useInstanceStatus/useInstanceStatus';
|
||||||
import { InstancePlan } from 'interfaces/instance';
|
import { InstancePlan } from 'interfaces/instance';
|
||||||
import useUiConfig from './api/getters/useUiConfig/useUiConfig';
|
import { BILLING_PRO_DEFAULT_INCLUDED_SEATS } from 'component/admin/billing/BillingDashboard/BillingPlan/BillingPlan';
|
||||||
|
|
||||||
export interface IUsersPlanOutput {
|
export interface IUsersPlanOutput {
|
||||||
planUsers: IUser[];
|
planUsers: IUser[];
|
||||||
@ -13,10 +13,9 @@ export interface IUsersPlanOutput {
|
|||||||
|
|
||||||
export const useUsersPlan = (users: IUser[]): IUsersPlanOutput => {
|
export const useUsersPlan = (users: IUser[]): IUsersPlanOutput => {
|
||||||
const { instanceStatus } = useInstanceStatus();
|
const { instanceStatus } = useInstanceStatus();
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
|
|
||||||
const isBillingUsers = Boolean(instanceStatus?.plan === InstancePlan.PRO);
|
const isBillingUsers = Boolean(instanceStatus?.plan === InstancePlan.PRO);
|
||||||
const seats = instanceStatus?.seats ?? 5;
|
const seats = BILLING_PRO_DEFAULT_INCLUDED_SEATS;
|
||||||
|
|
||||||
const planUsers = useMemo(
|
const planUsers = useMemo(
|
||||||
() => calculatePaidUsers(users, isBillingUsers, seats),
|
() => calculatePaidUsers(users, isBillingUsers, seats),
|
||||||
|
@ -6,6 +6,7 @@ export interface IInstanceStatus {
|
|||||||
billingCenter?: string;
|
billingCenter?: string;
|
||||||
state?: InstanceState;
|
state?: InstanceState;
|
||||||
seats?: number;
|
seats?: number;
|
||||||
|
minSeats?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum InstanceState {
|
export enum InstanceState {
|
||||||
|
Loading…
Reference in New Issue
Block a user