diff --git a/.changeset/migrate-to-show.md b/.changeset/migrate-to-show.md new file mode 100644 index 00000000000..fad0e293d77 --- /dev/null +++ b/.changeset/migrate-to-show.md @@ -0,0 +1,5 @@ +--- +'@clerk/upgrade': minor +--- + +Add a `transform-protect-to-show` codemod that migrates client-side `` usage to `` with automatic prop and import updates. diff --git a/.changeset/show-the-guards.md b/.changeset/show-the-guards.md new file mode 100644 index 00000000000..76f30d82828 --- /dev/null +++ b/.changeset/show-the-guards.md @@ -0,0 +1,11 @@ +--- +'@clerk/astro': major +'@clerk/chrome-extension': major +'@clerk/expo': major +'@clerk/nextjs': major +'@clerk/react': major +'@clerk/shared': minor +'@clerk/vue': major +--- + +Introduce `` as the cross-framework authorization control component and remove ``, ``, and `` in favor of ``, updating shared types and framework wrappers to align with the new API. diff --git a/integration/templates/astro-hybrid/src/pages/index.astro b/integration/templates/astro-hybrid/src/pages/index.astro index 47168af011b..88ab11cf71c 100644 --- a/integration/templates/astro-hybrid/src/pages/index.astro +++ b/integration/templates/astro-hybrid/src/pages/index.astro @@ -1,5 +1,5 @@ --- -import { UserButton, SignInButton, SignedIn, SignedOut } from '@clerk/astro/components'; +import { Show, UserButton, SignInButton } from '@clerk/astro/components'; import { OrganizationSwitcher } from '@clerk/astro/react'; import Layout from '../layouts/Layout.astro'; @@ -7,16 +7,16 @@ export const prerender = true; --- - +

Signed out

-
- +
+

Signed in

-
+
diff --git a/integration/templates/astro-hybrid/src/pages/ssr.astro b/integration/templates/astro-hybrid/src/pages/ssr.astro index 0db930a6145..0c0611e626f 100644 --- a/integration/templates/astro-hybrid/src/pages/ssr.astro +++ b/integration/templates/astro-hybrid/src/pages/ssr.astro @@ -1,5 +1,5 @@ --- -import { UserButton, SignInButton, SignedIn, SignedOut } from '@clerk/astro/components'; +import { Show, UserButton, SignInButton } from '@clerk/astro/components'; import { OrganizationSwitcher } from '@clerk/astro/react'; import Layout from '../layouts/Layout.astro'; @@ -7,16 +7,16 @@ export const prerender = false; --- - +

Signed out

-
- +
+

Signed in

