Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
762aae7
refactor(ui): copy ConfigureSSO wizard into elements/ and split Foote…
iagodahlem May 6, 2026
e31a32b
refactor(ui): drop layout wrapper from Wizard root
iagodahlem May 6, 2026
da8c880
feat(ui): add Breadcrumbs primitive for ConfigureSSO
iagodahlem May 6, 2026
02317de
refactor(ui): convert Breadcrumbs to declarative children-based API
iagodahlem May 6, 2026
801810d
feat(ui): add ConfigureSSO layout components
iagodahlem May 6, 2026
23b49fd
refactor(ui): inline StepLayout chrome into each ConfigureSSO step
iagodahlem May 6, 2026
8dd147b
refactor(ui): switch ConfigureSSO to layered wizard architecture
iagodahlem May 6, 2026
05b559d
refactor(ui): make ConfigureSSO Wizard state-driven and render children
iagodahlem May 6, 2026
c563316
feat(ui): add ConfigureSSO navbar and skeleton, refine breadcrumb sizing
iagodahlem May 6, 2026
8246e23
refactor(ui): extract ProfileCard primitives, refine ConfigureSSO layout
iagodahlem May 7, 2026
642f51a
refactor(ui): rename Breadcrumbs to Stepper and make it UI-only
iagodahlem May 7, 2026
39002b6
refactor(ui): add ProfileCardBody and ProfileCardSection primitives
iagodahlem May 7, 2026
3afda1b
feat(ui): add Stepper.Skeleton primitive
iagodahlem May 7, 2026
21486b7
refactor(ui): compound Step primitives, fix sx merging, refactor rema…
iagodahlem May 7, 2026
0830fd7
refactor(ui): move Step primitive into elements/ and tidy ProfileCard
iagodahlem May 7, 2026
0dc6b56
refactor(ui): use common.mutedBackground for ProfileCardFooter
iagodahlem May 7, 2026
3aa9d76
refactor(ui): make ConfigureSSO step footers self-owned, drop registry
iagodahlem May 7, 2026
7bf55cd
style(ui): add minHeight to ProfileCardFooter
iagodahlem May 7, 2026
e4f92df
refactor(ui): make Step.Footer a compound of UI-only buttons
iagodahlem May 7, 2026
8cd823a
refactor(ui): consolidate verify-email-domain into one outer step
iagodahlem May 7, 2026
ee672f3
refactor(ui): consolidate step files and split Step.Body from Section
iagodahlem May 7, 2026
b2afce0
refactor(ui): split TestConfigurationStep into test URL and results s…
iagodahlem May 7, 2026
b823d63
refactor(ui): hoist Step.Body to outer wizard and center inner sections
iagodahlem May 7, 2026
1c4bbdc
feat(ui): add SelectProviderStep boilerplate as the first wizard step
iagodahlem May 7, 2026
3362d6a
feat(ui): allow Wizard to take an initialStepId
iagodahlem May 7, 2026
7f84287
chore(ui): drop trailing periods from SSO step descriptions
iagodahlem May 7, 2026
9f4aa4c
refactor(ui): inline ConfigureSSO wizard steps into parent component
iagodahlem May 7, 2026
0260497
chore(ui): add changeset for ConfigureSSO wizard refactor
iagodahlem May 7, 2026
955699e
chore(ui): remove unused ConfigureSSOWizard module
iagodahlem May 7, 2026
80a1933
refactor(ui): remove initialStepId override from ConfigureSSO wizard
iagodahlem May 7, 2026
7bd72fe
chore(ui): rename ConfigureSSO descriptors and wire missing sites
iagodahlem May 8, 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 .changeset/wizard-architecture-layers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/ui': patch
---

