diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index 7ab2c5d32..fb13a6575 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -3,8 +3,18 @@ import { getLicenseServerUrl } from "../../../config"; import { licenseServerKeyRequest } from "../../../config/request"; import { EELicenseService } from "../../services"; +export const getOrganizationPlansTable = async (req: Request, res: Response) => { + const billingCycle = req.query.billingCycle as string; + + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` + ); + + return res.status(200).send(data); +} + /** - * Return the organization's current plan and allowed feature set + * Return the organization current plan's feature set */ export const getOrganizationPlan = async (req: Request, res: Response) => { const { organizationId } = req.params; @@ -18,26 +28,58 @@ export const getOrganizationPlan = async (req: Request, res: Response) => { } /** - * Update the organization plan to product with id [productId] + * Return the organization's current plan's billing info * @param req * @param res * @returns */ -export const updateOrganizationPlan = async (req: Request, res: Response) => { - const { - productId, - } = req.body; - - const { data } = await licenseServerKeyRequest.patch( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan`, - { - productId, - } +export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => { + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/billing` ); return res.status(200).send(data); } +/** + * Return the organization's current plan's feature table + * @param req + * @param res + * @returns + */ +export const getOrganizationPlanTable = async (req: Request, res: Response) => { + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan/table` + ); + + return res.status(200).send(data); +} + +export const getOrganizationBillingDetails = async (req: Request, res: Response) => { + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details` + ); + + return res.status(200).send(data); +} + +export const updateOrganizationBillingDetails = async (req: Request, res: Response) => { + const { + name, + email + } = req.body; + + const { data } = await licenseServerKeyRequest.patch( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details`, + { + ...(name ? { name } : {}), + ...(email ? { email } : {}) + } + ); + + return res.status(200).send(data); +} + /** * Return the organization's payment methods on file */ @@ -46,9 +88,7 @@ export const getOrganizationPmtMethods = async (req: Request, res: Response) => `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods` ); - return res.status(200).send({ - pmtMethods, - }); + return res.status(200).send(pmtMethods); } /** @@ -81,4 +121,53 @@ export const deleteOrganizationPmtMethod = async (req: Request, res: Response) = ); return res.status(200).send(data); +} + +/** + * Return the organization's tax ids on file + */ +export const getOrganizationTaxIds = async (req: Request, res: Response) => { + const { data: { tax_ids } } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids` + ); + + return res.status(200).send(tax_ids); +} + +/** + * Add tax id to organization + */ +export const addOrganizationTaxId = async (req: Request, res: Response) => { + const { + type, + value + } = req.body; + + const { data } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids`, + { + type, + value + } + ); + + return res.status(200).send(data); +} + +export const deleteOrganizationTaxId = async (req: Request, res: Response) => { + const { taxId } = req.params; + + const { data } = await licenseServerKeyRequest.delete( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/tax-ids/${taxId}`, + ); + + return res.status(200).send(data); +} + +export const getOrganizationInvoices = async (req: Request, res: Response) => { + const { data: { invoices } } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices` + ); + + return res.status(200).send(invoices); } \ No newline at end of file diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts index 5a9c603b1..37be3a96d 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -11,10 +11,25 @@ import { ACCEPTED, ADMIN, MEMBER, OWNER, } from "../../../variables"; +router.get( + "/:organizationId/plans/table", + requireAuth({ + acceptedAuthModes: ["jwt"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + query("billingCycle").exists().isString().isIn(["monthly", "yearly"]), + validateRequest, + organizationsController.getOrganizationPlansTable +); + router.get( "/:organizationId/plan", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -26,25 +41,70 @@ router.get( organizationsController.getOrganizationPlan ); -router.patch( - "/:organizationId/plan", +router.get( + "/:organizationId/plan/billing", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED], }), param("organizationId").exists().trim(), - body("productId").exists().isString(), + query("workspaceId").optional().isString(), validateRequest, - organizationsController.updateOrganizationPlan + organizationsController.getOrganizationPlanBillingInfo +); + +router.get( + "/:organizationId/plan/table", + requireAuth({ + acceptedAuthModes: ["jwt"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + query("workspaceId").optional().isString(), + validateRequest, + organizationsController.getOrganizationPlanTable +); + +router.get( + "/:organizationId/billing-details", + requireAuth({ + acceptedAuthModes: ["jwt"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + validateRequest, + organizationsController.getOrganizationBillingDetails +); + +router.patch( + "/:organizationId/billing-details", + requireAuth({ + acceptedAuthModes: ["jwt"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + body("email").optional().isString().trim(), + body("name").optional().isString().trim(), + validateRequest, + organizationsController.updateOrganizationBillingDetails ); router.get( "/:organizationId/billing-details/payment-methods", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -58,7 +118,7 @@ router.get( router.post( "/:organizationId/billing-details/payment-methods", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -74,7 +134,22 @@ router.post( router.delete( "/:organizationId/billing-details/payment-methods/:pmtMethodId", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + param("pmtMethodId").exists().trim(), + validateRequest, + organizationsController.deleteOrganizationPmtMethod +); + +router.get( + "/:organizationId/billing-details/tax-ids", + requireAuth({ + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -82,7 +157,52 @@ router.delete( }), param("organizationId").exists().trim(), validateRequest, - organizationsController.deleteOrganizationPmtMethod + organizationsController.getOrganizationTaxIds +); + +router.post( + "/:organizationId/billing-details/tax-ids", + requireAuth({ + acceptedAuthModes: ["jwt"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + body("type").exists().isString(), + body("value").exists().isString(), + validateRequest, + organizationsController.addOrganizationTaxId +); + +router.delete( + "/:organizationId/billing-details/tax-ids/:taxId", + requireAuth({ + acceptedAuthModes: ["jwt"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + param("taxId").exists().trim(), + validateRequest, + organizationsController.deleteOrganizationTaxId +); + +router.get( + "/:organizationId/invoices", + requireAuth({ + acceptedAuthModes: ["jwt"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + validateRequest, + organizationsController.getOrganizationInvoices ); export default router; \ No newline at end of file diff --git a/frontend/src/components/login/InitialLoginStep.tsx b/frontend/src/components/login/InitialLoginStep.tsx index 5f17b1cf9..bcce19d1c 100644 --- a/frontend/src/components/login/InitialLoginStep.tsx +++ b/frontend/src/components/login/InitialLoginStep.tsx @@ -97,7 +97,7 @@ export default function InitialLoginStep({ setIsLoading(false); } - + return

Login to Infisical

{/*
diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index 6891de792..ee7274fdc 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -57,7 +57,7 @@ export default function NavHeader({ ); return ( -
+
{currentOrg?.name?.charAt(0)}
diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index 107978949..cffa80d63 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -1 +1,16 @@ -export { useGetOrganization, useRenameOrg } from "./queries"; +export { + useAddOrgPmtMethod, + useAddOrgTaxId, + useCreateCustomerPortalSession, + useDeleteOrgPmtMethod, + useDeleteOrgTaxId, + useGetOrganization, + useGetOrgBillingDetails, + useGetOrgInvoices, + useGetOrgPlanBillingInfo, + useGetOrgPlansTable, + useGetOrgPlanTable, + useGetOrgPmtMethods, + useGetOrgTaxIds, + useRenameOrg, + useUpdateOrgBillingDetails} from "./queries"; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 7e02b21a5..d3f161f64 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -2,22 +2,39 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { apiRequest } from "@app/config/request"; -import { Organization, RenameOrgDTO } from "./types"; +import { + BillingDetails, + Invoice, + Organization, + OrgPlanTable, + PlanBillingInfo, + PmtMethod, + ProductsTable, + RenameOrgDTO, + TaxID} from "./types"; const organizationKeys = { - getUserOrganization: ["organization"] as const + getUserOrganization: ["organization"] as const, + getOrgPlanBillingInfo: (orgId: string) => [{ orgId }, "organization-plan-billing"] as const, + getOrgPlanTable: (orgId: string) => [{ orgId }, "organization-plan-table"] as const, + getOrgPlansTable: (orgId: string, billingCycle: "monthly" | "yearly") => [{ orgId, billingCycle }, "organization-plans-table"] as const, + getOrgBillingDetails: (orgId: string) => [{ orgId }, "organization-billing-details"] as const, + getOrgPmtMethods: (orgId: string) => [{ orgId }, "organization-pmt-methods"] as const, + getOrgTaxIds: (orgId: string) => [{ orgId }, "organization-tax-ids"] as const, + getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const }; -const fetchUserOrganization = async () => { - const { data } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization"); +export const useGetOrganization = () => { + return useQuery({ + queryKey: organizationKeys.getUserOrganization, + queryFn: async () => { + const { data } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization"); - return data.organizations; -}; + return data.organizations; + } + }); +} -export const useGetOrganization = () => - useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization }); - -// mutation export const useRenameOrg = () => { const queryClient = useQueryClient(); @@ -29,3 +46,250 @@ export const useRenameOrg = () => { } }); }; + +export const useGetOrgPlanBillingInfo = (organizationId: string) => { + return useQuery({ + queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/plan/billing` + ); + + return data; + }, + enabled: true + }); +} + +export const useGetOrgPlanTable = (organizationId: string) => { + return useQuery({ + queryKey: organizationKeys.getOrgPlanTable(organizationId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/plan/table` + ); + + return data; + }, + enabled: true + }); +} + +export const useGetOrgPlansTable = ({ + organizationId, + billingCycle +}: { + organizationId: string; + billingCycle: "monthly" | "yearly" +}) => { + return useQuery({ + queryKey: organizationKeys.getOrgPlansTable(organizationId, billingCycle), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/plans/table?billingCycle=${billingCycle}` + ); + + return data; + }, + enabled: true + }); +} + +export const useGetOrgBillingDetails = (organizationId: string) => { + return useQuery({ + queryKey: organizationKeys.getOrgBillingDetails(organizationId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/billing-details` + ); + + return data; + }, + enabled: true + }); +} + +export const useUpdateOrgBillingDetails = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + organizationId, + name, + email + }: { + organizationId: string; + name?: string; + email?: string; + }) => { + const { data } = await apiRequest.patch( + `/api/v1/organizations/${organizationId}/billing-details`, + { + name, + email + } + ); + + return data; + }, + onSuccess(_, dto) { + queryClient.invalidateQueries(organizationKeys.getOrgBillingDetails(dto.organizationId)); + } + }); +}; + +export const useGetOrgPmtMethods = (organizationId: string) => { + return useQuery({ + queryKey: organizationKeys.getOrgPmtMethods(organizationId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/billing-details/payment-methods` + ); + + return data; + }, + enabled: true + }); +} + +export const useAddOrgPmtMethod = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + organizationId, + success_url, + cancel_url + }: { + organizationId: string; + success_url: string; + cancel_url: string; + }) => { + const { data: { url } } = await apiRequest.post( + `/api/v1/organizations/${organizationId}/billing-details/payment-methods`, + { + success_url, + cancel_url + } + ); + + return url; + }, + onSuccess(_, dto) { + queryClient.invalidateQueries(organizationKeys.getOrgPmtMethods(dto.organizationId)); + } + }); +}; + +export const useDeleteOrgPmtMethod = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + organizationId, + pmtMethodId, + }: { + organizationId: string; + pmtMethodId: string; + }) => { + const { data } = await apiRequest.delete( + `/api/v1/organizations/${organizationId}/billing-details/payment-methods/${pmtMethodId}` + ); + + return data; + }, + onSuccess(_, dto) { + queryClient.invalidateQueries(organizationKeys.getOrgPmtMethods(dto.organizationId)); + } + }); +} + +export const useGetOrgTaxIds = (organizationId: string) => { + return useQuery({ + queryKey: organizationKeys.getOrgTaxIds(organizationId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/billing-details/tax-ids` + ); + + return data; + }, + enabled: true + }); +} + +export const useAddOrgTaxId = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + organizationId, + type, + value + }: { + organizationId: string; + type: string; + value: string; + }) => { + const { data } = await apiRequest.post( + `/api/v1/organizations/${organizationId}/billing-details/tax-ids`, + { + type, + value + } + ); + + return data; + }, + onSuccess(_, dto) { + queryClient.invalidateQueries(organizationKeys.getOrgTaxIds(dto.organizationId)); + } + }); +}; + +export const useDeleteOrgTaxId = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + organizationId, + taxId, + }: { + organizationId: string; + taxId: string; + }) => { + const { data } = await apiRequest.delete( + `/api/v1/organizations/${organizationId}/billing-details/tax-ids/${taxId}` + ); + + return data; + }, + onSuccess(_, dto) { + queryClient.invalidateQueries(organizationKeys.getOrgTaxIds(dto.organizationId)); + } + }); +} + +export const useGetOrgInvoices = (organizationId: string) => { + return useQuery({ + queryKey: organizationKeys.getOrgInvoices(organizationId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/invoices` + ); + + return data; + }, + enabled: true + }); +} + +export const useCreateCustomerPortalSession = () => { + return useMutation({ + mutationFn: async (organizationId: string) => { + const { data } = await apiRequest.post( + `/api/v1/organization/${organizationId}/customer-portal-session` + ); + return data; + } + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts index 92a5c5f1d..a33a7f875 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -9,3 +9,79 @@ export type RenameOrgDTO = { orgId: string; newOrgName: string; }; + +export type BillingDetails = { + name: string; + email: string; +} + +export type PlanBillingInfo = { + amount: number; + currentPeriodEnd: number; + currentPeriodStart: number; + interval: "month" | "year"; + intervalCount: number; + quantity: number; +} + +export type Invoice = { + _id: string; + created: number; + invoice_pdf: string; + number: string; + paid: boolean; + total: number; +} + +export type PmtMethod = { + _id: string; + brand: string; + exp_month: number; + exp_year: number; + funding: string; + last4: string; +} + +export type TaxID = { + _id: string; + country: string; + type: string; + value: string; +} + +export type OrgPlanTableHead = { + name: string; +} + +export type OrgPlanTableRow = { + name: string; + allowed: number | boolean | null; + used: string; +} + +export type OrgPlanTable = { + head: OrgPlanTableHead[]; + rows: OrgPlanTableRow[]; +} + +export type ProductsTableHead = { + name: string; + price: number | null; + priceLine: string; + productId: string; + slug: string; + tier: number; +} + +export type ProductsTableRow = { + name: string; + starter: number | boolean | null; + team: number | boolean | null; + pro: number | boolean | null; + enterprise: number | boolean | null; +} + +export type ProductsTable = { + head: ProductsTableHead[]; + rows: ProductsTableRow[]; +} \ No newline at end of file diff --git a/frontend/src/hooks/usePopUp.tsx b/frontend/src/hooks/usePopUp.tsx index f3b849870..e9d8257e3 100644 --- a/frontend/src/hooks/usePopUp.tsx +++ b/frontend/src/hooks/usePopUp.tsx @@ -10,14 +10,14 @@ interface UsePopUpProps { * checks which type of inputProps were given and converts them into key-names * SIDENOTE: On inputting give it as const and not string with (as const) */ -type UsePopUpState | UsePopUpProps[]> = { +export type UsePopUpState | UsePopUpProps[]> = { [P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: { isOpen: boolean; data?: unknown; }; }; -interface UsePopUpReturn | UsePopUpProps[]> { +export interface UsePopUpReturn | UsePopUpProps[]> { popUp: UsePopUpState; handlePopUpOpen: (popUpName: keyof UsePopUpState, data?: unknown) => void; handlePopUpClose: (popUpName: keyof UsePopUpState) => void; diff --git a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx b/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx index 26bb1c99f..5db9b7a32 100644 --- a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx +++ b/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx @@ -20,7 +20,7 @@ import { Menu, Transition } from "@headlessui/react"; import {TFunction} from "i18next"; import guidGenerator from "@app/components/utilities/randomId"; -import { useOrganization, useUser } from "@app/context"; +import { useOrganization, useSubscription,useUser } from "@app/context"; import { useLogoutUser } from "@app/hooks/api"; const supportOptions = (t: TFunction) => [ @@ -63,6 +63,9 @@ export interface IUser { */ export const Navbar = () => { const router = useRouter(); + const { subscription } = useSubscription(); + + console.log("subscription: ", subscription); const { currentOrg, orgs } = useOrganization(); const { user } = useUser(); @@ -220,22 +223,24 @@ export const Navbar = () => { />
-
- +
null} + role="button" + tabIndex={0} + onClick={() => router.push(`/settings/billing/${router.query.id}`)} + className="relative mt-1 flex cursor-pointer select-none justify-start rounded-md py-2 px-2 text-gray-400 duration-200 hover:bg-white/5 hover:text-gray-200" + > + +
{t("nav.user.usage-billing")}
+
+ + )} + )} + + + {({ selected }) => ( + + )} + + + + + + + + + + + + + + ); +} diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx new file mode 100644 index 000000000..7e0a73b2f --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx @@ -0,0 +1,176 @@ +import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + Button, + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr, +} from "@app/components/v2"; +import { useOrganization,useSubscription } from "@app/context"; +import { + useCreateCustomerPortalSession, + useGetOrgPlansTable} from "@app/hooks/api"; + +type Props = { + billingCycle: "monthly" | "yearly" +} + +export const ManagePlansTable = ({ + billingCycle +}: Props) => { + const { currentOrg } = useOrganization(); + const { subscription } = useSubscription(); + const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({ + organizationId: currentOrg?._id ?? "", + billingCycle + }); + const createCustomerPortalSession = useCreateCustomerPortalSession(); + + const displayCell = (value: null | number | string | boolean) => { + if (value === null) return "-"; + + if (typeof value === "boolean") { + if (value) return ( + + ); + + return ( + + ); + } + + return value; + } + + return ( + + + + {subscription && !isTableDataLoading && tableData && ( + + + {tableData.head.map(({ + name, + priceLine + }) => { + return ( + + ); + })} + + )} + + + {subscription && !isTableDataLoading && tableData && tableData.rows.map(({ + name, + starter, + team, + pro, + enterprise + }) => { + return ( + + + + + + + + ); + })} + {isTableDataLoading && } + {!isTableDataLoading && tableData?.rows.length === 0 && ( + + + + )} + {subscription && !isTableDataLoading && tableData && ( + + + ) : ( + + ); + })} + + )} + +
Feature / Limit +

{name}

+

{priceLine}

+
{displayCell(name)} + {displayCell(starter)} + + {displayCell(team)} + + {displayCell(pro)} + + {displayCell(enterprise)} +
+ +
+ {tableData.head.map(({ + slug, + tier + }) => { + + const isCurrentPlan = slug === subscription.slug; + let subscriptionText = "Upgrade"; + + if (subscription.tier > tier) { + subscriptionText = "Downgrade" + } + + if (tier === 3) { + subscriptionText = "Contact sales" + } + + return isCurrentPlan ? ( + + + + +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx new file mode 100644 index 000000000..4050e7936 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx @@ -0,0 +1,92 @@ +import { Button } from "@app/components/v2"; +import { useOrganization,useSubscription } from "@app/context"; +import { + useCreateCustomerPortalSession, + useGetOrgPlanBillingInfo} from "@app/hooks/api"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { ManagePlansModal } from "./ManagePlansModal"; + +export const PreviewSection = () => { + const { currentOrg } = useOrganization(); + const { subscription, isLoading: isSubscriptionLoading } = useSubscription(); + const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? ""); + const createCustomerPortalSession = useCreateCustomerPortalSession(); + + const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([ + "managePlan" + ] as const); + + const formatAmount = (amount: number) => { + const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", { + style: "currency", + currency: "USD", + }); + + return formattedTotal; + } + + const formatDate = (date: number) => { + const createdDate = new Date(date * 1000); + const day: number = createdDate.getDate(); + const month: number = createdDate.getMonth() + 1; + const year: number = createdDate.getFullYear(); + const formattedDate: string = `${day}/${month}/${year}`; + + return formattedDate; + } + + return ( +
+ {!isSubscriptionLoading && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && ( +
+
+

Become Infisical

+

Unlimited members, projects, RBAC, smart alerts, and so much more

+
+ +
+ )} + {!isLoading && data && ( +
+
+

Current plan

+

Starter

+ +
+
+

Price

+

+ {`${formatAmount(data.amount)} / ${data.interval}`} +

+
+
+

Subscription renews on

+

+ {formatDate(data.currentPeriodEnd)} +

+
+
+ )} + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/index.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/index.tsx new file mode 100644 index 000000000..07696dc7e --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/index.tsx @@ -0,0 +1 @@ +export { BillingCloudTab } from "./BillingCloudTab"; \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/BillingDetailsTab.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/BillingDetailsTab.tsx new file mode 100644 index 000000000..3e149a1fd --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/BillingDetailsTab.tsx @@ -0,0 +1,15 @@ +import { CompanyNameSection } from "./CompanyNameSection"; +import { InvoiceEmailSection } from "./InvoiceEmailSection"; +import { PmtMethodsSection } from "./PmtMethodsSection"; +import { TaxIDSection } from "./TaxIDSection"; + +export const BillingDetailsTab = () => { + return ( + <> + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/CompanyNameSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/CompanyNameSection.tsx new file mode 100644 index 000000000..fb2eadf86 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/CompanyNameSection.tsx @@ -0,0 +1,96 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import Button from "@app/components/basic/buttons/Button"; +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { FormControl,Input } from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useGetOrgBillingDetails, + useUpdateOrgBillingDetails +} from "@app/hooks/api"; + +const schema = yup.object({ + name: yup.string().required("Company name is required") +}).required(); + +export const CompanyNameSection = () => { + const { createNotification } = useNotificationContext(); + const { currentOrg } = useOrganization(); + const { reset, control, handleSubmit } = useForm({ + defaultValues: { + name: "" + }, + resolver: yupResolver(schema) + }); + const { data } = useGetOrgBillingDetails(currentOrg?._id ?? ""); + const updateOrgBillingDetails = useUpdateOrgBillingDetails(); + + useEffect(() => { + if (data) { + reset({ + name: data?.name ?? "" + }); + } + }, [data]); + + const onFormSubmit = async ({ name }: { name: string }) => { + try { + if (!currentOrg?._id) return; + if (name === "") return; + await updateOrgBillingDetails.mutateAsync({ + name, + organizationId: currentOrg._id + }); + + createNotification({ + text: "Successfully updated business name", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to update business name", + type: "error" + }); + } + } + + return ( +
+

+ Business name +

+
+ ( + + + + )} + control={control} + name="name" + /> +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/InvoiceEmailSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/InvoiceEmailSection.tsx new file mode 100644 index 000000000..991a21869 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/InvoiceEmailSection.tsx @@ -0,0 +1,99 @@ +import { useEffect } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import Button from "@app/components/basic/buttons/Button"; +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + FormControl, + Input} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useGetOrgBillingDetails, + useUpdateOrgBillingDetails +} from "@app/hooks/api"; + +const schema = yup.object({ + email: yup.string().required("Email is required") +}).required(); + +export const InvoiceEmailSection = () => { + const { createNotification } = useNotificationContext(); + const { currentOrg } = useOrganization(); + const { reset, control, handleSubmit } = useForm({ + defaultValues: { + email: "" + }, + resolver: yupResolver(schema) + }); + const { data } = useGetOrgBillingDetails(currentOrg?._id ?? ""); + const updateOrgBillingDetails = useUpdateOrgBillingDetails(); + + useEffect(() => { + if (data) { + reset({ + email: data?.email ?? "" + }); + } + }, [data]); + + const onFormSubmit = async ({ email }: { email: string }) => { + try { + if (!currentOrg?._id) return; + if (email === "") return; + + await updateOrgBillingDetails.mutateAsync({ + email, + organizationId: currentOrg._id + }); + + createNotification({ + text: "Successfully updated invoice email recipient", + type: "success" + }); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to update invoice email recipient", + type: "error" + }); + } + } + + return ( +
+

+ Invoice email recipient +

+
+ ( + + + + )} + control={control} + name="email" + /> +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsSection.tsx new file mode 100644 index 000000000..ecdf27bb7 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsSection.tsx @@ -0,0 +1,44 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; + +import Button from "@app/components/basic/buttons/Button"; +import { useOrganization } from "@app/context"; +import { useAddOrgPmtMethod } from "@app/hooks/api"; + +import { PmtMethodsTable } from "./PmtMethodsTable"; + +export const PmtMethodsSection = () => { + const { currentOrg } = useOrganization(); + const addOrgPmtMethod = useAddOrgPmtMethod(); + + const handleAddPmtMethodBtnClick = async () => { + if (!currentOrg?._id) return; + const url = await addOrgPmtMethod.mutateAsync({ + organizationId: currentOrg._id, + success_url: window.location.href, + cancel_url: window.location.href + }); + + window.location.href = url; + } + + return ( +
+
+

+ Payment Methods +

+
+
+
+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsTable.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsTable.tsx new file mode 100644 index 000000000..4eba19a50 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsTable.tsx @@ -0,0 +1,90 @@ +import { faCreditCard, faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + EmptyState, + IconButton, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useDeleteOrgPmtMethod, + useGetOrgPmtMethods +} from "@app/hooks/api"; + +export const PmtMethodsTable = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? ""); + const deleteOrgPmtMethod = useDeleteOrgPmtMethod(); + + const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => { + if (!currentOrg?._id) return; + await deleteOrgPmtMethod.mutateAsync({ + organizationId: currentOrg._id, + pmtMethodId + }); + } + + return ( + + + + + + + + + + + + {!isLoading && data && data?.length > 0 && data.map(({ + _id, + brand, + exp_month, + exp_year, + funding, + last4 + }) => ( + + + + + + + + ))} + {isLoading && } + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
BrandTypeLast 4 DigitsExpiration +
{brand.charAt(0).toUpperCase() + brand.slice(1)}{funding.charAt(0).toUpperCase() + funding.slice(1)}{last4}{`${exp_month}/${exp_year}`} + { + await handleDeletePmtMethodBtnClick(_id); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + > + + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDModal.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDModal.tsx new file mode 100644 index 000000000..d08d14377 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDModal.tsx @@ -0,0 +1,195 @@ +import { Controller, useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import * as yup from "yup"; + +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { + Button, + FormControl, + Input, + Modal, + ModalContent, + Select, + SelectItem} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { useAddOrgTaxId } from "@app/hooks/api"; +import { UsePopUpState } from "@app/hooks/usePopUp"; + +const taxIDTypes = [ + { label: "Australia ABN", value: "au_abn" }, + { label: "Australia ARN", value: "au_arn" }, + { label: "Bulgaria UIC", value: "bg_uic" }, + { label: "Brazil CNPJ", value: "br_cnpj" }, + { label: "Brazil CPF", value: "br_cpf" }, + { label: "Canada BN", value: "ca_bn" }, + { label: "Canada GST/HST", value: "ca_gst_hst" }, + { label: "Canada PST BC", value: "ca_pst_bc" }, + { label: "Canada PST MB", value: "ca_pst_mb" }, + { label: "Canada PST SK", value: "ca_pst_sk" }, + { label: "Canada QST", value: "ca_qst" }, + { label: "Switzerland VAT", value: "ch_vat" }, + { label: "Chile TIN", value: "cl_tin" }, + { label: "Egypt TIN", value: "eg_tin" }, + { label: "Spain CIF", value: "es_cif" }, + { label: "EU OSS VAT", value: "eu_oss_vat" }, + { label: "EU VAT", value: "eu_vat" }, + { label: "GB VAT", value: "gb_vat" }, + { label: "Georgia VAT", value: "ge_vat" }, + { label: "Hong Kong BR", value: "hk_br" }, + { label: "Hungary TIN", value: "hu_tin" }, + { label: "Indonesia NPWP", value: "id_npwp" }, + { label: "Israel VAT", value: "il_vat" }, + { label: "India GST", value: "in_gst" }, + { label: "Iceland VAT", value: "is_vat" }, + { label: "Japan CN", value: "jp_cn" }, + { label: "Japan RN", value: "jp_rn" }, + { label: "Japan TRN", value: "jp_trn" }, + { label: "Kenya PIN", value: "ke_pin" }, + { label: "South Korea BRN", value: "kr_brn" }, + { label: "Liechtenstein UID", value: "li_uid" }, + { label: "Mexico RFC", value: "mx_rfc" }, + { label: "Malaysia FRP", value: "my_frp" }, + { label: "Malaysia ITN", value: "my_itn" }, + { label: "Malaysia SST", value: "my_sst" }, + { label: "Norway VAT", value: "no_vat" }, + { label: "New Zealand GST", value: "nz_gst" }, + { label: "Philippines TIN", value: "ph_tin" }, + { label: "Russia INN", value: "ru_inn" }, + { label: "Russia KPP", value: "ru_kpp" }, + { label: "Saudi Arabia VAT", value: "sa_vat" }, + { label: "Singapore GST", value: "sg_gst" }, + { label: "Singapore UEN", value: "sg_uen" }, + { label: "Slovenia TIN", value: "si_tin" }, + { label: "Thailand VAT", value: "th_vat" }, + { label: "Turkey TIN", value: "tr_tin" }, + { label: "Taiwan VAT", value: "tw_vat" }, + { label: "Ukraine VAT", value: "ua_vat" }, + { label: "US EIN", value: "us_ein" }, + { label: "South Africa VAT", value: "za_vat" } +]; + +const schema = yup.object({ + type: yup.string().required("Tax ID type is required"), + value: yup.string().required("Tax ID value is required") +}).required(); + +export type AddTaxIDFormData = yup.InferType; + +type Props = { + popUp: UsePopUpState<["addTaxID"]>; + handlePopUpClose: (popUpName: keyof UsePopUpState<["addTaxID"]>) => void; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["addTaxID"]>, state?: boolean) => void; +}; + +export const TaxIDModal = ({ + popUp, + handlePopUpClose, + handlePopUpToggle +}: Props) => { + const { createNotification } = useNotificationContext(); + const { currentOrg } = useOrganization(); + const addOrgTaxId = useAddOrgTaxId(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(schema) + }); + + const onTaxIDModalSubmit = async ({ type, value }: AddTaxIDFormData) => { + try { + if (!currentOrg?._id) return; + await addOrgTaxId.mutateAsync({ + organizationId: currentOrg._id, + type, + value + }); + + createNotification({ + text: "Successfully added Tax ID", + type: "success" + }); + handlePopUpClose("addTaxID"); + } catch (err) { + console.error(err); + createNotification({ + text: "Failed to add Tax ID", + type: "error" + }); + } + } + + return ( + { + handlePopUpToggle("addTaxID", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDSection.tsx new file mode 100644 index 000000000..ff5f0430d --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDSection.tsx @@ -0,0 +1,39 @@ +import { faPlus } from "@fortawesome/free-solid-svg-icons"; + +import Button from "@app/components/basic/buttons/Button"; +import { usePopUp } from "@app/hooks/usePopUp"; + +import { TaxIDModal } from "./TaxIDModal"; +import { TaxIDTable } from "./TaxIDTable"; + +export const TaxIDSection = () => { + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "addTaxID" + ] as const); + + return ( +
+
+

+ Tax ID +

+
+
+
+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDTable.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDTable.tsx new file mode 100644 index 000000000..873b41cf6 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDTable.tsx @@ -0,0 +1,137 @@ +import { faFileInvoice,faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + EmptyState, + IconButton, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr, +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useDeleteOrgTaxId, + useGetOrgTaxIds +} from "@app/hooks/api"; + +const taxIDTypeLabelMap: { [key: string]: string } = { + "au_abn": "Australia ABN", + "au_arn": "Australia ARN", + "bg_uic": "Bulgaria UIC", + "br_cnpj": "Brazil CNPJ", + "br_cpf": "Brazil CPF", + "ca_bn": "Canada BN", + "ca_gst_hst": "Canada GST/HST", + "ca_pst_bc": "Canada PST BC", + "ca_pst_mb": "Canada PST MB", + "ca_pst_sk": "Canada PST SK", + "ca_qst": "Canada QST", + "ch_vat": "Switzerland VAT", + "cl_tin": "Chile TIN", + "eg_tin": "Egypt TIN", + "es_cif": "Spain CIF", + "eu_oss_vat": "EU OSS VAT", + "eu_vat": "EU VAT", + "gb_vat": "GB VAT", + "ge_vat": "Georgia VAT", + "hk_br": "Hong Kong BR", + "hu_tin": "Hungary TIN", + "id_npwp": "Indonesia NPWP", + "il_vat": "Israel VAT", + "in_gst": "India GST", + "is_vat": "Iceland VAT", + "jp_cn": "Japan CN", + "jp_rn": "Japan RN", + "jp_trn": "Japan TRN", + "ke_pin": "Kenya PIN", + "kr_brn": "South Korea BRN", + "li_uid": "Liechtenstein UID", + "mx_rfc": "Mexico RFC", + "my_frp": "Malaysia FRP", + "my_itn": "Malaysia ITN", + "my_sst": "Malaysia SST", + "no_vat": "Norway VAT", + "nz_gst": "New Zealand GST", + "ph_tin": "Philippines TIN", + "ru_inn": "Russia INN", + "ru_kpp": "Russia KPP", + "sa_vat": "Saudi Arabia VAT", + "sg_gst": "Singapore GST", + "sg_uen": "Singapore UEN", + "si_tin": "Slovenia TIN", + "th_vat": "Thailand VAT", + "tr_tin": "Turkey TIN", + "tw_vat": "Taiwan VAT", + "ua_vat": "Ukraine VAT", + "us_ein": "US EIN", + "za_vat": "South Africa VAT" +}; + +export const TaxIDTable = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? ""); + const deleteOrgTaxId = useDeleteOrgTaxId(); + + const handleDeleteTaxIdBtnClick = async (taxId: string) => { + if (!currentOrg?._id) return; + await deleteOrgTaxId.mutateAsync({ + organizationId: currentOrg._id, + taxId + }); + } + + return ( + + + + + + + + + + {!isLoading && data && data?.length > 0 && data.map(({ + _id, + type, + value + }) => ( + + + + + + ))} + {isLoading && } + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
TypeValue +
{taxIDTypeLabelMap[type]}{value} + { + await handleDeleteTaxIdBtnClick(_id); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + > + + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/index.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/index.tsx new file mode 100644 index 000000000..359e74012 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/index.tsx @@ -0,0 +1 @@ +export { BillingDetailsTab } from "./BillingDetailsTab"; \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/BillingReceiptsTab.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/BillingReceiptsTab.tsx new file mode 100644 index 000000000..f5c542186 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/BillingReceiptsTab.tsx @@ -0,0 +1,10 @@ +import { InvoicesTable } from "./InvoicesTable"; + +export const BillingReceiptsTab = () => { + return ( +
+

Invoices

+ +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/InvoicesTable.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/InvoicesTable.tsx new file mode 100644 index 000000000..9050f9d5f --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/InvoicesTable.tsx @@ -0,0 +1,88 @@ +import { faDownload, faFileInvoice } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + +import { + EmptyState, + IconButton, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useGetOrgInvoices +} from "@app/hooks/api"; + +export const InvoicesTable = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? ""); + return ( + + + + + + + + + + + + {!isLoading && data && data?.length > 0 && data.map(({ + _id, + created, + paid, + number, + total, + invoice_pdf + }) => { + const formattedTotal = (Math.floor(total) / 100).toLocaleString("en-US", { + style: "currency", + currency: "USD", + }); + const createdDate = new Date(created * 1000); + const day: number = createdDate.getDate(); + const month: number = createdDate.getMonth() + 1; + const year: number = createdDate.getFullYear(); + const formattedDate: string = `${day}/${month}/${year}`; + + return ( + + + + + + + + ); + })} + {isLoading && } + {!isLoading && data && data?.length === 0 && ( + + + + )} + +
Invoice #DateStatusAmount +
{number}{formattedDate}{paid ? "Paid" : "Not Paid"}{formattedTotal} + window.open(invoice_pdf)} + size="lg" + variant="plain" + ariaLabel="update" + > + + +
+ +
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/index.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/index.tsx new file mode 100644 index 000000000..4f4b60a8c --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/index.tsx @@ -0,0 +1 @@ +export { BillingReceiptsTab } from "./BillingReceiptsTab"; \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingTabGroup/BillingTabGroup.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingTabGroup/BillingTabGroup.tsx new file mode 100644 index 000000000..72b2e942b --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingTabGroup/BillingTabGroup.tsx @@ -0,0 +1,44 @@ +import { Fragment } from "react" +import { Tab } from "@headlessui/react" + +import { BillingCloudTab } from "../BillingCloudTab"; +import { BillingDetailsTab } from "../BillingDetailsTab"; +import { BillingReceiptsTab } from "../BillingReceiptsTab"; + +const tabs = [ + { name: "Infisical Cloud", key: "tab-infisical-cloud" }, + { name: "Receipts", key: "tab-receipts" }, + { name: "Billing details", key: "tab-billing-details" } +]; + +export const BillingTabGroup = () => { + return ( + + + {tabs.map((tab) => ( + + {({ selected }) => ( + + )} + + ))} + + + + + + + + + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingTabGroup/index.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingTabGroup/index.tsx new file mode 100644 index 000000000..6c13599f8 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingTabGroup/index.tsx @@ -0,0 +1 @@ +export { BillingTabGroup } from "./BillingTabGroup"; \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/index.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/index.tsx new file mode 100644 index 000000000..6c13599f8 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/index.tsx @@ -0,0 +1 @@ +export { BillingTabGroup } from "./BillingTabGroup"; \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/index.tsx b/frontend/src/views/Settings/BillingSettingsPage/index.tsx new file mode 100644 index 000000000..2c4e77101 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/index.tsx @@ -0,0 +1 @@ +export { BillingSettingsPage } from "./BillingSettingsPage"; \ No newline at end of file diff --git a/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx b/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx index b9a409e0d..d51d31f39 100644 --- a/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx +++ b/frontend/src/views/Settings/OrgSettingsPage/OrgSettingsPage.tsx @@ -210,27 +210,6 @@ export const OrgSettingsPage = () => { } }; - /** - * This function deleted a workspace. - * It first checks if there is more than one workspace available. Otherwise, it doesn't delete - * It then checks if the name of the workspace to be deleted is correct. Otherwise, it doesn't delete. - * It then deletes the workspace and forwards the user to another available workspace. - */ - // const executeDeletingWorkspace = async () => { - // const userWorkspaces = await getWorkspaces(); - // - // if (userWorkspaces.length > 1) { - // if ( - // userWorkspaces.filter((workspace) => workspace._id === workspaceId)[0].name === - // workspaceToBeDeletedName - // ) { - // await deleteWorkspace(workspaceId); - // const ws = await getWorkspaces(); - // router.push(`/dashboard/${ws[0]._id}`); - // } - // } - // }; - // return (