diff --git a/packages/clerk-js/sandbox/app.ts b/packages/clerk-js/sandbox/app.ts index 064c740eb27..9645df1688a 100644 --- a/packages/clerk-js/sandbox/app.ts +++ b/packages/clerk-js/sandbox/app.ts @@ -36,6 +36,7 @@ const AVAILABLE_COMPONENTS = [ 'taskChooseOrganization', 'taskResetPassword', 'taskSetupMFA', + 'configureSSO', ] as const; type AvailableComponent = (typeof AVAILABLE_COMPONENTS)[number]; @@ -140,6 +141,7 @@ const componentControls: Record = { taskChooseOrganization: buildComponentControls('taskChooseOrganization'), taskResetPassword: buildComponentControls('taskResetPassword'), taskSetupMFA: buildComponentControls('taskSetupMFA'), + configureSSO: buildComponentControls('configureSSO'), }; declare global { @@ -448,6 +450,9 @@ void (async () => { '/organization-profile': () => { Clerk.mountOrganizationProfile(app, componentControls.organizationProfile.getProps() ?? {}); }, + '/configure-sso': () => { + Clerk.mountConfigureSSO(app, componentControls.configureSSO.getProps() ?? {}); + }, '/organization-switcher': () => { Clerk.mountOrganizationSwitcher(app, componentControls.organizationSwitcher.getProps() ?? {}); }, diff --git a/packages/clerk-js/sandbox/template.html b/packages/clerk-js/sandbox/template.html index 422e7496cb8..d6914fa4979 100644 --- a/packages/clerk-js/sandbox/template.html +++ b/packages/clerk-js/sandbox/template.html @@ -139,6 +139,13 @@ >Organization Profile +
  • + Configure SSO +
  • ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); }; + public mountConfigureSSO = (node: HTMLDivElement, props?: ConfigureSSOProps) => { + this.assertComponentsReady(this.#clerkUI); + const component = 'ConfigureSSO'; + 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)); + }; + + public unmountConfigureSSO = (node: HTMLDivElement) => { + void this.#clerkUI?.then(ui => ui.ensureMounted()).then(controls => controls.unmountComponent({ node })); + }; + public mountCreateOrganization = (node: HTMLDivElement, props?: CreateOrganizationProps) => { const { isEnabled: isOrganizationsEnabled } = this.__internal_attemptToEnableEnvironmentSetting({ for: 'organizations', diff --git a/packages/localizations/src/en-US.ts b/packages/localizations/src/en-US.ts index 3b5b3e1bc61..d1b7eadc0ae 100644 --- a/packages/localizations/src/en-US.ts +++ b/packages/localizations/src/en-US.ts @@ -1506,4 +1506,33 @@ export const enUS: LocalizationResource = { noneAvailable: 'No Solana Web3 wallets detected. Please install a Web3 supported {{ solanaWalletsLink || link("wallet extension") }}.', }, + configureSSO: { + headerTitle: 'Configure Single Sign-On (SSO)', + navbar: { + title: 'SSO', + description: 'Configure SSO connection.', + }, + configureSSOSection: { + title: '{{step}}. Configure SSO', + actionButtonLabel: 'Configure enterprise connection', + actionButtonDescription: + 'Select the identity provider you want to connect to via SAML or OIDC, then configure the connection.', + }, + testSSOSection: { + title: '{{step}}. Test SSO', + actionButtonLabel: 'Test your SSO configuration', + actionButtonDescription: + 'Test your SSO configuration to verify you can successfully authenticate via your identity provider', + }, + verifyDomainSection: { + title: '{{step}}. Verify domain', + actionButtonLabel: 'Verify the domain for your enterprise connection', + actionButtonDescription: 'Verify the domain you want to enable the enterprise connection on.', + }, + enterpriseConnectionDrawer: { + title: 'Configure enterprise connection', + selectProviderTitle: 'Select your identity provider', + selectProviderDescription: "We'll guide you through the detailed setup process next.", + }, + }, } as const; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index c01f3171d25..82ca8b017f2 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -1,5 +1,6 @@ export { APIKeys, + ConfigureSSO, CreateOrganization, GoogleOneTap, OrganizationList, diff --git a/packages/react/src/components/uiComponents.tsx b/packages/react/src/components/uiComponents.tsx index a87b83af675..e57c829c418 100644 --- a/packages/react/src/components/uiComponents.tsx +++ b/packages/react/src/components/uiComponents.tsx @@ -1,4 +1,5 @@ import type { + ConfigureSSOProps, __internal_OAuthConsentProps, APIKeysProps, CreateOrganizationProps, @@ -644,6 +645,34 @@ export const APIKeys = withClerk( { component: 'ApiKeys', renderWhileLoading: true }, ); +export const ConfigureSSO = withClerk( + ({ clerk, component, fallback, ...props }: WithClerkProp) => { + const mountingStatus = useWaitForComponentMount(component); + const shouldShowFallback = mountingStatus === 'rendering' || !clerk.loaded; + + const rendererRootProps = { + ...(shouldShowFallback && fallback && { style: { display: 'none' } }), + }; + + return ( + <> + {shouldShowFallback && fallback} + {clerk.loaded && ( + + )} + + ); + }, + { component: 'ConfigureSSO', renderWhileLoading: true }, +); + export const OAuthConsent = withClerk( ({ clerk, component, fallback, ...props }: WithClerkProp<__internal_OAuthConsentProps & FallbackProp>) => { const mountingStatus = useWaitForComponentMount(component); diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 32b050d36c4..c7a705a096b 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -2,6 +2,7 @@ import { inBrowser } from '@clerk/shared/browser'; import { clerkEvents, createClerkEventBus } from '@clerk/shared/clerkEventBus'; import { loadClerkJSScript, loadClerkUIScript } from '@clerk/shared/loadClerkJsScript'; import type { + ConfigureSSOProps, __internal_AttemptToEnableEnvironmentSettingParams, __internal_AttemptToEnableEnvironmentSettingResult, __internal_CheckoutProps, @@ -162,6 +163,7 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { private premountTaskChooseOrganizationNodes = new Map(); private premountTaskResetPasswordNodes = new Map(); private premountTaskSetupMFANodes = new Map(); + private premountConfigureSSONodes = new Map(); // A separate Map of `addListener` method calls to handle multiple listeners. private premountAddListenerCalls = new Map< ListenerCallback, @@ -762,6 +764,10 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { clerkjs.mountTaskSetupMFA(node, props); }); + this.premountConfigureSSONodes.forEach((props, node) => { + clerkjs.mountConfigureSSO(node, props); + }); + /** * Only update status in case `clerk.status` is missing. In any other case, `clerk-js` should be the orchestrator. */ @@ -1282,6 +1288,22 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; + mountConfigureSSO = (node: HTMLDivElement, props?: ConfigureSSOProps): void => { + if (this.clerkjs && this.loaded) { + this.clerkjs.mountConfigureSSO(node, props); + } else { + this.premountConfigureSSONodes.set(node, props); + } + }; + + unmountConfigureSSO = (node: HTMLDivElement): void => { + if (this.clerkjs && this.loaded) { + this.clerkjs.unmountConfigureSSO(node); + } else { + this.premountConfigureSSONodes.delete(node); + } + }; + __internal_mountOAuthConsent = (node: HTMLDivElement, props?: __internal_OAuthConsentProps) => { if (this.clerkjs && this.loaded) { this.clerkjs.__internal_mountOAuthConsent(node, props); diff --git a/packages/shared/src/types/clerk.ts b/packages/shared/src/types/clerk.ts index f4ab6a08140..b7f74319c4b 100644 --- a/packages/shared/src/types/clerk.ts +++ b/packages/shared/src/types/clerk.ts @@ -661,6 +661,16 @@ export interface Clerk { */ unmountAPIKeys: (targetNode: HTMLDivElement) => void; + /** + * Mount the Configure SSO component at the target element. + */ + mountConfigureSSO: (targetNode: HTMLDivElement, props?: ConfigureSSOProps) => void; + + /** + * Unmount the Configure SSO component from the target element. + */ + unmountConfigureSSO: (targetNode: HTMLDivElement) => void; + /** * Mounts a OAuth consent component at the target element. * @@ -1794,6 +1804,13 @@ export type OrganizationProfileModalProps = WithoutRouting HTMLElement | null; }; +export type ConfigureSSOProps = RoutingOptions & { + /** + * Customisation options to fully match the Clerk components to your own brand. + */ + appearance?: ClerkAppearanceTheme; +}; + export type CreateOrganizationProps = RoutingOptions & { /** * Full URL or path to navigate to after creating a new Organization. diff --git a/packages/shared/src/types/elementIds.ts b/packages/shared/src/types/elementIds.ts index 77f71404daa..957e30679c1 100644 --- a/packages/shared/src/types/elementIds.ts +++ b/packages/shared/src/types/elementIds.ts @@ -45,7 +45,10 @@ export type ProfileSectionId = | 'organizationDomains' | 'manageVerifiedDomains' | 'subscriptionsList' - | 'paymentMethods'; + | 'paymentMethods' + | 'configureSSO' + | 'testSSO' + | 'verifyDomain'; export type ProfilePageId = 'account' | 'security' | 'organizationGeneral' | 'organizationMembers' | 'billing'; export type UserPreviewId = 'userButton' | 'personalWorkspace'; diff --git a/packages/shared/src/types/enterpriseAccount.ts b/packages/shared/src/types/enterpriseAccount.ts index ee9e0d94423..bb4cb25d112 100644 --- a/packages/shared/src/types/enterpriseAccount.ts +++ b/packages/shared/src/types/enterpriseAccount.ts @@ -1,6 +1,6 @@ +import type { SamlIdpSlug } from './enterpriseSso'; import type { OAuthProvider } from './oauth'; import type { ClerkResource } from './resource'; -import type { SamlIdpSlug } from './saml'; import type { EnterpriseAccountConnectionJSONSnapshot, EnterpriseAccountJSONSnapshot } from './snapshots'; import type { VerificationResource } from './verification'; diff --git a/packages/shared/src/types/saml.ts b/packages/shared/src/types/enterpriseSso.ts similarity index 83% rename from packages/shared/src/types/saml.ts rename to packages/shared/src/types/enterpriseSso.ts index 72a619dad70..15e44e1878b 100644 --- a/packages/shared/src/types/saml.ts +++ b/packages/shared/src/types/enterpriseSso.ts @@ -6,3 +6,5 @@ export type SamlIdp = { }; export type SamlIdpMap = Record; + +export type OidcIdpSlug = 'oidc_custom'; diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 2849a74a140..9b75a73c995 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -3,6 +3,7 @@ export type * from './apiKeysSettings'; export type * from './attributes'; export type * from './authConfig'; export type * from './authObject'; +export type * from './authorization'; export type * from './backupCode'; export type * from './billing'; export type * from './clerk'; @@ -48,7 +49,6 @@ export type * from './passwords'; export type * from './permission'; export type * from './phoneCodeChannel'; export type * from './phoneNumber'; -export type * from './authorization'; export type * from './protectConfig'; export type * from './redirects'; export type * from './resource'; @@ -58,8 +58,8 @@ export type * from './router'; * TODO @revamp-hooks: Drop this in the next major release. */ export type * from '../ui/types'; +export type * from './enterpriseSso'; export type * from './runtime-values'; -export type * from './saml'; export type * from './session'; export type * from './sessionVerification'; export type * from './signIn'; diff --git a/packages/shared/src/types/localization.ts b/packages/shared/src/types/localization.ts index 222509565bb..c70ad65a3e8 100644 --- a/packages/shared/src/types/localization.ts +++ b/packages/shared/src/types/localization.ts @@ -1442,6 +1442,33 @@ export type __internal_LocalizationResource = { continue: LocalizationValue<'walletName'>; noneAvailable: LocalizationValue<'solanaWalletsLink'>; }; + configureSSO: { + headerTitle: LocalizationValue; + navbar: { + title: LocalizationValue; + description: LocalizationValue; + }; + configureSSOSection: { + title: LocalizationValue<'step'>; + actionButtonLabel: LocalizationValue; + actionButtonDescription: LocalizationValue; + }; + testSSOSection: { + title: LocalizationValue<'step'>; + actionButtonLabel: LocalizationValue; + actionButtonDescription: LocalizationValue; + }; + verifyDomainSection: { + title: LocalizationValue<'step'>; + actionButtonLabel: LocalizationValue; + actionButtonDescription: LocalizationValue; + }; + enterpriseConnectionDrawer: { + title: LocalizationValue; + selectProviderTitle: LocalizationValue; + selectProviderDescription: LocalizationValue; + }; + }; }; type WithParamName = T & diff --git a/packages/shared/src/types/runtime-values.ts b/packages/shared/src/types/runtime-values.ts index 1fa5fc6ce94..5756f12000c 100644 --- a/packages/shared/src/types/runtime-values.ts +++ b/packages/shared/src/types/runtime-values.ts @@ -1,5 +1,5 @@ +import type { SamlIdpMap } from './enterpriseSso'; import type { OAuthProvider, OAuthProviderData } from './oauth'; -import type { SamlIdpMap } from './saml'; import type { OAuthStrategy, Web3Strategy } from './strategies'; import type { Web3Provider, Web3ProviderData } from './web3'; diff --git a/packages/ui/src/components/ConfigureSSO/ConfigureEnterpriseConnectionDrawer.tsx b/packages/ui/src/components/ConfigureSSO/ConfigureEnterpriseConnectionDrawer.tsx new file mode 100644 index 00000000000..e80c9167d36 --- /dev/null +++ b/packages/ui/src/components/ConfigureSSO/ConfigureEnterpriseConnectionDrawer.tsx @@ -0,0 +1,125 @@ +import { iconImageUrl } from '@clerk/shared/constants'; +import type { OAuthProvider, OidcIdpSlug, SamlIdpSlug } from '@clerk/shared/types'; +import { useState } from 'react'; + +import { ProviderIcon } from '@/common'; +import { Col, localizationKeys, Text } from '@/customizables'; +import { Drawer } from '@/elements/Drawer'; +import { IconButton } from '@/elements/IconButton'; + +type ConfigureEnterpriseConnectionDrawerProps = { + open: boolean; + onOpenChange: (open: boolean) => void; +}; + +export const ConfigureEnterpriseConnectionDrawer = (props: ConfigureEnterpriseConnectionDrawerProps) => { + const { open, onOpenChange } = props; + const [provider, setProvider] = useState(null); + + return ( + + + + + + {provider ? ( + + {provider} + + ) : ( + ({ padding: t.space.$4 })} + > + + + + + + + SAML + + setProvider('saml_okta')} + /> + setProvider('saml_custom')} + /> + + + + OpenID Connect (OIDC) + setProvider('oidc_custom')} + /> + + + )} + + + + ); +}; + +type EnterpriseConnectionProviderSlug = Extract | OidcIdpSlug; + +type EnterpriseConnectionProviderButtonProps = { + provider: EnterpriseConnectionProviderSlug; + onClick: () => void; +}; + +const providerButtons: Record = { + saml_okta: { + iconUrl: iconImageUrl('okta', 'svg'), + name: 'Okta Workforce', + }, + saml_custom: { + iconUrl: iconImageUrl('saml', 'svg'), + name: 'Custom SAML Provider', + }, + oidc_custom: { + iconUrl: iconImageUrl('oidc', 'svg'), + name: 'Custom OIDC Provider', + }, +} as const; + +const EnterpriseConnectionProviderButton = (props: EnterpriseConnectionProviderButtonProps) => { + const { provider, onClick } = props; + + const providerWithoutPrefix = provider.replace(/(oauth_|saml_)/, '').trim() as OAuthProvider; + const { iconUrl, name } = providerButtons[provider]; + + return ( + ({ gap: t.space.$2, justifyContent: 'flex-start', padding: t.space.$4 })} + type='button' + onClick={onClick} + variant='outline' + icon={ + + } + aria-label={name} + > + {name} + + ); +}; diff --git a/packages/ui/src/components/ConfigureSSO/ConfigureSSONavbar.tsx b/packages/ui/src/components/ConfigureSSO/ConfigureSSONavbar.tsx new file mode 100644 index 00000000000..f2f5245a96c --- /dev/null +++ b/packages/ui/src/components/ConfigureSSO/ConfigureSSONavbar.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import { NavBar, NavbarContextProvider } from '@/ui/elements/Navbar'; + +import { localizationKeys } from '../../localization'; +import type { PropsOfComponent } from '../../styledSystem'; + +export const ConfigureSSONavbar = ( + props: React.PropsWithChildren, 'contentRef'>>, +) => { + return ( + + + {props.children} + + ); +}; diff --git a/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.test.tsx b/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.test.tsx new file mode 100644 index 00000000000..23feb5122aa --- /dev/null +++ b/packages/ui/src/components/ConfigureSSO/__tests__/ConfigureSSO.test.tsx @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest'; + +import { bindCreateFixtures } from '@/test/create-fixtures'; +import { render, screen, waitFor } from '@/test/utils'; + +import { ConfigureSSO } from '../'; + +const { createFixtures } = bindCreateFixtures('ConfigureSSO'); + +describe('ConfigureSSO', () => { + it('opens the enterprise connection drawer when the Configure SSO action is clicked', async () => { + const { wrapper } = await createFixtures(f => { + f.withUser({ email_addresses: ['test@clerk.com'] }); + }); + + const { userEvent, findByRole, findByText } = render(, { wrapper }); + + const configureButton = await findByRole('button', { name: /configure enterprise connection/i }); + await userEvent.click(configureButton); + + expect(await findByText(/select your identity provider/i)).toBeVisible(); + }); + + it('shows the step to verify domain when the primary email is not verified', async () => { + const { wrapper } = await createFixtures(f => { + f.withUser({ + email_addresses: [ + { + email_address: 'unverified@clerk.com', + verification: { + status: 'unverified', + strategy: 'email_link', + attempts: null, + expire_at: null, + }, + }, + ], + }); + }); + + render(, { wrapper }); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /verify the domain for your enterprise connection/i })).toBeVisible(); + }); + + expect(screen.getByText(/verify the domain you want to enable the enterprise connection on/i)).toBeVisible(); + }); +}); diff --git a/packages/ui/src/components/ConfigureSSO/index.tsx b/packages/ui/src/components/ConfigureSSO/index.tsx new file mode 100644 index 00000000000..51de8e20b87 --- /dev/null +++ b/packages/ui/src/components/ConfigureSSO/index.tsx @@ -0,0 +1,123 @@ +import { useUser } from '@clerk/shared/react/index'; +import type { ConfigureSSOProps } from '@clerk/shared/types'; +import React, { type ComponentProps } from 'react'; + +import { CONFIGURE_SSO_CARD_SCROLLBOX_ID } from '@/constants'; +import { withCoreUserGuard } from '@/contexts'; +import { Col, Flow, localizationKeys, Text } from '@/customizables'; +import { withCardStateProvider } from '@/elements/contexts'; +import { Header } from '@/elements/Header'; +import { ProfileCard } from '@/elements/ProfileCard'; +import { ProfileSection } from '@/elements/Section'; +import { ArrowRightIcon } from '@/icons'; + +import { ConfigureEnterpriseConnectionDrawer } from './ConfigureEnterpriseConnectionDrawer'; +import { ConfigureSSONavbar } from './ConfigureSSONavbar'; + +const ConfigureSSOInternal = withCoreUserGuard(() => { + const contentRef = React.useRef(null); + + return ( + + + ({ display: 'grid', gridTemplateColumns: '1fr 3fr', height: t.sizes.$176, overflow: 'hidden' })} + > + + + + + + + ); +}); + +const ConfigureSSOProfileContent = () => { + const { user } = useUser(); + const [enterpriseConnectionDrawerOpen, setEnterpriseConnectionDrawerOpen] = React.useState(false); + + const hasPrimaryEmailAddressVerified = user?.primaryEmailAddress?.verification.status === 'verified'; + + return ( + <> + + + ({ marginBottom: t.space.$4 })} + textVariant='h2' + /> + + + setEnterpriseConnectionDrawerOpen(true)} + /> + + {/* TODO -> Disable test SSO section until enterprise connection is created */} + + + {!hasPrimaryEmailAddressVerified && ( + // TODO -> Test this section with a user who has a primary email address that is not verified + + )} + + + + + ); +}; + +type ConfigureSSOSectionProps = Pick, 'title' | 'id'> & { + actionButtonLabel: NonNullable['localizationKey']>; + actionButtonDescription: NonNullable['localizationKey']>; + onActionClick?: () => void; +}; + +const ConfigureSSOSection = (props: ConfigureSSOSectionProps) => { + const { title, id, actionButtonLabel, actionButtonDescription, onActionClick } = props; + + return ( + + + + + ({ + paddingInlineStart: t.space.$2x5, + })} + colorScheme='secondary' + /> + + + ); +}; + +export const ConfigureSSO: React.ComponentType = withCardStateProvider(ConfigureSSOInternal); diff --git a/packages/ui/src/constants.ts b/packages/ui/src/constants.ts index 430524de6f9..17640ac08b4 100644 --- a/packages/ui/src/constants.ts +++ b/packages/ui/src/constants.ts @@ -21,3 +21,4 @@ export const USER_BUTTON_ITEM_ID = { export const USER_PROFILE_CARD_SCROLLBOX_ID = 'clerk-profileCardScrollBox'; export const ORGANIZATION_PROFILE_CARD_SCROLLBOX_ID = 'clerk-organizationProfileScrollBox'; +export const CONFIGURE_SSO_CARD_SCROLLBOX_ID = 'clerk-configureSSOScrollBox'; diff --git a/packages/ui/src/contexts/ClerkUIComponentsContext.tsx b/packages/ui/src/contexts/ClerkUIComponentsContext.tsx index 3b27bf9aa63..fdd29a2a7e7 100644 --- a/packages/ui/src/contexts/ClerkUIComponentsContext.tsx +++ b/packages/ui/src/contexts/ClerkUIComponentsContext.tsx @@ -169,6 +169,8 @@ export function ComponentContextProvider({ ); + case 'ConfigureSSO': + return <>{children}; default: throw new Error(`Unknown component context: ${componentName}`); } diff --git a/packages/ui/src/elements/ArrowBlockButton.tsx b/packages/ui/src/elements/ArrowBlockButton.tsx index 7ebc83e5407..65bead1a1ca 100644 --- a/packages/ui/src/elements/ArrowBlockButton.tsx +++ b/packages/ui/src/elements/ArrowBlockButton.tsx @@ -9,7 +9,7 @@ import type { PropsOfComponent, ThemableCssProp } from '../styledSystem'; type ArrowBlockButtonProps = PropsOfComponent & { rightIcon?: React.ComponentType | null; rightIconSx?: ThemableCssProp; - leftIcon?: React.ComponentType | React.ReactElement; + leftIcon?: React.ComponentType | React.ReactElement | null; leftIconSx?: ThemableCssProp; childrenSx?: ThemableCssProp; leftIconElementDescriptor?: ElementDescriptor; diff --git a/packages/ui/src/elements/Section.tsx b/packages/ui/src/elements/Section.tsx index d5cc3846248..21070ecb129 100644 --- a/packages/ui/src/elements/Section.tsx +++ b/packages/ui/src/elements/Section.tsx @@ -158,8 +158,9 @@ const ProfileSectionItem = (props: ProfileSectionItemProps) => { ); }; -type ProfileSectionButtonProps = PropsOfComponent & { +type ProfileSectionButtonProps = Omit, 'leftIcon'> & { id: ProfileSectionId; + leftIcon?: PropsOfComponent['leftIcon'] | null; }; const ProfileSectionButton = (props: ProfileSectionButtonProps) => { @@ -186,7 +187,10 @@ const ProfileSectionButton = (props: ProfileSectionButtonProps) => { }; const ProfileSectionArrowButton = forwardRef((props, ref) => { - const { children, leftIcon = Plus, id, sx, localizationKey, ...rest } = props; + const { children, leftIcon, id, sx, localizationKey, ...rest } = props; + + const resolvedLeftIcon = leftIcon === null ? undefined : (leftIcon ?? Plus); + return ( ({ width: t.sizes.$4, height: t.sizes.$4 })} ref={ref} {...rest} diff --git a/packages/ui/src/elements/contexts/index.tsx b/packages/ui/src/elements/contexts/index.tsx index cecccfe3d88..a585afc947b 100644 --- a/packages/ui/src/elements/contexts/index.tsx +++ b/packages/ui/src/elements/contexts/index.tsx @@ -104,7 +104,8 @@ export type FlowMetadata = { | 'taskChooseOrganization' | 'enableOrganizations' | 'taskResetPassword' - | 'taskSetupMfa'; + | 'taskSetupMfa' + | 'configureSSO'; part?: | 'start' | 'emailCode' diff --git a/packages/ui/src/internal/appearance.ts b/packages/ui/src/internal/appearance.ts index 71bc4815a94..07fb8de8594 100644 --- a/packages/ui/src/internal/appearance.ts +++ b/packages/ui/src/internal/appearance.ts @@ -1018,6 +1018,7 @@ export type OAuthConsentTheme = Theme; export type TaskChooseOrganizationTheme = Theme; export type TaskResetPasswordTheme = Theme; export type TaskSetupMFATheme = Theme; +export type ConfigureSSOTheme = Theme; type GlobalAppearanceOptions = { /** @@ -1106,4 +1107,8 @@ export type Appearance = T & * Theme overrides that only apply to the `` component */ enableOrganizations?: T; + /** + * Theme overrides that only apply to the `` component. + */ + configureSSO?: T; }; diff --git a/packages/ui/src/lazyModules/components.ts b/packages/ui/src/lazyModules/components.ts index 0cc57f424ca..d31c74e21ae 100644 --- a/packages/ui/src/lazyModules/components.ts +++ b/packages/ui/src/lazyModules/components.ts @@ -32,6 +32,7 @@ const componentImportPaths = { OAuthConsent: () => import(/* webpackChunkName: "oauthConsent" */ '../components/OAuthConsent/OAuthConsent'), EnableOrganizationsPrompt: () => import(/* webpackChunkName: "enableOrganizationsPrompt" */ '../components/devPrompts/EnableOrganizationsPrompt'), + ConfigureSSO: () => import(/* webpackChunkName: "configureSSO" */ '../components/ConfigureSSO'), } as const; export const SignIn = lazy(() => componentImportPaths.SignIn().then(module => ({ default: module.SignIn }))); @@ -146,6 +147,10 @@ export const OAuthConsent = lazy(() => componentImportPaths.OAuthConsent().then(module => ({ default: module.OAuthConsent })), ); +export const ConfigureSSO = lazy(() => + componentImportPaths.ConfigureSSO().then(module => ({ default: module.ConfigureSSO })), +); + export const SessionTasks = lazy(() => componentImportPaths.SessionTasks().then(module => ({ default: module.SessionTasks })), ); @@ -185,6 +190,7 @@ export const ClerkComponents = { TaskChooseOrganization, TaskResetPassword, TaskSetupMFA, + ConfigureSSO, }; export type ClerkComponentName = keyof typeof ClerkComponents; diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 7a354a46ae4..296ddd44d6d 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -1,4 +1,5 @@ import type { + ConfigureSSOProps, __internal_CheckoutProps, __internal_OAuthConsentProps, __internal_PlanDetailsProps, @@ -67,7 +68,8 @@ export type AvailableComponentProps = | __internal_OAuthConsentProps | TaskChooseOrganizationProps | TaskResetPasswordProps - | TaskSetupMFAProps; + | TaskSetupMFAProps + | ConfigureSSOProps; type ComponentMode = 'modal' | 'mounted'; type SignInMode = 'modal' | 'redirect'; @@ -166,6 +168,10 @@ export type TaskSetupMFACtx = TaskSetupMFAProps & { componentName: 'TaskSetupMFA'; }; +export type ConfigureSSOCtx = ConfigureSSOProps & { + componentName: 'ConfigureSSO'; +}; + export type OAuthConsentCtx = { componentName: 'OAuthConsent'; /** @@ -254,5 +260,6 @@ export type AvailableComponentCtx = | PlanDetailsCtx | TaskChooseOrganizationCtx | TaskResetPasswordCtx - | TaskSetupMFACtx; + | TaskSetupMFACtx + | ConfigureSSOCtx; export type AvailableComponentName = AvailableComponentCtx['componentName'];