Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/profile-runtime-composition-sketch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/ui': patch
'@clerk/clerk-js': patch
'@clerk/shared': patch
---

Keep composed profile runtime wiring inside the UI package instead of exposing Clerk's module manager as a new internal API.
4 changes: 0 additions & 4 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2917,10 +2917,6 @@ export class Clerk implements ClerkInterface {
return this.environment;
}

get __internal_moduleManager() {
return this.#moduleManager;
}

// TODO: Fix this properly
// eslint-disable-next-line @typescript-eslint/require-await
__internal_setEnvironment = async (env: EnvironmentJSON) => {
Expand Down
5 changes: 0 additions & 5 deletions packages/shared/src/types/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,6 @@ export interface Clerk {
*/
__internal_getOption<K extends keyof ClerkOptions>(key: K): ClerkOptions[K];

/**
* @internal
*/
__internal_moduleManager: import('../moduleManager').ModuleManager;

frontendApi: string;

/** Clerk Publishable Key string. */
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/src/ClerkUI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ 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';
import { setModuleManager } from './internal/moduleManagerStore';

/**
* Core rendering engine for Clerk's prebuilt UI components.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { useOrganization } from '@clerk/shared/react';

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

import { Protect, useProtect } from '../../common';
import { useEnvironment } from '../../contexts';
import { Col, descriptors, localizationKeys, Text } from '../../customizables';
import { Col, localizationKeys, Text } from '../../customizables';
import { Action } from '../../elements/Action';
import { useActionContext } from '../../elements/Action/ActionRoot';
import { DeleteOrganizationForm, LeaveOrganizationForm } from './ActionConfirmationPage';
Expand Down Expand Up @@ -58,29 +57,17 @@ const DeleteOrganizationScreen = () => {
export const OrganizationGeneralPage = () => {
return (
<ProfileCard.Page>
<Col
elementDescriptor={descriptors.page}
sx={t => ({ gap: t.space.$8, isolation: 'isolate' })}
<ProfileCard.PageSection
pageId='organizationGeneral'
titleKey={localizationKeys('organizationProfile.start.headerTitle__general')}
>
<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>
<OrganizationProfileSection />
<Protect permission='org:sys_domains:read'>
<OrganizationDomainsSection />
</Protect>
<OrganizationLeaveSection />
<OrganizationDeleteSection />
</Col>
</Col>
<OrganizationProfileSection />
<Protect permission='org:sys_domains:read'>
<OrganizationDomainsSection />
</Protect>
<OrganizationLeaveSection />
<OrganizationDeleteSection />
</ProfileCard.PageSection>
</ProfileCard.Page>
);
};
Expand Down
65 changes: 65 additions & 0 deletions packages/ui/src/components/Profile/ProfileRuntimeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { ModuleManager } from '@clerk/shared/moduleManager';
import type { EnvironmentResource } from '@clerk/shared/types';
import type { PropsWithChildren, ReactNode } from 'react';

import { AppearanceProvider } from '@/ui/customizables/AppearanceContext';
import { FlowMetadataProvider } from '@/ui/elements/contexts';
import type { Appearance } from '@/ui/internal/appearance';
import { RouteContext, type RouteContextValue } 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 { AppearanceOverrides } from '../../elements/AppearanceOverrides';
import type { Elements } from '../../internal/appearance';

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

type ProfileRuntimeProviderProps = PropsWithChildren<{
environment: EnvironmentResource;
moduleManager: ModuleManager;
router: RouteContextValue;
appearanceKey: 'userProfile' | 'organizationProfile';
flow: 'userProfile' | 'organizationProfile';
globalAppearance: Appearance | undefined;
appearance?: Appearance;
}>;

export function ProfileRuntimeProvider({
children,
environment,
moduleManager,
router,
appearanceKey,
flow,
globalAppearance,
appearance,
}: ProfileRuntimeProviderProps): ReactNode {
return (
<StyleCacheProvider>
<AppearanceProvider
appearanceKey={appearanceKey}
globalAppearance={globalAppearance}
appearance={appearance}
>
<FlowMetadataProvider flow={flow}>
<InternalThemeProvider>
<ModuleManagerProvider moduleManager={moduleManager}>
<OptionsProvider value={{}}>
<EnvironmentProvider value={environment}>
<RouteContext.Provider value={router}>
<AppearanceOverrides elements={profileCompositionOverrides}>{children}</AppearanceOverrides>
</RouteContext.Provider>
</EnvironmentProvider>
</OptionsProvider>
</ModuleManagerProvider>
</InternalThemeProvider>
</FlowMetadataProvider>
</AppearanceProvider>
</StyleCacheProvider>
);
}
88 changes: 22 additions & 66 deletions packages/ui/src/components/UserProfile/AccountPage.tsx
Original file line number Diff line number Diff line change
@@ -1,80 +1,36 @@
import { useUser } from '@clerk/shared/react';

import { Card } from '@/ui/elements/Card';
import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
import { Header } from '@/ui/elements/Header';
import { ProfileCard } from '@/ui/elements/ProfileCard';

import { useEnvironment, useUserProfileContext } from '../../contexts';
import { Col, descriptors, localizationKeys } from '../../customizables';
import { ConnectedAccountsSection } from './ConnectedAccountsSection';
import { EmailsSection } from './EmailsSection';
import { EnterpriseAccountsSection } from './EnterpriseAccountsSection';
import { PhoneSection } from './PhoneSection';
import { UsernameSection } from './UsernameSection';
import { localizationKeys } from '../../customizables';
import {
AccountUsername,
AccountEmails,
AccountPhone,
AccountConnectedAccounts,
AccountEnterpriseAccounts,
AccountWeb3,
} from './AccountSections';
import { UserProfileSection } from './UserProfileSection';
import { Web3Section } from './Web3Section';

export const AccountPage = withCardStateProvider(() => {
const { attributes, social, enterpriseSSO } = useEnvironment().userSettings;
const card = useCardState();
const { user } = useUser();
const { shouldAllowIdentificationCreation, immutableAttributes } = useUserProfileContext();

const showUsername = attributes.username?.enabled;
const showEmail = attributes.email_address?.enabled;
const showPhone = attributes.phone_number?.enabled;
const showConnectedAccounts = social && Object.values(social).filter(p => p.enabled).length > 0;
const showEnterpriseAccounts = user && enterpriseSSO.enabled;
const showWeb3 = attributes.web3_wallet?.enabled;

const isEmailImmutable = immutableAttributes.has('email_address');
const isPhoneImmutable = immutableAttributes.has('phone_number');
const isUsernameImmutable = immutableAttributes.has('username');

return (
<ProfileCard.Page>
<Col
elementDescriptor={descriptors.page}
sx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground, isolation: 'isolate' })}
<ProfileCard.PageSection
pageId='account'
titleKey={localizationKeys('userProfile.start.headerTitle__account')}
alertContent={card.error}
outerSx={t => ({ gap: t.space.$8, color: t.colors.$colorForeground, isolation: 'isolate' })}
>
<Col
elementDescriptor={descriptors.profilePage}
elementId={descriptors.profilePage.setId('account')}
>
<Header.Root>
<Header.Title
localizationKey={localizationKeys('userProfile.start.headerTitle__account')}
sx={t => ({ marginBottom: t.space.$4 })}
textVariant='h2'
/>
</Header.Root>

<Card.Alert>{card.error}</Card.Alert>

<UserProfileSection />
{showUsername && <UsernameSection isImmutable={isUsernameImmutable} />}
{showEmail && (
<EmailsSection
shouldAllowCreation={shouldAllowIdentificationCreation && !isEmailImmutable}
shouldAllowDeletion={!isEmailImmutable}
/>
)}
{showPhone && (
<PhoneSection
shouldAllowCreation={shouldAllowIdentificationCreation && !isPhoneImmutable}
shouldAllowDeletion={!isPhoneImmutable}
/>
)}
{showConnectedAccounts && (
<ConnectedAccountsSection shouldAllowCreation={shouldAllowIdentificationCreation} />
)}

{/*TODO-STEP-UP: Verify that these work as expected*/}
{showEnterpriseAccounts && <EnterpriseAccountsSection />}
{showWeb3 && <Web3Section shouldAllowCreation={shouldAllowIdentificationCreation} />}
</Col>
</Col>
<UserProfileSection />
<AccountUsername />
<AccountEmails />
<AccountPhone />
<AccountConnectedAccounts />
<AccountEnterpriseAccounts />
<AccountWeb3 />
</ProfileCard.PageSection>
</ProfileCard.Page>
);
});
77 changes: 77 additions & 0 deletions packages/ui/src/components/UserProfile/AccountSections.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { useUser } from '@clerk/shared/react';
import type { ReactNode } from 'react';

