diff --git a/.env.example b/.env.example
index dd9aa60e05..1dd2f66d99 100644
--- a/.env.example
+++ b/.env.example
@@ -295,6 +295,7 @@ AB_TEST_BUCKET_PROBABILITY=50
# whether we redirect to the future/event-types from event-types or not
APP_ROUTER_EVENT_TYPES_ENABLED=1
APP_ROUTER_SETTINGS_ADMIN_ENABLED=1
+APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=1
APP_ROUTER_APPS_SLUG_ENABLED=1
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=1
# whether we redirect to the future/apps/categories from /apps/categories or not
diff --git a/apps/web/abTest/middlewareFactory.ts b/apps/web/abTest/middlewareFactory.ts
index f6743672da..3f23bde3e4 100644
--- a/apps/web/abTest/middlewareFactory.ts
+++ b/apps/web/abTest/middlewareFactory.ts
@@ -6,6 +6,7 @@ import z from "zod";
const ROUTES: [URLPattern, boolean][] = [
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
["/settings/admin/:path*", process.env.APP_ROUTER_SETTINGS_ADMIN_ENABLED === "1"] as const,
+ ["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
["/apps/:slug/setup", process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED === "1"] as const,
["/apps/categories", process.env.APP_ROUTER_APPS_CATEGORIES_ENABLED === "1"] as const,
diff --git a/apps/web/app/future/(individual-page-wrapper)/apps/installed/[category]/layout.tsx b/apps/web/app/future/(individual-page-wrapper)/apps/installed/[category]/layout.tsx
new file mode 100644
index 0000000000..918ae3fa16
--- /dev/null
+++ b/apps/web/app/future/(individual-page-wrapper)/apps/installed/[category]/layout.tsx
@@ -0,0 +1,15 @@
+import { type ReactElement } from "react";
+
+import PageWrapper from "@components/PageWrapperAppDir";
+
+type EventTypesLayoutProps = {
+ children: ReactElement;
+};
+
+export default function Layout({ children }: EventTypesLayoutProps) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/web/app/future/(individual-page-wrapper)/apps/installed/[category]/page.tsx b/apps/web/app/future/(individual-page-wrapper)/apps/installed/[category]/page.tsx
new file mode 100644
index 0000000000..203ad830b5
--- /dev/null
+++ b/apps/web/app/future/(individual-page-wrapper)/apps/installed/[category]/page.tsx
@@ -0,0 +1,36 @@
+import LegacyPage from "@pages/apps/installed/[category]";
+import { _generateMetadata } from "app/_utils";
+import { notFound } from "next/navigation";
+import { z } from "zod";
+
+import { APP_NAME } from "@calcom/lib/constants";
+import { AppCategories } from "@calcom/prisma/enums";
+
+const querySchema = z.object({
+ category: z.nativeEnum(AppCategories),
+});
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ (t) => `${t("installed_apps")} | ${APP_NAME}`,
+ (t) => t("manage_your_connected_apps")
+ );
+};
+
+const getPageProps = async ({ params }: { params: Record }) => {
+ const p = querySchema.safeParse(params);
+
+ if (!p.success) {
+ return notFound();
+ }
+
+ return {
+ category: p.data.category,
+ };
+};
+
+export default async function Page({ params }: { params: Record }) {
+ const { category } = await getPageProps({ params });
+
+ return ;
+}
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts
index fa4da2d680..dac4ef0d21 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/middleware.ts
@@ -64,6 +64,23 @@ const middleware = async (req: NextRequest): Promise> => {
requestHeaders.set("x-csp-enforce", "true");
}
+ if (url.pathname.startsWith("/future/apps/installed")) {
+ const returnTo = req.cookies.get("return-to")?.value;
+ if (returnTo !== undefined) {
+ requestHeaders.set("Set-Cookie", "return-to=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT");
+
+ let validPathname = returnTo;
+
+ try {
+ validPathname = new URL(returnTo).pathname;
+ } catch (e) {}
+
+ const nextUrl = url.clone();
+ nextUrl.pathname = validPathname;
+ return NextResponse.redirect(nextUrl, { headers: requestHeaders });
+ }
+ }
+
requestHeaders.set("x-pathname", url.pathname);
const locale = await getLocale(req);
@@ -103,6 +120,8 @@ export const config = {
"/future/event-types/",
"/settings/admin/:path*",
"/future/settings/admin/:path*",
+ "/apps/installed/:category/",
+ "/future/apps/installed/:category/",
"/apps/:slug/",
"/future/apps/:slug/",
"/apps/:slug/setup/",
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index 2106aeb0c1..9ac7032c8c 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -516,6 +516,11 @@ const nextConfig = {
destination: "/apps/installed/conferencing",
permanent: true,
},
+ {
+ source: "/apps/installed",
+ destination: "/apps/installed/calendar",
+ permanent: true,
+ },
// OAuth callbacks when sent to localhost:3000(w would be expected) should be redirected to corresponding to WEBAPP_URL
...(process.env.NODE_ENV === "development" &&
// Safer to enable the redirect only when the user is opting to test out organizations
diff --git a/apps/web/pages/apps/installed/[category].tsx b/apps/web/pages/apps/installed/[category].tsx
index 2cffde2cdf..5a207ecc13 100644
--- a/apps/web/pages/apps/installed/[category].tsx
+++ b/apps/web/pages/apps/installed/[category].tsx
@@ -1,3 +1,5 @@
+"use client";
+
import { useReducer } from "react";
import { z } from "zod";
diff --git a/apps/web/playwright/ab-tests-redirect.e2e.ts b/apps/web/playwright/ab-tests-redirect.e2e.ts
index 601c04f282..330da414c1 100644
--- a/apps/web/playwright/ab-tests-redirect.e2e.ts
+++ b/apps/web/playwright/ab-tests-redirect.e2e.ts
@@ -5,6 +5,33 @@ import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
test.describe("apps/ A/B tests", () => {
+ test("should point to the /future/apps/installed/[category]", async ({ page, users, context }) => {
+ await context.addCookies([
+ {
+ name: "x-calcom-future-routes-override",
+ value: "1",
+ url: "http://localhost:3000",
+ },
+ ]);
+ const user = await users.create();
+
+ await user.apiLogin();
+
+ await page.goto("/apps/installed/messaging");
+
+ await page.waitForLoadState();
+
+ const dataNextJsRouter = await page.evaluate(() =>
+ window.document.documentElement.getAttribute("data-nextjs-router")
+ );
+
+ expect(dataNextJsRouter).toEqual("app");
+
+ const locator = page.getByRole("heading", { name: "Messaging" });
+
+ await expect(locator).toBeVisible();
+ });
+
test("should point to the /future/apps/[slug]", async ({ page, users, context }) => {
await context.addCookies([
{
diff --git a/turbo.json b/turbo.json
index f6a0f7145b..4820f14d8a 100644
--- a/turbo.json
+++ b/turbo.json
@@ -198,6 +198,7 @@
"ALLOWED_HOSTNAMES",
"ANALYZE",
"API_KEY_PREFIX",
+ "APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED",
"APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED",
"APP_ROUTER_APPS_CATEGORIES_ENABLED",
"APP_ROUTER_APPS_SLUG_ENABLED",