Merge branch 'trunk' into PCP-190-override-language-used-to-display-PayPal-buttons

# Conflicts:
#	modules/ppcp-button/services.php
#	modules/ppcp-button/src/Assets/SmartButton.php
#	modules/ppcp-wc-gateway/services.php
This commit is contained in:
Narek Zakarian 2023-10-31 15:19:44 +04:00
commit e3fbadf419
No known key found for this signature in database
GPG key ID: 07AFD7E7A9C164A7
465 changed files with 41582 additions and 6679 deletions

View file

@ -0,0 +1,47 @@
<?php
/**
* The interface for the button asset renderer.
*
* @package WooCommerce\PayPalCommerce\Button\Assets
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Assets;
/**
* Interface ButtonInterface
*/
interface ButtonInterface {
/**
* Initializes the button.
*/
public function initialize(): void;
/**
* Indicates if the button is enabled.
*
* @return bool
*/
public function is_enabled(): bool;
/**
* Renders the necessary HTML.
*
* @return bool
*/
public function render(): bool;
/**
* Enqueues scripts/styles.
*/
public function enqueue(): void;
/**
* The configuration for the smart buttons.
*
* @return array
*/
public function script_data(): array;
}

View file

@ -24,12 +24,16 @@ class DisabledSmartButton implements SmartButtonInterface {
}
/**
* Enqueues necessary scripts.
*
* @return bool
* Whether the scripts should be loaded.
*/
public function enqueue(): bool {
return true;
public function should_load_ppcp_script(): bool {
return false;
}
/**
* Enqueues necessary scripts.
*/
public function enqueue(): void {
}
/**
@ -41,4 +45,13 @@ class DisabledSmartButton implements SmartButtonInterface {
return false;
}
/**
* The configuration for the smart buttons.
*
* @return array
*/
public function script_data(): array {
return array();
}
}

File diff suppressed because it is too large Load diff

View file

@ -22,16 +22,19 @@ interface SmartButtonInterface {
public function render_wrapper(): bool;
/**
* Enqueues the necessary scripts.
*
* @return bool
* Whether any of our scripts (for DCC or product, mini-cart, non-block cart/checkout) should be loaded.
*/
public function enqueue(): bool;
public function should_load_ppcp_script(): bool;
/**
* Whether the running installation could save vault tokens or not.
*
* @return bool
* Enqueues our scripts/styles (for DCC and product, mini-cart and non-block cart/checkout)
*/
public function can_save_vault_token(): bool;
public function enqueue(): void;
/**
* The configuration for the smart buttons.
*
* @return array
*/
public function script_data(): array;
}

View file