- +
diff --git a/integration/templates/astro-node/src/layouts/Layout.astro b/integration/templates/astro-node/src/layouts/Layout.astro index 3e168321da2..17639bb1214 100644 --- a/integration/templates/astro-node/src/layouts/Layout.astro +++ b/integration/templates/astro-node/src/layouts/Layout.astro @@ -5,7 +5,7 @@ interface Props { const { title } = Astro.props; -import { SignedIn, SignedOut } from '@clerk/astro/components'; +import { Show } from '@clerk/astro/components'; import { LanguagePicker } from '../components/LanguagePicker'; import CustomUserButton from '../components/CustomUserButton.astro'; --- @@ -80,11 +80,11 @@ import CustomUserButton from '../components/CustomUserButton.astro';
- + - + - +
- + diff --git a/integration/templates/astro-node/src/layouts/react/Layout.astro b/integration/templates/astro-node/src/layouts/react/Layout.astro index 41b878880e3..4a5fc2be65c 100644 --- a/integration/templates/astro-node/src/layouts/react/Layout.astro +++ b/integration/templates/astro-node/src/layouts/react/Layout.astro @@ -5,7 +5,7 @@ interface Props { const { title } = Astro.props; -import { SignedIn, SignedOut, UserButton } from '@clerk/astro/react'; +import { Show, UserButton } from '@clerk/astro/react'; import { LanguagePicker } from '../../components/LanguagePicker'; --- @@ -79,11 +79,11 @@ import { LanguagePicker } from '../../components/LanguagePicker';
- + - + - +
- + diff --git a/integration/templates/astro-node/src/pages/billing/checkout-btn.astro b/integration/templates/astro-node/src/pages/billing/checkout-btn.astro index 736992e6033..3ae0fbfa9db 100644 --- a/integration/templates/astro-node/src/pages/billing/checkout-btn.astro +++ b/integration/templates/astro-node/src/pages/billing/checkout-btn.astro @@ -1,17 +1,17 @@ --- -import { SignedIn, __experimental_CheckoutButton as CheckoutButton } from '@clerk/astro/components'; +import { Show, __experimental_CheckoutButton as CheckoutButton } from '@clerk/astro/components'; import Layout from '../../layouts/Layout.astro'; ---
- + Checkout Now - +
diff --git a/integration/templates/astro-node/src/pages/index.astro b/integration/templates/astro-node/src/pages/index.astro index 089eac14653..c7a92f9330c 100644 --- a/integration/templates/astro-node/src/pages/index.astro +++ b/integration/templates/astro-node/src/pages/index.astro @@ -2,12 +2,12 @@ import Layout from '../layouts/Layout.astro'; import Card from '../components/Card.astro'; -import { SignedIn, SignedOut, SignOutButton, OrganizationSwitcher } from '@clerk/astro/components'; +import { Show, SignOutButton, OrganizationSwitcher } from '@clerk/astro/components'; ---

Welcome to Astro

- + Sign out! - +
@@ -26,7 +26,7 @@ import { SignedIn, SignedOut, SignOutButton, OrganizationSwitcher } from '@clerk role='list' class='link-card-grid' > - + - - + + - +
diff --git a/integration/templates/astro-node/src/pages/react/index.astro b/integration/templates/astro-node/src/pages/react/index.astro index 5fe777167f7..11271836228 100644 --- a/integration/templates/astro-node/src/pages/react/index.astro +++ b/integration/templates/astro-node/src/pages/react/index.astro @@ -2,12 +2,12 @@ import Layout from '../../layouts/react/Layout.astro'; import Card from '../../components/Card.astro'; -import { SignedIn, SignedOut, SignOutButton, OrganizationSwitcher } from '@clerk/astro/react'; +import { Show, SignOutButton, OrganizationSwitcher } from '@clerk/astro/react'; ---

Welcome to Astro + React

- + Sign out! - +
@@ -31,7 +31,7 @@ import { SignedIn, SignedOut, SignOutButton, OrganizationSwitcher } from '@clerk role='list' class='link-card-grid' > - + - - + + - +
diff --git a/integration/templates/astro-node/src/pages/react/only-admins.astro b/integration/templates/astro-node/src/pages/react/only-admins.astro index 0ad2bc1b2ba..bc3b46e75d8 100644 --- a/integration/templates/astro-node/src/pages/react/only-admins.astro +++ b/integration/templates/astro-node/src/pages/react/only-admins.astro @@ -1,23 +1,28 @@ --- -import { Protect } from '@clerk/astro/react'; +import { Show } from '@clerk/astro/react'; import Layout from '../../layouts/react/Layout.astro'; ---
- - -

Not an admin

-
Go to Members Page -

I'm an admin

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

Not an admin

+ + Go to Members Page + +
diff --git a/integration/templates/astro-node/src/pages/react/only-members.astro b/integration/templates/astro-node/src/pages/react/only-members.astro index e0fd91dc11f..df8813acc3b 100644 --- a/integration/templates/astro-node/src/pages/react/only-members.astro +++ b/integration/templates/astro-node/src/pages/react/only-members.astro @@ -1,23 +1,28 @@ --- -import { Protect } from '@clerk/astro/react'; +import { Show } from '@clerk/astro/react'; import Layout from '../../layouts/react/Layout.astro'; ---
- - -

Not a member

- Go to Admin Page -

I'm a member

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

Not a member

+ + Go to Admin Page + +
diff --git a/integration/templates/astro-node/src/pages/transitions/index.astro b/integration/templates/astro-node/src/pages/transitions/index.astro index af29b083fcc..4985e2b77e3 100644 --- a/integration/templates/astro-node/src/pages/transitions/index.astro +++ b/integration/templates/astro-node/src/pages/transitions/index.astro @@ -1,15 +1,15 @@ --- -import { SignedIn, SignedOut, UserButton } from '@clerk/astro/components'; +import { Show, UserButton } from '@clerk/astro/components'; import Layout from '../../layouts/ViewTransitionsLayout.astro'; ---
- + Sign in - - + + - +
diff --git a/integration/templates/expo-web/app/index.tsx b/integration/templates/expo-web/app/index.tsx index 431bf8c209f..ee296309576 100644 --- a/integration/templates/expo-web/app/index.tsx +++ b/integration/templates/expo-web/app/index.tsx @@ -1,6 +1,6 @@ -import { Text, View } from 'react-native'; -import { SignedIn, SignedOut } from '@clerk/expo'; +import { Show } from '@clerk/expo'; import { UserButton } from '@clerk/expo/web'; +import { Text, View } from 'react-native'; export default function Index() { return ( @@ -11,13 +11,13 @@ export default function Index() { alignItems: 'center', }} > - + You are signed in! - - + + You are signed out - + ); } diff --git a/integration/templates/next-app-router-quickstart/src/app/page.tsx b/integration/templates/next-app-router-quickstart/src/app/page.tsx index 98ee4d4bcd3..bf1940d4bdf 100644 --- a/integration/templates/next-app-router-quickstart/src/app/page.tsx +++ b/integration/templates/next-app-router-quickstart/src/app/page.tsx @@ -1,17 +1,44 @@ -import { SignedIn, SignedOut, SignInButton, SignUpButton, UserButton } from '@clerk/nextjs'; +import * as Clerk from '@clerk/nextjs'; +import type { ComponentType, ReactNode } from 'react'; + +type ShowProps = { + children: ReactNode; + when: 'signedIn' | 'signedOut'; +}; + +const Show: ComponentType = + (Clerk as { Show?: ComponentType }).Show || + (({ children, when }) => { + const SignedIn = (Clerk as { SignedIn?: ComponentType<{ children: ReactNode }> }).SignedIn; + const SignedOut = (Clerk as { SignedOut?: ComponentType<{ children: ReactNode }> }).SignedOut; + + if (when === 'signedIn' && SignedIn) { + return {children}; + } + + if (when === 'signedOut' && SignedOut) { + return {children}; + } + + return null; + }); + +const SignInButton = (Clerk as { SignInButton: ComponentType }).SignInButton; +const SignUpButton = (Clerk as { SignUpButton: ComponentType }).SignUpButton; +const UserButton = (Clerk as { UserButton: ComponentType }).UserButton; export default function Home() { return (
- +

signed-out-state

-
- + +

signed-in-state

-
+
); } diff --git a/integration/templates/next-app-router/src/app/billing/checkout-btn/page.tsx b/integration/templates/next-app-router/src/app/billing/checkout-btn/page.tsx index 4904d056e95..2ba15a81a67 100644 --- a/integration/templates/next-app-router/src/app/billing/checkout-btn/page.tsx +++ b/integration/templates/next-app-router/src/app/billing/checkout-btn/page.tsx @@ -1,17 +1,17 @@ -import { SignedIn } from '@clerk/nextjs'; +import { Show } from '@clerk/nextjs'; import { CheckoutButton } from '@clerk/nextjs/experimental'; export default function Home() { return (
- + Checkout Now - +
); } diff --git a/integration/templates/next-app-router/src/app/page.tsx b/integration/templates/next-app-router/src/app/page.tsx index 86ba722b3f3..241053ed048 100644 --- a/integration/templates/next-app-router/src/app/page.tsx +++ b/integration/templates/next-app-router/src/app/page.tsx @@ -1,4 +1,4 @@ -import { Protect, SignedIn, SignedOut, SignIn, UserButton } from '@clerk/nextjs'; +import { Show, SignIn, UserButton } from '@clerk/nextjs'; import Link from 'next/link'; import { ClientId } from './client-id'; @@ -7,18 +7,23 @@ export default function Home() {
Loading user button} /> - SignedIn - SignedOut - SignedIn from protect - + SignedIn + SignedOut + + SignedIn from protect + +

user in free

-
- + +

user in pro

-
- + +

user in plus

-
+ - +

user in free

-
- + +

user in pro

-
- + +

user in plus

-
+ ); diff --git a/integration/templates/next-app-router/src/app/settings/rcc-protect/page.tsx b/integration/templates/next-app-router/src/app/settings/rcc-protect/page.tsx index 5b371ed9b2f..bd13e14387d 100644 --- a/integration/templates/next-app-router/src/app/settings/rcc-protect/page.tsx +++ b/integration/templates/next-app-router/src/app/settings/rcc-protect/page.tsx @@ -1,14 +1,13 @@ 'use client'; -import { Protect } from '@clerk/nextjs'; +import { Show } from '@clerk/nextjs'; export default function Page() { return ( - User is missing permissions

} + when={{ permission: 'org:posts:manage' }} >

User has access

-
+ ); } diff --git a/integration/templates/next-app-router/src/app/settings/rsc-protect/page.tsx b/integration/templates/next-app-router/src/app/settings/rsc-protect/page.tsx index 9e21b23d034..56871f6d926 100644 --- a/integration/templates/next-app-router/src/app/settings/rsc-protect/page.tsx +++ b/integration/templates/next-app-router/src/app/settings/rsc-protect/page.tsx @@ -1,12 +1,12 @@ -import { Protect } from '@clerk/nextjs'; +import { Show } from '@clerk/nextjs'; export default function Page() { return ( - User is not admin

} + when={{ role: 'org:admin' }} >

User has access

-
+ ); } diff --git a/integration/templates/react-cra/src/App.tsx b/integration/templates/react-cra/src/App.tsx index 38197953f08..28309fe6b6f 100644 --- a/integration/templates/react-cra/src/App.tsx +++ b/integration/templates/react-cra/src/App.tsx @@ -1,15 +1,15 @@ // @ts-ignore import React from 'react'; import './App.css'; -import { SignedIn, SignedOut, SignIn, UserButton } from '@clerk/react'; +import { Show, SignIn, UserButton } from '@clerk/react'; function App() { return (
- + - - Signed In + + Signed In
); diff --git a/integration/templates/react-router-library/src/App.tsx b/integration/templates/react-router-library/src/App.tsx index 93dfdf04385..259bb2fc944 100644 --- a/integration/templates/react-router-library/src/App.tsx +++ b/integration/templates/react-router-library/src/App.tsx @@ -1,15 +1,15 @@ -import { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/react-router'; +import { Show, SignInButton, UserButton } from '@clerk/react-router'; import './App.css'; function App() { return (
- + - - + + - +
); } diff --git a/integration/templates/react-router-node/app/routes/home.tsx b/integration/templates/react-router-node/app/routes/home.tsx index 57161c90b48..9adefddec39 100644 --- a/integration/templates/react-router-node/app/routes/home.tsx +++ b/integration/templates/react-router-node/app/routes/home.tsx @@ -1,4 +1,4 @@ -import { SignedIn, SignedOut, UserButton } from '@clerk/react-router'; +import { Show, UserButton } from '@clerk/react-router'; import type { Route } from './+types/home'; export function meta({}: Route.MetaArgs) { @@ -9,8 +9,8 @@ export default function Home() { return (
- SignedIn - SignedOut + SignedIn + SignedOut
); } diff --git a/integration/templates/react-vite/src/App.tsx b/integration/templates/react-vite/src/App.tsx index 3c7aabd5906..a826457118f 100644 --- a/integration/templates/react-vite/src/App.tsx +++ b/integration/templates/react-vite/src/App.tsx @@ -1,4 +1,4 @@ -import { OrganizationSwitcher, SignedIn, SignedOut, UserButton } from '@clerk/react'; +import { OrganizationSwitcher, Show, UserButton } from '@clerk/react'; import { Link } from 'react-router-dom'; import React from 'react'; import { ClientId } from './client-id'; @@ -9,8 +9,8 @@ function App() { Loading organization switcher} /> - SignedOut - SignedIn + SignedOut + SignedIn Protected
); diff --git a/integration/templates/react-vite/src/protected/index.tsx b/integration/templates/react-vite/src/protected/index.tsx index 2eb58aa8d76..1a8bcccaac5 100644 --- a/integration/templates/react-vite/src/protected/index.tsx +++ b/integration/templates/react-vite/src/protected/index.tsx @@ -1,11 +1,11 @@ -import { SignedIn } from '@clerk/react'; +import { Show } from '@clerk/react'; export default function Page() { return (
- +
Protected
-
+
); } diff --git a/integration/templates/tanstack-react-start/src/routes/index.tsx b/integration/templates/tanstack-react-start/src/routes/index.tsx index a5c9bfe8dd4..7564211722a 100644 --- a/integration/templates/tanstack-react-start/src/routes/index.tsx +++ b/integration/templates/tanstack-react-start/src/routes/index.tsx @@ -1,4 +1,4 @@ -import { SignedIn, UserButton, SignOutButton, SignedOut, SignIn } from '@clerk/tanstack-react-start'; +import { Show, SignIn, SignOutButton, UserButton } from '@clerk/tanstack-react-start'; import { createFileRoute } from '@tanstack/react-router'; export const Route = createFileRoute('/')({ @@ -9,7 +9,7 @@ function Home() { return (

Index Route

- +

You are signed in!

View your profile here

@@ -18,12 +18,12 @@ function Home() {
- - + +

You are signed out

-
+
); } diff --git a/integration/templates/vue-vite/src/App.vue b/integration/templates/vue-vite/src/App.vue index 6477a90213f..c0c615dd2ec 100644 --- a/integration/templates/vue-vite/src/App.vue +++ b/integration/templates/vue-vite/src/App.vue @@ -1,5 +1,5 @@ @@ -11,12 +11,12 @@ import LanguagePicker from './components/LanguagePicker.vue';

Vue Clerk Integration test

- + - - + + Sign in - +
diff --git a/integration/templates/vue-vite/src/views/Admin.vue b/integration/templates/vue-vite/src/views/Admin.vue index cda8c50afb7..1a685a48e50 100644 --- a/integration/templates/vue-vite/src/views/Admin.vue +++ b/integration/templates/vue-vite/src/views/Admin.vue @@ -1,12 +1,12 @@ diff --git a/integration/templates/vue-vite/src/views/Home.vue b/integration/templates/vue-vite/src/views/Home.vue index e12e3680290..e89dbf87707 100644 --- a/integration/templates/vue-vite/src/views/Home.vue +++ b/integration/templates/vue-vite/src/views/Home.vue @@ -1,16 +1,18 @@ diff --git a/integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue b/integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue index 39c23365733..70c7dbd545e 100644 --- a/integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue +++ b/integration/templates/vue-vite/src/views/billing/CheckoutBtn.vue @@ -1,17 +1,17 @@ diff --git a/integration/tests/vue/components.test.ts b/integration/tests/vue/components.test.ts index c803a6adc6b..c5aa518a358 100644 --- a/integration/tests/vue/components.test.ts +++ b/integration/tests/vue/components.test.ts @@ -259,7 +259,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })('basic te await u.po.signIn.waitForMounted(); }); - test('renders component contents to admins', async ({ page, context }) => { + test('renders guard contents to admins', async ({ page, context }) => { const u = createTestUtils({ app, page, context }); await u.page.goToRelative('/sign-in'); await u.po.signIn.waitForMounted(); diff --git a/packages/astro/src/astro-components/control/Show.astro b/packages/astro/src/astro-components/control/Show.astro new file mode 100644 index 00000000000..5ec00379627 --- /dev/null +++ b/packages/astro/src/astro-components/control/Show.astro @@ -0,0 +1,27 @@ +--- +import ShowCSR from './ShowCSR.astro'; +import ShowSSR from './ShowSSR.astro'; + +import { isStaticOutput } from 'virtual:@clerk/astro/config'; + +type Props = { + when: 'signedIn' | 'signedOut'; + isStatic?: boolean; + /** + * The class name to apply to the outermost element of the component. + * This class is only applied to static components. + */ + class?: string; +}; + +const { when, isStatic, class: className } = Astro.props; + +const ShowComponent = isStaticOutput(isStatic) ? ShowCSR : ShowSSR; +--- + + + + diff --git a/packages/astro/src/astro-components/control/ShowCSR.astro b/packages/astro/src/astro-components/control/ShowCSR.astro new file mode 100644 index 00000000000..75be7aa27d5 --- /dev/null +++ b/packages/astro/src/astro-components/control/ShowCSR.astro @@ -0,0 +1,49 @@ +--- +type Props = { + when: 'signedIn' | 'signedOut'; + class?: string; +}; + +const { when, class: className } = Astro.props; +--- + + + + diff --git a/packages/astro/src/astro-components/control/ShowSSR.astro b/packages/astro/src/astro-components/control/ShowSSR.astro new file mode 100644 index 00000000000..c9a4817ef2f --- /dev/null +++ b/packages/astro/src/astro-components/control/ShowSSR.astro @@ -0,0 +1,11 @@ +--- +type Props = { + when: 'signedIn' | 'signedOut'; +}; + +const { when } = Astro.props; +const { userId } = Astro.locals.auth(); +--- + +{when === 'signedIn' ? userId ? : null : null} +{when === 'signedOut' ? !userId ? : null : null} diff --git a/packages/astro/src/astro-components/index.ts b/packages/astro/src/astro-components/index.ts index 5c9d9b8361f..b9ef5796eb6 100644 --- a/packages/astro/src/astro-components/index.ts +++ b/packages/astro/src/astro-components/index.ts @@ -1,8 +1,7 @@ /** * Control Components */ -export { default as SignedIn } from './control/SignedIn.astro'; -export { default as SignedOut } from './control/SignedOut.astro'; +export { default as Show } from './control/Show.astro'; export { default as Protect } from './control/Protect.astro'; export { default as AuthenticateWithRedirectCallback } from './control/AuthenticateWithRedirectCallback.astro'; diff --git a/packages/astro/src/react/controlComponents.tsx b/packages/astro/src/react/controlComponents.tsx index 956a9f61347..5950aa83aa4 100644 --- a/packages/astro/src/react/controlComponents.tsx +++ b/packages/astro/src/react/controlComponents.tsx @@ -1,30 +1,10 @@ -import type { HandleOAuthCallbackParams, PendingSessionOptions } from '@clerk/shared/types'; +import type { HandleOAuthCallbackParams, PendingSessionOptions, ShowWhenCondition } from '@clerk/shared/types'; import { computed } from 'nanostores'; -import type { PropsWithChildren } from 'react'; import React, { useEffect, useState } from 'react'; import { $csrState } from '../stores/internal'; -import type { ProtectProps as _ProtectProps } from '../types'; import { useAuth } from './hooks'; -import type { WithClerkProp } from './utils'; -import { withClerk } from './utils'; - -export function SignedOut({ children, treatPendingAsSignedOut }: PropsWithChildren) { - const { userId } = useAuth({ treatPendingAsSignedOut }); - - if (userId) { - return null; - } - return children; -} - -export function SignedIn({ children, treatPendingAsSignedOut }: PropsWithChildren) { - const { userId } = useAuth({ treatPendingAsSignedOut }); - if (!userId) { - return null; - } - return children; -} +import { withClerk, type WithClerkProp } from './utils'; const $isLoadingClerkStore = computed($csrState, state => state.isLoaded); @@ -69,70 +49,40 @@ export const ClerkLoading = ({ children }: React.PropsWithChildren): JSX.Element return <>{children}; }; -export type ProtectProps = React.PropsWithChildren< - _ProtectProps & { fallback?: React.ReactNode } & PendingSessionOptions +export type ShowProps = React.PropsWithChildren< + { + fallback?: React.ReactNode; + when: ShowWhenCondition; + } & PendingSessionOptions >; -/** - * Use `` in order to prevent unauthenticated or unauthorized users from accessing the children passed to the component. - * - * Examples: - * ``` - * - * - * has({permission:"a_permission_key"})} /> - * has({role:"a_role_key"})} /> - * Unauthorized

} /> - * ``` - */ -export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAuthorizedParams }: ProtectProps) => { - const { isLoaded, has, userId } = useAuth({ treatPendingAsSignedOut }); +export const Show = ({ children, fallback, treatPendingAsSignedOut, when }: ShowProps) => { + const { has, isLoaded, userId } = useAuth({ treatPendingAsSignedOut }); - /** - * Avoid flickering children or fallback while clerk is loading sessionId or userId - */ if (!isLoaded) { return null; } - /** - * Fallback to UI provided by user or `null` if authorization checks failed - */ + const authorized = <>{children}; const unauthorized = <>{fallback ?? null}; - const authorized = <>{children}; + if (when === 'signedOut') { + return userId ? unauthorized : authorized; + } if (!userId) { return unauthorized; } - /** - * Check against the results of `has` called inside the callback - */ - if (typeof restAuthorizedParams.condition === 'function') { - if (restAuthorizedParams.condition(has)) { - return authorized; - } - return unauthorized; + if (when === 'signedIn') { + return authorized; } - if ( - restAuthorizedParams.role || - restAuthorizedParams.permission || - restAuthorizedParams.feature || - restAuthorizedParams.plan - ) { - if (has?.(restAuthorizedParams)) { - return authorized; - } - return unauthorized; + if (typeof when === 'function') { + return when(has) ? authorized : unauthorized; } - /** - * If neither of the authorization params are passed behave as the ``. - * If fallback is present render that instead of rendering nothing. - */ - return authorized; + return has(when) ? authorized : unauthorized; }; /** @@ -140,7 +90,7 @@ export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAu */ export const AuthenticateWithRedirectCallback = withClerk( ({ clerk, ...handleRedirectCallbackParams }: WithClerkProp) => { - React.useEffect(() => { + useEffect(() => { void clerk?.handleRedirectCallback(handleRedirectCallbackParams); }, []); diff --git a/packages/chrome-extension/docs/clerk-provider.md b/packages/chrome-extension/docs/clerk-provider.md index 150922e5f17..3d2801182ba 100644 --- a/packages/chrome-extension/docs/clerk-provider.md +++ b/packages/chrome-extension/docs/clerk-provider.md @@ -4,22 +4,22 @@ ```tsx // App.tsx -import { SignedIn, SignedOut, SignInButton, UserButton } from '@clerk/chrome-extension'; +import { Show, SignInButton, UserButton } from '@clerk/chrome-extension'; function App() { return ( <>
- + - - + + - +
- Please Sign In - Welcome! + Please Sign In + Welcome!
); @@ -61,7 +61,7 @@ export default IndexPopup; You can hook into the router of your choice to handle navigation. Here's an example using `react-router-dom`: ```tsx -import { ClerkProvider } from '@clerk/chrome-extension'; +import { ClerkProvider, Show, SignIn, SignUp } from '@clerk/chrome-extension'; import { useNavigate, Routes, Route, MemoryRouter } from 'react-router-dom'; import App from './App'; @@ -80,13 +80,13 @@ function AppWithRouting() { path='/' element={ <> - Welcome User! - + Welcome User! + - +
} /> diff --git a/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap index 120fb6d4a1c..9848db006d1 100644 --- a/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/chrome-extension/src/__tests__/__snapshots__/exports.test.ts.snap @@ -15,20 +15,18 @@ exports[`public exports > should not include a breaking change 1`] = ` "OrganizationProfile", "OrganizationSwitcher", "PricingTable", - "Protect", "RedirectToCreateOrganization", "RedirectToOrganizationProfile", "RedirectToSignIn", "RedirectToSignUp", "RedirectToUserProfile", + "Show", "SignIn", "SignInButton", "SignInWithMetamaskButton", "SignOutButton", "SignUp", "SignUpButton", - "SignedIn", - "SignedOut", "UserAvatar", "UserButton", "UserProfile", diff --git a/packages/chrome-extension/src/react/re-exports.ts b/packages/chrome-extension/src/react/re-exports.ts index 2838dc6264b..d05f4a29ba5 100644 --- a/packages/chrome-extension/src/react/re-exports.ts +++ b/packages/chrome-extension/src/react/re-exports.ts @@ -10,20 +10,18 @@ export { OrganizationProfile, OrganizationSwitcher, PricingTable, - Protect, RedirectToCreateOrganization, RedirectToOrganizationProfile, RedirectToSignIn, RedirectToSignUp, RedirectToUserProfile, + Show, SignIn, SignInButton, SignInWithMetamaskButton, SignOutButton, SignUp, SignUpButton, - SignedIn, - SignedOut, UserAvatar, UserButton, UserProfile, diff --git a/packages/expo/src/components/controlComponents.tsx b/packages/expo/src/components/controlComponents.tsx index bc42b9dbc73..5ef4f45e015 100644 --- a/packages/expo/src/components/controlComponents.tsx +++ b/packages/expo/src/components/controlComponents.tsx @@ -1 +1 @@ -export { ClerkLoaded, ClerkLoading, SignedIn, SignedOut, Protect } from '@clerk/react'; +export { ClerkLoaded, ClerkLoading, Show } from '@clerk/react'; diff --git a/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx new file mode 100644 index 00000000000..680f8c96b1d --- /dev/null +++ b/packages/nextjs/src/app-router/server/__tests__/controlComponents.test.tsx @@ -0,0 +1,118 @@ +import type { ShowWhenCondition } from '@clerk/shared/types'; +import React from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +import { auth } from '../auth'; +import { Show } from '../controlComponents'; + +vi.mock('../auth', () => ({ + auth: vi.fn(), +})); + +const mockAuth = auth as unknown as ReturnType; + +const render = async (element: Promise) => { + const resolved = await element; + if (!resolved) { + return ''; + } + return renderToStaticMarkup(resolved); +}; + +const setAuthReturn = (value: { has?: (params: unknown) => boolean; userId: string | null }) => { + mockAuth.mockResolvedValue(value); +}; + +const signedInWhen: ShowWhenCondition = 'signedIn'; +const signedOutWhen: ShowWhenCondition = 'signedOut'; + +describe('Show (App Router server)', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders children when signed in', async () => { + const has = vi.fn(); + setAuthReturn({ has, userId: 'user_123' }); + + const html = await render( + Show({ + children:
signed-in
, + fallback:
fallback
, + treatPendingAsSignedOut: false, + when: signedInWhen, + }), + ); + + expect(mockAuth).toHaveBeenCalledWith({ treatPendingAsSignedOut: false }); + expect(html).toContain('signed-in'); + }); + + it('renders children when signed out', async () => { + const has = vi.fn(); + setAuthReturn({ has, userId: null }); + + const html = await render( + Show({ + children:
signed-out
, + fallback:
fallback
, + treatPendingAsSignedOut: false, + when: signedOutWhen, + }), + ); + + expect(html).toContain('signed-out'); + }); + + it('renders fallback when signed out but user is present', async () => { + const has = vi.fn(); + setAuthReturn({ has, userId: 'user_123' }); + + const html = await render( + Show({ + children:
signed-out
, + fallback:
fallback
, + treatPendingAsSignedOut: false, + when: signedOutWhen, + }), + ); + + expect(html).toContain('fallback'); + }); + + it('uses has() when when is an authorization object', async () => { + const has = vi.fn().mockReturnValue(true); + setAuthReturn({ has, userId: 'user_123' }); + + const html = await render( + Show({ + children:
authorized
, + fallback:
fallback
, + treatPendingAsSignedOut: false, + when: { role: 'admin' }, + }), + ); + + expect(has).toHaveBeenCalledWith({ role: 'admin' }); + expect(html).toContain('authorized'); + }); + + it('uses predicate when when is a function', async () => { + const has = vi.fn().mockReturnValue(true); + const predicate = vi.fn().mockReturnValue(true); + setAuthReturn({ has, userId: 'user_123' }); + + const html = await render( + Show({ + children:
predicate-pass
, + fallback:
fallback
, + treatPendingAsSignedOut: false, + when: predicate, + }), + ); + + expect(predicate).toHaveBeenCalledWith(has); + expect(html).toContain('predicate-pass'); + }); +}); diff --git a/packages/nextjs/src/app-router/server/controlComponents.tsx b/packages/nextjs/src/app-router/server/controlComponents.tsx index d640c63a055..c10416a1633 100644 --- a/packages/nextjs/src/app-router/server/controlComponents.tsx +++ b/packages/nextjs/src/app-router/server/controlComponents.tsx @@ -1,71 +1,66 @@ -import type { ProtectProps } from '@clerk/react'; -import type { PendingSessionOptions } from '@clerk/shared/types'; +import type { PendingSessionOptions, ShowWhenCondition } from '@clerk/shared/types'; import React from 'react'; import { auth } from './auth'; -export async function SignedIn( - props: React.PropsWithChildren, -): Promise { - const { children } = props; - const { userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut }); - return userId ? <>{children} : null; -} - -export async function SignedOut( - props: React.PropsWithChildren, -): Promise { - const { children } = props; - const { userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut }); - return userId ? null : <>{children}; -} +export type AppRouterShowProps = React.PropsWithChildren< + PendingSessionOptions & { + fallback?: React.ReactNode; + when: ShowWhenCondition; + } +>; /** - * Use `` in order to prevent unauthenticated or unauthorized users from accessing the children passed to the component. + * Use `` to render children when an authorization or sign-in condition passes. + * When `treatPendingAsSignedOut` is true, pending sessions are treated as signed out. + * Renders the provided `fallback` (or `null`) when the condition fails. * - * Examples: - * ``` - * - * - * has({permission:"a_permission_key"})} /> - * has({role:"a_role_key"})} /> - * Unauthorized

} /> + * The `when` prop supports: + * - `"signedIn"` or `"signedOut"` shorthands + * - Authorization objects such as `{ permission: "..." }`, `{ role: "..." }`, `{ feature: "..." }`, or `{ plan: "..." }` + * - Predicate functions `(has) => boolean` that receive the `has` helper + * + * @example + * ```tsx + * Unauthorized

}> + * + *
+ * + * + * + * + * + * has({ permission: "org:read" }) && isFeatureEnabled}> + * + * + * + * + * + * * ``` */ -export async function Protect(props: ProtectProps): Promise { - const { children, fallback, ...restAuthorizedParams } = props; - const { has, userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut }); +export async function Show(props: AppRouterShowProps): Promise { + const { children, fallback, treatPendingAsSignedOut, when } = props; + const { has, userId } = await auth({ treatPendingAsSignedOut }); - /** - * Fallback to UI provided by user or `null` if authorization checks failed - */ + const resolvedWhen = when; + const authorized = <>{children}; const unauthorized = fallback ? <>{fallback} : null; - const authorized = <>{children}; + if (typeof resolvedWhen === 'string') { + if (resolvedWhen === 'signedOut') { + return userId ? unauthorized : authorized; + } + return userId ? authorized : unauthorized; + } if (!userId) { return unauthorized; } - /** - * Check against the results of `has` called inside the callback - */ - if (typeof restAuthorizedParams.condition === 'function') { - return restAuthorizedParams.condition(has) ? authorized : unauthorized; - } - - if ( - restAuthorizedParams.role || - restAuthorizedParams.permission || - restAuthorizedParams.feature || - restAuthorizedParams.plan - ) { - return has(restAuthorizedParams) ? authorized : unauthorized; + if (typeof resolvedWhen === 'function') { + return resolvedWhen(has) ? authorized : unauthorized; } - /** - * If neither of the authorization params are passed behave as the ``. - * If fallback is present render that instead of rendering nothing. - */ - return authorized; + return has(resolvedWhen) ? authorized : unauthorized; } diff --git a/packages/nextjs/src/client-boundary/controlComponents.ts b/packages/nextjs/src/client-boundary/controlComponents.ts index 1ab240a18f5..544c2e10145 100644 --- a/packages/nextjs/src/client-boundary/controlComponents.ts +++ b/packages/nextjs/src/client-boundary/controlComponents.ts @@ -1,20 +1,18 @@ 'use client'; export { - ClerkLoaded, - ClerkLoading, + AuthenticateWithRedirectCallback, ClerkDegraded, ClerkFailed, - SignedOut, - SignedIn, - Protect, + ClerkLoaded, + ClerkLoading, + RedirectToCreateOrganization, + RedirectToOrganizationProfile, RedirectToSignIn, RedirectToSignUp, RedirectToTasks, RedirectToUserProfile, - AuthenticateWithRedirectCallback, - RedirectToCreateOrganization, - RedirectToOrganizationProfile, + Show, } from '@clerk/react'; export { MultisessionAppSupport } from '@clerk/react/internal'; diff --git a/packages/nextjs/src/components.client.ts b/packages/nextjs/src/components.client.ts index aac3f82f65b..4635a9f1367 100644 --- a/packages/nextjs/src/components.client.ts +++ b/packages/nextjs/src/components.client.ts @@ -1,2 +1,2 @@ export { ClerkProvider } from './client-boundary/ClerkProvider'; -export { SignedIn, SignedOut, Protect } from './client-boundary/controlComponents'; +export { Show } from './client-boundary/controlComponents'; diff --git a/packages/nextjs/src/components.server.ts b/packages/nextjs/src/components.server.ts index f73c8cc91c5..11eab24d2e6 100644 --- a/packages/nextjs/src/components.server.ts +++ b/packages/nextjs/src/components.server.ts @@ -1,11 +1,9 @@ import { ClerkProvider } from './app-router/server/ClerkProvider'; -import { Protect, SignedIn, SignedOut } from './app-router/server/controlComponents'; +import { Show } from './app-router/server/controlComponents'; -export { ClerkProvider, SignedOut, SignedIn, Protect }; +export { ClerkProvider, Show }; export type ServerComponentsServerModuleTypes = { ClerkProvider: typeof ClerkProvider; - SignedIn: typeof SignedIn; - SignedOut: typeof SignedOut; - Protect: typeof Protect; + Show: typeof Show; }; diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index b9c24e9b7ce..c4123f6729c 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -73,6 +73,4 @@ import * as ComponentsModule from '#components'; import type { ServerComponentsServerModuleTypes } from './components.server'; export const ClerkProvider = ComponentsModule.ClerkProvider as ServerComponentsServerModuleTypes['ClerkProvider']; -export const SignedIn = ComponentsModule.SignedIn as ServerComponentsServerModuleTypes['SignedIn']; -export const SignedOut = ComponentsModule.SignedOut as ServerComponentsServerModuleTypes['SignedOut']; -export const Protect = ComponentsModule.Protect as ServerComponentsServerModuleTypes['Protect']; +export const Show = ComponentsModule.Show as ServerComponentsServerModuleTypes['Show']; diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index c5d42b4b6c3..0f0fb72e6f0 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -175,14 +175,12 @@ export default defineNuxtModule({ // Control Components 'ClerkLoaded', 'ClerkLoading', - 'Protect', 'RedirectToSignIn', 'RedirectToSignUp', 'RedirectToUserProfile', 'RedirectToOrganizationProfile', 'RedirectToCreateOrganization', - 'SignedIn', - 'SignedOut', + 'Show', 'Waitlist', ]; otherComponents.forEach(component => { diff --git a/packages/nuxt/src/runtime/components/index.ts b/packages/nuxt/src/runtime/components/index.ts index 61bde896c00..5d4cf17560a 100644 --- a/packages/nuxt/src/runtime/components/index.ts +++ b/packages/nuxt/src/runtime/components/index.ts @@ -9,9 +9,7 @@ export { // Control components ClerkLoaded, ClerkLoading, - SignedOut, - SignedIn, - Protect, + Show, RedirectToSignIn, RedirectToSignUp, RedirectToUserProfile, diff --git a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap index 54b196e9899..b1fb6544b7b 100644 --- a/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/react-router/src/__tests__/__snapshots__/exports.test.ts.snap @@ -29,21 +29,19 @@ exports[`root public exports > should not change unexpectedly 1`] = ` "OrganizationProfile", "OrganizationSwitcher", "PricingTable", - "Protect", "RedirectToCreateOrganization", "RedirectToOrganizationProfile", "RedirectToSignIn", "RedirectToSignUp", "RedirectToTasks", "RedirectToUserProfile", + "Show", "SignIn", "SignInButton", "SignInWithMetamaskButton", "SignOutButton", "SignUp", "SignUpButton", - "SignedIn", - "SignedOut", "TaskChooseOrganization", "TaskResetPassword", "UserAvatar", diff --git a/packages/react/src/components/CheckoutButton.tsx b/packages/react/src/components/CheckoutButton.tsx index f095bcc77ff..bc041c275be 100644 --- a/packages/react/src/components/CheckoutButton.tsx +++ b/packages/react/src/components/CheckoutButton.tsx @@ -7,27 +7,26 @@ import { assertSingleChild, normalizeWithDefaultValue, safeExecute } from '../ut import { withClerk } from './withClerk'; /** - * A button component that opens the Clerk Checkout drawer when clicked. This component must be rendered - * inside a `` component to ensure the user is authenticated. + * A button component that opens the Clerk Checkout drawer when clicked. Render only when the user is signed in (e.g., wrap with ``). * * @example * ```tsx - * import { SignedIn } from '@clerk/react'; + * import { Show } from '@clerk/react'; * import { CheckoutButton } from '@clerk/react/experimental'; * * // Basic usage with default "Checkout" text * function BasicCheckout() { * return ( - * + * * - * + * * ); * } * * // Custom button with organization subscription * function OrganizationCheckout() { * return ( - * + * * * * - * + *
* ); * } * ``` * - * @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/protect.ts b/packages/shared/src/types/protect.ts index 0498c2b5f1b..4727dab46cb 100644 --- a/packages/shared/src/types/protect.ts +++ b/packages/shared/src/types/protect.ts @@ -1,11 +1,11 @@ import type { OrganizationCustomPermissionKey, OrganizationCustomRoleKey } from './organizationMembership'; -import type { CheckAuthorizationWithCustomPermissions } from './session'; +import type { CheckAuthorizationWithCustomPermissions, PendingSessionOptions } from './session'; import type { Autocomplete } from './utils'; /** - * Props for the `` component, which restricts access to its children based on authentication and authorization. + * Authorization parameters used by `` and `auth.protect()`. * - * Use `ProtectProps` to specify the required Role, Permission, Feature, or Plan for access. + * Use `ProtectParams` to specify the required role, permission, feature, or plan for access. * * @example * ```tsx @@ -21,11 +21,11 @@ import type { Autocomplete } from './utils'; * // Require a specific Feature * * - * // Require a specific Plan + * // Require a specific plan * * ``` */ -export type ProtectProps = +export type ProtectParams = | { condition?: never; role: OrganizationCustomRoleKey; @@ -68,3 +68,46 @@ export type ProtectProps = feature?: never; plan?: never; }; + +/** + * @deprecated Use {@link ProtectParams} instead. + */ +export type ProtectProps = ProtectParams; + +/** + * Authorization condition for the `when` prop in ``. + * Can be an object specifying role, permission, feature, or plan, + * or a callback function receiving the `has` helper for complex conditions. + */ +export type ShowWhenCondition = + | 'signedIn' + | 'signedOut' + | ProtectParams + | ((has: CheckAuthorizationWithCustomPermissions) => boolean); + +/** + * Props for the `` component, which conditionally renders children based on authorization. + * + * @example + * ```tsx + * // Require a specific permission + * ... + * + * // Require a specific role + * ... + * + * // Use a custom condition callback + * has({ permission: "org:read" }) && someCondition}>... + * + * // Require a specific feature + * ... + * + * // Require a specific plan + * ... + * ``` + * + */ +export type ShowProps = PendingSessionOptions & { + fallback?: unknown; + when: ShowWhenCondition; +}; diff --git a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap index 3e1c592195b..eaba504c812 100644 --- a/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap +++ b/packages/tanstack-react-start/src/__tests__/__snapshots__/exports.test.ts.snap @@ -34,21 +34,19 @@ exports[`root public exports > should not change unexpectedly 1`] = ` "OrganizationProfile", "OrganizationSwitcher", "PricingTable", - "Protect", "RedirectToCreateOrganization", "RedirectToOrganizationProfile", "RedirectToSignIn", "RedirectToSignUp", "RedirectToTasks", "RedirectToUserProfile", + "Show", "SignIn", "SignInButton", "SignInWithMetamaskButton", "SignOutButton", "SignUp", "SignUpButton", - "SignedIn", - "SignedOut", "TaskChooseOrganization", "TaskResetPassword", "UserAvatar", diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js new file mode 100644 index 00000000000..4b40915c4df --- /dev/null +++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-protect-to-show.fixtures.js @@ -0,0 +1,315 @@ +export const fixtures = [ + { + name: 'Transforms Protect import', + source: ` +import { Protect } from "@clerk/react" + `, + output: ` +import { Show } from "@clerk/react" +`, + }, + { + name: 'Transforms SignedIn and SignedOut imports', + source: ` +import { SignedIn, SignedOut } from "@clerk/react" + `, + output: ` +import { Show } from "@clerk/react"; +`, + }, + { + name: 'Transforms Protect in TSX', + source: ` +import { Protect } from "@clerk/react" + +function App() { + return ( + + + + ) +} + `, + output: ` +import { Show } from "@clerk/react" + +function App() { + return ( + + + + ); +} +`, + }, + { + name: 'Transforms SignedIn usage', + source: ` +import { SignedIn } from "@clerk/react" + +const App = () => ( + +
Child
+
+) + `, + output: ` +import { Show } from "@clerk/react" + +const App = () => ( + +
Child
+
+) +`, + }, + { + name: 'Transforms SignedOut usage', + source: ` +import { SignedOut } from "@clerk/react" + +const App = () => ( + +
Child
+
+) + `, + output: ` +import { Show } from "@clerk/react" + +const App = () => ( + +
Child
+
+) +`, + }, + { + name: 'Transforms SignedIn namespace import', + source: ` +import * as Clerk from "@clerk/react" + +const App = () => ( + +
Child
+
+) + `, + output: ` +import * as Clerk from "@clerk/react" + +const App = () => ( + +
Child
+
+) +`, + }, + { + name: 'Transforms Protect condition callback', + source: ` +import { Protect } from "@clerk/react" + +function App() { + return ( + has({ role: "admin" })}> + + + ) +} + `, + output: ` +import { Show } from "@clerk/react" + +function App() { + return ( + has({ role: "admin" })}> + + + ); +} +`, + }, + { + name: 'Transforms SignedIn import with other specifiers', + source: ` +import { ClerkProvider, SignedIn } from "@clerk/nextjs" + `, + output: ` +import { ClerkProvider, Show } from "@clerk/nextjs" +`, + }, + { + name: 'Transforms ProtectProps type', + source: ` +import { ProtectProps } from "@clerk/react"; +type Props = ProtectProps; + `, + output: ` +import { ShowProps } from "@clerk/react"; +type Props = ShowProps; +`, + }, + { + name: 'Self-closing Protect defaults to signedIn', + source: ` +import { Protect } from "@clerk/react" + +const Thing = () => + `, + output: ` +import { Show } from "@clerk/react" + +const Thing = () => +`, + }, + { + name: 'Transforms Protect from hybrid package without client directive', + source: ` +import { Protect } from "@clerk/nextjs" + +const App = () => ( + +
Child
+
+) + `, + output: ` +import { Show } from "@clerk/nextjs" + +const App = () => ( + +
Child
+
+) +`, + }, + { + name: 'Transforms SignedOut to Show with fallback prop', + source: ` +import { SignedOut } from "@clerk/react" + +const App = () => ( + }> +
Child
+
+) + `, + output: ` +import { Show } from "@clerk/react" + +const App = () => ( + }> +
Child
+
+) +`, + }, + { + name: 'Transforms SignedOut namespace import with fallback', + source: ` +import * as Clerk from "@clerk/react" + +const App = () => ( + }> +
Child
+
+) + `, + output: ` +import * as Clerk from "@clerk/react" + +const App = () => ( + }> +
Child
+
+) +`, + }, + { + name: 'Aliased Protect import is transformed', + source: ` +import { Protect as CanAccess } from "@clerk/react" + +function App() { + return ( + + + + ) +} + `, + output: ` +import { Show as CanAccess } from "@clerk/react" + +function App() { + return ( + + + + ); +} +`, + }, + { + name: 'ProtectProps type aliases update', + source: ` +import { ProtectProps } from "@clerk/react"; +type Props = ProtectProps; +type Another = ProtectProps; + `, + output: ` +import { ShowProps } from "@clerk/react"; +type Props = ShowProps; +type Another = ShowProps; +`, + }, + { + name: 'Protect with fallback prop', + source: ` +import { Protect } from "@clerk/react" + +function App() { + return ( + }> + + + ) +} + `, + output: ` +import { Show } from "@clerk/react" + +function App() { + return ( + }> + + + ); +} +`, + }, + { + name: 'Protect with spread props', + source: ` +import { Protect } from "@clerk/react" + +const props = { permission: "org:read" } +const App = () => + `, + output: ` +import { Show } from "@clerk/react" + +const props = { permission: "org:read" } +const App = () => +`, + }, +]; diff --git a/packages/upgrade/src/codemods/__tests__/transform-protect-to-show.test.js b/packages/upgrade/src/codemods/__tests__/transform-protect-to-show.test.js new file mode 100644 index 00000000000..435c84b524d --- /dev/null +++ b/packages/upgrade/src/codemods/__tests__/transform-protect-to-show.test.js @@ -0,0 +1,18 @@ +import { applyTransform } from 'jscodeshift/dist/testUtils'; +import { describe, expect, it } from 'vitest'; + +import transformer from '../transform-protect-to-show.cjs'; +import { fixtures } from './__fixtures__/transform-protect-to-show.fixtures'; + +describe('transform-protect-to-show', () => { + it.each(fixtures)(`$name`, ({ source, output }) => { + const result = applyTransform(transformer, {}, { source }); + + if (output === null) { + // null output means no transformation should occur + expect(result).toBeFalsy(); + } else { + expect(result).toEqual(output.trim()); + } + }); +}); diff --git a/packages/upgrade/src/codemods/transform-protect-to-show.cjs b/packages/upgrade/src/codemods/transform-protect-to-show.cjs new file mode 100644 index 00000000000..cc50aef7416 --- /dev/null +++ b/packages/upgrade/src/codemods/transform-protect-to-show.cjs @@ -0,0 +1,246 @@ +// Packages that are always client-side +const CLIENT_ONLY_PACKAGES = ['@clerk/chrome-extension', '@clerk/expo', '@clerk/react', '@clerk/vue']; +// Packages that can be used in both RSC and client components +const HYBRID_PACKAGES = ['@clerk/astro', '@clerk/nextjs']; + +/** + * Transforms `` component usage to `` component. + * + * Handles the following transformations: + * - `` → `` + * - `` → `` + * - `` → `` + * - `` → `` + * - ` ...}>` → ` ...}>` + * - `...` → `...` + * - `...` → `...` + * + * Also updates imports from `Protect` to `Show`. + * + * @param {import('jscodeshift').FileInfo} fileInfo - The file information + * @param {import('jscodeshift').API} api - The API object provided by jscodeshift + * @returns {string|undefined} - The transformed source code if modifications were made + */ +module.exports = function transformProtectToShow({ source }, { jscodeshift: j }) { + const root = j(source); + let dirtyFlag = false; + const componentKindByLocalName = {}; + const protectPropsLocalsToRename = []; + const namespaceImports = new Set(); + + // Transform imports: Protect → Show, ProtectProps → ShowProps + const allPackages = [...CLIENT_ONLY_PACKAGES, ...HYBRID_PACKAGES]; + allPackages.forEach(packageName => { + root.find(j.ImportDeclaration, { source: { value: packageName } }).forEach(path => { + const node = path.node; + const specifiers = node.specifiers || []; + + specifiers.forEach(spec => { + if (j.ImportNamespaceSpecifier.check(spec)) { + if (spec.local?.name) { + namespaceImports.add(spec.local.name); + } + return; + } + + if (j.ImportSpecifier.check(spec)) { + const originalImportedName = spec.imported.name; + + if (['Protect', 'SignedIn', 'SignedOut'].includes(originalImportedName)) { + const effectiveLocalName = spec.local ? spec.local.name : originalImportedName; + componentKindByLocalName[effectiveLocalName] = + originalImportedName === 'Protect' + ? 'protect' + : originalImportedName === 'SignedIn' + ? 'signedIn' + : 'signedOut'; + spec.imported.name = 'Show'; + if (spec.local && spec.local.name === originalImportedName) { + spec.local.name = 'Show'; + } + dirtyFlag = true; + } + + if (spec.imported.name === 'ProtectProps') { + const effectiveLocalName = spec.local ? spec.local.name : spec.imported.name; + spec.imported.name = 'ShowProps'; + if (spec.local && spec.local.name === 'ProtectProps') { + spec.local.name = 'ShowProps'; + } + if (effectiveLocalName === 'ProtectProps') { + protectPropsLocalsToRename.push(effectiveLocalName); + } + dirtyFlag = true; + } + } + }); + + const seenLocalNames = new Set(); + node.specifiers = specifiers.reduce((acc, spec) => { + let localName = null; + + if (spec.local && j.Identifier.check(spec.local)) { + localName = spec.local.name; + } else if (j.ImportSpecifier.check(spec) && j.Identifier.check(spec.imported)) { + localName = spec.imported.name; + } + + if (localName) { + if (seenLocalNames.has(localName)) { + dirtyFlag = true; + return acc; + } + seenLocalNames.add(localName); + } + + acc.push(spec); + return acc; + }, []); + }); + }); + + // Rename references to ProtectProps (only when local name was ProtectProps) + if (protectPropsLocalsToRename.length > 0) { + root + .find(j.TSTypeReference, { + typeName: { + type: 'Identifier', + name: 'ProtectProps', + }, + }) + .forEach(path => { + const typeName = path.node.typeName; + if (j.Identifier.check(typeName) && typeName.name === 'ProtectProps') { + typeName.name = 'ShowProps'; + dirtyFlag = true; + } + }); + } + + // Transform JSX: + root.find(j.JSXElement).forEach(path => { + const openingElement = path.node.openingElement; + const closingElement = path.node.closingElement; + + let kind = null; + let renameNodeToShow = null; + + if (j.JSXIdentifier.check(openingElement.name)) { + const originalName = openingElement.name.name; + kind = componentKindByLocalName[originalName]; + + if (['Protect', 'SignedIn', 'SignedOut'].includes(originalName)) { + renameNodeToShow = node => { + if (j.JSXIdentifier.check(node)) { + node.name = 'Show'; + } + }; + } + } else if (j.JSXMemberExpression.check(openingElement.name)) { + const member = openingElement.name; + if (j.Identifier.check(member.object) && j.Identifier.check(member.property)) { + const objectName = member.object.name; + const propertyName = member.property.name; + + if (namespaceImports.has(objectName) && ['Protect', 'SignedIn', 'SignedOut'].includes(propertyName)) { + kind = propertyName === 'Protect' ? 'protect' : propertyName === 'SignedIn' ? 'signedIn' : 'signedOut'; + + renameNodeToShow = node => { + if (j.JSXMemberExpression.check(node) && j.Identifier.check(node.property)) { + node.property.name = 'Show'; + } + }; + } + } + } + + if (!kind) { + return; + } + + if (renameNodeToShow) { + renameNodeToShow(openingElement.name); + if (closingElement && closingElement.name) { + renameNodeToShow(closingElement.name); + } + } + + const attributes = openingElement.attributes || []; + const authAttributes = []; + const otherAttributes = []; + let conditionAttr = null; + + // Separate auth-related attributes from other attributes + attributes.forEach(attr => { + if (!j.JSXAttribute.check(attr)) { + otherAttributes.push(attr); + return; + } + + const attrName = attr.name.name; + if (attrName === 'condition') { + conditionAttr = attr; + } else if (['feature', 'permission', 'plan', 'role'].includes(attrName)) { + authAttributes.push(attr); + } else { + otherAttributes.push(attr); + } + }); + + // Build the `when` prop + let whenValue = null; + + if (kind === 'signedIn' || kind === 'signedOut') { + whenValue = j.stringLiteral(kind === 'signedIn' ? 'signedIn' : 'signedOut'); + } else if (conditionAttr) { + // condition prop becomes the when callback directly + whenValue = conditionAttr.value; + } else if (authAttributes.length > 0) { + // Build an object from auth attributes + const properties = authAttributes.map(attr => { + const key = j.identifier(attr.name.name); + let value; + + if (j.JSXExpressionContainer.check(attr.value)) { + value = attr.value.expression; + } else if (j.StringLiteral.check(attr.value) || j.Literal.check(attr.value)) { + value = attr.value; + } else if (attr.value == null) { + value = j.booleanLiteral(true); + } else { + // Default string value + value = j.stringLiteral(attr.value?.value || ''); + } + + return j.objectProperty(key, value); + }); + + whenValue = j.jsxExpressionContainer(j.objectExpression(properties)); + } + + // Reconstruct attributes with `when` prop + const newAttributes = []; + + const defaultWhenValue = kind === 'signedOut' ? 'signedOut' : 'signedIn'; + const finalWhenValue = whenValue || j.stringLiteral(defaultWhenValue); + + newAttributes.push(j.jsxAttribute(j.jsxIdentifier('when'), finalWhenValue)); + + // Add remaining attributes (fallback, etc.) + otherAttributes.forEach(attr => newAttributes.push(attr)); + + openingElement.attributes = newAttributes; + dirtyFlag = true; + }); + + if (!dirtyFlag) { + return undefined; + } + + let result = root.toSource(); + // Fix double semicolons that can occur when recast reprints directive prologues + result = result.replace(/^(['"`][^'"`]+['"`]);;/gm, '$1;'); + return result; +}; + +module.exports.parser = 'tsx'; diff --git a/packages/vue/src/components/CheckoutButton.vue b/packages/vue/src/components/CheckoutButton.vue index 3d5332a4e61..6774e48c452 100644 --- a/packages/vue/src/components/CheckoutButton.vue +++ b/packages/vue/src/components/CheckoutButton.vue @@ -15,7 +15,7 @@ const attrs = useAttrs(); // Authentication checks - similar to React implementation if (userId.value === null) { - throw new Error('Ensure that `` is rendered inside a `` component.'); + throw new Error('Ensure that `` is rendered inside a `` component.'); } if (orgId.value === null && props.for === 'organization') { diff --git a/packages/vue/src/components/SubscriptionDetailsButton.vue b/packages/vue/src/components/SubscriptionDetailsButton.vue index 1d3dce1819a..b41e1bd7642 100644 --- a/packages/vue/src/components/SubscriptionDetailsButton.vue +++ b/packages/vue/src/components/SubscriptionDetailsButton.vue @@ -15,7 +15,9 @@ const attrs = useAttrs(); // Authentication checks - similar to React implementation if (userId.value === null) { - throw new Error('Ensure that `` is rendered inside a `` component.'); + throw new Error( + 'Ensure that `` is rendered inside a `` component.', + ); } if (orgId.value === null && props.for === 'organization') { diff --git a/packages/vue/src/components/controlComponents.ts b/packages/vue/src/components/controlComponents.ts index 5148700900f..8422a75b1eb 100644 --- a/packages/vue/src/components/controlComponents.ts +++ b/packages/vue/src/components/controlComponents.ts @@ -2,28 +2,16 @@ import { deprecated } from '@clerk/shared/deprecated'; import type { HandleOAuthCallbackParams, PendingSessionOptions, - ProtectProps as _ProtectProps, RedirectOptions, + ShowWhenCondition, } from '@clerk/shared/types'; -import { defineComponent } from 'vue'; +import { defineComponent, type VNodeChild } from 'vue'; import { useAuth } from '../composables/useAuth'; import { useClerk } from '../composables/useClerk'; import { useClerkContext } from '../composables/useClerkContext'; import { useClerkLoaded } from '../utils/useClerkLoaded'; -export const SignedIn = defineComponent(({ treatPendingAsSignedOut }, { slots }) => { - const { userId } = useAuth({ treatPendingAsSignedOut }); - - return () => (userId.value ? slots.default?.() : null); -}); - -export const SignedOut = defineComponent(({ treatPendingAsSignedOut }, { slots }) => { - const { userId } = useAuth({ treatPendingAsSignedOut }); - - return () => (userId.value === null ? slots.default?.() : null); -}); - export const ClerkLoaded = defineComponent((_, { slots }) => { const clerk = useClerk(); @@ -112,9 +100,28 @@ export const AuthenticateWithRedirectCallback = defineComponent((props: HandleOA return () => null; }); -export type ProtectProps = _ProtectProps & PendingSessionOptions; +/** + * Props for `` that control when content renders based on sign-in or authorization state. + * + * @public + * @property fallback Optional content shown when the condition fails; can be provided via prop or `fallback` slot. + * @property when Condition controlling visibility; supports `"signedIn"`, `"signedOut"`, authorization descriptors, or a predicate that receives the `has` helper. + * @property treatPendingAsSignedOut Inherited from `PendingSessionOptions`; treat pending sessions as signed out while loading. + * @example + * ```vue + * + * + * + * + * + * + * + * + * ``` + */ +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={ <> - + - - + + - + } /> diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4bbb3ed5f9a..3c167fe6a74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2355,7 +2355,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {'0': node >=0.10.0} + engines: {node: '>=0.10.0'} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==}