Merge pull request #965 from woocommerce/pcp-456-aub-choice

Improve cart subscriptions check and "All products for subscriptions" compatibility
This commit is contained in:
Emili Castells 2023-03-03 10:44:03 +01:00 committed by GitHub
commit 1debc92453
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 241 additions and 107 deletions

View file

@ -6,7 +6,6 @@ import PayNowBootstrap from "./modules/ContextBootstrap/PayNowBootstrap";
import Renderer from './modules/Renderer/Renderer'; import Renderer from './modules/Renderer/Renderer';
import ErrorHandler from './modules/ErrorHandler'; import ErrorHandler from './modules/ErrorHandler';
import CreditCardRenderer from "./modules/Renderer/CreditCardRenderer"; import CreditCardRenderer from "./modules/Renderer/CreditCardRenderer";
import dataClientIdAttributeHandler from "./modules/DataClientIdAttributeHandler";
import MessageRenderer from "./modules/Renderer/MessageRenderer"; import MessageRenderer from "./modules/Renderer/MessageRenderer";
import Spinner from "./modules/Helper/Spinner"; import Spinner from "./modules/Helper/Spinner";
import { import {
@ -19,6 +18,7 @@ import {isChangePaymentPage} from "./modules/Helper/Subscriptions";
import FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler"; import FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler";
import FormSaver from './modules/Helper/FormSaver'; import FormSaver from './modules/Helper/FormSaver';
import FormValidator from "./modules/Helper/FormValidator"; import FormValidator from "./modules/Helper/FormValidator";
import {loadPaypalScript} from "./modules/Helper/ScriptLoading";
// TODO: could be a good idea to have a separate spinner for each gateway, // TODO: could be a good idea to have a separate spinner for each gateway,
// but I think we care mainly about the script loading, so one spinner should be enough. // but I think we care mainly about the script loading, so one spinner should be enough.
@ -258,24 +258,10 @@ document.addEventListener(
hideOrderButtonIfPpcpGateway(); hideOrderButtonIfPpcpGateway();
}); });
const script = document.createElement('script'); loadPaypalScript(PayPalCommerceGateway, () => {
script.addEventListener('load', (event) => {
bootstrapped = true; bootstrapped = true;
bootstrap(); bootstrap();
}); });
script.setAttribute('src', PayPalCommerceGateway.button.url);
Object.entries(PayPalCommerceGateway.script_attributes).forEach(
(keyValue) => {
script.setAttribute(keyValue[0], keyValue[1]);
}
);
if (PayPalCommerceGateway.data_client_id.set_attribute) {
dataClientIdAttributeHandler(script, PayPalCommerceGateway.data_client_id);
return;
}
document.body.appendChild(script);
}, },
); );

View file

