diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts
index fb13a6575..652fc4b32 100644
--- a/backend/src/ee/controllers/v1/organizationsController.ts
+++ b/backend/src/ee/controllers/v1/organizationsController.ts
@@ -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
* @param req
diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts
index 37be3a96d..c9104d964 100644
--- a/backend/src/ee/routes/v1/organizations.ts
+++ b/backend/src/ee/routes/v1/organizations.ts
@@ -41,6 +41,21 @@ router.get(
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(
"/:organizationId/plan/billing",
requireAuth({
diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts
index b4bb97753..88220933d 100644
--- a/backend/src/ee/services/EELicenseService.ts
+++ b/backend/src/ee/services/EELicenseService.ts
@@ -32,6 +32,7 @@ interface FeatureSet {
auditLogs: boolean;
status: 'incomplete' | 'incomplete_expired' | 'trialing' | 'active' | 'past_due' | 'canceled' | 'unpaid' | null;
trial_end: number | null;
+ has_used_trial: boolean;
}
/**
@@ -63,7 +64,8 @@ class EELicenseService {
customAlerts: true,
auditLogs: false,
status: null,
- trial_end: null
+ trial_end: null,
+ has_used_trial: true
}
public localFeatureSet: NodeCache;
@@ -71,7 +73,7 @@ class EELicenseService {
constructor() {
this._isLicenseValid = true;
this.localFeatureSet = new NodeCache({
- stdTTL: 300,
+ stdTTL: 60,
});
}
@@ -112,6 +114,12 @@ class EELicenseService {
await this.getPlan(organizationId, workspaceId);
}
}
+
+ public async delPlan(organizationId: string) {
+ if (this.instanceType === "cloud") {
+ this.localFeatureSet.del(`${organizationId}-`);
+ }
+ }
public async initGlobalFeatureSet() {
const licenseServerKey = await getLicenseServerKey();
diff --git a/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx b/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx
index 968f131bc..e09d35920 100644
--- a/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx
+++ b/frontend/src/components/v2/UpgradePlanModal/UpgradePlanModal.tsx
@@ -1,9 +1,11 @@
import Link from "next/link";
-
import { useSubscription } from "@app/context";
-
-import { Button } from "../Button";
+import { Button } from "@app/components/v2";
import { Modal, ModalClose, ModalContent } from "../Modal";
+import { useOrganization } from "@app/context";
+import {
+ useGetOrgTrialUrl
+} from "@app/hooks/api";
type Props = {
isOpen?: boolean;
@@ -13,32 +15,61 @@ type Props = {
export const UpgradePlanModal = ({ text, isOpen, onOpenChange }: Props): JSX.Element => {
const { subscription } = useSubscription();
+ const { currentOrg } = useOrganization();
+ const { mutateAsync, isLoading } = useGetOrgTrialUrl();
const link = (subscription && subscription.slug !== null)
? `/settings/billing/${localStorage.getItem("projectData.id") as string}`
: "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 (
- Upgrade Plan
- ,
-
-
- Cancel
-
-
- ]}
>
{text}
Upgrade and get access to this, as well as to other powerful enhancements.
+
+
+ {(subscription && !subscription.has_used_trial) ? "Start Pro Free Trial" : "Upgrade Plan"}
+
+ onOpenChange && onOpenChange(false)}
+ >
+ Cancel
+
+
)
diff --git a/frontend/src/hooks/api/organization/index.ts b/frontend/src/hooks/api/organization/index.ts
index cffa80d63..d523e1c04 100644
--- a/frontend/src/hooks/api/organization/index.ts
+++ b/frontend/src/hooks/api/organization/index.ts
@@ -13,4 +13,6 @@ export {
useGetOrgPmtMethods,
useGetOrgTaxIds,
useRenameOrg,
- useUpdateOrgBillingDetails} from "./queries";
+ useGetOrgTrialUrl,
+ useUpdateOrgBillingDetails
+} from "./queries";
diff --git a/frontend/src/hooks/api/organization/queries.tsx b/frontend/src/hooks/api/organization/queries.tsx
index d3f161f64..293ad7022 100644
--- a/frontend/src/hooks/api/organization/queries.tsx
+++ b/frontend/src/hooks/api/organization/queries.tsx
@@ -10,8 +10,10 @@ import {
PlanBillingInfo,
PmtMethod,
ProductsTable,
- RenameOrgDTO,
- TaxID} from "./types";
+ RenameOrgDTO,
+ GetOrgTrialUrlDTO,
+ TaxID
+} from "./types";
const organizationKeys = {
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) => {
return useQuery({
queryKey: organizationKeys.getOrgPlanBillingInfo(organizationId),
diff --git a/frontend/src/hooks/api/organization/types.ts b/frontend/src/hooks/api/organization/types.ts
index a33a7f875..b2573400b 100644
--- a/frontend/src/hooks/api/organization/types.ts
+++ b/frontend/src/hooks/api/organization/types.ts
@@ -10,6 +10,11 @@ export type RenameOrgDTO = {
newOrgName: string;
};
+export type GetOrgTrialUrlDTO = {
+ orgId: string;
+ success_url: string;
+}
+
export type BillingDetails = {
name: string;
email: string;
diff --git a/frontend/src/hooks/api/subscriptions/types.ts b/frontend/src/hooks/api/subscriptions/types.ts
index ca50255c8..d3d673764 100644
--- a/frontend/src/hooks/api/subscriptions/types.ts
+++ b/frontend/src/hooks/api/subscriptions/types.ts
@@ -15,4 +15,5 @@ export type SubscriptionPlan = {
environmentLimit: number;
status: "incomplete" | "incomplete_expired" | "trialing" | "active" | "past_due" | "canceled" | "unpaid" | null;
trial_end: number | null;
+ has_used_trial: boolean;
};
diff --git a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx b/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx
index b263a25ff..52fa0ba5b 100644
--- a/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx
+++ b/frontend/src/layouts/AppLayout/components/NavBar/NavBar.tsx
@@ -22,7 +22,10 @@ import {TFunction} from "i18next";
import guidGenerator from "@app/components/utilities/randomId";
import { useOrganization, useSubscription,useUser } from "@app/context";
-import { useLogoutUser } from "@app/hooks/api";
+import {
+ useLogoutUser,
+ useGetOrgTrialUrl
+} from "@app/hooks/api";
const supportOptions = (t: TFunction) => [
[
@@ -67,6 +70,7 @@ export const Navbar = () => {
const { subscription } = useSubscription();
const { currentOrg, orgs } = useOrganization();
+ const { mutateAsync, isLoading } = useGetOrgTrialUrl();
const { user } = useUser();
const logout = useLogoutUser();
@@ -346,12 +350,25 @@ export const Navbar = () => {
- {subscription && subscription.status === "trialing" && subscription.trial_end && (
-
-
- {`Currently trialing the ${formatPlanSlug(subscription.slug)} plan until ${formatDate(subscription.trial_end).formattedDate} - ${formatDate(subscription.trial_end).remainingDays} day(s) left. `}
- Add a card to avoid being downgraded to the Starter plan afterward →
-
+ {subscription && subscription.slug === "starter" && !subscription.has_used_trial && (
+
+ {
+ 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 Starter plan. Unlock the full power of Infisical on the Pro Free Trial →
+
)}
diff --git a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx
index 23a4be24d..cccbfaf2e 100644
--- a/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx
+++ b/frontend/src/views/Settings/BillingSettingsPage/components/BillingCloudTab/PreviewSection.tsx
@@ -2,7 +2,9 @@ import { Button } from "@app/components/v2";
import { useOrganization,useSubscription } from "@app/context";
import {
useCreateCustomerPortalSession,
- useGetOrgPlanBillingInfo} from "@app/hooks/api";
+ useGetOrgPlanBillingInfo,
+ useGetOrgTrialUrl
+} from "@app/hooks/api";
import { usePopUp } from "@app/hooks/usePopUp";
import { ManagePlansModal } from "./ManagePlansModal";
@@ -11,6 +13,7 @@ export const PreviewSection = () => {
const { currentOrg } = useOrganization();
const { subscription, isLoading: isSubscriptionLoading } = useSubscription();
const { data, isLoading } = useGetOrgPlanBillingInfo(currentOrg?._id ?? "");
+ const getOrgTrialUrl = useGetOrgTrialUrl();
const createCustomerPortalSession = useCreateCustomerPortalSession();
const { popUp, handlePopUpOpen, handlePopUpToggle } = usePopUp([
@@ -42,19 +45,41 @@ export const PreviewSection = () => {
.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 (
- {!isSubscriptionLoading && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && (
+ {subscription && subscription?.slug !== "enterprise" && subscription?.slug !== "pro" && subscription?.slug !== "pro-annual" && (
Become Infisical
Unlimited members, projects, RBAC, smart alerts, and so much more
handlePopUpOpen("managePlan")}
+ // onClick={() => handlePopUpOpen("managePlan")}
+ onClick={() => handleUpgradeBtnClick()}
color="mineshaft"
>
- Upgrade
+ {!subscription.has_used_trial ? "Start Pro Free Trial" : "Upgrade Plan"}
)}