Merge branch 'trunk' into PCP-3535-fix-editor-paypal-buttons-grid

This commit is contained in:
Alex P. 2024-10-01 10:44:09 +03:00
commit cacd9e5598
No known key found for this signature in database
GPG key ID: 54487A734A204D71
76 changed files with 45917 additions and 376 deletions

View file

@ -1,6 +1,10 @@
*** Changelog ***
= 2.9.1 - xxxx-xx-xx =
= 2.9.2 - xxxx-xx-xx =
* Enhancement - Add Fastlane support for Classic Checkout
* Fix - Fatal error when Pay Later messaging configurator was disabled with a code snippet
= 2.9.1 - 2024-09-24 =
* Fix - Improve card fields hiding #2574
* Fix - Google Pay: Shipping callback not calculating totals correctly on Single Product page #2513
* Fix - Fix shipping callback condition in status report #2578
@ -15,6 +19,8 @@
* Enhancement - Require PHP 7.4+, WP 6.3+, WC 6.9+ #2556
* Enhancement - Modularity module migration #1944
* Enhancement - Keep only 5 tags in readme.txt #2562
* Enhancement - Select ACDC by default during onboarding for China store locations #2619
* Enhancement - Add title, description and gatewayId to the express payment method #2566
= 2.9.0 - 2024-09-02 =
* Fix - Fatal error in Block Editor when using WooCommerce blocks #2534

View file

@ -84,9 +84,10 @@ return function ( string $root_dir ): iterable {
if ( apply_filters(
'woocommerce.feature-flags.woocommerce_paypal_payments.axo_enabled',
getenv( 'PCP_AXO_ENABLED' ) === '1'
getenv( 'PCP_AXO_ENABLED' ) !== '0'
) ) {
$modules[] = ( require "$modules_dir/ppcp-axo/module.php" )();
$modules[] = ( require "$modules_dir/ppcp-axo-block/module.php" )();
}
return $modules;

View file

@ -0,0 +1,14 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3.25.0"
}
],
[
"@babel/preset-react"
]
]
}

View file

