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 }; };