Skip to content

Commit e77e33f

Browse files
committed
Create connection for organization
1 parent d8a8c2a commit e77e33f

5 files changed

Lines changed: 75 additions & 24 deletions

File tree

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

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

@@ -53,13 +53,16 @@ const AuthenticatedContent = withCoreUserGuard(() => {
5353
const { parsedOptions } = useAppearance();
5454
const hasLogo = Boolean(parsedOptions.logoImageUrl || logoImageUrl);
5555

56-
// Gate the entire wizard behind the org-level permission. When the
57-
// user can't manage enterprise connections, we still render the
58-
// outer sidebar/title chrome but replace the wizard with a
59-
// permissions-error message so the layout stays consistent
60-
const canManageEnterpriseConnections = useProtect({
56+
const { session } = useSession();
57+
const hasActiveOrganization = Boolean(session?.lastActiveOrganizationId);
58+
59+
// Gate the entire wizard behind the org-level permission. `useProtect`
60+
// must be called unconditionally to satisfy the rules of hooks; we
61+
// combine its result with `hasActiveOrganization` afterwards
62+
const hasManagePermission = useProtect({
6163
permission: 'org:sys_enterprise_connections:manage',
6264
});
65+
const canManageEnterpriseConnections = hasActiveOrganization && hasManagePermission;
6366

6467
const {
6568
data: enterpriseConnections,
@@ -69,7 +72,7 @@ const AuthenticatedContent = withCoreUserGuard(() => {
6972
deleteEnterpriseConnection,
7073
revalidate: revalidateEnterpriseConnections,
7174
} = __internal_useUserEnterpriseConnections({
72-
enabled: canManageEnterpriseConnections,
75+
enabled: true,
7376
});
7477
// Currently FAPI only supports one enterprise connection per user
7578
const enterpriseConnection = enterpriseConnections?.[0];
@@ -148,7 +151,7 @@ const AuthenticatedContent = withCoreUserGuard(() => {
148151
flex: 1,
149152
})}
150153
>
151-
{canManageEnterpriseConnections ? (
154+
{canManageEnterpriseConnections || !hasActiveOrganization ? (
152155
<ConfigureSSOFlowProvider
153156
enterpriseConnection={enterpriseConnection}
154157
isLoading={isLoadingEnterpriseConnections}
@@ -185,6 +188,7 @@ const ConfigureSSOSteps = () => {
185188
id='select-provider'
186189
path='select-provider'
187190
label='Select provider'
191+
hideFromBreadcrumb
188192
>
189193
<SelectProviderStep />
190194
</ConfigureSSOWizard.Step>
@@ -237,9 +241,18 @@ const ConfigureSSOSteps = () => {
237241
};
238242

239243
const OrganizationSidebarSubtitle = () => {
240-
const { organization } = useOrganization();
244+
// Resolve the active org's name without `useOrganization()` (which
245+
// would subscribe to the organization resource). The active id lives
246+
// on the session, and the user already carries the matching
247+
// membership eagerly, so we can join the two without an extra fetch
248+
const { user } = useUser();
249+
const { session } = useSession();
250+
const activeOrganizationId = session?.lastActiveOrganizationId ?? null;
251+
const activeOrganization = activeOrganizationId
252+
? user?.organizationMemberships.find(m => m.organization.id === activeOrganizationId)?.organization
253+
: undefined;
241254

242-
if (!organization) {
255+
if (!activeOrganization) {
243256
return null;
244257
}
245258

@@ -249,7 +262,7 @@ const OrganizationSidebarSubtitle = () => {
249262
truncate
250263
sx={t => ({ color: t.colors.$colorMutedForeground })}
251264
>
252-
{organization?.name}
265+
{activeOrganization.name}
253266
</Text>
254267
);
255268
};

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useUser } from '@clerk/shared/react';
1+
import { useSession, useUser } from '@clerk/shared/react';
22
import React from 'react';
33

44
import { Box, Button, Col, descriptors, Flex, Flow, Icon, Input, Spinner, Text } from '@/customizables';
@@ -19,6 +19,10 @@ export const ConfigureCreateApp = (): JSX.Element => {
1919
useConfigureSSOFlow();
2020
const { user } = useUser();
2121
const card = useCardState();
22+
// Active org id straight off the session — `useOrganization()` would
23+
// subscribe to the organization resource and we only need the id
24+
const { session } = useSession();
25+
const activeOrganizationId = session?.lastActiveOrganizationId ?? undefined;
2226

2327
const primaryEmail = user?.primaryEmailAddress?.emailAddress ?? '';
2428
const emailDomain = getEmailDomain(primaryEmail);
@@ -51,6 +55,7 @@ export const ConfigureCreateApp = (): JSX.Element => {
5155
createEnterpriseConnection({
5256
provider: selectedProvider,
5357
name: emailDomain,
58+
organizationId: activeOrganizationId,
5459
})
5560
.catch(err => handleError(err as Error, [], card.setError))
5661
.finally(() => setIsCreating(false));

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

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useOrganization, useReverification, useUser } from '@clerk/shared/react';
1+
import { useReverification, useSession, useUser } from '@clerk/shared/react';
22
import React from 'react';
33

44
import { Col, Flow, Heading, Icon, Input, localizationKeys, Text, useLocalizations } from '@/customizables';
@@ -18,19 +18,23 @@ export const VerifyDomainStep = (): JSX.Element | null => {
1818
const card = useCardState();
1919
const { t } = useLocalizations();
2020
const { user } = useUser();
21-
const { organization } = useOrganization();
21+
const { session } = useSession();
22+
const activeOrganizationId = session?.lastActiveOrganizationId ?? null;
2223

2324
const emailToVerify =
2425
user?.primaryEmailAddress ?? user?.emailAddresses?.find(e => e.verification.status !== 'verified');
2526
const isVerified = emailToVerify?.verification.status === 'verified';
2627
const isAlreadyPrimary = Boolean(emailToVerify && emailToVerify.id === user?.primaryEmailAddressId);
2728

29+
// Domain that the existing connection is registered for.
30+
const conflictingDomain = enterpriseConnection?.domains?.[0] ?? getEmailDomain(emailToVerify?.emailAddress ?? '');
31+
2832
// The user's domain is already wired to an enterprise connection that
2933
// doesn't belong to the org they're currently configuring. They can't
3034
// take it over from here — they need the existing app's owner to
3135
// re-configure (or share) the connection
3236
const isDomainTakenByOtherOrg = Boolean(
33-
isVerified && enterpriseConnection && enterpriseConnection.organizationId !== (organization?.id ?? null),
37+
isVerified && enterpriseConnection && enterpriseConnection.organizationId !== activeOrganizationId,
3438
);
3539

3640
const prepareEmailVerification = useReverification(() =>
@@ -147,14 +151,16 @@ export const VerifyDomainStep = (): JSX.Element | null => {
147151
textVariant='h1'
148152
sx={t => ({ color: t.colors.$colorForeground, fontSize: t.fontSizes.$md })}
149153
>
150-
That domain already has an SSO connection
154+
{conflictingDomain
155+
? `This domain (${conflictingDomain}) already has an SSO connection`
156+
: 'This domain already has an SSO connection'}
151157
</Heading>
152158
<Text
153159
as='p'
154160
variant='body'
155161
sx={t => ({ color: t.colors.$colorMutedForeground })}
156162
>
157-
Contact the application's administrator to get access through the existing connection.
163+
Contact the application&apos;s administrator to get access through the existing connection.
158164
</Text>
159165
</Col>
160166
</>
@@ -217,3 +223,11 @@ export const VerifyDomainStep = (): JSX.Element | null => {
217223
</Flow.Part>
218224
);
219225
};
226+
227+
function getEmailDomain(emailAddress: string): string {
228+
const at = emailAddress.lastIndexOf('@');
229+
if (at === -1) {
230+
return '';
231+
}
232+
return emailAddress.slice(at + 1).toLowerCase();
233+
}

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

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ function extractSteps(children: React.ReactNode): ConfigureSSOWizardActiveStep[]
5555
path: props.path,
5656
label: props.label,
5757
isCompleted: props.isCompleted,
58+
hideFromBreadcrumb: props.hideFromBreadcrumb,
5859
children: props.children,
5960
});
6061
});
@@ -265,12 +266,16 @@ const StepBody = ({ step }: { step: ConfigureSSOWizardActiveStep }): JSX.Element
265266
/**
266267
* Numbered breadcrumb of the outermost wizard's active steps.
267268
* Completed and current steps are clickable for backwards navigation,
268-
* future steps are disabled
269+
* future steps are disabled. Steps marked with `hideFromBreadcrumb`
270+
* are silently skipped over and the visible steps are renumbered, so
271+
* dropping a hidden step from the wizard later doesn't shift numbers
269272
*/
270273
const Header = (): JSX.Element => {
271274
const { activeSteps, currentIndex, isLoading, goToStep } = useConfigureSSOWizard();
272275
const { t } = useLocalizations();
273276

277+
const visibleSteps = React.useMemo(() => activeSteps.filter(s => !s.hideFromBreadcrumb), [activeSteps]);
278+
274279
return (
275280
<Flex
276281
elementDescriptor={descriptors.configureSSOWizardHeader}
@@ -284,10 +289,15 @@ const Header = (): JSX.Element => {
284289
flexWrap: 'wrap',
285290
})}
286291
>
287-
{activeSteps.map((step, index) => {
288-
const isCurrent = index === currentIndex;
289-
const isCompleted = step.isCompleted ?? index < currentIndex;
290-
const isReachable = isCompleted || index <= currentIndex;
292+
{visibleSteps.map((step, visibleIndex) => {
293+
// `currentIndex` is computed against the full `activeSteps`
294+
// list, so look the visible step back up there to keep
295+
// current/completed/reachable consistent with the wizard's
296+
// own routing state
297+
const actualIndex = activeSteps.findIndex(s => s.id === step.id);
298+
const isCurrent = actualIndex === currentIndex;
299+
const isCompleted = step.isCompleted ?? actualIndex < currentIndex;
300+
const isReachable = isCompleted || actualIndex <= currentIndex;
291301
const labelText = step.label ? (typeof step.label === 'string' ? step.label : t(step.label)) : '';
292302

293303
return (
@@ -342,7 +352,7 @@ const Header = (): JSX.Element => {
342352
size='xs'
343353
/>
344354
) : (
345-
index + 1
355+
visibleIndex + 1
346356
)}
347357
</Flex>
348358
<Text
@@ -356,7 +366,7 @@ const Header = (): JSX.Element => {
356366
</Text>
357367
</Button>
358368
)}
359-
{index < activeSteps.length - 1 && (
369+
{visibleIndex < visibleSteps.length - 1 && (
360370
<Icon
361371
elementDescriptor={descriptors.configureSSOWizardHeaderSeparator}
362372
icon={CaretRight}

packages/ui/src/components/ConfigureSSO/wizard/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ export interface ConfigureSSOWizardStepProps {
3131
* to the current step
3232
*/
3333
isCompleted?: boolean;
34+
/**
35+
* Hides this step from the wizard breadcrumb/header while keeping
36+
* it routable. Useful for "pre-flight" steps like provider
37+
* selection that shouldn't be visible (or clickable) once the user
38+
* has moved past them — and to avoid the visible step count
39+
* shifting when the step is later dropped from the wizard
40+
*/
41+
hideFromBreadcrumb?: boolean;
3442
/**
3543
* The step body. Anything React, including a nested
3644
* `<ConfigureSSOWizard>` for inner sub-steps
@@ -74,6 +82,7 @@ export interface ConfigureSSOWizardActiveStep {
7482
path: string;
7583
label?: LocalizationKey | string;
7684
isCompleted?: boolean;
85+
hideFromBreadcrumb?: boolean;
7786
children: React.ReactNode;
7887
}
7988

0 commit comments

Comments
 (0)