From c10c3e36e7235759046c9cd3d3330c90ae816b8b Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Tue, 4 Mar 2025 12:09:58 -0300
Subject: [PATCH 01/16] Add first draft of integration tests

---
 integration/.keys.json.sample                 |  4 ++
 integration/presets/envs.ts                   |  8 +++
 integration/presets/longRunningApps.ts        |  5 ++
 .../tests/session-tasks-sign-in.test.ts       | 70 +++++++++++++++++++
 4 files changed, 87 insertions(+)
 create mode 100644 integration/tests/session-tasks-sign-in.test.ts

diff --git a/integration/.keys.json.sample b/integration/.keys.json.sample
index caa70922c39..1cb3b47dc50 100644
--- a/integration/.keys.json.sample
+++ b/integration/.keys.json.sample
@@ -46,5 +46,9 @@
   "with-waitlist-mode": {
     "pk": "",
     "sk": ""
+  },
+ "with-session-tasks": {
+    "pk": "",
+    "sk": ""
   }
 }
diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts
index 4fbf4d2bb22..8254244547f 100644
--- a/integration/presets/envs.ts
+++ b/integration/presets/envs.ts
@@ -137,6 +137,13 @@ const withSignInOrUpwithRestrictedModeFlow = withEmailCodes
   .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-restricted-mode').pk)
   .setEnvVariable('public', 'CLERK_SIGN_UP_URL', undefined);
 
