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

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

View File

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

View File

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

View File

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

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
*/
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 );

View File

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

View File

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