Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
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
7 changes: 7 additions & 0 deletions .changeset/happy-dodos-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@clerk/clerk-js': minor
'@clerk/clerk-react': minor
'@clerk/types': minor
---

[Experimental] Signal Errors
4 changes: 2 additions & 2 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": "621KB" },
{ "path": "./dist/clerk.js", "maxSize": "622KB" },
{ "path": "./dist/clerk.browser.js", "maxSize": "75KB" },
{ "path": "./dist/clerk.legacy.browser.js", "maxSize": "117KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "57.1KB" },
{ "path": "./dist/clerk.headless*.js", "maxSize": "58KB" },
{ "path": "./dist/ui-common*.js", "maxSize": "113KB" },
{ "path": "./dist/ui-common*.legacy.*.js", "maxSize": "118KB" },
{ "path": "./dist/vendors*.js", "maxSize": "40.2KB" },
Expand Down
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/SignIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,7 @@ class SignInFuture implements SignInFutureResource {
});

return { error: null };
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand All @@ -530,7 +530,7 @@ class SignInFuture implements SignInFutureResource {
path: this.resource.pathRoot,
body: { identifier, password },
});
} catch (err) {
} catch (err: unknown) {
eventBus.emit('resource:error', { resource: this.resource, error: err });
return { error: err };
}
Expand Down
76 changes: 73 additions & 3 deletions packages/clerk-js/src/core/signals.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,87 @@
import { isClerkAPIResponseError } from '@clerk/shared/error';
import type { Errors } from '@clerk/types';
import { computed, signal } from 'alien-signals';

import type { SignIn } from './resources/SignIn';

export const signInSignal = signal<{ resource: SignIn | null }>({ resource: null });
export const signInErrorSignal = signal<{ errors: unknown }>({ errors: null });
export const signInErrorSignal = signal<{ error: unknown }>({ error: null });

export const signInComputedSignal = computed(() => {
const signIn = signInSignal().resource;
const errors = signInErrorSignal().errors;
const error = signInErrorSignal().error;

const errors = errorsToParsedErrors(error);

if (!signIn) {
return { errors: null, signIn: null };
return { errors, signIn: null };
}

return { errors, signIn: signIn.__internal_future };
});

