Skip to content

Commit ab31ae8

Browse files
authored
fix(clerk-js): Navigate to tasks when switching sessions (#6273)
1 parent f6a1c35 commit ab31ae8

File tree

8 files changed

+150
-70
lines changed

8 files changed

+150
-70
lines changed

.changeset/tall-dolls-wish.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/clerk-js': patch
3+
---
4+
5+
Navigate to tasks when switching sessions

integration/templates/next-app-router/src/app/page.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { SignedIn, SignedOut, SignIn, UserButton, Protect } from '@clerk/nextjs';
1+
import { Protect, SignedIn, SignedOut, SignIn, UserButton } from '@clerk/nextjs';
22
import Link from 'next/link';
33
import { ClientId } from './client-id';
44

@@ -11,7 +11,7 @@ export default function Home() {
1111
<SignedOut>SignedOut</SignedOut>
1212
<Protect fallback={'SignedOut from protect'}>SignedIn from protect</Protect>
1313
<SignIn
14-
path={'/'}
14+
routing='hash'
1515
signUpUrl={'/sign-up'}
1616
/>
1717
<ul>
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { test } from '@playwright/test';
2+
3+
import { appConfigs } from '../presets';
4+
import type { FakeUser } from '../testUtils';
5+
import { createTestUtils, testAgainstRunningApps } from '../testUtils';
6+
7+
testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
8+
'session tasks multi-session flow @nextjs',
9+
({ app }) => {
10+
test.describe.configure({ mode: 'serial' });
11+
12+
let user1: FakeUser;
13+
let user2: FakeUser;
14+
15+
test.beforeAll(async () => {
16+
const u = createTestUtils({ app });
17+
18+
user1 = u.services.users.createFakeUser();
19+
user2 = u.services.users.createFakeUser();
20+
21+
await u.services.users.createBapiUser(user1);
22+
await u.services.users.createBapiUser(user2);
23+
});
24+
25+
test.afterAll(async () => {
26+
const u = createTestUtils({ app });
27+
await user1.deleteIfExists();
28+
await user2.deleteIfExists();
29+
await u.services.organizations.deleteAll();
30+
await app.teardown();
31+
});
32+
33+
test.afterEach(async ({ page, context }) => {
34+
const u = createTestUtils({ app, page, context });
35+
await u.page.signOut();
36+
await u.page.context().clearCookies();
37+
});
38+
39+
test('when switching sessions, navigate to task', async ({ page, context }) => {
40+
const u = createTestUtils({ app, page, context });
41+
42+
// Performs sign-in
43+
await u.po.signIn.goTo();
44+
await u.po.signIn.setIdentifier(user1.email);
45+
await u.po.signIn.continue();
46+
await u.po.signIn.setPassword(user1.password);
47+
await u.po.signIn.continue();
48+
await u.po.expect.toBeSignedIn();
49+
50+
// Resolves task
51+
const fakeOrganization = Object.assign(u.services.organizations.createFakeOrganization(), {
52+
slug: u.services.organizations.createFakeOrganization().slug + '-with-session-tasks',
53+
});
54+
55+
await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization);
56+
await u.po.expect.toHaveResolvedTask();
57+
58+
// Navigates to after sign-in
59+
await u.page.waitForAppUrl('/');
60+
61+
// Create second user, to initiate a pending session
62+
// Don't resolve task and switch to active session afterwards
63+
await u.po.signIn.goTo();
64+
await u.po.signIn.setIdentifier(user2.email);
65+
await u.po.signIn.continue();
66+
await u.po.signIn.setPassword(user2.password);
67+
await u.po.signIn.continue();
68+
69+
// Sign-in again back with active session
70+
await u.po.signIn.goTo();
71+
await u.po.signIn.setIdentifier(user1.email);
72+
await u.po.signIn.continue();
73+
await u.po.signIn.setPassword(user1.password);
74+
await u.po.signIn.continue();
75+
76+
// Navigate to protected page, with active session, where user button gets rendered
77+
await u.page.goToRelative('/user-button');
78+
79+
// Switch account, to a session that has a pending status
80+
await u.po.userButton.waitForMounted();
81+
await u.po.userButton.toggleTrigger();
82+
await u.po.userButton.waitForPopover();
83+
await u.po.userButton.toHaveVisibleMenuItems([/Manage account/i, /Sign out$/i]);
84+
await u.po.userButton.switchAccount(user2.email);
85+
86+
// Resolve task
87+
await u.po.signIn.waitForMounted();
88+
const fakeOrganization2 = u.services.organizations.createFakeOrganization();
89+
await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization2);
90+
await u.po.expect.toHaveResolvedTask();
91+
92+
// Navigates to after sign-in
93+
await u.page.waitForAppUrl('/');
94+
});
95+
},
96+
);
Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createClerkClient } from '@clerk/backend';
2-
import { expect, test } from '@playwright/test';
2+
import { test } from '@playwright/test';
33

