From 3b5a4b7f23d7ecc8cc4894df17b73d9f595616e5 Mon Sep 17 00:00:00 2001
From: Pedro Silva
Date: Wed, 5 Jul 2023 17:10:26 +0100
Subject: [PATCH] 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;
+ }
+
+}