Skip to content
Open
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
6 changes: 6 additions & 0 deletions packages/clerk-js/sandbox/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const AVAILABLE_COMPONENTS = [
'pricingTable',
'apiKeys',
'configureSSO',
'configureDirectorySync',
'oauthConsent',
'taskChooseOrganization',
'taskResetPassword',
Expand Down Expand Up @@ -153,6 +154,7 @@ const componentControls: Record<AvailableComponent, ComponentPropsControl> = {
pricingTable: buildComponentControls('pricingTable'),
apiKeys: buildComponentControls('apiKeys'),
configureSSO: buildComponentControls('configureSSO'),
configureDirectorySync: buildComponentControls('configureDirectorySync'),
oauthConsent: buildComponentControls('oauthConsent'),
taskChooseOrganization: buildComponentControls('taskChooseOrganization'),
taskResetPassword: buildComponentControls('taskResetPassword'),
Expand Down Expand Up @@ -425,6 +427,10 @@ void (async () => {
'/pricing-table': { mount: 'mountPricingTable', component: 'pricingTable' },
'/api-keys': { mount: 'mountAPIKeys', component: 'apiKeys' },
'/configure-sso': { mount: '__internal_mountConfigureSSO', component: 'configureSSO' },
'/configure-directory-sync': {
mount: '__internal_mountConfigureDirectorySync',
component: 'configureDirectorySync',
},
'/task-choose-organization': {
mount: 'mountTaskChooseOrganization',
component: 'taskChooseOrganization',
Expand Down
5 changes: 5 additions & 0 deletions packages/clerk-js/sandbox/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@
label="Configure SSO"
component="<ConfigureSSO />"
></nav-link>
<nav-link
href="/configure-directory-sync"
label="Configure Directory Sync"
component="<ConfigureDirectorySync />"
></nav-link>
<nav-link
href="/open-invite-members"
label="Open invite members"
Expand Down
71 changes: 71 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
disabledEmailAddressAttribute,
disabledOrganizationAPIKeysFeature,
disabledOrganizationsFeature,
disabledSelfServeDirectorySyncFeature,
disabledSelfServeSSOFeature,
disabledUserAPIKeysFeature,
isSignedInAndSingleSessionModeEnabled,
Expand Down Expand Up @@ -1681,6 +1682,76 @@ export class Clerk implements ClerkInterface {
void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node }));
};

/**
* Mount the Directory Sync onboarding component at the target element.
* Directory Sync rides on the self-serve SSO gates: it provisions through
* the organization's SSO connection, so the same preconditions apply.
*
* @param targetNode Target to mount the ConfigureDirectorySync component.
* @param props Configuration parameters.
* @hidden
*/
public __internal_mountConfigureDirectorySync = (node: HTMLDivElement, props?: ConfigureSSOProps) => {
const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({
for: 'organizations',
caller: 'ConfigureDirectorySync',
onClose: () => {
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('ConfigureDirectorySync'), {
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
});
},
});

if (!isOrganizationsEnabled) {
return;
}

const userExists = !noUserExists(this);
if (noOrganizationExists(this) && userExists) {
if (this.#instanceType === 'development') {
throw new ClerkRuntimeError(warnings.createCannotRenderComponentWhenOrgDoesNotExist('ConfigureDirectorySync'), {
code: CANNOT_RENDER_ORGANIZATION_MISSING_ERROR_CODE,
});
}
return;
}

if (disabledSelfServeDirectorySyncFeature(this, this.environment)) {
if (this.#instanceType === 'development') {
throw new ClerkRuntimeError(warnings.cannotRenderConfigureDirectorySyncComponentWhenDisabled, {
code: CANNOT_RENDER_SELF_SERVE_SSO_DISABLED_ERROR_CODE,
});
}
return;
}

this.assertComponentsReady(this.#clerkUI);
const component = 'ConfigureDirectorySync';
void this.#clerkUI
.then(ui => ui.ensureMounted({ preloadHint: component }))
.then(controls =>
controls.mountComponent({
name: component,
appearanceKey: 'configureSSO',
node,
props,
}),
);

this.telemetry?.record(eventPrebuiltComponentMounted(component, props));
};

/**
* Unmount the Directory Sync onboarding component from the target element.
* If there is no component mounted at the target node, results in a noop.
*
* @param targetNode Target node to unmount the ConfigureDirectorySync component from.
* @hidden
*/
public __internal_unmountConfigureDirectorySync = (node: HTMLDivElement) => {
void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node }));
};