44
import { appConfigs } from '../presets';
55
import { instanceKeys } from '../presets/envs';
@@ -12,46 +12,54 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
1212
({ app }) => {
1313
test.describe.configure({ mode: 'serial' });
1414

15-
let fakeUser: FakeUser;
15+
let user: FakeUser;
1616

1717
test.beforeAll(async () => {
1818
const u = createTestUtils({ app });
19-
fakeUser = u.services.users.createFakeUser();
20-
await u.services.users.createBapiUser(fakeUser);
19+
user = u.services.users.createFakeUser();
20+
await u.services.users.createBapiUser(user);
2121
});
2222

2323
test.afterAll(async () => {
2424
const u = createTestUtils({ app });
25-
await fakeUser.deleteIfExists();
25+
await user.deleteIfExists();
2626
await u.services.organizations.deleteAll();
2727
await app.teardown();
2828
});
2929

30-
test.skip('with email and password, navigate to task on after sign-in', async ({ page, context }) => {
30+
test.afterEach(async ({ page, context }) => {
31+
const u = createTestUtils({ app, page, context });
32+
await u.page.signOut();
33+
await u.page.context().clearCookies();
34+
});
35+
36+
test('with email and password, navigate to task on after sign-in', async ({ page, context }) => {
3137
const u = createTestUtils({ app, page, context });
3238

3339
// Performs sign-in
3440
await u.po.signIn.goTo();
35-
await u.po.signIn.setIdentifier(fakeUser.email);
41+
await u.po.signIn.setIdentifier(user.email);
3642
await u.po.signIn.continue();
37-
await u.po.signIn.setPassword(fakeUser.password);
43+
await u.po.signIn.setPassword(user.password);
3844
await u.po.signIn.continue();
3945
await u.po.expect.toBeSignedIn();
4046

4147
// Redirects back to tasks when accessing protected route by `auth.protect`
4248
await u.page.goToRelative('/page-protected');
43-
expect(page.url()).toContain('tasks');
4449

4550
// Resolves task
46-
const fakeOrganization = u.services.organizations.createFakeOrganization();
51+
const fakeOrganization = Object.assign(u.services.organizations.createFakeOrganization(), {
52+
slug: u.services.organizations.createFakeOrganization().slug + '-with-sign-in-password',
53+
});
54+
await u.po.signIn.waitForMounted();
4755
await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization);
4856
await u.po.expect.toHaveResolvedTask();
4957

5058
// Navigates to after sign-in
5159
await u.page.waitForAppUrl('/');
5260
});
5361

54-
test.skip('with sso, navigate to task on after sign-in', async ({ page, context }) => {
62+
test('with sso, navigate to task on after sign-in', async ({ page, context }) => {
5563
const u = createTestUtils({ app, page, context });
5664

5765
// Create a clerkClient for the OAuth provider instance
@@ -60,32 +68,35 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
6068
publishableKey: instanceKeys.get('oauth-provider').pk,
6169
});
6270
const users = createUserService(client);
63-
fakeUser = users.createFakeUser({
71+
const userFromOAuth = users.createFakeUser({
6472
withUsername: true,
6573
});
6674
// Create the user on the OAuth provider instance so we do not need to sign up twice
67-
await users.createBapiUser(fakeUser);
75+
await users.createBapiUser(userFromOAuth);
6876

6977
// Performs sign-in with SSO
7078
await u.po.signIn.goTo();
7179
await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click();
7280
await u.page.getByText('Sign in to oauth-provider').waitFor();
73-
await u.po.signIn.setIdentifier(fakeUser.email);
81+
await u.po.signIn.setIdentifier(userFromOAuth.email);
7482
await u.po.signIn.continue();
7583
await u.po.signIn.enterTestOtpCode();
7684

7785
// Resolves task
78-
const fakeOrganization = u.services.organizations.createFakeOrganization();
86+
await u.po.signIn.waitForMounted();
87+
const fakeOrganization = Object.assign(u.services.organizations.createFakeOrganization(), {
88+
slug: u.services.organizations.createFakeOrganization().slug + '-with-sign-in-sso',
89+
});
7990
await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization);
8091
await u.po.expect.toHaveResolvedTask();
8192

8293
// Navigates to after sign-in
8394
await u.page.waitForAppUrl('/');
8495

8596
// Delete the user on the OAuth provider instance
86-
await fakeUser.deleteIfExists();
97+
await userFromOAuth.deleteIfExists();
8798
// Delete the user on the app instance.
88-
await u.services.users.deleteIfExists({ email: fakeUser.email });
99+
await u.services.users.deleteIfExists({ email: userFromOAuth.email });
89100
});
90101
},
91102
);
Lines changed: 4 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
import { expect, test } from '@playwright/test';
22

3-
import { createClerkClient } from '@clerk/backend';
43
import { appConfigs } from '../presets';
5-
import { instanceKeys } from '../presets/envs';
64
import type { FakeUser } from '../testUtils';
75
import { createTestUtils, testAgainstRunningApps } from '../testUtils';
8-
import { createUserService } from '../testUtils/usersService';
96

107
testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
118
'session tasks after sign-up flow @nextjs',
@@ -30,7 +27,7 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
3027
await app.teardown();
3128
});
3229