@ -0,0 +1,17 @@
{
"name": "woocommerce/ppcp-axo-block",
"type": "dhii-mod",
"description": "Axo Block module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {
"psr-4": {
"WooCommerce\\PayPalCommerce\\AxoBlock\\": "src"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View file

@ -0,0 +1,12 @@
<?php
/**
* The Axo Block module extensions.
*
* @package WooCommerce\PayPalCommerce\AxoBlock
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AxoBlock;
return array();

View file

@ -0,0 +1,14 @@
<?php
/**
* The Axo Block module.
*
* @package WooCommerce\PayPalCommerce\AxoBlock
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AxoBlock;
return static function () : AxoBlockModule {
return new AxoBlockModule();
};

7142
modules/ppcp-axo-block/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
{
"name": "ppcp-axo-block",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"browserslist": [
"> 0.5%",
"Safari >= 8",
"Chrome >= 41",
"Firefox >= 43",
"Edge >= 14"
],
"dependencies": {
"@paypal/paypal-js": "^8.1.1",
"@paypal/react-paypal-js": "^8.5.0",
"core-js": "^3.25.0",
"react": "^17.0.0",
"react-dom": "^17.0.0"
},
"devDependencies": {
"@babel/core": "^7.19",
"@babel/preset-env": "^7.19",
"@babel/preset-react": "^7.18.6",
"@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"webpack": "^5.76",
"webpack-cli": "^4.10"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
"dev": "cross-env BABEL_ENV=default webpack --watch"
},
"resolve": {
"alias": {
"@woocommerce/base-context": "assets/js/base/context"
}
}
}

View file

@ -0,0 +1,303 @@
// 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 {
@include flex-center;
width: 100%;
margin-bottom: 2em;
&__inner {
display: flex;
flex-direction: column;
align-items: center;
max-width: 300px;
width: 100%;
}
&__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 $transition-duration ease-in,
scale $transition-duration ease-in,
display $transition-duration ease-in;
transition-behavior: allow-discrete;
@starting-style {
opacity: 0;
scale: 1.1;
}
&.wc-block-axo-is-authenticated {
opacity: 0;
scale: 0.9;
display: none !important;
transition-duration: $fast-transition-duration;
transition-timing-function: var(--ease-out-5);
}
}
// 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,
.wp-block-woocommerce-checkout-terms-block,
.wp-block-woocommerce-checkout-actions-block {
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"
"error";
grid-template-columns: 1fr;
gap: 6px;
align-items: start;
input[type="email"] {
grid-area: input;
width: 100%;
}
}
#email {
align-self: stretch;
}
// 4.5 Email Submit Button
.wc-block-axo-email-submit-button-container {
grid-area: button;
align-self: stretch;
.wc-block-components-button {
white-space: nowrap;
width: 100%;
}
}
// 4.6 Watermark Container
.wc-block-checkout-axo-block-watermark-container {
grid-area: watermark;
justify-self: end;
grid-column: 1;
}
&:not(.wc-block-axo-is-authenticated) .wc-block-checkout-axo-block-watermark-container {
margin-top: 0;
}
// 4.7 Validation Error
.wc-block-components-address-form__email .wc-block-components-validation-error {
grid-area: error;
width: 100%;
margin-top: 4px;
grid-row: 3;
@media (min-width: 783px) {
grid-row: 2;
}
}
@media (min-width: 783px) {
.wp-block-woocommerce-checkout-contact-information-block .wc-block-components-text-input {
grid-template-areas:
"input button"
"watermark watermark"
"error error";
grid-template-columns: 1fr auto;
gap: 6px 8px;
}
#email {
align-self: center;
}
.wc-block-axo-email-submit-button-container {
align-self: center;
.wc-block-components-button {
width: auto;
}
}
}
// 4.8 Counter fix
.wc-block-checkout__form {
counter-reset: visible-step;
.wc-block-components-checkout-step--with-step-number {
counter-increment: visible-step;
.wc-block-components-checkout-step__title:before {
content: counter(visible-step) ". ";
}
}
}
}
// 5. Shipping/Card Change Link
a.wc-block-axo-change-link {
color: var(--wp--preset--color--secondary);
text-decoration: underline;
&:hover {
text-decoration: none;
}
&:focus {
text-decoration: underline dashed;
}
&:active {
color: var(--wp--preset--color--secondary);
text-decoration: none;
}
}
// 6. Watermark Container
.wc-block-checkout-axo-block-watermark-container {
height: 25px;
margin-top: 5px;
margin-left: 5px;
}
// 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;
.wc-block-components-spinner {
box-sizing: content-box;
color: inherit;
font-size: 1em;
height: auto;
width: auto;
position: relative;
margin-top: 12px;
}
}
}
// 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;
opacity: 0;
}
}
// 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 {
transition: opacity 0.5s ease-in-out;
@starting-style {
opacity: 0;
scale: 1.1;
}
}
// 10. Shipping Fields
#shipping-fields .wc-block-components-checkout-step__heading {
display: flex;
}

View file

@ -0,0 +1,72 @@
import { useMemo } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { Watermark } from '../Watermark';
import { STORE_NAME } from '../../stores/axoStore';
const cardIcons = {
VISA: 'visa-light.svg',
MASTER_CARD: 'mastercard-light.svg',
AMEX: 'amex-light.svg',
DISCOVER: 'discover-light.svg',
DINERS: 'dinersclub-light.svg',
JCB: 'jcb-light.svg',
UNIONPAY: 'unionpay-light.svg',
};
const Card = ( { fastlaneSdk, showWatermark = true } ) => {
const { card } = useSelect(
( select ) => ( {
card: select( STORE_NAME ).getCardDetails(),
} ),
[]
);
const { brand, lastDigits, expiry, name } = card?.paymentSource?.card ?? {};
const cardLogo = useMemo( () => {
return cardIcons[ brand ] ? (
<img
className="wc-block-axo-block-card__meta-icon"
title={ brand }
src={ `${ window.wc_ppcp_axo.icons_directory }${ cardIcons[ brand ] }` }
alt={ brand }
/>
) : (
<span>{ brand }</span>
);
}, [ brand ] );
const formattedExpiry = expiry
? `${ expiry.split( '-' )[ 1 ] }/${ expiry.split( '-' )[ 0 ] }`
: '';
return (
<div className="wc-block-checkout-axo-block-card">
<div className="wc-block-checkout-axo-block-card__inner">
<div className="wc-block-checkout-axo-block-card__content">
<div className="wc-block-checkout-axo-block-card__meta">
<span className="wc-block-checkout-axo-block-card__meta-digits">
{ `**** **** **** ${ lastDigits }` }
</span>
{ cardLogo }
</div>
<div className="wc-block-checkout-axo-block-card__meta">
<span>{ name }</span>
<span>{ formattedExpiry }</span>{ ' ' }
</div>
</div>
<div className="wc-block-checkout-axo-block-card__watermark">
{ showWatermark && (
<Watermark
fastlaneSdk={ fastlaneSdk }
name="wc-block-checkout-axo-card-watermark"
includeAdditionalInfo={ false }
/>
) }
</div>
</div>
</div>
);
};
export default Card;

View file

@ -0,0 +1,19 @@
import { createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
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', 'woocommerce-paypal-payments' )
);
export default CardChangeButton;

View file

@ -0,0 +1,39 @@
import { createElement, createRoot, useEffect } from '@wordpress/element';
import CardChangeButton from './CardChangeButton';
const CardChangeButtonManager = ( { onChangeButtonClick } ) => {
useEffect( () => {
const radioLabelElement = document.getElementById(
'ppcp-axo-block-radio-label'
);
if ( radioLabelElement ) {
if (
! radioLabelElement.querySelector(
'.wc-block-checkout-axo-block-card__edit'
)
) {
const buttonContainer = document.createElement( 'div' );
radioLabelElement.appendChild( buttonContainer );
const root = createRoot( buttonContainer );
root.render(
createElement( CardChangeButton, { onChangeButtonClick } )
);
}
}
return () => {
const button = document.querySelector(
'.wc-block-checkout-axo-block-card__edit'
);
if ( button && button.parentNode ) {
button.parentNode.remove();
}
};
}, [ onChangeButtonClick ] );
return null;
};
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

@ -0,0 +1,51 @@
import { STORE_NAME } from '../../stores/axoStore';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
const EmailButton = ( { handleSubmit } ) => {
const { isGuest, isAxoActive, isEmailSubmitted } = useSelect(
( select ) => ( {
isGuest: select( STORE_NAME ).getIsGuest(),
isAxoActive: select( STORE_NAME ).getIsAxoActive(),
isEmailSubmitted: select( STORE_NAME ).getIsEmailSubmitted(),
} )
);
if ( ! isGuest || ! isAxoActive ) {
return null;
}
return (
<button
type="button"
onClick={ handleSubmit }
className={ `wc-block-components-button wp-element-button ${
isEmailSubmitted ? 'is-loading' : ''
}` }
disabled={ isEmailSubmitted }
>
<span
className="wc-block-components-button__text"
style={ {
visibility: isEmailSubmitted ? 'hidden' : 'visible',
} }
>
{ __( 'Continue', 'woocommerce-paypal-payments' ) }
</span>
{ isEmailSubmitted && (
<span
className="wc-block-components-spinner"
aria-hidden="true"
style={ {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
} }
/>
) }
</button>
);
};
export default EmailButton;

View file

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

View file

@ -0,0 +1,125 @@
import { createElement, createRoot } from '@wordpress/element';
import { log } from '../../../../../ppcp-axo/resources/js/Helper/Debug';
import { STORE_NAME } from '../../stores/axoStore';
import EmailButton from './EmailButton';
let emailInput = null;
let submitButtonReference = {
container: null,
root: null,
unsubscribe: null,
};
let keydownHandler = null;
const getEmailInput = () => {
if ( ! emailInput ) {
emailInput = document.getElementById( 'email' );
}
return emailInput;
};
export const setupEmailFunctionality = ( onEmailSubmit ) => {
const input = getEmailInput();
if ( ! input ) {
log(
'Email input element not found. Functionality not added.',
'warn'
);
return;
}
const handleEmailSubmit = async () => {
const isEmailSubmitted = wp.data
.select( STORE_NAME )
.getIsEmailSubmitted();
if ( isEmailSubmitted || ! input.value ) {
return;
}
wp.data.dispatch( STORE_NAME ).setIsEmailSubmitted( true );
renderButton();
try {
await onEmailSubmit( input.value );
} catch ( error ) {
log( `Error during email submission: ${ error }`, 'error' );
} finally {
wp.data.dispatch( STORE_NAME ).setIsEmailSubmitted( false );
renderButton();
}
};
keydownHandler = ( event ) => {
const isAxoActive = wp.data.select( STORE_NAME ).getIsAxoActive();
if ( event.key === 'Enter' && isAxoActive ) {
event.preventDefault();
handleEmailSubmit();
}
};
input.addEventListener( 'keydown', keydownHandler );
// Set up submit button
if ( ! submitButtonReference.container ) {
submitButtonReference.container = document.createElement( 'div' );
submitButtonReference.container.setAttribute(
'class',
'wc-block-axo-email-submit-button-container'
);
input.parentNode.insertBefore(
submitButtonReference.container,
input.nextSibling
);
submitButtonReference.root = createRoot(
submitButtonReference.container
);
}
const renderButton = () => {
if ( submitButtonReference.root ) {
submitButtonReference.root.render(
createElement( EmailButton, {
handleSubmit: handleEmailSubmit,
} )
);
}
};
renderButton();
// Subscribe to state changes
submitButtonReference.unsubscribe = wp.data.subscribe( () => {
renderButton();
} );
};
export const removeEmailFunctionality = () => {
const input = getEmailInput();
if ( input && keydownHandler ) {
input.removeEventListener( 'keydown', keydownHandler );
}
if ( submitButtonReference.root ) {
submitButtonReference.root.unmount();
}
if ( submitButtonReference.unsubscribe ) {
submitButtonReference.unsubscribe();
}
if (
submitButtonReference.container &&
submitButtonReference.container.parentNode
) {
submitButtonReference.container.parentNode.removeChild(
submitButtonReference.container
);
}
submitButtonReference = { container: null, root: null, unsubscribe: null };
keydownHandler = null;
};
export const isEmailFunctionalitySetup = () => {
return !! submitButtonReference.root;
};

View file

@ -0,0 +1,58 @@
import { useEffect, useCallback, useState } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { Card } from '../Card';
import { STORE_NAME } from '../../stores/axoStore';
export const Payment = ( { fastlaneSdk, onPaymentLoad } ) => {
const [ isCardElementReady, setIsCardElementReady ] = useState( false );
const { isGuest, isEmailLookupCompleted } = useSelect(
( select ) => ( {
isGuest: select( STORE_NAME ).getIsGuest(),
isEmailLookupCompleted:
select( STORE_NAME ).getIsEmailLookupCompleted(),
} ),
[]
);
const loadPaymentComponent = useCallback( async () => {
if ( isGuest && isEmailLookupCompleted && isCardElementReady ) {
const paymentComponent = await fastlaneSdk.FastlaneCardComponent(
{}
);
paymentComponent.render( `#fastlane-card` );
onPaymentLoad( paymentComponent );
}
}, [
isGuest,
isEmailLookupCompleted,
isCardElementReady,
fastlaneSdk,
onPaymentLoad,
] );
useEffect( () => {
if ( isGuest && isEmailLookupCompleted ) {
setIsCardElementReady( true );
}
}, [ isGuest, isEmailLookupCompleted ] );
useEffect( () => {
loadPaymentComponent();
}, [ loadPaymentComponent ] );
if ( isGuest ) {
if ( isEmailLookupCompleted ) {
return <div id="fastlane-card" key="fastlane-card" />;
}
return (
<div id="ppcp-axo-block-radio-content">
{ __(
'Enter your email address above to continue.',
'woocommerce-paypal-payments'
) }
</div>
);
}
return <Card fastlaneSdk={ fastlaneSdk } showWatermark={ ! isGuest } />;
};

View file

@ -0,0 +1,19 @@
import { __ } from '@wordpress/i18n';
const ShippingChangeButton = ( { onChangeShippingAddressClick } ) => (
<a
className="wc-block-axo-change-link"
role="button"
onClick={ ( event ) => {
event.preventDefault();
onChangeShippingAddressClick();
} }
>
{ __(
'Choose a different shipping address',
'woocommerce-paypal-payments'
) }
</a>
);
export default ShippingChangeButton;

View file

@ -0,0 +1,39 @@
import { useEffect, createRoot } from '@wordpress/element';
import ShippingChangeButton from './ShippingChangeButton';
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 default ShippingChangeButtonManager;

View file

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

View file

@ -0,0 +1,28 @@
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 }
/>
);
}
};
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

@ -0,0 +1,47 @@
import { useEffect, useRef } from '@wordpress/element';
import { log } from '../../../../../ppcp-axo/resources/js/Helper/Debug';
const Watermark = ( {
fastlaneSdk,
name = 'fastlane-watermark-container',
includeAdditionalInfo = true,
} ) => {
const containerRef = useRef( null );
const watermarkRef = useRef( null );
useEffect( () => {
const renderWatermark = async () => {
if ( ! containerRef.current ) {
return;
}
// Clear the container
containerRef.current.innerHTML = '';
try {
const watermark = await fastlaneSdk.FastlaneWatermarkComponent(
{
includeAdditionalInfo,
}
);
watermarkRef.current = watermark;
watermark.render( `#${ name }` );
} catch ( error ) {
log( `Error rendering watermark: ${ error }`, 'error' );
}
};
renderWatermark();
return () => {
if ( containerRef.current ) {
containerRef.current.innerHTML = '';
}
};
}, [ fastlaneSdk, name, includeAdditionalInfo ] );
return <div id={ name } ref={ containerRef } />;
};
export default Watermark;

View file

@ -0,0 +1,40 @@
import { useEffect } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { STORE_NAME } from '../../stores/axoStore';
import {
createWatermarkContainer,
removeWatermark,
updateWatermarkContent,
} from './utils';
const WatermarkManager = ( { fastlaneSdk } ) => {
const isGuest = useSelect( ( select ) =>
select( STORE_NAME ).getIsGuest()
);
const isAxoActive = useSelect( ( select ) =>
select( STORE_NAME ).getIsAxoActive()
);
const isAxoScriptLoaded = useSelect( ( select ) =>
select( STORE_NAME ).getIsAxoScriptLoaded()
);
useEffect( () => {
if ( isAxoActive || ( ! isAxoActive && ! isAxoScriptLoaded ) ) {
createWatermarkContainer();
updateWatermarkContent( {
isAxoActive,
isAxoScriptLoaded,
fastlaneSdk,
isGuest,
} );
} else {
removeWatermark();
}
return removeWatermark;
}, [ fastlaneSdk, isGuest, isAxoActive, isAxoScriptLoaded ] );
return null;
};
export default WatermarkManager;

View file

@ -0,0 +1,3 @@
export { default as Watermark } from './Watermark';
export { default as WatermarkManager } from './WatermarkManager';
export { setupWatermark, removeWatermark } from './utils';

View file

@ -0,0 +1,109 @@
import { createElement, createRoot } from '@wordpress/element';
import { Watermark, WatermarkManager } from '../Watermark';
const watermarkReference = {
container: null,
root: null,
};
export const createWatermarkContainer = () => {
const textInputContainer = document.querySelector(
'.wp-block-woocommerce-checkout-contact-information-block .wc-block-components-text-input'
);
if ( textInputContainer && ! watermarkReference.container ) {
const emailInput =
textInputContainer.querySelector( 'input[id="email"]' );
if ( emailInput ) {
watermarkReference.container = document.createElement( 'div' );
watermarkReference.container.setAttribute(
'class',
'wc-block-checkout-axo-block-watermark-container'
);
const emailButton = textInputContainer.querySelector(
'.wc-block-axo-email-submit-button-container'
);
// If possible, insert the watermark after the "Continue" button.
const insertAfterElement = emailButton || emailInput;
insertAfterElement.parentNode.insertBefore(
watermarkReference.container,
insertAfterElement.nextSibling
);
watermarkReference.root = createRoot(
watermarkReference.container
);
}
}
};
export const setupWatermark = ( fastlaneSdk ) => {
const container = document.createElement( 'div' );
document.body.appendChild( container );
const root = createRoot( container );
root.render( createElement( WatermarkManager, { fastlaneSdk } ) );
return () => {
root.unmount();
if ( container && container.parentNode ) {
container.parentNode.removeChild( container );
}
};
};
export const removeWatermark = () => {
if ( watermarkReference.root ) {
watermarkReference.root.unmount();
}
if ( watermarkReference.container ) {
if ( watermarkReference.container.parentNode ) {
watermarkReference.container.parentNode.removeChild(
watermarkReference.container
);
} else {
const detachedContainer = document.querySelector(
'.wc-block-checkout-axo-block-watermark-container'
);
if ( detachedContainer ) {
detachedContainer.remove();
}
}
}
Object.assign( watermarkReference, { container: null, root: null } );
};
export const renderWatermarkContent = ( content ) => {
if ( watermarkReference.root ) {
watermarkReference.root.render( content );
}
};
export const updateWatermarkContent = ( {
isAxoActive,
isAxoScriptLoaded,
fastlaneSdk,
isGuest,
} ) => {
if ( ! isAxoActive && ! isAxoScriptLoaded ) {
renderWatermarkContent(
createElement( 'span', {
className: 'wc-block-components-spinner',
'aria-hidden': 'true',
} )
);
} else if ( isAxoActive ) {
renderWatermarkContent(
createElement( Watermark, {
fastlaneSdk,
name: 'fastlane-watermark-email',
includeAdditionalInfo: isGuest,
} )
);
} else {
renderWatermarkContent( null );
}
};

View file

@ -0,0 +1,96 @@
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
import { populateWooFields } from '../helpers/fieldHelpers';
import { injectShippingChangeButton } from '../components/Shipping';
import { injectCardChangeButton } from '../components/Card';
import { setIsGuest, setIsEmailLookupCompleted } from '../stores/axoStore';
export const createEmailLookupHandler = (
fastlaneSdk,
setShippingAddress,
setCardDetails,
snapshotFields,
wooShippingAddress,
wooBillingAddress,
setWooShippingAddress,
setWooBillingAddress,
onChangeShippingAddressClick,
onChangeCardButtonClick
) => {
return async ( email ) => {
try {
log( `Email value being looked up: ${ email }` );
if ( ! fastlaneSdk ) {
throw new Error( 'FastlaneSDK is not initialized' );
}
if ( ! fastlaneSdk.identity ) {
throw new Error(
'FastlaneSDK identity object is not available'
);
}
const lookup =
await fastlaneSdk.identity.lookupCustomerByEmail( email );
log( `Lookup response: ${ JSON.stringify( lookup ) }` );
// Gary flow
if ( lookup && lookup.customerContextId === '' ) {
setIsEmailLookupCompleted( true );
}
if ( ! lookup || ! lookup.customerContextId ) {
log( 'No customerContextId found in the response', 'warn' );
return;
}
const authResponse =
await fastlaneSdk.identity.triggerAuthenticationFlow(
lookup.customerContextId
);
if ( ! authResponse || ! authResponse.authenticationState ) {
throw new Error( 'Invalid authentication response' );
}
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 ) {
setShippingAddress( profileData.shippingAddress );
}
if ( profileData && profileData.card ) {
setCardDetails( profileData.card );
}
log( `Profile Data: ${ JSON.stringify( profileData ) }` );
populateWooFields(
profileData,
setWooShippingAddress,
setWooBillingAddress
);
injectShippingChangeButton( onChangeShippingAddressClick );
injectCardChangeButton( onChangeCardButtonClick );
} else {
log( 'Authentication failed or did not succeed', 'warn' );
}
} catch ( error ) {
log(
`Error during email lookup or authentication:
${ error }`
);
throw error;
}
};
};

View file

@ -0,0 +1,155 @@
import { select, subscribe } from '@wordpress/data';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
import { STORE_NAME } from '../stores/axoStore';
/**
* Sets up a class toggle based on the isGuest state for the express payment block.
* @return {Function} Unsubscribe function for cleanup.
*/
export const setupAuthenticationClassToggle = () => {
const targetSelector =
'.wp-block-woocommerce-checkout-express-payment-block';
const authClass = 'wc-block-axo-is-authenticated';
const updateAuthenticationClass = () => {
const targetElement = document.querySelector( targetSelector );
if ( ! targetElement ) {
log(
`Authentication class target element not found: ${ targetSelector }`,
'warn'
);
return;
}
const isGuest = select( STORE_NAME ).getIsGuest();
if ( ! isGuest ) {
targetElement.classList.add( authClass );
} else {
targetElement.classList.remove( authClass );
}
};
// Initial update
updateAuthenticationClass();
// Subscribe to state changes
const unsubscribe = subscribe( () => {
updateAuthenticationClass();
} );
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 ) {
log(
`Email lookup completed class target element not found: ${ targetSelector }`,
'warn'
);
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 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 updateCheckoutBlockClassToggles = () => {
const targetElement = document.querySelector( targetSelector );
if ( ! targetElement ) {
log(
`Checkout block class target element not found: ${ targetSelector }`,
'warn'
);
return;
}
const isAxoActive = select( STORE_NAME ).getIsAxoActive();
const isGuest = select( STORE_NAME ).getIsGuest();
const isEmailLookupCompleted =
select( STORE_NAME ).getIsEmailLookupCompleted();
if ( isAxoActive ) {
targetElement.classList.add( axoLoadedClass );
} else {
targetElement.classList.remove( axoLoadedClass );
}
if ( ! isGuest ) {
targetElement.classList.add( authClass );
} else {
targetElement.classList.remove( authClass );
}
if ( isEmailLookupCompleted ) {
targetElement.classList.add( emailLookupCompletedClass );
} else {
targetElement.classList.remove( emailLookupCompletedClass );
}
};
// Initial update
updateCheckoutBlockClassToggles();
// Subscribe to state changes
const unsubscribe = subscribe( () => {
updateCheckoutBlockClassToggles();
} );
return unsubscribe;
};
/**
* Initializes all class toggles.
* @return {Function} Cleanup function.
*/
export const initializeClassToggles = () => {
const unsubscribeAuth = setupAuthenticationClassToggle();
const unsubscribeEmailLookupCompleted =
setupEmailLookupCompletedClassToggle();
const unsubscribeContactInfo = setupCheckoutBlockClassToggles();
return () => {
if ( unsubscribeAuth ) {
unsubscribeAuth();
}
if ( unsubscribeEmailLookupCompleted ) {
unsubscribeEmailLookupCompleted();
}
if ( unsubscribeContactInfo ) {
unsubscribeContactInfo();
}
};
};

View file

@ -0,0 +1,144 @@
import { dispatch } from '@wordpress/data';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
export const snapshotFields = ( shippingAddress, billingAddress ) => {
if ( ! shippingAddress || ! billingAddress ) {
log(
`Shipping or billing address is missing: ${ JSON.stringify( {
shippingAddress,
billingAddress,
} ) }`,
'warn'
);
}
const originalData = { shippingAddress, billingAddress };
log( `Snapshot data: ${ JSON.stringify( originalData ) }` );
try {
localStorage.setItem(
'axoOriginalCheckoutFields',
JSON.stringify( originalData )
);
} catch ( error ) {
log( `Error saving to localStorage: ${ error }`, 'error' );
}
};
export const restoreOriginalFields = (
updateShippingAddress,
updateBillingAddress
) => {
log( 'Attempting to restore original fields' );
let savedData;
try {
savedData = localStorage.getItem( 'axoOriginalCheckoutFields' );
log(
`Data retrieved from localStorage: ${ JSON.stringify( savedData ) }`
);
} catch ( error ) {
log( `Error retrieving from localStorage: ${ error }`, 'error' );
}
if ( savedData ) {
try {
const parsedData = JSON.parse( savedData );
if ( parsedData.shippingAddress ) {
updateShippingAddress( parsedData.shippingAddress );
} else {
log( `No shipping address found in saved data`, 'warn' );
}
if ( parsedData.billingAddress ) {
log(
`Restoring billing address:
${ JSON.stringify( parsedData.billingAddress ) }`
);
updateBillingAddress( parsedData.billingAddress );
} else {
log( 'No billing address found in saved data', 'warn' );
}
} catch ( error ) {
log( `Error parsing saved data: ${ error }` );
}
} else {
log(
'No data found in localStorage under axoOriginalCheckoutFields',
'warn'
);
}
};
export const populateWooFields = (
profileData,
setWooShippingAddress,
setWooBillingAddress
) => {
const CHECKOUT_STORE_KEY = 'wc/store/checkout';
log(
`Populating WooCommerce fields with profile data: ${ JSON.stringify(
profileData
) }`
);
const checkoutDispatch = dispatch( CHECKOUT_STORE_KEY );
// Uncheck the 'Use same address for billing' checkbox if the method exists.
if (
typeof checkoutDispatch.__internalSetUseShippingAsBilling === 'function'
) {
checkoutDispatch.__internalSetUseShippingAsBilling( false );
}
// 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,
};
log(
`Setting WooCommerce shipping address: ${ JSON.stringify(
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,
};
log(
`Setting WooCommerce billing address: ${ JSON.stringify(
billingAddress
) }`
);
setWooBillingAddress( billingAddress );
// Collapse shipping address input fields into the card view.
if ( typeof checkoutDispatch.setEditingShippingAddress === 'function' ) {
checkoutDispatch.setEditingShippingAddress( false );
}
// Collapse billing address input fields into the card view.
if ( typeof checkoutDispatch.setEditingBillingAddress === 'function' ) {
checkoutDispatch.setEditingBillingAddress( false );
}
};

View file

@ -0,0 +1,51 @@
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
? store.getEditingShippingAddress()
: true,
isEditingBillingAddress: store.getEditingBillingAddress
? store.getEditingBillingAddress()
: true,
};
},
[]
);
const { setEditingShippingAddress, setEditingBillingAddress } =
useDispatch( CHECKOUT_STORE_KEY );
const setShippingAddressEditing = useCallback(
( isEditing ) => {
if ( typeof setEditingShippingAddress === 'function' ) {
setEditingShippingAddress( isEditing );
}
},
[ setEditingShippingAddress ]
);
const setBillingAddressEditing = useCallback(
( isEditing ) => {
if ( typeof setEditingBillingAddress === 'function' ) {
setEditingBillingAddress( isEditing );
}
},
[ setEditingBillingAddress ]
);
return {
isEditingShippingAddress,
isEditingBillingAddress,
setShippingAddressEditing,
setBillingAddressEditing,
};
};
export default useAddressEditing;

View file

@ -0,0 +1,48 @@
import { useEffect } from '@wordpress/element';
import { useDispatch } from '@wordpress/data';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
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( () => {
return () => {
log( 'Cleaning up: Restoring WooCommerce fields' );
restoreOriginalFields(
updateWooShippingAddress,
updateWooBillingAddress
);
};
}, [ updateWooShippingAddress, updateWooBillingAddress ] );
useEffect( () => {
return () => {
log( 'Cleaning up Axo component' );
setIsAxoActive( false );
setIsGuest( true );
removeShippingChangeButton();
removeCardChangeButton();
removeWatermark();
if ( isEmailFunctionalitySetup() ) {
log( 'Removing email functionality' );
removeEmailFunctionality();
}
};
}, [] );
};
export default useAxoCleanup;

View file

@ -0,0 +1,80 @@
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 usePhoneSyncHandler from './usePhoneSyncHandler';
import { initializeClassToggles } from '../helpers/classnamesManager';
import { snapshotFields } from '../helpers/fieldHelpers';
import useCustomerData from './useCustomerData';
import useShippingAddressChange from './useShippingAddressChange';
import useCardChange from './useCardChange';
const useAxoSetup = ( ppcpConfig, fastlaneSdk, paymentComponent ) => {
const {
setIsAxoActive,
setIsAxoScriptLoaded,
setShippingAddress,
setCardDetails,
} = useDispatch( STORE_NAME );
const paypalLoaded = usePayPalScript( ppcpConfig );
const onChangeCardButtonClick = useCardChange( fastlaneSdk );
const onChangeShippingAddressClick = useShippingAddressChange(
fastlaneSdk,
setShippingAddress
);
const {
shippingAddress: wooShippingAddress,
billingAddress: wooBillingAddress,
setShippingAddress: setWooShippingAddress,
setBillingAddress: setWooBillingAddress,
} = useCustomerData();
usePhoneSyncHandler( paymentComponent );
useEffect( () => {
initializeClassToggles();
}, [] );
useEffect( () => {
setupWatermark( fastlaneSdk );
if ( paypalLoaded && fastlaneSdk ) {
setIsAxoScriptLoaded( true );
setIsAxoActive( true );
const emailLookupHandler = createEmailLookupHandler(
fastlaneSdk,
setShippingAddress,
setCardDetails,
snapshotFields,
wooShippingAddress,
wooBillingAddress,
setWooShippingAddress,
setWooBillingAddress,
onChangeShippingAddressClick,
onChangeCardButtonClick
);
setupEmailFunctionality( emailLookupHandler );
}
}, [
paypalLoaded,
fastlaneSdk,
setIsAxoActive,
setIsAxoScriptLoaded,
wooShippingAddress,
wooBillingAddress,
setWooShippingAddress,
setWooBillingAddress,
onChangeShippingAddressClick,
onChangeCardButtonClick,
setShippingAddress,
setCardDetails,
paymentComponent,
] );
return paypalLoaded;
};
export default useAxoSetup;

View file

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

View file

@ -0,0 +1,44 @@
import { useCallback, useMemo } from '@wordpress/element';
import { useDispatch, useSelect } from '@wordpress/data';
export const useCustomerData = () => {
const customerData = useSelect( ( select ) =>
select( 'wc/store/cart' ).getCustomerData()
);
const {
setShippingAddress: setShippingAddressDispatch,
setBillingAddress: setBillingAddressDispatch,
} = useDispatch( 'wc/store/cart' );
const setShippingAddress = useCallback(
( address ) => {
setShippingAddressDispatch( address );
},
[ setShippingAddressDispatch ]
);
const setBillingAddress = useCallback(
( address ) => {
setBillingAddressDispatch( address );
},
[ setBillingAddressDispatch ]
);
return useMemo(
() => ( {
shippingAddress: customerData.shippingAddress,
billingAddress: customerData.billingAddress,
setShippingAddress,
setBillingAddress,
} ),
[
customerData.shippingAddress,
customerData.billingAddress,
setShippingAddress,
setBillingAddress,
]
);
};
export default useCustomerData;

View file

@ -0,0 +1,27 @@
import { useCallback } from '@wordpress/element';
const isObject = ( value ) => typeof value === 'object' && value !== null;
const isNonEmptyString = ( value ) => value !== '';
const removeEmptyValues = ( obj ) => {
if ( ! isObject( obj ) ) {
return obj;
}
return Object.fromEntries(
Object.entries( obj )
.map( ( [ key, value ] ) => [
key,
isObject( value ) ? removeEmptyValues( value ) : value,
] )
.filter( ( [ _, value ] ) =>
isObject( value )
? Object.keys( value ).length > 0
: isNonEmptyString( value )
)
);
};
export const useDeleteEmptyKeys = () => {
return useCallback( removeEmptyValues, [] );
};

View file

@ -0,0 +1,57 @@
import { useEffect, useRef, useState, useMemo } from '@wordpress/element';
import Fastlane from '../../../../ppcp-axo/resources/js/Connection/Fastlane';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
import { useDeleteEmptyKeys } from './useDeleteEmptyKeys';
const useFastlaneSdk = ( axoConfig, ppcpConfig ) => {
const [ fastlaneSdk, setFastlaneSdk ] = useState( null );
const initializingRef = useRef( false );
const configRef = useRef( { axoConfig, ppcpConfig } );
const deleteEmptyKeys = useDeleteEmptyKeys();
const styleOptions = useMemo( () => {
return deleteEmptyKeys( configRef.current.axoConfig.style_options );
}, [ deleteEmptyKeys ] );
useEffect( () => {
const initFastlane = async () => {
if ( initializingRef.current || fastlaneSdk ) {
return;
}
initializingRef.current = true;
log( 'Init Fastlane' );
try {
const fastlane = new Fastlane();
if ( configRef.current.axoConfig.environment.is_sandbox ) {
window.localStorage.setItem( 'axoEnv', 'sandbox' );
}
await fastlane.connect( {
locale: configRef.current.ppcpConfig.locale,
styles: styleOptions,
} );
fastlane.setLocale( 'en_us' );
setFastlaneSdk( fastlane );
} catch ( error ) {
log( `Failed to initialize Fastlane: ${ error }`, 'error' );
} finally {
initializingRef.current = false;
}
};
initFastlane();
}, [ fastlaneSdk, styleOptions ] );
useEffect( () => {
configRef.current = { axoConfig, ppcpConfig };
}, [ axoConfig, ppcpConfig ] );
return fastlaneSdk;
};
export default useFastlaneSdk;

View file

@ -0,0 +1,59 @@
import { useCallback } from '@wordpress/element';
import { useSelect } from '@wordpress/data';
import { STORE_NAME } from '../stores/axoStore';
const useHandlePaymentSetup = (
emitResponse,
paymentComponent,
tokenizedCustomerData
) => {
const { cardDetails } = useSelect(
( select ) => ( {
shippingAddress: select( STORE_NAME ).getShippingAddress(),
cardDetails: select( STORE_NAME ).getCardDetails(),
} ),
[]
);
return useCallback( async () => {
const isRyanFlow = !! cardDetails?.id;
let cardToken = cardDetails?.id;
if ( ! cardToken && paymentComponent ) {
cardToken = await paymentComponent
.getPaymentToken( tokenizedCustomerData )
.then( ( response ) => response.id );
}
if ( ! cardToken ) {
let reason = 'tokenization error';
if ( ! paymentComponent ) {
reason = 'initialization error';
}
return {
type: emitResponse.responseTypes.ERROR,
message: `Could not process the payment (${ reason })`,
};
}
return {
type: emitResponse.responseTypes.SUCCESS,
meta: {
paymentMethodData: {
fastlane_member: isRyanFlow,
axo_nonce: cardToken,
},
},
};
}, [
cardDetails?.id,
emitResponse.responseTypes.ERROR,
emitResponse.responseTypes.SUCCESS,
paymentComponent,
tokenizedCustomerData,
] );
};
export default useHandlePaymentSetup;

View file

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

View file

@ -0,0 +1,30 @@
import { useEffect, useCallback } from '@wordpress/element';
const usePaymentSetupEffect = (
onPaymentSetup,
handlePaymentSetup,
setPaymentComponent
) => {
/**
* `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 ) => {
setPaymentComponent( component );
},
[ setPaymentComponent ]
);
return { handlePaymentLoad };
};
export default usePaymentSetupEffect;

