Skip to content

Commit

Permalink
Add with-session-tasks to integration tests
Browse files Browse the repository at this point in the history
  • Loading branch information
LauraBeatris committed Feb 26, 2025
1 parent b9f8870 commit 0a1d36d
Show file tree
Hide file tree
Showing 15 changed files with 108 additions and 97 deletions.
2 changes: 1 addition & 1 deletion .changeset/old-cherries-laugh.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@
'@clerk/types': patch
---

Navigate to session tasks
Navigate to session task
4 changes: 4 additions & 0 deletions integration/.keys.json.sample
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,9 @@
"with-waitlist-mode": {
"pk": "",
"sk": ""
},
"with-session-tasks": {
"pk": "",
"sk": ""
}
}
8 changes: 8 additions & 0 deletions integration/presets/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -157,4 +164,5 @@ export const envs = {
withSignInOrUpFlow,
withSignInOrUpEmailLinksFlow,
withSignInOrUpwithRestrictedModeFlow,
withSessionTasks,
} as const;
53 changes: 31 additions & 22 deletions integration/tests/session-tasks.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {});
});
});
18 changes: 0 additions & 18 deletions packages/clerk-js/src/core/__tests__/clerk.redirects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
});
18 changes: 16 additions & 2 deletions packages/clerk-js/src/core/auth/AuthCookieService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}
}
62 changes: 19 additions & 43 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down Expand Up @@ -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 '';
}
Expand All @@ -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
Expand All @@ -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;
}

/**
Expand All @@ -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 {
Expand Down Expand Up @@ -1296,9 +1292,10 @@ export class Clerk implements ClerkInterface {
return;
};

public redirectToTasks = async (): Promise<unknown> => {
// Redirect to intermediary route
public redirectToTask = async (): Promise<unknown> => {
if (inBrowser()) {
return this.navigate(this.buildTasksUrl());
return this.navigate(this.buildTaskUrl());
}
return;
};
Expand Down Expand Up @@ -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();
}
};
Expand Down Expand Up @@ -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
Expand Down
6 changes: 2 additions & 4 deletions packages/clerk-js/src/core/events.ts
Original file line number Diff line number Diff line change
@@ -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];
Expand All @@ -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 = () => {
Expand Down
2 changes: 2 additions & 0 deletions packages/clerk-js/src/core/resources/Session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
});
Expand Down
2 changes: 1 addition & 1 deletion packages/clerk-js/src/ui/common/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ type SessionTaskRoutePath = (typeof sessionTaskRoutePaths)[number];
* @internal
*/
export const sessionTaskKeyToRoutePaths: Record<SessionTask['key'], SessionTaskRoutePath> = {
org: '/select-organization',
org: 'select-organization',
};
2 changes: 1 addition & 1 deletion packages/clerk-js/src/ui/common/withRedirect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export const withRedirectToTasks = <P extends AvailableComponentProps>(Component
return withRedirect(
Component,
hasPendingTasksAndSingleSessionModeEnabled,
({ clerk }) => clerk.buildTasksUrl(),
({ clerk }) => clerk.buildTaskUrl(),
warnings.cannotRenderComponentWithPendingTasks,
)(props);
};
Expand Down
6 changes: 3 additions & 3 deletions packages/react/src/isomorphicClerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
};

Expand Down
9 changes: 7 additions & 2 deletions packages/types/src/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,9 +477,9 @@ export interface Clerk {
buildWaitlistUrl(opts?: { initialValues?: Record<string, string> }): 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;

/**
*
Expand Down Expand Up @@ -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}
Expand Down
Loading

0 comments on commit 0a1d36d

Please sign in to comment.