Skip to content

Commit 9b5b6c0

Browse files
committed
Introduce root route to resolve tasks
1 parent a8a6526 commit 9b5b6c0

File tree

16 files changed

+126
-42
lines changed

16 files changed

+126
-42
lines changed

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -486,14 +486,14 @@ describe('Clerk singleton', () => {
486486
getToken: jest.fn(),
487487
lastActiveToken: { getRawString: () => 'mocked-token' },
488488
tasks: [{ key: 'org' }],
489-
currentTask: { key: 'org', __internal_getUrl: () => 'https://foocorp.com/add-organization' },
489+
currentTask: { key: 'org', __internal_getUrl: () => 'https://sut/tasks/add-organization' },
490490
reload: jest.fn(() =>
491491
Promise.resolve({
492492
id: '1',
493493
status: 'pending',
494494
user: {},
495495
tasks: [{ key: 'org' }],
496-
currentTask: { key: 'org', __internal_getUrl: () => 'https://foocorp.com/add-organization' },
496+
currentTask: { key: 'org', __internal_getUrl: () => 'https://sut/tasks/add-organization' },
497497
}),
498498
),
499499
};
@@ -2276,7 +2276,7 @@ describe('Clerk singleton', () => {
22762276
status: 'pending',
22772277
user: {},
22782278
tasks: [{ key: 'org' }],
2279-
currentTask: { key: 'org', __internal_getUrl: () => 'https://foocorp.com/add-organization' },
2279+
currentTask: { key: 'org', __internal_getUrl: () => 'https://sut/tasks/add-organization' },
22802280
lastActiveToken: { getRawString: () => 'mocked-token' },
22812281
};
22822282

@@ -2305,7 +2305,7 @@ describe('Clerk singleton', () => {
23052305
await sut.setActive({ session: mockResource as any as PendingSessionResource });
23062306
await sut.__experimental_nextTask();
23072307

2308-
expect(mockNavigate.mock.calls[0][0]).toBe('/sign-in#/add-organization');
2308+
expect(mockNavigate.mock.calls[0][0]).toBe('/sign-in#/tasks/add-organization');
23092309
});
23102310
});
23112311

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export function navigateToTask(
3030
const taskRoute = `/tasks/${SESSION_TASK_ROUTE_BY_KEY[routeKey]}`;
3131

3232
if (componentNavigationContext) {
33-
return componentNavigationContext.navigate(componentNavigationContext.indexPath + routeKey);
33+
return componentNavigationContext.navigate(componentNavigationContext.indexPath + taskRoute);
3434
}
3535

3636
const signInUrl = options['signInUrl'] || environment.displayConfig.signInUrl;

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ const createMessageForDisabledOrganizations = (
1212
);
1313
};
1414
const warnings = {
15-
cannotRenderComponentWhenSessionExists:
16-
'The <SignUp/> and <SignIn/> components 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 Home URL instead.',
15+
cannotRenderComponentWithoutSession:
16+
'The component cannot render if the user does not have a valid session. Clerk is redirecting to the value set in `signInUrl` URL instead.',
1717
cannotRenderSignUpComponentWhenSessionExists:
1818
'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.',
1919
cannotRenderSignUpComponentWhenTaskExists:

packages/clerk-js/src/ui/common/redirects.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { SessionTask } from '@clerk/types';
22

3+
import { SESSION_TASK_ROUTE_BY_KEY } from '../../core/sessionTasks';
34
import { buildURL } from '../../utils/url';
45
import type { SignInContextType, SignUpContextType, UserProfileContextType } from './../contexts';
56

@@ -44,7 +45,7 @@ export function buildSessionTaskRedirectUrl({
4445
routing,
4546
baseUrl,
4647
path,
47-
endpoint: '/tasks',
48+
endpoint: `/tasks/${SESSION_TASK_ROUTE_BY_KEY[task.key]}`,
4849
authQueryString: null,
4950
});
5051
}

packages/clerk-js/src/ui/common/withRedirect.tsx

+22-3
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@ import { isDevelopmentFromPublishableKey } from '@clerk/shared/keys';
22
import { useClerk } from '@clerk/shared/react';
33
import type { Clerk, ClerkOptions, EnvironmentResource } from '@clerk/types';
44
import type { ComponentType } from 'react';
5-
import React from 'react';
5+
import React, { useContext } from 'react';
66

