Update free plan logic

This commit is contained in:
Tuan Dang
2023-07-10 13:48:46 +07:00
parent 25b1673321
commit e91e7f96c2
10 changed files with 180 additions and 32 deletions
@@ -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
+15
View File
@@ -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({
+10 -2
View File
@@ -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>
) )
+3 -1
View File
@@ -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 &rarr;</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 &rarr;</span>
</button>
</div> </div>
)} )}
</div> </div>
@@ -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>
)} )}