File "payment-method-data-context.tsx"
Full Path: /home/warrior1/public_html/plugins/woocommerce/packages/woocommerce-blocks/assets/js/base/context/providers/cart-checkout/payment-methods/payment-method-data-context.tsx
File size: 10.56 KB
MIME-type: text/x-java
Charset: utf-8
/**
* External dependencies
*/
import {
createContext,
useContext,
useReducer,
useCallback,
useRef,
useEffect,
useMemo,
} from '@wordpress/element';
import { objectHasProp } from '@woocommerce/types';
import { useDispatch } from '@wordpress/data';
/**
* Internal dependencies
*/
import type {
CustomerPaymentMethods,
PaymentMethodDataContextType,
} from './types';
import {
STATUS,
DEFAULT_PAYMENT_DATA_CONTEXT_STATE,
DEFAULT_PAYMENT_METHOD_DATA,
} from './constants';
import reducer from './reducer';
import {
usePaymentMethods,
useExpressPaymentMethods,
} from './use-payment-method-registration';
import { usePaymentMethodDataDispatchers } from './use-payment-method-dispatchers';
import { useCheckoutContext } from '../checkout-state';
import { useEditorContext } from '../../editor-context';
import {
EMIT_TYPES,
useEventEmitters,
emitEventWithAbort,
reducer as emitReducer,
} from './event-emit';
import { useValidationContext } from '../../validation';
import { useEmitResponse } from '../../../hooks/use-emit-response';
import { getCustomerPaymentMethods } from './utils';
const PaymentMethodDataContext = createContext( DEFAULT_PAYMENT_METHOD_DATA );
export const usePaymentMethodDataContext = (): PaymentMethodDataContextType => {
return useContext( PaymentMethodDataContext );
};
/**
* PaymentMethodDataProvider is automatically included in the CheckoutDataProvider.
*
* This provides the api interface (via the context hook) for payment method status and data.
*
* @param {Object} props Incoming props for provider
* @param {Object} props.children The wrapped components in this provider.
*/
export const PaymentMethodDataProvider = ( {
children,
}: {
children: React.ReactNode;
} ): JSX.Element => {
const {
isProcessing: checkoutIsProcessing,
isIdle: checkoutIsIdle,
isCalculating: checkoutIsCalculating,
hasError: checkoutHasError,
} = useCheckoutContext();
const { isEditor, getPreviewData } = useEditorContext();
const { setValidationErrors } = useValidationContext();
const { createErrorNotice: addErrorNotice, removeNotice } =
useDispatch( 'core/notices' );
const {
isSuccessResponse,
isErrorResponse,
isFailResponse,
noticeContexts,
} = useEmitResponse();
const [ observers, observerDispatch ] = useReducer( emitReducer, {} );
const { onPaymentProcessing } = useEventEmitters( observerDispatch );
const currentObservers = useRef( observers );
// ensure observers are always current.
useEffect( () => {
currentObservers.current = observers;
}, [ observers ] );
const [ paymentData, dispatch ] = useReducer(
reducer,
DEFAULT_PAYMENT_DATA_CONTEXT_STATE
);
const { dispatchActions, setPaymentStatus } =
usePaymentMethodDataDispatchers( dispatch );
const paymentMethodsInitialized = usePaymentMethods(
dispatchActions.setRegisteredPaymentMethods
);
const expressPaymentMethodsInitialized = useExpressPaymentMethods(
dispatchActions.setRegisteredExpressPaymentMethods
);
const customerPaymentMethods = useMemo( (): CustomerPaymentMethods => {
if ( isEditor ) {
return getPreviewData(
'previewSavedPaymentMethods'
) as CustomerPaymentMethods;
}
return paymentMethodsInitialized
? getCustomerPaymentMethods( paymentData.paymentMethods )
: {};
}, [
isEditor,
getPreviewData,
paymentMethodsInitialized,
paymentData.paymentMethods,
] );
const setExpressPaymentError = useCallback(
( message ) => {
if ( message ) {
addErrorNotice( message, {
id: 'wc-express-payment-error',
context: noticeContexts.EXPRESS_PAYMENTS,
} );
} else {
removeNotice(
'wc-express-payment-error',
noticeContexts.EXPRESS_PAYMENTS
);
}
},
[ addErrorNotice, noticeContexts.EXPRESS_PAYMENTS, removeNotice ]
);
const isExpressPaymentMethodActive = Object.keys(
paymentData.expressPaymentMethods
).includes( paymentData.activePaymentMethod );
const currentStatus = useMemo(
() => ( {
isPristine: paymentData.currentStatus === STATUS.PRISTINE,
isStarted: paymentData.currentStatus === STATUS.STARTED,
isProcessing: paymentData.currentStatus === STATUS.PROCESSING,
isFinished: [
STATUS.ERROR,
STATUS.FAILED,
STATUS.SUCCESS,
].includes( paymentData.currentStatus ),
hasError: paymentData.currentStatus === STATUS.ERROR,
hasFailed: paymentData.currentStatus === STATUS.FAILED,
isSuccessful: paymentData.currentStatus === STATUS.SUCCESS,
isDoingExpressPayment:
paymentData.currentStatus !== STATUS.PRISTINE &&
isExpressPaymentMethodActive,
} ),
[ paymentData.currentStatus, isExpressPaymentMethodActive ]
);
/**
* Active Gateway Selection
*
* Updates the active (selected) payment method when it is empty, or invalid. This uses the first saved payment
* method found (if applicable), or the first standard gateway.
*/
useEffect( () => {
const paymentMethodKeys = Object.keys( paymentData.paymentMethods );
if ( ! paymentMethodsInitialized || ! paymentMethodKeys.length ) {
return;
}
const allPaymentMethodKeys = [
...paymentMethodKeys,
...Object.keys( paymentData.expressPaymentMethods ),
];
// Return if current method is valid.
if (
paymentData.activePaymentMethod &&
allPaymentMethodKeys.includes( paymentData.activePaymentMethod )
) {
return;
}
setPaymentStatus().pristine();
const customerPaymentMethod =
Object.keys( customerPaymentMethods ).flatMap(
( type ) => customerPaymentMethods[ type ]
)[ 0 ] || undefined;
if ( customerPaymentMethod ) {
const token = customerPaymentMethod.tokenId.toString();
const paymentMethodSlug = customerPaymentMethod.method.gateway;
const savedTokenKey = `wc-${ paymentMethodSlug }-payment-token`;
dispatchActions.setActivePaymentMethod( paymentMethodSlug, {
token,
payment_method: paymentMethodSlug,
[ savedTokenKey ]: token,
isSavedToken: true,
} );
return;
}
dispatchActions.setActivePaymentMethod(
Object.keys( paymentData.paymentMethods )[ 0 ]
);
}, [
paymentMethodsInitialized,
paymentData.paymentMethods,
paymentData.expressPaymentMethods,
dispatchActions,
setPaymentStatus,
paymentData.activePaymentMethod,
customerPaymentMethods,
] );
// flip payment to processing if checkout processing is complete, there are no errors, and payment status is started.
useEffect( () => {
if (
checkoutIsProcessing &&
! checkoutHasError &&
! checkoutIsCalculating &&
! currentStatus.isFinished
) {
setPaymentStatus().processing();
}
}, [
checkoutIsProcessing,
checkoutHasError,
checkoutIsCalculating,
currentStatus.isFinished,
setPaymentStatus,
] );
// When checkout is returned to idle, set payment status to pristine but only if payment status is already not finished.
useEffect( () => {
if ( checkoutIsIdle && ! currentStatus.isSuccessful ) {
setPaymentStatus().pristine();
}
}, [ checkoutIsIdle, currentStatus.isSuccessful, setPaymentStatus ] );
// if checkout has an error sync payment status back to pristine.
useEffect( () => {
if ( checkoutHasError && currentStatus.isSuccessful ) {
setPaymentStatus().pristine();
}
}, [ checkoutHasError, currentStatus.isSuccessful, setPaymentStatus ] );
useEffect( () => {
// Note: the nature of this event emitter is that it will bail on any
// observer that returns a response that !== true. However, this still
// allows for other observers that return true for continuing through
// to the next observer (or bailing if there's a problem).
if ( currentStatus.isProcessing ) {
removeNotice( 'wc-payment-error', noticeContexts.PAYMENTS );
emitEventWithAbort(
currentObservers.current,
EMIT_TYPES.PAYMENT_PROCESSING,
{}
).then( ( observerResponses ) => {
let successResponse, errorResponse;
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 ) {
setPaymentStatus().success(
successResponse?.meta?.paymentMethodData,
successResponse?.meta?.billingAddress,
successResponse?.meta?.shippingData
);
} else if ( errorResponse && isFailResponse( errorResponse ) ) {
if (
errorResponse.message &&
errorResponse.message.length
) {
addErrorNotice( errorResponse.message, {
id: 'wc-payment-error',
isDismissible: false,
context:
errorResponse?.messageContext ||
noticeContexts.PAYMENTS,
} );
}
setPaymentStatus().failed(
errorResponse?.message,
errorResponse?.meta?.paymentMethodData,
errorResponse?.meta?.billingAddress
);
} else if ( errorResponse ) {
if (
errorResponse.message &&
errorResponse.message.length
) {
addErrorNotice( errorResponse.message, {
id: 'wc-payment-error',
isDismissible: false,
context:
errorResponse?.messageContext ||
noticeContexts.PAYMENTS,
} );
}
setPaymentStatus().error( errorResponse.message );
setValidationErrors( errorResponse?.validationErrors );
} else {
// otherwise there are no payment methods doing anything so
// just consider success
setPaymentStatus().success();
}
} );
}
}, [
currentStatus.isProcessing,
setValidationErrors,
setPaymentStatus,
removeNotice,
noticeContexts.PAYMENTS,
isSuccessResponse,
isFailResponse,
isErrorResponse,
addErrorNotice,
] );
const activeSavedToken =
typeof paymentData.paymentMethodData === 'object' &&
objectHasProp( paymentData.paymentMethodData, 'token' )
? paymentData.paymentMethodData.token + ''
: '';
const paymentContextData: PaymentMethodDataContextType = {
setPaymentStatus,
currentStatus,
paymentStatuses: STATUS,
paymentMethodData: paymentData.paymentMethodData,
errorMessage: paymentData.errorMessage,
activePaymentMethod: paymentData.activePaymentMethod,
activeSavedToken,
setActivePaymentMethod: dispatchActions.setActivePaymentMethod,
onPaymentProcessing,
customerPaymentMethods,
paymentMethods: paymentData.paymentMethods,
expressPaymentMethods: paymentData.expressPaymentMethods,
paymentMethodsInitialized,
expressPaymentMethodsInitialized,
setExpressPaymentError,
isExpressPaymentMethodActive,
shouldSavePayment: paymentData.shouldSavePaymentMethod,
setShouldSavePayment: dispatchActions.setShouldSavePayment,
};
return (
<PaymentMethodDataContext.Provider value={ paymentContextData }>
{ children }
</PaymentMethodDataContext.Provider>
);
};