@ -9,6 +9,11 @@ declare(strict_types=1);
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;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
@ -60,14 +65,12 @@ class ButtonModule implements ModuleInterface {
add_action(
'wp_enqueue_scripts',
static function () use ( $c ) {
$smart_button = $c->get( 'button.smart-button' );
/**
* The Smart Button.
*
* @var SmartButtonInterface $smart_button
*/
$smart_button->enqueue();
assert( $smart_button instanceof SmartButtonInterface );
if ( $smart_button->should_load_ppcp_script() ) {
$smart_button->enqueue();
}
}
);
@ -95,7 +98,7 @@ class ButtonModule implements ModuleInterface {
*
* @param ContainerInterface $container The Container.
*/
private function register_ajax_endpoints( ContainerInterface $container ) {
private function register_ajax_endpoints( ContainerInterface $container ): void {
add_action(
'wc_ajax_' . DataClientIdEndpoint::ENDPOINT,
static function () use ( $container ) {
@ -118,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 ) {
@ -144,6 +160,16 @@ class ButtonModule implements ModuleInterface {
}
);
add_action(
'wc_ajax_' . ApproveSubscriptionEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.approve-subscription' );
assert( $endpoint instanceof ApproveSubscriptionEndpoint );
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . CreateOrderEndpoint::ENDPOINT,
static function () use ( $container ) {
@ -156,6 +182,34 @@ class ButtonModule implements ModuleInterface {
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . SaveCheckoutFormEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.save-checkout-form' );
assert( $endpoint instanceof SaveCheckoutFormEndpoint );
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . ValidateCheckoutEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.validate-checkout' );
assert( $endpoint instanceof ValidateCheckoutEndpoint );
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . CartScriptParamsEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.cart-script-params' );
assert( $endpoint instanceof CartScriptParamsEndpoint );
$endpoint->handle_request();
}
);
}
/**

View file

@ -0,0 +1,186 @@
<?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;
use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper;
/**
* Abstract Class AbstractCartEndpoint
*/
abstract class AbstractCartEndpoint implements EndpointInterface {
const ENDPOINT = '';
/**
* The current cart object.
*
* @var \WC_Cart
*/
protected $cart;
/**
* The cart products helper.
*
* @var CartProductsHelper
*/
protected $cart_products;
/**
* 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 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 );
try {
$this->cart_products->add_products( $products );
} catch ( Exception $e ) {
$this->handle_error();
}
return true;
}
/**
* Handles errors.
*
* @param bool $send_response If this error handling should return the response.
* @return void
*/
protected function handle_error( bool $send_response = true ): 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();
}
if ( $send_response ) {
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->cart_products->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;
}
/**
* Removes stored cart items from WooCommerce cart.
*
* @return void
*/
protected function remove_cart_items(): void {
$this->cart_products->remove_cart_items();
}
}

View file

@ -180,7 +180,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
);
}
$this->session_handler->replace_order( $order );
wp_send_json_success( $order );
wp_send_json_success();
}
if ( $this->order_helper->contains_physical_goods( $order ) && ! $order->status()->is( OrderStatus::APPROVED ) && ! $order->status()->is( OrderStatus::CREATED ) ) {
@ -198,7 +198,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
$this->session_handler->replace_funding_source( $funding_source );
$this->session_handler->replace_order( $order );
wp_send_json_success( $order );
wp_send_json_success();
return true;
} catch ( Exception $error ) {
$this->logger->error( 'Order approve failed: ' . $error->getMessage() );

View file

@ -0,0 +1,94 @@
<?php
/**
* Endpoint to handle PayPal Subscription created.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
/**
* Class ApproveSubscriptionEndpoint
*/
class ApproveSubscriptionEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-approve-subscription';
/**
* The request data helper.
*
* @var RequestData
*/
private $request_data;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The session handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* ApproveSubscriptionEndpoint constructor.
*
* @param RequestData $request_data The request data helper.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
*/
public function __construct(
RequestData $request_data,
OrderEndpoint $order_endpoint,
SessionHandler $session_handler
) {
$this->request_data = $request_data;
$this->order_endpoint = $order_endpoint;
$this->session_handler = $session_handler;
}
/**
* The nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
* @throws RuntimeException When order not found or handling failed.
*/
public function handle_request(): bool {
$data = $this->request_data->read_request( $this->nonce() );
if ( ! isset( $data['order_id'] ) ) {
throw new RuntimeException(
__( 'No order id given', 'woocommerce-paypal-payments' )
);
}
$order = $this->order_endpoint->order( $data['order_id'] );
$this->session_handler->replace_order( $order );
if ( isset( $data['subscription_id'] ) ) {
WC()->session->set( 'ppcp_subscription_id', $data['subscription_id'] );
}
wp_send_json_success();
return true;
}
}

View file

@ -0,0 +1,104 @@
<?php
/**
* The endpoint for returning the PayPal SDK Script parameters for the current cart.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
/**
* Class CartScriptParamsEndpoint.
*/
class CartScriptParamsEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-cart-script-params';
/**
* The SmartButton.
*
* @var SmartButton
*/
private $smart_button;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* CartScriptParamsEndpoint constructor.
*
* @param SmartButton $smart_button he SmartButton.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
SmartButton $smart_button,
LoggerInterface $logger
) {
$this->smart_button = $smart_button;
$this->logger = $logger;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
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();
$total = (float) WC()->cart->get_total( 'numeric' );
// Shop settings.
$base_location = wc_get_base_location();
$shop_country_code = $base_location['country'] ?? '';
$currency_code = get_woocommerce_currency();
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' ),
'total' => $total,
'total_str' => ( new Money( $total, $currency_code ) )->value_str(),
'currency_code' => $currency_code,
'country_code' => $shop_country_code,
)
);
return true;
} catch ( Throwable $error ) {
$this->logger->error( "CartScriptParamsEndpoint execution failed. {$error->getMessage()} {$error->getFile()}:{$error->getLine()}" );
wp_send_json_error();
return false;
}
}
}

View file

