mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-09-06 09:08:09 +08:00
Merge branch 'trunk' into PCP-991-v2-detach-vaulting-from-wc-subscriptions-support
This commit is contained in:
commit
d6c12ce29d
34 changed files with 669 additions and 379 deletions
|
@ -164,6 +164,7 @@ const bootstrap = () => {
|
|||
const cartBootstrap = new CartBootstrap(
|
||||
PayPalCommerceGateway,
|
||||
renderer,
|
||||
messageRenderer,
|
||||
errorHandler,
|
||||
);
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue