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

@ -1,6 +1,6 @@
*** Changelog ***
= 2.2.0 - TBD =
= 2.2.0 - 2023-07-17 =
* Fix - Improve handling of APM payments when buyer did not return to Checkout #1233
* Fix - Use order currency instead of shop currency on order-pay page #1363
* Fix - Do not show broken card button gateway when no checkout location #1358
@ -11,6 +11,7 @@
* Fix - Incompatibility with WooCommerce One Page Checkout (or similar use cases) in Version 2.1.0 #1473
* Fix - Prevent Repetitive Token Migration and Database Overload After 2.1.0 Update #1461
* Fix - Onboarding from connection page with CSRF parameter manipulates email and merchant id fields #1502
* Fix - Do not complete non-checkout button orders via webhooks #1513
* Enhancement - Remove feature flag requirement for express cart/checkout block integration #1483
* Enhancement - Add notice when shop currency is unsupported #1433
* Enhancement - Improve ACDC error message when empty fields #1360

View file

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

View file

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

View file

@ -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.
*
@ -91,11 +94,11 @@ class ApplicationContext {
private $cancel_url;
/**
* The payment method.
* The payment method preference.
*
* @var null
* @var string
*/
private $payment_method;
private $payment_method_preference;
/**
* ApplicationContext constructor.
@ -107,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.
*/
@ -117,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 ) ) {
@ -129,16 +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;
// Currently we have not implemented the payment method.
$this->payment_method = null;
$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;
}
/**
@ -205,12 +208,10 @@ class ApplicationContext {
}
/**
* Returns the payment method.
*
* @return PaymentMethod|null
* Returns the payment method preference.
*/
public function payment_method() {
return $this->payment_method;
public function payment_method_preference(): string {
return $this->payment_method_preference;
}
/**
@ -223,9 +224,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();
}
@ -244,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;
}
}

View file

@ -73,6 +73,13 @@ class Item {
*/
protected $tax_rate;
/**
* The cart item key.
*
* @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|null
*/
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;
}
}

View file

@ -1,81 +0,0 @@
<?php
/**
* The PaymentMethod object
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class PaymentMethod
*/
class PaymentMethod {
const PAYER_SELECTED_DEFAULT = 'PAYPAL';
const PAYEE_PREFERRED_UNRESTRICTED = 'UNRESTRICTED';
const PAYEE_PREFERRED_IMMEDIATE_PAYMENT_REQUIRED = 'IMMEDIATE_PAYMENT_REQUIRED';
/**
* The preferred value.
*
* @var string
*/
private $preferred;
/**
* The selected value.
*
* @var string
*/
private $selected;
/**
* PaymentMethod constructor.
*
* @param string $preferred The preferred value.
* @param string $selected The selected value.
*/
public function __construct(
string $preferred = self::PAYEE_PREFERRED_UNRESTRICTED,
string $selected = self::PAYER_SELECTED_DEFAULT
) {
$this->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(),
);
}
}

View file

