diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx
index 956a9f61347..5e9ac4ce889 100644
--- a/packages/astro/src/react/controlComponents.tsx
+++ b/packages/astro/src/react/controlComponents.tsx
@@ -1,13 +1,10 @@
-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 React, { type PropsWithChildren, useEffect, useState } from 'react';
import { $csrState } from '../stores/internal';
-import type { ProtectProps as _ProtectProps } from '../types';
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 });
@@ -70,7 +67,7 @@ export const ClerkLoading = ({ children }: React.PropsWithChildren): JSX.Element
};
export type ProtectProps = React.PropsWithChildren<
- _ProtectProps & { fallback?: React.ReactNode } & PendingSessionOptions
+ ProtectParams & { fallback?: React.ReactNode } & PendingSessionOptions
>;
/**
@@ -140,7 +137,7 @@ export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAu
*/
export const AuthenticateWithRedirectCallback = withClerk(
({ clerk, ...handleRedirectCallbackParams }: WithClerkProp) => {
- React.useEffect(() => {
+ useEffect(() => {
void clerk?.handleRedirectCallback(handleRedirectCallbackParams);
}, []);
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/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/__tests__/controlComponents.test.tsx b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx
new file mode 100644
index 00000000000..680f8c96b1d
--- /dev/null
+++ b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx
@@ -0,0 +1,118 @@
+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 { auth } from '../auth';
+import { Show } from '../controlComponents';
+
+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');
+ });
+});
diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx
index d640c63a055..3f50e6684f7 100644
--- a/packages/nextjs/src/app-router/server/controlComponents.tsx
+++ b/packages/nextjs/src/app-router/server/controlComponents.tsx
@@ -1,9 +1,21 @@
-import type { ProtectProps } from '@clerk/react';
-import type { PendingSessionOptions } from '@clerk/shared/types';
+import type { PendingSessionOptions, ProtectParams, ShowWhenCondition } from '@clerk/shared/types';
import React from 'react';
import { auth } from './auth';
+export type AppRouterProtectProps = React.PropsWithChildren<
+ ProtectParams & {
+ fallback?: React.ReactNode;
+ } & PendingSessionOptions
+>;
+
+export type AppRouterShowProps = React.PropsWithChildren<
+ PendingSessionOptions & {
+ fallback?: React.ReactNode;
+ when: ShowWhenCondition;
+ }
+>;
+
export async function SignedIn(
props: React.PropsWithChildren,
): Promise {
@@ -32,7 +44,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 });
@@ -69,3 +81,59 @@ export async function Protect(props: ProtectProps): Promise` 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;
+ 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/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/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/nextjs/src/index.ts b/packages/nextjs/src/index.ts
index b9c24e9b7ce..98f1ffa9698 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';
/**
@@ -73,6 +74,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-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",
diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx
index bdeefbfa05a..046d75dece7 100644
--- a/packages/react/src/components/controlComponents.tsx
+++ b/packages/react/src/components/controlComponents.tsx
@@ -1,9 +1,5 @@
import { deprecated } from '@clerk/shared/deprecated';
-import type {
- HandleOAuthCallbackParams,
- PendingSessionOptions,
- ProtectProps as _ProtectProps,
-} from '@clerk/shared/types';
+import type { HandleOAuthCallbackParams, PendingSessionOptions, ShowWhenCondition } from '@clerk/shared/types';
import React from 'react';
import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
@@ -73,76 +69,74 @@ export const ClerkDegraded = ({ children }: React.PropsWithChildren) =>
return children;
};
-export type ProtectProps = React.PropsWithChildren<
- _ProtectProps & {
+export type ShowProps = React.PropsWithChildren<
+ {
fallback?: React.ReactNode;
+ when: ShowWhenCondition;
} & 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 or sign-in state.
*
- * 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 resolvedWhen = when;
+ const authorized = children;
const unauthorized = fallback ?? null;
- const authorized = children;
+ if (resolvedWhen === 'signedOut') {
+ return userId ? unauthorized : authorized;
+ }
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 (resolvedWhen === 'signedIn') {
+ return authorized;
}
- if (
- restAuthorizedParams.role ||
- restAuthorizedParams.permission ||
- restAuthorizedParams.feature ||
- restAuthorizedParams.plan
- ) {
- if (has(restAuthorizedParams)) {
- return authorized;
- }
- return unauthorized;
+ if (checkAuthorization(resolvedWhen, 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: Exclude,
+ 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 dfbcedcfa93..247bb29ecd3 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -22,18 +22,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 0498c2b5f1b..4727dab46cb 100644
--- a/packages/shared/src/types/protect.ts
+++ b/packages/shared/src/types/protect.ts
@@ -1,11 +1,11 @@
import type { OrganizationCustomPermissionKey, OrganizationCustomRoleKey } from './organizationMembership';
-import type { CheckAuthorizationWithCustomPermissions } from './session';
+import type { CheckAuthorizationWithCustomPermissions, PendingSessionOptions } 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
@@ -21,11 +21,11 @@ import type { Autocomplete } from './utils';
* // Require a specific Feature
*
*
- * // Require a specific Plan
+ * // Require a specific plan
*
* ```
*/
-export type ProtectProps =
+export type ProtectParams =
| {
condition?: never;
role: OrganizationCustomRoleKey;
@@ -68,3 +68,46 @@ export type ProtectProps =
feature?: never;
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,
+ * or a callback function receiving the `has` helper for complex conditions.
+ */
+export type ShowWhenCondition =
+ | 'signedIn'
+ | 'signedOut'
+ | ProtectParams
+ | ((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 = PendingSessionOptions & {
+ fallback?: unknown;
+ when: ShowWhenCondition;
+};
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",
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..8a99c58f739
--- /dev/null
+++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
@@ -0,0 +1,338 @@
+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: '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: `
+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..c531aaee66b
--- /dev/null
+++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
@@ -0,0 +1,207 @@
+// Packages that are always client-side
+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/astro', '@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 protectLocalNames = [];
+
+ 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') {
+ 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;
+ }
+ });
+ });
+ });
+
+ // 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) || !protectLocalNames.includes(openingElement.name.name)) {
+ return;
+ }
+
+ 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 || [];
+ 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';
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 });
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==}