-
-
Notifications
You must be signed in to change notification settings - Fork 720
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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). ![image](https://github.com/user-attachments/assets/97a5a420-a4f6-4b6c-93d6-3fffddbacbc7)
- Loading branch information
Showing
15 changed files
with
431 additions
and
272 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 3 additions & 11 deletions
14
frontend/src/component/admin/billing/BillingDashboard/BillingDashboard.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,12 @@ | ||
import { Grid } from '@mui/material'; | ||
import type { IInstanceStatus } from 'interfaces/instance'; | ||
import type { VFC } from 'react'; | ||
import { BillingInformation } from './BillingInformation/BillingInformation'; | ||
import { BillingPlan } from './BillingPlan/BillingPlan'; | ||
|
||
interface IBillingDashboardProps { | ||
instanceStatus: IInstanceStatus; | ||
} | ||
|
||
export const BillingDashboard: VFC<IBillingDashboardProps> = ({ | ||
instanceStatus, | ||
}) => { | ||
export const BillingDashboard = () => { | ||
return ( | ||
<Grid container spacing={4}> | ||
<BillingInformation instanceStatus={instanceStatus} /> | ||
<BillingPlan instanceStatus={instanceStatus} /> | ||
<BillingInformation /> | ||
<BillingPlan /> | ||
</Grid> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,9 @@ | ||
import type { FC } from 'react'; | ||
import { Alert, Divider, Grid, styled, Typography } from '@mui/material'; | ||
import { BillingInformationButton } from './BillingInformationButton/BillingInformationButton'; | ||
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 }) => ({ | ||
padding: theme.spacing(4), | ||
|
@@ -28,13 +29,22 @@ const StyledDivider = styled(Divider)(({ theme }) => ({ | |
margin: `${theme.spacing(2.5)} 0`, | ||
borderColor: theme.palette.divider, | ||
})); | ||
interface IBillingInformationProps { | ||
instanceStatus: IInstanceStatus; | ||
} | ||
|
||
export const BillingInformation: FC<IBillingInformationProps> = ({ | ||
instanceStatus, | ||
}) => { | ||
export const BillingInformation = () => { | ||
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; | ||
|
||
return ( | ||
|
@@ -58,7 +68,9 @@ export const BillingInformation: FC<IBillingInformationProps> = ({ | |
</StyledInfoLabel> | ||
<StyledDivider /> | ||
<StyledInfoLabel> | ||
<a href='mailto:[email protected]?subject=PRO plan clarifications'> | ||
<a | ||
href={`mailto:[email protected]?subject=${plan} plan clarifications`} | ||
> | ||
Get in touch with us | ||
</a>{' '} | ||
for any clarification | ||
|
23 changes: 23 additions & 0 deletions
23
frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetails.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
}; |
103 changes: 103 additions & 0 deletions
103
frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingDetailsPAYG.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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> | ||
</> | ||
); | ||
}; |
Oops, something went wrong.