From 587e065fba83e0270f359ea36797974101b9fd53 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Fri, 23 Jun 2023 08:23:11 +0100
Subject: [PATCH 1/4] Refactored button display logic Replaced show / hide
buttons mode with enable / disable buttons mode
---
.../SingleProductActionHandler.js | 1 -
.../ContextBootstrap/SingleProductBootstap.js | 67 +++++++++++--------
.../js/modules/Helper/ButtonDisabler.js | 58 ++++++++++++++++
.../modules/Helper/ButtonsToggleListener.js | 39 -----------
4 files changed, 98 insertions(+), 67 deletions(-)
create mode 100644 modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js
delete mode 100644 modules/ppcp-button/resources/js/modules/Helper/ButtonsToggleListener.js
diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js
index 3c3371542..146fd7e94 100644
--- a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js
+++ b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js
@@ -1,4 +1,3 @@
-import ButtonsToggleListener from '../Helper/ButtonsToggleListener';
import Product from '../Entity/Product';
import onApprove from '../OnApproveHandler/onApproveForContinue';
import {payerData} from "../Helper/PayerData";
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
index 376962772..c6b86657c 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
@@ -1,7 +1,6 @@
import UpdateCart from "../Helper/UpdateCart";
import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler";
-import {hide, show, setVisible} from "../Helper/Hiding";
-import ButtonsToggleListener from "../Helper/ButtonsToggleListener";
+import {disable, enable} from "../Helper/ButtonDisabler";
class SingleProductBootstap {
constructor(gateway, renderer, messages, errorHandler) {
@@ -10,56 +9,70 @@ class SingleProductBootstap {
this.messages = messages;
this.errorHandler = errorHandler;
this.mutationObserver = new MutationObserver(this.handleChange.bind(this));
+ this.formSelector = 'form.cart';
}
+ form() {
+ return document.querySelector(this.formSelector);
+ }
handleChange() {
- const shouldRender = this.shouldRender();
- setVisible(this.gateway.button.wrapper, shouldRender);
- setVisible(this.gateway.messages.wrapper, shouldRender);
- if (!shouldRender) {
+ if (!this.shouldRender()) {
return;
}
this.render();
+ this.handleButtonStatus();
+ }
+
+ handleButtonStatus() {
+ if (!this.shouldEnable()) {
+ disable(this.gateway.button.wrapper, this.formSelector);
+ disable(this.gateway.messages.wrapper);
+ return;
+ }
+ enable(this.gateway.button.wrapper);
+ enable(this.gateway.messages.wrapper);
+ this.messages.renderWithAmount(this.priceAmount())
}
init() {
- const form = document.querySelector('form.cart');
+ const form = this.form();
+
if (!form) {
return;
}
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();
+ const addToCartButton = form.querySelector('.single_add_to_cart_button');
+
+ if (addToCartButton) {
+ (new MutationObserver(this.handleButtonStatus.bind(this)))
+ .observe(addToCartButton, { attributes : true });
+ }
if (!this.shouldRender()) {
- hide(this.gateway.button.wrapper);
- hide(this.gateway.messages.wrapper);
return;
}
this.render();
+ this.handleButtonStatus();
}
shouldRender() {
- return document.querySelector('form.cart') !== null
+ return this.form() !== null
+ && !this.isDisabledReasonExternalPlugins();
+ }
+
+ shouldEnable() {
+ const form = this.form();
+ const addToCartButton = form ? form.querySelector('.single_add_to_cart_button') : null;
+
+ return this.shouldRender()
&& !this.priceAmountIsZero()
- && !this.isSubscriptionMode();
+ && ((null === addToCartButton) || !addToCartButton.classList.contains('disabled'));
}
priceAmount() {
@@ -93,7 +106,7 @@ class SingleProductBootstap {
return !price || price === 0;
}
- isSubscriptionMode() {
+ isDisabledReasonExternalPlugins() {
// 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
@@ -106,7 +119,7 @@ class SingleProductBootstap {
this.gateway.ajax.change_cart.endpoint,
this.gateway.ajax.change_cart.nonce,
),
- document.querySelector('form.cart'),
+ this.form(),
this.errorHandler,
);
diff --git a/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js b/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js
new file mode 100644
index 000000000..b9f98cbab
--- /dev/null
+++ b/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js
@@ -0,0 +1,58 @@
+/**
+ * @param selectorOrElement
+ * @returns {Element}
+ */
+const getElement = (selectorOrElement) => {
+ if (typeof selectorOrElement === 'string') {
+ return document.querySelector(selectorOrElement);
+ }
+ return selectorOrElement;
+}
+
+export const setEnabled = (selectorOrElement, enable, form = null) => {
+ const element = getElement(selectorOrElement);
+
+ if (!element) {
+ return;
+ }
+
+ if (enable) {
+ jQuery(element).css({
+ 'cursor': '',
+ '-webkit-filter': '',
+ 'filter': '',
+ } )
+ .off('mouseup')
+ .find('> *')
+ .css('pointer-events', '');
+ } else {
+ jQuery(element).css( {
+ 'cursor': '',
+ '-webkit-filter': '',
+ 'filter': '',
+ } )
+ .css({
+ 'cursor': 'not-allowed',
+ '-webkit-filter': 'grayscale(100%)',
+ 'filter': 'grayscale(100%)',
+ })
+ .on('mouseup', function(event) {
+ event.stopImmediatePropagation();
+
+ if (form) {
+ // Trigger form submit to show the error message
+ jQuery(form).find(':submit').trigger('click');
+ }
+ })
+ .find('> *')
+ .css('pointer-events', 'none');
+ }
+};
+
+export const disable = (selectorOrElement, form = null) => {
+ setEnabled(selectorOrElement, false, form);
+};
+
+export const enable = (selectorOrElement) => {
+ setEnabled(selectorOrElement, true);
+};
diff --git a/modules/ppcp-button/resources/js/modules/Helper/ButtonsToggleListener.js b/modules/ppcp-button/resources/js/modules/Helper/ButtonsToggleListener.js
deleted file mode 100644
index add1ee28a..000000000
--- a/modules/ppcp-button/resources/js/modules/Helper/ButtonsToggleListener.js
+++ /dev/null
@@ -1,39 +0,0 @@
-/**
- * When you can't add something to the cart, the PayPal buttons should not show.
- * Therefore we listen for changes on the add to cart button and show/hide the buttons accordingly.
- */
-
-class ButtonsToggleListener {
- constructor(element, showCallback, hideCallback)
- {
- this.element = element;
- this.showCallback = showCallback;
- this.hideCallback = hideCallback;
- this.observer = null;
- }
-
- init()
- {
- if (!this.element) {
- return;
- }
- const config = { attributes : true };
- const callback = () => {
- if (this.element.classList.contains('disabled')) {
- this.hideCallback();
- return;
- }
- this.showCallback();
- }
- this.observer = new MutationObserver(callback);
- this.observer.observe(this.element, config);
- callback();
- }
-
- disconnect()
- {
- this.observer.disconnect();
- }
-}
-
-export default ButtonsToggleListener;
From 75bf98c1748459ba821884b68b1627743aa68766 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Fri, 23 Jun 2023 15:49:08 +0100
Subject: [PATCH 2/4] Add hide / show conditions on SingleProduct Buttons for
when they shouldn't be rendered. Refactor MessageRenderer not to reload when
it has no changes.
---
.../ContextBootstrap/SingleProductBootstap.js | 7 +++
.../js/modules/Renderer/MessageRenderer.js | 52 +++++++++++++------
2 files changed, 44 insertions(+), 15 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
index c6b86657c..8d6f61cb1 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
@@ -1,5 +1,6 @@
import UpdateCart from "../Helper/UpdateCart";
import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler";
+import {hide, show} from "../Helper/Hiding";
import {disable, enable} from "../Helper/ButtonDisabler";
class SingleProductBootstap {
@@ -18,10 +19,16 @@ class SingleProductBootstap {
handleChange() {
if (!this.shouldRender()) {
+ hide(this.gateway.button.wrapper, this.formSelector);
+ hide(this.gateway.messages.wrapper);
return;
}
this.render();
+
+ show(this.gateway.button.wrapper, this.formSelector);
+ show(this.gateway.messages.wrapper);
+
this.handleButtonStatus();
}
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
index a07e81d21..82c10769f 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
@@ -2,6 +2,7 @@ class MessageRenderer {
constructor(config) {
this.config = config;
+ this.optionsFingerprint = null;
}
render() {
@@ -9,18 +10,20 @@ class MessageRenderer {
return;
}
- paypal.Messages({
+ const options = {
amount: this.config.amount,
placement: this.config.placement,
style: this.config.style
- }).render(this.config.wrapper);
+ };
+
+ if (this.isOptionsFingerprintEqual(options)) {
+ return;
+ }
+
+ paypal.Messages(options).render(this.config.wrapper);
jQuery(document.body).on('updated_cart_totals', () => {
- paypal.Messages({
- amount: this.config.amount,
- placement: this.config.placement,
- style: this.config.style
- }).render(this.config.wrapper);
+ paypal.Messages(options).render(this.config.wrapper);
});
}
@@ -30,17 +33,36 @@ class MessageRenderer {
return;
}
- const newWrapper = document.createElement('div');
- newWrapper.setAttribute('id', this.config.wrapper.replace('#', ''));
-
- const sibling = document.querySelector(this.config.wrapper).nextSibling;
- document.querySelector(this.config.wrapper).parentElement.removeChild(document.querySelector(this.config.wrapper));
- sibling.parentElement.insertBefore(newWrapper, sibling);
- paypal.Messages({
+ const options = {
amount,
placement: this.config.placement,
style: this.config.style
- }).render(this.config.wrapper);
+ };
+
+ if (this.isOptionsFingerprintEqual(options)) {
+ return;
+ }
+
+ const newWrapper = document.createElement('div');
+ newWrapper.setAttribute('id', this.config.wrapper.replace('#', ''));
+
+ const oldWrapper = document.querySelector(this.config.wrapper);
+ const sibling = oldWrapper.nextSibling;
+ oldWrapper.parentElement.removeChild(oldWrapper);
+ sibling.parentElement.insertBefore(newWrapper, sibling);
+
+ paypal.Messages(options).render(this.config.wrapper);
+ }
+
+ isOptionsFingerprintEqual(options) {
+ const fingerprint = JSON.stringify(options);
+
+ if (this.optionsFingerprint === fingerprint) {
+ return true;
+ }
+
+ this.optionsFingerprint = fingerprint;
+ return false;
}
shouldRender() {
From adf7a2e2977e8fc55e810c4e217f4b56d57a4d7a Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Mon, 26 Jun 2023 11:55:30 +0100
Subject: [PATCH 3/4] Remove unnecessary code
---
.../resources/js/modules/Helper/ButtonDisabler.js | 15 +++++----------
1 file changed, 5 insertions(+), 10 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js b/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js
index b9f98cbab..9e4bf51ee 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/ButtonDisabler.js
@@ -18,20 +18,15 @@ export const setEnabled = (selectorOrElement, enable, form = null) => {
if (enable) {
jQuery(element).css({
- 'cursor': '',
- '-webkit-filter': '',
- 'filter': '',
- } )
+ 'cursor': '',
+ '-webkit-filter': '',
+ 'filter': '',
+ } )
.off('mouseup')
.find('> *')
.css('pointer-events', '');
} else {
- jQuery(element).css( {
- 'cursor': '',
- '-webkit-filter': '',
- 'filter': '',
- } )
- .css({
+ jQuery(element).css({
'cursor': 'not-allowed',
'-webkit-filter': 'grayscale(100%)',
'filter': 'grayscale(100%)',
From 82828c299123fe7f40421deb9a356cae6b7ea545 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Mon, 26 Jun 2023 18:14:41 +0100
Subject: [PATCH 4/4] Add support for PayPal SmartButtons enable / disable
Rename functions Remove invalid function arguments
---
modules/ppcp-button/resources/js/button.js | 18 ++++++++++--
.../ContextBootstrap/SingleProductBootstap.js | 29 +++++++++++++++----
.../js/modules/Renderer/MessageRenderer.js | 7 ++---
.../resources/js/modules/Renderer/Renderer.js | 17 ++++++++++-
4 files changed, 57 insertions(+), 14 deletions(-)
diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js
index 6708b0275..0fbccd095 100644
--- a/modules/ppcp-button/resources/js/button.js
+++ b/modules/ppcp-button/resources/js/button.js
@@ -121,10 +121,22 @@ const bootstrap = () => {
return actions.reject();
}
};
- const onSmartButtonsInit = () => {
- buttonsSpinner.unblock();
+
+ let smartButtonsOptions = {
+ onInit: null,
+ init: function (actions) {
+ this.actions = actions;
+ if (typeof this.onInit === 'function') {
+ this.onInit();
+ }
+ }
};
- const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit);
+
+ const onSmartButtonsInit = (data, actions) => {
+ buttonsSpinner.unblock();
+ smartButtonsOptions.init(actions);
+ };
+ const renderer = new Renderer(creditCardRenderer, PayPalCommerceGateway, onSmartButtonClick, onSmartButtonsInit, smartButtonsOptions);
const messageRenderer = new MessageRenderer(PayPalCommerceGateway.messages);
const context = PayPalCommerceGateway.context;
if (context === 'mini-cart' || context === 'product') {
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
index 8d6f61cb1..540b085c1 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
@@ -11,6 +11,12 @@ class SingleProductBootstap {
this.errorHandler = errorHandler;
this.mutationObserver = new MutationObserver(this.handleChange.bind(this));
this.formSelector = 'form.cart';
+
+ if (this.renderer && this.renderer.smartButtonsOptions) {
+ this.renderer.smartButtonsOptions.onInit = () => {
+ this.handleChange();
+ };
+ }
}
form() {
@@ -19,6 +25,7 @@ class SingleProductBootstap {
handleChange() {
if (!this.shouldRender()) {
+ this.renderer.disableSmartButtons();
hide(this.gateway.button.wrapper, this.formSelector);
hide(this.gateway.messages.wrapper);
return;
@@ -26,7 +33,8 @@ class SingleProductBootstap {
this.render();
- show(this.gateway.button.wrapper, this.formSelector);
+ this.renderer.enableSmartButtons();
+ show(this.gateway.button.wrapper);
show(this.gateway.messages.wrapper);
this.handleButtonStatus();
@@ -34,13 +42,14 @@ class SingleProductBootstap {
handleButtonStatus() {
if (!this.shouldEnable()) {
+ this.renderer.disableSmartButtons();
disable(this.gateway.button.wrapper, this.formSelector);
disable(this.gateway.messages.wrapper);
return;
}
+ this.renderer.enableSmartButtons();
enable(this.gateway.button.wrapper);
enable(this.gateway.messages.wrapper);
- this.messages.renderWithAmount(this.priceAmount())
}
init() {
@@ -50,7 +59,15 @@ class SingleProductBootstap {
return;
}
- form.addEventListener('change', this.handleChange.bind(this));
+ form.addEventListener('change', () => {
+ this.handleChange();
+
+ setTimeout(() => { // Wait for the DOM to be fully updated
+ // For the moment renderWithAmount should only be done here to prevent undesired side effects due to priceAmount()
+ // not being correctly formatted in some cases, can be moved to handleButtonStatus() once this issue is fixed
+ this.messages.renderWithAmount(this.priceAmount());
+ }, 100);
+ });
this.mutationObserver.observe(form, { childList: true, subtree: true });
const addToCartButton = form.querySelector('.single_add_to_cart_button');
@@ -65,12 +82,12 @@ class SingleProductBootstap {
}
this.render();
- this.handleButtonStatus();
+ this.handleChange();
}
shouldRender() {
return this.form() !== null
- && !this.isDisabledReasonExternalPlugins();
+ && !this.isWcsattSubscriptionMode();
}
shouldEnable() {
@@ -113,7 +130,7 @@ class SingleProductBootstap {
return !price || price === 0;
}
- isDisabledReasonExternalPlugins() {
+ isWcsattSubscriptionMode() {
// 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
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
index 82c10769f..80c90c0ef 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
@@ -16,7 +16,7 @@ class MessageRenderer {
style: this.config.style
};
- if (this.isOptionsFingerprintEqual(options)) {
+ if (this.optionsEqual(options)) {
return;
}
@@ -28,7 +28,6 @@ class MessageRenderer {
}
renderWithAmount(amount) {
-
if (! this.shouldRender()) {
return;
}
@@ -39,7 +38,7 @@ class MessageRenderer {
style: this.config.style
};
- if (this.isOptionsFingerprintEqual(options)) {
+ if (this.optionsEqual(options)) {
return;
}
@@ -54,7 +53,7 @@ class MessageRenderer {
paypal.Messages(options).render(this.config.wrapper);
}
- isOptionsFingerprintEqual(options) {
+ optionsEqual(options) {
const fingerprint = JSON.stringify(options);
if (this.optionsFingerprint === fingerprint) {
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
index 17f58aff3..efd3c0bfa 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
@@ -1,11 +1,12 @@
import merge from "deepmerge";
class Renderer {
- constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) {
+ constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit, smartButtonsOptions) {
this.defaultSettings = defaultSettings;
this.creditCardRenderer = creditCardRenderer;
this.onSmartButtonClick = onSmartButtonClick;
this.onSmartButtonsInit = onSmartButtonsInit;
+ this.smartButtonsOptions = smartButtonsOptions;
this.renderedSources = new Set();
}
@@ -106,6 +107,20 @@ class Renderer {
enableCreditCardFields() {
this.creditCardRenderer.enableFields();
}
+
+ disableSmartButtons() {
+ if (!this.smartButtonsOptions || !this.smartButtonsOptions.actions) {
+ return;
+ }
+ this.smartButtonsOptions.actions.disable();
+ }
+
+ enableSmartButtons() {
+ if (!this.smartButtonsOptions || !this.smartButtonsOptions.actions) {
+ return;
+ }
+ this.smartButtonsOptions.actions.enable();
+ }
}
export default Renderer;