From 4e50d1d6baffc76a872fb844be67e7b6ac70ea0d Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Thu, 20 Jul 2023 08:02:15 +0100
Subject: [PATCH] * Add paypal script reloading * Add button reloading in cart
and product page * Add checking filters asynchronously
---
modules/ppcp-button/package.json | 5 +-
modules/ppcp-button/resources/js/button.js | 3 -
.../SingleProductActionHandler.js | 63 ++--
.../modules/ContextBootstrap/CartBootstap.js | 16 +-
.../ContextBootstrap/SingleProductBootstap.js | 60 +++-
.../js/modules/Helper/SimulateCart.js | 48 +++
.../resources/js/modules/Helper/Utils.js | 41 +++
.../js/modules/Renderer/MessageRenderer.js | 2 +-
.../resources/js/modules/Renderer/Renderer.js | 70 +++-
modules/ppcp-button/services.php | 3 +-
.../ppcp-button/src/Assets/SmartButton.php | 25 +-
.../src/Endpoint/AbstractCartEndpoint.php | 315 ++++++++++++++++++
.../src/Endpoint/CartScriptParamsEndpoint.php | 5 +
.../src/Endpoint/ChangeCartEndpoint.php | 252 +-------------
.../src/Endpoint/SimulateCartEndpoint.php | 235 ++-----------
modules/ppcp-button/yarn.lock | 12 +
16 files changed, 626 insertions(+), 529 deletions(-)
create mode 100644 modules/ppcp-button/resources/js/modules/Helper/SimulateCart.js
create mode 100644 modules/ppcp-button/resources/js/modules/Helper/Utils.js
create mode 100644 modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php
diff --git a/modules/ppcp-button/package.json b/modules/ppcp-button/package.json
index 61e31d70f..77828e436 100644
--- a/modules/ppcp-button/package.json
+++ b/modules/ppcp-button/package.json
@@ -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",
diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js
index 790030920..1a4219c48 100644
--- a/modules/ppcp-button/resources/js/button.js
+++ b/modules/ppcp-button/resources/js/button.js
@@ -194,9 +194,6 @@ const bootstrap = () => {
payNowBootstrap.init();
}
- if (context !== 'checkout') {
- messageRenderer.render();
- }
};
document.addEventListener(
'DOMContentLoaded',
diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js
index 5cb64febc..1e5f54ea4 100644
--- a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js
+++ b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js
@@ -98,43 +98,38 @@ class SingleProductActionHandler {
}
}
+ getProducts()
+ {
+ if ( this.isBookingProduct() ) {
+ 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() ) {
+ const products = [];
+ this.formElement.querySelectorAll('input[type="number"]').forEach((element) => {
+ if (! element.value) {
+ return;
+ }
+ const elementName = element.getAttribute('name').match(/quantity\[([\d]*)\]/);
+ if (elementName.length !== 2) {
+ return;
+ }
+ const id = parseInt(elementName[1]);
+ const quantity = parseInt(element.value);
+ products.push(new Product(id, quantity, null));
+ })
+ return products;
+ } else {
+ 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;
- 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) {
- return;
- }
- const elementName = element.getAttribute('name').match(/quantity\[([\d]*)\]/);
- if (elementName.length !== 2) {
- return;
- }
- const id = parseInt(elementName[1]);
- const quantity = parseInt(element.value);
- 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)];
- }
- }
- })();
-
return (data, actions) => {
this.errorHandler.clear();
@@ -170,7 +165,7 @@ class SingleProductActionHandler {
});
};
- return this.updateCart.update(onResolve, getProducts());
+ return this.updateCart.update(onResolve, this.getProducts());
};
}
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js
index 5aadb7f61..e3f65e8c5 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js
@@ -40,11 +40,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', this.gateway);
+ }
+
+ // handle button status
+ if ( result.data.button ) {
+ this.gateway.button = result.data.button;
+ }
+
+ this.handleButtonStatus();
if (this.lastAmount !== result.data.amount) {
this.lastAmount = result.data.amount;
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
index 231460642..e5ddfb245 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
@@ -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} from "../Helper/Utils";
class SingleProductBootstap {
constructor(gateway, renderer, messages, errorHandler) {
@@ -38,10 +40,60 @@ class SingleProductBootstap {
this.handleButtonStatus();
}
- handleButtonStatus() {
+ handleButtonStatus(simulateCart = true) {
BootstrapHelper.handleButtonStatus(this, {
formSelector: this.formSelector
});
+
+ if (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());
+
+ //------
+ }
}
init() {
@@ -53,12 +105,6 @@ class SingleProductBootstap {
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 });
diff --git a/modules/ppcp-button/resources/js/modules/Helper/SimulateCart.js b/modules/ppcp-button/resources/js/modules/Helper/SimulateCart.js
new file mode 100644
index 000000000..106dfe989
--- /dev/null
+++ b/modules/ppcp-button/resources/js/modules/Helper/SimulateCart.js
@@ -0,0 +1,48 @@
+class SimulateCart {
+
+ constructor(endpoint, nonce)
+ {
+ this.endpoint = endpoint;
+ this.nonce = nonce;
+ }
+
+ /**
+ *
+ * @param onResolve
+ * @param {Product[]} products
+ * @returns {Promise}
+ */
+ 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;
diff --git a/modules/ppcp-button/resources/js/modules/Helper/Utils.js b/modules/ppcp-button/resources/js/modules/Helper/Utils.js
new file mode 100644
index 000000000..565b64863
--- /dev/null
+++ b/modules/ppcp-button/resources/js/modules/Helper/Utils.js
@@ -0,0 +1,41 @@
+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);
+};
+
+const Utils = {
+ toCamelCase,
+ keysToCamelCase,
+ strAddWord,
+ strRemoveWord
+};
+
+export default Utils;
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
index 7b1577aad..d7fa55f11 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
@@ -44,7 +44,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)) {
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
index ae0997c17..c5e9f0525 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
@@ -1,4 +1,6 @@
import merge from "deepmerge";
+import {loadScript} from "@paypal/paypal-js";
+import {keysToCamelCase} from "../Helper/Utils";
class Renderer {
constructor(creditCardRenderer, defaultSettings, onSmartButtonClick, onSmartButtonsInit) {
@@ -10,6 +12,8 @@ class Renderer {
this.buttonsOptions = {};
this.onButtonsInitListeners = {};
+ this.activeButtons = {};
+
this.renderedSources = new Set();
}
@@ -68,32 +72,61 @@ 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) ) {
return;
}
+ console.log('rendering', wrapper);
+
if (fundingSource) {
contextConfig.fundingSource = fundingSource;
}
- const btn = paypal.Buttons({
- style,
- ...contextConfig,
- onClick: this.onSmartButtonClick,
- onInit: (data, actions) => {
- if (this.onSmartButtonsInit) {
- this.onSmartButtonsInit(data, actions);
- }
- this.handleOnButtonsInit(wrapper, data, actions);
- },
- });
- if (!btn.isEligible()) {
- return;
+ const buttonsOptions = () => {
+ return {
+ style,
+ ...contextConfig,
+ onClick: this.onSmartButtonClick,
+ onInit: (data, actions) => {
+ if (this.onSmartButtonsInit) {
+ this.onSmartButtonsInit(data, actions);
+ }
+ this.handleOnButtonsInit(wrapper, data, actions);
+ },
+ }
}
- btn.render(wrapper);
+ const buildButtons = (paypal) => {
+ const btn = paypal.Buttons(buttonsOptions());
+
+ this.activeButtons[wrapper] = btn;
+
+ if (!btn.isEligible()) {
+ return;
+ }
+
+ btn.render(wrapper);
+ }
+
+ jQuery(wrapper).off('ppcp-reload-buttons');
+ jQuery(wrapper).on('ppcp-reload-buttons', (event, settingsOverride = {}) => {
+ const settings = merge(this.defaultSettings, settingsOverride);
+ const scriptOptions = keysToCamelCase(settings.url_params);
+
+ // if (this.activeButtons[wrapper]) {
+ // this.activeButtons[wrapper].close();
+ // }
+
+ loadScript(scriptOptions).then((paypal) => {
+ buildButtons(paypal);
+ });
+ });
this.renderedSources.add(wrapper + fundingSource ?? '');
+
+ if (typeof paypal !== 'undefined' && typeof paypal.Buttons !== 'undefined') {
+ buildButtons(paypal);
+ }
}
isAlreadyRendered(wrapper, fundingSource, hasEnabledSeparateGateways) {
@@ -101,9 +134,10 @@ class Renderer {
// 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();
- }
+
+ // if (!hasEnabledSeparateGateways) {
+ // return document.querySelector(wrapper).hasChildNodes();
+ // }
return this.renderedSources.has(wrapper + fundingSource ?? '');
}
diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php
index 97f2a94cb..88ee20dd0 100644
--- a/modules/ppcp-button/services.php
+++ b/modules/ppcp-button/services.php
@@ -129,11 +129,12 @@ return array(
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( $cart, $request_data, $data_store, $logger );
+ return new SimulateCartEndpoint( $smart_button, $cart, $request_data, $data_store, $logger );
},
'button.endpoint.change-cart' => static function ( ContainerInterface $container ): ChangeCartEndpoint {
if ( ! \WC()->cart ) {
diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php
index 4c47d1502..4aae9f12e 100644
--- a/modules/ppcp-button/src/Assets/SmartButton.php
+++ b/modules/ppcp-button/src/Assets/SmartButton.php
@@ -1384,10 +1384,10 @@ class SmartButton implements SmartButtonInterface {
* 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 float|null $price_total The price total to be considered.
* @return bool
*/
- protected function is_button_disabled( string $context = null ): bool {
+ public function is_button_disabled( string $context = null, float $price_total = null ): bool {
if ( null === $context ) {
$context = $this->context();
}
@@ -1415,7 +1415,8 @@ class SmartButton implements SmartButtonInterface {
$is_disabled = apply_filters(
'woocommerce_paypal_payments_buttons_disabled',
null,
- $context
+ $context,
+ null === $price_total ? $this->get_cart_price_total() : $price_total
);
if ( $is_disabled !== null ) {
@@ -1429,9 +1430,10 @@ class SmartButton implements SmartButtonInterface {
* Checks a filter if pay_later/messages should be rendered on a given location / context.
*
* @param string $location The location.
+ * @param float|null $price_total The price total to be considered.
* @return bool
*/
- protected function is_pay_later_filter_enabled_for_location( string $location ): bool {
+ private function is_pay_later_filter_enabled_for_location( string $location, float $price_total = null ): bool {
if ( 'product' === $location ) {
$product = wc_get_product();
@@ -1446,8 +1448,7 @@ class SmartButton implements SmartButtonInterface {
$is_disabled = apply_filters(
'woocommerce_paypal_payments_product_buttons_paylater_disabled',
null,
- $product,
- $product->get_price( 'numeric' )
+ $product
);
if ( $is_disabled !== null ) {
@@ -1462,7 +1463,7 @@ class SmartButton implements SmartButtonInterface {
'woocommerce_paypal_payments_buttons_paylater_disabled',
null,
$location,
- $this->get_cart_price_total()
+ null === $price_total ? $this->get_cart_price_total() : $price_total
);
if ( $is_disabled !== null ) {
@@ -1476,10 +1477,11 @@ class SmartButton implements SmartButtonInterface {
* Check whether Pay Later button is enabled for a given location.
*
* @param string $location The location.
+ * @param float|null $price_total The price total to be considered.
* @return bool true if is enabled, otherwise false.
*/
- private function is_pay_later_button_enabled_for_location( string $location ): bool {
- return $this->is_pay_later_filter_enabled_for_location( $location )
+ public function is_pay_later_button_enabled_for_location( string $location, float $price_total = null ): bool {
+ return $this->is_pay_later_filter_enabled_for_location( $location, $price_total )
&& $this->settings_status->is_pay_later_button_enabled_for_location( $location );
}
@@ -1488,10 +1490,11 @@ class SmartButton implements SmartButtonInterface {
* Check whether Pay Later message is enabled for a given location.
*
* @param string $location The location setting name.
+ * @param float|null $price_total The price total to be considered.
* @return bool true if is enabled, otherwise false.
*/
- private function is_pay_later_messaging_enabled_for_location( string $location ): bool {
- return $this->is_pay_later_filter_enabled_for_location( $location )
+ public function is_pay_later_messaging_enabled_for_location( string $location, float $price_total = null ): bool {
+ return $this->is_pay_later_filter_enabled_for_location( $location, $price_total )
&& $this->settings_status->is_pay_later_messaging_enabled_for_location( $location );
}
diff --git a/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php b/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php
new file mode 100644
index 000000000..9c518938e
--- /dev/null
+++ b/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php
@@ -0,0 +1,315 @@
+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
+ */
+ 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(),
+ )
+ );
+ }
+
+ /**
+ * @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;
+ }
+
+ /**
+ * @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 );
+ }
+ }
+
+}
diff --git a/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php b/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php
index 440af66f1..d8c1a3522 100644
--- a/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php
+++ b/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php
@@ -65,11 +65,16 @@ 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'],
'amount' => WC()->cart->get_total( 'raw' ),
)
);
diff --git a/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php b/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php
index 189099485..146af5ca4 100644
--- a/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php
+++ b/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php
@@ -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,19 @@ 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 );
- if ( ! $products ) {
- wp_send_json_error(
- array(
- 'name' => '',
- 'message' => __(
- 'Necessary fields not defined. Action aborted.',
- 'woocommerce-paypal-payments'
- ),
- 'code' => 0,
- 'details' => array(),
- )
- );
+ protected function handle_data(): bool {
+ if ( ! $products = $this->products_from_request() ) {
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;
}
/**
diff --git a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
index 561207c04..8e09a25b2 100644
--- a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
+++ b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
@@ -11,95 +11,45 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
-use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
-use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
+use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
/**
* Class SimulateCartEndpoint
*/
-class SimulateCartEndpoint implements EndpointInterface {
+class SimulateCartEndpoint extends AbstractCartEndpoint {
const ENDPOINT = 'ppc-simulate-cart';
/**
- * The cart object.
+ * The SmartButton.
*
- * @var \WC_Cart
+ * @var SmartButton
*/
- private $cart;
-
- /**
- * The request data helper.
- *
- * @var RequestData
- */
- private $request_data;
-
- /**
- * The product data store.
- *
- * @var \WC_Data_Store
- */
- private $product_data_store;
-
- /**
- * The logger.
- *
- * @var LoggerInterface
- */
- protected $logger;
+ 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;
- }
- /**
- * 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 simulation 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 = 'simulation';
}
/**
@@ -108,167 +58,44 @@ class SimulateCartEndpoint 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 );
- if ( ! $products ) {
- wp_send_json_error(
- array(
- 'name' => '',
- 'message' => __(
- 'Necessary fields not defined. Action aborted.',
- 'woocommerce-paypal-payments'
- ),
- 'code' => 0,
- 'details' => array(),
- )
- );
+ protected function handle_data(): bool {
+ if ( ! $products = $this->products_from_request() ) {
return false;
}
- $this->cart->empty_cart( false );
- $success = true;
- foreach ( $products as $product ) {
- $success = $success && ( ! $product['product']->is_type( 'variable' ) ) ?
- $this->add_product( $product['product'], $product['quantity'] )
- : $this->add_variable_product(
- $product['product'],
- $product['quantity'],
- $product['variations']
- );
- }
- if ( ! $success ) {
- $this->handle_error();
- return $success;
+ // Set WC default cart as the clone.
+ // Store a reference to the real cart.
+ $activeCart = WC()->cart;
+ WC()->cart = $this->cart;
+
+ if ( ! $this->add_products($products) ) {
+ return false;
}
$this->cart->calculate_totals();
+ $total = (float) $this->cart->get_total( 'numeric' );
- $total = $this->cart->get_total( 'numeric' );
+ $this->remove_cart_items();
+ // Restore cart and unset cart clone
+ WC()->cart = $activeCart;
unset( $this->cart );
wp_send_json_success(
array(
'total' => $total,
- )
- );
- 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(),
+ 'funding' => [
+ 'paylater' => [
+ '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 ),
+ ]
+ ],
+ 'button' => [
+ 'is_disabled' => $this->smart_button->is_button_disabled( 'cart', $total ),
+ ]
)
);
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' => isset( $product['variations'] ) ? $product['variations'] : 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
- );
- }
-
}
diff --git a/modules/ppcp-button/yarn.lock b/modules/ppcp-button/yarn.lock
index e76c992e1..9d5ebef6e 100644
--- a/modules/ppcp-button/yarn.lock
+++ b/modules/ppcp-button/yarn.lock
@@ -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"