🔀 Merge branch 'PCP-3380’

# Conflicts:
#	modules/ppcp-axo-block/resources/js/index.js
This commit is contained in:
Philipp Stracker 2024-09-16 13:11:23 +02:00
commit 50711fb37d
No known key found for this signature in database
47 changed files with 924 additions and 748 deletions

View file

@ -5,10 +5,8 @@
* @package WooCommerce\PayPalCommerce\AxoBlock
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AxoBlock;
return array(
);
return array();

View file

@ -5,10 +5,10 @@
* @package WooCommerce\PayPalCommerce\AxoBlock
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AxoBlock;
return static function (): AxoBlockModule {
return static function () : AxoBlockModule {
return new AxoBlockModule();
};

View file

@ -1,91 +1,102 @@
#ppcp-axo-block-radio-label {
// Variables
$border-color: hsla(0, 0%, 7%, 0.11);
$transition-duration: 0.3s;
$fast-transition-duration: 0.5s;
// Mixins
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
@mixin flex-space-between {
display: flex;
justify-content: space-between;
align-items: center;
}
// 1. AXO Block Radio Label
#ppcp-axo-block-radio-label {
@include flex-space-between;
width: 100%;
padding-right: 1em;
}
// 2. AXO Block Card
.wc-block-checkout-axo-block-card {
display: flex;
justify-content: center;
align-items: center;
@include flex-center;
width: 100%;
margin-bottom: 10px;
}
.wc-block-checkout-axo-block-card__inner {
display: flex;
flex-direction: column;
align-items: center;
max-width: 300px;
width: 100%;
}
.wc-block-checkout-axo-block-card__content {
box-sizing: border-box;
aspect-ratio: 1.586;
display: flex;
flex-direction: column;
justify-content: space-between;
border: 1px solid hsla(0,0%,7%,.11);
font-size: .875em;
font-family: monospace;
padding: 1em;
margin-bottom: 1em;
margin-top: 1em;
border-radius: 4px;
width: 100%;
}
.wc-block-checkout-axo-block-card__meta {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.wc-block-checkout-axo-block-card__meta-digits {
letter-spacing: 2px;
}
.wc-block-checkout-axo-block-card__watermark {
align-self: flex-end;
}
.wc-block-checkout-axo-block-card__edit {
background-color: transparent;
border: 0;
color: inherit;
cursor: pointer;
display: block;
font-family: inherit;
margin: 0 0 0 auto;
font-size: .875em;
font-weight: normal;
&:hover {
text-decoration: underline;
&__inner {
display: flex;
flex-direction: column;
align-items: center;
max-width: 300px;
width: 100%;
}
}
.wc-block-axo-block-card__meta:last-child {
align-self: flex-end;
&__content {
box-sizing: border-box;
aspect-ratio: 1.586;
display: flex;
flex-direction: column;
justify-content: space-between;
border: 1px solid $border-color;
font-size: 0.875em;
font-family: monospace;
padding: 1em;
margin: 1em 0;
border-radius: 4px;
width: 100%;
}
&__meta {
@include flex-space-between;
width: 100%;
&-digits {
letter-spacing: 2px;
}
&:last-child {
align-self: flex-end;
}
}
&__watermark {
align-self: flex-end;
}
&__edit {
background-color: transparent;
border: 0;
color: inherit;
cursor: pointer;
display: block;
font-family: inherit;
margin: 0 0 0 auto;
font-size: 0.875em;
font-weight: normal;
&:hover {
text-decoration: underline;
}
}
}
.wc-block-axo-block-card__meta-icon {
max-height: 25px;
}
// 3. Express Payment Block
.wp-block-woocommerce-checkout-express-payment-block {
transition: opacity .3s ease-in,
scale .3s ease-in,
display .3s ease-in;
/* key to transitioning out */
transition: opacity $transition-duration ease-in,
scale $transition-duration ease-in,
display $transition-duration ease-in;
transition-behavior: allow-discrete;
/* stage enter */
/* key to transitioning in */
@starting-style {
opacity: 0;
scale: 1.1;
@ -93,80 +104,85 @@
&.wc-block-axo-is-authenticated {
opacity: 0;
scale: .9;
/* hidden sets display: none, but loses easily */
scale: 0.9;
display: none !important;
/* faster leaving the stage then entering */
transition-duration: .5s;
transition-duration: $fast-transition-duration;
transition-timing-function: var(--ease-out-5);
}
}
.wc-block-axo-is-loaded .wc-block-components-text-input {
display: flex;
}
.wc-block-axo-is-loaded .wc-block-components-text-input {
margin-bottom: 0.5em;
}
.wc-block-checkout-axo-block-watermark-container {
margin-left: 5px;
}
// 4. AXO Loaded State
.wc-block-axo-is-loaded {
// 4.1 Text Input
.wc-block-components-text-input {
display: flex;
margin-bottom: 0.5em;
}
// 4.2 Hidden Fields
&:not(.wc-block-axo-email-lookup-completed) {
#shipping-fields,
#billing-fields,
#shipping-option,
#order-notes {
display: none;
}
}
// 4.3 Authenticated State
&.wc-block-axo-is-authenticated .wc-block-components-text-input {
gap: 14px 0;
}
// 4.4 Contact Information Block
.wp-block-woocommerce-checkout-contact-information-block .wc-block-components-text-input {
display: grid;
grid-template-areas:
"input button"
"watermark watermark"
"error error";
"input button"
"watermark watermark"
"error error";
grid-template-columns: 1fr auto;
gap: 6px 8px;
align-items: start;
input[type="email"] {
grid-area: input;
width: 100%;
}
}
#email {
align-self: center;
}
&.wc-block-axo-is-authenticated .wc-block-components-text-input {
gap: 14px 0;
}
.wc-block-components-text-input input[type="email"] {
grid-area: input;
width: 100%;
}
// 4.5 Email Submit Button
.wc-block-axo-email-submit-button-container {
grid-area: button;
align-self: center;
.wc-block-components-button {
white-space: nowrap;
}
}
// 4.6 Watermark Container
.wc-block-checkout-axo-block-watermark-container {
grid-area: watermark;
width: 100%;
}
// 4.7 Validation Error
.wc-block-components-validation-error {
grid-area: error;
width: 100%;
margin-top: 4px;
}
.wc-block-axo-email-submit-button-container .wc-block-components-button {
white-space: nowrap;
}
}
// 5. Shipping/Card Change Link
a.wc-block-axo-change-link {
& {
color: var(--wp--preset--color--secondary);
text-decoration: underline;
}
color: var(--wp--preset--color--secondary);
text-decoration: underline;
&:hover {
text-decoration: none;
@ -182,29 +198,36 @@ a.wc-block-axo-change-link {
}
}
// 6. Watermark Container
.wc-block-checkout-axo-block-watermark-container {
height: 25px;
margin-top: 5px;
margin-left: 5px;
}
.wp-block-woocommerce-checkout-contact-information-block:not( .wc-block-axo-is-loaded ) .wc-block-checkout-axo-block-watermark-container {
display: flex;
justify-content: left;
margin-left: 10px;
align-items: center;
position: relative;
.wc-block-components-spinner {
box-sizing: content-box;
color: inherit;
font-size: 1em;
height: auto;
width: auto;
// 7. Checkout Fields Block (AXO Not Loaded)
.wp-block-woocommerce-checkout-fields-block:not(.wc-block-axo-is-loaded) {
.wc-block-checkout-axo-block-watermark-container {
display: flex;
justify-content: left;
margin-left: 10px;
align-items: center;
position: relative;
margin-top: 12px;
.wc-block-components-spinner {
box-sizing: content-box;
color: inherit;
font-size: 1em;
height: auto;
width: auto;
position: relative;
margin-top: 12px;
}
}
}
.wp-block-woocommerce-checkout-contact-information-block.wc-block-axo-is-loaded {
// 8. AXO Loaded Contact Information Block
.wc-block-axo-is-loaded .wp-block-woocommerce-checkout-contact-information-block {
.wc-block-checkout-axo-block-watermark-container .wc-block-components-spinner {
display: none;
visibility: hidden;
@ -212,19 +235,19 @@ a.wc-block-axo-change-link {
}
}
// 9. Transitions
.wc-block-axo-email-submit-button-container,
.wc-block-checkout-axo-block-watermark-container #fastlane-watermark-email,
a.wc-block-axo-change-link{
a.wc-block-axo-change-link {
transition: opacity 0.5s ease-in-out;
/* stage enter */
/* key to transitioning in */
@starting-style {
opacity: 0;
scale: 1.1;
}
}
// 10. Shipping Fields
#shipping-fields .wc-block-components-checkout-step__heading {
display: flex;
}

View file

@ -1,5 +1,5 @@
import { useMemo } from '@wordpress/element';
import { Watermark } from '../watermark';
import { Watermark } from '../Watermark';
const cardIcons = {
VISA: 'visa-light.svg',
@ -11,7 +11,7 @@ const cardIcons = {
UNIONPAY: 'unionpay-light.svg',
};
export const Card = ( { card, fastlaneSdk, showWatermark = true } ) => {
const Card = ( { card, fastlaneSdk, showWatermark = true } ) => {
const { brand, lastDigits, expiry, name } = card?.paymentSource?.card ?? {};
const cardLogo = useMemo( () => {
@ -59,3 +59,5 @@ export const Card = ( { card, fastlaneSdk, showWatermark = true } ) => {
</div>
);
};
export default Card;

View file

@ -0,0 +1,18 @@
import { createElement } from '@wordpress/element';
const CardChangeButton = ( { onChangeButtonClick } ) =>
createElement(
'a',
{
className:
'wc-block-checkout-axo-block-card__edit wc-block-axo-change-link',
role: 'button',
onClick: ( event ) => {
event.preventDefault();
onChangeButtonClick();
},
},
'Choose a different card'
);
export default CardChangeButton;

View file

@ -1,19 +1,5 @@
import { createElement, useEffect, createRoot } from '@wordpress/element';
const CardChangeButton = ( { onChangeButtonClick } ) =>
createElement(
'a',
{
className:
'wc-block-checkout-axo-block-card__edit wc-block-axo-change-link',
role: 'button',
onClick: ( event ) => {
event.preventDefault();
onChangeButtonClick();
},
},
'Choose a different card'
);
import { createElement, createRoot, useEffect } from '@wordpress/element';
import CardChangeButton from './CardChangeButton';
const CardChangeButtonManager = ( { onChangeButtonClick } ) => {
useEffect( () => {
@ -50,21 +36,4 @@ const CardChangeButtonManager = ( { onChangeButtonClick } ) => {
return null;
};
export const injectCardChangeButton = ( onChangeButtonClick ) => {
const container = document.createElement( 'div' );
document.body.appendChild( container );
createRoot( container ).render(
createElement( CardChangeButtonManager, { onChangeButtonClick } )
);
};
export const removeCardChangeButton = () => {
const button = document.querySelector(
'.wc-block-checkout-axo-block-card__edit'
);
if ( button && button.parentNode ) {
button.parentNode.remove();
}
};
export default CardChangeButtonManager;

View file

@ -0,0 +1,4 @@
export { default as Card } from './Card';
export { default as CardChangeButton } from './CardChangeButton';
export { default as CardChangeButtonManager } from './CardChangeButtonManager';
export { injectCardChangeButton, removeCardChangeButton } from './utils';

View file

@ -0,0 +1,19 @@
import { createElement, createRoot } from '@wordpress/element';
import CardChangeButtonManager from './CardChangeButtonManager';
export const injectCardChangeButton = ( onChangeButtonClick ) => {
const container = document.createElement( 'div' );
document.body.appendChild( container );
createRoot( container ).render(
createElement( CardChangeButtonManager, { onChangeButtonClick } )
);
};
export const removeCardChangeButton = () => {
const button = document.querySelector(
'.wc-block-checkout-axo-block-card__edit'
);
if ( button && button.parentNode ) {
button.parentNode.remove();
}
};

View file

@ -1,12 +1,12 @@
import { STORE_NAME } from '../../stores/axoStore';
import { useSelect } from '@wordpress/data';
export const EmailButton = ( { handleSubmit } ) => {
const EmailButton = ( { handleSubmit } ) => {
const { isGuest, isAxoActive, isEmailSubmitted } = useSelect(
( select ) => ( {
isGuest: select( STORE_NAME ).getIsGuest(),
isAxoActive: select( STORE_NAME ).getIsAxoActive(),
isEmailSubmitted: select( STORE_NAME ).isEmailSubmitted(),
isEmailSubmitted: select( STORE_NAME ).getIsEmailSubmitted(),
} )
);
@ -46,3 +46,5 @@ export const EmailButton = ( { handleSubmit } ) => {
</button>
);
};
export default EmailButton;

View file

@ -0,0 +1,6 @@
export { default as EmailButton } from './EmailButton';
export {
setupEmailFunctionality,
removeEmailFunctionality,
isEmailFunctionalitySetup,
} from './utils';

View file

@ -1,6 +1,6 @@
import { createElement, createRoot } from '@wordpress/element';
import { STORE_NAME } from '../../stores/axoStore';
import { EmailButton } from './EmailButton';
import EmailButton from './EmailButton';
let emailInput = null;
let submitButtonReference = {
@ -17,7 +17,7 @@ const getEmailInput = () => {
return emailInput;
};
const setupEmailFunctionality = ( onEmailSubmit ) => {
export const setupEmailFunctionality = ( onEmailSubmit ) => {
const input = getEmailInput();
if ( ! input ) {
console.warn(
@ -29,7 +29,7 @@ const setupEmailFunctionality = ( onEmailSubmit ) => {
const handleEmailSubmit = async () => {
const isEmailSubmitted = wp.data
.select( STORE_NAME )
.isEmailSubmitted();
.getIsEmailSubmitted();
if ( isEmailSubmitted || ! input.value ) {
return;
@ -96,7 +96,7 @@ const setupEmailFunctionality = ( onEmailSubmit ) => {
} );
};
const removeEmailFunctionality = () => {
export const removeEmailFunctionality = () => {
const input = getEmailInput();
if ( input && keydownHandler ) {
input.removeEventListener( 'keydown', keydownHandler );
@ -120,12 +120,6 @@ const removeEmailFunctionality = () => {
keydownHandler = null;
};
const isEmailFunctionalitySetup = () => {
export const isEmailFunctionalitySetup = () => {
return !! submitButtonReference.root;
};
export {
setupEmailFunctionality,
removeEmailFunctionality,
isEmailFunctionalitySetup,
};

View file

@ -1,7 +1,7 @@
import { useEffect, useCallback } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { Card } from './card/Card';
import { STORE_NAME } from '../stores/axoStore';
import { Card } from '../Card';
import { STORE_NAME } from '../../stores/axoStore';
export const Payment = ( { fastlaneSdk, card, onPaymentLoad } ) => {
const isGuest = useSelect( ( select ) =>

View file

@ -1,9 +1,6 @@
export { default as ShippingChangeButton } from './ShippingChangeButton';
export { default as ShippingChangeButtonManager } from './ShippingChangeButtonManager';
export {
snapshotFields,
restoreOriginalFields,
populateWooFields,
injectShippingChangeButton,
removeShippingChangeButton,
} from './utils';

View file

@ -0,0 +1,32 @@
import { createRoot } from '@wordpress/element';
import ShippingChangeButtonManager from './ShippingChangeButtonManager';
export const injectShippingChangeButton = ( onChangeShippingAddressClick ) => {
const existingButton = document.querySelector(
'#shipping-fields .wc-block-checkout-axo-block-card__edit'
);
if ( ! existingButton ) {
const container = document.createElement( 'div' );
document.body.appendChild( container );
createRoot( container ).render(
<ShippingChangeButtonManager
onChangeShippingAddressClick={ onChangeShippingAddressClick }
/>
);
} else {
console.log(
'Shipping change button already exists. Skipping injection.'
);
}
};
export const removeShippingChangeButton = () => {
const span = document.querySelector(
'#shipping-fields .wc-block-checkout-axo-block-card__edit'
);
if ( span ) {
createRoot( span ).unmount();
span.remove();
}
};

View file

@ -19,7 +19,7 @@ const WatermarkManager = ( { fastlaneSdk } ) => {
select( STORE_NAME ).getIsAxoActive()
);
const isAxoScriptLoaded = useSelect( ( select ) =>
select( STORE_NAME ).isAxoScriptLoaded()
select( STORE_NAME ).getIsAxoScriptLoaded()
);
console.log( 'WatermarkManager state', {

View file

@ -1,5 +1,5 @@
import { createElement, createRoot } from '@wordpress/element';
import { Watermark, WatermarkManager } from '../watermark';
import { Watermark, WatermarkManager } from '../Watermark';
const watermarkReference = {
container: null,

View file

@ -1,15 +0,0 @@
import { EmailButton } from './EmailButton';
import {
setupEmailFunctionality,
removeEmailFunctionality,
isEmailFunctionalitySetup,
} from './utils';
export {
EmailButton,
setupEmailFunctionality,
removeEmailFunctionality,
isEmailFunctionalitySetup,
};
export default EmailButton;

View file

@ -1,149 +0,0 @@
import { createRoot } from '@wordpress/element';
import ShippingChangeButtonManager from './ShippingChangeButtonManager';
export const snapshotFields = ( shippingAddress, billingAddress ) => {
console.log( 'Attempting to snapshot fields' );
if ( ! shippingAddress || ! billingAddress ) {
console.warn( 'Shipping or billing address is missing:', {
shippingAddress,
billingAddress,
} );
}
const originalData = { shippingAddress, billingAddress };
console.log( 'Snapshot data:', originalData );
try {
localStorage.setItem(
'axoOriginalCheckoutFields',
JSON.stringify( originalData )
);
console.log( 'Original fields saved to localStorage', originalData );
} catch ( error ) {
console.error( 'Error saving to localStorage:', error );
}
};
export const restoreOriginalFields = (
updateShippingAddress,
updateBillingAddress
) => {
console.log( 'Attempting to restore original fields' );
let savedData;
try {
savedData = localStorage.getItem( 'axoOriginalCheckoutFields' );
console.log( 'Data retrieved from localStorage:', savedData );
} catch ( error ) {
console.error( 'Error retrieving from localStorage:', error );
}
if ( savedData ) {
try {
const parsedData = JSON.parse( savedData );
console.log( 'Parsed data:', parsedData );
if ( parsedData.shippingAddress ) {
console.log(
'Restoring shipping address:',
parsedData.shippingAddress
);
updateShippingAddress( parsedData.shippingAddress );
} else {
console.warn( 'No shipping address found in saved data' );
}
if ( parsedData.billingAddress ) {
console.log(
'Restoring billing address:',
parsedData.billingAddress
);
updateBillingAddress( parsedData.billingAddress );
} else {
console.warn( 'No billing address found in saved data' );
}
console.log(
'Original fields restored from localStorage',
parsedData
);
} catch ( error ) {
console.error( 'Error parsing saved data:', error );
}
} else {
console.warn(
'No data found in localStorage under axoOriginalCheckoutFields'
);
}
};
export const populateWooFields = (
profileData,
setWooShippingAddress,
setWooBillingAddress
) => {
console.log(
'Populating WooCommerce fields with profile data:',
profileData
);
// Save shipping address
const { address, name, phoneNumber } = profileData.shippingAddress;
const shippingAddress = {
first_name: name.firstName,
last_name: name.lastName,
address_1: address.addressLine1,
address_2: address.addressLine2 || '',
city: address.adminArea2,
state: address.adminArea1,
postcode: address.postalCode,
country: address.countryCode,
phone: phoneNumber.nationalNumber,
};
console.log( 'Setting WooCommerce shipping address:', shippingAddress );
setWooShippingAddress( shippingAddress );
// Save billing address
const billingData = profileData.card.paymentSource.card.billingAddress;
const billingAddress = {
first_name: profileData.name.firstName,
last_name: profileData.name.lastName,
address_1: billingData.addressLine1,
address_2: billingData.addressLine2 || '',
city: billingData.adminArea2,
state: billingData.adminArea1,
postcode: billingData.postalCode,
country: billingData.countryCode,
};
console.log( 'Setting WooCommerce billing address:', billingAddress );
setWooBillingAddress( billingAddress );
};
export const injectShippingChangeButton = ( onChangeShippingAddressClick ) => {
const existingButton = document.querySelector(
'#shipping-fields .wc-block-checkout-axo-block-card__edit'
);
if ( ! existingButton ) {
const container = document.createElement( 'div' );
document.body.appendChild( container );
createRoot( container ).render(
<ShippingChangeButtonManager
onChangeShippingAddressClick={ onChangeShippingAddressClick }
/>
);
} else {
console.log(
'Shipping change button already exists. Skipping injection.'
);
}
};
export const removeShippingChangeButton = () => {
const span = document.querySelector(
'#shipping-fields .wc-block-checkout-axo-block-card__edit'
);
if ( span ) {
createRoot( span ).unmount();
span.remove();
}
};

View file

@ -1,7 +1,7 @@
import { populateWooFields } from '../helpers/fieldHelpers';
import { injectShippingChangeButton } from '../components/shipping';
import { injectCardChangeButton } from '../helpers/cardChangeButtonManager';
import { setIsGuest } from '../stores/axoStore';
import { injectShippingChangeButton } from '../components/Shipping';
import { injectCardChangeButton } from '../components/Card';
import { setIsGuest, setIsEmailLookupCompleted } from '../stores/axoStore';
export const createEmailLookupHandler = (
fastlaneSdk,
@ -34,6 +34,11 @@ export const createEmailLookupHandler = (
console.log( 'Lookup response:', lookup );
// Gary flow
if ( lookup && lookup.customerContextId === '' ) {
setIsEmailLookupCompleted( true );
}
if ( ! lookup || ! lookup.customerContextId ) {
console.warn( 'No customerContextId found in the response' );
return;
@ -50,9 +55,13 @@ export const createEmailLookupHandler = (
const { authenticationState, profileData } = authResponse;
// OTP success/fail/cancel flow
if ( authResponse ) {
setIsEmailLookupCompleted( true );
}
if ( authenticationState === 'succeeded' ) {
snapshotFields( wooShippingAddress, wooBillingAddress );
setIsGuest( false );
if ( profileData && profileData.shippingAddress ) {

View file

@ -37,17 +37,49 @@ export const setupAuthenticationClassToggle = () => {
return unsubscribe;
};
export const setupEmailLookupCompletedClassToggle = () => {
const targetSelector = '.wp-block-woocommerce-checkout-fields-block';
const emailLookupCompletedClass = 'wc-block-axo-email-lookup-completed';
const updateEmailLookupCompletedClass = () => {
const targetElement = document.querySelector( targetSelector );
if ( ! targetElement ) {
console.warn( `Target element not found: ${ targetSelector }` );
return;
}
const isEmailLookupCompleted =
select( STORE_NAME ).getIsEmailLookupCompleted();
if ( isEmailLookupCompleted ) {
targetElement.classList.add( emailLookupCompletedClass );
} else {
targetElement.classList.remove( emailLookupCompletedClass );
}
};
// Initial update
updateEmailLookupCompletedClass();
// Subscribe to state changes
const unsubscribe = subscribe( () => {
updateEmailLookupCompletedClass();
} );
return unsubscribe;
};
/**
* Sets up class toggles for the contact information block based on isAxoActive and isGuest states.
* @return {Function} Unsubscribe function for cleanup.
*/
export const setupContactInfoClassToggles = () => {
const targetSelector =
'.wp-block-woocommerce-checkout-contact-information-block';
export const setupCheckoutBlockClassToggles = () => {
const targetSelector = '.wp-block-woocommerce-checkout-fields-block';
const axoLoadedClass = 'wc-block-axo-is-loaded';
const authClass = 'wc-block-axo-is-authenticated';
const emailLookupCompletedClass = 'wc-block-axo-email-lookup-completed';
const updateContactInfoClasses = () => {
const updateCheckoutBlockClassToggles = () => {
const targetElement = document.querySelector( targetSelector );
if ( ! targetElement ) {
console.warn( `Target element not found: ${ targetSelector }` );
@ -56,6 +88,8 @@ export const setupContactInfoClassToggles = () => {
const isAxoActive = select( STORE_NAME ).getIsAxoActive();
const isGuest = select( STORE_NAME ).getIsGuest();
const isEmailLookupCompleted =
select( STORE_NAME ).getIsEmailLookupCompleted();
if ( isAxoActive ) {
targetElement.classList.add( axoLoadedClass );
@ -68,14 +102,20 @@ export const setupContactInfoClassToggles = () => {
} else {
targetElement.classList.remove( authClass );
}
if ( isEmailLookupCompleted ) {
targetElement.classList.add( emailLookupCompletedClass );
} else {
targetElement.classList.remove( emailLookupCompletedClass );
}
};
// Initial update
updateContactInfoClasses();
updateCheckoutBlockClassToggles();
// Subscribe to state changes
const unsubscribe = subscribe( () => {
updateContactInfoClasses();
updateCheckoutBlockClassToggles();
} );
return unsubscribe;
@ -87,12 +127,17 @@ export const setupContactInfoClassToggles = () => {
*/
export const initializeClassToggles = () => {
const unsubscribeAuth = setupAuthenticationClassToggle();
const unsubscribeContactInfo = setupContactInfoClassToggles();
const unsubscribeEmailLookupCompleted =
setupEmailLookupCompletedClassToggle();
const unsubscribeContactInfo = setupCheckoutBlockClassToggles();
return () => {
if ( unsubscribeAuth ) {
unsubscribeAuth();
}
if ( unsubscribeEmailLookupCompleted ) {
unsubscribeEmailLookupCompleted();
}
if ( unsubscribeContactInfo ) {
unsubscribeContactInfo();
}

View file

@ -1,3 +1,5 @@
import { dispatch } from '@wordpress/data';
export const snapshotFields = ( shippingAddress, billingAddress ) => {
console.log( 'Attempting to snapshot fields' );
if ( ! shippingAddress || ! billingAddress ) {
@ -74,11 +76,16 @@ export const populateWooFields = (
setWooShippingAddress,
setWooBillingAddress
) => {
const CHECKOUT_STORE_KEY = 'wc/store/checkout';
console.log(
'Populating WooCommerce fields with profile data:',
profileData
);
// Disable the 'Use same address for billing' checkbox
dispatch( CHECKOUT_STORE_KEY ).__internalSetUseShippingAsBilling( false );
// Save shipping address
const { address, name, phoneNumber } = profileData.shippingAddress;
@ -113,4 +120,7 @@ export const populateWooFields = (
console.log( 'Setting WooCommerce billing address:', billingAddress );
setWooBillingAddress( billingAddress );
dispatch( CHECKOUT_STORE_KEY ).setEditingShippingAddress( false );
dispatch( CHECKOUT_STORE_KEY ).setEditingBillingAddress( false );
};

View file

@ -1,81 +0,0 @@
import { useEffect, createRoot } from '@wordpress/element';
const ShippingChangeButton = ( { onChangeShippingAddressClick } ) => (
<a
className="wc-block-axo-change-link"
role="button"
onClick={ ( event ) => {
event.preventDefault();
onChangeShippingAddressClick();
} }
>
Choose a different shipping address
</a>
);
const ShippingChangeButtonManager = ( { onChangeShippingAddressClick } ) => {
useEffect( () => {
const shippingHeading = document.querySelector(
'#shipping-fields .wc-block-components-checkout-step__heading'
);
if (
shippingHeading &&
! shippingHeading.querySelector(
'.wc-block-checkout-axo-block-card__edit'
)
) {
const spanElement = document.createElement( 'span' );
spanElement.className = 'wc-block-checkout-axo-block-card__edit';
shippingHeading.appendChild( spanElement );
const root = createRoot( spanElement );
root.render(
<ShippingChangeButton
onChangeShippingAddressClick={
onChangeShippingAddressClick
}
/>
);
return () => {
root.unmount();
spanElement.remove();
};
}
}, [ onChangeShippingAddressClick ] );
return null;
};
export const injectShippingChangeButton = ( onChangeShippingAddressClick ) => {
const existingButton = document.querySelector(
'#shipping-fields .wc-block-checkout-axo-block-card__edit'
);
if ( ! existingButton ) {
const container = document.createElement( 'div' );
document.body.appendChild( container );
createRoot( container ).render(
<ShippingChangeButtonManager
onChangeShippingAddressClick={ onChangeShippingAddressClick }
/>
);
} else {
console.log(
'Shipping change button already exists. Skipping injection.'
);
}
};
export const removeShippingChangeButton = () => {
const span = document.querySelector(
'#shipping-fields .wc-block-checkout-axo-block-card__edit'
);
if ( span ) {
createRoot( span ).unmount();
span.remove();
}
};
export default ShippingChangeButtonManager;

View file

@ -0,0 +1,43 @@
import { useCallback } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
const CHECKOUT_STORE_KEY = 'wc/store/checkout';
export const useAddressEditing = () => {
const { isEditingShippingAddress, isEditingBillingAddress } = useSelect(
( select ) => {
const store = select( CHECKOUT_STORE_KEY );
return {
isEditingShippingAddress: store.getEditingShippingAddress(),
isEditingBillingAddress: store.getEditingBillingAddress(),
};
},
[]
);
const { setEditingShippingAddress, setEditingBillingAddress } =
useDispatch( CHECKOUT_STORE_KEY );
const setShippingAddressEditing = useCallback(
( isEditing ) => {
setEditingShippingAddress( isEditing );
},
[ setEditingShippingAddress ]
);
const setBillingAddressEditing = useCallback(
( isEditing ) => {
setEditingBillingAddress( isEditing );
},
[ setEditingBillingAddress ]
);
return {
isEditingShippingAddress,
isEditingBillingAddress,
setShippingAddressEditing,
setBillingAddressEditing,
};
};
export default useAddressEditing;

View file

@ -0,0 +1,49 @@
import { useEffect } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { STORE_NAME } from '../stores/axoStore';
import { removeShippingChangeButton } from '../components/Shipping';
import { removeCardChangeButton } from '../components/Card';
import { removeWatermark } from '../components/Watermark';
import {
removeEmailFunctionality,
isEmailFunctionalitySetup,
} from '../components/EmailButton';
import { restoreOriginalFields } from '../helpers/fieldHelpers';
import useCustomerData from './useCustomerData';
const useAxoCleanup = () => {
const { setIsAxoActive, setIsGuest } = useDispatch( STORE_NAME );
const {
setShippingAddress: updateWooShippingAddress,
setBillingAddress: updateWooBillingAddress,
} = useCustomerData();
useEffect( () => {
console.log( 'Setting up cleanup for WooCommerce fields' );
return () => {
console.log( 'Cleaning up: Restoring WooCommerce fields' );
restoreOriginalFields(
updateWooShippingAddress,
updateWooBillingAddress
);
};
}, [ updateWooShippingAddress, updateWooBillingAddress ] );
useEffect( () => {
console.log( 'Setting up cleanup for Axo component' );
return () => {
console.log( 'Cleaning up Axo component' );
setIsAxoActive( false );
setIsGuest( true );
removeShippingChangeButton();
removeCardChangeButton();
removeWatermark();
if ( isEmailFunctionalitySetup() ) {
console.log( 'Removing email functionality' );
removeEmailFunctionality();
}
};
}, [] );
};
export default useAxoCleanup;

View file

@ -0,0 +1,81 @@
import { useEffect } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { STORE_NAME } from '../stores/axoStore';
import usePayPalScript from './usePayPalScript';
import { setupWatermark } from '../components/Watermark';
import { setupEmailFunctionality } from '../components/EmailButton';
import { createEmailLookupHandler } from '../events/emailLookupManager';
import { initializeClassToggles } from '../helpers/classnamesManager';
import { snapshotFields } from '../helpers/fieldHelpers';
import useCustomerData from './useCustomerData';
import useShippingAddressChange from './useShippingAddressChange';
const useAxoSetup = (
ppcpConfig,
fastlaneSdk,
onChangeCardButtonClick,
setShippingAddress,
setCard
) => {
const { setIsAxoActive, setIsAxoScriptLoaded } = useDispatch( STORE_NAME );
const paypalLoaded = usePayPalScript( ppcpConfig );
const onChangeShippingAddressClick = useShippingAddressChange(
fastlaneSdk,
setShippingAddress
);
const {
shippingAddress: wooShippingAddress,
billingAddress: wooBillingAddress,
setShippingAddress: setWooShippingAddress,
setBillingAddress: setWooBillingAddress,
} = useCustomerData();
useEffect( () => {
console.log( 'Initializing class toggles' );
initializeClassToggles();
}, [] );
useEffect( () => {
console.log( 'Setting up Axo functionality' );
setupWatermark( fastlaneSdk );
if ( paypalLoaded && fastlaneSdk ) {
console.log(
'PayPal loaded and FastlaneSDK available, setting up email functionality'
);
setIsAxoScriptLoaded( true );
setIsAxoActive( true );
const emailLookupHandler = createEmailLookupHandler(
fastlaneSdk,
setShippingAddress,
setCard,
snapshotFields,
wooShippingAddress,
wooBillingAddress,
setWooShippingAddress,
setWooBillingAddress,
onChangeShippingAddressClick,
onChangeCardButtonClick
);
setupEmailFunctionality( emailLookupHandler );
}
}, [
paypalLoaded,
fastlaneSdk,
setIsAxoActive,
setIsAxoScriptLoaded,
wooShippingAddress,
wooBillingAddress,
setWooShippingAddress,
setWooBillingAddress,
onChangeShippingAddressClick,
onChangeCardButtonClick,
setShippingAddress,
setCard,
] );
return paypalLoaded;
};
export default useAxoSetup;

View file

@ -1,7 +1,11 @@
import { useCallback } from '@wordpress/element';
import useFastlaneSdk from "./useFastlaneSdk";
import { useAddressEditing } from './useAddressEditing';
import useCustomerData from './useCustomerData';
export const useCardChange = ( fastlaneSdk, setCard ) => {
const { setBillingAddressEditing } = useAddressEditing();
const { setBillingAddress: setWooBillingAddress } = useCustomerData();
export const useCardChange = ( fastlaneSdk, setCard, setWooBillingAddress ) => {
return useCallback( async () => {
if ( fastlaneSdk ) {
const { selectionChanged, selectedCard } =
@ -18,7 +22,7 @@ export const useCardChange = ( fastlaneSdk, setCard, setWooBillingAddress ) => {
const firstName = nameParts[ 0 ];
const lastName = nameParts.slice( 1 ).join( ' ' );
setWooBillingAddress( {
const newBillingAddress = {
first_name: firstName,
last_name: lastName,
address_1: billingAddress.addressLine1,
@ -27,12 +31,25 @@ export const useCardChange = ( fastlaneSdk, setCard, setWooBillingAddress ) => {
state: billingAddress.adminArea1,
postcode: billingAddress.postalCode,
country: billingAddress.countryCode,
};
await new Promise( ( resolve ) => {
setWooBillingAddress( newBillingAddress );
resolve();
} );
console.log( 'Billing address updated:', billingAddress );
await new Promise( ( resolve ) => {
setBillingAddressEditing( false );
resolve();
} );
}
}
}, [ fastlaneSdk, setCard ] );
}, [
fastlaneSdk,
setCard,
setWooBillingAddress,
setBillingAddressEditing,
] );
};
export default useCardChange;

View file

@ -1,6 +1,5 @@
import { useCallback, useMemo } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
import useFastlaneSdk from "./useFastlaneSdk";
export const useCustomerData = () => {
const customerData = useSelect( ( select ) =>

View file

@ -0,0 +1,42 @@
import { useCallback } from '@wordpress/element';
const useHandlePaymentSetup = (
emitResponse,
card,
paymentComponent,
tokenizedCustomerData
) => {
return useCallback( async () => {
const isRyanFlow = !! card?.id;
let cardToken = card?.id;
if ( ! cardToken && paymentComponent ) {
cardToken = await paymentComponent
.getPaymentToken( tokenizedCustomerData )
.then( ( response ) => response.id );
}
if ( ! cardToken ) {
return {
type: emitResponse.responseTypes.ERROR,
message: 'Could not process the payment (tokenization error)',
};
}
return {
type: emitResponse.responseTypes.SUCCESS,
meta: {
paymentMethodData: {
fastlane_member: isRyanFlow,
axo_nonce: cardToken,
},
},
};
}, [
card,
paymentComponent,
tokenizedCustomerData,
] );
};
export default useHandlePaymentSetup;

View file

@ -0,0 +1,20 @@
import { useState, useEffect } from '@wordpress/element';
import { loadPaypalScript } from '../../../../ppcp-button/resources/js/modules/Helper/ScriptLoading';
const usePayPalScript = ( ppcpConfig ) => {
const [ isLoaded, setIsLoaded ] = useState( false );
useEffect( () => {
if ( ! isLoaded ) {
console.log( 'Loading PayPal script' );
loadPaypalScript( ppcpConfig, () => {
console.log( 'PayPal script loaded' );
setIsLoaded( true );
} );
}
}, [ ppcpConfig, isLoaded ] );
return isLoaded;
};
export default usePayPalScript;

View file

@ -0,0 +1,27 @@
import { useEffect } from '@wordpress/element';
const usePaymentSetup = ( onPaymentSetup, emitResponse, card ) => {
useEffect( () => {
const unsubscribe = onPaymentSetup( async () => {
return {
type: emitResponse.responseTypes.SUCCESS,
meta: {
paymentMethodData: {
axo_nonce: card?.id,
},
},
};
} );
return () => {
unsubscribe();
};
}, [
emitResponse.responseTypes.ERROR,
emitResponse.responseTypes.SUCCESS,
onPaymentSetup,
card,
] );
};
export default usePaymentSetup;

View file

@ -0,0 +1,24 @@
import { useEffect, useCallback } from '@wordpress/element';
const usePaymentSetupEffect = ( onPaymentSetup, handlePaymentSetup ) => {
/**
* `onPaymentSetup()` fires when we enter the "PROCESSING" state in the checkout flow.
* It pre-processes the payment details and returns data for server-side processing.
*/
useEffect( () => {
const unsubscribe = onPaymentSetup( handlePaymentSetup );
return () => {
unsubscribe();
};
}, [ onPaymentSetup, handlePaymentSetup ] );
const handlePaymentLoad = useCallback( ( component ) => {
// We'll return this function instead of calling setPaymentComponent directly
return component;
}, [] );
return { handlePaymentLoad };
};
export default usePaymentSetupEffect;

View file

@ -1,24 +1,21 @@
import { useCallback } from '@wordpress/element';
import { useAddressEditing } from './useAddressEditing';
import useCustomerData from './useCustomerData';
export const useShippingAddressChange = ( fastlaneSdk, setShippingAddress ) => {
const { setShippingAddressEditing } = useAddressEditing();
const { setShippingAddress: setWooShippingAddress } = useCustomerData();
export const useShippingAddressChange = (
fastlaneSdk,
setShippingAddress,
setWooShippingAddress
) => {
return useCallback( async () => {
if ( fastlaneSdk ) {
const { selectionChanged, selectedAddress } =
await fastlaneSdk.profile.showShippingAddressSelector();
if ( selectionChanged ) {
setShippingAddress( selectedAddress );
console.log(
'Selected shipping address changed:',
selectedAddress
);
const { address, name, phoneNumber } = selectedAddress;
setWooShippingAddress( {
const newShippingAddress = {
first_name: name.firstName,
last_name: name.lastName,
address_1: address.addressLine1,
@ -28,10 +25,25 @@ export const useShippingAddressChange = (
postcode: address.postalCode,
country: address.countryCode,
phone: phoneNumber.nationalNumber,
};
await new Promise( ( resolve ) => {
setWooShippingAddress( newShippingAddress );
resolve();
} );
await new Promise( ( resolve ) => {
setShippingAddressEditing( false );
resolve();
} );
}
}
}, [ fastlaneSdk, setShippingAddress, setWooShippingAddress ] );
}, [
fastlaneSdk,
setShippingAddress,
setWooShippingAddress,
setShippingAddressEditing,
] );
};
export default useShippingAddressChange;

View file

@ -0,0 +1,49 @@
import { useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
export const useTokenizeCustomerData = () => {
const customerData = useSelect( ( select ) =>
select( 'wc/store/cart' ).getCustomerData()
);
const isValidAddress = ( address ) => {
// At least one name must be present.
if ( ! address.first_name && ! address.last_name ) {
return false;
}
// Street, city, postcode, country are mandatory; state is optional.
return (
address.address_1 &&
address.city &&
address.postcode &&
address.country
);
};
// Memoize the customer data to avoid unnecessary re-renders (and potential infinite loops).
return useMemo( () => {
const { billingAddress, shippingAddress } = customerData;
// Prefer billing address, but fallback to shipping address if billing address is not valid.
const mainAddress = isValidAddress( billingAddress )
? billingAddress
: shippingAddress;
return {
cardholderName: {
fullName: `${ mainAddress.first_name } ${ mainAddress.last_name }`,
},
billingAddress: {
addressLine1: mainAddress.address_1,
addressLine2: mainAddress.address_2,
adminArea1: mainAddress.state,
adminArea2: mainAddress.city,
postalCode: mainAddress.postcode,
countryCode: mainAddress.country,
},
};
}, [ customerData ] );
};
export default useTokenizeCustomerData;

View file

@ -1,39 +1,21 @@
import { useCallback, useEffect, useState } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { useState } from '@wordpress/element';
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading';
// Hooks
import useFastlaneSdk from './hooks/useFastlaneSdk';
import useCustomerData from './hooks/useCustomerData';
import useShippingAddressChange from './hooks/useShippingAddressChange';
import useTokenizeCustomerData from './hooks/useTokenizeCustomerData';
import useCardChange from './hooks/useCardChange';
import useAxoSetup from './hooks/useAxoSetup';
import usePaymentSetup from './hooks/usePaymentSetup';
import useAxoCleanup from './hooks/useAxoCleanup';
import useHandlePaymentSetup from './hooks/useHandlePaymentSetup';
// Components
import { Payment } from './components/Payment';
import { Payment } from './components/Payment/Payment';
import usePaymentSetupEffect from './hooks/usePaymentSetupEffect';
// Helpers
import { snapshotFields, restoreOriginalFields } from './helpers/fieldHelpers';
import { removeWatermark, setupWatermark } from './components/watermark';
import { removeCardChangeButton } from './helpers/cardChangeButtonManager';
import { removeShippingChangeButton } from './components/shipping';
import { initializeClassToggles } from './helpers/classnamesManager';
// Stores
import { STORE_NAME } from './stores/axoStore';
// Event handlers
import { createEmailLookupHandler } from './events/emailLookupManager';
import { usePhoneSyncHandler } from './events/phoneSyncManager';
import {
setupEmailFunctionality,
removeEmailFunctionality,
isEmailFunctionalitySetup,
} from './components/email-button';
const ppcpConfig = wc.wcSettings.getSetting( 'ppcp-credit-card-gateway_data' );
const gatewayHandle = 'ppcp-axo-gateway';
const ppcpConfig = wc.wcSettings.getSetting( `${ gatewayHandle }_data` );
if ( typeof window.PayPalCommerceGateway === 'undefined' ) {
window.PayPalCommerceGateway = ppcpConfig;
@ -44,151 +26,37 @@ const axoConfig = window.wc_ppcp_axo;
const Axo = ( props ) => {
const { eventRegistration, emitResponse } = props;
const { onPaymentSetup } = eventRegistration;
const [ paypalLoaded, setPaypalLoaded ] = useState( false );
const [ shippingAddress, setShippingAddress ] = useState( null );
const [ card, setCard ] = useState( null );
const [ wooPhone, setWooPhone ] = useState( '' );
const [ paymentComponent, setPaymentComponent ] = useState( null );
const fastlaneSdk = useFastlaneSdk( axoConfig, ppcpConfig );
console.log( 'Axo component rendering' );
useEffect( () => {
const unsubscribe = onPaymentSetup( async () => {
// Validate payment options and emit response.
// Note: This response supports the Ryan flow (payment via saved card-token)
return {
type: emitResponse.responseTypes.SUCCESS,
meta: {
paymentMethodData: {
axo_nonce: card?.id,
},
},
};
} );
// Unsubscribes when this component is unmounted.
return () => {
unsubscribe();
};
}, [
emitResponse.responseTypes.ERROR,
emitResponse.responseTypes.SUCCESS,
onPaymentSetup,
const tokenizedCustomerData = useTokenizeCustomerData();
const onChangeCardButtonClick = useCardChange( fastlaneSdk, setCard );
const handlePaymentSetup = useHandlePaymentSetup(
emitResponse,
card,
] );
const { setIsAxoActive, setIsGuest, setIsAxoScriptLoaded } =
useDispatch( STORE_NAME );
const {
shippingAddress: wooShippingAddress,
billingAddress: wooBillingAddress,
setShippingAddress: updateWooShippingAddress,
setBillingAddress: updateWooBillingAddress,
} = useCustomerData();
useEffect( () => {
console.log( 'Initializing class toggles' );
initializeClassToggles();
}, [] );
useEffect( () => {
console.log( 'Setting up cleanup for WooCommerce fields' );
return () => {
console.log( 'Cleaning up: Restoring WooCommerce fields' );
restoreOriginalFields(
updateWooShippingAddress,
updateWooBillingAddress
);
};
}, [ updateWooShippingAddress, updateWooBillingAddress ] );
useEffect( () => {
if ( ! paypalLoaded ) {
console.log( 'Loading PayPal script' );
loadPaypalScript( ppcpConfig, () => {
console.log( 'PayPal script loaded' );
setPaypalLoaded( true );
} );
}
}, [ paypalLoaded, ppcpConfig ] );
const {
setShippingAddress: setWooShippingAddress,
setBillingAddress: setWooBillingAddress,
} = useCustomerData();
const onChangeShippingAddressClick = useShippingAddressChange(
fastlaneSdk,
setShippingAddress,
updateWooShippingAddress
);
const onChangeCardButtonClick = useCardChange(
fastlaneSdk,
setCard,
updateWooBillingAddress
paymentComponent,
tokenizedCustomerData
);
usePhoneSyncHandler( setWooPhone );
useEffect( () => {
console.log( 'Setting up Axo functionality' );
setupWatermark( fastlaneSdk );
if ( paypalLoaded && fastlaneSdk ) {
console.log(
'PayPal loaded and FastlaneSDK available, setting up email functionality'
);
setIsAxoScriptLoaded( true );
setIsAxoActive( true );
const emailLookupHandler = createEmailLookupHandler(
fastlaneSdk,
setShippingAddress,
setCard,
snapshotFields,
wooShippingAddress,
wooBillingAddress,
setWooShippingAddress,
setWooBillingAddress,
onChangeShippingAddressClick,
onChangeCardButtonClick
);
setupEmailFunctionality( emailLookupHandler );
}
}, [
paypalLoaded,
useAxoSetup(
ppcpConfig,
fastlaneSdk,
setIsAxoActive,
setIsAxoScriptLoaded,
wooShippingAddress,
wooBillingAddress,
setWooShippingAddress,
setWooBillingAddress,
onChangeShippingAddressClick,
onChangeCardButtonClick,
] );
setShippingAddress,
setCard
);
usePaymentSetup( onPaymentSetup, emitResponse, card );
useEffect( () => {
console.log( 'Setting up cleanup for Axo component' );
return () => {
console.log( 'Cleaning up Axo component' );
setIsAxoActive( false );
setIsGuest( true );
removeShippingChangeButton();
removeCardChangeButton();
removeWatermark();
if ( isEmailFunctionalitySetup() ) {
console.log( 'Removing email functionality' );
removeEmailFunctionality();
}
};
}, [] );
const { handlePaymentLoad } = usePaymentSetupEffect(
onPaymentSetup,
handlePaymentSetup
);
const handlePaymentLoad = useCallback( ( paymentComponent ) => {
console.log( 'Payment component loaded', paymentComponent );
}, [] );
useAxoCleanup();
const handleChange = ( selectedCard ) => {
const handleCardChange = ( selectedCard ) => {
console.log( 'Card selection changed', selectedCard );
setCard( selectedCard );
};
@ -203,7 +71,7 @@ const Axo = ( props ) => {
<Payment
fastlaneSdk={ fastlaneSdk }
card={ card }
onChange={ handleChange }
onChange={ handleCardChange }
onPaymentLoad={ handlePaymentLoad }
onChangeButtonClick={ onChangeCardButtonClick }
/>
@ -212,7 +80,6 @@ const Axo = ( props ) => {
);
};
// Register the payment method
registerPaymentMethod( {
name: ppcpConfig.id,
label: (
@ -224,15 +91,11 @@ registerPaymentMethod( {
content: <Axo />,
edit: <h1>This is Axo Blocks in the editor</h1>,
ariaLabel: ppcpConfig.title,
canMakePayment: () => {
return true;
},
canMakePayment: () => true,
supports: {
showSavedCards: true,
features: ppcpConfig.supports,
},
} );
console.log( 'Axo module loaded' );
export default Axo;

View file

@ -10,6 +10,7 @@ const DEFAULT_STATE = {
isAxoActive: false,
isAxoScriptLoaded: false,
isEmailSubmitted: false,
isEmailLookupCompleted: false,
};
// Actions
@ -30,6 +31,10 @@ const actions = {
type: 'SET_IS_EMAIL_SUBMITTED',
payload: isEmailSubmitted,
} ),
setIsEmailLookupCompleted: ( isEmailLookupCompleted ) => ( {
type: 'SET_IS_EMAIL_LOOKUP_COMPLETED',
payload: isEmailLookupCompleted,
} ),
};
// Reducer
@ -43,6 +48,8 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
return { ...state, isAxoScriptLoaded: action.payload };
case 'SET_IS_EMAIL_SUBMITTED':
return { ...state, isEmailSubmitted: action.payload };
case 'SET_IS_EMAIL_LOOKUP_COMPLETED':
return { ...state, isEmailLookupCompleted: action.payload };
default:
return state;
}
@ -52,8 +59,9 @@ const reducer = ( state = DEFAULT_STATE, action ) => {
const selectors = {
getIsGuest: ( state ) => state.isGuest,
getIsAxoActive: ( state ) => state.isAxoActive,
isAxoScriptLoaded: ( state ) => state.isAxoScriptLoaded,
isEmailSubmitted: ( state ) => state.isEmailSubmitted,
getIsAxoScriptLoaded: ( state ) => state.isAxoScriptLoaded,
getIsEmailSubmitted: ( state ) => state.isEmailSubmitted,
getIsEmailLookupCompleted: ( state ) => state.isEmailLookupCompleted,
};
// Create and register the store
@ -65,7 +73,6 @@ const store = createReduxStore( STORE_NAME, {
register( store );
// Utility functions
export const setIsGuest = ( isGuest ) => {
try {
dispatch( STORE_NAME ).setIsGuest( isGuest );
@ -73,3 +80,13 @@ export const setIsGuest = ( isGuest ) => {
console.error( 'Error updating isGuest state:', error );
}
};
export const setIsEmailLookupCompleted = ( isEmailLookupCompleted ) => {
try {
dispatch( STORE_NAME ).setIsEmailLookupCompleted(
isEmailLookupCompleted
);
} catch ( error ) {
console.error( 'Error updating isEmailLookupCompleted state:', error );
}
};

View file

@ -5,7 +5,7 @@
* @package WooCommerce\PayPalCommerce\Axo
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AxoBlock;
@ -14,10 +14,10 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
// If AXO Block is configured and onboarded.
'axoblock.available' => static function ( ContainerInterface $container ): bool {
'axoblock.available' => static function ( ContainerInterface $container ) : bool {
return true;
},
'axoblock.url' => static function ( ContainerInterface $container ): string {
'axoblock.url' => static function ( ContainerInterface $container ) : string {
/**
* The path cannot be false.
*
@ -28,14 +28,12 @@ return array(
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'axoblock.method' => static function( ContainerInterface $container ): AxoBlockPaymentMethod {
'axoblock.method' => static function ( ContainerInterface $container ) : AxoBlockPaymentMethod {
return new AxoBlockPaymentMethod(
$container->get( 'axoblock.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'wcgateway.credit-card-gateway' ),
function () use ( $container ): SmartButtonInterface {
return $container->get( 'button.smart-button' );
},
$container->get( 'axo.gateway' ),
fn() : SmartButtonInterface => $container->get( 'button.smart-button' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'onboarding.environment' ),
$container->get( 'wcgateway.url' )

View file

@ -5,15 +5,16 @@
* @package WooCommerce\PayPalCommerce\AxoBlock
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AxoBlock;
use WC_Payment_Gateway;
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
use WooCommerce\PayPalCommerce\Axo\FrontendLoggerEndpoint;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
@ -38,7 +39,7 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
/**
* Credit card gateway.
*
* @var CreditCardGateway
* @var WC_Payment_Gateway
*/
private $gateway;
@ -73,29 +74,30 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
/**
* AdvancedCardPaymentMethod constructor.
*
* @param string $module_url The URL of this module.
* @param string $version The assets version.
* @param CreditCardGateway $gateway Credit card gateway.
* @param SmartButtonInterface|callable $smart_button The smart button script loading handler.
* @param Settings $settings The settings.
* @param Environment $environment The environment object.
* @param string $wcgateway_module_url The WcGateway module URL.
* @param string $module_url The URL of this module.
* @param string $version The assets version.
* @param WC_Payment_Gateway $gateway Credit card gateway.
* @param SmartButtonInterface|callable $smart_button The smart button script loading
* handler.
* @param Settings $settings The settings.
* @param Environment $environment The environment object.
* @param string $wcgateway_module_url The WcGateway module URL.
*/
public function __construct(
string $module_url,
string $version,
CreditCardGateway $gateway,
WC_Payment_Gateway $gateway,
$smart_button,
Settings $settings,
Environment $environment,
string $wcgateway_module_url
) {
$this->name = CreditCardGateway::ID;
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->smart_button = $smart_button;
$this->settings = $settings;
$this->name = AxoGateway::ID;
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->smart_button = $smart_button;
$this->settings = $settings;
$this->environment = $environment;
$this->wcgateway_module_url = $wcgateway_module_url;
@ -110,29 +112,29 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
/**
* {@inheritDoc}
*/
public function is_active() {
return true;
public function is_active() : bool {
return $this->gateway->is_available();
}
/**
* {@inheritDoc}
*/
public function get_payment_method_script_handles() {
public function get_payment_method_script_handles() : array {
$script_path = 'assets/js/index.js';
$script_asset_path = trailingslashit( $this->module_url ) . 'assets/js/index.asset.php';
$script_asset = file_exists( $script_asset_path )
? require( $script_asset_path )
? require $script_asset_path
: array(
'dependencies' => array(),
'version' => '1.0.0'
'version' => '1.0.0',
);
$script_url = trailingslashit( $this->module_url ) . $script_path;
wp_register_script(
'ppcp-axo-block',
$script_url,
$script_asset[ 'dependencies' ],
$script_asset[ 'version' ],
$script_asset['dependencies'],
$script_asset['version'],
true
);
@ -142,19 +144,25 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
$this->script_data()
);
return [ 'ppcp-axo-block' ];
return array( 'ppcp-axo-block' );
}
/**
* {@inheritDoc}
*/
public function get_payment_method_data() {
return [
'id' => $this->name,
return array(
'id' => $this->name,
'title' => 'Debit & Credit Cards',
'description' => 'Axo Description',
'supports' => array_filter( $this->gateway->supports, [ $this->gateway, 'supports' ] ),
];
'supports' => array_filter(
$this->gateway->supports,
array(
$this->gateway,
'supports',
)
),
);
}
/**
@ -162,8 +170,11 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
*
* @return array
*/
private function script_data(): array
{ if(is_admin()) { return array(); }
private function script_data() : array {
if ( is_admin() ) {
return array();
}
return array(
'environment' => array(
'is_sandbox' => $this->environment->current_environment() === 'sandbox',
@ -175,14 +186,14 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ),
'session_id' =>
(WC()->session && method_exists(WC()->session, 'get_customer_unique_id'))
? substr(md5(WC()->session->get_customer_unique_id()), 0, 16)
( WC()->session && method_exists( WC()->session, 'get_customer_unique_id' ) )
? substr( md5( WC()->session->get_customer_unique_id() ), 0, 16 )
: '',
'amount' => array(
'amount' => array(
'currency_code' => get_woocommerce_currency(),
'value' => (WC()->cart && method_exists(WC()->cart, 'get_total'))
? WC()->cart->get_total('numeric')
: null, // Set to null if WC()->cart is null or get_total doesn't exist
'value' => ( WC()->cart && method_exists( WC()->cart, 'get_total' ) )
? WC()->cart->get_total( 'numeric' )
: null, // Set to null if WC()->cart is null or get_total doesn't exist.
),
),
'style_options' => array(
@ -224,5 +235,4 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
'billing_email_button_text' => __( 'Continue', 'woocommerce-paypal-payments' ),
);
}
}

View file

@ -75,6 +75,7 @@ return array(
$container->get( 'wcgateway.settings.render' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.url' ),
$container->get( 'session.handler' ),
$container->get( 'wcgateway.order-processor' ),
$container->get( 'axo.card_icons' ),
$container->get( 'axo.card_icons.axo' ),

View file

@ -28,6 +28,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WC_Payment_Gateways;
/**
* Class AxoModule
*/
@ -130,6 +132,23 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
}
);
// Enforce Fastlane to always be the first payment method in the list.
add_action(
'wc_payment_gateways_initialized',
function ( WC_Payment_Gateways $gateways ) {
if ( is_admin() ) {
return;
}
foreach ( $gateways->payment_gateways as $key => $gateway ) {
if ( $gateway->id === AxoGateway::ID ) {
unset( $gateways->payment_gateways[ $key ] );
array_unshift( $gateways->payment_gateways, $gateway );
break;
}
}
}
);
// Force 'cart-block' and 'cart' Smart Button locations in the settings.
add_action(
'admin_init',

View file

@ -5,20 +5,20 @@
* @package WooCommerce\PayPalCommerce\WcGateway\Gateway
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Axo\Gateway;
use Psr\Log\LoggerInterface;
use Exception;
use WC_Order;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewaySettingsRendererTrait;
@ -26,12 +26,15 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\TransactionUrlProvider;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\ProcessPaymentTrait;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
/**
* Class AXOGateway.
*/
class AxoGateway extends WC_Payment_Gateway {
use OrderMetaTrait, GatewaySettingsRendererTrait;
use OrderMetaTrait, GatewaySettingsRendererTrait, ProcessPaymentTrait;
const ID = 'ppcp-axo-gateway';
@ -119,26 +122,35 @@ class AxoGateway extends WC_Payment_Gateway {
*/
protected $logger;
/**
* The Session Handler.
*
* @var SessionHandler
*/
protected $session_handler;
/**
* AXOGateway constructor.
*
* @param SettingsRenderer $settings_renderer The settings renderer.
* @param ContainerInterface $ppcp_settings The settings.
* @param string $wcgateway_module_url The WcGateway module URL.
* @param OrderProcessor $order_processor The Order processor.
* @param array $card_icons The card icons.
* @param array $card_icons_axo The card icons.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory.
* @param ShippingPreferenceFactory $shipping_preference_factory The shipping preference factory.
* @param TransactionUrlProvider $transaction_url_provider The transaction url provider.
* @param Environment $environment The environment.
* @param LoggerInterface $logger The logger.
* @param SettingsRenderer $settings_renderer The settings renderer.
* @param ContainerInterface $ppcp_settings The settings.
* @param string $wcgateway_module_url The WcGateway module URL.
* @param SessionHandler $session_handler The session handler.
* @param OrderProcessor $order_processor The Order processor.
* @param array $card_icons The card icons.
* @param array $card_icons_axo The card icons.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory.
* @param ShippingPreferenceFactory $shipping_preference_factory Shipping preference factory.
* @param TransactionUrlProvider $transaction_url_provider The transaction url provider.
* @param Environment $environment The environment.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
SettingsRenderer $settings_renderer,
ContainerInterface $ppcp_settings,
string $wcgateway_module_url,
SessionHandler $session_handler,
OrderProcessor $order_processor,
array $card_icons,
array $card_icons_axo,
@ -154,6 +166,7 @@ class AxoGateway extends WC_Payment_Gateway {
$this->settings_renderer = $settings_renderer;
$this->ppcp_settings = $ppcp_settings;
$this->wcgateway_module_url = $wcgateway_module_url;
$this->session_handler = $session_handler;
$this->order_processor = $order_processor;
$this->card_icons = $card_icons;
$this->card_icons_axo = $card_icons_axo;
@ -213,80 +226,83 @@ class AxoGateway extends WC_Payment_Gateway {
* Processes the order.
*
* @param int $order_id The WC order ID.
*
* @return array
*/
public function process_payment( $order_id ) {
$wc_order = wc_get_order( $order_id );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$fastlane_member = wc_clean( wp_unslash( $_POST['fastlane_member'] ?? '' ) );
if ( $fastlane_member ) {
$payment_method_title = __( 'Debit & Credit Cards (via Fastlane by PayPal)', 'woocommerce-paypal-payments' );
$wc_order->set_payment_method_title( $payment_method_title );
$wc_order->save();
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return $this->handle_payment_failure(
null,
new GatewayGenericException( new Exception( 'WC order was not found.' ) )
);
}
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$nonce = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) );
try {
$shipping_preference = $this->shipping_preference_factory->from_state(
$purchase_unit,
'checkout'
);
$payment_source_properties = new \stdClass();
$payment_source_properties->single_use_token = $nonce;
$payment_source = new PaymentSource(
'card',
$payment_source_properties
);
$order = $this->order_endpoint->create(
array( $purchase_unit ),
$shipping_preference,
null,
null,
'',
ApplicationContext::USER_ACTION_CONTINUE,
'',
array(),
$payment_source
);
$this->order_processor->process_captured_and_authorized( $wc_order, $order );
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$fastlane_member = wc_clean( wp_unslash( $_POST['fastlane_member'] ?? '' ) );
if ( $fastlane_member ) {
$payment_method_title = __( 'Debit & Credit Cards (via Fastlane by PayPal)', 'woocommerce-paypal-payments' );
$wc_order->set_payment_method_title( $payment_method_title );
$wc_order->save();
}
$this->logger->error( $error );
wc_add_notice( $error, 'error' );
// The `axo_nonce` is not a WP nonce, but a card-token generated by the JS SDK.
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$token = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) );
$wc_order->update_status(
'failed',
$error
);
$order = $this->create_paypal_order( $wc_order, $token );
return array(
'result' => 'failure',
'redirect' => wc_get_checkout_url(),
);
$this->order_processor->process_captured_and_authorized( $wc_order, $order );
} catch ( Exception $exception ) {
return $this->handle_payment_failure( $wc_order, $exception );
}
WC()->cart->empty_cart();
$result = array(
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $wc_order ),
);
}
return $result;
/**
* Create a new PayPal order from the existing WC_Order instance.
*
* @param WC_Order $wc_order The WooCommerce order to use as a base.
* @param string $payment_token The payment token, generated by the JS SDK.
*
* @return Order The PayPal order.
*/
protected function create_paypal_order( WC_Order $wc_order, string $payment_token ) : Order {
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
$shipping_preference = $this->shipping_preference_factory->from_state(
$purchase_unit,
'checkout'
);
$payment_source_properties = (object) array(
'single_use_token' => $payment_token,
);
$payment_source = new PaymentSource(
'card',
$payment_source_properties
);
return $this->order_endpoint->create(
array( $purchase_unit ),
$shipping_preference,
null,
null,
'',
ApplicationContext::USER_ACTION_CONTINUE,
'',
array(),
$payment_source
);
}
/**
@ -328,7 +344,7 @@ class AxoGateway extends WC_Payment_Gateway {
*
* @return string
*/
public function get_transaction_url( $order ): string {
public function get_transaction_url( $order ) : string {
$this->view_transaction_url = $this->transaction_url_provider->get_transaction_url_base( $order );
return parent::get_transaction_url( $order );
@ -362,7 +378,7 @@ class AxoGateway extends WC_Payment_Gateway {
*
* @return SettingsRenderer
*/
protected function settings_renderer(): SettingsRenderer {
protected function settings_renderer() : SettingsRenderer {
return $this->settings_renderer;
}
}

View file

@ -13,6 +13,7 @@ use Exception;
use Throwable;
use WC_Order;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
/**
* Trait ProcessPaymentTrait
@ -74,8 +75,13 @@ trait ProcessPaymentTrait {
* @param Throwable $exception The exception to format.
* @return string
*/
protected function format_exception( Throwable $exception ): string {
$output = $exception->getMessage() . ' ' . basename( $exception->getFile() ) . ':' . $exception->getLine();
protected function format_exception( Throwable $exception ) : string {
$message = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$message = $exception->get_details( $message );
}
$output = $message . ' ' . basename( $exception->getFile() ) . ':' . $exception->getLine();
$prev = $exception->getPrevious();
if ( ! $prev ) {
return $output;