Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7e8bcd2
init
alexcarpenter May 21, 2026
f5f8e20
wip
alexcarpenter May 21, 2026
e95ff5a
fix
alexcarpenter May 21, 2026
fb2c362
Create COMPOSED_API_PLAN.md
alexcarpenter May 22, 2026
7b8b327
init
alexcarpenter May 22, 2026
20bbc37
wip
alexcarpenter May 22, 2026
6d6b9fe
Create composed-provider-wiring.test.tsx
alexcarpenter May 23, 2026
87745cb
docs
alexcarpenter May 26, 2026
58c54df
unify docs
alexcarpenter May 26, 2026
1b36c82
fix: make composed exports SSR-safe for Next.js
alexcarpenter May 27, 2026
56799af
fix: add explicit ReactNode return types to all composed exports
alexcarpenter May 27, 2026
1f12772
fix: use Object.assign pattern for composed exports
alexcarpenter May 27, 2026
2708119
fix Animated in react strict mode usage
alexcarpenter May 27, 2026
29b67d8
fix: globalAppearance usage
alexcarpenter May 27, 2026
0593362
feat(ui): add OrganizationProfile.ConfigureSSO composed export
alexcarpenter May 28, 2026
c91f7ac
add AppearanceOverrides
alexcarpenter May 28, 2026
f9d608e
use AppearanceOverrides
alexcarpenter May 28, 2026
08f26f4
remove playground app
alexcarpenter May 29, 2026
716d04e
cleanup
alexcarpenter May 29, 2026
9188aca
Update pnpm-workspace.yaml
alexcarpenter May 29, 2026
1e0df16
Update pnpm-lock.yaml
alexcarpenter May 29, 2026
3a89eb3
fix(ui): skip Web3 composed test that requires real moduleManager
alexcarpenter May 29, 2026
a1310a1
fix(ui): guard autoAnimate init against missing element.animate (jsdom)
alexcarpenter May 29, 2026
44dac2e
fix(ui): mock Element.animate return value in auto-animate tests
alexcarpenter May 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@
"import": "./dist/themes/experimental.js",
"default": "./dist/themes/experimental.js"
},
"./experimental": {
"types": "./dist/experimental/index.d.ts",
"import": "./dist/experimental/index.js",
"default": "./dist/experimental/index.js"
},
"./themes/shadcn.css": "./dist/themes/shadcn.css",
"./register": {
"import": {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/ClerkUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { ClerkUIInstance, ComponentControls as SharedComponentControls } fr
import { isVersionAtLeast, parseVersion } from '@clerk/shared/versionCheck';

import { type MountComponentRenderer, mountComponentRenderer } from './Components';
import { setModuleManager } from './composed/moduleManagerStore';
import { MIN_CLERK_JS_VERSION } from './constants';

/**
Expand Down Expand Up @@ -78,6 +79,7 @@ export class ClerkUI implements ClerkUIInstance {
}
}

setModuleManager(moduleManager);
this.#componentRenderer = mountComponentRenderer(getClerk, getEnvironment, options, moduleManager);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const OrganizationBillingPageInternal = withCardStateProvider(() => {
<ProfileCard.Page>
<Col
elementDescriptor={descriptors.page}
sx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground })}
sx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground, isolation: 'isolate' })}
>
<Col
elementDescriptor={descriptors.profilePage}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const OrganizationGeneralPage = () => {
<ProfileCard.Page>
<Col
elementDescriptor={descriptors.page}
sx={t => ({ gap: t.space.$8 })}
sx={t => ({ gap: t.space.$8, isolation: 'isolate' })}
>
<Col
elementDescriptor={descriptors.profilePage}
Expand All @@ -85,7 +85,7 @@ export const OrganizationGeneralPage = () => {
);
};

const OrganizationProfileSection = () => {
export const OrganizationProfileSection = () => {
const { organization } = useOrganization();

if (!organization) {
Expand Down Expand Up @@ -134,7 +134,7 @@ const OrganizationProfileSection = () => {
);
};

const OrganizationDomainsSection = () => {
export const OrganizationDomainsSection = () => {
const { organizationSettings } = useEnvironment();
const { organization } = useOrganization();

Expand Down Expand Up @@ -183,7 +183,7 @@ const OrganizationDomainsSection = () => {
);
};

const OrganizationLeaveSection = () => {
export const OrganizationLeaveSection = () => {
const { organization } = useOrganization();

if (!organization) {
Expand Down Expand Up @@ -229,7 +229,7 @@ const OrganizationLeaveSection = () => {
);
};

const OrganizationDeleteSection = () => {
export const OrganizationDeleteSection = () => {
const { organization } = useOrganization();
const canDeleteOrganization = useProtect({ permission: 'org:sys_profile:delete' });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export const OrganizationMembers = withCardStateProvider(() => {
<Col
elementDescriptor={descriptors.page}
gap={2}
sx={{ isolation: 'isolate' }}
>
<Col
elementDescriptor={descriptors.profilePage}
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/UserProfile/APIKeysPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const APIKeysPage = () => {
<Col
gap={4}
elementDescriptor={descriptors.page}
sx={{ isolation: 'isolate' }}
>
<Header.Root>
<Header.Title
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/UserProfile/AccountPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export const AccountPage = withCardStateProvider(() => {
<ProfileCard.Page>
<Col
elementDescriptor={descriptors.page}
sx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground })}
sx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground, isolation: 'isolate' })}
>
<Col
elementDescriptor={descriptors.profilePage}
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/UserProfile/BillingPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const BillingPageInternal = withCardStateProvider(() => {
<ProfileCard.Page>
<Col
elementDescriptor={descriptors.page}
sx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground })}
sx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground, isolation: 'isolate' })}
>
<Col
elementDescriptor={descriptors.profilePage}
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/components/UserProfile/SecurityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const SecurityPage = withCardStateProvider(() => {
<ProfileCard.Page>
<Col
elementDescriptor={descriptors.page}
sx={t => ({ gap: t.space.$8 })}
sx={t => ({ gap: t.space.$8, isolation: 'isolate' })}
>
<Col
elementDescriptor={descriptors.profilePage}
Expand Down
17 changes: 17 additions & 0 deletions packages/ui/src/composed/OrganizationProfile/APIKeys.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { lazy, Suspense, type ReactNode } from 'react';

import { CardStateProvider } from '../../elements/contexts';

const OrganizationAPIKeysPage = lazy(() =>
import('../../components/OrganizationProfile/OrganizationAPIKeysPage').then(m => ({
default: m.OrganizationAPIKeysPage,
})),
);

export const APIKeys = (): ReactNode => (
<CardStateProvider>
<Suspense fallback={''}>
<OrganizationAPIKeysPage />
</Suspense>
</CardStateProvider>
);
53 changes: 53 additions & 0 deletions packages/ui/src/composed/OrganizationProfile/Billing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { lazy, Suspense, type ReactNode } from 'react';

import { RouteContext } from '../../router/RouteContext';
import { useBillingRouter } from '../useBillingRouter';

const OrganizationBillingPage = lazy(() =>
import('../../components/OrganizationProfile/OrganizationBillingPage').then(m => ({
default: m.OrganizationBillingPage,
})),
);

const OrganizationPlansPage = lazy(() =>
import('../../components/OrganizationProfile/OrganizationPlansPage').then(m => ({
default: m.OrganizationPlansPage,
})),
);

const OrganizationStatementPage = lazy(() =>
import('../../components/OrganizationProfile/OrganizationStatementPage').then(m => ({
default: m.OrganizationStatementPage,
})),
);

const OrganizationPaymentAttemptPage = lazy(() =>
import('../../components/OrganizationProfile/OrganizationPaymentAttemptPage').then(m => ({
default: m.OrganizationPaymentAttemptPage,
})),
);

export const Billing = (): ReactNode => {
const { router, route } = useBillingRouter();

let content: ReactNode;
switch (route.page) {
case 'plans':
content = <OrganizationPlansPage />;
break;
case 'statement':
content = <OrganizationStatementPage />;
break;
case 'payment-attempt':
content = <OrganizationPaymentAttemptPage />;
break;
default:
content = <OrganizationBillingPage />;
}

return (
<RouteContext.Provider value={router}>
<Suspense fallback={''}>{content}</Suspense>
</RouteContext.Provider>
);
};
22 changes: 22 additions & 0 deletions packages/ui/src/composed/OrganizationProfile/ConfigureSSO.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useRef, type ReactNode } from 'react';

import type { Elements } from '../../internal/appearance';
import { ConfigureSSOContent } from '../../components/ConfigureSSO/ConfigureSSO';
import { AppearanceOverrides } from '../../elements/AppearanceOverrides';
import { CardStateProvider } from '../../elements/contexts';

const embeddedOverrides: Elements = {
configureSSOFooter: { background: 'transparent' },
};

export const ConfigureSSO = (): ReactNode => {
const contentRef = useRef<HTMLDivElement>(null);

return (
<CardStateProvider>
<AppearanceOverrides elements={embeddedOverrides}>
<ConfigureSSOContent contentRef={contentRef} />
</AppearanceOverrides>
</CardStateProvider>
);
};
39 changes: 39 additions & 0 deletions packages/ui/src/composed/OrganizationProfile/General.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { PropsWithChildren, ReactNode } from 'react';

import { Header } from '@/ui/elements/Header';
import { ProfileCard } from '@/ui/elements/ProfileCard';

import { Col, descriptors, localizationKeys } from '../../customizables';
import { OrganizationGeneralPage } from '../../components/OrganizationProfile/OrganizationGeneralPage';
import { PageContext } from '../PageContext';

export function General({ children }: PropsWithChildren): ReactNode {
if (!children) {
return <OrganizationGeneralPage />;
}

return (
<PageContext.Provider value='general'>
<ProfileCard.Page>
<Col
elementDescriptor={descriptors.page}
sx={t => ({ gap: t.space.$8, isolation: 'isolate' })}
>
<Col
elementDescriptor={descriptors.profilePage}
elementId={descriptors.profilePage.setId('organizationGeneral')}
>
<Header.Root>
<Header.Title
localizationKey={localizationKeys('organizationProfile.start.headerTitle__general')}
sx={t => ({ marginBottom: t.space.$4 })}
textVariant='h2'
/>
</Header.Root>
{children}
</Col>
</Col>
</ProfileCard.Page>
</PageContext.Provider>
);
}
5 changes: 5 additions & 0 deletions packages/ui/src/composed/OrganizationProfile/Members.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { ReactNode } from 'react';

import { OrganizationMembers } from '../../components/OrganizationProfile/OrganizationMembers';

export const Members = (): ReactNode => <OrganizationMembers />;
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { ModuleManager } from '@clerk/shared/moduleManager';
import { useClerk, useOrganization, useUser } from '@clerk/shared/react';
import type { EnvironmentResource, OAuthProvider, OAuthScope } from '@clerk/shared/types';
import React, { useMemo, type ReactNode } from 'react';

import { AppearanceProvider } from '@/ui/customizables/AppearanceContext';
import { FlowMetadataProvider } from '@/ui/elements/contexts';
import type { Appearance } from '@/ui/internal/appearance';
import { RouteContext } from '@/ui/router/RouteContext';
import { InternalThemeProvider } from '@/ui/styledSystem';
import { StyleCacheProvider } from '@/ui/styledSystem/StyleCacheProvider';

import { EnvironmentProvider } from '../../contexts/EnvironmentContext';
import { ModuleManagerProvider } from '../../contexts/ModuleManagerContext';
import { OptionsProvider } from '../../contexts/OptionsContext';
import { SubscriberTypeContext } from '../../contexts/components/SubscriberType';
import { OrganizationProfileContext } from '../../contexts/components/OrganizationProfile';
import { AppearanceOverrides } from '../../elements/AppearanceOverrides';
import type { Elements } from '../../internal/appearance';
import { getModuleManager } from '../moduleManagerStore';
import { createComposedRouter } from '../stubRouter';

const fallbackModuleManager: ModuleManager = {
import: () => Promise.resolve(undefined) as any,
};

const composedOverrides: Elements = {
profilePageContent: { padding: 0 },
};

type OrganizationProfileProviderProps = React.PropsWithChildren<{
appearance?: Appearance;
additionalOAuthScopes?: Partial<Record<OAuthProvider, OAuthScope[]>>;
}>;

export const OrganizationProfileProvider = (props: OrganizationProfileProviderProps): ReactNode => {
const { children, appearance } = props;
const clerk = useClerk();
const { isLoaded, user } = useUser();
const { organization } = useOrganization();

const environment = (clerk as any).__internal_environment as EnvironmentResource | null | undefined;
const moduleManager: ModuleManager = getModuleManager() ?? fallbackModuleManager;
const router = useMemo(() => createComposedRouter(clerk.navigate), [clerk]);

if (!isLoaded || !user || !organization || !environment) {
return null;
}

const orgProfileCtxValue = {
componentName: 'OrganizationProfile' as const,
mode: 'mounted' as const,
routing: 'hash' as const,
path: undefined,
customPages: [],
};

const globalAppearance = clerk.__internal_getOption('appearance');

return (
<StyleCacheProvider>
<AppearanceProvider
appearanceKey='organizationProfile'
globalAppearance={globalAppearance}
appearance={appearance}
>
<FlowMetadataProvider flow='organizationProfile'>
<InternalThemeProvider>
<ModuleManagerProvider moduleManager={moduleManager}>
<OptionsProvider value={{}}>
<EnvironmentProvider value={environment}>
<RouteContext.Provider value={router}>
<SubscriberTypeContext.Provider value='organization'>
<OrganizationProfileContext.Provider value={orgProfileCtxValue}>
<AppearanceOverrides elements={composedOverrides}>{children}</AppearanceOverrides>
</OrganizationProfileContext.Provider>
</SubscriberTypeContext.Provider>
</RouteContext.Provider>
</EnvironmentProvider>
</OptionsProvider>
</ModuleManagerProvider>
</InternalThemeProvider>
</FlowMetadataProvider>
</AppearanceProvider>
</StyleCacheProvider>
);
};
24 changes: 24 additions & 0 deletions packages/ui/src/composed/OrganizationProfile/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { APIKeys } from './APIKeys';
import { Billing } from './Billing';
import { General } from './General';
import { Members } from './Members';
import { OrganizationProfileProvider } from './OrganizationProfileProvider';
import {
GeneralDeleteOrganization,
GeneralLeaveOrganization,
GeneralOrganizationProfile,
GeneralVerifiedDomains,
} from './sectionWrappers';
import { ConfigureSSO } from './ConfigureSSO';

export const OrganizationProfile = Object.assign(OrganizationProfileProvider, {
General,
Members,
Billing,
APIKeys,
ConfigureSSO,
GeneralOrganizationProfile,
GeneralVerifiedDomains,
GeneralLeaveOrganization,
GeneralDeleteOrganization,
});
Loading
Loading