Skip to content

Commit 4d01dc8

Browse files
committed
Update initial step initialization
1 parent f4690d1 commit 4d01dc8

3 files changed

Lines changed: 99 additions & 21 deletions

File tree

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

Lines changed: 29 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,17 +68,21 @@ 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 { data: enterpriseConnections, isLoading: isLoadingEnterpriseConnections } =
72+
__internal_useUserEnterpriseConnections({ enabled: true });
6973
// Currently FAPI only supports one enterprise connection per user
7074
const enterpriseConnection = enterpriseConnections?.[0];
7175

76+
const { hasSuccessfulTestRun, isLoading: isLoadingTestRuns } = useHasSuccessfulTestRun(enterpriseConnection);
77+
78+
const isLoading = isLoadingEnterpriseConnections || isLoadingTestRuns;
7279
if (isLoading && !enterpriseConnection) {
7380
return <ConfigureSSOSkeleton />;
7481
}
7582

7683
return (
7784
<ConfigureSSOProvider
85+
hasSuccessfulTestRun={hasSuccessfulTestRun}
7886
enterpriseConnection={enterpriseConnection}
7987
contentRef={contentRef}
8088
>
@@ -183,5 +191,22 @@ const MissingManageEnterpriseConnectionsPermission = () => (
183191
</>
184192
);
185193

194+
/**
195+
* Fetches a single successful test run for the given connection on mount
196+
*/
197+
const useHasSuccessfulTestRun = (
198+
enterpriseConnection: EnterpriseConnectionResource | undefined,
199+
): { hasSuccessfulTestRun: boolean; isLoading: boolean } => {
200+
const { data: successfulTestRuns, isLoading } = __internal_useEnterpriseConnectionTestRuns({
201+
enterpriseConnectionId: enterpriseConnection?.id ?? null,
202+
params: { initialPage: 1, pageSize: 1, status: ['success'] },
203+
});
204+
205+
return {
206+
hasSuccessfulTestRun: (successfulTestRuns?.length ?? 0) > 0,
207+
isLoading,
208+
};
209+
};
210+
186211
export const ConfigureSSO: React.ComponentType<__experimental_ConfigureSSOProps> =
187212
withCardStateProvider(ConfigureSSOInternal);

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

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

55
import { useCardState } from '@/elements/contexts';
@@ -35,10 +35,16 @@ export interface ConfigureSSOData {
3535
* Creates a new enterprise connection.
3636
*/
3737
createEnterpriseConnection: (provider: ProviderType) => Promise<void>;
38+
/**
39+
* Determines if the user's domain is already wired to an enterprise connection that
40+
* doesn't belong to the org they're currently configuring
41+
*/
42+
isDomainTakenByOtherOrg: boolean;
3843
}
3944

4045
interface ConfigureSSOProviderProps {
4146
enterpriseConnection: EnterpriseConnectionResource | undefined;
47+
hasSuccessfulTestRun: boolean;
4248
contentRef: React.RefObject<HTMLDivElement>;
4349
}
4450

@@ -47,6 +53,7 @@ ConfigureSSOContext.displayName = 'ConfigureSSOContext';
4753

4854
export const ConfigureSSOProvider = ({
4955
enterpriseConnection,
56+
hasSuccessfulTestRun,
5057
contentRef,
5158
children,
5259
}: PropsWithChildren<ConfigureSSOProviderProps>): JSX.Element => {
@@ -57,7 +64,9 @@ export const ConfigureSSOProvider = ({
5764
const { user } = useUser();
5865
const { session } = useSession();
5966
const card = useCardState();
60-
const initialStepId = deriveInitialStep(enterpriseConnection);
67+
68+
const isDomainTakenByOtherOrg = checkDomainTakenByOtherOrg(user, session, enterpriseConnection);
69+
const initialStepId = deriveInitialStep(enterpriseConnection, { isDomainTakenByOtherOrg, hasSuccessfulTestRun });
6170

6271
const createEnterpriseConnection = useCallback(
6372
async (provider: ProviderType): Promise<void> => {
@@ -85,14 +94,15 @@ export const ConfigureSSOProvider = ({
8594

8695
const value = React.useMemo<ConfigureSSOData>(
8796
() => ({
97+
provider,
98+
contentRef,
99+
setProvider,
88100
initialStepId,
89101
enterpriseConnection,
90-
provider,
102+
isDomainTakenByOtherOrg,
91103
createEnterpriseConnection,
92-
setProvider,
93-
contentRef,
94104
}),
95-
[initialStepId, enterpriseConnection, createEnterpriseConnection, provider, contentRef],
105+
[provider, contentRef, initialStepId, enterpriseConnection, createEnterpriseConnection, isDomainTakenByOtherOrg],
96106
);
97107

98108
return <ConfigureSSOContext.Provider value={value}>{children}</ConfigureSSOContext.Provider>;
@@ -105,3 +115,20 @@ export const useConfigureSSO = (): ConfigureSSOData => {
105115
}
106116
return ctx;
107117
};
118+
119+
/**
120+
* Determines if the user's domain is already wired to an enterprise connection that
121+
* doesn't belong to the org they're currently configuring
122+
*/
123+
const checkDomainTakenByOtherOrg = (
124+
user: UserResource | null | undefined,
125+
session: SignedInSessionResource | null | undefined,
126+
enterpriseConnection: EnterpriseConnectionResource | undefined,
127+
): boolean => {
128+
const emailToVerify =
129+
user?.primaryEmailAddress ?? user?.emailAddresses?.find(e => e.verification.status !== 'verified');
130+
const isVerified = emailToVerify?.verification.status === 'verified';
131+
const activeOrganizationId = session?.lastActiveOrganizationId ?? null;
132+
133+
return Boolean(isVerified && enterpriseConnection && enterpriseConnection.organizationId !== activeOrganizationId);
134+
};

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+
};

0 commit comments

Comments
 (0)