Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ed15c44
refactor(ui): copy ConfigureSSO wizard into elements/ and split Foote…
iagodahlem May 6, 2026
431d64e
refactor(ui): drop layout wrapper from Wizard root
iagodahlem May 6, 2026
c3bab8c
feat(ui): add Breadcrumbs primitive for ConfigureSSO
iagodahlem May 6, 2026
a789efd
refactor(ui): convert Breadcrumbs to declarative children-based API
iagodahlem May 6, 2026
b1282e9
feat(ui): add ConfigureSSO layout components
iagodahlem May 6, 2026
1f73e31
refactor(ui): inline StepLayout chrome into each ConfigureSSO step
iagodahlem May 6, 2026
04942d4
refactor(ui): switch ConfigureSSO to layered wizard architecture
iagodahlem May 6, 2026
9e97f6c
refactor(ui): make ConfigureSSO Wizard state-driven and render children
iagodahlem May 6, 2026
4f1f0cc
feat(ui): add ConfigureSSO navbar and skeleton, refine breadcrumb sizing
iagodahlem May 6, 2026
239224c
refactor(ui): extract ProfileCard primitives, refine ConfigureSSO layout
iagodahlem May 7, 2026
a23f4d2
refactor(ui): rename Breadcrumbs to Stepper and make it UI-only
iagodahlem May 7, 2026
575d753
refactor(ui): add ProfileCardBody and ProfileCardSection primitives
iagodahlem May 7, 2026
d6f39a9
feat(ui): add Stepper.Skeleton primitive
iagodahlem May 7, 2026
99d5e7b
refactor(ui): compound Step primitives, fix sx merging, refactor rema…
iagodahlem May 7, 2026
de290ea
refactor(ui): move Step primitive into elements/ and tidy ProfileCard
iagodahlem May 7, 2026
1b12e46
refactor(ui): use common.mutedBackground for ProfileCardFooter
iagodahlem May 7, 2026
585dad3
refactor(ui): make ConfigureSSO step footers self-owned, drop registry
iagodahlem May 7, 2026
7382eff
style(ui): add minHeight to ProfileCardFooter
iagodahlem May 7, 2026
0fdad62
refactor(ui): make Step.Footer a compound of UI-only buttons
iagodahlem May 7, 2026
b4454b0
refactor(ui): consolidate verify-email-domain into one outer step
iagodahlem May 7, 2026
34d2154
refactor(ui): consolidate step files and split Step.Body from Section
iagodahlem May 7, 2026
8cc89c5
refactor(ui): split TestConfigurationStep into test URL and results s…
iagodahlem May 7, 2026
12a6ad0
refactor(ui): hoist Step.Body to outer wizard and center inner sections
iagodahlem May 7, 2026
0bebb36
feat(ui): add SelectProviderStep boilerplate as the first wizard step
iagodahlem May 7, 2026
3868397
feat(ui): allow Wizard to take an initialStepId
iagodahlem May 7, 2026
cf2880c
chore(ui): drop trailing periods from SSO step descriptions
iagodahlem May 7, 2026
9db457c
refactor(ui): inline ConfigureSSO wizard steps into parent component
iagodahlem May 7, 2026
737d05d
chore(ui): add changeset for ConfigureSSO wizard refactor
iagodahlem May 7, 2026
0ff55e3
chore(ui): remove unused ConfigureSSOWizard module
iagodahlem May 7, 2026
b8ba139
refactor(ui): remove initialStepId override from ConfigureSSO wizard
iagodahlem May 7, 2026
cd08d2c
chore(ui): rename ConfigureSSO descriptors and wire missing sites
iagodahlem May 8, 2026
94f83bd
refactor(ui): simplify Stepper.Item API
iagodahlem May 8, 2026
c7a50b2
refactor(ui): polish ConfigureSSO Stepper styling
iagodahlem May 11, 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
42 changes: 42 additions & 0 deletions packages/ui/src/components/ConfigureSSO/ConfigureSSOHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useLocalizations } from '@/customizables';

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();
const { t } = useLocalizations();
// 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 labelText = step.label ? (typeof step.label === 'string' ? step.label : t(step.label)) : '';

return (
<Stepper.Item
key={step.id}
bullet={index + 1}
isCurrent={isCurrent}
isCompleted={isCompleted}
isReachable={isReachable}
onClick={() => void goToStep(step.id)}
>
{labelText}
</Stepper.Item>
);
})}
</Stepper>
</ProfileCardHeader>
);
};
Loading
Loading