/** * 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 { isErrorResponse, isFailResponse, isSuccessResponse, shouldRetry, } from '../../base/context/event-emit'; import { CheckoutAndPaymentNotices, CheckoutAfterProcessingWithErrorEventData, } from './types'; import { DispatchFromMap } from '../mapped-types'; import * as actions from './actions'; /** * 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.__internalSetComplete( errorResponse ); } else { dispatch.__internalSetIdle(); } } 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.__internalSetIdle(); } }; /** * 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.__internalSetComplete( 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.__internalSetComplete( errorResponse ); } else { // this will set an error which will end up // triggering the onCheckoutAfterProcessingWithError emitter. // and then setting checkout to IDLE state. dispatch.__internalSetHasError( true ); } } else { // nothing hooked in had any response type so let's just consider successful. dispatch.__internalSetComplete(); } }; /** * 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; };