import { useEnvironment, useUserProfileContext } from '../../contexts';
import { ConnectedAccountsSection } from './ConnectedAccountsSection';
import { EmailsSection } from './EmailsSection';
import { EnterpriseAccountsSection } from './EnterpriseAccountsSection';
import { PhoneSection } from './PhoneSection';
import { UsernameSection } from './UsernameSection';
import { Web3Section } from './Web3Section';

export function AccountUsername(): ReactNode {
const { attributes } = useEnvironment().userSettings;
const { immutableAttributes } = useUserProfileContext();

if (!attributes.username?.enabled) return null;

const isImmutable = immutableAttributes.has('username');
return <UsernameSection isImmutable={isImmutable} />;
}

export function AccountEmails(): ReactNode {
const { attributes } = useEnvironment().userSettings;
const { shouldAllowIdentificationCreation, immutableAttributes } = useUserProfileContext();

if (!attributes.email_address?.enabled) return null;

const isImmutable = immutableAttributes.has('email_address');
return (
<EmailsSection
shouldAllowCreation={shouldAllowIdentificationCreation && !isImmutable}
shouldAllowDeletion={!isImmutable}
/>
);
}

export function AccountPhone(): ReactNode {
const { attributes } = useEnvironment().userSettings;
const { shouldAllowIdentificationCreation, immutableAttributes } = useUserProfileContext();

if (!attributes.phone_number?.enabled) return null;

const isImmutable = immutableAttributes.has('phone_number');
return (
<PhoneSection
shouldAllowCreation={shouldAllowIdentificationCreation && !isImmutable}
shouldAllowDeletion={!isImmutable}
/>
);
}

export function AccountConnectedAccounts(): ReactNode {
const { social } = useEnvironment().userSettings;
const { shouldAllowIdentificationCreation } = useUserProfileContext();

if (!social || Object.values(social).filter(p => p.enabled).length === 0) return null;

return <ConnectedAccountsSection shouldAllowCreation={shouldAllowIdentificationCreation} />;
}

export function AccountEnterpriseAccounts(): ReactNode {
const { enterpriseSSO } = useEnvironment().userSettings;
const { user } = useUser();

if (!user || !enterpriseSSO.enabled) return null;

return <EnterpriseAccountsSection />;
}

export function AccountWeb3(): ReactNode {
const { attributes } = useEnvironment().userSettings;
const { shouldAllowIdentificationCreation } = useUserProfileContext();

if (!attributes.web3_wallet?.enabled) return null;

return <Web3Section shouldAllowCreation={shouldAllowIdentificationCreation} />;
}
Loading
Loading