public mountTaskChooseOrganization = (node: HTMLDivElement, props?: TaskChooseOrganizationProps) => {
const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({
for: 'organizations',
Expand Down
7 changes: 7 additions & 0 deletions packages/shared/src/internal/clerk-js/componentGuards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ export const disabledSelfServeSSOFeature: ComponentGuard = (clerk, environment)
return !environment?.userSettings.enterpriseSSO.self_serve_sso || !clerk.organization?.selfServeSSOEnabled;
};

export const disabledSelfServeDirectorySyncFeature: ComponentGuard = (clerk, environment) => {
return (
disabledSelfServeSSOFeature(clerk, environment) ||
!environment?.userSettings.enterpriseSSO.self_serve_directory_sync
);
};

export const disabledEmailAddressAttribute: ComponentGuard = (_, environment) => {
return !environment?.userSettings.attributes.email_address?.enabled;
};
7 changes: 5 additions & 2 deletions packages/shared/src/internal/clerk-js/warnings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@ const createMessageForDisabledOrganizations = (
| 'OrganizationList'
| 'CreateOrganization'
| 'TaskChooseOrganization'
| 'ConfigureSSO',
| 'ConfigureSSO'
| 'ConfigureDirectorySync',
) => {
return formatWarning(
`The <${componentName}/> cannot be rendered when the feature is turned off. Visit 'dashboard.clerk.com' to enable the feature. Since the feature is turned off, this is no-op.`,
);
};

const createCannotRenderComponentWhenOrgDoesNotExist = (
componentName: 'OrganizationProfile' | 'InviteMembers' | 'ConfigureSSO',
componentName: 'OrganizationProfile' | 'InviteMembers' | 'ConfigureSSO' | 'ConfigureDirectorySync',
) => {
return formatWarning(
`<${componentName}/> cannot render unless an organization is active. Since no organization is currently active, this is no-op.`,
Expand Down Expand Up @@ -86,6 +87,8 @@ const warnings = {
'<ConfigureSSO/> cannot render unless a user is signed in. Since no user is signed in, this is no-op.',
cannotRenderConfigureSSOComponentWhenDisabled:
'The <ConfigureSSO/> component cannot be rendered when self-serve SSO is disabled. Visit `https://dashboard.clerk.com` to enable the feature. Since self-serve SSO is disabled, this is no-op.',
cannotRenderConfigureDirectorySyncComponentWhenDisabled:
'The <ConfigureDirectorySync/> component cannot be rendered when self-serve Directory Sync is disabled. Since self-serve Directory Sync is disabled, this is no-op.',
cannotRenderConfigureSSOComponentWhenEmailAddressDisabled:
'The <ConfigureSSO/> component cannot be rendered when email addresses are disabled on the instance. Visit `https://dashboard.clerk.com` to enable email addresses. Since email addresses are disabled, this is no-op.',
};
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/types/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1967,6 +1967,7 @@ export type __internal_AttemptToEnableEnvironmentSettingParams = {
| 'CreateOrganization'
| 'TaskChooseOrganization'
| 'ConfigureSSO'
| 'ConfigureDirectorySync'
| 'useOrganizationList'
| 'useOrganization';
onClose?: () => void;
Expand Down
1 change: 1 addition & 0 deletions packages/shared/src/types/elementIds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export type ProfileSectionId =
| 'subscriptionsList'
| 'paymentMethods'
| 'sso'
| 'directorySync'
| 'ssoStatus'
| 'enableSso'
| 'ssoDomain'
Expand Down
17 changes: 17 additions & 0 deletions packages/shared/src/types/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,23 @@ export type __internal_LocalizationResource = {
tooltip__noRole: LocalizationValue;
tooltipLabel: LocalizationValue;
};
directorySyncSection: {
title: LocalizationValue;
badge__unconfigured: LocalizationValue;
badge__active: LocalizationValue;
badge__inactive: LocalizationValue;
description: LocalizationValue;
primaryButton__startConfiguration: LocalizationValue;
menuAction__edit: LocalizationValue;
menuAction__activate: LocalizationValue;
menuAction__deactivate: LocalizationValue;
menuAction__remove: LocalizationValue;
removeDialog: {
title: LocalizationValue;
subtitle: LocalizationValue;
confirmButton: LocalizationValue;
};
};
};
membersPage: {
detailsTitle__emptyRow: LocalizationValue;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import type { ConfigureSSOProps } from '@clerk/shared/types';
import React from 'react';

import { withCoreUserGuard } from '@/contexts';
import { Flow } from '@/customizables';
import { withCardStateProvider } from '@/elements/contexts';
import { ProfileCard } from '@/elements/ProfileCard';
import { Route, Switch } from '@/router';

import { ConfigureDirectorySyncWizard } from './ConfigureDirectorySyncWizard';
import { DirectorySyncNavbar } from './DirectorySyncNavbar';

/**
* Standalone host for the Directory Sync onboarding wizard, mirroring
* ConfigureSSO's shell. Reuses the configureSSO flow id/appearance until the
* flow gets its own appearance surface.
*/
const ConfigureDirectorySyncInternal = (): JSX.Element => {
return (
<Flow.Root flow='configureSSO'>
<Switch>
<Route>
<AuthenticatedContent />
</Route>
</Switch>
</Flow.Root>
);
};

const AuthenticatedContent = withCoreUserGuard(() => {
const contentRef = React.useRef<HTMLDivElement>(null);

return (
<ProfileCard.Root
sx={t => ({ display: 'grid', gridTemplateColumns: '1fr 3fr', height: t.sizes.$176, overflow: 'hidden' })}
>
<DirectorySyncNavbar contentRef={contentRef}>
<ConfigureDirectorySyncWizard />
</DirectorySyncNavbar>
</ProfileCard.Root>
);
});

export const ConfigureDirectorySync: React.ComponentType<ConfigureSSOProps> =
withCardStateProvider(ConfigureDirectorySyncInternal);
Loading
Loading