From a127d452bd4ce22ddb6ad455cc112a62f1b734de Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Sun, 25 Jun 2023 17:03:41 +0700 Subject: [PATCH 1/4] Continue to make progress on usage and billing page revamp --- .../controllers/v1/organizationsController.ts | 142 ++++++++- backend/src/ee/routes/v1/organizations.ts | 151 +++++++++ .../src/components/navigation/NavHeader.tsx | 2 +- frontend/src/hooks/api/organization/index.ts | 19 +- .../src/hooks/api/organization/queries.tsx | 289 +++++++++++++++++- frontend/src/hooks/api/organization/types.ts | 5 + frontend/src/pages/settings/billing/[id].tsx | 80 ++--- .../BillingSettingsPage.tsx | 120 ++++++++ .../BillingCloudTab/BillingCloudTab.tsx | 33 ++ .../BillingCloudTab/CurrentPlanSection.tsx | 94 ++++++ .../BillingCloudTab/ManagePlansModal.tsx | 78 +++++ .../BillingCloudTab/ManagePlansTable.tsx | 187 ++++++++++++ .../BillingCloudTab/PreviewSection.tsx | 75 +++++ .../components/BillingCloudTab/index.tsx | 1 + .../BillingDetailsTab/BillingDetailsTab.tsx | 15 + .../BillingDetailsTab/CompanyNameSection.tsx | 98 ++++++ .../BillingDetailsTab/InvoiceEmailSection.tsx | 86 ++++++ .../BillingDetailsTab/PmtMethodsSection.tsx | 130 ++++++++ .../BillingDetailsTab/TaxIDSection.tsx | 276 +++++++++++++++++ .../components/BillingDetailsTab/index.tsx | 1 + .../BillingReceiptsTab/BillingReceiptsTab.tsx | 100 ++++++ .../components/BillingReceiptsTab/index.tsx | 1 + .../BillingSelfHostedTab.tsx | 0 .../components/BillingSelfHostedTab/index.tsx | 0 .../BillingSettingsPage/components/index.tsx | 3 + .../Settings/BillingSettingsPage/index.tsx | 1 + .../OrgSettingsPage/OrgSettingsPage.tsx | 21 -- 27 files changed, 1940 insertions(+), 68 deletions(-) create mode 100644 frontend/src/views/Settings/BillingSettingsPage/BillingSettingsPage.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/BillingCloudTab.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansModal.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/index.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/BillingDetailsTab.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/CompanyNameSection.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/InvoiceEmailSection.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsSection.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDSection.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/index.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/BillingReceiptsTab.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/index.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/BillingSelfHostedTab.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/index.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/index.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/index.tsx diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index 7ab2c5d32..d91efa057 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -3,8 +3,37 @@ import { getLicenseServerUrl } from "../../../config"; import { licenseServerKeyRequest } from "../../../config/request"; import { EELicenseService } from "../../services"; +export const createProductCheckoutSession = async (req: Request, res: Response) => { + const { + productId, + success_url + } = req.body; + console.log('createProductCheckoutSession req.body: ', req.body); + + const { data } = await licenseServerKeyRequest.post( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/session`, + { + productId, + success_url + } + ); + console.log('createProductCheckoutSession data: ', data); + + return res.status(200).send(data); +} + +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; @@ -17,6 +46,34 @@ export const getOrganizationPlan = async (req: Request, res: Response) => { }); } +/** + * Return the organization's current plan's billing info + * @param req + * @param res + * @returns + */ +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); +} + /** * Update the organization plan to product with id [productId] * @param req @@ -28,16 +85,44 @@ export const updateOrganizationPlan = async (req: Request, res: Response) => { productId, } = req.body; - const { data } = await licenseServerKeyRequest.patch( + console.log('backend update productId: ', productId); + const { data } = await licenseServerKeyRequest.patch( `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan`, { productId, } ); + + console.log(' ttproductId: ', data); 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 +131,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 +164,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 } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices` + ); + + return res.status(200).send(data); } \ 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..e9ea8f0e4 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -11,6 +11,37 @@ import { ACCEPTED, ADMIN, MEMBER, OWNER, } from "../../../variables"; +router.post( + "/:organizationId/billing-details/session", + requireAuth({ + acceptedAuthModes: ["jwt", "apiKey"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + body("productId").exists().trim(), + body("success_url").exists().trim(), + validateRequest, + organizationsController.createProductCheckoutSession +); + +router.get( + "/:organizationId/plans/table", + requireAuth({ + acceptedAuthModes: ["jwt", "apiKey"], + }), + 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({ @@ -26,6 +57,36 @@ router.get( organizationsController.getOrganizationPlan ); +router.get( + "/:organizationId/plan/billing", + requireAuth({ + acceptedAuthModes: ["jwt", "apiKey"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + query("workspaceId").optional().isString(), + validateRequest, + organizationsController.getOrganizationPlanBillingInfo +); + +router.get( + "/:organizationId/plan/table", + requireAuth({ + acceptedAuthModes: ["jwt", "apiKey"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + query("workspaceId").optional().isString(), + validateRequest, + organizationsController.getOrganizationPlanTable +); + router.patch( "/:organizationId/plan", requireAuth({ @@ -41,6 +102,36 @@ router.patch( organizationsController.updateOrganizationPlan ); +router.get( + "/:organizationId/billing-details", + requireAuth({ + acceptedAuthModes: ["jwt", "apiKey"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + validateRequest, + organizationsController.getOrganizationBillingDetails +); + +router.patch( + "/:organizationId/billing-details", + requireAuth({ + acceptedAuthModes: ["jwt", "apiKey"], + }), + 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({ @@ -81,8 +172,68 @@ router.delete( acceptedStatuses: [ACCEPTED], }), param("organizationId").exists().trim(), + param("pmtMethodId").exists().trim(), validateRequest, organizationsController.deleteOrganizationPmtMethod ); +router.get( + "/:organizationId/billing-details/tax-ids", + requireAuth({ + acceptedAuthModes: ["jwt", "apiKey"], + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + }), + param("organizationId").exists().trim(), + validateRequest, + organizationsController.getOrganizationTaxIds +); + +router.post( + "/:organizationId/billing-details/tax-ids", + requireAuth({ + acceptedAuthModes: ["jwt", "apiKey"], + }), + 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", "apiKey"], + }), + 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", "apiKey"], + }), + 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/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index 6891de792..4bbdce1bc 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..a856001c7 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -1 +1,18 @@ -export { useGetOrganization, useRenameOrg } from "./queries"; +export { + useGetOrgPlanBillingInfo, + useGetOrgPlanTable, + useGetOrgPlansTable, + useGetOrganization, + useRenameOrg, + useGetOrgBillingDetails, + useUpdateOrgBillingDetails, + useGetOrgPmtMethods, + useAddOrgPmtMethod, + useDeleteOrgPmtMethod, + useGetOrgTaxIds, + useAddOrgTaxId, + useDeleteOrgTaxId, + useGetOrgInvoices, + useUpdateOrgPlan, + useCreateProductCheckoutSession +} from "./queries"; diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx index 7e02b21a5..08476ba00 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -1,11 +1,16 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; - import { apiRequest } from "@app/config/request"; - -import { Organization, RenameOrgDTO } from "./types"; +import { Organization, RenameOrgDTO, BillingDetails } 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 () => { @@ -14,6 +19,40 @@ const fetchUserOrganization = async () => { return data.organizations; }; +// TODO: fix the type situation here and move fetches directly into hooks + +const fetchOrgBillingDetails = async (organizationId: string) => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/billing-details` + ); + + return data; +} + +const fetchOrgPmtMethods = async (organizationId: string) => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/billing-details/payment-methods` + ); + + return data; +} + +const fetchOrgTaxIds = async (organizationId: string) => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/billing-details/tax-ids` + ); + + return data; +} + +const fetchOrgInvoices = async (organizationId: string) => { + const { data: { invoices } } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/invoices` + ); + + return invoices; +} + export const useGetOrganization = () => useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization }); @@ -29,3 +68,245 @@ 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" | "annual" +}) => { + 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: () => fetchOrgBillingDetails(organizationId), + 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: () => fetchOrgPmtMethods(organizationId), + 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: () => fetchOrgTaxIds(organizationId), + 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: () => fetchOrgInvoices(organizationId), + enabled: true + }); +} + +export const useUpdateOrgPlan = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + organizationId, + productId + }: { + organizationId: string; + productId: string; + }) => { + const { data } = await apiRequest.patch(`/api/v1/organizations/${organizationId}/plan`, { + productId + }); + return data; + }, + onSuccess(_, dto) { + queryClient.invalidateQueries([ + organizationKeys.getOrgPlanTable(dto.organizationId), + ]); + } + }); +}; + +export const useCreateProductCheckoutSession = () => { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: async ({ + organizationId, + productId, + success_url + }: { + organizationId: string; + productId: string; + success_url: string; + }) => { + const { data } = await apiRequest.post(`/api/v1/organizations/${organizationId}/billing-details/session`, { + productId, + success_url + }); + return data; + }, + onSuccess(_, dto) { + console.log('onSuccess'); + } + }); +}; \ 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..94528cd15 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -9,3 +9,8 @@ export type RenameOrgDTO = { orgId: string; newOrgName: string; }; + +export type BillingDetails = { + name: string; + email: string; +} \ No newline at end of file diff --git a/frontend/src/pages/settings/billing/[id].tsx b/frontend/src/pages/settings/billing/[id].tsx index e7b396fa3..39c8e71ac 100644 --- a/frontend/src/pages/settings/billing/[id].tsx +++ b/frontend/src/pages/settings/billing/[id].tsx @@ -1,9 +1,7 @@ import { useTranslation } from "react-i18next"; import Head from "next/head"; - -import Plan from "@app/components/billing/Plan"; -import NavHeader from "@app/components/navigation/NavHeader"; import { useSubscription } from "@app/context"; +import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage"; export default function SettingsBilling() { const { subscription } = useSubscription(); @@ -55,45 +53,55 @@ export default function SettingsBilling() { ]; return ( -
+
{t("common.head-title", { title: t("billing.title") })} -
-
- -
-
-

{t("billing.title")}

-

{t("billing.description")}

-
-
-
-

{t("billing.subscription")}

-
- {plans.map((plan) => ( - - ))} -
- {/*

{t("billing.current-usage")}

-
-
-

{numUsers}

-

- Organization members -

-
-
-

1

-

Organization projects

-
-
*/} -
-
-
+
); } SettingsBilling.requireAuth = true; + + + +// return ( +//
+// +// {t("common.head-title", { title: t("billing.title") })} +// +// +//
+// +//
+//
+//

{t("billing.title")}

+//

{t("billing.description")}

+//
+//
+//
+//

{t("billing.subscription")}

+//
+// {plans.map((plan) => ( +// +// ))} +//
+// {/*

{t("billing.current-usage")}

+//
+//
+//

{numUsers}

+//

+// Organization members +//

+//
+//
+//

1

+//

Organization projects

+//
+//
*/} +//
+//
+//
+// ); diff --git a/frontend/src/views/Settings/BillingSettingsPage/BillingSettingsPage.tsx b/frontend/src/views/Settings/BillingSettingsPage/BillingSettingsPage.tsx new file mode 100644 index 000000000..02ee258c9 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/BillingSettingsPage.tsx @@ -0,0 +1,120 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; +import Plan from "@app/components/billing/Plan"; +import NavHeader from "@app/components/navigation/NavHeader"; +import { useSubscription } from "@app/context"; +import { + Input, + Button, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus, faMagnifyingGlass, faDownload } from "@fortawesome/free-solid-svg-icons"; + +import { Tab } from '@headlessui/react' +import { Fragment } from 'react' + +import { + BillingCloudTab, + BillingReceiptsTab, + BillingDetailsTab +} from "./components"; + +export const BillingSettingsPage = () => { + const { t } = useTranslation(); + return ( +
+ + +
+
+

{t("billing.title")}

+ {/*

+ Manage usage and billing for Infisical Cloud and Self-hosted instances here +

*/} +
+
+
+
+ + + + + {({ selected }) => ( + /* Use the `selected` state to conditionally style the selected tab. */ + + )} + + {/* + {({ selected }) => ( + + )} + */} + + {({ selected }) => ( + /* Use the `selected` state to conditionally style the selected tab. */ + + )} + + + {({ selected }) => ( + /* Use the `selected` state to conditionally style the selected tab. */ + + )} + + + + + + + {/* Content 2 */} + + + + + + + + + + + + {/*
+

{t("billing.subscription")}

+
+ {plans.map((plan) => ( + + ))} +
+

{t("billing.current-usage")}

+
+
+

{numUsers}

+

+ Organization members +

+
+
+

1

+

Organization projects

+
+
+
*/} +
+ ); +}; \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/BillingCloudTab.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/BillingCloudTab.tsx new file mode 100644 index 000000000..331b7545c --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/BillingCloudTab.tsx @@ -0,0 +1,33 @@ +import { useTranslation } from "react-i18next"; +import Head from "next/head"; +import Plan from "@app/components/billing/Plan"; +import NavHeader from "@app/components/navigation/NavHeader"; +import { + Input, + Button, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus, faMagnifyingGlass, faDownload } from "@fortawesome/free-solid-svg-icons"; + +import { PreviewSection } from "./PreviewSection"; +import { CurrentPlanSection } from "./CurrentPlanSection"; + +// TODO: optimize + modularize +// TODO: get cloud plan full + +export const BillingCloudTab = () => { + return ( +
+ + +
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx new file mode 100644 index 000000000..cfdacd869 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx @@ -0,0 +1,94 @@ +import { + Input, + IconButton, + Button, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr, + EmptyState +} from "@app/components/v2"; +import { + useGetOrgPlanTable +} from "@app/hooks/api"; +import { useOrganization } from "@app/context"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFileInvoice, faCircleCheck, faCircleXmark } from "@fortawesome/free-solid-svg-icons"; + +export const CurrentPlanSection = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? ''); + + const displayCell = (value: null | number | string | boolean) => { + if (value === null) return '-'; + + if (typeof value === 'boolean') { + if (value) return ( + + ); + + return ( + + ); + } + + return value; + } + + return ( +
+

Current Usage

+ + + + + + + + + + + {!isLoading && data?.rows?.length > 0 && data.rows.map(({ + name, + allowed, + used + }: { + name: string; + allowed: number | boolean; + used: string; + }) => { + return ( + + + + + + ); + })} + {isLoading && } + {!isLoading && data?.length === 0 && ( + + + + )} + +
FeatureAllowedUsed
{name}{displayCell(allowed)}{used}
+ +
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansModal.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansModal.tsx new file mode 100644 index 000000000..6a7da542f --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansModal.tsx @@ -0,0 +1,78 @@ +import { useSubscription } from "@app/context"; +import { useOrganization } from "@app/context"; +import { Tab } from '@headlessui/react' +import { Fragment } from 'react' +import { + useGetOrgPlanBillingInfo, + useGetOrgPlanTable, + useGetOrgPlansTable +} from "@app/hooks/api"; +import { + FormControl, + Button, + IconButton, + Input, + Select, + SelectItem, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr, + EmptyState, + Modal, + ModalContent +} from "@app/components/v2"; +import { usePopUp } from "@app/hooks/usePopUp"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFileInvoice, faCircleCheck, faCircleXmark, faExternalLinkAlt } from "@fortawesome/free-solid-svg-icons"; + +import { ManagePlansTable } from "./ManagePlansTable"; + +export const ManagePlansModal = ({ + popUp, + handlePopUpToggle +}) => { + const { subscription, isLoading: isSubscriptionLoading } = useSubscription(); + + return ( + { + handlePopUpToggle("managePlan", isOpen); + }} + > + + + + + {({ selected }) => ( + + )} + + + {({ selected }) => ( + + )} + + + + + + + + + + + + + + ); +} \ No newline at end of file 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..f4ae1884c --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx @@ -0,0 +1,187 @@ +import { useSubscription } from "@app/context"; +import { useOrganization } from "@app/context"; +import { Tab } from '@headlessui/react' +import { Fragment } from 'react' +import { + useGetOrgPlanBillingInfo, + useGetOrgPlanTable, + useGetOrgPlansTable, + useUpdateOrgPlan, + useCreateProductCheckoutSession +} from "@app/hooks/api"; +import { + FormControl, + Button, + IconButton, + Input, + Select, + SelectItem, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr, + EmptyState, + Modal, + ModalContent +} from "@app/components/v2"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faFileInvoice, faCircleCheck, faCircleXmark } from "@fortawesome/free-solid-svg-icons"; + +// TODO: upgrade + +type Props = { + billingCycle: 'monthly' | 'yearly' +} + +export const ManagePlansTable = ({ + billingCycle +}: Props) => { + const { currentOrg } = useOrganization(); + const { subscription, isLoading: isSubscriptionLoading } = useSubscription(); + const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({ + organizationId: currentOrg?._id ?? '', + billingCycle + }); + const updateOrgPlan = useUpdateOrgPlan(); + const createProductCheckoutSession = useCreateProductCheckoutSession(); + + console.log('tableData: ', tableData); + + const displayCell = (value: null | number | string | boolean) => { + if (value === null) return '-'; + + if (typeof value === 'boolean') { + if (value) return ( + + ); + + return ( + + ); + } + + return value; + } + + return ( + + + + {!isTableDataLoading && tableData?.head.length > 0 && ( + + + {tableData.head.map(({ + name, + slug, + priceLine + }: { + name: string; + slug: string; + priceLine: string; + }) => { + return ( + + ); + })} + + )} + + + {!isTableDataLoading && tableData?.rows.length > 0 && tableData.rows.map(({ + name, + starter, + team, + pro, + enterprise + }: { + name: string; + starter: null | number | string | boolean; + team: null | number | string | boolean; + pro: null | number | string | boolean; + enterprise: null | number | string | boolean; + }) => { + return ( + + + + + + + + ); + })} + {isTableDataLoading && } + {!isTableDataLoading && tableData?.rows.length === 0 && ( + + + + )} + {subscription && !isTableDataLoading && tableData?.head.length > 0 && ( + + + {tableData.head.map(({ + slug, + productId + }: { + slug: string; + productId: string; + }) => { + const isCurrentPlan = slug === subscription.slug; + + console.log('productId: ', productId); + return isCurrentPlan ? ( + + ) : ( + + ); + })} + + )} + +
Feature / Limit +

{name}

+

{priceLine}

+
{displayCell(name)}{displayCell(starter)}{displayCell(team)}{displayCell(pro)}{displayCell(enterprise)}
+ +
+

Current

+
+ +
+
+ ); +} \ 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..e03cb14c9 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx @@ -0,0 +1,75 @@ +import { useSubscription } from "@app/context"; +import { useOrganization } from "@app/context"; +import { useGetOrgPlanBillingInfo } from "@app/hooks/api"; +import { Button } from "@app/components/v2"; +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 { popUp, handlePopUpOpen, handlePopUpClose, 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

+

Manage plan →

+
+
+

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..471cdb050 --- /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..b8c88ff10 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/CompanyNameSection.tsx @@ -0,0 +1,98 @@ +import { useEffect } from "react"; +import { + Input, +// Button, + FormControl +} from "@app/components/v2"; +import { Controller, useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; +import { useOrganization } from "@app/context"; +import { + useGetOrgBillingDetails, + useUpdateOrgBillingDetails +} from "@app/hooks/api"; + +import Button from "@app/components/basic/buttons/Button"; + +const schema = yup.object({ + name: yup.string().required('Company name is required') +}).required(); + +export const CompanyNameSection = () => { + const { currentOrg } = useOrganization(); + const { reset, control, register, handleSubmit, watch, formState: { errors } } = 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 + }); + } catch (err) { + console.error(err); + } + } + + return ( +
+
+

+ Business name +

+ +
+
+ ( + + + + )} + control={control} + name="name" + /> +
+
+
+
+ ( + + + + )} + 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..f05be12fb --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsSection.tsx @@ -0,0 +1,130 @@ +import { + Button, + IconButton, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr, + EmptyState +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useGetOrgPmtMethods, + useAddOrgPmtMethod, + useDeleteOrgPmtMethod +} from "@app/hooks/api"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus, faXmark, faCreditCard } from "@fortawesome/free-solid-svg-icons"; + +// TODO: optimize + modularize + +export const PmtMethodsSection = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? ''); + const addOrgPmtMethod = useAddOrgPmtMethod(); + const deleteOrgPmtMethod = useDeleteOrgPmtMethod(); + + 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; + } + + const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => { + if (!currentOrg?._id) return; + await deleteOrgPmtMethod.mutateAsync({ + organizationId: currentOrg._id, + pmtMethodId + }); + } + + return ( +
+
+

+ Payment Methods +

+ +
+ + + + + + + + + + + + + {!isLoading && data?.length > 0 && data.map(({ + _id, + brand, + exp_month, + exp_year, + funding, + last4 + }: { + _id: string; + brand: string; + exp_month: number; + exp_year: number; + funding: string; + last4: string; + }) => ( + + + + + + + + ))} + {isLoading && } + {!isLoading && data?.length === 0 && ( + + + + )} + +
BrandTypeLast 4 DigitsExpiration
{brand}{funding}{last4}{`${exp_month}/${exp_year}`} + { + console.log('delete pmt method!'); + await handleDeletePmtMethodBtnClick(_id); + console.log('delete pmt method done!'); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + > + + +
+ +
+
+
+ ); +} \ 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..4b299a471 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDSection.tsx @@ -0,0 +1,276 @@ +import { + FormControl, + Button, + IconButton, + Input, + Select, + SelectItem, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr, + EmptyState, + Modal, + ModalContent +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useGetOrgTaxIds, + useAddOrgTaxId, + useDeleteOrgTaxId +} from "@app/hooks/api"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPlus, faXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons"; +import { usePopUp } from "@app/hooks/usePopUp"; +import { Controller, useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import * as yup from 'yup'; + +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; + +// TODO: optimize + modularize + +export const TaxIDSection = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? ''); + const addOrgTaxId = useAddOrgTaxId(); + const deleteOrgTaxId = useDeleteOrgTaxId(); + + const { + control, + handleSubmit, + reset, + formState: { isSubmitting } + } = useForm({ + resolver: yupResolver(schema) + }); + + const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + "addTaxID" + ] as const); + + const onTaxIDModalSubmit = async ({ type, value }: AddTaxIDFormData) => { + try { + if (!currentOrg?._id) return; + await addOrgTaxId.mutateAsync({ + organizationId: currentOrg._id, + type, + value + }); + handlePopUpClose("addTaxID"); + } catch (err) { + console.error(err); + } + } + + const handleDeleteTaxIdBtnClick = async (taxId: string) => { + if (!currentOrg?._id) return; + await deleteOrgTaxId.mutateAsync({ + organizationId: currentOrg._id, + taxId + }); + } + + return ( +
+
+

+ Tax ID +

+ +
+ + + + + + + + + + + {!isLoading && data?.length > 0 && data.map(({ + _id, + country, + type, + value + }: { + _id: string; + country: string; + type: string; + value: string; + }) => ( + + + + + + ))} + {isLoading && } + {!isLoading && data?.length === 0 && ( + + + + )} + +
TypeValue
{type}{value} + { + console.log('del'); + await handleDeleteTaxIdBtnClick(_id); + console.log('del done'); + }} + size="lg" + colorSchema="danger" + variant="plain" + ariaLabel="update" + > + + +
+ +
+
+ { + handlePopUpToggle("addTaxID", isOpen); + reset(); + }} + > + +
+ ( + + + + )} + /> + ( + + + + )} + /> +
+ + +
+ +
+
+
+ ); +} \ 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..fd7d049c0 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/BillingReceiptsTab.tsx @@ -0,0 +1,100 @@ +import { + IconButton, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr, + EmptyState +} from "@app/components/v2"; +import { useOrganization } from "@app/context"; +import { + useGetOrgInvoices +} from "@app/hooks/api"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faDownload, faFileInvoice } from "@fortawesome/free-solid-svg-icons"; + +// TODO: optimize + modularize + +export const BillingReceiptsTab = () => { + const { currentOrg } = useOrganization(); + const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? ''); + return ( +
+

