Move payment method registration state to payment data context and add saved payment methods handling (https://github.com/woocommerce/woocommerce-blocks/pull/2029)
* Add saved-payment-method options handling and improve payment method registration initialization * add server side exposure of saved customer payment methods * fix reducer for express payment method state * fix default for customerPaymentMethods
This commit is contained in:
parent
7921c3e5ba
commit
690f61ec93
|
@ -10,15 +10,20 @@ import {
|
||||||
cloneElement,
|
cloneElement,
|
||||||
useRef,
|
useRef,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useState,
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { useCheckoutContext } from '@woocommerce/base-context';
|
import {
|
||||||
|
useCheckoutContext,
|
||||||
|
usePaymentMethodDataContext,
|
||||||
|
} from '@woocommerce/base-context';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import Tabs from '../tabs';
|
import Tabs from '../tabs';
|
||||||
import NoPaymentMethods from './no-payment-methods/index';
|
import NoPaymentMethods from './no-payment-methods';
|
||||||
|
import SavedPaymentMethodOptions from './saved-payment-method-options';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a payment method for the given context.
|
* Returns a payment method for the given context.
|
||||||
|
@ -46,6 +51,7 @@ const getPaymentMethod = ( id, paymentMethods, isEditor ) => {
|
||||||
*/
|
*/
|
||||||
const PaymentMethods = () => {
|
const PaymentMethods = () => {
|
||||||
const { isEditor } = useCheckoutContext();
|
const { isEditor } = useCheckoutContext();
|
||||||
|
const { customerPaymentMethods = {} } = usePaymentMethodDataContext();
|
||||||
const { isInitialized, paymentMethods } = usePaymentMethods();
|
const { isInitialized, paymentMethods } = usePaymentMethods();
|
||||||
const currentPaymentMethods = useRef( paymentMethods );
|
const currentPaymentMethods = useRef( paymentMethods );
|
||||||
const {
|
const {
|
||||||
|
@ -54,6 +60,7 @@ const PaymentMethods = () => {
|
||||||
...paymentMethodInterface
|
...paymentMethodInterface
|
||||||
} = usePaymentMethodInterface();
|
} = usePaymentMethodInterface();
|
||||||
const currentPaymentMethodInterface = useRef( paymentMethodInterface );
|
const currentPaymentMethodInterface = useRef( paymentMethodInterface );
|
||||||
|
const [ selectedToken, setSelectedToken ] = useState( 0 );
|
||||||
|
|
||||||
// update ref on change.
|
// update ref on change.
|
||||||
useEffect( () => {
|
useEffect( () => {
|
||||||
|
@ -78,16 +85,20 @@ const PaymentMethods = () => {
|
||||||
[ isEditor, activePaymentMethod ]
|
[ isEditor, activePaymentMethod ]
|
||||||
);
|
);
|
||||||
|
|
||||||
if ( ! isInitialized || Object.keys( paymentMethods ).length === 0 ) {
|
if (
|
||||||
|
! isInitialized ||
|
||||||
|
Object.keys( currentPaymentMethods.current ).length === 0
|
||||||
|
) {
|
||||||
return <NoPaymentMethods />;
|
return <NoPaymentMethods />;
|
||||||
}
|
}
|
||||||
|
const renderedTabs = (
|
||||||
return (
|
|
||||||
<Tabs
|
<Tabs
|
||||||
className="wc-block-components-checkout-payment-methods"
|
className="wc-block-components-checkout-payment-methods"
|
||||||
onSelect={ ( tabName ) => setActivePaymentMethod( tabName ) }
|
onSelect={ ( tabName ) => setActivePaymentMethod( tabName ) }
|
||||||
tabs={ Object.keys( paymentMethods ).map( ( id ) => {
|
tabs={ Object.keys( currentPaymentMethods.current ).map( ( id ) => {
|
||||||
const { label, ariaLabel } = paymentMethods[ id ];
|
const { label, ariaLabel } = currentPaymentMethods.current[
|
||||||
|
id
|
||||||
|
];
|
||||||
return {
|
return {
|
||||||
name: id,
|
name: id,
|
||||||
title: () => label,
|
title: () => label,
|
||||||
|
@ -103,6 +114,22 @@ const PaymentMethods = () => {
|
||||||
{ getRenderedTab() }
|
{ getRenderedTab() }
|
||||||
</Tabs>
|
</Tabs>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderedSavedPaymentOptions = (
|
||||||
|
<SavedPaymentMethodOptions onSelect={ setSelectedToken } />
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderedTabsAndSavedPaymentOptions = (
|
||||||
|
<>
|
||||||
|
{ renderedSavedPaymentOptions }
|
||||||
|
{ renderedTabs }
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return Object.keys( customerPaymentMethods ).length > 0 &&
|
||||||
|
selectedToken !== 0
|
||||||
|
? renderedSavedPaymentOptions
|
||||||
|
: renderedTabsAndSavedPaymentOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PaymentMethods;
|
export default PaymentMethods;
|
||||||
|
|
|
@ -0,0 +1,137 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { useEffect, useState, useRef, useCallback } from '@wordpress/element';
|
||||||
|
import { __, sprintf } from '@wordpress/i18n';
|
||||||
|
import { usePaymentMethodDataContext } from '@woocommerce/base-context';
|
||||||
|
import RadioControl from '@woocommerce/base-components/radio-control';
|
||||||
|
|
||||||
|
/** @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the option object for a cc or echeck saved payment method token.
|
||||||
|
*
|
||||||
|
* @param {CustomerPaymentMethod} savedPaymentMethod
|
||||||
|
*
|
||||||
|
* @return {Object} An option objects to use for RadioControl.
|
||||||
|
*/
|
||||||
|
const getCcOrEcheckPaymentMethodOption = ( { method, expires, tokenId } ) => {
|
||||||
|
return {
|
||||||
|
value: tokenId,
|
||||||
|
label: sprintf(
|
||||||
|
__(
|
||||||
|
'%1$s ending in %2$s (expires %3$s)',
|
||||||
|
'woo-gutenberg-product-blocks'
|
||||||
|
),
|
||||||
|
method.brand,
|
||||||
|
method.last4,
|
||||||
|
expires
|
||||||
|
),
|
||||||
|
name: `wc-saved-payment-method-token-${ tokenId }`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the option object for any non specific saved payment method.
|
||||||
|
*
|
||||||
|
* @param {CustomerPaymentMethod} savedPaymentMethod
|
||||||
|
*
|
||||||
|
* @return {Object} An option objects to use for RadioControl.
|
||||||
|
*/
|
||||||
|
const getDefaultPaymentMethodOptions = ( { method, tokenId } ) => {
|
||||||
|
return {
|
||||||
|
value: tokenId,
|
||||||
|
label: sprintf(
|
||||||
|
__( 'Saved token for %s', 'woo-gutenberg-products-block' ),
|
||||||
|
method.gateway
|
||||||
|
),
|
||||||
|
name: `wc-saved-payment-method-token-${ tokenId }`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const SavedPaymentMethodOptions = ( { onSelect } ) => {
|
||||||
|
const {
|
||||||
|
setPaymentStatus,
|
||||||
|
customerPaymentMethods,
|
||||||
|
activePaymentMethod,
|
||||||
|
} = usePaymentMethodDataContext();
|
||||||
|
const [ selectedToken, setSelectedToken ] = useState( null );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Object} Options
|
||||||
|
* @property {Array} current The current options on the type.
|
||||||
|
*/
|
||||||
|
const currentOptions = useRef( [] );
|
||||||
|
useEffect( () => {
|
||||||
|
let options = [];
|
||||||
|
const paymentMethodKeys = Object.keys( customerPaymentMethods );
|
||||||
|
if ( paymentMethodKeys.length > 0 ) {
|
||||||
|
paymentMethodKeys.forEach( ( type ) => {
|
||||||
|
const paymentMethods = customerPaymentMethods[ type ];
|
||||||
|
if ( paymentMethods.length > 0 ) {
|
||||||
|
options = options.concat(
|
||||||
|
paymentMethods.map( ( paymentMethod ) => {
|
||||||
|
if (
|
||||||
|
paymentMethod.is_default &&
|
||||||
|
selectedToken === null
|
||||||
|
) {
|
||||||
|
setSelectedToken(
|
||||||
|
parseInt( paymentMethod.tokenId, 10 )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return type === 'cc' || type === 'echeck'
|
||||||
|
? getCcOrEcheckPaymentMethodOption(
|
||||||
|
paymentMethod
|
||||||
|
)
|
||||||
|
: getDefaultPaymentMethodOptions(
|
||||||
|
paymentMethod
|
||||||
|
);
|
||||||
|
} )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} );
|
||||||
|
currentOptions.current = options;
|
||||||
|
currentOptions.current.push( {
|
||||||
|
value: 0,
|
||||||
|
label: __(
|
||||||
|
'Use a new payment method',
|
||||||
|
'woo-gutenberg-product-blocks'
|
||||||
|
),
|
||||||
|
name: `wc-saved-payment-method-token-new`,
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}, [ customerPaymentMethods, selectedToken ] );
|
||||||
|
const updateToken = useCallback(
|
||||||
|
( token ) => {
|
||||||
|
if ( token !== 0 ) {
|
||||||
|
// @todo this will need updated when the signature changes to
|
||||||
|
// allow no billing data included.
|
||||||
|
setPaymentStatus().success( null, {
|
||||||
|
payment_method: activePaymentMethod,
|
||||||
|
savedPaymentMethodToken: token,
|
||||||
|
} );
|
||||||
|
} else {
|
||||||
|
setPaymentStatus().started();
|
||||||
|
}
|
||||||
|
setSelectedToken( parseInt( token, 10 ) );
|
||||||
|
onSelect( parseInt( token, 10 ) );
|
||||||
|
},
|
||||||
|
[ setSelectedToken, setPaymentStatus, onSelect ]
|
||||||
|
);
|
||||||
|
useEffect( () => {
|
||||||
|
if ( selectedToken && currentOptions.current.length > 0 ) {
|
||||||
|
updateToken( selectedToken );
|
||||||
|
}
|
||||||
|
}, [ selectedToken, updateToken ] );
|
||||||
|
|
||||||
|
return currentOptions.current.length > 0 ? (
|
||||||
|
<RadioControl
|
||||||
|
id={ 'wc-payment-method-stripe-saved-tokens' }
|
||||||
|
selected={ selectedToken }
|
||||||
|
onChange={ updateToken }
|
||||||
|
options={ currentOptions.current }
|
||||||
|
/>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SavedPaymentMethodOptions;
|
|
@ -1,13 +1,19 @@
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { STATUS } from './constants';
|
import { ACTION_TYPES } from './constants';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@woocommerce/type-defs/cart').CartBillingData} CartBillingData
|
* @typedef {import('@woocommerce/type-defs/cart').CartBillingData} CartBillingData
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const { ERROR, FAILED, SUCCESS } = STATUS;
|
const {
|
||||||
|
ERROR,
|
||||||
|
FAILED,
|
||||||
|
SUCCESS,
|
||||||
|
SET_REGISTERED_PAYMENT_METHOD,
|
||||||
|
SET_REGISTERED_EXPRESS_PAYMENT_METHOD,
|
||||||
|
} = ACTION_TYPES;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to dispatch a status update only for the given type.
|
* Used to dispatch a status update only for the given type.
|
||||||
|
@ -80,3 +86,27 @@ export const success = ( { billingData, paymentMethodData } ) => ( {
|
||||||
billingData,
|
billingData,
|
||||||
paymentMethodData,
|
paymentMethodData,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to dispatch an action for updating a registered payment method in the
|
||||||
|
* state.
|
||||||
|
*
|
||||||
|
* @param {Object} paymentMethod Payment method to register.
|
||||||
|
* @return {Object} An action object.
|
||||||
|
*/
|
||||||
|
export const setRegisteredPaymentMethod = ( paymentMethod ) => ( {
|
||||||
|
type: SET_REGISTERED_PAYMENT_METHOD,
|
||||||
|
paymentMethod,
|
||||||
|
} );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to dispatch an action for updating a registered express payment
|
||||||
|
* method in the state.
|
||||||
|
*
|
||||||
|
* @param {Object} paymentMethod Payment method to register.
|
||||||
|
* @return {Object} An action object.
|
||||||
|
*/
|
||||||
|
export const setRegisteredExpressPaymentMethod = ( paymentMethod ) => ( {
|
||||||
|
type: SET_REGISTERED_EXPRESS_PAYMENT_METHOD,
|
||||||
|
paymentMethod,
|
||||||
|
} );
|
||||||
|
|
|
@ -12,6 +12,13 @@ export const STATUS = {
|
||||||
COMPLETE: 'complete',
|
COMPLETE: 'complete',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ACTION_TYPES = {
|
||||||
|
...STATUS,
|
||||||
|
SET_REGISTERED_PAYMENT_METHOD: 'set_registered_payment_method',
|
||||||
|
SET_REGISTERED_EXPRESS_PAYMENT_METHOD:
|
||||||
|
'set_registered_express_payment_method',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @todo do typedefs for the payment event state.
|
* @todo do typedefs for the payment event state.
|
||||||
*/
|
*/
|
||||||
|
@ -25,6 +32,8 @@ export const DEFAULT_PAYMENT_DATA = {
|
||||||
// processing server side.
|
// processing server side.
|
||||||
},
|
},
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
|
paymentMethods: {},
|
||||||
|
expressPaymentMethods: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,4 +55,9 @@ export const DEFAULT_PAYMENT_METHOD_DATA = {
|
||||||
errorMessage: '',
|
errorMessage: '',
|
||||||
activePaymentMethod: '',
|
activePaymentMethod: '',
|
||||||
setActivePaymentMethod: () => void null,
|
setActivePaymentMethod: () => void null,
|
||||||
|
customerPaymentMethods: {},
|
||||||
|
paymentMethods: {},
|
||||||
|
expressPaymentMethods: {},
|
||||||
|
paymentMethodsInitialized: false,
|
||||||
|
expressPaymentMethodsInitialized: false,
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,7 +7,18 @@ import {
|
||||||
DEFAULT_PAYMENT_METHOD_DATA,
|
DEFAULT_PAYMENT_METHOD_DATA,
|
||||||
} from './constants';
|
} from './constants';
|
||||||
import reducer from './reducer';
|
import reducer from './reducer';
|
||||||
import { statusOnly, error, failed, success } from './actions';
|
import {
|
||||||
|
statusOnly,
|
||||||
|
error,
|
||||||
|
failed,
|
||||||
|
success,
|
||||||
|
setRegisteredPaymentMethod,
|
||||||
|
setRegisteredExpressPaymentMethod,
|
||||||
|
} from './actions';
|
||||||
|
import {
|
||||||
|
usePaymentMethods,
|
||||||
|
useExpressPaymentMethods,
|
||||||
|
} from './use-payment-method-registration';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
|
@ -17,13 +28,17 @@ import {
|
||||||
useContext,
|
useContext,
|
||||||
useState,
|
useState,
|
||||||
useReducer,
|
useReducer,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
} from '@wordpress/element';
|
} from '@wordpress/element';
|
||||||
|
import { getSetting } from '@woocommerce/settings';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('@woocommerce/type-defs/contexts').PaymentMethodDataContext} PaymentMethodDataContext
|
* @typedef {import('@woocommerce/type-defs/contexts').PaymentMethodDataContext} PaymentMethodDataContext
|
||||||
* @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatch} PaymentStatusDispatch
|
* @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatch} PaymentStatusDispatch
|
||||||
* @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatchers} PaymentStatusDispatchers
|
* @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatchers} PaymentStatusDispatchers
|
||||||
* @typedef {import('@woocommerce/type-defs/cart').CartBillingData} CartBillingData
|
* @typedef {import('@woocommerce/type-defs/cart').CartBillingData} CartBillingData
|
||||||
|
* @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -46,9 +61,6 @@ export const usePaymentMethodDataContext = () => {
|
||||||
return useContext( PaymentMethodDataContext );
|
return useContext( PaymentMethodDataContext );
|
||||||
};
|
};
|
||||||
|
|
||||||
// @todo implement billing (and payment method) data saved to the cart data
|
|
||||||
// store. That way checkout can obtain it for processing the order.
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PaymentMethodDataProvider is automatically included in the
|
* PaymentMethodDataProvider is automatically included in the
|
||||||
* CheckoutDataProvider.
|
* CheckoutDataProvider.
|
||||||
|
@ -69,6 +81,7 @@ export const PaymentMethodDataProvider = ( {
|
||||||
const [ activePaymentMethod, setActive ] = useState(
|
const [ activePaymentMethod, setActive ] = useState(
|
||||||
initialActivePaymentMethod
|
initialActivePaymentMethod
|
||||||
);
|
);
|
||||||
|
const customerPaymentMethods = getSetting( 'customerPaymentMethods', {} );
|
||||||
const [ paymentStatus, dispatch ] = useReducer(
|
const [ paymentStatus, dispatch ] = useReducer(
|
||||||
reducer,
|
reducer,
|
||||||
DEFAULT_PAYMENT_DATA
|
DEFAULT_PAYMENT_DATA
|
||||||
|
@ -77,43 +90,71 @@ export const PaymentMethodDataProvider = ( {
|
||||||
setActive( paymentMethodSlug );
|
setActive( paymentMethodSlug );
|
||||||
dispatch( statusOnly( PRISTINE ) );
|
dispatch( statusOnly( PRISTINE ) );
|
||||||
};
|
};
|
||||||
|
const paymentMethodsInitialized = usePaymentMethods( ( paymentMethod ) =>
|
||||||
|
dispatch( setRegisteredPaymentMethod( paymentMethod ) )
|
||||||
|
);
|
||||||
|
const expressPaymentMethodsInitialized = useExpressPaymentMethods(
|
||||||
|
( paymentMethod ) => {
|
||||||
|
dispatch( setRegisteredExpressPaymentMethod( paymentMethod ) );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
// set initial active payment method if it's undefined.
|
||||||
|
useEffect( () => {
|
||||||
|
const paymentMethodKeys = Object.keys( paymentStatus.paymentMethods );
|
||||||
|
if (
|
||||||
|
paymentMethodsInitialized &&
|
||||||
|
! activePaymentMethod &&
|
||||||
|
paymentMethodKeys.length > 0
|
||||||
|
) {
|
||||||
|
setActivePaymentMethod(
|
||||||
|
Object.keys( paymentStatus.paymentMethods )[ 0 ]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
activePaymentMethod,
|
||||||
|
paymentMethodsInitialized,
|
||||||
|
paymentStatus.paymentMethods,
|
||||||
|
] );
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {PaymentStatusDispatch}
|
* @type {PaymentStatusDispatch}
|
||||||
*/
|
*/
|
||||||
const setPaymentStatus = () => ( {
|
const setPaymentStatus = useCallback(
|
||||||
started: () => dispatch( statusOnly( STARTED ) ),
|
() => ( {
|
||||||
processing: () => dispatch( statusOnly( PROCESSING ) ),
|
started: () => dispatch( statusOnly( STARTED ) ),
|
||||||
completed: () => dispatch( statusOnly( COMPLETE ) ),
|
processing: () => dispatch( statusOnly( PROCESSING ) ),
|
||||||
/**
|
completed: () => dispatch( statusOnly( COMPLETE ) ),
|
||||||
* @param {string} errorMessage An error message
|
/**
|
||||||
*/
|
* @param {string} errorMessage An error message
|
||||||
error: ( errorMessage ) => dispatch( error( errorMessage ) ),
|
*/
|
||||||
/**
|
error: ( errorMessage ) => dispatch( error( errorMessage ) ),
|
||||||
* @param {string} errorMessage An error message
|
/**
|
||||||
* @param {CartBillingData} billingData The billing data accompanying the payment method
|
* @param {string} errorMessage An error message
|
||||||
* @param {Object} paymentMethodData Arbitrary payment method data to accompany the checkout submission.
|
* @param {CartBillingData} billingData The billing data accompanying the payment method
|
||||||
*/
|
* @param {Object} paymentMethodData Arbitrary payment method data to accompany the checkout submission.
|
||||||
failed: ( errorMessage, billingData, paymentMethodData ) =>
|
*/
|
||||||
dispatch(
|
failed: ( errorMessage, billingData, paymentMethodData ) =>
|
||||||
failed( {
|
dispatch(
|
||||||
errorMessage,
|
failed( {
|
||||||
billingData,
|
errorMessage,
|
||||||
paymentMethodData,
|
billingData,
|
||||||
} )
|
paymentMethodData,
|
||||||
),
|
} )
|
||||||
/**
|
),
|
||||||
* @param {CartBillingData} billingData The billing data accompanying the payment method.
|
/**
|
||||||
* @param {Object} paymentMethodData Arbitrary payment method data to accompany the checkout.
|
* @param {CartBillingData} billingData The billing data accompanying the payment method.
|
||||||
*/
|
* @param {Object} paymentMethodData Arbitrary payment method data to accompany the checkout.
|
||||||
success: ( billingData, paymentMethodData ) =>
|
*/
|
||||||
dispatch(
|
success: ( billingData, paymentMethodData ) =>
|
||||||
success( {
|
dispatch(
|
||||||
billingData,
|
success( {
|
||||||
paymentMethodData,
|
billingData,
|
||||||
} )
|
paymentMethodData,
|
||||||
),
|
} )
|
||||||
} );
|
),
|
||||||
|
} ),
|
||||||
|
[ dispatch ]
|
||||||
|
);
|
||||||
|
|
||||||
const currentStatus = {
|
const currentStatus = {
|
||||||
isPristine: paymentStatus === PRISTINE,
|
isPristine: paymentStatus === PRISTINE,
|
||||||
|
@ -135,6 +176,11 @@ export const PaymentMethodDataProvider = ( {
|
||||||
errorMessage: paymentStatus.errorMessage,
|
errorMessage: paymentStatus.errorMessage,
|
||||||
activePaymentMethod,
|
activePaymentMethod,
|
||||||
setActivePaymentMethod,
|
setActivePaymentMethod,
|
||||||
|
customerPaymentMethods,
|
||||||
|
paymentMethods: paymentStatus.paymentMethods,
|
||||||
|
expressPaymentMethods: paymentStatus.expressPaymentMethods,
|
||||||
|
paymentMethodsInitialized,
|
||||||
|
expressPaymentMethodsInitialized,
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<PaymentMethodDataContext.Provider value={ paymentData }>
|
<PaymentMethodDataContext.Provider value={ paymentData }>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* Internal dependencies
|
||||||
*/
|
*/
|
||||||
import { STATUS, DEFAULT_PAYMENT_DATA } from './constants';
|
import { ACTION_TYPES, DEFAULT_PAYMENT_DATA } from './constants';
|
||||||
const {
|
const {
|
||||||
STARTED,
|
STARTED,
|
||||||
ERROR,
|
ERROR,
|
||||||
|
@ -10,9 +10,9 @@ const {
|
||||||
PROCESSING,
|
PROCESSING,
|
||||||
PRISTINE,
|
PRISTINE,
|
||||||
COMPLETE,
|
COMPLETE,
|
||||||
} = STATUS;
|
SET_REGISTERED_PAYMENT_METHOD,
|
||||||
|
SET_REGISTERED_EXPRESS_PAYMENT_METHOD,
|
||||||
const SET_BILLING_DATA = 'set_billing_data';
|
} = ACTION_TYPES;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reducer for payment data state
|
* Reducer for payment data state
|
||||||
|
@ -22,7 +22,7 @@ const SET_BILLING_DATA = 'set_billing_data';
|
||||||
*/
|
*/
|
||||||
const reducer = (
|
const reducer = (
|
||||||
state = DEFAULT_PAYMENT_DATA,
|
state = DEFAULT_PAYMENT_DATA,
|
||||||
{ type, billingData, paymentMethodData, errorMessage }
|
{ type, billingData, paymentMethodData, errorMessage, paymentMethod }
|
||||||
) => {
|
) => {
|
||||||
switch ( type ) {
|
switch ( type ) {
|
||||||
case STARTED:
|
case STARTED:
|
||||||
|
@ -66,11 +66,29 @@ const reducer = (
|
||||||
return {
|
return {
|
||||||
...DEFAULT_PAYMENT_DATA,
|
...DEFAULT_PAYMENT_DATA,
|
||||||
currentStatus: PRISTINE,
|
currentStatus: PRISTINE,
|
||||||
|
// keep payment method registration state
|
||||||
|
paymentMethods: {
|
||||||
|
...state.paymentMethods,
|
||||||
|
},
|
||||||
|
expressPaymentMethods: {
|
||||||
|
...state.expressPaymentMethods,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
case SET_BILLING_DATA:
|
case SET_REGISTERED_PAYMENT_METHOD:
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
billingData,
|
paymentMethods: {
|
||||||
|
...state.paymentMethods,
|
||||||
|
[ paymentMethod.id ]: paymentMethod,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
case SET_REGISTERED_EXPRESS_PAYMENT_METHOD:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
expressPaymentMethods: {
|
||||||
|
...state.expressPaymentMethods,
|
||||||
|
[ paymentMethod.id ]: paymentMethod,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
getPaymentMethods,
|
||||||
|
getExpressPaymentMethods,
|
||||||
|
} from '@woocommerce/blocks-registry';
|
||||||
|
import { useState, useEffect, useRef } from '@wordpress/element';
|
||||||
|
import { useCheckoutContext } from '@woocommerce/base-context';
|
||||||
|
|
||||||
|
const usePaymentMethodRegistration = (
|
||||||
|
dispatcher,
|
||||||
|
registeredPaymentMethods
|
||||||
|
) => {
|
||||||
|
const { isEditor } = useCheckoutContext();
|
||||||
|
const [ isInitialized, setIsInitialized ] = useState( false );
|
||||||
|
const countPaymentMethodsInitializing = useRef(
|
||||||
|
Object.keys( registeredPaymentMethods ).length
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect( () => {
|
||||||
|
// if all payment methods are initialized then bail.
|
||||||
|
if ( isInitialized ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const updatePaymentMethods = ( current, canPay = true ) => {
|
||||||
|
if ( canPay ) {
|
||||||
|
dispatcher( current );
|
||||||
|
}
|
||||||
|
// update the initialized count
|
||||||
|
countPaymentMethodsInitializing.current--;
|
||||||
|
// if count remaining less than 1, then set initialized.
|
||||||
|
if ( countPaymentMethodsInitializing.current < 1 ) {
|
||||||
|
setIsInitialized( true );
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// loop through payment methods and see what the state is
|
||||||
|
for ( const paymentMethodId in registeredPaymentMethods ) {
|
||||||
|
const current = registeredPaymentMethods[ paymentMethodId ];
|
||||||
|
// if in editor context then we bypass can pay check.
|
||||||
|
if ( isEditor ) {
|
||||||
|
updatePaymentMethods( current );
|
||||||
|
} else {
|
||||||
|
current.canMakePayment
|
||||||
|
.then( ( canPay ) => {
|
||||||
|
updatePaymentMethods( current, canPay );
|
||||||
|
} )
|
||||||
|
.catch( ( error ) => {
|
||||||
|
// @todo, would be a good place to use the checkout error
|
||||||
|
// hooks here? Or maybe throw and catch by error boundary?
|
||||||
|
throw new Error(
|
||||||
|
'Problem with payment method initialization' +
|
||||||
|
( error.message || '' )
|
||||||
|
);
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [ isInitialized, isEditor ] );
|
||||||
|
|
||||||
|
return isInitialized;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePaymentMethods = ( dispatcher ) =>
|
||||||
|
usePaymentMethodRegistration( dispatcher, getPaymentMethods() );
|
||||||
|
export const useExpressPaymentMethods = ( dispatcher ) =>
|
||||||
|
usePaymentMethodRegistration( dispatcher, getExpressPaymentMethods() );
|
|
@ -1,59 +1,22 @@
|
||||||
/**
|
/**
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import {
|
import { usePaymentMethodDataContext } from '@woocommerce/base-context';
|
||||||
getPaymentMethods,
|
|
||||||
getExpressPaymentMethods,
|
|
||||||
} from '@woocommerce/blocks-registry';
|
|
||||||
import { useState, useEffect, useRef } from '@wordpress/element';
|
|
||||||
|
|
||||||
const usePaymentMethodState = ( registeredPaymentMethods ) => {
|
const usePaymentMethodState = ( express = false ) => {
|
||||||
const [ paymentMethods, setPaymentMethods ] = useState( [] );
|
const {
|
||||||
const [ isInitialized, setIsInitialized ] = useState( false );
|
paymentMethods,
|
||||||
const countPaymentMethodsInitializing = useRef(
|
expressPaymentMethods,
|
||||||
Object.keys( registeredPaymentMethods ).length
|
paymentMethodsInitialized,
|
||||||
);
|
expressPaymentMethodsInitialized,
|
||||||
|
} = usePaymentMethodDataContext();
|
||||||
useEffect( () => {
|
return express
|
||||||
// if all payment methods are initialized then bail.
|
? {
|
||||||
if ( isInitialized ) {
|
paymentMethods: expressPaymentMethods,
|
||||||
return;
|
isInitialized: expressPaymentMethodsInitialized,
|
||||||
}
|
}
|
||||||
// loop through payment methods and see what the state is
|
: { paymentMethods, isInitialized: paymentMethodsInitialized };
|
||||||
for ( const paymentMethodId in registeredPaymentMethods ) {
|
|
||||||
const current = registeredPaymentMethods[ paymentMethodId ];
|
|
||||||
current.canMakePayment
|
|
||||||
.then( ( canPay ) => {
|
|
||||||
if ( canPay ) {
|
|
||||||
setPaymentMethods( ( previousPaymentMethods ) => {
|
|
||||||
return {
|
|
||||||
...previousPaymentMethods,
|
|
||||||
[ current.id ]: current,
|
|
||||||
};
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
// update the initialized count
|
|
||||||
countPaymentMethodsInitializing.current--;
|
|
||||||
// if count remaining less than 1, then set initialized.
|
|
||||||
if ( countPaymentMethodsInitializing.current < 1 ) {
|
|
||||||
setIsInitialized( true );
|
|
||||||
}
|
|
||||||
} )
|
|
||||||
.catch( ( error ) => {
|
|
||||||
// @todo, would be a good place to use the checkout error
|
|
||||||
// hooks here? Or maybe throw and catch by error boundary?
|
|
||||||
throw new Error(
|
|
||||||
'Problem with payment method initialization' +
|
|
||||||
( error.message || '' )
|
|
||||||
);
|
|
||||||
} );
|
|
||||||
}
|
|
||||||
}, [ isInitialized ] );
|
|
||||||
|
|
||||||
return { paymentMethods, isInitialized };
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePaymentMethods = () =>
|
export const usePaymentMethods = () => usePaymentMethodState();
|
||||||
usePaymentMethodState( getPaymentMethods() );
|
export const useExpressPaymentMethods = () => usePaymentMethodState( true );
|
||||||
export const useExpressPaymentMethods = () =>
|
|
||||||
usePaymentMethodState( getExpressPaymentMethods() );
|
|
||||||
|
|
|
@ -92,6 +92,30 @@
|
||||||
* completed it's processing successfully.
|
* completed it's processing successfully.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A saved customer payment method object (if exists)
|
||||||
|
*
|
||||||
|
* @typedef {Object} CustomerPaymentMethod
|
||||||
|
*
|
||||||
|
* @property {Object} method The payment method object (varies on what it
|
||||||
|
* might contain)
|
||||||
|
* @property {string} expires Short form of expiry for payment method.
|
||||||
|
* @property {boolean} is_default Whether it is the default payment method of
|
||||||
|
* the customer or not.
|
||||||
|
* @property {number} tokenId The id of the saved payment method.
|
||||||
|
* @property {Object} actions Varies, actions that can be done to interact
|
||||||
|
* with the payment method.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Saved Customer Payment methods object
|
||||||
|
*
|
||||||
|
* This is an object where the keys are payment gateway slugs and the values
|
||||||
|
* Are an array of CustomerPaymentMethod objects.
|
||||||
|
*
|
||||||
|
* @typedef {Object} SavedCustomerPaymentMethods
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} PaymentStatusDispatchers
|
* @typedef {Object} PaymentStatusDispatchers
|
||||||
*
|
*
|
||||||
|
@ -110,34 +134,38 @@
|
||||||
/**
|
/**
|
||||||
* @typedef {Object} PaymentMethodDataContext
|
* @typedef {Object} PaymentMethodDataContext
|
||||||
*
|
*
|
||||||
* @property {PaymentStatusDispatch} setPaymentStatus Sets the
|
* @property {PaymentStatusDispatch} setPaymentStatus Sets the payment status
|
||||||
* payment status
|
* for the payment method.
|
||||||
* for the payment
|
* @property {PaymentMethodCurrentStatus} currentStatus The current payment
|
||||||
* method.
|
* status.
|
||||||
* @property {PaymentMethodCurrentStatus} currentStatus The current
|
* @property {Object} paymentStatuses An object of payment
|
||||||
* payment status.
|
* status constants.
|
||||||
* @property {Object} paymentStatuses An object of
|
* @property {Object} paymentMethodData Arbitrary data to be
|
||||||
* payment status
|
* passed along for
|
||||||
* constants.
|
* processing by the
|
||||||
* @property {Object} paymentMethodData Arbitrary data
|
* payment method on the
|
||||||
* to be passed
|
* server.
|
||||||
* along for
|
* @property {string} errorMessage An error message
|
||||||
* processing by
|
* provided by the payment
|
||||||
* the payment
|
* method if there is an
|
||||||
* method on the
|
* error.
|
||||||
* server.
|
* @property {string} activePaymentMethod The active payment
|
||||||
* @property {string} errorMessage An error
|
* method slug.
|
||||||
* message provided
|
* @property {function()} setActivePaymentMethod A function for setting
|
||||||
* by the payment
|
* the active payment
|
||||||
* method if there
|
* method.
|
||||||
* is an error.
|
* @property {SavedCustomerPaymentMethods} customerPaymentMethods Returns the customer
|
||||||
* @property {string} activePaymentMethod The active
|
* if it exists.
|
||||||
* payment method
|
* @property {Object} paymentMethods Registered payment
|
||||||
* slug.
|
* methods.
|
||||||
* @property {function()} setActivePaymentMethod A function for
|
* @property {Object} expressPaymentMethods Registered express
|
||||||
* setting the
|
* payment methods.
|
||||||
* active payment
|
* @property {boolean} paymentMethodsInitialized True when all registered
|
||||||
* method.
|
* payment methods have
|
||||||
|
* been initialized.
|
||||||
|
* @property {boolean} expressPaymentMethodsInitialized True when all registered
|
||||||
|
* express payment methods
|
||||||
|
* have been initialized.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -199,7 +227,7 @@
|
||||||
* @property {boolean} isEditor Indicates whether in
|
* @property {boolean} isEditor Indicates whether in
|
||||||
* the editor context
|
* the editor context
|
||||||
* (true) or not (false).
|
* (true) or not (false).
|
||||||
* @property {CartBillingData} billingData An object containing
|
* @property {CartBillingData} billingData An object containing
|
||||||
* all billing info like
|
* all billing info like
|
||||||
* address, email and tokens.
|
* address, email and tokens.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -81,10 +81,41 @@ class Checkout extends AbstractBlock {
|
||||||
$data_registry->add( 'shippingMethodsExist', $methods_exist );
|
$data_registry->add( 'shippingMethodsExist', $methods_exist );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ( is_user_logged_in() && ! $data_registry->exists( 'customerPaymentMethods' ) ) {
|
||||||
|
add_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
|
||||||
|
$data_registry->add(
|
||||||
|
'customerPaymentMethods',
|
||||||
|
wc_get_customer_saved_methods_list( get_current_user_id() )
|
||||||
|
);
|
||||||
|
remove_filter( 'woocommerce_payment_methods_list_item', [ $this, 'include_token_id_with_payment_methods' ], 10, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
\Automattic\WooCommerce\Blocks\Assets::register_block_script( $this->block_name . '-frontend', $this->block_name . '-block-frontend' );
|
\Automattic\WooCommerce\Blocks\Assets::register_block_script( $this->block_name . '-frontend', $this->block_name . '-block-frontend' );
|
||||||
return $content . $this->get_skeleton();
|
return $content . $this->get_skeleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback for woocommerce_payment_methods_list_item filter to add token id
|
||||||
|
* to the generated list.
|
||||||
|
*
|
||||||
|
* @param array $list_item The current list item for the saved payment method.
|
||||||
|
* @param \WC_Token $token The token for the current list item.
|
||||||
|
*
|
||||||
|
* @return array The list item with the token id added.
|
||||||
|
*/
|
||||||
|
public static function include_token_id_with_payment_methods( $list_item, $token ) {
|
||||||
|
$list_item['tokenId'] = $token->get_id();
|
||||||
|
$brand = ! empty( $list_item['method']['brand'] ) ?
|
||||||
|
strtolower( $list_item['method']['brand'] ) :
|
||||||
|
'';
|
||||||
|
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch -- need to match on translated value from core.
|
||||||
|
if ( ! empty( $brand ) && esc_html__( 'Credit card', 'woocommerce' ) !== $brand ) {
|
||||||
|
$list_item['method']['brand'] = wc_get_credit_card_type_label( $brand );
|
||||||
|
}
|
||||||
|
return $list_item;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render skeleton markup for the checkout block.
|
* Render skeleton markup for the checkout block.
|
||||||
*/
|
*/
|
||||||
|
|
Loading…
Reference in New Issue