diff --git a/.changeset/empty-dots-confess.md b/.changeset/empty-dots-confess.md new file mode 100644 index 00000000000..971117f134d --- /dev/null +++ b/.changeset/empty-dots-confess.md @@ -0,0 +1,9 @@ +--- +'@clerk/elements': patch +'@clerk/nextjs': patch +'@clerk/shared': patch +'@clerk/types': patch +'@clerk/clerk-js': patch +--- + +Fixes issues in `ClerkRouter` that were causing inaccurate pathnames within Elements flows. Also fixes a dependency issue where `@clerk/elements` was pulling in the wrong version of `@clerk/shared`. diff --git a/package-lock.json b/package-lock.json index e099e17a10a..81486860d44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46243,7 +46243,6 @@ "version": "0.18.5", "license": "MIT", "dependencies": { - "@clerk/shared": "2.11.5", "@clerk/types": "^4.30.0", "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-slot": "^1.1.0", @@ -46254,6 +46253,7 @@ "devDependencies": { "@clerk/clerk-react": "5.15.1", "@clerk/eslint-config-custom": "*", + "@clerk/shared": "2.11.5", "@statelyai/inspect": "^0.4.0", "@types/node": "^18.19.33", "@types/react": "*", @@ -46269,6 +46269,8 @@ "node": ">=18.17.0" }, "peerDependencies": { + "@clerk/shared": "2.x", + "next": "^13.5.4 || ^14.0.3 || ^15", "react": "^18.0.0 || ^19.0.0-beta", "react-dom": "^18.0.0 || ^19.0.0-beta" }, diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index e149a29e5a4..c145b469095 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -3,7 +3,7 @@ { "path": "./dist/clerk.js", "maxSize": "707kB" }, { "path": "./dist/clerk.browser.js", "maxSize": "75kB" }, { "path": "./dist/clerk.headless.js", "maxSize": "48kB" }, - { "path": "./dist/ui-common*.js", "maxSize": "87KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "88KB" }, { "path": "./dist/vendors*.js", "maxSize": "70KB" }, { "path": "./dist/coinbase*.js", "maxSize": "58KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, diff --git a/packages/clerk-js/jest.setup.ts b/packages/clerk-js/jest.setup.ts index c9637424917..cf496bb4130 100644 --- a/packages/clerk-js/jest.setup.ts +++ b/packages/clerk-js/jest.setup.ts @@ -35,6 +35,7 @@ if (typeof window !== 'undefined') { global.__PKG_NAME__ = ''; global.__PKG_VERSION__ = ''; + global.BUILD_ENABLE_NEW_COMPONENTS = ''; //@ts-expect-error global.IntersectionObserver = class IntersectionObserver { diff --git a/packages/clerk-js/rspack.config.js b/packages/clerk-js/rspack.config.js index 2350b6a96a1..ac44ce8865e 100644 --- a/packages/clerk-js/rspack.config.js +++ b/packages/clerk-js/rspack.config.js @@ -37,6 +37,7 @@ const common = ({ mode }) => { __DEV__: isDevelopment(mode), __PKG_VERSION__: JSON.stringify(packageJSON.version), __PKG_NAME__: JSON.stringify(packageJSON.name), + BUILD_ENABLE_NEW_COMPONENTS: JSON.stringify(process.env.BUILD_ENABLE_NEW_COMPONENTS), }), new rspack.EnvironmentPlugin({ CLERK_ENV: mode, diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 350888a84a9..a6bf1255c37 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -340,12 +340,14 @@ export class Clerk implements ClerkInterface { this.#loaded = await this.#loadInNonStandardBrowser(); } - if (clerkIsLoaded(this)) { - this.__experimental_ui = new UI({ - router: this.#options.__experimental_router, - clerk: this, - options: this.#options, - }); + if (BUILD_ENABLE_NEW_COMPONENTS) { + if (clerkIsLoaded(this)) { + this.__experimental_ui = new UI({ + router: this.#options.__experimental_router, + clerk: this, + options: this.#options, + }); + } } }; diff --git a/packages/clerk-js/src/global.d.ts b/packages/clerk-js/src/global.d.ts index 4882a05c185..9ceed16624c 100644 --- a/packages/clerk-js/src/global.d.ts +++ b/packages/clerk-js/src/global.d.ts @@ -2,3 +2,5 @@ declare module '@clerk/ui/styles.css' { const content: string; export default content; } + +declare const BUILD_ENABLE_NEW_COMPONENTS: string; diff --git a/packages/clerk-js/src/ui/new/index.tsx b/packages/clerk-js/src/ui/new/index.tsx index d066810767e..3a8646928f3 100644 --- a/packages/clerk-js/src/ui/new/index.tsx +++ b/packages/clerk-js/src/ui/new/index.tsx @@ -33,21 +33,23 @@ export class UI { this.clerk = clerk; this.options = options; - // register components - this.register('SignIn', { - type: 'component', - load: () => - import(/* webpackChunkName: "rebuild--sign-in" */ '@clerk/ui/sign-in').then(({ SignIn }) => ({ - default: SignIn, - })), - }); - this.register('SignUp', { - type: 'component', - load: () => - import(/* webpackChunkName: "rebuild--sign-up" */ '@clerk/ui/sign-up').then(({ SignUp }) => ({ - default: SignUp, - })), - }); + if (BUILD_ENABLE_NEW_COMPONENTS) { + // register components + this.register('SignIn', { + type: 'component', + load: () => + import(/* webpackChunkName: "rebuild--sign-in" */ '@clerk/ui/sign-in').then(({ SignIn }) => ({ + default: SignIn, + })), + }); + this.register('SignUp', { + type: 'component', + load: () => + import(/* webpackChunkName: "rebuild--sign-up" */ '@clerk/ui/sign-up').then(({ SignUp }) => ({ + default: SignUp, + })), + }); + } } // Mount a component from the registry diff --git a/packages/clerk-js/turbo.json b/packages/clerk-js/turbo.json index c3dfec917c9..db709d21bcb 100644 --- a/packages/clerk-js/turbo.json +++ b/packages/clerk-js/turbo.json @@ -2,6 +2,7 @@ "extends": ["//"], "tasks": { "build": { + "env": ["BUILD_ENABLE_NEW_COMPONENTS"], "inputs": [ "*.d.ts", "bundlewatch.config.json", diff --git a/packages/elements/package.json b/packages/elements/package.json index 47c3ac1932f..0dc9435d509 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -71,7 +71,6 @@ "test:cache:clear": "jest --clearCache --useStderr" }, "dependencies": { - "@clerk/shared": "2.11.5", "@clerk/types": "^4.30.0", "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-slot": "^1.1.0", @@ -82,6 +81,7 @@ "devDependencies": { "@clerk/clerk-react": "5.15.1", "@clerk/eslint-config-custom": "*", + "@clerk/shared": "2.11.5", "@statelyai/inspect": "^0.4.0", "@types/node": "^18.19.33", "@types/react": "*", @@ -94,6 +94,8 @@ "typescript": "*" }, "peerDependencies": { + "@clerk/shared": "2.x", + "next": "^13.5.4 || ^14.0.3 || ^15", "react": "^18.0.0 || ^19.0.0-beta", "react-dom": "^18.0.0 || ^19.0.0-beta" }, diff --git a/packages/elements/src/internals/machines/third-party/third-party.actors.ts b/packages/elements/src/internals/machines/third-party/third-party.actors.ts index 050dca6ed2d..592f273814f 100644 --- a/packages/elements/src/internals/machines/third-party/third-party.actors.ts +++ b/packages/elements/src/internals/machines/third-party/third-party.actors.ts @@ -80,8 +80,8 @@ export const handleRedirectCallback = fromCallback { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const inferredBasePath = usePathnameWithoutCatchAll(); + + // The window.history APIs seem to prevent Next.js from triggering a full page re-render, allowing us to + // preserve internal state between steps. + const canUseWindowHistoryAPIs = + typeof window !== 'undefined' && window.next && window.next.version >= NEXT_WINDOW_HISTORY_SUPPORT_VERSION; + + return { + mode: 'path', + name: 'NextRouter', + push: (path: string) => router.push(path), + replace: (path: string) => + canUseWindowHistoryAPIs ? window.history.replaceState(null, '', path) : router.replace(path), + shallowPush(path: string) { + canUseWindowHistoryAPIs ? window.history.pushState(null, '', path) : router.push(path, {}); + }, + pathname: () => pathname, + searchParams: () => searchParams, + inferredBasePath: () => inferredBasePath, + }; +}; diff --git a/packages/elements/src/react/sign-in/root.tsx b/packages/elements/src/react/sign-in/root.tsx index ca39454dcb1..77ad01cd870 100644 --- a/packages/elements/src/react/sign-in/root.tsx +++ b/packages/elements/src/react/sign-in/root.tsx @@ -1,5 +1,4 @@ import { useClerk } from '@clerk/shared/react'; -import { useClerkHostRouter } from '@clerk/shared/router'; import { eventComponentMounted } from '@clerk/shared/telemetry'; import { useSelector } from '@xstate/react'; import React, { useEffect } from 'react'; @@ -10,11 +9,10 @@ import { FormStoreProvider, useFormStore } from '~/internals/machines/form/form. import type { SignInRouterInitEvent } from '~/internals/machines/sign-in'; import { SignInRouterMachine } from '~/internals/machines/sign-in'; import { inspect } from '~/internals/utils/inspector'; -import { Router, useClerkRouter, useVirtualRouter } from '~/react/router'; +import { ClerkHostRouterContext, Router, useClerkRouter, useNextRouter, useVirtualRouter } from '~/react/router'; import { SignInRouterCtx } from '~/react/sign-in/context'; import { Form } from '../common/form'; -import { removeOptionalCatchAllSegment } from '../utils/path-inference/utils'; type SignInFlowProviderProps = { children: React.ReactNode; @@ -118,9 +116,9 @@ export function SignInRoot({ routing = ROUTING.path, }: SignInRootProps): JSX.Element | null { const clerk = useClerk(); - const router = (routing === ROUTING.virtual ? useVirtualRouter : useClerkHostRouter)(); + const router = (routing === ROUTING.virtual ? useVirtualRouter : useNextRouter)(); const pathname = router.pathname(); - const inferredPath = removeOptionalCatchAllSegment(pathname); + const inferredPath = router.inferredBasePath?.(); const path = pathProp || inferredPath || SIGN_IN_DEFAULT_BASE_PATH; const isRootPath = path === pathname; @@ -134,19 +132,18 @@ export function SignInRoot({ ); return ( - - - - {children} - - - + + + + + {children} + + + + ); } diff --git a/packages/elements/src/react/sign-up/root.tsx b/packages/elements/src/react/sign-up/root.tsx index 7cca4bb5f78..dd21232525e 100644 --- a/packages/elements/src/react/sign-up/root.tsx +++ b/packages/elements/src/react/sign-up/root.tsx @@ -1,5 +1,4 @@ import { useClerk } from '@clerk/shared/react'; -import { useClerkHostRouter } from '@clerk/shared/router'; import { eventComponentMounted } from '@clerk/shared/telemetry'; import { useSelector } from '@xstate/react'; import { useEffect } from 'react'; @@ -10,11 +9,10 @@ import { FormStoreProvider, useFormStore } from '~/internals/machines/form/form. import type { SignUpRouterInitEvent } from '~/internals/machines/sign-up'; import { SignUpRouterMachine } from '~/internals/machines/sign-up'; import { inspect } from '~/internals/utils/inspector'; -import { Router, useClerkRouter, useVirtualRouter } from '~/react/router'; +import { ClerkHostRouterContext, Router, useClerkRouter, useNextRouter, useVirtualRouter } from '~/react/router'; import { SignUpRouterCtx } from '~/react/sign-up/context'; import { Form } from '../common/form'; -import { removeOptionalCatchAllSegment } from '../utils/path-inference/utils'; type SignUpFlowProviderProps = { children: React.ReactNode; @@ -117,9 +115,9 @@ export function SignUpRoot({ routing = ROUTING.path, }: SignUpRootProps): JSX.Element | null { const clerk = useClerk(); - const router = (routing === ROUTING.virtual ? useVirtualRouter : useClerkHostRouter)(); + const router = (routing === ROUTING.virtual ? useVirtualRouter : useNextRouter)(); const pathname = router.pathname(); - const inferredPath = removeOptionalCatchAllSegment(pathname); + const inferredPath = router.inferredBasePath?.(); const path = pathProp || inferredPath || SIGN_UP_DEFAULT_BASE_PATH; const isRootPath = path === pathname; @@ -133,19 +131,18 @@ export function SignUpRoot({ ); return ( - - - - {children} - - - + + + + + {children} + + + + ); } diff --git a/packages/nextjs/src/app-router/client/ClerkProvider.tsx b/packages/nextjs/src/app-router/client/ClerkProvider.tsx index 36cd10b4406..30919561609 100644 --- a/packages/nextjs/src/app-router/client/ClerkProvider.tsx +++ b/packages/nextjs/src/app-router/client/ClerkProvider.tsx @@ -1,7 +1,5 @@ 'use client'; import { ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react'; -import type { ClerkHostRouter } from '@clerk/shared/router'; -import { ClerkHostRouterContext } from '@clerk/shared/router'; import { useRouter } from 'next/navigation'; import React, { useEffect, useTransition } from 'react'; @@ -25,40 +23,9 @@ declare global { } } -// The version that Next added support for the window.history.pushState and replaceState APIs. -// ref: https://nextjs.org/blog/next-14-1#windowhistorypushstate-and-windowhistoryreplacestate -const NEXT_WINDOW_HISTORY_SUPPORT_VERSION = '14.1.0'; - -/** - * Clerk router integration with Next.js's router. - */ -const useNextRouter = (): ClerkHostRouter => { - const router = useRouter(); - - // The window.history APIs seem to prevent Next.js from triggering a full page re-render, allowing us to - // preserve internal state between steps. - const canUseWindowHistoryAPIs = - typeof window !== 'undefined' && window.next && window.next.version >= NEXT_WINDOW_HISTORY_SUPPORT_VERSION; - - return { - mode: 'path', - name: 'NextRouter', - push: (path: string) => router.push(path), - replace: (path: string) => - canUseWindowHistoryAPIs ? window.history.replaceState(null, '', path) : router.replace(path), - shallowPush(path: string) { - canUseWindowHistoryAPIs ? window.history.pushState(null, '', path) : router.push(path, {}); - }, - pathname: () => (typeof window !== 'undefined' ? window.location.pathname : ''), - searchParams: () => - typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : new URLSearchParams(), - }; -}; - export const ClientClerkProvider = (props: NextClerkProviderProps) => { const { __unstable_invokeMiddlewareOnAuthStateChange = true, children } = props; const router = useRouter(); - const clerkRouter = useNextRouter(); const push = useAwaitablePush(); const replace = useAwaitableReplace(); const [isPending, startTransition] = useTransition(); @@ -119,7 +86,6 @@ export const ClientClerkProvider = (props: NextClerkProviderProps) => { const mergedProps = mergeNextClerkPropsWithEnv({ ...props, - __experimental_router: clerkRouter, routerPush: push, routerReplace: replace, }); @@ -128,7 +94,7 @@ export const ClientClerkProvider = (props: NextClerkProviderProps) => { - {children} + {children} ); diff --git a/packages/nextjs/src/pages/ClerkProvider.tsx b/packages/nextjs/src/pages/ClerkProvider.tsx index 4e1bb09948e..1c15cd31fe8 100644 --- a/packages/nextjs/src/pages/ClerkProvider.tsx +++ b/packages/nextjs/src/pages/ClerkProvider.tsx @@ -1,7 +1,6 @@ import { ClerkProvider as ReactClerkProvider } from '@clerk/clerk-react'; // Override Clerk React error thrower to show that errors come from @clerk/nextjs import { setClerkJsLoadingErrorPackageName, setErrorThrowerOptions } from '@clerk/clerk-react/internal'; -import type { ClerkHostRouter } from '@clerk/shared/router'; import { useRouter } from 'next/router'; import React from 'react'; @@ -16,40 +15,9 @@ import { removeBasePath } from '../utils/removeBasePath'; setErrorThrowerOptions({ packageName: PACKAGE_NAME }); setClerkJsLoadingErrorPackageName(PACKAGE_NAME); -// The version that Next added support for the window.history.pushState and replaceState APIs. -// ref: https://nextjs.org/blog/next-14-1#windowhistorypushstate-and-windowhistoryreplacestate -const NEXT_WINDOW_HISTORY_SUPPORT_VERSION = '14.1.0'; - -/** - * Clerk router integration with Next.js's router. - */ -const useNextRouter = (): ClerkHostRouter => { - const router = useRouter(); - - // The window.history APIs seem to prevent Next.js from triggering a full page re-render, allowing us to - // preserve internal state between steps. - const canUseWindowHistoryAPIs = - typeof window !== 'undefined' && window.next && window.next.version >= NEXT_WINDOW_HISTORY_SUPPORT_VERSION; - - return { - mode: 'path', - name: 'NextRouter', - push: (path: string) => router.push(path), - replace: (path: string) => - canUseWindowHistoryAPIs ? window.history.replaceState(null, '', path) : router.replace(path), - shallowPush(path: string) { - canUseWindowHistoryAPIs ? window.history.pushState(null, '', path) : router.push(path, {}); - }, - pathname: () => (typeof window !== 'undefined' ? window.location.pathname : ''), - searchParams: () => - typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : new URLSearchParams(), - }; -}; - export function ClerkProvider({ children, ...props }: NextClerkProviderProps): JSX.Element { const { __unstable_invokeMiddlewareOnAuthStateChange = true } = props; const { push, replace } = useRouter(); - const clerkRouter = useNextRouter(); ReactClerkProvider.displayName = 'ReactClerkProvider'; useSafeLayoutEffect(() => { @@ -71,7 +39,6 @@ export function ClerkProvider({ children, ...props }: NextClerkProviderProps): J const replaceNavigate = (to: string) => replace(removeBasePath(to)); const mergedProps = mergeNextClerkPropsWithEnv({ ...props, - __experimental_router: clerkRouter, routerPush: navigate, routerReplace: replaceNavigate, }); diff --git a/packages/types/src/router.ts b/packages/types/src/router.ts index b159142ac7a..3a0cdad2354 100644 --- a/packages/types/src/router.ts +++ b/packages/types/src/router.ts @@ -11,4 +11,5 @@ export type ClerkHostRouter = { replace: (path: string) => void; searchParams: () => URLSearchParams; shallowPush: (path: string) => void; + inferredBasePath?: () => string; };