Add throttling to simulate_cart ajax call
This commit is contained in:
Pedro Silva 2023-07-20 17:51:48 +01:00
parent a0480e35bb
commit 84a362a7c5
No known key found for this signature in database
GPG key ID: E2EE20C0669D24B3
13 changed files with 203 additions and 114 deletions

View file

@ -13,13 +13,12 @@ import {
ORDER_BUTTON_SELECTOR, ORDER_BUTTON_SELECTOR,
PaymentMethods PaymentMethods
} from "./modules/Helper/CheckoutMethodState"; } from "./modules/Helper/CheckoutMethodState";
import {hide, setVisible, setVisibleByClass} from "./modules/Helper/Hiding"; import {setVisibleByClass} from "./modules/Helper/Hiding";
import {isChangePaymentPage} from "./modules/Helper/Subscriptions"; 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"; import {loadPaypalScript} from "./modules/Helper/ScriptLoading";
import widgetBuilder from "./modules/Renderer/WidgetBuilder";
// 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.
@ -268,8 +267,6 @@ document.addEventListener(
}); });
loadPaypalScript(PayPalCommerceGateway, () => { loadPaypalScript(PayPalCommerceGateway, () => {
widgetBuilder.setPaypal(paypal);
bootstrapped = true; bootstrapped = true;
bootstrap(); bootstrap();

View file

@ -49,11 +49,10 @@ class CartBootstrap {
} }
// handle button status // handle button status
if ( result.data.button ) { if (result.data.button) {
this.gateway.button = result.data.button; this.gateway.button = result.data.button;
}
this.handleButtonStatus(); this.handleButtonStatus();
}
if (this.lastAmount !== result.data.amount) { if (this.lastAmount !== result.data.amount) {
this.lastAmount = result.data.amount; this.lastAmount = result.data.amount;

View file

@ -3,7 +3,7 @@ import SingleProductActionHandler from "../ActionHandler/SingleProductActionHand
import {hide, show} from "../Helper/Hiding"; import {hide, show} from "../Helper/Hiding";
import BootstrapHelper from "../Helper/BootstrapHelper"; import BootstrapHelper from "../Helper/BootstrapHelper";
import SimulateCart from "../Helper/SimulateCart"; import SimulateCart from "../Helper/SimulateCart";
import {strRemoveWord, strAddWord} from "../Helper/Utils"; import {strRemoveWord, strAddWord, throttle} from "../Helper/Utils";
class SingleProductBootstap { class SingleProductBootstap {
constructor(gateway, renderer, messages, errorHandler) { constructor(gateway, renderer, messages, errorHandler) {
@ -14,6 +14,9 @@ class SingleProductBootstap {
this.mutationObserver = new MutationObserver(this.handleChange.bind(this)); this.mutationObserver = new MutationObserver(this.handleChange.bind(this));
this.formSelector = 'form.cart'; this.formSelector = 'form.cart';
// Prevent simulate cart being called too many times in a burst.
this.simulateCartThrottled = throttle(this.simulateCart, 5000);
this.renderer.onButtonsInit(this.gateway.button.wrapper, () => { this.renderer.onButtonsInit(this.gateway.button.wrapper, () => {
this.handleChange(); this.handleChange();
}, true); }, true);
@ -46,53 +49,7 @@ class SingleProductBootstap {
}); });
if (simulateCart) { if (simulateCart) {
//------ this.simulateCartThrottled();
const actionHandler = new SingleProductActionHandler(
null,
null,
this.form(),
this.errorHandler,
);
(new SimulateCart(
this.gateway.ajax.simulate_cart.endpoint,
this.gateway.ajax.simulate_cart.nonce,
)).simulate((data) => {
this.messages.renderWithAmount(data.total);
let enableFunding = this.gateway.url_params['enable-funding'];
let disableFunding = this.gateway.url_params['disable-funding'];
for (const [fundingSource, funding] of Object.entries(data.funding)) {
if (funding.enabled === true) {
enableFunding = strAddWord(enableFunding, fundingSource);
disableFunding = strRemoveWord(disableFunding, fundingSource);
} else if (funding.enabled === false) {
enableFunding = strRemoveWord(enableFunding, fundingSource);
disableFunding = strAddWord(disableFunding, fundingSource);
}
}
if (
(enableFunding !== this.gateway.url_params['enable-funding']) ||
(disableFunding !== this.gateway.url_params['disable-funding'])
) {
this.gateway.url_params['enable-funding'] = enableFunding;
this.gateway.url_params['disable-funding'] = disableFunding;
jQuery(this.gateway.button.wrapper).trigger('ppcp-reload-buttons');
}
if (typeof data.button.is_disabled === 'boolean') {
this.gateway.button.is_disabled = data.button.is_disabled;
}
this.handleButtonStatus(false);
}, actionHandler.getProducts());
//------
} }
} }
@ -197,6 +154,52 @@ class SingleProductBootstap {
actionHandler.configuration() actionHandler.configuration()
); );
} }
simulateCart() {
const actionHandler = new SingleProductActionHandler(
null,
null,
this.form(),
this.errorHandler,
);
(new SimulateCart(
this.gateway.ajax.simulate_cart.endpoint,
this.gateway.ajax.simulate_cart.nonce,
)).simulate((data) => {
this.messages.renderWithAmount(data.total);
let enableFunding = this.gateway.url_params['enable-funding'];
let disableFunding = this.gateway.url_params['disable-funding'];
for (const [fundingSource, funding] of Object.entries(data.funding)) {
if (funding.enabled === true) {
enableFunding = strAddWord(enableFunding, fundingSource);
disableFunding = strRemoveWord(disableFunding, fundingSource);
} else if (funding.enabled === false) {
enableFunding = strRemoveWord(enableFunding, fundingSource);
disableFunding = strAddWord(disableFunding, fundingSource);
}
}
if (
(enableFunding !== this.gateway.url_params['enable-funding']) ||
(disableFunding !== this.gateway.url_params['disable-funding'])
) {
this.gateway.url_params['enable-funding'] = enableFunding;
this.gateway.url_params['disable-funding'] = disableFunding;
jQuery(this.gateway.button.wrapper).trigger('ppcp-reload-buttons');
}
if (typeof data.button.is_disabled === 'boolean') {
this.gateway.button.is_disabled = data.button.is_disabled;
}
this.handleButtonStatus(false);
}, actionHandler.getProducts());
}
} }
export default SingleProductBootstap; export default SingleProductBootstap;

