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
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" },
3 changes: 2 additions & 1 deletion packages/clerk-js/package.json
Original file line number Diff line number Diff line change
@@ -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",
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
@@ -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"
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;
};
@@ -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();
}
@@ -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> {
@@ -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
@@ -17,7 +17,6 @@ import type {
EmailLinkConfig,
EnterpriseSSOConfig,
PassKeyConfig,
PasskeyFactor,
PhoneCodeConfig,
PrepareFirstFactorParams,
PrepareSecondFactorParams,
@@ -39,6 +38,8 @@ import type {
Web3SignatureConfig,
Web3SignatureFactor,
} from '@clerk/types';
import { devtools } from 'zustand/middleware';
import { createStore } from 'zustand/vanilla';

import {
generateSignatureWithCoinbaseWallet,
@@ -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
Contributor

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
Contributor

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> => {
@@ -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> => {
@@ -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;
@@ -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);
}

@@ -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
Contributor

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;
}

156 changes: 112 additions & 44 deletions packages/clerk-js/src/core/resources/Verification.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { errorToJSON, parseError } from '@clerk/shared/error';
import type {
ClerkAPIError,
ClerkAPIErrorJSON,
PasskeyVerificationResource,
PhoneCodeChannel,
PublicKeyCredentialCreationOptionsJSON,
@@ -16,51 +16,129 @@ import type {
VerificationResource,
VerificationStatus,
} from '@clerk/types';
import { devtools } from 'zustand/middleware';
import { createStore } from 'zustand/vanilla';

import { unixEpochToDate } from '../../utils/date';
import { convertJSONToPublicKeyCreateOptions } from '../../utils/passkeys';
import { BaseResource } from './internal';
import { createResourceSlice, type ResourceStore } from './state';

/**
* Verification slice state type
*/
type VerificationSliceState = {
verification: {
error: ClerkAPIErrorJSON | null;
setError: (error: ClerkAPIErrorJSON | null) => void;
clearError: () => void;
hasError: () => boolean;
};
};

/**
* Creates a verification slice following the Zustand slices pattern.
* This slice handles verification-specific state management.
* All verification state is namespaced under the 'verification' key.
*/
const createVerificationSlice = (set: any, get: any): VerificationSliceState => ({
verification: {
error: null,
setError: (error: ClerkAPIErrorJSON | null) => {
set((state: any) => ({
...state,
verification: {
...state.verification,
error: error,
},
}));
},
clearError: () => {
set((state: any) => ({
...state,
verification: {
...state.verification,
error: null,
},
}));
},
hasError: () => {
const state = get();
return state.verification.error !== null;
},
},
});

type CombinedVerificationStore = ResourceStore<Verification> & VerificationSliceState;

export class Verification extends BaseResource implements VerificationResource {
pathRoot = '';

status: VerificationStatus | null = null;
strategy: string | null = null;
nonce: string | null = null;
message: string | null = null;
externalVerificationRedirectURL: URL | null = null;
attempts: number | null = null;
channel?: PhoneCodeChannel;
expireAt: Date | null = null;
error: ClerkAPIError | null = null;
externalVerificationRedirectURL: URL | null = null;
message: string | null = null;
nonce: string | null = null;
status: VerificationStatus | null = null;
strategy: string | null = null;
verifiedAtClient: string | null = null;
channel?: PhoneCodeChannel;

constructor(data: VerificationJSON | VerificationJSONSnapshot | null) {
super();
// Override the base _store with our combined store using slices pattern with namespacing
this._store = createStore<CombinedVerificationStore>()(
devtools(
(set, get) => ({
...createResourceSlice<Verification>(set, get),
...createVerificationSlice(set, get),
}),
{ name: 'VerificationStore' },
),
) as any;
this.fromJSON(data);
}

/**
* Reactive error property backed by the store.
* Reading goes directly from the store.
*/
get error(): ClerkAPIErrorJSON | null {
return (this._store.getState() as CombinedVerificationStore).verification.error;
}

verifiedFromTheSameClient = (): boolean => {
return this.verifiedAtClient === BaseResource.clerk?.client?.id;
};

protected fromJSON(data: VerificationJSON | VerificationJSONSnapshot | null): this {
if (data) {
this.status = data.status;
this.verifiedAtClient = data.verified_at_client;
this.strategy = data.strategy;
this.nonce = data.nonce || null;
this.message = data.message || null;
if (data.external_verification_redirect_url) {
this.externalVerificationRedirectURL = new URL(data.external_verification_redirect_url);
} else {
this.externalVerificationRedirectURL = null;
}
this.attempts = data.attempts;
this.expireAt = unixEpochToDate(data.expire_at || undefined);
this.error = data.error ? parseError(data.error) : null;
this.channel = data.channel || undefined;
if (!data) {
return this;
}

this.status = data.status;
this.verifiedAtClient = data.verified_at_client;
this.strategy = data.strategy;
this.nonce = data.nonce || null;
this.message = data.message || null;
if (data.external_verification_redirect_url) {
this.externalVerificationRedirectURL = new URL(data.external_verification_redirect_url);
} else {
this.externalVerificationRedirectURL = null;
}
this.attempts = data.attempts;
this.expireAt = unixEpochToDate(data.expire_at || undefined);

// Set error state directly in the verification slice
if (data.error) {
const parsedError = errorToJSON(parseError(data.error));
(this._store.getState() as CombinedVerificationStore).verification.setError(parsedError);
} else {
(this._store.getState() as CombinedVerificationStore).verification.clearError();
}

this.channel = data.channel || undefined;

return this;
}

@@ -75,7 +153,7 @@ export class Verification extends BaseResource implements VerificationResource {
external_verification_redirect_url: this.externalVerificationRedirectURL?.toString() || null,
attempts: this.attempts,
expire_at: this.expireAt?.getTime() || null,
error: errorToJSON(this.error),
error: this.error || { code: '', message: '' },
verified_at_client: this.verifiedAtClient,
};
}
@@ -86,19 +164,21 @@ export class PasskeyVerification extends Verification implements PasskeyVerifica

constructor(data: VerificationJSON | VerificationJSONSnapshot | null) {
super(data);
this.fromJSON(data);
}

/**
* Transform base64url encoded strings to ArrayBuffer
*/
protected fromJSON(data: VerificationJSON | VerificationJSONSnapshot | null): this {
super.fromJSON(data);
if (data?.nonce) {
if (!data?.nonce) {
this.publicKey = null;
} else {
this.publicKey = convertJSONToPublicKeyCreateOptions(
JSON.parse(data.nonce) as PublicKeyCredentialCreationOptionsJSON,
);
}

return this;
}
}
@@ -110,17 +190,10 @@ export class SignUpVerifications implements SignUpVerificationsResource {
externalAccount: VerificationResource;

constructor(data: SignUpVerificationsJSON | SignUpVerificationsJSONSnapshot | null) {
if (data) {
this.emailAddress = new SignUpVerification(data.email_address);
this.phoneNumber = new SignUpVerification(data.phone_number);
this.web3Wallet = new SignUpVerification(data.web3_wallet);
this.externalAccount = new Verification(data.external_account);
} else {
this.emailAddress = new SignUpVerification(null);
this.phoneNumber = new SignUpVerification(null);
this.web3Wallet = new SignUpVerification(null);
this.externalAccount = new Verification(null);
}
this.emailAddress = new SignUpVerification(data?.email_address ?? null);
this.phoneNumber = new SignUpVerification(data?.phone_number ?? null);
this.web3Wallet = new SignUpVerification(data?.web3_wallet ?? null);
this.externalAccount = new Verification(data?.external_account ?? null);
}

public __internal_toSnapshot(): SignUpVerificationsJSONSnapshot {
@@ -139,13 +212,8 @@ export class SignUpVerification extends Verification {

constructor(data: SignUpVerificationJSON | SignUpVerificationJSONSnapshot | null) {
super(data);
if (data) {
this.nextAction = data.next_action;
this.supportedStrategies = data.supported_strategies;
} else {
this.nextAction = '';
this.supportedStrategies = [];
}
this.nextAction = data?.next_action ?? '';
this.supportedStrategies = data?.supported_strategies ?? [];
}

public __internal_toSnapshot(): SignUpVerificationJSONSnapshot {
133 changes: 133 additions & 0 deletions packages/clerk-js/src/core/resources/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type { ClerkAPIErrorJSON } from '@clerk/types';
import { devtools } from 'zustand/middleware';
import { createStore } from 'zustand/vanilla';

/**
* Represents the possible states of a resource.
*/
export type ResourceState<T> =
| { status: 'idle'; data: null; error: null }
| { status: 'loading'; data: null; error: null }
| { status: 'error'; data: null; error: ClerkAPIErrorJSON | null }
| { status: 'success'; data: T; error: null };

/**
* Resource actions for the Zustand store
*/
export type ResourceAction<T> =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; data: T }
| { type: 'FETCH_ERROR'; error: ClerkAPIErrorJSON }
| { type: 'RESET' };

/**
* Resource store shape using Zustand slices pattern
*/
export type ResourceStore<T> = {
resource: {
status: 'idle' | 'loading' | 'error' | 'success';
data: T | null;
error: ClerkAPIErrorJSON | null;
dispatch: (action: ResourceAction<T>) => void;
getData: () => T | null;
getError: () => ClerkAPIErrorJSON | null;
hasError: () => boolean;
getStatus: () => 'idle' | 'loading' | 'error' | 'success';
};
};

/**
* Creates a resource slice following the Zustand slices pattern.
* This slice handles generic resource state management (loading, success, error states).
* All resource state is namespaced under the 'resource' key and flattened (no nested 'state' object).
*/
const createResourceSlice = <T>(set: any, get: any): ResourceStore<T> => {
const dispatch = (action: ResourceAction<T>) => {
set((state: any) => {
let newResourceState;

switch (action.type) {
case 'FETCH_START':
newResourceState = {
status: 'loading' as const,
data: state.resource.data, // Keep existing data during loading
error: null,
};
break;
case 'FETCH_SUCCESS':
newResourceState = {
status: 'success' as const,
data: action.data,
error: null,
};
break;
case 'FETCH_ERROR':
newResourceState = {
status: 'error' as const,
data: state.resource.data, // Keep existing data on error
error: action.error,
};
break;
case 'RESET':
newResourceState = {
status: 'idle' as const,
data: null,
error: null,
};
break;
default:
return state;
}

return {
...state,
resource: {
...state.resource,
...newResourceState,
},
};
});
};

return {
resource: {
status: 'idle' as const,
data: null,
error: null,
dispatch,
getData: () => {
const state = get();
return state.resource.data;
},
getError: () => {
const state = get();
return state.resource.error;
},
hasError: () => {
const state = get();
return state.resource.status === 'error';
},
getStatus: () => {
const state = get();
return state.resource.status;
},
},
};
};

/**
* Creates a basic resource store using just the resource slice.
* This is used by BaseResource for backward compatibility.
*/
export const createResourceStore = <T>(name = 'ResourceStore') => {
return createStore<ResourceStore<T>>()(
devtools(
(set, get) => ({
...createResourceSlice<T>(set, get),
}),
{ name },
),
);
};

export { createResourceSlice };
52 changes: 39 additions & 13 deletions packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx
Original file line number Diff line number Diff line change
@@ -30,7 +30,6 @@ import {
import { useCoreSignIn, useEnvironment, useSignInContext } from '../../contexts';
import { Col, descriptors, Flow, localizationKeys } from '../../customizables';
import { CaptchaElement } from '../../elements/CaptchaElement';
import { useLoadingStatus } from '../../hooks';
import { useSupportEmail } from '../../hooks/useSupportEmail';
import { useRouter } from '../../router';
import type { FormControlState } from '../../utils';
@@ -77,7 +76,6 @@ const useAutoFillPasskey = () => {
function SignInStartInternal(): JSX.Element {
const card = useCardState();
const clerk = useClerk();
const status = useLoadingStatus();
const { displayConfig, userSettings, authConfig } = useEnvironment();
const signIn = useCoreSignIn();
const { navigate } = useRouter();
@@ -196,8 +194,24 @@ function SignInStartInternal(): JSX.Element {
}
}, [identifierField.value, identifierAttributes]);

const signInStatus = signIn.status;
const signInFetchStatus = signIn.store.getState().status;

useEffect(() => {
console.log('Component mounted');
console.log('Initial organizationTicket:', organizationTicket);
console.log('Initial signInFetchStatus:', signInFetchStatus);
console.log('Initial signInStatus:', signInStatus);
}, []);

useEffect(() => {
if (!organizationTicket) {
console.log('useEffect triggered');
console.log('organizationTicket:', organizationTicket);
console.log('signInFetchStatus:', signInFetchStatus);
console.log('signInStatus:', signInStatus);

if (!organizationTicket || signInFetchStatus === 'fetching' || signInStatus === 'complete') {
console.log('Early return from useEffect');
return;
}

@@ -206,65 +220,70 @@ function SignInStartInternal(): JSX.Element {
if (organizationTicket) {
paramsToForward.set('__clerk_ticket', organizationTicket);
}
// We explicitly navigate to 'create' in the combined flow to trigger a client-side navigation. Navigating to
// signUpUrl triggers a full page reload when used with the hash router.
console.log('Navigating to signUpUrl with params:', paramsToForward.toString());
void navigate(isCombinedFlow ? `create` : signUpUrl, { searchParams: paramsToForward });
return;
}

status.setLoading();
console.log('Setting card to loading state');
card.setLoading();
signIn
.create({
strategy: 'ticket',
ticket: organizationTicket,
})
.then(res => {
console.log('API response:', res);
switch (res.status) {
case 'needs_first_factor':
console.log('Status: needs_first_factor');
if (hasOnlyEnterpriseSSOFirstFactors(res)) {
console.log('Authenticating with Enterprise SSO');
return authenticateWithEnterpriseSSO();
}

return navigate('factor-one');
case 'needs_second_factor':
console.log('Status: needs_second_factor');
return navigate('factor-two');
case 'complete':
console.log('Status: complete');
removeClerkQueryParam('__clerk_ticket');
return clerk.setActive({
session: res.createdSessionId,
redirectUrl: afterSignInUrl,
});
default: {
console.error('Invalid API response status:', res.status);
console.error(clerkInvalidFAPIResponse(res.status, supportEmail));
return;
}
}
})
.catch(err => {
console.error('Error during signIn.create:', err);
return attemptToRecoverFromSignInError(err);
})
.finally(() => {
// Keep the card in loading state during SSO redirect to prevent UI flicker
// This is necessary because there's a brief delay between initiating the SSO flow
// and the actual redirect to the external Identity Provider
const isRedirectingToSSOProvider = hasOnlyEnterpriseSSOFirstFactors(signIn);
if (isRedirectingToSSOProvider) return;

status.setIdle();
console.log('Setting card to idle state');
card.setIdle();
});
}, []);
}, [organizationTicket, signInFetchStatus, signInStatus]);

useEffect(() => {
console.log('OAuth error handling useEffect triggered');
async function handleOauthError() {
const defaultErrorHandler = () => {
// Error from server may be too much information for the end user, so set a generic error
console.error('Default error handler triggered');
card.setError('Unable to complete action at this time. If the problem persists please contact support.');
};

const error = signIn?.firstFactorVerification?.error;
if (error) {
console.log('OAuth error detected:', error);
switch (error.code) {
case ERROR_CODES.NOT_ALLOWED_TO_SIGN_UP:
case ERROR_CODES.OAUTH_ACCESS_DENIED:
@@ -292,6 +311,7 @@ function SignInStartInternal(): JSX.Element {

// TODO: This is a workaround in order to reset the sign in attempt
// so that the oauth error does not persist on full page reloads.
console.log('Resetting sign-in attempt');
void (await signIn.create({}));
}
}
@@ -368,6 +388,8 @@ function SignInStartInternal(): JSX.Element {

switch (res.status) {
case 'needs_identifier':
console.log('needs_identifier');
console.log('res.supportedFirstFactors:', res.supportedFirstFactors);
Comment on lines +391 to +392
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

Remove debugging logs from signInWithFields function.

         case 'needs_identifier':
-          console.log('needs_identifier');
-          console.log('res.supportedFirstFactors:', res.supportedFirstFactors);
           // Check if we need to initiate an enterprise sso flow
📝 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
console.log('needs_identifier');
console.log('res.supportedFirstFactors:', res.supportedFirstFactors);
case 'needs_identifier':
// Check if we need to initiate an enterprise sso flow
🤖 Prompt for AI Agents
In packages/clerk-js/src/ui/components/SignIn/SignInStart.tsx at lines 391 to
392, remove the console.log statements used for debugging inside the
signInWithFields function to clean up the code and avoid unnecessary logging in
production.

// Check if we need to initiate an enterprise sso flow
if (res.supportedFirstFactors?.some(ff => ff.strategy === 'saml' || ff.strategy === 'enterprise_sso')) {
await authenticateWithEnterpriseSSO();
@@ -495,12 +517,16 @@ function SignInStartInternal(): JSX.Element {
return components[identifierField.type as keyof typeof components];
}, [identifierField.type]);

if (status.isLoading || clerkStatus === 'sign_up') {
if (clerkStatus === 'sign_up') {
// clerkStatus being sign_up will trigger a navigation to the sign up flow, so show a loading card instead of
// rendering the sign in flow.
return <LoadingCard />;
}

if (signInStatus === 'complete') {
return <div>Sign-in complete!</div>;
}

// @ts-expect-error `action` is not typed
const { action, ...identifierFieldProps } = identifierField.props;
return (
3 changes: 2 additions & 1 deletion packages/react/package.json
Original file line number Diff line number Diff line change
@@ -78,7 +78,8 @@
"dependencies": {
"@clerk/shared": "workspace:^",
"@clerk/types": "workspace:^",
"tslib": "catalog:repo"
"tslib": "catalog:repo",
"zustand": "5.0.5"
},
"devDependencies": {
"@clerk/localizations": "workspace:*",
1 change: 1 addition & 0 deletions packages/react/src/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@ export { useAuth } from './useAuth';
export { useEmailLink } from './useEmailLink';
export { useSignIn } from './useSignIn';
export { useSignUp } from './useSignUp';
export { createResourceStoreHooks } from './useResourceStore';
export {
useClerk,
useOrganization,
62 changes: 62 additions & 0 deletions packages/react/src/hooks/useResourceStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { StoreApi } from 'zustand';
import { useStore } from 'zustand';

interface ResourceStore<T> {
state: any;
dispatch: (action: any) => void;
getData: () => T | null;
getError: () => any;
status: () => 'idle' | 'loading' | 'error' | 'success';
hasError: () => boolean;
}
Comment on lines +4 to +11
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

Interface doesn't match the actual store structure

Based on the relevant code snippets, the actual ResourceStore<T> type has a nested structure with properties under a resource namespace. The current interface appears to be flattened.

Update the interface to match the actual structure:

 interface ResourceStore<T> {
-  state: any;
-  dispatch: (action: any) => void;
-  getData: () => T | null;
-  getError: () => any;
-  status: () => 'idle' | 'loading' | 'error' | 'success';
-  hasError: () => boolean;
+  resource: {
+    status: 'idle' | 'loading' | 'error' | 'success';
+    data: T | null;
+    error: any;
+    dispatch: (action: any) => void;
+    getData: () => T | null;
+    getError: () => any;
+    hasError: () => boolean;
+    getStatus: () => 'idle' | 'loading' | 'error' | 'success';
+  };
 }

Additionally, update the hooks to use the correct paths:

-  const useResourceState = () => useStore(store, state => state.state);
+  const useResourceState = () => useStore(store, state => state.resource);
📝 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
interface ResourceStore<T> {
state: any;
dispatch: (action: any) => void;
getData: () => T | null;
getError: () => any;
status: () => 'idle' | 'loading' | 'error' | 'success';
hasError: () => boolean;
}
interface ResourceStore<T> {
resource: {
status: 'idle' | 'loading' | 'error' | 'success';
data: T | null;
error: any;
dispatch: (action: any) => void;
getData: () => T | null;
getError: () => any;
hasError: () => boolean;
getStatus: () => 'idle' | 'loading' | 'error' | 'success';
};
}
🤖 Prompt for AI Agents
In packages/react/src/hooks/useResourceStore.ts between lines 4 and 11, the
ResourceStore<T> interface is incorrectly flattened and does not reflect the
actual nested structure under a resource property. Update the interface to nest
state, dispatch, getData, getError, status, and hasError inside a resource
object to match the real store shape. Then, revise any hook usages to access
these properties through the resource namespace accordingly.


/**
* React hooks for using the resource store in React components.
* This provides React-specific integration for the framework-agnostic resource store.
*/
export const createResourceStoreHooks = <T>(store: StoreApi<ResourceStore<T>>) => {
/**
* Hook to get the entire store state
*/
const useResourceStore = () => useStore(store);

/**
* Hook to get the current resource state
*/
const useResourceState = () => useStore(store, state => state.state);

/**
* Hook to get the dispatch function
*/
const useResourceDispatch = () => useStore(store, state => state.dispatch);

/**
* Hook to get the current data
*/
const useResourceData = () => useStore(store, state => state.state.data);

/**
* Hook to get the current error
*/
const useResourceError = () => useStore(store, state => state.state.error);

/**
* Hook to get the current status
*/
const useResourceStatus = () => useStore(store, state => state.state.status);

/**
* Hook to check if the resource has an error
*/
const useResourceHasError = () => useStore(store, state => !!state.state.error);

return {
useResourceStore,
useResourceState,
useResourceDispatch,
useResourceData,
useResourceError,
useResourceStatus,
useResourceHasError,
};
};
233 changes: 177 additions & 56 deletions packages/react/src/hooks/useSignIn.ts
Original file line number Diff line number Diff line change
@@ -1,69 +1,190 @@
import { useClientContext } from '@clerk/shared/react';
import { ClerkInstanceContext, useClientContext } from '@clerk/shared/react';
import { eventMethodCalled } from '@clerk/shared/telemetry';
import type { UseSignInReturn } from '@clerk/types';
import type { SetActive, SignInResource } from '@clerk/types';
import { useCallback,useContext, useMemo } from 'react';
import { useStore } from 'zustand';

import { useIsomorphicClerkContext } from '../contexts/IsomorphicClerkContext';
import { useAssertWrappedByClerkProvider } from './useAssertWrappedByClerkProvider';

type QueuedCall = {
target: 'signIn' | 'setActive';
method: string;
args: any[];
resolve: (value: any) => void;
reject: (error: any) => void;
};

/**
* The `useSignIn()` hook provides access to the [`SignIn`](https://clerk.com/docs/references/javascript/sign-in) object, which allows you to check the current state of a sign-in attempt and manage the sign-in flow. You can use this to create a [custom sign-in flow](https://clerk.com/docs/custom-flows/overview#sign-in-flow).
*
* @unionReturnHeadings
* ["Initialization", "Loaded"]
*
* @example
* ### Check the current state of a sign-in
*
* The following example uses the `useSignIn()` hook to access the [`SignIn`](https://clerk.com/docs/references/javascript/sign-in) object, which contains the current sign-in attempt status and methods to create a new sign-in attempt. The `isLoaded` property is used to handle the loading state.
*
* <Tabs items='React,Next.js'>
* <Tab>
*
* ```tsx {{ filename: 'src/pages/SignInPage.tsx' }}
* import { useSignIn } from '@clerk/clerk-react'
*
* export default function SignInPage() {
* const { isLoaded, signIn } = useSignIn()
*
* if (!isLoaded) {
* // Handle loading state
* return null
* }
*
* return <div>The current sign-in attempt status is {signIn?.status}.</div>
* }
* ```
*
* </Tab>
* <Tab>
*
* {@include ../../docs/use-sign-in.md#nextjs-01}
*
* </Tab>
* </Tabs>
*
* @example
* ### Create a custom sign-in flow with `useSignIn()`
*
* The `useSignIn()` hook can also be used to build fully custom sign-in flows, if Clerk's prebuilt components don't meet your specific needs or if you require more control over the authentication flow. Different sign-in flows include email and password, email and phone codes, email links, and multifactor (MFA). To learn more about using the `useSignIn()` hook to create custom flows, see the [custom flow guides](https://clerk.com/docs/custom-flows/overview).
*
* ```empty```
* Enhanced SignInResource with observable capabilities for React.
*/
export const useSignIn = (): UseSignInReturn => {
useAssertWrappedByClerkProvider('useSignIn');
type ObservableSignInResource = SignInResource & {
/**
* The observable store state. This is not a function but the actual state.
* Components using this should access it directly from the useSignIn hook result.
*/
observableState?: any;
};

/**
* Return type for the useSignIn hook
*/
type UseSignInReturn = {
isLoaded: boolean;
signIn: ObservableSignInResource;
setActive: SetActive;
/**
* The observable store state. Use this to access the SignIn store state.
* This value will trigger re-renders when the store state changes.
*/
signInStore: any;
};

/**
* A stable fallback store that maintains consistent behavior when no real store exists.
*/
const FALLBACK_STATE = {};
const FALLBACK_STORE = {
getState: () => FALLBACK_STATE,
getInitialState: () => FALLBACK_STATE,
subscribe: () => () => {}, // Return unsubscribe function
setState: () => {},
Comment on lines +48 to +51
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

subscribe signature incompatible with Zustand → runtime/type errors

useStore expects the store’s subscribe method to accept a listener parameter ((listener, selector?, equalityFn?)), but the fallback stub is declared with no parameters.
Type-checking will fail and, at runtime, Zustand will still call the function with arguments that will be silently ignored, making the fallback store impossible to observe.

-  subscribe: () => () => {}, // Return unsubscribe function
+  subscribe: (_listener: unknown) => () => {}, // accept listener to satisfy Zustand
📝 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
getState: () => FALLBACK_STATE,
getInitialState: () => FALLBACK_STATE,
subscribe: () => () => {}, // Return unsubscribe function
setState: () => {},
getState: () => FALLBACK_STATE,
getInitialState: () => FALLBACK_STATE,
subscribe: (_listener: unknown) => () => {}, // accept listener to satisfy Zustand
setState: () => {},
🤖 Prompt for AI Agents
In packages/react/src/hooks/useSignIn.ts around lines 48 to 51, the fallback
store's subscribe method is declared without parameters, which is incompatible
with Zustand's expected signature that requires a listener and optional selector
and equality function parameters. Update the subscribe method to accept these
parameters and return an unsubscribe function, ensuring it matches Zustand's
signature to avoid type errors and enable proper observation of the fallback
store.

destroy: () => {}
};

export const useSignIn = (): UseSignInReturn => {
// Check if ClerkProvider context is available first
const clerkInstanceContext = useContext(ClerkInstanceContext);

const isomorphicClerk = useIsomorphicClerkContext();
const client = useClientContext();

isomorphicClerk.telemetry?.record(eventMethodCalled('useSignIn'));

if (!client) {
return { isLoaded: false, signIn: undefined, setActive: undefined };
// Only assert ClerkProvider if we have some context - this allows proxy fallback
if (clerkInstanceContext) {
useAssertWrappedByClerkProvider('useSignIn');
}

return {
isLoaded: true,
signIn: client.signIn,
setActive: isomorphicClerk.setActive,
};
};
isomorphicClerk?.telemetry?.record(eventMethodCalled('useSignIn'));

// Get the store reference - this must be done at the top level
const store = useMemo(() => {
if (!client?.signIn) return FALLBACK_STORE;

// Try both 'store' and '_store' properties, but default to fallback
return (client.signIn as any).store || (client.signIn as any)._store || FALLBACK_STORE;
}, [client?.signIn]);

// Always call useStore at the top level with a consistent store reference
const storeState = useStore(store);

// Determine if we have a real store
const hasRealStore = store !== FALLBACK_STORE;

// Compute the final store state
const signInStore = useMemo(() => {
if (!hasRealStore) {
return {};
}
return storeState;
}, [hasRealStore, storeState]);

const callQueue: QueuedCall[] = [];

Comment on lines +91 to +92
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

Queue is recreated on every render → pending calls may be lost

callQueue lives in the render scope, so a re-render (e.g. when client becomes available) allocates a fresh array.
Promises enqueued by proxies created in earlier renders will never be flushed, leaving callers hanging and causing silent logic breaks.

Fix: hoist the queue into a stable ref (or module-level) and process it from a useEffect so it survives renders:

-import { useClientContext } from '@clerk/shared/react';
+import { useClientContext } from '@clerk/shared/react';
+import { useRef, useEffect } from 'react';
...
-  const callQueue: QueuedCall[] = [];
+  const callQueueRef = useRef<QueuedCall[]>([]);
...
-              callQueue.push({
+              callQueueRef.current.push({
                 target,
...
-    while (callQueue.length > 0) {
-      const queuedCall = callQueue.shift();
+    while (callQueueRef.current.length > 0) {
+      const queuedCall = callQueueRef.current.shift();
...
-  if (client) {
-    processQueue(client.signIn, isomorphicClerk.setActive);
+  useEffect(() => {
+    if (client) {
+      processQueue(client.signIn, isomorphicClerk.setActive);
+    }
+  }, [client]);

Without this change, any call issued before client resolves is effectively dropped.

Also applies to: 51-65

🤖 Prompt for AI Agents
In packages/react/src/hooks/useSignIn.ts around lines 24 to 25 and 51 to 65, the
callQueue array is declared inside the render scope, causing it to be recreated
on every render and losing any pending calls. To fix this, move callQueue into a
stable React ref using useRef so it persists across renders, and handle
processing the queue inside a useEffect hook to ensure all queued calls are
properly flushed once the client becomes available.

const processQueue = useCallback((signIn: SignInResource, setActive: SetActive) => {
while (callQueue.length > 0) {
const queuedCall = callQueue.shift();
if (!queuedCall) continue;

const { target, method, args, resolve, reject } = queuedCall;
try {
const targetObj = target === 'setActive' ? setActive : signIn;
const result = (targetObj as any)[method](...args);
resolve(result);
} catch (error) {
reject(error);
}
}
}, []);

const createProxy = useCallback(<T>(target: 'signIn' | 'setActive'): T => {
const proxyTarget: any = {};

return new Proxy(
proxyTarget,
{
get(_, prop) {
// Prevent React from treating this proxy as a Promise by returning undefined for 'then'
if (prop === 'then') {
return undefined;
}

// Handle Symbol properties and other non-method properties
if (typeof prop === 'symbol' || typeof prop !== 'string') {
return undefined;
}

// For observableState property, return undefined in proxy mode
if (prop === 'observableState' && target === 'signIn') {
return undefined;
}

return (...args: any[]) => {
return new Promise((resolve, reject) => {
callQueue.push({
target,
method: String(prop),
args,
resolve,
reject,
});
});
};
},
has(_, prop) {
// Return false for 'then' to prevent Promise-like behavior
if (prop === 'then') {
return false;
}
// Return true for all other properties to indicate they exist on the proxy
return true;
},
ownKeys(_) {
return Object.getOwnPropertyNames(proxyTarget);
},
getOwnPropertyDescriptor(_, prop) {
return Object.getOwnPropertyDescriptor(proxyTarget, prop);
}
},
) as T;
}, []);
Comment on lines +109 to +159
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

setActive proxy is not callable – will throw at runtime

createProxy always uses a plain object ({}) as proxyTarget.
When the target is 'setActive', downstream code expects a function and will do:

const { setActive } = useSignIn();
await setActive('someSessionId');

A proxy over a non-callable object cannot be invoked and will raise
TypeError: setActive is not a function.

Fix: make the proxy target callable and implement an apply trap so direct
invocations are routed through the queue, in line with how property access is
handled for signIn methods.

   const createProxy = useCallback(<T>(target: 'signIn' | 'setActive'): T => {
-    const proxyTarget: any = {};
+    // Function targets stay callable; object targets keep method semantics
+    const proxyTarget: any =
+      target === 'setActive' ? (..._args: unknown[]) => undefined : {};

     return new Proxy(
       proxyTarget,
       {
+        /* Proxy invoked like a function → enqueue */
+        apply(_unused, _thisArg, args) {
+          return new Promise((resolve, reject) => {
+            callQueue.push({
+              target,
+              // Special marker for direct invocation
+              method: 'apply',
+              args,
+              resolve,
+              reject,
+            });
+          });
+        },

This keeps setActive usable before Clerk is loaded and prevents hard crashes.

🤖 Prompt for AI Agents
In packages/react/src/hooks/useSignIn.ts between lines 108 and 158, the
createProxy function uses a plain object as the proxy target for both 'signIn'
and 'setActive', but 'setActive' is expected to be callable. To fix this, change
the proxy target to a callable function when target is 'setActive' and add an
apply trap to the proxy handler that routes direct function calls through the
callQueue, similar to how property accesses are handled. This will prevent
runtime TypeErrors when invoking setActive before Clerk is loaded.


// Memoize the result to prevent unnecessary re-renders
return useMemo(() => {
if (client) {
processQueue(client.signIn, isomorphicClerk.setActive);

// Create an enhanced signIn object that includes the observable state
const enhancedSignIn: ObservableSignInResource = Object.create(client.signIn);
Object.defineProperty(enhancedSignIn, 'observableState', {
value: signInStore,
writable: false,
enumerable: true,
configurable: true
});

return {
isLoaded: true,
signIn: enhancedSignIn,
setActive: isomorphicClerk.setActive,
signInStore,
};
}

return {
isLoaded: true,
signIn: createProxy<ObservableSignInResource>('signIn'),
setActive: createProxy<SetActive>('setActive'),
signInStore: {},
};
}, [client, isomorphicClerk?.setActive, signInStore, processQueue, createProxy]);
};
31 changes: 17 additions & 14 deletions packages/types/src/client.ts
Original file line number Diff line number Diff line change
@@ -5,26 +5,29 @@ import type { SignUpResource } from './signUp';
import type { ClientJSONSnapshot } from './snapshots';

export interface ClientResource extends ClerkResource {
sessions: SessionResource[];
signedInSessions: SignedInSessionResource[];
signUp: SignUpResource;
signIn: SignInResource;
isNew: () => boolean;
create: () => Promise<ClientResource>;
destroy: () => Promise<void>;
removeSessions: () => Promise<ClientResource>;
clearCache: () => void;
isEligibleForTouch: () => boolean;
buildTouchUrl: (params: { redirectUrl: URL }) => string;
lastActiveSessionId: string | null;
captchaBypass: boolean;
cookieExpiresAt: Date | null;
createdAt: Date | null;
lastActiveSessionId: string | null;
sessions: SessionResource[];
signedInSessions: SignedInSessionResource[];
signIn: SignInResource;
signUp: SignUpResource;
updatedAt: Date | null;
__internal_sendCaptchaToken: (params: unknown) => Promise<ClientResource>;
__internal_toSnapshot: () => ClientJSONSnapshot;
/**
* @deprecated Use `signedInSessions` instead.
*/
activeSessions: ActiveSessionResource[];

buildTouchUrl: (params: { redirectUrl: URL }) => string;
clearCache: () => void;
create: () => Promise<ClientResource>;
destroy: () => Promise<void>;
isEligibleForTouch: () => boolean;
isNew: () => boolean;
removeSessions: () => Promise<ClientResource>;

__internal_sendCaptchaToken: (params: unknown) => Promise<ClientResource>;
__internal_toSnapshot: () => ClientJSONSnapshot;

}
36 changes: 16 additions & 20 deletions packages/types/src/hooks.ts
Original file line number Diff line number Diff line change
@@ -121,26 +121,22 @@ export type UseAuthReturn =
/**
* @inline
*/
export type UseSignInReturn =
| {
/**
* A boolean that indicates whether Clerk has completed initialization. Initially `false`, becomes `true` once Clerk loads.
*/
isLoaded: false;
/**
* An object that contains the current sign-in attempt status and methods to create a new sign-in attempt.
*/
signIn: undefined;
/**
* A function that sets the active session.
*/
setActive: undefined;
}
| {
isLoaded: true;
signIn: SignInResource;
setActive: SetActive;
};
export type UseSignInReturn = {
/**
* Always `true`. The hook returns proxy objects that queue method calls until Clerk initializes.
* Method calls are buffered and executed once initialization completes, allowing immediate usage
* without waiting for the loading state to resolve.
*/
isLoaded: true;
/**
* An object that contains the current sign-in attempt status and methods to create a new sign-in attempt.
*/
signIn: SignInResource;
/**
* A function that sets the active session.
*/
setActive: SetActive;
};

/**
* @inline
16 changes: 16 additions & 0 deletions packages/types/src/resource.ts
Original file line number Diff line number Diff line change
@@ -2,6 +2,18 @@ export type ClerkResourceReloadParams = {
rotatingTokenNonce?: string;
};

/**
* Minimal store interface to avoid importing zustand in types package
*/
export interface ResourceStoreApi<T = any> {
getState: () => T;
setState: {
(partial: T | Partial<T> | ((state: T) => T | Partial<T>), replace?: false | undefined): void;
(state: T | ((state: T) => T), replace: true): void;
};
subscribe: (listener: (state: T, prevState: T) => void) => () => void;
}

/**
* Defines common properties and methods that all Clerk resources must implement.
*/
@@ -18,4 +30,8 @@ export interface ClerkResource {
* Reload the resource and return the resource itself.
*/
reload(p?: ClerkResourceReloadParams): Promise<this>;
/**
* The reactive store for this resource
*/
store: ResourceStoreApi;
}
11 changes: 6 additions & 5 deletions packages/types/src/signIn.ts
Original file line number Diff line number Diff line change
@@ -81,13 +81,14 @@ export interface SignInResource extends ClerkResource {
/**
* @deprecated This attribute will be removed in the next major version.
*/
supportedIdentifiers: SignInIdentifier[];
supportedFirstFactors: SignInFirstFactor[] | null;
supportedSecondFactors: SignInSecondFactor[] | null;
createdSessionId: string | null;
firstFactorVerification: VerificationResource;
secondFactorVerification: VerificationResource;
identifier: string | null;
createdSessionId: string | null;
secondFactorVerification: VerificationResource;
signInError: { global: string | null; fields: Record<string, string> };
supportedFirstFactors: SignInFirstFactor[] | null;
supportedIdentifiers: SignInIdentifier[];
supportedSecondFactors: SignInSecondFactor[] | null;
userData: UserData;

create: (params: SignInCreateParams) => Promise<SignInResource>;
93 changes: 83 additions & 10 deletions packages/vue/src/composables/useSignIn.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
import { eventMethodCalled } from '@clerk/shared/telemetry';
import type { UseSignInReturn } from '@clerk/types';
import type { SetActive,SignInResource, UseSignInReturn } from '@clerk/types';
import { computed, watch } from 'vue';

import type { ToComputedRefs } from '../utils';
import { toComputedRefs } from '../utils';
import { useClerkContext } from './useClerkContext';

type UseSignIn = () => ToComputedRefs<UseSignInReturn>;
type UseSignIn = () => ToComputedRefs<UseSignInReturn> | ToComputedRefs<DeferredUseSignInReturn>;

/**
* A deferred proxy type that represents a resource that is not yet available
* but will be hydrated once Clerk is loaded. This prevents unsafe type casting
* and provides proper static typing for methods that return Promises.
*/
type Deferred<T> = {
[K in keyof T]: T[K] extends (...args: infer Args) => Promise<infer Return>
? (...args: Args) => Promise<Return>
: T[K] extends (...args: infer Args) => infer Return
? (...args: Args) => Promise<Return>
: T[K];
};

type DeferredUseSignInReturn = {
isLoaded: true;
signIn: Deferred<SignInResource>;
setActive: Deferred<SetActive>;
};

/**
* Returns the current [`SignIn`](https://clerk.com/docs/references/javascript/sign-in) object which provides
@@ -20,11 +39,7 @@ type UseSignIn = () => ToComputedRefs<UseSignInReturn>;
* </script>
*
* <template>
* <div v-if="!isLoaded">
* <!-- Handle loading state -->
* </div>
*
* <div v-else>
* <div>
* The current sign in attempt status is {{ signIn.status }}.
* </div>
* </template>
@@ -39,16 +54,74 @@ export const useSignIn: UseSignIn = () => {
}
});

const result = computed<UseSignInReturn>(() => {
const result = computed<UseSignInReturn | DeferredUseSignInReturn>(() => {
if (!clerk.value || !clientCtx.value) {
return { isLoaded: false, signIn: undefined, setActive: undefined };
// Create proxy objects that queue calls until clerk loads
const createProxy = <T>(target: 'signIn' | 'setActive'): Deferred<T> => {
return new Proxy({}, {
get(_, prop) {
// Prevent Vue from treating this proxy as a Promise by returning undefined for 'then'
if (prop === 'then') {
return undefined;
}

// Handle Symbol properties and other non-method properties
if (typeof prop === 'symbol' || typeof prop !== 'string') {
return undefined;
}

return (...args: any[]) => {
return new Promise((resolve, reject) => {
// Wait for next tick and try again
setTimeout(() => {
if (clerk.value && clientCtx.value) {
const targetObj = target === 'setActive' ? clerk.value.setActive : clientCtx.value.signIn;
try {
// Type-safe method call by checking if the property exists and is callable
if (targetObj && typeof targetObj === 'object' && prop in targetObj) {
const method = (targetObj as any)[prop];
if (typeof method === 'function') {
const result = method.apply(targetObj, args);
resolve(result);
} else {
reject(new Error(`Property ${prop} is not a function on ${target}`));
}
} else {
reject(new Error(`Method ${prop} not found on ${target}`));
}
} catch (error) {
reject(error);
}
} else {
reject(new Error('Clerk not loaded'));
}
}, 0);
});
};
Comment on lines +57 to +100
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

Deferred proxy rejects after a single 0 ms retry – promises are lost if Clerk loads later

createProxy schedules one setTimeout(0) per call and then rejects if clerk is still undefined (see lines 75-98).
In realistic apps Clerk can load hundreds of ms later (network, hydration, etc.), causing every early call to fail even though the resource eventually becomes available.

Contrast this with the React implementation which queues calls until the client is ready.

Suggested minimal fix: queue the calls in memory and flush once clerk.value && clientCtx.value become truthy, mirroring the React hook.

-                setTimeout(() => {
-                  if (clerk.value && clientCtx.value) {
-
-                  } else {
-                    reject(new Error('Clerk not loaded'));
-                  }
-                }, 0);
+                const attempt = () => {
+                  if (clerk.value && clientCtx.value) {
+
+                  } else {
+                    // try again on next tick until either loaded or a timeout limit is hit
+                    setTimeout(attempt, 10);
+                  }
+                };
+                attempt();

Without this, early calls in SSR/slow-network scenarios break the “always-loaded” contract.

📝 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 result = computed<UseSignInReturn | DeferredUseSignInReturn>(() => {
if (!clerk.value || !clientCtx.value) {
return { isLoaded: false, signIn: undefined, setActive: undefined };
// Create proxy objects that queue calls until clerk loads
const createProxy = <T>(target: 'signIn' | 'setActive'): Deferred<T> => {
return new Proxy({}, {
get(_, prop) {
// Prevent Vue from treating this proxy as a Promise by returning undefined for 'then'
if (prop === 'then') {
return undefined;
}
// Handle Symbol properties and other non-method properties
if (typeof prop === 'symbol' || typeof prop !== 'string') {
return undefined;
}
return (...args: any[]) => {
return new Promise((resolve, reject) => {
// Wait for next tick and try again
setTimeout(() => {
if (clerk.value && clientCtx.value) {
const targetObj = target === 'setActive' ? clerk.value.setActive : clientCtx.value.signIn;
try {
// Type-safe method call by checking if the property exists and is callable
if (targetObj && typeof targetObj === 'object' && prop in targetObj) {
const method = (targetObj as any)[prop];
if (typeof method === 'function') {
const result = method.apply(targetObj, args);
resolve(result);
} else {
reject(new Error(`Property ${prop} is not a function on ${target}`));
}
} else {
reject(new Error(`Method ${prop} not found on ${target}`));
}
} catch (error) {
reject(error);
}
} else {
reject(new Error('Clerk not loaded'));
}
}, 0);
});
};
const result = computed<UseSignInReturn | DeferredUseSignInReturn>(() => {
if (!clerk.value || !clientCtx.value) {
// Create proxy objects that queue calls until clerk loads
const createProxy = <T>(target: 'signIn' | 'setActive'): Deferred<T> => {
return new Proxy({}, {
get(_, prop) {
// Prevent Vue from treating this proxy as a Promise by returning undefined for 'then'
if (prop === 'then') {
return undefined;
}
// Handle Symbol properties and other non-method properties
if (typeof prop === 'symbol' || typeof prop !== 'string') {
return undefined;
}
return (...args: any[]) => {
return new Promise((resolve, reject) => {
- // Wait for next tick and try again
- setTimeout(() => {
- if (clerk.value && clientCtx.value) {
- const targetObj = target === 'setActive'
- ? clerk.value.setActive
- : clientCtx.value.signIn;
- try {
- // Type-safe method call by checking if the property exists and is callable
- if (targetObj && typeof targetObj === 'object' && prop in targetObj) {
- const method = (targetObj as any)[prop];
- if (typeof method === 'function') {
- const result = method.apply(targetObj, args);
- resolve(result);
- } else {
- reject(new Error(`Property ${prop} is not a function on ${target}`));
- }
- } else {
- reject(new Error(`Method ${prop} not found on ${target}`));
- }
- } catch (error) {
- reject(error);
- }
- } else {
- reject(new Error('Clerk not loaded'));
- }
- }, 0);
+ // Retry on next ticks until Clerk is loaded
+ const attempt = () => {
+ if (clerk.value && clientCtx.value) {
+ const targetObj = target === 'setActive'
+ ? clerk.value.setActive
+ : clientCtx.value.signIn;
+ try {
+ // Type-safe method call by checking if the property exists and is callable
+ if (targetObj && typeof targetObj === 'object' && prop in targetObj) {
+ const method = (targetObj as any)[prop];
+ if (typeof method === 'function') {
+ const result = method.apply(targetObj, args);
+ resolve(result);
+ } else {
+ reject(new Error(`Property ${prop} is not a function on ${target}`));
+ }
+ } else {
+ reject(new Error(`Method ${prop} not found on ${target}`));
+ }
+ } catch (error) {
+ reject(error);
+ }
+ } else {
+ // try again after a short delay
+ setTimeout(attempt, 10);
+ }
+ };
+ attempt();
});
};
},
});
};
return {
isLoaded: false,
signIn: createProxy('signIn'),
setActive: createProxy('setActive'),
};
}
// ... rest of the loaded case ...
});
🤖 Prompt for AI Agents
In packages/vue/src/composables/useSignIn.ts between lines 57 and 100, the
deferred proxy created by createProxy currently retries only once with
setTimeout(0) and rejects if clerk or clientCtx are not ready, causing early
calls to fail in slow load scenarios. To fix this, implement an in-memory queue
to store all calls made before clerk and clientCtx are available, and once they
become truthy, flush the queue by invoking the stored calls on the actual
methods. This approach ensures calls are not lost and mirrors the React hook
behavior, maintaining the "always-loaded" contract.

},
has(_, prop) {
// Return false for 'then' to prevent Promise-like behavior
if (prop === 'then') {
return false;
}
// Return true for all other properties to indicate they exist on the proxy
return true;
},
}) as Deferred<T>;
};

return {
isLoaded: true,
signIn: createProxy<SignInResource>('signIn'),
setActive: createProxy<SetActive>('setActive'),
} satisfies DeferredUseSignInReturn;
}

return {
isLoaded: true,
signIn: clientCtx.value.signIn,
setActive: clerk.value.setActive,
};
} satisfies UseSignInReturn;
});

return toComputedRefs(result);
32 changes: 31 additions & 1 deletion pnpm-lock.yaml