Skip to content

Commit

Permalink
Button to subscribe to pro plans (without payment) (#2283)
Browse files Browse the repository at this point in the history
  • Loading branch information
PopDaph authored Oct 26, 2023
1 parent 72d2401 commit bf42a4f
Show file tree
Hide file tree
Showing 4 changed files with 358 additions and 20 deletions.
56 changes: 36 additions & 20 deletions front/components/sparkle/navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
PaperAirplaneIcon,
PlanetIcon,
RobotIcon,
ShapesIcon,
} from "@dust-tt/sparkle";
import { UsersIcon } from "@heroicons/react/20/solid";

Expand All @@ -27,6 +28,7 @@ export type TopNavigationId = "assistant" | "settings";
export type SubNavigationAdminId =
| "data_sources_managed"
| "data_sources_static"
| "subscription"
| "workspace"
| "members"
| "developers"
Expand Down Expand Up @@ -150,30 +152,44 @@ export const subNavigationAdmin = ({
});

if (owner.role === "admin") {
const menus: SparkleAppLayoutNavigation[] = [];
if (isDevelopmentOrDustWorkspace(owner)) {
menus.push({
id: "subscription",
label: "Subscription (Dust only)",
icon: ShapesIcon,
href: `/w/${owner.sId}/subscription`,
current: current === "subscription",
subMenuLabel: current === "subscription" ? subMenuLabel : undefined,
subMenu: current === "subscription" ? subMenu : undefined,
});
}
menus.push(
{
id: "workspace",
label: "Workspace",
icon: PlanetIcon,
href: `/w/${owner.sId}/workspace`,
current: current === "workspace",
subMenuLabel: current === "workspace" ? subMenuLabel : undefined,
subMenu: current === "workspace" ? subMenu : undefined,
},
{
id: "members",
label: "Members",
icon: UsersIcon,
href: `/w/${owner.sId}/members`,
current: current === "members",
subMenuLabel: current === "members" ? subMenuLabel : undefined,
subMenu: current === "members" ? subMenu : undefined,
}
);

nav.push({
id: "workspace",
label: "Settings",
variant: "secondary",
menus: [
{
id: "workspace",
label: "Workspace",
icon: PlanetIcon,
href: `/w/${owner.sId}/workspace`,
current: current === "workspace",
subMenuLabel: current === "workspace" ? subMenuLabel : undefined,
subMenu: current === "workspace" ? subMenu : undefined,
},
{
id: "members",
label: "Members",
icon: UsersIcon,
href: `/w/${owner.sId}/members`,
current: current === "members",
subMenuLabel: current === "members" ? subMenuLabel : undefined,
subMenu: current === "members" ? subMenu : undefined,
},
],
menus,
});
}

Expand Down
110 changes: 110 additions & 0 deletions front/lib/plans/subscription.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { Authenticator } from "@app/lib/auth";
import { front_sequelize } from "@app/lib/databases";
import { Plan, Subscription, Workspace } from "@app/lib/models";
import {
FREE_TEST_PLAN_CODE,
FREE_TEST_PLAN_DATA,
FREE_UPGRADED_PLAN_CODE,
PlanAttributes,
Expand Down Expand Up @@ -156,3 +158,111 @@ export const internalSubscribeWorkspaceToFreeUpgradedPlan = async ({
};
});
};

