Merge branch 'trunk' of github.com:woocommerce/woocommerce-paypal-payments into PCP-3783-fastlane-allow-merchants-to-disable-specific-card-types

This commit is contained in:
Daniel Dudzic 2024-11-12 15:03:35 +01:00
commit f3c32dbe6c
No known key found for this signature in database
GPG key ID: 31B40D33E3465483
270 changed files with 48005 additions and 70038 deletions

View file

@ -14,6 +14,7 @@ nfs_mount_enabled: false
mutagen_enabled: false mutagen_enabled: false
use_dns_when_possible: true use_dns_when_possible: true
composer_version: "2" composer_version: "2"
nodejs_version: "22"
hooks: hooks:
pre-start: pre-start:
- exec-host: "mkdir -p .ddev/wordpress/wp-content/plugins/${DDEV_PROJECT}" - exec-host: "mkdir -p .ddev/wordpress/wp-content/plugins/${DDEV_PROJECT}"

View file

@ -30,6 +30,7 @@ jobs:
uses: inpsyde/reusable-workflows/.github/workflows/build-plugin-archive.yml@main uses: inpsyde/reusable-workflows/.github/workflows/build-plugin-archive.yml@main
with: with:
PHP_VERSION: 7.4 PHP_VERSION: 7.4
NODE_VERSION: 22
PLUGIN_MAIN_FILE: ./woocommerce-paypal-payments.php PLUGIN_MAIN_FILE: ./woocommerce-paypal-payments.php
PLUGIN_VERSION: ${{ needs.check_version.outputs.version }} PLUGIN_VERSION: ${{ needs.check_version.outputs.version }}
PLUGIN_FOLDER_NAME: woocommerce-paypal-payments PLUGIN_FOLDER_NAME: woocommerce-paypal-payments

View file

@ -2120,3 +2120,40 @@ class WC_Product_Subscription_Variation extends WC_Product_Variation {}
* *
*/ */
class WC_Product_Variable_Subscription extends WC_Product_Variable {} class WC_Product_Variable_Subscription extends WC_Product_Variable {}
class WCS_Manual_Renewal_Manager {
/**
* Initalise the class and attach callbacks.
*/
public static function init() {
}
/**
* Adds the manual renewal settings.
*
* @since 4.0.0
* @param $settings The full subscription settings array.
* @return $settings.
*/
public static function add_settings( $settings ) {
}
/**
* Checks if manual renewals are required - automatic renewals are disabled.
*
* @since 4.0.0
* @return bool Weather manual renewal is required.
*/
public static function is_manual_renewal_required() {
}
/**
* Checks if manual renewals are enabled.
*
* @since 4.0.0
* @return bool Weather manual renewal is enabled.
*/
public static function is_manual_renewal_enabled() {
}
}

View file

@ -1,5 +1,27 @@
*** Changelog *** *** Changelog ***
= 2.9.4 - 2024-11-11 =
* Fix - Apple Pay button preview missing in Standard payment and Advanced Processing tabs #2755
* Fix - Set "Sold individually" only for subscription connected to PayPal #2710
* Fix - Ensure Google Pay button does not appear for subscriptions #2718
* Fix - PayPal Subscriptions API renewal order not created in WooCommerce #2612
* Fix - Apple Pay button disappears on Classic Checkout #2722
* Fix - Google Pay and Apple Pay as separate gateways does not show button when checkout remove from button locations #2756
* Fix - Add GW refund support for Apple Pay #2746
* Fix - PayPal Subscriptions cancel and suspend from Subscriptions list page does not work #2632
* Fix - Displaying of HTML tags in product title on choosing a product for tracking (2801) #2701
* Fix - Payment with OXXO cause continuation state for next payment #2702
* Fix - Fix problems with autoptimize plugin #2705
* Fix - Missing custom field PayPal Transaction Fee for OXXO #2700
* Enhancement - Add void button #2678
* Enhancement - Use basic redirect gateway when checkout smart buttons disabled #2714
* Enhancement - Receive button properties from the Checkout Block #2448
* Enhancement - Run PPEC\DeactivateNote query only in backend #2719
* Enhancement - Prevent plugin use for "Send only" countries #2721
* Enhancement - Do not add pay later button in editor #2570
* Enhancement - Axo: Remove the submit button when Fastlane is disabled #2720
* Enhancement - Sync the PayPal product page button state to Apple/Google Pay buttons, show alerts #2742
= 2.9.3 - 2024-10-15 = = 2.9.3 - 2024-10-15 =
* Fix - Multi-currency support #2667 * Fix - Multi-currency support #2667
* Fix - "0.00" amount in Google Pay for virtual products #2636 * Fix - "0.00" amount in Google Pay for virtual products #2636

View file

@ -90,5 +90,12 @@ return function ( string $root_dir ): iterable {
$modules[] = ( require "$modules_dir/ppcp-axo-block/module.php" )(); $modules[] = ( require "$modules_dir/ppcp-axo-block/module.php" )();
} }
if ( apply_filters(
'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled',
getenv( 'PCP_SETTINGS_ENABLED' ) === '1'
) ) {
$modules[] = ( require "$modules_dir/ppcp-settings/module.php" )();
}
return $modules; return $modules;
}; };

View file

