feat(ui): Expose profile sub components#8654
Conversation
🦋 Changeset detectedLatest commit: 2389631 The changes in this PR will be included in the next version bump. This PR includes changesets to release 23 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
!snapshot |
This comment has been minimized.
This comment has been minimized.
|
!snapshot |
This comment has been minimized.
This comment has been minimized.
|
!snapshot |
This comment has been minimized.
This comment has been minimized.
|
!snapshot |
This comment has been minimized.
This comment has been minimized.
|
!snapshot |
This comment has been minimized.
This comment has been minimized.
e47d56e to
4c41b6b
Compare
…tion factory - Extract createSection factory to eliminate repeated useRequirePage guard pattern across 18 section files (9→4 lines each) - Extract shared BillingSection and APIKeysSection to deduplicate identical components between UserProfile and OrganizationProfile - Memoize useBillingRouter to prevent unnecessary RouteContext consumer re-renders - Change useRequirePage from console.warn to throw for consistent error behavior with other context guards - Remove dead additionalOAuthScopes prop from OrganizationProfileProvider - Add tree-shaking regression tests to enforce per-file module isolation
…k/shared Sibling-mounted composed UserProfile/OrganizationProfile share one emotion cache per Clerk instance, thread localization/supportEmail through, hoist nested appearance.theme.cssLayerName for @layer wrapping, and track the consumer URL via popstate. ProfileProviderShell collapses LazyProviders + LazyComponentRenderer (portal path) for composed mounts. Moves the moduleManager WeakMap to @clerk/shared/moduleManager and has the Clerk constructor register its internal ModuleManager so @clerk/ui composed components resolve it without requiring <ClerkProvider ui={ui} />. No clerk-js public ABI added.
- stubRouter.matches/refresh/getMatchData throw in dev to catch accidental <Route>/refresh()/getMatchData() use inside composed sections - drop popstate-driven currentPath snapshot from ProfileProviderShell (never fired at the right time; section unmount already clears per-section CardStateProvider) - document useBillingRouter as intentionally in-memory (back/forward, refresh, deep-links do not preserve sub-route or tab state) - drop unnecessary __internal_getOption as-any casts; keep nonce cast (nonce lives on IsomorphicClerkOptions, not ClerkOptions) - AppearanceOverrides: destructure parsedElements instead of slice math - useBillingRouter: drop dead _currentRoute parameter - tests pinning the decisions above
Mock at line 100 extracted the method without 'this', so falling through to the real implementation crashed accessing this.#options. Surfaced after composed code stopped masking the call site with as-any casts.
…rapper clerk-js's Clerk constructor registers its ModuleManager keyed against the inner Clerk instance. Composed/subcomponent UserProfile reads it via getModuleManager(useClerk()), which returns the IsomorphicClerk wrapper — a different object identity, so the WeakMap lookup misses and the provider falls back to a no-op import shim. Dynamic-imported features (Coinbase Wallet, Base, zxcvbn, Stripe) silently break; e.g. the Coinbase Wallet flow POSTs an empty web3_wallet and the user sees a 422 form_param_nil. Copy the manager onto the wrapper's key when replayInterceptedInvocations runs, so reads via useClerk() resolve correctly. Also guard Web3Form against empty identifiers locally so any future missing-provider scenario surfaces as a clear UI message rather than a mystifying server-side 422.
Replace the namespace re-export pattern (parts.ts + `export * as`) with explicit compound objects assembled in index.tsx, so property-access tree-shaking on `UserProfile.X` / `OrganizationProfile.X` can drop unused leaves under `sideEffects: false`. Push the 'use client' directive down to each leaf component instead of marking the whole barrel client — the barrel itself contains no React code now, so server components can import the namespace and have unused server-safe siblings tree-shaken out. Update the tree-shaking invariant test to check index.tsx for the new shape (no `export *`, leaves don't import siblings).
…e handoff clerk-js loads standalone from the CDN with its own inlined @clerk/shared, so the module-scoped WeakMap in @clerk/shared/moduleManager is invisible to consumers that import @clerk/shared from node_modules (every framework SDK). The WeakMap-only registration in clerk-js's constructor never reached IsomorphicClerk or @clerk/ui's composed UserProfile, so dynamic- imported features (Coinbase Wallet, Base, Stripe, zxcvbn) silently fell back to a no-op manager. Restore the `__internal_moduleManager` getter on the Clerk class (originally added in 82399ea, removed in 81913dc). IsomorphicClerk forwards through the getter on load, and writes the manager into the node_modules @clerk/shared WeakMap so composed UserProfile's `getModuleManager(useClerk())` lookup resolves correctly. WeakMap registration in clerk-js stays as a same-bundle fallback. Tradeoff acknowledged: `__internal_moduleManager` is an ABI surface on Clerk, but it's the right channel — IsomorphicClerk proxies every other piece of clerk-js state through public-ish properties, this is just one more. The earlier refactor that hid the channel inside a WeakMap made the contract uninspectable without removing it.
The Clerk interface now requires `__internal_moduleManager` (added so framework SDK wrappers can forward clerk-js's ModuleManager across the bundle boundary). IsomorphicClerk implements that interface but only held the manager via the @clerk/shared WeakMap channel — TypeScript's type-check rightly flagged the missing property. Add a proxy getter that reads from `this.clerkjs?.__internal_moduleManager` so the wrapper exposes the same surface as the inner Clerk. Composed UI components continue to read through the WeakMap channel populated in replayInterceptedInvocations; the getter is just there to satisfy the interface and give direct access for code that has the wrapper.
Inline `import('../moduleManager').ModuleManager` violated
@typescript-eslint/consistent-type-imports. Hoist to a top-level
`import type` instead.
…rphicClerk propagation
137ce3d to
4033979
Compare
Replace the UserProfile/OrganizationProfile namespace objects with flat named exports (UserProfile*Panel/*Section, OrganizationProfile*Panel/*Section) so each component is a first-class client reference and renders in a React Server Component tree without a consumer 'use client' boundary.
Add an integration suite that drives the experimental @clerk/ui/experimental composed UserProfile and OrganizationProfile exports through the shared @clerk/testing page-object flows (username, name, email/phone with OTP, MFA, account deletion; org render, rename, delete) with backend assertions. Fix OrganizationProfileGeneralPanel crashing with 'CardState not found' when a leave/delete section opens: wrap children in CardStateProvider, matching the composed UserProfile account/security panels.
Replace the standalone OrganizationProfileConfigureSSOPanel composed export with OrganizationProfileSecurityPanel, which renders the full standard OrganizationSecurityPage (SSO overview + configuration wizard) so the composed API can reproduce the OrganizationProfile security tab. The security tab has no composable sub-sections, so the panel takes no children. Add e2e parity coverage that renders the composed security panel and asserts it shows the same security page the standard component renders on its security route (Security header, SSO overview, Start configuration).
- Add return types + JSDoc to exported OrganizationGeneralPage sections - Cover the enabled branch of AccountEnterpriseAccounts - Fail fast in fallbackModuleManager instead of silently resolving undefined
|
!snapshot |
|
Hey @alexcarpenter - the snapshot version command generated the following package versions:
Tip: Use the snippet copy button below to quickly install the required packages. npm i @clerk/astro@3.4.13-snapshot.v20260707124635 --save-exact
npm i @clerk/backend@3.11.1-snapshot.v20260707124635 --save-exact
npm i @clerk/chrome-extension@3.1.49-snapshot.v20260707124635 --save-exact
npm i @clerk/clerk-js@6.25.0-snapshot.v20260707124635 --save-exact
npm i @clerk/electron@0.0.10-snapshot.v20260707124635 --save-exact
npm i @clerk/electron-passkeys@0.0.4-snapshot.v20260707124635 --save-exact
npm i @clerk/eslint-plugin@0.2.1-snapshot.v20260707124635 --save-exact
npm i @clerk/expo@3.7.1-snapshot.v20260707124635 --save-exact
npm i @clerk/expo-passkeys@1.2.1-snapshot.v20260707124635 --save-exact
npm i @clerk/express@2.1.37-snapshot.v20260707124635 --save-exact
npm i @clerk/fastify@3.1.47-snapshot.v20260707124635 --save-exact
npm i @clerk/headless@0.0.8-snapshot.v20260707124635 --save-exact
npm i @clerk/hono@0.1.47-snapshot.v20260707124635 --save-exact
npm i @clerk/localizations@4.13.0-snapshot.v20260707124635 --save-exact
npm i @clerk/msw@0.0.44-snapshot.v20260707124635 --save-exact
npm i @clerk/nextjs@7.5.14-snapshot.v20260707124635 --save-exact
npm i @clerk/nuxt@2.6.13-snapshot.v20260707124635 --save-exact
npm i @clerk/react@6.12.0-snapshot.v20260707124635 --save-exact
npm i @clerk/react-router@3.5.6-snapshot.v20260707124635 --save-exact
npm i @clerk/shared@4.25.0-snapshot.v20260707124635 --save-exact
npm i @clerk/swingset@0.0.15-snapshot.v20260707124635 --save-exact
npm i @clerk/tanstack-react-start@1.4.14-snapshot.v20260707124635 --save-exact
npm i @clerk/testing@2.2.4-snapshot.v20260707124635 --save-exact
npm i @clerk/ui@1.25.0-snapshot.v20260707124635 --save-exact
npm i @clerk/upgrade@2.0.5-snapshot.v20260707124635 --save-exact
npm i @clerk/vue@2.4.12-snapshot.v20260707124635 --save-exact |
…ed profile providers
…ed registry Composed UserProfile/OrganizationProfile resolved the ModuleManager through a @clerk/shared module-scoped WeakMap, which only works when the read and write sides share the same physical @clerk/shared copy. Under version skew or no-dedupe that registry splits and every dynamic-import feature silently falls back to the rejecting manager. Read clerk.__internal_moduleManager directly (plain cross-bundle property access) and delete the WeakMap plumbing.
…d-failure contract
Rename the ProfileCard.PageSection element to PagePanel across all call sites, and fix an incorrect oxlint-disable directive (repo uses eslint).
The assertion (connect yields a non-empty identifier) requires a real moduleManager performing the wallet-SDK dynamic import, which jsdom can't provide. Belongs in e2e, not a permanently-skipped unit test.
…ests Remove the useEffect cleanup from useSafeAutoAnimate. Under React 18 StrictMode, effects double-invoke (setup/cleanup/setup) while callback refs fire once, so the cleanup destroyed the auto-animate controller during the simulated unmount and the ref never recreated it, leaving no MutationObserver and no entrance animation in dev. Teardown is now handled solely by the callback ref's null-branch. Also trims redundant/library-characterization tests in the composed profile suite (delete context-parity + tree-shaking, de-dupe the moduleManager getter test, assert the visibility matrix once, collapse action-animation, retarget the StrictMode test at the production Animated component, trim emotion-internals cssLayerName tests).
Summary
Ships a composable
UserProfile/OrganizationProfilefrom@clerk/ui/experimental. Consumers render individual panels and sections (Account, Security, Members, Billing, …) inline in their own app instead of inside Clerk's modal/page flow.The API is a set of flat, top-level named exports — not a namespace object like
UserProfile.Account. Each named export of a'use client'module becomes its own React Server Component client reference, so consumers can render these directly in a Server Component tree without adding their own'use client'boundary. (Property access on a namespace object would not survive the RSC boundary.)Design decisions worth scrutiny
@clerk/ui/experimental, not a property on a namespace object.flat-exports.test.tspins the surface and asserts the namespace objects areundefined.ProfileProviderShell.tsx)LazyProviders+LazyComponentRenderer/LazyModalRenderer(portal path) into one shell.ClerkContextProvideromitted.<ClerkProvider>already suppliesclerkviauseClerk().WeakMap(internal/styleCacheStore.ts).cl-internalcaches and double-insert rules. Keyed on the clerk instance object, so it auto-GCs on teardown.moduleManager__internal_moduleManagergetter onClerk(clerk.ts:288); IsomorphicClerk forwards it (isomorphicClerk.ts:298). Composed shell readsclerk.__internal_moduleManagerdirectly.<ClerkProvider ui={ui} />optional for composed (Web3 works out of box). Plain property access is the only channel that crosses the bundle boundary — clerk-js loads standalone with its own inlined@clerk/shared, so@clerk/react/@clerk/uireading@clerk/sharedfromnode_modulessee a different copy.localization+supportEmail+noncefromclerk.__internal_getOption, pass viaOptionsProvider/SharedStyleCacheProvider.makeLocalizable,parseLocalization,useSupportEmail, Emotion CSP nonce).nonceneeds anas anycast (typed onIsomorphicClerkOptions, notClerkOptions).cssLayerNameextractCssLayerNameFromAppearanceonglobalAppearancebefore reading.Components.tsx:209). Without it, nestedappearance.theme.cssLayerNameis ignored.stubRouter.ts)navigate/baseNavigate/resolveare real and SSR-safe.currentPathis a static empty string (nopopstatelistener).matches()/refresh()/getMatchData()throw in dev, no-op in prod.__tests__/stub-limitations.test.ts.UserProfileAccountPanel→AccountPage). With children, sub-sections render directly underPageContext+CardStateProvider.matches()/refresh()/getMatchData()(dev throw, prod no-op).OrganizationProfileSecurityPanelrenders the whole standardOrganizationSecurityPage(SSO overview + configuration wizard). It takes no composable children — the security tab is not section-based, unlike General. There is no standalone ConfigureSSO composable.<OrganizationProfile />shows on its security route.createSection(name, Component)factory +PageContextguard viauseRequirePage.Suggested review order
packages/ui/src/composed/ProfileProviderShell.tsx: the architectural heart.packages/clerk-js/src/core/clerk.ts(__internal_moduleManagergetter,clerk.ts:288) +packages/react/src/isomorphicClerk.ts(forwards to the wrapper,:298): confirm the cross-bundle wiring and the ABI tradeoff.packages/ui/src/composed/stubRouter.ts+__tests__/stub-limitations.test.ts: router contract and dev/prod asymmetry.packages/ui/src/composed/{UserProfile,OrganizationProfile}/*Provider.tsx: section wiring +__internal_environmentcast.packages/ui/src/composed/__tests__/: parity, wiring, style-cache, flat-export, and tree-shaking tests pin the design.integration/tests/composed-components.test.ts: e2e parity — the composed exports run the same trusted sign-in / profile flows as the standard components against a real clerk-js runtime.__internal_moduleManagerABI tradeoffNew getter on clerk-js's
Clerkclass. Once shipped, removing it breaks every pinned SDK version that reads it.Why accept the cost:
__internal_prefix signals "may change between majors" — same contract as the rest of__internal_*on Clerk.Alternatives considered and rejected:
@clerk/sharedWeakMap registry (a prior design, now deleted): doesn't cross the bundle boundary. clerk-js inlines its own@clerk/shared, so the WeakMap clerk-js writes isn't the one@clerk/react/@clerk/uiread. Under version skew / no-dedupe the registry splits and dynamic-import features silently fall back to the rejecting manager.Symbol.for(...)-keyedglobalThisWeakMap: crosses bundles, but hides the contract in an unnamed channel. Action-at-a-distance, lifecycle issues with test workers and HMR, still on the hook to keep the symbol stable forever — same cost, less inspectable.Resolves PD-56
Test plan
<ClerkProvider ui={ui} />: confirm Web3 Add-Coinbase flow resolves the SDKUserProfileProviderand anOrganizationProfileProvidersibling-mounted on one page: singlecl-internalcache, localized strings renderintegration/tests/composed-components.test.ts: the composed UserProfile / OrganizationProfile exports pass the same trusted flows as the standard components (account edits, security, org general/members/security)