Skip to content

Commit e09b5e7

Browse files
committed
Abstract route paths resolution
Introduces a resourse class for tasks in order to resolve paths strings, avoiding placing that logic in the main `Clerk` interface which is more developer facing All methods are appended with `__internal` as it's not intented for public usage even by custom flows
1 parent fafd3b7 commit e09b5e7

File tree

14 files changed

+126
-76
lines changed

14 files changed

+126
-76
lines changed

integration/tests/session-tasks-sign-in.test.ts

+5-24
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect, test } from '@playwright/test';
1+
import { test } from '@playwright/test';
22

33
import type { Application } from '../models/application';
44
import { appConfigs } from '../presets';
@@ -29,31 +29,12 @@ test.describe('session tasks sign in flow @nextjs', () => {
2929
await app.teardown();
3030
});
3131

32-
test('on after sign-in, navigates to task', async ({ page, context }) => {
33-
const u = createTestUtils({ app, page, context });
34-
await u.po.signIn.goTo();
35-
await u.po.signIn.setIdentifier(fakeUser.email);
36-
await u.po.signIn.continue();
37-
await u.po.signIn.setPassword(fakeUser.password);
38-
await u.po.signIn.continue();
39-
await u.po.expect.toBeSignedIn();
40-
await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible();
41-
expect(u.page.url()).toContain('/sign-in/add-organization');
32+
test.fixme('on after sign-in, navigates to task', async () => {
33+
// todo
4234
});
4335

44-
test('redirects back to task when accessing root sign in component', async ({ page, context }) => {
45-
const u = createTestUtils({ app, page, context });
46-
await u.po.signIn.goTo();
47-
await u.po.signIn.setIdentifier(fakeUser.email);
48-
await u.po.signIn.continue();
49-
await u.po.signIn.setPassword(fakeUser.password);
50-
await u.po.signIn.continue();
51-
await u.po.expect.toBeSignedIn();
52-
await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible();
53-
expect(u.page.url()).toContain('/sign-in/add-organization');
54-
await u.po.signIn.goTo();
55-
await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible();
56-
expect(u.page.url()).toContain('/sign-in/add-organization');
36+
test.fixme('redirects back to task when accessing root sign in component', async () => {
37+
// todo
5738
});
5839

5940
test.fixme('redirects to after sign-in url when accessing root sign in component with a active session', {

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

+3-23
Original file line numberDiff line numberDiff line change
@@ -929,12 +929,12 @@ export class Clerk implements ClerkInterface {
929929
const hasSessionTaskToResolve = !!newSession?.tasks?.length;
930930
if (hasSessionTaskToResolve && this.#options.standardBrowser) {
931931
const hasEventHandler = eventBus.has(events.InternalComponentNavigate);
932-
if (hasEventHandler) {
932+
if (hasEventHandler && newSession) {
933933
await new Promise<void>(resolveNavigation =>
934-
eventBus.dispatch(events.InternalComponentNavigate, resolveNavigation),
934+
eventBus.dispatch(events.InternalComponentNavigate, { resolveNavigation, session: newSession }),
935935
);
936936
} else {
937-
await this.navigate(this.internal__buildSessionTaskUrl());
937+
await this.navigate(newSession?.currentTask?.__internal_getAbsoluteUrl(this.#options, this.environment));
938938
}
939939
}
940940

@@ -949,9 +949,7 @@ export class Clerk implements ClerkInterface {
949949
'Use the `redirectUrl` property instead. Example `Clerk.setActive({redirectUrl:"/"})`',
950950
);
951951
beforeUnloadTracker?.startTracking();
952-
953952
this.#setTransitiveState();
954-
955953
await beforeEmit(newSession);
956954
beforeUnloadTracker?.stopTracking();
957955
}
@@ -1157,24 +1155,6 @@ export class Clerk implements ClerkInterface {
11571155
return this.buildUrlWithAuth(this.environment.displayConfig.organizationProfileUrl);
11581156
}
11591157

1160-
// pass this to the session object as internal
1161-
public internal__buildSessionTaskUrl(): string {
1162-
const [currentTask] = this.session?.tasks ?? [];
1163-
1164-
if (!currentTask || !this.environment || !this.environment.displayConfig) {
1165-
return '';
1166-
}
1167-
1168-
const signInUrl = this.#options['signInUrl'] || this.environment.displayConfig.signInUrl;
1169-
const signUpUrl = this.#options['signUpUrl'] || this.environment.displayConfig.signUpUrl;
1170-
const isReferrerSignUpUrl = window.location.href.startsWith(signUpUrl);
1171-
1172-
return buildURL(
1173-
{ base: isReferrerSignUpUrl ? signUpUrl : signInUrl, hashPath: '/add-organization' },
1174-
{ stringify: true },
1175-
);
1176-
}
1177-
11781158
#redirectToSatellite = async (): Promise<unknown> => {
11791159
if (!inBrowser()) {
11801160
return;

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { SignUpModes } from '@clerk/types';
1+
import type { SessionTaskKey, SignUpModes } from '@clerk/types';
22

33
// TODO: Do we still have a use for this or can we simply preserve all params?
44
export const PRESERVED_QUERYSTRING_PARAMS = [
@@ -48,5 +48,10 @@ export const SIGN_UP_MODES: Record<string, SignUpModes> = {
4848
WAITLIST: 'waitlist',
4949
};
5050

51+
export const SESSION_TASK_ROUTE_PATH_BY_KEY: Record<SessionTaskKey, string> = {
52+
org: 'add-organization',
53+
};
54+
export const SESSION_TASK_ROUTE_PATHS = Object.values(SESSION_TASK_ROUTE_PATH_BY_KEY);
55+
5156
// This is the currently supported version of the Frontend API
5257
export const SUPPORTED_FAPI_VERSION = '2024-10-01';

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { TokenResource } from '@clerk/types';
1+
import type { SessionResource, TokenResource } from '@clerk/types';
22

33
export const events = {
44
TokenUpdate: 'token:update',
@@ -10,11 +10,12 @@ type ClerkEvent = (typeof events)[keyof typeof events];
1010
type EventHandler<E extends ClerkEvent> = (payload: EventPayload[E]) => void;
1111

1212
type TokenUpdatePayload = { token: TokenResource | null };
13+
type InternalComponentNavigatePayload = { resolveNavigation: () => void; session: SessionResource };
1314

1415
type EventPayload = {
1516
[events.TokenUpdate]: TokenUpdatePayload;
1617
[events.UserSignOut]: null;
17-
[events.InternalComponentNavigate]: () => void;
18+
[events.InternalComponentNavigate]: InternalComponentNavigatePayload;
1819
};
1920

2021
const createEventBus = () => {

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

+7-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import type {
1212
SessionJSONSnapshot,
1313
SessionResource,
1414
SessionStatus,
15-
SessionTask,
1615
SessionVerificationJSON,
1716
SessionVerificationResource,
1817
SessionVerifyAttemptFirstFactorParams,
@@ -29,6 +28,7 @@ import { clerkInvalidStrategy } from '../errors';
2928
import { eventBus, events } from '../events';
3029
import { SessionTokenCache } from '../tokenCache';
3130
import { BaseResource, PublicUserData, Token, User } from './internal';
31+
import { SessionTask } from './SessionTask';
3232
import { SessionVerification } from './SessionVerification';
3333

3434
export class Session extends BaseResource implements SessionResource {
@@ -226,7 +226,7 @@ export class Session extends BaseResource implements SessionResource {
226226
this.createdAt = unixEpochToDate(data.created_at);
227227
this.updatedAt = unixEpochToDate(data.updated_at);
228228
this.user = new User(data.user);
229-
this.tasks = data.tasks;
229+
this.tasks = data.tasks?.map(task => new SessionTask(task)) ?? [];
230230

231231
if (data.public_user_data) {
232232
this.publicUserData = new PublicUserData(data.public_user_data);
@@ -303,4 +303,9 @@ export class Session extends BaseResource implements SessionResource {
303303
return token.getRawString() || null;
304304
});
305305
}
306+
307+
get currentTask() {
308+
const [task] = this.tasks ?? [];
309+
return task;
310+
}
306311
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type {
2+
ClerkOptions,
3+
EnvironmentResource,
4+
SessionTaskJSON,
5+
SessionTaskJSONSnapshot,
6+
SessionTaskKey,
7+
SessionTaskResource,
8+
} from '@clerk/types';
9+
10+
import { buildURL, inBrowser } from '../../utils';
11+
12+
export const SESSION_TASK_PATHS = ['add-organization'] as const;
13+
type SessionTaskPath = (typeof SESSION_TASK_PATHS)[number];
14+
15+
export const SESSION_TASK_PATH_BY_KEY: Record<SessionTaskKey, SessionTaskPath> = {
16+
org: 'add-organization',
17+
} as const;
18+
19+
export class SessionTask implements SessionTaskResource {
20+
key!: SessionTaskKey;
21+
22+
constructor(data: SessionTaskJSON | SessionTaskJSONSnapshot) {
23+
this.fromJSON(data);
24+
}
25+
26+
protected fromJSON(data: SessionTaskJSON | SessionTaskJSONSnapshot): this {
27+
if (!data) {
28+
return this;
29+
}
30+
31+
this.key = data.key;
32+
33+
return this;
34+
}
35+
36+
public __internal_toSnapshot(): SessionTaskJSONSnapshot {
37+
return {
38+
key: this.key,
39+
};
40+
}
41+
42+
public __internal_getUrlPath(): `/${SessionTaskPath}` {
43+
return `/${SESSION_TASK_PATH_BY_KEY[this.key]}`;
44+
}
45+
46+
public __internal_getAbsoluteUrl(options: ClerkOptions, environment?: EnvironmentResource | null): string {
47+
if (!environment || !inBrowser()) {
48+
return '';
49+
}
50+
51+
const signInUrl = options['signInUrl'] || environment.displayConfig.signInUrl;
52+
const signUpUrl = options['signUpUrl'] || environment.displayConfig.signUpUrl;
53+
const isReferrerSignUpUrl = window.location.href.startsWith(signUpUrl);
54+
55+
return buildURL(
56+
// TODO - Introduce custom `tasksUrl` option to be used as a base path fallback for custom flows
57+
{ base: isReferrerSignUpUrl ? signUpUrl : signInUrl, hashPath: this.__internal_getUrlPath() },
58+
{ stringify: true },
59+
);
60+
}
61+
}

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

+1-4
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,6 @@ export function withRedirect<P extends AvailableComponentProps>(
3030

3131
const hasTaskAndSingleSessionMode = !!clerk.session?.tasks && environment?.authConfig.singleSessionMode;
3232
const shouldRedirect = hasTaskAndSingleSessionMode ? false : condition(clerk, environment, options);
33-
const redirectUrlWithDefault = hasTaskAndSingleSessionMode
34-
? () => clerk.internal__buildSessionTaskUrl()
35-
: redirectUrl;
3633

3734
React.useEffect(() => {
3835
if (shouldRedirect) {
@@ -41,7 +38,7 @@ export function withRedirect<P extends AvailableComponentProps>(
4138
}
4239
// TODO: Fix this properly
4340
// eslint-disable-next-line @typescript-eslint/no-floating-promises
44-
navigate(redirectUrlWithDefault({ clerk, environment, options }));
41+
navigate(redirectUrl({ clerk, environment, options }));
4542
}
4643
}, []);
4744

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useSessionContext } from '@clerk/shared/react/index';
2-
import type { SessionTask } from '@clerk/types';
2+
import type { SessionTaskKey } from '@clerk/types';
33
import { type ComponentType } from 'react';
44

55
import { OrganizationListContext } from '../../contexts';
@@ -8,7 +8,7 @@ import { OrganizationList } from '../OrganizationList';
88
/**
99
* @internal
1010
*/
11-
const SessionTaskRegistry: Record<SessionTask['key'], ComponentType> = {
11+
const SessionTaskRegistry: Record<SessionTaskKey, ComponentType> = {
1212
org: () => (
1313
// TODO - Hide personal workspace within organization list context based on environment
1414
<OrganizationListContext.Provider value={{ componentName: 'OrganizationList', hidePersonal: true }}>
@@ -20,7 +20,7 @@ const SessionTaskRegistry: Record<SessionTask['key'], ComponentType> = {
2020
/**
2121
* @internal
2222
*/
23-
export function Task(): React.ReactNode {
23+
export function SessionTask(): React.ReactNode {
2424
const session = useSessionContext();
2525
const [currentTask] = session?.tasks ?? [];
2626

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

+10-5
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { useClerk } from '@clerk/shared/react';
22
import type { SignInModalProps, SignInProps } from '@clerk/types';
33
import React from 'react';
44

5+
import { SESSION_TASK_ROUTE_PATHS } from '../../../core/constants';
56
import { normalizeRoutingOptions } from '../../../utils/normalizeRoutingOptions';
67
import { SignInEmailLinkFlowComplete, SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard';
78
import type { SignUpContextType } from '../../contexts';
@@ -14,7 +15,7 @@ import {
1415
} from '../../contexts';
1516
import { Flow } from '../../customizables';
1617
import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
17-
import { Task } from '../SessionTask/SessionTask';
18+
import { SessionTask } from '../SessionTask/SessionTask';
1819
import { SignUpContinue } from '../SignUp/SignUpContinue';
1920
import { SignUpSSOCallback } from '../SignUp/SignUpSSOCallback';
2021
import { SignUpStart } from '../SignUp/SignUpStart';
@@ -77,10 +78,14 @@ function SignInRoutes(): JSX.Element {
7778
redirectUrl='../factor-two'
7879
/>
7980
</Route>
80-
{/* Make it type safe based on the possible route paths / introduce abstraction */}
81-
<Route path='add-organization'>
82-
<Task />
83-
</Route>
81+
{SESSION_TASK_ROUTE_PATHS.map(path => (
82+
<Route
83+
path={path}
84+
key={path}
85+
>
86+
<SessionTask />
87+
</Route>
88+
))}
8489
{signInContext.isCombinedFlow && (
8590
<Route path='create'>
8691
<Route

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

+7-5
Original file line numberDiff line numberDiff line change
@@ -113,18 +113,20 @@ export const useSignInContext = (): SignInContextType => {
113113
const signUpContinueUrl = buildURL({ base: signUpUrl, hashPath: '/continue' }, { stringify: true });
114114

115115
useEffect(() => {
116-
eventBus.on(events.InternalComponentNavigate, async resolveNavigation => {
116+
eventBus.on(events.InternalComponentNavigate, ({ resolveNavigation, session }) => {
117+
if (!session.currentTask) {
118+
return;
119+
}
120+
117121
const tasksUrl = buildRedirectUrl({
118122
routing: ctx.routing,
119123
baseUrl: signInUrl,
120124
path: ctx.path,
121-
endpoint: '/add-organization',
125+
endpoint: session.currentTask.__internal_getUrlPath(),
122126
authQueryString: null,
123127
});
124128

125-
await navigate(tasksUrl);
126-
127-
resolveNavigation();
129+
void navigate(tasksUrl).then(resolveNavigation);
128130
});
129131
}, []);
130132

packages/types/src/clerk.ts

-2
Original file line numberDiff line numberDiff line change
@@ -476,8 +476,6 @@ export interface Clerk {
476476
*/
477477
buildWaitlistUrl(opts?: { initialValues?: Record<string, string> }): string;
478478

479-
internal__buildSessionTaskUrl(): string;
480-
481479
/**
482480
*
483481
* Redirects to the provided url after decorating it with the auth token for development instances.

packages/types/src/json.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type { OrganizationCustomRoleKey, OrganizationPermissionKey } from './org
1212
import type { OrganizationSettingsJSON } from './organizationSettings';
1313
import type { OrganizationSuggestionStatus } from './organizationSuggestion';
1414
import type { SamlIdpSlug } from './saml';
15-
import type { SessionStatus, SessionTask } from './session';
15+
import type { SessionStatus, SessionTaskKey } from './session';
1616
import type { SessionVerificationLevel, SessionVerificationStatus } from './sessionVerification';
1717
import type { SignInFirstFactor, SignInJSON, SignInSecondFactor } from './signIn';
1818
import type { SignUpField, SignUpIdentificationField, SignUpStatus } from './signUp';
@@ -101,6 +101,10 @@ export interface SignUpJSON extends ClerkResourceJSON {
101101
verifications: SignUpVerificationsJSON | null;
102102
}
103103

104+
export interface SessionTaskJSON {
105+
key: SessionTaskKey;
106+
}
107+
104108
export interface SessionJSON extends ClerkResourceJSON {
105109
object: 'session';
106110
id: string;
@@ -117,7 +121,7 @@ export interface SessionJSON extends ClerkResourceJSON {
117121
last_active_token: TokenJSON;
118122
last_active_organization_id: string | null;
119123
actor: ActJWTClaim | null;
120-
tasks: Array<SessionTask> | null;
124+
tasks: Array<SessionTaskJSON> | null;
121125
user: UserJSON;
122126
public_user_data: PublicUserDataJSON;
123127
created_at: number;

0 commit comments

Comments
 (0)