Merge pull request #2637 from woocommerce/axo-blocks-additional-minor-code-cleanup-refactor

Axo Blocks: Reduce prop drilling and move shipping address, card details and phone number state to the Redux store
This commit is contained in:
Danny Dudzic 2024-09-25 23:57:53 +02:00 committed by GitHub
commit b44dd0a8d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 170 additions and 126 deletions

View file

@ -1,5 +1,7 @@
import { useMemo } from '@wordpress/element'; import { useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { Watermark } from '../Watermark'; import { Watermark } from '../Watermark';
import { STORE_NAME } from '../../stores/axoStore';
const cardIcons = { const cardIcons = {
VISA: 'visa-light.svg', VISA: 'visa-light.svg',
@ -11,7 +13,14 @@ const cardIcons = {
UNIONPAY: 'unionpay-light.svg', UNIONPAY: 'unionpay-light.svg',
}; };
const Card = ( { card, fastlaneSdk, showWatermark = true } ) => { const Card = ( { fastlaneSdk, showWatermark = true } ) => {
const { card } = useSelect(
( select ) => ( {
card: select( STORE_NAME ).getCardDetails(),
} ),
[]
);
const { brand, lastDigits, expiry, name } = card?.paymentSource?.card ?? {}; const { brand, lastDigits, expiry, name } = card?.paymentSource?.card ?? {};
const cardLogo = useMemo( () => { const cardLogo = useMemo( () => {

View file

@ -83,8 +83,6 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => {
handleSubmit: handleEmailSubmit, handleSubmit: handleEmailSubmit,
} ) } )
); );
} else {
console.warn( 'Submit button root not found' );
} }
}; };

View file