View file

@ -1,3 +1,6 @@
import {loadScript} from "@paypal/paypal-js";
import widgetBuilder from "./Renderer/WidgetBuilder";
const storageKey = 'ppcp-data-client-id'; const storageKey = 'ppcp-data-client-id';
const validateToken = (token, user) => { const validateToken = (token, user) => {
@ -24,7 +27,7 @@ const storeToken = (token) => {
sessionStorage.setItem(storageKey, JSON.stringify(token)); sessionStorage.setItem(storageKey, JSON.stringify(token));
} }
const dataClientIdAttributeHandler = (script, config) => { const dataClientIdAttributeHandler = (scriptOptions, config, callback) => {
fetch(config.endpoint, { fetch(config.endpoint, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -42,8 +45,14 @@ const dataClientIdAttributeHandler = (script, config) => {
return; return;
} }
storeToken(data); storeToken(data);
script.setAttribute('data-client-token', data.token);
document.body.appendChild(script); scriptOptions['data-client-token'] = data.token;
loadScript(scriptOptions).then((paypal) => {
if (typeof callback === 'function') {
callback(paypal);
}
});
}); });
} }

View file

@ -1,4 +1,8 @@
import dataClientIdAttributeHandler from "../DataClientIdAttributeHandler"; import dataClientIdAttributeHandler from "../DataClientIdAttributeHandler";
import {loadScript} from "@paypal/paypal-js";
import widgetBuilder from "../Renderer/WidgetBuilder";
import merge from "deepmerge";
import {keysToCamelCase} from "./Utils";
export const loadPaypalScript = (config, onLoaded) => { export const loadPaypalScript = (config, onLoaded) => {
if (typeof paypal !== 'undefined') { if (typeof paypal !== 'undefined') {
@ -6,19 +10,18 @@ export const loadPaypalScript = (config, onLoaded) => {
return; return;
} }
const script = document.createElement('script'); const callback = (paypal) => {
script.addEventListener('load', onLoaded); widgetBuilder.setPaypal(paypal);
script.setAttribute('src', config.url); onLoaded();
Object.entries(config.script_attributes).forEach(
(keyValue) => {
script.setAttribute(keyValue[0], keyValue[1]);
} }
);
let scriptOptions = keysToCamelCase(config.url_params);
scriptOptions = merge(scriptOptions, config.script_attributes);
if (config.data_client_id.set_attribute) { if (config.data_client_id.set_attribute) {
dataClientIdAttributeHandler(script, config.data_client_id); dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback);
return; return;
} }
document.body.appendChild(script); loadScript(scriptOptions).then(callback);
} }

