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 ( - - , - - - - ]} >

{text}

Upgrade and get access to this, as well as to other powerful enhancements.

+
+ + +
) 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 && ( +
+
)}
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

)}