Skip to content

Commit b0fdc9e

Browse files
authored
feat(clerk-js,clerk-react,nextjs): Introduce <TaskSelectOrganization /> component (#6376)
1 parent 6876563 commit b0fdc9e

File tree

26 files changed

+323
-213
lines changed

26 files changed

+323
-213
lines changed

.changeset/bright-cats-lay.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@clerk/clerk-js': minor
3+
---
4+
5+
Introduce `<TaskSelectOrganization />` component.
6+
7+
It allows you to eject the organization selection task flow from the default `SignIn` and `SignUp` components and render it on custom URL paths using `taskUrls`.
8+
9+
Usage example:
10+
```tsx
11+
<ClerkProvider taskUrls={{ 'select-organization': '/onboarding/select-organization' }}>
12+
<App />
13+
</ClerkProvider>
14+
```
15+
16+
```tsx
17+
function OnboardingSelectOrganization() {
18+
return <TaskSelectOrganization redirectUrlComplete="/dashboard/onboarding-complete" />
19+
}
20+
```

.changeset/vast-places-tap.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/types': patch
3+
---
4+
5+
Add TypeScript types for `<TaskSelectOrganization />` component.

packages/clerk-js/src/core/clerk.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import type {
7676
SignUpProps,
7777
SignUpRedirectOptions,
7878
SignUpResource,
79+
TaskSelectOrganizationProps,
7980
UnsubscribeCallback,
8081
UserButtonProps,
8182
UserProfileProps,
@@ -1164,6 +1165,35 @@ export class Clerk implements ClerkInterface {
11641165
void this.#componentControls.ensureMounted().then(controls => controls.unmountComponent({ node }));
11651166
};
11661167

1168+
public mountTaskSelectOrganization = (node: HTMLDivElement, props?: TaskSelectOrganizationProps) => {
1169+
this.assertComponentsReady(this.#componentControls);
1170+
1171+
if (disabledOrganizationsFeature(this, this.environment)) {
1172+
if (this.#instanceType === 'development') {
1173+
throw new ClerkRuntimeError(warnings.cannotRenderAnyOrganizationComponent('TaskSelectOrganization'), {
1174+
code: CANNOT_RENDER_ORGANIZATIONS_DISABLED_ERROR_CODE,
1175+
});
1176+
}
1177+
return;
1178+
}
1179+
1180+
void this.#componentControls.ensureMounted({ preloadHint: 'TaskSelectOrganization' }).then(controls =>
1181+
controls.mountComponent({
1182+
name: 'TaskSelectOrganization',
1183+
appearanceKey: 'taskSelectOrganization',
1184+
node,
1185+
props,
1186+
}),
1187+
);
1188+
1189+
this.telemetry?.record(eventPrebuiltComponentMounted('TaskSelectOrganization', props));
1190+
};
1191+
1192+
public unmountTaskSelectOrganization = (node: HTMLDivElement) => {
1193+
this.assertComponentsReady(this.#componentControls);
1194+
void this.#componentControls.ensureMounted().then(controls => controls.unmountComponent({ node }));
1195+
};
1196+
11671197
/**
11681198
* `setActive` can be used to set the active session and/or organization.
11691199
*/

packages/clerk-js/src/core/sessionTasks.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,23 +27,25 @@ export function navigateToTask(
2727
routeKey: keyof typeof INTERNAL_SESSION_TASK_ROUTE_BY_KEY,
2828
{ componentNavigationContext, globalNavigate, options, environment }: NavigateToTaskOptions,
2929
) {
30-
const taskRoute = `/tasks/${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[routeKey]}`;
30+
const customTaskUrl = options?.taskUrls?.[routeKey];
31+
const internalTaskRoute = `/tasks/${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[routeKey]}`;
3132

32-
if (componentNavigationContext) {
33-
return componentNavigationContext.navigate(componentNavigationContext.indexPath + taskRoute);
33+
if (componentNavigationContext && !customTaskUrl) {
34+
return componentNavigationContext.navigate(componentNavigationContext.indexPath + internalTaskRoute);
3435
}
3536

3637
const signInUrl = options['signInUrl'] || environment.displayConfig.signInUrl;
3738
const signUpUrl = options['signUpUrl'] || environment.displayConfig.signUpUrl;
3839
const isReferrerSignUpUrl = window.location.href.startsWith(signUpUrl);
3940

40-
const sessionTaskUrl = buildURL(
41-
{
42-
base: isReferrerSignUpUrl ? signUpUrl : signInUrl,
43-
hashPath: taskRoute,
44-
},
45-
{ stringify: true },
41+
return globalNavigate(
42+
customTaskUrl ??
43+
buildURL(
44+
{
45+
base: isReferrerSignUpUrl ? signUpUrl : signInUrl,
46+
hashPath: internalTaskRoute,
47+
},
48+
{ stringify: true },
49+
),
4650
);
47-
48-
return globalNavigate(options.taskUrls?.[routeKey] ?? sessionTaskUrl);
4951
}

packages/clerk-js/src/core/warnings.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@ const formatWarning = (msg: string) => {
55
};
66

77
const createMessageForDisabledOrganizations = (
8-
componentName: 'OrganizationProfile' | 'OrganizationSwitcher' | 'OrganizationList' | 'CreateOrganization',
8+
componentName:
9+
| 'OrganizationProfile'
10+
| 'OrganizationSwitcher'
11+
| 'OrganizationList'
12+
| 'CreateOrganization'
13+
| 'TaskSelectOrganization',
914
) => {
1015
return formatWarning(
1116
`The <${componentName}/> cannot be rendered when the feature is turned off. Visit 'dashboard.clerk.com' to enable the feature. Since the feature is turned off, this is no-op.`,
@@ -23,6 +28,8 @@ const warnings = {
2328
'The <SignUp/> component cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the value set in `afterSignUp` URL instead.',
2429
cannotRenderSignUpComponentWhenTaskExists:
2530
'The <SignUp/> component cannot render when a user has a pending task, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the task instead.',
31+
cannotRenderComponentWhenTaskDoesNotExist:
32+
'<TaskSelectOrganization/> cannot render unless a session task is pending. Clerk is redirecting to the value set in `redirectUrlComplete` instead.',
2633
cannotRenderSignInComponentWhenSessionExists:
2734
'The <SignIn/> component cannot render when a user is already signed in, unless the application allows multiple sessions. Since a user is signed in and this application only allows a single session, Clerk is redirecting to the `afterSignIn` URL instead.',
2835
cannotRenderSignInComponentWhenTaskExists:

packages/clerk-js/src/ui/components/CreateOrganization/CreateOrganizationForm.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useOrganization, useOrganizationList } from '@clerk/shared/react';
1+
import { useClerk, useOrganization, useOrganizationList } from '@clerk/shared/react';
22
import type { CreateOrganizationParams, OrganizationResource } from '@clerk/types';
33
import React, { useContext } from 'react';
44

@@ -41,6 +41,7 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani
4141
const card = useCardState();
4242
const wizard = useWizard({ onNextStep: () => card.setError(undefined) });
4343
const sessionTasksContext = useContext(SessionTasksContext);
44+
const clerk = useClerk();
4445

4546
const lastCreatedOrganizationRef = React.useRef<OrganizationResource | null>(null);
4647
const { createOrganization, isLoaded, setActive, userMemberships } = useOrganizationList({
@@ -89,13 +90,15 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani
8990
lastCreatedOrganizationRef.current = organization;
9091
await setActive({ organization });
9192

92-
void userMemberships.revalidate?.();
93-
9493
if (sessionTasksContext) {
95-
await sessionTasksContext.nextTask();
94+
await clerk.__internal_navigateToTaskIfAvailable({
95+
redirectUrlComplete: sessionTasksContext.redirectUrlComplete,
96+
});
9697
return;
9798
}
9899

100+
void userMemberships.revalidate?.();
101+
99102
if (props.skipInvitationScreen ?? organization.maxAllowedMemberships === 1) {
100103
return completeFlow();
101104
}

packages/clerk-js/src/ui/components/OrganizationList/UserMembershipList.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useOrganizationList, useUser } from '@clerk/shared/react';
1+
import { useClerk, useOrganizationList, useUser } from '@clerk/shared/react';
22
import type { OrganizationResource } from '@clerk/types';
33
import { useContext } from 'react';
44

@@ -15,6 +15,7 @@ export const MembershipPreview = withCardStateProvider((props: { organization: O
1515
const card = useCardState();
1616
const { navigateAfterSelectOrganization } = useOrganizationListContext();
1717
const { isLoaded, setActive } = useOrganizationList();
18+
const clerk = useClerk();
1819
const sessionTasksContext = useContext(SessionTasksContext);
1920

2021
if (!isLoaded) {
@@ -26,8 +27,10 @@ export const MembershipPreview = withCardStateProvider((props: { organization: O
2627
organization,
2728
});
2829

29-
if (sessionTasksContext?.nextTask) {
30-
return sessionTasksContext?.nextTask();
30+
if (sessionTasksContext) {
31+
return clerk.__internal_navigateToTaskIfAvailable({
32+
redirectUrlComplete: sessionTasksContext.redirectUrlComplete,
33+
});
3134
}
3235

3336
await navigateAfterSelectOrganization(organization);

packages/clerk-js/src/ui/components/SessionTasks/index.tsx

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { useClerk } from '@clerk/shared/react';
22
import { eventComponentMounted } from '@clerk/shared/telemetry';
3-
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
3+
import { useContext, useEffect, useRef } from 'react';
44

55
import { Card } from '@/ui/elements/Card';
66
import { withCardStateProvider } from '@/ui/elements/contexts';
77
import { LoadingCardContainer } from '@/ui/elements/LoadingCard';
88

99
import { INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks';
1010
import { SignInContext, SignUpContext } from '../../../ui/contexts';
11-
import { SessionTasksContext, useSessionTasksContext } from '../../contexts/components/SessionTasks';
11+
import {
12+
SessionTasksContext,
13+
TaskSelectOrganizationContext,
14+
useSessionTasksContext,
15+
} from '../../contexts/components/SessionTasks';
1216
import { Route, Switch, useRouter } from '../../router';
13-
import { ForceOrganizationSelectionTask } from './tasks/ForceOrganizationSelection';
17+
import { TaskSelectOrganization } from './tasks/TaskSelectOrganization';
1418

1519
const SessionTasksStart = () => {
1620
const clerk = useClerk();
@@ -36,10 +40,16 @@ const SessionTasksStart = () => {
3640
};
3741

3842
function SessionTaskRoutes(): JSX.Element {
43+
const ctx = useSessionTasksContext();
44+
3945
return (
4046
<Switch>
4147
<Route path={INTERNAL_SESSION_TASK_ROUTE_BY_KEY['select-organization']}>
42-
<ForceOrganizationSelectionTask />
48+
<TaskSelectOrganizationContext.Provider
49+
value={{ componentName: 'TaskSelectOrganization', redirectUrlComplete: ctx.redirectUrlComplete }}
50+
>
51+
<TaskSelectOrganization />
52+
</TaskSelectOrganizationContext.Provider>
4353
</Route>
4454
<Route index>
4555
<SessionTasksStart />
@@ -56,7 +66,6 @@ export const SessionTask = withCardStateProvider(() => {
5666
const { navigate } = useRouter();
5767
const signInContext = useContext(SignInContext);
5868
const signUpContext = useContext(SignUpContext);
59-
const [isNavigatingToTask, setIsNavigatingToTask] = useState(false);
6069
const currentTaskContainer = useRef<HTMLDivElement>(null);
6170

6271
const redirectUrlComplete =
@@ -67,10 +76,6 @@ export const SessionTask = withCardStateProvider(() => {
6776
// for example by using browser back navigation. Since there are no pending tasks,
6877
// we redirect them to their intended destination.
6978
useEffect(() => {
70-
if (isNavigatingToTask) {
71-
return;
72-
}
73-
7479
// Tasks can only exist on pending sessions, but we check both conditions
7580
// here to be defensive and ensure proper redirection
7681
const task = clerk.session?.currentTask;
@@ -80,14 +85,7 @@ export const SessionTask = withCardStateProvider(() => {
8085
}
8186

8287
clerk.telemetry?.record(eventComponentMounted('SessionTask', { task: task.key }));
83-
}, [clerk, navigate, isNavigatingToTask, redirectUrlComplete]);
84-
85-
const nextTask = useCallback(() => {
86-
setIsNavigatingToTask(true);
87-
return clerk
88-
.__internal_navigateToTaskIfAvailable({ redirectUrlComplete })
89-
.finally(() => setIsNavigatingToTask(false));
90-
}, [clerk, redirectUrlComplete]);
88+
}, [clerk, navigate, redirectUrlComplete]);
9189

9290
if (!clerk.session?.currentTask) {
9391
return (
@@ -105,7 +103,7 @@ export const SessionTask = withCardStateProvider(() => {
105103
}
106104

107105
return (
108-
<SessionTasksContext.Provider value={{ nextTask, redirectUrlComplete, currentTaskContainer }}>
106+
<SessionTasksContext.Provider value={{ redirectUrlComplete, currentTaskContainer }}>
109107
<SessionTaskRoutes />
110108
</SessionTasksContext.Provider>
111109
);

0 commit comments

Comments
 (0)