Skip to content
Open
6 changes: 6 additions & 0 deletions .changeset/add-signin-reset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
---

Add `reset` method to the sign-in resource.
6 changes: 6 additions & 0 deletions .changeset/cozy-webs-matter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@clerk/clerk-js': patch
'@clerk/shared': patch
---

Add `reset` method to the new signUp resource.
13 changes: 13 additions & 0 deletions packages/clerk-js/src/core/resources/Client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
} from '@clerk/shared/types';

import { unixEpochToDate } from '../../utils/date';
import { eventBus } from '../events';
import { SessionTokenCache } from '../tokenCache';
import { BaseResource, Session, SignIn, SignUp } from './internal';

Expand Down Expand Up @@ -93,6 +94,18 @@ export class Client extends BaseResource implements ClientResource {
});
}

resetSignIn(): void {
this.signIn = new SignIn(null);
// Cast needed because this.signIn is typed as SignInResource (interface), not SignIn (class extending BaseResource)
eventBus.emit('resource:error', { resource: this.signIn as SignIn, error: null });
}

resetSignUp(): void {
this.signUp = new SignUp(null);
// Cast needed because this.signUp is typed as SignUpResource (interface), not SignUp (class extending BaseResource)
eventBus.emit('resource:error', { resource: this.signUp as SignUp, error: null });
}
Comment on lines +97 to +107
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Client.ts resetSignIn and resetSignUp implementation ==="
sed -n '97,107p' packages/clerk-js/src/core/resources/Client.ts

echo ""
echo "=== State onResourceError handler ==="
rg -n -A 15 'onResourceError.*\(' packages/clerk-js/src/core/state.ts | head -40

echo ""
echo "=== State onResourceUpdated handler ==="
rg -n -A 20 'onResourceUpdated.*\(' packages/clerk-js/src/core/state.ts | head -50

echo ""
echo "=== SignIn constructor and initialization ==="
rg -n -A 10 'constructor.*null' packages/clerk-js/src/core/resources/SignIn.ts | head -30

echo ""
echo "=== Check if SignIn/SignUp emit resource:update on construction ==="
rg -n 'resource:update' packages/clerk-js/src/core/resources/SignIn.ts

Repository: clerk/javascript

Length of output: 2887


🏁 Script executed:

#!/bin/bash
echo "=== SignIn fromJSON implementation ==="
rg -n -A 20 'fromJSON.*=' packages/clerk-js/src/core/resources/SignIn.ts | head -50

echo ""
echo "=== shouldIgnoreNullUpdate implementation ==="
rg -n -B 2 -A 10 'shouldIgnoreNullUpdate' packages/clerk-js/src/core/state.ts

echo ""
echo "=== Context of resource:update at line 573 in SignIn.ts ==="
sed -n '565,580p' packages/clerk-js/src/core/resources/SignIn.ts

Repository: clerk/javascript

Length of output: 2223


🏁 Script executed:

#!/bin/bash
echo "=== Full fromJSON method in SignIn.ts ==="
rg -n -B 5 'resource:update.*this' packages/clerk-js/src/core/resources/SignIn.ts | grep -A 50 'fromJSON'

echo ""
echo "=== Check fromJSON method signature and start ==="
rg -n 'fromJSON\s*=' packages/clerk-js/src/core/resources/SignIn.ts -A 25 | head -40

echo ""
echo "=== Check if there's a condition on emitting resource:update ==="
sed -n '550,580p' packages/clerk-js/src/core/resources/SignIn.ts

Repository: clerk/javascript

Length of output: 1532


🏁 Script executed:

#!/bin/bash
echo "=== Check BaseResource for __internal_future property ==="
rg -n '__internal_future' packages/clerk-js/src/core/resources/ -B 2 -A 2

echo ""
echo "=== Check if canBeDiscarded is set anywhere in SignIn/SignUp ==="
rg -n 'canBeDiscarded' packages/clerk-js/src/core/resources/

