🔀 Merge branch 'PCP-3771’

# Conflicts:
#	modules/ppcp-googlepay/resources/js/GooglepayManager.js
This commit is contained in:
Philipp Stracker 2024-10-08 14:01:10 +02:00
commit 69d3e032d8
No known key found for this signature in database
52 changed files with 1234 additions and 253 deletions

View file

@ -301,3 +301,13 @@ a.wc-block-axo-change-link {
#shipping-fields .wc-block-components-checkout-step__heading { #shipping-fields .wc-block-components-checkout-step__heading {
display: flex; display: flex;
} }
// 11. Fastlane modal info message fix
.wc-block-components-text-input {
.wc-block-components-form &,
& {
paypal-watermark {
white-space: wrap;
}
}
}

View file

@ -5,7 +5,7 @@ import { STORE_NAME } from '../../stores/axoStore';
const cardIcons = { const cardIcons = {
VISA: 'visa-light.svg', VISA: 'visa-light.svg',
MASTER_CARD: 'mastercard-light.svg', MASTERCARD: 'mastercard-light.svg',
AMEX: 'amex-light.svg', AMEX: 'amex-light.svg',
DISCOVER: 'discover-light.svg', DISCOVER: 'discover-light.svg',
DINERS: 'dinersclub-light.svg', DINERS: 'dinersclub-light.svg',

View file

@ -1,6 +1,13 @@
import { createElement } from '@wordpress/element'; import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n'; 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 } ) => const CardChangeButton = ( { onChangeButtonClick } ) =>
createElement( createElement(
'a', 'a',
@ -9,7 +16,9 @@ const CardChangeButton = ( { onChangeButtonClick } ) =>
'wc-block-checkout-axo-block-card__edit wc-block-axo-change-link', 'wc-block-checkout-axo-block-card__edit wc-block-axo-change-link',
role: 'button', role: 'button',
onClick: ( event ) => { onClick: ( event ) => {
// Prevent default anchor behavior
event.preventDefault(); event.preventDefault();
// Call the provided click handler
onChangeButtonClick(); onChangeButtonClick();
}, },
}, },

View file

@ -1,6 +1,13 @@
import { createElement, createRoot, useEffect } from '@wordpress/element'; import { createElement, createRoot, useEffect } from '@wordpress/element';
import CardChangeButton from './CardChangeButton'; 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 } ) => { const CardChangeButtonManager = ( { onChangeButtonClick } ) => {
useEffect( () => { useEffect( () => {
const radioLabelElement = document.getElementById( const radioLabelElement = document.getElementById(
@ -8,14 +15,17 @@ const CardChangeButtonManager = ( { onChangeButtonClick } ) => {
); );
if ( radioLabelElement ) { if ( radioLabelElement ) {
// Check if the change button doesn't already exist
if ( if (
! radioLabelElement.querySelector( ! radioLabelElement.querySelector(
'.wc-block-checkout-axo-block-card__edit' '.wc-block-checkout-axo-block-card__edit'
) )
) { ) {
// Create a new container for the button
const buttonContainer = document.createElement( 'div' ); const buttonContainer = document.createElement( 'div' );
radioLabelElement.appendChild( buttonContainer ); radioLabelElement.appendChild( buttonContainer );
// Create a React root and render the CardChangeButton
const root = createRoot( buttonContainer ); const root = createRoot( buttonContainer );
root.render( root.render(
createElement( CardChangeButton, { onChangeButtonClick } ) createElement( CardChangeButton, { onChangeButtonClick } )
@ -23,6 +33,7 @@ const CardChangeButtonManager = ( { onChangeButtonClick } ) => {
} }
} }
// Cleanup function to remove the button when the component unmounts
return () => { return () => {
const button = document.querySelector( const button = document.querySelector(
'.wc-block-checkout-axo-block-card__edit' '.wc-block-checkout-axo-block-card__edit'
@ -33,6 +44,7 @@ const CardChangeButtonManager = ( { onChangeButtonClick } ) => {
}; };
}, [ onChangeButtonClick ] ); }, [ onChangeButtonClick ] );
// This component doesn't render anything directly
return null; return null;
}; };

View file

@ -1,18 +1,31 @@
import { createElement, createRoot } from '@wordpress/element'; import { createElement, createRoot } from '@wordpress/element';
import CardChangeButtonManager from './CardChangeButtonManager'; 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 ) => { export const injectCardChangeButton = ( onChangeButtonClick ) => {
// Create a container for the button
const container = document.createElement( 'div' ); const container = document.createElement( 'div' );
document.body.appendChild( container ); document.body.appendChild( container );
// Render the CardChangeButtonManager in the new container
createRoot( container ).render( createRoot( container ).render(
createElement( CardChangeButtonManager, { onChangeButtonClick } ) createElement( CardChangeButtonManager, { onChangeButtonClick } )
); );
}; };
/**
* Removes the card change button from the DOM if it exists.
*/
export const removeCardChangeButton = () => { export const removeCardChangeButton = () => {
const button = document.querySelector( const button = document.querySelector(
'.wc-block-checkout-axo-block-card__edit' '.wc-block-checkout-axo-block-card__edit'
); );
// Remove the button's parent node if it exists
if ( button && button.parentNode ) { if ( button && button.parentNode ) {
button.parentNode.remove(); button.parentNode.remove();
} }

View file

@ -2,7 +2,15 @@ import { STORE_NAME } from '../../stores/axoStore';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n'; 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 } ) => { const EmailButton = ( { handleSubmit } ) => {
// Select relevant states from the AXO store
const { isGuest, isAxoActive, isEmailSubmitted } = useSelect( const { isGuest, isAxoActive, isEmailSubmitted } = useSelect(
( select ) => ( { ( select ) => ( {
isGuest: select( STORE_NAME ).getIsGuest(), 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 ) { if ( ! isGuest || ! isAxoActive ) {
return null; return null;
} }
@ -24,6 +33,7 @@ const EmailButton = ( { handleSubmit } ) => {
}` } }` }
disabled={ isEmailSubmitted } disabled={ isEmailSubmitted }
> >
{ /* Button text */ }
<span <span
className="wc-block-components-button__text" className="wc-block-components-button__text"
style={ { style={ {
@ -32,6 +42,7 @@ const EmailButton = ( { handleSubmit } ) => {
> >
{ __( 'Continue', 'woocommerce-paypal-payments' ) } { __( 'Continue', 'woocommerce-paypal-payments' ) }
</span> </span>
{ /* Loading spinner */ }
{ isEmailSubmitted && ( { isEmailSubmitted && (
<span <span
className="wc-block-components-spinner" className="wc-block-components-spinner"

View file

@ -3,6 +3,7 @@ import { log } from '../../../../../ppcp-axo/resources/js/Helper/Debug';
import { STORE_NAME } from '../../stores/axoStore'; import { STORE_NAME } from '../../stores/axoStore';
import EmailButton from './EmailButton'; import EmailButton from './EmailButton';
// Cache for DOM elements and references
let emailInput = null; let emailInput = null;
let submitButtonReference = { let submitButtonReference = {
container: null, container: null,
@ -11,6 +12,11 @@ let submitButtonReference = {
}; };
let keydownHandler = null; let keydownHandler = null;
/**
* Retrieves or caches the email input element.
*
* @return {HTMLElement|null} The email input element or null if not found.
*/
const getEmailInput = () => { const getEmailInput = () => {
if ( ! emailInput ) { if ( ! emailInput ) {
emailInput = document.getElementById( 'email' ); emailInput = document.getElementById( 'email' );
@ -18,6 +24,11 @@ const getEmailInput = () => {
return emailInput; return emailInput;
}; };
/**
* Sets up email functionality for AXO checkout.
*
* @param {Function} onEmailSubmit - Callback function to handle email submission.
*/
export const setupEmailFunctionality = ( onEmailSubmit ) => { export const setupEmailFunctionality = ( onEmailSubmit ) => {
const input = getEmailInput(); const input = getEmailInput();
if ( ! input ) { if ( ! input ) {
@ -28,6 +39,7 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => {
return; return;
} }
// Handler for email submission
const handleEmailSubmit = async () => { const handleEmailSubmit = async () => {
const isEmailSubmitted = wp.data const isEmailSubmitted = wp.data
.select( STORE_NAME ) .select( STORE_NAME )
@ -50,6 +62,7 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => {
} }
}; };
// Set up keydown handler for Enter key
keydownHandler = ( event ) => { keydownHandler = ( event ) => {
const isAxoActive = wp.data.select( STORE_NAME ).getIsAxoActive(); const isAxoActive = wp.data.select( STORE_NAME ).getIsAxoActive();
if ( event.key === 'Enter' && isAxoActive ) { if ( event.key === 'Enter' && isAxoActive ) {
@ -78,6 +91,7 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => {
); );
} }
// Function to render the EmailButton
const renderButton = () => { const renderButton = () => {
if ( submitButtonReference.root ) { if ( submitButtonReference.root ) {
submitButtonReference.root.render( submitButtonReference.root.render(
@ -90,12 +104,15 @@ export const setupEmailFunctionality = ( onEmailSubmit ) => {
renderButton(); renderButton();
// Subscribe to state changes // Subscribe to state changes and re-render button
submitButtonReference.unsubscribe = wp.data.subscribe( () => { submitButtonReference.unsubscribe = wp.data.subscribe( () => {
renderButton(); renderButton();
} ); } );
}; };
/**
* Removes email functionality and cleans up event listeners and DOM elements.
*/
export const removeEmailFunctionality = () => { export const removeEmailFunctionality = () => {
const input = getEmailInput(); const input = getEmailInput();
if ( input && keydownHandler ) { if ( input && keydownHandler ) {
@ -120,6 +137,11 @@ export const removeEmailFunctionality = () => {
keydownHandler = null; keydownHandler = null;
}; };
/**
* Checks if email functionality is currently set up.
*
* @return {boolean} True if email functionality is set up, false otherwise.
*/
export const isEmailFunctionalitySetup = () => { export const isEmailFunctionalitySetup = () => {
return !! submitButtonReference.root; return !! submitButtonReference.root;
}; };

View file

@ -4,8 +4,18 @@ import { __ } from '@wordpress/i18n';
import { Card } from '../Card'; import { Card } from '../Card';
import { STORE_NAME } from '../../stores/axoStore'; 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 } ) => { export const Payment = ( { fastlaneSdk, onPaymentLoad } ) => {
const [ isCardElementReady, setIsCardElementReady ] = useState( false ); const [ isCardElementReady, setIsCardElementReady ] = useState( false );
// Select relevant states from the AXO store
const { isGuest, isEmailLookupCompleted } = useSelect( const { isGuest, isEmailLookupCompleted } = useSelect(
( select ) => ( { ( select ) => ( {
isGuest: select( STORE_NAME ).getIsGuest(), isGuest: select( STORE_NAME ).getIsGuest(),
@ -31,16 +41,22 @@ export const Payment = ( { fastlaneSdk, onPaymentLoad } ) => {
onPaymentLoad, onPaymentLoad,
] ); ] );
// Set card element ready when guest email lookup is completed
useEffect( () => { useEffect( () => {
if ( isGuest && isEmailLookupCompleted ) { if ( isGuest && isEmailLookupCompleted ) {
setIsCardElementReady( true ); setIsCardElementReady( true );
} }
}, [ isGuest, isEmailLookupCompleted ] ); }, [ isGuest, isEmailLookupCompleted ] );
// Load payment component when dependencies change
useEffect( () => { useEffect( () => {
loadPaymentComponent(); loadPaymentComponent();
}, [ 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 ( isGuest ) {
if ( isEmailLookupCompleted ) { if ( isEmailLookupCompleted ) {
return <div id="fastlane-card" key="fastlane-card" />; return <div id="fastlane-card" key="fastlane-card" />;

View file

@ -1,11 +1,20 @@
import { __ } from '@wordpress/i18n'; 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 } ) => ( const ShippingChangeButton = ( { onChangeShippingAddressClick } ) => (
<a <a
className="wc-block-axo-change-link" className="wc-block-axo-change-link"
role="button" role="button"
onClick={ ( event ) => { onClick={ ( event ) => {
// Prevent default anchor behavior
event.preventDefault(); event.preventDefault();
// Call the provided click handler
onChangeShippingAddressClick(); onChangeShippingAddressClick();
} } } }
> >

View file

@ -1,22 +1,32 @@
import { useEffect, createRoot } from '@wordpress/element'; import { useEffect, createRoot } from '@wordpress/element';
import ShippingChangeButton from './ShippingChangeButton'; 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 } ) => { const ShippingChangeButtonManager = ( { onChangeShippingAddressClick } ) => {
useEffect( () => { useEffect( () => {
const shippingHeading = document.querySelector( const shippingHeading = document.querySelector(
'#shipping-fields .wc-block-components-checkout-step__heading' '#shipping-fields .wc-block-components-checkout-step__heading'
); );
// Check if the shipping heading exists and doesn't already have a change button
if ( if (
shippingHeading && shippingHeading &&
! shippingHeading.querySelector( ! shippingHeading.querySelector(
'.wc-block-checkout-axo-block-card__edit' '.wc-block-checkout-axo-block-card__edit'
) )
) { ) {
// Create a new span element to contain the ShippingChangeButton
const spanElement = document.createElement( 'span' ); const spanElement = document.createElement( 'span' );
spanElement.className = 'wc-block-checkout-axo-block-card__edit'; spanElement.className = 'wc-block-checkout-axo-block-card__edit';
shippingHeading.appendChild( spanElement ); shippingHeading.appendChild( spanElement );
// Create a React root and render the ShippingChangeButton
const root = createRoot( spanElement ); const root = createRoot( spanElement );
root.render( root.render(
<ShippingChangeButton <ShippingChangeButton
@ -26,6 +36,7 @@ const ShippingChangeButtonManager = ( { onChangeShippingAddressClick } ) => {
/> />
); );
// Cleanup function to remove the button when the component unmounts
return () => { return () => {
root.unmount(); root.unmount();
spanElement.remove(); spanElement.remove();
@ -33,6 +44,7 @@ const ShippingChangeButtonManager = ( { onChangeShippingAddressClick } ) => {
} }
}, [ onChangeShippingAddressClick ] ); }, [ onChangeShippingAddressClick ] );
// This component doesn't render anything directly
return null; return null;
}; };

View file

@ -1,14 +1,23 @@
import { createRoot } from '@wordpress/element'; import { createRoot } from '@wordpress/element';
import ShippingChangeButtonManager from './ShippingChangeButtonManager'; 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 ) => { export const injectShippingChangeButton = ( onChangeShippingAddressClick ) => {
// Check if the button already exists
const existingButton = document.querySelector( const existingButton = document.querySelector(
'#shipping-fields .wc-block-checkout-axo-block-card__edit' '#shipping-fields .wc-block-checkout-axo-block-card__edit'
); );
if ( ! existingButton ) { if ( ! existingButton ) {
// Create a new container for the button
const container = document.createElement( 'div' ); const container = document.createElement( 'div' );
document.body.appendChild( container ); document.body.appendChild( container );
// Render the ShippingChangeButtonManager in the new container
createRoot( container ).render( createRoot( container ).render(
<ShippingChangeButtonManager <ShippingChangeButtonManager
onChangeShippingAddressClick={ onChangeShippingAddressClick } onChangeShippingAddressClick={ onChangeShippingAddressClick }
@ -17,6 +26,9 @@ export const injectShippingChangeButton = ( onChangeShippingAddressClick ) => {
} }
}; };
/**
* Removes the shipping change button from the DOM if it exists.
*/
export const removeShippingChangeButton = () => { export const removeShippingChangeButton = () => {
const span = document.querySelector( const span = document.querySelector(
'#shipping-fields .wc-block-checkout-axo-block-card__edit' '#shipping-fields .wc-block-checkout-axo-block-card__edit'

View file

@ -1,6 +1,15 @@
import { useEffect, useRef } from '@wordpress/element'; import { useEffect, useRef } from '@wordpress/element';
import { log } from '../../../../../ppcp-axo/resources/js/Helper/Debug'; 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 = ( { const Watermark = ( {
fastlaneSdk, fastlaneSdk,
name = 'fastlane-watermark-container', name = 'fastlane-watermark-container',
@ -10,15 +19,19 @@ const Watermark = ( {
const watermarkRef = useRef( null ); const watermarkRef = useRef( null );
useEffect( () => { useEffect( () => {
/**
* Renders the Fastlane watermark.
*/
const renderWatermark = async () => { const renderWatermark = async () => {
if ( ! containerRef.current ) { if ( ! containerRef.current ) {
return; return;
} }
// Clear the container // Clear the container before rendering
containerRef.current.innerHTML = ''; containerRef.current.innerHTML = '';
try { try {
// Create and render the Fastlane watermark
const watermark = await fastlaneSdk.FastlaneWatermarkComponent( const watermark = await fastlaneSdk.FastlaneWatermarkComponent(
{ {
includeAdditionalInfo, includeAdditionalInfo,
@ -34,6 +47,7 @@ const Watermark = ( {
renderWatermark(); renderWatermark();
// Cleanup function to clear the container on unmount
return () => { return () => {
if ( containerRef.current ) { if ( containerRef.current ) {
containerRef.current.innerHTML = ''; containerRef.current.innerHTML = '';
@ -41,6 +55,7 @@ const Watermark = ( {
}; };
}, [ fastlaneSdk, name, includeAdditionalInfo ] ); }, [ fastlaneSdk, name, includeAdditionalInfo ] );
// Render the container for the watermark
return <div id={ name } ref={ containerRef } />; return <div id={ name } ref={ containerRef } />;
}; };

View file

@ -7,7 +7,15 @@ import {
updateWatermarkContent, updateWatermarkContent,
} from './utils'; } 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 } ) => { const WatermarkManager = ( { fastlaneSdk } ) => {
// Select relevant states from the AXO store
const isGuest = useSelect( ( select ) => const isGuest = useSelect( ( select ) =>
select( STORE_NAME ).getIsGuest() select( STORE_NAME ).getIsGuest()
); );
@ -20,6 +28,7 @@ const WatermarkManager = ( { fastlaneSdk } ) => {
useEffect( () => { useEffect( () => {
if ( isAxoActive || ( ! isAxoActive && ! isAxoScriptLoaded ) ) { if ( isAxoActive || ( ! isAxoActive && ! isAxoScriptLoaded ) ) {
// Create watermark container and update content when AXO is active or loading
createWatermarkContainer(); createWatermarkContainer();
updateWatermarkContent( { updateWatermarkContent( {
isAxoActive, isAxoActive,
@ -28,12 +37,15 @@ const WatermarkManager = ( { fastlaneSdk } ) => {
isGuest, isGuest,
} ); } );
} else { } else {
// Remove watermark when AXO is inactive and not loading
removeWatermark(); removeWatermark();
} }
// Cleanup function to remove watermark on unmount
return removeWatermark; return removeWatermark;
}, [ fastlaneSdk, isGuest, isAxoActive, isAxoScriptLoaded ] ); }, [ fastlaneSdk, isGuest, isAxoActive, isAxoScriptLoaded ] );
// This component doesn't render anything directly
return null; return null;
}; };

View file

@ -1,11 +1,15 @@
import { createElement, createRoot } from '@wordpress/element'; import { createElement, createRoot } from '@wordpress/element';
import { Watermark, WatermarkManager } from '../Watermark'; import { Watermark, WatermarkManager } from '../Watermark';
// Object to store references to the watermark container and root
const watermarkReference = { const watermarkReference = {
container: null, container: null,
root: null, root: null,
}; };
/**
* Creates a container for the watermark in the checkout contact information block.
*/
export const createWatermarkContainer = () => { export const createWatermarkContainer = () => {
const textInputContainer = document.querySelector( const textInputContainer = document.querySelector(
'.wp-block-woocommerce-checkout-contact-information-block .wc-block-components-text-input' '.wp-block-woocommerce-checkout-contact-information-block .wc-block-components-text-input'
@ -16,6 +20,7 @@ export const createWatermarkContainer = () => {
textInputContainer.querySelector( 'input[id="email"]' ); textInputContainer.querySelector( 'input[id="email"]' );
if ( emailInput ) { if ( emailInput ) {
// Create watermark container
watermarkReference.container = document.createElement( 'div' ); watermarkReference.container = document.createElement( 'div' );
watermarkReference.container.setAttribute( watermarkReference.container.setAttribute(
'class', 'class',
@ -26,7 +31,7 @@ export const createWatermarkContainer = () => {
'.wc-block-axo-email-submit-button-container' '.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; const insertAfterElement = emailButton || emailInput;
insertAfterElement.parentNode.insertBefore( insertAfterElement.parentNode.insertBefore(
@ -34,6 +39,7 @@ export const createWatermarkContainer = () => {
insertAfterElement.nextSibling insertAfterElement.nextSibling
); );
// Create a root for the watermark
watermarkReference.root = createRoot( watermarkReference.root = createRoot(
watermarkReference.container 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 ) => { export const setupWatermark = ( fastlaneSdk ) => {
const container = document.createElement( 'div' ); const container = document.createElement( 'div' );
document.body.appendChild( container ); document.body.appendChild( container );
const root = createRoot( container ); const root = createRoot( container );
root.render( createElement( WatermarkManager, { fastlaneSdk } ) ); root.render( createElement( WatermarkManager, { fastlaneSdk } ) );
// Return cleanup function
return () => { return () => {
root.unmount(); root.unmount();
if ( container && container.parentNode ) { 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 = () => { export const removeWatermark = () => {
if ( watermarkReference.root ) { if ( watermarkReference.root ) {
watermarkReference.root.unmount(); watermarkReference.root.unmount();
@ -65,6 +81,7 @@ export const removeWatermark = () => {
watermarkReference.container watermarkReference.container
); );
} else { } else {
// Fallback removal if parent node is not available
const detachedContainer = document.querySelector( const detachedContainer = document.querySelector(
'.wc-block-checkout-axo-block-watermark-container' '.wc-block-checkout-axo-block-watermark-container'
); );
@ -73,15 +90,30 @@ export const removeWatermark = () => {
} }
} }
} }
// Reset watermark reference
Object.assign( watermarkReference, { container: null, root: null } ); Object.assign( watermarkReference, { container: null, root: null } );
}; };
/**
* Renders content in the watermark container.
*
* @param {ReactElement} content - The content to render.
*/
export const renderWatermarkContent = ( content ) => { export const renderWatermarkContent = ( content ) => {
if ( watermarkReference.root ) { if ( watermarkReference.root ) {
watermarkReference.root.render( content ); 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 = ( { export const updateWatermarkContent = ( {
isAxoActive, isAxoActive,
isAxoScriptLoaded, isAxoScriptLoaded,
@ -89,6 +121,7 @@ export const updateWatermarkContent = ( {
isGuest, isGuest,
} ) => { } ) => {
if ( ! isAxoActive && ! isAxoScriptLoaded ) { if ( ! isAxoActive && ! isAxoScriptLoaded ) {
// Show loading spinner
renderWatermarkContent( renderWatermarkContent(
createElement( 'span', { createElement( 'span', {
className: 'wc-block-components-spinner', className: 'wc-block-components-spinner',
@ -96,6 +129,7 @@ export const updateWatermarkContent = ( {
} ) } )
); );
} else if ( isAxoActive ) { } else if ( isAxoActive ) {
// Show Fastlane watermark
renderWatermarkContent( renderWatermarkContent(
createElement( Watermark, { createElement( Watermark, {
fastlaneSdk, fastlaneSdk,
@ -104,6 +138,7 @@ export const updateWatermarkContent = ( {
} ) } )
); );
} else { } else {
// Clear watermark content
renderWatermarkContent( null ); renderWatermarkContent( null );
} }
}; };

View file

@ -4,6 +4,21 @@ import { injectShippingChangeButton } from '../components/Shipping';
import { injectCardChangeButton } from '../components/Card'; import { injectCardChangeButton } from '../components/Card';
import { setIsGuest, setIsEmailLookupCompleted } from '../stores/axoStore'; 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 = ( export const createEmailLookupHandler = (
fastlaneSdk, fastlaneSdk,
setShippingAddress, setShippingAddress,
@ -20,6 +35,7 @@ export const createEmailLookupHandler = (
try { try {
log( `Email value being looked up: ${ email }` ); log( `Email value being looked up: ${ email }` );
// Validate Fastlane SDK initialization
if ( ! fastlaneSdk ) { if ( ! fastlaneSdk ) {
throw new Error( 'FastlaneSDK is not initialized' ); throw new Error( 'FastlaneSDK is not initialized' );
} }
@ -30,12 +46,13 @@ export const createEmailLookupHandler = (
); );
} }
// Perform email lookup
const lookup = const lookup =
await fastlaneSdk.identity.lookupCustomerByEmail( email ); await fastlaneSdk.identity.lookupCustomerByEmail( email );
log( `Lookup response: ${ JSON.stringify( lookup ) }` ); log( `Lookup response: ${ JSON.stringify( lookup ) }` );
// Gary flow // Handle Gary flow (new user)
if ( lookup && lookup.customerContextId === '' ) { if ( lookup && lookup.customerContextId === '' ) {
setIsEmailLookupCompleted( true ); setIsEmailLookupCompleted( true );
} }
@ -45,6 +62,7 @@ export const createEmailLookupHandler = (
return; return;
} }
// Trigger authentication flow
const authResponse = const authResponse =
await fastlaneSdk.identity.triggerAuthenticationFlow( await fastlaneSdk.identity.triggerAuthenticationFlow(
lookup.customerContextId lookup.customerContextId
@ -56,15 +74,18 @@ export const createEmailLookupHandler = (
const { authenticationState, profileData } = authResponse; const { authenticationState, profileData } = authResponse;
// OTP success/fail/cancel flow // Mark email lookup as completed for OTP flow
if ( authResponse ) { if ( authResponse ) {
setIsEmailLookupCompleted( true ); setIsEmailLookupCompleted( true );
} }
// Handle successful authentication
if ( authenticationState === 'succeeded' ) { if ( authenticationState === 'succeeded' ) {
// Save current field values
snapshotFields( wooShippingAddress, wooBillingAddress ); snapshotFields( wooShippingAddress, wooBillingAddress );
setIsGuest( false ); setIsGuest( false );
// Update store with profile data
if ( profileData && profileData.shippingAddress ) { if ( profileData && profileData.shippingAddress ) {
setShippingAddress( profileData.shippingAddress ); setShippingAddress( profileData.shippingAddress );
} }
@ -74,12 +95,14 @@ export const createEmailLookupHandler = (
log( `Profile Data: ${ JSON.stringify( profileData ) }` ); log( `Profile Data: ${ JSON.stringify( profileData ) }` );
// Populate WooCommerce fields with profile data
populateWooFields( populateWooFields(
profileData, profileData,
setWooShippingAddress, setWooShippingAddress,
setWooBillingAddress setWooBillingAddress
); );
// Inject change buttons for shipping and card
injectShippingChangeButton( onChangeShippingAddressClick ); injectShippingChangeButton( onChangeShippingAddressClick );
injectCardChangeButton( onChangeCardButtonClick ); injectCardChangeButton( onChangeCardButtonClick );
} else { } else {

View file

@ -4,6 +4,8 @@ import { STORE_NAME } from '../stores/axoStore';
/** /**
* Sets up a class toggle based on the isGuest state for the express payment block. * Sets up a class toggle based on the isGuest state for the express payment block.
* This hides the express payment methods if the user is authenticated (Ryan flow).
*
* @return {Function} Unsubscribe function for cleanup. * @return {Function} Unsubscribe function for cleanup.
*/ */
export const setupAuthenticationClassToggle = () => { export const setupAuthenticationClassToggle = () => {
@ -41,6 +43,13 @@ export const setupAuthenticationClassToggle = () => {
return unsubscribe; return unsubscribe;
}; };
/**
* Sets up a class toggle based on the isEmailLookupCompleted state for the checkout fields block.
* This hides the Shipping Address fields, Billing Address fields, Shipping Options section,
* Order Notes section, Checkout Terms section, and Place Order button until email lookup is completed.
*
* @return {Function} Unsubscribe function for cleanup.
*/
export const setupEmailLookupCompletedClassToggle = () => { export const setupEmailLookupCompletedClassToggle = () => {
const targetSelector = '.wp-block-woocommerce-checkout-fields-block'; const targetSelector = '.wp-block-woocommerce-checkout-fields-block';
const emailLookupCompletedClass = 'wc-block-axo-email-lookup-completed'; const emailLookupCompletedClass = 'wc-block-axo-email-lookup-completed';
@ -77,7 +86,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. * @return {Function} Unsubscribe function for cleanup.
*/ */
export const setupCheckoutBlockClassToggles = () => { export const setupCheckoutBlockClassToggles = () => {
@ -133,7 +142,7 @@ export const setupCheckoutBlockClassToggles = () => {
/** /**
* Initializes all class toggles. * Initializes all class toggles.
* @return {Function} Cleanup function. * @return {Function} Cleanup function to unsubscribe all listeners.
*/ */
export const initializeClassToggles = () => { export const initializeClassToggles = () => {
const unsubscribeAuth = setupAuthenticationClassToggle(); const unsubscribeAuth = setupAuthenticationClassToggle();
@ -141,6 +150,7 @@ export const initializeClassToggles = () => {
setupEmailLookupCompletedClassToggle(); setupEmailLookupCompletedClassToggle();
const unsubscribeContactInfo = setupCheckoutBlockClassToggles(); const unsubscribeContactInfo = setupCheckoutBlockClassToggles();
// Return a cleanup function that unsubscribes all listeners
return () => { return () => {
if ( unsubscribeAuth ) { if ( unsubscribeAuth ) {
unsubscribeAuth(); unsubscribeAuth();

View file

@ -1,6 +1,12 @@
import { dispatch } from '@wordpress/data'; import { dispatch } from '@wordpress/data';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; 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 ) => { export const snapshotFields = ( shippingAddress, billingAddress ) => {
if ( ! shippingAddress || ! billingAddress ) { if ( ! shippingAddress || ! billingAddress ) {
log( log(
@ -15,6 +21,7 @@ export const snapshotFields = ( shippingAddress, billingAddress ) => {
const originalData = { shippingAddress, billingAddress }; const originalData = { shippingAddress, billingAddress };
log( `Snapshot data: ${ JSON.stringify( originalData ) }` ); log( `Snapshot data: ${ JSON.stringify( originalData ) }` );
try { try {
// Save the original data to localStorage
localStorage.setItem( localStorage.setItem(
'axoOriginalCheckoutFields', 'axoOriginalCheckoutFields',
JSON.stringify( originalData ) 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 = ( export const restoreOriginalFields = (
updateShippingAddress, updateShippingAddress,
updateBillingAddress updateBillingAddress
@ -31,6 +44,7 @@ export const restoreOriginalFields = (
log( 'Attempting to restore original fields' ); log( 'Attempting to restore original fields' );
let savedData; let savedData;
try { try {
// Retrieve saved data from localStorage
savedData = localStorage.getItem( 'axoOriginalCheckoutFields' ); savedData = localStorage.getItem( 'axoOriginalCheckoutFields' );
log( log(
`Data retrieved from localStorage: ${ JSON.stringify( savedData ) }` `Data retrieved from localStorage: ${ JSON.stringify( savedData ) }`
@ -42,11 +56,13 @@ export const restoreOriginalFields = (
if ( savedData ) { if ( savedData ) {
try { try {
const parsedData = JSON.parse( savedData ); const parsedData = JSON.parse( savedData );
// Restore shipping address if available
if ( parsedData.shippingAddress ) { if ( parsedData.shippingAddress ) {
updateShippingAddress( parsedData.shippingAddress ); updateShippingAddress( parsedData.shippingAddress );
} else { } else {
log( `No shipping address found in saved data`, 'warn' ); log( `No shipping address found in saved data`, 'warn' );
} }
// Restore billing address if available
if ( parsedData.billingAddress ) { if ( parsedData.billingAddress ) {
log( log(
`Restoring billing address: `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 = ( export const populateWooFields = (
profileData, profileData,
setWooShippingAddress, setWooShippingAddress,
@ -82,14 +105,14 @@ export const populateWooFields = (
const checkoutDispatch = dispatch( CHECKOUT_STORE_KEY ); 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 ( if (
typeof checkoutDispatch.__internalSetUseShippingAsBilling === 'function' typeof checkoutDispatch.__internalSetUseShippingAsBilling === 'function'
) { ) {
checkoutDispatch.__internalSetUseShippingAsBilling( false ); checkoutDispatch.__internalSetUseShippingAsBilling( false );
} }
// Save shipping address. // Prepare and set shipping address
const { address, name, phoneNumber } = profileData.shippingAddress; const { address, name, phoneNumber } = profileData.shippingAddress;
const shippingAddress = { const shippingAddress = {
@ -111,7 +134,7 @@ export const populateWooFields = (
); );
setWooShippingAddress( shippingAddress ); setWooShippingAddress( shippingAddress );
// Save billing address. // Prepare and set billing address
const billingData = profileData.card.paymentSource.card.billingAddress; const billingData = profileData.card.paymentSource.card.billingAddress;
const billingAddress = { const billingAddress = {
@ -132,12 +155,12 @@ export const populateWooFields = (
); );
setWooBillingAddress( billingAddress ); 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' ) { if ( typeof checkoutDispatch.setEditingShippingAddress === 'function' ) {
checkoutDispatch.setEditingShippingAddress( false ); 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' ) { if ( typeof checkoutDispatch.setEditingBillingAddress === 'function' ) {
checkoutDispatch.setEditingBillingAddress( false ); checkoutDispatch.setEditingBillingAddress( false );
} }

View file

@ -3,11 +3,21 @@ import { useDispatch, useSelect } from '@wordpress/data';
const CHECKOUT_STORE_KEY = 'wc/store/checkout'; 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 = () => { export const useAddressEditing = () => {
// Select address editing states from the checkout store
const { isEditingShippingAddress, isEditingBillingAddress } = useSelect( const { isEditingShippingAddress, isEditingBillingAddress } = useSelect(
( select ) => { ( select ) => {
const store = select( CHECKOUT_STORE_KEY ); const store = select( CHECKOUT_STORE_KEY );
return { return {
// Default to true if the getter function doesn't exist
isEditingShippingAddress: store.getEditingShippingAddress isEditingShippingAddress: store.getEditingShippingAddress
? store.getEditingShippingAddress() ? store.getEditingShippingAddress()
: true, : true,
@ -19,9 +29,11 @@ export const useAddressEditing = () => {
[] []
); );
// Get dispatch functions to update address editing states
const { setEditingShippingAddress, setEditingBillingAddress } = const { setEditingShippingAddress, setEditingBillingAddress } =
useDispatch( CHECKOUT_STORE_KEY ); useDispatch( CHECKOUT_STORE_KEY );
// Memoized function to update shipping address editing state
const setShippingAddressEditing = useCallback( const setShippingAddressEditing = useCallback(
( isEditing ) => { ( isEditing ) => {
if ( typeof setEditingShippingAddress === 'function' ) { if ( typeof setEditingShippingAddress === 'function' ) {
@ -31,6 +43,7 @@ export const useAddressEditing = () => {
[ setEditingShippingAddress ] [ setEditingShippingAddress ]
); );
// Memoized function to update billing address editing state
const setBillingAddressEditing = useCallback( const setBillingAddressEditing = useCallback(
( isEditing ) => { ( isEditing ) => {
if ( typeof setEditingBillingAddress === 'function' ) { if ( typeof setEditingBillingAddress === 'function' ) {
@ -40,6 +53,7 @@ export const useAddressEditing = () => {
[ setEditingBillingAddress ] [ setEditingBillingAddress ]
); );
// Return an object with address editing states and setter functions
return { return {
isEditingShippingAddress, isEditingShippingAddress,
isEditingBillingAddress, isEditingBillingAddress,

View file

@ -12,13 +12,21 @@ import {
import { restoreOriginalFields } from '../helpers/fieldHelpers'; import { restoreOriginalFields } from '../helpers/fieldHelpers';
import useCustomerData from './useCustomerData'; 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 = () => { const useAxoCleanup = () => {
// Get dispatch functions from the AXO store
const { setIsAxoActive, setIsGuest } = useDispatch( STORE_NAME ); const { setIsAxoActive, setIsGuest } = useDispatch( STORE_NAME );
// Get functions to update WooCommerce shipping and billing addresses
const { const {
setShippingAddress: updateWooShippingAddress, setShippingAddress: updateWooShippingAddress,
setBillingAddress: updateWooBillingAddress, setBillingAddress: updateWooBillingAddress,
} = useCustomerData(); } = useCustomerData();
// Effect to restore original WooCommerce fields on unmount
useEffect( () => { useEffect( () => {
return () => { return () => {
log( 'Cleaning up: Restoring WooCommerce fields' ); log( 'Cleaning up: Restoring WooCommerce fields' );
@ -29,14 +37,21 @@ const useAxoCleanup = () => {
}; };
}, [ updateWooShippingAddress, updateWooBillingAddress ] ); }, [ updateWooShippingAddress, updateWooBillingAddress ] );
// Effect to clean up AXO-specific functionality on unmount
useEffect( () => { useEffect( () => {
return () => { return () => {
log( 'Cleaning up Axo component' ); log( 'Cleaning up Axo component' );
// Reset AXO state
setIsAxoActive( false ); setIsAxoActive( false );
setIsGuest( true ); setIsGuest( true );
// Remove AXO UI elements
removeShippingChangeButton(); removeShippingChangeButton();
removeCardChangeButton(); removeCardChangeButton();
removeWatermark(); removeWatermark();
// Remove email functionality if it was set up
if ( isEmailFunctionalitySetup() ) { if ( isEmailFunctionalitySetup() ) {
log( 'Removing email functionality' ); log( 'Removing email functionality' );
removeEmailFunctionality(); removeEmailFunctionality();

View file

@ -12,20 +12,46 @@ import useCustomerData from './useCustomerData';
import useShippingAddressChange from './useShippingAddressChange'; import useShippingAddressChange from './useShippingAddressChange';
import useCardChange from './useCardChange'; import useCardChange from './useCardChange';
const useAxoSetup = ( ppcpConfig, fastlaneSdk, paymentComponent ) => { /**
* Custom hook to set up AXO functionality.
*
* @param {string} namespace - Namespace for the PayPal script.
* @param {Object} ppcpConfig - PayPal Checkout configuration.
* @param {boolean} isConfigLoaded - Whether the PayPal config has loaded.
* @param {Object} fastlaneSdk - Fastlane SDK instance.
* @param {Object} paymentComponent - Payment component instance.
* @return {boolean} Whether PayPal script has loaded.
*/
const useAxoSetup = (
namespace,
ppcpConfig,
isConfigLoaded,
fastlaneSdk,
paymentComponent
) => {
// Get dispatch functions from the AXO store
const { const {
setIsAxoActive, setIsAxoActive,
setIsAxoScriptLoaded, setIsAxoScriptLoaded,
setShippingAddress, setShippingAddress,
setCardDetails, setCardDetails,
} = useDispatch( STORE_NAME ); } = useDispatch( STORE_NAME );
const paypalLoaded = usePayPalScript( ppcpConfig );
// Check if PayPal script has loaded
const paypalLoaded = usePayPalScript(
namespace,
ppcpConfig,
isConfigLoaded
);
// Set up card and shipping address change handlers
const onChangeCardButtonClick = useCardChange( fastlaneSdk ); const onChangeCardButtonClick = useCardChange( fastlaneSdk );
const onChangeShippingAddressClick = useShippingAddressChange( const onChangeShippingAddressClick = useShippingAddressChange(
fastlaneSdk, fastlaneSdk,
setShippingAddress setShippingAddress
); );
// Get customer data and setter functions
const { const {
shippingAddress: wooShippingAddress, shippingAddress: wooShippingAddress,
billingAddress: wooBillingAddress, billingAddress: wooBillingAddress,
@ -33,17 +59,22 @@ const useAxoSetup = ( ppcpConfig, fastlaneSdk, paymentComponent ) => {
setBillingAddress: setWooBillingAddress, setBillingAddress: setWooBillingAddress,
} = useCustomerData(); } = useCustomerData();
// Set up phone sync handler
usePhoneSyncHandler( paymentComponent ); usePhoneSyncHandler( paymentComponent );
// Initialize class toggles on mount
useEffect( () => { useEffect( () => {
initializeClassToggles(); initializeClassToggles();
}, [] ); }, [] );
// Set up AXO functionality when PayPal and Fastlane are loaded
useEffect( () => { useEffect( () => {
setupWatermark( fastlaneSdk ); setupWatermark( fastlaneSdk );
if ( paypalLoaded && fastlaneSdk ) { if ( paypalLoaded && fastlaneSdk ) {
setIsAxoScriptLoaded( true ); setIsAxoScriptLoaded( true );
setIsAxoActive( true ); setIsAxoActive( true );
// Create and set up email lookup handler
const emailLookupHandler = createEmailLookupHandler( const emailLookupHandler = createEmailLookupHandler(
fastlaneSdk, fastlaneSdk,
setShippingAddress, setShippingAddress,

View file

@ -5,22 +5,29 @@ import { useAddressEditing } from './useAddressEditing';
import useCustomerData from './useCustomerData'; import useCustomerData from './useCustomerData';
import { STORE_NAME } from '../stores/axoStore'; 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 ) => { export const useCardChange = ( fastlaneSdk ) => {
const { setBillingAddressEditing } = useAddressEditing(); const { setBillingAddressEditing } = useAddressEditing();
const { setBillingAddress: setWooBillingAddress } = useCustomerData(); const { setBillingAddress: setWooBillingAddress } = useCustomerData();
const { setCardDetails, setShippingAddress } = useDispatch( STORE_NAME ); const { setCardDetails } = useDispatch( STORE_NAME );
return useCallback( async () => { return useCallback( async () => {
if ( fastlaneSdk ) { if ( fastlaneSdk ) {
// Show card selector and get the user's selection
const { selectionChanged, selectedCard } = const { selectionChanged, selectedCard } =
await fastlaneSdk.profile.showCardSelector(); await fastlaneSdk.profile.showCardSelector();
if ( selectionChanged && selectedCard?.paymentSource?.card ) { 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 } = const { name, billingAddress } =
selectedCard.paymentSource.card; 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 firstName = '';
let lastName = ''; let lastName = '';
@ -30,6 +37,7 @@ export const useCardChange = ( fastlaneSdk ) => {
lastName = nameParts.slice( 1 ).join( ' ' ); lastName = nameParts.slice( 1 ).join( ' ' );
} }
// Transform the billing address into WooCommerce format
const newBillingAddress = { const newBillingAddress = {
first_name: firstName, first_name: firstName,
last_name: lastName, last_name: lastName,
@ -41,20 +49,19 @@ export const useCardChange = ( fastlaneSdk ) => {
country: billingAddress?.countryCode || '', country: billingAddress?.countryCode || '',
}; };
// Batch state updates. // Batch update states
await Promise.all( [ await Promise.all( [
// Update the selected card details in the custom store
new Promise( ( resolve ) => { new Promise( ( resolve ) => {
setCardDetails( selectedCard ); setCardDetails( selectedCard );
resolve(); resolve();
} ), } ),
// Update the WooCommerce billing address in the WooCommerce store
new Promise( ( resolve ) => { new Promise( ( resolve ) => {
setWooBillingAddress( newBillingAddress ); setWooBillingAddress( newBillingAddress );
resolve(); resolve();
} ), } ),
new Promise( ( resolve ) => { // Trigger the Address Card view by setting the billing address editing state to false
setShippingAddress( newBillingAddress );
resolve();
} ),
new Promise( ( resolve ) => { new Promise( ( resolve ) => {
setBillingAddressEditing( false ); setBillingAddressEditing( false );
resolve(); resolve();
@ -68,7 +75,6 @@ export const useCardChange = ( fastlaneSdk ) => {
fastlaneSdk, fastlaneSdk,
setCardDetails, setCardDetails,
setWooBillingAddress, setWooBillingAddress,
setShippingAddress,
setBillingAddressEditing, setBillingAddressEditing,
] ); ] );
}; };

View file

@ -1,16 +1,24 @@
import { useCallback, useMemo } from '@wordpress/element'; import { useCallback, useMemo } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data'; 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 = () => { export const useCustomerData = () => {
// Fetch customer data from the WooCommerce store
const customerData = useSelect( ( select ) => const customerData = useSelect( ( select ) =>
select( 'wc/store/cart' ).getCustomerData() select( 'wc/store/cart' ).getCustomerData()
); );
// Get dispatch functions to update shipping and billing addresses
const { const {
setShippingAddress: setShippingAddressDispatch, setShippingAddress: setShippingAddressDispatch,
setBillingAddress: setBillingAddressDispatch, setBillingAddress: setBillingAddressDispatch,
} = useDispatch( 'wc/store/cart' ); } = useDispatch( 'wc/store/cart' );
// Memoized function to update shipping address
const setShippingAddress = useCallback( const setShippingAddress = useCallback(
( address ) => { ( address ) => {
setShippingAddressDispatch( address ); setShippingAddressDispatch( address );
@ -18,6 +26,7 @@ export const useCustomerData = () => {
[ setShippingAddressDispatch ] [ setShippingAddressDispatch ]
); );
// Memoized function to update billing address
const setBillingAddress = useCallback( const setBillingAddress = useCallback(
( address ) => { ( address ) => {
setBillingAddressDispatch( address ); setBillingAddressDispatch( address );
@ -25,6 +34,7 @@ export const useCustomerData = () => {
[ setBillingAddressDispatch ] [ setBillingAddressDispatch ]
); );
// Return memoized object with customer data and setter functions
return useMemo( return useMemo(
() => ( { () => ( {
shippingAddress: customerData.shippingAddress, shippingAddress: customerData.shippingAddress,

View file

@ -3,17 +3,30 @@ import { useCallback } from '@wordpress/element';
const isObject = ( value ) => typeof value === 'object' && value !== null; const isObject = ( value ) => typeof value === 'object' && value !== null;
const isNonEmptyString = ( value ) => value !== ''; 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 ) => { const removeEmptyValues = ( obj ) => {
// If not an object, return the value as is
if ( ! isObject( obj ) ) { if ( ! isObject( obj ) ) {
return obj; return obj;
} }
return Object.fromEntries( return Object.fromEntries(
Object.entries( obj ) Object.entries( obj )
// Recursively apply removeEmptyValues to nested objects
.map( ( [ key, value ] ) => [ .map( ( [ key, value ] ) => [
key, key,
isObject( value ) ? removeEmptyValues( value ) : value, isObject( value ) ? removeEmptyValues( value ) : value,
] ) ] )
// Filter out empty values
.filter( ( [ _, value ] ) => .filter( ( [ _, value ] ) =>
isObject( value ) isObject( value )
? Object.keys( value ).length > 0 ? 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 = () => { export const useDeleteEmptyKeys = () => {
return useCallback( removeEmptyValues, [] ); return useCallback( removeEmptyValues, [] );
}; };

View file

@ -1,21 +1,39 @@
import { useEffect, useRef, useState, useMemo } from '@wordpress/element'; import { useEffect, useRef, useState, useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import Fastlane from '../../../../ppcp-axo/resources/js/Connection/Fastlane'; import Fastlane from '../../../../ppcp-axo/resources/js/Connection/Fastlane';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
import { useDeleteEmptyKeys } from './useDeleteEmptyKeys'; import { useDeleteEmptyKeys } from './useDeleteEmptyKeys';
import { STORE_NAME } from '../stores/axoStore';
const useFastlaneSdk = ( axoConfig, ppcpConfig ) => { /**
* Custom hook to initialize and manage the Fastlane SDK.
*
* @param {string} namespace - Namespace for the PayPal script.
* @param {Object} axoConfig - Configuration for AXO.
* @param {Object} ppcpConfig - Configuration for PPCP.
* @return {Object|null} The initialized Fastlane SDK instance or null.
*/
const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
const [ fastlaneSdk, setFastlaneSdk ] = useState( null ); const [ fastlaneSdk, setFastlaneSdk ] = useState( null );
const initializingRef = useRef( false ); const initializingRef = useRef( false );
const configRef = useRef( { axoConfig, ppcpConfig } ); const configRef = useRef( { axoConfig, ppcpConfig } );
const deleteEmptyKeys = useDeleteEmptyKeys(); const deleteEmptyKeys = useDeleteEmptyKeys();
const { isPayPalLoaded } = useSelect(
( select ) => ( {
isPayPalLoaded: select( STORE_NAME ).getIsPayPalLoaded(),
} ),
[]
);
const styleOptions = useMemo( () => { const styleOptions = useMemo( () => {
return deleteEmptyKeys( configRef.current.axoConfig.style_options ); return deleteEmptyKeys( configRef.current.axoConfig.style_options );
}, [ deleteEmptyKeys ] ); }, [ deleteEmptyKeys ] );
// Effect to initialize Fastlane SDK
useEffect( () => { useEffect( () => {
const initFastlane = async () => { const initFastlane = async () => {
if ( initializingRef.current || fastlaneSdk ) { if ( initializingRef.current || fastlaneSdk || ! isPayPalLoaded ) {
return; return;
} }
@ -23,17 +41,20 @@ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => {
log( 'Init Fastlane' ); log( 'Init Fastlane' );
try { try {
const fastlane = new Fastlane(); const fastlane = new Fastlane( namespace );
// Set sandbox environment if configured
if ( configRef.current.axoConfig.environment.is_sandbox ) { if ( configRef.current.axoConfig.environment.is_sandbox ) {
window.localStorage.setItem( 'axoEnv', 'sandbox' ); window.localStorage.setItem( 'axoEnv', 'sandbox' );
} }
// Connect to Fastlane with locale and style options
await fastlane.connect( { await fastlane.connect( {
locale: configRef.current.ppcpConfig.locale, locale: configRef.current.ppcpConfig.locale,
styles: styleOptions, styles: styleOptions,
} ); } );
// Set locale (hardcoded to 'en_us' for now)
fastlane.setLocale( 'en_us' ); fastlane.setLocale( 'en_us' );
setFastlaneSdk( fastlane ); setFastlaneSdk( fastlane );
@ -45,8 +66,9 @@ const useFastlaneSdk = ( axoConfig, ppcpConfig ) => {
}; };
initFastlane(); initFastlane();
}, [ fastlaneSdk, styleOptions ] ); }, [ fastlaneSdk, styleOptions, isPayPalLoaded, namespace ] );
// Effect to update the config ref when configs change
useEffect( () => { useEffect( () => {
configRef.current = { axoConfig, ppcpConfig }; configRef.current = { axoConfig, ppcpConfig };
}, [ axoConfig, ppcpConfig ] ); }, [ axoConfig, ppcpConfig ] );

View file

@ -2,29 +2,40 @@ import { useCallback } from '@wordpress/element';
import { useSelect } from '@wordpress/data'; import { useSelect } from '@wordpress/data';
import { STORE_NAME } from '../stores/axoStore'; 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 = ( const useHandlePaymentSetup = (
emitResponse, emitResponse,
paymentComponent, paymentComponent,
tokenizedCustomerData tokenizedCustomerData
) => { ) => {
// Select card details from the store
const { cardDetails } = useSelect( const { cardDetails } = useSelect(
( select ) => ( { ( select ) => ( {
shippingAddress: select( STORE_NAME ).getShippingAddress(),
cardDetails: select( STORE_NAME ).getCardDetails(), cardDetails: select( STORE_NAME ).getCardDetails(),
} ), } ),
[] []
); );
return useCallback( async () => { return useCallback( async () => {
// Determine if it's a Ryan flow (saved card) based on the presence of card ID
const isRyanFlow = !! cardDetails?.id; const isRyanFlow = !! cardDetails?.id;
let cardToken = cardDetails?.id; let cardToken = cardDetails?.id;
// If no card token and payment component exists, get a new token
if ( ! cardToken && paymentComponent ) { if ( ! cardToken && paymentComponent ) {
cardToken = await paymentComponent cardToken = await paymentComponent
.getPaymentToken( tokenizedCustomerData ) .getPaymentToken( tokenizedCustomerData )
.then( ( response ) => response.id ); .then( ( response ) => response.id );
} }
// Handle error cases when card token is not available
if ( ! cardToken ) { if ( ! cardToken ) {
let reason = 'tokenization error'; let reason = 'tokenization error';

View file

@ -0,0 +1,46 @@
import { useState, useEffect } from '@wordpress/element';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
/**
* Custom hook to load and manage the PayPal Commerce Gateway configuration.
*
* @param {Object} initialConfig - Initial configuration object.
* @return {Object} An object containing the loaded config and a boolean indicating if it's loaded.
*/
const usePayPalCommerceGateway = ( initialConfig ) => {
const [ isConfigLoaded, setIsConfigLoaded ] = useState( false );
const [ ppcpConfig, setPpcpConfig ] = useState( initialConfig );
useEffect( () => {
/**
* Function to load the PayPal Commerce Gateway configuration.
*/
const loadConfig = () => {
if ( typeof window.PayPalCommerceGateway !== 'undefined' ) {
setPpcpConfig( window.PayPalCommerceGateway );
setIsConfigLoaded( true );
} else {
log( 'PayPal Commerce Gateway config not loaded.', 'error' );
}
};
// Check if the DOM is still loading
if ( document.readyState === 'loading' ) {
// If it's loading, add an event listener for when the DOM is fully loaded
document.addEventListener( 'DOMContentLoaded', loadConfig );
} else {
// If it's already loaded, call the loadConfig function immediately
loadConfig();
}
// Cleanup function to remove the event listener
return () => {
document.removeEventListener( 'DOMContentLoaded', loadConfig );
};
}, [] );
// Return the loaded configuration and the loading status
return { isConfigLoaded, ppcpConfig };
};
export default usePayPalCommerceGateway;

View file

@ -1,21 +1,48 @@
import { useState, useEffect } from '@wordpress/element'; import { useEffect } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
import { loadPaypalScript } from '../../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; import { loadPayPalScript } from '../../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
import { STORE_NAME } from '../stores/axoStore';
const usePayPalScript = ( ppcpConfig ) => { /**
const [ isLoaded, setIsLoaded ] = useState( false ); * Custom hook to load the PayPal script.
*
* @param {string} namespace - Namespace for the PayPal script.
* @param {Object} ppcpConfig - Configuration object for PayPal script.
* @param {boolean} isConfigLoaded - Whether the PayPal Commerce Gateway config is loaded.
* @return {boolean} True if the PayPal script has loaded, false otherwise.
*/
const usePayPalScript = ( namespace, ppcpConfig, isConfigLoaded ) => {
// Get dispatch functions from the AXO store
const { setIsPayPalLoaded } = useDispatch( STORE_NAME );
// Select relevant states from the AXO store
const { isPayPalLoaded } = useSelect(
( select ) => ( {
isPayPalLoaded: select( STORE_NAME ).getIsPayPalLoaded(),
} ),
[]
);
useEffect( () => { useEffect( () => {
if ( ! isLoaded ) { const loadScript = async () => {
log( 'Loading PayPal script' ); if ( ! isPayPalLoaded && isConfigLoaded ) {
loadPaypalScript( ppcpConfig, () => { try {
log( 'PayPal script loaded' ); await loadPayPalScript( namespace, ppcpConfig );
setIsLoaded( true ); setIsPayPalLoaded( true );
} ); } catch ( error ) {
} log(
}, [ ppcpConfig, isLoaded ] ); `Error loading PayPal script for namespace: ${ namespace }. Error: ${ error }`,
'error'
);
}
}
};
return isLoaded; loadScript();
}, [ ppcpConfig, isConfigLoaded, isPayPalLoaded ] );
return isPayPalLoaded;
}; };
export default usePayPalScript; export default usePayPalScript;

View file

@ -1,5 +1,13 @@
import { useEffect, useCallback } from '@wordpress/element'; 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 = ( const usePaymentSetupEffect = (
onPaymentSetup, onPaymentSetup,
handlePaymentSetup, handlePaymentSetup,
@ -17,6 +25,11 @@ const usePaymentSetupEffect = (
}; };
}, [ onPaymentSetup, handlePaymentSetup ] ); }, [ onPaymentSetup, handlePaymentSetup ] );
/**
* Callback function to handle payment component loading.
*
* @param {Object} component - The loaded payment component.
*/
const handlePaymentLoad = useCallback( const handlePaymentLoad = useCallback(
( component ) => { ( component ) => {
setPaymentComponent( component ); setPaymentComponent( component );

View file

@ -2,19 +2,30 @@ import { useCallback } from '@wordpress/element';
import { useAddressEditing } from './useAddressEditing'; import { useAddressEditing } from './useAddressEditing';
import useCustomerData from './useCustomerData'; 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 ) => { export const useShippingAddressChange = ( fastlaneSdk, setShippingAddress ) => {
const { setShippingAddressEditing } = useAddressEditing(); const { setShippingAddressEditing } = useAddressEditing();
const { setShippingAddress: setWooShippingAddress } = useCustomerData(); const { setShippingAddress: setWooShippingAddress } = useCustomerData();
return useCallback( async () => { return useCallback( async () => {
if ( fastlaneSdk ) { if ( fastlaneSdk ) {
// Show shipping address selector and get the user's selection
const { selectionChanged, selectedAddress } = const { selectionChanged, selectedAddress } =
await fastlaneSdk.profile.showShippingAddressSelector(); await fastlaneSdk.profile.showShippingAddressSelector();
if ( selectionChanged ) { if ( selectionChanged ) {
// Update the shipping address in the custom store with the selected address
setShippingAddress( selectedAddress ); setShippingAddress( selectedAddress );
const { address, name, phoneNumber } = selectedAddress; const { address, name, phoneNumber } = selectedAddress;
// Transform the selected address into WooCommerce format
const newShippingAddress = { const newShippingAddress = {
first_name: name.firstName, first_name: name.firstName,
last_name: name.lastName, last_name: name.lastName,
@ -27,11 +38,13 @@ export const useShippingAddressChange = ( fastlaneSdk, setShippingAddress ) => {
phone: phoneNumber.nationalNumber, phone: phoneNumber.nationalNumber,
}; };
// Update the WooCommerce shipping address in the WooCommerce store
await new Promise( ( resolve ) => { await new Promise( ( resolve ) => {
setWooShippingAddress( newShippingAddress ); setWooShippingAddress( newShippingAddress );
resolve(); resolve();
} ); } );
// Trigger the Address Card view by setting the shipping address editing state to false
await new Promise( ( resolve ) => { await new Promise( ( resolve ) => {
setShippingAddressEditing( false ); setShippingAddressEditing( false );
resolve(); resolve();

View file

@ -1,18 +1,27 @@
import { useMemo } from '@wordpress/element'; 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 = () => { export const useTokenizeCustomerData = () => {
const customerData = useSelect( ( select ) => const { billingAddress, shippingAddress } = useCustomerData();
select( 'wc/store/cart' ).getCustomerData()
);
/**
* 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 ) => { const isValidAddress = ( address ) => {
// At least one name must be present. // At least one name must be present
if ( ! address.first_name && ! address.last_name ) { if ( ! address.first_name && ! address.last_name ) {
return false; return false;
} }
// Street, city, postcode, country are mandatory; state is optional. // Street, city, postcode, country are mandatory; state is optional
return ( return (
address.address_1 && address.address_1 &&
address.city && 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( () => { return useMemo( () => {
const { billingAddress, shippingAddress } = customerData; // Determine the main address, preferring billing address if valid
// Prefer billing address, but fallback to shipping address if billing address is not valid.
const mainAddress = isValidAddress( billingAddress ) const mainAddress = isValidAddress( billingAddress )
? billingAddress ? billingAddress
: shippingAddress; : shippingAddress;
// Format the customer data for tokenization
return { return {
cardholderName: { cardholderName: {
fullName: `${ mainAddress.first_name } ${ mainAddress.last_name }`, fullName: `${ mainAddress.first_name } ${ mainAddress.last_name }`,
@ -43,7 +51,7 @@ export const useTokenizeCustomerData = () => {
countryCode: mainAddress.country, countryCode: mainAddress.country,
}, },
}; };
}, [ customerData ] ); }, [ billingAddress, shippingAddress ] );
}; };
export default useTokenizeCustomerData; export default useTokenizeCustomerData;

View file

@ -9,25 +9,25 @@ 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'; import usePaymentSetupEffect from './hooks/usePaymentSetupEffect';
import usePayPalCommerceGateway from './hooks/usePayPalCommerceGateway';
// Components // Components
import { Payment } from './components/Payment/Payment'; import { Payment } from './components/Payment/Payment';
const gatewayHandle = 'ppcp-axo-gateway'; const gatewayHandle = 'ppcp-axo-gateway';
const ppcpConfig = wc.wcSettings.getSetting( `${ gatewayHandle }_data` ); const namespace = 'ppcpBlocksPaypalAxo';
const initialConfig = wc.wcSettings.getSetting( `${ gatewayHandle }_data` );
if ( typeof window.PayPalCommerceGateway === 'undefined' ) {
window.PayPalCommerceGateway = ppcpConfig;
}
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 [ paymentComponent, setPaymentComponent ] = useState( null ); const [ paymentComponent, setPaymentComponent ] = useState( null );
const fastlaneSdk = useFastlaneSdk( axoConfig, ppcpConfig ); const { isConfigLoaded, ppcpConfig } =
usePayPalCommerceGateway( initialConfig );
const axoConfig = window.wc_ppcp_axo;
const fastlaneSdk = useFastlaneSdk( namespace, axoConfig, ppcpConfig );
const tokenizedCustomerData = useTokenizeCustomerData(); const tokenizedCustomerData = useTokenizeCustomerData();
const handlePaymentSetup = useHandlePaymentSetup( const handlePaymentSetup = useHandlePaymentSetup(
emitResponse, emitResponse,
@ -35,7 +35,13 @@ const Axo = ( props ) => {
tokenizedCustomerData tokenizedCustomerData
); );
useAxoSetup( ppcpConfig, fastlaneSdk, paymentComponent ); const isScriptLoaded = useAxoSetup(
namespace,
ppcpConfig,
isConfigLoaded,
fastlaneSdk,
paymentComponent
);
const { handlePaymentLoad } = usePaymentSetupEffect( const { handlePaymentLoad } = usePaymentSetupEffect(
onPaymentSetup, onPaymentSetup,
@ -45,31 +51,57 @@ const Axo = ( props ) => {
useAxoCleanup(); useAxoCleanup();
return fastlaneSdk ? ( if ( ! isConfigLoaded ) {
return (
<>
{ __(
'Loading configuration…',
'woocommerce-paypal-payments'
) }
</>
);
}
if ( ! isScriptLoaded ) {
return (
<>
{ __(
'Loading PayPal script…',
'woocommerce-paypal-payments'
) }
</>
);
}
if ( ! fastlaneSdk ) {
return (
<>{ __( 'Loading Fastlane…', 'woocommerce-paypal-payments' ) }</>
);
}
return (
<Payment <Payment
fastlaneSdk={ fastlaneSdk } fastlaneSdk={ fastlaneSdk }
onPaymentLoad={ handlePaymentLoad } onPaymentLoad={ handlePaymentLoad }
/> />
) : (
<>{ __( 'Loading Fastlane…', 'woocommerce-paypal-payments' ) }</>
); );
}; };
registerPaymentMethod( { registerPaymentMethod( {
name: ppcpConfig.id, name: initialConfig.id,
label: ( label: (
<div <div
id="ppcp-axo-block-radio-label" id="ppcp-axo-block-radio-label"
dangerouslySetInnerHTML={ { __html: ppcpConfig.title } } dangerouslySetInnerHTML={ { __html: initialConfig.title } }
/> />
), ),
content: <Axo />, content: <Axo />,
edit: createElement( ppcpConfig.title ), edit: createElement( initialConfig.title ),
ariaLabel: ppcpConfig.title, ariaLabel: initialConfig.title,
canMakePayment: () => true, canMakePayment: () => true,
supports: { supports: {
showSavedCards: true, showSavedCards: true,
features: ppcpConfig.supports, features: initialConfig.supports,
}, },
} ); } );

View file

@ -2,8 +2,8 @@ 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';
// Initial state
const DEFAULT_STATE = { const DEFAULT_STATE = {
isPayPalLoaded: false,
isGuest: true, isGuest: true,
isAxoActive: false, isAxoActive: false,
isAxoScriptLoaded: false, isAxoScriptLoaded: false,
@ -14,8 +14,12 @@ const DEFAULT_STATE = {
phoneNumber: '', phoneNumber: '',
}; };
// Actions // Action creators for updating the store state
const actions = { const actions = {
setIsPayPalLoaded: ( isPayPalLoaded ) => ( {
type: 'SET_IS_PAYPAL_LOADED',
payload: isPayPalLoaded,
} ),
setIsGuest: ( isGuest ) => ( { setIsGuest: ( isGuest ) => ( {
type: 'SET_IS_GUEST', type: 'SET_IS_GUEST',
payload: isGuest, payload: isGuest,
@ -50,9 +54,17 @@ 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 ) => { const reducer = ( state = DEFAULT_STATE, action ) => {
switch ( action.type ) { switch ( action.type ) {
case 'SET_IS_PAYPAL_LOADED':
return { ...state, isPayPalLoaded: action.payload };
case 'SET_IS_GUEST': case 'SET_IS_GUEST':
return { ...state, isGuest: action.payload }; return { ...state, isGuest: action.payload };
case 'SET_IS_AXO_ACTIVE': case 'SET_IS_AXO_ACTIVE':
@ -74,8 +86,9 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
} }
}; };
// Selectors // Selector functions to retrieve specific pieces of state
const selectors = { const selectors = {
getIsPayPalLoaded: ( state ) => state.isPayPalLoaded,
getIsGuest: ( state ) => state.isGuest, getIsGuest: ( state ) => state.isGuest,
getIsAxoActive: ( state ) => state.isAxoActive, getIsAxoActive: ( state ) => state.isAxoActive,
getIsAxoScriptLoaded: ( state ) => state.isAxoScriptLoaded, getIsAxoScriptLoaded: ( state ) => state.isAxoScriptLoaded,
@ -86,7 +99,7 @@ const selectors = {
getPhoneNumber: ( state ) => state.phoneNumber, getPhoneNumber: ( state ) => state.phoneNumber,
}; };
// Create and register the store // Create and register the Redux store for the AXO block
const store = createReduxStore( STORE_NAME, { const store = createReduxStore( STORE_NAME, {
reducer, reducer,
actions, actions,
@ -96,22 +109,57 @@ const store = createReduxStore( STORE_NAME, {
register( store ); register( store );
// Action dispatchers // Action dispatchers
/**
* Action dispatcher to update the PayPal script load status in the store.
*
* @param {boolean} isPayPalLoaded - Whether the PayPal script has loaded.
*/
export const setIsPayPalLoaded = ( isPayPalLoaded ) => {
dispatch( STORE_NAME ).setIsPayPalLoaded( isPayPalLoaded );
};
/**
* 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 ) => { export const setIsGuest = ( isGuest ) => {
dispatch( STORE_NAME ).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 ) => { export const setIsEmailLookupCompleted = ( isEmailLookupCompleted ) => {
dispatch( STORE_NAME ).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 ) => { export const setShippingAddress = ( shippingAddress ) => {
dispatch( STORE_NAME ).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 ) => { export const setCardDetails = ( cardDetails ) => {
dispatch( STORE_NAME ).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 ) => { export const setPhoneNumber = ( phoneNumber ) => {
dispatch( STORE_NAME ).setPhoneNumber( phoneNumber ); dispatch( STORE_NAME ).setPhoneNumber( phoneNumber );
}; };

View file

@ -102,7 +102,14 @@ class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule
add_action( add_action(
'woocommerce_blocks_payment_method_type_registration', 'woocommerce_blocks_payment_method_type_registration',
function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void { function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void {
$payment_method_registry->register( $c->get( 'axoblock.method' ) ); /*
* Only register the method if we are not in the admin
* (to avoid two Debit & Credit Cards gateways in the
* checkout block in the editor: one from ACDC one from Axo).
*/
if ( ! is_admin() ) {
$payment_method_registry->register( $c->get( 'axoblock.method' ) );
}
} }
); );

View file

@ -52,11 +52,12 @@ class AxoManager {
billingView = null; billingView = null;
cardView = null; cardView = null;
constructor( axoConfig, ppcpConfig ) { constructor( namespace, axoConfig, ppcpConfig ) {
this.namespace = namespace;
this.axoConfig = axoConfig; this.axoConfig = axoConfig;
this.ppcpConfig = ppcpConfig; this.ppcpConfig = ppcpConfig;
this.fastlane = new Fastlane(); this.fastlane = new Fastlane( namespace );
this.$ = jQuery; this.$ = jQuery;
this.status = { this.status = {
@ -801,8 +802,6 @@ class AxoManager {
} }
async onChangeEmail() { async onChangeEmail() {
this.clearData();
if ( ! this.status.active ) { if ( ! this.status.active ) {
log( 'Email checking skipped, AXO not active.' ); log( 'Email checking skipped, AXO not active.' );
return; return;
@ -813,11 +812,17 @@ class AxoManager {
return; return;
} }
if ( this.data.email === this.emailInput.value ) {
log( 'Email has not changed since last validation.' );
return;
}
log( log(
`Email changed: ${ `Email changed: ${
this.emailInput ? this.emailInput.value : '<empty>' this.emailInput ? this.emailInput.value : '<empty>'
}` }`
); );
this.clearData();
this.emailInput.value = this.stripSpaces( this.emailInput.value ); this.emailInput.value = this.stripSpaces( this.emailInput.value );

View file

@ -1,30 +1,35 @@
class FormFieldGroup { class FormFieldGroup {
#stored;
#data = {};
#active = false;
#baseSelector;
#contentSelector;
#fields = {};
#template;
constructor( config ) { constructor( config ) {
this.data = {}; this.#baseSelector = config.baseSelector;
this.#contentSelector = config.contentSelector;
this.baseSelector = config.baseSelector; this.#fields = config.fields || {};
this.contentSelector = config.contentSelector; this.#template = config.template;
this.fields = config.fields || {}; this.#stored = new Map();
this.template = config.template;
this.active = false;
} }
setData( data ) { setData( data ) {
this.data = data; this.#data = data;
this.refresh(); this.refresh();
} }
dataValue( fieldKey ) { dataValue( fieldKey ) {
if ( ! fieldKey || ! this.fields[ fieldKey ] ) { if ( ! fieldKey || ! this.#fields[ fieldKey ] ) {
return ''; return '';
} }
if ( typeof this.fields[ fieldKey ].valueCallback === 'function' ) { if ( typeof this.#fields[ fieldKey ].valueCallback === 'function' ) {
return this.fields[ fieldKey ].valueCallback( this.data ); return this.#fields[ fieldKey ].valueCallback( this.#data );
} }
const path = this.fields[ fieldKey ].valuePath; const path = this.#fields[ fieldKey ].valuePath;
if ( ! path ) { if ( ! path ) {
return ''; return '';
@ -35,27 +40,84 @@ class FormFieldGroup {
.reduce( .reduce(
( acc, key ) => ( acc, key ) =>
acc && acc[ key ] !== undefined ? acc[ key ] : undefined, acc && acc[ key ] !== undefined ? acc[ key ] : undefined,
this.data this.#data
); );
return value ? value : ''; return value ? value : '';
} }
/**
* Changes the value of the input field.
*
* @param {Element|null} field
* @param {string|boolean} value
* @return {boolean} True indicates that the previous value was different from the new value.
*/
#setFieldValue( field, value ) {
let oldVal;
const isValidOption = () => {
for ( let i = 0; i < field.options.length; i++ ) {
if ( field.options[ i ].value === value ) {
return true;
}
}
return false;
};
if ( ! field ) {
return false;
}
// If an invalid option is provided, do nothing.
if ( 'SELECT' === field.tagName && ! isValidOption() ) {
return false;
}
if ( 'checkbox' === field.type || 'radio' === field.type ) {
value = !! value;
oldVal = field.checked;
field.checked = value;
} else {
oldVal = field.value;
field.value = value;
}
return oldVal !== value;
}
/**
* Activate form group: Render a custom Fastlane UI to replace the WooCommerce form.
*
* Indicates: Ryan flow.
*/
activate() { activate() {
this.active = true; this.#active = true;
this.storeFormData();
this.refresh(); this.refresh();
} }
/**
* Deactivate form group: Remove the custom Fastlane UI - either display the default
* WooCommerce checkout form or no form at all (when no email was provided yet).
*
* Indicates: Gary flow / no email provided / not using Fastlane.
*/
deactivate() { deactivate() {
this.active = false; this.#active = false;
this.restoreFormData();
this.refresh(); this.refresh();
} }
toggle() { toggle() {
this.active ? this.deactivate() : this.activate(); if ( this.#active ) {
this.deactivate();
} else {
this.activate();
}
} }
refresh() { refresh() {
const content = document.querySelector( this.contentSelector ); const content = document.querySelector( this.#contentSelector );
if ( ! content ) { if ( ! content ) {
return; return;
@ -63,44 +125,145 @@ class FormFieldGroup {
content.innerHTML = ''; content.innerHTML = '';
if ( ! this.active ) { if ( ! this.#active ) {
this.hideField( this.contentSelector ); this.hideField( this.#contentSelector );
} else { } else {
this.showField( this.contentSelector ); this.showField( this.#contentSelector );
} }
Object.keys( this.fields ).forEach( ( key ) => { this.loopFields( ( { selector } ) => {
const field = this.fields[ key ]; if ( this.#active /* && ! field.showInput */ ) {
this.hideField( selector );
if ( this.active && ! field.showInput ) {
this.hideField( field.selector );
} else { } else {
this.showField( field.selector ); this.showField( selector );
} }
} ); } );
if ( typeof this.template === 'function' ) { if ( typeof this.#template === 'function' ) {
content.innerHTML = this.template( { content.innerHTML = this.#template( {
value: ( fieldKey ) => { value: ( fieldKey ) => {
return this.dataValue( fieldKey ); return this.dataValue( fieldKey );
}, },
isEmpty: () => { isEmpty: () => {
let isEmpty = true; let isEmpty = true;
Object.keys( this.fields ).forEach( ( fieldKey ) => {
this.loopFields( ( field, fieldKey ) => {
if ( this.dataValue( fieldKey ) ) { if ( this.dataValue( fieldKey ) ) {
isEmpty = false; isEmpty = false;
return false; return false;
} }
} ); } );
return isEmpty; return isEmpty;
}, },
} ); } );
} }
} }
/**
* Invoke a callback on every field in the current group.
*
* @param {(field: object, key: string) => void} callback
*/
loopFields( callback ) {
for ( const [ key, field ] of Object.entries( this.#fields ) ) {
const { selector, inputName } = field;
const inputSelector = `${ selector } [name="${ inputName }"]`;
const fieldInfo = {
inputSelector: inputName ? inputSelector : '',
...field,
};
callback( fieldInfo, key );
}
}
/**
* Stores the current form data in an internal storage.
* This allows the original form to be restored later.
*/
storeFormData() {
const storeValue = ( field, name ) => {
if ( 'checkbox' === field.type || 'radio' === field.type ) {
this.#stored.set( name, field.checked );
this.#setFieldValue( field, this.dataValue( name ) );
} else {
this.#stored.set( name, field.value );
this.#setFieldValue( field, '' );
}
};
this.loopFields( ( { inputSelector }, fieldKey ) => {
if ( inputSelector && ! this.#stored.has( fieldKey ) ) {
const elInput = document.querySelector( inputSelector );
if ( elInput ) {
storeValue( elInput, fieldKey );
}
}
} );
}
/**
* Restores the form data to its initial state before the form group was activated.
* This function iterates through the stored form fields and resets their values or states.
*/
restoreFormData() {
let formHasChanged = false;
// Reset form fields to their initial state.
this.loopFields( ( { inputSelector }, fieldKey ) => {
if ( ! this.#stored.has( fieldKey ) ) {
return;
}
const elInput = inputSelector
? document.querySelector( inputSelector )
: null;
const oldValue = this.#stored.get( fieldKey );
this.#stored.delete( fieldKey );
if ( this.#setFieldValue( elInput, oldValue ) ) {
formHasChanged = true;
}
} );
if ( formHasChanged ) {
document.body.dispatchEvent( new Event( 'update_checkout' ) );
}
}
/**
* Syncs the internal field-data with the hidden checkout form fields.
*/
syncDataToForm() {
if ( ! this.#active ) {
return;
}
let formHasChanged = false;
// Push data to the (hidden) checkout form.
this.loopFields( ( { inputSelector }, fieldKey ) => {
const elInput = inputSelector
? document.querySelector( inputSelector )
: null;
if ( this.#setFieldValue( elInput, this.dataValue( fieldKey ) ) ) {
formHasChanged = true;
}
} );
// Tell WooCommerce about the changes.
if ( formHasChanged ) {
document.body.dispatchEvent( new Event( 'update_checkout' ) );
}
}
showField( selector ) { showField( selector ) {
const field = document.querySelector( const field = document.querySelector(
this.baseSelector + ' ' + selector this.#baseSelector + ' ' + selector
); );
if ( field ) { if ( field ) {
field.classList.remove( 'ppcp-axo-field-hidden' ); field.classList.remove( 'ppcp-axo-field-hidden' );
@ -109,7 +272,7 @@ class FormFieldGroup {
hideField( selector ) { hideField( selector ) {
const field = document.querySelector( const field = document.querySelector(
this.baseSelector + ' ' + selector this.#baseSelector + ' ' + selector
); );
if ( field ) { if ( field ) {
field.classList.add( 'ppcp-axo-field-hidden' ); field.classList.add( 'ppcp-axo-field-hidden' );
@ -117,7 +280,7 @@ class FormFieldGroup {
} }
inputElement( name ) { inputElement( name ) {
const baseSelector = this.fields[ name ].selector; const baseSelector = this.#fields[ name ].selector;
const select = document.querySelector( baseSelector + ' select' ); const select = document.querySelector( baseSelector + ' select' );
if ( select ) { if ( select ) {
@ -138,9 +301,7 @@ class FormFieldGroup {
} }
toSubmitData( data ) { toSubmitData( data ) {
Object.keys( this.fields ).forEach( ( fieldKey ) => { this.loopFields( ( field, fieldKey ) => {
const field = this.fields[ fieldKey ];
if ( ! field.valuePath || ! field.selector ) { if ( ! field.valuePath || ! field.selector ) {
return true; return true;
} }

View file

@ -1,5 +1,6 @@
class Fastlane { class Fastlane {
construct() { constructor( namespace ) {
this.namespace = namespace;
this.connection = null; this.connection = null;
this.identity = null; this.identity = null;
this.profile = null; this.profile = null;
@ -10,7 +11,16 @@ class Fastlane {
connect( config ) { connect( config ) {
return new Promise( ( resolve, reject ) => { return new Promise( ( resolve, reject ) => {
window.paypal if ( ! window[ this.namespace ] ) {
reject(
new Error(
`Namespace ${ this.namespace } not found on window object`
)
);
return;
}
window[ this.namespace ]
.Fastlane( config ) .Fastlane( config )
.then( ( result ) => { .then( ( result ) => {
this.init( result ); this.init( result );
@ -18,7 +28,7 @@ class Fastlane {
} ) } )
.catch( ( error ) => { .catch( ( error ) => {
console.error( error ); console.error( error );
reject(); reject( error );
} ); } );
} ); } );
} }

View file

@ -45,42 +45,52 @@ class BillingView {
firstName: { firstName: {
selector: '#billing_first_name_field', selector: '#billing_first_name_field',
valuePath: null, valuePath: null,
inputName: 'billing_first_name',
}, },
lastName: { lastName: {
selector: '#billing_last_name_field', selector: '#billing_last_name_field',
valuePath: null, valuePath: null,
inputName: 'billing_last_name',
}, },
street1: { street1: {
selector: '#billing_address_1_field', selector: '#billing_address_1_field',
valuePath: 'billing.address.addressLine1', valuePath: 'billing.address.addressLine1',
inputName: 'billing_address_1',
}, },
street2: { street2: {
selector: '#billing_address_2_field', selector: '#billing_address_2_field',
valuePath: null, valuePath: null,
inputName: 'billing_address_2',
}, },
postCode: { postCode: {
selector: '#billing_postcode_field', selector: '#billing_postcode_field',
valuePath: 'billing.address.postalCode', valuePath: 'billing.address.postalCode',
inputName: 'billing_postcode',
}, },
city: { city: {
selector: '#billing_city_field', selector: '#billing_city_field',
valuePath: 'billing.address.adminArea2', valuePath: 'billing.address.adminArea2',
inputName: 'billing_city',
}, },
stateCode: { stateCode: {
selector: '#billing_state_field', selector: '#billing_state_field',
valuePath: 'billing.address.adminArea1', valuePath: 'billing.address.adminArea1',
inputName: 'billing_state',
}, },
countryCode: { countryCode: {
selector: '#billing_country_field', selector: '#billing_country_field',
valuePath: 'billing.address.countryCode', valuePath: 'billing.address.countryCode',
inputName: 'billing_country',
}, },
company: { company: {
selector: '#billing_company_field', selector: '#billing_company_field',
valuePath: null, valuePath: null,
inputName: 'billing_company',
}, },
phone: { phone: {
selector: '#billing_phone_field', selector: '#billing_phone_field',
valuePath: 'billing.phoneNumber', valuePath: 'billing.phoneNumber',
inputName: 'billing_phone',
}, },
}, },
} ); } );

View file

@ -90,42 +90,54 @@ class ShippingView {
key: 'firstName', key: 'firstName',
selector: '#shipping_first_name_field', selector: '#shipping_first_name_field',
valuePath: 'shipping.name.firstName', valuePath: 'shipping.name.firstName',
inputName: 'shipping_first_name',
}, },
lastName: { lastName: {
selector: '#shipping_last_name_field', selector: '#shipping_last_name_field',
valuePath: 'shipping.name.lastName', valuePath: 'shipping.name.lastName',
inputName: 'shipping_last_name',
}, },
street1: { street1: {
selector: '#shipping_address_1_field', selector: '#shipping_address_1_field',
valuePath: 'shipping.address.addressLine1', valuePath: 'shipping.address.addressLine1',
inputName: 'shipping_address_1',
}, },
street2: { street2: {
selector: '#shipping_address_2_field', selector: '#shipping_address_2_field',
valuePath: null, valuePath: null,
inputName: 'shipping_address_2',
}, },
postCode: { postCode: {
selector: '#shipping_postcode_field', selector: '#shipping_postcode_field',
valuePath: 'shipping.address.postalCode', valuePath: 'shipping.address.postalCode',
inputName: 'shipping_postcode',
}, },
city: { city: {
selector: '#shipping_city_field', selector: '#shipping_city_field',
valuePath: 'shipping.address.adminArea2', valuePath: 'shipping.address.adminArea2',
inputName: 'shipping_city',
}, },
stateCode: { stateCode: {
selector: '#shipping_state_field', selector: '#shipping_state_field',
valuePath: 'shipping.address.adminArea1', valuePath: 'shipping.address.adminArea1',
inputName: 'shipping_state',
}, },
countryCode: { countryCode: {
selector: '#shipping_country_field', selector: '#shipping_country_field',
valuePath: 'shipping.address.countryCode', valuePath: 'shipping.address.countryCode',
inputName: 'shipping_country',
}, },
company: { company: {
selector: '#shipping_company_field', selector: '#shipping_company_field',
valuePath: null, valuePath: null,
inputName: 'shipping_company',
}, },
shipDifferentAddress: { shipDifferentAddress: {
selector: '#ship-to-different-address', selector: '#ship-to-different-address',
valuePath: null, valuePath: null,
inputName: 'ship_to_different_address',
// Used by Woo to ensure correct location for taxes & shipping cost.
valueCallback: () => true,
}, },
phone: { phone: {
//'selector': '#billing_phone_field', // There is no shipping phone field. //'selector': '#billing_phone_field', // There is no shipping phone field.
@ -163,6 +175,7 @@ class ShippingView {
activate() { activate() {
this.group.activate(); this.group.activate();
this.group.syncDataToForm();
} }
deactivate() { deactivate() {
@ -175,6 +188,7 @@ class ShippingView {
setData( data ) { setData( data ) {
this.group.setData( data ); this.group.setData( data );
this.group.syncDataToForm();
} }
toSubmitData( data ) { toSubmitData( data ) {

View file

@ -1,21 +1,27 @@
import AxoManager from './AxoManager'; import AxoManager from './AxoManager';
import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
import { log } from './Helper/Debug';
( function ( { axoConfig, ppcpConfig, jQuery } ) { ( function ( { axoConfig, ppcpConfig, jQuery } ) {
const namespace = 'ppcpPaypalClassicAxo';
const bootstrap = () => { const bootstrap = () => {
new AxoManager( axoConfig, ppcpConfig ); new AxoManager( namespace, axoConfig, ppcpConfig );
}; };
document.addEventListener( 'DOMContentLoaded', () => { document.addEventListener( 'DOMContentLoaded', () => {
if ( ! typeof PayPalCommerceGateway ) { if ( typeof PayPalCommerceGateway === 'undefined' ) {
console.error( 'AXO could not be configured.' ); console.error( 'AXO could not be configured.' );
return; return;
} }
// Load PayPal // Load PayPal
loadPaypalScript( ppcpConfig, () => { loadPayPalScript( namespace, ppcpConfig )
bootstrap(); .then( () => {
} ); bootstrap();
} )
.catch( ( error ) => {
log( `Failed to load PayPal script: ${ error }`, 'error' );
} );
} ); } );
} )( { } )( {
axoConfig: window.wc_ppcp_axo, axoConfig: window.wc_ppcp_axo,

View file

@ -378,7 +378,8 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
return ! is_user_logged_in() return ! is_user_logged_in()
&& CartCheckoutDetector::has_classic_checkout() && CartCheckoutDetector::has_classic_checkout()
&& $dcc_configuration->use_fastlane() && $dcc_configuration->use_fastlane()
&& ! $this->is_excluded_endpoint(); && ! $this->is_excluded_endpoint()
&& is_checkout();
} }
/** /**

View file

@ -7,7 +7,7 @@ registerPaymentMethod( {
name: config.id, name: config.id,
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />, label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
content: <CardFields config={ config } />, content: <CardFields config={ config } />,
edit: <div></div>, edit: <CardFields config={ config } />,
ariaLabel: config.title, ariaLabel: config.title,
canMakePayment: () => { canMakePayment: () => {
return true; return true;

View file

@ -15,21 +15,21 @@ import {
cartHasSubscriptionProducts, cartHasSubscriptionProducts,
isPayPalSubscription, isPayPalSubscription,
} from './Helper/Subscription'; } from './Helper/Subscription';
import { loadPaypalScriptPromise } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js'; import { PayPalScriptProvider, PayPalButtons } from '@paypal/react-paypal-js';
import { normalizeStyleForFundingSource } from '../../../ppcp-button/resources/js/modules/Helper/Style'; import { normalizeStyleForFundingSource } from '../../../ppcp-button/resources/js/modules/Helper/Style';
import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
import BlockCheckoutMessagesBootstrap from './Bootstrap/BlockCheckoutMessagesBootstrap'; import BlockCheckoutMessagesBootstrap from './Bootstrap/BlockCheckoutMessagesBootstrap';
import { keysToCamelCase } from '../../../ppcp-button/resources/js/modules/Helper/Utils'; const namespace = 'ppcpBlocksPaypalExpressButtons';
import { handleShippingOptionsChange } from '../../../ppcp-button/resources/js/modules/Helper/ShippingHandler';
const config = wc.wcSettings.getSetting( 'ppcp-gateway_data' ); const config = wc.wcSettings.getSetting( 'ppcp-gateway_data' );
window.ppcpFundingSource = config.fundingSource; window.ppcpFundingSource = config.fundingSource;
let registeredContext = false; let registeredContext = false;
let paypalScriptPromise = null; let paypalScriptPromise = null;
const PAYPAL_GATEWAY_ID = 'ppcp-gateway';
const PayPalComponent = ( { const PayPalComponent = ( {
onClick, onClick,
onClose, onClose,
@ -47,6 +47,7 @@ const PayPalComponent = ( {
const { responseTypes } = emitResponse; const { responseTypes } = emitResponse;
const [ paypalOrder, setPaypalOrder ] = useState( null ); const [ paypalOrder, setPaypalOrder ] = useState( null );
const [ continuationFilled, setContinuationFilled ] = useState( false );
const [ gotoContinuationOnError, setGotoContinuationOnError ] = const [ gotoContinuationOnError, setGotoContinuationOnError ] =
useState( false ); useState( false );
@ -55,7 +56,10 @@ const PayPalComponent = ( {
if ( ! paypalScriptLoaded ) { if ( ! paypalScriptLoaded ) {
if ( ! paypalScriptPromise ) { if ( ! paypalScriptPromise ) {
// for editor, since canMakePayment was not called // for editor, since canMakePayment was not called
paypalScriptPromise = loadPaypalScriptPromise( config.scriptData ); paypalScriptPromise = loadPayPalScript(
namespace,
config.scriptData
);
} }
paypalScriptPromise.then( () => setPaypalScriptLoaded( true ) ); paypalScriptPromise.then( () => setPaypalScriptLoaded( true ) );
} }
@ -64,15 +68,33 @@ const PayPalComponent = ( {
? `${ config.id }-${ fundingSource }` ? `${ config.id }-${ fundingSource }`
: config.id; : config.id;
useEffect( () => { /**
// fill the form if in continuation (for product or mini-cart buttons) * The block cart displays express checkout buttons. Those buttons are handled by the
if ( * PAYPAL_GATEWAY_ID method on the server ("PayPal Smart Buttons").
! config.scriptData.continuation || *
! config.scriptData.continuation.order || * A possible bug in WooCommerce does not use the correct payment method ID for the express
window.ppcpContinuationFilled * payment buttons inside the cart, but sends the ID of the _first_ active payment method.
) { *
* This function uses an internal WooCommerce dispatcher method to set the correct method ID.
*/
const enforcePaymentMethodForCart = () => {
// Do nothing, unless we're handling block cart express payment buttons.
if ( 'cart-block' !== config.scriptData.context ) {
return; return;
} }
// Set the active payment method to PAYPAL_GATEWAY_ID.
wp.data
.dispatch( 'wc/store/payment' )
.__internalSetActivePaymentMethod( PAYPAL_GATEWAY_ID, {} );
};
useEffect( () => {
// fill the form if in continuation (for product or mini-cart buttons)
if ( continuationFilled || ! config.scriptData.continuation?.order ) {
return;
}
try { try {
const paypalAddresses = paypalOrderToWcAddresses( const paypalAddresses = paypalOrderToWcAddresses(
config.scriptData.continuation.order config.scriptData.continuation.order
@ -81,9 +103,11 @@ const PayPalComponent = ( {
.select( 'wc/store/cart' ) .select( 'wc/store/cart' )
.getCustomerData(); .getCustomerData();
const addresses = mergeWcAddress( wcAddresses, paypalAddresses ); const addresses = mergeWcAddress( wcAddresses, paypalAddresses );
wp.data wp.data
.dispatch( 'wc/store/cart' ) .dispatch( 'wc/store/cart' )
.setBillingAddress( addresses.billingAddress ); .setBillingAddress( addresses.billingAddress );
if ( shippingData.needsShipping ) { if ( shippingData.needsShipping ) {
wp.data wp.data
.dispatch( 'wc/store/cart' ) .dispatch( 'wc/store/cart' )
@ -93,9 +117,10 @@ const PayPalComponent = ( {
// sometimes the PayPal address is missing, skip in this case. // sometimes the PayPal address is missing, skip in this case.
console.log( err ); console.log( err );
} }
// this useEffect should run only once, but adding this in case of some kind of full re-rendering // this useEffect should run only once, but adding this in case of some kind of full re-rendering
window.ppcpContinuationFilled = true; setContinuationFilled( true );
}, [] ); }, [ shippingData, continuationFilled ] );
const createOrder = async ( data, actions ) => { const createOrder = async ( data, actions ) => {
try { try {
@ -232,6 +257,7 @@ const PayPalComponent = ( {
location.href = getCheckoutRedirectUrl(); location.href = getCheckoutRedirectUrl();
} else { } else {
setGotoContinuationOnError( true ); setGotoContinuationOnError( true );
enforcePaymentMethodForCart();
onSubmit(); onSubmit();
} }
} catch ( err ) { } catch ( err ) {
@ -323,6 +349,7 @@ const PayPalComponent = ( {
location.href = getCheckoutRedirectUrl(); location.href = getCheckoutRedirectUrl();
} else { } else {
setGotoContinuationOnError( true ); setGotoContinuationOnError( true );
enforcePaymentMethodForCart();
onSubmit(); onSubmit();
} }
} catch ( err ) { } catch ( err ) {
@ -365,19 +392,19 @@ const PayPalComponent = ( {
}; };
const shouldHandleShippingInPayPal = () => { const shouldHandleShippingInPayPal = () => {
return shouldskipFinalConfirmation() && config.needShipping return shouldskipFinalConfirmation() && config.needShipping;
}; };
const shouldskipFinalConfirmation = () => { const shouldskipFinalConfirmation = () => {
if ( config.finalReviewEnabled ) { if ( config.finalReviewEnabled ) {
return false; return false;
} }
return ( return (
window.ppcpFundingSource !== 'venmo' || window.ppcpFundingSource !== 'venmo' ||
! config.scriptData.vaultingEnabled ! config.scriptData.vaultingEnabled
); );
}; };
let handleShippingOptionsChange = null; let handleShippingOptionsChange = null;
let handleShippingAddressChange = null; let handleShippingAddressChange = null;
@ -591,7 +618,10 @@ const PayPalComponent = ( {
return null; return null;
} }
const PayPalButton = paypal.Buttons.driver( 'react', { React, ReactDOM } ); const PayPalButton = ppcpBlocksPaypalExpressButtons.Buttons.driver(
'react',
{ React, ReactDOM }
);
const getOnShippingOptionsChange = ( fundingSource ) => { const getOnShippingOptionsChange = ( fundingSource ) => {
if ( fundingSource === 'venmo' ) { if ( fundingSource === 'venmo' ) {
@ -611,11 +641,11 @@ const PayPalComponent = ( {
} }
return ( data, actions ) => { return ( data, actions ) => {
let shippingAddressChange = shouldHandleShippingInPayPal() const shippingAddressChange = shouldHandleShippingInPayPal()
? handleShippingAddressChange( data, actions ) ? handleShippingAddressChange( data, actions )
: null; : null;
return shippingAddressChange; return shippingAddressChange;
}; };
}; };
@ -795,7 +825,8 @@ if ( block_enabled && config.enabled ) {
ariaLabel: config.title, ariaLabel: config.title,
canMakePayment: async () => { canMakePayment: async () => {
if ( ! paypalScriptPromise ) { if ( ! paypalScriptPromise ) {
paypalScriptPromise = loadPaypalScriptPromise( paypalScriptPromise = loadPayPalScript(
namespace,
config.scriptData config.scriptData
); );
paypalScriptPromise.then( () => { paypalScriptPromise.then( () => {
@ -808,7 +839,9 @@ if ( block_enabled && config.enabled ) {
} }
await paypalScriptPromise; await paypalScriptPromise;
return paypal.Buttons( { fundingSource } ).isEligible(); return ppcpBlocksPaypalExpressButtons
.Buttons( { fundingSource } )
.isEligible();
}, },
supports: { supports: {
features, features,

View file

@ -0,0 +1,34 @@
import merge from 'deepmerge';
import { v4 as uuidv4 } from 'uuid';
import { keysToCamelCase } from './Utils';
const processAxoConfig = ( config ) => {
const scriptOptions = {};
const sdkClientToken = config?.axo?.sdk_client_token;
const uuid = uuidv4().replace( /-/g, '' );
if ( sdkClientToken ) {
scriptOptions[ 'data-sdk-client-token' ] = sdkClientToken;
scriptOptions[ 'data-client-metadata-id' ] = uuid;
}
return scriptOptions;
};
const processUserIdToken = ( config, sdkClientToken ) => {
const userIdToken = config?.save_payment_methods?.id_token;
return userIdToken && ! sdkClientToken
? { 'data-user-id-token': userIdToken }
: {};
};
export const processConfig = ( config ) => {
let scriptOptions = keysToCamelCase( config.url_params );
if ( config.script_attributes ) {
scriptOptions = merge( scriptOptions, config.script_attributes );
}
const axoOptions = processAxoConfig( config );
const userIdTokenOptions = processUserIdToken(
config,
axoOptions[ 'data-sdk-client-token' ]
);
return merge.all( [ scriptOptions, axoOptions, userIdTokenOptions ] );
};

View file

@ -0,0 +1,101 @@
import { loadScript } from '@paypal/paypal-js';
import dataClientIdAttributeHandler from '../DataClientIdAttributeHandler';
import widgetBuilder from '../Renderer/WidgetBuilder';
import { processConfig } from './ConfigProcessor';
const loadedScripts = new Map();
const scriptPromises = new Map();
const handleDataClientIdAttribute = async ( scriptOptions, config ) => {
if (
config.data_client_id?.set_attribute &&
config.vault_v3_enabled !== '1'
) {
return new Promise( ( resolve, reject ) => {
dataClientIdAttributeHandler(
scriptOptions,
config.data_client_id,
( paypal ) => {
widgetBuilder.setPaypal( paypal );
resolve( paypal );
},
reject
);
} );
}
return null;
};
export const loadPayPalScript = async ( namespace, config ) => {
if ( ! namespace ) {
throw new Error( 'Namespace is required' );
}
if ( loadedScripts.has( namespace ) ) {
console.log( `Script already loaded for namespace: ${ namespace }` );
return loadedScripts.get( namespace );
}
if ( scriptPromises.has( namespace ) ) {
console.log(
`Script loading in progress for namespace: ${ namespace }`
);
return scriptPromises.get( namespace );
}
const scriptOptions = {
...processConfig( config ),
'data-namespace': namespace,
};
const dataClientIdResult = await handleDataClientIdAttribute(
scriptOptions,
config
);
if ( dataClientIdResult ) {
return dataClientIdResult;
}
const scriptPromise = new Promise( ( resolve, reject ) => {
loadScript( scriptOptions )
.then( ( script ) => {
widgetBuilder.setPaypal( script );
loadedScripts.set( namespace, script );
console.log( `Script loaded for namespace: ${ namespace }` );
resolve( script );
} )
.catch( ( error ) => {
console.error(
`Failed to load script for namespace: ${ namespace }`,
error
);
reject( error );
} )
.finally( () => {
scriptPromises.delete( namespace );
} );
} );
scriptPromises.set( namespace, scriptPromise );
return scriptPromise;
};
export const loadAndRenderPayPalScript = async (
namespace,
options,
renderFunction,
renderTarget
) => {
if ( ! namespace ) {
throw new Error( 'Namespace is required' );
}
const scriptOptions = {
...options,
'data-namespace': namespace,
};
const script = await loadScript( scriptOptions );
widgetBuilder.setPaypal( script );
await renderFunction( script, renderTarget );
};

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button\Helper;
use Exception; use Exception;
use RuntimeException; use RuntimeException;
use WC_Cart; use WC_Cart;
use WC_Customer;
use WC_Data_Exception; use WC_Data_Exception;
use WC_Order; use WC_Order;
use WC_Order_Item_Product; use WC_Order_Item_Product;
@ -94,7 +95,7 @@ class WooCommerceOrderCreator {
$this->configure_payment_source( $wc_order ); $this->configure_payment_source( $wc_order );
$this->configure_customer( $wc_order ); $this->configure_customer( $wc_order );
$this->configure_line_items( $wc_order, $wc_cart, $payer, $shipping ); $this->configure_line_items( $wc_order, $wc_cart, $payer, $shipping );
$this->configure_shipping( $wc_order, $payer, $shipping, $wc_cart ); $this->configure_addresses( $wc_order, $payer, $shipping, $wc_cart );
$this->configure_coupons( $wc_order, $wc_cart->get_applied_coupons() ); $this->configure_coupons( $wc_order, $wc_cart->get_applied_coupons() );
$wc_order->calculate_totals(); $wc_order->calculate_totals();
@ -162,7 +163,7 @@ class WooCommerceOrderCreator {
$item->set_total( $subscription_total ); $item->set_total( $subscription_total );
$subscription->add_product( $product ); $subscription->add_product( $product );
$this->configure_shipping( $subscription, $payer, $shipping, $wc_cart ); $this->configure_addresses( $subscription, $payer, $shipping, $wc_cart );
$this->configure_payment_source( $subscription ); $this->configure_payment_source( $subscription );
$this->configure_coupons( $subscription, $wc_cart->get_applied_coupons() ); $this->configure_coupons( $subscription, $wc_cart->get_applied_coupons() );
@ -190,8 +191,9 @@ class WooCommerceOrderCreator {
* @param WC_Cart $wc_cart The Cart. * @param WC_Cart $wc_cart The Cart.
* @return void * @return void
* @throws WC_Data_Exception|RuntimeException When failing to configure shipping. * @throws WC_Data_Exception|RuntimeException When failing to configure shipping.
* @psalm-suppress RedundantConditionGivenDocblockType
*/ */
protected function configure_shipping( WC_Order $wc_order, ?Payer $payer, ?Shipping $shipping, WC_Cart $wc_cart ): void { protected function configure_addresses( WC_Order $wc_order, ?Payer $payer, ?Shipping $shipping, WC_Cart $wc_cart ): void {
$shipping_address = null; $shipping_address = null;
$billing_address = null; $billing_address = null;
$shipping_options = null; $shipping_options = null;
@ -200,7 +202,16 @@ class WooCommerceOrderCreator {
$address = $payer->address(); $address = $payer->address();
$payer_name = $payer->name(); $payer_name = $payer->name();
$wc_email = null;
$wc_customer = WC()->customer;
if ( $wc_customer instanceof WC_Customer ) {
$wc_email = $wc_customer->get_email();
}
$email = $wc_email ?: $payer->email_address();
$billing_address = array( $billing_address = array(
'email' => $email ?: '',
'first_name' => $payer_name ? $payer_name->given_name() : '', 'first_name' => $payer_name ? $payer_name->given_name() : '',
'last_name' => $payer_name ? $payer_name->surname() : '', 'last_name' => $payer_name ? $payer_name->surname() : '',
'address_1' => $address ? $address->address_line_1() : '', 'address_1' => $address ? $address->address_line_1() : '',

View file

@ -8,11 +8,17 @@ function renderField( cardField, inputField ) {
// Insert the PayPal card field after the original input field. // Insert the PayPal card field after the original input field.
const styles = cardFieldStyles( inputField ); const styles = cardFieldStyles( inputField );
cardField( { style: { input: styles } } ).render( inputField.parentNode ); const fieldOptions = {style: { input: styles },};
// Hide the original input field. if ( inputField.getAttribute( 'placeholder' ) ) {
hide( inputField, true ); fieldOptions.placeholder = inputField.getAttribute( 'placeholder' );
inputField.hidden = true; }
cardField( fieldOptions ).render( inputField.parentNode );
// Hide the original input field.
hide( inputField, true );
inputField.hidden = true;
} }
export function renderFields( cardFields ) { export function renderFields( cardFields ) {

View file

@ -5,7 +5,6 @@ import {
import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/PaymentButton'; import PaymentButton from '../../../ppcp-button/resources/js/modules/Renderer/PaymentButton';
import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder'; import widgetBuilder from '../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder';
import UpdatePaymentData from './Helper/UpdatePaymentData'; import UpdatePaymentData from './Helper/UpdatePaymentData';
import TransactionInfo from './Helper/TransactionInfo';
import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState'; import { PaymentMethods } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
import { setPayerData } from '../../../ppcp-button/resources/js/modules/Helper/PayerData'; import { setPayerData } from '../../../ppcp-button/resources/js/modules/Helper/PayerData';
import moduleStorage from './Helper/GooglePayStorage'; import moduleStorage from './Helper/GooglePayStorage';
@ -42,17 +41,11 @@ import moduleStorage from './Helper/GooglePayStorage';
* *
* @see https://developers.google.com/pay/api/web/reference/client * @see https://developers.google.com/pay/api/web/reference/client
* @typedef {Object} PaymentsClient * @typedef {Object} PaymentsClient
* @property {Function} createButton - The convenience method is used to * @property {Function} createButton - The convenience method is used to generate a Google Pay payment button styled with the latest Google Pay branding for insertion into a webpage.
* generate a Google Pay payment button styled with the latest Google Pay branding for * @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) method to determine a user's ability to return a form of payment from the Google Pay API.
* insertion into a webpage. * @property {(Object) => Promise} loadPaymentData - This method presents a Google Pay payment sheet that allows selection of a payment method and optionally configured parameters
* @property {Function} isReadyToPay - Use the isReadyToPay(isReadyToPayRequest) * @property {Function} onPaymentAuthorized - This method is called when a payment is authorized in the payment sheet.
* method to determine a user's ability to return a form of payment from the Google Pay API. * @property {Function} onPaymentDataChanged - This method handles payment data changes in the payment sheet such as shipping address and shipping options.
* @property {(Object) => Promise} loadPaymentData - This method presents a Google Pay payment
* sheet that allows selection of a payment method and optionally configured parameters
* @property {Function} onPaymentAuthorized - This method is called when a payment is
* authorized in the payment sheet.
* @property {Function} onPaymentDataChanged - This method handles payment data changes
* in the payment sheet such as shipping address and shipping options.
*/ */
/** /**
@ -62,18 +55,12 @@ import moduleStorage from './Helper/GooglePayStorage';
* @typedef {Object} TransactionInfo * @typedef {Object} TransactionInfo
* @property {string} currencyCode - Required. The ISO 4217 alphabetic currency code. * @property {string} currencyCode - Required. The ISO 4217 alphabetic currency code.
* @property {string} countryCode - Optional. required for EEA countries, * @property {string} countryCode - Optional. required for EEA countries,
* @property {string} transactionId - Optional. A unique ID that identifies a facilitation * @property {string} transactionId - Optional. A unique ID that identifies a facilitation attempt. Highly encouraged for troubleshooting.
* attempt. Highly encouraged for troubleshooting. * @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price used.
* @property {string} totalPriceStatus - Required. [ESTIMATED|FINAL] The status of the total price * @property {string} totalPrice - Required. Total monetary value of the transaction with an optional decimal precision of two decimal places.
* used: * @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet (e.g. subtotals, sales taxes, shipping charges, discounts etc.).
* @property {string} totalPrice - Required. Total monetary value of the transaction with an * @property {string} totalPriceLabel - Optional. Custom label for the total price within the display items.
* optional decimal precision of two decimal places. * @property {string} checkoutOption - Optional. Affects the submit button text displayed in the Google Pay payment sheet.
* @property {Array} displayItems - Optional. A list of cart items shown in the payment sheet
* (e.g. subtotals, sales taxes, shipping charges, discounts etc.).
* @property {string} totalPriceLabel - Optional. Custom label for the total price within the
* display items.
* @property {string} checkoutOption - Optional. Affects the submit button text displayed in the
* Google Pay payment sheet.
*/ */
function payerDataFromPaymentResponse( response ) { function payerDataFromPaymentResponse( response ) {

View file

@ -1,63 +1,78 @@
/* global paypal */
import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
import GooglepayButton from './GooglepayButton'; import GooglepayButton from './GooglepayButton';
import ContextHandlerFactory from './Context/ContextHandlerFactory'; import ContextHandlerFactory from './Context/ContextHandlerFactory';
class GooglepayManager { class GooglepayManager {
constructor( buttonConfig, ppcpConfig ) { constructor( namespace, buttonConfig, ppcpConfig ) {
this.namespace = namespace;
this.buttonConfig = buttonConfig; this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig; this.ppcpConfig = ppcpConfig;
this.googlePayConfig = null; this.googlePayConfig = null;
this.transactionInfo = null; this.transactionInfo = null;
this.contextHandler = null; this.contextHandler = null;
this.buttons = []; this.buttons = [];
this.onContextBootstrap = this.onContextBootstrap.bind( this ); buttonModuleWatcher.watchContextBootstrap( async ( bootstrap ) => {
buttonModuleWatcher.watchContextBootstrap( this.onContextBootstrap ); this.contextHandler = ContextHandlerFactory.create(
} bootstrap.context,
buttonConfig,
ppcpConfig,
bootstrap.handler
);
async onContextBootstrap( bootstrap ) { const button = GooglepayButton.createButton(
this.contextHandler = ContextHandlerFactory.create( bootstrap.context,
bootstrap.context, bootstrap.handler,
this.buttonConfig, buttonConfig,
this.ppcpConfig, ppcpConfig,
bootstrap.handler this.contextHandler
); );
const button = GooglepayButton.createButton( this.buttons.push( button );
bootstrap.context,
bootstrap.handler,
this.buttonConfig,
this.ppcpConfig,
this.contextHandler
);
this.buttons.push( button ); const initButton = () => {
button.configure( this.googlePayConfig, this.transactionInfo );
button.init();
};
// Ensure googlePayConfig and transactionInfo are loaded. // Initialize button only if googlePayConfig and transactionInfo are already fetched.
await this.init(); if ( this.googlePayConfig && this.transactionInfo ) {
initButton();
} else {
await this.init();
button.configure( this.googlePayConfig, this.transactionInfo ); if ( this.googlePayConfig && this.transactionInfo ) {
button.init(); initButton();
}
}
} );
} }
async init() { async init() {
try { try {
if ( ! this.googlePayConfig ) { if ( ! this.googlePayConfig ) {
// Gets GooglePay configuration of the PayPal merchant. // Gets GooglePay configuration of the PayPal merchant.
this.googlePayConfig = await paypal.Googlepay().config(); this.googlePayConfig = await window[ this.namespace ]
.Googlepay()
if ( ! this.googlePayConfig ) { .config();
console.error( 'No GooglePayConfig received during init' );
}
} }
if ( ! this.transactionInfo ) { if ( ! this.transactionInfo ) {
this.transactionInfo = await this.fetchTransactionInfo(); this.transactionInfo = await this.fetchTransactionInfo();
}
if ( ! this.transactionInfo ) { if ( ! this.googlePayConfig ) {
console.error( 'No transactionInfo found during init' ); console.error( 'No GooglePayConfig received during init' );
} else if ( ! this.transactionInfo ) {
console.error( 'No transactionInfo found during init' );
} else {
for ( const button of this.buttons ) {
button.configure(
this.googlePayConfig,
this.transactionInfo
);
button.init();
} }
} }
} catch ( error ) { } catch ( error ) {

View file

@ -2,7 +2,8 @@ import GooglepayButton from './GooglepayButton';
import ContextHandlerFactory from './Context/ContextHandlerFactory'; import ContextHandlerFactory from './Context/ContextHandlerFactory';
class GooglepayManagerBlockEditor { class GooglepayManagerBlockEditor {
constructor( buttonConfig, ppcpConfig ) { constructor( namespace, buttonConfig, ppcpConfig ) {
this.namespace = namespace;
this.buttonConfig = buttonConfig; this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig; this.ppcpConfig = ppcpConfig;
this.googlePayConfig = null; this.googlePayConfig = null;
@ -19,7 +20,9 @@ class GooglepayManagerBlockEditor {
async config() { async config() {
try { try {
// Gets GooglePay configuration of the PayPal merchant. // Gets GooglePay configuration of the PayPal merchant.
this.googlePayConfig = await ppcpBlocksEditorPaypalGooglepay.Googlepay().config(); this.googlePayConfig = await window[ this.namespace ]
.Googlepay()
.config();
// Fetch transaction information. // Fetch transaction information.
this.transactionInfo = await this.fetchTransactionInfo(); this.transactionInfo = await this.fetchTransactionInfo();

View file

@ -8,6 +8,8 @@ export default class TransactionInfo {
this.#country = country; this.#country = country;
this.#currency = currency; this.#currency = currency;
shippingFee = this.toAmount( shippingFee );
total = this.toAmount( total );
this.shippingFee = shippingFee; this.shippingFee = shippingFee;
this.amount = total - shippingFee; this.amount = total - shippingFee;
} }

View file

@ -4,7 +4,7 @@ import {
registerPaymentMethod, registerPaymentMethod,
} from '@woocommerce/blocks-registry'; } from '@woocommerce/blocks-registry';
import { __ } from '@wordpress/i18n'; import { __ } from '@wordpress/i18n';
import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
import GooglepayManager from './GooglepayManager'; import GooglepayManager from './GooglepayManager';
import { loadCustomScript } from '@paypal/paypal-js'; import { loadCustomScript } from '@paypal/paypal-js';
import GooglepayManagerBlockEditor from './GooglepayManagerBlockEditor'; import GooglepayManagerBlockEditor from './GooglepayManagerBlockEditor';
@ -14,7 +14,7 @@ const ppcpConfig = ppcpData.scriptData;
const buttonData = wc.wcSettings.getSetting( 'ppcp-googlepay_data' ); const buttonData = wc.wcSettings.getSetting( 'ppcp-googlepay_data' );
const buttonConfig = buttonData.scriptData; const buttonConfig = buttonData.scriptData;
const dataNamespace = 'ppcpBlocksEditorPaypalGooglepay'; const namespace = 'ppcpBlocksPaypalGooglepay';
if ( typeof window.PayPalCommerceGateway === 'undefined' ) { if ( typeof window.PayPalCommerceGateway === 'undefined' ) {
window.PayPalCommerceGateway = ppcpConfig; window.PayPalCommerceGateway = ppcpConfig;
@ -24,14 +24,7 @@ const GooglePayComponent = ( props ) => {
const [ bootstrapped, setBootstrapped ] = useState( false ); const [ bootstrapped, setBootstrapped ] = useState( false );
const [ paypalLoaded, setPaypalLoaded ] = useState( false ); const [ paypalLoaded, setPaypalLoaded ] = useState( false );
const [ googlePayLoaded, setGooglePayLoaded ] = useState( false ); const [ googlePayLoaded, setGooglePayLoaded ] = useState( false );
const [ manager, setManager ] = useState( null );
const bootstrap = function () {
const ManagerClass = props.isEditing
? GooglepayManagerBlockEditor
: GooglepayManager;
const manager = new ManagerClass( buttonConfig, ppcpConfig );
manager.init();
};
useEffect( () => { useEffect( () => {
// Load GooglePay SDK // Load GooglePay SDK
@ -41,22 +34,36 @@ const GooglePayComponent = ( props ) => {
ppcpConfig.url_params.components += ',googlepay'; ppcpConfig.url_params.components += ',googlepay';
if ( props.isEditing ) {
ppcpConfig.data_namespace = dataNamespace;
}
// Load PayPal // Load PayPal
loadPaypalScript( ppcpConfig, () => { loadPayPalScript( namespace, ppcpConfig )
setPaypalLoaded( true ); .then( () => {
} ); setPaypalLoaded( true );
} )
.catch( ( error ) => {
console.error( 'Failed to load PayPal script: ', error );
} );
}, [] ); }, [] );
useEffect( () => { useEffect( () => {
if ( ! bootstrapped && paypalLoaded && googlePayLoaded ) { if ( paypalLoaded && googlePayLoaded && ! manager ) {
setBootstrapped( true ); const ManagerClass = props.isEditing
bootstrap(); ? GooglepayManagerBlockEditor
: GooglepayManager;
const newManager = new ManagerClass(
namespace,
buttonConfig,
ppcpConfig
);
setManager( newManager );
} }
}, [ paypalLoaded, googlePayLoaded ] ); }, [ paypalLoaded, googlePayLoaded, props.isEditing ] );
useEffect( () => {
if ( manager && ! bootstrapped ) {
setBootstrapped( true );
manager.init();
}
}, [ manager, bootstrapped ] );
return ( return (
<div <div

View file

@ -8,7 +8,7 @@
*/ */
import { loadCustomScript } from '@paypal/paypal-js'; import { loadCustomScript } from '@paypal/paypal-js';
import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'; import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
import GooglepayManager from './GooglepayManager'; import GooglepayManager from './GooglepayManager';
import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper'; import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper';
import { CheckoutBootstrap } from './ContextBootstrap/CheckoutBootstrap'; import { CheckoutBootstrap } from './ContextBootstrap/CheckoutBootstrap';
@ -16,14 +16,18 @@ import moduleStorage from './Helper/GooglePayStorage';
( function ( { buttonConfig, ppcpConfig = {} } ) { ( function ( { buttonConfig, ppcpConfig = {} } ) {
const context = ppcpConfig.context; const context = ppcpConfig.context;
const namespace = 'ppcpPaypalGooglepay';
function bootstrapPayButton() { function bootstrapPayButton() {
if ( ! buttonConfig || ! ppcpConfig ) { if ( ! buttonConfig || ! ppcpConfig ) {
return; return;
} }
const manager = new GooglepayManager( buttonConfig, ppcpConfig ); const manager = new GooglepayManager(
manager.init(); namespace,
buttonConfig,
ppcpConfig
);
setupButtonEvents( function () { setupButtonEvents( function () {
manager.reinit(); manager.reinit();
@ -31,9 +35,11 @@ import moduleStorage from './Helper/GooglePayStorage';
} }
function bootstrapCheckout() { function bootstrapCheckout() {
if ( context if (
&& ! [ 'checkout' ].includes( context ) context &&
&& ! (context === 'mini-cart' && ppcpConfig.continuation) ) { ! [ 'checkout' ].includes( context ) &&
! ( context === 'mini-cart' && ppcpConfig.continuation )
) {
// Context must be missing/empty, or "checkout"/checkout continuation to proceed. // Context must be missing/empty, or "checkout"/checkout continuation to proceed.
return; return;
} }
@ -80,10 +86,14 @@ import moduleStorage from './Helper/GooglePayStorage';
} ); } );
// Load PayPal // Load PayPal
loadPaypalScript( ppcpConfig, () => { loadPayPalScript( namespace, ppcpConfig )
paypalLoaded = true; .then( () => {
tryToBoot(); paypalLoaded = true;
} ); tryToBoot();
} )
.catch( ( error ) => {
console.error( 'Failed to load PayPal script: ', error );
} );
} ); } );
} )( { } )( {
buttonConfig: window.wc_ppcp_googlepay, buttonConfig: window.wc_ppcp_googlepay,