Refactor `<__experimental_ConfigureSSO />` into a layered primitive set: a state-driven Wizard, a UI-only Stepper, a `Step` compound, and ProfileCard chrome. No public component API change. Drops the central FooterActionsContext registry — each step now renders its own footer via `Step.Footer.Previous` / `Step.Footer.Continue` purely-presentational compounds. Adds a SelectProviderStep boilerplate filtered out of the breadcrumb.
203 changes: 53 additions & 150 deletions packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import { __internal_useUserEnterpriseConnections, useOrganization, useUser } from '@clerk/shared/react';
import { __internal_useUserEnterpriseConnections } from '@clerk/shared/react';
import type { __experimental_ConfigureSSOProps } from '@clerk/shared/types';
import React from 'react';

import { useEnvironment, withCoreUserGuard } from '@/contexts';
import { Box, Col, descriptors, Flex, Flow, Icon, localizationKeys, Text, useAppearance } from '@/customizables';
import { ApplicationLogo } from '@/elements/ApplicationLogo';
import { withCoreUserGuard } from '@/contexts';
import { Col, descriptors, Flow } from '@/customizables';
import { withCardStateProvider } from '@/elements/contexts';
import { NavBar, NavbarContextProvider } from '@/elements/Navbar';
import { ProfileCard } from '@/elements/ProfileCard';
import { BoxIcon } from '@/icons';
import { Route, Switch } from '@/router';

import { ConfigureSSOFlowProvider } from './ConfigureSSOContext';
import { ConfigureCreateApp, ConfirmationStep, ProvideEmail, TestConfigurationStep, VerifyDomainStep } from './steps';
import { ConfigureSSOWizard } from './wizard';
import { ConfigureSSOHeader } from './ConfigureSSOHeader';
import { ConfigureSSONavbar } from './ConfigureSSONavbar';
import { ConfigureSSOSkeleton } from './ConfigureSSOSkeleton';
import { Wizard } from './elements/Wizard';
import { ConfigureStep, ConfirmationStep, SelectProviderStep, TestConfigurationStep, VerifyDomainStep } from './steps';

