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 (
- +
Protected
-
+
); } 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 + * + * + * + * + * + * + * + * + * ``` + */ 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 @@ 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 @@ 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 @@ 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'; ---
- - -

Not an admin

-
Go to Members Page -

I'm an admin

- + + + !has({ role: 'org:admin' })} + > +

Not an admin

+ + Go to Members Page + +
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'; ---
- - -

Not a member

- Go to Admin Page -

I'm a member

-
+ + + !has({ role: 'basic_member' })} + > +

Not a member

+ + Go to Admin Page + +
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'; ---
- + Sign in - - + + - +
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); + }