Skip to content

Commit dadb356

Browse files
committed
Update initial step initialization
1 parent f4690d1 commit dadb356

5 files changed

Lines changed: 142 additions & 34 deletions

File tree

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1-
import { __internal_useUserEnterpriseConnections, useSession } from '@clerk/shared/react';
2-
import type { __experimental_ConfigureSSOProps } from '@clerk/shared/types';
1+
import {
2+
__internal_useEnterpriseConnectionTestRuns,
3+
__internal_useUserEnterpriseConnections,
4+
useSession,
5+
} from '@clerk/shared/react';
6+
import type { __experimental_ConfigureSSOProps, EnterpriseConnectionResource } from '@clerk/shared/types';
37
import React from 'react';
48

59
import { useProtect } from '@/common';
@@ -64,19 +68,31 @@ const AuthenticatedContent = withCoreUserGuard(() => {
6468
});
6569

6670
const ConfigureSSOCardContent = ({ contentRef }: { contentRef: React.RefObject<HTMLDivElement> }) => {
67-
const { data: enterpriseConnections, isLoading } = __internal_useUserEnterpriseConnections({ enabled: true });
68-
71+
const {
72+
data: enterpriseConnections,
73+
isLoading: isLoadingEnterpriseConnections,
74+
createEnterpriseConnection,
75+
updateEnterpriseConnection,
76+
deleteEnterpriseConnection,
77+
} = __internal_useUserEnterpriseConnections({ enabled: true });
6978
// Currently FAPI only supports one enterprise connection per user
7079
const enterpriseConnection = enterpriseConnections?.[0];
7180

81+
const { hasSuccessfulTestRun, isLoading: isLoadingTestRuns } = useHasSuccessfulTestRun(enterpriseConnection);
82+
83+
const isLoading = isLoadingEnterpriseConnections || isLoadingTestRuns;
7284
if (isLoading && !enterpriseConnection) {
7385
return <ConfigureSSOSkeleton />;
7486
}
7587

7688
return (
7789
<ConfigureSSOProvider
90+
hasSuccessfulTestRun={hasSuccessfulTestRun}
7891
enterpriseConnection={enterpriseConnection}
7992
contentRef={contentRef}
93+
createEnterpriseConnection={createEnterpriseConnection}
94+
updateEnterpriseConnection={updateEnterpriseConnection}
95+
deleteEnterpriseConnection={deleteEnterpriseConnection}
8096
>
8197
<ConfigureSSOSteps />
8298
</ConfigureSSOProvider>
@@ -183,5 +199,22 @@ const MissingManageEnterpriseConnectionsPermission = () => (
183199
</>
184200
);
185201

202+
/**
203+
* Fetches a single successful test run for the given connection on mount
204+
*/
205+
const useHasSuccessfulTestRun = (
206+
enterpriseConnection: EnterpriseConnectionResource | undefined,
207+
): { hasSuccessfulTestRun: boolean; isLoading: boolean } => {
208+
const { data: successfulTestRuns, isLoading } = __internal_useEnterpriseConnectionTestRuns({
209+
enterpriseConnectionId: enterpriseConnection?.id ?? null,
210+
params: { initialPage: 1, pageSize: 1, status: ['success'] },
211+
});
212+
213+
return {
214+
hasSuccessfulTestRun: (successfulTestRuns?.length ?? 0) > 0,
215+
isLoading,
216+
};
217+
};
218+
186219
export const ConfigureSSO: React.ComponentType<__experimental_ConfigureSSOProps> =
187220
withCardStateProvider(ConfigureSSOInternal);

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

Lines changed: 63 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { __internal_useUserEnterpriseConnections, useSession, useUser } from '@clerk/shared/react/index';
2-
import type { EnterpriseConnectionResource } from '@clerk/shared/types';
1+
import type { UseUserEnterpriseConnectionsReturn } from '@clerk/shared/react/index';
2+
import { useSession, useUser } from '@clerk/shared/react/index';
3+
import type { EnterpriseConnectionResource, SignedInSessionResource, UserResource } from '@clerk/shared/types';
34
import React, { type PropsWithChildren, useCallback } from 'react';
45

56
import { useCardState } from '@/elements/contexts';
@@ -32,32 +33,54 @@ export interface ConfigureSSOData {
3233
*/
3334
contentRef: React.RefObject<HTMLDivElement>;
3435
/**
35-
* Creates a new enterprise connection.
36+
* Creates a new enterprise connection
3637
*/
3738
createEnterpriseConnection: (provider: ProviderType) => Promise<void>;
39+
/**
40+
* Updates an existing enterprise connection
41+
*/
42+
updateEnterpriseConnection: UseUserEnterpriseConnectionsReturn['updateEnterpriseConnection'];
43+
/**
44+
* Deletes an enterprise connection
45+
*/
46+
deleteEnterpriseConnection: UseUserEnterpriseConnectionsReturn['deleteEnterpriseConnection'];
47+
/**
48+
* Determines if the user's domain is already wired to an enterprise connection that
49+
* doesn't belong to the org they're currently configuring
50+
*/
51+
isDomainTakenByOtherOrg: boolean;
3852
}
3953

4054
interface ConfigureSSOProviderProps {
4155
enterpriseConnection: EnterpriseConnectionResource | undefined;
56+
hasSuccessfulTestRun: boolean;
4257
contentRef: React.RefObject<HTMLDivElement>;
58+
createEnterpriseConnection: UseUserEnterpriseConnectionsReturn['createEnterpriseConnection'];
59+
updateEnterpriseConnection: UseUserEnterpriseConnectionsReturn['updateEnterpriseConnection'];
60+
deleteEnterpriseConnection: UseUserEnterpriseConnectionsReturn['deleteEnterpriseConnection'];
4361
}
4462

4563
const ConfigureSSOContext = React.createContext<ConfigureSSOData | null>(null);
4664
ConfigureSSOContext.displayName = 'ConfigureSSOContext';
4765

4866
export const ConfigureSSOProvider = ({
4967
enterpriseConnection,
68+
hasSuccessfulTestRun,
5069
contentRef,
70+
createEnterpriseConnection: createEnterpriseConnectionApi,
71+
updateEnterpriseConnection,
72+
deleteEnterpriseConnection,
5173
children,
5274
}: PropsWithChildren<ConfigureSSOProviderProps>): JSX.Element => {
5375
const [provider, setProvider] = React.useState<ProviderType | undefined>(
5476
enterpriseConnection?.provider as ProviderType,
5577
);
56-
const enterpriseConnectionApi = __internal_useUserEnterpriseConnections();
5778
const { user } = useUser();
5879
const { session } = useSession();
5980
const card = useCardState();
60-
const initialStepId = deriveInitialStep(enterpriseConnection);
81+
82+
const isDomainTakenByOtherOrg = checkDomainTakenByOtherOrg(user, session, enterpriseConnection);
83+
const initialStepId = deriveInitialStep(enterpriseConnection, { isDomainTakenByOtherOrg, hasSuccessfulTestRun });
6184

6285
const createEnterpriseConnection = useCallback(
6386
async (provider: ProviderType): Promise<void> => {
@@ -71,7 +94,7 @@ export const ConfigureSSOProvider = ({
7194
card.setLoading();
7295

7396
try {
74-
await enterpriseConnectionApi.createEnterpriseConnection({
97+
await createEnterpriseConnectionApi({
7598
provider,
7699
name: emailDomain,
77100
organizationId,
@@ -80,19 +103,31 @@ export const ConfigureSSOProvider = ({
80103
card.setIdle();
81104
}
82105
},
83-
[user, card, session, enterpriseConnectionApi],
106+
[user, card, session, createEnterpriseConnectionApi],
84107
);
85108

86109
const value = React.useMemo<ConfigureSSOData>(
87110
() => ({
111+
provider,
112+
contentRef,
113+
setProvider,
88114
initialStepId,
89115
enterpriseConnection,
90-
provider,
116+
isDomainTakenByOtherOrg,
91117
createEnterpriseConnection,
92-
setProvider,
93-
contentRef,
118+
updateEnterpriseConnection,
119+
deleteEnterpriseConnection,
94120
}),
95-
[initialStepId, enterpriseConnection, createEnterpriseConnection, provider, contentRef],
121+
[
122+
provider,
123+
contentRef,
124+
initialStepId,
125+
enterpriseConnection,
126+
createEnterpriseConnection,
127+
updateEnterpriseConnection,
128+
deleteEnterpriseConnection,
129+
isDomainTakenByOtherOrg,
130+
],
96131
);
97132

98133
return <ConfigureSSOContext.Provider value={value}>{children}</ConfigureSSOContext.Provider>;
@@ -105,3 +140,20 @@ export const useConfigureSSO = (): ConfigureSSOData => {
105140
}
106141
return ctx;
107142
};
143+
144+
/**
145+
* Determines if the user's domain is already wired to an enterprise connection that
146+
* doesn't belong to the org they're currently configuring
147+
*/
148+
const checkDomainTakenByOtherOrg = (
149+
user: UserResource | null | undefined,
150+
session: SignedInSessionResource | null | undefined,
151+
enterpriseConnection: EnterpriseConnectionResource | undefined,
152+
): boolean => {
153+
const emailToVerify =
154+
user?.primaryEmailAddress ?? user?.emailAddresses?.find(e => e.verification.status !== 'verified');
155+
const isVerified = emailToVerify?.verification.status === 'verified';
156+
const activeOrganizationId = session?.lastActiveOrganizationId ?? null;
157+
158+
return Boolean(isVerified && enterpriseConnection && enterpriseConnection.organizationId !== activeOrganizationId);
159+
};

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

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,47 @@ import type { WizardStepId } from './types';
55
/**
66
* Decides where the ConfigureSSO wizard should mount on (re)load based on
77
* the current state of the user's enterprise connection.
8-
*
9-
* No connection → `select-provider`
10-
* Connection without SAML IdP metadata → `configure`
11-
* Connection with SAML IdP metadata → `confirmation`
12-
*
13-
* The `test` step is intentionally absent — we can't derive a "last test
14-
* passed" signal synchronously from the resource. Users can re-test from
15-
* Confirmation.
168
*/
17-
export const deriveInitialStep = (connection: EnterpriseConnectionResource | undefined): WizardStepId => {
18-
if (!connection) {
9+
export const deriveInitialStep = (
10+
enterpriseConnection: EnterpriseConnectionResource | undefined,
11+
options: { isDomainTakenByOtherOrg: boolean; hasSuccessfulTestRun: boolean },
12+
): WizardStepId => {
13+
const { isDomainTakenByOtherOrg, hasSuccessfulTestRun } = options;
14+
15+
// Go to the verify domain step in order to display warning
16+
if (isDomainTakenByOtherOrg) {
17+
return 'verify-domain';
18+
}
19+
20+
// If no initial connection, go to the select provider step
21+
if (!enterpriseConnection) {
1922
return 'select-provider';
2023
}
21-
if (!connection.samlConnection?.idpSsoUrl) {
24+
25+
// Connection is enabled, go to the confirmation step
26+
const isEnabled = enterpriseConnection?.active;
27+
if (isEnabled) {
28+
return 'confirmation';
29+
}
30+
31+
const hasMinimumIdPConfiguration = checkHasMinimumIdPConfiguration(enterpriseConnection);
32+
33+
// If the connection hasn't finished configuring it, go to the configure step
34+
// Connection exists, but is not enabled and hasn't finished configuring it
35+
if (!hasMinimumIdPConfiguration) {
2236
return 'configure';
2337
}
38+
39+
// If the connection hasn't been tested, go to the test step
40+
if (!hasSuccessfulTestRun) {
41+
return 'test';
42+
}
43+
44+
// Connection is disabled but has been tested and configured
2445
return 'confirmation';
2546
};
47+
48+
// TODO - Update to support OpenID Connect
49+
const checkHasMinimumIdPConfiguration = (connection: EnterpriseConnectionResource): boolean => {
50+
return Boolean(connection.samlConnection?.idpSsoUrl && connection.samlConnection?.idpEntityId);
51+
};

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { __internal_useUserEnterpriseConnections, useReverification } from '@clerk/shared/react';
1+
import { useReverification } from '@clerk/shared/react';
22
import type { FieldId, UpdateMeEnterpriseConnectionParams } from '@clerk/shared/types';
33
import React, { type JSX } from 'react';
44

@@ -599,8 +599,7 @@ export const SubmitSamlConfigSubStep = (): JSX.Element => {
599599
const card = useCardState();
600600
const { t } = useLocalizations();
601601
const { goNext, goPrev, isFirstStep } = useWizard();
602-
const { enterpriseConnection } = useConfigureSSO();
603-
const { updateEnterpriseConnection } = __internal_useUserEnterpriseConnections();
602+
const { enterpriseConnection, updateEnterpriseConnection } = useConfigureSSO();
604603
const { key } = useConfigureStepTranslations();
605604

606605
const samlConnection = enterpriseConnection?.samlConnection;

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { __internal_useUserEnterpriseConnections, useReverification } from '@clerk/shared/react';
1+
import { useReverification } from '@clerk/shared/react';
22
import { useState } from 'react';
33

44
import { Badge, Col, descriptors, Flex, Flow, Grid, Link, localizationKeys, Text } from '@/customizables';
@@ -65,8 +65,7 @@ const SsoStatusSection = (): JSX.Element => {
6565
};
6666

6767
const EnableSsoSection = (): JSX.Element => {
68-
const { enterpriseConnection } = useConfigureSSO();
69-
const { updateEnterpriseConnection } = __internal_useUserEnterpriseConnections({ enabled: false });
68+
const { enterpriseConnection, updateEnterpriseConnection } = useConfigureSSO();
7069
const card = useCardState();
7170

7271
const [isChecked, setIsChecked] = useState(!!enterpriseConnection?.active);
@@ -220,8 +219,7 @@ const ConfigurationDetailsSection = (): JSX.Element => {
220219
const ResetConnectionForm = withCardStateProvider((props: FormProps) => {
221220
const { onReset, onSuccess } = props;
222221
const card = useCardState();
223-
const { enterpriseConnection } = useConfigureSSO();
224-
const { deleteEnterpriseConnection } = __internal_useUserEnterpriseConnections({ enabled: false });
222+
const { enterpriseConnection, deleteEnterpriseConnection } = useConfigureSSO();
225223
const { goToStep } = useWizard();
226224

227225
const deleteConnection = useReverification((id: string) => deleteEnterpriseConnection(id));

0 commit comments

Comments
 (0)