diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 15615d7ee..f33c6e6d5 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -23,9 +23,6 @@ class ApplepayButton { this.ppcpConfig ); - //PRODUCT DETAIL PAGE - this.refreshContextData(); - this.updated_contact_info = [] this.selectedShippingMethod = [] this.nonce = document.getElementById('woocommerce-process-checkout-nonce').value @@ -35,6 +32,21 @@ class ApplepayButton { console.log('[ApplePayButton]', ...arguments); } } + + //PRODUCT DETAIL PAGE + this.refreshContextData(); + + if (this.context === 'product') { + jQuery(document).on('appleclick', () => { + (this.onshippingcontactselected())({ + shippingContact: { + locality: 'New York', + postalCode: '10001', + countryCode: 'US' + } + }); + }); + } } init(config) { @@ -260,6 +272,8 @@ class ApplepayButton { case 'product': // Refresh product data that makes the price change. this.productQuantity = document.querySelector('input.qty').value; + this.products = this.contextHandler.products(); + this.log('Products updated', this.products); break; } } @@ -389,6 +403,7 @@ class ApplepayButton { return { action: 'ppcp_update_shipping_contact', product_id: product_id, + products: JSON.stringify(this.products), caller_page: 'productDetail', product_quantity: this.productQuantity, simplified_contact: event.shippingContact, @@ -419,6 +434,7 @@ class ApplepayButton { action: 'ppcp_update_shipping_method', shipping_method: event.shippingMethod, product_id: product_id, + products: JSON.stringify(this.products), caller_page: 'productDetail', product_quantity: this.productQuantity, simplified_contact: this.updated_contact_info, @@ -456,6 +472,7 @@ class ApplepayButton { action: 'ppcp_create_order', 'caller_page': this.context, 'product_id': this.buttonConfig.product.id ?? null, + 'products': JSON.stringify(this.products), 'product_quantity': this.productQuantity ?? null, 'shipping_contact': shippingContact, 'billing_contact': billingContact, diff --git a/modules/ppcp-applepay/resources/js/Context/SingleProductHandler.js b/modules/ppcp-applepay/resources/js/Context/SingleProductHandler.js index ddb7ad531..f9adb7774 100644 --- a/modules/ppcp-applepay/resources/js/Context/SingleProductHandler.js +++ b/modules/ppcp-applepay/resources/js/Context/SingleProductHandler.js @@ -49,12 +49,20 @@ class SingleProductHandler extends BaseHandler { } createOrder() { + return this.actionHandler().configuration().createOrder(); + } + + products() { + return this.actionHandler().getProducts(); + } + + actionHandler() { const errorHandler = new ErrorHandler( this.ppcpConfig.labels.error.generic, document.querySelector('.woocommerce-notices-wrapper') ); - const actionHandler = new SingleProductActionHandler( + return new SingleProductActionHandler( this.ppcpConfig, new UpdateCart( this.ppcpConfig.ajax.change_cart.endpoint, @@ -63,8 +71,6 @@ class SingleProductHandler extends BaseHandler { document.querySelector('form.cart'), errorHandler, ); - - return actionHandler.configuration().createOrder(); } } diff --git a/modules/ppcp-applepay/services.php b/modules/ppcp-applepay/services.php index 919f6a25b..6274a5fb8 100644 --- a/modules/ppcp-applepay/services.php +++ b/modules/ppcp-applepay/services.php @@ -100,7 +100,6 @@ return array( return new DataToAppleButtonScripts( $container->get( 'applepay.sdk_script_url' ), $container->get( 'wcgateway.settings' ) ); }, 'applepay.button' => static function ( ContainerInterface $container ): ApplePayButton { - return new ApplePayButton( $container->get( 'wcgateway.settings' ), $container->get( 'woocommerce.logger.woocommerce' ), @@ -108,8 +107,9 @@ return array( $container->get( 'applepay.url' ), $container->get( 'ppcp.asset-version' ), $container->get( 'applepay.data_to_scripts' ), - $container->get( 'wcgateway.settings.status' ) - ); + $container->get( 'wcgateway.settings.status' ), + $container->get( 'button.helper.cart-products' ) + ); }, 'applepay.blocks-payment-method' => static function ( ContainerInterface $container ): PaymentMethodTypeInterface { return new BlocksPaymentMethod( diff --git a/modules/ppcp-applepay/src/Assets/ApplePayButton.php b/modules/ppcp-applepay/src/Assets/ApplePayButton.php index 693c5bf04..fe7313a85 100644 --- a/modules/ppcp-applepay/src/Assets/ApplePayButton.php +++ b/modules/ppcp-applepay/src/Assets/ApplePayButton.php @@ -14,6 +14,7 @@ use Psr\Log\LoggerInterface; use WC_Cart; use WC_Order; use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface; +use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; @@ -25,18 +26,21 @@ use WooCommerce\PayPalCommerce\Webhooks\Handler\RequestHandlerTrait; */ class ApplePayButton implements ButtonInterface { use RequestHandlerTrait; + /** * The settings. * * @var Settings */ private $settings; + /** * The logger. * * @var LoggerInterface */ private $logger; + /** * The response templates. * @@ -58,12 +62,14 @@ class ApplePayButton implements ButtonInterface { * @var string */ protected $id; + /** * The method title. * * @var string */ protected $method_title; + /** * The processor for orders. * @@ -84,18 +90,21 @@ class ApplePayButton implements ButtonInterface { * @var string */ private $version; + /** * The module URL. * * @var string */ private $module_url; + /** * The data to send to the ApplePay button script. * * @var DataToAppleButtonScripts */ private $script_data; + /** * The Settings status helper. * @@ -103,6 +112,13 @@ class ApplePayButton implements ButtonInterface { */ private $settings_status; + /** + * The cart products helper. + * + * @var CartProductsHelper + */ + protected $cart_products; + /** * PayPalPaymentMethod constructor. * @@ -113,6 +129,7 @@ class ApplePayButton implements ButtonInterface { * @param string $version The module version. * @param DataToAppleButtonScripts $data The data to send to the ApplePay button script. * @param SettingsStatus $settings_status The settings status helper. + * @param CartProductsHelper $cart_products The cart products helper. */ public function __construct( Settings $settings, @@ -121,7 +138,8 @@ class ApplePayButton implements ButtonInterface { string $module_url, string $version, DataToAppleButtonScripts $data, - SettingsStatus $settings_status + SettingsStatus $settings_status, + CartProductsHelper $cart_products ) { $this->settings = $settings; $this->response_templates = new ResponsesToApple(); @@ -133,6 +151,7 @@ class ApplePayButton implements ButtonInterface { $this->version = $version; $this->script_data = $data; $this->settings_status = $settings_status; + $this->cart_products = $cart_products; } /** @@ -822,15 +841,24 @@ class ApplePayButton implements ButtonInterface { * * @param ApplePayDataObjectHttp $applepay_request_data_object The request data object. * @return bool | string The cart item key after adding to the new cart. - * @throws \Exception If cannot be added to cart. + * @throws \Exception If it cannot be added to cart. */ public function prepare_cart( ApplePayDataObjectHttp $applepay_request_data_object ): string { $this->save_old_cart(); - $cart = WC()->cart; - return $cart->add_to_cart( - (int) $applepay_request_data_object->product_id(), - (int) $applepay_request_data_object->product_quantity() + $this->cart_products->set_cart( WC()->cart ); + + $product = $this->cart_products->product_from_data( + array( + 'id' => (int) $applepay_request_data_object->product_id(), + 'quantity' => (int) $applepay_request_data_object->product_quantity(), + 'variations' => $applepay_request_data_object->product_variations(), + 'extra' => $applepay_request_data_object->product_extra(), + 'booking' => $applepay_request_data_object->product_booking(), + ) ); + + $this->cart_products->add_products( array( $product ) ); + return $this->cart_products->cart_item_keys()[0]; } /** diff --git a/modules/ppcp-applepay/src/Assets/ApplePayDataObjectHttp.php b/modules/ppcp-applepay/src/Assets/ApplePayDataObjectHttp.php index ddc05a9fe..ab000a0e1 100644 --- a/modules/ppcp-applepay/src/Assets/ApplePayDataObjectHttp.php +++ b/modules/ppcp-applepay/src/Assets/ApplePayDataObjectHttp.php @@ -38,13 +38,6 @@ class ApplePayDataObjectHttp { */ protected $need_shipping; - /** - * The product id. - * - * @var mixed - */ - protected $product_id = ''; - /** * The caller page. * @@ -52,6 +45,13 @@ class ApplePayDataObjectHttp { */ protected $caller_page; + /** + * The product id. + * + * @var mixed + */ + protected $product_id = ''; + /** * The product quantity. * @@ -59,6 +59,27 @@ class ApplePayDataObjectHttp { */ protected $product_quantity = ''; + /** + * The product variations. + * + * @var string + */ + protected $product_variations = array(); + + /** + * The product extra. + * + * @var string + */ + protected $product_extra = array(); + + /** + * The product booking. + * + * @var string + */ + protected $product_booking = array(); + /** * The shipping methods. * @@ -166,6 +187,9 @@ class ApplePayDataObjectHttp { if ( ! $data ) { return; } + + $data = $this->preprocess_request_data( $data ); + $result = $this->update_required_data( $data, PropertiesDictionary::UPDATE_CONTACT_SINGLE_PROD_REQUIRED_FIELDS, @@ -198,6 +222,9 @@ class ApplePayDataObjectHttp { if ( ! $data ) { return; } + + $data = $this->preprocess_request_data( $data ); + $result = $this->update_required_data( $data, PropertiesDictionary::UPDATE_METHOD_SINGLE_PROD_REQUIRED_FIELDS, @@ -226,6 +253,9 @@ class ApplePayDataObjectHttp { if ( ! $data ) { return; } + + $data = $this->preprocess_request_data( $data ); + $data[ PropertiesDictionary::CALLER_PAGE ] = $caller_page; $result = $this->update_required_data( $data, @@ -261,6 +291,27 @@ class ApplePayDataObjectHttp { $this->update_shipping_method( $data ); } + /** + * Pre-processes request data to transform it to a standard format. + * + * @param array $data + * @return array + */ + protected function preprocess_request_data( array $data ): array { + // Fill product variables if a products object is received. + if ( is_array( $data[ PropertiesDictionary::PRODUCTS ] ?? null ) ) { + $product = $data[ PropertiesDictionary::PRODUCTS ][0]; + + $data[ PropertiesDictionary::PRODUCT_ID ] = $product['id'] ?? 0; + $data[ PropertiesDictionary::PRODUCT_QUANTITY ] = $product['quantity'] ?? array(); + $data[ PropertiesDictionary::PRODUCT_VARIATIONS ] = $product['variations'] ?? array(); + $data[ PropertiesDictionary::PRODUCT_EXTRA ] = $product['extra'] ?? array(); + $data[ PropertiesDictionary::PRODUCT_BOOKING ] = $product['booking'] ?? array(); + } + unset( $data[ PropertiesDictionary::PRODUCTS ] ); + return $data; + } + /** * Checks if the array contains all required fields and if those * are not empty. @@ -300,7 +351,7 @@ class ApplePayDataObjectHttp { */ protected function assign_data_object_values( array $data ): void { foreach ( $data as $key => $value ) { - // Null values may give origin to type errors. If necessary replace condition this with a specialized field filter. + // Null values may give origin to type errors. If necessary replace this condition with a specialized field filter. if ( null === $value ) { continue; } @@ -547,6 +598,33 @@ class ApplePayDataObjectHttp { return $this->product_quantity; } + /** + * Returns the product variations. + * + * @return string + */ + public function product_variations(): array { + return $this->product_variations; + } + + /** + * Returns the product extra. + * + * @return string + */ + public function product_extra(): array { + return $this->product_extra; + } + + /** + * Returns the product booking. + * + * @return string + */ + public function product_booking(): array { + return $this->product_booking; + } + /** * Returns the nonce. * @@ -580,7 +658,7 @@ class ApplePayDataObjectHttp { * @return array|false|null */ public function get_filtered_request_data() { - return filter_input_array( + $data = filter_input_array( INPUT_POST, array( PropertiesDictionary::CALLER_PAGE => FILTER_SANITIZE_SPECIAL_CHARS, @@ -604,8 +682,28 @@ class ApplePayDataObjectHttp { ), PropertiesDictionary::PRODUCT_ID => FILTER_SANITIZE_NUMBER_INT, PropertiesDictionary::PRODUCT_QUANTITY => FILTER_SANITIZE_NUMBER_INT, + PropertiesDictionary::PRODUCT_VARIATIONS => array( + 'filter' => FILTER_SANITIZE_SPECIAL_CHARS, + 'flags' => FILTER_REQUIRE_ARRAY, + ), + PropertiesDictionary::PRODUCT_EXTRA => array( + 'filter' => FILTER_SANITIZE_SPECIAL_CHARS, + 'flags' => FILTER_REQUIRE_ARRAY, + ), + PropertiesDictionary::PRODUCT_BOOKING => array( + 'filter' => FILTER_SANITIZE_SPECIAL_CHARS, + 'flags' => FILTER_REQUIRE_ARRAY, + ), ) ); + + $products = json_decode( wp_unslash( $_POST[ PropertiesDictionary::PRODUCTS ] ?? '' ), true ); + + if ( $products ) { + $data[ PropertiesDictionary::PRODUCTS ] = $products; + } + + return $data; } /** diff --git a/modules/ppcp-applepay/src/Assets/PropertiesDictionary.php b/modules/ppcp-applepay/src/Assets/PropertiesDictionary.php index fdb374fb7..61e8aa006 100644 --- a/modules/ppcp-applepay/src/Assets/PropertiesDictionary.php +++ b/modules/ppcp-applepay/src/Assets/PropertiesDictionary.php @@ -14,7 +14,6 @@ namespace WooCommerce\PayPalCommerce\Applepay\Assets; */ class PropertiesDictionary { - public const BILLING_CONTACT_INVALID = 'billing Contact Invalid'; public const CREATE_ORDER_SINGLE_PROD_REQUIRED_FIELDS = @@ -62,18 +61,20 @@ class PropertiesDictionary { self::SIMPLIFIED_CONTACT, ); - public const PRODUCT_ID = 'product_id'; - - public const SIMPLIFIED_CONTACT = 'simplified_contact'; - - public const SHIPPING_METHOD = 'shipping_method'; - - public const SHIPPING_CONTACT = 'shipping_contact'; + public const PRODUCTS = 'products'; + public const PRODUCT_ID = 'product_id'; + public const PRODUCT_QUANTITY = 'product_quantity'; + public const PRODUCT_VARIATIONS = 'product_variations'; + public const PRODUCT_EXTRA = 'product_extra'; + public const PRODUCT_BOOKING = 'product_booking'; + public const SIMPLIFIED_CONTACT = 'simplified_contact'; + public const SHIPPING_METHOD = 'shipping_method'; + public const SHIPPING_CONTACT = 'shipping_contact'; public const SHIPPING_CONTACT_INVALID = 'shipping Contact Invalid'; + public const BILLING_CONTACT = 'billing_contact'; - public const NONCE = 'nonce'; - + public const NONCE = 'nonce'; public const WCNONCE = 'woocommerce-process-checkout-nonce'; public const CREATE_ORDER_CART_REQUIRED_FIELDS = @@ -83,25 +84,16 @@ class PropertiesDictionary { self::SHIPPING_CONTACT, ); - public const PRODUCT_QUANTITY = 'product_quantity'; - public const CALLER_PAGE = 'caller_page'; - public const BILLING_CONTACT = 'billing_contact'; - public const NEED_SHIPPING = 'need_shipping'; public const UPDATE_SHIPPING_CONTACT = 'ppcp_update_shipping_contact'; - - public const UPDATE_SHIPPING_METHOD = 'ppcp_update_shipping_method'; - - public const CREATE_ORDER = 'ppcp_create_order'; - - public const CREATE_ORDER_CART = 'ppcp_create_order_cart'; - - public const REDIRECT = 'ppcp_redirect'; - - public const VALIDATE = 'ppcp_validate'; + public const UPDATE_SHIPPING_METHOD = 'ppcp_update_shipping_method'; + public const CREATE_ORDER = 'ppcp_create_order'; + public const CREATE_ORDER_CART = 'ppcp_create_order_cart'; + public const REDIRECT = 'ppcp_redirect'; + public const VALIDATE = 'ppcp_validate'; /** * Returns the possible list of button colors. diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 5ca7fb38f..374f15910 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -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\SimulateCartEndpoint; +use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper; use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator; @@ -128,24 +129,24 @@ return array( if ( ! \WC()->cart ) { throw new RuntimeException( 'cant initialize endpoint at this moment' ); } - $smart_button = $container->get( 'button.smart-button' ); - $cart = WC()->cart; - $request_data = $container->get( 'button.request-data' ); - $data_store = \WC_Data_Store::load( 'product' ); - $logger = $container->get( 'woocommerce.logger.woocommerce' ); - return new SimulateCartEndpoint( $smart_button, $cart, $request_data, $data_store, $logger ); + $smart_button = $container->get( 'button.smart-button' ); + $cart = WC()->cart; + $request_data = $container->get( 'button.request-data' ); + $cart_products = $container->get( 'button.helper.cart-products' ); + $logger = $container->get( 'woocommerce.logger.woocommerce' ); + return new SimulateCartEndpoint( $smart_button, $cart, $request_data, $cart_products, $logger ); }, 'button.endpoint.change-cart' => static function ( ContainerInterface $container ): ChangeCartEndpoint { if ( ! \WC()->cart ) { throw new RuntimeException( 'cant initialize endpoint at this moment' ); } - $cart = WC()->cart; - $shipping = WC()->shipping(); - $request_data = $container->get( 'button.request-data' ); + $cart = WC()->cart; + $shipping = WC()->shipping(); + $request_data = $container->get( 'button.request-data' ); $purchase_unit_factory = $container->get( 'api.factory.purchase-unit' ); - $data_store = \WC_Data_Store::load( 'product' ); - $logger = $container->get( 'woocommerce.logger.woocommerce' ); - return new ChangeCartEndpoint( $cart, $shipping, $request_data, $purchase_unit_factory, $data_store, $logger ); + $cart_products = $container->get( 'button.helper.cart-products' ); + $logger = $container->get( 'woocommerce.logger.woocommerce' ); + return new ChangeCartEndpoint( $cart, $shipping, $request_data, $purchase_unit_factory, $cart_products, $logger ); }, 'button.endpoint.create-order' => static function ( ContainerInterface $container ): CreateOrderEndpoint { $request_data = $container->get( 'button.request-data' ); @@ -251,6 +252,12 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, + + 'button.helper.cart-products' => static function ( ContainerInterface $container ): CartProductsHelper { + $data_store = \WC_Data_Store::load( 'product' ); + return new CartProductsHelper( $data_store ); + }, + 'button.helper.three-d-secure' => static function ( ContainerInterface $container ): ThreeDSecure { $logger = $container->get( 'woocommerce.logger.woocommerce' ); return new ThreeDSecure( $logger ); diff --git a/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php b/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php index 3133cf544..604c25124 100644 --- a/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/AbstractCartEndpoint.php @@ -10,6 +10,7 @@ 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 @@ -26,11 +27,11 @@ abstract class AbstractCartEndpoint implements EndpointInterface { protected $cart; /** - * The product data store. + * The cart products helper. * - * @var \WC_Data_Store + * @var CartProductsHelper */ - protected $product_data_store; + protected $cart_products; /** * The request data helper. @@ -53,13 +54,6 @@ abstract class AbstractCartEndpoint implements EndpointInterface { */ protected $logger_tag = ''; - /** - * The added cart item IDs - * - * @var array - */ - private $cart_item_keys = array(); - /** * The nonce. * @@ -110,44 +104,13 @@ abstract class AbstractCartEndpoint implements EndpointInterface { protected function add_products( array $products ): bool { $this->cart->empty_cart( false ); - $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 ) { + try { + $this->cart_products->add_products( $products ); + } catch ( Exception $e ) { $this->handle_error(); } - return $success; + return true; } /** @@ -193,7 +156,7 @@ abstract class AbstractCartEndpoint implements EndpointInterface { */ protected function products_from_request() { $data = $this->request_data->read_request( $this->nonce() ); - $products = $this->products_from_data( $data ); + $products = $this->cart_products->products_from_data( $data ); if ( ! $products ) { wp_send_json_error( array( @@ -212,141 +175,12 @@ abstract class AbstractCartEndpoint implements EndpointInterface { 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, - 'extra' => $product['extra'] ?? 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 ); - - 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. - */ - 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 - ); - - 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. - */ - 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 ); - - 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 */ protected function remove_cart_items(): void { - foreach ( $this->cart_item_keys as $cart_item_key ) { - if ( ! $cart_item_key ) { - continue; - } - $this->cart->remove_cart_item( $cart_item_key ); - } + $this->cart_products->remove_cart_items(); } - } diff --git a/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php b/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php index 95979d1b2..701076bed 100644 --- a/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/ChangeCartEndpoint.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint; use Exception; use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; +use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper; /** * Class ChangeCartEndpoint @@ -41,7 +42,7 @@ class ChangeCartEndpoint extends AbstractCartEndpoint { * @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( @@ -49,7 +50,7 @@ class ChangeCartEndpoint extends AbstractCartEndpoint { \WC_Shipping $shipping, RequestData $request_data, PurchaseUnitFactory $purchase_unit_factory, - \WC_Data_Store $product_data_store, + CartProductsHelper $cart_products, LoggerInterface $logger ) { @@ -57,7 +58,7 @@ class ChangeCartEndpoint extends AbstractCartEndpoint { $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; $this->logger_tag = 'updating'; @@ -70,6 +71,8 @@ class ChangeCartEndpoint extends AbstractCartEndpoint { * @throws Exception On error. */ protected function handle_data(): bool { + $this->cart_products->set_cart( $this->cart ); + $products = $this->products_from_request(); if ( ! $products ) { diff --git a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php index 25ac4ba91..e1b1247c4 100644 --- a/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/SimulateCartEndpoint.php @@ -13,6 +13,7 @@ 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 @@ -38,23 +39,23 @@ class SimulateCartEndpoint extends AbstractCartEndpoint { /** * 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. + * @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, - \WC_Data_Store $product_data_store, + CartProductsHelper $cart_products, LoggerInterface $logger ) { $this->smart_button = $smart_button; $this->cart = clone $cart; $this->request_data = $request_data; - $this->product_data_store = $product_data_store; + $this->cart_products = $cart_products; $this->logger = $logger; $this->logger_tag = 'simulation'; @@ -147,6 +148,7 @@ class SimulateCartEndpoint extends AbstractCartEndpoint { // Store a reference to the real cart. $this->real_cart = WC()->cart; WC()->cart = $this->cart; + $this->cart_products->set_cart( $this->cart ); } /** diff --git a/modules/ppcp-button/src/Helper/CartProductsHelper.php b/modules/ppcp-button/src/Helper/CartProductsHelper.php new file mode 100644 index 000000000..eec427825 --- /dev/null +++ b/modules/ppcp-button/src/Helper/CartProductsHelper.php @@ -0,0 +1,286 @@ +product_data_store = $product_data_store; + } + + /** + * Sets a new cart instance. + * + * @param WC_Cart $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->products_from_data( $product ); + if ( $product ) { + $products[] = $product; + } + } + return $products; + } + + /** + * Returns product information from a data array. + * + * @param array $product + * @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 { + $this->validate_cart(); + + $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 { + $this->validate_cart(); + + $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 { + $this->validate_cart(); + + 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 + */ + public function remove_cart_items(): void { + $this->validate_cart(); + + foreach ( $this->cart_item_keys as $cart_item_key ) { + if ( ! $cart_item_key ) { + continue; + } + $this->cart->remove_cart_item( $cart_item_key ); + } + } + + /** + * @return array + */ + public function cart_item_keys(): array { + return $this->cart_item_keys; + } + + /** + * Throws the cart not set exception. + * + * @return void + * @throws Exception + */ + private function validate_cart(): void { + if ( ! $this->cart ) { + throw new Exception( 'Cart not set.' ); + } + } +}