View file

@ -0,0 +1,88 @@
import { useEffect, useRef, useCallback } from '@wordpress/element';
import { useSelect, useDispatch } from '@wordpress/data';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
import { debounce } from '../../../../ppcp-blocks/resources/js/Helper/debounce';
import { STORE_NAME } from '../stores/axoStore';
import useCustomerData from './useCustomerData';
const PHONE_DEBOUNCE_DELAY = 250;
/**
* Sanitizes a phone number by removing country code and non-numeric characters.
* Only returns the sanitized number if it's exactly 10 digits long (US phone number).
*
* @param {string} phoneNumber - The phone number to sanitize.
* @return {string} The sanitized phone number; an empty string if it's invalid.
*/
const sanitizePhoneNumber = ( phoneNumber = '' ) => {
const localNumber = phoneNumber.replace( /^\+?[01]+/, '' );
const cleanNumber = localNumber.replace( /[^0-9]/g, '' );
return cleanNumber.length === 10 ? cleanNumber : '';
};
/**
* Updates the prefilled phone number in the Fastlane CardField component.
*
* @param {Object} paymentComponent - The CardField component from Fastlane
* @param {string} phoneNumber - The new phone number to prefill.
*/
const updatePrefills = ( paymentComponent, phoneNumber ) => {
log( `Update the phone prefill value: ${ phoneNumber }` );
paymentComponent.updatePrefills( { phoneNumber } );
};
/**
* Custom hook to synchronize the WooCommerce phone number with a React component state.
*
* @param {Object} paymentComponent - The CardField component from Fastlane.
*/
const usePhoneSyncHandler = ( paymentComponent ) => {
const { setPhoneNumber } = useDispatch( STORE_NAME );
const { phoneNumber } = useSelect( ( select ) => ( {
phoneNumber: select( STORE_NAME ).getPhoneNumber(),
} ) );
const { shippingAddress, billingAddress } = useCustomerData();
// Create a debounced function that updates the prefilled phone-number.
const debouncedUpdatePhone = useRef(
debounce( updatePrefills, PHONE_DEBOUNCE_DELAY )
).current;
// Fetch and update the phone number from the billing or shipping address.
const fetchAndUpdatePhoneNumber = useCallback( () => {
const billingPhone = billingAddress?.phone || '';
const shippingPhone = shippingAddress?.phone || '';
const sanitizedPhoneNumber = sanitizePhoneNumber(
billingPhone || shippingPhone
);
if ( sanitizedPhoneNumber && sanitizedPhoneNumber !== phoneNumber ) {
setPhoneNumber( sanitizedPhoneNumber );
}
}, [ billingAddress, shippingAddress, phoneNumber, setPhoneNumber ] );
// Fetch and update the phone number from the billing or shipping address.
useEffect( () => {
fetchAndUpdatePhoneNumber();
}, [ fetchAndUpdatePhoneNumber ] );
// Invoke debounced function when paymentComponent or phoneNumber changes.
useEffect( () => {
if ( paymentComponent && phoneNumber ) {
debouncedUpdatePhone( paymentComponent, phoneNumber );
}
}, [ debouncedUpdatePhone, paymentComponent, phoneNumber ] );
// Cleanup on unmount, canceling any pending debounced calls.
useEffect( () => {
return () => {
if ( debouncedUpdatePhone?.cancel ) {
debouncedUpdatePhone.cancel();
}
};
}, [ debouncedUpdatePhone ] );
};
export default usePhoneSyncHandler;

