diff --git a/frontend/src/component/admin/billing/Billing.tsx b/frontend/src/component/admin/billing/Billing.tsx new file mode 100644 index 0000000000..c57975f5a1 --- /dev/null +++ b/frontend/src/component/admin/billing/Billing.tsx @@ -0,0 +1,48 @@ +import AdminMenu from '../menu/AdminMenu'; +import { PageContent } from 'component/common/PageContent/PageContent'; +import { useContext } from 'react'; +import { ADMIN } from 'component/providers/AccessProvider/permissions'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import AccessContext from 'contexts/AccessContext'; +import { AdminAlert } from 'component/common/AdminAlert/AdminAlert'; +import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus'; +import { Alert } from '@mui/material'; +import { BillingDashboard } from './BillingDashboard/BillingDashboard'; +import { BillingHistory } from './BillingHistory/BillingHistory'; +import useInvoices from 'hooks/api/getters/useInvoices/useInvoices'; + +export const Billing = () => { + const { instanceStatus, isBilling } = useInstanceStatus(); + const { invoices } = useInvoices(); + const { hasAccess } = useContext(AccessContext); + + return ( +
+ + + ( + <> + + + + )} + elseShow={() => } + /> + } + elseShow={ + + Billing is not enabled for this instance. + + } + /> + +
+ ); +}; diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingDashboard.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingDashboard.tsx new file mode 100644 index 0000000000..179f1d2bb3 --- /dev/null +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingDashboard.tsx @@ -0,0 +1,20 @@ +import { Grid } from '@mui/material'; +import { IInstanceStatus } from 'interfaces/instance'; +import { VFC } from 'react'; +import { BillingInformation } from './BillingInformation/BillingInformation'; +import { BillingPlan } from './BillingPlan/BillingPlan'; + +interface IBillingDashboardProps { + instanceStatus: IInstanceStatus; +} + +export const BillingDashboard: VFC = ({ + instanceStatus, +}) => { + return ( + + + + + ); +}; diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformation.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformation.tsx new file mode 100644 index 0000000000..cca43c7ae2 --- /dev/null +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformation.tsx @@ -0,0 +1,70 @@ +import { FC } from 'react'; +import { Alert, Divider, Grid, styled, Typography } from '@mui/material'; +import { BillingInformationButton } from './BillingInformationButton/BillingInformationButton'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { IInstanceStatus, InstanceState } from 'interfaces/instance'; + +interface IBillingInformationProps { + instanceStatus: IInstanceStatus; +} + +export const BillingInformation: FC = ({ + instanceStatus, +}) => { + const inactive = instanceStatus.state !== InstanceState.ACTIVE; + + return ( + + + Billing Information + + In order to Upgrade trial you need + to provide us your billing information. + + } + /> + + + {inactive + ? 'Once we have received your billing information we will upgrade your trial within 1 business day' + : 'These changes may take up to 1 business day and they will be visible on your next invoice'} + + + + + Get in touch with us + {' '} + for any clarification + + + + ); +}; + +const StyledInfoBox = styled('aside')(({ theme }) => ({ + padding: theme.spacing(4), + height: '100%', + borderRadius: theme.shape.borderRadiusLarge, + backgroundColor: theme.palette.secondaryContainer, +})); + +const StyledTitle = styled(Typography)(({ theme }) => ({ + marginBottom: theme.spacing(4), +})); + +const StyledAlert = styled(Alert)(({ theme }) => ({ + marginBottom: theme.spacing(4), +})); + +const StyledInfoLabel = styled(Typography)(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + color: theme.palette.text.secondary, +})); + +const StyledDivider = styled(Divider)(({ theme }) => ({ + margin: `${theme.spacing(2.5)} 0`, + borderColor: theme.palette.dividerAlternative, +})); diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformationButton/BillingInformationButton.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformationButton/BillingInformationButton.tsx new file mode 100644 index 0000000000..82e0ba1c87 --- /dev/null +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingInformation/BillingInformationButton/BillingInformationButton.tsx @@ -0,0 +1,36 @@ +import { Button, styled } from '@mui/material'; +import { VFC } from 'react'; + +const href = `mailto:elise@getunleash.ai?subject=Continue with Unleash&body=Hi Unleash,%0D%0A%0D%0A +I would like to continue with Unleash.%0D%0A%0D%0A%0D%0A%0D%0A + +Billing information:%0D%0A%0D%0A + +1. Company name (legal name): [add your information here]%0D%0A%0D%0A +2. Email address (where we will send the invoice): [add your information here]%0D%0A%0D%0A +3. Address: [add your information here]%0D%0A%0D%0A +4. Country: [add your information here]%0D%0A%0D%0A +5. VAT ID (optional - only European countries): [add your information here]%0D%0A%0D%0A%0D%0A%0D%0A%0D%0A + + + +-- Thank you for signing up. We will upgrade your trial as quick as possible and we will grant you access to the application again. --`; + +interface IBillingInformationButtonProps { + update?: boolean; +} + +export const BillingInformationButton: VFC = ({ + update, +}) => { + return ( + + {update ? 'Update billing information' : 'Add billing information'} + + ); +}; + +const StyledButton = styled(Button)(({ theme }) => ({ + width: '100%', + marginBottom: theme.spacing(1.5), +})); diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx new file mode 100644 index 0000000000..ca2acc67c4 --- /dev/null +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/BillingPlan.tsx @@ -0,0 +1,256 @@ +import { FC } from 'react'; +import { Alert, Divider, Grid, styled, Typography } from '@mui/material'; +import { Link } from 'react-router-dom'; +import CheckIcon from '@mui/icons-material/Check'; +import useUsers from 'hooks/api/getters/useUsers/useUsers'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { + IInstanceStatus, + InstanceState, + InstancePlan, +} from 'interfaces/instance'; +import { calculateTrialDaysRemaining } from 'utils/billing'; +import { GridRow } from 'component/common/GridRow/GridRow'; +import { GridCol } from 'component/common/GridCol/GridCol'; +import { GridColLink } from './GridColLink/GridColLink'; +import { STRIPE } from 'component/admin/billing/flags'; + +interface IBillingPlanProps { + instanceStatus: IInstanceStatus; +} + +export const BillingPlan: FC = ({ instanceStatus }) => { + const { users } = useUsers(); + const trialDaysRemaining = calculateTrialDaysRemaining(instanceStatus); + + const price = { + [InstancePlan.PRO]: 80, + [InstancePlan.COMPANY]: 0, + [InstancePlan.TEAM]: 0, + [InstancePlan.UNKNOWN]: 0, + user: 15, + }; + + const statusExpired = + instanceStatus.state === InstanceState.TRIAL && + typeof trialDaysRemaining === 'number' && + trialDaysRemaining <= 0; + + const planPrice = price[instanceStatus.plan]; + const seats = instanceStatus.seats ?? 5; + const freeAssigned = Math.min(users.length, seats); + const paidAssigned = users.length - freeAssigned; + const paidAssignedPrice = price.user * paidAssigned; + const finalPrice = planPrice + paidAssignedPrice; + const inactive = instanceStatus.state !== InstanceState.ACTIVE; + + return ( + + + + After you have sent your billing information, your + instance will be upgraded - you don't have to do + anything.{' '} + + Get in touch with us + {' '} + for any clarification + + } + /> + Current plan + + ({ marginBottom: theme.spacing(3) })}> + + + {instanceStatus.plan} + + ({ + color: statusExpired + ? theme.palette.error.dark + : theme.palette.warning.dark, + })} + > + {statusExpired + ? 'Trial expired' + : instanceStatus.trialExtended + ? 'Extended Trial' + : 'Trial'} + + } + /> + + + 0} + show={ + + ${planPrice.toFixed(2)} + + } + /> + + + + + + ({ + marginBottom: theme.spacing(1.5), + })} + > + + + {seats} team + members + + + {freeAssigned} assigned + + + + + + + + included + + + + + + + Paid members + + + {paidAssigned} assigned + + + + + Add up to 15 extra paid members - $ + {price.user} + /month per member + + + + ({ + fontSize: + theme.fontSizes.mainHeader, + })} + > + ${paidAssignedPrice.toFixed(2)} + + + + + + + + + ({ + fontWeight: + theme.fontWeight.bold, + fontSize: + theme.fontSizes.mainHeader, + })} + > + Total per month + + + + ({ + fontWeight: + theme.fontWeight.bold, + fontSize: '2rem', + })} + > + ${finalPrice.toFixed(2)} + + + + + + } + /> + + + ); +}; + +const StyledPlanBox = styled('aside')(({ theme }) => ({ + padding: theme.spacing(2.5), + height: '100%', + borderRadius: theme.shape.borderRadiusLarge, + boxShadow: theme.boxShadows.elevated, + [theme.breakpoints.up('md')]: { + padding: theme.spacing(6.5), + }, +})); + +const StyledInfoLabel = styled(Typography)(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + color: theme.palette.text.secondary, +})); + +const StyledPlanBadge = styled('span')(({ theme }) => ({ + padding: `${theme.spacing(0.5)} ${theme.spacing(1)}`, + borderRadius: theme.shape.borderRadiusLarge, + fontSize: theme.fontSizes.smallerBody, + backgroundColor: theme.palette.statusBadge.success, + color: theme.palette.success.dark, + fontWeight: theme.fontWeight.bold, +})); + +const StyledPlanSpan = styled('span')(({ theme }) => ({ + fontSize: '3.25rem', + lineHeight: 1, + color: theme.palette.primary.main, + fontWeight: 800, +})); + +const StyledTrialSpan = styled('span')(({ theme }) => ({ + marginLeft: theme.spacing(1.5), + fontWeight: theme.fontWeight.bold, +})); + +const StyledPriceSpan = styled('span')(({ theme }) => ({ + color: theme.palette.primary.main, + fontSize: theme.fontSizes.mainHeader, + fontWeight: theme.fontWeight.bold, +})); + +const StyledAlert = styled(Alert)(({ theme }) => ({ + fontSize: theme.fontSizes.smallerBody, + marginBottom: theme.spacing(3), + marginTop: theme.spacing(-1.5), + [theme.breakpoints.up('md')]: { + marginTop: theme.spacing(-4.5), + }, +})); + +const StyledCheckIcon = styled(CheckIcon)(({ theme }) => ({ + fontSize: '1rem', + marginRight: theme.spacing(1), +})); + +const StyledDivider = styled(Divider)(({ theme }) => ({ + margin: `${theme.spacing(3)} 0`, +})); diff --git a/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/GridColLink/GridColLink.tsx b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/GridColLink/GridColLink.tsx new file mode 100644 index 0000000000..e6079a374e --- /dev/null +++ b/frontend/src/component/admin/billing/BillingDashboard/BillingPlan/GridColLink/GridColLink.tsx @@ -0,0 +1,11 @@ +import { styled } from '@mui/material'; +import { FC } from 'react'; + +export const GridColLink: FC = ({ children }) => { + return ({children}); +}; + +const StyledSpan = styled('span')(({ theme }) => ({ + fontSize: theme.fontSizes.smallBody, + marginLeft: theme.spacing(1), +})); diff --git a/frontend/src/component/admin/billing/BillingHistory/BillingHistory.tsx b/frontend/src/component/admin/billing/BillingHistory/BillingHistory.tsx new file mode 100644 index 0000000000..233108e9a1 --- /dev/null +++ b/frontend/src/component/admin/billing/BillingHistory/BillingHistory.tsx @@ -0,0 +1,118 @@ +import { + Table, + SortableTableHeader, + TableBody, + TableCell, + TableRow, + TablePlaceholder, +} from 'component/common/Table'; +import { PageContent } from 'component/common/PageContent/PageContent'; +import { DateCell } from 'component/common/Table/cells/DateCell/DateCell'; +import { useMemo, VFC } from 'react'; +import { useGlobalFilter, useSortBy, useTable } from 'react-table'; +import { sortTypes } from 'utils/sortTypes'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { Box, IconButton, styled, Typography } from '@mui/material'; +import FileDownload from '@mui/icons-material/FileDownload'; +import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; + +interface IBillingHistoryProps { + data: Record[]; + isLoading?: boolean; +} + +const columns = [ + { + Header: 'Amount', + accessor: 'amountFormatted', + }, + { + Header: 'Status', + accessor: 'status', + }, + { + Header: 'Due date', + accessor: 'dueDate', + Cell: DateCell, + sortType: 'date', + }, + { + Header: 'Download', + accessor: 'invoicePDF', + align: 'center', + disableSortBy: true, + Cell: ({ value }: { value: string }) => ( + + + + + + ), + width: 100, + }, +]; + +export const BillingHistory: VFC = ({ + data, + isLoading = false, +}) => { + const initialState = useMemo( + () => ({ + sortBy: [{ id: 'createdAt', desc: false }], + }), + [] + ); + + const { getTableProps, getTableBodyProps, headerGroups, rows, prepareRow } = + useTable( + { + columns, + data, + initialState, + sortTypes, + autoResetGlobalFilter: false, + disableSortRemove: true, + defaultColumn: { + Cell: TextCell, + }, + }, + useGlobalFilter, + useSortBy + ); + + return ( + + Payment history + + + + {rows.map(row => { + prepareRow(row); + return ( + + {row.cells.map(cell => ( + + {cell.render('Cell')} + + ))} + + ); + })} + +
+ No invoices to show.} + /> +
+ ); +}; + +const StyledTitle = styled(Typography)(({ theme }) => ({ + marginTop: theme.spacing(6), + marginBottom: theme.spacing(2.5), + fontSize: theme.fontSizes.mainHeader, +})); diff --git a/frontend/src/component/admin/billing/RedirectAdminInvoices/RedirectAdminInvoices.tsx b/frontend/src/component/admin/billing/RedirectAdminInvoices/RedirectAdminInvoices.tsx new file mode 100644 index 0000000000..07111c962e --- /dev/null +++ b/frontend/src/component/admin/billing/RedirectAdminInvoices/RedirectAdminInvoices.tsx @@ -0,0 +1,7 @@ +import { Navigate } from 'react-router-dom'; + +const RedirectAdminInvoices = () => { + return ; +}; + +export default RedirectAdminInvoices; diff --git a/frontend/src/component/admin/billing/flags.ts b/frontend/src/component/admin/billing/flags.ts new file mode 100644 index 0000000000..3ae06e3aea --- /dev/null +++ b/frontend/src/component/admin/billing/flags.ts @@ -0,0 +1 @@ +export const STRIPE = false; diff --git a/frontend/src/component/admin/invoice/InvoiceAdminPage.tsx b/frontend/src/component/admin/invoice/InvoiceAdminPage.tsx deleted file mode 100644 index f9197c0df6..0000000000 --- a/frontend/src/component/admin/invoice/InvoiceAdminPage.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { useContext } from 'react'; -import InvoiceList from './InvoiceList'; -import AccessContext from 'contexts/AccessContext'; -import { ADMIN } from 'component/providers/AccessProvider/permissions'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { Alert } from '@mui/material'; - -const InvoiceAdminPage = () => { - const { hasAccess } = useContext(AccessContext); - return ( -
- } - elseShow={ - - You need to be instance admin to access this section. - - } - /> -
- ); -}; - -export default InvoiceAdminPage; diff --git a/frontend/src/component/admin/invoice/InvoiceList.tsx b/frontend/src/component/admin/invoice/InvoiceList.tsx deleted file mode 100644 index 48e8db4317..0000000000 --- a/frontend/src/component/admin/invoice/InvoiceList.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import { useEffect, useState } from 'react'; -import { - Table, - TableHead, - TableBody, - TableRow, - TableCell, - Button, -} from '@mui/material'; -import OpenInNew from '@mui/icons-material/OpenInNew'; -import { PageContent } from 'component/common/PageContent/PageContent'; -import { PageHeader } from 'component/common/PageHeader/PageHeader'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import { formatApiPath } from 'utils/formatPath'; -import useInvoices from 'hooks/api/getters/useInvoices/useInvoices'; -import { IInvoice } from 'interfaces/invoice'; -import { useLocationSettings } from 'hooks/useLocationSettings'; -import { formatDateYMD } from 'utils/formatDate'; - -const PORTAL_URL = formatApiPath('api/admin/invoices/portal'); - -const InvoiceList = () => { - const { refetchInvoices, invoices } = useInvoices(); - const [isLoaded, setLoaded] = useState(false); - const { locationSettings } = useLocationSettings(); - - useEffect(() => { - refetchInvoices(); - setLoaded(true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - return ( - 0} - show={ - } - > - Billing portal - - } - /> - } - > -
- - - - Amount - Status - Due date - PDF - Link - - - - {invoices.map((item: IInvoice) => ( - - - {item.amountFomratted} - - - {item.status} - - - {item.dueDate && - formatDateYMD( - item.dueDate, - locationSettings.locale - )} - - - PDF - - - - Payment link - - - - ))} - -
-
-
- } - elseShow={
{isLoaded && 'No invoices to show.'}
} - /> - ); -}; -export default InvoiceList; diff --git a/frontend/src/component/admin/menu/AdminMenu.tsx b/frontend/src/component/admin/menu/AdminMenu.tsx index 319bfe28ce..bb341cf307 100644 --- a/frontend/src/component/admin/menu/AdminMenu.tsx +++ b/frontend/src/component/admin/menu/AdminMenu.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { NavLink, useLocation } from 'react-router-dom'; import { Paper, Tab, Tabs } from '@mui/material'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; +import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus'; const navLinkStyle = { display: 'flex', @@ -30,6 +31,7 @@ const createNavLinkStyle = (props: { function AdminMenu() { const { uiConfig } = useUiConfig(); const { pathname } = useLocation(); + const { isBilling } = useInstanceStatus(); const { flags } = uiConfig; return ( @@ -79,6 +81,19 @@ function AdminMenu() { } /> + {isBilling && ( + + Billing + + } + /> + )} ); diff --git a/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.tsx b/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.tsx index e0cba8f626..ff8a9f140f 100644 --- a/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.tsx +++ b/frontend/src/component/admin/users/UsersList/UserListItem/UserListItem.tsx @@ -7,7 +7,7 @@ import { Typography, } from '@mui/material'; import classnames from 'classnames'; -import { Delete, Edit, Lock } from '@mui/icons-material'; +import { Delete, Edit, Lock, MonetizationOn } from '@mui/icons-material'; import { SyntheticEvent, useContext } from 'react'; import { ADMIN } from 'component/providers/AccessProvider/permissions'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; @@ -27,6 +27,7 @@ interface IUserListItemProps { openDelDialog: (user: IUser) => (e: SyntheticEvent) => void; locationSettings: ILocationSettings; search: string; + isBillingUsers?: boolean; } const UserListItem = ({ @@ -36,6 +37,7 @@ const UserListItem = ({ openPwDialog, locationSettings, search, + isBillingUsers, }: IUserListItemProps) => { const { hasAccess } = useContext(AccessContext); const navigate = useNavigate(); @@ -43,7 +45,7 @@ const UserListItem = ({ const renderTimeAgo = (date: string) => ( + + + ({ + color: theme.palette.primary.light, + fontSize: '1.75rem', + })} + /> + + } + elseShow={Free} + /> + + } + /> {formatDateYMD(user.createdAt, locationSettings.locale)} diff --git a/frontend/src/component/admin/users/UsersList/UsersList.tsx b/frontend/src/component/admin/users/UsersList/UsersList.tsx index acc52b84bf..b71922565e 100644 --- a/frontend/src/component/admin/users/UsersList/UsersList.tsx +++ b/frontend/src/component/admin/users/UsersList/UsersList.tsx @@ -30,6 +30,7 @@ import { useUsersFilter } from 'hooks/useUsersFilter'; import { useUsersSort } from 'hooks/useUsersSort'; import { TableCellSortable } from 'component/common/Table/TableCellSortable/TableCellSortable'; import { useStyles } from './UserListItem/UserListItem.styles'; +import { useUsersPlan } from 'hooks/useUsersPlan'; interface IUsersListProps { search: string; @@ -52,7 +53,8 @@ const UsersList = ({ search }: IUsersListProps) => { const [inviteLink, setInviteLink] = useState(''); const [delUser, setDelUser] = useState(); const ref = useLoading(loading); - const { filtered, setFilter } = useUsersFilter(users); + const { planUsers, isBillingUsers } = useUsersPlan(users); + const { filtered, setFilter } = useUsersFilter(planUsers); const { sorted, sort, setSort } = useUsersSort(filtered); const filterUsersByQueryPage = (user: IUser) => { @@ -144,6 +146,7 @@ const UsersList = ({ search }: IUsersListProps) => { locationSettings={locationSettings} renderRole={renderRole} search={search} + isBillingUsers={isBillingUsers} /> ); }); @@ -156,6 +159,17 @@ const UsersList = ({ search }: IUsersListProps) => { + + Type + + } + /> void; - onClose?: (e: React.SyntheticEvent) => void; + onClose?: (e: React.SyntheticEvent, reason?: string) => void; style?: object; title: string; fullWidth?: boolean; diff --git a/frontend/src/component/common/GridCol/GridCol.tsx b/frontend/src/component/common/GridCol/GridCol.tsx new file mode 100644 index 0000000000..95b56c7be7 --- /dev/null +++ b/frontend/src/component/common/GridCol/GridCol.tsx @@ -0,0 +1,19 @@ +import { Grid } from '@mui/material'; +import { FC } from 'react'; + +export const GridCol: FC<{ vertical?: boolean }> = ({ + children, + vertical = false, +}) => { + return ( + + {children} + + ); +}; diff --git a/frontend/src/component/common/GridRow/GridRow.tsx b/frontend/src/component/common/GridRow/GridRow.tsx new file mode 100644 index 0000000000..a8c100fedb --- /dev/null +++ b/frontend/src/component/common/GridRow/GridRow.tsx @@ -0,0 +1,21 @@ +import { Grid, styled, SxProps, Theme } from '@mui/material'; +import { FC } from 'react'; + +export const GridRow: FC<{ sx?: SxProps }> = ({ sx, children }) => { + return ( + + {children} + + ); +}; + +const StyledGrid = styled(Grid)(({ theme }) => ({ + flexWrap: 'nowrap', + gap: theme.spacing(1), +})); diff --git a/frontend/src/component/common/InstanceStatus/InstanceStatus.tsx b/frontend/src/component/common/InstanceStatus/InstanceStatus.tsx index 678cb57c6b..d9cee6065d 100644 --- a/frontend/src/component/common/InstanceStatus/InstanceStatus.tsx +++ b/frontend/src/component/common/InstanceStatus/InstanceStatus.tsx @@ -1,17 +1,130 @@ import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus'; -import React, { FC } from 'react'; +import React, { FC, VFC, useEffect, useState, useContext } from 'react'; import { InstanceStatusBar } from 'component/common/InstanceStatus/InstanceStatusBar'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { Dialogue } from 'component/common/Dialogue/Dialogue'; +import { Typography } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; +import { IInstanceStatus, InstanceState } from 'interfaces/instance'; +import { ADMIN } from 'component/providers/AccessProvider/permissions'; +import AccessContext from 'contexts/AccessContext'; +import useInstanceStatusApi from 'hooks/api/actions/useInstanceStatusApi/useInstanceStatusApi'; +import { calculateTrialDaysRemaining } from 'utils/billing'; +import useToast from 'hooks/useToast'; +import { formatUnknownError } from 'utils/formatUnknownError'; -export const InstanceStatus: FC = ({ children }) => { - const { instanceStatus } = useInstanceStatus(); +interface ITrialDialogProps { + instanceStatus: IInstanceStatus; + onExtendTrial: () => Promise; +} + +const TrialDialog: VFC = ({ + instanceStatus, + onExtendTrial, +}) => { + const { hasAccess } = useContext(AccessContext); + const navigate = useNavigate(); + const trialDaysRemaining = calculateTrialDaysRemaining(instanceStatus); + + const statusExpired = + instanceStatus.state === InstanceState.TRIAL && + typeof trialDaysRemaining === 'number' && + trialDaysRemaining <= 0; + + const [dialogOpen, setDialogOpen] = useState(statusExpired); + + useEffect(() => { + setDialogOpen(statusExpired); + const interval = setInterval(() => { + setDialogOpen(statusExpired); + }, 60000); + return () => clearInterval(interval); + }, [statusExpired]); + + if (hasAccess(ADMIN)) { + return ( + { + navigate('/admin/billing'); + setDialogOpen(false); + }} + onClose={(_: any, reason?: string) => { + if ( + reason !== 'backdropClick' && + reason !== 'escapeKeyDown' + ) { + onExtendTrial(); + setDialogOpen(false); + } + }} + title={`Your free ${instanceStatus.plan} trial has expired!`} + > + + Upgrade trial otherwise your{' '} + account will be deleted. + + + ); + } return ( -