mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-06 13:44:42 +08:00
Merge pull request #1511 from woocommerce/PCP-1389-pay-later-button-and-message-get-hidden-when-product-cart-checkout-value-is-outside-of-range
Pay Later button and message get hidden when product/cart/checkout value is outside of range (1389, 1886, 1847)
This commit is contained in:
commit
a35bea207e
24 changed files with 1113 additions and 371 deletions
|
@ -11,9 +11,10 @@
|
|||
"Edge >= 14"
|
||||
],
|
||||
"dependencies": {
|
||||
"@paypal/paypal-js": "^6.0.0",
|
||||
"core-js": "^3.25.0",
|
||||
"formdata-polyfill": "^4.0.10",
|
||||
"deepmerge": "^4.2.2"
|
||||
"deepmerge": "^4.2.2",
|
||||
"formdata-polyfill": "^4.0.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.19",
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
ORDER_BUTTON_SELECTOR,
|
||||
PaymentMethods
|
||||
} 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 FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler";
|
||||
import FormSaver from './modules/Helper/FormSaver';
|
||||
|
@ -194,9 +194,6 @@ const bootstrap = () => {
|
|||
payNowBootstrap.init();
|
||||
}
|
||||
|
||||
if (context !== 'checkout') {
|
||||
messageRenderer.render();
|
||||
}
|
||||
};
|
||||
document.addEventListener(
|
||||
'DOMContentLoaded',
|
||||
|
|
|
@ -40,8 +40,7 @@ class SingleProductActionHandler {
|
|||
}).then((res)=>{
|
||||
return res.json();
|
||||
}).then(() => {
|
||||
const id = document.querySelector('[name="add-to-cart"]').value;
|
||||
const products = [new Product(id, 1, null)];
|
||||
const products = this.getSubscriptionProducts();
|
||||
|
||||
fetch(this.config.ajax.change_cart.endpoint, {
|
||||
method: 'POST',
|
||||
|
@ -71,6 +70,12 @@ class SingleProductActionHandler {
|
|||
}
|
||||
}
|
||||
|
||||
getSubscriptionProducts()
|
||||
{
|
||||
const id = document.querySelector('[name="add-to-cart"]').value;
|
||||
return [new Product(id, 1, null)];
|
||||
}
|
||||
|
||||
configuration()
|
||||
{
|
||||
return {
|
||||
|
@ -98,18 +103,12 @@ class SingleProductActionHandler {
|
|||
}
|
||||
}
|
||||
|
||||
createOrder()
|
||||
getProducts()
|
||||
{
|
||||
this.cartHelper = null;
|
||||
|
||||
let getProducts = (() => {
|
||||
if ( this.isBookingProduct() ) {
|
||||
return () => {
|
||||
const id = document.querySelector('[name="add-to-cart"]').value;
|
||||
return [new BookingProduct(id, 1, FormHelper.getPrefixedFields(this.formElement, "wc_bookings_field"))];
|
||||
}
|
||||
} else if ( this.isGroupedProduct() ) {
|
||||
return () => {
|
||||
const products = [];
|
||||
this.formElement.querySelectorAll('input[type="number"]').forEach((element) => {
|
||||
if (! element.value) {
|
||||
|
@ -124,16 +123,17 @@ class SingleProductActionHandler {
|
|||
products.push(new Product(id, quantity, null));
|
||||
})
|
||||
return products;
|
||||
}
|
||||
} else {
|
||||
return () => {
|
||||
const id = document.querySelector('[name="add-to-cart"]').value;
|
||||
const qty = document.querySelector('[name="quantity"]').value;
|
||||
const variations = this.variations();
|
||||
return [new Product(id, qty, variations)];
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
createOrder()
|
||||
{
|
||||
this.cartHelper = null;
|
||||
|
||||
return (data, actions) => {
|
||||
this.errorHandler.clear();
|
||||
|
@ -170,7 +170,7 @@ class SingleProductActionHandler {
|
|||
});
|
||||
};
|
||||
|
||||
return this.updateCart.update(onResolve, getProducts());
|
||||
return this.updateCart.update(onResolve, this.getProducts());
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import CartActionHandler from '../ActionHandler/CartActionHandler';
|
||||
import BootstrapHelper from "../Helper/BootstrapHelper";
|
||||
import {setVisible} from "../Helper/Hiding";
|
||||
|
||||
class CartBootstrap {
|
||||
constructor(gateway, renderer, messages, errorHandler) {
|
||||
|
@ -40,11 +39,21 @@ class CartBootstrap {
|
|||
return;
|
||||
}
|
||||
|
||||
// handle script reload
|
||||
const newParams = result.data.url_params;
|
||||
const reloadRequired = this.gateway.url_params.intent !== newParams.intent;
|
||||
const reloadRequired = JSON.stringify(this.gateway.url_params) !== JSON.stringify(newParams);
|
||||
|
||||
// TODO: should reload the script instead
|
||||
setVisible(this.gateway.button.wrapper, !reloadRequired)
|
||||
if (reloadRequired) {
|
||||
this.gateway.url_params = newParams;
|
||||
jQuery(this.gateway.button.wrapper).trigger('ppcp-reload-buttons');
|
||||
}
|
||||
|
||||
// handle button status
|
||||
if (result.data.button || result.data.messages) {
|
||||
this.gateway.button = result.data.button;
|
||||
this.gateway.messages = result.data.messages;
|
||||
this.handleButtonStatus();
|
||||
}
|
||||
|
||||
if (this.lastAmount !== result.data.amount) {
|
||||
this.lastAmount = result.data.amount;
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
PaymentMethods
|
||||
} from "../Helper/CheckoutMethodState";
|
||||
import BootstrapHelper from "../Helper/BootstrapHelper";
|
||||
import {disable, enable} from "../Helper/ButtonDisabler";
|
||||
|
||||
class CheckoutBootstap {
|
||||
constructor(gateway, renderer, messages, spinner, errorHandler) {
|
||||
|
|
|
@ -2,6 +2,8 @@ import UpdateCart from "../Helper/UpdateCart";
|
|||
import SingleProductActionHandler from "../ActionHandler/SingleProductActionHandler";
|
||||
import {hide, show} from "../Helper/Hiding";
|
||||
import BootstrapHelper from "../Helper/BootstrapHelper";
|
||||
import SimulateCart from "../Helper/SimulateCart";
|
||||
import {strRemoveWord, strAddWord, throttle} from "../Helper/Utils";
|
||||
|
||||
class SingleProductBootstap {
|
||||
constructor(gateway, renderer, messages, errorHandler) {
|
||||
|
@ -12,6 +14,9 @@ class SingleProductBootstap {
|
|||
this.mutationObserver = new MutationObserver(this.handleChange.bind(this));
|
||||
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.handleChange();
|
||||
}, true);
|
||||
|
@ -38,10 +43,14 @@ class SingleProductBootstap {
|
|||
this.handleButtonStatus();
|
||||
}
|
||||
|
||||
handleButtonStatus() {
|
||||
handleButtonStatus(simulateCart = true) {
|
||||
BootstrapHelper.handleButtonStatus(this, {
|
||||
formSelector: this.formSelector
|
||||
});
|
||||
|
||||
if (simulateCart) {
|
||||
this.simulateCartThrottled();
|
||||
}
|
||||
}
|
||||
|
||||
init() {
|
||||
|
@ -53,12 +62,6 @@ class SingleProductBootstap {
|
|||
|
||||
jQuery(document).on('change', this.formSelector, () => {
|
||||
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 });
|
||||
|
||||
|
@ -161,6 +164,62 @@ class SingleProductBootstap {
|
|||
actionHandler.configuration()
|
||||
);
|
||||
}
|
||||
|
||||
simulateCart() {
|
||||
const actionHandler = new SingleProductActionHandler(
|
||||
null,
|
||||
null,
|
||||
this.form(),
|
||||
this.errorHandler,
|
||||
);
|
||||
|
||||
const hasSubscriptions = PayPalCommerceGateway.data_client_id.has_subscriptions
|
||||
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled;
|
||||
|
||||
const products = hasSubscriptions
|
||||
? actionHandler.getSubscriptionProducts()
|
||||
: actionHandler.getProducts();
|
||||
|
||||
(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;
|
||||
}
|
||||
if (typeof data.messages.is_hidden === 'boolean') {
|
||||
this.gateway.messages.is_hidden = data.messages.is_hidden;
|
||||
}
|
||||
|
||||
this.handleButtonStatus(false);
|
||||
|
||||
}, products);
|
||||
}
|
||||
}
|
||||
|
||||
export default SingleProductBootstap;
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
import {loadScript} from "@paypal/paypal-js";
|
||||
import widgetBuilder from "./Renderer/WidgetBuilder";
|
||||
|
||||
const storageKey = 'ppcp-data-client-id';
|
||||
|
||||
const validateToken = (token, user) => {
|
||||
|
@ -24,7 +27,7 @@ const storeToken = (token) => {
|
|||
sessionStorage.setItem(storageKey, JSON.stringify(token));
|
||||
}
|
||||
|
||||
const dataClientIdAttributeHandler = (script, config) => {
|
||||
const dataClientIdAttributeHandler = (scriptOptions, config, callback) => {
|
||||
fetch(config.endpoint, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -42,8 +45,14 @@ const dataClientIdAttributeHandler = (script, config) => {
|
|||
return;
|
||||
}
|
||||
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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {disable, enable} from "./ButtonDisabler";
|
||||
import {hide, show} from "./Hiding";
|
||||
|
||||
/**
|
||||
* Common Bootstrap methods to avoid code repetition.
|
||||
|
@ -11,20 +12,28 @@ export default class BootstrapHelper {
|
|||
options.messagesWrapper = options.messagesWrapper || bs.gateway.messages.wrapper;
|
||||
options.skipMessages = options.skipMessages || false;
|
||||
|
||||
if (!bs.shouldEnable()) {
|
||||
// Handle messages hide / show
|
||||
if (this.shouldShowMessages(bs, options)) {
|
||||
show(options.messagesWrapper);
|
||||
} else {
|
||||
hide(options.messagesWrapper);
|
||||
}
|
||||
|
||||
// Handle enable / disable
|
||||
if (bs.shouldEnable()) {
|
||||
bs.renderer.enableSmartButtons(options.wrapper);
|
||||
enable(options.wrapper);
|
||||
|
||||
if (!options.skipMessages) {
|
||||
enable(options.messagesWrapper);
|
||||
}
|
||||
} else {
|
||||
bs.renderer.disableSmartButtons(options.wrapper);
|
||||
disable(options.wrapper, options.formSelector || null);
|
||||
|
||||
if (!options.skipMessages) {
|
||||
disable(options.messagesWrapper);
|
||||
}
|
||||
return;
|
||||
}
|
||||
bs.renderer.enableSmartButtons(options.wrapper);
|
||||
enable(options.wrapper);
|
||||
|
||||
if (!options.skipMessages) {
|
||||
enable(options.messagesWrapper);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,4 +46,13 @@ export default class BootstrapHelper {
|
|||
return bs.shouldRender()
|
||||
&& options.isDisabled !== true;
|
||||
}
|
||||
|
||||
static shouldShowMessages(bs, options) {
|
||||
options = options || {};
|
||||
if (typeof options.isMessagesHidden === 'undefined') {
|
||||
options.isMessagesHidden = bs.gateway.messages.is_hidden;
|
||||
}
|
||||
|
||||
return options.isMessagesHidden !== true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
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) => {
|
||||
if (typeof paypal !== 'undefined') {
|
||||
|
@ -6,19 +10,18 @@ export const loadPaypalScript = (config, 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]);
|
||||
const callback = (paypal) => {
|
||||
widgetBuilder.setPaypal(paypal);
|
||||
onLoaded();
|
||||
}
|
||||
);
|
||||
|
||||
let scriptOptions = keysToCamelCase(config.url_params);
|
||||
scriptOptions = merge(scriptOptions, config.script_attributes);
|
||||
|
||||
if (config.data_client_id.set_attribute) {
|
||||
dataClientIdAttributeHandler(script, config.data_client_id);
|
||||
dataClientIdAttributeHandler(scriptOptions, config.data_client_id, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.appendChild(script);
|
||||
loadScript(scriptOptions).then(callback);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
class SimulateCart {
|
||||
|
||||
constructor(endpoint, nonce)
|
||||
{
|
||||
this.endpoint = endpoint;
|
||||
this.nonce = nonce;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param onResolve
|
||||
* @param {Product[]} products
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
simulate(onResolve, products)
|
||||
{
|
||||
return new Promise((resolve, reject) => {
|
||||
fetch(
|
||||
this.endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({
|
||||
nonce: this.nonce,
|
||||
products,
|
||||
})
|
||||
}
|
||||
).then(
|
||||
(result) => {
|
||||
return result.json();
|
||||
}
|
||||
).then((result) => {
|
||||
if (! result.success) {
|
||||
reject(result.data);
|
||||
return;
|
||||
}
|
||||
|
||||
const resolved = onResolve(result.data);
|
||||
resolve(resolved);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default SimulateCart;
|
69
modules/ppcp-button/resources/js/modules/Helper/Utils.js
Normal file
69
modules/ppcp-button/resources/js/modules/Helper/Utils.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
export const toCamelCase = (str) => {
|
||||
return str.replace(/([-_]\w)/g, function(match) {
|
||||
return match[1].toUpperCase();
|
||||
});
|
||||
}
|
||||
|
||||
export const keysToCamelCase = (obj) => {
|
||||
let output = {};
|
||||
for (const key in obj) {
|
||||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||||
output[toCamelCase(key)] = obj[key];
|
||||
}
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
export const strAddWord = (str, word, separator = ',') => {
|
||||
let arr = str.split(separator);
|
||||
if (!arr.includes(word)) {
|
||||
arr.push(word);
|
||||
}
|
||||
return arr.join(separator);
|
||||
};
|
||||
|
||||
export const strRemoveWord = (str, word, separator = ',') => {
|
||||
let arr = str.split(separator);
|
||||
let index = arr.indexOf(word);
|
||||
if (index !== -1) {
|
||||
arr.splice(index, 1);
|
||||
}
|
||||
return arr.join(separator);
|
||||
};
|
||||
|
||||
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 = {
|
||||
toCamelCase,
|
||||
keysToCamelCase,
|
||||
strAddWord,
|
||||
strRemoveWord,
|
||||
throttle
|
||||
};
|
||||
|
||||
export default Utils;
|
|
@ -1,3 +1,5 @@
|
|||
import widgetBuilder from "./WidgetBuilder";
|
||||
|
||||
class MessageRenderer {
|
||||
|
||||
constructor(config) {
|
||||
|
@ -28,7 +30,8 @@ class MessageRenderer {
|
|||
oldWrapper.parentElement.removeChild(oldWrapper);
|
||||
sibling.parentElement.insertBefore(newWrapper, sibling);
|
||||
|
||||
paypal.Messages(options).render(this.config.wrapper);
|
||||
widgetBuilder.registerMessages(this.config.wrapper, options);
|
||||
widgetBuilder.renderMessages(this.config.wrapper);
|
||||
}
|
||||
|
||||
optionsEqual(options) {
|
||||
|
@ -44,7 +47,7 @@ class MessageRenderer {
|
|||
|
||||
shouldRender() {
|
||||
|
||||
if (typeof paypal.Messages === 'undefined' || typeof this.config.wrapper === 'undefined' ) {
|
||||
if (typeof paypal === 'undefined' || typeof paypal.Messages === 'undefined' || typeof this.config.wrapper === 'undefined' ) {
|
||||
return false;
|
||||
}
|
||||
if (! document.querySelector(this.config.wrapper)) {
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import merge from "deepmerge";
|
||||
import {loadScript} from "@paypal/paypal-js";
|
||||
import {keysToCamelCase} from "../Helper/Utils";
|
||||
import widgetBuilder from "./WidgetBuilder";
|
||||
|
||||
class Renderer {
|
||||
constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) {
|
||||
|
@ -11,6 +14,8 @@ class Renderer {
|
|||
this.onButtonsInitListeners = {};
|
||||
|
||||
this.renderedSources = new Set();
|
||||
|
||||
this.reloadEventName = 'ppcp-reload-buttons';
|
||||
}
|
||||
|
||||
render(contextConfig, settingsOverride = {}, contextConfigOverride = () => {}) {
|
||||
|
@ -68,7 +73,9 @@ class Renderer {
|
|||
}
|
||||
|
||||
renderButtons(wrapper, style, contextConfig, hasEnabledSeparateGateways, fundingSource = null) {
|
||||
if (! document.querySelector(wrapper) || this.isAlreadyRendered(wrapper, fundingSource, hasEnabledSeparateGateways) || 'undefined' === typeof paypal.Buttons ) {
|
||||
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, fundingSource]);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -76,7 +83,8 @@ class Renderer {
|
|||
contextConfig.fundingSource = fundingSource;
|
||||
}
|
||||
|
||||
const btn = paypal.Buttons({
|
||||
const buttonsOptions = () => {
|
||||
return {
|
||||
style,
|
||||
...contextConfig,
|
||||
onClick: this.onSmartButtonClick,
|
||||
|
@ -86,25 +94,39 @@ class Renderer {
|
|||
}
|
||||
this.handleOnButtonsInit(wrapper, data, actions);
|
||||
},
|
||||
});
|
||||
if (!btn.isEligible()) {
|
||||
}
|
||||
}
|
||||
|
||||
jQuery(document)
|
||||
.off(this.reloadEventName, wrapper)
|
||||
.on(this.reloadEventName, wrapper, (event, settingsOverride = {}, triggeredFundingSource) => {
|
||||
|
||||
// Only accept events from the matching funding source
|
||||
if (fundingSource && triggeredFundingSource && (triggeredFundingSource !== fundingSource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
btn.render(wrapper);
|
||||
const settings = merge(this.defaultSettings, settingsOverride);
|
||||
let scriptOptions = keysToCamelCase(settings.url_params);
|
||||
scriptOptions = merge(scriptOptions, settings.script_attributes);
|
||||
|
||||
this.renderedSources.add(wrapper + fundingSource ?? '');
|
||||
loadScript(scriptOptions).then((paypal) => {
|
||||
widgetBuilder.setPaypal(paypal);
|
||||
widgetBuilder.registerButtons([wrapper, fundingSource], buttonsOptions());
|
||||
widgetBuilder.renderAll();
|
||||
});
|
||||
});
|
||||
|
||||
this.renderedSources.add(wrapper + (fundingSource ?? ''));
|
||||
|
||||
if (typeof paypal !== 'undefined' && typeof paypal.Buttons !== 'undefined') {
|
||||
widgetBuilder.registerButtons([wrapper, fundingSource], buttonsOptions());
|
||||
widgetBuilder.renderButtons([wrapper, fundingSource]);
|
||||
}
|
||||
}
|
||||
|
||||
isAlreadyRendered(wrapper, fundingSource, hasEnabledSeparateGateways) {
|
||||
// Simply check that has child nodes when we do not need to render buttons separately,
|
||||
// this will reduce the risk of breaking with different themes/plugins
|
||||
// and on the cart page (where we also do not need to render separately), which may fully reload this part of the page.
|
||||
// Ideally we should also find a way to detect such full reloads and remove the corresponding keys from the set.
|
||||
if (!hasEnabledSeparateGateways) {
|
||||
return document.querySelector(wrapper).hasChildNodes();
|
||||
}
|
||||
return this.renderedSources.has(wrapper + fundingSource ?? '');
|
||||
isAlreadyRendered(wrapper, fundingSource) {
|
||||
return this.renderedSources.has(wrapper + (fundingSource ?? ''));
|
||||
}
|
||||
|
||||
disableCreditCardFields() {
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
/**
|
||||
* Handles the registration and rendering of PayPal widgets: Buttons and Messages.
|
||||
* To have several Buttons per wrapper, an array should be provided, ex: [wrapper, fundingSource].
|
||||
*/
|
||||
class WidgetBuilder {
|
||||
|
||||
constructor() {
|
||||
this.paypal = null;
|
||||
this.buttons = new Map();
|
||||
this.messages = new Map();
|
||||
|
||||
this.renderEventName = 'ppcp-render';
|
||||
|
||||
document.ppcpWidgetBuilderStatus = () => {
|
||||
console.log({
|
||||
buttons: this.buttons,
|
||||
messages: this.messages,
|
||||
});
|
||||
}
|
||||
|
||||
jQuery(document)
|
||||
.off(this.renderEventName)
|
||||
.on(this.renderEventName, () => {
|
||||
this.renderAll();
|
||||
});
|
||||
}
|
||||
|
||||
setPaypal(paypal) {
|
||||
this.paypal = paypal;
|
||||
}
|
||||
|
||||
registerButtons(wrapper, options) {
|
||||
wrapper = this.sanitizeWrapper(wrapper);
|
||||
|
||||
this.buttons.set(this.toKey(wrapper), {
|
||||
wrapper: wrapper,
|
||||
options: options,
|
||||
});
|
||||
}
|
||||
|
||||
renderButtons(wrapper) {
|
||||
wrapper = this.sanitizeWrapper(wrapper);
|
||||
|
||||
if (!this.buttons.has(this.toKey(wrapper))) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hasRendered(wrapper)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = this.buttons.get(this.toKey(wrapper));
|
||||
const btn = this.paypal.Buttons(entry.options);
|
||||
|
||||
if (!btn.isEligible()) {
|
||||
this.buttons.delete(this.toKey(wrapper));
|
||||
return;
|
||||
}
|
||||
|
||||
let target = this.buildWrapperTarget(wrapper);
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
btn.render(target);
|
||||
}
|
||||
|
||||
renderAllButtons() {
|
||||
for (const [wrapper, entry] of this.buttons) {
|
||||
this.renderButtons(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
registerMessages(wrapper, options) {
|
||||
this.messages.set(wrapper, {
|
||||
wrapper: wrapper,
|
||||
options: options
|
||||
});
|
||||
}
|
||||
|
||||
renderMessages(wrapper) {
|
||||
if (!this.messages.has(wrapper)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hasRendered(wrapper)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry = this.messages.get(wrapper);
|
||||
const btn = this.paypal.Messages(entry.options);
|
||||
|
||||
btn.render(entry.wrapper);
|
||||
|
||||
// watchdog to try to handle some strange cases where the wrapper may not be present
|
||||
setTimeout(() => {
|
||||
if (!this.hasRendered(wrapper)) {
|
||||
btn.render(entry.wrapper);
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
renderAllMessages() {
|
||||
for (const [wrapper, entry] of this.messages) {
|
||||
this.renderMessages(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
renderAll() {
|
||||
this.renderAllButtons();
|
||||
this.renderAllMessages();
|
||||
}
|
||||
|
||||
hasRendered(wrapper) {
|
||||
let selector = wrapper;
|
||||
|
||||
if (Array.isArray(wrapper)) {
|
||||
selector = wrapper[0];
|
||||
for (const item of wrapper.slice(1)) {
|
||||
selector += ' .item-' + item;
|
||||
}
|
||||
}
|
||||
|
||||
const element = document.querySelector(selector);
|
||||
return element && element.hasChildNodes();
|
||||
}
|
||||
|
||||
sanitizeWrapper(wrapper) {
|
||||
if (Array.isArray(wrapper)) {
|
||||
wrapper = wrapper.filter(item => !!item);
|
||||
if (wrapper.length === 1) {
|
||||
wrapper = wrapper[0];
|
||||
}
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
buildWrapperTarget(wrapper) {
|
||||
let target = wrapper;
|
||||
|
||||
if (Array.isArray(wrapper)) {
|
||||
const $wrapper = jQuery(wrapper[0]);
|
||||
|
||||
if (!$wrapper.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const itemClass = 'item-' + wrapper[1];
|
||||
|
||||
// Check if the parent element exists and it doesn't already have the div with the class
|
||||
let $item = $wrapper.find('.' + itemClass);
|
||||
|
||||
if (!$item.length) {
|
||||
$item = jQuery(`<div class="${itemClass}"></div>`);
|
||||
$wrapper.append($item);
|
||||
}
|
||||
|
||||
target = $item.get(0);
|
||||
}
|
||||
|
||||
if (!jQuery(target).length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return target;
|
||||
}
|
||||
|
||||
toKey(wrapper) {
|
||||
if (Array.isArray(wrapper)) {
|
||||
return JSON.stringify(wrapper);
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
export default new WidgetBuilder();
|
|
@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Button;
|
|||
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
|
||||
|
@ -124,6 +125,17 @@ return array(
|
|||
'button.request-data' => static function ( ContainerInterface $container ): RequestData {
|
||||
return new RequestData();
|
||||
},
|
||||
'button.endpoint.simulate-cart' => static function ( ContainerInterface $container ): SimulateCartEndpoint {
|
||||
if ( ! \WC()->cart ) {
|
||||
throw new RuntimeException( 'cant initialize endpoint at this moment' );
|
||||
}
|
||||
$smart_button = $container->get( 'button.smart-button' );
|
||||
$cart = WC()->cart;
|
||||
$request_data = $container->get( 'button.request-data' );
|
||||
$data_store = \WC_Data_Store::load( 'product' );
|
||||
$logger = $container->get( 'woocommerce.logger.woocommerce' );
|
||||
return new SimulateCartEndpoint( $smart_button, $cart, $request_data, $data_store, $logger );
|
||||
},
|
||||
'button.endpoint.change-cart' => static function ( ContainerInterface $container ): ChangeCartEndpoint {
|
||||
if ( ! \WC()->cart ) {
|
||||
throw new RuntimeException( 'cant initialize endpoint at this moment' );
|
||||
|
|
|
@ -25,6 +25,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
|
|||
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
|
||||
|
@ -683,6 +684,7 @@ class SmartButton implements SmartButtonInterface {
|
|||
|
||||
return array(
|
||||
'wrapper' => '#ppcp-messages',
|
||||
'is_hidden' => ! $this->is_pay_later_filter_enabled_for_location( $this->context() ),
|
||||
'amount' => $amount,
|
||||
'placement' => $placement,
|
||||
'style' => array(
|
||||
|
@ -827,6 +829,10 @@ class SmartButton implements SmartButtonInterface {
|
|||
'redirect' => wc_get_checkout_url(),
|
||||
'context' => $this->context(),
|
||||
'ajax' => array(
|
||||
'simulate_cart' => array(
|
||||
'endpoint' => \WC_AJAX::get_endpoint( SimulateCartEndpoint::ENDPOINT ),
|
||||
'nonce' => wp_create_nonce( SimulateCartEndpoint::nonce() ),
|
||||
),
|
||||
'change_cart' => array(
|
||||
'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ),
|
||||
'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ),
|
||||
|
@ -1073,8 +1079,8 @@ class SmartButton implements SmartButtonInterface {
|
|||
|
||||
$enable_funding = array( 'venmo' );
|
||||
|
||||
if ( $this->settings_status->is_pay_later_button_enabled_for_location( $context ) ||
|
||||
$this->settings_status->is_pay_later_messaging_enabled_for_location( $context )
|
||||
if ( $this->is_pay_later_button_enabled_for_location( $context ) ||
|
||||
$this->is_pay_later_messaging_enabled_for_location( $context )
|
||||
) {
|
||||
$enable_funding[] = 'paylater';
|
||||
} else {
|
||||
|
@ -1363,49 +1369,112 @@ class SmartButton implements SmartButtonInterface {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills and returns the product context_data array to be used in filters.
|
||||
*
|
||||
* @param array $context_data The context data for this filter.
|
||||
* @return array
|
||||
*/
|
||||
private function product_filter_context_data( array $context_data = array() ): array {
|
||||
if ( ! isset( $context_data['product'] ) ) {
|
||||
$context_data['product'] = wc_get_product();
|
||||
}
|
||||
if ( ! $context_data['product'] ) {
|
||||
return array();
|
||||
}
|
||||
if ( ! isset( $context_data['order_total'] ) && ( $context_data['product'] instanceof WC_Product ) ) {
|
||||
$context_data['order_total'] = (float) $context_data['product']->get_price( 'raw' );
|
||||
}
|
||||
|
||||
return $context_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if PayPal buttons/messages should be rendered for the current page.
|
||||
*
|
||||
* @param string|null $context The context that should be checked, use default otherwise.
|
||||
*
|
||||
* @param array $context_data The context data for this filter.
|
||||
* @return bool
|
||||
*/
|
||||
protected function is_button_disabled( string $context = null ): bool {
|
||||
public function is_button_disabled( string $context = null, array $context_data = array() ): bool {
|
||||
if ( null === $context ) {
|
||||
$context = $this->context();
|
||||
}
|
||||
|
||||
if ( 'product' === $context ) {
|
||||
$product = wc_get_product();
|
||||
|
||||
/**
|
||||
* Allows to decide if the button should be disabled for a given product
|
||||
* Allows to decide if the button should be disabled for a given product.
|
||||
*/
|
||||
$is_disabled = apply_filters(
|
||||
return apply_filters(
|
||||
'woocommerce_paypal_payments_product_buttons_disabled',
|
||||
null,
|
||||
$product
|
||||
false,
|
||||
$this->product_filter_context_data( $context_data )
|
||||
);
|
||||
|
||||
if ( $is_disabled !== null ) {
|
||||
return $is_disabled;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to decide if the button should be disabled globally or on a given context
|
||||
* Allows to decide if the button should be disabled globally or on a given context.
|
||||
*/
|
||||
$is_disabled = apply_filters(
|
||||
return apply_filters(
|
||||
'woocommerce_paypal_payments_buttons_disabled',
|
||||
null,
|
||||
false,
|
||||
$context
|
||||
);
|
||||
|
||||
if ( $is_disabled !== null ) {
|
||||
return $is_disabled;
|
||||
}
|
||||
|
||||
return false;
|
||||
/**
|
||||
* Checks a filter if pay_later/messages should be rendered on a given location / context.
|
||||
*
|
||||
* @param string $location The location.
|
||||
* @param array $context_data The context data for this filter.
|
||||
* @return bool
|
||||
*/
|
||||
private function is_pay_later_filter_enabled_for_location( string $location, array $context_data = array() ): bool {
|
||||
|
||||
if ( 'product' === $location ) {
|
||||
/**
|
||||
* Allows to decide if the button should be disabled for a given product.
|
||||
*/
|
||||
return ! apply_filters(
|
||||
'woocommerce_paypal_payments_product_buttons_paylater_disabled',
|
||||
false,
|
||||
$this->product_filter_context_data( $context_data )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to decide if the button should be disabled on a given context.
|
||||
*/
|
||||
return ! apply_filters(
|
||||
'woocommerce_paypal_payments_buttons_paylater_disabled',
|
||||
false,
|
||||
$location
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether Pay Later button is enabled for a given location.
|
||||
*
|
||||
* @param string $location The location.
|
||||
* @param array $context_data The context data for this filter.
|
||||
* @return bool true if is enabled, otherwise false.
|
||||
*/
|
||||
public function is_pay_later_button_enabled_for_location( string $location, array $context_data = array() ): bool {
|
||||
return $this->is_pay_later_filter_enabled_for_location( $location, $context_data )
|
||||
&& $this->settings_status->is_pay_later_button_enabled_for_location( $location );
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether Pay Later message is enabled for a given location.
|
||||
*
|
||||
* @param string $location The location setting name.
|
||||
* @param array $context_data The context data for this filter.
|
||||
* @return bool true if is enabled, otherwise false.
|
||||
*/
|
||||
public function is_pay_later_messaging_enabled_for_location( string $location, array $context_data = array() ): bool {
|
||||
return $this->is_pay_later_filter_enabled_for_location( $location, $context_data )
|
||||
&& $this->settings_status->is_pay_later_messaging_enabled_for_location( $location );
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button;
|
|||
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
|
||||
|
@ -120,6 +121,19 @@ class ButtonModule implements ModuleInterface {
|
|||
}
|
||||
);
|
||||
|
||||
add_action(
|
||||
'wc_ajax_' . SimulateCartEndpoint::ENDPOINT,
|
||||
static function () use ( $container ) {
|
||||
$endpoint = $container->get( 'button.endpoint.simulate-cart' );
|
||||
/**
|
||||
* The Simulate Cart Endpoint.
|
||||
*
|
||||
* @var SimulateCartEndpoint $endpoint
|
||||
*/
|
||||
$endpoint->handle_request();
|
||||
}
|
||||
);
|
||||
|
||||
add_action(
|
||||
'wc_ajax_' . ChangeCartEndpoint::ENDPOINT,
|
||||
static function () use ( $container ) {
|
||||
|
|
327
modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php
Normal file
327
modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php
Normal file
|
@ -0,0 +1,327 @@
|
|||
<?php
|
||||
/**
|
||||
* Abstract class for cart Endpoints.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Button\Endpoint
|
||||
*/
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
|
||||
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
|
||||
/**
|
||||
* Abstract Class AbstractCartEndpoint
|
||||
*/
|
||||
abstract class AbstractCartEndpoint implements EndpointInterface {
|
||||
|
||||
const ENDPOINT = '';
|
||||
|
||||
/**
|
||||
* The current cart object.
|
||||
*
|
||||
* @var \WC_Cart
|
||||
*/
|
||||
protected $cart;
|
||||
|
||||
/**
|
||||
* The product data store.
|
||||
*
|
||||
* @var \WC_Data_Store
|
||||
*/
|
||||
protected $product_data_store;
|
||||
|
||||
/**
|
||||
* The request data helper.
|
||||
*
|
||||
* @var RequestData
|
||||
*/
|
||||
protected $request_data;
|
||||
|
||||
/**
|
||||
* The logger.
|
||||
*
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* The tag to be added to logs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $logger_tag = '';
|
||||
|
||||
/**
|
||||
* The added cart item IDs
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $cart_item_keys = array();
|
||||
|
||||
/**
|
||||
* The nonce.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function nonce(): string {
|
||||
return static::ENDPOINT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function handle_request(): bool {
|
||||
try {
|
||||
return $this->handle_data();
|
||||
} catch ( Exception $error ) {
|
||||
$this->logger->error( 'Cart ' . $this->logger_tag . ' failed: ' . $error->getMessage() );
|
||||
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'name' => is_a( $error, PayPalApiException::class ) ? $error->name() : '',
|
||||
'message' => $error->getMessage(),
|
||||
'code' => $error->getCode(),
|
||||
'details' => is_a( $error, PayPalApiException::class ) ? $error->details() : array(),
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the request data.
|
||||
*
|
||||
* @return bool
|
||||
* @throws Exception On error.
|
||||
*/
|
||||
abstract protected function handle_data(): bool;
|
||||
|
||||
/**
|
||||
* Adds products to cart.
|
||||
*
|
||||
* @param array $products Array of products to be added to cart.
|
||||
* @return bool
|
||||
* @throws Exception Add to cart methods throw an exception on fail.
|
||||
*/
|
||||
protected function add_products( array $products ): bool {
|
||||
$this->cart->empty_cart( false );
|
||||
|
||||
$success = true;
|
||||
foreach ( $products as $product ) {
|
||||
if ( $product['product']->is_type( 'booking' ) ) {
|
||||
$success = $success && $this->add_booking_product(
|
||||
$product['product'],
|
||||
$product['booking']
|
||||
);
|
||||
} elseif ( $product['product']->is_type( 'variable' ) ) {
|
||||
$success = $success && $this->add_variable_product(
|
||||
$product['product'],
|
||||
$product['quantity'],
|
||||
$product['variations']
|
||||
);
|
||||
} else {
|
||||
$success = $success && $this->add_product(
|
||||
$product['product'],
|
||||
$product['quantity']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $success ) {
|
||||
$this->handle_error();
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function handle_error(): void {
|
||||
|
||||
$message = __(
|
||||
'Something went wrong. Action aborted',
|
||||
'woocommerce-paypal-payments'
|
||||
);
|
||||
$errors = wc_get_notices( 'error' );
|
||||
if ( count( $errors ) ) {
|
||||
$message = array_reduce(
|
||||
$errors,
|
||||
static function ( string $add, array $error ): string {
|
||||
return $add . $error['notice'] . ' ';
|
||||
},
|
||||
''
|
||||
);
|
||||
wc_clear_notices();
|
||||
}
|
||||
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'name' => '',
|
||||
'message' => $message,
|
||||
'code' => 0,
|
||||
'details' => array(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns product information from request data.
|
||||
*
|
||||
* @return array|false
|
||||
*/
|
||||
protected function products_from_request() {
|
||||
$data = $this->request_data->read_request( $this->nonce() );
|
||||
$products = $this->products_from_data( $data );
|
||||
if ( ! $products ) {
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'name' => '',
|
||||
'message' => __(
|
||||
'Necessary fields not defined. Action aborted.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'code' => 0,
|
||||
'details' => array(),
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $products;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns product information from a data array.
|
||||
*
|
||||
* @param array $data The data array.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
protected function products_from_data( array $data ): ?array {
|
||||
|
||||
$products = array();
|
||||
|
||||
if (
|
||||
! isset( $data['products'] )
|
||||
|| ! is_array( $data['products'] )
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
foreach ( $data['products'] as $product ) {
|
||||
if ( ! isset( $product['quantity'] ) || ! isset( $product['id'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$wc_product = wc_get_product( (int) $product['id'] );
|
||||
|
||||
if ( ! $wc_product ) {
|
||||
return null;
|
||||
}
|
||||
$products[] = array(
|
||||
'product' => $wc_product,
|
||||
'quantity' => (int) $product['quantity'],
|
||||
'variations' => $product['variations'] ?? null,
|
||||
'booking' => $product['booking'] ?? null,
|
||||
);
|
||||
}
|
||||
return $products;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a product to the cart.
|
||||
*
|
||||
* @param \WC_Product $product The Product.
|
||||
* @param int $quantity The Quantity.
|
||||
*
|
||||
* @return bool
|
||||
* @throws Exception When product could not be added.
|
||||
*/
|
||||
private function add_product( \WC_Product $product, int $quantity ): bool {
|
||||
$cart_item_key = $this->cart->add_to_cart( $product->get_id(), $quantity );
|
||||
|
||||
$this->cart_item_keys[] = $cart_item_key;
|
||||
return false !== $cart_item_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds variations to the cart.
|
||||
*
|
||||
* @param \WC_Product $product The Product.
|
||||
* @param int $quantity The Quantity.
|
||||
* @param array $post_variations The variations.
|
||||
*
|
||||
* @return bool
|
||||
* @throws Exception When product could not be added.
|
||||
*/
|
||||
private function add_variable_product(
|
||||
\WC_Product $product,
|
||||
int $quantity,
|
||||
array $post_variations
|
||||
): bool {
|
||||
|
||||
$variations = array();
|
||||
foreach ( $post_variations as $key => $value ) {
|
||||
$variations[ $value['name'] ] = $value['value'];
|
||||
}
|
||||
|
||||
$variation_id = $this->product_data_store->find_matching_product_variation( $product, $variations );
|
||||
|
||||
// ToDo: Check stock status for variation.
|
||||
$cart_item_key = $this->cart->add_to_cart(
|
||||
$product->get_id(),
|
||||
$quantity,
|
||||
$variation_id,
|
||||
$variations
|
||||
);
|
||||
|
||||
$this->cart_item_keys[] = $cart_item_key;
|
||||
return false !== $cart_item_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds booking to the cart.
|
||||
*
|
||||
* @param \WC_Product $product The Product.
|
||||
* @param array $data Data used by the booking plugin.
|
||||
*
|
||||
* @return bool
|
||||
* @throws Exception When product could not be added.
|
||||
*/
|
||||
private function add_booking_product(
|
||||
\WC_Product $product,
|
||||
array $data
|
||||
): bool {
|
||||
|
||||
if ( ! is_callable( 'wc_bookings_get_posted_data' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cart_item_data = array(
|
||||
'booking' => wc_bookings_get_posted_data( $data, $product ),
|
||||
);
|
||||
|
||||
$cart_item_key = $this->cart->add_to_cart( $product->get_id(), 1, 0, array(), $cart_item_data );
|
||||
|
||||
$this->cart_item_keys[] = $cart_item_key;
|
||||
return false !== $cart_item_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes stored cart items from WooCommerce cart.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function remove_cart_items(): void {
|
||||
foreach ( $this->cart_item_keys as $cart_item_key ) {
|
||||
$this->cart->remove_cart_item( $cart_item_key );
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -65,11 +65,17 @@ class CartScriptParamsEndpoint implements EndpointInterface {
|
|||
*/
|
||||
public function handle_request(): bool {
|
||||
try {
|
||||
if ( is_callable( 'wc_maybe_define_constant' ) ) {
|
||||
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
|
||||
}
|
||||
|
||||
$script_data = $this->smart_button->script_data();
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'url_params' => $script_data['url_params'],
|
||||
'button' => $script_data['button'],
|
||||
'messages' => $script_data['messages'],
|
||||
'amount' => WC()->cart->get_total( 'raw' ),
|
||||
)
|
||||
);
|
||||
|
|
|
@ -11,26 +11,15 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint;
|
|||
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
|
||||
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Class ChangeCartEndpoint
|
||||
*/
|
||||
class ChangeCartEndpoint implements EndpointInterface {
|
||||
|
||||
class ChangeCartEndpoint extends AbstractCartEndpoint {
|
||||
|
||||
const ENDPOINT = 'ppc-change-cart';
|
||||
|
||||
/**
|
||||
* The current cart object.
|
||||
*
|
||||
* @var \WC_Cart
|
||||
*/
|
||||
private $cart;
|
||||
|
||||
/**
|
||||
* The current shipping object.
|
||||
*
|
||||
|
@ -38,13 +27,6 @@ class ChangeCartEndpoint implements EndpointInterface {
|
|||
*/
|
||||
private $shipping;
|
||||
|
||||
/**
|
||||
* The request data helper.
|
||||
*
|
||||
* @var RequestData
|
||||
*/
|
||||
private $request_data;
|
||||
|
||||
/**
|
||||
* The PurchaseUnit factory.
|
||||
*
|
||||
|
@ -52,20 +34,6 @@ class ChangeCartEndpoint implements EndpointInterface {
|
|||
*/
|
||||
private $purchase_unit_factory;
|
||||
|
||||
/**
|
||||
* The product data store.
|
||||
*
|
||||
* @var \WC_Data_Store
|
||||
*/
|
||||
private $product_data_store;
|
||||
|
||||
/**
|
||||
* The logger.
|
||||
*
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* ChangeCartEndpoint constructor.
|
||||
*
|
||||
|
@ -91,38 +59,8 @@ class ChangeCartEndpoint implements EndpointInterface {
|
|||
$this->purchase_unit_factory = $purchase_unit_factory;
|
||||
$this->product_data_store = $product_data_store;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* The nonce.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function nonce(): string {
|
||||
return self::ENDPOINT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function handle_request(): bool {
|
||||
try {
|
||||
return $this->handle_data();
|
||||
} catch ( Exception $error ) {
|
||||
$this->logger->error( 'Cart updating failed: ' . $error->getMessage() );
|
||||
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'name' => is_a( $error, PayPalApiException::class ) ? $error->name() : '',
|
||||
'message' => $error->getMessage(),
|
||||
'code' => $error->getCode(),
|
||||
'details' => is_a( $error, PayPalApiException::class ) ? $error->details() : array(),
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
$this->logger_tag = 'updating';
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -131,195 +69,21 @@ class ChangeCartEndpoint implements EndpointInterface {
|
|||
* @return bool
|
||||
* @throws Exception On error.
|
||||
*/
|
||||
private function handle_data(): bool {
|
||||
$data = $this->request_data->read_request( $this->nonce() );
|
||||
$products = $this->products_from_data( $data );
|
||||
protected function handle_data(): bool {
|
||||
$products = $this->products_from_request();
|
||||
|
||||
if ( ! $products ) {
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'name' => '',
|
||||
'message' => __(
|
||||
'Necessary fields not defined. Action aborted.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'code' => 0,
|
||||
'details' => array(),
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->shipping->reset_shipping();
|
||||
$this->cart->empty_cart( false );
|
||||
$success = true;
|
||||
foreach ( $products as $product ) {
|
||||
if ( $product['product']->is_type( 'booking' ) ) {
|
||||
$success = $success && $this->add_booking_product(
|
||||
$product['product'],
|
||||
$product['booking']
|
||||
);
|
||||
} elseif ( $product['product']->is_type( 'variable' ) ) {
|
||||
$success = $success && $this->add_variable_product(
|
||||
$product['product'],
|
||||
$product['quantity'],
|
||||
$product['variations']
|
||||
);
|
||||
} else {
|
||||
$success = $success && $this->add_product(
|
||||
$product['product'],
|
||||
$product['quantity']
|
||||
);
|
||||
}
|
||||
}
|
||||
if ( ! $success ) {
|
||||
$this->handle_error();
|
||||
return $success;
|
||||
}
|
||||
|
||||
wp_send_json_success( $this->generate_purchase_units() );
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles errors.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function handle_error(): bool {
|
||||
|
||||
$message = __(
|
||||
'Something went wrong. Action aborted',
|
||||
'woocommerce-paypal-payments'
|
||||
);
|
||||
$errors = wc_get_notices( 'error' );
|
||||
if ( count( $errors ) ) {
|
||||
$message = array_reduce(
|
||||
$errors,
|
||||
static function ( string $add, array $error ): string {
|
||||
return $add . $error['notice'] . ' ';
|
||||
},
|
||||
''
|
||||
);
|
||||
wc_clear_notices();
|
||||
}
|
||||
|
||||
wp_send_json_error(
|
||||
array(
|
||||
'name' => '',
|
||||
'message' => $message,
|
||||
'code' => 0,
|
||||
'details' => array(),
|
||||
)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns product information from an data array.
|
||||
*
|
||||
* @param array $data The data array.
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function products_from_data( array $data ) {
|
||||
|
||||
$products = array();
|
||||
|
||||
if (
|
||||
! isset( $data['products'] )
|
||||
|| ! is_array( $data['products'] )
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
foreach ( $data['products'] as $product ) {
|
||||
if ( ! isset( $product['quantity'] ) || ! isset( $product['id'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$wc_product = wc_get_product( (int) $product['id'] );
|
||||
|
||||
if ( ! $wc_product ) {
|
||||
return null;
|
||||
}
|
||||
$products[] = array(
|
||||
'product' => $wc_product,
|
||||
'quantity' => (int) $product['quantity'],
|
||||
'variations' => $product['variations'] ?? null,
|
||||
'booking' => $product['booking'] ?? null,
|
||||
);
|
||||
}
|
||||
return $products;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a product to the cart.
|
||||
*
|
||||
* @param \WC_Product $product The Product.
|
||||
* @param int $quantity The Quantity.
|
||||
*
|
||||
* @return bool
|
||||
* @throws Exception When product could not be added.
|
||||
*/
|
||||
private function add_product( \WC_Product $product, int $quantity ): bool {
|
||||
return false !== $this->cart->add_to_cart( $product->get_id(), $quantity );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Adds variations to the cart.
|
||||
*
|
||||
* @param \WC_Product $product The Product.
|
||||
* @param int $quantity The Quantity.
|
||||
* @param array $post_variations The variations.
|
||||
*
|
||||
* @return bool
|
||||
* @throws Exception When product could not be added.
|
||||
*/
|
||||
private function add_variable_product(
|
||||
\WC_Product $product,
|
||||
int $quantity,
|
||||
array $post_variations
|
||||
): bool {
|
||||
|
||||
$variations = array();
|
||||
foreach ( $post_variations as $key => $value ) {
|
||||
$variations[ $value['name'] ] = $value['value'];
|
||||
}
|
||||
|
||||
$variation_id = $this->product_data_store->find_matching_product_variation( $product, $variations );
|
||||
|
||||
// ToDo: Check stock status for variation.
|
||||
return false !== $this->cart->add_to_cart(
|
||||
$product->get_id(),
|
||||
$quantity,
|
||||
$variation_id,
|
||||
$variations
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds variations to the cart.
|
||||
*
|
||||
* @param \WC_Product $product The Product.
|
||||
* @param array $data Data used by the booking plugin.
|
||||
*
|
||||
* @return bool
|
||||
* @throws Exception When product could not be added.
|
||||
*/
|
||||
private function add_booking_product(
|
||||
\WC_Product $product,
|
||||
array $data
|
||||
): bool {
|
||||
|
||||
if ( ! is_callable( 'wc_bookings_get_posted_data' ) ) {
|
||||
if ( ! $this->add_products( $products ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$cart_item_data = array(
|
||||
'booking' => wc_bookings_get_posted_data( $data, $product ),
|
||||
);
|
||||
|
||||
return false !== $this->cart->add_to_cart( $product->get_id(), 1, 0, array(), $cart_item_data );
|
||||
wp_send_json_success( $this->generate_purchase_units() );
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
122
modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
Normal file
122
modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
Normal file
|
@ -0,0 +1,122 @@
|
|||
<?php
|
||||
/**
|
||||
* Endpoint to simulate adding products to the cart.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Button\Endpoint
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
|
||||
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
|
||||
|
||||
/**
|
||||
* Class SimulateCartEndpoint
|
||||
*/
|
||||
class SimulateCartEndpoint extends AbstractCartEndpoint {
|
||||
|
||||
const ENDPOINT = 'ppc-simulate-cart';
|
||||
|
||||
/**
|
||||
* The SmartButton.
|
||||
*
|
||||
* @var SmartButton
|
||||
*/
|
||||
private $smart_button;
|
||||
|
||||
/**
|
||||
* ChangeCartEndpoint constructor.
|
||||
*
|
||||
* @param SmartButton $smart_button The SmartButton.
|
||||
* @param \WC_Cart $cart The current WC cart object.
|
||||
* @param RequestData $request_data The request data helper.
|
||||
* @param \WC_Data_Store $product_data_store The data store for products.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
*/
|
||||
public function __construct(
|
||||
SmartButton $smart_button,
|
||||
\WC_Cart $cart,
|
||||
RequestData $request_data,
|
||||
\WC_Data_Store $product_data_store,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
$this->smart_button = $smart_button;
|
||||
$this->cart = clone $cart;
|
||||
$this->request_data = $request_data;
|
||||
$this->product_data_store = $product_data_store;
|
||||
$this->logger = $logger;
|
||||
|
||||
$this->logger_tag = 'simulation';
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the request data.
|
||||
*
|
||||
* @return bool
|
||||
* @throws Exception On error.
|
||||
*/
|
||||
protected function handle_data(): bool {
|
||||
$products = $this->products_from_request();
|
||||
|
||||
if ( ! $products ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Set WC default cart as the clone.
|
||||
// Store a reference to the real cart.
|
||||
$active_cart = WC()->cart;
|
||||
WC()->cart = $this->cart;
|
||||
|
||||
if ( ! $this->add_products( $products ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->cart->calculate_totals();
|
||||
$total = (float) $this->cart->get_total( 'numeric' );
|
||||
|
||||
// Remove from cart because some plugins reserve resources internally when adding to cart.
|
||||
$this->remove_cart_items();
|
||||
|
||||
// Restore cart and unset cart clone.
|
||||
WC()->cart = $active_cart;
|
||||
unset( $this->cart );
|
||||
|
||||
// Process filters.
|
||||
$pay_later_enabled = true;
|
||||
$pay_later_messaging_enabled = true;
|
||||
$button_enabled = true;
|
||||
|
||||
foreach ( $products as $product ) {
|
||||
$context_data = array(
|
||||
'product' => $product['product'],
|
||||
'order_total' => $total,
|
||||
);
|
||||
|
||||
$pay_later_enabled = $pay_later_enabled && $this->smart_button->is_pay_later_button_enabled_for_location( 'product', $context_data );
|
||||
$pay_later_messaging_enabled = $pay_later_messaging_enabled && $this->smart_button->is_pay_later_messaging_enabled_for_location( 'product', $context_data );
|
||||
$button_enabled = $button_enabled && ! $this->smart_button->is_button_disabled( 'product', $context_data );
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'total' => $total,
|
||||
'funding' => array(
|
||||
'paylater' => array(
|
||||
'enabled' => $pay_later_enabled,
|
||||
),
|
||||
),
|
||||
'button' => array(
|
||||
'is_disabled' => ! $button_enabled,
|
||||
),
|
||||
'messages' => array(
|
||||
'is_hidden' => ! $pay_later_messaging_enabled,
|
||||
),
|
||||
)
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -956,6 +956,13 @@
|
|||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@paypal/paypal-js@^6.0.0":
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@paypal/paypal-js/-/paypal-js-6.0.0.tgz#a5a9556af29e4a0124049bf9a093606f52b8a951"
|
||||
integrity sha512-FYzjYby9F7tgg4tUxYNseZ6vkeDJcdcjoULsyNhfrWZZjicDpdj5932fZlyUlQXDSR9KlhjXH6H4nPIJ0Lq0Kw==
|
||||
dependencies:
|
||||
promise-polyfill "^8.3.0"
|
||||
|
||||
"@types/eslint-scope@^3.7.3":
|
||||
version "3.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
|
||||
|
@ -1858,6 +1865,11 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0:
|
|||
dependencies:
|
||||
find-up "^4.0.0"
|
||||
|
||||
promise-polyfill@^8.3.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.3.0.tgz#9284810268138d103807b11f4e23d5e945a4db63"
|
||||
integrity sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==
|
||||
|
||||
punycode@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
|
||||
|
|
|
@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\Onboarding;
|
|||
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Onboarding\Assets\OnboardingAssets;
|
||||
use WooCommerce\PayPalCommerce\Onboarding\Endpoint\LoginSellerEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingRenderer;
|
||||
|
@ -88,9 +87,9 @@ class OnboardingModule implements ModuleInterface {
|
|||
$endpoint = $c->get( 'onboarding.endpoint.login-seller' );
|
||||
|
||||
/**
|
||||
* The ChangeCartEndpoint.
|
||||
* The LoginSellerEndpoint.
|
||||
*
|
||||
* @var ChangeCartEndpoint $endpoint
|
||||
* @var LoginSellerEndpoint $endpoint
|
||||
*/
|
||||
$endpoint->handle_request();
|
||||
}
|
||||
|
|
|
@ -2,9 +2,10 @@ import { loadScript } from "@paypal/paypal-js";
|
|||
import {debounce} from "./helper/debounce";
|
||||
import Renderer from '../../../ppcp-button/resources/js/modules/Renderer/Renderer'
|
||||
import MessageRenderer from "../../../ppcp-button/resources/js/modules/Renderer/MessageRenderer";
|
||||
import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/modules/Helper/Hiding"
|
||||
import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/modules/Helper/Hiding";
|
||||
import widgetBuilder from "../../../ppcp-button/resources/js/modules/Renderer/WidgetBuilder";
|
||||
|
||||
;document.addEventListener(
|
||||
document.addEventListener(
|
||||
'DOMContentLoaded',
|
||||
() => {
|
||||
function disableAll(nodeList){
|
||||
|
@ -138,6 +139,8 @@ import {setVisibleByClass, isVisible} from "../../../ppcp-button/resources/js/mo
|
|||
function loadPaypalScript(settings, onLoaded = () => {}) {
|
||||
loadScript(JSON.parse(JSON.stringify(settings))) // clone the object to prevent modification
|
||||
.then(paypal => {
|
||||
widgetBuilder.setPaypal(paypal);
|
||||
|
||||
document.dispatchEvent(new CustomEvent('ppcp_paypal_script_loaded'));
|
||||
|
||||
onLoaded(paypal);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue