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..76f30d82828
--- /dev/null
+++ b/.changeset/show-the-guards.md
@@ -0,0 +1,11 @@
+---
+'@clerk/astro': major
+'@clerk/chrome-extension': major
+'@clerk/expo': major
+'@clerk/nextjs': major
+'@clerk/react': major
+'@clerk/shared': minor
+'@clerk/vue': major
+---
+
+Introduce `` as the cross-framework authorization control component and remove ``, ``, and `` in favor of ``, updating shared types and framework wrappers to align with the new API.
diff --git a/integration/templates/astro-hybrid/src/pages/index.astro b/integration/templates/astro-hybrid/src/pages/index.astro
index 47168af011b..88ab11cf71c 100644
--- a/integration/templates/astro-hybrid/src/pages/index.astro
+++ b/integration/templates/astro-hybrid/src/pages/index.astro
@@ -1,5 +1,5 @@
---
-import { UserButton, SignInButton, SignedIn, SignedOut } from '@clerk/astro/components';
+import { Show, UserButton, SignInButton } from '@clerk/astro/components';
import { OrganizationSwitcher } from '@clerk/astro/react';
import Layout from '../layouts/Layout.astro';
@@ -7,16 +7,16 @@ export const prerender = true;
---
-
+
Signed out
-
-
+
+
Signed in
-
+
diff --git a/integration/templates/astro-hybrid/src/pages/ssr.astro b/integration/templates/astro-hybrid/src/pages/ssr.astro
index 0db930a6145..0c0611e626f 100644
--- a/integration/templates/astro-hybrid/src/pages/ssr.astro
+++ b/integration/templates/astro-hybrid/src/pages/ssr.astro
@@ -1,5 +1,5 @@
---
-import { UserButton, SignInButton, SignedIn, SignedOut } from '@clerk/astro/components';
+import { Show, UserButton, SignInButton } from '@clerk/astro/components';
import { OrganizationSwitcher } from '@clerk/astro/react';
import Layout from '../layouts/Layout.astro';
@@ -7,16 +7,16 @@ export const prerender = false;
---
-
+
Signed out
-
-
+
+
Signed in
-
+
diff --git a/integration/templates/astro-node/src/layouts/Layout.astro b/integration/templates/astro-node/src/layouts/Layout.astro
index 3e168321da2..17639bb1214 100644
--- a/integration/templates/astro-node/src/layouts/Layout.astro
+++ b/integration/templates/astro-node/src/layouts/Layout.astro
@@ -5,7 +5,7 @@ interface Props {
const { title } = Astro.props;
-import { SignedIn, SignedOut } from '@clerk/astro/components';
+import { Show } from '@clerk/astro/components';
import { LanguagePicker } from '../components/LanguagePicker';
import CustomUserButton from '../components/CustomUserButton.astro';
---
@@ -80,11 +80,11 @@ import CustomUserButton from '../components/CustomUserButton.astro';
-
+
diff --git a/integration/templates/astro-node/src/layouts/react/Layout.astro b/integration/templates/astro-node/src/layouts/react/Layout.astro
index 41b878880e3..4a5fc2be65c 100644
--- a/integration/templates/astro-node/src/layouts/react/Layout.astro
+++ b/integration/templates/astro-node/src/layouts/react/Layout.astro
@@ -5,7 +5,7 @@ interface Props {
const { title } = Astro.props;
-import { SignedIn, SignedOut, UserButton } from '@clerk/astro/react';
+import { Show, UserButton } from '@clerk/astro/react';
import { LanguagePicker } from '../../components/LanguagePicker';
---
@@ -79,11 +79,11 @@ import { LanguagePicker } from '../../components/LanguagePicker';
-
+
diff --git a/integration/templates/astro-node/src/pages/billing/checkout-btn.astro b/integration/templates/astro-node/src/pages/billing/checkout-btn.astro
index 736992e6033..3ae0fbfa9db 100644
--- a/integration/templates/astro-node/src/pages/billing/checkout-btn.astro
+++ b/integration/templates/astro-node/src/pages/billing/checkout-btn.astro
@@ -1,17 +1,17 @@
---
-import { SignedIn, __experimental_CheckoutButton as CheckoutButton } from '@clerk/astro/components';
+import { Show, __experimental_CheckoutButton as CheckoutButton } from '@clerk/astro/components';
import Layout from '../../layouts/Layout.astro';
---
-
+
Checkout Now
-
+
diff --git a/integration/templates/astro-node/src/pages/index.astro b/integration/templates/astro-node/src/pages/index.astro
index 089eac14653..c7a92f9330c 100644
--- a/integration/templates/astro-node/src/pages/index.astro
+++ b/integration/templates/astro-node/src/pages/index.astro
@@ -2,12 +2,12 @@
import Layout from '../layouts/Layout.astro';
import Card from '../components/Card.astro';
-import { SignedIn, SignedOut, SignOutButton, OrganizationSwitcher } from '@clerk/astro/components';
+import { Show, SignOutButton, OrganizationSwitcher } from '@clerk/astro/components';
---
Welcome to Astro
-
+
Sign out!
-
+
@@ -26,7 +26,7 @@ import { SignedIn, SignedOut, SignOutButton, OrganizationSwitcher } from '@clerk
role='list'
class='link-card-grid'
>
-
+
-
-
+
+
-
+
diff --git a/integration/templates/astro-node/src/pages/react/index.astro b/integration/templates/astro-node/src/pages/react/index.astro
index 5fe777167f7..11271836228 100644
--- a/integration/templates/astro-node/src/pages/react/index.astro
+++ b/integration/templates/astro-node/src/pages/react/index.astro
@@ -2,12 +2,12 @@
import Layout from '../../layouts/react/Layout.astro';
import Card from '../../components/Card.astro';
-import { SignedIn, SignedOut, SignOutButton, OrganizationSwitcher } from '@clerk/astro/react';
+import { Show, SignOutButton, OrganizationSwitcher } from '@clerk/astro/react';
---
Welcome to Astro + React
-
+
Sign out!
-
+
@@ -31,7 +31,7 @@ import { SignedIn, SignedOut, SignOutButton, OrganizationSwitcher } from '@clerk
role='list'
class='link-card-grid'
>
-
+
-
-
+
+
-
+
diff --git a/integration/templates/astro-node/src/pages/react/only-admins.astro b/integration/templates/astro-node/src/pages/react/only-admins.astro
index 0ad2bc1b2ba..bc3b46e75d8 100644
--- a/integration/templates/astro-node/src/pages/react/only-admins.astro
+++ b/integration/templates/astro-node/src/pages/react/only-admins.astro
@@ -1,23 +1,28 @@
---
-import { Protect } from '@clerk/astro/react';
+import { Show } from '@clerk/astro/react';
import Layout from '../../layouts/react/Layout.astro';
---
diff --git a/integration/templates/astro-node/src/pages/react/only-members.astro b/integration/templates/astro-node/src/pages/react/only-members.astro
index e0fd91dc11f..df8813acc3b 100644
--- a/integration/templates/astro-node/src/pages/react/only-members.astro
+++ b/integration/templates/astro-node/src/pages/react/only-members.astro
@@ -1,23 +1,28 @@
---
-import { Protect } from '@clerk/astro/react';
+import { Show } from '@clerk/astro/react';
import Layout from '../../layouts/react/Layout.astro';
---
diff --git a/integration/templates/astro-node/src/pages/transitions/index.astro b/integration/templates/astro-node/src/pages/transitions/index.astro
index af29b083fcc..4985e2b77e3 100644
--- a/integration/templates/astro-node/src/pages/transitions/index.astro
+++ b/integration/templates/astro-node/src/pages/transitions/index.astro
@@ -1,15 +1,15 @@
---
-import { SignedIn, SignedOut, UserButton } from '@clerk/astro/components';
+import { Show, UserButton } from '@clerk/astro/components';
import Layout from '../../layouts/ViewTransitionsLayout.astro';
---
diff --git a/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..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,17 +1,44 @@
-import { SignedIn, SignedOut, 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 (
-
+
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 86ba722b3f3..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 { Protect, 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,18 +7,23 @@ export default function Home() {
Loading user button>} />
- SignedIn
- SignedOut
- SignedIn from protect
-
+ SignedIn
+ SignedOut
+
+ 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
-
+
);
}
diff --git a/integration/templates/react-cra/src/App.tsx b/integration/templates/react-cra/src/App.tsx
index 38197953f08..28309fe6b6f 100644
--- a/integration/templates/react-cra/src/App.tsx
+++ b/integration/templates/react-cra/src/App.tsx
@@ -1,15 +1,15 @@
// @ts-ignore
import React from 'react';
import './App.css';
-import { SignedIn, SignedOut, SignIn, UserButton } from '@clerk/react';
+import { Show, SignIn, UserButton } from '@clerk/react';
function App() {
return (
-
+
-
- Signed In
+
+ Signed In
);
diff --git a/integration/templates/react-router-library/src/App.tsx b/integration/templates/react-router-library/src/App.tsx
index 93dfdf04385..259bb2fc944 100644
--- a/integration/templates/react-router-library/src/App.tsx
+++ b/integration/templates/react-router-library/src/App.tsx
@@ -1,15 +1,15 @@
-import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/react-router';
+import { Show, SignInButton, UserButton } from '@clerk/react-router';
import './App.css';
function App() {
return (
);
}
diff --git a/integration/templates/react-router-node/app/routes/home.tsx b/integration/templates/react-router-node/app/routes/home.tsx
index 57161c90b48..9adefddec39 100644
--- a/integration/templates/react-router-node/app/routes/home.tsx
+++ b/integration/templates/react-router-node/app/routes/home.tsx
@@ -1,4 +1,4 @@
-import { SignedIn, SignedOut, UserButton } from '@clerk/react-router';
+import { Show, UserButton } from '@clerk/react-router';
import type { Route } from './+types/home';
export function meta({}: Route.MetaArgs) {
@@ -9,8 +9,8 @@ export default function Home() {
return (
- SignedIn
- SignedOut
+ SignedIn
+ SignedOut
);
}
diff --git a/integration/templates/react-vite/src/App.tsx b/integration/templates/react-vite/src/App.tsx
index 3c7aabd5906..a826457118f 100644
--- a/integration/templates/react-vite/src/App.tsx
+++ b/integration/templates/react-vite/src/App.tsx
@@ -1,4 +1,4 @@
-import { OrganizationSwitcher, SignedIn, SignedOut, UserButton } from '@clerk/react';
+import { OrganizationSwitcher, Show, UserButton } from '@clerk/react';
import { Link } from 'react-router-dom';
import React from 'react';
import { ClientId } from './client-id';
@@ -9,8 +9,8 @@ function App() {
Loading organization switcher>} />
- SignedOut
- SignedIn
+ SignedOut
+ SignedIn
Protected
);
diff --git a/integration/templates/react-vite/src/protected/index.tsx b/integration/templates/react-vite/src/protected/index.tsx
index 2eb58aa8d76..1a8bcccaac5 100644
--- a/integration/templates/react-vite/src/protected/index.tsx
+++ b/integration/templates/react-vite/src/protected/index.tsx
@@ -1,11 +1,11 @@
-import { SignedIn } from '@clerk/react';
+import { Show } from '@clerk/react';
export default function Page() {
return (
);
}
diff --git a/integration/templates/tanstack-react-start/src/routes/index.tsx b/integration/templates/tanstack-react-start/src/routes/index.tsx
index a5c9bfe8dd4..7564211722a 100644
--- a/integration/templates/tanstack-react-start/src/routes/index.tsx
+++ b/integration/templates/tanstack-react-start/src/routes/index.tsx
@@ -1,4 +1,4 @@
-import { SignedIn, UserButton, SignOutButton, SignedOut, SignIn } from '@clerk/tanstack-react-start';
+import { Show, SignIn, SignOutButton, UserButton } from '@clerk/tanstack-react-start';
import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/')({
@@ -9,7 +9,7 @@ function Home() {
return (
Index Route
-
+
You are signed in!
View your profile here
@@ -18,12 +18,12 @@ function Home() {
-
-
+
+
You are signed out
-
+
);
}
diff --git a/integration/templates/vue-vite/src/App.vue b/integration/templates/vue-vite/src/App.vue
index 6477a90213f..c0c615dd2ec 100644
--- a/integration/templates/vue-vite/src/App.vue
+++ b/integration/templates/vue-vite/src/App.vue
@@ -1,5 +1,5 @@
@@ -11,12 +11,12 @@ import LanguagePicker from './components/LanguagePicker.vue';
Vue Clerk Integration test
-
+
-
-
+
+
Sign in
-
+
diff --git a/integration/templates/vue-vite/src/views/Admin.vue b/integration/templates/vue-vite/src/views/Admin.vue
index cda8c50afb7..1a685a48e50 100644
--- a/integration/templates/vue-vite/src/views/Admin.vue
+++ b/integration/templates/vue-vite/src/views/Admin.vue
@@ -1,12 +1,12 @@
-
+
I am an admin
Not an admin
-
+
diff --git a/integration/templates/vue-vite/src/views/Home.vue b/integration/templates/vue-vite/src/views/Home.vue
index e12e3680290..e89dbf87707 100644
--- a/integration/templates/vue-vite/src/views/Home.vue
+++ b/integration/templates/vue-vite/src/views/Home.vue
@@ -1,16 +1,18 @@
-
+
-
+
- Profile
+
+
+ Profile
+
+
diff --git a/integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue b/integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue
index 39c23365733..70c7dbd545e 100644
--- a/integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue
+++ b/integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue
@@ -1,17 +1,17 @@
-
+
Checkout Now
-
+
diff --git a/integration/tests/vue/components.test.ts b/integration/tests/vue/components.test.ts
index c803a6adc6b..c5aa518a358 100644
--- a/integration/tests/vue/components.test.ts
+++ b/integration/tests/vue/components.test.ts
@@ -259,7 +259,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te
await u.po.signIn.waitForMounted();
});
- test('renders component contents to admins', async ({ page, context }) => {
+ test('renders guard contents to admins', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.page.goToRelative('/sign-in');
await u.po.signIn.waitForMounted();
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..c9a4817ef2f
--- /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';
diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx
index 956a9f61347..5950aa83aa4 100644
--- a/packages/astro/src/react/controlComponents.tsx
+++ b/packages/astro/src/react/controlComponents.tsx
@@ -1,30 +1,10 @@
-import type { HandleOAuthCallbackParams, PendingSessionOptions } from '@clerk/shared/types';
+import type { HandleOAuthCallbackParams, PendingSessionOptions, ShowWhenCondition } 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 { ProtectProps as _ProtectProps } from '../types';
import { useAuth } from './hooks';
-import type { WithClerkProp } from './utils';
-import { withClerk } 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;
-}
+import { withClerk, type WithClerkProp } from './utils';
const $isLoadingClerkStore = computed($csrState, state => state.isLoaded);
@@ -69,70 +49,40 @@ export const ClerkLoading = ({ children }: React.PropsWithChildren): JSX.Element
return <>{children}>;
};
-export type ProtectProps = React.PropsWithChildren<
- _ProtectProps & { 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;
};
/**
@@ -140,7 +90,7 @@ export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAu
*/
export const AuthenticateWithRedirectCallback = withClerk(
({ clerk, ...handleRedirectCallbackParams }: WithClerkProp) => {
- React.useEffect(() => {
+ useEffect(() => {
void clerk?.handleRedirectCallback(handleRedirectCallbackParams);
}, []);
diff --git a/packages/chrome-extension/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/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap
index 120fb6d4a1c..9848db006d1 100644
--- a/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap
+++ b/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -15,20 +15,18 @@ exports[`public exports > should not include a breaking change 1`] = `
"OrganizationProfile",
"OrganizationSwitcher",
"PricingTable",
- "Protect",
"RedirectToCreateOrganization",
"RedirectToOrganizationProfile",
"RedirectToSignIn",
"RedirectToSignUp",
"RedirectToUserProfile",
+ "Show",
"SignIn",
"SignInButton",
"SignInWithMetamaskButton",
"SignOutButton",
"SignUp",
"SignUpButton",
- "SignedIn",
- "SignedOut",
"UserAvatar",
"UserButton",
"UserProfile",
diff --git a/packages/chrome-extension/src/react/re-exports.ts b/packages/chrome-extension/src/react/re-exports.ts
index 2838dc6264b..d05f4a29ba5 100644
--- a/packages/chrome-extension/src/react/re-exports.ts
+++ b/packages/chrome-extension/src/react/re-exports.ts
@@ -10,20 +10,18 @@ export {
OrganizationProfile,
OrganizationSwitcher,
PricingTable,
- Protect,
RedirectToCreateOrganization,
RedirectToOrganizationProfile,
RedirectToSignIn,
RedirectToSignUp,
RedirectToUserProfile,
+ Show,
SignIn,
SignInButton,
SignInWithMetamaskButton,
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 bc42b9dbc73..5ef4f45e015 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 } from '@clerk/react';
diff --git a/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx
new file mode 100644
index 00000000000..680f8c96b1d
--- /dev/null
+++ b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx
@@ -0,0 +1,118 @@
+import type { ShowWhenCondition } from '@clerk/shared/types';
+import React from 'react';
+import { renderToStaticMarkup } from 'react-dom/server';
+import { afterEach, describe, expect, it, vi } from 'vitest';
+
+import { auth } from '../auth';
+import { Show } from '../controlComponents';
+
+vi.mock('../auth', () => ({
+ auth: vi.fn(),
+}));
+
+const mockAuth = auth as unknown as ReturnType;
+
+const render = async (element: Promise) => {
+ const resolved = await element;
+ if (!resolved) {
+ return '';
+ }
+ return renderToStaticMarkup(resolved);
+};
+
+const setAuthReturn = (value: { has?: (params: unknown) => boolean; userId: string | null }) => {
+ mockAuth.mockResolvedValue(value);
+};
+
+const signedInWhen: ShowWhenCondition = 'signedIn';
+const signedOutWhen: ShowWhenCondition = 'signedOut';
+
+describe('Show (App Router server)', () => {
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it('renders children when signed in', async () => {
+ const has = vi.fn();
+ setAuthReturn({ has, userId: 'user_123' });
+
+ const html = await render(
+ Show({
+ children: signed-in
,
+ fallback: fallback
,
+ treatPendingAsSignedOut: false,
+ when: signedInWhen,
+ }),
+ );
+
+ expect(mockAuth).toHaveBeenCalledWith({ treatPendingAsSignedOut: false });
+ expect(html).toContain('signed-in');
+ });
+
+ it('renders children when signed out', async () => {
+ const has = vi.fn();
+ setAuthReturn({ has, userId: null });
+
+ const html = await render(
+ Show({
+ children: signed-out
,
+ fallback: fallback
,
+ treatPendingAsSignedOut: false,
+ when: signedOutWhen,
+ }),
+ );
+
+ expect(html).toContain('signed-out');
+ });
+
+ it('renders fallback when signed out but user is present', async () => {
+ const has = vi.fn();
+ setAuthReturn({ has, userId: 'user_123' });
+
+ const html = await render(
+ Show({
+ children: signed-out
,
+ fallback: fallback
,
+ treatPendingAsSignedOut: false,
+ when: signedOutWhen,
+ }),
+ );
+
+ expect(html).toContain('fallback');
+ });
+
+ it('uses has() when when is an authorization object', async () => {
+ const has = vi.fn().mockReturnValue(true);
+ setAuthReturn({ has, userId: 'user_123' });
+
+ const html = await render(
+ Show({
+ children: authorized
,
+ fallback: fallback
,
+ treatPendingAsSignedOut: false,
+ when: { role: 'admin' },
+ }),
+ );
+
+ expect(has).toHaveBeenCalledWith({ role: 'admin' });
+ expect(html).toContain('authorized');
+ });
+
+ it('uses predicate when when is a function', async () => {
+ const has = vi.fn().mockReturnValue(true);
+ const predicate = vi.fn().mockReturnValue(true);
+ setAuthReturn({ has, userId: 'user_123' });
+
+ const html = await render(
+ Show({
+ children: predicate-pass
,
+ fallback: fallback
,
+ treatPendingAsSignedOut: false,
+ when: predicate,
+ }),
+ );
+
+ expect(predicate).toHaveBeenCalledWith(has);
+ expect(html).toContain('predicate-pass');
+ });
+});
diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx
index d640c63a055..c10416a1633 100644
--- a/packages/nextjs/src/app-router/server/controlComponents.tsx
+++ b/packages/nextjs/src/app-router/server/controlComponents.tsx
@@ -1,71 +1,66 @@
-import type { ProtectProps } from '@clerk/react';
-import type { PendingSessionOptions } from '@clerk/shared/types';
+import type { PendingSessionOptions, ShowWhenCondition } from '@clerk/shared/types';
import React from 'react';
import { auth } from './auth';
-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}>;
-}
+export type AppRouterShowProps = React.PropsWithChildren<
+ PendingSessionOptions & {
+ fallback?: React.ReactNode;
+ when: ShowWhenCondition;
+ }
+>;
/**
- * Use ` ` in order to prevent unauthenticated or unauthorized users from accessing the children passed to the component.
+ * Use ` ` 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.
*
- * Examples:
- * ```
- *
- *
- * has({permission:"a_permission_key"})} />
- * has({role:"a_role_key"})} />
- * Unauthorized} />
+ * 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
+ * Unauthorized}>
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * has({ permission: "org:read" }) && isFeatureEnabled}>
+ *
+ *
+ *
+ *
+ *
+ *
* ```
*/
-export async function Protect(props: ProtectProps): Promise {
- const { children, fallback, ...restAuthorizedParams } = props;
- const { has, userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut });
+export async function Show(props: AppRouterShowProps): Promise {
+ const { children, fallback, treatPendingAsSignedOut, when } = props;
+ const { has, userId } = await auth({ treatPendingAsSignedOut });
- /**
- * Fallback to UI provided by user or `null` if authorization checks failed
- */
+ const resolvedWhen = when;
+ const authorized = <>{children}>;
const unauthorized = fallback ? <>{fallback}> : null;
- const authorized = <>{children}>;
+ if (typeof resolvedWhen === 'string') {
+ if (resolvedWhen === 'signedOut') {
+ return userId ? unauthorized : authorized;
+ }
+ return userId ? authorized : unauthorized;
+ }
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 (typeof resolvedWhen === 'function') {
+ return resolvedWhen(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(resolvedWhen) ? authorized : unauthorized;
}
diff --git a/packages/nextjs/src/client-boundary/controlComponents.ts b/packages/nextjs/src/client-boundary/controlComponents.ts
index 1ab240a18f5..544c2e10145 100644
--- a/packages/nextjs/src/client-boundary/controlComponents.ts
+++ b/packages/nextjs/src/client-boundary/controlComponents.ts
@@ -1,20 +1,18 @@
'use client';
export {
- ClerkLoaded,
- ClerkLoading,
+ AuthenticateWithRedirectCallback,
ClerkDegraded,
ClerkFailed,
- SignedOut,
- SignedIn,
- Protect,
+ ClerkLoaded,
+ ClerkLoading,
+ RedirectToCreateOrganization,
+ RedirectToOrganizationProfile,
RedirectToSignIn,
RedirectToSignUp,
RedirectToTasks,
RedirectToUserProfile,
- AuthenticateWithRedirectCallback,
- RedirectToCreateOrganization,
- RedirectToOrganizationProfile,
+ Show,
} 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..4635a9f1367 100644
--- a/packages/nextjs/src/components.client.ts
+++ b/packages/nextjs/src/components.client.ts
@@ -1,2 +1,2 @@
export { ClerkProvider } from './client-boundary/ClerkProvider';
-export { SignedIn, SignedOut, Protect } from './client-boundary/controlComponents';
+export { Show } from './client-boundary/controlComponents';
diff --git a/packages/nextjs/src/components.server.ts b/packages/nextjs/src/components.server.ts
index f73c8cc91c5..11eab24d2e6 100644
--- a/packages/nextjs/src/components.server.ts
+++ b/packages/nextjs/src/components.server.ts
@@ -1,11 +1,9 @@
import { ClerkProvider } from './app-router/server/ClerkProvider';
-import { Protect, SignedIn, SignedOut } from './app-router/server/controlComponents';
+import { Show } from './app-router/server/controlComponents';
-export { ClerkProvider, SignedOut, SignedIn, Protect };
+export { ClerkProvider, Show };
export type ServerComponentsServerModuleTypes = {
ClerkProvider: typeof ClerkProvider;
- SignedIn: typeof SignedIn;
- SignedOut: typeof SignedOut;
- Protect: typeof Protect;
+ Show: typeof Show;
};
diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts
index b9c24e9b7ce..c4123f6729c 100644
--- a/packages/nextjs/src/index.ts
+++ b/packages/nextjs/src/index.ts
@@ -73,6 +73,4 @@ import * as ComponentsModule from '#components';
import type { ServerComponentsServerModuleTypes } from './components.server';
export const ClerkProvider = ComponentsModule.ClerkProvider as ServerComponentsServerModuleTypes['ClerkProvider'];
-export const SignedIn = ComponentsModule.SignedIn as ServerComponentsServerModuleTypes['SignedIn'];
-export const SignedOut = ComponentsModule.SignedOut as ServerComponentsServerModuleTypes['SignedOut'];
-export const Protect = ComponentsModule.Protect as ServerComponentsServerModuleTypes['Protect'];
+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-router/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap
index 54b196e9899..b1fb6544b7b 100644
--- a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap
+++ b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap
@@ -29,21 +29,19 @@ exports[`root public exports > should not change unexpectedly 1`] = `
"OrganizationProfile",
"OrganizationSwitcher",
"PricingTable",
- "Protect",
"RedirectToCreateOrganization",
"RedirectToOrganizationProfile",
"RedirectToSignIn",
"RedirectToSignUp",
"RedirectToTasks",
"RedirectToUserProfile",
+ "Show",
"SignIn",
"SignInButton",
"SignInWithMetamaskButton",
"SignOutButton",
"SignUp",
"SignUpButton",
- "SignedIn",
- "SignedOut",
"TaskChooseOrganization",
"TaskResetPassword",
"UserAvatar",
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 (
- *
+ *
*
* Subscribe Now
*
- *
+ *
* );
* }
* ```
*
- * @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 (
- *
- * View Plan Details
- *
+ *
+ *
+ * View Plan Details
+ *
+ *
* );
* }
* ```
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')}
- * >
- * View Organization Subscription
- *
+ *
+ * console.log('Subscription canceled')}
+ * >
+ * View Organization Subscription
+ *
+ *
* );
* }
* ```
*
- * @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 bdeefbfa05a..eca08e7ec90 100644
--- a/packages/react/src/components/controlComponents.tsx
+++ b/packages/react/src/components/controlComponents.tsx
@@ -1,9 +1,5 @@
import { deprecated } from '@clerk/shared/deprecated';
-import type {
- HandleOAuthCallbackParams,
- PendingSessionOptions,
- ProtectProps as _ProtectProps,
-} from '@clerk/shared/types';
+import type { HandleOAuthCallbackParams, PendingSessionOptions, ShowWhenCondition } from '@clerk/shared/types';
import React from 'react';
import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
@@ -13,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');
@@ -73,76 +49,81 @@ export const ClerkDegraded = ({ children }: React.PropsWithChildren) =>
return children;
};
-export type ProtectProps = React.PropsWithChildren<
- _ProtectProps & {
+export type ShowProps = React.PropsWithChildren<
+ {
fallback?: React.ReactNode;
+ when: ShowWhenCondition;
} & PendingSessionOptions
>;
/**
- * Use ` ` in order to prevent unauthenticated or unauthorized users from accessing the children passed to the component.
+ * Use ` ` to conditionally render content based on user authorization or sign-in state.
+ * Returns `null` while auth is loading. Set `treatPendingAsSignedOut` to treat
+ * pending sessions as signed out during that period.
*
- * Examples:
- * ```
- *
- *
- * has({permission:"a_permission_key"})} />
- * has({role:"a_role_key"})} />
- * Unauthorized} />
+ * 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
+ * Unauthorized}>
+ *
+ *
+ *
+ *
+ *
+ *
+ *
+ * has({ permission: "org:read" }) && isFeatureEnabled}>
+ *
+ *
* ```
+ *
*/
-export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAuthorizedParams }: ProtectProps) => {
- useAssertWrappedByClerkProvider('Protect');
+export const Show = ({ children, fallback, treatPendingAsSignedOut, when }: ShowProps) => {
+ useAssertWrappedByClerkProvider('Show');
- const { isLoaded, has, userId } = useAuth({ treatPendingAsSignedOut });
+ const { has, isLoaded, userId } = useAuth({ treatPendingAsSignedOut });
- /**
- * Avoid flickering children or fallback while clerk is loading sessionId or userId
- */
if (!isLoaded) {
return null;
}
- /**
- * Fallback to UI provided by user or `null` if authorization checks failed
- */
+ const resolvedWhen = when;
+ const authorized = children;
const unauthorized = fallback ?? null;
- const authorized = children;
+ if (resolvedWhen === 'signedOut') {
+ return userId ? unauthorized : authorized;
+ }
if (!userId) {
return unauthorized;
}
- /**
- * Check against the results of `has` called inside the callback
- */
- if (typeof restAuthorizedParams.condition === 'function') {
- if (restAuthorizedParams.condition(has)) {
- return authorized;
- }
- return unauthorized;
+ if (resolvedWhen === 'signedIn') {
+ return authorized;
}
- if (
- restAuthorizedParams.role ||
- restAuthorizedParams.permission ||
- restAuthorizedParams.feature ||
- restAuthorizedParams.plan
- ) {
- if (has(restAuthorizedParams)) {
- return authorized;
- }
- return unauthorized;
+ if (checkAuthorization(resolvedWhen, has)) {
+ return authorized;
}
- /**
- * If neither of the authorization params are passed behave as the ` `.
- * If fallback is present render that instead of rendering nothing.
- */
- return authorized;
+ return unauthorized;
};
+function checkAuthorization(
+ when: Exclude,
+ has: NonNullable['has']>,
+): boolean {
+ if (typeof when === 'function') {
+ return when(has);
+ }
+ return has(when);
+}
+
export const RedirectToSignIn = withClerk(({ clerk, ...props }: WithClerkProp) => {
const { client, session } = clerk;
diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts
index dfbcedcfa93..c200f386236 100644
--- a/packages/react/src/components/index.ts
+++ b/packages/react/src/components/index.ts
@@ -22,18 +22,16 @@ export {
ClerkFailed,
ClerkLoaded,
ClerkLoading,
- Protect,
RedirectToCreateOrganization,
RedirectToOrganizationProfile,
RedirectToSignIn,
RedirectToSignUp,
RedirectToTasks,
RedirectToUserProfile,
- SignedIn,
- SignedOut,
+ Show,
} 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/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/shared/src/types/protect.ts b/packages/shared/src/types/protect.ts
index 0498c2b5f1b..4727dab46cb 100644
--- a/packages/shared/src/types/protect.ts
+++ b/packages/shared/src/types/protect.ts
@@ -1,11 +1,11 @@
import type { OrganizationCustomPermissionKey, OrganizationCustomRoleKey } from './organizationMembership';
-import type { CheckAuthorizationWithCustomPermissions } from './session';
+import type { CheckAuthorizationWithCustomPermissions, PendingSessionOptions } from './session';
import type { Autocomplete } from './utils';
/**
- * Props for the ` ` component, which restricts access to its children based on authentication and authorization.
+ * Authorization parameters used by ` ` and `auth.protect()`.
*
- * Use `ProtectProps` to specify the required Role, Permission, Feature, or Plan for access.
+ * Use `ProtectParams` to specify the required role, permission, feature, or plan for access.
*
* @example
* ```tsx
@@ -21,11 +21,11 @@ import type { Autocomplete } from './utils';
* // Require a specific Feature
*
*
- * // Require a specific Plan
+ * // Require a specific plan
*
* ```
*/
-export type ProtectProps =
+export type ProtectParams =
| {
condition?: never;
role: OrganizationCustomRoleKey;
@@ -68,3 +68,46 @@ export type ProtectProps =
feature?: never;
plan?: never;
};
+
+/**
+ * @deprecated Use {@link ProtectParams} instead.
+ */
+export type ProtectProps = ProtectParams;
+
+/**
+ * Authorization condition for the `when` prop in ` `.
+ * Can be an object specifying role, permission, feature, or plan,
+ * or a callback function receiving the `has` helper for complex conditions.
+ */
+export type ShowWhenCondition =
+ | 'signedIn'
+ | 'signedOut'
+ | ProtectParams
+ | ((has: CheckAuthorizationWithCustomPermissions) => boolean);
+
+/**
+ * Props for the ` ` component, which conditionally renders children based on authorization.
+ *
+ * @example
+ * ```tsx
+ * // Require a specific permission
+ * ...
+ *
+ * // Require a specific role
+ * ...
+ *
+ * // Use a custom condition callback
+ * has({ permission: "org:read" }) && someCondition}>...
+ *
+ * // Require a specific feature
+ * ...
+ *
+ * // Require a specific plan
+ * ...
+ * ```
+ *
+ */
+export type ShowProps = PendingSessionOptions & {
+ fallback?: unknown;
+ when: ShowWhenCondition;
+};
diff --git a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap
index 3e1c592195b..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
@@ -34,21 +34,19 @@ exports[`root public exports > should not change unexpectedly 1`] = `
"OrganizationProfile",
"OrganizationSwitcher",
"PricingTable",
- "Protect",
"RedirectToCreateOrganization",
"RedirectToOrganizationProfile",
"RedirectToSignIn",
"RedirectToSignUp",
"RedirectToTasks",
"RedirectToUserProfile",
+ "Show",
"SignIn",
"SignInButton",
"SignInWithMetamaskButton",
"SignOutButton",
"SignUp",
"SignUpButton",
- "SignedIn",
- "SignedOut",
"TaskChooseOrganization",
"TaskResetPassword",
"UserAvatar",
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..4b40915c4df
--- /dev/null
+++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
@@ -0,0 +1,315 @@
+export const fixtures = [
+ {
+ name: 'Transforms Protect import',
+ source: `
+import { Protect } from "@clerk/react"
+ `,
+ output: `
+import { Show } from "@clerk/react"
+`,
+ },
+ {
+ name: 'Transforms SignedIn and SignedOut imports',
+ source: `
+import { SignedIn, SignedOut } from "@clerk/react"
+ `,
+ output: `
+import { Show } from "@clerk/react";
+`,
+ },
+ {
+ name: 'Transforms Protect in TSX',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ )
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ );
+}
+`,
+ },
+ {
+ name: 'Transforms SignedIn usage',
+ source: `
+import { SignedIn } from "@clerk/react"
+
+const App = () => (
+
+ Child
+
+)
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+const App = () => (
+
+ Child
+
+)
+`,
+ },
+ {
+ name: 'Transforms SignedOut usage',
+ source: `
+import { SignedOut } from "@clerk/react"
+
+const App = () => (
+
+ Child
+
+)
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+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
+
+)
+`,
+ },
+ {
+ name: 'Transforms Protect condition callback',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App() {
+ return (
+ has({ role: "admin" })}>
+
+
+ )
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App() {
+ return (
+ has({ role: "admin" })}>
+
+
+ );
+}
+`,
+ },
+ {
+ name: 'Transforms SignedIn import with other specifiers',
+ source: `
+import { ClerkProvider, SignedIn } from "@clerk/nextjs"
+ `,
+ output: `
+import { ClerkProvider, Show } from "@clerk/nextjs"
+`,
+ },
+ {
+ name: 'Transforms ProtectProps type',
+ source: `
+import { ProtectProps } from "@clerk/react";
+type Props = ProtectProps;
+ `,
+ output: `
+import { ShowProps } from "@clerk/react";
+type Props = ShowProps;
+`,
+ },
+ {
+ name: 'Self-closing Protect defaults to signedIn',
+ source: `
+import { Protect } from "@clerk/react"
+
+const Thing = () =>
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+const Thing = () =>
+`,
+ },
+ {
+ name: 'Transforms Protect from hybrid package without client directive',
+ source: `
+import { Protect } from "@clerk/nextjs"
+
+const App = () => (
+
+ Child
+
+)
+ `,
+ output: `
+import { Show } from "@clerk/nextjs"
+
+const App = () => (
+
+ Child
+
+)
+`,
+ },
+ {
+ name: 'Transforms SignedOut to Show with fallback prop',
+ source: `
+import { SignedOut } from "@clerk/react"
+
+const App = () => (
+ }>
+ Child
+
+)
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+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
+
+)
+`,
+ },
+ {
+ name: 'Aliased Protect import is transformed',
+ source: `
+import { Protect as CanAccess } from "@clerk/react"
+
+function App() {
+ return (
+
+
+
+ )
+}
+ `,
+ output: `
+import { Show as CanAccess } from "@clerk/react"
+
+function App() {
+ 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;
+`,
+ },
+ {
+ name: 'Protect with fallback prop',
+ source: `
+import { Protect } from "@clerk/react"
+
+function App() {
+ return (
+ }>
+
+
+ )
+}
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+function App() {
+ return (
+ }>
+
+
+ );
+}
+`,
+ },
+ {
+ name: 'Protect with spread props',
+ source: `
+import { Protect } from "@clerk/react"
+
+const props = { permission: "org:read" }
+const App = () =>
+ `,
+ output: `
+import { Show } from "@clerk/react"
+
+const props = { permission: "org:read" }
+const App = () =>
+`,
+ },
+];
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..cc50aef7416
--- /dev/null
+++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
@@ -0,0 +1,246 @@
+// Packages that are always client-side
+const CLIENT_ONLY_PACKAGES = ['@clerk/chrome-extension', '@clerk/expo', '@clerk/react', '@clerk/vue'];
+// Packages that can be used in both RSC and client components
+const HYBRID_PACKAGES = ['@clerk/astro', '@clerk/nextjs'];
+
+/**
+ * Transforms `` component usage to `` component.
+ *
+ * Handles the following transformations:
+ * - `` → ``
+ * - `` → ``
+ * - `` → ``
+ * - `` → ``
+ * - ` ...}>` → ` ...}>`
+ * - `...` → `...`
+ * - `...` → `...`
+ *
+ * Also updates imports from `Protect` to `Show`.
+ *
+ * @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 componentKindByLocalName = {};
+ const protectPropsLocalsToRename = [];
+ const namespaceImports = new Set();
+
+ // 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 => {
+ const node = path.node;
+ 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;
+
+ 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 (spec.local && spec.local.name === originalImportedName) {
+ spec.local.name = 'Show';
+ }
+ dirtyFlag = true;
+ }
+
+ 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;
+ }
+ }
+ });
+
+ 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;
+ }, []);
+ });
+ });
+
+ // 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;
+ const closingElement = path.node.closingElement;
+
+ let kind = null;
+ let renameNodeToShow = null;
+
+ 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;
+ }
+
+ if (renameNodeToShow) {
+ renameNodeToShow(openingElement.name);
+ if (closingElement && closingElement.name) {
+ renameNodeToShow(closingElement.name);
+ }
+ }
+
+ 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 (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) {
+ // 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 if (attr.value == null) {
+ value = j.booleanLiteral(true);
+ } 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 = [];
+
+ const defaultWhenValue = kind === 'signedOut' ? 'signedOut' : 'signedIn';
+ const finalWhenValue = whenValue || j.stringLiteral(defaultWhenValue);
+
+ newAttributes.push(j.jsxAttribute(j.jsxIdentifier('when'), finalWhenValue));
+
+ // Add remaining attributes (fallback, etc.)
+ otherAttributes.forEach(attr => newAttributes.push(attr));
+
+ openingElement.attributes = newAttributes;
+ dirtyFlag = true;
+ });
+
+ if (!dirtyFlag) {
+ return undefined;
+ }
+
+ let result = root.toSource();
+ // Fix double semicolons that can occur when recast reprints directive prologues
+ result = result.replace(/^(['"`][^'"`]+['"`]);;/gm, '$1;');
+ return result;
+};
+
+module.exports.parser = 'tsx';
diff --git a/packages/vue/src/components/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') {
diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts
index 5148700900f..8422a75b1eb 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,
- ProtectProps as _ProtectProps,
RedirectOptions,
+ ShowWhenCondition,
} 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,28 @@ export const AuthenticateWithRedirectCallback = defineComponent((props: HandleOA
return () => null;
});
-export type ProtectProps = _ProtectProps & PendingSessionOptions;
+/**
+ * Props for `` that control when content renders based on sign-in or authorization state.
+ *
+ * @public
+ * @property fallback Optional content shown when the condition fails; can be provided via prop or `fallback` slot.
+ * @property when Condition controlling visibility; supports `"signedIn"`, `"signedOut"`, authorization descriptors, or a predicate that receives the `has` helper.
+ * @property treatPendingAsSignedOut Inherited from `PendingSessionOptions`; treat pending sessions as signed out while loading.
+ * @example
+ * ```vue
+ *
+ *
+ *
+ *
+ *
+ * Not authorized
+ *
+ *
+ * ```
+ */
+export type ShowProps = PendingSessionOptions & { fallback?: unknown; when: ShowWhenCondition };
-export const Protect = defineComponent((props: ProtectProps, { slots }) => {
+export const Show = defineComponent((props: ShowProps, { slots }) => {
const { isLoaded, has, userId } = useAuth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut });
return () => {
@@ -125,37 +132,33 @@ 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;
+
+ if (props.when === 'signedOut') {
+ return userId.value ? unauthorized : authorized;
}
- /**
- * 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 (!userId.value) {
+ return unauthorized;
+ }
- return slots.fallback?.();
+ if (props.when === 'signedIn') {
+ return authorized;
}
- if (props.role || props.permission || props.feature || props.plan) {
- if (has.value?.(props)) {
- return slots.default?.();
- }
+ const hasValue = has.value;
- return slots.fallback?.();
+ if (!hasValue) {
+ return 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?.();
+ if (typeof props.when === 'function') {
+ return props.when(hasValue) ? authorized : unauthorized;
+ }
+
+ return hasValue(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 (
<>
-
+
Home
@@ -20,8 +20,8 @@ export const NavBar = () => {
-
-
+
+
Home
@@ -37,7 +37,7 @@ export const NavBar = () => {
-
+
>
)
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..6191257178e 100644
--- a/playground/nextjs/app/app-dir/client/page.tsx
+++ b/playground/nextjs/app/app-dir/client/page.tsx
@@ -1,13 +1,11 @@
'use client';
-import { SignedIn, SignedOut } from '@clerk/nextjs';
+import { Show } from '@clerk/nextjs';
export default function Page() {
return (
- {/* @ts-ignore */}
- Hello In
- {/* @ts-ignore */}
- Hello Out
+ Hello In
+ 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={
<>
-
+
-
-
+
+
-
+
>
}
/>
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4bbb3ed5f9a..3c167fe6a74 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==}