+const withSessionTasks = base
+  .clone()
+  .setId('withSessionTasks')
+  .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-session-tasks').sk)
+  .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks').pk)
+  .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key');
+
 export const envs = {
   base,
   withKeyless,
@@ -157,4 +164,5 @@ export const envs = {
   withSignInOrUpFlow,
   withSignInOrUpEmailLinksFlow,
   withSignInOrUpwithRestrictedModeFlow,
+  withSessionTasks,
 } as const;
diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts
index d5573f015e0..f595591a3c3 100644
--- a/integration/presets/longRunningApps.ts
+++ b/integration/presets/longRunningApps.ts
@@ -37,6 +37,11 @@ export const createLongRunningApps = () => {
       config: next.appRouter,
       env: envs.withSignInOrUpEmailLinksFlow,
     },
+    {
+      id: 'next.appRouter.withSessionTasks',
+      config: next.appRouter,
+      env: envs.withSessionTasks,
+    },
     { id: 'quickstart.next.appRouter', config: next.appRouterQuickstart, env: envs.withEmailCodesQuickstart },
     { id: 'elements.next.appRouter', config: elements.nextAppRouter, env: envs.withEmailCodes },
     { id: 'astro.node.withCustomRoles', config: astro.node, env: envs.withCustomRoles },
diff --git a/integration/tests/session-tasks-sign-in.test.ts b/integration/tests/session-tasks-sign-in.test.ts
new file mode 100644
index 00000000000..4cb25c09f96
--- /dev/null
+++ b/integration/tests/session-tasks-sign-in.test.ts
@@ -0,0 +1,70 @@
+import { expect, test } from '@playwright/test';
+
+import type { Application } from '../models/application';
+import { appConfigs } from '../presets';
+import type { FakeUser } from '../testUtils';
+import { createTestUtils } from '../testUtils';
+
+test.describe('session tasks sign in flow @nextjs', () => {
+  test.describe.configure({ mode: 'serial' });
+  let app: Application;
+  let fakeUser: FakeUser;
+
+  test.beforeAll(async () => {
+    app = await appConfigs.next.appRouter.clone().commit();
+    await app.setup();
+    await app.withEnv(appConfigs.envs.withSessionTasks);
+    await app.dev();
+
+    const m = createTestUtils({ app });
+    fakeUser = m.services.users.createFakeUser({
+      withPhoneNumber: true,
+      withUsername: true,
+    });
+    await m.services.users.createBapiUser(fakeUser);
+  });
+
+  test.afterAll(async () => {
+    await fakeUser.deleteIfExists();
+    await app.teardown();
+  });
+
+  test('on after sign-in, navigates to task', async ({ page, context }) => {
+    const u = createTestUtils({ app, page, context });
+    await u.po.signIn.goTo();
+    await u.po.signIn.setIdentifier(fakeUser.email);
+    await u.po.signIn.continue();
+    await u.po.signIn.setPassword(fakeUser.password);
+    await u.po.signIn.continue();
+    await u.po.expect.toBeSignedIn();
+    await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible();
+    expect(u.page.url()).toContain('/sign-in/add-organization');
+  });
+
+  test('redirects back to task when accessing root sign in component', async ({ page, context }) => {
+    const u = createTestUtils({ app, page, context });
+    await u.po.signIn.goTo();
+    await u.po.signIn.setIdentifier(fakeUser.email);
+    await u.po.signIn.continue();
+    await u.po.signIn.setPassword(fakeUser.password);
+    await u.po.signIn.continue();
+    await u.po.expect.toBeSignedIn();
+    await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible();
+    expect(u.page.url()).toContain('/sign-in/add-organization');
+    await u.po.signIn.goTo();
+    await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible();
+    expect(u.page.url()).toContain('/sign-in/add-organization');
+  });
+
+  test.fixme('redirects to after sign-in url when accessing root sign in component with a active session', {
+    // todo
+  });
+
+  test.fixme('redirects to after sign-in url once resolving task', () => {
+    // todo
+  });
+
+  test.fixme('without a session, does not allow to access task component', async () => {
+    // todo
+  });
+});

From 0fc67c8bdbdd39ccd872739f39e26bd8e5625824 Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Tue, 4 Mar 2025 13:24:09 -0300
Subject: [PATCH 02/16] Update `setActive` to handle `pending` session status

---
 packages/clerk-js/src/core/clerk.ts           | 36 +++++++++++++++++++
 .../clerk-js/src/ui/common/withRedirect.tsx   | 15 +++-----
 packages/types/src/clerk.ts                   |  2 ++
 3 files changed, 43 insertions(+), 10 deletions(-)

diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 454792854d8..bc569d6f92b 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -44,6 +44,7 @@ import type {
   OrganizationProfileProps,
   OrganizationResource,
   OrganizationSwitcherProps,
+  PendingSessionResource,
   PublicKeyCredentialCreationOptionsWithoutExtensions,
   PublicKeyCredentialRequestOptionsWithoutExtensions,
   PublicKeyCredentialWithAuthenticatorAssertionResponse,
@@ -199,6 +200,7 @@ export class Clerk implements ClerkInterface {
   #options: ClerkOptions = {};
   #pageLifecycle: ReturnType<typeof createPageLifecycle> | null = null;
   #touchThrottledUntil = 0;
+  #internalComponentNavigate: ((to: string) => Promise<unknown>) | null = null;
 
   public __internal_getCachedResources:
     | (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>)
@@ -436,6 +438,27 @@ export class Clerk implements ClerkInterface {
       await onAfterSetActive();
     };
 
+    #handlePendingSession = async (session: PendingSessionResource) => {
+      if (!session.currentTask || !this.environment) {
+        return;
+      }
+
+      if (session?.lastActiveToken) {
+        eventBus.dispatch(events.TokenUpdate, { token: session.lastActiveToken });
+      }
+
+      if (this.#internalComponentNavigate) {
+        // Handles navigation for UI components
+        await this.#internalComponentNavigate(session.currentTask.__internal_getPath());
+      } else {
+        // Handles navigation for custom flows
+        await this.navigate(session.currentTask.__internal_getUrl(this.#options, this.environment));
+      }
+
+      this.#setAccessors(session);
+      this.#emit();
+    };
+
     /**
      * Clears the router cache for `@clerk/nextjs` on all routes except the current one.
      * Note: Calling `onBeforeSetActive` before signing out, allows for new RSC prefetch requests to render as signed in.
@@ -956,6 +979,11 @@ export class Clerk implements ClerkInterface {
 
     let newSession = session === undefined ? this.session : session;
 
+    if (newSession?.status === 'pending') {
+      await this.#handlePendingSession(newSession);
+      return;
+    }
+
     // At this point, the `session` variable should contain either an `SignedInSessionResource`
     // ,`null` or `undefined`.
     // We now want to set the last active organization id on that session (if it exists).
@@ -1070,6 +1098,14 @@ export class Clerk implements ClerkInterface {
     return unsubscribe;
   };
 
+  public __internal_setComponentNavigate = (navigate: (to: string) => Promise<unknown>): UnsubscribeCallback => {
+    this.#internalComponentNavigate = navigate;
+    const unsubscribe = () => {
+      this.#internalComponentNavigate = null;
+    };
+    return unsubscribe;
+  };
+
   public navigate = async (to: string | undefined, options?: NavigateOptions): Promise<unknown> => {
     if (!to || !inBrowser()) {
       return;
diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx
index 43ec9b95172..80337f4567b 100644
--- a/packages/clerk-js/src/ui/common/withRedirect.tsx
+++ b/packages/clerk-js/src/ui/common/withRedirect.tsx
@@ -28,7 +28,10 @@ export function withRedirect<P extends AvailableComponentProps>(
     const environment = useEnvironment();
     const options = useOptions();
 
-    const shouldRedirect = condition(clerk, environment, options);
+    const hasTaskAndSingleSessionMode = !!clerk.session?.tasks && environment?.authConfig.singleSessionMode;
+    const shouldRedirect = hasTaskAndSingleSessionMode || condition(clerk, environment, options);
+    const redirectUrlWithDefault = hasTaskAndSingleSessionMode ? () => clerk.buildSessionTaskUrl() : redirectUrl;
+
     React.useEffect(() => {
       if (shouldRedirect) {
         if (warning && isDevelopmentFromPublishableKey(clerk.publishableKey)) {
@@ -36,7 +39,7 @@ export function withRedirect<P extends AvailableComponentProps>(
         }
         // TODO: Fix this properly
         // eslint-disable-next-line @typescript-eslint/no-floating-promises
-        navigate(redirectUrl({ clerk, environment, options }));
+        navigate(redirectUrlWithDefault({ clerk, environment, options }));
       }
     }, []);
 
@@ -89,11 +92,3 @@ export const withRedirectToAfterSignUp = <P extends AvailableComponentProps>(Com
 
   return HOC;
 };
-
-export const withRedirectToHomeSingleSessionGuard = <P extends AvailableComponentProps>(Component: ComponentType<P>) =>
-  withRedirect(
-    Component,
-    sessionExistsAndSingleSessionModeEnabled,
-    ({ environment }) => environment.displayConfig.homeUrl,
-    warnings.cannotRenderComponentWhenSessionExists,
-  );
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 29834688505..2e675fc4f52 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -500,6 +500,8 @@ export interface Clerk {
    */
   buildWaitlistUrl(opts?: { initialValues?: Record<string, string> }): string;
 
+  buildSessionTaskUrl(): string;
+
   /**
    *
    * Redirects to the provided url after decorating it with the auth token for development instances.

From a0fa5c2a1299b4d39160a36a673427449facc348 Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Tue, 4 Mar 2025 14:35:33 -0300
Subject: [PATCH 03/16] Handle internal component navigation

---
 .../tests/session-tasks-sign-in.test.ts       | 29 ++-------
 packages/clerk-js/src/core/clerk.ts           | 42 ++++++-------
 packages/clerk-js/src/core/events.ts          | 11 +++-
 .../clerk-js/src/core/resources/Session.ts    |  9 ++-
 .../src/core/resources/SessionTask.ts         | 61 +++++++++++++++++++
 packages/clerk-js/src/core/warnings.ts        |  4 ++
 .../clerk-js/src/ui/common/withRedirect.tsx   | 55 ++++++++++++++---
 .../ui/components/SessionTask/SessionTask.tsx | 34 +++++++++++
 .../src/ui/components/SessionTask/index.ts    |  1 +
 .../src/ui/components/SignIn/SignIn.tsx       | 32 +++++++++-
 .../src/ui/components/SignUp/SignUp.tsx       | 13 ++++
 .../src/ui/contexts/components/SignIn.ts      | 19 ++++++
 .../src/ui/contexts/components/SignUp.ts      | 19 ++++++
 .../src/ui/hooks/useNavigateOnEvent.ts        | 45 ++++++++++++++
 .../clerk-js/src/utils/componentGuards.ts     |  4 ++
 packages/types/src/clerk.ts                   |  2 -
 packages/types/src/json.ts                    |  8 ++-
 packages/types/src/session.ts                 | 14 ++++-
 packages/types/src/snapshots.ts               |  3 +
 19 files changed, 340 insertions(+), 65 deletions(-)
 create mode 100644 packages/clerk-js/src/core/resources/SessionTask.ts
 create mode 100644 packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx
 create mode 100644 packages/clerk-js/src/ui/components/SessionTask/index.ts
 create mode 100644 packages/clerk-js/src/ui/hooks/useNavigateOnEvent.ts

diff --git a/integration/tests/session-tasks-sign-in.test.ts b/integration/tests/session-tasks-sign-in.test.ts
index 4cb25c09f96..a4514c1ec2a 100644
--- a/integration/tests/session-tasks-sign-in.test.ts
+++ b/integration/tests/session-tasks-sign-in.test.ts
@@ -1,4 +1,4 @@
-import { expect, test } from '@playwright/test';
+import { test } from '@playwright/test';
 
 import type { Application } from '../models/application';
 import { appConfigs } from '../presets';
@@ -29,31 +29,12 @@ test.describe('session tasks sign in flow @nextjs', () => {
     await app.teardown();
   });
 
-  test('on after sign-in, navigates to task', async ({ page, context }) => {
-    const u = createTestUtils({ app, page, context });
-    await u.po.signIn.goTo();
-    await u.po.signIn.setIdentifier(fakeUser.email);
-    await u.po.signIn.continue();
-    await u.po.signIn.setPassword(fakeUser.password);
-    await u.po.signIn.continue();
-    await u.po.expect.toBeSignedIn();
-    await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible();
-    expect(u.page.url()).toContain('/sign-in/add-organization');
+  test.fixme('on after sign-in, navigates to task', async () => {
+    // todo
   });
 
-  test('redirects back to task when accessing root sign in component', async ({ page, context }) => {
-    const u = createTestUtils({ app, page, context });
-    await u.po.signIn.goTo();
-    await u.po.signIn.setIdentifier(fakeUser.email);
-    await u.po.signIn.continue();
-    await u.po.signIn.setPassword(fakeUser.password);
-    await u.po.signIn.continue();
-    await u.po.expect.toBeSignedIn();
-    await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible();
-    expect(u.page.url()).toContain('/sign-in/add-organization');
-    await u.po.signIn.goTo();
-    await expect(u.page.getByRole('heading', { name: 'Create Organization' })).toBeVisible();
-    expect(u.page.url()).toContain('/sign-in/add-organization');
+  test.fixme('redirects back to task when accessing root sign in component', async () => {
+    // todo
   });
 
   test.fixme('redirects to after sign-in url when accessing root sign in component with a active session', {
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index bc569d6f92b..a0c54ac931a 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -438,27 +438,6 @@ export class Clerk implements ClerkInterface {
       await onAfterSetActive();
     };
 
-    #handlePendingSession = async (session: PendingSessionResource) => {
-      if (!session.currentTask || !this.environment) {
-        return;
-      }
-
-      if (session?.lastActiveToken) {
-        eventBus.dispatch(events.TokenUpdate, { token: session.lastActiveToken });
-      }
-
-      if (this.#internalComponentNavigate) {
-        // Handles navigation for UI components
-        await this.#internalComponentNavigate(session.currentTask.__internal_getPath());
-      } else {
-        // Handles navigation for custom flows
-        await this.navigate(session.currentTask.__internal_getUrl(this.#options, this.environment));
-      }
-
-      this.#setAccessors(session);
-      this.#emit();
-    };
-
     /**
      * Clears the router cache for `@clerk/nextjs` on all routes except the current one.
      * Note: Calling `onBeforeSetActive` before signing out, allows for new RSC prefetch requests to render as signed in.
@@ -1071,6 +1050,27 @@ export class Clerk implements ClerkInterface {
     await onAfterSetActive();
   };
 
+  #handlePendingSession = async (session: PendingSessionResource) => {
+    if (!session.currentTask || !this.environment) {
+      return;
+    }
+
+    if (session?.lastActiveToken) {
+      eventBus.dispatch(events.TokenUpdate, { token: session.lastActiveToken });
+    }
+
+    if (this.#internalComponentNavigate) {
+      // Handles navigation for UI components
+      await this.#internalComponentNavigate(session.currentTask.__internal_getPath());
+    } else {
+      // Handles navigation for custom flows
+      await this.navigate(session.currentTask.__internal_getUrl(this.#options, this.environment));
+    }
+
+    this.#setAccessors(session);
+    this.#emit();
+  };
+
   public addListener = (listener: ListenerCallback): UnsubscribeCallback => {
     listener = memoizeListenerCallback(listener);
     this.#listeners.push(listener);
diff --git a/packages/clerk-js/src/core/events.ts b/packages/clerk-js/src/core/events.ts
index 7401dd91370..4982ed35da0 100644
--- a/packages/clerk-js/src/core/events.ts
+++ b/packages/clerk-js/src/core/events.ts
@@ -1,18 +1,21 @@
-import type { TokenResource } from '@clerk/types';
+import type { SessionResource, TokenResource } from '@clerk/types';
 
 export const events = {
   TokenUpdate: 'token:update',
   UserSignOut: 'user:signOut',
+  InternalComponentNavigate: 'task:internalNavigate',
 } as const;
 
 type ClerkEvent = (typeof events)[keyof typeof events];
 type EventHandler<E extends ClerkEvent> = (payload: EventPayload[E]) => void;
 
 type TokenUpdatePayload = { token: TokenResource | null };
+type InternalComponentNavigatePayload = { resolveNavigation: () => void; session: SessionResource };
 
 type EventPayload = {
   [events.TokenUpdate]: TokenUpdatePayload;
   [events.UserSignOut]: null;
+  [events.InternalComponentNavigate]: InternalComponentNavigatePayload;
 };
 
 const createEventBus = () => {
@@ -45,7 +48,11 @@ const createEventBus = () => {
     eventToHandlersMap.set(event, []);
   };
 
-  return { on, dispatch, off };
+  const has = <E extends ClerkEvent>(event: E) => {
+    return !!eventToHandlersMap.has(event);
+  };
+
+  return { on, dispatch, off, has };
 };
 
 export const eventBus = createEventBus();
diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts
index d3972d75327..5f530d76789 100644
--- a/packages/clerk-js/src/core/resources/Session.ts
+++ b/packages/clerk-js/src/core/resources/Session.ts
@@ -13,7 +13,6 @@ import type {
   SessionJSONSnapshot,
   SessionResource,
   SessionStatus,
-  SessionTask,
   SessionVerificationJSON,
   SessionVerificationResource,
   SessionVerifyAttemptFirstFactorParams,
@@ -35,6 +34,7 @@ import { clerkInvalidStrategy, clerkMissingWebAuthnPublicKeyOptions } from '../e
 import { eventBus, events } from '../events';
 import { SessionTokenCache } from '../tokenCache';
 import { BaseResource, PublicUserData, Token, User } from './internal';
+import { SessionTask } from './SessionTask';
 import { SessionVerification } from './SessionVerification';
 
 export class Session extends BaseResource implements SessionResource {
@@ -286,7 +286,7 @@ export class Session extends BaseResource implements SessionResource {
     this.createdAt = unixEpochToDate(data.created_at);
     this.updatedAt = unixEpochToDate(data.updated_at);
     this.user = new User(data.user);
-    this.tasks = data.tasks;
+    this.tasks = data.tasks?.map(task => new SessionTask(task)) ?? [];
 
     if (data.public_user_data) {
       this.publicUserData = new PublicUserData(data.public_user_data);
@@ -363,4 +363,9 @@ export class Session extends BaseResource implements SessionResource {
       return token.getRawString() || null;
     });
   }
+
+  get currentTask() {
+    const [task] = this.tasks ?? [];
+    return task;
+  }
 }
diff --git a/packages/clerk-js/src/core/resources/SessionTask.ts b/packages/clerk-js/src/core/resources/SessionTask.ts
new file mode 100644
index 00000000000..af4760ba012
--- /dev/null
+++ b/packages/clerk-js/src/core/resources/SessionTask.ts
@@ -0,0 +1,61 @@
+import type {
+  ClerkOptions,
+  EnvironmentResource,
+  SessionTaskJSON,
+  SessionTaskJSONSnapshot,
+  SessionTaskKey,
+  SessionTaskResource,
+} from '@clerk/types';
+
+import { buildURL, inBrowser } from '../../utils';
+
+export const SESSION_TASK_PATHS = ['add-organization'] as const;
+type SessionTaskPath = (typeof SESSION_TASK_PATHS)[number];
+
+export const SESSION_TASK_PATH_BY_KEY: Record<SessionTaskKey, SessionTaskPath> = {
+  org: 'add-organization',
+} as const;
+
+export class SessionTask implements SessionTaskResource {
+  key!: SessionTaskKey;
+
+  constructor(data: SessionTaskJSON | SessionTaskJSONSnapshot) {
+    this.fromJSON(data);
+  }
+
+  protected fromJSON(data: SessionTaskJSON | SessionTaskJSONSnapshot): this {
+    if (!data) {
+      return this;
+    }
+
+    this.key = data.key;
+
+    return this;
+  }
+
+  public __internal_toSnapshot(): SessionTaskJSONSnapshot {
+    return {
+      key: this.key,
+    };
+  }
+
+  public __internal_getUrlPath(): `/${SessionTaskPath}` {
+    return `/${SESSION_TASK_PATH_BY_KEY[this.key]}`;
+  }
+
+  public __internal_getAbsoluteUrl(options: ClerkOptions, environment?: EnvironmentResource | null): string {
+    if (!environment || !inBrowser()) {
+      return '';
+    }
+
+    const signInUrl = options['signInUrl'] || environment.displayConfig.signInUrl;
+    const signUpUrl = options['signUpUrl'] || environment.displayConfig.signUpUrl;
+    const isReferrerSignUpUrl = window.location.href.startsWith(signUpUrl);
+
+    return buildURL(
+      // TODO - Introduce custom `tasksUrl` option to be used as a base path fallback for custom flows
+      { base: isReferrerSignUpUrl ? signUpUrl : signInUrl, hashPath: this.__internal_getUrlPath() },
+      { stringify: true },
+    );
+  }
+}
diff --git a/packages/clerk-js/src/core/warnings.ts b/packages/clerk-js/src/core/warnings.ts
index 1fd500a1e91..a3eefbcd9f8 100644
--- a/packages/clerk-js/src/core/warnings.ts
+++ b/packages/clerk-js/src/core/warnings.ts
@@ -21,6 +21,10 @@ const warnings = {
   cannotRenderComponentWhenUserDoesNotExist:
     '<UserProfile/> cannot render unless a user is signed in. Since no user is signed in, this is no-op.',
   cannotRenderComponentWhenOrgDoesNotExist: `<OrganizationProfile/> cannot render unless an organization is active. Since no organization is currently active, this is no-op.`,
+  cannotRenderSessionTaskComponentOnSignIn:
+    'Cannot render component unless a session task exists. Clerk is redirecting to `signInUrl` instead.',
+  cannotRenderSessionTaskComponentOnSignUp:
+    'Cannot render component unless a session task exists. Clerk is redirecting to `signUpUrl` instead.',
   cannotRenderAnyOrganizationComponent: createMessageForDisabledOrganizations,
   cannotOpenUserProfile:
     'The UserProfile modal cannot render unless a user is signed in. Since no user is signed in, this is no-op.',
diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx
index 80337f4567b..85d73efb3e6 100644
--- a/packages/clerk-js/src/ui/common/withRedirect.tsx
+++ b/packages/clerk-js/src/ui/common/withRedirect.tsx
@@ -6,7 +6,7 @@ import React from 'react';
 
 import { warnings } from '../../core/warnings';
 import type { ComponentGuard } from '../../utils';
-import { sessionExistsAndSingleSessionModeEnabled } from '../../utils';
+import { noTaskExists, sessionExistsAndSingleSessionModeEnabled } from '../../utils';
 import { useEnvironment, useOptions, useSignInContext, useSignUpContext } from '../contexts';
 import { useRouter } from '../router';
 import type { AvailableComponentProps } from '../types';
@@ -28,10 +28,7 @@ export function withRedirect<P extends AvailableComponentProps>(
     const environment = useEnvironment();
     const options = useOptions();
 
-    const hasTaskAndSingleSessionMode = !!clerk.session?.tasks && environment?.authConfig.singleSessionMode;
-    const shouldRedirect = hasTaskAndSingleSessionMode || condition(clerk, environment, options);
-    const redirectUrlWithDefault = hasTaskAndSingleSessionMode ? () => clerk.buildSessionTaskUrl() : redirectUrl;
-
+    const shouldRedirect = condition(clerk, environment, options);
     React.useEffect(() => {
       if (shouldRedirect) {
         if (warning && isDevelopmentFromPublishableKey(clerk.publishableKey)) {
@@ -39,7 +36,7 @@ export function withRedirect<P extends AvailableComponentProps>(
         }
         // TODO: Fix this properly
         // eslint-disable-next-line @typescript-eslint/no-floating-promises
-        navigate(redirectUrlWithDefault({ clerk, environment, options }));
+        navigate(redirectUrl({ clerk, environment, options }));
       }
     }, []);
 
@@ -64,7 +61,7 @@ export const withRedirectToAfterSignIn = <P extends AvailableComponentProps>(Com
     return withRedirect(
       Component,
       sessionExistsAndSingleSessionModeEnabled,
-      ({ clerk }) => signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(),
+      ({ clerk }) => signInCtx.tasksUrl || signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(),
       warnings.cannotRenderSignInComponentWhenSessionExists,
     )(props);
   };
@@ -83,7 +80,7 @@ export const withRedirectToAfterSignUp = <P extends AvailableComponentProps>(Com
     return withRedirect(
       Component,
       sessionExistsAndSingleSessionModeEnabled,
-      ({ clerk }) => signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(),
+      ({ clerk }) => signUpCtx.tasksUrl || signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(),
       warnings.cannotRenderSignUpComponentWhenSessionExists,
     )(props);
   };
@@ -92,3 +89,45 @@ export const withRedirectToAfterSignUp = <P extends AvailableComponentProps>(Com
 
   return HOC;
 };
+
+export const withRedirectToSignUpIfNoTasksAvailable = <P extends AvailableComponentProps>(
+  Component: ComponentType<P>,
+) => {
+  const displayName = Component.displayName || Component.name || 'Component';
+  Component.displayName = displayName;
+
+  const HOC = (props: P) => {
+    const signUpCtx = useSignUpContext();
+    return withRedirect(
+      Component,
+      noTaskExists,
+      ({ clerk }) => signUpCtx.signUpUrl || clerk.buildSignUpUrl(),
+      warnings.cannotRenderSessionTaskComponentOnSignUp,
+    )(props);
+  };
+
+  HOC.displayName = `withRedirectToSignUpIfNoTasksAvailable(${displayName})`;
+
+  return HOC;
+};
+
+export const withRedirectToSignInIfNoTasksAvailable = <P extends AvailableComponentProps>(
+  Component: ComponentType<P>,
+) => {
+  const displayName = Component.displayName || Component.name || 'Component';
+  Component.displayName = displayName;
+
+  const HOC = (props: P) => {
+    const signInCtx = useSignInContext();
+    return withRedirect(
+      Component,
+      noTaskExists,
+      ({ clerk }) => signInCtx.signInUrl || clerk.buildSignInUrl(),
+      warnings.cannotRenderSessionTaskComponentOnSignUp,
+    )(props);
+  };
+
+  HOC.displayName = `withRedirectToSignInIfNoTasksAvailable(${displayName})`;
+
+  return HOC;
+};
diff --git a/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx b/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx
new file mode 100644
index 00000000000..1a74f54e1cd
--- /dev/null
+++ b/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx
@@ -0,0 +1,34 @@
+import { useSessionContext } from '@clerk/shared/react/index';
+import type { SessionTaskKey } from '@clerk/types';
+import { type ComponentType } from 'react';
+
+import { OrganizationListContext } from '../../contexts';
+import { OrganizationList } from '../OrganizationList';
+
+/**
+ * @internal
+ */
+const SessionTaskRegistry: Record<SessionTaskKey, ComponentType> = {
+  org: () => (
+    // TODO - Hide personal workspace within organization list context based on environment
+    <OrganizationListContext.Provider value={{ componentName: 'OrganizationList', hidePersonal: true }}>
+      <OrganizationList />
+    </OrganizationListContext.Provider>
+  ),
+};
+
+/**
+ * @internal
+ */
+export function SessionTask(): React.ReactNode {
+  const session = useSessionContext();
+  const [currentTask] = session?.tasks ?? [];
+
+  if (!currentTask) {
+    return null;
+  }
+
+  const Content = SessionTaskRegistry[currentTask.key];
+
+  return Content ? <Content /> : null;
+}
diff --git a/packages/clerk-js/src/ui/components/SessionTask/index.ts b/packages/clerk-js/src/ui/components/SessionTask/index.ts
new file mode 100644
index 00000000000..ddae613a6e2
--- /dev/null
+++ b/packages/clerk-js/src/ui/components/SessionTask/index.ts
@@ -0,0 +1 @@
+export * from './SessionTask';
diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
index c30b60ed5a0..e5c5a5263e5 100644
--- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
@@ -2,7 +2,9 @@ import { useClerk } from '@clerk/shared/react';
 import type { SignInModalProps, SignInProps } from '@clerk/types';
 import React from 'react';
 
+import { SESSION_TASK_PATHS, SessionTask } from '../../../core/resources/SessionTask';
 import { normalizeRoutingOptions } from '../../../utils/normalizeRoutingOptions';
+import { withRedirectToSignInIfNoTasksAvailable } from '../../common';
 import { SignInEmailLinkFlowComplete, SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard';
 import type { SignUpContextType } from '../../contexts';
 import {
@@ -80,7 +82,6 @@ function SignInRoutes(): JSX.Element {
             redirectUrl='../factor-two'
           />
         </Route>
-
         {signInContext.isCombinedFlow && (
           <Route path='create'>
             <Route
@@ -128,16 +129,43 @@ function SignInRoutes(): JSX.Element {
               >
                 <LazySignUpVerifyPhone />
               </Route>
+              {SESSION_TASK_PATHS.map(path => (
+                <Route
+                  path={path}
+                  key={path}
+                >
+                  <SignInSessionTask />
+                </Route>
+              ))}
               <Route index>
                 <LazySignUpContinue />
               </Route>
             </Route>
+
+            {SESSION_TASK_PATHS.map(path => (
+              <Route
+                path={path}
+                key={path}
+              >
+                <SignInSessionTask />
+              </Route>
+            ))}
+
             <Route index>
               <LazySignUpStart />
             </Route>
           </Route>
         )}
 
+        {SESSION_TASK_PATHS.map(path => (
+          <Route
+            path={path}
+            key={path}
+          >
+            <SignInSessionTask />
+          </Route>
+        ))}
+
         <Route index>
           <SignInStart />
         </Route>
@@ -209,3 +237,5 @@ export const SignInModal = (props: SignInModalProps): JSX.Element => {
     </Route>
   );
 };
+
+const SignInSessionTask = withRedirectToSignInIfNoTasksAvailable(SessionTask);
diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
index c96abac836b..12ca62f4948 100644
--- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
@@ -2,10 +2,13 @@ import { useClerk } from '@clerk/shared/react';
 import type { SignUpModalProps, SignUpProps } from '@clerk/types';
 import React from 'react';
 
+import { SESSION_TASK_PATHS } from '../../../core/resources/SessionTask';
+import { withRedirectToSignUpIfNoTasksAvailable } from '../../../ui/common';
 import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard';
 import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts';
 import { Flow } from '../../customizables';
 import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
+import { SessionTask } from '../SessionTask';
 import { SignUpContinue } from './SignUpContinue';
 import { SignUpSSOCallback } from './SignUpSSOCallback';
 import { SignUpStart } from './SignUpStart';
@@ -74,6 +77,14 @@ function SignUpRoutes(): JSX.Element {
             <SignUpContinue />
           </Route>
         </Route>
+        {SESSION_TASK_PATHS.map(path => (
+          <Route
+            path={path}
+            key={path}
+          >
+            <SignUpSessionTask />
+          </Route>
+        ))}
         <Route index>
           <SignUpStart />
         </Route>
@@ -118,4 +129,6 @@ export const SignUpModal = (props: SignUpModalProps): JSX.Element => {
   );
 };
 
+const SignUpSessionTask = withRedirectToSignUpIfNoTasksAvailable(SessionTask);
+
 export { SignUpContinue, SignUpSSOCallback, SignUpStart, SignUpVerifyEmail, SignUpVerifyPhone };
diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
index 564f755992c..48018f1b574 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
@@ -7,6 +7,7 @@ import { buildURL } from '../../../utils';
 import { RedirectUrls } from '../../../utils/redirectUrls';
 import { buildRedirectUrl, MAGIC_LINK_VERIFY_PATH_ROUTE, SSO_CALLBACK_PATH_ROUTE } from '../../common/redirects';
 import { useEnvironment, useOptions } from '../../contexts';
+import { useNavigateOnEvent } from '../../hooks/useNavigateOnEvent';
 import type { ParsedQueryString } from '../../router';
 import { useRouter } from '../../router';
 import type { SignInCtx } from '../../types';
@@ -21,6 +22,7 @@ export type SignInContextType = SignInCtx & {
   authQueryString: string | null;
   afterSignUpUrl: string;
   afterSignInUrl: string;
+  tasksUrl: string | null;
   transferable: boolean;
   waitlistUrl: string;
   emailLinkRedirectUrl: string;
@@ -112,6 +114,22 @@ export const useSignInContext = (): SignInContextType => {
 
   const signUpContinueUrl = buildURL({ base: signUpUrl, hashPath: '/continue' }, { stringify: true });
 
+  const tasksUrl = clerk.session?.currentTask
+    ? buildRedirectUrl({
+        routing: ctx.routing,
+        baseUrl: signInUrl,
+        path: ctx.path,
+        endpoint: clerk.session?.currentTask?.__internal_getUrlPath(),
+        authQueryString: null,
+      })
+    : null;
+
+  useNavigateOnEvent({
+    routing: ctx.routing,
+    baseUrl: signInUrl,
+    path: ctx.path,
+  });
+
   return {
     ...(ctx as SignInCtx),
     transferable: ctx.transferable ?? true,
@@ -123,6 +141,7 @@ export const useSignInContext = (): SignInContextType => {
     afterSignUpUrl,
     emailLinkRedirectUrl,
     ssoCallbackUrl,
+    tasksUrl,
     navigateAfterSignIn,
     signUpContinueUrl,
     queryParams,
diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
index 32b4e4794c0..4239965ecde 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
@@ -7,6 +7,7 @@ import { buildURL } from '../../../utils';
 import { RedirectUrls } from '../../../utils/redirectUrls';
 import { buildRedirectUrl, MAGIC_LINK_VERIFY_PATH_ROUTE, SSO_CALLBACK_PATH_ROUTE } from '../../common/redirects';
 import { useEnvironment, useOptions } from '../../contexts';
+import { useNavigateOnEvent } from '../../hooks/useNavigateOnEvent';
 import type { ParsedQueryString } from '../../router';
 import { useRouter } from '../../router';
 import type { SignUpCtx } from '../../types';
@@ -22,6 +23,7 @@ export type SignUpContextType = SignUpCtx & {
   afterSignUpUrl: string;
   afterSignInUrl: string;
   waitlistUrl: string;
+  tasksUrl: string | null;
   isCombinedFlow: boolean;
   emailLinkRedirectUrl: string;
   ssoCallbackUrl: string;
@@ -107,6 +109,22 @@ export const useSignUpContext = (): SignUpContextType => {
   // TODO: Avoid building this url again to remove duplicate code. Get it from window.Clerk instead.
   const secondFactorUrl = buildURL({ base: signInUrl, hashPath: '/factor-two' }, { stringify: true });
 
+  const tasksUrl = clerk.session?.currentTask
+    ? buildRedirectUrl({
+        routing: ctx.routing,
+        baseUrl: signUpUrl,
+        path: ctx.path,
+        endpoint: clerk.session?.currentTask?.__internal_getUrlPath(),
+        authQueryString: null,
+      })
+    : null;
+
+  useNavigateOnEvent({
+    routing: ctx.routing,
+    baseUrl: signUpUrl,
+    path: ctx.path,
+  });
+
   return {
     ...ctx,
     componentName,
@@ -118,6 +136,7 @@ export const useSignUpContext = (): SignUpContextType => {
     afterSignInUrl,
     emailLinkRedirectUrl,
     ssoCallbackUrl,
+    tasksUrl,
     navigateAfterSignUp,
     queryParams,
     initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams },
diff --git a/packages/clerk-js/src/ui/hooks/useNavigateOnEvent.ts b/packages/clerk-js/src/ui/hooks/useNavigateOnEvent.ts
new file mode 100644
index 00000000000..09e1e37f5f1
--- /dev/null
+++ b/packages/clerk-js/src/ui/hooks/useNavigateOnEvent.ts
@@ -0,0 +1,45 @@
+import type { SessionResource } from '@clerk/types';
+import { useEffect } from 'react';
+
+import { eventBus, events } from '../../core/events';
+import { buildRedirectUrl } from '../common';
+import { useRouter } from '../router';
+
+type UseNavigateOnEventOptions = Pick<Parameters<typeof buildRedirectUrl>[0], 'routing' | 'baseUrl' | 'path'>;
+
+/**
+ * Custom hook to trigger internal component navigation by a event.
+ */
+export const useNavigateOnEvent = ({ routing, baseUrl, path }: UseNavigateOnEventOptions) => {
+  const { navigate } = useRouter();
+
+  useEffect(() => {
+    const handleNavigation = ({
+      resolveNavigation,
+      session,
+    }: {
+      resolveNavigation: () => void;
+      session: SessionResource;
+    }) => {
+      if (!session.currentTask) {
+        return;
+      }
+
+      void navigate(
+        buildRedirectUrl({
+          routing,
+          baseUrl,
+          path,
+          endpoint: session.currentTask.__internal_getUrlPath(),
+          authQueryString: null,
+        }),
+      ).then(resolveNavigation);
+    };
+
+    eventBus.on(events.InternalComponentNavigate, handleNavigation);
+
+    return () => {
+      eventBus.off(events.InternalComponentNavigate, handleNavigation);
+    };
+  }, []);
+};
diff --git a/packages/clerk-js/src/utils/componentGuards.ts b/packages/clerk-js/src/utils/componentGuards.ts
index f0084a6a47d..07b1c08e751 100644
--- a/packages/clerk-js/src/utils/componentGuards.ts
+++ b/packages/clerk-js/src/utils/componentGuards.ts
@@ -14,6 +14,10 @@ export const noUserExists: ComponentGuard = clerk => {
   return !clerk.user;
 };
 
+export const noTaskExists: ComponentGuard = clerk => {
+  return !clerk.session?.currentTask;
+};
+
 export const noOrganizationExists: ComponentGuard = clerk => {
   return !clerk.organization;
 };
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 2e675fc4f52..29834688505 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -500,8 +500,6 @@ export interface Clerk {
    */
   buildWaitlistUrl(opts?: { initialValues?: Record<string, string> }): string;
 
-  buildSessionTaskUrl(): string;
-
   /**
    *
    * Redirects to the provided url after decorating it with the auth token for development instances.
diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts
index 21f378f0cf3..afab7bcf0f6 100644
--- a/packages/types/src/json.ts
+++ b/packages/types/src/json.ts
@@ -13,7 +13,7 @@ import type { OrganizationCustomRoleKey, OrganizationPermissionKey } from './org
 import type { OrganizationSettingsJSON } from './organizationSettings';
 import type { OrganizationSuggestionStatus } from './organizationSuggestion';
 import type { SamlIdpSlug } from './saml';
-import type { SessionStatus, SessionTask } from './session';
+import type { SessionStatus, SessionTaskKey } from './session';
 import type { SessionVerificationLevel, SessionVerificationStatus } from './sessionVerification';
 import type { SignInFirstFactor, SignInJSON, SignInSecondFactor } from './signIn';
 import type { SignUpField, SignUpIdentificationField, SignUpStatus } from './signUp';
@@ -103,6 +103,10 @@ export interface SignUpJSON extends ClerkResourceJSON {
   verifications: SignUpVerificationsJSON | null;
 }
 
+export interface SessionTaskJSON {
+  key: SessionTaskKey;
+}
+
 export interface SessionJSON extends ClerkResourceJSON {
   object: 'session';
   id: string;
@@ -119,7 +123,7 @@ export interface SessionJSON extends ClerkResourceJSON {
   last_active_token: TokenJSON;
   last_active_organization_id: string | null;
   actor: ActJWTClaim | null;
-  tasks: Array<SessionTask> | null;
+  tasks: Array<SessionTaskJSON> | null;
   user: UserJSON;
   public_user_data: PublicUserDataJSON;
   created_at: number;
diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts
index 6f40a118250..5b8a92677e6 100644
--- a/packages/types/src/session.ts
+++ b/packages/types/src/session.ts
@@ -1,3 +1,6 @@
+import type { ClerkOptions } from 'clerk';
+
+import type { EnvironmentResource } from './environment';
 import type {
   BackupCodeAttempt,
   EmailCodeAttempt,
@@ -123,7 +126,8 @@ export interface SessionResource extends ClerkResource {
   lastActiveOrganizationId: string | null;
   lastActiveAt: Date;
   actor: ActJWTClaim | null;
-  tasks: Array<SessionTask> | null;
+  tasks: Array<SessionTaskResource> | null;
+  currentTask?: SessionTaskResource;
   /**
    * The user associated with the session.
    */
@@ -223,8 +227,12 @@ export interface PublicUserData {
   userId?: string;
 }
 
-export interface SessionTask {
-  key: 'orgs';
+export type SessionTaskKey = 'org';
+
+export interface SessionTaskResource {
+  key: SessionTaskKey;
+  __internal_getUrlPath: () => string;
+  __internal_getAbsoluteUrl: (options: ClerkOptions, environment?: EnvironmentResource | null) => string;
 }
 
 export type GetTokenOptions = {
diff --git a/packages/types/src/snapshots.ts b/packages/types/src/snapshots.ts
index bc398bb633e..babd482803c 100644
--- a/packages/types/src/snapshots.ts
+++ b/packages/types/src/snapshots.ts
@@ -19,6 +19,7 @@ import type {
   SamlAccountConnectionJSON,
   SamlAccountJSON,
   SessionJSON,
+  SessionTaskJSON,
   SignUpJSON,
   SignUpVerificationJSON,
   SignUpVerificationsJSON,
@@ -94,6 +95,8 @@ export type SessionJSONSnapshot = Override<
   }
 >;
 
+export type SessionTaskJSONSnapshot = SessionTaskJSON;
+
 export type SignUpJSONSnapshot = Override<
   Nullable<SignUpJSON, 'status'>,
   {

From 18c7cd8b6842bc2b3ba9bd8330e87ce98988de05 Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Wed, 5 Mar 2025 14:53:12 -0300
Subject: [PATCH 04/16] Introduce test coverage

---
 .changeset/brave-pears-add.md                 |   6 +
 .../tests/session-tasks-sign-in.test.ts       |  38 +-
 .../tests/session-tasks-sign-up.test.ts       |  40 ++
 .../clerk-js/src/core/__tests__/clerk.test.ts | 598 ++++++++++--------
 packages/clerk-js/src/core/clerk.ts           |  51 +-
 packages/clerk-js/src/core/events.ts          |  11 +-
 .../clerk-js/src/core/resources/Session.ts    |   4 +-
 .../src/core/resources/SessionTask.ts         |  61 --
 packages/clerk-js/src/core/sessionTasks.ts    |  46 ++
 packages/clerk-js/src/core/warnings.ts        |   4 -
 packages/clerk-js/src/ui/common/redirects.ts  |  19 +
 .../clerk-js/src/ui/common/withRedirect.tsx   |  48 +-
 .../ui/components/SessionTask/SessionTask.tsx |  19 +-
 .../src/ui/components/SignIn/SignIn.tsx       |  41 +-
 .../src/ui/components/SignIn/lazy-sign-up.ts  |   2 +
 .../src/ui/components/SignUp/SignUp.tsx       |  17 +-
 .../src/ui/contexts/components/SignIn.ts      |  36 +-
 .../src/ui/contexts/components/SignUp.ts      |  36 +-
 .../src/ui/hooks/useNavigateOnEvent.ts        |  45 --
 .../clerk-js/src/utils/componentGuards.ts     |   4 -
 packages/react/src/isomorphicClerk.ts         |   1 +
 packages/types/src/clerk.ts                   |   7 +
 packages/types/src/json.ts                    |   8 +-
 packages/types/src/session.ts                 |  16 +-
 packages/types/src/snapshots.ts               |   3 -
 25 files changed, 574 insertions(+), 587 deletions(-)
 create mode 100644 .changeset/brave-pears-add.md
 create mode 100644 integration/tests/session-tasks-sign-up.test.ts
 delete mode 100644 packages/clerk-js/src/core/resources/SessionTask.ts
 create mode 100644 packages/clerk-js/src/core/sessionTasks.ts
 delete mode 100644 packages/clerk-js/src/ui/hooks/useNavigateOnEvent.ts

diff --git a/.changeset/brave-pears-add.md b/.changeset/brave-pears-add.md
new file mode 100644
index 00000000000..ce2ecccbfc2
--- /dev/null
+++ b/.changeset/brave-pears-add.md
@@ -0,0 +1,6 @@
+---
+'@clerk/clerk-js': minor
+'@clerk/types': minor
+---
+
+Navigate to tasks on after sign-in/sign-up
diff --git a/integration/tests/session-tasks-sign-in.test.ts b/integration/tests/session-tasks-sign-in.test.ts
index a4514c1ec2a..8e4ae9e7a2b 100644
--- a/integration/tests/session-tasks-sign-in.test.ts
+++ b/integration/tests/session-tasks-sign-in.test.ts
@@ -1,11 +1,11 @@
-import { test } from '@playwright/test';
+import { expect, test } from '@playwright/test';
 
 import type { Application } from '../models/application';
 import { appConfigs } from '../presets';
 import type { FakeUser } from '../testUtils';
 import { createTestUtils } from '../testUtils';
 
-test.describe('session tasks sign in flow @nextjs', () => {
+test.describe('session tasks after sign-in flow @nextjs', () => {
   test.describe.configure({ mode: 'serial' });
   let app: Application;
   let fakeUser: FakeUser;
@@ -17,10 +17,7 @@ test.describe('session tasks sign in flow @nextjs', () => {
     await app.dev();
 
     const m = createTestUtils({ app });
-    fakeUser = m.services.users.createFakeUser({
-      withPhoneNumber: true,
-      withUsername: true,
-    });
+    fakeUser = m.services.users.createFakeUser();
     await m.services.users.createBapiUser(fakeUser);
   });
 
@@ -29,23 +26,16 @@ test.describe('session tasks sign in flow @nextjs', () => {
     await app.teardown();
   });
 
-  test.fixme('on after sign-in, navigates to task', async () => {
-    // todo
-  });
-
-  test.fixme('redirects back to task when accessing root sign in component', async () => {
-    // todo
-  });
-
-  test.fixme('redirects to after sign-in url when accessing root sign in component with a active session', {
-    // todo
-  });
-
-  test.fixme('redirects to after sign-in url once resolving task', () => {
-    // todo
-  });
-
-  test.fixme('without a session, does not allow to access task component', async () => {
-    // todo
+  test('navigate to task on after sign-in', async ({ page, context }) => {
+    const u = createTestUtils({ app, page, context });
+    await u.po.signIn.goTo();
+    await u.po.signIn.setIdentifier(fakeUser.email);
+    await u.po.signIn.continue();
+    await u.po.signIn.setPassword(fakeUser.password);
+    await u.po.signIn.continue();
+    await u.po.expect.toBeSignedIn();
+
+    await expect(u.page.getByRole('button', { name: /create organization/i })).toBeVisible();
+    expect(page.url()).toContain('add-organization');
   });
 });
diff --git a/integration/tests/session-tasks-sign-up.test.ts b/integration/tests/session-tasks-sign-up.test.ts
new file mode 100644
index 00000000000..6270b8967a1
--- /dev/null
+++ b/integration/tests/session-tasks-sign-up.test.ts
@@ -0,0 +1,40 @@
+import { expect, test } from '@playwright/test';
+
+import type { Application } from '../models/application';
+import { appConfigs } from '../presets';
+import { createTestUtils } from '../testUtils';
+
+test.describe('session tasks after sign-up flow @nextjs', () => {
+  test.describe.configure({ mode: 'serial' });
+  let app: Application;
+
+  test.beforeAll(async () => {
+    app = await appConfigs.next.appRouter.clone().commit();
+    await app.setup();
+    await app.withEnv(appConfigs.envs.withSessionTasks);
+    await app.dev();
+  });
+
+  test.afterAll(async () => {
+    await app.teardown();
+  });
+
+  test('navigate to task on after sign-up', async ({ page, context }) => {
+    const u = createTestUtils({ app, page, context });
+    const fakeUser = u.services.users.createFakeUser({
+      fictionalEmail: true,
+      withPhoneNumber: true,
+      withUsername: true,
+    });
+    await u.po.signUp.goTo();
+    await u.po.signUp.signUpWithEmailAndPassword({
+      email: fakeUser.email,
+      password: fakeUser.password,
+    });
+
+    await expect(u.page.getByRole('button', { name: /create organization/i })).toBeVisible();
+    expect(page.url()).toContain('add-organization');
+
+    await fakeUser.deleteIfExists();
+  });
+});
diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts
index 48f0a85a291..ea573685bd0 100644
--- a/packages/clerk-js/src/core/__tests__/clerk.test.ts
+++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts
@@ -155,163 +155,302 @@ describe('Clerk singleton', () => {
   });
 
   describe('.setActive', () => {
-    describe.each(['active', 'pending'] satisfies Array<SignedInSessionResource['status']>)(
-      'when session has %s status',
-      status => {
-        const mockSession = {
-          id: '1',
-          remove: jest.fn(),
-          status,
-          user: {},
-          touch: jest.fn(() => Promise.resolve()),
-          getToken: jest.fn(),
-          lastActiveToken: { getRawString: () => 'mocked-token' },
-        };
-        let eventBusSpy;
+    describe('with `active` session status', () => {
+      const mockSession = {
+        id: '1',
+        remove: jest.fn(),
+        status: 'active',
+        user: {},
+        touch: jest.fn(() => Promise.resolve()),
+        getToken: jest.fn(),
+        lastActiveToken: { getRawString: () => 'mocked-token' },
+      };
+      let eventBusSpy;
 
-        beforeEach(() => {
-          eventBusSpy = jest.spyOn(eventBus, 'dispatch');
-        });
+      beforeEach(() => {
+        eventBusSpy = jest.spyOn(eventBus, 'dispatch');
+      });
 
-        afterEach(() => {
-          mockSession.remove.mockReset();
-          mockSession.touch.mockReset();
+      afterEach(() => {
+        mockSession.remove.mockReset();
+        mockSession.touch.mockReset();
 
-          eventBusSpy?.mockRestore();
-          // cleanup global window pollution
-          (window as any).__unstable__onBeforeSetActive = null;
-          (window as any).__unstable__onAfterSetActive = null;
+        eventBusSpy?.mockRestore();
+        // cleanup global window pollution
+        (window as any).__unstable__onBeforeSetActive = null;
+        (window as any).__unstable__onAfterSetActive = null;
+      });
+
+      it('does not call session touch on signOut', async () => {
+        mockSession.touch.mockReturnValueOnce(Promise.resolve());
+        mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
+
+        const sut = new Clerk(productionPublishableKey);
+        await sut.load();
+        await sut.setActive({ session: null });
+        await waitFor(() => {
+          expect(mockSession.touch).not.toHaveBeenCalled();
+          expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: null });
         });
+      });
 
-        it('does not call session touch on signOut', async () => {
-          mockSession.touch.mockReturnValueOnce(Promise.resolve());
-          mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
+      it('calls session.touch by default', async () => {
+        mockSession.touch.mockReturnValue(Promise.resolve());
+        mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
 
-          const sut = new Clerk(productionPublishableKey);
-          await sut.load();
-          await sut.setActive({ session: null });
-          await waitFor(() => {
-            expect(mockSession.touch).not.toHaveBeenCalled();
-            expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: null });
-          });
+        const sut = new Clerk(productionPublishableKey);
+        await sut.load();
+        await sut.setActive({ session: mockSession as any as ActiveSessionResource });
+        expect(mockSession.touch).toHaveBeenCalled();
+      });
+
+      it('does not call session.touch if Clerk was initialised with touchSession set to false', async () => {
+        mockSession.touch.mockReturnValueOnce(Promise.resolve());
+        mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
+        mockSession.getToken.mockResolvedValue('mocked-token');
+
+        const sut = new Clerk(productionPublishableKey);
+        await sut.load({ touchSession: false });
+        await sut.setActive({ session: mockSession as any as ActiveSessionResource });
+        await waitFor(() => {
+          expect(mockSession.touch).not.toHaveBeenCalled();
+          expect(mockSession.getToken).toHaveBeenCalled();
         });
+      });
 
-        it('calls session.touch by default', async () => {
-          mockSession.touch.mockReturnValue(Promise.resolve());
-          mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
+      it('calls __unstable__onBeforeSetActive before session.touch', async () => {
+        mockSession.touch.mockReturnValueOnce(Promise.resolve());
+        mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
 
-          const sut = new Clerk(productionPublishableKey);
-          await sut.load();
-          await sut.setActive({ session: mockSession as any as ActiveSessionResource });
+        (window as any).__unstable__onBeforeSetActive = () => {
+          expect(mockSession.touch).not.toHaveBeenCalled();
+        };
+
+        const sut = new Clerk(productionPublishableKey);
+        await sut.load();
+        await sut.setActive({ session: mockSession as any as ActiveSessionResource });
+        expect(mockSession.touch).toHaveBeenCalled();
+      });
+
+      it('sets __session and __client_uat cookie before calling __unstable__onBeforeSetActive', async () => {
+        mockSession.touch.mockReturnValueOnce(Promise.resolve());
+        mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
+
+        (window as any).__unstable__onBeforeSetActive = () => {
+          expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: mockSession.lastActiveToken });
+        };
+
+        const sut = new Clerk(productionPublishableKey);
+        await sut.load();
+        await sut.setActive({ session: mockSession as any as ActiveSessionResource });
+      });
+
+      it('calls __unstable__onAfterSetActive after beforeEmit and session.touch', async () => {
+        const beforeEmitMock = jest.fn();
+        mockSession.touch.mockReturnValueOnce(Promise.resolve());
+        mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
+
+        (window as any).__unstable__onAfterSetActive = () => {
           expect(mockSession.touch).toHaveBeenCalled();
-        });
+          expect(beforeEmitMock).toHaveBeenCalled();
+        };
 
-        it('does not call session.touch if Clerk was initialised with touchSession set to false', async () => {
-          mockSession.touch.mockReturnValueOnce(Promise.resolve());
-          mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
-          mockSession.getToken.mockResolvedValue('mocked-token');
+        const sut = new Clerk(productionPublishableKey);
+        await sut.load();
+        await sut.setActive({ session: mockSession as any as ActiveSessionResource, beforeEmit: beforeEmitMock });
+      });
 
-          const sut = new Clerk(productionPublishableKey);
-          await sut.load({ touchSession: false });
-          await sut.setActive({ session: mockSession as any as ActiveSessionResource });
-          await waitFor(() => {
-            expect(mockSession.touch).not.toHaveBeenCalled();
-            expect(mockSession.getToken).toHaveBeenCalled();
-          });
+      // TODO: @dimkl include set transitive state
+      it('calls session.touch -> set cookie -> before emit with touched session on session switch', async () => {
+        const mockSession2 = {
+          id: '2',
+          remove: jest.fn(),
+          status: 'active',
+          user: {},
+          touch: jest.fn(),
+          getToken: jest.fn(),
+        };
+        mockClientFetch.mockReturnValue(
+          Promise.resolve({
+            signedInSessions: [mockSession, mockSession2],
+          }),
+        );
+
+        const sut = new Clerk(productionPublishableKey);
+        await sut.load();
+
+        const executionOrder: string[] = [];
+        mockSession2.touch.mockImplementationOnce(() => {
+          sut.session = mockSession2 as any;
+          executionOrder.push('session.touch');
+          return Promise.resolve();
+        });
+        mockSession2.getToken.mockImplementation(() => {
+          executionOrder.push('set cookie');
+          return 'mocked-token-2';
+        });
+        const beforeEmitMock = jest.fn().mockImplementationOnce(() => {
+          executionOrder.push('before emit');
+          return Promise.resolve();
         });
 
-        it('calls __unstable__onBeforeSetActive before session.touch', async () => {
-          mockSession.touch.mockReturnValueOnce(Promise.resolve());
-          mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
+        await sut.setActive({ session: mockSession2 as any as ActiveSessionResource, beforeEmit: beforeEmitMock });
 
-          (window as any).__unstable__onBeforeSetActive = () => {
-            expect(mockSession.touch).not.toHaveBeenCalled();
-          };
+        await waitFor(() => {
+          expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']);
+          expect(mockSession2.touch).toHaveBeenCalled();
+          expect(mockSession2.getToken).toHaveBeenCalled();
+          expect(beforeEmitMock).toHaveBeenCalledWith(mockSession2);
+          expect(sut.session).toMatchObject(mockSession2);
+        });
+      });
 
-          const sut = new Clerk(productionPublishableKey);
-          await sut.load();
-          await sut.setActive({ session: mockSession as any as ActiveSessionResource });
-          expect(mockSession.touch).toHaveBeenCalled();
+      // TODO: @dimkl include set transitive state
+      it('calls with lastActiveOrganizationId session.touch -> set cookie -> before emit -> set accessors with touched session on organization switch', async () => {
+        mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
+        const sut = new Clerk(productionPublishableKey);
+        await sut.load();
+
+        const executionOrder: string[] = [];
+        mockSession.touch.mockImplementationOnce(() => {
+          sut.session = mockSession as any;
+          executionOrder.push('session.touch');
+          return Promise.resolve();
+        });
+        mockSession.getToken.mockImplementation(() => {
+          executionOrder.push('set cookie');
+          return 'mocked-token';
         });
 
-        it('sets __session and __client_uat cookie before calling __unstable__onBeforeSetActive', async () => {
-          mockSession.touch.mockReturnValueOnce(Promise.resolve());
-          mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
+        const beforeEmitMock = jest.fn().mockImplementationOnce(() => {
+          executionOrder.push('before emit');
+          return Promise.resolve();
+        });
 
-          (window as any).__unstable__onBeforeSetActive = () => {
-            expect(eventBusSpy).toHaveBeenCalledWith('token:update', { token: mockSession.lastActiveToken });
-          };
+        await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock });
 
-          const sut = new Clerk(productionPublishableKey);
-          await sut.load();
-          await sut.setActive({ session: mockSession as any as ActiveSessionResource });
+        await waitFor(() => {
+          expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']);
+          expect(mockSession.touch).toHaveBeenCalled();
+          expect(mockSession.getToken).toHaveBeenCalled();
+          expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
+          expect(beforeEmitMock).toHaveBeenCalledWith(mockSession);
+          expect(sut.session).toMatchObject(mockSession);
         });
+      });
 
-        it('calls __unstable__onAfterSetActive after beforeEmit and session.touch', async () => {
-          const beforeEmitMock = jest.fn();
-          mockSession.touch.mockReturnValueOnce(Promise.resolve());
-          mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
+      it('sets active organization by slug', async () => {
+        const mockSession2 = {
+          id: '1',
+          status,
+          user: {
+            organizationMemberships: [
+              {
+                id: 'orgmem_id',
+                organization: {
+                  id: 'org_id',
+                  slug: 'some-org-slug',
+                },
+              },
+            ],
+          },
+          touch: jest.fn(),
+          getToken: jest.fn(),
+        };
+        mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession2] }));
+        const sut = new Clerk(productionPublishableKey);
+        await sut.load();
 
-          (window as any).__unstable__onAfterSetActive = () => {
-            expect(mockSession.touch).toHaveBeenCalled();
-            expect(beforeEmitMock).toHaveBeenCalled();
-          };
+        mockSession2.touch.mockImplementationOnce(() => {
+          sut.session = mockSession2 as any;
+          return Promise.resolve();
+        });
+        mockSession2.getToken.mockImplementation(() => 'mocked-token');
 
-          const sut = new Clerk(productionPublishableKey);
-          await sut.load();
-          await sut.setActive({ session: mockSession as any as ActiveSessionResource, beforeEmit: beforeEmitMock });
+        await sut.setActive({ organization: 'some-org-slug' });
+
+        await waitFor(() => {
+          expect(mockSession2.touch).toHaveBeenCalled();
+          expect(mockSession2.getToken).toHaveBeenCalled();
+          expect((mockSession2 as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
+          expect(sut.session).toMatchObject(mockSession2);
         });
+      });
 
-        // TODO: @dimkl include set transitive state
-        it('calls session.touch -> set cookie -> before emit with touched session on session switch', async () => {
-          const mockSession2 = {
-            id: '2',
-            remove: jest.fn(),
-            status: 'active',
-            user: {},
-            touch: jest.fn(),
-            getToken: jest.fn(),
-          };
-          mockClientFetch.mockReturnValue(
-            Promise.resolve({
-              signedInSessions: [mockSession, mockSession2],
-            }),
-          );
+      it('redirects the user to the /v1/client/touch endpoint if the cookie_expires_at is less than 8 days away', async () => {
+        mockSession.touch.mockReturnValue(Promise.resolve());
+        mockClientFetch.mockReturnValue(
+          Promise.resolve({
+            signedInSessions: [mockSession],
+            cookieExpiresAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
+            isEligibleForTouch: () => true,
+            buildTouchUrl: () =>
+              `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`,
+          }),
+        );
 
-          const sut = new Clerk(productionPublishableKey);
-          await sut.load();
+        const sut = new Clerk(productionPublishableKey);
+        sut.navigate = jest.fn();
+        await sut.load();
+        await sut.setActive({
+          session: mockSession as any as ActiveSessionResource,
+          redirectUrl: '/redirect-url-path',
+        });
+        const redirectUrl = new URL((sut.navigate as jest.Mock).mock.calls[0]);
+        expect(redirectUrl.pathname).toEqual('/v1/client/touch');
+        expect(redirectUrl.searchParams.get('redirect_url')).toEqual(`${mockWindowLocation.href}/redirect-url-path`);
+      });
 
-          const executionOrder: string[] = [];
-          mockSession2.touch.mockImplementationOnce(() => {
-            sut.session = mockSession2 as any;
-            executionOrder.push('session.touch');
-            return Promise.resolve();
-          });
-          mockSession2.getToken.mockImplementation(() => {
-            executionOrder.push('set cookie');
-            return 'mocked-token-2';
-          });
-          const beforeEmitMock = jest.fn().mockImplementationOnce(() => {
-            executionOrder.push('before emit');
-            return Promise.resolve();
-          });
+      it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is more than 8 days away', async () => {
+        mockSession.touch.mockReturnValue(Promise.resolve());
+        mockClientFetch.mockReturnValue(
+          Promise.resolve({
+            signedInSessions: [mockSession],
+            cookieExpiresAt: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10 days from now
+            isEligibleForTouch: () => false,
+            buildTouchUrl: () =>
+              `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`,
+          }),
+        );
 
-          await sut.setActive({ session: mockSession2 as any as ActiveSessionResource, beforeEmit: beforeEmitMock });
+        const sut = new Clerk(productionPublishableKey);
+        sut.navigate = jest.fn();
+        await sut.load();
+        await sut.setActive({
+          session: mockSession as any as ActiveSessionResource,
+          redirectUrl: '/redirect-url-path',
+        });
+        expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path');
+      });
 
-          await waitFor(() => {
-            expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']);
-            expect(mockSession2.touch).toHaveBeenCalled();
-            expect(mockSession2.getToken).toHaveBeenCalled();
-            expect(beforeEmitMock).toHaveBeenCalledWith(mockSession2);
-            expect(sut.session).toMatchObject(mockSession2);
-          });
+      it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is not set', async () => {
+        mockSession.touch.mockReturnValue(Promise.resolve());
+        mockClientFetch.mockReturnValue(
+          Promise.resolve({
+            signedInSessions: [mockSession],
+            cookieExpiresAt: null,
+            isEligibleForTouch: () => false,
+            buildTouchUrl: () =>
+              `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`,
+          }),
+        );
+
+        const sut = new Clerk(productionPublishableKey);
+        sut.navigate = jest.fn();
+        await sut.load();
+        await sut.setActive({
+          session: mockSession as any as ActiveSessionResource,
+          redirectUrl: '/redirect-url-path',
         });
+        expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path');
+      });
 
-        // TODO: @dimkl include set transitive state
-        it('calls with lastActiveOrganizationId session.touch -> set cookie -> before emit -> set accessors with touched session on organization switch', async () => {
+      mockNativeRuntime(() => {
+        it('calls session.touch in a non-standard browser', async () => {
           mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
+
           const sut = new Clerk(productionPublishableKey);
-          await sut.load();
+          await sut.load({ standardBrowser: false });
 
           const executionOrder: string[] = [];
           mockSession.touch.mockImplementationOnce(() => {
@@ -319,11 +458,6 @@ describe('Clerk singleton', () => {
             executionOrder.push('session.touch');
             return Promise.resolve();
           });
-          mockSession.getToken.mockImplementation(() => {
-            executionOrder.push('set cookie');
-            return 'mocked-token';
-          });
-
           const beforeEmitMock = jest.fn().mockImplementationOnce(() => {
             executionOrder.push('before emit');
             return Promise.resolve();
@@ -331,152 +465,98 @@ describe('Clerk singleton', () => {
 
           await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock });
 
-          await waitFor(() => {
-            expect(executionOrder).toEqual(['session.touch', 'set cookie', 'before emit']);
-            expect(mockSession.touch).toHaveBeenCalled();
-            expect(mockSession.getToken).toHaveBeenCalled();
-            expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
-            expect(beforeEmitMock).toHaveBeenCalledWith(mockSession);
-            expect(sut.session).toMatchObject(mockSession);
-          });
+          expect(executionOrder).toEqual(['session.touch', 'before emit']);
+          expect(mockSession.touch).toHaveBeenCalled();
+          expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
+          expect(mockSession.getToken).toHaveBeenCalled();
+          expect(beforeEmitMock).toHaveBeenCalledWith(mockSession);
+          expect(sut.session).toMatchObject(mockSession);
         });
+      });
+    });
 
-        it('sets active organization by slug', async () => {
-          const mockSession2 = {
-            id: '1',
-            status,
-            user: {
-              organizationMemberships: [
-                {
-                  id: 'orgmem_id',
-                  organization: {
-                    id: 'org_id',
-                    slug: 'some-org-slug',
-                  },
-                },
-              ],
-            },
-            touch: jest.fn(),
-            getToken: jest.fn(),
-          };
-          mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession2] }));
-          const sut = new Clerk(productionPublishableKey);
-          await sut.load();
+    describe('with `pending` session status', () => {
+      const mockSession = {
+        id: '1',
+        remove: jest.fn(),
+        status: 'pending',
+        user: {},
+        touch: jest.fn(() => Promise.resolve()),
+        getToken: jest.fn(),
+        lastActiveToken: { getRawString: () => 'mocked-token' },
+        tasks: [{ key: 'org' }],
+        currentTask: { key: 'org', __internal_getUrl: () => 'https://foocorp.com/add-organization' },
+      };
+      let eventBusSpy;
 
-          mockSession2.touch.mockImplementationOnce(() => {
-            sut.session = mockSession2 as any;
-            return Promise.resolve();
-          });
-          mockSession2.getToken.mockImplementation(() => 'mocked-token');
+      beforeEach(() => {
+        eventBusSpy = jest.spyOn(eventBus, 'dispatch');
+      });
 
-          await sut.setActive({ organization: 'some-org-slug' });
+      afterEach(() => {
+        mockSession.remove.mockReset();
+        mockSession.touch.mockReset();
 
-          await waitFor(() => {
-            expect(mockSession2.touch).toHaveBeenCalled();
-            expect(mockSession2.getToken).toHaveBeenCalled();
-            expect((mockSession2 as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
-            expect(sut.session).toMatchObject(mockSession2);
-          });
-        });
+        eventBusSpy?.mockRestore();
+        // cleanup global window pollution
+        (window as any).__unstable__onBeforeSetActive = null;
+        (window as any).__unstable__onAfterSetActive = null;
+      });
 
-        it('redirects the user to the /v1/client/touch endpoint if the cookie_expires_at is less than 8 days away', async () => {
-          mockSession.touch.mockReturnValue(Promise.resolve());
-          mockClientFetch.mockReturnValue(
-            Promise.resolve({
-              signedInSessions: [mockSession],
-              cookieExpiresAt: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000), // 2 days from now
-              isEligibleForTouch: () => true,
-              buildTouchUrl: () =>
-                `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`,
-            }),
-          );
+      it('calls session.touch by default', async () => {
+        mockSession.touch.mockReturnValue(Promise.resolve());
+        mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
 
-          const sut = new Clerk(productionPublishableKey);
-          sut.navigate = jest.fn();
-          await sut.load();
-          await sut.setActive({
-            session: mockSession as any as ActiveSessionResource,
-            redirectUrl: '/redirect-url-path',
-          });
-          const redirectUrl = new URL((sut.navigate as jest.Mock).mock.calls[0]);
-          expect(redirectUrl.pathname).toEqual('/v1/client/touch');
-          expect(redirectUrl.searchParams.get('redirect_url')).toEqual(`${mockWindowLocation.href}/redirect-url-path`);
-        });
+        const sut = new Clerk(productionPublishableKey);
+        await sut.load();
+        await sut.setActive({ session: mockSession as any as ActiveSessionResource });
+        expect(mockSession.touch).toHaveBeenCalled();
+      });
 
-        it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is more than 8 days away', async () => {
-          mockSession.touch.mockReturnValue(Promise.resolve());
-          mockClientFetch.mockReturnValue(
-            Promise.resolve({
-              signedInSessions: [mockSession],
-              cookieExpiresAt: new Date(Date.now() + 10 * 24 * 60 * 60 * 1000), // 10 days from now
-              isEligibleForTouch: () => false,
-              buildTouchUrl: () =>
-                `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`,
-            }),
-          );
+      it('does not call session.touch if Clerk was initialised with touchSession set to false', async () => {
+        mockSession.touch.mockReturnValueOnce(Promise.resolve());
+        mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
+        mockSession.getToken.mockResolvedValue('mocked-token');
 
-          const sut = new Clerk(productionPublishableKey);
-          sut.navigate = jest.fn();
-          await sut.load();
-          await sut.setActive({
-            session: mockSession as any as ActiveSessionResource,
-            redirectUrl: '/redirect-url-path',
-          });
-          expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path');
+        const sut = new Clerk(productionPublishableKey);
+        await sut.load({ touchSession: false });
+        await sut.setActive({ session: mockSession as any as ActiveSessionResource });
+        await waitFor(() => {
+          expect(mockSession.touch).not.toHaveBeenCalled();
+          expect(mockSession.getToken).toHaveBeenCalled();
         });
+      });
 
-        it('does not redirect the user to the /v1/client/touch endpoint if the cookie_expires_at is not set', async () => {
-          mockSession.touch.mockReturnValue(Promise.resolve());
-          mockClientFetch.mockReturnValue(
-            Promise.resolve({
-              signedInSessions: [mockSession],
-              cookieExpiresAt: null,
-              isEligibleForTouch: () => false,
-              buildTouchUrl: () =>
-                `https://clerk.example.com/v1/client/touch?redirect_url=${mockWindowLocation.href}/redirect-url-path`,
-            }),
-          );
+      it('navigates to task and set accessors with touched session', async () => {
+        mockClientFetch.mockReturnValue(Promise.resolve({ sessions: [mockSession], signedInSessions: [mockSession] }));
+        const sut = new Clerk(productionPublishableKey);
+        await sut.load();
 
-          const sut = new Clerk(productionPublishableKey);
-          sut.navigate = jest.fn();
-          await sut.load();
-          await sut.setActive({
-            session: mockSession as any as ActiveSessionResource,
-            redirectUrl: '/redirect-url-path',
-          });
-          expect(sut.navigate).toHaveBeenCalledWith('/redirect-url-path');
+        const executionOrder: string[] = [];
+        mockSession.touch.mockImplementationOnce(() => {
+          sut.session = mockSession as any;
+          executionOrder.push('session.touch');
+          return Promise.resolve();
+        });
+        mockSession.getToken.mockImplementation(() => {
+          executionOrder.push('set cookie');
+          return 'mocked-token';
+        });
+        sut.navigate = jest.fn().mockImplementationOnce(() => {
+          executionOrder.push('navigate');
+          return Promise.resolve();
         });
 
-        mockNativeRuntime(() => {
-          it('calls session.touch in a non-standard browser', async () => {
-            mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockSession] }));
-
-            const sut = new Clerk(productionPublishableKey);
-            await sut.load({ standardBrowser: false });
-
-            const executionOrder: string[] = [];
-            mockSession.touch.mockImplementationOnce(() => {
-              sut.session = mockSession as any;
-              executionOrder.push('session.touch');
-              return Promise.resolve();
-            });
-            const beforeEmitMock = jest.fn().mockImplementationOnce(() => {
-              executionOrder.push('before emit');
-              return Promise.resolve();
-            });
-
-            await sut.setActive({ organization: { id: 'org_id' } as Organization, beforeEmit: beforeEmitMock });
-
-            expect(executionOrder).toEqual(['session.touch', 'before emit']);
-            expect(mockSession.touch).toHaveBeenCalled();
-            expect((mockSession as any as ActiveSessionResource)?.lastActiveOrganizationId).toEqual('org_id');
-            expect(mockSession.getToken).toHaveBeenCalled();
-            expect(beforeEmitMock).toHaveBeenCalledWith(mockSession);
-            expect(sut.session).toMatchObject(mockSession);
-          });
+        await sut.setActive({ session: mockSession.id });
+
+        await waitFor(() => {
+          expect(executionOrder).toEqual(['session.touch', 'set cookie', 'navigate']);
+          expect(mockSession.touch).toHaveBeenCalled();
+          expect(mockSession.getToken).toHaveBeenCalled();
+          expect(sut.session).toMatchObject(mockSession);
         });
-      },
-    );
+      });
+    });
   });
 
   describe('.load()', () => {
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index a0c54ac931a..ec004989fb7 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -44,7 +44,6 @@ import type {
   OrganizationProfileProps,
   OrganizationResource,
   OrganizationSwitcherProps,
-  PendingSessionResource,
   PublicKeyCredentialCreationOptionsWithoutExtensions,
   PublicKeyCredentialRequestOptionsWithoutExtensions,
   PublicKeyCredentialWithAuthenticatorAssertionResponse,
@@ -132,6 +131,7 @@ import {
   Organization,
   Waitlist,
 } from './resources/internal';
+import { navigateToTask } from './sessionTasks';
 import { warnings } from './warnings';
 
 type SetActiveHook = (intent?: 'sign-out') => void | Promise<void>;
@@ -956,13 +956,13 @@ export class Clerk implements ClerkInterface {
       session = (this.client.sessions.find(x => x.id === session) as SignedInSessionResource) || null;
     }
 
-    let newSession = session === undefined ? this.session : session;
-
-    if (newSession?.status === 'pending') {
-      await this.#handlePendingSession(newSession);
+    if (session?.status === 'pending') {
+      await this.#handlePendingSession(session);
       return;
     }
 
+    let newSession = session === undefined ? this.session : session;
+
     // At this point, the `session` variable should contain either an `SignedInSessionResource`
     // ,`null` or `undefined`.
     // We now want to set the last active organization id on that session (if it exists).
@@ -1050,21 +1050,38 @@ export class Clerk implements ClerkInterface {
     await onAfterSetActive();
   };
 
-  #handlePendingSession = async (session: PendingSessionResource) => {
-    if (!session.currentTask || !this.environment) {
+  #handlePendingSession = async (session: SignedInSessionResource) => {
+    if (!this.environment) {
       return;
     }
 
-    if (session?.lastActiveToken) {
+    if (session.lastActiveToken) {
       eventBus.dispatch(events.TokenUpdate, { token: session.lastActiveToken });
     }
 
-    if (this.#internalComponentNavigate) {
-      // Handles navigation for UI components
-      await this.#internalComponentNavigate(session.currentTask.__internal_getPath());
-    } else {
-      // Handles navigation for custom flows
-      await this.navigate(session.currentTask.__internal_getUrl(this.#options, this.environment));
+    // Handles multi-session scenario when switching from `active`
+    // to `pending`
+    if (inActiveBrowserTab() || !this.#options.standardBrowser) {
+      await this.#touchCurrentSession(session);
+      session = this.#getSessionFromClient(session.id) ?? session;
+    }
+
+    // Syncs __session and __client_uat, in case the `pending` session
+    // has expired, it needs to trigger a sign-out
+    const token = await session.getToken();
+    if (!token) {
+      eventBus.dispatch(events.TokenUpdate, { token: null });
+    }
+
+    if (session.currentTask) {
+      await navigateToTask(session.currentTask, {
+        isInternalNavigation: !!this.#internalComponentNavigate,
+        navigate: this.#internalComponentNavigate ?? this.navigate,
+        options: this.#options,
+        environment: this.environment,
+      });
+
+      this.#internalComponentNavigate = null;
     }
 
     this.#setAccessors(session);
@@ -1098,12 +1115,8 @@ export class Clerk implements ClerkInterface {
     return unsubscribe;
   };
 
-  public __internal_setComponentNavigate = (navigate: (to: string) => Promise<unknown>): UnsubscribeCallback => {
+  public __internal_setComponentNavigate = (navigate: (to: string) => Promise<unknown>) => {
     this.#internalComponentNavigate = navigate;
-    const unsubscribe = () => {
-      this.#internalComponentNavigate = null;
-    };
-    return unsubscribe;
   };
 
   public navigate = async (to: string | undefined, options?: NavigateOptions): Promise<unknown> => {
diff --git a/packages/clerk-js/src/core/events.ts b/packages/clerk-js/src/core/events.ts
index 4982ed35da0..7401dd91370 100644
--- a/packages/clerk-js/src/core/events.ts
+++ b/packages/clerk-js/src/core/events.ts
@@ -1,21 +1,18 @@
-import type { SessionResource, TokenResource } from '@clerk/types';
+import type { TokenResource } from '@clerk/types';
 
 export const events = {
   TokenUpdate: 'token:update',
   UserSignOut: 'user:signOut',
-  InternalComponentNavigate: 'task:internalNavigate',
 } as const;
 
 type ClerkEvent = (typeof events)[keyof typeof events];
 type EventHandler<E extends ClerkEvent> = (payload: EventPayload[E]) => void;
 
 type TokenUpdatePayload = { token: TokenResource | null };
-type InternalComponentNavigatePayload = { resolveNavigation: () => void; session: SessionResource };
 
 type EventPayload = {
   [events.TokenUpdate]: TokenUpdatePayload;
   [events.UserSignOut]: null;
-  [events.InternalComponentNavigate]: InternalComponentNavigatePayload;
 };
 
 const createEventBus = () => {
@@ -48,11 +45,7 @@ const createEventBus = () => {
     eventToHandlersMap.set(event, []);
   };
 
-  const has = <E extends ClerkEvent>(event: E) => {
-    return !!eventToHandlersMap.has(event);
-  };
-
-  return { on, dispatch, off, has };
+  return { on, dispatch, off };
 };
 
 export const eventBus = createEventBus();
diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts
index 5f530d76789..1c9aa745b69 100644
--- a/packages/clerk-js/src/core/resources/Session.ts
+++ b/packages/clerk-js/src/core/resources/Session.ts
@@ -13,6 +13,7 @@ import type {
   SessionJSONSnapshot,
   SessionResource,
   SessionStatus,
+  SessionTask,
   SessionVerificationJSON,
   SessionVerificationResource,
   SessionVerifyAttemptFirstFactorParams,
@@ -34,7 +35,6 @@ import { clerkInvalidStrategy, clerkMissingWebAuthnPublicKeyOptions } from '../e
 import { eventBus, events } from '../events';
 import { SessionTokenCache } from '../tokenCache';
 import { BaseResource, PublicUserData, Token, User } from './internal';
-import { SessionTask } from './SessionTask';
 import { SessionVerification } from './SessionVerification';
 
 export class Session extends BaseResource implements SessionResource {
@@ -286,7 +286,7 @@ export class Session extends BaseResource implements SessionResource {
     this.createdAt = unixEpochToDate(data.created_at);
     this.updatedAt = unixEpochToDate(data.updated_at);
     this.user = new User(data.user);
-    this.tasks = data.tasks?.map(task => new SessionTask(task)) ?? [];
+    this.tasks = data.tasks;
 
     if (data.public_user_data) {
       this.publicUserData = new PublicUserData(data.public_user_data);
diff --git a/packages/clerk-js/src/core/resources/SessionTask.ts b/packages/clerk-js/src/core/resources/SessionTask.ts
deleted file mode 100644
index af4760ba012..00000000000
--- a/packages/clerk-js/src/core/resources/SessionTask.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import type {
-  ClerkOptions,
-  EnvironmentResource,
-  SessionTaskJSON,
-  SessionTaskJSONSnapshot,
-  SessionTaskKey,
-  SessionTaskResource,
-} from '@clerk/types';
-
-import { buildURL, inBrowser } from '../../utils';
-
-export const SESSION_TASK_PATHS = ['add-organization'] as const;
-type SessionTaskPath = (typeof SESSION_TASK_PATHS)[number];
-
-export const SESSION_TASK_PATH_BY_KEY: Record<SessionTaskKey, SessionTaskPath> = {
-  org: 'add-organization',
-} as const;
-
-export class SessionTask implements SessionTaskResource {
-  key!: SessionTaskKey;
-
-  constructor(data: SessionTaskJSON | SessionTaskJSONSnapshot) {
-    this.fromJSON(data);
-  }
-
-  protected fromJSON(data: SessionTaskJSON | SessionTaskJSONSnapshot): this {
-    if (!data) {
-      return this;
-    }
-
-    this.key = data.key;
-
-    return this;
-  }
-
-  public __internal_toSnapshot(): SessionTaskJSONSnapshot {
-    return {
-      key: this.key,
-    };
-  }
-
-  public __internal_getUrlPath(): `/${SessionTaskPath}` {
-    return `/${SESSION_TASK_PATH_BY_KEY[this.key]}`;
-  }
-
-  public __internal_getAbsoluteUrl(options: ClerkOptions, environment?: EnvironmentResource | null): string {
-    if (!environment || !inBrowser()) {
-      return '';
-    }
-
-    const signInUrl = options['signInUrl'] || environment.displayConfig.signInUrl;
-    const signUpUrl = options['signUpUrl'] || environment.displayConfig.signUpUrl;
-    const isReferrerSignUpUrl = window.location.href.startsWith(signUpUrl);
-
-    return buildURL(
-      // TODO - Introduce custom `tasksUrl` option to be used as a base path fallback for custom flows
-      { base: isReferrerSignUpUrl ? signUpUrl : signInUrl, hashPath: this.__internal_getUrlPath() },
-      { stringify: true },
-    );
-  }
-}
diff --git a/packages/clerk-js/src/core/sessionTasks.ts b/packages/clerk-js/src/core/sessionTasks.ts
new file mode 100644
index 00000000000..ff39cdbd5db
--- /dev/null
+++ b/packages/clerk-js/src/core/sessionTasks.ts
@@ -0,0 +1,46 @@
+import type { ClerkOptions, EnvironmentResource, SessionTask } from '@clerk/types';
+
+import { buildURL } from '../utils';
+
+export const SESSION_TASK_ROUTE_BY_KEY: Record<SessionTask['key'], string> = {
+  org: '/add-organization',
+} as const;
+
+function buildTasksUrl(task: SessionTask, options: ClerkOptions, environment: EnvironmentResource): string {
+  const signInUrl = options['signInUrl'] || environment.displayConfig.signInUrl;
+  const signUpUrl = options['signUpUrl'] || environment.displayConfig.signUpUrl;
+  const isReferrerSignUpUrl = window.location.href.startsWith(signUpUrl);
+
+  return buildURL(
+    // TODO - Accept custom URL option for custom flows in order to eject out of `signInUrl/signUpUrl`
+    {
+      base: isReferrerSignUpUrl ? signUpUrl : signInUrl,
+      hashPath: SESSION_TASK_ROUTE_BY_KEY[task.key],
+    },
+    { stringify: true },
+  );
+}
+
+interface NavigateToTaskOptions {
+  isInternalNavigation: boolean;
+  navigate: (to: string) => Promise<unknown>;
+  options: ClerkOptions;
+  environment: EnvironmentResource;
+}
+
+/**
+ * Initiates navigation to the tasks URL based on the application context such
+ * as internal component routing or custom flows.
+ * @internal
+ */
+export function navigateToTask(
+  task: SessionTask,
+  { isInternalNavigation, navigate, options, environment }: NavigateToTaskOptions,
+) {
+  if (!isInternalNavigation) {
+    // Handles navigation for custom flows, which is triggered outside of UI components routing context
+    return navigate(buildTasksUrl(task, options, environment));
+  } else {
+    return navigate(SESSION_TASK_ROUTE_BY_KEY['org']);
+  }
+}
diff --git a/packages/clerk-js/src/core/warnings.ts b/packages/clerk-js/src/core/warnings.ts
index a3eefbcd9f8..1fd500a1e91 100644
--- a/packages/clerk-js/src/core/warnings.ts
+++ b/packages/clerk-js/src/core/warnings.ts
@@ -21,10 +21,6 @@ const warnings = {
   cannotRenderComponentWhenUserDoesNotExist:
     '<UserProfile/> cannot render unless a user is signed in. Since no user is signed in, this is no-op.',
   cannotRenderComponentWhenOrgDoesNotExist: `<OrganizationProfile/> cannot render unless an organization is active. Since no organization is currently active, this is no-op.`,
-  cannotRenderSessionTaskComponentOnSignIn:
-    'Cannot render component unless a session task exists. Clerk is redirecting to `signInUrl` instead.',
-  cannotRenderSessionTaskComponentOnSignUp:
-    'Cannot render component unless a session task exists. Clerk is redirecting to `signUpUrl` instead.',
   cannotRenderAnyOrganizationComponent: createMessageForDisabledOrganizations,
   cannotOpenUserProfile:
     'The UserProfile modal cannot render unless a user is signed in. Since no user is signed in, this is no-op.',
diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts
index 655e5fcefce..add5954cfeb 100644
--- a/packages/clerk-js/src/ui/common/redirects.ts
+++ b/packages/clerk-js/src/ui/common/redirects.ts
@@ -1,3 +1,6 @@
+import type { SessionTask } from '@clerk/types';
+
+import { SESSION_TASK_ROUTE_BY_KEY } from '../../core/sessionTasks';
 import { buildURL } from '../../utils/url';
 import type { SignInContextType, SignUpContextType, UserProfileContextType } from './../contexts';
 
@@ -25,6 +28,22 @@ export function buildVerificationRedirectUrl({
   });
 }
 
+export function buildSessionTaskRedirectUrl(
+  ctx: Pick<SignInContextType | SignUpContextType, 'routing' | 'path'>,
+  baseUrl: string,
+  task: SessionTask,
+) {
+  const { routing, path } = ctx;
+
+  return buildRedirectUrl({
+    routing,
+    baseUrl,
+    path,
+    endpoint: SESSION_TASK_ROUTE_BY_KEY[task.key],
+    authQueryString: null,
+  });
+}
+
 export function buildSSOCallbackURL(
   ctx: Partial<SignInContextType | SignUpContextType>,
   baseUrl: string | undefined = '',
diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx
index 85d73efb3e6..b1d77dcd759 100644
--- a/packages/clerk-js/src/ui/common/withRedirect.tsx
+++ b/packages/clerk-js/src/ui/common/withRedirect.tsx
@@ -6,7 +6,7 @@ import React from 'react';
 
 import { warnings } from '../../core/warnings';
 import type { ComponentGuard } from '../../utils';
-import { noTaskExists, sessionExistsAndSingleSessionModeEnabled } from '../../utils';
+import { sessionExistsAndSingleSessionModeEnabled } from '../../utils';
 import { useEnvironment, useOptions, useSignInContext, useSignUpContext } from '../contexts';
 import { useRouter } from '../router';
 import type { AvailableComponentProps } from '../types';
@@ -61,7 +61,7 @@ export const withRedirectToAfterSignIn = <P extends AvailableComponentProps>(Com
     return withRedirect(
       Component,
       sessionExistsAndSingleSessionModeEnabled,
-      ({ clerk }) => signInCtx.tasksUrl || signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(),
+      ({ clerk }) => signInCtx.taskUrl || signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(),
       warnings.cannotRenderSignInComponentWhenSessionExists,
     )(props);
   };
@@ -80,7 +80,7 @@ export const withRedirectToAfterSignUp = <P extends AvailableComponentProps>(Com
     return withRedirect(
       Component,
       sessionExistsAndSingleSessionModeEnabled,
-      ({ clerk }) => signUpCtx.tasksUrl || signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(),
+      ({ clerk }) => signUpCtx.taskUrl || signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(),
       warnings.cannotRenderSignUpComponentWhenSessionExists,
     )(props);
   };
@@ -89,45 +89,3 @@ export const withRedirectToAfterSignUp = <P extends AvailableComponentProps>(Com
 
   return HOC;
 };
-
-export const withRedirectToSignUpIfNoTasksAvailable = <P extends AvailableComponentProps>(
-  Component: ComponentType<P>,
-) => {
-  const displayName = Component.displayName || Component.name || 'Component';
-  Component.displayName = displayName;
-
-  const HOC = (props: P) => {
-    const signUpCtx = useSignUpContext();
-    return withRedirect(
-      Component,
-      noTaskExists,
-      ({ clerk }) => signUpCtx.signUpUrl || clerk.buildSignUpUrl(),
-      warnings.cannotRenderSessionTaskComponentOnSignUp,
-    )(props);
-  };
-
-  HOC.displayName = `withRedirectToSignUpIfNoTasksAvailable(${displayName})`;
-
-  return HOC;
-};
-
-export const withRedirectToSignInIfNoTasksAvailable = <P extends AvailableComponentProps>(
-  Component: ComponentType<P>,
-) => {
-  const displayName = Component.displayName || Component.name || 'Component';
-  Component.displayName = displayName;
-
-  const HOC = (props: P) => {
-    const signInCtx = useSignInContext();
-    return withRedirect(
-      Component,
-      noTaskExists,
-      ({ clerk }) => signInCtx.signInUrl || clerk.buildSignInUrl(),
-      warnings.cannotRenderSessionTaskComponentOnSignUp,
-    )(props);
-  };
-
-  HOC.displayName = `withRedirectToSignInIfNoTasksAvailable(${displayName})`;
-
-  return HOC;
-};
diff --git a/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx b/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx
index 1a74f54e1cd..0f0cf55fa6e 100644
--- a/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx
+++ b/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx
@@ -1,5 +1,4 @@
-import { useSessionContext } from '@clerk/shared/react/index';
-import type { SessionTaskKey } from '@clerk/types';
+import type { SessionTask } from '@clerk/types';
 import { type ComponentType } from 'react';
 
 import { OrganizationListContext } from '../../contexts';
@@ -8,7 +7,7 @@ import { OrganizationList } from '../OrganizationList';
 /**
  * @internal
  */
-const SessionTaskRegistry: Record<SessionTaskKey, ComponentType> = {
+const SessionTaskRegistry: Record<SessionTask['key'], ComponentType> = {
   org: () => (
     // TODO - Hide personal workspace within organization list context based on environment
     <OrganizationListContext.Provider value={{ componentName: 'OrganizationList', hidePersonal: true }}>
@@ -20,15 +19,7 @@ const SessionTaskRegistry: Record<SessionTaskKey, ComponentType> = {
 /**
  * @internal
  */
-export function SessionTask(): React.ReactNode {
-  const session = useSessionContext();
-  const [currentTask] = session?.tasks ?? [];
-
-  if (!currentTask) {
-    return null;
-  }
-
-  const Content = SessionTaskRegistry[currentTask.key];
-
-  return Content ? <Content /> : null;
+export function SessionTask({ task }: { task: SessionTask['key'] }): React.ReactNode {
+  const Content = SessionTaskRegistry[task];
+  return <Content />;
 }
diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
index e5c5a5263e5..b04615ebbb6 100644
--- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
@@ -2,9 +2,7 @@ import { useClerk } from '@clerk/shared/react';
 import type { SignInModalProps, SignInProps } from '@clerk/types';
 import React from 'react';
 
-import { SESSION_TASK_PATHS, SessionTask } from '../../../core/resources/SessionTask';
 import { normalizeRoutingOptions } from '../../../utils/normalizeRoutingOptions';
-import { withRedirectToSignInIfNoTasksAvailable } from '../../common';
 import { SignInEmailLinkFlowComplete, SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard';
 import type { SignUpContextType } from '../../contexts';
 import {
@@ -17,8 +15,10 @@ import {
 import { Flow } from '../../customizables';
 import { useFetch } from '../../hooks';
 import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
+import { SessionTask } from '../SessionTask';
 import {
   LazySignUpContinue,
+  LazySignUpSessionTask,
   LazySignUpSSOCallback,
   LazySignUpStart,
   LazySignUpVerifyEmail,
@@ -82,6 +82,7 @@ function SignInRoutes(): JSX.Element {
             redirectUrl='../factor-two'
           />
         </Route>
+
         {signInContext.isCombinedFlow && (
           <Route path='create'>
             <Route
@@ -129,43 +130,21 @@ function SignInRoutes(): JSX.Element {
               >
                 <LazySignUpVerifyPhone />
               </Route>
-              {SESSION_TASK_PATHS.map(path => (
-                <Route
-                  path={path}
-                  key={path}
-                >
-                  <SignInSessionTask />
-                </Route>
-              ))}
+              <Route path='add-organization'>
+                <LazySignUpSessionTask task='org' />
+              </Route>
               <Route index>
                 <LazySignUpContinue />
               </Route>
             </Route>
-
-            {SESSION_TASK_PATHS.map(path => (
-              <Route
-                path={path}
-                key={path}
-              >
-                <SignInSessionTask />
-              </Route>
-            ))}
-
             <Route index>
               <LazySignUpStart />
             </Route>
           </Route>
         )}
-
-        {SESSION_TASK_PATHS.map(path => (
-          <Route
-            path={path}
-            key={path}
-          >
-            <SignInSessionTask />
-          </Route>
-        ))}
-
+        <Route path='add-organization'>
+          <SessionTask task='org' />
+        </Route>
         <Route index>
           <SignInStart />
         </Route>
@@ -237,5 +216,3 @@ export const SignInModal = (props: SignInModalProps): JSX.Element => {
     </Route>
   );
 };
-
-const SignInSessionTask = withRedirectToSignInIfNoTasksAvailable(SessionTask);
diff --git a/packages/clerk-js/src/ui/components/SignIn/lazy-sign-up.ts b/packages/clerk-js/src/ui/components/SignIn/lazy-sign-up.ts
index e3c08113fd9..1101083cab1 100644
--- a/packages/clerk-js/src/ui/components/SignIn/lazy-sign-up.ts
+++ b/packages/clerk-js/src/ui/components/SignIn/lazy-sign-up.ts
@@ -7,6 +7,7 @@ const LazySignUpVerifyEmail = lazy(() => preloadSignUp().then(m => ({ default: m
 const LazySignUpStart = lazy(() => preloadSignUp().then(m => ({ default: m.SignUpStart })));
 const LazySignUpSSOCallback = lazy(() => preloadSignUp().then(m => ({ default: m.SignUpSSOCallback })));
 const LazySignUpContinue = lazy(() => preloadSignUp().then(m => ({ default: m.SignUpContinue })));
+const LazySignUpSessionTask = lazy(() => preloadSignUp().then(m => ({ default: m.SessionTask })));
 
 const lazyCompleteSignUpFlow = () =>
   import(/* webpackChunkName: "signUp" */ '../SignUp/util').then(m => m.completeSignUpFlow);
@@ -19,4 +20,5 @@ export {
   LazySignUpSSOCallback,
   LazySignUpContinue,
   lazyCompleteSignUpFlow,
+  LazySignUpSessionTask,
 };
diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
index 12ca62f4948..04478446b76 100644
--- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
@@ -2,8 +2,6 @@ import { useClerk } from '@clerk/shared/react';
 import type { SignUpModalProps, SignUpProps } from '@clerk/types';
 import React from 'react';
 
-import { SESSION_TASK_PATHS } from '../../../core/resources/SessionTask';
-import { withRedirectToSignUpIfNoTasksAvailable } from '../../../ui/common';
 import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard';
 import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts';
 import { Flow } from '../../customizables';
@@ -77,14 +75,9 @@ function SignUpRoutes(): JSX.Element {
             <SignUpContinue />
           </Route>
         </Route>
-        {SESSION_TASK_PATHS.map(path => (
-          <Route
-            path={path}
-            key={path}
-          >
-            <SignUpSessionTask />
-          </Route>
-        ))}
+        <Route path='add-organization'>
+          <SessionTask task='org' />
+        </Route>
         <Route index>
           <SignUpStart />
         </Route>
@@ -129,6 +122,4 @@ export const SignUpModal = (props: SignUpModalProps): JSX.Element => {
   );
 };
 
-const SignUpSessionTask = withRedirectToSignUpIfNoTasksAvailable(SessionTask);
-
-export { SignUpContinue, SignUpSSOCallback, SignUpStart, SignUpVerifyEmail, SignUpVerifyPhone };
+export { SignUpContinue, SignUpSSOCallback, SignUpStart, SignUpVerifyEmail, SignUpVerifyPhone, SessionTask };
diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
index 48018f1b574..bcc6aafed84 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
@@ -1,13 +1,17 @@
 import { useClerk } from '@clerk/shared/react';
 import { isAbsoluteUrl } from '@clerk/shared/url';
-import { createContext, useContext, useMemo } from 'react';
+import { createContext, useContext, useEffect, useMemo } from 'react';
 
 import { SIGN_IN_INITIAL_VALUE_KEYS } from '../../../core/constants';
 import { buildURL } from '../../../utils';
 import { RedirectUrls } from '../../../utils/redirectUrls';
-import { buildRedirectUrl, MAGIC_LINK_VERIFY_PATH_ROUTE, SSO_CALLBACK_PATH_ROUTE } from '../../common/redirects';
+import {
+  buildRedirectUrl,
+  buildSessionTaskRedirectUrl,
+  MAGIC_LINK_VERIFY_PATH_ROUTE,
+  SSO_CALLBACK_PATH_ROUTE,
+} from '../../common/redirects';
 import { useEnvironment, useOptions } from '../../contexts';
-import { useNavigateOnEvent } from '../../hooks/useNavigateOnEvent';
 import type { ParsedQueryString } from '../../router';
 import { useRouter } from '../../router';
 import type { SignInCtx } from '../../types';
@@ -22,7 +26,7 @@ export type SignInContextType = SignInCtx & {
   authQueryString: string | null;
   afterSignUpUrl: string;
   afterSignInUrl: string;
-  tasksUrl: string | null;
+  taskUrl: string | null;
   transferable: boolean;
   waitlistUrl: string;
   emailLinkRedirectUrl: string;
@@ -114,21 +118,17 @@ export const useSignInContext = (): SignInContextType => {
 
   const signUpContinueUrl = buildURL({ base: signUpUrl, hashPath: '/continue' }, { stringify: true });
 
-  const tasksUrl = clerk.session?.currentTask
-    ? buildRedirectUrl({
-        routing: ctx.routing,
-        baseUrl: signInUrl,
-        path: ctx.path,
-        endpoint: clerk.session?.currentTask?.__internal_getUrlPath(),
-        authQueryString: null,
-      })
+  const taskUrl = clerk.session?.currentTask
+    ? buildSessionTaskRedirectUrl({ routing: ctx.routing, path: ctx.path }, signInUrl, clerk.session?.currentTask)
     : null;
 
-  useNavigateOnEvent({
-    routing: ctx.routing,
-    baseUrl: signInUrl,
-    path: ctx.path,
-  });
+  useEffect(() => {
+    clerk.__internal_setComponentNavigate((endpoint: string) =>
+      navigate(
+        buildRedirectUrl({ routing: ctx.routing, path: ctx.path, baseUrl: signInUrl, endpoint, authQueryString }),
+      ),
+    );
+  }, []);
 
   return {
     ...(ctx as SignInCtx),
@@ -141,7 +141,7 @@ export const useSignInContext = (): SignInContextType => {
     afterSignUpUrl,
     emailLinkRedirectUrl,
     ssoCallbackUrl,
-    tasksUrl,
+    taskUrl,
     navigateAfterSignIn,
     signUpContinueUrl,
     queryParams,
diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
index 4239965ecde..44fdbf06542 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
@@ -1,13 +1,17 @@
 import { useClerk } from '@clerk/shared/react';
 import { isAbsoluteUrl } from '@clerk/shared/url';
-import { createContext, useContext, useMemo } from 'react';
+import { createContext, useContext, useEffect, useMemo } from 'react';
 
 import { SIGN_UP_INITIAL_VALUE_KEYS } from '../../../core/constants';
 import { buildURL } from '../../../utils';
 import { RedirectUrls } from '../../../utils/redirectUrls';
-import { buildRedirectUrl, MAGIC_LINK_VERIFY_PATH_ROUTE, SSO_CALLBACK_PATH_ROUTE } from '../../common/redirects';
+import {
+  buildRedirectUrl,
+  buildSessionTaskRedirectUrl,
+  MAGIC_LINK_VERIFY_PATH_ROUTE,
+  SSO_CALLBACK_PATH_ROUTE,
+} from '../../common/redirects';
 import { useEnvironment, useOptions } from '../../contexts';
-import { useNavigateOnEvent } from '../../hooks/useNavigateOnEvent';
 import type { ParsedQueryString } from '../../router';
 import { useRouter } from '../../router';
 import type { SignUpCtx } from '../../types';
@@ -23,7 +27,7 @@ export type SignUpContextType = SignUpCtx & {
   afterSignUpUrl: string;
   afterSignInUrl: string;
   waitlistUrl: string;
-  tasksUrl: string | null;
+  taskUrl: string | null;
   isCombinedFlow: boolean;
   emailLinkRedirectUrl: string;
   ssoCallbackUrl: string;
@@ -109,21 +113,17 @@ export const useSignUpContext = (): SignUpContextType => {
   // TODO: Avoid building this url again to remove duplicate code. Get it from window.Clerk instead.
   const secondFactorUrl = buildURL({ base: signInUrl, hashPath: '/factor-two' }, { stringify: true });
 
-  const tasksUrl = clerk.session?.currentTask
-    ? buildRedirectUrl({
-        routing: ctx.routing,
-        baseUrl: signUpUrl,
-        path: ctx.path,
-        endpoint: clerk.session?.currentTask?.__internal_getUrlPath(),
-        authQueryString: null,
-      })
+  const taskUrl = clerk.session?.currentTask
+    ? buildSessionTaskRedirectUrl({ routing: ctx.routing, path: ctx.path }, signUpUrl, clerk.session?.currentTask)
     : null;
 
-  useNavigateOnEvent({
-    routing: ctx.routing,
-    baseUrl: signUpUrl,
-    path: ctx.path,
-  });
+  useEffect(() => {
+    clerk.__internal_setComponentNavigate((endpoint: string) =>
+      navigate(
+        buildRedirectUrl({ routing: ctx.routing, path: ctx.path, baseUrl: signUpUrl, endpoint, authQueryString }),
+      ),
+    );
+  }, []);
 
   return {
     ...ctx,
@@ -136,7 +136,7 @@ export const useSignUpContext = (): SignUpContextType => {
     afterSignInUrl,
     emailLinkRedirectUrl,
     ssoCallbackUrl,
-    tasksUrl,
+    taskUrl,
     navigateAfterSignUp,
     queryParams,
     initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams },
diff --git a/packages/clerk-js/src/ui/hooks/useNavigateOnEvent.ts b/packages/clerk-js/src/ui/hooks/useNavigateOnEvent.ts
deleted file mode 100644
index 09e1e37f5f1..00000000000
--- a/packages/clerk-js/src/ui/hooks/useNavigateOnEvent.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-import type { SessionResource } from '@clerk/types';
-import { useEffect } from 'react';
-
-import { eventBus, events } from '../../core/events';
-import { buildRedirectUrl } from '../common';
-import { useRouter } from '../router';
-
-type UseNavigateOnEventOptions = Pick<Parameters<typeof buildRedirectUrl>[0], 'routing' | 'baseUrl' | 'path'>;
-
-/**
- * Custom hook to trigger internal component navigation by a event.
- */
-export const useNavigateOnEvent = ({ routing, baseUrl, path }: UseNavigateOnEventOptions) => {
-  const { navigate } = useRouter();
-
-  useEffect(() => {
-    const handleNavigation = ({
-      resolveNavigation,
-      session,
-    }: {
-      resolveNavigation: () => void;
-      session: SessionResource;
-    }) => {
-      if (!session.currentTask) {
-        return;
-      }
-
-      void navigate(
-        buildRedirectUrl({
-          routing,
-          baseUrl,
-          path,
-          endpoint: session.currentTask.__internal_getUrlPath(),
-          authQueryString: null,
-        }),
-      ).then(resolveNavigation);
-    };
-
-    eventBus.on(events.InternalComponentNavigate, handleNavigation);
-
-    return () => {
-      eventBus.off(events.InternalComponentNavigate, handleNavigation);
-    };
-  }, []);
-};
diff --git a/packages/clerk-js/src/utils/componentGuards.ts b/packages/clerk-js/src/utils/componentGuards.ts
index 07b1c08e751..f0084a6a47d 100644
--- a/packages/clerk-js/src/utils/componentGuards.ts
+++ b/packages/clerk-js/src/utils/componentGuards.ts
@@ -14,10 +14,6 @@ export const noUserExists: ComponentGuard = clerk => {
   return !clerk.user;
 };
 
-export const noTaskExists: ComponentGuard = clerk => {
-  return !clerk.session?.currentTask;
-};
-
 export const noOrganizationExists: ComponentGuard = clerk => {
   return !clerk.organization;
 };
diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts
index 455b9a72aa8..7db4b79b573 100644
--- a/packages/react/src/isomorphicClerk.ts
+++ b/packages/react/src/isomorphicClerk.ts
@@ -93,6 +93,7 @@ type IsomorphicLoadedClerk = Without<
   | '__internal_getCachedResources'
   | '__internal_reloadInitialResources'
   | '__experimental_commerce'
+  | '__internal_setComponentNavigate'
 > & {
   client: ClientResource | undefined;
   __experimental_commerce: __experimental_CommerceNamespace | undefined;
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 29834688505..f412b2e8a57 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -426,6 +426,13 @@ export interface Clerk {
    */
   __internal_addNavigationListener: (callback: () => void) => UnsubscribeCallback;
 
+  /**
+   * Registers an internal navigate function for UI components in order to be triggered
+   * from `Clerk`
+   * @internal
+   */
+  __internal_setComponentNavigate: (navigate: (to: string) => Promise<unknown>) => void;
+
   /**
    * Set the active session and organization explicitly.
    *
diff --git a/packages/types/src/json.ts b/packages/types/src/json.ts
index afab7bcf0f6..21f378f0cf3 100644
--- a/packages/types/src/json.ts
+++ b/packages/types/src/json.ts
@@ -13,7 +13,7 @@ import type { OrganizationCustomRoleKey, OrganizationPermissionKey } from './org
 import type { OrganizationSettingsJSON } from './organizationSettings';
 import type { OrganizationSuggestionStatus } from './organizationSuggestion';
 import type { SamlIdpSlug } from './saml';
-import type { SessionStatus, SessionTaskKey } from './session';
+import type { SessionStatus, SessionTask } from './session';
 import type { SessionVerificationLevel, SessionVerificationStatus } from './sessionVerification';
 import type { SignInFirstFactor, SignInJSON, SignInSecondFactor } from './signIn';
 import type { SignUpField, SignUpIdentificationField, SignUpStatus } from './signUp';
@@ -103,10 +103,6 @@ export interface SignUpJSON extends ClerkResourceJSON {
   verifications: SignUpVerificationsJSON | null;
 }
 
-export interface SessionTaskJSON {
-  key: SessionTaskKey;
-}
-
 export interface SessionJSON extends ClerkResourceJSON {
   object: 'session';
   id: string;
@@ -123,7 +119,7 @@ export interface SessionJSON extends ClerkResourceJSON {
   last_active_token: TokenJSON;
   last_active_organization_id: string | null;
   actor: ActJWTClaim | null;
-  tasks: Array<SessionTaskJSON> | null;
+  tasks: Array<SessionTask> | null;
   user: UserJSON;
   public_user_data: PublicUserDataJSON;
   created_at: number;
diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts
index 5b8a92677e6..bd48545b462 100644
--- a/packages/types/src/session.ts
+++ b/packages/types/src/session.ts
@@ -1,6 +1,3 @@
-import type { ClerkOptions } from 'clerk';
-
-import type { EnvironmentResource } from './environment';
 import type {
   BackupCodeAttempt,
   EmailCodeAttempt,
@@ -126,8 +123,8 @@ export interface SessionResource extends ClerkResource {
   lastActiveOrganizationId: string | null;
   lastActiveAt: Date;
   actor: ActJWTClaim | null;
-  tasks: Array<SessionTaskResource> | null;
-  currentTask?: SessionTaskResource;
+  tasks: Array<SessionTask> | null;
+  currentTask?: SessionTask;
   /**
    * The user associated with the session.
    */
@@ -177,6 +174,7 @@ export interface ActiveSessionResource extends SessionResource {
 export interface PendingSessionResource extends SessionResource {
   status: 'pending';
   user: UserResource;
+  currentTask: SessionTask;
 }
 
 /**
@@ -227,12 +225,8 @@ export interface PublicUserData {
   userId?: string;
 }
 
-export type SessionTaskKey = 'org';
-
-export interface SessionTaskResource {
-  key: SessionTaskKey;
-  __internal_getUrlPath: () => string;
-  __internal_getAbsoluteUrl: (options: ClerkOptions, environment?: EnvironmentResource | null) => string;
+export interface SessionTask {
+  key: 'org';
 }
 
 export type GetTokenOptions = {
diff --git a/packages/types/src/snapshots.ts b/packages/types/src/snapshots.ts
index babd482803c..bc398bb633e 100644
--- a/packages/types/src/snapshots.ts
+++ b/packages/types/src/snapshots.ts
@@ -19,7 +19,6 @@ import type {
   SamlAccountConnectionJSON,
   SamlAccountJSON,
   SessionJSON,
-  SessionTaskJSON,
   SignUpJSON,
   SignUpVerificationJSON,
   SignUpVerificationsJSON,
@@ -95,8 +94,6 @@ export type SessionJSONSnapshot = Override<
   }
 >;
 
-export type SessionTaskJSONSnapshot = SessionTaskJSON;
-
 export type SignUpJSONSnapshot = Override<
   Nullable<SignUpJSON, 'status'>,
   {

From c8daa94a09426d2d011d09d481c35263b7037f94 Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Fri, 7 Mar 2025 21:11:25 -0300
Subject: [PATCH 05/16] Introduce experimental prop for lazy loading routes

---
 .../next-app-router/src/app/layout.tsx        |  1 +
 packages/clerk-js/bundlewatch.config.json     |  1 -
 packages/clerk-js/src/core/clerk.ts           |  4 +-
 packages/clerk-js/src/core/sessionTasks.ts    | 45 +++++++++----------
 .../src/ui/components/SignIn/SignIn.tsx       | 24 ++++++----
 .../src/ui/components/SignIn/lazy-sign-up.ts  |  2 -
 .../src/ui/components/SignUp/SignUp.tsx       | 18 +++++---
 .../src/ui/contexts/components/SignIn.ts      |  2 +
 .../src/ui/contexts/components/SignUp.ts      |  2 +
 .../clerk-js/src/ui/lazyModules/components.ts |  6 +++
 packages/types/src/clerk.ts                   |  1 +
 11 files changed, 64 insertions(+), 42 deletions(-)

diff --git a/integration/templates/next-app-router/src/app/layout.tsx b/integration/templates/next-app-router/src/app/layout.tsx
index 2e56184f39d..16e2fce74d5 100644
--- a/integration/templates/next-app-router/src/app/layout.tsx
+++ b/integration/templates/next-app-router/src/app/layout.tsx
@@ -21,6 +21,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
         },
       }}
       experimental={{
+        withSessionTasks: true,
         persistClient: process.env.NEXT_PUBLIC_EXPERIMENTAL_PERSIST_CLIENT
           ? process.env.NEXT_PUBLIC_EXPERIMENTAL_PERSIST_CLIENT === 'true'
           : undefined,
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index d9133f5333a..f6f8251ad34 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -1,6 +1,5 @@
 {
   "files": [
-    { "path": "./dist/clerk.js", "maxSize": "570kB" },
     { "path": "./dist/clerk.browser.js", "maxSize": "76kB" },
     { "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
     { "path": "./dist/ui-common*.js", "maxSize": "92KB" },
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index ec004989fb7..6c1390eef18 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -1075,8 +1075,8 @@ export class Clerk implements ClerkInterface {
 
     if (session.currentTask) {
       await navigateToTask(session.currentTask, {
-        isInternalNavigation: !!this.#internalComponentNavigate,
-        navigate: this.#internalComponentNavigate ?? this.navigate,
+        globalNavigate: this.navigate,
+        internalNavigate: this.#internalComponentNavigate,
         options: this.#options,
         environment: this.environment,
       });
diff --git a/packages/clerk-js/src/core/sessionTasks.ts b/packages/clerk-js/src/core/sessionTasks.ts
index ff39cdbd5db..3a296a3d155 100644
--- a/packages/clerk-js/src/core/sessionTasks.ts
+++ b/packages/clerk-js/src/core/sessionTasks.ts
@@ -6,41 +6,38 @@ export const SESSION_TASK_ROUTE_BY_KEY: Record<SessionTask['key'], string> = {
   org: '/add-organization',
 } as const;
 
-function buildTasksUrl(task: SessionTask, options: ClerkOptions, environment: EnvironmentResource): string {
-  const signInUrl = options['signInUrl'] || environment.displayConfig.signInUrl;
-  const signUpUrl = options['signUpUrl'] || environment.displayConfig.signUpUrl;
-  const isReferrerSignUpUrl = window.location.href.startsWith(signUpUrl);
-
-  return buildURL(
-    // TODO - Accept custom URL option for custom flows in order to eject out of `signInUrl/signUpUrl`
-    {
-      base: isReferrerSignUpUrl ? signUpUrl : signInUrl,
-      hashPath: SESSION_TASK_ROUTE_BY_KEY[task.key],
-    },
-    { stringify: true },
-  );
-}
-
 interface NavigateToTaskOptions {
-  isInternalNavigation: boolean;
-  navigate: (to: string) => Promise<unknown>;
+  internalNavigate: ((to: string) => Promise<unknown>) | null;
+  globalNavigate: (to: string) => Promise<unknown>;
   options: ClerkOptions;
   environment: EnvironmentResource;
 }
 
 /**
- * Initiates navigation to the tasks URL based on the application context such
+ * Handles navigation to the tasks URL based on the application context such
  * as internal component routing or custom flows.
  * @internal
  */
 export function navigateToTask(
   task: SessionTask,
-  { isInternalNavigation, navigate, options, environment }: NavigateToTaskOptions,
+  { internalNavigate, globalNavigate, options, environment }: NavigateToTaskOptions,
 ) {
-  if (!isInternalNavigation) {
-    // Handles navigation for custom flows, which is triggered outside of UI components routing context
-    return navigate(buildTasksUrl(task, options, environment));
-  } else {
-    return navigate(SESSION_TASK_ROUTE_BY_KEY['org']);
+  if (internalNavigate) {
+    return internalNavigate(SESSION_TASK_ROUTE_BY_KEY['org']);
   }
+
+  const signInUrl = options['signInUrl'] || environment.displayConfig.signInUrl;
+  const signUpUrl = options['signUpUrl'] || environment.displayConfig.signUpUrl;
+  const isReferrerSignUpUrl = window.location.href.startsWith(signUpUrl);
+
+  const taskUrl = buildURL(
+    // TODO - Accept custom URL option for custom flows in order to eject out of `signInUrl/signUpUrl`
+    {
+      base: isReferrerSignUpUrl ? signUpUrl : signInUrl,
+      hashPath: SESSION_TASK_ROUTE_BY_KEY[task.key],
+    },
+    { stringify: true },
+  );
+
+  return globalNavigate(taskUrl);
 }
diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
index b04615ebbb6..1ff0699bb43 100644
--- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
@@ -14,11 +14,10 @@ import {
 } from '../../contexts';
 import { Flow } from '../../customizables';
 import { useFetch } from '../../hooks';
+import { preloadSessionTask, SessionTask } from '../../lazyModules/components';
 import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
-import { SessionTask } from '../SessionTask';
 import {
   LazySignUpContinue,
-  LazySignUpSessionTask,
   LazySignUpSSOCallback,
   LazySignUpStart,
   LazySignUpVerifyEmail,
@@ -130,9 +129,11 @@ function SignInRoutes(): JSX.Element {
               >
                 <LazySignUpVerifyPhone />
               </Route>
-              <Route path='add-organization'>
-                <LazySignUpSessionTask task='org' />
-              </Route>
+              {signInContext.withSessionTasks && (
+                <Route path='add-organization'>
+                  <SessionTask task='org' />
+                </Route>
+              )}
               <Route index>
                 <LazySignUpContinue />
               </Route>
@@ -142,9 +143,11 @@ function SignInRoutes(): JSX.Element {
             </Route>
           </Route>
         )}
-        <Route path='add-organization'>
-          <SessionTask task='org' />
-        </Route>
+        {signInContext.withSessionTasks && (
+          <Route path='add-organization'>
+            <SessionTask task='org' />
+          </Route>
+        )}
         <Route index>
           <SignInStart />
         </Route>
@@ -159,6 +162,9 @@ function SignInRoutes(): JSX.Element {
 const usePreloadSignUp = (enabled = false) =>
   useFetch(enabled ? preloadSignUp : undefined, 'preloadComponent', { staleTime: Infinity });
 
+const usePreloadSessionTask = (enabled = false) =>
+  useFetch(enabled ? preloadSessionTask : undefined, 'preloadComponent', { staleTime: Infinity });
+
 function SignInRoot() {
   const signInContext = useSignInContext();
   const normalizedSignUpContext = {
@@ -177,6 +183,8 @@ function SignInRoot() {
    */
   usePreloadSignUp(signInContext.isCombinedFlow);
 
+  usePreloadSessionTask(signInContext.withSessionTasks);
+
   return (
     <SignUpContext.Provider value={normalizedSignUpContext}>
       <SignInRoutes />
diff --git a/packages/clerk-js/src/ui/components/SignIn/lazy-sign-up.ts b/packages/clerk-js/src/ui/components/SignIn/lazy-sign-up.ts
index 1101083cab1..e3c08113fd9 100644
--- a/packages/clerk-js/src/ui/components/SignIn/lazy-sign-up.ts
+++ b/packages/clerk-js/src/ui/components/SignIn/lazy-sign-up.ts
@@ -7,7 +7,6 @@ const LazySignUpVerifyEmail = lazy(() => preloadSignUp().then(m => ({ default: m
 const LazySignUpStart = lazy(() => preloadSignUp().then(m => ({ default: m.SignUpStart })));
 const LazySignUpSSOCallback = lazy(() => preloadSignUp().then(m => ({ default: m.SignUpSSOCallback })));
 const LazySignUpContinue = lazy(() => preloadSignUp().then(m => ({ default: m.SignUpContinue })));
-const LazySignUpSessionTask = lazy(() => preloadSignUp().then(m => ({ default: m.SessionTask })));
 
 const lazyCompleteSignUpFlow = () =>
   import(/* webpackChunkName: "signUp" */ '../SignUp/util').then(m => m.completeSignUpFlow);
@@ -20,5 +19,4 @@ export {
   LazySignUpSSOCallback,
   LazySignUpContinue,
   lazyCompleteSignUpFlow,
-  LazySignUpSessionTask,
 };
diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
index 04478446b76..5086f413fba 100644
--- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
@@ -5,14 +5,18 @@ import React from 'react';
 import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard';
 import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts';
 import { Flow } from '../../customizables';
+import { useFetch } from '../../hooks';
+import { preloadSessionTask, SessionTask } from '../../lazyModules/components';
 import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
-import { SessionTask } from '../SessionTask';
 import { SignUpContinue } from './SignUpContinue';
 import { SignUpSSOCallback } from './SignUpSSOCallback';
 import { SignUpStart } from './SignUpStart';
 import { SignUpVerifyEmail } from './SignUpVerifyEmail';
 import { SignUpVerifyPhone } from './SignUpVerifyPhone';
 
+const usePreloadSessionTask = (enabled = false) =>
+  useFetch(enabled ? preloadSessionTask : undefined, 'preloadComponent', { staleTime: Infinity });
+
 function RedirectToSignUp() {
   const clerk = useClerk();
   React.useEffect(() => {
@@ -24,6 +28,8 @@ function RedirectToSignUp() {
 function SignUpRoutes(): JSX.Element {
   const signUpContext = useSignUpContext();
 
+  usePreloadSessionTask(signUpContext.withSessionTasks);
+
   return (
     <Flow.Root flow='signUp'>
       <Switch>
@@ -75,9 +81,11 @@ function SignUpRoutes(): JSX.Element {
             <SignUpContinue />
           </Route>
         </Route>
-        <Route path='add-organization'>
-          <SessionTask task='org' />
-        </Route>
+        {signUpContext.withSessionTasks && (
+          <Route path='add-organization'>
+            <SessionTask task='org' />
+          </Route>
+        )}
         <Route index>
           <SignUpStart />
         </Route>
@@ -122,4 +130,4 @@ export const SignUpModal = (props: SignUpModalProps): JSX.Element => {
   );
 };
 
-export { SignUpContinue, SignUpSSOCallback, SignUpStart, SignUpVerifyEmail, SignUpVerifyPhone, SessionTask };
+export { SignUpContinue, SignUpSSOCallback, SignUpStart, SignUpVerifyEmail, SignUpVerifyPhone };
diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
index bcc6aafed84..320da48584e 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
@@ -32,6 +32,7 @@ export type SignInContextType = SignInCtx & {
   emailLinkRedirectUrl: string;
   ssoCallbackUrl: string;
   isCombinedFlow: boolean;
+  withSessionTasks: boolean;
 };
 
 export const SignInContext = createContext<SignInCtx | null>(null);
@@ -142,6 +143,7 @@ export const useSignInContext = (): SignInContextType => {
     emailLinkRedirectUrl,
     ssoCallbackUrl,
     taskUrl,
+    withSessionTasks: options.experimental?.withSessionTasks,
     navigateAfterSignIn,
     signUpContinueUrl,
     queryParams,
diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
index 44fdbf06542..660079551c4 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
@@ -31,6 +31,7 @@ export type SignUpContextType = SignUpCtx & {
   isCombinedFlow: boolean;
   emailLinkRedirectUrl: string;
   ssoCallbackUrl: string;
+  withSessionTasks: boolean;
 };
 
 export const SignUpContext = createContext<SignUpCtx | null>(null);
@@ -142,5 +143,6 @@ export const useSignUpContext = (): SignUpContextType => {
     initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams },
     authQueryString,
     isCombinedFlow,
+    withSessionTasks: options.experimental?.withSessionTasks,
   };
 };
diff --git a/packages/clerk-js/src/ui/lazyModules/components.ts b/packages/clerk-js/src/ui/lazyModules/components.ts
index 51788d64910..6182576c716 100644
--- a/packages/clerk-js/src/ui/lazyModules/components.ts
+++ b/packages/clerk-js/src/ui/lazyModules/components.ts
@@ -19,6 +19,7 @@ const componentImportPaths = {
   KeylessPrompt: () => import(/* webpackChunkName: "keylessPrompt" */ '../components/KeylessPrompt'),
   PricingTable: () => import(/* webpackChunkName: "pricingTable" */ '../components/PricingTable'),
   Checkout: () => import(/* webpackChunkName: "checkout" */ '../components/Checkout'),
+  SessionTask: () => import(/* webpackChunkName: "sessionTask" */ '../components/SessionTask'),
 } as const;
 
 export const SignIn = lazy(() => componentImportPaths.SignIn().then(module => ({ default: module.SignIn })));
@@ -94,6 +95,11 @@ export const PricingTable = lazy(() =>
   componentImportPaths.PricingTable().then(module => ({ default: module.__experimental_PricingTable })),
 );
 
+export const preloadSessionTask = () => import(/* webpackChunkName: "sessionTask" */ '../components/SessionTask');
+export const SessionTask = lazy(() =>
+  componentImportPaths.SessionTask().then(module => ({ default: module.SessionTask })),
+);
+
 export const preloadComponent = async (component: unknown) => {
   return componentImportPaths[component as keyof typeof componentImportPaths]?.();
 };
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index f412b2e8a57..f3c777a000b 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -810,6 +810,7 @@ export type ClerkOptions = ClerkOptionsNavigation &
          */
         rethrowOfflineNetworkErrors: boolean;
         commerce: boolean;
+        withSessionTasks: boolean;
       },
       Record<string, any>
     >;

From f34745acf92b7261e2f499ff5c892c287f704464 Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Sat, 8 Mar 2025 08:59:11 -0300
Subject: [PATCH 06/16] Send telemetry event

---
 packages/clerk-js/src/core/sessionTasks.ts        |  4 ++--
 packages/clerk-js/src/ui/common/withRedirect.tsx  |  4 ++--
 .../src/ui/components/SessionTask/SessionTask.tsx | 15 +++++++++------
 .../clerk-js/src/ui/components/SignIn/SignIn.tsx  |  4 ++--
 .../clerk-js/src/ui/components/SignUp/SignUp.tsx  |  4 ++--
 .../clerk-js/src/ui/contexts/components/SignIn.ts |  6 +++---
 .../clerk-js/src/ui/contexts/components/SignUp.ts |  6 +++---
 7 files changed, 23 insertions(+), 20 deletions(-)

diff --git a/packages/clerk-js/src/core/sessionTasks.ts b/packages/clerk-js/src/core/sessionTasks.ts
index 3a296a3d155..e7aece853e3 100644
--- a/packages/clerk-js/src/core/sessionTasks.ts
+++ b/packages/clerk-js/src/core/sessionTasks.ts
@@ -30,7 +30,7 @@ export function navigateToTask(
   const signUpUrl = options['signUpUrl'] || environment.displayConfig.signUpUrl;
   const isReferrerSignUpUrl = window.location.href.startsWith(signUpUrl);
 
-  const taskUrl = buildURL(
+  const sessionTaskUrl = buildURL(
     // TODO - Accept custom URL option for custom flows in order to eject out of `signInUrl/signUpUrl`
     {
       base: isReferrerSignUpUrl ? signUpUrl : signInUrl,
@@ -39,5 +39,5 @@ export function navigateToTask(
     { stringify: true },
   );
 
-  return globalNavigate(taskUrl);
+  return globalNavigate(sessionTaskUrl);
 }
diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx
index b1d77dcd759..9e0f3b3deed 100644
--- a/packages/clerk-js/src/ui/common/withRedirect.tsx
+++ b/packages/clerk-js/src/ui/common/withRedirect.tsx
@@ -61,7 +61,7 @@ export const withRedirectToAfterSignIn = <P extends AvailableComponentProps>(Com
     return withRedirect(
       Component,
       sessionExistsAndSingleSessionModeEnabled,
-      ({ clerk }) => signInCtx.taskUrl || signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(),
+      ({ clerk }) => signInCtx.sessionTaskUrl || signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(),
       warnings.cannotRenderSignInComponentWhenSessionExists,
     )(props);
   };
@@ -80,7 +80,7 @@ export const withRedirectToAfterSignUp = <P extends AvailableComponentProps>(Com
     return withRedirect(
       Component,
       sessionExistsAndSingleSessionModeEnabled,
-      ({ clerk }) => signUpCtx.taskUrl || signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(),
+      ({ clerk }) => signUpCtx.sessionTaskUrl || signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(),
       warnings.cannotRenderSignUpComponentWhenSessionExists,
     )(props);
   };
diff --git a/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx b/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx
index 0f0cf55fa6e..b869d62265e 100644
--- a/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx
+++ b/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx
@@ -1,13 +1,11 @@
+import { useClerk } from '@clerk/shared/react/index';
+import { eventComponentMounted } from '@clerk/shared/telemetry';
 import type { SessionTask } from '@clerk/types';
-import { type ComponentType } from 'react';
 
 import { OrganizationListContext } from '../../contexts';
 import { OrganizationList } from '../OrganizationList';
 
-/**
- * @internal
- */
-const SessionTaskRegistry: Record<SessionTask['key'], ComponentType> = {
+const ContentRegistry: Record<SessionTask['key'], React.ComponentType> = {
   org: () => (
     // TODO - Hide personal workspace within organization list context based on environment
     <OrganizationListContext.Provider value={{ componentName: 'OrganizationList', hidePersonal: true }}>
@@ -20,6 +18,11 @@ const SessionTaskRegistry: Record<SessionTask['key'], ComponentType> = {
  * @internal
  */
 export function SessionTask({ task }: { task: SessionTask['key'] }): React.ReactNode {
-  const Content = SessionTaskRegistry[task];
+  const clerk = useClerk();
+
+  clerk.telemetry?.record(eventComponentMounted('SessionTask', { task }));
+
+  const Content = ContentRegistry[task];
+
   return <Content />;
 }
diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
index 1ff0699bb43..1c4daeba125 100644
--- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
@@ -14,7 +14,7 @@ import {
 } from '../../contexts';
 import { Flow } from '../../customizables';
 import { useFetch } from '../../hooks';
-import { preloadSessionTask, SessionTask } from '../../lazyModules/components';
+import { preloadComponent, SessionTask } from '../../lazyModules/components';
 import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
 import {
   LazySignUpContinue,
@@ -163,7 +163,7 @@ const usePreloadSignUp = (enabled = false) =>
   useFetch(enabled ? preloadSignUp : undefined, 'preloadComponent', { staleTime: Infinity });
 
 const usePreloadSessionTask = (enabled = false) =>
-  useFetch(enabled ? preloadSessionTask : undefined, 'preloadComponent', { staleTime: Infinity });
+  useFetch(enabled ? void preloadComponent('SessionTask') : undefined, 'preloadComponent', { staleTime: Infinity });
 
 function SignInRoot() {
   const signInContext = useSignInContext();
diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
index 5086f413fba..810770e27f0 100644
--- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
@@ -6,7 +6,7 @@ import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowC
 import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts';
 import { Flow } from '../../customizables';
 import { useFetch } from '../../hooks';
-import { preloadSessionTask, SessionTask } from '../../lazyModules/components';
+import { preloadComponent, SessionTask } from '../../lazyModules/components';
 import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
 import { SignUpContinue } from './SignUpContinue';
 import { SignUpSSOCallback } from './SignUpSSOCallback';
@@ -15,7 +15,7 @@ import { SignUpVerifyEmail } from './SignUpVerifyEmail';
 import { SignUpVerifyPhone } from './SignUpVerifyPhone';
 
 const usePreloadSessionTask = (enabled = false) =>
-  useFetch(enabled ? preloadSessionTask : undefined, 'preloadComponent', { staleTime: Infinity });
+  useFetch(enabled ? void preloadComponent('SessionTask') : undefined, 'preloadComponent', { staleTime: Infinity });
 
 function RedirectToSignUp() {
   const clerk = useClerk();
diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
index 320da48584e..c8aead4f2c8 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
@@ -26,7 +26,7 @@ export type SignInContextType = SignInCtx & {
   authQueryString: string | null;
   afterSignUpUrl: string;
   afterSignInUrl: string;
-  taskUrl: string | null;
+  sessionTaskUrl: string | null;
   transferable: boolean;
   waitlistUrl: string;
   emailLinkRedirectUrl: string;
@@ -119,7 +119,7 @@ export const useSignInContext = (): SignInContextType => {
 
   const signUpContinueUrl = buildURL({ base: signUpUrl, hashPath: '/continue' }, { stringify: true });
 
-  const taskUrl = clerk.session?.currentTask
+  const sessionTaskUrl = clerk.session?.currentTask
     ? buildSessionTaskRedirectUrl({ routing: ctx.routing, path: ctx.path }, signInUrl, clerk.session?.currentTask)
     : null;
 
@@ -142,7 +142,7 @@ export const useSignInContext = (): SignInContextType => {
     afterSignUpUrl,
     emailLinkRedirectUrl,
     ssoCallbackUrl,
-    taskUrl,
+    sessionTaskUrl,
     withSessionTasks: options.experimental?.withSessionTasks,
     navigateAfterSignIn,
     signUpContinueUrl,
diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
index 660079551c4..992cfd4ad08 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
@@ -27,7 +27,7 @@ export type SignUpContextType = SignUpCtx & {
   afterSignUpUrl: string;
   afterSignInUrl: string;
   waitlistUrl: string;
-  taskUrl: string | null;
+  sessionTaskUrl: string | null;
   isCombinedFlow: boolean;
   emailLinkRedirectUrl: string;
   ssoCallbackUrl: string;
@@ -114,7 +114,7 @@ export const useSignUpContext = (): SignUpContextType => {
   // TODO: Avoid building this url again to remove duplicate code. Get it from window.Clerk instead.
   const secondFactorUrl = buildURL({ base: signInUrl, hashPath: '/factor-two' }, { stringify: true });
 
-  const taskUrl = clerk.session?.currentTask
+  const sessionTaskUrl = clerk.session?.currentTask
     ? buildSessionTaskRedirectUrl({ routing: ctx.routing, path: ctx.path }, signUpUrl, clerk.session?.currentTask)
     : null;
 
@@ -137,7 +137,7 @@ export const useSignUpContext = (): SignUpContextType => {
     afterSignInUrl,
     emailLinkRedirectUrl,
     ssoCallbackUrl,
-    taskUrl,
+    sessionTaskUrl,
     navigateAfterSignUp,
     queryParams,
     initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams },

From 250d69cfa172c966a31a0ec6e3cfc38d23e21606 Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Mon, 10 Mar 2025 18:42:30 -0300
Subject: [PATCH 07/16] Refactor internal routing to set on `BaseRouter`

---
 .../next-app-router/src/app/layout.tsx        |  3 ++-
 packages/clerk-js/src/core/clerk.ts           | 22 ++++++++++++++----
 packages/clerk-js/src/core/sessionTasks.ts    | 23 +++++++++++++++----
 .../src/ui/components/SignIn/SignIn.tsx       |  4 ++--
 .../src/ui/components/SignUp/SignUp.tsx       |  4 ++--
 .../src/ui/contexts/components/SignIn.ts      | 12 ++--------
 .../src/ui/contexts/components/SignUp.ts      | 12 ++--------
 .../clerk-js/src/ui/router/BaseRouter.tsx     |  6 ++++-
 packages/react/src/isomorphicClerk.ts         |  2 +-
 packages/types/src/clerk.ts                   | 11 ++++++---
 10 files changed, 60 insertions(+), 39 deletions(-)

diff --git a/integration/templates/next-app-router/src/app/layout.tsx b/integration/templates/next-app-router/src/app/layout.tsx
index 16e2fce74d5..f83c142ffcb 100644
--- a/integration/templates/next-app-router/src/app/layout.tsx
+++ b/integration/templates/next-app-router/src/app/layout.tsx
@@ -21,10 +21,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
         },
       }}
       experimental={{
-        withSessionTasks: true,
         persistClient: process.env.NEXT_PUBLIC_EXPERIMENTAL_PERSIST_CLIENT
           ? process.env.NEXT_PUBLIC_EXPERIMENTAL_PERSIST_CLIENT === 'true'
           : undefined,
+        // `withSessionTasks` will be removed soon in favor of checking via environment response
+        withSessionTasks: true,
       }}
     >
       <html lang='en'>
diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 6c1390eef18..dd8bb1449a8 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -200,7 +200,10 @@ export class Clerk implements ClerkInterface {
   #options: ClerkOptions = {};
   #pageLifecycle: ReturnType<typeof createPageLifecycle> | null = null;
   #touchThrottledUntil = 0;
-  #internalComponentNavigate: ((to: string) => Promise<unknown>) | null = null;
+  #componentNavigationContext: {
+    navigate: (toURL: URL | undefined) => Promise<unknown>;
+    basePath: string;
+  } | null = null;
 
   public __internal_getCachedResources:
     | (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>)
@@ -1076,12 +1079,14 @@ export class Clerk implements ClerkInterface {
     if (session.currentTask) {
       await navigateToTask(session.currentTask, {
         globalNavigate: this.navigate,
-        internalNavigate: this.#internalComponentNavigate,
+        componentNavigationContext: this.#componentNavigationContext,
         options: this.#options,
         environment: this.environment,
       });
 
-      this.#internalComponentNavigate = null;
+      // Reset component navigation context once navigation finishes
+      // to not conflict with after sign-in / after sign-up
+      this.#componentNavigationContext = null;
     }
 
     this.#setAccessors(session);
@@ -1115,8 +1120,15 @@ export class Clerk implements ClerkInterface {
     return unsubscribe;
   };
 
-  public __internal_setComponentNavigate = (navigate: (to: string) => Promise<unknown>) => {
-    this.#internalComponentNavigate = navigate;
+  public __internal_setComponentNavigationContext = (
+    context: {
+      navigate: (toURL: URL | undefined) => Promise<unknown>;
+      basePath: string;
+    } | null,
+  ) => {
+    this.#componentNavigationContext = context;
+
+    return () => (this.#componentNavigationContext = null);
   };
 
   public navigate = async (to: string | undefined, options?: NavigateOptions): Promise<unknown> => {
diff --git a/packages/clerk-js/src/core/sessionTasks.ts b/packages/clerk-js/src/core/sessionTasks.ts
index e7aece853e3..07ca4dc18d9 100644
--- a/packages/clerk-js/src/core/sessionTasks.ts
+++ b/packages/clerk-js/src/core/sessionTasks.ts
@@ -7,7 +7,10 @@ export const SESSION_TASK_ROUTE_BY_KEY: Record<SessionTask['key'], string> = {
 } as const;
 
 interface NavigateToTaskOptions {
-  internalNavigate: ((to: string) => Promise<unknown>) | null;
+  componentNavigationContext: {
+    basePath: string;
+    navigate: (toURL: URL | undefined) => Promise<unknown>;
+  } | null;
   globalNavigate: (to: string) => Promise<unknown>;
   options: ClerkOptions;
   environment: EnvironmentResource;
@@ -20,10 +23,22 @@ interface NavigateToTaskOptions {
  */
 export function navigateToTask(
   task: SessionTask,
-  { internalNavigate, globalNavigate, options, environment }: NavigateToTaskOptions,
+  { componentNavigationContext, globalNavigate, options, environment }: NavigateToTaskOptions,
 ) {
-  if (internalNavigate) {
-    return internalNavigate(SESSION_TASK_ROUTE_BY_KEY['org']);
+  if (componentNavigationContext) {
+    const isHashRouting = !!new URL(window.location.href).hash;
+    const taskUrl = buildURL({
+      base: componentNavigationContext.basePath,
+      ...(isHashRouting
+        ? {
+            hashPath: SESSION_TASK_ROUTE_BY_KEY[task.key],
+          }
+        : {
+            pathname: componentNavigationContext.basePath + SESSION_TASK_ROUTE_BY_KEY[task.key],
+          }),
+    }) as URL;
+
+    return componentNavigationContext.navigate(taskUrl);
   }
 
   const signInUrl = options['signInUrl'] || environment.displayConfig.signInUrl;
diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
index 1c4daeba125..1ff0699bb43 100644
--- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
@@ -14,7 +14,7 @@ import {
 } from '../../contexts';
 import { Flow } from '../../customizables';
 import { useFetch } from '../../hooks';
-import { preloadComponent, SessionTask } from '../../lazyModules/components';
+import { preloadSessionTask, SessionTask } from '../../lazyModules/components';
 import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
 import {
   LazySignUpContinue,
@@ -163,7 +163,7 @@ const usePreloadSignUp = (enabled = false) =>
   useFetch(enabled ? preloadSignUp : undefined, 'preloadComponent', { staleTime: Infinity });
 
 const usePreloadSessionTask = (enabled = false) =>
-  useFetch(enabled ? void preloadComponent('SessionTask') : undefined, 'preloadComponent', { staleTime: Infinity });
+  useFetch(enabled ? preloadSessionTask : undefined, 'preloadComponent', { staleTime: Infinity });
 
 function SignInRoot() {
   const signInContext = useSignInContext();
diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
index 810770e27f0..5086f413fba 100644
--- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
@@ -6,7 +6,7 @@ import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowC
 import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts';
 import { Flow } from '../../customizables';
 import { useFetch } from '../../hooks';
-import { preloadComponent, SessionTask } from '../../lazyModules/components';
+import { preloadSessionTask, SessionTask } from '../../lazyModules/components';
 import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
 import { SignUpContinue } from './SignUpContinue';
 import { SignUpSSOCallback } from './SignUpSSOCallback';
@@ -15,7 +15,7 @@ import { SignUpVerifyEmail } from './SignUpVerifyEmail';
 import { SignUpVerifyPhone } from './SignUpVerifyPhone';
 
 const usePreloadSessionTask = (enabled = false) =>
-  useFetch(enabled ? void preloadComponent('SessionTask') : undefined, 'preloadComponent', { staleTime: Infinity });
+  useFetch(enabled ? preloadSessionTask : undefined, 'preloadComponent', { staleTime: Infinity });
 
 function RedirectToSignUp() {
   const clerk = useClerk();
diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
index c8aead4f2c8..d8fbbb7e961 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
@@ -1,6 +1,6 @@
 import { useClerk } from '@clerk/shared/react';
 import { isAbsoluteUrl } from '@clerk/shared/url';
-import { createContext, useContext, useEffect, useMemo } from 'react';
+import { createContext, useContext, useMemo } from 'react';
 
 import { SIGN_IN_INITIAL_VALUE_KEYS } from '../../../core/constants';
 import { buildURL } from '../../../utils';
@@ -123,14 +123,6 @@ export const useSignInContext = (): SignInContextType => {
     ? buildSessionTaskRedirectUrl({ routing: ctx.routing, path: ctx.path }, signInUrl, clerk.session?.currentTask)
     : null;
 
-  useEffect(() => {
-    clerk.__internal_setComponentNavigate((endpoint: string) =>
-      navigate(
-        buildRedirectUrl({ routing: ctx.routing, path: ctx.path, baseUrl: signInUrl, endpoint, authQueryString }),
-      ),
-    );
-  }, []);
-
   return {
     ...(ctx as SignInCtx),
     transferable: ctx.transferable ?? true,
@@ -143,12 +135,12 @@ export const useSignInContext = (): SignInContextType => {
     emailLinkRedirectUrl,
     ssoCallbackUrl,
     sessionTaskUrl,
-    withSessionTasks: options.experimental?.withSessionTasks,
     navigateAfterSignIn,
     signUpContinueUrl,
     queryParams,
     initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams },
     authQueryString,
     isCombinedFlow,
+    withSessionTasks: !!options.experimental?.withSessionTasks,
   };
 };
diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
index 992cfd4ad08..3b16a4fd147 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
@@ -1,6 +1,6 @@
 import { useClerk } from '@clerk/shared/react';
 import { isAbsoluteUrl } from '@clerk/shared/url';
-import { createContext, useContext, useEffect, useMemo } from 'react';
+import { createContext, useContext, useMemo } from 'react';
 
 import { SIGN_UP_INITIAL_VALUE_KEYS } from '../../../core/constants';
 import { buildURL } from '../../../utils';
@@ -118,14 +118,6 @@ export const useSignUpContext = (): SignUpContextType => {
     ? buildSessionTaskRedirectUrl({ routing: ctx.routing, path: ctx.path }, signUpUrl, clerk.session?.currentTask)
     : null;
 
-  useEffect(() => {
-    clerk.__internal_setComponentNavigate((endpoint: string) =>
-      navigate(
-        buildRedirectUrl({ routing: ctx.routing, path: ctx.path, baseUrl: signUpUrl, endpoint, authQueryString }),
-      ),
-    );
-  }, []);
-
   return {
     ...ctx,
     componentName,
@@ -143,6 +135,6 @@ export const useSignUpContext = (): SignUpContextType => {
     initialValues: { ...ctx.initialValues, ...initialValuesFromQueryParams },
     authQueryString,
     isCombinedFlow,
-    withSessionTasks: options.experimental?.withSessionTasks,
+    withSessionTasks: !!options.experimental?.withSessionTasks,
   };
 };
diff --git a/packages/clerk-js/src/ui/router/BaseRouter.tsx b/packages/clerk-js/src/ui/router/BaseRouter.tsx
index 1874116aa04..c13d0d17b04 100644
--- a/packages/clerk-js/src/ui/router/BaseRouter.tsx
+++ b/packages/clerk-js/src/ui/router/BaseRouter.tsx
@@ -40,7 +40,7 @@ export const BaseRouter = ({
 }: BaseRouterProps): JSX.Element => {
   // Disabling is acceptable since this is a Router component
   // eslint-disable-next-line custom-rules/no-navigate-useClerk
-  const { navigate: clerkNavigate } = useClerk();
+  const { navigate: clerkNavigate, __internal_setComponentNavigationContext } = useClerk();
 
   const [routeParts, setRouteParts] = React.useState({
     path: getPath(),
@@ -121,6 +121,10 @@ export const BaseRouter = ({
     return internalNavRes;
   };
 
+  React.useEffect(() => {
+    return __internal_setComponentNavigationContext?.({ basePath, navigate: baseNavigate });
+  }, []);
+
   return (
     <RouteContext.Provider
       value={{
diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts
index 7db4b79b573..c2b85093152 100644
--- a/packages/react/src/isomorphicClerk.ts
+++ b/packages/react/src/isomorphicClerk.ts
@@ -93,7 +93,7 @@ type IsomorphicLoadedClerk = Without<
   | '__internal_getCachedResources'
   | '__internal_reloadInitialResources'
   | '__experimental_commerce'
-  | '__internal_setComponentNavigate'
+  | '__internal_setComponentNavigationContext'
 > & {
   client: ClientResource | undefined;
   __experimental_commerce: __experimental_CommerceNamespace | undefined;
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index f3c777a000b..66b1ddffe7a 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -427,11 +427,16 @@ export interface Clerk {
   __internal_addNavigationListener: (callback: () => void) => UnsubscribeCallback;
 
   /**
-   * Registers an internal navigate function for UI components in order to be triggered
-   * from `Clerk`
+   * Registers the internal navigation context from UI components in order to
+   * be triggered from `Clerk` methods
    * @internal
    */
-  __internal_setComponentNavigate: (navigate: (to: string) => Promise<unknown>) => void;
+  __internal_setComponentNavigationContext: (
+    context: {
+      navigate: (toURL: URL | undefined) => Promise<unknown>;
+      basePath: string;
+    } | null,
+  ) => () => void;
 
   /**
    * Set the active session and organization explicitly.

From f27b7c25bd2251d516ff96e366d11b0efe7d0717 Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Tue, 11 Mar 2025 10:37:33 -0300
Subject: [PATCH 08/16] Refactor integration tests to use use long running apps

---
 .../tests/session-tasks-sign-in.test.ts       | 59 +++++++++----------
 .../tests/session-tasks-sign-up.test.ts       | 58 ++++++++----------
 packages/clerk-js/bundlewatch.config.json     |  3 +-
 3 files changed, 56 insertions(+), 64 deletions(-)

diff --git a/integration/tests/session-tasks-sign-in.test.ts b/integration/tests/session-tasks-sign-in.test.ts
index 8e4ae9e7a2b..eebc35002c1 100644
--- a/integration/tests/session-tasks-sign-in.test.ts
+++ b/integration/tests/session-tasks-sign-in.test.ts
@@ -1,41 +1,38 @@
 import { expect, test } from '@playwright/test';
 
-import type { Application } from '../models/application';
 import { appConfigs } from '../presets';
 import type { FakeUser } from '../testUtils';
-import { createTestUtils } from '../testUtils';
+import { createTestUtils, testAgainstRunningApps } from '../testUtils';
 
-test.describe('session tasks after sign-in flow @nextjs', () => {
-  test.describe.configure({ mode: 'serial' });
-  let app: Application;
-  let fakeUser: FakeUser;
+testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
+  'session tasks after sign-in flow @nextjs',
+  ({ app }) => {
+    test.describe.configure({ mode: 'serial' });
 
-  test.beforeAll(async () => {
-    app = await appConfigs.next.appRouter.clone().commit();
-    await app.setup();
-    await app.withEnv(appConfigs.envs.withSessionTasks);
-    await app.dev();
+    let fakeUser: FakeUser;
 
-    const m = createTestUtils({ app });
-    fakeUser = m.services.users.createFakeUser();
-    await m.services.users.createBapiUser(fakeUser);
-  });
+    test.beforeAll(async () => {
+      const u = createTestUtils({ app });
+      fakeUser = u.services.users.createFakeUser();
+      await u.services.users.createBapiUser(fakeUser);
+    });
 
-  test.afterAll(async () => {
-    await fakeUser.deleteIfExists();
-    await app.teardown();
-  });
+    test.afterAll(async () => {
+      await fakeUser.deleteIfExists();
+      await app.teardown();
+    });
 
-  test('navigate to task on after sign-in', async ({ page, context }) => {
-    const u = createTestUtils({ app, page, context });
-    await u.po.signIn.goTo();
-    await u.po.signIn.setIdentifier(fakeUser.email);
-    await u.po.signIn.continue();
-    await u.po.signIn.setPassword(fakeUser.password);
-    await u.po.signIn.continue();
-    await u.po.expect.toBeSignedIn();
+    test('navigate to task on after sign-in', async ({ page, context }) => {
+      const u = createTestUtils({ app, page, context });
+      await u.po.signIn.goTo();
+      await u.po.signIn.setIdentifier(fakeUser.email);
+      await u.po.signIn.continue();
+      await u.po.signIn.setPassword(fakeUser.password);
+      await u.po.signIn.continue();
+      await u.po.expect.toBeSignedIn();
 
-    await expect(u.page.getByRole('button', { name: /create organization/i })).toBeVisible();
-    expect(page.url()).toContain('add-organization');
-  });
-});
+      await expect(u.page.getByRole('button', { name: /create organization/i })).toBeVisible();
+      expect(page.url()).toContain('add-organization');
+    });
+  },
+);
diff --git a/integration/tests/session-tasks-sign-up.test.ts b/integration/tests/session-tasks-sign-up.test.ts
index 6270b8967a1..ad5003eb2f3 100644
--- a/integration/tests/session-tasks-sign-up.test.ts
+++ b/integration/tests/session-tasks-sign-up.test.ts
@@ -1,40 +1,34 @@
 import { expect, test } from '@playwright/test';
 
-import type { Application } from '../models/application';
 import { appConfigs } from '../presets';
-import { createTestUtils } from '../testUtils';
+import { createTestUtils, testAgainstRunningApps } from '../testUtils';
 
-test.describe('session tasks after sign-up flow @nextjs', () => {
-  test.describe.configure({ mode: 'serial' });
-  let app: Application;
+testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
+  'session tasks after sign-up flow @nextjs',
+  ({ app }) => {
+    test.describe.configure({ mode: 'serial' });
 
-  test.beforeAll(async () => {
-    app = await appConfigs.next.appRouter.clone().commit();
-    await app.setup();
-    await app.withEnv(appConfigs.envs.withSessionTasks);
-    await app.dev();
-  });
-
-  test.afterAll(async () => {
-    await app.teardown();
-  });
-
-  test('navigate to task on after sign-up', async ({ page, context }) => {
-    const u = createTestUtils({ app, page, context });
-    const fakeUser = u.services.users.createFakeUser({
-      fictionalEmail: true,
-      withPhoneNumber: true,
-      withUsername: true,
-    });
-    await u.po.signUp.goTo();
-    await u.po.signUp.signUpWithEmailAndPassword({
-      email: fakeUser.email,
-      password: fakeUser.password,
+    test.afterAll(async () => {
+      await app.teardown();
     });
 
-    await expect(u.page.getByRole('button', { name: /create organization/i })).toBeVisible();
-    expect(page.url()).toContain('add-organization');
+    test('navigate to task on after sign-up', async ({ page, context }) => {
+      const u = createTestUtils({ app, page, context });
+      const fakeUser = u.services.users.createFakeUser({
+        fictionalEmail: true,
+        withPhoneNumber: true,
+        withUsername: true,
+      });
+      await u.po.signUp.goTo();
+      await u.po.signUp.signUpWithEmailAndPassword({
+        email: fakeUser.email,
+        password: fakeUser.password,
+      });
 
-    await fakeUser.deleteIfExists();
-  });
-});
+      await expect(u.page.getByRole('button', { name: /create organization/i })).toBeVisible();
+      expect(page.url()).toContain('add-organization');
+
+      await fakeUser.deleteIfExists();
+    });
+  },
+);
diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index f6f8251ad34..f62025992aa 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -1,5 +1,6 @@
 {
   "files": [
+    { "path": "./dist/clerk.js", "maxSize": "570kB" },
     { "path": "./dist/clerk.browser.js", "maxSize": "76kB" },
     { "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
     { "path": "./dist/ui-common*.js", "maxSize": "92KB" },
@@ -11,7 +12,7 @@
     { "path": "./dist/organizationswitcher*.js", "maxSize": "5KB" },
     { "path": "./dist/organizationlist*.js", "maxSize": "5.5KB" },
     { "path": "./dist/signin*.js", "maxSize": "12.4KB" },
-    { "path": "./dist/signup*.js", "maxSize": "6.4KB" },
+    { "path": "./dist/signup*.js", "maxSize": "6.5KB" },
     { "path": "./dist/userbutton*.js", "maxSize": "5KB" },
     { "path": "./dist/userprofile*.js", "maxSize": "15KB" },
     { "path": "./dist/userverification*.js", "maxSize": "5KB" },

From bb6c8dd1daf8673047a83bb915291129f4c76bac Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Tue, 11 Mar 2025 19:03:37 -0300
Subject: [PATCH 09/16] Remove unused `TokenUpdate` dispatch

---
 packages/clerk-js/src/core/clerk.ts | 4 ----
 1 file changed, 4 deletions(-)

diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index dd8bb1449a8..07135756734 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -1058,10 +1058,6 @@ export class Clerk implements ClerkInterface {
       return;
     }
 
-    if (session.lastActiveToken) {
-      eventBus.dispatch(events.TokenUpdate, { token: session.lastActiveToken });
-    }
-
     // Handles multi-session scenario when switching from `active`
     // to `pending`
     if (inActiveBrowserTab() || !this.#options.standardBrowser) {

From 68c2a041c2d6bf69f4bf5c6f924d53cd96220cfc Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Tue, 11 Mar 2025 19:49:28 -0300
Subject: [PATCH 10/16] Refactor internal routing logic

- Use the typed constant when declaring Route components to catch breaking changes on `path` changes
- Call `setComponentNavigationContext` within SignIn/SignUp root components to trigger cleanup on unmount, instead of relying on `BaseRouter`
- Remove cleanup for `Clerk.#componentNavigationContext` withi `Clerk.#handlePendingSession`
---
 packages/clerk-js/src/core/clerk.ts           | 18 ++++++++-----
 packages/clerk-js/src/core/sessionTasks.ts    | 27 ++++++++-----------
 packages/clerk-js/src/ui/common/redirects.ts  | 20 +++++++++-----
 .../src/ui/components/SignIn/SignIn.tsx       | 14 +++++++---
 .../src/ui/components/SignUp/SignUp.tsx       | 11 ++++++--
 .../src/ui/contexts/components/SignIn.ts      |  9 ++++---
 .../src/ui/contexts/components/SignUp.ts      |  9 ++++---
 .../clerk-js/src/ui/router/BaseRouter.tsx     |  6 +----
 packages/types/src/clerk.ts                   |  7 ++++-
 9 files changed, 75 insertions(+), 46 deletions(-)

diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 07135756734..936743ff582 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -201,7 +201,12 @@ export class Clerk implements ClerkInterface {
   #pageLifecycle: ReturnType<typeof createPageLifecycle> | null = null;
   #touchThrottledUntil = 0;
   #componentNavigationContext: {
-    navigate: (toURL: URL | undefined) => Promise<unknown>;
+    navigate: (
+      to: string,
+      options?: {
+        searchParams?: URLSearchParams;
+      },
+    ) => Promise<unknown>;
     basePath: string;
   } | null = null;
 
@@ -1079,10 +1084,6 @@ export class Clerk implements ClerkInterface {
         options: this.#options,
         environment: this.environment,
       });
-
-      // Reset component navigation context once navigation finishes
-      // to not conflict with after sign-in / after sign-up
-      this.#componentNavigationContext = null;
     }
 
     this.#setAccessors(session);
@@ -1118,7 +1119,12 @@ export class Clerk implements ClerkInterface {
 
   public __internal_setComponentNavigationContext = (
     context: {
-      navigate: (toURL: URL | undefined) => Promise<unknown>;
+      navigate: (
+        to: string,
+        options?: {
+          searchParams?: URLSearchParams;
+        },
+      ) => Promise<unknown>;
       basePath: string;
     } | null,
   ) => {
diff --git a/packages/clerk-js/src/core/sessionTasks.ts b/packages/clerk-js/src/core/sessionTasks.ts
index 07ca4dc18d9..29d7d9e0481 100644
--- a/packages/clerk-js/src/core/sessionTasks.ts
+++ b/packages/clerk-js/src/core/sessionTasks.ts
@@ -3,13 +3,18 @@ import type { ClerkOptions, EnvironmentResource, SessionTask } from '@clerk/type
 import { buildURL } from '../utils';
 
 export const SESSION_TASK_ROUTE_BY_KEY: Record<SessionTask['key'], string> = {
-  org: '/add-organization',
+  org: 'add-organization',
 } as const;
 
 interface NavigateToTaskOptions {
   componentNavigationContext: {
+    navigate: (
+      to: string,
+      options?: {
+        searchParams?: URLSearchParams;
+      },
+    ) => Promise<unknown>;
     basePath: string;
-    navigate: (toURL: URL | undefined) => Promise<unknown>;
   } | null;
   globalNavigate: (to: string) => Promise<unknown>;
   options: ClerkOptions;
@@ -25,20 +30,10 @@ export function navigateToTask(
   task: SessionTask,
   { componentNavigationContext, globalNavigate, options, environment }: NavigateToTaskOptions,
 ) {
+  const taskRoute = `/${SESSION_TASK_ROUTE_BY_KEY[task.key]}`;
+
   if (componentNavigationContext) {
-    const isHashRouting = !!new URL(window.location.href).hash;
-    const taskUrl = buildURL({
-      base: componentNavigationContext.basePath,
-      ...(isHashRouting
-        ? {
-            hashPath: SESSION_TASK_ROUTE_BY_KEY[task.key],
-          }
-        : {
-            pathname: componentNavigationContext.basePath + SESSION_TASK_ROUTE_BY_KEY[task.key],
-          }),
-    }) as URL;
-
-    return componentNavigationContext.navigate(taskUrl);
+    return componentNavigationContext.navigate(`/${componentNavigationContext.basePath + taskRoute}`);
   }
 
   const signInUrl = options['signInUrl'] || environment.displayConfig.signInUrl;
@@ -49,7 +44,7 @@ export function navigateToTask(
     // TODO - Accept custom URL option for custom flows in order to eject out of `signInUrl/signUpUrl`
     {
       base: isReferrerSignUpUrl ? signUpUrl : signInUrl,
-      hashPath: SESSION_TASK_ROUTE_BY_KEY[task.key],
+      hashPath: taskRoute,
     },
     { stringify: true },
   );
diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts
index add5954cfeb..6b785c9d694 100644
--- a/packages/clerk-js/src/ui/common/redirects.ts
+++ b/packages/clerk-js/src/ui/common/redirects.ts
@@ -28,18 +28,24 @@ export function buildVerificationRedirectUrl({
   });
 }
 
-export function buildSessionTaskRedirectUrl(
-  ctx: Pick<SignInContextType | SignUpContextType, 'routing' | 'path'>,
-  baseUrl: string,
-  task: SessionTask,
-) {
-  const { routing, path } = ctx;
+export function buildSessionTaskRedirectUrl({
+  routing,
+  path,
+  baseUrl,
+  task,
+}: Pick<SignInContextType | SignUpContextType, 'routing' | 'path'> & {
+  baseUrl: string;
+  task?: SessionTask;
+}) {
+  if (!task) {
+    return null;
+  }
 
   return buildRedirectUrl({
     routing,
     baseUrl,
     path,
-    endpoint: SESSION_TASK_ROUTE_BY_KEY[task.key],
+    endpoint: `/${SESSION_TASK_ROUTE_BY_KEY[task.key]}`,
     authQueryString: null,
   });
 }
diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
index 1ff0699bb43..0cbb5a25cc0 100644
--- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
@@ -2,6 +2,7 @@ import { useClerk } from '@clerk/shared/react';
 import type { SignInModalProps, SignInProps } from '@clerk/types';
 import React from 'react';
 
+import { SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks';
 import { normalizeRoutingOptions } from '../../../utils/normalizeRoutingOptions';
 import { SignInEmailLinkFlowComplete, SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard';
 import type { SignUpContextType } from '../../contexts';
@@ -15,7 +16,7 @@ import {
 import { Flow } from '../../customizables';
 import { useFetch } from '../../hooks';
 import { preloadSessionTask, SessionTask } from '../../lazyModules/components';
-import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
+import { Route, Switch, useRouter, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
 import {
   LazySignUpContinue,
   LazySignUpSSOCallback,
@@ -130,7 +131,7 @@ function SignInRoutes(): JSX.Element {
                 <LazySignUpVerifyPhone />
               </Route>
               {signInContext.withSessionTasks && (
-                <Route path='add-organization'>
+                <Route path={SESSION_TASK_ROUTE_BY_KEY['org']}>
                   <SessionTask task='org' />
                 </Route>
               )}
@@ -144,7 +145,7 @@ function SignInRoutes(): JSX.Element {
           </Route>
         )}
         {signInContext.withSessionTasks && (
-          <Route path='add-organization'>
+          <Route path={SESSION_TASK_ROUTE_BY_KEY['org']}>
             <SessionTask task='org' />
           </Route>
         )}
@@ -166,6 +167,9 @@ const usePreloadSessionTask = (enabled = false) =>
   useFetch(enabled ? preloadSessionTask : undefined, 'preloadComponent', { staleTime: Infinity });
 
 function SignInRoot() {
+  const { __internal_setComponentNavigationContext } = useClerk();
+  const { navigate, basePath } = useRouter();
+
   const signInContext = useSignInContext();
   const normalizedSignUpContext = {
     componentName: 'SignUp',
@@ -185,6 +189,10 @@ function SignInRoot() {
 
   usePreloadSessionTask(signInContext.withSessionTasks);
 
+  React.useEffect(() => {
+    return __internal_setComponentNavigationContext?.({ basePath, navigate });
+  }, []);
+
   return (
     <SignUpContext.Provider value={normalizedSignUpContext}>
       <SignInRoutes />
diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
index 5086f413fba..a3ed31c64f4 100644
--- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
@@ -2,12 +2,13 @@ import { useClerk } from '@clerk/shared/react';
 import type { SignUpModalProps, SignUpProps } from '@clerk/types';
 import React from 'react';
 
+import { SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks';
 import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard';
 import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts';
 import { Flow } from '../../customizables';
 import { useFetch } from '../../hooks';
 import { preloadSessionTask, SessionTask } from '../../lazyModules/components';
-import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
+import { Route, Switch, useRouter, VIRTUAL_ROUTER_BASE_PATH } from '../../router';
 import { SignUpContinue } from './SignUpContinue';
 import { SignUpSSOCallback } from './SignUpSSOCallback';
 import { SignUpStart } from './SignUpStart';
@@ -26,10 +27,16 @@ function RedirectToSignUp() {
 }
 
 function SignUpRoutes(): JSX.Element {
+  const { __internal_setComponentNavigationContext } = useClerk();
+  const { navigate, basePath } = useRouter();
   const signUpContext = useSignUpContext();
 
   usePreloadSessionTask(signUpContext.withSessionTasks);
 
+  React.useEffect(() => {
+    return __internal_setComponentNavigationContext?.({ basePath, navigate });
+  }, []);
+
   return (
     <Flow.Root flow='signUp'>
       <Switch>
@@ -82,7 +89,7 @@ function SignUpRoutes(): JSX.Element {
           </Route>
         </Route>
         {signUpContext.withSessionTasks && (
-          <Route path='add-organization'>
+          <Route path={SESSION_TASK_ROUTE_BY_KEY['org']}>
             <SessionTask task='org' />
           </Route>
         )}
diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
index d8fbbb7e961..d709a0d14b4 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts
@@ -119,9 +119,12 @@ export const useSignInContext = (): SignInContextType => {
 
   const signUpContinueUrl = buildURL({ base: signUpUrl, hashPath: '/continue' }, { stringify: true });
 
-  const sessionTaskUrl = clerk.session?.currentTask
-    ? buildSessionTaskRedirectUrl({ routing: ctx.routing, path: ctx.path }, signInUrl, clerk.session?.currentTask)
-    : null;
+  const sessionTaskUrl = buildSessionTaskRedirectUrl({
+    task: clerk.session?.currentTask,
+    path: ctx.path,
+    routing: ctx.routing,
+    baseUrl: signInUrl,
+  });
 
   return {
     ...(ctx as SignInCtx),
diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
index 3b16a4fd147..59ce5f4ab09 100644
--- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts
+++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts
@@ -114,9 +114,12 @@ export const useSignUpContext = (): SignUpContextType => {
   // TODO: Avoid building this url again to remove duplicate code. Get it from window.Clerk instead.
   const secondFactorUrl = buildURL({ base: signInUrl, hashPath: '/factor-two' }, { stringify: true });
 
-  const sessionTaskUrl = clerk.session?.currentTask
-    ? buildSessionTaskRedirectUrl({ routing: ctx.routing, path: ctx.path }, signUpUrl, clerk.session?.currentTask)
-    : null;
+  const sessionTaskUrl = buildSessionTaskRedirectUrl({
+    task: clerk.session?.currentTask,
+    path: ctx.path,
+    routing: ctx.routing,
+    baseUrl: signUpUrl,
+  });
 
   return {
     ...ctx,
diff --git a/packages/clerk-js/src/ui/router/BaseRouter.tsx b/packages/clerk-js/src/ui/router/BaseRouter.tsx
index c13d0d17b04..1874116aa04 100644
--- a/packages/clerk-js/src/ui/router/BaseRouter.tsx
+++ b/packages/clerk-js/src/ui/router/BaseRouter.tsx
@@ -40,7 +40,7 @@ export const BaseRouter = ({
 }: BaseRouterProps): JSX.Element => {
   // Disabling is acceptable since this is a Router component
   // eslint-disable-next-line custom-rules/no-navigate-useClerk
-  const { navigate: clerkNavigate, __internal_setComponentNavigationContext } = useClerk();
+  const { navigate: clerkNavigate } = useClerk();
 
   const [routeParts, setRouteParts] = React.useState({
     path: getPath(),
@@ -121,10 +121,6 @@ export const BaseRouter = ({
     return internalNavRes;
   };
 
-  React.useEffect(() => {
-    return __internal_setComponentNavigationContext?.({ basePath, navigate: baseNavigate });
-  }, []);
-
   return (
     <RouteContext.Provider
       value={{
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 66b1ddffe7a..3e33a9a59e8 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -433,7 +433,12 @@ export interface Clerk {
    */
   __internal_setComponentNavigationContext: (
     context: {
-      navigate: (toURL: URL | undefined) => Promise<unknown>;
+      navigate: (
+        to: string,
+        options?: {
+          searchParams?: URLSearchParams;
+        },
+      ) => Promise<unknown>;
       basePath: string;
     } | null,
   ) => () => void;

From a6fdfd5e9c37532ae74786d12f114fefeb861eb3 Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Tue, 11 Mar 2025 20:56:30 -0300
Subject: [PATCH 11/16] Remove unused `null` from type

---
 packages/clerk-js/src/core/clerk.ts | 20 +++++++++-----------
 packages/types/src/clerk.ts         | 20 +++++++++-----------
 2 files changed, 18 insertions(+), 22 deletions(-)

diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts
index 936743ff582..32f0fb7ece7 100644
--- a/packages/clerk-js/src/core/clerk.ts
+++ b/packages/clerk-js/src/core/clerk.ts
@@ -1117,17 +1117,15 @@ export class Clerk implements ClerkInterface {
     return unsubscribe;
   };
 
-  public __internal_setComponentNavigationContext = (
-    context: {
-      navigate: (
-        to: string,
-        options?: {
-          searchParams?: URLSearchParams;
-        },
-      ) => Promise<unknown>;
-      basePath: string;
-    } | null,
-  ) => {
+  public __internal_setComponentNavigationContext = (context: {
+    navigate: (
+      to: string,
+      options?: {
+        searchParams?: URLSearchParams;
+      },
+    ) => Promise<unknown>;
+    basePath: string;
+  }) => {
     this.#componentNavigationContext = context;
 
     return () => (this.#componentNavigationContext = null);
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 3e33a9a59e8..3a24769af92 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -431,17 +431,15 @@ export interface Clerk {
    * be triggered from `Clerk` methods
    * @internal
    */
-  __internal_setComponentNavigationContext: (
-    context: {
-      navigate: (
-        to: string,
-        options?: {
-          searchParams?: URLSearchParams;
-        },
-      ) => Promise<unknown>;
-      basePath: string;
-    } | null,
-  ) => () => void;
+  __internal_setComponentNavigationContext: (context: {
+    navigate: (
+      to: string,
+      options?: {
+        searchParams?: URLSearchParams;
+      },
+    ) => Promise<unknown>;
+    basePath: string;
+  }) => () => void;
 
   /**
    * Set the active session and organization explicitly.

From 8f8fd26ada1199b90f3b54ebefb556f0e9a4f64a Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Tue, 11 Mar 2025 20:58:54 -0300
Subject: [PATCH 12/16] Add comment to clarify temporary
 `experimental.withSessionTasks`

---
 integration/templates/next-app-router/src/app/layout.tsx | 2 +-
 packages/clerk-js/src/ui/components/SignIn/SignIn.tsx    | 1 +
 packages/clerk-js/src/ui/components/SignUp/SignUp.tsx    | 1 +
 packages/types/src/clerk.ts                              | 1 +
 4 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/integration/templates/next-app-router/src/app/layout.tsx b/integration/templates/next-app-router/src/app/layout.tsx
index f83c142ffcb..e598334db29 100644
--- a/integration/templates/next-app-router/src/app/layout.tsx
+++ b/integration/templates/next-app-router/src/app/layout.tsx
@@ -24,7 +24,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
         persistClient: process.env.NEXT_PUBLIC_EXPERIMENTAL_PERSIST_CLIENT
           ? process.env.NEXT_PUBLIC_EXPERIMENTAL_PERSIST_CLIENT === 'true'
           : undefined,
-        // `withSessionTasks` will be removed soon in favor of checking via environment response
+        // `experimental.withSessionTasks` will be removed soon in favor of checking via environment response
         withSessionTasks: true,
       }}
     >
diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
index 0cbb5a25cc0..2b0368e831b 100644
--- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
@@ -187,6 +187,7 @@ function SignInRoot() {
    */
   usePreloadSignUp(signInContext.isCombinedFlow);
 
+  // `experimental.withSessionTasks` will be removed soon in favor of checking via environment response
   usePreloadSessionTask(signInContext.withSessionTasks);
 
   React.useEffect(() => {
diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
index a3ed31c64f4..adfe0c8d4e7 100644
--- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
@@ -31,6 +31,7 @@ function SignUpRoutes(): JSX.Element {
   const { navigate, basePath } = useRouter();
   const signUpContext = useSignUpContext();
 
+  // `experimental.withSessionTasks` will be removed soon in favor of checking via environment response
   usePreloadSessionTask(signUpContext.withSessionTasks);
 
   React.useEffect(() => {
diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts
index 3a24769af92..e1366319454 100644
--- a/packages/types/src/clerk.ts
+++ b/packages/types/src/clerk.ts
@@ -818,6 +818,7 @@ export type ClerkOptions = ClerkOptionsNavigation &
          */
         rethrowOfflineNetworkErrors: boolean;
         commerce: boolean;
+        // `experimental.withSessionTasks` will be removed soon in favor of checking via environment response
         withSessionTasks: boolean;
       },
       Record<string, any>

From 48179a460a4389581218802bf111fc15c1cd1111 Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Wed, 12 Mar 2025 14:10:12 -0300
Subject: [PATCH 13/16] Update effect call to re-run via dependencies

---
 packages/clerk-js/src/ui/components/SignIn/SignIn.tsx | 2 +-
 packages/clerk-js/src/ui/components/SignUp/SignUp.tsx | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
index 2b0368e831b..2a5895836f0 100644
--- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
+++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx
@@ -192,7 +192,7 @@ function SignInRoot() {
 
   React.useEffect(() => {
     return __internal_setComponentNavigationContext?.({ basePath, navigate });
-  }, []);
+  }, [basePath, navigate]);
 
   return (
     <SignUpContext.Provider value={normalizedSignUpContext}>
diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
index adfe0c8d4e7..9e42f7efac8 100644
--- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
+++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx
@@ -36,7 +36,7 @@ function SignUpRoutes(): JSX.Element {
 
   React.useEffect(() => {
     return __internal_setComponentNavigationContext?.({ basePath, navigate });
-  }, []);
+  }, [basePath, navigate]);
 
   return (
     <Flow.Root flow='signUp'>

From c95edabc666b0ad51e3bae7d2f76aef88dc26ef3 Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Wed, 12 Mar 2025 15:59:13 -0300
Subject: [PATCH 14/16] Update changeset to include `clerk-react`

---
 .changeset/brave-pears-add.md | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.changeset/brave-pears-add.md b/.changeset/brave-pears-add.md
index ce2ecccbfc2..74870af7af9 100644
--- a/.changeset/brave-pears-add.md
+++ b/.changeset/brave-pears-add.md
@@ -1,6 +1,7 @@
 ---
 '@clerk/clerk-js': minor
 '@clerk/types': minor
+'@clerk/clerk-react': minor
 ---
 
 Navigate to tasks on after sign-in/sign-up

From 16d9332802a73b7b4e58f3e54996328bff3d8741 Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Wed, 12 Mar 2025 18:50:44 -0300
Subject: [PATCH 15/16] Update bundlewatch based on latest rebase

---
 packages/clerk-js/bundlewatch.config.json | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json
index f62025992aa..d5015e80235 100644
--- a/packages/clerk-js/bundlewatch.config.json
+++ b/packages/clerk-js/bundlewatch.config.json
@@ -1,7 +1,7 @@
 {
   "files": [
-    { "path": "./dist/clerk.js", "maxSize": "570kB" },
-    { "path": "./dist/clerk.browser.js", "maxSize": "76kB" },
+    { "path": "./dist/clerk.js", "maxSize": "572kB" },
+    { "path": "./dist/clerk.browser.js", "maxSize": "78kB" },
     { "path": "./dist/clerk.headless.js", "maxSize": "50KB" },
     { "path": "./dist/ui-common*.js", "maxSize": "92KB" },
     { "path": "./dist/vendors*.js", "maxSize": "26.5KB" },

From b10bf3fc0095a79f2d7eff18d8c4d2ba761641da Mon Sep 17 00:00:00 2001
From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com>
Date: Wed, 12 Mar 2025 19:04:45 -0300
Subject: [PATCH 16/16] Update redirect guard warning based on task URL

---
 packages/clerk-js/src/core/warnings.ts           | 4 ++++
 packages/clerk-js/src/ui/common/withRedirect.tsx | 8 ++++++--
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/packages/clerk-js/src/core/warnings.ts b/packages/clerk-js/src/core/warnings.ts
index 1fd500a1e91..2513a4d00cc 100644
--- a/packages/clerk-js/src/core/warnings.ts
+++ b/packages/clerk-js/src/core/warnings.ts
@@ -16,8 +16,12 @@ const warnings = {
     '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.',
   cannotRenderSignUpComponentWhenSessionExists:
     '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.',
+  cannotRenderSignUpComponentWhenTaskExists:
+    '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.',
   cannotRenderSignInComponentWhenSessionExists:
     '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.',
+  cannotRenderSignInComponentWhenTaskExists:
+    'The <SignIn/> 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.',
   cannotRenderComponentWhenUserDoesNotExist:
     '<UserProfile/> cannot render unless a user is signed in. Since no user is signed in, this is no-op.',
   cannotRenderComponentWhenOrgDoesNotExist: `<OrganizationProfile/> cannot render unless an organization is active. Since no organization is currently active, this is no-op.`,
diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx
index 9e0f3b3deed..81336a63ec2 100644
--- a/packages/clerk-js/src/ui/common/withRedirect.tsx
+++ b/packages/clerk-js/src/ui/common/withRedirect.tsx
@@ -62,7 +62,9 @@ export const withRedirectToAfterSignIn = <P extends AvailableComponentProps>(Com
       Component,
       sessionExistsAndSingleSessionModeEnabled,
       ({ clerk }) => signInCtx.sessionTaskUrl || signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(),
-      warnings.cannotRenderSignInComponentWhenSessionExists,
+      signInCtx.sessionTaskUrl
+        ? warnings.cannotRenderSignInComponentWhenTaskExists
+        : warnings.cannotRenderSignInComponentWhenSessionExists,
     )(props);
   };
 
@@ -81,7 +83,9 @@ export const withRedirectToAfterSignUp = <P extends AvailableComponentProps>(Com
       Component,
       sessionExistsAndSingleSessionModeEnabled,
       ({ clerk }) => signUpCtx.sessionTaskUrl || signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(),
-      warnings.cannotRenderSignUpComponentWhenSessionExists,
+      signUpCtx.sessionTaskUrl
+        ? warnings.cannotRenderSignUpComponentWhenTaskExists
+        : warnings.cannotRenderSignUpComponentWhenSessionExists,
     )(props);
   };