@ -10,18 +10,18 @@
"Edge >= 14" "Edge >= 14"
], ],
"dependencies": { "dependencies": {
"core-js": "^3.25.0" "core-js": "^3.39"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

File diff suppressed because it is too large Load diff

View file

@ -572,7 +572,6 @@ return array(
'CZK', 'CZK',
'DKK', 'DKK',
'EUR', 'EUR',
'HKD',
'HUF', 'HUF',
'ILS', 'ILS',
'JPY', 'JPY',
@ -585,7 +584,6 @@ return array(
'PLN', 'PLN',
'GBP', 'GBP',
'RUB', 'RUB',
'SGD',
'SEK', 'SEK',
'CHF', 'CHF',
'THB', 'THB',
@ -606,7 +604,6 @@ return array(
'DKK', 'DKK',
'EUR', 'EUR',
'GBP', 'GBP',
'HKD',
'HUF', 'HUF',
'ILS', 'ILS',
'JPY', 'JPY',
@ -616,7 +613,6 @@ return array(
'PHP', 'PHP',
'PLN', 'PLN',
'SEK', 'SEK',
'SGD',
'THB', 'THB',
'TWD', 'TWD',
'USD', 'USD',
@ -642,7 +638,6 @@ return array(
'FR' => $default_currencies, 'FR' => $default_currencies,
'DE' => $default_currencies, 'DE' => $default_currencies,
'GR' => $default_currencies, 'GR' => $default_currencies,
'HK' => $default_currencies,
'HU' => $default_currencies, 'HU' => $default_currencies,
'IE' => $default_currencies, 'IE' => $default_currencies,
'IT' => $default_currencies, 'IT' => $default_currencies,
@ -660,7 +655,6 @@ return array(
'PT' => $default_currencies, 'PT' => $default_currencies,
'RO' => $default_currencies, 'RO' => $default_currencies,
'SK' => $default_currencies, 'SK' => $default_currencies,
'SG' => $default_currencies,
'SI' => $default_currencies, 'SI' => $default_currencies,
'ES' => $default_currencies, 'ES' => $default_currencies,
'SE' => $default_currencies, 'SE' => $default_currencies,

View file

@ -14,7 +14,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Token;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/** /**
* Class PayPalBearer * Class PayPalBearer
@ -28,7 +28,7 @@ class PayPalBearer implements Bearer {
/** /**
* The settings. * The settings.
* *
* @var Settings * @var ContainerInterface
*/ */
protected $settings; protected $settings;
@ -70,12 +70,12 @@ class PayPalBearer implements Bearer {
/** /**
* PayPalBearer constructor. * PayPalBearer constructor.
* *
* @param Cache $cache The cache. * @param Cache $cache The cache.
* @param string $host The host. * @param string $host The host.
* @param string $key The key. * @param string $key The key.
* @param string $secret The secret. * @param string $secret The secret.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
* @param Settings $settings The settings. * @param ContainerInterface $settings The settings.
*/ */
public function __construct( public function __construct(
Cache $cache, Cache $cache,
@ -83,7 +83,7 @@ class PayPalBearer implements Bearer {
string $key, string $key,
string $secret, string $secret,
LoggerInterface $logger, LoggerInterface $logger,
Settings $settings ContainerInterface $settings
) { ) {
$this->cache = $cache; $this->cache = $cache;
@ -136,8 +136,7 @@ class PayPalBearer implements Bearer {
$error = new RuntimeException( $error = new RuntimeException(
__( 'Could not create token.', 'woocommerce-paypal-payments' ) __( 'Could not create token.', 'woocommerce-paypal-payments' )
); );
$this->logger->log( $this->logger->warning(
'warning',
$error->getMessage(), $error->getMessage(),
array( array(
'args' => $args, 'args' => $args,

View file

@ -67,7 +67,7 @@ class BillingSubscriptions {
*/ */
public function suspend( string $id ):void { public function suspend( string $id ):void {
$data = array( $data = array(
'reason' => 'Suspended by customer', 'reason' => sprintf( 'Suspended by %s.', is_admin() ? 'merchant' : 'customer' ),
); );
$bearer = $this->bearer->bearer(); $bearer = $this->bearer->bearer();
@ -107,7 +107,7 @@ class BillingSubscriptions {
*/ */
public function activate( string $id ): void { public function activate( string $id ): void {
$data = array( $data = array(
'reason' => 'Reactivated by customer', 'reason' => sprintf( 'Reactivated by %s.', is_admin() ? 'merchant' : 'customer' ),
); );
$bearer = $this->bearer->bearer(); $bearer = $this->bearer->bearer();
@ -148,7 +148,7 @@ class BillingSubscriptions {
*/ */
public function cancel( string $id ): void { public function cancel( string $id ): void {
$data = array( $data = array(
'reason' => 'Cancelled by customer', 'reason' => sprintf( 'Cancelled by %s.', is_admin() ? 'merchant' : 'customer' ),
); );
$bearer = $this->bearer->bearer(); $bearer = $this->bearer->bearer();

View file

@ -58,7 +58,7 @@ class Cache {
* *
* @param string $key The key. * @param string $key The key.
*/ */
public function delete( string $key ) { public function delete( string $key ): void {
delete_transient( $this->prefix . $key ); delete_transient( $this->prefix . $key );
} }

View file

@ -0,0 +1,66 @@
<?php
/**
* An in-memory version of Cache.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
/**
* An in-memory version of Cache. The data is kept only within the class instance.
*/
class InMemoryCache extends Cache {
/**
* The in-memory storage.
*
* @var array<string, mixed>
*/
private array $data = array();
/**
* InMemoryCache constructor
*/
public function __construct() {
parent::__construct( '' );
}
/**
* Gets a value.
*
* @param string $key The key under which the value is stored.
*
* @return mixed
*/
public function get( string $key ) {
if ( ! array_key_exists( $key, $this->data ) ) {
return false;
}
return $this->data[ $key ];
}
/**
* Deletes a cache.
*
* @param string $key The key.
*/
public function delete( string $key ): void {
unset( $this->data[ $key ] );
}
/**
* Caches a value.
*
* @param string $key The key under which the value should be cached.
* @param mixed $value The value to cache.
* @param int $expiration Unused.
*
* @return bool
*/
public function set( string $key, $value, int $expiration = 0 ): bool {
$this->data[ $key ] = $value;
return true;
}
}

View file

@ -11,20 +11,20 @@
], ],
"dependencies": { "dependencies": {
"@paypal/paypal-js": "^6.0.0", "@paypal/paypal-js": "^6.0.0",
"core-js": "^3.25.0" "core-js": "^3.39"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.25",
"@woocommerce/dependency-extraction-webpack-plugin": "^2.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

View file

@ -57,7 +57,3 @@
} }
} }
} }
#ppc-button-ppcp-applepay {
display: none;
}

File diff suppressed because it is too large Load diff

View file

@ -1,54 +1,90 @@
import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher'; import buttonModuleWatcher from '../../../ppcp-button/resources/js/modules/ButtonModuleWatcher';
import ApplePayButton from './ApplepayButton'; import ApplePayButton from './ApplepayButton';
import ContextHandlerFactory from './Context/ContextHandlerFactory';
class ApplePayManager { class ApplePayManager {
#namespace = '';
#buttonConfig = null;
#ppcpConfig = null;
#applePayConfig = null;
#contextHandler = null;
#transactionInfo = null;
#buttons = [];
constructor( namespace, buttonConfig, ppcpConfig ) { constructor( namespace, buttonConfig, ppcpConfig ) {
this.namespace = namespace; this.#namespace = namespace;
this.buttonConfig = buttonConfig; this.#buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig; this.#ppcpConfig = ppcpConfig;
this.ApplePayConfig = null;
this.buttons = [];
buttonModuleWatcher.watchContextBootstrap( ( bootstrap ) => { this.onContextBootstrap = this.onContextBootstrap.bind( this );
const button = new ApplePayButton( buttonModuleWatcher.watchContextBootstrap( this.onContextBootstrap );
bootstrap.context,
bootstrap.handler,
buttonConfig,
ppcpConfig
);
this.buttons.push( button );
if ( this.ApplePayConfig ) {
button.init( this.ApplePayConfig );
}
} );
} }
init() { async onContextBootstrap( bootstrap ) {
( async () => { this.#contextHandler = ContextHandlerFactory.create(
await this.config(); bootstrap.context,
for ( const button of this.buttons ) { this.#buttonConfig,
button.init( this.ApplePayConfig ); this.#ppcpConfig,
} bootstrap.handler
} )(); );
const button = ApplePayButton.createButton(
bootstrap.context,
bootstrap.handler,
this.#buttonConfig,
this.#ppcpConfig,
this.#contextHandler
);
this.#buttons.push( button );
// Ensure ApplePayConfig is loaded before proceeding.
await this.init();
button.configure( this.#applePayConfig, this.#transactionInfo );
button.init();
} }
reinit() { async init() {
for ( const button of this.buttons ) { try {
button.reinit(); if ( ! this.#applePayConfig ) {
this.#applePayConfig = await window[ this.#namespace ]
.Applepay()
.config();
if ( ! this.#applePayConfig ) {
console.error( 'No ApplePayConfig received during init' );
}
}
if ( ! this.#transactionInfo ) {
this.#transactionInfo = await this.fetchTransactionInfo();
if ( ! this.#applePayConfig ) {
console.error( 'No transactionInfo found during init' );
}
}
} catch ( error ) {
console.error( 'Error during initialization:', error );
} }
} }
/** async fetchTransactionInfo() {
* Gets Apple Pay configuration of the PayPal merchant. try {
*/ if ( ! this.#contextHandler ) {
async config() { throw new Error( 'ContextHandler is not initialized' );
this.ApplePayConfig = await window[ this.namespace ] }
.Applepay() return await this.#contextHandler.transactionInfo();
.config(); } catch ( error ) {
console.error( 'Error fetching transaction info:', error );
throw error;
}
}
return this.ApplePayConfig; reinit() {
for ( const button of this.#buttons ) {
button.reinit();
}
} }
} }

View file

@ -1,37 +1,15 @@
import ApplePayButton from './ApplepayButton'; import ApplepayButton from './Block/components/ApplePayButton';
class ApplePayManagerBlockEditor { const ApplePayManagerBlockEditor = ( {
constructor( namespace, buttonConfig, ppcpConfig ) { namespace,
this.namespace = namespace; buttonConfig,
this.buttonConfig = buttonConfig; ppcpConfig,
this.ppcpConfig = ppcpConfig; } ) => (
<ApplepayButton
/* namespace={ namespace }
* On the front-end, the init method is called when a new button context was detected buttonConfig={ buttonConfig }
* via `buttonModuleWatcher`. In the block editor, we do not need to wait for the ppcpConfig={ ppcpConfig }
* context, but can initialize the button in the next event loop. />
*/ );
setTimeout( () => this.init() );
}
async init() {
try {
this.applePayConfig = await window[ this.namespace ]
.Applepay()
.config();
const button = new ApplePayButton(
this.ppcpConfig.context,
null,
this.buttonConfig,
this.ppcpConfig
);
button.init( this.applePayConfig );
} catch ( error ) {
console.error( 'Failed to initialize Apple Pay:', error );
}
}
}
export default ApplePayManagerBlockEditor; export default ApplePayManagerBlockEditor;

View file

@ -0,0 +1,51 @@
import { useState, useEffect } from '@wordpress/element';
import useApiToGenerateButton from '../hooks/useApiToGenerateButton';
import usePayPalScript from '../hooks/usePayPalScript';
import useApplepayScript from '../hooks/useApplepayScript';
import useApplepayConfig from '../hooks/useApplepayConfig';
const ApplepayButton = ( { namespace, buttonConfig, ppcpConfig } ) => {
const [ buttonHtml, setButtonHtml ] = useState( '' );
const [ buttonElement, setButtonElement ] = useState( null );
const [ componentFrame, setComponentFrame ] = useState( null );
const isPayPalLoaded = usePayPalScript( namespace, ppcpConfig );
const isApplepayLoaded = useApplepayScript(
componentFrame,
buttonConfig,
isPayPalLoaded
);
const applepayConfig = useApplepayConfig( namespace, isApplepayLoaded );
useEffect( () => {
if ( ! buttonElement ) {
return;
}
setComponentFrame( buttonElement.ownerDocument );
}, [ buttonElement ] );
const applepayButton = useApiToGenerateButton(
componentFrame,
namespace,
buttonConfig,
ppcpConfig,
applepayConfig
);
useEffect( () => {
if ( applepayButton ) {
setButtonHtml( applepayButton.outerHTML );
}
}, [ applepayButton ] );
return (
<div
ref={ setButtonElement }
dangerouslySetInnerHTML={ { __html: buttonHtml } }
/>
);
};
export default ApplepayButton;

View file

@ -0,0 +1,37 @@
import { useEffect, useState } from '@wordpress/element';
import useButtonStyles from './useButtonStyles';
const useApiToGenerateButton = (
componentDocument,
namespace,
buttonConfig,
ppcpConfig,
applepayConfig
) => {
const [ applepayButton, setApplepayButton ] = useState( null );
const buttonStyles = useButtonStyles( buttonConfig, ppcpConfig );
useEffect( () => {
if ( ! buttonConfig || ! applepayConfig ) {
return;
}
const button = document.createElement( 'apple-pay-button' );
button.setAttribute(
'buttonstyle',
buttonConfig.buttonColor || 'black'
);
button.setAttribute( 'type', buttonConfig.buttonType || 'pay' );
button.setAttribute( 'locale', buttonConfig.buttonLocale || 'en' );
setApplepayButton( button );
return () => {
setApplepayButton( null );
};
}, [ namespace, buttonConfig, ppcpConfig, applepayConfig, buttonStyles ] );
return applepayButton;
};
export default useApiToGenerateButton;

View file

@ -0,0 +1,26 @@
import { useState, useEffect } from '@wordpress/element';
const useApplepayConfig = ( namespace, isApplepayLoaded ) => {
const [ applePayConfig, setApplePayConfig ] = useState( null );
useEffect( () => {
const fetchConfig = async () => {
if ( ! isApplepayLoaded ) {
return;
}
try {
const config = await window[ namespace ].Applepay().config();
setApplePayConfig( config );
} catch ( error ) {
console.error( 'Failed to fetch Apple Pay config:', error );
}
};
fetchConfig();
}, [ namespace, isApplepayLoaded ] );
return applePayConfig;
};
export default useApplepayConfig;

View file

@ -0,0 +1,65 @@
import { useState, useEffect } from '@wordpress/element';
import { loadCustomScript } from '@paypal/paypal-js';
const useApplepayScript = (
componentDocument,
buttonConfig,
isPayPalLoaded
) => {
const [ isApplepayLoaded, setIsApplepayLoaded ] = useState( false );
useEffect( () => {
if ( ! componentDocument ) {
return;
}
const injectScriptToFrame = ( scriptSrc ) => {
if ( document === componentDocument ) {
return;
}
const script = document.querySelector(
`script[src^="${ scriptSrc }"]`
);
if ( script ) {
const newScript = componentDocument.createElement( 'script' );
newScript.src = script.src;
newScript.async = script.async;
newScript.type = script.type;
componentDocument.head.appendChild( newScript );
} else {
console.error( 'Script not found in the document:', scriptSrc );
}
};
const loadApplepayScript = async () => {
if ( ! isPayPalLoaded ) {
return;
}
if ( ! buttonConfig || ! buttonConfig.sdk_url ) {
console.error( 'Invalid buttonConfig or missing sdk_url' );
return;
}
try {
await loadCustomScript( { url: buttonConfig.sdk_url } ).then(
() => {
injectScriptToFrame( buttonConfig.sdk_url );
}
);
setIsApplepayLoaded( true );
} catch ( error ) {
console.error( 'Failed to load Applepay script:', error );
}
};
loadApplepayScript();
}, [ componentDocument, buttonConfig, isPayPalLoaded ] );
return isApplepayLoaded;
};
export default useApplepayScript;

View file

@ -0,0 +1,19 @@
import { useMemo } from '@wordpress/element';
import { combineStyles } from '../../../../../ppcp-button/resources/js/modules/Helper/PaymentButtonHelpers';
const useButtonStyles = ( buttonConfig, ppcpConfig ) => {
return useMemo( () => {
const styles = combineStyles(
ppcpConfig?.button || {},
buttonConfig?.button || {}
);
if ( styles.MiniCart && styles.MiniCart.type === 'buy' ) {
styles.MiniCart.type = 'pay';
}
return styles;
}, [ buttonConfig, ppcpConfig ] );
};
export default useButtonStyles;

View file

@ -0,0 +1,25 @@
import { useState, useEffect } from '@wordpress/element';
import { loadPayPalScript } from '../../../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
const usePayPalScript = ( namespace, ppcpConfig ) => {
const [ isPayPalLoaded, setIsPayPalLoaded ] = useState( false );
ppcpConfig.url_params.components += ',applepay';
useEffect( () => {
const loadScript = async () => {
try {
await loadPayPalScript( namespace, ppcpConfig );
setIsPayPalLoaded( true );
} catch ( error ) {
console.error( `Error loading PayPal script: ${ error }` );
}
};
loadScript();
}, [ namespace, ppcpConfig ] );
return isPayPalLoaded;
};
export default usePayPalScript;

View file

@ -5,6 +5,11 @@ import PreviewButton from '../../../../ppcp-button/resources/js/modules/Preview/
* A single Apple Pay preview button instance. * A single Apple Pay preview button instance.
*/ */
export default class ApplePayPreviewButton extends PreviewButton { export default class ApplePayPreviewButton extends PreviewButton {
/**
* @type {?PaymentButton}
*/
#button = null;
constructor( args ) { constructor( args ) {
super( args ); super( args );
@ -19,14 +24,18 @@ export default class ApplePayPreviewButton extends PreviewButton {
} }
createButton( buttonConfig ) { createButton( buttonConfig ) {
const button = new ApplepayButton( if ( ! this.#button ) {
'preview', this.#button = new ApplepayButton(
null, 'preview',
buttonConfig, null,
this.ppcpConfig buttonConfig,
); this.ppcpConfig
);
}
button.init( this.apiConfig ); this.#button.configure( this.apiConfig, null );
this.#button.applyButtonStyles( buttonConfig, this.ppcpConfig );
this.#button.reinit();
} }
/** /**

View file

@ -1,5 +1,6 @@
import { useEffect, useState } from '@wordpress/element'; import { useEffect, useRef, useState } from '@wordpress/element';
import { registerExpressPaymentMethod } from '@woocommerce/blocks-registry'; import { registerExpressPaymentMethod } from '@woocommerce/blocks-registry';
import { __ } from '@wordpress/i18n';
import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading'; import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
import { cartHasSubscriptionProducts } from '../../../ppcp-blocks/resources/js/Helper/Subscription'; import { cartHasSubscriptionProducts } from '../../../ppcp-blocks/resources/js/Helper/Subscription';
import { loadCustomScript } from '@paypal/paypal-js'; import { loadCustomScript } from '@paypal/paypal-js';
@ -18,20 +19,16 @@ if ( typeof window.PayPalCommerceGateway === 'undefined' ) {
window.PayPalCommerceGateway = ppcpConfig; window.PayPalCommerceGateway = ppcpConfig;
} }
const ApplePayComponent = ( props ) => { const ApplePayComponent = ( { isEditing } ) => {
const [ bootstrapped, setBootstrapped ] = useState( false );
const [ paypalLoaded, setPaypalLoaded ] = useState( false ); const [ paypalLoaded, setPaypalLoaded ] = useState( false );
const [ applePayLoaded, setApplePayLoaded ] = useState( false ); const [ applePayLoaded, setApplePayLoaded ] = useState( false );
const wrapperRef = useRef( null );
const bootstrap = function () {
const ManagerClass = props.isEditing
? ApplePayManagerBlockEditor
: ApplePayManager;
const manager = new ManagerClass( namespace, buttonConfig, ppcpConfig );
manager.init();
};
useEffect( () => { useEffect( () => {
if ( isEditing ) {
return;
}
// Load ApplePay SDK // Load ApplePay SDK
loadCustomScript( { url: buttonConfig.sdk_url } ).then( () => { loadCustomScript( { url: buttonConfig.sdk_url } ).then( () => {
setApplePayLoaded( true ); setApplePayLoaded( true );
@ -47,17 +44,35 @@ const ApplePayComponent = ( props ) => {
.catch( ( error ) => { .catch( ( error ) => {
console.error( 'Failed to load PayPal script: ', error ); console.error( 'Failed to load PayPal script: ', error );
} ); } );
}, [] ); }, [ isEditing ] );
useEffect( () => { useEffect( () => {
if ( ! bootstrapped && paypalLoaded && applePayLoaded ) { if ( isEditing || ! paypalLoaded || ! applePayLoaded ) {
setBootstrapped( true ); return;
bootstrap();
} }
}, [ paypalLoaded, applePayLoaded ] );
const ManagerClass = isEditing
? ApplePayManagerBlockEditor
: ApplePayManager;
buttonConfig.reactWrapper = wrapperRef.current;
new ManagerClass( namespace, buttonConfig, ppcpConfig );
}, [ paypalLoaded, applePayLoaded, isEditing ] );
if ( isEditing ) {
return (
<ApplePayManagerBlockEditor
namespace={ namespace }
buttonConfig={ buttonConfig }
ppcpConfig={ ppcpConfig }
/>
);
}
return ( return (
<div <div
ref={ wrapperRef }
id={ buttonConfig.button.wrapper.replace( '#', '' ) } id={ buttonConfig.button.wrapper.replace( '#', '' ) }
className="ppcp-button-apm ppcp-button-applepay" className="ppcp-button-apm ppcp-button-applepay"
></div> ></div>
@ -75,6 +90,11 @@ if (
registerExpressPaymentMethod( { registerExpressPaymentMethod( {
name: buttonData.id, name: buttonData.id,
title: `PayPal - ${ buttonData.title }`,
description: __(
'Eligible users will see the PayPal button.',
'woocommerce-paypal-payments'
),
label: <div dangerouslySetInnerHTML={ { __html: buttonData.title } } />, label: <div dangerouslySetInnerHTML={ { __html: buttonData.title } } />,
content: <ApplePayComponent isEditing={ false } />, content: <ApplePayComponent isEditing={ false } />,
edit: <ApplePayComponent isEditing={ true } />, edit: <ApplePayComponent isEditing={ true } />,

View file

@ -3,33 +3,49 @@ import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Help
import ApplePayManager from './ApplepayManager'; import ApplePayManager from './ApplepayManager';
import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper'; import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Helper/ButtonRefreshHelper';
( function ( { buttonConfig, ppcpConfig, jQuery } ) { ( function ( { buttonConfig, ppcpConfig } ) {
const namespace = 'ppcpPaypalApplepay'; const namespace = 'ppcpPaypalApplepay';
let manager;
const bootstrap = function () { function bootstrapPayButton() {
manager = new ApplePayManager( namespace, buttonConfig, ppcpConfig ); if ( ! buttonConfig || ! ppcpConfig ) {
manager.init();
};
setupButtonEvents( function () {
if ( manager ) {
manager.reinit();
}
} );
document.addEventListener( 'DOMContentLoaded', () => {
if (
typeof buttonConfig === 'undefined' ||
typeof ppcpConfig === 'undefined'
) {
return; return;
} }
const isMiniCart = ppcpConfig.mini_cart_buttons_enabled;
const isButton = jQuery( '#' + buttonConfig.button.wrapper ).length > 0; const manager = new ApplePayManager(
namespace,
buttonConfig,
ppcpConfig
);
setupButtonEvents( function () {
manager.reinit();
} );
}
function bootstrap() {
bootstrapPayButton();
// Other Apple Pay bootstrapping could happen here.
}
document.addEventListener( 'DOMContentLoaded', () => {
if ( ! buttonConfig || ! ppcpConfig ) {
/*
* No PayPal buttons present on this page, but maybe a bootstrap module needs to be
* initialized. Skip loading the SDK or gateway configuration, and directly initialize
* the module.
*/
bootstrap();
return;
}
const usedInMiniCart = ppcpConfig.mini_cart_buttons_enabled;
const pageHasButton =
null !== document.getElementById( buttonConfig.button.wrapper );
// If button wrapper is not present then there is no need to load the scripts. // If button wrapper is not present then there is no need to load the scripts.
// minicart loads later? // minicart loads later?
if ( ! isMiniCart && ! isButton ) { if ( ! usedInMiniCart && ! pageHasButton ) {
return; return;
} }
@ -63,5 +79,4 @@ import { setupButtonEvents } from '../../../ppcp-button/resources/js/modules/Hel
} )( { } )( {
buttonConfig: window.wc_ppcp_applepay, buttonConfig: window.wc_ppcp_applepay,
ppcpConfig: window.PayPalCommerceGateway, ppcpConfig: window.PayPalCommerceGateway,
jQuery: window.jQuery,
} ); } );

View file

@ -234,7 +234,6 @@ return array(
'DKK', // Danish Krone 'DKK', // Danish Krone
'EUR', // Euro 'EUR', // Euro
'GBP', // British Pound Sterling 'GBP', // British Pound Sterling
'HKD', // Hong Kong Dollar
'HUF', // Hungarian Forint 'HUF', // Hungarian Forint
'ILS', // Israeli New Shekel 'ILS', // Israeli New Shekel
'JPY', // Japanese Yen 'JPY', // Japanese Yen
@ -244,7 +243,6 @@ return array(
'PHP', // Philippine Peso 'PHP', // Philippine Peso
'PLN', // Polish Zloty 'PLN', // Polish Zloty
'SEK', // Swedish Krona 'SEK', // Swedish Krona
'SGD', // Singapore Dollar
'THB', // Thai Baht 'THB', // Thai Baht
'TWD', // New Taiwan Dollar 'TWD', // New Taiwan Dollar
'USD', // United States Dollar 'USD', // United States Dollar

View file

@ -105,6 +105,11 @@ class ApplePayGateway extends WC_Payment_Gateway {
) { ) {
$this->id = self::ID; $this->id = self::ID;
$this->supports = array(
'refunds',
'products',
);
$this->method_title = __( 'Apple Pay (via PayPal) ', 'woocommerce-paypal-payments' ); $this->method_title = __( 'Apple Pay (via PayPal) ', 'woocommerce-paypal-payments' );
$this->method_description = __( 'Display Apple Pay as a standalone payment option instead of bundling it with PayPal.', 'woocommerce-paypal-payments' ); $this->method_description = __( 'Display Apple Pay as a standalone payment option instead of bundling it with PayPal.', 'woocommerce-paypal-payments' );

View file

@ -168,6 +168,20 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
} }
); );
add_filter(
'woocommerce_paypal_payments_selected_button_locations',
function( array $locations, string $setting_name ): array {
$gateway = WC()->payment_gateways()->payment_gateways()[ ApplePayGateway::ID ] ?? '';
if ( $gateway && $gateway->enabled === 'yes' && $setting_name === 'smart_button_locations' ) {
$locations[] = 'checkout';
}
return $locations;
},
10,
2
);
return true; return true;
} }

View file

@ -1008,7 +1008,7 @@ class ApplePayButton implements ButtonInterface {
*/ */
protected function hide_gateway_until_eligible(): void { protected function hide_gateway_until_eligible(): void {
?> ?>
<style id="ppcp-hide-apple-pay">.wc_payment_method.payment_method_ppcp-applepay{display:none}</style> <style data-hide-gateway="ppcp-applepay">.wc_payment_method.payment_method_ppcp-applepay{display:none}</style>
<?php <?php
} }

View file

@ -113,7 +113,7 @@ class BlocksPaymentMethod extends AbstractPaymentMethodType {
'id' => $this->name, 'id' => $this->name,
'title' => $paypal_data['title'], // TODO : see if we should use another. 'title' => $paypal_data['title'], // TODO : see if we should use another.
'description' => $paypal_data['description'], // TODO : see if we should use another. 'description' => $paypal_data['description'], // TODO : see if we should use another.
'enabled' => $paypal_data['enabled'], // This button is enabled when PayPal buttons are. 'enabled' => $paypal_data['smartButtonsEnabled'], // This button is enabled when PayPal buttons are.
'scriptData' => $script_data, 'scriptData' => $script_data,
); );
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -12,22 +12,22 @@
"dependencies": { "dependencies": {
"@paypal/paypal-js": "^8.1.1", "@paypal/paypal-js": "^8.1.1",
"@paypal/react-paypal-js": "^8.5.0", "@paypal/react-paypal-js": "^8.5.0",
"core-js": "^3.25.0", "core-js": "^3.39",
"react": "^17.0.0", "react": "^18",
"react-dom": "^17.0.0" "react-dom": "^18"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.25",
"@woocommerce/dependency-extraction-webpack-plugin": "2.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

View file

@ -0,0 +1,21 @@
import { useMemo } from '@wordpress/element';
/**
* Custom hook returning the allowed shipping locations based on configuration.
*
* @param {Object} axoConfig - The AXO configuration object.
* @param {Array|undefined} axoConfig.enabled_shipping_locations - The list of enabled shipping locations.
* @return {Array} The final list of allowed shipping locations.
*/
const useAllowedLocations = ( axoConfig ) => {
return useMemo( () => {
const enabledShippingLocations =
axoConfig.enabled_shipping_locations || [];
return Array.isArray( enabledShippingLocations )
? enabledShippingLocations
: [];
}, [ axoConfig.enabled_shipping_locations ] );
};
export default useAllowedLocations;

View file

@ -4,6 +4,7 @@ import Fastlane from '../../../../ppcp-axo/resources/js/Connection/Fastlane';
import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug'; import { log } from '../../../../ppcp-axo/resources/js/Helper/Debug';
import { useDeleteEmptyKeys } from './useDeleteEmptyKeys'; import { useDeleteEmptyKeys } from './useDeleteEmptyKeys';
import useCardOptions from './useCardOptions'; import useCardOptions from './useCardOptions';
import useAllowedLocations from './useAllowedLocations';
import { STORE_NAME } from '../stores/axoStore'; import { STORE_NAME } from '../stores/axoStore';
/** /**
@ -33,6 +34,8 @@ const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
return deleteEmptyKeys( configRef.current.axoConfig.style_options ); return deleteEmptyKeys( configRef.current.axoConfig.style_options );
}, [ deleteEmptyKeys ] ); }, [ deleteEmptyKeys ] );
const allowedLocations = useAllowedLocations( axoConfig );
// Effect to initialize Fastlane SDK // Effect to initialize Fastlane SDK
useEffect( () => { useEffect( () => {
const initFastlane = async () => { const initFastlane = async () => {
@ -58,6 +61,9 @@ const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
cardOptions: { cardOptions: {
allowedBrands: cardOptions, allowedBrands: cardOptions,
}, },
shippingAddressOptions: {
allowedLocations,
},
} ); } );
// Set locale (hardcoded to 'en_us' for now) // Set locale (hardcoded to 'en_us' for now)
@ -72,7 +78,14 @@ const useFastlaneSdk = ( namespace, axoConfig, ppcpConfig ) => {
}; };
initFastlane(); initFastlane();
}, [ fastlaneSdk, styleOptions, isPayPalLoaded, namespace, cardOptions ] ); }, [
fastlaneSdk,
styleOptions,
isPayPalLoaded,
namespace,
cardOptions,
allowedLocations,
] );
// Effect to update the config ref when configs change // Effect to update the config ref when configs change
useEffect( () => { useEffect( () => {

View file

@ -39,6 +39,7 @@ return array(
$container->get( 'onboarding.environment' ), $container->get( 'onboarding.environment' ),
$container->get( 'wcgateway.url' ), $container->get( 'wcgateway.url' ),
$container->get( 'axo.supported-country-card-type-matrix' ), $container->get( 'axo.supported-country-card-type-matrix' ),
$container->get( 'axo.shipping-wc-enabled-locations' )
); );
}, },
); );

View file

@ -86,6 +86,13 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
*/ */
private $supported_country_card_type_matrix; private $supported_country_card_type_matrix;
/**
* The list of WooCommerce enabled shipping locations.
*
* @var array
*/
private array $enabled_shipping_locations;
/** /**
* AdvancedCardPaymentMethod constructor. * AdvancedCardPaymentMethod constructor.
* *
@ -99,6 +106,7 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
* @param Environment $environment The environment object. * @param Environment $environment The environment object.
* @param string $wcgateway_module_url The WcGateway module URL. * @param string $wcgateway_module_url The WcGateway module URL.
* @param array $supported_country_card_type_matrix The supported country card type matrix for Axo. * @param array $supported_country_card_type_matrix The supported country card type matrix for Axo.
* @param array $enabled_shipping_locations The list of WooCommerce enabled shipping locations.
*/ */
public function __construct( public function __construct(
string $module_url, string $module_url,
@ -109,7 +117,8 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
DCCGatewayConfiguration $dcc_configuration, DCCGatewayConfiguration $dcc_configuration,
Environment $environment, Environment $environment,
string $wcgateway_module_url, string $wcgateway_module_url,
array $supported_country_card_type_matrix array $supported_country_card_type_matrix,
array $enabled_shipping_locations
) { ) {
$this->name = AxoGateway::ID; $this->name = AxoGateway::ID;
$this->module_url = $module_url; $this->module_url = $module_url;
@ -121,6 +130,7 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
$this->environment = $environment; $this->environment = $environment;
$this->wcgateway_module_url = $wcgateway_module_url; $this->wcgateway_module_url = $wcgateway_module_url;
$this->supported_country_card_type_matrix = $supported_country_card_type_matrix; $this->supported_country_card_type_matrix = $supported_country_card_type_matrix;
$this->enabled_shipping_locations = $enabled_shipping_locations;
} }
/** /**
@ -218,7 +228,8 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
), ),
'allowed_cards' => $this->supported_country_card_type_matrix, 'allowed_cards' => $this->supported_country_card_type_matrix,
'disable_cards' => $this->settings->has( 'disable_cards' ) ? (array) $this->settings->get( 'disable_cards' ) : array(), 'disable_cards' => $this->settings->has( 'disable_cards' ) ? (array) $this->settings->get( 'disable_cards' ) : array(),
'style_options' => array( 'enabled_shipping_locations' => $this->enabled_shipping_locations,
'style_options' => array(
'root' => array( 'root' => array(
'backgroundColor' => $this->settings->has( 'axo_style_root_bg_color' ) ? $this->settings->get( 'axo_style_root_bg_color' ) : '', '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' ) : '', 'errorColor' => $this->settings->has( 'axo_style_root_error_color' ) ? $this->settings->get( 'axo_style_root_error_color' ) : '',
@ -244,9 +255,9 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
'CA' => WC()->countries->get_states( 'CA' ), 'CA' => WC()->countries->get_states( 'CA' ),
), ),
), ),
'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/', 'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/',
'module_url' => untrailingslashit( $this->module_url ), 'module_url' => untrailingslashit( $this->module_url ),
'ajax' => array( 'ajax' => array(
'frontend_logger' => array( 'frontend_logger' => array(
'endpoint' => \WC_AJAX::get_endpoint( FrontendLoggerEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( FrontendLoggerEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ), 'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ),

File diff suppressed because it is too large Load diff

View file

@ -11,20 +11,20 @@
], ],
"dependencies": { "dependencies": {
"@paypal/paypal-js": "^6.0.0", "@paypal/paypal-js": "^6.0.0",
"core-js": "^3.25.0" "core-js": "^3.39"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.25",
"@woocommerce/dependency-extraction-webpack-plugin": "^2.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

View file

@ -87,6 +87,9 @@ class AxoManager {
this.cardOptions = this.getCardOptions(); this.cardOptions = this.getCardOptions();
this.enabledShippingLocations =
this.axoConfig.enabled_shipping_locations;
this.registerEventHandlers(); this.registerEventHandlers();
this.shippingView = new ShippingView( this.shippingView = new ShippingView(
@ -666,6 +669,9 @@ class AxoManager {
cardOptions: { cardOptions: {
allowedBrands: this.cardOptions, allowedBrands: this.cardOptions,
}, },
shippingAddressOptions: {
allowedLocations: this.enabledShippingLocations,
},
} ); } );
this.fastlane.setLocale( 'en_us' ); this.fastlane.setLocale( 'en_us' );

View file

@ -68,7 +68,8 @@ return array(
$container->get( 'api.shop.currency.getter' ), $container->get( 'api.shop.currency.getter' ),
$container->get( 'woocommerce.logger.woocommerce' ), $container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'wcgateway.url' ), $container->get( 'wcgateway.url' ),
$container->get( 'axo.supported-country-card-type-matrix' ) $container->get( 'axo.supported-country-card-type-matrix' ),
$container->get( 'axo.shipping-wc-enabled-locations' )
); );
}, },
@ -276,4 +277,46 @@ return array(
$active_plugins_list $active_plugins_list
); );
}, },
'axo.shipping-wc-enabled-locations' => static function ( ContainerInterface $container ): array {
$default_zone = new \WC_Shipping_Zone( 0 );
$is_method_enabled = fn( \WC_Shipping_Method $method): bool => $method->enabled === 'yes';
$is_default_zone_enabled = ! empty(
array_filter(
$default_zone->get_shipping_methods(),
$is_method_enabled
)
);
if ( $is_default_zone_enabled ) {
return array();
}
$shipping_zones = \WC_Shipping_Zones::get_zones();
$get_zone_locations = fn( \WC_Shipping_Zone $zone): array =>
! empty( array_filter( $zone->get_shipping_methods(), $is_method_enabled ) )
? array_map(
fn( object $location): string => $location->code,
$zone->get_zone_locations()
)
: array();
$enabled_locations = array_unique(
array_merge(
...array_map(
$get_zone_locations,
array_map(
fn( $zone): \WC_Shipping_Zone =>
$zone instanceof \WC_Shipping_Zone ? $zone : new \WC_Shipping_Zone( $zone['id'] ),
$shipping_zones
)
)
)
);
return $enabled_locations;
},
); );

View file

@ -92,6 +92,13 @@ class AxoManager {
* @var array * @var array
*/ */
private array $supported_country_card_type_matrix; private array $supported_country_card_type_matrix;
/**
* The list of WooCommerce enabled shipping locations.
*
* @var array
*/
private array $enabled_shipping_locations;
/** /**
* AxoManager constructor. * AxoManager constructor.
* *
@ -105,6 +112,7 @@ class AxoManager {
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
* @param string $wcgateway_module_url The WcGateway module URL. * @param string $wcgateway_module_url The WcGateway module URL.
* @param array $supported_country_card_type_matrix The supported country card type matrix for Axo. * @param array $supported_country_card_type_matrix The supported country card type matrix for Axo.
* @param array $enabled_shipping_locations The list of WooCommerce enabled shipping locations.
*/ */
public function __construct( public function __construct(
string $module_url, string $module_url,
@ -116,7 +124,8 @@ class AxoManager {
CurrencyGetter $currency, CurrencyGetter $currency,
LoggerInterface $logger, LoggerInterface $logger,
string $wcgateway_module_url, string $wcgateway_module_url,
array $supported_country_card_type_matrix array $supported_country_card_type_matrix,
array $enabled_shipping_locations
) { ) {
$this->module_url = $module_url; $this->module_url = $module_url;
@ -129,6 +138,7 @@ class AxoManager {
$this->logger = $logger; $this->logger = $logger;
$this->wcgateway_module_url = $wcgateway_module_url; $this->wcgateway_module_url = $wcgateway_module_url;
$this->supported_country_card_type_matrix = $supported_country_card_type_matrix; $this->supported_country_card_type_matrix = $supported_country_card_type_matrix;
$this->enabled_shipping_locations = $enabled_shipping_locations;
} }
/** /**
@ -171,13 +181,13 @@ class AxoManager {
*/ */
private function script_data() { private function script_data() {
return array( return array(
'environment' => array( 'environment' => array(
'is_sandbox' => $this->environment->current_environment() === 'sandbox', 'is_sandbox' => $this->environment->current_environment() === 'sandbox',
), ),
'widgets' => array( 'widgets' => array(
'email' => 'render', 'email' => 'render',
), ),
'insights' => array( 'insights' => array(
'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG, 'enabled' => defined( 'WP_DEBUG' ) && WP_DEBUG,
'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ), 'client_id' => ( $this->settings->has( 'client_id' ) ? $this->settings->get( 'client_id' ) : null ),
'session_id' => 'session_id' =>
@ -193,7 +203,8 @@ class AxoManager {
), ),
'allowed_cards' => $this->supported_country_card_type_matrix, 'allowed_cards' => $this->supported_country_card_type_matrix,
'disable_cards' => $this->settings->has( 'disable_cards' ) ? (array) $this->settings->get( 'disable_cards' ) : array(), 'disable_cards' => $this->settings->has( 'disable_cards' ) ? (array) $this->settings->get( 'disable_cards' ) : array(),
'style_options' => array( 'enabled_shipping_locations' => $this->enabled_shipping_locations,
'style_options' => array(
'root' => array( 'root' => array(
'backgroundColor' => $this->settings->has( 'axo_style_root_bg_color' ) ? $this->settings->get( 'axo_style_root_bg_color' ) : '', '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' ) : '', 'errorColor' => $this->settings->has( 'axo_style_root_error_color' ) ? $this->settings->get( 'axo_style_root_error_color' ) : '',
@ -212,16 +223,16 @@ class AxoManager {
'focusBorderColor' => $this->settings->has( 'axo_style_input_focus_border_color' ) ? $this->settings->get( 'axo_style_input_focus_border_color' ) : '', 'focusBorderColor' => $this->settings->has( 'axo_style_input_focus_border_color' ) ? $this->settings->get( 'axo_style_input_focus_border_color' ) : '',
), ),
), ),
'name_on_card' => $this->settings->has( 'axo_name_on_card' ) ? $this->settings->get( 'axo_name_on_card' ) : '', 'name_on_card' => $this->settings->has( 'axo_name_on_card' ) ? $this->settings->get( 'axo_name_on_card' ) : '',
'woocommerce' => array( 'woocommerce' => array(
'states' => array( 'states' => array(
'US' => WC()->countries->get_states( 'US' ), 'US' => WC()->countries->get_states( 'US' ),
'CA' => WC()->countries->get_states( 'CA' ), 'CA' => WC()->countries->get_states( 'CA' ),
), ),
), ),
'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/', 'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/',
'module_url' => untrailingslashit( $this->module_url ), 'module_url' => untrailingslashit( $this->module_url ),
'ajax' => array( 'ajax' => array(
'frontend_logger' => array( 'frontend_logger' => array(
'endpoint' => \WC_AJAX::get_endpoint( FrontendLoggerEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( FrontendLoggerEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ), 'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ),

View file

@ -222,8 +222,10 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
// Render submit button. // Render submit button.
add_action( add_action(
$manager->checkout_button_renderer_hook(), $manager->checkout_button_renderer_hook(),
static function () use ( $c, $manager ) { static function () use ( $c, $manager, $module ) {
$manager->render_checkout_button(); if ( $module->should_render_fastlane( $c ) ) {
$manager->render_checkout_button();
}
} }
); );

File diff suppressed because it is too large Load diff

View file

@ -11,23 +11,23 @@
], ],
"dependencies": { "dependencies": {
"@paypal/react-paypal-js": "^8.5.0", "@paypal/react-paypal-js": "^8.5.0",
"core-js": "^3.25.0", "core-js": "^3.39",
"react": "^17.0.0", "react": "^18",
"react-dom": "^17.0.0" "react-dom": "^18"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.25",
"@wordpress/i18n": "^5.6.0", "@wordpress/i18n": "^5.11",
"@woocommerce/dependency-extraction-webpack-plugin": "2.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

View file

@ -0,0 +1,3 @@
li[id^="express-payment-method-ppcp-"] div[class^="ppc-button-container-"] {
display: flex;
}

View file

@ -19,11 +19,9 @@ export function CardFields( {
config, config,
eventRegistration, eventRegistration,
emitResponse, emitResponse,
components,
} ) { } ) {
const { onPaymentSetup } = eventRegistration; const { onPaymentSetup } = eventRegistration;
const { responseTypes } = emitResponse; const { responseTypes } = emitResponse;
const { PaymentMethodIcons } = components;
const [ cardFieldsForm, setCardFieldsForm ] = useState(); const [ cardFieldsForm, setCardFieldsForm ] = useState();
const getCardFieldsForm = ( cardFieldsForm ) => { const getCardFieldsForm = ( cardFieldsForm ) => {
@ -95,10 +93,6 @@ export function CardFields( {
} } } }
> >
<PayPalCardFieldsForm /> <PayPalCardFieldsForm />
<PaymentMethodIcons
icons={ config.card_icons }
align="left"
/>
<CheckoutHandler <CheckoutHandler
getCardFieldsForm={ getCardFieldsForm } getCardFieldsForm={ getCardFieldsForm }
getSavePayment={ getSavePayment } getSavePayment={ getSavePayment }

View file

@ -1,9 +1,44 @@
export const debounce = ( callback, delayMs ) => { export const debounce = ( callback, delayMs ) => {
let timeoutId = null; const state = {
return ( ...args ) => { timeoutId: null,
window.clearTimeout( timeoutId ); args: null,
timeoutId = window.setTimeout( () => {
callback.apply( null, args );
}, delayMs );
}; };
/**
* Cancels any pending debounced execution.
*/
const cancel = () => {
if ( state.timeoutId ) {
window.clearTimeout( state.timeoutId );
}
state.timeoutId = null;
state.args = null;
};
/**
* Immediately executes the debounced function if there's a pending execution.
* @return {void}
*/
const flush = () => {
// If there's nothing pending, return early.
if ( ! state.timeoutId ) {
return;
}
callback.apply( null, state.args || [] );
cancel();
};
const debouncedFunc = ( ...args ) => {
cancel();
state.args = args;
state.timeoutId = window.setTimeout( flush, delayMs );
};
// Attach utility methods
debouncedFunc.cancel = cancel;
debouncedFunc.flush = flush;
return debouncedFunc;
}; };

View file

@ -1,19 +1,30 @@
import { registerPaymentMethod } from '@woocommerce/blocks-registry'; import {registerPaymentMethod} from '@woocommerce/blocks-registry';
import { CardFields } from './Components/card-fields'; import {CardFields} from './Components/card-fields';
const config = wc.wcSettings.getSetting( 'ppcp-credit-card-gateway_data' ); const config = wc.wcSettings.getSetting('ppcp-credit-card-gateway_data');
registerPaymentMethod( { const Label = ({components, config}) => {
name: config.id, const {PaymentMethodIcons} = components;
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />, return <>
content: <CardFields config={ config } />, <span dangerouslySetInnerHTML={{__html: config.title}}/>
edit: <CardFields config={ config } />, <PaymentMethodIcons
ariaLabel: config.title, icons={ config.card_icons }
canMakePayment: () => { align="right"
return true; />
}, </>
supports: { }
showSavedCards: true,
features: config.supports, registerPaymentMethod({
}, name: config.id,
} ); label: <Label config={config}/>,
content: <CardFields config={config}/>,
edit: <CardFields config={config}/>,
ariaLabel: config.title,
canMakePayment: () => {
return true;
},
supports: {
showSavedCards: true,
features: config.supports,
},
});

View file

@ -1,4 +1,4 @@
import { useEffect, useState } from '@wordpress/element'; import { useEffect, useState, useMemo } from '@wordpress/element';
import { import {
registerExpressPaymentMethod, registerExpressPaymentMethod,
registerPaymentMethod, registerPaymentMethod,
@ -41,6 +41,7 @@ const PayPalComponent = ( {
shippingData, shippingData,
isEditing, isEditing,
fundingSource, fundingSource,
buttonAttributes,
} ) => { } ) => {
const { onPaymentSetup, onCheckoutFail, onCheckoutValidation } = const { onPaymentSetup, onCheckoutFail, onCheckoutValidation } =
eventRegistration; eventRegistration;
@ -614,6 +615,15 @@ const PayPalComponent = ( {
fundingSource fundingSource
); );
if ( typeof buttonAttributes !== 'undefined' ) {
style.height = buttonAttributes?.height
? Number( buttonAttributes.height )
: style.height;
style.borderRadius = buttonAttributes?.borderRadius
? Number( buttonAttributes.borderRadius )
: style.borderRadius;
}
if ( ! paypalScriptLoaded ) { if ( ! paypalScriptLoaded ) {
return null; return null;
} }
@ -688,19 +698,46 @@ const PayPalComponent = ( {
); );
}; };
const BlockEditorPayPalComponent = () => { const BlockEditorPayPalComponent = ( { fundingSource, buttonAttributes } ) => {
const urlParams = { const urlParams = useMemo(
clientId: 'test', () => ( {
...config.scriptData.url_params, clientId: 'test',
dataNamespace: 'ppcp-blocks-editor-paypal-buttons', ...config.scriptData.url_params,
components: 'buttons', dataNamespace: 'ppcp-blocks-editor-paypal-buttons',
}; components: 'buttons',
} ),
[]
);
const style = useMemo( () => {
const configStyle = normalizeStyleForFundingSource(
config.scriptData.button.style,
fundingSource
);
if ( buttonAttributes ) {
return {
...configStyle,
height: buttonAttributes.height
? Number( buttonAttributes.height )
: configStyle.height,
borderRadius: buttonAttributes.borderRadius
? Number( buttonAttributes.borderRadius )
: configStyle.borderRadius,
};
}
return configStyle;
}, [ fundingSource, buttonAttributes ] );
return ( return (
<PayPalScriptProvider options={ urlParams }> <PayPalScriptProvider options={ urlParams }>
<PayPalButtons <PayPalButtons
onClick={ ( data, actions ) => { className={ `ppc-button-container-${ fundingSource }` }
return false; fundingSource={ fundingSource }
} } style={ style }
forceReRender={ [ buttonAttributes || {} ] }
onClick={ () => false }
/> />
</PayPalScriptProvider> </PayPalScriptProvider>
); );
@ -739,11 +776,8 @@ if ( cartHasSubscriptionProducts( config.scriptData ) ) {
features.push( 'subscriptions' ); features.push( 'subscriptions' );
} }
if ( block_enabled && config.enabled ) { if ( block_enabled ) {
if ( if ( config.placeOrderEnabled && ! config.scriptData.continuation ) {
( config.addPlaceOrderMethod || config.usePlaceOrder ) &&
! config.scriptData.continuation
) {
let descriptionElement = ( let descriptionElement = (
<div <div
dangerouslySetInnerHTML={ { __html: config.description } } dangerouslySetInnerHTML={ { __html: config.description } }
@ -776,7 +810,7 @@ if ( block_enabled && config.enabled ) {
placeOrderButtonLabel: config.placeOrderButtonText, placeOrderButtonLabel: config.placeOrderButtonText,
ariaLabel: config.title, ariaLabel: config.title,
canMakePayment: () => { canMakePayment: () => {
return config.enabled; return true;
}, },
supports: { supports: {
features, features,
@ -789,7 +823,7 @@ if ( block_enabled && config.enabled ) {
name: config.id, name: config.id,
label: <div dangerouslySetInnerHTML={ { __html: config.title } } />, label: <div dangerouslySetInnerHTML={ { __html: config.title } } />,
content: <PayPalComponent isEditing={ false } />, content: <PayPalComponent isEditing={ false } />,
edit: <BlockEditorPayPalComponent />, edit: <BlockEditorPayPalComponent fundingSource={ 'paypal' } />,
ariaLabel: config.title, ariaLabel: config.title,
canMakePayment: () => { canMakePayment: () => {
return true; return true;
@ -798,7 +832,7 @@ if ( block_enabled && config.enabled ) {
features: [ ...features, 'ppcp_continuation' ], features: [ ...features, 'ppcp_continuation' ],
}, },
} ); } );
} else if ( ! config.usePlaceOrder ) { } else if ( config.smartButtonsEnabled ) {
for ( const fundingSource of [ for ( const fundingSource of [
'paypal', 'paypal',
...config.enabledFundingSources, ...config.enabledFundingSources,
@ -821,7 +855,11 @@ if ( block_enabled && config.enabled ) {
fundingSource={ fundingSource } fundingSource={ fundingSource }
/> />
), ),
edit: <BlockEditorPayPalComponent />, edit: (
<BlockEditorPayPalComponent
fundingSource={ fundingSource }
/>
),
ariaLabel: config.title, ariaLabel: config.title,
canMakePayment: async () => { canMakePayment: async () => {
if ( ! paypalScriptPromise ) { if ( ! paypalScriptPromise ) {
@ -845,6 +883,7 @@ if ( block_enabled && config.enabled ) {
}, },
supports: { supports: {
features, features,
style: [ 'height', 'borderRadius' ],
}, },
} ); } );
} }

View file

@ -47,6 +47,7 @@ return array(
$container->get( 'blocks.settings.final_review_enabled' ), $container->get( 'blocks.settings.final_review_enabled' ),
$container->get( 'session.cancellation.view' ), $container->get( 'session.cancellation.view' ),
$container->get( 'session.handler' ), $container->get( 'session.handler' ),
$container->get( 'wc-subscriptions.helper' ),
$container->get( 'blocks.add-place-order-method' ), $container->get( 'blocks.add-place-order-method' ),
$container->get( 'wcgateway.use-place-order-button' ), $container->get( 'wcgateway.use-place-order-button' ),
$container->get( 'wcgateway.place-order-button-text' ), $container->get( 'wcgateway.place-order-button-text' ),

View file

@ -126,6 +126,23 @@ class BlocksModule implements ServiceModule, ExtendingModule, ExecutableModule {
} }
); );
// Enqueue editor styles.
add_action(
'enqueue_block_editor_assets',
static function () use ( $c ) {
$module_url = $c->get( 'blocks.url' );
$asset_version = $c->get( 'ppcp.asset-version' );
wp_register_style(
'wc-ppcp-blocks-editor',
untrailingslashit( $module_url ) . '/assets/css/gateway-editor.css',
array(),
$asset_version
);
wp_enqueue_style( 'wc-ppcp-blocks-editor' );
}
);
add_filter( add_filter(
'woocommerce_paypal_payments_sdk_components_hook', 'woocommerce_paypal_payments_sdk_components_hook',
function( array $components ) { function( array $components ) {

View file

@ -19,6 +19,7 @@ use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
/** /**
* Class PayPalPaymentMethod * Class PayPalPaymentMethod
@ -87,6 +88,13 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
*/ */
private $session_handler; private $session_handler;
/**
* The Subscription Helper.
*
* @var SubscriptionHelper
*/
private $subscription_helper;
/** /**
* Whether to create a non-express method with the standard "Place order" button. * Whether to create a non-express method with the standard "Place order" button.
* *
@ -141,6 +149,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
* @param bool $final_review_enabled Whether the final review is enabled. * @param bool $final_review_enabled Whether the final review is enabled.
* @param CancelView $cancellation_view The cancellation view. * @param CancelView $cancellation_view The cancellation view.
* @param SessionHandler $session_handler The Session handler. * @param SessionHandler $session_handler The Session handler.
* @param SubscriptionHelper $subscription_helper The subscription helper.
* @param bool $add_place_order_method Whether to create a non-express method with the standard "Place order" button. * @param bool $add_place_order_method Whether to create a non-express method with the standard "Place order" button.
* @param bool $use_place_order Whether to use the standard "Place order" button instead of PayPal buttons. * @param bool $use_place_order Whether to use the standard "Place order" button instead of PayPal buttons.
* @param string $place_order_button_text The text for the standard "Place order" button. * @param string $place_order_button_text The text for the standard "Place order" button.
@ -158,6 +167,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
bool $final_review_enabled, bool $final_review_enabled,
CancelView $cancellation_view, CancelView $cancellation_view,
SessionHandler $session_handler, SessionHandler $session_handler,
SubscriptionHelper $subscription_helper,
bool $add_place_order_method, bool $add_place_order_method,
bool $use_place_order, bool $use_place_order,
string $place_order_button_text, string $place_order_button_text,
@ -175,6 +185,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
$this->final_review_enabled = $final_review_enabled; $this->final_review_enabled = $final_review_enabled;
$this->cancellation_view = $cancellation_view; $this->cancellation_view = $cancellation_view;
$this->session_handler = $session_handler; $this->session_handler = $session_handler;
$this->subscription_helper = $subscription_helper;
$this->add_place_order_method = $add_place_order_method; $this->add_place_order_method = $add_place_order_method;
$this->use_place_order = $use_place_order; $this->use_place_order = $use_place_order;
$this->place_order_button_text = $place_order_button_text; $this->place_order_button_text = $place_order_button_text;
@ -195,9 +206,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
// Do not load when definitely not needed, // Do not load when definitely not needed,
// but we still need to check the locations later and handle in JS // but we still need to check the locations later and handle in JS
// because has_block cannot be called here (too early). // because has_block cannot be called here (too early).
return $this->plugin_settings->has( 'enabled' ) && $this->plugin_settings->get( 'enabled' ) return $this->plugin_settings->has( 'enabled' ) && $this->plugin_settings->get( 'enabled' );
&& ( $this->settings_status->is_smart_button_enabled_for_location( 'checkout-block-express' ) ||
$this->settings_status->is_smart_button_enabled_for_location( 'cart-block' ) );
} }
/** /**
@ -245,15 +254,19 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
); );
} }
$smart_buttons_enabled = ! $this->use_place_order
&& $this->settings_status->is_smart_button_enabled_for_location( $script_data['context'] ?? 'block-checkout' );
$place_order_enabled = ( $this->use_place_order || $this->add_place_order_method )
&& ! $this->subscription_helper->cart_contains_subscription();
return array( return array(
'id' => $this->gateway->id, 'id' => $this->gateway->id,
'title' => $this->gateway->title, 'title' => $this->gateway->title,
'description' => $this->gateway->description, 'description' => $this->gateway->description,
'enabled' => $this->settings_status->is_smart_button_enabled_for_location( $script_data['context'] ?? 'checkout' ), 'smartButtonsEnabled' => $smart_buttons_enabled,
'placeOrderEnabled' => $place_order_enabled,
'fundingSource' => $this->session_handler->funding_source(), 'fundingSource' => $this->session_handler->funding_source(),
'finalReviewEnabled' => $this->final_review_enabled, 'finalReviewEnabled' => $this->final_review_enabled,
'addPlaceOrderMethod' => $this->add_place_order_method,
'usePlaceOrder' => $this->use_place_order,
'placeOrderButtonText' => $this->place_order_button_text, 'placeOrderButtonText' => $this->place_order_button_text,
'placeOrderButtonDescription' => $this->place_order_button_description, 'placeOrderButtonDescription' => $this->place_order_button_description,
'enabledFundingSources' => $funding_sources, 'enabledFundingSources' => $funding_sources,

View file

@ -11,7 +11,8 @@ module.exports = {
entry: { entry: {
'checkout-block': path.resolve('./resources/js/checkout-block.js'), 'checkout-block': path.resolve('./resources/js/checkout-block.js'),
'advanced-card-checkout-block': path.resolve('./resources/js/advanced-card-checkout-block.js'), 'advanced-card-checkout-block': path.resolve('./resources/js/advanced-card-checkout-block.js'),
"gateway": path.resolve('./resources/css/gateway.scss') "gateway": path.resolve('./resources/css/gateway.scss'),
"gateway-editor": path.resolve('./resources/css/gateway-editor.scss')
}, },
output: { output: {
path: path.resolve(__dirname, 'assets/'), path: path.resolve(__dirname, 'assets/'),

File diff suppressed because it is too large Load diff

View file

@ -12,20 +12,20 @@
], ],
"dependencies": { "dependencies": {
"@paypal/paypal-js": "^6.0.0", "@paypal/paypal-js": "^6.0.0",
"core-js": "^3.25.0", "core-js": "^3.39",
"deepmerge": "^4.2.2", "deepmerge": "^4.3",
"formdata-polyfill": "^4.0.10" "formdata-polyfill": "^4.0.10"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

View file

@ -233,6 +233,15 @@ class SingleProductBootstap {
this.form(), this.form(),
this.errorHandler this.errorHandler
); );
if (
! this.gateway.vaultingEnabled &&
[ 'subscription', 'variable-subscription' ].includes(
this.gateway.productType
) &&
this.gateway.manualRenewalEnabled !== '1'
) {
return;
}
if ( if (
PayPalCommerceGateway.data_client_id.has_subscriptions && PayPalCommerceGateway.data_client_id.has_subscriptions &&

View file

@ -38,6 +38,12 @@ class CardFieldsFreeTrialRenderer {
if ( hideDccGateway ) { if ( hideDccGateway ) {
hideDccGateway.parentNode.removeChild( hideDccGateway ); hideDccGateway.parentNode.removeChild( hideDccGateway );
} }
const dccGatewayLi = document.querySelector(
'.wc_payment_method.payment_method_ppcp-credit-card-gateway'
);
if (dccGatewayLi.style.display === 'none' || dccGatewayLi.style.display === '') {
dccGatewayLi.style.display = 'block';
}
this.errorHandler.clear(); this.errorHandler.clear();

View file

@ -44,6 +44,12 @@ class CardFieldsRenderer {
if ( hideDccGateway ) { if ( hideDccGateway ) {
hideDccGateway.parentNode.removeChild( hideDccGateway ); hideDccGateway.parentNode.removeChild( hideDccGateway );
} }
const dccGatewayLi = document.querySelector(
'.wc_payment_method.payment_method_ppcp-credit-card-gateway'
);
if (dccGatewayLi.style.display === 'none' || dccGatewayLi.style.display === '') {
dccGatewayLi.style.display = 'block';
}
const cardFields = paypal.CardFields( { const cardFields = paypal.CardFields( {
createOrder: contextConfig.createOrder, createOrder: contextConfig.createOrder,

View file

@ -52,6 +52,12 @@ class HostedFieldsRenderer {
if ( hideDccGateway ) { if ( hideDccGateway ) {
hideDccGateway.parentNode.removeChild( hideDccGateway ); hideDccGateway.parentNode.removeChild( hideDccGateway );
} }
const dccGatewayLi = document.querySelector(
'.wc_payment_method.payment_method_ppcp-credit-card-gateway'
);
if (dccGatewayLi.style.display === 'none' || dccGatewayLi.style.display === '') {
dccGatewayLi.style.display = 'block';
}
const cardNumberField = document.querySelector( const cardNumberField = document.querySelector(
'#ppcp-credit-card-gateway-card-number' '#ppcp-credit-card-gateway-card-number'

View file

@ -10,6 +10,8 @@ import {
dispatchButtonEvent, dispatchButtonEvent,
observeButtonEvent, observeButtonEvent,
} from '../Helper/PaymentButtonHelpers'; } from '../Helper/PaymentButtonHelpers';
import { isVisible } from '../Helper/Hiding';
import { isDisabled, setEnabled } from '../Helper/ButtonDisabler';
/** /**
* Collection of all available styling options for this button. * Collection of all available styling options for this button.
@ -173,9 +175,9 @@ export default class PaymentButton {
/** /**
* Whether the current browser/website support the payment method. * Whether the current browser/website support the payment method.
* *
* @type {boolean} * @type {?boolean}
*/ */
#isEligible = false; #isEligible = null;
/** /**
* Whether this button is visible. Modified by `show()` and `hide()` * Whether this button is visible. Modified by `show()` and `hide()`
@ -184,6 +186,13 @@ export default class PaymentButton {
*/ */
#isVisible = true; #isVisible = true;
/**
* Whether this button is enabled (can be clicked).
*
* @type {boolean}
*/
#isEnabled = true;
/** /**
* The currently visible payment button. * The currently visible payment button.
* *
@ -192,6 +201,13 @@ export default class PaymentButton {
*/ */
#button = null; #button = null;
/**
* List of checks to perform to verify the PaymentButton has is configured correctly.
*
* @type {{check, errorMessage, shouldPass}[]}
*/
#validationChecks = [];
/** /**
* Factory method to create a new PaymentButton while limiting a single instance per context. * Factory method to create a new PaymentButton while limiting a single instance per context.
* *
@ -305,6 +321,11 @@ export default class PaymentButton {
); );
this.applyButtonStyles( this.#buttonConfig ); this.applyButtonStyles( this.#buttonConfig );
this.registerValidationRules(
this.#assertIsInvalid.bind( this ),
this.#assertIsValid.bind( this )
);
apmButtonsInit( this.#ppcpConfig ); apmButtonsInit( this.#ppcpConfig );
this.initEventListeners(); this.initEventListeners();
} }
@ -559,6 +580,29 @@ export default class PaymentButton {
this.triggerRedraw(); this.triggerRedraw();
} }
/**
* The enabled/disabled state of the button (whether it can be clicked).
*
* @return {boolean} True indicates, that the button is enabled.
*/
get isEnabled() {
return this.#isEnabled;
}
/**
* Change the enabled/disabled state of the button.
*
* @param {boolean} newState Whether the button is enabled.
*/
set isEnabled( newState ) {
if ( this.#isEnabled === newState ) {
return;
}
this.#isEnabled = newState;
this.triggerRedraw();
}
/** /**
* Returns the HTML element that wraps the current button * Returns the HTML element that wraps the current button
* *
@ -569,6 +613,23 @@ export default class PaymentButton {
return document.getElementById( this.wrapperId ); return document.getElementById( this.wrapperId );
} }
/**
* Returns the standard PayPal smart button selector for the current context.
*
* @return {string | null} The selector, or null if not available.
*/
get ppcpButtonWrapperSelector() {
if ( PaymentContext.Blocks.includes( this.context ) ) {
return null;
}
if ( this.context === PaymentContext.MiniCart ) {
return this.ppcpConfig?.button?.mini_cart_wrapper;
}
return this.ppcpConfig?.button?.wrapper;
}
/** /**
* Checks whether the main button-wrapper is present in the current DOM. * Checks whether the main button-wrapper is present in the current DOM.
* *
@ -634,16 +695,75 @@ export default class PaymentButton {
this.#logger.group( label ); this.#logger.group( label );
} }
/**
* Register a validation check that marks the configuration as invalid when passed.
*
* @param {Function} check - A function that returns a truthy value if the check passes.
* @param {string} errorMessage - The error message to display if the check fails.
*/
#assertIsInvalid( check, errorMessage ) {
this.#validationChecks.push( {
check,
errorMessage,
shouldPass: false,
} );
}
/**
* Register a validation check that instantly marks the configuration as valid when passed.
*
* @param {Function} check - A function that returns a truthy value if the check passes.
*/
#assertIsValid( check ) {
this.#validationChecks.push( { check, shouldPass: true } );
}
/**
* Defines a series of validation steps to ensure the payment button is configured correctly.
*
* Each validation step is executed in the order they are defined within this method.
*
* If a validation step using `invalidIf` returns true, the configuration is immediately considered
* invalid, and an error message is logged. Conversely, if a validation step using `validIf`
* returns true, the configuration is immediately considered valid.
*
* If no validation step returns true, the configuration is assumed to be valid by default.
*
* @param {(condition: () => boolean, errorMessage: string) => void} invalidIf - Registers a validation step that fails if the condition returns true.
* @param {(condition: () => boolean) => void} validIf - Registers a validation step that passes if the condition returns true.
*/
// eslint-disable-next-line no-unused-vars
registerValidationRules( invalidIf, validIf ) {}
/** /**
* Determines if the current button instance has valid and complete configuration details. * Determines if the current button instance has valid and complete configuration details.
* Used during initialization to decide if the button can be initialized or should be skipped. * Used during initialization to decide if the button can be initialized or should be skipped.
* *
* Can be implemented by the derived class. * All required validation steps must be registered in the constructor of the derived class
* using `this.addValidationFailure()` or `this.addValidationSuccess()`.
* *
* @param {boolean} [silent=false] - Set to true to suppress console errors. * @param {boolean} [silent=false] - Set to true to suppress console errors.
* @return {boolean} True indicates the config is valid and initialization can continue. * @return {boolean} True indicates the config is valid and initialization can continue.
*/ */
validateConfiguration( silent = false ) { validateConfiguration( silent = false ) {
for ( const step of this.#validationChecks ) {
const result = step.check();
if ( step.shouldPass && result ) {
// If a success check passes, mark as valid immediately.
return true;
}
if ( ! step.shouldPass && result ) {
// If a failure check passes, mark as invalid.
if ( ! silent && step.errorMessage ) {
this.error( step.errorMessage );
}
return false;
}
}
return true; return true;
} }
@ -697,7 +817,23 @@ export default class PaymentButton {
} }
/** /**
* Attaches event listeners to show or hide the payment button when needed. * Applies the visibility and enabled state from the PayPal button.
* Intended for the product page, may not work correctly on the checkout page.
*/
syncProductButtonsState() {
const ppcpButton = document.querySelector(
this.ppcpButtonWrapperSelector
);
if ( ! ppcpButton ) {
return;
}
this.isVisible = isVisible( ppcpButton );
this.isEnabled = ! isDisabled( ppcpButton );
}
/**
* Attaches event listeners to show/hide or enable/disable the payment button when needed.
*/ */
initEventListeners() { initEventListeners() {
// Refresh the button - this might show, hide or re-create the payment button. // Refresh the button - this might show, hide or re-create the payment button.
@ -726,6 +862,24 @@ export default class PaymentButton {
callback: () => ( this.isVisible = true ), callback: () => ( this.isVisible = true ),
} ); } );
} }
// On the product page, copy the visibility and enabled state from the PayPal button.
if ( this.context === PaymentContext.Product ) {
jQuery( document ).on(
'ppcp-shown ppcp-hidden ppcp-enabled ppcp-disabled',
( ev, data ) => {
if (
! jQuery( data.selector ).is(
this.ppcpButtonWrapperSelector
)
) {
return;
}
this.syncProductButtonsState();
}
);
}
} }
/** /**
@ -735,6 +889,10 @@ export default class PaymentButton {
if ( ! this.isPresent ) { if ( ! this.isPresent ) {
return; return;
} }
if ( ! this.isEligible ) {
this.wrapperElement.style.display = 'none';
return;
}
this.applyWrapperStyles(); this.applyWrapperStyles();
@ -763,11 +921,16 @@ export default class PaymentButton {
const styleSelector = `style[data-hide-gateway="${ this.methodId }"]`; const styleSelector = `style[data-hide-gateway="${ this.methodId }"]`;
const wrapperSelector = `#${ this.wrappers.Default }`; const wrapperSelector = `#${ this.wrappers.Default }`;
const paymentMethodLi = document.querySelector(`.wc_payment_method.payment_method_${ this.methodId }`);
document document
.querySelectorAll( styleSelector ) .querySelectorAll( styleSelector )
.forEach( ( el ) => el.remove() ); .forEach( ( el ) => el.remove() );
if (paymentMethodLi.style.display === 'none' || paymentMethodLi.style.display === '') {
paymentMethodLi.style.display = 'block';
}
document document
.querySelectorAll( wrapperSelector ) .querySelectorAll( wrapperSelector )
.forEach( ( el ) => el.remove() ); .forEach( ( el ) => el.remove() );
@ -807,6 +970,12 @@ export default class PaymentButton {
// Apply the wrapper visibility. // Apply the wrapper visibility.
wrapper.style.display = this.isVisible ? 'block' : 'none'; wrapper.style.display = this.isVisible ? 'block' : 'none';
// Apply the enabled/disabled state.
// On the product page, use the form to display error messages if clicked while disabled.
const form =
this.context === PaymentContext.Product ? 'form.cart' : null;
setEnabled( wrapper, this.isEnabled, form );
} }
/** /**

View file

@ -37,6 +37,7 @@ use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure; use WooCommerce\PayPalCommerce\Button\Helper\ThreeDSecure;
use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
return array( return array(
'button.client_id' => static function ( ContainerInterface $container ): string { 'button.client_id' => static function ( ContainerInterface $container ): string {
@ -101,12 +102,20 @@ return array(
return $obj->get_context(); return $obj->get_context();
}, },
'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface { 'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface {
$state = $container->get( 'onboarding.state' ); $context = $container->get( 'button.context' );
if ( $container->get( 'wcgateway.use-place-order-button' )
&& in_array( $container->get( 'button.context' ), array( 'checkout', 'pay-now' ), true ) $settings_status = $container->get( 'wcgateway.settings.status' );
) { assert( $settings_status instanceof SettingsStatus );
return new DisabledSmartButton();
if ( in_array( $context, array( 'checkout', 'pay-now' ), true ) ) {
if ( $container->get( 'wcgateway.use-place-order-button' )
|| ! $settings_status->is_smart_button_enabled_for_location( $context )
) {
return new DisabledSmartButton();
}
} }
$state = $container->get( 'onboarding.state' );
if ( $state->current_state() !== State::STATE_ONBOARDED ) { if ( $state->current_state() !== State::STATE_ONBOARDED ) {
return new DisabledSmartButton(); return new DisabledSmartButton();
} }
@ -125,7 +134,6 @@ return array(
$messages_apply = $container->get( 'button.helper.messages-apply' ); $messages_apply = $container->get( 'button.helper.messages-apply' );
$environment = $container->get( 'onboarding.environment' ); $environment = $container->get( 'onboarding.environment' );
$payment_token_repository = $container->get( 'vaulting.repository.payment-token' ); $payment_token_repository = $container->get( 'vaulting.repository.payment-token' );
$settings_status = $container->get( 'wcgateway.settings.status' );
return new SmartButton( return new SmartButton(
$container->get( 'button.url' ), $container->get( 'button.url' ),
$container->get( 'ppcp.asset-version' ), $container->get( 'ppcp.asset-version' ),

View file

@ -1031,8 +1031,11 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
* @return bool * @return bool
*/ */
private function has_subscriptions(): bool { private function has_subscriptions(): bool {
if ( ! $this->subscription_helper->plugin_is_active() ) {
return false;
}
if ( if (
! $this->subscription_helper->accept_only_automatic_payment_gateways() $this->subscription_helper->accept_manual_renewals()
&& $this->paypal_subscriptions_enabled() !== true && $this->paypal_subscriptions_enabled() !== true
) { ) {
return false; return false;
@ -1318,8 +1321,17 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
'should_handle_shipping_in_paypal' => $this->should_handle_shipping_in_paypal && ! $this->is_checkout(), 'should_handle_shipping_in_paypal' => $this->should_handle_shipping_in_paypal && ! $this->is_checkout(),
'needShipping' => $this->need_shipping(), 'needShipping' => $this->need_shipping(),
'vaultingEnabled' => $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ), 'vaultingEnabled' => $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ),
'productType' => null,
'manualRenewalEnabled' => $this->subscription_helper->accept_manual_renewals(),
); );
if ( is_product() ) {
$product = wc_get_product( get_the_ID() );
if ( is_a( $product, \WC_Product::class ) ) {
$localize['productType'] = $product->get_type();
}
}
if ( 'pay-now' === $this->context() ) { if ( 'pay-now' === $this->context() ) {
$localize['pay_now'] = $this->pay_now_script_data(); $localize['pay_now'] = $this->pay_now_script_data();
} }

File diff suppressed because it is too large Load diff

View file

@ -10,18 +10,18 @@
"Edge >= 14" "Edge >= 14"
], ],
"dependencies": { "dependencies": {
"core-js": "^3.25.0" "core-js": "^3.39"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

File diff suppressed because it is too large Load diff

View file

@ -11,18 +11,18 @@
"Edge >= 14" "Edge >= 14"
], ],
"dependencies": { "dependencies": {
"core-js": "^3.25.0" "core-js": "^3.39"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

View file

@ -101,7 +101,7 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule {
add_action( add_action(
'woocommerce_init', 'woocommerce_init',
function() { function() {
if ( is_callable( array( WC(), 'is_wc_admin_active' ) ) && WC()->is_wc_admin_active() && class_exists( 'Automattic\WooCommerce\Admin\Notes\Notes' ) ) { if ( is_admin() && is_callable( array( WC(), 'is_wc_admin_active' ) ) && WC()->is_wc_admin_active() && class_exists( 'Automattic\WooCommerce\Admin\Notes\Notes' ) ) {
PPEC\DeactivateNote::init(); PPEC\DeactivateNote::init();
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -11,21 +11,21 @@
], ],
"dependencies": { "dependencies": {
"@paypal/paypal-js": "^6.0.0", "@paypal/paypal-js": "^6.0.0",
"core-js": "^3.25.0" "core-js": "^3.39"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.25",
"@wordpress/i18n": "^5.6.0", "@wordpress/i18n": "^5.11",
"@woocommerce/dependency-extraction-webpack-plugin": "^2.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

View file

@ -215,45 +215,31 @@ class GooglepayButton extends PaymentButton {
/** /**
* @inheritDoc * @inheritDoc
*/ */
validateConfiguration( silent = false ) { registerValidationRules( invalidIf, validIf ) {
const validEnvs = [ 'PRODUCTION', 'TEST' ]; invalidIf(
() =>
! [ 'TEST', 'PRODUCTION' ].includes(
this.buttonConfig.environment
),
`Invalid environment: ${ this.buttonConfig.environment }`
);
const isInvalid = ( ...args ) => { validIf( () => this.isPreview );
if ( ! silent ) {
this.error( ...args );
}
return false;
};
if ( ! validEnvs.includes( this.buttonConfig.environment ) ) { invalidIf(
return isInvalid( () => ! this.googlePayConfig,
'Invalid environment:', 'No API configuration - missing configure() call?'
this.buttonConfig.environment );
);
}
// Preview buttons only need a valid environment. invalidIf(
if ( this.isPreview ) { () => ! this.transactionInfo,
return true; 'No transactionInfo - missing configure() call?'
} );
if ( ! this.googlePayConfig ) { invalidIf(
return isInvalid( () => ! this.contextHandler?.validateContext(),
'No API configuration - missing configure() call?' `Invalid context handler.`
); );
}
if ( ! this.transactionInfo ) {
return isInvalid(
'No transactionInfo - missing configure() call?'
);
}
if ( ! typeof this.contextHandler?.validateContext() ) {
return isInvalid( 'Invalid context handler.', this.contextHandler );
}
return true;
} }
/** /**

View file

@ -149,7 +149,6 @@ return array(
'DKK', // Danish Krone 'DKK', // Danish Krone
'EUR', // Euro 'EUR', // Euro
'GBP', // British Pound Sterling 'GBP', // British Pound Sterling
'HKD', // Hong Kong Dollar
'HUF', // Hungarian Forint 'HUF', // Hungarian Forint
'ILS', // Israeli New Shekel 'ILS', // Israeli New Shekel
'JPY', // Japanese Yen 'JPY', // Japanese Yen
@ -159,7 +158,6 @@ return array(
'PHP', // Philippine Peso 'PHP', // Philippine Peso
'PLN', // Polish Zloty 'PLN', // Polish Zloty
'SEK', // Swedish Krona 'SEK', // Swedish Krona
'SGD', // Singapore Dollar
'THB', // Thai Baht 'THB', // Thai Baht
'TWD', // New Taiwan Dollar 'TWD', // New Taiwan Dollar
'USD', // United States Dollar 'USD', // United States Dollar
@ -174,6 +172,7 @@ return array(
$container->get( 'googlepay.sdk_url' ), $container->get( 'googlepay.sdk_url' ),
$container->get( 'ppcp.asset-version' ), $container->get( 'ppcp.asset-version' ),
$container->get( 'session.handler' ), $container->get( 'session.handler' ),
$container->get( 'wc-subscriptions.helper' ),
$container->get( 'wcgateway.settings' ), $container->get( 'wcgateway.settings' ),
$container->get( 'onboarding.environment' ), $container->get( 'onboarding.environment' ),
$container->get( 'wcgateway.settings.status' ), $container->get( 'wcgateway.settings.status' ),

View file

@ -107,7 +107,7 @@ class BlocksPaymentMethod extends AbstractPaymentMethodType {
'id' => $this->name, 'id' => $this->name,
'title' => $paypal_data['title'], // See if we should use another. 'title' => $paypal_data['title'], // See if we should use another.
'description' => $paypal_data['description'], // See if we should use another. 'description' => $paypal_data['description'], // See if we should use another.
'enabled' => $paypal_data['enabled'], // This button is enabled when PayPal buttons are. 'enabled' => $paypal_data['smartButtonsEnabled'], // This button is enabled when PayPal buttons are.
'scriptData' => $this->button->script_data(), 'scriptData' => $this->button->script_data(),
); );
} }

View file

@ -21,6 +21,7 @@ use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
/** /**
* Class Button * Class Button
@ -85,37 +86,47 @@ class Button implements ButtonInterface {
*/ */
private $session_handler; private $session_handler;
/**
* The Subscription Helper.
*
* @var SubscriptionHelper
*/
private $subscription_helper;
/** /**
* SmartButton constructor. * SmartButton constructor.
* *
* @param string $module_url The URL to the module. * @param string $module_url The URL to the module.
* @param string $sdk_url The URL to the SDK. * @param string $sdk_url The URL to the SDK.
* @param string $version The assets version. * @param string $version The assets version.
* @param SessionHandler $session_handler The Session handler. * @param SessionHandler $session_handler The Session handler.
* @param Settings $settings The Settings. * @param SubscriptionHelper $subscription_helper The subscription helper.
* @param Environment $environment The environment object. * @param Settings $settings The Settings.
* @param SettingsStatus $settings_status The Settings status helper. * @param Environment $environment The environment object.
* @param LoggerInterface $logger The logger. * @param SettingsStatus $settings_status The Settings status helper.
* @param LoggerInterface $logger The logger.
*/ */
public function __construct( public function __construct(
string $module_url, string $module_url,
string $sdk_url, string $sdk_url,
string $version, string $version,
SessionHandler $session_handler, SessionHandler $session_handler,
SubscriptionHelper $subscription_helper,
Settings $settings, Settings $settings,
Environment $environment, Environment $environment,
SettingsStatus $settings_status, SettingsStatus $settings_status,
LoggerInterface $logger LoggerInterface $logger
) { ) {
$this->module_url = $module_url; $this->module_url = $module_url;
$this->sdk_url = $sdk_url; $this->sdk_url = $sdk_url;
$this->version = $version; $this->version = $version;
$this->session_handler = $session_handler; $this->session_handler = $session_handler;
$this->settings = $settings; $this->subscription_helper = $subscription_helper;
$this->environment = $environment; $this->settings = $settings;
$this->settings_status = $settings_status; $this->environment = $environment;
$this->logger = $logger; $this->settings_status = $settings_status;
$this->logger = $logger;
} }
/** /**
@ -233,6 +244,21 @@ class Button implements ButtonInterface {
$button_enabled_payorder = true; $button_enabled_payorder = true;
$button_enabled_minicart = $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' ); $button_enabled_minicart = $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' );
if (
$this->subscription_helper->plugin_is_active()
&& ! $this->subscription_helper->accept_manual_renewals()
) {
if ( is_product() && $this->subscription_helper->current_product_is_subscription() ) {
return false;
}
if ( $this->subscription_helper->order_pay_contains_subscription() ) {
return false;
}
if ( $this->subscription_helper->cart_contains_subscription() ) {
return false;
}
}
/** /**
* Param types removed to avoid third-party issues. * Param types removed to avoid third-party issues.
* *

View file

@ -218,6 +218,20 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
} }
); );
add_filter(
'woocommerce_paypal_payments_selected_button_locations',
function( array $locations, string $setting_name ): array {
$gateway = WC()->payment_gateways()->payment_gateways()[ GooglePayGateway::ID ] ?? '';
if ( $gateway && $gateway->enabled === 'yes' && $setting_name === 'smart_button_locations' ) {
$locations[] = 'checkout';
}
return $locations;
},
10,
2
);
return true; return true;
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -10,20 +10,20 @@
"Edge >= 14" "Edge >= 14"
], ],
"dependencies": { "dependencies": {
"core-js": "^3.25.0" "core-js": "^3.39"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.25",
"@woocommerce/dependency-extraction-webpack-plugin": "2.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

File diff suppressed because it is too large Load diff

View file

@ -11,18 +11,18 @@
"Edge >= 14" "Edge >= 14"
], ],
"dependencies": { "dependencies": {
"core-js": "^3.25.0" "core-js": "^3.39"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

View file

@ -1,3 +1,8 @@
import {
setVisible,
setVisibleByClass,
} from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
document.addEventListener( 'DOMContentLoaded', () => { document.addEventListener( 'DOMContentLoaded', () => {
const payLaterMessagingSelectableLocations = [ const payLaterMessagingSelectableLocations = [
'product', 'product',
@ -216,6 +221,18 @@ document.addEventListener( 'DOMContentLoaded', () => {
replace(); replace();
}; };
const hideElements = ( selectorGroup ) => {
selectorGroup.forEach( ( selector ) =>
setVisibleByClass( selector, false, 'hide' )
);
};
const showElements = ( selectorGroup ) => {
selectorGroup.forEach( ( selector ) =>
setVisibleByClass( selector, true, 'hide' )
);
};
const toggleInputsBySelectedLocations = ( const toggleInputsBySelectedLocations = (
stylingPerSelector, stylingPerSelector,
locationsSelector, locationsSelector,
@ -226,30 +243,30 @@ document.addEventListener( 'DOMContentLoaded', () => {
const payLaterMessagingEnabled = document.querySelector( const payLaterMessagingEnabled = document.querySelector(
payLaterMessagingEnabledSelector payLaterMessagingEnabledSelector
); );
const stylingPerElement = document.querySelector( stylingPerSelector );
const locationsElement = document.querySelector( locationsSelector );
const stylingPerElementWrapper = stylingPerElement?.closest( 'tr' );
const stylingPerElementWrapperSelector =
'#' + stylingPerElementWrapper?.getAttribute( 'id' );
const stylingPerElement = document.querySelector( stylingPerSelector );
if ( ! stylingPerElement ) { if ( ! stylingPerElement ) {
return; return;
} }
const stylingPerElementWrapper = stylingPerElement.closest( 'tr' );
const toggleElementsBySelectedLocations = () => { const toggleElementsBySelectedLocations = () => {
stylingPerElementWrapper.style.display = '';
const selectedLocations = getSelectedLocations( locationsSelector ); const selectedLocations = getSelectedLocations( locationsSelector );
const emptySmartButtonLocationMessage = jQuery(
'.ppcp-empty-smart-button-location' setVisibleByClass(
stylingPerElementWrapper,
selectedLocations.length > 0,
'hide'
); );
if ( selectedLocations.length === 0 ) { if ( selectedLocations.length === 0 ) {
hideElements( hideElements( groupToHideOnChecked );
groupToHideOnChecked.concat(
stylingPerElementWrapperSelector const emptySmartButtonLocationMessage = document.querySelector(
) '.ppcp-empty-smart-button-location'
); );
if ( emptySmartButtonLocationMessage.length === 0 ) { if ( ! emptySmartButtonLocationMessage ) {
jQuery( jQuery(
PayPalCommerceSettings.empty_smart_button_location_message PayPalCommerceSettings.empty_smart_button_location_message
).insertAfter( ).insertAfter(
@ -277,11 +294,11 @@ document.addEventListener( 'DOMContentLoaded', () => {
); );
groupToShowOnChecked.forEach( ( element ) => { groupToShowOnChecked.forEach( ( element ) => {
if ( inputSelectors.includes( element ) ) { setVisibleByClass(
document.querySelector( element ).style.display = ''; element,
return; inputSelectors.includes( element ),
} 'hide'
document.querySelector( element ).style.display = 'none'; );
} ); } );
if ( inputType === 'messages' ) { if ( inputType === 'messages' ) {
@ -289,18 +306,6 @@ document.addEventListener( 'DOMContentLoaded', () => {
} }
}; };
const hideElements = ( selectroGroup ) => {
selectroGroup.forEach( ( elementToHide ) => {
document.querySelector( elementToHide ).style.display = 'none';
} );
};
const showElements = ( selectroGroup ) => {
selectroGroup.forEach( ( elementToShow ) => {
document.querySelector( elementToShow ).style.display = '';
} );
};
groupToggle( stylingPerSelector, groupToShowOnChecked ); groupToggle( stylingPerSelector, groupToShowOnChecked );
toggleElementsBySelectedLocations(); toggleElementsBySelectedLocations();
@ -327,7 +332,7 @@ document.addEventListener( 'DOMContentLoaded', () => {
} ); } );
// We need to use jQuery here as the select might be a select2 element, which doesn't use native events. // We need to use jQuery here as the select might be a select2 element, which doesn't use native events.
jQuery( locationsElement ).on( 'change', function () { jQuery( locationsSelector ).on( 'change', function () {
const emptySmartButtonLocationMessage = jQuery( const emptySmartButtonLocationMessage = jQuery(
'.ppcp-empty-smart-button-location' '.ppcp-empty-smart-button-location'
); );
@ -457,6 +462,38 @@ document.addEventListener( 'DOMContentLoaded', () => {
} }
}; };
/**
* Hide the subscription settings when smart buttons are disabled for checkout,
* since the basic redirect gateway is disabled for subscriptions.
*/
const initSettingsHidingForPlaceOrderGateway = () => {
const selectors = [
'#field-paypal_saved_payments',
'#field-subscriptions_mode',
'#field-vault_enabled',
];
const updateSettingsVisibility = () => {
const selectedLocations = getSelectedLocations(
smartButtonLocationsSelect
);
const hasCheckoutSmartButtons =
selectedLocations.includes( 'checkout' ) ||
selectedLocations.includes( 'checkout-block-express' );
selectors.forEach( ( selector ) => {
setVisibleByClass( selector, hasCheckoutSmartButtons, 'hide' );
} );
};
updateSettingsVisibility();
jQuery( smartButtonLocationsSelect ).on(
'change',
updateSettingsVisibility
);
};
( () => { ( () => {
removeDisabledCardIcons( removeDisabledCardIcons(
'select[name="ppcp[disable_cards][]"]', 'select[name="ppcp[disable_cards][]"]',
@ -488,6 +525,8 @@ document.addEventListener( 'DOMContentLoaded', () => {
toggleMessagingEnabled(); toggleMessagingEnabled();
initSettingsHidingForPlaceOrderGateway();
groupToggle( '#ppcp-vault_enabled', [ groupToggle( '#ppcp-vault_enabled', [
'#field-subscription_behavior_when_vault_fails', '#field-subscription_behavior_when_vault_fails',
] ); ] );

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Onboarding; namespace WooCommerce\PayPalCommerce\Onboarding;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
@ -19,7 +20,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets; use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Endpoint\UpdateSignupLinksEndpoint;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingSendOnlyNoticeRenderer;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer; use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer;
use WooCommerce\PayPalCommerce\Onboarding\OnboardingRESTController; use WooCommerce\PayPalCommerce\Onboarding\OnboardingRESTController;
@ -239,6 +240,11 @@ return array(
'sandbox-express_checkout', 'sandbox-express_checkout',
); );
}, },
'onboarding.render-send-only-notice' => static function( ContainerInterface $container ) {
return new OnboardingSendOnlyNoticeRenderer(
$container->get( 'wcgateway.send-only-message' )
);
},
'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer { 'onboarding.render' => static function ( ContainerInterface $container ) : OnboardingRenderer {
$partner_referrals = $container->get( 'api.endpoint.partner-referrals-production' ); $partner_referrals = $container->get( 'api.endpoint.partner-referrals-production' );
$partner_referrals_sandbox = $container->get( 'api.endpoint.partner-referrals-sandbox' ); $partner_referrals_sandbox = $container->get( 'api.endpoint.partner-referrals-sandbox' );

View file

@ -117,7 +117,7 @@ class OnboardingAssets {
array( array(
'empty_smart_button_location_message' => sprintf( 'empty_smart_button_location_message' => sprintf(
'<p class="description ppcp-empty-smart-button-location">%1$s</p>', '<p class="description ppcp-empty-smart-button-location">%1$s</p>',
__( 'Note: If no button location is selected, the PayPal gateway will not be available.', 'woocommerce-paypal-payments' ) __( 'Note: PayPal buttons and advanced payment features (Alternative Payment Methods, Subscriptions, etc.) are unavailable if no Smart Button Location is configured.', 'woocommerce-paypal-payments' )
), ),
) )
); );

View file

@ -0,0 +1,41 @@
<?php
/**
* Creates an admin message that notifies user about send only country while onboarding.
*
* @package WooCommerce\PayPalCommerce\Onboarding\Render
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Onboarding\Render;
/**
* Class OnboardingRenderer
*/
class OnboardingSendOnlyNoticeRenderer {
/**
* The notice message.
*
* @var string
*/
protected string $message;
/**
* AdminNotice constructor.
*
* @param string $message The notice message.
*/
public function __construct( string $message ) {
$this->message = $message;
}
/**
* Renders the notice.
*
* @return string
*/
public function render(): string {
return '<div class="notice notice-warning ppcp-notice-wrapper inline"><p>' . $this->message . '</p></div>';
}
}

File diff suppressed because it is too large Load diff

View file

@ -11,18 +11,18 @@
"Edge >= 14" "Edge >= 14"
], ],
"dependencies": { "dependencies": {
"core-js": "^3.25.0" "core-js": "^3.39"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

View file

@ -124,7 +124,7 @@ class MetaBoxRenderer {
<label for="ppcp-tracking-items"><?php echo esc_html__( 'Select items for this shipment', 'woocommerce-paypal-payments' ); ?></label> <label for="ppcp-tracking-items"><?php echo esc_html__( 'Select items for this shipment', 'woocommerce-paypal-payments' ); ?></label>
<select multiple class="wc-enhanced-select ppcp-tracking-items" id="ppcp-tracking-items" name="ppcp-tracking[items]"> <select multiple class="wc-enhanced-select ppcp-tracking-items" id="ppcp-tracking-items" name="ppcp-tracking[items]">
<?php foreach ( $order_items as $item ) : ?> <?php foreach ( $order_items as $item ) : ?>
<option value="<?php echo intval( $item->get_id() ); ?>"><?php echo esc_html( $item->get_name() ); ?></option> <option value="<?php echo intval( $item->get_id() ); ?>"><?php echo esc_attr( wp_strip_all_tags( $item->get_name() ) ); ?></option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</div> </div>

File diff suppressed because it is too large Load diff

View file

@ -11,22 +11,22 @@
], ],
"dependencies": { "dependencies": {
"@paypal/react-paypal-js": "^8.2.0", "@paypal/react-paypal-js": "^8.2.0",
"core-js": "^3.25.0", "core-js": "^3.39",
"react": "^17.0.0", "react": "^18",
"react-dom": "^17.0.0" "react-dom": "^18"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.25",
"@woocommerce/dependency-extraction-webpack-plugin": "2.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

File diff suppressed because it is too large Load diff

View file

@ -10,20 +10,20 @@
"Edge >= 14" "Edge >= 14"
], ],
"dependencies": { "dependencies": {
"core-js": "^3.25.0" "core-js": "^3.39"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.25",
"@woocommerce/dependency-extraction-webpack-plugin": "^2.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

File diff suppressed because it is too large Load diff

View file

@ -11,22 +11,22 @@
], ],
"dependencies": { "dependencies": {
"@paypal/react-paypal-js": "^8.2.0", "@paypal/react-paypal-js": "^8.2.0",
"core-js": "^3.25.0", "core-js": "^3.39",
"react": "^17.0.0", "react": "^18",
"react-dom": "^17.0.0" "react-dom": "^18"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"@babel/preset-react": "^7.18.6", "@babel/preset-react": "^7.25",
"@woocommerce/dependency-extraction-webpack-plugin": "2.2.0", "@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

File diff suppressed because it is too large Load diff

View file

@ -10,18 +10,18 @@
"Edge >= 14" "Edge >= 14"
], ],
"dependencies": { "dependencies": {
"core-js": "^3.25.0" "core-js": "^3.39"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.19", "@babel/core": "^7.26",
"@babel/preset-env": "^7.19", "@babel/preset-env": "^7.26",
"babel-loader": "^8.2", "babel-loader": "^9.2",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"file-loader": "^6.2.0", "file-loader": "^6.2.0",
"sass": "^1.42.1", "sass": "^1.80",
"sass-loader": "^12.1.0", "sass-loader": "^16",
"webpack": "^5.76", "webpack": "^5.96",
"webpack-cli": "^4.10" "webpack-cli": "^5"
}, },
"scripts": { "scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",

View file

@ -40,4 +40,13 @@ return array(
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
); );
}, },
'paypal-subscriptions.renewal-handler' => static function ( ContainerInterface $container ): RenewalHandler {
return new RenewalHandler( $container->get( 'woocommerce.logger.woocommerce' ) );
},
'paypal-subscriptions.status' => static function ( ContainerInterface $container ): SubscriptionStatus {
return new SubscriptionStatus(
$container->get( 'api.endpoint.billing-subscriptions' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
); );

View file

@ -187,6 +187,9 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
30 30
); );
/**
* Executed when updating WC Subscription.
*/
add_action( add_action(
'woocommerce_process_shop_subscription_meta', 'woocommerce_process_shop_subscription_meta',
/** /**
@ -194,65 +197,41 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* *
* @psalm-suppress MissingClosureParamType * @psalm-suppress MissingClosureParamType
*/ */
function( $id, $post ) use ( $c ) { function( $id ) use ( $c ) {
$subscription = wcs_get_subscription( $id ); $subscription = wcs_get_subscription( $id );
if ( ! is_a( $subscription, WC_Subscription::class ) ) { if ( $subscription === false ) {
return; return;
} }
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( ! $subscription_id ) { if ( ! $subscription_id ) {
return; return;
} }
$subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' );
assert( $subscriptions_endpoint instanceof BillingSubscriptions );
if ( $subscription->get_status() === 'cancelled' ) { $subscription_status = $c->get( 'paypal-subscriptions.status' );
try { assert( $subscription_status instanceof SubscriptionStatus );
$subscriptions_endpoint->cancel( $subscription_id );
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' ); $subscription_status->update_status( $subscription->get_status(), $subscription_id );
$logger->error( 'Could not cancel subscription product on PayPal. ' . $error );
}
}
if ( $subscription->get_status() === 'pending-cancel' ) {
try {
$subscriptions_endpoint->suspend( $subscription_id );
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not suspend subscription product on PayPal. ' . $error );
}
}
if ( $subscription->get_status() === 'active' ) {
try {
$current_subscription = $subscriptions_endpoint->subscription( $subscription_id );
if ( $current_subscription->status === 'SUSPENDED' ) {
$subscriptions_endpoint->activate( $subscription_id );
}
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not reactivate subscription product on PayPal. ' . $error );
}
}
}, },
20, 20
2 );
/**
* Update subscription status from WC Subscriptions list page action link.
*/
add_action(
'woocommerce_subscription_status_updated',
function( WC_Subscription $subscription ) use ( $c ) {
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( ! $subscription_id ) {
return;
}
$subscription_status = $c->get( 'paypal-subscriptions.status' );
assert( $subscription_status instanceof SubscriptionStatus );
$subscription_status->update_status( $subscription->get_status(), $subscription_id );
}
); );
add_filter( add_filter(
@ -717,11 +696,18 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* @param WC_Product $product The product. * @param WC_Product $product The product.
* @param SubscriptionsApiHandler $subscriptions_api_handler The subscription api handler. * @param SubscriptionsApiHandler $subscriptions_api_handler The subscription api handler.
* @return void * @return void
*
* @psalm-suppress PossiblyInvalidCast
*/ */
private function update_subscription_product_meta( WC_Product $product, SubscriptionsApiHandler $subscriptions_api_handler ): void { private function update_subscription_product_meta( WC_Product $product, SubscriptionsApiHandler $subscriptions_api_handler ): void {
// phpcs:ignore WordPress.Security.NonceVerification // phpcs:ignore WordPress.Security.NonceVerification
$enable_subscription_product = wc_clean( wp_unslash( $_POST['_ppcp_enable_subscription_product'] ?? '' ) ); $enable_subscription_product = wc_string_to_bool( (string) wc_clean( wp_unslash( $_POST['_ppcp_enable_subscription_product'] ?? '' ) ) );
$product->update_meta_data( '_ppcp_enable_subscription_product', $enable_subscription_product ); $product->update_meta_data( '_ppcp_enable_subscription_product', wc_bool_to_string( $enable_subscription_product ) );
if ( ! $enable_subscription_product ) {
$product->save();
return;
}
if ( ! $product->get_sold_individually() ) { if ( ! $product->get_sold_individually() ) {
$product->set_sold_individually( true ); $product->set_sold_individually( true );
@ -729,7 +715,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
$product->save(); $product->save();
if ( ( $product->get_type() === 'subscription' || $product->get_type() === 'subscription_variation' ) && $enable_subscription_product === 'yes' ) { if ( $product->get_type() === 'subscription' || $product->get_type() === 'subscription_variation' ) {
if ( $product->meta_exists( 'ppcp_subscription_product' ) && $product->meta_exists( 'ppcp_subscription_plan' ) ) { if ( $product->meta_exists( 'ppcp_subscription_product' ) && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
$subscriptions_api_handler->update_product( $product ); $subscriptions_api_handler->update_product( $product );
$subscriptions_api_handler->update_plan( $product ); $subscriptions_api_handler->update_plan( $product );

View file

@ -0,0 +1,107 @@
<?php
/**
* Subscriptions renewal handler.
*
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use Psr\Log\LoggerInterface;
use WC_Data_Exception;
use WC_Order;
use WC_Subscription;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
/**
* Class RenewalHandler
*/
class RenewalHandler {
use TransactionIdHandlingTrait;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* RenewalHandler constructor.
*
* @param LoggerInterface $logger The logger.
*/
public function __construct( LoggerInterface $logger ) {
$this->logger = $logger;
}
/**
* Process subscription renewal.
*
* @param WC_Subscription[] $subscriptions WC Subscriptions.
* @param string $transaction_id PayPal transaction ID.
* @return void
* @throws WC_Data_Exception If something goes wrong while setting payment method.
*/
public function process( array $subscriptions, string $transaction_id ): void {
foreach ( $subscriptions as $subscription ) {
if ( $this->is_for_renewal_order( $subscription ) ) {
$renewal_order = wcs_create_renewal_order( $subscription );
if ( is_a( $renewal_order, WC_Order::class ) ) {
$this->logger->info(
sprintf(
'Processing renewal order #%s for subscription #%s',
$renewal_order->get_id(),
$subscription->get_id()
)
);
$renewal_order->set_payment_method( $subscription->get_payment_method() );
$renewal_order->payment_complete();
$this->update_transaction_id( $transaction_id, $renewal_order, $this->logger );
break;
}
}
$parent_order = wc_get_order( $subscription->get_parent() );
if ( is_a( $parent_order, WC_Order::class ) ) {
$this->logger->info(
sprintf(
'Processing parent order #%s for subscription #%s',
$parent_order->get_id(),
$subscription->get_id()
)
);
$subscription->update_meta_data( '_ppcp_is_subscription_renewal', 'true' );
$subscription->save_meta_data();
$this->update_transaction_id( $transaction_id, $parent_order, $this->logger );
}
}
}
/**
* Checks whether subscription order is for renewal or not.
*
* @param WC_Subscription $subscription WC Subscription.
* @return bool
*/
private function is_for_renewal_order( WC_Subscription $subscription ): bool {
$subscription_renewal_meta = $subscription->get_meta( '_ppcp_is_subscription_renewal' ) ?? '';
if ( $subscription_renewal_meta === 'true' ) {
return true;
}
if (
time() >= $subscription->get_time( 'start' )
&& ( time() - $subscription->get_time( 'start' ) ) <= ( 8 * HOUR_IN_SECONDS )
) {
return false;
}
return true;
}
}

View file

@ -0,0 +1,144 @@
<?php
/**
* Handles PayPal subscription status.
*
* @package WooCommerce\PayPalCommerce\WcSubscriptions
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\PayPalSubscriptions;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class SubscriptionStatus
*/
class SubscriptionStatus {
/**
* Billing subscriptions endpoint.
*
* @var BillingSubscriptions
*/
private $subscriptions_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* SubscriptionStatus constructor.
*
* @param BillingSubscriptions $subscriptions_endpoint Billing subscriptions endpoint.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
BillingSubscriptions $subscriptions_endpoint,
LoggerInterface $logger
) {
$this->subscriptions_endpoint = $subscriptions_endpoint;
$this->logger = $logger;
}
/**
* Updates PayPal subscription status from the given WC Subscription status.
*
* @param string $subscription_status The WC Subscription status.
* @param string $subscription_id The PayPal Subscription ID.
* @return void
*/
public function update_status( string $subscription_status, string $subscription_id ): void {
if ( $subscription_status === 'pending-cancel' || $subscription_status === 'cancelled' ) {
try {
$current_subscription = $this->subscriptions_endpoint->subscription( $subscription_id );
if ( $current_subscription->status === 'CANCELLED' ) {
return;
}
$this->logger->info(
sprintf(
'Canceling PayPal subscription #%s.',
$subscription_id
)
);
$this->subscriptions_endpoint->cancel( $subscription_id );
} catch ( RuntimeException $exception ) {
$this->logger->error(
sprintf(
'Could not cancel PayPal subscription #%s. %s',
$subscription_id,
$this->get_error( $exception )
)
);
}
}
if ( $subscription_status === 'on-hold' ) {
try {
$this->logger->info(
sprintf(
'Suspending PayPal subscription #%s.',
$subscription_id
)
);
$this->subscriptions_endpoint->suspend( $subscription_id );
} catch ( RuntimeException $exception ) {
$this->logger->error(
sprintf(
'Could not suspend PayPal subscription #%s. %s',
$subscription_id,
$this->get_error( $exception )
)
);
}
}
if ( $subscription_status === 'active' ) {
try {
$current_subscription = $this->subscriptions_endpoint->subscription( $subscription_id );
if ( $current_subscription->status === 'SUSPENDED' ) {
$this->logger->info(
sprintf(
'Activating suspended PayPal subscription #%s.',
$subscription_id
)
);
$this->subscriptions_endpoint->activate( $subscription_id );
}
} catch ( RuntimeException $exception ) {
$this->logger->error(
sprintf(
'Could not reactivate PayPal subscription #%s. %s',
$subscription_id,
$this->get_error( $exception )
)
);
}
}
}
/**
* Get error from exception.
*
* @param RuntimeException $exception The exception.
* @return string
*/
private function get_error( RuntimeException $exception ): string {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
return $error;
}
}

File diff suppressed because it is too large Load diff

Some files were not shown because too many files have changed in this diff Show more