From c9f3d62afd3e85d4bfa3cf6d41115d107d28f084 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Thu, 4 Dec 2025 20:51:00 -0600
Subject: [PATCH 01/17] feat(repo): Protect -> Show
---
.../expo/src/components/controlComponents.tsx | 2 +-
.../app-router/server/controlComponents.tsx | 9 +-
.../src/client-boundary/controlComponents.ts | 16 +-
packages/nextjs/src/components.client.ts | 21 +-
packages/nextjs/src/index.ts | 7 +-
.../src/components/controlComponents.tsx | 80 ++---
packages/react/src/components/index.ts | 4 +-
packages/shared/src/types/protect.ts | 37 ++
.../transform-protect-to-show.fixtures.js | 329 ++++++++++++++++++
.../transform-protect-to-show.test.js | 18 +
.../codemods/transform-protect-to-show.cjs | 197 +++++++++++
11 files changed, 658 insertions(+), 62 deletions(-)
create mode 100644 packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
create mode 100644 packages/upgrade/src/codemods/__tests__/transform-protect-to-show.test.js
create mode 100644 packages/upgrade/src/codemods/transform-protect-to-show.cjs
diff --git a/packages/expo/src/components/controlComponents.tsx b/packages/expo/src/components/controlComponents.tsx
index bc42b9dbc73..33edad58240 100644
--- a/packages/expo/src/components/controlComponents.tsx
+++ b/packages/expo/src/components/controlComponents.tsx
@@ -1 +1 @@
-export { ClerkLoaded, ClerkLoading, SignedIn, SignedOut, Protect } from '@clerk/react';
+export { ClerkLoaded, ClerkLoading, Show, SignedIn, SignedOut } from '@clerk/react';
diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx
index d640c63a055..60039b33752 100644
--- a/packages/nextjs/src/app-router/server/controlComponents.tsx
+++ b/packages/nextjs/src/app-router/server/controlComponents.tsx
@@ -1,9 +1,14 @@
-import type { ProtectProps } from '@clerk/react';
-import type { PendingSessionOptions } from '@clerk/shared/types';
+import type { PendingSessionOptions, ProtectProps as _ProtectProps } from '@clerk/shared/types';
import React from 'react';
import { auth } from './auth';
+type ProtectProps = React.PropsWithChildren<
+ _ProtectProps & {
+ fallback?: React.ReactNode;
+ } & PendingSessionOptions
+>;
+
export async function SignedIn(
props: React.PropsWithChildren,
): Promise {
diff --git a/packages/nextjs/src/client-boundary/controlComponents.ts b/packages/nextjs/src/client-boundary/controlComponents.ts
index 1ab240a18f5..9006fbc594e 100644
--- a/packages/nextjs/src/client-boundary/controlComponents.ts
+++ b/packages/nextjs/src/client-boundary/controlComponents.ts
@@ -1,20 +1,20 @@
'use client';
export {
- ClerkLoaded,
- ClerkLoading,
+ AuthenticateWithRedirectCallback,
ClerkDegraded,
ClerkFailed,
- SignedOut,
- SignedIn,
- Protect,
+ ClerkLoaded,
+ ClerkLoading,
+ RedirectToCreateOrganization,
+ RedirectToOrganizationProfile,
RedirectToSignIn,
RedirectToSignUp,
RedirectToTasks,
RedirectToUserProfile,
- AuthenticateWithRedirectCallback,
- RedirectToCreateOrganization,
- RedirectToOrganizationProfile,
+ Show,
+ SignedIn,
+ SignedOut,
} from '@clerk/react';
export { MultisessionAppSupport } from '@clerk/react/internal';
diff --git a/packages/nextjs/src/components.client.ts b/packages/nextjs/src/components.client.ts
index aac3f82f65b..1d6fd04d0e6 100644
--- a/packages/nextjs/src/components.client.ts
+++ b/packages/nextjs/src/components.client.ts
@@ -1,2 +1,21 @@
export { ClerkProvider } from './client-boundary/ClerkProvider';
-export { SignedIn, SignedOut, Protect } from './client-boundary/controlComponents';
+export { Show, SignedIn, SignedOut } from './client-boundary/controlComponents';
+
+/**
+ * `` is only available as a React Server Component in the App Router.
+ * For client-side conditional rendering, use `` instead.
+ *
+ * @example
+ * ```tsx
+ * // Server Component (App Router)
+ * ...
+ *
+ * // Client Component
+ * ...
+ * ```
+ */
+export const Protect = () => {
+ throw new Error(
+ '`` is only available as a React Server Component. For client components, use `` instead.',
+ );
+};
diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts
index 2e29bcd7568..bc727044900 100644
--- a/packages/nextjs/src/index.ts
+++ b/packages/nextjs/src/index.ts
@@ -14,6 +14,7 @@ export {
RedirectToSignUp,
RedirectToTasks,
RedirectToUserProfile,
+ Show,
} from './client-boundary/controlComponents';
/**
@@ -72,6 +73,10 @@ import * as ComponentsModule from '#components';
import type { ServerComponentsServerModuleTypes } from './components.server';
export const ClerkProvider = ComponentsModule.ClerkProvider as ServerComponentsServerModuleTypes['ClerkProvider'];
+/**
+ * Use `` in RSC (App Router) to restrict access based on authentication and authorization.
+ * For client components, use `` instead.
+ */
+export const Protect = ComponentsModule.Protect as ServerComponentsServerModuleTypes['Protect'];
export const SignedIn = ComponentsModule.SignedIn as ServerComponentsServerModuleTypes['SignedIn'];
export const SignedOut = ComponentsModule.SignedOut as ServerComponentsServerModuleTypes['SignedOut'];
-export const Protect = ComponentsModule.Protect as ServerComponentsServerModuleTypes['Protect'];
diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx
index bdeefbfa05a..9dcd97956e3 100644
--- a/packages/react/src/components/controlComponents.tsx
+++ b/packages/react/src/components/controlComponents.tsx
@@ -2,7 +2,8 @@ import { deprecated } from '@clerk/shared/deprecated';
import type {
HandleOAuthCallbackParams,
PendingSessionOptions,
- ProtectProps as _ProtectProps,
+ ShowProps as _ShowProps,
+ ShowWhenCondition,
} from '@clerk/shared/types';
import React from 'react';
@@ -73,76 +74,61 @@ export const ClerkDegraded = ({ children }: React.PropsWithChildren) =>
return children;
};
-export type ProtectProps = React.PropsWithChildren<
- _ProtectProps & {
+export type ShowProps = React.PropsWithChildren<
+ _ShowProps & {
fallback?: React.ReactNode;
} & PendingSessionOptions
>;
/**
- * Use `` in order to prevent unauthenticated or unauthorized users from accessing the children passed to the component.
+ * Use `` to conditionally render content based on user authorization.
*
- * Examples:
- * ```
- *
- *
- * has({permission:"a_permission_key"})} />
- * has({role:"a_role_key"})} />
- * Unauthorized
} />
+ * @example
+ * ```tsx
+ * Unauthorized}>
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * has({ permission: "org:read" }) && isFeatureEnabled}>
+ *
+ *
* ```
*/
-export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAuthorizedParams }: ProtectProps) => {
- useAssertWrappedByClerkProvider('Protect');
+export const Show = ({ children, fallback, treatPendingAsSignedOut, when }: ShowProps) => {
+ useAssertWrappedByClerkProvider('Show');
- const { isLoaded, has, userId } = useAuth({ treatPendingAsSignedOut });
+ const { has, isLoaded, userId } = useAuth({ treatPendingAsSignedOut });
- /**
- * Avoid flickering children or fallback while clerk is loading sessionId or userId
- */
if (!isLoaded) {
return null;
}
- /**
- * Fallback to UI provided by user or `null` if authorization checks failed
- */
- const unauthorized = fallback ?? null;
-
const authorized = children;
+ const unauthorized = fallback ?? null;
if (!userId) {
return unauthorized;
}
- /**
- * Check against the results of `has` called inside the callback
- */
- if (typeof restAuthorizedParams.condition === 'function') {
- if (restAuthorizedParams.condition(has)) {
- return authorized;
- }
- return unauthorized;
- }
-
- if (
- restAuthorizedParams.role ||
- restAuthorizedParams.permission ||
- restAuthorizedParams.feature ||
- restAuthorizedParams.plan
- ) {
- if (has(restAuthorizedParams)) {
- return authorized;
- }
- return unauthorized;
+ // At this point, userId is defined so has() is guaranteed to be available
+ if (checkAuthorization(when, has!)) {
+ return authorized;
}
- /**
- * If neither of the authorization params are passed behave as the ``.
- * If fallback is present render that instead of rendering nothing.
- */
- return authorized;
+ return unauthorized;
};
+function checkAuthorization(when: ShowWhenCondition, has: NonNullable['has']>): boolean {
+ if (typeof when === 'function') {
+ return when(has);
+ }
+ return has(when);
+}
+
export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => {
const { client, session } = clerk;
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index cbf9b77aba1..23523b1c16f 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -21,18 +21,18 @@ export {
ClerkFailed,
ClerkLoaded,
ClerkLoading,
- Protect,
RedirectToCreateOrganization,
RedirectToOrganizationProfile,
RedirectToSignIn,
RedirectToSignUp,
RedirectToTasks,
RedirectToUserProfile,
+ Show,
SignedIn,
SignedOut,
} from './controlComponents';
-export type { ProtectProps } from './controlComponents';
+export type { ShowProps } from './controlComponents';
export { SignInButton } from './SignInButton';
export { SignInWithMetamaskButton } from './SignInWithMetamaskButton';
diff --git a/packages/shared/src/types/protect.ts b/packages/shared/src/types/protect.ts
index e96df803046..70bc9118295 100644
--- a/packages/shared/src/types/protect.ts
+++ b/packages/shared/src/types/protect.ts
@@ -68,3 +68,40 @@ export type ProtectProps =
feature?: never;
plan?: never;
};
+
+/**
+ * Authorization condition for the `when` prop in ``.
+ * Can be an object specifying role, permission, feature, or plan,
+ * or a callback function receiving the `has` helper for complex conditions.
+ */
+export type ShowWhenCondition =
+ | { role: OrganizationCustomRoleKey }
+ | { permission: OrganizationCustomPermissionKey }
+ | { feature: Autocomplete<`org:${string}` | `user:${string}`> }
+ | { plan: Autocomplete<`org:${string}` | `user:${string}`> }
+ | ((has: CheckAuthorizationWithCustomPermissions) => boolean);
+
+/**
+ * Props for the `` component, which conditionally renders children based on authorization.
+ *
+ * @example
+ * ```tsx
+ * // Require a specific permission
+ * ...
+ *
+ * // Require a specific role
+ * ...
+ *
+ * // Use a custom condition callback
+ * has({ permission: "org:read" }) && someCondition}>...
+ *
+ * // Require a specific feature
+ * ...
+ *
+ * // Require a specific plan
+ * ...
+ * ```
+ */
+export type ShowProps = {
+ when: ShowWhenCondition;
+};
diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
new file mode 100644
index 00000000000..e91211cd7b6
--- /dev/null
+++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
@@ -0,0 +1,329 @@
+export const fixtures = [
+ {
+ name: 'Basic import transform',
+ source: `
+import { Protect } from "@clerk/react"
+ `,
+ output: `
+import { Show } from "@clerk/react"
+`,
+ },
+ {
+ name: 'Import transform with other imports',
+ source: `
+import { ClerkProvider, Protect, SignedIn } from "@clerk/react"
+ `,
+ output: `
+import { ClerkProvider, Show, SignedIn } from "@clerk/react"
+`,
+ },
+ {
+ name: 'Import from @clerk/nextjs without use client - should NOT transform (RSC)',
+ source: `
+import { Protect } from "@clerk/nextjs"
+ `,
+ output: null,
+ },
+ {
+ name: 'Basic permission prop transform',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ )
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ );
+}
+`,
+ },
+ {
+ name: 'Basic role prop transform',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ )
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ );
+}
+`,
+ },
+ {
+ name: 'Feature prop transform',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ )
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ );
+}
+`,
+ },
+ {
+ name: 'Plan prop transform',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ )
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ );
+}
+`,
+ },
+ {
+ name: 'Condition prop transform',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App() {
+ return (
+ has({ permission: "org:read" })}>
+
+
+ )
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App() {
+ return (
+ has({ permission: "org:read" })}>
+
+
+ );
+}
+`,
+ },
+ {
+ name: 'With fallback prop',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App() {
+ return (
+ }>
+
+
+ )
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App() {
+ return (
+ }>
+
+
+ );
+}
+`,
+ },
+ {
+ name: 'Self-closing Protect',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App() {
+ return
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App() {
+ return (
+
+ );
+}
+`,
+ },
+ {
+ name: 'Handles directives',
+ source: `"use client";
+
+import { Protect } from "@clerk/nextjs";
+
+export function Protected() {
+ return (
+
+
+
+ );
+}
+`,
+ output: `"use client";
+
+import { Show } from "@clerk/nextjs";
+
+export function Protected() {
+ return (
+
+
+
+ );
+}`,
+ },
+ {
+ name: 'Dynamic permission value',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App({ requiredPermission }) {
+ return (
+
+
+
+ )
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App({ requiredPermission }) {
+ return (
+
+
+
+ );
+}
+`,
+ },
+ {
+ name: 'RSC file (no use client) from @clerk/nextjs - should NOT transform',
+ source: `import { Protect } from "@clerk/nextjs";
+
+export default async function Page() {
+ return (
+
+
+
+ );
+}
+`,
+ output: null,
+ },
+ {
+ name: 'Client file (use client) from @clerk/nextjs - should transform',
+ source: `"use client";
+
+import { Protect } from "@clerk/nextjs";
+
+export function ClientComponent() {
+ return (
+
+
+
+ );
+}
+`,
+ output: `"use client";
+
+import { Show } from "@clerk/nextjs";
+
+export function ClientComponent() {
+ return (
+
+
+
+ );
+}`,
+ },
+ {
+ name: 'Client-only package (@clerk/react) without use client - should still transform',
+ source: `import { Protect } from "@clerk/react";
+
+function Component() {
+ return (
+
+
+
+ );
+}
+`,
+ output: `import { Show } from "@clerk/react";
+
+function Component() {
+ return (
+
+
+
+ );
+}`,
+ },
+];
diff --git a/packages/upgrade/src/codemods/__tests__/transform-protect-to-show.test.js b/packages/upgrade/src/codemods/__tests__/transform-protect-to-show.test.js
new file mode 100644
index 00000000000..435c84b524d
--- /dev/null
+++ b/packages/upgrade/src/codemods/__tests__/transform-protect-to-show.test.js
@@ -0,0 +1,18 @@
+import { applyTransform } from 'jscodeshift/dist/testUtils';
+import { describe, expect, it } from 'vitest';
+
+import transformer from '../transform-protect-to-show.cjs';
+import { fixtures } from './__fixtures__/transform-protect-to-show.fixtures';
+
+describe('transform-protect-to-show', () => {
+ it.each(fixtures)(`$name`, ({ source, output }) => {
+ const result = applyTransform(transformer, {}, { source });
+
+ if (output === null) {
+ // null output means no transformation should occur
+ expect(result).toBeFalsy();
+ } else {
+ expect(result).toEqual(output.trim());
+ }
+ });
+});
diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
new file mode 100644
index 00000000000..bc20b06ab2e
--- /dev/null
+++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
@@ -0,0 +1,197 @@
+// Packages that are always client-side
+const CLIENT_ONLY_PACKAGES = ['@clerk/react', '@clerk/expo'];
+// Packages that can be used in both RSC and client components
+const HYBRID_PACKAGES = ['@clerk/nextjs'];
+
+/**
+ * Checks if a file has a 'use client' directive at the top.
+ */
+function hasUseClientDirective(root, j) {
+ const program = root.find(j.Program).get();
+ const body = program.node.body;
+
+ if (body.length === 0) {
+ return false;
+ }
+
+ const firstStatement = body[0];
+
+ // Check for 'use client' as an expression statement with a string literal
+ if (j.ExpressionStatement.check(firstStatement)) {
+ const expression = firstStatement.expression;
+ if (j.Literal.check(expression) || j.StringLiteral.check(expression)) {
+ const value = expression.value;
+ return value === 'use client';
+ }
+ // Handle DirectiveLiteral (used by some parsers like babel)
+ if (expression.type === 'DirectiveLiteral') {
+ return expression.value === 'use client';
+ }
+ }
+
+ // Also check directive field (some parsers use this)
+ if (firstStatement.directive === 'use client') {
+ return true;
+ }
+
+ // Check for directives array in program node (babel parser)
+ const directives = program.node.directives;
+ if (directives && directives.length > 0) {
+ return directives.some(d => d.value && d.value.value === 'use client');
+ }
+
+ return false;
+}
+
+/**
+ * Transforms `` component usage to `` component.
+ *
+ * Handles the following transformations:
+ * - `` → ``
+ * - `` → ``
+ * - `` → ``
+ * - `` → ``
+ * - ` ...}>` → ` ...}>`
+ *
+ * Also updates imports from `Protect` to `Show`.
+ *
+ * NOTE: For @clerk/nextjs, this only transforms files with 'use client' directive.
+ * RSC files using from @clerk/nextjs should NOT be transformed,
+ * as is still valid as an RSC-only component.
+ *
+ * @param {import('jscodeshift').FileInfo} fileInfo - The file information
+ * @param {import('jscodeshift').API} api - The API object provided by jscodeshift
+ * @returns {string|undefined} - The transformed source code if modifications were made
+ */
+module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) {
+ const root = j(source);
+ let dirtyFlag = false;
+
+ const isClientComponent = hasUseClientDirective(root, j);
+
+ // Check if this file imports Protect from a hybrid package (like @clerk/nextjs)
+ // If so, and it's NOT a client component, skip the transformation
+ let hasHybridPackageImport = false;
+ HYBRID_PACKAGES.forEach(packageName => {
+ root.find(j.ImportDeclaration, { source: { value: packageName } }).forEach(path => {
+ const specifiers = path.node.specifiers || [];
+ if (specifiers.some(spec => j.ImportSpecifier.check(spec) && spec.imported.name === 'Protect')) {
+ hasHybridPackageImport = true;
+ }
+ });
+ });
+
+ // Skip RSC files that import from hybrid packages
+ if (hasHybridPackageImport && !isClientComponent) {
+ return undefined;
+ }
+
+ // Transform imports: Protect → Show
+ const allPackages = [...CLIENT_ONLY_PACKAGES, ...HYBRID_PACKAGES];
+ allPackages.forEach(packageName => {
+ root.find(j.ImportDeclaration, { source: { value: packageName } }).forEach(path => {
+ const node = path.node;
+ const specifiers = node.specifiers || [];
+
+ specifiers.forEach(spec => {
+ if (j.ImportSpecifier.check(spec) && spec.imported.name === 'Protect') {
+ spec.imported.name = 'Show';
+ if (spec.local && spec.local.name === 'Protect') {
+ spec.local.name = 'Show';
+ }
+ dirtyFlag = true;
+ }
+ });
+ });
+ });
+
+ // Transform JSX: →
+ root.find(j.JSXElement).forEach(path => {
+ const openingElement = path.node.openingElement;
+ const closingElement = path.node.closingElement;
+
+ // Check if this is a element
+ if (!j.JSXIdentifier.check(openingElement.name) || openingElement.name.name !== 'Protect') {
+ return;
+ }
+
+ // Rename to Show
+ openingElement.name.name = 'Show';
+ if (closingElement && j.JSXIdentifier.check(closingElement.name)) {
+ closingElement.name.name = 'Show';
+ }
+
+ const attributes = openingElement.attributes || [];
+ const authAttributes = [];
+ const otherAttributes = [];
+ let conditionAttr = null;
+
+ // Separate auth-related attributes from other attributes
+ attributes.forEach(attr => {
+ if (!j.JSXAttribute.check(attr)) {
+ otherAttributes.push(attr);
+ return;
+ }
+
+ const attrName = attr.name.name;
+ if (attrName === 'condition') {
+ conditionAttr = attr;
+ } else if (['feature', 'permission', 'plan', 'role'].includes(attrName)) {
+ authAttributes.push(attr);
+ } else {
+ otherAttributes.push(attr);
+ }
+ });
+
+ // Build the `when` prop
+ let whenValue = null;
+
+ if (conditionAttr) {
+ // condition prop becomes the when callback directly
+ whenValue = conditionAttr.value;
+ } else if (authAttributes.length > 0) {
+ // Build an object from auth attributes
+ const properties = authAttributes.map(attr => {
+ const key = j.identifier(attr.name.name);
+ let value;
+
+ if (j.JSXExpressionContainer.check(attr.value)) {
+ value = attr.value.expression;
+ } else if (j.StringLiteral.check(attr.value) || j.Literal.check(attr.value)) {
+ value = attr.value;
+ } else {
+ // Default string value
+ value = j.stringLiteral(attr.value?.value || '');
+ }
+
+ return j.objectProperty(key, value);
+ });
+
+ whenValue = j.jsxExpressionContainer(j.objectExpression(properties));
+ }
+
+ // Reconstruct attributes with `when` prop
+ const newAttributes = [];
+
+ if (whenValue) {
+ newAttributes.push(j.jsxAttribute(j.jsxIdentifier('when'), whenValue));
+ }
+
+ // Add remaining attributes (fallback, etc.)
+ otherAttributes.forEach(attr => newAttributes.push(attr));
+
+ openingElement.attributes = newAttributes;
+ dirtyFlag = true;
+ });
+
+ if (!dirtyFlag) {
+ return undefined;
+ }
+
+ let result = root.toSource();
+ // Fix double semicolons that can occur when recast reprints directive prologues
+ result = result.replace(/^(['"`][^'"`]+['"`]);;/gm, '$1;');
+ return result;
+};
+
+module.exports.parser = 'tsx';
From 1f8e65252b050f62b6ca34f7f106d50e764e208f Mon Sep 17 00:00:00 2001
From: Jacek
Date: Thu, 4 Dec 2025 21:31:08 -0600
Subject: [PATCH 02/17] typing tweak
---
packages/astro/src/react/controlComponents.tsx | 4 ++--
.../__tests__/__snapshots__/exports.test.ts.snap | 2 +-
packages/chrome-extension/src/react/re-exports.ts | 2 +-
.../src/app-router/server/controlComponents.tsx | 8 ++++----
packages/react/src/components/controlComponents.tsx | 10 +++-------
packages/shared/src/types/protect.ts | 13 +++++++++----
packages/vue/src/components/controlComponents.ts | 4 ++--
7 files changed, 22 insertions(+), 21 deletions(-)
diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx
index 956a9f61347..5a574164748 100644
--- a/packages/astro/src/react/controlComponents.tsx
+++ b/packages/astro/src/react/controlComponents.tsx
@@ -4,7 +4,7 @@ import type { PropsWithChildren } from 'react';
import React, { useEffect, useState } from 'react';
import { $csrState } from '../stores/internal';
-import type { ProtectProps as _ProtectProps } from '../types';
+import type { ProtectParams } from '@clerk/shared/types';
import { useAuth } from './hooks';
import type { WithClerkProp } from './utils';
import { withClerk } from './utils';
@@ -70,7 +70,7 @@ export const ClerkLoading = ({ children }: React.PropsWithChildren): JSX.Element
};
export type ProtectProps = React.PropsWithChildren<
- _ProtectProps & { fallback?: React.ReactNode } & PendingSessionOptions
+ ProtectParams & { fallback?: React.ReactNode } & PendingSessionOptions
>;
/**
diff --git a/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap
index 120fb6d4a1c..01e780dea00 100644
--- a/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap
+++ b/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -15,12 +15,12 @@ exports[`public exports > should not include a breaking change 1`] = `
"OrganizationProfile",
"OrganizationSwitcher",
"PricingTable",
- "Protect",
"RedirectToCreateOrganization",
"RedirectToOrganizationProfile",
"RedirectToSignIn",
"RedirectToSignUp",
"RedirectToUserProfile",
+ "Show",
"SignIn",
"SignInButton",
"SignInWithMetamaskButton",
diff --git a/packages/chrome-extension/src/react/re-exports.ts b/packages/chrome-extension/src/react/re-exports.ts
index 2838dc6264b..f13e8e45c13 100644
--- a/packages/chrome-extension/src/react/re-exports.ts
+++ b/packages/chrome-extension/src/react/re-exports.ts
@@ -10,12 +10,12 @@ export {
OrganizationProfile,
OrganizationSwitcher,
PricingTable,
- Protect,
RedirectToCreateOrganization,
RedirectToOrganizationProfile,
RedirectToSignIn,
RedirectToSignUp,
RedirectToUserProfile,
+ Show,
SignIn,
SignInButton,
SignInWithMetamaskButton,
diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx
index 60039b33752..2572a7a256f 100644
--- a/packages/nextjs/src/app-router/server/controlComponents.tsx
+++ b/packages/nextjs/src/app-router/server/controlComponents.tsx
@@ -1,10 +1,10 @@
-import type { PendingSessionOptions, ProtectProps as _ProtectProps } from '@clerk/shared/types';
+import type { PendingSessionOptions, ProtectParams } from '@clerk/shared/types';
import React from 'react';
import { auth } from './auth';
-type ProtectProps = React.PropsWithChildren<
- _ProtectProps & {
+export type AppRouterProtectProps = React.PropsWithChildren<
+ ProtectParams & {
fallback?: React.ReactNode;
} & PendingSessionOptions
>;
@@ -37,7 +37,7 @@ export async function SignedOut(
* Unauthorized} />
* ```
*/
-export async function Protect(props: ProtectProps): Promise {
+export async function Protect(props: AppRouterProtectProps): Promise {
const { children, fallback, ...restAuthorizedParams } = props;
const { has, userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut });
diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx
index 9dcd97956e3..715aa3056ad 100644
--- a/packages/react/src/components/controlComponents.tsx
+++ b/packages/react/src/components/controlComponents.tsx
@@ -1,10 +1,5 @@
import { deprecated } from '@clerk/shared/deprecated';
-import type {
- HandleOAuthCallbackParams,
- PendingSessionOptions,
- ShowProps as _ShowProps,
- ShowWhenCondition,
-} from '@clerk/shared/types';
+import type { HandleOAuthCallbackParams, PendingSessionOptions, ShowWhenCondition } from '@clerk/shared/types';
import React from 'react';
import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
@@ -75,7 +70,8 @@ export const ClerkDegraded = ({ children }: React.PropsWithChildren) =>
};
export type ShowProps = React.PropsWithChildren<
- _ShowProps & {
+ {
+ when: ShowWhenCondition;
fallback?: React.ReactNode;
} & PendingSessionOptions
>;
diff --git a/packages/shared/src/types/protect.ts b/packages/shared/src/types/protect.ts
index 70bc9118295..e06cf74afb5 100644
--- a/packages/shared/src/types/protect.ts
+++ b/packages/shared/src/types/protect.ts
@@ -3,9 +3,9 @@ import type { CheckAuthorizationWithCustomPermissions } from './session';
import type { Autocomplete } from './utils';
/**
- * Props for the `` component, which restricts access to its children based on authentication and authorization.
+ * Authorization parameters used by `` and `auth.protect()`.
*
- * Use `ProtectProps` to specify the required role, permission, feature, or plan for access.
+ * Use `ProtectParams` to specify the required role, permission, feature, or plan for access.
*
* @example
* ```tsx
@@ -22,10 +22,10 @@ import type { Autocomplete } from './utils';
*
*
* // Require a specific plan
- *
+ *
* ```
*/
-export type ProtectProps =
+export type ProtectParams =
| {
condition?: never;
role: OrganizationCustomRoleKey;
@@ -69,6 +69,11 @@ export type ProtectProps =
plan?: never;
};
+/**
+ * @deprecated Use {@link ProtectParams} instead.
+ */
+export type ProtectProps = ProtectParams;
+
/**
* Authorization condition for the `when` prop in ``.
* Can be an object specifying role, permission, feature, or plan,
diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts
index 5148700900f..eeb7dd546d6 100644
--- a/packages/vue/src/components/controlComponents.ts
+++ b/packages/vue/src/components/controlComponents.ts
@@ -2,7 +2,7 @@ import { deprecated } from '@clerk/shared/deprecated';
import type {
HandleOAuthCallbackParams,
PendingSessionOptions,
- ProtectProps as _ProtectProps,
+ ProtectParams,
RedirectOptions,
} from '@clerk/shared/types';
import { defineComponent } from 'vue';
@@ -112,7 +112,7 @@ export const AuthenticateWithRedirectCallback = defineComponent((props: HandleOA
return () => null;
});
-export type ProtectProps = _ProtectProps & PendingSessionOptions;
+export type ProtectProps = ProtectParams & PendingSessionOptions;
export const Protect = defineComponent((props: ProtectProps, { slots }) => {
const { isLoaded, has, userId } = useAuth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut });
From fe77607b3f9c1706d69aec2599f4a8c8b2d6f657 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Fri, 5 Dec 2025 14:39:58 -0600
Subject: [PATCH 03/17] wip
---
.../app-router/server/controlComponents.tsx | 38 ++++++++++++++++++-
packages/nextjs/src/components.server.ts | 7 ++--
.../src/components/controlComponents.tsx | 21 ++++++++--
packages/shared/src/types/protect.ts | 13 ++++---
4 files changed, 65 insertions(+), 14 deletions(-)
diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx
index 2572a7a256f..392823abecd 100644
--- a/packages/nextjs/src/app-router/server/controlComponents.tsx
+++ b/packages/nextjs/src/app-router/server/controlComponents.tsx
@@ -1,4 +1,4 @@
-import type { PendingSessionOptions, ProtectParams } from '@clerk/shared/types';
+import type { PendingSessionOptions, ProtectParams, ShowWhenCondition } from '@clerk/shared/types';
import React from 'react';
import { auth } from './auth';
@@ -9,6 +9,13 @@ export type AppRouterProtectProps = React.PropsWithChildren<
} & PendingSessionOptions
>;
+export type AppRouterShowProps = React.PropsWithChildren<
+ PendingSessionOptions & {
+ fallback?: React.ReactNode;
+ when: ShowWhenCondition;
+ }
+>;
+
export async function SignedIn(
props: React.PropsWithChildren,
): Promise {
@@ -74,3 +81,32 @@ export async function Protect(props: AppRouterProtectProps): Promise` to render children based on authorization or sign-in state.
+ */
+export async function Show(props: AppRouterShowProps): Promise {
+ const { children, fallback, treatPendingAsSignedOut, when } = props;
+ const { has, userId } = await auth({ treatPendingAsSignedOut });
+
+ const resolvedWhen = when;
+ const authorized = <>{children}>;
+ const unauthorized = fallback ? <>{fallback}> : null;
+
+ if (typeof resolvedWhen === 'string') {
+ if (resolvedWhen === 'signedOut') {
+ return userId ? unauthorized : authorized;
+ }
+ return userId ? authorized : unauthorized;
+ }
+
+ if (!userId) {
+ return unauthorized;
+ }
+
+ if (typeof resolvedWhen === 'function') {
+ return resolvedWhen(has) ? authorized : unauthorized;
+ }
+
+ return has(resolvedWhen) ? authorized : unauthorized;
+}
diff --git a/packages/nextjs/src/components.server.ts b/packages/nextjs/src/components.server.ts
index f73c8cc91c5..291aa1df659 100644
--- a/packages/nextjs/src/components.server.ts
+++ b/packages/nextjs/src/components.server.ts
@@ -1,11 +1,12 @@
import { ClerkProvider } from './app-router/server/ClerkProvider';
-import { Protect, SignedIn, SignedOut } from './app-router/server/controlComponents';
+import { Protect, Show, SignedIn, SignedOut } from './app-router/server/controlComponents';
-export { ClerkProvider, SignedOut, SignedIn, Protect };
+export { ClerkProvider, Protect, Show, SignedIn, SignedOut };
export type ServerComponentsServerModuleTypes = {
ClerkProvider: typeof ClerkProvider;
+ Protect: typeof Protect;
+ Show: typeof Show;
SignedIn: typeof SignedIn;
SignedOut: typeof SignedOut;
- Protect: typeof Protect;
};
diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx
index 715aa3056ad..4454feedbda 100644
--- a/packages/react/src/components/controlComponents.tsx
+++ b/packages/react/src/components/controlComponents.tsx
@@ -71,13 +71,13 @@ export const ClerkDegraded = ({ children }: React.PropsWithChildren) =>
export type ShowProps = React.PropsWithChildren<
{
- when: ShowWhenCondition;
fallback?: React.ReactNode;
+ when: ShowWhenCondition;
} & PendingSessionOptions
>;
/**
- * Use `` to conditionally render content based on user authorization.
+ * Use `` to conditionally render content based on user authorization or sign-in state.
*
* @example
* ```tsx
@@ -93,6 +93,7 @@ export type ShowProps = React.PropsWithChildren<
*
*
* ```
+ *
*/
export const Show = ({ children, fallback, treatPendingAsSignedOut, when }: ShowProps) => {
useAssertWrappedByClerkProvider('Show');
@@ -103,22 +104,34 @@ export const Show = ({ children, fallback, treatPendingAsSignedOut, when }: Show
return null;
}
+ const resolvedWhen = when;
const authorized = children;
const unauthorized = fallback ?? null;
+ if (resolvedWhen === 'signedOut') {
+ return userId ? unauthorized : authorized;
+ }
+
if (!userId) {
return unauthorized;
}
+ if (resolvedWhen === 'signedIn') {
+ return authorized;
+ }
+
// At this point, userId is defined so has() is guaranteed to be available
- if (checkAuthorization(when, has!)) {
+ if (checkAuthorization(resolvedWhen, has!)) {
return authorized;
}
return unauthorized;
};
-function checkAuthorization(when: ShowWhenCondition, has: NonNullable['has']>): boolean {
+function checkAuthorization(
+ when: Exclude,
+ has: NonNullable['has']>,
+): boolean {
if (typeof when === 'function') {
return when(has);
}
diff --git a/packages/shared/src/types/protect.ts b/packages/shared/src/types/protect.ts
index e06cf74afb5..ccca27d82c9 100644
--- a/packages/shared/src/types/protect.ts
+++ b/packages/shared/src/types/protect.ts
@@ -1,5 +1,5 @@
import type { OrganizationCustomPermissionKey, OrganizationCustomRoleKey } from './organizationMembership';
-import type { CheckAuthorizationWithCustomPermissions } from './session';
+import type { CheckAuthorizationWithCustomPermissions, PendingSessionOptions } from './session';
import type { Autocomplete } from './utils';
/**
@@ -80,10 +80,9 @@ export type ProtectProps = ProtectParams;
* or a callback function receiving the `has` helper for complex conditions.
*/
export type ShowWhenCondition =
- | { role: OrganizationCustomRoleKey }
- | { permission: OrganizationCustomPermissionKey }
- | { feature: Autocomplete<`org:${string}` | `user:${string}`> }
- | { plan: Autocomplete<`org:${string}` | `user:${string}`> }
+ | 'signedIn'
+ | 'signedOut'
+ | ProtectParams
| ((has: CheckAuthorizationWithCustomPermissions) => boolean);
/**
@@ -106,7 +105,9 @@ export type ShowWhenCondition =
* // Require a specific plan
* ...
* ```
+ *
*/
-export type ShowProps = {
+export type ShowProps = PendingSessionOptions & {
+ fallback?: unknown;
when: ShowWhenCondition;
};
From e007eed2cf317035b3aae76bc5034f23012666be Mon Sep 17 00:00:00 2001
From: Jacek
Date: Fri, 5 Dec 2025 20:15:48 -0600
Subject: [PATCH 04/17] wip
---
.../__tests__/controlComponents.test.tsx | 118 ++++++++++++++++++
1 file changed, 118 insertions(+)
create mode 100644 packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx
diff --git a/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx
new file mode 100644
index 00000000000..abb1a538d0e
--- /dev/null
+++ b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx
@@ -0,0 +1,118 @@
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import type { ShowWhenCondition } from '@clerk/shared/types';
+import { Show } from '../controlComponents';
+import { auth } from '../auth';
+
+vi.mock('../auth', () => ({
+ auth: vi.fn(),
+}));
+
+const mockAuth = auth as unknown as ReturnType;
+
+const render = async (element: Promise) => {
+ const resolved = await element;
+ if (!resolved) {
+ return '';
+ }
+ return renderToStaticMarkup(resolved);
+};
+
+const setAuthReturn = (value: { has?: (params: unknown) => boolean; userId: string | null }) => {
+ mockAuth.mockResolvedValue(value);
+};
+
+const signedInWhen: ShowWhenCondition = 'signedIn';
+const signedOutWhen: ShowWhenCondition = 'signedOut';
+
+describe('Show (App Router server)', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('renders children when signed in', async () => {
+ const has = vi.fn();
+ setAuthReturn({ has, userId: 'user_123' });
+
+ const html = await render(
+ Show({
+ children: signed-in
,
+ fallback: fallback
,
+ treatPendingAsSignedOut: false,
+ when: signedInWhen,
+ }),
+ );
+
+ expect(mockAuth).toHaveBeenCalledWith({ treatPendingAsSignedOut: false });
+ expect(html).toContain('signed-in');
+ });
+
+ it('renders children when signed out', async () => {
+ const has = vi.fn();
+ setAuthReturn({ has, userId: null });
+
+ const html = await render(
+ Show({
+ children: signed-out
,
+ fallback: fallback
,
+ treatPendingAsSignedOut: false,
+ when: signedOutWhen,
+ }),
+ );
+
+ expect(html).toContain('signed-out');
+ });
+
+ it('renders fallback when signed out but user is present', async () => {
+ const has = vi.fn();
+ setAuthReturn({ has, userId: 'user_123' });
+
+ const html = await render(
+ Show({
+ children: signed-out
,
+ fallback: fallback
,
+ treatPendingAsSignedOut: false,
+ when: signedOutWhen,
+ }),
+ );
+
+ expect(html).toContain('fallback');
+ });
+
+ it('uses has() when when is an authorization object', async () => {
+ const has = vi.fn().mockReturnValue(true);
+ setAuthReturn({ has, userId: 'user_123' });
+
+ const html = await render(
+ Show({
+ children: authorized
,
+ fallback: fallback
,
+ treatPendingAsSignedOut: false,
+ when: { role: 'admin' },
+ }),
+ );
+
+ expect(has).toHaveBeenCalledWith({ role: 'admin' });
+ expect(html).toContain('authorized');
+ });
+
+ it('uses predicate when when is a function', async () => {
+ const has = vi.fn().mockReturnValue(true);
+ const predicate = vi.fn().mockReturnValue(true);
+ setAuthReturn({ has, userId: 'user_123' });
+
+ const html = await render(
+ Show({
+ children: predicate-pass
,
+ fallback: fallback
,
+ treatPendingAsSignedOut: false,
+ when: predicate,
+ }),
+ );
+
+ expect(predicate).toHaveBeenCalledWith(has);
+ expect(html).toContain('predicate-pass');
+ });
+});
From 11726bb31d77ba936d7e81b5ef1f445faa2f9658 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Mon, 8 Dec 2025 09:45:53 -0600
Subject: [PATCH 05/17] wip
---
packages/astro/src/react/controlComponents.tsx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx
index 5a574164748..e9574b30434 100644
--- a/packages/astro/src/react/controlComponents.tsx
+++ b/packages/astro/src/react/controlComponents.tsx
@@ -1,10 +1,9 @@
-import type { HandleOAuthCallbackParams, PendingSessionOptions } from '@clerk/shared/types';
+import type { HandleOAuthCallbackParams, PendingSessionOptions, ProtectParams } from '@clerk/shared/types';
import { computed } from 'nanostores';
import type { PropsWithChildren } from 'react';
import React, { useEffect, useState } from 'react';
import { $csrState } from '../stores/internal';
-import type { ProtectParams } from '@clerk/shared/types';
import { useAuth } from './hooks';
import type { WithClerkProp } from './utils';
import { withClerk } from './utils';
From b632485ec581661f0f0cb188fc23a311706bb3f2 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Mon, 8 Dec 2025 10:11:28 -0600
Subject: [PATCH 06/17] wip
---
packages/astro/src/react/controlComponents.tsx | 10 ++++------
packages/react/src/components/controlComponents.tsx | 3 +--
.../src/__tests__/__snapshots__/exports.test.ts.snap | 2 +-
3 files changed, 6 insertions(+), 9 deletions(-)
diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx
index e9574b30434..678e6b56b65 100644
--- a/packages/astro/src/react/controlComponents.tsx
+++ b/packages/astro/src/react/controlComponents.tsx
@@ -1,12 +1,10 @@
import type { HandleOAuthCallbackParams, PendingSessionOptions, ProtectParams } from '@clerk/shared/types';
import { computed } from 'nanostores';
-import type { PropsWithChildren } from 'react';
-import React, { useEffect, useState } from 'react';
+import React, { type PropsWithChildren, useEffect, useState } from 'react';
import { $csrState } from '../stores/internal';
import { useAuth } from './hooks';
-import type { WithClerkProp } from './utils';
-import { withClerk } from './utils';
+import { withClerk, type WithClerkProp } from './utils';
export function SignedOut({ children, treatPendingAsSignedOut }: PropsWithChildren) {
const { userId } = useAuth({ treatPendingAsSignedOut });
@@ -139,9 +137,9 @@ export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAu
*/
export const AuthenticateWithRedirectCallback = withClerk(
({ clerk, ...handleRedirectCallbackParams }: WithClerkProp) => {
- React.useEffect(() => {
+ useEffect(() => {
void clerk?.handleRedirectCallback(handleRedirectCallbackParams);
- }, []);
+ }, [clerk, handleRedirectCallbackParams]);
return null;
},
diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx
index 4454feedbda..046d75dece7 100644
--- a/packages/react/src/components/controlComponents.tsx
+++ b/packages/react/src/components/controlComponents.tsx
@@ -120,8 +120,7 @@ export const Show = ({ children, fallback, treatPendingAsSignedOut, when }: Show
return authorized;
}
- // At this point, userId is defined so has() is guaranteed to be available
- if (checkAuthorization(resolvedWhen, has!)) {
+ if (checkAuthorization(resolvedWhen, has)) {
return authorized;
}
diff --git a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap
index 3e1c592195b..42a6ab133df 100644
--- a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap
+++ b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -34,13 +34,13 @@ exports[`root public exports > should not change unexpectedly 1`] = `
"OrganizationProfile",
"OrganizationSwitcher",
"PricingTable",
- "Protect",
"RedirectToCreateOrganization",
"RedirectToOrganizationProfile",
"RedirectToSignIn",
"RedirectToSignUp",
"RedirectToTasks",
"RedirectToUserProfile",
+ "Show",
"SignIn",
"SignInButton",
"SignInWithMetamaskButton",
From 07a3f7dd34741bf4558bb994229d4d32fa3965d9 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Mon, 8 Dec 2025 10:24:13 -0600
Subject: [PATCH 07/17] wip
---
.../src/__tests__/__snapshots__/exports.test.ts.snap | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap
index 54b196e9899..f3e3a74564e 100644
--- a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap
+++ b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -29,13 +29,13 @@ exports[`root public exports > should not change unexpectedly 1`] = `
"OrganizationProfile",
"OrganizationSwitcher",
"PricingTable",
- "Protect",
"RedirectToCreateOrganization",
"RedirectToOrganizationProfile",
"RedirectToSignIn",
"RedirectToSignUp",
"RedirectToTasks",
"RedirectToUserProfile",
+ "Show",
"SignIn",
"SignInButton",
"SignInWithMetamaskButton",
From 05b73ca3a8a7860f2b93f96edaea7d69aaf7f4e8 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Mon, 8 Dec 2025 14:09:00 -0600
Subject: [PATCH 08/17] wip
---
.../app-router/server/__tests__/controlComponents.test.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx
index abb1a538d0e..680f8c96b1d 100644
--- a/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx
+++ b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx
@@ -1,10 +1,10 @@
+import type { ShowWhenCondition } from '@clerk/shared/types';
import React from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { afterEach, describe, expect, it, vi } from 'vitest';
-import type { ShowWhenCondition } from '@clerk/shared/types';
-import { Show } from '../controlComponents';
import { auth } from '../auth';
+import { Show } from '../controlComponents';
vi.mock('../auth', () => ({
auth: vi.fn(),
From 0f345b8a0754d15b2cbf07b4f9db94dac3e79c90 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Mon, 8 Dec 2025 14:14:44 -0600
Subject: [PATCH 09/17] wip
---
packages/astro/src/react/controlComponents.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx
index 678e6b56b65..3950a0a059a 100644
--- a/packages/astro/src/react/controlComponents.tsx
+++ b/packages/astro/src/react/controlComponents.tsx
@@ -137,9 +137,10 @@ export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAu
*/
export const AuthenticateWithRedirectCallback = withClerk(
({ clerk, ...handleRedirectCallbackParams }: WithClerkProp) => {
+ // eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
void clerk?.handleRedirectCallback(handleRedirectCallbackParams);
- }, [clerk, handleRedirectCallbackParams]);
+ }, []);
return null;
},
From 7c0b86c41ba49675b295334d9103d1dec6522a55 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Mon, 8 Dec 2025 14:16:50 -0600
Subject: [PATCH 10/17] better JSDoc
---
.../app-router/server/controlComponents.tsx | 29 ++++++++++++++++++-
1 file changed, 28 insertions(+), 1 deletion(-)
diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx
index 392823abecd..3f50e6684f7 100644
--- a/packages/nextjs/src/app-router/server/controlComponents.tsx
+++ b/packages/nextjs/src/app-router/server/controlComponents.tsx
@@ -83,7 +83,34 @@ export async function Protect(props: AppRouterProtectProps): Promise` to render children based on authorization or sign-in state.
+ * Use `` to render children when an authorization or sign-in condition passes.
+ *
+ * @param props.when Condition that controls rendering. Accepts:
+ * - authorization objects such as `{ permission: "..." }`, `{ role: "..." }`, `{ feature: "..." }`, or `{ plan: "..." }`
+ * - the string `"signedIn"` to render when a user is present
+ * - the string `"signedOut"` to render when no user is present
+ * - predicate functions `(has) => boolean` that receive the `has` helper
+ * @param props.fallback Optional content rendered when the condition fails.
+ * @param props.children Content rendered when the condition passes.
+ *
+ * @example
+ * ```tsx
+ * Unauthorized}>
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * has({ permission: "org:read" }) && isFeatureEnabled}>
+ *
+ *
+ *
+ *
+ *
+ *
+ * ```
*/
export async function Show(props: AppRouterShowProps): Promise {
const { children, fallback, treatPendingAsSignedOut, when } = props;
From c133f9acd540c56f3b1994108b2d7adfafe5caa2 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Mon, 8 Dec 2025 14:32:38 -0600
Subject: [PATCH 11/17] wip
---
.../transform-protect-to-show.fixtures.js | 9 ++++
.../codemods/transform-protect-to-show.cjs | 2 +-
.../upgrade/src/components/SDKWorkflow.js | 53 +++++++++++++++++--
3 files changed, 58 insertions(+), 6 deletions(-)
diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
index e91211cd7b6..8a99c58f739 100644
--- a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
+++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
@@ -24,6 +24,15 @@ import { Protect } from "@clerk/nextjs"
`,
output: null,
},
+ {
+ name: 'Import transform for @clerk/chrome-extension',
+ source: `
+import { Protect } from "@clerk/chrome-extension"
+ `,
+ output: `
+import { Show } from "@clerk/chrome-extension"
+`,
+ },
{
name: 'Basic permission prop transform',
source: `
diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
index bc20b06ab2e..235e388ebfd 100644
--- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs
+++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
@@ -1,5 +1,5 @@
// Packages that are always client-side
-const CLIENT_ONLY_PACKAGES = ['@clerk/react', '@clerk/expo'];
+const CLIENT_ONLY_PACKAGES = ['@clerk/chrome-extension', '@clerk/expo', '@clerk/react'];
// Packages that can be used in both RSC and client components
const HYBRID_PACKAGES = ['@clerk/nextjs'];
diff --git a/packages/upgrade/src/components/SDKWorkflow.js b/packages/upgrade/src/components/SDKWorkflow.js
index ecd2a491c71..d348289adf5 100644
--- a/packages/upgrade/src/components/SDKWorkflow.js
+++ b/packages/upgrade/src/components/SDKWorkflow.js
@@ -12,6 +12,7 @@ import { UpgradeSDK } from './UpgradeSDK.js';
const CODEMODS = {
ASYNC_REQUEST: 'transform-async-request',
CLERK_REACT_V6: 'transform-clerk-react-v6',
+ PROTECT_TO_SHOW: 'transform-protect-to-show',
REMOVE_DEPRECATED_PROPS: 'transform-remove-deprecated-props',
};
@@ -141,6 +142,7 @@ function NextjsWorkflow({
version,
}) {
const [v6CodemodComplete, setV6CodemodComplete] = useState(false);
+ const [removeDeprecatedPropsComplete, setRemoveDeprecatedPropsComplete] = useState(false);
const [glob, setGlob] = useState();
return (
@@ -174,12 +176,20 @@ function NextjsWorkflow({
) : null}
{v6CodemodComplete ? (
) : null}
+ {removeDeprecatedPropsComplete ? (
+
+ ) : null}
>
)}
{version === 6 && (
@@ -198,12 +208,20 @@ function NextjsWorkflow({
) : null}
{v6CodemodComplete ? (
) : null}
+ {removeDeprecatedPropsComplete ? (
+
+ ) : null}
>
)}
{version === 7 && (
@@ -218,12 +236,20 @@ function NextjsWorkflow({
/>
{v6CodemodComplete ? (
) : null}
+ {removeDeprecatedPropsComplete ? (
+
+ ) : null}
>
) : (
<>
@@ -302,6 +328,7 @@ function ReactSdkWorkflow({
version,
}) {
const [v6CodemodComplete, setV6CodemodComplete] = useState(false);
+ const [removeDeprecatedPropsComplete, setRemoveDeprecatedPropsComplete] = useState(false);
const [glob, setGlob] = useState();
const replacePackage = sdk === 'clerk-react' || sdk === 'clerk-expo';
const needsUpgrade = versionNeedsUpgrade(sdk, version);
@@ -338,12 +365,20 @@ function ReactSdkWorkflow({
) : null}
{v6CodemodComplete ? (
) : null}
+ {removeDeprecatedPropsComplete ? (
+
+ ) : null}
>
)}
{!needsUpgrade && (
@@ -358,12 +393,20 @@ function ReactSdkWorkflow({
/>
{v6CodemodComplete ? (
) : null}
+ {removeDeprecatedPropsComplete ? (
+
+ ) : null}
>
) : (
<>
From f774e21783e9f49f3401b530d9720832deac6c95 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Mon, 8 Dec 2025 20:03:22 -0600
Subject: [PATCH 12/17] wip
---
packages/astro/src/react/controlComponents.tsx | 1 -
.../src/codemods/transform-protect-to-show.cjs | 11 ++++++++---
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx
index 3950a0a059a..5e9ac4ce889 100644
--- a/packages/astro/src/react/controlComponents.tsx
+++ b/packages/astro/src/react/controlComponents.tsx
@@ -137,7 +137,6 @@ export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAu
*/
export const AuthenticateWithRedirectCallback = withClerk(
({ clerk, ...handleRedirectCallbackParams }: WithClerkProp) => {
- // eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => {
void clerk?.handleRedirectCallback(handleRedirectCallbackParams);
}, []);
diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
index 235e388ebfd..fea4957a723 100644
--- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs
+++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
@@ -1,7 +1,7 @@
// Packages that are always client-side
-const CLIENT_ONLY_PACKAGES = ['@clerk/chrome-extension', '@clerk/expo', '@clerk/react'];
+const CLIENT_ONLY_PACKAGES = ['@clerk/chrome-extension', '@clerk/expo', '@clerk/react', '@clerk/vue'];
// Packages that can be used in both RSC and client components
-const HYBRID_PACKAGES = ['@clerk/nextjs'];
+const HYBRID_PACKAGES = ['@clerk/astro', '@clerk/nextjs'];
/**
* Checks if a file has a 'use client' directive at the top.
@@ -66,6 +66,7 @@ function hasUseClientDirective(root, j) {
module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) {
const root = j(source);
let dirtyFlag = false;
+ const protectLocalNames = [];
const isClientComponent = hasUseClientDirective(root, j);
@@ -95,10 +96,14 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
specifiers.forEach(spec => {
if (j.ImportSpecifier.check(spec) && spec.imported.name === 'Protect') {
+ const effectiveLocalName = spec.local ? spec.local.name : spec.imported.name;
spec.imported.name = 'Show';
if (spec.local && spec.local.name === 'Protect') {
spec.local.name = 'Show';
}
+ if (!protectLocalNames.includes(effectiveLocalName)) {
+ protectLocalNames.push(effectiveLocalName);
+ }
dirtyFlag = true;
}
});
@@ -111,7 +116,7 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
const closingElement = path.node.closingElement;
// Check if this is a element
- if (!j.JSXIdentifier.check(openingElement.name) || openingElement.name.name !== 'Protect') {
+ if (!j.JSXIdentifier.check(openingElement.name) || !protectLocalNames.includes(openingElement.name.name)) {
return;
}
From fdbb5cdadb7a5987ecb0b118a91437835ea143bc Mon Sep 17 00:00:00 2001
From: Jacek
Date: Mon, 8 Dec 2025 22:54:17 -0600
Subject: [PATCH 13/17] wip
---
.../src/codemods/transform-protect-to-show.cjs | 13 +++++++++----
pnpm-lock.yaml | 2 +-
2 files changed, 10 insertions(+), 5 deletions(-)
diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
index fea4957a723..c531aaee66b 100644
--- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs
+++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
@@ -120,10 +120,15 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
return;
}
- // Rename to Show
- openingElement.name.name = 'Show';
- if (closingElement && j.JSXIdentifier.check(closingElement.name)) {
- closingElement.name.name = 'Show';
+ const originalName = openingElement.name.name;
+
+ // Only rename if the component was used without an alias (as ).
+ // For aliased imports (e.g., Protect as MyProtect), keep the alias in place.
+ if (originalName === 'Protect') {
+ openingElement.name.name = 'Show';
+ if (closingElement && j.JSXIdentifier.check(closingElement.name)) {
+ closingElement.name.name = 'Show';
+ }
}
const attributes = openingElement.attributes || [];
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 232cb752a0f..13de6825908 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -2355,7 +2355,7 @@ packages:
'@expo/bunyan@4.0.1':
resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==}
- engines: {'0': node >=0.10.0}
+ engines: {node: '>=0.10.0'}
'@expo/cli@0.22.26':
resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==}
From 4a3a968649e183a34e18819b5bd6e6384aaf7e9d Mon Sep 17 00:00:00 2001
From: Jacek
Date: Tue, 9 Dec 2025 10:27:14 -0600
Subject: [PATCH 14/17] update codemod
---
.../transform-protect-to-show.fixtures.js | 27 +++++++++++++++++++
.../codemods/transform-protect-to-show.cjs | 2 ++
2 files changed, 29 insertions(+)
diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
index 8a99c58f739..98be7b026f5 100644
--- a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
+++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
@@ -85,6 +85,33 @@ function App() {
);
}
+`,
+ },
+ {
+ name: 'Boolean shorthand auth prop transforms to true',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ )
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ );
+}
`,
},
{
diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
index c531aaee66b..8039c8a718f 100644
--- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs
+++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
@@ -169,6 +169,8 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
value = attr.value.expression;
} else if (j.StringLiteral.check(attr.value) || j.Literal.check(attr.value)) {
value = attr.value;
+ } else if (attr.value == null) {
+ value = j.booleanLiteral(true);
} else {
// Default string value
value = j.stringLiteral(attr.value?.value || '');
From 197ae2cca2df3ec287359291ee8ef6366b1cc8b7 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Tue, 9 Dec 2025 12:51:43 -0600
Subject: [PATCH 15/17] backfill codemod
---
.../transform-protect-to-show.fixtures.js | 38 +++++++++++++
.../codemods/transform-protect-to-show.cjs | 57 +++++++++++++++----
2 files changed, 83 insertions(+), 12 deletions(-)
diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
index 98be7b026f5..5ffdb646778 100644
--- a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
+++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
@@ -362,4 +362,42 @@ function Component() {
);
}`,
},
+ {
+ name: 'Bare Protect defaults to signedIn',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ )
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ );
+}
+`,
+ },
+ {
+ name: 'ProtectProps import rewrites to ShowProps',
+ source: `
+import { ProtectProps } from "@clerk/react";
+
+type Props = ProtectProps;
+ `,
+ output: `
+import { ShowProps } from "@clerk/react";
+
+type Props = ShowProps;
+`,
+ },
];
diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
index 8039c8a718f..b985e83d3cb 100644
--- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs
+++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
@@ -67,6 +67,7 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
const root = j(source);
let dirtyFlag = false;
const protectLocalNames = [];
+ const protectPropsLocalsToRename = [];
const isClientComponent = hasUseClientDirective(root, j);
@@ -87,7 +88,7 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
return undefined;
}
- // Transform imports: Protect → Show
+ // Transform imports: Protect → Show, ProtectProps → ShowProps
const allPackages = [...CLIENT_ONLY_PACKAGES, ...HYBRID_PACKAGES];
allPackages.forEach(packageName => {
root.find(j.ImportDeclaration, { source: { value: packageName } }).forEach(path => {
@@ -95,21 +96,53 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
const specifiers = node.specifiers || [];
specifiers.forEach(spec => {
- if (j.ImportSpecifier.check(spec) && spec.imported.name === 'Protect') {
- const effectiveLocalName = spec.local ? spec.local.name : spec.imported.name;
- spec.imported.name = 'Show';
- if (spec.local && spec.local.name === 'Protect') {
- spec.local.name = 'Show';
+ if (j.ImportSpecifier.check(spec)) {
+ if (spec.imported.name === 'Protect') {
+ const effectiveLocalName = spec.local ? spec.local.name : spec.imported.name;
+ spec.imported.name = 'Show';
+ if (spec.local && spec.local.name === 'Protect') {
+ spec.local.name = 'Show';
+ }
+ if (!protectLocalNames.includes(effectiveLocalName)) {
+ protectLocalNames.push(effectiveLocalName);
+ }
+ dirtyFlag = true;
}
- if (!protectLocalNames.includes(effectiveLocalName)) {
- protectLocalNames.push(effectiveLocalName);
+
+ if (spec.imported.name === 'ProtectProps') {
+ const effectiveLocalName = spec.local ? spec.local.name : spec.imported.name;
+ spec.imported.name = 'ShowProps';
+ if (spec.local && spec.local.name === 'ProtectProps') {
+ spec.local.name = 'ShowProps';
+ }
+ if (effectiveLocalName === 'ProtectProps') {
+ protectPropsLocalsToRename.push(effectiveLocalName);
+ }
+ dirtyFlag = true;
}
- dirtyFlag = true;
}
});
});
});
+ // Rename references to ProtectProps (only when local name was ProtectProps)
+ if (protectPropsLocalsToRename.length > 0) {
+ root
+ .find(j.TSTypeReference, {
+ typeName: {
+ type: 'Identifier',
+ name: 'ProtectProps',
+ },
+ })
+ .forEach(path => {
+ const typeName = path.node.typeName;
+ if (j.Identifier.check(typeName) && typeName.name === 'ProtectProps') {
+ typeName.name = 'ShowProps';
+ dirtyFlag = true;
+ }
+ });
+ }
+
// Transform JSX: →
root.find(j.JSXElement).forEach(path => {
const openingElement = path.node.openingElement;
@@ -185,9 +218,9 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
// Reconstruct attributes with `when` prop
const newAttributes = [];
- if (whenValue) {
- newAttributes.push(j.jsxAttribute(j.jsxIdentifier('when'), whenValue));
- }
+ const finalWhenValue = whenValue || j.stringLiteral('signedIn');
+
+ newAttributes.push(j.jsxAttribute(j.jsxIdentifier('when'), finalWhenValue));
// Add remaining attributes (fallback, etc.)
otherAttributes.forEach(attr => newAttributes.push(attr));
From 88f3e3541592393841a7ed4e5ca3b6052221020a Mon Sep 17 00:00:00 2001
From: Jacek
Date: Tue, 9 Dec 2025 13:03:32 -0600
Subject: [PATCH 16/17] wip
---
packages/upgrade/src/codemods/transform-protect-to-show.cjs | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
index b985e83d3cb..f0daf66c185 100644
--- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs
+++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
@@ -98,11 +98,9 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
specifiers.forEach(spec => {
if (j.ImportSpecifier.check(spec)) {
if (spec.imported.name === 'Protect') {
- const effectiveLocalName = spec.local ? spec.local.name : spec.imported.name;
+ const originalImportedName = spec.imported.name;
+ const effectiveLocalName = spec.local ? spec.local.name : originalImportedName;
spec.imported.name = 'Show';
- if (spec.local && spec.local.name === 'Protect') {
- spec.local.name = 'Show';
- }
if (!protectLocalNames.includes(effectiveLocalName)) {
protectLocalNames.push(effectiveLocalName);
}
From aba8aad1337bc8827ffc223359cb4a5f9e7fdcd9 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Tue, 9 Dec 2025 14:03:18 -0600
Subject: [PATCH 17/17] adjust JSDocs
---
.../src/app-router/server/controlComponents.tsx | 13 ++++++-------
packages/react/src/components/controlComponents.tsx | 7 +++++++
2 files changed, 13 insertions(+), 7 deletions(-)
diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx
index 3f50e6684f7..f2370d9c0e2 100644
--- a/packages/nextjs/src/app-router/server/controlComponents.tsx
+++ b/packages/nextjs/src/app-router/server/controlComponents.tsx
@@ -84,14 +84,13 @@ export async function Protect(props: AppRouterProtectProps): Promise` to render children when an authorization or sign-in condition passes.
+ * When `treatPendingAsSignedOut` is true, pending sessions are treated as signed out.
+ * Renders the provided `fallback` (or `null`) when the condition fails.
*
- * @param props.when Condition that controls rendering. Accepts:
- * - authorization objects such as `{ permission: "..." }`, `{ role: "..." }`, `{ feature: "..." }`, or `{ plan: "..." }`
- * - the string `"signedIn"` to render when a user is present
- * - the string `"signedOut"` to render when no user is present
- * - predicate functions `(has) => boolean` that receive the `has` helper
- * @param props.fallback Optional content rendered when the condition fails.
- * @param props.children Content rendered when the condition passes.
+ * The `when` prop supports:
+ * - `"signedIn"` or `"signedOut"` shorthands
+ * - Authorization objects such as `{ permission: "..." }`, `{ role: "..." }`, `{ feature: "..." }`, or `{ plan: "..." }`
+ * - Predicate functions `(has) => boolean` that receive the `has` helper
*
* @example
* ```tsx
diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx
index 046d75dece7..b0c5f72f81d 100644
--- a/packages/react/src/components/controlComponents.tsx
+++ b/packages/react/src/components/controlComponents.tsx
@@ -78,6 +78,13 @@ export type ShowProps = React.PropsWithChildren<
/**
* Use `` to conditionally render content based on user authorization or sign-in state.
+ * Returns `null` while auth is loading. Set `treatPendingAsSignedOut` to treat
+ * pending sessions as signed out during that period.
+ *
+ * The `when` prop supports:
+ * - `"signedIn"` or `"signedOut"` shorthands
+ * - Authorization descriptors (e.g., `{ permission: "org:billing:manage" }`, `{ role: "admin" }`)
+ * - A predicate function `(has) => boolean` that receives the `has` helper
*
* @example
* ```tsx