Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 5 additions & 8 deletions packages/astro/src/react/controlComponents.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import type { HandleOAuthCallbackParams, PendingSessionOptions } from '@clerk/shared/types';
import type { HandleOAuthCallbackParams, PendingSessionOptions, ProtectParams } from '@clerk/shared/types';
import { computed } from 'nanostores';
import type { PropsWithChildren } from 'react';
import React, { useEffect, useState } from 'react';
import React, { type PropsWithChildren, 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';
import { withClerk, type WithClerkProp } from './utils';

export function SignedOut({ children, treatPendingAsSignedOut }: PropsWithChildren<PendingSessionOptions>) {
const { userId } = useAuth({ treatPendingAsSignedOut });
Expand Down Expand Up @@ -70,7 +67,7 @@ export const ClerkLoading = ({ children }: React.PropsWithChildren): JSX.Element
};

export type ProtectProps = React.PropsWithChildren<
_ProtectProps & { fallback?: React.ReactNode } & PendingSessionOptions
ProtectParams & { fallback?: React.ReactNode } & PendingSessionOptions
>;

/**
Expand Down Expand Up @@ -140,7 +137,7 @@ export const Protect = ({ children, fallback, treatPendingAsSignedOut, ...restAu
*/
export const AuthenticateWithRedirectCallback = withClerk(
({ clerk, ...handleRedirectCallbackParams }: WithClerkProp<HandleOAuthCallbackParams>) => {
React.useEffect(() => {
useEffect(() => {
void clerk?.handleRedirectCallback(handleRedirectCallbackParams);
}, []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ exports[`public exports > should not include a breaking change 1`] = `
"OrganizationProfile",
"OrganizationSwitcher",
"PricingTable",
"Protect",
"RedirectToCreateOrganization",
"RedirectToOrganizationProfile",
"RedirectToSignIn",
"RedirectToSignUp",
"RedirectToUserProfile",
"Show",
"SignIn",
"SignInButton",
"SignInWithMetamaskButton",
Expand Down
2 changes: 1 addition & 1 deletion packages/chrome-extension/src/react/re-exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ export {
OrganizationProfile,
OrganizationSwitcher,
PricingTable,
Protect,
RedirectToCreateOrganization,
RedirectToOrganizationProfile,
RedirectToSignIn,
RedirectToSignUp,
RedirectToUserProfile,
Show,
SignIn,
SignInButton,
SignInWithMetamaskButton,
Expand Down
2 changes: 1 addition & 1 deletion packages/expo/src/components/controlComponents.tsx
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { ClerkLoaded, ClerkLoading, SignedIn, SignedOut, Protect } from '@clerk/react';
export { ClerkLoaded, ClerkLoading, Show, SignedIn, SignedOut } from '@clerk/react';
Original file line number Diff line number Diff line change
@@ -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<typeof vi.fn>;

const render = async (element: Promise<React.JSX.Element | null>) => {
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: <div>signed-in</div>,
fallback: <div>fallback</div>,
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: <div>signed-out</div>,
fallback: <div>fallback</div>,
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: <div>signed-out</div>,
fallback: <div>fallback</div>,
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: <div>authorized</div>,
fallback: <div>fallback</div>,
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: <div>predicate-pass</div>,
fallback: <div>fallback</div>,
treatPendingAsSignedOut: false,
when: predicate,
}),
);

expect(predicate).toHaveBeenCalledWith(has);
expect(html).toContain('predicate-pass');
});
});
74 changes: 71 additions & 3 deletions packages/nextjs/src/app-router/server/controlComponents.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import type { ProtectProps } from '@clerk/react';
import type { PendingSessionOptions } from '@clerk/shared/types';
import type { PendingSessionOptions, ProtectParams, ShowWhenCondition } from '@clerk/shared/types';
import React from 'react';

import { auth } from './auth';

export type AppRouterProtectProps = React.PropsWithChildren<
ProtectParams & {
fallback?: React.ReactNode;
} & PendingSessionOptions
>;

export type AppRouterShowProps = React.PropsWithChildren<
PendingSessionOptions & {
fallback?: React.ReactNode;
when: ShowWhenCondition;
}
>;

export async function SignedIn(
props: React.PropsWithChildren<PendingSessionOptions>,
): Promise<React.JSX.Element | null> {
Expand Down Expand Up @@ -32,7 +44,7 @@ export async function SignedOut(
* <Protect fallback={<p>Unauthorized</p>} />
* ```
*/
export async function Protect(props: ProtectProps): Promise<React.JSX.Element | null> {
export async function Protect(props: AppRouterProtectProps): Promise<React.JSX.Element | null> {
const { children, fallback, ...restAuthorizedParams } = props;
const { has, userId } = await auth({ treatPendingAsSignedOut: props.treatPendingAsSignedOut });

Expand Down Expand Up @@ -69,3 +81,59 @@ export async function Protect(props: ProtectProps): Promise<React.JSX.Element |
*/
return authorized;
}

/**
* Use `<Show/>` to render children when an authorization or sign-in condition passes.
*
* @param props.when Condition that controls rendering. Accepts:
* - authorization objects such as `{ permission: "..." }`, `{ role: "..." }`, `{ feature: "..." }`, or `{ plan: "..." }`
* - the string `"signedIn"` to render when a user is present
* - the string `"signedOut"` to render when no user is present
* - predicate functions `(has) => boolean` that receive the `has` helper
* @param props.fallback Optional content rendered when the condition fails.
* @param props.children Content rendered when the condition passes.
*
* @example
* ```tsx
* <Show when={{ permission: "org:billing:manage" }} fallback={<p>Unauthorized</p>}>
* <BillingSettings />
* </Show>
*
* <Show when={{ role: "admin" }}>
* <AdminPanel />
* </Show>
*
* <Show when={(has) => has({ permission: "org:read" }) && isFeatureEnabled}>
* <ProtectedFeature />
* </Show>
*
* <Show when="signedIn">
* <Dashboard />
* </Show>
* ```
*/
export async function Show(props: AppRouterShowProps): Promise<React.JSX.Element | null> {
const { children, fallback, treatPendingAsSignedOut, when } = props;
const { has, userId } = await auth({ treatPendingAsSignedOut });

const resolvedWhen = when;
const authorized = <>{children}</>;
const unauthorized = fallback ? <>{fallback}</> : null;

if (typeof resolvedWhen === 'string') {
if (resolvedWhen === 'signedOut') {
return userId ? unauthorized : authorized;
}
return userId ? authorized : unauthorized;
}

if (!userId) {
return unauthorized;
}

if (typeof resolvedWhen === 'function') {
return resolvedWhen(has) ? authorized : unauthorized;
}

return has(resolvedWhen) ? authorized : unauthorized;
}
16 changes: 8 additions & 8 deletions packages/nextjs/src/client-boundary/controlComponents.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
'use client';

export {
ClerkLoaded,
ClerkLoading,
AuthenticateWithRedirectCallback,
ClerkDegraded,
ClerkFailed,
SignedOut,
SignedIn,
Protect,
ClerkLoaded,
ClerkLoading,
RedirectToCreateOrganization,
RedirectToOrganizationProfile,
RedirectToSignIn,
RedirectToSignUp,
RedirectToTasks,
RedirectToUserProfile,
AuthenticateWithRedirectCallback,
RedirectToCreateOrganization,
RedirectToOrganizationProfile,
Show,
SignedIn,
SignedOut,
} from '@clerk/react';

export { MultisessionAppSupport } from '@clerk/react/internal';
21 changes: 20 additions & 1 deletion packages/nextjs/src/components.client.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,21 @@
export { ClerkProvider } from './client-boundary/ClerkProvider';
export { SignedIn, SignedOut, Protect } from './client-boundary/controlComponents';
export { Show, SignedIn, SignedOut } from './client-boundary/controlComponents';

/**
* `<Protect>` is only available as a React Server Component in the App Router.
* For client-side conditional rendering, use `<Show when={...} />` instead.
*
* @example
* ```tsx
* // Server Component (App Router)
* <Protect permission="org:read">...</Protect>
*
* // Client Component
* <Show when={{ permission: "org:read" }}>...</Show>
* ```
*/
export const Protect = () => {
throw new Error(
'`<Protect>` is only available as a React Server Component. For client components, use `<Show when={...} />` instead.',
);
};
7 changes: 4 additions & 3 deletions packages/nextjs/src/components.server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { ClerkProvider } from './app-router/server/ClerkProvider';
import { Protect, SignedIn, SignedOut } from './app-router/server/controlComponents';
import { Protect, Show, SignedIn, SignedOut } from './app-router/server/controlComponents';

export { ClerkProvider, SignedOut, SignedIn, Protect };
export { ClerkProvider, Protect, Show, SignedIn, SignedOut };

export type ServerComponentsServerModuleTypes = {
ClerkProvider: typeof ClerkProvider;
Protect: typeof Protect;
Show: typeof Show;
SignedIn: typeof SignedIn;
SignedOut: typeof SignedOut;
Protect: typeof Protect;
};
7 changes: 6 additions & 1 deletion packages/nextjs/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export {
RedirectToSignUp,
RedirectToTasks,
RedirectToUserProfile,
Show,
} from './client-boundary/controlComponents';

/**
Expand Down Expand Up @@ -73,6 +74,10 @@ import * as ComponentsModule from '#components';
import type { ServerComponentsServerModuleTypes } from './components.server';

export const ClerkProvider = ComponentsModule.ClerkProvider as ServerComponentsServerModuleTypes['ClerkProvider'];
/**
* Use `<Protect/>` in RSC (App Router) to restrict access based on authentication and authorization.
* For client components, use `<Show when={...} />` instead.
*/
export const Protect = ComponentsModule.Protect as ServerComponentsServerModuleTypes['Protect'];
export const SignedIn = ComponentsModule.SignedIn as ServerComponentsServerModuleTypes['SignedIn'];
export const SignedOut = ComponentsModule.SignedOut as ServerComponentsServerModuleTypes['SignedOut'];
export const Protect = ComponentsModule.Protect as ServerComponentsServerModuleTypes['Protect'];
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ exports[`root public exports > should not change unexpectedly 1`] = `
"OrganizationProfile",
"OrganizationSwitcher",
"PricingTable",
"Protect",
"RedirectToCreateOrganization",
"RedirectToOrganizationProfile",
"RedirectToSignIn",
"RedirectToSignUp",
"RedirectToTasks",
"RedirectToUserProfile",
"Show",
"SignIn",
"SignInButton",
"SignInWithMetamaskButton",
Expand Down
Loading
Loading