From 8dc0aba82889981457431e0bd395732251607157 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 01/14] Update `setActive` to handle `pending` session status --- packages/clerk-js/src/core/clerk.ts | 55 ++++++++++++------- .../clerk-js/src/ui/common/withRedirect.tsx | 7 ++- packages/types/src/clerk.ts | 2 + 3 files changed, 41 insertions(+), 23 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 32f0fb7ece7..8fbaeded971 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, @@ -200,15 +201,7 @@ export class Clerk implements ClerkInterface { #options: ClerkOptions = {}; #pageLifecycle: ReturnType | null = null; #touchThrottledUntil = 0; - #componentNavigationContext: { - navigate: ( - to: string, - options?: { - searchParams?: URLSearchParams; - }, - ) => Promise; - basePath: string; - } | null = null; + #internalComponentNavigate: ((to: string) => Promise) | null = null; public __internal_getCachedResources: | (() => Promise<{ client: ClientJSONSnapshot | null; environment: EnvironmentJSONSnapshot | null }>) @@ -446,6 +439,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. @@ -971,6 +985,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). @@ -1117,18 +1136,12 @@ export class Clerk implements ClerkInterface { return unsubscribe; }; - public __internal_setComponentNavigationContext = (context: { - navigate: ( - to: string, - options?: { - searchParams?: URLSearchParams; - }, - ) => Promise; - basePath: string; - }) => { - this.#componentNavigationContext = context; - - return () => (this.#componentNavigationContext = null); + public __internal_setComponentNavigate = (navigate: (to: string) => Promise): UnsubscribeCallback => { + this.#internalComponentNavigate = navigate; + const unsubscribe = () => { + this.#internalComponentNavigate = null; + }; + return unsubscribe; }; public navigate = async (to: string | undefined, options?: NavigateOptions): Promise => { diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx index 81336a63ec2..0dfba79e3c2 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

( 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

( } // TODO: Fix this properly // eslint-disable-next-line @typescript-eslint/no-floating-promises - navigate(redirectUrl({ clerk, environment, options })); + navigate(redirectUrlWithDefault({ clerk, environment, options })); } }, []); diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index e1366319454..af71c26b3dc 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -515,6 +515,8 @@ export interface Clerk { */ buildWaitlistUrl(opts?: { initialValues?: Record }): string; + buildSessionTaskUrl(): string; + /** * * Redirects to the provided url after decorating it with the auth token for development instances. From ae5b620f088ad23b1bb50ffa6eb4e0514f221145 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 02/14] Handle internal component navigation --- .../tests/session-tasks-sign-in.test.ts | 31 +++++---- packages/clerk-js/src/core/clerk.ts | 52 +++------------ 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/warnings.ts | 4 ++ .../clerk-js/src/ui/common/withRedirect.tsx | 63 ++++++++++++++----- .../ui/components/SessionTask/SessionTask.tsx | 24 ++++--- .../src/ui/components/SignIn/SignIn.tsx | 40 +++++++++--- .../src/ui/components/SignUp/SignUp.tsx | 21 ++++--- .../src/ui/contexts/components/SignIn.ts | 27 ++++---- .../src/ui/contexts/components/SignUp.ts | 27 ++++---- .../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 | 15 +++-- packages/types/src/snapshots.ts | 3 + 18 files changed, 314 insertions(+), 128 deletions(-) create mode 100644 packages/clerk-js/src/core/resources/SessionTask.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 eebc35002c1..916bbacddbf 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 { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; @@ -22,17 +22,24 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( 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(); - - await expect(u.page.getByRole('button', { name: /create organization/i })).toBeVisible(); - expect(page.url()).toContain('add-organization'); + 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 }); }, ); diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 8fbaeded971..9c6498594c7 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -439,27 +439,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. @@ -1077,32 +1056,21 @@ export class Clerk implements ClerkInterface { await onAfterSetActive(); }; - #handlePendingSession = async (session: SignedInSessionResource) => { - if (!this.environment) { + #handlePendingSession = async (session: PendingSessionResource) => { + if (!session.currentTask || !this.environment) { return; } - // 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?.lastActiveToken) { + eventBus.dispatch(events.TokenUpdate, { token: session.lastActiveToken }); } - if (session.currentTask) { - await navigateToTask(session.currentTask, { - globalNavigate: this.navigate, - componentNavigationContext: this.#componentNavigationContext, - options: this.#options, - environment: this.environment, - }); + 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); 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 = (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 = (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 1c9aa745b69..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); 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 = { + 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 2513a4d00cc..81f22523d8e 100644 --- a/packages/clerk-js/src/core/warnings.ts +++ b/packages/clerk-js/src/core/warnings.ts @@ -25,6 +25,10 @@ const warnings = { cannotRenderComponentWhenUserDoesNotExist: ' cannot render unless a user is signed in. Since no user is signed in, this is no-op.', cannotRenderComponentWhenOrgDoesNotExist: ` 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 0dfba79e3c2..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

( 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

( } // TODO: Fix this properly // eslint-disable-next-line @typescript-eslint/no-floating-promises - navigate(redirectUrlWithDefault({ clerk, environment, options })); + navigate(redirectUrl({ clerk, environment, options })); } }, []); @@ -64,10 +61,8 @@ export const withRedirectToAfterSignIn =

(Com return withRedirect( Component, sessionExistsAndSingleSessionModeEnabled, - ({ clerk }) => signInCtx.sessionTaskUrl || signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), - signInCtx.sessionTaskUrl - ? warnings.cannotRenderSignInComponentWhenTaskExists - : warnings.cannotRenderSignInComponentWhenSessionExists, + ({ clerk }) => signInCtx.tasksUrl || signInCtx.afterSignInUrl || clerk.buildAfterSignInUrl(), + warnings.cannotRenderSignInComponentWhenSessionExists, )(props); }; @@ -85,10 +80,8 @@ export const withRedirectToAfterSignUp =

(Com return withRedirect( Component, sessionExistsAndSingleSessionModeEnabled, - ({ clerk }) => signUpCtx.sessionTaskUrl || signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(), - signUpCtx.sessionTaskUrl - ? warnings.cannotRenderSignUpComponentWhenTaskExists - : warnings.cannotRenderSignUpComponentWhenSessionExists, + ({ clerk }) => signUpCtx.tasksUrl || signUpCtx.afterSignUpUrl || clerk.buildAfterSignUpUrl(), + warnings.cannotRenderSignUpComponentWhenSessionExists, )(props); }; @@ -96,3 +89,45 @@ export const withRedirectToAfterSignUp =

(Com return HOC; }; + +export const withRedirectToSignUpIfNoTasksAvailable =

( + Component: ComponentType

, +) => { + 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 =

( + Component: ComponentType

, +) => { + 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 b869d62265e..1a74f54e1cd 100644 --- a/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx +++ b/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx @@ -1,11 +1,14 @@ -import { useClerk } from '@clerk/shared/react/index'; -import { eventComponentMounted } from '@clerk/shared/telemetry'; -import type { SessionTask } from '@clerk/types'; +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'; -const ContentRegistry: Record = { +/** + * @internal + */ +const SessionTaskRegistry: Record = { org: () => ( // TODO - Hide personal workspace within organization list context based on environment @@ -17,12 +20,15 @@ const ContentRegistry: Record = { /** * @internal */ -export function SessionTask({ task }: { task: SessionTask['key'] }): React.ReactNode { - const clerk = useClerk(); +export function SessionTask(): React.ReactNode { + const session = useSessionContext(); + const [currentTask] = session?.tasks ?? []; - clerk.telemetry?.record(eventComponentMounted('SessionTask', { task })); + if (!currentTask) { + return null; + } - const Content = ContentRegistry[task]; + const Content = SessionTaskRegistry[currentTask.key]; - return ; + return Content ? : null; } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index 2a5895836f0..2c5d3b4483d 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -2,8 +2,9 @@ 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 { 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 { @@ -82,7 +83,6 @@ function SignInRoutes(): JSX.Element { redirectUrl='../factor-two' /> - {signInContext.isCombinedFlow && ( - {signInContext.withSessionTasks && ( - - + {SESSION_TASK_PATHS.map(path => ( + + - )} + ))} + + {SESSION_TASK_PATHS.map(path => ( + + + + ))} + )} - {signInContext.withSessionTasks && ( - - + + {SESSION_TASK_PATHS.map(path => ( + + - )} + ))} + @@ -233,3 +251,5 @@ export const SignInModal = (props: SignInModalProps): JSX.Element => { ); }; + +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 9e42f7efac8..f9b1e070db3 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -2,18 +2,20 @@ 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 { 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 { useFetch } from '../../hooks'; -import { preloadSessionTask, SessionTask } from '../../lazyModules/components'; import { Route, Switch, useRouter, 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'; +import { useFetch } from 'ui/hooks'; +import { preloadSessionTask } from 'ui/lazyModules/components'; const usePreloadSessionTask = (enabled = false) => useFetch(enabled ? preloadSessionTask : undefined, 'preloadComponent', { staleTime: Infinity }); @@ -89,11 +91,14 @@ function SignUpRoutes(): JSX.Element { - {signUpContext.withSessionTasks && ( - - + {SESSION_TASK_PATHS.map(path => ( + + - )} + ))} @@ -138,4 +143,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 d709a0d14b4..d3eecf11319 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -5,13 +5,9 @@ import { createContext, useContext, useMemo } from 'react'; import { SIGN_IN_INITIAL_VALUE_KEYS } from '../../../core/constants'; import { buildURL } from '../../../utils'; import { RedirectUrls } from '../../../utils/redirectUrls'; -import { - buildRedirectUrl, - buildSessionTaskRedirectUrl, - MAGIC_LINK_VERIFY_PATH_ROUTE, - SSO_CALLBACK_PATH_ROUTE, -} from '../../common/redirects'; +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'; @@ -26,7 +22,7 @@ export type SignInContextType = SignInCtx & { authQueryString: string | null; afterSignUpUrl: string; afterSignInUrl: string; - sessionTaskUrl: string | null; + tasksUrl: string | null; transferable: boolean; waitlistUrl: string; emailLinkRedirectUrl: string; @@ -119,11 +115,20 @@ export const useSignInContext = (): SignInContextType => { const signUpContinueUrl = buildURL({ base: signUpUrl, hashPath: '/continue' }, { stringify: true }); - const sessionTaskUrl = buildSessionTaskRedirectUrl({ - task: clerk.session?.currentTask, - path: ctx.path, + 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 { @@ -137,7 +142,7 @@ export const useSignInContext = (): SignInContextType => { afterSignUpUrl, emailLinkRedirectUrl, ssoCallbackUrl, - sessionTaskUrl, + 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 59ce5f4ab09..725da05a3c3 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts @@ -5,13 +5,9 @@ import { createContext, useContext, useMemo } from 'react'; import { SIGN_UP_INITIAL_VALUE_KEYS } from '../../../core/constants'; import { buildURL } from '../../../utils'; import { RedirectUrls } from '../../../utils/redirectUrls'; -import { - buildRedirectUrl, - buildSessionTaskRedirectUrl, - MAGIC_LINK_VERIFY_PATH_ROUTE, - SSO_CALLBACK_PATH_ROUTE, -} from '../../common/redirects'; +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'; @@ -27,7 +23,7 @@ export type SignUpContextType = SignUpCtx & { afterSignUpUrl: string; afterSignInUrl: string; waitlistUrl: string; - sessionTaskUrl: string | null; + tasksUrl: string | null; isCombinedFlow: boolean; emailLinkRedirectUrl: string; ssoCallbackUrl: string; @@ -114,11 +110,20 @@ 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 = buildSessionTaskRedirectUrl({ - task: clerk.session?.currentTask, - path: ctx.path, + 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 { @@ -132,7 +137,7 @@ export const useSignUpContext = (): SignUpContextType => { afterSignInUrl, emailLinkRedirectUrl, ssoCallbackUrl, - sessionTaskUrl, + 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[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 af71c26b3dc..e1366319454 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -515,8 +515,6 @@ export interface Clerk { */ buildWaitlistUrl(opts?: { initialValues?: Record }): 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 | null; + tasks: Array | 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 bd48545b462..8109bfa6365 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,8 +126,8 @@ export interface SessionResource extends ClerkResource { lastActiveOrganizationId: string | null; lastActiveAt: Date; actor: ActJWTClaim | null; - tasks: Array | null; - currentTask?: SessionTask; + tasks: Array | null; + currentTask?: SessionTaskResource; /** * The user associated with the session. */ @@ -225,8 +228,12 @@ export interface PublicUserData { userId?: string; } -export interface SessionTask { - key: 'org'; +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, { From 82d70fdf0c2e2ce18e408ea0749f0a61683bd427 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 03/14] Introduce test coverage --- .changeset/brave-pears-add.md | 1 - .../tests/session-tasks-sign-in.test.ts | 54 +++++++--------- .../tests/session-tasks-sign-up.test.ts | 58 ++++++++++-------- packages/clerk-js/src/core/clerk.ts | 47 ++++++++------ 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 | 57 ++++++++--------- packages/clerk-js/src/core/warnings.ts | 4 -- packages/clerk-js/src/ui/common/redirects.ts | 20 +++--- .../clerk-js/src/ui/common/withRedirect.tsx | 48 +-------------- .../ui/components/SessionTask/SessionTask.tsx | 19 ++---- .../src/ui/components/SignIn/SignIn.tsx | 43 +++---------- .../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 | 3 +- packages/types/src/clerk.ts | 14 +---- packages/types/src/json.ts | 8 +-- packages/types/src/session.ts | 15 ++--- packages/types/src/snapshots.ts | 3 - 24 files changed, 188 insertions(+), 422 deletions(-) delete mode 100644 packages/clerk-js/src/core/resources/SessionTask.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 index 74870af7af9..ce2ecccbfc2 100644 --- a/.changeset/brave-pears-add.md +++ b/.changeset/brave-pears-add.md @@ -1,7 +1,6 @@ --- '@clerk/clerk-js': minor '@clerk/types': minor -'@clerk/clerk-react': 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 916bbacddbf..30ed91411bc 100644 --- a/integration/tests/session-tasks-sign-in.test.ts +++ b/integration/tests/session-tasks-sign-in.test.ts @@ -1,45 +1,37 @@ -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; +import { Application } from '../models/application'; -testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( - 'session tasks after sign-in flow @nextjs', - ({ app }) => { - test.describe.configure({ mode: 'serial' }); +test.describe('session tasks after sign-in flow @nextjs', () => { + test.describe.configure({ mode: 'serial' }); + let app: Application; + let fakeUser: FakeUser; let fakeUser: FakeUser; - test.beforeAll(async () => { - const u = createTestUtils({ app }); - fakeUser = u.services.users.createFakeUser(); - await u.services.users.createBapiUser(fakeUser); - }); + const m = createTestUtils({ app }); + fakeUser = m.services.users.createFakeUser(); + await m.services.users.createBapiUser(fakeUser); + }); test.afterAll(async () => { await fakeUser.deleteIfExists(); 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 index ad5003eb2f3..6270b8967a1 100644 --- a/integration/tests/session-tasks-sign-up.test.ts +++ b/integration/tests/session-tasks-sign-up.test.ts @@ -1,34 +1,40 @@ import { expect, test } from '@playwright/test'; +import type { Application } from '../models/application'; import { appConfigs } from '../presets'; -import { createTestUtils, testAgainstRunningApps } from '../testUtils'; +import { createTestUtils } from '../testUtils'; -testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })( - 'session tasks after sign-up flow @nextjs', - ({ app }) => { - test.describe.configure({ mode: 'serial' }); +test.describe('session tasks after sign-up flow @nextjs', () => { + test.describe.configure({ mode: 'serial' }); + let app: Application; - 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.beforeAll(async () => { + app = await appConfigs.next.appRouter.clone().commit(); + await app.setup(); + await app.withEnv(appConfigs.envs.withSessionTasks); + await app.dev(); + }); - await expect(u.page.getByRole('button', { name: /create organization/i })).toBeVisible(); - expect(page.url()).toContain('add-organization'); + test.afterAll(async () => { + await app.teardown(); + }); - await fakeUser.deleteIfExists(); + 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/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 9c6498594c7..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, @@ -964,11 +963,6 @@ 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). @@ -1056,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); @@ -1104,12 +1115,8 @@ export class Clerk implements ClerkInterface { return unsubscribe; }; - public __internal_setComponentNavigate = (navigate: (to: string) => Promise): UnsubscribeCallback => { + public __internal_setComponentNavigate = (navigate: (to: string) => Promise) => { this.#internalComponentNavigate = navigate; - const unsubscribe = () => { - this.#internalComponentNavigate = null; - }; - return unsubscribe; }; public navigate = async (to: string | undefined, options?: NavigateOptions): Promise => { 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 = (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 = (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 = { - 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 index 29d7d9e0481..ff39cdbd5db 100644 --- a/packages/clerk-js/src/core/sessionTasks.ts +++ b/packages/clerk-js/src/core/sessionTasks.ts @@ -3,51 +3,44 @@ import type { ClerkOptions, EnvironmentResource, SessionTask } from '@clerk/type import { buildURL } from '../utils'; export const SESSION_TASK_ROUTE_BY_KEY: Record = { - org: 'add-organization', + 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 { - componentNavigationContext: { - navigate: ( - to: string, - options?: { - searchParams?: URLSearchParams; - }, - ) => Promise; - basePath: string; - } | null; - globalNavigate: (to: string) => Promise; + isInternalNavigation: boolean; + navigate: (to: string) => Promise; options: ClerkOptions; environment: EnvironmentResource; } /** - * Handles navigation to the tasks URL based on the application context such + * 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, - { componentNavigationContext, globalNavigate, options, environment }: NavigateToTaskOptions, + { isInternalNavigation, navigate, options, environment }: NavigateToTaskOptions, ) { - const taskRoute = `/${SESSION_TASK_ROUTE_BY_KEY[task.key]}`; - - if (componentNavigationContext) { - return componentNavigationContext.navigate(`/${componentNavigationContext.basePath + taskRoute}`); + 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']); } - - const signInUrl = options['signInUrl'] || environment.displayConfig.signInUrl; - const signUpUrl = options['signUpUrl'] || environment.displayConfig.signUpUrl; - 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, - }, - { stringify: true }, - ); - - return globalNavigate(sessionTaskUrl); } diff --git a/packages/clerk-js/src/core/warnings.ts b/packages/clerk-js/src/core/warnings.ts index 81f22523d8e..2513a4d00cc 100644 --- a/packages/clerk-js/src/core/warnings.ts +++ b/packages/clerk-js/src/core/warnings.ts @@ -25,10 +25,6 @@ const warnings = { cannotRenderComponentWhenUserDoesNotExist: ' cannot render unless a user is signed in. Since no user is signed in, this is no-op.', cannotRenderComponentWhenOrgDoesNotExist: ` 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 6b785c9d694..add5954cfeb 100644 --- a/packages/clerk-js/src/ui/common/redirects.ts +++ b/packages/clerk-js/src/ui/common/redirects.ts @@ -28,24 +28,18 @@ export function buildVerificationRedirectUrl({ }); } -export function buildSessionTaskRedirectUrl({ - routing, - path, - baseUrl, - task, -}: Pick & { - baseUrl: string; - task?: SessionTask; -}) { - if (!task) { - return null; - } +export function buildSessionTaskRedirectUrl( + ctx: Pick, + baseUrl: string, + task: SessionTask, +) { + const { routing, path } = ctx; 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/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 =

(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 =

(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 =

(Com return HOC; }; - -export const withRedirectToSignUpIfNoTasksAvailable =

( - Component: ComponentType

, -) => { - 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 =

( - Component: ComponentType

, -) => { - 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 = { +const SessionTaskRegistry: Record = { org: () => ( // TODO - Hide personal workspace within organization list context based on environment @@ -20,15 +19,7 @@ const SessionTaskRegistry: Record = { /** * @internal */ -export function SessionTask(): React.ReactNode { - const session = useSessionContext(); - const [currentTask] = session?.tasks ?? []; - - if (!currentTask) { - return null; - } - - const Content = SessionTaskRegistry[currentTask.key]; - - return Content ? : null; +export function SessionTask({ task }: { task: SessionTask['key'] }): React.ReactNode { + const Content = SessionTaskRegistry[task]; + return ; } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index 2c5d3b4483d..1643b9b027c 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 { @@ -16,10 +14,11 @@ import { } from '../../contexts'; import { Flow } from '../../customizables'; import { useFetch } from '../../hooks'; -import { preloadSessionTask, SessionTask } from '../../lazyModules/components'; import { Route, Switch, useRouter, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; +import { SessionTask } from '../SessionTask'; import { LazySignUpContinue, + LazySignUpSessionTask, LazySignUpSSOCallback, LazySignUpStart, LazySignUpVerifyEmail, @@ -33,6 +32,7 @@ import { SignInFactorOne } from './SignInFactorOne'; import { SignInFactorTwo } from './SignInFactorTwo'; import { SignInSSOCallback } from './SignInSSOCallback'; import { SignInStart } from './SignInStart'; +import { preloadSessionTask } from 'ui/lazyModules/components'; function RedirectToSignIn() { const clerk = useClerk(); @@ -83,6 +83,7 @@ function SignInRoutes(): JSX.Element { redirectUrl='../factor-two' /> + {signInContext.isCombinedFlow && ( - {SESSION_TASK_PATHS.map(path => ( - - - - ))} + + + - - {SESSION_TASK_PATHS.map(path => ( - - - - ))} - )} - - {SESSION_TASK_PATHS.map(path => ( - - - - ))} - + + + @@ -251,5 +230,3 @@ export const SignInModal = (props: SignInModalProps): JSX.Element => { ); }; - -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 f9b1e070db3..69c11c20372 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'; @@ -91,14 +89,9 @@ function SignUpRoutes(): JSX.Element { - {SESSION_TASK_PATHS.map(path => ( - - - - ))} + + + @@ -143,6 +136,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 d3eecf11319..3514d757e9d 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; @@ -115,21 +119,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), @@ -142,7 +142,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 725da05a3c3..db3e9759c02 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; @@ -110,21 +114,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, @@ -137,7 +137,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[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 c2b85093152..a5e9f92c7ac 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -92,8 +92,7 @@ type IsomorphicLoadedClerk = Without< | '__internal_addNavigationListener' | '__internal_getCachedResources' | '__internal_reloadInitialResources' - | '__experimental_commerce' - | '__internal_setComponentNavigationContext' + | '__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 e1366319454..eb01c5da7b6 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -427,19 +427,11 @@ export interface Clerk { __internal_addNavigationListener: (callback: () => void) => UnsubscribeCallback; /** - * Registers the internal navigation context from UI components in order to - * be triggered from `Clerk` methods + * Registers an internal navigate function for UI components in order to be triggered + * from `Clerk` * @internal */ - __internal_setComponentNavigationContext: (context: { - navigate: ( - to: string, - options?: { - searchParams?: URLSearchParams; - }, - ) => Promise; - basePath: string; - }) => () => void; + __internal_setComponentNavigate: (navigate: (to: string) => Promise) => 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 | null; + tasks: Array | 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 8109bfa6365..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 | null; - currentTask?: SessionTaskResource; + tasks: Array | null; + currentTask?: SessionTask; /** * The user associated with the session. */ @@ -228,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, { From 0203bc081d1b12ec43d72c0a4ee720e03d84c311 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 04/14] Introduce experimental prop for lazy loading routes --- .../next-app-router/src/app/layout.tsx | 1 + packages/clerk-js/bundlewatch.config.json | 10 ++--- packages/clerk-js/src/core/clerk.ts | 4 +- packages/clerk-js/src/core/sessionTasks.ts | 45 +++++++++---------- .../src/ui/components/SignIn/SignIn.tsx | 28 ++++++------ .../src/ui/components/SignIn/lazy-sign-up.ts | 2 - .../src/ui/components/SignUp/SignUp.tsx | 25 +++++------ .../src/ui/contexts/components/SignIn.ts | 1 + .../src/ui/contexts/components/SignUp.ts | 2 +- .../clerk-js/src/ui/lazyModules/components.ts | 6 --- packages/types/src/clerk.ts | 2 - 11 files changed, 56 insertions(+), 70 deletions(-) diff --git a/integration/templates/next-app-router/src/app/layout.tsx b/integration/templates/next-app-router/src/app/layout.tsx index e598334db29..e9e15ecbc86 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 d5015e80235..9a46b5b5a45 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -1,10 +1,10 @@ { "files": [ - { "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" }, + { "path": "./dist/clerk.js", "maxSize": "560kB" }, + { "path": "./dist/clerk.browser.js", "maxSize": "75kB" }, + { "path": "./dist/clerk.headless.js", "maxSize": "48.5KB" }, + { "path": "./dist/ui-common*.js", "maxSize": "89.2KB" }, + { "path": "./dist/vendors*.js", "maxSize": "25.1KB" }, { "path": "./dist/coinbase*.js", "maxSize": "35.5KB" }, { "path": "./dist/createorganization*.js", "maxSize": "5KB" }, { "path": "./dist/impersonationfab*.js", "maxSize": "5KB" }, 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 = { 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; + internalNavigate: ((to: string) => Promise) | null; + globalNavigate: (to: string) => Promise; 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 1643b9b027c..3ed92e5ace6 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -1,6 +1,7 @@ import { useClerk } from '@clerk/shared/react'; import type { SignInModalProps, SignInProps } from '@clerk/types'; import React from 'react'; +import { preloadSessionTask, SessionTask } from 'ui/lazyModules/components'; import { normalizeRoutingOptions } from '../../../utils/normalizeRoutingOptions'; import { SignInEmailLinkFlowComplete, SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; @@ -9,16 +10,15 @@ import { SignInContext, SignUpContext, useSignInContext, + useSignInContext, useSignUpContext, withCoreSessionSwitchGuard, } from '../../contexts'; import { Flow } from '../../customizables'; import { useFetch } from '../../hooks'; -import { Route, Switch, useRouter, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; -import { SessionTask } from '../SessionTask'; +import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; import { LazySignUpContinue, - LazySignUpSessionTask, LazySignUpSSOCallback, LazySignUpStart, LazySignUpVerifyEmail, @@ -32,7 +32,6 @@ import { SignInFactorOne } from './SignInFactorOne'; import { SignInFactorTwo } from './SignInFactorTwo'; import { SignInSSOCallback } from './SignInSSOCallback'; import { SignInStart } from './SignInStart'; -import { preloadSessionTask } from 'ui/lazyModules/components'; function RedirectToSignIn() { const clerk = useClerk(); @@ -131,9 +130,11 @@ function SignInRoutes(): JSX.Element { > - - - + {signInContext.withSessionTasks && ( + + + + )} @@ -143,9 +144,11 @@ function SignInRoutes(): JSX.Element { )} - - - + {signInContext.withSessionTasks && ( + + + + )} @@ -184,13 +187,8 @@ function SignInRoot() { */ usePreloadSignUp(signInContext.isCombinedFlow); - // `experimental.withSessionTasks` will be removed soon in favor of checking via environment response usePreloadSessionTask(signInContext.withSessionTasks); - React.useEffect(() => { - return __internal_setComponentNavigationContext?.({ basePath, navigate }); - }, [basePath, navigate]); - return ( 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 69c11c20372..05e5701e414 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -1,19 +1,21 @@ import { useClerk } from '@clerk/shared/react'; import type { SignUpModalProps, SignUpProps } from '@clerk/types'; import React from 'react'; +import { useFetch } from 'ui/hooks'; +import { preloadSessionTask, SessionTask } from 'ui/lazyModules/components'; import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts'; import { Flow } from '../../customizables'; -import { Route, Switch, useRouter, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; -import { SessionTask } from '../SessionTask'; +import { Route, Switch, VIRTUAL_ROUTER_BASE_PATH } from '../../router'; import { SignUpContinue } from './SignUpContinue'; import { SignUpSSOCallback } from './SignUpSSOCallback'; import { SignUpStart } from './SignUpStart'; import { SignUpVerifyEmail } from './SignUpVerifyEmail'; import { SignUpVerifyPhone } from './SignUpVerifyPhone'; -import { useFetch } from 'ui/hooks'; -import { preloadSessionTask } from 'ui/lazyModules/components'; + +const usePreloadSessionTask = (enabled = false) => + useFetch(enabled ? preloadSessionTask : undefined, 'preloadComponent', { staleTime: Infinity }); const usePreloadSessionTask = (enabled = false) => useFetch(enabled ? preloadSessionTask : undefined, 'preloadComponent', { staleTime: Infinity }); @@ -31,13 +33,8 @@ 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(() => { - return __internal_setComponentNavigationContext?.({ basePath, navigate }); - }, [basePath, navigate]); - return ( @@ -89,9 +86,11 @@ function SignUpRoutes(): JSX.Element { - - - + {signUpContext.withSessionTasks && ( + + + + )} @@ -136,4 +135,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 3514d757e9d..575eb77213d 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignIn.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignIn.ts @@ -143,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 db3e9759c02..660079551c4 100644 --- a/packages/clerk-js/src/ui/contexts/components/SignUp.ts +++ b/packages/clerk-js/src/ui/contexts/components/SignUp.ts @@ -143,6 +143,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/lazyModules/components.ts b/packages/clerk-js/src/ui/lazyModules/components.ts index 6182576c716..50c34f78b30 100644 --- a/packages/clerk-js/src/ui/lazyModules/components.ts +++ b/packages/clerk-js/src/ui/lazyModules/components.ts @@ -17,8 +17,6 @@ const componentImportPaths = { UserVerification: () => import(/* webpackChunkName: "userverification" */ './../components/UserVerification'), Waitlist: () => import(/* webpackChunkName: "waitlist" */ './../components/Waitlist'), 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; @@ -91,10 +89,6 @@ export const KeylessPrompt = lazy(() => componentImportPaths.KeylessPrompt().then(module => ({ default: module.KeylessPrompt })), ); -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 })), diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index eb01c5da7b6..42bf0f293e1 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -809,8 +809,6 @@ export type ClerkOptions = ClerkOptionsNavigation & * Clerk will rethrow network errors that occur while the user is offline. */ rethrowOfflineNetworkErrors: boolean; - commerce: boolean; - // `experimental.withSessionTasks` will be removed soon in favor of checking via environment response withSessionTasks: boolean; }, Record From 234f912cf36015143d744817b07d151a0aa8db3c 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 05/14] 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 | 5 +++-- .../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 +++--- .../clerk-js/src/ui/lazyModules/components.ts | 1 - 8 files changed, 24 insertions(+), 21 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 =

(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 =

(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 = { +const ContentRegistry: Record = { org: () => ( // TODO - Hide personal workspace within organization list context based on environment @@ -20,6 +18,11 @@ const SessionTaskRegistry: Record = { * @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 ; } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index 3ed92e5ace6..8cb31687413 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -1,7 +1,7 @@ import { useClerk } from '@clerk/shared/react'; import type { SignInModalProps, SignInProps } from '@clerk/types'; import React from 'react'; -import { preloadSessionTask, SessionTask } from 'ui/lazyModules/components'; +import { preloadComponent, SessionTask } from 'ui/lazyModules/components'; import { normalizeRoutingOptions } from '../../../utils/normalizeRoutingOptions'; import { SignInEmailLinkFlowComplete, SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; @@ -11,6 +11,7 @@ import { SignUpContext, useSignInContext, useSignInContext, + useSignInContext, useSignUpContext, withCoreSessionSwitchGuard, } from '../../contexts'; @@ -164,7 +165,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 { __internal_setComponentNavigationContext } = useClerk(); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx index 05e5701e414..80e2cdb353a 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -2,7 +2,7 @@ import { useClerk } from '@clerk/shared/react'; import type { SignUpModalProps, SignUpProps } from '@clerk/types'; import React from 'react'; import { useFetch } from 'ui/hooks'; -import { preloadSessionTask, SessionTask } from 'ui/lazyModules/components'; +import { preloadComponent, preloadSessionTask, SessionTask } from 'ui/lazyModules/components'; import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts'; @@ -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 }); const usePreloadSessionTask = (enabled = false) => useFetch(enabled ? preloadSessionTask : undefined, 'preloadComponent', { staleTime: Infinity }); diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 575eb77213d..31c7147ad83 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 }, diff --git a/packages/clerk-js/src/ui/lazyModules/components.ts b/packages/clerk-js/src/ui/lazyModules/components.ts index 50c34f78b30..8a3e29d94fa 100644 --- a/packages/clerk-js/src/ui/lazyModules/components.ts +++ b/packages/clerk-js/src/ui/lazyModules/components.ts @@ -89,7 +89,6 @@ export const KeylessPrompt = lazy(() => componentImportPaths.KeylessPrompt().then(module => ({ default: module.KeylessPrompt })), ); -export const preloadSessionTask = () => import(/* webpackChunkName: "sessionTask" */ '../components/SessionTask'); export const SessionTask = lazy(() => componentImportPaths.SessionTask().then(module => ({ default: module.SessionTask })), ); From b4f1d7e11a35c408fbfc93c0e0cfb025cd6ee8f2 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 06/14] Refactor internal routing to set on `BaseRouter` --- .../next-app-router/src/app/layout.tsx | 3 +-- packages/clerk-js/bundlewatch.config.json | 2 +- packages/clerk-js/src/core/clerk.ts | 22 ++++++++++++++---- packages/clerk-js/src/core/sessionTasks.ts | 23 +++++++++++++++---- .../src/ui/components/SignIn/SignIn.tsx | 3 ++- .../src/ui/components/SignUp/SignUp.tsx | 4 ++-- .../src/ui/contexts/components/SignIn.ts | 11 +-------- .../src/ui/contexts/components/SignUp.ts | 12 ++-------- .../clerk-js/src/ui/lazyModules/components.ts | 1 + .../clerk-js/src/ui/router/BaseRouter.tsx | 6 ++++- packages/react/src/isomorphicClerk.ts | 2 +- packages/types/src/clerk.ts | 11 ++++++--- 12 files changed, 60 insertions(+), 40 deletions(-) diff --git a/integration/templates/next-app-router/src/app/layout.tsx b/integration/templates/next-app-router/src/app/layout.tsx index e9e15ecbc86..f83c142ffcb 100644 --- a/integration/templates/next-app-router/src/app/layout.tsx +++ b/integration/templates/next-app-router/src/app/layout.tsx @@ -21,11 +21,10 @@ 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, - // `experimental.withSessionTasks` will be removed soon in favor of checking via environment response + // `withSessionTasks` will be removed soon in favor of checking via environment response withSessionTasks: true, }} > diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 9a46b5b5a45..74ccfec9eb1 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -2,7 +2,7 @@ "files": [ { "path": "./dist/clerk.js", "maxSize": "560kB" }, { "path": "./dist/clerk.browser.js", "maxSize": "75kB" }, - { "path": "./dist/clerk.headless.js", "maxSize": "48.5KB" }, + { "path": "./dist/clerk.headless.js", "maxSize": "48.6KB" }, { "path": "./dist/ui-common*.js", "maxSize": "89.2KB" }, { "path": "./dist/vendors*.js", "maxSize": "25.1KB" }, { "path": "./dist/coinbase*.js", "maxSize": "35.5KB" }, 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 | null = null; #touchThrottledUntil = 0; - #internalComponentNavigate: ((to: string) => Promise) | null = null; + #componentNavigationContext: { + navigate: (toURL: URL | undefined) => Promise; + 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) => { - this.#internalComponentNavigate = navigate; + public __internal_setComponentNavigationContext = ( + context: { + navigate: (toURL: URL | undefined) => Promise; + basePath: string; + } | null, + ) => { + this.#componentNavigationContext = context; + + return () => (this.#componentNavigationContext = null); }; public navigate = async (to: string | undefined, options?: NavigateOptions): Promise => { 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 = { } as const; interface NavigateToTaskOptions { - internalNavigate: ((to: string) => Promise) | null; + componentNavigationContext: { + basePath: string; + navigate: (toURL: URL | undefined) => Promise; + } | null; globalNavigate: (to: string) => Promise; 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 8cb31687413..137e4b47240 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -17,6 +17,7 @@ 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 { LazySignUpContinue, @@ -165,7 +166,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 { __internal_setComponentNavigationContext } = useClerk(); diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx index 80e2cdb353a..05e5701e414 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -2,7 +2,7 @@ import { useClerk } from '@clerk/shared/react'; import type { SignUpModalProps, SignUpProps } from '@clerk/types'; import React from 'react'; import { useFetch } from 'ui/hooks'; -import { preloadComponent, preloadSessionTask, SessionTask } from 'ui/lazyModules/components'; +import { preloadSessionTask, SessionTask } from 'ui/lazyModules/components'; import { SignUpEmailLinkFlowComplete } from '../../common/EmailLinkCompleteFlowCard'; import { SignUpContext, useSignUpContext, withCoreSessionSwitchGuard } from '../../contexts'; @@ -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 }); const usePreloadSessionTask = (enabled = false) => useFetch(enabled ? preloadSessionTask : undefined, 'preloadComponent', { staleTime: Infinity }); diff --git a/packages/clerk-js/src/ui/contexts/components/SignIn.ts b/packages/clerk-js/src/ui/contexts/components/SignIn.ts index 31c7147ad83..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,7 +135,6 @@ export const useSignInContext = (): SignInContextType => { emailLinkRedirectUrl, ssoCallbackUrl, sessionTaskUrl, - 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 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/lazyModules/components.ts b/packages/clerk-js/src/ui/lazyModules/components.ts index 8a3e29d94fa..50c34f78b30 100644 --- a/packages/clerk-js/src/ui/lazyModules/components.ts +++ b/packages/clerk-js/src/ui/lazyModules/components.ts @@ -89,6 +89,7 @@ export const KeylessPrompt = lazy(() => componentImportPaths.KeylessPrompt().then(module => ({ default: module.KeylessPrompt })), ); +export const preloadSessionTask = () => import(/* webpackChunkName: "sessionTask" */ '../components/SessionTask'); export const SessionTask = lazy(() => componentImportPaths.SessionTask().then(module => ({ default: module.SessionTask })), ); 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 ( & { client: ClientResource | undefined; __experimental_commerce: __experimental_CommerceNamespace | undefined; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 42bf0f293e1..94ab6227f58 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) => void; + __internal_setComponentNavigationContext: ( + context: { + navigate: (toURL: URL | undefined) => Promise; + basePath: string; + } | null, + ) => () => void; /** * Set the active session and organization explicitly. From 9f0f4d0c892af9049174480bd452f08a84850f46 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 07/14] Refactor integration tests to use use long running apps --- .../tests/session-tasks-sign-in.test.ts | 43 +++++++------- .../tests/session-tasks-sign-up.test.ts | 58 +++++++++---------- packages/clerk-js/bundlewatch.config.json | 2 +- 3 files changed, 49 insertions(+), 54 deletions(-) diff --git a/integration/tests/session-tasks-sign-in.test.ts b/integration/tests/session-tasks-sign-in.test.ts index 30ed91411bc..eebc35002c1 100644 --- a/integration/tests/session-tasks-sign-in.test.ts +++ b/integration/tests/session-tasks-sign-in.test.ts @@ -3,35 +3,36 @@ import { expect, test } from '@playwright/test'; import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../testUtils'; -import { Application } from '../models/application'; -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' }); 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('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 74ccfec9eb1..9e67fe87a5b 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -2,7 +2,7 @@ "files": [ { "path": "./dist/clerk.js", "maxSize": "560kB" }, { "path": "./dist/clerk.browser.js", "maxSize": "75kB" }, - { "path": "./dist/clerk.headless.js", "maxSize": "48.6KB" }, + { "path": "./dist/clerk.headless.js", "maxSize": "48.7KB" }, { "path": "./dist/ui-common*.js", "maxSize": "89.2KB" }, { "path": "./dist/vendors*.js", "maxSize": "25.1KB" }, { "path": "./dist/coinbase*.js", "maxSize": "35.5KB" }, From 674b8a3a160f035e57a372d8b108a74b9f4ef595 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Tue, 11 Mar 2025 18:10:52 -0300 Subject: [PATCH 08/14] Update bundlewatch limit for headless bundle --- packages/clerk-js/bundlewatch.config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 9e67fe87a5b..b5549943e43 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -2,7 +2,7 @@ "files": [ { "path": "./dist/clerk.js", "maxSize": "560kB" }, { "path": "./dist/clerk.browser.js", "maxSize": "75kB" }, - { "path": "./dist/clerk.headless.js", "maxSize": "48.7KB" }, + { "path": "./dist/clerk.headless.js", "maxSize": "50KB" }, { "path": "./dist/ui-common*.js", "maxSize": "89.2KB" }, { "path": "./dist/vendors*.js", "maxSize": "25.1KB" }, { "path": "./dist/coinbase*.js", "maxSize": "35.5KB" }, From e02429e8d1b71d242c49928fbc0e7aa601b5287d 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/14] 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 1731e49496a278cf660316d646efc57e9d3ac788 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/14] 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 | 11 +++++--- .../src/ui/components/SignUp/SignUp.tsx | 9 +++++-- .../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, 70 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 | null = null; #touchThrottledUntil = 0; #componentNavigationContext: { - navigate: (toURL: URL | undefined) => Promise; + navigate: ( + to: string, + options?: { + searchParams?: URLSearchParams; + }, + ) => Promise; 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; + navigate: ( + to: string, + options?: { + searchParams?: URLSearchParams; + }, + ) => Promise; 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 = { - org: '/add-organization', + org: 'add-organization', } as const; interface NavigateToTaskOptions { componentNavigationContext: { + navigate: ( + to: string, + options?: { + searchParams?: URLSearchParams; + }, + ) => Promise; basePath: string; - navigate: (toURL: URL | undefined) => Promise; } | null; globalNavigate: (to: string) => Promise; 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, - baseUrl: string, - task: SessionTask, -) { - const { routing, path } = ctx; +export function buildSessionTaskRedirectUrl({ + routing, + path, + baseUrl, + task, +}: Pick & { + 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 137e4b47240..590c6808681 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -3,6 +3,7 @@ import type { SignInModalProps, SignInProps } from '@clerk/types'; import React from 'react'; import { preloadComponent, SessionTask } from 'ui/lazyModules/components'; +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'; @@ -18,7 +19,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, @@ -133,7 +134,7 @@ function SignInRoutes(): JSX.Element { {signInContext.withSessionTasks && ( - + )} @@ -147,7 +148,7 @@ function SignInRoutes(): JSX.Element { )} {signInContext.withSessionTasks && ( - + )} @@ -191,6 +192,10 @@ function SignInRoot() { usePreloadSessionTask(signInContext.withSessionTasks); + React.useEffect(() => { + return __internal_setComponentNavigationContext?.({ basePath, navigate }); + }, []); + return ( diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx index 05e5701e414..97e8677cdbf 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -4,10 +4,11 @@ import React from 'react'; import { useFetch } from 'ui/hooks'; import { preloadSessionTask, SessionTask } from 'ui/lazyModules/components'; +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 { 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'; @@ -35,6 +36,10 @@ function SignUpRoutes(): JSX.Element { usePreloadSessionTask(signUpContext.withSessionTasks); + React.useEffect(() => { + return __internal_setComponentNavigationContext?.({ basePath, navigate }); + }, []); + return ( @@ -87,7 +92,7 @@ function SignUpRoutes(): JSX.Element { {signUpContext.withSessionTasks && ( - + )} 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 ( Promise; + navigate: ( + to: string, + options?: { + searchParams?: URLSearchParams; + }, + ) => Promise; basePath: string; } | null, ) => () => void; From afa6ed4a81dec06ad920928441ab0c7a7d884fcb 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/14] 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; - basePath: string; - } | null, - ) => { + public __internal_setComponentNavigationContext = (context: { + navigate: ( + to: string, + options?: { + searchParams?: URLSearchParams; + }, + ) => Promise; + basePath: string; + }) => { this.#componentNavigationContext = context; return () => (this.#componentNavigationContext = null); diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index f09c588f22d..1c8a924dd1b 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; - basePath: string; - } | null, - ) => () => void; + __internal_setComponentNavigationContext: (context: { + navigate: ( + to: string, + options?: { + searchParams?: URLSearchParams; + }, + ) => Promise; + basePath: string; + }) => () => void; /** * Set the active session and organization explicitly. From 47d30778b73447f6654b2baf24c0ac789fd8a76a 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/14] 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 590c6808681..51f18567bbf 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -190,6 +190,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 97e8677cdbf..265560d282f 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -34,6 +34,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 1c8a924dd1b..def3aab30f8 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -817,6 +817,7 @@ export type ClerkOptions = ClerkOptionsNavigation & * Clerk will rethrow network errors that occur while the user is offline. */ rethrowOfflineNetworkErrors: boolean; + // `experimental.withSessionTasks` will be removed soon in favor of checking via environment response withSessionTasks: boolean; }, Record From 39724c13c01792c08502ff6dddfdd60ab91cf971 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:34:02 -0300 Subject: [PATCH 13/14] Forward after sign-in/sign-up URLs to `SessionTask` component context --- .../ui/components/SessionTask/SessionTask.tsx | 25 +++++++++++++++---- .../src/ui/components/SignIn/SignIn.tsx | 10 ++++++-- .../src/ui/components/SignUp/SignUp.tsx | 5 +++- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx b/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx index b869d62265e..27b4a121e38 100644 --- a/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx +++ b/packages/clerk-js/src/ui/components/SessionTask/SessionTask.tsx @@ -5,10 +5,25 @@ import type { SessionTask } from '@clerk/types'; import { OrganizationListContext } from '../../contexts'; import { OrganizationList } from '../OrganizationList'; -const ContentRegistry: Record = { - org: () => ( +interface SessionTaskProps { + task: SessionTask['key']; + redirectUrlComplete: string; +} + +const ContentRegistry: Record< + SessionTask['key'], + React.ComponentType> +> = { + org: ({ redirectUrlComplete }) => ( // TODO - Hide personal workspace within organization list context based on environment - + ), @@ -17,12 +32,12 @@ const ContentRegistry: Record = { /** * @internal */ -export function SessionTask({ task }: { task: SessionTask['key'] }): React.ReactNode { +export function SessionTask({ task, redirectUrlComplete }: SessionTaskProps): React.ReactNode { const clerk = useClerk(); clerk.telemetry?.record(eventComponentMounted('SessionTask', { task })); const Content = ContentRegistry[task]; - return ; + return ; } diff --git a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx index 51f18567bbf..a54391f70ef 100644 --- a/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx +++ b/packages/clerk-js/src/ui/components/SignIn/SignIn.tsx @@ -135,7 +135,10 @@ function SignInRoutes(): JSX.Element { {signInContext.withSessionTasks && ( - + )} @@ -149,7 +152,10 @@ function SignInRoutes(): JSX.Element { )} {signInContext.withSessionTasks && ( - + )} diff --git a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx index 265560d282f..825db66f3f9 100644 --- a/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx +++ b/packages/clerk-js/src/ui/components/SignUp/SignUp.tsx @@ -94,7 +94,10 @@ function SignUpRoutes(): JSX.Element { {signUpContext.withSessionTasks && ( - + )} From 0c5cb8c439b32b3a03293bf010da99f09c402a71 Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 12 Mar 2025 11:37:28 -0300 Subject: [PATCH 14/14] Handle navigation to next task upon `/touch` --- packages/clerk-js/src/core/clerk.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 32f0fb7ece7..b02b208651d 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, @@ -1009,6 +1010,11 @@ export class Clerk implements ClerkInterface { newSession = this.#getSessionFromClient(newSession?.id); } + if (newSession?.status === 'pending') { + await this.#handlePendingSession(newSession); + return; + } + // getToken syncs __session and __client_uat to cookies using events.TokenUpdate dispatched event. const token = await newSession?.getToken(); if (!token) { @@ -1058,7 +1064,7 @@ export class Clerk implements ClerkInterface { await onAfterSetActive(); }; - #handlePendingSession = async (session: SignedInSessionResource) => { + #handlePendingSession = async (session: PendingSessionResource) => { if (!this.environment) { return; } @@ -1067,7 +1073,7 @@ export class Clerk implements ClerkInterface { // to `pending` if (inActiveBrowserTab() || !this.#options.standardBrowser) { await this.#touchCurrentSession(session); - session = this.#getSessionFromClient(session.id) ?? session; + session = (this.#getSessionFromClient(session.id) ?? session) as PendingSessionResource; } // Syncs __session and __client_uat, in case the `pending` session