218 lines
5.2 KiB
TypeScript
218 lines
5.2 KiB
TypeScript
/**
|
|
* External dependencies
|
|
*/
|
|
import { createNotice, DEFAULT_ERROR_MESSAGE } from '@woocommerce/base-utils';
|
|
import { decodeEntities } from '@wordpress/html-entities';
|
|
import {
|
|
objectHasProp,
|
|
ApiErrorResponse,
|
|
isApiErrorResponse,
|
|
ApiErrorResponseData,
|
|
isObject,
|
|
isString,
|
|
} from '@woocommerce/types';
|
|
import { noticeContexts } from '@woocommerce/base-context/event-emit/utils';
|
|
|
|
type ApiParamError = {
|
|
param: string;
|
|
id: string;
|
|
code: string;
|
|
message: string;
|
|
data?: ApiErrorResponseData;
|
|
};
|
|
|
|
/**
|
|
* Flattens error details which are returned from the API when multiple params are not valid.
|
|
*
|
|
* - Codes will be prefixed with the param. For example, `invalid_email` becomes `billing_address_invalid_email`.
|
|
* - Additional error messages will be flattened alongside the main error message.
|
|
* - Supports 1 level of nesting.
|
|
* - Decodes HTML entities in error messages.
|
|
*/
|
|
export const getErrorDetails = (
|
|
response: ApiErrorResponse
|
|
): ApiParamError[] => {
|
|
const errorDetails = objectHasProp( response.data, 'details' )
|
|
? Object.entries( response.data.details )
|
|
: null;
|
|
|
|
if ( ! errorDetails ) {
|
|
return [];
|
|
}
|
|
|
|
return errorDetails.reduce(
|
|
(
|
|
acc,
|
|
[
|
|
param,
|
|
{
|
|
code,
|
|
message,
|
|
additional_errors: additionalErrors = [],
|
|
data,
|
|
},
|
|
]
|
|
) => {
|
|
return [
|
|
...acc,
|
|
{
|
|
param,
|
|
id: `${ param }_${ code }`,
|
|
code,
|
|
message: decodeEntities( message ),
|
|
data,
|
|
},
|
|
...( Array.isArray( additionalErrors )
|
|
? additionalErrors.flatMap( ( additionalError ) => {
|
|
if (
|
|
! objectHasProp( additionalError, 'code' ) ||
|
|
! objectHasProp( additionalError, 'message' )
|
|
) {
|
|
return [];
|
|
}
|
|
const errorObject = [
|
|
{
|
|
param,
|
|
id: `${ param }_${ additionalError.code }`,
|
|
code: additionalError.code,
|
|
message: decodeEntities(
|
|
additionalError.message
|
|
),
|
|
data,
|
|
},
|
|
];
|
|
if ( typeof additionalError.data !== 'undefined' ) {
|
|
return [
|
|
...errorObject,
|
|
...getErrorDetails( additionalError ),
|
|
];
|
|
}
|
|
return errorObject;
|
|
} )
|
|
: [] ),
|
|
];
|
|
},
|
|
[] as ApiParamError[]
|
|
);
|
|
};
|
|
|
|
/**
|
|
* Gets appropriate error context from error code.
|
|
*/
|
|
const getErrorContextFromCode = ( code: string ): string => {
|
|
switch ( code ) {
|
|
case 'woocommerce_rest_missing_email_address':
|
|
case 'woocommerce_rest_invalid_email_address':
|
|
return noticeContexts.CONTACT_INFORMATION;
|
|
default:
|
|
return noticeContexts.CART;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Gets appropriate error context from error param name. Also checks the error code to check if it's an email error
|
|
* (Email is part of the billing address).
|
|
*/
|
|
const getErrorContextFromParam = (
|
|
param: string,
|
|
code: string
|
|
): string | undefined => {
|
|
switch ( param ) {
|
|
case 'invalid_email':
|
|
return noticeContexts.CONTACT_INFORMATION;
|
|
case 'billing_address':
|
|
if ( code === 'invalid_email' ) {
|
|
return noticeContexts.CONTACT_INFORMATION;
|
|
}
|
|
return noticeContexts.BILLING_ADDRESS;
|
|
case 'shipping_address':
|
|
return noticeContexts.SHIPPING_ADDRESS;
|
|
default:
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Gets appropriate error context from additional field location.
|
|
*/
|
|
const getErrorContextFromAdditionalFieldLocation = (
|
|
location: string
|
|
): string | undefined => {
|
|
switch ( location ) {
|
|
case 'contact':
|
|
return noticeContexts.CONTACT_INFORMATION;
|
|
case 'order':
|
|
return noticeContexts.ORDER_INFORMATION;
|
|
default:
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Processes the response for an invalid param error, with response code rest_invalid_param.
|
|
*/
|
|
const processInvalidParamResponse = (
|
|
response: ApiErrorResponse,
|
|
context: string | undefined
|
|
) => {
|
|
const errorDetails = getErrorDetails( response );
|
|
|
|
errorDetails.forEach( ( { code, message, id, param, data } ) => {
|
|
let additionalFieldContext: string | undefined = '';
|
|
// Check if this error response comes from an additional field.
|
|
if (
|
|
isObject( data ) &&
|
|
objectHasProp( data, 'key' ) &&
|
|
objectHasProp( data, 'location' ) &&
|
|
isString( data.location )
|
|
) {
|
|
additionalFieldContext = getErrorContextFromAdditionalFieldLocation(
|
|
data.location
|
|
);
|
|
}
|
|
|
|
createNotice( 'error', message, {
|
|
id,
|
|
context:
|
|
context ||
|
|
additionalFieldContext ||
|
|
getErrorContextFromParam( param, code ) ||
|
|
getErrorContextFromCode( code ),
|
|
} );
|
|
} );
|
|
};
|
|
|
|
/**
|
|
* Takes an API response object and creates error notices to display to the customer.
|
|
*
|
|
* This is where we can handle specific error codes and display notices in specific contexts.
|
|
*/
|
|
export const processErrorResponse = (
|
|
response: ApiErrorResponse | null,
|
|
context?: string | undefined
|
|
) => {
|
|
if ( ! isApiErrorResponse( response ) ) {
|
|
return;
|
|
}
|
|
|
|
if ( response.code === 'rest_invalid_param' ) {
|
|
return processInvalidParamResponse( response, context );
|
|
}
|
|
|
|
let errorMessage =
|
|
decodeEntities( response.message ) || DEFAULT_ERROR_MESSAGE;
|
|
|
|
// Replace the generic invalid JSON message with something more user friendly.
|
|
if ( response.code === 'invalid_json' ) {
|
|
errorMessage = DEFAULT_ERROR_MESSAGE;
|
|
}
|
|
|
|
createNotice( 'error', errorMessage, {
|
|
id: response.code,
|
|
context:
|
|
context ||
|
|
response?.data?.context ||
|
|
getErrorContextFromCode( response.code ),
|
|
} );
|
|
};
|