diff --git a/.changeset/migrate-to-show.md b/.changeset/migrate-to-show.md
new file mode 100644
index 00000000000..ddf86e9fbd0
--- /dev/null
+++ b/.changeset/migrate-to-show.md
@@ -0,0 +1,5 @@
+---
+'@clerk/upgrade': minor
+---
+
+Add a `transform-protect-to-show` codemod that migrates ``, ``, `` usages 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/presets/envs.ts b/integration/presets/envs.ts
index a1ebb09ec63..b85cc6926ce 100644
--- a/integration/presets/envs.ts
+++ b/integration/presets/envs.ts
@@ -81,26 +81,6 @@ const withEmailCodesQuickstart = withEmailCodes
.setEnvVariable('public', 'CLERK_SIGN_IN_URL', '')
.setEnvVariable('public', 'CLERK_SIGN_UP_URL', '');
-const withAPCore1ClerkV4 = environmentConfig()
- .setId('withAPCore1ClerkV4')
- .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true)
- .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-email-codes').sk)
- .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-email-codes').pk);
-
-// Uses staging instance which runs Core 3
-const withAPCore3ClerkV4 = environmentConfig()
- .setId('withAPCore3ClerkV4')
- .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true)
- .setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev')
- .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing-staging').sk)
- .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing-staging').pk);
-
-const withAPCore1ClerkV6 = environmentConfig()
- .setId('withAPCore1ClerkV6')
- .setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true)
- .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-email-codes').sk)
- .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-email-codes').pk);
-
// Uses staging instance which runs Core 3
const withAPCore3ClerkV6 = environmentConfig()
.setId('withAPCore3ClerkV6')
@@ -213,9 +193,6 @@ export const envs = {
base,
sessionsProd1,
withAPIKeys,
- withAPCore1ClerkV4,
- withAPCore1ClerkV6,
- withAPCore3ClerkV4,
withAPCore3ClerkLatest,
withAPCore3ClerkV6,
withBilling,
diff --git a/integration/presets/next.ts b/integration/presets/next.ts
index 4bae080f834..dd68d2068d9 100644
--- a/integration/presets/next.ts
+++ b/integration/presets/next.ts
@@ -26,25 +26,12 @@ const appRouterQuickstart = appRouter
const appRouterAPWithClerkNextLatest = appRouterQuickstart.clone().setName('next-app-router-ap-clerk-next-latest');
-const appRouterAPWithClerkNextV4 = appRouterQuickstart
+const appRouterQuickstartV6 = appRouter
.clone()
- .setName('next-app-router-ap-clerk-next-v4')
- .addDependency('@clerk/nextjs', '4')
- .addFile(
- 'src/middleware.ts',
- () => `import { authMiddleware } from '@clerk/nextjs';
+ .setName('next-app-router-quickstart-v6')
+ .useTemplate(templates['next-app-router-quickstart-v6']);
- export default authMiddleware({
- publicRoutes: ['/']
- });
-
- export const config = {
- matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'],
- };
- `,
- );
-
-const appRouterAPWithClerkNextV6 = appRouterQuickstart
+const appRouterAPWithClerkNextV6 = appRouterQuickstartV6
.clone()
.setName('next-app-router-ap-clerk-next-v6')
.addDependency('@clerk/nextjs', '6');
@@ -54,6 +41,6 @@ export const next = {
appRouterTurbo,
appRouterQuickstart,
appRouterAPWithClerkNextLatest,
- appRouterAPWithClerkNextV4,
appRouterAPWithClerkNextV6,
+ appRouterQuickstartV6,
} as const;
diff --git a/integration/scripts/logger.ts b/integration/scripts/logger.ts
index 8b690573bb2..b27c60a845b 100644
--- a/integration/scripts/logger.ts
+++ b/integration/scripts/logger.ts
@@ -34,6 +34,11 @@ export const createLogger = (opts: CreateLoggerOptions) => {
console.info(`${chalk[prefixColor](`[${prefix}]`)} ${msg}`);
}
},
+ warn: (msg: string, error?: unknown) => {
+ const errorMsg = error instanceof Error ? error.message : String(error ?? '');
+ const fullMsg = errorMsg ? `${msg} ${errorMsg}` : msg;
+ console.warn(`${chalk.yellow(`[${prefix}]`)} ${fullMsg}`);
+ },
child: (childOpts: CreateLoggerOptions) => {
return createLogger({ prefix: `${prefix} :: ${childOpts.prefix}`, color: prefixColor });
},
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/only-admins.astro b/integration/templates/astro-hybrid/src/pages/only-admins.astro
index 9a786b993a0..a185fa480cb 100644
--- a/integration/templates/astro-hybrid/src/pages/only-admins.astro
+++ b/integration/templates/astro-hybrid/src/pages/only-admins.astro
@@ -1,13 +1,13 @@
---
-import { Protect } from '@clerk/astro/components';
+import { Show } from '@clerk/astro/components';
import Layout from '../layouts/Layout.astro';
export const prerender = true;
---
-
+
I'm an admin
Not an admin
-
+
diff --git a/integration/templates/astro-hybrid/src/pages/only-members.astro b/integration/templates/astro-hybrid/src/pages/only-members.astro
index cf6f6b05e48..0ab0261ce81 100644
--- a/integration/templates/astro-hybrid/src/pages/only-members.astro
+++ b/integration/templates/astro-hybrid/src/pages/only-members.astro
@@ -1,16 +1,16 @@
---
-import { Protect } from '@clerk/astro/components';
+import { Show } from '@clerk/astro/components';
import Layout from '../layouts/Layout.astro';
export const prerender = false;
---
-
I'm a member
Not a member
-
+
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/only-admins.astro b/integration/templates/astro-node/src/pages/only-admins.astro
index f2241732454..8fcb3f86062 100644
--- a/integration/templates/astro-node/src/pages/only-admins.astro
+++ b/integration/templates/astro-node/src/pages/only-admins.astro
@@ -1,11 +1,12 @@
---
-import { Protect } from '@clerk/astro/components';
+import { Show } from '@clerk/astro/components';
import Layout from '../layouts/Layout.astro';
---
diff --git a/integration/templates/astro-node/src/pages/only-members.astro b/integration/templates/astro-node/src/pages/only-members.astro
index f013bd27cdb..2f220e5760a 100644
--- a/integration/templates/astro-node/src/pages/only-members.astro
+++ b/integration/templates/astro-node/src/pages/only-members.astro
@@ -1,11 +1,12 @@
---
-import { Protect } from '@clerk/astro/components';
+import { Show } from '@clerk/astro/components';
import Layout from '../layouts/Layout.astro';
---
diff --git a/integration/templates/astro-node/src/pages/pricing-table.astro b/integration/templates/astro-node/src/pages/pricing-table.astro
index 85539e1158f..2e6bbfc6d09 100644
--- a/integration/templates/astro-node/src/pages/pricing-table.astro
+++ b/integration/templates/astro-node/src/pages/pricing-table.astro
@@ -1,5 +1,5 @@
---
-import { Protect, PricingTable } from '@clerk/astro/components';
+import { Show, PricingTable } from '@clerk/astro/components';
import Layout from '../layouts/Layout.astro';
const newSubscriptionRedirectUrl = Astro.url.searchParams.get('newSubscriptionRedirectUrl');
@@ -7,15 +7,15 @@ const newSubscriptionRedirectUrl = Astro.url.searchParams.get('newSubscriptionRe
-
+
user in free
-
-
+
+
user in pro
-
-
+
+
user in plus
-
+
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..51f20301b6e 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/server-islands.astro b/integration/templates/astro-node/src/pages/server-islands.astro
index 47f43bb3aef..19f27ff1694 100644
--- a/integration/templates/astro-node/src/pages/server-islands.astro
+++ b/integration/templates/astro-node/src/pages/server-islands.astro
@@ -1,24 +1,26 @@
---
-import { Protect } from '@clerk/astro/components';
+import { Show } from '@clerk/astro/components';
import Layout from '../layouts/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/index.ts b/integration/templates/index.ts
index eb97913a159..14875609189 100644
--- a/integration/templates/index.ts
+++ b/integration/templates/index.ts
@@ -6,6 +6,7 @@ export const templates = {
// 'next-app-router': fileURLToPath(new URL('./next-app-router', import.meta.url)),
'next-app-router': resolve(__dirname, './next-app-router'),
'next-app-router-quickstart': resolve(__dirname, './next-app-router-quickstart'),
+ 'next-app-router-quickstart-v6': resolve(__dirname, './next-app-router-quickstart-v6'),
'react-cra': resolve(__dirname, './react-cra'),
'react-vite': resolve(__dirname, './react-vite'),
'express-vite': resolve(__dirname, './express-vite'),
diff --git a/integration/templates/next-app-router-quickstart-v6/.gitignore b/integration/templates/next-app-router-quickstart-v6/.gitignore
new file mode 100644
index 00000000000..8f322f0d8f4
--- /dev/null
+++ b/integration/templates/next-app-router-quickstart-v6/.gitignore
@@ -0,0 +1,35 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.js
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env*.local
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
diff --git a/integration/templates/next-app-router-quickstart-v6/README.md b/integration/templates/next-app-router-quickstart-v6/README.md
new file mode 100644
index 00000000000..f4da3c4c1cf
--- /dev/null
+++ b/integration/templates/next-app-router-quickstart-v6/README.md
@@ -0,0 +1,34 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+# or
+pnpm dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
+
+This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/integration/templates/next-app-router-quickstart-v6/next.config.js b/integration/templates/next-app-router-quickstart-v6/next.config.js
new file mode 100644
index 00000000000..954fac0d40b
--- /dev/null
+++ b/integration/templates/next-app-router-quickstart-v6/next.config.js
@@ -0,0 +1,8 @@
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+ eslint: {
+ ignoreDuringBuilds: true,
+ },
+};
+
+module.exports = nextConfig;
diff --git a/integration/templates/next-app-router-quickstart-v6/package.json b/integration/templates/next-app-router-quickstart-v6/package.json
new file mode 100644
index 00000000000..355009e1e5c
--- /dev/null
+++ b/integration/templates/next-app-router-quickstart-v6/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "next-app-router-quickstart-v6",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "build": "next build",
+ "dev": "next dev",
+ "lint": "next lint",
+ "start": "next start"
+ },
+ "dependencies": {
+ "@types/node": "^20.12.12",
+ "@types/react": "18.3.12",
+ "@types/react-dom": "18.3.1",
+ "next": "^15.0.1",
+ "react": "18.3.1",
+ "react-dom": "18.3.1",
+ "typescript": "^5.7.3"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ }
+}
diff --git a/integration/templates/next-app-router-quickstart-v6/public/next.svg b/integration/templates/next-app-router-quickstart-v6/public/next.svg
new file mode 100644
index 00000000000..5174b28c565
--- /dev/null
+++ b/integration/templates/next-app-router-quickstart-v6/public/next.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/integration/templates/next-app-router-quickstart-v6/public/vercel.svg b/integration/templates/next-app-router-quickstart-v6/public/vercel.svg
new file mode 100644
index 00000000000..d2f84222734
--- /dev/null
+++ b/integration/templates/next-app-router-quickstart-v6/public/vercel.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/integration/templates/next-app-router-quickstart-v6/src/app/favicon.ico b/integration/templates/next-app-router-quickstart-v6/src/app/favicon.ico
new file mode 100644
index 00000000000..718d6fea483
Binary files /dev/null and b/integration/templates/next-app-router-quickstart-v6/src/app/favicon.ico differ
diff --git a/integration/templates/next-app-router-quickstart-v6/src/app/globals.css b/integration/templates/next-app-router-quickstart-v6/src/app/globals.css
new file mode 100644
index 00000000000..760b257c8cc
--- /dev/null
+++ b/integration/templates/next-app-router-quickstart-v6/src/app/globals.css
@@ -0,0 +1,78 @@
+:root {
+ --max-width: 1100px;
+ --border-radius: 12px;
+ --font-mono:
+ ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono', 'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace',
+ 'Source Code Pro', 'Fira Mono', 'Droid Sans Mono', 'Courier New', monospace;
+
+ --foreground-rgb: 0, 0, 0;
+ --background-start-rgb: 214, 219, 220;
+ --background-end-rgb: 255, 255, 255;
+
+ --primary-glow: conic-gradient(
+ from 180deg at 50% 50%,
+ #16abff33 0deg,
+ #0885ff33 55deg,
+ #54d6ff33 120deg,
+ #0071ff33 160deg,
+ transparent 360deg
+ );
+ --secondary-glow: radial-gradient(rgba(255, 255, 255, 1), rgba(255, 255, 255, 0));
+
+ --tile-start-rgb: 239, 245, 249;
+ --tile-end-rgb: 228, 232, 233;
+ --tile-border: conic-gradient(#00000080, #00000040, #00000030, #00000020, #00000010, #00000010, #00000080);
+
+ --callout-rgb: 238, 240, 241;
+ --callout-border-rgb: 172, 175, 176;
+ --card-rgb: 180, 185, 188;
+ --card-border-rgb: 131, 134, 135;
+}
+
+@media (prefers-color-scheme: dark) {
+ :root {
+ --foreground-rgb: 255, 255, 255;
+ --background-start-rgb: 0, 0, 0;
+ --background-end-rgb: 0, 0, 0;
+
+ --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0));
+ --secondary-glow: linear-gradient(to bottom right, rgba(1, 65, 255, 0), rgba(1, 65, 255, 0), rgba(1, 65, 255, 0.3));
+
+ --tile-start-rgb: 2, 13, 46;
+ --tile-end-rgb: 2, 5, 19;
+ --tile-border: conic-gradient(#ffffff80, #ffffff40, #ffffff30, #ffffff20, #ffffff10, #ffffff10, #ffffff80);
+
+ --callout-rgb: 20, 20, 20;
+ --callout-border-rgb: 108, 108, 108;
+ --card-rgb: 100, 100, 100;
+ --card-border-rgb: 200, 200, 200;
+ }
+}
+
+* {
+ box-sizing: border-box;
+ padding: 0;
+ margin: 0;
+}
+
+html,
+body {
+ max-width: 100vw;
+ overflow-x: hidden;
+}
+
+body {
+ color: rgb(var(--foreground-rgb));
+ background: linear-gradient(to bottom, transparent, rgb(var(--background-end-rgb))) rgb(var(--background-start-rgb));
+}
+
+a {
+ color: inherit;
+ text-decoration: none;
+}
+
+@media (prefers-color-scheme: dark) {
+ html {
+ color-scheme: dark;
+ }
+}
diff --git a/integration/templates/next-app-router-quickstart-v6/src/app/layout.tsx b/integration/templates/next-app-router-quickstart-v6/src/app/layout.tsx
new file mode 100644
index 00000000000..411ba883c93
--- /dev/null
+++ b/integration/templates/next-app-router-quickstart-v6/src/app/layout.tsx
@@ -0,0 +1,26 @@
+import './globals.css';
+import { Inter } from 'next/font/google';
+import { ClerkProvider } from '@clerk/nextjs';
+
+const inter = Inter({ subsets: ['latin'] });
+
+export const metadata = {
+ title: 'Create Next App',
+ description: 'Generated by create next app',
+};
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/integration/templates/next-app-router-quickstart-v6/src/app/page.module.css b/integration/templates/next-app-router-quickstart-v6/src/app/page.module.css
new file mode 100644
index 00000000000..14b1649f654
--- /dev/null
+++ b/integration/templates/next-app-router-quickstart-v6/src/app/page.module.css
@@ -0,0 +1,223 @@
+.main {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ align-items: center;
+ padding: 6rem;
+ min-height: 100vh;
+}
+
+.description {
+ display: inherit;
+ justify-content: inherit;
+ align-items: inherit;
+ font-size: 0.85rem;
+ max-width: var(--max-width);
+ width: 100%;
+ z-index: 2;
+ font-family: var(--font-mono);
+}
+
+.description a {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.description p {
+ position: relative;
+ margin: 0;
+ padding: 1rem;
+ background-color: rgba(var(--callout-rgb), 0.5);
+ border: 1px solid rgba(var(--callout-border-rgb), 0.3);
+ border-radius: var(--border-radius);
+}
+
+.code {
+ font-weight: 700;
+ font-family: var(--font-mono);
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(25%, auto));
+ width: var(--max-width);
+ max-width: 100%;
+}
+
+.card {
+ padding: 1rem 1.2rem;
+ border-radius: var(--border-radius);
+ background: rgba(var(--card-rgb), 0);
+ border: 1px solid rgba(var(--card-border-rgb), 0);
+ transition:
+ background 200ms,
+ border 200ms;
+}
+
+.card span {
+ display: inline-block;
+ transition: transform 200ms;
+}
+
+.card h2 {
+ font-weight: 600;
+ margin-bottom: 0.7rem;
+}
+
+.card p {
+ margin: 0;
+ opacity: 0.6;
+ font-size: 0.9rem;
+ line-height: 1.5;
+ max-width: 30ch;
+}
+
+.center {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ position: relative;
+ padding: 4rem 0;
+}
+
+.center::before {
+ background: var(--secondary-glow);
+ border-radius: 50%;
+ width: 480px;
+ height: 360px;
+ margin-left: -400px;
+}
+
+.center::after {
+ background: var(--primary-glow);
+ width: 240px;
+ height: 180px;
+ z-index: -1;
+}
+
+.center::before,
+.center::after {
+ content: '';
+ left: 50%;
+ position: absolute;
+ filter: blur(45px);
+ transform: translateZ(0);
+}
+
+.logo {
+ position: relative;
+}
+/* Enable hover only on non-touch devices */
+@media (hover: hover) and (pointer: fine) {
+ .card:hover {
+ background: rgba(var(--card-rgb), 0.1);
+ border: 1px solid rgba(var(--card-border-rgb), 0.15);
+ }
+
+ .card:hover span {
+ transform: translateX(4px);
+ }
+}
+
+@media (prefers-reduced-motion) {
+ .card:hover span {
+ transform: none;
+ }
+}
+
+/* Mobile */
+@media (max-width: 700px) {
+ .content {
+ padding: 4rem;
+ }
+
+ .grid {
+ grid-template-columns: 1fr;
+ margin-bottom: 120px;
+ max-width: 320px;
+ text-align: center;
+ }
+
+ .card {
+ padding: 1rem 2.5rem;
+ }
+
+ .card h2 {
+ margin-bottom: 0.5rem;
+ }
+
+ .center {
+ padding: 8rem 0 6rem;
+ }
+
+ .center::before {
+ transform: none;
+ height: 300px;
+ }
+
+ .description {
+ font-size: 0.8rem;
+ }
+
+ .description a {
+ padding: 1rem;
+ }
+
+ .description p,
+ .description div {
+ display: flex;
+ justify-content: center;
+ position: fixed;
+ width: 100%;
+ }
+
+ .description p {
+ align-items: center;
+ inset: 0 0 auto;
+ padding: 2rem 1rem 1.4rem;
+ border-radius: 0;
+ border: none;
+ border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25);
+ background: linear-gradient(to bottom, rgba(var(--background-start-rgb), 1), rgba(var(--callout-rgb), 0.5));
+ background-clip: padding-box;
+ backdrop-filter: blur(24px);
+ }
+
+ .description div {
+ align-items: flex-end;
+ pointer-events: none;
+ inset: auto 0 0;
+ padding: 2rem;
+ height: 200px;
+ background: linear-gradient(to bottom, transparent 0%, rgb(var(--background-end-rgb)) 40%);
+ z-index: 1;
+ }
+}
+
+/* Tablet and Smaller Desktop */
+@media (min-width: 701px) and (max-width: 1120px) {
+ .grid {
+ grid-template-columns: repeat(2, 50%);
+ }
+}
+
+@media (prefers-color-scheme: dark) {
+ .vercelLogo {
+ filter: invert(1);
+ }
+
+ .logo {
+ filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70);
+ }
+}
+
+@keyframes rotate {
+ from {
+ transform: rotate(360deg);
+ }
+ to {
+ transform: rotate(0deg);
+ }
+}
diff --git a/integration/templates/next-app-router-quickstart-v6/src/app/page.tsx b/integration/templates/next-app-router-quickstart-v6/src/app/page.tsx
new file mode 100644
index 00000000000..7e15c54f93e
--- /dev/null
+++ b/integration/templates/next-app-router-quickstart-v6/src/app/page.tsx
@@ -0,0 +1,17 @@
+import { SignInButton, SignUpButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs';
+
+export default function Home() {
+ return (
+
+
+ signed-out-state
+
+
+
+
+ signed-in-state
+
+
+
+ );
+}
diff --git a/integration/templates/next-app-router-quickstart-v6/src/middleware.ts b/integration/templates/next-app-router-quickstart-v6/src/middleware.ts
new file mode 100644
index 00000000000..71c73d054cb
--- /dev/null
+++ b/integration/templates/next-app-router-quickstart-v6/src/middleware.ts
@@ -0,0 +1,7 @@
+import { clerkMiddleware } from '@clerk/nextjs/server';
+
+export default clerkMiddleware();
+
+export const config = {
+ matcher: ['/((?!.*\\..*|_next).*)', '/', '/(api|trpc)(.*)'],
+};
diff --git a/integration/templates/next-app-router-quickstart-v6/tsconfig.json b/integration/templates/next-app-router-quickstart-v6/tsconfig.json
new file mode 100644
index 00000000000..683a38afc1d
--- /dev/null
+++ b/integration/templates/next-app-router-quickstart-v6/tsconfig.json
@@ -0,0 +1,30 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": ["dom", "dom.iterable", "esnext"],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "preserve",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": ["./src/*"],
+ "@clerk/nextjs": ["../../../packages/nextjs/src/index.ts"],
+ "@clerk/nextjs/*": ["../../../packages/nextjs/src/*"]
+ }
+ },
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
+ "exclude": ["node_modules"]
+}
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-quickstart/tsconfig.json b/integration/templates/next-app-router-quickstart/tsconfig.json
index 0c7555fa765..683a38afc1d 100644
--- a/integration/templates/next-app-router-quickstart/tsconfig.json
+++ b/integration/templates/next-app-router-quickstart/tsconfig.json
@@ -9,7 +9,7 @@
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
- "moduleResolution": "node",
+ "moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
@@ -20,7 +20,9 @@
}
],
"paths": {
- "@/*": ["./src/*"]
+ "@/*": ["./src/*"],
+ "@clerk/nextjs": ["../../../packages/nextjs/src/index.ts"],
+ "@clerk/nextjs/*": ["../../../packages/nextjs/src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
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/astro/components.test.ts b/integration/tests/astro/components.test.ts
index 93e8f21b35b..2ecf0c3f14e 100644
--- a/integration/tests/astro/components.test.ts
+++ b/integration/tests/astro/components.test.ts
@@ -351,7 +351,7 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
- await expect(u.page.getByText('Not a member')).toBeVisible();
+ await expect(u.page.getByText("I'm a member")).toBeVisible();
});
// --- react/components
@@ -449,7 +449,7 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin2.email, password: fakeAdmin2.password });
await u.po.expect.toBeSignedIn();
- await expect(u.page.getByText('Not a member')).toBeVisible();
+ await expect(u.page.getByText("I'm a member")).toBeVisible();
});
test('renders components and keep internal routing behavior when view transitions is enabled', async ({
@@ -491,7 +491,7 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f
// This is being investigated upstream with the Astro team. The test is commented out for now
// to unblock development and will be revisited once the root cause is resolved.
// await expect(u.page.getByText('Loading')).toBeHidden();
- await expect(u.page.getByText('Not an admin')).toBeVisible();
+ await expect(u.page.getByText('Loading')).toBeVisible();
// Sign in as admin user
await u.page.goToRelative('/sign-in');
@@ -509,6 +509,6 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f
// This is being investigated upstream with the Astro team. The test is commented out for now
// to unblock development and will be revisited once the root cause is resolved.
// await expect(u.page.getByText('Loading')).toBeHidden();
- await expect(u.page.getByText("I'm an admin")).toBeVisible();
+ await expect(u.page.getByText("I'm an admin")).toBeVisible({ timeout: 15_000 });
});
});
diff --git a/integration/tests/astro/hybrid.test.ts b/integration/tests/astro/hybrid.test.ts
index a0ff4c92fb3..31ea27d2f6e 100644
--- a/integration/tests/astro/hybrid.test.ts
+++ b/integration/tests/astro/hybrid.test.ts
@@ -108,7 +108,7 @@ testAgainstRunningApps({ withPattern: ['astro.static.withCustomRoles'] })(
await u.page.goToRelative('/only-members');
- await expect(u.page.getByText('Not a member')).toBeVisible();
+ await expect(u.page.getByText("I'm a member")).toBeVisible();
});
},
);
diff --git a/integration/tests/next-account-portal/clerk-v6-ap-core-3.test.ts b/integration/tests/next-account-portal/clerk-ap-core-3-v6.test.ts
similarity index 100%
rename from integration/tests/next-account-portal/clerk-v6-ap-core-3.test.ts
rename to integration/tests/next-account-portal/clerk-ap-core-3-v6.test.ts
diff --git a/integration/tests/next-account-portal/clerk-v7-ap-core-3.test.ts b/integration/tests/next-account-portal/clerk-ap-core-3-v7.test.ts
similarity index 100%
rename from integration/tests/next-account-portal/clerk-v7-ap-core-3.test.ts
rename to integration/tests/next-account-portal/clerk-ap-core-3-v7.test.ts
diff --git a/integration/tests/next-account-portal/clerk-v4-ap-core-1.test.ts b/integration/tests/next-account-portal/clerk-v4-ap-core-1.test.ts
deleted file mode 100644
index 984f846ebf7..00000000000
--- a/integration/tests/next-account-portal/clerk-v4-ap-core-1.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { test } from '@playwright/test';
-
-import type { Application } from '../../models/application';
-import { appConfigs } from '../../presets';
-import type { FakeUser } from '../../testUtils';
-import { createTestUtils } from '../../testUtils';
-import { testSignIn, testSignUp, testSSR } from './common';
-
-test.describe('Next with ClerkJS V4 <-> Account Portal Core 1 @ap-flows', () => {
- test.describe.configure({ mode: 'serial' });
- let app: Application;
- let fakeUser: FakeUser;
-
- test.beforeAll(async () => {
- test.setTimeout(90_000); // Wait for app to be ready
- app = await appConfigs.next.appRouterAPWithClerkNextV4.clone().commit();
- await app.setup();
- await app.withEnv(appConfigs.envs.withAPCore1ClerkV4);
- await app.dev();
- const u = createTestUtils({ app });
- fakeUser = u.services.users.createFakeUser();
- await u.services.users.createBapiUser(fakeUser);
- });
-
- test.afterAll(async () => {
- await fakeUser.deleteIfExists();
- await app.teardown();
- });
-
- test('sign in', async ({ page, context }) => {
- await testSignIn({ app, page, context, fakeUser });
- });
-
- test('sign up', async ({ page, context }) => {
- await testSignUp({ app, page, context, fakeUser });
- });
-
- test('ssr', async ({ page, context }) => {
- await testSSR({ app, page, context, fakeUser });
- });
-});
diff --git a/integration/tests/next-account-portal/clerk-v4-ap-core-3.test.ts b/integration/tests/next-account-portal/clerk-v4-ap-core-3.test.ts
deleted file mode 100644
index 5d4cb8ee01b..00000000000
--- a/integration/tests/next-account-portal/clerk-v4-ap-core-3.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { test } from '@playwright/test';
-
-import type { Application } from '../../models/application';
-import { appConfigs } from '../../presets';
-import type { FakeUser } from '../../testUtils';
-import { createTestUtils } from '../../testUtils';
-import { testSignIn, testSignUp, testSSR } from './common';
-
-test.describe('Next with ClerkJS V4 <-> Account Portal Core 3 @ap-flows', () => {
- test.describe.configure({ mode: 'serial' });
- let app: Application;
- let fakeUser: FakeUser;
-
- test.beforeAll(async () => {
- test.setTimeout(90_000); // Wait for app to be ready
- app = await appConfigs.next.appRouterAPWithClerkNextV4.clone().commit();
- await app.setup();
- await app.withEnv(appConfigs.envs.withAPCore3ClerkV4);
- await app.dev();
- const u = createTestUtils({ app });
- fakeUser = u.services.users.createFakeUser();
- await u.services.users.createBapiUser(fakeUser);
- });
-
- test.afterAll(async () => {
- await fakeUser.deleteIfExists();
- await app.teardown();
- });
-
- test('sign in', async ({ page, context }) => {
- await testSignIn({ app, page, context, fakeUser });
- });
-
- test('sign up', async ({ page, context }) => {
- await testSignUp({ app, page, context, fakeUser });
- });
-
- test('ssr', async ({ page, context }) => {
- await testSSR({ app, page, context, fakeUser });
- });
-});
diff --git a/integration/tests/next-account-portal/clerk-v6-ap-core-1.test.ts b/integration/tests/next-account-portal/clerk-v6-ap-core-1.test.ts
deleted file mode 100644
index 1c0b935c510..00000000000
--- a/integration/tests/next-account-portal/clerk-v6-ap-core-1.test.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import { test } from '@playwright/test';
-
-import type { Application } from '../../models/application';
-import { appConfigs } from '../../presets';
-import type { FakeUser } from '../../testUtils';
-import { createTestUtils } from '../../testUtils';
-import { testSignIn, testSignUp, testSSR } from './common';
-
-test.describe('Next with ClerkJS V6 <-> Account Portal Core 1 @ap-flows', () => {
- test.describe.configure({ mode: 'serial' });
- let app: Application;
- let fakeUser: FakeUser;
-
- test.beforeAll(async () => {
- test.setTimeout(90_000); // Wait for app to be ready
- app = await appConfigs.next.appRouterAPWithClerkNextV6.clone().commit();
- await app.setup();
- await app.withEnv(appConfigs.envs.withAPCore1ClerkV6);
- await app.dev();
- const u = createTestUtils({ app });
- fakeUser = u.services.users.createFakeUser();
- await u.services.users.createBapiUser(fakeUser);
- });
-
- test.afterAll(async () => {
- await fakeUser.deleteIfExists();
- await app.teardown();
- });
-
- test('sign in', async ({ page, context }) => {
- await testSignIn({ app, page, context, fakeUser });
- });
-
- test('sign up', async ({ page, context }) => {
- await testSignUp({ app, page, context, fakeUser });
- });
-
- test('ssr', async ({ page, context }) => {
- await testSSR({ app, page, context, fakeUser });
- });
-});
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/Protect.astro b/packages/astro/src/astro-components/control/Protect.astro
deleted file mode 100644
index c7e039987f4..00000000000
--- a/packages/astro/src/astro-components/control/Protect.astro
+++ /dev/null
@@ -1,52 +0,0 @@
----
-import ProtectCSR from './ProtectCSR.astro';
-import ProtectSSR from './ProtectSSR.astro';
-
-import { isStaticOutput } from 'virtual:@clerk/astro/config';
-import type { ProtectProps } from '../../types';
-
-type Props = ProtectProps & {
- isStatic?: boolean;
- /**
- * The class name to apply to the outermost element of the component.
- * This class is only applied to static components.
- */
- class?: string;
- /**
- * The class name to apply to the wrapper element of the default slot.
- * This class is only applied to static components.
- */
- defaultSlotWrapperClass?: string;
- /**
- * The class name to apply to the wrapper element of the fallback slot.
- * This class is only applied to static components.
- */
- fallbackSlotWrapperClass?: string;
-};
-
-const { isStatic, ...props } = Astro.props;
-
-const ProtectComponent = isStaticOutput(isStatic) ? ProtectCSR : ProtectSSR;
-
-// Note: Astro server islands also use a "fallback" slot for loading states
-// See: https://docs.astro.build/en/guides/server-islands/#server-island-fallback-content
-// We use "protect-fallback" as our preferred slot name to avoid conflicts
-const hasProtectFallback = Astro.slots.has('protect-fallback');
----
-
-
-
- {
- hasProtectFallback ? (
-
- ) : (
-
- )
- }
-
diff --git a/packages/astro/src/astro-components/control/ProtectCSR.astro b/packages/astro/src/astro-components/control/ProtectCSR.astro
deleted file mode 100644
index e3aa5ca8f3c..00000000000
--- a/packages/astro/src/astro-components/control/ProtectCSR.astro
+++ /dev/null
@@ -1,79 +0,0 @@
----
-import type { ProtectProps } from '../../types';
-
-type Props = Omit & {
- class?: string;
- defaultSlotWrapperClass?: string;
- fallbackSlotWrapperClass?: string;
-};
-
-const {
- role,
- permission,
- feature,
- plan,
- class: className,
- defaultSlotWrapperClass,
- fallbackSlotWrapperClass,
-} = Astro.props;
----
-
-
-
-
-
-
-
-
-
-
-
diff --git a/packages/astro/src/astro-components/control/ProtectSSR.astro b/packages/astro/src/astro-components/control/ProtectSSR.astro
deleted file mode 100644
index e894af3ee03..00000000000
--- a/packages/astro/src/astro-components/control/ProtectSSR.astro
+++ /dev/null
@@ -1,15 +0,0 @@
----
-import type { ProtectProps } from '../../types';
-
-type Props = ProtectProps;
-
-const { has, userId } = Astro.locals.auth();
-const isUnauthorized =
- !userId ||
- (typeof Astro.props.condition === 'function' && !Astro.props.condition(has)) ||
- ((Astro.props.role || Astro.props.permission || Astro.props.feature || Astro.props.plan) && !has(Astro.props));
-
-const hasProtectFallback = Astro.slots.has('protect-fallback');
----
-
-{isUnauthorized ? hasProtectFallback ? : : }
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..9a72534ddff
--- /dev/null
+++ b/packages/astro/src/astro-components/control/Show.astro
@@ -0,0 +1,48 @@
+---
+import ShowCSR from './ShowCSR.astro';
+import ShowSSR from './ShowSSR.astro';
+
+import { isStaticOutput } from 'virtual:@clerk/astro/config';
+import type { ShowProps } from '../../types';
+
+type Props = ShowProps & {
+ 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 { isStatic, when, ...rest } = Astro.props;
+
+if (typeof when === 'undefined') {
+ throw new Error('@clerk/astro: requires a `when` prop.');
+}
+
+const props = { ...rest, when };
+
+const ShowComponent = isStaticOutput(isStatic) ? ShowCSR : ShowSSR;
+
+// Note: Astro server islands also use a "fallback" slot for loading states
+// See: https://docs.astro.build/en/guides/server-islands/#server-island-fallback-content
+// We use "show-fallback" as our preferred slot name to avoid conflicts
+const hasShowFallback = Astro.slots.has('show-fallback');
+---
+
+
+
+ {
+ hasShowFallback ? (
+
+ ) : (
+
+ )
+ }
+
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..b58ae386d86
--- /dev/null
+++ b/packages/astro/src/astro-components/control/ShowCSR.astro
@@ -0,0 +1,90 @@
+---
+import type { ShowProps } from '../../types';
+
+type Props = Omit & {
+ class?: string;
+};
+
+const { when, class: className } = Astro.props;
+
+// For CSR, we need to serialize the when prop
+// String values ('signedIn', 'signedOut') are used as-is
+// Object values are serialized as data attributes
+const isStringWhen = typeof when === 'string';
+const whenCondition = isStringWhen ? when : null;
+const role = !isStringWhen && typeof when === 'object' ? when.role : undefined;
+const permission = !isStringWhen && typeof when === 'object' ? when.permission : undefined;
+const feature = !isStringWhen && typeof when === 'object' ? when.feature : undefined;
+const plan = !isStringWhen && typeof when === 'object' ? when.plan : undefined;
+---
+
+
+
+
+
+
+
+
+
+
+
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..1150160d735
--- /dev/null
+++ b/packages/astro/src/astro-components/control/ShowSSR.astro
@@ -0,0 +1,30 @@
+---
+import type { ShowProps } from '../../types';
+
+type Props = ShowProps;
+
+const { has, userId } = Astro.locals.auth();
+const { when } = Astro.props;
+
+const showContent = (() => {
+ // String conditions
+ if (when === 'signedIn') return !!userId;
+ if (when === 'signedOut') return !userId;
+
+ // Function condition
+ if (typeof when === 'function') return !!userId && when(has);
+
+ // Object-based conditions (role, permission, feature, plan)
+ if (typeof when === 'object' && when !== null) {
+ if (!userId) return false;
+ return has(when);
+ }
+
+ // Default: show if signed in
+ return !!userId;
+})();
+
+const hasShowFallback = Astro.slots.has('show-fallback');
+---
+
+{showContent ? : hasShowFallback ? : }
diff --git a/packages/astro/src/astro-components/control/SignedIn.astro b/packages/astro/src/astro-components/control/SignedIn.astro
deleted file mode 100644
index 5b1b484e13d..00000000000
--- a/packages/astro/src/astro-components/control/SignedIn.astro
+++ /dev/null
@@ -1,23 +0,0 @@
----
-import SignedInCSR from './SignedInCSR.astro';
-import SignedInSSR from './SignedInSSR.astro';
-
-import { isStaticOutput } from 'virtual:@clerk/astro/config';
-
-type Props = {
- 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 { isStatic, class: className } = Astro.props;
-
-const SignedInComponent = isStaticOutput(isStatic) ? SignedInCSR : SignedInSSR;
----
-
-
-
-
diff --git a/packages/astro/src/astro-components/control/SignedInCSR.astro b/packages/astro/src/astro-components/control/SignedInCSR.astro
deleted file mode 100644
index 750c60f718e..00000000000
--- a/packages/astro/src/astro-components/control/SignedInCSR.astro
+++ /dev/null
@@ -1,30 +0,0 @@
----
-type Props = {
- class?: string;
-};
-
-const { class: className } = Astro.props;
----
-
-
-
-
-
-
diff --git a/packages/astro/src/astro-components/control/SignedInSSR.astro b/packages/astro/src/astro-components/control/SignedInSSR.astro
deleted file mode 100644
index 446b1997116..00000000000
--- a/packages/astro/src/astro-components/control/SignedInSSR.astro
+++ /dev/null
@@ -1,5 +0,0 @@
----
-const { userId } = Astro.locals.auth();
----
-
-{userId ? : null}
diff --git a/packages/astro/src/astro-components/control/SignedOut.astro b/packages/astro/src/astro-components/control/SignedOut.astro
deleted file mode 100644
index 9161a518d3b..00000000000
--- a/packages/astro/src/astro-components/control/SignedOut.astro
+++ /dev/null
@@ -1,23 +0,0 @@
----
-import SignedOutCSR from './SignedOutCSR.astro';
-import SignedOutSSR from './SignedOutSSR.astro';
-
-import { isStaticOutput } from 'virtual:@clerk/astro/config';
-
-type Props = {
- 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 { isStatic, class: className } = Astro.props;
-
-const SignedOutComponent = isStaticOutput(isStatic) ? SignedOutCSR : SignedOutSSR;
----
-
-
-
-
diff --git a/packages/astro/src/astro-components/control/SignedOutCSR.astro b/packages/astro/src/astro-components/control/SignedOutCSR.astro
deleted file mode 100644
index 3417917ac94..00000000000
--- a/packages/astro/src/astro-components/control/SignedOutCSR.astro
+++ /dev/null
@@ -1,30 +0,0 @@
----
-type Props = {
- class?: string;
-};
-
-const { class: className } = Astro.props;
----
-
-
-
-
-
-
diff --git a/packages/astro/src/astro-components/control/SignedOutSSR.astro b/packages/astro/src/astro-components/control/SignedOutSSR.astro
deleted file mode 100644
index df4e890b890..00000000000
--- a/packages/astro/src/astro-components/control/SignedOutSSR.astro
+++ /dev/null
@@ -1,5 +0,0 @@
----
-const { userId } = Astro.locals.auth();
----
-
-{!userId ? : null}
diff --git a/packages/astro/src/astro-components/index.ts b/packages/astro/src/astro-components/index.ts
index 5c9d9b8361f..f4472c143f9 100644
--- a/packages/astro/src/astro-components/index.ts
+++ b/packages/astro/src/astro-components/index.ts
@@ -1,9 +1,7 @@
/**
* Control Components
*/
-export { default as SignedIn } from './control/SignedIn.astro';
-export { default as SignedOut } from './control/SignedOut.astro';
-export { default as Protect } from './control/Protect.astro';
+export { default as Show } from './control/Show.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..4b345a18fd8 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,44 @@ 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) => {
+ if (typeof when === 'undefined') {
+ throw new Error('@clerk/astro: requires a `when` prop.');
+ }
+
+ 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 +94,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/astro/src/types.ts b/packages/astro/src/types.ts
index 9de65b01579..7f0613e5968 100644
--- a/packages/astro/src/types.ts
+++ b/packages/astro/src/types.ts
@@ -3,7 +3,8 @@ import type {
ClerkOptions,
ClientResource,
MultiDomainAndOrProxyPrimitives,
- ProtectProps,
+ ProtectParams,
+ ShowProps,
Without,
} from '@clerk/shared/types';
import type { ClerkUiConstructor } from '@clerk/shared/ui';
@@ -62,7 +63,16 @@ declare global {
}
}
-export type { AstroClerkUpdateOptions, AstroClerkIntegrationParams, AstroClerkCreateInstanceParams, ProtectProps };
+export type {
+ AstroClerkUpdateOptions,
+ AstroClerkIntegrationParams,
+ AstroClerkCreateInstanceParams,
+ ProtectParams,
+ ShowProps,
+};
+
+// Backward compatibility alias
+export type ProtectProps = ProtectParams;
export type ButtonProps = {
/**
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-d.ts b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test-d.ts
new file mode 100644
index 00000000000..1c054077b7a
--- /dev/null
+++ b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test-d.ts
@@ -0,0 +1,8 @@
+import type { ShowWhenCondition } from '@clerk/shared/types';
+import { test } from 'vitest';
+
+test('ShowWhenCondition rejects empty authorization objects', () => {
+ // @ts-expect-error - empty object must not satisfy ShowWhenCondition/ProtectParams
+ const when: ShowWhenCondition = {};
+ void when;
+});
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 (
- *
+ *
*
*
*
- *
+ *
* );
* }
* ```
*
- * @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 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/authorization.ts b/packages/shared/src/types/authorization.ts
new file mode 100644
index 00000000000..9d7552fa4be
--- /dev/null
+++ b/packages/shared/src/types/authorization.ts
@@ -0,0 +1,82 @@
+import type { OrganizationCustomPermissionKey, OrganizationCustomRoleKey } from './organizationMembership';
+import type { CheckAuthorizationWithCustomPermissions, PendingSessionOptions } from './session';
+import type { Autocomplete } from './utils';
+
+/**
+ * Authorization parameters used by `auth.protect()`.
+ *
+ * Use `ProtectParams` to specify the required role, permission, feature, or plan for access.
+ */
+export type ProtectParams =
+ | {
+ condition?: never;
+ feature?: never;
+ permission?: never;
+ plan?: never;
+ role: OrganizationCustomRoleKey;
+ }
+ | {
+ condition?: never;
+ feature?: never;
+ permission: OrganizationCustomPermissionKey;
+ plan?: never;
+ role?: never;
+ }
+ | {
+ condition: (has: CheckAuthorizationWithCustomPermissions) => boolean;
+ feature?: never;
+ permission?: never;
+ plan?: never;
+ role?: never;
+ }
+ | {
+ condition?: never;
+ feature: Autocomplete<`user:${string}` | `org:${string}`>;
+ permission?: never;
+ plan?: never;
+ role?: never;
+ }
+ | {
+ condition?: never;
+ feature?: never;
+ permission?: never;
+ plan: Autocomplete<`user:${string}` | `org:${string}`>;
+ role?: 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 =
+ | '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/shared/src/types/index.ts b/packages/shared/src/types/index.ts
index 85cecc41a27..d1f50cfde7c 100644
--- a/packages/shared/src/types/index.ts
+++ b/packages/shared/src/types/index.ts
@@ -44,7 +44,7 @@ export type * from './passwords';
export type * from './permission';
export type * from './phoneCodeChannel';
export type * from './phoneNumber';
-export type * from './protect';
+export type * from './authorization';
export type * from './protectConfig';
export type * from './redirects';
export type * from './resource';
diff --git a/packages/shared/src/types/protect.ts b/packages/shared/src/types/protect.ts
deleted file mode 100644
index 0498c2b5f1b..00000000000
--- a/packages/shared/src/types/protect.ts
+++ /dev/null
@@ -1,70 +0,0 @@
-import type { OrganizationCustomPermissionKey, OrganizationCustomRoleKey } from './organizationMembership';
-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.
- *
- * Use `ProtectProps` to specify the required Role, Permission, Feature, or Plan for access.
- *
- * @example
- * ```tsx
- * // Require a specific Permission
- *
- *
- * // Require a specific Role
- *
- *
- * // Use a custom condition callback
- * has({ permission: "a_permission_key" })} />
- *
- * // Require a specific Feature
- *
- *
- * // Require a specific Plan
- *
- * ```
- */
-export type ProtectProps =
- | {
- condition?: never;
- role: OrganizationCustomRoleKey;
- permission?: never;
- feature?: never;
- plan?: never;
- }
- | {
- condition?: never;
- role?: never;
- feature?: never;
- plan?: never;
- permission: OrganizationCustomPermissionKey;
- }
- | {
- condition: (has: CheckAuthorizationWithCustomPermissions) => boolean;
- role?: never;
- permission?: never;
- feature?: never;
- plan?: never;
- }
- | {
- condition?: never;
- role?: never;
- permission?: never;
- feature: Autocomplete<`user:${string}` | `org:${string}`>;
- plan?: never;
- }
- | {
- condition?: never;
- role?: never;
- permission?: never;
- feature?: never;
- plan: Autocomplete<`user:${string}` | `org:${string}`>;
- }
- | {
- condition?: never;
- role?: never;
- permission?: never;
- feature?: never;
- plan?: never;
- };
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..c9115431792
--- /dev/null
+++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js
@@ -0,0 +1,584 @@
+export const fixtures = [
+ {
+ name: 'Transforms Protect import',
+ source: `
+import { Protect } from "@clerk/react"
+ `,
+ output: `
+import { Show } from "@clerk/react"
+`,
+ },
+ {
+ name: 'Transforms Protect import from legacy package',
+ source: `
+import { Protect } from "@clerk/clerk-react"
+ `,
+ output: `
+import { Show } from "@clerk/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 = () =>
+`,
+ },
+ {
+ name: 'Transforms Protect require destructuring',
+ source: `
+const { Protect } = require("@clerk/react");
+
+function App() {
+ return ok;
+}
+ `,
+ output: `
+const { Show } = require("@clerk/react");
+
+function App() {
+ return (
+ ok
+ );
+}
+`,
+ },
+ {
+ name: 'Transforms SignedIn and SignedOut require destructuring',
+ source: `
+const { SignedIn, SignedOut } = require("@clerk/react");
+
+const App = () => (
+ <>
+ in
+ out
+ >
+);
+ `,
+ output: `
+const {
+ Show
+} = require("@clerk/react");
+
+const App = () => (
+ <>
+ in
+ out
+ >
+);
+`,
+ },
+ {
+ name: 'Transforms namespace require',
+ source: `
+const Clerk = require("@clerk/react");
+
+const App = () => (
+
+ ok
+
+);
+ `,
+ output: `
+const Clerk = require("@clerk/react");
+
+const App = () => (
+
+ ok
+
+);
+`,
+ },
+ {
+ name: 'Transforms Protect from other @clerk packages',
+ source: `
+import { Protect as ProtectExpo } from "@clerk/expo";
+import { Protect as ProtectVue } from "@clerk/vue";
+import { Protect as ProtectChrome } from "@clerk/chrome-extension";
+ `,
+ output: `
+import { Show as ProtectExpo } from "@clerk/expo";
+import { Show as ProtectVue } from "@clerk/vue";
+import { Show as ProtectChrome } from "@clerk/chrome-extension";
+`,
+ },
+ {
+ name: 'Transforms default import member usage',
+ source: `
+import Clerk from "@clerk/react";
+
+const App = () => (
+
+ ok
+
+);
+ `,
+ output: `
+import Clerk from "@clerk/react";
+
+const App = () => (
+
+ ok
+
+);
+`,
+ },
+ {
+ name: 'Transforms Protect namespace import member usage',
+ source: `
+import * as Clerk from "@clerk/react";
+
+const App = () => (
+
+ ok
+
+);
+ `,
+ output: `
+import * as Clerk from "@clerk/react";
+
+const App = () => (
+
+ ok
+
+);
+`,
+ },
+ {
+ name: 'Self-closing SignedIn and SignedOut are transformed',
+ source: `
+import { SignedIn, SignedOut } from "@clerk/react";
+
+const App = () => (
+ <>
+
+
+ >
+);
+ `,
+ output: `
+import { Show } from "@clerk/react";
+
+const App = () => (
+ <>
+
+
+ >
+);
+`,
+ },
+ {
+ name: 'Transforms SignedIn alias import usage',
+ source: `
+import { SignedIn as OnlyWhenSignedIn } from "@clerk/react";
+
+const App = () => (
+
+ ok
+
+);
+ `,
+ output: `
+import { Show as OnlyWhenSignedIn } from "@clerk/react";
+
+const App = () => (
+
+ ok
+
+);
+`,
+ },
+ {
+ name: 'Transforms Protect require destructuring with alias',
+ source: `
+const { Protect: CanAccess } = require("@clerk/react");
+
+const App = () => (
+
+ ok
+
+);
+ `,
+ output: `
+const { Show: CanAccess } = require("@clerk/react");
+
+const App = () => (
+
+ ok
+
+);
+`,
+ },
+ {
+ name: 'Transforms import with duplicate Show specifier',
+ source: `
+import { Protect, Show } from "@clerk/react";
+
+const App = () => ;
+ `,
+ output: `
+import { Show } from "@clerk/react";
+
+const App = () => ;
+`,
+ },
+ {
+ name: 'Transforms import type ProtectProps',
+ source: `
+import type { ProtectProps } from "@clerk/react";
+type Props = ProtectProps;
+ `,
+ output: `
+import type { ShowProps } from "@clerk/react";
+type Props = ShowProps;
+`,
+ },
+ {
+ name: 'Sorts when object keys for determinism',
+ source: `
+import { Protect } from "@clerk/react";
+
+const App = () => (
+
+ ok
+
+);
+ `,
+ output: `
+import { Show } from "@clerk/react";
+
+const App = () => (
+
+ ok
+
+);
+`,
+ },
+ {
+ name: 'Does not transform non-clerk Protect',
+ source: `
+import { Protect } from "./local";
+
+const App = () => (
+
+ ok
+
+);
+ `,
+ output: null,
+ },
+];
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..63dfe4dd72c
--- /dev/null
+++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs
@@ -0,0 +1,350 @@
+const CLERK_PACKAGE_PREFIX = '@clerk/';
+
+const isClerkPackageSource = sourceValue => {
+ return typeof sourceValue === 'string' && sourceValue.startsWith(CLERK_PACKAGE_PREFIX);
+};
+
+/**
+ * Transforms `` component usage to `` component.
+ *
+ * Handles the following transformations:
+ * - `` → ``
+ * - `` → ``
+ * - `` → ``
+ * - `` → ``
+ * - ` ...}>` → ` ...}>`
+ * - `...` → `...`
+ * - `...` → `...`
+ *
+ * Also updates ESM/CJS 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 ESM imports: Protect → Show, ProtectProps → ShowProps
+ root.find(j.ImportDeclaration).forEach(path => {
+ const node = path.node;
+ const sourceValue = node.source?.value;
+
+ if (!isClerkPackageSource(sourceValue)) {
+ return;
+ }
+
+ const specifiers = node.specifiers || [];
+
+ specifiers.forEach(spec => {
+ if (j.ImportDefaultSpecifier.check(spec) || j.ImportNamespaceSpecifier.check(spec)) {
+ if (spec.local?.name) {
+ namespaceImports.add(spec.local.name);
+ }
+ return;
+ }
+
+ if (!j.ImportSpecifier.check(spec)) {
+ return;
+ }
+
+ 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;
+ return;
+ }
+
+ 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;
+ }, []);
+ });
+
+ // Transform CJS requires: Protect → Show
+ root.find(j.VariableDeclarator).forEach(path => {
+ const declarator = path.node;
+ const init = declarator.init;
+
+ if (!init || !j.CallExpression.check(init)) {
+ return;
+ }
+
+ if (!j.Identifier.check(init.callee) || init.callee.name !== 'require') {
+ return;
+ }
+
+ const args = init.arguments || [];
+ if (args.length !== 1) {
+ return;
+ }
+
+ const arg = args[0];
+ const sourceValue = j.Literal.check(arg) ? arg.value : j.StringLiteral.check(arg) ? arg.value : null;
+
+ if (!isClerkPackageSource(sourceValue)) {
+ return;
+ }
+
+ const id = declarator.id;
+
+ if (j.Identifier.check(id)) {
+ namespaceImports.add(id.name);
+ return;
+ }
+
+ if (!j.ObjectPattern.check(id)) {
+ return;
+ }
+
+ const properties = id.properties || [];
+ const seenLocalNames = new Set();
+
+ id.properties = properties.reduce((acc, prop) => {
+ if (!(j.ObjectProperty.check(prop) || j.Property.check(prop))) {
+ acc.push(prop);
+ return acc;
+ }
+
+ if (!j.Identifier.check(prop.key)) {
+ acc.push(prop);
+ return acc;
+ }
+
+ const originalImportedName = prop.key.name;
+ const originalLocalName = j.Identifier.check(prop.value) ? prop.value.name : null;
+ const effectiveLocalName = originalLocalName || originalImportedName;
+
+ if (['Protect', 'SignedIn', 'SignedOut'].includes(originalImportedName)) {
+ componentKindByLocalName[effectiveLocalName] =
+ originalImportedName === 'Protect'
+ ? 'protect'
+ : originalImportedName === 'SignedIn'
+ ? 'signedIn'
+ : 'signedOut';
+
+ prop.key.name = 'Show';
+
+ if (j.Identifier.check(prop.value) && prop.value.name === originalImportedName) {
+ prop.value.name = 'Show';
+ }
+
+ if (prop.shorthand) {
+ prop.value = j.identifier('Show');
+ }
+
+ dirtyFlag = true;
+ }
+
+ const newLocalName = j.Identifier.check(prop.value) ? prop.value.name : null;
+ const finalLocalName = newLocalName || (j.Identifier.check(prop.key) ? prop.key.name : null);
+
+ if (finalLocalName) {
+ if (seenLocalNames.has(finalLocalName)) {
+ dirtyFlag = true;
+ return acc;
+ }
+ seenLocalNames.add(finalLocalName);
+ }
+
+ acc.push(prop);
+ 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);
+ });
+
+ properties.sort((a, b) => {
+ const aKey = j.Identifier.check(a.key) ? a.key.name : '';
+ const bKey = j.Identifier.check(b.key) ? b.key.name : '';
+ return aKey.localeCompare(bKey);
+ });
+
+ 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 (
<>
-
+
-
-
+
+
-
+
>
)
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={
<>
-
+
-
-
+
+
-
+
>
}
/>