export function errorsToParsedErrors(error: unknown): Errors {
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

Add JSDoc documentation for exported function

Per the coding guidelines, all public APIs must be documented with JSDoc.

+/**
+ * Parses an error into a structured Errors object with field-specific and global errors.
+ * @param error - The error to parse, typically from a Clerk API response
+ * @returns A structured Errors object with parsed field errors and global errors
+ */
 export function errorsToParsedErrors(error: unknown): Errors {
📝 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
export function errorsToParsedErrors(error: unknown): Errors {
/**
* Parses an error into a structured Errors object with field-specific and global errors.
* @param error - The error to parse, typically from a Clerk API response
* @returns A structured Errors object with parsed field errors and global errors
*/
export function errorsToParsedErrors(error: unknown): Errors {
🤖 Prompt for AI Agents
In packages/clerk-js/src/core/signals.ts around line 23, the exported function
errorsToParsedErrors is missing JSDoc documentation; add a JSDoc block
immediately above the function that describes the function purpose, its
parameters (type and meaning for the unknown error input), return type (Errors)
and any thrown errors or notes about accepted input shapes, include examples or
behavior for common inputs if relevant, and follow the repo JSDoc style (summary
line, @param, @returns, and any @throws or @example tags as appropriate).

const parsedErrors: Errors = {
fields: {
firstName: null,
lastName: null,
emailAddress: null,
identifier: null,
phoneNumber: null,
password: null,
username: null,
code: null,
captcha: null,
legalAccepted: null,
},
raw: [],
global: [],
};

if (!isClerkAPIResponseError(error)) {
parsedErrors.raw.push(error);
parsedErrors.global.push(error);
return parsedErrors;
}

error.errors.forEach(error => {
if ('meta' in error && error.meta && 'paramName' in error.meta) {
switch (error.meta.paramName) {
case 'first_name':
parsedErrors.fields.firstName = error;
break;
case 'last_name':
parsedErrors.fields.lastName = error;
break;
case 'email_address':
parsedErrors.fields.emailAddress = error;
break;
case 'identifier':
parsedErrors.fields.identifier = error;
break;
case 'phone_number':
parsedErrors.fields.phoneNumber = error;
break;
case 'password':
parsedErrors.fields.password = error;
break;
case 'username':
parsedErrors.fields.username = error;
break;
case 'code':
parsedErrors.fields.code = error;
break;
case 'captcha':
parsedErrors.fields.captcha = error;
break;
case 'legal_accepted':
parsedErrors.fields.legalAccepted = error;
break;
}
} else {
parsedErrors.global.push(error);
}
});

return parsedErrors;
}
2 changes: 1 addition & 1 deletion packages/clerk-js/src/core/state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export class State implements StateInterface {

private onResourceError = (payload: { resource: BaseResource; error: unknown }) => {
if (payload.resource instanceof SignIn) {
this.signInErrorSignal({ errors: payload.error });
this.signInErrorSignal({ error: payload.error });
}
};

Expand Down
3 changes: 1 addition & 2 deletions packages/react/src/hooks/useClerkSignal.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { SignInFutureResource } from '@clerk/types';
import { useCallback, useSyncExternalStore } from 'react';

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

function useClerkSignal(signal: 'signIn'): { errors: unknown; signIn: SignInFutureResource | null } | null {
function useClerkSignal(signal: 'signIn') {
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

Restore explicit return types for public APIs

Per guidelines, exported hooks should have explicit return types. You can capture the core’s type without duplicating shapes via ReturnType of the State['signInSignal'].

+import type { State as ClerkState } from '@clerk/types';
 
-function useClerkSignal(signal: 'signIn') {
+function useClerkSignal(signal: 'signIn'): ReturnType<ClerkState['signInSignal']> | null {
   …
 }
 
-export function useSignInSignal() {
+export function useSignInSignal(): ReturnType<ClerkState['signInSignal']> | null {
   return useClerkSignal('signIn');
 }
📝 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
function useClerkSignal(signal: 'signIn') {
// Add the type import
import type { State as ClerkState } from '@clerk/types';
function useClerkSignal(
signal: 'signIn'
): ReturnType<ClerkState['signInSignal']> | null {
}
export function useSignInSignal(): ReturnType<ClerkState['signInSignal']> | null {
return useClerkSignal('signIn');
}
🤖 Prompt for AI Agents
In packages/react/src/hooks/useClerkSignal.ts at line 6, the exported hook
useClerkSignal lacks an explicit return type. Fix this by adding an explicit
return type using ReturnType<typeof State['signInSignal']> to capture the core
type without duplicating shapes, ensuring the public API follows the guidelines.

useAssertWrappedByClerkProvider('useSignInSignal');

const clerk = useIsomorphicClerkContext();
Expand Down
28 changes: 26 additions & 2 deletions packages/types/src/state.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,39 @@
import type { SignInFutureResource } from './signIn';

interface FieldError {
code: string;
longMessage?: string;
message: string;
}

interface FieldErrors {
firstName: FieldError | null;
lastName: FieldError | null;
emailAddress: FieldError | null;
identifier: FieldError | null;
phoneNumber: FieldError | null;
password: FieldError | null;
username: FieldError | null;
code: FieldError | null;
captcha: FieldError | null;
legalAccepted: FieldError | null;
}

export interface Errors {
fields: FieldErrors;
raw: unknown[];
global: unknown[]; // does not include any errors that could be parsed as a field error
}

export interface State {
/**
* A Signal that updates when the underlying `SignIn` resource changes, including errors.
*/
signInSignal: {
(): {
errors: unknown;
errors: Errors;
signIn: SignInFutureResource | null;
};
(value: { errors: unknown; signIn: SignInFutureResource | null }): void;
};

/**
Expand Down
Loading