const ConfigureSSOInternal = () => {
return (
Expand All @@ -29,73 +29,12 @@ const ConfigureSSOInternal = () => {

const AuthenticatedContent = withCoreUserGuard(() => {
const contentRef = React.useRef<HTMLDivElement>(null);
const { applicationName, logoImageUrl } = useEnvironment().displayConfig;
const { organizationSettings } = useEnvironment();
const { parsedOptions } = useAppearance();
const hasLogo = Boolean(parsedOptions.logoImageUrl || logoImageUrl);

const { data: enterpriseConnections, isLoading: isLoadingEnterpriseConnections } =
__internal_useUserEnterpriseConnections({ enabled: true });
// Currently FAPI only supports one enterprise connection per user
const enterpriseConnection = enterpriseConnections?.[0];

return (
<ProfileCard.Root
sx={t => ({ display: 'grid', gridTemplateColumns: '1fr 3fr', height: t.sizes.$176, overflow: 'hidden' })}
>
<NavbarContextProvider contentRef={contentRef}>
<NavBar
header={
<Flex
align='center'
sx={t => ({
gap: t.space.$2,
padding: `${t.space.$none} ${t.space.$3}`,
maxWidth: '100%',
})}
>
{hasLogo ? (
<ApplicationLogo
sx={t => ({ width: t.space.$9, height: t.space.$9, borderRadius: t.radii.$md, overflow: 'hidden' })}
/>
) : (
<Box
sx={t => ({
width: t.space.$9,
height: t.space.$9,
flexShrink: 0,
borderRadius: t.radii.$md,
backgroundColor: t.colors.$primary500,
color: t.colors.$colorPrimaryForeground,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
})}
aria-hidden
>
<Icon
icon={BoxIcon}
sx={t => ({ width: t.sizes.$4, height: t.sizes.$4 })}
/>
</Box>
)}

<Col sx={{ minWidth: 0 }}>
<Text
as='p'
truncate
>
{applicationName}
</Text>
{organizationSettings.enabled && <OrganizationSidebarSubtitle />}
</Col>
</Flex>
}
titleSx={t => ({ fontSize: t.fontSizes.$lg })}
title={localizationKeys('configureSSO.navbar.title')}
routes={[]}
contentRef={contentRef}
/>
<ConfigureSSONavbar contentRef={contentRef}>
<Col
ref={contentRef}
elementDescriptor={descriptors.scrollBox}
Expand All @@ -108,100 +47,64 @@ const AuthenticatedContent = withCoreUserGuard(() => {
borderWidth: t.borderWidths.$normal,
borderStyle: t.borderStyles.$solid,
borderColor: t.colors.$borderAlpha150,
marginBlock: '-1px',
marginInlineEnd: '-1px',
flex: 1,
})}
>
<ConfigureSSOFlowProvider
enterpriseConnection={enterpriseConnection}
isLoading={isLoadingEnterpriseConnections}
>
<ConfigureSSOSteps />
</ConfigureSSOFlowProvider>
<ConfigureSSOCardContent />
</Col>
</NavbarContextProvider>
</ConfigureSSONavbar>
</ProfileCard.Root>
);
});

const ConfigureSSOSteps = () => {
const { user } = useUser();
const ConfigureSSOCardContent = () => {
const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });
// Currently FAPI only supports one enterprise connection per user
const enterpriseConnection = enterpriseConnections?.[0];

const primaryEmailAddress = user?.primaryEmailAddress;
// Initial-load gate at root — wizard never sees isLoading
if (isLoading && !enterpriseConnection) {
return <ConfigureSSOSkeleton />;
}

return (
<ConfigureSSOWizard>
<ConfigureSSOWizard.Step
id='verify-email-domain'
path='verify-email-domain'
label='Verify domain'
>
<ConfigureSSOWizard>
{!primaryEmailAddress && (
<ConfigureSSOWizard.Step
id='provide-email'
path='provide-email'
>
<ProvideEmail />
</ConfigureSSOWizard.Step>
)}
<ConfigureSSOWizard.Step
id='verify-domain'
path='verify-domain'
>
<VerifyDomainStep />
</ConfigureSSOWizard.Step>
</ConfigureSSOWizard>
</ConfigureSSOWizard.Step>
<ConfigureSSOWizard.Step
id='configure'
path='configure'
label='Configure'
>
<ConfigureSSOWizard>
{/* TODO: Implement configure steps */}
<ConfigureSSOWizard.Step
id='create-app'
path='create-app'
>
<ConfigureCreateApp />
</ConfigureSSOWizard.Step>
</ConfigureSSOWizard>
</ConfigureSSOWizard.Step>
<ConfigureSSOWizard.Step
id='test'
path='test'
label='Test'
>
<TestConfigurationStep />
</ConfigureSSOWizard.Step>
<ConfigureSSOWizard.Step
id='confirmation'
path='confirmation'
label='Confirmation'
>
<ConfirmationStep />
</ConfigureSSOWizard.Step>
</ConfigureSSOWizard>
);
};
<ConfigureSSOFlowProvider enterpriseConnection={enterpriseConnection}>
<Wizard>
<ConfigureSSOHeader />

const OrganizationSidebarSubtitle = () => {
const { organization } = useOrganization();
<Wizard.Step id='select-provider'>
<SelectProviderStep />
</Wizard.Step>

if (!organization) {
return null;
}
<Wizard.Step
id='verify-domain'
label='Verify domain'
>
<VerifyDomainStep />
</Wizard.Step>

return (
<Text
as='span'
truncate
sx={t => ({ color: t.colors.$colorMutedForeground })}
>
{organization?.name}
</Text>
<Wizard.Step
id='configure'
label='Configure'
>
<ConfigureStep />
</Wizard.Step>

<Wizard.Step
id='test'
label='Test'
>
<TestConfigurationStep />
</Wizard.Step>

<Wizard.Step
id='confirmation'
label='Confirmation'
>
<ConfirmationStep />
</Wizard.Step>
</Wizard>
</ConfigureSSOFlowProvider>
);
};

Expand Down
19 changes: 4 additions & 15 deletions packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,39 +11,28 @@ export interface ConfigureSSOData {
enterpriseConnection: EnterpriseConnectionResource | undefined;
}

export interface ConfigureSSOContextValue extends ConfigureSSOData {
/**
* `true` while the parent is still fetching the user's enterprise
* connection
*/
isLoading: boolean;
}

interface ConfigureSSOFlowProviderProps {
enterpriseConnection: EnterpriseConnectionResource | undefined;
isLoading: boolean;
}

const ConfigureSSOFlowContext = React.createContext<ConfigureSSOContextValue | null>(null);
const ConfigureSSOFlowContext = React.createContext<ConfigureSSOData | null>(null);
ConfigureSSOFlowContext.displayName = 'ConfigureSSOFlowContext';

export const ConfigureSSOFlowProvider = ({
enterpriseConnection,
isLoading,
children,
}: PropsWithChildren<ConfigureSSOFlowProviderProps>): JSX.Element => {
const value = React.useMemo<ConfigureSSOContextValue>(
const value = React.useMemo<ConfigureSSOData>(
() => ({
enterpriseConnection,
isLoading,
}),
[enterpriseConnection, isLoading],
[enterpriseConnection],
);

return <ConfigureSSOFlowContext.Provider value={value}>{children}</ConfigureSSOFlowContext.Provider>;
};

export const useConfigureSSOFlow = (): ConfigureSSOContextValue => {
export const useConfigureSSOFlow = (): ConfigureSSOData => {
const ctx = React.useContext(ConfigureSSOFlowContext);
if (!ctx) {
throw new Error('useConfigureSSOFlow called outside <ConfigureSSOFlowProvider>.');
Expand Down
63 changes: 63 additions & 0 deletions packages/ui/src/components/ConfigureSSO/ConfigureSSOHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Icon, Text } from '@/customizables';
import { Check } from '@/icons';

import { ProfileCardHeader } from './elements/ProfileCard';
import { Stepper } from './elements/Stepper';
import { useWizard } from './elements/Wizard';

export const ConfigureSSOHeader = (): JSX.Element => {
const { activeSteps, currentStep, goToStep } = useWizard();
// Select Provider isn't part of the visual breadcrumb per the design —
// filter it out here. The wizard still tracks it as the first step
// for navigation (goNext from it advances to verify-domain, Previous
// is naturally disabled because isFirstStep is true).
const visibleSteps = activeSteps.filter(step => step.id !== 'select-provider');
const currentIndex = visibleSteps.findIndex(step => step.id === currentStep?.id);

return (
<ProfileCardHeader>
<Stepper>
{visibleSteps.map((step, index) => {
const isCurrent = index === currentIndex;
const isCompleted = step.isCompleted ?? index < currentIndex;
const isReachable = isCompleted || index <= currentIndex;
const showCheck = isCompleted && !isCurrent;

return (
<Stepper.Item
key={step.id}
label={step.label}
bullet={
showCheck ? (
<Icon
icon={Check}
sx={theme => ({ width: theme.sizes.$2, height: theme.sizes.$2, color: theme.colors.$white })}
/>
) : (
<Text
as='span'
sx={theme => ({
fontSize: theme.fontSizes.$xs,
fontWeight: theme.fontWeights.$semibold,
color: theme.colors.$colorBackground,
})}
>
{index + 1}
</Text>
)
}
isCurrent={isCurrent}
isCompleted={isCompleted}
isReachable={isReachable}
onClick={() => {
if (isReachable) {
void goToStep(step.id);
}
}}
/>
);
})}
</Stepper>
</ProfileCardHeader>
);
};
Loading
Loading