diff --git a/changelog/7588-express-checkout-utilities b/changelog/7588-express-checkout-utilities new file mode 100644 index 00000000000..143b670d60c --- /dev/null +++ b/changelog/7588-express-checkout-utilities @@ -0,0 +1,4 @@ +Significance: patch +Type: fix + +Introduce WC_Payments_Express_Checkout_Button_Utils class. diff --git a/includes/class-wc-payments-express-checkout-button-display-handler.php b/includes/class-wc-payments-express-checkout-button-display-handler.php index ad8db375684..5a9cf2ec12c 100644 --- a/includes/class-wc-payments-express-checkout-button-display-handler.php +++ b/includes/class-wc-payments-express-checkout-button-display-handler.php @@ -35,17 +35,26 @@ class WC_Payments_Express_Checkout_Button_Display_Handler { */ private $platform_checkout_button_handler; + /** + * Express Checkout Helper instance. + * + * @var WC_Payments_Express_Checkout_Button_Helper + */ + private $express_checkout_helper; + /** * Initialize class actions. * * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway. * @param WC_Payments_Payment_Request_Button_Handler $payment_request_button_handler Payment request button handler. * @param WC_Payments_WooPay_Button_Handler $platform_checkout_button_handler Platform checkout button handler. + * @param WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper Express checkout helper. */ - public function __construct( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Payment_Request_Button_Handler $payment_request_button_handler, WC_Payments_WooPay_Button_Handler $platform_checkout_button_handler ) { + public function __construct( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Payment_Request_Button_Handler $payment_request_button_handler, WC_Payments_WooPay_Button_Handler $platform_checkout_button_handler, WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper ) { $this->gateway = $gateway; $this->payment_request_button_handler = $payment_request_button_handler; $this->platform_checkout_button_handler = $platform_checkout_button_handler; + $this->express_checkout_helper = $express_checkout_helper; $this->platform_checkout_button_handler->init(); $this->payment_request_button_handler->init(); @@ -54,6 +63,8 @@ public function __construct( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Paym $is_payment_request_enabled = 'yes' === $this->gateway->get_option( 'payment_request' ); 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( 'woocommerce_after_add_to_cart_form', [ $this, 'display_express_checkout_buttons' ], 1 ); add_action( 'woocommerce_proceed_to_checkout', [ $this, 'display_express_checkout_buttons' ], 21 ); add_action( 'woocommerce_checkout_before_customer_details', [ $this, 'display_express_checkout_buttons' ], 1 ); diff --git a/includes/class-wc-payments-payment-request-button-handler.php b/includes/class-wc-payments-payment-request-button-handler.php index ff030924969..7a30adad550 100644 --- a/includes/class-wc-payments-payment-request-button-handler.php +++ b/includes/class-wc-payments-payment-request-button-handler.php @@ -36,15 +36,24 @@ class WC_Payments_Payment_Request_Button_Handler { */ private $gateway; + /** + * Express Checkout Helper instance. + * + * @var WC_Payments_Express_Checkout_Button_Helper + */ + private $express_checkout_helper; + /** * Initialize class actions. * - * @param WC_Payments_Account $account Account information. - * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway. + * @param WC_Payments_Account $account Account information. + * @param WC_Payment_Gateway_WCPay $gateway WCPay gateway. + * @param WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper Express checkout helper. */ - public function __construct( WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway ) { - $this->account = $account; - $this->gateway = $gateway; + public function __construct( WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper ) { + $this->account = $account; + $this->gateway = $gateway; + $this->express_checkout_helper = $express_checkout_helper; } /** @@ -78,7 +87,6 @@ public function init() { add_action( 'wc_ajax_wcpay_get_shipping_options', [ $this, 'ajax_get_shipping_options' ] ); add_action( 'wc_ajax_wcpay_update_shipping_method', [ $this, 'ajax_update_shipping_method' ] ); add_action( 'wc_ajax_wcpay_create_order', [ $this, 'ajax_create_order' ] ); - add_action( 'wc_ajax_wcpay_add_to_cart', [ $this, 'ajax_add_to_cart' ] ); add_action( 'wc_ajax_wcpay_get_selected_product_data', [ $this, 'ajax_get_selected_product_data' ] ); add_action( 'wc_ajax_wcpay_pay_for_order', [ $this, 'ajax_pay_for_order' ] ); @@ -134,17 +142,6 @@ public function is_account_creation_possible() { ); } - /** - * Gets total label. - * - * @return string - */ - public function get_total_label() { - // Get statement descriptor from API/cached account data. - $statement_descriptor = $this->account->get_statement_descriptor(); - return str_replace( "'", '', $statement_descriptor ) . apply_filters( 'wcpay_payment_request_total_label_suffix', ' (via WooCommerce)' ); - } - /** * Sets the WC customer session if one is not set. * This is needed so nonces can be verified by AJAX Request. @@ -211,7 +208,7 @@ public function get_button_height() { */ public function get_product_price( $product ) { // If prices should include tax, using tax inclusive price. - if ( $this->cart_prices_include_tax() ) { + if ( $this->express_checkout_helper->cart_prices_include_tax() ) { $base_price = wc_get_price_including_tax( $product ); } else { $base_price = wc_get_price_excluding_tax( $product ); @@ -319,7 +316,7 @@ public function get_product_data() { $data['displayItems'] = $items; $data['total'] = [ - 'label' => apply_filters( 'wcpay_payment_request_total_label', $this->get_total_label() ), + 'label' => apply_filters( 'wcpay_payment_request_total_label', $this->express_checkout_helper->get_total_label() ), 'amount' => WC_Payments_Utils::prepare_amount( $price + $total_tax, $currency ), 'pending' => true, ]; @@ -382,7 +379,7 @@ public function display_pay_for_order_page_html( $order ) { $data['displayItems'] = $items; $data['needs_shipping'] = false; // This should be already entered/prepared. $data['total'] = [ - 'label' => apply_filters( 'wcpay_payment_request_total_label', $this->get_total_label() ), + 'label' => apply_filters( 'wcpay_payment_request_total_label', $this->express_checkout_helper->get_total_label() ), 'amount' => WC_Payments_Utils::prepare_amount( $order->get_total(), $currency ), 'pending' => true, ]; @@ -400,7 +397,7 @@ public function get_cart_data() { return false; } - return $this->build_display_items(); + return $this->express_checkout_helper->build_display_items(); } /** @@ -802,7 +799,7 @@ public function scripts() { 'is_pay_for_order' => $this->is_pay_for_order_page(), 'has_block' => has_block( 'woocommerce/cart' ) || has_block( 'woocommerce/checkout' ), 'product' => $this->get_product_data(), - 'total_label' => $this->get_total_label(), + 'total_label' => $this->express_checkout_helper->get_total_label(), ]; WC_Payments::register_script_with_dependencies( 'WCPAY_PAYMENT_REQUEST', 'dist/payment-request', [ 'jquery', 'stripe' ] ); @@ -890,7 +887,7 @@ public function ajax_get_cart_details() { WC()->cart->calculate_totals(); - wp_send_json( array_merge( $this->build_display_items(), [ 'needs_shipping' => WC()->cart->needs_shipping() ] ) ); + wp_send_json( array_merge( $this->express_checkout_helper->build_display_items(), [ 'needs_shipping' => WC()->cart->needs_shipping() ] ) ); } /** @@ -986,10 +983,10 @@ public function get_shipping_options( $shipping_address, $itemized_display_items WC()->cart->calculate_totals(); - $data += $this->build_display_items( $itemized_display_items ); + $data += $this->express_checkout_helper->build_display_items( $itemized_display_items ); $data['result'] = 'success'; } catch ( Exception $e ) { - $data += $this->build_display_items( $itemized_display_items ); + $data += $this->express_checkout_helper->build_display_items( $itemized_display_items ); $data['result'] = 'invalid_shipping_address'; } @@ -1015,7 +1012,7 @@ public function ajax_update_shipping_method() { $should_show_itemized_view = ! isset( $product_view_options['is_product_page'] ) ? true : filter_var( $product_view_options['is_product_page'], FILTER_VALIDATE_BOOLEAN ); $data = []; - $data += $this->build_display_items( $should_show_itemized_view ); + $data += $this->express_checkout_helper->build_display_items( $should_show_itemized_view ); $data['result'] = 'success'; wp_send_json( $data ); @@ -1121,7 +1118,7 @@ public function ajax_get_selected_product_data() { $data['displayItems'] = $items; $data['total'] = [ - 'label' => $this->get_total_label(), + 'label' => $this->express_checkout_helper->get_total_label(), 'amount' => WC_Payments_Utils::prepare_amount( $total + $total_tax, $currency ), 'pending' => true, ]; @@ -1139,62 +1136,6 @@ public function ajax_get_selected_product_data() { } } - /** - * Adds the current product to the cart. Used on product detail page. - */ - public function ajax_add_to_cart() { - check_ajax_referer( 'wcpay-add-to-cart', 'security' ); - - if ( ! defined( 'WOOCOMMERCE_CART' ) ) { - define( 'WOOCOMMERCE_CART', true ); - } - - WC()->shipping->reset_shipping(); - - $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; - $product = wc_get_product( $product_id ); - - if ( ! $product ) { - wp_send_json( - [ - 'error' => [ - 'code' => 'invalid_product_id', - 'message' => __( 'Invalid product id', 'woocommerce-payments' ), - ], - ], - 404 - ); - return; - } - - $qty = ! isset( $_POST['qty'] ) ? 1 : absint( $_POST['qty'] ); - $product_type = $product->get_type(); - - // First empty the cart to prevent wrong calculation. - WC()->cart->empty_cart(); - - if ( ( 'variable' === $product_type || 'variable-subscription' === $product_type ) && isset( $_POST['attributes'] ) ) { - $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) ); - - $data_store = WC_Data_Store::load( 'product' ); - $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); - - WC()->cart->add_to_cart( $product->get_id(), $qty, $variation_id, $attributes ); - } - - if ( in_array( $product_type, [ 'simple', 'variation', 'subscription', 'subscription_variation' ], true ) ) { - WC()->cart->add_to_cart( $product->get_id(), $qty ); - } - - WC()->cart->calculate_totals(); - - $data = []; - $data += $this->build_display_items(); - $data['result'] = 'success'; - - wp_send_json( $data ); - } - /** * Handles payment requests on the Pay for Order page. * @@ -1510,16 +1451,6 @@ protected function calculate_shipping( $address = [] ) { WC()->shipping->calculate_shipping( $packages ); } - /** - * Whether tax should be displayed on separate line in cart. - * returns true if tax is disabled or display of tax in checkout is set to inclusive. - * - * @return boolean - */ - private function cart_prices_include_tax() { - return ! wc_tax_enabled() || 'incl' === get_option( 'woocommerce_tax_display_cart' ); - } - /** * Builds the shipping methods to pass to Payment Request * @@ -1544,103 +1475,6 @@ protected function build_shipping_methods( $shipping_methods ) { return $shipping; } - /** - * Builds the line items to pass to Payment Request - * - * @param boolean $itemized_display_items Indicates whether to show subtotals or itemized views. - */ - public function build_display_items( $itemized_display_items = false ) { - if ( ! defined( 'WOOCOMMERCE_CART' ) ) { - define( 'WOOCOMMERCE_CART', true ); - } - - $items = []; - $subtotal = 0; - $discounts = 0; - $currency = get_woocommerce_currency(); - - // Default show only subtotal instead of itemization. - if ( ! apply_filters( 'wcpay_payment_request_hide_itemization', true ) || $itemized_display_items ) { - foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - $amount = $cart_item['line_subtotal']; - $subtotal += $cart_item['line_subtotal']; - $quantity_label = 1 < $cart_item['quantity'] ? ' (x' . $cart_item['quantity'] . ')' : ''; - - $product_name = $cart_item['data']->get_name(); - - $item_tax = $this->cart_prices_include_tax() ? ( $cart_item['line_subtotal_tax'] ?? 0 ) : 0; - - $item = [ - 'label' => $product_name . $quantity_label, - 'amount' => WC_Payments_Utils::prepare_amount( $amount + $item_tax, $currency ), - ]; - - $items[] = $item; - } - } - - if ( version_compare( WC_VERSION, '3.2', '<' ) ) { - $discounts = wc_format_decimal( WC()->cart->get_cart_discount_total(), WC()->cart->dp ); - } else { - $applied_coupons = array_values( WC()->cart->get_coupon_discount_totals() ); - - foreach ( $applied_coupons as $amount ) { - $discounts += (float) $amount; - } - } - - $discounts = wc_format_decimal( $discounts, WC()->cart->dp ); - $tax = wc_format_decimal( WC()->cart->tax_total + WC()->cart->shipping_tax_total, WC()->cart->dp ); - $shipping = wc_format_decimal( WC()->cart->shipping_total, WC()->cart->dp ); - $items_total = wc_format_decimal( WC()->cart->cart_contents_total, WC()->cart->dp ) + $discounts; - $order_total = version_compare( WC_VERSION, '3.2', '<' ) ? wc_format_decimal( $items_total + $tax + $shipping - $discounts, WC()->cart->dp ) : WC()->cart->get_total( '' ); - - if ( ! $this->cart_prices_include_tax() ) { - $items[] = [ - 'label' => esc_html( __( 'Tax', 'woocommerce-payments' ) ), - 'amount' => WC_Payments_Utils::prepare_amount( $tax, $currency ), - ]; - } - - if ( WC()->cart->needs_shipping() ) { - $shipping_tax = $this->cart_prices_include_tax() ? WC()->cart->shipping_tax_total : 0; - $items[] = [ - 'label' => esc_html( __( 'Shipping', 'woocommerce-payments' ) ), - 'amount' => WC_Payments_Utils::prepare_amount( $shipping + $shipping_tax, $currency ), - ]; - } - - if ( WC()->cart->has_discount() ) { - $items[] = [ - 'label' => esc_html( __( 'Discount', 'woocommerce-payments' ) ), - 'amount' => WC_Payments_Utils::prepare_amount( $discounts, $currency ), - ]; - } - - if ( version_compare( WC_VERSION, '3.2', '<' ) ) { - $cart_fees = WC()->cart->fees; - } else { - $cart_fees = WC()->cart->get_fees(); - } - - // Include fees and taxes as display items. - foreach ( $cart_fees as $key => $fee ) { - $items[] = [ - 'label' => $fee->name, - 'amount' => WC_Payments_Utils::prepare_amount( $fee->amount, $currency ), - ]; - } - - return [ - 'displayItems' => $items, - 'total' => [ - 'label' => $this->get_total_label(), - 'amount' => max( 0, apply_filters( 'wcpay_calculated_total', WC_Payments_Utils::prepare_amount( $order_total, $currency ), $order_total, WC()->cart ) ), - 'pending' => false, - ], - ]; - } - /** * Calculates whether Apple Pay is enabled for this store. * The option value is not stored in the database, and is calculated @@ -1715,7 +1549,7 @@ public function get_login_confirmation_settings() { * @return array An array of final taxes. */ private function get_taxes_like_cart( $product, $price ) { - if ( ! wc_tax_enabled() || $this->cart_prices_include_tax() ) { + if ( ! wc_tax_enabled() || $this->express_checkout_helper->cart_prices_include_tax() ) { // Only proceed when taxes are enabled, but not included. return []; } diff --git a/includes/class-wc-payments-woopay-button-handler.php b/includes/class-wc-payments-woopay-button-handler.php index 44154c29ee7..5045174a7b3 100644 --- a/includes/class-wc-payments-woopay-button-handler.php +++ b/includes/class-wc-payments-woopay-button-handler.php @@ -125,8 +125,6 @@ public function init() { add_filter( 'wcpay_payment_fields_js_config', [ $this, 'add_woopay_config' ] ); - add_action( 'wc_ajax_wcpay_add_to_cart', [ $this, 'ajax_add_to_cart' ] ); - add_action( 'wp_ajax_woopay_express_checkout_button_show_error_notice', [ $this, 'show_error_notice' ] ); add_action( 'wp_ajax_nopriv_woopay_express_checkout_button_show_error_notice', [ $this, 'show_error_notice' ] ); } @@ -214,195 +212,6 @@ public function show_error_notice() { wp_die(); } - /** - * Adds the current product to the cart. Used on product detail page. - */ - public function ajax_add_to_cart() { - check_ajax_referer( 'wcpay-add-to-cart', 'security' ); - - if ( ! defined( 'WOOCOMMERCE_CART' ) ) { - define( 'WOOCOMMERCE_CART', true ); - } - - WC()->shipping->reset_shipping(); - - $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; - $quantity = ! isset( $_POST['quantity'] ) ? 1 : absint( $_POST['quantity'] ); - $product = wc_get_product( $product_id ); - - if ( ! $product ) { - wp_send_json( - [ - 'error' => [ - 'code' => 'invalid_product_id', - 'message' => __( 'Invalid product id', 'woocommerce-payments' ), - ], - ], - 404 - ); - return; - } - - $product_type = $product->get_type(); - - // First empty the cart to prevent wrong calculation. - WC()->cart->empty_cart(); - - $is_add_to_cart_valid = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity ); - - if ( ! $is_add_to_cart_valid ) { - // Some extensions error messages needs to be - // submitted to show error messages. - wp_send_json( - [ - 'error' => true, - 'submit' => true, - ], - 400 - ); - return; - } - - if ( ( 'variable' === $product_type || 'variable-subscription' === $product_type ) && isset( $_POST['attributes'] ) ) { - $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) ); - - $data_store = WC_Data_Store::load( 'product' ); - $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); - - WC()->cart->add_to_cart( $product->get_id(), $quantity, $variation_id, $attributes ); - } - - if ( in_array( $product_type, [ 'simple', 'subscription', 'subscription_variation', 'bundle', 'mix-and-match' ], true ) ) { - WC()->cart->add_to_cart( $product->get_id(), $quantity ); - } - - WC()->cart->calculate_totals(); - - $data = []; - $data += $this->build_display_items(); - $data['result'] = 'success'; - - wp_send_json( $data ); - } - - /** - * Builds the line items to pass to Payment Request - * - * @param boolean $itemized_display_items Indicates whether to show subtotals or itemized views. - */ - protected function build_display_items( $itemized_display_items = false ) { - if ( ! defined( 'WOOCOMMERCE_CART' ) ) { - define( 'WOOCOMMERCE_CART', true ); - } - - $items = []; - $subtotal = 0; - $discounts = 0; - $currency = get_woocommerce_currency(); - - // Default show only subtotal instead of itemization. - if ( ! apply_filters( 'wcpay_payment_request_hide_itemization', true ) || $itemized_display_items ) { - foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { - $amount = $cart_item['line_subtotal']; - $subtotal += $cart_item['line_subtotal']; - $quantity_label = 1 < $cart_item['quantity'] ? ' (x' . $cart_item['quantity'] . ')' : ''; - - $product_name = $cart_item['data']->get_name(); - - $item_tax = $this->cart_prices_include_tax() ? ( $cart_item['line_subtotal_tax'] ?? 0 ) : 0; - - $item = [ - 'label' => $product_name . $quantity_label, - 'amount' => WC_Payments_Utils::prepare_amount( $amount + $item_tax, $currency ), - ]; - - $items[] = $item; - } - } - - if ( version_compare( WC_VERSION, '3.2', '<' ) ) { - $discounts = wc_format_decimal( WC()->cart->get_cart_discount_total(), WC()->cart->dp ); - } else { - $applied_coupons = array_values( WC()->cart->get_coupon_discount_totals() ); - - foreach ( $applied_coupons as $amount ) { - $discounts += (float) $amount; - } - } - - $discounts = wc_format_decimal( $discounts, WC()->cart->dp ); - $tax = wc_format_decimal( WC()->cart->tax_total + WC()->cart->shipping_tax_total, WC()->cart->dp ); - $shipping = wc_format_decimal( WC()->cart->shipping_total, WC()->cart->dp ); - $items_total = wc_format_decimal( WC()->cart->cart_contents_total, WC()->cart->dp ) + $discounts; - $order_total = version_compare( WC_VERSION, '3.2', '<' ) ? wc_format_decimal( $items_total + $tax + $shipping - $discounts, WC()->cart->dp ) : WC()->cart->get_total( '' ); - - if ( ! $this->cart_prices_include_tax() ) { - $items[] = [ - 'label' => esc_html( __( 'Tax', 'woocommerce-payments' ) ), - 'amount' => WC_Payments_Utils::prepare_amount( $tax, $currency ), - ]; - } - - if ( WC()->cart->needs_shipping() ) { - $shipping_tax = $this->cart_prices_include_tax() ? WC()->cart->shipping_tax_total : 0; - $items[] = [ - 'label' => esc_html( __( 'Shipping', 'woocommerce-payments' ) ), - 'amount' => WC_Payments_Utils::prepare_amount( $shipping + $shipping_tax, $currency ), - ]; - } - - if ( WC()->cart->has_discount() ) { - $items[] = [ - 'label' => esc_html( __( 'Discount', 'woocommerce-payments' ) ), - 'amount' => WC_Payments_Utils::prepare_amount( $discounts, $currency ), - ]; - } - - if ( version_compare( WC_VERSION, '3.2', '<' ) ) { - $cart_fees = WC()->cart->fees; - } else { - $cart_fees = WC()->cart->get_fees(); - } - - // Include fees and taxes as display items. - foreach ( $cart_fees as $key => $fee ) { - $items[] = [ - 'label' => $fee->name, - 'amount' => WC_Payments_Utils::prepare_amount( $fee->amount, $currency ), - ]; - } - - return [ - 'displayItems' => $items, - 'total' => [ - 'label' => $this->get_total_label(), - 'amount' => max( 0, apply_filters( 'wcpay_calculated_total', WC_Payments_Utils::prepare_amount( $order_total, $currency ), $order_total, WC()->cart ) ), - 'pending' => false, - ], - ]; - } - - /** - * Whether tax should be displayed on separate line in cart. - * returns true if tax is disabled or display of tax in checkout is set to inclusive. - * - * @return boolean - */ - private function cart_prices_include_tax() { - return ! wc_tax_enabled() || 'incl' === get_option( 'woocommerce_tax_display_cart' ); - } - - /** - * Gets total label. - * - * @return string - */ - public function get_total_label() { - // Get statement descriptor from API/cached account data. - $statement_descriptor = $this->account->get_statement_descriptor(); - return str_replace( "'", '', $statement_descriptor ) . apply_filters( 'wcpay_payment_request_total_label_suffix', ' (via WooCommerce)' ); - } - /** * Checks if this is a product page or content contains a product_page shortcode. * diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php index baf1ea9a8ca..bd0cb8dc97f 100644 --- a/includes/class-wc-payments.php +++ b/includes/class-wc-payments.php @@ -295,6 +295,13 @@ class WC_Payments { */ private static $incentives_service; + /** + * Instance of WC_Payments_Express_Checkout_Button_Helper, created in init function. + * + * @var WC_Payments_Express_Checkout_Button_Helper + */ + private static $express_checkout_helper; + /** * Instance of Compatibility_Service, created in init function * @@ -415,6 +422,7 @@ public static function init() { include_once __DIR__ . '/payment-methods/class-affirm-payment-method.php'; include_once __DIR__ . '/payment-methods/class-afterpay-payment-method.php'; include_once __DIR__ . '/payment-methods/class-klarna-payment-method.php'; + include_once __DIR__ . '/express-checkout/class-wc-payments-express-checkout-button-helper.php'; include_once __DIR__ . '/class-wc-payment-token-wcpay-sepa.php'; include_once __DIR__ . '/class-wc-payments-status.php'; include_once __DIR__ . '/class-wc-payments-token-service.php'; @@ -501,6 +509,7 @@ public static function init() { self::$failed_transaction_rate_limiter = new Session_Rate_Limiter( Session_Rate_Limiter::SESSION_KEY_DECLINED_CARD_REGISTRY, 5, 10 * MINUTE_IN_SECONDS ); self::$order_success_page = new WC_Payments_Order_Success_Page(); self::$onboarding_service = new WC_Payments_Onboarding_Service( self::$api_client, self::$database_cache ); + self::$express_checkout_helper = new WC_Payments_Express_Checkout_Button_Helper( self::$account ); self::$woopay_util = new WooPay_Utilities(); self::$woopay_tracker = new WooPay_Tracker( self::get_wc_payments_http() ); self::$incentives_service = new WC_Payments_Incentives_Service( self::$database_cache ); @@ -1504,9 +1513,9 @@ function ( $container ) { */ public static function maybe_display_express_checkout_buttons() { if ( WC_Payments_Features::are_payments_enabled() ) { - $payment_request_button_handler = new WC_Payments_Payment_Request_Button_Handler( self::$account, self::get_gateway() ); + $payment_request_button_handler = new WC_Payments_Payment_Request_Button_Handler( self::$account, self::get_gateway(), self::$express_checkout_helper ); $woopay_button_handler = new WC_Payments_WooPay_Button_Handler( self::$account, self::get_gateway(), self::$woopay_util ); - $express_checkout_button_display_handler = new WC_Payments_Express_Checkout_Button_Display_Handler( self::get_gateway(), $payment_request_button_handler, $woopay_button_handler ); + $express_checkout_button_display_handler = new WC_Payments_Express_Checkout_Button_Display_Handler( self::get_gateway(), $payment_request_button_handler, $woopay_button_handler, self::$express_checkout_helper ); } } 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 new file mode 100644 index 00000000000..1eb77586ebb --- /dev/null +++ b/includes/express-checkout/class-wc-payments-express-checkout-button-helper.php @@ -0,0 +1,235 @@ +account = $account; + } + + /** + * Adds the current product to the cart. Used on product detail page. + */ + public function ajax_add_to_cart() { + check_ajax_referer( 'wcpay-add-to-cart', 'security' ); + + if ( ! defined( 'WOOCOMMERCE_CART' ) ) { + define( 'WOOCOMMERCE_CART', true ); + } + + WC()->shipping->reset_shipping(); + + $product_id = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : false; + $product = wc_get_product( $product_id ); + + if ( ! $product ) { + wp_send_json( + [ + 'error' => [ + 'code' => 'invalid_product_id', + 'message' => __( 'Invalid product id', 'woocommerce-payments' ), + ], + ], + 404 + ); + return; + } + + $quantity = $this->get_quantity(); + + $product_type = $product->get_type(); + + $is_add_to_cart_valid = apply_filters( 'woocommerce_add_to_cart_validation', true, $product_id, $quantity ); + + if ( ! $is_add_to_cart_valid ) { + // Some extensions error messages needs to be + // submitted to show error messages. + wp_send_json( + [ + 'error' => true, + 'submit' => true, + ], + 400 + ); + return; + } + + // First empty the cart to prevent wrong calculation. + WC()->cart->empty_cart(); + + if ( ( 'variable' === $product_type || 'variable-subscription' === $product_type ) && isset( $_POST['attributes'] ) ) { + $attributes = wc_clean( wp_unslash( $_POST['attributes'] ) ); + + $data_store = WC_Data_Store::load( 'product' ); + $variation_id = $data_store->find_matching_product_variation( $product, $attributes ); + + WC()->cart->add_to_cart( $product->get_id(), $quantity, $variation_id, $attributes ); + } + + if ( in_array( $product_type, [ 'simple', 'subscription', 'subscription_variation', 'bundle', 'mix-and-match' ], true ) ) { + WC()->cart->add_to_cart( $product->get_id(), $quantity ); + } + + WC()->cart->calculate_totals(); + + $data = []; + $data += $this->build_display_items(); + $data['result'] = 'success'; + + wp_send_json( $data ); + } + + /** + * Builds the line items to pass to Payment Request + * + * @param boolean $itemized_display_items Indicates whether to show subtotals or itemized views. + */ + public function build_display_items( $itemized_display_items = false ) { + if ( ! defined( 'WOOCOMMERCE_CART' ) ) { + define( 'WOOCOMMERCE_CART', true ); + } + + $items = []; + $subtotal = 0; + $discounts = 0; + $currency = get_woocommerce_currency(); + + // Default show only subtotal instead of itemization. + if ( ! apply_filters( 'wcpay_payment_request_hide_itemization', true ) || $itemized_display_items ) { + foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) { + $amount = $cart_item['line_subtotal']; + $subtotal += $cart_item['line_subtotal']; + $quantity_label = 1 < $cart_item['quantity'] ? ' (x' . $cart_item['quantity'] . ')' : ''; + + $product_name = $cart_item['data']->get_name(); + + $item_tax = $this->cart_prices_include_tax() ? ( $cart_item['line_subtotal_tax'] ?? 0 ) : 0; + + $item = [ + 'label' => $product_name . $quantity_label, + 'amount' => WC_Payments_Utils::prepare_amount( $amount + $item_tax, $currency ), + ]; + + $items[] = $item; + } + } + + if ( version_compare( WC_VERSION, '3.2', '<' ) ) { + $discounts = wc_format_decimal( WC()->cart->get_cart_discount_total(), WC()->cart->dp ); + } else { + $applied_coupons = array_values( WC()->cart->get_coupon_discount_totals() ); + + foreach ( $applied_coupons as $amount ) { + $discounts += (float) $amount; + } + } + + $discounts = wc_format_decimal( $discounts, WC()->cart->dp ); + $tax = wc_format_decimal( WC()->cart->tax_total + WC()->cart->shipping_tax_total, WC()->cart->dp ); + $shipping = wc_format_decimal( WC()->cart->shipping_total, WC()->cart->dp ); + $items_total = wc_format_decimal( WC()->cart->cart_contents_total, WC()->cart->dp ) + $discounts; + $order_total = version_compare( WC_VERSION, '3.2', '<' ) ? wc_format_decimal( $items_total + $tax + $shipping - $discounts, WC()->cart->dp ) : WC()->cart->get_total( '' ); + + if ( ! $this->cart_prices_include_tax() ) { + $items[] = [ + 'label' => esc_html( __( 'Tax', 'woocommerce-payments' ) ), + 'amount' => WC_Payments_Utils::prepare_amount( $tax, $currency ), + ]; + } + + if ( WC()->cart->needs_shipping() ) { + $shipping_tax = $this->cart_prices_include_tax() ? WC()->cart->shipping_tax_total : 0; + $items[] = [ + 'label' => esc_html( __( 'Shipping', 'woocommerce-payments' ) ), + 'amount' => WC_Payments_Utils::prepare_amount( $shipping + $shipping_tax, $currency ), + ]; + } + + if ( WC()->cart->has_discount() ) { + $items[] = [ + 'label' => esc_html( __( 'Discount', 'woocommerce-payments' ) ), + 'amount' => WC_Payments_Utils::prepare_amount( $discounts, $currency ), + ]; + } + + if ( version_compare( WC_VERSION, '3.2', '<' ) ) { + $cart_fees = WC()->cart->fees; + } else { + $cart_fees = WC()->cart->get_fees(); + } + + // Include fees and taxes as display items. + foreach ( $cart_fees as $key => $fee ) { + $items[] = [ + 'label' => $fee->name, + 'amount' => WC_Payments_Utils::prepare_amount( $fee->amount, $currency ), + ]; + } + + return [ + 'displayItems' => $items, + 'total' => [ + 'label' => $this->get_total_label(), + 'amount' => max( 0, apply_filters( 'wcpay_calculated_total', WC_Payments_Utils::prepare_amount( $order_total, $currency ), $order_total, WC()->cart ) ), + 'pending' => false, + ], + ]; + } + + /** + * Whether tax should be displayed on separate line in cart. + * returns true if tax is disabled or display of tax in checkout is set to inclusive. + * + * @return boolean + */ + public function cart_prices_include_tax() { + return ! wc_tax_enabled() || 'incl' === get_option( 'woocommerce_tax_display_cart' ); + } + + /** + * Gets total label. + * + * @return string + */ + public function get_total_label() { + // Get statement descriptor from API/cached account data. + $statement_descriptor = $this->account->get_statement_descriptor(); + return str_replace( "'", '', $statement_descriptor ) . apply_filters( 'wcpay_payment_request_total_label_suffix', ' (via WooCommerce)' ); + } + + /** + * Gets quantity from request. + * + * @return int + */ + private function get_quantity() { + // Payment Request Button sends the quantity as qty. WooPay sends it as quantity. + if ( isset( $_POST['quantity'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing + return absint( $_POST['quantity'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + } elseif ( isset( $_POST['qty'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing + return absint( $_POST['qty'] ); // phpcs:ignore WordPress.Security.NonceVerification.Missing + } else { + return 1; + } + } +} diff --git a/tests/unit/test-class-wc-payments-express-checkout-button-display-handler.php b/tests/unit/test-class-wc-payments-express-checkout-button-display-handler.php index 220f3d2ff03..dfb059ba7b4 100644 --- a/tests/unit/test-class-wc-payments-express-checkout-button-display-handler.php +++ b/tests/unit/test-class-wc-payments-express-checkout-button-display-handler.php @@ -63,6 +63,13 @@ class WC_Payments_Express_Checkout_Button_Display_Handler_Test extends WCPAY_Uni */ private $mock_woopay_utilities; + /** + * Express Checkout Helper instance. + * + * @var WC_Payments_Express_Checkout_Button_Helper + */ + private $express_checkout_helper; + /** * Sets up things all tests need. */ @@ -103,11 +110,14 @@ public function set_up() { ) ->getMock(); + $this->express_checkout_helper = new WC_Payments_Express_Checkout_Button_Helper( $this->mock_wcpay_account ); + $this->mock_payment_request_button_handler = $this->getMockBuilder( WC_Payments_Payment_Request_Button_Handler::class ) ->setConstructorArgs( [ $this->mock_wcpay_account, $this->mock_wcpay_gateway, + $this->express_checkout_helper, ] ) ->setMethods( @@ -118,7 +128,7 @@ public function set_up() { ) ->getMock(); - $this->express_checkout_button_display_handler = new WC_Payments_Express_Checkout_Button_Display_Handler( $this->mock_wcpay_gateway, $this->mock_payment_request_button_handler, $this->mock_woopay_button_handler ); + $this->express_checkout_button_display_handler = new WC_Payments_Express_Checkout_Button_Display_Handler( $this->mock_wcpay_gateway, $this->mock_payment_request_button_handler, $this->mock_woopay_button_handler, $this->express_checkout_helper ); add_filter( 'woocommerce_available_payment_gateways', diff --git a/tests/unit/test-class-wc-payments-payment-request-button-handler.php b/tests/unit/test-class-wc-payments-payment-request-button-handler.php index 7940d7b20f0..ce1283265b4 100644 --- a/tests/unit/test-class-wc-payments-payment-request-button-handler.php +++ b/tests/unit/test-class-wc-payments-payment-request-button-handler.php @@ -76,6 +76,13 @@ class WC_Payments_Payment_Request_Button_Handler_Test extends WCPAY_UnitTestCase */ private $mock_wcpay_gateway; + /** + * Express Checkout Helper instance. + * + * @var WC_Payments_Express_Checkout_Button_Helper + */ + private $express_checkout_helper; + /** * Sets up things all tests need. */ @@ -100,7 +107,9 @@ public function set_up() { $this->mock_wcpay_gateway = $this->make_wcpay_gateway(); - $this->pr = new WC_Payments_Payment_Request_Button_Handler( $this->mock_wcpay_account, $this->mock_wcpay_gateway ); + $this->express_checkout_helper = new WC_Payments_Express_Checkout_Button_Helper( $this->mock_wcpay_account ); + + $this->pr = new WC_Payments_Payment_Request_Button_Handler( $this->mock_wcpay_account, $this->mock_wcpay_gateway, $this->express_checkout_helper ); $this->simple_product = WC_Helper_Product::create_simple_product(); @@ -294,7 +303,7 @@ public function test_get_shipping_options_keeps_chosen_option() { public function test_get_button_settings() { $this->mock_wcpay_gateway = $this->make_wcpay_gateway(); - $this->pr = new WC_Payments_Payment_Request_Button_Handler( $this->mock_wcpay_account, $this->mock_wcpay_gateway ); + $this->pr = new WC_Payments_Payment_Request_Button_Handler( $this->mock_wcpay_account, $this->mock_wcpay_gateway, $this->express_checkout_helper ); $this->assertEquals( [ @@ -320,7 +329,7 @@ function() { } ); $this->mock_wcpay_gateway = $this->make_wcpay_gateway(); - $this->pr = new WC_Payments_Payment_Request_Button_Handler( $this->mock_wcpay_account, $this->mock_wcpay_gateway ); + $this->pr = new WC_Payments_Payment_Request_Button_Handler( $this->mock_wcpay_account, $this->mock_wcpay_gateway, $this->express_checkout_helper ); $this->assertFalse( $this->pr->has_allowed_items_in_cart() ); } @@ -505,10 +514,10 @@ public function test_get_product_data_returns_the_same_as_build_display_items_wi add_filter( 'pre_option_woocommerce_tax_display_cart', [ $this, "__return_$tax_display_cart" ] ); // reset in tear_down. add_filter( 'wc_shipping_enabled', '__return_false' ); // reset in tear_down. WC()->cart->calculate_totals(); - $build_display_items_result = $this->pr->build_display_items( true ); + $build_display_items_result = $this->express_checkout_helper->build_display_items( true ); $mock_pr = $this->getMockBuilder( WC_Payments_Payment_Request_Button_Handler::class ) - ->setConstructorArgs( [ $this->mock_wcpay_account, $this->mock_wcpay_gateway ] ) + ->setConstructorArgs( [ $this->mock_wcpay_account, $this->mock_wcpay_gateway, $this->express_checkout_helper ] ) ->setMethods( [ 'is_product', 'get_product' ] ) ->getMock();