@ -11,26 +11,16 @@ 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;
use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper;
/**
* 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 +28,6 @@ class ChangeCartEndpoint implements EndpointInterface {
*/
private $shipping;
/**
* The request data helper.
*
* @var RequestData
*/
private $request_data;
/**
* The PurchaseUnit factory.
*
@ -52,20 +35,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.
*
@ -73,7 +42,7 @@ class ChangeCartEndpoint implements EndpointInterface {
* @param \WC_Shipping $shipping The current WC shipping object.
* @param RequestData $request_data The request data helper.
* @param PurchaseUnitFactory $purchase_unit_factory The PurchaseUnit factory.
* @param \WC_Data_Store $product_data_store The data store for products.
* @param CartProductsHelper $cart_products The cart products helper.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
@ -81,7 +50,7 @@ class ChangeCartEndpoint implements EndpointInterface {
\WC_Shipping $shipping,
RequestData $request_data,
PurchaseUnitFactory $purchase_unit_factory,
\WC_Data_Store $product_data_store,
CartProductsHelper $cart_products,
LoggerInterface $logger
) {
@ -89,40 +58,10 @@ class ChangeCartEndpoint implements EndpointInterface {
$this->shipping = $shipping;
$this->request_data = $request_data;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->product_data_store = $product_data_store;
$this->cart_products = $cart_products;
$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,161 +70,25 @@ 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 {
$this->cart_products->set_cart( $this->cart );
$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 ) {
$success = $success && ( ! $product['product']->is_type( 'variable' ) ) ?
$this->add_product( $product['product'], $product['quantity'] )
: $this->add_variable_product(
$product['product'],
$product['quantity'],
$product['variations']
);
}
if ( ! $success ) {
$this->handle_error();
return $success;
if ( ! $this->add_products( $products ) ) {
return false;
}
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' => isset( $product['variations'] ) ? $product['variations'] : 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
);
}
/**
* Based on the cart contents, the purchase units are created.
*

View file

@ -19,20 +19,19 @@ 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;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
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;
@ -138,6 +137,27 @@ class CreateOrderEndpoint implements EndpointInterface {
*/
protected $early_validation_enabled;
/**
* The contexts that should have the Pay Now button.
*
* @var string[]
*/
private $pay_now_contexts;
/**
* If true, the shipping methods are sent to PayPal allowing the customer to select it inside the popup.
*
* @var bool
*/
private $handle_shipping_in_paypal;
/**
* The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back.
*
* @var string[]
*/
private $funding_sources_without_redirect;
/**
* The logger.
*
@ -145,6 +165,13 @@ class CreateOrderEndpoint implements EndpointInterface {
*/
protected $logger;
/**
* The form data, or empty if not available.
*
* @var array
*/
private $form = array();
/**
* CreateOrderEndpoint constructor.
*
@ -159,6 +186,9 @@ class CreateOrderEndpoint implements EndpointInterface {
* @param bool $registration_needed Whether a new user must be registered during checkout.
* @param string $card_billing_data_mode The value of card_billing_data_mode from the settings.
* @param bool $early_validation_enabled Whether to execute WC validation of the checkout form.
* @param string[] $pay_now_contexts The contexts that should have the Pay Now button.
* @param bool $handle_shipping_in_paypal If true, the shipping methods are sent to PayPal allowing the customer to select it inside the popup.
* @param string[] $funding_sources_without_redirect The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
@ -173,21 +203,27 @@ class CreateOrderEndpoint implements EndpointInterface {
bool $registration_needed,
string $card_billing_data_mode,
bool $early_validation_enabled,
array $pay_now_contexts,
bool $handle_shipping_in_paypal,
array $funding_sources_without_redirect,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->shipping_preference_factory = $shipping_preference_factory;
$this->api_endpoint = $order_endpoint;
$this->payer_factory = $payer_factory;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->early_order_handler = $early_order_handler;
$this->registration_needed = $registration_needed;
$this->card_billing_data_mode = $card_billing_data_mode;
$this->early_validation_enabled = $early_validation_enabled;
$this->logger = $logger;
$this->request_data = $request_data;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->shipping_preference_factory = $shipping_preference_factory;
$this->api_endpoint = $order_endpoint;
$this->payer_factory = $payer_factory;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->early_order_handler = $early_order_handler;
$this->registration_needed = $registration_needed;
$this->card_billing_data_mode = $card_billing_data_mode;
$this->early_validation_enabled = $early_validation_enabled;
$this->pay_now_contexts = $pay_now_contexts;
$this->handle_shipping_in_paypal = $handle_shipping_in_paypal;
$this->funding_sources_without_redirect = $funding_sources_without_redirect;
$this->logger = $logger;
}
/**
@ -226,7 +262,13 @@ class CreateOrderEndpoint implements EndpointInterface {
}
$this->purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
} else {
$this->purchase_unit = $this->purchase_unit_factory->from_wc_cart();
$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 ( (
@ -246,18 +288,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 {
@ -268,11 +312,16 @@ class CreateOrderEndpoint implements EndpointInterface {
}
if ( 'checkout' === $data['context'] ) {
if ( $payment_method === PayPalGateway::ID && ! in_array( $funding_source, $this->funding_sources_without_redirect, true ) ) {
$this->session_handler->replace_order( $order );
$this->session_handler->replace_funding_source( $funding_source );
}
if (
! $this->early_order_handler->should_create_early_order()
|| $this->registration_needed
|| isset( $data['createaccount'] ) && '1' === $data['createaccount'] ) {
wp_send_json_success( $order->to_array() );
wp_send_json_success( $this->make_response( $order ) );
}
$this->early_order_handler->register_for_order( $order );
@ -282,18 +331,23 @@ class CreateOrderEndpoint implements EndpointInterface {
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
$wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() );
$wc_order->save_meta_data();
do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order );
}
wp_send_json_success( $order->to_array() );
wp_send_json_success( $this->make_response( $order ) );
return true;
} catch ( ValidationException $error ) {
wp_send_json_error(
array(
'message' => $error->getMessage(),
'errors' => $error->errors(),
)
$response = array(
'message' => $error->getMessage(),
'errors' => $error->errors(),
'refresh' => isset( WC()->session->refresh_totals ),
);
unset( WC()->session->refresh_totals );
wp_send_json_error( $response );
} catch ( \RuntimeException $error ) {
$this->logger->error( 'Order creation failed: ' . $error->getMessage() );
@ -339,7 +393,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* during the "onApprove"-JS callback or the webhook listener.
*/
if ( ! $this->early_order_handler->should_create_early_order() ) {
wp_send_json_success( $order->to_array() );
wp_send_json_success( $this->make_response( $order ) );
}
$this->early_order_handler->register_for_order( $order );
return $data;
@ -382,6 +436,9 @@ class CreateOrderEndpoint implements EndpointInterface {
$funding_source
);
$action = in_array( $this->parsed_request_data['context'], $this->pay_now_contexts, true ) ?
ApplicationContext::USER_ACTION_PAY_NOW : ApplicationContext::USER_ACTION_CONTINUE;
if ( 'card' === $funding_source ) {
if ( CardBillingMode::MINIMAL_INPUT === $this->card_billing_data_mode ) {
if ( ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS === $shipping_preference ) {
@ -407,7 +464,8 @@ class CreateOrderEndpoint implements EndpointInterface {
$shipping_preference,
$payer,
null,
$this->payment_method()
'',
$action
);
} catch ( PayPalApiException $exception ) {
// Looks like currently there is no proper way to validate the shipping address for PayPal,
@ -428,8 +486,7 @@ class CreateOrderEndpoint implements EndpointInterface {
array( $this->purchase_unit ),
$shipping_preference,
$payer,
null,
$this->payment_method()
null
);
}
@ -467,11 +524,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 );
}
}
@ -493,24 +548,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.
*
@ -541,4 +578,17 @@ class CreateOrderEndpoint implements EndpointInterface {
);
}
}
/**
* Returns the response data for success response.
*
* @param Order $order The PayPal order.
* @return array
*/
private function make_response( Order $order ): array {
return array(
'id' => $order->id(),
'custom_id' => $order->purchase_units()[0]->custom_id(),
);
}
}

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

