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 /> | ||||
|                 <BillingInvoices /> | ||||
|                 <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.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> | ||||
|     ); | ||||
| }; | ||||
|  | ||||
| @ -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