77
import { warnings } from '../../core/warnings';
88
import type { ComponentGuard } from '../../utils';
9-
import { sessionExistsAndSingleSessionModeEnabled } from '../../utils';
10-
import { useEnvironment, useOptions, useSignInContext, useSignUpContext } from '../contexts';
9+
import { noSessionExists, sessionExistsAndSingleSessionModeEnabled } from '../../utils';
10+
import { SignInContext, useEnvironment, useOptions, useSignInContext, useSignUpContext } from '../contexts';
1111
import { useRouter } from '../router';
1212
import type { AvailableComponentProps } from '../types';
1313

@@ -93,3 +93,22 @@ export const withRedirectToAfterSignUp = <P extends AvailableComponentProps>(Com
9393

9494
return HOC;
9595
};
96+
97+
export const withRedirectToSignIn = <P extends AvailableComponentProps>(Component: ComponentType<P>) => {
98+
const displayName = Component.displayName || Component.name || 'Component';
99+
Component.displayName = displayName;
100+
101+
const HOC = (props: P) => {
102+
const signInCtx = useContext(SignInContext);
103+
return withRedirect(
104+
Component,
105+
noSessionExists,
106+
({ clerk }) => signInCtx?.signInUrl || clerk.buildSignInUrl(),
107+
warnings.cannotRenderComponentWithoutSession,
108+
)(props);
109+
};
110+
111+
HOC.displayName = `withRedirectToSignIn(${displayName})`;
112+
113+
return HOC;
114+
};

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { useOrganizationList, useUser } from '@clerk/shared/react';
22
import { useContext, useState } from 'react';
33