@ -0,0 +1,94 @@
<?php
/**
* Saves the form data to the WC customer and session.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
/**
* Class SaveCheckoutFormEndpoint
*/
class SaveCheckoutFormEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-save-checkout-form';
/**
* The Request Data Helper.
*
* @var RequestData
*/
private $request_data;
/**
* The checkout form saver.
*
* @var CheckoutFormSaver
*/
private $checkout_form_saver;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* SaveCheckoutFormEndpoint constructor.
*
* @param RequestData $request_data The Request Data Helper.
* @param CheckoutFormSaver $checkout_form_saver The checkout form saver.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
CheckoutFormSaver $checkout_form_saver,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->checkout_form_saver = $checkout_form_saver;
$this->logger = $logger;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
$this->checkout_form_saver->save( $data['form'] );
wp_send_json_success();
return true;
} catch ( Exception $error ) {
$this->logger->error( 'Checkout form saving failed: ' . $error->getMessage() );
wp_send_json_error(
array(
'message' => $error->getMessage(),
)
);
return false;
}
}
}

View file

@ -0,0 +1,178 @@
<?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\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper;
/**
* Class SimulateCartEndpoint
*/
class SimulateCartEndpoint extends AbstractCartEndpoint {
const ENDPOINT = 'ppc-simulate-cart';
/**
* The SmartButton.
*
* @var SmartButton
*/
private $smart_button;
/**
* The WooCommerce real active cart.
*
* @var \WC_Cart|null
*/
private $real_cart = null;
/**
* 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 CartProductsHelper $cart_products The cart products helper.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
SmartButton $smart_button,
\WC_Cart $cart,
RequestData $request_data,
CartProductsHelper $cart_products,
LoggerInterface $logger
) {
$this->smart_button = $smart_button;
$this->cart = clone $cart;
$this->request_data = $request_data;
$this->cart_products = $cart_products;
$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;
}
$this->replace_real_cart();
$this->add_products( $products );
$this->cart->calculate_totals();
$total = (float) $this->cart->get_total( 'numeric' );
$this->restore_real_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 );
}
// Shop settings.
$base_location = wc_get_base_location();
$shop_country_code = $base_location['country'];
$currency_code = get_woocommerce_currency();
wp_send_json_success(
array(
'total' => $total,
'total_str' => ( new Money( $total, $currency_code ) )->value_str(),
'currency_code' => $currency_code,
'country_code' => $shop_country_code,
'funding' => array(
'paylater' => array(
'enabled' => $pay_later_enabled,
),
),
'button' => array(
'is_disabled' => ! $button_enabled,
),
'messages' => array(
'is_hidden' => ! $pay_later_messaging_enabled,
),
)
);
return true;
}
/**
* Handles errors.
*
* @param bool $send_response If this error handling should return the response.
* @return void
*
* phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
*/
protected function handle_error( bool $send_response = false ): void {
parent::handle_error( $send_response );
}
/**
* Replaces the real cart with the clone.
*
* @return void
*/
private function replace_real_cart() {
// Set WC default cart as the clone.
// Store a reference to the real cart.
$this->real_cart = WC()->cart;
WC()->cart = $this->cart;
$this->cart_products->set_cart( $this->cart );
}
/**
* Restores the real cart.
* Currently, unsets the WC cart to prevent race conditions arising from it being persisted.
*
* @return void
*/
private function restore_real_cart() {
// Remove from cart because some plugins reserve resources internally when adding to cart.
$this->remove_cart_items();
if ( apply_filters( 'woocommerce_paypal_payments_simulate_cart_prevent_updates', true ) ) {
// Removes shutdown actions to prevent persisting session, transients and save cookies.
remove_all_actions( 'shutdown' );
unset( WC()->cart );
} else {
// Restores cart, may lead to race conditions.
if ( null !== $this->real_cart ) {
WC()->cart = $this->real_cart;
}
}
unset( $this->cart );
}
}

