Skip to content

Commit 547f49d

Browse files
authored
fix(clerk-js): Do not display organization list after creating organization throw tasks flow (#6117)
1 parent 15a945c commit 547f49d

File tree

8 files changed

+197
-45
lines changed

8 files changed

+197
-45
lines changed

.changeset/common-mice-ring.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Prevent organization list from displaying after creating an organization through the force organization selection flow

packages/clerk-js/bundlewatch.config.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
{
22
"files": [
3-
{ "path": "./dist/clerk.js", "maxSize": "610.32kB" },
4-
{ "path": "./dist/clerk.browser.js", "maxSize": "70.2KB" },
5-
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "113KB" },
6-
{ "path": "./dist/clerk.headless*.js", "maxSize": "53.06KB" },
7-
{ "path": "./dist/ui-common*.js", "maxSize": "108.75KB" },
3+
{ "path": "./dist/clerk.js", "maxSize": "612kB" },
4+
{ "path": "./dist/clerk.browser.js", "maxSize": "72.2KB" },
5+
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "115KB" },
6+
{ "path": "./dist/clerk.headless*.js", "maxSize": "55KB" },
7+
{ "path": "./dist/ui-common*.js", "maxSize": "110KB" },
88
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
99
{ "path": "./dist/coinbase*.js", "maxSize": "38KB" },
1010
{ "path": "./dist/createorganization*.js", "maxSize": "5KB" },
@@ -25,6 +25,6 @@
2525
{ "path": "./dist/paymentSources*.js", "maxSize": "9.17KB" },
2626
{ "path": "./dist/up-billing-page*.js", "maxSize": "3.0KB" },
2727
{ "path": "./dist/op-billing-page*.js", "maxSize": "3.0KB" },
28-
{ "path": "./dist/sessionTasks*.js", "maxSize": "1KB" }
28+
{ "path": "./dist/sessionTasks*.js", "maxSize": "1.5KB" }
2929
]
3030
}

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,7 +1283,16 @@ export class Clerk implements ClerkInterface {
12831283
};
12841284

