diff --git a/src/app/AppRootLayoutClient.tsx b/src/app/AppRootLayoutClient.tsx index d4d1724b..6e414a0f 100644 --- a/src/app/AppRootLayoutClient.tsx +++ b/src/app/AppRootLayoutClient.tsx @@ -1,27 +1,19 @@ "use client"; -import { ApolloProvider } from "@apollo/client"; import { AppRouterCacheProvider } from "@mui/material-nextjs/v16-appRouter"; import { Analytics } from "@vercel/analytics/react"; import { SpeedInsights } from "@vercel/speed-insights/next"; import type { Session } from "next-auth"; -import { SessionProvider } from "next-auth/react"; -import { SnackbarProvider } from "notistack"; import React, { type ReactNode } from "react"; import "symbol-observable"; import { CookieConsentRoot } from "#/features/cookieConsent/ui/CookieConsentRoot"; import { ProjectBrowser } from "#/features/project/ui/ProjectBrowser/ProjectBrowser"; import { ProjectBrowserProvider } from "#/features/project/ui/ProjectBrowser/ProjectBrowserContext"; -import { apolloClient } from "#/graphql/apolloClient"; import { type I18nProps } from "#/i18n/getI18nProps"; import type { Locales } from "#/i18n/i18n-types"; -import { TrpcProvider } from "#/shared/trpc/TrpcProvider"; -import { SnackbarCloseButton } from "#/shared/ui/atoms/SnackbarCloseButton"; +import { AppShellProviders } from "#/shared/ui/providers/AppShellProviders"; import { AppRouterI18nProvider } from "#/shared/ui/providers/I18nProvider"; -import { StateThemeProvider } from "#/shared/ui/providers/StateThemeProvider"; -import { isSnackbarClosable } from "#/shared/ui/snackbarClosability"; -import { ReduxProvider } from "#/store/provider"; type AppRootLayoutClientProps = { children: ReactNode; @@ -31,7 +23,7 @@ type AppRootLayoutClientProps = { }; /** - * Mirrors `pages/_app` providers for App Router routes (default locale home). + * App Router provider shell. Shares {@link AppShellProviders} with Pages `_app`. */ export const AppRootLayoutClient: React.FC = ({ children, @@ -41,43 +33,18 @@ export const AppRootLayoutClient: React.FC = ({ }) => { return ( - - - - - - - isSnackbarClosable(snackbarKey) ? ( - - ) : null - } - classes={{ - containerAnchorOriginBottomLeft: - "snackbar-mobile-bottom-margin", - containerAnchorOriginBottomCenter: - "snackbar-mobile-bottom-margin", - containerAnchorOriginBottomRight: - "snackbar-mobile-bottom-margin", - }} - > - - - - {children} - - - - - - - - - - - - + + + + + {children} + + + + + + + ); }; diff --git a/src/app/internal-marketing/[locale]/layout.tsx b/src/app/internal-marketing/[locale]/layout.tsx index da4ce061..23059e6d 100644 --- a/src/app/internal-marketing/[locale]/layout.tsx +++ b/src/app/internal-marketing/[locale]/layout.tsx @@ -5,7 +5,7 @@ import { notFound } from "next/navigation"; import type { Locales } from "#/i18n/i18n-types"; import { locales } from "#/i18n/i18n-util"; import { loadI18nForLocale } from "#/i18n/loadI18nForLocale"; -import { authOptions } from "#/pages/api/auth/[...nextauth]"; +import { authOptions } from "#/server/auth/authOptions"; import { AppRootLayoutClient } from "#/app/AppRootLayoutClient"; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 101bc439..d47a5d7e 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -1,25 +1,17 @@ -import { ApolloProvider } from "@apollo/client"; import type { Session } from "next-auth"; -import { SessionProvider } from "next-auth/react"; import { type AppProps } from "next/app"; -import { SnackbarProvider } from "notistack"; import React from "react"; import "symbol-observable"; import { CookieConsentRoot } from "#/features/cookieConsent/ui/CookieConsentRoot"; import { ProjectBrowser } from "#/features/project/ui/ProjectBrowser/ProjectBrowser"; import { ProjectBrowserProvider } from "#/features/project/ui/ProjectBrowser/ProjectBrowserContext"; -import { apolloClient } from "#/graphql/apolloClient"; import { type I18nProps } from "#/i18n/getI18nProps"; import { EmotionCacheProvider } from "#/shared/emotion/EmotionCacheContext"; // `_document` is server-only; importing fonts here registers @font-face + CSS vars in the client bundle. import "#/shared/fonts/appFonts"; -import { TrpcProvider } from "#/shared/trpc/TrpcProvider"; -import { SnackbarCloseButton } from "#/shared/ui/atoms/SnackbarCloseButton"; +import { AppShellProviders } from "#/shared/ui/providers/AppShellProviders"; import { I18nProvider } from "#/shared/ui/providers/I18nProvider"; -import { StateThemeProvider } from "#/shared/ui/providers/StateThemeProvider"; -import { isSnackbarClosable } from "#/shared/ui/snackbarClosability"; -import { ReduxProvider } from "#/store/provider"; import type { SsrDeviceType } from "#/themes"; import "#/styles/globals.css"; @@ -35,41 +27,19 @@ type MyAppProps = { const MyApp: React.FC> = ({ Component, pageProps }) => { return ( - - - - - - - isSnackbarClosable(snackbarKey) ? ( - - ) : null - } - classes={{ - containerAnchorOriginBottomLeft: - "snackbar-mobile-bottom-margin", - containerAnchorOriginBottomCenter: - "snackbar-mobile-bottom-margin", - containerAnchorOriginBottomRight: - "snackbar-mobile-bottom-margin", - }} - > - - - - - - - - - - - - - - + + + + + + + + + + ); }; diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts index 97c4e13f..fb288782 100644 --- a/src/pages/api/auth/[...nextauth].ts +++ b/src/pages/api/auth/[...nextauth].ts @@ -1,38 +1,7 @@ -// Prisma adapter for NextAuth, optional and can be removed -import { PrismaAdapter } from "@next-auth/prisma-adapter"; -import NextAuth, { type NextAuthOptions } from "next-auth"; -import GHProvider from "next-auth/providers/github"; -// import DiscordProvider from "next-auth/providers/discord"; -import GoogleProvider from "next-auth/providers/google"; +import NextAuth from "next-auth"; -import { env } from "#/env/server.mjs"; -import { db } from "#/server/db/client"; +import { authOptions } from "#/server/auth/authOptions"; -export const authOptions: NextAuthOptions = { - // Include user.id on session - callbacks: { - session({ session, user }) { - Object.assign(session.user, user); - return session; - }, - }, - // Configure one or more authentication providers - adapter: PrismaAdapter(db as any), // TODO: Remove `as any` when @next-auth/prisma-adapter fixes types - providers: [ - // DiscordProvider({ - // clientId: env.DISCORD_CLIENT_ID, - // clientSecret: env.DISCORD_CLIENT_SECRET, - // }), - GHProvider({ - clientId: env.GITHUB_CLIENT_ID ?? "", - clientSecret: env.GITHUB_CLIENT_SECRET ?? "", - }), - GoogleProvider({ - clientId: env.GOOGLE_CLIENT_ID ?? "", - clientSecret: env.GOOGLE_CLIENT_SECRET ?? "", - }), - // ...add more providers here - ], -}; +export { authOptions }; export default NextAuth(authOptions); diff --git a/src/server/auth/authOptions.ts b/src/server/auth/authOptions.ts new file mode 100644 index 00000000..dd7d08fd --- /dev/null +++ b/src/server/auth/authOptions.ts @@ -0,0 +1,37 @@ +// Prisma adapter for NextAuth, optional and can be removed +import { PrismaAdapter } from "@next-auth/prisma-adapter"; +import { type NextAuthOptions } from "next-auth"; +import GHProvider from "next-auth/providers/github"; +// import DiscordProvider from "next-auth/providers/discord"; +import GoogleProvider from "next-auth/providers/google"; + +import { env } from "#/env/server.mjs"; +import { db } from "#/server/db/client"; + +/** Shared NextAuth config for Pages API routes and App Router RSC layouts. */ +export const authOptions: NextAuthOptions = { + // Include user.id on session + callbacks: { + session({ session, user }) { + Object.assign(session.user, user); + return session; + }, + }, + // Configure one or more authentication providers + adapter: PrismaAdapter(db as any), // TODO: Remove `as any` when @next-auth/prisma-adapter fixes types + providers: [ + // DiscordProvider({ + // clientId: env.DISCORD_CLIENT_ID, + // clientSecret: env.DISCORD_CLIENT_SECRET, + // }), + GHProvider({ + clientId: env.GITHUB_CLIENT_ID ?? "", + clientSecret: env.GITHUB_CLIENT_SECRET ?? "", + }), + GoogleProvider({ + clientId: env.GOOGLE_CLIENT_ID ?? "", + clientSecret: env.GOOGLE_CLIENT_SECRET ?? "", + }), + // ...add more providers here + ], +}; diff --git a/src/server/auth/get-server-auth-session.ts b/src/server/auth/get-server-auth-session.ts index 3efcaffa..3cb54889 100644 --- a/src/server/auth/get-server-auth-session.ts +++ b/src/server/auth/get-server-auth-session.ts @@ -1,7 +1,7 @@ import { type GetServerSidePropsContext } from "next"; import { getServerSession } from "next-auth"; -import { authOptions } from "#/pages/api/auth/[...nextauth]"; +import { authOptions } from "#/server/auth/authOptions"; /** * Wrapper for getServerSession https://next-auth.js.org/configuration/nextjs diff --git a/src/shared/ui/providers/AppShellProviders.tsx b/src/shared/ui/providers/AppShellProviders.tsx new file mode 100644 index 00000000..1a97c9a1 --- /dev/null +++ b/src/shared/ui/providers/AppShellProviders.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { ApolloProvider } from "@apollo/client"; +import type { Session } from "next-auth"; +import { SessionProvider } from "next-auth/react"; +import { SnackbarProvider } from "notistack"; +import React, { type ReactNode } from "react"; + +import { apolloClient } from "#/graphql/apolloClient"; +import { TrpcProvider } from "#/shared/trpc/TrpcProvider"; +import { SnackbarCloseButton } from "#/shared/ui/atoms/SnackbarCloseButton"; +import { StateThemeProvider } from "#/shared/ui/providers/StateThemeProvider"; +import { isSnackbarClosable } from "#/shared/ui/snackbarClosability"; +import { ReduxProvider } from "#/store/provider"; +import type { SsrDeviceType } from "#/themes"; + +type AppShellProvidersProps = { + children: ReactNode; + session: Session | null; + ssrDeviceType?: SsrDeviceType; +}; + +/** + * Shared client provider stack for Pages `_app` and App Router layouts. + * Locale/i18n and router-specific cache wrappers stay outside this tree. + */ +export const AppShellProviders: React.FC = ({ + children, + session, + ssrDeviceType, +}) => ( + + + + + + + isSnackbarClosable(snackbarKey) ? ( + + ) : null + } + classes={{ + containerAnchorOriginBottomLeft: + "snackbar-mobile-bottom-margin", + containerAnchorOriginBottomCenter: + "snackbar-mobile-bottom-margin", + containerAnchorOriginBottomRight: + "snackbar-mobile-bottom-margin", + }} + > + {children} + + + + + + +); diff --git a/vibe-docs/Instant-Navigations-TODO.md b/vibe-docs/Instant-Navigations-TODO.md index c458a305..fd33ea1b 100644 --- a/vibe-docs/Instant-Navigations-TODO.md +++ b/vibe-docs/Instant-Navigations-TODO.md @@ -9,9 +9,9 @@ ## Phase 1 — 16.3.x on Vercel -- [x] Tried `next@16.3.0` — local OK; Vercel preview `/api/*` → `/en/404` -- [x] **Pinned back to `next@16.2.12`** so previews + App Router pilot can be verified -- [ ] Re-try latest 16.3.x when adapter-vercel i18n API fix is in a release that works on Turbopack previews +- [x] Tried `next@16.3.0` — local OK; Vercel preview `/api/*` → `/en/404` (2026-08, pre-adapter fix) +- [x] Re-tried on PR #165 preview (2026-08-09) — **same regression** (`x-matched-path: /en/404`) +- [x] **Stay on `next@16.2.12`** until Vercel + Pages `i18n` API routing is fixed - [ ] Preview smoke green on 16.3.x before enabling Instant Nav flags ## Phase 2 — App Router pilot @@ -29,6 +29,8 @@ - [ ] `cacheComponents` / `partialPrefetching` (blocked: root `headers()` + need 16.3) - [ ] `unstable_instant` on pilot routes (blocked until `cacheComponents`) - [x] Remove unused `@trpc/next` dependency +- [x] Extract `authOptions` to `src/server/auth/authOptions.ts` +- [x] Extract `AppShellProviders` shared by `_app` and `AppRootLayoutClient` ## Phase 3+ — Playground / full migration