From c9f3d62afd3e85d4bfa3cf6d41115d107d28f084 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Thu, 4 Dec 2025 20:51:00 -0600
Subject: [PATCH 01/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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/33] 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
From 8db6d35dbc3b76e669e6e39e43825adfeefb8697 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Wed, 10 Dec 2025 21:31:53 -0600
Subject: [PATCH 18/33] update app router template
---
.../next-app-router/src/app/page.tsx | 21 ++++++++++++-------
.../src/app/pricing-table/page.tsx | 14 ++++++-------
.../src/app/settings/rcc-protect/page.tsx | 9 ++++----
.../src/app/settings/rsc-protect/page.tsx | 8 +++----
4 files changed, 28 insertions(+), 24 deletions(-)
diff --git a/integration/templates/next-app-router/src/app/page.tsx b/integration/templates/next-app-router/src/app/page.tsx
index 86ba722b3f3..8335c84a8c1 100644
--- a/integration/templates/next-app-router/src/app/page.tsx
+++ b/integration/templates/next-app-router/src/app/page.tsx
@@ -1,4 +1,4 @@
-import { Protect, SignedIn, SignedOut, SignIn, UserButton } from '@clerk/nextjs';
+import { Show, SignedIn, SignedOut, SignIn, UserButton } from '@clerk/nextjs';
import Link from 'next/link';
import { ClientId } from './client-id';
@@ -9,16 +9,21 @@ export default function Home() {
SignedIn
SignedOut
- SignedIn from protect
-
+
+ SignedIn from protect
+
+
user in free
-
-
+
+
user in pro
-
-
+
+
user in plus
-
+
-
+
user in free
-
-
+
+
user in pro
-
-
+
+
user in plus
-
+
>
);
diff --git a/integration/templates/next-app-router/src/app/settings/rcc-protect/page.tsx b/integration/templates/next-app-router/src/app/settings/rcc-protect/page.tsx
index 5b371ed9b2f..bd13e14387d 100644
--- a/integration/templates/next-app-router/src/app/settings/rcc-protect/page.tsx
+++ b/integration/templates/next-app-router/src/app/settings/rcc-protect/page.tsx
@@ -1,14 +1,13 @@
'use client';
-import { Protect } from '@clerk/nextjs';
+import { Show } from '@clerk/nextjs';
export default function Page() {
return (
- User is missing permissions}
+ when={{ permission: 'org:posts:manage' }}
>
User has access
-
+
);
}
diff --git a/integration/templates/next-app-router/src/app/settings/rsc-protect/page.tsx b/integration/templates/next-app-router/src/app/settings/rsc-protect/page.tsx
index 9e21b23d034..56871f6d926 100644
--- a/integration/templates/next-app-router/src/app/settings/rsc-protect/page.tsx
+++ b/integration/templates/next-app-router/src/app/settings/rsc-protect/page.tsx
@@ -1,12 +1,12 @@
-import { Protect } from '@clerk/nextjs';
+import { Show } from '@clerk/nextjs';
export default function Page() {
return (
- User is not admin}
+ when={{ role: 'org:admin' }}
>
User has access
-
+
);
}
From a603b4398a1dcaf5288a12121ac4f546b331dafb Mon Sep 17 00:00:00 2001
From: Jacek
Date: Wed, 10 Dec 2025 21:43:56 -0600
Subject: [PATCH 19/33] wip
---
.../next-app-router/src/app/settings/rsc-protect/page.tsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/integration/templates/next-app-router/src/app/settings/rsc-protect/page.tsx b/integration/templates/next-app-router/src/app/settings/rsc-protect/page.tsx
index 56871f6d926..a3ea713e57f 100644
--- a/integration/templates/next-app-router/src/app/settings/rsc-protect/page.tsx
+++ b/integration/templates/next-app-router/src/app/settings/rsc-protect/page.tsx
@@ -1,12 +1,12 @@
-import { Show } from '@clerk/nextjs';
+import { Protect } from '@clerk/nextjs';
export default function Page() {
return (
- User is not admin}
- when={{ role: 'org:admin' }}
+ role='org:admin'
>
User has access
-
+
);
}
From 671c0f1af37c926e659cea9fec8d2b7b1b9a3d70 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Wed, 10 Dec 2025 22:10:04 -0600
Subject: [PATCH 20/33] changesets
---
.changeset/migrate-to-show.md | 5 +++++
.changeset/show-the-guards.md | 11 +++++++++++
2 files changed, 16 insertions(+)
create mode 100644 .changeset/migrate-to-show.md
create mode 100644 .changeset/show-the-guards.md
diff --git a/.changeset/migrate-to-show.md b/.changeset/migrate-to-show.md
new file mode 100644
index 00000000000..fad0e293d77
--- /dev/null
+++ b/.changeset/migrate-to-show.md
@@ -0,0 +1,5 @@
+---
+'@clerk/upgrade': minor
+---
+
+Add a `transform-protect-to-show` codemod that migrates client-side `` usage to `` with automatic prop and import updates.
diff --git a/.changeset/show-the-guards.md b/.changeset/show-the-guards.md
new file mode 100644
index 00000000000..2eea380a52b
--- /dev/null
+++ b/.changeset/show-the-guards.md
@@ -0,0 +1,11 @@
+---
+'@clerk/react': major
+'@clerk/nextjs': major
+'@clerk/expo': major
+'@clerk/chrome-extension': major
+'@clerk/shared': minor
+'@clerk/astro': patch
+'@clerk/vue': patch
+---
+
+Restrict `` to App Router server usage and introduce `` as the client-side authorization component, updating shared types and Astro/Vue wrappers to align with the new API.
From 3aacf7b63bd4acea852421bd95ee8f3d67a56f83 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Thu, 11 Dec 2025 15:00:46 -0600
Subject: [PATCH 21/33] more removals
---
integration/templates/expo-web/app/index.tsx | 12 +-
.../src/app/page.tsx | 10 +-
.../src/app/billing/checkout-btn/page.tsx | 6 +-
.../next-app-router/src/app/page.tsx | 6 +-
.../src/app/settings/rsc-protect/page.tsx | 8 +-
integration/templates/react-cra/src/App.tsx | 8 +-
.../react-router-library/src/App.tsx | 10 +-
.../react-router-node/app/routes/home.tsx | 6 +-
integration/templates/react-vite/src/App.tsx | 6 +-
.../react-vite/src/protected/index.tsx | 6 +-
.../tanstack-react-start/src/routes/index.tsx | 10 +-
.../astro/src/react/controlComponents.tsx | 83 +---
.../chrome-extension/src/react/re-exports.ts | 2 -
.../expo/src/components/controlComponents.tsx | 2 +-
.../app-router/server/controlComponents.tsx | 74 +---
.../src/client-boundary/controlComponents.ts | 2 -
packages/nextjs/src/components.client.ts | 21 +-
packages/nextjs/src/components.server.ts | 7 +-
packages/nextjs/src/index.ts | 9 +-
packages/nuxt/src/module.ts | 4 +-
packages/nuxt/src/runtime/components/index.ts | 4 +-
.../react/src/components/CheckoutButton.tsx | 19 +-
.../src/components/PlanDetailsButton.tsx | 14 +-
.../components/SubscriptionDetailsButton.tsx | 26 +-
.../__tests__/CheckoutButton.test.tsx | 2 +-
.../SubscriptionDetailsButton.test.tsx | 2 +-
.../src/components/controlComponents.tsx | 20 -
packages/react/src/components/index.ts | 2 -
.../transform-protect-to-show.fixtures.js | 358 ++++++------------
.../codemods/transform-protect-to-show.cjs | 99 ++---
.../vue/src/components/controlComponents.ts | 61 +--
packages/vue/src/components/index.ts | 4 +-
.../app-router/src/app/protected/page.tsx | 14 +-
.../src/pages/user/[[...index]].tsx | 6 +-
.../src/components/nav-bar.tsx | 10 +-
playground/expo/App.tsx | 10 +-
playground/nextjs/app/app-dir/client/page.tsx | 6 +-
playground/nextjs/app/app-dir/page.tsx | 10 +-
playground/nextjs/pages/_app.tsx | 11 +-
playground/react-router/app/root.tsx | 10 +-
playground/vite-react-ts/src/App.tsx | 11 +-
41 files changed, 305 insertions(+), 686 deletions(-)
diff --git a/integration/templates/expo-web/app/index.tsx b/integration/templates/expo-web/app/index.tsx
index 431bf8c209f..ee296309576 100644
--- a/integration/templates/expo-web/app/index.tsx
+++ b/integration/templates/expo-web/app/index.tsx
@@ -1,6 +1,6 @@
-import { Text, View } from 'react-native';
-import { SignedIn, SignedOut } from '@clerk/expo';
+import { Show } from '@clerk/expo';
import { UserButton } from '@clerk/expo/web';
+import { Text, View } from 'react-native';
export default function Index() {
return (
@@ -11,13 +11,13 @@ export default function Index() {
alignItems: 'center',
}}
>
-
+
You are signed in!
-
-
+
+
You are signed out
-
+
);
}
diff --git a/integration/templates/next-app-router-quickstart/src/app/page.tsx b/integration/templates/next-app-router-quickstart/src/app/page.tsx
index 98ee4d4bcd3..797aceb64a1 100644
--- a/integration/templates/next-app-router-quickstart/src/app/page.tsx
+++ b/integration/templates/next-app-router-quickstart/src/app/page.tsx
@@ -1,17 +1,17 @@
-import { SignedIn, SignedOut, SignInButton, SignUpButton, UserButton } from '@clerk/nextjs';
+import { Show, SignInButton, SignUpButton, UserButton } from '@clerk/nextjs';
export default function Home() {
return (
-
+
signed-out-state
-
-
+
+
signed-in-state
-
+
);
}
diff --git a/integration/templates/next-app-router/src/app/billing/checkout-btn/page.tsx b/integration/templates/next-app-router/src/app/billing/checkout-btn/page.tsx
index 4904d056e95..2ba15a81a67 100644
--- a/integration/templates/next-app-router/src/app/billing/checkout-btn/page.tsx
+++ b/integration/templates/next-app-router/src/app/billing/checkout-btn/page.tsx
@@ -1,17 +1,17 @@
-import { SignedIn } from '@clerk/nextjs';
+import { Show } from '@clerk/nextjs';
import { CheckoutButton } from '@clerk/nextjs/experimental';
export default function Home() {
return (
-
+
Checkout Now
-
+
);
}
diff --git a/integration/templates/next-app-router/src/app/page.tsx b/integration/templates/next-app-router/src/app/page.tsx
index 8335c84a8c1..241053ed048 100644
--- a/integration/templates/next-app-router/src/app/page.tsx
+++ b/integration/templates/next-app-router/src/app/page.tsx
@@ -1,4 +1,4 @@
-import { Show, SignedIn, SignedOut, SignIn, UserButton } from '@clerk/nextjs';
+import { Show, SignIn, UserButton } from '@clerk/nextjs';
import Link from 'next/link';
import { ClientId } from './client-id';
@@ -7,8 +7,8 @@ export default function Home() {
Loading user button>} />
- SignedIn
- SignedOut
+ SignedIn
+ SignedOut
User is not admin}
- role='org:admin'
+ when={{ role: 'org:admin' }}
>
User has access
-
+
);
}
diff --git a/integration/templates/react-cra/src/App.tsx b/integration/templates/react-cra/src/App.tsx
index 38197953f08..28309fe6b6f 100644
--- a/integration/templates/react-cra/src/App.tsx
+++ b/integration/templates/react-cra/src/App.tsx
@@ -1,15 +1,15 @@
// @ts-ignore
import React from 'react';
import './App.css';
-import { SignedIn, SignedOut, SignIn, UserButton } from '@clerk/react';
+import { Show, SignIn, UserButton } from '@clerk/react';
function App() {
return (
-
+
-
- Signed In
+
+ Signed In
);
diff --git a/integration/templates/react-router-library/src/App.tsx b/integration/templates/react-router-library/src/App.tsx
index 93dfdf04385..259bb2fc944 100644
--- a/integration/templates/react-router-library/src/App.tsx
+++ b/integration/templates/react-router-library/src/App.tsx
@@ -1,15 +1,15 @@
-import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/react-router';
+import { Show, SignInButton, UserButton } from '@clerk/react-router';
import './App.css';
function App() {
return (
);
}
diff --git a/integration/templates/react-router-node/app/routes/home.tsx b/integration/templates/react-router-node/app/routes/home.tsx
index 57161c90b48..9adefddec39 100644
--- a/integration/templates/react-router-node/app/routes/home.tsx
+++ b/integration/templates/react-router-node/app/routes/home.tsx
@@ -1,4 +1,4 @@
-import { SignedIn, SignedOut, UserButton } from '@clerk/react-router';
+import { Show, UserButton } from '@clerk/react-router';
import type { Route } from './+types/home';
export function meta({}: Route.MetaArgs) {
@@ -9,8 +9,8 @@ export default function Home() {
return (
- SignedIn
- SignedOut
+ SignedIn
+ SignedOut
);
}
diff --git a/integration/templates/react-vite/src/App.tsx b/integration/templates/react-vite/src/App.tsx
index 3c7aabd5906..a826457118f 100644
--- a/integration/templates/react-vite/src/App.tsx
+++ b/integration/templates/react-vite/src/App.tsx
@@ -1,4 +1,4 @@
-import { OrganizationSwitcher, SignedIn, SignedOut, UserButton } from '@clerk/react';
+import { OrganizationSwitcher, Show, UserButton } from '@clerk/react';
import { Link } from 'react-router-dom';
import React from 'react';
import { ClientId } from './client-id';
@@ -9,8 +9,8 @@ function App() {
Loading organization switcher>} />
- SignedOut
- SignedIn
+ SignedOut
+ SignedIn
Protected
);
diff --git a/integration/templates/react-vite/src/protected/index.tsx b/integration/templates/react-vite/src/protected/index.tsx
index 2eb58aa8d76..1a8bcccaac5 100644
--- a/integration/templates/react-vite/src/protected/index.tsx
+++ b/integration/templates/react-vite/src/protected/index.tsx
@@ -1,11 +1,11 @@
-import { SignedIn } from '@clerk/react';
+import { Show } from '@clerk/react';
export default function Page() {
return (
);
}
diff --git a/integration/templates/tanstack-react-start/src/routes/index.tsx b/integration/templates/tanstack-react-start/src/routes/index.tsx
index a5c9bfe8dd4..7564211722a 100644
--- a/integration/templates/tanstack-react-start/src/routes/index.tsx
+++ b/integration/templates/tanstack-react-start/src/routes/index.tsx
@@ -1,4 +1,4 @@
-import { SignedIn, UserButton, SignOutButton, SignedOut, SignIn } from '@clerk/tanstack-react-start';
+import { Show, SignIn, SignOutButton, UserButton } from '@clerk/tanstack-react-start';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/')({
@@ -9,7 +9,7 @@ function Home() {
return (
Index Route
-
+
You are signed in!
View your profile here
@@ -18,12 +18,12 @@ function Home() {
-
-
+
+
You are signed out
-
+
);
}
diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx
index 5e9ac4ce889..5950aa83aa4 100644
--- a/packages/astro/src/react/controlComponents.tsx
+++ b/packages/astro/src/react/controlComponents.tsx
@@ -1,28 +1,11 @@
-import type { HandleOAuthCallbackParams, PendingSessionOptions, ProtectParams } from '@clerk/shared/types';
+import type { HandleOAuthCallbackParams, PendingSessionOptions, ShowWhenCondition } from '@clerk/shared/types';
import { computed } from 'nanostores';
-import React, { type PropsWithChildren, useEffect, useState } from 'react';
+import React, { useEffect, useState } from 'react';
import { $csrState } from '../stores/internal';
import { useAuth } from './hooks';
import { withClerk, type WithClerkProp } from './utils';
-export function SignedOut({ children, treatPendingAsSignedOut }: PropsWithChildren) {
- const { userId } = useAuth({ treatPendingAsSignedOut });
-
- if (userId) {
- return null;
- }
- return children;
-}
-
-export function SignedIn({ children, treatPendingAsSignedOut }: PropsWithChildren) {
- const { userId } = useAuth({ treatPendingAsSignedOut });
- if (!userId) {
- return null;
- }
- return children;
-}
-
const $isLoadingClerkStore = computed($csrState, state => state.isLoaded);
/*
@@ -66,70 +49,40 @@ export const ClerkLoading = ({ children }: React.PropsWithChildren): JSX.Element
return <>{children}>;
};
-export type ProtectProps = React.PropsWithChildren<
- ProtectParams & { fallback?: React.ReactNode } & PendingSessionOptions
+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.
- *
- * Examples:
- * ```
- *
- *
- * has({permission:"a_permission_key"})} />
- * has({role:"a_role_key"})} />
- * Unauthorized} />
- * ```
- */
-export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAuthorizedParams }: ProtectProps) => {
- const { isLoaded, has, userId } = useAuth({ treatPendingAsSignedOut });
+export const Show = ({ children, fallback, treatPendingAsSignedOut, when }: ShowProps) => {
+ 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 authorized = <>{children}>;
const unauthorized = <>{fallback ?? null}>;
- const authorized = <>{children}>;
+ if (when === '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 (when === 'signedIn') {
+ return authorized;
}
- if (
- restAuthorizedParams.role ||
- restAuthorizedParams.permission ||
- restAuthorizedParams.feature ||
- restAuthorizedParams.plan
- ) {
- if (has?.(restAuthorizedParams)) {
- return authorized;
- }
- return unauthorized;
+ if (typeof when === 'function') {
+ return when(has) ? authorized : unauthorized;
}
- /**
- * If neither of the authorization params are passed behave as the ``.
- * If fallback is present render that instead of rendering nothing.
- */
- return authorized;
+ return has(when) ? authorized : unauthorized;
};
/**
diff --git a/packages/chrome-extension/src/react/re-exports.ts b/packages/chrome-extension/src/react/re-exports.ts
index f13e8e45c13..d05f4a29ba5 100644
--- a/packages/chrome-extension/src/react/re-exports.ts
+++ b/packages/chrome-extension/src/react/re-exports.ts
@@ -22,8 +22,6 @@ export {
SignOutButton,
SignUp,
SignUpButton,
- SignedIn,
- SignedOut,
UserAvatar,
UserButton,
UserProfile,
diff --git a/packages/expo/src/components/controlComponents.tsx b/packages/expo/src/components/controlComponents.tsx
index 33edad58240..5ef4f45e015 100644
--- a/packages/expo/src/components/controlComponents.tsx
+++ b/packages/expo/src/components/controlComponents.tsx
@@ -1 +1 @@
-export { ClerkLoaded, ClerkLoading, Show, SignedIn, SignedOut } from '@clerk/react';
+export { ClerkLoaded, ClerkLoading, Show } from '@clerk/react';
diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx
index f2370d9c0e2..c10416a1633 100644
--- a/packages/nextjs/src/app-router/server/controlComponents.tsx
+++ b/packages/nextjs/src/app-router/server/controlComponents.tsx
@@ -1,14 +1,8 @@
-import type { PendingSessionOptions, ProtectParams, ShowWhenCondition } from '@clerk/shared/types';
+import type { PendingSessionOptions, 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;
@@ -16,72 +10,6 @@ export type AppRouterShowProps = React.PropsWithChildren<
}
>;
-export async function SignedIn(
- props: React.PropsWithChildren,
-): Promise {
- const { children } = props;
- const { userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut });
- return userId ? <>{children}> : null;
-}
-
-export async function SignedOut(
- props: React.PropsWithChildren,
-): Promise {
- const { children } = props;
- const { userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut });
- return userId ? null : <>{children}>;
-}
-
-/**
- * Use `` in order to prevent unauthenticated or unauthorized users from accessing the children passed to the component.
- *
- * Examples:
- * ```
- *
- *
- * has({permission:"a_permission_key"})} />
- * has({role:"a_role_key"})} />
- * Unauthorized} />
- * ```
- */
-export async function Protect(props: AppRouterProtectProps): Promise {
- const { children, fallback, ...restAuthorizedParams } = props;
- const { has, userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut });
-
- /**
- * Fallback to UI provided by user or `null` if authorization checks failed
- */
- const unauthorized = fallback ? <>{fallback}> : null;
-
- const authorized = <>{children}>;
-
- if (!userId) {
- return unauthorized;
- }
-
- /**
- * Check against the results of `has` called inside the callback
- */
- if (typeof restAuthorizedParams.condition === 'function') {
- return restAuthorizedParams.condition(has) ? authorized : unauthorized;
- }
-
- if (
- restAuthorizedParams.role ||
- restAuthorizedParams.permission ||
- restAuthorizedParams.feature ||
- restAuthorizedParams.plan
- ) {
- return has(restAuthorizedParams) ? authorized : unauthorized;
- }
-
- /**
- * If neither of the authorization params are passed behave as the ``.
- * If fallback is present render that instead of rendering nothing.
- */
- return authorized;
-}
-
/**
* Use `` to render children when an authorization or sign-in condition passes.
* When `treatPendingAsSignedOut` is true, pending sessions are treated as signed out.
diff --git a/packages/nextjs/src/client-boundary/controlComponents.ts b/packages/nextjs/src/client-boundary/controlComponents.ts
index 9006fbc594e..544c2e10145 100644
--- a/packages/nextjs/src/client-boundary/controlComponents.ts
+++ b/packages/nextjs/src/client-boundary/controlComponents.ts
@@ -13,8 +13,6 @@ export {
RedirectToTasks,
RedirectToUserProfile,
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 1d6fd04d0e6..4635a9f1367 100644
--- a/packages/nextjs/src/components.client.ts
+++ b/packages/nextjs/src/components.client.ts
@@ -1,21 +1,2 @@
export { ClerkProvider } from './client-boundary/ClerkProvider';
-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.',
- );
-};
+export { Show } from './client-boundary/controlComponents';
diff --git a/packages/nextjs/src/components.server.ts b/packages/nextjs/src/components.server.ts
index 291aa1df659..11eab24d2e6 100644
--- a/packages/nextjs/src/components.server.ts
+++ b/packages/nextjs/src/components.server.ts
@@ -1,12 +1,9 @@
import { ClerkProvider } from './app-router/server/ClerkProvider';
-import { Protect, Show, SignedIn, SignedOut } from './app-router/server/controlComponents';
+import { Show } from './app-router/server/controlComponents';
-export { ClerkProvider, Protect, Show, SignedIn, SignedOut };
+export { ClerkProvider, Show };
export type ServerComponentsServerModuleTypes = {
ClerkProvider: typeof ClerkProvider;
- Protect: typeof Protect;
Show: typeof Show;
- SignedIn: typeof SignedIn;
- SignedOut: typeof SignedOut;
};
diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts
index 98f1ffa9698..c4123f6729c 100644
--- a/packages/nextjs/src/index.ts
+++ b/packages/nextjs/src/index.ts
@@ -14,7 +14,6 @@ export {
RedirectToSignUp,
RedirectToTasks,
RedirectToUserProfile,
- Show,
} from './client-boundary/controlComponents';
/**
@@ -74,10 +73,4 @@ 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 Show = ComponentsModule.Show as ServerComponentsServerModuleTypes['Show'];
diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts
index c5d42b4b6c3..0f0fb72e6f0 100644
--- a/packages/nuxt/src/module.ts
+++ b/packages/nuxt/src/module.ts
@@ -175,14 +175,12 @@ export default defineNuxtModule({
// Control Components
'ClerkLoaded',
'ClerkLoading',
- 'Protect',
'RedirectToSignIn',
'RedirectToSignUp',
'RedirectToUserProfile',
'RedirectToOrganizationProfile',
'RedirectToCreateOrganization',
- 'SignedIn',
- 'SignedOut',
+ 'Show',
'Waitlist',
];
otherComponents.forEach(component => {
diff --git a/packages/nuxt/src/runtime/components/index.ts b/packages/nuxt/src/runtime/components/index.ts
index 61bde896c00..5d4cf17560a 100644
--- a/packages/nuxt/src/runtime/components/index.ts
+++ b/packages/nuxt/src/runtime/components/index.ts
@@ -9,9 +9,7 @@ export {
// Control components
ClerkLoaded,
ClerkLoading,
- SignedOut,
- SignedIn,
- Protect,
+ Show,
RedirectToSignIn,
RedirectToSignUp,
RedirectToUserProfile,
diff --git a/packages/react/src/components/CheckoutButton.tsx b/packages/react/src/components/CheckoutButton.tsx
index f095bcc77ff..bc041c275be 100644
--- a/packages/react/src/components/CheckoutButton.tsx
+++ b/packages/react/src/components/CheckoutButton.tsx
@@ -7,27 +7,26 @@ import { assertSingleChild, normalizeWithDefaultValue, safeExecute } from '../ut
import { withClerk } from './withClerk';
/**
- * A button component that opens the Clerk Checkout drawer when clicked. This component must be rendered
- * inside a `` component to ensure the user is authenticated.
+ * A button component that opens the Clerk Checkout drawer when clicked. Render only when the user is signed in (e.g., wrap with ``).
*
* @example
* ```tsx
- * import { SignedIn } from '@clerk/react';
+ * import { Show } from '@clerk/react';
* import { CheckoutButton } from '@clerk/react/experimental';
*
* // Basic usage with default "Checkout" text
* function BasicCheckout() {
* return (
- *
+ *
*
- *
+ *
* );
* }
*
* // Custom button with organization subscription
* function OrganizationCheckout() {
* return (
- *
+ *
*
*
*
- *
+ *
* );
* }
* ```
*
- * @throws {Error} When rendered outside of a `` component
+ * @throws {Error} When rendered while the user is signed out
* @throws {Error} When `for="organization"` is used without an active organization context
*
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change. It is advised to [pin](https://clerk.com/docs/pinning) the SDK version and the clerk-js version to avoid breaking changes.
@@ -61,7 +60,9 @@ export const CheckoutButton = withClerk(
const { userId, orgId } = useAuth();
if (userId === null) {
- throw new Error('Clerk: Ensure that `` is rendered inside a `` component.');
+ throw new Error(
+ 'Clerk: Ensure that `` is rendered only when the user is signed in (wrap with `` or guard with `useAuth()`).',
+ );
}
if (orgId === null && _for === 'organization') {
diff --git a/packages/react/src/components/PlanDetailsButton.tsx b/packages/react/src/components/PlanDetailsButton.tsx
index 4ad2cb4ad1c..cfcd72b3d12 100644
--- a/packages/react/src/components/PlanDetailsButton.tsx
+++ b/packages/react/src/components/PlanDetailsButton.tsx
@@ -11,22 +11,22 @@ import { withClerk } from './withClerk';
*
* @example
* ```tsx
- * import { SignedIn } from '@clerk/react';
+ * import { Show } from '@clerk/react';
* import { PlanDetailsButton } from '@clerk/react/experimental';
*
* // Basic usage with default "Plan details" text
* function BasicPlanDetails() {
- * return (
- *
- * );
+ * return ;
* }
*
* // Custom button with custom text
* function CustomPlanDetails() {
* return (
- *
- *
- *
+ *
+ *
+ *
+ *
+ *
* );
* }
* ```
diff --git a/packages/react/src/components/SubscriptionDetailsButton.tsx b/packages/react/src/components/SubscriptionDetailsButton.tsx
index 59e04a35f43..bce5269942f 100644
--- a/packages/react/src/components/SubscriptionDetailsButton.tsx
+++ b/packages/react/src/components/SubscriptionDetailsButton.tsx
@@ -7,34 +7,34 @@ import { assertSingleChild, normalizeWithDefaultValue, safeExecute } from '../ut
import { withClerk } from './withClerk';
/**
- * A button component that opens the Clerk Subscription Details drawer when clicked. This component must be rendered inside a `` component to ensure the user is authenticated.
+ * A button component that opens the Clerk Subscription Details drawer when clicked. Render only when the user is signed in (e.g., wrap with ``).
*
* @example
* ```tsx
- * import { SignedIn } from '@clerk/react';
+ * import { Show } from '@clerk/react';
* import { SubscriptionDetailsButton } from '@clerk/react/experimental';
*
* // Basic usage with default "Subscription details" text
* function BasicSubscriptionDetails() {
- * return (
- *
- * );
+ * return ;
* }
*
* // Custom button with Organization Subscription
* function OrganizationSubscriptionDetails() {
* return (
- * console.log('Subscription canceled')}
- * >
- *
- *
+ *
+ * console.log('Subscription canceled')}
+ * >
+ *
+ *
+ *
* );
* }
* ```
*
- * @throws {Error} When rendered outside of a `` component
+ * @throws {Error} When rendered while the user is signed out
* @throws {Error} When `for="organization"` is used without an Active Organization context
*
* @experimental This is an experimental API for the Billing feature that is available under a public beta, and the API is subject to change. It is advised to [pin](https://clerk.com/docs/pinning) the SDK version and the clerk-js version to avoid breaking changes.
@@ -53,7 +53,7 @@ export const SubscriptionDetailsButton = withClerk(
if (userId === null) {
throw new Error(
- 'Clerk: Ensure that `` is rendered inside a `` component.',
+ 'Clerk: Ensure that `` is rendered only when the user is signed in (wrap with `` or guard with `useAuth()`).',
);
}
diff --git a/packages/react/src/components/__tests__/CheckoutButton.test.tsx b/packages/react/src/components/__tests__/CheckoutButton.test.tsx
index 94bbf8172c2..6a921c4a9a4 100644
--- a/packages/react/src/components/__tests__/CheckoutButton.test.tsx
+++ b/packages/react/src/components/__tests__/CheckoutButton.test.tsx
@@ -46,7 +46,7 @@ describe('CheckoutButton', () => {
// Expect the component to throw an error
expect(() => render()).toThrow(
- 'Ensure that `` is rendered inside a `` component.',
+ 'Ensure that `` is rendered only when the user is signed in (wrap with `` or guard with `useAuth()`).',
);
});
diff --git a/packages/react/src/components/__tests__/SubscriptionDetailsButton.test.tsx b/packages/react/src/components/__tests__/SubscriptionDetailsButton.test.tsx
index 96b2d479192..800cfa9ba13 100644
--- a/packages/react/src/components/__tests__/SubscriptionDetailsButton.test.tsx
+++ b/packages/react/src/components/__tests__/SubscriptionDetailsButton.test.tsx
@@ -46,7 +46,7 @@ describe('SubscriptionDetailsButton', () => {
// Expect the component to throw an error
expect(() => render()).toThrow(
- 'Ensure that `` is rendered inside a `` component.',
+ 'Ensure that `` is rendered only when the user is signed in (wrap with `` or guard with `useAuth()`).',
);
});
diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx
index b0c5f72f81d..eca08e7ec90 100644
--- a/packages/react/src/components/controlComponents.tsx
+++ b/packages/react/src/components/controlComponents.tsx
@@ -9,26 +9,6 @@ import { useAssertWrappedByClerkProvider } from '../hooks/useAssertWrappedByCler
import type { RedirectToSignInProps, RedirectToSignUpProps, RedirectToTasksProps, WithClerkProp } from '../types';
import { withClerk } from './withClerk';
-export const SignedIn = ({ children, treatPendingAsSignedOut }: React.PropsWithChildren) => {
- useAssertWrappedByClerkProvider('SignedIn');
-
- const { userId } = useAuth({ treatPendingAsSignedOut });
- if (userId) {
- return children;
- }
- return null;
-};
-
-export const SignedOut = ({ children, treatPendingAsSignedOut }: React.PropsWithChildren) => {
- useAssertWrappedByClerkProvider('SignedOut');
-
- const { userId } = useAuth({ treatPendingAsSignedOut });
- if (userId === null) {
- return children;
- }
- return null;
-};
-
export const ClerkLoaded = ({ children }: React.PropsWithChildren) => {
useAssertWrappedByClerkProvider('ClerkLoaded');
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index 247bb29ecd3..c200f386236 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -29,8 +29,6 @@ export {
RedirectToTasks,
RedirectToUserProfile,
Show,
- SignedIn,
- SignedOut,
} from './controlComponents';
export type { ShowProps } from './controlComponents';
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 5ffdb646778..569b47393c6 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
@@ -1,6 +1,6 @@
export const fixtures = [
{
- name: 'Basic import transform',
+ name: 'Transforms Protect import',
source: `
import { Protect } from "@clerk/react"
`,
@@ -9,32 +9,16 @@ import { Show } from "@clerk/react"
`,
},
{
- name: 'Import transform with other imports',
+ name: 'Transforms SignedIn and SignedOut imports',
source: `
-import { ClerkProvider, Protect, SignedIn } from "@clerk/react"
+import { SignedIn, SignedOut } 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"
+import { Show } from "@clerk/react"
`,
},
{
- name: 'Basic permission prop transform',
+ name: 'Transforms Protect in TSX',
source: `
import { Protect } from "@clerk/react"
@@ -61,68 +45,56 @@ function App() {
`,
},
{
- name: 'Basic role prop transform',
+ name: 'Transforms SignedIn usage',
source: `
-import { Protect } from "@clerk/react"
+import { SignedIn } from "@clerk/react"
-function App() {
- return (
-
-
-
- )
-}
+const App = () => (
+
+ Child
+
+)
`,
output: `
import { Show } from "@clerk/react"
-function App() {
- return (
-
-
-
- );
-}
+const App = () => (
+
+ Child
+
+);
`,
},
{
- name: 'Boolean shorthand auth prop transforms to true',
+ name: 'Transforms SignedOut usage',
source: `
-import { Protect } from "@clerk/react"
+import { SignedOut } from "@clerk/react"
-function App() {
- return (
-
-
-
- )
-}
+const App = () => (
+
+ Child
+
+)
`,
output: `
import { Show } from "@clerk/react"
-function App() {
- return (
-
-
-
- );
-}
+const App = () => (
+
+ Child
+
+);
`,
},
{
- name: 'Feature prop transform',
+ name: 'Transforms Protect condition callback',
source: `
import { Protect } from "@clerk/react"
function App() {
return (
-
-
+ has({ role: "admin" })}>
+
)
}
@@ -132,245 +104,139 @@ import { Show } from "@clerk/react"
function App() {
return (
-
-
+ has({ role: "admin" })}>
+
);
}
`,
},
{
- name: 'Plan prop transform',
+ name: 'Transforms SignedIn import with other specifiers',
source: `
-import { Protect } from "@clerk/react"
-
-function App() {
- return (
-
-
-
- )
-}
+import { ClerkProvider, SignedIn } from "@clerk/nextjs"
`,
output: `
-import { Show } from "@clerk/react"
-
-function App() {
- return (
-
-
-
- );
-}
+import { ClerkProvider, Show } from "@clerk/nextjs"
`,
},
{
- name: 'Condition prop transform',
+ name: 'Transforms ProtectProps type',
source: `
-import { Protect } from "@clerk/react"
-
-function App() {
- return (
- has({ permission: "org:read" })}>
-
-
- )
-}
+import { ProtectProps } from "@clerk/react";
+type Props = ProtectProps;
`,
output: `
-import { Show } from "@clerk/react"
-
-function App() {
- return (
- has({ permission: "org:read" })}>
-
-
- );
-}
+import { ShowProps } from "@clerk/react";
+type Props = ShowProps;
`,
},
{
- name: 'With fallback prop',
+ name: 'Self-closing Protect defaults to signedIn',
source: `
import { Protect } from "@clerk/react"
-function App() {
- return (
- }>
-
-
- )
-}
+const Thing = () =>
`,
output: `
import { Show } from "@clerk/react"
-function App() {
- return (
- }>
-
-
- );
-}
+const Thing = () =>
`,
},
{
- name: 'Self-closing Protect',
+ name: 'Transforms Protect from hybrid package without client directive',
source: `
-import { Protect } from "@clerk/react"
+import { Protect } from "@clerk/nextjs"
-function App() {
- return
-}
+const App = () => (
+
+ Child
+
+)
`,
output: `
-import { Show } from "@clerk/react"
-
-function App() {
- return (
-
- );
-}
+import { Show } from "@clerk/nextjs"
+
+const App = () => (
+
+ Child
+
+);
`,
},
{
- name: 'Handles directives',
- source: `"use client";
+ name: 'Transforms SignedOut to Show with fallback prop',
+ source: `
+import { SignedOut } from "@clerk/react"
-import { Protect } from "@clerk/nextjs";
+const App = () => (
+ }>
+ Child
+
+)
+ `,
+ output: `
+import { Show } from "@clerk/react"
-export function Protected() {
- return (
-
-
-
- );
-}
+const App = () => (
+ }>
+ Child
+
+);
`,
- output: `"use client";
-
-import { Show } from "@clerk/nextjs";
-
-export function Protected() {
- return (
-
-
-
- );
-}`,
},
{
- name: 'Dynamic permission value',
+ name: 'Aliased Protect import is transformed',
source: `
-import { Protect } from "@clerk/react"
+import { Protect as CanAccess } from "@clerk/react"
-function App({ requiredPermission }) {
+function App() {
return (
-
-
-
+
+
+
)
}
`,
output: `
-import { Show } from "@clerk/react"
+import { Show as CanAccess } from "@clerk/react"
-function App({ requiredPermission }) {
+function App() {
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 (
-
-
-
- );
-}
+ name: 'ProtectProps type aliases update',
+ source: `
+import { ProtectProps } from "@clerk/react";
+type Props = ProtectProps;
+type Another = ProtectProps;
+ `,
+ output: `
+import { ShowProps } from "@clerk/react";
+type Props = ShowProps;
+type Another = ShowProps;
`,
- output: `import { Show } from "@clerk/react";
-
-function Component() {
- return (
-
-
-
- );
-}`,
},
{
- name: 'Bare Protect defaults to signedIn',
+ name: 'Protect with fallback prop',
source: `
import { Protect } from "@clerk/react"
function App() {
return (
-
-
+ }>
+
)
}
@@ -380,24 +246,30 @@ import { Show } from "@clerk/react"
function App() {
return (
-
-
+ }>
+
);
}
`,
},
{
- name: 'ProtectProps import rewrites to ShowProps',
+ name: 'Protect with spread props',
source: `
-import { ProtectProps } from "@clerk/react";
+import { Protect } from "@clerk/react"
-type Props = ProtectProps;
+const props = { permission: "org:read" }
+const App = () =>
`,
output: `
-import { ShowProps } from "@clerk/react";
+import { Show } from "@clerk/react"
-type Props = ShowProps;
+const props = { permission: "org:read" }
+const App = () =>
`,
},
];
diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
index f0daf66c185..2be8f389ff1 100644
--- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs
+++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
@@ -3,46 +3,6 @@ const CLIENT_ONLY_PACKAGES = ['@clerk/chrome-extension', '@clerk/expo', '@clerk/
// 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.
*
@@ -55,10 +15,6 @@ function hasUseClientDirective(root, j) {
*
* 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
@@ -66,28 +22,9 @@ function hasUseClientDirective(root, j) {
module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) {
const root = j(source);
let dirtyFlag = false;
- const protectLocalNames = [];
+ const componentKindByLocalName = {};
const protectPropsLocalsToRename = [];
- 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, ProtectProps → ShowProps
const allPackages = [...CLIENT_ONLY_PACKAGES, ...HYBRID_PACKAGES];
allPackages.forEach(packageName => {
@@ -97,13 +34,17 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
specifiers.forEach(spec => {
if (j.ImportSpecifier.check(spec)) {
- if (spec.imported.name === 'Protect') {
- const originalImportedName = spec.imported.name;
+ const originalImportedName = spec.imported.name;
+
+ if (['Protect', 'SignedIn', 'SignedOut'].includes(originalImportedName)) {
const effectiveLocalName = spec.local ? spec.local.name : originalImportedName;
+ componentKindByLocalName[effectiveLocalName] =
+ originalImportedName === 'Protect'
+ ? 'protect'
+ : originalImportedName === 'SignedIn'
+ ? 'signedIn'
+ : 'signedOut';
spec.imported.name = 'Show';
- if (!protectLocalNames.includes(effectiveLocalName)) {
- protectLocalNames.push(effectiveLocalName);
- }
dirtyFlag = true;
}
@@ -146,16 +87,21 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
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)) {
+ // Check if this is a transformed control component
+ if (!j.JSXIdentifier.check(openingElement.name)) {
return;
}
const originalName = openingElement.name.name;
+ const kind = componentKindByLocalName[originalName];
+
+ if (!kind) {
+ return;
+ }
- // Only rename if the component was used without an alias (as ).
+ // 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') {
+ if (['Protect', 'SignedIn', 'SignedOut'].includes(originalName)) {
openingElement.name.name = 'Show';
if (closingElement && j.JSXIdentifier.check(closingElement.name)) {
closingElement.name.name = 'Show';
@@ -187,7 +133,9 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
// Build the `when` prop
let whenValue = null;
- if (conditionAttr) {
+ if (kind === 'signedIn' || kind === 'signedOut') {
+ whenValue = j.stringLiteral(kind === 'signedIn' ? 'signedIn' : 'signedOut');
+ } else if (conditionAttr) {
// condition prop becomes the when callback directly
whenValue = conditionAttr.value;
} else if (authAttributes.length > 0) {
@@ -216,7 +164,8 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
// Reconstruct attributes with `when` prop
const newAttributes = [];
- const finalWhenValue = whenValue || j.stringLiteral('signedIn');
+ const defaultWhenValue = kind === 'signedOut' ? 'signedOut' : 'signedIn';
+ const finalWhenValue = whenValue || j.stringLiteral(defaultWhenValue);
newAttributes.push(j.jsxAttribute(j.jsxIdentifier('when'), finalWhenValue));
diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts
index eeb7dd546d6..058eaeb1d03 100644
--- a/packages/vue/src/components/controlComponents.ts
+++ b/packages/vue/src/components/controlComponents.ts
@@ -2,28 +2,16 @@ import { deprecated } from '@clerk/shared/deprecated';
import type {
HandleOAuthCallbackParams,
PendingSessionOptions,
- ProtectParams,
+ ShowWhenCondition,
RedirectOptions,
} from '@clerk/shared/types';
-import { defineComponent } from 'vue';
+import { defineComponent, type VNodeChild } from 'vue';
import { useAuth } from '../composables/useAuth';
import { useClerk } from '../composables/useClerk';
import { useClerkContext } from '../composables/useClerkContext';
import { useClerkLoaded } from '../utils/useClerkLoaded';
-export const SignedIn = defineComponent(({ treatPendingAsSignedOut }, { slots }) => {
- const { userId } = useAuth({ treatPendingAsSignedOut });
-
- return () => (userId.value ? slots.default?.() : null);
-});
-
-export const SignedOut = defineComponent(({ treatPendingAsSignedOut }, { slots }) => {
- const { userId } = useAuth({ treatPendingAsSignedOut });
-
- return () => (userId.value === null ? slots.default?.() : null);
-});
-
export const ClerkLoaded = defineComponent((_, { slots }) => {
const clerk = useClerk();
@@ -112,9 +100,9 @@ export const AuthenticateWithRedirectCallback = defineComponent((props: HandleOA
return () => null;
});
-export type ProtectProps = ProtectParams & PendingSessionOptions;
+export type ShowProps = PendingSessionOptions & { fallback?: unknown; when: ShowWhenCondition };
-export const Protect = defineComponent((props: ProtectProps, { slots }) => {
+export const Show = defineComponent((props: ShowProps, { slots }) => {
const { isLoaded, has, userId } = useAuth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut });
return () => {
@@ -125,37 +113,28 @@ export const Protect = defineComponent((props: ProtectProps, { slots }) => {
return null;
}
- /**
- * Fallback to UI provided by user or `null` if authorization checks failed
- */
- if (!userId.value) {
- return slots.fallback?.();
- }
+ const authorized = (slots.default?.() ?? null) as VNodeChild | null;
+ const fallbackFromSlot = slots.fallback?.() ?? null;
+ const fallbackFromProp = (props.fallback as VNodeChild | null | undefined) ?? null;
+ const unauthorized = (fallbackFromSlot ?? fallbackFromProp ?? null) as VNodeChild | null;
- /**
- * Check against the results of `has` called inside the callback
- */
- if (typeof props.condition === 'function') {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- if (props.condition(has.value!)) {
- return slots.default?.();
- }
+ if (props.when === 'signedOut') {
+ return userId.value ? unauthorized : authorized;
+ }
- return slots.fallback?.();
+ if (!userId.value) {
+ return unauthorized;
}
- if (props.role || props.permission || props.feature || props.plan) {
- if (has.value?.(props)) {
- return slots.default?.();
- }
+ if (props.when === 'signedIn') {
+ return authorized;
+ }
- return slots.fallback?.();
+ if (typeof props.when === 'function') {
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return props.when(has.value!) ? authorized : unauthorized;
}
- /**
- * If neither of the authorization params are passed behave as the ``.
- * If fallback is present render that instead of rendering nothing.
- */
- return slots.default?.();
+ return has.value?.(props.when) ? authorized : unauthorized;
};
});
diff --git a/packages/vue/src/components/index.ts b/packages/vue/src/components/index.ts
index 65c8398137f..2aaa15af860 100644
--- a/packages/vue/src/components/index.ts
+++ b/packages/vue/src/components/index.ts
@@ -14,9 +14,7 @@ export { UserButton } from './ui-components/UserButton';
export {
ClerkLoaded,
ClerkLoading,
- SignedOut,
- SignedIn,
- Protect,
+ Show,
RedirectToSignIn,
RedirectToSignUp,
RedirectToUserProfile,
diff --git a/playground/app-router/src/app/protected/page.tsx b/playground/app-router/src/app/protected/page.tsx
index b93598f1d56..1d41a58bf40 100644
--- a/playground/app-router/src/app/protected/page.tsx
+++ b/playground/app-router/src/app/protected/page.tsx
@@ -1,4 +1,4 @@
-import { ClerkLoaded, SignedIn, SignedOut, UserButton } from '@clerk/nextjs';
+import { ClerkLoaded, Show, UserButton } from '@clerk/nextjs';
import { auth } from '@clerk/nextjs/server';
import React from 'react';
import { ClientSideWrapper } from '@/app/protected/ClientSideWrapper';
@@ -13,12 +13,12 @@ export default async function Page() {
Protected page
-
+
Signed in
-
-
+
+
Signed out
-
+
Clerk loaded
@@ -26,9 +26,9 @@ export default async function Page() {
server content
-
+
SignedIn
-
+
ClerkLoaded
diff --git a/playground/app-router/src/pages/user/[[...index]].tsx b/playground/app-router/src/pages/user/[[...index]].tsx
index 965be25b361..391f19f3f0c 100644
--- a/playground/app-router/src/pages/user/[[...index]].tsx
+++ b/playground/app-router/src/pages/user/[[...index]].tsx
@@ -1,4 +1,4 @@
-import { SignedIn, UserProfile } from '@clerk/nextjs';
+import { Show, UserProfile } from '@clerk/nextjs';
import { getAuth } from '@clerk/nextjs/server';
import type { GetServerSideProps, NextPage } from 'next';
import React from 'react';
@@ -14,9 +14,9 @@ const UserProfilePage: NextPage = (props: any) => {
/pages/user
{props.message}
-
+
SignedIn
-
+
);
diff --git a/playground/browser-extension/src/components/nav-bar.tsx b/playground/browser-extension/src/components/nav-bar.tsx
index 828fc565a93..6d422d38b46 100644
--- a/playground/browser-extension/src/components/nav-bar.tsx
+++ b/playground/browser-extension/src/components/nav-bar.tsx
@@ -1,11 +1,11 @@
-import { SignedIn, SignedOut, UserButton } from "@clerk/chrome-extension"
+import { Show, UserButton } from "@clerk/chrome-extension"
import { Link } from "react-router-dom"
import { Button } from "./ui/button"
export const NavBar = () => {
return (
<>
-
+
-
-
+
+
-
+
>
)
diff --git a/playground/expo/App.tsx b/playground/expo/App.tsx
index ffa3ce37f24..d6a5d988cb3 100644
--- a/playground/expo/App.tsx
+++ b/playground/expo/App.tsx
@@ -1,4 +1,4 @@
-import { ClerkProvider, SignedIn, SignedOut, useAuth, useSignIn, useUser } from '@clerk/expo';
+import { ClerkProvider, Show, useAuth, useSignIn, useUser } from '@clerk/expo';
import { passkeys } from '@clerk/expo/passkeys';
import * as SecureStore from 'expo-secure-store';
import React from 'react';
@@ -145,12 +145,12 @@ export default function App() {
__experimental_passkeys={passkeys}
>
-
+
-
-
+
+
-
+
);
diff --git a/playground/nextjs/app/app-dir/client/page.tsx b/playground/nextjs/app/app-dir/client/page.tsx
index 5baa35ba0b2..e6c100e337f 100644
--- a/playground/nextjs/app/app-dir/client/page.tsx
+++ b/playground/nextjs/app/app-dir/client/page.tsx
@@ -1,13 +1,13 @@
'use client';
-import { SignedIn, SignedOut } from '@clerk/nextjs';
+import { Show } from '@clerk/nextjs';
export default function Page() {
return (
{/* @ts-ignore */}
- Hello In
+ Hello In
{/* @ts-ignore */}
- Hello Out
+ Hello Out
);
}
diff --git a/playground/nextjs/app/app-dir/page.tsx b/playground/nextjs/app/app-dir/page.tsx
index 28b60975ec7..d5a773b6b36 100644
--- a/playground/nextjs/app/app-dir/page.tsx
+++ b/playground/nextjs/app/app-dir/page.tsx
@@ -1,4 +1,4 @@
-import { OrganizationSwitcher, SignedIn, SignedOut, SignIn, UserButton } from '@clerk/nextjs';
+import { OrganizationSwitcher, Show, SignIn, UserButton } from '@clerk/nextjs';
import { auth, clerkClient, currentUser } from '@clerk/nextjs/server';
import Link from 'next/link';
@@ -27,7 +27,7 @@ export default async function Page() {
Hello, Next.js!
{userId ? Signed in as: {userId}
: Signed out
}
{/* @ts-ignore */}
-
+
{JSON.stringify(user)}
{JSON.stringify(currentUser_)}
-
+
{/* @ts-ignore */}
-
+
-
+
);
diff --git a/playground/nextjs/pages/_app.tsx b/playground/nextjs/pages/_app.tsx
index 2aa8a84e7cf..88b9b4ded35 100644
--- a/playground/nextjs/pages/_app.tsx
+++ b/playground/nextjs/pages/_app.tsx
@@ -4,8 +4,7 @@ import '../styles/globals.css';
import {
ClerkProvider,
OrganizationSwitcher,
- SignedIn,
- SignedOut,
+ Show,
SignInButton,
SignOutButton,
UserButton,
@@ -156,14 +155,14 @@ const AppBar = (props: AppBarProps) => {
{/* @ts-ignore */}
-
+
-
+
{/* @ts-ignore */}
-
+
-
+
);
};
diff --git a/playground/react-router/app/root.tsx b/playground/react-router/app/root.tsx
index bb6fb1e5f66..983723cb1a3 100644
--- a/playground/react-router/app/root.tsx
+++ b/playground/react-router/app/root.tsx
@@ -7,7 +7,7 @@ import {
ScrollRestoration,
} from "react-router";
import { rootAuthLoader } from "@clerk/react-router/ssr.server";
-import { ClerkProvider, SignedIn, SignedOut, UserButton, SignInButton } from "@clerk/react-router";
+import { ClerkProvider, Show, SignInButton, UserButton } from "@clerk/react-router";
import type { Route } from "./+types/root";
import stylesheet from "./app.css?url";
@@ -52,12 +52,12 @@ export default function App({ loaderData }: Route.ComponentProps) {
return (
diff --git a/playground/vite-react-ts/src/App.tsx b/playground/vite-react-ts/src/App.tsx
index 14bea78dc23..acca91648c3 100644
--- a/playground/vite-react-ts/src/App.tsx
+++ b/playground/vite-react-ts/src/App.tsx
@@ -1,8 +1,7 @@
import {
ClerkProvider,
RedirectToSignIn,
- SignedIn,
- SignedOut,
+ Show,
SignIn,
SignUp,
UserButton,
@@ -126,12 +125,12 @@ function ClerkProviderWithRoutes() {
path='/protected'
element={
<>
-
+
-
-
+
+
-
+
>
}
/>
From 6fc15a381c41494d3de28cc178a4260c3e529714 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Thu, 11 Dec 2025 15:31:42 -0600
Subject: [PATCH 22/33] update codemods
---
.../transform-protect-to-show.fixtures.js | 56 +++++++++++--
.../codemods/transform-protect-to-show.cjs | 81 ++++++++++++++++---
2 files changed, 117 insertions(+), 20 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 569b47393c6..4b40915c4df 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
@@ -14,7 +14,7 @@ import { Show } from "@clerk/react"
import { SignedIn, SignedOut } from "@clerk/react"
`,
output: `
-import { Show } from "@clerk/react"
+import { Show } from "@clerk/react";
`,
},
{
@@ -62,7 +62,7 @@ const App = () => (
Child
-);
+)
`,
},
{
@@ -83,7 +83,28 @@ const App = () => (
Child
-);
+)
+`,
+ },
+ {
+ name: 'Transforms SignedIn namespace import',
+ source: `
+import * as Clerk from "@clerk/react"
+
+const App = () => (
+
+ Child
+
+)
+ `,
+ output: `
+import * as Clerk from "@clerk/react"
+
+const App = () => (
+
+ Child
+
+)
`,
},
{
@@ -164,7 +185,7 @@ const App = () => (
}}>
Child
-);
+)
`,
},
{
@@ -185,7 +206,28 @@ const App = () => (
}>
Child
-);
+)
+`,
+ },
+ {
+ name: 'Transforms SignedOut namespace import with fallback',
+ source: `
+import * as Clerk from "@clerk/react"
+
+const App = () => (
+ }>
+ Child
+
+)
+ `,
+ output: `
+import * as Clerk from "@clerk/react"
+
+const App = () => (
+ }>
+ Child
+
+)
`,
},
{
@@ -267,9 +309,7 @@ const App = () =>
import { Show } from "@clerk/react"
const props = { permission: "org:read" }
-const App = () =>
+const App = () =>
`,
},
];
diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
index 2be8f389ff1..cc50aef7416 100644
--- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs
+++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
@@ -12,6 +12,8 @@ const HYBRID_PACKAGES = ['@clerk/astro', '@clerk/nextjs'];
* - `` → ``
* - `` → ``
* - ` ...}>` → ` ...}>`
+ * - `...` → `...`
+ * - `...` → `...`
*
* Also updates imports from `Protect` to `Show`.
*
@@ -24,6 +26,7 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
let dirtyFlag = false;
const componentKindByLocalName = {};
const protectPropsLocalsToRename = [];
+ const namespaceImports = new Set();
// Transform imports: Protect → Show, ProtectProps → ShowProps
const allPackages = [...CLIENT_ONLY_PACKAGES, ...HYBRID_PACKAGES];
@@ -33,6 +36,13 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
const specifiers = node.specifiers || [];
specifiers.forEach(spec => {
+ if (j.ImportNamespaceSpecifier.check(spec)) {
+ if (spec.local?.name) {
+ namespaceImports.add(spec.local.name);
+ }
+ return;
+ }
+
if (j.ImportSpecifier.check(spec)) {
const originalImportedName = spec.imported.name;
@@ -45,6 +55,9 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
? 'signedIn'
: 'signedOut';
spec.imported.name = 'Show';
+ if (spec.local && spec.local.name === originalImportedName) {
+ spec.local.name = 'Show';
+ }
dirtyFlag = true;
}
@@ -61,6 +74,28 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
}
}
});
+
+ const seenLocalNames = new Set();
+ node.specifiers = specifiers.reduce((acc, spec) => {
+ let localName = null;
+
+ if (spec.local && j.Identifier.check(spec.local)) {
+ localName = spec.local.name;
+ } else if (j.ImportSpecifier.check(spec) && j.Identifier.check(spec.imported)) {
+ localName = spec.imported.name;
+ }
+
+ if (localName) {
+ if (seenLocalNames.has(localName)) {
+ dirtyFlag = true;
+ return acc;
+ }
+ seenLocalNames.add(localName);
+ }
+
+ acc.push(spec);
+ return acc;
+ }, []);
});
});
@@ -87,24 +122,46 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j })
const openingElement = path.node.openingElement;
const closingElement = path.node.closingElement;
- // Check if this is a transformed control component
- if (!j.JSXIdentifier.check(openingElement.name)) {
- return;
- }
+ let kind = null;
+ let renameNodeToShow = null;
- const originalName = openingElement.name.name;
- const kind = componentKindByLocalName[originalName];
+ if (j.JSXIdentifier.check(openingElement.name)) {
+ const originalName = openingElement.name.name;
+ kind = componentKindByLocalName[originalName];
+
+ if (['Protect', 'SignedIn', 'SignedOut'].includes(originalName)) {
+ renameNodeToShow = node => {
+ if (j.JSXIdentifier.check(node)) {
+ node.name = 'Show';
+ }
+ };
+ }
+ } else if (j.JSXMemberExpression.check(openingElement.name)) {
+ const member = openingElement.name;
+ if (j.Identifier.check(member.object) && j.Identifier.check(member.property)) {
+ const objectName = member.object.name;
+ const propertyName = member.property.name;
+
+ if (namespaceImports.has(objectName) && ['Protect', 'SignedIn', 'SignedOut'].includes(propertyName)) {
+ kind = propertyName === 'Protect' ? 'protect' : propertyName === 'SignedIn' ? 'signedIn' : 'signedOut';
+
+ renameNodeToShow = node => {
+ if (j.JSXMemberExpression.check(node) && j.Identifier.check(node.property)) {
+ node.property.name = 'Show';
+ }
+ };
+ }
+ }
+ }
if (!kind) {
return;
}
- // 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 (['Protect', 'SignedIn', 'SignedOut'].includes(originalName)) {
- openingElement.name.name = 'Show';
- if (closingElement && j.JSXIdentifier.check(closingElement.name)) {
- closingElement.name.name = 'Show';
+ if (renameNodeToShow) {
+ renameNodeToShow(openingElement.name);
+ if (closingElement && closingElement.name) {
+ renameNodeToShow(closingElement.name);
}
}
From 826a9cbae11ec56230e129f19ce9ae0cd36e755b Mon Sep 17 00:00:00 2001
From: Jacek
Date: Thu, 11 Dec 2025 15:42:27 -0600
Subject: [PATCH 23/33] wip
---
packages/vue/src/components/controlComponents.ts | 11 ++++++++---
playground/nextjs/app/app-dir/client/page.tsx | 2 --
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts
index 058eaeb1d03..e6f925ee0a8 100644
--- a/packages/vue/src/components/controlComponents.ts
+++ b/packages/vue/src/components/controlComponents.ts
@@ -130,11 +130,16 @@ export const Show = defineComponent((props: ShowProps, { slots }) => {
return authorized;
}
+ const hasValue = has.value;
+
+ if (!hasValue) {
+ return unauthorized;
+ }
+
if (typeof props.when === 'function') {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- return props.when(has.value!) ? authorized : unauthorized;
+ return props.when(hasValue) ? authorized : unauthorized;
}
- return has.value?.(props.when) ? authorized : unauthorized;
+ return hasValue(props.when) ? authorized : unauthorized;
};
});
diff --git a/playground/nextjs/app/app-dir/client/page.tsx b/playground/nextjs/app/app-dir/client/page.tsx
index e6c100e337f..6191257178e 100644
--- a/playground/nextjs/app/app-dir/client/page.tsx
+++ b/playground/nextjs/app/app-dir/client/page.tsx
@@ -4,9 +4,7 @@ import { Show } from '@clerk/nextjs';
export default function Page() {
return (
- {/* @ts-ignore */}
Hello In
- {/* @ts-ignore */}
Hello Out
);
From 9121fbcbc84dd709387d28a60e46dbedb3a0d7f3 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Thu, 11 Dec 2025 15:58:20 -0600
Subject: [PATCH 24/33] wip
---
.../vue/src/components/controlComponents.ts | 21 ++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts
index e6f925ee0a8..8422a75b1eb 100644
--- a/packages/vue/src/components/controlComponents.ts
+++ b/packages/vue/src/components/controlComponents.ts
@@ -2,8 +2,8 @@ import { deprecated } from '@clerk/shared/deprecated';
import type {
HandleOAuthCallbackParams,
PendingSessionOptions,
- ShowWhenCondition,
RedirectOptions,
+ ShowWhenCondition,
} from '@clerk/shared/types';
import { defineComponent, type VNodeChild } from 'vue';
@@ -100,6 +100,25 @@ export const AuthenticateWithRedirectCallback = defineComponent((props: HandleOA
return () => null;
});
+/**
+ * Props for `` that control when content renders based on sign-in or authorization state.
+ *
+ * @public
+ * @property fallback Optional content shown when the condition fails; can be provided via prop or `fallback` slot.
+ * @property when Condition controlling visibility; supports `"signedIn"`, `"signedOut"`, authorization descriptors, or a predicate that receives the `has` helper.
+ * @property treatPendingAsSignedOut Inherited from `PendingSessionOptions`; treat pending sessions as signed out while loading.
+ * @example
+ * ```vue
+ *
+ *
+ *
+ *
+ *
+ * Not authorized
+ *
+ *
+ * ```
+ */
export type ShowProps = PendingSessionOptions & { fallback?: unknown; when: ShowWhenCondition };
export const Show = defineComponent((props: ShowProps, { slots }) => {
From 01afa556de6490b3536cde2f251134b0e105dd54 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Thu, 11 Dec 2025 19:35:33 -0600
Subject: [PATCH 25/33] fix vue tests
---
integration/templates/vue-vite/src/App.vue | 10 +++++-----
integration/templates/vue-vite/src/views/Admin.vue | 6 +++---
integration/templates/vue-vite/src/views/Home.vue | 14 ++++++++------
.../vue-vite/src/views/billing/CheckoutBtn.vue | 6 +++---
integration/tests/vue/components.test.ts | 2 +-
5 files changed, 20 insertions(+), 18 deletions(-)
diff --git a/integration/templates/vue-vite/src/App.vue b/integration/templates/vue-vite/src/App.vue
index 6477a90213f..c0c615dd2ec 100644
--- a/integration/templates/vue-vite/src/App.vue
+++ b/integration/templates/vue-vite/src/App.vue
@@ -1,5 +1,5 @@
@@ -11,12 +11,12 @@ import LanguagePicker from './components/LanguagePicker.vue';
Vue Clerk Integration test
-
+
-
-
+
+
Sign in
-
+
diff --git a/integration/templates/vue-vite/src/views/Admin.vue b/integration/templates/vue-vite/src/views/Admin.vue
index cda8c50afb7..1a685a48e50 100644
--- a/integration/templates/vue-vite/src/views/Admin.vue
+++ b/integration/templates/vue-vite/src/views/Admin.vue
@@ -1,12 +1,12 @@
-
+
I am an admin
Not an admin
-
+
diff --git a/integration/templates/vue-vite/src/views/Home.vue b/integration/templates/vue-vite/src/views/Home.vue
index e12e3680290..e89dbf87707 100644
--- a/integration/templates/vue-vite/src/views/Home.vue
+++ b/integration/templates/vue-vite/src/views/Home.vue
@@ -1,16 +1,18 @@
-
+
-
+
- - Profile
+
+ -
+ Profile
+
+
diff --git a/integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue b/integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue
index 39c23365733..70c7dbd545e 100644
--- a/integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue
+++ b/integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue
@@ -1,17 +1,17 @@
-
+
Checkout Now
-
+
diff --git a/integration/tests/vue/components.test.ts b/integration/tests/vue/components.test.ts
index c803a6adc6b..c5aa518a358 100644
--- a/integration/tests/vue/components.test.ts
+++ b/integration/tests/vue/components.test.ts
@@ -259,7 +259,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te
await u.po.signIn.waitForMounted();
});
- test('renders component contents to admins', async ({ page, context }) => {
+ test('renders guard contents to admins', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToRelative('/sign-in');
await u.po.signIn.waitForMounted();
From 5d6149beed81a3f70ddd41bec6e707b2f18a2e13 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Thu, 11 Dec 2025 19:59:21 -0600
Subject: [PATCH 26/33] wip
---
.../__snapshots__/exports.test.ts.snap | 2 -
.../__snapshots__/exports.test.ts.snap | 2 -
.../src/client/ReactRouterClerkProvider.tsx | 91 ++++++++++++++++++-
.../__snapshots__/exports.test.ts.snap | 2 -
.../src/client/ClerkProvider.tsx | 91 ++++++++++++++++++-
5 files changed, 180 insertions(+), 8 deletions(-)
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 01e780dea00..9848db006d1 100644
--- a/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap
+++ b/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -27,8 +27,6 @@ exports[`public exports > should not include a breaking change 1`] = `
"SignOutButton",
"SignUp",
"SignUpButton",
- "SignedIn",
- "SignedOut",
"UserAvatar",
"UserButton",
"UserProfile",
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 f3e3a74564e..b1fb6544b7b 100644
--- a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap
+++ b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -42,8 +42,6 @@ exports[`root public exports > should not change unexpectedly 1`] = `
"SignOutButton",
"SignUp",
"SignUpButton",
- "SignedIn",
- "SignedOut",
"TaskChooseOrganization",
"TaskResetPassword",
"UserAvatar",
diff --git a/packages/react-router/src/client/ReactRouterClerkProvider.tsx b/packages/react-router/src/client/ReactRouterClerkProvider.tsx
index 33ea4406868..024499414ee 100644
--- a/packages/react-router/src/client/ReactRouterClerkProvider.tsx
+++ b/packages/react-router/src/client/ReactRouterClerkProvider.tsx
@@ -1,3 +1,48 @@
+import {
+ APIKeys,
+ AuthenticateWithRedirectCallback,
+ ClerkDegraded,
+ ClerkFailed,
+ ClerkLoaded,
+ ClerkLoading,
+ CreateOrganization,
+ GoogleOneTap,
+ OrganizationList,
+ OrganizationSwitcher,
+ PricingTable,
+ RedirectToCreateOrganization,
+ RedirectToOrganizationProfile,
+ RedirectToSignIn,
+ RedirectToSignUp,
+ RedirectToTasks,
+ RedirectToUserProfile,
+ Show,
+ SignInButton,
+ SignInWithMetamaskButton,
+ SignOutButton,
+ SignUpButton,
+ TaskChooseOrganization,
+ TaskResetPassword,
+ UserAvatar,
+ UserButton,
+ Waitlist,
+ __experimental_CheckoutProvider,
+ __experimental_PaymentElement,
+ __experimental_PaymentElementProvider,
+ __experimental_useCheckout,
+ __experimental_usePaymentElement,
+ useAuth,
+ useClerk,
+ useEmailLink,
+ useOrganization,
+ useOrganizationList,
+ useReverification,
+ useSession,
+ useSessionList,
+ useSignIn,
+ useSignUp,
+ useUser,
+} from '@clerk/react';
import { ClerkProvider as ReactClerkProvider } from '@clerk/react';
import type { Ui } from '@clerk/react/internal';
import React from 'react';
@@ -12,7 +57,51 @@ import { ClerkReactRouterOptionsProvider } from './ReactRouterOptionsContext';
import type { ClerkState, ReactRouterClerkProviderProps } from './types';
import { useAwaitableNavigate } from './useAwaitableNavigate';
-export * from '@clerk/react';
+export {
+ APIKeys,
+ AuthenticateWithRedirectCallback,
+ ClerkDegraded,
+ ClerkFailed,
+ ClerkLoaded,
+ ClerkLoading,
+ CreateOrganization,
+ GoogleOneTap,
+ OrganizationList,
+ OrganizationSwitcher,
+ PricingTable,
+ RedirectToCreateOrganization,
+ RedirectToOrganizationProfile,
+ RedirectToSignIn,
+ RedirectToSignUp,
+ RedirectToTasks,
+ RedirectToUserProfile,
+ Show,
+ SignInButton,
+ SignInWithMetamaskButton,
+ SignOutButton,
+ SignUpButton,
+ TaskChooseOrganization,
+ TaskResetPassword,
+ UserAvatar,
+ UserButton,
+ Waitlist,
+ __experimental_CheckoutProvider,
+ __experimental_PaymentElement,
+ __experimental_PaymentElementProvider,
+ __experimental_useCheckout,
+ __experimental_usePaymentElement,
+ useAuth,
+ useClerk,
+ useEmailLink,
+ useOrganization,
+ useOrganizationList,
+ useReverification,
+ useSession,
+ useSessionList,
+ useSignIn,
+ useSignUp,
+ useUser,
+};
const SDK_METADATA = {
name: PACKAGE_NAME,
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 42a6ab133df..eaba504c812 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
@@ -47,8 +47,6 @@ exports[`root public exports > should not change unexpectedly 1`] = `
"SignOutButton",
"SignUp",
"SignUpButton",
- "SignedIn",
- "SignedOut",
"TaskChooseOrganization",
"TaskResetPassword",
"UserAvatar",
diff --git a/packages/tanstack-react-start/src/client/ClerkProvider.tsx b/packages/tanstack-react-start/src/client/ClerkProvider.tsx
index 74d4702eeff..96deaa93cd3 100644
--- a/packages/tanstack-react-start/src/client/ClerkProvider.tsx
+++ b/packages/tanstack-react-start/src/client/ClerkProvider.tsx
@@ -1,3 +1,48 @@
+import {
+ APIKeys,
+ AuthenticateWithRedirectCallback,
+ ClerkDegraded,
+ ClerkFailed,
+ ClerkLoaded,
+ ClerkLoading,
+ CreateOrganization,
+ GoogleOneTap,
+ OrganizationList,
+ OrganizationSwitcher,
+ PricingTable,
+ RedirectToCreateOrganization,
+ RedirectToOrganizationProfile,
+ RedirectToSignIn,
+ RedirectToSignUp,
+ RedirectToTasks,
+ RedirectToUserProfile,
+ Show,
+ SignInButton,
+ SignInWithMetamaskButton,
+ SignOutButton,
+ SignUpButton,
+ TaskChooseOrganization,
+ TaskResetPassword,
+ UserAvatar,
+ UserButton,
+ Waitlist,
+ __experimental_CheckoutProvider,
+ __experimental_PaymentElement,
+ __experimental_PaymentElementProvider,
+ __experimental_useCheckout,
+ __experimental_usePaymentElement,
+ useAuth,
+ useClerk,
+ useEmailLink,
+ useOrganization,
+ useOrganizationList,
+ useReverification,
+ useSession,
+ useSessionList,
+ useSignIn,
+ useSignUp,
+ useUser,
+} from '@clerk/react';
import { ClerkProvider as ReactClerkProvider } from '@clerk/react';
import type { Ui } from '@clerk/react/internal';
import { ScriptOnce } from '@tanstack/react-router';
@@ -10,7 +55,51 @@ import type { TanstackStartClerkProviderProps } from './types';
import { useAwaitableNavigate } from './useAwaitableNavigate';
import { mergeWithPublicEnvs, pickFromClerkInitState } from './utils';
-export * from '@clerk/react';
+export {
+ APIKeys,
+ AuthenticateWithRedirectCallback,
+ ClerkDegraded,
+ ClerkFailed,
+ ClerkLoaded,
+ ClerkLoading,
+ CreateOrganization,
+ GoogleOneTap,
+ OrganizationList,
+ OrganizationSwitcher,
+ PricingTable,
+ RedirectToCreateOrganization,
+ RedirectToOrganizationProfile,
+ RedirectToSignIn,
+ RedirectToSignUp,
+ RedirectToTasks,
+ RedirectToUserProfile,
+ Show,
+ SignInButton,
+ SignInWithMetamaskButton,
+ SignOutButton,
+ SignUpButton,
+ TaskChooseOrganization,
+ TaskResetPassword,
+ UserAvatar,
+ UserButton,
+ Waitlist,
+ __experimental_CheckoutProvider,
+ __experimental_PaymentElement,
+ __experimental_PaymentElementProvider,
+ __experimental_useCheckout,
+ __experimental_usePaymentElement,
+ useAuth,
+ useClerk,
+ useEmailLink,
+ useOrganization,
+ useOrganizationList,
+ useReverification,
+ useSession,
+ useSessionList,
+ useSignIn,
+ useSignUp,
+ useUser,
+};
const SDK_METADATA = {
name: PACKAGE_NAME,
From 4657f99b4efad79c3b680cfcef1b93d24d868513 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Thu, 11 Dec 2025 20:17:38 -0600
Subject: [PATCH 27/33] wip
---
.../src/client/ReactRouterClerkProvider.tsx | 91 +------------------
.../src/client/ClerkProvider.tsx | 91 +------------------
2 files changed, 2 insertions(+), 180 deletions(-)
diff --git a/packages/react-router/src/client/ReactRouterClerkProvider.tsx b/packages/react-router/src/client/ReactRouterClerkProvider.tsx
index 024499414ee..33ea4406868 100644
--- a/packages/react-router/src/client/ReactRouterClerkProvider.tsx
+++ b/packages/react-router/src/client/ReactRouterClerkProvider.tsx
@@ -1,48 +1,3 @@
-import {
- APIKeys,
- AuthenticateWithRedirectCallback,
- ClerkDegraded,
- ClerkFailed,
- ClerkLoaded,
- ClerkLoading,
- CreateOrganization,
- GoogleOneTap,
- OrganizationList,
- OrganizationSwitcher,
- PricingTable,
- RedirectToCreateOrganization,
- RedirectToOrganizationProfile,
- RedirectToSignIn,
- RedirectToSignUp,
- RedirectToTasks,
- RedirectToUserProfile,
- Show,
- SignInButton,
- SignInWithMetamaskButton,
- SignOutButton,
- SignUpButton,
- TaskChooseOrganization,
- TaskResetPassword,
- UserAvatar,
- UserButton,
- Waitlist,
- __experimental_CheckoutProvider,
- __experimental_PaymentElement,
- __experimental_PaymentElementProvider,
- __experimental_useCheckout,
- __experimental_usePaymentElement,
- useAuth,
- useClerk,
- useEmailLink,
- useOrganization,
- useOrganizationList,
- useReverification,
- useSession,
- useSessionList,
- useSignIn,
- useSignUp,
- useUser,
-} from '@clerk/react';
import { ClerkProvider as ReactClerkProvider } from '@clerk/react';
import type { Ui } from '@clerk/react/internal';
import React from 'react';
@@ -57,51 +12,7 @@ import { ClerkReactRouterOptionsProvider } from './ReactRouterOptionsContext';
import type { ClerkState, ReactRouterClerkProviderProps } from './types';
import { useAwaitableNavigate } from './useAwaitableNavigate';
-export {
- APIKeys,
- AuthenticateWithRedirectCallback,
- ClerkDegraded,
- ClerkFailed,
- ClerkLoaded,
- ClerkLoading,
- CreateOrganization,
- GoogleOneTap,
- OrganizationList,
- OrganizationSwitcher,
- PricingTable,
- RedirectToCreateOrganization,
- RedirectToOrganizationProfile,
- RedirectToSignIn,
- RedirectToSignUp,
- RedirectToTasks,
- RedirectToUserProfile,
- Show,
- SignInButton,
- SignInWithMetamaskButton,
- SignOutButton,
- SignUpButton,
- TaskChooseOrganization,
- TaskResetPassword,
- UserAvatar,
- UserButton,
- Waitlist,
- __experimental_CheckoutProvider,
- __experimental_PaymentElement,
- __experimental_PaymentElementProvider,
- __experimental_useCheckout,
- __experimental_usePaymentElement,
- useAuth,
- useClerk,
- useEmailLink,
- useOrganization,
- useOrganizationList,
- useReverification,
- useSession,
- useSessionList,
- useSignIn,
- useSignUp,
- useUser,
-};
+export * from '@clerk/react';
const SDK_METADATA = {
name: PACKAGE_NAME,
diff --git a/packages/tanstack-react-start/src/client/ClerkProvider.tsx b/packages/tanstack-react-start/src/client/ClerkProvider.tsx
index 96deaa93cd3..74d4702eeff 100644
--- a/packages/tanstack-react-start/src/client/ClerkProvider.tsx
+++ b/packages/tanstack-react-start/src/client/ClerkProvider.tsx
@@ -1,48 +1,3 @@
-import {
- APIKeys,
- AuthenticateWithRedirectCallback,
- ClerkDegraded,
- ClerkFailed,
- ClerkLoaded,
- ClerkLoading,
- CreateOrganization,
- GoogleOneTap,
- OrganizationList,
- OrganizationSwitcher,
- PricingTable,
- RedirectToCreateOrganization,
- RedirectToOrganizationProfile,
- RedirectToSignIn,
- RedirectToSignUp,
- RedirectToTasks,
- RedirectToUserProfile,
- Show,
- SignInButton,
- SignInWithMetamaskButton,
- SignOutButton,
- SignUpButton,
- TaskChooseOrganization,
- TaskResetPassword,
- UserAvatar,
- UserButton,
- Waitlist,
- __experimental_CheckoutProvider,
- __experimental_PaymentElement,
- __experimental_PaymentElementProvider,
- __experimental_useCheckout,
- __experimental_usePaymentElement,
- useAuth,
- useClerk,
- useEmailLink,
- useOrganization,
- useOrganizationList,
- useReverification,
- useSession,
- useSessionList,
- useSignIn,
- useSignUp,
- useUser,
-} from '@clerk/react';
import { ClerkProvider as ReactClerkProvider } from '@clerk/react';
import type { Ui } from '@clerk/react/internal';
import { ScriptOnce } from '@tanstack/react-router';
@@ -55,51 +10,7 @@ import type { TanstackStartClerkProviderProps } from './types';
import { useAwaitableNavigate } from './useAwaitableNavigate';
import { mergeWithPublicEnvs, pickFromClerkInitState } from './utils';
-export {
- APIKeys,
- AuthenticateWithRedirectCallback,
- ClerkDegraded,
- ClerkFailed,
- ClerkLoaded,
- ClerkLoading,
- CreateOrganization,
- GoogleOneTap,
- OrganizationList,
- OrganizationSwitcher,
- PricingTable,
- RedirectToCreateOrganization,
- RedirectToOrganizationProfile,
- RedirectToSignIn,
- RedirectToSignUp,
- RedirectToTasks,
- RedirectToUserProfile,
- Show,
- SignInButton,
- SignInWithMetamaskButton,
- SignOutButton,
- SignUpButton,
- TaskChooseOrganization,
- TaskResetPassword,
- UserAvatar,
- UserButton,
- Waitlist,
- __experimental_CheckoutProvider,
- __experimental_PaymentElement,
- __experimental_PaymentElementProvider,
- __experimental_useCheckout,
- __experimental_usePaymentElement,
- useAuth,
- useClerk,
- useEmailLink,
- useOrganization,
- useOrganizationList,
- useReverification,
- useSession,
- useSessionList,
- useSignIn,
- useSignUp,
- useUser,
-};
+export * from '@clerk/react';
const SDK_METADATA = {
name: PACKAGE_NAME,
From 3bb1953ce00cc03ae942fd921070a11f367dd30b Mon Sep 17 00:00:00 2001
From: Jacek
Date: Thu, 11 Dec 2025 20:53:05 -0600
Subject: [PATCH 28/33] wip
---
.../src/app/page.tsx | 29 ++++++++++++++++++-
1 file changed, 28 insertions(+), 1 deletion(-)
diff --git a/integration/templates/next-app-router-quickstart/src/app/page.tsx b/integration/templates/next-app-router-quickstart/src/app/page.tsx
index 797aceb64a1..bf1940d4bdf 100644
--- a/integration/templates/next-app-router-quickstart/src/app/page.tsx
+++ b/integration/templates/next-app-router-quickstart/src/app/page.tsx
@@ -1,4 +1,31 @@
-import { Show, SignInButton, SignUpButton, UserButton } from '@clerk/nextjs';
+import * as Clerk from '@clerk/nextjs';
+import type { ComponentType, ReactNode } from 'react';
+
+type ShowProps = {
+ children: ReactNode;
+ when: 'signedIn' | 'signedOut';
+};
+
+const Show: ComponentType =
+ (Clerk as { Show?: ComponentType }).Show ||
+ (({ children, when }) => {
+ const SignedIn = (Clerk as { SignedIn?: ComponentType<{ children: ReactNode }> }).SignedIn;
+ const SignedOut = (Clerk as { SignedOut?: ComponentType<{ children: ReactNode }> }).SignedOut;
+
+ if (when === 'signedIn' && SignedIn) {
+ return {children};
+ }
+
+ if (when === 'signedOut' && SignedOut) {
+ return {children};
+ }
+
+ return null;
+ });
+
+const SignInButton = (Clerk as { SignInButton: ComponentType }).SignInButton;
+const SignUpButton = (Clerk as { SignUpButton: ComponentType }).SignUpButton;
+const UserButton = (Clerk as { UserButton: ComponentType }).UserButton;
export default function Home() {
return (
From 8c719cd0e160f956e4f06589cd17c6bb3f02a347 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Thu, 11 Dec 2025 21:58:36 -0600
Subject: [PATCH 29/33] wip
---
.../chrome-extension/docs/clerk-provider.md | 22 +++++++++----------
.../shared/src/react/hooks/useCheckout.ts | 2 +-
.../vue/src/components/CheckoutButton.vue | 2 +-
.../components/SubscriptionDetailsButton.vue | 4 +++-
4 files changed, 16 insertions(+), 14 deletions(-)
diff --git a/packages/chrome-extension/docs/clerk-provider.md b/packages/chrome-extension/docs/clerk-provider.md
index 150922e5f17..3d2801182ba 100644
--- a/packages/chrome-extension/docs/clerk-provider.md
+++ b/packages/chrome-extension/docs/clerk-provider.md
@@ -4,22 +4,22 @@
```tsx
// App.tsx
-import { SignedIn, SignedOut, SignInButton, UserButton } from '@clerk/chrome-extension';
+import { Show, SignInButton, UserButton } from '@clerk/chrome-extension';
function App() {
return (
<>
- Please Sign In
- Welcome!
+ Please Sign In
+ Welcome!
>
);
@@ -61,7 +61,7 @@ export default IndexPopup;
You can hook into the router of your choice to handle navigation. Here's an example using `react-router-dom`:
```tsx
-import { ClerkProvider } from '@clerk/chrome-extension';
+import { ClerkProvider, Show, SignIn, SignUp } from '@clerk/chrome-extension';
import { useNavigate, Routes, Route, MemoryRouter } from 'react-router-dom';
import App from './App';
@@ -80,13 +80,13 @@ function AppWithRouting() {
path='/'
element={
<>
- Welcome User!
-
+ Welcome User!
+
-
+
>
}
/>
diff --git a/packages/shared/src/react/hooks/useCheckout.ts b/packages/shared/src/react/hooks/useCheckout.ts
index 6ca07b297f1..b31268e337e 100644
--- a/packages/shared/src/react/hooks/useCheckout.ts
+++ b/packages/shared/src/react/hooks/useCheckout.ts
@@ -22,7 +22,7 @@ export const useCheckout = (options?: UseCheckoutParams): CheckoutSignalValue =>
const clerk = useClerkInstanceContext();
if (user === null && isLoaded) {
- throw new Error('Clerk: Ensure that `useCheckout` is inside a component wrapped with ``.');
+ throw new Error('Clerk: Ensure that `useCheckout` is inside a component wrapped with ``.');
}
if (isLoaded && forOrganization === 'organization' && organization === null) {
diff --git a/packages/vue/src/components/CheckoutButton.vue b/packages/vue/src/components/CheckoutButton.vue
index 3d5332a4e61..6774e48c452 100644
--- a/packages/vue/src/components/CheckoutButton.vue
+++ b/packages/vue/src/components/CheckoutButton.vue
@@ -15,7 +15,7 @@ const attrs = useAttrs();
// Authentication checks - similar to React implementation
if (userId.value === null) {
- throw new Error('Ensure that `` is rendered inside a `` component.');
+ throw new Error('Ensure that `` is rendered inside a `` component.');
}
if (orgId.value === null && props.for === 'organization') {
diff --git a/packages/vue/src/components/SubscriptionDetailsButton.vue b/packages/vue/src/components/SubscriptionDetailsButton.vue
index 1d3dce1819a..b41e1bd7642 100644
--- a/packages/vue/src/components/SubscriptionDetailsButton.vue
+++ b/packages/vue/src/components/SubscriptionDetailsButton.vue
@@ -15,7 +15,9 @@ const attrs = useAttrs();
// Authentication checks - similar to React implementation
if (userId.value === null) {
- throw new Error('Ensure that `` is rendered inside a `` component.');
+ throw new Error(
+ 'Ensure that `` is rendered inside a `` component.',
+ );
}
if (orgId.value === null && props.for === 'organization') {
From d768f6e702fbcc86245a291687316061d536952a Mon Sep 17 00:00:00 2001
From: Jacek
Date: Fri, 12 Dec 2025 10:38:05 -0600
Subject: [PATCH 30/33] update changeset
---
.changeset/show-the-guards.md | 12 ++++++------
1 file changed, 6 insertions(+), 6 deletions(-)
diff --git a/.changeset/show-the-guards.md b/.changeset/show-the-guards.md
index 2eea380a52b..f682108dd1d 100644
--- a/.changeset/show-the-guards.md
+++ b/.changeset/show-the-guards.md
@@ -1,11 +1,11 @@
---
-'@clerk/react': major
-'@clerk/nextjs': major
-'@clerk/expo': major
+'@clerk/astro': major
'@clerk/chrome-extension': major
+'@clerk/expo': major
+'@clerk/nextjs': major
+'@clerk/react': major
'@clerk/shared': minor
-'@clerk/astro': patch
-'@clerk/vue': patch
+'@clerk/vue': major
---
-Restrict `` to App Router server usage and introduce `` as the client-side authorization component, updating shared types and Astro/Vue wrappers to align with the new API.
+Introduce `` as the cross-framework authorization control component and remove client-side ``, ``, and `` in favor of ``, updating shared types and framework wrappers to align with the new API.
From b06408240d94dadb0618728610a628c84815538e Mon Sep 17 00:00:00 2001
From: Jacek
Date: Fri, 12 Dec 2025 11:47:49 -0600
Subject: [PATCH 31/33] update astro SDK
---
.changeset/show-the-guards.md | 2 +-
.../astro-hybrid/src/pages/index.astro | 10 ++--
.../astro-hybrid/src/pages/ssr.astro | 10 ++--
.../astro-node/src/layouts/Layout.astro | 10 ++--
.../astro-node/src/layouts/react/Layout.astro | 10 ++--
.../src/pages/billing/checkout-btn.astro | 6 +--
.../astro-node/src/pages/index.astro | 14 +++---
.../astro-node/src/pages/react/index.astro | 14 +++---
.../src/pages/react/only-admins.astro | 29 ++++++-----
.../src/pages/react/only-members.astro | 29 ++++++-----
.../src/pages/transitions/index.astro | 10 ++--
.../src/astro-components/control/Show.astro | 27 ++++++++++
.../astro-components/control/ShowCSR.astro | 49 +++++++++++++++++++
.../astro-components/control/ShowSSR.astro | 11 +++++
packages/astro/src/astro-components/index.ts | 3 +-
15 files changed, 165 insertions(+), 69 deletions(-)
create mode 100644 packages/astro/src/astro-components/control/Show.astro
create mode 100644 packages/astro/src/astro-components/control/ShowCSR.astro
create mode 100644 packages/astro/src/astro-components/control/ShowSSR.astro
diff --git a/.changeset/show-the-guards.md b/.changeset/show-the-guards.md
index f682108dd1d..76f30d82828 100644
--- a/.changeset/show-the-guards.md
+++ b/.changeset/show-the-guards.md
@@ -8,4 +8,4 @@
'@clerk/vue': major
---
-Introduce `` as the cross-framework authorization control component and remove client-side ``, ``, and `` in favor of ``, updating shared types and framework wrappers to align with the new API.
+Introduce `` as the cross-framework authorization control component and remove ``, ``, and `` in favor of ``, updating shared types and framework wrappers to align with the new API.
diff --git a/integration/templates/astro-hybrid/src/pages/index.astro b/integration/templates/astro-hybrid/src/pages/index.astro
index 47168af011b..88ab11cf71c 100644
--- a/integration/templates/astro-hybrid/src/pages/index.astro
+++ b/integration/templates/astro-hybrid/src/pages/index.astro
@@ -1,5 +1,5 @@
---
-import { UserButton, SignInButton, SignedIn, SignedOut } from '@clerk/astro/components';
+import { Show, UserButton, SignInButton } from '@clerk/astro/components';
import { OrganizationSwitcher } from '@clerk/astro/react';
import Layout from '../layouts/Layout.astro';
@@ -7,16 +7,16 @@ export const prerender = true;
---
-
+
Signed out
-
-
+
+
Signed in
-
+
diff --git a/integration/templates/astro-hybrid/src/pages/ssr.astro b/integration/templates/astro-hybrid/src/pages/ssr.astro
index 0db930a6145..0c0611e626f 100644
--- a/integration/templates/astro-hybrid/src/pages/ssr.astro
+++ b/integration/templates/astro-hybrid/src/pages/ssr.astro
@@ -1,5 +1,5 @@
---
-import { UserButton, SignInButton, SignedIn, SignedOut } from '@clerk/astro/components';
+import { Show, UserButton, SignInButton } from '@clerk/astro/components';
import { OrganizationSwitcher } from '@clerk/astro/react';
import Layout from '../layouts/Layout.astro';
@@ -7,16 +7,16 @@ export const prerender = false;
---
-
+
Signed out
-
-
+
+
Signed in
-
+
diff --git a/integration/templates/astro-node/src/layouts/Layout.astro b/integration/templates/astro-node/src/layouts/Layout.astro
index 3e168321da2..17639bb1214 100644
--- a/integration/templates/astro-node/src/layouts/Layout.astro
+++ b/integration/templates/astro-node/src/layouts/Layout.astro
@@ -5,7 +5,7 @@ interface Props {
const { title } = Astro.props;
-import { SignedIn, SignedOut } from '@clerk/astro/components';
+import { Show } from '@clerk/astro/components';
import { LanguagePicker } from '../components/LanguagePicker';
import CustomUserButton from '../components/CustomUserButton.astro';
---
@@ -80,11 +80,11 @@ import CustomUserButton from '../components/CustomUserButton.astro';
-
+
diff --git a/integration/templates/astro-node/src/layouts/react/Layout.astro b/integration/templates/astro-node/src/layouts/react/Layout.astro
index 41b878880e3..4a5fc2be65c 100644
--- a/integration/templates/astro-node/src/layouts/react/Layout.astro
+++ b/integration/templates/astro-node/src/layouts/react/Layout.astro
@@ -5,7 +5,7 @@ interface Props {
const { title } = Astro.props;
-import { SignedIn, SignedOut, UserButton } from '@clerk/astro/react';
+import { Show, UserButton } from '@clerk/astro/react';
import { LanguagePicker } from '../../components/LanguagePicker';
---
@@ -79,11 +79,11 @@ import { LanguagePicker } from '../../components/LanguagePicker';
-
+
diff --git a/integration/templates/astro-node/src/pages/billing/checkout-btn.astro b/integration/templates/astro-node/src/pages/billing/checkout-btn.astro
index 736992e6033..3ae0fbfa9db 100644
--- a/integration/templates/astro-node/src/pages/billing/checkout-btn.astro
+++ b/integration/templates/astro-node/src/pages/billing/checkout-btn.astro
@@ -1,17 +1,17 @@
---
-import { SignedIn, __experimental_CheckoutButton as CheckoutButton } from '@clerk/astro/components';
+import { Show, __experimental_CheckoutButton as CheckoutButton } from '@clerk/astro/components';
import Layout from '../../layouts/Layout.astro';
---
-
+
Checkout Now
-
+
diff --git a/integration/templates/astro-node/src/pages/index.astro b/integration/templates/astro-node/src/pages/index.astro
index 089eac14653..c7a92f9330c 100644
--- a/integration/templates/astro-node/src/pages/index.astro
+++ b/integration/templates/astro-node/src/pages/index.astro
@@ -2,12 +2,12 @@
import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro';
-import { SignedIn, SignedOut, SignOutButton, OrganizationSwitcher } from '@clerk/astro/components';
+import { Show, SignOutButton, OrganizationSwitcher } from '@clerk/astro/components';
---
Welcome to Astro
-
+
Sign out!
-
+
@@ -26,7 +26,7 @@ import { SignedIn, SignedOut, SignOutButton, OrganizationSwitcher } from '@clerk
role='list'
class='link-card-grid'
>
-
+
-
-
+
+
-
+
diff --git a/integration/templates/astro-node/src/pages/react/index.astro b/integration/templates/astro-node/src/pages/react/index.astro
index 5fe777167f7..11271836228 100644
--- a/integration/templates/astro-node/src/pages/react/index.astro
+++ b/integration/templates/astro-node/src/pages/react/index.astro
@@ -2,12 +2,12 @@
import Layout from '../../layouts/react/Layout.astro';
import Card from '../../components/Card.astro';
-import { SignedIn, SignedOut, SignOutButton, OrganizationSwitcher } from '@clerk/astro/react';
+import { Show, SignOutButton, OrganizationSwitcher } from '@clerk/astro/react';
---
Welcome to Astro + React
-
+
Sign out!
-
+
@@ -31,7 +31,7 @@ import { SignedIn, SignedOut, SignOutButton, OrganizationSwitcher } from '@clerk
role='list'
class='link-card-grid'
>
-
+
-
-
+
+
-
+
diff --git a/integration/templates/astro-node/src/pages/react/only-admins.astro b/integration/templates/astro-node/src/pages/react/only-admins.astro
index 0ad2bc1b2ba..bc3b46e75d8 100644
--- a/integration/templates/astro-node/src/pages/react/only-admins.astro
+++ b/integration/templates/astro-node/src/pages/react/only-admins.astro
@@ -1,23 +1,28 @@
---
-import { Protect } from '@clerk/astro/react';
+import { Show } from '@clerk/astro/react';
import Layout from '../../layouts/react/Layout.astro';
---
diff --git a/integration/templates/astro-node/src/pages/react/only-members.astro b/integration/templates/astro-node/src/pages/react/only-members.astro
index e0fd91dc11f..df8813acc3b 100644
--- a/integration/templates/astro-node/src/pages/react/only-members.astro
+++ b/integration/templates/astro-node/src/pages/react/only-members.astro
@@ -1,23 +1,28 @@
---
-import { Protect } from '@clerk/astro/react';
+import { Show } from '@clerk/astro/react';
import Layout from '../../layouts/react/Layout.astro';
---
diff --git a/integration/templates/astro-node/src/pages/transitions/index.astro b/integration/templates/astro-node/src/pages/transitions/index.astro
index af29b083fcc..4985e2b77e3 100644
--- a/integration/templates/astro-node/src/pages/transitions/index.astro
+++ b/integration/templates/astro-node/src/pages/transitions/index.astro
@@ -1,15 +1,15 @@
---
-import { SignedIn, SignedOut, UserButton } from '@clerk/astro/components';
+import { Show, UserButton } from '@clerk/astro/components';
import Layout from '../../layouts/ViewTransitionsLayout.astro';
---
diff --git a/packages/astro/src/astro-components/control/Show.astro b/packages/astro/src/astro-components/control/Show.astro
new file mode 100644
index 00000000000..5ec00379627
--- /dev/null
+++ b/packages/astro/src/astro-components/control/Show.astro
@@ -0,0 +1,27 @@
+---
+import ShowCSR from './ShowCSR.astro';
+import ShowSSR from './ShowSSR.astro';
+
+import { isStaticOutput } from 'virtual:@clerk/astro/config';
+
+type Props = {
+ when: 'signedIn' | 'signedOut';
+ isStatic?: boolean;
+ /**
+ * The class name to apply to the outermost element of the component.
+ * This class is only applied to static components.
+ */
+ class?: string;
+};
+
+const { when, isStatic, class: className } = Astro.props;
+
+const ShowComponent = isStaticOutput(isStatic) ? ShowCSR : ShowSSR;
+---
+
+
+
+
diff --git a/packages/astro/src/astro-components/control/ShowCSR.astro b/packages/astro/src/astro-components/control/ShowCSR.astro
new file mode 100644
index 00000000000..75be7aa27d5
--- /dev/null
+++ b/packages/astro/src/astro-components/control/ShowCSR.astro
@@ -0,0 +1,49 @@
+---
+type Props = {
+ when: 'signedIn' | 'signedOut';
+ class?: string;
+};
+
+const { when, class: className } = Astro.props;
+---
+
+
+
+
+
+
diff --git a/packages/astro/src/astro-components/control/ShowSSR.astro b/packages/astro/src/astro-components/control/ShowSSR.astro
new file mode 100644
index 00000000000..d4aaa48b1ec
--- /dev/null
+++ b/packages/astro/src/astro-components/control/ShowSSR.astro
@@ -0,0 +1,11 @@
+---
+type Props = {
+ when: 'signedIn' | 'signedOut';
+};
+
+const { when } = Astro.props;
+const { userId } = Astro.locals.auth();
+---
+
+{when === 'signedIn' ? (userId ? : null) : null}
+{when === 'signedOut' ? (!userId ? : null) : null}
diff --git a/packages/astro/src/astro-components/index.ts b/packages/astro/src/astro-components/index.ts
index 5c9d9b8361f..b9ef5796eb6 100644
--- a/packages/astro/src/astro-components/index.ts
+++ b/packages/astro/src/astro-components/index.ts
@@ -1,8 +1,7 @@
/**
* Control Components
*/
-export { default as SignedIn } from './control/SignedIn.astro';
-export { default as SignedOut } from './control/SignedOut.astro';
+export { default as Show } from './control/Show.astro';
export { default as Protect } from './control/Protect.astro';
export { default as AuthenticateWithRedirectCallback } from './control/AuthenticateWithRedirectCallback.astro';
From 3014891e9558d38a17a39d30cb1319abe8047bcb Mon Sep 17 00:00:00 2001
From: Jacek
Date: Fri, 12 Dec 2025 11:52:43 -0600
Subject: [PATCH 32/33] wip
---
packages/astro/src/astro-components/control/ShowSSR.astro | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/packages/astro/src/astro-components/control/ShowSSR.astro b/packages/astro/src/astro-components/control/ShowSSR.astro
index d4aaa48b1ec..c9a4817ef2f 100644
--- a/packages/astro/src/astro-components/control/ShowSSR.astro
+++ b/packages/astro/src/astro-components/control/ShowSSR.astro
@@ -7,5 +7,5 @@ const { when } = Astro.props;
const { userId } = Astro.locals.auth();
---
-{when === 'signedIn' ? (userId ? : null) : null}
-{when === 'signedOut' ? (!userId ? : null) : null}
+{when === 'signedIn' ? userId ? : null : null}
+{when === 'signedOut' ? !userId ? : null : null}
From 8496c81498935b01bda92e922c0f89fb207709a6 Mon Sep 17 00:00:00 2001
From: Jacek
Date: Sat, 13 Dec 2025 08:09:35 -0600
Subject: [PATCH 33/33] pr feedback
---
packages/astro/src/astro-components/control/ShowCSR.astro | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/packages/astro/src/astro-components/control/ShowCSR.astro b/packages/astro/src/astro-components/control/ShowCSR.astro
index 75be7aa27d5..0ad9957fa11 100644
--- a/packages/astro/src/astro-components/control/ShowCSR.astro
+++ b/packages/astro/src/astro-components/control/ShowCSR.astro
@@ -45,5 +45,7 @@ const { when, class: className } = Astro.props;
}
}
- customElements.define('clerk-show', ClerkShow);
+ if (!customElements.get('clerk-show')) {
+ customElements.define('clerk-show', ClerkShow);
+ }