initial commit
This commit is contained in:
@ -0,0 +1,85 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
PaymentMethods,
|
||||
ExpressPaymentMethods,
|
||||
} from '@woocommerce/type-defs/payments';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { ACTION, STATUS } from './constants';
|
||||
|
||||
export interface ActionType {
|
||||
type: ACTION | STATUS;
|
||||
errorMessage?: string;
|
||||
paymentMethodData?: Record< string, unknown >;
|
||||
paymentMethods?: PaymentMethods | ExpressPaymentMethods;
|
||||
shouldSavePaymentMethod?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* All the actions that can be dispatched for payment methods.
|
||||
*/
|
||||
export const actions = {
|
||||
statusOnly: ( type: STATUS ): { type: STATUS } => ( { type } as const ),
|
||||
error: ( errorMessage: string ): ActionType =>
|
||||
( {
|
||||
type: STATUS.ERROR,
|
||||
errorMessage,
|
||||
} as const ),
|
||||
failed: ( {
|
||||
errorMessage,
|
||||
paymentMethodData,
|
||||
}: {
|
||||
errorMessage: string;
|
||||
paymentMethodData: Record< string, unknown >;
|
||||
} ): ActionType =>
|
||||
( {
|
||||
type: STATUS.FAILED,
|
||||
errorMessage,
|
||||
paymentMethodData,
|
||||
} as const ),
|
||||
success: ( {
|
||||
paymentMethodData,
|
||||
}: {
|
||||
paymentMethodData?: Record< string, unknown >;
|
||||
} ): ActionType =>
|
||||
( {
|
||||
type: STATUS.SUCCESS,
|
||||
paymentMethodData,
|
||||
} as const ),
|
||||
started: ( {
|
||||
paymentMethodData,
|
||||
}: {
|
||||
paymentMethodData?: Record< string, unknown >;
|
||||
} ): ActionType =>
|
||||
( {
|
||||
type: STATUS.STARTED,
|
||||
paymentMethodData,
|
||||
} as const ),
|
||||
setRegisteredPaymentMethods: (
|
||||
paymentMethods: PaymentMethods
|
||||
): ActionType =>
|
||||
( {
|
||||
type: ACTION.SET_REGISTERED_PAYMENT_METHODS,
|
||||
paymentMethods,
|
||||
} as const ),
|
||||
setRegisteredExpressPaymentMethods: (
|
||||
paymentMethods: ExpressPaymentMethods
|
||||
): ActionType =>
|
||||
( {
|
||||
type: ACTION.SET_REGISTERED_EXPRESS_PAYMENT_METHODS,
|
||||
paymentMethods,
|
||||
} as const ),
|
||||
setShouldSavePaymentMethod: (
|
||||
shouldSavePaymentMethod: boolean
|
||||
): ActionType =>
|
||||
( {
|
||||
type: ACTION.SET_SHOULD_SAVE_PAYMENT_METHOD,
|
||||
shouldSavePaymentMethod,
|
||||
} as const ),
|
||||
};
|
||||
|
||||
export default actions;
|
@ -0,0 +1,77 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type {
|
||||
PaymentMethodDataContextType,
|
||||
PaymentMethodDataContextState,
|
||||
} from './types';
|
||||
|
||||
export enum STATUS {
|
||||
PRISTINE = 'pristine',
|
||||
STARTED = 'started',
|
||||
PROCESSING = 'processing',
|
||||
ERROR = 'has_error',
|
||||
FAILED = 'failed',
|
||||
SUCCESS = 'success',
|
||||
COMPLETE = 'complete',
|
||||
}
|
||||
|
||||
export enum ACTION {
|
||||
SET_REGISTERED_PAYMENT_METHODS = 'set_registered_payment_methods',
|
||||
SET_REGISTERED_EXPRESS_PAYMENT_METHODS = 'set_registered_express_payment_methods',
|
||||
SET_SHOULD_SAVE_PAYMENT_METHOD = 'set_should_save_payment_method',
|
||||
}
|
||||
|
||||
// Note - if fields are added/shape is changed, you may want to update PRISTINE reducer clause to preserve your new field.
|
||||
export const DEFAULT_PAYMENT_DATA_CONTEXT_STATE: PaymentMethodDataContextState = {
|
||||
currentStatus: STATUS.PRISTINE,
|
||||
shouldSavePaymentMethod: false,
|
||||
paymentMethodData: {
|
||||
payment_method: '',
|
||||
},
|
||||
hasSavedToken: false,
|
||||
errorMessage: '',
|
||||
paymentMethods: {},
|
||||
expressPaymentMethods: {},
|
||||
};
|
||||
|
||||
export const DEFAULT_PAYMENT_METHOD_DATA: PaymentMethodDataContextType = {
|
||||
setPaymentStatus: () => ( {
|
||||
pristine: () => void null,
|
||||
started: () => void null,
|
||||
processing: () => void null,
|
||||
completed: () => void null,
|
||||
error: ( errorMessage: string ) => void errorMessage,
|
||||
failed: ( errorMessage, paymentMethodData ) =>
|
||||
void [ errorMessage, paymentMethodData ],
|
||||
success: ( paymentMethodData, billingData ) =>
|
||||
void [ paymentMethodData, billingData ],
|
||||
} ),
|
||||
currentStatus: {
|
||||
isPristine: true,
|
||||
isStarted: false,
|
||||
isProcessing: false,
|
||||
isFinished: false,
|
||||
hasError: false,
|
||||
hasFailed: false,
|
||||
isSuccessful: false,
|
||||
isDoingExpressPayment: false,
|
||||
},
|
||||
paymentStatuses: STATUS,
|
||||
paymentMethodData: {},
|
||||
errorMessage: '',
|
||||
activePaymentMethod: '',
|
||||
setActivePaymentMethod: () => void null,
|
||||
activeSavedToken: '',
|
||||
setActiveSavedToken: () => void null,
|
||||
customerPaymentMethods: {},
|
||||
paymentMethods: {},
|
||||
expressPaymentMethods: {},
|
||||
paymentMethodsInitialized: false,
|
||||
expressPaymentMethodsInitialized: false,
|
||||
onPaymentProcessing: () => () => () => void null,
|
||||
setExpressPaymentError: () => void null,
|
||||
isExpressPaymentMethodActive: false,
|
||||
setShouldSavePayment: () => void null,
|
||||
shouldSavePayment: false,
|
||||
};
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useMemo } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
reducer,
|
||||
emitEvent,
|
||||
emitEventWithAbort,
|
||||
emitterCallback,
|
||||
ActionType,
|
||||
} from '../../../event-emit';
|
||||
|
||||
const EMIT_TYPES = {
|
||||
PAYMENT_PROCESSING: 'payment_processing',
|
||||
};
|
||||
|
||||
type EventEmittersType = Record< string, ReturnType< typeof emitterCallback > >;
|
||||
|
||||
/**
|
||||
* Receives a reducer dispatcher and returns an object with the
|
||||
* various event emitters for the payment processing events.
|
||||
*
|
||||
* Calling the event registration function with the callback will register it
|
||||
* for the event emitter and will return a dispatcher for removing the
|
||||
* registered callback (useful for implementation in `useEffect`).
|
||||
*
|
||||
* @param {Function} observerDispatch The emitter reducer dispatcher.
|
||||
* @return {Object} An object with the various payment event emitter registration functions
|
||||
*/
|
||||
const useEventEmitters = (
|
||||
observerDispatch: React.Dispatch< ActionType >
|
||||
): EventEmittersType => {
|
||||
const eventEmitters = useMemo(
|
||||
() => ( {
|
||||
onPaymentProcessing: emitterCallback(
|
||||
EMIT_TYPES.PAYMENT_PROCESSING,
|
||||
observerDispatch
|
||||
),
|
||||
} ),
|
||||
[ observerDispatch ]
|
||||
);
|
||||
return eventEmitters;
|
||||
};
|
||||
|
||||
export { EMIT_TYPES, useEventEmitters, reducer, emitEvent, emitEventWithAbort };
|
@ -0,0 +1,4 @@
|
||||
export {
|
||||
PaymentMethodDataProvider,
|
||||
usePaymentMethodDataContext,
|
||||
} from './payment-method-data-context';
|
@ -0,0 +1,355 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useReducer,
|
||||
useCallback,
|
||||
useRef,
|
||||
useEffect,
|
||||
useMemo,
|
||||
} from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* 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 { useActivePaymentMethod } from './use-active-payment-method';
|
||||
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 { useStoreNotices } from '../../../hooks/use-store-notices';
|
||||
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.ReactChildren;
|
||||
} ): JSX.Element => {
|
||||
const {
|
||||
isProcessing: checkoutIsProcessing,
|
||||
isIdle: checkoutIsIdle,
|
||||
isCalculating: checkoutIsCalculating,
|
||||
hasError: checkoutHasError,
|
||||
} = useCheckoutContext();
|
||||
const { isEditor, getPreviewData } = useEditorContext();
|
||||
const { setValidationErrors } = useValidationContext();
|
||||
const { addErrorNotice, removeNotice } = useStoreNotices();
|
||||
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 {
|
||||
activePaymentMethod,
|
||||
activeSavedToken,
|
||||
setActivePaymentMethod,
|
||||
setActiveSavedToken,
|
||||
} = useActivePaymentMethod();
|
||||
|
||||
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( 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 ]
|
||||
);
|
||||
|
||||
// Update the active (selected) payment method when it is empty, or invalid.
|
||||
useEffect( () => {
|
||||
const paymentMethodKeys = Object.keys( paymentData.paymentMethods );
|
||||
const allPaymentMethodKeys = [
|
||||
...paymentMethodKeys,
|
||||
...Object.keys( paymentData.expressPaymentMethods ),
|
||||
];
|
||||
if ( ! paymentMethodsInitialized || ! paymentMethodKeys.length ) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActivePaymentMethod( ( 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 )
|
||||
) {
|
||||
setPaymentStatus().pristine();
|
||||
return Object.keys( paymentData.paymentMethods )[ 0 ];
|
||||
}
|
||||
return currentActivePaymentMethod;
|
||||
} );
|
||||
}, [
|
||||
paymentMethodsInitialized,
|
||||
paymentData.paymentMethods,
|
||||
paymentData.expressPaymentMethods,
|
||||
setActivePaymentMethod,
|
||||
setPaymentStatus,
|
||||
] );
|
||||
|
||||
// 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 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
|
||||
) {
|
||||
setPaymentStatus().pristine();
|
||||
}
|
||||
}, [
|
||||
checkoutHasError,
|
||||
currentStatus.isSuccessful,
|
||||
paymentData.hasSavedToken,
|
||||
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?.billingData,
|
||||
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?.billingData
|
||||
);
|
||||
} 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 paymentContextData: PaymentMethodDataContextType = {
|
||||
setPaymentStatus,
|
||||
currentStatus,
|
||||
paymentStatuses: STATUS,
|
||||
paymentMethodData: paymentData.paymentMethodData,
|
||||
errorMessage: paymentData.errorMessage,
|
||||
activePaymentMethod,
|
||||
setActivePaymentMethod,
|
||||
activeSavedToken,
|
||||
setActiveSavedToken,
|
||||
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>
|
||||
);
|
||||
};
|
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
ACTION,
|
||||
STATUS,
|
||||
DEFAULT_PAYMENT_DATA_CONTEXT_STATE,
|
||||
} from './constants';
|
||||
import type { PaymentMethodDataContextState } from './types';
|
||||
import type { ActionType } from './actions';
|
||||
|
||||
const hasSavedPaymentToken = (
|
||||
paymentMethodData: Record< string, unknown > | undefined
|
||||
): boolean => {
|
||||
return !! (
|
||||
typeof paymentMethodData === 'object' && paymentMethodData.isSavedToken
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer for payment data state
|
||||
*/
|
||||
const reducer = (
|
||||
state = DEFAULT_PAYMENT_DATA_CONTEXT_STATE,
|
||||
{
|
||||
type,
|
||||
paymentMethodData,
|
||||
shouldSavePaymentMethod = false,
|
||||
errorMessage = '',
|
||||
paymentMethods = {},
|
||||
}: ActionType
|
||||
): PaymentMethodDataContextState => {
|
||||
switch ( type ) {
|
||||
case STATUS.STARTED:
|
||||
return {
|
||||
...state,
|
||||
currentStatus: STATUS.STARTED,
|
||||
paymentMethodData: paymentMethodData || state.paymentMethodData,
|
||||
hasSavedToken: hasSavedPaymentToken(
|
||||
paymentMethodData || state.paymentMethodData
|
||||
),
|
||||
};
|
||||
case STATUS.ERROR:
|
||||
return state.currentStatus !== STATUS.ERROR
|
||||
? {
|
||||
...state,
|
||||
currentStatus: STATUS.ERROR,
|
||||
errorMessage: errorMessage || state.errorMessage,
|
||||
}
|
||||
: state;
|
||||
case STATUS.FAILED:
|
||||
return state.currentStatus !== STATUS.FAILED
|
||||
? {
|
||||
...state,
|
||||
currentStatus: STATUS.FAILED,
|
||||
paymentMethodData:
|
||||
paymentMethodData || state.paymentMethodData,
|
||||
errorMessage: errorMessage || state.errorMessage,
|
||||
}
|
||||
: state;
|
||||
case STATUS.SUCCESS:
|
||||
return state.currentStatus !== STATUS.SUCCESS
|
||||
? {
|
||||
...state,
|
||||
currentStatus: STATUS.SUCCESS,
|
||||
paymentMethodData:
|
||||
paymentMethodData || state.paymentMethodData,
|
||||
hasSavedToken: hasSavedPaymentToken(
|
||||
paymentMethodData || state.paymentMethodData
|
||||
),
|
||||
}
|
||||
: state;
|
||||
case STATUS.PROCESSING:
|
||||
return state.currentStatus !== STATUS.PROCESSING
|
||||
? {
|
||||
...state,
|
||||
currentStatus: STATUS.PROCESSING,
|
||||
errorMessage: '',
|
||||
}
|
||||
: state;
|
||||
case STATUS.COMPLETE:
|
||||
return state.currentStatus !== STATUS.COMPLETE
|
||||
? {
|
||||
...state,
|
||||
currentStatus: STATUS.COMPLETE,
|
||||
}
|
||||
: state;
|
||||
|
||||
case STATUS.PRISTINE:
|
||||
return {
|
||||
...DEFAULT_PAYMENT_DATA_CONTEXT_STATE,
|
||||
currentStatus: STATUS.PRISTINE,
|
||||
// keep payment method registration state
|
||||
paymentMethods: {
|
||||
...state.paymentMethods,
|
||||
},
|
||||
expressPaymentMethods: {
|
||||
...state.expressPaymentMethods,
|
||||
},
|
||||
shouldSavePaymentMethod: state.shouldSavePaymentMethod,
|
||||
};
|
||||
case ACTION.SET_REGISTERED_PAYMENT_METHODS:
|
||||
return {
|
||||
...state,
|
||||
paymentMethods,
|
||||
};
|
||||
case ACTION.SET_REGISTERED_EXPRESS_PAYMENT_METHODS:
|
||||
return {
|
||||
...state,
|
||||
expressPaymentMethods: paymentMethods,
|
||||
};
|
||||
case ACTION.SET_SHOULD_SAVE_PAYMENT_METHOD:
|
||||
return {
|
||||
...state,
|
||||
shouldSavePaymentMethod,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default reducer;
|
@ -0,0 +1,251 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { previewCart } from '@woocommerce/resource-previews';
|
||||
import { dispatch } from '@wordpress/data';
|
||||
import { CART_STORE_KEY as storeKey } from '@woocommerce/block-data';
|
||||
import {
|
||||
registerPaymentMethod,
|
||||
registerExpressPaymentMethod,
|
||||
__experimentalDeRegisterPaymentMethod,
|
||||
__experimentalDeRegisterExpressPaymentMethod,
|
||||
} from '@woocommerce/blocks-registry';
|
||||
import { default as fetchMock } from 'jest-fetch-mock';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
usePaymentMethodDataContext,
|
||||
PaymentMethodDataProvider,
|
||||
} from '../payment-method-data-context';
|
||||
import {
|
||||
CheckoutExpressPayment,
|
||||
SavedPaymentMethodOptions,
|
||||
} from '../../../../../../blocks/cart-checkout/payment-methods';
|
||||
import { defaultCartState } from '../../../../../../data/default-states';
|
||||
|
||||
jest.mock( '@woocommerce/settings', () => {
|
||||
const originalModule = jest.requireActual( '@woocommerce/settings' );
|
||||
|
||||
return {
|
||||
// @ts-ignore We know @woocommerce/settings is an object.
|
||||
...originalModule,
|
||||
getSetting: ( setting, ...rest ) => {
|
||||
if ( setting === 'customerPaymentMethods' ) {
|
||||
return {
|
||||
cc: [
|
||||
{
|
||||
method: {
|
||||
gateway: 'stripe',
|
||||
last4: '4242',
|
||||
brand: 'Visa',
|
||||
},
|
||||
expires: '12/22',
|
||||
is_default: true,
|
||||
tokenId: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return originalModule.getSetting( setting, ...rest );
|
||||
},
|
||||
};
|
||||
} );
|
||||
|
||||
const registerMockPaymentMethods = () => {
|
||||
[ 'cheque', 'bacs' ].forEach( ( name ) => {
|
||||
registerPaymentMethod( {
|
||||
name,
|
||||
label: name,
|
||||
content: <div>A payment method</div>,
|
||||
edit: <div>A payment method</div>,
|
||||
icons: null,
|
||||
canMakePayment: () => true,
|
||||
supports: {
|
||||
features: [ 'products' ],
|
||||
},
|
||||
ariaLabel: name,
|
||||
} );
|
||||
} );
|
||||
[ 'stripe' ].forEach( ( name ) => {
|
||||
registerPaymentMethod( {
|
||||
name,
|
||||
label: name,
|
||||
content: <div>A payment method</div>,
|
||||
edit: <div>A payment method</div>,
|
||||
icons: null,
|
||||
canMakePayment: () => true,
|
||||
supports: {
|
||||
showSavedCards: true,
|
||||
showSaveOption: true,
|
||||
features: [ 'products' ],
|
||||
},
|
||||
ariaLabel: name,
|
||||
} );
|
||||
} );
|
||||
[ 'express-payment' ].forEach( ( name ) => {
|
||||
const Content = ( {
|
||||
onClose = () => void null,
|
||||
onClick = () => void null,
|
||||
} ) => {
|
||||
return (
|
||||
<>
|
||||
<button onClick={ onClick }>
|
||||
{ name + ' express payment method' }
|
||||
</button>
|
||||
<button onClick={ onClose }>
|
||||
{ name + ' express payment method close' }
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
registerExpressPaymentMethod( {
|
||||
name,
|
||||
content: <Content />,
|
||||
edit: <div>An express payment method</div>,
|
||||
canMakePayment: () => true,
|
||||
paymentMethodId: name,
|
||||
supports: {
|
||||
features: [ 'products' ],
|
||||
},
|
||||
} );
|
||||
} );
|
||||
};
|
||||
|
||||
const resetMockPaymentMethods = () => {
|
||||
[ 'cheque', 'bacs', 'stripe' ].forEach( ( name ) => {
|
||||
__experimentalDeRegisterPaymentMethod( name );
|
||||
} );
|
||||
[ 'express-payment' ].forEach( ( name ) => {
|
||||
__experimentalDeRegisterExpressPaymentMethod( name );
|
||||
} );
|
||||
};
|
||||
|
||||
describe( 'Testing Payment Method Data Context Provider', () => {
|
||||
beforeEach( async () => {
|
||||
registerMockPaymentMethods();
|
||||
fetchMock.mockResponse( ( req ) => {
|
||||
if ( req.url.match( /wc\/store\/cart/ ) ) {
|
||||
return Promise.resolve( JSON.stringify( previewCart ) );
|
||||
}
|
||||
return Promise.resolve( '' );
|
||||
} );
|
||||
// need to clear the store resolution state between tests.
|
||||
await dispatch( storeKey ).invalidateResolutionForStore();
|
||||
await dispatch( storeKey ).receiveCart( defaultCartState.cartData );
|
||||
} );
|
||||
afterEach( async () => {
|
||||
resetMockPaymentMethods();
|
||||
fetchMock.resetMocks();
|
||||
} );
|
||||
it( 'toggles active payment method correctly for express payment activation and close', async () => {
|
||||
const TriggerActiveExpressPaymentMethod = () => {
|
||||
const { activePaymentMethod } = usePaymentMethodDataContext();
|
||||
return (
|
||||
<>
|
||||
<CheckoutExpressPayment />
|
||||
{ 'Active Payment Method: ' + activePaymentMethod }
|
||||
</>
|
||||
);
|
||||
};
|
||||
const TestComponent = () => {
|
||||
return (
|
||||
<PaymentMethodDataProvider>
|
||||
<TriggerActiveExpressPaymentMethod />
|
||||
</PaymentMethodDataProvider>
|
||||
);
|
||||
};
|
||||
render( <TestComponent /> );
|
||||
// should initialize by default the first payment method.
|
||||
await waitFor( () => {
|
||||
const activePaymentMethod = screen.queryByText(
|
||||
/Active Payment Method: cheque/
|
||||
);
|
||||
expect( activePaymentMethod ).not.toBeNull();
|
||||
} );
|
||||
// Express payment method clicked.
|
||||
fireEvent.click(
|
||||
screen.getByText( 'express-payment express payment method' )
|
||||
);
|
||||
await waitFor( () => {
|
||||
const activePaymentMethod = screen.queryByText(
|
||||
/Active Payment Method: express-payment/
|
||||
);
|
||||
expect( activePaymentMethod ).not.toBeNull();
|
||||
} );
|
||||
// Express payment method closed.
|
||||
fireEvent.click(
|
||||
screen.getByText( 'express-payment express payment method close' )
|
||||
);
|
||||
await waitFor( () => {
|
||||
const activePaymentMethod = screen.queryByText(
|
||||
/Active Payment Method: cheque/
|
||||
);
|
||||
expect( activePaymentMethod ).not.toBeNull();
|
||||
} );
|
||||
// ["`select` control in `@wordpress/data-controls` is deprecated. Please use built-in `resolveSelect` control in `@wordpress/data` instead."]
|
||||
expect( console ).toHaveWarned();
|
||||
} );
|
||||
|
||||
it( 'resets saved payment method data after starting and closing an express payment method', async () => {
|
||||
const TriggerActiveExpressPaymentMethod = () => {
|
||||
const {
|
||||
activePaymentMethod,
|
||||
paymentMethodData,
|
||||
} = usePaymentMethodDataContext();
|
||||
return (
|
||||
<>
|
||||
<CheckoutExpressPayment />
|
||||
<SavedPaymentMethodOptions onChange={ () => void null } />
|
||||
{ 'Active Payment Method: ' + activePaymentMethod }
|
||||
{ paymentMethodData[ 'wc-stripe-payment-token' ] && (
|
||||
<span>Stripe token</span>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
const TestComponent = () => {
|
||||
return (
|
||||
<PaymentMethodDataProvider>
|
||||
<TriggerActiveExpressPaymentMethod />
|
||||
</PaymentMethodDataProvider>
|
||||
);
|
||||
};
|
||||
render( <TestComponent /> );
|
||||
// Should initialize by default the default saved payment method.
|
||||
await waitFor( () => {
|
||||
const activePaymentMethod = screen.queryByText(
|
||||
/Active Payment Method: stripe/
|
||||
);
|
||||
const stripeToken = screen.queryByText( /Stripe token/ );
|
||||
expect( activePaymentMethod ).not.toBeNull();
|
||||
expect( stripeToken ).not.toBeNull();
|
||||
} );
|
||||
// Express payment method clicked.
|
||||
fireEvent.click(
|
||||
screen.getByText( 'express-payment express payment method' )
|
||||
);
|
||||
await waitFor( () => {
|
||||
const activePaymentMethod = screen.queryByText(
|
||||
/Active Payment Method: express-payment/
|
||||
);
|
||||
const stripeToken = screen.queryByText( /Stripe token/ );
|
||||
expect( activePaymentMethod ).not.toBeNull();
|
||||
expect( stripeToken ).toBeNull();
|
||||
} );
|
||||
// Express payment method closed.
|
||||
fireEvent.click(
|
||||
screen.getByText( 'express-payment express payment method close' )
|
||||
);
|
||||
await waitFor( () => {
|
||||
const activePaymentMethod = screen.queryByText(
|
||||
/Active Payment Method: stripe/
|
||||
);
|
||||
const stripeToken = screen.queryByText( /Stripe token/ );
|
||||
expect( activePaymentMethod ).not.toBeNull();
|
||||
expect( stripeToken ).not.toBeNull();
|
||||
} );
|
||||
} );
|
||||
} );
|
@ -0,0 +1,130 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import {
|
||||
PaymentMethodConfiguration,
|
||||
PaymentMethods,
|
||||
ExpressPaymentMethods,
|
||||
} from '@woocommerce/type-defs/payments';
|
||||
import type {
|
||||
EmptyObjectType,
|
||||
ObjectType,
|
||||
} from '@woocommerce/type-defs/objects';
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { emitterCallback } from '../../../event-emit';
|
||||
import { STATUS } from './constants';
|
||||
|
||||
export interface CustomerPaymentMethod {
|
||||
method: PaymentMethodConfiguration;
|
||||
expires: string;
|
||||
is_default: boolean;
|
||||
tokenId: number;
|
||||
actions: ObjectType;
|
||||
}
|
||||
export type CustomerPaymentMethods =
|
||||
| Record< string, CustomerPaymentMethod >
|
||||
| EmptyObjectType;
|
||||
|
||||
export type PaymentMethodDispatchers = {
|
||||
setRegisteredPaymentMethods: ( paymentMethods: PaymentMethods ) => void;
|
||||
setRegisteredExpressPaymentMethods: (
|
||||
paymentMethods: ExpressPaymentMethods
|
||||
) => void;
|
||||
setShouldSavePayment: ( shouldSave: boolean ) => void;
|
||||
};
|
||||
|
||||
export interface PaymentStatusDispatchers {
|
||||
pristine: () => void;
|
||||
started: ( paymentMethodData?: ObjectType | EmptyObjectType ) => void;
|
||||
processing: () => void;
|
||||
completed: () => void;
|
||||
error: ( error: string ) => void;
|
||||
failed: (
|
||||
error?: string,
|
||||
paymentMethodData?: ObjectType | EmptyObjectType,
|
||||
billingData?: ObjectType | EmptyObjectType
|
||||
) => void;
|
||||
success: (
|
||||
paymentMethodData?: ObjectType | EmptyObjectType,
|
||||
billingData?: ObjectType | EmptyObjectType,
|
||||
shippingData?: ObjectType | EmptyObjectType
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface PaymentMethodDataContextState {
|
||||
currentStatus: STATUS;
|
||||
shouldSavePaymentMethod: boolean;
|
||||
paymentMethodData: ObjectType | EmptyObjectType;
|
||||
hasSavedToken: boolean;
|
||||
errorMessage: string;
|
||||
paymentMethods: PaymentMethods;
|
||||
expressPaymentMethods: ExpressPaymentMethods;
|
||||
}
|
||||
|
||||
export type PaymentMethodCurrentStatusType = {
|
||||
// If true then the payment method state in checkout is pristine.
|
||||
isPristine: boolean;
|
||||
// If true then the payment method has been initialized and has started.
|
||||
isStarted: boolean;
|
||||
// If true then the payment method is processing payment.
|
||||
isProcessing: boolean;
|
||||
// If true then the payment method is in a finished state (which may mean it's status is either error, failed, or success).
|
||||
isFinished: boolean;
|
||||
// If true then the payment method is in an error state.
|
||||
hasError: boolean;
|
||||
// If true then the payment method has failed (usually indicates a problem with the payment method used, not logic error).
|
||||
hasFailed: boolean;
|
||||
// If true then the payment method has completed it's processing successfully.
|
||||
isSuccessful: boolean;
|
||||
// If true, an express payment is in progress.
|
||||
isDoingExpressPayment: boolean;
|
||||
};
|
||||
|
||||
export type PaymentMethodDataContextType = {
|
||||
// Sets the payment status for the payment method.
|
||||
setPaymentStatus: () => PaymentStatusDispatchers;
|
||||
// The current payment status.
|
||||
currentStatus: PaymentMethodCurrentStatusType;
|
||||
// An object of payment status constants.
|
||||
paymentStatuses: ObjectType;
|
||||
// Arbitrary data to be passed along for processing by the payment method on the server.
|
||||
paymentMethodData: ObjectType | EmptyObjectType;
|
||||
// An error message provided by the payment method if there is an error.
|
||||
errorMessage: string;
|
||||
// The active payment method slug.
|
||||
activePaymentMethod: string;
|
||||
// A function for setting the active payment method.
|
||||
setActivePaymentMethod: ( paymentMethod: string ) => void;
|
||||
// Current active token.
|
||||
activeSavedToken: string;
|
||||
// A function for setting the active payment method token.
|
||||
setActiveSavedToken: ( activeSavedToken: string ) => void;
|
||||
// Returns the customer payment for the customer if it exists.
|
||||
customerPaymentMethods:
|
||||
| Record< string, CustomerPaymentMethod >
|
||||
| EmptyObjectType;
|
||||
// Registered payment methods.
|
||||
paymentMethods: PaymentMethods;
|
||||
// Registered express payment methods.
|
||||
expressPaymentMethods: ExpressPaymentMethods;
|
||||
// True when all registered payment methods have been initialized.
|
||||
paymentMethodsInitialized: boolean;
|
||||
// True when all registered express payment methods have been initialized.
|
||||
expressPaymentMethodsInitialized: boolean;
|
||||
// Event registration callback for registering observers for the payment processing event.
|
||||
onPaymentProcessing: ReturnType< typeof emitterCallback >;
|
||||
// A function used by express payment methods to indicate an error for checkout to handle. It receives an error message string. Does not change payment status.
|
||||
setExpressPaymentError: ( error: string ) => void;
|
||||
// True if an express payment method is active.
|
||||
isExpressPaymentMethodActive: boolean;
|
||||
// A function used to set the shouldSavePayment value.
|
||||
setShouldSavePayment: ( shouldSavePayment: boolean ) => void;
|
||||
// True means that the configured payment method option is saved for the customer.
|
||||
shouldSavePayment: boolean;
|
||||
};
|
||||
|
||||
export type PaymentMethodsDispatcherType = (
|
||||
paymentMethods: PaymentMethods
|
||||
) => undefined;
|
@ -0,0 +1,38 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useState, useEffect } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useStoreEvents } from '../../../hooks/use-store-events';
|
||||
|
||||
export const useActivePaymentMethod = (): {
|
||||
activePaymentMethod: string;
|
||||
activeSavedToken: string;
|
||||
setActivePaymentMethod: React.Dispatch< React.SetStateAction< string > >;
|
||||
setActiveSavedToken: ( token: string ) => void;
|
||||
} => {
|
||||
const { dispatchCheckoutEvent } = useStoreEvents();
|
||||
|
||||
// The active payment method - e.g. Stripe CC or BACS.
|
||||
const [ activePaymentMethod, setActivePaymentMethod ] = useState( '' );
|
||||
|
||||
// If a previously saved payment method is active, the token for that method. For example, a for a Stripe CC card saved to user account.
|
||||
const [ activeSavedToken, setActiveSavedToken ] = useState( '' );
|
||||
|
||||
// Trigger event on change.
|
||||
useEffect( () => {
|
||||
dispatchCheckoutEvent( 'set-active-payment-method', {
|
||||
activePaymentMethod,
|
||||
} );
|
||||
}, [ dispatchCheckoutEvent, activePaymentMethod ] );
|
||||
|
||||
return {
|
||||
activePaymentMethod,
|
||||
activeSavedToken,
|
||||
setActivePaymentMethod,
|
||||
setActiveSavedToken,
|
||||
};
|
||||
};
|
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { useCallback, useMemo } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { actions, ActionType } from './actions';
|
||||
import { STATUS } from './constants';
|
||||
import { useCustomerDataContext } from '../customer';
|
||||
import { useShippingDataContext } from '../shipping';
|
||||
import type {
|
||||
PaymentStatusDispatchers,
|
||||
PaymentMethodDispatchers,
|
||||
} from './types';
|
||||
|
||||
export const usePaymentMethodDataDispatchers = (
|
||||
dispatch: React.Dispatch< ActionType >
|
||||
): {
|
||||
dispatchActions: PaymentMethodDispatchers;
|
||||
setPaymentStatus: () => PaymentStatusDispatchers;
|
||||
} => {
|
||||
const { setBillingData } = useCustomerDataContext();
|
||||
const { setShippingAddress } = useShippingDataContext();
|
||||
|
||||
const dispatchActions = useMemo(
|
||||
(): PaymentMethodDispatchers => ( {
|
||||
setRegisteredPaymentMethods: ( paymentMethods ) =>
|
||||
void dispatch(
|
||||
actions.setRegisteredPaymentMethods( paymentMethods )
|
||||
),
|
||||
setRegisteredExpressPaymentMethods: ( paymentMethods ) =>
|
||||
void dispatch(
|
||||
actions.setRegisteredExpressPaymentMethods( paymentMethods )
|
||||
),
|
||||
setShouldSavePayment: ( shouldSave ) =>
|
||||
void dispatch(
|
||||
actions.setShouldSavePaymentMethod( shouldSave )
|
||||
),
|
||||
} ),
|
||||
[ dispatch ]
|
||||
);
|
||||
|
||||
const setPaymentStatus = useCallback(
|
||||
(): PaymentStatusDispatchers => ( {
|
||||
pristine: () => dispatch( actions.statusOnly( STATUS.PRISTINE ) ),
|
||||
started: ( paymentMethodData ) => {
|
||||
dispatch(
|
||||
actions.started( {
|
||||
paymentMethodData,
|
||||
} )
|
||||
);
|
||||
},
|
||||
processing: () =>
|
||||
dispatch( actions.statusOnly( STATUS.PROCESSING ) ),
|
||||
completed: () => dispatch( actions.statusOnly( STATUS.COMPLETE ) ),
|
||||
error: ( errorMessage ) =>
|
||||
dispatch( actions.error( errorMessage ) ),
|
||||
failed: (
|
||||
errorMessage,
|
||||
paymentMethodData,
|
||||
billingData = undefined
|
||||
) => {
|
||||
if ( billingData ) {
|
||||
setBillingData( billingData );
|
||||
}
|
||||
dispatch(
|
||||
actions.failed( {
|
||||
errorMessage: errorMessage || '',
|
||||
paymentMethodData: paymentMethodData || {},
|
||||
} )
|
||||
);
|
||||
},
|
||||
success: (
|
||||
paymentMethodData,
|
||||
billingData = undefined,
|
||||
shippingData = undefined
|
||||
) => {
|
||||
if ( billingData ) {
|
||||
setBillingData( billingData );
|
||||
}
|
||||
if (
|
||||
typeof shippingData !== undefined &&
|
||||
shippingData?.address
|
||||
) {
|
||||
setShippingAddress(
|
||||
shippingData.address as Record< string, unknown >
|
||||
);
|
||||
}
|
||||
dispatch(
|
||||
actions.success( {
|
||||
paymentMethodData,
|
||||
} )
|
||||
);
|
||||
},
|
||||
} ),
|
||||
[ dispatch, setBillingData, setShippingAddress ]
|
||||
);
|
||||
|
||||
return {
|
||||
dispatchActions,
|
||||
setPaymentStatus,
|
||||
};
|
||||
};
|
@ -0,0 +1,229 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, sprintf } from '@wordpress/i18n';
|
||||
import {
|
||||
getPaymentMethods,
|
||||
getExpressPaymentMethods,
|
||||
} from '@woocommerce/blocks-registry';
|
||||
import { useState, useEffect, useRef, useCallback } from '@wordpress/element';
|
||||
import { useShallowEqual } from '@woocommerce/base-hooks';
|
||||
import { CURRENT_USER_IS_ADMIN, getSetting } from '@woocommerce/settings';
|
||||
import type {
|
||||
PaymentMethods,
|
||||
ExpressPaymentMethods,
|
||||
PaymentMethodConfigInstance,
|
||||
ExpressPaymentMethodConfigInstance,
|
||||
} from '@woocommerce/type-defs/payments';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { useEditorContext } from '../../editor-context';
|
||||
import { useShippingDataContext } from '../shipping';
|
||||
import { useCustomerDataContext } from '../customer';
|
||||
import { useStoreCart } from '../../../hooks/cart/use-store-cart';
|
||||
import { useStoreNotices } from '../../../hooks/use-store-notices';
|
||||
import { useEmitResponse } from '../../../hooks/use-emit-response';
|
||||
import type { PaymentMethodsDispatcherType } from './types';
|
||||
|
||||
/**
|
||||
* This hook handles initializing registered payment methods and exposing all
|
||||
* registered payment methods that can be used in the current environment (via
|
||||
* the payment method's `canMakePayment` property).
|
||||
*
|
||||
* @param {function(Object):undefined} dispatcher A dispatcher for setting registered payment methods to an external state.
|
||||
* @param {Object} registeredPaymentMethods Registered payment methods to process.
|
||||
* @param {Array} paymentMethodsSortOrder Array of payment method names to sort by. This should match keys of registeredPaymentMethods.
|
||||
* @param {string} noticeContext Id of the context to append notices to.
|
||||
*
|
||||
* @return {boolean} Whether the payment methods have been initialized or not. True when all payment methods have been initialized.
|
||||
*/
|
||||
const usePaymentMethodRegistration = (
|
||||
dispatcher: PaymentMethodsDispatcherType,
|
||||
registeredPaymentMethods: PaymentMethods | ExpressPaymentMethods,
|
||||
paymentMethodsSortOrder: string[],
|
||||
noticeContext: string
|
||||
) => {
|
||||
const [ isInitialized, setIsInitialized ] = useState( false );
|
||||
const { isEditor } = useEditorContext();
|
||||
const { selectedRates } = useShippingDataContext();
|
||||
const { billingData, shippingAddress } = useCustomerDataContext();
|
||||
const selectedShippingMethods = useShallowEqual( selectedRates );
|
||||
const paymentMethodsOrder = useShallowEqual( paymentMethodsSortOrder );
|
||||
const cart = useStoreCart();
|
||||
const { cartTotals, cartNeedsShipping, paymentRequirements } = cart;
|
||||
const canPayArgument = useRef( {
|
||||
cart,
|
||||
cartTotals,
|
||||
cartNeedsShipping,
|
||||
billingData,
|
||||
shippingAddress,
|
||||
selectedShippingMethods,
|
||||
paymentRequirements,
|
||||
} );
|
||||
const { addErrorNotice } = useStoreNotices();
|
||||
|
||||
useEffect( () => {
|
||||
canPayArgument.current = {
|
||||
cart,
|
||||
cartTotals,
|
||||
cartNeedsShipping,
|
||||
billingData,
|
||||
shippingAddress,
|
||||
selectedShippingMethods,
|
||||
paymentRequirements,
|
||||
};
|
||||
}, [
|
||||
cart,
|
||||
cartTotals,
|
||||
cartNeedsShipping,
|
||||
billingData,
|
||||
shippingAddress,
|
||||
selectedShippingMethods,
|
||||
paymentRequirements,
|
||||
] );
|
||||
|
||||
const refreshCanMakePayments = useCallback( async () => {
|
||||
let availablePaymentMethods = {};
|
||||
|
||||
const addAvailablePaymentMethod = (
|
||||
paymentMethod:
|
||||
| PaymentMethodConfigInstance
|
||||
| ExpressPaymentMethodConfigInstance
|
||||
) => {
|
||||
availablePaymentMethods = {
|
||||
...availablePaymentMethods,
|
||||
[ paymentMethod.name ]: paymentMethod,
|
||||
};
|
||||
};
|
||||
|
||||
for ( let i = 0; i < paymentMethodsOrder.length; i++ ) {
|
||||
const paymentMethodName = paymentMethodsOrder[ i ];
|
||||
const paymentMethod = registeredPaymentMethods[ paymentMethodName ];
|
||||
if ( ! paymentMethod ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// See if payment method should be available. This always evaluates to true in the editor context.
|
||||
try {
|
||||
const canPay = isEditor
|
||||
? true
|
||||
: await Promise.resolve(
|
||||
paymentMethod.canMakePayment(
|
||||
canPayArgument.current
|
||||
)
|
||||
);
|
||||
|
||||
if ( canPay ) {
|
||||
if (
|
||||
typeof canPay === 'object' &&
|
||||
canPay !== null &&
|
||||
canPay.error
|
||||
) {
|
||||
throw new Error( canPay.error.message );
|
||||
}
|
||||
|
||||
addAvailablePaymentMethod( paymentMethod );
|
||||
}
|
||||
} catch ( e ) {
|
||||
if ( CURRENT_USER_IS_ADMIN || isEditor ) {
|
||||
const errorText = sprintf(
|
||||
/* translators: %s the id of the payment method being registered (bank transfer, Stripe...) */
|
||||
__(
|
||||
`There was an error registering the payment method with id '%s': `,
|
||||
'woo-gutenberg-products-block'
|
||||
),
|
||||
paymentMethod.paymentMethodId
|
||||
);
|
||||
addErrorNotice( `${ errorText } ${ e }`, {
|
||||
context: noticeContext,
|
||||
id: `wc-${ paymentMethod.paymentMethodId }-registration-error`,
|
||||
} );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-dispatch available payment methods to store.
|
||||
dispatcher( availablePaymentMethods );
|
||||
|
||||
// Note: some payment methods use the `canMakePayment` callback to initialize / setup.
|
||||
// Example: Stripe CC, Stripe Payment Request.
|
||||
// That's why we track "is initialized" state here.
|
||||
setIsInitialized( true );
|
||||
}, [
|
||||
addErrorNotice,
|
||||
dispatcher,
|
||||
isEditor,
|
||||
noticeContext,
|
||||
paymentMethodsOrder,
|
||||
registeredPaymentMethods,
|
||||
] );
|
||||
|
||||
const [ debouncedRefreshCanMakePayments ] = useDebouncedCallback(
|
||||
refreshCanMakePayments,
|
||||
500
|
||||
);
|
||||
|
||||
// Determine which payment methods are available initially and whenever
|
||||
// shipping methods, cart or the billing data change.
|
||||
// Some payment methods (e.g. COD) can be disabled for specific shipping methods.
|
||||
useEffect( () => {
|
||||
debouncedRefreshCanMakePayments();
|
||||
}, [
|
||||
debouncedRefreshCanMakePayments,
|
||||
cart,
|
||||
selectedShippingMethods,
|
||||
billingData,
|
||||
] );
|
||||
|
||||
return isInitialized;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook for setting up payment methods (standard, non-express).
|
||||
*
|
||||
* @param {function(Object):undefined} dispatcher
|
||||
*
|
||||
* @return {boolean} True when standard payment methods have been initialized.
|
||||
*/
|
||||
export const usePaymentMethods = (
|
||||
dispatcher: PaymentMethodsDispatcherType
|
||||
): boolean => {
|
||||
const standardMethods: PaymentMethods = getPaymentMethods() as PaymentMethods;
|
||||
const { noticeContexts } = useEmitResponse();
|
||||
// Ensure all methods are present in order.
|
||||
// Some payment methods may not be present in paymentGatewaySortOrder if they
|
||||
// depend on state, e.g. COD can depend on shipping method.
|
||||
const displayOrder = new Set( [
|
||||
...( getSetting( 'paymentGatewaySortOrder', [] ) as [ ] ),
|
||||
...Object.keys( standardMethods ),
|
||||
] );
|
||||
return usePaymentMethodRegistration(
|
||||
dispatcher,
|
||||
standardMethods,
|
||||
Array.from( displayOrder ),
|
||||
noticeContexts.PAYMENTS
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook for setting up express payment methods.
|
||||
*
|
||||
* @param {function(Object):undefined} dispatcher
|
||||
*
|
||||
* @return {boolean} True when express payment methods have been initialized.
|
||||
*/
|
||||
export const useExpressPaymentMethods = (
|
||||
dispatcher: PaymentMethodsDispatcherType
|
||||
): boolean => {
|
||||
const expressMethods: ExpressPaymentMethods = getExpressPaymentMethods() as ExpressPaymentMethods;
|
||||
const { noticeContexts } = useEmitResponse();
|
||||
return usePaymentMethodRegistration(
|
||||
dispatcher,
|
||||
expressMethods,
|
||||
Object.keys( expressMethods ),
|
||||
noticeContexts.EXPRESS_PAYMENTS
|
||||
);
|
||||
};
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import type { PaymentMethods, CustomerPaymentMethod } from './types';
|
||||
|
||||
/**
|
||||
* Gets the payment methods saved for the current user after filtering out disabled ones.
|
||||
*/
|
||||
export const getCustomerPaymentMethods = (
|
||||
availablePaymentMethods: PaymentMethods = {}
|
||||
): Record< string, CustomerPaymentMethod > => {
|
||||
if ( Object.keys( availablePaymentMethods ).length === 0 ) {
|
||||
return {};
|
||||
}
|
||||
const customerPaymentMethods = getSetting( 'customerPaymentMethods', {} );
|
||||
const paymentMethodKeys = Object.keys( customerPaymentMethods );
|
||||
const enabledCustomerPaymentMethods = {} as Record<
|
||||
string,
|
||||
CustomerPaymentMethod
|
||||
>;
|
||||
paymentMethodKeys.forEach( ( type ) => {
|
||||
const methods = customerPaymentMethods[ type ].filter(
|
||||
( {
|
||||
method: { gateway },
|
||||
}: {
|
||||
method: {
|
||||
gateway: string;
|
||||
};
|
||||
} ) =>
|
||||
gateway in availablePaymentMethods &&
|
||||
availablePaymentMethods[ gateway ].supports?.showSavedCards
|
||||
);
|
||||
if ( methods.length ) {
|
||||
enabledCustomerPaymentMethods[ type ] = methods;
|
||||
}
|
||||
} );
|
||||
return enabledCustomerPaymentMethods;
|
||||
};
|
Reference in New Issue
Block a user