diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 63c7e8690e6..4cdc88edff9 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -80,6 +80,7 @@ import type { InstanceType, JoinWaitlistParams, ListenerCallback, + ListenerOptions, NavigateOptions, OrganizationListProps, OrganizationProfileProps, @@ -1476,11 +1477,11 @@ export class Clerk implements ClerkInterface { } }; - public addListener = (listener: ListenerCallback): UnsubscribeCallback => { + public addListener = (listener: ListenerCallback, options?: ListenerOptions): UnsubscribeCallback => { listener = memoizeListenerCallback(listener); this.#listeners.push(listener); // emit right away - if (this.client) { + if (this.client && !options?.skipInitialEmit) { listener({ client: this.client, session: this.session, 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..55da31e967b 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 { InitialStateProvider } from '@clerk/shared/react'; 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..f40061cce4a 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 { 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,28 +30,17 @@ 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() : undefined; + const noncePromiseOrValue = dynamic ? getNonceHeaders() : ''; const propsWithEnvs = mergeNextClerkPropsWithEnv({ ...rest, + initialState: statePromiseOrValue as InitialState | Promise | undefined, + nonce: await noncePromiseOrValue, }); const { shouldRunAsKeyless, runningWithClaimedKeys } = await getKeylessStatus(propsWithEnvs); - let output: ReactNode; - try { const detectKeylessEnvDrift = await import('../../server/keyless-telemetry.js').then( mod => mod.detectKeylessEnvDrift, @@ -64,35 +51,15 @@ 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..d7ec0d51d0c 100644 --- a/packages/nextjs/src/app-router/server/keyless-provider.tsx +++ b/packages/nextjs/src/app-router/server/keyless-provider.tsx @@ -1,4 +1,3 @@ -import type { AuthObject } from '@clerk/backend'; import type { Without } from '@clerk/shared/types'; import { headers } from 'next/headers'; import type { PropsWithChildren } from 'react'; @@ -35,12 +34,10 @@ export async function getKeylessStatus( type KeylessProviderProps = PropsWithChildren<{ rest: Without; runningWithClaimedKeys: boolean; - generateStatePromise: () => Promise; - generateNonce: () => Promise; }>; export const KeylessProvider = async (props: KeylessProviderProps) => { - const { rest, runningWithClaimedKeys, generateNonce, generateStatePromise, children } = props; + const { rest, runningWithClaimedKeys, 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 +53,6 @@ export const KeylessProvider = async (props: KeylessProviderProps) => { return ( {children} @@ -75,8 +70,6 @@ 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()} > {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..ac26fb7db9d --- /dev/null +++ b/packages/react/src/contexts/AuthContext.tsx @@ -0,0 +1,92 @@ +import type { DeriveStateReturnType } from '@clerk/shared/deriveState'; +import { deriveFromClientSideState, deriveFromSsrInitialState } from '@clerk/shared/deriveState'; +import { useClerkInstanceContext, useInitialStateContext } from '@clerk/shared/react'; +import type { + ActClaim, + ClientResource, + JwtPayload, + OrganizationCustomPermissionKey, + OrganizationCustomRoleKey, + SessionStatusClaim, +} from '@clerk/shared/types'; +import { useCallback, useDeferredValue, useMemo, useSyncExternalStore } from 'react'; + +type AuthStateValue = { + 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 defaultDerivedInitialState = { + actor: undefined, + factorVerificationAge: null, + orgId: undefined, + orgPermissions: undefined, + orgRole: undefined, + orgSlug: undefined, + sessionClaims: undefined, + sessionId: undefined, + sessionStatus: undefined, + userId: undefined, +}; + +export function useAuthState(): AuthStateValue { + const clerk = useClerkInstanceContext(); + const initialStateContext = useInitialStateContext(); + // If we make initialState support a promise in the future, this is where we would use() that promise + const initialSnapshot = useMemo(() => { + if (!initialStateContext) { + return defaultDerivedInitialState; + } + const fullState = deriveFromSsrInitialState(initialStateContext); + return authStateFromFull(fullState); + }, [initialStateContext]); + + const snapshot = useMemo(() => { + if (!clerk.loaded) { + return initialSnapshot; + } + const state = { + client: clerk.client as ClientResource, + session: clerk.session, + user: clerk.user, + organization: clerk.organization, + }; + const fullState = deriveFromClientSideState(state); + return authStateFromFull(fullState); + }, [clerk.client, clerk.session, clerk.user, clerk.organization, initialSnapshot, clerk.loaded]); + + const authState = useSyncExternalStore( + useCallback(callback => clerk.addListener(callback, { skipInitialEmit: true }), [clerk]), + useCallback(() => snapshot, [snapshot]), + useCallback(() => initialSnapshot, [initialSnapshot]), + ); + + // If an updates comes in during a transition, uSES usually deopts that transition to be synchronous, + // which for example means that already mounted boundaries might suddenly show their fallback. + // This makes all auth state changes into transitions, but does not deopt to be synchronous. If it's + // called during a transition, it immediately uses the new value without deferring. + return useDeferredValue(authState); +} + +function authStateFromFull(derivedState: DeriveStateReturnType) { + return { + sessionId: derivedState.sessionId, + sessionStatus: derivedState.sessionStatus, + sessionClaims: derivedState.sessionClaims, + userId: derivedState.userId, + actor: derivedState.actor, + orgId: derivedState.orgId, + orgRole: derivedState.orgRole, + orgSlug: derivedState.orgSlug, + orgPermissions: derivedState.orgPermissions, + factorVerificationAge: derivedState.factorVerificationAge, + }; +} diff --git a/packages/react/src/contexts/ClerkContextProvider.tsx b/packages/react/src/contexts/ClerkContextProvider.tsx deleted file mode 100644 index 09f2ce7eb04..00000000000 --- a/packages/react/src/contexts/ClerkContextProvider.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import { deriveState } from '@clerk/shared/deriveState'; -import { - __experimental_CheckoutProvider as CheckoutProvider, - ClientContext, - OrganizationProvider, - SessionContext, - UserContext, -} from '@clerk/shared/react'; -import type { ClientResource, InitialState, Resources } from '@clerk/shared/types'; -import React from 'react'; - -import { IsomorphicClerk } from '../isomorphicClerk'; -import type { IsomorphicClerkOptions } from '../types'; -import { AuthContext } from './AuthContext'; -import { IsomorphicClerkContext } from './IsomorphicClerkContext'; - -type ClerkContextProvider = { - isomorphicClerkOptions: IsomorphicClerkOptions; - initialState: InitialState | undefined; - children: React.ReactNode; -}; - -export type ClerkContextProviderState = Resources; - -export function ClerkContextProvider(props: ClerkContextProvider) { - const { isomorphicClerkOptions, initialState, children } = props; - const { isomorphicClerk: clerk, clerkStatus } = useLoadedIsomorphicClerk(isomorphicClerkOptions); - - const [state, setState] = React.useState({ - client: clerk.client as ClientResource, - session: clerk.session, - user: clerk.user, - organization: clerk.organization, - }); - - React.useEffect(() => { - return clerk.addListener(e => setState({ ...e })); - }, []); - - const derivedState = deriveState(clerk.loaded, state, initialState); - const clerkCtx = React.useMemo( - () => ({ value: clerk }), - [ - // Only update the clerk reference on status change - clerkStatus, - ], - ); - 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 sessionCtx = React.useMemo(() => ({ value: session }), [sessionId, session]); - const userCtx = React.useMemo(() => ({ value: user }), [userId, user]); - const organizationCtx = React.useMemo(() => { - const value = { - organization: organization, - }; - return { value }; - }, [orgId, organization]); - - return ( - // @ts-expect-error value passed is of type IsomorphicClerk where the context expects LoadedClerk - - - - - - - - {children} - - - - - - - - ); -} - -const useLoadedIsomorphicClerk = (options: IsomorphicClerkOptions) => { - const isomorphicClerkRef = React.useRef(IsomorphicClerk.getOrCreateInstance(options)); - const [clerkStatus, setClerkStatus] = React.useState(isomorphicClerkRef.current.status); - - React.useEffect(() => { - void isomorphicClerkRef.current.__unstable__updateProps({ appearance: options.appearance }); - }, [options.appearance]); - - React.useEffect(() => { - void isomorphicClerkRef.current.__unstable__updateProps({ options }); - }, [options.localization]); - - React.useEffect(() => { - isomorphicClerkRef.current.on('status', setClerkStatus); - return () => { - if (isomorphicClerkRef.current) { - isomorphicClerkRef.current.off('status', setClerkStatus); - } - IsomorphicClerk.clearInstance(); - }; - }, []); - - return { isomorphicClerk: isomorphicClerkRef.current, clerkStatus }; -}; diff --git a/packages/react/src/contexts/ClerkProvider.tsx b/packages/react/src/contexts/ClerkProvider.tsx index 66b21ed8a35..f7ff2c9a3d3 100644 --- a/packages/react/src/contexts/ClerkProvider.tsx +++ b/packages/react/src/contexts/ClerkProvider.tsx @@ -1,11 +1,12 @@ import { isPublishableKey } from '@clerk/shared/keys'; +import { ClerkContextProvider } from '@clerk/shared/react'; import React from 'react'; import { errorThrower } from '../errors/errorThrower'; import { multipleClerkProvidersError } from '../errors/messages'; -import type { ClerkProviderProps } from '../types'; +import { IsomorphicClerk } from '../isomorphicClerk'; +import type { ClerkProviderProps, IsomorphicClerkOptions } from '../types'; import { withMaxAllowedInstancesGuard } from '../utils'; -import { ClerkContextProvider } from './ClerkContextProvider'; function ClerkProviderBase(props: ClerkProviderProps) { const { initialState, children, __internal_bypassMissingPublishableKey, ...restIsomorphicClerkOptions } = props; @@ -19,10 +20,14 @@ function ClerkProviderBase(props: ClerkProviderProps) { } } + const { isomorphicClerk, clerkStatus } = useLoadedIsomorphicClerk(restIsomorphicClerkOptions); + return ( {children} @@ -34,3 +39,28 @@ const ClerkProvider = withMaxAllowedInstancesGuard(ClerkProviderBase, 'ClerkProv ClerkProvider.displayName = 'ClerkProvider'; export { ClerkProvider }; + +const useLoadedIsomorphicClerk = (options: IsomorphicClerkOptions) => { + const isomorphicClerkRef = React.useRef(IsomorphicClerk.getOrCreateInstance(options)); + const [clerkStatus, setClerkStatus] = React.useState(isomorphicClerkRef.current.status); + + React.useEffect(() => { + void isomorphicClerkRef.current.__unstable__updateProps({ appearance: options.appearance }); + }, [options.appearance]); + + React.useEffect(() => { + void isomorphicClerkRef.current.__unstable__updateProps({ options }); + }, [options.localization]); + + React.useEffect(() => { + isomorphicClerkRef.current.on('status', setClerkStatus); + return () => { + if (isomorphicClerkRef.current) { + isomorphicClerkRef.current.off('status', setClerkStatus); + } + IsomorphicClerk.clearInstance(); + }; + }, []); + + return { isomorphicClerk: isomorphicClerkRef.current, clerkStatus }; +}; diff --git a/packages/react/src/contexts/IsomorphicClerkContext.tsx b/packages/react/src/contexts/IsomorphicClerkContext.tsx index 765326db501..7cd10217707 100644 --- a/packages/react/src/contexts/IsomorphicClerkContext.tsx +++ b/packages/react/src/contexts/IsomorphicClerkContext.tsx @@ -1,6 +1,5 @@ -import { ClerkInstanceContext, useClerkInstanceContext } from '@clerk/shared/react'; +import { useClerkInstanceContext } from '@clerk/shared/react'; import type { IsomorphicClerk } from '../isomorphicClerk'; -export const IsomorphicClerkContext = ClerkInstanceContext; export const useIsomorphicClerkContext = useClerkInstanceContext as unknown as () => IsomorphicClerk; diff --git a/packages/react/src/contexts/OrganizationContext.tsx b/packages/react/src/contexts/OrganizationContext.tsx index 099dc09105a..2661fbdfc28 100644 --- a/packages/react/src/contexts/OrganizationContext.tsx +++ b/packages/react/src/contexts/OrganizationContext.tsx @@ -1 +1 @@ -export { OrganizationProvider, useOrganizationContext } from '@clerk/shared/react'; +export { useOrganizationContext } from '@clerk/shared/react'; diff --git a/packages/react/src/contexts/SessionContext.tsx b/packages/react/src/contexts/SessionContext.tsx index 4de21025933..c4b5c1d1bd3 100644 --- a/packages/react/src/contexts/SessionContext.tsx +++ b/packages/react/src/contexts/SessionContext.tsx @@ -1 +1 @@ -export { SessionContext, useSessionContext } from '@clerk/shared/react'; +export { useSessionContext } from '@clerk/shared/react'; diff --git a/packages/react/src/contexts/UserContext.tsx b/packages/react/src/contexts/UserContext.tsx index c5ef71321e0..24c6fb4ab94 100644 --- a/packages/react/src/contexts/UserContext.tsx +++ b/packages/react/src/contexts/UserContext.tsx @@ -1 +1 @@ -export { UserContext, useUserContext } from '@clerk/shared/react'; +export { useUserContext } from '@clerk/shared/react'; 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..be1096d1330 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -9,8 +9,8 @@ import type { UseAuthReturn, } from '@clerk/shared/types'; import { useCallback } from 'react'; +import { useAuthState } from 'src/contexts/AuthContext'; -import { useAuthContext } 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,11 @@ 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 authContextFromHook = useAuthContext(); - let authContext = authContextFromHook; - - if (authContext.sessionId === undefined && authContext.userId === undefined) { - authContext = initialAuthState != null ? initialAuthState : {}; - } + const { treatPendingAsSignedOut } = options ?? {}; + const authState = useAuthState(); const isomorphicClerk = useIsomorphicClerkContext(); const getToken: GetToken = useCallback(createGetToken(isomorphicClerk), [isomorphicClerk]); @@ -113,7 +106,7 @@ export const useAuth = (initialAuthStateOrOptions: UseAuthOptions = {}): UseAuth return useDerivedAuth( { - ...authContext, + ...authState, getToken, signOut, }, diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index e868f13de9d..ce00d0f3908 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -29,6 +29,7 @@ import type { HandleOAuthCallbackParams, JoinWaitlistParams, ListenerCallback, + ListenerOptions, LoadedClerk, OrganizationListProps, OrganizationProfileProps, @@ -153,8 +154,11 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { private premountAddListenerCalls = new Map< ListenerCallback, { - unsubscribe: UnsubscribeCallback; - nativeUnsubscribe?: UnsubscribeCallback; + options?: ListenerOptions; + handlers: { + unsubscribe: UnsubscribeCallback; + nativeUnsubscribe?: UnsubscribeCallback; + }; } >(); private loadedListeners: Array<() => void> = []; @@ -575,8 +579,8 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { this.clerkjs = clerkjs; this.premountMethodCalls.forEach(cb => cb()); - this.premountAddListenerCalls.forEach((listenerHandlers, listener) => { - listenerHandlers.nativeUnsubscribe = clerkjs.addListener(listener); + this.premountAddListenerCalls.forEach((listenerExtras, listener) => { + listenerExtras.handlers.nativeUnsubscribe = clerkjs.addListener(listener, listenerExtras.options); }); this.#eventBus.internal.retrieveListeners('status')?.forEach(listener => { @@ -1207,18 +1211,18 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; - addListener = (listener: ListenerCallback): UnsubscribeCallback => { + addListener = (listener: ListenerCallback, options?: ListenerOptions): UnsubscribeCallback => { if (this.clerkjs) { - return this.clerkjs.addListener(listener); + return this.clerkjs.addListener(listener, options); } else { const unsubscribe = () => { - const listenerHandlers = this.premountAddListenerCalls.get(listener); - if (listenerHandlers) { - listenerHandlers.nativeUnsubscribe?.(); + const listenerExtras = this.premountAddListenerCalls.get(listener); + if (listenerExtras?.handlers) { + listenerExtras?.handlers.nativeUnsubscribe?.(); this.premountAddListenerCalls.delete(listener); } }; - this.premountAddListenerCalls.set(listener, { unsubscribe, nativeUnsubscribe: undefined }); + this.premountAddListenerCalls.set(listener, { options, handlers: { unsubscribe, nativeUnsubscribe: undefined } }); return unsubscribe; } }; diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 2a2dfc939bb..1fd2474cafd 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -40,7 +40,7 @@ export type ClerkProviderProps = IsomorphicClerkOptions & { /** * Provide an initial state of the Clerk client during server-side rendering. You don't need to set this value yourself unless you're [developing an SDK](https://clerk.com/docs/guides/development/sdk-development/overview). */ - initialState?: InitialState; + initialState?: InitialState | Promise; /** * Indicates to silently fail the initialization process when the publishable keys is not provided, instead of throwing an error. * @default false 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; diff --git a/packages/shared/src/react/ClerkContextProvider.tsx b/packages/shared/src/react/ClerkContextProvider.tsx new file mode 100644 index 00000000000..a4dc0871d2d --- /dev/null +++ b/packages/shared/src/react/ClerkContextProvider.tsx @@ -0,0 +1,47 @@ +import React from 'react'; + +import type { Clerk, ClerkStatus, InitialState, LoadedClerk } from '../types'; +import { + __experimental_CheckoutProvider as CheckoutProvider, + ClerkInstanceContext, + InitialStateProvider, +} from './contexts'; +import { SWRConfigCompat } from './providers/SWRConfigCompat'; +import { assertClerkSingletonExists } from './utils'; + +type ClerkContextProps = { + clerk: Clerk; + clerkStatus?: ClerkStatus; + children: React.ReactNode; + swrConfig?: any; + initialState?: InitialState | Promise; +}; + +export function ClerkContextProvider(props: ClerkContextProps): JSX.Element | null { + const clerk = props.clerk as LoadedClerk; + + assertClerkSingletonExists(clerk); + + const clerkCtx = React.useMemo( + () => ({ value: clerk }), + // clerkStatus is a way to control the referential integrity of the clerk object from the outside, + // we only change the context value when the status changes. Since clerk is mutable, any read from + // the object will always be the latest value anyway. + [props.clerkStatus], + ); + + return ( + + + + + {props.children} + + + + + ); +} diff --git a/packages/shared/src/react/contexts.tsx b/packages/shared/src/react/contexts.tsx index 8e9e11a75c8..a32345355dc 100644 --- a/packages/shared/src/react/contexts.tsx +++ b/packages/shared/src/react/contexts.tsx @@ -6,22 +6,47 @@ import React from 'react'; import type { BillingSubscriptionPlanPeriod, ClerkOptions, - ClientResource, ForPayerType, + InitialState, LoadedClerk, OrganizationResource, - SignedInSessionResource, - UserResource, } from '../types'; +import { useOrganizationBase } from './hooks/base/useOrganizationBase'; import { createContextAndHook } from './hooks/createContextAndHook'; -import { SWRConfigCompat } from './providers/SWRConfigCompat'; const [ClerkInstanceContext, useClerkInstanceContext] = createContextAndHook('ClerkInstanceContext'); -const [UserContext, useUserContext] = createContextAndHook('UserContext'); -const [ClientContext, useClientContext] = createContextAndHook('ClientContext'); -const [SessionContext, useSessionContext] = createContextAndHook( - 'SessionContext', -); +export { useUserBase as useUserContext } from './hooks/base/useUserBase'; +export { useClientBase as useClientContext } from './hooks/base/useClientBase'; +export { useSessionBase as useSessionContext } from './hooks/base/useSessionBase'; + +const [InitialStateContext, _useInitialStateContext] = createContextAndHook< + InitialState | Promise | undefined +>('InitialStateContext'); +export { useInitialStateContext }; +export function InitialStateProvider({ + children, + initialState, +}: { + children: React.ReactNode; + initialState: InitialState | Promise | undefined; +}) { + const initialStateCtx = React.useMemo(() => ({ value: initialState }), [initialState]); + return {children}; +} +function useInitialStateContext(): InitialState | undefined { + const initialState = _useInitialStateContext(); + + if (!initialState) { + return undefined; + } + + if (initialState && 'then' in initialState) { + // TODO: If we want to preserve backwards compatibility, we'd need to throw here instead + // @ts-expect-error See above + return React.use(initialState); + } + return initialState; +} const OptionsContext = React.createContext({}); @@ -62,35 +87,10 @@ function useOptionsContext(): ClerkOptions { return context; } -type OrganizationContextProps = { - organization: OrganizationResource | null | undefined; -}; -const [OrganizationContextInternal, useOrganizationContext] = createContextAndHook<{ - organization: OrganizationResource | null | undefined; -}>('OrganizationContext'); - -const OrganizationProvider = ({ - children, - organization, - swrConfig, -}: PropsWithChildren< - OrganizationContextProps & { - // Exporting inferred types directly from SWR will result in error while building declarations - swrConfig?: any; - } ->) => { - return ( - - - {children} - - - ); -}; +function useOrganizationContext(): { organization: OrganizationResource | null | undefined } { + const organization = useOrganizationBase(); + return React.useMemo(() => ({ organization }), [organization]); +} /** * @internal @@ -119,17 +119,10 @@ Learn more: https://clerk.com/docs/components/clerk-provider`.trim(), export { __experimental_CheckoutProvider, ClerkInstanceContext, - ClientContext, OptionsContext, - OrganizationProvider, - SessionContext, useAssertWrappedByClerkProvider, useCheckoutContext, useClerkInstanceContext, - useClientContext, useOptionsContext, useOrganizationContext, - UserContext, - useSessionContext, - useUserContext, }; diff --git a/packages/shared/src/react/hooks/base/useClientBase.ts b/packages/shared/src/react/hooks/base/useClientBase.ts new file mode 100644 index 00000000000..5a2b3427a71 --- /dev/null +++ b/packages/shared/src/react/hooks/base/useClientBase.ts @@ -0,0 +1,29 @@ +import { useCallback, useDeferredValue, useMemo, useSyncExternalStore } from 'react'; + +import type { ClientResource } from '@/types'; + +import { useClerkInstanceContext } from '../../contexts'; + +const initialSnapshot = undefined; +export function useClientBase(): ClientResource | null | undefined { + const clerk = useClerkInstanceContext(); + + const snapshot = useMemo(() => { + if (!clerk.loaded) { + return initialSnapshot; + } + return clerk.client; + }, [clerk.client, clerk.loaded]); + + const client = useSyncExternalStore( + useCallback(callback => clerk.addListener(callback, { skipInitialEmit: true }), [clerk]), + useCallback(() => snapshot, [snapshot]), + useCallback(() => initialSnapshot, []), + ); + + // If an updates comes in during a transition, uSES usually deopts that transition to be synchronous, + // which for example means that already mounted boundaries might suddenly show their fallback. + // This makes all auth state changes into transitions, but does not deopt to be synchronous. If it's + // called during a transition, it immediately uses the new value without deferring. + return useDeferredValue(client); +} diff --git a/packages/shared/src/react/hooks/base/useOrganizationBase.ts b/packages/shared/src/react/hooks/base/useOrganizationBase.ts new file mode 100644 index 00000000000..04a464b7149 --- /dev/null +++ b/packages/shared/src/react/hooks/base/useOrganizationBase.ts @@ -0,0 +1,36 @@ +import { useCallback, useDeferredValue, useMemo, useSyncExternalStore } from 'react'; + +import type { OrganizationResource } from '@/types'; + +import { useClerkInstanceContext, useInitialStateContext } from '../../contexts'; + +export function useOrganizationBase(): OrganizationResource | null | undefined { + const clerk = useClerkInstanceContext(); + const initialStateContext = useInitialStateContext(); + // If we make initialState support a promise in the future, this is where we would use() that promise + const initialSnapshot = useMemo(() => { + if (!initialStateContext) { + return undefined; + } + return initialStateContext.organization; + }, [initialStateContext]); + + const snapshot = useMemo(() => { + if (!clerk.loaded) { + return initialSnapshot; + } + return clerk.organization; + }, [clerk.organization, initialSnapshot, clerk.loaded]); + + const organization = useSyncExternalStore( + useCallback(callback => clerk.addListener(callback, { skipInitialEmit: true }), [clerk]), + useCallback(() => snapshot, [snapshot]), + useCallback(() => initialSnapshot, [initialSnapshot]), + ); + + // If an updates comes in during a transition, uSES usually deopts that transition to be synchronous, + // which for example means that already mounted boundaries might suddenly show their fallback. + // This makes all auth state changes into transitions, but does not deopt to be synchronous. If it's + // called during a transition, it immediately uses the new value without deferring. + return useDeferredValue(organization); +} diff --git a/packages/shared/src/react/hooks/base/useSessionBase.ts b/packages/shared/src/react/hooks/base/useSessionBase.ts new file mode 100644 index 00000000000..7232573e65a --- /dev/null +++ b/packages/shared/src/react/hooks/base/useSessionBase.ts @@ -0,0 +1,36 @@ +import { useCallback, useDeferredValue, useMemo, useSyncExternalStore } from 'react'; + +import type { SignedInSessionResource } from '@/types'; + +import { useClerkInstanceContext, useInitialStateContext } from '../../contexts'; + +export function useSessionBase(): SignedInSessionResource | null | undefined { + const clerk = useClerkInstanceContext(); + const initialStateContext = useInitialStateContext(); + // If we make initialState support a promise in the future, this is where we would use() that promise + const initialSnapshot = useMemo(() => { + if (!initialStateContext) { + return undefined; + } + return initialStateContext.session as SignedInSessionResource; + }, [initialStateContext]); + + const snapshot = useMemo(() => { + if (!clerk.loaded) { + return initialSnapshot; + } + return clerk.session; + }, [clerk.session, initialSnapshot, clerk.loaded]); + + const session = useSyncExternalStore( + useCallback(callback => clerk.addListener(callback, { skipInitialEmit: true }), [clerk]), + useCallback(() => snapshot, [snapshot]), + useCallback(() => initialSnapshot, [initialSnapshot]), + ); + + // If an updates comes in during a transition, uSES usually deopts that transition to be synchronous, + // which for example means that already mounted boundaries might suddenly show their fallback. + // This makes all auth state changes into transitions, but does not deopt to be synchronous. If it's + // called during a transition, it immediately uses the new value without deferring. + return useDeferredValue(session); +} diff --git a/packages/shared/src/react/hooks/base/useUserBase.ts b/packages/shared/src/react/hooks/base/useUserBase.ts new file mode 100644 index 00000000000..765336257cf --- /dev/null +++ b/packages/shared/src/react/hooks/base/useUserBase.ts @@ -0,0 +1,36 @@ +import { useCallback, useDeferredValue, useMemo, useSyncExternalStore } from 'react'; + +import type { UserResource } from '@/types'; + +import { useClerkInstanceContext, useInitialStateContext } from '../../contexts'; + +export function useUserBase(): UserResource | null | undefined { + const clerk = useClerkInstanceContext(); + const initialStateContext = useInitialStateContext(); + // If we make initialState support a promise in the future, this is where we would use() that promise + const initialSnapshot = useMemo(() => { + if (!initialStateContext) { + return undefined; + } + return initialStateContext.user; + }, [initialStateContext]); + + const snapshot = useMemo(() => { + if (!clerk.loaded) { + return initialSnapshot; + } + return clerk.user; + }, [clerk.user, initialSnapshot, clerk.loaded]); + + const user = useSyncExternalStore( + useCallback(callback => clerk.addListener(callback, { skipInitialEmit: true }), [clerk]), + useCallback(() => snapshot, [snapshot]), + useCallback(() => initialSnapshot, [initialSnapshot]), + ); + + // If an updates comes in during a transition, uSES usually deopts that transition to be synchronous, + // which for example means that already mounted boundaries might suddenly show their fallback. + // This makes all auth state changes into transitions, but does not deopt to be synchronous. If it's + // called during a transition, it immediately uses the new value without deferring. + return useDeferredValue(user); +} diff --git a/packages/shared/src/react/index.ts b/packages/shared/src/react/index.ts index c1f8f761236..a6d84714b00 100644 --- a/packages/shared/src/react/index.ts +++ b/packages/shared/src/react/index.ts @@ -2,19 +2,19 @@ export * from './hooks'; export { ClerkInstanceContext, - ClientContext, OptionsContext, - OrganizationProvider, - SessionContext, useAssertWrappedByClerkProvider, useClerkInstanceContext, useClientContext, useOptionsContext, useOrganizationContext, - UserContext, useSessionContext, useUserContext, __experimental_CheckoutProvider, + InitialStateProvider, + useInitialStateContext, } from './contexts'; export * from './commerce'; + +export { ClerkContextProvider } from './ClerkContextProvider'; diff --git a/packages/shared/src/react/utils.ts b/packages/shared/src/react/utils.ts new file mode 100644 index 00000000000..c404daa0b7b --- /dev/null +++ b/packages/shared/src/react/utils.ts @@ -0,0 +1,8 @@ +import { clerkCoreErrorNoClerkSingleton } from '../internal/clerk-js/errors'; +import type { Clerk } from '../types'; + +export function assertClerkSingletonExists(clerk: Clerk | undefined): asserts clerk is Clerk { + if (!clerk) { + clerkCoreErrorNoClerkSingleton(); + } +} diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index c2f385cef37..99c4092da9f 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -139,6 +139,7 @@ export type SDKMetadata = { }; export type ListenerCallback = (emission: Resources) => void; +export type ListenerOptions = { skipInitialEmit?: boolean }; export type UnsubscribeCallback = () => void; export type BeforeEmitCallback = (session?: SignedInSessionResource | null) => void | Promise; export type SetActiveNavigate = ({ session }: { session: SessionResource }) => void | Promise; @@ -662,7 +663,7 @@ export interface Clerk { * @param callback - Callback function receiving the most updated Clerk resources after a change. * @returns - Unsubscribe callback */ - addListener: (callback: ListenerCallback) => UnsubscribeCallback; + addListener: (callback: ListenerCallback, options?: ListenerOptions) => UnsubscribeCallback; /** * Registers an event handler for a specific Clerk event. diff --git a/packages/ui/src/contexts/CoreClerkContextWrapper.tsx b/packages/ui/src/contexts/CoreClerkContextWrapper.tsx deleted file mode 100644 index 85e451a784d..00000000000 --- a/packages/ui/src/contexts/CoreClerkContextWrapper.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import { - __experimental_CheckoutProvider as CheckoutProvider, - ClerkInstanceContext, - ClientContext, - OrganizationProvider, - SessionContext, - UserContext, -} from '@clerk/shared/react'; -import type { Clerk, LoadedClerk, Resources } from '@clerk/shared/types'; -import React from 'react'; - -import { assertClerkSingletonExists } from './utils'; - -type CoreClerkContextWrapperProps = { - clerk: Clerk; - children: React.ReactNode; - swrConfig?: any; -}; - -type CoreClerkContextProviderState = Resources; - -export function CoreClerkContextWrapper(props: CoreClerkContextWrapperProps): JSX.Element | null { - // TODO: Revise Clerk and LoadedClerk - const clerk = props.clerk as LoadedClerk; - assertClerkSingletonExists(clerk); - - const [state, setState] = React.useState({ - client: clerk.client, - session: clerk.session, - user: clerk.user, - organization: clerk.organization, - }); - - React.useEffect(() => { - return clerk.addListener(e => setState({ ...e })); - }, []); - - const { client, session, user, organization } = state; - const clerkCtx = React.useMemo(() => ({ value: clerk }), []); - const clientCtx = React.useMemo(() => ({ value: client }), [client]); - const sessionCtx = React.useMemo(() => ({ value: session }), [session]); - const userCtx = React.useMemo(() => ({ value: user }), [user]); - const organizationCtx = React.useMemo( - () => ({ - value: { organization: organization }, - }), - [organization], - ); - - return ( - - - - - - - {props.children} - - - - - - - ); -} diff --git a/packages/ui/src/contexts/CoreClientContext.tsx b/packages/ui/src/contexts/CoreClientContext.tsx index 5f22e8bc3a9..aa7979efaae 100644 --- a/packages/ui/src/contexts/CoreClientContext.tsx +++ b/packages/ui/src/contexts/CoreClientContext.tsx @@ -1,14 +1,16 @@ -import { assertContextExists, ClientContext, useClientContext } from '@clerk/shared/react'; +import { assertContextExists, useClientContext } from '@clerk/shared/react'; import type { SignInResource, SignUpResource } from '@clerk/shared/types'; export function useCoreSignIn(): SignInResource { const ctx = useClientContext(); - assertContextExists(ctx, ClientContext); + // TODO: useClientContext doesn't actually rely on a context anymore, so we should update this message + assertContextExists(ctx, 'ClientContext'); return ctx.signIn; } export function useCoreSignUp(): SignUpResource { const ctx = useClientContext(); - assertContextExists(ctx, ClientContext); + // TODO: useClientContext doesn't actually rely on a context anymore, so we should update this message + assertContextExists(ctx, 'ClientContext'); return ctx.signUp; } diff --git a/packages/ui/src/contexts/index.ts b/packages/ui/src/contexts/index.ts index 1ae2a67e02c..82a284bb334 100644 --- a/packages/ui/src/contexts/index.ts +++ b/packages/ui/src/contexts/index.ts @@ -4,5 +4,5 @@ export * from './EnvironmentContext'; export * from './OptionsContext'; export * from './CoreSessionContext'; export * from './CoreClientContext'; -export * from './CoreClerkContextWrapper'; export * from './AcceptedUserInvitations'; +export { ClerkContextProvider } from '@clerk/shared/react'; diff --git a/packages/ui/src/contexts/utils.ts b/packages/ui/src/contexts/utils.ts index 423f5151701..260c8965dbe 100644 --- a/packages/ui/src/contexts/utils.ts +++ b/packages/ui/src/contexts/utils.ts @@ -1,18 +1,8 @@ -import { - clerkCoreErrorContextProviderNotFound, - clerkCoreErrorNoClerkSingleton, -} from '@clerk/shared/internal/clerk-js/errors'; -import type { Clerk } from '@clerk/shared/types'; +import { clerkCoreErrorContextProviderNotFound } from '@clerk/shared/internal/clerk-js/errors'; import { snakeToCamel } from '@clerk/shared/underscore'; import { createDynamicParamParser } from '../utils/dynamicParamParser'; -export function assertClerkSingletonExists(clerk: Clerk | undefined): asserts clerk is Clerk { - if (!clerk) { - clerkCoreErrorNoClerkSingleton(); - } -} - export function assertContextExists(contextVal: unknown, providerName: string): asserts contextVal { if (!contextVal) { clerkCoreErrorContextProviderNotFound(providerName); diff --git a/packages/ui/src/lazyModules/providers.tsx b/packages/ui/src/lazyModules/providers.tsx index 9e2c871f23d..7404246bbd4 100644 --- a/packages/ui/src/lazyModules/providers.tsx +++ b/packages/ui/src/lazyModules/providers.tsx @@ -9,7 +9,7 @@ import type { AvailableComponentCtx } from '../types'; import type { ClerkComponentName } from './components'; import { ClerkComponents } from './components'; -const CoreClerkContextWrapper = lazy(() => import('../contexts').then(m => ({ default: m.CoreClerkContextWrapper }))); +const ClerkContextProvider = lazy(() => import('../contexts').then(m => ({ default: m.ClerkContextProvider }))); const EnvironmentProvider = lazy(() => import('../contexts').then(m => ({ default: m.EnvironmentProvider }))); const OptionsProvider = lazy(() => import('../contexts').then(m => ({ default: m.OptionsProvider }))); const AppearanceProvider = lazy(() => import('../customizables').then(m => ({ default: m.AppearanceProvider }))); @@ -40,11 +40,11 @@ export const LazyProviders = (props: LazyProvidersProps) => { nonce={props.options.nonce} cssLayerName={props.options.appearance?.cssLayerName} > - + {props.children} - + ); };