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,
|
||||
useRef,
|
||||
useEffect,
|
||||
useState,
|
||||
} from '@wordpress/element';
|
||||
import { __ } from '@wordpress/i18n';
|
||||
import { useCheckoutContext } from '@woocommerce/base-context';
|
||||
import {
|
||||
useCheckoutContext,
|
||||
usePaymentMethodDataContext,
|
||||
} from '@woocommerce/base-context';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
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.
|
||||
|
@ -46,6 +51,7 @@ const getPaymentMethod = ( id, paymentMethods, isEditor ) => {
|
|||
*/
|
||||
const PaymentMethods = () => {
|
||||
const { isEditor } = useCheckoutContext();
|
||||
const { customerPaymentMethods = {} } = usePaymentMethodDataContext();
|
||||
const { isInitialized, paymentMethods } = usePaymentMethods();
|
||||
const currentPaymentMethods = useRef( paymentMethods );
|
||||
const {
|
||||
|
@ -54,6 +60,7 @@ const PaymentMethods = () => {
|
|||
...paymentMethodInterface
|
||||
} = usePaymentMethodInterface();
|
||||
const currentPaymentMethodInterface = useRef( paymentMethodInterface );
|
||||
const [ selectedToken, setSelectedToken ] = useState( 0 );
|
||||
|
||||
// update ref on change.
|
||||
useEffect( () => {
|
||||
|
@ -78,16 +85,20 @@ const PaymentMethods = () => {
|
|||
[ isEditor, activePaymentMethod ]
|
||||
);
|
||||
|
||||
if ( ! isInitialized || Object.keys( paymentMethods ).length === 0 ) {
|
||||
if (
|
||||
! isInitialized ||
|
||||
Object.keys( currentPaymentMethods.current ).length === 0
|
||||
) {
|
||||
return <NoPaymentMethods />;
|
||||
}
|
||||
|
||||
return (
|
||||
const renderedTabs = (
|
||||
<Tabs
|
||||
className="wc-block-components-checkout-payment-methods"
|
||||
onSelect={ ( tabName ) => setActivePaymentMethod( tabName ) }
|
||||
tabs={ Object.keys( paymentMethods ).map( ( id ) => {
|
||||
const { label, ariaLabel } = paymentMethods[ id ];
|
||||
tabs={ Object.keys( currentPaymentMethods.current ).map( ( id ) => {
|
||||
const { label, ariaLabel } = currentPaymentMethods.current[
|
||||
id
|
||||
];
|
||||
return {
|
||||
name: id,
|
||||
title: () => label,
|
||||
|
@ -103,6 +114,22 @@ const PaymentMethods = () => {
|
|||
{ getRenderedTab() }
|
||||
</Tabs>
|
||||
);
|
||||
|
||||
const renderedSavedPaymentOptions = (
|
||||
<SavedPaymentMethodOptions onSelect={ setSelectedToken } />
|
||||
);
|
||||
|
||||
const renderedTabsAndSavedPaymentOptions = (
|
||||
<>
|
||||
{ renderedSavedPaymentOptions }
|
||||
{ renderedTabs }
|
||||
</>
|
||||
);
|
||||
|
||||
return Object.keys( customerPaymentMethods ).length > 0 &&
|
||||
selectedToken !== 0
|
||||
? renderedSavedPaymentOptions
|
||||
: renderedTabsAndSavedPaymentOptions;
|
||||
};
|
||||
|
||||
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
|
||||
*/
|
||||
import { STATUS } from './constants';
|
||||
import { ACTION_TYPES } from './constants';
|
||||
|
||||
/**
|
||||
* @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.
|
||||
|
@ -80,3 +86,27 @@ export const success = ( { billingData, paymentMethodData } ) => ( {
|
|||
billingData,
|
||||
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',
|
||||
};
|
||||
|
||||
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.
|
||||
*/
|
||||
|
@ -25,6 +32,8 @@ export const DEFAULT_PAYMENT_DATA = {
|
|||
// processing server side.
|
||||
},
|
||||
errorMessage: '',
|
||||
paymentMethods: {},
|
||||
expressPaymentMethods: {},
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -46,4 +55,9 @@ export const DEFAULT_PAYMENT_METHOD_DATA = {
|
|||
errorMessage: '',
|
||||
activePaymentMethod: '',
|
||||
setActivePaymentMethod: () => void null,
|
||||
customerPaymentMethods: {},
|
||||
paymentMethods: {},
|
||||
expressPaymentMethods: {},
|
||||
paymentMethodsInitialized: false,
|
||||
expressPaymentMethodsInitialized: false,
|
||||
};
|
||||
|
|
|
@ -7,7 +7,18 @@ import {
|
|||
DEFAULT_PAYMENT_METHOD_DATA,
|
||||
} from './constants';
|
||||
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
|
||||
|
@ -17,13 +28,17 @@ import {
|
|||
useContext,
|
||||
useState,
|
||||
useReducer,
|
||||
useCallback,
|
||||
useEffect,
|
||||
} from '@wordpress/element';
|
||||
import { getSetting } from '@woocommerce/settings';
|
||||
|
||||
/**
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').PaymentMethodDataContext} PaymentMethodDataContext
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatch} PaymentStatusDispatch
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').PaymentStatusDispatchers} PaymentStatusDispatchers
|
||||
* @typedef {import('@woocommerce/type-defs/cart').CartBillingData} CartBillingData
|
||||
* @typedef {import('@woocommerce/type-defs/contexts').CustomerPaymentMethod} CustomerPaymentMethod
|
||||
*/
|
||||
|
||||
const {
|
||||
|
@ -46,9 +61,6 @@ export const usePaymentMethodDataContext = () => {
|
|||
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
|
||||
* CheckoutDataProvider.
|
||||
|
@ -69,6 +81,7 @@ export const PaymentMethodDataProvider = ( {
|
|||
const [ activePaymentMethod, setActive ] = useState(
|
||||
initialActivePaymentMethod
|
||||
);
|
||||
const customerPaymentMethods = getSetting( 'customerPaymentMethods', {} );
|
||||
const [ paymentStatus, dispatch ] = useReducer(
|
||||
reducer,
|
||||
DEFAULT_PAYMENT_DATA
|
||||
|
@ -77,43 +90,71 @@ export const PaymentMethodDataProvider = ( {
|
|||
setActive( paymentMethodSlug );
|
||||
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}
|
||||
*/
|
||||
const setPaymentStatus = () => ( {
|
||||
started: () => dispatch( statusOnly( STARTED ) ),
|
||||
processing: () => dispatch( statusOnly( PROCESSING ) ),
|
||||
completed: () => dispatch( statusOnly( COMPLETE ) ),
|
||||
/**
|
||||
* @param {string} errorMessage An error message
|
||||
*/
|
||||
error: ( errorMessage ) => dispatch( error( errorMessage ) ),
|
||||
/**
|
||||
* @param {string} errorMessage An error message
|
||||
* @param {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,
|
||||
} )
|
||||
),
|
||||
/**
|
||||
* @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,
|
||||
} )
|
||||
),
|
||||
} );
|
||||
const setPaymentStatus = useCallback(
|
||||
() => ( {
|
||||
started: () => dispatch( statusOnly( STARTED ) ),
|
||||
processing: () => dispatch( statusOnly( PROCESSING ) ),
|
||||
completed: () => dispatch( statusOnly( COMPLETE ) ),
|
||||
/**
|
||||
* @param {string} errorMessage An error message
|
||||
*/
|
||||
error: ( errorMessage ) => dispatch( error( errorMessage ) ),
|
||||
/**
|
||||
* @param {string} errorMessage An error message
|
||||
* @param {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,
|
||||
} )
|
||||
),
|
||||
/**
|
||||
* @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,
|
||||
} )
|
||||
),
|
||||
} ),
|
||||
[ dispatch ]
|
||||
);
|
||||
|
||||
const currentStatus = {
|
||||
isPristine: paymentStatus === PRISTINE,
|
||||
|
@ -135,6 +176,11 @@ export const PaymentMethodDataProvider = ( {
|
|||
errorMessage: paymentStatus.errorMessage,
|
||||
activePaymentMethod,
|
||||
setActivePaymentMethod,
|
||||
customerPaymentMethods,
|
||||
paymentMethods: paymentStatus.paymentMethods,
|
||||
expressPaymentMethods: paymentStatus.expressPaymentMethods,
|
||||
paymentMethodsInitialized,
|
||||
expressPaymentMethodsInitialized,
|
||||
};
|
||||
return (
|
||||
<PaymentMethodDataContext.Provider value={ paymentData }>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { STATUS, DEFAULT_PAYMENT_DATA } from './constants';
|
||||
import { ACTION_TYPES, DEFAULT_PAYMENT_DATA } from './constants';
|
||||
const {
|
||||
STARTED,
|
||||
ERROR,
|
||||
|
@ -10,9 +10,9 @@ const {
|
|||
PROCESSING,
|
||||
PRISTINE,
|
||||
COMPLETE,
|
||||
} = STATUS;
|
||||
|
||||
const SET_BILLING_DATA = 'set_billing_data';
|
||||
SET_REGISTERED_PAYMENT_METHOD,
|
||||
SET_REGISTERED_EXPRESS_PAYMENT_METHOD,
|
||||
} = ACTION_TYPES;
|
||||
|
||||
/**
|
||||
* Reducer for payment data state
|
||||
|
@ -22,7 +22,7 @@ const SET_BILLING_DATA = 'set_billing_data';
|
|||
*/
|
||||
const reducer = (
|
||||
state = DEFAULT_PAYMENT_DATA,
|
||||
{ type, billingData, paymentMethodData, errorMessage }
|
||||
{ type, billingData, paymentMethodData, errorMessage, paymentMethod }
|
||||
) => {
|
||||
switch ( type ) {
|
||||
case STARTED:
|
||||
|
@ -66,11 +66,29 @@ const reducer = (
|
|||
return {
|
||||
...DEFAULT_PAYMENT_DATA,
|
||||
currentStatus: PRISTINE,
|
||||
// keep payment method registration state
|
||||
paymentMethods: {
|
||||
...state.paymentMethods,
|
||||
},
|
||||
expressPaymentMethods: {
|
||||
...state.expressPaymentMethods,
|
||||
},
|
||||
};
|
||||
case SET_BILLING_DATA:
|
||||
case SET_REGISTERED_PAYMENT_METHOD:
|
||||
return {
|
||||
...state,
|
||||
billingData,
|
||||
paymentMethods: {
|
||||
...state.paymentMethods,
|
||||
[ paymentMethod.id ]: paymentMethod,
|
||||
},
|
||||
};
|
||||
case SET_REGISTERED_EXPRESS_PAYMENT_METHOD:
|
||||
return {
|
||||
...state,
|
||||
expressPaymentMethods: {
|
||||
...state.expressPaymentMethods,
|
||||
[ paymentMethod.id ]: paymentMethod,
|
||||
},
|
||||
};
|
||||
}
|
||||
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
|
||||
*/
|
||||
import {
|
||||
getPaymentMethods,
|
||||
getExpressPaymentMethods,
|
||||
} from '@woocommerce/blocks-registry';
|
||||
import { useState, useEffect, useRef } from '@wordpress/element';
|
||||
import { usePaymentMethodDataContext } from '@woocommerce/base-context';
|
||||
|
||||
const usePaymentMethodState = ( registeredPaymentMethods ) => {
|
||||
const [ paymentMethods, setPaymentMethods ] = useState( [] );
|
||||
const [ isInitialized, setIsInitialized ] = useState( false );
|
||||
const countPaymentMethodsInitializing = useRef(
|
||||
Object.keys( registeredPaymentMethods ).length
|
||||
);
|
||||
|
||||
useEffect( () => {
|
||||
// if all payment methods are initialized then bail.
|
||||
if ( isInitialized ) {
|
||||
return;
|
||||
}
|
||||
// loop through payment methods and see what the state is
|
||||
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 };
|
||||
const usePaymentMethodState = ( express = false ) => {
|
||||
const {
|
||||
paymentMethods,
|
||||
expressPaymentMethods,
|
||||
paymentMethodsInitialized,
|
||||
expressPaymentMethodsInitialized,
|
||||
} = usePaymentMethodDataContext();
|
||||
return express
|
||||
? {
|
||||
paymentMethods: expressPaymentMethods,
|
||||
isInitialized: expressPaymentMethodsInitialized,
|
||||
}
|
||||
: { paymentMethods, isInitialized: paymentMethodsInitialized };
|
||||
};
|
||||
|
||||
export const usePaymentMethods = () =>
|
||||
usePaymentMethodState( getPaymentMethods() );
|
||||
export const useExpressPaymentMethods = () =>
|
||||
usePaymentMethodState( getExpressPaymentMethods() );
|
||||
export const usePaymentMethods = () => usePaymentMethodState();
|
||||
export const useExpressPaymentMethods = () => usePaymentMethodState( true );
|
||||
|
|
|
@ -92,6 +92,30 @@
|
|||
* 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
|
||||
*
|
||||
|
@ -110,34 +134,38 @@
|
|||
/**
|
||||
* @typedef {Object} PaymentMethodDataContext
|
||||
*
|
||||
* @property {PaymentStatusDispatch} setPaymentStatus Sets the
|
||||
* payment status
|
||||
* for the payment
|
||||
* method.
|
||||
* @property {PaymentMethodCurrentStatus} currentStatus The current
|
||||
* payment status.
|
||||
* @property {Object} paymentStatuses An object of
|
||||
* payment status
|
||||
* constants.
|
||||
* @property {Object} paymentMethodData Arbitrary data
|
||||
* to be passed
|
||||
* along for
|
||||
* processing by
|
||||
* the payment
|
||||
* method on the
|
||||
* server.
|
||||
* @property {string} errorMessage An error
|
||||
* message provided
|
||||
* by the payment
|
||||
* method if there
|
||||
* is an error.
|
||||
* @property {string} activePaymentMethod The active
|
||||
* payment method
|
||||
* slug.
|
||||
* @property {function()} setActivePaymentMethod A function for
|
||||
* setting the
|
||||
* active payment
|
||||
* method.
|
||||
* @property {PaymentStatusDispatch} setPaymentStatus Sets the payment status
|
||||
* for the payment method.
|
||||
* @property {PaymentMethodCurrentStatus} currentStatus The current payment
|
||||
* status.
|
||||
* @property {Object} paymentStatuses An object of payment
|
||||
* status constants.
|
||||
* @property {Object} paymentMethodData Arbitrary data to be
|
||||
* passed along for
|
||||
* processing by the
|
||||
* payment method on the
|
||||
* server.
|
||||
* @property {string} errorMessage An error message
|
||||
* provided by the payment
|
||||
* method if there is an
|
||||
* error.
|
||||
* @property {string} activePaymentMethod The active payment
|
||||
* method slug.
|
||||
* @property {function()} setActivePaymentMethod A function for setting
|
||||
* the active payment
|
||||
* method.
|
||||
* @property {SavedCustomerPaymentMethods} customerPaymentMethods Returns the customer
|
||||
* if it exists.
|
||||
* @property {Object} paymentMethods Registered payment
|
||||
* methods.
|
||||
* @property {Object} expressPaymentMethods Registered express
|
||||
* payment methods.
|
||||
* @property {boolean} paymentMethodsInitialized True when all registered
|
||||
* 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
|
||||
* the editor context
|
||||
* (true) or not (false).
|
||||
* @property {CartBillingData} billingData An object containing
|
||||
* @property {CartBillingData} billingData An object containing
|
||||
* all billing info like
|
||||
* address, email and tokens.
|
||||
*/
|
||||
|
|
|
@ -81,10 +81,41 @@ class Checkout extends AbstractBlock {
|
|||
$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' );
|
||||
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.
|
||||
*/
|
||||
|
|
Loading…
Reference in New Issue