View file

@ -31,11 +31,54 @@ export const strRemoveWord = (str, word, separator = ',') => {
return arr.join(separator); return arr.join(separator);
}; };
export const debounce = (func, wait) => {
let timeout;
return function() {
const context = this;
const args = arguments;
const later = function() {
timeout = null;
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
export const throttle = (func, limit) => {
let inThrottle, lastArgs, lastContext;
function execute() {
inThrottle = true;
func.apply(this, arguments);
setTimeout(() => {
inThrottle = false;
if (lastArgs) {
const nextArgs = lastArgs;
const nextContext = lastContext;
lastArgs = lastContext = null;
execute.apply(nextContext, nextArgs);
}
}, limit);
}
return function() {
if (!inThrottle) {
execute.apply(this, arguments);
} else {
lastArgs = arguments;
lastContext = this;
}
};
}
const Utils = { const Utils = {
toCamelCase, toCamelCase,
keysToCamelCase, keysToCamelCase,
strAddWord, strAddWord,
strRemoveWord strRemoveWord,
debounce,
throttle
}; };
export default Utils; export default Utils;

View file

@ -74,6 +74,8 @@ class Renderer {
renderButtons(wrapper, style, contextConfig, hasEnabledSeparateGateways, fundingSource = null) { renderButtons(wrapper, style, contextConfig, hasEnabledSeparateGateways, fundingSource = null) {
if (! document.querySelector(wrapper) || this.isAlreadyRendered(wrapper, fundingSource, hasEnabledSeparateGateways) ) { if (! document.querySelector(wrapper) || this.isAlreadyRendered(wrapper, fundingSource, hasEnabledSeparateGateways) ) {
// Try to render registered buttons again in case they were removed from the DOM by an external source.
widgetBuilder.renderButtons(wrapper);
return; return;
} }
@ -95,8 +97,9 @@ class Renderer {
} }
} }
//jQuery(document).off('ppcp-reload-buttons'); jQuery(document)
jQuery(document).on('ppcp-reload-buttons', wrapper, (event, settingsOverride = {}) => { .off('ppcp-reload-buttons', wrapper)
.on('ppcp-reload-buttons', wrapper, (event, settingsOverride = {}) => {
const settings = merge(this.defaultSettings, settingsOverride); const settings = merge(this.defaultSettings, settingsOverride);
let scriptOptions = keysToCamelCase(settings.url_params); let scriptOptions = keysToCamelCase(settings.url_params);
scriptOptions = merge(scriptOptions, settings.script_attributes); scriptOptions = merge(scriptOptions, settings.script_attributes);
@ -104,8 +107,7 @@ class Renderer {
loadScript(scriptOptions).then((paypal) => { loadScript(scriptOptions).then((paypal) => {
widgetBuilder.setPaypal(paypal); widgetBuilder.setPaypal(paypal);
widgetBuilder.registerButtons(wrapper, buttonsOptions()); widgetBuilder.registerButtons(wrapper, buttonsOptions());
widgetBuilder.renderAllButtons(); widgetBuilder.renderAll();
widgetBuilder.renderAllMessages();
}); });
}); });

View file

@ -30,6 +30,10 @@ class WidgetBuilder {
return; return;
} }
if (this.hasRendered(wrapper)) {
return;
}
const entry = this.buttons.get(wrapper); const entry = this.buttons.get(wrapper);
const btn = this.paypal.Buttons(entry.options); const btn = this.paypal.Buttons(entry.options);
@ -58,6 +62,10 @@ class WidgetBuilder {
return; return;
} }
if (this.hasRendered(wrapper)) {
return;
}
const entry = this.messages.get(wrapper); const entry = this.messages.get(wrapper);
const btn = this.paypal.Messages(entry.options); const btn = this.paypal.Messages(entry.options);
@ -70,6 +78,14 @@ class WidgetBuilder {
} }
} }
renderAll() {
this.renderAllButtons();
this.renderAllMessages();
}
hasRendered(wrapper) {
return document.querySelector(wrapper).hasChildNodes();
}
} }
export default new WidgetBuilder(); export default new WidgetBuilder();

View file

