Skip to content

Commit 0f8aed2

Browse files
authored
chore(ui): Layer architecture for <ConfigureSSO /> (#8493)
1 parent ba158ac commit 0f8aed2

31 files changed

Lines changed: 1268 additions & 1169 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/ui': patch
3+
---
4+
5+
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.

packages/ui/src/components/ConfigureSSO/ConfigureSSO.tsx

Lines changed: 53 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
1-
import { __internal_useUserEnterpriseConnections, useOrganization, useUser } from '@clerk/shared/react';
1+
import { __internal_useUserEnterpriseConnections } from '@clerk/shared/react';
22
import type { __experimental_ConfigureSSOProps } from '@clerk/shared/types';
33
import React from 'react';
44

5-
import { useEnvironment, withCoreUserGuard } from '@/contexts';
6-
import { Box, Col, descriptors, Flex, Flow, Icon, localizationKeys, Text, useAppearance } from '@/customizables';
7-
import { ApplicationLogo } from '@/elements/ApplicationLogo';
5+
import { withCoreUserGuard } from '@/contexts';
6+
import { Col, descriptors, Flow } from '@/customizables';
87
import { withCardStateProvider } from '@/elements/contexts';
9-
import { NavBar, NavbarContextProvider } from '@/elements/Navbar';
108
import { ProfileCard } from '@/elements/ProfileCard';
11-
import { BoxIcon } from '@/icons';
129
import { Route, Switch } from '@/router';
1310

1411
import { ConfigureSSOFlowProvider } from './ConfigureSSOContext';
15-
import { ConfigureCreateApp, ConfirmationStep, ProvideEmail, TestConfigurationStep, VerifyDomainStep } from './steps';
16-
import { ConfigureSSOWizard } from './wizard';
12+
import { ConfigureSSOHeader } from './ConfigureSSOHeader';
13+
import { ConfigureSSONavbar } from './ConfigureSSONavbar';
14+
import { ConfigureSSOSkeleton } from './ConfigureSSOSkeleton';
15+
import { Wizard } from './elements/Wizard';
16+
import { ConfigureStep, ConfirmationStep, SelectProviderStep, TestConfigurationStep, VerifyDomainStep } from './steps';
1717

1818
const ConfigureSSOInternal = () => {
1919
return (
@@ -29,73 +29,12 @@ const ConfigureSSOInternal = () => {
2929

3030
const AuthenticatedContent = withCoreUserGuard(() => {
3131
const contentRef = React.useRef<HTMLDivElement>(null);
32-
const { applicationName, logoImageUrl } = useEnvironment().displayConfig;
33-
const { organizationSettings } = useEnvironment();
34-
const { parsedOptions } = useAppearance();
35-
const hasLogo = Boolean(parsedOptions.logoImageUrl || logoImageUrl);
36-
37-
const { data: enterpriseConnections, isLoading: isLoadingEnterpriseConnections } =
38-
__internal_useUserEnterpriseConnections({ enabled: true });
39-
// Currently FAPI only supports one enterprise connection per user
40-
const enterpriseConnection = enterpriseConnections?.[0];
4132

4233
return (
4334
<ProfileCard.Root
4435
sx={t => ({ display: 'grid', gridTemplateColumns: '1fr 3fr', height: t.sizes.$176, overflow: 'hidden' })}
4536
>
46-
<NavbarContextProvider contentRef={contentRef}>
47-
<NavBar
48-
header={
49-
<Flex
50-
align='center'
51-
sx={t => ({
52-
gap: t.space.$2,
53-
padding: `${t.space.$none} ${t.space.$3}`,
54-
maxWidth: '100%',
55-
})}
56-
>
57-
{hasLogo ? (
58-
<ApplicationLogo
59-
sx={t => ({ width: t.space.$9, height: t.space.$9, borderRadius: t.radii.$md, overflow: 'hidden' })}
60-
/>
61-
) : (
62-
<Box
63-
sx={t => ({
64-
width: t.space.$9,
65-
height: t.space.$9,
66-
flexShrink: 0,
67-
borderRadius: t.radii.$md,
68-
backgroundColor: t.colors.$primary500,
69-
color: t.colors.$colorPrimaryForeground,
70-
display: 'flex',
71-
alignItems: 'center',
72-
justifyContent: 'center',
73-
})}
74-
aria-hidden
75-
>
76-
<Icon
77-
icon={BoxIcon}
78-
sx={t => ({ width: t.sizes.$4, height: t.sizes.$4 })}
79-
/>
80-
</Box>
81-
)}
82-
83-
<Col sx={{ minWidth: 0 }}>
84-
<Text
85-
as='p'
86-
truncate
87-
>
88-
{applicationName}
89-
</Text>
90-
{organizationSettings.enabled && <OrganizationSidebarSubtitle />}
91-
</Col>
92-
</Flex>
93-
}
94-
titleSx={t => ({ fontSize: t.fontSizes.$lg })}
95-
title={localizationKeys('configureSSO.navbar.title')}
96-
routes={[]}
97-
contentRef={contentRef}
98-
/>
37+
<ConfigureSSONavbar contentRef={contentRef}>
9938
<Col
10039
ref={contentRef}
10140
elementDescriptor={descriptors.scrollBox}
@@ -108,100 +47,64 @@ const AuthenticatedContent = withCoreUserGuard(() => {
10847
borderWidth: t.borderWidths.$normal,
10948
borderStyle: t.borderStyles.$solid,
11049
borderColor: t.colors.$borderAlpha150,
111-
marginBlock: '-1px',
112-
marginInlineEnd: '-1px',
11350
flex: 1,
11451
})}
11552
>
116-
<ConfigureSSOFlowProvider
117-
enterpriseConnection={enterpriseConnection}
118-
isLoading={isLoadingEnterpriseConnections}
119-
>
120-
<ConfigureSSOSteps />
121-
</ConfigureSSOFlowProvider>
53+
<ConfigureSSOCardContent />
12254
</Col>
123-
</NavbarContextProvider>
55+
</ConfigureSSONavbar>
12456
</ProfileCard.Root>
12557
);
12658
});
12759

128-
const ConfigureSSOSteps = () => {
129-
const { user } = useUser();
60+
const ConfigureSSOCardContent = () => {
61+
const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });
62+
// Currently FAPI only supports one enterprise connection per user
63+
const enterpriseConnection = enterpriseConnections?.[0];
13064

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

13370
return (
134-
<ConfigureSSOWizard>
135-
<ConfigureSSOWizard.Step
136-
id='verify-email-domain'
137-
path='verify-email-domain'
138-
label='Verify domain'
139-
>
140-
<ConfigureSSOWizard>
141-
{!primaryEmailAddress && (
142-
<ConfigureSSOWizard.Step
143-
id='provide-email'
144-
path='provide-email'
145-
>
146-
<ProvideEmail />
147-
</ConfigureSSOWizard.Step>
148-
)}
149-
<ConfigureSSOWizard.Step
150-
id='verify-domain'
151-
path='verify-domain'
152-
>
153-
<VerifyDomainStep />
154-
</ConfigureSSOWizard.Step>
155-
</ConfigureSSOWizard>
156-
</ConfigureSSOWizard.Step>
157-
<ConfigureSSOWizard.Step
158-
id='configure'
159-
path='configure'
160-
label='Configure'
161-
>
162-
<ConfigureSSOWizard>
163-
{/* TODO: Implement configure steps */}
164-
<ConfigureSSOWizard.Step
165-
id='create-app'
166-
path='create-app'
167-
>
168-
<ConfigureCreateApp />
169-
</ConfigureSSOWizard.Step>
170-
</ConfigureSSOWizard>
171-
</ConfigureSSOWizard.Step>
172-
<ConfigureSSOWizard.Step
173-
id='test'
174-
path='test'
175-
label='Test'
176-
>
177-
<TestConfigurationStep />
178-
</ConfigureSSOWizard.Step>
179-
<ConfigureSSOWizard.Step
180-
id='confirmation'
181-
path='confirmation'
182-
label='Confirmation'
183-
>
184-
<ConfirmationStep />
185-
</ConfigureSSOWizard.Step>
186-
</ConfigureSSOWizard>
187-
);
188-
};
71+
<ConfigureSSOFlowProvider enterpriseConnection={enterpriseConnection}>
72+
<Wizard>
73+
<ConfigureSSOHeader />
18974

190-
const OrganizationSidebarSubtitle = () => {
191-
const { organization } = useOrganization();
75+
<Wizard.Step id='select-provider'>
76+
<SelectProviderStep />
77+
</Wizard.Step>
19278

193-
if (!organization) {
194-
return null;
195-
}
79+
<Wizard.Step
80+
id='verify-domain'
81+
label='Verify domain'
82+
>
83+
<VerifyDomainStep />
84+
</Wizard.Step>
19685

197-
return (
198-
<Text
199-
as='span'
200-
truncate
201-
sx={t => ({ color: t.colors.$colorMutedForeground })}
202-
>
203-
{organization?.name}
204-
</Text>
86+
<Wizard.Step
87+
id='configure'
88+
label='Configure'
89+
>
90+
<ConfigureStep />
91+
</Wizard.Step>
92+
93+
<Wizard.Step
94+
id='test'
95+
label='Test'
96+
>
97+
<TestConfigurationStep />
98+
</Wizard.Step>
99+
100+
<Wizard.Step
101+
id='confirmation'
102+
label='Confirmation'
103+
>
104+
<ConfirmationStep />
105+
</Wizard.Step>
106+
</Wizard>
107+
</ConfigureSSOFlowProvider>
205108
);
206109
};
207110

packages/ui/src/components/ConfigureSSO/ConfigureSSOContext.tsx

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,39 +11,28 @@ export interface ConfigureSSOData {
1111
enterpriseConnection: EnterpriseConnectionResource | undefined;
1212
}
1313

14-
export interface ConfigureSSOContextValue extends ConfigureSSOData {
15-
/**
16-
* `true` while the parent is still fetching the user's enterprise
17-
* connection
18-
*/
19-
isLoading: boolean;
20-
}
21-
2214
interface ConfigureSSOFlowProviderProps {
2315
enterpriseConnection: EnterpriseConnectionResource | undefined;
24-
isLoading: boolean;
2516
}
2617

27-
const ConfigureSSOFlowContext = React.createContext<ConfigureSSOContextValue | null>(null);
18+
const ConfigureSSOFlowContext = React.createContext<ConfigureSSOData | null>(null);
2819
ConfigureSSOFlowContext.displayName = 'ConfigureSSOFlowContext';
2920

3021
export const ConfigureSSOFlowProvider = ({
3122
enterpriseConnection,
32-
isLoading,
3323
children,
3424
}: PropsWithChildren<ConfigureSSOFlowProviderProps>): JSX.Element => {
35-
const value = React.useMemo<ConfigureSSOContextValue>(
25+
const value = React.useMemo<ConfigureSSOData>(
3626
() => ({
3727
enterpriseConnection,
38-
isLoading,
3928
}),
40-
[enterpriseConnection, isLoading],
29+
[enterpriseConnection],
4130
);
4231

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

46-
export const useConfigureSSOFlow = (): ConfigureSSOContextValue => {
35+
export const useConfigureSSOFlow = (): ConfigureSSOData => {
4736
const ctx = React.useContext(ConfigureSSOFlowContext);
4837
if (!ctx) {
4938
throw new Error('useConfigureSSOFlow called outside <ConfigureSSOFlowProvider>.');
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useLocalizations } from '@/customizables';
2+
3+
import { ProfileCardHeader } from './elements/ProfileCard';
4+
import { Stepper } from './elements/Stepper';
5+
import { useWizard } from './elements/Wizard';
6+
7+
export const ConfigureSSOHeader = (): JSX.Element => {
8+
const { activeSteps, currentStep, goToStep } = useWizard();
9+
const { t } = useLocalizations();
10+
// Select Provider isn't part of the visual breadcrumb per the design —
11+
// filter it out here. The wizard still tracks it as the first step
12+
// for navigation (goNext from it advances to verify-domain, Previous
13+
// is naturally disabled because isFirstStep is true).
14+
const visibleSteps = activeSteps.filter(step => step.id !== 'select-provider');
15+
const currentIndex = visibleSteps.findIndex(step => step.id === currentStep?.id);
16+
17+
return (
18+
<ProfileCardHeader>
19+
<Stepper>
20+
{visibleSteps.map((step, index) => {
21+
const isCurrent = index === currentIndex;
22+
const isCompleted = step.isCompleted ?? index < currentIndex;
23+
const isReachable = isCompleted || index <= currentIndex;
24+
const labelText = step.label ? (typeof step.label === 'string' ? step.label : t(step.label)) : '';
25+
26+
return (
27+
<Stepper.Item
28+
key={step.id}
29+
bullet={index + 1}
30+
isCurrent={isCurrent}
31+
isCompleted={isCompleted}
32+
isReachable={isReachable}
33+
onClick={() => void goToStep(step.id)}
34+
>
35+
{labelText}
36+
</Stepper.Item>
37+
);
38+
})}
39+
</Stepper>
40+
</ProfileCardHeader>
41+
);
42+
};

0 commit comments

Comments
 (0)