33-
test.skip('navigate to task on after sign-up', async ({ page, context }) => {
30+
test('navigate to task on after sign-up', async ({ page, context }) => {
3431
// Performs sign-up
3532
const u = createTestUtils({ app, page, context });
3633
await u.po.signUp.goTo();
@@ -42,52 +39,17 @@ testAgainstRunningApps({ withEnv: [appConfigs.envs.withSessionTasks] })(
4239

4340
// Redirects back to tasks when accessing protected route by `auth.protect`
4441
await u.page.goToRelative('/page-protected');
45-
expect(page.url()).toContain('tasks');
42+
expect(u.page.url()).toContain('tasks');
4643

4744
// Resolves task
48-
const fakeOrganization = u.services.organizations.createFakeOrganization();
49-
await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization);
50-
await u.po.expect.toHaveResolvedTask();
51-
52-
// Navigates to after sign-up
53-
await u.page.waitForAppUrl('/');
54-
});
55-
56-
test.skip('with sso, navigate to task on after sign-up', async ({ page, context }) => {
57-
const u = createTestUtils({ app, page, context });
58-
59-
// Create a clerkClient for the OAuth provider instance
60-
const client = createClerkClient({
61-
secretKey: instanceKeys.get('oauth-provider').sk,
62-
publishableKey: instanceKeys.get('oauth-provider').pk,
63-
});
64-
const users = createUserService(client);
65-
fakeUser = users.createFakeUser({
66-
withUsername: true,
45+
const fakeOrganization = Object.assign(u.services.organizations.createFakeOrganization(), {
46+
slug: u.services.organizations.createFakeOrganization().slug + '-with-sign-up',
6747
});
68-
// Create the user on the OAuth provider instance so we do not need to sign up twice
69-
await users.createBapiUser(fakeUser);
70-
71-
// Performs sign-up (transfer flow with sign-in) with SSO
72-
await u.po.signIn.goTo();
73-
await u.page.getByRole('button', { name: 'E2E OAuth Provider' }).click();
74-
await u.page.getByText('Sign in to oauth-provider').waitFor();
75-
await u.po.signIn.setIdentifier(fakeUser.email);
76-
await u.po.signIn.continue();
77-
await u.po.signIn.enterTestOtpCode();
78-
79-
// Resolves task
80-
const fakeOrganization = u.services.organizations.createFakeOrganization();
8148
await u.po.sessionTask.resolveForceOrganizationSelectionTask(fakeOrganization);
8249
await u.po.expect.toHaveResolvedTask();
8350

8451
// Navigates to after sign-up
8552
await u.page.waitForAppUrl('/');
86-
87-
// Delete the user on the OAuth provider instance
88-
await fakeUser.deleteIfExists();
89-
// Delete the user on the app instance.
90-
await u.services.users.deleteIfExists({ email: fakeUser.email });
9153
});
9254
},
9355
);

packages/clerk-js/bundlewatch.config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"files": [
3-
{ "path": "./dist/clerk.js", "maxSize": "612.37kB" },
3+
{ "path": "./dist/clerk.js", "maxSize": "614kB" },
44
{ "path": "./dist/clerk.browser.js", "maxSize": "72.2KB" },
55
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "115KB" },
66
{ "path": "./dist/clerk.headless*.js", "maxSize": "55KB" },

packages/clerk-js/src/core/clerk.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,16 +1209,17 @@ export class Clerk implements ClerkInterface {
12091209
}
12101210
}
12111211

1212-
if (newSession?.status === 'pending') {
1213-
await this.#handlePendingSession(newSession);
1214-
return;
1215-
}
1216-
12171212
/**
12181213
* Hint to each framework, that the user will be signed out when `{session: null}` is provided.
12191214
*/
12201215
await onBeforeSetActive(newSession === null ? 'sign-out' : undefined);
12211216

1217+
if (newSession?.status === 'pending') {
1218+
await this.#handlePendingSession(newSession);
1219+
await onAfterSetActive();
1220+
return;
1221+
}
1222+
12221223
//1. setLastActiveSession to passed user session (add a param).
12231224
// Note that this will also update the session's active organization
12241225
// id.
@@ -1301,8 +1302,10 @@ export class Clerk implements ClerkInterface {
13011302
eventBus.emit(events.TokenUpdate, { token: null });
13021303
}
13031304

1304-
// Only triggers navigation for internal routing, in order to not affect custom flows
1305-
if (newSession?.currentTask && this.#componentNavigationContext) {
1305+
// Only triggers navigation for internal AIO components routing or multi-session switch
1306+
const isSwitchingSessions = this.session?.id != session.id;
1307+
const shouldNavigateOnSetActive = this.#componentNavigationContext || isSwitchingSessions;
1308+
if (newSession?.currentTask && shouldNavigateOnSetActive) {
13061309
await navigateToTask(session.currentTask.key, {
13071310
options: this.#options,
13081311
environment: this.environment,
@@ -1317,7 +1320,7 @@ export class Clerk implements ClerkInterface {
13171320

13181321
public __experimental_navigateToTask = async ({ redirectUrlComplete }: NextTaskParams = {}): Promise<void> => {
13191322
/**
1320-
* Invalidate previously cache pages with auth state before navigating
1323+
* Invalidate previously cached pages with auth state before navigating
13211324
*/
13221325
const onBeforeSetActive: SetActiveHook =
13231326
typeof window !== 'undefined' && typeof window.__unstable__onBeforeSetActive === 'function'

packages/testing/src/playwright/unstable/page-objects/userButton.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ export const createUserButtonPageObject = (testArgs: { page: EnhancedPage }) =>
3232
triggerManageAccount: () => {
3333
return page.getByRole('menuitem', { name: /Manage account/i }).click();
3434
},
35+
switchAccount: (emailAddress: string) => {
36+
return page.getByText(emailAddress).click();
37+
},
3538
};
3639

3740
return self;

0 commit comments

Comments
 (0)