diff --git a/.env.example b/.env.example
index c0ecaa4d653fc5..dd9aa60e05062d 100644
--- a/.env.example
+++ b/.env.example
@@ -297,3 +297,7 @@ APP_ROUTER_EVENT_TYPES_ENABLED=1
APP_ROUTER_SETTINGS_ADMIN_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
+APP_ROUTER_APPS_CATEGORIES_ENABLED=1
+# whether we redirect to the future/apps/categories/[category] from /apps/categories/[category] or not
+APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED=1
diff --git a/apps/web/abTest/middlewareFactory.ts b/apps/web/abTest/middlewareFactory.ts
index 596a8b3035ae0a..76be7e14472e29 100644
--- a/apps/web/abTest/middlewareFactory.ts
+++ b/apps/web/abTest/middlewareFactory.ts
@@ -6,8 +6,10 @@ 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/:slug", Boolean(process.env.APP_ROUTER_APPS_SLUG_ENABLED)] as const,
- ["/apps/:slug/setup", Boolean(process.env.APP_ROUTER_APPS_SLUG_SETUP_ENABLED)] 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,
+ ["/apps/categories/:category", process.env.APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED === "1"] as const,
].map(([pathname, enabled]) => [
new URLPattern({
pathname,
diff --git a/apps/web/app/future/(individual-page-wrapper)/apps/categories/[category]/layout.tsx b/apps/web/app/future/(individual-page-wrapper)/apps/categories/[category]/layout.tsx
new file mode 100644
index 00000000000000..dfccdf2d777b16
--- /dev/null
+++ b/apps/web/app/future/(individual-page-wrapper)/apps/categories/[category]/layout.tsx
@@ -0,0 +1,19 @@
+import { headers } from "next/headers";
+import { type ReactElement } from "react";
+
+import PageWrapper from "@components/PageWrapperAppDir";
+
+type WrapperWithLayoutProps = {
+ children: ReactElement;
+};
+
+export default async function WrapperWithLayout({ children }: WrapperWithLayoutProps) {
+ const h = headers();
+ const nonce = h.get("x-nonce") ?? undefined;
+
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/web/app/future/(individual-page-wrapper)/apps/categories/[category]/page.tsx b/apps/web/app/future/(individual-page-wrapper)/apps/categories/[category]/page.tsx
new file mode 100644
index 00000000000000..50f504cef3ac8a
--- /dev/null
+++ b/apps/web/app/future/(individual-page-wrapper)/apps/categories/[category]/page.tsx
@@ -0,0 +1,73 @@
+import CategoryPage from "@pages/apps/categories/[category]";
+import { Prisma } from "@prisma/client";
+import { _generateMetadata } from "app/_utils";
+import { notFound } from "next/navigation";
+import z from "zod";
+
+import { getAppRegistry } from "@calcom/app-store/_appRegistry";
+import { APP_NAME } from "@calcom/lib/constants";
+import prisma from "@calcom/prisma";
+import { AppCategories } from "@calcom/prisma/enums";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ () => `${APP_NAME} | ${APP_NAME}`,
+ () => ""
+ );
+};
+
+export const getStaticParams = async () => {
+ const paths = Object.keys(AppCategories);
+
+ try {
+ await prisma.$queryRaw`SELECT 1`;
+ } catch (e: unknown) {
+ if (e instanceof Prisma.PrismaClientInitializationError) {
+ // Database is not available at build time. Make sure we fall back to building these pages on demand
+ return [];
+ } else {
+ throw e;
+ }
+ }
+
+ return paths.map((category) => ({ category }));
+};
+
+const querySchema = z.object({
+ category: z.nativeEnum(AppCategories),
+});
+
+const getPageProps = async ({ params }: { params: Record }) => {
+ const p = querySchema.safeParse(params);
+
+ if (!p.success) {
+ return notFound();
+ }
+
+ const appQuery = await prisma.app.findMany({
+ where: {
+ categories: {
+ has: p.data.category,
+ },
+ },
+ select: {
+ slug: true,
+ },
+ });
+
+ const dbAppsSlugs = appQuery.map((category) => category.slug);
+
+ const appStore = await getAppRegistry();
+
+ const apps = appStore.filter((app) => dbAppsSlugs.includes(app.slug));
+ return {
+ apps,
+ };
+};
+
+export default async function Page({ params }: { params: Record }) {
+ const { apps } = await getPageProps({ params });
+ return ;
+}
+
+export const dynamic = "force-static";
diff --git a/apps/web/app/future/(individual-page-wrapper)/apps/categories/page.tsx b/apps/web/app/future/(individual-page-wrapper)/apps/categories/page.tsx
new file mode 100644
index 00000000000000..c0d6c3d15e714e
--- /dev/null
+++ b/apps/web/app/future/(individual-page-wrapper)/apps/categories/page.tsx
@@ -0,0 +1,56 @@
+import LegacyPage from "@pages/apps/categories/index";
+import { ssrInit } from "app/_trpc/ssrInit";
+import { _generateMetadata } from "app/_utils";
+import { cookies, headers } from "next/headers";
+
+import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
+import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
+import { APP_NAME } from "@calcom/lib/constants";
+
+import PageWrapper from "@components/PageWrapperAppDir";
+
+export const generateMetadata = async () => {
+ return await _generateMetadata(
+ () => `Categories | ${APP_NAME}`,
+ () => ""
+ );
+};
+
+async function getPageProps() {
+ const ssr = await ssrInit();
+ const req = { headers: headers(), cookies: cookies() };
+
+ // @ts-expect-error Type '{ headers: ReadonlyHeaders; cookies: ReadonlyRequestCookies; }' is not assignable to type 'NextApiRequest | IncomingMessage
+ const session = await getServerSession({ req });
+
+ let appStore;
+ if (session?.user?.id) {
+ appStore = await getAppRegistryWithCredentials(session.user.id);
+ } else {
+ appStore = await getAppRegistry();
+ }
+
+ const categories = appStore.reduce((c, app) => {
+ for (const category of app.categories) {
+ c[category] = c[category] ? c[category] + 1 : 1;
+ }
+ return c;
+ }, {} as Record);
+
+ return {
+ categories: Object.entries(categories).map(([name, count]) => ({ name, count })),
+ dehydratedState: await ssr.dehydrate(),
+ };
+}
+
+export default async function Page() {
+ const props = await getPageProps();
+ const h = headers();
+ const nonce = h.get("x-nonce") ?? undefined;
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts
index 40f17053153197..fa4da2d6807e23 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/middleware.ts
@@ -103,12 +103,14 @@ export const config = {
"/future/event-types/",
"/settings/admin/:path*",
"/future/settings/admin/:path*",
-
"/apps/:slug/",
"/future/apps/:slug/",
-
"/apps/:slug/setup/",
"/future/apps/:slug/setup/",
+ "/apps/categories/",
+ "/future/apps/categories/",
+ "/apps/categories/:category/",
+ "/future/apps/categories/:category/",
],
};
diff --git a/apps/web/pages/apps/categories/[category].tsx b/apps/web/pages/apps/categories/[category].tsx
index 81de5e70c86331..0cad97bfa1016d 100644
--- a/apps/web/pages/apps/categories/[category].tsx
+++ b/apps/web/pages/apps/categories/[category].tsx
@@ -1,3 +1,5 @@
+"use client";
+
import { Prisma } from "@prisma/client";
import type { GetStaticPropsContext, InferGetStaticPropsType } from "next";
import Link from "next/link";
diff --git a/apps/web/pages/apps/categories/index.tsx b/apps/web/pages/apps/categories/index.tsx
index 7b40ade47b0a2e..1012640fc8d71f 100644
--- a/apps/web/pages/apps/categories/index.tsx
+++ b/apps/web/pages/apps/categories/index.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
@@ -13,7 +15,7 @@ import PageWrapper from "@components/PageWrapper";
import { ssrInit } from "@server/lib/ssr";
-export default function Apps({ categories }: inferSSRProps) {
+export default function Apps({ categories }: Omit, "trpcState">) {
const { t, isLocaleReady } = useLocale();
return (
diff --git a/apps/web/playwright/ab-tests-redirect.e2e.ts b/apps/web/playwright/ab-tests-redirect.e2e.ts
index feb64ac1eb8db5..58c474228f17ab 100644
--- a/apps/web/playwright/ab-tests-redirect.e2e.ts
+++ b/apps/web/playwright/ab-tests-redirect.e2e.ts
@@ -58,4 +58,58 @@ test.describe("apps/ A/B tests", () => {
await expect(locator).toBeVisible();
});
-});
+
+ test("should point to the /future/apps/categories", 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/categories");
+
+ await page.waitForLoadState();
+
+ const dataNextJsRouter = await page.evaluate(() =>
+ window.document.documentElement.getAttribute("data-nextjs-router")
+ );
+
+ expect(dataNextJsRouter).toEqual("app");
+
+ const locator = page.getByTestId("app-store-category-messaging");
+
+ await expect(locator).toBeVisible();
+ });
+
+ test("should point to the /future/apps/categories/[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/categories/messaging");
+
+ await page.waitForLoadState();
+
+ const dataNextJsRouter = await page.evaluate(() =>
+ window.document.documentElement.getAttribute("data-nextjs-router")
+ );
+
+ expect(dataNextJsRouter).toEqual("app");
+
+ const locator = page.getByText(/messaging apps/i);
+
+ await expect(locator).toBeVisible();
+ });
+});
\ No newline at end of file
diff --git a/turbo.json b/turbo.json
index 100cd41c08a091..f6a0f7145b0a5a 100644
--- a/turbo.json
+++ b/turbo.json
@@ -198,6 +198,8 @@
"ALLOWED_HOSTNAMES",
"ANALYZE",
"API_KEY_PREFIX",
+ "APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED",
+ "APP_ROUTER_APPS_CATEGORIES_ENABLED",
"APP_ROUTER_APPS_SLUG_ENABLED",
"APP_ROUTER_APPS_SLUG_SETUP_ENABLED",
"APP_ROUTER_EVENT_TYPES_ENABLED",