mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-06 13:44:42 +08:00
🔀 Merge branch 'PCP-3771’
# Conflicts: # modules/ppcp-googlepay/resources/js/GooglepayManager.js
This commit is contained in:
commit
69d3e032d8
52 changed files with 1234 additions and 253 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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" />;
|
||||||
|
|
|
@ -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();
|
||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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 } />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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 );
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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 );
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
] );
|
] );
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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, [] );
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 ] );
|
||||||
|
|
|
@ -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';
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
|
|
@ -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 );
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
|
|
@ -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 );
|
||||||
};
|
};
|
||||||
|
|
|
@ -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' ) );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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 );
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 );
|
||||||
} );
|
} );
|
||||||
} );
|
} );
|
||||||
}
|
}
|
||||||
|
|
|
@ -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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -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 ) {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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 ] );
|
||||||
|
};
|
|
@ -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 );
|
||||||
|
};
|
|
@ -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() : '',
|
||||||
|
|
|
@ -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 ) {
|
||||||
|
|
|
@ -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 ) {
|
||||||
|
|
|
@ -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 ) {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue