diff --git a/.changeset/dry-bobcats-drop.md b/.changeset/dry-bobcats-drop.md new file mode 100644 index 00000000000..d54364aec78 --- /dev/null +++ b/.changeset/dry-bobcats-drop.md @@ -0,0 +1,15 @@ +--- +'@clerk/react': major +'@clerk/expo': major +'@clerk/nextjs': major +'@clerk/react-router': major +'@clerk/tanstack-react-start': major +'@clerk/chrome-extension': patch +'@clerk/elements': patch +--- + +Remove `initialAuthState` option from `useAuth` hook. + +This option was mainly used internally but is no longer necessary, so we are removing it to keep the API simple. + +If you want `useAuth` to return a populated auth state before Clerk has fully loaded, for example during server rendering, see your framework-specific documentation for guidance. diff --git a/packages/expo/src/hooks/useAuth.ts b/packages/expo/src/hooks/useAuth.ts index ab44ce7b726..1a4e0f04e72 100644 --- a/packages/expo/src/hooks/useAuth.ts +++ b/packages/expo/src/hooks/useAuth.ts @@ -8,8 +8,8 @@ import { SessionJWTCache } from '../cache'; * This hook extends the useAuth hook to add experimental JWT caching. * The caching is used only when no options are passed to getToken. */ -export const useAuth = (initialAuthState?: any): UseAuthReturn => { - const { getToken: getTokenBase, ...rest } = useAuthBase(initialAuthState); +export const useAuth = (options?: Parameters[0]): UseAuthReturn => { + const { getToken: getTokenBase, ...rest } = useAuthBase(options); const getToken: GetToken = (opts?: GetTokenOptions): Promise => getTokenBase(opts) diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx index f2efa059292..5a35a71d719 100644 --- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx @@ -1,5 +1,6 @@ 'use client'; import { ClerkProvider as ReactClerkProvider } from '@clerk/react'; +import { InitialAuthStateProvider as ReactInitialAuthStateProvider } from '@clerk/react/internal'; import dynamic from 'next/dynamic'; import { useRouter } from 'next/navigation'; import React from 'react'; @@ -37,12 +38,6 @@ const NextClientClerkProvider = (props: NextClerkProviderProps) => { } }, []); - // Avoid rendering nested ClerkProviders by checking for the existence of the ClerkNextOptions context provider - const isNested = Boolean(useClerkNextOptions()); - if (isNested) { - return props.children; - } - useSafeLayoutEffect(() => { window.__unstable__onBeforeSetActive = intent => { /** @@ -112,6 +107,16 @@ export const ClientClerkProvider = (props: NextClerkProviderProps & { disableKey const { children, disableKeyless = false, ...rest } = props; const safePublishableKey = mergeNextClerkPropsWithEnv(rest).publishableKey; + // Avoid rendering nested ClerkProviders by checking for the existence of the ClerkNextOptions context provider + const isNested = Boolean(useClerkNextOptions()); + if (isNested) { + if (rest.initialState) { + // If using inside a , we do want the initial state to be available for this subtree + return {children}; + } + return children; + } + if (safePublishableKey || !canUseKeyless || disableKeyless) { return {children}; } diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index c7ab1ff28e0..81b393983be 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -1,9 +1,7 @@ -import type { InitialState, Without } from '@clerk/shared/types'; +import type { Without } from '@clerk/shared/types'; import { headers } from 'next/headers'; -import type { ReactNode } from 'react'; import React from 'react'; -import { PromisifiedAuthProvider } from '../../client-boundary/PromisifiedAuthProvider'; import { getDynamicAuthData } from '../../server/buildClerkProps'; import type { NextClerkProviderProps } from '../../types'; import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; @@ -32,19 +30,8 @@ export async function ClerkProvider( ) { const { children, dynamic, ...rest } = props; - async function generateStatePromise() { - if (!dynamic) { - return Promise.resolve(null); - } - return getDynamicClerkState(); - } - - async function generateNonce() { - if (!dynamic) { - return Promise.resolve(''); - } - return getNonceHeaders(); - } + const statePromiseOrValue = dynamic ? getDynamicClerkState() : null; + const noncePromiseOrValue = dynamic ? getNonceHeaders() : ''; const propsWithEnvs = mergeNextClerkPropsWithEnv({ ...rest, @@ -52,8 +39,6 @@ export async function ClerkProvider( const { shouldRunAsKeyless, runningWithClaimedKeys } = await getKeylessStatus(propsWithEnvs); - let output: ReactNode; - try { const detectKeylessEnvDrift = await import('../../server/keyless-telemetry.js').then( mod => mod.detectKeylessEnvDrift, @@ -64,35 +49,25 @@ export async function ClerkProvider( } if (shouldRunAsKeyless) { - output = ( + return ( {children} ); - } else { - output = ( - - {children} - - ); } - if (dynamic) { - return ( - // TODO: fix types so AuthObject is compatible with InitialState - }> - {output} - - ); - } - return output; + return ( + + {children} + + ); } diff --git a/packages/nextjs/src/app-router/server/keyless-provider.tsx b/packages/nextjs/src/app-router/server/keyless-provider.tsx index fa16913af25..b559bf20acb 100644 --- a/packages/nextjs/src/app-router/server/keyless-provider.tsx +++ b/packages/nextjs/src/app-router/server/keyless-provider.tsx @@ -35,12 +35,12 @@ export async function getKeylessStatus( type KeylessProviderProps = PropsWithChildren<{ rest: Without; runningWithClaimedKeys: boolean; - generateStatePromise: () => Promise; - generateNonce: () => Promise; + initialState: AuthObject | null; + nonce: string; }>; export const KeylessProvider = async (props: KeylessProviderProps) => { - const { rest, runningWithClaimedKeys, generateNonce, generateStatePromise, children } = props; + const { rest, runningWithClaimedKeys, initialState, nonce, children } = props; // NOTE: Create or read keys on every render. Usually this means only on hard refresh or hard navigations. const newOrReadKeys = await import('../../server/keyless-node.js') @@ -56,8 +56,8 @@ export const KeylessProvider = async (props: KeylessProviderProps) => { return ( {children} @@ -75,8 +75,8 @@ export const KeylessProvider = async (props: KeylessProviderProps) => { // Explicitly use `null` instead of `undefined` here to avoid persisting `deleteKeylessAction` during merging of options. __internal_keyless_dismissPrompt: runningWithClaimedKeys ? deleteKeylessAction : null, })} - nonce={await generateNonce()} - initialState={await generateStatePromise()} + nonce={nonce} + initialState={initialState} > {children} diff --git a/packages/nextjs/src/client-boundary/PromisifiedAuthProvider.tsx b/packages/nextjs/src/client-boundary/PromisifiedAuthProvider.tsx deleted file mode 100644 index dbed9233ca0..00000000000 --- a/packages/nextjs/src/client-boundary/PromisifiedAuthProvider.tsx +++ /dev/null @@ -1,78 +0,0 @@ -'use client'; - -import { useAuth } from '@clerk/react'; -import { useDerivedAuth } from '@clerk/react/internal'; -import type { InitialState } from '@clerk/shared/types'; -import { useRouter } from 'next/compat/router'; -import React from 'react'; - -const PromisifiedAuthContext = React.createContext | InitialState | null>(null); - -export function PromisifiedAuthProvider({ - authPromise, - children, -}: { - authPromise: Promise | InitialState; - children: React.ReactNode; -}) { - return {children}; -} - -/** - * Returns the current auth state, the user and session ids and the `getToken` - * that can be used to retrieve the given template or the default Clerk token. - * - * Until Clerk loads, `isLoaded` will be set to `false`. - * Once Clerk loads, `isLoaded` will be set to `true`, and you can - * safely access the `userId` and `sessionId` variables. - * - * For projects using NextJs or Remix, you can have immediate access to this data during SSR - * simply by using the `ClerkProvider`. - * - * @example - * import { useAuth } from '@clerk/nextjs' - * - * function Hello() { - * const { isSignedIn, sessionId, userId } = useAuth(); - * if(isSignedIn) { - * return null; - * } - * console.log(sessionId, userId) - * return
...
- * } - * - * @example - * This page will be fully rendered during SSR. - * - * ```tsx - * import { useAuth } from '@clerk/nextjs' - * - * export HelloPage = () => { - * const { isSignedIn, sessionId, userId } = useAuth(); - * console.log(isSignedIn, sessionId, userId) - * return
...
- * } - * ``` - */ -export function usePromisifiedAuth(options: Parameters[0] = {}) { - const isPagesRouter = useRouter(); - const valueFromContext = React.useContext(PromisifiedAuthContext); - - let resolvedData = valueFromContext; - if (valueFromContext && 'then' in valueFromContext) { - resolvedData = React.use(valueFromContext); - } - - // At this point we should have a usable auth object - if (typeof window === 'undefined') { - // Pages router should always use useAuth as it is able to grab initial auth state from context during SSR. - if (isPagesRouter) { - return useAuth(options); - } - - // We don't need to deal with Clerk being loaded here - return useDerivedAuth({ ...resolvedData, ...options }); - } else { - return useAuth({ ...resolvedData, ...options }); - } -} diff --git a/packages/nextjs/src/client-boundary/hooks.ts b/packages/nextjs/src/client-boundary/hooks.ts index 168c2bbba01..465d013c1c3 100644 --- a/packages/nextjs/src/client-boundary/hooks.ts +++ b/packages/nextjs/src/client-boundary/hooks.ts @@ -1,6 +1,7 @@ 'use client'; export { + useAuth, useClerk, useEmailLink, useOrganization, @@ -23,5 +24,3 @@ export { EmailLinkErrorCode, EmailLinkErrorCodeStatus, } from '@clerk/react/errors'; - -export { usePromisifiedAuth as useAuth } from './PromisifiedAuthProvider'; diff --git a/packages/react/src/contexts/AuthContext.ts b/packages/react/src/contexts/AuthContext.ts deleted file mode 100644 index 0391e2e4a74..00000000000 --- a/packages/react/src/contexts/AuthContext.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { createContextAndHook } from '@clerk/shared/react'; -import type { - ActClaim, - JwtPayload, - OrganizationCustomPermissionKey, - OrganizationCustomRoleKey, - SessionStatusClaim, -} from '@clerk/shared/types'; - -export type AuthContextValue = { - userId: string | null | undefined; - sessionId: string | null | undefined; - sessionStatus: SessionStatusClaim | null | undefined; - sessionClaims: JwtPayload | null | undefined; - actor: ActClaim | null | undefined; - orgId: string | null | undefined; - orgRole: OrganizationCustomRoleKey | null | undefined; - orgSlug: string | null | undefined; - orgPermissions: OrganizationCustomPermissionKey[] | null | undefined; - factorVerificationAge: [number, number] | null; -}; - -export const [AuthContext, useAuthContext] = createContextAndHook('AuthContext'); diff --git a/packages/react/src/contexts/AuthContext.tsx b/packages/react/src/contexts/AuthContext.tsx new file mode 100644 index 00000000000..321c91a71eb --- /dev/null +++ b/packages/react/src/contexts/AuthContext.tsx @@ -0,0 +1,84 @@ +import type { DeriveStateReturnType } from '@clerk/shared/deriveState'; +import { deriveFromClientSideState, deriveFromSsrInitialState } from '@clerk/shared/deriveState'; +import { createContextAndHook } from '@clerk/shared/react'; +import type { + ActClaim, + InitialState, + JwtPayload, + OrganizationCustomPermissionKey, + OrganizationCustomRoleKey, + Resources, + SessionStatusClaim, +} from '@clerk/shared/types'; +import React from 'react'; + +export type AuthContextValue = { + userId: string | null | undefined; + sessionId: string | null | undefined; + sessionStatus: SessionStatusClaim | null | undefined; + sessionClaims: JwtPayload | null | undefined; + actor: ActClaim | null | undefined; + orgId: string | null | undefined; + orgRole: OrganizationCustomRoleKey | null | undefined; + orgSlug: string | null | undefined; + orgPermissions: OrganizationCustomPermissionKey[] | null | undefined; + factorVerificationAge: [number, number] | null; +}; + +export const [InitialAuthContext, useInitialAuthContext] = createContextAndHook( + 'InitialAuthContext', +); +export function InitialAuthStateProvider(props: { children: React.ReactNode; initialState: InitialState | undefined }) { + const initialAuthStateCtxValue = useDeriveAuthContext( + props.initialState ? deriveFromSsrInitialState(props.initialState) : undefined, + ); + return {props.children}; +} + +export const [AuthContext, useAuthContext] = createContextAndHook('AuthContext'); +export function AuthStateProvider(props: { children: React.ReactNode; state: Resources }) { + const authStateCtxValue = useDeriveAuthContext(deriveFromClientSideState(props.state)); + return {props.children}; +} + +const emptyAuthCtx = { value: undefined }; +// We want the types to be: +// Pass in value known not to be undefined -> { value: AuthContextValue } +// Pass in value that might be undefined -> { value: AuthContextValue | undefined } +type DerivedAuthContextValue = { value: T extends undefined ? undefined : AuthContextValue }; + +// Narrow full state to only what we need for the AuthContextValue +function useDeriveAuthContext(fullState: T): DerivedAuthContextValue { + const fullReturn = React.useMemo(() => { + const value = { + sessionId: fullState?.sessionId, + sessionStatus: fullState?.sessionStatus, + sessionClaims: fullState?.sessionClaims, + userId: fullState?.userId, + actor: fullState?.actor, + orgId: fullState?.orgId, + orgRole: fullState?.orgRole, + orgSlug: fullState?.orgSlug, + orgPermissions: fullState?.orgPermissions, + factorVerificationAge: fullState?.factorVerificationAge, + }; + return { value }; + }, [ + fullState?.sessionId, + fullState?.sessionStatus, + fullState?.sessionClaims, + fullState?.userId, + fullState?.actor, + fullState?.orgId, + fullState?.orgRole, + fullState?.orgSlug, + fullState?.orgPermissions, + fullState?.factorVerificationAge, + ]); + + if (fullState === undefined) { + return emptyAuthCtx as DerivedAuthContextValue; + } + + return fullReturn as DerivedAuthContextValue; +} diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx index 09f2ce7eb04..284934a7425 100644 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ b/packages/react/src/contexts/ClerkContextProvider.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { IsomorphicClerk } from '../isomorphicClerk'; import type { IsomorphicClerkOptions } from '../types'; -import { AuthContext } from './AuthContext'; +import { AuthStateProvider, InitialAuthStateProvider } from './AuthContext'; import { IsomorphicClerkContext } from './IsomorphicClerkContext'; type ClerkContextProvider = { @@ -37,7 +37,6 @@ export function ClerkContextProvider(props: ClerkContextProvider) { return clerk.addListener(e => setState({ ...e })); }, []); - const derivedState = deriveState(clerk.loaded, state, initialState); const clerkCtx = React.useMemo( () => ({ value: clerk }), [ @@ -47,37 +46,8 @@ export function ClerkContextProvider(props: ClerkContextProvider) { ); const clientCtx = React.useMemo(() => ({ value: state.client }), [state.client]); - const { - sessionId, - sessionStatus, - sessionClaims, - session, - userId, - user, - orgId, - actor, - organization, - orgRole, - orgSlug, - orgPermissions, - factorVerificationAge, - } = derivedState; - - const authCtx = React.useMemo(() => { - const value = { - sessionId, - sessionStatus, - sessionClaims, - userId, - actor, - orgId, - orgRole, - orgSlug, - orgPermissions, - factorVerificationAge, - }; - return { value }; - }, [sessionId, sessionStatus, userId, actor, orgId, orgRole, orgSlug, factorVerificationAge, sessionClaims?.__raw]); + const resolvedState = deriveState(clerk.loaded, state, initialState); + const { sessionId, session, userId, user, orgId, organization } = resolvedState; const sessionCtx = React.useMemo(() => ({ value: session }), [sessionId, session]); const userCtx = React.useMemo(() => ({ value: user }), [userId, user]); @@ -94,16 +64,18 @@ export function ClerkContextProvider(props: ClerkContextProvider) { - - - - {children} - - - + + + + + {children} + + + + diff --git a/packages/react/src/hooks/__tests__/useAuth.test.tsx b/packages/react/src/hooks/__tests__/useAuth.test.tsx index fcffe1bdc17..2e1d9af4a10 100644 --- a/packages/react/src/hooks/__tests__/useAuth.test.tsx +++ b/packages/react/src/hooks/__tests__/useAuth.test.tsx @@ -5,7 +5,7 @@ import { render, renderHook } from '@testing-library/react'; import React from 'react'; import { afterAll, beforeAll, beforeEach, describe, expect, expectTypeOf, it, test, vi } from 'vitest'; -import { AuthContext } from '../../contexts/AuthContext'; +import { AuthContext, InitialAuthContext } from '../../contexts/AuthContext'; import { errorThrower } from '../../errors/errorThrower'; import { invalidStateError } from '../../errors/messages'; import { useAuth, useDerivedAuth } from '../useAuth'; @@ -70,9 +70,11 @@ describe('useAuth', () => { expect(() => { render( - - - + + + + + , ); }).not.toThrow(); diff --git a/packages/react/src/hooks/__tests__/useAuth.type.test.ts b/packages/react/src/hooks/__tests__/useAuth.type.test.ts index 34ae3a05176..280efed08b5 100644 --- a/packages/react/src/hooks/__tests__/useAuth.type.test.ts +++ b/packages/react/src/hooks/__tests__/useAuth.type.test.ts @@ -1,9 +1,7 @@ -import type { PendingSessionOptions } from '@clerk/shared/types'; import { describe, expectTypeOf, it } from 'vitest'; import type { useAuth } from '../useAuth'; -type UseAuthParameters = Parameters[0]; type HasFunction = Exclude['has'], undefined>; type ParamsOfHas = Parameters[0]; @@ -145,18 +143,4 @@ describe('useAuth type tests', () => { } as const).not.toMatchTypeOf(); }); }); - - describe('with parameters', () => { - it('allows passing any auth state object', () => { - expectTypeOf({ orgId: null }).toMatchTypeOf(); - }); - - it('do not allow invalid option types', () => { - const invalidValue = 5; - expectTypeOf({ treatPendingAsSignedOut: invalidValue } satisfies Record< - keyof PendingSessionOptions, - any - >).toMatchTypeOf(); - }); - }); }); diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index 2f002127697..72580b21a6e 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -10,7 +10,7 @@ import type { } from '@clerk/shared/types'; import { useCallback } from 'react'; -import { useAuthContext } from '../contexts/AuthContext'; +import { useAuthContext, useInitialAuthContext } from '../contexts/AuthContext'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; import { errorThrower } from '../errors/errorThrower'; import { invalidStateError } from '../errors/messages'; @@ -20,7 +20,7 @@ import { createGetToken, createSignOut } from './utils'; /** * @inline */ -type UseAuthOptions = Record | PendingSessionOptions | undefined | null; +type UseAuthOptions = PendingSessionOptions | undefined | null; /** * The `useAuth()` hook provides access to the current user's authentication state and methods to manage the active session. @@ -35,7 +35,7 @@ type UseAuthOptions = Record | PendingSessionOptions | undefined | * @unionReturnHeadings * ["Initialization", "Signed out", "Signed in (no active organization)", "Signed in (with active organization)"] * - * @param [initialAuthStateOrOptions] - An object containing the initial authentication state or options for the `useAuth()` hook. If not provided, the hook will attempt to derive the state from the context. `treatPendingAsSignedOut` is a boolean that indicates whether pending sessions are considered as signed out or not. Defaults to `true`. + * @param [options] - An object containing options for the `useAuth()` hook. `treatPendingAsSignedOut` is a boolean that indicates whether pending sessions are considered as signed out or not. Defaults to `true`. * * @function * @@ -92,18 +92,15 @@ type UseAuthOptions = Record | PendingSessionOptions | undefined | * * */ -export const useAuth = (initialAuthStateOrOptions: UseAuthOptions = {}): UseAuthReturn => { +export const useAuth = (options: UseAuthOptions = {}): UseAuthReturn => { useAssertWrappedByClerkProvider('useAuth'); - const { treatPendingAsSignedOut, ...rest } = initialAuthStateOrOptions ?? {}; - const initialAuthState = rest as any; + const { treatPendingAsSignedOut } = options ?? {}; + const clerk = useIsomorphicClerkContext(); + const initialAuthContext = useInitialAuthContext(); + const authContext = useAuthContext(); - const authContextFromHook = useAuthContext(); - let authContext = authContextFromHook; - - if (authContext.sessionId === undefined && authContext.userId === undefined) { - authContext = initialAuthState != null ? initialAuthState : {}; - } + const resolvedAuthContext = !clerk.loaded && initialAuthContext !== undefined ? initialAuthContext : authContext; const isomorphicClerk = useIsomorphicClerkContext(); const getToken: GetToken = useCallback(createGetToken(isomorphicClerk), [isomorphicClerk]); @@ -113,7 +110,7 @@ export const useAuth = (initialAuthStateOrOptions: UseAuthOptions = {}): UseAuth return useDerivedAuth( { - ...authContext, + ...resolvedAuthContext, getToken, signOut, }, diff --git a/packages/react/src/internal.ts b/packages/react/src/internal.ts index 4fa353724b6..62307ba0b41 100644 --- a/packages/react/src/internal.ts +++ b/packages/react/src/internal.ts @@ -10,3 +10,5 @@ export { buildClerkUiScriptAttributes, setClerkJsLoadingErrorPackageName, } from '@clerk/shared/loadClerkJsScript'; + +export { InitialAuthStateProvider } from './contexts/AuthContext'; diff --git a/packages/shared/src/deriveState.ts b/packages/shared/src/deriveState.ts index 7b84fb42d2c..5994af6726f 100644 --- a/packages/shared/src/deriveState.ts +++ b/packages/shared/src/deriveState.ts @@ -9,17 +9,26 @@ import type { UserResource, } from './types'; +// We use the ReturnType of deriveFromSsrInitialState, which in turn uses the ReturnType of deriveFromClientSideState, +// to ensure these stay in sync without having to manually type them out. +export type DeriveStateReturnType = ReturnType; + /** * Derives authentication state based on the current rendering context (SSR or client-side). */ -export const deriveState = (clerkOperational: boolean, state: Resources, initialState: InitialState | undefined) => { +export const deriveState = ( + clerkOperational: boolean, + state: Resources, + initialState: InitialState | undefined, +): DeriveStateReturnType => { if (!clerkOperational && initialState) { return deriveFromSsrInitialState(initialState); } return deriveFromClientSideState(state); }; -const deriveFromSsrInitialState = (initialState: InitialState) => { +// We use the ReturnType of deriveFromClientSideState to ensure these stay in sync +export const deriveFromSsrInitialState = (initialState: InitialState): ReturnType => { const userId = initialState.userId; const user = initialState.user as UserResource; const sessionId = initialState.sessionId; @@ -51,7 +60,7 @@ const deriveFromSsrInitialState = (initialState: InitialState) => { }; }; -const deriveFromClientSideState = (state: Resources) => { +export const deriveFromClientSideState = (state: Resources) => { const userId: string | null | undefined = state.user ? state.user.id : state.user; const user = state.user; const sessionId: string | null | undefined = state.session ? state.session.id : state.session;