@ -4,15 +4,15 @@ import { __ } from '@wordpress/i18n';
import { Card } from '../Card'; import { Card } from '../Card';
import { STORE_NAME } from '../../stores/axoStore'; import { STORE_NAME } from '../../stores/axoStore';
export const Payment = ( { fastlaneSdk, card, onPaymentLoad } ) => { export const Payment = ( { fastlaneSdk, onPaymentLoad } ) => {
const [ isCardElementReady, setIsCardElementReady ] = useState( false ); const [ isCardElementReady, setIsCardElementReady ] = useState( false );
const { isGuest, isEmailLookupCompleted } = useSelect(
const isGuest = useSelect( ( select ) => ( select ) => ( {
select( STORE_NAME ).getIsGuest() isGuest: select( STORE_NAME ).getIsGuest(),
); isEmailLookupCompleted:
select( STORE_NAME ).getIsEmailLookupCompleted(),
const isEmailLookupCompleted = useSelect( ( select ) => } ),
select( STORE_NAME ).getIsEmailLookupCompleted() []
); );
const loadPaymentComponent = useCallback( async () => { const loadPaymentComponent = useCallback( async () => {
@ -54,11 +54,5 @@ export const Payment = ( { fastlaneSdk, card, onPaymentLoad } ) => {
</div> </div>
); );
} }
return ( return <Card fastlaneSdk={ fastlaneSdk } showWatermark={ ! isGuest } />;
<Card
card={ card }
fastlaneSdk={ fastlaneSdk }
showWatermark={ ! isGuest }
/>
);
}; };

View file

@ -6,7 +6,7 @@ import { setIsGuest, setIsEmailLookupCompleted } from '../stores/axoStore';
export const createEmailLookupHandler = ( export const createEmailLookupHandler = (
fastlaneSdk, fastlaneSdk,
setShippingAddress, setShippingAddress,
setCard, setCardDetails,
snapshotFields, snapshotFields,
wooShippingAddress, wooShippingAddress,
wooBillingAddress, wooBillingAddress,
@ -68,7 +68,7 @@ export const createEmailLookupHandler = (
setShippingAddress( profileData.shippingAddress ); setShippingAddress( profileData.shippingAddress );
} }
if ( profileData && profileData.card ) { if ( profileData && profileData.card ) {
setCard( profileData.card ); setCardDetails( profileData.card );
} }
console.log( 'Profile Data:', profileData ); console.log( 'Profile Data:', profileData );

View file

@ -5,23 +5,22 @@ import usePayPalScript from './usePayPalScript';
import { setupWatermark } from '../components/Watermark'; import { setupWatermark } from '../components/Watermark';
import { setupEmailFunctionality } from '../components/EmailButton'; import { setupEmailFunctionality } from '../components/EmailButton';
import { createEmailLookupHandler } from '../events/emailLookupManager'; import { createEmailLookupHandler } from '../events/emailLookupManager';
import { usePhoneSyncHandler } from './usePhoneSyncHandler'; import usePhoneSyncHandler from './usePhoneSyncHandler';
import { initializeClassToggles } from '../helpers/classnamesManager'; import { initializeClassToggles } from '../helpers/classnamesManager';
import { snapshotFields } from '../helpers/fieldHelpers'; import { snapshotFields } from '../helpers/fieldHelpers';
import useCustomerData from './useCustomerData'; import useCustomerData from './useCustomerData';
import useShippingAddressChange from './useShippingAddressChange'; import useShippingAddressChange from './useShippingAddressChange';
import useCardChange from './useCardChange';
const useAxoSetup = ( const useAxoSetup = ( ppcpConfig, fastlaneSdk, paymentComponent ) => {
ppcpConfig, const {
fastlaneSdk, setIsAxoActive,
paymentComponent, setIsAxoScriptLoaded,
onChangeCardButtonClick, setShippingAddress,
setShippingAddress, setCardDetails,
setCard } = useDispatch( STORE_NAME );
) => {
const { setIsAxoActive, setIsAxoScriptLoaded } = useDispatch( STORE_NAME );
const paypalLoaded = usePayPalScript( ppcpConfig ); const paypalLoaded = usePayPalScript( ppcpConfig );
const onChangeCardButtonClick = useCardChange( fastlaneSdk );
const onChangeShippingAddressClick = useShippingAddressChange( const onChangeShippingAddressClick = useShippingAddressChange(
fastlaneSdk, fastlaneSdk,
setShippingAddress setShippingAddress
@ -53,7 +52,7 @@ const useAxoSetup = (
const emailLookupHandler = createEmailLookupHandler( const emailLookupHandler = createEmailLookupHandler(
fastlaneSdk, fastlaneSdk,
setShippingAddress, setShippingAddress,
setCard, setCardDetails,
snapshotFields, snapshotFields,
wooShippingAddress, wooShippingAddress,
wooBillingAddress, wooBillingAddress,
@ -76,7 +75,7 @@ const useAxoSetup = (
onChangeShippingAddressClick, onChangeShippingAddressClick,
onChangeCardButtonClick, onChangeCardButtonClick,
setShippingAddress, setShippingAddress,
setCard, setCardDetails,
paymentComponent, paymentComponent,
] ); ] );

View file

@ -1,53 +1,73 @@
import { useCallback } from '@wordpress/element'; import { useCallback } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { useAddressEditing } from './useAddressEditing'; import { useAddressEditing } from './useAddressEditing';
import useCustomerData from './useCustomerData'; import useCustomerData from './useCustomerData';
import { STORE_NAME } from '../stores/axoStore';
export const useCardChange = ( fastlaneSdk, setCard ) => { export const useCardChange = ( fastlaneSdk ) => {
const { setBillingAddressEditing } = useAddressEditing(); const { setBillingAddressEditing } = useAddressEditing();
const { setBillingAddress: setWooBillingAddress } = useCustomerData(); const { setBillingAddress: setWooBillingAddress } = useCustomerData();
const { setCardDetails, setShippingAddress } = useDispatch( STORE_NAME );
return useCallback( async () => { return useCallback( async () => {
if ( fastlaneSdk ) { if ( fastlaneSdk ) {
const { selectionChanged, selectedCard } = const { selectionChanged, selectedCard } =
await fastlaneSdk.profile.showCardSelector(); await fastlaneSdk.profile.showCardSelector();
if ( selectionChanged ) {
setCard( selectedCard ); if ( selectionChanged && selectedCard?.paymentSource?.card ) {
console.log( 'Selected card changed:', selectedCard ); // Use the fallback logic for cardholder's name.
console.log( 'Setting new billing details:', selectedCard );
const { name, billingAddress } = const { name, billingAddress } =
selectedCard.paymentSource.card; selectedCard.paymentSource.card;
// Split the full name into first and last name // If name is missing, use billing details as a fallback for the name.
const nameParts = name.split( ' ' ); let firstName = '';
const firstName = nameParts[ 0 ]; let lastName = '';
const lastName = nameParts.slice( 1 ).join( ' ' );
if ( name ) {
const nameParts = name.split( ' ' );
firstName = nameParts[ 0 ];
lastName = nameParts.slice( 1 ).join( ' ' );
}
const newBillingAddress = { const newBillingAddress = {
first_name: firstName, first_name: firstName,
last_name: lastName, last_name: lastName,
address_1: billingAddress.addressLine1, address_1: billingAddress?.addressLine1 || '',
address_2: billingAddress.addressLine2 || '', address_2: billingAddress?.addressLine2 || '',
city: billingAddress.adminArea2, city: billingAddress?.adminArea2 || '',
state: billingAddress.adminArea1, state: billingAddress?.adminArea1 || '',
postcode: billingAddress.postalCode, postcode: billingAddress?.postalCode || '',
country: billingAddress.countryCode, country: billingAddress?.countryCode || '',
}; };
await new Promise( ( resolve ) => { // Batch state updates.
setWooBillingAddress( newBillingAddress ); await Promise.all( [
resolve(); new Promise( ( resolve ) => {
} ); setCardDetails( selectedCard );
resolve();
await new Promise( ( resolve ) => { } ),
setBillingAddressEditing( false ); new Promise( ( resolve ) => {
resolve(); setWooBillingAddress( newBillingAddress );
} ); resolve();
} ),
new Promise( ( resolve ) => {
setShippingAddress( newBillingAddress );
resolve();
} ),
new Promise( ( resolve ) => {
setBillingAddressEditing( false );
resolve();
} ),
] );
} else {
console.error( 'Selected card or billing address is missing.' );
} }
} }
}, [ }, [
fastlaneSdk, fastlaneSdk,
setCard, setCardDetails,
setWooBillingAddress, setWooBillingAddress,
setShippingAddress,
setBillingAddressEditing, setBillingAddressEditing,
] ); ] );
}; };

View file

@ -1,14 +1,23 @@
import { useCallback } from '@wordpress/element'; import { useCallback } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { STORE_NAME } from '../stores/axoStore';
const useHandlePaymentSetup = ( const useHandlePaymentSetup = (
emitResponse, emitResponse,
card,
paymentComponent, paymentComponent,
tokenizedCustomerData tokenizedCustomerData
) => { ) => {
const { cardDetails } = useSelect(
( select ) => ( {
shippingAddress: select( STORE_NAME ).getShippingAddress(),
cardDetails: select( STORE_NAME ).getCardDetails(),
} ),
[]
);
return useCallback( async () => { return useCallback( async () => {
const isRyanFlow = !! card?.id; const isRyanFlow = !! cardDetails?.id;
let cardToken = card?.id; let cardToken = cardDetails?.id;
if ( ! cardToken && paymentComponent ) { if ( ! cardToken && paymentComponent ) {
cardToken = await paymentComponent cardToken = await paymentComponent
@ -38,7 +47,13 @@ const useHandlePaymentSetup = (
}, },
}, },
}; };
}, [ card, paymentComponent, tokenizedCustomerData ] ); }, [
cardDetails?.id,
emitResponse.responseTypes.ERROR,
emitResponse.responseTypes.SUCCESS,
paymentComponent,
tokenizedCustomerData,
] );
}; };
export default useHandlePaymentSetup; export default useHandlePaymentSetup;

View file

@ -1,8 +1,9 @@
import { useEffect, useRef } from 'react'; import { useEffect, useRef, useCallback } from '@wordpress/element';
import { useSelect } from '@wordpress/data'; import { useSelect, useDispatch } from '@wordpress/data';
import { debounce } from '../../../../ppcp-blocks/resources/js/Helper/debounce'; import { debounce } from '../../../../ppcp-blocks/resources/js/Helper/debounce';
import { STORE_NAME } from '../stores/axoStore';
import useCustomerData from './useCustomerData';
const WOO_STORE_NAME = 'wc/store/cart';
const PHONE_DEBOUNCE_DELAY = 250; const PHONE_DEBOUNCE_DELAY = 250;
/** /**
@ -15,23 +16,9 @@ const PHONE_DEBOUNCE_DELAY = 250;
const sanitizePhoneNumber = ( phoneNumber = '' ) => { const sanitizePhoneNumber = ( phoneNumber = '' ) => {
const localNumber = phoneNumber.replace( /^\+?[01]+/, '' ); const localNumber = phoneNumber.replace( /^\+?[01]+/, '' );
const cleanNumber = localNumber.replace( /[^0-9]/g, '' ); const cleanNumber = localNumber.replace( /[^0-9]/g, '' );
return cleanNumber.length === 10 ? cleanNumber : ''; return cleanNumber.length === 10 ? cleanNumber : '';
}; };
/**
* Retrieves and sanitizes the phone number from WooCommerce customer data.
*
* @param {Function} select - The select function from @wordpress/data.
* @return {string} The sanitized phone number.
*/
const getSanitizedPhoneNumber = ( select ) => {
const data = select( WOO_STORE_NAME ).getCustomerData() || {};
const billingPhone = sanitizePhoneNumber( data.billingAddress?.phone );
const shippingPhone = sanitizePhoneNumber( data.shippingAddress?.phone );
return billingPhone || shippingPhone || '';
};
/** /**
* Updates the prefilled phone number in the Fastlane CardField component. * Updates the prefilled phone number in the Fastlane CardField component.
* *
@ -48,17 +35,38 @@ const updatePrefills = ( paymentComponent, phoneNumber ) => {
* *
* @param {Object} paymentComponent - The CardField component from Fastlane. * @param {Object} paymentComponent - The CardField component from Fastlane.
*/ */
export const usePhoneSyncHandler = ( paymentComponent ) => { const usePhoneSyncHandler = ( paymentComponent ) => {
// Fetch and sanitize phone number from WooCommerce. const { setPhoneNumber } = useDispatch( STORE_NAME );
const phoneNumber = useSelect( ( select ) =>
getSanitizedPhoneNumber( select ) const { phoneNumber } = useSelect( ( select ) => ( {
); phoneNumber: select( STORE_NAME ).getPhoneNumber(),
} ) );
const { shippingAddress, billingAddress } = useCustomerData();
// Create a debounced function that updates the prefilled phone-number. // Create a debounced function that updates the prefilled phone-number.
const debouncedUpdatePhone = useRef( const debouncedUpdatePhone = useRef(
debounce( updatePrefills, PHONE_DEBOUNCE_DELAY ) debounce( updatePrefills, PHONE_DEBOUNCE_DELAY )
).current; ).current;
// Fetch and update the phone number from the billing or shipping address.
const fetchAndUpdatePhoneNumber = useCallback( () => {
const billingPhone = billingAddress?.phone || '';
const shippingPhone = shippingAddress?.phone || '';
const sanitizedPhoneNumber = sanitizePhoneNumber(
billingPhone || shippingPhone
);
if ( sanitizedPhoneNumber && sanitizedPhoneNumber !== phoneNumber ) {
setPhoneNumber( sanitizedPhoneNumber );
}
}, [ billingAddress, shippingAddress, phoneNumber, setPhoneNumber ] );
// Fetch and update the phone number from the billing or shipping address.
useEffect( () => {
fetchAndUpdatePhoneNumber();
}, [ fetchAndUpdatePhoneNumber ] );
// Invoke debounced function when paymentComponent or phoneNumber changes. // Invoke debounced function when paymentComponent or phoneNumber changes.
useEffect( () => { useEffect( () => {
if ( paymentComponent && phoneNumber ) { if ( paymentComponent && phoneNumber ) {
@ -75,3 +83,5 @@ export const usePhoneSyncHandler = ( paymentComponent ) => {
}; };
}, [ debouncedUpdatePhone ] ); }, [ debouncedUpdatePhone ] );
}; };
export default usePhoneSyncHandler;

View file

@ -5,14 +5,13 @@ import { registerPaymentMethod } from '@woocommerce/blocks-registry';
// Hooks // Hooks
import useFastlaneSdk from './hooks/useFastlaneSdk'; import useFastlaneSdk from './hooks/useFastlaneSdk';
import useTokenizeCustomerData from './hooks/useTokenizeCustomerData'; import useTokenizeCustomerData from './hooks/useTokenizeCustomerData';
import useCardChange from './hooks/useCardChange';
import useAxoSetup from './hooks/useAxoSetup'; import useAxoSetup from './hooks/useAxoSetup';
import useAxoCleanup from './hooks/useAxoCleanup'; import useAxoCleanup from './hooks/useAxoCleanup';
import useHandlePaymentSetup from './hooks/useHandlePaymentSetup'; import useHandlePaymentSetup from './hooks/useHandlePaymentSetup';
import usePaymentSetupEffect from './hooks/usePaymentSetupEffect';
// Components // Components
import { Payment } from './components/Payment/Payment'; import { Payment } from './components/Payment/Payment';
import usePaymentSetupEffect from './hooks/usePaymentSetupEffect';
const gatewayHandle = 'ppcp-axo-gateway'; const gatewayHandle = 'ppcp-axo-gateway';
const ppcpConfig = wc.wcSettings.getSetting( `${ gatewayHandle }_data` ); const ppcpConfig = wc.wcSettings.getSetting( `${ gatewayHandle }_data` );
@ -26,28 +25,17 @@ const axoConfig = window.wc_ppcp_axo;
const Axo = ( props ) => { const Axo = ( props ) => {
const { eventRegistration, emitResponse } = props; const { eventRegistration, emitResponse } = props;
const { onPaymentSetup } = eventRegistration; const { onPaymentSetup } = eventRegistration;
const [ shippingAddress, setShippingAddress ] = useState( null );
const [ card, setCard ] = useState( null );
const [ paymentComponent, setPaymentComponent ] = useState( null ); const [ paymentComponent, setPaymentComponent ] = useState( null );
const fastlaneSdk = useFastlaneSdk( axoConfig, ppcpConfig ); const fastlaneSdk = useFastlaneSdk( axoConfig, ppcpConfig );
const tokenizedCustomerData = useTokenizeCustomerData(); const tokenizedCustomerData = useTokenizeCustomerData();
const onChangeCardButtonClick = useCardChange( fastlaneSdk, setCard );
const handlePaymentSetup = useHandlePaymentSetup( const handlePaymentSetup = useHandlePaymentSetup(
emitResponse, emitResponse,
card,
paymentComponent, paymentComponent,
tokenizedCustomerData tokenizedCustomerData
); );
useAxoSetup( useAxoSetup( ppcpConfig, fastlaneSdk, paymentComponent );
ppcpConfig,
fastlaneSdk,
paymentComponent,
onChangeCardButtonClick,
setShippingAddress,
setCard
);
const { handlePaymentLoad } = usePaymentSetupEffect( const { handlePaymentLoad } = usePaymentSetupEffect(
onPaymentSetup, onPaymentSetup,
@ -57,24 +45,10 @@ const Axo = ( props ) => {
useAxoCleanup(); useAxoCleanup();
const handleCardChange = ( selectedCard ) => {
console.log( 'Card selection changed', selectedCard );
setCard( selectedCard );
};
console.log( 'Rendering Axo component', {
fastlaneSdk,
card,
shippingAddress,
} );
return fastlaneSdk ? ( return fastlaneSdk ? (
<Payment <Payment
fastlaneSdk={ fastlaneSdk } fastlaneSdk={ fastlaneSdk }
card={ card }
onChange={ handleCardChange }
onPaymentLoad={ handlePaymentLoad } onPaymentLoad={ handlePaymentLoad }
onChangeButtonClick={ onChangeCardButtonClick }
/> />
) : ( ) : (
<>{ __( 'Loading Fastlane…', 'woocommerce-paypal-payments' ) }</> <>{ __( 'Loading Fastlane…', 'woocommerce-paypal-payments' ) }</>

View file

@ -1,5 +1,3 @@
// File: axoStore.js
import { createReduxStore, register, dispatch } from '@wordpress/data'; import { createReduxStore, register, dispatch } from '@wordpress/data';
export const STORE_NAME = 'woocommerce-paypal-payments/axo-block'; export const STORE_NAME = 'woocommerce-paypal-payments/axo-block';
@ -11,6 +9,9 @@ const DEFAULT_STATE = {
isAxoScriptLoaded: false, isAxoScriptLoaded: false,
isEmailSubmitted: false, isEmailSubmitted: false,
isEmailLookupCompleted: false, isEmailLookupCompleted: false,
shippingAddress: null,
cardDetails: null,
phoneNumber: '',
}; };
// Actions // Actions
@ -35,6 +36,18 @@ const actions = {
type: 'SET_IS_EMAIL_LOOKUP_COMPLETED', type: 'SET_IS_EMAIL_LOOKUP_COMPLETED',
payload: isEmailLookupCompleted, payload: isEmailLookupCompleted,
} ), } ),
setShippingAddress: ( shippingAddress ) => ( {
type: 'SET_SHIPPING_ADDRESS',
payload: shippingAddress,
} ),
setCardDetails: ( cardDetails ) => ( {
type: 'SET_CARD_DETAILS',
payload: cardDetails,
} ),
setPhoneNumber: ( phoneNumber ) => ( {
type: 'SET_PHONE_NUMBER',
payload: phoneNumber,
} ),
}; };
// Reducer // Reducer
@ -50,6 +63,12 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
return { ...state, isEmailSubmitted: action.payload }; return { ...state, isEmailSubmitted: action.payload };
case 'SET_IS_EMAIL_LOOKUP_COMPLETED': case 'SET_IS_EMAIL_LOOKUP_COMPLETED':
return { ...state, isEmailLookupCompleted: action.payload }; return { ...state, isEmailLookupCompleted: action.payload };
case 'SET_SHIPPING_ADDRESS':
return { ...state, shippingAddress: action.payload };
case 'SET_CARD_DETAILS':
return { ...state, cardDetails: action.payload };
case 'SET_PHONE_NUMBER':
return { ...state, phoneNumber: action.payload };
default: default:
return state; return state;
} }
@ -62,6 +81,9 @@ const selectors = {
getIsAxoScriptLoaded: ( state ) => state.isAxoScriptLoaded, getIsAxoScriptLoaded: ( state ) => state.isAxoScriptLoaded,
getIsEmailSubmitted: ( state ) => state.isEmailSubmitted, getIsEmailSubmitted: ( state ) => state.isEmailSubmitted,
getIsEmailLookupCompleted: ( state ) => state.isEmailLookupCompleted, getIsEmailLookupCompleted: ( state ) => state.isEmailLookupCompleted,
getShippingAddress: ( state ) => state.shippingAddress,
getCardDetails: ( state ) => state.cardDetails,
getPhoneNumber: ( state ) => state.phoneNumber,
}; };
// Create and register the store // Create and register the store
@ -73,20 +95,23 @@ const store = createReduxStore( STORE_NAME, {
register( store ); register( store );
// Action dispatchers
export const setIsGuest = ( isGuest ) => { export const setIsGuest = ( isGuest ) => {
try { dispatch( STORE_NAME ).setIsGuest( isGuest );
dispatch( STORE_NAME ).setIsGuest( isGuest );
} catch ( error ) {
console.error( 'Error updating isGuest state:', error );
}
}; };
export const setIsEmailLookupCompleted = ( isEmailLookupCompleted ) => { export const setIsEmailLookupCompleted = ( isEmailLookupCompleted ) => {
try { dispatch( STORE_NAME ).setIsEmailLookupCompleted( isEmailLookupCompleted );
dispatch( STORE_NAME ).setIsEmailLookupCompleted( };
isEmailLookupCompleted
); export const setShippingAddress = ( shippingAddress ) => {
} catch ( error ) { dispatch( STORE_NAME ).setShippingAddress( shippingAddress );
console.error( 'Error updating isEmailLookupCompleted state:', error ); };
}
export const setCardDetails = ( cardDetails ) => {
dispatch( STORE_NAME ).setCardDetails( cardDetails );
};
export const setPhoneNumber = ( phoneNumber ) => {
dispatch( STORE_NAME ).setPhoneNumber( phoneNumber );
}; };