From c9f3d62afd3e85d4bfa3cf6d41115d107d28f084 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 4 Dec 2025 20:51:00 -0600 Subject: [PATCH 01/17] feat(repo): Protect -> Show --- .../expo/src/components/controlComponents.tsx | 2 +- .../app-router/server/controlComponents.tsx | 9 +- .../src/client-boundary/controlComponents.ts | 16 +- packages/nextjs/src/components.client.ts | 21 +- packages/nextjs/src/index.ts | 7 +- .../src/components/controlComponents.tsx | 80 ++--- packages/react/src/components/index.ts | 4 +- packages/shared/src/types/protect.ts | 37 ++ .../transform-protect-to-show.fixtures.js | 329 ++++++++++++++++++ .../transform-protect-to-show.test.js | 18 + .../codemods/transform-protect-to-show.cjs | 197 +++++++++++ 11 files changed, 658 insertions(+), 62 deletions(-) create mode 100644 packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js create mode 100644 packages/upgrade/src/codemods/__tests__/transform-protect-to-show.test.js create mode 100644 packages/upgrade/src/codemods/transform-protect-to-show.cjs diff --git a/packages/expo/src/components/controlComponents.tsx b/packages/expo/src/components/controlComponents.tsx index bc42b9dbc73..33edad58240 100644 --- a/packages/expo/src/components/controlComponents.tsx +++ b/packages/expo/src/components/controlComponents.tsx @@ -1 +1 @@ -export { ClerkLoaded, ClerkLoading, SignedIn, SignedOut, Protect } from '@clerk/react'; +export { ClerkLoaded, ClerkLoading, Show, SignedIn, SignedOut } from '@clerk/react'; diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index d640c63a055..60039b33752 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -1,9 +1,14 @@ -import type { ProtectProps } from '@clerk/react'; -import type { PendingSessionOptions } from '@clerk/shared/types'; +import type { PendingSessionOptions, ProtectProps as _ProtectProps } from '@clerk/shared/types'; import React from 'react'; import { auth } from './auth'; +type ProtectProps = React.PropsWithChildren< + _ProtectProps & { + fallback?: React.ReactNode; + } & PendingSessionOptions +>; + export async function SignedIn( props: React.PropsWithChildren, ): Promise { diff --git a/packages/nextjs/src/client-boundary/controlComponents.ts b/packages/nextjs/src/client-boundary/controlComponents.ts index 1ab240a18f5..9006fbc594e 100644 --- a/packages/nextjs/src/client-boundary/controlComponents.ts +++ b/packages/nextjs/src/client-boundary/controlComponents.ts @@ -1,20 +1,20 @@ 'use client'; export { - ClerkLoaded, - ClerkLoading, + AuthenticateWithRedirectCallback, ClerkDegraded, ClerkFailed, - SignedOut, - SignedIn, - Protect, + ClerkLoaded, + ClerkLoading, + RedirectToCreateOrganization, + RedirectToOrganizationProfile, RedirectToSignIn, RedirectToSignUp, RedirectToTasks, RedirectToUserProfile, - AuthenticateWithRedirectCallback, - RedirectToCreateOrganization, - RedirectToOrganizationProfile, + Show, + SignedIn, + SignedOut, } from '@clerk/react'; export { MultisessionAppSupport } from '@clerk/react/internal'; diff --git a/packages/nextjs/src/components.client.ts b/packages/nextjs/src/components.client.ts index aac3f82f65b..1d6fd04d0e6 100644 --- a/packages/nextjs/src/components.client.ts +++ b/packages/nextjs/src/components.client.ts @@ -1,2 +1,21 @@ export { ClerkProvider } from './client-boundary/ClerkProvider'; -export { SignedIn, SignedOut, Protect } from './client-boundary/controlComponents'; +export { Show, SignedIn, SignedOut } from './client-boundary/controlComponents'; + +/** + * `` is only available as a React Server Component in the App Router. + * For client-side conditional rendering, use `` instead. + * + * @example + * ```tsx + * // Server Component (App Router) + * ... + * + * // Client Component + * ... + * ``` + */ +export const Protect = () => { + throw new Error( + '`` is only available as a React Server Component. For client components, use `` instead.', + ); +}; diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 2e29bcd7568..bc727044900 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -14,6 +14,7 @@ export { RedirectToSignUp, RedirectToTasks, RedirectToUserProfile, + Show, } from './client-boundary/controlComponents'; /** @@ -72,6 +73,10 @@ import * as ComponentsModule from '#components'; import type { ServerComponentsServerModuleTypes } from './components.server'; export const ClerkProvider = ComponentsModule.ClerkProvider as ServerComponentsServerModuleTypes['ClerkProvider']; +/** + * Use `` in RSC (App Router) to restrict access based on authentication and authorization. + * For client components, use `` instead. + */ +export const Protect = ComponentsModule.Protect as ServerComponentsServerModuleTypes['Protect']; export const SignedIn = ComponentsModule.SignedIn as ServerComponentsServerModuleTypes['SignedIn']; export const SignedOut = ComponentsModule.SignedOut as ServerComponentsServerModuleTypes['SignedOut']; -export const Protect = ComponentsModule.Protect as ServerComponentsServerModuleTypes['Protect']; diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index bdeefbfa05a..9dcd97956e3 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -2,7 +2,8 @@ import { deprecated } from '@clerk/shared/deprecated'; import type { HandleOAuthCallbackParams, PendingSessionOptions, - ProtectProps as _ProtectProps, + ShowProps as _ShowProps, + ShowWhenCondition, } from '@clerk/shared/types'; import React from 'react'; @@ -73,76 +74,61 @@ export const ClerkDegraded = ({ children }: React.PropsWithChildren) => return children; }; -export type ProtectProps = React.PropsWithChildren< - _ProtectProps & { +export type ShowProps = React.PropsWithChildren< + _ShowProps & { fallback?: React.ReactNode; } & PendingSessionOptions >; /** - * Use `` in order to prevent unauthenticated or unauthorized users from accessing the children passed to the component. + * Use `` to conditionally render content based on user authorization. * - * Examples: - * ``` - * - * - * has({permission:"a_permission_key"})} /> - * has({role:"a_role_key"})} /> - * Unauthorized

} /> + * @example + * ```tsx + * Unauthorized

}> + * + *
+ * + * + * + * + * + * has({ permission: "org:read" }) && isFeatureEnabled}> + * + * * ``` */ -export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAuthorizedParams }: ProtectProps) => { - useAssertWrappedByClerkProvider('Protect'); +export const Show = ({ children, fallback, treatPendingAsSignedOut, when }: ShowProps) => { + useAssertWrappedByClerkProvider('Show'); - const { isLoaded, has, userId } = useAuth({ treatPendingAsSignedOut }); + const { has, isLoaded, userId } = useAuth({ treatPendingAsSignedOut }); - /** - * Avoid flickering children or fallback while clerk is loading sessionId or userId - */ if (!isLoaded) { return null; } - /** - * Fallback to UI provided by user or `null` if authorization checks failed - */ - const unauthorized = fallback ?? null; - const authorized = children; + const unauthorized = fallback ?? null; if (!userId) { return unauthorized; } - /** - * Check against the results of `has` called inside the callback - */ - if (typeof restAuthorizedParams.condition === 'function') { - if (restAuthorizedParams.condition(has)) { - return authorized; - } - return unauthorized; - } - - if ( - restAuthorizedParams.role || - restAuthorizedParams.permission || - restAuthorizedParams.feature || - restAuthorizedParams.plan - ) { - if (has(restAuthorizedParams)) { - return authorized; - } - return unauthorized; + // At this point, userId is defined so has() is guaranteed to be available + if (checkAuthorization(when, has!)) { + return authorized; } - /** - * If neither of the authorization params are passed behave as the ``. - * If fallback is present render that instead of rendering nothing. - */ - return authorized; + return unauthorized; }; +function checkAuthorization(when: ShowWhenCondition, has: NonNullable['has']>): boolean { + if (typeof when === 'function') { + return when(has); + } + return has(when); +} + export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => { const { client, session } = clerk; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index cbf9b77aba1..23523b1c16f 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -21,18 +21,18 @@ export { ClerkFailed, ClerkLoaded, ClerkLoading, - Protect, RedirectToCreateOrganization, RedirectToOrganizationProfile, RedirectToSignIn, RedirectToSignUp, RedirectToTasks, RedirectToUserProfile, + Show, SignedIn, SignedOut, } from './controlComponents'; -export type { ProtectProps } from './controlComponents'; +export type { ShowProps } from './controlComponents'; export { SignInButton } from './SignInButton'; export { SignInWithMetamaskButton } from './SignInWithMetamaskButton'; diff --git a/packages/shared/src/types/protect.ts b/packages/shared/src/types/protect.ts index e96df803046..70bc9118295 100644 --- a/packages/shared/src/types/protect.ts +++ b/packages/shared/src/types/protect.ts @@ -68,3 +68,40 @@ export type ProtectProps = feature?: never; plan?: never; }; + +/** + * Authorization condition for the `when` prop in ``. + * Can be an object specifying role, permission, feature, or plan, + * or a callback function receiving the `has` helper for complex conditions. + */ +export type ShowWhenCondition = + | { role: OrganizationCustomRoleKey } + | { permission: OrganizationCustomPermissionKey } + | { feature: Autocomplete<`org:${string}` | `user:${string}`> } + | { plan: Autocomplete<`org:${string}` | `user:${string}`> } + | ((has: CheckAuthorizationWithCustomPermissions) => boolean); + +/** + * Props for the `` component, which conditionally renders children based on authorization. + * + * @example + * ```tsx + * // Require a specific permission + * ... + * + * // Require a specific role + * ... + * + * // Use a custom condition callback + * has({ permission: "org:read" }) && someCondition}>... + * + * // Require a specific feature + * ... + * + * // Require a specific plan + * ... + * ``` + */ +export type ShowProps = { + when: ShowWhenCondition; +}; diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js new file mode 100644 index 00000000000..e91211cd7b6 --- /dev/null +++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js @@ -0,0 +1,329 @@ +export const fixtures = [ + { + name: 'Basic import transform', + source: ` +import { Protect } from "@clerk/react" + `, + output: ` +import { Show } from "@clerk/react" +`, + }, + { + name: 'Import transform with other imports', + source: ` +import { ClerkProvider, Protect, SignedIn } from "@clerk/react" + `, + output: ` +import { ClerkProvider, Show, SignedIn } from "@clerk/react" +`, + }, + { + name: 'Import from @clerk/nextjs without use client - should NOT transform (RSC)', + source: ` +import { Protect } from "@clerk/nextjs" + `, + output: null, + }, + { + name: 'Basic permission prop transform', + source: ` +import { Protect } from "@clerk/react" + +function App() { + return ( + + + + ) +} + `, + output: ` +import { Show } from "@clerk/react" + +function App() { + return ( + + + + ); +} +`, + }, + { + name: 'Basic role prop transform', + source: ` +import { Protect } from "@clerk/react" + +function App() { + return ( + + + + ) +} + `, + output: ` +import { Show } from "@clerk/react" + +function App() { + return ( + + + + ); +} +`, + }, + { + name: 'Feature prop transform', + source: ` +import { Protect } from "@clerk/react" + +function App() { + return ( + + + + ) +} + `, + output: ` +import { Show } from "@clerk/react" + +function App() { + return ( + + + + ); +} +`, + }, + { + name: 'Plan prop transform', + source: ` +import { Protect } from "@clerk/react" + +function App() { + return ( + + + + ) +} + `, + output: ` +import { Show } from "@clerk/react" + +function App() { + return ( + + + + ); +} +`, + }, + { + name: 'Condition prop transform', + source: ` +import { Protect } from "@clerk/react" + +function App() { + return ( + has({ permission: "org:read" })}> + + + ) +} + `, + output: ` +import { Show } from "@clerk/react" + +function App() { + return ( + has({ permission: "org:read" })}> + + + ); +} +`, + }, + { + name: 'With fallback prop', + source: ` +import { Protect } from "@clerk/react" + +function App() { + return ( + }> + + + ) +} + `, + output: ` +import { Show } from "@clerk/react" + +function App() { + return ( + }> + + + ); +} +`, + }, + { + name: 'Self-closing Protect', + source: ` +import { Protect } from "@clerk/react" + +function App() { + return +} + `, + output: ` +import { Show } from "@clerk/react" + +function App() { + return ( + + ); +} +`, + }, + { + name: 'Handles directives', + source: `"use client"; + +import { Protect } from "@clerk/nextjs"; + +export function Protected() { + return ( + + + + ); +} +`, + output: `"use client"; + +import { Show } from "@clerk/nextjs"; + +export function Protected() { + return ( + + + + ); +}`, + }, + { + name: 'Dynamic permission value', + source: ` +import { Protect } from "@clerk/react" + +function App({ requiredPermission }) { + return ( + + + + ) +} + `, + output: ` +import { Show } from "@clerk/react" + +function App({ requiredPermission }) { + return ( + + + + ); +} +`, + }, + { + name: 'RSC file (no use client) from @clerk/nextjs - should NOT transform', + source: `import { Protect } from "@clerk/nextjs"; + +export default async function Page() { + return ( + + + + ); +} +`, + output: null, + }, + { + name: 'Client file (use client) from @clerk/nextjs - should transform', + source: `"use client"; + +import { Protect } from "@clerk/nextjs"; + +export function ClientComponent() { + return ( + + + + ); +} +`, + output: `"use client"; + +import { Show } from "@clerk/nextjs"; + +export function ClientComponent() { + return ( + + + + ); +}`, + }, + { + name: 'Client-only package (@clerk/react) without use client - should still transform', + source: `import { Protect } from "@clerk/react"; + +function Component() { + return ( + + + + ); +} +`, + output: `import { Show } from "@clerk/react"; + +function Component() { + return ( + + + + ); +}`, + }, +]; diff --git a/packages/upgrade/src/codemods/__tests__/transform-protect-to-show.test.js b/packages/upgrade/src/codemods/__tests__/transform-protect-to-show.test.js new file mode 100644 index 00000000000..435c84b524d --- /dev/null +++ b/packages/upgrade/src/codemods/__tests__/transform-protect-to-show.test.js @@ -0,0 +1,18 @@ +import { applyTransform } from 'jscodeshift/dist/testUtils'; +import { describe, expect, it } from 'vitest'; + +import transformer from '../transform-protect-to-show.cjs'; +import { fixtures } from './__fixtures__/transform-protect-to-show.fixtures'; + +describe('transform-protect-to-show', () => { + it.each(fixtures)(`$name`, ({ source, output }) => { + const result = applyTransform(transformer, {}, { source }); + + if (output === null) { + // null output means no transformation should occur + expect(result).toBeFalsy(); + } else { + expect(result).toEqual(output.trim()); + } + }); +}); diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs new file mode 100644 index 00000000000..bc20b06ab2e --- /dev/null +++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs @@ -0,0 +1,197 @@ +// Packages that are always client-side +const CLIENT_ONLY_PACKAGES = ['@clerk/react', '@clerk/expo']; +// Packages that can be used in both RSC and client components +const HYBRID_PACKAGES = ['@clerk/nextjs']; + +/** + * Checks if a file has a 'use client' directive at the top. + */ +function hasUseClientDirective(root, j) { + const program = root.find(j.Program).get(); + const body = program.node.body; + + if (body.length === 0) { + return false; + } + + const firstStatement = body[0]; + + // Check for 'use client' as an expression statement with a string literal + if (j.ExpressionStatement.check(firstStatement)) { + const expression = firstStatement.expression; + if (j.Literal.check(expression) || j.StringLiteral.check(expression)) { + const value = expression.value; + return value === 'use client'; + } + // Handle DirectiveLiteral (used by some parsers like babel) + if (expression.type === 'DirectiveLiteral') { + return expression.value === 'use client'; + } + } + + // Also check directive field (some parsers use this) + if (firstStatement.directive === 'use client') { + return true; + } + + // Check for directives array in program node (babel parser) + const directives = program.node.directives; + if (directives && directives.length > 0) { + return directives.some(d => d.value && d.value.value === 'use client'); + } + + return false; +} + +/** + * Transforms `` component usage to `` component. + * + * Handles the following transformations: + * - `` → `` + * - `` → `` + * - `` → `` + * - `` → `` + * - ` ...}>` → ` ...}>` + * + * Also updates imports from `Protect` to `Show`. + * + * NOTE: For @clerk/nextjs, this only transforms files with 'use client' directive. + * RSC files using from @clerk/nextjs should NOT be transformed, + * as is still valid as an RSC-only component. + * + * @param {import('jscodeshift').FileInfo} fileInfo - The file information + * @param {import('jscodeshift').API} api - The API object provided by jscodeshift + * @returns {string|undefined} - The transformed source code if modifications were made + */ +module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) { + const root = j(source); + let dirtyFlag = false; + + const isClientComponent = hasUseClientDirective(root, j); + + // Check if this file imports Protect from a hybrid package (like @clerk/nextjs) + // If so, and it's NOT a client component, skip the transformation + let hasHybridPackageImport = false; + HYBRID_PACKAGES.forEach(packageName => { + root.find(j.ImportDeclaration, { source: { value: packageName } }).forEach(path => { + const specifiers = path.node.specifiers || []; + if (specifiers.some(spec => j.ImportSpecifier.check(spec) && spec.imported.name === 'Protect')) { + hasHybridPackageImport = true; + } + }); + }); + + // Skip RSC files that import from hybrid packages + if (hasHybridPackageImport && !isClientComponent) { + return undefined; + } + + // Transform imports: Protect → Show + const allPackages = [...CLIENT_ONLY_PACKAGES, ...HYBRID_PACKAGES]; + allPackages.forEach(packageName => { + root.find(j.ImportDeclaration, { source: { value: packageName } }).forEach(path => { + const node = path.node; + const specifiers = node.specifiers || []; + + specifiers.forEach(spec => { + if (j.ImportSpecifier.check(spec) && spec.imported.name === 'Protect') { + spec.imported.name = 'Show'; + if (spec.local && spec.local.name === 'Protect') { + spec.local.name = 'Show'; + } + dirtyFlag = true; + } + }); + }); + }); + + // Transform JSX: + root.find(j.JSXElement).forEach(path => { + const openingElement = path.node.openingElement; + const closingElement = path.node.closingElement; + + // Check if this is a element + if (!j.JSXIdentifier.check(openingElement.name) || openingElement.name.name !== 'Protect') { + return; + } + + // Rename to Show + openingElement.name.name = 'Show'; + if (closingElement && j.JSXIdentifier.check(closingElement.name)) { + closingElement.name.name = 'Show'; + } + + const attributes = openingElement.attributes || []; + const authAttributes = []; + const otherAttributes = []; + let conditionAttr = null; + + // Separate auth-related attributes from other attributes + attributes.forEach(attr => { + if (!j.JSXAttribute.check(attr)) { + otherAttributes.push(attr); + return; + } + + const attrName = attr.name.name; + if (attrName === 'condition') { + conditionAttr = attr; + } else if (['feature', 'permission', 'plan', 'role'].includes(attrName)) { + authAttributes.push(attr); + } else { + otherAttributes.push(attr); + } + }); + + // Build the `when` prop + let whenValue = null; + + if (conditionAttr) { + // condition prop becomes the when callback directly + whenValue = conditionAttr.value; + } else if (authAttributes.length > 0) { + // Build an object from auth attributes + const properties = authAttributes.map(attr => { + const key = j.identifier(attr.name.name); + let value; + + if (j.JSXExpressionContainer.check(attr.value)) { + value = attr.value.expression; + } else if (j.StringLiteral.check(attr.value) || j.Literal.check(attr.value)) { + value = attr.value; + } else { + // Default string value + value = j.stringLiteral(attr.value?.value || ''); + } + + return j.objectProperty(key, value); + }); + + whenValue = j.jsxExpressionContainer(j.objectExpression(properties)); + } + + // Reconstruct attributes with `when` prop + const newAttributes = []; + + if (whenValue) { + newAttributes.push(j.jsxAttribute(j.jsxIdentifier('when'), whenValue)); + } + + // Add remaining attributes (fallback, etc.) + otherAttributes.forEach(attr => newAttributes.push(attr)); + + openingElement.attributes = newAttributes; + dirtyFlag = true; + }); + + if (!dirtyFlag) { + return undefined; + } + + let result = root.toSource(); + // Fix double semicolons that can occur when recast reprints directive prologues + result = result.replace(/^(['"`][^'"`]+['"`]);;/gm, '$1;'); + return result; +}; + +module.exports.parser = 'tsx'; From 1f8e65252b050f62b6ca34f7f106d50e764e208f Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 4 Dec 2025 21:31:08 -0600 Subject: [PATCH 02/17] typing tweak --- packages/astro/src/react/controlComponents.tsx | 4 ++-- .../__tests__/__snapshots__/exports.test.ts.snap | 2 +- packages/chrome-extension/src/react/re-exports.ts | 2 +- .../src/app-router/server/controlComponents.tsx | 8 ++++---- packages/react/src/components/controlComponents.tsx | 10 +++------- packages/shared/src/types/protect.ts | 13 +++++++++---- packages/vue/src/components/controlComponents.ts | 4 ++-- 7 files changed, 22 insertions(+), 21 deletions(-) diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx index 956a9f61347..5a574164748 100644 --- a/packages/astro/src/react/controlComponents.tsx +++ b/packages/astro/src/react/controlComponents.tsx @@ -4,7 +4,7 @@ import type { PropsWithChildren } from 'react'; import React, { useEffect, useState } from 'react'; import { $csrState } from '../stores/internal'; -import type { ProtectProps as _ProtectProps } from '../types'; +import type { ProtectParams } from '@clerk/shared/types'; import { useAuth } from './hooks'; import type { WithClerkProp } from './utils'; import { withClerk } from './utils'; @@ -70,7 +70,7 @@ export const ClerkLoading = ({ children }: React.PropsWithChildren): JSX.Element }; export type ProtectProps = React.PropsWithChildren< - _ProtectProps & { fallback?: React.ReactNode } & PendingSessionOptions + ProtectParams & { fallback?: React.ReactNode } & PendingSessionOptions >; /** diff --git a/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap index 120fb6d4a1c..01e780dea00 100644 --- a/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap @@ -15,12 +15,12 @@ exports[`public exports > should not include a breaking change 1`] = ` "OrganizationProfile", "OrganizationSwitcher", "PricingTable", - "Protect", "RedirectToCreateOrganization", "RedirectToOrganizationProfile", "RedirectToSignIn", "RedirectToSignUp", "RedirectToUserProfile", + "Show", "SignIn", "SignInButton", "SignInWithMetamaskButton", diff --git a/packages/chrome-extension/src/react/re-exports.ts b/packages/chrome-extension/src/react/re-exports.ts index 2838dc6264b..f13e8e45c13 100644 --- a/packages/chrome-extension/src/react/re-exports.ts +++ b/packages/chrome-extension/src/react/re-exports.ts @@ -10,12 +10,12 @@ export { OrganizationProfile, OrganizationSwitcher, PricingTable, - Protect, RedirectToCreateOrganization, RedirectToOrganizationProfile, RedirectToSignIn, RedirectToSignUp, RedirectToUserProfile, + Show, SignIn, SignInButton, SignInWithMetamaskButton, diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index 60039b33752..2572a7a256f 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -1,10 +1,10 @@ -import type { PendingSessionOptions, ProtectProps as _ProtectProps } from '@clerk/shared/types'; +import type { PendingSessionOptions, ProtectParams } from '@clerk/shared/types'; import React from 'react'; import { auth } from './auth'; -type ProtectProps = React.PropsWithChildren< - _ProtectProps & { +export type AppRouterProtectProps = React.PropsWithChildren< + ProtectParams & { fallback?: React.ReactNode; } & PendingSessionOptions >; @@ -37,7 +37,7 @@ export async function SignedOut( * Unauthorized

} /> * ``` */ -export async function Protect(props: ProtectProps): Promise { +export async function Protect(props: AppRouterProtectProps): Promise { const { children, fallback, ...restAuthorizedParams } = props; const { has, userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut }); diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index 9dcd97956e3..715aa3056ad 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -1,10 +1,5 @@ import { deprecated } from '@clerk/shared/deprecated'; -import type { - HandleOAuthCallbackParams, - PendingSessionOptions, - ShowProps as _ShowProps, - ShowWhenCondition, -} from '@clerk/shared/types'; +import type { HandleOAuthCallbackParams, PendingSessionOptions, ShowWhenCondition } from '@clerk/shared/types'; import React from 'react'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; @@ -75,7 +70,8 @@ export const ClerkDegraded = ({ children }: React.PropsWithChildren) => }; export type ShowProps = React.PropsWithChildren< - _ShowProps & { + { + when: ShowWhenCondition; fallback?: React.ReactNode; } & PendingSessionOptions >; diff --git a/packages/shared/src/types/protect.ts b/packages/shared/src/types/protect.ts index 70bc9118295..e06cf74afb5 100644 --- a/packages/shared/src/types/protect.ts +++ b/packages/shared/src/types/protect.ts @@ -3,9 +3,9 @@ import type { CheckAuthorizationWithCustomPermissions } from './session'; import type { Autocomplete } from './utils'; /** - * Props for the `` component, which restricts access to its children based on authentication and authorization. + * Authorization parameters used by `` and `auth.protect()`. * - * Use `ProtectProps` to specify the required role, permission, feature, or plan for access. + * Use `ProtectParams` to specify the required role, permission, feature, or plan for access. * * @example * ```tsx @@ -22,10 +22,10 @@ import type { Autocomplete } from './utils'; * * * // Require a specific plan - * + * * ``` */ -export type ProtectProps = +export type ProtectParams = | { condition?: never; role: OrganizationCustomRoleKey; @@ -69,6 +69,11 @@ export type ProtectProps = plan?: never; }; +/** + * @deprecated Use {@link ProtectParams} instead. + */ +export type ProtectProps = ProtectParams; + /** * Authorization condition for the `when` prop in ``. * Can be an object specifying role, permission, feature, or plan, diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts index 5148700900f..eeb7dd546d6 100644 --- a/packages/vue/src/components/controlComponents.ts +++ b/packages/vue/src/components/controlComponents.ts @@ -2,7 +2,7 @@ import { deprecated } from '@clerk/shared/deprecated'; import type { HandleOAuthCallbackParams, PendingSessionOptions, - ProtectProps as _ProtectProps, + ProtectParams, RedirectOptions, } from '@clerk/shared/types'; import { defineComponent } from 'vue'; @@ -112,7 +112,7 @@ export const AuthenticateWithRedirectCallback = defineComponent((props: HandleOA return () => null; }); -export type ProtectProps = _ProtectProps & PendingSessionOptions; +export type ProtectProps = ProtectParams & PendingSessionOptions; export const Protect = defineComponent((props: ProtectProps, { slots }) => { const { isLoaded, has, userId } = useAuth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut }); From fe77607b3f9c1706d69aec2599f4a8c8b2d6f657 Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 5 Dec 2025 14:39:58 -0600 Subject: [PATCH 03/17] wip --- .../app-router/server/controlComponents.tsx | 38 ++++++++++++++++++- packages/nextjs/src/components.server.ts | 7 ++-- .../src/components/controlComponents.tsx | 21 ++++++++-- packages/shared/src/types/protect.ts | 13 ++++--- 4 files changed, 65 insertions(+), 14 deletions(-) diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index 2572a7a256f..392823abecd 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -1,4 +1,4 @@ -import type { PendingSessionOptions, ProtectParams } from '@clerk/shared/types'; +import type { PendingSessionOptions, ProtectParams, ShowWhenCondition } from '@clerk/shared/types'; import React from 'react'; import { auth } from './auth'; @@ -9,6 +9,13 @@ export type AppRouterProtectProps = React.PropsWithChildren< } & PendingSessionOptions >; +export type AppRouterShowProps = React.PropsWithChildren< + PendingSessionOptions & { + fallback?: React.ReactNode; + when: ShowWhenCondition; + } +>; + export async function SignedIn( props: React.PropsWithChildren, ): Promise { @@ -74,3 +81,32 @@ export async function Protect(props: AppRouterProtectProps): Promise` to render children based on authorization or sign-in state. + */ +export async function Show(props: AppRouterShowProps): Promise { + const { children, fallback, treatPendingAsSignedOut, when } = props; + const { has, userId } = await auth({ treatPendingAsSignedOut }); + + const resolvedWhen = when; + const authorized = <>{children}; + const unauthorized = fallback ? <>{fallback} : null; + + if (typeof resolvedWhen === 'string') { + if (resolvedWhen === 'signedOut') { + return userId ? unauthorized : authorized; + } + return userId ? authorized : unauthorized; + } + + if (!userId) { + return unauthorized; + } + + if (typeof resolvedWhen === 'function') { + return resolvedWhen(has) ? authorized : unauthorized; + } + + return has(resolvedWhen) ? authorized : unauthorized; +} diff --git a/packages/nextjs/src/components.server.ts b/packages/nextjs/src/components.server.ts index f73c8cc91c5..291aa1df659 100644 --- a/packages/nextjs/src/components.server.ts +++ b/packages/nextjs/src/components.server.ts @@ -1,11 +1,12 @@ import { ClerkProvider } from './app-router/server/ClerkProvider'; -import { Protect, SignedIn, SignedOut } from './app-router/server/controlComponents'; +import { Protect, Show, SignedIn, SignedOut } from './app-router/server/controlComponents'; -export { ClerkProvider, SignedOut, SignedIn, Protect }; +export { ClerkProvider, Protect, Show, SignedIn, SignedOut }; export type ServerComponentsServerModuleTypes = { ClerkProvider: typeof ClerkProvider; + Protect: typeof Protect; + Show: typeof Show; SignedIn: typeof SignedIn; SignedOut: typeof SignedOut; - Protect: typeof Protect; }; diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index 715aa3056ad..4454feedbda 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -71,13 +71,13 @@ export const ClerkDegraded = ({ children }: React.PropsWithChildren) => export type ShowProps = React.PropsWithChildren< { - when: ShowWhenCondition; fallback?: React.ReactNode; + when: ShowWhenCondition; } & PendingSessionOptions >; /** - * Use `` to conditionally render content based on user authorization. + * Use `` to conditionally render content based on user authorization or sign-in state. * * @example * ```tsx @@ -93,6 +93,7 @@ export type ShowProps = React.PropsWithChildren< * * * ``` + * */ export const Show = ({ children, fallback, treatPendingAsSignedOut, when }: ShowProps) => { useAssertWrappedByClerkProvider('Show'); @@ -103,22 +104,34 @@ export const Show = ({ children, fallback, treatPendingAsSignedOut, when }: Show return null; } + const resolvedWhen = when; const authorized = children; const unauthorized = fallback ?? null; + if (resolvedWhen === 'signedOut') { + return userId ? unauthorized : authorized; + } + if (!userId) { return unauthorized; } + if (resolvedWhen === 'signedIn') { + return authorized; + } + // At this point, userId is defined so has() is guaranteed to be available - if (checkAuthorization(when, has!)) { + if (checkAuthorization(resolvedWhen, has!)) { return authorized; } return unauthorized; }; -function checkAuthorization(when: ShowWhenCondition, has: NonNullable['has']>): boolean { +function checkAuthorization( + when: Exclude, + has: NonNullable['has']>, +): boolean { if (typeof when === 'function') { return when(has); } diff --git a/packages/shared/src/types/protect.ts b/packages/shared/src/types/protect.ts index e06cf74afb5..ccca27d82c9 100644 --- a/packages/shared/src/types/protect.ts +++ b/packages/shared/src/types/protect.ts @@ -1,5 +1,5 @@ import type { OrganizationCustomPermissionKey, OrganizationCustomRoleKey } from './organizationMembership'; -import type { CheckAuthorizationWithCustomPermissions } from './session'; +import type { CheckAuthorizationWithCustomPermissions, PendingSessionOptions } from './session'; import type { Autocomplete } from './utils'; /** @@ -80,10 +80,9 @@ export type ProtectProps = ProtectParams; * or a callback function receiving the `has` helper for complex conditions. */ export type ShowWhenCondition = - | { role: OrganizationCustomRoleKey } - | { permission: OrganizationCustomPermissionKey } - | { feature: Autocomplete<`org:${string}` | `user:${string}`> } - | { plan: Autocomplete<`org:${string}` | `user:${string}`> } + | 'signedIn' + | 'signedOut' + | ProtectParams | ((has: CheckAuthorizationWithCustomPermissions) => boolean); /** @@ -106,7 +105,9 @@ export type ShowWhenCondition = * // Require a specific plan * ... * ``` + * */ -export type ShowProps = { +export type ShowProps = PendingSessionOptions & { + fallback?: unknown; when: ShowWhenCondition; }; From e007eed2cf317035b3aae76bc5034f23012666be Mon Sep 17 00:00:00 2001 From: Jacek Date: Fri, 5 Dec 2025 20:15:48 -0600 Subject: [PATCH 04/17] wip --- .../__tests__/controlComponents.test.tsx | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx diff --git a/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx new file mode 100644 index 00000000000..abb1a538d0e --- /dev/null +++ b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import type { ShowWhenCondition } from '@clerk/shared/types'; +import { Show } from '../controlComponents'; +import { auth } from '../auth'; + +vi.mock('../auth', () => ({ + auth: vi.fn(), +})); + +const mockAuth = auth as unknown as ReturnType; + +const render = async (element: Promise) => { + const resolved = await element; + if (!resolved) { + return ''; + } + return renderToStaticMarkup(resolved); +}; + +const setAuthReturn = (value: { has?: (params: unknown) => boolean; userId: string | null }) => { + mockAuth.mockResolvedValue(value); +}; + +const signedInWhen: ShowWhenCondition = 'signedIn'; +const signedOutWhen: ShowWhenCondition = 'signedOut'; + +describe('Show (App Router server)', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders children when signed in', async () => { + const has = vi.fn(); + setAuthReturn({ has, userId: 'user_123' }); + + const html = await render( + Show({ + children:
signed-in
, + fallback:
fallback
, + treatPendingAsSignedOut: false, + when: signedInWhen, + }), + ); + + expect(mockAuth).toHaveBeenCalledWith({ treatPendingAsSignedOut: false }); + expect(html).toContain('signed-in'); + }); + + it('renders children when signed out', async () => { + const has = vi.fn(); + setAuthReturn({ has, userId: null }); + + const html = await render( + Show({ + children:
signed-out
, + fallback:
fallback
, + treatPendingAsSignedOut: false, + when: signedOutWhen, + }), + ); + + expect(html).toContain('signed-out'); + }); + + it('renders fallback when signed out but user is present', async () => { + const has = vi.fn(); + setAuthReturn({ has, userId: 'user_123' }); + + const html = await render( + Show({ + children:
signed-out
, + fallback:
fallback
, + treatPendingAsSignedOut: false, + when: signedOutWhen, + }), + ); + + expect(html).toContain('fallback'); + }); + + it('uses has() when when is an authorization object', async () => { + const has = vi.fn().mockReturnValue(true); + setAuthReturn({ has, userId: 'user_123' }); + + const html = await render( + Show({ + children:
authorized
, + fallback:
fallback
, + treatPendingAsSignedOut: false, + when: { role: 'admin' }, + }), + ); + + expect(has).toHaveBeenCalledWith({ role: 'admin' }); + expect(html).toContain('authorized'); + }); + + it('uses predicate when when is a function', async () => { + const has = vi.fn().mockReturnValue(true); + const predicate = vi.fn().mockReturnValue(true); + setAuthReturn({ has, userId: 'user_123' }); + + const html = await render( + Show({ + children:
predicate-pass
, + fallback:
fallback
, + treatPendingAsSignedOut: false, + when: predicate, + }), + ); + + expect(predicate).toHaveBeenCalledWith(has); + expect(html).toContain('predicate-pass'); + }); +}); From 11726bb31d77ba936d7e81b5ef1f445faa2f9658 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 8 Dec 2025 09:45:53 -0600 Subject: [PATCH 05/17] wip --- packages/astro/src/react/controlComponents.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx index 5a574164748..e9574b30434 100644 --- a/packages/astro/src/react/controlComponents.tsx +++ b/packages/astro/src/react/controlComponents.tsx @@ -1,10 +1,9 @@ -import type { HandleOAuthCallbackParams, PendingSessionOptions } from '@clerk/shared/types'; +import type { HandleOAuthCallbackParams, PendingSessionOptions, ProtectParams } from '@clerk/shared/types'; import { computed } from 'nanostores'; import type { PropsWithChildren } from 'react'; import React, { useEffect, useState } from 'react'; import { $csrState } from '../stores/internal'; -import type { ProtectParams } from '@clerk/shared/types'; import { useAuth } from './hooks'; import type { WithClerkProp } from './utils'; import { withClerk } from './utils'; From b632485ec581661f0f0cb188fc23a311706bb3f2 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 8 Dec 2025 10:11:28 -0600 Subject: [PATCH 06/17] wip --- packages/astro/src/react/controlComponents.tsx | 10 ++++------ packages/react/src/components/controlComponents.tsx | 3 +-- .../src/__tests__/__snapshots__/exports.test.ts.snap | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx index e9574b30434..678e6b56b65 100644 --- a/packages/astro/src/react/controlComponents.tsx +++ b/packages/astro/src/react/controlComponents.tsx @@ -1,12 +1,10 @@ import type { HandleOAuthCallbackParams, PendingSessionOptions, ProtectParams } from '@clerk/shared/types'; import { computed } from 'nanostores'; -import type { PropsWithChildren } from 'react'; -import React, { useEffect, useState } from 'react'; +import React, { type PropsWithChildren, useEffect, useState } from 'react'; import { $csrState } from '../stores/internal'; import { useAuth } from './hooks'; -import type { WithClerkProp } from './utils'; -import { withClerk } from './utils'; +import { withClerk, type WithClerkProp } from './utils'; export function SignedOut({ children, treatPendingAsSignedOut }: PropsWithChildren) { const { userId } = useAuth({ treatPendingAsSignedOut }); @@ -139,9 +137,9 @@ export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAu */ export const AuthenticateWithRedirectCallback = withClerk( ({ clerk, ...handleRedirectCallbackParams }: WithClerkProp) => { - React.useEffect(() => { + useEffect(() => { void clerk?.handleRedirectCallback(handleRedirectCallbackParams); - }, []); + }, [clerk, handleRedirectCallbackParams]); return null; }, diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index 4454feedbda..046d75dece7 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -120,8 +120,7 @@ export const Show = ({ children, fallback, treatPendingAsSignedOut, when }: Show return authorized; } - // At this point, userId is defined so has() is guaranteed to be available - if (checkAuthorization(resolvedWhen, has!)) { + if (checkAuthorization(resolvedWhen, has)) { return authorized; } diff --git a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap index 3e1c592195b..42a6ab133df 100644 --- a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap @@ -34,13 +34,13 @@ exports[`root public exports > should not change unexpectedly 1`] = ` "OrganizationProfile", "OrganizationSwitcher", "PricingTable", - "Protect", "RedirectToCreateOrganization", "RedirectToOrganizationProfile", "RedirectToSignIn", "RedirectToSignUp", "RedirectToTasks", "RedirectToUserProfile", + "Show", "SignIn", "SignInButton", "SignInWithMetamaskButton", From 07a3f7dd34741bf4558bb994229d4d32fa3965d9 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 8 Dec 2025 10:24:13 -0600 Subject: [PATCH 07/17] wip --- .../src/__tests__/__snapshots__/exports.test.ts.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap index 54b196e9899..f3e3a74564e 100644 --- a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap @@ -29,13 +29,13 @@ exports[`root public exports > should not change unexpectedly 1`] = ` "OrganizationProfile", "OrganizationSwitcher", "PricingTable", - "Protect", "RedirectToCreateOrganization", "RedirectToOrganizationProfile", "RedirectToSignIn", "RedirectToSignUp", "RedirectToTasks", "RedirectToUserProfile", + "Show", "SignIn", "SignInButton", "SignInWithMetamaskButton", From 05b73ca3a8a7860f2b93f96edaea7d69aaf7f4e8 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 8 Dec 2025 14:09:00 -0600 Subject: [PATCH 08/17] wip --- .../app-router/server/__tests__/controlComponents.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx index abb1a538d0e..680f8c96b1d 100644 --- a/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx +++ b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx @@ -1,10 +1,10 @@ +import type { ShowWhenCondition } from '@clerk/shared/types'; import React from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import type { ShowWhenCondition } from '@clerk/shared/types'; -import { Show } from '../controlComponents'; import { auth } from '../auth'; +import { Show } from '../controlComponents'; vi.mock('../auth', () => ({ auth: vi.fn(), From 0f345b8a0754d15b2cbf07b4f9db94dac3e79c90 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 8 Dec 2025 14:14:44 -0600 Subject: [PATCH 09/17] wip --- packages/astro/src/react/controlComponents.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx index 678e6b56b65..3950a0a059a 100644 --- a/packages/astro/src/react/controlComponents.tsx +++ b/packages/astro/src/react/controlComponents.tsx @@ -137,9 +137,10 @@ export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAu */ export const AuthenticateWithRedirectCallback = withClerk( ({ clerk, ...handleRedirectCallbackParams }: WithClerkProp) => { + // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { void clerk?.handleRedirectCallback(handleRedirectCallbackParams); - }, [clerk, handleRedirectCallbackParams]); + }, []); return null; }, From 7c0b86c41ba49675b295334d9103d1dec6522a55 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 8 Dec 2025 14:16:50 -0600 Subject: [PATCH 10/17] better JSDoc --- .../app-router/server/controlComponents.tsx | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index 392823abecd..3f50e6684f7 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -83,7 +83,34 @@ export async function Protect(props: AppRouterProtectProps): Promise` to render children based on authorization or sign-in state. + * Use `` to render children when an authorization or sign-in condition passes. + * + * @param props.when Condition that controls rendering. Accepts: + * - authorization objects such as `{ permission: "..." }`, `{ role: "..." }`, `{ feature: "..." }`, or `{ plan: "..." }` + * - the string `"signedIn"` to render when a user is present + * - the string `"signedOut"` to render when no user is present + * - predicate functions `(has) => boolean` that receive the `has` helper + * @param props.fallback Optional content rendered when the condition fails. + * @param props.children Content rendered when the condition passes. + * + * @example + * ```tsx + * Unauthorized

}> + * + *
+ * + * + * + * + * + * has({ permission: "org:read" }) && isFeatureEnabled}> + * + * + * + * + * + * + * ``` */ export async function Show(props: AppRouterShowProps): Promise { const { children, fallback, treatPendingAsSignedOut, when } = props; From c133f9acd540c56f3b1994108b2d7adfafe5caa2 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 8 Dec 2025 14:32:38 -0600 Subject: [PATCH 11/17] wip --- .../transform-protect-to-show.fixtures.js | 9 ++++ .../codemods/transform-protect-to-show.cjs | 2 +- .../upgrade/src/components/SDKWorkflow.js | 53 +++++++++++++++++-- 3 files changed, 58 insertions(+), 6 deletions(-) diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js index e91211cd7b6..8a99c58f739 100644 --- a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js +++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js @@ -24,6 +24,15 @@ import { Protect } from "@clerk/nextjs" `, output: null, }, + { + name: 'Import transform for @clerk/chrome-extension', + source: ` +import { Protect } from "@clerk/chrome-extension" + `, + output: ` +import { Show } from "@clerk/chrome-extension" +`, + }, { name: 'Basic permission prop transform', source: ` diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs index bc20b06ab2e..235e388ebfd 100644 --- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs +++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs @@ -1,5 +1,5 @@ // Packages that are always client-side -const CLIENT_ONLY_PACKAGES = ['@clerk/react', '@clerk/expo']; +const CLIENT_ONLY_PACKAGES = ['@clerk/chrome-extension', '@clerk/expo', '@clerk/react']; // Packages that can be used in both RSC and client components const HYBRID_PACKAGES = ['@clerk/nextjs']; diff --git a/packages/upgrade/src/components/SDKWorkflow.js b/packages/upgrade/src/components/SDKWorkflow.js index ecd2a491c71..d348289adf5 100644 --- a/packages/upgrade/src/components/SDKWorkflow.js +++ b/packages/upgrade/src/components/SDKWorkflow.js @@ -12,6 +12,7 @@ import { UpgradeSDK } from './UpgradeSDK.js'; const CODEMODS = { ASYNC_REQUEST: 'transform-async-request', CLERK_REACT_V6: 'transform-clerk-react-v6', + PROTECT_TO_SHOW: 'transform-protect-to-show', REMOVE_DEPRECATED_PROPS: 'transform-remove-deprecated-props', }; @@ -141,6 +142,7 @@ function NextjsWorkflow({ version, }) { const [v6CodemodComplete, setV6CodemodComplete] = useState(false); + const [removeDeprecatedPropsComplete, setRemoveDeprecatedPropsComplete] = useState(false); const [glob, setGlob] = useState(); return ( @@ -174,12 +176,20 @@ function NextjsWorkflow({ ) : null} {v6CodemodComplete ? ( ) : null} + {removeDeprecatedPropsComplete ? ( + + ) : null} )} {version === 6 && ( @@ -198,12 +208,20 @@ function NextjsWorkflow({ ) : null} {v6CodemodComplete ? ( ) : null} + {removeDeprecatedPropsComplete ? ( + + ) : null} )} {version === 7 && ( @@ -218,12 +236,20 @@ function NextjsWorkflow({ /> {v6CodemodComplete ? ( ) : null} + {removeDeprecatedPropsComplete ? ( + + ) : null} ) : ( <> @@ -302,6 +328,7 @@ function ReactSdkWorkflow({ version, }) { const [v6CodemodComplete, setV6CodemodComplete] = useState(false); + const [removeDeprecatedPropsComplete, setRemoveDeprecatedPropsComplete] = useState(false); const [glob, setGlob] = useState(); const replacePackage = sdk === 'clerk-react' || sdk === 'clerk-expo'; const needsUpgrade = versionNeedsUpgrade(sdk, version); @@ -338,12 +365,20 @@ function ReactSdkWorkflow({ ) : null} {v6CodemodComplete ? ( ) : null} + {removeDeprecatedPropsComplete ? ( + + ) : null} )} {!needsUpgrade && ( @@ -358,12 +393,20 @@ function ReactSdkWorkflow({ /> {v6CodemodComplete ? ( ) : null} + {removeDeprecatedPropsComplete ? ( + + ) : null} ) : ( <> From f774e21783e9f49f3401b530d9720832deac6c95 Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 8 Dec 2025 20:03:22 -0600 Subject: [PATCH 12/17] wip --- packages/astro/src/react/controlComponents.tsx | 1 - .../src/codemods/transform-protect-to-show.cjs | 11 ++++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx index 3950a0a059a..5e9ac4ce889 100644 --- a/packages/astro/src/react/controlComponents.tsx +++ b/packages/astro/src/react/controlComponents.tsx @@ -137,7 +137,6 @@ export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAu */ export const AuthenticateWithRedirectCallback = withClerk( ({ clerk, ...handleRedirectCallbackParams }: WithClerkProp) => { - // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => { void clerk?.handleRedirectCallback(handleRedirectCallbackParams); }, []); diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs index 235e388ebfd..fea4957a723 100644 --- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs +++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs @@ -1,7 +1,7 @@ // Packages that are always client-side -const CLIENT_ONLY_PACKAGES = ['@clerk/chrome-extension', '@clerk/expo', '@clerk/react']; +const CLIENT_ONLY_PACKAGES = ['@clerk/chrome-extension', '@clerk/expo', '@clerk/react', '@clerk/vue']; // Packages that can be used in both RSC and client components -const HYBRID_PACKAGES = ['@clerk/nextjs']; +const HYBRID_PACKAGES = ['@clerk/astro', '@clerk/nextjs']; /** * Checks if a file has a 'use client' directive at the top. @@ -66,6 +66,7 @@ function hasUseClientDirective(root, j) { module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) { const root = j(source); let dirtyFlag = false; + const protectLocalNames = []; const isClientComponent = hasUseClientDirective(root, j); @@ -95,10 +96,14 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) specifiers.forEach(spec => { if (j.ImportSpecifier.check(spec) && spec.imported.name === 'Protect') { + const effectiveLocalName = spec.local ? spec.local.name : spec.imported.name; spec.imported.name = 'Show'; if (spec.local && spec.local.name === 'Protect') { spec.local.name = 'Show'; } + if (!protectLocalNames.includes(effectiveLocalName)) { + protectLocalNames.push(effectiveLocalName); + } dirtyFlag = true; } }); @@ -111,7 +116,7 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) const closingElement = path.node.closingElement; // Check if this is a element - if (!j.JSXIdentifier.check(openingElement.name) || openingElement.name.name !== 'Protect') { + if (!j.JSXIdentifier.check(openingElement.name) || !protectLocalNames.includes(openingElement.name.name)) { return; } From fdbb5cdadb7a5987ecb0b118a91437835ea143bc Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 8 Dec 2025 22:54:17 -0600 Subject: [PATCH 13/17] wip --- .../src/codemods/transform-protect-to-show.cjs | 13 +++++++++---- pnpm-lock.yaml | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs index fea4957a723..c531aaee66b 100644 --- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs +++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs @@ -120,10 +120,15 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) return; } - // Rename to Show - openingElement.name.name = 'Show'; - if (closingElement && j.JSXIdentifier.check(closingElement.name)) { - closingElement.name.name = 'Show'; + const originalName = openingElement.name.name; + + // Only rename if the component was used without an alias (as ). + // For aliased imports (e.g., Protect as MyProtect), keep the alias in place. + if (originalName === 'Protect') { + openingElement.name.name = 'Show'; + if (closingElement && j.JSXIdentifier.check(closingElement.name)) { + closingElement.name.name = 'Show'; + } } const attributes = openingElement.attributes || []; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 232cb752a0f..13de6825908 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2355,7 +2355,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {'0': node >=0.10.0} + engines: {node: '>=0.10.0'} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} From 4a3a968649e183a34e18819b5bd6e6384aaf7e9d Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 9 Dec 2025 10:27:14 -0600 Subject: [PATCH 14/17] update codemod --- .../transform-protect-to-show.fixtures.js | 27 +++++++++++++++++++ .../codemods/transform-protect-to-show.cjs | 2 ++ 2 files changed, 29 insertions(+) diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js index 8a99c58f739..98be7b026f5 100644 --- a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js +++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js @@ -85,6 +85,33 @@ function App() {
); } +`, + }, + { + name: 'Boolean shorthand auth prop transforms to true', + source: ` +import { Protect } from "@clerk/react" + +function App() { + return ( + + + + ) +} + `, + output: ` +import { Show } from "@clerk/react" + +function App() { + return ( + + + + ); +} `, }, { diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs index c531aaee66b..8039c8a718f 100644 --- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs +++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs @@ -169,6 +169,8 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) value = attr.value.expression; } else if (j.StringLiteral.check(attr.value) || j.Literal.check(attr.value)) { value = attr.value; + } else if (attr.value == null) { + value = j.booleanLiteral(true); } else { // Default string value value = j.stringLiteral(attr.value?.value || ''); From 197ae2cca2df3ec287359291ee8ef6366b1cc8b7 Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 9 Dec 2025 12:51:43 -0600 Subject: [PATCH 15/17] backfill codemod --- .../transform-protect-to-show.fixtures.js | 38 +++++++++++++ .../codemods/transform-protect-to-show.cjs | 57 +++++++++++++++---- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js index 98be7b026f5..5ffdb646778 100644 --- a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js +++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js @@ -362,4 +362,42 @@ function Component() { ); }`, }, + { + name: 'Bare Protect defaults to signedIn', + source: ` +import { Protect } from "@clerk/react" + +function App() { + return ( + + + + ) +} + `, + output: ` +import { Show } from "@clerk/react" + +function App() { + return ( + + + + ); +} +`, + }, + { + name: 'ProtectProps import rewrites to ShowProps', + source: ` +import { ProtectProps } from "@clerk/react"; + +type Props = ProtectProps; + `, + output: ` +import { ShowProps } from "@clerk/react"; + +type Props = ShowProps; +`, + }, ]; diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs index 8039c8a718f..b985e83d3cb 100644 --- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs +++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs @@ -67,6 +67,7 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) const root = j(source); let dirtyFlag = false; const protectLocalNames = []; + const protectPropsLocalsToRename = []; const isClientComponent = hasUseClientDirective(root, j); @@ -87,7 +88,7 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) return undefined; } - // Transform imports: Protect → Show + // Transform imports: Protect → Show, ProtectProps → ShowProps const allPackages = [...CLIENT_ONLY_PACKAGES, ...HYBRID_PACKAGES]; allPackages.forEach(packageName => { root.find(j.ImportDeclaration, { source: { value: packageName } }).forEach(path => { @@ -95,21 +96,53 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) const specifiers = node.specifiers || []; specifiers.forEach(spec => { - if (j.ImportSpecifier.check(spec) && spec.imported.name === 'Protect') { - const effectiveLocalName = spec.local ? spec.local.name : spec.imported.name; - spec.imported.name = 'Show'; - if (spec.local && spec.local.name === 'Protect') { - spec.local.name = 'Show'; + if (j.ImportSpecifier.check(spec)) { + if (spec.imported.name === 'Protect') { + const effectiveLocalName = spec.local ? spec.local.name : spec.imported.name; + spec.imported.name = 'Show'; + if (spec.local && spec.local.name === 'Protect') { + spec.local.name = 'Show'; + } + if (!protectLocalNames.includes(effectiveLocalName)) { + protectLocalNames.push(effectiveLocalName); + } + dirtyFlag = true; } - if (!protectLocalNames.includes(effectiveLocalName)) { - protectLocalNames.push(effectiveLocalName); + + if (spec.imported.name === 'ProtectProps') { + const effectiveLocalName = spec.local ? spec.local.name : spec.imported.name; + spec.imported.name = 'ShowProps'; + if (spec.local && spec.local.name === 'ProtectProps') { + spec.local.name = 'ShowProps'; + } + if (effectiveLocalName === 'ProtectProps') { + protectPropsLocalsToRename.push(effectiveLocalName); + } + dirtyFlag = true; } - dirtyFlag = true; } }); }); }); + // Rename references to ProtectProps (only when local name was ProtectProps) + if (protectPropsLocalsToRename.length > 0) { + root + .find(j.TSTypeReference, { + typeName: { + type: 'Identifier', + name: 'ProtectProps', + }, + }) + .forEach(path => { + const typeName = path.node.typeName; + if (j.Identifier.check(typeName) && typeName.name === 'ProtectProps') { + typeName.name = 'ShowProps'; + dirtyFlag = true; + } + }); + } + // Transform JSX: root.find(j.JSXElement).forEach(path => { const openingElement = path.node.openingElement; @@ -185,9 +218,9 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) // Reconstruct attributes with `when` prop const newAttributes = []; - if (whenValue) { - newAttributes.push(j.jsxAttribute(j.jsxIdentifier('when'), whenValue)); - } + const finalWhenValue = whenValue || j.stringLiteral('signedIn'); + + newAttributes.push(j.jsxAttribute(j.jsxIdentifier('when'), finalWhenValue)); // Add remaining attributes (fallback, etc.) otherAttributes.forEach(attr => newAttributes.push(attr)); From 88f3e3541592393841a7ed4e5ca3b6052221020a Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 9 Dec 2025 13:03:32 -0600 Subject: [PATCH 16/17] wip --- packages/upgrade/src/codemods/transform-protect-to-show.cjs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs index b985e83d3cb..f0daf66c185 100644 --- a/packages/upgrade/src/codemods/transform-protect-to-show.cjs +++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs @@ -98,11 +98,9 @@ module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) specifiers.forEach(spec => { if (j.ImportSpecifier.check(spec)) { if (spec.imported.name === 'Protect') { - const effectiveLocalName = spec.local ? spec.local.name : spec.imported.name; + const originalImportedName = spec.imported.name; + const effectiveLocalName = spec.local ? spec.local.name : originalImportedName; spec.imported.name = 'Show'; - if (spec.local && spec.local.name === 'Protect') { - spec.local.name = 'Show'; - } if (!protectLocalNames.includes(effectiveLocalName)) { protectLocalNames.push(effectiveLocalName); } From aba8aad1337bc8827ffc223359cb4a5f9e7fdcd9 Mon Sep 17 00:00:00 2001 From: Jacek Date: Tue, 9 Dec 2025 14:03:18 -0600 Subject: [PATCH 17/17] adjust JSDocs --- .../src/app-router/server/controlComponents.tsx | 13 ++++++------- packages/react/src/components/controlComponents.tsx | 7 +++++++ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index 3f50e6684f7..f2370d9c0e2 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -84,14 +84,13 @@ export async function Protect(props: AppRouterProtectProps): Promise` to render children when an authorization or sign-in condition passes. + * When `treatPendingAsSignedOut` is true, pending sessions are treated as signed out. + * Renders the provided `fallback` (or `null`) when the condition fails. * - * @param props.when Condition that controls rendering. Accepts: - * - authorization objects such as `{ permission: "..." }`, `{ role: "..." }`, `{ feature: "..." }`, or `{ plan: "..." }` - * - the string `"signedIn"` to render when a user is present - * - the string `"signedOut"` to render when no user is present - * - predicate functions `(has) => boolean` that receive the `has` helper - * @param props.fallback Optional content rendered when the condition fails. - * @param props.children Content rendered when the condition passes. + * The `when` prop supports: + * - `"signedIn"` or `"signedOut"` shorthands + * - Authorization objects such as `{ permission: "..." }`, `{ role: "..." }`, `{ feature: "..." }`, or `{ plan: "..." }` + * - Predicate functions `(has) => boolean` that receive the `has` helper * * @example * ```tsx diff --git a/packages/react/src/components/controlComponents.tsx b/packages/react/src/components/controlComponents.tsx index 046d75dece7..b0c5f72f81d 100644 --- a/packages/react/src/components/controlComponents.tsx +++ b/packages/react/src/components/controlComponents.tsx @@ -78,6 +78,13 @@ export type ShowProps = React.PropsWithChildren< /** * Use `` to conditionally render content based on user authorization or sign-in state. + * Returns `null` while auth is loading. Set `treatPendingAsSignedOut` to treat + * pending sessions as signed out during that period. + * + * The `when` prop supports: + * - `"signedIn"` or `"signedOut"` shorthands + * - Authorization descriptors (e.g., `{ permission: "org:billing:manage" }`, `{ role: "admin" }`) + * - A predicate function `(has) => boolean` that receives the `has` helper * * @example * ```tsx