From 3b50b67bd40da33c9e36773aa05462717e9f44cc Mon Sep 17 00:00:00 2001 From: Bryce Kalow Date: Thu, 17 Oct 2024 14:58:49 -0500 Subject: [PATCH] feat(clerk-js): Render `@clerk/ui` (#4114) Co-authored-by: Alex Carpenter Co-authored-by: Dylan Staley <88163+dstaley@users.noreply.github.com> Co-authored-by: Nikos Douvlis Co-authored-by: panteliselef Co-authored-by: Jacek Radko --- .changeset/late-kiwis-warn.md | 5 + .changeset/nervous-guests-guess.md | 9 ++ .changeset/unlucky-steaks-protect.md | 5 + package-lock.json | 17 ++- packages/clerk-js/bundlewatch.config.json | 6 +- packages/clerk-js/global.d.ts | 4 - packages/clerk-js/package.json | 1 + packages/clerk-js/src/core/clerk.ts | 59 +++++++--- packages/clerk-js/src/global.d.ts | 4 + packages/clerk-js/src/ui/new/index.tsx | 98 ++++++++++++++++ packages/clerk-js/src/ui/new/renderer.tsx | 107 ++++++++++++++++++ packages/clerk-js/src/ui/new/types.ts | 12 ++ packages/clerk-js/webpack.config.js | 98 ++++++++++++++-- packages/elements/package.json | 3 +- .../elements/src/internals/constants/index.ts | 1 + .../third-party/third-party.actors.ts | 2 +- .../src/app-router/client/ClerkProvider.tsx | 14 +-- packages/nextjs/src/pages/ClerkProvider.tsx | 38 ++++++- packages/shared/src/router.ts | 3 +- packages/shared/src/router/router.ts | 18 +-- packages/shared/src/router/types.ts | 1 - packages/types/src/clerk.ts | 14 +++ packages/types/src/index.ts | 1 + packages/types/src/router.ts | 14 +++ packages/ui/package.json | 10 +- packages/ui/src/common/phone-number-field.tsx | 2 +- packages/ui/src/common/router-link.tsx | 28 +++++ .../ui/src/components/sign-in/sign-in.tsx | 10 +- .../sign-in/steps/choose-session.tsx | 2 +- .../sign-in/steps/choose-strategy.tsx | 4 +- .../ui/src/components/sign-in/steps/start.tsx | 12 +- .../ui/src/components/sign-up/sign-up.tsx | 21 ++-- .../src/components/sign-up/steps/continue.tsx | 12 +- .../ui/src/components/sign-up/steps/start.tsx | 12 +- .../sign-up/steps/verifications.tsx | 2 +- .../ui/src/contexts/AppearanceContext.tsx | 5 +- packages/ui/src/hooks/use-display-config.ts | 2 +- .../ui/src/hooks/use-enabled-connections.ts | 2 +- packages/ui/src/hooks/use-environment.ts | 2 +- packages/ui/src/hooks/use-options.ts | 9 +- .../ui/src/hooks/use-reset-password-factor.ts | 2 +- packages/ui/src/primitives/card.tsx | 34 +++++- packages/ui/src/themes/full.ts | 3 +- packages/ui/src/themes/layout.ts | 3 +- packages/ui/tsup.config.ts | 2 +- 45 files changed, 606 insertions(+), 107 deletions(-) create mode 100644 .changeset/late-kiwis-warn.md create mode 100644 .changeset/nervous-guests-guess.md create mode 100644 .changeset/unlucky-steaks-protect.md delete mode 100644 packages/clerk-js/global.d.ts create mode 100644 packages/clerk-js/src/global.d.ts create mode 100644 packages/clerk-js/src/ui/new/index.tsx create mode 100644 packages/clerk-js/src/ui/new/renderer.tsx create mode 100644 packages/clerk-js/src/ui/new/types.ts delete mode 100644 packages/shared/src/router/types.ts create mode 100644 packages/types/src/router.ts create mode 100644 packages/ui/src/common/router-link.tsx diff --git a/.changeset/late-kiwis-warn.md b/.changeset/late-kiwis-warn.md new file mode 100644 index 0000000000..6ab9fa5a6b --- /dev/null +++ b/.changeset/late-kiwis-warn.md @@ -0,0 +1,5 @@ +--- +"@clerk/elements": patch +--- + +Remove @clerk/clerk-react as a dev depedency. Move @clerk/shared to depedencies (previously devDepedencies). diff --git a/.changeset/nervous-guests-guess.md b/.changeset/nervous-guests-guess.md new file mode 100644 index 0000000000..2e2d2d3586 --- /dev/null +++ b/.changeset/nervous-guests-guess.md @@ -0,0 +1,9 @@ +--- +"@clerk/clerk-js": minor +"@clerk/elements": minor +"@clerk/nextjs": minor +"@clerk/shared": minor +"@clerk/types": minor +--- + +Add experimental support for new UI components diff --git a/.changeset/unlucky-steaks-protect.md b/.changeset/unlucky-steaks-protect.md new file mode 100644 index 0000000000..e5a01da6c2 --- /dev/null +++ b/.changeset/unlucky-steaks-protect.md @@ -0,0 +1,5 @@ +--- +"@clerk/types": patch +--- + +Fix `SignInProps`/`SignUpProps` `__experimental` type to allow for arbitrary properties diff --git a/package-lock.json b/package-lock.json index a49c038f1e..cb1aac77c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44318,6 +44318,7 @@ "@clerk/localizations": "3.3.0", "@clerk/shared": "2.9.2", "@clerk/types": "4.26.0", + "@clerk/ui": "0.1.9", "@coinbase/wallet-sdk": "4.0.4", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.1", @@ -44864,6 +44865,7 @@ "version": "0.16.2", "license": "MIT", "dependencies": { + "@clerk/shared": "2.9.2", "@clerk/types": "^4.26.0", "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-slot": "^1.1.0", @@ -44874,7 +44876,6 @@ "devDependencies": { "@clerk/clerk-react": "5.12.0", "@clerk/eslint-config-custom": "*", - "@clerk/shared": "2.9.2", "@statelyai/inspect": "^0.4.0", "@types/node": "^18.19.33", "@types/react": "*", @@ -44890,7 +44891,6 @@ "node": ">=18.17.0" }, "peerDependencies": { - "@clerk/shared": "^2.0.0", "react": "^18.0.0 || ^19.0.0-beta", "react-dom": "^18.0.0 || ^19.0.0-beta" }, @@ -44900,6 +44900,18 @@ } } }, + "packages/elements/node_modules/@clerk/shared/node_modules/@clerk/types": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.25.1.tgz", + "integrity": "sha512-ILvR2YXz6BSGXDoozBAd2BGj8ZF/FQrfWQd0FtLz1JXt1vurkgRncAhEcC427OiXNRnq5R2Pn++urfFEa0PqYA==", + "extraneous": true, + "dependencies": { + "csstype": "3.1.1" + }, + "engines": { + "node": ">=18.17.0" + } + }, "packages/elements/node_modules/@next/env": { "version": "14.2.4", "dev": true, @@ -47959,7 +47971,6 @@ "version": "0.1.9", "license": "MIT", "dependencies": { - "@clerk/clerk-react": "file:../react", "@clerk/elements": "file:../elements", "@clerk/shared": "file:../shared", "@clerk/types": "file:../types", diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 30ee61bacb..7e0a980381 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,7 +1,7 @@ { "files": [ - { "path": "./dist/clerk.browser.js", "maxSize": "65.5kB" }, - { "path": "./dist/clerk.headless.js", "maxSize": "43kB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "68kB" }, + { "path": "./dist/clerk.headless.js", "maxSize": "44kB" }, { "path": "./dist/ui-common*.js", "maxSize": "86KB" }, { "path": "./dist/vendors*.js", "maxSize": "70KB" }, { "path": "./dist/coinbase*.js", "maxSize": "58KB" }, @@ -15,6 +15,6 @@ { "path": "./dist/userbutton*.js", "maxSize": "5KB" }, { "path": "./dist/userprofile*.js", "maxSize": "15KB" }, { "path": "./dist/userverification*.js", "maxSize": "5KB" }, - { "path": "./dist/onetap*.js", "maxSize": "1KB" } + { "path": "./dist/onetap*.js", "maxSize": "2KB" } ] } diff --git a/packages/clerk-js/global.d.ts b/packages/clerk-js/global.d.ts deleted file mode 100644 index 4684851c62..0000000000 --- a/packages/clerk-js/global.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module '*.module.scss' { - const content: Record; - export default content; -} diff --git a/packages/clerk-js/package.json b/packages/clerk-js/package.json index 4904c807c7..c89672de83 100644 --- a/packages/clerk-js/package.json +++ b/packages/clerk-js/package.json @@ -53,6 +53,7 @@ "@clerk/localizations": "3.3.0", "@clerk/shared": "2.9.2", "@clerk/types": "4.26.0", + "@clerk/ui": "0.1.9", "@coinbase/wallet-sdk": "4.0.4", "@emotion/cache": "11.11.0", "@emotion/react": "11.11.1", diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index dc4b101985..5ed5a1034b 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -37,6 +37,7 @@ import type { HandleOAuthCallbackParams, InstanceType, ListenerCallback, + LoadedClerk, NavigateOptions, OrganizationListProps, OrganizationProfileProps, @@ -64,6 +65,7 @@ import type { } from '@clerk/types'; import type { MountComponentRenderer } from '../ui/Components'; +import { UI } from '../ui/new'; import { ALLOWED_PROTOCOLS, buildURL, @@ -145,7 +147,12 @@ const defaultOptions: ClerkOptions = { signUpForceRedirectUrl: undefined, }; +function clerkIsLoaded(clerk: ClerkInterface): clerk is LoadedClerk { + return !!clerk.client; +} + export class Clerk implements ClerkInterface { + public __experimental_ui?: UI; public static mountComponentRenderer?: MountComponentRenderer; public static version: string = __PKG_VERSION__; @@ -317,6 +324,14 @@ export class Clerk implements ClerkInterface { } else { this.#loaded = await this.#loadInNonStandardBrowser(); } + + if (clerkIsLoaded(this)) { + this.__experimental_ui = new UI({ + router: this.#options.__experimental_router, + clerk: this, + options: this.#options, + }); + } }; public signOut: SignOut = async (callbackOrOptions?: SignOutCallback | SignOutOptions, options?: SignOutOptions) => { @@ -495,15 +510,19 @@ export class Clerk implements ClerkInterface { }; public mountSignIn = (node: HTMLDivElement, props?: SignInProps): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted({ preloadHint: 'SignIn' }).then(controls => - controls.mountComponent({ - name: 'SignIn', - appearanceKey: 'signIn', - node, - props, - }), - ); + if (props && props.__experimental?.newComponents && this.__experimental_ui) { + this.__experimental_ui.mount('SignIn', node, props); + } else { + this.assertComponentsReady(this.#componentControls); + void this.#componentControls.ensureMounted({ preloadHint: 'SignIn' }).then(controls => + controls.mountComponent({ + name: 'SignIn', + appearanceKey: 'signIn', + node, + props, + }), + ); + } this.telemetry?.record(eventPrebuiltComponentMounted('SignIn', props)); }; @@ -517,15 +536,19 @@ export class Clerk implements ClerkInterface { }; public mountSignUp = (node: HTMLDivElement, props?: SignUpProps): void => { - this.assertComponentsReady(this.#componentControls); - void this.#componentControls.ensureMounted({ preloadHint: 'SignUp' }).then(controls => - controls.mountComponent({ - name: 'SignUp', - appearanceKey: 'signUp', - node, - props, - }), - ); + if (props && props.__experimental?.newComponents && this.__experimental_ui) { + this.__experimental_ui.mount('SignUp', node, props); + } else { + this.assertComponentsReady(this.#componentControls); + void this.#componentControls.ensureMounted({ preloadHint: 'SignUp' }).then(controls => + controls.mountComponent({ + name: 'SignUp', + appearanceKey: 'signUp', + node, + props, + }), + ); + } this.telemetry?.record(eventPrebuiltComponentMounted('SignUp', props)); }; diff --git a/packages/clerk-js/src/global.d.ts b/packages/clerk-js/src/global.d.ts new file mode 100644 index 0000000000..4882a05c18 --- /dev/null +++ b/packages/clerk-js/src/global.d.ts @@ -0,0 +1,4 @@ +declare module '@clerk/ui/styles.css' { + const content: string; + export default content; +} diff --git a/packages/clerk-js/src/ui/new/index.tsx b/packages/clerk-js/src/ui/new/index.tsx new file mode 100644 index 0000000000..42b8614102 --- /dev/null +++ b/packages/clerk-js/src/ui/new/index.tsx @@ -0,0 +1,98 @@ +import { createDeferredPromise } from '@clerk/shared'; +import type { ClerkHostRouter } from '@clerk/shared/router'; +import type { ClerkOptions, LoadedClerk } from '@clerk/types'; + +import type { init } from './renderer'; +import type { ClerkNewComponents, ComponentDefinition } from './types'; + +function assertRouter(router: ClerkHostRouter | undefined): asserts router is ClerkHostRouter { + if (!router) { + throw new Error(`Clerk: Attempted to use functionality that requires the "router" option to be provided to Clerk.`); + } +} + +export class UI { + router?: ClerkHostRouter; + clerk: LoadedClerk; + options: ClerkOptions; + componentRegistry = new Map(); + + #rendererPromise?: ReturnType; + #renderer?: ReturnType; + + constructor({ + router, + clerk, + options, + }: { + router: ClerkHostRouter | undefined; + clerk: LoadedClerk; + options: ClerkOptions; + }) { + this.router = router; + 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, + })), + }); + } + + // Mount a component from the registry + mount(componentName: C, node: HTMLElement, props: ClerkNewComponents[C]): void { + const component = this.componentRegistry.get(componentName); + if (!component) { + throw new Error(`clerk/ui: Unable to find component definition for ${componentName}`); + } + + // immediately start loading the component + component.load(); + + this.renderer() + .then(() => { + this.#renderer?.mount(this.#renderer.createElementFromComponentDefinition(component), props, node); + }) + .catch(err => { + console.error(`clerk/ui: Error mounting component ${componentName}:`, err); + }); + } + + unmount(node: HTMLElement) { + this.#renderer?.unmount(node); + } + + // Registers a component for rendering later + register(componentName: string, componentDefinition: ComponentDefinition) { + this.componentRegistry.set(componentName, componentDefinition); + } + + renderer() { + if (this.#rendererPromise) { + return this.#rendererPromise.promise; + } + + this.#rendererPromise = createDeferredPromise(); + + import('./renderer').then(({ init, wrapperInit }) => { + assertRouter(this.router); + this.#renderer = init({ + wrapper: wrapperInit({ clerk: this.clerk, options: this.options, router: this.router }), + }); + this.#rendererPromise?.resolve(); + }); + + return this.#rendererPromise.promise; + } +} diff --git a/packages/clerk-js/src/ui/new/renderer.tsx b/packages/clerk-js/src/ui/new/renderer.tsx new file mode 100644 index 0000000000..95358f25f3 --- /dev/null +++ b/packages/clerk-js/src/ui/new/renderer.tsx @@ -0,0 +1,107 @@ +import { ClerkInstanceContext, OptionsContext } from '@clerk/shared/react'; +import type { ClerkHostRouter } from '@clerk/shared/router'; +import { ClerkHostRouterContext } from '@clerk/shared/router'; +import type { ClerkOptions, LoadedClerk } from '@clerk/types'; +import stylesheetURLOrContent from '@clerk/ui/styles.css'; +import type { ElementType, ReactNode } from 'react'; +import { createElement, lazy } from 'react'; +import { createPortal } from 'react-dom'; +import { createRoot } from 'react-dom/client'; + +import type { ComponentDefinition } from './types'; + +const ROOT_ELEMENT_ID = 'clerk-components-new'; + +export function wrapperInit({ + clerk, + options, + router, +}: { + clerk: LoadedClerk; + options: ClerkOptions; + router: ClerkHostRouter; +}) { + return function Wrapper({ children }: { children: ReactNode }) { + return ( + + + {children} + + + ); + }; +} + +// Initializes the react renderer +export function init({ wrapper }: { wrapper: ElementType }) { + const renderedComponents = new Map]>(); + let rootElement = document.getElementById(ROOT_ELEMENT_ID); + + if (!rootElement) { + rootElement = document.createElement('div'); + rootElement.setAttribute('id', 'clerk-components'); + document.body.appendChild(rootElement); + + // Just for completeness, we check to see if we've already added the stylesheet to the DOM. + const STYLESHEET_SIGIL = 'data-clerk-injected-styles'; + const existingStylesheet = document.querySelector(`[${STYLESHEET_SIGIL}]`); + if (!existingStylesheet) { + let stylesheet: HTMLLinkElement | HTMLStyleElement; + + if (stylesheetURLOrContent.endsWith('.css')) { + // stylesheetURLOrContent is a URL to a stylesheet + stylesheet = document.createElement('link'); + (stylesheet as HTMLLinkElement).href = stylesheetURLOrContent; + (stylesheet as HTMLLinkElement).rel = 'stylesheet'; + } else { + // stylesheetURLOrContent is CSS + stylesheet = document.createElement('style'); + stylesheet.textContent = stylesheetURLOrContent; + } + + stylesheet.setAttribute(STYLESHEET_SIGIL, ''); + // Add as first stylesheet so that application styles take precedence over our styles. + document.head.prepend(stylesheet); + } + } + + const root = createRoot(rootElement); + + // (re-)renders the render wrapper, rendering any components present in the `renderedComponents` map. + // React's render function retains state, so it's safe to call multiple times as additional components are mounted and unmounted. + function render() { + root.render( + createElement( + wrapper, + null, + Array.from(renderedComponents.entries()).map(([node, [element, props]]) => + createPortal(createElement(element, props), node), + ), + ), + ); + } + + function mount(element: ElementType, props: any, node: HTMLElement) { + renderedComponents.set(node, [element, props]); + render(); + } + + function unmount(node: HTMLElement) { + if (!renderedComponents.has(node)) { + return; + } + + renderedComponents.delete(node); + render(); + } + + function createElementFromComponentDefinition(componentDefinition: ComponentDefinition) { + return lazy(componentDefinition.load); + } + + return { + mount, + unmount, + createElementFromComponentDefinition, + }; +} diff --git a/packages/clerk-js/src/ui/new/types.ts b/packages/clerk-js/src/ui/new/types.ts new file mode 100644 index 0000000000..ef5a328196 --- /dev/null +++ b/packages/clerk-js/src/ui/new/types.ts @@ -0,0 +1,12 @@ +import type { SignInProps, SignUpProps } from '@clerk/types'; +import type { ComponentType } from 'react'; + +export interface ComponentDefinition { + type: 'component' | 'modal'; + load: () => Promise<{ default: ComponentType }>; +} + +export type ClerkNewComponents = { + SignIn: SignInProps; + SignUp: SignUpProps; +}; diff --git a/packages/clerk-js/webpack.config.js b/packages/clerk-js/webpack.config.js index 4e9d2382b1..c74ce2e750 100644 --- a/packages/clerk-js/webpack.config.js +++ b/packages/clerk-js/webpack.config.js @@ -27,6 +27,7 @@ const variantToSourceFile = { /** @returns { import('webpack').Configuration } */ const common = ({ mode }) => { + /** @type { import('webpack').Configuration } */ return { mode, resolve: { @@ -75,7 +76,11 @@ const common = ({ mode }) => { minChunks: 1, name: 'ui-common', priority: -20, - test: module => module.resource && !module.resource.includes('/ui/components'), + test: module => + module.resource && + !module.resource.includes('/ui/components') && + !module.resource.includes('packages/elements') && + !module.resource.includes('packages/ui'), }, defaultVendors: { minChunks: 1, @@ -83,6 +88,21 @@ const common = ({ mode }) => { name: 'vendors', priority: -10, }, + commonNew: { + minChunks: 2, + name: 'common-new', + chunks(chunk) { + return chunk.name?.startsWith('rebuild--'); + }, + priority: 0, + }, + react: { + chunks: 'all', + test: /[\\/]node_modules[\\/](react-dom|scheduler)[\\/]/, + name: 'framework', + priority: 40, + enforce: true, + }, }, }, }, @@ -150,13 +170,58 @@ const typescriptLoaderDev = () => { }; }; +/** + * Used in outputs that utilize chunking, and returns a URL to the stylesheet. + * @type { () => (import('webpack').RuleSetRule) } + */ +const clerkUICSSLoader = () => { + // This emits a module exporting a URL to the styles.css file. + return { + test: /packages\/ui\/dist\/styles\.css/, + type: 'asset/resource', + }; +}; + +/** + * Used in outputs that _do not_ utilize chunking, and returns the contents of the stylesheet. + * @type { () => (import('webpack').RuleSetRule) } + */ +const clerkUICSSSourceLoader = () => { + // This emits a module exporting the contents of the styles.css file. + return { + test: /packages\/ui\/dist\/styles\.css/, + type: 'asset/source', + }; +}; + +/** + * Used for production builds that have dynamicly loaded chunks. + * @type { () => (import('webpack').Configuration) } + * */ +const commonForProdChunked = () => { + return { + module: { + rules: [svgLoader(), typescriptLoaderProd(), clerkUICSSLoader()], + }, + }; +}; + +/** + * Used for production builds that combine all files into one single file (such as for Chrome Extensions). + * @type { () => (import('webpack').Configuration) } + * */ +const commonForProdBundled = () => { + return { + module: { + rules: [svgLoader(), typescriptLoaderProd(), clerkUICSSSourceLoader()], + }, + }; +}; + /** @type { () => (import('webpack').Configuration) } */ const commonForProd = () => { return { devtool: undefined, - module: { - rules: [svgLoader(), typescriptLoaderProd()], - }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].js', @@ -170,6 +235,8 @@ const commonForProd = () => { new TerserPlugin({ terserOptions: { compress: { + unused: true, + dead_code: true, passes: 2, }, mangle: { @@ -207,12 +274,18 @@ const entryForVariant = variant => { /** @type { () => (import('webpack').Configuration)[] } */ const prodConfig = ({ mode }) => { - const clerkBrowser = merge(entryForVariant(variants.clerkBrowser), common({ mode }), commonForProd()); + const clerkBrowser = merge( + entryForVariant(variants.clerkBrowser), + common({ mode }), + commonForProd(), + commonForProdChunked(), + ); const clerkHeadless = merge( entryForVariant(variants.clerkHeadless), common({ mode }), commonForProd(), + commonForProdChunked(), // Disable chunking for the headless variant, since it's meant to be used in a non-browser environment and // attempting to load chunks causes issues due to usage of a dynamic publicPath. We generally are only concerned with // chunking in our browser bundles. @@ -231,10 +304,11 @@ const prodConfig = ({ mode }) => { entryForVariant(variants.clerkHeadlessBrowser), common({ mode }), commonForProd(), + commonForProdChunked(), // externalsForHeadless(), ); - const clerkEsm = merge(entryForVariant(variants.clerk), common({ mode }), commonForProd(), { + const clerkEsm = merge(entryForVariant(variants.clerk), common({ mode }), commonForProd(), commonForProdBundled(), { experiments: { outputModule: true, }, @@ -252,11 +326,19 @@ const prodConfig = ({ mode }) => { ], }); - const clerkCjs = merge(clerkEsm, { + const clerkCjs = merge(entryForVariant(variants.clerk), common({ mode }), commonForProd(), commonForProdBundled(), { output: { filename: '[name].js', libraryTarget: 'commonjs', }, + plugins: [ + // Include the lazy chunks in the bundle as well + // so that the final bundle can be imported and bundled again + // by a different bundler, eg the webpack instance used by react-scripts + new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1, + }), + ], }); return [clerkBrowser, clerkHeadless, clerkHeadlessBrowser, clerkEsm, clerkCjs]; @@ -271,7 +353,7 @@ const devConfig = ({ mode, env }) => { const commonForDev = () => { return { module: { - rules: [svgLoader(), typescriptLoaderDev()], + rules: [svgLoader(), typescriptLoaderDev(), clerkUICSSLoader()], }, plugins: [ new ReactRefreshWebpackPlugin({ overlay: { sockHost: devUrl.host } }), diff --git a/packages/elements/package.json b/packages/elements/package.json index 3898162fef..e46d3c6f46 100644 --- a/packages/elements/package.json +++ b/packages/elements/package.json @@ -71,6 +71,7 @@ "test:cache:clear": "jest --clearCache --useStderr" }, "dependencies": { + "@clerk/shared": "2.9.2", "@clerk/types": "^4.26.0", "@radix-ui/react-form": "^0.1.0", "@radix-ui/react-slot": "^1.1.0", @@ -81,7 +82,6 @@ "devDependencies": { "@clerk/clerk-react": "5.12.0", "@clerk/eslint-config-custom": "*", - "@clerk/shared": "2.9.2", "@statelyai/inspect": "^0.4.0", "@types/node": "^18.19.33", "@types/react": "*", @@ -94,7 +94,6 @@ "typescript": "*" }, "peerDependencies": { - "@clerk/shared": "^2.0.0", "react": "^18.0.0 || ^19.0.0-beta", "react-dom": "^18.0.0 || ^19.0.0-beta" }, diff --git a/packages/elements/src/internals/constants/index.ts b/packages/elements/src/internals/constants/index.ts index 9e7dc36875..9ec7e3cc2d 100644 --- a/packages/elements/src/internals/constants/index.ts +++ b/packages/elements/src/internals/constants/index.ts @@ -58,6 +58,7 @@ export const ERROR_CODES = { export const ROUTING = { path: 'path', virtual: 'virtual', + hash: 'hash', } as const; export type ROUTING = (typeof ROUTING)[keyof typeof ROUTING]; 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 519e2153f0..050dca6ed2 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 @@ -74,7 +74,7 @@ export const handleRedirectCallback = fromCallback { +const useNextRouter = (): ClerkHostRouter => { const router = useRouter(); - const pathname = usePathname(); - // eslint-disable-next-line react-hooks/rules-of-hooks -- The order doesn't differ between renders as we're checking the execution environment. - const searchParams = typeof window === 'undefined' ? new URLSearchParams() : useSearchParams(); // The window.history APIs seem to prevent Next.js from triggering a full page re-render, allowing us to // preserve internal state between steps. @@ -52,8 +49,8 @@ export const useNextRouter = (): ClerkHostRouter => { shallowPush(path: string) { canUseWindowHistoryAPIs ? window.history.pushState(null, '', path) : router.push(path, {}); }, - pathname: () => pathname, - searchParams: () => searchParams, + pathname: () => window.location.pathname, + searchParams: () => new URLSearchParams(window.location.search), }; }; @@ -113,6 +110,7 @@ export const ClientClerkProvider = (props: NextClerkProviderProps) => { const mergedProps = mergeNextClerkPropsWithEnv({ ...props, + __experimental_router: clerkRouter, routerPush: push, routerReplace: replace, }); diff --git a/packages/nextjs/src/pages/ClerkProvider.tsx b/packages/nextjs/src/pages/ClerkProvider.tsx index 0f7c71d3f2..1da9a7c725 100644 --- a/packages/nextjs/src/pages/ClerkProvider.tsx +++ b/packages/nextjs/src/pages/ClerkProvider.tsx @@ -1,6 +1,7 @@ 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'; @@ -15,9 +16,39 @@ 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: () => window.location.pathname, + searchParams: () => new URLSearchParams(window.location.search), + }; +}; + export function ClerkProvider({ children, ...props }: NextClerkProviderProps): JSX.Element { const { __unstable_invokeMiddlewareOnAuthStateChange = true } = props; const { push, replace } = useRouter(); + const clerkRouter = useNextRouter(); ReactClerkProvider.displayName = 'ReactClerkProvider'; useSafeLayoutEffect(() => { @@ -37,7 +68,12 @@ export function ClerkProvider({ children, ...props }: NextClerkProviderProps): J const navigate = (to: string) => push(removeBasePath(to)); const replaceNavigate = (to: string) => replace(removeBasePath(to)); - const mergedProps = mergeNextClerkPropsWithEnv({ ...props, routerPush: navigate, routerReplace: replaceNavigate }); + const mergedProps = mergeNextClerkPropsWithEnv({ + ...props, + __experimental_router: clerkRouter, + routerPush: navigate, + routerReplace: replaceNavigate, + }); // ClerkProvider automatically injects __clerk_ssr_state // getAuth returns a user-facing authServerSideProps that hides __clerk_ssr_state // @ts-expect-error initialState is hidden from the types as it's a private prop diff --git a/packages/shared/src/router.ts b/packages/shared/src/router.ts index 32dc0abf2e..b842f0ad05 100644 --- a/packages/shared/src/router.ts +++ b/packages/shared/src/router.ts @@ -1,5 +1,4 @@ -export { type ClerkRouter, type ClerkHostRouter, createClerkRouter } from './router/router'; -export { type RoutingMode } from './router/types'; +export { type ClerkRouter, type ClerkHostRouter, type RoutingMode, createClerkRouter } from './router/router'; export { Router, useClerkRouter, diff --git a/packages/shared/src/router/router.ts b/packages/shared/src/router/router.ts index dca2e40946..e8c1696922 100644 --- a/packages/shared/src/router/router.ts +++ b/packages/shared/src/router/router.ts @@ -1,21 +1,9 @@ +import type { ClerkHostRouter, RoutingMode } from '@clerk/types'; + import { isAbsoluteUrl, withLeadingSlash, withoutTrailingSlash } from '../url'; -import type { RoutingMode } from './types'; export const PRESERVED_QUERYSTRING_PARAMS = ['after_sign_in_url', 'after_sign_up_url', 'redirect_url']; -/** - * This type represents a generic router interface that Clerk relies on to interact with the host router. - */ -export type ClerkHostRouter = { - readonly mode: RoutingMode; - readonly name: string; - pathname: () => string; - push: (path: string) => void; - replace: (path: string) => void; - searchParams: () => URLSearchParams; - shallowPush: (path: string) => void; -}; - /** * Internal Clerk router, used by Clerk components to interact with the host's router. */ @@ -156,3 +144,5 @@ export function createClerkRouter(router: ClerkHostRouter, basePath: string = '/ basePath: normalizedBasePath, }; } + +export type { ClerkHostRouter, RoutingMode }; diff --git a/packages/shared/src/router/types.ts b/packages/shared/src/router/types.ts deleted file mode 100644 index d0f948e71d..0000000000 --- a/packages/shared/src/router/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type RoutingMode = 'path' | 'virtual'; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 7f7e11cd13..841ddb1e25 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -32,6 +32,7 @@ import type { SignUpFallbackRedirectUrl, SignUpForceRedirectUrl, } from './redirects'; +import type { ClerkHostRouter } from './router'; import type { ActiveSessionResource } from './session'; import type { __experimental_SessionVerificationLevel } from './sessionVerification'; import type { SignInResource } from './signIn'; @@ -701,6 +702,11 @@ export type ClerkOptions = ClerkOptionsNavigation & }, Record >; + + /** + * [EXPERIMENTAL] Provide the underlying host router, required for the new experimental UI components. + */ + __experimental_router?: ClerkHostRouter; }; export interface NavigateOptions { @@ -836,6 +842,10 @@ export type SignInProps = RoutingOptions & { * Initial values that are used to prefill the sign in form. */ initialValues?: SignInInitialValues; + /** + * Enable experimental flags to gain access to new features. These flags are not guaranteed to be stable and may change drastically in between patch or minor versions. + */ + __experimental?: Record & { newComponents?: boolean }; } & TransferableOption & SignUpForceRedirectUrl & SignUpFallbackRedirectUrl & @@ -942,6 +952,10 @@ export type SignUpProps = RoutingOptions & { * Initial values that are used to prefill the sign up form. */ initialValues?: SignUpInitialValues; + /** + * Enable experimental flags to gain access to new features. These flags are not guaranteed to be stable and may change drastically in between patch or minor versions. + */ + __experimental?: Record & { newComponents?: boolean }; } & SignInFallbackRedirectUrl & SignInForceRedirectUrl & LegacyRedirectProps & diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 482d807e42..b8ccf135b7 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -36,6 +36,7 @@ export * from './phoneNumber'; export * from './redirects'; export * from './resource'; export * from './role'; +export * from './router'; export * from './saml'; export * from './samlAccount'; export * from './session'; diff --git a/packages/types/src/router.ts b/packages/types/src/router.ts new file mode 100644 index 0000000000..b159142ac7 --- /dev/null +++ b/packages/types/src/router.ts @@ -0,0 +1,14 @@ +export type RoutingMode = 'path' | 'virtual'; + +/** + * This type represents a generic router interface that Clerk relies on to interact with the host router. + */ +export type ClerkHostRouter = { + readonly mode: RoutingMode; + readonly name: string; + pathname: () => string; + push: (path: string) => void; + replace: (path: string) => void; + searchParams: () => URLSearchParams; + shallowPush: (path: string) => void; +}; diff --git a/packages/ui/package.json b/packages/ui/package.json index 4c18ca7e07..fab9ec5c55 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,7 +1,6 @@ { "name": "@clerk/ui", "version": "0.1.9", - "private": true, "repository": { "type": "git", "url": "git+https://github.com/clerk/javascript.git", @@ -13,6 +12,9 @@ "email": "support@clerk.com", "url": "git+https://github.com/clerk/javascript.git" }, + "sideEffects": [ + "*.css" + ], "exports": { "./contexts": { "import": { @@ -27,13 +29,16 @@ "./*": { "import": { "types": "./dist/components/*.d.mts", + "browser": "./dist/components/*.mjs", "default": "./dist/components/*.mjs" }, "require": { "types": "./dist/components/*.d.ts", + "browser": "./dist/components/*.js", "default": "./dist/components/*.js" } - } + }, + "./styles.css": "./dist/styles.css" }, "main": "index.js", "files": [ @@ -47,7 +52,6 @@ "test": "vitest" }, "dependencies": { - "@clerk/clerk-react": "file:../react", "@clerk/elements": "file:../elements", "@clerk/shared": "file:../shared", "@clerk/types": "file:../types", diff --git a/packages/ui/src/common/phone-number-field.tsx b/packages/ui/src/common/phone-number-field.tsx index 7599f413d5..0ae7a55a87 100644 --- a/packages/ui/src/common/phone-number-field.tsx +++ b/packages/ui/src/common/phone-number-field.tsx @@ -1,5 +1,5 @@ -import { useClerk } from '@clerk/clerk-react'; import * as Common from '@clerk/elements/common'; +import { useClerk } from '@clerk/shared/react'; import { Command } from 'cmdk'; import { cx } from 'cva'; import * as React from 'react'; diff --git a/packages/ui/src/common/router-link.tsx b/packages/ui/src/common/router-link.tsx new file mode 100644 index 0000000000..944de7df28 --- /dev/null +++ b/packages/ui/src/common/router-link.tsx @@ -0,0 +1,28 @@ +import { useClerkHostRouter } from '@clerk/shared/router'; +import { Slot } from '@radix-ui/react-slot'; +import * as React from 'react'; + +export const RouterLink = React.forwardRef< + HTMLAnchorElement, + React.AnchorHTMLAttributes & { + asChild?: boolean; + } +>(function RouterLink({ asChild, children, href, ...props }, forwardedRef) { + const router = useClerkHostRouter(); + const Comp = asChild ? Slot : 'a'; + return ( + { + e.preventDefault(); + if (href) { + router.push(href); + } + }} + > + {children} + + ); +}); diff --git a/packages/ui/src/components/sign-in/sign-in.tsx b/packages/ui/src/components/sign-in/sign-in.tsx index 2d24dc89aa..28ca14f01f 100644 --- a/packages/ui/src/components/sign-in/sign-in.tsx +++ b/packages/ui/src/components/sign-in/sign-in.tsx @@ -1,4 +1,5 @@ import { Root as SignInRoot } from '@clerk/elements/sign-in'; +import type { SignInProps } from '@clerk/types'; import * as React from 'react'; import { GetHelpContext } from '~/components/sign-in/hooks/use-get-help'; @@ -23,13 +24,16 @@ import { type Appearance, AppearanceProvider } from '~/contexts'; * where we'll consider its integration within Elements, as well as ensure * bulletproof a11y. */ -export function SignIn({ appearance }: { appearance?: Appearance }) { +export function SignIn({ appearance, ...props }: { appearance?: Appearance } & SignInProps) { const [showHelp, setShowHelp] = React.useState(false); + // If __experimental.newComponents is `true`, we should use __experimental.appearance instead of appearance. + const componentAppearance = props.__experimental?.newComponents ? props.__experimental.appearance : appearance; + return ( - + - + {showHelp ? ( ) : ( diff --git a/packages/ui/src/components/sign-in/steps/choose-session.tsx b/packages/ui/src/components/sign-in/steps/choose-session.tsx index 4da6e0b3a5..91fb9ba794 100644 --- a/packages/ui/src/components/sign-in/steps/choose-session.tsx +++ b/packages/ui/src/components/sign-in/steps/choose-session.tsx @@ -1,5 +1,5 @@ -import { useClerk } from '@clerk/clerk-react'; import * as SignIn from '@clerk/elements/sign-in'; +import { useClerk } from '@clerk/shared/react'; import { cva } from 'cva'; import { Button } from 'react-aria-components'; diff --git a/packages/ui/src/components/sign-in/steps/choose-strategy.tsx b/packages/ui/src/components/sign-in/steps/choose-strategy.tsx index 98bed6614b..84131d46d2 100644 --- a/packages/ui/src/components/sign-in/steps/choose-strategy.tsx +++ b/packages/ui/src/components/sign-in/steps/choose-strategy.tsx @@ -1,6 +1,6 @@ -import { useSignIn } from '@clerk/clerk-react'; import * as Common from '@clerk/elements/common'; import * as SignIn from '@clerk/elements/sign-in'; +import { useClerk } from '@clerk/shared/react'; import { Connections } from '~/common/connections'; import { GlobalError } from '~/common/global-error'; @@ -22,7 +22,7 @@ import { LinkButton } from '~/primitives/link'; ============================================ */ function FirstFactorConnections({ isGlobalLoading }: { isGlobalLoading: boolean }) { - const { signIn } = useSignIn(); + const { signIn } = useClerk().client; const isFirstFactor = signIn?.status === 'needs_first_factor'; if (isFirstFactor) { diff --git a/packages/ui/src/components/sign-in/steps/start.tsx b/packages/ui/src/components/sign-in/steps/start.tsx index ffce8e560f..f63b9b52d0 100644 --- a/packages/ui/src/components/sign-in/steps/start.tsx +++ b/packages/ui/src/components/sign-in/steps/start.tsx @@ -1,5 +1,6 @@ import * as Common from '@clerk/elements/common'; import * as SignIn from '@clerk/elements/sign-in'; +import { useClerk } from '@clerk/shared/react'; import { cx } from 'cva'; import * as React from 'react'; @@ -12,6 +13,7 @@ import { GlobalError } from '~/common/global-error'; import { PasswordField } from '~/common/password-field'; import { PhoneNumberField } from '~/common/phone-number-field'; import { PhoneNumberOrUsernameField } from '~/common/phone-number-or-username-field'; +import { RouterLink } from '~/common/router-link'; import { UsernameField } from '~/common/username-field'; import { LOCALIZATION_NEEDED } from '~/constants/localizations'; import { SIGN_UP_MODES } from '~/constants/user-settings'; @@ -23,6 +25,7 @@ import { useDisplayConfig } from '~/hooks/use-display-config'; import { useEnabledConnections } from '~/hooks/use-enabled-connections'; import { useEnvironment } from '~/hooks/use-environment'; import { useLocalizations } from '~/hooks/use-localizations'; +import { useOptions } from '~/hooks/use-options'; import { Button } from '~/primitives/button'; import * as Card from '~/primitives/card'; import CaretRightLegacySm from '~/primitives/icons/caret-right-legacy-sm'; @@ -44,6 +47,8 @@ export function SignInStart() { const isDev = useDevModeWarning(); const { options } = useAppearance().parsedAppearance; const { logoProps, footerProps } = useCard(); + const clerk = useClerk(); + const { signUpUrl } = useOptions(); return ( @@ -196,7 +201,12 @@ export function SignInStart() { {t('signIn.start.actionText')}{' '} - {t('signIn.start.actionLink')} + + {t('signIn.start.actionLink')} + ) : null} diff --git a/packages/ui/src/components/sign-up/sign-up.tsx b/packages/ui/src/components/sign-up/sign-up.tsx index 61de264e5f..835bb2d231 100644 --- a/packages/ui/src/components/sign-up/sign-up.tsx +++ b/packages/ui/src/components/sign-up/sign-up.tsx @@ -1,17 +1,24 @@ import { Root as SignUpRoot } from '@clerk/elements/sign-up'; +import type { SignUpProps } from '@clerk/types'; import { SignUpContinue } from '~/components/sign-up/steps/continue'; import { SignUpStart } from '~/components/sign-up/steps/start'; // import { SignUpStatus } from '~/components/sign-up/steps/status'; import { SignUpVerifications } from '~/components/sign-up/steps/verifications'; +import { type Appearance, AppearanceProvider } from '~/contexts'; + +export function SignUp({ appearance, ...props }: { appearance?: Appearance } & SignUpProps) { + // If __experimental.newComponents is `true`, we should use __experimental.appearance instead of appearance. + const componentAppearance = props.__experimental?.newComponents ? props.__experimental.appearance : appearance; -export function SignUp() { return ( - - - - - {/* */} - + + + + + + {/* */} + + ); } diff --git a/packages/ui/src/components/sign-up/steps/continue.tsx b/packages/ui/src/components/sign-up/steps/continue.tsx index 600d9e82a9..3690611737 100644 --- a/packages/ui/src/components/sign-up/steps/continue.tsx +++ b/packages/ui/src/components/sign-up/steps/continue.tsx @@ -1,5 +1,6 @@ import * as Common from '@clerk/elements/common'; import * as SignUp from '@clerk/elements/sign-up'; +import { useClerk } from '@clerk/shared/react'; import { EmailField } from '~/common/email-field'; import { FirstNameField } from '~/common/first-name-field'; @@ -7,17 +8,21 @@ import { GlobalError } from '~/common/global-error'; import { LastNameField } from '~/common/last-name-field'; import { PasswordField } from '~/common/password-field'; import { PhoneNumberField } from '~/common/phone-number-field'; +import { RouterLink } from '~/common/router-link'; import { UsernameField } from '~/common/username-field'; import { LOCALIZATION_NEEDED } from '~/constants/localizations'; import { useAttributes } from '~/hooks/use-attributes'; import { useCard } from '~/hooks/use-card'; import { useDevModeWarning } from '~/hooks/use-dev-mode-warning'; import { useLocalizations } from '~/hooks/use-localizations'; +import { useOptions } from '~/hooks/use-options'; import { Button } from '~/primitives/button'; import * as Card from '~/primitives/card'; import CaretRightLegacySm from '~/primitives/icons/caret-right-legacy-sm'; export function SignUpContinue() { + const clerk = useClerk(); + const { signInUrl } = useOptions(); const { t } = useLocalizations(); const { enabled: firstNameEnabled, required: firstNameRequired } = useAttributes('first_name'); const { enabled: lastNameEnabled, required: lastNameRequired } = useAttributes('last_name'); @@ -114,7 +119,12 @@ export function SignUpContinue() { {t('signUp.continue.actionText')}{' '} - {t('signUp.continue.actionLink')} + + {t('signUp.continue.actionLink')} + diff --git a/packages/ui/src/components/sign-up/steps/start.tsx b/packages/ui/src/components/sign-up/steps/start.tsx index 27804a2f02..90450609b6 100644 --- a/packages/ui/src/components/sign-up/steps/start.tsx +++ b/packages/ui/src/components/sign-up/steps/start.tsx @@ -1,6 +1,6 @@ -import { useClerk } from '@clerk/clerk-react'; import * as Common from '@clerk/elements/common'; import * as SignUp from '@clerk/elements/sign-up'; +import { useClerk } from '@clerk/shared/react'; import { Connections } from '~/common/connections'; import { EmailField } from '~/common/email-field'; @@ -10,6 +10,7 @@ import { GlobalError } from '~/common/global-error'; import { LastNameField } from '~/common/last-name-field'; import { PasswordField } from '~/common/password-field'; import { PhoneNumberField } from '~/common/phone-number-field'; +import { RouterLink } from '~/common/router-link'; import { UsernameField } from '~/common/username-field'; import { LOCALIZATION_NEEDED } from '~/constants/localizations'; import { useAppearance } from '~/contexts'; @@ -20,6 +21,7 @@ import { useDisplayConfig } from '~/hooks/use-display-config'; import { useEnabledConnections } from '~/hooks/use-enabled-connections'; import { useEnvironment } from '~/hooks/use-environment'; import { useLocalizations } from '~/hooks/use-localizations'; +import { useOptions } from '~/hooks/use-options'; import { Button } from '~/primitives/button'; import * as Card from '~/primitives/card'; import CaretRightLegacySm from '~/primitives/icons/caret-right-legacy-sm'; @@ -27,6 +29,7 @@ import { Separator } from '~/primitives/separator'; export function SignUpStart() { const clerk = useClerk(); + const { signInUrl } = useOptions(); const enabledConnections = useEnabledConnections(); const { userSettings } = useEnvironment(); const { t } = useLocalizations(); @@ -170,7 +173,12 @@ export function SignUpStart() { {t('signUp.start.actionText')}{' '} - {t('signUp.start.actionLink')} + + {t('signUp.start.actionLink')} + diff --git a/packages/ui/src/components/sign-up/steps/verifications.tsx b/packages/ui/src/components/sign-up/steps/verifications.tsx index 3c63aa26f6..202fd9664d 100644 --- a/packages/ui/src/components/sign-up/steps/verifications.tsx +++ b/packages/ui/src/components/sign-up/steps/verifications.tsx @@ -1,6 +1,6 @@ -import { useClerk } from '@clerk/clerk-react'; import * as Common from '@clerk/elements/common'; import * as SignUp from '@clerk/elements/sign-up'; +import { useClerk } from '@clerk/shared/react'; import { GlobalError } from '~/common/global-error'; import { OTPField } from '~/common/otp-field'; diff --git a/packages/ui/src/contexts/AppearanceContext.tsx b/packages/ui/src/contexts/AppearanceContext.tsx index 90c1882173..51b8fcdab4 100644 --- a/packages/ui/src/contexts/AppearanceContext.tsx +++ b/packages/ui/src/contexts/AppearanceContext.tsx @@ -4,13 +4,14 @@ import React from 'react'; import { fullTheme } from '~/themes'; -type AlertDescriptorIdentifier = 'alert' | 'alert__error' | 'alert__warning' | 'alertRoot' | 'alertIcon'; +type AlertDescriptorIdentifier = 'alert' | 'alert__error' | 'alert__warning' | 'alertIcon'; type SeparatorDescriptorIdentifier = 'separator'; +type CardDescriptorIdentifier = 'logoBox' | 'logoLink' | 'logoImage'; /** * Union of all valid descriptors used throughout the components. */ -export type DescriptorIdentifier = AlertDescriptorIdentifier | SeparatorDescriptorIdentifier; +export type DescriptorIdentifier = AlertDescriptorIdentifier | SeparatorDescriptorIdentifier | CardDescriptorIdentifier; /** * The final resulting descriptor that gets passed to mergeDescriptors and spread on the element. diff --git a/packages/ui/src/hooks/use-display-config.ts b/packages/ui/src/hooks/use-display-config.ts index 7a7a3169ee..e472f282dd 100644 --- a/packages/ui/src/hooks/use-display-config.ts +++ b/packages/ui/src/hooks/use-display-config.ts @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; import type { EnvironmentResource } from '@clerk/types'; export function useDisplayConfig() { diff --git a/packages/ui/src/hooks/use-enabled-connections.ts b/packages/ui/src/hooks/use-enabled-connections.ts index 096839649e..e019dbe5f8 100644 --- a/packages/ui/src/hooks/use-enabled-connections.ts +++ b/packages/ui/src/hooks/use-enabled-connections.ts @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; import { type EnvironmentResource, OAUTH_PROVIDERS, WEB3_PROVIDERS } from '@clerk/types'; export function useEnabledConnections() { diff --git a/packages/ui/src/hooks/use-environment.ts b/packages/ui/src/hooks/use-environment.ts index c824b3a703..ee20c7e679 100644 --- a/packages/ui/src/hooks/use-environment.ts +++ b/packages/ui/src/hooks/use-environment.ts @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; import type { EnvironmentResource } from '@clerk/types'; export function useEnvironment() { diff --git a/packages/ui/src/hooks/use-options.ts b/packages/ui/src/hooks/use-options.ts index ec8c06ee29..399f015b70 100644 --- a/packages/ui/src/hooks/use-options.ts +++ b/packages/ui/src/hooks/use-options.ts @@ -1,8 +1,3 @@ -import { useClerk } from '@clerk/clerk-react'; -import type { ClerkOptions } from '@clerk/types'; +import { useOptionsContext } from '@clerk/shared/react'; -export function useOptions() { - const clerk = useClerk(); - const options = (clerk as any)?.options as ClerkOptions; - return options; -} +export const useOptions = useOptionsContext; diff --git a/packages/ui/src/hooks/use-reset-password-factor.ts b/packages/ui/src/hooks/use-reset-password-factor.ts index 502027be2e..8cb9fd1701 100644 --- a/packages/ui/src/hooks/use-reset-password-factor.ts +++ b/packages/ui/src/hooks/use-reset-password-factor.ts @@ -1,4 +1,4 @@ -import { useClerk } from '@clerk/clerk-react'; +import { useClerk } from '@clerk/shared/react'; import type { ResetPasswordCodeFactor, SignInStrategy } from '@clerk/types'; const resetPasswordStrategies: SignInStrategy[] = ['reset_password_phone_code', 'reset_password_email_code']; diff --git a/packages/ui/src/primitives/card.tsx b/packages/ui/src/primitives/card.tsx index 0df9a6f4c4..3e0ee817d0 100644 --- a/packages/ui/src/primitives/card.tsx +++ b/packages/ui/src/primitives/card.tsx @@ -1,6 +1,8 @@ import { cva, cx } from 'cva'; import * as React from 'react'; +import { useAppearance } from '~/contexts'; +import { mergeDescriptors, type ParsedElementsFragment } from '~/contexts/AppearanceContext'; import type { PolymorphicForwardRefExoticComponent, PolymorphicPropsWithoutRef } from '~/types/utils'; import { ClerkLogo } from './clerk-logo'; @@ -106,9 +108,24 @@ export const Header = React.forwardRef, ) { + const { elements } = useAppearance().parsedAppearance; if (!src) { return null; } @@ -128,15 +146,15 @@ export const Logo = React.forwardRef(function CardLogo( src={src} size={200} {...props} - className={cx('size-full object-contain', className)} + {...mergeDescriptors(elements.logoImage)} /> ); return ( -
+
{href ? ( {img} @@ -384,3 +402,11 @@ const FooterPageLink = React.forwardRef { 'components/sign-up': 'src/components/sign-up/index.tsx', contexts: 'src/contexts/index.ts', }, - external: ['react', 'react-dom'], + external: ['react', 'react-dom', '@clerk/shared'], format: ['cjs', 'esm'], minify: false, sourcemap: true,