44
import { useEnvironment, useOrganizationListContext } from '../../contexts';
5-
import { SessionTaskContext } from '../../contexts/components/SessionTask';
5+
import { SessionTasksContext } from '../../contexts/components/SessionTasks';
66
import { Box, Col, descriptors, Flex, localizationKeys, Spinner } from '../../customizables';
77
import { Action, Actions, Card, Header, useCardState, withCardStateProvider } from '../../elements';
88
import { useInView } from '../../hooks';
@@ -112,7 +112,7 @@ export const OrganizationListPage = withCardStateProvider(() => {
112112
const OrganizationListFlows = ({ showListInitially }: { showListInitially: boolean }) => {
113113
const { navigateAfterCreateOrganization, skipInvitationScreen, hideSlug } = useOrganizationListContext();
114114
const [isCreateOrganizationFlow, setCreateOrganizationFlow] = useState(!showListInitially);
115-
const sessionTaskContext = useContext(SessionTaskContext);
115+
const sessionTasksContext = useContext(SessionTasksContext);
116116
return (
117117
<>
118118
{!isCreateOrganizationFlow && (
@@ -127,7 +127,7 @@ const OrganizationListFlows = ({ showListInitially }: { showListInitially: boole
127127
>
128128
<CreateOrganizationForm
129129
flow='organizationList'
130-
onComplete={sessionTaskContext?.nextTask}
130+
onComplete={sessionTasksContext?.nextTask}
131131
startPage={{ headerTitle: localizationKeys('organizationList.createOrganization') }}
132132
skipInvitationScreen={skipInvitationScreen}
133133
navigateAfterCreateOrganization={org =>

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

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { OrganizationResource } from '@clerk/types';
33
import { useContext } from 'react';
44

55
import { useOrganizationListContext } from '../../contexts';
6-
import { SessionTaskContext } from '../../contexts/components/SessionTask';
6+
import { SessionTasksContext } from '../../contexts/components/SessionTasks';
77
import { OrganizationPreview, PersonalWorkspacePreview, useCardState, withCardStateProvider } from '../../elements';
88
import { localizationKeys } from '../../localization';
99
import { OrganizationListPreviewButton, sharedMainIdentifierSx } from './shared';
@@ -12,7 +12,7 @@ export const MembershipPreview = withCardStateProvider((props: { organization: O
1212
const card = useCardState();
1313
const { navigateAfterSelectOrganization } = useOrganizationListContext();
1414
const { isLoaded, setActive } = useOrganizationList();
15-
const sessionTaskContext = useContext(SessionTaskContext);
15+
const sessionTasksContext = useContext(SessionTasksContext);
1616

1717
if (!isLoaded) {
1818
return null;
@@ -23,8 +23,8 @@ export const MembershipPreview = withCardStateProvider((props: { organization: O
2323
organization,
2424
});
2525

26-
if (sessionTaskContext?.nextTask) {
27-
return sessionTaskContext?.nextTask();
26+
if (sessionTasksContext?.nextTask) {
27+
return sessionTasksContext?.nextTask();
2828
}
2929

3030
await navigateAfterSelectOrganization(organization);
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,97 @@
11
import { useClerk } from '@clerk/shared/react';
22
import { eventComponentMounted } from '@clerk/shared/telemetry';
33
import type { SessionTask } from '@clerk/types';
4-
import { useCallback, useEffect } from 'react';
4+
import { useCallback, useContext, useEffect } from 'react';
55

66
import { SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks';
7-
import { SessionTaskContext as SessionTaskContext } from '../../contexts/components/SessionTask';
7+
import { withRedirectToSignIn } from '../../../ui/common';
8+
import { SignInContext, SignUpContext } from '../../../ui/contexts';
9+
import { Card, LoadingCardContainer, withCardStateProvider } from '../../../ui/elements';
10+
import { SessionTasksContext as SessionTasksContext } from '../../contexts/components/SessionTasks';
811
import { Route, Switch, useRouter } from '../../router';
912
import { LazyOrganizationSelectionTask } from './lazyTasks';
13+
import { usePrefetchTasks } from './usePrefetchTasks';
1014
import { usePreloadTasks } from './usePreloadTasks';
1115

16+
const SessionTasksStart = withCardStateProvider(() => {
17+
const clerk = useClerk();
18+
const { navigate } = useRouter();
19+
20+
useEffect(() => {
21+
const timeoutId = setTimeout(() => {
22+
void clerk.session?.reload().then(session => {
23+
if (!session.currentTask?.key) {
24+
void navigate(clerk.buildAfterSignInUrl());
25+
return;
26+
}
27+
28+
void navigate(SESSION_TASK_ROUTE_BY_KEY[session.currentTask?.key]);
29+
});
30+
}, 1000);
31+
return () => clearTimeout(timeoutId);
32+
}, [navigate, clerk]);
33+
34+
return (
35+
<Card.Root>
36+
<Card.Content>
37+
<LoadingCardContainer />
38+
</Card.Content>
39+
<Card.Footer />
40+
</Card.Root>
41+
);
42+
});
43+
1244
function SessionTaskRoutes(): JSX.Element {
1345
return (
1446
<Switch>
1547
<Route path={SESSION_TASK_ROUTE_BY_KEY['org']}>
1648
<LazyOrganizationSelectionTask />
1749
</Route>
50+
<Route index>
51+
<SessionTasksStart />
52+
</Route>
1853
</Switch>
1954
);
2055
}
2156

2257
/**
2358
* @internal
2459
*/
25-
export function SessionTask({ redirectUrlComplete }: { redirectUrlComplete: string }): JSX.Element {
60+
function _SessionTask(): JSX.Element {
61+
usePrefetchTasks();
2662
usePreloadTasks();
2763

28-
const { __experimental_nextTask, session, telemetry } = useClerk();
64+
const clerk = useClerk();
2965
const { navigate } = useRouter();
66+
const signInContext = useContext(SignInContext);
67+
const signUpContext = useContext(SignUpContext);
3068

31-
const task = session?.currentTask;
69+
const redirectUrlComplete =
70+
signInContext?.afterSignInUrl ?? signUpContext?.afterSignUpUrl ?? clerk?.buildAfterSignInUrl();
3271

3372
useEffect(() => {
34-
if (task) {
35-
telemetry?.record(eventComponentMounted('SessionTask', { task: task.key }));
73+
const task = clerk.session?.currentTask;
74+
75+
if (!task) {
76+
void navigate(redirectUrlComplete);
3677
return;
3778
}
3879

39-
void navigate(redirectUrlComplete);
40-
}, [task, telemetry, navigate, redirectUrlComplete]);
80+
clerk.telemetry?.record(eventComponentMounted('SessionTask', { task: task.key }));
81+
}, [clerk, navigate, redirectUrlComplete]);
4182

4283
const nextTask = useCallback(
43-
() => __experimental_nextTask({ redirectUrlComplete }),
44-
[__experimental_nextTask, redirectUrlComplete],
84+
() => clerk.__experimental_nextTask({ redirectUrlComplete: redirectUrlComplete }),
85+
[clerk, redirectUrlComplete],
4586
);
4687

4788
return (
48-
<SessionTaskContext.Provider value={{ nextTask }}>
89+
<SessionTasksContext.Provider value={{ nextTask }}>
4990
<SessionTaskRoutes />
50-
</SessionTaskContext.Provider>
91+
</SessionTasksContext.Provider>
5192
);
5293
}
94+
95+
const SessionTask = withRedirectToSignIn(_SessionTask);
96+
97+
export { SessionTask };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { useOrganizationList } from '@clerk/shared/react/index';
2+
3+
import { useEnvironment } from '../../../ui/contexts';
4+
import { organizationListParams } from '../OrganizationSwitcher/utils';
5+
6+
/**
7+
* Prefetch tasks resources based on the environment settings
8+
* @internal
9+
*/
10+
export function usePrefetchTasks() {
11+
const { organizationSettings } = useEnvironment();
12+
13+
useOrganizationList({
14+
...(organizationSettings.forceOrganizationSelection ? organizationListParams : {}),
15+
});
16+
}

packages/clerk-js/src/ui/components/SignIn/SignIn.tsx

+2-2
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ function SignInRoutes(): JSX.Element {
130130
<LazySignUpVerifyPhone />
131131
</Route>
132132
<Route path='tasks'>
133-
<SessionTasks redirectUrlComplete={signInContext.afterSignInUrl} />
133+
<SessionTasks />
134134
</Route>
135135
<Route index>
136136
<LazySignUpContinue />
@@ -142,7 +142,7 @@ function SignInRoutes(): JSX.Element {
142142
</Route>
143143
)}
144144
<Route path='tasks'>
145-
<SessionTasks redirectUrlComplete={signInContext.afterSignInUrl} />
145+
<SessionTasks />
146146
</Route>
147147
<Route index>
148148
<SignInStart />

packages/clerk-js/src/ui/components/SignUp/SignUp.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ function SignUpRoutes(): JSX.Element {
8282
</Route>
8383
</Route>
8484
<Route path='tasks'>
85-
<SessionTasks redirectUrlComplete={signUpContext.afterSignUpUrl} />
85+
<SessionTasks />
8686
</Route>
8787
<Route index>
8888
<SignUpStart />

packages/clerk-js/src/ui/contexts/components/SessionTask.ts

-5
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { createContext } from 'react';
2+
3+
import type { SessionTasksCtx } from '../../types';
4+
5+
export const SessionTasksContext = createContext<SessionTasksCtx | null>(null);

packages/clerk-js/src/ui/lazyModules/components.ts

-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ export const PricingTable = lazy(() =>
9595
componentImportPaths.PricingTable().then(module => ({ default: module.__experimental_PricingTable })),
9696
);
9797

98-
export const preloadSessionTasks = () => import(/* webpackChunkName: "sessionTasks" */ '../components/SessionTasks');
9998
export const SessionTasks = lazy(() =>
10099
componentImportPaths.SessionTasks().then(module => ({ default: module.SessionTask })),
101100
);

packages/clerk-js/src/ui/types.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ export type __experimental_CheckoutCtx = __experimental_CheckoutProps & {
112112
setIsOpen?: (open: boolean) => void;
113113
};
114114

115-
export type SessionTaskCtx = {
116-
nextTask: () => void;
115+
export type SessionTasksCtx = {
116+
nextTask: () => Promise<void>;
117117
};
118118

119119
export type AvailableComponentCtx =

packages/clerk-js/src/utils/componentGuards.ts

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ export const sessionExistsAndSingleSessionModeEnabled: ComponentGuard = (clerk,
1010
return !!(clerk.session && environment?.authConfig.singleSessionMode);
1111
};
1212

13+
export const noSessionExists: ComponentGuard = clerk => {
14+
return !clerk.isSignedIn;
15+
};
16+
1317
export const noUserExists: ComponentGuard = clerk => {
1418
return !clerk.user;
1519
};

0 commit comments

Comments
 (0)