echo ""
echo "=== How does __internal_future get set initially in BaseResource ==="
rg -n '__internal_future' packages/clerk-js/src/core/resources/BaseResource.ts -B 3 -A 3

Repository: clerk/javascript

Length of output: 50482


🏁 Script executed:

#!/bin/bash
echo "=== Search for canBeDiscarded implementation ==="
rg -n 'canBeDiscarded' packages/clerk-js/src/core/resources/ -B 3 -A 3

echo ""
echo "=== Check reset method in SignUpFuture ==="
rg -n -A 25 'reset.*=' packages/clerk-js/src/core/resources/SignUpFuture.ts | head -60

Repository: clerk/javascript

Length of output: 4761


🏁 Script executed:

#!/bin/bash
echo "=== Check tests for resetSignIn/resetSignUp behavior ==="
rg -n -B 5 -A 15 'resetSignIn|resetSignUp' packages/clerk-js/src/core/resources/__tests__/Client.test.ts 2>/dev/null | head -80

echo ""
echo "=== Check if there's a test for signal update after reset ==="
rg -n 'signInResourceSignal.*reset' packages/clerk-js/src/core/ -B 5 -A 5

echo ""
echo "=== Check the full reset flow in SignInFuture/SignUpFuture ==="
rg -n -B 2 -A 8 'reset\(\)' packages/clerk-js/src/core/resources/SignInFuture.ts 2>/dev/null | head -30

echo ""
echo "=== Check what happens when you create SignIn with null ==="
sed -n '160,175p' packages/clerk-js/src/core/resources/SignIn.ts

Repository: clerk/javascript

Length of output: 872


🏁 Script executed:

#!/bin/bash
echo "=== Check if there are tests showing expected behavior after reset ==="
rg -n 'reset.*signal\|signal.*reset' packages/clerk-js/src/core/resources/__tests__/ -B 5 -A 10 | head -100

echo ""
echo "=== Look at full SignUpFuture reset implementation ==="
rg -n -B 5 -A 10 'async reset' packages/clerk-js/src/core/resources/SignUp.ts

echo ""
echo "=== Look at full SignInFuture reset implementation ==="
rg -n -B 5 -A 10 'async reset' packages/clerk-js/src/core/resources/SignIn.ts

Repository: clerk/javascript

Length of output: 242


🏁 Script executed:

#!/bin/bash
echo "=== Find Future class files ==="
fd -t f 'Future' packages/clerk-js/src/core/resources/

echo ""
echo "=== Search for reset method in SignUp.ts ==="
grep -n "reset" packages/clerk-js/src/core/resources/SignUp.ts | head -20

echo ""
echo "=== Get the reset method from SignUp.ts ==="
sed -n '980,1000p' packages/clerk-js/src/core/resources/SignUp.ts

echo ""
echo "=== Get the reset method from SignIn.ts ==="
sed -n '1250,1270p' packages/clerk-js/src/core/resources/SignIn.ts

Repository: clerk/javascript

Length of output: 1948


🏁 Script executed:

#!/bin/bash
echo "=== Verify the reset flow: when is canBeDiscarded set? ==="
echo "In SignUpFuture.reset():"
sed -n '984,991p' packages/clerk-js/src/core/resources/SignUp.ts

echo ""
echo "Then Client.resetSignUp() is called:"
sed -n '104,107p' packages/clerk-js/src/core/resources/Client.ts

echo ""
echo "=== Check if shouldIgnoreNullUpdate would allow the update if canBeDiscarded is true ==="
sed -n '115,119p' packages/clerk-js/src/core/state.ts

echo ""
echo "=== Simulate the flow: does emitting resource:update fix it? ==="
echo "After Future.reset() sets canBeDiscarded=true, then Client.resetSignUp() is called"
echo "If resource:update is emitted, State.onResourceUpdated will:"
echo "1. Call shouldIgnoreNullUpdate(previousResource, newResource)"
echo "2. previousResource.__internal_future.canBeDiscarded is NOW true (set by Future.reset())"
echo "3. So shouldIgnoreNullUpdate returns: !newResource?.id && previousResource && false"
echo "4. Which is: true && true && false = FALSE"
echo "5. So the signal WILL be updated"