export const subscribeWorkspaceToPlan = async (
auth: Authenticator,
{ planCode }: { planCode: string }
): Promise<PlanType> => {
const user = auth.user();
const workspace = auth.workspace();
const activePlan = auth.plan();

if (!user || !auth.isAdmin() || !workspace || !activePlan) {
throw new Error(
"Unauthorized `auth` data: cannot process to subscription of new Plan."
);
}

// We prevent the user to subscribe to the FREE_UPGRADED_PLAN: this is an internal plan for Dust workspaces only.
if (planCode === FREE_UPGRADED_PLAN_CODE) {
throw new Error(
`Unauthorized: cannot subscribe to ${FREE_UPGRADED_PLAN_CODE}.`
);
}

// Case of a downgrade to the free default plan: we use the internal function
if (planCode === FREE_TEST_PLAN_CODE) {
return await internalSubscribeWorkspaceToFreeTestPlan({
workspaceId: workspace.sId,
});
}

const now = new Date();

return await front_sequelize.transaction(async (t) => {
// We get the plan to subscribe to
const newPlan = await Plan.findOne({
where: { code: planCode },
transaction: t,
});
if (!newPlan) {
throw new Error(`Cannot subscribe to plan ${planCode}: not found.`);
}

// We search for an active subscription for this workspace
const activeSubscription = await Subscription.findOne({
where: { workspaceId: workspace.id, status: "active" },
transaction: t,
});

// We check if the user is already subscribed to this plan
if (activeSubscription && activeSubscription.planId === newPlan.id) {
throw new Error(
`Cannot subscribe to plan ${planCode}: already subscribed.`
);
}

// We end the active subscription if any
if (activeSubscription) {
await activeSubscription.update(
{
status: "ended",
endDate: now,
},
{ transaction: t }
);
}

// We create a new subscription
const newSubscription = await Subscription.create(
{
sId: generateModelSId(),
workspaceId: workspace.id,
planId: newPlan.id,
status: "active",
startDate: now,
},
{ transaction: t }
);

return {
code: newPlan.code,
name: newPlan.name,
status: "active",
startDate: newSubscription.startDate.getTime(),
endDate: newSubscription.endDate?.getTime() || null,
limits: {
assistant: {
isSlackBotAllowed: newPlan.isSlackbotAllowed,
maxMessages: newPlan.maxMessages,
},
connections: {
isSlackAllowed: newPlan.isManagedSlackAllowed,
isNotionAllowed: newPlan.isManagedNotionAllowed,
isGoogleDriveAllowed: newPlan.isManagedGoogleDriveAllowed,
isGithubAllowed: newPlan.isManagedGithubAllowed,
},
dataSources: {
count: newPlan.maxNbStaticDataSources,
documents: {
count: newPlan.maxNbStaticDocuments,
sizeMb: newPlan.maxSizeStaticDataSources,
},
},
users: {
maxUsers: newPlan.maxUsersInWorkspace,
},
},
};
});
};
74 changes: 74 additions & 0 deletions front/pages/api/w/[wId]/subscription/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { NextApiRequest, NextApiResponse } from "next";

import { Authenticator, getSession } from "@app/lib/auth";
import { subscribeWorkspaceToPlan } from "@app/lib/plans/subscription";
import { apiError, withLogging } from "@app/logger/withlogging";
import { PlanType } from "@app/types/user";

export type GetSubscriptionResponseBody = {
plan: PlanType;
};

async function handler(
req: NextApiRequest,
res: NextApiResponse<GetSubscriptionResponseBody>
): Promise<void> {
const session = await getSession(req, res);
const auth = await Authenticator.fromSession(
session,
req.query.wId as string
);

const owner = auth.workspace();
const plan = auth.plan();
if (!owner || !plan) {
return apiError(req, res, {
status_code: 404,
api_error: {
type: "workspace_not_found",
message: "The workspace was not found.",
},
});
}

if (!auth.isAdmin()) {
return apiError(req, res, {
status_code: 403,
api_error: {
type: "workspace_auth_error",
message:
"Only users that are `admins` for the current workspace can see memberships or modify it.",
},
});
}

switch (req.method) {
case "GET":
// Should return the list of featured plans
return apiError(req, res, {
status_code: 404,
api_error: {
type: "invalid_request_error",
message: "Not implemented yet.",
},
});

case "POST":
const newPlan = await subscribeWorkspaceToPlan(auth, {
planCode: req.body.planCode,
});
res.status(200).json({ plan: newPlan });
return;

default:
return apiError(req, res, {
status_code: 405,
api_error: {
type: "method_not_supported_error",
message: "The method passed is not supported, GET is expected.",
},
});
}
}

export default withLogging(handler);
Loading

0 comments on commit bf42a4f

Please sign in to comment.