@ -9,31 +9,17 @@ class SingleProductActionHandler {
constructor( constructor(
config, config,
updateCart, updateCart,
showButtonCallback,
hideButtonCallback,
formElement, formElement,
errorHandler errorHandler
) { ) {
this.config = config; this.config = config;
this.updateCart = updateCart; this.updateCart = updateCart;
this.showButtonCallback = showButtonCallback;
this.hideButtonCallback = hideButtonCallback;
this.formElement = formElement; this.formElement = formElement;
this.errorHandler = errorHandler; this.errorHandler = errorHandler;
} }
configuration() configuration()
{ {
if ( this.hasVariations() ) {
const observer = new ButtonsToggleListener(
this.formElement.querySelector('.single_add_to_cart_button'),
this.showButtonCallback,
this.hideButtonCallback
);
observer.init();
}
return { return {
createOrder: this.createOrder(), createOrder: this.createOrder(),
onApprove: onApprove(this, this.errorHandler), onApprove: onApprove(this, this.errorHandler),

View file

@ -1,4 +1,5 @@
import CartActionHandler from '../ActionHandler/CartActionHandler'; import CartActionHandler from '../ActionHandler/CartActionHandler';
import {setVisible} from "../Helper/Hiding";
class CartBootstrap { class CartBootstrap {
constructor(gateway, renderer, errorHandler) { constructor(gateway, renderer, errorHandler) {
@ -16,13 +17,31 @@ class CartBootstrap {
jQuery(document.body).on('updated_cart_totals updated_checkout', () => { jQuery(document.body).on('updated_cart_totals updated_checkout', () => {
this.render(); this.render();
fetch(
this.gateway.ajax.cart_script_params.endpoint,
{
method: 'GET',
credentials: 'same-origin',
}
)
.then(result => result.json())
.then(result => {
if (! result.success) {
return;
}
const newParams = result.data;
const reloadRequired = this.gateway.url_params.intent !== newParams.intent;
// TODO: should reload the script instead
setVisible(this.gateway.button.wrapper, !reloadRequired)
});
}); });
} }
shouldRender() { shouldRender() {
return document.querySelector(this.gateway.button.wrapper) !== return document.querySelector(this.gateway.button.wrapper) !== null;
null || document.querySelector(this.gateway.hosted_fields.wrapper) !==
null;
} }
render() { render() {

View file

@ -1,5 +1,7 @@
import UpdateCart from "../Helper/UpdateCart"; import UpdateCart from "../Helper/UpdateCart";
import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler"; import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler";
import {hide, show, setVisible} from "../Helper/Hiding";
import ButtonsToggleListener from "../Helper/ButtonsToggleListener";
class SingleProductBootstap { class SingleProductBootstap {
constructor(gateway, renderer, messages, errorHandler) { constructor(gateway, renderer, messages, errorHandler) {
@ -12,10 +14,10 @@ class SingleProductBootstap {
handleChange() { handleChange() {
if (!this.shouldRender()) { const shouldRender = this.shouldRender();
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper); setVisible(this.gateway.button.wrapper, shouldRender);
this.renderer.hideButtons(this.gateway.button.wrapper); setVisible(this.gateway.messages.wrapper, shouldRender);
this.messages.hideMessages(); if (!shouldRender) {
return; return;
} }
@ -23,7 +25,6 @@ class SingleProductBootstap {
} }
init() { init() {
const form = document.querySelector('form.cart'); const form = document.querySelector('form.cart');
if (!form) { if (!form) {
return; return;
@ -32,20 +33,33 @@ class SingleProductBootstap {
form.addEventListener('change', this.handleChange.bind(this)); form.addEventListener('change', this.handleChange.bind(this));
this.mutationObserver.observe(form, {childList: true, subtree: true}); this.mutationObserver.observe(form, {childList: true, subtree: true});
const buttonObserver = new ButtonsToggleListener(
form.querySelector('.single_add_to_cart_button'),
() => {
show(this.gateway.button.wrapper);
show(this.gateway.messages.wrapper);
this.messages.renderWithAmount(this.priceAmount())
},
() => {
hide(this.gateway.button.wrapper);
hide(this.gateway.messages.wrapper);
},
);
buttonObserver.init();
if (!this.shouldRender()) { if (!this.shouldRender()) {
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper); hide(this.gateway.button.wrapper);
this.messages.hideMessages(); hide(this.gateway.messages.wrapper);
return; return;
} }
this.render(); this.render();
} }
shouldRender() { shouldRender() {
return document.querySelector('form.cart') !== null
return document.querySelector('form.cart') !== null && !this.priceAmountIsZero(); && !this.priceAmountIsZero()
&& !this.isSubscriptionMode();
} }
priceAmount() { priceAmount() {
@ -74,6 +88,12 @@ class SingleProductBootstap {
return !price || price === 0; return !price || price === 0;
} }
isSubscriptionMode() {
// Check "All products for subscriptions" plugin.
return document.querySelector('.wcsatt-options-product:not(.wcsatt-options-product--hidden) .subscription-option input[type="radio"]:checked') !== null
|| document.querySelector('.wcsatt-options-prompt-label-subscription input[type="radio"]:checked') !== null; // grouped
}
render() { render() {
const actionHandler = new SingleProductActionHandler( const actionHandler = new SingleProductActionHandler(
this.gateway, this.gateway,
@ -81,16 +101,6 @@ class SingleProductBootstap {
this.gateway.ajax.change_cart.endpoint, this.gateway.ajax.change_cart.endpoint,
this.gateway.ajax.change_cart.nonce, this.gateway.ajax.change_cart.nonce,
), ),
() => {
this.renderer.showButtons(this.gateway.button.wrapper);
this.renderer.showButtons(this.gateway.hosted_fields.wrapper);
this.messages.renderWithAmount(this.priceAmount())
},
() => {
this.renderer.hideButtons(this.gateway.button.wrapper);
this.renderer.hideButtons(this.gateway.hosted_fields.wrapper);
this.messages.hideMessages();
},
document.querySelector('form.cart'), document.querySelector('form.cart'),
this.errorHandler, this.errorHandler,
); );

View file

@ -14,6 +14,9 @@ class ButtonsToggleListener {
init() init()
{ {
if (!this.element) {
return;
}
const config = { attributes : true }; const config = { attributes : true };
const callback = () => { const callback = () => {
if (this.element.classList.contains('disabled')) { if (this.element.classList.contains('disabled')) {
@ -33,4 +36,4 @@ class ButtonsToggleListener {
} }
} }
export default ButtonsToggleListener; export default ButtonsToggleListener;

View file

@ -0,0 +1,24 @@
import dataClientIdAttributeHandler from "../DataClientIdAttributeHandler";
export const loadPaypalScript = (config, onLoaded) => {
if (typeof paypal !== 'undefined') {
onLoaded();
return;
}
const script = document.createElement('script');
script.addEventListener('load', onLoaded);
script.setAttribute('src', config.url);
Object.entries(config.script_attributes).forEach(
(keyValue) => {
script.setAttribute(keyValue[0], keyValue[1]);
}
);
if (config.data_client_id.set_attribute) {
dataClientIdAttributeHandler(script, config.data_client_id);
return;
}
document.body.appendChild(script);
}

View file

@ -53,14 +53,5 @@ class MessageRenderer {
} }
return true; return true;
} }
hideMessages() {
const domElement = document.querySelector(this.config.wrapper);
if (! domElement ) {
return false;
}
domElement.style.display = 'none';
return true;
}
} }
export default MessageRenderer; export default MessageRenderer;

View file

@ -99,24 +99,6 @@ class Renderer {
return this.renderedSources.has(wrapper + fundingSource ?? ''); return this.renderedSources.has(wrapper + fundingSource ?? '');
} }
hideButtons(element) {
const domElement = document.querySelector(element);
if (! domElement ) {
return false;
}
domElement.style.display = 'none';
return true;
}
showButtons(element) {
const domElement = document.querySelector(element);
if (! domElement ) {
return false;
}
domElement.style.display = 'block';
return true;
}
disableCreditCardFields() { disableCreditCardFields() {
this.creditCardRenderer.disableFields(); this.creditCardRenderer.disableFields();
} }

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button; namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver; use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator; use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
@ -220,6 +221,12 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
'button.endpoint.cart-script-params' => static function ( ContainerInterface $container ): CartScriptParamsEndpoint {
return new CartScriptParamsEndpoint(
$container->get( 'button.smart-button' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure { 'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure {
$logger = $container->get( 'woocommerce.logger.woocommerce' ); $logger = $container->get( 'woocommerce.logger.woocommerce' );
return new ThreeDSecure( $logger ); return new ThreeDSecure( $logger );

View file

@ -24,12 +24,16 @@ class DisabledSmartButton implements SmartButtonInterface {
} }
/** /**
* Enqueues necessary scripts. * Whether the scripts should be loaded.
*
* @return bool
*/ */
public function enqueue(): bool { public function should_load(): bool {
return true; return false;
}
/**
* Enqueues necessary scripts.
*/
public function enqueue(): void {
} }
/** /**
@ -41,4 +45,13 @@ class DisabledSmartButton implements SmartButtonInterface {
return false; return false;
} }
/**
* The configuration for the smart buttons.
*
* @return array
*/
public function script_data(): array {
return array();
}
} }

View file

@ -17,6 +17,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
@ -506,17 +507,25 @@ class SmartButton implements SmartButtonInterface {
} }
/** /**
* Enqueues the script. * Whether the scripts should be loaded.
*
* @return bool
* @throws NotFoundException When a setting was not found.
*/ */
public function enqueue(): bool { public function should_load(): bool {
$buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); $buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' );
if ( ! is_checkout() && ! $buttons_enabled ) { if ( ! is_checkout() && ! $buttons_enabled ) {
return false; return false;
} }
return true;
}
/**
* Enqueues the scripts.
*/
public function enqueue(): void {
if ( ! $this->should_load() ) {
return;
}
$load_script = false; $load_script = false;
if ( is_checkout() && $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) { if ( is_checkout() && $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) {
$load_script = true; $load_script = true;
@ -554,10 +563,9 @@ class SmartButton implements SmartButtonInterface {
wp_localize_script( wp_localize_script(
'ppcp-smart-button', 'ppcp-smart-button',
'PayPalCommerceGateway', 'PayPalCommerceGateway',
$this->localize_script() $this->script_data()
); );
} }
return true;
} }
/** /**
@ -751,18 +759,22 @@ class SmartButton implements SmartButtonInterface {
} }
/** /**
* The localized data for the smart button. * The configuration for the smart buttons.
* *
* @return array * @return array
* @throws NotFoundException If a setting hasn't been found. * @throws NotFoundException If a setting hasn't been found.
*/ */
private function localize_script(): array { public function script_data(): array {
global $wp; global $wp;
$is_free_trial_cart = $this->is_free_trial_cart(); $is_free_trial_cart = $this->is_free_trial_cart();
$url_params = $this->url_params();
$this->request_data->enqueue_nonce_fix(); $this->request_data->enqueue_nonce_fix();
$localize = array( $localize = array(
'url' => add_query_arg( $url_params, 'https://www.paypal.com/sdk/js' ),
'url_params' => $url_params,
'script_attributes' => $this->attributes(), 'script_attributes' => $this->attributes(),
'data_client_id' => array( 'data_client_id' => array(
'set_attribute' => ( is_checkout() && $this->dcc_is_enabled() ) || $this->can_save_vault_token(), 'set_attribute' => ( is_checkout() && $this->dcc_is_enabled() ) || $this->can_save_vault_token(),
@ -798,6 +810,9 @@ class SmartButton implements SmartButtonInterface {
'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ), 'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ),
), ),
'cart_script_params' => array(
'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ),
),
), ),
'enforce_vault' => $this->has_subscriptions(), 'enforce_vault' => $this->has_subscriptions(),
'can_save_vault_token' => $this->can_save_vault_token(), 'can_save_vault_token' => $this->can_save_vault_token(),
@ -809,7 +824,6 @@ class SmartButton implements SmartButtonInterface {
'wrapper' => '#ppc-button-' . PayPalGateway::ID, 'wrapper' => '#ppc-button-' . PayPalGateway::ID,
'mini_cart_wrapper' => '#ppc-button-minicart', 'mini_cart_wrapper' => '#ppc-button-minicart',
'cancel_wrapper' => '#ppcp-cancel', 'cancel_wrapper' => '#ppcp-cancel',
'url' => $this->url(),
'mini_cart_style' => array( 'mini_cart_style' => array(
'layout' => $this->style_for_context( 'layout', 'mini-cart' ), 'layout' => $this->style_for_context( 'layout', 'mini-cart' ),
'color' => $this->style_for_context( 'color', 'mini-cart' ), 'color' => $this->style_for_context( 'color', 'mini-cart' ),
@ -916,12 +930,12 @@ class SmartButton implements SmartButtonInterface {
} }
/** /**
* The JavaScript SDK url to load. * The JavaScript SDK url parameters.
* *
* @return string * @return array
* @throws NotFoundException If a setting was not found. * @throws NotFoundException If a setting was not found.
*/ */
private function url(): string { private function url_params(): array {
$intent = ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture'; $intent = ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture';
$product_intent = $this->subscription_helper->current_product_is_subscription() ? 'authorize' : $intent; $product_intent = $this->subscription_helper->current_product_is_subscription() ? 'authorize' : $intent;
$other_context_intent = $this->subscription_helper->cart_contains_subscription() ? 'authorize' : $intent; $other_context_intent = $this->subscription_helper->cart_contains_subscription() ? 'authorize' : $intent;
@ -993,8 +1007,7 @@ class SmartButton implements SmartButtonInterface {
$params['enable-funding'] = implode( ',', array_unique( $enable_funding ) ); $params['enable-funding'] = implode( ',', array_unique( $enable_funding ) );
} }
$smart_button_url = add_query_arg( $params, 'https://www.paypal.com/sdk/js' ); return $params;
return $smart_button_url;
} }
/** /**

View file

@ -22,11 +22,14 @@ interface SmartButtonInterface {
public function render_wrapper(): bool; public function render_wrapper(): bool;
/** /**
* Enqueues the necessary scripts. * Whether the scripts should be loaded.
*
* @return bool
*/ */
public function enqueue(): bool; public function should_load(): bool;
/**
* Enqueues the necessary scripts.
*/
public function enqueue(): void;
/** /**
* Whether the running installation could save vault tokens or not. * Whether the running installation could save vault tokens or not.
@ -34,4 +37,11 @@ interface SmartButtonInterface {
* @return bool * @return bool
*/ */
public function can_save_vault_token(): bool; public function can_save_vault_token(): bool;
/**
* The configuration for the smart buttons.
*
* @return array
*/
public function script_data(): array;
} }

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button; namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
@ -177,6 +178,15 @@ class ButtonModule implements ModuleInterface {
$endpoint->handle_request(); $endpoint->handle_request();
} }
); );
add_action(
'wc_ajax_' . CartScriptParamsEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.cart-script-params' );
assert( $endpoint instanceof CartScriptParamsEndpoint );
$endpoint->handle_request();
}
);
} }
/** /**

View file

@ -0,0 +1,80 @@
<?php
/**
* The endpoint for returning the PayPal SDK Script parameters for the current cart.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
/**
* Class CartScriptParamsEndpoint.
*/
class CartScriptParamsEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-cart-script-params';
/**
* The SmartButton.
*
* @var SmartButton
*/
private $smart_button;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* CartScriptParamsEndpoint constructor.
*
* @param SmartButton $smart_button he SmartButton.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
SmartButton $smart_button,
LoggerInterface $logger
) {
$this->smart_button = $smart_button;
$this->logger = $logger;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
try {
$script_data = $this->smart_button->script_data();
wp_send_json_success( $script_data['url_params'] );
return true;
} catch ( Throwable $error ) {
$this->logger->error( "CartScriptParamsEndpoint execution failed. {$error->getMessage()} {$error->getFile()}:{$error->getLine()}" );
wp_send_json_error();
return false;
}
}
}

View file

@ -50,7 +50,7 @@ class SubscriptionHelper {
if ( ! isset( $item['data'] ) || ! is_a( $item['data'], WC_Product::class ) ) { if ( ! isset( $item['data'] ) || ! is_a( $item['data'], WC_Product::class ) ) {
continue; continue;
} }
if ( $item['data']->is_type( 'subscription' ) || $item['data']->is_type( 'subscription_variation' ) ) { if ( WC_Subscriptions_Product::is_subscription( $item['data'] ) ) {
return true; return true;
} }
} }