464 lines
13 KiB
JavaScript
464 lines
13 KiB
JavaScript
/**
|
|
* External dependencies
|
|
*/
|
|
import {
|
|
createContext,
|
|
useContext,
|
|
useState,
|
|
useReducer,
|
|
useCallback,
|
|
useEffect,
|
|
useRef,
|
|
useMemo,
|
|
} from '@wordpress/element';
|
|
import { getSetting } from '@woocommerce/settings';
|
|
import { useStoreNotices, useEmitResponse } from '@woocommerce/base-hooks';
|
|
import { useEditorContext } from '@woocommerce/base-context';
|
|
|
|
/**
|
|
* Internal dependencies
|
|
*/
|
|
import {
|
|
STATUS,
|
|
DEFAULT_PAYMENT_DATA,
|
|
DEFAULT_PAYMENT_METHOD_DATA,
|
|
} from './constants';
|
|
import reducer from './reducer';
|
|
import {
|
|
statusOnly,
|
|
error,
|
|
failed,
|
|
success,
|
|
setRegisteredPaymentMethods,
|
|
setRegisteredExpressPaymentMethods,
|
|
setShouldSavePaymentMethod,
|
|
} from './actions';
|
|
import {
|
|
usePaymentMethods,
|
|
useExpressPaymentMethods,
|
|
} from './use-payment-method-registration';
|
|
import { useCustomerDataContext } from '../customer';
|
|
import { useCheckoutContext } from '../checkout-state';
|
|
import { useShippingDataContext } from '../shipping';
|
|
import {
|
|
EMIT_TYPES,
|
|
emitterSubscribers,
|
|
emitEventWithAbort,
|
|
reducer as emitReducer,
|
|
} from './event-emit';
|
|
import { useValidationContext } from '../../shared/validation';
|
|
|
|
/**
|
|
* @typedef {import('@woocommerce/type-defs/contexts').PaymentMethodDataContext} PaymentMethodDataContext
|
|
* @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatch} PaymentStatusDispatch
|
|
* @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatchers} PaymentStatusDispatchers
|
|
* @typedef {import('@woocommerce/type-defs/billing').BillingData} BillingData
|
|
* @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod
|
|
* @typedef {import('@woocommerce/type-defs/contexts').ShippingDataResponse} ShippingDataResponse
|
|
*/
|
|
|
|
const {
|
|
STARTED,
|
|
PROCESSING,
|
|
COMPLETE,
|
|
PRISTINE,
|
|
ERROR,
|
|
FAILED,
|
|
SUCCESS,
|
|
} = STATUS;
|
|
|
|
const PaymentMethodDataContext = createContext( DEFAULT_PAYMENT_METHOD_DATA );
|
|
|
|
/**
|
|
* @return {PaymentMethodDataContext} The data and functions exposed by the
|
|
* payment method context provider.
|
|
*/
|
|
export const usePaymentMethodDataContext = () => {
|
|
return useContext( PaymentMethodDataContext );
|
|
};
|
|
|
|
/**
|
|
* Gets the payment methods saved for the current user after filtering out
|
|
* disabled ones.
|
|
*
|
|
* @param {Object} availablePaymentMethods List of available payment methods.
|
|
* @return {Object} Object containing the payment methods saved for a specific
|
|
* user which are available.
|
|
*/
|
|
const getCustomerPaymentMethods = ( availablePaymentMethods = {} ) => {
|
|
const customerPaymentMethods = getSetting( 'customerPaymentMethods', {} );
|
|
const paymentMethodKeys = Object.keys( customerPaymentMethods );
|
|
const enabledCustomerPaymentMethods = {};
|
|
paymentMethodKeys.forEach( ( type ) => {
|
|
const methods = customerPaymentMethods[ type ].filter(
|
|
( { method: { gateway } } ) => {
|
|
const isAvailable = gateway in availablePaymentMethods;
|
|
return (
|
|
isAvailable &&
|
|
availablePaymentMethods[ gateway ].supports?.savePaymentInfo
|
|
);
|
|
}
|
|
);
|
|
if ( methods.length ) {
|
|
enabledCustomerPaymentMethods[ type ] = methods;
|
|
}
|
|
} );
|
|
return enabledCustomerPaymentMethods;
|
|
};
|
|
|
|
/**
|
|
* 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 } ) => {
|
|
const { setBillingData } = useCustomerDataContext();
|
|
const {
|
|
isProcessing: checkoutIsProcessing,
|
|
isIdle: checkoutIsIdle,
|
|
isCalculating: checkoutIsCalculating,
|
|
hasError: checkoutHasError,
|
|
} = useCheckoutContext();
|
|
const {
|
|
isSuccessResponse,
|
|
isErrorResponse,
|
|
isFailResponse,
|
|
noticeContexts,
|
|
} = useEmitResponse();
|
|
const [ activePaymentMethod, setActive ] = useState( '' );
|
|
const [ observers, subscriber ] = useReducer( emitReducer, {} );
|
|
const currentObservers = useRef( observers );
|
|
|
|
const { isEditor, previewData } = useEditorContext();
|
|
const [ paymentData, dispatch ] = useReducer(
|
|
reducer,
|
|
DEFAULT_PAYMENT_DATA
|
|
);
|
|
const setActivePaymentMethod = useCallback(
|
|
( paymentMethodSlug ) => {
|
|
setActive( paymentMethodSlug );
|
|
dispatch( statusOnly( PRISTINE ) );
|
|
},
|
|
[ setActive, dispatch ]
|
|
);
|
|
const paymentMethodsDispatcher = useCallback(
|
|
( paymentMethods ) => {
|
|
dispatch( setRegisteredPaymentMethods( paymentMethods ) );
|
|
},
|
|
[ dispatch ]
|
|
);
|
|
const expressPaymentMethodsDispatcher = useCallback(
|
|
( paymentMethods ) => {
|
|
dispatch( setRegisteredExpressPaymentMethods( paymentMethods ) );
|
|
},
|
|
[ dispatch ]
|
|
);
|
|
const paymentMethodsInitialized = usePaymentMethods(
|
|
paymentMethodsDispatcher
|
|
);
|
|
const expressPaymentMethodsInitialized = useExpressPaymentMethods(
|
|
expressPaymentMethodsDispatcher
|
|
);
|
|
const { setValidationErrors } = useValidationContext();
|
|
const { addErrorNotice, removeNotice } = useStoreNotices();
|
|
const { setShippingAddress } = useShippingDataContext();
|
|
const setShouldSavePayment = useCallback(
|
|
( shouldSave ) => {
|
|
dispatch( setShouldSavePaymentMethod( shouldSave ) );
|
|
},
|
|
[ dispatch ]
|
|
);
|
|
|
|
const customerPaymentMethods = useMemo( () => {
|
|
if ( isEditor && previewData.previewSavedPaymentMethods ) {
|
|
return previewData.previewSavedPaymentMethods;
|
|
}
|
|
if (
|
|
! paymentMethodsInitialized ||
|
|
Object.keys( paymentData.paymentMethods ).length === 0
|
|
) {
|
|
return {};
|
|
}
|
|
return getCustomerPaymentMethods( paymentData.paymentMethods );
|
|
}, [
|
|
isEditor,
|
|
previewData.previewSavedPaymentMethods,
|
|
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 ]
|
|
);
|
|
// ensure observers are always current.
|
|
useEffect( () => {
|
|
currentObservers.current = observers;
|
|
}, [ observers ] );
|
|
const onPaymentProcessing = useMemo(
|
|
() => emitterSubscribers( subscriber ).onPaymentProcessing,
|
|
[ subscriber ]
|
|
);
|
|
|
|
const currentStatus = useMemo(
|
|
() => ( {
|
|
isPristine: paymentData.currentStatus === PRISTINE,
|
|
isStarted: paymentData.currentStatus === STARTED,
|
|
isProcessing: paymentData.currentStatus === PROCESSING,
|
|
isFinished: [ ERROR, FAILED, SUCCESS ].includes(
|
|
paymentData.currentStatus
|
|
),
|
|
hasError: paymentData.currentStatus === ERROR,
|
|
hasFailed: paymentData.currentStatus === FAILED,
|
|
isSuccessful: paymentData.currentStatus === SUCCESS,
|
|
} ),
|
|
[ paymentData.currentStatus ]
|
|
);
|
|
|
|
/**
|
|
* @type {PaymentStatusDispatch}
|
|
*/
|
|
const setPaymentStatus = useCallback(
|
|
() => ( {
|
|
started: () => dispatch( statusOnly( STARTED ) ),
|
|
processing: () => dispatch( statusOnly( PROCESSING ) ),
|
|
completed: () => dispatch( statusOnly( COMPLETE ) ),
|
|
/**
|
|
* @param {string} errorMessage An error message
|
|
*/
|
|
error: ( errorMessage ) => dispatch( error( errorMessage ) ),
|
|
/**
|
|
* @param {string} errorMessage An error message
|
|
* @param {Object} paymentMethodData Arbitrary payment method data to
|
|
* accompany the checkout submission.
|
|
* @param {BillingData|null} [billingData] The billing data accompanying the
|
|
* payment method.
|
|
*/
|
|
failed: ( errorMessage, paymentMethodData, billingData = null ) => {
|
|
if ( billingData ) {
|
|
setBillingData( billingData );
|
|
}
|
|
dispatch(
|
|
failed( {
|
|
errorMessage,
|
|
paymentMethodData,
|
|
} )
|
|
);
|
|
},
|
|
/**
|
|
* @param {Object} [paymentMethodData] Arbitrary payment method data to
|
|
* accompany the checkout.
|
|
* @param {BillingData|null} [billingData] The billing data accompanying the
|
|
* payment method.
|
|
* @param {ShippingDataResponse|null} [shippingData] The shipping data accompanying the
|
|
* payment method.
|
|
*/
|
|
success: (
|
|
paymentMethodData = {},
|
|
billingData = null,
|
|
shippingData = null
|
|
) => {
|
|
if ( billingData ) {
|
|
setBillingData( billingData );
|
|
}
|
|
if ( shippingData !== null && shippingData?.address ) {
|
|
setShippingAddress( shippingData.address );
|
|
}
|
|
dispatch(
|
|
success( {
|
|
paymentMethodData,
|
|
} )
|
|
);
|
|
},
|
|
} ),
|
|
[ dispatch, setBillingData, setShippingAddress ]
|
|
);
|
|
|
|
// 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 ) {
|
|
dispatch( statusOnly( PRISTINE ) );
|
|
}
|
|
}, [ checkoutIsIdle, currentStatus.isSuccessful ] );
|
|
|
|
// if checkout has an error and payment is not being made with a saved token
|
|
// and payment status is success, then let's sync payment status back to
|
|
// pristine.
|
|
useEffect( () => {
|
|
if (
|
|
checkoutHasError &&
|
|
currentStatus.isSuccessful &&
|
|
! paymentData.hasSavedToken
|
|
) {
|
|
dispatch( statusOnly( PRISTINE ) );
|
|
}
|
|
}, [
|
|
checkoutHasError,
|
|
currentStatus.isSuccessful,
|
|
paymentData.hasSavedToken,
|
|
] );
|
|
|
|
// Set active (selected) payment method as needed.
|
|
useEffect( () => {
|
|
const paymentMethodKeys = Object.keys( paymentData.paymentMethods );
|
|
const allPaymentMethodKeys = [
|
|
...paymentMethodKeys,
|
|
...Object.keys( paymentData.expressPaymentMethods ),
|
|
];
|
|
if ( ! paymentMethodsInitialized || ! paymentMethodKeys.length ) {
|
|
return;
|
|
}
|
|
|
|
setActive( ( currentActivePaymentMethod ) => {
|
|
// If there's no active payment method, or the active payment method has
|
|
// been removed (e.g. COD vs shipping methods), set one as active.
|
|
// Note: It's possible that the active payment method might be an
|
|
// express payment method. So registered express payment methods are
|
|
// included in the check here.
|
|
if (
|
|
! currentActivePaymentMethod ||
|
|
! allPaymentMethodKeys.includes( currentActivePaymentMethod )
|
|
) {
|
|
dispatch( statusOnly( PRISTINE ) );
|
|
return Object.keys( paymentData.paymentMethods )[ 0 ];
|
|
}
|
|
return currentActivePaymentMethod;
|
|
} );
|
|
}, [
|
|
paymentMethodsInitialized,
|
|
paymentData.paymentMethods,
|
|
paymentData.expressPaymentMethods,
|
|
setActive,
|
|
] );
|
|
|
|
// emit events.
|
|
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( ( response ) => {
|
|
if ( isSuccessResponse( response ) ) {
|
|
setPaymentStatus().success(
|
|
response?.meta?.paymentMethodData,
|
|
response?.meta?.billingData,
|
|
response?.meta?.shippingData
|
|
);
|
|
} else if ( isFailResponse( response ) ) {
|
|
if ( response.message && response.message.length ) {
|
|
addErrorNotice( response.message, {
|
|
id: 'wc-payment-error',
|
|
isDismissible: false,
|
|
context:
|
|
response?.messageContext ||
|
|
noticeContexts.PAYMENTS,
|
|
} );
|
|
}
|
|
setPaymentStatus().failed(
|
|
response?.message,
|
|
response?.meta?.paymentMethodData,
|
|
response?.meta?.billingData
|
|
);
|
|
} else if ( isErrorResponse( response ) ) {
|
|
if ( response.message && response.message.length ) {
|
|
addErrorNotice( response.message, {
|
|
id: 'wc-payment-error',
|
|
isDismissible: false,
|
|
context:
|
|
response?.messageContext ||
|
|
noticeContexts.PAYMENTS,
|
|
} );
|
|
}
|
|
setPaymentStatus().error( response.message );
|
|
setValidationErrors( response?.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,
|
|
] );
|
|
|
|
/**
|
|
* @type {PaymentMethodDataContext}
|
|
*/
|
|
const paymentContextData = {
|
|
setPaymentStatus,
|
|
currentStatus,
|
|
paymentStatuses: STATUS,
|
|
paymentMethodData: paymentData.paymentMethodData,
|
|
errorMessage: paymentData.errorMessage,
|
|
activePaymentMethod,
|
|
setActivePaymentMethod,
|
|
onPaymentProcessing,
|
|
customerPaymentMethods,
|
|
paymentMethods: paymentData.paymentMethods,
|
|
expressPaymentMethods: paymentData.expressPaymentMethods,
|
|
paymentMethodsInitialized,
|
|
expressPaymentMethodsInitialized,
|
|
setExpressPaymentError,
|
|
shouldSavePayment: paymentData.shouldSavePaymentMethod,
|
|
setShouldSavePayment,
|
|
};
|
|
return (
|
|
<PaymentMethodDataContext.Provider value={ paymentContextData }>
|
|
{ children }
|
|
</PaymentMethodDataContext.Provider>
|
|
);
|
|
};
|