mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-08 20:28:16 +00:00
Fix lint errors for revamped billing and usage page
This commit is contained in:
@@ -3,25 +3,6 @@ import { getLicenseServerUrl } from "../../../config";
|
|||||||
import { licenseServerKeyRequest } from "../../../config/request";
|
import { licenseServerKeyRequest } from "../../../config/request";
|
||||||
import { EELicenseService } from "../../services";
|
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) => {
|
export const getOrganizationPlansTable = async (req: Request, res: Response) => {
|
||||||
const billingCycle = req.query.billingCycle as string;
|
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);
|
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) => {
|
export const getOrganizationBillingDetails = async (req: Request, res: Response) => {
|
||||||
const { data } = await licenseServerKeyRequest.get(
|
const { data } = await licenseServerKeyRequest.get(
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details`
|
`${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) => {
|
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`
|
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/invoices`
|
||||||
);
|
);
|
||||||
|
|
||||||
return res.status(200).send(data);
|
return res.status(200).send(invoices);
|
||||||
}
|
}
|
||||||
@@ -11,26 +11,10 @@ import {
|
|||||||
ACCEPTED, ADMIN, MEMBER, OWNER,
|
ACCEPTED, ADMIN, MEMBER, OWNER,
|
||||||
} from "../../../variables";
|
} 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(
|
router.get(
|
||||||
"/:organizationId/plans/table",
|
"/:organizationId/plans/table",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
@@ -45,7 +29,7 @@ router.get(
|
|||||||
router.get(
|
router.get(
|
||||||
"/:organizationId/plan",
|
"/:organizationId/plan",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
@@ -60,7 +44,7 @@ router.get(
|
|||||||
router.get(
|
router.get(
|
||||||
"/:organizationId/plan/billing",
|
"/:organizationId/plan/billing",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
@@ -75,7 +59,7 @@ router.get(
|
|||||||
router.get(
|
router.get(
|
||||||
"/:organizationId/plan/table",
|
"/:organizationId/plan/table",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
@@ -87,25 +71,10 @@ router.get(
|
|||||||
organizationsController.getOrganizationPlanTable
|
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(
|
router.get(
|
||||||
"/:organizationId/billing-details",
|
"/:organizationId/billing-details",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
@@ -119,7 +88,7 @@ router.get(
|
|||||||
router.patch(
|
router.patch(
|
||||||
"/:organizationId/billing-details",
|
"/:organizationId/billing-details",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
@@ -135,7 +104,7 @@ router.patch(
|
|||||||
router.get(
|
router.get(
|
||||||
"/:organizationId/billing-details/payment-methods",
|
"/:organizationId/billing-details/payment-methods",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
@@ -149,7 +118,7 @@ router.get(
|
|||||||
router.post(
|
router.post(
|
||||||
"/:organizationId/billing-details/payment-methods",
|
"/:organizationId/billing-details/payment-methods",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
@@ -165,7 +134,7 @@ router.post(
|
|||||||
router.delete(
|
router.delete(
|
||||||
"/:organizationId/billing-details/payment-methods/:pmtMethodId",
|
"/:organizationId/billing-details/payment-methods/:pmtMethodId",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
@@ -180,7 +149,7 @@ router.delete(
|
|||||||
router.get(
|
router.get(
|
||||||
"/:organizationId/billing-details/tax-ids",
|
"/:organizationId/billing-details/tax-ids",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
@@ -194,7 +163,7 @@ router.get(
|
|||||||
router.post(
|
router.post(
|
||||||
"/:organizationId/billing-details/tax-ids",
|
"/:organizationId/billing-details/tax-ids",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
@@ -210,7 +179,7 @@ router.post(
|
|||||||
router.delete(
|
router.delete(
|
||||||
"/:organizationId/billing-details/tax-ids/:taxId",
|
"/:organizationId/billing-details/tax-ids/:taxId",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
@@ -225,7 +194,7 @@ router.delete(
|
|||||||
router.get(
|
router.get(
|
||||||
"/:organizationId/invoices",
|
"/:organizationId/invoices",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ["jwt", "apiKey"],
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireOrganizationAuth({
|
requireOrganizationAuth({
|
||||||
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
|||||||
@@ -1,18 +1,16 @@
|
|||||||
export {
|
export {
|
||||||
useGetOrgPlanBillingInfo,
|
|
||||||
useGetOrgPlanTable,
|
|
||||||
useGetOrgPlansTable,
|
|
||||||
useGetOrganization,
|
|
||||||
useRenameOrg,
|
|
||||||
useGetOrgBillingDetails,
|
|
||||||
useUpdateOrgBillingDetails,
|
|
||||||
useGetOrgPmtMethods,
|
|
||||||
useAddOrgPmtMethod,
|
useAddOrgPmtMethod,
|
||||||
useDeleteOrgPmtMethod,
|
|
||||||
useGetOrgTaxIds,
|
|
||||||
useAddOrgTaxId,
|
useAddOrgTaxId,
|
||||||
|
useCreateCustomerPortalSession,
|
||||||
|
useDeleteOrgPmtMethod,
|
||||||
useDeleteOrgTaxId,
|
useDeleteOrgTaxId,
|
||||||
|
useGetOrganization,
|
||||||
|
useGetOrgBillingDetails,
|
||||||
useGetOrgInvoices,
|
useGetOrgInvoices,
|
||||||
useUpdateOrgPlan,
|
useGetOrgPlanBillingInfo,
|
||||||
useCreateProductCheckoutSession
|
useGetOrgPlansTable,
|
||||||
} from "./queries";
|
useGetOrgPlanTable,
|
||||||
|
useGetOrgPmtMethods,
|
||||||
|
useGetOrgTaxIds,
|
||||||
|
useRenameOrg,
|
||||||
|
useUpdateOrgBillingDetails} from "./queries";
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
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 = {
|
const organizationKeys = {
|
||||||
getUserOrganization: ["organization"] as const,
|
getUserOrganization: ["organization"] as const,
|
||||||
@@ -13,50 +24,17 @@ const organizationKeys = {
|
|||||||
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const
|
getOrgInvoices: (orgId: string) => [{ orgId }, "organization-invoices"] as const
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchUserOrganization = async () => {
|
export const useGetOrganization = () => {
|
||||||
const { data } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization");
|
return useQuery({
|
||||||
|
queryKey: organizationKeys.getUserOrganization,
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{ organizations: Organization[] }>("/api/v1/organization");
|
||||||
|
|
||||||
return data.organizations;
|
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 });
|
|
||||||
|
|
||||||
// mutation
|
|
||||||
export const useRenameOrg = () => {
|
export const useRenameOrg = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
@@ -73,7 +51,7 @@ export const useGetOrgPlanBillingInfo = (organizationId: string) => {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId),
|
queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<BillingDetails>(
|
const { data } = await apiRequest.get<PlanBillingInfo>(
|
||||||
`/api/v1/organizations/${organizationId}/plan/billing`
|
`/api/v1/organizations/${organizationId}/plan/billing`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -87,7 +65,7 @@ export const useGetOrgPlanTable = (organizationId: string) => {
|
|||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: organizationKeys.getOrgPlanTable(organizationId),
|
queryKey: organizationKeys.getOrgPlanTable(organizationId),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<BillingDetails>(
|
const { data } = await apiRequest.get<OrgPlanTable>(
|
||||||
`/api/v1/organizations/${organizationId}/plan/table`
|
`/api/v1/organizations/${organizationId}/plan/table`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -102,12 +80,12 @@ export const useGetOrgPlansTable = ({
|
|||||||
billingCycle
|
billingCycle
|
||||||
}: {
|
}: {
|
||||||
organizationId: string;
|
organizationId: string;
|
||||||
billingCycle: "monthly" | "annual"
|
billingCycle: "monthly" | "yearly"
|
||||||
}) => {
|
}) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: organizationKeys.getOrgPlansTable(organizationId, billingCycle),
|
queryKey: organizationKeys.getOrgPlansTable(organizationId, billingCycle),
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await apiRequest.get<BillingDetails>(
|
const { data } = await apiRequest.get<ProductsTable>(
|
||||||
`/api/v1/organizations/${organizationId}/plans/table?billingCycle=${billingCycle}`
|
`/api/v1/organizations/${organizationId}/plans/table?billingCycle=${billingCycle}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -120,7 +98,13 @@ export const useGetOrgPlansTable = ({
|
|||||||
export const useGetOrgBillingDetails = (organizationId: string) => {
|
export const useGetOrgBillingDetails = (organizationId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: organizationKeys.getOrgBillingDetails(organizationId),
|
queryKey: organizationKeys.getOrgBillingDetails(organizationId),
|
||||||
queryFn: () => fetchOrgBillingDetails(organizationId),
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<BillingDetails>(
|
||||||
|
`/api/v1/organizations/${organizationId}/billing-details`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -137,10 +121,14 @@ export const useUpdateOrgBillingDetails = () => {
|
|||||||
name?: string;
|
name?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.patch(`/api/v1/organizations/${organizationId}/billing-details`, {
|
const { data } = await apiRequest.patch(
|
||||||
name,
|
`/api/v1/organizations/${organizationId}/billing-details`,
|
||||||
email
|
{
|
||||||
});
|
name,
|
||||||
|
email
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess(_, dto) {
|
onSuccess(_, dto) {
|
||||||
@@ -152,7 +140,13 @@ export const useUpdateOrgBillingDetails = () => {
|
|||||||
export const useGetOrgPmtMethods = (organizationId: string) => {
|
export const useGetOrgPmtMethods = (organizationId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: organizationKeys.getOrgPmtMethods(organizationId),
|
queryKey: organizationKeys.getOrgPmtMethods(organizationId),
|
||||||
queryFn: () => fetchOrgPmtMethods(organizationId),
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<PmtMethod[]>(
|
||||||
|
`/api/v1/organizations/${organizationId}/billing-details/payment-methods`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -170,10 +164,14 @@ export const useAddOrgPmtMethod = () => {
|
|||||||
success_url: string;
|
success_url: string;
|
||||||
cancel_url: string;
|
cancel_url: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { data: { url } } = await apiRequest.post(`/api/v1/organizations/${organizationId}/billing-details/payment-methods`, {
|
const { data: { url } } = await apiRequest.post(
|
||||||
success_url,
|
`/api/v1/organizations/${organizationId}/billing-details/payment-methods`,
|
||||||
cancel_url
|
{
|
||||||
});
|
success_url,
|
||||||
|
cancel_url
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
},
|
},
|
||||||
onSuccess(_, dto) {
|
onSuccess(_, dto) {
|
||||||
@@ -193,7 +191,10 @@ export const useDeleteOrgPmtMethod = () => {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
pmtMethodId: 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;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess(_, dto) {
|
onSuccess(_, dto) {
|
||||||
@@ -205,7 +206,13 @@ export const useDeleteOrgPmtMethod = () => {
|
|||||||
export const useGetOrgTaxIds = (organizationId: string) => {
|
export const useGetOrgTaxIds = (organizationId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: organizationKeys.getOrgTaxIds(organizationId),
|
queryKey: organizationKeys.getOrgTaxIds(organizationId),
|
||||||
queryFn: () => fetchOrgTaxIds(organizationId),
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<TaxID[]>(
|
||||||
|
`/api/v1/organizations/${organizationId}/billing-details/tax-ids`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -223,10 +230,13 @@ export const useAddOrgTaxId = () => {
|
|||||||
type: string;
|
type: string;
|
||||||
value: string;
|
value: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { data } = await apiRequest.post(`/api/v1/organizations/${organizationId}/billing-details/tax-ids`, {
|
const { data } = await apiRequest.post(
|
||||||
type,
|
`/api/v1/organizations/${organizationId}/billing-details/tax-ids`,
|
||||||
value
|
{
|
||||||
});
|
type,
|
||||||
|
value
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
@@ -247,7 +257,10 @@ export const useDeleteOrgTaxId = () => {
|
|||||||
organizationId: string;
|
organizationId: string;
|
||||||
taxId: 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;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess(_, dto) {
|
onSuccess(_, dto) {
|
||||||
@@ -259,54 +272,24 @@ export const useDeleteOrgTaxId = () => {
|
|||||||
export const useGetOrgInvoices = (organizationId: string) => {
|
export const useGetOrgInvoices = (organizationId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: organizationKeys.getOrgInvoices(organizationId),
|
queryKey: organizationKeys.getOrgInvoices(organizationId),
|
||||||
queryFn: () => fetchOrgInvoices(organizationId),
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<Invoice[]>(
|
||||||
|
`/api/v1/organizations/${organizationId}/invoices`
|
||||||
|
);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
},
|
||||||
enabled: true
|
enabled: true
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUpdateOrgPlan = () => {
|
export const useCreateCustomerPortalSession = () => {
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async (organizationId: string) => {
|
||||||
organizationId,
|
const { data } = await apiRequest.post(
|
||||||
productId
|
`/api/v1/organization/${organizationId}/customer-portal-session`
|
||||||
}: {
|
);
|
||||||
organizationId: string;
|
|
||||||
productId: string;
|
|
||||||
}) => {
|
|
||||||
const { data } = await apiRequest.patch(`/api/v1/organizations/${organizationId}/plan`, {
|
|
||||||
productId
|
|
||||||
});
|
|
||||||
return data;
|
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');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -14,3 +14,74 @@ export type BillingDetails = {
|
|||||||
name: string;
|
name: string;
|
||||||
email: 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[];
|
||||||
|
}
|
||||||
@@ -10,14 +10,14 @@ interface UsePopUpProps {
|
|||||||
* checks which type of inputProps were given and converts them into key-names
|
* 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)
|
* SIDENOTE: On inputting give it as const and not string with (as const)
|
||||||
*/
|
*/
|
||||||
type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = {
|
export type UsePopUpState<T extends Readonly<string[]> | UsePopUpProps[]> = {
|
||||||
[P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: {
|
[P in T extends UsePopUpProps[] ? T[number]["name"] : T[number]]: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
data?: unknown;
|
data?: unknown;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
interface UsePopUpReturn<T extends Readonly<string[]> | UsePopUpProps[]> {
|
export interface UsePopUpReturn<T extends Readonly<string[]> | UsePopUpProps[]> {
|
||||||
popUp: UsePopUpState<T>;
|
popUp: UsePopUpState<T>;
|
||||||
handlePopUpOpen: (popUpName: keyof UsePopUpState<T>, data?: unknown) => void;
|
handlePopUpOpen: (popUpName: keyof UsePopUpState<T>, data?: unknown) => void;
|
||||||
handlePopUpClose: (popUpName: keyof UsePopUpState<T>) => void;
|
handlePopUpClose: (popUpName: keyof UsePopUpState<T>) => void;
|
||||||
|
|||||||
@@ -1,57 +1,11 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useSubscription } from "@app/context";
|
|
||||||
import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage";
|
import { BillingSettingsPage } from "@app/views/Settings/BillingSettingsPage";
|
||||||
|
|
||||||
export default function SettingsBilling() {
|
export default function SettingsBilling() {
|
||||||
const { subscription } = useSubscription();
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
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 (
|
return (
|
||||||
<div className="h-full bg-bunker-800">
|
<div className="h-full bg-bunker-800">
|
||||||
<Head>
|
<Head>
|
||||||
@@ -64,44 +18,3 @@ export default function SettingsBilling() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SettingsBilling.requireAuth = true;
|
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>
|
|
||||||
// );
|
|
||||||
|
|||||||
@@ -1,30 +1,9 @@
|
|||||||
import { useTranslation } from "react-i18next";
|
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 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 {
|
import {
|
||||||
BillingCloudTab,
|
BillingTabGroup
|
||||||
BillingReceiptsTab,
|
|
||||||
BillingDetailsTab
|
|
||||||
} from "./components";
|
} from "./components";
|
||||||
|
|
||||||
export const BillingSettingsPage = () => {
|
export const BillingSettingsPage = () => {
|
||||||
@@ -36,85 +15,10 @@ export const BillingSettingsPage = () => {
|
|||||||
<div className="flex text-3xl mt-8 items-start max-w-screen-lg">
|
<div className="flex text-3xl mt-8 items-start max-w-screen-lg">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="font-semibold text-gray-200">{t("billing.title")}</p>
|
<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>
|
||||||
|
<div />
|
||||||
</div>
|
</div>
|
||||||
|
<BillingTabGroup />
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
+1
-23
@@ -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";
|
import { CurrentPlanSection } from "./CurrentPlanSection";
|
||||||
|
import { PreviewSection } from "./PreviewSection";
|
||||||
// TODO: optimize + modularize
|
|
||||||
// TODO: get cloud plan full
|
|
||||||
|
|
||||||
export const BillingCloudTab = () => {
|
export const BillingCloudTab = () => {
|
||||||
return (
|
return (
|
||||||
|
|||||||
+11
-18
@@ -1,7 +1,8 @@
|
|||||||
|
import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Input,
|
EmptyState,
|
||||||
IconButton,
|
|
||||||
Button,
|
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -9,24 +10,20 @@ import {
|
|||||||
Td,
|
Td,
|
||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr} from "@app/components/v2";
|
||||||
EmptyState
|
import { useOrganization } from "@app/context";
|
||||||
} from "@app/components/v2";
|
|
||||||
import {
|
import {
|
||||||
useGetOrgPlanTable
|
useGetOrgPlanTable
|
||||||
} from "@app/hooks/api";
|
} 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 = () => {
|
export const CurrentPlanSection = () => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? '');
|
const { data, isLoading } = useGetOrgPlanTable(currentOrg?._id ?? "");
|
||||||
|
|
||||||
const displayCell = (value: null | number | string | boolean) => {
|
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 (
|
if (value) return (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCircleCheck}
|
icon={faCircleCheck}
|
||||||
@@ -58,14 +55,10 @@ export const CurrentPlanSection = () => {
|
|||||||
</Tr>
|
</Tr>
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{!isLoading && data?.rows?.length > 0 && data.rows.map(({
|
{!isLoading && data && data?.rows?.length > 0 && data.rows.map(({
|
||||||
name,
|
name,
|
||||||
allowed,
|
allowed,
|
||||||
used
|
used
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
allowed: number | boolean;
|
|
||||||
used: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Tr key={`current-plan-row-${name}`} className="h-12">
|
<Tr key={`current-plan-row-${name}`} className="h-12">
|
||||||
@@ -76,7 +69,7 @@ export const CurrentPlanSection = () => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{isLoading && <TableSkeleton columns={5} key="invoices" />}
|
{isLoading && <TableSkeleton columns={5} key="invoices" />}
|
||||||
{!isLoading && data?.length === 0 && (
|
{!isLoading && data && data?.rows?.length === 0 && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Td colSpan={3}>
|
<Td colSpan={3}>
|
||||||
<EmptyState
|
<EmptyState
|
||||||
|
|||||||
+21
-35
@@ -1,45 +1,25 @@
|
|||||||
import { useSubscription } from "@app/context";
|
import { Fragment } from "react"
|
||||||
import { useOrganization } from "@app/context";
|
import { Tab } from "@headlessui/react"
|
||||||
import { Tab } from '@headlessui/react'
|
|
||||||
import { Fragment } from 'react'
|
|
||||||
import {
|
import {
|
||||||
useGetOrgPlanBillingInfo,
|
Modal,
|
||||||
useGetOrgPlanTable,
|
ModalContent
|
||||||
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";
|
} from "@app/components/v2";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { UsePopUpState } 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";
|
import { ManagePlansTable } from "./ManagePlansTable";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
popUp: UsePopUpState<["managePlan"]>;
|
||||||
|
handlePopUpToggle: (popUpName: keyof UsePopUpState<["managePlan"]>, state?: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
export const ManagePlansModal = ({
|
export const ManagePlansModal = ({
|
||||||
popUp,
|
popUp,
|
||||||
handlePopUpToggle
|
handlePopUpToggle
|
||||||
}) => {
|
}: Props) => {
|
||||||
const { subscription, isLoading: isSubscriptionLoading } = useSubscription();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.managePlan?.isOpen}
|
isOpen={popUp?.managePlan?.isOpen}
|
||||||
onOpenChange={(isOpen) => {
|
onOpenChange={(isOpen) => {
|
||||||
handlePopUpToggle("managePlan", isOpen);
|
handlePopUpToggle("managePlan", isOpen);
|
||||||
@@ -50,14 +30,20 @@ export const ManagePlansModal = ({
|
|||||||
<Tab.List className="border-b-2 border-mineshaft-600 max-w-screen-lg">
|
<Tab.List className="border-b-2 border-mineshaft-600 max-w-screen-lg">
|
||||||
<Tab as={Fragment}>
|
<Tab as={Fragment}>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<button className={`p-4 ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"} w-30 font-semibold outline-none`}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`p-4 ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"} w-30 font-semibold outline-none`}
|
||||||
|
>
|
||||||
Bill monthly
|
Bill monthly
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab as={Fragment}>
|
<Tab as={Fragment}>
|
||||||
{({ selected }) => (
|
{({ selected }) => (
|
||||||
<button className={`p-4 ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"} w-30 font-semibold outline-none`}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`p-4 ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"} w-30 font-semibold outline-none`}
|
||||||
|
>
|
||||||
Bill yearly
|
Bill yearly
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+62
-73
@@ -1,21 +1,9 @@
|
|||||||
import { useSubscription } from "@app/context";
|
import { faCircleCheck, faCircleXmark,faFileInvoice } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { useOrganization } from "@app/context";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { Tab } from '@headlessui/react'
|
|
||||||
import { Fragment } from 'react'
|
|
||||||
import {
|
import {
|
||||||
useGetOrgPlanBillingInfo,
|
|
||||||
useGetOrgPlanTable,
|
|
||||||
useGetOrgPlansTable,
|
|
||||||
useUpdateOrgPlan,
|
|
||||||
useCreateProductCheckoutSession
|
|
||||||
} from "@app/hooks/api";
|
|
||||||
import {
|
|
||||||
FormControl,
|
|
||||||
Button,
|
Button,
|
||||||
IconButton,
|
EmptyState,
|
||||||
Input,
|
|
||||||
Select,
|
|
||||||
SelectItem,
|
|
||||||
Table,
|
Table,
|
||||||
TableContainer,
|
TableContainer,
|
||||||
TableSkeleton,
|
TableSkeleton,
|
||||||
@@ -24,37 +12,31 @@ import {
|
|||||||
Th,
|
Th,
|
||||||
THead,
|
THead,
|
||||||
Tr,
|
Tr,
|
||||||
EmptyState,
|
|
||||||
Modal,
|
|
||||||
ModalContent
|
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { useOrganization,useSubscription } from "@app/context";
|
||||||
import { faFileInvoice, faCircleCheck, faCircleXmark } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
|
useCreateCustomerPortalSession,
|
||||||
// TODO: upgrade
|
useGetOrgPlansTable} from "@app/hooks/api";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
billingCycle: 'monthly' | 'yearly'
|
billingCycle: "monthly" | "yearly"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ManagePlansTable = ({
|
export const ManagePlansTable = ({
|
||||||
billingCycle
|
billingCycle
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { subscription, isLoading: isSubscriptionLoading } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({
|
const { data: tableData, isLoading: isTableDataLoading } = useGetOrgPlansTable({
|
||||||
organizationId: currentOrg?._id ?? '',
|
organizationId: currentOrg?._id ?? "",
|
||||||
billingCycle
|
billingCycle
|
||||||
});
|
});
|
||||||
const updateOrgPlan = useUpdateOrgPlan();
|
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
||||||
const createProductCheckoutSession = useCreateProductCheckoutSession();
|
|
||||||
|
|
||||||
console.log('tableData: ', tableData);
|
|
||||||
|
|
||||||
const displayCell = (value: null | number | string | boolean) => {
|
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 (
|
if (value) return (
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
icon={faCircleCheck}
|
icon={faCircleCheck}
|
||||||
@@ -77,20 +59,18 @@ export const ManagePlansTable = ({
|
|||||||
<TableContainer>
|
<TableContainer>
|
||||||
<Table>
|
<Table>
|
||||||
<THead>
|
<THead>
|
||||||
{!isTableDataLoading && tableData?.head.length > 0 && (
|
{subscription && !isTableDataLoading && tableData && (
|
||||||
<Tr>
|
<Tr>
|
||||||
<Th className="">Feature / Limit</Th>
|
<Th className="">Feature / Limit</Th>
|
||||||
{tableData.head.map(({
|
{tableData.head.map(({
|
||||||
name,
|
name,
|
||||||
slug,
|
|
||||||
priceLine
|
priceLine
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
priceLine: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Th className={`${slug === subscription.slug ? "bg-mineshaft-600 text-center" : "text-center"}`}>
|
<Th
|
||||||
|
key={`plans-feature-head-${billingCycle}-${name}`}
|
||||||
|
className="text-center flex-1"
|
||||||
|
>
|
||||||
<p>{name}</p>
|
<p>{name}</p>
|
||||||
<p>{priceLine}</p>
|
<p>{priceLine}</p>
|
||||||
</Th>
|
</Th>
|
||||||
@@ -100,26 +80,28 @@ export const ManagePlansTable = ({
|
|||||||
)}
|
)}
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
<TBody>
|
||||||
{!isTableDataLoading && tableData?.rows.length > 0 && tableData.rows.map(({
|
{subscription && !isTableDataLoading && tableData && tableData.rows.map(({
|
||||||
name,
|
name,
|
||||||
starter,
|
starter,
|
||||||
team,
|
team,
|
||||||
pro,
|
pro,
|
||||||
enterprise
|
enterprise
|
||||||
}: {
|
|
||||||
name: string;
|
|
||||||
starter: null | number | string | boolean;
|
|
||||||
team: null | number | string | boolean;
|
|
||||||
pro: null | number | string | boolean;
|
|
||||||
enterprise: null | number | string | boolean;
|
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Tr className="h-12">
|
<Tr className="h-12" key={`plans-feature-row-${billingCycle}-${name}`}>
|
||||||
<Td>{displayCell(name)}</Td>
|
<Td>{displayCell(name)}</Td>
|
||||||
<Td className={'starter' === subscription.slug ? "bg-mineshaft-600 text-center" : "text-center"}>{displayCell(starter)}</Td>
|
<Td className="text-center">
|
||||||
<Td className={'team' === subscription.slug ? "bg-mineshaft-600 text-center" : "text-center"}>{displayCell(team)}</Td>
|
{displayCell(starter)}
|
||||||
<Td className={'pro' === subscription.slug ? "bg-mineshaft-600 text-center" : "text-center"}>{displayCell(pro)}</Td>
|
</Td>
|
||||||
<Td className={'enterprise' === subscription.slug ? "bg-mineshaft-600 text-center" : "text-center"}>{displayCell(enterprise)}</Td>
|
<Td className="text-center">
|
||||||
|
{displayCell(team)}
|
||||||
|
</Td>
|
||||||
|
<Td className="text-center">
|
||||||
|
{displayCell(pro)}
|
||||||
|
</Td>
|
||||||
|
<Td className="text-center">
|
||||||
|
{displayCell(enterprise)}
|
||||||
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -134,46 +116,53 @@ export const ManagePlansTable = ({
|
|||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
)}
|
)}
|
||||||
{subscription && !isTableDataLoading && tableData?.head.length > 0 && (
|
{subscription && !isTableDataLoading && tableData && (
|
||||||
<Tr className="h-12">
|
<Tr className="h-12">
|
||||||
<Td></Td>
|
<Td />
|
||||||
{tableData.head.map(({
|
{tableData.head.map(({
|
||||||
slug,
|
slug,
|
||||||
productId
|
tier
|
||||||
}: {
|
|
||||||
slug: string;
|
|
||||||
productId: string;
|
|
||||||
}) => {
|
}) => {
|
||||||
const isCurrentPlan = slug === subscription.slug;
|
|
||||||
|
|
||||||
console.log('productId: ', productId);
|
const isCurrentPlan = slug === subscription.slug;
|
||||||
|
let subscriptionText = "Upgrade";
|
||||||
|
|
||||||
|
if (subscription.tier > tier) {
|
||||||
|
subscriptionText = "Downgrade"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tier === 3) {
|
||||||
|
subscriptionText = "Contact sales"
|
||||||
|
}
|
||||||
|
|
||||||
return isCurrentPlan ? (
|
return isCurrentPlan ? (
|
||||||
<Td className="bg-mineshaft-600">
|
<Td>
|
||||||
<p className="text-center font-semibold">Current</p>
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
className="w-full"
|
||||||
|
isDisabled
|
||||||
|
>
|
||||||
|
Current
|
||||||
|
</Button>
|
||||||
</Td>
|
</Td>
|
||||||
) : (
|
) : (
|
||||||
<Td>
|
<Td>
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
console.log('upgrade to product: ', productId);
|
|
||||||
if (!currentOrg?._id) return;
|
if (!currentOrg?._id) return;
|
||||||
// const test = await updateOrgPlan.mutateAsync({
|
|
||||||
// organizationId: currentOrg._id,
|
|
||||||
// productId
|
|
||||||
// });
|
|
||||||
|
|
||||||
const { url } = await createProductCheckoutSession.mutateAsync({
|
if (tier !== 3) {
|
||||||
organizationId: currentOrg._id,
|
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
|
||||||
productId,
|
window.location.href = url;
|
||||||
success_url: window.location.href
|
return;
|
||||||
})
|
}
|
||||||
|
|
||||||
window.location.href = url;
|
window.location.href = "https://infisical.com/scheduledemo";
|
||||||
}}
|
}}
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
Upgrade
|
{subscriptionText}
|
||||||
</Button>
|
</Button>
|
||||||
</Td>
|
</Td>
|
||||||
);
|
);
|
||||||
|
|||||||
+29
-12
@@ -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 { 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 { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { ManagePlansModal } from "./ManagePlansModal";
|
import { ManagePlansModal } from "./ManagePlansModal";
|
||||||
|
|
||||||
export const PreviewSection = () => {
|
export const PreviewSection = () => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { subscription, isLoading: isSubscriptionLoading } = useSubscription();
|
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"
|
"managePlan"
|
||||||
] as const);
|
] as const);
|
||||||
|
|
||||||
const formatAmount = (amount: number) => {
|
const formatAmount = (amount: number) => {
|
||||||
const formattedTotal = (Math.floor(amount) / 100).toLocaleString('en-US', {
|
const formattedTotal = (Math.floor(amount) / 100).toLocaleString("en-US", {
|
||||||
style: 'currency',
|
style: "currency",
|
||||||
currency: 'USD',
|
currency: "USD",
|
||||||
});
|
});
|
||||||
|
|
||||||
return formattedTotal;
|
return formattedTotal;
|
||||||
@@ -35,7 +38,7 @@ export const PreviewSection = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!isSubscriptionLoading && subscription?.slug !== 'enterprise' && subscription?.slug !== 'pro' && subscription?.slug !== 'pro-annual' && (
|
{!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="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">
|
<div className="flex-1">
|
||||||
<h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2>
|
<h2 className="text-xl font-semibold text-mineshaft-50">Become Infisical</h2>
|
||||||
@@ -54,15 +57,29 @@ export const PreviewSection = () => {
|
|||||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 mr-4 border border-mineshaft-600">
|
<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="mb-2 text-gray-400">Current plan</p>
|
||||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">Starter</p>
|
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">Starter</p>
|
||||||
<p className="text-primary">Manage plan →</p>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
if (!currentOrg?._id) return;
|
||||||
|
const { url } = await createCustomerPortalSession.mutateAsync(currentOrg._id);
|
||||||
|
window.location.href = url;
|
||||||
|
}}
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
|
Manage plan →
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600 mr-4">
|
<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="mb-2 text-gray-400">Price</p>
|
||||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">{`${formatAmount(data.amount)} / ${data.interval}`}</p>
|
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
|
||||||
|
{`${formatAmount(data.amount)} / ${data.interval}`}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4 bg-mineshaft-900 rounded-lg flex-1 border border-mineshaft-600">
|
<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="mb-2 text-gray-400">Subscription renews on</p>
|
||||||
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">{formatDate(data.currentPeriodEnd)}</p>
|
<p className="text-2xl mb-8 text-mineshaft-50 font-semibold">
|
||||||
|
{formatDate(data.currentPeriodEnd)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+2
-2
@@ -5,11 +5,11 @@ import { TaxIDSection } from "./TaxIDSection";
|
|||||||
|
|
||||||
export const BillingDetailsTab = () => {
|
export const BillingDetailsTab = () => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<CompanyNameSection />
|
<CompanyNameSection />
|
||||||
<InvoiceEmailSection />
|
<InvoiceEmailSection />
|
||||||
<PmtMethodsSection />
|
<PmtMethodsSection />
|
||||||
<TaxIDSection />
|
<TaxIDSection />
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+35
-37
@@ -1,39 +1,37 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import {
|
import { Controller, useForm } from "react-hook-form";
|
||||||
Input,
|
import { yupResolver } from "@hookform/resolvers/yup";
|
||||||
// Button,
|
import * as yup from "yup";
|
||||||
FormControl
|
|
||||||
} from "@app/components/v2";
|
import Button from "@app/components/basic/buttons/Button";
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
|
||||||
import { yupResolver } from '@hookform/resolvers/yup';
|
import { FormControl,Input } from "@app/components/v2";
|
||||||
import * as yup from 'yup';
|
|
||||||
import { useOrganization } from "@app/context";
|
import { useOrganization } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useGetOrgBillingDetails,
|
useGetOrgBillingDetails,
|
||||||
useUpdateOrgBillingDetails
|
useUpdateOrgBillingDetails
|
||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
import Button from "@app/components/basic/buttons/Button";
|
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required('Company name is required')
|
name: yup.string().required("Company name is required")
|
||||||
}).required();
|
}).required();
|
||||||
|
|
||||||
export const CompanyNameSection = () => {
|
export const CompanyNameSection = () => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { reset, control, register, handleSubmit, watch, formState: { errors } } = useForm({
|
const { reset, control, handleSubmit } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: ''
|
name: ""
|
||||||
},
|
},
|
||||||
resolver: yupResolver(schema)
|
resolver: yupResolver(schema)
|
||||||
});
|
});
|
||||||
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? '');
|
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
|
||||||
const updateOrgBillingDetails = useUpdateOrgBillingDetails();
|
const updateOrgBillingDetails = useUpdateOrgBillingDetails();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
reset({
|
reset({
|
||||||
name: data?.name ?? ''
|
name: data?.name ?? ""
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@@ -41,13 +39,22 @@ export const CompanyNameSection = () => {
|
|||||||
const onFormSubmit = async ({ name }: { name: string }) => {
|
const onFormSubmit = async ({ name }: { name: string }) => {
|
||||||
try {
|
try {
|
||||||
if (!currentOrg?._id) return;
|
if (!currentOrg?._id) return;
|
||||||
if (name === '') return;
|
if (name === "") return;
|
||||||
await updateOrgBillingDetails.mutateAsync({
|
await updateOrgBillingDetails.mutateAsync({
|
||||||
name,
|
name,
|
||||||
organizationId: currentOrg._id
|
organizationId: currentOrg._id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated business name",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update business name",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,17 +63,9 @@ export const CompanyNameSection = () => {
|
|||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600"
|
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-mineshaft-100 mb-8">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
Business name
|
||||||
Business name
|
</h2>
|
||||||
</h2>
|
|
||||||
<Button
|
|
||||||
color="mineshaft"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="max-w-md">
|
<div className="max-w-md">
|
||||||
<Controller
|
<Controller
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
@@ -83,16 +82,15 @@ export const CompanyNameSection = () => {
|
|||||||
name="name"
|
name="name"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<div className="inline-block">
|
||||||
text="Test"
|
<Button
|
||||||
onButtonPressed={() => {
|
text="Save"
|
||||||
console.log('Button pressed');
|
type="submit"
|
||||||
// setIsAddApiKeyDialogOpen(true);
|
color="mineshaft"
|
||||||
}}
|
size="md"
|
||||||
color="mineshaft"
|
onButtonPressed={() => console.log("Saved company name")}
|
||||||
// icon={faPlus}
|
/>
|
||||||
size="md"
|
</div>
|
||||||
/>
|
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+37
-24
@@ -1,12 +1,13 @@
|
|||||||
import { useEffect } from "react";
|
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 {
|
import {
|
||||||
Input,
|
FormControl,
|
||||||
Button,
|
Input} from "@app/components/v2";
|
||||||
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 { useOrganization } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useGetOrgBillingDetails,
|
useGetOrgBillingDetails,
|
||||||
@@ -14,24 +15,25 @@ import {
|
|||||||
} from "@app/hooks/api";
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
email: yup.string().required('Email is required')
|
email: yup.string().required("Email is required")
|
||||||
}).required();
|
}).required();
|
||||||
|
|
||||||
export const InvoiceEmailSection = () => {
|
export const InvoiceEmailSection = () => {
|
||||||
|
const { createNotification } = useNotificationContext();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { reset, control, register, handleSubmit, watch, formState: { errors } } = useForm({
|
const { reset, control, handleSubmit } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: ''
|
email: ""
|
||||||
},
|
},
|
||||||
resolver: yupResolver(schema)
|
resolver: yupResolver(schema)
|
||||||
});
|
});
|
||||||
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? '');
|
const { data } = useGetOrgBillingDetails(currentOrg?._id ?? "");
|
||||||
const updateOrgBillingDetails = useUpdateOrgBillingDetails();
|
const updateOrgBillingDetails = useUpdateOrgBillingDetails();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data) {
|
if (data) {
|
||||||
reset({
|
reset({
|
||||||
email: data?.email ?? ''
|
email: data?.email ?? ""
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
@@ -39,13 +41,23 @@ export const InvoiceEmailSection = () => {
|
|||||||
const onFormSubmit = async ({ email }: { email: string }) => {
|
const onFormSubmit = async ({ email }: { email: string }) => {
|
||||||
try {
|
try {
|
||||||
if (!currentOrg?._id) return;
|
if (!currentOrg?._id) return;
|
||||||
if (email === '') return;
|
if (email === "") return;
|
||||||
|
|
||||||
await updateOrgBillingDetails.mutateAsync({
|
await updateOrgBillingDetails.mutateAsync({
|
||||||
email,
|
email,
|
||||||
organizationId: currentOrg._id
|
organizationId: currentOrg._id
|
||||||
});
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
text: "Successfully updated invoice email recipient",
|
||||||
|
type: "success"
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
createNotification({
|
||||||
|
text: "Failed to update invoice email recipient",
|
||||||
|
type: "error"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,17 +66,9 @@ export const InvoiceEmailSection = () => {
|
|||||||
onSubmit={handleSubmit(onFormSubmit)}
|
onSubmit={handleSubmit(onFormSubmit)}
|
||||||
className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600"
|
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 mb-8">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
Invoice email recipient
|
||||||
Invoice email recipient
|
</h2>
|
||||||
</h2>
|
|
||||||
<Button
|
|
||||||
color="mineshaft"
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="max-w-md">
|
<div className="max-w-md">
|
||||||
<Controller
|
<Controller
|
||||||
defaultValue=""
|
defaultValue=""
|
||||||
@@ -81,6 +85,15 @@ export const InvoiceEmailSection = () => {
|
|||||||
name="email"
|
name="email"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="inline-block">
|
||||||
|
<Button
|
||||||
|
text="Save"
|
||||||
|
type="submit"
|
||||||
|
color="mineshaft"
|
||||||
|
size="md"
|
||||||
|
onButtonPressed={() => console.log("Saved email address")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+17
-103
@@ -1,32 +1,14 @@
|
|||||||
import {
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
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
|
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 = () => {
|
export const PmtMethodsSection = () => {
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const { data, isLoading } = useGetOrgPmtMethods(currentOrg?._id ?? '');
|
|
||||||
const addOrgPmtMethod = useAddOrgPmtMethod();
|
const addOrgPmtMethod = useAddOrgPmtMethod();
|
||||||
const deleteOrgPmtMethod = useDeleteOrgPmtMethod();
|
|
||||||
|
|
||||||
const handleAddPmtMethodBtnClick = async () => {
|
const handleAddPmtMethodBtnClick = async () => {
|
||||||
if (!currentOrg?._id) return;
|
if (!currentOrg?._id) return;
|
||||||
@@ -39,92 +21,24 @@ export const PmtMethodsSection = () => {
|
|||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDeletePmtMethodBtnClick = async (pmtMethodId: string) => {
|
|
||||||
if (!currentOrg?._id) return;
|
|
||||||
await deleteOrgPmtMethod.mutateAsync({
|
|
||||||
organizationId: currentOrg._id,
|
|
||||||
pmtMethodId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
<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">
|
<div className="flex items-center mb-8">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||||
Payment Methods
|
Payment Methods
|
||||||
</h2>
|
</h2>
|
||||||
<Button
|
<div className="inline-block">
|
||||||
color="mineshaft"
|
<Button
|
||||||
type="submit"
|
text="Add method"
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
type="submit"
|
||||||
onClick={handleAddPmtMethodBtnClick}
|
color="mineshaft"
|
||||||
>
|
size="md"
|
||||||
Add method
|
icon={faPlus}
|
||||||
</Button>
|
onButtonPressed={handleAddPmtMethodBtnClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TableContainer className="mt-4">
|
<PmtMethodsTable />
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+90
@@ -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 (
|
||||||
|
<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" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{!isLoading && data && data?.length > 0 && data.map(({
|
||||||
|
_id,
|
||||||
|
brand,
|
||||||
|
exp_month,
|
||||||
|
exp_year,
|
||||||
|
funding,
|
||||||
|
last4
|
||||||
|
}) => (
|
||||||
|
<Tr key={`pmt-method-${_id}`} className="h-10">
|
||||||
|
<Td>{brand.charAt(0).toUpperCase() + brand.slice(1)}</Td>
|
||||||
|
<Td>{funding.charAt(0).toUpperCase() + funding.slice(1)}</Td>
|
||||||
|
<Td>{last4}</Td>
|
||||||
|
<Td>{`${exp_month}/${exp_year}`}</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => {
|
||||||
|
await handleDeletePmtMethodBtnClick(_id);
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
{isLoading && <TableSkeleton columns={5} key="pmt-methods" />}
|
||||||
|
{!isLoading && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<EmptyState
|
||||||
|
title="No payment methods on file"
|
||||||
|
icon={faCreditCard}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
+195
@@ -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<typeof schema>;
|
||||||
|
|
||||||
|
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<AddTaxIDFormData>({
|
||||||
|
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 (
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
+20
-257
@@ -1,276 +1,39 @@
|
|||||||
import {
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
FormControl,
|
|
||||||
Button,
|
import Button from "@app/components/basic/buttons/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 { 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 = [
|
import { TaxIDModal } from "./TaxIDModal";
|
||||||
{ label: 'Australia ABN', value: 'au_abn' },
|
import { TaxIDTable } from "./TaxIDTable";
|
||||||
{ 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 = () => {
|
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([
|
const { popUp, handlePopUpOpen, handlePopUpClose, handlePopUpToggle } = usePopUp([
|
||||||
"addTaxID"
|
"addTaxID"
|
||||||
] as const);
|
] 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 (
|
return (
|
||||||
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
<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">
|
<div className="flex items-center mb-8">
|
||||||
<h2 className="text-xl font-semibold flex-1 text-white">
|
<h2 className="text-xl font-semibold flex-1 text-white">
|
||||||
Tax ID
|
Tax ID
|
||||||
</h2>
|
</h2>
|
||||||
<Button
|
<div className="inline-block">
|
||||||
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
|
<Button
|
||||||
className="mr-4"
|
text="Add Tax ID"
|
||||||
size="sm"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={isSubmitting}
|
color="mineshaft"
|
||||||
isDisabled={isSubmitting}
|
size="md"
|
||||||
>
|
icon={faPlus}
|
||||||
Add
|
onButtonPressed={() => handlePopUpOpen("addTaxID")}
|
||||||
</Button>
|
/>
|
||||||
<Button colorSchema="secondary" variant="plain">
|
</div>
|
||||||
Cancel
|
</div>
|
||||||
</Button>
|
<TaxIDTable />
|
||||||
</div>
|
<TaxIDModal
|
||||||
</form>
|
popUp={popUp}
|
||||||
</ModalContent>
|
handlePopUpClose={handlePopUpClose}
|
||||||
</Modal>
|
handlePopUpToggle={handlePopUpToggle}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+137
@@ -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 (
|
||||||
|
<TableContainer className="mt-4">
|
||||||
|
<Table>
|
||||||
|
<THead>
|
||||||
|
<Tr>
|
||||||
|
<Th className="flex-1">Type</Th>
|
||||||
|
<Th className="flex-1">Value</Th>
|
||||||
|
<Th className="w-5" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{!isLoading && data && data?.length > 0 && data.map(({
|
||||||
|
_id,
|
||||||
|
type,
|
||||||
|
value
|
||||||
|
}) => (
|
||||||
|
<Tr key={`tax-id-${_id}`} className="h-10">
|
||||||
|
<Td>{taxIDTypeLabelMap[type]}</Td>
|
||||||
|
<Td>{value}</Td>
|
||||||
|
<Td>
|
||||||
|
<IconButton
|
||||||
|
onClick={async () => {
|
||||||
|
await handleDeleteTaxIdBtnClick(_id);
|
||||||
|
}}
|
||||||
|
size="lg"
|
||||||
|
colorSchema="danger"
|
||||||
|
variant="plain"
|
||||||
|
ariaLabel="update"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
{isLoading && <TableSkeleton columns={3} key="tax-ids" />}
|
||||||
|
{!isLoading && data && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<EmptyState
|
||||||
|
title="No Tax IDs on file"
|
||||||
|
icon={faFileInvoice}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
+2
-92
@@ -1,100 +1,10 @@
|
|||||||
import {
|
import { InvoicesTable } from "./InvoicesTable";
|
||||||
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 = () => {
|
export const BillingReceiptsTab = () => {
|
||||||
const { currentOrg } = useOrganization();
|
|
||||||
const { data, isLoading } = useGetOrgInvoices(currentOrg?._id ?? '');
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-mineshaft-900 mt-8 max-w-screen-lg rounded-lg border border-mineshaft-600">
|
<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>
|
<h2 className="text-xl font-semibold flex-1 text-white">Invoices</h2>
|
||||||
<TableContainer className="mt-4">
|
<InvoicesTable />
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+88
@@ -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 (
|
||||||
|
<TableContainer className="mt-8">
|
||||||
|
<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" />
|
||||||
|
</Tr>
|
||||||
|
</THead>
|
||||||
|
<TBody>
|
||||||
|
{!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 (
|
||||||
|
<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 && data?.length === 0 && (
|
||||||
|
<Tr>
|
||||||
|
<Td colSpan={5}>
|
||||||
|
<EmptyState
|
||||||
|
title="No invoices on file"
|
||||||
|
icon={faFileInvoice}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
)}
|
||||||
|
</TBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
||||||
+44
@@ -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 (
|
||||||
|
<Tab.Group>
|
||||||
|
<Tab.List className="mt-8 border-b-2 border-mineshaft-800 max-w-screen-lg">
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<Tab as={Fragment} key={tab.key}>
|
||||||
|
{({ selected }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`w-30 p-4 font-semibold outline-none ${selected ? "border-b-2 border-white text-white" : "text-mineshaft-400"}`}
|
||||||
|
>
|
||||||
|
{tab.name}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</Tab>
|
||||||
|
))}
|
||||||
|
</Tab.List>
|
||||||
|
<Tab.Panels>
|
||||||
|
<Tab.Panel>
|
||||||
|
<BillingCloudTab />
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>
|
||||||
|
<BillingReceiptsTab />
|
||||||
|
</Tab.Panel>
|
||||||
|
<Tab.Panel>
|
||||||
|
<BillingDetailsTab />
|
||||||
|
</Tab.Panel>
|
||||||
|
</Tab.Panels>
|
||||||
|
</Tab.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { BillingTabGroup } from "./BillingTabGroup";
|
||||||
@@ -1,3 +1 @@
|
|||||||
export { BillingCloudTab } from "./BillingCloudTab";
|
export { BillingTabGroup } from "./BillingTabGroup";
|
||||||
export { BillingReceiptsTab } from "./BillingReceiptsTab";
|
|
||||||
export { BillingDetailsTab } from "./BillingDetailsTab";
|
|
||||||
Reference in New Issue
Block a user