woocommerce/plugins/woocommerce-blocks/assets/js/data/checkout/utils.ts

225 lines
6.6 KiB
TypeScript

/**
* External dependencies
*/
import { isString, isObject } from '@woocommerce/types';
import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
import type { PaymentResult, CheckoutResponse } from '@woocommerce/types';
import type { createErrorNotice as originalCreateErrorNotice } from '@wordpress/notices/store/actions';
/**
* Internal dependencies
*/
import { useEmitResponse } from '../../base/context/hooks/use-emit-response';
import {
CheckoutAndPaymentNotices,
CheckoutAfterProcessingWithErrorEventData,
} from './types';
import { DispatchFromMap } from '../mapped-types';
import * as actions from './actions';
const { isErrorResponse, isFailResponse, isSuccessResponse, shouldRetry } =
useEmitResponse(); // eslint-disable-line react-hooks/rules-of-hooks
// TODO: `useEmitResponse` is not a react hook, it just exposes some functions as
// properties of an object. Refactor this to not be a hook, we could simply import
// those functions where needed
/**
* Based on the given observers, create Error Notices where necessary
* and return the error response of the last registered observer
*/
export const handleErrorResponse = ( {
observerResponses,
createErrorNotice,
}: {
observerResponses: unknown[];
createErrorNotice: typeof originalCreateErrorNotice;
} ) => {
let errorResponse = null;
observerResponses.forEach( ( response ) => {
if ( isErrorResponse( response ) || isFailResponse( response ) ) {
if ( response.message && isString( response.message ) ) {
const errorOptions =
response.messageContext &&
isString( response.messageContext )
? // The `as string` is OK here because of the type guard above.
{
context: response.messageContext as string,
}
: undefined;
errorResponse = response;
createErrorNotice( response.message, errorOptions );
}
}
} );
return errorResponse;
};
/**
* This functions runs after the CHECKOUT_AFTER_PROCESSING_WITH_ERROR event has been triggered and
* all observers have been processed. It sets any Error Notices and the status of the Checkout
* based on the observer responses
*/
export const runCheckoutAfterProcessingWithErrorObservers = ( {
observerResponses,
notices,
dispatch,
createErrorNotice,
data,
}: {
observerResponses: unknown[];
notices: CheckoutAndPaymentNotices;
dispatch: DispatchFromMap< typeof actions >;
data: CheckoutAfterProcessingWithErrorEventData;
createErrorNotice: typeof originalCreateErrorNotice;
} ) => {
const errorResponse = handleErrorResponse( {
observerResponses,
createErrorNotice,
} );
if ( errorResponse !== null ) {
// irrecoverable error so set complete
if ( ! shouldRetry( errorResponse ) ) {
dispatch.setComplete( errorResponse );
} else {
dispatch.setIdle();
}
} else {
const hasErrorNotices =
notices.checkoutNotices.some(
( notice: { status: string } ) => notice.status === 'error'
) ||
notices.expressPaymentNotices.some(
( notice: { status: string } ) => notice.status === 'error'
) ||
notices.paymentNotices.some(
( notice: { status: string } ) => notice.status === 'error'
);
if ( ! hasErrorNotices ) {
// no error handling in place by anything so let's fall
// back to default
const message =
data.processingResponse?.message ||
__(
'Something went wrong. Please contact us to get assistance.',
'woo-gutenberg-products-block'
);
createErrorNotice( message, {
id: 'checkout',
context: 'wc/checkout',
} );
}
dispatch.setIdle();
}
};
/**
* This functions runs after the CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS event has been triggered and
* all observers have been processed. It sets any Error Notices and the status of the Checkout
* based on the observer responses
*/
export const runCheckoutAfterProcessingWithSuccessObservers = ( {
observerResponses,
dispatch,
createErrorNotice,
}: {
observerResponses: unknown[];
dispatch: DispatchFromMap< typeof actions >;
createErrorNotice: typeof originalCreateErrorNotice;
} ) => {
let successResponse = null as null | Record< string, unknown >;
let errorResponse = null as null | Record< string, unknown >;
observerResponses.forEach( ( response ) => {
if ( isSuccessResponse( response ) ) {
// the last observer response always "wins" for success.
successResponse = response;
}
if ( isErrorResponse( response ) || isFailResponse( response ) ) {
errorResponse = response;
}
} );
if ( successResponse && ! errorResponse ) {
dispatch.setComplete( successResponse );
} else if ( isObject( errorResponse ) ) {
if ( errorResponse.message && isString( errorResponse.message ) ) {
const errorOptions =
errorResponse.messageContext &&
isString( errorResponse.messageContext )
? {
context: errorResponse.messageContext,
}
: undefined;
createErrorNotice( errorResponse.message, errorOptions );
}
if ( ! shouldRetry( errorResponse ) ) {
dispatch.setComplete( errorResponse );
} else {
// this will set an error which will end up
// triggering the onCheckoutAfterProcessingWithError emitter.
// and then setting checkout to IDLE state.
dispatch.setHasError( true );
}
} else {
// nothing hooked in had any response type so let's just consider successful.
dispatch.setComplete();
}
};
/**
* Prepares the payment_result data from the server checkout endpoint response.
*/
export const getPaymentResultFromCheckoutResponse = (
response: CheckoutResponse
): PaymentResult => {
const paymentResult = {
message: '',
paymentStatus: 'not set',
redirectUrl: '',
paymentDetails: {},
} as PaymentResult;
// payment_result is present in successful responses.
if ( 'payment_result' in response ) {
paymentResult.paymentStatus = response.payment_result.payment_status;
paymentResult.redirectUrl = response.payment_result.redirect_url;
if (
response.payment_result.hasOwnProperty( 'payment_details' ) &&
Array.isArray( response.payment_result.payment_details )
) {
response.payment_result.payment_details.forEach(
( { key, value }: { key: string; value: string } ) => {
paymentResult.paymentDetails[ key ] =
decodeEntities( value );
}
);
}
}
// message is present in error responses.
if ( 'message' in response ) {
paymentResult.message = decodeEntities( response.message );
}
// If there was an error code but no message, set a default message.
if (
! paymentResult.message &&
'data' in response &&
'status' in response.data &&
response.data.status > 299
) {
paymentResult.message = __(
'Something went wrong. Please contact us to get assistance.',
'woo-gutenberg-products-block'
);
}
return paymentResult;
};