Add support for card icons to Axo Block and move the Change Card Link handling to the store. Also migrate the Change Card Link markup management logic to React

This commit is contained in:
Daniel Dudzic 2024-10-16 12:51:26 +02:00
parent 53af77897f
commit 42805c4fb5
No known key found for this signature in database
GPG key ID: 31B40D33E3465483
17 changed files with 172 additions and 120 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 { STORE_NAME } from '../stores/axoStore';
/**
@ -26,6 +27,8 @@ const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
[]
);
const cardOptions = useCardOptions( axoConfig );
const styleOptions = useMemo( () => {
return deleteEmptyKeys( configRef.current.axoConfig.style_options );
}, [ deleteEmptyKeys ] );
@ -48,10 +51,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,
},
} );
// Set locale (hardcoded to 'en_us' for now)
@ -66,7 +72,7 @@ const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
};
initFastlane();
}, [ fastlaneSdk, styleOptions, isPayPalLoaded, namespace ] );
}, [ fastlaneSdk, styleOptions, isPayPalLoaded, namespace, cardOptions ] );
// Effect to update the config ref when configs change
useEffect( () => {

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

@ -37,7 +37,8 @@ return array(
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.configuration.dcc' ),
$container->get( 'onboarding.environment' ),
$container->get( 'wcgateway.url' )
$container->get( 'wcgateway.url' ),
$container->get( 'axo.supported-country-card-type-matrix' ),
);
},
);

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;
/**
* AdvancedCardPaymentMethod constructor.
*
@ -91,6 +98,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.
*/
public function __construct(
string $module_url,
@ -100,7 +108,8 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
Settings $settings,
DCCGatewayConfiguration $dcc_configuration,
Environment $environment,
string $wcgateway_module_url
string $wcgateway_module_url,
array $supported_country_card_type_matrix
) {
$this->name = AxoGateway::ID;
$this->module_url = $module_url;
@ -111,7 +120,7 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
$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;
}
/**
@ -207,6 +216,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(),
'style_options' => array(
'root' => array(
'backgroundColor' => $this->settings->has( 'axo_style_root_bg_color' ) ? $this->settings->get( 'axo_style_root_bg_color' ) : '',
@ -243,6 +254,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(),
);
}
}