Skip to content

chore(clerk-js): Trigger Next.js hooks on session status transition to pending #6511

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mean-jobs-stare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/clerk-js': patch
---

Trigger Next.js hooks on session status transition from `active` to `pending` to update authentication context state
64 changes: 64 additions & 0 deletions packages/clerk-js/src/core/__tests__/clerk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2543,4 +2543,68 @@ describe('Clerk singleton', () => {
});
});
});

describe('updateClient', () => {
afterEach(() => {
// cleanup global window pollution
(window as any).__unstable__onBeforeSetActive = null;
(window as any).__unstable__onAfterSetActive = null;
});

it('runs server revalidation hooks when session transitions from `active` to `pending`', async () => {
const mockOnBeforeSetActive = jest.fn().mockReturnValue(Promise.resolve());
const mockOnAfterSetActive = jest.fn().mockReturnValue(Promise.resolve());
(window as any).__unstable__onBeforeSetActive = mockOnBeforeSetActive;
(window as any).__unstable__onAfterSetActive = mockOnAfterSetActive;

const mockActiveSession = {
id: 'session_1',
status: 'active',
user: { id: 'user_1' },
lastActiveToken: { getRawString: () => 'token_1' },
};

const mockPendingSession = {
id: 'session_1',
status: 'pending',
user: { id: 'user_1' },
lastActiveToken: { getRawString: () => 'token_1' },
};

const mockInitialClient = {
sessions: [mockActiveSession],
signedInSessions: [mockActiveSession],
lastActiveSessionId: 'session_1',
};

const mockUpdatedClient = {
sessions: [mockPendingSession],
signedInSessions: [mockPendingSession],
lastActiveSessionId: 'session_1',
};

const sut = new Clerk(productionPublishableKey);

// Manually set the initial client and session state to simulate active session
// without going through load() or setActive()
sut.updateClient(mockInitialClient as any);

// Verify we start with an active session
expect(sut.session?.status).toBe('active');

// Call updateClient with the new client that has pending session
sut.updateClient(mockUpdatedClient as any);

// Verify hooks were called
await waitFor(() => {
expect(mockOnBeforeSetActive).toHaveBeenCalledTimes(1);
expect(mockOnAfterSetActive).toHaveBeenCalledTimes(1);
});

// Verify that onAfterSetActive was called after onBeforeSetActive
const beforeCallTime = mockOnBeforeSetActive.mock.invocationCallOrder[0];
const afterCallTime = mockOnAfterSetActive.mock.invocationCallOrder[0];
expect(afterCallTime).toBeGreaterThan(beforeCallTime);
});
});
});
17 changes: 17 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2258,6 +2258,23 @@ export class Clerk implements ClerkInterface {
if (this.session) {
const session = this.#getSessionFromClient(this.session.id);

const hasTransitionedToPendingStatus = this.session.status === 'active' && session?.status === 'pending';
if (hasTransitionedToPendingStatus) {
const onBeforeSetActive: SetActiveHook =
typeof window !== 'undefined' && typeof window.__unstable__onBeforeSetActive === 'function'
? window.__unstable__onBeforeSetActive
: noop;

const onAfterSetActive: SetActiveHook =
typeof window !== 'undefined' && typeof window.__unstable__onAfterSetActive === 'function'
? window.__unstable__onAfterSetActive
: noop;

// Execute hooks to update server authentication context and trigger
// page protections in clerkMiddleware or server components
void onBeforeSetActive()?.then?.(() => void onAfterSetActive());
}

// Note: this might set this.session to null
this.#setAccessors(session);

Expand Down