1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-27 11:02:16 +01:00

Billing info updates (#10761)

This commit is contained in:
Tymoteusz Czech 2025-10-08 14:11:24 +02:00 committed by GitHub
parent bb3d938f57
commit 4ed138c151
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 186 additions and 93 deletions

View File

@ -4,12 +4,28 @@ import { ADMIN } from 'component/providers/AccessProvider/permissions';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
import { Alert, Box } from '@mui/material';
import { Alert, Box, styled, Typography } from '@mui/material';
import { BillingDashboard } from './BillingDashboard/BillingDashboard.tsx';
import { BillingHistory } from './BillingHistory/BillingHistory.tsx';
import useInvoices from 'hooks/api/getters/useInvoices/useInvoices';
import { useUiFlag } from 'hooks/useUiFlag';
import { BillingInvoices } from './BillingInvoices/BillingInvoices.tsx';
import { BillingInfo } from './BillingInfo/BillingInfo.tsx';
const StyledHeader = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.mainHeader,
color: theme.palette.text.primary,
}));
const StyledPageGrid = styled(Box)(({ theme }) => ({
display: 'grid',
gridTemplateColumns: '1fr 320px',
gap: theme.spacing(2),
[theme.breakpoints.down('md')]: {
display: 'flex',
flexDirection: 'column-reverse',
},
}));
export const Billing = () => {
const { isBilling, refetchInstanceStatus, refresh, loading } =
@ -34,8 +50,13 @@ export const Billing = () => {
gap: theme.spacing(4),
})}
>
<BillingDashboard />
<BillingInvoices />
<StyledHeader>Usage and invoices</StyledHeader>
<StyledPageGrid>
<BillingInvoices />
<div>
<BillingInfo />
</div>
</StyledPageGrid>
</Box>
);
}

View File

@ -101,6 +101,7 @@ export const BillingPlan = () => {
return (
<Grid item xs={12} md={7}>
{JSON.stringify({ isPAYG })}
<StyledWrapper>
<ConditionallyRender
condition={inactive}

View File

@ -0,0 +1,127 @@
import type { FC } from 'react';
import { Button, Divider, Paper, styled, Typography } from '@mui/material';
import CreditCardIcon from '@mui/icons-material/CreditCard';
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { InstanceState } from 'interfaces/instance';
import { formatApiPath } from 'utils/formatPath';
const PORTAL_URL = formatApiPath('api/admin/invoices');
type BillingInfoProps = {};
const StyledWrapper = styled(Paper)(({ theme }) => ({
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadiusLarge,
boxShadow: theme.boxShadows.card,
}));
const StyledRow = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'space-between',
marginTop: theme.spacing(1),
fontSize: theme.typography.body2.fontSize,
gap: theme.spacing(1),
}));
const StyledItemTitle = styled('span')(({ theme }) => ({
color: theme.palette.text.secondary,
whiteSpace: 'nowrap',
}));
const StyledItemValue = styled('span')(({ theme }) => ({
textAlign: 'right',
}));
const StyledButton = styled(Button)(({ theme }) => ({
margin: theme.spacing(0, 0, 2, 0),
}));
const StyledInfoLabel = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
color: theme.palette.text.secondary,
marginBottom: theme.spacing(1),
}));
const StyledDivider = styled(Divider)(({ theme }) => ({
margin: `${theme.spacing(2.5)} 0`,
borderColor: theme.palette.divider,
}));
const GetInTouch: FC = () => (
<StyledInfoLabel>
<a
href={`mailto:support@getunleash.io?subject=Billing plan clarifications`}
>
Get in touch with us
</a>{' '}
for any clarification
</StyledInfoLabel>
);
export const BillingInfo: FC<BillingInfoProps> = () => {
const { instanceStatus } = useInstanceStatus();
const {
uiConfig: { billing },
} = useUiConfig();
if (!instanceStatus) {
return (
<StyledWrapper>
<StyledInfoLabel>
Your billing is managed by Unleash
</StyledInfoLabel>
<GetInTouch />
</StyledWrapper>
);
}
const isPAYG = billing === 'pay-as-you-go';
const plan = `${instanceStatus.plan}${isPAYG ? ' Pay-as-You-Go' : ''}`;
const isEnterpriseConsumption = billing === 'enterprise-consumption';
const inactive = instanceStatus.state !== InstanceState.ACTIVE;
const { isCustomBilling } = instanceStatus;
if (isCustomBilling) {
return (
<StyledWrapper>
<StyledInfoLabel>
Your billing is managed by Unleash
</StyledInfoLabel>
<GetInTouch />
</StyledWrapper>
);
}
return (
<StyledWrapper>
<Typography variant='h3'>Billing details</Typography>
<StyledRow>
<StyledItemTitle>Current plan</StyledItemTitle>{' '}
<StyledItemValue>
{plan}
{isPAYG || isEnterpriseConsumption ? ' Pay-as-You-Go' : ''}
{isEnterpriseConsumption ? ' Consumption' : ''}
</StyledItemValue>
</StyledRow>
<StyledRow>
<StyledItemTitle>Plan price</StyledItemTitle>{' '}
{/* FIXME: where to take data from? */}
<StyledItemValue>$450 / month</StyledItemValue>
</StyledRow>
<StyledDivider />
<StyledButton
href={`${PORTAL_URL}/${!inactive ? 'portal' : 'checkout'}`}
variant={!inactive ? 'outlined' : 'contained'}
startIcon={<CreditCardIcon />}
>
{!inactive ? 'Edit billing details' : 'Add billing details'}
</StyledButton>
<StyledInfoLabel>
{inactive
? 'Once we have received your billing information we will upgrade your trial within 1 business day.'
: 'Update your credit card and business information and change which email address we send invoices to.'}
</StyledInfoLabel>
<GetInTouch />
</StyledWrapper>
);
};

