Merge pull request #2704 from woocommerce/PCP-3783-fastlane-allow-merchants-to-disable-specific-card-types

Axo: Add support for card icons in the Block Checkout and add card type limiting for both block and classic checkouts (3783)
This commit is contained in:
Emili Castells 2024-11-19 10:32:19 +01:00 committed by GitHub
commit c7dfe3293b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 240 additions and 144 deletions

View file

@ -17,10 +17,19 @@ $fast-transition-duration: 0.5s;
}
// 1. AXO Block Radio Label
#ppcp-axo-block-radio-label {
@include flex-space-between;
.wc-block-checkout__payment-method label[for="radio-control-wc-payment-method-options-ppcp-axo-gateway"] {
padding-right: .875em;
}
#radio-control-wc-payment-method-options-ppcp-axo-gateway__label {
display: flex;
align-items: center;
width: 100%;
padding-right: 1em;
.wc-block-components-payment-method-icons {
margin: 0;
}
}
// 2. AXO Block Card
@ -70,15 +79,16 @@ $fast-transition-duration: 0.5s;
}
&__edit {
background-color: transparent;
flex-grow: 1;
margin-left: auto;
text-align: right;
border: 0;
color: inherit;
cursor: pointer;
display: block;
font-family: inherit;
margin: 0 0 0 auto;
font-size: 0.875em;
font-weight: normal;
color: inherit;
background-color: transparent;
cursor: pointer;
&:hover {
text-decoration: underline;

View file

@ -1,15 +1,28 @@
import { createElement } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { STORE_NAME } from '../../stores/axoStore';
/**
* Renders a button to change the selected card in the checkout process.
*
* @param {Object} props
* @param {Function} props.onChangeButtonClick - Callback function to handle the click event.
* @return {JSX.Element} The rendered button as an anchor tag.
* @return {JSX.Element|null} The rendered button as an anchor tag, or null if conditions aren't met.
*/
const CardChangeButton = ( { onChangeButtonClick } ) =>
createElement(
const CardChangeButton = () => {
const { isGuest, cardDetails, cardChangeHandler } = useSelect(
( select ) => ( {
isGuest: select( STORE_NAME ).getIsGuest(),
cardDetails: select( STORE_NAME ).getCardDetails(),
cardChangeHandler: select( STORE_NAME ).getCardChangeHandler(),
} ),
[]
);
if ( isGuest || ! cardDetails || ! cardChangeHandler ) {
return null;
}
return createElement(
'a',
{
className:
@ -19,10 +32,11 @@ const CardChangeButton = ( { onChangeButtonClick } ) =>
// Prevent default anchor behavior
event.preventDefault();
// Call the provided click handler
onChangeButtonClick();
cardChangeHandler();
},
},
__( 'Choose a different card', 'woocommerce-paypal-payments' )
);
};
export default CardChangeButton;

View file

@ -1,51 +0,0 @@
import { createElement, createRoot, useEffect } from '@wordpress/element';
import CardChangeButton from './CardChangeButton';
/**
* Manages the insertion and removal of the CardChangeButton in the DOM.
*
* @param {Object} props
* @param {Function} props.onChangeButtonClick - Callback function for when the card change button is clicked.
* @return {null} This component doesn't render any visible elements directly.
*/
const CardChangeButtonManager = ( { onChangeButtonClick } ) => {
useEffect( () => {
const radioLabelElement = document.getElementById(
'ppcp-axo-block-radio-label'
);
if ( radioLabelElement ) {
// Check if the change button doesn't already exist
if (
! radioLabelElement.querySelector(
'.wc-block-checkout-axo-block-card__edit'
)
) {
// Create a new container for the button
const buttonContainer = document.createElement( 'div' );
radioLabelElement.appendChild( buttonContainer );
// Create a React root and render the CardChangeButton
const root = createRoot( buttonContainer );
root.render(
createElement( CardChangeButton, { onChangeButtonClick } )
);
}
}
// Cleanup function to remove the button when the component unmounts
return () => {
const button = document.querySelector(
'.wc-block-checkout-axo-block-card__edit'
);
if ( button && button.parentNode ) {
button.parentNode.remove();
}
};
}, [ onChangeButtonClick ] );
// This component doesn't render anything directly
return null;
};
export default CardChangeButtonManager;

View file

@ -1,4 +1,2 @@
export { default as Card } from './Card';
export { default as CardChangeButton } from './CardChangeButton';
export { default as CardChangeButtonManager } from './CardChangeButtonManager';
export { injectCardChangeButton, removeCardChangeButton } from './utils';

View file

@ -1,32 +0,0 @@
import { createElement, createRoot } from '@wordpress/element';
import CardChangeButtonManager from './CardChangeButtonManager';
/**
* Injects a card change button into the DOM.
*
* @param {Function} onChangeButtonClick - Callback function for when the card change button is clicked.
*/
export const injectCardChangeButton = ( onChangeButtonClick ) => {
// Create a container for the button
const container = document.createElement( 'div' );
document.body.appendChild( container );
// Render the CardChangeButtonManager in the new container
createRoot( container ).render(
createElement( CardChangeButtonManager, { onChangeButtonClick } )
);
};
/**
* Removes the card change button from the DOM if it exists.
*/
export const removeCardChangeButton = () => {
const button = document.querySelector(
'.wc-block-checkout-axo-block-card__edit'
);
// Remove the button's parent node if it exists
if ( button && button.parentNode ) {
button.parentNode.remove();
}
};

View file

@ -0,0 +1,24 @@
import CardChangeButton from './../Card/CardChangeButton';
/**
* TitleLabel component for displaying a payment method title with icons and a change card button.
*
* @param {Object} props - Component props
* @param {Object} props.components - Object containing WooCommerce components
* @param {Object} props.config - Configuration object for the payment method
* @return {JSX.Element} WordPress element
*/
const TitleLabel = ( { components, config } ) => {
const axoConfig = window.wc_ppcp_axo;
const { PaymentMethodIcons } = components;
return (
<>
<span dangerouslySetInnerHTML={ { __html: config.title } } />
<PaymentMethodIcons icons={ axoConfig?.card_icons } />
<CardChangeButton />
</>
);
};
export default TitleLabel;

View file

@ -0,0 +1 @@
export { default as TitleLabel } from './TitleLabel';

View file

@ -1,7 +1,6 @@
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
import { populateWooFields } from '../helpers/fieldHelpers';
import { injectShippingChangeButton } from '../components/Shipping';
import { injectCardChangeButton } from '../components/Card';
import { setIsGuest, setIsEmailLookupCompleted } from '../stores/axoStore';
/**
@ -16,7 +15,6 @@ import { setIsGuest, setIsEmailLookupCompleted } from '../stores/axoStore';
* @param {Function} setWooShippingAddress - Function to update WooCommerce shipping address.
* @param {Function} setWooBillingAddress - Function to update WooCommerce billing address.
* @param {Function} onChangeShippingAddressClick - Handler for shipping address change.
* @param {Function} onChangeCardButtonClick - Handler for card change.
* @return {Function} The email lookup handler function.
*/
export const createEmailLookupHandler = (
@ -28,8 +26,7 @@ export const createEmailLookupHandler = (
wooBillingAddress,
setWooShippingAddress,
setWooBillingAddress,
onChangeShippingAddressClick,
onChangeCardButtonClick
onChangeShippingAddressClick
) => {
return async ( email ) => {
try {
@ -102,9 +99,8 @@ export const createEmailLookupHandler = (
setWooBillingAddress
);
// Inject change buttons for shipping and card
// Inject the change button for shipping
injectShippingChangeButton( onChangeShippingAddressClick );
injectCardChangeButton( onChangeCardButtonClick );
} else {
log( 'Authentication failed or did not succeed', 'warn' );
}

View file

@ -3,7 +3,6 @@ import { useDispatch } from '@wordpress/data';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
import { STORE_NAME } from '../stores/axoStore';
import { removeShippingChangeButton } from '../components/Shipping';
import { removeCardChangeButton } from '../components/Card';
import { removeWatermark } from '../components/Watermark';
import {
removeEmailFunctionality,
@ -50,7 +49,6 @@ const useAxoCleanup = () => {
// Remove AXO UI elements
removeShippingChangeButton();
removeCardChangeButton();
removeWatermark();
// Remove email functionality if it was set up

View file

@ -35,6 +35,7 @@ const useAxoSetup = (
setIsAxoScriptLoaded,
setShippingAddress,
setCardDetails,
setCardChangeHandler,
} = useDispatch( STORE_NAME );
// Check if PayPal script has loaded
@ -73,6 +74,7 @@ const useAxoSetup = (
if ( paypalLoaded && fastlaneSdk ) {
setIsAxoScriptLoaded( true );
setIsAxoActive( true );
setCardChangeHandler( onChangeCardButtonClick );
// Create and set up email lookup handler
const emailLookupHandler = createEmailLookupHandler(
@ -84,8 +86,7 @@ const useAxoSetup = (
wooBillingAddress,
setWooShippingAddress,
setWooBillingAddress,
onChangeShippingAddressClick,
onChangeCardButtonClick
onChangeShippingAddressClick
);
setupEmailFunctionality( emailLookupHandler );
}

View file

@ -0,0 +1,36 @@
import { useMemo } from '@wordpress/element';
const DEFAULT_ALLOWED_CARDS = [ 'VISA', 'MASTERCARD', 'AMEX', 'DISCOVER' ];
/**
* Custom hook to determine the allowed card options based on configuration.
*
* @param {Object} axoConfig - The AXO configuration object.
* @return {Array} The final list of allowed card options.
*/
const useCardOptions = ( axoConfig ) => {
const merchantCountry = axoConfig.merchant_country || 'US';
return useMemo( () => {
const allowedCards = new Set(
axoConfig.allowed_cards?.[ merchantCountry ] ||
DEFAULT_ALLOWED_CARDS
);
// Create a Set of disabled cards, converting each to uppercase
const disabledCards = new Set(
( axoConfig.disable_cards || [] ).map( ( card ) =>
card.toUpperCase()
)
);
// Filter out disabled cards from the allowed cards
const finalCardOptions = [ ...allowedCards ].filter(
( card ) => ! disabledCards.has( card )
);
return finalCardOptions;
}, [ axoConfig.allowed_cards, axoConfig.disable_cards, merchantCountry ] );
};
export default useCardOptions;

View file

@ -3,6 +3,7 @@ import { useSelect } from '@wordpress/data';
import Fastlane from '../../../../ppcp-axo/resources/js/Connection/Fastlane';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
import { useDeleteEmptyKeys } from './useDeleteEmptyKeys';
import useCardOptions from './useCardOptions';
import useAllowedLocations from './useAllowedLocations';
import { STORE_NAME } from '../stores/axoStore';
@ -27,6 +28,8 @@ const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
[]
);
const cardOptions = useCardOptions( axoConfig );
const styleOptions = useMemo( () => {
return deleteEmptyKeys( configRef.current.axoConfig.style_options );
}, [ deleteEmptyKeys ] );
@ -51,10 +54,13 @@ const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
window.localStorage.setItem( 'axoEnv', 'sandbox' );
}
// Connect to Fastlane with locale and style options
// Connect to Fastlane with locale, style options, and allowed card brands
await fastlane.connect( {
locale: configRef.current.ppcpConfig.locale,
styles: styleOptions,
cardOptions: {
allowedBrands: cardOptions,
},
shippingAddressOptions: {
allowedLocations,
},
@ -77,6 +83,7 @@ const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
styleOptions,
isPayPalLoaded,
namespace,
cardOptions,
allowedLocations,
] );

View file

@ -13,6 +13,7 @@ import usePayPalCommerceGateway from './hooks/usePayPalCommerceGateway';
// Components
import { Payment } from './components/Payment/Payment';
import { TitleLabel } from './components/TitleLabel';
const gatewayHandle = 'ppcp-axo-gateway';
const namespace = 'ppcpBlocksPaypalAxo';
@ -89,12 +90,7 @@ const Axo = ( props ) => {
registerPaymentMethod( {
name: initialConfig.id,
label: (
<div
id="ppcp-axo-block-radio-label"
dangerouslySetInnerHTML={ { __html: initialConfig.title } }
/>
),
label: <TitleLabel config={ initialConfig } />,
content: <Axo />,
edit: createElement( initialConfig.title ),
ariaLabel: initialConfig.title,

View file

@ -12,6 +12,7 @@ const DEFAULT_STATE = {
shippingAddress: null,
cardDetails: null,
phoneNumber: '',
cardChangeHandler: null,
};
// Action creators for updating the store state
@ -52,6 +53,10 @@ const actions = {
type: 'SET_PHONE_NUMBER',
payload: phoneNumber,
} ),
setCardChangeHandler: ( cardChangeHandler ) => ( {
type: 'SET_CARD_CHANGE_HANDLER',
payload: cardChangeHandler,
} ),
};
/**
@ -81,6 +86,8 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
return { ...state, cardDetails: action.payload };
case 'SET_PHONE_NUMBER':
return { ...state, phoneNumber: action.payload };
case 'SET_CARD_CHANGE_HANDLER':
return { ...state, cardChangeHandler: action.payload };
default:
return state;
}
@ -97,6 +104,7 @@ const selectors = {
getShippingAddress: ( state ) => state.shippingAddress,
getCardDetails: ( state ) => state.cardDetails,
getPhoneNumber: ( state ) => state.phoneNumber,
getCardChangeHandler: ( state ) => state.cardChangeHandler,
};
// Create and register the Redux store for the AXO block
@ -163,3 +171,12 @@ export const setCardDetails = ( cardDetails ) => {
export const setPhoneNumber = ( phoneNumber ) => {
dispatch( STORE_NAME ).setPhoneNumber( phoneNumber );
};
/**
* Action dispatcher to update the card change handler in the store.
*
* @param {Function} cardChangeHandler - The card change handler function.
*/
export const setCardChangeHandler = ( cardChangeHandler ) => {
dispatch( STORE_NAME ).setCardChangeHandler( cardChangeHandler );
};

View file

@ -38,6 +38,7 @@ return array(
$container->get( 'wcgateway.configuration.dcc' ),
$container->get( 'onboarding.environment' ),
$container->get( 'wcgateway.url' ),
$container->get( 'axo.supported-country-card-type-matrix' ),
$container->get( 'axo.shipping-wc-enabled-locations' )
);
},

View file

@ -79,6 +79,13 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
*/
private $wcgateway_module_url;
/**
* The supported country card type matrix.
*
* @var array
*/
private $supported_country_card_type_matrix;
/**
* The list of WooCommerce enabled shipping locations.
*
@ -98,6 +105,7 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
* @param DCCGatewayConfiguration $dcc_configuration The DCC gateway settings.
* @param Environment $environment The environment object.
* @param string $wcgateway_module_url The WcGateway module URL.
* @param array $supported_country_card_type_matrix The supported country card type matrix for Axo.
* @param array $enabled_shipping_locations The list of WooCommerce enabled shipping locations.
*/
public function __construct(
@ -109,18 +117,20 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
DCCGatewayConfiguration $dcc_configuration,
Environment $environment,
string $wcgateway_module_url,
array $supported_country_card_type_matrix,
array $enabled_shipping_locations
) {
$this->name = AxoGateway::ID;
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->smart_button = $smart_button;
$this->settings = $settings;
$this->dcc_configuration = $dcc_configuration;
$this->environment = $environment;
$this->wcgateway_module_url = $wcgateway_module_url;
$this->enabled_shipping_locations = $enabled_shipping_locations;
$this->name = AxoGateway::ID;
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->smart_button = $smart_button;
$this->settings = $settings;
$this->dcc_configuration = $dcc_configuration;
$this->environment = $environment;
$this->wcgateway_module_url = $wcgateway_module_url;
$this->supported_country_card_type_matrix = $supported_country_card_type_matrix;
$this->enabled_shipping_locations = $enabled_shipping_locations;
}
/**
@ -216,6 +226,8 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
: null, // Set to null if WC()->cart is null or get_total doesn't exist.
),
),
'allowed_cards' => $this->supported_country_card_type_matrix,
'disable_cards' => $this->settings->has( 'disable_cards' ) ? (array) $this->settings->get( 'disable_cards' ) : array(),
'enabled_shipping_locations' => $this->enabled_shipping_locations,
'style_options' => array(
'root' => array(
@ -253,6 +265,8 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
),
'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '',
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'card_icons' => $this->settings->has( 'card_icons' ) ? (array) $this->settings->get( 'card_icons' ) : array(),
'merchant_country' => WC()->countries->get_base_country(),
);
}
}

View file

@ -85,6 +85,8 @@ class AxoManager {
},
};
this.cardOptions = this.getCardOptions();
this.enabledShippingLocations =
this.axoConfig.enabled_shipping_locations;
@ -664,6 +666,9 @@ class AxoManager {
await this.fastlane.connect( {
locale: this.locale,
styles: this.styles,
cardOptions: {
allowedBrands: this.cardOptions,
},
shippingAddressOptions: {
allowedLocations: this.enabledShippingLocations,
},
@ -1251,6 +1256,31 @@ class AxoManager {
return this.axoConfig?.widgets?.email === 'use_widget';
}
getCardOptions() {
const DEFAULT_ALLOWED_CARDS = [
'VISA',
'MASTERCARD',
'AMEX',
'DISCOVER',
];
const merchantCountry = this.axoConfig.merchant_country || 'US';
const allowedCards = new Set(
this.axoConfig.allowed_cards?.[ merchantCountry ] ||
DEFAULT_ALLOWED_CARDS
);
const disabledCards = new Set(
( this.axoConfig.disable_cards || [] ).map( ( card ) =>
card.toUpperCase()
)
);
return [ ...allowedCards ].filter(
( card ) => ! disabledCards.has( card )
);
}
deleteKeysWithEmptyString = ( obj ) => {
for ( const key of Object.keys( obj ) ) {
if ( obj[ key ] === '' ) {

View file

@ -68,6 +68,7 @@ return array(
$container->get( 'api.shop.currency.getter' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'wcgateway.url' ),
$container->get( 'axo.supported-country-card-type-matrix' ),
$container->get( 'axo.shipping-wc-enabled-locations' )
);
},
@ -111,7 +112,31 @@ return array(
)
);
},
/**
* The matrix which countries and card type combinations can be used for AXO.
*/
'axo.supported-country-card-type-matrix' => static function ( ContainerInterface $container ) : array {
/**
* Returns which countries and card type combinations can be used for AXO.
*/
return apply_filters(
'woocommerce_paypal_payments_axo_supported_country_card_type_matrix',
array(
'US' => array(
'VISA',
'MASTERCARD',
'AMEX',
'DISCOVER',
),
'CA' => array(
'VISA',
'MASTERCARD',
'AMEX',
'DISCOVER',
),
)
);
},
'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );

View file

@ -29,28 +29,28 @@ class AxoManager {
*
* @var string
*/
private $module_url;
private string $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
private string $version;
/**
* The settings.
*
* @var Settings
*/
private $settings;
private Settings $settings;
/**
* The environment object.
*
* @var Environment
*/
private $environment;
private Environment $environment;
/**
* The Settings status helper.
@ -71,22 +71,27 @@ class AxoManager {
*
* @var LoggerInterface
*/
private $logger;
private LoggerInterface $logger;
/**
* Session handler.
*
* @var SessionHandler
*/
private $session_handler;
private SessionHandler $session_handler;
/**
* The WcGateway module URL.
*
* @var string
*/
private $wcgateway_module_url;
private string $wcgateway_module_url;
/**
* The supported country card type matrix.
*
* @var array
*/
private array $supported_country_card_type_matrix;
/**
* The list of WooCommerce enabled shipping locations.
*
@ -106,6 +111,7 @@ class AxoManager {
* @param CurrencyGetter $currency The getter of the 3-letter currency code of the shop.
* @param LoggerInterface $logger The logger.
* @param string $wcgateway_module_url The WcGateway module URL.
* @param array $supported_country_card_type_matrix The supported country card type matrix for Axo.
* @param array $enabled_shipping_locations The list of WooCommerce enabled shipping locations.
*/
public function __construct(
@ -118,19 +124,21 @@ class AxoManager {
CurrencyGetter $currency,
LoggerInterface $logger,
string $wcgateway_module_url,
array $supported_country_card_type_matrix,
array $enabled_shipping_locations
) {
$this->module_url = $module_url;
$this->version = $version;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->environment = $environment;
$this->settings_status = $settings_status;
$this->currency = $currency;
$this->logger = $logger;
$this->wcgateway_module_url = $wcgateway_module_url;
$this->enabled_shipping_locations = $enabled_shipping_locations;
$this->module_url = $module_url;
$this->version = $version;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->environment = $environment;
$this->settings_status = $settings_status;
$this->currency = $currency;
$this->logger = $logger;
$this->wcgateway_module_url = $wcgateway_module_url;
$this->supported_country_card_type_matrix = $supported_country_card_type_matrix;
$this->enabled_shipping_locations = $enabled_shipping_locations;
}
/**
@ -193,6 +201,8 @@ class AxoManager {
'value' => WC()->cart->get_total( 'numeric' ),
),
),
'allowed_cards' => $this->supported_country_card_type_matrix,
'disable_cards' => $this->settings->has( 'disable_cards' ) ? (array) $this->settings->get( 'disable_cards' ) : array(),
'enabled_shipping_locations' => $this->enabled_shipping_locations,
'style_options' => array(
'root' => array(
@ -231,6 +241,7 @@ class AxoManager {
'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '',
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'billing_email_button_text' => __( 'Continue', 'woocommerce-paypal-payments' ),
'merchant_country' => WC()->countries->get_base_country(),
);
}