View file

@ -0,0 +1,110 @@
<?php
/**
* The endpoint for validating the checkout form.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Psr\Log\LoggerInterface;
use Throwable;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
/**
* Class ValidateCheckoutEndpoint.
*/
class ValidateCheckoutEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-validate-checkout';
/**
* The Request Data Helper.
*
* @var RequestData
*/
private $request_data;
/**
* The CheckoutFormValidator.
*
* @var CheckoutFormValidator
*/
private $checkout_form_validator;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* ValidateCheckoutEndpoint constructor.
*
* @param RequestData $request_data The Request Data Helper.
* @param CheckoutFormValidator $checkout_form_validator The CheckoutFormValidator.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
CheckoutFormValidator $checkout_form_validator,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->checkout_form_validator = $checkout_form_validator;
$this->logger = $logger;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
$form_fields = $data['form'];
$this->checkout_form_validator->validate( $form_fields );
wp_send_json_success();
return true;
} catch ( ValidationException $exception ) {
$response = array(
'message' => $exception->getMessage(),
'errors' => $exception->errors(),
'refresh' => isset( WC()->session->refresh_totals ),
);
unset( WC()->session->refresh_totals );
wp_send_json_error( $response );
return false;
} catch ( Throwable $error ) {
$this->logger->error( "Form validation execution failed. {$error->getMessage()} {$error->getFile()}:{$error->getLine()}" );
wp_send_json_error(
array(
'message' => $error->getMessage(),
)
);
return false;
}
}
}

