Skip to content

poc: observable signin resource #6078

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

Open
wants to merge 40 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
6cbb936
poc: observable signin
jacekradko Jun 10, 2025
62db5aa
use zustand/vanilla store
jacekradko Jun 10, 2025
7d23568
use vanilla store
jacekradko Jun 10, 2025
c22693b
add react useResourceStore
jacekradko Jun 10, 2025
9e91e93
wip
jacekradko Jun 10, 2025
13d3539
separate state slices
jacekradko Jun 10, 2025
7b816ad
wip
jacekradko Jun 10, 2025
023f492
wip
jacekradko Jun 10, 2025
4ce2005
wip
jacekradko Jun 10, 2025
d0ae5ed
proxied useSignIn
jacekradko Jun 11, 2025
084c46b
Merge branch 'main' into poc/observable-signin
jacekradko Jun 11, 2025
68f998b
wip
jacekradko Jun 11, 2025
e49cd1b
wip
jacekradko Jun 11, 2025
64f9db8
wip
jacekradko Jun 11, 2025
8da5fd1
wip
jacekradko Jun 11, 2025
c25fee7
Merge branch 'main' into poc/observable-signin
jacekradko Jun 11, 2025
8c80879
wip
jacekradko Jun 11, 2025
c52b7fb
wip
jacekradko Jun 11, 2025
e0e152e
wip
jacekradko Jun 11, 2025
c3ec5ee
wip
jacekradko Jun 11, 2025
90c702a
wip
jacekradko Jun 11, 2025
c9aa1bf
wip
jacekradko Jun 11, 2025
87620c3
wip
jacekradko Jun 11, 2025
c25e560
wip
jacekradko Jun 11, 2025
8de72f0
return observable property
jacekradko Jun 11, 2025
714d264
update UseSignInReturn TSDoc
jacekradko Jun 11, 2025
7d5e6b3
use zustand useStore
jacekradko Jun 11, 2025
b2d84b5
wip
jacekradko Jun 11, 2025
0d8d45a
wip
jacekradko Jun 11, 2025
c113bd8
wip
jacekradko Jun 11, 2025
8820c2c
wip
jacekradko Jun 11, 2025
e6c2610
wip
jacekradko Jun 11, 2025
39a7097
wip
jacekradko Jun 12, 2025
abcafc2
wip
jacekradko Jun 12, 2025
cd7afb7
wip
jacekradko Jun 12, 2025
fd74064
wip
jacekradko Jun 12, 2025
c211b68
wip
jacekradko Jun 12, 2025
5768162
wip
jacekradko Jun 12, 2025
20a49e1
Merge branch 'main' into poc/observable-signin
jacekradko Jun 12, 2025
c90c461
improve docs
jacekradko Jun 12, 2025
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
8 changes: 4 additions & 4 deletions packages/clerk-js/bundlewatch.config.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{
"files": [
{ "path": "./dist/clerk.js", "maxSize": "605kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "69.2KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "113KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "53KB" },
{ "path": "./dist/clerk.js", "maxSize": "608kB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "73KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "114KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "55KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "106.3KB" },
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
{ "path": "./dist/coinbase*.js", "maxSize": "38KB" },
Expand Down
3 changes: 2 additions & 1 deletion packages/clerk-js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@
"dequal": "2.0.3",
"qrcode.react": "4.2.0",
"regenerator-runtime": "0.14.1",
"swr": "2.3.3"
"swr": "2.3.3",
"zustand": "5.0.5"
},
"devDependencies": {
"@emotion/jest": "^11.13.0",
Expand Down
860 changes: 859 additions & 1 deletion packages/clerk-js/sandbox/app.ts

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions packages/clerk-js/sandbox/template.html
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,13 @@
>Sign In</a
>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
href="/sign-in-observable"
>Sign In Observable</a
>
</li>
<li class="relative">
<a
class="relative isolate flex w-full rounded-md border border-white px-2 py-[0.4375rem] text-sm hover:bg-gray-50 aria-[current]:bg-gray-50"
Expand Down
66 changes: 54 additions & 12 deletions packages/clerk-js/src/core/resources/Base.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import { isValidBrowserOnline } from '@clerk/shared/browser';
import { isProductionFromPublishableKey } from '@clerk/shared/keys';
import type { ClerkAPIErrorJSON, ClerkResourceJSON, ClerkResourceReloadParams, DeletedObjectJSON } from '@clerk/types';
import type { StoreApi } from 'zustand';

import { clerkMissingFapiClientInResources } from '../errors';
import type { FapiClient, FapiRequestInit, FapiResponse, FapiResponseJSON, HTTPMethod } from '../fapiClient';
import { FraudProtection } from '../fraudProtection';
import type { Clerk } from './internal';
import { ClerkAPIResponseError, ClerkRuntimeError, Client } from './internal';
import { createResourceStore, type ResourceStore } from './state';

export type BaseFetchOptions = ClerkResourceReloadParams & {
action?: string;
search?: URLSearchParams;
headers?: HeadersInit;
forceUpdateClient?: boolean;
fetchMaxTries?: number;
};
Expand Down Expand Up @@ -49,6 +54,16 @@ export abstract class BaseResource {
id?: string;
pathRoot = '';

protected _store: StoreApi<ResourceStore<this>>;

constructor() {
this._store = createResourceStore<this>();
}

public get store() {
return this._store;
}

static get fapiClient(): FapiClient {
return BaseResource.clerk.getFapiClient();
}
Expand Down Expand Up @@ -187,22 +202,45 @@ export abstract class BaseResource {
}

protected async _baseGet<J extends ClerkResourceJSON | null>(opts: BaseFetchOptions = {}): Promise<this> {
const json = await BaseResource._fetch<J>(
{
method: 'GET',
path: this.path(),
rotatingTokenNonce: opts.rotatingTokenNonce,
},
opts,
);
this.store.getState().resource.dispatch({ type: 'FETCH_START' });

return this.fromJSON((json?.response || json) as J);
try {
const { forceUpdateClient, fetchMaxTries, ...fetchOpts } = opts;
const json = await BaseResource._fetch<J>({
method: 'GET',
path: this.path(opts.action),
...fetchOpts,
});

const data = this.fromJSON((json?.response || json) as J);
this.store.getState().resource.dispatch({ type: 'FETCH_SUCCESS', data });
return data;
} catch (error) {
this.store.getState().resource.dispatch({
type: 'FETCH_ERROR',
error: error as ClerkAPIErrorJSON,
});
throw error;
}
}

protected async _baseMutate<J extends ClerkResourceJSON | null>(params: BaseMutateParams): Promise<this> {
const { action, body, method, path } = params;
const json = await BaseResource._fetch<J>({ method, path: path || this.path(action), body });
return this.fromJSON((json?.response || json) as J);
this.store.getState().resource.dispatch({ type: 'FETCH_START' });

try {
const { action, body, method, path } = params;
const json = await BaseResource._fetch<J>({ method, path: path || this.path(action), body });

const data = this.fromJSON((json?.response || json) as J);
this.store.getState().resource.dispatch({ type: 'FETCH_SUCCESS', data });
return data;
} catch (error) {
this.store.getState().resource.dispatch({
type: 'FETCH_ERROR',
error: error as ClerkAPIErrorJSON,
});
throw error;
}
}

protected async _baseMutateBypass<J extends ClerkResourceJSON | null>(params: BaseMutateParams): Promise<this> {
Expand Down Expand Up @@ -235,4 +273,8 @@ export abstract class BaseResource {
const experimental = BaseResource.clerk?.__internal_getOption?.('experimental');
return experimental?.rethrowOfflineNetworkErrors || false;
}

public reset(): void {
this.store.getState().resource.dispatch({ type: 'RESET' });
}
}
184 changes: 137 additions & 47 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import type {
EmailLinkConfig,
EnterpriseSSOConfig,
PassKeyConfig,
PasskeyFactor,
PhoneCodeConfig,
PrepareFirstFactorParams,
PrepareSecondFactorParams,
Expand All @@ -39,6 +38,8 @@ import type {
Web3SignatureConfig,
Web3SignatureFactor,
} from '@clerk/types';
import { devtools } from 'zustand/middleware';
import { createStore } from 'zustand/vanilla';

import {
generateSignatureWithCoinbaseWallet,
Expand Down Expand Up @@ -67,31 +68,116 @@ import {
clerkVerifyWeb3WalletCalledBeforeCreate,
} from '../errors';
import { BaseResource, UserData, Verification } from './internal';
import { createResourceSlice, type ResourceStore } from './state';

type SignInSliceState = {
signin: {
status: SignInStatus | null;
setStatus: (status: SignInStatus | null) => void;
error: { global: string | null; fields: Record<string, string> };
setError: (error: { global: string | null; fields: Record<string, string> }) => void;
};
};

/**
* Creates a SignIn slice following the Zustand slices pattern.
* This slice handles SignIn-specific state management.
* All SignIn state is namespaced under the 'signin' key.
*/
const createSignInSlice = (set: any, _get: any): SignInSliceState => ({
signin: {
status: null,
setStatus: (status: SignInStatus | null) => {
set((state: any) => ({
...state,
signin: {
...state.signin,
status: status,
},
}));
},
error: { global: null, fields: {} },
setError: (error: { global: string | null; fields: Record<string, string> }) => {
set((state: any) => ({
...state,
signin: {
...state.signin,
error: error,
},
}));
},
},
});
Comment on lines +87 to +110
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Strong-type the slice instead of falling back to any

Both set and get can be fully typed via StoreApi<CombinedSignInStore>, giving us compile-time safety and IntelliSense for the callback bodies.

-const createSignInSlice = (set: any, _get: any): SignInSliceState => ({
+import type { StoreApi } from 'zustand/vanilla';
+
+const createSignInSlice = (
+  set: StoreApi<CombinedSignInStore>['setState'],
+  _get: StoreApi<CombinedSignInStore>['getState'],
+): SignInSliceState => ({

This eliminates the need for the repetitive state: any casts further down.

📝 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
const createSignInSlice = (set: any, _get: any): SignInSliceState => ({
signin: {
status: null,
setStatus: (status: SignInStatus | null) => {
set((state: any) => ({
...state,
signin: {
...state.signin,
status: status,
},
}));
},
error: { global: null, fields: {} },
setError: (error: { global: string | null; fields: Record<string, string> }) => {
set((state: any) => ({
...state,
signin: {
...state.signin,
error: error,
},
}));
},
},
});
import type { StoreApi } from 'zustand/vanilla';
const createSignInSlice = (
set: StoreApi<CombinedSignInStore>['setState'],
_get: StoreApi<CombinedSignInStore>['getState'],
): SignInSliceState => ({
signin: {
status: null,
setStatus: (status: SignInStatus | null) => {
set((state: any) => ({
...state,
signin: {
...state.signin,
status: status,
},
}));
},
error: { global: null, fields: {} },
setError: (error: { global: string | null; fields: Record<string, string> }) => {
set((state: any) => ({
...state,
signin: {
...state.signin,
error: error,
},
}));
},
},
});
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/resources/SignIn.ts around lines 87 to 110, the
parameters 'set' and '_get' are typed as 'any', which removes type safety and
IntelliSense support. To fix this, replace the 'any' types with the appropriate
typed version using 'StoreApi<CombinedSignInStore>' for both 'set' and '_get'.
This will provide compile-time safety and remove the need for casting 'state' as
'any' inside the callback functions.


type CombinedSignInStore = ResourceStore<SignIn> & SignInSliceState;

export class SignIn extends BaseResource implements SignInResource {
pathRoot = '/client/sign_ins';

createdSessionId: string | null = null;
firstFactorVerification: VerificationResource = new Verification(null);
id?: string;
status: SignInStatus | null = null;
supportedIdentifiers: SignInIdentifier[] = [];
identifier: string | null = null;
secondFactorVerification: VerificationResource = new Verification(null);
supportedFirstFactors: SignInFirstFactor[] | null = [];
supportedIdentifiers: SignInIdentifier[] = [];
supportedSecondFactors: SignInSecondFactor[] | null = null;
firstFactorVerification: VerificationResource = new Verification(null);
secondFactorVerification: VerificationResource = new Verification(null);
identifier: string | null = null;
createdSessionId: string | null = null;
userData: UserData = new UserData(null);

constructor(data: SignInJSON | SignInJSONSnapshot | null = null) {
super();
// Override the base _store with our combined store using slices pattern with namespacing
this._store = createStore<CombinedSignInStore>()(
devtools(
(set, get) => ({
...createResourceSlice<SignIn>(set, get),
...createSignInSlice(set, get),
}),
{ name: 'SignInStore' },
),
) as any;
Comment on lines +131 to +138
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Avoid shipping devtools middleware in production bundles

zustand/middleware devtools adds a sizable footprint and opens an extra connection to the Redux DevTools extension. Unless you tree-shake it out at build time (e.g., via process.env.NODE_ENV !== 'production' && devtools(...)), every consumer of @clerk/clerk-js will pay the cost.

-    this._store = createStore<CombinedSignInStore>()(
-      devtools(
-        (set, get) => ({
-          ...createResourceSlice<SignIn>(set, get),
-          ...createSignInSlice(set, get),
-        }),
-        { name: 'SignInStore' },
-      ),
-    ) as any;
+    const initializer = (set: any, get: any) => ({
+      ...createResourceSlice<SignIn>(set, get),
+      ...createSignInSlice(set, get),
+    });
+
+    this._store = createStore<CombinedSignInStore>()(
+      process.env.NODE_ENV === 'production' ? initializer : devtools(initializer, { name: 'SignInStore' }),
+    ) as any;
📝 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
devtools(
(set, get) => ({
...createResourceSlice<SignIn>(set, get),
...createSignInSlice(set, get),
}),
{ name: 'SignInStore' },
),
) as any;
// Define a shared initializer for the store slices
const initializer = (set: any, get: any) => ({
...createResourceSlice<SignIn>(set, get),
...createSignInSlice(set, get),
});
// Only apply devtools in non-production environments
this._store = createStore<CombinedSignInStore>()(
process.env.NODE_ENV === 'production'
? initializer
: devtools(initializer, { name: 'SignInStore' }),
) as any;
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/resources/SignIn.ts around lines 131 to 138, the
devtools middleware is included unconditionally, which increases bundle size and
opens a Redux DevTools connection in production. Modify the code to only apply
the devtools middleware when process.env.NODE_ENV is not 'production', ensuring
it is excluded from production builds to reduce footprint and avoid unnecessary
connections.

this.fromJSON(data);
}

create = (params: SignInCreateParams): Promise<this> => {
return this._basePost({
path: this.pathRoot,
body: params,
});
/**
* Reactive status property backed by the store.
* Reading and writing goes directly to/from the store.
*/
get status(): SignInStatus | null {
return (this._store.getState() as unknown as CombinedSignInStore).signin.status;
}

set status(newStatus: SignInStatus | null) {
(this._store.getState() as unknown as CombinedSignInStore).signin.setStatus(newStatus);
}

/**
* Reactive signInError property backed by the store.
* Reading and writing goes directly to/from the store.
*/
get signInError(): { global: string | null; fields: Record<string, string> } {
return (this._store.getState() as unknown as CombinedSignInStore).signin.error;
}

set signInError(newError: { global: string | null; fields: Record<string, string> }) {
(this._store.getState() as unknown as CombinedSignInStore).signin.setError(newError);
}

private updateError(globalError: string | null, fieldErrors: Record<string, string> = {}) {
this.signInError = { global: globalError, fields: fieldErrors };
}

create = async (params: SignInCreateParams): Promise<SignInResource> => {
try {
const result = await this._basePost({
path: this.pathRoot,
body: params,
});
return result;
} catch (error) {
this.updateError(error instanceof Error ? error.message : 'An unexpected error occurred');
throw error;
}
};

resetPassword = (params: ResetPasswordParams): Promise<SignInResource> => {
Expand Down Expand Up @@ -160,22 +246,31 @@ export class SignIn extends BaseResource implements SignInResource {
});
};

attemptFirstFactor = (attemptFactor: AttemptFirstFactorParams): Promise<SignInResource> => {
let config;
switch (attemptFactor.strategy) {
case 'passkey':
config = {
publicKeyCredential: JSON.stringify(serializePublicKeyCredentialAssertion(attemptFactor.publicKeyCredential)),
};
break;
default:
config = { ...attemptFactor };
}
attemptFirstFactor = async (attemptFactor: AttemptFirstFactorParams): Promise<SignInResource> => {
try {
let config;
switch (attemptFactor.strategy) {
case 'passkey':
config = {
publicKeyCredential: JSON.stringify(
serializePublicKeyCredentialAssertion(attemptFactor.publicKeyCredential),
),
};
break;
default:
config = { ...attemptFactor };
}

return this._basePost({
body: { ...config, strategy: attemptFactor.strategy },
action: 'attempt_first_factor',
});
const result = await this._basePost({
body: { ...config, strategy: attemptFactor.strategy },
action: 'attempt_first_factor',
});

return result;
} catch (error) {
this.updateError(error instanceof Error ? error.message : 'An unexpected error occurred');
throw error;
}
};

createEmailLinkFlow = (): CreateEmailLinkFlowReturn<SignInStartEmailLinkFlowParams, SignInResource> => {
Expand Down Expand Up @@ -311,7 +406,7 @@ export class SignIn extends BaseResource implements SignInResource {
//
// error code 4001 means the user rejected the request
// Reference: https://docs.cdp.coinbase.com/wallet-sdk/docs/errors
if (provider === 'coinbase_wallet' && err.code === 4001) {
if (provider === 'coinbase_wallet' && err instanceof Error && 'code' in err && err.code === 4001) {
signature = await generateSignature({ identifier, nonce: message, provider });
} else {
throw err;
Expand Down Expand Up @@ -386,19 +481,13 @@ export class SignIn extends BaseResource implements SignInResource {
}

if (flow === 'autofill' || flow === 'discoverable') {
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
await this.create({ strategy: 'passkey' });
} else {
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
const passKeyFactor = this.supportedFirstFactors.find(
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
f => f.strategy === 'passkey',
) as PasskeyFactor;
const passKeyFactor = this.supportedFirstFactors?.find(f => f.strategy === 'passkey');

if (!passKeyFactor) {
clerkVerifyPasskeyCalledBeforeCreate();
}
// @ts-ignore As this is experimental we want to support it at runtime, but not at the type level
await this.prepareFirstFactor(passKeyFactor);
}

Expand Down Expand Up @@ -445,18 +534,19 @@ export class SignIn extends BaseResource implements SignInResource {
};

protected fromJSON(data: SignInJSON | SignInJSONSnapshot | null): this {
if (data) {
this.id = data.id;
this.status = data.status;
this.supportedIdentifiers = data.supported_identifiers;
this.identifier = data.identifier;
this.supportedFirstFactors = deepSnakeToCamel(data.supported_first_factors) as SignInFirstFactor[] | null;
this.supportedSecondFactors = deepSnakeToCamel(data.supported_second_factors) as SignInSecondFactor[] | null;
this.firstFactorVerification = new Verification(data.first_factor_verification);
this.secondFactorVerification = new Verification(data.second_factor_verification);
this.createdSessionId = data.created_session_id;
this.userData = new UserData(data.user_data);
}
if (!data) return this;

this.createdSessionId = data.created_session_id;
this.firstFactorVerification = new Verification(data.first_factor_verification);
this.id = data.id;
this.identifier = data.identifier;
this.secondFactorVerification = new Verification(data.second_factor_verification);
this.status = data.status;
this.supportedFirstFactors = deepSnakeToCamel(data.supported_first_factors) as SignInFirstFactor[] | null;
this.supportedIdentifiers = data.supported_identifiers;
this.supportedSecondFactors = deepSnakeToCamel(data.supported_second_factors) as SignInSecondFactor[] | null;
this.userData = new UserData(data.user_data);

Comment on lines 536 to +549
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

deepSnakeToCamel on potentially null arrays risks a runtime error

data.supported_first_factors and supported_second_factors can be null per the type definition. deepSnakeToCamel(null) will throw because it expects an object/array. Guard first:

-    this.supportedFirstFactors = deepSnakeToCamel(data.supported_first_factors) as SignInFirstFactor[] | null;
+    this.supportedFirstFactors = data.supported_first_factors
+      ? (deepSnakeToCamel(data.supported_first_factors) as SignInFirstFactor[])
+      : null;

Apply the same pattern to supported_second_factors.

📝 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
protected fromJSON(data: SignInJSON | SignInJSONSnapshot | null): this {
if (data) {
this.id = data.id;
this.status = data.status;
this.supportedIdentifiers = data.supported_identifiers;
this.identifier = data.identifier;
this.supportedFirstFactors = deepSnakeToCamel(data.supported_first_factors) as SignInFirstFactor[] | null;
this.supportedSecondFactors = deepSnakeToCamel(data.supported_second_factors) as SignInSecondFactor[] | null;
this.firstFactorVerification = new Verification(data.first_factor_verification);
this.secondFactorVerification = new Verification(data.second_factor_verification);
this.createdSessionId = data.created_session_id;
this.userData = new UserData(data.user_data);
}
if (!data) return this;
this.createdSessionId = data.created_session_id;
this.firstFactorVerification = new Verification(data.first_factor_verification);
this.id = data.id;
this.identifier = data.identifier;
this.secondFactorVerification = new Verification(data.second_factor_verification);
this.status = data.status;
this.supportedFirstFactors = deepSnakeToCamel(data.supported_first_factors) as SignInFirstFactor[] | null;
this.supportedIdentifiers = data.supported_identifiers;
this.supportedSecondFactors = deepSnakeToCamel(data.supported_second_factors) as SignInSecondFactor[] | null;
this.userData = new UserData(data.user_data);
protected fromJSON(data: SignInJSON | SignInJSONSnapshot | null): this {
if (!data) return this;
this.createdSessionId = data.created_session_id;
this.firstFactorVerification = new Verification(data.first_factor_verification);
this.id = data.id;
this.identifier = data.identifier;
this.secondFactorVerification = new Verification(data.second_factor_verification);
this.status = data.status;
- this.supportedFirstFactors = deepSnakeToCamel(data.supported_first_factors) as SignInFirstFactor[] | null;
+ this.supportedFirstFactors = data.supported_first_factors
+ ? (deepSnakeToCamel(data.supported_first_factors) as SignInFirstFactor[])
+ : null;
this.supportedIdentifiers = data.supported_identifiers;
this.supportedSecondFactors = deepSnakeToCamel(data.supported_second_factors) as SignInSecondFactor[] | null;
this.userData = new UserData(data.user_data);
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/resources/SignIn.ts around lines 536 to 549, the
calls to deepSnakeToCamel on data.supported_first_factors and
data.supported_second_factors do not check for null, which can cause runtime
errors. Fix this by adding a null check before calling deepSnakeToCamel, only
transforming if the value is not null; otherwise, assign null directly. Apply
this null guard pattern to both supported_first_factors and
supported_second_factors properties.

return this;
}

Expand Down
Loading