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() {