Repository: clerk/javascript

Length of output: 1747


🏁 Script executed:

#!/bin/bash
echo "=== Search for Client reset tests ==="
find packages/clerk-js/src -name "*.test.ts" -exec grep -l "resetSignIn\|resetSignUp" {} \;

echo ""
echo "=== Check if any tests verify signInResourceSignal is updated after reset ==="
rg -n 'signInResourceSignal' packages/clerk-js/src/core/resources/__tests__/

Repository: clerk/javascript

Length of output: 966


Emit resource:update event to update resource signal after reset

resetSignIn()/resetSignUp() only emit resource:error, which updates the error signal but not the resource signal. The signInResourceSignal/signUpResourceSignal never reflects the new reset instance because State's onResourceUpdated handler is never called.

When resource:update is emitted after canBeDiscarded is set to true (by the Future.reset() call), the shouldIgnoreNullUpdate check will pass and the signal will be properly updated.

Proposed fix
  resetSignIn(): void {
    this.signIn = new SignIn(null);
-   // Cast needed because this.signIn is typed as SignInResource (interface), not SignIn (class extending BaseResource)
-   eventBus.emit('resource:error', { resource: this.signIn as SignIn, error: null });
+   eventBus.emit('resource:update', { resource: this.signIn as SignIn });
+   eventBus.emit('resource:error', { resource: this.signIn as SignIn, error: null });
  }

  resetSignUp(): void {
    this.signUp = new SignUp(null);
-   // Cast needed because this.signUp is typed as SignUpResource (interface), not SignUp (class extending BaseResource)
-   eventBus.emit('resource:error', { resource: this.signUp as SignUp, error: null });
+   eventBus.emit('resource:update', { resource: this.signUp as SignUp });
+   eventBus.emit('resource:error', { resource: this.signUp as SignUp, error: null });
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
resetSignIn(): void {
this.signIn = new SignIn(null);
// Cast needed because this.signIn is typed as SignInResource (interface), not SignIn (class extending BaseResource)
eventBus.emit('resource:error', { resource: this.signIn as SignIn, error: null });
}
resetSignUp(): void {
this.signUp = new SignUp(null);
// Cast needed because this.signUp is typed as SignUpResource (interface), not SignUp (class extending BaseResource)
eventBus.emit('resource:error', { resource: this.signUp as SignUp, error: null });
}
resetSignIn(): void {
this.signIn = new SignIn(null);
eventBus.emit('resource:update', { resource: this.signIn as SignIn });
eventBus.emit('resource:error', { resource: this.signIn as SignIn, error: null });
}
resetSignUp(): void {
this.signUp = new SignUp(null);
eventBus.emit('resource:update', { resource: this.signUp as SignUp });
eventBus.emit('resource:error', { resource: this.signUp as SignUp, error: null });
}
🤖 Prompt for AI Agents
In `@packages/clerk-js/src/core/resources/Client.ts` around lines 97 - 107,
resetSignIn() and resetSignUp() currently only emit 'resource:error' so the
resource signals (signInResourceSignal/signUpResourceSignal) are not updated;
after creating the new instance and emitting the error event, also emit a
'resource:update' event via eventBus.emit('resource:update', { resource:
this.signIn as SignIn }) and similarly for this.signUp (cast as needed) so
State.onResourceUpdated runs and the resource signals reflect the new reset
instance.


clearCache(): void {
return this.sessions.forEach(s => s.clearCache());
}
Expand Down
22 changes: 18 additions & 4 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -650,7 +650,7 @@ class SignInFuture implements SignInFutureResource {
verifyBackupCode: this.verifyBackupCode.bind(this),
};

#hasBeenFinalized = false;
#canBeDiscarded = false;
readonly #resource: SignIn;

constructor(resource: SignIn) {
Expand Down Expand Up @@ -710,8 +710,8 @@ class SignInFuture implements SignInFutureResource {
return this.#resource.secondFactorVerification;
}

get hasBeenFinalized() {
return this.#hasBeenFinalized;
get canBeDiscarded() {
return this.#canBeDiscarded;
}

async sendResetPasswordEmailCode(): Promise<{ error: ClerkError | null }> {
Expand Down Expand Up @@ -1242,11 +1242,25 @@ class SignInFuture implements SignInFutureResource {
await SignIn.clerk.client.reload();
}

this.#hasBeenFinalized = true;
this.#canBeDiscarded = true;
await SignIn.clerk.setActive({ session: this.#resource.createdSessionId, navigate });
});
}

/**
* Resets the current sign-in attempt by clearing all local state back to null.
* Unlike other methods, this does NOT emit resource:fetch with 'fetching' status,
* allowing for smooth UI transitions without loading states.
*/
reset(): Promise<{ error: ClerkError | null }> {
if (!SignIn.clerk.client) {
throw new Error('Cannot reset sign-in without a client.');
}
this.#canBeDiscarded = true;
SignIn.clerk.client.resetSignIn();
return Promise.resolve({ error: null });
}

private selectFirstFactor(
params: Extract<SelectFirstFactorParams, { strategy: 'email_code' }>,
): EmailCodeFactor | null;
Expand Down
22 changes: 18 additions & 4 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,7 @@ class SignUpFuture implements SignUpFutureResource {
verifyPhoneCode: this.verifyPhoneCode.bind(this),
};

#hasBeenFinalized = false;
#canBeDiscarded = false;
readonly #resource: SignUp;

constructor(resource: SignUp) {
Expand Down Expand Up @@ -701,8 +701,8 @@ class SignUpFuture implements SignUpFutureResource {
return undefined;
}

get hasBeenFinalized() {
return this.#hasBeenFinalized;
get canBeDiscarded() {
return this.#canBeDiscarded;
}

private async getCaptchaToken(): Promise<{
Expand Down Expand Up @@ -971,10 +971,24 @@ class SignUpFuture implements SignUpFutureResource {
throw new Error('Cannot finalize sign-up without a created session.');
}

this.#hasBeenFinalized = true;
this.#canBeDiscarded = true;
await SignUp.clerk.setActive({ session: this.#resource.createdSessionId, navigate });
});
}

/**
* Resets the current sign-up attempt by clearing all local state back to null.
* Unlike other methods, this does NOT emit resource:fetch with 'fetching' status,
* allowing for smooth UI transitions without loading states.
*/
reset(): Promise<{ error: ClerkError | null }> {
if (!SignUp.clerk.client) {
throw new Error('Cannot reset sign-up without a client.');
}
this.#canBeDiscarded = true;
SignUp.clerk.client.resetSignUp();
return Promise.resolve({ error: null });
}
}

class SignUpEnterpriseConnection extends BaseResource implements SignUpEnterpriseConnectionResource {
Expand Down
116 changes: 115 additions & 1 deletion packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

import { eventBus } from '../../events';
import { signInErrorSignal, signInResourceSignal } from '../../signals';
import { BaseResource } from '../internal';
import { SignIn } from '../SignIn';

Expand Down Expand Up @@ -1890,5 +1892,117 @@ describe('SignIn', () => {
await expect(signIn.__internal_future.finalize()).rejects.toThrow();
});
});

describe('reset', () => {
let mockClient: { signIn: SignIn; resetSignIn: ReturnType<typeof vi.fn> };

beforeEach(() => {
// Set up mock client with resetSignIn method that simulates what the real
// Client.resetSignIn does: creates a new SignIn, updates signals via events,
// and the State class responds by updating the actual signal values
mockClient = {
signIn: new SignIn(null),
resetSignIn: vi.fn().mockImplementation(function (this: typeof mockClient) {
const newSignIn = new SignIn(null);
this.signIn = newSignIn;
// Emit events like the real implementation
eventBus.emit('resource:error', { resource: newSignIn, error: null });
// Also update signals directly since State isn't set up in tests
signInResourceSignal({ resource: newSignIn });
signInErrorSignal({ error: null });
}),
};
SignIn.clerk = {
client: mockClient,
} as any;
});

afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
// Reset signals to initial state
signInResourceSignal({ resource: null });
signInErrorSignal({ error: null });
});

it('does NOT emit resource:fetch with status fetching', async () => {
const emitSpy = vi.spyOn(eventBus, 'emit');
const mockFetch = vi.fn();
BaseResource._fetch = mockFetch;

const signIn = new SignIn({ id: 'signin_123', status: 'needs_first_factor' } as any);
await signIn.__internal_future.reset();

// Verify that resource:fetch was NOT called with status: 'fetching'
const fetchingCalls = emitSpy.mock.calls.filter(
call => call[0] === 'resource:fetch' && call[1]?.status === 'fetching',
);
expect(fetchingCalls).toHaveLength(0);
// Verify no API calls were made
expect(mockFetch).not.toHaveBeenCalled();
});

it('clears any previous errors by updating signInErrorSignal', async () => {
// Set an initial error
signInErrorSignal({ error: new Error('Previous error') });
expect(signInErrorSignal().error).toBeTruthy();

const signIn = new SignIn({ id: 'signin_123', status: 'needs_first_factor' } as any);
await signIn.__internal_future.reset();

// Verify that error signal was cleared
expect(signInErrorSignal().error).toBeNull();
});

it('returns error: null on success', async () => {
const signIn = new SignIn({ id: 'signin_123', status: 'needs_first_factor' } as any);
const result = await signIn.__internal_future.reset();

expect(result).toHaveProperty('error', null);
});

it('resets an existing signin with data to a fresh null state', async () => {
const signIn = new SignIn({
id: 'signin_123',
status: 'needs_first_factor',
identifier: 'user@example.com',
} as any);

// Verify initial state
expect(signIn.id).toBe('signin_123');
expect(signIn.status).toBe('needs_first_factor');
expect(signIn.identifier).toBe('user@example.com');

await signIn.__internal_future.reset();

// Verify that signInResourceSignal was updated with a new SignIn(null) instance
const updatedSignIn = signInResourceSignal().resource;
expect(updatedSignIn).toBeInstanceOf(SignIn);
expect(updatedSignIn?.id).toBeUndefined();
expect(updatedSignIn?.status).toBeNull();
expect(updatedSignIn?.identifier).toBeNull();
});

it('updates clerk.client.signIn with the fresh null instance', async () => {
const originalSignIn = new SignIn({
id: 'signin_123',
status: 'needs_first_factor',
identifier: 'user@example.com',
} as any);
mockClient.signIn = originalSignIn;

// Verify initial state
expect(mockClient.signIn.id).toBe('signin_123');
expect(mockClient.signIn.status).toBe('needs_first_factor');

await originalSignIn.__internal_future.reset();

// Verify that clerk.client.signIn was updated with a new SignIn(null) instance
expect(mockClient.signIn).toBeInstanceOf(SignIn);
expect(mockClient.signIn.id).toBeUndefined();
expect(mockClient.signIn.status).toBeNull();
expect(mockClient.signIn.identifier).toBeNull();
});
});
});
});
121 changes: 121 additions & 0 deletions packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';

