Merge trunk

This commit is contained in:
Emili Castells Guasch 2023-08-03 17:20:28 +02:00
commit 2d0726044f
51 changed files with 1480 additions and 688 deletions

View file

@ -25,6 +25,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
@ -683,6 +684,7 @@ class SmartButton implements SmartButtonInterface {
return array(
'wrapper' => '#ppcp-messages',
'is_hidden' => ! $this->is_pay_later_filter_enabled_for_location( $this->context() ),
'amount' => $amount,
'placement' => $placement,
'style' => array(
@ -826,9 +828,13 @@ class SmartButton implements SmartButtonInterface {
'has_subscriptions' => $this->has_subscriptions(),
'paypal_subscriptions_enabled' => $this->paypal_subscriptions_enabled(),
),
'redirect' => wc_get_checkout_url(),
'context' => $this->context(),
'ajax' => array(
'redirect' => wc_get_checkout_url(),
'context' => $this->context(),
'ajax' => array(
'simulate_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( SimulateCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SimulateCartEndpoint::nonce() ),
),
'change_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ),
@ -1076,8 +1082,8 @@ class SmartButton implements SmartButtonInterface {
$enable_funding = array( 'venmo' );
if ( $this->settings_status->is_pay_later_button_enabled_for_location( $context ) ||
$this->settings_status->is_pay_later_messaging_enabled_for_location( $context )
if ( $this->is_pay_later_button_enabled_for_location( $context ) ||
$this->is_pay_later_messaging_enabled_for_location( $context )
) {
$enable_funding[] = 'paylater';
} else {
@ -1366,49 +1372,112 @@ class SmartButton implements SmartButtonInterface {
);
}
/**
* Fills and returns the product context_data array to be used in filters.
*
* @param array $context_data The context data for this filter.
* @return array
*/
private function product_filter_context_data( array $context_data = array() ): array {
if ( ! isset( $context_data['product'] ) ) {
$context_data['product'] = wc_get_product();
}
if ( ! $context_data['product'] ) {
return array();
}
if ( ! isset( $context_data['order_total'] ) && ( $context_data['product'] instanceof WC_Product ) ) {
$context_data['order_total'] = (float) $context_data['product']->get_price( 'raw' );
}
return $context_data;
}
/**
* Checks if PayPal buttons/messages should be rendered for the current page.
*
* @param string|null $context The context that should be checked, use default otherwise.
*
* @param array $context_data The context data for this filter.
* @return bool
*/
protected function is_button_disabled( string $context = null ): bool {
public function is_button_disabled( string $context = null, array $context_data = array() ): bool {
if ( null === $context ) {
$context = $this->context();
}
if ( 'product' === $context ) {
$product = wc_get_product();
/**
* Allows to decide if the button should be disabled for a given product
* Allows to decide if the button should be disabled for a given product.
*/
$is_disabled = apply_filters(
return apply_filters(
'woocommerce_paypal_payments_product_buttons_disabled',
null,
$product
false,
$this->product_filter_context_data( $context_data )
);
if ( $is_disabled !== null ) {
return $is_disabled;
}
}
/**
* Allows to decide if the button should be disabled globally or on a given context
* Allows to decide if the button should be disabled globally or on a given context.
*/
$is_disabled = apply_filters(
return apply_filters(
'woocommerce_paypal_payments_buttons_disabled',
null,
false,
$context
);
}
if ( $is_disabled !== null ) {
return $is_disabled;
/**
* Checks a filter if pay_later/messages should be rendered on a given location / context.
*
* @param string $location The location.
* @param array $context_data The context data for this filter.
* @return bool
*/
private function is_pay_later_filter_enabled_for_location( string $location, array $context_data = array() ): bool {
if ( 'product' === $location ) {
/**
* Allows to decide if the button should be disabled for a given product.
*/
return ! apply_filters(
'woocommerce_paypal_payments_product_buttons_paylater_disabled',
false,
$this->product_filter_context_data( $context_data )
);
}
return false;
/**
* Allows to decide if the button should be disabled on a given context.
*/
return ! apply_filters(
'woocommerce_paypal_payments_buttons_paylater_disabled',
false,
$location
);
}
/**
* Check whether Pay Later button is enabled for a given location.
*
* @param string $location The location.
* @param array $context_data The context data for this filter.
* @return bool true if is enabled, otherwise false.
*/
public function is_pay_later_button_enabled_for_location( string $location, array $context_data = array() ): bool {
return $this->is_pay_later_filter_enabled_for_location( $location, $context_data )
&& $this->settings_status->is_pay_later_button_enabled_for_location( $location );
}
/**
* Check whether Pay Later message is enabled for a given location.
*
* @param string $location The location setting name.
* @param array $context_data The context data for this filter.
* @return bool true if is enabled, otherwise false.
*/
public function is_pay_later_messaging_enabled_for_location( string $location, array $context_data = array() ): bool {
return $this->is_pay_later_filter_enabled_for_location( $location, $context_data )
&& $this->settings_status->is_pay_later_messaging_enabled_for_location( $location );
}
/**

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
@ -120,6 +121,19 @@ class ButtonModule implements ModuleInterface {
}
);
add_action(
'wc_ajax_' . SimulateCartEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.simulate-cart' );
/**
* The Simulate Cart Endpoint.
*
* @var SimulateCartEndpoint $endpoint
*/
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . ChangeCartEndpoint::ENDPOINT,
static function () use ( $container ) {

View file

@ -0,0 +1,327 @@
<?php
/**
* Abstract class for cart Endpoints.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
/**
* Abstract Class AbstractCartEndpoint
*/
abstract class AbstractCartEndpoint implements EndpointInterface {
const ENDPOINT = '';
/**
* The current cart object.
*
* @var \WC_Cart
*/
protected $cart;
/**
* The product data store.
*
* @var \WC_Data_Store
*/
protected $product_data_store;
/**
* The request data helper.
*
* @var RequestData
*/
protected $request_data;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* The tag to be added to logs.
*
* @var string
*/
protected $logger_tag = '';
/**
* The added cart item IDs
*
* @var array
*/
private $cart_item_keys = array();
/**
* The nonce.
*
* @return string
*/
public static function nonce(): string {
return static::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
try {
return $this->handle_data();
} catch ( Exception $error ) {
$this->logger->error( 'Cart ' . $this->logger_tag . ' failed: ' . $error->getMessage() );
wp_send_json_error(
array(
'name' => is_a( $error, PayPalApiException::class ) ? $error->name() : '',
'message' => $error->getMessage(),
'code' => $error->getCode(),
'details' => is_a( $error, PayPalApiException::class ) ? $error->details() : array(),
)
);
return false;
}
}
/**
* Handles the request data.
*
* @return bool
* @throws Exception On error.
*/
abstract protected function handle_data(): bool;
/**
* Adds products to cart.
*
* @param array $products Array of products to be added to cart.
* @return bool
* @throws Exception Add to cart methods throw an exception on fail.
*/
protected function add_products( array $products ): bool {
$this->cart->empty_cart( false );
$success = true;
foreach ( $products as $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();
}
return $success;
}
/**
* Handles errors.
*
* @return void
*/
private function handle_error(): void {
$message = __(
'Something went wrong. Action aborted',
'woocommerce-paypal-payments'
);
$errors = wc_get_notices( 'error' );
if ( count( $errors ) ) {
$message = array_reduce(
$errors,
static function ( string $add, array $error ): string {
return $add . $error['notice'] . ' ';
},
''
);
wc_clear_notices();
}
wp_send_json_error(
array(
'name' => '',
'message' => $message,
'code' => 0,
'details' => array(),
)
);
}
/**
* Returns product information from request data.
*
* @return array|false
*/
protected function products_from_request() {
$data = $this->request_data->read_request( $this->nonce() );
$products = $this->products_from_data( $data );
if ( ! $products ) {
wp_send_json_error(
array(
'name' => '',
'message' => __(
'Necessary fields not defined. Action aborted.',
'woocommerce-paypal-payments'
),
'code' => 0,
'details' => array(),
)
);
return false;
}
return $products;
}
/**
* Returns product information from a data array.
*
* @param array $data The data array.
*
* @return array|null
*/
protected function products_from_data( array $data ): ?array {
$products = array();
if (
! isset( $data['products'] )
|| ! is_array( $data['products'] )
) {
return null;
}
foreach ( $data['products'] as $product ) {
if ( ! isset( $product['quantity'] ) || ! isset( $product['id'] ) ) {
return null;
}
$wc_product = wc_get_product( (int) $product['id'] );
if ( ! $wc_product ) {
return null;
}
$products[] = array(
'product' => $wc_product,
'quantity' => (int) $product['quantity'],
'variations' => $product['variations'] ?? null,
'booking' => $product['booking'] ?? null,
);
}
return $products;
}
/**
* Adds a product to the cart.
*
* @param \WC_Product $product The Product.
* @param int $quantity The Quantity.
*
* @return bool
* @throws Exception When product could not be added.
*/
private function add_product( \WC_Product $product, int $quantity ): bool {
$cart_item_key = $this->cart->add_to_cart( $product->get_id(), $quantity );
$this->cart_item_keys[] = $cart_item_key;
return false !== $cart_item_key;
}
/**
* Adds variations to the cart.
*
* @param \WC_Product $product The Product.
* @param int $quantity The Quantity.
* @param array $post_variations The variations.
*
* @return bool
* @throws Exception When product could not be added.
*/
private function add_variable_product(
\WC_Product $product,
int $quantity,
array $post_variations
): bool {
$variations = array();
foreach ( $post_variations as $key => $value ) {
$variations[ $value['name'] ] = $value['value'];
}
$variation_id = $this->product_data_store->find_matching_product_variation( $product, $variations );
// ToDo: Check stock status for variation.
$cart_item_key = $this->cart->add_to_cart(
$product->get_id(),
$quantity,
$variation_id,
$variations
);
$this->cart_item_keys[] = $cart_item_key;
return false !== $cart_item_key;
}
/**
* Adds booking 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 ),
);
$cart_item_key = $this->cart->add_to_cart( $product->get_id(), 1, 0, array(), $cart_item_data );
$this->cart_item_keys[] = $cart_item_key;
return false !== $cart_item_key;
}
/**
* Removes stored cart items from WooCommerce cart.
*
* @return void
*/
protected function remove_cart_items(): void {
foreach ( $this->cart_item_keys as $cart_item_key ) {
$this->cart->remove_cart_item( $cart_item_key );
}
}
}

View file

@ -65,11 +65,17 @@ class CartScriptParamsEndpoint implements EndpointInterface {
*/
public function handle_request(): bool {
try {
if ( is_callable( 'wc_maybe_define_constant' ) ) {
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
}
$script_data = $this->smart_button->script_data();
wp_send_json_success(
array(
'url_params' => $script_data['url_params'],
'button' => $script_data['button'],
'messages' => $script_data['messages'],
'amount' => WC()->cart->get_total( 'raw' ),
)
);

View file

@ -11,26 +11,15 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
/**
* Class ChangeCartEndpoint
*/
class ChangeCartEndpoint implements EndpointInterface {
class ChangeCartEndpoint extends AbstractCartEndpoint {
const ENDPOINT = 'ppc-change-cart';
/**
* The current cart object.
*
* @var \WC_Cart
*/
private $cart;
/**
* The current shipping object.
*
@ -38,13 +27,6 @@ class ChangeCartEndpoint implements EndpointInterface {
*/
private $shipping;
/**
* The request data helper.
*
* @var RequestData
*/
private $request_data;
/**
* The PurchaseUnit factory.
*
@ -52,20 +34,6 @@ class ChangeCartEndpoint implements EndpointInterface {
*/
private $purchase_unit_factory;
/**
* The product data store.
*
* @var \WC_Data_Store
*/
private $product_data_store;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* ChangeCartEndpoint constructor.
*
@ -91,38 +59,8 @@ class ChangeCartEndpoint implements EndpointInterface {
$this->purchase_unit_factory = $purchase_unit_factory;
$this->product_data_store = $product_data_store;
$this->logger = $logger;
}
/**
* The nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
try {
return $this->handle_data();
} catch ( Exception $error ) {
$this->logger->error( 'Cart updating failed: ' . $error->getMessage() );
wp_send_json_error(
array(
'name' => is_a( $error, PayPalApiException::class ) ? $error->name() : '',
'message' => $error->getMessage(),
'code' => $error->getCode(),
'details' => is_a( $error, PayPalApiException::class ) ? $error->details() : array(),
)
);
return false;
}
$this->logger_tag = 'updating';
}
/**
@ -131,195 +69,21 @@ class ChangeCartEndpoint implements EndpointInterface {
* @return bool
* @throws Exception On error.
*/
private function handle_data(): bool {
$data = $this->request_data->read_request( $this->nonce() );
$products = $this->products_from_data( $data );
protected function handle_data(): bool {
$products = $this->products_from_request();
if ( ! $products ) {
wp_send_json_error(
array(
'name' => '',
'message' => __(
'Necessary fields not defined. Action aborted.',
'woocommerce-paypal-payments'
),
'code' => 0,
'details' => array(),
)
);
return false;
}
$this->shipping->reset_shipping();
$this->cart->empty_cart( false );
$success = true;
foreach ( $products as $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();
return $success;
}
wp_send_json_success( $this->generate_purchase_units() );
return $success;
}
/**
* Handles errors.
*
* @return bool
*/
private function handle_error(): bool {
$message = __(
'Something went wrong. Action aborted',
'woocommerce-paypal-payments'
);
$errors = wc_get_notices( 'error' );
if ( count( $errors ) ) {
$message = array_reduce(
$errors,
static function ( string $add, array $error ): string {
return $add . $error['notice'] . ' ';
},
''
);
wc_clear_notices();
}
wp_send_json_error(
array(
'name' => '',
'message' => $message,
'code' => 0,
'details' => array(),
)
);
return true;
}
/**
* Returns product information from an data array.
*
* @param array $data The data array.
*
* @return array|null
*/
private function products_from_data( array $data ) {
$products = array();
if (
! isset( $data['products'] )
|| ! is_array( $data['products'] )
) {
return null;
}
foreach ( $data['products'] as $product ) {
if ( ! isset( $product['quantity'] ) || ! isset( $product['id'] ) ) {
return null;
}
$wc_product = wc_get_product( (int) $product['id'] );
if ( ! $wc_product ) {
return null;
}
$products[] = array(
'product' => $wc_product,
'quantity' => (int) $product['quantity'],
'variations' => $product['variations'] ?? null,
'booking' => $product['booking'] ?? null,
);
}
return $products;
}
/**
* Adds a product to the cart.
*
* @param \WC_Product $product The Product.
* @param int $quantity The Quantity.
*
* @return bool
* @throws Exception When product could not be added.
*/
private function add_product( \WC_Product $product, int $quantity ): bool {
return false !== $this->cart->add_to_cart( $product->get_id(), $quantity );
}
/**
* Adds variations to the cart.
*
* @param \WC_Product $product The Product.
* @param int $quantity The Quantity.
* @param array $post_variations The variations.
*
* @return bool
* @throws Exception When product could not be added.
*/
private function add_variable_product(
\WC_Product $product,
int $quantity,
array $post_variations
): bool {
$variations = array();
foreach ( $post_variations as $key => $value ) {
$variations[ $value['name'] ] = $value['value'];
}
$variation_id = $this->product_data_store->find_matching_product_variation( $product, $variations );
// ToDo: Check stock status for variation.
return false !== $this->cart->add_to_cart(
$product->get_id(),
$quantity,
$variation_id,
$variations
);
}
/**
* 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' ) ) {
if ( ! $this->add_products( $products ) ) {
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 );
wp_send_json_success( $this->generate_purchase_units() );
return true;
}
/**

View file

@ -0,0 +1,122 @@
<?php
/**
* Endpoint to simulate adding products to the cart.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
/**
* Class SimulateCartEndpoint
*/
class SimulateCartEndpoint extends AbstractCartEndpoint {
const ENDPOINT = 'ppc-simulate-cart';
/**
* The SmartButton.
*
* @var SmartButton
*/
private $smart_button;
/**
* ChangeCartEndpoint constructor.
*
* @param SmartButton $smart_button The SmartButton.
* @param \WC_Cart $cart The current WC cart object.
* @param RequestData $request_data The request data helper.
* @param \WC_Data_Store $product_data_store The data store for products.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
SmartButton $smart_button,
\WC_Cart $cart,
RequestData $request_data,
\WC_Data_Store $product_data_store,
LoggerInterface $logger
) {
$this->smart_button = $smart_button;
$this->cart = clone $cart;
$this->request_data = $request_data;
$this->product_data_store = $product_data_store;
$this->logger = $logger;
$this->logger_tag = 'simulation';
}
/**
* Handles the request data.
*
* @return bool
* @throws Exception On error.
*/
protected function handle_data(): bool {
$products = $this->products_from_request();
if ( ! $products ) {
return false;
}
// Set WC default cart as the clone.
// Store a reference to the real cart.
$active_cart = WC()->cart;
WC()->cart = $this->cart;
if ( ! $this->add_products( $products ) ) {
return false;
}
$this->cart->calculate_totals();
$total = (float) $this->cart->get_total( 'numeric' );
// Remove from cart because some plugins reserve resources internally when adding to cart.
$this->remove_cart_items();
// Restore cart and unset cart clone.
WC()->cart = $active_cart;
unset( $this->cart );
// Process filters.
$pay_later_enabled = true;
$pay_later_messaging_enabled = true;
$button_enabled = true;
foreach ( $products as $product ) {
$context_data = array(
'product' => $product['product'],
'order_total' => $total,
);
$pay_later_enabled = $pay_later_enabled && $this->smart_button->is_pay_later_button_enabled_for_location( 'product', $context_data );
$pay_later_messaging_enabled = $pay_later_messaging_enabled && $this->smart_button->is_pay_later_messaging_enabled_for_location( 'product', $context_data );
$button_enabled = $button_enabled && ! $this->smart_button->is_button_disabled( 'product', $context_data );
}
wp_send_json_success(
array(
'total' => $total,
'funding' => array(
'paylater' => array(
'enabled' => $pay_later_enabled,
),
),
'button' => array(
'is_disabled' => ! $button_enabled,
),
'messages' => array(
'is_hidden' => ! $pay_later_messaging_enabled,
),
)
);
return true;
}
}