diff --git a/.changeset/whole-knives-attend.md b/.changeset/whole-knives-attend.md
new file mode 100644
index 00000000000..232c5603fc6
--- /dev/null
+++ b/.changeset/whole-knives-attend.md
@@ -0,0 +1,14 @@
+---
+'@clerk/clerk-js': patch
+'@clerk/types': patch
+---
+
+Add `taskUrls` option to customize task flow URLs:
+
+```tsx
+
+```
diff --git a/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap b/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap
index f24a109e99b..a36c2dbcd77 100644
--- a/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap
+++ b/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap
@@ -106,6 +106,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = `
"types/server-get-token.mdx",
"types/session-resource.mdx",
"types/session-status-claim.mdx",
+ "types/session-task.mdx",
"types/session-verification-level.mdx",
"types/session-verification-types.mdx",
"types/set-active-params.mdx",
diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts
index 1be58ac9236..035da4a48cb 100644
--- a/packages/clerk-js/src/core/__tests__/clerk.test.ts
+++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts
@@ -2331,7 +2331,7 @@ describe('Clerk singleton', () => {
});
});
- describe('nextTask', () => {
+ describe('navigateToTask', () => {
describe('with `pending` session status', () => {
const mockSession = {
id: '1',
@@ -2350,7 +2350,7 @@ describe('Clerk singleton', () => {
reload: jest.fn(() => Promise.resolve(mockSession)),
};
- beforeAll(() => {
+ beforeEach(() => {
mockResource.touch.mockReturnValueOnce(Promise.resolve());
mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockResource] }));
});
@@ -2360,7 +2360,7 @@ describe('Clerk singleton', () => {
mockResource.touch.mockReset();
});
- it('navigates to next task', async () => {
+ it('navigates to next task with default internal routing for AIOs', async () => {
const sut = new Clerk(productionPublishableKey);
await sut.load(mockedLoadOptions);
@@ -2369,6 +2369,21 @@ describe('Clerk singleton', () => {
expect(mockNavigate.mock.calls[0][0]).toBe('/sign-in#/tasks/add-organization');
});
+
+ it('navigates to next task with custom routing from clerk options', async () => {
+ const sut = new Clerk(productionPublishableKey);
+ await sut.load({
+ ...mockedLoadOptions,
+ taskUrls: {
+ org: '/onboarding/select-organization',
+ },
+ });
+
+ await sut.setActive({ session: mockResource as any as PendingSessionResource });
+ await sut.__internal_navigateToTaskIfAvailable();
+
+ expect(mockNavigate.mock.calls[0][0]).toBe('/onboarding/select-organization');
+ });
});
describe('with `active` session status', () => {
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 2439952460e..6389fb02bc7 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -1317,7 +1317,7 @@ export class Clerk implements ClerkInterface {
eventBus.emit(events.TokenUpdate, { token: null });
}
- // Only triggers navigation for internal AIO components routing
+ // Only triggers navigation for internal AIO components routing or custom URLs
const shouldNavigateOnSetActive = this.#componentNavigationContext;
if (newSession?.currentTask && shouldNavigateOnSetActive) {
await navigateToTask(session.currentTask.key, {
diff --git a/packages/clerk-js/src/core/sessionTasks.ts b/packages/clerk-js/src/core/sessionTasks.ts
index 1971d68e870..da44e2a5756 100644
--- a/packages/clerk-js/src/core/sessionTasks.ts
+++ b/packages/clerk-js/src/core/sessionTasks.ts
@@ -7,7 +7,7 @@ import type {
import { buildURL } from '../utils';
-export const SESSION_TASK_ROUTE_BY_KEY: Record = {
+export const INTERNAL_SESSION_TASK_ROUTE_BY_KEY: Record = {
org: 'add-organization',
} as const;
@@ -24,10 +24,10 @@ interface NavigateToTaskOptions {
* @internal
*/
export function navigateToTask(
- routeKey: keyof typeof SESSION_TASK_ROUTE_BY_KEY,
+ routeKey: keyof typeof INTERNAL_SESSION_TASK_ROUTE_BY_KEY,
{ componentNavigationContext, globalNavigate, options, environment }: NavigateToTaskOptions,
) {
- const taskRoute = `/tasks/${SESSION_TASK_ROUTE_BY_KEY[routeKey]}`;
+ const taskRoute = `/tasks/${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[routeKey]}`;
if (componentNavigationContext) {
return componentNavigationContext.navigate(componentNavigationContext.indexPath + taskRoute);
@@ -38,7 +38,6 @@ export function navigateToTask(
const isReferrerSignUpUrl = window.location.href.startsWith(signUpUrl);
const sessionTaskUrl = buildURL(
- // TODO - Accept custom URL option for custom flows in order to eject out of `signInUrl/signUpUrl`
{
base: isReferrerSignUpUrl ? signUpUrl : signInUrl,
hashPath: taskRoute,
@@ -46,5 +45,5 @@ export function navigateToTask(
{ stringify: true },
);
- return globalNavigate(sessionTaskUrl);
+ return globalNavigate(options.taskUrls?.[routeKey] ?? sessionTaskUrl);
}
diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts
index 59636a9bb64..23e805815e0 100644
--- a/packages/clerk-js/src/ui/common/redirects.ts
+++ b/packages/clerk-js/src/ui/common/redirects.ts
@@ -1,6 +1,6 @@
-import type { SessionTask } from '@clerk/types';
+import type { ClerkOptions, SessionTask } from '@clerk/types';
-import { SESSION_TASK_ROUTE_BY_KEY } from '../../core/sessionTasks';
+import { INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '../../core/sessionTasks';
import { buildURL } from '../../utils/url';
import type { SignInContextType, SignUpContextType, UserProfileContextType } from './../contexts';
@@ -33,21 +33,26 @@ export function buildSessionTaskRedirectUrl({
path,
baseUrl,
task,
+ taskUrls,
}: Pick & {
baseUrl: string;
task?: SessionTask;
+ taskUrls?: ClerkOptions['taskUrls'];
}) {
if (!task) {
return null;
}
- return buildRedirectUrl({
- routing,
- baseUrl,
- path,
- endpoint: `/tasks/${SESSION_TASK_ROUTE_BY_KEY[task.key]}`,
- authQueryString: null,
- });
+ return (
+ taskUrls?.[task.key] ??
+ buildRedirectUrl({
+ routing,
+ baseUrl,
+ path,
+ endpoint: `/tasks/${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[task.key]}`,
+ authQueryString: null,
+ })
+ );
}
export function buildSSOCallbackURL(
diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx
index 23627b0bfe5..20f4229594d 100644
--- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx
+++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx
@@ -6,7 +6,7 @@ import { Card } from '@/ui/elements/Card';
import { withCardStateProvider } from '@/ui/elements/contexts';
import { LoadingCardContainer } from '@/ui/elements/LoadingCard';
-import { SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks';
+import { INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks';
import { SignInContext, SignUpContext } from '../../../ui/contexts';
import { SessionTasksContext, useSessionTasksContext } from '../../contexts/components/SessionTasks';
import { Route, Switch, useRouter } from '../../router';
@@ -38,7 +38,7 @@ const SessionTasksStart = () => {
function SessionTaskRoutes(): JSX.Element {
return (
-
+
diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
index 67f6442bb3a..75aa4b4b1b5 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
@@ -126,6 +126,7 @@ export const useSignInContext = (): SignInContextType => {
path: ctx.path,
routing: ctx.routing,
baseUrl: signInUrl,
+ taskUrls: clerk.__internal_getOption('taskUrls'),
});
return {
diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
index 7632e782f2d..f91efbea7b1 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
@@ -121,6 +121,7 @@ export const useSignUpContext = (): SignUpContextType => {
path: ctx.path,
routing: ctx.routing,
baseUrl: signUpUrl,
+ taskUrls: clerk.__internal_getOption('taskUrls'),
});
return {
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 543f45d40a0..3abdb10a664 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -48,7 +48,7 @@ import type {
SignUpFallbackRedirectUrl,
SignUpForceRedirectUrl,
} from './redirects';
-import type { PendingSessionOptions, SignedInSessionResource } from './session';
+import type { PendingSessionOptions, SessionTask, SignedInSessionResource } from './session';
import type { SessionVerificationLevel } from './sessionVerification';
import type { SignInResource } from './signIn';
import type { SignUpResource } from './signUp';
@@ -1050,6 +1050,14 @@ export type ClerkOptions = PendingSessionOptions &
* @internal
*/
__internal_keyless_dismissPrompt?: (() => Promise) | null;
+
+ /**
+ * Customize the URL paths users are redirected to after sign-in or sign-up when specific
+ * session tasks need to be completed.
+ *
+ * @default undefined - Uses Clerk's default task flow URLs
+ */
+ taskUrls?: Record;
};
export interface NavigateOptions {
diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts
index a6adc59c33e..84ebb5eaf1d 100644
--- a/packages/types/src/session.ts
+++ b/packages/types/src/session.ts
@@ -326,7 +326,14 @@ export interface PublicUserData {
userId?: string;
}
+/**
+ * Represents a required action that a user must complete
+ * before their session becomes fully active
+ */
export interface SessionTask {
+ /**
+ * The unique identifier for the type of task that needs to be completed
+ */
key: 'org';
}