diff --git a/changelog/fix-4444-payment-request-bookable-product-support b/changelog/fix-4444-payment-request-bookable-product-support new file mode 100644 index 00000000000..0a471e21e50 --- /dev/null +++ b/changelog/fix-4444-payment-request-bookable-product-support @@ -0,0 +1,4 @@ +Significance: minor +Type: add + +Add support for bookable products to payment request buttons on product pages. diff --git a/client/checkout/api/index.js b/client/checkout/api/index.js index 3ccaa9026a8..4629e96bf8c 100644 --- a/client/checkout/api/index.js +++ b/client/checkout/api/index.js @@ -495,6 +495,19 @@ export default class WCPayAPI { } ); } + /** + * Empty the cart. + * + * @param {number} bookingId Booking ID (optional). + * @return {Promise} Promise for the request to the server. + */ + paymentRequestEmptyCart( bookingId ) { + return this.request( getPaymentRequestAjaxURL( 'empty_cart' ), { + security: getPaymentRequestData( 'nonce' )?.empty_cart, + booking_id: bookingId, + } ); + } + /** * Get selected product data from variable product page. * diff --git a/client/payment-request/index.js b/client/payment-request/index.js index 698ac1a5810..eee6252bd96 100644 --- a/client/payment-request/index.js +++ b/client/payment-request/index.js @@ -179,6 +179,10 @@ jQuery( ( $ ) => { .val(); } + if ( $( '.wc-bookings-booking-form' ).length ) { + productId = $( '.wc-booking-product-id' ).val(); + } + const data = { product_id: productId, qty: $( '.quantity .qty' ).val(), @@ -187,10 +191,10 @@ jQuery( ( $ ) => { : [], }; - // Add addons data to the POST body + // Add extension data to the POST body const formData = $( 'form.cart' ).serializeArray(); $.each( formData, ( i, field ) => { - if ( /^addon-/.test( field.name ) ) { + if ( /^(addon-|wc_)/.test( field.name ) ) { if ( /\[\]$/.test( field.name ) ) { const fieldName = field.name.substring( 0, @@ -295,6 +299,10 @@ jQuery( ( $ ) => { .val(); } + if ( $( '.wc-bookings-booking-form' ).length ) { + productId = $( '.wc-booking-product-id' ).val(); + } + const addons = $( '#product-addons-total' ).data( 'price_data' ) || []; const addonValue = addons.reduce( @@ -616,4 +624,40 @@ jQuery( ( $ ) => { $( document.body ).on( 'updated_checkout', () => { wcpayPaymentRequest.init(); } ); + + // Handle bookable products on the product page. + let wcBookingFormChanged = false; + + $( document.body ) + .off( 'wc_booking_form_changed' ) + .on( 'wc_booking_form_changed', () => { + wcBookingFormChanged = true; + } ); + + // Listen for the WC Bookings wc_bookings_calculate_costs event to complete + // and add the bookable product to the cart, using the response to update the + // payment request request params with correct totals. + $( document ).ajaxComplete( function ( event, xhr, settings ) { + if ( wcBookingFormChanged ) { + if ( + settings.url === window.booking_form_params.ajax_url && + settings.data.includes( 'wc_bookings_calculate_costs' ) && + xhr.responseText.includes( 'SUCCESS' ) + ) { + wcpayPaymentRequest.blockPaymentRequestButton(); + wcBookingFormChanged = false; + return wcpayPaymentRequest.addToCart().then( ( response ) => { + wcpayPaymentRequestParams.product.total = response.total; + wcpayPaymentRequestParams.product.displayItems = + response.displayItems; + // Empty the cart to avoid having 2 products in the cart when payment request is not used. + api.paymentRequestEmptyCart( response.bookingId ); + + wcpayPaymentRequest.init(); + + wcpayPaymentRequest.unblockPaymentRequestButton(); + } ); + } + } + } ); } ); diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index 346cb7529a7..7c2b2a254af 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -700,6 +700,7 @@ public function scripts() { 'update_shipping' => wp_create_nonce( 'wcpay-update-shipping-method' ), 'checkout' => wp_create_nonce( 'woocommerce-process_checkout' ), 'add_to_cart' => wp_create_nonce( 'wcpay-add-to-cart' ), + 'empty_cart' => wp_create_nonce( 'wcpay-empty-cart' ), 'get_selected_product_data' => wp_create_nonce( 'wcpay-get-selected-product-data' ), 'platform_tracker' => wp_create_nonce( 'platform_tracks_nonce' ), 'pay_for_order' => wp_create_nonce( 'pay_for_order' ), diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php index a153136a9d8..d2bacd56084 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php @@ -64,6 +64,7 @@ public function __construct( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Paym if ( $is_woopay_enabled || $is_payment_request_enabled ) { add_action( 'wc_ajax_wcpay_add_to_cart', [ $this->express_checkout_helper, 'ajax_add_to_cart' ] ); + add_action( 'wc_ajax_wcpay_empty_cart', [ $this->express_checkout_helper, 'ajax_empty_cart' ] ); add_action( 'woocommerce_after_add_to_cart_form', [ $this, 'display_express_checkout_buttons' ], 1 ); add_action( 'woocommerce_proceed_to_checkout', [ $this, 'display_express_checkout_buttons' ], 21 ); diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php index 80551350144..4c42635d102 100644 --- a/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php @@ -95,19 +95,65 @@ public function ajax_add_to_cart() { WC()->cart->add_to_cart( $product->get_id(), $quantity, $variation_id, $attributes ); } - if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation', 'bundle', 'mix-and-match' ], true ) ) { + if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation', 'booking', 'bundle', 'mix-and-match' ], true ) ) { WC()->cart->add_to_cart( $product->get_id(), $quantity ); } WC()->cart->calculate_totals(); + if ( 'booking' === $product_type ) { + $booking_id = $this->get_booking_id_from_cart(); + } + $data = []; $data += $this->build_display_items(); $data['result'] = 'success'; + if ( $booking_id ) { + $data['bookingId'] = $booking_id; + } + wp_send_json( $data ); } + /** + * Gets the booking id from the cart. + * It's expected that the cart only contains one item which was added via ajax_add_to_cart. + * Used to remove the booking from WC Bookings in-cart status. + * + * @return int|false + */ + public function get_booking_id_from_cart() { + $cart = WC()->cart->get_cart(); + $cart_item = reset( $cart ); + + if ( $cart_item && isset( $cart_item['booking']['_booking_id'] ) ) { + return $cart_item['booking']['_booking_id']; + } + + return false; + } + + /** + * Empties the cart via AJAX. Used on the product page. + */ + public function ajax_empty_cart() { + check_ajax_referer( 'wcpay-empty-cart', 'security' ); + + $booking_id = isset( $_POST['booking_id'] ) ? absint( $_POST['booking_id'] ) : null; + + WC()->cart->empty_cart(); + + if ( $booking_id ) { + // When a bookable product is added to the cart, a 'booking' is create with status 'in-cart'. + // This status is used to prevent the booking from being booked by another customer + // and should be removed when the cart is emptied for PRB purposes. + do_action( 'wc-booking-remove-inactive-cart', $booking_id ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores + } + + wp_send_json( [ 'result' => 'success' ] ); + } + /** * Builds the line items to pass to Payment Request *