View file

@ -0,0 +1,49 @@
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();
return useCallback( async () => {
if ( fastlaneSdk ) {
const { selectionChanged, selectedAddress } =
await fastlaneSdk.profile.showShippingAddressSelector();
if ( selectionChanged ) {
setShippingAddress( selectedAddress );
const { address, name, phoneNumber } = selectedAddress;
const newShippingAddress = {
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,
};
await new Promise( ( resolve ) => {
setWooShippingAddress( newShippingAddress );
resolve();
} );
await new Promise( ( resolve ) => {
setShippingAddressEditing( false );
resolve();
} );
}
}
}, [
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

@ -0,0 +1,76 @@
import { useState, createElement } from '@wordpress/element';
import { __ } from '@wordpress/i18n';
import { registerPaymentMethod } from '@woocommerce/blocks-registry';
// Hooks
import useFastlaneSdk from './hooks/useFastlaneSdk';
import useTokenizeCustomerData from './hooks/useTokenizeCustomerData';
import useAxoSetup from './hooks/useAxoSetup';
import useAxoCleanup from './hooks/useAxoCleanup';
import useHandlePaymentSetup from './hooks/useHandlePaymentSetup';
import usePaymentSetupEffect from './hooks/usePaymentSetupEffect';
// Components
import { Payment } from './components/Payment/Payment';
const gatewayHandle = 'ppcp-axo-gateway';
const ppcpConfig = wc.wcSettings.getSetting( `${ gatewayHandle }_data` );
if ( typeof window.PayPalCommerceGateway === 'undefined' ) {
window.PayPalCommerceGateway = ppcpConfig;
}
const axoConfig = window.wc_ppcp_axo;
const Axo = ( props ) => {
const { eventRegistration, emitResponse } = props;
const { onPaymentSetup } = eventRegistration;
const [ paymentComponent, setPaymentComponent ] = useState( null );
const fastlaneSdk = useFastlaneSdk( axoConfig, ppcpConfig );
const tokenizedCustomerData = useTokenizeCustomerData();
const handlePaymentSetup = useHandlePaymentSetup(
emitResponse,
paymentComponent,
tokenizedCustomerData
);
useAxoSetup( ppcpConfig, fastlaneSdk, paymentComponent );
const { handlePaymentLoad } = usePaymentSetupEffect(
onPaymentSetup,
handlePaymentSetup,
setPaymentComponent
);
useAxoCleanup();
return fastlaneSdk ? (
<Payment
fastlaneSdk={ fastlaneSdk }
onPaymentLoad={ handlePaymentLoad }
/>
) : (
<>{ __( 'Loading Fastlane…', 'woocommerce-paypal-payments' ) }</>
);
};
registerPaymentMethod( {
name: ppcpConfig.id,
label: (
<div
id="ppcp-axo-block-radio-label"
dangerouslySetInnerHTML={ { __html: ppcpConfig.title } }
/>
),
content: <Axo />,
edit: createElement( ppcpConfig.title ),
ariaLabel: ppcpConfig.title,
canMakePayment: () => true,
supports: {
showSavedCards: true,
features: ppcpConfig.supports,
},
} );
export default Axo;

View file

@ -0,0 +1,117 @@
import { createReduxStore, register, dispatch } from '@wordpress/data';
export const STORE_NAME = 'woocommerce-paypal-payments/axo-block';
// Initial state
const DEFAULT_STATE = {
isGuest: true,
isAxoActive: false,
isAxoScriptLoaded: false,
isEmailSubmitted: false,
isEmailLookupCompleted: false,
shippingAddress: null,
cardDetails: null,
phoneNumber: '',
};
// Actions
const actions = {
setIsGuest: ( isGuest ) => ( {
type: 'SET_IS_GUEST',
payload: isGuest,
} ),
setIsAxoActive: ( isAxoActive ) => ( {
type: 'SET_IS_AXO_ACTIVE',
payload: isAxoActive,
} ),
setIsAxoScriptLoaded: ( isAxoScriptLoaded ) => ( {
type: 'SET_IS_AXO_SCRIPT_LOADED',
payload: isAxoScriptLoaded,
} ),
setIsEmailSubmitted: ( isEmailSubmitted ) => ( {
type: 'SET_IS_EMAIL_SUBMITTED',
payload: isEmailSubmitted,
} ),
setIsEmailLookupCompleted: ( isEmailLookupCompleted ) => ( {
type: 'SET_IS_EMAIL_LOOKUP_COMPLETED',
payload: isEmailLookupCompleted,
} ),
setShippingAddress: ( shippingAddress ) => ( {
type: 'SET_SHIPPING_ADDRESS',
payload: shippingAddress,
} ),
setCardDetails: ( cardDetails ) => ( {
type: 'SET_CARD_DETAILS',
payload: cardDetails,
} ),
setPhoneNumber: ( phoneNumber ) => ( {
type: 'SET_PHONE_NUMBER',
payload: phoneNumber,
} ),
};
// Reducer
const reducer = ( state = DEFAULT_STATE, action ) => {
switch ( action.type ) {
case 'SET_IS_GUEST':
return { ...state, isGuest: action.payload };
case 'SET_IS_AXO_ACTIVE':
return { ...state, isAxoActive: action.payload };
case 'SET_IS_AXO_SCRIPT_LOADED':
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 };
case 'SET_SHIPPING_ADDRESS':
return { ...state, shippingAddress: action.payload };
case 'SET_CARD_DETAILS':
return { ...state, cardDetails: action.payload };
case 'SET_PHONE_NUMBER':
return { ...state, phoneNumber: action.payload };
default:
return state;
}
};
// Selectors
const selectors = {
getIsGuest: ( state ) => state.isGuest,
getIsAxoActive: ( state ) => state.isAxoActive,
getIsAxoScriptLoaded: ( state ) => state.isAxoScriptLoaded,
getIsEmailSubmitted: ( state ) => state.isEmailSubmitted,
getIsEmailLookupCompleted: ( state ) => state.isEmailLookupCompleted,
getShippingAddress: ( state ) => state.shippingAddress,
getCardDetails: ( state ) => state.cardDetails,
getPhoneNumber: ( state ) => state.phoneNumber,
};
// Create and register the store
const store = createReduxStore( STORE_NAME, {
reducer,
actions,
selectors,
} );
register( store );
// Action dispatchers
export const setIsGuest = ( isGuest ) => {
dispatch( STORE_NAME ).setIsGuest( isGuest );
};
export const setIsEmailLookupCompleted = ( isEmailLookupCompleted ) => {
dispatch( STORE_NAME ).setIsEmailLookupCompleted( isEmailLookupCompleted );
};
export const setShippingAddress = ( shippingAddress ) => {
dispatch( STORE_NAME ).setShippingAddress( shippingAddress );
};
export const setCardDetails = ( cardDetails ) => {
dispatch( STORE_NAME ).setCardDetails( cardDetails );
};
export const setPhoneNumber = ( phoneNumber ) => {
dispatch( STORE_NAME ).setPhoneNumber( phoneNumber );
};

View file

@ -0,0 +1,43 @@
<?php
/**
* The Axo module services.
*
* @package WooCommerce\PayPalCommerce\Axo
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AxoBlock;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
// If AXO Block is configured and onboarded.
'axoblock.available' => static function ( ContainerInterface $container ) : bool {
return true;
},
'axoblock.url' => static function ( ContainerInterface $container ) : string {
/**
* The path cannot be false.
*
* @psalm-suppress PossiblyFalseArgument
*/
return plugins_url(
'/modules/ppcp-axo-block/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'axoblock.method' => static function ( ContainerInterface $container ) : AxoBlockPaymentMethod {
return new AxoBlockPaymentMethod(
$container->get( 'axoblock.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'axo.gateway' ),
fn() : SmartButtonInterface => $container->get( 'button.smart-button' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.configuration.dcc' ),
$container->get( 'onboarding.environment' ),
$container->get( 'wcgateway.url' )
);
},
);

View file

@ -0,0 +1,162 @@
<?php
/**
* The Axo Block module.
*
* @package WooCommerce\PayPalCommerce\AxoBlock
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\AxoBlock;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* Class AxoBlockModule
*/
class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
if (
! class_exists( 'Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType' )
|| ! function_exists( 'woocommerce_store_api_register_payment_requirements' )
) {
add_action(
'admin_notices',
function () {
printf(
'<div class="notice notice-error"><p>%1$s</p></div>',
wp_kses_post(
__(
'Fastlane checkout block initialization failed, possibly old WooCommerce version or disabled WooCommerce Blocks plugin.',
'woocommerce-paypal-payments'
)
)
);
}
);
}
add_action(
'wp_loaded',
function () use ( $c ) {
add_filter(
'woocommerce_paypal_payments_localized_script_data',
function( array $localized_script_data ) use ( $c ) {
$module = $this;
$api = $c->get( 'api.sdk-client-token' );
assert( $api instanceof SdkClientToken );
$logger = $c->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
return $module->add_sdk_client_token_to_script_data( $api, $logger, $localized_script_data );
}
);
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
add_filter(
'woocommerce_paypal_payments_sdk_components_hook',
function( $components ) {
$components[] = 'fastlane';
return $components;
}
);
}
);
add_action(
'woocommerce_blocks_payment_method_type_registration',
function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void {
$payment_method_registry->register( $c->get( 'axoblock.method' ) );
}
);
// Enqueue frontend scripts.
add_action(
'wp_enqueue_scripts',
static function () use ( $c ) {
if ( ! has_block( 'woocommerce/checkout' ) && ! has_block( 'woocommerce/cart' ) ) {
return;
}
$module_url = $c->get( 'axoblock.url' );
$asset_version = $c->get( 'ppcp.asset-version' );
wp_register_style(
'wc-ppcp-axo-block',
untrailingslashit( $module_url ) . '/assets/css/gateway.css',
array(),
$asset_version
);
wp_enqueue_style( 'wc-ppcp-axo-block' );
}
);
return true;
}
/**
* Adds id token to localized script data.
*
* @param SdkClientToken $api User id token api.
* @param LoggerInterface $logger The logger.
* @param array $localized_script_data The localized script data.
* @return array
*/
private function add_sdk_client_token_to_script_data(
SdkClientToken $api,
LoggerInterface $logger,
array $localized_script_data
): array {
try {
$sdk_client_token = $api->sdk_client_token();
$localized_script_data['axo'] = array(
'sdk_client_token' => $sdk_client_token,
);
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger->error( $error );
}
return $localized_script_data;
}
}

View file