12851285
public __experimental_navigateToTask = async ({ redirectUrlComplete }: NextTaskParams = {}): Promise<void> => {
1286-
const session = await this.session?.reload();
1286+
/**
1287+
* Invalidate previously cache pages with auth state before navigating
1288+
*/
1289+
const onBeforeSetActive: SetActiveHook =
1290+
typeof window !== 'undefined' && typeof window.__unstable__onBeforeSetActive === 'function'
1291+
? window.__unstable__onBeforeSetActive
1292+
: noop;
1293+
await onBeforeSetActive();
1294+
1295+
const session = this.session;
12871296
if (!session || !this.environment) {
12881297
return;
12891298
}
@@ -1301,8 +1310,6 @@ export class Clerk implements ClerkInterface {
13011310
const tracker = createBeforeUnloadTracker(this.#options.standardBrowser);
13021311
const defaultRedirectUrlComplete = this.client?.signUp ? this.buildAfterSignUpUrl() : this.buildAfterSignInUrl();
13031312

1304-
this.#setTransitiveState();
1305-
13061313
await tracker.track(async () => {
13071314
await this.navigate(redirectUrlComplete ?? defaultRedirectUrlComplete);
13081315
});
@@ -1313,6 +1320,17 @@ export class Clerk implements ClerkInterface {
13131320

13141321
this.#setAccessors(session);
13151322
this.#emit();
1323+
1324+
/**
1325+
* Invoke the Next.js middleware to synchronize server and client state after resolving a session task.
1326+
* This ensures that any server-side logic depending on the session status (like middleware-based
1327+
* redirects or protected routes) correctly reflects the updated client authentication state.
1328+
*/
1329+
const onAfterSetActive: SetActiveHook =
1330+
typeof window !== 'undefined' && typeof window.__unstable__onAfterSetActive === 'function'
1331+
? window.__unstable__onAfterSetActive
1332+
: noop;
1333+
await onAfterSetActive();
13161334
};
13171335

13181336
public addListener = (listener: ListenerCallback): UnsubscribeCallback => {

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

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

5+
import { SessionTasksContext } from '@/ui/contexts/components/SessionTasks';
56
import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
67
import { Form } from '@/ui/elements/Form';
78
import { FormButtonContainer } from '@/ui/elements/FormButtons';
@@ -23,7 +24,7 @@ import { organizationListParams } from '../OrganizationSwitcher/utils';
2324

2425
type CreateOrganizationFormProps = {
2526
skipInvitationScreen: boolean;
26-
navigateAfterCreateOrganization: (organization: OrganizationResource) => Promise<unknown>;
27+
navigateAfterCreateOrganization?: (organization: OrganizationResource) => Promise<unknown>;
2728
onCancel?: () => void;
2829
onComplete?: () => void;
2930
flow: 'default' | 'organizationList';
@@ -37,6 +38,7 @@ type CreateOrganizationFormProps = {
3738
export const CreateOrganizationForm = withCardStateProvider((props: CreateOrganizationFormProps) => {
3839
const card = useCardState();
3940
const wizard = useWizard({ onNextStep: () => card.setError(undefined) });
41+
const sessionTasksContext = useContext(SessionTasksContext);
4042

4143
const lastCreatedOrganizationRef = React.useRef<OrganizationResource | null>(null);
4244
const { createOrganization, isLoaded, setActive, userMemberships } = useOrganizationList({
@@ -87,6 +89,11 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani
8789

8890
void userMemberships.revalidate?.();
8991

92+
if (sessionTasksContext) {
93+
await sessionTasksContext.nextTask();
94+
return;
95+
}
96+
9097
if (props.skipInvitationScreen ?? organization.maxAllowedMemberships === 1) {
9198
return completeFlow();
9299
}
@@ -100,7 +107,7 @@ export const CreateOrganizationForm = withCardStateProvider((props: CreateOrgani
100107
const completeFlow = () => {
101108
// We are confident that lastCreatedOrganizationRef.current will never be null
102109
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
103-
void props.navigateAfterCreateOrganization(lastCreatedOrganizationRef.current!);
110+
void props.navigateAfterCreateOrganization?.(lastCreatedOrganizationRef.current!);
104111

105112
props.onComplete?.();
106113
};

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

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { useOrganizationList, useUser } from '@clerk/shared/react';
2-
import { useContext, useState } from 'react';
2+
import { useState } from 'react';
33

44
import { Action, Actions } from '@/ui/elements/Actions';
55
import { Card } from '@/ui/elements/Card';
66
import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
77
import { Header } from '@/ui/elements/Header';
88

99
import { useEnvironment, useOrganizationListContext } from '../../contexts';
10-
import { SessionTasksContext } from '../../contexts/components/SessionTasks';
1110
import { Box, Col, descriptors, Flex, localizationKeys, Spinner } from '../../customizables';
1211
import { useInView } from '../../hooks';
1312
import { Add } from '../../icons';
@@ -116,7 +115,6 @@ export const OrganizationListPage = withCardStateProvider(() => {
116115
const OrganizationListFlows = ({ showListInitially }: { showListInitially: boolean }) => {
117116
const { navigateAfterCreateOrganization, skipInvitationScreen, hideSlug } = useOrganizationListContext();
118117
const [isCreateOrganizationFlow, setCreateOrganizationFlow] = useState(!showListInitially);
119-
const sessionTasksContext = useContext(SessionTasksContext);
120118
return (
121119
<>
122120
{!isCreateOrganizationFlow && (
@@ -131,7 +129,6 @@ const OrganizationListFlows = ({ showListInitially }: { showListInitially: boole
131129
>
132130
<CreateOrganizationForm
133131
flow='organizationList'
134-
onComplete={sessionTasksContext?.nextTask}
135132
startPage={{ headerTitle: localizationKeys('organizationList.createOrganization') }}
136133
skipInvitationScreen={skipInvitationScreen}
137134
navigateAfterCreateOrganization={org =>
@@ -148,7 +145,7 @@ const OrganizationListFlows = ({ showListInitially }: { showListInitially: boole
148145
);
149146
};
150147

151-
const OrganizationListPageList = (props: { onCreateOrganizationClick: () => void }) => {
148+
export const OrganizationListPageList = (props: { onCreateOrganizationClick: () => void }) => {
152149
const environment = useEnvironment();
153150

154151
const { ref, userMemberships, userSuggestions, userInvitations } = useOrganizationListInView();
Lines changed: 37 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,18 @@
11
import { useClerk } from '@clerk/shared/react';
22
import { eventComponentMounted } from '@clerk/shared/telemetry';
3-
import type { SessionTask } from '@clerk/types';
4-
import { useCallback, useContext, useEffect } from 'react';
3+
import { useCallback, useContext, useEffect, useState } from 'react';
54

65
import { Card } from '@/ui/elements/Card';
76
import { withCardStateProvider } from '@/ui/elements/contexts';
87
import { LoadingCardContainer } from '@/ui/elements/LoadingCard';
98

109
import { SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks';
11-
import { OrganizationListContext, SignInContext, SignUpContext } from '../../../ui/contexts';
12-
import {
13-
SessionTasksContext as SessionTasksContext,
14-
useSessionTasksContext,
15-
} from '../../contexts/components/SessionTasks';
10+
import { SignInContext, SignUpContext } from '../../../ui/contexts';
11+
import { SessionTasksContext, useSessionTasksContext } from '../../contexts/components/SessionTasks';
1612
import { Route, Switch, useRouter } from '../../router';
17-
import { OrganizationList } from '../OrganizationList';
13+
import { ForceOrganizationSelectionTask } from './tasks/ForceOrganizationSelection';
1814

19-
const SessionTasksStart = withCardStateProvider(() => {
15+
const SessionTasksStart = () => {
2016
const clerk = useClerk();
2117
const { navigate } = useRouter();
2218
const { redirectUrlComplete } = useSessionTasksContext();
@@ -37,20 +33,13 @@ const SessionTasksStart = withCardStateProvider(() => {
3733
<Card.Footer />
3834
</Card.Root>
3935
);
40-
});
36+
};
4137

4238
function SessionTaskRoutes(): JSX.Element {
4339
return (
4440
<Switch>
4541
<Route path={SESSION_TASK_ROUTE_BY_KEY['org']}>
46-
<OrganizationListContext.Provider
47-
value={{
48-
componentName: 'OrganizationList',
49-
skipInvitationScreen: true,
50-
}}
51-
>
52-
<OrganizationList />
53-
</OrganizationListContext.Provider>
42+
<ForceOrganizationSelectionTask />
5443
</Route>
5544
<Route index>
5645
<SessionTasksStart />
@@ -62,34 +51,55 @@ function SessionTaskRoutes(): JSX.Element {
6251
/**
6352
* @internal
6453
*/
65-
export function SessionTask(): JSX.Element {
54+
export const SessionTask = withCardStateProvider(() => {
6655
const clerk = useClerk();
6756
const { navigate } = useRouter();
6857
const signInContext = useContext(SignInContext);
6958
const signUpContext = useContext(SignUpContext);
59+
const [isNavigatingToTask, setIsNavigatingToTask] = useState(false);
7060

7161
const redirectUrlComplete =
7262
signInContext?.afterSignInUrl ?? signUpContext?.afterSignUpUrl ?? clerk?.buildAfterSignInUrl();
7363

64+
// If there are no pending tasks, navigate away from the tasks flow.
65+
// This handles cases where a user with an active session returns to the tasks URL,
66+
// for example by using browser back navigation. Since there are no pending tasks,
67+
// we redirect them to their intended destination.
7468
useEffect(() => {
75-
const task = clerk.session?.currentTask;
69+
if (isNavigatingToTask) {
70+
return;
71+
}
7672

77-
if (!task) {
73+
// Tasks can only exist on pending sessions, but we check both conditions
74+
// here to be defensive and ensure proper redirection
75+
const task = clerk.session?.currentTask;
76+
if (!task || clerk.session?.status === 'active') {
7877
void navigate(redirectUrlComplete);
7978
return;
8079
}
8180

8281
clerk.telemetry?.record(eventComponentMounted('SessionTask', { task: task.key }));
83-
}, [clerk, navigate, redirectUrlComplete]);
82+
}, [clerk, navigate, isNavigatingToTask, redirectUrlComplete]);
8483

85-
const nextTask = useCallback(
86-
() => clerk.__experimental_navigateToTask({ redirectUrlComplete }),
87-
[clerk, redirectUrlComplete],
88-
);
84+
const nextTask = useCallback(() => {
85+
setIsNavigatingToTask(true);
86+
return clerk.__experimental_navigateToTask({ redirectUrlComplete }).finally(() => setIsNavigatingToTask(false));
87+
}, [clerk, redirectUrlComplete]);
88+
89+
if (!clerk.session?.currentTask) {
90+
return (
91+
<Card.Root>
92+
<Card.Content>
93+
<LoadingCardContainer />
94+
</Card.Content>
95+
<Card.Footer />
96+
</Card.Root>
97+
);
98+
}
8999

90100
return (
91101
<SessionTasksContext.Provider value={{ nextTask, redirectUrlComplete }}>
92102
<SessionTaskRoutes />
93103
</SessionTasksContext.Provider>
94104
);
95-
}
105+
});
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { useOrganizationList } from '@clerk/shared/react/index';
2+
import type { PropsWithChildren } from 'react';
3+
import { useState } from 'react';
4+
5+
import { OrganizationListContext } from '@/ui/contexts';
6+
import { Card } from '@/ui/elements/Card';
7+
import { useCardState, withCardStateProvider } from '@/ui/elements/contexts';
8+
9+
import { Box, descriptors, Flex, localizationKeys, Spinner } from '../../../customizables';
10+
import { CreateOrganizationForm } from '../../CreateOrganization/CreateOrganizationForm';
11+
import { OrganizationListPageList } from '../../OrganizationList/OrganizationListPage';
12+
import { organizationListParams } from '../../OrganizationSwitcher/utils';
13+
14+
/**
15+
* @internal
16+
*/
17+
export const ForceOrganizationSelectionTask = withCardStateProvider(() => {
18+
const { userMemberships, userInvitations, userSuggestions } = useOrganizationList(organizationListParams);
19+
const isLoading = userMemberships?.isLoading || userInvitations?.isLoading || userSuggestions?.isLoading;
20+
const hasData = !!(userMemberships?.count || userInvitations?.count || userSuggestions?.count);
21+
22+
if (isLoading) {
23+
return (
24+
<FlowCard>
25+
<FlowLoadingState />
26+
</FlowCard>
27+
);
28+
}
29+
30+
if (hasData) {
31+
return <OrganizationSelectionPage />;
32+
}
33+
34+
return <CreateOrganizationPage />;
35+
});
36+
37+
const OrganizationSelectionPage = () => {
38+
const [showCreateOrganizationForm, setShowCreateOrganizationForm] = useState(false);
39+
40+
return (
41+
<OrganizationListContext.Provider
42+
value={{
43+
componentName: 'OrganizationList',
44+
skipInvitationScreen: true,
45+
}}
46+
>
47+
<FlowCard>
48+
{showCreateOrganizationForm ? (
49+
<Box
50+
sx={t => ({
51+
padding: `${t.space.$none} ${t.space.$5} ${t.space.$5}`,
52+
})}
53+
>
54+
<CreateOrganizationForm
55+
flow='default'
56+
startPage={{ headerTitle: localizationKeys('organizationList.createOrganization') }}
57+
skipInvitationScreen
58+
onCancel={() => setShowCreateOrganizationForm(false)}
59+
/>
60+
</Box>
61+
) : (
62+
<OrganizationListPageList onCreateOrganizationClick={() => setShowCreateOrganizationForm(true)} />
63+
)}
64+
</FlowCard>
65+
</OrganizationListContext.Provider>
66+
);
67+
};
68+
69+
const CreateOrganizationPage = () => {
70+
return (
71+
<FlowCard>
72+
<Box
73+
sx={t => ({
74+
padding: `${t.space.$none} ${t.space.$5} ${t.space.$5}`,
75+
})}
76+
>
77+
<CreateOrganizationForm
78+
flow='default'
79+
startPage={{ headerTitle: localizationKeys('organizationList.createOrganization') }}
80+
skipInvitationScreen
81+
/>
82+
</Box>
83+
</FlowCard>
84+
);
85+
};
86+
87+
const FlowCard = ({ children }: PropsWithChildren) => {
88+
const card = useCardState();
89+
90+
return (
91+
<Card.Root>
92+
<Card.Content sx={t => ({ padding: `${t.space.$8} ${t.space.$none} ${t.space.$none}` })}>
93+
<Card.Alert sx={t => ({ margin: `${t.space.$none} ${t.space.$5}` })}>{card.error}</Card.Alert>
94+
{children}
95+
</Card.Content>
96+
<Card.Footer />
97+
</Card.Root>
98+
);
99+
};
100+
101+
const FlowLoadingState = () => (
102+
<Flex
103+
direction='row'
104+
center
105+
sx={t => ({
106+
height: '100%',
107+
minHeight: t.sizes.$60,
108+
})}
109+
>
110+
<Spinner
111+
size='xl'
112+
colorScheme='primary'
113+
elementDescriptor={descriptors.spinner}
114+
/>
115+
</Flex>
116+
);

0 commit comments

Comments
 (0)