Invoices

+ + + + + + + + + + + + + {!isLoading && data?.length > 0 && data.map(({ + _id, + created, + paid, + number, + total, + invoice_pdf + }: { + _id: string; + created: number; + paid: boolean; + number: string; + total: number; + invoice_pdf: string; + }) => { + 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?.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/BillingSelfHostedTab/BillingSelfHostedTab.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/BillingSelfHostedTab.tsx new file mode 100644 index 000000000..e69de29bb diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/index.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/index.tsx new file mode 100644 index 000000000..e69de29bb 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..a14bd1320 --- /dev/null +++ b/frontend/src/views/Settings/BillingSettingsPage/components/index.tsx @@ -0,0 +1,3 @@ +export { BillingCloudTab } from "./BillingCloudTab"; +export { BillingReceiptsTab } from "./BillingReceiptsTab"; +export { BillingDetailsTab } from "./BillingDetailsTab"; \ 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 (
From 924a969307d7fc4bc1bc7d1c851e4d83008b6d87 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 27 Jun 2023 15:39:36 +0700 Subject: [PATCH 2/4] Fix lint errors for revamped billing and usage page --- .../controllers/v1/organizationsController.ts | 47 +-- backend/src/ee/routes/v1/organizations.ts | 57 +--- .../src/components/login/InitialLoginStep.tsx | 2 +- frontend/src/hooks/api/organization/index.ts | 24 +- .../src/hooks/api/organization/queries.tsx | 193 ++++++------ frontend/src/hooks/api/organization/types.ts | 71 +++++ frontend/src/hooks/usePopUp.tsx | 4 +- frontend/src/pages/settings/billing/[id].tsx | 91 +----- .../BillingSettingsPage.tsx | 106 +------ .../BillingCloudTab/BillingCloudTab.tsx | 24 +- .../BillingCloudTab/CurrentPlanSection.tsx | 29 +- .../BillingCloudTab/ManagePlansModal.tsx | 60 ++-- .../BillingCloudTab/ManagePlansTable.tsx | 133 ++++----- .../BillingCloudTab/PreviewSection.tsx | 41 ++- .../BillingDetailsTab/BillingDetailsTab.tsx | 4 +- .../BillingDetailsTab/CompanyNameSection.tsx | 72 +++-- .../BillingDetailsTab/InvoiceEmailSection.tsx | 61 ++-- .../BillingDetailsTab/PmtMethodsSection.tsx | 120 ++------ .../BillingDetailsTab/PmtMethodsTable.tsx | 90 ++++++ .../BillingDetailsTab/TaxIDModal.tsx | 195 ++++++++++++ .../BillingDetailsTab/TaxIDSection.tsx | 277 ++---------------- .../BillingDetailsTab/TaxIDTable.tsx | 137 +++++++++ .../BillingReceiptsTab/BillingReceiptsTab.tsx | 94 +----- .../BillingReceiptsTab/InvoicesTable.tsx | 88 ++++++ .../BillingSelfHostedTab.tsx | 0 .../components/BillingSelfHostedTab/index.tsx | 0 .../BillingTabGroup/BillingTabGroup.tsx | 44 +++ .../components/BillingTabGroup/index.tsx | 1 + .../BillingSettingsPage/components/index.tsx | 4 +- 29 files changed, 989 insertions(+), 1080 deletions(-) create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsTable.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDModal.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDTable.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/InvoicesTable.tsx delete mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/BillingSelfHostedTab.tsx delete mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/index.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingTabGroup/BillingTabGroup.tsx create mode 100644 frontend/src/views/Settings/BillingSettingsPage/components/BillingTabGroup/index.tsx diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index d91efa057..fb13a6575 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -3,25 +3,6 @@ import { getLicenseServerUrl } from "../../../config"; import { licenseServerKeyRequest } from "../../../config/request"; import { EELicenseService } from "../../services"; -export const createProductCheckoutSession = async (req: Request, res: Response) => { - const { - productId, - success_url - } = req.body; - console.log('createProductCheckoutSession req.body: ', req.body); - - const { data } = await licenseServerKeyRequest.post( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/session`, - { - productId, - success_url - } - ); - console.log('createProductCheckoutSession data: ', data); - - return res.status(200).send(data); -} - export const getOrganizationPlansTable = async (req: Request, res: Response) => { const billingCycle = req.query.billingCycle as string; @@ -74,30 +55,6 @@ export const getOrganizationPlanTable = async (req: Request, res: Response) => { return res.status(200).send(data); } -/** - * Update the organization plan to product with id [productId] - * @param req - * @param res - * @returns - */ -export const updateOrganizationPlan = async (req: Request, res: Response) => { - const { - productId, - } = req.body; - - console.log('backend update productId: ', productId); - const { data } = await licenseServerKeyRequest.patch( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan`, - { - productId, - } - ); - - console.log(' ttproductId: ', data); - - 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` @@ -208,9 +165,9 @@ export const deleteOrganizationTaxId = async (req: Request, res: Response) => { } export const getOrganizationInvoices = async (req: Request, res: Response) => { - const { data } = await licenseServerKeyRequest.get( + const { data: { invoices } } = await licenseServerKeyRequest.get( `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices` ); - return res.status(200).send(data); + 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 e9ea8f0e4..37be3a96d 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -11,26 +11,10 @@ import { ACCEPTED, ADMIN, MEMBER, OWNER, } from "../../../variables"; -router.post( - "/:organizationId/billing-details/session", - requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("productId").exists().trim(), - body("success_url").exists().trim(), - validateRequest, - organizationsController.createProductCheckoutSession -); - router.get( "/:organizationId/plans/table", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -45,7 +29,7 @@ router.get( router.get( "/:organizationId/plan", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -60,7 +44,7 @@ router.get( router.get( "/:organizationId/plan/billing", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -75,7 +59,7 @@ router.get( router.get( "/:organizationId/plan/table", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -87,25 +71,10 @@ router.get( organizationsController.getOrganizationPlanTable ); -router.patch( - "/:organizationId/plan", - requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], - }), - requireOrganizationAuth({ - acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED], - }), - param("organizationId").exists().trim(), - body("productId").exists().isString(), - validateRequest, - organizationsController.updateOrganizationPlan -); - router.get( "/:organizationId/billing-details", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -119,7 +88,7 @@ router.get( router.patch( "/:organizationId/billing-details", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -135,7 +104,7 @@ router.patch( router.get( "/:organizationId/billing-details/payment-methods", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -149,7 +118,7 @@ router.get( router.post( "/:organizationId/billing-details/payment-methods", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -165,7 +134,7 @@ router.post( router.delete( "/:organizationId/billing-details/payment-methods/:pmtMethodId", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -180,7 +149,7 @@ router.delete( router.get( "/:organizationId/billing-details/tax-ids", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -194,7 +163,7 @@ router.get( router.post( "/:organizationId/billing-details/tax-ids", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -210,7 +179,7 @@ router.post( router.delete( "/:organizationId/billing-details/tax-ids/:taxId", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -225,7 +194,7 @@ router.delete( router.get( "/:organizationId/invoices", requireAuth({ - acceptedAuthModes: ["jwt", "apiKey"], + acceptedAuthModes: ["jwt"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], 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/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts index a856001c7..cffa80d63 100644 --- a/frontend/src/hooks/api/organization/index.ts +++ b/frontend/src/hooks/api/organization/index.ts @@ -1,18 +1,16 @@ export { - useGetOrgPlanBillingInfo, - useGetOrgPlanTable, - useGetOrgPlansTable, - useGetOrganization, - useRenameOrg, - useGetOrgBillingDetails, - useUpdateOrgBillingDetails, - useGetOrgPmtMethods, useAddOrgPmtMethod, - useDeleteOrgPmtMethod, - useGetOrgTaxIds, useAddOrgTaxId, + useCreateCustomerPortalSession, + useDeleteOrgPmtMethod, useDeleteOrgTaxId, + useGetOrganization, + useGetOrgBillingDetails, useGetOrgInvoices, - useUpdateOrgPlan, - useCreateProductCheckoutSession -} from "./queries"; + 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 08476ba00..d3f161f64 100644 --- a/frontend/src/hooks/api/organization/queries.tsx +++ b/frontend/src/hooks/api/organization/queries.tsx @@ -1,6 +1,17 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; + import { apiRequest } from "@app/config/request"; -import { Organization, RenameOrgDTO, BillingDetails } from "./types"; + +import { + BillingDetails, + Invoice, + Organization, + OrgPlanTable, + PlanBillingInfo, + PmtMethod, + ProductsTable, + RenameOrgDTO, + TaxID} from "./types"; const organizationKeys = { getUserOrganization: ["organization"] as const, @@ -13,50 +24,17 @@ const organizationKeys = { 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; -}; - -// TODO: fix the type situation here and move fetches directly into hooks - -const fetchOrgBillingDetails = async (organizationId: string) => { - const { data } = await apiRequest.get( - `/api/v1/organizations/${organizationId}/billing-details` - ); - - return data; + return data.organizations; + } + }); } -const fetchOrgPmtMethods = async (organizationId: string) => { - const { data } = await apiRequest.get( - `/api/v1/organizations/${organizationId}/billing-details/payment-methods` - ); - - return data; -} - -const fetchOrgTaxIds = async (organizationId: string) => { - const { data } = await apiRequest.get( - `/api/v1/organizations/${organizationId}/billing-details/tax-ids` - ); - - return data; -} - -const fetchOrgInvoices = async (organizationId: string) => { - const { data: { invoices } } = await apiRequest.get( - `/api/v1/organizations/${organizationId}/invoices` - ); - - return invoices; -} - -export const useGetOrganization = () => - useQuery({ queryKey: organizationKeys.getUserOrganization, queryFn: fetchUserOrganization }); - -// mutation export const useRenameOrg = () => { const queryClient = useQueryClient(); @@ -73,7 +51,7 @@ export const useGetOrgPlanBillingInfo = (organizationId: string) => { return useQuery({ queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId), queryFn: async () => { - const { data } = await apiRequest.get( + const { data } = await apiRequest.get( `/api/v1/organizations/${organizationId}/plan/billing` ); @@ -87,7 +65,7 @@ export const useGetOrgPlanTable = (organizationId: string) => { return useQuery({ queryKey: organizationKeys.getOrgPlanTable(organizationId), queryFn: async () => { - const { data } = await apiRequest.get( + const { data } = await apiRequest.get( `/api/v1/organizations/${organizationId}/plan/table` ); @@ -102,12 +80,12 @@ export const useGetOrgPlansTable = ({ billingCycle }: { organizationId: string; - billingCycle: "monthly" | "annual" + billingCycle: "monthly" | "yearly" }) => { return useQuery({ queryKey: organizationKeys.getOrgPlansTable(organizationId, billingCycle), queryFn: async () => { - const { data } = await apiRequest.get( + const { data } = await apiRequest.get( `/api/v1/organizations/${organizationId}/plans/table?billingCycle=${billingCycle}` ); @@ -120,7 +98,13 @@ export const useGetOrgPlansTable = ({ export const useGetOrgBillingDetails = (organizationId: string) => { return useQuery({ queryKey: organizationKeys.getOrgBillingDetails(organizationId), - queryFn: () => fetchOrgBillingDetails(organizationId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/billing-details` + ); + + return data; + }, enabled: true }); } @@ -137,10 +121,14 @@ export const useUpdateOrgBillingDetails = () => { name?: string; email?: string; }) => { - const { data } = await apiRequest.patch(`/api/v1/organizations/${organizationId}/billing-details`, { - name, - email - }); + const { data } = await apiRequest.patch( + `/api/v1/organizations/${organizationId}/billing-details`, + { + name, + email + } + ); + return data; }, onSuccess(_, dto) { @@ -152,7 +140,13 @@ export const useUpdateOrgBillingDetails = () => { export const useGetOrgPmtMethods = (organizationId: string) => { return useQuery({ queryKey: organizationKeys.getOrgPmtMethods(organizationId), - queryFn: () => fetchOrgPmtMethods(organizationId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/billing-details/payment-methods` + ); + + return data; + }, enabled: true }); } @@ -170,10 +164,14 @@ export const useAddOrgPmtMethod = () => { success_url: string; cancel_url: string; }) => { - const { data: { url } } = await apiRequest.post(`/api/v1/organizations/${organizationId}/billing-details/payment-methods`, { - success_url, - cancel_url - }); + const { data: { url } } = await apiRequest.post( + `/api/v1/organizations/${organizationId}/billing-details/payment-methods`, + { + success_url, + cancel_url + } + ); + return url; }, onSuccess(_, dto) { @@ -193,7 +191,10 @@ export const useDeleteOrgPmtMethod = () => { organizationId: string; pmtMethodId: string; }) => { - const { data } = await apiRequest.delete(`/api/v1/organizations/${organizationId}/billing-details/payment-methods/${pmtMethodId}`); + const { data } = await apiRequest.delete( + `/api/v1/organizations/${organizationId}/billing-details/payment-methods/${pmtMethodId}` + ); + return data; }, onSuccess(_, dto) { @@ -205,7 +206,13 @@ export const useDeleteOrgPmtMethod = () => { export const useGetOrgTaxIds = (organizationId: string) => { return useQuery({ queryKey: organizationKeys.getOrgTaxIds(organizationId), - queryFn: () => fetchOrgTaxIds(organizationId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/billing-details/tax-ids` + ); + + return data; + }, enabled: true }); } @@ -223,10 +230,13 @@ export const useAddOrgTaxId = () => { type: string; value: string; }) => { - const { data } = await apiRequest.post(`/api/v1/organizations/${organizationId}/billing-details/tax-ids`, { - type, - value - }); + const { data } = await apiRequest.post( + `/api/v1/organizations/${organizationId}/billing-details/tax-ids`, + { + type, + value + } + ); return data; }, @@ -247,7 +257,10 @@ export const useDeleteOrgTaxId = () => { organizationId: string; taxId: string; }) => { - const { data } = await apiRequest.delete(`/api/v1/organizations/${organizationId}/billing-details/tax-ids/${taxId}`); + const { data } = await apiRequest.delete( + `/api/v1/organizations/${organizationId}/billing-details/tax-ids/${taxId}` + ); + return data; }, onSuccess(_, dto) { @@ -259,54 +272,24 @@ export const useDeleteOrgTaxId = () => { export const useGetOrgInvoices = (organizationId: string) => { return useQuery({ queryKey: organizationKeys.getOrgInvoices(organizationId), - queryFn: () => fetchOrgInvoices(organizationId), + queryFn: async () => { + const { data } = await apiRequest.get( + `/api/v1/organizations/${organizationId}/invoices` + ); + + return data; + }, enabled: true }); } -export const useUpdateOrgPlan = () => { - const queryClient = useQueryClient(); +export const useCreateCustomerPortalSession = () => { return useMutation({ - mutationFn: async ({ - organizationId, - productId - }: { - organizationId: string; - productId: string; - }) => { - const { data } = await apiRequest.patch(`/api/v1/organizations/${organizationId}/plan`, { - productId - }); + mutationFn: async (organizationId: string) => { + const { data } = await apiRequest.post( + `/api/v1/organization/${organizationId}/customer-portal-session` + ); return data; - }, - onSuccess(_, dto) { - queryClient.invalidateQueries([ - organizationKeys.getOrgPlanTable(dto.organizationId), - ]); - } - }); -}; - -export const useCreateProductCheckoutSession = () => { - const queryClient = useQueryClient(); - return useMutation({ - mutationFn: async ({ - organizationId, - productId, - success_url - }: { - organizationId: string; - productId: string; - success_url: string; - }) => { - const { data } = await apiRequest.post(`/api/v1/organizations/${organizationId}/billing-details/session`, { - productId, - success_url - }); - return data; - }, - onSuccess(_, dto) { - console.log('onSuccess'); } }); }; \ 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 94528cd15..a33a7f875 100644 --- a/frontend/src/hooks/api/organization/types.ts +++ b/frontend/src/hooks/api/organization/types.ts @@ -13,4 +13,75 @@ export type RenameOrgDTO = { 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/pages/settings/billing/[id].tsx b/frontend/src/pages/settings/billing/[id].tsx index 39c8e71ac..20cb57f02 100644 --- a/frontend/src/pages/settings/billing/[id].tsx +++ b/frontend/src/pages/settings/billing/[id].tsx @@ -1,57 +1,11 @@ import { useTranslation } from "react-i18next"; import Head from "next/head"; -import { useSubscription } from "@app/context"; + import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage"; export default function SettingsBilling() { - const { subscription } = useSubscription(); - const { t } = useTranslation(); - const plans = [ - { - key: 1, - name: t("billing.starter.name")!, - price: t("billing.free")!, - priceExplanation: t("billing.starter.price-explanation")!, - text: t("billing.starter.text")!, - subtext: t("billing.starter.subtext")!, - buttonTextMain: t("billing.downgrade")!, - buttonTextSecondary: t("billing.learn-more")!, - current: subscription?.slug === "starter" - }, - { - key: 2, - name: "Team", - price: "$8", - priceExplanation: t("billing.professional.price-explanation")!, - text: "Unlimited members, up to 10 projects. Additional developer experience features.", - buttonTextMain: t("billing.upgrade")!, - buttonTextSecondary: t("billing.learn-more")!, - current: subscription?.slug === "team" || subscription?.slug === "team-annual" - }, - { - key: 3, - name: t("billing.professional.name")!, - price: "$18", - priceExplanation: t("billing.professional.price-explanation")!, - text: t("billing.enterprise.text")!, - subtext: t("billing.professional.subtext")!, - buttonTextMain: t("billing.upgrade")!, - buttonTextSecondary: t("billing.learn-more")!, - current: subscription?.slug === "pro" || subscription?.slug === "pro-annual" - }, - { - key: 4, - name: t("billing.enterprise.name")!, - price: t("billing.custom-pricing")!, - text: "Boost the security and efficiency of your engineering teams.", - buttonTextMain: t("billing.schedule-demo")!, - buttonTextSecondary: t("billing.learn-more")!, - current: subscription?.slug === "enterprise" - } - ]; - return (
@@ -63,45 +17,4 @@ export default function SettingsBilling() { ); } -SettingsBilling.requireAuth = true; - - - -// return ( -//
-// -// {t("common.head-title", { title: t("billing.title") })} -// -// -//
-// -//
-//
-//

{t("billing.title")}

-//

{t("billing.description")}

-//
-//
-//
-//

{t("billing.subscription")}

-//
-// {plans.map((plan) => ( -// -// ))} -//
-// {/*

{t("billing.current-usage")}

-//
-//
-//

{numUsers}

-//

-// Organization members -//

-//
-//
-//

1

-//

Organization projects

-//
-//
*/} -//
-//
-//
-// ); +SettingsBilling.requireAuth = true; \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/BillingSettingsPage.tsx b/frontend/src/views/Settings/BillingSettingsPage/BillingSettingsPage.tsx index 02ee258c9..32f61c006 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/BillingSettingsPage.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/BillingSettingsPage.tsx @@ -1,30 +1,9 @@ import { useTranslation } from "react-i18next"; -import Head from "next/head"; -import Plan from "@app/components/billing/Plan"; + import NavHeader from "@app/components/navigation/NavHeader"; -import { useSubscription } from "@app/context"; + import { - Input, - Button, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr -} from "@app/components/v2"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPlus, faMagnifyingGlass, faDownload } from "@fortawesome/free-solid-svg-icons"; - -import { Tab } from '@headlessui/react' -import { Fragment } from 'react' - -import { - BillingCloudTab, - BillingReceiptsTab, - BillingDetailsTab + BillingTabGroup } from "./components"; export const BillingSettingsPage = () => { @@ -36,85 +15,10 @@ export const BillingSettingsPage = () => {

{t("billing.title")}

- {/*

- Manage usage and billing for Infisical Cloud and Self-hosted instances here -

*/} -
-
+
- - - - - {({ selected }) => ( - /* Use the `selected` state to conditionally style the selected tab. */ - - )} - - {/* - {({ selected }) => ( - - )} - */} - - {({ selected }) => ( - /* Use the `selected` state to conditionally style the selected tab. */ - - )} - - - {({ selected }) => ( - /* Use the `selected` state to conditionally style the selected tab. */ - - )} - - - - - - - {/* Content 2 */} - - - - - - - - - - - - {/*
-

{t("billing.subscription")}

-
- {plans.map((plan) => ( - - ))} -
-

{t("billing.current-usage")}

-
-
-

{numUsers}

-

- Organization members -

-
-
-

1

-

Organization projects

-
-
-
*/} +
); }; \ No newline at end of file diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/BillingCloudTab.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/BillingCloudTab.tsx index 331b7545c..4ce5fb1d7 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/BillingCloudTab.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/BillingCloudTab.tsx @@ -1,27 +1,5 @@ -import { useTranslation } from "react-i18next"; -import Head from "next/head"; -import Plan from "@app/components/billing/Plan"; -import NavHeader from "@app/components/navigation/NavHeader"; -import { - Input, - Button, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr -} from "@app/components/v2"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPlus, faMagnifyingGlass, faDownload } from "@fortawesome/free-solid-svg-icons"; - -import { PreviewSection } from "./PreviewSection"; import { CurrentPlanSection } from "./CurrentPlanSection"; - -// TODO: optimize + modularize -// TODO: get cloud plan full +import { PreviewSection } from "./PreviewSection"; export const BillingCloudTab = () => { return ( diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx index cfdacd869..c9973646a 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/CurrentPlanSection.tsx @@ -1,7 +1,8 @@ +import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + import { - Input, - IconButton, - Button, + EmptyState, Table, TableContainer, TableSkeleton, @@ -9,24 +10,20 @@ import { Td, Th, THead, - Tr, - EmptyState -} from "@app/components/v2"; + Tr} from "@app/components/v2"; +import { useOrganization } from "@app/context"; import { useGetOrgPlanTable } from "@app/hooks/api"; -import { useOrganization } from "@app/context"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faFileInvoice, faCircleCheck, faCircleXmark } from "@fortawesome/free-solid-svg-icons"; export const CurrentPlanSection = () => { const { currentOrg } = useOrganization(); - const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? ''); + const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? ""); const displayCell = (value: null | number | string | boolean) => { - if (value === null) return '-'; + if (value === null) return "-"; - if (typeof value === 'boolean') { + if (typeof value === "boolean") { if (value) return ( { - {!isLoading && data?.rows?.length > 0 && data.rows.map(({ + {!isLoading && data && data?.rows?.length > 0 && data.rows.map(({ name, allowed, used - }: { - name: string; - allowed: number | boolean; - used: string; }) => { return ( @@ -76,7 +69,7 @@ export const CurrentPlanSection = () => { ); })} {isLoading && } - {!isLoading && data?.length === 0 && ( + {!isLoading && data && data?.rows?.length === 0 && ( ; + handlePopUpToggle: (popUpName: keyof UsePopUpState<["managePlan"]>, state?: boolean) => void; +}; + export const ManagePlansModal = ({ popUp, - handlePopUpToggle -}) => { - const { subscription, isLoading: isSubscriptionLoading } = useSubscription(); - + handlePopUpToggle +}: Props) => { return ( - { handlePopUpToggle("managePlan", isOpen); @@ -50,14 +30,20 @@ export const ManagePlansModal = ({ {({ selected }) => ( - )} {({ selected }) => ( - )} @@ -75,4 +61,4 @@ export const ManagePlansModal = ({ ); -} \ No newline at end of file +} diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx index f4ae1884c..7e0a73b2f 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/ManagePlansTable.tsx @@ -1,21 +1,9 @@ -import { useSubscription } from "@app/context"; -import { useOrganization } from "@app/context"; -import { Tab } from '@headlessui/react' -import { Fragment } from 'react' -import { - useGetOrgPlanBillingInfo, - useGetOrgPlanTable, - useGetOrgPlansTable, - useUpdateOrgPlan, - useCreateProductCheckoutSession -} from "@app/hooks/api"; +import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; + import { - FormControl, Button, - IconButton, - Input, - Select, - SelectItem, + EmptyState, Table, TableContainer, TableSkeleton, @@ -24,37 +12,31 @@ import { Th, THead, Tr, - EmptyState, - Modal, - ModalContent } from "@app/components/v2"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faFileInvoice, faCircleCheck, faCircleXmark } from "@fortawesome/free-solid-svg-icons"; - -// TODO: upgrade +import { useOrganization,useSubscription } from "@app/context"; +import { + useCreateCustomerPortalSession, + useGetOrgPlansTable} from "@app/hooks/api"; type Props = { - billingCycle: 'monthly' | 'yearly' + billingCycle: "monthly" | "yearly" } export const ManagePlansTable = ({ billingCycle }: Props) => { const { currentOrg } = useOrganization(); - const { subscription, isLoading: isSubscriptionLoading } = useSubscription(); + const { subscription } = useSubscription(); const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({ - organizationId: currentOrg?._id ?? '', + organizationId: currentOrg?._id ?? "", billingCycle }); - const updateOrgPlan = useUpdateOrgPlan(); - const createProductCheckoutSession = useCreateProductCheckoutSession(); - - console.log('tableData: ', tableData); + const createCustomerPortalSession = useCreateCustomerPortalSession(); const displayCell = (value: null | number | string | boolean) => { - if (value === null) return '-'; + if (value === null) return "-"; - if (typeof value === 'boolean') { + if (typeof value === "boolean") { if (value) return ( - {!isTableDataLoading && tableData?.head.length > 0 && ( + {subscription && !isTableDataLoading && tableData && ( {tableData.head.map(({ name, - slug, priceLine - }: { - name: string; - slug: string; - priceLine: string; }) => { return ( - @@ -100,26 +80,28 @@ export const ManagePlansTable = ({ )} - {!isTableDataLoading && tableData?.rows.length > 0 && tableData.rows.map(({ + {subscription && !isTableDataLoading && tableData && tableData.rows.map(({ name, starter, team, pro, enterprise - }: { - name: string; - starter: null | number | string | boolean; - team: null | number | string | boolean; - pro: null | number | string | boolean; - enterprise: null | number | string | boolean; }) => { return ( - + - - - - + + + + ); })} @@ -134,46 +116,53 @@ export const ManagePlansTable = ({ )} - {subscription && !isTableDataLoading && tableData?.head.length > 0 && ( + {subscription && !isTableDataLoading && tableData && ( - + ) : ( ); diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx index e03cb14c9..4050e7936 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx @@ -1,23 +1,26 @@ -import { useSubscription } from "@app/context"; -import { useOrganization } from "@app/context"; -import { useGetOrgPlanBillingInfo } from "@app/hooks/api"; 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 { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? ""); + const createCustomerPortalSession = useCreateCustomerPortalSession(); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ + 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', + const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", { + style: "currency", + currency: "USD", }); return formattedTotal; @@ -35,7 +38,7 @@ export const PreviewSection = () => { return (
- {!isSubscriptionLoading && subscription?.slug !== 'enterprise' && subscription?.slug !== 'pro' && subscription?.slug !== 'pro-annual' && ( + {!isSubscriptionLoading && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && (

Become Infisical

@@ -54,15 +57,29 @@ export const PreviewSection = () => {

Current plan

Starter

-

Manage plan →

+

Price

-

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

+

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

Subscription renews on

-

{formatDate(data.currentPeriodEnd)}

+

+ {formatDate(data.currentPeriodEnd)} +

)} diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/BillingDetailsTab.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/BillingDetailsTab.tsx index 471cdb050..3e149a1fd 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/BillingDetailsTab.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/BillingDetailsTab.tsx @@ -5,11 +5,11 @@ 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 index b8c88ff10..fb2eadf86 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/CompanyNameSection.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/CompanyNameSection.tsx @@ -1,39 +1,37 @@ import { useEffect } from "react"; -import { - Input, -// Button, - FormControl -} from "@app/components/v2"; -import { Controller, useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import * as yup from 'yup'; +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"; -import Button from "@app/components/basic/buttons/Button"; - const schema = yup.object({ - name: yup.string().required('Company name is required') + name: yup.string().required("Company name is required") }).required(); export const CompanyNameSection = () => { + const { createNotification } = useNotificationContext(); const { currentOrg } = useOrganization(); - const { reset, control, register, handleSubmit, watch, formState: { errors } } = useForm({ + const { reset, control, handleSubmit } = useForm({ defaultValues: { - name: '' + name: "" }, resolver: yupResolver(schema) }); - const { data } = useGetOrgBillingDetails(currentOrg?._id ?? ''); + const { data } = useGetOrgBillingDetails(currentOrg?._id ?? ""); const updateOrgBillingDetails = useUpdateOrgBillingDetails(); useEffect(() => { if (data) { reset({ - name: data?.name ?? '' + name: data?.name ?? "" }); } }, [data]); @@ -41,13 +39,22 @@ export const CompanyNameSection = () => { const onFormSubmit = async ({ name }: { name: string }) => { try { if (!currentOrg?._id) return; - if (name === '') 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" + }); } } @@ -56,17 +63,9 @@ export const CompanyNameSection = () => { onSubmit={handleSubmit(onFormSubmit)} className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600" > -
-

- Business name -

- -
+

+ Business name +

{ 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 index cb21aeff2..991a21869 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/InvoiceEmailSection.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/InvoiceEmailSection.tsx @@ -1,12 +1,13 @@ 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 { - Input, - Button, - FormControl -} from "@app/components/v2"; -import { Controller, useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import * as yup from 'yup'; + FormControl, + Input} from "@app/components/v2"; import { useOrganization } from "@app/context"; import { useGetOrgBillingDetails, @@ -14,24 +15,25 @@ import { } from "@app/hooks/api"; const schema = yup.object({ - email: yup.string().required('Email is required') + email: yup.string().required("Email is required") }).required(); export const InvoiceEmailSection = () => { + const { createNotification } = useNotificationContext(); const { currentOrg } = useOrganization(); - const { reset, control, register, handleSubmit, watch, formState: { errors } } = useForm({ + const { reset, control, handleSubmit } = useForm({ defaultValues: { - name: '' + email: "" }, resolver: yupResolver(schema) }); - const { data } = useGetOrgBillingDetails(currentOrg?._id ?? ''); + const { data } = useGetOrgBillingDetails(currentOrg?._id ?? ""); const updateOrgBillingDetails = useUpdateOrgBillingDetails(); useEffect(() => { if (data) { reset({ - email: data?.email ?? '' + email: data?.email ?? "" }); } }, [data]); @@ -39,13 +41,23 @@ export const InvoiceEmailSection = () => { const onFormSubmit = async ({ email }: { email: string }) => { try { if (!currentOrg?._id) return; - if (email === '') 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" + }); } } @@ -54,17 +66,9 @@ export const InvoiceEmailSection = () => { onSubmit={handleSubmit(onFormSubmit)} className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600" > -
-

- Invoice email recipient -

- -
+

+ Invoice email recipient +

{ 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 index f05be12fb..ecdf27bb7 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsSection.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/PmtMethodsSection.tsx @@ -1,32 +1,14 @@ -import { - Button, - IconButton, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr, - EmptyState -} from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { - useGetOrgPmtMethods, - useAddOrgPmtMethod, - useDeleteOrgPmtMethod -} from "@app/hooks/api"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPlus, faXmark, faCreditCard } from "@fortawesome/free-solid-svg-icons"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; -// TODO: optimize + modularize +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 { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? ''); const addOrgPmtMethod = useAddOrgPmtMethod(); - const deleteOrgPmtMethod = useDeleteOrgPmtMethod(); const handleAddPmtMethodBtnClick = async () => { if (!currentOrg?._id) return; @@ -38,14 +20,6 @@ export const PmtMethodsSection = () => { window.location.href = url; } - - const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => { - if (!currentOrg?._id) return; - await deleteOrgPmtMethod.mutateAsync({ - organizationId: currentOrg._id, - pmtMethodId - }); - } return (
@@ -53,78 +27,18 @@ export const PmtMethodsSection = () => {

Payment Methods

- +
+
- -
Feature / Limit +

{name}

{priceLine}

{displayCell(name)}{displayCell(starter)}{displayCell(team)}{displayCell(pro)}{displayCell(enterprise)} + {displayCell(starter)} + + {displayCell(team)} + + {displayCell(pro)} + + {displayCell(enterprise)} +
{tableData.head.map(({ slug, - productId - }: { - slug: string; - productId: string; + tier }) => { + const isCurrentPlan = slug === subscription.slug; + let subscriptionText = "Upgrade"; + + if (subscription.tier > tier) { + subscriptionText = "Downgrade" + } + + if (tier === 3) { + subscriptionText = "Contact sales" + } - console.log('productId: ', productId); return isCurrentPlan ? ( - -

Current

+
+
- - - - - - - - - - - {!isLoading && data?.length > 0 && data.map(({ - _id, - brand, - exp_month, - exp_year, - funding, - last4 - }: { - _id: string; - brand: string; - exp_month: number; - exp_year: number; - funding: string; - last4: string; - }) => ( - - - - - - - - ))} - {isLoading && } - {!isLoading && data?.length === 0 && ( - - - - )} - -
BrandTypeLast 4 DigitsExpiration
{brand}{funding}{last4}{`${exp_month}/${exp_year}`} - { - console.log('delete pmt method!'); - await handleDeletePmtMethodBtnClick(_id); - console.log('delete pmt method done!'); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - > - - -
- -
- +
); } \ 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 index 4b299a471..ff5f0430d 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDSection.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingDetailsTab/TaxIDSection.tsx @@ -1,276 +1,39 @@ -import { - FormControl, - Button, - IconButton, - Input, - Select, - SelectItem, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr, - EmptyState, - Modal, - ModalContent -} from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { - useGetOrgTaxIds, - useAddOrgTaxId, - useDeleteOrgTaxId -} from "@app/hooks/api"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faPlus, faXmark, faFileInvoice } from "@fortawesome/free-solid-svg-icons"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; + +import Button from "@app/components/basic/buttons/Button"; import { usePopUp } from "@app/hooks/usePopUp"; -import { Controller, useForm } from 'react-hook-form'; -import { yupResolver } from '@hookform/resolvers/yup'; -import * as yup from 'yup'; -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; - -// TODO: optimize + modularize +import { TaxIDModal } from "./TaxIDModal"; +import { TaxIDTable } from "./TaxIDTable"; export const TaxIDSection = () => { - const { currentOrg } = useOrganization(); - const { data, isLoading } = useGetOrgTaxIds(currentOrg?._id ?? ''); - const addOrgTaxId = useAddOrgTaxId(); - const deleteOrgTaxId = useDeleteOrgTaxId(); - - const { - control, - handleSubmit, - reset, - formState: { isSubmitting } - } = useForm({ - resolver: yupResolver(schema) - }); - const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([ "addTaxID" ] as const); - const onTaxIDModalSubmit = async ({ type, value }: AddTaxIDFormData) => { - try { - if (!currentOrg?._id) return; - await addOrgTaxId.mutateAsync({ - organizationId: currentOrg._id, - type, - value - }); - handlePopUpClose("addTaxID"); - } catch (err) { - console.error(err); - } - } - - const handleDeleteTaxIdBtnClick = async (taxId: string) => { - if (!currentOrg?._id) return; - await deleteOrgTaxId.mutateAsync({ - organizationId: currentOrg._id, - taxId - }); - } - return (

Tax ID

- -
- - - - - - - - - - - {!isLoading && data?.length > 0 && data.map(({ - _id, - country, - type, - value - }: { - _id: string; - country: string; - type: string; - value: string; - }) => ( - - - - - - ))} - {isLoading && } - {!isLoading && data?.length === 0 && ( - - - - )} - -
TypeValue
{type}{value} - { - console.log('del'); - await handleDeleteTaxIdBtnClick(_id); - console.log('del done'); - }} - size="lg" - colorSchema="danger" - variant="plain" - ariaLabel="update" - > - - -
- -
-
- { - handlePopUpToggle("addTaxID", isOpen); - reset(); - }} - > - -
- ( - - - - )} - /> - ( - - - - )} - /> -
+
- -
- - - + color="mineshaft" + size="md" + icon={faPlus} + onButtonPressed={() => handlePopUpOpen("addTaxID")} + /> +
+
+ +
); } \ 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/BillingReceiptsTab/BillingReceiptsTab.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/BillingReceiptsTab.tsx index fd7d049c0..f5c542186 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/BillingReceiptsTab.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingReceiptsTab/BillingReceiptsTab.tsx @@ -1,100 +1,10 @@ -import { - IconButton, - Table, - TableContainer, - TableSkeleton, - TBody, - Td, - Th, - THead, - Tr, - EmptyState -} from "@app/components/v2"; -import { useOrganization } from "@app/context"; -import { - useGetOrgInvoices -} from "@app/hooks/api"; -import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { faDownload, faFileInvoice } from "@fortawesome/free-solid-svg-icons"; - -// TODO: optimize + modularize +import { InvoicesTable } from "./InvoicesTable"; export const BillingReceiptsTab = () => { - const { currentOrg } = useOrganization(); - const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? ''); return (

Invoices

- - - - - - - - - - - - - {!isLoading && data?.length > 0 && data.map(({ - _id, - created, - paid, - number, - total, - invoice_pdf - }: { - _id: string; - created: number; - paid: boolean; - number: string; - total: number; - invoice_pdf: string; - }) => { - 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?.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/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/BillingSelfHostedTab/BillingSelfHostedTab.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/BillingSelfHostedTab.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/index.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingSelfHostedTab/index.tsx deleted file mode 100644 index e69de29bb..000000000 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 index a14bd1320..6c13599f8 100644 --- a/frontend/src/views/Settings/BillingSettingsPage/components/index.tsx +++ b/frontend/src/views/Settings/BillingSettingsPage/components/index.tsx @@ -1,3 +1 @@ -export { BillingCloudTab } from "./BillingCloudTab"; -export { BillingReceiptsTab } from "./BillingReceiptsTab"; -export { BillingDetailsTab } from "./BillingDetailsTab"; \ No newline at end of file +export { BillingTabGroup } from "./BillingTabGroup"; \ No newline at end of file From da144b4d02a297c179dd8833f5b02c4939ada27e Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 27 Jun 2023 15:56:48 +0700 Subject: [PATCH 3/4] Hide usage and billing from Navbar in self-hosted --- .../AppLayout/components/NavBar/NavBar.tsx | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) 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")}
+
+ + )}