|
| 1 | +'use client'; |
| 2 | + |
| 3 | +// Composed UserProfile / OrganizationProfile mount outside the clerk-js portal |
| 4 | +// tree, so this shell rebuilds the providers normally split between |
| 5 | +// `LazyProviders` and `LazyComponentRenderer` / `LazyModalRenderer` in |
| 6 | +// `packages/ui/src/lazyModules/providers.tsx`. `ClerkContextProvider` is |
| 7 | +// intentionally omitted — the consumer's `<ClerkProvider>` supplies `clerk` via |
| 8 | +// `useClerk()`. The emotion cache is keyed per clerk instance in |
| 9 | +// `styleCacheStore` so sibling composed roots don't duplicate style insertions. |
| 10 | + |
| 11 | +import { ClerkRuntimeError } from '@clerk/shared/error'; |
| 12 | +import type { ModuleManager } from '@clerk/shared/moduleManager'; |
| 13 | +import type { EnvironmentResource, LoadedClerk } from '@clerk/shared/types'; |
| 14 | +// eslint-disable-next-line no-restricted-imports |
| 15 | +import { CacheProvider } from '@emotion/react'; |
| 16 | +import type { PropsWithChildren, ReactNode } from 'react'; |
| 17 | +import { useMemo } from 'react'; |
| 18 | + |
| 19 | +import { AppearanceProvider } from '@/ui/customizables/AppearanceContext'; |
| 20 | +import { FlowMetadataProvider } from '@/ui/elements/contexts'; |
| 21 | +import type { Appearance, Elements } from '@/ui/internal/appearance'; |
| 22 | +import { getStyleCache, setStyleCache } from '@/ui/internal/styleCacheStore'; |
| 23 | +import { RouteContext } from '@/ui/router/RouteContext'; |
| 24 | +import { InternalThemeProvider } from '@/ui/styledSystem'; |
| 25 | +import { createEmotionCache } from '@/ui/styledSystem/createEmotionCache'; |
| 26 | +import { extractCssLayerNameFromAppearance } from '@/ui/utils/extractCssLayerNameFromAppearance'; |
| 27 | + |
| 28 | +import { EnvironmentProvider } from '../contexts/EnvironmentContext'; |
| 29 | +import { ModuleManagerProvider } from '../contexts/ModuleManagerContext'; |
| 30 | +import { OptionsProvider } from '../contexts/OptionsContext'; |
| 31 | +import { AppearanceOverrides } from '../elements/AppearanceOverrides'; |
| 32 | +import { createComposedRouter } from './stubRouter'; |
| 33 | + |
| 34 | +// Used when `clerk.__internal_moduleManager` is `undefined`. In a correctly wired app clerk-js |
| 35 | +// exposes its ModuleManager through that getter, so reaching this means the loaded clerk-js is too |
| 36 | +// old to expose it (an older clerk-js also predates composed profiles entirely). Fail loudly on the |
| 37 | +// first dynamic import (Web3, billing, password strength) instead of silently resolving `undefined` |
| 38 | +// and surfacing later as an opaque access on the missing module. |
| 39 | +export const fallbackModuleManager: ModuleManager = { |
| 40 | + import: () => |
| 41 | + Promise.reject( |
| 42 | + new ClerkRuntimeError( |
| 43 | + 'Composed profile components could not resolve a Clerk module manager: this Clerk instance does not expose one. This usually means the loaded @clerk/clerk-js is too old to support composed profiles.', |
| 44 | + { code: 'composed_module_manager_unavailable' }, |
| 45 | + ), |
| 46 | + ), |
| 47 | +}; |
| 48 | + |
| 49 | +const composedOverrides: Elements = { |
| 50 | + profilePageContent: { padding: 0 }, |
| 51 | +}; |
| 52 | + |
| 53 | +type ProfileProviderShellProps = PropsWithChildren<{ |
| 54 | + clerk: LoadedClerk; |
| 55 | + environment: EnvironmentResource; |
| 56 | + moduleManager: ModuleManager; |
| 57 | + appearanceKey: 'userProfile' | 'organizationProfile'; |
| 58 | + flow: 'userProfile' | 'organizationProfile'; |
| 59 | + globalAppearance: Appearance | undefined; |
| 60 | + appearance?: Appearance; |
| 61 | +}>; |
| 62 | + |
| 63 | +type SharedStyleCacheProviderProps = PropsWithChildren<{ |
| 64 | + clerk: LoadedClerk; |
| 65 | + nonce?: string; |
| 66 | + cssLayerName?: string; |
| 67 | +}>; |
| 68 | + |
| 69 | +// One emotion cache per clerk instance, so sibling composed roots share inserts. |
| 70 | +function SharedStyleCacheProvider({ clerk, nonce, cssLayerName, children }: SharedStyleCacheProviderProps): ReactNode { |
| 71 | + const cache = useMemo(() => { |
| 72 | + const existing = getStyleCache(clerk); |
| 73 | + if (existing) { |
| 74 | + return existing; |
| 75 | + } |
| 76 | + const next = createEmotionCache({ nonce, cssLayerName }); |
| 77 | + setStyleCache(clerk, next); |
| 78 | + return next; |
| 79 | + }, [clerk, nonce, cssLayerName]); |
| 80 | + |
| 81 | + return <CacheProvider value={cache}>{children}</CacheProvider>; |
| 82 | +} |
| 83 | + |
| 84 | +export function ProfileProviderShell({ |
| 85 | + children, |
| 86 | + clerk, |
| 87 | + environment, |
| 88 | + moduleManager, |
| 89 | + appearanceKey, |
| 90 | + flow, |
| 91 | + globalAppearance, |
| 92 | + appearance, |
| 93 | +}: ProfileProviderShellProps): ReactNode { |
| 94 | + // currentPath is left empty: composed has no Clerk-internal navigation. Each |
| 95 | + // section owns its own CardStateProvider, so errors clear on section unmount |
| 96 | + // (single-section mounting). Side-by-side sections keep independent error |
| 97 | + // state — a consumer URL change wouldn't be a meaningful signal to clear |
| 98 | + // either, so observing it would only cause spurious clears. |
| 99 | + const router = useMemo(() => createComposedRouter(clerk.navigate), [clerk]); |
| 100 | + // Match the portal path's normalization (Components.tsx:209) so a cssLayerName |
| 101 | + // nested inside appearance.theme gets hoisted to top-level for @layer wrapping. |
| 102 | + const normalizedGlobalAppearance = useMemo( |
| 103 | + () => extractCssLayerNameFromAppearance(globalAppearance), |
| 104 | + [globalAppearance], |
| 105 | + ); |
| 106 | + const options = useMemo( |
| 107 | + () => ({ |
| 108 | + localization: clerk.__internal_getOption('localization'), |
| 109 | + supportEmail: clerk.__internal_getOption('supportEmail'), |
| 110 | + }), |
| 111 | + [clerk], |
| 112 | + ); |
| 113 | + |
| 114 | + return ( |
| 115 | + <SharedStyleCacheProvider |
| 116 | + clerk={clerk} |
| 117 | + // nonce lives on IsomorphicClerkOptions, not ClerkOptions, so the typed K-constraint rejects it |
| 118 | + nonce={(clerk as any).__internal_getOption('nonce')} |
| 119 | + cssLayerName={normalizedGlobalAppearance?.cssLayerName} |
| 120 | + > |
| 121 | + {/* parsed appearance for cl-* styled components */} |
| 122 | + <AppearanceProvider |
| 123 | + appearanceKey={appearanceKey} |
| 124 | + globalAppearance={normalizedGlobalAppearance} |
| 125 | + appearance={appearance} |
| 126 | + > |
| 127 | + {/* flow= for Flow.Root/Part data-clerk-* selectors */} |
| 128 | + <FlowMetadataProvider flow={flow}> |
| 129 | + {/* Emotion ThemeProvider over parsed theme */} |
| 130 | + <InternalThemeProvider> |
| 131 | + {/* dynamic-import bridge (Web3) */} |
| 132 | + <ModuleManagerProvider moduleManager={moduleManager}> |
| 133 | + {/* threads localization + supportEmail from the consumer's <ClerkProvider> */} |
| 134 | + <OptionsProvider value={options}> |
| 135 | + {/* read by useEnvironment() across MFA/account sections */} |
| 136 | + <EnvironmentProvider value={environment}> |
| 137 | + {/* router stub: navigate→clerk.navigate, matches/refresh no-op */} |
| 138 | + <RouteContext.Provider value={router}> |
| 139 | + {/* zero out profilePageContent padding when embedded */} |
| 140 | + <AppearanceOverrides elements={composedOverrides}>{children}</AppearanceOverrides> |
| 141 | + </RouteContext.Provider> |
| 142 | + </EnvironmentProvider> |
| 143 | + </OptionsProvider> |
| 144 | + </ModuleManagerProvider> |
| 145 | + </InternalThemeProvider> |
| 146 | + </FlowMetadataProvider> |
| 147 | + </AppearanceProvider> |
| 148 | + </SharedStyleCacheProvider> |
| 149 | + ); |
| 150 | +} |
0 commit comments