Refactor checkout status and event emitters to support stripe intents and more complex payment methods. (https://github.com/woocommerce/woocommerce-blocks/pull/2189)
* initial mapping out of stripe payment intents * rename checkout processing statuses to be clearer * Add new status and refactor checkout complete behaviour. * Make sure payment result data is included in checkout processing response * add payment intent handling Still testing * make sure promise is returned * include site url with endpoint * modify setComplete status to optionally receive redirectUrl for changing in state at the same time as setting status * fix typo in property retrieval * add error handling for after checkout processing event * add notices area for payment methods * implement error handling for stripe intents * hook into stripe error processing and include error in payment response * clear notices so they don’t show in block and merge payment details * add notice handling to payment context * modify error processing in checkout processor * handle errors with fallback in checkout state context * hook into after processing for stripe cc error handling * set checkout to idle status if before processing emitters result in error * Add emit response type-defs and normalize expectations for observer responses * improve doc block * switch checkoutIsComplete check to checkoutAfterProcessing for payment complete status change * remove unneeded event emitters and consolidate some logic * fix idle status set logic
This commit is contained in:
parent
6b63f19fcb
commit
5850966894
|
@ -5,8 +5,11 @@ import { TYPES } from './constants';
|
|||
|
||||
const {
|
||||
SET_PRISTINE,
|
||||
SET_IDLE,
|
||||
SET_PROCESSING,
|
||||
SET_PROCESSING_COMPLETE,
|
||||
SET_BEFORE_PROCESSING,
|
||||
SET_AFTER_PROCESSING,
|
||||
SET_PROCESSING_RESPONSE,
|
||||
SET_REDIRECT_URL,
|
||||
SET_COMPLETE,
|
||||
SET_HAS_ERROR,
|
||||
|
@ -23,6 +26,9 @@ export const actions = {
|
|||
setPristine: () => ( {
|
||||
type: SET_PRISTINE,
|
||||
} ),
|
||||
setIdle: () => ( {
|
||||
type: SET_IDLE,
|
||||
} ),
|
||||
setProcessing: () => ( {
|
||||
type: SET_PROCESSING,
|
||||
} ),
|
||||
|
@ -30,11 +36,19 @@ export const actions = {
|
|||
type: SET_REDIRECT_URL,
|
||||
url,
|
||||
} ),
|
||||
setComplete: () => ( {
|
||||
type: SET_COMPLETE,
|
||||
setProcessingResponse: ( data ) => ( {
|
||||
type: SET_PROCESSING_RESPONSE,
|
||||
data,
|
||||
} ),
|
||||
setProcessingComplete: () => ( {
|
||||
type: SET_PROCESSING_COMPLETE,
|
||||
setComplete: ( data ) => ( {
|
||||
type: SET_COMPLETE,
|
||||
data,
|
||||
} ),
|
||||
setBeforeProcessing: () => ( {
|
||||
type: SET_BEFORE_PROCESSING,
|
||||
} ),
|
||||
setAfterProcessing: () => ( {
|
||||
type: SET_AFTER_PROCESSING,
|
||||
} ),
|
||||
setHasError: ( hasError = true ) => {
|
||||
const type = hasError ? SET_HAS_ERROR : SET_NO_ERROR;
|
||||
|
|
|
@ -11,7 +11,8 @@ export const STATUS = {
|
|||
IDLE: 'idle',
|
||||
PROCESSING: 'processing',
|
||||
COMPLETE: 'complete',
|
||||
PROCESSING_COMPLETE: 'processing_complete',
|
||||
BEFORE_PROCESSING: 'before_processing',
|
||||
AFTER_PROCESSING: 'after_processing',
|
||||
};
|
||||
|
||||
const checkoutData = getSetting( 'checkoutData', {
|
||||
|
@ -26,13 +27,17 @@ export const DEFAULT_STATE = {
|
|||
calculatingCount: 0,
|
||||
orderId: checkoutData.order_id,
|
||||
customerId: checkoutData.customer_id,
|
||||
processingResponse: null,
|
||||
};
|
||||
|
||||
export const TYPES = {
|
||||
SET_IDLE: 'set_idle',
|
||||
SET_PRISTINE: 'set_pristine',
|
||||
SET_REDIRECT_URL: 'set_redirect_url',
|
||||
SET_COMPLETE: 'set_checkout_complete',
|
||||
SET_PROCESSING_COMPLETE: 'set_processing_complete',
|
||||
SET_BEFORE_PROCESSING: 'set_before_processing',
|
||||
SET_AFTER_PROCESSING: 'set_after_processing',
|
||||
SET_PROCESSING_RESPONSE: 'set_processing_response',
|
||||
SET_PROCESSING: 'set_checkout_is_processing',
|
||||
SET_HAS_ERROR: 'set_checkout_has_error',
|
||||
SET_NO_ERROR: 'set_checkout_no_error',
|
||||
|
|
|
@ -9,14 +9,16 @@ import {
|
|||
} from '../event-emit';
|
||||
|
||||
const EMIT_TYPES = {
|
||||
CHECKOUT_COMPLETE_WITH_SUCCESS: 'checkout_complete',
|
||||
CHECKOUT_COMPLETE_WITH_ERROR: 'checkout_complete_error',
|
||||
CHECKOUT_PROCESSING: 'checkout_processing',
|
||||
CHECKOUT_BEFORE_PROCESSING: 'checkout_before_processing',
|
||||
CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS:
|
||||
'checkout_after_processing_with_success',
|
||||
CHECKOUT_AFTER_PROCESSING_WITH_ERROR:
|
||||
'checkout_after_processing_with_error',
|
||||
};
|
||||
|
||||
/**
|
||||
* Receives a reducer dispatcher and returns an object with the
|
||||
* onCheckoutComplete callback registration function for the checkout emit
|
||||
* callback registration function for the checkout emit
|
||||
* events.
|
||||
*
|
||||
* Calling the event registration function with the callback will register it
|
||||
|
@ -25,19 +27,19 @@ const EMIT_TYPES = {
|
|||
*
|
||||
* @param {Function} dispatcher The emitter reducer dispatcher.
|
||||
*
|
||||
* @return {Object} An object with the `onCheckoutComplete` emmitter registration
|
||||
* @return {Object} An object with the checkout emmitter registration
|
||||
*/
|
||||
const emitterSubscribers = ( dispatcher ) => ( {
|
||||
onCheckoutCompleteSuccess: emitterCallback(
|
||||
EMIT_TYPES.CHECKOUT_COMPLETE_WITH_SUCCESS,
|
||||
onCheckoutAfterProcessingWithSuccess: emitterCallback(
|
||||
EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS,
|
||||
dispatcher
|
||||
),
|
||||
onCheckoutCompleteError: emitterCallback(
|
||||
EMIT_TYPES.CHECKOUT_COMPLETE_WITH_ERROR,
|
||||
onCheckoutAfterProcessingWithError: emitterCallback(
|
||||
EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_ERROR,
|
||||
dispatcher
|
||||
),
|
||||
onCheckoutProcessing: emitterCallback(
|
||||
EMIT_TYPES.CHECKOUT_PROCESSING,
|
||||
onCheckoutBeforeProcessing: emitterCallback(
|
||||
EMIT_TYPES.CHECKOUT_BEFORE_PROCESSING,
|
||||
dispatcher
|
||||
),
|
||||
} );
|
||||
|
|
|
@ -10,18 +10,19 @@ import {
|
|||
useEffect,
|
||||
} from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useStoreNotices } from '@woocommerce/base-hooks';
|
||||
import { useStoreNotices, useEmitResponse } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { actions } from './actions';
|
||||
import { reducer } from './reducer';
|
||||
import { reducer, prepareResponseData } from './reducer';
|
||||
import { DEFAULT_STATE, STATUS } from './constants';
|
||||
import {
|
||||
EMIT_TYPES,
|
||||
emitterSubscribers,
|
||||
emitEvent,
|
||||
emitEventWithAbort,
|
||||
reducer as emitReducer,
|
||||
} from './event-emit';
|
||||
import { useValidationContext } from '../validation';
|
||||
|
@ -38,18 +39,20 @@ const CheckoutContext = createContext( {
|
|||
isIdle: false,
|
||||
isCalculating: false,
|
||||
isProcessing: false,
|
||||
isProcessingComplete: false,
|
||||
isBeforeProcessing: false,
|
||||
isAfterProcessing: false,
|
||||
hasError: false,
|
||||
redirectUrl: '',
|
||||
orderId: 0,
|
||||
onCheckoutCompleteSuccess: ( callback ) => void callback,
|
||||
onCheckoutCompleteError: ( callback ) => void callback,
|
||||
onCheckoutProcessing: ( callback ) => void callback,
|
||||
customerId: 0,
|
||||
onCheckoutAfterProcessingWithSuccess: ( callback ) => void callback,
|
||||
onCheckoutAfterProcessingWithError: ( callback ) => void callback,
|
||||
onCheckoutBeforeProcessing: ( callback ) => void callback,
|
||||
dispatchActions: {
|
||||
resetCheckout: () => void null,
|
||||
setRedirectUrl: ( url ) => void url,
|
||||
setHasError: ( hasError ) => void hasError,
|
||||
setComplete: () => void null,
|
||||
setAfterProcessing: ( response ) => void response,
|
||||
incrementCalculating: () => void null,
|
||||
decrementCalculating: () => void null,
|
||||
setOrderId: ( id ) => void id,
|
||||
|
@ -94,23 +97,30 @@ export const CheckoutStateProvider = ( {
|
|||
const currentObservers = useRef( observers );
|
||||
const { setValidationErrors } = useValidationContext();
|
||||
const { addErrorNotice, removeNotices } = useStoreNotices();
|
||||
|
||||
const isCalculating = checkoutState.calculatingCount > 0;
|
||||
const {
|
||||
isSuccessResponse,
|
||||
isErrorResponse,
|
||||
isFailResponse,
|
||||
} = useEmitResponse();
|
||||
|
||||
// set observers on ref so it's always current.
|
||||
useEffect( () => {
|
||||
currentObservers.current = observers;
|
||||
}, [ observers ] );
|
||||
const onCheckoutCompleteSuccess = useMemo(
|
||||
() => emitterSubscribers( subscriber ).onCheckoutCompleteSuccess,
|
||||
const onCheckoutAfterProcessingWithSuccess = useMemo(
|
||||
() =>
|
||||
emitterSubscribers( subscriber )
|
||||
.onCheckoutAfterProcessingWithSuccess,
|
||||
[ subscriber ]
|
||||
);
|
||||
const onCheckoutCompleteError = useMemo(
|
||||
() => emitterSubscribers( subscriber ).onCheckoutCompleteError,
|
||||
const onCheckoutAfterProcessingWithError = useMemo(
|
||||
() =>
|
||||
emitterSubscribers( subscriber ).onCheckoutAfterProcessingWithError,
|
||||
[ subscriber ]
|
||||
);
|
||||
const onCheckoutProcessing = useMemo(
|
||||
() => emitterSubscribers( subscriber ).onCheckoutProcessing,
|
||||
const onCheckoutBeforeProcessing = useMemo(
|
||||
() => emitterSubscribers( subscriber ).onCheckoutBeforeProcessing,
|
||||
[ subscriber ]
|
||||
);
|
||||
|
||||
|
@ -130,8 +140,25 @@ export const CheckoutStateProvider = ( {
|
|||
void dispatch( actions.decrementCalculating() ),
|
||||
setOrderId: ( orderId ) =>
|
||||
void dispatch( actions.setOrderId( orderId ) ),
|
||||
setComplete: () => {
|
||||
void dispatch( actions.setComplete() );
|
||||
setAfterProcessing: ( response ) => {
|
||||
if ( response.payment_result ) {
|
||||
if (
|
||||
// eslint-disable-next-line camelcase
|
||||
response.payment_result?.redirect_url
|
||||
) {
|
||||
dispatch(
|
||||
actions.setRedirectUrl(
|
||||
response.payment_result.redirect_url
|
||||
)
|
||||
);
|
||||
}
|
||||
dispatch(
|
||||
actions.setProcessingResponse(
|
||||
prepareResponseData( response.payment_result )
|
||||
)
|
||||
);
|
||||
}
|
||||
void dispatch( actions.setAfterProcessing() );
|
||||
},
|
||||
} ),
|
||||
[]
|
||||
|
@ -140,11 +167,11 @@ export const CheckoutStateProvider = ( {
|
|||
// emit events.
|
||||
useEffect( () => {
|
||||
const { status } = checkoutState;
|
||||
if ( status === STATUS.PROCESSING ) {
|
||||
if ( status === STATUS.BEFORE_PROCESSING ) {
|
||||
removeNotices( 'error' );
|
||||
emitEvent(
|
||||
currentObservers.current,
|
||||
EMIT_TYPES.CHECKOUT_PROCESSING,
|
||||
EMIT_TYPES.CHECKOUT_BEFORE_PROCESSING,
|
||||
{}
|
||||
).then( ( response ) => {
|
||||
if ( response !== true ) {
|
||||
|
@ -156,34 +183,109 @@ export const CheckoutStateProvider = ( {
|
|||
}
|
||||
);
|
||||
}
|
||||
dispatch( actions.setComplete() );
|
||||
dispatch( actions.setIdle() );
|
||||
} else {
|
||||
dispatch( actions.setProcessingComplete() );
|
||||
dispatch( actions.setProcessing() );
|
||||
}
|
||||
} );
|
||||
}
|
||||
}, [ checkoutState.status, setValidationErrors ] );
|
||||
|
||||
useEffect( () => {
|
||||
if ( checkoutState.status === STATUS.COMPLETE ) {
|
||||
if ( checkoutState.status === STATUS.AFTER_PROCESSING ) {
|
||||
const data = {
|
||||
redirectUrl: checkoutState.redirectUrl,
|
||||
orderId: checkoutState.orderId,
|
||||
customerId: checkoutState.customerId,
|
||||
customerNote: checkoutState.customerNote,
|
||||
processingResponse: checkoutState.processingResponse,
|
||||
};
|
||||
if ( checkoutState.hasError ) {
|
||||
emitEvent(
|
||||
// allow payment methods or other things to customize the error
|
||||
// with a fallback if nothing customizes it.
|
||||
emitEventWithAbort(
|
||||
currentObservers.current,
|
||||
EMIT_TYPES.CHECKOUT_COMPLETE_WITH_ERROR,
|
||||
{}
|
||||
);
|
||||
EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_ERROR,
|
||||
data
|
||||
).then( ( response ) => {
|
||||
if (
|
||||
isErrorResponse( response ) ||
|
||||
isFailResponse( response )
|
||||
) {
|
||||
if ( response.message ) {
|
||||
const errorOptions = response.messageContext
|
||||
? { context: response.messageContext }
|
||||
: undefined;
|
||||
addErrorNotice( response.message, errorOptions );
|
||||
}
|
||||
// irrecoverable error so set complete
|
||||
if (
|
||||
typeof response.retry !== 'undefined' &&
|
||||
response.retry !== true
|
||||
) {
|
||||
dispatch( actions.setComplete( response ) );
|
||||
} else {
|
||||
dispatch( actions.setIdle() );
|
||||
}
|
||||
} else {
|
||||
// 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'
|
||||
);
|
||||
addErrorNotice( message, {
|
||||
id: 'checkout',
|
||||
} );
|
||||
dispatch( actions.setIdle() );
|
||||
}
|
||||
} );
|
||||
} else {
|
||||
emitEvent(
|
||||
emitEventWithAbort(
|
||||
currentObservers.current,
|
||||
EMIT_TYPES.CHECKOUT_COMPLETE_WITH_SUCCESS,
|
||||
{}
|
||||
);
|
||||
EMIT_TYPES.CHECKOUT_AFTER_PROCESSING_WITH_SUCCESS,
|
||||
data
|
||||
).then( ( response ) => {
|
||||
if ( isSuccessResponse( response ) ) {
|
||||
dispatch( actions.setComplete( response ) );
|
||||
}
|
||||
if (
|
||||
isErrorResponse( response ) ||
|
||||
isFailResponse( response )
|
||||
) {
|
||||
if ( response.message ) {
|
||||
const errorOptions = response.messageContext
|
||||
? { context: response.messageContext }
|
||||
: undefined;
|
||||
addErrorNotice( response.message, errorOptions );
|
||||
}
|
||||
if ( ! response.retry ) {
|
||||
dispatch( actions.setComplete( response ) );
|
||||
} else {
|
||||
// this will set an error which will end up
|
||||
// triggering the onCheckoutAfterProcessingWithErrors emitter.
|
||||
// and then setting checkout to IDLE state.
|
||||
dispatch( actions.setHasError( true ) );
|
||||
}
|
||||
}
|
||||
} );
|
||||
}
|
||||
}
|
||||
}, [ checkoutState.status, checkoutState.hasError ] );
|
||||
}, [
|
||||
checkoutState.status,
|
||||
checkoutState.hasError,
|
||||
checkoutState.redirectUrl,
|
||||
checkoutState.orderId,
|
||||
checkoutState.customerId,
|
||||
checkoutState.customerNote,
|
||||
checkoutState.processingResponse,
|
||||
dispatchActions,
|
||||
] );
|
||||
|
||||
const onSubmit = () => {
|
||||
dispatch( actions.setProcessing() );
|
||||
dispatch( actions.setBeforeProcessing() );
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -196,13 +298,13 @@ export const CheckoutStateProvider = ( {
|
|||
isIdle: checkoutState.status === STATUS.IDLE,
|
||||
isCalculating,
|
||||
isProcessing: checkoutState.status === STATUS.PROCESSING,
|
||||
isProcessingComplete:
|
||||
checkoutState.status === STATUS.PROCESSING_COMPLETE,
|
||||
isBeforeProcessing: checkoutState.status === STATUS.BEFORE_PROCESSING,
|
||||
isAfterProcessing: checkoutState.status === STATUS.AFTER_PROCESSING,
|
||||
hasError: checkoutState.hasError,
|
||||
redirectUrl: checkoutState.redirectUrl,
|
||||
onCheckoutCompleteSuccess,
|
||||
onCheckoutCompleteError,
|
||||
onCheckoutProcessing,
|
||||
onCheckoutAfterProcessingWithSuccess,
|
||||
onCheckoutAfterProcessingWithError,
|
||||
onCheckoutBeforeProcessing,
|
||||
dispatchActions,
|
||||
isCart,
|
||||
orderId: checkoutState.orderId,
|
||||
|
|
|
@ -5,8 +5,11 @@ import { TYPES, DEFAULT_STATE, STATUS } from './constants';
|
|||
|
||||
const {
|
||||
SET_PRISTINE,
|
||||
SET_IDLE,
|
||||
SET_PROCESSING,
|
||||
SET_PROCESSING_COMPLETE,
|
||||
SET_BEFORE_PROCESSING,
|
||||
SET_AFTER_PROCESSING,
|
||||
SET_PROCESSING_RESPONSE,
|
||||
SET_REDIRECT_URL,
|
||||
SET_COMPLETE,
|
||||
SET_HAS_ERROR,
|
||||
|
@ -16,7 +19,41 @@ const {
|
|||
SET_ORDER_ID,
|
||||
} = TYPES;
|
||||
|
||||
const { PRISTINE, IDLE, PROCESSING, PROCESSING_COMPLETE, COMPLETE } = STATUS;
|
||||
const {
|
||||
PRISTINE,
|
||||
IDLE,
|
||||
PROCESSING,
|
||||
BEFORE_PROCESSING,
|
||||
AFTER_PROCESSING,
|
||||
COMPLETE,
|
||||
} = STATUS;
|
||||
|
||||
/**
|
||||
* Prepares the payment_result data from the server checkout endpoint response.
|
||||
*
|
||||
* @param {Object} data The value of `payment_result` from the checkout
|
||||
* processing endpoint response.
|
||||
* @param {string} data.payment_status The payment status. One of 'success', 'failure',
|
||||
* 'pending', 'error'.
|
||||
* @param {Array<Object>} data.payment_details An array of Objects with a 'key' property that is a
|
||||
* string and value property that is a string. These are
|
||||
* converted to a flat object where the key becomes the
|
||||
* object property and value the property value.
|
||||
*
|
||||
* @return {Object} A new object with 'paymentStatus', and 'paymentDetails' as the properties.
|
||||
*/
|
||||
export const prepareResponseData = ( data ) => {
|
||||
const responseData = {
|
||||
paymentStatus: data.payment_status,
|
||||
paymentDetails: {},
|
||||
};
|
||||
if ( Array.isArray( data.payment_details ) ) {
|
||||
data.payment_details.forEach( ( { key, value } ) => {
|
||||
responseData.paymentDetails[ key ] = value;
|
||||
} );
|
||||
}
|
||||
return responseData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer for the checkout state
|
||||
|
@ -24,12 +61,24 @@ const { PRISTINE, IDLE, PROCESSING, PROCESSING_COMPLETE, COMPLETE } = STATUS;
|
|||
* @param {Object} state Current state.
|
||||
* @param {Object} action Incoming action object.
|
||||
*/
|
||||
export const reducer = ( state = DEFAULT_STATE, { url, type, orderId } ) => {
|
||||
export const reducer = (
|
||||
state = DEFAULT_STATE,
|
||||
{ url, type, orderId, data }
|
||||
) => {
|
||||
let newState;
|
||||
switch ( type ) {
|
||||
case SET_PRISTINE:
|
||||
newState = DEFAULT_STATE;
|
||||
break;
|
||||
case SET_IDLE:
|
||||
newState =
|
||||
state.state !== IDLE
|
||||
? {
|
||||
...state,
|
||||
status: IDLE,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case SET_REDIRECT_URL:
|
||||
newState =
|
||||
url !== state.url
|
||||
|
@ -39,12 +88,20 @@ export const reducer = ( state = DEFAULT_STATE, { url, type, orderId } ) => {
|
|||
}
|
||||
: state;
|
||||
break;
|
||||
case SET_PROCESSING_RESPONSE:
|
||||
newState = {
|
||||
...state,
|
||||
processingResponse: data,
|
||||
};
|
||||
break;
|
||||
|
||||
case SET_COMPLETE:
|
||||
newState =
|
||||
state.status !== COMPLETE
|
||||
? {
|
||||
...state,
|
||||
status: COMPLETE,
|
||||
redirectUrl: data.redirectUrl || state.redirectUrl,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
|
@ -63,16 +120,25 @@ export const reducer = ( state = DEFAULT_STATE, { url, type, orderId } ) => {
|
|||
? newState
|
||||
: { ...newState, hasError: false };
|
||||
break;
|
||||
case SET_PROCESSING_COMPLETE:
|
||||
case SET_BEFORE_PROCESSING:
|
||||
newState =
|
||||
state.status !== SET_PROCESSING_COMPLETE
|
||||
state.status !== BEFORE_PROCESSING
|
||||
? {
|
||||
...state,
|
||||
status: PROCESSING_COMPLETE,
|
||||
status: BEFORE_PROCESSING,
|
||||
hasError: false,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case SET_AFTER_PROCESSING:
|
||||
newState =
|
||||
state.status !== AFTER_PROCESSING
|
||||
? {
|
||||
...state,
|
||||
status: AFTER_PROCESSING,
|
||||
}
|
||||
: state;
|
||||
break;
|
||||
case SET_HAS_ERROR:
|
||||
newState = state.hasError
|
||||
? state
|
||||
|
@ -82,7 +148,7 @@ export const reducer = ( state = DEFAULT_STATE, { url, type, orderId } ) => {
|
|||
};
|
||||
newState =
|
||||
state.status === PROCESSING ||
|
||||
state.status === PROCESSING_COMPLETE
|
||||
state.status === BEFORE_PROCESSING
|
||||
? {
|
||||
...newState,
|
||||
status: IDLE,
|
||||
|
|
|
@ -47,12 +47,12 @@ const preparePaymentData = ( paymentData ) => {
|
|||
const CheckoutProcessor = () => {
|
||||
const {
|
||||
hasError: checkoutHasError,
|
||||
onCheckoutProcessing,
|
||||
onCheckoutCompleteSuccess,
|
||||
onCheckoutBeforeProcessing,
|
||||
dispatchActions,
|
||||
redirectUrl,
|
||||
isProcessing: checkoutIsProcessing,
|
||||
isProcessingComplete: checkoutIsProcessingComplete,
|
||||
isBeforeProcessing: checkoutIsBeforeProcessing,
|
||||
isComplete: checkoutIsComplete,
|
||||
} = useCheckoutContext();
|
||||
const { hasValidationErrors } = useValidationContext();
|
||||
const { shippingAddress, shippingErrorStatus } = useShippingDataContext();
|
||||
|
@ -69,6 +69,7 @@ const CheckoutProcessor = () => {
|
|||
const { addErrorNotice, removeNotice } = useStoreNotices();
|
||||
const currentBillingData = useRef( billingData );
|
||||
const currentShippingAddress = useRef( shippingAddress );
|
||||
const currentRedirectUrl = useRef( redirectUrl );
|
||||
const [ isProcessingOrder, setIsProcessingOrder ] = useState( false );
|
||||
const expressPaymentMethodActive = Object.keys(
|
||||
expressPaymentMethods
|
||||
|
@ -87,7 +88,7 @@ const CheckoutProcessor = () => {
|
|||
useEffect( () => {
|
||||
if (
|
||||
checkoutWillHaveError !== checkoutHasError &&
|
||||
( checkoutIsProcessing || checkoutIsProcessingComplete ) &&
|
||||
( checkoutIsProcessing || checkoutIsBeforeProcessing ) &&
|
||||
! expressPaymentMethodActive
|
||||
) {
|
||||
dispatchActions.setHasError( checkoutWillHaveError );
|
||||
|
@ -96,7 +97,7 @@ const CheckoutProcessor = () => {
|
|||
checkoutWillHaveError,
|
||||
checkoutHasError,
|
||||
checkoutIsProcessing,
|
||||
checkoutIsProcessingComplete,
|
||||
checkoutIsBeforeProcessing,
|
||||
expressPaymentMethodActive,
|
||||
] );
|
||||
|
||||
|
@ -104,12 +105,13 @@ const CheckoutProcessor = () => {
|
|||
! checkoutHasError &&
|
||||
! checkoutWillHaveError &&
|
||||
( currentPaymentStatus.isSuccessful || ! cartNeedsPayment ) &&
|
||||
checkoutIsProcessingComplete;
|
||||
checkoutIsProcessing;
|
||||
|
||||
useEffect( () => {
|
||||
currentBillingData.current = billingData;
|
||||
currentShippingAddress.current = shippingAddress;
|
||||
}, [ billingData, shippingAddress ] );
|
||||
currentRedirectUrl.current = redirectUrl;
|
||||
}, [ billingData, shippingAddress, redirectUrl ] );
|
||||
|
||||
useEffect( () => {
|
||||
if ( errorMessage ) {
|
||||
|
@ -157,14 +159,21 @@ const CheckoutProcessor = () => {
|
|||
useEffect( () => {
|
||||
let unsubscribeProcessing;
|
||||
if ( ! expressPaymentMethodActive ) {
|
||||
unsubscribeProcessing = onCheckoutProcessing( checkValidation, 0 );
|
||||
unsubscribeProcessing = onCheckoutBeforeProcessing(
|
||||
checkValidation,
|
||||
0
|
||||
);
|
||||
}
|
||||
return () => {
|
||||
if ( ! expressPaymentMethodActive ) {
|
||||
unsubscribeProcessing();
|
||||
}
|
||||
};
|
||||
}, [ onCheckoutProcessing, checkValidation, expressPaymentMethodActive ] );
|
||||
}, [
|
||||
onCheckoutBeforeProcessing,
|
||||
checkValidation,
|
||||
expressPaymentMethodActive,
|
||||
] );
|
||||
|
||||
const processOrder = useCallback( () => {
|
||||
setIsProcessingOrder( true );
|
||||
|
@ -212,29 +221,17 @@ const CheckoutProcessor = () => {
|
|||
);
|
||||
}
|
||||
dispatchActions.setHasError();
|
||||
} else {
|
||||
dispatchActions.setRedirectUrl(
|
||||
response.payment_result.redirect_url
|
||||
);
|
||||
}
|
||||
|
||||
dispatchActions.setComplete();
|
||||
dispatchActions.setAfterProcessing( response );
|
||||
setIsProcessingOrder( false );
|
||||
} );
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
const message =
|
||||
error.message ||
|
||||
__(
|
||||
'Something went wrong. Please contact us to get assistance.',
|
||||
'woo-gutenberg-products-block'
|
||||
);
|
||||
addErrorNotice( message, {
|
||||
id: 'checkout',
|
||||
error.json().then( function( response ) {
|
||||
dispatchActions.setHasError();
|
||||
dispatchActions.setAfterProcessing( response );
|
||||
setIsProcessingOrder( false );
|
||||
} );
|
||||
dispatchActions.setHasError();
|
||||
dispatchActions.setComplete();
|
||||
setIsProcessingOrder( false );
|
||||
} );
|
||||
}, [
|
||||
addErrorNotice,
|
||||
|
@ -243,15 +240,12 @@ const CheckoutProcessor = () => {
|
|||
paymentMethodData,
|
||||
cartNeedsPayment,
|
||||
] );
|
||||
// setup checkout processing event observers.
|
||||
// redirect when checkout is complete and there is a redirect url.
|
||||
useEffect( () => {
|
||||
const unsubscribeRedirect = onCheckoutCompleteSuccess( () => {
|
||||
window.location.href = redirectUrl;
|
||||
}, 999 );
|
||||
return () => {
|
||||
unsubscribeRedirect();
|
||||
};
|
||||
}, [ onCheckoutCompleteSuccess, redirectUrl ] );
|
||||
if ( currentRedirectUrl.current ) {
|
||||
window.location.href = currentRedirectUrl.current;
|
||||
}
|
||||
}, [ checkoutIsComplete ] );
|
||||
|
||||
// process order if conditions are good.
|
||||
useEffect( () => {
|
||||
|
|
|
@ -10,9 +10,6 @@ import {
|
|||
|
||||
const EMIT_TYPES = {
|
||||
PAYMENT_PROCESSING: 'payment_processing',
|
||||
PAYMENT_SUCCESS: 'payment_success',
|
||||
PAYMENT_FAIL: 'payment_fail',
|
||||
PAYMENT_ERROR: 'payment_error',
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -33,9 +30,6 @@ const emitterSubscribers = ( dispatcher ) => ( {
|
|||
EMIT_TYPES.PAYMENT_PROCESSING,
|
||||
dispatcher
|
||||
),
|
||||
onPaymentSuccess: emitterCallback( EMIT_TYPES.PAYMENT_SUCCESS, dispatcher ),
|
||||
onPaymentFail: emitterCallback( EMIT_TYPES.PAYMENT_FAIL, dispatcher ),
|
||||
onPaymentError: emitterCallback( EMIT_TYPES.PAYMENT_ERROR, dispatcher ),
|
||||
} );
|
||||
|
||||
export {
|
||||
|
|
|
@ -25,7 +25,6 @@ import { useShippingDataContext } from '../shipping';
|
|||
import {
|
||||
EMIT_TYPES,
|
||||
emitterSubscribers,
|
||||
emitEvent,
|
||||
emitEventWithAbort,
|
||||
reducer as emitReducer,
|
||||
} from './event-emit';
|
||||
|
@ -45,7 +44,7 @@ import {
|
|||
useMemo,
|
||||
} from '@wordpress/element';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
import { useStoreNotices } from '@woocommerce/base-hooks';
|
||||
import { useStoreNotices, useEmitResponse } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').PaymentMethodDataContext} PaymentMethodDataContext
|
||||
|
@ -76,23 +75,6 @@ export const usePaymentMethodDataContext = () => {
|
|||
return useContext( PaymentMethodDataContext );
|
||||
};
|
||||
|
||||
const isSuccessResponse = ( response ) => {
|
||||
return (
|
||||
( typeof response === 'object' &&
|
||||
typeof response.fail === 'undefined' &&
|
||||
typeof response.errorMessage === 'undefined' ) ||
|
||||
response === true
|
||||
);
|
||||
};
|
||||
|
||||
const isFailResponse = ( response ) => {
|
||||
return response && typeof response.fail === 'object';
|
||||
};
|
||||
|
||||
const isErrorResponse = ( response ) => {
|
||||
return response && typeof response.errorMessage !== 'undefined';
|
||||
};
|
||||
|
||||
/**
|
||||
* PaymentMethodDataProvider is automatically included in the
|
||||
* CheckoutDataProvider.
|
||||
|
@ -112,11 +94,16 @@ export const PaymentMethodDataProvider = ( {
|
|||
} ) => {
|
||||
const { setBillingData } = useBillingDataContext();
|
||||
const {
|
||||
isComplete: checkoutIsComplete,
|
||||
isProcessingComplete: checkoutIsProcessingComplete,
|
||||
isProcessing: checkoutIsProcessing,
|
||||
isIdle: checkoutIsIdle,
|
||||
isCalculating: checkoutIsCalculating,
|
||||
hasError: checkoutHasError,
|
||||
} = useCheckoutContext();
|
||||
const {
|
||||
isSuccessResponse,
|
||||
isErrorResponse,
|
||||
isFailResponse,
|
||||
} = useEmitResponse();
|
||||
const [ activePaymentMethod, setActive ] = useState(
|
||||
initialActivePaymentMethod
|
||||
);
|
||||
|
@ -164,18 +151,6 @@ export const PaymentMethodDataProvider = ( {
|
|||
() => emitterSubscribers( subscriber ).onPaymentProcessing,
|
||||
[ subscriber ]
|
||||
);
|
||||
const onPaymentSuccess = useMemo(
|
||||
() => emitterSubscribers( subscriber ).onPaymentSuccess,
|
||||
[ subscriber ]
|
||||
);
|
||||
const onPaymentFail = useMemo(
|
||||
() => emitterSubscribers( subscriber ).onPaymentFail,
|
||||
[ subscriber ]
|
||||
);
|
||||
const onPaymentError = useMemo(
|
||||
() => emitterSubscribers( subscriber ).onPaymentError,
|
||||
[ subscriber ]
|
||||
);
|
||||
|
||||
const currentStatus = useMemo(
|
||||
() => ( {
|
||||
|
@ -196,7 +171,7 @@ export const PaymentMethodDataProvider = ( {
|
|||
// no errors, and payment status is started.
|
||||
useEffect( () => {
|
||||
if (
|
||||
checkoutIsProcessingComplete &&
|
||||
checkoutIsProcessing &&
|
||||
! checkoutHasError &&
|
||||
! checkoutIsCalculating &&
|
||||
! currentStatus.isFinished
|
||||
|
@ -204,12 +179,17 @@ export const PaymentMethodDataProvider = ( {
|
|||
setPaymentStatus().processing();
|
||||
}
|
||||
}, [
|
||||
checkoutIsProcessingComplete,
|
||||
checkoutIsProcessing,
|
||||
checkoutHasError,
|
||||
checkoutIsCalculating,
|
||||
currentStatus.isFinished,
|
||||
] );
|
||||
|
||||
// when checkout is returned to idle, set payment status to pristine.
|
||||
useEffect( () => {
|
||||
dispatch( statusOnly( PRISTINE ) );
|
||||
}, [ checkoutIsIdle ] );
|
||||
|
||||
// set initial active payment method if it's undefined.
|
||||
useEffect( () => {
|
||||
const paymentMethodKeys = Object.keys( paymentData.paymentMethods );
|
||||
|
@ -289,9 +269,8 @@ export const PaymentMethodDataProvider = ( {
|
|||
|
||||
// emit events.
|
||||
useEffect( () => {
|
||||
// Note: the nature of this event emitter is that it will bail on a
|
||||
// successful payment (that is an observer hooked in that returns an
|
||||
// object in the shape of a successful payment). However, this still
|
||||
// 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 ) {
|
||||
|
@ -302,48 +281,29 @@ export const PaymentMethodDataProvider = ( {
|
|||
).then( ( response ) => {
|
||||
if ( isSuccessResponse( response ) ) {
|
||||
setPaymentStatus().success(
|
||||
response.paymentMethodData,
|
||||
response.billingData,
|
||||
response.shippingData
|
||||
response?.meta?.paymentMethodData,
|
||||
response?.meta?.billingData,
|
||||
response?.meta?.shippingData
|
||||
);
|
||||
} else if ( isFailResponse( response ) ) {
|
||||
addErrorNotice( response.message, {
|
||||
context: 'wc/payment-area',
|
||||
} );
|
||||
setPaymentStatus().failed(
|
||||
response.fail.errorMessage,
|
||||
response.fail.paymentMethodData,
|
||||
response.fail.billingData
|
||||
response.message,
|
||||
response?.meta?.paymentMethodData,
|
||||
response?.meta?.billingData
|
||||
);
|
||||
} else if ( isErrorResponse( response ) ) {
|
||||
setPaymentStatus().error( response.errorMessage );
|
||||
setValidationErrors( response.validationErrors );
|
||||
addErrorNotice( response.message, {
|
||||
context: 'wc/payment-area',
|
||||
} );
|
||||
setPaymentStatus().error( response.message );
|
||||
setValidationErrors( response?.validationErrors );
|
||||
}
|
||||
} );
|
||||
}
|
||||
if (
|
||||
currentStatus.isSuccessful &&
|
||||
checkoutIsComplete &&
|
||||
! checkoutHasError
|
||||
) {
|
||||
emitEvent(
|
||||
currentObservers.current,
|
||||
EMIT_TYPES.PAYMENT_SUCCESS,
|
||||
{}
|
||||
).then( () => {
|
||||
setPaymentStatus().completed();
|
||||
} );
|
||||
}
|
||||
if ( currentStatus.hasFailed ) {
|
||||
emitEvent( currentObservers.current, EMIT_TYPES.PAYMENT_FAIL, {} );
|
||||
}
|
||||
if ( currentStatus.hasError ) {
|
||||
emitEvent( currentObservers.current, EMIT_TYPES.PAYMENT_ERROR, {} );
|
||||
}
|
||||
}, [
|
||||
currentStatus,
|
||||
setValidationErrors,
|
||||
setPaymentStatus,
|
||||
checkoutIsComplete,
|
||||
checkoutHasError,
|
||||
] );
|
||||
}, [ currentStatus, setValidationErrors, setPaymentStatus ] );
|
||||
|
||||
/**
|
||||
* @type {PaymentMethodDataContext}
|
||||
|
@ -357,9 +317,6 @@ export const PaymentMethodDataProvider = ( {
|
|||
activePaymentMethod,
|
||||
setActivePaymentMethod,
|
||||
onPaymentProcessing,
|
||||
onPaymentSuccess,
|
||||
onPaymentFail,
|
||||
onPaymentError,
|
||||
customerPaymentMethods,
|
||||
paymentMethods: paymentData.paymentMethods,
|
||||
expressPaymentMethods: paymentData.expressPaymentMethods,
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from './use-checkout-redirect-url';
|
||||
export * from './use-checkout-submit';
|
||||
export * from './use-emit-response';
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/hooks').EmitResponseTypes} EmitResponseTypes
|
||||
* @typedef {import('@woocommerce/type-defs/hooks').NoticeContexts} NoticeContexts
|
||||
* @typedef {import('@woocommerce/type-defs/hooks').EmitResponseApi} EmitResponseApi
|
||||
*/
|
||||
|
||||
const isResponseOf = ( response, type ) => {
|
||||
return !! response.type && response.type === type;
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {EmitResponseTypes}
|
||||
*/
|
||||
const responseTypes = {
|
||||
SUCCESS: 'success',
|
||||
FAIL: 'failure',
|
||||
ERROR: 'error',
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {NoticeContexts}
|
||||
*/
|
||||
const noticeContexts = {
|
||||
PAYMENTS: 'wc/payment-area',
|
||||
EXPRESS_PAYMENTS: 'wc/express-payment-area',
|
||||
};
|
||||
|
||||
const isSuccessResponse = ( response ) => {
|
||||
return isResponseOf( response, responseTypes.SUCCESS );
|
||||
};
|
||||
|
||||
const isErrorResponse = ( response ) => {
|
||||
return isResponseOf( response, responseTypes.ERROR );
|
||||
};
|
||||
|
||||
const isFailResponse = ( response ) => {
|
||||
return isResponseOf( response, responseTypes.FAIL );
|
||||
};
|
||||
|
||||
/**
|
||||
* A custom hook exposing response utilities for emitters.
|
||||
*
|
||||
* @return {EmitResponseApi} Various interfaces for validating and implementing
|
||||
* emitter response properties.
|
||||
*/
|
||||
export const useEmitResponse = () => {
|
||||
return {
|
||||
responseTypes,
|
||||
noticeContexts,
|
||||
isSuccessResponse,
|
||||
isErrorResponse,
|
||||
isFailResponse,
|
||||
};
|
||||
};
|
|
@ -13,6 +13,7 @@ import { useEffect, useRef } from '@wordpress/element';
|
|||
import { DISPLAY_CART_PRICES_INCLUDING_TAX } from '@woocommerce/block-settings';
|
||||
import { ValidationInputError } from '@woocommerce/base-components/validation';
|
||||
import CheckboxControl from '@woocommerce/base-components/checkbox-control';
|
||||
import { useEmitResponse } from '@woocommerce/base-hooks';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
|
@ -87,9 +88,9 @@ export const usePaymentMethodInterface = () => {
|
|||
isComplete,
|
||||
isIdle,
|
||||
isProcessing,
|
||||
onCheckoutCompleteSuccess,
|
||||
onCheckoutCompleteError,
|
||||
onCheckoutProcessing,
|
||||
onCheckoutAfterProcessingWithSuccess,
|
||||
onCheckoutAfterProcessingWithError,
|
||||
onCheckoutBeforeProcessing,
|
||||
onSubmit,
|
||||
customerId,
|
||||
} = useCheckoutContext();
|
||||
|
@ -97,9 +98,6 @@ export const usePaymentMethodInterface = () => {
|
|||
currentStatus,
|
||||
activePaymentMethod,
|
||||
onPaymentProcessing,
|
||||
onPaymentSuccess,
|
||||
onPaymentFail,
|
||||
onPaymentError,
|
||||
setExpressPaymentError,
|
||||
} = usePaymentMethodDataContext();
|
||||
const {
|
||||
|
@ -122,6 +120,7 @@ export const usePaymentMethodInterface = () => {
|
|||
const { order, isLoading: orderLoading } = useStoreOrder();
|
||||
const { cartTotals } = useStoreCart();
|
||||
const { appliedCoupons } = useStoreCartCoupons();
|
||||
const { noticeContexts, responseTypes } = useEmitResponse();
|
||||
const currentCartTotals = useRef(
|
||||
prepareTotalItems( cartTotals, needsShipping )
|
||||
);
|
||||
|
@ -176,22 +175,23 @@ export const usePaymentMethodInterface = () => {
|
|||
customerId,
|
||||
},
|
||||
eventRegistration: {
|
||||
onCheckoutCompleteSuccess,
|
||||
onCheckoutCompleteError,
|
||||
onCheckoutProcessing,
|
||||
onCheckoutAfterProcessingWithSuccess,
|
||||
onCheckoutAfterProcessingWithError,
|
||||
onCheckoutBeforeProcessing,
|
||||
onShippingRateSuccess,
|
||||
onShippingRateFail,
|
||||
onShippingRateSelectSuccess,
|
||||
onShippingRateSelectFail,
|
||||
onPaymentProcessing,
|
||||
onPaymentSuccess,
|
||||
onPaymentFail,
|
||||
onPaymentError,
|
||||
},
|
||||
components: {
|
||||
ValidationInputError,
|
||||
CheckboxControl,
|
||||
},
|
||||
emitResponse: {
|
||||
noticeContexts,
|
||||
responseTypes,
|
||||
},
|
||||
onSubmit,
|
||||
activePaymentMethod,
|
||||
setExpressPaymentError,
|
||||
|
|
|
@ -28,6 +28,7 @@ import {
|
|||
useShippingDataContext,
|
||||
useBillingDataContext,
|
||||
useValidationContext,
|
||||
StoreNoticesProvider,
|
||||
} from '@woocommerce/base-context';
|
||||
import { useStoreCart, usePaymentMethods } from '@woocommerce/base-hooks';
|
||||
import {
|
||||
|
@ -355,7 +356,9 @@ const Checkout = ( { attributes, scrollToTop } ) => {
|
|||
: ''
|
||||
}
|
||||
>
|
||||
<PaymentMethods />
|
||||
<StoreNoticesProvider context="wc/payment-area">
|
||||
<PaymentMethods />
|
||||
</StoreNoticesProvider>
|
||||
</FormStep>
|
||||
) }
|
||||
</CheckoutForm>
|
||||
|
|
|
@ -24,7 +24,12 @@ import { __ } from '@wordpress/i18n';
|
|||
*
|
||||
* @param {RegisteredPaymentMethodProps} props Incoming props
|
||||
*/
|
||||
const CreditCardComponent = ( { billing, eventRegistration, components } ) => {
|
||||
const CreditCardComponent = ( {
|
||||
billing,
|
||||
eventRegistration,
|
||||
emitResponse,
|
||||
components,
|
||||
} ) => {
|
||||
const { ValidationInputError, CheckboxControl } = components;
|
||||
const { customerId } = billing;
|
||||
const [ sourceId, setSourceId ] = useState( 0 );
|
||||
|
@ -39,6 +44,7 @@ const CreditCardComponent = ( { billing, eventRegistration, components } ) => {
|
|||
sourceId,
|
||||
setSourceId,
|
||||
shouldSavePayment,
|
||||
emitResponse,
|
||||
stripe,
|
||||
elements
|
||||
);
|
||||
|
|
|
@ -12,11 +12,12 @@ import {
|
|||
getStripeServerData,
|
||||
getErrorMessageForTypeAndCode,
|
||||
} from '../stripe-utils';
|
||||
import { usePaymentIntents } from './use-payment-intents';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EventRegistrationProps} EventRegistrationProps
|
||||
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').PaymentStatusProps} PaymentStatusProps
|
||||
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').BillingDataProps} BillingDataProps
|
||||
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EmitResponseProps} EmitResponseProps
|
||||
* @typedef {import('../stripe-utils/type-defs').Stripe} Stripe
|
||||
* @typedef {import('react').Dispatch<number>} SourceIdDispatch
|
||||
*/
|
||||
|
@ -24,16 +25,13 @@ import {
|
|||
/**
|
||||
* A custom hook for the Stripe processing and event observer logic.
|
||||
*
|
||||
* @param {EventRegistrationProps} eventRegistration Event registration
|
||||
* functions.
|
||||
* @param {BillingDataProps} billing Various billing data
|
||||
* items.
|
||||
* @param {number} sourceId Current set stripe
|
||||
* source id.
|
||||
* @param {SourceIdDispatch} setSourceId Setter for stripe
|
||||
* source id.
|
||||
* @param {boolean} shouldSavePayment Whether to save the
|
||||
* payment or not.
|
||||
* @param {EventRegistrationProps} eventRegistration Event registration functions.
|
||||
* @param {EmitResponseProps} emitResponse Various helpers for usage with observer
|
||||
* response objects.
|
||||
* @param {BillingDataProps} billing Various billing data items.
|
||||
* @param {number} sourceId Current set stripe source id.
|
||||
* @param {SourceIdDispatch} setSourceId Setter for stripe source id.
|
||||
* @param {boolean} shouldSavePayment Whether to save the payment or not.
|
||||
* @param {Stripe} stripe The stripe.js object.
|
||||
* @param {Object} elements Stripe Elements object.
|
||||
*
|
||||
|
@ -45,6 +43,7 @@ export const useCheckoutSubscriptions = (
|
|||
sourceId,
|
||||
setSourceId,
|
||||
shouldSavePayment,
|
||||
emitResponse,
|
||||
stripe,
|
||||
elements
|
||||
) => {
|
||||
|
@ -52,6 +51,11 @@ export const useCheckoutSubscriptions = (
|
|||
const onStripeError = useRef( ( event ) => {
|
||||
return event;
|
||||
} );
|
||||
usePaymentIntents(
|
||||
stripe,
|
||||
eventRegistration.onCheckoutAfterProcessingWithSuccess,
|
||||
emitResponse
|
||||
);
|
||||
// hook into and register callbacks for events.
|
||||
useEffect( () => {
|
||||
onStripeError.current = ( event ) => {
|
||||
|
@ -80,19 +84,23 @@ export const useCheckoutSubscriptions = (
|
|||
// if there's an error return that.
|
||||
if ( error ) {
|
||||
return {
|
||||
errorMessage: error,
|
||||
type: emitResponse.responseTypes.ERROR,
|
||||
message: error,
|
||||
};
|
||||
}
|
||||
// use token if it's set.
|
||||
if ( sourceId !== 0 ) {
|
||||
return {
|
||||
paymentMethodData: {
|
||||
paymentMethod: PAYMENT_METHOD_NAME,
|
||||
paymentRequestType: 'cc',
|
||||
stripe_source: sourceId,
|
||||
shouldSavePayment,
|
||||
type: emitResponse.responseTypes.SUCCESS,
|
||||
meta: {
|
||||
paymentMethodData: {
|
||||
paymentMethod: PAYMENT_METHOD_NAME,
|
||||
paymentRequestType: 'cc',
|
||||
stripe_source: sourceId,
|
||||
shouldSavePayment,
|
||||
},
|
||||
billingData,
|
||||
},
|
||||
billingData,
|
||||
};
|
||||
}
|
||||
const ownerInfo = {
|
||||
|
@ -118,35 +126,54 @@ export const useCheckoutSubscriptions = (
|
|||
const response = await createSource( ownerInfo );
|
||||
if ( response.error ) {
|
||||
return {
|
||||
errorMessage: onStripeError.current( response ),
|
||||
type: emitResponse.responseTypes.ERROR,
|
||||
message: onStripeError.current( response ),
|
||||
};
|
||||
}
|
||||
setSourceId( response.source.id );
|
||||
return {
|
||||
paymentMethodData: {
|
||||
stripe_source: response.source.id,
|
||||
paymentMethod: PAYMENT_METHOD_NAME,
|
||||
paymentRequestType: 'cc',
|
||||
shouldSavePayment,
|
||||
type: emitResponse.responseTypes.SUCCESS,
|
||||
meta: {
|
||||
paymentMethodData: {
|
||||
stripe_source: response.source.id,
|
||||
paymentMethod: PAYMENT_METHOD_NAME,
|
||||
paymentRequestType: 'cc',
|
||||
shouldSavePayment,
|
||||
},
|
||||
billingData,
|
||||
},
|
||||
billingData,
|
||||
};
|
||||
} catch ( e ) {
|
||||
return {
|
||||
errorMessage: e,
|
||||
type: emitResponse.responseTypes.ERROR,
|
||||
message: e,
|
||||
};
|
||||
}
|
||||
};
|
||||
const onError = ( { processingResponse } ) => {
|
||||
if ( processingResponse?.paymentDetails?.errorMessage ) {
|
||||
return {
|
||||
type: emitResponse.responseTypes.ERROR,
|
||||
message: processingResponse.paymentDetails.errorMessage,
|
||||
messageContext: emitResponse.noticeContexts.PAYMENTS,
|
||||
};
|
||||
}
|
||||
// leave for checkout to handle.
|
||||
return null;
|
||||
};
|
||||
const unsubscribeProcessing = eventRegistration.onPaymentProcessing(
|
||||
onSubmit
|
||||
);
|
||||
const unsubscribeAfterProcessing = eventRegistration.onCheckoutAfterProcessingWithError(
|
||||
onError
|
||||
);
|
||||
return () => {
|
||||
unsubscribeProcessing();
|
||||
unsubscribeAfterProcessing();
|
||||
};
|
||||
}, [
|
||||
eventRegistration.onCheckoutProcessing,
|
||||
eventRegistration.onCheckoutCompleteSuccess,
|
||||
eventRegistration.onCheckoutCompleteError,
|
||||
eventRegistration.onPaymentProcessing,
|
||||
eventRegistration.onCheckoutAfterProcessingWithError,
|
||||
stripe,
|
||||
sourceId,
|
||||
billing.billingData,
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useEffect } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').EmitResponseProps} EmitResponseProps
|
||||
* @typedef {import('../stripe-utils/type-defs').Stripe} Stripe
|
||||
*/
|
||||
|
||||
/**
|
||||
* Opens the modal for PaymentIntent authorizations.
|
||||
*
|
||||
* @param {Stripe} stripe The stripe object.
|
||||
* @param {Object} paymentDetails The payment details from the server after checkout
|
||||
* processing.
|
||||
* @param {EmitResponseProps} emitResponse Various helpers for usage with observer response
|
||||
* objects.
|
||||
*/
|
||||
const openIntentModal = ( stripe, paymentDetails, emitResponse ) => {
|
||||
const checkoutResponse = { type: emitResponse.responseTypes.SUCCESS };
|
||||
if (
|
||||
! paymentDetails.setup_intent &&
|
||||
! paymentDetails.payment_intent_secret
|
||||
) {
|
||||
return checkoutResponse;
|
||||
}
|
||||
const isSetupIntent = !! paymentDetails.setupIntent;
|
||||
const verificationUrl = paymentDetails.verification_endpoint;
|
||||
const intentSecret = isSetupIntent
|
||||
? paymentDetails.setup_intent
|
||||
: paymentDetails.payment_intent_secret;
|
||||
return stripe[ isSetupIntent ? 'confirmCardSetup' : 'confirmCardPayment' ](
|
||||
intentSecret
|
||||
)
|
||||
.then( function( response ) {
|
||||
if ( response.error ) {
|
||||
throw response.error;
|
||||
}
|
||||
const intent =
|
||||
response[ isSetupIntent ? 'setupIntent' : 'paymentIntent' ];
|
||||
if (
|
||||
intent.status !== 'requires_capture' &&
|
||||
intent.status !== 'succeeded'
|
||||
) {
|
||||
return checkoutResponse;
|
||||
}
|
||||
checkoutResponse.redirectUrl = verificationUrl;
|
||||
return checkoutResponse;
|
||||
} )
|
||||
.catch( function( error ) {
|
||||
checkoutResponse.type = emitResponse.responseTypes.ERROR;
|
||||
checkoutResponse.message = error.message;
|
||||
checkoutResponse.retry = true;
|
||||
checkoutResponse.messageContext =
|
||||
emitResponse.noticeContexts.PAYMENTS;
|
||||
// Reports back to the server.
|
||||
window.fetch( verificationUrl + '&is_ajax' );
|
||||
return checkoutResponse;
|
||||
} );
|
||||
};
|
||||
|
||||
export const usePaymentIntents = ( stripe, subscriber, emitResponse ) => {
|
||||
useEffect( () => {
|
||||
const unsubscribe = subscriber( ( { processingResponse } ) => {
|
||||
const paymentDetails = processingResponse.paymentDetails || {};
|
||||
return openIntentModal( stripe, paymentDetails, emitResponse );
|
||||
} );
|
||||
return () => unsubscribe();
|
||||
}, [ subscriber, stripe ] );
|
||||
};
|
|
@ -55,6 +55,7 @@ const PaymentRequestExpressComponent = ( {
|
|||
eventRegistration,
|
||||
onSubmit,
|
||||
setExpressPaymentError,
|
||||
emitResponse,
|
||||
onClick,
|
||||
onClose,
|
||||
} ) => {
|
||||
|
@ -210,29 +211,48 @@ const PaymentRequestExpressComponent = ( {
|
|||
const handlers = eventHandlers.current;
|
||||
if ( handlers.sourceEvent && isProcessing ) {
|
||||
const response = {
|
||||
billingData: getBillingData( handlers.sourceEvent ),
|
||||
paymentMethodData: getPaymentMethodData(
|
||||
handlers.sourceEvent,
|
||||
paymentRequestType
|
||||
),
|
||||
shippingData: getShippingData( handlers.sourceEvent ),
|
||||
type: emitResponse.responseTypes.SUCCESS,
|
||||
meta: {
|
||||
billingData: getBillingData( handlers.sourceEvent ),
|
||||
paymentMethodData: getPaymentMethodData(
|
||||
handlers.sourceEvent,
|
||||
paymentRequestType
|
||||
),
|
||||
shippingData: getShippingData( handlers.sourceEvent ),
|
||||
},
|
||||
};
|
||||
return response;
|
||||
}
|
||||
return true;
|
||||
return { type: emitResponse.responseTypes.SUCCESS };
|
||||
};
|
||||
|
||||
const onCheckoutComplete = ( forSuccess = true ) => () => {
|
||||
const onCheckoutComplete = ( checkoutResponse ) => {
|
||||
const handlers = eventHandlers.current;
|
||||
let response = { type: emitResponse.responseTypes.SUCCESS };
|
||||
if ( handlers.sourceEvent && isProcessing ) {
|
||||
if ( forSuccess ) {
|
||||
const { paymentStatus, paymentDetails } = checkoutResponse;
|
||||
if ( paymentStatus === emitResponse.responseTypes.SUCCESS ) {
|
||||
completePayment( handlers.sourceEvent );
|
||||
} else {
|
||||
abortPayment( handlers.sourceEvent );
|
||||
}
|
||||
if (
|
||||
paymentStatus === emitResponse.responseTypes.ERROR ||
|
||||
paymentStatus === emitResponse.responseTypes.FAIL
|
||||
) {
|
||||
const paymentResponse = abortPayment(
|
||||
handlers.sourceEvent,
|
||||
paymentDetails?.errorMessage
|
||||
);
|
||||
response = {
|
||||
type: emitResponse.responseTypes.ERROR,
|
||||
message: paymentResponse.message,
|
||||
messageContext:
|
||||
emitResponse.noticeContexts.EXPRESS_PAYMENTS,
|
||||
retry: true,
|
||||
};
|
||||
}
|
||||
handlers.sourceEvent = null;
|
||||
}
|
||||
return true;
|
||||
return response;
|
||||
};
|
||||
|
||||
// when canMakePayment is true, then we set listeners on payment request for
|
||||
|
@ -301,11 +321,11 @@ const PaymentRequestExpressComponent = ( {
|
|||
const unsubscribePaymentProcessing = subscriber.onPaymentProcessing(
|
||||
onPaymentProcessing
|
||||
);
|
||||
const unsubscribeCheckoutCompleteSuccess = subscriber.onCheckoutCompleteSuccess(
|
||||
onCheckoutComplete()
|
||||
const unsubscribeCheckoutCompleteSuccess = subscriber.onCheckoutAfterProcessingWithSuccess(
|
||||
onCheckoutComplete
|
||||
);
|
||||
const unsubscribeCheckoutCompleteFail = subscriber.onCheckoutCompleteError(
|
||||
onCheckoutComplete( false )
|
||||
const unsubscribeCheckoutCompleteFail = subscriber.onCheckoutAfterProcessingWithError(
|
||||
onCheckoutComplete
|
||||
);
|
||||
return () => {
|
||||
unsubscribeCheckoutCompleteFail();
|
||||
|
@ -326,8 +346,8 @@ const PaymentRequestExpressComponent = ( {
|
|||
eventRegistration.onShippingRateSelectSuccess,
|
||||
eventRegistration.onShippingRateSelectFail,
|
||||
eventRegistration.onPaymentProcessing,
|
||||
eventRegistration.onCheckoutCompleteSuccess,
|
||||
eventRegistration.onCheckoutCompleteError,
|
||||
eventRegistration.onCheckoutAfterProcessingWithSuccess,
|
||||
eventRegistration.onCheckoutAfterProcessingWithError,
|
||||
] );
|
||||
|
||||
// locale is not a valid value for the paymentRequestButton style.
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
* redirectUrl to the given value.
|
||||
* @property {function(boolean=)} setHasError Dispatches an action that sets the
|
||||
* checkout status to having an error.
|
||||
* @property {function()} setComplete Dispatches an action that sets the
|
||||
* checkout status to complete.
|
||||
* @property {function(Object)} setAfterProcessing Dispatches an action that sets the
|
||||
* checkout status to after processing and
|
||||
* also sets the response data accordingly.
|
||||
* @property {function()} incrementCalculating Dispatches an action that increments
|
||||
* the calculating state for checkout by one.
|
||||
* @property {function()} decrementCalculating Dispatches an action that decrements
|
||||
|
@ -20,18 +21,20 @@
|
|||
/**
|
||||
* @typedef {Object} CheckoutStatusConstants
|
||||
*
|
||||
* @property {string} PRISTINE Checkout is in it's initialized state.
|
||||
* @property {string} IDLE When checkout state has changed but
|
||||
* there is no activity happening.
|
||||
* @property {string} PROCESSING This is the state when the checkout
|
||||
* button has been pressed and the
|
||||
* checkout data has been sent to the
|
||||
* server for processing.
|
||||
* @property {string} PROCESSING_COMPLETE This is the state when the checkout
|
||||
* processing has been completed.
|
||||
* @property {string} COMPLETE This is the status when the server has
|
||||
* completed processing the data
|
||||
* successfully.
|
||||
* @property {string} PRISTINE Checkout is in it's initialized state.
|
||||
* @property {string} IDLE When checkout state has changed but there is no
|
||||
* activity happening.
|
||||
* @property {string} BEFORE_PROCESSING This is the state before checkout processing
|
||||
* begins after the checkout button has been
|
||||
* pressed/submitted.
|
||||
* @property {string} PROCESSING After BEFORE_PROCESSING status emitters have
|
||||
* finished successfully. Payment processing is
|
||||
* started on this checkout status.
|
||||
* @property {string} AFTER_PROCESSING After server side checkout processing is completed
|
||||
* this status is set.
|
||||
* @property {string} COMPLETE After the AFTER_PROCESSING event emitters have
|
||||
* completed. This status triggers the checkout
|
||||
* redirect.
|
||||
*/
|
||||
|
||||
export {};
|
||||
|
|
|
@ -199,19 +199,6 @@
|
|||
* observers for the
|
||||
* payment processing
|
||||
* event.
|
||||
* @property {function(function())} onPaymentSuccess Event registration
|
||||
* callback for registering
|
||||
* observers for the
|
||||
* successful payment
|
||||
* event.
|
||||
* @property {function(function())} onPaymentFail Event registration
|
||||
* callback for registering
|
||||
* observers for the
|
||||
* failed payment event.
|
||||
* @property {function(function())} onPaymentError Event registration
|
||||
* callback for registering
|
||||
* observers for the
|
||||
* payment error event.
|
||||
* @property {function(string)} setExpressPaymentError A function used by
|
||||
* express payment methods
|
||||
* to indicate an error
|
||||
|
@ -224,56 +211,65 @@
|
|||
/**
|
||||
* @typedef {Object} CheckoutDataContext
|
||||
*
|
||||
* @property {string} submitLabel The label to use for the
|
||||
* submit checkout button.
|
||||
* @property {function()} onSubmit The callback to register with
|
||||
* the checkout submit button.
|
||||
* @property {boolean} isComplete True when checkout is complete
|
||||
* and ready for redirect.
|
||||
* @property {boolean} isProcessingComplete True when checkout processing
|
||||
* is complete.
|
||||
* @property {boolean} isIdle True when the checkout state
|
||||
* has changed and checkout has
|
||||
* no activity.
|
||||
* @property {boolean} isProcessing True when checkout has been
|
||||
* submitted and is being
|
||||
* processed by the server.
|
||||
* @property {boolean} isCalculating True when something in the
|
||||
* checkout is resulting in
|
||||
* totals being calculated.
|
||||
* @property {boolean} hasError True when the checkout is in
|
||||
* an error state. Whatever
|
||||
* caused the error
|
||||
* (validation/payment method)
|
||||
* will likely have triggered a
|
||||
* notice.
|
||||
* @property {string} redirectUrl This is the url that checkout
|
||||
* will redirect to when it's
|
||||
* ready.
|
||||
* @property {function(function(),number=)} onCheckoutCompleteSuccess Used to register a callback
|
||||
* that will fire when the
|
||||
* checkout is marked complete
|
||||
* successfully.
|
||||
* @property {function(function(),number=)} onCheckoutCompleteError Used to register a callback
|
||||
* that will fire when the
|
||||
* checkout is marked complete
|
||||
* and has an error.
|
||||
* @property {function(function(),number=)} onCheckoutProcessing Used to register a callback
|
||||
* that will fire when the
|
||||
* checkout has been submitted
|
||||
* before being sent off to the
|
||||
* server.
|
||||
* @property {CheckoutDispatchActions} dispatchActions Various actions that can be
|
||||
* dispatched for the checkout
|
||||
* context data.
|
||||
* @property {number} orderId This is the ID for the draft
|
||||
* order if one exists.
|
||||
* @property {boolean} hasOrder True when the checkout has a
|
||||
* draft order from the API.
|
||||
* @property {boolean} isCart When true, means the provider
|
||||
* is providing data for the cart.
|
||||
* @property {number} customerId This is the ID of the customer
|
||||
* the draft order belongs to.
|
||||
* @property {string} submitLabel The label to use for the
|
||||
* submit checkout button.
|
||||
* @property {function()} onSubmit The callback to register with
|
||||
* the checkout submit button.
|
||||
* @property {boolean} isComplete True when checkout is complete
|
||||
* and ready for redirect.
|
||||
* @property {boolean} isBeforeProcessing True during any observers
|
||||
* executing logic before
|
||||
* checkout processing (eg.
|
||||
* validation).
|
||||
* @property {boolean} isAfterProcessing True when checkout status is
|
||||
* AFTER_PROCESSING.
|
||||
* @property {boolean} isIdle True when the checkout state
|
||||
* has changed and checkout has
|
||||
* no activity.
|
||||
* @property {boolean} isProcessing True when checkout has been
|
||||
* submitted and is being
|
||||
* processed. Note, payment
|
||||
* related processing happens
|
||||
* during this state. When
|
||||
* payemnt status is success,
|
||||
* processing happens on the
|
||||
* server.
|
||||
* @property {boolean} isCalculating True when something in the
|
||||
* checkout is resulting in
|
||||
* totals being calculated.
|
||||
* @property {boolean} hasError True when the checkout is in
|
||||
* an error state. Whatever
|
||||
* caused the error
|
||||
* (validation/payment method)
|
||||
* will likely have triggered a
|
||||
* notice.
|
||||
* @property {string} redirectUrl This is the url that checkout
|
||||
* will redirect to when it's
|
||||
* ready.
|
||||
* @property {function(function(),number=)} onCheckoutAfterProcessingWithSuccess Used to register a
|
||||
* callback that will fire after
|
||||
* checkout has been processed
|
||||
* and there are no errors.
|
||||
* @property {function(function(),number=)} onCheckoutAfterProcessingWithError Used to register a
|
||||
* callback that will fire when
|
||||
* the checkout has been
|
||||
* processed and has an error.
|
||||
* @property {function(function(),number=)} onCheckoutBeforeProcessing Used to register a callback
|
||||
* that will fire when the
|
||||
* checkout has been submitted
|
||||
* before being sent off to the
|
||||
* server.
|
||||
* @property {CheckoutDispatchActions} dispatchActions Various actions that can be
|
||||
* dispatched for the checkout
|
||||
* context data.
|
||||
* @property {number} orderId This is the ID for the draft
|
||||
* order if one exists.
|
||||
* @property {boolean} hasOrder True when the checkout has a
|
||||
* draft order from the API.
|
||||
* @property {boolean} isCart When true, means the provider
|
||||
* is providing data for the cart.
|
||||
* @property {number} customerId This is the ID of the customer
|
||||
* the draft order belongs to.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
|
|
@ -73,4 +73,82 @@
|
|||
* the cart.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EmitResponseTypes
|
||||
*
|
||||
* @property {string} SUCCESS To indicate a success response.
|
||||
* @property {string} FAIL To indicate a failed response.
|
||||
* @property {string} ERROR To indicate an error response.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} NoticeContexts
|
||||
*
|
||||
* @property {string} PAYMENTS Notices for the payments step.
|
||||
* @property {string} EXPRESS_PAYMENTS Notices for the express payments step.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {NoticeContexts['PAYMENTS']|NoticeContexts['EXPRESS_PAYMENTS']} NoticeContextsEnum
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EmitSuccessResponse
|
||||
*
|
||||
* @property {EmitResponseTypes['SUCCESS']} type Should have the value of
|
||||
* EmitResponseTypes.SUCCESS.
|
||||
* @property {string} [redirectUrl] If the redirect url should be changed set
|
||||
* this. Note, this is ignored for some
|
||||
* emitters.
|
||||
* @property {Object} [meta] Additional data returned for the success
|
||||
* response. This varies between context
|
||||
* emitters.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EmitFailResponse
|
||||
*
|
||||
* @property {EmitResponseTypes['FAIL']} type Should have the value of
|
||||
* EmitResponseTypes.FAIL
|
||||
* @property {string} message A message to trigger a notice for.
|
||||
* @property {NoticeContextsEnum} [messageContext] What context to display any message in.
|
||||
* @property {Object} [meta] Additional data returned for the fail
|
||||
* response. This varies between context
|
||||
* emitters.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EmitErrorResponse
|
||||
*
|
||||
* @property {EmitResponseTypes['ERROR']} type Should have the value of
|
||||
* EmitResponseTypes.ERROR
|
||||
* @property {string} message A message to trigger a notice for.
|
||||
* @property {boolean} retry If false, then it means an
|
||||
* irrecoverable error so don't allow for
|
||||
* shopper to retry checkout (which may
|
||||
* mean either a different payment or
|
||||
* fixing validation errors).
|
||||
* @property {Object} [validationErrors] If provided, will be set as validation
|
||||
* errors in the validation context.
|
||||
* @property {NoticeContextsEnum} [messageContext] What context to display any message in.
|
||||
* @property {Object} [meta] Additional data returned for the fail
|
||||
* response. This varies between context
|
||||
* emitters.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} EmitResponseApi
|
||||
*
|
||||
* @property {EmitResponseTypes} responseTypes An object of various response types that can
|
||||
* be used in returned response objects.
|
||||
* @property {NoticeContexts} noticeContexts An object of various notice contexts that can
|
||||
* be used for targeting where a notice appears.
|
||||
* @property {function(Object):boolean} isSuccessResponse Returns whether the given response is of a
|
||||
* success response type.
|
||||
* @property {function(Object):boolean} isErrorResponse Returns whether the given response is of an
|
||||
* error response type.
|
||||
* @property {function(Object):boolean} isFailResponse Returns whether the given response is of a
|
||||
* fail response type.
|
||||
*/
|
||||
|
||||
export {};
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
* @typedef {import('@woocommerce/type-defs/contexts').ShippingErrorStatus} ShippingErrorStatus
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').ShippingErrorTypes} ShippingErrorTypes
|
||||
* @typedef {import('@woocommerce/type-defs/settings').WooCommerceSiteCurrency} SiteCurrency
|
||||
* @typedef {import('@woocommerce/type-defs/hooks').EmitResponseTypes} EmitResponseTypes
|
||||
* @typedef {import('@woocommerce/type-defs/hooks').NoticeContexts} NoticeContexts
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -134,13 +136,13 @@
|
|||
/**
|
||||
* @typedef EventRegistrationProps
|
||||
*
|
||||
* @property {function(function())} onCheckoutCompleteSuccess Used to subscribe callbacks firing
|
||||
* when checkout has completed
|
||||
* @property {function(function())} onCheckoutAfterProcessingWithSuccess Used to subscribe callbacks
|
||||
* firing when checkout has completed
|
||||
* processing successfully.
|
||||
* @property {function(function())} onCheckoutCompleteError Used to subscribe callbacks firing
|
||||
* when checkout has completed
|
||||
* @property {function(function())} onCheckoutAfterProcessingWithError Used to subscribe callbacks
|
||||
* firing when checkout has completed
|
||||
* processing with an error.
|
||||
* @property {function(function())} onCheckoutProcessing Used to subscribe callbacks that
|
||||
* @property {function(function())} onCheckoutBeforeProcessing Used to subscribe callbacks that
|
||||
* will fire when checkout begins
|
||||
* processing (as a part of the
|
||||
* processing process).
|
||||
|
@ -160,15 +162,6 @@
|
|||
* @property {function(function())} onPaymentProcessing Event registration callback for
|
||||
* registering observers for the
|
||||
* payment processing event.
|
||||
* @property {function(function())} onPaymentSuccess Event registration callback for
|
||||
* registering observers for the
|
||||
* successful payment event.
|
||||
* @property {function(function())} onPaymentFail Event registration callback for
|
||||
* registering observers for the
|
||||
* failed payment event.
|
||||
* @property {function(function())} onPaymentError Event registration callback for
|
||||
* registering observers for the
|
||||
* payment error event.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -180,6 +173,16 @@
|
|||
* saved payment method functionality
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef EmitResponseProps
|
||||
*
|
||||
* @property {EmitResponseTypes} responseTypes Response types that can be returned from emitter
|
||||
* observers.
|
||||
* @property {NoticeContexts} noticeContexts Available contexts that can be returned as the value
|
||||
* for the messageContext property on the object
|
||||
* returned from an emitter observer.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Registered payment method props
|
||||
*
|
||||
|
@ -194,13 +197,15 @@
|
|||
* @property {EventRegistrationProps} eventRegistration Various event registration helpers
|
||||
* for subscribing callbacks for
|
||||
* events.
|
||||
* @property {EmitResponseProps} emitResponse Utilities for usage in event
|
||||
* observer response objects.
|
||||
* @property {Function} [onSubmit] Used to trigger checkout
|
||||
* processing.
|
||||
* @property {string} [activePaymentMethod] Indicates what the active payment
|
||||
* method is.
|
||||
* @property {ComponentProps} components Components exposed to payment
|
||||
* methods for use.
|
||||
* @property {function(string)} [setExpressPaymentError] For setting an error (error
|
||||
* @property {function(string)} [setExpressPaymentError] For setting an error (error
|
||||
* message string) for express
|
||||
* payment methods. Does not change
|
||||
* payment status.
|
||||
|
|
|
@ -231,13 +231,12 @@ class Library {
|
|||
$_POST = $context->payment_data;
|
||||
|
||||
// Call the process payment method of the chosen gatway.
|
||||
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
|
||||
$payment_method_object = $context->get_payment_method_instance();
|
||||
|
||||
if ( ! isset( $available_gateways[ $context->payment_method ] ) ) {
|
||||
if ( ! $payment_method_object instanceof \WC_Payment_Gateway ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payment_method_object = $available_gateways[ $context->payment_method ];
|
||||
$payment_method_object->validate_fields();
|
||||
|
||||
if ( 0 !== wc_notice_count( 'error' ) ) {
|
||||
|
@ -250,9 +249,14 @@ class Library {
|
|||
// Restore $_POST data.
|
||||
$_POST = $post_data;
|
||||
|
||||
// Clear notices so they don't show up in the block.
|
||||
wc_clear_notices();
|
||||
|
||||
// Handle result.
|
||||
$result->set_status( isset( $gateway_result['result'] ) && 'success' === $gateway_result['result'] ? 'success' : 'failure' );
|
||||
$result->set_payment_details( [] );
|
||||
|
||||
// set payment_details from result.
|
||||
$result->set_payment_details( array_merge( $result->payment_details, $gateway_result ) );
|
||||
$result->set_redirect_url( $gateway_result['redirect'] );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ final class Stripe extends AbstractPaymentMethodType {
|
|||
public function __construct( Api $asset_api ) {
|
||||
$this->asset_api = $asset_api;
|
||||
add_action( 'woocommerce_rest_checkout_process_payment_with_context', [ $this, 'add_payment_request_order_meta' ], 8, 2 );
|
||||
add_action( 'woocommerce_rest_checkout_process_payment_with_context', [ $this, 'add_stripe_intents' ], 9999, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -227,7 +228,7 @@ final class Stripe extends AbstractPaymentMethodType {
|
|||
* @param PaymentContext $context Holds context for the payment.
|
||||
* @param PaymentResult $result Result object for the payment.
|
||||
*/
|
||||
public function add_payment_request_order_meta( PaymentContext $context, PaymentResult $result ) {
|
||||
public function add_payment_request_order_meta( PaymentContext $context, PaymentResult &$result ) {
|
||||
$data = $context->payment_data;
|
||||
if ( ! empty( $data['payment_request_type'] ) && 'stripe' === $context->payment_method ) {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification
|
||||
|
@ -236,5 +237,50 @@ final class Stripe extends AbstractPaymentMethodType {
|
|||
WC_Stripe_Payment_Request::add_order_meta( $context->order->id, $context->payment_data );
|
||||
$_POST = $post_data;
|
||||
}
|
||||
|
||||
// hook into stripe error processing so that we can capture the error to
|
||||
// payment details (which is added to notices and thus not helpful for
|
||||
// this context).
|
||||
if ( 'stripe' === $context->payment_method ) {
|
||||
add_action(
|
||||
'wc_gateway_stripe_process_payment_error',
|
||||
function( $error ) use ( &$result ) {
|
||||
$payment_details = $result->payment_details;
|
||||
$payment_details['errorMessage'] = $error->getLocalizedMessage();
|
||||
$result->set_payment_details( $payment_details );
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles any potential stripe intents on the order that need handled.
|
||||
*
|
||||
* This is configured to execute after legacy payment processing has
|
||||
* happened on the woocommerce_rest_checkout_process_payment_with_context
|
||||
* action hook.
|
||||
*
|
||||
* @param PaymentContext $context Holds context for the payment.
|
||||
* @param PaymentResult $result Result object for the payment.
|
||||
*/
|
||||
public function add_stripe_intents( PaymentContext $context, PaymentResult &$result ) {
|
||||
if ( 'stripe' === $context->payment_method
|
||||
&& (
|
||||
! empty( $result->payment_details['payment_intent_secret'] )
|
||||
|| ! empty( $result->payment_details['setup_intent_secret'] )
|
||||
)
|
||||
) {
|
||||
$payment_details = $result->payment_details;
|
||||
$payment_details['verification_endpoint'] = add_query_arg(
|
||||
[
|
||||
'order' => $context->order->get_id(),
|
||||
'nonce' => wp_create_nonce( 'wc_stripe_confirm_pi' ),
|
||||
'redirect_to' => rawurlencode( $result->redirect_url ),
|
||||
],
|
||||
home_url() . \WC_Ajax::get_endpoint( 'wc_stripe_verify_intent' )
|
||||
);
|
||||
$result->set_payment_details( $payment_details );
|
||||
$result->set_status( 'success' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,19 @@ class PaymentContext {
|
|||
$this->payment_method = (string) $payment_method;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the payment method instance for the current set payment method.
|
||||
*
|
||||
* @return {\WC_Payment_Gateway|null} An instance of the payment gateway if it exists.
|
||||
*/
|
||||
public function get_payment_method_instance() {
|
||||
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
|
||||
if ( ! isset( $available_gateways[ $this->payment_method ] ) ) {
|
||||
return;
|
||||
}
|
||||
return $available_gateways[ $this->payment_method ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the order context.
|
||||
*
|
||||
|
|
|
@ -166,9 +166,31 @@ class CheckoutSchema extends AbstractSchema {
|
|||
'payment_method' => $order->get_payment_method(),
|
||||
'payment_result' => [
|
||||
'payment_status' => $payment_result->status,
|
||||
'payment_details' => $payment_result->payment_details,
|
||||
'payment_details' => $this->prepare_payment_details_for_response( $payment_result->payment_details ),
|
||||
'redirect_url' => $payment_result->redirect_url,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* This prepares the payment details for the response so it's following the
|
||||
* schema where it's an array of objects.
|
||||
*
|
||||
* @param array $payment_details An array of payment details from the processed payment.
|
||||
*
|
||||
* @return array An array of objects where each object has the key and value
|
||||
* as distinct properties.
|
||||
*/
|
||||
protected function prepare_payment_details_for_response( array $payment_details ) {
|
||||
return array_map(
|
||||
function( $key, $value ) {
|
||||
return (object) [
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
];
|
||||
},
|
||||
array_keys( $payment_details ),
|
||||
$payment_details
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue