From f812d043af40da570edcbdd21b4629a7ec4d6d66 Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Tue, 30 May 2023 15:11:13 +0200 Subject: [PATCH 01/26] Add soft descriptor to PurchaseUnitFactory PCP-1709 --- modules/ppcp-api-client/services.php | 4 +++- .../src/Factory/PurchaseUnitFactory.php | 18 ++++++++++++++---- modules/ppcp-wc-gateway/services.php | 4 ++++ 3 files changed, 21 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index e7347b3bd..0deb058f8 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -298,6 +298,7 @@ return array( $shipping_factory = $container->get( 'api.factory.shipping' ); $payments_factory = $container->get( 'api.factory.payments' ); $prefix = $container->get( 'api.prefix' ); + $soft_descriptor = $container->get( 'wcgateway.soft-descriptor' ); return new PurchaseUnitFactory( $amount_factory, @@ -306,7 +307,8 @@ return array( $item_factory, $shipping_factory, $payments_factory, - $prefix + $prefix, + $soft_descriptor ); }, 'api.factory.patch-collection-factory' => static function ( ContainerInterface $container ): PatchCollectionFactory { diff --git a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php index 9f8f12c5e..d5b0a801e 100644 --- a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php +++ b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php @@ -68,6 +68,13 @@ class PurchaseUnitFactory { */ private $prefix; + /** + * The Soft Descriptor. + * + * @var string + */ + private $soft_descriptor; + /** * PurchaseUnitFactory constructor. * @@ -78,6 +85,7 @@ class PurchaseUnitFactory { * @param ShippingFactory $shipping_factory The shipping factory. * @param PaymentsFactory $payments_factory The payments factory. * @param string $prefix The prefix. + * @param string $soft_descriptor The soft descriptor. */ public function __construct( AmountFactory $amount_factory, @@ -86,7 +94,8 @@ class PurchaseUnitFactory { ItemFactory $item_factory, ShippingFactory $shipping_factory, PaymentsFactory $payments_factory, - string $prefix = 'WC-' + string $prefix = 'WC-', + string $soft_descriptor = '' ) { $this->amount_factory = $amount_factory; @@ -96,6 +105,7 @@ class PurchaseUnitFactory { $this->shipping_factory = $shipping_factory; $this->payments_factory = $payments_factory; $this->prefix = $prefix; + $this->soft_descriptor = $soft_descriptor; } /** @@ -126,7 +136,7 @@ class PurchaseUnitFactory { $payee = $this->payee_repository->payee(); $custom_id = (string) $order->get_id(); $invoice_id = $this->prefix . $order->get_order_number(); - $soft_descriptor = ''; + $soft_descriptor = $this->soft_descriptor; $purchase_unit = new PurchaseUnit( $amount, @@ -189,7 +199,7 @@ class PurchaseUnitFactory { $custom_id = ''; $invoice_id = ''; - $soft_descriptor = ''; + $soft_descriptor = $this->soft_descriptor; $purchase_unit = new PurchaseUnit( $amount, $items, @@ -224,7 +234,7 @@ class PurchaseUnitFactory { $description = ( isset( $data->description ) ) ? $data->description : ''; $custom_id = ( isset( $data->custom_id ) ) ? $data->custom_id : ''; $invoice_id = ( isset( $data->invoice_id ) ) ? $data->invoice_id : ''; - $soft_descriptor = ( isset( $data->soft_descriptor ) ) ? $data->soft_descriptor : ''; + $soft_descriptor = ( isset( $data->soft_descriptor ) ) ? $data->soft_descriptor : $this->soft_descriptor; $items = array(); if ( isset( $data->items ) && is_array( $data->items ) ) { $items = array_map( diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 9577f027d..255de3e76 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -955,6 +955,10 @@ return array( return 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s'; }, + 'wcgateway.soft-descriptor' => static function ( ContainerInterface $container ): string { + return 'soft descriptor test'; + }, + 'wcgateway.transaction-url-provider' => static function ( ContainerInterface $container ): TransactionUrlProvider { $sandbox_url_base = $container->get( 'wcgateway.transaction-url-sandbox' ); $live_url_base = $container->get( 'wcgateway.transaction-url-live' ); From 220da3cfea343de68d9943122039a0ab3ff40241 Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Fri, 2 Jun 2023 10:48:47 +0200 Subject: [PATCH 02/26] Add soft descriptor setting PCP-1709 --- modules/ppcp-wc-gateway/services.php | 7 ++++++- .../src/Settings/Fields/connection-tab-fields.php | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 255de3e76..f7141684d 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -956,7 +956,12 @@ return array( }, 'wcgateway.soft-descriptor' => static function ( ContainerInterface $container ): string { - return 'soft descriptor test'; + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings); + if ( $settings->has( 'soft_descriptor' ) ) { + return $settings->get( 'soft_descriptor' ); + } + return ''; }, 'wcgateway.transaction-url-provider' => static function ( ContainerInterface $container ): TransactionUrlProvider { diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index 6ac84583a..dd3d95f37 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -423,6 +423,20 @@ return function ( ContainerInterface $container, array $fields ): array { '' ), ), + 'soft_descriptor' => array( + 'title' => __( 'Soft Descriptor', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => __( 'The soft descriptor is the dynamic text used to construct the statement descriptor that appears on a payer\'s card statement.', 'woocommerce-paypal-payments' ), + 'maxlength' => 22, + 'default' => '', + 'screens' => array( + State::STATE_START, + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => Settings::CONNECTION_TAB_ID, + ), 'prefix' => array( 'title' => __( 'Invoice prefix', 'woocommerce-paypal-payments' ), 'type' => 'text', From b7c4055610413f98c0dabc3c7e5103606ed0526e Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Mon, 12 Jun 2023 09:00:08 +0200 Subject: [PATCH 03/26] Fix CS --- modules/ppcp-wc-gateway/services.php | 2 +- .../Settings/Fields/connection-tab-fields.php | 20 +++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 8a82c0ea5..da2baa718 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -956,7 +956,7 @@ return array( 'wcgateway.soft-descriptor' => static function ( ContainerInterface $container ): string { $settings = $container->get( 'wcgateway.settings' ); - assert( $settings instanceof Settings); + assert( $settings instanceof Settings ); if ( $settings->has( 'soft_descriptor' ) ) { return $settings->get( 'soft_descriptor' ); } diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index dd3d95f37..e5f006cfb 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -423,19 +423,19 @@ return function ( ContainerInterface $container, array $fields ): array { '' ), ), - 'soft_descriptor' => array( - 'title' => __( 'Soft Descriptor', 'woocommerce-paypal-payments' ), - 'type' => 'text', - 'desc_tip' => true, - 'description' => __( 'The soft descriptor is the dynamic text used to construct the statement descriptor that appears on a payer\'s card statement.', 'woocommerce-paypal-payments' ), - 'maxlength' => 22, - 'default' => '', - 'screens' => array( + 'soft_descriptor' => array( + 'title' => __( 'Soft Descriptor', 'woocommerce-paypal-payments' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => __( 'The soft descriptor is the dynamic text used to construct the statement descriptor that appears on a payer\'s card statement.', 'woocommerce-paypal-payments' ), + 'maxlength' => 22, + 'default' => '', + 'screens' => array( State::STATE_START, State::STATE_ONBOARDED, ), - 'requirements' => array(), - 'gateway' => Settings::CONNECTION_TAB_ID, + 'requirements' => array(), + 'gateway' => Settings::CONNECTION_TAB_ID, ), 'prefix' => array( 'title' => __( 'Invoice prefix', 'woocommerce-paypal-payments' ), From 5f550ae976fe87d71c8dabbae7006885ec031cf4 Mon Sep 17 00:00:00 2001 From: carmenmaymo Date: Wed, 14 Jun 2023 12:19:41 +0200 Subject: [PATCH 04/26] Add max value to tooltip --- .../src/Settings/Fields/connection-tab-fields.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index e5f006cfb..d0d25bc61 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -427,7 +427,7 @@ return function ( ContainerInterface $container, array $fields ): array { 'title' => __( 'Soft Descriptor', 'woocommerce-paypal-payments' ), 'type' => 'text', 'desc_tip' => true, - 'description' => __( 'The soft descriptor is the dynamic text used to construct the statement descriptor that appears on a payer\'s card statement.', 'woocommerce-paypal-payments' ), + 'description' => __( 'The soft descriptor is the dynamic text used to construct the statement descriptor that appears on a payer\'s card statement. Text field, max value of 22 characters.', 'woocommerce-paypal-payments' ), 'maxlength' => 22, 'default' => '', 'screens' => array( From b6a85f0d1374dea0a186e2f4d0c524b06da52c5b Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 16 Jun 2023 11:39:20 +0300 Subject: [PATCH 05/26] Update Pay Later amount on the cart page when cart total changes --- modules/ppcp-button/resources/js/button.js | 1 + .../js/modules/ContextBootstrap/CartBootstap.js | 13 +++++++++++-- .../src/Endpoint/CartScriptParamsEndpoint.php | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/modules/ppcp-button/resources/js/button.js b/modules/ppcp-button/resources/js/button.js index 6708b0275..718378875 100644 --- a/modules/ppcp-button/resources/js/button.js +++ b/modules/ppcp-button/resources/js/button.js @@ -154,6 +154,7 @@ const bootstrap = () => { const cartBootstrap = new CartBootstrap( PayPalCommerceGateway, renderer, + messageRenderer, errorHandler, ); diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js index 72d28932b..6cc9a13af 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js @@ -2,10 +2,12 @@ import CartActionHandler from '../ActionHandler/CartActionHandler'; import {setVisible} from "../Helper/Hiding"; class CartBootstrap { - constructor(gateway, renderer, errorHandler) { + constructor(gateway, renderer, messages, errorHandler) { this.gateway = gateway; this.renderer = renderer; + this.messages = messages; this.errorHandler = errorHandler; + this.lastAmount = this.gateway.messages.amount; } init() { @@ -31,11 +33,16 @@ class CartBootstrap { return; } - const newParams = result.data; + const newParams = result.data.url_params; const reloadRequired = this.gateway.url_params.intent !== newParams.intent; // TODO: should reload the script instead setVisible(this.gateway.button.wrapper, !reloadRequired) + + if (this.lastAmount !== result.data.amount) { + this.lastAmount = result.data.amount; + this.messages.renderWithAmount(this.lastAmount); + } }); }); } @@ -61,6 +68,8 @@ class CartBootstrap { this.renderer.render( actionHandler.configuration() ); + + this.messages.renderWithAmount(this.lastAmount); } } diff --git a/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php b/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php index ee976cac3..440af66f1 100644 --- a/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CartScriptParamsEndpoint.php @@ -67,7 +67,12 @@ class CartScriptParamsEndpoint implements EndpointInterface { try { $script_data = $this->smart_button->script_data(); - wp_send_json_success( $script_data['url_params'] ); + wp_send_json_success( + array( + 'url_params' => $script_data['url_params'], + 'amount' => WC()->cart->get_total( 'raw' ), + ) + ); return true; } catch ( Throwable $error ) { From 7b04290eac05b4ed6b028c038d305fb769ecf482 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 30 Jun 2023 10:00:44 +0100 Subject: [PATCH 06/26] Add support for WooCommerce Bookings on single product page --- .../SingleProductActionHandler.js | 84 ++++++++++++------- .../js/modules/Entity/BookingProduct.js | 18 ++++ .../src/Endpoint/ChangeCartEndpoint.php | 44 +++++++++- 3 files changed, 111 insertions(+), 35 deletions(-) create mode 100644 modules/ppcp-button/resources/js/modules/Entity/BookingProduct.js diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js index 146fd7e94..a7c8ec8ec 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js @@ -1,4 +1,5 @@ import Product from '../Entity/Product'; +import BookingProduct from "../Entity/BookingProduct"; import onApprove from '../OnApproveHandler/onApproveForContinue'; import {payerData} from "../Helper/PayerData"; import {PaymentMethods} from "../Helper/CheckoutMethodState"; @@ -80,33 +81,51 @@ class SingleProductActionHandler { createOrder() { - var getProducts = null; - if (! this.isGroupedProduct() ) { - getProducts = () => { - 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)]; - } - } else { - getProducts = () => { - const products = []; - this.formElement.querySelectorAll('input[type="number"]').forEach((element) => { - if (! element.value) { - return; + let getProducts = (() => { + if ( this.isBookingProduct() ) { + return () => { + + const getPrefixedFields = (formElement, prefix) => { + let fields = {}; + for(const element of formElement.elements) { + if( element.name.startsWith(prefix) ) { + fields[element.name] = element.value; + } + } + return fields; } - 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; + + const id = document.querySelector('[name="add-to-cart"]').value; + return [new BookingProduct(id, 1, 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)]; + } } - } - const createOrder = (data, actions) => { + })(); + + return (data, actions) => { this.errorHandler.clear(); const onResolve = (purchase_units) => { @@ -139,19 +158,16 @@ class SingleProductActionHandler { }); }; - const promise = this.updateCart.update(onResolve, getProducts()); - return promise; + return this.updateCart.update(onResolve, getProducts()); }; - return createOrder; } variations() { - if (! this.hasVariations()) { return null; } - const attributes = [...this.formElement.querySelectorAll("[name^='attribute_']")].map( + return [...this.formElement.querySelectorAll("[name^='attribute_']")].map( (element) => { return { value:element.value, @@ -159,7 +175,6 @@ class SingleProductActionHandler { } } ); - return attributes; } hasVariations() @@ -171,5 +186,12 @@ class SingleProductActionHandler { { return this.formElement.classList.contains('grouped_form'); } + + isBookingProduct() + { + // detection for "woocommerce-bookings" plugin + return !!this.formElement.querySelector('.wc-booking-product-id'); + } + } export default SingleProductActionHandler; diff --git a/modules/ppcp-button/resources/js/modules/Entity/BookingProduct.js b/modules/ppcp-button/resources/js/modules/Entity/BookingProduct.js new file mode 100644 index 000000000..71552f8e2 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Entity/BookingProduct.js @@ -0,0 +1,18 @@ +import Product from "./Product"; + +class BookingProduct extends Product { + + constructor(id, quantity, booking) { + super(id, quantity, null); + this.booking = booking; + } + + data() { + return { + ...super.data(), + booking: this.booking + } + } +} + +export default BookingProduct; diff --git a/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php b/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php index 1c64ddb0d..189099485 100644 --- a/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php @@ -153,13 +153,23 @@ class ChangeCartEndpoint implements EndpointInterface { $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( + 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(); @@ -234,7 +244,8 @@ class ChangeCartEndpoint implements EndpointInterface { $products[] = array( 'product' => $wc_product, 'quantity' => (int) $product['quantity'], - 'variations' => isset( $product['variations'] ) ? $product['variations'] : null, + 'variations' => $product['variations'] ?? null, + 'booking' => $product['booking'] ?? null, ); } return $products; @@ -286,6 +297,31 @@ class ChangeCartEndpoint implements EndpointInterface { ); } + /** + * 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' ) ) { + 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 ); + } + /** * Based on the cart contents, the purchase units are created. * From b9aed09f0f545ed571a42d49a548213c18cd9995 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 30 Jun 2023 10:58:21 +0100 Subject: [PATCH 07/26] Fix phpunit tests --- tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php b/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php index 9eb6c654b..1949e62b3 100644 --- a/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php +++ b/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php @@ -88,6 +88,10 @@ class ChangeCartEndpointTest extends TestCase $defaultProduct ->shouldReceive('get_id') ->andReturn(1); + $defaultProduct + ->shouldReceive('is_type') + ->with('booking') + ->andReturn(false); $defaultProduct ->shouldReceive('is_type') ->with('variable') @@ -97,6 +101,10 @@ class ChangeCartEndpointTest extends TestCase $variationProduct ->shouldReceive('get_id') ->andReturn(2); + $variationProduct + ->shouldReceive('is_type') + ->with('booking') + ->andReturn(false); $variationProduct ->shouldReceive('is_type') ->with('variable') From 28dd3ba0a2a3fad521df255972717f4e71807216 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Fri, 30 Jun 2023 13:56:54 +0100 Subject: [PATCH 08/26] Add booking test case for ChangeCartEndpointTest::testProducts --- .../Endpoint/ChangeCartEndpointTest.php | 157 ++++++++++++------ 1 file changed, 110 insertions(+), 47 deletions(-) diff --git a/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php b/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php index 1949e62b3..64b74d91d 100644 --- a/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php +++ b/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php @@ -26,13 +26,8 @@ class ChangeCartEndpointTest extends TestCase ->once() ->with($singleProductArray['id']) ->andReturn($products[$productKey]); - if (! $singleProductArray['__test_data_is_variation']) { - $cart - ->expects('add_to_cart') - ->with($singleProductArray['id'], $singleProductArray['quantity']) - ->andReturnTrue(); - } - if ($singleProductArray['__test_data_is_variation']) { + + if ($singleProductArray['__test_data_is_variation'] ?? false) { $dataStore ->expects('find_matching_product_variation') ->with($products[$productKey], $singleProductArray['__test_data_variation_map']) @@ -47,7 +42,34 @@ class ChangeCartEndpointTest extends TestCase ) ->andReturnTrue(); } - } + elseif ($singleProductArray['__test_data_is_booking'] ?? false) { + + $processedBooking = array(); + foreach ($singleProductArray['booking'] as $key => $value) { + $processedBooking['_processed_' . $key] = $value; + } + + expect('wc_bookings_get_posted_data') + ->with($singleProductArray['booking']) + ->andReturn($processedBooking); + $cart + ->expects('add_to_cart') + ->with( + $singleProductArray['id'], + $singleProductArray['quantity'], + 0, + array(), + array('booking' => $processedBooking) + ) + ->andReturnTrue(); + } + else { + $cart + ->expects('add_to_cart') + ->with($singleProductArray['id'], $singleProductArray['quantity']) + ->andReturnTrue(); + } + } $cart ->expects('empty_cart') ->with(false); @@ -110,14 +132,33 @@ class ChangeCartEndpointTest extends TestCase ->with('variable') ->andReturn(true); - $testData = [ + $bookingData = [ + '_duration' => 2, + '_start_day' => 12, + '_start_month' => 6, + '_start_year' => 2023, + ]; + + $bookingProduct = Mockery::mock(\WC_Product::class); + $bookingProduct + ->shouldReceive('get_id') + ->andReturn(3); + $bookingProduct + ->shouldReceive('is_type') + ->with('booking') + ->andReturn(true); + $bookingProduct + ->shouldReceive('is_type') + ->with('variable') + ->andReturn(false); + + $testData = [ 'default' => [ [ 'products' => [ [ 'quantity' => 2, 'id' => 1, - '__test_data_is_variation' => false, ], ] ], @@ -129,43 +170,65 @@ class ChangeCartEndpointTest extends TestCase ] ], 'variation' => [ - [ - 'products' => [ - [ - 'quantity' => 2, - 'id' => 1, - '__test_data_is_variation' => false, - ], - [ - 'quantity' => 2, - 'id' => 2, - 'variations' => [ - [ - 'name' => 'variation-1', - 'value' => 'abc', - ], - [ - 'name' => 'variation-2', - 'value' => 'def', - ], - ], - '__test_data_is_variation' => true, - '__test_data_variation_id' => 123, - '__test_data_variation_map' => [ - 'variation-1' => 'abc', - 'variation-2' => 'def', - ] - ], - ] - ], - [ - $defaultProduct, - $variationProduct, - ], - [ - [1, 2] - ] - ] + [ + 'products' => [ + [ + 'quantity' => 2, + 'id' => 1, + ], + [ + 'quantity' => 2, + 'id' => 2, + 'variations' => [ + [ + 'name' => 'variation-1', + 'value' => 'abc', + ], + [ + 'name' => 'variation-2', + 'value' => 'def', + ], + ], + '__test_data_is_variation' => true, + '__test_data_variation_id' => 123, + '__test_data_variation_map' => [ + 'variation-1' => 'abc', + 'variation-2' => 'def', + ] + ], + ] + ], + [ + $defaultProduct, + $variationProduct, + ], + [ + [1, 2] + ] + ], + 'booking' => [ + [ + 'products' => [ + [ + 'quantity' => 2, + 'id' => 1, + ], + [ + 'quantity' => 1, + 'id' => 3, + 'booking' => $bookingData, + '__test_data_is_booking' => true, + ], + ] + ], + [ + $defaultProduct, + $bookingProduct, + ], + [ + [1, 3] + ] + ], ]; return $testData; From a9e56a3ff39d15d7119d32cd8b62afd0f7f03170 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Tue, 4 Jul 2023 17:53:59 +0100 Subject: [PATCH 09/26] Remove dependency of ACDC Vault with Reference Transaction enabled --- modules/ppcp-wc-gateway/services.php | 1 - modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 215c56d40..1ace7b2c0 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -864,7 +864,6 @@ return array( $billing_agreements_endpoint = $container->get( 'api.endpoint.billing-agreements' ); if ( ! $billing_agreements_endpoint->reference_transaction_enabled() ) { unset( $fields['vault_enabled'] ); - unset( $fields['vault_enabled_dcc'] ); } /** diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index 0d2aa71e7..d28e9e026 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -241,6 +241,8 @@ class PayPalGateway extends \WC_Payment_Gateway { 'subscription_payment_method_change_admin', 'multiple_subscriptions' ); + } elseif ( $this->config->has( 'vault_enabled_dcc' ) && $this->config->get( 'vault_enabled_dcc' ) ) { + $this->supports[] = 'tokenization'; } } From c9bf899b75cdd14ecdcc26ea6a10c00d82d345ab Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 28 Jun 2023 16:34:14 +0300 Subject: [PATCH 10/26] Remove not working payment_method in root --- .../src/Endpoint/OrderEndpoint.php | 19 ++--- .../src/Entity/ApplicationContext.php | 22 ----- .../src/Entity/PaymentMethod.php | 81 ------------------- .../src/Endpoint/CreateOrderEndpoint.php | 24 +----- 4 files changed, 7 insertions(+), 139 deletions(-) delete mode 100644 modules/ppcp-api-client/src/Entity/PaymentMethod.php diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index cf22e8106..3b0954fae 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -18,7 +18,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\PatchCollection; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer; -use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentMethod; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; @@ -27,7 +26,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory; use WooCommerce\PayPalCommerce\ApiClient\Helper\ErrorResponse; use WooCommerce\PayPalCommerce\ApiClient\Repository\ApplicationContextRepository; -use WooCommerce\PayPalCommerce\ApiClient\Repository\PayPalRequestIdRepository; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet; @@ -176,13 +174,12 @@ class OrderEndpoint { /** * Creates an order. * - * @param PurchaseUnit[] $items The purchase unit items for the order. - * @param string $shipping_preference One of ApplicationContext::SHIPPING_PREFERENCE_ values. - * @param Payer|null $payer The payer off the order. - * @param PaymentToken|null $payment_token The payment token. - * @param PaymentMethod|null $payment_method The payment method. - * @param string $paypal_request_id The paypal request id. - * @param string $user_action The user action. + * @param PurchaseUnit[] $items The purchase unit items for the order. + * @param string $shipping_preference One of ApplicationContext::SHIPPING_PREFERENCE_ values. + * @param Payer|null $payer The payer off the order. + * @param PaymentToken|null $payment_token The payment token. + * @param string $paypal_request_id The paypal request id. + * @param string $user_action The user action. * * @return Order * @throws RuntimeException If the request fails. @@ -192,7 +189,6 @@ class OrderEndpoint { string $shipping_preference, Payer $payer = null, PaymentToken $payment_token = null, - PaymentMethod $payment_method = null, string $paypal_request_id = '', string $user_action = ApplicationContext::USER_ACTION_CONTINUE ): Order { @@ -221,9 +217,6 @@ class OrderEndpoint { if ( $payment_token ) { $data['payment_source']['token'] = $payment_token->to_array(); } - if ( $payment_method ) { - $data['payment_method'] = $payment_method->to_array(); - } /** * The filter can be used to modify the order creation request body data. diff --git a/modules/ppcp-api-client/src/Entity/ApplicationContext.php b/modules/ppcp-api-client/src/Entity/ApplicationContext.php index 51a3ad005..2efe5b888 100644 --- a/modules/ppcp-api-client/src/Entity/ApplicationContext.php +++ b/modules/ppcp-api-client/src/Entity/ApplicationContext.php @@ -90,13 +90,6 @@ class ApplicationContext { */ private $cancel_url; - /** - * The payment method. - * - * @var null - */ - private $payment_method; - /** * ApplicationContext constructor. * @@ -136,9 +129,6 @@ class ApplicationContext { $this->landing_page = $landing_page; $this->shipping_preference = $shipping_preference; $this->user_action = $user_action; - - // Currently we have not implemented the payment method. - $this->payment_method = null; } /** @@ -204,15 +194,6 @@ class ApplicationContext { return $this->cancel_url; } - /** - * Returns the payment method. - * - * @return PaymentMethod|null - */ - public function payment_method() { - return $this->payment_method; - } - /** * Returns the object as array. * @@ -223,9 +204,6 @@ class ApplicationContext { if ( $this->user_action() ) { $data['user_action'] = $this->user_action(); } - if ( $this->payment_method() ) { - $data['payment_method'] = $this->payment_method(); - } if ( $this->shipping_preference() ) { $data['shipping_preference'] = $this->shipping_preference(); } diff --git a/modules/ppcp-api-client/src/Entity/PaymentMethod.php b/modules/ppcp-api-client/src/Entity/PaymentMethod.php deleted file mode 100644 index 0362e0093..000000000 --- a/modules/ppcp-api-client/src/Entity/PaymentMethod.php +++ /dev/null @@ -1,81 +0,0 @@ -preferred = $preferred; - $this->selected = $selected; - } - - /** - * Returns the payer preferred value. - * - * @return string - */ - public function payee_preferred(): string { - return $this->preferred; - } - - /** - * Returns the payer selected value. - * - * @return string - */ - public function payer_selected(): string { - return $this->selected; - } - - /** - * Returns the object as array. - * - * @return array - */ - public function to_array(): array { - return array( - 'payee_preferred' => $this->payee_preferred(), - 'payer_selected' => $this->payer_selected(), - ); - } -} diff --git a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php index 686b87dd8..76e9e7f1a 100644 --- a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php @@ -19,7 +19,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\ApplicationContext; use WooCommerce\PayPalCommerce\ApiClient\Entity\Money; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer; -use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentMethod; use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; @@ -32,7 +31,6 @@ use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Subscription\FreeTrialHandlerTrait; use WooCommerce\PayPalCommerce\WcGateway\CardBillingMode; -use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; @@ -448,7 +446,6 @@ class CreateOrderEndpoint implements EndpointInterface { $shipping_preference, $payer, null, - $this->payment_method(), '', $action ); @@ -471,8 +468,7 @@ class CreateOrderEndpoint implements EndpointInterface { array( $this->purchase_unit ), $shipping_preference, $payer, - null, - $this->payment_method() + null ); } @@ -536,24 +532,6 @@ class CreateOrderEndpoint implements EndpointInterface { $this->api_endpoint->with_bn_code( $bn_code ); } - /** - * Returns the PaymentMethod object for the order. - * - * @return PaymentMethod - */ - private function payment_method() : PaymentMethod { - try { - $payee_preferred = $this->settings->has( 'payee_preferred' ) && $this->settings->get( 'payee_preferred' ) ? - PaymentMethod::PAYEE_PREFERRED_IMMEDIATE_PAYMENT_REQUIRED - : PaymentMethod::PAYEE_PREFERRED_UNRESTRICTED; - } catch ( NotFoundException $exception ) { - $payee_preferred = PaymentMethod::PAYEE_PREFERRED_UNRESTRICTED; - } - - $payment_method = new PaymentMethod( $payee_preferred ); - return $payment_method; - } - /** * Checks whether the form fields are valid. * From 249439d32747a5c09806ac69da17d64c06b854ca Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 5 Jul 2023 10:03:10 +0300 Subject: [PATCH 11/26] Send payee_preferred --- .../src/Entity/ApplicationContext.php | 41 +++++++++++++++---- .../ApplicationContextRepository.php | 13 +++--- .../ApplicationContextRepositoryTest.php | 2 + 3 files changed, 43 insertions(+), 13 deletions(-) diff --git a/modules/ppcp-api-client/src/Entity/ApplicationContext.php b/modules/ppcp-api-client/src/Entity/ApplicationContext.php index 2efe5b888..c70bbf850 100644 --- a/modules/ppcp-api-client/src/Entity/ApplicationContext.php +++ b/modules/ppcp-api-client/src/Entity/ApplicationContext.php @@ -41,6 +41,9 @@ class ApplicationContext { self::USER_ACTION_PAY_NOW, ); + const PAYMENT_METHOD_UNRESTRICTED = 'UNRESTRICTED'; + const PAYMENT_METHOD_IMMEDIATE_PAYMENT_REQUIRED = 'IMMEDIATE_PAYMENT_REQUIRED'; + /** * The brand name. * @@ -90,6 +93,13 @@ class ApplicationContext { */ private $cancel_url; + /** + * The payment method preference. + * + * @var string + */ + private $payment_method_preference; + /** * ApplicationContext constructor. * @@ -100,6 +110,7 @@ class ApplicationContext { * @param string $landing_page The landing page. * @param string $shipping_preference The shipping preference. * @param string $user_action The user action. + * @param string $payment_method_preference The payment method preference. * * @throws RuntimeException When values are not valid. */ @@ -110,7 +121,8 @@ class ApplicationContext { string $locale = '', string $landing_page = self::LANDING_PAGE_NO_PREFERENCE, string $shipping_preference = self::SHIPPING_PREFERENCE_NO_SHIPPING, - string $user_action = self::USER_ACTION_CONTINUE + string $user_action = self::USER_ACTION_CONTINUE, + string $payment_method_preference = self::PAYMENT_METHOD_IMMEDIATE_PAYMENT_REQUIRED ) { if ( ! in_array( $landing_page, self::VALID_LANDING_PAGE_VALUES, true ) ) { @@ -122,13 +134,14 @@ class ApplicationContext { if ( ! in_array( $user_action, self::VALID_USER_ACTION_VALUES, true ) ) { throw new RuntimeException( 'User action preference not correct' ); } - $this->return_url = $return_url; - $this->cancel_url = $cancel_url; - $this->brand_name = $brand_name; - $this->locale = $locale; - $this->landing_page = $landing_page; - $this->shipping_preference = $shipping_preference; - $this->user_action = $user_action; + $this->return_url = $return_url; + $this->cancel_url = $cancel_url; + $this->brand_name = $brand_name; + $this->locale = $locale; + $this->landing_page = $landing_page; + $this->shipping_preference = $shipping_preference; + $this->user_action = $user_action; + $this->payment_method_preference = $payment_method_preference; } /** @@ -194,6 +207,13 @@ class ApplicationContext { return $this->cancel_url; } + /** + * Returns the payment method preference. + */ + public function payment_method_preference(): string { + return $this->payment_method_preference; + } + /** * Returns the object as array. * @@ -222,6 +242,11 @@ class ApplicationContext { if ( $this->cancel_url() ) { $data['cancel_url'] = $this->cancel_url(); } + if ( $this->payment_method_preference ) { + $data['payment_method'] = array( + 'payee_preferred' => $this->payment_method_preference, + ); + } return $data; } } diff --git a/modules/ppcp-api-client/src/Repository/ApplicationContextRepository.php b/modules/ppcp-api-client/src/Repository/ApplicationContextRepository.php index acfb4a6cf..244b955a0 100644 --- a/modules/ppcp-api-client/src/Repository/ApplicationContextRepository.php +++ b/modules/ppcp-api-client/src/Repository/ApplicationContextRepository.php @@ -47,18 +47,21 @@ class ApplicationContextRepository { string $user_action = ApplicationContext::USER_ACTION_CONTINUE ): ApplicationContext { - $brand_name = $this->settings->has( 'brand_name' ) ? $this->settings->get( 'brand_name' ) : ''; - $locale = $this->valid_bcp47_code(); - $landingpage = $this->settings->has( 'landing_page' ) ? + $brand_name = $this->settings->has( 'brand_name' ) ? $this->settings->get( 'brand_name' ) : ''; + $locale = $this->valid_bcp47_code(); + $landingpage = $this->settings->has( 'landing_page' ) ? $this->settings->get( 'landing_page' ) : ApplicationContext::LANDING_PAGE_NO_PREFERENCE; - $context = new ApplicationContext( + $payment_preference = $this->settings->has( 'payee_preferred' ) && $this->settings->get( 'payee_preferred' ) ? + ApplicationContext::PAYMENT_METHOD_IMMEDIATE_PAYMENT_REQUIRED : ApplicationContext::PAYMENT_METHOD_UNRESTRICTED; + $context = new ApplicationContext( network_home_url( \WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) ), (string) wc_get_checkout_url(), (string) $brand_name, $locale, (string) $landingpage, $shipping_preferences, - $user_action + $user_action, + $payment_preference ); return $context; } diff --git a/tests/PHPUnit/WcGateway/Repository/ApplicationContextRepositoryTest.php b/tests/PHPUnit/WcGateway/Repository/ApplicationContextRepositoryTest.php index 7d3bf003a..1bd5b59f3 100644 --- a/tests/PHPUnit/WcGateway/Repository/ApplicationContextRepositoryTest.php +++ b/tests/PHPUnit/WcGateway/Repository/ApplicationContextRepositoryTest.php @@ -73,6 +73,7 @@ class ApplicationContextRepositoryTest extends TestCase 'container' => [ 'brand_name' => 'Acme corp.', 'landing_page' => ApplicationContext::LANDING_PAGE_BILLING, + 'payee_preferred' => '', ], 'user_locale' => 'de_DE', 'shippingPreference' => ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING, @@ -81,6 +82,7 @@ class ApplicationContextRepositoryTest extends TestCase 'brand_name' => 'Acme corp.', 'landing_page' => ApplicationContext::LANDING_PAGE_BILLING, 'shipping_preference' => ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING, + 'payment_method_preference' => ApplicationContext::PAYMENT_METHOD_UNRESTRICTED, ], ], ]; From 01717e740a31b74e1128c1b6356efa002ca10496 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 5 Jul 2023 10:48:29 +0100 Subject: [PATCH 12/26] Add ACDC vault declaring support for subscriptions --- modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index d28e9e026..7ab3f8948 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -225,6 +225,7 @@ class PayPalGateway extends \WC_Payment_Gateway { if ( ( $this->config->has( 'vault_enabled' ) && $this->config->get( 'vault_enabled' ) ) + || ( $this->config->has( 'vault_enabled_dcc' ) && $this->config->get( 'vault_enabled_dcc' ) ) || ( $this->config->has( 'subscriptions_mode' ) && $this->config->get( 'subscriptions_mode' ) === 'subscriptions_api' ) ) { array_push( @@ -241,8 +242,6 @@ class PayPalGateway extends \WC_Payment_Gateway { 'subscription_payment_method_change_admin', 'multiple_subscriptions' ); - } elseif ( $this->config->has( 'vault_enabled_dcc' ) && $this->config->get( 'vault_enabled_dcc' ) ) { - $this->supports[] = 'tokenization'; } } From 3b5a4b7f23d7ecc8cc4894df17b73d9f595616e5 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 5 Jul 2023 17:10:26 +0100 Subject: [PATCH 13/26] Add cart cleanup functionality to single page Bookable products --- modules/ppcp-api-client/src/Entity/Item.php | 43 +++++++++--- .../src/Factory/ItemFactory.php | 7 +- .../SingleProductActionHandler.js | 48 ++++++++++---- .../js/modules/Helper/CartJanitor.js | 65 +++++++++++++++++++ .../resources/js/modules/Helper/FormHelper.js | 17 +++++ 5 files changed, 156 insertions(+), 24 deletions(-) create mode 100644 modules/ppcp-button/resources/js/modules/Helper/CartJanitor.js create mode 100644 modules/ppcp-button/resources/js/modules/Helper/FormHelper.js diff --git a/modules/ppcp-api-client/src/Entity/Item.php b/modules/ppcp-api-client/src/Entity/Item.php index 374c2efcb..0f576f32c 100644 --- a/modules/ppcp-api-client/src/Entity/Item.php +++ b/modules/ppcp-api-client/src/Entity/Item.php @@ -73,6 +73,13 @@ class Item { */ protected $tax_rate; + /** + * The tax rate. + * + * @var string|null + */ + protected $cart_item_key; + /** * Item constructor. * @@ -84,6 +91,7 @@ class Item { * @param string $sku The SKU. * @param string $category The category. * @param float $tax_rate The tax rate. + * @param ?string $cart_item_key The cart key for this item. */ public function __construct( string $name, @@ -93,18 +101,20 @@ class Item { Money $tax = null, string $sku = '', string $category = 'PHYSICAL_GOODS', - float $tax_rate = 0 + float $tax_rate = 0, + string $cart_item_key = null ) { - $this->name = $name; - $this->unit_amount = $unit_amount; - $this->quantity = $quantity; - $this->description = $description; - $this->tax = $tax; - $this->sku = $sku; - $this->category = ( self::DIGITAL_GOODS === $category ) ? self::DIGITAL_GOODS : self::PHYSICAL_GOODS; - $this->category = $category; - $this->tax_rate = $tax_rate; + $this->name = $name; + $this->unit_amount = $unit_amount; + $this->quantity = $quantity; + $this->description = $description; + $this->tax = $tax; + $this->sku = $sku; + $this->category = ( self::DIGITAL_GOODS === $category ) ? self::DIGITAL_GOODS : self::PHYSICAL_GOODS; + $this->category = $category; + $this->tax_rate = $tax_rate; + $this->cart_item_key = $cart_item_key; } /** @@ -179,6 +189,15 @@ class Item { return round( (float) $this->tax_rate, 2 ); } + /** + * Returns the cart key for this item. + * + * @return string + */ + public function cart_item_key():?string { + return $this->cart_item_key; + } + /** * Returns the object as array. * @@ -202,6 +221,10 @@ class Item { $item['tax_rate'] = (string) $this->tax_rate(); } + if ( $this->cart_item_key() ) { + $item['cart_item_key'] = (string) $this->cart_item_key(); + } + return $item; } } diff --git a/modules/ppcp-api-client/src/Factory/ItemFactory.php b/modules/ppcp-api-client/src/Factory/ItemFactory.php index 0c5e9b542..ab3f0a95b 100644 --- a/modules/ppcp-api-client/src/Factory/ItemFactory.php +++ b/modules/ppcp-api-client/src/Factory/ItemFactory.php @@ -44,7 +44,8 @@ class ItemFactory { public function from_wc_cart( \WC_Cart $cart ): array { $items = array_map( function ( array $item ): Item { - $product = $item['data']; + $product = $item['data']; + $cart_item_key = $item['key'] ?? null; /** * The WooCommerce product. @@ -61,7 +62,9 @@ class ItemFactory { $this->prepare_description( $product->get_description() ), null, $product->get_sku(), - ( $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS + ( $product->is_virtual() ) ? Item::DIGITAL_GOODS : Item::PHYSICAL_GOODS, + 0, + $cart_item_key ); }, $cart->get_cart_contents() diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js index a7c8ec8ec..6d2e83706 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js @@ -3,6 +3,8 @@ import BookingProduct from "../Entity/BookingProduct"; import onApprove from '../OnApproveHandler/onApproveForContinue'; import {payerData} from "../Helper/PayerData"; import {PaymentMethods} from "../Helper/CheckoutMethodState"; +import CartJanitor from "../Helper/CartJanitor"; +import FormHelper from "../Helper/FormHelper"; class SingleProductActionHandler { @@ -16,6 +18,7 @@ class SingleProductActionHandler { this.updateCart = updateCart; this.formElement = formElement; this.errorHandler = errorHandler; + this.cartJanitor = null; } subscriptionsConfiguration() { @@ -74,29 +77,36 @@ class SingleProductActionHandler { createOrder: this.createOrder(), onApprove: onApprove(this, this.errorHandler), onError: (error) => { + this.refreshMiniCart(); + + if (this.isBookingProduct() && error.message) { + this.errorHandler.clear(); + this.errorHandler.message(error.message); + return; + } this.errorHandler.genericError(); + }, + onCancel: () => { + // Could be used for every product type, + // but only clean the cart for Booking products for now. + if (this.isBookingProduct()) { + this.cleanCart(); + } else { + this.refreshMiniCart(); + } } } } createOrder() { + this.cartJanitor = null; + let getProducts = (() => { if ( this.isBookingProduct() ) { return () => { - - const getPrefixedFields = (formElement, prefix) => { - let fields = {}; - for(const element of formElement.elements) { - if( element.name.startsWith(prefix) ) { - fields[element.name] = element.value; - } - } - return fields; - } - const id = document.querySelector('[name="add-to-cart"]').value; - return [new BookingProduct(id, 1, getPrefixedFields(this.formElement, "wc_bookings_field"))]; + return [new BookingProduct(id, 1, FormHelper.getPrefixedFields(this.formElement, "wc_bookings_field"))]; } } else if ( this.isGroupedProduct() ) { return () => { @@ -129,6 +139,8 @@ class SingleProductActionHandler { this.errorHandler.clear(); const onResolve = (purchase_units) => { + this.cartJanitor = (new CartJanitor()).addFromPurchaseUnits(purchase_units); + const payer = payerData(); const bnCode = typeof this.config.bn_codes[this.config.context] !== 'undefined' ? this.config.bn_codes[this.config.context] : ''; @@ -193,5 +205,17 @@ class SingleProductActionHandler { return !!this.formElement.querySelector('.wc-booking-product-id'); } + cleanCart() { + this.cartJanitor.removeFromCart().then(() => { + this.refreshMiniCart(); + }).catch(error => { + this.refreshMiniCart(); + }); + } + + refreshMiniCart() { + jQuery(document.body).trigger('wc_fragment_refresh'); + } + } export default SingleProductActionHandler; diff --git a/modules/ppcp-button/resources/js/modules/Helper/CartJanitor.js b/modules/ppcp-button/resources/js/modules/Helper/CartJanitor.js new file mode 100644 index 000000000..aa4c1d839 --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/CartJanitor.js @@ -0,0 +1,65 @@ +class CartJanitor { + + constructor(cartItemKeys = []) + { + this.endpoint = wc_cart_fragments_params.wc_ajax_url.toString().replace('%%endpoint%%', 'remove_from_cart'); + this.cartItemKeys = cartItemKeys; + } + + addFromPurchaseUnits(purchaseUnits) { + for (const purchaseUnit of purchaseUnits || []) { + for (const item of purchaseUnit.items || []) { + if (!item.cart_item_key) { + continue; + } + this.cartItemKeys.push(item.cart_item_key); + } + } + + return this; + } + + removeFromCart() + { + return new Promise((resolve, reject) => { + if (!this.cartItemKeys || !this.cartItemKeys.length) { + resolve(); + return; + } + + const numRequests = this.cartItemKeys.length; + let numResponses = 0; + + const tryToResolve = () => { + numResponses++; + if (numResponses >= numRequests) { + resolve(); + } + } + + for (const cartItemKey of this.cartItemKeys) { + const params = new URLSearchParams(); + params.append('cart_item_key', cartItemKey); + + if (!cartItemKey) { + tryToResolve(); + continue; + } + + fetch(this.endpoint, { + method: 'POST', + credentials: 'same-origin', + body: params + }).then(function (res) { + return res.json(); + }).then(() => { + tryToResolve(); + }).catch(() => { + tryToResolve(); + }); + } + }); + } +} + +export default CartJanitor; diff --git a/modules/ppcp-button/resources/js/modules/Helper/FormHelper.js b/modules/ppcp-button/resources/js/modules/Helper/FormHelper.js new file mode 100644 index 000000000..ff796e3eb --- /dev/null +++ b/modules/ppcp-button/resources/js/modules/Helper/FormHelper.js @@ -0,0 +1,17 @@ + +/** + * Common Form utility methods + */ +export default class FormHelper { + + static getPrefixedFields(formElement, prefix) { + let fields = {}; + for(const element of formElement.elements) { + if( element.name.startsWith(prefix) ) { + fields[element.name] = element.value; + } + } + return fields; + } + +} From 25282b9514dc5d85886c15a2c8cb15e0ec6181c1 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Mon, 10 Jul 2023 08:53:05 +0100 Subject: [PATCH 14/26] Fix code review ajustments --- modules/ppcp-api-client/src/Entity/Item.php | 4 ++-- .../ActionHandler/SingleProductActionHandler.js | 10 +++++----- .../modules/Helper/{CartJanitor.js => CartHelper.js} | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) rename modules/ppcp-button/resources/js/modules/Helper/{CartJanitor.js => CartHelper.js} (97%) diff --git a/modules/ppcp-api-client/src/Entity/Item.php b/modules/ppcp-api-client/src/Entity/Item.php index 0f576f32c..efcaa179d 100644 --- a/modules/ppcp-api-client/src/Entity/Item.php +++ b/modules/ppcp-api-client/src/Entity/Item.php @@ -74,7 +74,7 @@ class Item { protected $tax_rate; /** - * The tax rate. + * The cart item key. * * @var string|null */ @@ -192,7 +192,7 @@ class Item { /** * Returns the cart key for this item. * - * @return string + * @return string|null */ public function cart_item_key():?string { return $this->cart_item_key; diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js index 6d2e83706..5cb64febc 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js @@ -3,7 +3,7 @@ import BookingProduct from "../Entity/BookingProduct"; import onApprove from '../OnApproveHandler/onApproveForContinue'; import {payerData} from "../Helper/PayerData"; import {PaymentMethods} from "../Helper/CheckoutMethodState"; -import CartJanitor from "../Helper/CartJanitor"; +import CartHelper from "../Helper/CartHelper"; import FormHelper from "../Helper/FormHelper"; class SingleProductActionHandler { @@ -18,7 +18,7 @@ class SingleProductActionHandler { this.updateCart = updateCart; this.formElement = formElement; this.errorHandler = errorHandler; - this.cartJanitor = null; + this.cartHelper = null; } subscriptionsConfiguration() { @@ -100,7 +100,7 @@ class SingleProductActionHandler { createOrder() { - this.cartJanitor = null; + this.cartHelper = null; let getProducts = (() => { if ( this.isBookingProduct() ) { @@ -139,7 +139,7 @@ class SingleProductActionHandler { this.errorHandler.clear(); const onResolve = (purchase_units) => { - this.cartJanitor = (new CartJanitor()).addFromPurchaseUnits(purchase_units); + this.cartHelper = (new CartHelper()).addFromPurchaseUnits(purchase_units); const payer = payerData(); const bnCode = typeof this.config.bn_codes[this.config.context] !== 'undefined' ? @@ -206,7 +206,7 @@ class SingleProductActionHandler { } cleanCart() { - this.cartJanitor.removeFromCart().then(() => { + this.cartHelper.removeFromCart().then(() => { this.refreshMiniCart(); }).catch(error => { this.refreshMiniCart(); diff --git a/modules/ppcp-button/resources/js/modules/Helper/CartJanitor.js b/modules/ppcp-button/resources/js/modules/Helper/CartHelper.js similarity index 97% rename from modules/ppcp-button/resources/js/modules/Helper/CartJanitor.js rename to modules/ppcp-button/resources/js/modules/Helper/CartHelper.js index aa4c1d839..8220d7d00 100644 --- a/modules/ppcp-button/resources/js/modules/Helper/CartJanitor.js +++ b/modules/ppcp-button/resources/js/modules/Helper/CartHelper.js @@ -1,4 +1,4 @@ -class CartJanitor { +class CartHelper { constructor(cartItemKeys = []) { @@ -62,4 +62,4 @@ class CartJanitor { } } -export default CartJanitor; +export default CartHelper; From 395102ef26e375dceae9b36471d429241ab18125 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 11 Jul 2023 16:52:46 +0300 Subject: [PATCH 15/26] Update Pay Later amount in checkout when cart total changes --- .../ContextBootstrap/CheckoutBootstap.js | 32 +++++++++++++++++-- .../js/modules/Renderer/MessageRenderer.js | 22 ------------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index b426ac3ee..db1cba84f 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -15,6 +15,7 @@ class CheckoutBootstap { this.messages = messages; this.spinner = spinner; this.errorHandler = errorHandler; + this.lastAmount = this.gateway.messages.amount; this.standardOrderButtonSelector = ORDER_BUTTON_SELECTOR; @@ -36,6 +37,27 @@ class CheckoutBootstap { jQuery(document.body).on('updated_checkout', () => { this.render() this.handleButtonStatus(); + + if (this.shouldRenderMessages()) { // currently we need amount only for Pay Later + fetch( + this.gateway.ajax.cart_script_params.endpoint, + { + method: 'GET', + credentials: 'same-origin', + } + ) + .then(result => result.json()) + .then(result => { + if (! result.success) { + return; + } + + if (this.lastAmount !== result.data.amount) { + this.lastAmount = result.data.amount; + this.updateUi(); + } + }); + } }); jQuery(document.body).on('updated_checkout payment_method_selected', () => { @@ -117,8 +139,8 @@ class CheckoutBootstap { setVisible(wrapper, gatewayId === currentPaymentMethod); } - if (isPaypal && !isFreeTrial) { - this.messages.render(); + if (this.shouldRenderMessages()) { + this.messages.renderWithAmount(this.lastAmount); } if (isCard) { @@ -130,6 +152,12 @@ class CheckoutBootstap { } } + shouldRenderMessages() { + return getCurrentPaymentMethod() === PaymentMethods.PAYPAL + && !PayPalCommerceGateway.is_free_trial_cart + && this.messages.shouldRender(); + } + disableCreditCardFields() { jQuery('label[for="ppcp-credit-card-gateway-card-number"]').addClass('ppcp-credit-card-gateway-form-field-disabled') jQuery('#ppcp-credit-card-gateway-card-number').addClass('ppcp-credit-card-gateway-form-field-disabled') diff --git a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js index 80c90c0ef..7b1577aad 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/MessageRenderer.js @@ -5,28 +5,6 @@ class MessageRenderer { this.optionsFingerprint = null; } - render() { - if (! this.shouldRender()) { - return; - } - - const options = { - amount: this.config.amount, - placement: this.config.placement, - style: this.config.style - }; - - if (this.optionsEqual(options)) { - return; - } - - paypal.Messages(options).render(this.config.wrapper); - - jQuery(document.body).on('updated_cart_totals', () => { - paypal.Messages(options).render(this.config.wrapper); - }); - } - renderWithAmount(amount) { if (! this.shouldRender()) { return; From aa2889472d1637a8e4848ca15ab919d0f244013b Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Tue, 11 Jul 2023 15:47:45 +0100 Subject: [PATCH 16/26] Add subscriptions mode to WooCommerce status page. --- .../src/StatusReportModule.php | 41 ++++++++++++++++++ modules/ppcp-wc-gateway/services.php | 42 ++++++++++--------- 2 files changed, 64 insertions(+), 19 deletions(-) diff --git a/modules/ppcp-status-report/src/StatusReportModule.php b/modules/ppcp-status-report/src/StatusReportModule.php index 9e2df9599..d7a728bd7 100644 --- a/modules/ppcp-status-report/src/StatusReportModule.php +++ b/modules/ppcp-status-report/src/StatusReportModule.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\StatusReport; +use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; @@ -49,6 +50,8 @@ class StatusReportModule implements ModuleInterface { $settings = $c->get( 'wcgateway.settings' ); assert( $settings instanceof ContainerInterface ); + $subscriptions_mode_settings = $c->get( 'wcgateway.settings.fields.subscriptions_mode' ) ?: array(); + /* @var State $state The state. */ $state = $c->get( 'onboarding.state' ); @@ -61,6 +64,9 @@ class StatusReportModule implements ModuleInterface { /* @var MessagesApply $messages_apply The messages apply. */ $messages_apply = $c->get( 'button.helper.messages-apply' ); + /* @var SubscriptionHelper $subscription_helper The subscription helper class. */ + $subscription_helper = $c->get( 'subscription.helper' ); + $last_webhook_storage = $c->get( 'webhook.last-webhook-storage' ); assert( $last_webhook_storage instanceof WebhookEventStorage ); @@ -167,6 +173,20 @@ class StatusReportModule implements ModuleInterface { ), ); + // For now only show this status if PPCP_FLAG_SUBSCRIPTIONS_API is true. + if ( defined( 'PPCP_FLAG_SUBSCRIPTIONS_API' ) && PPCP_FLAG_SUBSCRIPTIONS_API ) { + $items[] = array( + 'label' => esc_html__( 'Subscriptions Mode', 'woocommerce-paypal-payments' ), + 'exported_label' => 'Subscriptions Mode', + 'description' => esc_html__( 'Whether subscriptions are active and their mode.', 'woocommerce-paypal-payments' ), + 'value' => $this->subscriptions_mode_text( + $subscription_helper->plugin_is_active(), + $settings->has( 'subscriptions_mode' ) ? (string) $settings->get( 'subscriptions_mode' ) : '', + $subscriptions_mode_settings + ), + ); + } + echo wp_kses_post( $renderer->render( esc_html__( 'WooCommerce PayPal Payments', 'woocommerce-paypal-payments' ), @@ -200,6 +220,27 @@ class StatusReportModule implements ModuleInterface { return $token->is_valid() && $current_state === $state::STATE_ONBOARDED; } + /** + * Returns the text associated with the subscriptions mode status. + * + * @param bool $is_plugin_active Indicates if the WooCommerce Subscriptions plugin is active. + * @param string $subscriptions_mode The subscriptions mode stored in settings. + * @param array $field_settings The subscriptions mode field settings. + * @return string + */ + private function subscriptions_mode_text( bool $is_plugin_active, string $subscriptions_mode, array $field_settings ): string { + if ( ! $is_plugin_active || ! $field_settings ) { + return 'Disabled'; + } + + if ( ! $subscriptions_mode ) { + $subscriptions_mode = $field_settings['default'] ?? ''; + } + + // Return the options value or if it's missing from options the settings value. + return $field_settings['options'][ $subscriptions_mode ] ?? $subscriptions_mode; + } + /** * Checks if reference transactions are enabled in account. * diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 478cc35e0..8422dbfc1 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -385,6 +385,28 @@ return array( return array_key_exists( $current_page_id, $sections ); }, + 'wcgateway.settings.fields.subscriptions_mode' => static function ( ContainerInterface $container ): array { + return array( + 'title' => __( 'Subscriptions Mode', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'desc_tip' => true, + 'description' => __( 'Utilize PayPal Vaulting for flexible subscription processing with saved payment methods, create “PayPal Subscriptions” to bill customers at regular intervals, or disable PayPal for subscription-type products.', 'woocommerce-paypal-payments' ), + 'default' => 'vaulting_api', + 'options' => array( + 'vaulting_api' => __( 'PayPal Vaulting', 'woocommerce-paypal-payments' ), + 'subscriptions_api' => __( 'PayPal Subscriptions', 'woocommerce-paypal-payments' ), + 'disable_paypal_subscriptions' => __( 'Disable PayPal for subscriptions', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'paypal', + ); + }, + 'wcgateway.settings.fields' => static function ( ContainerInterface $container ): array { $should_render_settings = $container->get( 'wcgateway.settings.should-render-settings' ); @@ -797,25 +819,7 @@ return array( 'requirements' => array(), 'gateway' => 'paypal', ), - 'subscriptions_mode' => array( - 'title' => __( 'Subscriptions Mode', 'woocommerce-paypal-payments' ), - 'type' => 'select', - 'class' => array(), - 'input_class' => array( 'wc-enhanced-select' ), - 'desc_tip' => true, - 'description' => __( 'Utilize PayPal Vaulting for flexible subscription processing with saved payment methods, create “PayPal Subscriptions” to bill customers at regular intervals, or disable PayPal for subscription-type products.', 'woocommerce-paypal-payments' ), - 'default' => 'vaulting_api', - 'options' => array( - 'vaulting_api' => __( 'PayPal Vaulting', 'woocommerce-paypal-payments' ), - 'subscriptions_api' => __( 'PayPal Subscriptions', 'woocommerce-paypal-payments' ), - 'disable_paypal_subscriptions' => __( 'Disable PayPal for subscriptions', 'woocommerce-paypal-payments' ), - ), - 'screens' => array( - State::STATE_ONBOARDED, - ), - 'requirements' => array(), - 'gateway' => 'paypal', - ), + 'subscriptions_mode' => $container->get( 'wcgateway.settings.fields.subscriptions_mode' ), 'vault_enabled' => array( 'title' => __( 'Vaulting', 'woocommerce-paypal-payments' ), 'type' => 'checkbox', From d0877d97093c1693ca0e1fd9b9952b3501915ce6 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Tue, 11 Jul 2023 15:53:47 +0100 Subject: [PATCH 17/26] Fix subscriptions mode status text when mode is 'disable_paypal_subscriptions' --- modules/ppcp-status-report/src/StatusReportModule.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ppcp-status-report/src/StatusReportModule.php b/modules/ppcp-status-report/src/StatusReportModule.php index d7a728bd7..636bb35c8 100644 --- a/modules/ppcp-status-report/src/StatusReportModule.php +++ b/modules/ppcp-status-report/src/StatusReportModule.php @@ -229,7 +229,7 @@ class StatusReportModule implements ModuleInterface { * @return string */ private function subscriptions_mode_text( bool $is_plugin_active, string $subscriptions_mode, array $field_settings ): string { - if ( ! $is_plugin_active || ! $field_settings ) { + if ( ! $is_plugin_active || ! $field_settings || $subscriptions_mode === 'disable_paypal_subscriptions' ) { return 'Disabled'; } From 512150a88937dccab9b8ece28565683a93191491 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Tue, 11 Jul 2023 18:25:37 +0100 Subject: [PATCH 18/26] Add format validation for Merchant ID field --- .../Settings/Fields/connection-tab-fields.php | 44 +++++++++++-------- .../src/Settings/SettingsRenderer.php | 22 ++++++++-- 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index 6ac84583a..9745fe033 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -245,18 +245,22 @@ return function ( ContainerInterface $container, array $fields ): array { 'gateway' => Settings::CONNECTION_TAB_ID, ), 'merchant_id_production' => array( - 'title' => __( 'Live Merchant Id', 'woocommerce-paypal-payments' ), - 'classes' => array( State::STATE_ONBOARDED === $state->production_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ), - 'type' => 'ppcp-text-input', - 'desc_tip' => true, - 'description' => __( 'The merchant id of your account ', 'woocommerce-paypal-payments' ), - 'default' => false, - 'screens' => array( + 'title' => __( 'Live Merchant Id', 'woocommerce-paypal-payments' ), + 'classes' => array( State::STATE_ONBOARDED === $state->production_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ), + 'type' => 'ppcp-text-input', + 'desc_tip' => true, + 'description' => __( 'The merchant id of your account. Should be exactly 13 alphanumeric uppercase letters.', 'woocommerce-paypal-payments' ), + 'maxlength' => 13, + 'custom_attributes' => array( + 'pattern' => '[A-Z0-9]{13}', + ), + 'default' => false, + 'screens' => array( State::STATE_START, State::STATE_ONBOARDED, ), - 'requirements' => array(), - 'gateway' => Settings::CONNECTION_TAB_ID, + 'requirements' => array(), + 'gateway' => Settings::CONNECTION_TAB_ID, ), 'client_id_production' => array( 'title' => __( 'Live Client Id', 'woocommerce-paypal-payments' ), @@ -303,18 +307,22 @@ return function ( ContainerInterface $container, array $fields ): array { 'gateway' => Settings::CONNECTION_TAB_ID, ), 'merchant_id_sandbox' => array( - 'title' => __( 'Sandbox Merchant Id', 'woocommerce-paypal-payments' ), - 'classes' => array( State::STATE_ONBOARDED === $state->sandbox_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ), - 'type' => 'ppcp-text-input', - 'desc_tip' => true, - 'description' => __( 'The merchant id of your account ', 'woocommerce-paypal-payments' ), - 'default' => false, - 'screens' => array( + 'title' => __( 'Sandbox Merchant Id', 'woocommerce-paypal-payments' ), + 'classes' => array( State::STATE_ONBOARDED === $state->sandbox_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ), + 'type' => 'ppcp-text-input', + 'desc_tip' => true, + 'description' => __( 'The merchant id of your account. Should be exactly 13 alphanumeric uppercase letters.', 'woocommerce-paypal-payments' ), + 'maxlength' => 13, + 'custom_attributes' => array( + 'pattern' => '[A-Z0-9]{13}', + ), + 'default' => false, + 'screens' => array( State::STATE_START, State::STATE_ONBOARDED, ), - 'requirements' => array(), - 'gateway' => Settings::CONNECTION_TAB_ID, + 'requirements' => array(), + 'gateway' => Settings::CONNECTION_TAB_ID, ), 'client_id_sandbox' => array( 'title' => __( 'Sandbox Client Id', 'woocommerce-paypal-payments' ), diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsRenderer.php b/modules/ppcp-wc-gateway/src/Settings/SettingsRenderer.php index 18372ef34..1dbec568b 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SettingsRenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/SettingsRenderer.php @@ -260,17 +260,33 @@ class SettingsRenderer { return $field; } + // Custom attribute handling. + $custom_attributes = array(); + $config['custom_attributes'] = array_filter( (array) $config['custom_attributes'], 'strlen' ); + + if ( $config['maxlength'] ) { + $config['custom_attributes']['maxlength'] = absint( $config['maxlength'] ); + } + + if ( ! empty( $config['custom_attributes'] ) ) { + foreach ( $config['custom_attributes'] as $attribute => $attribute_value ) { + $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"'; + } + } + $html = sprintf( '', + %s + >', esc_attr( implode( ' ', $config['class'] ) ), esc_attr( $key ), - esc_attr( $value ) + esc_attr( $value ), + implode( ' ', $custom_attributes ) ); return $html; From 7cba0edebfb628162cbc926734e7fee9ef203c47 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 12 Jul 2023 08:31:55 +0100 Subject: [PATCH 19/26] Revert ppcp-text-input settings fields to text and add autocomplete="off" --- .../Settings/Fields/connection-tab-fields.php | 16 ++++-- .../src/Settings/SettingsListener.php | 1 - .../src/Settings/SettingsRenderer.php | 49 ------------------- .../ppcp-wc-gateway/src/WCGatewayModule.php | 1 - 4 files changed, 12 insertions(+), 55 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index 9745fe033..60cde049f 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -247,12 +247,13 @@ return function ( ContainerInterface $container, array $fields ): array { 'merchant_id_production' => array( 'title' => __( 'Live Merchant Id', 'woocommerce-paypal-payments' ), 'classes' => array( State::STATE_ONBOARDED === $state->production_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ), - 'type' => 'ppcp-text-input', + 'type' => 'text', 'desc_tip' => true, 'description' => __( 'The merchant id of your account. Should be exactly 13 alphanumeric uppercase letters.', 'woocommerce-paypal-payments' ), 'maxlength' => 13, 'custom_attributes' => array( 'pattern' => '[A-Z0-9]{13}', + 'autocomplete' => 'off', ), 'default' => false, 'screens' => array( @@ -265,9 +266,12 @@ return function ( ContainerInterface $container, array $fields ): array { 'client_id_production' => array( 'title' => __( 'Live Client Id', 'woocommerce-paypal-payments' ), 'classes' => array( State::STATE_ONBOARDED === $state->production_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ), - 'type' => 'ppcp-text-input', + 'type' => 'text', 'desc_tip' => true, 'description' => __( 'The client id of your api ', 'woocommerce-paypal-payments' ), + 'custom_attributes' => array( + 'autocomplete' => 'off', + ), 'default' => false, 'screens' => array( State::STATE_START, @@ -309,12 +313,13 @@ return function ( ContainerInterface $container, array $fields ): array { 'merchant_id_sandbox' => array( 'title' => __( 'Sandbox Merchant Id', 'woocommerce-paypal-payments' ), 'classes' => array( State::STATE_ONBOARDED === $state->sandbox_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ), - 'type' => 'ppcp-text-input', + 'type' => 'text', 'desc_tip' => true, 'description' => __( 'The merchant id of your account. Should be exactly 13 alphanumeric uppercase letters.', 'woocommerce-paypal-payments' ), 'maxlength' => 13, 'custom_attributes' => array( 'pattern' => '[A-Z0-9]{13}', + 'autocomplete' => 'off' ), 'default' => false, 'screens' => array( @@ -327,9 +332,12 @@ return function ( ContainerInterface $container, array $fields ): array { 'client_id_sandbox' => array( 'title' => __( 'Sandbox Client Id', 'woocommerce-paypal-payments' ), 'classes' => array( State::STATE_ONBOARDED === $state->sandbox_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ), - 'type' => 'ppcp-text-input', + 'type' => 'text', 'desc_tip' => true, 'description' => __( 'The client id of your api ', 'woocommerce-paypal-payments' ), + 'custom_attributes' => array( + 'autocomplete' => 'off', + ), 'default' => false, 'screens' => array( State::STATE_START, diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php index 0756e73f9..ee6d1518e 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php +++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php @@ -454,7 +454,6 @@ class SettingsListener { break; case 'text': case 'number': - case 'ppcp-text-input': $settings[ $key ] = isset( $raw_data[ $key ] ) ? wp_kses_post( $raw_data[ $key ] ) : ''; break; case 'ppcp-password': diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsRenderer.php b/modules/ppcp-wc-gateway/src/Settings/SettingsRenderer.php index 1dbec568b..95046579b 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SettingsRenderer.php +++ b/modules/ppcp-wc-gateway/src/Settings/SettingsRenderer.php @@ -243,55 +243,6 @@ class SettingsRenderer { return $html; } - - /** - * Renders the text input field. - * - * @param string $field The current field HTML. - * @param string $key The current key. - * @param array $config The configuration array. - * @param string $value The current value. - * - * @return string - */ - public function render_text_input( $field, $key, $config, $value ): string { - - if ( 'ppcp-text-input' !== $config['type'] ) { - return $field; - } - - // Custom attribute handling. - $custom_attributes = array(); - $config['custom_attributes'] = array_filter( (array) $config['custom_attributes'], 'strlen' ); - - if ( $config['maxlength'] ) { - $config['custom_attributes']['maxlength'] = absint( $config['maxlength'] ); - } - - if ( ! empty( $config['custom_attributes'] ) ) { - foreach ( $config['custom_attributes'] as $attribute => $attribute_value ) { - $custom_attributes[] = esc_attr( $attribute ) . '="' . esc_attr( $attribute_value ) . '"'; - } - } - - $html = sprintf( - '', - esc_attr( implode( ' ', $config['class'] ) ), - esc_attr( $key ), - esc_attr( $value ), - implode( ' ', $custom_attributes ) - ); - - return $html; - } - /** * Renders the heading field. * diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index 0a9c4cdb0..28c610b7e 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -528,7 +528,6 @@ class WCGatewayModule implements ModuleInterface { */ $field = $renderer->render_multiselect( $field, $key, $args, $value ); $field = $renderer->render_password( $field, $key, $args, $value ); - $field = $renderer->render_text_input( $field, $key, $args, $value ); $field = $renderer->render_heading( $field, $key, $args, $value ); $field = $renderer->render_table( $field, $key, $args, $value ); return $field; From 1655a7aec965b1a2bc45d9e7a7b50cbf5b295278 Mon Sep 17 00:00:00 2001 From: Pedro Silva Date: Wed, 12 Jul 2023 08:37:26 +0100 Subject: [PATCH 20/26] Fix lint --- .../Settings/Fields/connection-tab-fields.php | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index 60cde049f..0061b5fbf 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -252,7 +252,7 @@ return function ( ContainerInterface $container, array $fields ): array { 'description' => __( 'The merchant id of your account. Should be exactly 13 alphanumeric uppercase letters.', 'woocommerce-paypal-payments' ), 'maxlength' => 13, 'custom_attributes' => array( - 'pattern' => '[A-Z0-9]{13}', + 'pattern' => '[A-Z0-9]{13}', 'autocomplete' => 'off', ), 'default' => false, @@ -264,21 +264,21 @@ return function ( ContainerInterface $container, array $fields ): array { 'gateway' => Settings::CONNECTION_TAB_ID, ), 'client_id_production' => array( - 'title' => __( 'Live Client Id', 'woocommerce-paypal-payments' ), - 'classes' => array( State::STATE_ONBOARDED === $state->production_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ), - 'type' => 'text', - 'desc_tip' => true, - 'description' => __( 'The client id of your api ', 'woocommerce-paypal-payments' ), + 'title' => __( 'Live Client Id', 'woocommerce-paypal-payments' ), + 'classes' => array( State::STATE_ONBOARDED === $state->production_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => __( 'The client id of your api ', 'woocommerce-paypal-payments' ), 'custom_attributes' => array( 'autocomplete' => 'off', ), - 'default' => false, - 'screens' => array( + 'default' => false, + 'screens' => array( State::STATE_START, State::STATE_ONBOARDED, ), - 'requirements' => array(), - 'gateway' => Settings::CONNECTION_TAB_ID, + 'requirements' => array(), + 'gateway' => Settings::CONNECTION_TAB_ID, ), 'client_secret_production' => array( 'title' => __( 'Live Secret Key', 'woocommerce-paypal-payments' ), @@ -318,8 +318,8 @@ return function ( ContainerInterface $container, array $fields ): array { 'description' => __( 'The merchant id of your account. Should be exactly 13 alphanumeric uppercase letters.', 'woocommerce-paypal-payments' ), 'maxlength' => 13, 'custom_attributes' => array( - 'pattern' => '[A-Z0-9]{13}', - 'autocomplete' => 'off' + 'pattern' => '[A-Z0-9]{13}', + 'autocomplete' => 'off', ), 'default' => false, 'screens' => array( @@ -330,21 +330,21 @@ return function ( ContainerInterface $container, array $fields ): array { 'gateway' => Settings::CONNECTION_TAB_ID, ), 'client_id_sandbox' => array( - 'title' => __( 'Sandbox Client Id', 'woocommerce-paypal-payments' ), - 'classes' => array( State::STATE_ONBOARDED === $state->sandbox_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ), - 'type' => 'text', - 'desc_tip' => true, - 'description' => __( 'The client id of your api ', 'woocommerce-paypal-payments' ), + 'title' => __( 'Sandbox Client Id', 'woocommerce-paypal-payments' ), + 'classes' => array( State::STATE_ONBOARDED === $state->sandbox_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ), + 'type' => 'text', + 'desc_tip' => true, + 'description' => __( 'The client id of your api ', 'woocommerce-paypal-payments' ), 'custom_attributes' => array( 'autocomplete' => 'off', ), - 'default' => false, - 'screens' => array( + 'default' => false, + 'screens' => array( State::STATE_START, State::STATE_ONBOARDED, ), - 'requirements' => array(), - 'gateway' => Settings::CONNECTION_TAB_ID, + 'requirements' => array(), + 'gateway' => Settings::CONNECTION_TAB_ID, ), 'client_secret_sandbox' => array( 'title' => __( 'Sandbox Secret Key', 'woocommerce-paypal-payments' ), From 639e8409c8d7ed7ba5838f539809fc374fbf3cf0 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 13 Jul 2023 14:43:14 +0300 Subject: [PATCH 21/26] Handle complex form fields when submitting checkout form Our current way of handling the checkout form via ajax does not match the WC behavior which submits them in urlencoded request instead of JSON. When it is submitted as JSON object PHP does not parse it for $_POST etc., and we do not get its handling of arrays, breaking some plugin. Now submitting the form as an urlencoded string inside JSON and parsing via `parse_str` which seems to handle it the same as $_POST. The parsing is handled in `RequestData` to avoid duplicating it in multiple places and to keep our weird sanitization here. Not sure if it's a good idea to sanitize so early, but for now keeping it like this to avoid major refactoring or introducing new vulnerabilities. --- .../js/modules/ActionHandler/CheckoutActionHandler.js | 5 ++--- .../resources/js/modules/Helper/FormValidator.js | 3 +-- modules/ppcp-button/src/Endpoint/RequestData.php | 9 +++++++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js index f528f289e..b6882ff3f 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js @@ -50,8 +50,6 @@ class CheckoutActionHandler { const formSelector = this.config.context === 'checkout' ? 'form.checkout' : 'form#order_review'; const formData = new FormData(document.querySelector(formSelector)); - // will not handle fields with multiple values (checkboxes,