From eeff554c1d233ee309d9cd18e54433ddc48c4500 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Wed, 29 Oct 2025 16:31:21 -0700 Subject: [PATCH 1/8] StackHandler is now a Client Component --- apps/dashboard/tsconfig.json | 2 +- packages/init-stack/src/index.ts | 3 +- .../src/components-page/stack-handler.tsx | 178 +++++++----------- 3 files changed, 70 insertions(+), 113 deletions(-) diff --git a/apps/dashboard/tsconfig.json b/apps/dashboard/tsconfig.json index 5d05b2a1e7..fb9c090878 100644 --- a/apps/dashboard/tsconfig.json +++ b/apps/dashboard/tsconfig.json @@ -14,7 +14,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "react-jsx", + "jsx": "preserve", "incremental": true, "noErrorTruncation": true, "plugins": [ diff --git a/packages/init-stack/src/index.ts b/packages/init-stack/src/index.ts index d5853c2fe4..3842fc9eda 100644 --- a/packages/init-stack/src/index.ts +++ b/packages/init-stack/src/index.ts @@ -947,8 +947,7 @@ ${shouldInheritFromClient ? `${indentation}inheritsFrom: stackClientApp,` : `${i } laterWriteFileIfNotExists( handlerPath, - `import { StackHandler } from "@stackframe/stack"; \nimport { stackServerApp } from "../../../stack/server"; \n\nexport default function Handler(props${handlerFileExtension.includes("ts") ? ": unknown" : "" - }) { \n${projectInfo.indentation} return ; \n } \n` + `import { StackHandler } from "@stackframe/stack"; \n\nexport default function Handler() { \n${projectInfo.indentation}return ; \n} \n` ); }, diff --git a/packages/template/src/components-page/stack-handler.tsx b/packages/template/src/components-page/stack-handler.tsx index 7a14578fcf..9904a6b78c 100644 --- a/packages/template/src/components-page/stack-handler.tsx +++ b/packages/template/src/components-page/stack-handler.tsx @@ -1,10 +1,11 @@ +"use client"; // THIS_LINE_PLATFORM next import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; -import { FilterUndefined, filterUndefined, pick } from "@stackframe/stack-shared/dist/utils/objects"; +import { FilterUndefined, filterUndefined } from "@stackframe/stack-shared/dist/utils/objects"; import { getRelativePart } from "@stackframe/stack-shared/dist/utils/urls"; -import { RedirectType, notFound, redirect } from 'next/navigation'; // THIS_LINE_PLATFORM next +import { notFound, redirect, RedirectType, usePathname, useSearchParams } from 'next/navigation'; // THIS_LINE_PLATFORM next import { useMemo } from 'react'; import { SignIn, SignUp, StackServerApp } from ".."; -import { MessageCard } from "../components/message-cards/message-card"; +import { useStackApp } from "../lib/hooks"; import { HandlerUrls, StackClientApp } from "../lib/stack-app"; import { AccountSettings } from "./account-settings"; import { CliAuthConfirmation } from "./cli-auth-confirm"; @@ -12,8 +13,8 @@ import { EmailVerification } from "./email-verification"; import { ErrorPage } from "./error-page"; import { ForgotPassword } from "./forgot-password"; import { MagicLinkCallback } from "./magic-link-callback"; -import { OAuthCallback } from "./oauth-callback"; import { MFA } from "./mfa"; +import { OAuthCallback } from "./oauth-callback"; import { PasswordReset } from "./password-reset"; import { SignOut } from "./sign-out"; import { TeamInvitation } from "./team-invitation"; @@ -39,7 +40,19 @@ type RouteProps = { searchParams: Promise> | Record, }; -const next15DeprecationWarning = "DEPRECATION WARNING: Next.js 15 disallows spreading the props argument of like `{...props}`, so you must now explicitly pass them in the `routeProps` argument: `routeProps={props}`. You can fix this by updating the code in the file `app/handler/[...stack]/route.tsx`."; +/** + * @deprecated The app parameter is no longer necessary. You can safely remove it. + */ +type DeprecatedAppProp = { + app?: StackClientApp | StackServerApp, +}; + +/** + * @deprecated The routeProps parameter is no longer necessary. You can safely remove it. + */ +type DeprecatedRouteProps = { + routeProps?: RouteProps | unknown, +}; const availablePaths = { signIn: 'sign-in', @@ -193,111 +206,52 @@ function renderComponent(props: { } } -// IF_PLATFORM next -async function NextStackHandler(props: BaseHandlerProps & { - app: StackServerApp, -} & ( - | Partial - | { - routeProps: RouteProps | unknown, - } -)): Promise { - if (!("routeProps" in props)) { - console.warn(next15DeprecationWarning); - } - - const routeProps = "routeProps" in props ? props.routeProps as RouteProps : pick(props, ["params", "searchParams"] as any); - const params = await routeProps.params; - const searchParams = await routeProps.searchParams; - - if (!params?.stack) { - return ( - -

Can't use {""} at this location. Make sure that the file is in a folder called [...stack] and you are passing the routeProps prop.

-
- ); - } - - const path = params.stack.join('/'); - - const redirectIfNotHandler = (name: keyof HandlerUrls) => { - const url = props.app.urls[name]; - const handlerUrl = props.app.urls.handler; - - if (url !== handlerUrl && url.startsWith(handlerUrl + "/")) { - return; - } - - const urlObj = new URL(url, "http://example.com"); - if (searchParams) { - for (const [key, value] of Object.entries(searchParams)) { - urlObj.searchParams.set(key, value); - } - } - - redirect(getRelativePart(urlObj), RedirectType.replace); - }; - - const result = renderComponent({ - path, - searchParams: searchParams ?? {}, - fullPage: props.fullPage, - componentProps: props.componentProps, - redirectIfNotHandler, - onNotFound: () => notFound(), - app: props.app, - }); - - if (result && 'redirect' in result) { - redirect(result.redirect, RedirectType.replace); - } - - return <> - {process.env.NODE_ENV === "development" && !("routeProps" in props) && ( - - {next15DeprecationWarning}. This warning will not be shown in production. - - )} - {result} - ; -} - -// ELSE_IF_PLATFORM react +function StackHandler(props: BaseHandlerProps & Partial & Partial & Partial & { location?: string }) { + // Use hooks to get app + const stackApp = props.app ?? useStackApp(); + + // IF_PLATFORM next + const pathname = usePathname(); + const searchParamsFromHook = useSearchParams(); + const currentLocation = pathname; + const searchParamsSource = searchParamsFromHook; + const origin = 'http://example.com'; + /* ELSE_IF_PLATFORM react + const currentLocation = props.location ?? window.location.pathname; + const searchParamsSource = new URLSearchParams(window.location.search); + const origin = window.location.origin; + END_PLATFORM */ -function ReactStackHandler(props: BaseHandlerProps & { - app: StackClientApp, - location: string, // Path like "/abc/def" -}) { const { path, searchParams } = useMemo(() => { - // Get search string from window.location since it's not included in currentLocation - const search = window.location.search; - const handlerPath = new URL(props.app.urls.handler, window.location.origin).pathname; - - // Remove the handler base path to get the relative path - const relativePath = props.location.startsWith(handlerPath) - ? props.location.slice(handlerPath.length).replace(/^\/+/, '') - : props.location.replace(/^\/+/, ''); + const handlerPath = new URL(stackApp.urls.handler, origin).pathname; + const relativePath = currentLocation.startsWith(handlerPath) + ? currentLocation.slice(handlerPath.length).replace(/^\/+/, '') + : currentLocation.replace(/^\/+/, ''); return { path: relativePath, - searchParams: Object.fromEntries(new URLSearchParams(search).entries()) + searchParams: Object.fromEntries(searchParamsSource.entries()) }; - }, [props.location, props.app.urls.handler]); + }, [currentLocation, searchParamsSource, stackApp.urls.handler, origin]); const redirectIfNotHandler = (name: keyof HandlerUrls) => { - const url = props.app.urls[name]; - const handlerUrl = props.app.urls.handler; + const url = stackApp.urls[name]; + const handlerUrl = stackApp.urls.handler; if (url !== handlerUrl && url.startsWith(handlerUrl + "/")) { return; } - const urlObj = new URL(url, window.location.origin); + const urlObj = new URL(url, origin); for (const [key, value] of Object.entries(searchParams)) { urlObj.searchParams.set(key, value); } + // IF_PLATFORM next + redirect(getRelativePart(urlObj), RedirectType.replace); + /* ELSE_IF_PLATFORM react window.location.href = getRelativePart(urlObj); + END_PLATFORM */ }; const result = renderComponent({ @@ -306,34 +260,38 @@ function ReactStackHandler(props: BaseHandlerProp fullPage: props.fullPage, componentProps: props.componentProps, redirectIfNotHandler, - onNotFound: () => ( - props.app.redirectToHome()} - > - The page you are looking for could not be found. Please check the URL and try again. - - ), - app: props.app, + onNotFound: () => + // IF_PLATFORM next + notFound() + /* ELSE_IF_PLATFORM react + ( + stackApp.redirectToHome()} + > + The page you are looking for could not be found. Please check the URL and try again. + + ) + END_PLATFORM */ + , + app: stackApp, }); if (result && 'redirect' in result) { + // IF_PLATFORM next + redirect(result.redirect, RedirectType.replace); + /* ELSE_IF_PLATFORM react window.location.href = result.redirect; return null; + END_PLATFORM */ } return result; } -// END_PLATFORM - -// IF_PLATFORM next -export default NextStackHandler; -/* ELSE_IF_PLATFORM react -export default ReactStackHandler; -END_PLATFORM */ +export default StackHandler; // filter undefined values in object. if object itself is undefined, return undefined function filterUndefinedINU(value: T | undefined): FilterUndefined | undefined { From e0868a694f882cc9f43a0c86d8408af2fd56683e Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Wed, 29 Oct 2025 16:54:29 -0700 Subject: [PATCH 2/8] Add more anonymous users tests --- apps/e2e/tests/js/list-users.test.ts | 50 ++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 apps/e2e/tests/js/list-users.test.ts diff --git a/apps/e2e/tests/js/list-users.test.ts b/apps/e2e/tests/js/list-users.test.ts new file mode 100644 index 0000000000..e0f59850a9 --- /dev/null +++ b/apps/e2e/tests/js/list-users.test.ts @@ -0,0 +1,50 @@ +import { it } from "../helpers"; +import { createApp } from "./js-helpers"; + +it("should list anonymous users when includeAnonymous is true", async ({ expect }) => { + const { serverApp, clientApp } = await createApp(); + + // Create a regular user + const regularUser = await serverApp.createUser({ + primaryEmail: "regular@test.com", + password: "password", + primaryEmailAuthEnabled: true, + }); + + // Create anonymous users + const anonymousUser1 = await clientApp.getUser({ or: "anonymous", tokenStore: { headers: new Headers() } }); + await anonymousUser1.signOut(); + const anonymousUser2 = await clientApp.getUser({ or: "anonymous", tokenStore: { headers: new Headers() } }); + + expect(anonymousUser1.id).not.toBe(anonymousUser2.id); + + // List users without includeAnonymous + const usersWithoutAnonymous = await serverApp.listUsers({ includeAnonymous: false, orderBy: "signedUpAt" }); + const userIdsWithoutAnonymous = usersWithoutAnonymous.map(u => u.id); + expect(userIdsWithoutAnonymous).toEqual([regularUser.id]); + + // List users with includeAnonymous + const usersWithAnonymous = await serverApp.listUsers({ includeAnonymous: true, orderBy: "signedUpAt" }); + const userIdsWithAnonymous = usersWithAnonymous.map(u => u.id); + expect(userIdsWithAnonymous).toEqual([regularUser.id, anonymousUser1.id, anonymousUser2.id]); +}); + +it("should default to excluding anonymous users when includeAnonymous is not specified", async ({ expect }) => { + const { serverApp, clientApp } = await createApp(); + + // Create a regular user + await serverApp.createUser({ + primaryEmail: "regular2@test.com", + password: "password", + primaryEmailAuthEnabled: true, + }); + + // Create an anonymous user + const anonymousUser = await clientApp.getUser({ or: "anonymous" }); + + // List users without specifying includeAnonymous + const users = await serverApp.listUsers(); + + // Verify anonymous user is NOT included by default + expect(users.map(u => u.id)).not.toContain(anonymousUser.id); +}); From cc6a5dd0bf5a9b51cc1945753d7c3275c43f5c1e Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Wed, 29 Oct 2025 17:09:09 -0700 Subject: [PATCH 3/8] fix StackHandler --- apps/backend/.env.development | 1 + .../components-page/stack-handler-client.tsx | 284 +++++++++++++++ .../src/components-page/stack-handler.tsx | 326 ++---------------- 3 files changed, 314 insertions(+), 297 deletions(-) create mode 100644 packages/template/src/components-page/stack-handler-client.tsx diff --git a/apps/backend/.env.development b/apps/backend/.env.development index ccaa92f7e3..9e166a3db4 100644 --- a/apps/backend/.env.development +++ b/apps/backend/.env.development @@ -6,6 +6,7 @@ STACK_SEED_INTERNAL_PROJECT_SIGN_UP_ENABLED=true STACK_SEED_INTERNAL_PROJECT_OTP_ENABLED=true STACK_SEED_INTERNAL_PROJECT_ALLOW_LOCALHOST=true STACK_SEED_INTERNAL_PROJECT_OAUTH_PROVIDERS=github,spotify,google,microsoft +STACK_SEED_INTERNAL_PROJECT_USER_GITHUB_ID=admin@example.com STACK_SEED_INTERNAL_PROJECT_USER_INTERNAL_ACCESS=true STACK_SEED_INTERNAL_PROJECT_PUBLISHABLE_CLIENT_KEY=this-publishable-client-key-is-for-local-development-only STACK_SEED_INTERNAL_PROJECT_SECRET_SERVER_KEY=this-secret-server-key-is-for-local-development-only diff --git a/packages/template/src/components-page/stack-handler-client.tsx b/packages/template/src/components-page/stack-handler-client.tsx new file mode 100644 index 0000000000..bdac769fe4 --- /dev/null +++ b/packages/template/src/components-page/stack-handler-client.tsx @@ -0,0 +1,284 @@ +"use client"; + +import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; +import { FilterUndefined, filterUndefined } from "@stackframe/stack-shared/dist/utils/objects"; +import { getRelativePart } from "@stackframe/stack-shared/dist/utils/urls"; +import { notFound, redirect, RedirectType, usePathname, useSearchParams } from 'next/navigation'; // THIS_LINE_PLATFORM next +import { useMemo } from 'react'; +import { SignIn, SignUp, StackServerApp } from ".."; +import { useStackApp } from "../lib/hooks"; +import { HandlerUrls, StackClientApp } from "../lib/stack-app"; +import { AccountSettings } from "./account-settings"; +import { CliAuthConfirmation } from "./cli-auth-confirm"; +import { EmailVerification } from "./email-verification"; +import { ErrorPage } from "./error-page"; +import { ForgotPassword } from "./forgot-password"; +import { MagicLinkCallback } from "./magic-link-callback"; +import { MFA } from "./mfa"; +import { OAuthCallback } from "./oauth-callback"; +import { PasswordReset } from "./password-reset"; +import { SignOut } from "./sign-out"; +import { TeamInvitation } from "./team-invitation"; + +type Components = { + SignIn: typeof SignIn, + SignUp: typeof SignUp, + EmailVerification: typeof EmailVerification, + PasswordReset: typeof PasswordReset, + ForgotPassword: typeof ForgotPassword, + SignOut: typeof SignOut, + OAuthCallback: typeof OAuthCallback, + MagicLinkCallback: typeof MagicLinkCallback, + TeamInvitation: typeof TeamInvitation, + ErrorPage: typeof ErrorPage, + AccountSettings: typeof AccountSettings, + CliAuthConfirmation: typeof CliAuthConfirmation, + MFA: typeof MFA, +}; + +type RouteProps = { + params: Promise<{ stack?: string[] }> | { stack?: string[] }, + searchParams: Promise> | Record, +}; + +const availablePaths = { + signIn: 'sign-in', + signUp: 'sign-up', + emailVerification: 'email-verification', + passwordReset: 'password-reset', + forgotPassword: 'forgot-password', + signOut: 'sign-out', + oauthCallback: 'oauth-callback', + magicLinkCallback: 'magic-link-callback', + teamInvitation: 'team-invitation', + accountSettings: 'account-settings', + cliAuthConfirm: 'cli-auth-confirm', + mfa: 'mfa', + error: 'error', +} as const; + +const pathAliases = { + // also includes the uppercase and non-dashed versions + ...Object.fromEntries(Object.entries(availablePaths).map(([key, value]) => [value, value])), + "log-in": availablePaths.signIn, + "register": availablePaths.signUp, +} as const; + +export type BaseHandlerProps = { + fullPage: boolean, + componentProps?: { + [K in keyof Components]?: Parameters[0]; + }, +}; + +function renderComponent(props: { + path: string, + searchParams: Record, + fullPage: boolean, + componentProps?: BaseHandlerProps['componentProps'], + redirectIfNotHandler?: (name: keyof HandlerUrls) => void, + onNotFound: () => any, + app: StackClientApp | StackServerApp, +}) { + const { path, searchParams, fullPage, componentProps, redirectIfNotHandler, onNotFound, app } = props; + + switch (path) { + case availablePaths.signIn: { + redirectIfNotHandler?.('signIn'); + return ; + } + case availablePaths.signUp: { + redirectIfNotHandler?.('signUp'); + return ; + } + case availablePaths.emailVerification: { + redirectIfNotHandler?.('emailVerification'); + return ; + } + case availablePaths.passwordReset: { + redirectIfNotHandler?.('passwordReset'); + return ; + } + case availablePaths.forgotPassword: { + redirectIfNotHandler?.('forgotPassword'); + return ; + } + case availablePaths.signOut: { + redirectIfNotHandler?.('signOut'); + return ; + } + case availablePaths.oauthCallback: { + redirectIfNotHandler?.('oauthCallback'); + return ; + } + case availablePaths.magicLinkCallback: { + redirectIfNotHandler?.('magicLinkCallback'); + return ; + } + case availablePaths.teamInvitation: { + redirectIfNotHandler?.('teamInvitation'); + return ; + } + case availablePaths.accountSettings: { + return ; + } + case availablePaths.error: { + return ; + } + case availablePaths.cliAuthConfirm: { + return ; + } + case availablePaths.mfa: { + redirectIfNotHandler?.('mfa'); + return ; + } + default: { + if (Object.values(availablePaths).includes(path as any)) { + throw new StackAssertionError(`Path alias ${path} not included in switch statement, but in availablePaths?`, { availablePaths }); + } + for (const [key, value] of Object.entries(pathAliases)) { + if (path === key.toLowerCase().replaceAll('-', '')) { + const redirectUrl = `${app.urls.handler}/${value}?${new URLSearchParams(searchParams).toString()}`; + return { redirect: redirectUrl }; + } + } + return onNotFound(); + } + } +} + +export function StackHandlerClient(props: BaseHandlerProps & Partial & { location?: string }) { + // Use hooks to get app + const stackApp = useStackApp(); + + // IF_PLATFORM next + const pathname = usePathname(); + const searchParamsFromHook = useSearchParams(); + const currentLocation = pathname; + const searchParamsSource = searchParamsFromHook; + const origin = 'http://example.com'; + /* ELSE_IF_PLATFORM react + const currentLocation = props.location ?? window.location.pathname; + const searchParamsSource = new URLSearchParams(window.location.search); + const origin = window.location.origin; + END_PLATFORM */ + + const { path, searchParams } = useMemo(() => { + const handlerPath = new URL(stackApp.urls.handler, origin).pathname; + const relativePath = currentLocation.startsWith(handlerPath) + ? currentLocation.slice(handlerPath.length).replace(/^\/+/, '') + : currentLocation.replace(/^\/+/, ''); + + return { + path: relativePath, + searchParams: Object.fromEntries(searchParamsSource.entries()) + }; + }, [currentLocation, searchParamsSource, stackApp.urls.handler, origin]); + + const redirectIfNotHandler = (name: keyof HandlerUrls) => { + const url = stackApp.urls[name]; + const handlerUrl = stackApp.urls.handler; + + if (url !== handlerUrl && url.startsWith(handlerUrl + "/")) { + return; + } + + const urlObj = new URL(url, origin); + for (const [key, value] of Object.entries(searchParams)) { + urlObj.searchParams.set(key, value); + } + + // IF_PLATFORM next + redirect(getRelativePart(urlObj), RedirectType.replace); + /* ELSE_IF_PLATFORM react + window.location.href = getRelativePart(urlObj); + END_PLATFORM */ + }; + + const result = renderComponent({ + path, + searchParams, + fullPage: props.fullPage, + componentProps: props.componentProps, + redirectIfNotHandler, + onNotFound: () => + // IF_PLATFORM next + notFound() + /* ELSE_IF_PLATFORM react + ( + stackApp.redirectToHome()} + > + The page you are looking for could not be found. Please check the URL and try again. + + ) + END_PLATFORM */ + , + app: stackApp, + }); + + if (result && 'redirect' in result) { + // IF_PLATFORM next + redirect(result.redirect, RedirectType.replace); + /* ELSE_IF_PLATFORM react + window.location.href = result.redirect; + return null; + END_PLATFORM */ + } + + return result; +} + +// filter undefined values in object. if object itself is undefined, return undefined +function filterUndefinedINU(value: T | undefined): FilterUndefined | undefined { + return value === undefined ? value : filterUndefined(value); +} diff --git a/packages/template/src/components-page/stack-handler.tsx b/packages/template/src/components-page/stack-handler.tsx index 9904a6b78c..3e4bde3dc5 100644 --- a/packages/template/src/components-page/stack-handler.tsx +++ b/packages/template/src/components-page/stack-handler.tsx @@ -1,299 +1,31 @@ -"use client"; // THIS_LINE_PLATFORM next -import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors"; -import { FilterUndefined, filterUndefined } from "@stackframe/stack-shared/dist/utils/objects"; -import { getRelativePart } from "@stackframe/stack-shared/dist/utils/urls"; -import { notFound, redirect, RedirectType, usePathname, useSearchParams } from 'next/navigation'; // THIS_LINE_PLATFORM next -import { useMemo } from 'react'; -import { SignIn, SignUp, StackServerApp } from ".."; -import { useStackApp } from "../lib/hooks"; -import { HandlerUrls, StackClientApp } from "../lib/stack-app"; -import { AccountSettings } from "./account-settings"; -import { CliAuthConfirmation } from "./cli-auth-confirm"; -import { EmailVerification } from "./email-verification"; -import { ErrorPage } from "./error-page"; -import { ForgotPassword } from "./forgot-password"; -import { MagicLinkCallback } from "./magic-link-callback"; -import { MFA } from "./mfa"; -import { OAuthCallback } from "./oauth-callback"; -import { PasswordReset } from "./password-reset"; -import { SignOut } from "./sign-out"; -import { TeamInvitation } from "./team-invitation"; - -type Components = { - SignIn: typeof SignIn, - SignUp: typeof SignUp, - EmailVerification: typeof EmailVerification, - PasswordReset: typeof PasswordReset, - ForgotPassword: typeof ForgotPassword, - SignOut: typeof SignOut, - OAuthCallback: typeof OAuthCallback, - MagicLinkCallback: typeof MagicLinkCallback, - TeamInvitation: typeof TeamInvitation, - ErrorPage: typeof ErrorPage, - AccountSettings: typeof AccountSettings, - CliAuthConfirmation: typeof CliAuthConfirmation, - MFA: typeof MFA, -}; - -type RouteProps = { - params: Promise<{ stack?: string[] }> | { stack?: string[] }, - searchParams: Promise> | Record, -}; - -/** - * @deprecated The app parameter is no longer necessary. You can safely remove it. - */ -type DeprecatedAppProp = { - app?: StackClientApp | StackServerApp, -}; - -/** - * @deprecated The routeProps parameter is no longer necessary. You can safely remove it. - */ -type DeprecatedRouteProps = { - routeProps?: RouteProps | unknown, -}; - -const availablePaths = { - signIn: 'sign-in', - signUp: 'sign-up', - emailVerification: 'email-verification', - passwordReset: 'password-reset', - forgotPassword: 'forgot-password', - signOut: 'sign-out', - oauthCallback: 'oauth-callback', - magicLinkCallback: 'magic-link-callback', - teamInvitation: 'team-invitation', - accountSettings: 'account-settings', - cliAuthConfirm: 'cli-auth-confirm', - mfa: 'mfa', - error: 'error', -} as const; - -const pathAliases = { - // also includes the uppercase and non-dashed versions - ...Object.fromEntries(Object.entries(availablePaths).map(([key, value]) => [value, value])), - "log-in": availablePaths.signIn, - "register": availablePaths.signUp, -} as const; - -type BaseHandlerProps = { - fullPage: boolean, - componentProps?: { - [K in keyof Components]?: Parameters[0]; - }, -}; - -function renderComponent(props: { - path: string, - searchParams: Record, - fullPage: boolean, - componentProps?: BaseHandlerProps['componentProps'], - redirectIfNotHandler?: (name: keyof HandlerUrls) => void, - onNotFound: () => any, - app: StackClientApp | StackServerApp, +// This file exists solely so the following old, deprecated code from when StackHandler used to still take props: +// +// does not throw the following error: +// Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported. +// This file exists as a component that can be both client and server, ignores its parameters, and returns + +import { BaseHandlerProps, StackHandlerClient } from "./stack-handler-client"; + +export default function StackHandler({ app, routeProps, params, searchParams, ...props }: BaseHandlerProps & { location?: string } & { + /** + * @deprecated The app parameter is no longer necessary. You can safely remove it. + */ + app?: any, + + /** + * @deprecated The routeProps parameter is no longer necessary. You can safely remove it. + */ + routeProps?: any, + + /** + * @deprecated The params parameter is no longer necessary. You can safely remove it. + */ + params?: any, + + /** + * @deprecated The searchParams parameter is no longer necessary. You can safely remove it. + */ + searchParams?: any, }) { - const { path, searchParams, fullPage, componentProps, redirectIfNotHandler, onNotFound, app } = props; - - switch (path) { - case availablePaths.signIn: { - redirectIfNotHandler?.('signIn'); - return ; - } - case availablePaths.signUp: { - redirectIfNotHandler?.('signUp'); - return ; - } - case availablePaths.emailVerification: { - redirectIfNotHandler?.('emailVerification'); - return ; - } - case availablePaths.passwordReset: { - redirectIfNotHandler?.('passwordReset'); - return ; - } - case availablePaths.forgotPassword: { - redirectIfNotHandler?.('forgotPassword'); - return ; - } - case availablePaths.signOut: { - redirectIfNotHandler?.('signOut'); - return ; - } - case availablePaths.oauthCallback: { - redirectIfNotHandler?.('oauthCallback'); - return ; - } - case availablePaths.magicLinkCallback: { - redirectIfNotHandler?.('magicLinkCallback'); - return ; - } - case availablePaths.teamInvitation: { - redirectIfNotHandler?.('teamInvitation'); - return ; - } - case availablePaths.accountSettings: { - return ; - } - case availablePaths.error: { - return ; - } - case availablePaths.cliAuthConfirm: { - return ; - } - case availablePaths.mfa: { - redirectIfNotHandler?.('mfa'); - return ; - } - default: { - if (Object.values(availablePaths).includes(path as any)) { - throw new StackAssertionError(`Path alias ${path} not included in switch statement, but in availablePaths?`, { availablePaths }); - } - for (const [key, value] of Object.entries(pathAliases)) { - if (path === key.toLowerCase().replaceAll('-', '')) { - const redirectUrl = `${app.urls.handler}/${value}?${new URLSearchParams(searchParams).toString()}`; - return { redirect: redirectUrl }; - } - } - return onNotFound(); - } - } -} - -function StackHandler(props: BaseHandlerProps & Partial & Partial & Partial & { location?: string }) { - // Use hooks to get app - const stackApp = props.app ?? useStackApp(); - - // IF_PLATFORM next - const pathname = usePathname(); - const searchParamsFromHook = useSearchParams(); - const currentLocation = pathname; - const searchParamsSource = searchParamsFromHook; - const origin = 'http://example.com'; - /* ELSE_IF_PLATFORM react - const currentLocation = props.location ?? window.location.pathname; - const searchParamsSource = new URLSearchParams(window.location.search); - const origin = window.location.origin; - END_PLATFORM */ - - const { path, searchParams } = useMemo(() => { - const handlerPath = new URL(stackApp.urls.handler, origin).pathname; - const relativePath = currentLocation.startsWith(handlerPath) - ? currentLocation.slice(handlerPath.length).replace(/^\/+/, '') - : currentLocation.replace(/^\/+/, ''); - - return { - path: relativePath, - searchParams: Object.fromEntries(searchParamsSource.entries()) - }; - }, [currentLocation, searchParamsSource, stackApp.urls.handler, origin]); - - const redirectIfNotHandler = (name: keyof HandlerUrls) => { - const url = stackApp.urls[name]; - const handlerUrl = stackApp.urls.handler; - - if (url !== handlerUrl && url.startsWith(handlerUrl + "/")) { - return; - } - - const urlObj = new URL(url, origin); - for (const [key, value] of Object.entries(searchParams)) { - urlObj.searchParams.set(key, value); - } - - // IF_PLATFORM next - redirect(getRelativePart(urlObj), RedirectType.replace); - /* ELSE_IF_PLATFORM react - window.location.href = getRelativePart(urlObj); - END_PLATFORM */ - }; - - const result = renderComponent({ - path, - searchParams, - fullPage: props.fullPage, - componentProps: props.componentProps, - redirectIfNotHandler, - onNotFound: () => - // IF_PLATFORM next - notFound() - /* ELSE_IF_PLATFORM react - ( - stackApp.redirectToHome()} - > - The page you are looking for could not be found. Please check the URL and try again. - - ) - END_PLATFORM */ - , - app: stackApp, - }); - - if (result && 'redirect' in result) { - // IF_PLATFORM next - redirect(result.redirect, RedirectType.replace); - /* ELSE_IF_PLATFORM react - window.location.href = result.redirect; - return null; - END_PLATFORM */ - } - - return result; -} - -export default StackHandler; - -// filter undefined values in object. if object itself is undefined, return undefined -function filterUndefinedINU(value: T | undefined): FilterUndefined | undefined { - return value === undefined ? value : filterUndefined(value); + return ; } From 61210c497461db301eabbb19bd7ee5c9e525f077 Mon Sep 17 00:00:00 2001 From: Konsti Wohlwend Date: Thu, 6 Nov 2025 00:39:04 -0800 Subject: [PATCH 4/8] Apply suggestion from @vercel[bot] Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> --- packages/template/src/components-page/stack-handler-client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/template/src/components-page/stack-handler-client.tsx b/packages/template/src/components-page/stack-handler-client.tsx index bdac769fe4..acb00ee7fe 100644 --- a/packages/template/src/components-page/stack-handler-client.tsx +++ b/packages/template/src/components-page/stack-handler-client.tsx @@ -183,7 +183,7 @@ function renderComponent(props: { throw new StackAssertionError(`Path alias ${path} not included in switch statement, but in availablePaths?`, { availablePaths }); } for (const [key, value] of Object.entries(pathAliases)) { - if (path === key.toLowerCase().replaceAll('-', '')) { + if (path.toLowerCase().replaceAll('-', '') === key.toLowerCase().replaceAll('-', '')) { const redirectUrl = `${app.urls.handler}/${value}?${new URLSearchParams(searchParams).toString()}`; return { redirect: redirectUrl }; } From 5c95e8b1dbafb3178435f198b55ed3dda8dbeb03 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Thu, 6 Nov 2025 10:05:27 -0800 Subject: [PATCH 5/8] fixes --- packages/init-stack/src/index.ts | 4 ++-- .../src/components-page/stack-handler-client.tsx | 12 +++++++----- .../template/src/components-page/stack-handler.tsx | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/init-stack/src/index.ts b/packages/init-stack/src/index.ts index d7be8ee050..6cb43c1459 100644 --- a/packages/init-stack/src/index.ts +++ b/packages/init-stack/src/index.ts @@ -8,8 +8,8 @@ import * as os from 'os'; import * as path from "path"; import { PostHog } from 'posthog-node'; import packageJson from '../package.json'; -import { invokeCallback } from "./telegram"; import { scheduleMcpConfiguration } from "./mcp"; +import { invokeCallback } from "./telegram"; import { Colorize, configureVerboseLogging, logVerbose, templateIdentity } from "./util"; export { templateIdentity } from "./util"; @@ -981,7 +981,7 @@ ${shouldInheritFromClient ? `${indentation}inheritsFrom: stackClientApp,` : `${i } laterWriteFileIfNotExists( handlerPath, - `import { StackHandler } from "@stackframe/stack"; \n\nexport default function Handler() { \n${projectInfo.indentation}return ; \n} \n` + `import { StackHandler } from "@stackframe/stack";\n\nexport default function Handler() {\n${projectInfo.indentation}return ;\n}\n` ); }, diff --git a/packages/template/src/components-page/stack-handler-client.tsx b/packages/template/src/components-page/stack-handler-client.tsx index acb00ee7fe..44ab35511d 100644 --- a/packages/template/src/components-page/stack-handler-client.tsx +++ b/packages/template/src/components-page/stack-handler-client.tsx @@ -20,6 +20,10 @@ import { PasswordReset } from "./password-reset"; import { SignOut } from "./sign-out"; import { TeamInvitation } from "./team-invitation"; +/* IF_PLATFORM react +import { MessageCard } from "@stackframe/stack-ui"; +// END_PLATFORM react */ + type Components = { SignIn: typeof SignIn, SignUp: typeof SignUp, @@ -202,15 +206,13 @@ export function StackHandlerClient(props: BaseHandlerProps & Partial const searchParamsFromHook = useSearchParams(); const currentLocation = pathname; const searchParamsSource = searchParamsFromHook; - const origin = 'http://example.com'; /* ELSE_IF_PLATFORM react const currentLocation = props.location ?? window.location.pathname; const searchParamsSource = new URLSearchParams(window.location.search); - const origin = window.location.origin; END_PLATFORM */ const { path, searchParams } = useMemo(() => { - const handlerPath = new URL(stackApp.urls.handler, origin).pathname; + const handlerPath = new URL(stackApp.urls.handler, 'http://example.com').pathname; const relativePath = currentLocation.startsWith(handlerPath) ? currentLocation.slice(handlerPath.length).replace(/^\/+/, '') : currentLocation.replace(/^\/+/, ''); @@ -219,7 +221,7 @@ export function StackHandlerClient(props: BaseHandlerProps & Partial path: relativePath, searchParams: Object.fromEntries(searchParamsSource.entries()) }; - }, [currentLocation, searchParamsSource, stackApp.urls.handler, origin]); + }, [currentLocation, searchParamsSource, stackApp.urls.handler]); const redirectIfNotHandler = (name: keyof HandlerUrls) => { const url = stackApp.urls[name]; @@ -229,7 +231,7 @@ export function StackHandlerClient(props: BaseHandlerProps & Partial return; } - const urlObj = new URL(url, origin); + const urlObj = new URL(url, 'http://example.com'); for (const [key, value] of Object.entries(searchParams)) { urlObj.searchParams.set(key, value); } diff --git a/packages/template/src/components-page/stack-handler.tsx b/packages/template/src/components-page/stack-handler.tsx index 3e4bde3dc5..9e9eed30ed 100644 --- a/packages/template/src/components-page/stack-handler.tsx +++ b/packages/template/src/components-page/stack-handler.tsx @@ -2,7 +2,7 @@ // // does not throw the following error: // Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported. -// This file exists as a component that can be both client and server, ignores its parameters, and returns +// This file exists as a component that can be both client and server, ignores its parameters, and returns import { BaseHandlerProps, StackHandlerClient } from "./stack-handler-client"; From f339b65dbc1ea8f521bfb845b04dba9b5d3b36b3 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Thu, 6 Nov 2025 10:21:24 -0800 Subject: [PATCH 6/8] Fix build --- packages/template/src/components-page/stack-handler-client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/template/src/components-page/stack-handler-client.tsx b/packages/template/src/components-page/stack-handler-client.tsx index 44ab35511d..aa02321209 100644 --- a/packages/template/src/components-page/stack-handler-client.tsx +++ b/packages/template/src/components-page/stack-handler-client.tsx @@ -21,7 +21,7 @@ import { SignOut } from "./sign-out"; import { TeamInvitation } from "./team-invitation"; /* IF_PLATFORM react -import { MessageCard } from "@stackframe/stack-ui"; +import { MessageCard } from "./message-card"; // END_PLATFORM react */ type Components = { From 3756863d32fcad7a6f51e6e7b633c1b4ed1244b7 Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Thu, 6 Nov 2025 10:33:06 -0800 Subject: [PATCH 7/8] fixes --- packages/template/src/components-page/stack-handler-client.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/template/src/components-page/stack-handler-client.tsx b/packages/template/src/components-page/stack-handler-client.tsx index aa02321209..0792d2cb13 100644 --- a/packages/template/src/components-page/stack-handler-client.tsx +++ b/packages/template/src/components-page/stack-handler-client.tsx @@ -21,7 +21,7 @@ import { SignOut } from "./sign-out"; import { TeamInvitation } from "./team-invitation"; /* IF_PLATFORM react -import { MessageCard } from "./message-card"; +import { MessageCard } from "../components/message-cards/message-card"; // END_PLATFORM react */ type Components = { From 75e07c26bb3273e752e4f48b3a4db780f113a68f Mon Sep 17 00:00:00 2001 From: Konstantin Wohlwend Date: Thu, 6 Nov 2025 10:33:38 -0800 Subject: [PATCH 8/8] fix --- apps/dashboard/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dashboard/tsconfig.json b/apps/dashboard/tsconfig.json index fb9c090878..5d05b2a1e7 100644 --- a/apps/dashboard/tsconfig.json +++ b/apps/dashboard/tsconfig.json @@ -14,7 +14,7 @@ "moduleResolution": "bundler", "resolveJsonModule": true, "isolatedModules": true, - "jsx": "preserve", + "jsx": "react-jsx", "incremental": true, "noErrorTruncation": true, "plugins": [