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
2 changes: 2 additions & 0 deletions .changeset/profile-extract-sections.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
---
---
2 changes: 1 addition & 1 deletion packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const AuthenticatedContent = withCoreUserGuard(() => {
);
});

const ConfigureSSOContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
export const ConfigureSSOContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
const {
isLoading,
enterpriseConnection,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,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
@@ -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,34 +57,28 @@ const DeleteOrganizationScreen = () => {
export const OrganizationGeneralPage = () => {
return (
<ProfileCard.Page>
<Col
elementDescriptor={descriptors.page}
sx={t => ({ gap: t.space.$8 })}
<ProfileCard.PagePanel
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.PagePanel>
</ProfileCard.Page>
);
};

const OrganizationProfileSection = () => {
/**
* Renders the organization profile section (name, logo) with inline edit when the user has
* `org:sys_profile:manage`.
*
* @returns The profile section, or `null` when no organization is active.
*/
export const OrganizationProfileSection = (): JSX.Element | null => {
const { organization } = useOrganization();

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

const OrganizationDomainsSection = () => {
/**
* Renders the verified-domains section.
*
* @returns The domains section, or `null` when domains are disabled, no organization is active, or
* there are no domains and the user cannot add any.
*/
export const OrganizationDomainsSection = (): JSX.Element | null => {
const { organizationSettings } = useEnvironment();
const { organization, domains } = useOrganization({ domains: { infinite: true } });
const canManageDomains = useProtect({ permission: 'org:sys_domains:manage' });
Expand Down Expand Up @@ -190,7 +189,12 @@ const OrganizationDomainsSection = () => {
);
};

const OrganizationLeaveSection = () => {
/**
* Renders the "leave organization" action in the danger section.
*
* @returns The leave-organization section, or `null` when no organization is active.
*/
export const OrganizationLeaveSection = (): JSX.Element | null => {
const { organization } = useOrganization();

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

const OrganizationDeleteSection = () => {
/**
* Renders the "delete organization" action in the danger section.
*
* @returns The delete-organization section, or `null` when no organization is active, the user
* lacks `org:sys_profile:delete`, or admin delete is disabled.
*/
export const OrganizationDeleteSection = (): JSX.Element | null => {
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
89 changes: 22 additions & 67 deletions packages/ui/src/components/UserProfile/AccountPage.tsx
Original file line number Diff line number Diff line change
@@ -1,81 +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 {
AccountConnectedAccounts,
AccountEmails,
AccountEnterpriseAccounts,
AccountPhone,
AccountUsername,
AccountWeb3,
} from './AccountSections';
import { UserProfileSection } from './UserProfileSection';
import { isAttributeAvailable } from './utils';
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 = isAttributeAvailable(attributes.username);
const showEmail = isAttributeAvailable(attributes.email_address);
const showPhone = isAttributeAvailable(attributes.phone_number);
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 })}
<ProfileCard.PagePanel
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.PagePanel>
</ProfileCard.Page>
);
});
90 changes: 90 additions & 0 deletions packages/ui/src/components/UserProfile/AccountSections.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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 { isAttributeAvailable } from './utils';
import { Web3Section } from './Web3Section';

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

if (!isAttributeAvailable(attributes.username)) {
return null;
}

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

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

if (!isAttributeAvailable(attributes.email_address)) {
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 (!isAttributeAvailable(attributes.phone_number)) {
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} />;
}
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 @@ -27,7 +27,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
Loading
Loading