View File

@ -12,6 +12,7 @@ export const StyledSubgrid = styled('div', {
margin: theme.spacing(0.25, 0),
padding: withBackground ? theme.spacing(0, 2, 1) : theme.spacing(0, 2),
borderRadius: theme.shape.borderRadiusLarge,
gap: theme.spacing(1),
}));
export const StyledAmountCell = styled('div')(({ theme }) => ({

View File

@ -23,7 +23,7 @@ const StyledTableFooterRow = styled('div')<{ last?: boolean }>(
const StyledTableFooterCell = styled('div', {
shouldForwardProp: (prop) => prop !== 'colSpan',
})<{ colSpan?: number }>(({ theme, colSpan }) => ({
padding: theme.spacing(1, 0),
padding: theme.spacing(1, 0, 1, 0.5),
...(colSpan ? { gridColumn: `span ${colSpan}` } : {}),
}));

View File

@ -12,6 +12,10 @@ describe('formatCurrency', () => {
);
expect(formatCurrency(0, 'USD')).toMatchInlineSnapshot(`"$0"`);
expect(formatCurrency(-500, 'USD')).toMatchInlineSnapshot(`"$-500"`);
expect(formatCurrency(1000, 'usd')).toMatchInlineSnapshot(`"$1,000"`);
expect(formatCurrency(1234.56, 'usd')).toMatchInlineSnapshot(
`"$1,234.56"`,
);
});
it('formats EUR currency', () => {
@ -24,6 +28,10 @@ describe('formatCurrency', () => {
);
expect(formatCurrency(0, 'EUR')).toMatchInlineSnapshot(`"€0"`);
expect(formatCurrency(-500, 'EUR')).toMatchInlineSnapshot(`"€500"`);
expect(formatCurrency(1000, 'eur')).toMatchInlineSnapshot(`"€1 000"`);
expect(formatCurrency(1234.56, 'eur')).toMatchInlineSnapshot(
`"€1 234,56"`,
);
});
it('formats other currencies', () => {

View File

@ -1,8 +1,8 @@
export const formatCurrency = (value: number, currency?: string) => {
if (currency === 'USD') {
if (currency && currency.toLocaleLowerCase() === 'usd') {
return `$${value.toLocaleString('en-US')}`;
}
if (currency === 'EUR') {
if (currency && currency.toLocaleLowerCase() === 'eur') {
return `\u2009${value.toLocaleString('no-NO')}`;
}

View File

@ -1,7 +1,8 @@
import { Box, styled, Typography } from '@mui/material';
import { Box, styled } from '@mui/material';
import type { FC } from 'react';
import { BillingInvoice } from './BillingInvoice/BillingInvoice.tsx';
import { useDetailedInvoices } from 'hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts';
import { TablePlaceholder } from 'component/common/Table';
const StyledContainer = styled(Box)(({ theme }) => ({
display: 'flex',
@ -9,22 +10,29 @@ const StyledContainer = styled(Box)(({ theme }) => ({
gap: theme.spacing(3),
}));
const StyledHeader = styled(Typography)(({ theme }) => ({
fontSize: theme.fontSizes.mainHeader,
fontWeight: theme.fontWeight.semi,
color: theme.palette.text.primary,
marginBottom: theme.spacing(2),
}));
export const BillingInvoices: FC = () => {
const { invoices } = useDetailedInvoices();
const { invoices, loading } = useDetailedInvoices();
if (loading) {
return null;
}
return (
<StyledContainer>
<StyledHeader>Usage and invoices</StyledHeader>
{invoices.map((invoice) => (
<BillingInvoice key={invoice.invoiceDate} {...invoice} />
))}
{invoices.length > 0 ? (
<>
{invoices.map((invoice) => (
<BillingInvoice
key={invoice.invoiceDate}
{...invoice}
/>
))}
</>
) : (
<TablePlaceholder>
There are no invoices or estimates available right now.
</TablePlaceholder>
)}
</StyledContainer>
);
};

View File

@ -21,78 +21,5 @@ export const useDetailedInvoices = (options: SWRConfiguration = {}) => {
const invoices = useMemo(() => data?.invoices ?? [], [data]);
// return { invoices, error, loading: isLoading };
return {
invoices: [
// FIXME: MOCK
{
status: 'upcoming',
dueDate: '2023-09-01',
invoiceDate: '2023-08-01',
invoicePDF: 'https://example.com/invoice/1.pdf',
invoiceURL: 'https://example.com/invoice/1',
totalAmount: 400,
mainLines: [
{
currency: 'USD',
description: 'Service C',
lookupKey: 'service-c',
quantity: 0,
consumption: 100,
limit: 120,
totalAmount: 200,
},
],
usageLines: [
{
currency: 'USD',
description: 'Service A',
lookupKey: 'service-a',
quantity: 1,
consumption: 100,
totalAmount: 100,
},
{
currency: 'USD',
description: 'Backend streaming connections',
lookupKey: 'service-b',
quantity: 324_000,
limit: 3_000_000,
consumption: 3_000_000,
totalAmount: 200,
},
{
currency: 'USD',
description: 'Frontend traffic bundle',
lookupKey: 'frontend-traffic-bundle',
quantity: 0,
consumption: 2_345_239,
limit: 5_000_000,
totalAmount: 0,
},
],
},
{
status: 'invoiced',
dueDate: '2023-09-15',
invoiceDate: '2023-08-15',
invoicePDF: 'https://example.com/invoice/2.pdf',
invoiceURL: 'https://example.com/invoice/2',
totalAmount: 200,
mainLines: [
{
currency: 'EUR',
description: 'Service C',
lookupKey: 'service-c',
quantity: 1,
totalAmount: 200,
},
],
usageLines: [],
},
] satisfies DetailedInvoicesSchema['invoices'],
error,
loading: isLoading,
};
return { invoices, error, loading: isLoading };
};