View file

@ -0,0 +1,285 @@
<?php
/**
* Handles the adding of products to WooCommerce cart.
*
* @package WooCommerce\PayPalCommerce\Button\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper;
use Exception;
use WC_Cart;
use WC_Data_Store;
/**
* Class CartProductsHelper
*/
class CartProductsHelper {
/**
* The cart
*
* @var ?WC_Cart
*/
private $cart;
/**
* The product data store.
*
* @var WC_Data_Store
*/
protected $product_data_store;
/**
* The added cart item IDs
*
* @var array
*/
private $cart_item_keys = array();
/**
* CheckoutFormSaver constructor.
*
* @param WC_Data_Store $product_data_store The data store for products.
*/
public function __construct(
WC_Data_Store $product_data_store
) {
$this->product_data_store = $product_data_store;
}
/**
* Sets a new cart instance.
*
* @param WC_Cart $cart The cart.
* @return void
*/
public function set_cart( WC_Cart $cart ): void {
$this->cart = $cart;
}
/**
* Returns products information from a data array.
*
* @param array $data The data array.
*
* @return array|null
*/
public function products_from_data( array $data ): ?array {
$products = array();
if (
! isset( $data['products'] )
|| ! is_array( $data['products'] )
) {
return null;
}
foreach ( $data['products'] as $product ) {
$product = $this->product_from_data( $product );
if ( $product ) {
$products[] = $product;
}
}
return $products;
}
/**
* Returns product information from a data array.
*
* @param array $product The product data array, usually provided by the product page form.
* @return array|null
*/
public function product_from_data( array $product ): ?array {
if ( ! isset( $product['quantity'] ) || ! isset( $product['id'] ) ) {
return null;
}
$wc_product = wc_get_product( (int) $product['id'] );
if ( ! $wc_product ) {
return null;
}
return array(
'product' => $wc_product,
'quantity' => (int) $product['quantity'],
'variations' => $product['variations'] ?? null,
'booking' => $product['booking'] ?? null,
'extra' => $product['extra'] ?? null,
);
}
/**
* 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.
*/
public function add_products( array $products ): bool {
$success = true;
foreach ( $products as $product ) {
// Add extras to POST, they are usually added by custom plugins.
if ( $product['extra'] && is_array( $product['extra'] ) ) {
// Handle cases like field[].
$query = http_build_query( $product['extra'] );
parse_str( $query, $extra );
foreach ( $extra as $key => $value ) {
$_POST[ $key ] = $value;
}
}
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 ) {
throw new Exception( 'Error adding products to cart.' );
}
return true;
}
/**
* 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.
*/
public function add_product( \WC_Product $product, int $quantity ): bool {
if ( ! $this->cart ) {
throw new Exception( 'Cart not set.' );
}
$cart_item_key = $this->cart->add_to_cart( $product->get_id(), $quantity );
if ( $cart_item_key ) {
$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.
*/
public function add_variable_product(
\WC_Product $product,
int $quantity,
array $post_variations
): bool {
if ( ! $this->cart ) {
throw new Exception( 'Cart not set.' );
}
$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
);
if ( $cart_item_key ) {
$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.
*/
public function add_booking_product(
\WC_Product $product,
array $data
): bool {
if ( ! $this->cart ) {
throw new Exception( 'Cart not set.' );
}
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 );
if ( $cart_item_key ) {
$this->cart_item_keys[] = $cart_item_key;
}
return false !== $cart_item_key;
}
/**
* Removes stored cart items from WooCommerce cart.
*
* @return void
* @throws Exception Throws if there's a failure removing the cart items.
*/
public function remove_cart_items(): void {
if ( ! $this->cart ) {
throw new Exception( 'Cart not set.' );
}
foreach ( $this->cart_item_keys as $cart_item_key ) {
if ( ! $cart_item_key ) {
continue;
}
$this->cart->remove_cart_item( $cart_item_key );
}
}
/**
* Returns the cart item keys of the items added to cart.
*
* @return array
*/
public function cart_item_keys(): array {
return $this->cart_item_keys;
}
}

View file

@ -0,0 +1,53 @@
<?php
/**
* Saves the form data to the WC customer and session.
*
* @package WooCommerce\PayPalCommerce\Button\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper;
use WC_Checkout;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
/**
* Class CheckoutFormSaver
*/
class CheckoutFormSaver extends WC_Checkout {
/**
* The Session handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* CheckoutFormSaver constructor.
*
* @param SessionHandler $session_handler The session handler.
*/
public function __construct(
SessionHandler $session_handler
) {
$this->session_handler = $session_handler;
}
/**
* Saves the form data to the WC customer and session.
*
* @param array $data The form data.
* @return void
*/
public function save( array $data ) {
foreach ( $data as $key => $value ) {
$_POST[ $key ] = $value;
}
$data = $this->get_posted_data();
$this->update_session( $data );
$this->session_handler->replace_checkout_form( $data );
}
}

View file

@ -0,0 +1,128 @@
<?php
/**
* Helper trait for context.
*
* @package WooCommerce\PayPalCommerce\Button\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
trait ContextTrait {
/**
* Checks WC is_checkout() + WC checkout ajax requests.
*/
private function is_checkout(): bool {
if ( is_checkout() ) {
return true;
}
/**
* The filter returning whether to detect WC checkout ajax requests.
*/
if ( apply_filters( 'ppcp_check_ajax_checkout', true ) ) {
// phpcs:ignore WordPress.Security
$wc_ajax = $_GET['wc-ajax'] ?? '';
if ( in_array( $wc_ajax, array( 'update_order_review' ), true ) ) {
return true;
}
}
return false;
}
/**
* The current context.
*
* @return string
*/
protected function context(): string {
if ( is_product() || wc_post_content_has_shortcode( 'product_page' ) ) {
// Do this check here instead of reordering outside conditions.
// In order to have more control over the context.
if ( $this->is_checkout() && ! $this->is_paypal_continuation() ) {
return 'checkout';
}
return 'product';
}
// has_block may not work if called too early, such as during the block registration.
if ( has_block( 'woocommerce/cart' ) ) {
return 'cart-block';
}
if ( is_cart() ) {
return 'cart';
}
if ( is_checkout_pay_page() ) {
return 'pay-now';
}
if ( has_block( 'woocommerce/checkout' ) ) {
return 'checkout-block';
}
if ( $this->is_checkout() && ! $this->is_paypal_continuation() ) {
return 'checkout';
}
return 'mini-cart';
}
/**
* The current location.
*
* @return string
*/
protected function location(): string {
$context = $this->context();
if ( $context !== 'mini-cart' ) {
return $context;
}
if ( is_shop() || is_product_category() ) {
return 'shop';
}
if ( is_front_page() ) {
return 'home';
}
return '';
}
/**
* Checks if PayPal payment was already initiated (on the product or cart pages).
*
* @return bool
*/
private function is_paypal_continuation(): bool {
$order = $this->session_handler->order();
if ( ! $order ) {
return false;
}
if ( ! $order->status()->is( OrderStatus::APPROVED )
&& ! $order->status()->is( OrderStatus::COMPLETED )
) {
return false;
}
$source = $order->payment_source();
if ( $source && $source->card() ) {
return false; // Ignore for DCC.
}
if ( 'card' === $this->session_handler->funding_source() ) {
return false; // Ignore for card buttons.
}
return true;
}
}

View file

@ -11,19 +11,17 @@ namespace WooCommerce\PayPalCommerce\Button\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\Webhooks\Handler\PrefixTrait;
/**
* Class EarlyOrderHandler
*/
class EarlyOrderHandler {
use PrefixTrait;
/**
* The State.
*
@ -51,19 +49,16 @@ class EarlyOrderHandler {
* @param State $state The State.
* @param OrderProcessor $order_processor The Order Processor.
* @param SessionHandler $session_handler The Session Handler.
* @param string $prefix The Prefix.
*/
public function __construct(
State $state,
OrderProcessor $order_processor,
SessionHandler $session_handler,
string $prefix
SessionHandler $session_handler
) {
$this->state = $state;
$this->order_processor = $order_processor;
$this->session_handler = $session_handler;
$this->prefix = $prefix;
}
/**
@ -101,7 +96,7 @@ class EarlyOrderHandler {
$order_id = false;
foreach ( $order->purchase_units() as $purchase_unit ) {
if ( $purchase_unit->custom_id() === sanitize_text_field( wp_unslash( $_REQUEST['ppcp-resume-order'] ) ) ) {
$order_id = (int) $this->sanitize_custom_id( $purchase_unit->custom_id() );
$order_id = (int) $purchase_unit->custom_id();
}
}
if ( $order_id === $resume_order_id ) {
@ -169,6 +164,10 @@ class EarlyOrderHandler {
/**
* Patch Order so we have the \WC_Order id added.
*/
return $this->order_processor->patch_order( $wc_order, $order );
$order = $this->order_processor->patch_order( $wc_order, $order );
do_action( 'woocommerce_paypal_payments_woocommerce_order_created', $wc_order, $order );
return $order;
}
}

