diff --git a/changelog.txt b/changelog.txt index 5bb4c9623..a840377a9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,6 +1,6 @@ *** Changelog *** -= 2.4.0 - xxxx-xx-xx = += 2.4.0 - 2023-10-31 = * Fix - Mini-Cart Bug cause of wrong DOM-Structure in v2.3.1 #1735 * Fix - ACDC disappearing after plugin updates #1751 * Fix - Subscription module hooks #1748 diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 7cf82b61c..e04fbba5f 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -606,6 +606,37 @@ return array( 'SGD', 'USD', ), + 'BE' => array( + 'EUR', + 'USD', + 'CAD', + 'GBP', + 'PLN', + 'SEK', + 'CHF', + ), + 'BG' => array( + 'EUR', + 'USD', + ), + 'CY' => array( + 'EUR', + 'USD', + 'CAD', + 'GBP', + 'AUD', + 'CZK', + 'DKK', + 'NOK', + 'PLN', + 'SEK', + 'CHF', + ), + 'CZ' => array( + 'EUR', + 'USD', + 'CZK', + ), 'DE' => array( 'AUD', 'CAD', @@ -624,6 +655,16 @@ return array( 'SGD', 'USD', ), + 'DK' => array( + 'EUR', + 'USD', + 'DKK', + 'NOK', + ), + 'EE' => array( + 'EUR', + 'USD', + ), 'ES' => array( 'AUD', 'CAD', @@ -642,6 +683,10 @@ return array( 'SGD', 'USD', ), + 'FI' => array( + 'EUR', + 'USD', + ), 'FR' => array( 'AUD', 'CAD', @@ -678,6 +723,16 @@ return array( 'SGD', 'USD', ), + 'GR' => array( + 'EUR', + 'USD', + 'GBP', + ), + 'HU' => array( + 'EUR', + 'USD', + 'HUF', + ), 'IT' => array( 'AUD', 'CAD', @@ -696,6 +751,32 @@ return array( 'SGD', 'USD', ), + 'LT' => array( + 'EUR', + 'USD', + 'CAD', + 'GBP', + 'JPY', + 'AUD', + 'CZK', + 'DKK', + 'HUF', + 'PLN', + 'SEK', + 'CHF', + 'NZD', + 'NOK', + ), + 'LU' => array( + 'EUR', + 'USD', + ), + 'LV' => array( + 'EUR', + 'USD', + 'CAD', + 'GBP', + ), 'US' => array( 'AUD', 'CAD', @@ -722,9 +803,81 @@ return array( 'SGD', 'USD', ), + 'MT' => array( + 'EUR', + 'USD', + 'CAD', + 'GBP', + 'JPY', + 'AUD', + 'CZK', + 'DKK', + 'HUF', + 'NOK', + 'PLN', + 'SEK', + 'CHF', + ), 'MX' => array( 'MXN', ), + 'NL' => array( + 'EUR', + 'GBP', + 'AUD', + 'CZK', + 'HUF', + 'CHF', + 'CAD', + 'USD', + ), + 'NO' => array( + 'EUR', + 'USD', + 'CAD', + 'GBP', + 'NOK', + ), + 'PL' => array( + 'EUR', + 'USD', + 'CAD', + 'GBP', + 'AUD', + 'DKK', + 'PLN', + 'SEK', + 'CZK', + ), + 'PT' => array( + 'EUR', + 'USD', + 'CAD', + 'GBP', + 'CZK', + ), + 'RO' => array( + 'EUR', + 'USD', + 'GBP', + ), + 'SE' => array( + 'EUR', + 'USD', + 'NOK', + 'SEK', + ), + 'SI' => array( + 'EUR', + 'USD', + ), + 'SK' => array( + 'EUR', + 'USD', + 'GBP', + 'CZK', + 'HUF', + ), 'JP' => array( 'AUD', 'CAD', @@ -762,16 +915,51 @@ return array( 'visa' => array(), 'amex' => array( 'AUD' ), ), + 'BE' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR', 'USD', 'CAD' ), + ), + 'BG' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'CY' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'CZ' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'CZK' ), + ), 'DE' => array( 'mastercard' => array(), 'visa' => array(), 'amex' => array( 'EUR' ), ), + 'DK' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'DKK' ), + ), + 'EE' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array(), + ), 'ES' => array( 'mastercard' => array(), 'visa' => array(), 'amex' => array( 'EUR' ), ), + 'FI' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), 'FR' => array( 'mastercard' => array(), 'visa' => array(), @@ -782,6 +970,16 @@ return array( 'visa' => array(), 'amex' => array( 'GBP', 'USD' ), ), + 'GR' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'HU' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'HUF' ), + ), 'IT' => array( 'mastercard' => array(), 'visa' => array(), @@ -799,11 +997,71 @@ return array( 'amex' => array( 'CAD' ), 'jcb' => array( 'CAD' ), ), + 'LT' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'LU' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'LV' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR', 'USD' ), + ), + 'MT' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), 'MX' => array( 'mastercard' => array(), 'visa' => array(), 'amex' => array(), ), + 'NL' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR', 'USD' ), + ), + 'NO' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'NOK' ), + ), + 'PL' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR', 'USD', 'GBP', 'PLN' ), + ), + 'PT' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR', 'USD', 'CAD', 'GBP' ), + ), + 'RO' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR', 'USD' ), + ), + 'SE' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR', 'SEK' ), + ), + 'SI' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR' ), + ), + 'SK' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'EUR', 'GBP' ), + ), 'JP' => array( 'mastercard' => array(), 'visa' => array(), diff --git a/modules/ppcp-applepay/resources/js/ApplepayButton.js b/modules/ppcp-applepay/resources/js/ApplepayButton.js index 15615d7ee..19c03c5d2 100644 --- a/modules/ppcp-applepay/resources/js/ApplepayButton.js +++ b/modules/ppcp-applepay/resources/js/ApplepayButton.js @@ -23,18 +23,17 @@ class ApplepayButton { this.ppcpConfig ); - //PRODUCT DETAIL PAGE - this.refreshContextData(); - this.updated_contact_info = [] this.selectedShippingMethod = [] - this.nonce = document.getElementById('woocommerce-process-checkout-nonce').value + this.nonce = document.getElementById('woocommerce-process-checkout-nonce')?.value this.log = function() { if ( this.buttonConfig.is_debug ) { console.log('[ApplePayButton]', ...arguments); } } + + this.refreshContextData(); } init(config) { @@ -260,6 +259,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 +390,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 +421,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 +459,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..a49ab7fad 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,7 +107,8 @@ 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 { diff --git a/modules/ppcp-applepay/src/ApplepayModule.php b/modules/ppcp-applepay/src/ApplepayModule.php index c63750fbc..b81329811 100644 --- a/modules/ppcp-applepay/src/ApplepayModule.php +++ b/modules/ppcp-applepay/src/ApplepayModule.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\Applepay; use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry; use WooCommerce\PayPalCommerce\Applepay\Assets\ApplePayButton; use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus; +use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary; use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface; use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface; use WooCommerce\PayPalCommerce\Applepay\Helper\AvailabilityNotice; @@ -40,6 +41,7 @@ class ApplepayModule implements ModuleInterface { * {@inheritDoc} */ public function run( ContainerInterface $c ): void { + $module = $this; // Clears product status when appropriate. add_action( @@ -51,38 +53,65 @@ class ApplepayModule implements ModuleInterface { } ); - // Check if the module is applicable, correct country, currency, ... etc. - if ( ! $c->get( 'applepay.eligible' ) ) { - return; - } + add_action( + 'init', + static function () use ( $c, $module ) { - // Load the button handler. - $apple_payment_method = $c->get( 'applepay.button' ); - // add onboarding and referrals hooks. - assert( $apple_payment_method instanceof ApplepayButton ); - $apple_payment_method->initialize(); + // Check if the module is applicable, correct country, currency, ... etc. + if ( ! $c->get( 'applepay.eligible' ) ) { + return; + } - // Show notice if there are product availability issues. - $availability_notice = $c->get( 'applepay.availability_notice' ); - assert( $availability_notice instanceof AvailabilityNotice ); - $availability_notice->execute(); + // Load the button handler. + $apple_payment_method = $c->get( 'applepay.button' ); + // add onboarding and referrals hooks. + assert( $apple_payment_method instanceof ApplepayButton ); + $apple_payment_method->initialize(); - // Return if server not supported. - if ( ! $c->get( 'applepay.server_supported' ) ) { - return; - } + // Show notice if there are product availability issues. + $availability_notice = $c->get( 'applepay.availability_notice' ); + assert( $availability_notice instanceof AvailabilityNotice ); + $availability_notice->execute(); - // Check if this merchant can activate / use the buttons. - // We allow non referral merchants as they can potentially still use ApplePay, we just have no way of checking the capability. - if ( ( ! $c->get( 'applepay.available' ) ) && $c->get( 'applepay.is_referral' ) ) { - return; - } + // Return if server not supported. + if ( ! $c->get( 'applepay.server_supported' ) ) { + return; + } - $this->load_assets( $c, $apple_payment_method ); - $this->handle_validation_file( $c ); - $this->render_buttons( $c, $apple_payment_method ); + // Check if this merchant can activate / use the buttons. + // We allow non referral merchants as they can potentially still use ApplePay, we just have no way of checking the capability. + if ( ( ! $c->get( 'applepay.available' ) ) && $c->get( 'applepay.is_referral' ) ) { + return; + } - $apple_payment_method->bootstrap_ajax_request(); + $module->load_assets( $c, $apple_payment_method ); + $module->handle_validation_file( $c ); + $module->render_buttons( $c, $apple_payment_method ); + + $apple_payment_method->bootstrap_ajax_request(); + } + ); + + add_filter( + 'nonce_user_logged_out', + /** + * Prevents nonce from being changed for non logged in users. + * + * @param int $uid The uid. + * @param string|int $action The action. + * @return int + * + * @psalm-suppress MissingClosureParamType + */ + function ( $uid, $action ) { + if ( $action === PropertiesDictionary::NONCE_ACTION ) { + return 0; + } + return $uid; + }, + 100, + 2 + ); } /** 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..219e3c6d9 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 array + */ + protected $product_variations = array(); + + /** + * The product extra. + * + * @var array + */ + protected $product_extra = array(); + + /** + * The product booking. + * + * @var array + */ + 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,10 @@ class ApplePayDataObjectHttp { if ( ! $data ) { return; } + + $data = $this->append_products_to_data( $data, $_POST ); + $data = $this->preprocess_request_data( $data ); + $data[ PropertiesDictionary::CALLER_PAGE ] = $caller_page; $result = $this->update_required_data( $data, @@ -261,6 +292,27 @@ class ApplePayDataObjectHttp { $this->update_shipping_method( $data ); } + /** + * Pre-processes request data to transform it to a standard format. + * + * @param array $data The 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 +352,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 +599,33 @@ class ApplePayDataObjectHttp { return $this->product_quantity; } + /** + * Returns the product variations. + * + * @return array + */ + public function product_variations(): array { + return $this->product_variations; + } + + /** + * Returns the product extra. + * + * @return array + */ + public function product_extra(): array { + return $this->product_extra; + } + + /** + * Returns the product booking. + * + * @return array + */ + public function product_booking(): array { + return $this->product_booking; + } + /** * Returns the nonce. * @@ -580,7 +659,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 +683,43 @@ 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, + ), ) ); + + if ( ! $data ) { + return false; + } + + return $this->append_products_to_data( $data, $_POST ); + } + + /** + * Appends product to a data array. + * + * @param array $data The data. + * @param array $request_data The request data. + * @return array + */ + public function append_products_to_data( array $data, array $request_data ): array { + $products = json_decode( wp_unslash( $request_data[ PropertiesDictionary::PRODUCTS ] ?? '' ), true ); + + if ( $products ) { + $data[ PropertiesDictionary::PRODUCTS ] = $products; + } + + return $data; } /** diff --git a/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php b/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php index 6558ac472..0b15fbe78 100644 --- a/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php +++ b/modules/ppcp-applepay/src/Assets/DataToAppleButtonScripts.php @@ -169,37 +169,32 @@ class DataToAppleButtonScripts { if ( ! $cart ) { return array(); } - $nonce = wp_nonce_field( 'woocommerce-process_checkout', 'woocommerce-process-checkout-nonce' ); - $button_markup = - '
' - . $nonce - . '
'; - $type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : ''; - $color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : ''; - $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; - $lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang ); + + $type = $this->settings->has( 'applepay_button_type' ) ? $this->settings->get( 'applepay_button_type' ) : ''; + $color = $this->settings->has( 'applepay_button_color' ) ? $this->settings->get( 'applepay_button_color' ) : ''; + $lang = $this->settings->has( 'applepay_button_language' ) ? $this->settings->get( 'applepay_button_language' ) : ''; + $lang = apply_filters( 'woocommerce_paypal_payments_applepay_button_language', $lang ); return array( - 'sdk_url' => $this->sdk_url, - 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, - 'button' => array( + 'sdk_url' => $this->sdk_url, + 'is_debug' => defined( 'WP_DEBUG' ) && WP_DEBUG ? true : false, + 'button' => array( 'wrapper' => 'applepay-container', 'mini_cart_wrapper' => 'applepay-container-minicart', 'type' => $type, 'color' => $color, 'lang' => $lang, ), - 'product' => array( + 'product' => array( 'needShipping' => $cart->needs_shipping(), 'subtotal' => $cart->get_subtotal(), ), - 'shop' => array( + 'shop' => array( 'countryCode' => $shop_country_code, 'currencyCode' => $currency_code, 'totalLabel' => $total_label, ), - 'ajax_url' => admin_url( 'admin-ajax.php' ), - 'buttonMarkup' => $button_markup, + 'ajax_url' => admin_url( 'admin-ajax.php' ), ); } } diff --git a/modules/ppcp-applepay/src/Assets/PropertiesDictionary.php b/modules/ppcp-applepay/src/Assets/PropertiesDictionary.php index fdb374fb7..25227e11b 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,19 +61,22 @@ 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 WCNONCE = 'woocommerce-process-checkout-nonce'; + public const NONCE = 'nonce'; + public const NONCE_ACTION = 'woocommerce-process_checkout'; + public const WCNONCE = 'woocommerce-process-checkout-nonce'; public const CREATE_ORDER_CART_REQUIRED_FIELDS = array( @@ -83,25 +85,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-blocks/resources/js/Helper/Address.js b/modules/ppcp-blocks/resources/js/Helper/Address.js index d522d208e..6a999f141 100644 --- a/modules/ppcp-blocks/resources/js/Helper/Address.js +++ b/modules/ppcp-blocks/resources/js/Helper/Address.js @@ -124,3 +124,29 @@ export const paypalOrderToWcAddresses = (order) => { return {billingAddress, shippingAddress}; } + +/** + * Merges two WC addresses. + * The objects can contain either the WC form fields or billingAddress, shippingAddress objects. + * + * @param {Object} address1 + * @param {Object} address2 + * @returns {any} + */ +export const mergeWcAddress = (address1, address2) => { + if ('billingAddress' in address1) { + return { + billingAddress: mergeWcAddress(address1.billingAddress, address2.billingAddress), + shippingAddress: mergeWcAddress(address1.shippingAddress, address2.shippingAddress), + } + } + + let address2WithoutEmpty = {...address2}; + Object.keys(address2).forEach(key => { + if (address2[key] === '') { + delete address2WithoutEmpty[key]; + } + }); + + return {...address1, ...address2WithoutEmpty}; +} diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js index 85bcd278f..582fdede3 100644 --- a/modules/ppcp-blocks/resources/js/checkout-block.js +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -1,6 +1,6 @@ import {useEffect, useState} from '@wordpress/element'; import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry'; -import {paypalAddressToWc, paypalOrderToWcAddresses} from "./Helper/Address"; +import {mergeWcAddress, paypalAddressToWc, paypalOrderToWcAddresses} from "./Helper/Address"; import {loadPaypalScript} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading' import buttonModuleWatcher from "../../../ppcp-button/resources/js/modules/ButtonModuleWatcher"; @@ -24,6 +24,22 @@ const PayPalComponent = ({ const [paypalOrder, setPaypalOrder] = useState(null); + useEffect(() => { + // fill the form if in continuation (for product or mini-cart buttons) + if (!config.scriptData.continuation || !config.scriptData.continuation.order || window.ppcpContinuationFilled) { + return; + } + const paypalAddresses = paypalOrderToWcAddresses(config.scriptData.continuation.order); + const wcAddresses = wp.data.select('wc/store/cart').getCustomerData(); + const addresses = mergeWcAddress(wcAddresses, paypalAddresses); + wp.data.dispatch('wc/store/cart').setBillingAddress(addresses.billingAddress); + if (shippingData.needsShipping) { + wp.data.dispatch('wc/store/cart').setShippingAddress(addresses.shippingAddress); + } + // this useEffect should run only once, but adding this in case of some kind of full re-rendering + window.ppcpContinuationFilled = true; + }, []) + const [loaded, setLoaded] = useState(false); useEffect(() => { if (!loaded) { diff --git a/modules/ppcp-blocks/src/PayPalPaymentMethod.php b/modules/ppcp-blocks/src/PayPalPaymentMethod.php index 1da58914d..3c407f2a2 100644 --- a/modules/ppcp-blocks/src/PayPalPaymentMethod.php +++ b/modules/ppcp-blocks/src/PayPalPaymentMethod.php @@ -167,6 +167,11 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType { $script_data['continuation']['cancel'] = array( 'html' => $this->cancellation_view->render_session_cancellation( $url, $this->session_handler->funding_source() ), ); + + $order = $this->session_handler->order(); + if ( $order ) { + $script_data['continuation']['order'] = $order->to_array(); + } } return array( diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 8661f54a6..0ef77144d 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..1040cc680 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,24 +39,24 @@ 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->logger = $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'; } @@ -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..804c26598 --- /dev/null +++ b/modules/ppcp-button/src/Helper/CartProductsHelper.php @@ -0,0 +1,285 @@ +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; + } + +} diff --git a/modules/ppcp-button/src/Helper/ContextTrait.php b/modules/ppcp-button/src/Helper/ContextTrait.php index babdcc28c..12a60c384 100644 --- a/modules/ppcp-button/src/Helper/ContextTrait.php +++ b/modules/ppcp-button/src/Helper/ContextTrait.php @@ -86,7 +86,7 @@ trait ContextTrait { return $context; } - if ( is_shop() ) { + if ( is_shop() || is_product_category() ) { return 'shop'; } diff --git a/modules/ppcp-compat/resources/js/tracking-compat.js b/modules/ppcp-compat/resources/js/tracking-compat.js index b62158ff1..f6d2c80e4 100644 --- a/modules/ppcp-compat/resources/js/tracking-compat.js +++ b/modules/ppcp-compat/resources/js/tracking-compat.js @@ -9,7 +9,9 @@ document.addEventListener( const loadLocation = location.href + " " + orderTrackingContainerSelector + ">*"; const gzdSyncEnabled = config.gzd_sync_enabled; const wcShipmentSyncEnabled = config.wc_shipment_sync_enabled; + const wcShippingTaxSyncEnabled = config.wc_shipping_tax_sync_enabled; const wcShipmentSaveButton = document.querySelector('#woocommerce-shipment-tracking .button-save-form'); + const wcShipmentTaxBuyLabelButtonSelector = '.components-modal__screen-overlay .label-purchase-modal__sidebar .purchase-section button.components-button'; const toggleLoaderVisibility = function() { const loader = document.querySelector('.ppcp-tracking-loader'); @@ -45,5 +47,20 @@ document.addEventListener( waitForTrackingUpdate(jQuery('#shipment-tracking-form')); }) } + + if (wcShippingTaxSyncEnabled && typeof(wcShippingTaxSyncEnabled) != 'undefined' && wcShippingTaxSyncEnabled != null) { + document.addEventListener('click', function(event) { + const wcShipmentTaxBuyLabelButton = event.target.closest(wcShipmentTaxBuyLabelButtonSelector); + + if (wcShipmentTaxBuyLabelButton) { + toggleLoaderVisibility(); + setTimeout(function () { + jQuery(orderTrackingContainerSelector).load(loadLocation, "", function(){ + toggleLoaderVisibility(); + }); + }, 10000); + } + }); + } }, ); diff --git a/modules/ppcp-compat/services.php b/modules/ppcp-compat/services.php index 035851b04..364686a84 100644 --- a/modules/ppcp-compat/services.php +++ b/modules/ppcp-compat/services.php @@ -65,6 +65,12 @@ return array( 'compat.ywot.is_supported_plugin_version_active' => function (): bool { return function_exists( 'yith_ywot_init' ); }, + 'compat.shipstation.is_supported_plugin_version_active' => function (): bool { + return function_exists( 'woocommerce_shipstation_init' ); + }, + 'compat.wc_shipping_tax.is_supported_plugin_version_active' => function (): bool { + return class_exists( 'WC_Connect_Loader' ); + }, 'compat.module.url' => static function ( ContainerInterface $container ): string { /** @@ -84,6 +90,7 @@ return array( $container->get( 'ppcp.asset-version' ), $container->get( 'compat.gzd.is_supported_plugin_version_active' ), $container->get( 'compat.wc_shipment_tracking.is_supported_plugin_version_active' ), + $container->get( 'compat.wc_shipping_tax.is_supported_plugin_version_active' ), $container->get( 'api.bearer' ) ); }, diff --git a/modules/ppcp-compat/src/Assets/CompatAssets.php b/modules/ppcp-compat/src/Assets/CompatAssets.php index c1febf93f..85e0bde7b 100644 --- a/modules/ppcp-compat/src/Assets/CompatAssets.php +++ b/modules/ppcp-compat/src/Assets/CompatAssets.php @@ -47,6 +47,13 @@ class CompatAssets { */ protected $is_wc_shipment_active; + /** + * Whether WC Shipping & Tax plugin is active + * + * @var bool + */ + private $is_wc_shipping_tax_active; + /** * The bearer. * @@ -61,6 +68,7 @@ class CompatAssets { * @param string $version The assets version. * @param bool $is_gzd_active Whether Germanized plugin is active. * @param bool $is_wc_shipment_active Whether WC Shipments plugin is active. + * @param bool $is_wc_shipping_tax_active Whether WC Shipping & Tax plugin is active. * @param Bearer $bearer The bearer. */ public function __construct( @@ -68,14 +76,16 @@ class CompatAssets { string $version, bool $is_gzd_active, bool $is_wc_shipment_active, + bool $is_wc_shipping_tax_active, Bearer $bearer ) { - $this->module_url = $module_url; - $this->version = $version; - $this->is_gzd_active = $is_gzd_active; - $this->is_wc_shipment_active = $is_wc_shipment_active; - $this->bearer = $bearer; + $this->module_url = $module_url; + $this->version = $version; + $this->is_gzd_active = $is_gzd_active; + $this->is_wc_shipment_active = $is_wc_shipment_active; + $this->is_wc_shipping_tax_active = $is_wc_shipping_tax_active; + $this->bearer = $bearer; } /** @@ -99,6 +109,7 @@ class CompatAssets { array( 'gzd_sync_enabled' => apply_filters( 'woocommerce_paypal_payments_sync_gzd_tracking', true ) && $this->is_gzd_active, 'wc_shipment_sync_enabled' => apply_filters( 'woocommerce_paypal_payments_sync_wc_shipment_tracking', true ) && $this->is_wc_shipment_active, + 'wc_shipping_tax_sync_enabled' => apply_filters( 'woocommerce_paypal_payments_sync_wc_shipping_tax', true ) && $this->is_wc_shipping_tax_active, ) ); } diff --git a/modules/ppcp-compat/src/CompatModule.php b/modules/ppcp-compat/src/CompatModule.php index 790c3edb0..edb4f0e67 100644 --- a/modules/ppcp-compat/src/CompatModule.php +++ b/modules/ppcp-compat/src/CompatModule.php @@ -9,22 +9,13 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Compat; -use Vendidero\Germanized\Shipments\ShipmentItem; -use WooCommerce\PayPalCommerce\OrderTracking\Shipment\ShipmentFactoryInterface; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; -use Exception; use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; -use Psr\Log\LoggerInterface; -use Vendidero\Germanized\Shipments\Shipment; -use WC_Order; use WooCommerce\PayPalCommerce\Compat\Assets\CompatAssets; -use WooCommerce\PayPalCommerce\OrderTracking\Endpoint\OrderTrackingEndpoint; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; -use WP_REST_Request; -use WP_REST_Response; /** * Class CompatModule @@ -124,230 +115,11 @@ class CompatModule implements ModuleInterface { * @return void */ protected function initialize_tracking_compat_layer( ContainerInterface $c ): void { - $is_gzd_active = $c->get( 'compat.gzd.is_supported_plugin_version_active' ); - $is_wc_shipment_tracking_active = $c->get( 'compat.wc_shipment_tracking.is_supported_plugin_version_active' ); - $is_ywot_active = $c->get( 'compat.ywot.is_supported_plugin_version_active' ); + $order_tracking_integrations = $c->get( 'order-tracking.integrations' ); - if ( $is_gzd_active ) { - $this->initialize_gzd_compat_layer( $c ); - } - - if ( $is_wc_shipment_tracking_active ) { - $this->initialize_wc_shipment_tracking_compat_layer( $c ); - } - - if ( $is_ywot_active ) { - $this->initialize_ywot_compat_layer( $c ); - } - } - - /** - * Sets up the Germanized for WooCommerce - * plugin compatibility layer. - * - * @link https://wordpress.org/plugins/woocommerce-germanized/ - * - * @param ContainerInterface $c The Container. - * @return void - */ - protected function initialize_gzd_compat_layer( ContainerInterface $c ): void { - add_action( - 'woocommerce_gzd_shipment_status_shipped', - function( int $shipment_id, Shipment $shipment ) use ( $c ) { - if ( ! apply_filters( 'woocommerce_paypal_payments_sync_gzd_tracking', true ) ) { - return; - } - - $wc_order = $shipment->get_order(); - - if ( ! is_a( $wc_order, WC_Order::class ) ) { - return; - } - - $order_id = $wc_order->get_id(); - $transaction_id = $wc_order->get_transaction_id(); - $tracking_number = $shipment->get_tracking_id(); - $carrier = $shipment->get_shipping_provider(); - $items = array_map( - function ( ShipmentItem $item ): int { - return $item->get_order_item_id(); - }, - $shipment->get_items() - ); - - if ( ! $tracking_number || ! $carrier || ! $transaction_id ) { - return; - } - - $this->create_tracking( $c, $order_id, $transaction_id, $tracking_number, $carrier, $items ); - }, - 500, - 2 - ); - } - - /** - * Sets up the Shipment Tracking - * plugin compatibility layer. - * - * @link https://woocommerce.com/document/shipment-tracking/ - * - * @param ContainerInterface $c The Container. - * @return void - */ - protected function initialize_wc_shipment_tracking_compat_layer( ContainerInterface $c ): void { - add_action( - 'wp_ajax_wc_shipment_tracking_save_form', - function() use ( $c ) { - check_ajax_referer( 'create-tracking-item', 'security', true ); - - if ( ! apply_filters( 'woocommerce_paypal_payments_sync_wc_shipment_tracking', true ) ) { - return; - } - - $order_id = (int) wc_clean( wp_unslash( $_POST['order_id'] ?? '' ) ); - $wc_order = wc_get_order( $order_id ); - if ( ! is_a( $wc_order, WC_Order::class ) ) { - return; - } - - $transaction_id = $wc_order->get_transaction_id(); - $tracking_number = wc_clean( wp_unslash( $_POST['tracking_number'] ?? '' ) ); - $carrier = wc_clean( wp_unslash( $_POST['tracking_provider'] ?? '' ) ); - $carrier_other = wc_clean( wp_unslash( $_POST['custom_tracking_provider'] ?? '' ) ); - $carrier = $carrier ?: $carrier_other ?: ''; - - if ( ! $tracking_number || ! is_string( $tracking_number ) || ! $carrier || ! is_string( $carrier ) || ! $transaction_id ) { - return; - } - - $this->create_tracking( $c, $order_id, $transaction_id, $tracking_number, $carrier, array() ); - } - ); - - add_filter( - 'woocommerce_rest_prepare_order_shipment_tracking', - function( WP_REST_Response $response, array $tracking_item, WP_REST_Request $request ) use ( $c ): WP_REST_Response { - if ( ! apply_filters( 'woocommerce_paypal_payments_sync_wc_shipment_tracking', true ) ) { - return $response; - } - - $callback = $request->get_attributes()['callback']['1'] ?? ''; - if ( $callback !== 'create_item' ) { - return $response; - } - - $order_id = $tracking_item['order_id'] ?? 0; - $wc_order = wc_get_order( $order_id ); - if ( ! is_a( $wc_order, WC_Order::class ) ) { - return $response; - } - - $transaction_id = $wc_order->get_transaction_id(); - $tracking_number = $tracking_item['tracking_number'] ?? ''; - $carrier = $tracking_item['tracking_provider'] ?? ''; - $carrier_other = $tracking_item['custom_tracking_provider'] ?? ''; - $carrier = $carrier ?: $carrier_other ?: ''; - - if ( ! $tracking_number || ! $carrier || ! $transaction_id ) { - return $response; - } - - $this->create_tracking( $c, $order_id, $transaction_id, $tracking_number, $carrier, array() ); - - return $response; - }, - 10, - 3 - ); - } - - /** - * Sets up the YITH WooCommerce Order & Shipment Tracking - * plugin compatibility layer. - * - * @link https://wordpress.org/plugins/yith-woocommerce-order-tracking/ - * - * @param ContainerInterface $c The Container. - * @return void - */ - protected function initialize_ywot_compat_layer( ContainerInterface $c ): void { - add_action( - 'woocommerce_process_shop_order_meta', - function( int $order_id ) use ( $c ) { - if ( ! apply_filters( 'woocommerce_paypal_payments_sync_ywot_tracking', true ) ) { - return; - } - - $wc_order = wc_get_order( $order_id ); - if ( ! is_a( $wc_order, WC_Order::class ) ) { - return; - } - - $transaction_id = $wc_order->get_transaction_id(); - // phpcs:ignore WordPress.Security.NonceVerification.Missing - $tracking_number = wc_clean( wp_unslash( $_POST['ywot_tracking_code'] ?? '' ) ); - // phpcs:ignore WordPress.Security.NonceVerification.Missing - $carrier = wc_clean( wp_unslash( $_POST['ywot_carrier_name'] ?? '' ) ); - - if ( ! $tracking_number || ! is_string( $tracking_number ) || ! $carrier || ! is_string( $carrier ) || ! $transaction_id ) { - return; - } - - $this->create_tracking( $c, $order_id, $transaction_id, $tracking_number, $carrier, array() ); - }, - 500, - 1 - ); - } - - /** - * Creates PayPal tracking. - * - * @param ContainerInterface $c The Container. - * @param int $wc_order_id The WC order ID. - * @param string $transaction_id The transaction ID. - * @param string $tracking_number The tracking number. - * @param string $carrier The shipment carrier. - * @param int[] $line_items The list of shipment line item IDs. - * @return void - */ - protected function create_tracking( - ContainerInterface $c, - int $wc_order_id, - string $transaction_id, - string $tracking_number, - string $carrier, - array $line_items - ) { - $endpoint = $c->get( 'order-tracking.endpoint.controller' ); - assert( $endpoint instanceof OrderTrackingEndpoint ); - - $logger = $c->get( 'woocommerce.logger.woocommerce' ); - assert( $logger instanceof LoggerInterface ); - - $shipment_factory = $c->get( 'order-tracking.shipment.factory' ); - assert( $shipment_factory instanceof ShipmentFactoryInterface ); - - try { - $ppcp_shipment = $shipment_factory->create_shipment( - $wc_order_id, - $transaction_id, - $tracking_number, - 'SHIPPED', - 'OTHER', - $carrier, - $line_items - ); - - $tracking_information = $endpoint->get_tracking_information( $wc_order_id, $tracking_number ); - - $tracking_information - ? $endpoint->update_tracking_information( $ppcp_shipment, $wc_order_id ) - : $endpoint->add_tracking_information( $ppcp_shipment, $wc_order_id ); - - } catch ( Exception $exception ) { - $logger->error( "Couldn't sync tracking information: " . $exception->getMessage() ); + foreach ( $order_tracking_integrations as $integration ) { + assert( $integration instanceof Integration ); + $integration->integrate(); } } diff --git a/modules/ppcp-compat/src/Integration.php b/modules/ppcp-compat/src/Integration.php new file mode 100644 index 000000000..f8aaf8bc6 --- /dev/null +++ b/modules/ppcp-compat/src/Integration.php @@ -0,0 +1,18 @@ +get( 'googlepay.eligible' ) ) { - return; - } - - // Load the button handler. - $button = $c->get( 'googlepay.button' ); - assert( $button instanceof ButtonInterface ); - $button->initialize(); - - // Show notice if there are product availability issues. - $availability_notice = $c->get( 'googlepay.availability_notice' ); - assert( $availability_notice instanceof AvailabilityNotice ); - $availability_notice->execute(); - - // Check if this merchant can activate / use the buttons. - // We allow non referral merchants as they can potentially still use GooglePay, we just have no way of checking the capability. - if ( ( ! $c->get( 'googlepay.available' ) ) && $c->get( 'googlepay.is_referral' ) ) { - return; - } - - // Initializes button rendering. add_action( - 'wp', - static function () use ( $c, $button ) { - if ( is_admin() ) { - return; - } - $button->render(); - } - ); - - // Enqueue frontend scripts. - add_action( - 'wp_enqueue_scripts', - static function () use ( $c, $button ) { - $smart_button = $c->get( 'button.smart-button' ); - assert( $smart_button instanceof SmartButtonInterface ); - if ( $smart_button->should_load_ppcp_script() ) { - $button->enqueue(); - } - - if ( has_block( 'woocommerce/checkout' ) || has_block( 'woocommerce/cart' ) ) { - /** - * Should add this to the ButtonInterface. - * - * @psalm-suppress UndefinedInterfaceMethod - */ - $button->enqueue_styles(); - } - } - ); - - // Enqueue backend scripts. - add_action( - 'admin_enqueue_scripts', - static function () use ( $c, $button ) { - if ( ! is_admin() ) { - return; - } - /** - * Should add this to the ButtonInterface. - * - * @psalm-suppress UndefinedInterfaceMethod - */ - $button->enqueue_admin(); - } - ); - - // Registers buttons on blocks pages. - add_action( - 'woocommerce_blocks_payment_method_type_registration', - function( PaymentMethodRegistry $payment_method_registry ) use ( $c, $button ): void { - if ( $button->is_enabled() ) { - $payment_method_registry->register( $c->get( 'googlepay.blocks-payment-method' ) ); - } - } - ); - - // Adds GooglePay component to the backend button preview settings. - add_action( - 'woocommerce_paypal_payments_admin_gateway_settings', - function( array $settings ) use ( $c, $button ): array { - if ( is_array( $settings['components'] ) ) { - $settings['components'][] = 'googlepay'; - } - return $settings; - } - ); - - // Initialize AJAX endpoints. - add_action( - 'wc_ajax_' . UpdatePaymentDataEndpoint::ENDPOINT, + 'init', static function () use ( $c ) { - $endpoint = $c->get( 'googlepay.endpoint.update-payment-data' ); - assert( $endpoint instanceof UpdatePaymentDataEndpoint ); - $endpoint->handle_request(); + + // Check if the module is applicable, correct country, currency, ... etc. + if ( ! $c->get( 'googlepay.eligible' ) ) { + return; + } + + // Load the button handler. + $button = $c->get( 'googlepay.button' ); + assert( $button instanceof ButtonInterface ); + $button->initialize(); + + // Show notice if there are product availability issues. + $availability_notice = $c->get( 'googlepay.availability_notice' ); + assert( $availability_notice instanceof AvailabilityNotice ); + $availability_notice->execute(); + + // Check if this merchant can activate / use the buttons. + // We allow non referral merchants as they can potentially still use GooglePay, we just have no way of checking the capability. + if ( ( ! $c->get( 'googlepay.available' ) ) && $c->get( 'googlepay.is_referral' ) ) { + return; + } + + // Initializes button rendering. + add_action( + 'wp', + static function () use ( $c, $button ) { + if ( is_admin() ) { + return; + } + $button->render(); + } + ); + + // Enqueue frontend scripts. + add_action( + 'wp_enqueue_scripts', + static function () use ( $c, $button ) { + $smart_button = $c->get( 'button.smart-button' ); + assert( $smart_button instanceof SmartButtonInterface ); + if ( $smart_button->should_load_ppcp_script() ) { + $button->enqueue(); + } + + if ( has_block( 'woocommerce/checkout' ) || has_block( 'woocommerce/cart' ) ) { + /** + * Should add this to the ButtonInterface. + * + * @psalm-suppress UndefinedInterfaceMethod + */ + $button->enqueue_styles(); + } + } + ); + + // Enqueue backend scripts. + add_action( + 'admin_enqueue_scripts', + static function () use ( $c, $button ) { + if ( ! is_admin() ) { + return; + } + + /** + * Should add this to the ButtonInterface. + * + * @psalm-suppress UndefinedInterfaceMethod + */ + $button->enqueue_admin(); + } + ); + + // Registers buttons on blocks pages. + add_action( + 'woocommerce_blocks_payment_method_type_registration', + function( PaymentMethodRegistry $payment_method_registry ) use ( $c, $button ): void { + if ( $button->is_enabled() ) { + $payment_method_registry->register( $c->get( 'googlepay.blocks-payment-method' ) ); + } + } + ); + + // Adds GooglePay component to the backend button preview settings. + add_action( + 'woocommerce_paypal_payments_admin_gateway_settings', + function( array $settings ) use ( $c ): array { + if ( is_array( $settings['components'] ) ) { + $settings['components'][] = 'googlepay'; + } + return $settings; + } + ); + + // Initialize AJAX endpoints. + add_action( + 'wc_ajax_' . UpdatePaymentDataEndpoint::ENDPOINT, + static function () use ( $c ) { + $endpoint = $c->get( 'googlepay.endpoint.update-payment-data' ); + assert( $endpoint instanceof UpdatePaymentDataEndpoint ); + $endpoint->handle_request(); + } + ); + } ); } diff --git a/modules/ppcp-order-tracking/resources/css/order-edit-page.scss b/modules/ppcp-order-tracking/resources/css/order-edit-page.scss index c9a112ead..fc0c511b2 100644 --- a/modules/ppcp-order-tracking/resources/css/order-edit-page.scss +++ b/modules/ppcp-order-tracking/resources/css/order-edit-page.scss @@ -49,6 +49,8 @@ h4 { display: inline-block; margin: 10px 0px; + max-width: 83%; + overflow: hidden; } button { diff --git a/modules/ppcp-order-tracking/services.php b/modules/ppcp-order-tracking/services.php index 144460571..37154cd65 100644 --- a/modules/ppcp-order-tracking/services.php +++ b/modules/ppcp-order-tracking/services.php @@ -9,15 +9,16 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\OrderTracking; -use WC_Order; -use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; -use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; +use WooCommerce\PayPalCommerce\OrderTracking\Integration\GermanizedShipmentIntegration; +use WooCommerce\PayPalCommerce\OrderTracking\Integration\ShipmentTrackingIntegration; +use WooCommerce\PayPalCommerce\OrderTracking\Integration\ShipStationIntegration; +use WooCommerce\PayPalCommerce\OrderTracking\Integration\WcShippingTaxIntegration; +use WooCommerce\PayPalCommerce\OrderTracking\Integration\YithShipmentIntegration; use WooCommerce\PayPalCommerce\OrderTracking\Shipment\ShipmentFactoryInterface; use WooCommerce\PayPalCommerce\OrderTracking\Shipment\ShipmentFactory; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\OrderTracking\Assets\OrderEditPageAssets; use WooCommerce\PayPalCommerce\OrderTracking\Endpoint\OrderTrackingEndpoint; -use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; return array( 'order-tracking.assets' => function( ContainerInterface $container ) : OrderEditPageAssets { @@ -92,4 +93,39 @@ return array( 'order-tracking.is-merchant-country-us' => static function ( ContainerInterface $container ): bool { return $container->get( 'api.shop.country' ) === 'US'; }, + 'order-tracking.integrations' => static function ( ContainerInterface $container ): array { + $shipment_factory = $container->get( 'order-tracking.shipment.factory' ); + $logger = $container->get( 'woocommerce.logger.woocommerce' ); + $endpoint = $container->get( 'order-tracking.endpoint.controller' ); + + $is_gzd_active = $container->get( 'compat.gzd.is_supported_plugin_version_active' ); + $is_wc_shipment_active = $container->get( 'compat.wc_shipment_tracking.is_supported_plugin_version_active' ); + $is_yith_ywot_active = $container->get( 'compat.ywot.is_supported_plugin_version_active' ); + $is_ship_station_active = $container->get( 'compat.shipstation.is_supported_plugin_version_active' ); + $is_wc_shipping_tax_active = $container->get( 'compat.wc_shipping_tax.is_supported_plugin_version_active' ); + + $integrations = array(); + + if ( $is_gzd_active ) { + $integrations[] = new GermanizedShipmentIntegration( $shipment_factory, $logger, $endpoint ); + } + + if ( $is_wc_shipment_active ) { + $integrations[] = new ShipmentTrackingIntegration( $shipment_factory, $logger, $endpoint ); + } + + if ( $is_yith_ywot_active ) { + $integrations[] = new YithShipmentIntegration( $shipment_factory, $logger, $endpoint ); + } + + if ( $is_ship_station_active ) { + $integrations[] = new ShipStationIntegration( $shipment_factory, $logger, $endpoint ); + } + + if ( $is_wc_shipping_tax_active ) { + $integrations[] = new WcShippingTaxIntegration( $shipment_factory, $logger, $endpoint ); + } + + return $integrations; + }, ); diff --git a/modules/ppcp-order-tracking/src/Integration/GermanizedShipmentIntegration.php b/modules/ppcp-order-tracking/src/Integration/GermanizedShipmentIntegration.php new file mode 100644 index 000000000..7e44fd765 --- /dev/null +++ b/modules/ppcp-order-tracking/src/Integration/GermanizedShipmentIntegration.php @@ -0,0 +1,123 @@ +shipment_factory = $shipment_factory; + $this->logger = $logger; + $this->endpoint = $endpoint; + } + + /** + * {@inheritDoc} + */ + public function integrate(): void { + + add_action( + 'woocommerce_gzd_shipment_status_shipped', + function( int $shipment_id, Shipment $shipment ) { + if ( ! apply_filters( 'woocommerce_paypal_payments_sync_gzd_tracking', true ) ) { + return; + } + + $wc_order = $shipment->get_order(); + + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return; + } + + $wc_order_id = $wc_order->get_id(); + $transaction_id = $wc_order->get_transaction_id(); + $tracking_number = $shipment->get_tracking_id(); + $carrier = $shipment->get_shipping_provider(); + + $items = array_map( + function ( ShipmentItem $item ): int { + return $item->get_order_item_id(); + }, + $shipment->get_items() + ); + + if ( ! $tracking_number || ! $carrier || ! $transaction_id ) { + return; + } + + try { + $ppcp_shipment = $this->shipment_factory->create_shipment( + $wc_order_id, + $transaction_id, + $tracking_number, + 'SHIPPED', + 'OTHER', + $carrier, + $items + ); + + $tracking_information = $this->endpoint->get_tracking_information( $wc_order_id, $tracking_number ); + + $tracking_information + ? $this->endpoint->update_tracking_information( $ppcp_shipment, $wc_order_id ) + : $this->endpoint->add_tracking_information( $ppcp_shipment, $wc_order_id ); + + } catch ( Exception $exception ) { + $this->logger->error( "Couldn't sync tracking information: " . $exception->getMessage() ); + } + }, + 500, + 2 + ); + } +} diff --git a/modules/ppcp-order-tracking/src/Integration/ShipStationIntegration.php b/modules/ppcp-order-tracking/src/Integration/ShipStationIntegration.php new file mode 100644 index 000000000..8d294f724 --- /dev/null +++ b/modules/ppcp-order-tracking/src/Integration/ShipStationIntegration.php @@ -0,0 +1,117 @@ +shipment_factory = $shipment_factory; + $this->logger = $logger; + $this->endpoint = $endpoint; + } + + /** + * {@inheritDoc} + */ + public function integrate(): void { + + add_action( + 'woocommerce_shipstation_shipnotify', + /** + * Param type for $wc_order can be different. + * + * @psalm-suppress MissingClosureParamType + */ + function( $wc_order, array $data ) { + if ( ! apply_filters( 'woocommerce_paypal_payments_sync_ship_station_tracking', true ) ) { + return; + } + + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return; + } + + $order_id = $wc_order->get_id(); + $transaction_id = $wc_order->get_transaction_id(); + $tracking_number = $data['tracking_number'] ?? ''; + $carrier = $data['carrier'] ?? ''; + + if ( ! $tracking_number || ! is_string( $tracking_number ) || ! $carrier || ! is_string( $carrier ) || ! $transaction_id ) { + return; + } + + try { + $ppcp_shipment = $this->shipment_factory->create_shipment( + $order_id, + $transaction_id, + $tracking_number, + 'SHIPPED', + 'OTHER', + $carrier, + array() + ); + + $tracking_information = $this->endpoint->get_tracking_information( $order_id, $tracking_number ); + + $tracking_information + ? $this->endpoint->update_tracking_information( $ppcp_shipment, $order_id ) + : $this->endpoint->add_tracking_information( $ppcp_shipment, $order_id ); + + } catch ( Exception $exception ) { + $this->logger->error( "Couldn't sync tracking information: " . $exception->getMessage() ); + } + }, + 500, + 1 + ); + } +} diff --git a/modules/ppcp-order-tracking/src/Integration/ShipmentTrackingIntegration.php b/modules/ppcp-order-tracking/src/Integration/ShipmentTrackingIntegration.php new file mode 100644 index 000000000..6c506a5e8 --- /dev/null +++ b/modules/ppcp-order-tracking/src/Integration/ShipmentTrackingIntegration.php @@ -0,0 +1,174 @@ +shipment_factory = $shipment_factory; + $this->logger = $logger; + $this->endpoint = $endpoint; + } + + /** + * {@inheritDoc} + */ + public function integrate(): void { + + add_action( + 'wp_ajax_wc_shipment_tracking_save_form', + function() { + check_ajax_referer( 'create-tracking-item', 'security', true ); + + if ( ! apply_filters( 'woocommerce_paypal_payments_sync_wc_shipment_tracking', true ) ) { + return; + } + + $order_id = (int) wc_clean( wp_unslash( $_POST['order_id'] ?? '' ) ); + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return; + } + + $transaction_id = $wc_order->get_transaction_id(); + $tracking_number = wc_clean( wp_unslash( $_POST['tracking_number'] ?? '' ) ); + $carrier = wc_clean( wp_unslash( $_POST['tracking_provider'] ?? '' ) ); + $carrier_other = wc_clean( wp_unslash( $_POST['custom_tracking_provider'] ?? '' ) ); + $carrier = $carrier ?: $carrier_other ?: ''; + + if ( ! $tracking_number || ! is_string( $tracking_number ) || ! $carrier || ! is_string( $carrier ) || ! $transaction_id ) { + return; + } + + $this->sync_tracking( $order_id, $transaction_id, $tracking_number, $carrier ); + } + ); + + /** + * Support the case when tracking is added via REST. + */ + add_filter( + 'woocommerce_rest_prepare_order_shipment_tracking', + function( WP_REST_Response $response, array $tracking_item, WP_REST_Request $request ): WP_REST_Response { + if ( ! apply_filters( 'woocommerce_paypal_payments_sync_wc_shipment_tracking', true ) ) { + return $response; + } + + $callback = $request->get_attributes()['callback']['1'] ?? ''; + if ( $callback !== 'create_item' ) { + return $response; + } + + $order_id = $tracking_item['order_id'] ?? 0; + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return $response; + } + + $transaction_id = $wc_order->get_transaction_id(); + $tracking_number = $tracking_item['tracking_number'] ?? ''; + $carrier = $tracking_item['tracking_provider'] ?? ''; + $carrier_other = $tracking_item['custom_tracking_provider'] ?? ''; + $carrier = $carrier ?: $carrier_other ?: ''; + + if ( ! $tracking_number || ! $carrier || ! $transaction_id ) { + return $response; + } + + $this->sync_tracking( $order_id, $transaction_id, $tracking_number, $carrier ); + + return $response; + }, + 10, + 3 + ); + } + + /** + * Syncs (add | update) the PayPal tracking with given info. + * + * @param int $wc_order_id The WC order ID. + * @param string $transaction_id The transaction ID. + * @param string $tracking_number The tracking number. + * @param string $carrier The shipment carrier. + * @return void + */ + protected function sync_tracking( + int $wc_order_id, + string $transaction_id, + string $tracking_number, + string $carrier + ) { + try { + $ppcp_shipment = $this->shipment_factory->create_shipment( + $wc_order_id, + $transaction_id, + $tracking_number, + 'SHIPPED', + 'OTHER', + $carrier, + array() + ); + + $tracking_information = $this->endpoint->get_tracking_information( $wc_order_id, $tracking_number ); + + $tracking_information + ? $this->endpoint->update_tracking_information( $ppcp_shipment, $wc_order_id ) + : $this->endpoint->add_tracking_information( $ppcp_shipment, $wc_order_id ); + + } catch ( Exception $exception ) { + $this->logger->error( "Couldn't sync tracking information: " . $exception->getMessage() ); + } + } +} diff --git a/modules/ppcp-order-tracking/src/Integration/WcShippingTaxIntegration.php b/modules/ppcp-order-tracking/src/Integration/WcShippingTaxIntegration.php new file mode 100644 index 000000000..a808c6ade --- /dev/null +++ b/modules/ppcp-order-tracking/src/Integration/WcShippingTaxIntegration.php @@ -0,0 +1,159 @@ +shipment_factory = $shipment_factory; + $this->logger = $logger; + $this->endpoint = $endpoint; + } + + /** + * {@inheritDoc} + */ + public function integrate(): void { + + add_filter( + 'rest_post_dispatch', + function( WP_HTTP_Response $response, WP_REST_Server $server, WP_REST_Request $request ): WP_HTTP_Response { + if ( ! apply_filters( 'woocommerce_paypal_payments_sync_wc_shipping_tax', true ) ) { + return $response; + } + + $params = $request->get_params(); + $order_id = (int) ( $params['order_id'] ?? 0 ); + $label_id = (int) ( $params['label_ids'] ?? 0 ); + + if ( ! $order_id || "/wc/v1/connect/label/{$order_id}/{$label_id}" !== $request->get_route() ) { + return $response; + } + + $data = $response->get_data() ?? array(); + $labels = $data['labels'] ?? array(); + + foreach ( $labels as $label ) { + $tracking_number = $label['tracking'] ?? ''; + if ( ! $tracking_number ) { + continue; + } + + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + continue; + } + + $transaction_id = $wc_order->get_transaction_id(); + $carrier = $label['carrier_id'] ?? $label['service_name'] ?? ''; + $items = array_map( 'intval', $label['product_ids'] ?? array() ); + + if ( ! $carrier || ! $transaction_id ) { + continue; + } + + $this->sync_tracking( $order_id, $transaction_id, $tracking_number, $carrier, $items ); + } + + return $response; + }, + 10, + 3 + ); + + } + + /** + * Syncs (add | update) the PayPal tracking with given info. + * + * @param int $wc_order_id The WC order ID. + * @param string $transaction_id The transaction ID. + * @param string $tracking_number The tracking number. + * @param string $carrier The shipment carrier. + * @param int[] $items The list of line items IDs. + * @return void + */ + protected function sync_tracking( + int $wc_order_id, + string $transaction_id, + string $tracking_number, + string $carrier, + array $items + ) { + try { + $ppcp_shipment = $this->shipment_factory->create_shipment( + $wc_order_id, + $transaction_id, + $tracking_number, + 'SHIPPED', + 'OTHER', + $carrier, + $items + ); + + $tracking_information = $this->endpoint->get_tracking_information( $wc_order_id, $tracking_number ); + + $tracking_information + ? $this->endpoint->update_tracking_information( $ppcp_shipment, $wc_order_id ) + : $this->endpoint->add_tracking_information( $ppcp_shipment, $wc_order_id ); + + } catch ( Exception $exception ) { + $this->logger->error( "Couldn't sync tracking information: " . $exception->getMessage() ); + } + } +} diff --git a/modules/ppcp-order-tracking/src/Integration/YithShipmentIntegration.php b/modules/ppcp-order-tracking/src/Integration/YithShipmentIntegration.php new file mode 100644 index 000000000..57a36babf --- /dev/null +++ b/modules/ppcp-order-tracking/src/Integration/YithShipmentIntegration.php @@ -0,0 +1,114 @@ +shipment_factory = $shipment_factory; + $this->logger = $logger; + $this->endpoint = $endpoint; + } + + /** + * {@inheritDoc} + */ + public function integrate(): void { + + add_action( + 'woocommerce_process_shop_order_meta', + function( int $order_id ) { + if ( ! apply_filters( 'woocommerce_paypal_payments_sync_ywot_tracking', true ) ) { + return; + } + + $wc_order = wc_get_order( $order_id ); + if ( ! is_a( $wc_order, WC_Order::class ) ) { + return; + } + + $transaction_id = $wc_order->get_transaction_id(); + // phpcs:ignore WordPress.Security.NonceVerification.Missing + $tracking_number = wc_clean( wp_unslash( $_POST['ywot_tracking_code'] ?? '' ) ); + // phpcs:ignore WordPress.Security.NonceVerification.Missing + $carrier = wc_clean( wp_unslash( $_POST['ywot_carrier_name'] ?? '' ) ); + + if ( ! $tracking_number || ! is_string( $tracking_number ) || ! $carrier || ! is_string( $carrier ) || ! $transaction_id ) { + return; + } + + try { + $ppcp_shipment = $this->shipment_factory->create_shipment( + $order_id, + $transaction_id, + $tracking_number, + 'SHIPPED', + 'OTHER', + $carrier, + array() + ); + + $tracking_information = $this->endpoint->get_tracking_information( $order_id, $tracking_number ); + + $tracking_information + ? $this->endpoint->update_tracking_information( $ppcp_shipment, $order_id ) + : $this->endpoint->add_tracking_information( $ppcp_shipment, $order_id ); + + } catch ( Exception $exception ) { + $this->logger->error( "Couldn't sync tracking information: " . $exception->getMessage() ); + } + }, + 500, + 1 + ); + } +} diff --git a/modules/ppcp-order-tracking/src/OrderTrackingModule.php b/modules/ppcp-order-tracking/src/OrderTrackingModule.php index 94dc1ffd0..ae6bfecc2 100644 --- a/modules/ppcp-order-tracking/src/OrderTrackingModule.php +++ b/modules/ppcp-order-tracking/src/OrderTrackingModule.php @@ -102,7 +102,7 @@ class OrderTrackingModule implements ModuleInterface { __( 'PayPal Package Tracking', 'woocommerce-paypal-payments' ), array( $meta_box_renderer, 'render' ), $screen, - 'normal' + 'side' ); }, 10, diff --git a/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php index a4d49e3e9..a979618bc 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php @@ -171,13 +171,17 @@ class CardButtonGateway extends \WC_Payment_Gateway { $this->payment_token_repository = $payment_token_repository; $this->logger = $logger; - if ( $this->onboarded ) { - $this->supports = array( 'refunds' ); - } - if ( $this->gateways_enabled() ) { - $this->supports = array( - 'refunds', - 'products', + $this->supports = array( + 'refunds', + 'products', + ); + + if ( + ( $this->config->has( 'vault_enabled' ) && $this->config->get( 'vault_enabled' ) ) + || ( $this->config->has( 'subscriptions_mode' ) && $this->config->get( 'subscriptions_mode' ) === 'subscriptions_api' ) + ) { + array_push( + $this->supports, 'subscriptions', 'subscription_cancellation', 'subscription_suspension', @@ -187,7 +191,7 @@ class CardButtonGateway extends \WC_Payment_Gateway { 'subscription_payment_method_change', 'subscription_payment_method_change_customer', 'subscription_payment_method_change_admin', - 'multiple_subscriptions', + 'multiple_subscriptions' ); } diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index 7f752ef18..3cf401b29 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -18,21 +18,6 @@ use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; * Trait ProcessPaymentTrait */ trait ProcessPaymentTrait { - /** - * Checks if PayPal or Credit Card gateways are enabled. - * - * @return bool Whether any of the gateways is enabled. - */ - protected function gateways_enabled(): bool { - if ( $this->config->has( 'enabled' ) && $this->config->get( 'enabled' ) ) { - return true; - } - if ( $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' ) ) { - return true; - } - return false; - } - /** * Handles the payment failure. * diff --git a/readme.txt b/readme.txt index 8e13325fd..7df738d23 100644 --- a/readme.txt +++ b/readme.txt @@ -180,7 +180,7 @@ If you encounter issues with the PayPal buttons not appearing after an update, p == Changelog == -= 2.4.0 - xxxx-xx-xx = += 2.4.0 - 2023-10-31 = * Fix - Mini-Cart Bug cause of wrong DOM-Structure in v2.3.1 #1735 * Fix - ACDC disappearing after plugin updates #1751 * Fix - Subscription module hooks #1748 diff --git a/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php b/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php index 64b74d91d..427e9ce79 100644 --- a/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php +++ b/tests/PHPUnit/Button/Endpoint/ChangeCartEndpointTest.php @@ -6,6 +6,7 @@ namespace WooCommerce\PayPalCommerce\Button\Endpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit; use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory; +use WooCommerce\PayPalCommerce\Button\Helper\CartProductsHelper; use WooCommerce\PayPalCommerce\TestCase; use Mockery; use WooCommerce\WooCommerce\Logging\Logger\NullLogger; @@ -91,12 +92,16 @@ class ChangeCartEndpointTest extends TestCase ->expects('from_wc_cart') ->andReturn($pu); + $productsHelper = new CartProductsHelper( + $dataStore + ); + $testee = new ChangeCartEndpoint( $cart, $shipping, $requestData, $purchase_unit_factory, - $dataStore, + $productsHelper, new NullLogger() );