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:
parent
bb3d938f57
commit
4ed138c151
@ -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 />
|
||||
<StyledHeader>Usage and invoices</StyledHeader>
|
||||
<StyledPageGrid>
|
||||
<BillingInvoices />
|
||||
<div>
|
||||
<BillingInfo />
|
||||
</div>
|
||||
</StyledPageGrid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
@ -101,6 +101,7 @@ export const BillingPlan = () => {
|
||||
|
||||
return (
|
||||
<Grid item xs={12} md={7}>
|
||||
{JSON.stringify({ isPAYG })}
|
||||
<StyledWrapper>
|
||||
<ConditionallyRender
|
||||
condition={inactive}
|
||||
|
||||
127
frontend/src/component/admin/billing/BillingInfo/BillingInfo.tsx
Normal file
127
frontend/src/component/admin/billing/BillingInfo/BillingInfo.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@ -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 }) => ({
|
||||
|
||||
@ -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}` } : {}),
|
||||
}));
|
||||
|
||||
|
||||
@ -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', () => {
|
||||
|
||||
@ -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')}`;
|
||||
}
|
||||
|
||||
|
||||
@ -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.length > 0 ? (
|
||||
<>
|
||||
{invoices.map((invoice) => (
|
||||
<BillingInvoice key={invoice.invoiceDate} {...invoice} />
|
||||
<BillingInvoice
|
||||
key={invoice.invoiceDate}
|
||||
{...invoice}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<TablePlaceholder>
|
||||
There are no invoices or estimates available right now.
|
||||
</TablePlaceholder>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user