diff --git a/modules/ppcp-axo-block/resources/js/components/Card/Card.js b/modules/ppcp-axo-block/resources/js/components/Card/Card.js index 51dfa15ae..3bf39b10b 100644 --- a/modules/ppcp-axo-block/resources/js/components/Card/Card.js +++ b/modules/ppcp-axo-block/resources/js/components/Card/Card.js @@ -5,7 +5,7 @@ import { STORE_NAME } from '../../stores/axoStore'; const cardIcons = { VISA: 'visa-light.svg', - MASTER_CARD: 'mastercard-light.svg', + MASTERCARD: 'mastercard-light.svg', AMEX: 'amex-light.svg', DISCOVER: 'discover-light.svg', DINERS: 'dinersclub-light.svg', diff --git a/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButton.js b/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButton.js index 1779d67b6..c2a4eaa65 100644 --- a/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButton.js +++ b/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButton.js @@ -1,6 +1,13 @@ import { createElement } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +/** + * 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. + */ const CardChangeButton = ( { onChangeButtonClick } ) => createElement( 'a', @@ -9,7 +16,9 @@ const CardChangeButton = ( { onChangeButtonClick } ) => 'wc-block-checkout-axo-block-card__edit wc-block-axo-change-link', role: 'button', onClick: ( event ) => { + // Prevent default anchor behavior event.preventDefault(); + // Call the provided click handler onChangeButtonClick(); }, }, diff --git a/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButtonManager.js b/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButtonManager.js index c38e6c0b6..2b6deba4f 100644 --- a/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButtonManager.js +++ b/modules/ppcp-axo-block/resources/js/components/Card/CardChangeButtonManager.js @@ -1,6 +1,13 @@ 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( @@ -8,14 +15,17 @@ const CardChangeButtonManager = ( { onChangeButtonClick } ) => { ); 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 } ) @@ -23,6 +33,7 @@ const CardChangeButtonManager = ( { onChangeButtonClick } ) => { } } + // Cleanup function to remove the button when the component unmounts return () => { const button = document.querySelector( '.wc-block-checkout-axo-block-card__edit' @@ -33,6 +44,7 @@ const CardChangeButtonManager = ( { onChangeButtonClick } ) => { }; }, [ onChangeButtonClick ] ); + // This component doesn't render anything directly return null; }; diff --git a/modules/ppcp-axo-block/resources/js/components/Card/utils.js b/modules/ppcp-axo-block/resources/js/components/Card/utils.js index 93f903142..915511885 100644 --- a/modules/ppcp-axo-block/resources/js/components/Card/utils.js +++ b/modules/ppcp-axo-block/resources/js/components/Card/utils.js @@ -1,18 +1,31 @@ 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(); } diff --git a/modules/ppcp-axo-block/resources/js/components/EmailButton/EmailButton.js b/modules/ppcp-axo-block/resources/js/components/EmailButton/EmailButton.js index ab41f067e..a7fcbb86d 100644 --- a/modules/ppcp-axo-block/resources/js/components/EmailButton/EmailButton.js +++ b/modules/ppcp-axo-block/resources/js/components/EmailButton/EmailButton.js @@ -2,7 +2,15 @@ import { STORE_NAME } from '../../stores/axoStore'; import { useSelect } from '@wordpress/data'; import { __ } from '@wordpress/i18n'; +/** + * Renders a submit button for email input in the AXO checkout process. + * + * @param {Object} props + * @param {Function} props.handleSubmit - Function to handle button click/submit. + * @return {JSX.Element|null} The rendered button or null if conditions are not met. + */ const EmailButton = ( { handleSubmit } ) => { + // Select relevant states from the AXO store const { isGuest, isAxoActive, isEmailSubmitted } = useSelect( ( select ) => ( { isGuest: select( STORE_NAME ).getIsGuest(), @@ -11,6 +19,7 @@ const EmailButton = ( { handleSubmit } ) => { } ) ); + // Only render the button for guests when AXO is active if ( ! isGuest || ! isAxoActive ) { return null; } @@ -24,6 +33,7 @@ const EmailButton = ( { handleSubmit } ) => { }` } disabled={ isEmailSubmitted } > + { /* Button text */ } { > { __( 'Continue', 'woocommerce-paypal-payments' ) } + { /* Loading spinner */ } { isEmailSubmitted && ( { if ( ! emailInput ) { emailInput = document.getElementById( 'email' ); @@ -18,6 +24,11 @@ const getEmailInput = () => { return emailInput; }; +/** + * Sets up email functionality for AXO checkout. + * + * @param {Function} onEmailSubmit - Callback function to handle email submission. + */ export const setupEmailFunctionality = ( onEmailSubmit ) => { const input = getEmailInput(); if ( ! input ) { @@ -28,6 +39,7 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => { return; } + // Handler for email submission const handleEmailSubmit = async () => { const isEmailSubmitted = wp.data .select( STORE_NAME ) @@ -50,6 +62,7 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => { } }; + // Set up keydown handler for Enter key keydownHandler = ( event ) => { const isAxoActive = wp.data.select( STORE_NAME ).getIsAxoActive(); if ( event.key === 'Enter' && isAxoActive ) { @@ -78,6 +91,7 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => { ); } + // Function to render the EmailButton const renderButton = () => { if ( submitButtonReference.root ) { submitButtonReference.root.render( @@ -90,12 +104,15 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => { renderButton(); - // Subscribe to state changes + // Subscribe to state changes and re-render button submitButtonReference.unsubscribe = wp.data.subscribe( () => { renderButton(); } ); }; +/** + * Removes email functionality and cleans up event listeners and DOM elements. + */ export const removeEmailFunctionality = () => { const input = getEmailInput(); if ( input && keydownHandler ) { @@ -120,6 +137,11 @@ export const removeEmailFunctionality = () => { keydownHandler = null; }; +/** + * Checks if email functionality is currently set up. + * + * @return {boolean} True if email functionality is set up, false otherwise. + */ export const isEmailFunctionalitySetup = () => { return !! submitButtonReference.root; }; diff --git a/modules/ppcp-axo-block/resources/js/components/Payment/Payment.js b/modules/ppcp-axo-block/resources/js/components/Payment/Payment.js index d6c96c51c..e1191c8ce 100644 --- a/modules/ppcp-axo-block/resources/js/components/Payment/Payment.js +++ b/modules/ppcp-axo-block/resources/js/components/Payment/Payment.js @@ -4,8 +4,18 @@ import { __ } from '@wordpress/i18n'; import { Card } from '../Card'; import { STORE_NAME } from '../../stores/axoStore'; +/** + * Renders the payment component based on the user's state (guest or authenticated). + * + * @param {Object} props + * @param {Object} props.fastlaneSdk - The Fastlane SDK instance. + * @param {Function} props.onPaymentLoad - Callback function when payment component is loaded. + * @return {JSX.Element} The rendered payment component. + */ export const Payment = ( { fastlaneSdk, onPaymentLoad } ) => { const [ isCardElementReady, setIsCardElementReady ] = useState( false ); + + // Select relevant states from the AXO store const { isGuest, isEmailLookupCompleted } = useSelect( ( select ) => ( { isGuest: select( STORE_NAME ).getIsGuest(), @@ -31,16 +41,22 @@ export const Payment = ( { fastlaneSdk, onPaymentLoad } ) => { onPaymentLoad, ] ); + // Set card element ready when guest email lookup is completed useEffect( () => { if ( isGuest && isEmailLookupCompleted ) { setIsCardElementReady( true ); } }, [ isGuest, isEmailLookupCompleted ] ); + // Load payment component when dependencies change useEffect( () => { loadPaymentComponent(); }, [ loadPaymentComponent ] ); + // Conditional rendering based on user state: + // 1. If authenticated: Render the Card component + // 2. If guest with completed email lookup: Render the card fields + // 3. If guest without completed email lookup: Render a message to enter email if ( isGuest ) { if ( isEmailLookupCompleted ) { return
; diff --git a/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButton.js b/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButton.js index b7d55d508..a68de9789 100644 --- a/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButton.js +++ b/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButton.js @@ -1,11 +1,20 @@ import { __ } from '@wordpress/i18n'; +/** + * Renders a button to change the shipping address. + * + * @param {Object} props + * @param {Function} props.onChangeShippingAddressClick - Callback function to handle the click event. + * @return {JSX.Element} The rendered button as an anchor tag. + */ const ShippingChangeButton = ( { onChangeShippingAddressClick } ) => ( { + // Prevent default anchor behavior event.preventDefault(); + // Call the provided click handler onChangeShippingAddressClick(); } } > diff --git a/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButtonManager.js b/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButtonManager.js index e072f0193..abbd7e4da 100644 --- a/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButtonManager.js +++ b/modules/ppcp-axo-block/resources/js/components/Shipping/ShippingChangeButtonManager.js @@ -1,22 +1,32 @@ import { useEffect, createRoot } from '@wordpress/element'; import ShippingChangeButton from './ShippingChangeButton'; +/** + * Manages the insertion and removal of the ShippingChangeButton in the DOM. + * + * @param {Object} props + * @param {Function} props.onChangeShippingAddressClick - Callback function for when the shipping change button is clicked. + * @return {null} This component doesn't render any visible elements directly. + */ const ShippingChangeButtonManager = ( { onChangeShippingAddressClick } ) => { useEffect( () => { const shippingHeading = document.querySelector( '#shipping-fields .wc-block-components-checkout-step__heading' ); + // Check if the shipping heading exists and doesn't already have a change button if ( shippingHeading && ! shippingHeading.querySelector( '.wc-block-checkout-axo-block-card__edit' ) ) { + // Create a new span element to contain the ShippingChangeButton const spanElement = document.createElement( 'span' ); spanElement.className = 'wc-block-checkout-axo-block-card__edit'; shippingHeading.appendChild( spanElement ); + // Create a React root and render the ShippingChangeButton const root = createRoot( spanElement ); root.render( { /> ); + // Cleanup function to remove the button when the component unmounts return () => { root.unmount(); spanElement.remove(); @@ -33,6 +44,7 @@ const ShippingChangeButtonManager = ( { onChangeShippingAddressClick } ) => { } }, [ onChangeShippingAddressClick ] ); + // This component doesn't render anything directly return null; }; diff --git a/modules/ppcp-axo-block/resources/js/components/Shipping/utils.js b/modules/ppcp-axo-block/resources/js/components/Shipping/utils.js index 1bce1ea3f..a131da555 100644 --- a/modules/ppcp-axo-block/resources/js/components/Shipping/utils.js +++ b/modules/ppcp-axo-block/resources/js/components/Shipping/utils.js @@ -1,14 +1,23 @@ import { createRoot } from '@wordpress/element'; import ShippingChangeButtonManager from './ShippingChangeButtonManager'; +/** + * Injects a shipping change button into the DOM if it doesn't already exist. + * + * @param {Function} onChangeShippingAddressClick - Callback function for when the shipping change button is clicked. + */ export const injectShippingChangeButton = ( onChangeShippingAddressClick ) => { + // Check if the button already exists const existingButton = document.querySelector( '#shipping-fields .wc-block-checkout-axo-block-card__edit' ); if ( ! existingButton ) { + // Create a new container for the button const container = document.createElement( 'div' ); document.body.appendChild( container ); + + // Render the ShippingChangeButtonManager in the new container createRoot( container ).render( { } }; +/** + * Removes the shipping change button from the DOM if it exists. + */ export const removeShippingChangeButton = () => { const span = document.querySelector( '#shipping-fields .wc-block-checkout-axo-block-card__edit' diff --git a/modules/ppcp-axo-block/resources/js/components/Watermark/Watermark.js b/modules/ppcp-axo-block/resources/js/components/Watermark/Watermark.js index fcc098768..2ec89de90 100644 --- a/modules/ppcp-axo-block/resources/js/components/Watermark/Watermark.js +++ b/modules/ppcp-axo-block/resources/js/components/Watermark/Watermark.js @@ -1,6 +1,15 @@ import { useEffect, useRef } from '@wordpress/element'; import { log } from '../../../../../ppcp-axo/resources/js/Helper/Debug'; +/** + * Watermark component for displaying AXO watermark. + * + * @param {Object} props + * @param {Object} props.fastlaneSdk - The Fastlane SDK instance. + * @param {string} [props.name='fastlane-watermark-container'] - ID for the watermark container. + * @param {boolean} [props.includeAdditionalInfo=true] - Whether to include additional info in the watermark. + * @return {JSX.Element} The watermark container element. + */ const Watermark = ( { fastlaneSdk, name = 'fastlane-watermark-container', @@ -10,15 +19,19 @@ const Watermark = ( { const watermarkRef = useRef( null ); useEffect( () => { + /** + * Renders the Fastlane watermark. + */ const renderWatermark = async () => { if ( ! containerRef.current ) { return; } - // Clear the container + // Clear the container before rendering containerRef.current.innerHTML = ''; try { + // Create and render the Fastlane watermark const watermark = await fastlaneSdk.FastlaneWatermarkComponent( { includeAdditionalInfo, @@ -34,6 +47,7 @@ const Watermark = ( { renderWatermark(); + // Cleanup function to clear the container on unmount return () => { if ( containerRef.current ) { containerRef.current.innerHTML = ''; @@ -41,6 +55,7 @@ const Watermark = ( { }; }, [ fastlaneSdk, name, includeAdditionalInfo ] ); + // Render the container for the watermark return
; }; diff --git a/modules/ppcp-axo-block/resources/js/components/Watermark/WatermarkManager.js b/modules/ppcp-axo-block/resources/js/components/Watermark/WatermarkManager.js index 41c078585..ef175a98f 100644 --- a/modules/ppcp-axo-block/resources/js/components/Watermark/WatermarkManager.js +++ b/modules/ppcp-axo-block/resources/js/components/Watermark/WatermarkManager.js @@ -7,7 +7,15 @@ import { updateWatermarkContent, } from './utils'; +/** + * Manages the lifecycle and content of the AXO watermark. + * + * @param {Object} props + * @param {Object} props.fastlaneSdk - The Fastlane SDK instance. + * @return {null} This component doesn't render any visible elements. + */ const WatermarkManager = ( { fastlaneSdk } ) => { + // Select relevant states from the AXO store const isGuest = useSelect( ( select ) => select( STORE_NAME ).getIsGuest() ); @@ -20,6 +28,7 @@ const WatermarkManager = ( { fastlaneSdk } ) => { useEffect( () => { if ( isAxoActive || ( ! isAxoActive && ! isAxoScriptLoaded ) ) { + // Create watermark container and update content when AXO is active or loading createWatermarkContainer(); updateWatermarkContent( { isAxoActive, @@ -28,12 +37,15 @@ const WatermarkManager = ( { fastlaneSdk } ) => { isGuest, } ); } else { + // Remove watermark when AXO is inactive and not loading removeWatermark(); } + // Cleanup function to remove watermark on unmount return removeWatermark; }, [ fastlaneSdk, isGuest, isAxoActive, isAxoScriptLoaded ] ); + // This component doesn't render anything directly return null; }; diff --git a/modules/ppcp-axo-block/resources/js/components/Watermark/utils.js b/modules/ppcp-axo-block/resources/js/components/Watermark/utils.js index 38f4a7582..638bffe61 100644 --- a/modules/ppcp-axo-block/resources/js/components/Watermark/utils.js +++ b/modules/ppcp-axo-block/resources/js/components/Watermark/utils.js @@ -1,11 +1,15 @@ import { createElement, createRoot } from '@wordpress/element'; import { Watermark, WatermarkManager } from '../Watermark'; +// Object to store references to the watermark container and root const watermarkReference = { container: null, root: null, }; +/** + * Creates a container for the watermark in the checkout contact information block. + */ export const createWatermarkContainer = () => { const textInputContainer = document.querySelector( '.wp-block-woocommerce-checkout-contact-information-block .wc-block-components-text-input' @@ -16,6 +20,7 @@ export const createWatermarkContainer = () => { textInputContainer.querySelector( 'input[id="email"]' ); if ( emailInput ) { + // Create watermark container watermarkReference.container = document.createElement( 'div' ); watermarkReference.container.setAttribute( 'class', @@ -26,7 +31,7 @@ export const createWatermarkContainer = () => { '.wc-block-axo-email-submit-button-container' ); - // If possible, insert the watermark after the "Continue" button. + // Insert the watermark after the "Continue" button or email input const insertAfterElement = emailButton || emailInput; insertAfterElement.parentNode.insertBefore( @@ -34,6 +39,7 @@ export const createWatermarkContainer = () => { insertAfterElement.nextSibling ); + // Create a root for the watermark watermarkReference.root = createRoot( watermarkReference.container ); @@ -41,12 +47,19 @@ export const createWatermarkContainer = () => { } }; +/** + * Sets up the watermark manager component. + * + * @param {Object} fastlaneSdk - The Fastlane SDK instance. + * @return {Function} Cleanup function to remove the watermark. + */ export const setupWatermark = ( fastlaneSdk ) => { const container = document.createElement( 'div' ); document.body.appendChild( container ); const root = createRoot( container ); root.render( createElement( WatermarkManager, { fastlaneSdk } ) ); + // Return cleanup function return () => { root.unmount(); if ( container && container.parentNode ) { @@ -55,6 +68,9 @@ export const setupWatermark = ( fastlaneSdk ) => { }; }; +/** + * Removes the watermark from the DOM and resets the reference. + */ export const removeWatermark = () => { if ( watermarkReference.root ) { watermarkReference.root.unmount(); @@ -65,6 +81,7 @@ export const removeWatermark = () => { watermarkReference.container ); } else { + // Fallback removal if parent node is not available const detachedContainer = document.querySelector( '.wc-block-checkout-axo-block-watermark-container' ); @@ -73,15 +90,30 @@ export const removeWatermark = () => { } } } + // Reset watermark reference Object.assign( watermarkReference, { container: null, root: null } ); }; +/** + * Renders content in the watermark container. + * + * @param {ReactElement} content - The content to render. + */ export const renderWatermarkContent = ( content ) => { if ( watermarkReference.root ) { watermarkReference.root.render( content ); } }; +/** + * Updates the watermark content based on the current state. + * + * @param {Object} params - State parameters. + * @param {boolean} params.isAxoActive - Whether AXO is active. + * @param {boolean} params.isAxoScriptLoaded - Whether AXO script is loaded. + * @param {Object} params.fastlaneSdk - The Fastlane SDK instance. + * @param {boolean} params.isGuest - Whether the user is a guest. + */ export const updateWatermarkContent = ( { isAxoActive, isAxoScriptLoaded, @@ -89,6 +121,7 @@ export const updateWatermarkContent = ( { isGuest, } ) => { if ( ! isAxoActive && ! isAxoScriptLoaded ) { + // Show loading spinner renderWatermarkContent( createElement( 'span', { className: 'wc-block-components-spinner', @@ -96,6 +129,7 @@ export const updateWatermarkContent = ( { } ) ); } else if ( isAxoActive ) { + // Show Fastlane watermark renderWatermarkContent( createElement( Watermark, { fastlaneSdk, @@ -104,6 +138,7 @@ export const updateWatermarkContent = ( { } ) ); } else { + // Clear watermark content renderWatermarkContent( null ); } }; diff --git a/modules/ppcp-axo-block/resources/js/events/emailLookupManager.js b/modules/ppcp-axo-block/resources/js/events/emailLookupManager.js index 85efa0234..563f9510d 100644 --- a/modules/ppcp-axo-block/resources/js/events/emailLookupManager.js +++ b/modules/ppcp-axo-block/resources/js/events/emailLookupManager.js @@ -4,6 +4,21 @@ import { injectShippingChangeButton } from '../components/Shipping'; import { injectCardChangeButton } from '../components/Card'; import { setIsGuest, setIsEmailLookupCompleted } from '../stores/axoStore'; +/** + * Creates an email lookup handler function for AXO checkout. + * + * @param {Object} fastlaneSdk - The Fastlane SDK instance. + * @param {Function} setShippingAddress - Function to set shipping address in the store. + * @param {Function} setCardDetails - Function to set card details in the store. + * @param {Function} snapshotFields - Function to save current field values. + * @param {Object} wooShippingAddress - Current WooCommerce shipping address. + * @param {Object} wooBillingAddress - Current WooCommerce billing address. + * @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 = ( fastlaneSdk, setShippingAddress, @@ -20,6 +35,7 @@ export const createEmailLookupHandler = ( try { log( `Email value being looked up: ${ email }` ); + // Validate Fastlane SDK initialization if ( ! fastlaneSdk ) { throw new Error( 'FastlaneSDK is not initialized' ); } @@ -30,12 +46,13 @@ export const createEmailLookupHandler = ( ); } + // Perform email lookup const lookup = await fastlaneSdk.identity.lookupCustomerByEmail( email ); log( `Lookup response: ${ JSON.stringify( lookup ) }` ); - // Gary flow + // Handle Gary flow (new user) if ( lookup && lookup.customerContextId === '' ) { setIsEmailLookupCompleted( true ); } @@ -45,6 +62,7 @@ export const createEmailLookupHandler = ( return; } + // Trigger authentication flow const authResponse = await fastlaneSdk.identity.triggerAuthenticationFlow( lookup.customerContextId @@ -56,15 +74,18 @@ export const createEmailLookupHandler = ( const { authenticationState, profileData } = authResponse; - // OTP success/fail/cancel flow + // Mark email lookup as completed for OTP flow if ( authResponse ) { setIsEmailLookupCompleted( true ); } + // Handle successful authentication if ( authenticationState === 'succeeded' ) { + // Save current field values snapshotFields( wooShippingAddress, wooBillingAddress ); setIsGuest( false ); + // Update store with profile data if ( profileData && profileData.shippingAddress ) { setShippingAddress( profileData.shippingAddress ); } @@ -74,12 +95,14 @@ export const createEmailLookupHandler = ( log( `Profile Data: ${ JSON.stringify( profileData ) }` ); + // Populate WooCommerce fields with profile data populateWooFields( profileData, setWooShippingAddress, setWooBillingAddress ); + // Inject change buttons for shipping and card injectShippingChangeButton( onChangeShippingAddressClick ); injectCardChangeButton( onChangeCardButtonClick ); } else { diff --git a/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js b/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js index 2af96aea9..411f815cb 100644 --- a/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js +++ b/modules/ppcp-axo-block/resources/js/helpers/classnamesManager.js @@ -41,6 +41,10 @@ export const setupAuthenticationClassToggle = () => { return unsubscribe; }; +/** + * Sets up a class toggle based on the isEmailLookupCompleted state for the checkout fields block. + * @return {Function} Unsubscribe function for cleanup. + */ export const setupEmailLookupCompletedClassToggle = () => { const targetSelector = '.wp-block-woocommerce-checkout-fields-block'; const emailLookupCompletedClass = 'wc-block-axo-email-lookup-completed'; @@ -77,7 +81,7 @@ export const setupEmailLookupCompletedClassToggle = () => { }; /** - * Sets up class toggles for the contact information block based on isAxoActive and isGuest states. + * Sets up class toggles for the contact information block based on isAxoActive, isGuest, and isEmailLookupCompleted states. * @return {Function} Unsubscribe function for cleanup. */ export const setupCheckoutBlockClassToggles = () => { @@ -133,7 +137,7 @@ export const setupCheckoutBlockClassToggles = () => { /** * Initializes all class toggles. - * @return {Function} Cleanup function. + * @return {Function} Cleanup function to unsubscribe all listeners. */ export const initializeClassToggles = () => { const unsubscribeAuth = setupAuthenticationClassToggle(); @@ -141,6 +145,7 @@ export const initializeClassToggles = () => { setupEmailLookupCompletedClassToggle(); const unsubscribeContactInfo = setupCheckoutBlockClassToggles(); + // Return a cleanup function that unsubscribes all listeners return () => { if ( unsubscribeAuth ) { unsubscribeAuth(); diff --git a/modules/ppcp-axo-block/resources/js/helpers/fieldHelpers.js b/modules/ppcp-axo-block/resources/js/helpers/fieldHelpers.js index 9c66d762d..c04b62a24 100644 --- a/modules/ppcp-axo-block/resources/js/helpers/fieldHelpers.js +++ b/modules/ppcp-axo-block/resources/js/helpers/fieldHelpers.js @@ -1,6 +1,12 @@ import { dispatch } from '@wordpress/data'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; +/** + * Saves the current shipping and billing address to localStorage. + * + * @param {Object} shippingAddress - The current shipping address. + * @param {Object} billingAddress - The current billing address. + */ export const snapshotFields = ( shippingAddress, billingAddress ) => { if ( ! shippingAddress || ! billingAddress ) { log( @@ -15,6 +21,7 @@ export const snapshotFields = ( shippingAddress, billingAddress ) => { const originalData = { shippingAddress, billingAddress }; log( `Snapshot data: ${ JSON.stringify( originalData ) }` ); try { + // Save the original data to localStorage localStorage.setItem( 'axoOriginalCheckoutFields', JSON.stringify( originalData ) @@ -24,6 +31,12 @@ export const snapshotFields = ( shippingAddress, billingAddress ) => { } }; +/** + * Restores the original shipping and billing addresses from localStorage. + * + * @param {Function} updateShippingAddress - Function to update the shipping address. + * @param {Function} updateBillingAddress - Function to update the billing address. + */ export const restoreOriginalFields = ( updateShippingAddress, updateBillingAddress @@ -31,6 +44,7 @@ export const restoreOriginalFields = ( log( 'Attempting to restore original fields' ); let savedData; try { + // Retrieve saved data from localStorage savedData = localStorage.getItem( 'axoOriginalCheckoutFields' ); log( `Data retrieved from localStorage: ${ JSON.stringify( savedData ) }` @@ -42,11 +56,13 @@ export const restoreOriginalFields = ( if ( savedData ) { try { const parsedData = JSON.parse( savedData ); + // Restore shipping address if available if ( parsedData.shippingAddress ) { updateShippingAddress( parsedData.shippingAddress ); } else { log( `No shipping address found in saved data`, 'warn' ); } + // Restore billing address if available if ( parsedData.billingAddress ) { log( `Restoring billing address: @@ -67,6 +83,13 @@ export const restoreOriginalFields = ( } }; +/** + * Populates WooCommerce fields with profile data from AXO. + * + * @param {Object} profileData - The profile data from AXO. + * @param {Function} setWooShippingAddress - Function to set WooCommerce shipping address. + * @param {Function} setWooBillingAddress - Function to set WooCommerce billing address. + */ export const populateWooFields = ( profileData, setWooShippingAddress, @@ -82,14 +105,14 @@ export const populateWooFields = ( const checkoutDispatch = dispatch( CHECKOUT_STORE_KEY ); - // Uncheck the 'Use same address for billing' checkbox if the method exists. + // Uncheck the 'Use same address for billing' checkbox if the method exists if ( typeof checkoutDispatch.__internalSetUseShippingAsBilling === 'function' ) { checkoutDispatch.__internalSetUseShippingAsBilling( false ); } - // Save shipping address. + // Prepare and set shipping address const { address, name, phoneNumber } = profileData.shippingAddress; const shippingAddress = { @@ -111,7 +134,7 @@ export const populateWooFields = ( ); setWooShippingAddress( shippingAddress ); - // Save billing address. + // Prepare and set billing address const billingData = profileData.card.paymentSource.card.billingAddress; const billingAddress = { @@ -132,12 +155,12 @@ export const populateWooFields = ( ); setWooBillingAddress( billingAddress ); - // Collapse shipping address input fields into the card view. + // Collapse shipping address input fields into the card view if ( typeof checkoutDispatch.setEditingShippingAddress === 'function' ) { checkoutDispatch.setEditingShippingAddress( false ); } - // Collapse billing address input fields into the card view. + // Collapse billing address input fields into the card view if ( typeof checkoutDispatch.setEditingBillingAddress === 'function' ) { checkoutDispatch.setEditingBillingAddress( false ); } diff --git a/modules/ppcp-axo-block/resources/js/hooks/useAddressEditing.js b/modules/ppcp-axo-block/resources/js/hooks/useAddressEditing.js index 59fc32769..f1e57c943 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useAddressEditing.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useAddressEditing.js @@ -3,11 +3,21 @@ import { useDispatch, useSelect } from '@wordpress/data'; const CHECKOUT_STORE_KEY = 'wc/store/checkout'; +/** + * Custom hook to manage address editing states in the checkout process. + * + * When set to true (default), the shipping and billing address forms are displayed. + * When set to false, the address forms are hidden and the user can only view the address details (card view). + * + * @return {Object} An object containing address editing states and setter functions. + */ export const useAddressEditing = () => { + // Select address editing states from the checkout store const { isEditingShippingAddress, isEditingBillingAddress } = useSelect( ( select ) => { const store = select( CHECKOUT_STORE_KEY ); return { + // Default to true if the getter function doesn't exist isEditingShippingAddress: store.getEditingShippingAddress ? store.getEditingShippingAddress() : true, @@ -19,9 +29,11 @@ export const useAddressEditing = () => { [] ); + // Get dispatch functions to update address editing states const { setEditingShippingAddress, setEditingBillingAddress } = useDispatch( CHECKOUT_STORE_KEY ); + // Memoized function to update shipping address editing state const setShippingAddressEditing = useCallback( ( isEditing ) => { if ( typeof setEditingShippingAddress === 'function' ) { @@ -31,6 +43,7 @@ export const useAddressEditing = () => { [ setEditingShippingAddress ] ); + // Memoized function to update billing address editing state const setBillingAddressEditing = useCallback( ( isEditing ) => { if ( typeof setEditingBillingAddress === 'function' ) { @@ -40,6 +53,7 @@ export const useAddressEditing = () => { [ setEditingBillingAddress ] ); + // Return an object with address editing states and setter functions return { isEditingShippingAddress, isEditingBillingAddress, diff --git a/modules/ppcp-axo-block/resources/js/hooks/useAxoCleanup.js b/modules/ppcp-axo-block/resources/js/hooks/useAxoCleanup.js index 23175e31f..871197722 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useAxoCleanup.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useAxoCleanup.js @@ -12,13 +12,21 @@ import { import { restoreOriginalFields } from '../helpers/fieldHelpers'; import useCustomerData from './useCustomerData'; +/** + * Custom hook to handle cleanup of AXO functionality. + * This hook ensures that all AXO-related changes are reverted when the component unmounts (a different payment method gets selected). + */ const useAxoCleanup = () => { + // Get dispatch functions from the AXO store const { setIsAxoActive, setIsGuest } = useDispatch( STORE_NAME ); + + // Get functions to update WooCommerce shipping and billing addresses const { setShippingAddress: updateWooShippingAddress, setBillingAddress: updateWooBillingAddress, } = useCustomerData(); + // Effect to restore original WooCommerce fields on unmount useEffect( () => { return () => { log( 'Cleaning up: Restoring WooCommerce fields' ); @@ -29,14 +37,21 @@ const useAxoCleanup = () => { }; }, [ updateWooShippingAddress, updateWooBillingAddress ] ); + // Effect to clean up AXO-specific functionality on unmount useEffect( () => { return () => { log( 'Cleaning up Axo component' ); + + // Reset AXO state setIsAxoActive( false ); setIsGuest( true ); + + // Remove AXO UI elements removeShippingChangeButton(); removeCardChangeButton(); removeWatermark(); + + // Remove email functionality if it was set up if ( isEmailFunctionalitySetup() ) { log( 'Removing email functionality' ); removeEmailFunctionality(); diff --git a/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js b/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js index bdc615461..37ac4898b 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useAxoSetup.js @@ -12,20 +12,34 @@ import useCustomerData from './useCustomerData'; import useShippingAddressChange from './useShippingAddressChange'; import useCardChange from './useCardChange'; +/** + * Custom hook to set up AXO functionality. + * + * @param {Object} ppcpConfig - PayPal Checkout configuration. + * @param {Object} fastlaneSdk - Fastlane SDK instance. + * @param {Object} paymentComponent - Payment component instance. + * @return {boolean} Whether PayPal script has loaded. + */ const useAxoSetup = ( ppcpConfig, fastlaneSdk, paymentComponent ) => { + // Get dispatch functions from the AXO store const { setIsAxoActive, setIsAxoScriptLoaded, setShippingAddress, setCardDetails, } = useDispatch( STORE_NAME ); + + // Check if PayPal script has loaded const paypalLoaded = usePayPalScript( ppcpConfig ); + + // Set up card and shipping address change handlers const onChangeCardButtonClick = useCardChange( fastlaneSdk ); const onChangeShippingAddressClick = useShippingAddressChange( fastlaneSdk, setShippingAddress ); + // Get customer data and setter functions const { shippingAddress: wooShippingAddress, billingAddress: wooBillingAddress, @@ -33,17 +47,22 @@ const useAxoSetup = ( ppcpConfig, fastlaneSdk, paymentComponent ) => { setBillingAddress: setWooBillingAddress, } = useCustomerData(); + // Set up phone sync handler usePhoneSyncHandler( paymentComponent ); + // Initialize class toggles on mount useEffect( () => { initializeClassToggles(); }, [] ); + // Set up AXO functionality when PayPal and Fastlane are loaded useEffect( () => { setupWatermark( fastlaneSdk ); if ( paypalLoaded && fastlaneSdk ) { setIsAxoScriptLoaded( true ); setIsAxoActive( true ); + + // Create and set up email lookup handler const emailLookupHandler = createEmailLookupHandler( fastlaneSdk, setShippingAddress, diff --git a/modules/ppcp-axo-block/resources/js/hooks/useCardChange.js b/modules/ppcp-axo-block/resources/js/hooks/useCardChange.js index 6d2482416..29ab2f42c 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useCardChange.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useCardChange.js @@ -5,22 +5,29 @@ import { useAddressEditing } from './useAddressEditing'; import useCustomerData from './useCustomerData'; import { STORE_NAME } from '../stores/axoStore'; +/** + * Custom hook to handle the 'Choose a different card' selection. + * + * @param {Object} fastlaneSdk - The Fastlane SDK instance. + * @return {Function} Callback function to trigger card selection and update related data. + */ export const useCardChange = ( fastlaneSdk ) => { const { setBillingAddressEditing } = useAddressEditing(); const { setBillingAddress: setWooBillingAddress } = useCustomerData(); - const { setCardDetails, setShippingAddress } = useDispatch( STORE_NAME ); + const { setCardDetails } = useDispatch( STORE_NAME ); return useCallback( async () => { if ( fastlaneSdk ) { + // Show card selector and get the user's selection const { selectionChanged, selectedCard } = await fastlaneSdk.profile.showCardSelector(); if ( selectionChanged && selectedCard?.paymentSource?.card ) { - // Use the fallback logic for cardholder's name. + // Extract cardholder and billing information from the selected card const { name, billingAddress } = selectedCard.paymentSource.card; - // If name is missing, use billing details as a fallback for the name. + // Parse cardholder's name, using billing details as a fallback if missing let firstName = ''; let lastName = ''; @@ -30,6 +37,7 @@ export const useCardChange = ( fastlaneSdk ) => { lastName = nameParts.slice( 1 ).join( ' ' ); } + // Transform the billing address into WooCommerce format const newBillingAddress = { first_name: firstName, last_name: lastName, @@ -41,20 +49,19 @@ export const useCardChange = ( fastlaneSdk ) => { country: billingAddress?.countryCode || '', }; - // Batch state updates. + // Batch update states await Promise.all( [ + // Update the selected card details in the custom store new Promise( ( resolve ) => { setCardDetails( selectedCard ); resolve(); } ), + // Update the WooCommerce billing address in the WooCommerce store new Promise( ( resolve ) => { setWooBillingAddress( newBillingAddress ); resolve(); } ), - new Promise( ( resolve ) => { - setShippingAddress( newBillingAddress ); - resolve(); - } ), + // Trigger the Address Card view by setting the billing address editing state to false new Promise( ( resolve ) => { setBillingAddressEditing( false ); resolve(); @@ -68,7 +75,6 @@ export const useCardChange = ( fastlaneSdk ) => { fastlaneSdk, setCardDetails, setWooBillingAddress, - setShippingAddress, setBillingAddressEditing, ] ); }; diff --git a/modules/ppcp-axo-block/resources/js/hooks/useCustomerData.js b/modules/ppcp-axo-block/resources/js/hooks/useCustomerData.js index 6f7ffe537..b839e3199 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useCustomerData.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useCustomerData.js @@ -1,16 +1,24 @@ import { useCallback, useMemo } from '@wordpress/element'; import { useDispatch, useSelect } from '@wordpress/data'; +/** + * Custom hook to manage customer data in the WooCommerce store. + * + * @return {Object} An object containing customer addresses and setter functions. + */ export const useCustomerData = () => { + // Fetch customer data from the WooCommerce store const customerData = useSelect( ( select ) => select( 'wc/store/cart' ).getCustomerData() ); + // Get dispatch functions to update shipping and billing addresses const { setShippingAddress: setShippingAddressDispatch, setBillingAddress: setBillingAddressDispatch, } = useDispatch( 'wc/store/cart' ); + // Memoized function to update shipping address const setShippingAddress = useCallback( ( address ) => { setShippingAddressDispatch( address ); @@ -18,6 +26,7 @@ export const useCustomerData = () => { [ setShippingAddressDispatch ] ); + // Memoized function to update billing address const setBillingAddress = useCallback( ( address ) => { setBillingAddressDispatch( address ); @@ -25,6 +34,7 @@ export const useCustomerData = () => { [ setBillingAddressDispatch ] ); + // Return memoized object with customer data and setter functions return useMemo( () => ( { shippingAddress: customerData.shippingAddress, diff --git a/modules/ppcp-axo-block/resources/js/hooks/useDeleteEmptyKeys.js b/modules/ppcp-axo-block/resources/js/hooks/useDeleteEmptyKeys.js index 04043e0a9..63eaffe14 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useDeleteEmptyKeys.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useDeleteEmptyKeys.js @@ -3,17 +3,30 @@ import { useCallback } from '@wordpress/element'; const isObject = ( value ) => typeof value === 'object' && value !== null; const isNonEmptyString = ( value ) => value !== ''; +/** + * Recursively removes empty values from an object. + * Empty values are considered to be: + * - Empty strings + * - Empty objects + * - Null or undefined values + * + * @param {Object} obj - The object to clean. + * @return {Object} A new object with empty values removed. + */ const removeEmptyValues = ( obj ) => { + // If not an object, return the value as is if ( ! isObject( obj ) ) { return obj; } return Object.fromEntries( Object.entries( obj ) + // Recursively apply removeEmptyValues to nested objects .map( ( [ key, value ] ) => [ key, isObject( value ) ? removeEmptyValues( value ) : value, ] ) + // Filter out empty values .filter( ( [ _, value ] ) => isObject( value ) ? Object.keys( value ).length > 0 @@ -22,6 +35,11 @@ const removeEmptyValues = ( obj ) => { ); }; +/** + * Custom hook that returns a memoized function to remove empty values from an object. + * + * @return {Function} A memoized function that removes empty values from an object. + */ export const useDeleteEmptyKeys = () => { return useCallback( removeEmptyValues, [] ); }; diff --git a/modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js b/modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js index 9bd9db8d5..bf1d4c791 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useFastlaneSdk.js @@ -3,16 +3,27 @@ import Fastlane from '../../../../ppcp-axo/resources/js/Connection/Fastlane'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; import { useDeleteEmptyKeys } from './useDeleteEmptyKeys'; +/** + * Custom hook to initialize and manage the Fastlane SDK. + * + * @param {Object} axoConfig - Configuration for AXO. + * @param {Object} ppcpConfig - Configuration for PPCP. + * @return {Object|null} The initialized Fastlane SDK instance or null. + */ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => { const [ fastlaneSdk, setFastlaneSdk ] = useState( null ); + // Ref to prevent multiple simultaneous initializations const initializingRef = useRef( false ); + // Ref to hold the latest config values const configRef = useRef( { axoConfig, ppcpConfig } ); + // Custom hook to remove empty keys from an object const deleteEmptyKeys = useDeleteEmptyKeys(); const styleOptions = useMemo( () => { return deleteEmptyKeys( configRef.current.axoConfig.style_options ); }, [ deleteEmptyKeys ] ); + // Effect to initialize Fastlane SDK useEffect( () => { const initFastlane = async () => { if ( initializingRef.current || fastlaneSdk ) { @@ -25,15 +36,18 @@ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => { try { const fastlane = new Fastlane(); + // Set sandbox environment if configured if ( configRef.current.axoConfig.environment.is_sandbox ) { window.localStorage.setItem( 'axoEnv', 'sandbox' ); } + // Connect to Fastlane with locale and style options await fastlane.connect( { locale: configRef.current.ppcpConfig.locale, styles: styleOptions, } ); + // Set locale (hardcoded to 'en_us' for now) fastlane.setLocale( 'en_us' ); setFastlaneSdk( fastlane ); @@ -47,6 +61,7 @@ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => { initFastlane(); }, [ fastlaneSdk, styleOptions ] ); + // Effect to update the config ref when configs change useEffect( () => { configRef.current = { axoConfig, ppcpConfig }; }, [ axoConfig, ppcpConfig ] ); diff --git a/modules/ppcp-axo-block/resources/js/hooks/useHandlePaymentSetup.js b/modules/ppcp-axo-block/resources/js/hooks/useHandlePaymentSetup.js index ca74c887b..86975f78c 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useHandlePaymentSetup.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useHandlePaymentSetup.js @@ -2,29 +2,40 @@ import { useCallback } from '@wordpress/element'; import { useSelect } from '@wordpress/data'; import { STORE_NAME } from '../stores/axoStore'; +/** + * Custom hook to handle payment setup in the checkout process. + * + * @param {Object} emitResponse - Object containing response types. + * @param {Object} paymentComponent - The payment component instance. + * @param {Object} tokenizedCustomerData - Tokenized customer data for payment. + * @return {Function} Callback function to handle payment setup. + */ const useHandlePaymentSetup = ( emitResponse, paymentComponent, tokenizedCustomerData ) => { + // Select card details from the store const { cardDetails } = useSelect( ( select ) => ( { - shippingAddress: select( STORE_NAME ).getShippingAddress(), cardDetails: select( STORE_NAME ).getCardDetails(), } ), [] ); return useCallback( async () => { + // Determine if it's a Ryan flow (saved card) based on the presence of card ID const isRyanFlow = !! cardDetails?.id; let cardToken = cardDetails?.id; + // If no card token and payment component exists, get a new token if ( ! cardToken && paymentComponent ) { cardToken = await paymentComponent .getPaymentToken( tokenizedCustomerData ) .then( ( response ) => response.id ); } + // Handle error cases when card token is not available if ( ! cardToken ) { let reason = 'tokenization error'; diff --git a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js index 8fc85829e..223525285 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js +++ b/modules/ppcp-axo-block/resources/js/hooks/usePayPalScript.js @@ -2,12 +2,20 @@ import { useState, useEffect } from '@wordpress/element'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; import { loadPaypalScript } from '../../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; +/** + * Custom hook to load the PayPal script. + * + * @param {Object} ppcpConfig - Configuration object for PayPal script. + * @return {boolean} True if the PayPal script has loaded, false otherwise. + */ const usePayPalScript = ( ppcpConfig ) => { const [ isLoaded, setIsLoaded ] = useState( false ); useEffect( () => { if ( ! isLoaded ) { log( 'Loading PayPal script' ); + + // Load the PayPal script using the provided configuration loadPaypalScript( ppcpConfig, () => { log( 'PayPal script loaded' ); setIsLoaded( true ); diff --git a/modules/ppcp-axo-block/resources/js/hooks/usePaymentSetupEffect.js b/modules/ppcp-axo-block/resources/js/hooks/usePaymentSetupEffect.js index 087fc807e..8e82b5011 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/usePaymentSetupEffect.js +++ b/modules/ppcp-axo-block/resources/js/hooks/usePaymentSetupEffect.js @@ -1,5 +1,13 @@ import { useEffect, useCallback } from '@wordpress/element'; +/** + * Custom hook to handle payment setup effects in the checkout flow. + * + * @param {Function} onPaymentSetup - Function to subscribe to payment setup events. + * @param {Function} handlePaymentSetup - Callback to process payment setup. + * @param {Function} setPaymentComponent - Function to update the payment component state. + * @return {Object} Object containing the handlePaymentLoad function. + */ const usePaymentSetupEffect = ( onPaymentSetup, handlePaymentSetup, @@ -17,6 +25,11 @@ const usePaymentSetupEffect = ( }; }, [ onPaymentSetup, handlePaymentSetup ] ); + /** + * Callback function to handle payment component loading. + * + * @param {Object} component - The loaded payment component. + */ const handlePaymentLoad = useCallback( ( component ) => { setPaymentComponent( component ); diff --git a/modules/ppcp-axo-block/resources/js/hooks/useShippingAddressChange.js b/modules/ppcp-axo-block/resources/js/hooks/useShippingAddressChange.js index 6d8e7b4a2..2c45b8930 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useShippingAddressChange.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useShippingAddressChange.js @@ -2,19 +2,30 @@ import { useCallback } from '@wordpress/element'; import { useAddressEditing } from './useAddressEditing'; import useCustomerData from './useCustomerData'; +/** + * Custom hook to handle the 'Choose a different shipping address' selection. + * + * @param {Object} fastlaneSdk - The Fastlane SDK instance. + * @param {Function} setShippingAddress - Function to update the shipping address state. + * @return {Function} Callback function to trigger shipping address selection and update. + */ export const useShippingAddressChange = ( fastlaneSdk, setShippingAddress ) => { const { setShippingAddressEditing } = useAddressEditing(); const { setShippingAddress: setWooShippingAddress } = useCustomerData(); return useCallback( async () => { if ( fastlaneSdk ) { + // Show shipping address selector and get the user's selection const { selectionChanged, selectedAddress } = await fastlaneSdk.profile.showShippingAddressSelector(); + if ( selectionChanged ) { + // Update the shipping address in the custom store with the selected address setShippingAddress( selectedAddress ); const { address, name, phoneNumber } = selectedAddress; + // Transform the selected address into WooCommerce format const newShippingAddress = { first_name: name.firstName, last_name: name.lastName, @@ -27,11 +38,13 @@ export const useShippingAddressChange = ( fastlaneSdk, setShippingAddress ) => { phone: phoneNumber.nationalNumber, }; + // Update the WooCommerce shipping address in the WooCommerce store await new Promise( ( resolve ) => { setWooShippingAddress( newShippingAddress ); resolve(); } ); + // Trigger the Address Card view by setting the shipping address editing state to false await new Promise( ( resolve ) => { setShippingAddressEditing( false ); resolve(); diff --git a/modules/ppcp-axo-block/resources/js/hooks/useTokenizeCustomerData.js b/modules/ppcp-axo-block/resources/js/hooks/useTokenizeCustomerData.js index 868ddcb85..c0a7dcf97 100644 --- a/modules/ppcp-axo-block/resources/js/hooks/useTokenizeCustomerData.js +++ b/modules/ppcp-axo-block/resources/js/hooks/useTokenizeCustomerData.js @@ -1,18 +1,27 @@ import { useMemo } from '@wordpress/element'; -import { useSelect } from '@wordpress/data'; +import useCustomerData from './useCustomerData'; +/** + * Custom hook to prepare customer data for tokenization. + * + * @return {Object} Formatted customer data for tokenization. + */ export const useTokenizeCustomerData = () => { - const customerData = useSelect( ( select ) => - select( 'wc/store/cart' ).getCustomerData() - ); + const { billingAddress, shippingAddress } = useCustomerData(); + /** + * Validates if an address contains the minimum required data. + * + * @param {Object} address - The address object to validate. + * @return {boolean} True if the address is valid, false otherwise. + */ const isValidAddress = ( address ) => { - // At least one name must be present. + // At least one name must be present if ( ! address.first_name && ! address.last_name ) { return false; } - // Street, city, postcode, country are mandatory; state is optional. + // Street, city, postcode, country are mandatory; state is optional return ( address.address_1 && address.city && @@ -21,15 +30,14 @@ export const useTokenizeCustomerData = () => { ); }; - // Memoize the customer data to avoid unnecessary re-renders (and potential infinite loops). + // Memoize the customer data to avoid unnecessary re-renders (and potential infinite loops) return useMemo( () => { - const { billingAddress, shippingAddress } = customerData; - - // Prefer billing address, but fallback to shipping address if billing address is not valid. + // Determine the main address, preferring billing address if valid const mainAddress = isValidAddress( billingAddress ) ? billingAddress : shippingAddress; + // Format the customer data for tokenization return { cardholderName: { fullName: `${ mainAddress.first_name } ${ mainAddress.last_name }`, @@ -43,7 +51,7 @@ export const useTokenizeCustomerData = () => { countryCode: mainAddress.country, }, }; - }, [ customerData ] ); + }, [ billingAddress, shippingAddress ] ); }; export default useTokenizeCustomerData; diff --git a/modules/ppcp-axo-block/resources/js/stores/axoStore.js b/modules/ppcp-axo-block/resources/js/stores/axoStore.js index 04323c6e1..da92d0e6f 100644 --- a/modules/ppcp-axo-block/resources/js/stores/axoStore.js +++ b/modules/ppcp-axo-block/resources/js/stores/axoStore.js @@ -2,7 +2,6 @@ import { createReduxStore, register, dispatch } from '@wordpress/data'; export const STORE_NAME = 'woocommerce-paypal-payments/axo-block'; -// Initial state const DEFAULT_STATE = { isGuest: true, isAxoActive: false, @@ -14,7 +13,7 @@ const DEFAULT_STATE = { phoneNumber: '', }; -// Actions +// Action creators for updating the store state const actions = { setIsGuest: ( isGuest ) => ( { type: 'SET_IS_GUEST', @@ -50,7 +49,13 @@ const actions = { } ), }; -// Reducer +/** + * Reducer function to handle state updates based on dispatched actions. + * + * @param {Object} state - Current state of the store. + * @param {Object} action - Dispatched action object. + * @return {Object} New state after applying the action. + */ const reducer = ( state = DEFAULT_STATE, action ) => { switch ( action.type ) { case 'SET_IS_GUEST': @@ -74,7 +79,7 @@ const reducer = ( state = DEFAULT_STATE, action ) => { } }; -// Selectors +// Selector functions to retrieve specific pieces of state const selectors = { getIsGuest: ( state ) => state.isGuest, getIsAxoActive: ( state ) => state.isAxoActive, @@ -86,7 +91,7 @@ const selectors = { getPhoneNumber: ( state ) => state.phoneNumber, }; -// Create and register the store +// Create and register the Redux store for the AXO block const store = createReduxStore( STORE_NAME, { reducer, actions, @@ -96,22 +101,48 @@ const store = createReduxStore( STORE_NAME, { register( store ); // Action dispatchers + +/** + * Action dispatcher to update the guest status in the store. + * + * @param {boolean} isGuest - Whether the user is a guest or not. + */ export const setIsGuest = ( isGuest ) => { dispatch( STORE_NAME ).setIsGuest( isGuest ); }; +/** + * Action dispatcher to update the email lookup completion status in the store. + * + * @param {boolean} isEmailLookupCompleted - Whether the email lookup is completed. + */ export const setIsEmailLookupCompleted = ( isEmailLookupCompleted ) => { dispatch( STORE_NAME ).setIsEmailLookupCompleted( isEmailLookupCompleted ); }; +/** + * Action dispatcher to update the shipping address in the store. + * + * @param {Object} shippingAddress - The user's shipping address. + */ export const setShippingAddress = ( shippingAddress ) => { dispatch( STORE_NAME ).setShippingAddress( shippingAddress ); }; +/** + * Action dispatcher to update the card details in the store. + * + * @param {Object} cardDetails - The user's card details. + */ export const setCardDetails = ( cardDetails ) => { dispatch( STORE_NAME ).setCardDetails( cardDetails ); }; +/** + * Action dispatcher to update the phone number in the store. + * + * @param {string} phoneNumber - The user's phone number. + */ export const setPhoneNumber = ( phoneNumber ) => { dispatch( STORE_NAME ).setPhoneNumber( phoneNumber ); };