Skip to content

Commit 70761a8

Browse files
committed
Query for enterprise connections
1 parent 9e0fafe commit 70761a8

15 files changed

Lines changed: 243 additions & 364 deletions

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

Lines changed: 61 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
1-
import { useOrganization } from '@clerk/shared/react/index';
1+
import { __internal_useUserEnterpriseConnections, useOrganization } from '@clerk/shared/react';
22
import type { __experimental_ConfigureSSOProps } from '@clerk/shared/types';
33
import React from 'react';
44

55
import { useEnvironment, withCoreUserGuard } from '@/contexts';
6-
import { Box, Col, Flex, Flow, Icon, localizationKeys, Text, useAppearance } from '@/customizables';
6+
import {
7+
Box,
8+
Col,
9+
descriptors,
10+
Flex,
11+
Flow,
12+
Icon,
13+
localizationKeys,
14+
Spinner,
15+
Text,
16+
useAppearance,
17+
} from '@/customizables';
718
import { ApplicationLogo } from '@/elements/ApplicationLogo';
819
import { withCardStateProvider } from '@/elements/contexts';
920
import { NavBar, NavbarContextProvider } from '@/elements/Navbar';
@@ -36,6 +47,11 @@ const AuthenticatedContent = withCoreUserGuard(() => {
3647
const { parsedOptions } = useAppearance();
3748
const hasLogo = Boolean(parsedOptions.logoImageUrl || logoImageUrl);
3849

50+
const { data: enterpriseConnections, isLoading: isLoadingEnterpriseConnections } =
51+
__internal_useUserEnterpriseConnections({ enabled: true });
52+
// Currently FAPI only supports one enterprise connection per user
53+
const enterpriseConnection = enterpriseConnections?.[0];
54+
3955
return (
4056
<ProfileCard.Root
4157
sx={t => ({ display: 'grid', gridTemplateColumns: '1fr 3fr', height: t.sizes.$176, overflow: 'hidden' })}
@@ -93,31 +109,58 @@ const AuthenticatedContent = withCoreUserGuard(() => {
93109
routes={[]}
94110
contentRef={contentRef}
95111
/>
96-
<ConfigureSSOFlowProvider>
97-
<ConfigureSSOWizardPanel contentRef={contentRef} />
98-
</ConfigureSSOFlowProvider>
112+
<Col
113+
ref={contentRef}
114+
elementDescriptor={descriptors.scrollBox}
115+
sx={t => ({
116+
backgroundColor: t.colors.$colorBackground,
117+
position: 'relative',
118+
borderRadius: t.radii.$lg,
119+
width: '100%',
120+
overflow: 'hidden',
121+
borderWidth: t.borderWidths.$normal,
122+
borderStyle: t.borderStyles.$solid,
123+
borderColor: t.colors.$borderAlpha150,
124+
marginBlock: '-1px',
125+
marginInlineEnd: '-1px',
126+
flex: 1,
127+
})}
128+
>
129+
{isLoadingEnterpriseConnections ? (
130+
<Flex
131+
align='center'
132+
justify='center'
133+
sx={{ flex: 1 }}
134+
>
135+
<Spinner
136+
size='lg'
137+
colorScheme='primary'
138+
elementDescriptor={descriptors.spinner}
139+
/>
140+
</Flex>
141+
) : (
142+
<ConfigureSSOFlowProvider enterpriseConnection={enterpriseConnection}>
143+
<ConfigureSSOWizardPanel />
144+
</ConfigureSSOFlowProvider>
145+
)}
146+
</Col>
99147
</NavbarContextProvider>
100148
</ProfileCard.Root>
101149
);
102150
});
103151

104-
const ConfigureSSOWizardPanel = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
152+
const ConfigureSSOWizardPanel = () => {
105153
const data = useConfigureSSOFlow();
106154

107155
return (
108-
<ProfileCard.Content
109-
contentRef={contentRef}
110-
disablePadding
156+
<Wizard.Root
157+
steps={CONFIGURE_SSO_STEPS}
158+
data={data}
111159
>
112-
<Wizard.Root
113-
steps={CONFIGURE_SSO_STEPS}
114-
data={data}
115-
>
116-
<Wizard.Header />
117-
<Wizard.Content />
118-
<Wizard.Footer />
119-
</Wizard.Root>
120-
</ProfileCard.Content>
160+
<Wizard.Header />
161+
<Wizard.Content />
162+
<Wizard.Footer />
163+
</Wizard.Root>
121164
);
122165
};
123166

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

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,54 @@
11
import { useUser } from '@clerk/shared/react/index';
2-
import React from 'react';
2+
import type { EnterpriseConnectionResource } from '@clerk/shared/types';
3+
import React, { type PropsWithChildren } from 'react';
34

45
/**
56
* Shared form state for the ConfigureSSO wizard. Lives outside the
67
* Wizard's own context so that:
78
* - it persists across step navigations (each step is its own
89
* `<Route>`, mounted/unmounted on navigation)
910
* - `shouldSkip` predicates on `WizardStep` can read it as plain data
10-
* via `<Wizard.Root data={ssoCtx} />`.
11+
* via `<Wizard.Root data={ssoCtx} />`
1112
*/
1213
export interface ConfigureSSOData {
13-
email: string;
1414
/**
15-
* Domain id returned by the API after the email is submitted.
16-
* Empty until the first step succeeds.
15+
* `true` if the user primary email address domain is already verified
1716
*/
18-
domainId: string;
17+
domainAlreadyVerified: boolean;
1918
/**
20-
* `true` if the domain returned by the API is already verified at the
21-
* time the user submits their email — the "Verify domain" step is
22-
* skipped in that case.
19+
* The enterprise connection from the user's primary email address domain
2320
*/
24-
domainAlreadyVerified: boolean;
21+
enterpriseConnection: EnterpriseConnectionResource | undefined;
2522
}
2623

27-
export interface ConfigureSSOContextValue extends ConfigureSSOData {
28-
setEmail: (email: string) => void;
29-
setDomainId: (id: string) => void;
24+
export type ConfigureSSOContextValue = ConfigureSSOData;
25+
26+
interface ConfigureSSOFlowProviderProps {
27+
/**
28+
* The user's enterprise connection, fetched by the parent so that
29+
* the wizard can show a loading state before mounting the panel.
30+
* `undefined` when the user has no enterprise connection
31+
*/
32+
enterpriseConnection: EnterpriseConnectionResource | undefined;
3033
}
3134

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

35-
export const ConfigureSSOFlowProvider = ({ children }: { children: React.ReactNode }): JSX.Element => {
36-
const [domainId, setDomainId] = React.useState('');
37-
38+
export const ConfigureSSOFlowProvider = ({
39+
enterpriseConnection,
40+
children,
41+
}: PropsWithChildren<ConfigureSSOFlowProviderProps>): JSX.Element => {
3842
const { user } = useUser();
3943

40-
// user is guaranteed to be defined because we're using the withCoreUserGuard HOC
41-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
42-
const [email, setEmail] = React.useState(user!.primaryEmailAddress?.emailAddress ?? '');
4344
const domainAlreadyVerified = user?.primaryEmailAddress?.verification.status === 'verified';
4445

4546
const value = React.useMemo<ConfigureSSOContextValue>(
4647
() => ({
47-
email,
48-
domainId,
48+
enterpriseConnection,
4949
domainAlreadyVerified,
50-
setEmail,
51-
setDomainId,
5250
}),
53-
[email, domainId, domainAlreadyVerified],
51+
[enterpriseConnection, domainAlreadyVerified],
5452
);
5553

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

packages/ui/src/components/ConfigureSSO/constants.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
ConfigureMapAttributes,
77
ConfirmationStep,
88
ProvideEmail,
9-
TestStep,
9+
TestConfigurationStep,
1010
VerifyDomain,
1111
} from './steps';
1212

@@ -15,10 +15,9 @@ export const CONFIGURE_SSO_STEPS: ReadonlyArray<WizardStep<ConfigureSSOData>> =
1515
id: 'verify-email-domain',
1616
path: 'verify-email-domain',
1717
label: 'Verify domain',
18-
isOptional: true,
1918
// Skip this step when there's a primary email address domain already verified
2019
shouldSkip: data => data.domainAlreadyVerified,
21-
steps: [
20+
innerSteps: [
2221
{
2322
id: 'provide-email',
2423
path: 'provide-email',
@@ -35,7 +34,7 @@ export const CONFIGURE_SSO_STEPS: ReadonlyArray<WizardStep<ConfigureSSOData>> =
3534
id: 'configure',
3635
path: 'configure',
3736
label: 'Configure',
38-
steps: [
37+
innerSteps: [
3938
{
4039
id: 'create-app',
4140
path: 'create-app',
@@ -52,7 +51,7 @@ export const CONFIGURE_SSO_STEPS: ReadonlyArray<WizardStep<ConfigureSSOData>> =
5251
id: 'test',
5352
path: 'test',
5453
label: 'Test',
55-
Component: TestStep,
54+
Component: TestConfigurationStep,
5655
},
5756
{
5857
id: 'confirmation',

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const ConfigureCreateApp = (): JSX.Element => {
1515
title='Configure Okta Workforce'
1616
subtitle='Create a new enterprise application in your Okta Dashboard.'
1717
>
18-
<Text as='p'>UI goes here</Text>
18+
<Text>UI goes here</Text>
1919
</StepLayout>
2020
);
2121
};

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const ConfigureMapAttributes = (): JSX.Element => {
1515
title='Map attributes'
1616
subtitle='Map identity provider attributes to Clerk user properties.'
1717
>
18-
<Text as='p'>UI goes here</Text>
18+
<Text>UI goes here</Text>
1919
</StepLayout>
2020
);
2121
};

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

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,7 @@ import { StepLayout } from './StepLayout';
55
export const ConfirmationStep = (): JSX.Element => {
66
return (
77
<StepLayout>
8-
<Text
9-
as='p'
10-
variant='body'
11-
sx={theme => ({ color: theme.colors.$colorMutedForeground })}
12-
>
13-
UI goes here
14-
</Text>
8+
<Text>UI goes here</Text>
159
</StepLayout>
1610
);
1711
};
Lines changed: 2 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,23 @@
1-
import { Col, Flex, Heading, Icon, Input, Text } from '@/customizables';
1+
import { Text } from '@/customizables';
22
import { useRegisterContinueAction, useWizard } from '@/elements/Wizard';
3-
import { Email } from '@/icons';
43

5-
import { useConfigureSSOFlow } from '../ConfigureSSOContext';
64
import { StepLayout } from './StepLayout';
75

8-
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
9-
10-
// TODO -> Conditionally render this step based on the user's email address
11-
// If the user already has an email address, skip this step
12-
// If the instance doesn't support email addresses, skip this step
13-
// If the user doesn't have an email address, render this step
146
export const ProvideEmail = (): JSX.Element => {
15-
const { email, setEmail } = useConfigureSSOFlow();
167
const { goNext } = useWizard();
178

18-
const isValid = EMAIL_RE.test(email.trim());
19-
209
useRegisterContinueAction({
2110
handler: () => {
22-
if (!isValid) {
23-
return;
24-
}
25-
26-
// TODO -> Call API to add email address to user
27-
2811
return goNext();
2912
},
30-
isDisabled: !isValid,
3113
});
3214

3315
return (
3416
<StepLayout
3517
title='Verify your domain'
3618
subtitle='Verify the domain you want to enable the enterprise connection on.'
3719
>
38-
<Flex
39-
direction='col'
40-
align='center'
41-
justify='center'
42-
sx={theme => ({
43-
flex: 1,
44-
gap: theme.space.$4,
45-
paddingBlock: theme.space.$8,
46-
})}
47-
>
48-
<Icon
49-
icon={Email}
50-
size='lg'
51-
sx={theme => ({ color: theme.colors.$colorMutedForeground })}
52-
/>
53-
<Col sx={theme => ({ gap: theme.space.$1, alignItems: 'center', textAlign: 'center' })}>
54-
<Heading textVariant='h3'>We need your email</Heading>
55-
<Text
56-
as='p'
57-
variant='body'
58-
sx={theme => ({ color: theme.colors.$colorMutedForeground })}
59-
>
60-
In order to start we will need your email address
61-
</Text>
62-
</Col>
63-
<Input
64-
type='email'
65-
placeholder='Paste URL here…'
66-
value={email}
67-
onChange={e => setEmail(e.currentTarget.value)}
68-
sx={theme => ({ maxWidth: theme.sizes.$60, width: '100%' })}
69-
/>
70-
</Flex>
20+
<Text as='p'>UI goes here</Text>
7121
</StepLayout>
7222
);
7323
};

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ interface StepLayoutProps {
1414
* underneath. Each individual step file owns the body content.
1515
*
1616
* The Step X/Y badge is rendered via `Wizard.StepIndicator`, which
17-
* self-hides on steps that have no inner sub-steps — so consumers
18-
* never have to opt in/out manually.
17+
* self-hides on steps that have no inner sub-steps
1918
*/
2019
export const StepLayout = ({ title, subtitle, children }: StepLayoutProps): JSX.Element => {
2120
return (
@@ -41,6 +40,7 @@ export const StepLayout = ({ title, subtitle, children }: StepLayoutProps): JSX.
4140
>
4241
{title}
4342
</Heading>
43+
4444
{subtitle ? (
4545
<Text
4646
as='p'
Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Col, Text } from '@/customizables';
1+
import { Text } from '@/customizables';
22

33
import { StepLayout } from './StepLayout';
44

@@ -8,22 +8,7 @@ export const TestConfigurationStep = (): JSX.Element => {
88
title='Test your SSO connection'
99
subtitle='Test your SSO configuration to verify you can successfully authenticate via your identity provider'
1010
>
11-
<Col
12-
sx={theme => ({
13-
gap: theme.space.$4,
14-
maxWidth: theme.sizes.$160,
15-
marginInline: 'auto',
16-
paddingBlock: theme.space.$8,
17-
})}
18-
>
19-
<Text
20-
as='p'
21-
variant='body'
22-
sx={theme => ({ color: theme.colors.$colorMutedForeground })}
23-
>
24-
UI goes here
25-
</Text>
26-
</Col>
11+
<Text>UI goes here</Text>
2712
</StepLayout>
2813
);
2914
};

0 commit comments

Comments
 (0)