From 728440bf67eabacaeb1f2fc1b1ed6a961d89a1fb Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Mon, 21 Jul 2025 21:07:15 -0300 Subject: [PATCH 1/5] Add `tasksUrl` as option --- .changeset/whole-knives-attend.md | 16 ++++++++++++++++ .../clerk-js/src/core/__tests__/clerk.test.ts | 19 +++++++++++++++++-- packages/clerk-js/src/core/clerk.ts | 4 ++-- packages/clerk-js/src/core/sessionTasks.ts | 7 +++---- packages/clerk-js/src/ui/common/redirects.ts | 8 +++++--- .../src/ui/components/SessionTasks/index.tsx | 4 ++-- .../src/ui/contexts/components/SignIn.ts | 1 + .../src/ui/contexts/components/SignUp.ts | 1 + packages/types/src/clerk.ts | 12 ++++++++++-- packages/types/src/session.ts | 7 +++++++ packages/types/src/utils.ts | 4 ++++ 11 files changed, 68 insertions(+), 15 deletions(-) create mode 100644 .changeset/whole-knives-attend.md diff --git a/.changeset/whole-knives-attend.md b/.changeset/whole-knives-attend.md new file mode 100644 index 00000000000..8761ae9c8f1 --- /dev/null +++ b/.changeset/whole-knives-attend.md @@ -0,0 +1,16 @@ +--- +'@clerk/clerk-js': patch +'@clerk/types': patch +--- + +Add `taskUrls` option to customize task flow URLs: + +```tsx + +``` + +**Breaking**: Task key renamed from `'select-organization'` to `'org'`. diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index 1be58ac9236..be65d9b6952 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -2331,7 +2331,7 @@ describe('Clerk singleton', () => { }); }); - describe('nextTask', () => { + describe('navigateToTask', () => { describe('with `pending` session status', () => { const mockSession = { id: '1', @@ -2360,7 +2360,7 @@ describe('Clerk singleton', () => { mockResource.touch.mockReset(); }); - it('navigates to next task', async () => { + it('navigates to next task with default internal routing for AIOs', async () => { const sut = new Clerk(productionPublishableKey); await sut.load(mockedLoadOptions); @@ -2369,6 +2369,21 @@ describe('Clerk singleton', () => { expect(mockNavigate.mock.calls[0][0]).toBe('/sign-in#/tasks/add-organization'); }); + + it('navigates to next task with custom routing from clerk options', async () => { + const sut = new Clerk(productionPublishableKey); + await sut.load({ + ...mockedLoadOptions, + taskUrls: { + org: '/onboarding/select-organization', + }, + }); + + await sut.setActive({ session: mockResource as any as PendingSessionResource }); + await sut.__internal_navigateToTaskIfAvailable(); + + expect(mockNavigate.mock.calls[0][0]).toBe('/onboarding/select-organization'); + }); }); describe('with `active` session status', () => { diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 2439952460e..9c978e9e219 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -30,9 +30,9 @@ import type { AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, AuthenticateWithOKXWalletParams, - Clerk as ClerkInterface, ClerkAPIError, ClerkAuthenticateWithWeb3Params, + Clerk as ClerkInterface, ClerkOptions, ClientJSONSnapshot, ClientResource, @@ -1317,7 +1317,7 @@ export class Clerk implements ClerkInterface { eventBus.emit(events.TokenUpdate, { token: null }); } - // Only triggers navigation for internal AIO components routing + // Only triggers navigation for internal AIO components routing or custom URLs const shouldNavigateOnSetActive = this.#componentNavigationContext; if (newSession?.currentTask && shouldNavigateOnSetActive) { await navigateToTask(session.currentTask.key, { diff --git a/packages/clerk-js/src/core/sessionTasks.ts b/packages/clerk-js/src/core/sessionTasks.ts index 1971d68e870..23858a8116c 100644 --- a/packages/clerk-js/src/core/sessionTasks.ts +++ b/packages/clerk-js/src/core/sessionTasks.ts @@ -7,7 +7,7 @@ import type { import { buildURL } from '../utils'; -export const SESSION_TASK_ROUTE_BY_KEY: Record = { +export const INTERNAL_SESSION_TASK_ROUTE_BY_KEY: Record = { org: 'add-organization', } as const; @@ -24,10 +24,10 @@ interface NavigateToTaskOptions { * @internal */ export function navigateToTask( - routeKey: keyof typeof SESSION_TASK_ROUTE_BY_KEY, + routeKey: keyof typeof INTERNAL_SESSION_TASK_ROUTE_BY_KEY, { componentNavigationContext, globalNavigate, options, environment }: NavigateToTaskOptions, ) { - const taskRoute = `/tasks/${SESSION_TASK_ROUTE_BY_KEY[routeKey]}`; + const taskRoute = options.taskUrls?.[routeKey] ?? `/tasks/${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[routeKey]}`; if (componentNavigationContext) { return componentNavigationContext.navigate(componentNavigationContext.indexPath + taskRoute); @@ -38,7 +38,6 @@ export function navigateToTask( const isReferrerSignUpUrl = window.location.href.startsWith(signUpUrl); const sessionTaskUrl = buildURL( - // TODO - Accept custom URL option for custom flows in order to eject out of `signInUrl/signUpUrl` { base: isReferrerSignUpUrl ? signUpUrl : signInUrl, hashPath: taskRoute, diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts index 59636a9bb64..354cb48272f 100644 --- a/packages/clerk-js/src/ui/common/redirects.ts +++ b/packages/clerk-js/src/ui/common/redirects.ts @@ -1,6 +1,6 @@ -import type { SessionTask } from '@clerk/types'; +import type { ClerkOptions, SessionTask } from '@clerk/types'; -import { SESSION_TASK_ROUTE_BY_KEY } from '../../core/sessionTasks'; +import { INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '../../core/sessionTasks'; import { buildURL } from '../../utils/url'; import type { SignInContextType, SignUpContextType, UserProfileContextType } from './../contexts'; @@ -33,9 +33,11 @@ export function buildSessionTaskRedirectUrl({ path, baseUrl, task, + taskUrls, }: Pick & { baseUrl: string; task?: SessionTask; + taskUrls?: ClerkOptions['taskUrls']; }) { if (!task) { return null; @@ -45,7 +47,7 @@ export function buildSessionTaskRedirectUrl({ routing, baseUrl, path, - endpoint: `/tasks/${SESSION_TASK_ROUTE_BY_KEY[task.key]}`, + endpoint: taskUrls?.[task.key] ?? `/tasks/${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[task.key]}`, authQueryString: null, }); } diff --git a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx index 23627b0bfe5..20f4229594d 100644 --- a/packages/clerk-js/src/ui/components/SessionTasks/index.tsx +++ b/packages/clerk-js/src/ui/components/SessionTasks/index.tsx @@ -6,7 +6,7 @@ import { Card } from '@/ui/elements/Card'; import { withCardStateProvider } from '@/ui/elements/contexts'; import { LoadingCardContainer } from '@/ui/elements/LoadingCard'; -import { SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks'; +import { INTERNAL_SESSION_TASK_ROUTE_BY_KEY } from '../../../core/sessionTasks'; import { SignInContext, SignUpContext } from '../../../ui/contexts'; import { SessionTasksContext, useSessionTasksContext } from '../../contexts/components/SessionTasks'; import { Route, Switch, useRouter } from '../../router'; @@ -38,7 +38,7 @@ const SessionTasksStart = () => { function SessionTaskRoutes(): JSX.Element { return ( - + diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 67f6442bb3a..75aa4b4b1b5 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -126,6 +126,7 @@ export const useSignInContext = (): SignInContextType => { path: ctx.path, routing: ctx.routing, baseUrl: signInUrl, + taskUrls: clerk.__internal_getOption('taskUrls'), }); return { diff --git a/packages/clerk-js/src/ui/contexts/components/SignUp.ts b/packages/clerk-js/src/ui/contexts/components/SignUp.ts index 7632e782f2d..f91efbea7b1 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts @@ -121,6 +121,7 @@ export const useSignUpContext = (): SignUpContextType => { path: ctx.path, routing: ctx.routing, baseUrl: signUpUrl, + taskUrls: clerk.__internal_getOption('taskUrls'), }); return { diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 543f45d40a0..f55b669abc4 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -48,7 +48,7 @@ import type { SignUpFallbackRedirectUrl, SignUpForceRedirectUrl, } from './redirects'; -import type { PendingSessionOptions, SignedInSessionResource } from './session'; +import type { PendingSessionOptions, SessionTask, SignedInSessionResource } from './session'; import type { SessionVerificationLevel } from './sessionVerification'; import type { SignInResource } from './signIn'; import type { SignUpResource } from './signUp'; @@ -56,7 +56,7 @@ import type { ClientJSONSnapshot, EnvironmentJSONSnapshot } from './snapshots'; import type { Web3Strategy } from './strategies'; import type { TelemetryCollector } from './telemetry'; import type { UserResource } from './user'; -import type { Autocomplete, DeepPartial, DeepSnakeToCamel } from './utils'; +import type { Autocomplete, CamelCase, DeepPartial, DeepSnakeToCamel } from './utils'; import type { WaitlistResource } from './waitlist'; type __experimental_CheckoutStatus = 'awaiting_initialization' | 'awaiting_confirmation' | 'completed'; @@ -1050,6 +1050,14 @@ export type ClerkOptions = PendingSessionOptions & * @internal */ __internal_keyless_dismissPrompt?: (() => Promise) | null; + + /** + * Customize the URL paths users are redirected to after sign-in or sign-up when specific + * session tasks need to be completed. + * + * @default undefined - Uses Clerk's default task flow URLs + */ + taskUrls?: Record, string>; }; export interface NavigateOptions { diff --git a/packages/types/src/session.ts b/packages/types/src/session.ts index a6adc59c33e..84ebb5eaf1d 100644 --- a/packages/types/src/session.ts +++ b/packages/types/src/session.ts @@ -326,7 +326,14 @@ export interface PublicUserData { userId?: string; } +/** + * Represents a required action that a user must complete + * before their session becomes fully active + */ export interface SessionTask { + /** + * The unique identifier for the type of task that needs to be completed + */ key: 'org'; } diff --git a/packages/types/src/utils.ts b/packages/types/src/utils.ts index 166ce7a1e58..b47b0689aa2 100644 --- a/packages/types/src/utils.ts +++ b/packages/types/src/utils.ts @@ -26,6 +26,10 @@ export type CamelToSnake = T extends `${infer C0}${infer R}` } : T; +export type CamelCase = S extends `${infer P1}_${infer P2}${infer P3}` + ? `${Lowercase}${Uppercase}${CamelCase}` + : Lowercase; + /** * @internal */ From 9865e723d6b926b782a4e76203cc89ff66a082e3 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 22 Jul 2025 05:16:22 -0300 Subject: [PATCH 2/5] Update changeset --- .changeset/whole-knives-attend.md | 2 -- packages/clerk-js/src/core/sessionTasks.ts | 4 ++-- packages/clerk-js/src/ui/common/redirects.ts | 17 ++++++++++------- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.changeset/whole-knives-attend.md b/.changeset/whole-knives-attend.md index 8761ae9c8f1..232c5603fc6 100644 --- a/.changeset/whole-knives-attend.md +++ b/.changeset/whole-knives-attend.md @@ -12,5 +12,3 @@ Add `taskUrls` option to customize task flow URLs: }} /> ``` - -**Breaking**: Task key renamed from `'select-organization'` to `'org'`. diff --git a/packages/clerk-js/src/core/sessionTasks.ts b/packages/clerk-js/src/core/sessionTasks.ts index 23858a8116c..da44e2a5756 100644 --- a/packages/clerk-js/src/core/sessionTasks.ts +++ b/packages/clerk-js/src/core/sessionTasks.ts @@ -27,7 +27,7 @@ export function navigateToTask( routeKey: keyof typeof INTERNAL_SESSION_TASK_ROUTE_BY_KEY, { componentNavigationContext, globalNavigate, options, environment }: NavigateToTaskOptions, ) { - const taskRoute = options.taskUrls?.[routeKey] ?? `/tasks/${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[routeKey]}`; + const taskRoute = `/tasks/${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[routeKey]}`; if (componentNavigationContext) { return componentNavigationContext.navigate(componentNavigationContext.indexPath + taskRoute); @@ -45,5 +45,5 @@ export function navigateToTask( { stringify: true }, ); - return globalNavigate(sessionTaskUrl); + return globalNavigate(options.taskUrls?.[routeKey] ?? sessionTaskUrl); } diff --git a/packages/clerk-js/src/ui/common/redirects.ts b/packages/clerk-js/src/ui/common/redirects.ts index 354cb48272f..23e805815e0 100644 --- a/packages/clerk-js/src/ui/common/redirects.ts +++ b/packages/clerk-js/src/ui/common/redirects.ts @@ -43,13 +43,16 @@ export function buildSessionTaskRedirectUrl({ return null; } - return buildRedirectUrl({ - routing, - baseUrl, - path, - endpoint: taskUrls?.[task.key] ?? `/tasks/${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[task.key]}`, - authQueryString: null, - }); + return ( + taskUrls?.[task.key] ?? + buildRedirectUrl({ + routing, + baseUrl, + path, + endpoint: `/tasks/${INTERNAL_SESSION_TASK_ROUTE_BY_KEY[task.key]}`, + authQueryString: null, + }) + ); } export function buildSSOCallbackURL( From 7c37f274cf42c3c76bae921b5619fcaed334c706 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 22 Jul 2025 07:57:45 -0300 Subject: [PATCH 3/5] Fix session mock on unit tests --- packages/clerk-js/src/core/__tests__/clerk.test.ts | 2 +- packages/clerk-js/src/core/clerk.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/core/__tests__/clerk.test.ts b/packages/clerk-js/src/core/__tests__/clerk.test.ts index be65d9b6952..035da4a48cb 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.test.ts @@ -2350,7 +2350,7 @@ describe('Clerk singleton', () => { reload: jest.fn(() => Promise.resolve(mockSession)), }; - beforeAll(() => { + beforeEach(() => { mockResource.touch.mockReturnValueOnce(Promise.resolve()); mockClientFetch.mockReturnValue(Promise.resolve({ signedInSessions: [mockResource] })); }); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 9c978e9e219..6389fb02bc7 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -30,9 +30,9 @@ import type { AuthenticateWithGoogleOneTapParams, AuthenticateWithMetamaskParams, AuthenticateWithOKXWalletParams, + Clerk as ClerkInterface, ClerkAPIError, ClerkAuthenticateWithWeb3Params, - Clerk as ClerkInterface, ClerkOptions, ClientJSONSnapshot, ClientResource, From 22d3ab8978ecf3d2a9db6acc90b064a27d36a6f2 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 22 Jul 2025 08:54:36 -0300 Subject: [PATCH 4/5] Add `session-task` to file structure for typedocs --- .typedoc/__tests__/__snapshots__/file-structure.test.ts.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap b/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap index f24a109e99b..a36c2dbcd77 100644 --- a/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap +++ b/.typedoc/__tests__/__snapshots__/file-structure.test.ts.snap @@ -106,6 +106,7 @@ exports[`Typedoc output > should have a deliberate file structure 1`] = ` "types/server-get-token.mdx", "types/session-resource.mdx", "types/session-status-claim.mdx", + "types/session-task.mdx", "types/session-verification-level.mdx", "types/session-verification-types.mdx", "types/set-active-params.mdx", From 3222c49b6bbcc36f365c4bcfaaf83e7d49344bbe Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 22 Jul 2025 12:33:48 -0300 Subject: [PATCH 5/5] Remove `CamelCase` type --- packages/types/src/clerk.ts | 4 ++-- packages/types/src/utils.ts | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index f55b669abc4..3abdb10a664 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -56,7 +56,7 @@ import type { ClientJSONSnapshot, EnvironmentJSONSnapshot } from './snapshots'; import type { Web3Strategy } from './strategies'; import type { TelemetryCollector } from './telemetry'; import type { UserResource } from './user'; -import type { Autocomplete, CamelCase, DeepPartial, DeepSnakeToCamel } from './utils'; +import type { Autocomplete, DeepPartial, DeepSnakeToCamel } from './utils'; import type { WaitlistResource } from './waitlist'; type __experimental_CheckoutStatus = 'awaiting_initialization' | 'awaiting_confirmation' | 'completed'; @@ -1057,7 +1057,7 @@ export type ClerkOptions = PendingSessionOptions & * * @default undefined - Uses Clerk's default task flow URLs */ - taskUrls?: Record, string>; + taskUrls?: Record; }; export interface NavigateOptions { diff --git a/packages/types/src/utils.ts b/packages/types/src/utils.ts index b47b0689aa2..166ce7a1e58 100644 --- a/packages/types/src/utils.ts +++ b/packages/types/src/utils.ts @@ -26,10 +26,6 @@ export type CamelToSnake = T extends `${infer C0}${infer R}` } : T; -export type CamelCase = S extends `${infer P1}_${infer P2}${infer P3}` - ? `${Lowercase}${Uppercase}${CamelCase}` - : Lowercase; - /** * @internal */