@ -0,0 +1,248 @@
<?php
/**
* Axo block payment method.
*
* @package WooCommerce\PayPalCommerce\AxoBlock
*/
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\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;
/**
* Class AxoBlockPaymentMethod
*/
class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
/**
* The URL of this module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* Credit card gateway.
*
* @var WC_Payment_Gateway
*/
private $gateway;
/**
* The smart button script loading handler.
*
* @var SmartButtonInterface|callable
*/
private $smart_button;
/**
* The settings.
*
* @var Settings
*/
protected $settings;
/**
* The DCC gateway settings.
*
* @var DCCGatewayConfiguration
*/
protected DCCGatewayConfiguration $dcc_configuration;
/**
* The environment object.
*
* @var Environment
*/
private $environment;
/**
* The WcGateway module URL.
*
* @var string
*/
private $wcgateway_module_url;
/**
* AdvancedCardPaymentMethod constructor.
*
* @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 DCCGatewayConfiguration $dcc_configuration The DCC gateway settings.
* @param Environment $environment The environment object.
* @param string $wcgateway_module_url The WcGateway module URL.
*/
public function __construct(
string $module_url,
string $version,
WC_Payment_Gateway $gateway,
$smart_button,
Settings $settings,
DCCGatewayConfiguration $dcc_configuration,
Environment $environment,
string $wcgateway_module_url
) {
$this->name = AxoGateway::ID;
$this->module_url = $module_url;
$this->version = $version;
$this->gateway = $gateway;
$this->smart_button = $smart_button;
$this->settings = $settings;
$this->dcc_configuration = $dcc_configuration;
$this->environment = $environment;
$this->wcgateway_module_url = $wcgateway_module_url;
}
/**
* {@inheritDoc}
*/
public function initialize() {
}
/**
* {@inheritDoc}
*/
public function is_active() : bool {
return $this->gateway->is_available();
}
/**
* {@inheritDoc}
*/
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
: array(
'dependencies' => array(),
'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'],
true
);
wp_localize_script(
'ppcp-axo-block',
'wc_ppcp_axo',
$this->script_data()
);
return array( 'ppcp-axo-block' );
}
/**
* {@inheritDoc}
*/
public function get_payment_method_data() {
return array(
'id' => $this->name,
'title' => 'Debit & Credit Cards',
'description' => 'Axo Description',
'supports' => array_filter(
$this->gateway->supports,
array(
$this->gateway,
'supports',
)
),
);
}
/**
* The configuration for AXO.
*
* @return array
*/
private function script_data() : array {
if ( is_admin() ) {
return array();
}
return array(
'environment' => array(
'is_sandbox' => $this->environment->current_environment() === 'sandbox',
),
'widgets' => array(
'email' => 'render',
),
'insights' => array(
'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 )
: '',
'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.
),
),
'style_options' => array(
'root' => array(
'backgroundColor' => $this->settings->has( 'axo_style_root_bg_color' ) ? $this->settings->get( 'axo_style_root_bg_color' ) : '',
'errorColor' => $this->settings->has( 'axo_style_root_error_color' ) ? $this->settings->get( 'axo_style_root_error_color' ) : '',
'fontFamily' => $this->settings->has( 'axo_style_root_font_family' ) ? $this->settings->get( 'axo_style_root_font_family' ) : '',
'textColorBase' => $this->settings->has( 'axo_style_root_text_color_base' ) ? $this->settings->get( 'axo_style_root_text_color_base' ) : '',
'fontSizeBase' => $this->settings->has( 'axo_style_root_font_size_base' ) ? $this->settings->get( 'axo_style_root_font_size_base' ) : '',
'padding' => $this->settings->has( 'axo_style_root_padding' ) ? $this->settings->get( 'axo_style_root_padding' ) : '',
'primaryColor' => $this->settings->has( 'axo_style_root_primary_color' ) ? $this->settings->get( 'axo_style_root_primary_color' ) : '',
),
'input' => array(
'backgroundColor' => $this->settings->has( 'axo_style_input_bg_color' ) ? $this->settings->get( 'axo_style_input_bg_color' ) : '',
'borderRadius' => $this->settings->has( 'axo_style_input_border_radius' ) ? $this->settings->get( 'axo_style_input_border_radius' ) : '',
'borderColor' => $this->settings->has( 'axo_style_input_border_color' ) ? $this->settings->get( 'axo_style_input_border_color' ) : '',
'borderWidth' => $this->settings->has( 'axo_style_input_border_width' ) ? $this->settings->get( 'axo_style_input_border_width' ) : '',
'textColorBase' => $this->settings->has( 'axo_style_input_text_color_base' ) ? $this->settings->get( 'axo_style_input_text_color_base' ) : '',
'focusBorderColor' => $this->settings->has( 'axo_style_input_focus_border_color' ) ? $this->settings->get( 'axo_style_input_focus_border_color' ) : '',
),
),
'name_on_card' => $this->dcc_configuration->show_name_on_card(),
'woocommerce' => array(
'states' => array(
'US' => WC()->countries->get_states( 'US' ),
'CA' => WC()->countries->get_states( 'CA' ),
),
),
'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/',
'module_url' => untrailingslashit( $this->module_url ),
'ajax' => array(
'frontend_logger' => array(
'endpoint' => \WC_AJAX::get_endpoint( FrontendLoggerEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ),
),
),
'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '',
'wp_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG,
);
}
}

View file

@ -0,0 +1,41 @@
const path = require( 'path' );
const isProduction = process.env.NODE_ENV === 'production';
const DependencyExtractionWebpackPlugin = require( '@woocommerce/dependency-extraction-webpack-plugin' );
module.exports = {
devtool: isProduction ? 'source-map' : 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
plugins: [ new DependencyExtractionWebpackPlugin() ],
entry: {
'index': path.resolve( './resources/js/index.js' ),
gateway: path.resolve( './resources/css/gateway.scss' ),
},
output: {
path: path.resolve( __dirname, 'assets/' ),
filename: 'js/[name].js',
},
module: {
rules: [
{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
{
loader: 'file-loader',
options: {
name: 'css/[name].css',
},
},
{ loader: 'sass-loader' },
],
},
],
},
};

File diff suppressed because it is too large Load diff

View file

