mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 21:26:04 +00:00
Update free plan logic
This commit is contained in:
@@ -27,6 +27,30 @@ export const getOrganizationPlan = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return checkout url for pro trial
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const startOrganizationTrial = async (req: Request, res: Response) => {
|
||||||
|
const { organizationId } = req.params;
|
||||||
|
const { success_url } = req.body;
|
||||||
|
|
||||||
|
const { data: { url } } = await licenseServerKeyRequest.post(
|
||||||
|
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/session/trial`,
|
||||||
|
{
|
||||||
|
success_url
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
EELicenseService.delPlan(organizationId);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
url
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the organization's current plan's billing info
|
* Return the organization's current plan's billing info
|
||||||
* @param req
|
* @param req
|
||||||
|
|||||||
@@ -41,6 +41,21 @@ router.get(
|
|||||||
organizationsController.getOrganizationPlan
|
organizationsController.getOrganizationPlan
|
||||||
);
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
"/:organizationId/session/trial",
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ["jwt"],
|
||||||
|
}),
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED],
|
||||||
|
}),
|
||||||
|
param("organizationId").exists().trim(),
|
||||||
|
body("success_url").exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
organizationsController.startOrganizationTrial
|
||||||
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
"/:organizationId/plan/billing",
|
"/:organizationId/plan/billing",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ interface FeatureSet {
|
|||||||
auditLogs: boolean;
|
auditLogs: boolean;
|
||||||
status: 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | null;
|
status: 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | null;
|
||||||
trial_end: number | null;
|
trial_end: number | null;
|
||||||
|
has_used_trial: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,7 +64,8 @@ class EELicenseService {
|
|||||||
customAlerts: true,
|
customAlerts: true,
|
||||||
auditLogs: false,
|
auditLogs: false,
|
||||||
status: null,
|
status: null,
|
||||||
trial_end: null
|
trial_end: null,
|
||||||
|
has_used_trial: true
|
||||||
}
|
}
|
||||||
|
|
||||||
public localFeatureSet: NodeCache;
|
public localFeatureSet: NodeCache;
|
||||||
@@ -71,7 +73,7 @@ class EELicenseService {
|
|||||||
constructor() {
|
constructor() {
|
||||||
this._isLicenseValid = true;
|
this._isLicenseValid = true;
|
||||||
this.localFeatureSet = new NodeCache({
|
this.localFeatureSet = new NodeCache({
|
||||||
stdTTL: 300,
|
stdTTL: 60,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,6 +114,12 @@ class EELicenseService {
|
|||||||
await this.getPlan(organizationId, workspaceId);
|
await this.getPlan(organizationId, workspaceId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async delPlan(organizationId: string) {
|
||||||
|
if (this.instanceType === "cloud") {
|
||||||
|
this.localFeatureSet.del(`${organizationId}-`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async initGlobalFeatureSet() {
|
public async initGlobalFeatureSet() {
|
||||||
const licenseServerKey = await getLicenseServerKey();
|
const licenseServerKey = await getLicenseServerKey();
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { useSubscription } from "@app/context";
|
import { useSubscription } from "@app/context";
|
||||||
|
import { Button } from "@app/components/v2";
|
||||||
import { Button } from "../Button";
|
|
||||||
import { Modal, ModalClose, ModalContent } from "../Modal";
|
import { Modal, ModalClose, ModalContent } from "../Modal";
|
||||||
|
import { useOrganization } from "@app/context";
|
||||||
|
import {
|
||||||
|
useGetOrgTrialUrl
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
@@ -13,32 +15,61 @@ type Props = {
|
|||||||
|
|
||||||
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => {
|
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => {
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
const { currentOrg } = useOrganization();
|
||||||
|
const { mutateAsync, isLoading } = useGetOrgTrialUrl();
|
||||||
const link = (subscription && subscription.slug !== null)
|
const link = (subscription && subscription.slug !== null)
|
||||||
? `/settings/billing/${localStorage.getItem("projectData.id") as string}`
|
? `/settings/billing/${localStorage.getItem("projectData.id") as string}`
|
||||||
: "https://infisical.com/scheduledemo";
|
: "https://infisical.com/scheduledemo";
|
||||||
|
|
||||||
|
const handleUpgradeBtnClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!subscription || !currentOrg) return;
|
||||||
|
|
||||||
|
if (!subscription.has_used_trial) {
|
||||||
|
// direct user to start pro trial
|
||||||
|
|
||||||
|
const url = await mutateAsync({
|
||||||
|
orgId: currentOrg._id,
|
||||||
|
success_url: window.location.href
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
} else {
|
||||||
|
// direct user to upgrade their plan
|
||||||
|
window.location.href = link;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
title="Unleash Infisical's Full Power"
|
title="Unleash Infisical's Full Power"
|
||||||
footerContent={[
|
|
||||||
<Link
|
|
||||||
href={link}
|
|
||||||
key="upgrade-plan"
|
|
||||||
>
|
|
||||||
<Button className="mr-4 ml-2 mb-2">Upgrade Plan</Button>
|
|
||||||
</Link>,
|
|
||||||
<ModalClose asChild key="upgrade-plan-cancel">
|
|
||||||
<Button colorSchema="secondary" variant="plain">
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
</ModalClose>
|
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<p className="mb-2 text-bunker-300">{text}</p>
|
<p className="mb-2 text-bunker-300">{text}</p>
|
||||||
<p className="text-bunker-300">
|
<p className="text-bunker-300">
|
||||||
Upgrade and get access to this, as well as to other powerful enhancements.
|
Upgrade and get access to this, as well as to other powerful enhancements.
|
||||||
</p>
|
</p>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button
|
||||||
|
isLoading={isLoading}
|
||||||
|
colorSchema="primary"
|
||||||
|
onClick={handleUpgradeBtnClick}
|
||||||
|
className="mr-4"
|
||||||
|
>
|
||||||
|
{(subscription && !subscription.has_used_trial) ? "Start Pro Free Trial" : "Upgrade Plan"}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
colorSchema="secondary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => onOpenChange && onOpenChange(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ export {
|
|||||||
useGetOrgPmtMethods,
|
useGetOrgPmtMethods,
|
||||||
useGetOrgTaxIds,
|
useGetOrgTaxIds,
|
||||||
useRenameOrg,
|
useRenameOrg,
|
||||||
useUpdateOrgBillingDetails} from "./queries";
|
useGetOrgTrialUrl,
|
||||||
|
useUpdateOrgBillingDetails
|
||||||
|
} from "./queries";
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import {
|
|||||||
PlanBillingInfo,
|
PlanBillingInfo,
|
||||||
PmtMethod,
|
PmtMethod,
|
||||||
ProductsTable,
|
ProductsTable,
|
||||||
RenameOrgDTO,
|
RenameOrgDTO,
|
||||||
TaxID} from "./types";
|
GetOrgTrialUrlDTO,
|
||||||
|
TaxID
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
const organizationKeys = {
|
const organizationKeys = {
|
||||||
getUserOrganization: ["organization"] as const,
|
getUserOrganization: ["organization"] as const,
|
||||||
@@ -47,6 +49,24 @@ export const useRenameOrg = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetOrgTrialUrl = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
orgId,
|
||||||
|
success_url
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
success_url: string;
|
||||||
|
}) => {
|
||||||
|
const { data: { url } } = await apiRequest.post(`/api/v1/organizations/${orgId}/session/trial`, {
|
||||||
|
success_url
|
||||||
|
})
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const useGetOrgPlanBillingInfo = (organizationId: string) => {
|
export const useGetOrgPlanBillingInfo = (organizationId: string) => {
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId),
|
queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId),
|
||||||
|
|||||||
@@ -10,6 +10,11 @@ export type RenameOrgDTO = {
|
|||||||
newOrgName: string;
|
newOrgName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GetOrgTrialUrlDTO = {
|
||||||
|
orgId: string;
|
||||||
|
success_url: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type BillingDetails = {
|
export type BillingDetails = {
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
|
|||||||
@@ -15,4 +15,5 @@ export type SubscriptionPlan = {
|
|||||||
environmentLimit: number;
|
environmentLimit: number;
|
||||||
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
|
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
|
||||||
trial_end: number | null;
|
trial_end: number | null;
|
||||||
|
has_used_trial: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -22,7 +22,10 @@ import {TFunction} from "i18next";
|
|||||||
|
|
||||||
import guidGenerator from "@app/components/utilities/randomId";
|
import guidGenerator from "@app/components/utilities/randomId";
|
||||||
import { useOrganization, useSubscription,useUser } from "@app/context";
|
import { useOrganization, useSubscription,useUser } from "@app/context";
|
||||||
import { useLogoutUser } from "@app/hooks/api";
|
import {
|
||||||
|
useLogoutUser,
|
||||||
|
useGetOrgTrialUrl
|
||||||
|
} from "@app/hooks/api";
|
||||||
|
|
||||||
const supportOptions = (t: TFunction) => [
|
const supportOptions = (t: TFunction) => [
|
||||||
[
|
[
|
||||||
@@ -67,6 +70,7 @@ export const Navbar = () => {
|
|||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
|
|
||||||
const { currentOrg, orgs } = useOrganization();
|
const { currentOrg, orgs } = useOrganization();
|
||||||
|
const { mutateAsync, isLoading } = useGetOrgTrialUrl();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const logout = useLogoutUser();
|
const logout = useLogoutUser();
|
||||||
@@ -346,12 +350,25 @@ export const Navbar = () => {
|
|||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{subscription && subscription.status === "trialing" && subscription.trial_end && (
|
{subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
|
||||||
<div className="w-full mx-auto border-t border-mineshaft-500">
|
<div className="w-full mx-auto border-t border-mineshaft-500 text-center">
|
||||||
<p className="text-center py-4 text-sm">
|
<button
|
||||||
{`Currently trialing the ${formatPlanSlug(subscription.slug)} plan until ${formatDate(subscription.trial_end).formattedDate} - ${formatDate(subscription.trial_end).remainingDays} day(s) left. `}
|
type="button"
|
||||||
<Link href={`/settings/billing/${localStorage.getItem("projectData.id")}`}>Add a card to avoid being downgraded to the Starter plan afterward →</Link>
|
onClick={async () => {
|
||||||
</p>
|
if (!subscription || !currentOrg) return;
|
||||||
|
|
||||||
|
// direct user to start pro trial
|
||||||
|
const url = await mutateAsync({
|
||||||
|
orgId: currentOrg._id,
|
||||||
|
success_url: window.location.href
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
}}
|
||||||
|
className="text-center py-4 text-sm mx-auto"
|
||||||
|
>
|
||||||
|
You are currently on the <span className="font-semibold">Starter</span> plan. Unlock the full power of Infisical on the <span className="font-semibold">Pro Free Trial →</span>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+29
-4
@@ -2,7 +2,9 @@ import { Button } from "@app/components/v2";
|
|||||||
import { useOrganization,useSubscription } from "@app/context";
|
import { useOrganization,useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
useCreateCustomerPortalSession,
|
useCreateCustomerPortalSession,
|
||||||
useGetOrgPlanBillingInfo} from "@app/hooks/api";
|
useGetOrgPlanBillingInfo,
|
||||||
|
useGetOrgTrialUrl
|
||||||
|
} from "@app/hooks/api";
|
||||||
import { usePopUp } from "@app/hooks/usePopUp";
|
import { usePopUp } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { ManagePlansModal } from "./ManagePlansModal";
|
import { ManagePlansModal } from "./ManagePlansModal";
|
||||||
@@ -11,6 +13,7 @@ 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 getOrgTrialUrl = useGetOrgTrialUrl();
|
||||||
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
const createCustomerPortalSession = useCreateCustomerPortalSession();
|
||||||
|
|
||||||
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
|
||||||
@@ -42,19 +45,41 @@ export const PreviewSection = () => {
|
|||||||
.replace(/-/g, " ");
|
.replace(/-/g, " ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleUpgradeBtnClick = async () => {
|
||||||
|
try {
|
||||||
|
if (!subscription || !currentOrg) return;
|
||||||
|
|
||||||
|
if (!subscription.has_used_trial) {
|
||||||
|
// direct user to start pro trial
|
||||||
|
const url = await getOrgTrialUrl.mutateAsync({
|
||||||
|
orgId: currentOrg._id,
|
||||||
|
success_url: window.location.href
|
||||||
|
});
|
||||||
|
|
||||||
|
window.location.href = url;
|
||||||
|
} else {
|
||||||
|
// open compare plans modal
|
||||||
|
handlePopUpOpen("managePlan");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!isSubscriptionLoading && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && (
|
{subscription && 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 mb-6 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 mb-6 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>
|
||||||
<p className="text-gray-400 mt-4">Unlimited members, projects, RBAC, smart alerts, and so much more</p>
|
<p className="text-gray-400 mt-4">Unlimited members, projects, RBAC, smart alerts, and so much more</p>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => handlePopUpOpen("managePlan")}
|
// onClick={() => handlePopUpOpen("managePlan")}
|
||||||
|
onClick={() => handleUpgradeBtnClick()}
|
||||||
color="mineshaft"
|
color="mineshaft"
|
||||||
>
|
>
|
||||||
Upgrade
|
{!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user