import { eventBus } from '../../events';
import { signUpErrorSignal, signUpResourceSignal } from '../../signals';
import { BaseResource } from '../internal';
import { SignUp } from '../SignUp';

Expand Down Expand Up @@ -701,5 +703,124 @@ describe('SignUp', () => {
expect(result.error).toBeInstanceOf(Error);
});
});

describe('reset', () => {
let mockClient: { signUp: SignUp; resetSignUp: ReturnType<typeof vi.fn> };

beforeEach(() => {
// Set up mock client with resetSignUp method that simulates what the real
// Client.resetSignUp does: creates a new SignUp, updates signals via events,
// and the State class responds by updating the actual signal values
mockClient = {
signUp: new SignUp(null),
resetSignUp: vi.fn().mockImplementation(function (this: typeof mockClient) {
const newSignUp = new SignUp(null);
this.signUp = newSignUp;
// Emit events like the real implementation
eventBus.emit('resource:error', { resource: newSignUp, error: null });
// Also update signals directly since State isn't set up in tests
signUpResourceSignal({ resource: newSignUp });
signUpErrorSignal({ error: null });
}),
};
SignUp.clerk = {
client: mockClient,
} as any;
});

afterEach(() => {
vi.clearAllMocks();
vi.restoreAllMocks();
// Reset signals to initial state
signUpResourceSignal({ resource: null });
signUpErrorSignal({ error: null });
});

it('does NOT emit resource:fetch with status fetching', async () => {
const emitSpy = vi.spyOn(eventBus, 'emit');
const mockFetch = vi.fn();
BaseResource._fetch = mockFetch;

const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);
await signUp.__internal_future.reset();

// Verify that resource:fetch was NOT called with status: 'fetching'
const fetchingCalls = emitSpy.mock.calls.filter(
call => call[0] === 'resource:fetch' && call[1]?.status === 'fetching',
);
expect(fetchingCalls).toHaveLength(0);
// Verify no API calls were made
expect(mockFetch).not.toHaveBeenCalled();
});

it('clears any previous errors by updating signUpErrorSignal', async () => {
// Set an initial error
signUpErrorSignal({ error: new Error('Previous error') });
expect(signUpErrorSignal().error).toBeTruthy();

const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);
await signUp.__internal_future.reset();

// Verify that error signal was cleared
expect(signUpErrorSignal().error).toBeNull();
});

