From 3dc0055c7eb2705bc713a83048eda8f858eaad42 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 15 Jan 2026 14:04:33 -0500 Subject: [PATCH 1/7] feat(clerk-js): Add `reset` method to SignUp resource --- .../clerk-js/src/core/resources/SignUp.ts | 18 +++ .../core/resources/__tests__/SignUp.test.ts | 125 ++++++++++++++++++ packages/shared/src/types/signUpFuture.ts | 9 ++ 3 files changed, 152 insertions(+) diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 53d3b0647d9..48b2f6e14f8 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -975,6 +975,24 @@ class SignUpFuture implements SignUpFutureResource { await SignUp.clerk.setActive({ session: this.#resource.createdSessionId, navigate }); }); } + + /** + * Resets the current sign-up attempt by creating a new empty sign-up. + * Unlike other methods, this does NOT emit resource:fetch with 'fetching' status, + * allowing for smooth UI transitions without loading states. + */ + async reset(): Promise<{ error: ClerkError | null }> { + // Clear any previous errors + eventBus.emit('resource:error', { resource: this.#resource, error: null }); + + try { + await this.#resource.__internal_basePost({ path: this.#resource.pathRoot, body: {} }); + return { error: null }; + } catch (err) { + eventBus.emit('resource:error', { resource: this.#resource, error: err }); + return { error: err }; + } + } } class SignUpEnterpriseConnection extends BaseResource implements SignUpEnterpriseConnectionResource { diff --git a/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts index 663fa63d6d2..a60fa84cf27 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { eventBus } from '../../events'; import { BaseResource } from '../internal'; import { SignUp } from '../SignUp'; @@ -701,5 +702,129 @@ describe('SignUp', () => { expect(result.error).toBeInstanceOf(Error); }); }); + + describe('reset', () => { + afterEach(() => { + vi.clearAllMocks(); + vi.restoreAllMocks(); + }); + + it('creates a new signup by POSTing to /client/sign_ups with empty body', async () => { + const mockFetch = vi.fn().mockResolvedValue({ + client: null, + response: { id: 'signup_new', status: 'missing_requirements' }, + }); + BaseResource._fetch = mockFetch; + + const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any); + await signUp.__internal_future.reset(); + + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ + method: 'POST', + path: '/client/sign_ups', + body: {}, + }), + ); + }); + + it('does NOT emit resource:fetch with status fetching', async () => { + const emitSpy = vi.spyOn(eventBus, 'emit'); + const mockFetch = vi.fn().mockResolvedValue({ + client: null, + response: { id: 'signup_new', status: 'missing_requirements' }, + }); + 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); + }); + + it('clears any previous errors by emitting resource:error with null', async () => { + const emitSpy = vi.spyOn(eventBus, 'emit'); + const mockFetch = vi.fn().mockResolvedValue({ + client: null, + response: { id: 'signup_new', status: 'missing_requirements' }, + }); + BaseResource._fetch = mockFetch; + + const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any); + await signUp.__internal_future.reset(); + + // Verify that resource:error was called to clear previous errors + expect(emitSpy).toHaveBeenCalledWith('resource:error', { + resource: signUp, + error: null, + }); + }); + + it('returns error: null on success', async () => { + const mockFetch = vi.fn().mockResolvedValue({ + client: null, + response: { id: 'signup_new', status: 'missing_requirements' }, + }); + BaseResource._fetch = mockFetch; + + 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('returns error and emits resource:error on failure', async () => { + const emitSpy = vi.spyOn(eventBus, 'emit'); + const mockError = new Error('API error'); + const mockFetch = vi.fn().mockRejectedValue(mockError); + BaseResource._fetch = mockFetch; + + const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any); + const result = await signUp.__internal_future.reset(); + + expect(result.error).toBe(mockError); + expect(emitSpy).toHaveBeenCalledWith('resource:error', { + resource: signUp, + error: mockError, + }); + }); + + it('resets an existing signup with data to a fresh state', async () => { + const mockFetch = vi.fn().mockResolvedValue({ + client: null, + response: { + id: 'signup_new', + status: 'missing_requirements', + email_address: null, + phone_number: null, + first_name: null, + last_name: null, + }, + }); + BaseResource._fetch = mockFetch; + + 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.emailAddress).toBe('user@example.com'); + expect(signUp.firstName).toBe('John'); + + await signUp.__internal_future.reset(); + + // After reset, the signup should have new values from the response + expect(signUp.id).toBe('signup_new'); + expect(signUp.emailAddress).toBeNull(); + expect(signUp.firstName).toBeNull(); + }); + }); }); }); diff --git a/packages/shared/src/types/signUpFuture.ts b/packages/shared/src/types/signUpFuture.ts index d44ba2e534c..68891edc7b8 100644 --- a/packages/shared/src/types/signUpFuture.ts +++ b/packages/shared/src/types/signUpFuture.ts @@ -463,4 +463,13 @@ export interface SignUpFutureResource { * session state (such as the `useUser()` hook) to update automatically. */ finalize: (params?: SignUpFutureFinalizeParams) => Promise<{ error: ClerkError | null }>; + + /** + * Resets the current sign-up attempt by creating a new empty sign-up. This is useful when you want to allow users + * to go back to the beginning of the sign-up flow (e.g., to change their email address during verification). + * + * Unlike other methods, `reset()` does not trigger the `fetchStatus` to change to `'fetching'`, allowing for + * smooth UI transitions without loading states. + */ + reset: () => Promise<{ error: ClerkError | null }>; } From df89c907a354548ab698f9036cbe338a9fae8954 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 15 Jan 2026 14:28:18 -0500 Subject: [PATCH 2/7] set to null to avoid api call --- .../clerk-js/src/core/resources/SignUp.ts | 19 ++-- .../core/resources/__tests__/SignUp.test.ts | 99 +++++-------------- packages/shared/src/types/signUpFuture.ts | 9 +- 3 files changed, 38 insertions(+), 89 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 48b2f6e14f8..068d5de0b23 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -62,6 +62,7 @@ import { clerkVerifyWeb3WalletCalledBeforeCreate, } from '../errors'; import { eventBus } from '../events'; +import { signUpResourceSignal, signUpErrorSignal } from '../signals'; import { BaseResource, SignUpVerifications } from './internal'; declare global { @@ -977,21 +978,19 @@ class SignUpFuture implements SignUpFutureResource { } /** - * Resets the current sign-up attempt by creating a new empty sign-up. + * 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. */ async reset(): Promise<{ error: ClerkError | null }> { - // Clear any previous errors - eventBus.emit('resource:error', { resource: this.#resource, error: null }); + // Clear errors + signUpErrorSignal({ error: null }); - try { - await this.#resource.__internal_basePost({ path: this.#resource.pathRoot, body: {} }); - return { error: null }; - } catch (err) { - eventBus.emit('resource:error', { resource: this.#resource, error: err }); - return { error: err }; - } + // Create a fresh null SignUp instance and update the signal directly + const freshSignUp = new SignUp(null); + signUpResourceSignal({ resource: freshSignUp }); + + return { error: null }; } } diff --git a/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts index a60fa84cf27..1f81f43c09d 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignUp.test.ts @@ -1,6 +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'; @@ -707,33 +708,14 @@ describe('SignUp', () => { afterEach(() => { vi.clearAllMocks(); vi.restoreAllMocks(); - }); - - it('creates a new signup by POSTing to /client/sign_ups with empty body', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - client: null, - response: { id: 'signup_new', status: 'missing_requirements' }, - }); - BaseResource._fetch = mockFetch; - - const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any); - await signUp.__internal_future.reset(); - - expect(mockFetch).toHaveBeenCalledWith( - expect.objectContaining({ - method: 'POST', - path: '/client/sign_ups', - body: {}, - }), - ); + // 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().mockResolvedValue({ - client: null, - response: { id: 'signup_new', status: 'missing_requirements' }, - }); + const mockFetch = vi.fn(); BaseResource._fetch = mockFetch; const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any); @@ -744,69 +726,30 @@ describe('SignUp', () => { 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 emitting resource:error with null', async () => { - const emitSpy = vi.spyOn(eventBus, 'emit'); - const mockFetch = vi.fn().mockResolvedValue({ - client: null, - response: { id: 'signup_new', status: 'missing_requirements' }, - }); - BaseResource._fetch = mockFetch; + 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 resource:error was called to clear previous errors - expect(emitSpy).toHaveBeenCalledWith('resource:error', { - resource: signUp, - error: null, - }); + // Verify that error signal was cleared + expect(signUpErrorSignal().error).toBeNull(); }); it('returns error: null on success', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - client: null, - response: { id: 'signup_new', status: 'missing_requirements' }, - }); - BaseResource._fetch = mockFetch; - 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('returns error and emits resource:error on failure', async () => { - const emitSpy = vi.spyOn(eventBus, 'emit'); - const mockError = new Error('API error'); - const mockFetch = vi.fn().mockRejectedValue(mockError); - BaseResource._fetch = mockFetch; - - const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements' } as any); - const result = await signUp.__internal_future.reset(); - - expect(result.error).toBe(mockError); - expect(emitSpy).toHaveBeenCalledWith('resource:error', { - resource: signUp, - error: mockError, - }); - }); - - it('resets an existing signup with data to a fresh state', async () => { - const mockFetch = vi.fn().mockResolvedValue({ - client: null, - response: { - id: 'signup_new', - status: 'missing_requirements', - email_address: null, - phone_number: null, - first_name: null, - last_name: null, - }, - }); - BaseResource._fetch = mockFetch; - + it('resets an existing signup with data to a fresh null state', async () => { const signUp = new SignUp({ id: 'signup_123', status: 'missing_requirements', @@ -815,15 +758,21 @@ describe('SignUp', () => { } 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(); - // After reset, the signup should have new values from the response - expect(signUp.id).toBe('signup_new'); - expect(signUp.emailAddress).toBeNull(); - expect(signUp.firstName).toBeNull(); + // 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(); }); }); }); diff --git a/packages/shared/src/types/signUpFuture.ts b/packages/shared/src/types/signUpFuture.ts index 68891edc7b8..5793c271852 100644 --- a/packages/shared/src/types/signUpFuture.ts +++ b/packages/shared/src/types/signUpFuture.ts @@ -465,11 +465,12 @@ export interface SignUpFutureResource { finalize: (params?: SignUpFutureFinalizeParams) => Promise<{ error: ClerkError | null }>; /** - * Resets the current sign-up attempt by creating a new empty sign-up. This is useful when you want to allow users - * to go back to the beginning of the sign-up flow (e.g., to change their email address during verification). + * Resets the current sign-up attempt by clearing all local state back to null. + * This is useful when you want to allow users to go back to the beginning of + * the sign-up flow (e.g., to change their email address during verification). * - * Unlike other methods, `reset()` does not trigger the `fetchStatus` to change to `'fetching'`, allowing for - * smooth UI transitions without loading states. + * Unlike other methods, `reset()` does not trigger the `fetchStatus` to change + * to `'fetching'` and does not make any API calls - it only clears local state. */ reset: () => Promise<{ error: ClerkError | null }>; } From 931c1e439273bba8fcc02e6b2862a916d00b23d1 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 15 Jan 2026 14:32:25 -0500 Subject: [PATCH 3/7] fix linting --- packages/clerk-js/src/core/resources/SignUp.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 068d5de0b23..ca8decd6eab 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -62,7 +62,7 @@ import { clerkVerifyWeb3WalletCalledBeforeCreate, } from '../errors'; import { eventBus } from '../events'; -import { signUpResourceSignal, signUpErrorSignal } from '../signals'; +import { signUpErrorSignal, signUpResourceSignal } from '../signals'; import { BaseResource, SignUpVerifications } from './internal'; declare global { @@ -982,7 +982,7 @@ class SignUpFuture implements SignUpFutureResource { * Unlike other methods, this does NOT emit resource:fetch with 'fetching' status, * allowing for smooth UI transitions without loading states. */ - async reset(): Promise<{ error: ClerkError | null }> { + reset(): Promise<{ error: ClerkError | null }> { // Clear errors signUpErrorSignal({ error: null }); @@ -990,7 +990,7 @@ class SignUpFuture implements SignUpFutureResource { const freshSignUp = new SignUp(null); signUpResourceSignal({ resource: freshSignUp }); - return { error: null }; + return Promise.resolve({ error: null }); } } From 921e6461443c8f2cafd4e7b99be986364f9685a5 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 15 Jan 2026 15:11:45 -0500 Subject: [PATCH 4/7] add changeset --- .changeset/cozy-webs-matter.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/cozy-webs-matter.md diff --git a/.changeset/cozy-webs-matter.md b/.changeset/cozy-webs-matter.md new file mode 100644 index 00000000000..f10d294ea54 --- /dev/null +++ b/.changeset/cozy-webs-matter.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +--- + +Add `reset` method to the new signUp resource. From 7f67200c4b43d761ec91c0e3c988bdfae6f3c810 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 15 Jan 2026 16:20:11 -0500 Subject: [PATCH 5/7] Update stateProxy.ts --- packages/react/src/stateProxy.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/react/src/stateProxy.ts b/packages/react/src/stateProxy.ts index 52d114f4dfd..5da09ed21d4 100644 --- a/packages/react/src/stateProxy.ts +++ b/packages/react/src/stateProxy.ts @@ -257,6 +257,9 @@ export class StateProxy implements State { get isTransferable() { return gateProperty(target, 'isTransferable', false); }, + get existingSession() { + return gateProperty(target, 'existingSession', undefined); + }, get hasBeenFinalized() { return gateProperty(target, 'hasBeenFinalized', false); }, @@ -268,6 +271,7 @@ export class StateProxy implements State { ticket: gateMethod(target, 'ticket'), web3: gateMethod(target, 'web3'), finalize: gateMethod(target, 'finalize'), + reset: gateMethod(target, 'reset'), verifications: wrapMethods(() => target().verifications, [ 'sendEmailCode', From 72250244c24fcb48dd93b36b59b49c690d2b5234 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Thu, 15 Jan 2026 16:22:22 -0500 Subject: [PATCH 6/7] Update stateProxy.ts --- packages/react/src/stateProxy.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/react/src/stateProxy.ts b/packages/react/src/stateProxy.ts index 5da09ed21d4..2eb8aa2db8b 100644 --- a/packages/react/src/stateProxy.ts +++ b/packages/react/src/stateProxy.ts @@ -257,9 +257,6 @@ export class StateProxy implements State { get isTransferable() { return gateProperty(target, 'isTransferable', false); }, - get existingSession() { - return gateProperty(target, 'existingSession', undefined); - }, get hasBeenFinalized() { return gateProperty(target, 'hasBeenFinalized', false); }, From 8fd50e2ea639d0a5fc41980a4967d81b075bd002 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Fri, 16 Jan 2026 11:21:16 -0500 Subject: [PATCH 7/7] feat: Add reset method to signInFuture resource --- .changeset/add-signin-reset.md | 6 ++ .../clerk-js/src/core/resources/SignIn.ts | 17 +++++ .../core/resources/__tests__/SignIn.test.ts | 70 +++++++++++++++++++ packages/react/src/stateProxy.ts | 1 + packages/shared/src/types/signInFuture.ts | 10 +++ 5 files changed, 104 insertions(+) create mode 100644 .changeset/add-signin-reset.md diff --git a/.changeset/add-signin-reset.md b/.changeset/add-signin-reset.md new file mode 100644 index 00000000000..ea79c972839 --- /dev/null +++ b/.changeset/add-signin-reset.md @@ -0,0 +1,6 @@ +--- +'@clerk/clerk-js': patch +'@clerk/shared': patch +--- + +Add `reset` method to the sign-in resource. diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 2ceec76dd3a..074a85fc212 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -92,6 +92,7 @@ import { clerkVerifyWeb3WalletCalledBeforeCreate, } from '../errors'; import { eventBus } from '../events'; +import { signInErrorSignal, signInResourceSignal } from '../signals'; import { BaseResource, UserData, Verification } from './internal'; export class SignIn extends BaseResource implements SignInResource { @@ -1247,6 +1248,22 @@ class SignInFuture implements SignInFutureResource { }); } + /** + * 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 }> { + // Clear errors + signInErrorSignal({ error: null }); + + // Create a fresh null SignIn instance and update the signal directly + const freshSignIn = new SignIn(null); + signInResourceSignal({ resource: freshSignIn }); + + return Promise.resolve({ error: null }); + } + private selectFirstFactor( params: Extract, ): EmailCodeFactor | null; diff --git a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts index db972315dcb..934fea4508b 100644 --- a/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts +++ b/packages/clerk-js/src/core/resources/__tests__/SignIn.test.ts @@ -1,5 +1,7 @@ import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'; +import { eventBus } from '../../events'; +import { signInErrorSignal, signInResourceSignal } from '../../signals'; import { BaseResource } from '../internal'; import { SignIn } from '../SignIn'; @@ -1890,5 +1892,73 @@ describe('SignIn', () => { await expect(signIn.__internal_future.finalize()).rejects.toThrow(); }); }); + + describe('reset', () => { + 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(); + }); + }); }); }); diff --git a/packages/react/src/stateProxy.ts b/packages/react/src/stateProxy.ts index 2eb8aa2db8b..deac6afcef1 100644 --- a/packages/react/src/stateProxy.ts +++ b/packages/react/src/stateProxy.ts @@ -158,6 +158,7 @@ export class StateProxy implements State { password: this.gateMethod(target, 'password'), sso: this.gateMethod(target, 'sso'), finalize: this.gateMethod(target, 'finalize'), + reset: this.gateMethod(target, 'reset'), emailCode: this.wrapMethods(() => target().emailCode, ['sendCode', 'verifyCode'] as const), emailLink: this.wrapStruct( diff --git a/packages/shared/src/types/signInFuture.ts b/packages/shared/src/types/signInFuture.ts index fa673bb4d66..f7376462c25 100644 --- a/packages/shared/src/types/signInFuture.ts +++ b/packages/shared/src/types/signInFuture.ts @@ -519,4 +519,14 @@ export interface SignInFutureResource { * session state (such as the `useUser()` hook) to update automatically. */ finalize: (params?: SignInFutureFinalizeParams) => Promise<{ error: ClerkError | null }>; + + /** + * Resets the current sign-in attempt by clearing all local state back to null. + * This is useful when you want to allow users to go back to the beginning of + * the sign-in flow (e.g., to change their identifier during verification). + * + * Unlike other methods, `reset()` does not trigger the `fetchStatus` to change + * to `'fetching'` and does not make any API calls - it only clears local state. + */ + reset: () => Promise<{ error: ClerkError | null }>; }