From d71e09bd09038d8bf12f9839bd6e8aa396592431 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Thu, 13 Jul 2023 11:59:31 +0100
Subject: [PATCH 01/14] Add
woocommerce_paypal_payments_buttons_paylater_disabled and
woocommerce_paypal_payments_product_buttons_paylater_disabled filters
---
.../ppcp-button/src/Assets/SmartButton.php | 88 +++++++++++++++++--
.../src/Settings/SettingsListener.php | 2 +-
2 files changed, 84 insertions(+), 6 deletions(-)
diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php
index fded20a67..517f43774 100644
--- a/modules/ppcp-button/src/Assets/SmartButton.php
+++ b/modules/ppcp-button/src/Assets/SmartButton.php
@@ -382,7 +382,11 @@ class SmartButton implements SmartButtonInterface {
* @throws NotFoundException When a setting was not found.
*/
private function render_message_wrapper_registrar(): bool {
- if ( ! $this->settings_status->is_pay_later_messaging_enabled() ) {
+ if ( ! $this->is_pay_later_messaging_enabled() ) {
+ return false;
+ }
+
+ if ( ! $this->is_pay_later_filter_enabled_for_location( $this->context() ) ) {
return false;
}
@@ -548,7 +552,7 @@ class SmartButton implements SmartButtonInterface {
$smart_button_enabled_for_current_location = $this->settings_status->is_smart_button_enabled_for_location( $this->context() );
$smart_button_enabled_for_mini_cart = $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' );
- $messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $this->context() );
+ $messaging_enabled_for_current_location = $this->is_pay_later_messaging_enabled_for_location( $this->context() );
switch ( $this->context() ) {
case 'checkout':
@@ -657,7 +661,7 @@ class SmartButton implements SmartButtonInterface {
* @throws NotFoundException When a setting was not found.
*/
private function message_values(): array {
- if ( ! $this->settings_status->is_pay_later_messaging_enabled() ) {
+ if ( ! $this->is_pay_later_messaging_enabled() ) {
return array();
}
@@ -1073,8 +1077,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 {
@@ -1408,6 +1412,80 @@ class SmartButton implements SmartButtonInterface {
return false;
}
+ /**
+ * Checks a filter if pay_later/messages should be rendered on a given location / context.
+ *
+ * @param string $location The location.
+ * @return bool
+ */
+ protected function is_pay_later_filter_enabled_for_location( string $location ): bool {
+
+ if ( 'product' === $location ) {
+ $product = wc_get_product();
+
+ /**
+ * Allows to decide if the button should be disabled for a given product
+ */
+ $is_disabled = apply_filters(
+ 'woocommerce_paypal_payments_product_buttons_paylater_disabled',
+ null,
+ $product
+ );
+
+ if ( $is_disabled !== null ) {
+ return ! $is_disabled;
+ }
+ }
+
+ /**
+ * Allows to decide if the button should be disabled globally or on a given context
+ */
+ $is_disabled = apply_filters(
+ 'woocommerce_paypal_payments_buttons_paylater_disabled',
+ null,
+ $location
+ );
+
+ if ( $is_disabled !== null ) {
+ return ! $is_disabled;
+ }
+
+ return true;
+ }
+
+ /**
+ * Check whether Pay Later button is enabled for a given location.
+ *
+ * @param string $location The location.
+ * @return bool true if is enabled, otherwise false.
+ */
+ private function is_pay_later_button_enabled_for_location( string $location ) {
+ return $this->is_pay_later_filter_enabled_for_location( $location )
+ && $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.
+ * @return bool true if is enabled, otherwise false.
+ */
+ private function is_pay_later_messaging_enabled_for_location( string $location ) {
+ return $this->is_pay_later_filter_enabled_for_location( $location )
+ && $this->settings_status->is_pay_later_messaging_enabled_for_location( $location );
+ }
+
+ /**
+ * Check whether Pay Later message is enabled
+ *
+ * @return bool true if is enabled, otherwise false.
+ */
+ private function is_pay_later_messaging_enabled() {
+ return $this->is_pay_later_filter_enabled_for_location( $this->context() )
+ && $this->settings_status->is_pay_later_messaging_enabled();
+ }
+
/**
* Retrieves all payment tokens for the user, via API or cached if already queried.
*
diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php
index 0756e73f9..7079b3de6 100644
--- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php
+++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php
@@ -167,7 +167,7 @@ class SettingsListener {
/**
* Listens if the merchant ID should be updated.
*/
- public function listen_for_merchant_id() {
+ public function listen_for_merchant_id(): void {
if ( ! $this->is_valid_site_request() || $this->state->current_state() === State::STATE_ONBOARDED ) {
return;
}
From 1df151fd499fb6f00aa45b9e006ac31dbbc1ad01 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Thu, 13 Jul 2023 16:10:59 +0100
Subject: [PATCH 02/14] Remove duplicate
is_pay_later_filter_enabled_for_location condition check
---
modules/ppcp-button/src/Assets/SmartButton.php | 4 ----
1 file changed, 4 deletions(-)
diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php
index 517f43774..619712749 100644
--- a/modules/ppcp-button/src/Assets/SmartButton.php
+++ b/modules/ppcp-button/src/Assets/SmartButton.php
@@ -386,10 +386,6 @@ class SmartButton implements SmartButtonInterface {
return false;
}
- if ( ! $this->is_pay_later_filter_enabled_for_location( $this->context() ) ) {
- return false;
- }
-
$selected_locations = $this->settings->has( 'pay_later_messaging_locations' ) ? $this->settings->get( 'pay_later_messaging_locations' ) : array();
$not_enabled_on_cart = ! in_array( 'cart', $selected_locations, true );
From 76804c25823e278dfc5026c7e891a0561091478c Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Tue, 18 Jul 2023 15:58:15 +0100
Subject: [PATCH 03/14] Add simulate cart endpoint
---
modules/ppcp-button/services.php | 11 +
.../ppcp-button/src/Assets/SmartButton.php | 33 ++-
modules/ppcp-button/src/ButtonModule.php | 14 +
.../src/Endpoint/SimulateCartEndpoint.php | 274 ++++++++++++++++++
.../ppcp-onboarding/src/OnboardingModule.php | 5 +-
5 files changed, 329 insertions(+), 8 deletions(-)
create mode 100644 modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php
index 56381fcc2..97f2a94cb 100644
--- a/modules/ppcp-button/services.php
+++ b/modules/ppcp-button/services.php
@@ -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,16 @@ 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' );
+ }
+ $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 );
+ },
'button.endpoint.change-cart' => static function ( ContainerInterface $container ): ChangeCartEndpoint {
if ( ! \WC()->cart ) {
throw new RuntimeException( 'cant initialize endpoint at this moment' );
diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php
index 619712749..4c47d1502 100644
--- a/modules/ppcp-button/src/Assets/SmartButton.php
+++ b/modules/ppcp-button/src/Assets/SmartButton.php
@@ -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;
@@ -827,6 +828,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() ),
@@ -1329,6 +1334,18 @@ class SmartButton implements SmartButtonInterface {
return WC()->cart && WC()->cart->get_total( 'numeric' ) == 0;
}
+ /**
+ * Returns the cart total.
+ *
+ * @return ?float
+ */
+ protected function get_cart_price_total(): ?float {
+ if ( ! WC()->cart ) {
+ return null;
+ }
+ return (float) WC()->cart->get_total( 'numeric' );
+ }
+
/**
* Checks if PayPal buttons/messages can be rendered for the given product.
*
@@ -1419,13 +1436,18 @@ class SmartButton implements SmartButtonInterface {
if ( 'product' === $location ) {
$product = wc_get_product();
+ if ( ! $product ) {
+ return true;
+ }
+
/**
* Allows to decide if the button should be disabled for a given product
*/
$is_disabled = apply_filters(
'woocommerce_paypal_payments_product_buttons_paylater_disabled',
null,
- $product
+ $product,
+ $product->get_price( 'numeric' )
);
if ( $is_disabled !== null ) {
@@ -1439,7 +1461,8 @@ class SmartButton implements SmartButtonInterface {
$is_disabled = apply_filters(
'woocommerce_paypal_payments_buttons_paylater_disabled',
null,
- $location
+ $location,
+ $this->get_cart_price_total()
);
if ( $is_disabled !== null ) {
@@ -1455,7 +1478,7 @@ class SmartButton implements SmartButtonInterface {
* @param string $location The location.
* @return bool true if is enabled, otherwise false.
*/
- private function is_pay_later_button_enabled_for_location( string $location ) {
+ private function is_pay_later_button_enabled_for_location( string $location ): bool {
return $this->is_pay_later_filter_enabled_for_location( $location )
&& $this->settings_status->is_pay_later_button_enabled_for_location( $location );
@@ -1467,7 +1490,7 @@ class SmartButton implements SmartButtonInterface {
* @param string $location The location setting name.
* @return bool true if is enabled, otherwise false.
*/
- private function is_pay_later_messaging_enabled_for_location( string $location ) {
+ private function is_pay_later_messaging_enabled_for_location( string $location ): bool {
return $this->is_pay_later_filter_enabled_for_location( $location )
&& $this->settings_status->is_pay_later_messaging_enabled_for_location( $location );
}
@@ -1477,7 +1500,7 @@ class SmartButton implements SmartButtonInterface {
*
* @return bool true if is enabled, otherwise false.
*/
- private function is_pay_later_messaging_enabled() {
+ private function is_pay_later_messaging_enabled(): bool {
return $this->is_pay_later_filter_enabled_for_location( $this->context() )
&& $this->settings_status->is_pay_later_messaging_enabled();
}
diff --git a/modules/ppcp-button/src/ButtonModule.php b/modules/ppcp-button/src/ButtonModule.php
index 2c7337297..67dec01f3 100644
--- a/modules/ppcp-button/src/ButtonModule.php
+++ b/modules/ppcp-button/src/ButtonModule.php
@@ -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 ) {
diff --git a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
new file mode 100644
index 000000000..561207c04
--- /dev/null
+++ b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
@@ -0,0 +1,274 @@
+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;
+ }
+ }
+
+ /**
+ * Handles the request data.
+ *
+ * @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(),
+ )
+ );
+ 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;
+ }
+
+ $this->cart->calculate_totals();
+
+ $total = $this->cart->get_total( 'numeric' );
+
+ 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(),
+ )
+ );
+ 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-onboarding/src/OnboardingModule.php b/modules/ppcp-onboarding/src/OnboardingModule.php
index b6eaaa1aa..1f94f0fee 100644
--- a/modules/ppcp-onboarding/src/OnboardingModule.php
+++ b/modules/ppcp-onboarding/src/OnboardingModule.php
@@ -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();
}
From 4e50d1d6baffc76a872fb844be67e7b6ac70ea0d Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Thu, 20 Jul 2023 08:02:15 +0100
Subject: [PATCH 04/14] * 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"
From a0480e35bb8741c73be1856c4cd38081e8c81d68 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Thu, 20 Jul 2023 14:19:18 +0100
Subject: [PATCH 05/14] Add paypal widget builder
---
modules/ppcp-button/resources/js/button.js | 3 +
.../modules/ContextBootstrap/CartBootstap.js | 3 +-
.../js/modules/Renderer/MessageRenderer.js | 7 +-
.../resources/js/modules/Renderer/Renderer.js | 42 ++++-------
.../js/modules/Renderer/WidgetBuilder.js | 75 +++++++++++++++++++
5 files changed, 100 insertions(+), 30 deletions(-)
create mode 100644 modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js
diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js
index 1a4219c48..38ef56112 100644
--- a/modules/ppcp-button/resources/js/button.js
+++ b/modules/ppcp-button/resources/js/button.js
@@ -19,6 +19,7 @@ import FreeTrialHandler from "./modules/ActionHandler/FreeTrialHandler";
import FormSaver from './modules/Helper/FormSaver';
import FormValidator from "./modules/Helper/FormValidator";
import {loadPaypalScript} from "./modules/Helper/ScriptLoading";
+import widgetBuilder from "./modules/Renderer/WidgetBuilder";
// TODO: could be a good idea to have a separate spinner for each gateway,
// but I think we care mainly about the script loading, so one spinner should be enough.
@@ -267,6 +268,8 @@ document.addEventListener(
});
loadPaypalScript(PayPalCommerceGateway, () => {
+ widgetBuilder.setPaypal(paypal);
+
bootstrapped = true;
bootstrap();
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js
index e3f65e8c5..5790e8f56 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js
@@ -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) {
@@ -46,7 +45,7 @@ class CartBootstrap {
if (reloadRequired) {
this.gateway.url_params = newParams;
- jQuery(this.gateway.button.wrapper).trigger('ppcp-reload-buttons', this.gateway);
+ jQuery(this.gateway.button.wrapper).trigger('ppcp-reload-buttons');
}
// handle button status
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
index d7fa55f11..45f67ac67 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
@@ -1,3 +1,5 @@
+import widgetBuilder from "./WidgetBuilder";
+
class MessageRenderer {
constructor(config) {
@@ -28,7 +30,10 @@ 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);
+
+ // paypal.Messages(options).render(this.config.wrapper);
}
optionsEqual(options) {
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
index c5e9f0525..828134e46 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
@@ -1,6 +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) {
@@ -12,9 +13,9 @@ class Renderer {
this.buttonsOptions = {};
this.onButtonsInitListeners = {};
- this.activeButtons = {};
-
this.renderedSources = new Set();
+
+ this.reloadEventName = 'ppcp-reload-buttons';
}
render(contextConfig, settingsOverride = {}, contextConfigOverride = () => {}) {
@@ -76,8 +77,6 @@ class Renderer {
return;
}
- console.log('rendering', wrapper);
-
if (fundingSource) {
contextConfig.fundingSource = fundingSource;
}
@@ -96,36 +95,25 @@ class Renderer {
}
}
- 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 = {}) => {
+ //jQuery(document).off('ppcp-reload-buttons');
+ jQuery(document).on('ppcp-reload-buttons', wrapper, (event, settingsOverride = {}) => {
const settings = merge(this.defaultSettings, settingsOverride);
- const scriptOptions = keysToCamelCase(settings.url_params);
-
- // if (this.activeButtons[wrapper]) {
- // this.activeButtons[wrapper].close();
- // }
+ let scriptOptions = keysToCamelCase(settings.url_params);
+ scriptOptions = merge(scriptOptions, settings.script_attributes);
loadScript(scriptOptions).then((paypal) => {
- buildButtons(paypal);
+ widgetBuilder.setPaypal(paypal);
+ widgetBuilder.registerButtons(wrapper, buttonsOptions());
+ widgetBuilder.renderAllButtons();
+ widgetBuilder.renderAllMessages();
});
});
- this.renderedSources.add(wrapper + fundingSource ?? '');
+ this.renderedSources.add(wrapper + (fundingSource ?? ''));
if (typeof paypal !== 'undefined' && typeof paypal.Buttons !== 'undefined') {
- buildButtons(paypal);
+ widgetBuilder.registerButtons(wrapper, buttonsOptions());
+ widgetBuilder.renderButtons(wrapper);
}
}
@@ -138,7 +126,7 @@ class Renderer {
// if (!hasEnabledSeparateGateways) {
// return document.querySelector(wrapper).hasChildNodes();
// }
- return this.renderedSources.has(wrapper + fundingSource ?? '');
+ return this.renderedSources.has(wrapper + (fundingSource ?? ''));
}
disableCreditCardFields() {
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js b/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js
new file mode 100644
index 000000000..45d5a0b82
--- /dev/null
+++ b/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js
@@ -0,0 +1,75 @@
+
+class WidgetBuilder {
+
+ constructor() {
+ this.paypal = null;
+ this.buttons = new Map();
+ this.messages = new Map();
+
+ document.ppcpWidgetBuilderStatus = () => {
+ console.log({
+ buttons: this.buttons,
+ messages: this.messages,
+ });
+ }
+ }
+
+ setPaypal(paypal) {
+ this.paypal = paypal;
+ }
+
+ registerButtons(wrapper, options) {
+ this.buttons.set(wrapper, {
+ wrapper: wrapper,
+ options: options
+ });
+ }
+
+ renderButtons(wrapper) {
+ if (!this.buttons.has(wrapper)) {
+ return;
+ }
+
+ const entry = this.buttons.get(wrapper);
+ const btn = this.paypal.Buttons(entry.options);
+
+ if (!btn.isEligible()) {
+ return;
+ }
+
+ btn.render(entry.wrapper);
+ }
+
+ 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;
+ }
+
+ const entry = this.messages.get(wrapper);
+ const btn = this.paypal.Messages(entry.options);
+
+ btn.render(entry.wrapper);
+ }
+
+ renderAllMessages() {
+ for (const [wrapper, entry] of this.messages) {
+ this.renderMessages(wrapper);
+ }
+ }
+
+}
+
+export default new WidgetBuilder();
From 84a362a7c5af5380fd8f6b8cfaae0c987386e10c Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Thu, 20 Jul 2023 17:51:48 +0100
Subject: [PATCH 06/14] Fix lint Add throttling to simulate_cart ajax call
---
modules/ppcp-button/resources/js/button.js | 5 +-
.../modules/ContextBootstrap/CartBootstap.js | 5 +-
.../ContextBootstrap/SingleProductBootstap.js | 99 ++++++++++---------
.../modules/DataClientIdAttributeHandler.js | 15 ++-
.../js/modules/Helper/ScriptLoading.js | 23 +++--
.../resources/js/modules/Helper/Utils.js | 45 ++++++++-
.../resources/js/modules/Renderer/Renderer.js | 24 ++---
.../js/modules/Renderer/WidgetBuilder.js | 16 +++
.../ppcp-button/src/Assets/SmartButton.php | 8 +-
.../src/Endpoint/AbstractCartEndpoint.php | 36 ++++---
.../src/Endpoint/CartScriptParamsEndpoint.php | 2 +-
.../src/Endpoint/ChangeCartEndpoint.php | 8 +-
.../src/Endpoint/SimulateCartEndpoint.php | 31 +++---
13 files changed, 203 insertions(+), 114 deletions(-)
diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js
index 38ef56112..ffb21ffc2 100644
--- a/modules/ppcp-button/resources/js/button.js
+++ b/modules/ppcp-button/resources/js/button.js
@@ -13,13 +13,12 @@ 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';
import FormValidator from "./modules/Helper/FormValidator";
import {loadPaypalScript} from "./modules/Helper/ScriptLoading";
-import widgetBuilder from "./modules/Renderer/WidgetBuilder";
// TODO: could be a good idea to have a separate spinner for each gateway,
// but I think we care mainly about the script loading, so one spinner should be enough.
@@ -268,8 +267,6 @@ document.addEventListener(
});
loadPaypalScript(PayPalCommerceGateway, () => {
- widgetBuilder.setPaypal(paypal);
-
bootstrapped = true;
bootstrap();
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js
index 5790e8f56..f55663b74 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js
@@ -49,12 +49,11 @@ class CartBootstrap {
}
// handle button status
- if ( result.data.button ) {
+ if (result.data.button) {
this.gateway.button = result.data.button;
+ this.handleButtonStatus();
}
- this.handleButtonStatus();
-
if (this.lastAmount !== result.data.amount) {
this.lastAmount = result.data.amount;
this.messages.renderWithAmount(this.lastAmount);
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
index e5ddfb245..f09ef7fda 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
@@ -3,7 +3,7 @@ import SingleProductActionHandler from "../ActionHandler/SingleProductActionHand
import {hide, show} from "../Helper/Hiding";
import BootstrapHelper from "../Helper/BootstrapHelper";
import SimulateCart from "../Helper/SimulateCart";
-import {strRemoveWord, strAddWord} from "../Helper/Utils";
+import {strRemoveWord, strAddWord, throttle} from "../Helper/Utils";
class SingleProductBootstap {
constructor(gateway, renderer, messages, errorHandler) {
@@ -14,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);
@@ -46,53 +49,7 @@ class SingleProductBootstap {
});
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());
-
- //------
+ this.simulateCartThrottled();
}
}
@@ -197,6 +154,52 @@ class SingleProductBootstap {
actionHandler.configuration()
);
}
+
+ simulateCart() {
+ const actionHandler = new SingleProductActionHandler(
+ null,
+ null,
+ this.form(),
+ this.errorHandler,
+ );
+
+ (new SimulateCart(
+ this.gateway.ajax.simulate_cart.endpoint,
+ this.gateway.ajax.simulate_cart.nonce,
+ )).simulate((data) => {
+
+ this.messages.renderWithAmount(data.total);
+
+ let enableFunding = this.gateway.url_params['enable-funding'];
+ let disableFunding = this.gateway.url_params['disable-funding'];
+
+ for (const [fundingSource, funding] of Object.entries(data.funding)) {
+ if (funding.enabled === true) {
+ enableFunding = strAddWord(enableFunding, fundingSource);
+ disableFunding = strRemoveWord(disableFunding, fundingSource);
+ } else if (funding.enabled === false) {
+ enableFunding = strRemoveWord(enableFunding, fundingSource);
+ disableFunding = strAddWord(disableFunding, fundingSource);
+ }
+ }
+
+ if (
+ (enableFunding !== this.gateway.url_params['enable-funding']) ||
+ (disableFunding !== this.gateway.url_params['disable-funding'])
+ ) {
+ this.gateway.url_params['enable-funding'] = enableFunding;
+ this.gateway.url_params['disable-funding'] = disableFunding;
+ jQuery(this.gateway.button.wrapper).trigger('ppcp-reload-buttons');
+ }
+
+ if (typeof data.button.is_disabled === 'boolean') {
+ this.gateway.button.is_disabled = data.button.is_disabled;
+ }
+
+ this.handleButtonStatus(false);
+
+ }, actionHandler.getProducts());
+ }
}
export default SingleProductBootstap;
diff --git a/modules/ppcp-button/resources/js/modules/DataClientIdAttributeHandler.js b/modules/ppcp-button/resources/js/modules/DataClientIdAttributeHandler.js
index 5f70d636b..1d61e3bb5 100644
--- a/modules/ppcp-button/resources/js/modules/DataClientIdAttributeHandler.js
+++ b/modules/ppcp-button/resources/js/modules/DataClientIdAttributeHandler.js
@@ -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);
+ }
+ });
});
}
diff --git a/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js b/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js
index c5742ab19..09c95aa87 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/ScriptLoading.js
@@ -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);
}
diff --git a/modules/ppcp-button/resources/js/modules/Helper/Utils.js b/modules/ppcp-button/resources/js/modules/Helper/Utils.js
index 565b64863..0e97443b8 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/Utils.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/Utils.js
@@ -31,11 +31,54 @@ export const strRemoveWord = (str, word, separator = ',') => {
return arr.join(separator);
};
+export const debounce = (func, wait) => {
+ let timeout;
+ return function() {
+ const context = this;
+ const args = arguments;
+ const later = function() {
+ timeout = null;
+ func.apply(context, args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+}
+
+export const throttle = (func, limit) => {
+ let inThrottle, lastArgs, lastContext;
+
+ function execute() {
+ inThrottle = true;
+ func.apply(this, arguments);
+ setTimeout(() => {
+ inThrottle = false;
+ if (lastArgs) {
+ const nextArgs = lastArgs;
+ const nextContext = lastContext;
+ lastArgs = lastContext = null;
+ execute.apply(nextContext, nextArgs);
+ }
+ }, limit);
+ }
+
+ return function() {
+ if (!inThrottle) {
+ execute.apply(this, arguments);
+ } else {
+ lastArgs = arguments;
+ lastContext = this;
+ }
+ };
+}
+
const Utils = {
toCamelCase,
keysToCamelCase,
strAddWord,
- strRemoveWord
+ strRemoveWord,
+ debounce,
+ throttle
};
export default Utils;
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
index 828134e46..4e6dad65d 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
@@ -74,6 +74,8 @@ class Renderer {
renderButtons(wrapper, style, contextConfig, hasEnabledSeparateGateways, fundingSource = null) {
if (! document.querySelector(wrapper) || this.isAlreadyRendered(wrapper, fundingSource, hasEnabledSeparateGateways) ) {
+ // Try to render registered buttons again in case they were removed from the DOM by an external source.
+ widgetBuilder.renderButtons(wrapper);
return;
}
@@ -95,19 +97,19 @@ class Renderer {
}
}
- //jQuery(document).off('ppcp-reload-buttons');
- jQuery(document).on('ppcp-reload-buttons', wrapper, (event, settingsOverride = {}) => {
- const settings = merge(this.defaultSettings, settingsOverride);
- let scriptOptions = keysToCamelCase(settings.url_params);
- scriptOptions = merge(scriptOptions, settings.script_attributes);
+ jQuery(document)
+ .off('ppcp-reload-buttons', wrapper)
+ .on('ppcp-reload-buttons', wrapper, (event, settingsOverride = {}) => {
+ const settings = merge(this.defaultSettings, settingsOverride);
+ let scriptOptions = keysToCamelCase(settings.url_params);
+ scriptOptions = merge(scriptOptions, settings.script_attributes);
- loadScript(scriptOptions).then((paypal) => {
- widgetBuilder.setPaypal(paypal);
- widgetBuilder.registerButtons(wrapper, buttonsOptions());
- widgetBuilder.renderAllButtons();
- widgetBuilder.renderAllMessages();
+ loadScript(scriptOptions).then((paypal) => {
+ widgetBuilder.setPaypal(paypal);
+ widgetBuilder.registerButtons(wrapper, buttonsOptions());
+ widgetBuilder.renderAll();
+ });
});
- });
this.renderedSources.add(wrapper + (fundingSource ?? ''));
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js b/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js
index 45d5a0b82..ed45b35b4 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js
@@ -30,6 +30,10 @@ class WidgetBuilder {
return;
}
+ if (this.hasRendered(wrapper)) {
+ return;
+ }
+
const entry = this.buttons.get(wrapper);
const btn = this.paypal.Buttons(entry.options);
@@ -58,6 +62,10 @@ class WidgetBuilder {
return;
}
+ if (this.hasRendered(wrapper)) {
+ return;
+ }
+
const entry = this.messages.get(wrapper);
const btn = this.paypal.Messages(entry.options);
@@ -70,6 +78,14 @@ class WidgetBuilder {
}
}
+ renderAll() {
+ this.renderAllButtons();
+ this.renderAllMessages();
+ }
+
+ hasRendered(wrapper) {
+ return document.querySelector(wrapper).hasChildNodes();
+ }
}
export default new WidgetBuilder();
diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php
index 4aae9f12e..c3ea172c8 100644
--- a/modules/ppcp-button/src/Assets/SmartButton.php
+++ b/modules/ppcp-button/src/Assets/SmartButton.php
@@ -1384,7 +1384,7 @@ 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.
+ * @param float|null $price_total The price total to be considered.
* @return bool
*/
public function is_button_disabled( string $context = null, float $price_total = null ): bool {
@@ -1429,7 +1429,7 @@ 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 string $location The location.
* @param float|null $price_total The price total to be considered.
* @return bool
*/
@@ -1476,7 +1476,7 @@ class SmartButton implements SmartButtonInterface {
/**
* Check whether Pay Later button is enabled for a given location.
*
- * @param string $location The 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.
*/
@@ -1489,7 +1489,7 @@ class SmartButton implements SmartButtonInterface {
/**
* Check whether Pay Later message is enabled for a given location.
*
- * @param string $location The location setting name.
+ * @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.
*/
diff --git a/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php b/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php
index 9c518938e..5f61d33b9 100644
--- a/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php
+++ b/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php
@@ -1,4 +1,9 @@
cart->empty_cart( false );
@@ -106,20 +114,20 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
foreach ( $products as $product ) {
if ( $product['product']->is_type( 'booking' ) ) {
$success = $success && $this->add_booking_product(
- $product['product'],
- $product['booking']
- );
+ $product['product'],
+ $product['booking']
+ );
} elseif ( $product['product']->is_type( 'variable' ) ) {
$success = $success && $this->add_variable_product(
- $product['product'],
- $product['quantity'],
- $product['variations']
- );
+ $product['product'],
+ $product['quantity'],
+ $product['variations']
+ );
} else {
$success = $success && $this->add_product(
- $product['product'],
- $product['quantity']
- );
+ $product['product'],
+ $product['quantity']
+ );
}
}
@@ -164,6 +172,8 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
}
/**
+ * Returns product information from request data.
+ *
* @return array|false
*/
protected function products_from_request() {
@@ -304,6 +314,8 @@ abstract class AbstractCartEndpoint implements EndpointInterface {
}
/**
+ * Removes stored cart items from WooCommerce cart.
+ *
* @return void
*/
protected function remove_cart_items(): void {
diff --git a/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php b/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php
index d8c1a3522..44449a761 100644
--- a/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php
+++ b/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php
@@ -65,7 +65,7 @@ class CartScriptParamsEndpoint implements EndpointInterface {
*/
public function handle_request(): bool {
try {
- if ( is_callable('wc_maybe_define_constant') ) {
+ if ( is_callable( 'wc_maybe_define_constant' ) ) {
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
}
diff --git a/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php b/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php
index 146af5ca4..95979d1b2 100644
--- a/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php
+++ b/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php
@@ -16,7 +16,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
/**
* Class ChangeCartEndpoint
*/
-class ChangeCartEndpoint extends AbstractCartEndpoint {
+class ChangeCartEndpoint extends AbstractCartEndpoint {
const ENDPOINT = 'ppc-change-cart';
@@ -70,13 +70,15 @@ class ChangeCartEndpoint extends AbstractCartEndpoint {
* @throws Exception On error.
*/
protected function handle_data(): bool {
- if ( ! $products = $this->products_from_request() ) {
+ $products = $this->products_from_request();
+
+ if ( ! $products ) {
return false;
}
$this->shipping->reset_shipping();
- if ( ! $this->add_products($products) ) {
+ if ( ! $this->add_products( $products ) ) {
return false;
}
diff --git a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
index 8e09a25b2..b99b69e8c 100644
--- a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
+++ b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
@@ -59,40 +59,43 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
* @throws Exception On error.
*/
protected function handle_data(): bool {
- if ( ! $products = $this->products_from_request() ) {
+ $products = $this->products_from_request();
+
+ if ( ! $products ) {
return false;
}
// Set WC default cart as the clone.
// Store a reference to the real cart.
- $activeCart = WC()->cart;
- WC()->cart = $this->cart;
+ $active_cart = WC()->cart;
+ WC()->cart = $this->cart;
- if ( ! $this->add_products($products) ) {
+ 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 = $activeCart;
+ // Restore cart and unset cart clone.
+ WC()->cart = $active_cart;
unset( $this->cart );
wp_send_json_success(
array(
- 'total' => $total,
- 'funding' => [
- 'paylater' => [
- 'enabled' => $this->smart_button->is_pay_later_button_enabled_for_location( 'cart', $total ),
+ 'total' => $total,
+ 'funding' => array(
+ 'paylater' => array(
+ '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' => [
+ ),
+ ),
+ 'button' => array(
'is_disabled' => $this->smart_button->is_button_disabled( 'cart', $total ),
- ]
+ ),
)
);
return true;
From 044a704048713a6f0d4a491b4cf0144a8eae957f Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Fri, 21 Jul 2023 10:39:18 +0100
Subject: [PATCH 07/14] Remove unused debounce function
---
.../resources/js/modules/Helper/Utils.js | 15 ---------------
.../resources/js/modules/Renderer/Renderer.js | 4 ++--
2 files changed, 2 insertions(+), 17 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Helper/Utils.js b/modules/ppcp-button/resources/js/modules/Helper/Utils.js
index 0e97443b8..c1fbb8aec 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/Utils.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/Utils.js
@@ -31,20 +31,6 @@ export const strRemoveWord = (str, word, separator = ',') => {
return arr.join(separator);
};
-export const debounce = (func, wait) => {
- let timeout;
- return function() {
- const context = this;
- const args = arguments;
- const later = function() {
- timeout = null;
- func.apply(context, args);
- };
- clearTimeout(timeout);
- timeout = setTimeout(later, wait);
- };
-}
-
export const throttle = (func, limit) => {
let inThrottle, lastArgs, lastContext;
@@ -77,7 +63,6 @@ const Utils = {
keysToCamelCase,
strAddWord,
strRemoveWord,
- debounce,
throttle
};
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
index 4e6dad65d..82caceea4 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
@@ -98,8 +98,8 @@ class Renderer {
}
jQuery(document)
- .off('ppcp-reload-buttons', wrapper)
- .on('ppcp-reload-buttons', wrapper, (event, settingsOverride = {}) => {
+ .off(this.reloadEventName, wrapper)
+ .on(this.reloadEventName, wrapper, (event, settingsOverride = {}) => {
const settings = merge(this.defaultSettings, settingsOverride);
let scriptOptions = keysToCamelCase(settings.url_params);
scriptOptions = merge(scriptOptions, settings.script_attributes);
From cc79f62cab6b5346aaf347906ccd441589bbca48 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Tue, 25 Jul 2023 08:35:07 +0100
Subject: [PATCH 08/14] * Remove commented code
---
.../resources/js/modules/Renderer/MessageRenderer.js | 2 --
.../resources/js/modules/Renderer/Renderer.js | 10 +---------
2 files changed, 1 insertion(+), 11 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
index 45f67ac67..afbb55efa 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js
@@ -32,8 +32,6 @@ class MessageRenderer {
widgetBuilder.registerMessages(this.config.wrapper, options);
widgetBuilder.renderMessages(this.config.wrapper);
-
- // paypal.Messages(options).render(this.config.wrapper);
}
optionsEqual(options) {
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
index 82caceea4..629c1abc9 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
@@ -119,15 +119,7 @@ class Renderer {
}
}
- 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();
- // }
+ isAlreadyRendered(wrapper, fundingSource) {
return this.renderedSources.has(wrapper + (fundingSource ?? ''));
}
From 0fed872c1309df4d257d041c9c001382f9bdc7cc Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Tue, 25 Jul 2023 16:33:56 +0100
Subject: [PATCH 09/14] Add ability in WidgetBuilder to have multiple buttons
rendered per wrapper.
---
.../SingleProductActionHandler.js | 9 +-
.../ContextBootstrap/SingleProductBootstap.js | 9 +-
.../resources/js/modules/Renderer/Renderer.js | 16 ++--
.../js/modules/Renderer/WidgetBuilder.js | 85 +++++++++++++++++--
4 files changed, 104 insertions(+), 15 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js
index 1e5f54ea4..abc07c594 100644
--- a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js
+++ b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js
@@ -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 {
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
index f09ef7fda..611e9e3e8 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
@@ -163,6 +163,13 @@ class SingleProductBootstap {
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,
@@ -198,7 +205,7 @@ class SingleProductBootstap {
this.handleButtonStatus(false);
- }, actionHandler.getProducts());
+ }, products);
}
}
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
index 629c1abc9..1fd2e22e3 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js
@@ -75,7 +75,7 @@ class Renderer {
renderButtons(wrapper, style, contextConfig, hasEnabledSeparateGateways, fundingSource = null) {
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);
+ widgetBuilder.renderButtons([wrapper, fundingSource]);
return;
}
@@ -99,14 +99,20 @@ class Renderer {
jQuery(document)
.off(this.reloadEventName, wrapper)
- .on(this.reloadEventName, wrapper, (event, settingsOverride = {}) => {
+ .on(this.reloadEventName, wrapper, (event, settingsOverride = {}, triggeredFundingSource) => {
+
+ // Only accept events from the matching funding source
+ if (fundingSource && triggeredFundingSource && (triggeredFundingSource !== fundingSource)) {
+ return;
+ }
+
const settings = merge(this.defaultSettings, settingsOverride);
let scriptOptions = keysToCamelCase(settings.url_params);
scriptOptions = merge(scriptOptions, settings.script_attributes);
loadScript(scriptOptions).then((paypal) => {
widgetBuilder.setPaypal(paypal);
- widgetBuilder.registerButtons(wrapper, buttonsOptions());
+ widgetBuilder.registerButtons([wrapper, fundingSource], buttonsOptions());
widgetBuilder.renderAll();
});
});
@@ -114,8 +120,8 @@ class Renderer {
this.renderedSources.add(wrapper + (fundingSource ?? ''));
if (typeof paypal !== 'undefined' && typeof paypal.Buttons !== 'undefined') {
- widgetBuilder.registerButtons(wrapper, buttonsOptions());
- widgetBuilder.renderButtons(wrapper);
+ widgetBuilder.registerButtons([wrapper, fundingSource], buttonsOptions());
+ widgetBuilder.renderButtons([wrapper, fundingSource]);
}
}
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js b/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js
index ed45b35b4..cc5077d40 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js
@@ -1,4 +1,7 @@
-
+/**
+ * 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() {
@@ -19,14 +22,18 @@ class WidgetBuilder {
}
registerButtons(wrapper, options) {
- this.buttons.set(wrapper, {
+ wrapper = this.sanitizeWrapper(wrapper);
+
+ this.buttons.set(this.toKey(wrapper), {
wrapper: wrapper,
- options: options
+ options: options,
});
}
renderButtons(wrapper) {
- if (!this.buttons.has(wrapper)) {
+ wrapper = this.sanitizeWrapper(wrapper);
+
+ if (!this.buttons.has(this.toKey(wrapper))) {
return;
}
@@ -34,14 +41,21 @@ class WidgetBuilder {
return;
}
- const entry = this.buttons.get(wrapper);
+ 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;
}
- btn.render(entry.wrapper);
+ let target = this.buildWrapperTarget(wrapper);
+
+ if (!target) {
+ return;
+ }
+
+ btn.render(target);
}
renderAllButtons() {
@@ -84,7 +98,64 @@ class WidgetBuilder {
}
hasRendered(wrapper) {
- return document.querySelector(wrapper).hasChildNodes();
+ 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(``);
+ $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;
}
}
From 8d2adc3ffe6185579f0318778bef0b8a4f0c5636 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Wed, 26 Jul 2023 10:29:33 +0100
Subject: [PATCH 10/14] Fix e2e tests
---
.github/workflows/e2e.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index c6496f58a..3731a1de4 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -15,7 +15,7 @@ jobs:
- uses: satackey/action-docker-layer-caching@v0.0.11
continue-on-error: true
- - uses: jonaseberle/github-action-setup-ddev@v1
+ - uses: ddev/github-action-setup-ddev@v1
with:
autostart: false
From 9434a8430194fe352cf0ce8aa42a2bf21c95f330 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Wed, 26 Jul 2023 15:21:31 +0100
Subject: [PATCH 11/14] Fix set paypal object to widgetBuilder on preview
buttons
---
modules/ppcp-wc-gateway/resources/js/gateway-settings.js | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js
index 01ac034c8..c20432d48 100644
--- a/modules/ppcp-wc-gateway/resources/js/gateway-settings.js
+++ b/modules/ppcp-wc-gateway/resources/js/gateway-settings.js
@@ -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);
From 9cdd11b3b3a0444f5fbfdea5cff2841630916ba6 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Thu, 27 Jul 2023 09:57:19 +0100
Subject: [PATCH 12/14] Refactor paylater and buttons filters for simplicity
Add show / hide messages functionality on the frontend
---
.../modules/ContextBootstrap/CartBootstap.js | 3 +-
.../ContextBootstrap/SingleProductBootstap.js | 3 +
.../js/modules/Helper/BootstrapHelper.js | 34 ++++-
.../ppcp-button/src/Assets/SmartButton.php | 143 +++++++-----------
.../src/Endpoint/CartScriptParamsEndpoint.php | 1 +
.../src/Endpoint/SimulateCartEndpoint.php | 23 ++-
6 files changed, 104 insertions(+), 103 deletions(-)
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js
index f55663b74..f02b628ee 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js
@@ -49,8 +49,9 @@ class CartBootstrap {
}
// handle button status
- if (result.data.button) {
+ if (result.data.button || result.data.messages) {
this.gateway.button = result.data.button;
+ this.gateway.messages = result.data.messages;
this.handleButtonStatus();
}
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
index 611e9e3e8..81a875ab4 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js
@@ -202,6 +202,9 @@ class SingleProductBootstap {
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);
diff --git a/modules/ppcp-button/resources/js/modules/Helper/BootstrapHelper.js b/modules/ppcp-button/resources/js/modules/Helper/BootstrapHelper.js
index ba00e2c76..8ba3d0e09 100644
--- a/modules/ppcp-button/resources/js/modules/Helper/BootstrapHelper.js
+++ b/modules/ppcp-button/resources/js/modules/Helper/BootstrapHelper.js
@@ -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;
+ }
}
diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php
index c3ea172c8..bd1577e09 100644
--- a/modules/ppcp-button/src/Assets/SmartButton.php
+++ b/modules/ppcp-button/src/Assets/SmartButton.php
@@ -383,7 +383,7 @@ class SmartButton implements SmartButtonInterface {
* @throws NotFoundException When a setting was not found.
*/
private function render_message_wrapper_registrar(): bool {
- if ( ! $this->is_pay_later_messaging_enabled() ) {
+ if ( ! $this->settings_status->is_pay_later_messaging_enabled() ) {
return false;
}
@@ -549,7 +549,7 @@ class SmartButton implements SmartButtonInterface {
$smart_button_enabled_for_current_location = $this->settings_status->is_smart_button_enabled_for_location( $this->context() );
$smart_button_enabled_for_mini_cart = $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' );
- $messaging_enabled_for_current_location = $this->is_pay_later_messaging_enabled_for_location( $this->context() );
+ $messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $this->context() );
switch ( $this->context() ) {
case 'checkout':
@@ -658,7 +658,7 @@ class SmartButton implements SmartButtonInterface {
* @throws NotFoundException When a setting was not found.
*/
private function message_values(): array {
- if ( ! $this->is_pay_later_messaging_enabled() ) {
+ if ( ! $this->settings_status->is_pay_later_messaging_enabled() ) {
return array();
}
@@ -684,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(
@@ -1334,18 +1335,6 @@ class SmartButton implements SmartButtonInterface {
return WC()->cart && WC()->cart->get_total( 'numeric' ) == 0;
}
- /**
- * Returns the cart total.
- *
- * @return ?float
- */
- protected function get_cart_price_total(): ?float {
- if ( ! WC()->cart ) {
- return null;
- }
- return (float) WC()->cart->get_total( 'numeric' );
- }
-
/**
* Checks if PayPal buttons/messages can be rendered for the given product.
*
@@ -1380,108 +1369,90 @@ class SmartButton implements SmartButtonInterface {
);
}
+ /**
+ * Fills and returns the product context_data array to be used in filters.
+ *
+ * @param array $context_data
+ * @return array
+ */
+ private function product_filter_context_data( array $context_data = [] ): array {
+ if ( ! isset( $context_data['product'] ) ) {
+ $context_data['product'] = wc_get_product();
+ }
+ if ( ! $context_data['product'] ) {
+ return [];
+ }
+ 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 float|null $price_total The price total to be considered.
+ * @param array $context_data The context data for this filter
* @return bool
*/
- public function is_button_disabled( string $context = null, float $price_total = null ): bool {
+ public function is_button_disabled( string $context = null, array $context_data = [] ): 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
- */
- $is_disabled = apply_filters(
+ // Allows to decide if the button should be disabled for a given product.
+ 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
- */
- $is_disabled = apply_filters(
+ // Allows to decide if the button should be disabled globally or on a given context.
+ return apply_filters(
'woocommerce_paypal_payments_buttons_disabled',
- null,
- $context,
- null === $price_total ? $this->get_cart_price_total() : $price_total
+ 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 float|null $price_total The price total to be considered.
+ * @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, float $price_total = null ): bool {
+ private function is_pay_later_filter_enabled_for_location( string $location, array $context_data = [] ): bool {
if ( 'product' === $location ) {
- $product = wc_get_product();
-
- if ( ! $product ) {
- return true;
- }
-
- /**
- * Allows to decide if the button should be disabled for a given product
- */
- $is_disabled = apply_filters(
+ // Allows to decide if the button should be disabled for a given product.
+ return ! apply_filters(
'woocommerce_paypal_payments_product_buttons_paylater_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
- */
- $is_disabled = apply_filters(
+ // Allows to decide if the button should be disabled on a given context.
+ return ! apply_filters(
'woocommerce_paypal_payments_buttons_paylater_disabled',
- null,
- $location,
- null === $price_total ? $this->get_cart_price_total() : $price_total
+ false,
+ $location
);
-
- if ( $is_disabled !== null ) {
- return ! $is_disabled;
- }
-
- return true;
}
/**
* 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.
+ * @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, float $price_total = null ): bool {
- return $this->is_pay_later_filter_enabled_for_location( $location, $price_total )
+ public function is_pay_later_button_enabled_for_location( string $location, array $context_data = [] ): bool {
+ return $this->is_pay_later_filter_enabled_for_location( $location, $context_data )
&& $this->settings_status->is_pay_later_button_enabled_for_location( $location );
}
@@ -1490,24 +1461,14 @@ 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.
+ * @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, float $price_total = null ): bool {
- return $this->is_pay_later_filter_enabled_for_location( $location, $price_total )
+ public function is_pay_later_messaging_enabled_for_location( string $location, array $context_data = [] ): bool {
+ return $this->is_pay_later_filter_enabled_for_location( $location, $context_data )
&& $this->settings_status->is_pay_later_messaging_enabled_for_location( $location );
}
- /**
- * Check whether Pay Later message is enabled
- *
- * @return bool true if is enabled, otherwise false.
- */
- private function is_pay_later_messaging_enabled(): bool {
- return $this->is_pay_later_filter_enabled_for_location( $this->context() )
- && $this->settings_status->is_pay_later_messaging_enabled();
- }
-
/**
* Retrieves all payment tokens for the user, via API or cached if already queried.
*
diff --git a/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php b/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php
index 44449a761..58350f6f8 100644
--- a/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php
+++ b/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php
@@ -75,6 +75,7 @@ class CartScriptParamsEndpoint implements EndpointInterface {
array(
'url_params' => $script_data['url_params'],
'button' => $script_data['button'],
+ 'messages' => $script_data['messages'],
'amount' => WC()->cart->get_total( 'raw' ),
)
);
diff --git a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
index b99b69e8c..62defeec6 100644
--- a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
+++ b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
@@ -84,17 +84,34 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
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 = [
+ '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' => $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 ),
+ 'enabled' => $pay_later_enabled,
),
),
'button' => array(
- 'is_disabled' => $this->smart_button->is_button_disabled( 'cart', $total ),
+ 'is_disabled' => ! $button_enabled,
+ ),
+ 'messages' => array(
+ 'is_hidden' => ! $pay_later_messaging_enabled,
),
)
);
From b25a124ffadc4c46445c74e8539bfb1e8f999d34 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Thu, 27 Jul 2023 10:29:24 +0100
Subject: [PATCH 13/14] * Fix lint
---
.../ppcp-button/src/Assets/SmartButton.php | 44 +++++++++++--------
.../src/Endpoint/SimulateCartEndpoint.php | 23 +++++-----
2 files changed, 38 insertions(+), 29 deletions(-)
diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php
index bd1577e09..b40bfd266 100644
--- a/modules/ppcp-button/src/Assets/SmartButton.php
+++ b/modules/ppcp-button/src/Assets/SmartButton.php
@@ -1372,15 +1372,15 @@ class SmartButton implements SmartButtonInterface {
/**
* Fills and returns the product context_data array to be used in filters.
*
- * @param array $context_data
+ * @param array $context_data The context data for this filter.
* @return array
*/
- private function product_filter_context_data( array $context_data = [] ): 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 [];
+ 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' );
@@ -1393,24 +1393,28 @@ 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 array $context_data The context data for this filter
+ * @param array $context_data The context data for this filter.
* @return bool
*/
- public function is_button_disabled( string $context = null, array $context_data = [] ): bool {
+ public function is_button_disabled( string $context = null, array $context_data = array() ): bool {
if ( null === $context ) {
$context = $this->context();
}
if ( 'product' === $context ) {
- // 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.
+ */
return apply_filters(
'woocommerce_paypal_payments_product_buttons_disabled',
false,
- $this->product_filter_context_data($context_data)
+ $this->product_filter_context_data( $context_data )
);
}
- // 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.
+ */
return apply_filters(
'woocommerce_paypal_payments_buttons_disabled',
false,
@@ -1422,21 +1426,25 @@ 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 array $context_data The context data for this filter
+ * @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 = [] ): 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.
+ /**
+ * 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)
+ $this->product_filter_context_data( $context_data )
);
}
- // Allows to decide if the button should be disabled on a given context.
+ /**
+ * Allows to decide if the button should be disabled on a given context.
+ */
return ! apply_filters(
'woocommerce_paypal_payments_buttons_paylater_disabled',
false,
@@ -1448,10 +1456,10 @@ class SmartButton implements SmartButtonInterface {
* 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
+ * @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 = [] ): bool {
+ 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 );
@@ -1460,11 +1468,11 @@ class SmartButton implements SmartButtonInterface {
/**
* 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
+ * @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 = [] ): bool {
+ 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 );
}
diff --git a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
index 62defeec6..5e3796dbb 100644
--- a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
+++ b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php
@@ -85,32 +85,33 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
unset( $this->cart );
// Process filters.
- $pay_later_enabled = true;
+ $pay_later_enabled = true;
$pay_later_messaging_enabled = true;
- $button_enabled = true;
+ $button_enabled = true;
foreach ( $products as $product ) {
- $context_data = [
- 'product' => $product['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_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 );
+ $button_enabled = $button_enabled && ! $this->smart_button->is_button_disabled( 'product', $context_data );
}
wp_send_json_success(
array(
- 'total' => $total,
- 'funding' => array(
+ 'total' => $total,
+ 'funding' => array(
'paylater' => array(
'enabled' => $pay_later_enabled,
),
),
- 'button' => array(
+ 'button' => array(
'is_disabled' => ! $button_enabled,
),
- 'messages' => array(
+ 'messages' => array(
'is_hidden' => ! $pay_later_messaging_enabled,
),
)
From ecbe4a9abffa8a7fd95e530ef40118367a46d689 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Thu, 27 Jul 2023 17:13:53 +0100
Subject: [PATCH 14/14] * Fix add fix for Messages loading edge case and a
Widget reload event listener.
---
.../modules/ContextBootstrap/CheckoutBootstap.js | 1 -
.../js/modules/Renderer/WidgetBuilder.js | 15 +++++++++++++++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js
index db1cba84f..85fdab285 100644
--- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js
+++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js
@@ -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) {
diff --git a/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js b/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js
index cc5077d40..07d7c057c 100644
--- a/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js
+++ b/modules/ppcp-button/resources/js/modules/Renderer/WidgetBuilder.js
@@ -9,12 +9,20 @@ class WidgetBuilder {
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) {
@@ -84,6 +92,13 @@ class WidgetBuilder {
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() {