From 0a1d36d921bd22601b72bc91c7cc2676c748c3ba Mon Sep 17 00:00:00 2001 From: Laura Beatris <48022589+LauraBeatris@users.noreply.github.com> Date: Wed, 26 Feb 2025 11:03:13 -0500 Subject: [PATCH] Add `with-session-tasks` to integration tests --- .changeset/old-cherries-laugh.md | 2 +- integration/.keys.json.sample | 4 ++ integration/presets/envs.ts | 8 +++ integration/tests/session-tasks.test.ts | 53 +++++++++------- .../core/__tests__/clerk.redirects.test.ts | 18 ------ .../src/core/auth/AuthCookieService.ts | 18 +++++- packages/clerk-js/src/core/clerk.ts | 62 ++++++------------- packages/clerk-js/src/core/events.ts | 6 +- .../clerk-js/src/core/resources/Session.ts | 2 + packages/clerk-js/src/ui/common/tasks.ts | 2 +- .../clerk-js/src/ui/common/withRedirect.tsx | 2 +- packages/react/src/isomorphicClerk.ts | 6 +- packages/types/src/clerk.ts | 9 ++- packages/types/src/jwt.ts | 7 +++ packages/types/src/jwtv2.ts | 6 ++ 15 files changed, 108 insertions(+), 97 deletions(-) diff --git a/.changeset/old-cherries-laugh.md b/.changeset/old-cherries-laugh.md index 4873561a826..d94e4755426 100644 --- a/.changeset/old-cherries-laugh.md +++ b/.changeset/old-cherries-laugh.md @@ -3,4 +3,4 @@ '@clerk/types': patch --- -Navigate to session tasks +Navigate to session task diff --git a/integration/.keys.json.sample b/integration/.keys.json.sample index caa70922c39..1aeddba9b72 100644 --- a/integration/.keys.json.sample +++ b/integration/.keys.json.sample @@ -46,5 +46,9 @@ "with-waitlist-mode": { "pk": "", "sk": "" + }, + "with-session-tasks": { + "pk": "", + "sk": "" } } diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index 4fbf4d2bb22..77bf8f93507 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -42,6 +42,13 @@ const withEmailCodes = base .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-email-codes').pk) .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'); +const withSessionTasks = base + .clone() + .setId('withSessionTasks') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-session-tasks').sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-session-tasks').pk) + .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'); + const withEmailCodes_destroy_client = withEmailCodes .clone() .setEnvVariable('public', 'EXPERIMENTAL_PERSIST_CLIENT', 'false'); @@ -157,4 +164,5 @@ export const envs = { withSignInOrUpFlow, withSignInOrUpEmailLinksFlow, withSignInOrUpwithRestrictedModeFlow, + withSessionTasks, } as const; diff --git a/integration/tests/session-tasks.test.ts b/integration/tests/session-tasks.test.ts index 2ecf8b8da69..6d792c295eb 100644 --- a/integration/tests/session-tasks.test.ts +++ b/integration/tests/session-tasks.test.ts @@ -1,35 +1,44 @@ +import { test } from '@playwright/test'; + import { appConfigs } from '../presets'; -import { testAgainstRunningApps } from '../testUtils'; +import type { FakeUser } from '../testUtils'; +import { createTestUtils, testAgainstRunningApps } from '../testUtils'; -// TODO ORGS-566 - Write integration tests for after-auth flow -testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('after-auth flows @generic @nextjs', () => { - describe('after sign-in', () => { - // /sign-in -> /sign-in/select-organization - it.todo('navigates to tasks'); +testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })('sign in flow @generic @nextjs', ({ app }) => { + test.describe.configure({ mode: 'serial' }); - // /sign-in -> /sign-in/select-organization -> /app (after-sign-in URL) - it.todo('navigates to after-sign-in URL when tasks get resolved'); + let fakeUser: FakeUser; - // with session status pending -> accesses /sign-in -> redirects to /sign-in/select-organization - it.todo('on single-session mode, sign-in redirects back to tasks when accessed with a pending session'); + test.beforeAll(async () => { + const u = createTestUtils({ app }); + fakeUser = u.services.users.createFakeUser({ + withPhoneNumber: true, + withUsername: true, + }); + await u.services.users.createBapiUser(fakeUser); }); - describe('after sign-up', () => { - // /sign-up -> /sign-up/select-organization - it.todo('navigates to tasks'); + test.afterAll(async () => { + await fakeUser.deleteIfExists(); + await app.teardown(); + }); - // /sign-up -> /sign-up/select-organization -> /app/welcome (after-sign-up URL) - it.todo('navigates to after-sign-up URL when tasks get resolved'); + test.describe('after sign-in', () => { + test('navigates to tasks', () => {}); - // with session status pending -> accesses /sign-up -> redirects to /sign-up/select-organization - it.todo('on single-session mode, sign-up redirects back to tasks when accessed with a pending session'); + // with session status pending -> accesses /sign-in -> redirects to /sign-in/select-organization + test('on single-session mode, sign-in redirects back to tasks when accessed with a pending session', () => {}); }); - describe('when user is using the app and session transitions to active to pending', () => { - // /my-dashboard/recipes -> /sign-in/select-organization - it.todo('on session transition to pending with tasks, redirects to tasks'); + test.describe('after sign-in', () => { + test('navigates to tasks', () => {}); - // /my-dashboard/recipes -> /sign-in/select-organization -> /my-dashboard/recipes - it.todo('navigates to middle app origin when tasks get resolved'); + // with session status pending -> accesses /sign-in -> redirects to /sign-in/select-organization + test('on single-session mode, sign-up redirects back to tasks when accessed with a pending session', () => {}); + }); + + test.describe('when user is using the app and session transitions to active to pending', () => { + // /my-dashboard/recipes -> /sign-in/select-organization + test('on session transition to pending with tasks, redirects to tasks', () => {}); }); }); diff --git a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts index 31d1d7046ad..8e405068e48 100644 --- a/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts +++ b/packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts @@ -311,22 +311,4 @@ describe('Clerk singleton - Redirects', () => { expect(mockHref).toHaveBeenNthCalledWith(2, `${host}/?__clerk_db_jwt=deadbeef`); }); }); - - describe('.redirectToTasks', () => { - describe('after sign-in with pending session', () => { - it('redirects to tasks URL with after sign-in URL appended as query param'); - }); - - describe('after sign-up with pending session', () => { - it.todo('redirects to tasks URL with after sign-up URL appended as query param'); - }); - - describe('after sign-up with pending session', () => { - it.todo('redirects to tasks URL with after sign-up URL appended as query param'); - }); - - describe('from a protected route', () => { - it.todo('redirects to tasks URL with app origin appended as query param'); - }); - }); }); diff --git a/packages/clerk-js/src/core/auth/AuthCookieService.ts b/packages/clerk-js/src/core/auth/AuthCookieService.ts index 4ed81e7fc75..b0d079ee710 100644 --- a/packages/clerk-js/src/core/auth/AuthCookieService.ts +++ b/packages/clerk-js/src/core/auth/AuthCookieService.ts @@ -2,7 +2,7 @@ import { isBrowserOnline } from '@clerk/shared/browser'; import { createCookieHandler } from '@clerk/shared/cookie'; import { setDevBrowserJWTInURL } from '@clerk/shared/devBrowser'; import { is4xxError, isClerkAPIResponseError, isNetworkError } from '@clerk/shared/error'; -import type { Clerk, InstanceType } from '@clerk/types'; +import type { Clerk, InstanceType, TokenResource } from '@clerk/types'; import { createOfflineScheduler } from '../../utils/offlineScheduler'; import { clerkCoreErrorTokenRefreshFailed, clerkMissingDevBrowserJwt } from '../errors'; @@ -56,10 +56,14 @@ export class AuthCookieService { cookieSuffix: string, private instanceType: InstanceType, ) { - // set cookie on token update eventBus.on(events.TokenUpdate, ({ token }) => { this.updateSessionCookie(token && token.getRawString()); this.setClientUatCookieForDevelopmentInstances(); + this.handleSessionStatus(token); + }); + + eventBus.on(events.NewSessionTask, () => { + void this.clerk.redirectToTask(); }); this.refreshTokenOnFocus(); @@ -223,4 +227,14 @@ export class AuthCookieService { return this.clerk.organization?.id === activeOrganizationId; } + + private handleSessionStatus(token: TokenResource | null) { + const hasPendingStatus = token?.jwt?.claims.sts === 'pending'; + + if (!hasPendingStatus) { + return; + } + + eventBus.dispatch(events.NewSessionTask, null); + } } diff --git a/packages/clerk-js/src/core/clerk.ts b/packages/clerk-js/src/core/clerk.ts index 2d0ae596298..cb82d1befa6 100644 --- a/packages/clerk-js/src/core/clerk.ts +++ b/packages/clerk-js/src/core/clerk.ts @@ -890,7 +890,7 @@ export class Clerk implements ClerkInterface { let newSession = session === undefined ? this.session : session; - const isResolvingSessionTasks = !!newSession?.currentTask || window.location.href.includes(this.buildTasksUrl()); + const isResolvingSessionTasks = !!newSession?.currentTask; // At this point, the `session` variable should contain either an `SignedInSessionResource` // ,`null` or `undefined`. @@ -1118,9 +1118,8 @@ export class Clerk implements ClerkInterface { return buildURL({ base: waitlistUrl, hashSearchParams: [initValues] }, { stringify: true }); } - public buildTasksUrl(): string { + public buildTaskUrl(): string { const currentTask = this.session?.currentTask; - if (!currentTask) { return ''; } @@ -1130,16 +1129,25 @@ export class Clerk implements ClerkInterface { return ''; } + return buildURL( + { + hashPath: sessionTaskKeyToRoutePaths[currentTask.key], + }, + { stringify: true }, + ); + } + + public buildAfterTasksUrl(): string { const signInUrl = this.#options.signInUrl || this.environment?.displayConfig.signInUrl; const signUpUrl = this.#options.signUpUrl || this.environment?.displayConfig.signUpUrl; const referrerIsSignInUrl = signInUrl && window.location.href.includes(signInUrl); const referrerIsSignUpUrl = signUpUrl && window.location.href.includes(signUpUrl); - let redirectUrl = ''; + let afterTasksUrl = ''; if (referrerIsSignInUrl) { - redirectUrl = this.buildAfterSignInUrl(); + afterTasksUrl = this.buildAfterSignInUrl(); } else if (referrerIsSignUpUrl) { - redirectUrl = this.buildAfterSignUpUrl(); + afterTasksUrl = this.buildAfterSignUpUrl(); } else { /** * User already has a active session and gets transition to a pending status @@ -1149,7 +1157,7 @@ export class Clerk implements ClerkInterface { * need to get resolved by the user, eg: Force MFA */ // Preserves the origin path, eg: /my-app/recipes -> /sign-in/select-organization -> /my-app/recipes - redirectUrl = window.location.href; + afterTasksUrl = window.location.href; } /** @@ -1159,20 +1167,8 @@ export class Clerk implements ClerkInterface { * If it's coming from a protected route where the user already exists, then * the base path becomes the sign in URL */ - const shouldAppendBasePath = !referrerIsSignInUrl && !referrerIsSignUpUrl; - return buildURL( - { - ...(shouldAppendBasePath - ? { - base: signInUrl, - } - : {}), - hashPath: sessionTaskKeyToRoutePaths[currentTask.key], - hashSearchParams: { redirect_url: redirectUrl }, - }, - { stringify: true }, - ); + return afterTasksUrl; } public buildAfterMultiSessionSingleSignOutUrl(): string { @@ -1296,9 +1292,10 @@ export class Clerk implements ClerkInterface { return; }; - public redirectToTasks = async (): Promise => { + // Redirect to intermediary route + public redirectToTask = async (): Promise => { if (inBrowser()) { - return this.navigate(this.buildTasksUrl()); + return this.navigate(this.buildTaskUrl()); } return; }; @@ -1796,22 +1793,12 @@ export class Clerk implements ClerkInterface { if (this.session) { const session = this.#getSessionFromClient(this.session.id); - const hasResolvedPreviousTask = this.session.currentTask != session?.currentTask; - // Note: this might set this.session to null this.#setAccessors(session); // A client response contains its associated sessions, along with a fresh token, so we dispatch a token update event. eventBus.dispatch(events.TokenUpdate, { token: this.session?.lastActiveToken }); - // Tasks handling must be reactive on client piggybacking to support - // immediate instance-level configuration changes by app owners - if (session?.currentTask) { - eventBus.dispatch(events.NewSessionTask, session); - } else if (session && hasResolvedPreviousTask) { - eventBus.dispatch(events.ResolvedSessionTask, session); - } - this.#emit(); } }; @@ -2154,17 +2141,6 @@ export class Clerk implements ClerkInterface { eventBus.on(events.UserSignOut, () => { this.#broadcastChannel?.postMessage({ type: 'signout' }); }); - - eventBus.on(events.NewSessionTask, () => { - void this.redirectToTasks(); - }); - - eventBus.on(events.ResolvedSessionTask, () => { - // `redirect_url` gets appended based on the origin (after sign-in vs after sign-up), - // if it gets accidentally deleted, then fallbacks to the sign in URL - const redirectUrl = new URLSearchParams(window.location.search).get('redirect_url') ?? this.buildSignInUrl(); - void this.navigate(redirectUrl); - }); }; // TODO: Be more conservative about touches. Throttle, don't touch when only one user, etc diff --git a/packages/clerk-js/src/core/events.ts b/packages/clerk-js/src/core/events.ts index fb436223c49..1f2a26cc2d2 100644 --- a/packages/clerk-js/src/core/events.ts +++ b/packages/clerk-js/src/core/events.ts @@ -1,10 +1,9 @@ -import type { SignedInSessionResource, TokenResource } from '@clerk/types'; +import type { TokenResource } from '@clerk/types'; export const events = { TokenUpdate: 'token:update', UserSignOut: 'user:signOut', NewSessionTask: 'sessionTask:new', - ResolvedSessionTask: 'sessionTask:resolve', } as const; type ClerkEvent = (typeof events)[keyof typeof events]; @@ -15,8 +14,7 @@ type TokenUpdatePayload = { token: TokenResource | null }; type EventPayload = { [events.TokenUpdate]: TokenUpdatePayload; [events.UserSignOut]: null; - [events.NewSessionTask]: SignedInSessionResource; - [events.ResolvedSessionTask]: SignedInSessionResource; + [events.NewSessionTask]: null; }; const createEventBus = () => { diff --git a/packages/clerk-js/src/core/resources/Session.ts b/packages/clerk-js/src/core/resources/Session.ts index 4ac8f4ad4a6..bc56c1ece34 100644 --- a/packages/clerk-js/src/core/resources/Session.ts +++ b/packages/clerk-js/src/core/resources/Session.ts @@ -299,6 +299,8 @@ export class Session extends BaseResource implements SessionResource { if (shouldDispatchTokenUpdate) { eventBus.dispatch(events.TokenUpdate, { token }); } + console.log('getToken', { token }); + // Return null when raw string is empty to indicate that there it's signed-out return token.getRawString() || null; }); diff --git a/packages/clerk-js/src/ui/common/tasks.ts b/packages/clerk-js/src/ui/common/tasks.ts index d2465f6a4a3..e23b93a323e 100644 --- a/packages/clerk-js/src/ui/common/tasks.ts +++ b/packages/clerk-js/src/ui/common/tasks.ts @@ -8,5 +8,5 @@ type SessionTaskRoutePath = (typeof sessionTaskRoutePaths)[number]; * @internal */ export const sessionTaskKeyToRoutePaths: Record = { - org: '/select-organization', + org: 'select-organization', }; diff --git a/packages/clerk-js/src/ui/common/withRedirect.tsx b/packages/clerk-js/src/ui/common/withRedirect.tsx index d6d62db4c65..05f633168ea 100644 --- a/packages/clerk-js/src/ui/common/withRedirect.tsx +++ b/packages/clerk-js/src/ui/common/withRedirect.tsx @@ -60,7 +60,7 @@ export const withRedirectToTasks =

(Component return withRedirect( Component, hasPendingTasksAndSingleSessionModeEnabled, - ({ clerk }) => clerk.buildTasksUrl(), + ({ clerk }) => clerk.buildTaskUrl(), warnings.cannotRenderComponentWithPendingTasks, )(props); }; diff --git a/packages/react/src/isomorphicClerk.ts b/packages/react/src/isomorphicClerk.ts index 2bf9024d94a..c067022ab4b 100644 --- a/packages/react/src/isomorphicClerk.ts +++ b/packages/react/src/isomorphicClerk.ts @@ -333,12 +333,12 @@ export class IsomorphicClerk implements IsomorphicLoadedClerk { } }; - buildTasksUrl = (): string | void => { - const callback = () => this.clerkjs?.buildTasksUrl() || ''; + buildTaskUrl = (): string | void => { + const callback = () => this.clerkjs?.buildTaskUrl() || ''; if (this.clerkjs && this.#loaded) { return callback(); } else { - this.premountMethodCalls.set('buildTasksUrl', callback); + this.premountMethodCalls.set('buildTaskUrl', callback); } }; diff --git a/packages/types/src/clerk.ts b/packages/types/src/clerk.ts index 4f3f134bdf7..502b04a0c11 100644 --- a/packages/types/src/clerk.ts +++ b/packages/types/src/clerk.ts @@ -477,9 +477,9 @@ export interface Clerk { buildWaitlistUrl(opts?: { initialValues?: Record }): string; /** - * Returns the URL where tasks get displayed. It defaults to `signInUrl` or `signUpUrl` as the base route. + * Returns the URL where session task get displayed. It appends to `signInUrl` or `signUpUrl` as the base route. */ - buildTasksUrl(): string; + buildTaskUrl(): string; /** * @@ -538,6 +538,11 @@ export interface Clerk { */ redirectToWaitlist: () => void; + /** + * Redirects to the URL to resolve a pending session task + */ + redirectToTask: () => void; + /** * Completes a Google One Tap redirection flow started by * {@link Clerk.authenticateWithGoogleOneTap} diff --git a/packages/types/src/jwt.ts b/packages/types/src/jwt.ts index 3b078fd2986..fea8008e263 100644 --- a/packages/types/src/jwt.ts +++ b/packages/types/src/jwt.ts @@ -1,3 +1,5 @@ +import type { SessionStatus } from 'session'; + import type { OrganizationCustomRoleKey } from './organizationMembership'; export interface JWT { @@ -46,6 +48,11 @@ export interface ClerkJWTClaims { */ sid: string; + /** + * Session status + */ + sts: SessionStatus; + /** * JWT Not Before - [RFC7519#section-4.1.5](https://tools.ietf.org/html/rfc7519#section-4.1.5). */ diff --git a/packages/types/src/jwtv2.ts b/packages/types/src/jwtv2.ts index 2ce8ed3b323..94d253d2456 100644 --- a/packages/types/src/jwtv2.ts +++ b/packages/types/src/jwtv2.ts @@ -1,4 +1,5 @@ import type { OrganizationCustomPermissionKey, OrganizationCustomRoleKey } from './organizationMembership'; +import type { SessionStatus } from './session'; export interface Jwt { header: JwtHeader; @@ -56,6 +57,11 @@ export interface JwtPayload extends CustomJwtSessionClaims { */ sid: string; + /** + * Session status + */ + sts?: SessionStatus; + /** * JWT Not Before - [RFC7519#section-4.1.5](https://tools.ietf.org/html/rfc7519#section-4.1.5). */