From fcd7975bf1a74cf25c7cb844fa5baf70fa96973a Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Mon, 21 Oct 2024 13:53:14 -0500 Subject: [PATCH 1/7] chore: Refactor creation of authObject into shared helper --- packages/nextjs/src/server/buildClerkProps.ts | 53 +++++------------- packages/nextjs/src/server/createGetAuth.ts | 50 +++-------------- .../src/server/data/getAuthDataFromRequest.ts | 55 +++++++++++++++++++ packages/nextjs/src/server/utils.ts | 17 +++--- packages/nextjs/src/utils/debugLogger.ts | 3 +- 5 files changed, 86 insertions(+), 92 deletions(-) create mode 100644 packages/nextjs/src/server/data/getAuthDataFromRequest.ts diff --git a/packages/nextjs/src/server/buildClerkProps.ts b/packages/nextjs/src/server/buildClerkProps.ts index f9cb3f41eb6..dd1c9e9b8a3 100644 --- a/packages/nextjs/src/server/buildClerkProps.ts +++ b/packages/nextjs/src/server/buildClerkProps.ts @@ -1,17 +1,8 @@ import type { Organization, Session, User } from '@clerk/backend'; -import { - AuthStatus, - constants, - makeAuthObjectSerializable, - signedInAuthObject, - signedOutAuthObject, - stripPrivateDataFromObject, -} from '@clerk/backend/internal'; -import { decodeJwt } from '@clerk/backend/jwt'; +import { makeAuthObjectSerializable, stripPrivateDataFromObject } from '@clerk/backend/internal'; -import { API_URL, API_VERSION, SECRET_KEY } from './constants'; +import { getAuthDataFromRequest } from './data/getAuthDataFromRequest'; import type { RequestLike } from './types'; -import { decryptClerkRequestData, getAuthKeyFromRequest, getHeader, injectSSRStateIntoObject } from './utils'; type BuildClerkPropsInitState = { user?: User | null; session?: Session | null; organization?: Organization | null }; @@ -33,34 +24,18 @@ type BuildClerkPropsInitState = { user?: User | null; session?: Session | null; */ type BuildClerkProps = (req: RequestLike, authState?: BuildClerkPropsInitState) => Record; -export const buildClerkProps: BuildClerkProps = (req, initState = {}) => { - const authStatus = getAuthKeyFromRequest(req, 'AuthStatus'); - const authToken = getAuthKeyFromRequest(req, 'AuthToken'); - const authMessage = getAuthKeyFromRequest(req, 'AuthMessage'); - const authReason = getAuthKeyFromRequest(req, 'AuthReason'); +export const buildClerkProps: BuildClerkProps = (req, initialState = {}) => { + const sanitizedAuthObject = getDynamicAuthData(req, initialState); - const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); - const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); - - const options = { - secretKey: decryptedRequestData.secretKey || SECRET_KEY, - apiUrl: API_URL, - apiVersion: API_VERSION, - authStatus, - authMessage, - authReason, - }; - - let authObject; - if (!authStatus || authStatus !== AuthStatus.SignedIn) { - authObject = signedOutAuthObject(options); - } else { - const jwt = decodeJwt(authToken as string); + // Serializing the state on dev env is a temp workaround for the following issue: + // https://github.com/vercel/next.js/discussions/11209|Next.js + const __clerk_ssr_state = + process.env.NODE_ENV !== 'production' ? JSON.parse(JSON.stringify(sanitizedAuthObject)) : sanitizedAuthObject; + return { __clerk_ssr_state }; +}; - // @ts-expect-error - TODO @nikos: Align types - authObject = signedInAuthObject(options, jwt.raw.text, jwt.payload); - } +export function getDynamicAuthData(req: RequestLike, initialState = {}) { + const authObject = getAuthDataFromRequest(req); - const sanitizedAuthObject = makeAuthObjectSerializable(stripPrivateDataFromObject({ ...authObject, ...initState })); - return injectSSRStateIntoObject({}, sanitizedAuthObject); -}; + return makeAuthObjectSerializable(stripPrivateDataFromObject({ ...authObject, ...initialState })); +} diff --git a/packages/nextjs/src/server/createGetAuth.ts b/packages/nextjs/src/server/createGetAuth.ts index 39b60a0d4d1..c8acc34ff66 100644 --- a/packages/nextjs/src/server/createGetAuth.ts +++ b/packages/nextjs/src/server/createGetAuth.ts @@ -1,12 +1,13 @@ import type { AuthObject } from '@clerk/backend'; -import { AuthStatus, constants, signedInAuthObject, signedOutAuthObject } from '@clerk/backend/internal'; +import { constants } from '@clerk/backend/internal'; import { decodeJwt } from '@clerk/backend/jwt'; +import { isTruthy } from '@clerk/shared'; import { withLogger } from '../utils/debugLogger'; -import { API_URL, API_VERSION, SECRET_KEY } from './constants'; +import { getAuthDataFromRequest } from './data/getAuthDataFromRequest'; import { getAuthAuthHeaderMissing } from './errors'; import type { RequestLike } from './types'; -import { assertTokenSignature, decryptClerkRequestData, getAuthKeyFromRequest, getCookie, getHeader } from './utils'; +import { assertAuthStatus, getCookie, getHeader } from './utils'; export const createGetAuth = ({ noAuthStatusMessage, @@ -17,50 +18,13 @@ export const createGetAuth = ({ }) => withLogger(debugLoggerName, logger => { return (req: RequestLike, opts?: { secretKey?: string }): AuthObject => { - if (getHeader(req, constants.Headers.EnableDebug) === 'true') { + if (isTruthy(getHeader(req, constants.Headers.EnableDebug))) { logger.enable(); } - // When the auth status is set, we trust that the middleware has already run - // Then, we don't have to re-verify the JWT here, - // we can just strip out the claims manually. - const authToken = getAuthKeyFromRequest(req, 'AuthToken'); - const authSignature = getAuthKeyFromRequest(req, 'AuthSignature'); - const authMessage = getAuthKeyFromRequest(req, 'AuthMessage'); - const authReason = getAuthKeyFromRequest(req, 'AuthReason'); - const authStatus = getAuthKeyFromRequest(req, 'AuthStatus') as AuthStatus; - logger.debug('Headers debug', { authStatus, authMessage, authReason }); + assertAuthStatus(req, noAuthStatusMessage); - if (!authStatus) { - throw new Error(noAuthStatusMessage); - } - - const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); - const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); - - const options = { - authStatus, - apiUrl: API_URL, - apiVersion: API_VERSION, - authMessage, - secretKey: opts?.secretKey || decryptedRequestData.secretKey || SECRET_KEY, - authReason, - }; - - logger.debug('Options debug', options); - - if (authStatus === AuthStatus.SignedIn) { - assertTokenSignature(authToken as string, options.secretKey, authSignature); - - const jwt = decodeJwt(authToken as string); - - logger.debug('JWT debug', jwt.raw.text); - - // @ts-expect-error - TODO @nikos: Align types - return signedInAuthObject(options, jwt.raw.text, jwt.payload); - } - - return signedOutAuthObject(options); + return getAuthDataFromRequest(req, { ...opts, logger }); }; }); diff --git a/packages/nextjs/src/server/data/getAuthDataFromRequest.ts b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts new file mode 100644 index 00000000000..69cbe911adf --- /dev/null +++ b/packages/nextjs/src/server/data/getAuthDataFromRequest.ts @@ -0,0 +1,55 @@ +import type { AuthObject } from '@clerk/backend'; +import { AuthStatus, constants, signedInAuthObject, signedOutAuthObject } from '@clerk/backend/internal'; +import { decodeJwt } from '@clerk/backend/jwt'; + +import type { LoggerNoCommit } from '../../utils/debugLogger'; +import { API_URL, API_VERSION, SECRET_KEY } from '../constants'; +import type { RequestLike } from '../types'; +import { assertTokenSignature, decryptClerkRequestData, getAuthKeyFromRequest, getHeader } from '../utils'; + +/** + * Given a request object, builds an auth object from the request data. Used in server-side environments to get access + * to auth data for a given request. + */ +export function getAuthDataFromRequest( + req: RequestLike, + opts: { secretKey?: string; logger?: LoggerNoCommit } = {}, +): AuthObject { + const authStatus = getAuthKeyFromRequest(req, 'AuthStatus'); + const authToken = getAuthKeyFromRequest(req, 'AuthToken'); + const authMessage = getAuthKeyFromRequest(req, 'AuthMessage'); + const authReason = getAuthKeyFromRequest(req, 'AuthReason'); + const authSignature = getAuthKeyFromRequest(req, 'AuthSignature'); + + opts.logger?.debug('headers', { authStatus, authMessage, authReason }); + + const encryptedRequestData = getHeader(req, constants.Headers.ClerkRequestData); + const decryptedRequestData = decryptClerkRequestData(encryptedRequestData); + + const options = { + secretKey: opts?.secretKey || decryptedRequestData.secretKey || SECRET_KEY, + apiUrl: API_URL, + apiVersion: API_VERSION, + authStatus, + authMessage, + authReason, + }; + + opts.logger?.debug('auth options', options); + + let authObject; + if (!authStatus || authStatus !== AuthStatus.SignedIn) { + authObject = signedOutAuthObject(options); + } else { + assertTokenSignature(authToken as string, options.secretKey, authSignature); + + const jwt = decodeJwt(authToken as string); + + opts.logger?.debug('jwt', jwt.raw); + + // @ts-expect-error -- Restrict parameter type of options to only list what's needed + authObject = signedInAuthObject(options, jwt.raw.text, jwt.payload); + } + + return authObject; +} diff --git a/packages/nextjs/src/server/utils.ts b/packages/nextjs/src/server/utils.ts index 29b558f2e42..90c2a0cb4c8 100644 --- a/packages/nextjs/src/server/utils.ts +++ b/packages/nextjs/src/server/utils.ts @@ -92,15 +92,6 @@ export const setRequestHeadersOnNextResponse = ( }); }; -export const injectSSRStateIntoObject = (obj: O, authObject: T) => { - // Serializing the state on dev env is a temp workaround for the following issue: - // https://github.com/vercel/next.js/discussions/11209|Next.js - const __clerk_ssr_state = ( - process.env.NODE_ENV !== 'production' ? JSON.parse(JSON.stringify({ ...authObject })) : { ...authObject } - ) as T; - return { ...obj, __clerk_ssr_state }; -}; - // Auth result will be set as both a query param & header when applicable export function decorateRequest( req: ClerkRequest, @@ -196,6 +187,14 @@ export const redirectAdapter = (url: string | URL) => { return NextResponse.redirect(url, { headers: { [constants.Headers.ClerkRedirectTo]: 'true' } }); }; +export function assertAuthStatus(req: RequestLike, error: string) { + const authStatus = getAuthKeyFromRequest(req, 'AuthStatus'); + + if (!authStatus) { + throw new Error(error); + } +} + export function assertKey(key: string, onError: () => never): string { if (!key) { onError(); diff --git a/packages/nextjs/src/utils/debugLogger.ts b/packages/nextjs/src/utils/debugLogger.ts index d2ddab16040..9d2ca496d80 100644 --- a/packages/nextjs/src/utils/debugLogger.ts +++ b/packages/nextjs/src/utils/debugLogger.ts @@ -11,6 +11,7 @@ export type Logger = { debug: (...args: Array L)>) => void; enable: () => void; }; +export type LoggerNoCommit = Omit; export const createDebugLogger = (name: string, formatter: (val: LogEntry) => string) => (): Logger => { const entries: LogEntry[] = []; @@ -57,7 +58,7 @@ export const createDebugLogger = (name: string, formatter: (val: LogEntry) => st type WithLogger = any>( loggerFactoryOrName: string | (() => L), - handlerCtor: (logger: Omit) => H, + handlerCtor: (logger: LoggerNoCommit) => H, ) => H; export const withLogger: WithLogger = (loggerFactoryOrName, handlerCtor) => { From e13919d68b31b25d5e6513a35f82015c1707620c Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Mon, 21 Oct 2024 21:06:31 -0700 Subject: [PATCH 2/7] feat: Introduce --- .../client/ClerkDynamicProvider.tsx | 4 ++ .../src/app-router/client/ClerkProvider.tsx | 8 +++- .../server/ClerkDynamicProvider.tsx | 22 +++++++++ .../src/app-router/server/ClerkProvider.tsx | 22 +++++++-- .../client-boundary/NextOptionsContext.tsx | 2 +- packages/nextjs/src/client-boundary/hooks.ts | 1 + packages/nextjs/src/components.client.ts | 3 ++ packages/nextjs/src/components.server.ts | 4 +- packages/nextjs/src/index.ts | 3 ++ packages/nextjs/src/types.ts | 6 +++ packages/react/src/contexts/AuthContext.ts | 6 ++- .../src/contexts/ClerkDynamicProvider.tsx | 46 +++++++++++++++++++ packages/react/src/contexts/index.ts | 1 + packages/react/src/hooks/useAuth.ts | 27 +++++++++-- 14 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 packages/nextjs/src/app-router/client/ClerkDynamicProvider.tsx create mode 100644 packages/nextjs/src/app-router/server/ClerkDynamicProvider.tsx create mode 100644 packages/react/src/contexts/ClerkDynamicProvider.tsx diff --git a/packages/nextjs/src/app-router/client/ClerkDynamicProvider.tsx b/packages/nextjs/src/app-router/client/ClerkDynamicProvider.tsx new file mode 100644 index 00000000000..9b7bf2e994c --- /dev/null +++ b/packages/nextjs/src/app-router/client/ClerkDynamicProvider.tsx @@ -0,0 +1,4 @@ +'use client'; +import { PromisifiedAuthProvider } from '@clerk/clerk-react'; + +export { PromisifiedAuthProvider }; diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx index af71badee47..8b9099a6198 100644 --- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx @@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation'; import React, { useEffect, useTransition } from 'react'; import { useSafeLayoutEffect } from '../../client-boundary/hooks/useSafeLayoutEffect'; -import { ClerkNextOptionsProvider } from '../../client-boundary/NextOptionsContext'; +import { ClerkNextOptionsProvider, useClerkNextOptions } from '../../client-boundary/NextOptionsContext'; import type { NextClerkProviderProps } from '../../types'; import { ClerkJSScript } from '../../utils/clerk-js-script'; import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; @@ -62,6 +62,12 @@ export const ClientClerkProvider = (props: NextClerkProviderProps) => { const replace = useAwaitableReplace(); const [isPending, startTransition] = useTransition(); + // Avoid rendering nested ClerkProviders by checking for the existence of the ClerkNextOptions context provider + const isNested = Boolean(useClerkNextOptions()); + if (isNested) { + return props.children; + } + useEffect(() => { if (!isPending) { window.__clerk_internal_invalidateCachePromise?.(); diff --git a/packages/nextjs/src/app-router/server/ClerkDynamicProvider.tsx b/packages/nextjs/src/app-router/server/ClerkDynamicProvider.tsx new file mode 100644 index 00000000000..bce367e126f --- /dev/null +++ b/packages/nextjs/src/app-router/server/ClerkDynamicProvider.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import { getDynamicAuthData } from '../../server/buildClerkProps'; +import { PromisifiedAuthProvider } from '../client/ClerkDynamicProvider'; +import { buildRequestLike } from './utils'; + +interface ClerkDynamicProviderOptions { + dynamic?: boolean; + children?: React.ReactNode; +} + +async function getDynamicClerkState() { + const request = await buildRequestLike(); + const data = await getDynamicAuthData(request); + + return data; +} + +export async function ClerkDynamicProvider({ children }: ClerkDynamicProviderOptions) { + const dataPromise = getDynamicClerkState(); + return {children}; +} diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index f405b2a87e8..558f39ea332 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -6,22 +6,34 @@ import type { NextClerkProviderProps } from '../../types'; import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; import { ClientClerkProvider } from '../client/ClerkProvider'; import { initialState } from './auth'; +import { ClerkDynamicProvider } from './ClerkDynamicProvider'; import { getScriptNonceFromHeader } from './utils'; export async function ClerkProvider( props: Without, ) { - const { children, ...rest } = props; - const state = (await initialState())?.__clerk_ssr_state as InitialState; - const cspHeader = (await headers()).get('Content-Security-Policy'); + const { children, dynamic, ...rest } = props; + let state = {}; + let nonce = ''; - return ( + if (dynamic) { + state = (await initialState())?.__clerk_ssr_state as InitialState; + nonce = getScriptNonceFromHeader((await headers()).get('Content-Security-Policy') || '') || ''; + } + + const output = ( {children} ); + + if (dynamic) { + return {output}; + } + + return output; } diff --git a/packages/nextjs/src/client-boundary/NextOptionsContext.tsx b/packages/nextjs/src/client-boundary/NextOptionsContext.tsx index 7bc421e2c90..05fa6b0172e 100644 --- a/packages/nextjs/src/client-boundary/NextOptionsContext.tsx +++ b/packages/nextjs/src/client-boundary/NextOptionsContext.tsx @@ -9,7 +9,7 @@ ClerkNextOptionsCtx.displayName = 'ClerkNextOptionsCtx'; const useClerkNextOptions = () => { const ctx = React.useContext(ClerkNextOptionsCtx) as { value: ClerkNextContextValue }; - return ctx.value; + return ctx?.value; }; const ClerkNextOptionsProvider = ( diff --git a/packages/nextjs/src/client-boundary/hooks.ts b/packages/nextjs/src/client-boundary/hooks.ts index 10fbeb540cc..90d7cfcb25f 100644 --- a/packages/nextjs/src/client-boundary/hooks.ts +++ b/packages/nextjs/src/client-boundary/hooks.ts @@ -11,6 +11,7 @@ export { useSignIn, useSignUp, useUser, + usePromisifiedAuth, } from '@clerk/clerk-react'; export { diff --git a/packages/nextjs/src/components.client.ts b/packages/nextjs/src/components.client.ts index aac3f82f65b..060f57363d2 100644 --- a/packages/nextjs/src/components.client.ts +++ b/packages/nextjs/src/components.client.ts @@ -1,2 +1,5 @@ export { ClerkProvider } from './client-boundary/ClerkProvider'; export { SignedIn, SignedOut, Protect } from './client-boundary/controlComponents'; +export function ClerkDynamicProvider() { + throw new Error('ClerkDynamicProvider imported into a client component.'); +} diff --git a/packages/nextjs/src/components.server.ts b/packages/nextjs/src/components.server.ts index f73c8cc91c5..92ec8d0c2bc 100644 --- a/packages/nextjs/src/components.server.ts +++ b/packages/nextjs/src/components.server.ts @@ -1,11 +1,13 @@ +import { ClerkDynamicProvider } from './app-router/server/ClerkDynamicProvider'; import { ClerkProvider } from './app-router/server/ClerkProvider'; import { Protect, SignedIn, SignedOut } from './app-router/server/controlComponents'; -export { ClerkProvider, SignedOut, SignedIn, Protect }; +export { ClerkProvider, SignedOut, SignedIn, Protect, ClerkDynamicProvider }; export type ServerComponentsServerModuleTypes = { ClerkProvider: typeof ClerkProvider; SignedIn: typeof SignedIn; SignedOut: typeof SignedOut; Protect: typeof Protect; + ClerkDynamicProvider: typeof ClerkDynamicProvider; }; diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 8a7e7513feb..91aa9825b43 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -48,6 +48,7 @@ export { useSignIn, useSignUp, useUser, + usePromisifiedAuth, } from './client-boundary/hooks'; /** @@ -66,3 +67,5 @@ export const ClerkProvider = ComponentsModule.ClerkProvider as ServerComponentsS 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 ClerkDynamicProvider = + ComponentsModule.ClerkDynamicProvider as ServerComponentsServerModuleTypes['ClerkDynamicProvider']; diff --git a/packages/nextjs/src/types.ts b/packages/nextjs/src/types.ts index d0eb56cbe64..96a89d74e54 100644 --- a/packages/nextjs/src/types.ts +++ b/packages/nextjs/src/types.ts @@ -16,4 +16,10 @@ export type NextClerkProviderProps = Without('AuthContext'); +}; + +export const [AuthContext, useAuthContext] = createContextAndHook('AuthContext'); diff --git a/packages/react/src/contexts/ClerkDynamicProvider.tsx b/packages/react/src/contexts/ClerkDynamicProvider.tsx new file mode 100644 index 00000000000..8f9d536d208 --- /dev/null +++ b/packages/react/src/contexts/ClerkDynamicProvider.tsx @@ -0,0 +1,46 @@ +import React from 'react'; + +import { useAuth, useDerivedAuth } from '../hooks/useAuth'; + +const PromisifiedAuthContext = React.createContext | unknown | null>(null); + +export function PromisifiedAuthProvider({ + authPromise, + children, +}: { + authPromise: Promise | unknown; + children: React.ReactNode; +}) { + return {children}; +} + +export function usePromisifiedAuth() { + const valueFromContext = React.useContext(PromisifiedAuthContext); + + let resolvedData = valueFromContext; + // @ts-expect-error -- TODO: fixme + if (valueFromContext && 'then' in valueFromContext) { + if (!('use' in React)) { + throw new Error( + `Attempting to read a promise from AuthContext but use() is not available. React version: ${React.version}`, + ); + } + // @ts-expect-error -- use() does not exist on stable React types yet + resolvedData = React.use(valueFromContext); + } + + // At this point we should have a usable auth object + + if (typeof window === 'undefined') { + if (!resolvedData) { + throw new Error('useAuth() called without ClerkDynamicProvider'); + } + // We don't need to deal with Clerk being loaded here + return useDerivedAuth(resolvedData); + } else { + return useAuth(resolvedData); + } + + // TODO: handle clerk loading state, or just delegate to useAuth()? we know that will be available + return {}; +} diff --git a/packages/react/src/contexts/index.ts b/packages/react/src/contexts/index.ts index aebcfc5ad9b..7c49a8ce559 100644 --- a/packages/react/src/contexts/index.ts +++ b/packages/react/src/contexts/index.ts @@ -1 +1,2 @@ export { ClerkProvider } from './ClerkProvider'; +export { PromisifiedAuthProvider, usePromisifiedAuth } from './ClerkDynamicProvider'; diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index d3aed868581..e73cfbbbfaf 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -6,7 +6,7 @@ import type { OrganizationCustomRoleKey, SignOut, } from '@clerk/types'; -import { useCallback } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { useAuthContext } from '../contexts/AuthContext'; import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext'; @@ -72,7 +72,7 @@ type UseAuthReturn = getToken: GetToken; }; -type UseAuth = () => UseAuthReturn; +type UseAuth = (initialAuthState?: any) => UseAuthReturn; /** * Returns the current auth state, the user and session ids and the `getToken` @@ -110,11 +110,22 @@ type UseAuth = () => UseAuthReturn; * return
...
* } */ -export const useAuth: UseAuth = () => { +export const useAuth: UseAuth = (initialAuthState = {}) => { useAssertWrappedByClerkProvider('useAuth'); + const authContext = useAuthContext(); + + const [authState, setAuthState] = useState(() => initialAuthState); + + useEffect(() => { + if (authContext.sessionId === undefined && authContext.userId === undefined) { + return; + } + setAuthState(authContext); + }, [authContext]); + const { sessionId, userId, actor, orgId, orgRole, orgSlug, orgPermissions, __experimental_factorVerificationAge } = - useAuthContext(); + authState; const isomorphicClerk = useIsomorphicClerkContext(); const getToken: GetToken = useCallback(createGetToken(isomorphicClerk), [isomorphicClerk]); @@ -133,6 +144,12 @@ export const useAuth: UseAuth = () => { [userId, __experimental_factorVerificationAge, orgId, orgRole, orgPermissions], ); + return useDerivedAuth({ sessionId, userId, actor, orgId, orgSlug, orgRole, getToken, signOut, has }); +}; + +export function useDerivedAuth(authObject: any): UseAuthReturn { + const { sessionId, userId, actor, orgId, orgSlug, orgRole, has, signOut, getToken } = authObject; + if (sessionId === undefined && userId === undefined) { return { isLoaded: false, @@ -198,4 +215,4 @@ export const useAuth: UseAuth = () => { } return errorThrower.throw(invalidStateError); -}; +} From 7e11e64560a0c472200254ce83d8ca0242ece05e Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Mon, 21 Oct 2024 22:09:09 -0700 Subject: [PATCH 3/7] chore: Refactor implementation to remove ClerkDynamicProvider export --- ...ovider.tsx => PromisifiedAuthProvider.tsx} | 2 ++ .../server/ClerkDynamicProvider.tsx | 22 ------------- .../src/app-router/server/ClerkProvider.tsx | 33 ++++++++++++------- packages/nextjs/src/app-router/server/auth.ts | 5 --- packages/nextjs/src/components.client.ts | 3 -- packages/nextjs/src/components.server.ts | 4 +-- packages/nextjs/src/index.ts | 2 -- .../src/contexts/ClerkDynamicProvider.tsx | 2 +- 8 files changed, 26 insertions(+), 47 deletions(-) rename packages/nextjs/src/app-router/client/{ClerkDynamicProvider.tsx => PromisifiedAuthProvider.tsx} (62%) delete mode 100644 packages/nextjs/src/app-router/server/ClerkDynamicProvider.tsx diff --git a/packages/nextjs/src/app-router/client/ClerkDynamicProvider.tsx b/packages/nextjs/src/app-router/client/PromisifiedAuthProvider.tsx similarity index 62% rename from packages/nextjs/src/app-router/client/ClerkDynamicProvider.tsx rename to packages/nextjs/src/app-router/client/PromisifiedAuthProvider.tsx index 9b7bf2e994c..50df7902c82 100644 --- a/packages/nextjs/src/app-router/client/ClerkDynamicProvider.tsx +++ b/packages/nextjs/src/app-router/client/PromisifiedAuthProvider.tsx @@ -1,4 +1,6 @@ 'use client'; + import { PromisifiedAuthProvider } from '@clerk/clerk-react'; +// This is re-exported from a module with a 'use client' directive export { PromisifiedAuthProvider }; diff --git a/packages/nextjs/src/app-router/server/ClerkDynamicProvider.tsx b/packages/nextjs/src/app-router/server/ClerkDynamicProvider.tsx deleted file mode 100644 index bce367e126f..00000000000 --- a/packages/nextjs/src/app-router/server/ClerkDynamicProvider.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -import { getDynamicAuthData } from '../../server/buildClerkProps'; -import { PromisifiedAuthProvider } from '../client/ClerkDynamicProvider'; -import { buildRequestLike } from './utils'; - -interface ClerkDynamicProviderOptions { - dynamic?: boolean; - children?: React.ReactNode; -} - -async function getDynamicClerkState() { - const request = await buildRequestLike(); - const data = await getDynamicAuthData(request); - - return data; -} - -export async function ClerkDynamicProvider({ children }: ClerkDynamicProviderOptions) { - const dataPromise = getDynamicClerkState(); - return {children}; -} diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 558f39ea332..87456fd01c0 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -1,38 +1,49 @@ -import type { InitialState, Without } from '@clerk/types'; +import type { Without } from '@clerk/types'; import { headers } from 'next/headers'; import React from 'react'; +import { getDynamicAuthData } from '../../server/buildClerkProps'; import type { NextClerkProviderProps } from '../../types'; import { mergeNextClerkPropsWithEnv } from '../../utils/mergeNextClerkPropsWithEnv'; import { ClientClerkProvider } from '../client/ClerkProvider'; -import { initialState } from './auth'; -import { ClerkDynamicProvider } from './ClerkDynamicProvider'; -import { getScriptNonceFromHeader } from './utils'; +import { PromisifiedAuthProvider } from '../client/PromisifiedAuthProvider'; +import { buildRequestLike, getScriptNonceFromHeader } from './utils'; + +const getDynamicClerkState = React.cache(async function getDynamicClerkState() { + const request = await buildRequestLike(); + const data = await getDynamicAuthData(request); + + return data; +}); + +const getNonceFromCSPHeader = React.cache(async function getNonceFromCSPHeader() { + return getScriptNonceFromHeader((await headers()).get('Content-Security-Policy') || '') || ''; +}); export async function ClerkProvider( props: Without, ) { const { children, dynamic, ...rest } = props; - let state = {}; - let nonce = ''; + let statePromise = Promise.resolve({}); + let nonce = Promise.resolve(''); if (dynamic) { - state = (await initialState())?.__clerk_ssr_state as InitialState; - nonce = getScriptNonceFromHeader((await headers()).get('Content-Security-Policy') || '') || ''; + statePromise = getDynamicClerkState(); + nonce = getNonceFromCSPHeader(); } const output = ( {children} ); if (dynamic) { - return {output}; + return {output}; } return output; diff --git a/packages/nextjs/src/app-router/server/auth.ts b/packages/nextjs/src/app-router/server/auth.ts index 70279323dda..d00466897f2 100644 --- a/packages/nextjs/src/app-router/server/auth.ts +++ b/packages/nextjs/src/app-router/server/auth.ts @@ -2,7 +2,6 @@ import type { AuthObject } from '@clerk/backend'; import { constants, createClerkRequest, createRedirect, type RedirectFun } from '@clerk/backend/internal'; import { notFound, redirect } from 'next/navigation'; -import { buildClerkProps } from '../../server/buildClerkProps'; import { PUBLISHABLE_KEY, SIGN_IN_URL, SIGN_UP_URL } from '../../server/constants'; import { createGetAuth } from '../../server/createGetAuth'; import { authAuthHeaderMissing } from '../../server/errors'; @@ -62,7 +61,3 @@ auth.protect = async (...args: any[]) => { }); return protect(...args); }; - -export async function initialState() { - return buildClerkProps(await buildRequestLike()); -} diff --git a/packages/nextjs/src/components.client.ts b/packages/nextjs/src/components.client.ts index 060f57363d2..aac3f82f65b 100644 --- a/packages/nextjs/src/components.client.ts +++ b/packages/nextjs/src/components.client.ts @@ -1,5 +1,2 @@ export { ClerkProvider } from './client-boundary/ClerkProvider'; export { SignedIn, SignedOut, Protect } from './client-boundary/controlComponents'; -export function ClerkDynamicProvider() { - throw new Error('ClerkDynamicProvider imported into a client component.'); -} diff --git a/packages/nextjs/src/components.server.ts b/packages/nextjs/src/components.server.ts index 92ec8d0c2bc..f73c8cc91c5 100644 --- a/packages/nextjs/src/components.server.ts +++ b/packages/nextjs/src/components.server.ts @@ -1,13 +1,11 @@ -import { ClerkDynamicProvider } from './app-router/server/ClerkDynamicProvider'; import { ClerkProvider } from './app-router/server/ClerkProvider'; import { Protect, SignedIn, SignedOut } from './app-router/server/controlComponents'; -export { ClerkProvider, SignedOut, SignedIn, Protect, ClerkDynamicProvider }; +export { ClerkProvider, SignedOut, SignedIn, Protect }; export type ServerComponentsServerModuleTypes = { ClerkProvider: typeof ClerkProvider; SignedIn: typeof SignedIn; SignedOut: typeof SignedOut; Protect: typeof Protect; - ClerkDynamicProvider: typeof ClerkDynamicProvider; }; diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 91aa9825b43..69f6582f8af 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -67,5 +67,3 @@ export const ClerkProvider = ComponentsModule.ClerkProvider as ServerComponentsS 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 ClerkDynamicProvider = - ComponentsModule.ClerkDynamicProvider as ServerComponentsServerModuleTypes['ClerkDynamicProvider']; diff --git a/packages/react/src/contexts/ClerkDynamicProvider.tsx b/packages/react/src/contexts/ClerkDynamicProvider.tsx index 8f9d536d208..1886815dbd0 100644 --- a/packages/react/src/contexts/ClerkDynamicProvider.tsx +++ b/packages/react/src/contexts/ClerkDynamicProvider.tsx @@ -33,7 +33,7 @@ export function usePromisifiedAuth() { if (typeof window === 'undefined') { if (!resolvedData) { - throw new Error('useAuth() called without ClerkDynamicProvider'); + throw new Error('useAuth() called in static mode, wrap this component '); } // We don't need to deal with Clerk being loaded here return useDerivedAuth(resolvedData); From 504a64077b8160077356ef957e72c49b1940d9c1 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Mon, 21 Oct 2024 22:13:43 -0700 Subject: [PATCH 4/7] chore: await instead of use in RSC --- packages/nextjs/src/app-router/server/ClerkProvider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 87456fd01c0..9c74615c649 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -35,8 +35,8 @@ export async function ClerkProvider( const output = ( {children} From 0e8d2612f330e26b8c891dad06309656a24419eb Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Mon, 21 Oct 2024 22:42:27 -0700 Subject: [PATCH 5/7] fix types and client-side only rendering --- .../nextjs/src/app-router/server/ClerkProvider.tsx | 12 +++++++++--- packages/nextjs/src/client-boundary/hooks.ts | 3 +-- packages/nextjs/src/index.ts | 1 - packages/nextjs/src/server/buildClerkProps.ts | 4 ++-- ...amicProvider.tsx => PromisifiedAuthProvider.tsx} | 13 ++++++------- packages/react/src/contexts/index.ts | 2 +- packages/react/src/hooks/useAuth.ts | 2 +- 7 files changed, 20 insertions(+), 17 deletions(-) rename packages/react/src/contexts/{ClerkDynamicProvider.tsx => PromisifiedAuthProvider.tsx} (73%) diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 9c74615c649..3ac42e5ba34 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -1,4 +1,5 @@ -import type { Without } from '@clerk/types'; +import type { AuthObject } from '@clerk/backend'; +import type { InitialState, Without } from '@clerk/types'; import { headers } from 'next/headers'; import React from 'react'; @@ -24,7 +25,7 @@ export async function ClerkProvider( props: Without, ) { const { children, dynamic, ...rest } = props; - let statePromise = Promise.resolve({}); + let statePromise: Promise = Promise.resolve(null); let nonce = Promise.resolve(''); if (dynamic) { @@ -43,7 +44,12 @@ export async function ClerkProvider( ); if (dynamic) { - return {output}; + return ( + // TODO: fix types so AuthObject is compatible with InitialState + }> + {output} + + ); } return output; diff --git a/packages/nextjs/src/client-boundary/hooks.ts b/packages/nextjs/src/client-boundary/hooks.ts index 90d7cfcb25f..43101f3501f 100644 --- a/packages/nextjs/src/client-boundary/hooks.ts +++ b/packages/nextjs/src/client-boundary/hooks.ts @@ -1,7 +1,6 @@ 'use client'; export { - useAuth, useClerk, useEmailLink, useOrganization, @@ -11,7 +10,7 @@ export { useSignIn, useSignUp, useUser, - usePromisifiedAuth, + usePromisifiedAuth as useAuth, } from '@clerk/clerk-react'; export { diff --git a/packages/nextjs/src/index.ts b/packages/nextjs/src/index.ts index 69f6582f8af..8a7e7513feb 100644 --- a/packages/nextjs/src/index.ts +++ b/packages/nextjs/src/index.ts @@ -48,7 +48,6 @@ export { useSignIn, useSignUp, useUser, - usePromisifiedAuth, } from './client-boundary/hooks'; /** diff --git a/packages/nextjs/src/server/buildClerkProps.ts b/packages/nextjs/src/server/buildClerkProps.ts index dd1c9e9b8a3..a6f51aa068c 100644 --- a/packages/nextjs/src/server/buildClerkProps.ts +++ b/packages/nextjs/src/server/buildClerkProps.ts @@ -1,4 +1,4 @@ -import type { Organization, Session, User } from '@clerk/backend'; +import type { AuthObject, Organization, Session, User } from '@clerk/backend'; import { makeAuthObjectSerializable, stripPrivateDataFromObject } from '@clerk/backend/internal'; import { getAuthDataFromRequest } from './data/getAuthDataFromRequest'; @@ -37,5 +37,5 @@ export const buildClerkProps: BuildClerkProps = (req, initialState = {}) => { export function getDynamicAuthData(req: RequestLike, initialState = {}) { const authObject = getAuthDataFromRequest(req); - return makeAuthObjectSerializable(stripPrivateDataFromObject({ ...authObject, ...initialState })); + return makeAuthObjectSerializable(stripPrivateDataFromObject({ ...authObject, ...initialState })) as AuthObject; } diff --git a/packages/react/src/contexts/ClerkDynamicProvider.tsx b/packages/react/src/contexts/PromisifiedAuthProvider.tsx similarity index 73% rename from packages/react/src/contexts/ClerkDynamicProvider.tsx rename to packages/react/src/contexts/PromisifiedAuthProvider.tsx index 1886815dbd0..a476a9b9ca2 100644 --- a/packages/react/src/contexts/ClerkDynamicProvider.tsx +++ b/packages/react/src/contexts/PromisifiedAuthProvider.tsx @@ -1,14 +1,15 @@ +import type { InitialState } from '@clerk/types'; import React from 'react'; import { useAuth, useDerivedAuth } from '../hooks/useAuth'; -const PromisifiedAuthContext = React.createContext | unknown | null>(null); +const PromisifiedAuthContext = React.createContext | InitialState | null>(null); export function PromisifiedAuthProvider({ authPromise, children, }: { - authPromise: Promise | unknown; + authPromise: Promise | InitialState; children: React.ReactNode; }) { return {children}; @@ -18,7 +19,6 @@ export function usePromisifiedAuth() { const valueFromContext = React.useContext(PromisifiedAuthContext); let resolvedData = valueFromContext; - // @ts-expect-error -- TODO: fixme if (valueFromContext && 'then' in valueFromContext) { if (!('use' in React)) { throw new Error( @@ -33,14 +33,13 @@ export function usePromisifiedAuth() { if (typeof window === 'undefined') { if (!resolvedData) { - throw new Error('useAuth() called in static mode, wrap this component '); + throw new Error( + 'Clerk: useAuth() called in static mode, wrap this component in to make auth data available during server-side rendering.', + ); } // We don't need to deal with Clerk being loaded here return useDerivedAuth(resolvedData); } else { return useAuth(resolvedData); } - - // TODO: handle clerk loading state, or just delegate to useAuth()? we know that will be available - return {}; } diff --git a/packages/react/src/contexts/index.ts b/packages/react/src/contexts/index.ts index 7c49a8ce559..e640a14a3cc 100644 --- a/packages/react/src/contexts/index.ts +++ b/packages/react/src/contexts/index.ts @@ -1,2 +1,2 @@ export { ClerkProvider } from './ClerkProvider'; -export { PromisifiedAuthProvider, usePromisifiedAuth } from './ClerkDynamicProvider'; +export { PromisifiedAuthProvider, usePromisifiedAuth } from './PromisifiedAuthProvider'; diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index e73cfbbbfaf..db8c4143ad0 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -115,7 +115,7 @@ export const useAuth: UseAuth = (initialAuthState = {}) => { const authContext = useAuthContext(); - const [authState, setAuthState] = useState(() => initialAuthState); + const [authState, setAuthState] = useState(() => initialAuthState ?? {}); useEffect(() => { if (authContext.sessionId === undefined && authContext.userId === undefined) { From 9746595120bd1f8d56e4d544113e188d54858a33 Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Mon, 21 Oct 2024 23:15:30 -0700 Subject: [PATCH 6/7] more fixes --- package-lock.json | 2 - .../client/PromisifiedAuthProvider.tsx | 6 -- .../src/app-router/server/ClerkProvider.tsx | 4 +- .../PromisifiedAuthProvider.tsx | 85 +++++++++++++++++++ packages/nextjs/src/client-boundary/hooks.ts | 3 +- .../src/contexts/PromisifiedAuthProvider.tsx | 45 ---------- packages/react/src/contexts/index.ts | 1 - packages/react/src/hooks/useAuth.ts | 8 +- packages/react/src/internal.ts | 1 + turbo.json | 3 +- 10 files changed, 99 insertions(+), 59 deletions(-) delete mode 100644 packages/nextjs/src/app-router/client/PromisifiedAuthProvider.tsx create mode 100644 packages/nextjs/src/client-boundary/PromisifiedAuthProvider.tsx delete mode 100644 packages/react/src/contexts/PromisifiedAuthProvider.tsx diff --git a/package-lock.json b/package-lock.json index 4b486b2b739..301f92b2d4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13278,7 +13278,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-cross-context/-/react-cross-context-1.74.5.tgz", "integrity": "sha512-a4BoKe1umpt4mmol2fUc7S11ilYIrn60ysoLot0NE+BeRsML83MvgPsLTAx7h1fTp0gmLjtpMjjubIJ8GlDIQg==", "dev": true, - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" @@ -13293,7 +13292,6 @@ "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.5.6.tgz", "integrity": "sha512-SitIpS5jTj28DajjLpWbIX+YetmJL+6PRY0DKKiCGBKfYIqj3ryODQYF3jB3SNoR9ifUA/jFkqbJdBKFtWd+AQ==", "dev": true, - "license": "MIT", "dependencies": { "@tanstack/store": "0.5.5", "use-sync-external-store": "^1.2.2" diff --git a/packages/nextjs/src/app-router/client/PromisifiedAuthProvider.tsx b/packages/nextjs/src/app-router/client/PromisifiedAuthProvider.tsx deleted file mode 100644 index 50df7902c82..00000000000 --- a/packages/nextjs/src/app-router/client/PromisifiedAuthProvider.tsx +++ /dev/null @@ -1,6 +0,0 @@ -'use client'; - -import { PromisifiedAuthProvider } from '@clerk/clerk-react'; - -// This is re-exported from a module with a 'use client' directive -export { PromisifiedAuthProvider }; diff --git a/packages/nextjs/src/app-router/server/ClerkProvider.tsx b/packages/nextjs/src/app-router/server/ClerkProvider.tsx index 3ac42e5ba34..b55db7dd9c3 100644 --- a/packages/nextjs/src/app-router/server/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/server/ClerkProvider.tsx @@ -3,16 +3,16 @@ import type { InitialState, Without } from '@clerk/types'; import { headers } from 'next/headers'; 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'; import { ClientClerkProvider } from '../client/ClerkProvider'; -import { PromisifiedAuthProvider } from '../client/PromisifiedAuthProvider'; import { buildRequestLike, getScriptNonceFromHeader } from './utils'; const getDynamicClerkState = React.cache(async function getDynamicClerkState() { const request = await buildRequestLike(); - const data = await getDynamicAuthData(request); + const data = getDynamicAuthData(request); return data; }); diff --git a/packages/nextjs/src/client-boundary/PromisifiedAuthProvider.tsx b/packages/nextjs/src/client-boundary/PromisifiedAuthProvider.tsx new file mode 100644 index 00000000000..54429ad5292 --- /dev/null +++ b/packages/nextjs/src/client-boundary/PromisifiedAuthProvider.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { useAuth } from '@clerk/clerk-react'; +import { useDerivedAuth } from '@clerk/clerk-react/internal'; +import type { InitialState } from '@clerk/types'; +import { useRouter } from 'next/compat/router'; +import { PHASE_PRODUCTION_BUILD } from 'next/constants'; +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 + * A simple example: + * + * import { useAuth } from '@clerk/nextjs' + * + * function Hello() { + * const { isSignedIn, sessionId, userId } = useAuth(); + * if(isSignedIn) { + * return null; + * } + * console.log(sessionId, userId) + * return
...
+ * } + * + * @example + * Basic example in a NextJs app. This page will be fully rendered during SSR: + * + * import { useAuth } from '@clerk/nextjs' + * + * export HelloPage = () => { + * const { isSignedIn, sessionId, userId } = useAuth(); + * console.log(isSignedIn, sessionId, userId) + * return
...
+ * } + */ +export function usePromisifiedAuth() { + 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(); + } + + if (!resolvedData && process.env.NEXT_PHASE !== PHASE_PRODUCTION_BUILD) { + throw new Error( + 'Clerk: useAuth() called in static mode, wrap this component in to make auth data available during server-side rendering.', + ); + } + // We don't need to deal with Clerk being loaded here + return useDerivedAuth(resolvedData); + } else { + return useAuth(resolvedData); + } +} diff --git a/packages/nextjs/src/client-boundary/hooks.ts b/packages/nextjs/src/client-boundary/hooks.ts index 43101f3501f..b3756943a47 100644 --- a/packages/nextjs/src/client-boundary/hooks.ts +++ b/packages/nextjs/src/client-boundary/hooks.ts @@ -10,7 +10,6 @@ export { useSignIn, useSignUp, useUser, - usePromisifiedAuth as useAuth, } from '@clerk/clerk-react'; export { @@ -20,3 +19,5 @@ export { isMetamaskError, EmailLinkErrorCode, } from '@clerk/clerk-react/errors'; + +export { usePromisifiedAuth as useAuth } from './PromisifiedAuthProvider'; diff --git a/packages/react/src/contexts/PromisifiedAuthProvider.tsx b/packages/react/src/contexts/PromisifiedAuthProvider.tsx deleted file mode 100644 index a476a9b9ca2..00000000000 --- a/packages/react/src/contexts/PromisifiedAuthProvider.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { InitialState } from '@clerk/types'; -import React from 'react'; - -import { useAuth, useDerivedAuth } from '../hooks/useAuth'; - -const PromisifiedAuthContext = React.createContext | InitialState | null>(null); - -export function PromisifiedAuthProvider({ - authPromise, - children, -}: { - authPromise: Promise | InitialState; - children: React.ReactNode; -}) { - return {children}; -} - -export function usePromisifiedAuth() { - const valueFromContext = React.useContext(PromisifiedAuthContext); - - let resolvedData = valueFromContext; - if (valueFromContext && 'then' in valueFromContext) { - if (!('use' in React)) { - throw new Error( - `Attempting to read a promise from AuthContext but use() is not available. React version: ${React.version}`, - ); - } - // @ts-expect-error -- use() does not exist on stable React types yet - resolvedData = React.use(valueFromContext); - } - - // At this point we should have a usable auth object - - if (typeof window === 'undefined') { - if (!resolvedData) { - throw new Error( - 'Clerk: useAuth() called in static mode, wrap this component in to make auth data available during server-side rendering.', - ); - } - // We don't need to deal with Clerk being loaded here - return useDerivedAuth(resolvedData); - } else { - return useAuth(resolvedData); - } -} diff --git a/packages/react/src/contexts/index.ts b/packages/react/src/contexts/index.ts index e640a14a3cc..aebcfc5ad9b 100644 --- a/packages/react/src/contexts/index.ts +++ b/packages/react/src/contexts/index.ts @@ -1,2 +1 @@ export { ClerkProvider } from './ClerkProvider'; -export { PromisifiedAuthProvider, usePromisifiedAuth } from './PromisifiedAuthProvider'; diff --git a/packages/react/src/hooks/useAuth.ts b/packages/react/src/hooks/useAuth.ts index db8c4143ad0..9cfbac8bddd 100644 --- a/packages/react/src/hooks/useAuth.ts +++ b/packages/react/src/hooks/useAuth.ts @@ -115,7 +115,13 @@ export const useAuth: UseAuth = (initialAuthState = {}) => { const authContext = useAuthContext(); - const [authState, setAuthState] = useState(() => initialAuthState ?? {}); + const [authState, setAuthState] = useState(() => { + // This indicates the authContext is not available, and so we fallback to the provided initialState + if (authContext.sessionId === undefined && authContext.userId === undefined) { + return initialAuthState ?? {}; + } + return authContext; + }); useEffect(() => { if (authContext.sessionId === undefined && authContext.userId === undefined) { diff --git a/packages/react/src/internal.ts b/packages/react/src/internal.ts index 938f3f4d9dd..3ee8e579cc6 100644 --- a/packages/react/src/internal.ts +++ b/packages/react/src/internal.ts @@ -1,6 +1,7 @@ export { setErrorThrowerOptions } from './errors/errorThrower'; export { MultisessionAppSupport } from './components/controlComponents'; export { useRoutingProps } from './hooks/useRoutingProps'; +export { useDerivedAuth } from './hooks/useAuth'; export { clerkJsScriptUrl, diff --git a/turbo.json b/turbo.json index d86965d7605..dd7a6d82cc3 100644 --- a/turbo.json +++ b/turbo.json @@ -24,7 +24,8 @@ "VERCEL", "VITE_CLERK_*", "EXPO_PUBLIC_CLERK_*", - "REACT_APP_CLERK_*" + "REACT_APP_CLERK_*", + "NEXT_PHASE" ], "globalPassThroughEnv": ["AWS_SECRET_KEY", "GITHUB_TOKEN", "ACTIONS_RUNNER_DEBUG", "ACTIONS_STEP_DEBUG"], "tasks": { From 2cbc8ac22914fc91b7d5f51696dfeb2f00b5fd8a Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Mon, 21 Oct 2024 23:23:04 -0700 Subject: [PATCH 7/7] changesets --- .changeset/gorgeous-suits-rush.md | 5 +++++ .changeset/two-bottles-report.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/gorgeous-suits-rush.md create mode 100644 .changeset/two-bottles-report.md diff --git a/.changeset/gorgeous-suits-rush.md b/.changeset/gorgeous-suits-rush.md new file mode 100644 index 00000000000..9cca69bf368 --- /dev/null +++ b/.changeset/gorgeous-suits-rush.md @@ -0,0 +1,5 @@ +--- +"@clerk/nextjs": major +--- + +Stop `` from opting applications into dynamic rendering. A new prop, `` can be used to opt-in to dynamic rendering and make auth data available during server-side rendering. The RSC `auth()` helper should be preferred for accessing auth data during dynamic rendering. diff --git a/.changeset/two-bottles-report.md b/.changeset/two-bottles-report.md new file mode 100644 index 00000000000..aecebdc39c1 --- /dev/null +++ b/.changeset/two-bottles-report.md @@ -0,0 +1,5 @@ +--- +"@clerk/clerk-react": minor +--- + +Internal changes to support ``