@ -70,7 +70,12 @@ return array(
'type' => 'checkbox',
'label' => __( 'Enable Fastlane by PayPal', 'woocommerce-paypal-payments' )
. '<p class="description">'
. __( 'Help accelerate the checkout process for guests with PayPal\'s autofill solution. When enabled, Fastlane is presented as the default payment method for guests.', 'woocommerce-paypal-payments' )
. sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Help accelerate the checkout process for guests with PayPal\'s autofill solution. When enabled, Fastlane is presented as the default payment method for guests. See the %1$sFastlane setup guide%2$s for more details on the Fastlane configuration.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#fastlane" target="_blank">',
'</a>'
)
. '</p>',
'default' => 'yes',
'screens' => array( State::STATE_ONBOARDED ),
@ -82,10 +87,7 @@ return array(
$display_manager
->rule()
->condition_element( 'axo_enabled', '1' )
->action_visible( 'axo_gateway_title' )
->action_visible( 'axo_main_notice' )
->action_visible( 'axo_privacy' )
->action_visible( 'axo_name_on_card' )
->action_visible( 'axo_style_heading' )
->action_class( 'axo_enabled', 'active' )
->to_array(),
@ -133,54 +135,6 @@ return array(
'requirements' => array( 'dcc', 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_gateway_title' => array(
'title' => __( 'Gateway Title', 'woocommerce-paypal-payments' ),
'type' => 'text',
'classes' => array( 'ppcp-field-indent' ),
'desc_tip' => true,
'description' => __(
'This controls the title of the Fastlane gateway the user sees on checkout.',
'woocommerce-paypal-payments'
),
'default' => __(
'Debit & Credit Cards',
'woocommerce-paypal-payments'
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array( 'axo' ),
'gateway' => array( 'dcc', 'axo' ),
),
'axo_privacy' => array(
'title' => __( 'Privacy', 'woocommerce-paypal-payments' ),
'type' => 'select',
'description' => __(
'PayPal powers this accelerated checkout solution from Fastlane. Since you\'ll share consumers\' email address with PayPal, please consult your legal advisors on the appropriate privacy setting for your business.',
'woocommerce-paypal-payments'
),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'yes',
'options' => PropertiesDictionary::privacy_options(),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => array( 'dcc', 'axo' ),
'requirements' => array( 'axo' ),
),
'axo_name_on_card' => array(
'title' => __( 'Cardholder Name', 'woocommerce-paypal-payments' ),
'type' => 'select',
'default' => 'yes',
'options' => PropertiesDictionary::cardholder_name_options(),
'classes' => array( 'ppcp-field-indent' ),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'description' => __( 'This setting will control whether or not the cardholder name is displayed in the card field\'s UI.', 'woocommerce-paypal-payments' ),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => array( 'dcc', 'axo' ),
'requirements' => array( 'axo' ),
),
'axo_style_heading' => array(
'heading' => __( 'Advanced Style Settings (optional)', 'woocommerce-paypal-payments' ),
'heading_html' => sprintf(

View file

@ -1,23 +1,48 @@
.ppcp-axo-watermark-container {
max-width: 200px;
margin-top: 10px;
position: relative;
justify-self: end;
align-self: end;
margin-top: 5px;
&.loader {
width: 15px;
align-self: center;
}
&:before {
display: none;
}
&.loader:before {
display: block; /* Same as WooCommerce's `@mixin loader()` uses */
height: 12px;
width: 12px;
margin-left: -6px;
margin-top: -6px;
left: 12px;
position: relative;
}
}
#ppcp-axo-billing-email-field-wrapper {
display: flex;
gap: 0.5rem;
.woocommerce-input-wrapper {
display: grid;
grid-template-areas:
"input"
"watermark"
"button";
grid-template-columns: 1fr;
gap: 6px;
align-items: start;
width: 100%;
}
}
#ppcp-axo-billing-email-submit-button {
grid-area: button;
width: 100%;
margin-top: 0;
position: relative;
transition: opacity 0.3s ease;
@ -36,6 +61,8 @@
}
.ppcp-axo-billing-email-submit-button {
position: relative;
&-hidden {
opacity: 0;
}
@ -47,7 +74,6 @@
.ppcp-axo-payment-container {
padding: 1rem 0;
background-color: #ffffff;
&.axo-hidden {
display: none;
@ -73,18 +99,102 @@
position: relative;
}
.axo-checkout-header-section {
display: flex;
align-items: baseline;
gap: 1em;
.axo-checkout-wrapper {
margin-bottom: 20px;
.axo-checkout-header-section {
display: flex;
align-items: baseline;
gap: 1em;
margin-bottom: 1em;
}
.axo-checkout-card-preview {
border: 2px solid #cccccc;
border-radius: 10px;
padding: 16px 20px;
background-color: #f6f6f6;
}
.ppcp-card-icon-wrapper {
float: right;
}
.ppcp-card-icon {
width: auto;
height: 25px;
}
.axo-card-number {
font-family: monospace;
font-size: 1rem;
margin-top: 10px;
}
.axo-card-owner {
text-transform: uppercase;
}
.styled-card {
position: relative;
width: 100%;
max-width: 340px;
height: 210px;
margin: 0 auto;
box-shadow: 0 3px 10px -3px #0004;
background-image: linear-gradient(60deg, #0001, #ccc1 65%, #fff6 68%, #fff0);
box-sizing: border-box;
padding: 0;
.ppcp-card-icon-wrapper {
position: absolute;
right: 32px;
top: 32px;
height: 40px;
}
.axo-card-number {
margin-top: 76px;
font-size: 24px;
text-shadow: 0 -1px 1px #fff, 0 1px 1px #0004;
color: #666;
text-align: center;
}
.axo-card-expiry {
font-size: 14px;
padding-right: 32px;
text-align: right;
}
.axo-card-owner {
position: absolute;
left: 24px;
bottom: 20px;
line-height: 1em;
}
@media (max-width: 480px) {
.axo-card-number {
font-size: 20px;
text-align: left;
padding-left: 20px;
}
}
@media (max-width: 360px) {
.axo-card-number {
font-size: 16px;
}
}
}
}
.ppcp-axo-order-button {
float: none;
width: 100%;
box-sizing: border-box;
margin: var(--global-md-spacing) 0 1em;
padding: 0.6em 1em;
float: none;
width: 100%;
box-sizing: border-box;
margin: var(--global-md-spacing) 0 1em;
padding: 0.6em 1em;
}
.ppcp-axo-watermark-loading {
@ -123,10 +233,6 @@
}
}
.ppcp-axo-customer-details #billing_email_field .woocommerce-input-wrapper {
flex: 1 1 auto;
}
@media screen and (max-width: 719px) {
#ppcp-axo-billing-email {
&-field-wrapper {
@ -137,7 +243,32 @@
align-self: auto;
}
}
input[type="email"] {
grid-area: input;
width: 100%;
}
#ppcp-axo-billing-email-submit-button {
align-self: center;
}
}
@media (min-width: 783px) {
#ppcp-axo-billing-email-field-wrapper .woocommerce-input-wrapper {
grid-template-areas:
"input button"
"watermark watermark";
grid-template-columns: 1fr auto;
gap: 6px 8px;
}
input[type="email"] {
align-self: center;
}
#ppcp-axo-billing-email-submit-button {
align-self: center;
width: auto;
}
}

View file

@ -689,28 +689,37 @@ class AxoManager {
this.el.billingEmailSubmitButtonSpinner;
if ( ! document.querySelector( billingEmailSubmitButton.selector ) ) {
document
.querySelector( this.el.billingEmailFieldWrapper.selector )
.insertAdjacentHTML(
'beforeend',
`
<button type="button" id="${ billingEmailSubmitButton.id }" class="${ billingEmailSubmitButton.className }">
${ this.axoConfig.billing_email_button_text }
<span id="${ billingEmailSubmitButtonSpinner.id }"></span>
</button>
`
);
const wrapper = document.querySelector(
'#billing_email_field .woocommerce-input-wrapper'
);
const watermarkContainer = document.querySelector(
'#ppcp-axo-watermark-container'
);
document.querySelector( this.el.billingEmailSubmitButton.selector )
.offsetHeight;
document
.querySelector( this.el.billingEmailSubmitButton.selector )
.classList.remove(
'ppcp-axo-billing-email-submit-button-hidden'
);
document
.querySelector( this.el.billingEmailSubmitButton.selector )
.classList.add( 'ppcp-axo-billing-email-submit-button-loaded' );
wrapper.insertAdjacentHTML(
'beforeend',
`
<button type="button" id="${ billingEmailSubmitButton.id }" class="${ billingEmailSubmitButton.className }">
${ this.axoConfig.billing_email_button_text }
<span id="${ billingEmailSubmitButtonSpinner.id }"></span>
</button>
`
);
const buttonElement = document.querySelector(
billingEmailSubmitButton.selector
);
// Reorder button to ensure it's before the watermark container
wrapper.insertBefore( buttonElement, watermarkContainer );
buttonElement.offsetHeight;
buttonElement.classList.remove(
'ppcp-axo-billing-email-submit-button-hidden'
);
buttonElement.classList.add(
'ppcp-axo-billing-email-submit-button-loaded'
);
}
}

View file

@ -8,6 +8,9 @@ export function log( message, level = 'info' ) {
case 'error':
console.error( `[AXO] ${ message }` );
break;
case 'warn':
console.warn( `[AXO] ${ message }` );
break;
default:
console.log( `[AXO] ${ message }` );
}

View file

@ -28,7 +28,7 @@ class CardView {
const cardIcons = {
VISA: 'visa-light.svg',
MASTER_CARD: 'mastercard-light.svg',
MASTERCARD: 'mastercard-light.svg',
AMEX: 'amex-light.svg',
DISCOVER: 'discover-light.svg',
DINERS: 'dinersclub-light.svg',
@ -37,15 +37,15 @@ class CardView {
};
return `
<div style="margin-bottom: 20px;">
<div class="axo-checkout-wrapper">
<div class="axo-checkout-header-section">
<h3>Card Details</h3>
<a href="javascript:void(0)" ${
this.el.changeCardLink.attributes
}>Edit</a>
</div>
<div style="border:2px solid #cccccc; border-radius: 10px; padding: 16px 20px; background-color:#f6f6f6">
<div style="float: right;">
<div class="axo-checkout-card-preview styled-card">
<div class="ppcp-card-icon-wrapper">
<img
class="ppcp-card-icon"
title="${ data.value( 'brand' ) }"
@ -55,14 +55,16 @@ class CardView {
alt="${ data.value( 'brand' ) }"
>
</div>
<div style="font-family: monospace; font-size: 1rem; margin-top: 10px;">${
<div class="axo-card-number">${
data.value( 'lastDigits' )
? '**** **** **** ' +
data.value( 'lastDigits' )
: ''
}</div>
<div>${ expiry[ 1 ] }/${ expiry[ 0 ] }</div>
<div style="text-transform: uppercase">${ data.value(
<div class="axo-card-expiry">${ expiry[ 1 ] }/${
expiry[ 0 ]
}</div>
<div class="axo-card-owner">${ data.value(
'name'
) }</div>
</div>

View file

@ -17,6 +17,7 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;
return array(
@ -74,10 +75,11 @@ return array(
return new AxoGateway(
$container->get( 'wcgateway.settings.render' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.configuration.dcc' ),
$container->get( 'wcgateway.url' ),
$container->get( 'session.handler' ),
$container->get( 'wcgateway.order-processor' ),
$container->get( 'axo.card_icons' ),
$container->get( 'axo.card_icons.axo' ),
$container->get( 'wcgateway.credit-card-icons' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'api.factory.shipping-preference' ),
@ -87,60 +89,6 @@ return array(
);
},
'axo.card_icons' => static function ( ContainerInterface $container ): array {
return array(
array(
'title' => 'Visa',
'file' => 'visa-dark.svg',
),
array(
'title' => 'MasterCard',
'file' => 'mastercard-dark.svg',
),
array(
'title' => 'American Express',
'file' => 'amex.svg',
),
array(
'title' => 'Discover',
'file' => 'discover.svg',
),
);
},
'axo.card_icons.axo' => static function ( ContainerInterface $container ): array {
return array(
array(
'title' => 'Visa',
'file' => 'visa-light.svg',
),
array(
'title' => 'MasterCard',
'file' => 'mastercard-light.svg',
),
array(
'title' => 'Amex',
'file' => 'amex-light.svg',
),
array(
'title' => 'Discover',
'file' => 'discover-light.svg',
),
array(
'title' => 'Diners Club',
'file' => 'dinersclub-light.svg',
),
array(
'title' => 'JCB',
'file' => 'jcb-light.svg',
),
array(
'title' => 'UnionPay',
'file' => 'unionpay-light.svg',
),
);
},
/**
* The matrix which countries and currency combinations can be used for AXO.
*/
@ -188,10 +136,10 @@ return array(
},
'axo.smart-button-location-notice' => static function ( ContainerInterface $container ) : string {
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$dcc_configuration = $container->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );
if ( $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ) ) {
if ( $dcc_configuration->use_fastlane() ) {
$fastlane_settings_url = admin_url(
sprintf(
'admin.php?page=wc-settings&tab=checkout&section=%1$s&ppcp-tab=%2$s#field-axo_heading',

View file

@ -28,6 +28,9 @@ 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;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;
/**
* Class AxoModule
*/
@ -85,13 +88,10 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
return $methods;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$dcc_configuration = $c->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );
$is_paypal_enabled = $settings->has( 'enabled' ) && $settings->get( 'enabled' ) ?? false;
$is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false;
if ( ! $is_paypal_enabled || ! $is_dcc_enabled ) {
if ( ! $dcc_configuration->is_enabled() ) {
return $methods;
}
@ -130,6 +130,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',
@ -137,11 +154,11 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
$listener = $c->get( 'wcgateway.settings.listener' );
assert( $listener instanceof SettingsListener );
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$dcc_configuration = $c->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );
$listener->filter_settings(
$settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ),
$dcc_configuration->use_fastlane(),
'smart_button_locations',
function( array $existing_setting_value ) {
$axo_forced_locations = array( 'cart-block', 'cart' );
@ -218,12 +235,10 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
echo '<script async src="https://www.paypalobjects.com/insights/v1/paypal-insights.sandbox.min.js"></script>';
// Add meta tag to allow feature-detection of the site's AXO payment state.
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$dcc_configuration = $c->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );
$this->add_feature_detection_tag(
$settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' )
);
$this->add_feature_detection_tag( $dcc_configuration->use_fastlane() );
}
);
@ -345,16 +360,12 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
* @return bool
*/
private function should_render_fastlane( ContainerInterface $c ): bool {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$is_axo_enabled = $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' ) ?? false;
$is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false;
$dcc_configuration = $c->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );
return ! is_user_logged_in()
&& CartCheckoutDetector::has_classic_checkout()
&& $is_axo_enabled
&& $is_dcc_enabled
&& $dcc_configuration->use_fastlane()
&& ! $this->is_excluded_endpoint();
}

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,16 @@ 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;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;
/**
* Class AXOGateway.
*/
class AxoGateway extends WC_Payment_Gateway {
use OrderMetaTrait, GatewaySettingsRendererTrait;
use OrderMetaTrait, GatewaySettingsRendererTrait, ProcessPaymentTrait;
const ID = 'ppcp-axo-gateway';
@ -49,6 +53,13 @@ class AxoGateway extends WC_Payment_Gateway {
*/
protected $ppcp_settings;
/**
* Gateway configuration object, providing relevant settings.
*
* @var DCCGatewayConfiguration
*/
protected DCCGatewayConfiguration $dcc_configuration;
/**
* The WcGateway module URL.
*
@ -70,13 +81,6 @@ class AxoGateway extends WC_Payment_Gateway {
*/
protected $card_icons;
/**
* The AXO card icons.
*
* @var array
*/
protected $card_icons_axo;
/**
* The order endpoint.
*
@ -119,15 +123,23 @@ 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 DCCGatewayConfiguration $dcc_configuration The DCC Gateway configuration.
* @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 array $card_icons 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.
@ -138,10 +150,11 @@ class AxoGateway extends WC_Payment_Gateway {
public function __construct(
SettingsRenderer $settings_renderer,
ContainerInterface $ppcp_settings,
DCCGatewayConfiguration $dcc_configuration,
string $wcgateway_module_url,
SessionHandler $session_handler,
OrderProcessor $order_processor,
array $card_icons,
array $card_icons_axo,
OrderEndpoint $order_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
ShippingPreferenceFactory $shipping_preference_factory,
@ -153,20 +166,19 @@ class AxoGateway extends WC_Payment_Gateway {
$this->settings_renderer = $settings_renderer;
$this->ppcp_settings = $ppcp_settings;
$this->dcc_configuration = $dcc_configuration;
$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;
$this->method_title = __( 'Fastlane Debit & Credit Cards', 'woocommerce-paypal-payments' );
$this->method_description = __( 'Fastlane accelerates the checkout experience for guest shoppers and autofills their details so they can pay in seconds. When enabled, Fastlane is presented as the default payment method for guests.', 'woocommerce-paypal-payments' );
$is_axo_enabled = $this->ppcp_settings->has( 'axo_enabled' ) && $this->ppcp_settings->get( 'axo_enabled' );
$is_axo_enabled = $this->dcc_configuration->use_fastlane();
$this->update_option( 'enabled', $is_axo_enabled ? 'yes' : 'no' );
$this->title = $this->ppcp_settings->has( 'axo_gateway_title' )
? $this->ppcp_settings->get( 'axo_gateway_title' )
: $this->get_option( 'title', $this->method_title );
$this->title = $this->dcc_configuration->gateway_title( $this->get_option( 'title', $this->method_title ) );
$this->description = __( 'Enter your email address above to continue.', 'woocommerce-paypal-payments' );
@ -213,80 +225,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
);
}
/**
@ -295,16 +310,10 @@ class AxoGateway extends WC_Payment_Gateway {
* @return string
*/
public function get_icon() {
$icon = parent::get_icon();
$icons = $this->card_icons;
$icons_src = esc_url( $this->wcgateway_module_url ) . 'assets/images/';
$icon = parent::get_icon();
$icons = $this->card_icons;
if ( $this->card_icons_axo ) {
$icons = $this->card_icons_axo;
$icons_src = esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/';
}
if ( empty( $this->card_icons ) ) {
if ( ! $icons ) {
return $icon;
}
@ -313,8 +322,8 @@ class AxoGateway extends WC_Payment_Gateway {
foreach ( $icons as $card ) {
$images[] = '<img
class="ppcp-card-icon"
title="' . $card['title'] . '"
src="' . $icons_src . $card['file'] . '"
title="' . esc_attr( $card['title'] ) . '"
src="' . esc_url( $card['url'] ) . '"
> ';
}
@ -328,7 +337,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 +371,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

@ -14,18 +14,6 @@ namespace WooCommerce\PayPalCommerce\Axo\Helper;
*/
class PropertiesDictionary {
/**
* Returns the list of possible privacy options.
*
* @return array
*/
public static function privacy_options(): array {
return array(
'yes' => __( 'Yes (Recommended)', 'woocommerce-paypal-payments' ),
'no' => __( 'No', 'woocommerce-paypal-payments' ),
);
}
/**
* Returns the list of possible cardholder name options.
*

View file

@ -62,7 +62,7 @@ class SettingsNoticeGenerator {
public function generate_checkout_notice(): string {
$checkout_page_link = esc_url( get_edit_post_link( wc_get_page_id( 'checkout' ) ) ?? '' );
$block_checkout_docs_link = __(
'https://woocommerce.com/document/cart-checkout-blocks-status/#reverting-to-the-cart-and-checkout-shortcodes',
'https://woocommerce.com/document/woocommerce-store-editing/customizing-cart-and-checkout/#using-the-cart-and-checkout-blocks',
'woocommerce-paypal-payments'
);
@ -72,27 +72,17 @@ class SettingsNoticeGenerator {
$notice_content = sprintf(
/* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store currently uses the <code>Elementor Checkout widget</code>. To enable Fastlane and accelerate payments, the page must include either the <code>Classic Checkout</code> or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> for instructions on how to switch to the classic layout.',
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store currently uses the <code>Elementor Checkout widget</code>. To enable Fastlane and accelerate payments, the page must include either the <code>Checkout</code> block, <code>Classic Checkout</code>, or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> for instructions on how to switch to the Checkout block.',
'woocommerce-paypal-payments'
),
esc_url( $checkout_page_link ),
esc_url( $block_checkout_docs_link )
);
} elseif ( CartCheckoutDetector::has_block_checkout() ) {
} elseif ( ! CartCheckoutDetector::has_classic_checkout() && ! CartCheckoutDetector::has_block_checkout() ) {
$notice_content = sprintf(
/* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store currently uses the WooCommerce <code>Checkout</code> block. To enable Fastlane and accelerate payments, the page must include either the <code>Classic Checkout</code> or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> for instructions on how to switch to the classic layout.',
'woocommerce-paypal-payments'
),
esc_url( $checkout_page_link ),
esc_url( $block_checkout_docs_link )
);
} elseif ( ! CartCheckoutDetector::has_classic_checkout() ) {
$notice_content = sprintf(
/* translators: %1$s: URL to the Checkout edit page. %2$s: URL to the block checkout docs. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store does not seem to be properly configured or uses an incompatible <code>third-party Checkout</code> solution. To enable Fastlane and accelerate payments, the page must include either the <code>Classic Checkout</code> or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> for instructions on how to switch to the classic layout.',
'<span class="highlight">Warning:</span> The <a href="%1$s">Checkout page</a> of your store does not seem to be properly configured or uses an incompatible <code>third-party Checkout</code> solution. To enable Fastlane and accelerate payments, the page must include either the <code>Checkout</code> block, <code>Classic Checkout</code>, or the <code>[woocommerce_checkout]</code> shortcode. See <a href="%2$s">this page</a> for instructions on how to switch to the Checkout block.',
'woocommerce-paypal-payments'
),
esc_url( $checkout_page_link ),

View file

@ -19,6 +19,7 @@
"@babel/core": "^7.19",
"@babel/preset-env": "^7.19",
"@babel/preset-react": "^7.18.6",
"@wordpress/i18n": "^5.6.0",
"@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",

View file

@ -3,6 +3,7 @@ import {
registerExpressPaymentMethod,
registerPaymentMethod,
} from '@woocommerce/blocks-registry';
import { __ } from '@wordpress/i18n';
import {
mergeWcAddress,
paypalAddressToWc,
@ -775,6 +776,12 @@ if ( block_enabled && config.enabled ) {
] ) {
registerExpressPaymentMethod( {
name: `${ config.id }-${ fundingSource }`,
title: 'PayPal',
description: __(
'Eligible users will see the PayPal button.',
'woocommerce-paypal-payments'
),
gatewayId: 'ppcp-gateway',
paymentMethodId: config.id,
label: (
<div dangerouslySetInnerHTML={ { __html: config.title } } />

View file

@ -68,7 +68,11 @@ class BlocksModule implements ServiceModule, ExtendingModule, ExecutableModule {
'woocommerce_blocks_payment_method_type_registration',
function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void {
$payment_method_registry->register( $c->get( 'blocks.method' ) );
$payment_method_registry->register( $c->get( 'blocks.advanced-card-method' ) );
// Include ACDC in the Block Checkout only in case Axo doesn't exist or is not available or the user is logged in.
if ( ! $c->has( 'axoblock.available' ) || ! $c->get( 'axoblock.available' ) || is_user_logged_in() ) {
$payment_method_registry->register( $c->get( 'blocks.advanced-card-method' ) );
}
}
);

View file

@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;
/**
* Class CardFieldsModule
@ -45,9 +46,9 @@ class CardFieldsModule implements ServiceModule, ExtendingModule, ExecutableModu
return true;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
if ( ! $settings->has( 'dcc_enabled' ) || ! $settings->get( 'dcc_enabled' ) ) {
$dcc_configuration = $c->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );
if ( ! $dcc_configuration->is_enabled() ) {
return true;
}

View file

@ -17,6 +17,7 @@
"@babel/core": "^7.19",
"@babel/preset-env": "^7.19",
"@babel/preset-react": "^7.18.6",
"@wordpress/i18n": "^5.6.0",
"@woocommerce/dependency-extraction-webpack-plugin": "^2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",

View file

@ -3,6 +3,7 @@ import {
registerExpressPaymentMethod,
registerPaymentMethod,
} from '@woocommerce/blocks-registry';
import { __ } from '@wordpress/i18n';
import { loadPaypalScript } from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading';
import GooglepayManager from './GooglepayManager';
import { loadCustomScript } from '@paypal/paypal-js';
@ -58,6 +59,12 @@ const features = [ 'products' ];
registerExpressPaymentMethod( {
name: buttonData.id,
title: `PayPal - ${ buttonData.title }`,
description: __(
'Eligible users will see the PayPal button.',
'woocommerce-paypal-payments'
),
gatewayId: 'ppcp-gateway',
label: <div dangerouslySetInnerHTML={ { __html: buttonData.title } } />,
content: <GooglePayComponent isEditing={ false } />,
edit: <GooglePayComponent isEditing={ true } />,

View file

@ -431,10 +431,20 @@ class Button implements ButtonInterface {
* @return array
*/
public function script_data(): array {
$use_shipping_form = $this->settings->has( 'googlepay_button_shipping_enabled' ) && $this->settings->get( 'googlepay_button_shipping_enabled' );
// On the product page, only show the shipping form for physical products.
$context = $this->context();
if ( $use_shipping_form && 'product' === $context ) {
$product = wc_get_product();
if ( ! $product || $product->is_downloadable() || $product->is_virtual() ) {
$use_shipping_form = false;
}
}
$shipping = array(
'enabled' => $this->settings->has( 'googlepay_button_shipping_enabled' )
? boolval( $this->settings->get( 'googlepay_button_shipping_enabled' ) )
: false,
'enabled' => $use_shipping_form,
'configured' => wc_shipping_enabled() && wc_get_shipping_method_count( false, true ) > 0,
);

View file

@ -78,7 +78,9 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\SectionsRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\Axo\Helper\PropertiesDictionary;
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;
return array(
'wcgateway.paypal-gateway' => static function ( ContainerInterface $container ): PayPalGateway {
@ -123,6 +125,7 @@ return array(
$order_processor = $container->get( 'wcgateway.order-processor' );
$settings_renderer = $container->get( 'wcgateway.settings.render' );
$settings = $container->get( 'wcgateway.settings' );
$dcc_configuration = $container->get( 'wcgateway.configuration.dcc' );
$module_url = $container->get( 'wcgateway.url' );
$session_handler = $container->get( 'session.handler' );
$refund_processor = $container->get( 'wcgateway.processor.refunds' );
@ -132,10 +135,14 @@ return array(
$payments_endpoint = $container->get( 'api.endpoint.payments' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
$vaulted_credit_card_handler = $container->get( 'vaulting.credit-card-handler' );
$icons = $container->get( 'wcgateway.credit-card-icons' );
return new CreditCardGateway(
$settings_renderer,
$order_processor,
$settings,
$dcc_configuration,
$icons,
$module_url,
$session_handler,
$refund_processor,
@ -153,6 +160,68 @@ return array(
$logger
);
},
'wcgateway.credit-card-labels' => static function ( ContainerInterface $container ) : array {
return array(
'visa' => _x(
'Visa',
'Name of credit card',
'woocommerce-paypal-payments'
),
'mastercard' => _x(
'Mastercard',
'Name of credit card',
'woocommerce-paypal-payments'
),
'amex' => _x(
'American Express',
'Name of credit card',
'woocommerce-paypal-payments'
),
'discover' => _x(
'Discover',
'Name of credit card',
'woocommerce-paypal-payments'
),
'jcb' => _x(
'JCB',
'Name of credit card',
'woocommerce-paypal-payments'
),
'elo' => _x(
'Elo',
'Name of credit card',
'woocommerce-paypal-payments'
),
'hiper' => _x(
'Hiper',
'Name of credit card',
'woocommerce-paypal-payments'
),
);
},
'wcgateway.credit-card-icons' => static function ( ContainerInterface $container ) : array {
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$icons = $settings->has( 'card_icons' ) ? (array) $settings->get( 'card_icons' ) : array();
$labels = $container->get( 'wcgateway.credit-card-labels' );
$module_url = $container->get( 'wcgateway.url' );
$url_root = esc_url( $module_url ) . 'assets/images/';
$icons_with_label = array();
foreach ( $icons as $icon ) {
$type = str_replace( '-dark', '', $icon );
$icons_with_label[] = array(
'type' => $type,
'title' => ucwords( $labels[ $type ] ?? $type ),
'url' => "$url_root/$icon.svg",
);
}
return $icons_with_label;
},
'wcgateway.card-button-gateway' => static function ( ContainerInterface $container ): CardButtonGateway {
return new CardButtonGateway(
$container->get( 'wcgateway.settings.render' ),
@ -315,9 +384,10 @@ return array(
assert( $settings instanceof Settings );
$axo_available = $container->has( 'axo.available' ) && $container->get( 'axo.available' );
$axo_enabled = $settings->has( 'axo_enabled' ) && $settings->get( 'axo_enabled' );
$dcc_configuration = $container->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );
if ( $axo_available && $axo_enabled ) {
if ( $axo_available && $dcc_configuration->use_fastlane() ) {
return '';
}
@ -555,6 +625,9 @@ return array(
$subscription_helper = $container->get( 'wc-subscriptions.helper' );
assert( $subscription_helper instanceof SubscriptionHelper );
$dcc_configuration = $container->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );
$fields = array(
'checkout_settings_heading' => array(
'heading' => __( 'Standard Payments Settings', 'woocommerce-paypal-payments' ),
@ -910,6 +983,20 @@ return array(
),
'gateway' => 'dcc',
),
'dcc_name_on_card' => array(
'title' => __( 'Cardholder Name', 'woocommerce-paypal-payments' ),
'type' => 'select',
'default' => $dcc_configuration->show_name_on_card(),
'options' => PropertiesDictionary::cardholder_name_options(),
'classes' => array(),
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'desc_tip' => true,
'description' => __( 'This setting will control whether or not the cardholder name is displayed in the card field\'s UI.', 'woocommerce-paypal-payments' ),
'screens' => array( State::STATE_ONBOARDED ),
'gateway' => array( 'dcc', 'axo' ),
'requirements' => array( 'axo' ),
),
'3d_secure_heading' => array(
'heading' => __( '3D Secure', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading',
@ -1213,6 +1300,13 @@ return array(
return new TransactionUrlProvider( $sandbox_url_base, $live_url_base );
},
'wcgateway.configuration.dcc' => static function ( ContainerInterface $container ) : DCCGatewayConfiguration {
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
return new DCCGatewayConfiguration( $settings );
},
'wcgateway.helper.dcc-product-status' => static function ( ContainerInterface $container ) : DCCProductStatus {
$settings = $container->get( 'wcgateway.settings' );
@ -1786,7 +1880,7 @@ return array(
$list_of_config = array();
if ( $container->get( 'paylater-configurator.is-available' ) ) {
if ( $container->has( 'paylater-configurator.is-available' ) && $container->get( 'paylater-configurator.is-available' ) ) {
$list_of_config[] = array(
'id' => 'pay-later-messaging-task',
'title' => __( 'Configure PayPal Pay Later messaging', 'woocommerce-paypal-payments' ),

View file

@ -35,6 +35,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;
/**
* Class CreditCardGateway
@ -59,6 +60,13 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
*/
protected $order_processor;
/**
* The card icons.
*
* @var array
*/
protected $card_icons;
/**
* The settings.
*
@ -66,6 +74,13 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
*/
protected $config;
/**
* The DCC Gateway Configuration.
*
* @var DCCGatewayConfiguration
*/
protected DCCGatewayConfiguration $dcc_configuration;
/**
* The vaulted credit card handler.
*
@ -184,6 +199,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
* @param SettingsRenderer $settings_renderer The Settings Renderer.
* @param OrderProcessor $order_processor The Order processor.
* @param ContainerInterface $config The settings.
* @param DCCGatewayConfiguration $dcc_configuration The DCC Gateway Configuration.
* @param array $card_icons The card icons.
* @param string $module_url The URL to the module.
* @param SessionHandler $session_handler The Session Handler.
* @param RefundProcessor $refund_processor The refund processor.
@ -204,6 +221,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
SettingsRenderer $settings_renderer,
OrderProcessor $order_processor,
ContainerInterface $config,
DCCGatewayConfiguration $dcc_configuration,
array $card_icons,
string $module_url,
SessionHandler $session_handler,
RefundProcessor $refund_processor,
@ -224,6 +243,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
$this->settings_renderer = $settings_renderer;
$this->order_processor = $order_processor;
$this->config = $config;
$this->dcc_configuration = $dcc_configuration;
$this->module_url = $module_url;
$this->session_handler = $session_handler;
$this->refund_processor = $refund_processor;
@ -260,10 +280,9 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
'Accept debit and credit cards, and local payment methods with PayPals latest solution.',
'woocommerce-paypal-payments'
);
$this->title = $this->config->has( 'dcc_gateway_title' ) ?
$this->config->get( 'dcc_gateway_title' ) : $this->method_title;
$this->description = $this->config->has( 'dcc_gateway_description' ) ?
$this->config->get( 'dcc_gateway_description' ) : $this->method_description;
$this->title = $this->dcc_configuration->gateway_title();
$this->description = $this->dcc_configuration->gateway_description();
$this->card_icons = $card_icons;
$this->init_form_fields();
$this->init_settings();
@ -339,74 +358,26 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
* @return string
*/
public function get_icon() {
$icon = parent::get_icon();
$icon = parent::get_icon();
$icons = $this->card_icons;
$icons = $this->config->has( 'card_icons' ) ? (array) $this->config->get( 'card_icons' ) : array();
if ( empty( $icons ) ) {
if ( ! $icons ) {
return $icon;
}
$title_options = $this->card_labels();
$images = array_map(
function ( string $type ) use ( $title_options ): string {
$striped_dark = str_replace( '-dark', '', $type );
return '<img
title="' . esc_attr( $title_options[ $striped_dark ] ) . '"
src="' . esc_url( $this->module_url ) . 'assets/images/' . esc_attr( $type ) . '.svg"
class="ppcp-card-icon"
> ';
},
$icons
);
$images = array();
foreach ( $icons as $card ) {
$images[] = '<img
class="ppcp-card-icon"
title="' . esc_attr( $card['title'] ) . '"
src="' . esc_url( $card['url'] ) . '"
> ';
}
return implode( '', $images );
}
/**
* Returns an array of credit card names.
*
* @return array
*/
private function card_labels(): array {
return array(
'visa' => _x(
'Visa',
'Name of credit card',
'woocommerce-paypal-payments'
),
'mastercard' => _x(
'Mastercard',
'Name of credit card',
'woocommerce-paypal-payments'
),
'amex' => _x(
'American Express',
'Name of credit card',
'woocommerce-paypal-payments'
),
'discover' => _x(
'Discover',
'Name of credit card',
'woocommerce-paypal-payments'
),
'jcb' => _x(
'JCB',
'Name of credit card',
'woocommerce-paypal-payments'
),
'elo' => _x(
'Elo',
'Name of credit card',
'woocommerce-paypal-payments'
),
'hiper' => _x(
'Hiper',
'Name of credit card',
'woocommerce-paypal-payments'
),
);
}
/**
* Whether the gateway is available or not.
*
@ -682,6 +653,8 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
$this->config->set( 'dcc_enabled', 'yes' === $value );
$this->config->persist();
$this->dcc_configuration->refresh();
return true;
}
@ -694,7 +667,7 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
* @return bool
*/
private function is_enabled(): bool {
return $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' );
return $this->dcc_configuration->is_enabled();
}
/**

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;

View file

@ -0,0 +1,215 @@
<?php
/**
* Encapsulates all configuration details for "Credit & Debit Card" gateway.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Helper
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Helper;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\Axo\Helper\PropertiesDictionary;
/**
* A simple DTO that provides access to the DCC/AXO gateway settings.
*
* This class should not implement business logic, but only provide a convenient
* way to access gateway settings by wrapping the Settings instance.
*/
class DCCGatewayConfiguration {
/**
* The plugin settings instance.
*
* @var Settings
*/
private Settings $settings;
/**
* Whether the Credit Card gateway is enabled.
*
* @var bool
*/
private bool $is_enabled = false;
/**
* Whether to use the Fastlane UI.
*
* @var bool
*/
private bool $use_fastlane = false;
/**
* Gateway title.
*
* @var string
*/
private string $gateway_title = '';
/**
* Gateway description.
*
* @var string
*/
private string $gateway_description = '';
/**
* Whether to display the cardholder's name on the payment form.
*
* @var string
*/
private string $show_name_on_card = 'no';
/**
* Whether the Fastlane watermark should be hidden on the front-end.
*
* @var bool
*/
private bool $hide_fastlane_watermark = false;
/**
* Initializes the gateway details based on the provided Settings instance.
*
* @throws NotFoundException If an expected gateway setting is not found.
*
* @param Settings $settings Plugin settings instance.
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
$this->refresh();
}
/**
* Refreshes the gateway configuration based on the current settings.
*
* This method should be used sparingly, usually only on the settings page
* when changes in gateway settings must be reflected immediately.
*
* @throws NotFoundException If an expected gateway setting is not found.
*/
public function refresh() : void {
$is_paypal_enabled = $this->settings->has( 'enabled' )
&& filter_var( $this->settings->get( 'enabled' ), FILTER_VALIDATE_BOOLEAN );
$is_dcc_enabled = $this->settings->has( 'dcc_enabled' )
&& filter_var( $this->settings->get( 'dcc_enabled' ), FILTER_VALIDATE_BOOLEAN );
$is_axo_enabled = $this->settings->has( 'axo_enabled' )
&& filter_var( $this->settings->get( 'axo_enabled' ), FILTER_VALIDATE_BOOLEAN );
$this->is_enabled = $is_paypal_enabled && $is_dcc_enabled;
$this->use_fastlane = $this->is_enabled && $is_axo_enabled;
$this->gateway_title = $this->settings->has( 'dcc_gateway_title' ) ?
$this->settings->get( 'dcc_gateway_title' ) : '';
$this->gateway_description = $this->settings->has( 'dcc_gateway_description' ) ?
$this->settings->get( 'dcc_gateway_description' ) : '';
$show_on_card = '';
if ( $this->settings->has( 'dcc_name_on_card' ) ) {
$show_on_card = $this->settings->get( 'dcc_name_on_card' );
} elseif ( $this->settings->has( 'axo_name_on_card' ) ) {
// Legacy. The AXO gateway setting was replaced by the DCC setting.
$show_on_card = $this->settings->get( 'axo_name_on_card' );
}
$valid_options = array_keys( PropertiesDictionary::cardholder_name_options() );
$this->show_name_on_card = in_array( $show_on_card, $valid_options, true )
? $show_on_card
: $valid_options[0];
/**
* Moved from setting "axo_privacy" to a hook-only filter:
* Changing this to true (and hiding the watermark) has potential legal
* consequences, and therefore is generally discouraged.
*/
$this->hide_fastlane_watermark = add_filter(
'woocommerce_paypal_payments_fastlane_watermark_enabled',
'__return_false'
);
}
/**
* Whether the Credit Card gateway is enabled.
*
* Requires PayPal features to be enabled.
*
* @return bool
* @todo Some classes still directly access `$settings->get('dcc_enabled')`
*/
public function is_enabled() : bool {
return $this->is_enabled;
}
/**
* Whether to prefer Fastlane instead of the default Credit Card UI, if
* available in the shop's region.
*
* Requires PayPal features and the Credit Card gateway to be enabled.
*
* @return bool
*/
public function use_fastlane() : bool {
return $this->use_fastlane;
}
/**
* User facing title of the gateway.
*
* @param string $fallback Fallback title if the gateway title is not set.
*
* @return string Display title of the gateway.
*/
public function gateway_title( string $fallback = '' ) : string {
if ( $this->gateway_title ) {
return $this->gateway_title;
}
return $fallback ?: __( 'Advanced Card Processing', 'woocommerce-paypal-payments' );
}
/**
* Descriptive text to display on the frontend.
*
* @param string $fallback Fallback description if the gateway description is not set.
*
* @return string Display description of the gateway.
*/
public function gateway_description( string $fallback = '' ) : string {
if ( $this->gateway_description ) {
return $this->gateway_description;
}
return $fallback ?: __(
'Accept debit and credit cards, and local payment methods with PayPals latest solution.',
'woocommerce-paypal-payments'
);
}
/**
* Whether to show a field for the cardholder's name in the payment form.
*
* Note, that this getter returns a string (not a boolean) because the
* setting is integrated as a select-list, not a toggle or checkbox.
*
* @return string ['yes'|'no']
*/
public function show_name_on_card() : string {
return $this->show_name_on_card;
}
/**
* Whether to display the watermark (text branding) for the Fastlane payment
* method in the front end.
*
* Note: This setting is planned but not implemented yet.
*
* @retun bool True means, the default watermark is displayed to customers.
*/
public function show_fastlane_watermark() : bool {
return ! $this->hide_fastlane_watermark;
}
}

View file

@ -57,6 +57,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\WcTasks\Registrar\TaskRegistrarInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;
/**
* Class WcGatewayModule
@ -181,6 +182,9 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$dcc_configuration = $c->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );
$assets = new SettingsPageAssets(
$c->get( 'wcgateway.url' ),
$c->get( 'ppcp.asset-version' ),
@ -193,7 +197,7 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
$settings->has( 'disable_funding' ) ? $settings->get( 'disable_funding' ) : array(),
$c->get( 'wcgateway.settings.funding-sources' ),
$c->get( 'wcgateway.is-ppcp-settings-page' ),
$settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ),
$dcc_configuration->is_enabled(),
$c->get( 'api.endpoint.billing-agreements' ),
$c->get( 'wcgateway.is-ppcp-settings-payment-methods-page' )
);
@ -554,10 +558,12 @@ class WCGatewayModule implements ServiceModule, ExtendingModule, ExecutableModul
return $methods;
}
$is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false;
$dcc_configuration = $container->get( 'wcgateway.configuration.dcc' );
assert( $dcc_configuration instanceof DCCGatewayConfiguration );
$standard_card_button = get_option( 'woocommerce_ppcp-card-button-gateway_settings' );
if ( $is_dcc_enabled && isset( $standard_card_button['enabled'] ) ) {
if ( $dcc_configuration->is_enabled() && isset( $standard_card_button['enabled'] ) ) {
$standard_card_button['enabled'] = 'no';
update_option( 'woocommerce_ppcp-card-button-gateway_settings', $standard_card_button );
}

32856
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "woocommerce-paypal-payments",
"version": "2.9.1",
"version": "2.9.2",
"description": "WooCommerce PayPal Payments",
"repository": "https://github.com/woocommerce/woocommerce-paypal-payments",
"license": "GPL-2.0",
@ -22,6 +22,7 @@
"install:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn install",
"install:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn install",
"install:modules:ppcp-axo": "cd modules/ppcp-axo && yarn install",
"install:modules:ppcp-axo-block": "cd modules/ppcp-axo-block && yarn install",
"install:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn install",
"install:modules:ppcp-card-fields": "cd modules/ppcp-card-fields && yarn install",
"install:modules:ppcp-compat": "cd modules/ppcp-compat && yarn install",
@ -40,6 +41,7 @@
"build:modules:ppcp-order-tracking": "cd modules/ppcp-order-tracking && yarn run build",
"build:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn run build",
"build:modules:ppcp-axo": "cd modules/ppcp-axo && yarn run build",
"build:modules:ppcp-axo-block": "cd modules/ppcp-axo-block && yarn run build",
"build:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn run build",
"build:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn run build",
"build:modules:ppcp-card-fields": "cd modules/ppcp-card-fields && yarn run build",
@ -61,6 +63,7 @@
"watch:modules:ppcp-paypal-subscriptions": "cd modules/ppcp-paypal-subscriptions && yarn run watch",
"watch:modules:ppcp-save-payment-methods": "cd modules/ppcp-save-payment-methods && yarn run watch",
"watch:modules:ppcp-axo": "cd modules/ppcp-axo && yarn run watch",
"watch:modules:ppcp-axo-block": "cd modules/ppcp-axo-block && yarn run watch",
"watch:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn run watch",
"watch:modules:ppcp-card-fields": "cd modules/ppcp-card-fields && yarn run watch",
"watch:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run watch",

View file

@ -4,7 +4,7 @@ Tags: woocommerce, paypal, payments, ecommerce, credit card
Requires at least: 6.3
Tested up to: 6.6
Requires PHP: 7.4
Stable tag: 2.9.1
Stable tag: 2.9.2
License: GPLv2
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@ -179,7 +179,11 @@ If you encounter issues with the PayPal buttons not appearing after an update, p
== Changelog ==
= 2.9.1 - xxxx-xx-xx =
= 2.9.2 - xxxx-xx-xx =
* Enhancement - Add Fastlane support for Classic Checkout
* Fix - Fatal error when Pay Later messaging configurator was disabled with a code snippet
= 2.9.1 - 2024-09-24 =
* Fix - Improve card fields hiding #2574
* Fix - Google Pay: Shipping callback not calculating totals correctly on Single Product page #2513
* Fix - Fix shipping callback condition in status report #2578
@ -194,6 +198,8 @@ If you encounter issues with the PayPal buttons not appearing after an update, p
* Enhancement - Require PHP 7.4+, WP 6.3+, WC 6.9+ #2556
* Enhancement - Modularity module migration #1944
* Enhancement - Keep only 5 tags in readme.txt #2562
* Enhancement - Select ACDC by default during onboarding for China store locations #2619
* Enhancement - Add title, description and gatewayId to the express payment method #2566
= 2.9.0 - 2024-09-02 =
* Fix - Fatal error in Block Editor when using WooCommerce blocks #2534

View file

@ -20,6 +20,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Endpoint\CaptureCardPayment;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Helper\DCCGatewayConfiguration;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
use function Brain\Monkey\Functions\when;
@ -28,6 +29,8 @@ class CreditCardGatewayTest extends TestCase
private $settingsRenderer;
private $orderProcessor;
private $config;
private $dcc_configuration;
private $creditCardIcons;
private $moduleUrl;
private $sessionHandler;
private $refundProcessor;
@ -52,6 +55,8 @@ class CreditCardGatewayTest extends TestCase
$this->settingsRenderer = Mockery::mock(SettingsRenderer::class);
$this->orderProcessor = Mockery::mock(OrderProcessor::class);
$this->config = Mockery::mock(ContainerInterface::class);
$this->dcc_configuration = Mockery::mock(DCCGatewayConfiguration::class);
$this->creditCardIcons = [];
$this->moduleUrl = '';
$this->sessionHandler = Mockery::mock(SessionHandler::class);
$this->refundProcessor = Mockery::mock(RefundProcessor::class);
@ -72,12 +77,18 @@ class CreditCardGatewayTest extends TestCase
$this->config->shouldReceive('has')->andReturn(true);
$this->config->shouldReceive('get')->andReturn('');
$this->dcc_configuration->shouldReceive('is_enabled')->andReturn(true);
$this->dcc_configuration->shouldReceive('gateway_title')->andReturn('');
$this->dcc_configuration->shouldReceive('gateway_description')->andReturn('');
when('wc_clean')->returnArg();
$this->testee = new CreditCardGateway(
$this->settingsRenderer,
$this->orderProcessor,
$this->config,
$this->dcc_configuration,
$this->creditCardIcons,
$this->moduleUrl,
$this->sessionHandler,
$this->refundProcessor,

View file

@ -3,7 +3,7 @@
* Plugin Name: WooCommerce PayPal Payments
* Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/
* Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage.
* Version: 2.9.1
* Version: 2.9.2
* Author: WooCommerce
* Author URI: https://woocommerce.com/
* License: GPL-2.0
@ -26,7 +26,7 @@ define( 'PAYPAL_API_URL', 'https://api-m.paypal.com' );
define( 'PAYPAL_URL', 'https://www.paypal.com' );
define( 'PAYPAL_SANDBOX_API_URL', 'https://api-m.sandbox.paypal.com' );
define( 'PAYPAL_SANDBOX_URL', 'https://www.sandbox.paypal.com' );
define( 'PAYPAL_INTEGRATION_DATE', '2024-09-16' );
define( 'PAYPAL_INTEGRATION_DATE', '2024-09-30' );
define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' );
! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' );