Skip to content

Commit 4e08a7f

Browse files
committed
feat(ui,shared): switch ConfigureSSO to org-scoped enterprise_connections
- Add __internal_useOrganizationEnterpriseConnections + test-runs hook in @clerk/shared/react - Swap all user.*EnterpriseConnection* call sites in <ConfigureSSO /> to organization.* - Preserve useReverification wrapping - Update tests + fixtures to reflect the new call surface ORGS-1597 — backend org-scoped paths (clerk_go#19109) ship the canonical surface; this routes the SDK consumer to them.
1 parent d9c0d20 commit 4e08a7f

12 files changed

Lines changed: 366 additions & 27 deletions
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
'@clerk/ui': minor
3+
'@clerk/shared': minor
4+
---
5+
6+
`<ConfigureSSO />` now calls the org-scoped enterprise connections endpoints via `organization.*EnterpriseConnection*` methods. Previously, the wizard called `user.*EnterpriseConnection*` against the `/me/*` paths.
7+
8+
Adds two new internal hooks in `@clerk/shared/react`, mirroring the user-scoped variants:
9+
- `__internal_useOrganizationEnterpriseConnections`
10+
- `__internal_useOrganizationEnterpriseConnectionTestRuns`
11+
12+
The existing `__internal_useUserEnterpriseConnections` and `__internal_useEnterpriseConnectionTestRuns` hooks remain available as `@deprecated` aliases.

packages/shared/src/react/hooks/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ export type {
4343
UseEnterpriseConnectionTestRunsParams,
4444
UseEnterpriseConnectionTestRunsReturn,
4545
} from './useEnterpriseConnectionTestRuns';
46+
export { __internal_useOrganizationEnterpriseConnections } from './useOrganizationEnterpriseConnections';
47+
export type {
48+
UseOrganizationEnterpriseConnectionsParams,
49+
UseOrganizationEnterpriseConnectionsReturn,
50+
} from './useOrganizationEnterpriseConnections';
51+
export { __internal_useOrganizationEnterpriseConnectionTestRuns } from './useOrganizationEnterpriseConnectionTestRuns';
52+
export type {
53+
UseOrganizationEnterpriseConnectionTestRunsParams,
54+
UseOrganizationEnterpriseConnectionTestRunsReturn,
55+
} from './useOrganizationEnterpriseConnectionTestRuns';
4656

4757
export { useUserBase as __internal_useUserBase } from './base/useUserBase';
4858
export { useClientBase as __internal_useClientBase } from './base/useClientBase';
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useMemo } from 'react';
2+
3+
import type { GetEnterpriseConnectionTestRunsParams } from '../../types/enterpriseConnectionTestRun';
4+
import { INTERNAL_STABLE_KEYS } from '../stable-keys';
5+
import { createCacheKeys } from './createCacheKeys';
6+
7+
/**
8+
* @internal
9+
*/
10+
export function useOrganizationEnterpriseConnectionTestRunsCacheKeys(params: {
11+
organizationId: string | null;
12+
enterpriseConnectionId: string | null;
13+
args: GetEnterpriseConnectionTestRunsParams;
14+
}) {
15+
const { organizationId, enterpriseConnectionId, args } = params;
16+
return useMemo(() => {
17+
return createCacheKeys({
18+
stablePrefix: INTERNAL_STABLE_KEYS.ORGANIZATION_ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
19+
authenticated: Boolean(organizationId),
20+
tracked: {
21+
organizationId: organizationId ?? null,
22+
enterpriseConnectionId: enterpriseConnectionId ?? null,
23+
},
24+
untracked: {
25+
args,
26+
},
27+
});
28+
// The args object is intentionally serialized via the consumer to keep stability.
29+
// eslint-disable-next-line react-hooks/exhaustive-deps
30+
}, [organizationId, enterpriseConnectionId, JSON.stringify(args)]);
31+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import { useCallback, useEffect, useState } from 'react';
2+
3+
import type {
4+
EnterpriseConnectionTestRunResource,
5+
GetEnterpriseConnectionTestRunsParams,
6+
} from '../../types/enterpriseConnectionTestRun';
7+
import { useClerkInstanceContext } from '../contexts';
8+
import { useClerkQueryClient } from '../query/use-clerk-query-client';
9+
import { useClerkQuery } from '../query/useQuery';
10+
import { useOrganizationBase } from './base/useOrganizationBase';
11+
import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut';
12+
import { useOrganizationEnterpriseConnectionTestRunsCacheKeys } from './useOrganizationEnterpriseConnectionTestRuns.shared';
13+
14+
const DEFAULT_POLL_INTERVAL_MS = 2_000;
15+
16+
export type UseOrganizationEnterpriseConnectionTestRunsParams = {
17+
enterpriseConnectionId: string | null;
18+
/**
19+
* Pass-through fetch parameters (pagination, status filter).
20+
* Defaults to `{ initialPage: 1, pageSize: 10 }`.
21+
*/
22+
params?: GetEnterpriseConnectionTestRunsParams;
23+
/**
24+
* Polling interval (ms) applied between `revalidate()` and the moment the
25+
* first record arrives in the response.
26+
*
27+
* @default 2000
28+
*/
29+
pollIntervalMs?: number;
30+
/**
31+
* If `false`, the hook is dormant — no fetch, no polling.
32+
*
33+
* @default true
34+
*/
35+
enabled?: boolean;
36+
};
37+
38+
export type UseOrganizationEnterpriseConnectionTestRunsReturn = {
39+
data: EnterpriseConnectionTestRunResource[] | undefined;
40+
totalCount: number | undefined;
41+
error: Error | null;
42+
isLoading: boolean;
43+
isFetching: boolean;
44+
/**
45+
* `true` while the hook is actively polling for the first record to appear
46+
*/
47+
isPolling: boolean;
48+
/**
49+
* Force a refetch and (if the list is currently empty) arm polling
50+
*/
51+
revalidate: () => Promise<void>;
52+
};
53+
54+
/**
55+
* Subscribes to the list of enterprise-connection test runs for the active organization
56+
*
57+
* @internal
58+
*/
59+
function useOrganizationEnterpriseConnectionTestRuns(
60+
params: UseOrganizationEnterpriseConnectionTestRunsParams,
61+
): UseOrganizationEnterpriseConnectionTestRunsReturn {
62+
const {
63+
enterpriseConnectionId,
64+
params: fetchParams = { initialPage: 1, pageSize: 10 },
65+
pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
66+
enabled = true,
67+
} = params;
68+
69+
const clerk = useClerkInstanceContext();
70+
const organization = useOrganizationBase();
71+
const [queryClient] = useClerkQueryClient();
72+
73+
const { queryKey, invalidationKey, stableKey, authenticated } = useOrganizationEnterpriseConnectionTestRunsCacheKeys({
74+
organizationId: organization?.id ?? null,
75+
enterpriseConnectionId,
76+
args: fetchParams,
77+
});
78+
79+
useClearQueriesOnSignOut({
80+
isSignedOut: organization === null,
81+
authenticated,
82+
stableKeys: stableKey,
83+
});
84+
85+
const queryEnabled = enabled && clerk.loaded && Boolean(organization) && Boolean(enterpriseConnectionId);
86+
87+
const [shouldPoll, setShouldPoll] = useState(false);
88+
89+
const query = useClerkQuery({
90+
queryKey,
91+
queryFn: () => {
92+
if (!enterpriseConnectionId) {
93+
throw new Error('enterpriseConnectionId is required to fetch test runs');
94+
}
95+
return organization?.getEnterpriseConnectionTestRuns(enterpriseConnectionId, fetchParams);
96+
},
97+
refetchInterval: q => {
98+
if (!shouldPoll) {
99+
return false;
100+
}
101+
102+
const hasRows = (q.state.data?.data?.length ?? 0) > 0;
103+
return hasRows ? false : pollIntervalMs;
104+
},
105+
enabled: queryEnabled,
106+
refetchIntervalInBackground: false,
107+
});
108+
109+
const hasRows = (query.data?.data?.length ?? 0) > 0;
110+
111+
useEffect(() => {
112+
if (shouldPoll && hasRows) {
113+
setShouldPoll(false);
114+
}
115+
}, [shouldPoll, hasRows]);
116+
117+
const revalidate = useCallback(async () => {
118+
// Only arm polling when there is nothing in the list yet — once any record
119+
// has been seen, this is a one-shot refetch.
120+
if (!hasRows) {
121+
setShouldPoll(true);
122+
}
123+
await queryClient.invalidateQueries({ queryKey: invalidationKey });
124+
}, [queryClient, invalidationKey, hasRows]);
125+
126+
const isPolling = queryEnabled && shouldPoll && !hasRows;
127+
128+
return {
129+
data: query.data?.data,
130+
totalCount: query.data?.total_count,
131+
error: query.error ?? null,
132+
isLoading: query.isLoading,
133+
isFetching: query.isFetching,
134+
isPolling,
135+
revalidate,
136+
};
137+
}
138+
139+
export { useOrganizationEnterpriseConnectionTestRuns as __internal_useOrganizationEnterpriseConnectionTestRuns };
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { useMemo } from 'react';
2+
3+
import { INTERNAL_STABLE_KEYS } from '../stable-keys';
4+
import { createCacheKeys } from './createCacheKeys';
5+
6+
/**
7+
* @internal
8+
*/
9+
export function useOrganizationEnterpriseConnectionsCacheKeys(params: {
10+
organizationId: string | null;
11+
withOrganizationAccountLinking?: boolean;
12+
}) {
13+
const { organizationId, withOrganizationAccountLinking = false } = params;
14+
return useMemo(() => {
15+
return createCacheKeys({
16+
stablePrefix: INTERNAL_STABLE_KEYS.ORGANIZATION_ENTERPRISE_CONNECTIONS_KEY,
17+
authenticated: Boolean(organizationId),
18+
tracked: {
19+
organizationId: organizationId ?? null,
20+
withOrganizationAccountLinking,
21+
},
22+
untracked: {
23+
args: {},
24+
},
25+
});
26+
}, [organizationId, withOrganizationAccountLinking]);
27+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { useCallback } from 'react';
2+
3+
import type { DeletedObjectResource } from '../../types/deletedObject';
4+
import type {
5+
CreateOrganizationEnterpriseConnectionParams,
6+
EnterpriseConnectionResource,
7+
UpdateOrganizationEnterpriseConnectionParams,
8+
} from '../../types/enterpriseConnection';
9+
import { useClerkInstanceContext } from '../contexts';
10+
import { defineKeepPreviousDataFn } from '../query/keep-previous-data';
11+
import { useClerkQueryClient } from '../query/use-clerk-query-client';
12+
import { useClerkQuery } from '../query/useQuery';
13+
import { useOrganizationBase } from './base/useOrganizationBase';
14+
import { useClearQueriesOnSignOut } from './useClearQueriesOnSignOut';
15+
import { useOrganizationEnterpriseConnectionsCacheKeys } from './useOrganizationEnterpriseConnections.shared';
16+
17+
export type UseOrganizationEnterpriseConnectionsParams = {
18+
enabled?: boolean;
19+
keepPreviousData?: boolean;
20+
withOrganizationAccountLinking?: boolean;
21+
};
22+
23+
export type UseOrganizationEnterpriseConnectionsReturn = {
24+
data: EnterpriseConnectionResource[] | undefined;
25+
error: Error | null;
26+
isLoading: boolean;
27+
isFetching: boolean;
28+
createEnterpriseConnection: (
29+
params: CreateOrganizationEnterpriseConnectionParams,
30+
) => Promise<EnterpriseConnectionResource | undefined>;
31+
updateEnterpriseConnection: (
32+
enterpriseConnectionId: string,
33+
params: UpdateOrganizationEnterpriseConnectionParams,
34+
) => Promise<EnterpriseConnectionResource | undefined>;
35+
deleteEnterpriseConnection: (enterpriseConnectionId: string) => Promise<DeletedObjectResource | undefined>;
36+
revalidate: () => Promise<void>;
37+
};
38+
39+
/**
40+
* Enterprise connections for the active organization
41+
*
42+
* @internal
43+
*/
44+
function useOrganizationEnterpriseConnections(
45+
params: UseOrganizationEnterpriseConnectionsParams = {},
46+
): UseOrganizationEnterpriseConnectionsReturn {
47+
const { keepPreviousData = true, enabled = true, withOrganizationAccountLinking = false } = params;
48+
const clerk = useClerkInstanceContext();
49+
const organization = useOrganizationBase();
50+
const [queryClient] = useClerkQueryClient();
51+
52+
const { queryKey, stableKey, authenticated } = useOrganizationEnterpriseConnectionsCacheKeys({
53+
organizationId: organization?.id ?? null,
54+
withOrganizationAccountLinking,
55+
});
56+
57+
const queryEnabled = enabled && clerk.loaded && Boolean(organization);
58+
59+
useClearQueriesOnSignOut({
60+
isSignedOut: organization === null,
61+
authenticated,
62+
stableKeys: stableKey,
63+
});
64+
65+
const query = useClerkQuery({
66+
queryKey,
67+
queryFn: () => organization?.getEnterpriseConnections({ withOrganizationAccountLinking }),
68+
enabled: queryEnabled,
69+
placeholderData: defineKeepPreviousDataFn(keepPreviousData),
70+
});
71+
72+
const revalidate = useCallback(
73+
() => queryClient.invalidateQueries({ queryKey: [stableKey] }),
74+
[queryClient, stableKey],
75+
);
76+
77+
const createEnterpriseConnection = useCallback(
78+
async (createParams: CreateOrganizationEnterpriseConnectionParams) => {
79+
const created = await organization?.createEnterpriseConnection(createParams);
80+
await revalidate();
81+
return created;
82+
},
83+
[organization, revalidate],
84+
);
85+
86+
const updateEnterpriseConnection = useCallback(
87+
async (enterpriseConnectionId: string, updateParams: UpdateOrganizationEnterpriseConnectionParams) => {
88+
const updated = await organization?.updateEnterpriseConnection(enterpriseConnectionId, updateParams);
89+
await revalidate();
90+
return updated;
91+
},
92+
[organization, revalidate],
93+
);
94+
95+
const deleteEnterpriseConnection = useCallback(
96+
async (enterpriseConnectionId: string) => {
97+
const deleted = await organization?.deleteEnterpriseConnection(enterpriseConnectionId);
98+
await revalidate();
99+
return deleted;
100+
},
101+
[organization, revalidate],
102+
);
103+
104+
return {
105+
data: query.data,
106+
error: query.error ?? null,
107+
isLoading: query.isLoading,
108+
isFetching: query.isFetching,
109+
createEnterpriseConnection,
110+
updateEnterpriseConnection,
111+
deleteEnterpriseConnection,
112+
revalidate,
113+
};
114+
}
115+
116+
export { useOrganizationEnterpriseConnections as __internal_useOrganizationEnterpriseConnections };

packages/shared/src/react/stable-keys.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,13 +74,17 @@ const BILLING_PLANS_KEY = 'billing-plan';
7474
const BILLING_STATEMENTS_KEY = 'billing-statement';
7575
const USER_ENTERPRISE_CONNECTIONS_KEY = 'userEnterpriseConnections';
7676
const ENTERPRISE_CONNECTION_TEST_RUNS_KEY = 'enterpriseConnectionTestRuns';
77+
const ORGANIZATION_ENTERPRISE_CONNECTIONS_KEY = 'organizationEnterpriseConnections';
78+
const ORGANIZATION_ENTERPRISE_CONNECTION_TEST_RUNS_KEY = 'organizationEnterpriseConnectionTestRuns';
7779

7880
export const INTERNAL_STABLE_KEYS = {
7981
PAYMENT_ATTEMPT_KEY,
8082
BILLING_PLANS_KEY,
8183
BILLING_STATEMENTS_KEY,
8284
USER_ENTERPRISE_CONNECTIONS_KEY,
8385
ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
86+
ORGANIZATION_ENTERPRISE_CONNECTIONS_KEY,
87+
ORGANIZATION_ENTERPRISE_CONNECTION_TEST_RUNS_KEY,
8488
} as const;
8589

8690
export type __internal_ResourceCacheStableKey = (typeof INTERNAL_STABLE_KEYS)[keyof typeof INTERNAL_STABLE_KEYS];

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
2-
__internal_useEnterpriseConnectionTestRuns,
3-
__internal_useUserEnterpriseConnections,
2+
__internal_useOrganizationEnterpriseConnections,
3+
__internal_useOrganizationEnterpriseConnectionTestRuns,
44
useSession,
55
} from '@clerk/shared/react';
66
import type { ConfigureSSOProps, EnterpriseConnectionResource } from '@clerk/shared/types';
@@ -72,7 +72,7 @@ export const ConfigureSSOContent = ({ contentRef }: { contentRef: React.RefObjec
7272
createEnterpriseConnection,
7373
updateEnterpriseConnection,
7474
deleteEnterpriseConnection,
75-
} = __internal_useUserEnterpriseConnections({ enabled: true });
75+
} = __internal_useOrganizationEnterpriseConnections({ enabled: true });
7676
// Currently FAPI only supports one enterprise connection per user
7777
const enterpriseConnection = enterpriseConnections?.[0];
7878

@@ -238,7 +238,7 @@ const ResetCardErrorOnStepChange = (): null => {
238238
const useHasSuccessfulTestRun = (
239239
enterpriseConnection: EnterpriseConnectionResource | undefined,
240240
): { hasSuccessfulTestRun: boolean; isLoading: boolean } => {
241-
const { data: successfulTestRuns, isLoading } = __internal_useEnterpriseConnectionTestRuns({
241+
const { data: successfulTestRuns, isLoading } = __internal_useOrganizationEnterpriseConnectionTestRuns({
242242
enterpriseConnectionId: enterpriseConnection?.id ?? null,
243243
params: { initialPage: 1, pageSize: 1, status: ['success'] },
244244
});

0 commit comments

Comments
 (0)