diff --git a/.changeset/happy-dodos-sneeze.md b/.changeset/happy-dodos-sneeze.md new file mode 100644 index 00000000000..3652f25f936 --- /dev/null +++ b/.changeset/happy-dodos-sneeze.md @@ -0,0 +1,7 @@ +--- +'@clerk/clerk-js': minor +'@clerk/clerk-react': minor +'@clerk/types': minor +--- + +[Experimental] Signal Errors diff --git a/packages/clerk-js/bundlewatch.config.json b/packages/clerk-js/bundlewatch.config.json index 6278f974848..ab2cdca30fb 100644 --- a/packages/clerk-js/bundlewatch.config.json +++ b/packages/clerk-js/bundlewatch.config.json @@ -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" }, diff --git a/packages/clerk-js/src/core/resources/SignIn.ts b/packages/clerk-js/src/core/resources/SignIn.ts index 1585c731f79..e02ae12614a 100644 --- a/packages/clerk-js/src/core/resources/SignIn.ts +++ b/packages/clerk-js/src/core/resources/SignIn.ts @@ -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 }; } @@ -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 }; } diff --git a/packages/clerk-js/src/core/signals.ts b/packages/clerk-js/src/core/signals.ts index 781b082822a..5e10161485c 100644 --- a/packages/clerk-js/src/core/signals.ts +++ b/packages/clerk-js/src/core/signals.ts @@ -1,17 +1,96 @@ +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 }; }); + +/** + * 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: unknown): 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, + }, + raw: [], + global: [], + }; + + if (!isClerkAPIResponseError(error)) { + parsedErrors.raw.push(error); + parsedErrors.global.push(error); + return parsedErrors; + } + + parsedErrors.raw.push(...error.errors); + + 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; + default: + parsedErrors.global.push(error); + break; + } + } else { + parsedErrors.global.push(error); + } + }); + + return parsedErrors; +} diff --git a/packages/clerk-js/src/core/state.ts b/packages/clerk-js/src/core/state.ts index b18c07ca78b..94255e8a6e6 100644 --- a/packages/clerk-js/src/core/state.ts +++ b/packages/clerk-js/src/core/state.ts @@ -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 }); } }; diff --git a/packages/react/src/hooks/useClerkSignal.ts b/packages/react/src/hooks/useClerkSignal.ts index 37e9851ec6b..25d2b37362a 100644 --- a/packages/react/src/hooks/useClerkSignal.ts +++ b/packages/react/src/hooks/useClerkSignal.ts @@ -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') { useAssertWrappedByClerkProvider('useSignInSignal'); const clerk = useIsomorphicClerkContext(); diff --git a/packages/types/src/state.ts b/packages/types/src/state.ts index 8f91dde4412..200fe15cebd 100644 --- a/packages/types/src/state.ts +++ b/packages/types/src/state.ts @@ -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; }; /**