-
Notifications
You must be signed in to change notification settings - Fork 407
feat(clerk-js,clerk-react,shared): Operation-specific errors fields #7195
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| --- | ||
| '@clerk/clerk-js': minor | ||
| '@clerk/shared': minor | ||
| '@clerk/clerk-react': minor | ||
| --- | ||
|
|
||
| [Experimental] Update `errors` to have specific field types based on whether it's a sign-in or a sign-up. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import type { ClerkError } from '@clerk/shared/error'; | ||
| import { ClerkAPIResponseError } from '@clerk/shared/error'; | ||
| import { describe, expect, it } from 'vitest'; | ||
|
|
||
| import { errorsToParsedErrors } from '../signals'; | ||
|
|
||
| describe('errorsToParsedErrors', () => { | ||
| it('returns empty errors object when error is null', () => { | ||
| const initialFields = { emailAddress: null, password: null }; | ||
| const result = errorsToParsedErrors(null, initialFields); | ||
|
|
||
| expect(result).toEqual({ | ||
| fields: { emailAddress: null, password: null }, | ||
| raw: null, | ||
| global: null, | ||
| }); | ||
| }); | ||
|
|
||
| it('handles non-API errors by putting them in raw and global arrays', () => { | ||
| const initialFields = { emailAddress: null, password: null }; | ||
| // Use a plain Error cast as ClerkError to test non-API error handling | ||
| const error = new Error('Something went wrong') as unknown as ClerkError; | ||
| const result = errorsToParsedErrors(error, initialFields); | ||
|
|
||
| expect(result.fields).toEqual({ emailAddress: null, password: null }); | ||
| expect(result.raw).toEqual([error]); | ||
| expect(result.global).toBeTruthy(); | ||
| expect(result.global?.length).toBe(1); | ||
| }); | ||
|
|
||
| it('handles API errors with field errors', () => { | ||
| const initialFields = { emailAddress: null, password: null }; | ||
| const error = new ClerkAPIResponseError('Validation failed', { | ||
| data: [ | ||
| { | ||
| code: 'form_identifier_not_found', | ||
| message: 'emailAddress not found', | ||
| meta: { param_name: 'emailAddress' }, | ||
| }, | ||
| ], | ||
| status: 400, | ||
| }); | ||
| const result = errorsToParsedErrors(error, initialFields); | ||
|
|
||
| expect(result.fields.emailAddress).toBeTruthy(); | ||
| expect(result.fields.password).toBeNull(); | ||
| expect(result.raw).toEqual([error.errors[0]]); | ||
| expect(result.global).toBeNull(); | ||
| }); | ||
|
|
||
| it('handles API errors without field errors', () => { | ||
| const initialFields = { emailAddress: null, password: null }; | ||
| const error = new ClerkAPIResponseError('Server error', { | ||
| data: [ | ||
| { | ||
| code: 'internal_error', | ||
| message: 'Something went wrong on the server', | ||
| }, | ||
| ], | ||
| status: 500, | ||
| }); | ||
| const result = errorsToParsedErrors(error, initialFields); | ||
|
|
||
| expect(result.fields).toEqual({ emailAddress: null, password: null }); | ||
| // When there are no field errors, individual ClerkAPIError instances are put in raw | ||
| expect(result.raw).toEqual([error.errors[0]]); | ||
| // Note: global is null when errors are processed individually without field errors | ||
| expect(result.global).toBeNull(); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,5 +1,5 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { type ClerkError, createClerkGlobalHookError, isClerkAPIResponseError } from '@clerk/shared/error'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { Errors, SignInSignal, SignUpSignal } from '@clerk/shared/types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { Errors, SignInErrors, SignInSignal, SignUpErrors, SignUpSignal } from '@clerk/shared/types'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { snakeToCamel } from '@clerk/shared/underscore'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { computed, signal } from 'alien-signals'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -15,7 +15,7 @@ export const signInComputedSignal: SignInSignal = computed(() => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const error = signInErrorSignal().error; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fetchStatus = signInFetchSignal().status; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const errors = errorsToParsedErrors(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const errors = errorsToSignInErrors(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { errors, fetchStatus, signIn: signIn ? signIn.__internal_future : null }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -29,7 +29,7 @@ export const signUpComputedSignal: SignUpSignal = computed(() => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const error = signUpErrorSignal().error; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const fetchStatus = signUpFetchSignal().status; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const errors = errorsToParsedErrors(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const errors = errorsToSignUpErrors(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { errors, fetchStatus, signUp: signUp ? signUp.__internal_future : null }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -38,20 +38,12 @@ export const signUpComputedSignal: SignUpSignal = computed(() => { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * Converts an error to a parsed errors object that reports the specific fields that the error pertains to. Will put | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * generic non-API errors into the global array. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function errorsToParsedErrors(error: ClerkError | null): Errors { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsedErrors: Errors = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fields: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| firstName: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastName: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emailAddress: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| identifier: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| phoneNumber: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| password: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| username: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| captcha: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| legalAccepted: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export function errorsToParsedErrors<T extends Record<string, unknown>>( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| error: ClerkError | null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| initialFields: T, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Errors<T> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsedErrors: Errors<T> = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fields: { ...initialFields }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| raw: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| global: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -76,7 +68,9 @@ function errorsToParsedErrors(error: ClerkError | null): Errors { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ('meta' in error && error.meta && 'paramName' in error.meta) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const name = snakeToCamel(error.meta.paramName); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parsedErrors.fields[name as keyof typeof parsedErrors.fields] = error; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (name in parsedErrors.fields) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (parsedErrors.fields as any)[name] = error; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Note that this assumes a given ClerkAPIResponseError will only have either field errors or global errors, but | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // not both. If a global error is present, it will be discarded. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -91,3 +85,25 @@ function errorsToParsedErrors(error: ClerkError | null): Errors { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return parsedErrors; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function errorsToSignInErrors(error: ClerkError | null): SignInErrors { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return errorsToParsedErrors(error, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| identifier: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| password: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| function errorsToSignUpErrors(error: ClerkError | null): SignUpErrors { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return errorsToParsedErrors(error, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| firstName: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastName: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| emailAddress: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| phoneNumber: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| password: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| username: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| code: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| captcha: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| legalAccepted: null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+97
to
+108
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initialize all sign-up fields before parsing errors. Once we add Please extend the initializer accordingly: function errorsToSignUpErrors(error: ClerkError | null): SignUpErrors {
return errorsToParsedErrors(error, {
firstName: null,
lastName: null,
emailAddress: null,
phoneNumber: null,
password: null,
username: null,
code: null,
captcha: null,
legalAccepted: null,
+ web3Wallet: null,
+ unsafeMetadata: null,
});
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace
anywith a type-safe assignment.The use of
anytype casting violates the project's TypeScript guidelines, which require avoidinganytypes. While the guard at line 71 ensures the field exists, the type system isn't aware of this refinement.Consider this type-safe alternative:
const name = snakeToCamel(error.meta.paramName); if (name in parsedErrors.fields) { - (parsedErrors.fields as any)[name] = error; + parsedErrors.fields = { + ...parsedErrors.fields, + [name]: error, + } as T; }Or use a more explicit approach with a helper function that preserves type safety:
🤖 Prompt for AI Agents