it('returns error: null on success', async () => {
const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any);
const result = await signUp.__internal_future.reset();

expect(result).toHaveProperty('error', null);
});

it('resets an existing signup with data to a fresh null state', async () => {
const signUp = new SignUp({
id: 'signup_123',
status: 'missing_requirements',
email_address: 'user@example.com',
first_name: 'John',
} as any);

// Verify initial state
expect(signUp.id).toBe('signup_123');
expect(signUp.emailAddress).toBe('user@example.com');
expect(signUp.firstName).toBe('John');

await signUp.__internal_future.reset();

// Verify that signUpResourceSignal was updated with a new SignUp(null) instance
const updatedSignUp = signUpResourceSignal().resource;
expect(updatedSignUp).toBeInstanceOf(SignUp);
expect(updatedSignUp?.id).toBeUndefined();
expect(updatedSignUp?.status).toBeNull();
expect(updatedSignUp?.emailAddress).toBeNull();
expect(updatedSignUp?.firstName).toBeNull();
expect(updatedSignUp?.lastName).toBeNull();
expect(updatedSignUp?.phoneNumber).toBeNull();
});

it('updates clerk.client.signUp with the fresh null instance', async () => {
const originalSignUp = new SignUp({
id: 'signup_123',
status: 'missing_requirements',
email_address: 'user@example.com',
first_name: 'John',
} as any);
mockClient.signUp = originalSignUp;

// Verify initial state
expect(mockClient.signUp.id).toBe('signup_123');
expect(mockClient.signUp.status).toBe('missing_requirements');
expect(mockClient.signUp.emailAddress).toBe('user@example.com');

await originalSignUp.__internal_future.reset();

// Verify that clerk.client.signUp was updated with a new SignUp(null) instance
expect(mockClient.signUp).toBeInstanceOf(SignUp);
expect(mockClient.signUp.id).toBeUndefined();
expect(mockClient.signUp.status).toBeNull();
expect(mockClient.signUp.emailAddress).toBeNull();
expect(mockClient.signUp.firstName).toBeNull();
});
});
});
});
Loading
Loading