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 { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
import { PermissionGuard } from 'component/common/PermissionGuard/PermissionGuard';
|
||||||
import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
|
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 { BillingDashboard } from './BillingDashboard/BillingDashboard.tsx';
|
||||||
import { BillingHistory } from './BillingHistory/BillingHistory.tsx';
|
import { BillingHistory } from './BillingHistory/BillingHistory.tsx';
|
||||||
import useInvoices from 'hooks/api/getters/useInvoices/useInvoices';
|
import useInvoices from 'hooks/api/getters/useInvoices/useInvoices';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import { BillingInvoices } from './BillingInvoices/BillingInvoices.tsx';
|
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 = () => {
|
export const Billing = () => {
|
||||||
const { isBilling, refetchInstanceStatus, refresh, loading } =
|
const { isBilling, refetchInstanceStatus, refresh, loading } =
|
||||||
@ -34,8 +50,13 @@ export const Billing = () => {
|
|||||||
gap: theme.spacing(4),
|
gap: theme.spacing(4),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<BillingDashboard />
|
<StyledHeader>Usage and invoices</StyledHeader>
|
||||||
<BillingInvoices />
|
<StyledPageGrid>
|
||||||
|
<BillingInvoices />
|
||||||
|
<div>
|
||||||
|
<BillingInfo />
|
||||||
|
</div>
|
||||||
|
</StyledPageGrid>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -101,6 +101,7 @@ export const BillingPlan = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Grid item xs={12} md={7}>
|
<Grid item xs={12} md={7}>
|
||||||
|
{JSON.stringify({ isPAYG })}
|
||||||
<StyledWrapper>
|
<StyledWrapper>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={inactive}
|
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),
|
margin: theme.spacing(0.25, 0),
|
||||||
padding: withBackground ? theme.spacing(0, 2, 1) : theme.spacing(0, 2),
|
padding: withBackground ? theme.spacing(0, 2, 1) : theme.spacing(0, 2),
|
||||||
borderRadius: theme.shape.borderRadiusLarge,
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
|
gap: theme.spacing(1),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledAmountCell = styled('div')(({ theme }) => ({
|
export const StyledAmountCell = styled('div')(({ theme }) => ({
|
||||||
|
|||||||
@ -23,7 +23,7 @@ const StyledTableFooterRow = styled('div')<{ last?: boolean }>(
|
|||||||
const StyledTableFooterCell = styled('div', {
|
const StyledTableFooterCell = styled('div', {
|
||||||
shouldForwardProp: (prop) => prop !== 'colSpan',
|
shouldForwardProp: (prop) => prop !== 'colSpan',
|
||||||
})<{ colSpan?: number }>(({ theme, colSpan }) => ({
|
})<{ colSpan?: number }>(({ theme, colSpan }) => ({
|
||||||
padding: theme.spacing(1, 0),
|
padding: theme.spacing(1, 0, 1, 0.5),
|
||||||
...(colSpan ? { gridColumn: `span ${colSpan}` } : {}),
|
...(colSpan ? { gridColumn: `span ${colSpan}` } : {}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@ -12,6 +12,10 @@ describe('formatCurrency', () => {
|
|||||||
);
|
);
|
||||||
expect(formatCurrency(0, 'USD')).toMatchInlineSnapshot(`"$0"`);
|
expect(formatCurrency(0, 'USD')).toMatchInlineSnapshot(`"$0"`);
|
||||||
expect(formatCurrency(-500, 'USD')).toMatchInlineSnapshot(`"$-500"`);
|
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', () => {
|
it('formats EUR currency', () => {
|
||||||
@ -24,6 +28,10 @@ describe('formatCurrency', () => {
|
|||||||
);
|
);
|
||||||
expect(formatCurrency(0, 'EUR')).toMatchInlineSnapshot(`"€ 0"`);
|
expect(formatCurrency(0, 'EUR')).toMatchInlineSnapshot(`"€ 0"`);
|
||||||
expect(formatCurrency(-500, 'EUR')).toMatchInlineSnapshot(`"€ −500"`);
|
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', () => {
|
it('formats other currencies', () => {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
export const formatCurrency = (value: number, currency?: string) => {
|
export const formatCurrency = (value: number, currency?: string) => {
|
||||||
if (currency === 'USD') {
|
if (currency && currency.toLocaleLowerCase() === 'usd') {
|
||||||
return `$${value.toLocaleString('en-US')}`;
|
return `$${value.toLocaleString('en-US')}`;
|
||||||
}
|
}
|
||||||
if (currency === 'EUR') {
|
if (currency && currency.toLocaleLowerCase() === 'eur') {
|
||||||
return `€\u2009${value.toLocaleString('no-NO')}`;
|
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 type { FC } from 'react';
|
||||||
import { BillingInvoice } from './BillingInvoice/BillingInvoice.tsx';
|
import { BillingInvoice } from './BillingInvoice/BillingInvoice.tsx';
|
||||||
import { useDetailedInvoices } from 'hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts';
|
import { useDetailedInvoices } from 'hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts';
|
||||||
|
import { TablePlaceholder } from 'component/common/Table';
|
||||||
|
|
||||||
const StyledContainer = styled(Box)(({ theme }) => ({
|
const StyledContainer = styled(Box)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -9,22 +10,29 @@ const StyledContainer = styled(Box)(({ theme }) => ({
|
|||||||
gap: theme.spacing(3),
|
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 = () => {
|
export const BillingInvoices: FC = () => {
|
||||||
const { invoices } = useDetailedInvoices();
|
const { invoices, loading } = useDetailedInvoices();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<StyledHeader>Usage and invoices</StyledHeader>
|
{invoices.length > 0 ? (
|
||||||
{invoices.map((invoice) => (
|
<>
|
||||||
<BillingInvoice key={invoice.invoiceDate} {...invoice} />
|
{invoices.map((invoice) => (
|
||||||
))}
|
<BillingInvoice
|
||||||
|
key={invoice.invoiceDate}
|
||||||
|
{...invoice}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<TablePlaceholder>
|
||||||
|
There are no invoices or estimates available right now.
|
||||||
|
</TablePlaceholder>
|
||||||
|
)}
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,78 +21,5 @@ export const useDetailedInvoices = (options: SWRConfiguration = {}) => {
|
|||||||
|
|
||||||
const invoices = useMemo(() => data?.invoices ?? [], [data]);
|
const invoices = useMemo(() => data?.invoices ?? [], [data]);
|
||||||
|
|
||||||
// return { invoices, error, loading: isLoading };
|
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,
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user