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:
Darren Ethier 2020-03-26 07:11:46 -04:00 committed by GitHub
parent 7921c3e5ba
commit 690f61ec93
10 changed files with 495 additions and 135 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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,
} );

View File

@ -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,
}; };

View File

@ -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 }>

View File

@ -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;

View File

@ -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() );

View File

@ -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() );

View File

@ -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.
*/ */

View File

@ -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.
*/ */