diff --git a/.changeset/shaky-books-occur.md b/.changeset/shaky-books-occur.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/shaky-books-occur.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 5f9e53b58c2..480c426ee52 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -612,6 +612,8 @@ class SignInFuture implements SignInFutureResource { verifyBackupCode: this.verifyBackupCode.bind(this), }; + #hasBeenFinalized = false; + constructor(readonly resource: SignIn) {} get id() { @@ -667,6 +669,10 @@ class SignInFuture implements SignInFutureResource { return this.resource.secondFactorVerification; } + get hasBeenFinalized() { + return this.#hasBeenFinalized; + } + async sendResetPasswordEmailCode(): Promise<{ error: unknown }> { return runAsyncResourceTask(this.resource, async () => { if (!this.resource.id) { @@ -1120,6 +1126,7 @@ class SignInFuture implements SignInFutureResource { // Reload the client to prevent an issue where the created session is not picked up. await SignIn.clerk.client?.reload(); + this.#hasBeenFinalized = true; await SignIn.clerk.setActive({ session: this.resource.createdSessionId, navigate }); }); } diff --git a/packages/clerk-js/src/core/resources/SignUp.ts b/packages/clerk-js/src/core/resources/SignUp.ts index 9b01c5d406b..141d768ee6f 100644 --- a/packages/clerk-js/src/core/resources/SignUp.ts +++ b/packages/clerk-js/src/core/resources/SignUp.ts @@ -576,6 +576,8 @@ class SignUpFuture implements SignUpFutureResource { verifyPhoneCode: this.verifyPhoneCode.bind(this), }; + #hasBeenFinalized = false; + constructor(readonly resource: SignUp) {} get id() { @@ -676,6 +678,10 @@ class SignUpFuture implements SignUpFutureResource { return undefined; } + get hasBeenFinalized() { + return this.#hasBeenFinalized; + } + private async getCaptchaToken(): Promise<{ captchaToken?: string; captchaWidgetType?: CaptchaWidgetType; @@ -900,6 +906,7 @@ class SignUpFuture implements SignUpFutureResource { throw new Error('Cannot finalize sign-up without a created session.'); } + this.#hasBeenFinalized = true; await SignUp.clerk.setActive({ session: this.resource.createdSessionId, navigate }); }); } diff --git a/packages/clerk-js/src/core/state.ts b/packages/clerk-js/src/core/state.ts index 82fed3485ac..2d37bb42008 100644 --- a/packages/clerk-js/src/core/state.ts +++ b/packages/clerk-js/src/core/state.ts @@ -48,10 +48,18 @@ export class State implements StateInterface { private onResourceUpdated = (payload: { resource: BaseResource }) => { if (payload.resource instanceof SignIn) { + const previousResource = this.signInResourceSignal().resource; + if (shouldIgnoreNullUpdate(previousResource, payload.resource)) { + return; + } this.signInResourceSignal({ resource: payload.resource }); } if (payload.resource instanceof SignUp) { + const previousResource = this.signUpResourceSignal().resource; + if (shouldIgnoreNullUpdate(previousResource, payload.resource)) { + return; + } this.signUpResourceSignal({ resource: payload.resource }); } }; @@ -66,3 +74,13 @@ export class State implements StateInterface { } }; } + +/** + * Returns true if the new resource is null and the previous resource has not been finalized. This is used to prevent + * nullifying the resource after it's been completed. + */ +function shouldIgnoreNullUpdate(previousResource: SignIn | null, newResource: SignIn | null): boolean; +function shouldIgnoreNullUpdate(previousResource: SignUp | null, newResource: SignUp | null): boolean; +function shouldIgnoreNullUpdate(previousResource: SignIn | SignUp | null, newResource: SignIn | SignUp | null) { + return !newResource?.id && previousResource && previousResource.__internal_future?.hasBeenFinalized === false; +} diff --git a/packages/react/src/stateProxy.ts b/packages/react/src/stateProxy.ts index dcb8c59cf0e..19708c1badd 100644 --- a/packages/react/src/stateProxy.ts +++ b/packages/react/src/stateProxy.ts @@ -105,6 +105,9 @@ export class StateProxy implements State { }, }); }, + get hasBeenFinalized() { + return gateProperty(target, 'hasBeenFinalized', false); + }, create: this.gateMethod(target, 'create'), password: this.gateMethod(target, 'password'), @@ -207,6 +210,9 @@ export class StateProxy implements State { get isTransferable() { return gateProperty(target, 'isTransferable', false); }, + get hasBeenFinalized() { + return gateProperty(target, 'hasBeenFinalized', false); + }, create: gateMethod(target, 'create'), update: gateMethod(target, 'update'), diff --git a/packages/shared/src/types/signInFuture.ts b/packages/shared/src/types/signInFuture.ts index 8a11ea7d7d7..6b6dd56b241 100644 --- a/packages/shared/src/types/signInFuture.ts +++ b/packages/shared/src/types/signInFuture.ts @@ -296,6 +296,13 @@ export interface SignInFutureResource { */ readonly userData: UserData; + /** + * Indicates that the sign-in has been finalized. + * + * @internal + */ + readonly hasBeenFinalized: boolean; + /** * Creates a new `SignIn` instance initialized with the provided parameters. The instance maintains the sign-in * lifecycle state through its `status` property, which updates as the authentication flow progresses. diff --git a/packages/shared/src/types/signUpFuture.ts b/packages/shared/src/types/signUpFuture.ts index 9559d0a4821..9e90cfe21d5 100644 --- a/packages/shared/src/types/signUpFuture.ts +++ b/packages/shared/src/types/signUpFuture.ts @@ -354,6 +354,13 @@ export interface SignUpFutureResource { */ readonly locale: string | null; + /** + * Indicates that the sign-up has been finalized. + * + * @internal + */ + readonly hasBeenFinalized: boolean; + /** * Creates a new `SignUp` instance initialized with the provided parameters. The instance maintains the sign-up * lifecycle state through its `status` property, which updates as the authentication flow progresses. Will also