@ -1,4 +1,9 @@
<?php <?php
/**
* Abstract class for cart Endpoints.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
namespace WooCommerce\PayPalCommerce\Button\Endpoint; namespace WooCommerce\PayPalCommerce\Button\Endpoint;
@ -6,6 +11,9 @@ use Exception;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
/**
* Abstract Class AbstractCartEndpoint
*/
abstract class AbstractCartEndpoint implements EndpointInterface { abstract class AbstractCartEndpoint implements EndpointInterface {
const ENDPOINT = ''; const ENDPOINT = '';
@ -50,7 +58,7 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
* *
* @var array * @var array
*/ */
private $cart_item_keys = []; private $cart_item_keys = array();
/** /**
* The nonce. * The nonce.
@ -97,7 +105,7 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
* *
* @param array $products Array of products to be added to cart. * @param array $products Array of products to be added to cart.
* @return bool * @return bool
* @throws Exception * @throws Exception Add to cart methods throw an exception on fail.
*/ */
protected function add_products( array $products ): bool { protected function add_products( array $products ): bool {
$this->cart->empty_cart( false ); $this->cart->empty_cart( false );
@ -164,6 +172,8 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
} }
/** /**
* Returns product information from request data.
*
* @return array|false * @return array|false
*/ */
protected function products_from_request() { protected function products_from_request() {
@ -304,6 +314,8 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
} }
/** /**
* Removes stored cart items from WooCommerce cart.
*
* @return void * @return void
*/ */
protected function remove_cart_items(): void { protected function remove_cart_items(): void {

View file

@ -65,7 +65,7 @@ class CartScriptParamsEndpoint implements EndpointInterface {
*/ */
public function handle_request(): bool { public function handle_request(): bool {
try { try {
if ( is_callable('wc_maybe_define_constant') ) { if ( is_callable( 'wc_maybe_define_constant' ) ) {
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true ); wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
} }

View file

@ -70,13 +70,15 @@ class ChangeCartEndpoint extends AbstractCartEndpoint {
* @throws Exception On error. * @throws Exception On error.
*/ */
protected function handle_data(): bool { protected function handle_data(): bool {
if ( ! $products = $this->products_from_request() ) { $products = $this->products_from_request();
if ( ! $products ) {
return false; return false;
} }
$this->shipping->reset_shipping(); $this->shipping->reset_shipping();
if ( ! $this->add_products($products) ) { if ( ! $this->add_products( $products ) ) {
return false; return false;
} }

View file

@ -59,40 +59,43 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
* @throws Exception On error. * @throws Exception On error.
*/ */
protected function handle_data(): bool { protected function handle_data(): bool {
if ( ! $products = $this->products_from_request() ) { $products = $this->products_from_request();
if ( ! $products ) {
return false; return false;
} }
// Set WC default cart as the clone. // Set WC default cart as the clone.
// Store a reference to the real cart. // Store a reference to the real cart.
$activeCart = WC()->cart; $active_cart = WC()->cart;
WC()->cart = $this->cart; WC()->cart = $this->cart;
if ( ! $this->add_products($products) ) { if ( ! $this->add_products( $products ) ) {
return false; return false;
} }
$this->cart->calculate_totals(); $this->cart->calculate_totals();
$total = (float) $this->cart->get_total( 'numeric' ); $total = (float) $this->cart->get_total( 'numeric' );
// Remove from cart because some plugins reserve resources internally when adding to cart.
$this->remove_cart_items(); $this->remove_cart_items();
// Restore cart and unset cart clone // Restore cart and unset cart clone.
WC()->cart = $activeCart; WC()->cart = $active_cart;
unset( $this->cart ); unset( $this->cart );
wp_send_json_success( wp_send_json_success(
array( array(
'total' => $total, 'total' => $total,
'funding' => [ 'funding' => array(
'paylater' => [ 'paylater' => array(
'enabled' => $this->smart_button->is_pay_later_button_enabled_for_location( 'cart', $total ), 'enabled' => $this->smart_button->is_pay_later_button_enabled_for_location( 'cart', $total ),
'messaging_enabled' => $this->smart_button->is_pay_later_messaging_enabled_for_location( 'cart', $total ), 'messaging_enabled' => $this->smart_button->is_pay_later_messaging_enabled_for_location( 'cart', $total ),
] ),
], ),
'button' => [ 'button' => array(
'is_disabled' => $this->smart_button->is_button_disabled( 'cart', $total ), 'is_disabled' => $this->smart_button->is_button_disabled( 'cart', $total ),
] ),
) )
); );
return true; return true;