From 4ed138c15101e6d2ed93cdf3d1941718219ad8c3 Mon Sep 17 00:00:00 2001
From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com>
Date: Wed, 8 Oct 2025 14:11:24 +0200
Subject: [PATCH] Billing info updates (#10761)
---
.../src/component/admin/billing/Billing.tsx | 27 +++-
.../BillingPlan/BillingPlan.tsx | 1 +
.../admin/billing/BillingInfo/BillingInfo.tsx | 127 ++++++++++++++++++
.../BillingInvoice/BillingInvoice.styles.tsx | 1 +
.../BillingInvoiceFooter.tsx | 2 +-
.../BillingInvoice/formatCurrency.test.ts | 8 ++
.../BillingInvoice/formatCurrency.ts | 4 +-
.../BillingInvoices/BillingInvoices.tsx | 34 +++--
.../useDetailedInvoices.ts | 75 +----------
9 files changed, 186 insertions(+), 93 deletions(-)
create mode 100644 frontend/src/component/admin/billing/BillingInfo/BillingInfo.tsx
diff --git a/frontend/src/component/admin/billing/Billing.tsx b/frontend/src/component/admin/billing/Billing.tsx
index 407c44a17c..2329f1ef25 100644
--- a/frontend/src/component/admin/billing/Billing.tsx
+++ b/frontend/src/component/admin/billing/Billing.tsx
@@ -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),
})}
>
-
-
+ Usage and invoices
+
+
+
+
+
+
);
}
diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx
index bfeda64da0..82f6a28117 100644
--- a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx
+++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx
@@ -101,6 +101,7 @@ export const BillingPlan = () => {
return (
+ {JSON.stringify({ isPAYG })}
({
+ 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 = () => (
+
+
+ Get in touch with us
+ {' '}
+ for any clarification
+
+);
+
+export const BillingInfo: FC = () => {
+ const { instanceStatus } = useInstanceStatus();
+ const {
+ uiConfig: { billing },
+ } = useUiConfig();
+
+ if (!instanceStatus) {
+ return (
+
+
+ Your billing is managed by Unleash
+
+
+
+ );
+ }
+
+ 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 (
+
+
+ Your billing is managed by Unleash
+
+
+
+ );
+ }
+
+ return (
+
+ Billing details
+
+ Current plan{' '}
+
+ {plan}
+ {isPAYG || isEnterpriseConsumption ? ' Pay-as-You-Go' : ''}
+ {isEnterpriseConsumption ? ' Consumption' : ''}
+
+
+
+ Plan price{' '}
+ {/* FIXME: where to take data from? */}
+ $450 / month
+
+
+ }
+ >
+ {!inactive ? 'Edit billing details' : 'Add billing details'}
+
+
+ {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.'}
+
+
+
+ );
+};
diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.styles.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.styles.tsx
index 4512d796ea..a7d0950111 100644
--- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.styles.tsx
+++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoice.styles.tsx
@@ -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 }) => ({
diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx
index 161055e5b2..1ec561f09d 100644
--- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx
+++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/BillingInvoiceFooter/BillingInvoiceFooter.tsx
@@ -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}` } : {}),
}));
diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.test.ts b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.test.ts
index 9677df8ccf..d057427973 100644
--- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.test.ts
+++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.test.ts
@@ -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', () => {
diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.ts b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.ts
index 3253d932de..2d243c66aa 100644
--- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.ts
+++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoice/formatCurrency.ts
@@ -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')}`;
}
diff --git a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoices.tsx b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoices.tsx
index 220dc400b5..6ffb6bfa54 100644
--- a/frontend/src/component/admin/billing/BillingInvoices/BillingInvoices.tsx
+++ b/frontend/src/component/admin/billing/BillingInvoices/BillingInvoices.tsx
@@ -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 (
- Usage and invoices
- {invoices.map((invoice) => (
-
- ))}
+ {invoices.length > 0 ? (
+ <>
+ {invoices.map((invoice) => (
+
+ ))}
+ >
+ ) : (
+
+ There are no invoices or estimates available right now.
+
+ )}
);
};
diff --git a/frontend/src/hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts b/frontend/src/hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts
index 70f9c87acf..be6e53e34a 100644
--- a/frontend/src/hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts
+++ b/frontend/src/hooks/api/getters/useDetailedInvoices/useDetailedInvoices.ts
@@ -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 };
};