Merge branch 'trunk' into PCP-991-v2-detach-vaulting-from-wc-subscriptions-support

This commit is contained in:
Emili Castells Guasch 2023-07-19 14:26:15 +02:00
commit d6c12ce29d
34 changed files with 669 additions and 379 deletions

View file

@ -164,6 +164,7 @@ const bootstrap = () => {
const cartBootstrap = new CartBootstrap(
PayPalCommerceGateway,
renderer,
messageRenderer,
errorHandler,
);

View file

@ -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, <select multiple>), but we do not care about this here
const formJsonObj = Object.fromEntries(formData.entries());
const createaccount = jQuery('#createaccount').is(":checked") ? true : false;
@ -72,7 +70,8 @@ class CheckoutActionHandler {
order_id:this.config.order_id,
payment_method: paymentMethod,
funding_source: fundingSource,
form: formJsonObj,
// send as urlencoded string to handle complex fields via PHP functions the same as normal form submit
form_encoded: new URLSearchParams(formData).toString(),
createaccount: createaccount
})
}).then(function (res) {

View file

@ -1,7 +1,10 @@
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";
import CartHelper from "../Helper/CartHelper";
import FormHelper from "../Helper/FormHelper";
class SingleProductActionHandler {
@ -15,6 +18,7 @@ class SingleProductActionHandler {
this.updateCart = updateCart;
this.formElement = formElement;
this.errorHandler = errorHandler;
this.cartHelper = null;
}
getPlanIdFromVariation = (variation) => {
@ -91,43 +95,70 @@ 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()
{
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)];
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)];
}
}
} else {
getProducts = () => {
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;
}
}
const createOrder = (data, actions) => {
})();
return (data, actions) => {
this.errorHandler.clear();
const onResolve = (purchase_units) => {
this.cartHelper = (new CartHelper()).addFromPurchaseUnits(purchase_units);
const payer = payerData();
const bnCode = typeof this.config.bn_codes[this.config.context] !== 'undefined' ?
this.config.bn_codes[this.config.context] : '';
@ -157,19 +188,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,
@ -177,7 +205,6 @@ class SingleProductActionHandler {
}
}
);
return attributes;
}
hasVariations()
@ -189,5 +216,24 @@ class SingleProductActionHandler {
{
return this.formElement.classList.contains('grouped_form');
}
isBookingProduct()
{
// detection for "woocommerce-bookings" plugin
return !!this.formElement.querySelector('.wc-booking-product-id');
}
cleanCart() {
this.cartHelper.removeFromCart().then(() => {
this.refreshMiniCart();
}).catch(error => {
this.refreshMiniCart();
});
}
refreshMiniCart() {
jQuery(document.body).trigger('wc_fragment_refresh');
}
}
export default SingleProductActionHandler;

View file

@ -3,10 +3,12 @@ import BootstrapHelper from "../Helper/BootstrapHelper";
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;
this.renderer.onButtonsInit(this.gateway.button.wrapper, () => {
this.handleButtonStatus();
@ -38,11 +40,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);
}
});
});
}
@ -76,6 +83,8 @@ class CartBootstrap {
this.renderer.render(
actionHandler.configuration()
);
this.messages.renderWithAmount(this.lastAmount);
}
}

View file

@ -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')

View file

@ -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;

View file

@ -0,0 +1,65 @@
class CartHelper {
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 CartHelper;

View file

@ -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;
}
}

View file

@ -6,7 +6,6 @@ export default class FormValidator {
async validate(form) {
const formData = new FormData(form);
const formJsonObj = Object.fromEntries(formData.entries());
const res = await fetch(this.url, {
method: 'POST',
@ -16,7 +15,7 @@ export default class FormValidator {
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.nonce,
form: formJsonObj,
form_encoded: new URLSearchParams(formData).toString(),
}),
});

View file

@ -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;