View file

@ -27,20 +27,91 @@ class CheckoutFormValidator extends WC_Checkout {
public function validate( array $data ) {
$errors = new WP_Error();
// Some plugins check their fields using $_POST,
// Some plugins check their fields using $_POST or $_REQUEST,
// also WC terms checkbox https://github.com/woocommerce/woocommerce/issues/35328 .
foreach ( $data as $key => $value ) {
$_POST[ $key ] = $value;
$_POST[ $key ] = $value;
$_REQUEST[ $key ] = $value;
}
// And we must call get_posted_data because it handles the shipping address.
$data = $this->get_posted_data();
// It throws some notices when checking fields etc., also from other plugins via hooks.
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@$this->validate_checkout( $data, $errors );
// Looks like without this WC()->shipping->get_packages() is empty which is used by some plugins.
WC()->cart->calculate_shipping();
if ( $errors->has_errors() ) {
throw new ValidationException( $errors->get_error_messages() );
// Some plugins/filters check is_checkout().
$is_checkout = function () {
return true;
};
add_filter( 'woocommerce_is_checkout', $is_checkout );
try {
// And we must call get_posted_data because it handles the shipping address.
$data = $this->get_posted_data();
// It throws some notices when checking fields etc., also from other plugins via hooks.
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
@$this->validate_checkout( $data, $errors );
} finally {
remove_filter( 'woocommerce_is_checkout', $is_checkout );
}
if (
apply_filters( 'woocommerce_paypal_payments_early_wc_checkout_account_creation_validation_enabled', true ) &&
! is_user_logged_in() && ( $this->is_registration_required() || ! empty( $data['createaccount'] ) )
) {
$username = ! empty( $data['account_username'] ) ? $data['account_username'] : '';
$email = $data['billing_email'] ?? '';
if ( email_exists( $email ) ) {
$errors->add(
'registration-error-email-exists',
apply_filters(
'woocommerce_registration_error_email_exists',
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
__( 'An account is already registered with your email address. <a href="#" class="showlogin">Please log in.</a>', 'woocommerce' ),
$email
)
);
}
if ( $username ) { // Empty username is already checked in validate_checkout, and it can be generated.
$username = sanitize_user( $username );
if ( empty( $username ) || ! validate_username( $username ) ) {
$errors->add(
'registration-error-invalid-username',
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
__( 'Please enter a valid account username.', 'woocommerce' )
);
}
if ( username_exists( $username ) ) {
$errors->add(
'registration-error-username-exists',
// phpcs:ignore WordPress.WP.I18n.TextDomainMismatch
__( 'An account is already registered with that username. Please choose another.', 'woocommerce' )
);
}
}
}
// Some plugins call wc_add_notice directly.
// We should retrieve such notices, and also clear them to avoid duplicates later.
// TODO: Normally WC converts the messages from validate_checkout into notices,
// maybe we should do the same for consistency, but it requires lots of changes in the way we handle/output errors.
$messages = array_merge(
$errors->get_error_messages(),
array_map(
function ( array $notice ): string {
return $notice['notice'];
},
wc_get_notices( 'error' )
)
);
if ( wc_notice_count( 'error' ) > 0 ) {
wc_clear_notices();
}
if ( $messages ) {
throw new ValidationException( $messages );
}
}
}