Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .changeset/warn_pending_session_status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/shared': patch
'@clerk/clerk-js': patch
---

Add a console warning when a session is in the `pending` state. The SDK treats pending sessions as signed out (`isSignedIn` returns `false`, `useAuth` reports signed out) while remaining session tasks are completed, which previously gave no feedback to developers. This brings the JS SDK in line with the equivalent logger on iOS and Android.
5 changes: 5 additions & 0 deletions packages/clerk-js/src/core/clerk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
getTaskEndpoint,
navigateIfTaskExists,
warnMissingPendingTaskHandlers,
warnPendingSessionStatus,
} from '@clerk/shared/internal/clerk-js/sessionTasks';
import { warnings } from '@clerk/shared/internal/clerk-js/warnings';
import { windowNavigate } from '@clerk/shared/internal/clerk-js/windowNavigate';
Expand Down Expand Up @@ -2892,6 +2893,10 @@ export class Clerk implements ClerkInterface {
eventBus.emit(events.TokenUpdate, { token: this.session?.lastActiveToken });
}

if (this.session?.status === 'pending') {
warnPendingSessionStatus(this.session);
}

if (!options?.__internal_dangerouslySkipEmit) {
this.#emit();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { warnPendingSessionStatus } from '../sessionTasks';

describe('warnPendingSessionStatus', () => {
let warnSpy: ReturnType<typeof vi.spyOn>;

beforeEach(() => {
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});

afterEach(() => {
warnSpy.mockRestore();
});

it('does not warn when status is active', () => {
warnPendingSessionStatus({ id: 'sess_active', status: 'active', currentTask: undefined as any });
expect(warnSpy).not.toHaveBeenCalled();
});

it('warns when status is pending and includes the session id', () => {
warnPendingSessionStatus({
id: 'sess_pending_1',
status: 'pending',
currentTask: { key: 'choose-organization' },
});
expect(warnSpy).toHaveBeenCalledTimes(1);
expect(warnSpy.mock.calls[0][0]).toContain('sess_pending_1');
expect(warnSpy.mock.calls[0][0]).toContain('pending');
});

it('includes the current task key in the message', () => {
warnPendingSessionStatus({
id: 'sess_pending_2',
status: 'pending',
currentTask: { key: 'choose-organization' },
});
expect(warnSpy.mock.calls[0][0]).toContain('choose-organization');
});

it('omits the tasks suffix when no current task is present', () => {
warnPendingSessionStatus({
id: 'sess_pending_3',
status: 'pending',
currentTask: undefined as any,
});
expect(warnSpy.mock.calls[0][0]).not.toContain('Remaining session tasks');
});

it('dedupes identical messages across calls', () => {
warnPendingSessionStatus({
id: 'sess_pending_dedupe',
status: 'pending',
currentTask: { key: 'choose-organization' },
});
warnPendingSessionStatus({
id: 'sess_pending_dedupe',
status: 'pending',
currentTask: { key: 'choose-organization' },
});
expect(warnSpy).toHaveBeenCalledTimes(1);
});

it('logs again when the task key changes within the same session', () => {
warnPendingSessionStatus({
id: 'sess_pending_task_change',
status: 'pending',
currentTask: { key: 'choose-organization' },
});
warnPendingSessionStatus({
id: 'sess_pending_task_change',
status: 'pending',
currentTask: { key: 'setup-mfa' },
});
expect(warnSpy).toHaveBeenCalledTimes(2);
});
});
24 changes: 24 additions & 0 deletions packages/shared/src/internal/clerk-js/sessionTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,27 @@ export function warnMissingPendingTaskHandlers(options: Record<string, unknown>)
`Clerk: Session has pending tasks but no handling is configured. To handle pending tasks, provide either "taskUrls" for navigation to custom URLs or "navigate" for programmatic navigation. Without these options, users may get stuck on incomplete flows.`,
);
}

/**
* Warns when a session is in the `pending` state. Mirrors the behavior of
* `SessionStatusLogger` on iOS / Android so developers across platforms get the
* same heads-up that the SDK is treating their signed-in user as signed out
* until the remaining session task is resolved.
*
* Dedupe is per-session/per-task: a new pending session or a task transition
* within the same session both surface a fresh log line.
*/
export function warnPendingSessionStatus(session: Pick<SessionResource, 'id' | 'status' | 'currentTask'>) {
if (session.status !== 'pending') {
return;
}

const taskKey = session.currentTask?.key;
const tasksDescription = taskKey ? ` Remaining session tasks: [${taskKey}].` : '';

logger.warnOnce(
`Clerk: Session ${session.id} is currently pending. ` +
`\`isSignedIn\` will return false and \`useAuth\` will report the user as signed out ` +
`until the remaining session tasks are completed.${tasksDescription}`,
);
}
Loading