@ -211,6 +211,15 @@ class PurchaseUnit {
return $this->custom_id;
}
/**
* Sets the custom ID.
*
* @param string $custom_id The value to set.
*/
public function set_custom_id( string $custom_id ): void {
$this->custom_id = $custom_id;
}
/**
* Returns the invoice id.
*

View file

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

View file

@ -70,6 +70,13 @@ class PurchaseUnitFactory {
*/
private $prefix;
/**
* The Soft Descriptor.
*
* @var string
*/
private $soft_descriptor;
/**
* PurchaseUnitFactory constructor.
*
@ -80,6 +87,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,
@ -88,7 +96,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;
@ -98,6 +107,7 @@ class PurchaseUnitFactory {
$this->shipping_factory = $shipping_factory;
$this->payments_factory = $payments_factory;
$this->prefix = $prefix;
$this->soft_descriptor = $soft_descriptor;
}
/**
@ -128,7 +138,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,
@ -198,7 +208,7 @@ class PurchaseUnitFactory {
}
}
$invoice_id = '';
$soft_descriptor = '';
$soft_descriptor = $this->soft_descriptor;
$purchase_unit = new PurchaseUnit(
$amount,
$items,
@ -233,7 +243,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(

View file

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

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;

View file

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

View file

@ -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.
*

View file

@ -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;
@ -166,6 +164,13 @@ class CreateOrderEndpoint implements EndpointInterface {
*/
protected $logger;
/**
* The form data, or empty if not available.
*
* @var array
*/
private $form = array();
/**
* CreateOrderEndpoint constructor.
*
@ -258,6 +263,12 @@ class CreateOrderEndpoint implements EndpointInterface {
} else {
$this->purchase_unit = $this->purchase_unit_factory->from_wc_cart( null, $this->handle_shipping_in_paypal );
// Do not allow completion by webhooks when started via non-checkout buttons,
// it is needed only for some APMs in checkout.
if ( in_array( $data['context'], array( 'product', 'cart', 'cart-block' ), true ) ) {
$this->purchase_unit->set_custom_id( '' );
}
// The cart does not have any info about payment method, so we must handle free trial here.
if ( (
in_array( $payment_method, array( CreditCardGateway::ID, CardButtonGateway::ID ), true )
@ -276,18 +287,20 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->set_bn_code( $data );
$form_fields = $data['form'] ?? null;
if ( isset( $data['form'] ) ) {
$this->form = $data['form'];
}
if ( $this->early_validation_enabled
&& is_array( $form_fields )
&& $this->form
&& 'checkout' === $data['context']
&& in_array( $payment_method, array( PayPalGateway::ID, CardButtonGateway::ID ), true )
) {
$this->validate_form( $form_fields );
$this->validate_form( $this->form );
}
if ( 'pay-now' === $data['context'] && is_array( $form_fields ) && get_option( 'woocommerce_terms_page_id', '' ) !== '' ) {
$this->validate_paynow_form( $form_fields );
if ( 'pay-now' === $data['context'] && $this->form && get_option( 'woocommerce_terms_page_id', '' ) !== '' ) {
$this->validate_paynow_form( $this->form );
}
try {
@ -448,7 +461,6 @@ class CreateOrderEndpoint implements EndpointInterface {
$shipping_preference,
$payer,
null,
$this->payment_method(),
'',
$action
);
@ -471,8 +483,7 @@ class CreateOrderEndpoint implements EndpointInterface {
array( $this->purchase_unit ),
$shipping_preference,
$payer,
null,
$this->payment_method()
null
);
}
@ -510,11 +521,9 @@ class CreateOrderEndpoint implements EndpointInterface {
$payer = $this->payer_factory->from_paypal_response( json_decode( wp_json_encode( $data['payer'] ) ) );
}
if ( ! $payer && isset( $data['form'] ) ) {
$form_fields = $data['form'];
if ( is_array( $form_fields ) && isset( $form_fields['billing_email'] ) && '' !== $form_fields['billing_email'] ) {
return $this->payer_factory->from_checkout_form( $form_fields );
if ( ! $payer && $this->form ) {
if ( isset( $this->form['billing_email'] ) && '' !== $this->form['billing_email'] ) {
return $this->payer_factory->from_checkout_form( $this->form );
}
}
@ -536,24 +545,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.
*

View file

@ -53,6 +53,11 @@ class RequestData {
}
$this->dequeue_nonce_fix();
if ( isset( $json['form_encoded'] ) ) {
$json['form'] = array();
parse_str( $json['form_encoded'], $json['form'] );
}
$sanitized = $this->sanitize( $json );
return $sanitized;
}
@ -80,6 +85,10 @@ class RequestData {
private function sanitize( array $assoc_array ): array {
$data = array();
foreach ( (array) $assoc_array as $raw_key => $raw_value ) {
if ( $raw_key === 'form_encoded' ) {
$data[ $raw_key ] = $raw_value;
continue;
}
if ( ! is_array( $raw_value ) ) {
// Not sure if it is a good idea to sanitize everything at this level,
// but should be fine for now since we do not send any HTML or multi-line texts via ajax.

View file

@ -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 || $subscriptions_mode === 'disable_paypal_subscriptions' ) {
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.
*

View file

@ -386,6 +386,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' );
@ -805,25 +827,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',
@ -872,7 +876,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'] );
}
/**
@ -976,6 +979,15 @@ return array(
return 'https://www.paypal.com/cgi-bin/webscr?cmd=_view-a-trans&id=%s';
},
'wcgateway.soft-descriptor' => static function ( ContainerInterface $container ): string {
$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 {
$sandbox_url_base = $container->get( 'wcgateway.transaction-url-sandbox' );
$live_url_base = $container->get( 'wcgateway.transaction-url-live' );

View file

@ -226,6 +226,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(

View file

@ -245,32 +245,40 @@ 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' => '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(
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' ),
'classes' => array( State::STATE_ONBOARDED === $state->production_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ),
'type' => 'ppcp-text-input',
'desc_tip' => true,
'description' => __( 'The client id of your api ', 'woocommerce-paypal-payments' ),
'default' => false,
'screens' => 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' ),
'custom_attributes' => array(
'autocomplete' => 'off',
),
'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' ),
@ -303,32 +311,40 @@ 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' => '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(
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' ),
'classes' => array( State::STATE_ONBOARDED === $state->sandbox_state() ? 'onboarded' : '', 'ppcp-always-shown-element' ),
'type' => 'ppcp-text-input',
'desc_tip' => true,
'description' => __( 'The client id of your api ', 'woocommerce-paypal-payments' ),
'default' => false,
'screens' => 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' ),
'custom_attributes' => array(
'autocomplete' => 'off',
),
'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' ),
@ -423,6 +439,20 @@ return function ( ContainerInterface $container, array $fields ): array {
'</a>'
),
),
'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. Text field, max value of 22 characters.', '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',

View file

@ -478,7 +478,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':

View file

@ -243,39 +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;
}
$html = sprintf(
'<input
type="text"
autocomplete="off"
class="%s"
name="%s"
value="%s"
>',
esc_attr( implode( ' ', $config['class'] ) ),
esc_attr( $key ),
esc_attr( $value )
);
return $html;
}
/**
* Renders the heading field.
*

View file

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

View file

@ -81,7 +81,7 @@ Follow the steps below to connect the plugin to your PayPal account:
== Changelog ==
= 2.2.0 - TBD =
= 2.2.0 - 2023-07-17 =
* Fix - Improve handling of APM payments when buyer did not return to Checkout #1233
* Fix - Use order currency instead of shop currency on order-pay page #1363
* Fix - Do not show broken card button gateway when no checkout location #1358
@ -92,6 +92,7 @@ Follow the steps below to connect the plugin to your PayPal account:
* Fix - Incompatibility with WooCommerce One Page Checkout (or similar use cases) in Version 2.1.0 #1473
* Fix - Prevent Repetitive Token Migration and Database Overload After 2.1.0 Update #1461
* Fix - Onboarding from connection page with CSRF parameter manipulates email and merchant id fields #1502
* Fix - Do not complete non-checkout button orders via webhooks #1513
* Enhancement - Remove feature flag requirement for express cart/checkout block integration #1483
* Enhancement - Add notice when shop currency is unsupported #1433
* Enhancement - Improve ACDC error message when empty fields #1360

View file

@ -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);
@ -88,6 +110,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,19 +123,42 @@ class ChangeCartEndpointTest extends TestCase
$variationProduct
->shouldReceive('get_id')
->andReturn(2);
$variationProduct
->shouldReceive('is_type')
->with('booking')
->andReturn(false);
$variationProduct
->shouldReceive('is_type')
->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,
],
]
],
@ -121,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;

View file

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