mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Continue to make progress on usage and billing page revamp
This commit is contained in:
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -57,7 +57,7 @@ export default function NavHeader({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="ml-6 flex flex-row items-center pt-6">
|
||||
<div className="flex flex-row items-center pt-6">
|
||||
<div className="mr-2 flex h-6 w-6 items-center justify-center rounded-md bg-primary-900 text-mineshaft-100">
|
||||
{currentOrg?.name?.charAt(0)}
|
||||
</div>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<BillingDetails>(
|
||||
`/api/v1/organizations/${organizationId}/billing-details`
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const fetchOrgPmtMethods = async (organizationId: string) => {
|
||||
const { data } = await apiRequest.get<BillingDetails>(
|
||||
`/api/v1/organizations/${organizationId}/billing-details/payment-methods`
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const fetchOrgTaxIds = async (organizationId: string) => {
|
||||
const { data } = await apiRequest.get<BillingDetails>(
|
||||
`/api/v1/organizations/${organizationId}/billing-details/tax-ids`
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const fetchOrgInvoices = async (organizationId: string) => {
|
||||
const { data: { invoices } } = await apiRequest.get<BillingDetails>(
|
||||
`/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<BillingDetails>(
|
||||
`/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<BillingDetails>(
|
||||
`/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<BillingDetails>(
|
||||
`/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');
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -9,3 +9,8 @@ export type RenameOrgDTO = {
|
||||
orgId: string;
|
||||
newOrgName: string;
|
||||
};
|
||||
|
||||
export type BillingDetails = {
|
||||
name: string;
|
||||
email: string;
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="flex flex-col justify-between bg-bunker-800 pb-4 text-white">
|
||||
<div className="h-full bg-bunker-800">
|
||||
<Head>
|
||||
<title>{t("common.head-title", { title: t("billing.title") })}</title>
|
||||
<link rel="icon" href="/infisical.ico" />
|
||||
</Head>
|
||||
<div className="flex flex-row">
|
||||
<div className="w-full pb-2">
|
||||
<NavHeader pageName={t("billing.title")} />
|
||||
<div className="my-8 ml-6 flex max-w-5xl flex-row items-center justify-between text-xl">
|
||||
<div className="flex flex-col items-start justify-start text-3xl">
|
||||
<p className="mr-4 font-semibold text-gray-200">{t("billing.title")}</p>
|
||||
<p className="mr-4 text-base font-normal text-gray-400">{t("billing.description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-6 flex w-max flex-col text-mineshaft-50">
|
||||
<p className="text-xl font-semibold">{t("billing.subscription")}</p>
|
||||
<div className="mt-4 grid grid-cols-2 grid-rows-2 gap-y-6 gap-x-3 overflow-x-auto">
|
||||
{plans.map((plan) => (
|
||||
<Plan key={plan.name} plan={plan} />
|
||||
))}
|
||||
</div>
|
||||
{/* <p className="mt-12 text-xl font-bold">{t("billing.current-usage")}</p>
|
||||
<div className="flex flex-row">
|
||||
<div className="mr-4 mt-8 flex w-60 flex-col items-center justify-center rounded-md bg-white/5 pt-6 pb-10 text-gray-300">
|
||||
<p className="text-6xl font-bold">{numUsers}</p>
|
||||
<p className="text-gray-300">
|
||||
Organization members
|
||||
</p>
|
||||
</div>
|
||||
<div className="mr-4 mt-8 text-gray-300 w-60 pt-6 pb-10 rounded-md bg-white/5 flex justify-center items-center flex flex-col">
|
||||
<p className="text-6xl font-bold">1 </p>
|
||||
<p className="text-gray-300">Organization projects</p>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<BillingSettingsPage />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SettingsBilling.requireAuth = true;
|
||||
|
||||
|
||||
|
||||
// return (
|
||||
// <div className="h-full bg-bunker-800">
|
||||
// <Head>
|
||||
// <title>{t("common.head-title", { title: t("billing.title") })}</title>
|
||||
// <link rel="icon" href="/infisical.ico" />
|
||||
// </Head>
|
||||
// <div className="h-full bg-red">
|
||||
// <NavHeader pageName={t("billing.title")} />
|
||||
// <div className="my-8 ml-6 flex max-w-5xl flex-row items-center justify-between text-xl">
|
||||
// <div className="flex flex-col items-start justify-start text-3xl">
|
||||
// <p className="mr-4 font-semibold text-gray-200">{t("billing.title")}</p>
|
||||
// <p className="mr-4 text-base font-normal text-gray-400">{t("billing.description")}</p>
|
||||
// </div>
|
||||
// </div>
|
||||
// <div className="ml-6 flex w-max flex-col text-mineshaft-50">
|
||||
// <p className="text-xl font-semibold">{t("billing.subscription")}</p>
|
||||
// <div className="mt-4 grid grid-cols-2 grid-rows-2 gap-y-6 gap-x-3 overflow-x-auto">
|
||||
// {plans.map((plan) => (
|
||||
// <Plan key={plan.name} plan={plan} />
|
||||
// ))}
|
||||
// </div>
|
||||
// {/* <p className="mt-12 text-xl font-bold">{t("billing.current-usage")}</p>
|
||||
// <div className="flex flex-row">
|
||||
// <div className="mr-4 mt-8 flex w-60 flex-col items-center justify-center rounded-md bg-white/5 pt-6 pb-10 text-gray-300">
|
||||
// <p className="text-6xl font-bold">{numUsers}</p>
|
||||
// <p className="text-gray-300">
|
||||
// Organization members
|
||||
// </p>
|
||||
// </div>
|
||||
// <div className="mr-4 mt-8 text-gray-300 w-60 pt-6 pb-10 rounded-md bg-white/5 flex justify-center items-center flex flex-col">
|
||||
// <p className="text-6xl font-bold">1 </p>
|
||||
// <p className="text-gray-300">Organization projects</p>
|
||||
// </div>
|
||||
// </div> */}
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
// );
|
||||
|
||||
@@ -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 (
|
||||
<div className="h-full p-8">
|
||||
<NavHeader pageName={t("billing.title")} />
|
||||
|
||||
<div className="flex text-3xl mt-8 items-start max-w-screen-lg">
|
||||
<div className="flex-1">
|
||||
<p className="font-semibold text-gray-200">{t("billing.title")}</p>
|
||||
{/* <p className="text-base font-normal text-gray-400">
|
||||
Manage usage and billing for Infisical Cloud and Self-hosted instances here
|
||||
</p> */}
|
||||
</div>
|
||||
<div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tab.Group>
|
||||
<Tab.List className="mt-8 border-b-2 border-mineshaft-800 max-w-screen-lg">
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
/* Use the `selected` state to conditionally style the selected tab. */
|
||||
<button className={`p-4 ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"} w-30 font-semibold outline-none`}>
|
||||
Infisical Cloud
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
{/* <Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button className={`p-4 ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"} w-30 font-semibold outline-none`}>
|
||||
Self-hosted
|
||||
</button>
|
||||
)}
|
||||
</Tab> */}
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
/* Use the `selected` state to conditionally style the selected tab. */
|
||||
<button className={`p-4 ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"} w-30 font-semibold outline-none`}>
|
||||
Receipts
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
/* Use the `selected` state to conditionally style the selected tab. */
|
||||
<button className={`p-4 ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"} w-30 font-semibold outline-none`}>
|
||||
Billing details
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels>
|
||||
<Tab.Panel>
|
||||
<BillingCloudTab />
|
||||
</Tab.Panel>
|
||||
{/* <Tab.Panel>Content 2</Tab.Panel> */}
|
||||
<Tab.Panel>
|
||||
<BillingReceiptsTab />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<BillingDetailsTab />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
|
||||
|
||||
|
||||
{/* <div className="flex w-max flex-col text-mineshaft-50 mt-8">
|
||||
<p className="text-xl font-semibold">{t("billing.subscription")}</p>
|
||||
<div className="mt-4 grid grid-cols-2 grid-rows-2 gap-y-6 gap-x-3 overflow-x-auto">
|
||||
{plans.map((plan) => (
|
||||
<Plan key={plan.name} plan={plan} />
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-12 text-xl font-bold">{t("billing.current-usage")}</p>
|
||||
<div className="flex flex-row">
|
||||
<div className="mr-4 mt-8 flex w-60 flex-col items-center justify-center rounded-md bg-white/5 pt-6 pb-10 text-gray-300">
|
||||
<p className="text-6xl font-bold">{numUsers}</p>
|
||||
<p className="text-gray-300">
|
||||
Organization members
|
||||
</p>
|
||||
</div>
|
||||
<div className="mr-4 mt-8 text-gray-300 w-60 pt-6 pb-10 rounded-md bg-white/5 flex justify-center items-center flex flex-col">
|
||||
<p className="text-6xl font-bold">1 </p>
|
||||
<p className="text-gray-300">Organization projects</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<div>
|
||||
<PreviewSection />
|
||||
<CurrentPlanSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleCheck}
|
||||
color='#2ecc71'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleXmark}
|
||||
color='#e74c3c'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white mb-8">Current Usage</h2>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="w-1/3">Feature</Th>
|
||||
<Th className="w-1/3">Allowed</Th>
|
||||
<Th className="w-1/3">Used</Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isLoading && data?.rows?.length > 0 && data.rows.map(({
|
||||
name,
|
||||
allowed,
|
||||
used
|
||||
}: {
|
||||
name: string;
|
||||
allowed: number | boolean;
|
||||
used: string;
|
||||
}) => {
|
||||
return (
|
||||
<Tr key={`current-plan-row-${name}`} className="h-12">
|
||||
<Td>{name}</Td>
|
||||
<Td>{displayCell(allowed)}</Td>
|
||||
<Td>{used}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isLoading && <TableSkeleton columns={5} key="invoices" />}
|
||||
{!isLoading && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={3}>
|
||||
<EmptyState
|
||||
title="No plan details found"
|
||||
icon={faFileInvoice}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<Modal
|
||||
isOpen={popUp?.managePlan?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("managePlan", isOpen);
|
||||
}}
|
||||
>
|
||||
<ModalContent className="max-w-screen-lg" title="Infisical Cloud Plans">
|
||||
<Tab.Group>
|
||||
<Tab.List className="border-b-2 border-mineshaft-600 max-w-screen-lg">
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button className={`p-4 ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"} w-30 font-semibold outline-none`}>
|
||||
Bill monthly
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
<Tab as={Fragment}>
|
||||
{({ selected }) => (
|
||||
<button className={`p-4 ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"} w-30 font-semibold outline-none`}>
|
||||
Bill yearly
|
||||
</button>
|
||||
)}
|
||||
</Tab>
|
||||
</Tab.List>
|
||||
<Tab.Panels className="mt-4">
|
||||
<Tab.Panel>
|
||||
<ManagePlansTable billingCycle="monthly" />
|
||||
</Tab.Panel>
|
||||
<Tab.Panel>
|
||||
<ManagePlansTable billingCycle="yearly" />
|
||||
</Tab.Panel>
|
||||
</Tab.Panels>
|
||||
</Tab.Group>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleCheck}
|
||||
color='#2ecc71'
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<FontAwesomeIcon
|
||||
icon={faCircleXmark}
|
||||
color='#e74c3c'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<THead>
|
||||
{!isTableDataLoading && tableData?.head.length > 0 && (
|
||||
<Tr>
|
||||
<Th className="">Feature / Limit</Th>
|
||||
{tableData.head.map(({
|
||||
name,
|
||||
slug,
|
||||
priceLine
|
||||
}: {
|
||||
name: string;
|
||||
slug: string;
|
||||
priceLine: string;
|
||||
}) => {
|
||||
return (
|
||||
<Th className={`${slug === subscription.slug ? "bg-mineshaft-600 text-center" : "text-center"}`}>
|
||||
<p>{name}</p>
|
||||
<p>{priceLine}</p>
|
||||
</Th>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
)}
|
||||
</THead>
|
||||
<TBody>
|
||||
{!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 (
|
||||
<Tr className="h-12">
|
||||
<Td>{displayCell(name)}</Td>
|
||||
<Td className={'starter' === subscription.slug ? "bg-mineshaft-600 text-center" : "text-center"}>{displayCell(starter)}</Td>
|
||||
<Td className={'team' === subscription.slug ? "bg-mineshaft-600 text-center" : "text-center"}>{displayCell(team)}</Td>
|
||||
<Td className={'pro' === subscription.slug ? "bg-mineshaft-600 text-center" : "text-center"}>{displayCell(pro)}</Td>
|
||||
<Td className={'enterprise' === subscription.slug ? "bg-mineshaft-600 text-center" : "text-center"}>{displayCell(enterprise)}</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isTableDataLoading && <TableSkeleton columns={5} key="cloud-products" />}
|
||||
{!isTableDataLoading && tableData?.rows.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState
|
||||
title="No cloud product details found"
|
||||
icon={faFileInvoice}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
{subscription && !isTableDataLoading && tableData?.head.length > 0 && (
|
||||
<Tr className="h-12">
|
||||
<Td></Td>
|
||||
{tableData.head.map(({
|
||||
slug,
|
||||
productId
|
||||
}: {
|
||||
slug: string;
|
||||
productId: string;
|
||||
}) => {
|
||||
const isCurrentPlan = slug === subscription.slug;
|
||||
|
||||
console.log('productId: ', productId);
|
||||
return isCurrentPlan ? (
|
||||
<Td className="bg-mineshaft-600">
|
||||
<p className="text-center font-semibold">Current</p>
|
||||
</Td>
|
||||
) : (
|
||||
<Td>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
console.log('upgrade to product: ', productId);
|
||||
if (!currentOrg?._id) return;
|
||||
// const test = await updateOrgPlan.mutateAsync({
|
||||
// organizationId: currentOrg._id,
|
||||
// productId
|
||||
// });
|
||||
|
||||
const { url } = await createProductCheckoutSession.mutateAsync({
|
||||
organizationId: currentOrg._id,
|
||||
productId,
|
||||
success_url: window.location.href
|
||||
})
|
||||
|
||||
window.location.href = url;
|
||||
}}
|
||||
color="mineshaft"
|
||||
className="w-full"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
</Td>
|
||||
);
|
||||
})}
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div>
|
||||
{!isSubscriptionLoading && subscription?.slug !== 'enterprise' && subscription?.slug !== 'pro' && subscription?.slug !== 'pro-annual' && (
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mt-8 flex items-center bg-mineshaft-600 max-w-screen-lg">
|
||||
<div className="flex-1">
|
||||
<h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2>
|
||||
<p className="text-gray-400 mt-4">Unlimited members, projects, RBAC, smart alerts, and so much more</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => handlePopUpOpen("managePlan")}
|
||||
color="mineshaft"
|
||||
>
|
||||
Upgrade
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && data && (
|
||||
<div className="flex mt-8 max-w-screen-lg">
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600">
|
||||
<p className="mb-2 text-gray-400">Current plan</p>
|
||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">Starter</p>
|
||||
<p className="text-primary">Manage plan →</p>
|
||||
</div>
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4">
|
||||
<p className="mb-2 text-gray-400">Price</p>
|
||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">{`${formatAmount(data.amount)} / ${data.interval}`}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600">
|
||||
<p className="mb-2 text-gray-400">Subscription renews on</p>
|
||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">{formatDate(data.currentPeriodEnd)}</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<ManagePlansModal
|
||||
popUp={popUp}
|
||||
handlePopUpToggle={handlePopUpToggle}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { BillingCloudTab } from "./BillingCloudTab";
|
||||
@@ -0,0 +1,15 @@
|
||||
import { CompanyNameSection } from "./CompanyNameSection";
|
||||
import { InvoiceEmailSection } from "./InvoiceEmailSection";
|
||||
import { PmtMethodsSection } from "./PmtMethodsSection";
|
||||
import { TaxIDSection } from "./TaxIDSection";
|
||||
|
||||
export const BillingDetailsTab = () => {
|
||||
return (
|
||||
<div>
|
||||
<CompanyNameSection />
|
||||
<InvoiceEmailSection />
|
||||
<PmtMethodsSection />
|
||||
<TaxIDSection />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||
Business name
|
||||
</h2>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="Acme Corp"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="name"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
text="Test"
|
||||
onButtonPressed={() => {
|
||||
console.log('Button pressed');
|
||||
// setIsAddApiKeyDialogOpen(true);
|
||||
}}
|
||||
color="mineshaft"
|
||||
// icon={faPlus}
|
||||
size="md"
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
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";
|
||||
|
||||
const schema = yup.object({
|
||||
email: yup.string().required('Email is required')
|
||||
}).required();
|
||||
|
||||
export const InvoiceEmailSection = () => {
|
||||
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({
|
||||
email: data?.email ?? ''
|
||||
});
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
const onFormSubmit = async ({ email }: { email: string }) => {
|
||||
try {
|
||||
if (!currentOrg?._id) return;
|
||||
if (email === '') return;
|
||||
await updateOrgBillingDetails.mutateAsync({
|
||||
email,
|
||||
organizationId: currentOrg._id
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onFormSubmit)}
|
||||
className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600"
|
||||
>
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||
Invoice email recipient
|
||||
</h2>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
type="submit"
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div className="max-w-md">
|
||||
<Controller
|
||||
defaultValue=""
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl isError={Boolean(error)} errorText={error?.message}>
|
||||
<Input
|
||||
placeholder="jane@acme.com"
|
||||
{...field}
|
||||
className="bg-mineshaft-800"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
control={control}
|
||||
name="email"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||
Payment Methods
|
||||
</h2>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={handleAddPmtMethodBtnClick}
|
||||
>
|
||||
Add method
|
||||
</Button>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">Brand</Th>
|
||||
<Th className="flex-1">Type</Th>
|
||||
<Th className="flex-1">Last 4 Digits</Th>
|
||||
<Th className="flex-1">Expiration</Th>
|
||||
<Th className="w-5"></Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!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;
|
||||
}) => (
|
||||
<Tr key={`pmt-method-${_id}`} className="h-10">
|
||||
<Td>{brand}</Td>
|
||||
<Td>{funding}</Td>
|
||||
<Td>{last4}</Td>
|
||||
<Td>{`${exp_month}/${exp_year}`}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
console.log('delete pmt method!');
|
||||
await handleDeletePmtMethodBtnClick(_id);
|
||||
console.log('delete pmt method done!');
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
{isLoading && <TableSkeleton columns={5} key="pmt-methods" />}
|
||||
{!isLoading && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState
|
||||
title="No payment methods on file"
|
||||
icon={faCreditCard}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<typeof schema>;
|
||||
|
||||
// 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<AddTaxIDFormData>({
|
||||
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 (
|
||||
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||
<div className="flex items-center mb-8">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||
Tax ID
|
||||
</h2>
|
||||
<Button
|
||||
color="mineshaft"
|
||||
type="submit"
|
||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||
onClick={() => handlePopUpOpen("addTaxID")}
|
||||
>
|
||||
Add Tax ID
|
||||
</Button>
|
||||
</div>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">Type</Th>
|
||||
<Th className="flex-1">Value</Th>
|
||||
<Th className="w-5"></Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!isLoading && data?.length > 0 && data.map(({
|
||||
_id,
|
||||
country,
|
||||
type,
|
||||
value
|
||||
}: {
|
||||
_id: string;
|
||||
country: string;
|
||||
type: string;
|
||||
value: string;
|
||||
}) => (
|
||||
<Tr key={`tax-id-${_id}`} className="h-10">
|
||||
<Td>{type}</Td>
|
||||
<Td>{value}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={async () => {
|
||||
console.log('del');
|
||||
await handleDeleteTaxIdBtnClick(_id);
|
||||
console.log('del done');
|
||||
}}
|
||||
size="lg"
|
||||
colorSchema="danger"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
))}
|
||||
{isLoading && <TableSkeleton columns={3} key="tax-ids" />}
|
||||
{!isLoading && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState
|
||||
title="No Tax IDs on file"
|
||||
icon={faFileInvoice}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<Modal
|
||||
isOpen={popUp?.addTaxID?.isOpen}
|
||||
onOpenChange={(isOpen) => {
|
||||
handlePopUpToggle("addTaxID", isOpen);
|
||||
reset();
|
||||
}}
|
||||
>
|
||||
<ModalContent title="Add Tax ID">
|
||||
<form onSubmit={handleSubmit(onTaxIDModalSubmit)}>
|
||||
<Controller
|
||||
control={control}
|
||||
name="type"
|
||||
defaultValue="eu_vat"
|
||||
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Type"
|
||||
errorText={error?.message}
|
||||
isError={Boolean(error)}
|
||||
>
|
||||
<Select
|
||||
defaultValue={field.value}
|
||||
{...field}
|
||||
onValueChange={(e) => onChange(e)}
|
||||
className="w-full"
|
||||
>
|
||||
{taxIDTypes.map(({ label, value }) => (
|
||||
<SelectItem value={String(value || "")} key={label}>
|
||||
{label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue=""
|
||||
name="value"
|
||||
render={({ field, fieldState: { error } }) => (
|
||||
<FormControl
|
||||
label="Value"
|
||||
isError={Boolean(error)}
|
||||
errorText={error?.message}
|
||||
>
|
||||
<Input
|
||||
{...field}
|
||||
placeholder="DE000000000"
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
<div className="mt-8 flex items-center">
|
||||
<Button
|
||||
className="mr-4"
|
||||
size="sm"
|
||||
type="submit"
|
||||
isLoading={isSubmitting}
|
||||
isDisabled={isSubmitting}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { BillingDetailsTab } from "./BillingDetailsTab";
|
||||
@@ -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 (
|
||||
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
||||
<h2 className="text-xl font-semibold flex-1 text-white">Invoices</h2>
|
||||
<TableContainer className="mt-4">
|
||||
<Table>
|
||||
<THead>
|
||||
<Tr>
|
||||
<Th className="flex-1">Invoice #</Th>
|
||||
<Th className="flex-1">Date</Th>
|
||||
<Th className="flex-1">Status</Th>
|
||||
<Th className="flex-1">Amount</Th>
|
||||
<Th className="w-5"></Th>
|
||||
</Tr>
|
||||
</THead>
|
||||
<TBody>
|
||||
{!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 (
|
||||
<Tr key={`invoice-${_id}`} className="h-10">
|
||||
<Td>{number}</Td>
|
||||
<Td>{formattedDate}</Td>
|
||||
<Td>{paid ? "Paid" : "Not Paid"}</Td>
|
||||
<Td>{formattedTotal}</Td>
|
||||
<Td>
|
||||
<IconButton
|
||||
onClick={async () => window.open(invoice_pdf)}
|
||||
size="lg"
|
||||
variant="plain"
|
||||
ariaLabel="update"
|
||||
>
|
||||
<FontAwesomeIcon icon={faDownload} />
|
||||
</IconButton>
|
||||
</Td>
|
||||
</Tr>
|
||||
);
|
||||
})}
|
||||
{isLoading && <TableSkeleton columns={5} key="invoices" />}
|
||||
{!isLoading && data?.length === 0 && (
|
||||
<Tr>
|
||||
<Td colSpan={5}>
|
||||
<EmptyState
|
||||
title="No invoices on file"
|
||||
icon={faFileInvoice}
|
||||
/>
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</TBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { BillingReceiptsTab } from "./BillingReceiptsTab";
|
||||
@@ -0,0 +1,3 @@
|
||||
export { BillingCloudTab } from "./BillingCloudTab";
|
||||
export { BillingReceiptsTab } from "./BillingReceiptsTab";
|
||||
export { BillingDetailsTab } from "./BillingDetailsTab";
|
||||
@@ -0,0 +1 @@
|
||||
export { BillingSettingsPage } from "./BillingSettingsPage";
|
||||
@@ -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 (
|
||||
<div className="container mx-auto flex flex-col justify-between bg-bunker-800 text-white">
|
||||
<NavHeader pageName={t("settings.org.title")} />
|
||||
|
||||
Reference in New Issue
Block a user