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 (
-
+
{
+ setDialogOpen(false);
+ }}
+ title={`Your free ${instanceStatus.plan} trial has expired!`}
+ >
+
+ Please inform your admin to Upgrade trial or
+ your account will be deleted.
+
+
+ );
+};
+
+export const InstanceStatus: FC = ({ children }) => {
+ const { instanceStatus, refetchInstanceStatus, isBilling } =
+ useInstanceStatus();
+ const { extendTrial } = useInstanceStatusApi();
+ const { setToastApiError } = useToast();
+
+ const onExtendTrial = async () => {
+ if (
+ instanceStatus?.state === InstanceState.TRIAL &&
+ !instanceStatus?.trialExtended
+ ) {
+ try {
+ await extendTrial();
+ await refetchInstanceStatus();
+ } catch (error: unknown) {
+ setToastApiError(formatUnknownError(error));
+ }
+ }
+ };
+
+ return (
+
(
-
+ <>
+
+
+ >
)}
/>
{children}
diff --git a/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx b/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx
index 33b9e3c883..726e788b85 100644
--- a/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx
+++ b/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx
@@ -1,5 +1,5 @@
import { InstanceStatusBar } from 'component/common/InstanceStatus/InstanceStatusBar';
-import { InstanceState } from 'interfaces/instance';
+import { InstancePlan, InstanceState } from 'interfaces/instance';
import { render } from 'utils/testRenderer';
import { screen } from '@testing-library/react';
import { addDays } from 'date-fns';
@@ -18,7 +18,7 @@ test('InstanceStatusBar should be hidden when the trial is far from expired', as
render(
{
render(
{
+ const { hasAccess } = useContext(AccessContext);
+
const trialDaysRemaining = calculateTrialDaysRemaining(instanceStatus);
if (
@@ -20,14 +26,22 @@ export const InstanceStatusBar = ({
trialDaysRemaining <= 0
) {
return (
-
-
-
- Heads up! Your free trial of the{' '}
- {instanceStatus.plan.toUpperCase()} version has expired.
-
-
-
+
+
+ ({
+ fontSize: theme.fontSizes.smallBody,
+ })}
+ >
+ Warning! Your free {instanceStatus.plan}{' '}
+ trial has expired. Upgrade trial otherwise
+ your account will be deleted.
+
+ }
+ />
+
);
}
@@ -37,54 +51,67 @@ export const InstanceStatusBar = ({
trialDaysRemaining <= 10
) {
return (
-
+
-
+ ({
+ fontSize: theme.fontSizes.smallBody,
+ })}
+ >
Heads up! You have{' '}
- {trialDaysRemaining} days remaining of your
- free trial of the {instanceStatus.plan.toUpperCase()}{' '}
- version.
-
-
-
+ {trialDaysRemaining} days left of your free{' '}
+ {instanceStatus.plan} trial.
+
+ }
+ />
+
);
}
return null;
};
-const ContactButton = () => {
+const UpgradeButton = () => {
+ const navigate = useNavigate();
+
return (
navigate('/admin/billing')}
variant="outlined"
>
- Contact us
+ Upgrade trial
);
};
-const calculateTrialDaysRemaining = (
- instanceStatus: IInstanceStatus
-): number | undefined => {
- return instanceStatus.trialExpiry
- ? differenceInDays(parseISO(instanceStatus.trialExpiry), new Date())
- : undefined;
-};
-
-// TODO - Cleanup to use theme instead of colors
-const StyledBar = styled('aside')(({ theme }) => ({
+const StyledWarningBar = styled('aside')(({ theme }) => ({
position: 'relative',
zIndex: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
- padding: theme.spacing(2),
+ padding: theme.spacing(1),
gap: theme.spacing(1),
borderBottom: '1px solid',
- borderColor: colors.blue[200],
- background: colors.blue[50],
- color: colors.blue[700],
+ borderColor: theme.palette.warning.border,
+ background: theme.palette.warning.light,
+ color: theme.palette.warning.dark,
+}));
+
+const StyledInfoBar = styled('aside')(({ theme }) => ({
+ position: 'relative',
+ zIndex: 1,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ padding: theme.spacing(1),
+ gap: theme.spacing(1),
+ borderBottom: '1px solid',
+ borderColor: theme.palette.info.border,
+ background: theme.palette.info.light,
+ color: theme.palette.info.dark,
}));
const StyledButton = styled(Button)(({ theme }) => ({
@@ -93,7 +120,10 @@ const StyledButton = styled(Button)(({ theme }) => ({
marginLeft: theme.spacing(2),
}));
-// TODO - Cleanup to use theme instead of colors
-const StyledInfoIcon = styled(Info)(({ theme }) => ({
- color: colors.blue[500],
+const StyledWarningIcon = styled(WarningAmber)(({ theme }) => ({
+ color: theme.palette.warning.main,
+}));
+
+const StyledInfoIcon = styled(InfoOutlined)(({ theme }) => ({
+ color: theme.palette.info.main,
}));
diff --git a/frontend/src/component/common/InstanceStatus/__snapshots__/InstanceStatusBar.test.tsx.snap b/frontend/src/component/common/InstanceStatus/__snapshots__/InstanceStatusBar.test.tsx.snap
index 1e109b9e48..4db5371e0d 100644
--- a/frontend/src/component/common/InstanceStatus/__snapshots__/InstanceStatusBar.test.tsx.snap
+++ b/frontend/src/component/common/InstanceStatus/__snapshots__/InstanceStatusBar.test.tsx.snap
@@ -2,59 +2,63 @@
exports[`InstanceStatusBar should warn when the trial has expired 1`] = `
`;
exports[`InstanceStatusBar should warn when the trial is about to expire 1`] = `
`;
diff --git a/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx b/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx
index 140ce9e0bf..bfeb186d58 100644
--- a/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx
+++ b/frontend/src/component/common/Table/cells/TextCell/TextCell.tsx
@@ -1,19 +1,20 @@
-import { VFC, ReactNode } from 'react';
+import { FC } from 'react';
import { Box } from '@mui/material';
-interface IDateCellProps {
- children?: ReactNode;
+interface ITextCellProps {
+ value?: string | null;
}
-export const TextCell: VFC = ({ children }) => {
- if (!children) {
+export const TextCell: FC = ({ value, children }) => {
+ const text = children ?? value;
+ if (!text) {
return ;
}
return (
- {children}
+ {text}
);
diff --git a/frontend/src/component/menu/Header/Header.tsx b/frontend/src/component/menu/Header/Header.tsx
index fb81420545..b37d4bcd06 100644
--- a/frontend/src/component/menu/Header/Header.tsx
+++ b/frontend/src/component/menu/Header/Header.tsx
@@ -23,6 +23,8 @@ import { useAuthPermissions } from 'hooks/api/getters/useAuth/useAuthPermissions
import { useStyles } from './Header.styles';
import classNames from 'classnames';
import { useId } from 'hooks/useId';
+import { useInstanceStatus } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
+import { IRoute } from 'interfaces/route';
const Header: VFC = () => {
const theme = useTheme();
@@ -55,12 +57,15 @@ const Header: VFC = () => {
}
}, [permissions]);
+ const { isBilling } = useInstanceStatus();
const routes = getRoutes();
const filteredMainRoutes = {
mainNavRoutes: routes.mainNavRoutes.filter(filterByFlags(flags)),
mobileRoutes: routes.mobileRoutes.filter(filterByFlags(flags)),
- adminRoutes: routes.adminRoutes.filter(filterByFlags(flags)),
+ adminRoutes: routes.adminRoutes.filter(
+ filterByFlags(flags) && filterByBilling(isBilling)
+ ),
};
if (smallScreen) {
@@ -191,4 +196,7 @@ const Header: VFC = () => {
);
};
+export const filterByBilling = (isBilling?: boolean) => (route: IRoute) =>
+ !route.menu.isBilling || isBilling;
+
export default Header;
diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap
index 41c5f6c22d..c969705c34 100644
--- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap
+++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap
@@ -407,6 +407,25 @@ exports[`returns all baseRoutes 1`] = `
"title": "Single sign-on",
"type": "protected",
},
+ {
+ "component": [Function],
+ "menu": {
+ "adminSettings": true,
+ "isBilling": true,
+ },
+ "parent": "/admin",
+ "path": "/admin/billing",
+ "title": "Billing",
+ "type": "protected",
+ },
+ {
+ "component": [Function],
+ "menu": {},
+ "parent": "/admin",
+ "path": "/admin-invoices",
+ "title": "Invoices",
+ "type": "protected",
+ },
{
"component": [Function],
"hidden": false,
diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts
index a9d845575c..673359fa53 100644
--- a/frontend/src/component/menu/routes.ts
+++ b/frontend/src/component/menu/routes.ts
@@ -7,9 +7,9 @@ import { TagTypeList } from 'component/tags/TagTypeList/TagTypeList';
import { AddonList } from 'component/addons/AddonList/AddonList';
import Admin from 'component/admin';
import AdminApi from 'component/admin/api';
-import AdminInvoice from 'component/admin/invoice/InvoiceAdminPage';
import AdminUsers from 'component/admin/users/UsersAdmin';
import { AuthSettings } from 'component/admin/auth/AuthSettings';
+import { Billing } from 'component/admin/billing/Billing';
import Login from 'component/user/Login/Login';
import { C, EEA, P, RE, SE } from 'component/common/flags';
import { NewUser } from 'component/user/NewUser/NewUser';
@@ -52,6 +52,7 @@ import { EditSegment } from 'component/segments/EditSegment/EditSegment';
import { SegmentsList } from 'component/segments/SegmentList/SegmentList';
import { IRoute } from 'interfaces/route';
import { EnvironmentTable } from 'component/environments/EnvironmentTable/EnvironmentTable';
+import RedirectAdminInvoices from 'component/admin/billing/RedirectAdminInvoices/RedirectAdminInvoices';
export const routes: IRoute[] = [
// Splash
@@ -455,12 +456,20 @@ export const routes: IRoute[] = [
menu: { adminSettings: true },
},
{
- path: '/admin-invoices',
- title: 'Invoices',
- component: AdminInvoice,
- hidden: true,
+ path: '/admin/billing',
+ parent: '/admin',
+ title: 'Billing',
+ component: Billing,
type: 'protected',
- menu: { adminSettings: true },
+ menu: { adminSettings: true, isBilling: true },
+ },
+ {
+ path: '/admin-invoices',
+ parent: '/admin',
+ title: 'Invoices',
+ component: RedirectAdminInvoices,
+ type: 'protected',
+ menu: {},
},
{
path: '/admin',
diff --git a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap
index 18c03eda96..7426caed56 100644
--- a/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap
+++ b/frontend/src/component/tags/TagTypeList/__tests__/__snapshots__/TagTypeList.test.tsx.snap
@@ -67,7 +67,7 @@ exports[`renders an empty list correctly 1`] = `
/>
{
+ const { makeRequest, createRequest, errors, loading } = useAPI({
+ propagateErrors: true,
+ });
+
+ const extendTrial = async (): Promise => {
+ const path = 'api/instance/extend';
+ const req = createRequest(path, { method: 'POST' }, 'extendTrial');
+ await makeRequest(req.caller, req.id);
+ };
+
+ return {
+ extendTrial,
+ loading,
+ errors,
+ };
+};
+
+export default useInstanceStatusApi;
diff --git a/frontend/src/hooks/api/getters/useInstanceStatus/useInstanceStatus.ts b/frontend/src/hooks/api/getters/useInstanceStatus/useInstanceStatus.ts
index c9f2ed73b3..48e0279a0e 100644
--- a/frontend/src/hooks/api/getters/useInstanceStatus/useInstanceStatus.ts
+++ b/frontend/src/hooks/api/getters/useInstanceStatus/useInstanceStatus.ts
@@ -1,31 +1,51 @@
-import { IInstanceStatus } from 'interfaces/instance';
+import { IInstanceStatus, InstancePlan } from 'interfaces/instance';
import { useApiGetter } from 'hooks/api/getters/useApiGetter/useApiGetter';
import { formatApiPath } from 'utils/formatPath';
-import { isLocalhostDomain } from 'utils/env';
+import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
+import { useEffect } from 'react';
export interface IUseInstanceStatusOutput {
instanceStatus?: IInstanceStatus;
refetchInstanceStatus: () => void;
+ isBilling: boolean;
loading: boolean;
error?: Error;
}
export const useInstanceStatus = (): IUseInstanceStatusOutput => {
+ const { uiConfig } = useUiConfig();
+ const {
+ flags: { UNLEASH_CLOUD },
+ } = uiConfig;
+
const { data, refetch, loading, error } = useApiGetter(
'useInstanceStatus',
- fetchInstanceStatus
+ () => fetchInstanceStatus(UNLEASH_CLOUD)
);
+ useEffect(() => {
+ refetch();
+ }, [refetch, UNLEASH_CLOUD]);
+
+ const billingPlans = [
+ InstancePlan.PRO,
+ InstancePlan.COMPANY,
+ InstancePlan.TEAM,
+ ];
+
return {
instanceStatus: data,
refetchInstanceStatus: refetch,
+ isBilling: billingPlans.includes(data?.plan ?? InstancePlan.UNKNOWN),
loading,
error,
};
};
-const fetchInstanceStatus = async (): Promise => {
- if (!enableInstanceStatusBarFeature()) {
+const fetchInstanceStatus = async (
+ UNLEASH_CLOUD?: boolean
+): Promise => {
+ if (!UNLEASH_CLOUD) {
return UNKNOWN_INSTANCE_STATUS;
}
@@ -38,11 +58,6 @@ const fetchInstanceStatus = async (): Promise => {
return res.json();
};
-// TODO(olav): Enable instance status bar feature outside of localhost.
-const enableInstanceStatusBarFeature = () => {
- return isLocalhostDomain();
-};
-
export const UNKNOWN_INSTANCE_STATUS: IInstanceStatus = {
- plan: 'unknown',
+ plan: InstancePlan.UNKNOWN,
};
diff --git a/frontend/src/hooks/api/getters/useUiConfig/defaultValue.ts b/frontend/src/hooks/api/getters/useUiConfig/defaultValue.ts
index d0cb77ed38..1ad5b11b71 100644
--- a/frontend/src/hooks/api/getters/useUiConfig/defaultValue.ts
+++ b/frontend/src/hooks/api/getters/useUiConfig/defaultValue.ts
@@ -14,6 +14,7 @@ export const defaultValue = {
CO: false,
SE: false,
T: false,
+ UNLEASH_CLOUD: false,
},
links: [
{
diff --git a/frontend/src/hooks/useUsersPlan.ts b/frontend/src/hooks/useUsersPlan.ts
new file mode 100644
index 0000000000..c8209e7400
--- /dev/null
+++ b/frontend/src/hooks/useUsersPlan.ts
@@ -0,0 +1,49 @@
+import { IUser } from 'interfaces/user';
+import { useMemo } from 'react';
+import { useInstanceStatus } from './api/getters/useInstanceStatus/useInstanceStatus';
+import { STRIPE } from 'component/admin/billing/flags';
+import { InstancePlan } from 'interfaces/instance';
+
+export interface IUsersPlanOutput {
+ planUsers: IUser[];
+ isBillingUsers: boolean;
+}
+
+export const useUsersPlan = (users: IUser[]): IUsersPlanOutput => {
+ const { instanceStatus } = useInstanceStatus();
+
+ const isBillingUsers = STRIPE && instanceStatus?.plan === InstancePlan.PRO;
+
+ const planUsers = useMemo(
+ () => calculatePaidUsers(users, isBillingUsers, instanceStatus?.seats),
+ [users, isBillingUsers, instanceStatus?.seats]
+ );
+
+ return {
+ planUsers,
+ isBillingUsers,
+ };
+};
+
+const calculatePaidUsers = (
+ users: IUser[],
+ isBillingUsers: boolean,
+ seats: number = 0
+) => {
+ if (!isBillingUsers || !seats) return users;
+
+ users
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt))
+ .forEach((user, index) => {
+ user.paid = false;
+
+ // If index is greater or equal to seat, the
+ // user isn't paid for and we will add use this
+ // to add costs and icons in the userlist
+ if (index >= seats) {
+ user.paid = true;
+ }
+ });
+
+ return users;
+};
diff --git a/frontend/src/interfaces/instance.ts b/frontend/src/interfaces/instance.ts
index ed9be47e76..6f04f759eb 100644
--- a/frontend/src/interfaces/instance.ts
+++ b/frontend/src/interfaces/instance.ts
@@ -1,10 +1,11 @@
export interface IInstanceStatus {
- plan: string;
+ plan: InstancePlan;
trialExpiry?: string;
trialStart?: string;
trialExtended?: number;
billingCenter?: string;
state?: InstanceState;
+ seats?: number;
}
export enum InstanceState {
@@ -14,3 +15,10 @@ export enum InstanceState {
EXPIRED = 'EXPIRED',
CHURNED = 'CHURNED',
}
+
+export enum InstancePlan {
+ PRO = 'Pro',
+ COMPANY = 'Company',
+ TEAM = 'Team',
+ UNKNOWN = 'Unknown',
+}
diff --git a/frontend/src/interfaces/invoice.ts b/frontend/src/interfaces/invoice.ts
index db49e34d9b..82790c14cd 100644
--- a/frontend/src/interfaces/invoice.ts
+++ b/frontend/src/interfaces/invoice.ts
@@ -1,5 +1,5 @@
export interface IInvoice {
- amountFomratted: string;
+ amountFormatted: string;
invoicePDF: string;
invoiceURL: string;
paid: boolean;
diff --git a/frontend/src/interfaces/route.ts b/frontend/src/interfaces/route.ts
index 495237f463..c924eb31c4 100644
--- a/frontend/src/interfaces/route.ts
+++ b/frontend/src/interfaces/route.ts
@@ -16,4 +16,5 @@ interface IRouteMenu {
mobile?: boolean;
advanced?: boolean;
adminSettings?: boolean;
+ isBilling?: boolean;
}
diff --git a/frontend/src/interfaces/uiConfig.ts b/frontend/src/interfaces/uiConfig.ts
index 4cd9a3a613..ea28afda0d 100644
--- a/frontend/src/interfaces/uiConfig.ts
+++ b/frontend/src/interfaces/uiConfig.ts
@@ -29,6 +29,7 @@ export interface IFlags {
CO?: boolean;
SE?: boolean;
T?: boolean;
+ UNLEASH_CLOUD?: boolean;
}
export interface IVersionInfo {
diff --git a/frontend/src/interfaces/user.ts b/frontend/src/interfaces/user.ts
index 4ccb3ad1e8..4357a5b830 100644
--- a/frontend/src/interfaces/user.ts
+++ b/frontend/src/interfaces/user.ts
@@ -11,6 +11,7 @@ export interface IUser {
seenAt: string | null;
username?: string;
isAPI: boolean;
+ paid?: boolean;
}
export interface IPermission {
diff --git a/frontend/src/themes/app.css b/frontend/src/themes/app.css
index 2cb24825e7..9867297ea3 100644
--- a/frontend/src/themes/app.css
+++ b/frontend/src/themes/app.css
@@ -135,6 +135,7 @@ p {
}
a {
+ cursor: pointer;
color: #615bc2;
}
diff --git a/frontend/src/themes/theme.ts b/frontend/src/themes/theme.ts
index 858143d202..940701e919 100644
--- a/frontend/src/themes/theme.ts
+++ b/frontend/src/themes/theme.ts
@@ -13,6 +13,7 @@ export default createTheme({
},
boxShadows: {
main: '0px 2px 4px rgba(129, 122, 254, 0.2)',
+ elevated: '0px 1px 20px rgba(45, 42, 89, 0.1)',
},
typography: {
fontFamily: 'Sen, Roboto, sans-serif',
@@ -61,26 +62,31 @@ export default createTheme({
light: colors.blue[50],
main: colors.blue[500],
dark: colors.blue[700],
+ border: colors.blue[200],
},
success: {
light: colors.green[50],
main: colors.green[500],
dark: colors.green[800],
+ border: colors.green[300],
},
warning: {
light: colors.orange[100],
main: colors.orange[800],
dark: colors.orange[900],
+ border: colors.orange[500],
},
error: {
light: colors.red[50],
main: colors.red[700],
dark: colors.red[800],
+ border: colors.red[300],
},
divider: colors.grey[300],
- dividerAlternative: colors.grey[500],
+ dividerAlternative: colors.grey[400],
tableHeaderHover: colors.grey[400],
highlight: '#FFEACC',
+ secondaryContainer: colors.grey[200],
sidebarContainer: 'rgba(32,32,33, 0.2)',
grey: colors.grey,
text: {
diff --git a/frontend/src/themes/themeTypes.ts b/frontend/src/themes/themeTypes.ts
index 2e84b2ceb9..29742fa030 100644
--- a/frontend/src/themes/themeTypes.ts
+++ b/frontend/src/themes/themeTypes.ts
@@ -24,6 +24,7 @@ declare module '@mui/material/styles' {
*/
boxShadows: {
main: string;
+ elevated: string;
};
}
@@ -65,7 +66,11 @@ declare module '@mui/material/styles' {
*/
highlight: string;
/**
- * For sidebar container background.
+ * Background color for secondary containers.
+ */
+ secondaryContainer: string;
+ /**
+ * Background color for sidebar containers.
*/
sidebarContainer: string;
/**
@@ -83,6 +88,19 @@ declare module '@mui/material/styles' {
interface Palette extends CustomPalette {}
interface PaletteOptions extends CustomPalette {}
+
+ interface PaletteColor {
+ light: string;
+ main: string;
+ dark: string;
+ border: string;
+ }
+ interface PaletteColorOptions {
+ light?: string;
+ main?: string;
+ dark?: string;
+ border?: string;
+ }
}
declare module '@mui/system/createTheme/shape' {
diff --git a/frontend/src/utils/billing.ts b/frontend/src/utils/billing.ts
new file mode 100644
index 0000000000..ec354604bd
--- /dev/null
+++ b/frontend/src/utils/billing.ts
@@ -0,0 +1,10 @@
+import { differenceInDays, parseISO } from 'date-fns';
+import { IInstanceStatus } from 'interfaces/instance';
+
+export const calculateTrialDaysRemaining = (
+ instanceStatus?: IInstanceStatus
+): number | undefined => {
+ return instanceStatus?.trialExpiry
+ ? differenceInDays(parseISO(instanceStatus.trialExpiry), new Date())
+ : undefined;
+};