Skip to content

Commit

Permalink
feat(clerk-js,shared,types): Add ClerkRuntimeError class (#1813)
Browse files Browse the repository at this point in the history
With the introduction of this class, we can localize error messages and display them in ClerkJS components. This functionality existed for FAPI errors, and we are now adding support for runtime errors.
  • Loading branch information
panteliselef authored Oct 4, 2023
1 parent ccf4210 commit 164f3aa
Show file tree
Hide file tree
Showing 10 changed files with 126 additions and 15 deletions.
8 changes: 8 additions & 0 deletions .changeset/curly-scissors-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@clerk/localizations': patch
'@clerk/clerk-js': patch
'@clerk/shared': patch
'@clerk/types': patch
---

Introduce ClerkRuntimeError class for localizing error messages in ClerkJS components
1 change: 1 addition & 0 deletions packages/clerk-js/src/core/resources/Error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export {
parseErrors,
MagicLinkError,
ClerkAPIResponseError,
isClerkRuntimeError,
} from '@clerk/shared';
export type { MetamaskError } from '@clerk/shared';
4 changes: 2 additions & 2 deletions packages/clerk-js/src/core/resources/SignUp.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Poller } from '@clerk/shared';
import { ClerkRuntimeError, Poller } from '@clerk/shared';
import type {
AttemptEmailAddressVerificationParams,
AttemptPhoneNumberVerificationParams,
Expand Down Expand Up @@ -80,7 +80,7 @@ export class SignUp extends BaseResource implements SignUpResource {
if (e.captchaError) {
paramsWithCaptcha.captchaError = e.captchaError;
} else {
throw e;
throw new ClerkRuntimeError(e.message, { code: 'captcha_unavailable' });
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createContextAndHook } from '@clerk/shared';
import type { ClerkAPIError } from '@clerk/types';
import type { ClerkAPIError, ClerkRuntimeError } from '@clerk/types';
import React from 'react';

import { useLocalizations } from '../../customizables';
Expand Down Expand Up @@ -31,7 +31,8 @@ const useCardState = () => {
const { translateError } = useLocalizations();

const setIdle = (metadata?: Metadata) => setState(s => ({ ...s, status: 'idle', metadata }));
const setError = (metadata: ClerkAPIError | Metadata) => setState(s => ({ ...s, error: translateError(metadata) }));
const setError = (metadata: ClerkRuntimeError | ClerkAPIError | Metadata) =>
setState(s => ({ ...s, error: translateError(metadata) }));
const setLoading = (metadata?: Metadata) => setState(s => ({ ...s, status: 'loading', metadata }));
const runAsync = async <T = unknown,>(cb: Promise<T> | (() => Promise<T>), metadata?: Metadata) => {
setLoading(metadata);
Expand Down
12 changes: 9 additions & 3 deletions packages/clerk-js/src/ui/localization/makeLocalizable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ClerkAPIError, LocalizationResource } from '@clerk/types';
import { isClerkRuntimeError } from '@clerk/shared';
import type { ClerkAPIError, ClerkRuntimeError, LocalizationResource } from '@clerk/types';
import React from 'react';

import { useOptions } from '../contexts';
Expand Down Expand Up @@ -70,11 +71,16 @@ export const useLocalizations = () => {
return localizedStringFromKey(localizationKey, parsedResource, globalTokens);
};

const translateError = (error: ClerkAPIError | string | undefined) => {
const translateError = (error: ClerkRuntimeError | ClerkAPIError | string | undefined) => {
if (!error || typeof error === 'string') {
return t(error);
}
const { code, message, longMessage, meta } = error || {};

if (isClerkRuntimeError(error)) {
return t(localizationKeys(`unstable__errors.${error.code}` as any)) || error.message;
}

const { code, message, longMessage, meta } = (error || {}) as ClerkAPIError;
const { paramName = '' } = meta || {};

if (!code) {
Expand Down
31 changes: 27 additions & 4 deletions packages/clerk-js/src/ui/utils/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import { snakeToCamel } from '@clerk/shared';
import type { ClerkAPIError } from '@clerk/types';

import { isClerkAPIResponseError, isKnownError, isMetamaskError } from '../../core/resources/internal';
import type { ClerkAPIError, ClerkRuntimeError } from '@clerk/types';

import {
isClerkAPIResponseError,
isClerkRuntimeError,
isKnownError,
isMetamaskError,
} from '../../core/resources/internal';
import type { FormControlState } from './useFormControl';

interface ParserErrors {
Expand Down Expand Up @@ -52,7 +57,7 @@ type HandleError = {
(
err: Error,
fieldStates: Array<FormControlState<string>>,
setGlobalError?: (err: ClerkAPIError | string | undefined) => void,
setGlobalError?: (err: ClerkRuntimeError | ClerkAPIError | string | undefined) => void,
): void;
};

Expand All @@ -69,6 +74,10 @@ export const handleError: HandleError = (err, fieldStates, setGlobalError) => {
if (isClerkAPIResponseError(err)) {
return handleClerkApiError(err, fieldStates, setGlobalError);
}

if (isClerkRuntimeError(err)) {
return handleClerkRuntimeError(err, fieldStates, setGlobalError);
}
};

// Returns the first global API error or undefined if none exists.
Expand Down Expand Up @@ -123,3 +132,17 @@ const handleClerkApiError: HandleError = (err, fieldStates, setGlobalError) => {
}
}
};

const handleClerkRuntimeError: HandleError = (err, _, setGlobalError) => {
if (!isClerkRuntimeError(err)) {
return;
}

if (setGlobalError) {
setGlobalError(undefined);
const firstGlobalError = err;
if (firstGlobalError) {
setGlobalError(firstGlobalError);
}
}
};
2 changes: 2 additions & 0 deletions packages/localizations/src/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,8 @@ export const enUS: LocalizationResource = {
identification_deletion_failed: 'You cannot delete your last identification.',
phone_number_exists: 'This phone number is taken. Please try another.',
form_identifier_not_found: '',
captcha_unavailable:
'Sign up unsuccessful due to failed bot validation. Please refresh the page to try again or reach out to support for more assistance.',
captcha_invalid:
'Sign up unsuccessful due to failed security validations. Please refresh the page to try again or reach out to support for more assistance.',
form_password_pwned:
Expand Down
72 changes: 68 additions & 4 deletions packages/shared/src/errors/Error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,37 @@ export interface MetamaskError extends Error {
}

export function isKnownError(error: any) {
return isClerkAPIResponseError(error) || isMetamaskError(error);
return isClerkAPIResponseError(error) || isMetamaskError(error) || isClerkRuntimeError(error);
}

export function isClerkAPIResponseError(err: any): err is ClerkAPIResponseError {
if (err instanceof ClerkAPIResponseError) {
return true;
}

return 'clerkError' in err;
}

/**
* Checks if the provided error object is an instance of ClerkRuntimeError.
*
* @param {any} err - The error object to check.
* @returns {boolean} True if the error is a ClerkRuntimeError, false otherwise.
*
* @example
* const error = new ClerkRuntimeError('An error occurred');
* if (isClerkRuntimeError(error)) {
* // Handle ClerkRuntimeError
* console.error('ClerkRuntimeError:', error.message);
* } else {
* // Handle other errors
* console.error('Other error:', error.message);
* }
*/
export function isClerkRuntimeError(err: any): err is ClerkRuntimeError {
return err instanceof ClerkRuntimeError;
}

export function isMetamaskError(err: any): err is MetamaskError {
return 'code' in err && [4001, 32602, 32603].includes(err.code) && 'message' in err;
}
Expand All @@ -44,8 +68,6 @@ export function parseError(error: ClerkAPIErrorJSON): ClerkAPIError {
}

export class ClerkAPIResponseError extends Error {
clerkError: true;

status: number;
message: string;

Expand All @@ -58,7 +80,6 @@ export class ClerkAPIResponseError extends Error {

this.status = status;
this.message = message;
this.clerkError = true;
this.errors = parseErrors(data);
}

Expand All @@ -69,6 +90,49 @@ export class ClerkAPIResponseError extends Error {
};
}

/**
* Custom error class for representing Clerk runtime errors.
*
* @class ClerkRuntimeError
* @example
* throw new ClerkRuntimeError('An error occurred', { code: 'password_invalid' });
*/
export class ClerkRuntimeError extends Error {
/**
* The error message.
*
* @type {string}
* @memberof ClerkRuntimeError
*/
message: string;

/**
* A unique code identifying the error, used for localization
*
* @type {string}
* @memberof ClerkRuntimeError
*/
code: string;
constructor(message: string, { code }: { code: string }) {
super(message);

Object.setPrototypeOf(this, ClerkRuntimeError.prototype);

this.code = code;
this.message = message;
}

/**
* Returns a string representation of the error.
*
* @returns {string} A formatted string with the error name and message.
* @memberof ClerkRuntimeError
*/
public toString = () => {
return `[${this.name}]\nMessage:${this.message}`;
};
}

export class MagicLinkError extends Error {
code: string;

Expand Down
5 changes: 5 additions & 0 deletions packages/types/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export interface ClerkAPIError {
};
}

export interface ClerkRuntimeError {
code: string;
message: string;
}

/**
* Pagination params
*/
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/localization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,7 @@ type UnstableErrors = WithParamName<{
identification_deletion_failed: LocalizationValue;
phone_number_exists: LocalizationValue;
form_identifier_not_found: LocalizationValue;
captcha_unavailable: LocalizationValue;
captcha_invalid: LocalizationValue;
form_password_pwned: LocalizationValue;
form_username_invalid_length: LocalizationValue;
Expand Down

0 comments on commit 164f3aa

Please sign in to comment.