225 lines
6.6 KiB
TypeScript
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;
|
|
};
|