get_id();
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotValidated
diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php
index 9cf4ed5a71f..1868fd3dcaa 100644
--- a/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php
+++ b/includes/express-checkout/class-wc-payments-express-checkout-button-handler.php
@@ -11,9 +11,7 @@
exit;
}
-use WCPay\Exceptions\Invalid_Price_Exception;
use WCPay\Fraud_Prevention\Fraud_Prevention_Service;
-use WCPay\Logger;
/**
* WC_Payments_Express_Checkout_Button_Handler class.
@@ -36,23 +34,32 @@ class WC_Payments_Express_Checkout_Button_Handler {
private $gateway;
/**
- * Express Checkout Helper instance.
+ * Express Checkout Ajax Handle instance.
*
* @var WC_Payments_Express_Checkout_Button_Helper
*/
private $express_checkout_helper;
+ /**
+ * Express Checkout Helper instance.
+ *
+ * @var WC_Payments_Express_Checkout_Ajax_Handler
+ */
+ private $express_checkout_ajax_handler;
+
/**
* Initialize class actions.
*
* @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.
+ * @param WC_Payments_Express_Checkout_Ajax_Handler $express_checkout_ajax_handler Express checkout ajax handler.
*/
- 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;
+ public function __construct( WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway, WC_Payments_Express_Checkout_Button_Helper $express_checkout_helper, WC_Payments_Express_Checkout_Ajax_Handler $express_checkout_ajax_handler ) {
+ $this->account = $account;
+ $this->gateway = $gateway;
+ $this->express_checkout_helper = $express_checkout_helper;
+ $this->express_checkout_ajax_handler = $express_checkout_ajax_handler;
}
/**
@@ -81,6 +88,8 @@ public function init() {
}
add_action( 'wp_enqueue_scripts', [ $this, 'scripts' ] );
+
+ $this->express_checkout_ajax_handler->init();
}
/**
@@ -100,159 +109,12 @@ public function get_button_settings() {
return array_merge( $common_settings, $payment_request_button_settings );
}
- /**
- * Checks whether Payment Request Button should be available on this page.
- *
- * @return bool
- */
- public function should_show_express_checkout_button() {
- // If account is not connected, then bail.
- if ( ! $this->account->is_stripe_connected( false ) ) {
- return false;
- }
-
- // If no SSL, bail.
- if ( ! WC_Payments::mode()->is_test() && ! is_ssl() ) {
- Logger::log( 'Stripe Payment Request live mode requires SSL.' );
-
- return false;
- }
-
- // Page not supported.
- if ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_checkout() ) {
- return false;
- }
-
- // Product page, but not available in settings.
- if ( $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_available_at( 'product', self::BUTTON_LOCATIONS ) ) {
- return false;
- }
-
- // Checkout page, but not available in settings.
- if ( $this->express_checkout_helper->is_checkout() && ! $this->express_checkout_helper->is_available_at( 'checkout', self::BUTTON_LOCATIONS ) ) {
- return false;
- }
-
- // Cart page, but not available in settings.
- if ( $this->express_checkout_helper->is_cart() && ! $this->express_checkout_helper->is_available_at( 'cart', self::BUTTON_LOCATIONS ) ) {
- return false;
- }
-
- // Product page, but has unsupported product type.
- if ( $this->express_checkout_helper->is_product() && ! $this->is_product_supported() ) {
- Logger::log( 'Product page has unsupported product type ( Payment Request button disabled )' );
- return false;
- }
-
- // Cart has unsupported product type.
- if ( ( $this->express_checkout_helper->is_checkout() || $this->express_checkout_helper->is_cart() ) && ! $this->has_allowed_items_in_cart() ) {
- Logger::log( 'Items in the cart have unsupported product type ( Payment Request button disabled )' );
- return false;
- }
-
- // Order total doesn't matter for Pay for Order page. Thus, this page should always display payment buttons.
- if ( $this->express_checkout_helper->is_pay_for_order_page() ) {
- return true;
- }
-
- // Cart total is 0 or is on product page and product price is 0.
- // Exclude pay-for-order pages from this check.
- if (
- ( ! $this->express_checkout_helper->is_product() && ! $this->express_checkout_helper->is_pay_for_order_page() && 0.0 === (float) WC()->cart->get_total( 'edit' ) ) ||
- ( $this->express_checkout_helper->is_product() && 0.0 === (float) $this->express_checkout_helper->get_product()->get_price() )
-
- ) {
- Logger::log( 'Order price is 0 ( Payment Request button disabled )' );
- return false;
- }
-
- return true;
- }
-
- /**
- * Checks to make sure product type is supported.
- *
- * @return array
- */
- public function supported_product_types() {
- return apply_filters(
- 'wcpay_payment_request_supported_types',
- [
- 'simple',
- 'variable',
- 'variation',
- 'subscription',
- 'variable-subscription',
- 'subscription_variation',
- 'booking',
- 'bundle',
- 'composite',
- 'mix-and-match',
- ]
- );
- }
-
- /**
- * Checks the cart to see if all items are allowed to be used.
- *
- * @return boolean
- *
- * @psalm-suppress UndefinedClass
- */
- public function has_allowed_items_in_cart() {
- /**
- * Pre Orders compatbility where we don't support charge upon release.
- *
- * @psalm-suppress UndefinedClass
- */
- if ( class_exists( 'WC_Pre_Orders_Cart' ) && WC_Pre_Orders_Cart::cart_contains_pre_order() && class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() ) ) {
- return false;
- }
-
- foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
- $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
-
- if ( ! in_array( $_product->get_type(), $this->supported_product_types(), true ) ) {
- return false;
- }
-
- /**
- * Filter whether product supports Payment Request Button on cart page.
- *
- * @since 6.9.0
- *
- * @param boolean $is_supported Whether product supports Payment Request Button on cart page.
- * @param object $_product Product object.
- */
- if ( ! apply_filters( 'wcpay_payment_request_is_cart_supported', true, $_product ) ) {
- return false;
- }
-
- /**
- * Trial subscriptions with shipping are not supported.
- *
- * @psalm-suppress UndefinedClass
- */
- if ( class_exists( 'WC_Subscriptions_Product' ) && WC_Subscriptions_Product::is_subscription( $_product ) && $_product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $_product ) > 0 ) {
- return false;
- }
- }
-
- // We don't support multiple packages with Payment Request Buttons because we can't offer a good UX.
- $packages = WC()->cart->get_shipping_packages();
- if ( 1 < ( is_countable( $packages ) ? count( $packages ) : 0 ) ) {
- return false;
- }
-
- return true;
- }
-
/**
* Load public scripts and styles.
*/
public function scripts() {
// Don't load scripts if page is not supported.
- if ( ! $this->should_show_express_checkout_button() ) {
+ if ( ! $this->express_checkout_helper->should_show_express_checkout_button() ) {
return;
}
@@ -288,7 +150,7 @@ public function scripts() {
'button_context' => $this->express_checkout_helper->get_button_context(),
'is_pay_for_order' => $this->express_checkout_helper->is_pay_for_order_page(),
'has_block' => has_block( 'woocommerce/cart' ) || has_block( 'woocommerce/checkout' ),
- 'product' => $this->get_product_data(),
+ 'product' => $this->express_checkout_helper->get_product_data(),
'total_label' => $this->express_checkout_helper->get_total_label(),
'is_checkout_page' => $this->express_checkout_helper->is_checkout(),
];
@@ -320,235 +182,11 @@ public function scripts() {
* Display the payment request button.
*/
public function display_express_checkout_button_html() {
- if ( ! $this->should_show_express_checkout_button() ) {
+ if ( ! $this->express_checkout_helper->should_show_express_checkout_button() ) {
return;
}
?>
express_checkout_helper->get_product();
- $is_supported = true;
-
- /**
- * Ignore undefined classes from 3rd party plugins.
- *
- * @psalm-suppress UndefinedClass
- */
- if ( is_null( $product )
- || ! is_object( $product )
- || ! in_array( $product->get_type(), $this->supported_product_types(), true )
- || ( class_exists( 'WC_Subscriptions_Product' ) && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) // Trial subscriptions with shipping are not supported.
- || ( class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) // Pre Orders charge upon release not supported.
- || ( class_exists( 'WC_Composite_Products' ) && $product->is_type( 'composite' ) ) // Composite products are not supported on the product page.
- || ( class_exists( 'WC_Mix_and_Match' ) && $product->is_type( 'mix-and-match' ) ) // Mix and match products are not supported on the product page.
- ) {
- $is_supported = false;
- } elseif ( class_exists( 'WC_Product_Addons_Helper' ) ) {
- // File upload addon not supported.
- $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() );
- foreach ( $product_addons as $addon ) {
- if ( 'file_upload' === $addon['type'] ) {
- $is_supported = false;
- break;
- }
- }
- }
-
- return apply_filters( 'wcpay_payment_request_is_product_supported', $is_supported, $product );
- }
-
- /**
- * Gets the product data for the currently viewed page.
- *
- * @return mixed Returns false if not on a product page, the product information otherwise.
- */
- public function get_product_data() {
- if ( ! $this->express_checkout_helper->is_product() ) {
- return false;
- }
-
- /** @var WC_Product_Variable $product */ // phpcs:ignore
- $product = $this->express_checkout_helper->get_product();
- $currency = get_woocommerce_currency();
-
- if ( 'variable' === $product->get_type() || 'variable-subscription' === $product->get_type() ) {
- $variation_attributes = $product->get_variation_attributes();
- $attributes = [];
-
- foreach ( $variation_attributes as $attribute_name => $attribute_values ) {
- $attribute_key = 'attribute_' . sanitize_title( $attribute_name );
-
- // Passed value via GET takes precedence. Otherwise get the default value for given attribute.
- $attributes[ $attribute_key ] = isset( $_GET[ $attribute_key ] ) // phpcs:ignore WordPress.Security.NonceVerification
- ? wc_clean( wp_unslash( $_GET[ $attribute_key ] ) ) // phpcs:ignore WordPress.Security.NonceVerification
- : $product->get_variation_default_attribute( $attribute_name );
- }
-
- $data_store = WC_Data_Store::load( 'product' );
- $variation_id = $data_store->find_matching_product_variation( $product, $attributes );
-
- if ( ! empty( $variation_id ) ) {
- $product = wc_get_product( $variation_id );
- }
- }
-
- try {
- $price = $this->get_product_price( $product );
- } catch ( Invalid_Price_Exception $e ) {
- Logger::log( $e->getMessage() );
- return false;
- }
-
- $data = [];
- $items = [];
-
- $items[] = [
- 'label' => $product->get_name(),
- 'amount' => WC_Payments_Utils::prepare_amount( $price, $currency ),
- ];
-
- $total_tax = 0;
- foreach ( $this->get_taxes_like_cart( $product, $price ) as $tax ) {
- $total_tax += $tax;
-
- $items[] = [
- 'label' => __( 'Tax', 'woocommerce-payments' ),
- 'amount' => WC_Payments_Utils::prepare_amount( $tax, $currency ),
- 'pending' => 0 === $tax,
- ];
- }
-
- if ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() ) {
- $items[] = [
- 'label' => __( 'Shipping', 'woocommerce-payments' ),
- 'amount' => 0,
- 'pending' => true,
- ];
-
- $data['shippingOptions'] = [
- 'id' => 'pending',
- 'label' => __( 'Pending', 'woocommerce-payments' ),
- 'detail' => '',
- 'amount' => 0,
- ];
- }
-
- $data['displayItems'] = $items;
- $data['total'] = [
- '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,
- ];
-
- $data['needs_shipping'] = ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() );
- $data['currency'] = strtolower( $currency );
- $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 );
-
- return apply_filters( 'wcpay_payment_request_product_data', $data, $product );
- }
-
- /**
- * Gets the product total price.
- *
- * @param object $product WC_Product_* object.
- * @param bool $is_deposit Whether customer is paying a deposit.
- * @param int $deposit_plan_id The ID of the deposit plan.
- * @return mixed Total price.
- *
- * @throws Invalid_Price_Exception Whenever a product has no price.
- *
- * @psalm-suppress UndefinedClass
- */
- public function get_product_price( $product, ?bool $is_deposit = null, int $deposit_plan_id = 0 ) {
- // If prices should include tax, using tax inclusive price.
- 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 );
- }
-
- // If WooCommerce Deposits is active, we need to get the correct price for the product.
- if ( class_exists( 'WC_Deposits_Product_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) {
- if ( is_null( $is_deposit ) ) {
- /**
- * If is_deposit is null, we use the default deposit type for the product.
- *
- * @psalm-suppress UndefinedClass
- */
- $is_deposit = 'deposit' === WC_Deposits_Product_Manager::get_deposit_selected_type( $product->get_id() );
- }
- if ( $is_deposit ) {
- /**
- * Ignore undefined classes from 3rd party plugins.
- *
- * @psalm-suppress UndefinedClass
- */
- $deposit_type = WC_Deposits_Product_Manager::get_deposit_type( $product->get_id() );
- $available_plan_ids = WC_Deposits_Plans_Manager::get_plan_ids_for_product( $product->get_id() );
- // Default to first (default) plan if no plan is specified.
- if ( 'plan' === $deposit_type && 0 === $deposit_plan_id && ! empty( $available_plan_ids ) ) {
- $deposit_plan_id = $available_plan_ids[0];
- }
-
- // Ensure the selected plan is available for the product.
- if ( 0 === $deposit_plan_id || in_array( $deposit_plan_id, $available_plan_ids, true ) ) {
- $base_price = WC_Deposits_Product_Manager::get_deposit_amount( $product, $deposit_plan_id, 'display', $base_price );
- }
- }
- }
-
- // Add subscription sign-up fees to product price.
- $sign_up_fee = 0;
- $subscription_types = [
- 'subscription',
- 'subscription_variation',
- ];
- if ( in_array( $product->get_type(), $subscription_types, true ) && class_exists( 'WC_Subscriptions_Product' ) ) {
- // When there is no sign-up fee, `get_sign_up_fee` falls back to an int 0.
- $sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $product );
- }
-
- if ( ! is_numeric( $base_price ) || ! is_numeric( $sign_up_fee ) ) {
- $error_message = sprintf(
- // Translators: %d is the numeric ID of the product without a price.
- __( 'Express checkout does not support products without prices! Please add a price to product #%d', 'woocommerce-payments' ),
- (int) $product->get_id()
- );
- throw new Invalid_Price_Exception(
- esc_html( $error_message )
- );
- }
-
- return $base_price + $sign_up_fee;
- }
-
- /**
- * Calculates taxes as displayed on cart, based on a product and a particular price.
- *
- * @param WC_Product $product The product, for retrieval of tax classes.
- * @param float $price The price, which to calculate taxes for.
- * @return array An array of final taxes.
- */
- private function get_taxes_like_cart( $product, $price ) {
- if ( ! wc_tax_enabled() || $this->express_checkout_helper->cart_prices_include_tax() ) {
- // Only proceed when taxes are enabled, but not included.
- return [];
- }
-
- // Follows the way `WC_Cart_Totals::get_item_tax_rates()` works.
- $tax_class = $product->get_tax_class();
- $rates = WC_Tax::get_rates( $tax_class );
- // No cart item, `woocommerce_cart_totals_get_item_tax_rates` can't be applied here.
-
- // Normally there should be a single tax, but `calc_tax` returns an array, let's use it.
- return WC_Tax::calc_tax( $price, $rates, false );
- }
}
\ No newline at end of file
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 75495a3b990..08a2fad1014 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
@@ -7,6 +7,10 @@
defined( 'ABSPATH' ) || exit;
+use WCPay\Constants\Country_Code;
+use WCPay\Exceptions\Invalid_Price_Exception;
+use WCPay\Logger;
+
/**
* Express Checkout Button Helper class.
*/
@@ -36,86 +40,6 @@ public function __construct( WC_Payment_Gateway_WCPay $gateway, WC_Payments_Acco
$this->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', '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 ( ! empty( $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.
@@ -134,26 +58,6 @@ public function get_booking_id_from_cart() {
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
*
@@ -165,15 +69,13 @@ public function build_display_items( $itemized_display_items = false ) {
}
$items = [];
- $subtotal = 0;
$discounts = 0;
$currency = get_woocommerce_currency();
// Default show only subtotal instead of itemization.
if ( ! apply_filters( 'wcpay_payment_request_hide_itemization', ! $itemized_display_items ) ) {
- foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
+ foreach ( WC()->cart->get_cart() as $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();
@@ -234,7 +136,7 @@ public function build_display_items( $itemized_display_items = false ) {
}
// Include fees and taxes as display items.
- foreach ( $cart_fees as $key => $fee ) {
+ foreach ( $cart_fees as $fee ) {
$items[] = [
'label' => $fee->name,
'amount' => WC_Payments_Utils::prepare_amount( $fee->amount, $currency ),
@@ -277,7 +179,7 @@ public function get_total_label() {
*
* @return int
*/
- private function get_quantity() {
+ public 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
@@ -433,4 +335,757 @@ public function is_product_subscription( WC_Product $product ): bool {
|| 'subscription_variation' === $product->get_type()
|| 'variable-subscription' === $product->get_type();
}
+
+ /**
+ * Checks whether Payment Request Button should be available on this page.
+ *
+ * @return bool
+ */
+ public function should_show_express_checkout_button() {
+ // If account is not connected, then bail.
+ if ( ! $this->account->is_stripe_connected( false ) ) {
+ return false;
+ }
+
+ // If no SSL, bail.
+ if ( ! WC_Payments::mode()->is_test() && ! is_ssl() ) {
+ Logger::log( 'Stripe Payment Request live mode requires SSL.' );
+
+ return false;
+ }
+
+ // Page not supported.
+ if ( ! $this->is_product() && ! $this->is_cart() && ! $this->is_checkout() ) {
+ return false;
+ }
+
+ // Product page, but not available in settings.
+ if ( $this->is_product() && ! $this->is_available_at( 'product', WC_Payments_Express_Checkout_Button_Handler::BUTTON_LOCATIONS ) ) {
+ return false;
+ }
+
+ // Checkout page, but not available in settings.
+ if ( $this->is_checkout() && ! $this->is_available_at( 'checkout', WC_Payments_Express_Checkout_Button_Handler::BUTTON_LOCATIONS ) ) {
+ return false;
+ }
+
+ // Cart page, but not available in settings.
+ if ( $this->is_cart() && ! $this->is_available_at( 'cart', WC_Payments_Express_Checkout_Button_Handler::BUTTON_LOCATIONS ) ) {
+ return false;
+ }
+
+ // Product page, but has unsupported product type.
+ if ( $this->is_product() && ! $this->is_product_supported() ) {
+ Logger::log( 'Product page has unsupported product type ( Payment Request button disabled )' );
+ return false;
+ }
+
+ // Cart has unsupported product type.
+ if ( ( $this->is_checkout() || $this->is_cart() ) && ! $this->has_allowed_items_in_cart() ) {
+ Logger::log( 'Items in the cart have unsupported product type ( Payment Request button disabled )' );
+ return false;
+ }
+
+ // Order total doesn't matter for Pay for Order page. Thus, this page should always display payment buttons.
+ if ( $this->is_pay_for_order_page() ) {
+ return true;
+ }
+
+ // Cart total is 0 or is on product page and product price is 0.
+ // Exclude pay-for-order pages from this check.
+ if (
+ ( ! $this->is_product() && ! $this->is_pay_for_order_page() && 0.0 === (float) WC()->cart->get_total( 'edit' ) ) ||
+ ( $this->is_product() && 0.0 === (float) $this->get_product()->get_price() )
+
+ ) {
+ Logger::log( 'Order price is 0 ( Payment Request button disabled )' );
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Checks to make sure product type is supported.
+ *
+ * @return array
+ */
+ public function supported_product_types() {
+ return apply_filters(
+ 'wcpay_payment_request_supported_types',
+ [
+ 'simple',
+ 'variable',
+ 'variation',
+ 'subscription',
+ 'variable-subscription',
+ 'subscription_variation',
+ 'booking',
+ 'bundle',
+ 'composite',
+ 'mix-and-match',
+ ]
+ );
+ }
+
+ /**
+ * Checks the cart to see if all items are allowed to be used.
+ *
+ * @return boolean
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ public function has_allowed_items_in_cart() {
+ /**
+ * Pre Orders compatbility where we don't support charge upon release.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ if ( class_exists( 'WC_Pre_Orders_Cart' ) && WC_Pre_Orders_Cart::cart_contains_pre_order() && class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( WC_Pre_Orders_Cart::get_pre_order_product() ) ) {
+ return false;
+ }
+
+ foreach ( WC()->cart->get_cart() as $cart_item_key => $cart_item ) {
+ $_product = apply_filters( 'woocommerce_cart_item_product', $cart_item['data'], $cart_item, $cart_item_key );
+
+ if ( ! in_array( $_product->get_type(), $this->supported_product_types(), true ) ) {
+ return false;
+ }
+
+ /**
+ * Filter whether product supports Payment Request Button on cart page.
+ *
+ * @since 6.9.0
+ *
+ * @param boolean $is_supported Whether product supports Payment Request Button on cart page.
+ * @param object $_product Product object.
+ */
+ if ( ! apply_filters( 'wcpay_payment_request_is_cart_supported', true, $_product ) ) {
+ return false;
+ }
+
+ /**
+ * Trial subscriptions with shipping are not supported.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ if ( class_exists( 'WC_Subscriptions_Product' ) && WC_Subscriptions_Product::is_subscription( $_product ) && $_product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $_product ) > 0 ) {
+ return false;
+ }
+ }
+
+ // We don't support multiple packages with Payment Request Buttons because we can't offer a good UX.
+ $packages = WC()->cart->get_shipping_packages();
+ if ( 1 < ( is_countable( $packages ) ? count( $packages ) : 0 ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Gets shipping options available for specified shipping address
+ *
+ * @param array $shipping_address Shipping address.
+ * @param boolean $itemized_display_items Indicates whether to show subtotals or itemized views.
+ *
+ * @return array Shipping options data.
+ *
+ * phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag
+ */
+ public function get_shipping_options( $shipping_address, $itemized_display_items = false ) {
+ try {
+ // Set the shipping options.
+ $data = [];
+
+ // Remember current shipping method before resetting.
+ $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] );
+ $this->calculate_shipping( apply_filters( 'wcpay_payment_request_shipping_posted_values', $shipping_address ) );
+
+ $packages = WC()->shipping->get_packages();
+
+ if ( ! empty( $packages ) && WC()->customer->has_calculated_shipping() ) {
+ foreach ( $packages as $package ) {
+ if ( empty( $package['rates'] ) ) {
+ throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-payments' ) );
+ }
+
+ foreach ( $package['rates'] as $rate ) {
+ $data['shipping_options'][] = [
+ 'id' => $rate->id,
+ 'displayName' => $rate->label,
+ 'amount' => WC_Payments_Utils::prepare_amount( $rate->cost, get_woocommerce_currency() ),
+ ];
+ }
+ }
+ } else {
+ throw new Exception( __( 'Unable to find shipping method for address.', 'woocommerce-payments' ) );
+ }
+
+ // The first shipping option is automatically applied on the client.
+ // Keep chosen shipping method by sorting shipping options if the method still available for new address.
+ // Fallback to the first available shipping method.
+ if ( isset( $data['shipping_options'][0] ) ) {
+ if ( isset( $chosen_shipping_methods[0] ) ) {
+ $chosen_method_id = $chosen_shipping_methods[0];
+ $compare_shipping_options = function ( $a, $b ) use ( $chosen_method_id ) {
+ if ( $a['id'] === $chosen_method_id ) {
+ return -1;
+ }
+
+ if ( $b['id'] === $chosen_method_id ) {
+ return 1;
+ }
+
+ return 0;
+ };
+ usort( $data['shipping_options'], $compare_shipping_options );
+ }
+
+ $first_shipping_method_id = $data['shipping_options'][0]['id'];
+ $this->update_shipping_method( [ $first_shipping_method_id ] );
+ }
+
+ WC()->cart->calculate_totals();
+
+ $this->maybe_restore_recurring_chosen_shipping_methods( $chosen_shipping_methods );
+
+ $data += $this->build_display_items( $itemized_display_items );
+ $data['result'] = 'success';
+ } catch ( Exception $e ) {
+ $data += $this->build_display_items( $itemized_display_items );
+ $data['result'] = 'invalid_shipping_address';
+ }
+
+ return $data;
+ }
+
+ /**
+ * Restores the shipping methods previously chosen for each recurring cart after shipping was reset and recalculated
+ * during the Payment Request get_shipping_options flow.
+ *
+ * When the cart contains multiple subscriptions with different billing periods, customers are able to select different shipping
+ * methods for each subscription, however, this is not supported when purchasing with Apple Pay and Google Pay as it's
+ * only concerned about handling the initial purchase.
+ *
+ * In order to avoid Woo Subscriptions's `WC_Subscriptions_Cart::validate_recurring_shipping_methods` throwing an error, we need to restore
+ * the previously chosen shipping methods for each recurring cart.
+ *
+ * This function needs to be called after `WC()->cart->calculate_totals()` is run, otherwise `WC()->cart->recurring_carts` won't exist yet.
+ *
+ * @param array $previous_chosen_methods The previously chosen shipping methods.
+ */
+ private function maybe_restore_recurring_chosen_shipping_methods( $previous_chosen_methods = [] ) {
+ if ( empty( WC()->cart->recurring_carts ) || ! method_exists( 'WC_Subscriptions_Cart', 'get_recurring_shipping_package_key' ) ) {
+ return;
+ }
+
+ $chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', [] );
+
+ foreach ( WC()->cart->recurring_carts as $recurring_cart_key => $recurring_cart ) {
+ foreach ( $recurring_cart->get_shipping_packages() as $recurring_cart_package_index => $recurring_cart_package ) {
+ // phpcs:ignore
+ /**
+ * @psalm-suppress UndefinedClass
+ */
+ $package_key = WC_Subscriptions_Cart::get_recurring_shipping_package_key( $recurring_cart_key, $recurring_cart_package_index );
+
+ // If the recurring cart package key is found in the previous chosen methods, but not in the current chosen methods, restore it.
+ if ( isset( $previous_chosen_methods[ $package_key ] ) && ! isset( $chosen_shipping_methods[ $package_key ] ) ) {
+ $chosen_shipping_methods[ $package_key ] = $previous_chosen_methods[ $package_key ];
+ }
+ }
+ }
+
+ WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
+ }
+
+ /**
+ * Gets the product data for the currently viewed page.
+ *
+ * @return mixed Returns false if not on a product page, the product information otherwise.
+ */
+ public function get_product_data() {
+ if ( ! $this->is_product() ) {
+ return false;
+ }
+
+ /** @var WC_Product_Variable $product */ // phpcs:ignore
+ $product = $this->get_product();
+ $currency = get_woocommerce_currency();
+
+ if ( 'variable' === $product->get_type() || 'variable-subscription' === $product->get_type() ) {
+ $variation_attributes = $product->get_variation_attributes();
+ $attributes = [];
+
+ foreach ( $variation_attributes as $attribute_name => $attribute_values ) {
+ $attribute_key = 'attribute_' . sanitize_title( $attribute_name );
+
+ // Passed value via GET takes precedence. Otherwise get the default value for given attribute.
+ $attributes[ $attribute_key ] = isset( $_GET[ $attribute_key ] ) // phpcs:ignore WordPress.Security.NonceVerification
+ ? wc_clean( wp_unslash( $_GET[ $attribute_key ] ) ) // phpcs:ignore WordPress.Security.NonceVerification
+ : $product->get_variation_default_attribute( $attribute_name );
+ }
+
+ $data_store = WC_Data_Store::load( 'product' );
+ $variation_id = $data_store->find_matching_product_variation( $product, $attributes );
+
+ if ( ! empty( $variation_id ) ) {
+ $product = wc_get_product( $variation_id );
+ }
+ }
+
+ try {
+ $price = $this->get_product_price( $product );
+ } catch ( Invalid_Price_Exception $e ) {
+ Logger::log( $e->getMessage() );
+ return false;
+ }
+
+ $data = [];
+ $items = [];
+
+ $items[] = [
+ 'label' => $product->get_name(),
+ 'amount' => WC_Payments_Utils::prepare_amount( $price, $currency ),
+ ];
+
+ $total_tax = 0;
+ foreach ( $this->get_taxes_like_cart( $product, $price ) as $tax ) {
+ $total_tax += $tax;
+
+ $items[] = [
+ 'label' => __( 'Tax', 'woocommerce-payments' ),
+ 'amount' => WC_Payments_Utils::prepare_amount( $tax, $currency ),
+ 'pending' => 0 === $tax,
+ ];
+ }
+
+ if ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() ) {
+ $items[] = [
+ 'label' => __( 'Shipping', 'woocommerce-payments' ),
+ 'amount' => 0,
+ 'pending' => true,
+ ];
+
+ $data['shippingOptions'] = [
+ 'id' => 'pending',
+ 'label' => __( 'Pending', 'woocommerce-payments' ),
+ 'detail' => '',
+ 'amount' => 0,
+ ];
+ }
+
+ $data['displayItems'] = $items;
+ $data['total'] = [
+ 'label' => apply_filters( 'wcpay_payment_request_total_label', $this->get_total_label() ),
+ 'amount' => WC_Payments_Utils::prepare_amount( $price + $total_tax, $currency ),
+ 'pending' => true,
+ ];
+
+ $data['needs_shipping'] = ( wc_shipping_enabled() && 0 !== wc_get_shipping_method_count( true ) && $product->needs_shipping() );
+ $data['currency'] = strtolower( $currency );
+ $data['country_code'] = substr( get_option( 'woocommerce_default_country' ), 0, 2 );
+
+ return apply_filters( 'wcpay_payment_request_product_data', $data, $product );
+ }
+
+ /**
+ * Whether product page has a supported product.
+ *
+ * @return boolean
+ */
+ private function is_product_supported() {
+ $product = $this->get_product();
+ $is_supported = true;
+
+ /**
+ * Ignore undefined classes from 3rd party plugins.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ if ( is_null( $product )
+ || ! is_object( $product )
+ || ! in_array( $product->get_type(), $this->supported_product_types(), true )
+ || ( class_exists( 'WC_Subscriptions_Product' ) && $product->needs_shipping() && WC_Subscriptions_Product::get_trial_length( $product ) > 0 ) // Trial subscriptions with shipping are not supported.
+ || ( class_exists( 'WC_Pre_Orders_Product' ) && WC_Pre_Orders_Product::product_is_charged_upon_release( $product ) ) // Pre Orders charge upon release not supported.
+ || ( class_exists( 'WC_Composite_Products' ) && $product->is_type( 'composite' ) ) // Composite products are not supported on the product page.
+ || ( class_exists( 'WC_Mix_and_Match' ) && $product->is_type( 'mix-and-match' ) ) // Mix and match products are not supported on the product page.
+ ) {
+ $is_supported = false;
+ } elseif ( class_exists( 'WC_Product_Addons_Helper' ) ) {
+ // File upload addon not supported.
+ $product_addons = WC_Product_Addons_Helper::get_product_addons( $product->get_id() );
+ foreach ( $product_addons as $addon ) {
+ if ( 'file_upload' === $addon['type'] ) {
+ $is_supported = false;
+ break;
+ }
+ }
+ }
+
+ return apply_filters( 'wcpay_payment_request_is_product_supported', $is_supported, $product );
+ }
+
+ /**
+ * Gets the product total price.
+ *
+ * @param object $product WC_Product_* object.
+ * @param bool $is_deposit Whether customer is paying a deposit.
+ * @param int $deposit_plan_id The ID of the deposit plan.
+ * @return mixed Total price.
+ *
+ * @throws Invalid_Price_Exception Whenever a product has no price.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ public function get_product_price( $product, ?bool $is_deposit = null, int $deposit_plan_id = 0 ) {
+ // If prices should include tax, using tax inclusive price.
+ if ( $this->cart_prices_include_tax() ) {
+ $base_price = wc_get_price_including_tax( $product );
+ } else {
+ $base_price = wc_get_price_excluding_tax( $product );
+ }
+
+ // If WooCommerce Deposits is active, we need to get the correct price for the product.
+ if ( class_exists( 'WC_Deposits_Product_Manager' ) && class_exists( 'WC_Deposits_Plans_Manager' ) && WC_Deposits_Product_Manager::deposits_enabled( $product->get_id() ) ) {
+ if ( is_null( $is_deposit ) ) {
+ /**
+ * If is_deposit is null, we use the default deposit type for the product.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ $is_deposit = 'deposit' === WC_Deposits_Product_Manager::get_deposit_selected_type( $product->get_id() );
+ }
+ if ( $is_deposit ) {
+ /**
+ * Ignore undefined classes from 3rd party plugins.
+ *
+ * @psalm-suppress UndefinedClass
+ */
+ $deposit_type = WC_Deposits_Product_Manager::get_deposit_type( $product->get_id() );
+ $available_plan_ids = WC_Deposits_Plans_Manager::get_plan_ids_for_product( $product->get_id() );
+ // Default to first (default) plan if no plan is specified.
+ if ( 'plan' === $deposit_type && 0 === $deposit_plan_id && ! empty( $available_plan_ids ) ) {
+ $deposit_plan_id = $available_plan_ids[0];
+ }
+
+ // Ensure the selected plan is available for the product.
+ if ( 0 === $deposit_plan_id || in_array( $deposit_plan_id, $available_plan_ids, true ) ) {
+ $base_price = WC_Deposits_Product_Manager::get_deposit_amount( $product, $deposit_plan_id, 'display', $base_price );
+ }
+ }
+ }
+
+ // Add subscription sign-up fees to product price.
+ $sign_up_fee = 0;
+ $subscription_types = [
+ 'subscription',
+ 'subscription_variation',
+ ];
+ if ( in_array( $product->get_type(), $subscription_types, true ) && class_exists( 'WC_Subscriptions_Product' ) ) {
+ // When there is no sign-up fee, `get_sign_up_fee` falls back to an int 0.
+ $sign_up_fee = WC_Subscriptions_Product::get_sign_up_fee( $product );
+ }
+
+ if ( ! is_numeric( $base_price ) || ! is_numeric( $sign_up_fee ) ) {
+ $error_message = sprintf(
+ // Translators: %d is the numeric ID of the product without a price.
+ __( 'Express checkout does not support products without prices! Please add a price to product #%d', 'woocommerce-payments' ),
+ (int) $product->get_id()
+ );
+ throw new Invalid_Price_Exception(
+ esc_html( $error_message )
+ );
+ }
+
+ return $base_price + $sign_up_fee;
+ }
+
+ /**
+ * Calculates taxes as displayed on cart, based on a product and a particular price.
+ *
+ * @param WC_Product $product The product, for retrieval of tax classes.
+ * @param float $price The price, which to calculate taxes for.
+ * @return array An array of final taxes.
+ */
+ private function get_taxes_like_cart( $product, $price ) {
+ if ( ! wc_tax_enabled() || $this->cart_prices_include_tax() ) {
+ // Only proceed when taxes are enabled, but not included.
+ return [];
+ }
+
+ // Follows the way `WC_Cart_Totals::get_item_tax_rates()` works.
+ $tax_class = $product->get_tax_class();
+ $rates = WC_Tax::get_rates( $tax_class );
+ // No cart item, `woocommerce_cart_totals_get_item_tax_rates` can't be applied here.
+
+ // Normally there should be a single tax, but `calc_tax` returns an array, let's use it.
+ return WC_Tax::calc_tax( $price, $rates, false );
+ }
+
+ /**
+ * Gets the normalized state/county field because in some
+ * cases, the state/county field is formatted differently from
+ * what WC is expecting and throws an error. An example
+ * for Ireland, the county dropdown in Chrome shows "Co. Clare" format.
+ *
+ * @param string $state Full state name or an already normalized abbreviation.
+ * @param string $country Two-letter country code.
+ *
+ * @return string Normalized state abbreviation.
+ */
+ public function get_normalized_state( $state, $country ) {
+ // If it's empty or already normalized, skip.
+ if ( ! $state || $this->is_normalized_state( $state, $country ) ) {
+ return $state;
+ }
+
+ // Try to match state from the Payment Request API list of states.
+ $state = $this->get_normalized_state_from_pr_states( $state, $country );
+
+ // If it's normalized, return.
+ if ( $this->is_normalized_state( $state, $country ) ) {
+ return $state;
+ }
+
+ // If the above doesn't work, fallback to matching against the list of translated
+ // states from WooCommerce.
+ return $this->get_normalized_state_from_wc_states( $state, $country );
+ }
+
+ /**
+ * The Payment Request API provides its own validation for the address form.
+ * For some countries, it might not provide a state field, so we need to return a more descriptive
+ * error message, indicating that the Payment Request button is not supported for that country.
+ */
+ public static function validate_state() {
+ $wc_checkout = WC_Checkout::instance();
+ $posted_data = $wc_checkout->get_posted_data();
+ $checkout_fields = $wc_checkout->get_checkout_fields();
+ $countries = WC()->countries->get_countries();
+
+ $is_supported = true;
+ // Checks if billing state is missing and is required.
+ if ( ! empty( $checkout_fields['billing']['billing_state']['required'] ) && '' === $posted_data['billing_state'] ) {
+ $is_supported = false;
+ }
+
+ // Checks if shipping state is missing and is required.
+ if ( WC()->cart->needs_shipping_address() && ! empty( $checkout_fields['shipping']['shipping_state']['required'] ) && '' === $posted_data['shipping_state'] ) {
+ $is_supported = false;
+ }
+
+ if ( ! $is_supported ) {
+ wc_add_notice(
+ sprintf(
+ /* translators: %s: country. */
+ __( 'The payment request button is not supported in %s because some required fields couldn\'t be verified. Please proceed to the checkout page and try again.', 'woocommerce-payments' ),
+ $countries[ $posted_data['billing_country'] ] ?? $posted_data['billing_country']
+ ),
+ 'error'
+ );
+ }
+ }
+
+ /**
+ * Normalizes billing and shipping state fields.
+ */
+ public function normalize_state() {
+ check_ajax_referer( 'woocommerce-process_checkout', '_wpnonce' );
+
+ $billing_country = ! empty( $_POST['billing_country'] ) ? wc_clean( wp_unslash( $_POST['billing_country'] ) ) : '';
+ $shipping_country = ! empty( $_POST['shipping_country'] ) ? wc_clean( wp_unslash( $_POST['shipping_country'] ) ) : '';
+ $billing_state = ! empty( $_POST['billing_state'] ) ? wc_clean( wp_unslash( $_POST['billing_state'] ) ) : '';
+ $shipping_state = ! empty( $_POST['shipping_state'] ) ? wc_clean( wp_unslash( $_POST['shipping_state'] ) ) : '';
+
+ if ( $billing_state && $billing_country ) {
+ $_POST['billing_state'] = $this->get_normalized_state( $billing_state, $billing_country );
+ }
+
+ if ( $shipping_state && $shipping_country ) {
+ $_POST['shipping_state'] = $this->get_normalized_state( $shipping_state, $shipping_country );
+ }
+ }
+
+ /**
+ * Checks if given state is normalized.
+ *
+ * @param string $state State.
+ * @param string $country Two-letter country code.
+ *
+ * @return bool Whether state is normalized or not.
+ */
+ public function is_normalized_state( $state, $country ) {
+ $wc_states = WC()->countries->get_states( $country );
+ return is_array( $wc_states ) && array_key_exists( $state, $wc_states );
+ }
+
+ /**
+ * Get normalized state from Payment Request API dropdown list of states.
+ *
+ * @param string $state Full state name or state code.
+ * @param string $country Two-letter country code.
+ *
+ * @return string Normalized state or original state input value.
+ */
+ public function get_normalized_state_from_pr_states( $state, $country ) {
+ // Include Payment Request API State list for compatibility with WC countries/states.
+ include_once WCPAY_ABSPATH . 'includes/constants/class-payment-request-button-states.php';
+ $pr_states = \WCPay\Constants\Payment_Request_Button_States::STATES;
+
+ if ( ! isset( $pr_states[ $country ] ) ) {
+ return $state;
+ }
+
+ foreach ( $pr_states[ $country ] as $wc_state_abbr => $pr_state ) {
+ $sanitized_state_string = $this->sanitize_string( $state );
+ // Checks if input state matches with Payment Request state code (0), name (1) or localName (2).
+ if (
+ ( ! empty( $pr_state[0] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[0] ) ) ||
+ ( ! empty( $pr_state[1] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[1] ) ) ||
+ ( ! empty( $pr_state[2] ) && $sanitized_state_string === $this->sanitize_string( $pr_state[2] ) )
+ ) {
+ return $wc_state_abbr;
+ }
+ }
+
+ return $state;
+ }
+
+ /**
+ * Get normalized state from WooCommerce list of translated states.
+ *
+ * @param string $state Full state name or state code.
+ * @param string $country Two-letter country code.
+ *
+ * @return string Normalized state or original state input value.
+ */
+ public function get_normalized_state_from_wc_states( $state, $country ) {
+ $wc_states = WC()->countries->get_states( $country );
+
+ if ( is_array( $wc_states ) ) {
+ foreach ( $wc_states as $wc_state_abbr => $wc_state_value ) {
+ if ( preg_match( '/' . preg_quote( $wc_state_value, '/' ) . '/i', $state ) ) {
+ return $wc_state_abbr;
+ }
+ }
+ }
+
+ return $state;
+ }
+
+ /**
+ * Normalizes postal code in case of redacted data from Apple Pay.
+ *
+ * @param string $postcode Postal code.
+ * @param string $country Country.
+ */
+ public function get_normalized_postal_code( $postcode, $country ) {
+ /**
+ * Currently, Apple Pay truncates the UK and Canadian postal codes to the first 4 and 3 characters respectively
+ * when passing it back from the shippingcontactselected object. This causes WC to invalidate
+ * the postal code and not calculate shipping zones correctly.
+ */
+ if ( Country_Code::UNITED_KINGDOM === $country ) {
+ // Replaces a redacted string with something like N1C0000.
+ return str_pad( preg_replace( '/\s+/', '', $postcode ), 7, '0' );
+ }
+ if ( Country_Code::CANADA === $country ) {
+ // Replaces a redacted string with something like H3B000.
+ return str_pad( preg_replace( '/\s+/', '', $postcode ), 6, '0' );
+ }
+
+ return $postcode;
+ }
+
+ /**
+ * Sanitize string for comparison.
+ *
+ * @param string $string String to be sanitized.
+ *
+ * @return string The sanitized string.
+ */
+ public function sanitize_string( $string ) {
+ return trim( wc_strtolower( remove_accents( $string ) ) );
+ }
+
+ /**
+ * Updates shipping method in WC session
+ *
+ * @param array $shipping_methods Array of selected shipping methods ids.
+ */
+ public function update_shipping_method( $shipping_methods ) {
+ $chosen_shipping_methods = (array) WC()->session->get( 'chosen_shipping_methods' );
+
+ if ( is_array( $shipping_methods ) ) {
+ foreach ( $shipping_methods as $i => $value ) {
+ $chosen_shipping_methods[ $i ] = wc_clean( $value );
+ }
+ }
+
+ WC()->session->set( 'chosen_shipping_methods', $chosen_shipping_methods );
+ }
+
+ /**
+ * Calculate and set shipping method.
+ *
+ * @param array $address Shipping address.
+ */
+ protected function calculate_shipping( $address = [] ) {
+ $country = $address['country'];
+ $state = $address['state'];
+ $postcode = $address['postcode'];
+ $city = $address['city'];
+ $address_1 = $address['address_1'];
+ $address_2 = $address['address_2'];
+
+ // Normalizes state to calculate shipping zones.
+ $state = $this->get_normalized_state( $state, $country );
+
+ // Normalizes postal code in case of redacted data from Apple Pay.
+ $postcode = $this->get_normalized_postal_code( $postcode, $country );
+
+ WC()->shipping->reset_shipping();
+
+ if ( $postcode && WC_Validation::is_postcode( $postcode, $country ) ) {
+ $postcode = wc_format_postcode( $postcode, $country );
+ }
+
+ if ( $country ) {
+ WC()->customer->set_location( $country, $state, $postcode, $city );
+ WC()->customer->set_shipping_location( $country, $state, $postcode, $city );
+ } else {
+ WC()->customer->set_billing_address_to_base();
+ WC()->customer->set_shipping_address_to_base();
+ }
+
+ WC()->customer->set_calculated_shipping( true );
+ WC()->customer->save();
+
+ $packages = [];
+
+ $packages[0]['contents'] = WC()->cart->get_cart();
+ $packages[0]['contents_cost'] = 0;
+ $packages[0]['applied_coupons'] = WC()->cart->applied_coupons;
+ $packages[0]['user']['ID'] = get_current_user_id();
+ $packages[0]['destination']['country'] = $country;
+ $packages[0]['destination']['state'] = $state;
+ $packages[0]['destination']['postcode'] = $postcode;
+ $packages[0]['destination']['city'] = $city;
+ $packages[0]['destination']['address'] = $address_1;
+ $packages[0]['destination']['address_2'] = $address_2;
+
+ foreach ( WC()->cart->get_cart() as $item ) {
+ if ( $item['data']->needs_shipping() ) {
+ if ( isset( $item['line_total'] ) ) {
+ $packages[0]['contents_cost'] += $item['line_total'];
+ }
+ }
+ }
+
+ $packages = apply_filters( 'woocommerce_cart_shipping_packages', $packages );
+
+ WC()->shipping->calculate_shipping( $packages );
+ }
}
diff --git a/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php b/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php
index 0095bc70559..155f99e1a4d 100644
--- a/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php
+++ b/includes/multi-currency/Compatibility/WooCommerceNameYourPrice.php
@@ -63,7 +63,7 @@ public function add_initial_currency( $cart_item, $product_id, $variation_id ) {
$nyp_id = $variation_id ? $variation_id : $product_id;
- if ( \WC_Name_Your_Price_Helpers::is_nyp( $nyp_id ) && isset( $cart_item['nyp'] ) ) {
+ if ( class_exists( '\WC_Name_Your_Price_Helpers' ) && \WC_Name_Your_Price_Helpers::is_nyp( $nyp_id ) && isset( $cart_item['nyp'] ) ) {
$currency = $this->multi_currency->get_selected_currency();
$cart_item['nyp_currency'] = $currency->get_code();
$cart_item['nyp_original'] = $cart_item['nyp'];
@@ -102,6 +102,7 @@ public function convert_cart_currency( $cart_item, $values ) {
$cart_item['nyp'] = $this->multi_currency->get_raw_conversion( $raw_price, $selected_currency->get_code(), $from_currency );
}
+ // @phpstan-ignore-next-line.
$cart_item = WC_Name_Your_Price()->cart->set_cart_item( $cart_item );
}
@@ -130,7 +131,7 @@ public function should_convert_product_price( bool $return, $product ): bool {
}
// Check to see if the product is a NYP product.
- if ( \WC_Name_Your_Price_Helpers::is_nyp( $product ) ) {
+ if ( class_exists( '\WC_Name_Your_Price_Helpers' ) && \WC_Name_Your_Price_Helpers::is_nyp( $product ) ) {
return false;
}
diff --git a/includes/multi-currency/Compatibility/WooCommerceProductAddOns.php b/includes/multi-currency/Compatibility/WooCommerceProductAddOns.php
index f3e04db64fc..adc5cb9dabb 100644
--- a/includes/multi-currency/Compatibility/WooCommerceProductAddOns.php
+++ b/includes/multi-currency/Compatibility/WooCommerceProductAddOns.php
@@ -125,8 +125,10 @@ public function get_item_data( $addon_data, $addon, $cart_item ): array {
// Quantity/multiplier add on needs to be split, calculated, then multiplied by input value.
$price = $this->multi_currency->get_price( $addon['price'] / $addon['value'], 'product' ) * $addon['value'];
}
- $price = \WC_Product_Addons_Helper::get_product_addon_price_for_display( $price, $cart_item['data'] );
- $name .= ' (' . wc_price( $price ) . ')';
+ if ( class_exists( '\WC_Product_Addons_Helper' ) ) {
+ $price = \WC_Product_Addons_Helper::get_product_addon_price_for_display( $price, $cart_item['data'] );
+ $name .= ' (' . wc_price( $price ) . ')';
+ }
} else {
// Get the percentage cost in the currency in use, and set the meta data on the product that the value was converted.
$_product = wc_get_product( $cart_item['product_id'] );
@@ -245,12 +247,14 @@ public function order_line_item_meta( array $meta_data, array $addon, \WC_Order_
// Convert all others.
$addon_price = $this->multi_currency->get_price( $addon['price'], 'product' );
}
- $price = html_entity_decode(
- wp_strip_all_tags( wc_price( \WC_Product_Addons_Helper::get_product_addon_price_for_display( $addon_price, $values['data'] ) ) ),
- ENT_QUOTES,
- get_bloginfo( 'charset' )
- );
- $addon['name'] .= ' (' . $price . ')';
+ if ( class_exists( '\WC_Product_Addons_Helper' ) ) {
+ $price = html_entity_decode(
+ wp_strip_all_tags( wc_price( \WC_Product_Addons_Helper::get_product_addon_price_for_display( $addon_price, $values['data'] ) ) ),
+ ENT_QUOTES,
+ get_bloginfo( 'charset' )
+ );
+ $addon['name'] .= ' (' . $price . ')';
+ }
}
if ( 'custom_price' === $addon['field_type'] ) {
diff --git a/includes/payment-methods/class-afterpay-payment-method.php b/includes/payment-methods/class-afterpay-payment-method.php
index 01690b64f1b..f4d1ef541aa 100644
--- a/includes/payment-methods/class-afterpay-payment-method.php
+++ b/includes/payment-methods/class-afterpay-payment-method.php
@@ -72,8 +72,10 @@ public function __construct( $token_service ) {
* Returns payment method title.
*
* @param string|null $account_country Country of merchants account.
- * @param array|false $payment_details Optional payment details from charge object.
+ * @param array|false $payment_details Payment details from charge object. Not used by this class.
* @return string|null
+ *
+ * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
*/
public function get_title( string $account_country = null, $payment_details = false ) {
if ( 'GB' === $account_country ) {
diff --git a/includes/payment-methods/class-upe-payment-method.php b/includes/payment-methods/class-upe-payment-method.php
index 51d091328fd..dabe8b1eeac 100644
--- a/includes/payment-methods/class-upe-payment-method.php
+++ b/includes/payment-methods/class-upe-payment-method.php
@@ -130,6 +130,8 @@ public function get_id() {
* @param array|false $payment_details Optional payment details from charge object.
*
* @return string
+ *
+ * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
*/
public function get_title( string $account_country = null, $payment_details = false ) {
return $this->title;
@@ -260,6 +262,8 @@ abstract public function get_testing_instructions();
*
* @param string|null $account_country Optional account country.
* @return string
+ *
+ * @phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
*/
public function get_icon( string $account_country = null ) {
return isset( $this->icon_url ) ? $this->icon_url : '';
diff --git a/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php b/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php
index 0834d078eeb..20cba8bc6cf 100644
--- a/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php
+++ b/includes/reports/class-wc-rest-payments-reports-authorizations-controller.php
@@ -40,7 +40,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P
\w+)',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)',
[
[
'methods' => WP_REST_Server::READABLE,
diff --git a/includes/reports/class-wc-rest-payments-reports-transactions-controller.php b/includes/reports/class-wc-rest-payments-reports-transactions-controller.php
index 1e38d6cd746..6e130400af2 100644
--- a/includes/reports/class-wc-rest-payments-reports-transactions-controller.php
+++ b/includes/reports/class-wc-rest-payments-reports-transactions-controller.php
@@ -39,7 +39,7 @@ public function register_routes() {
);
register_rest_route(
$this->namespace,
- '/' . $this->rest_base . '/(?P\w+)',
+ '/' . $this->rest_base . '/(?P[A-Za-z0-9_\-]+)',
[
[
'methods' => WP_REST_Server::READABLE,
diff --git a/includes/wc-payment-api/class-wc-payments-api-client.php b/includes/wc-payment-api/class-wc-payments-api-client.php
index 8c3e6c29ba9..4127e620606 100644
--- a/includes/wc-payment-api/class-wc-payments-api-client.php
+++ b/includes/wc-payment-api/class-wc-payments-api-client.php
@@ -518,8 +518,17 @@ public function get_disputes( array $filters = [] ) {
*
* @param string $dispute_id id of requested dispute.
* @return array dispute object.
+ * @throws API_Exception - Exception thrown in case route validation fails.
*/
public function get_dispute( $dispute_id ) {
+ if ( ! preg_match( '/(dp|dispute)_[A-Za-z0-9]+/', $dispute_id ) ) {
+ throw new API_Exception(
+ __( 'Route param validation failed.', 'woocommerce-payments' ),
+ 'wcpay_route_validation_failure',
+ 400
+ );
+ }
+
$dispute = $this->request( [], self::DISPUTES_API . '/' . $dispute_id, self::GET );
if ( is_wp_error( $dispute ) ) {
@@ -726,8 +735,17 @@ public function create_token( $request ) {
* @return array
*
* @throws Exception - Exception thrown on request failure.
+ * @throws API_Exception - Exception thrown in case route validation fails.
*/
public function get_timeline( $id ) {
+ if ( ! preg_match( '/(ch|pi|py)_[A-Za-z0-9]+/', $id ) ) {
+ throw new API_Exception(
+ __( 'Route param validation failed.', 'woocommerce-payments' ),
+ 'wcpay_route_validation_failure',
+ 400
+ );
+ }
+
$timeline = $this->request( [], self::TIMELINE_API . '/' . $id, self::GET );
$has_fraud_outcome_event = false;
@@ -954,36 +972,6 @@ public function get_onboarding_business_types(): array {
return $business_types;
}
- /**
- * Get the required verification information, needed for our KYC onboarding flow.
- *
- * @param string $country_code The country code.
- * @param string $type The business type.
- * @param string|null $structure The business structure (optional).
- *
- * @return array An array containing the required verification information.
- *
- * @throws API_Exception Exception thrown on request failure.
- */
- public function get_onboarding_required_verification_information( string $country_code, string $type, $structure = null ) {
- $params = [
- 'country' => $country_code,
- 'type' => $type,
- ];
-
- if ( ! is_null( $structure ) ) {
- $params = array_merge( $params, [ 'structure' => $structure ] );
- }
-
- return $this->request(
- $params,
- self::ONBOARDING_API . '/required_verification_information',
- self::GET,
- true,
- true
- );
- }
-
/**
* Get a link's details from the server.
*
@@ -1199,6 +1187,14 @@ public function update_charge( string $charge_id, array $data = [] ) {
* @throws API_Exception
*/
public function get_charge( string $charge_id ) {
+ if ( ! preg_match( '/(ch|pi|py)_[A-Za-z0-9]+/', $charge_id ) ) {
+ throw new API_Exception(
+ __( 'Route param validation failed.', 'woocommerce-payments' ),
+ 'wcpay_route_validation_failure',
+ 400
+ );
+ }
+
return $this->request(
[],
self::CHARGES_API . '/' . $charge_id,
diff --git a/includes/woopay/class-woopay-adapted-extensions.php b/includes/woopay/class-woopay-adapted-extensions.php
index 0180525f726..5b9c5c8643e 100644
--- a/includes/woopay/class-woopay-adapted-extensions.php
+++ b/includes/woopay/class-woopay-adapted-extensions.php
@@ -171,7 +171,7 @@ public function get_extension_data() {
];
}
- if ( $this->is_affiliate_for_woocommerce_enabled() ) {
+ if ( $this->is_affiliate_for_woocommerce_enabled() && function_exists( 'afwc_get_referrer_id' ) ) {
/**
* Suppress psalm warning.
*
@@ -207,12 +207,14 @@ public function update_order_extension_data( $order_id ) {
) {
$affiliate_id = (int) wc_clean( wp_unslash( $_GET['affiliate'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
- // phpcs:ignore
- /**
- * @psalm-suppress UndefinedClass
- */
- $affiliate_api = \AFWC_API::get_instance();
- $affiliate_api->track_conversion( $order_id, $affiliate_id, '', [ 'is_affiliate_eligible' => true ] );
+ if ( class_exists( '\AFWC_API' ) ) {
+ // phpcs:ignore
+ /**
+ * @psalm-suppress UndefinedClass
+ */
+ $affiliate_api = \AFWC_API::get_instance();
+ $affiliate_api->track_conversion( $order_id, $affiliate_id, '', [ 'is_affiliate_eligible' => true ] );
+ }
}
}
@@ -272,7 +274,10 @@ class_exists( '\AutomateWoo\Referrals\Referral_Manager' ) &&
* @return string|null
*/
private function get_automate_woo_advocate_id_from_cookie() {
- $advocate_from_key_cookie = \AutomateWoo\Referrals\Referral_Manager::get_advocate_key_from_cookie();
- return $advocate_from_key_cookie ? $advocate_from_key_cookie->get_advocate_id() : null;
+ if ( class_exists( '\AutomateWoo\Referrals\Referral_Manager' ) ) {
+ $advocate_from_key_cookie = \AutomateWoo\Referrals\Referral_Manager::get_advocate_key_from_cookie();
+ return $advocate_from_key_cookie ? $advocate_from_key_cookie->get_advocate_id() : null;
+ }
+ return null;
}
}
diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php
index 11befdcc6cd..06fc2a93a35 100644
--- a/includes/woopay/class-woopay-session.php
+++ b/includes/woopay/class-woopay-session.php
@@ -377,6 +377,38 @@ private static function get_checkout_data( $woopay_request ) {
return $checkout_data;
}
+ /**
+ * Retrieves the user email from the current session.
+ *
+ * @param \WP_User $user The user object.
+ * @return string The user email.
+ */
+ private static function get_user_email( $user ) {
+ if ( ! empty( $_POST['email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
+ return sanitize_email( wp_unslash( $_POST['email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
+ }
+
+ if ( ! empty( $_GET['email'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
+ return sanitize_email( wp_unslash( $_GET['email'] ) ); // phpcs:ignore WordPress.Security.NonceVerification
+ }
+
+ if ( ! empty( $_POST['encrypted_data'] ) && is_array( $_POST['encrypted_data'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
+ // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash
+ $decrypted_data = WooPay_Utilities::decrypt_signed_data( $_POST['encrypted_data'] );
+
+ if ( ! empty( $decrypted_data['user_email'] ) ) {
+ return sanitize_email( wp_unslash( $decrypted_data['user_email'] ) );
+ }
+ }
+
+ // As a last resort, we try to get the email from the customer logged in the store.
+ if ( $user->exists() ) {
+ return $user->user_email;
+ }
+
+ return '';
+ }
+
/**
* Returns the initial session request data.
*
@@ -424,13 +456,12 @@ public static function get_init_session_request( $order_id = null, $key = null,
$cart_data = self::get_cart_data( $is_pay_for_order, $order_id, $key, $billing_email, $woopay_request );
$checkout_data = self::get_checkout_data( $woopay_request );
+ $email = self::get_user_email( $user );
if ( $woopay_request ) {
$order_id = $checkout_data['order_id'] ?? null;
}
- $email = ! empty( $_POST['email'] ) ? wc_clean( wp_unslash( $_POST['email'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification
-
$request = [
'wcpay_version' => WCPAY_VERSION_NUMBER,
'user_id' => $user->ID,
diff --git a/includes/woopay/class-woopay-store-api-token.php b/includes/woopay/class-woopay-store-api-token.php
index 0601b410350..b8dfb72a433 100644
--- a/includes/woopay/class-woopay-store-api-token.php
+++ b/includes/woopay/class-woopay-store-api-token.php
@@ -55,6 +55,7 @@ public function get_args() {
* @psalm-suppress UndefinedMethod
*/
public function get_cart_token() {
+ // @phpstan-ignore-next-line.
return parent::get_cart_token();
}
}
diff --git a/includes/woopay/class-woopay-utilities.php b/includes/woopay/class-woopay-utilities.php
index 588285246cd..3c6116c2367 100644
--- a/includes/woopay/class-woopay-utilities.php
+++ b/includes/woopay/class-woopay-utilities.php
@@ -288,6 +288,42 @@ public static function encrypt_and_sign_data( $data ) {
];
}
+ /**
+ * Decode encrypted and signed data and return it.
+ *
+ * @param array $data The session, iv, and hash data for the encryption.
+ * @return mixed The decoded data.
+ */
+ public static function decrypt_signed_data( $data ) {
+ $store_blog_token = ( self::get_woopay_url() === self::DEFAULT_WOOPAY_URL ) ? Jetpack_Options::get_option( 'blog_token' ) : 'dev_mode';
+
+ if ( empty( $store_blog_token ) ) {
+ return null;
+ }
+
+ // Decode the data.
+ $decoded_data_request = array_map( 'base64_decode', $data );
+
+ // Verify the HMAC hash before decryption to ensure data integrity.
+ $computed_hash = hash_hmac( 'sha256', $decoded_data_request['iv'] . $decoded_data_request['data'], $store_blog_token );
+
+ // If the hashes don't match, the message may have been tampered with.
+ if ( ! hash_equals( $computed_hash, $decoded_data_request['hash'] ) ) {
+ return null;
+ }
+
+ // Decipher the data using the blog token and the IV.
+ $decrypted_data = openssl_decrypt( $decoded_data_request['data'], 'aes-256-cbc', $store_blog_token, OPENSSL_RAW_DATA, $decoded_data_request['iv'] );
+
+ if ( false === $decrypted_data ) {
+ return null;
+ }
+
+ $decrypted_data = json_decode( $decrypted_data, true );
+
+ return $decrypted_data;
+ }
+
/**
* Get the persisted available countries.
*
diff --git a/includes/woopay/services/class-checkout-service.php b/includes/woopay/services/class-checkout-service.php
index f10ca7bad7b..b776d505f27 100644
--- a/includes/woopay/services/class-checkout-service.php
+++ b/includes/woopay/services/class-checkout-service.php
@@ -64,7 +64,7 @@ public function create_and_confirm_setup_intention_request( Request $base_reques
*/
public function is_platform_payment_method( Payment_Information $payment_information ) {
// Return false for express checkout method.
- if ( isset( $_POST['payment_request_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
+ if ( isset( $_POST['payment_request_type'] ) || isset( $_POST['express_payment_type'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return false;
}
diff --git a/package-lock.json b/package-lock.json
index 7510fd8e2ef..60e9f7f8a8a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "woocommerce-payments",
- "version": "7.7.0",
+ "version": "7.8.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "woocommerce-payments",
- "version": "7.7.0",
+ "version": "7.8.0",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
diff --git a/package.json b/package.json
index 11ff44e46c4..5c001ab691d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "woocommerce-payments",
- "version": "7.7.0",
+ "version": "7.8.0",
"main": "webpack.config.js",
"author": "Automattic",
"license": "GPL-3.0-or-later",
diff --git a/readme.txt b/readme.txt
index 1aa88ad593b..f0756550b7f 100644
--- a/readme.txt
+++ b/readme.txt
@@ -4,7 +4,7 @@ Tags: woocommerce payments, apple pay, credit card, google pay, payment, payment
Requires at least: 6.0
Tested up to: 6.5
Requires PHP: 7.3
-Stable tag: 7.7.0
+Stable tag: 7.8.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -94,6 +94,46 @@ Please note that our support for the checkout block is still experimental and th
== Changelog ==
+= 7.8.0 - 2024-06-19 =
+* Add - Add a feedback survey modal upon deactivation.
+* Add - Add new select component to be used for reporting filters, e.g. Payments overview currency select
+* Add - Add payment processing using ECE in the Blocks checkout and cart pages.
+* Add - Add the WooPay Direct Checkout flow to the classic mini cart widget.
+* Add - Add woocommerce-return-previous-exceptions filter
+* Add - Enable adapted extensions compatibility with Direct Checkout.
+* Add - feat: add pay-for-order support w/ tokenized cart PRBs
+* Add - Fix ECE not working without WooPay.
+* Add - Reset notifications about duplicate enabled payment methods when new plugins are enabling them.
+* Fix - Fall back to credit card as default payment method when a payment method is toggled off.
+* Fix - fix: address normalization on checkout for tokenized cart PRBs
+* Fix - fix: itemized totals & pending amount on tokenized cart
+* Fix - fix: Store API tokenized cart payment method title
+* Fix - Fixes some cases where redirects to the onboarding will open in a new tab.
+* Fix - Fix input-specific credit card errors.
+* Fix - Fix Payment method title for PRBs not displaying correctly because of ECE code.
+* Fix - Fix Teams for WooCommerce Memberships on product WooPay Express Checkout Button.
+* Fix - Fix WooPay Direct Checkout feature check.
+* Fix - Improve consistency of Manage button for different WooPayments KYC states
+* Fix - Make it so that the WooPay button is not triggered on Checkout pages when the "Enter" key is pressed on a keyboard.
+* Fix - Prevent account creation during WooPay preflight request.
+* Update - chore: update incompatibility notice wrapping
+* Update - Declare compatibility with the Cart and Checkout blocks.
+* Update - Improve the transition from the WCPay KYC to the WC Admin Payments Task
+* Update - Update the Payments Overview screen with a new currency selection UI for stores with multiple deposit currencies
+* Update - Use FILTER_SANITIZE_EMAIL to sanitize email input
+* Dev - Add New_Process_Payment_Exception
+* Dev - Add Order_ID_Mismatch_Exception
+* Dev - Add sh support in pre-push husky script.
+* Dev - Add validation for path variables.
+* Dev - Bump WooCommerce Tested To version to 8.9.2
+* Dev - Bump WooCommerce Tested To version to 8.9.3
+* Dev - chore: EPMs to always send shipping phone
+* Dev - Clean up and refactor some old code which is no longer in use.
+* Dev - Fix PHPStan warnings.
+* Dev - Fix unused parameter phpcs sniffs in checkout classes.
+* Dev - Improve test coverage of upe.js and rename isPaymentMethodRestrictedToLocation to hasPaymentMethodCountryRestrictions
+* Dev - Remove redundant wrapper around method invocation.
+
= 7.7.0 - 2024-05-29 =
* Add - Add share key query param when sending data to Stripe KYC.
* Add - Add the WooPay Direct Checkout flow to the blocks mini cart widget.
diff --git a/src/Internal/Payment/Factor.php b/src/Internal/Payment/Factor.php
index 3a63e895738..957c131fd7a 100644
--- a/src/Internal/Payment/Factor.php
+++ b/src/Internal/Payment/Factor.php
@@ -100,6 +100,12 @@ class Factor extends Base_Constant {
*/
const PAYMENT_REQUEST = 'PAYMENT_REQUEST';
+ /**
+ * ECE buttons (Google Pay and Apple Pay)
+ * Type: Entry point
+ */
+ const EXPRESS_CHECKOUT_ELEMENT = 'EXPRESS_CHECKOUT_ELEMENT';
+
/**
* Returns all possible factors.
*
@@ -121,6 +127,7 @@ public static function get_all_factors() {
static::IPP_CAPTURE(),
static::STRIPE_LINK(),
static::PAYMENT_REQUEST(),
+ static::EXPRESS_CHECKOUT_ELEMENT(),
];
}
}
diff --git a/src/Internal/Payment/State/AbstractPaymentState.php b/src/Internal/Payment/State/AbstractPaymentState.php
index 19649b7eadb..942fec96ca2 100644
--- a/src/Internal/Payment/State/AbstractPaymentState.php
+++ b/src/Internal/Payment/State/AbstractPaymentState.php
@@ -79,6 +79,7 @@ public function get_context(): PaymentContext {
* @throws PaymentRequestException When data is not available or invalid.
*/
public function start_processing( PaymentRequest $request ) {
+ // @phpstan-ignore-next-line
$this->throw_unavailable_method_exception( __METHOD__ );
}
@@ -92,6 +93,7 @@ public function start_processing( PaymentRequest $request ) {
* @throws StateTransitionException
*/
public function complete_processing() {
+ // @phpstan-ignore-next-line
$this->throw_unavailable_method_exception( __METHOD__ );
}
// phpcs:enable Squiz.Commenting.FunctionComment.InvalidNoReturn
@@ -103,15 +105,15 @@ public function complete_processing() {
* This method should only be called whenever the process is ready to transition
* to the next state, as each new state will be considered the payment's latest one.
*
- * @template ConcreteState
- * @param class-string | string $state_class The class of the state to crate.
+ * @template ConcreteState of AbstractPaymentState
+ * @param class-string $state_class The class of the state to create.
*
- * @return AbstractPaymentState | ConcreteState
+ * @return ConcreteState The generated payment state instance.
*
* @throws StateTransitionException In case the new state could not be created.
* @throws ContainerException When the dependency container cannot instantiate the state.
*/
- protected function create_state( string $state_class ) {
+ protected function create_state( /*class-string*/ $state_class ): AbstractPaymentState {
$state = $this->state_factory->create_state( $state_class, $this->context );
// This is where logging will be added.
diff --git a/src/Internal/Payment/State/StateFactory.php b/src/Internal/Payment/State/StateFactory.php
index fa9554f4e73..7d0b77c375c 100644
--- a/src/Internal/Payment/State/StateFactory.php
+++ b/src/Internal/Payment/State/StateFactory.php
@@ -38,15 +38,15 @@ public function __construct( Container $container ) {
/**
* Creates a new state based on class name.
*
- * @template ConcreteState
- * @param class-string | string $state_class Name of the state class.
- * @param PaymentContext $context Context for the new state.
+ * @template ConcreteState of AbstractPaymentState
+ * @param class-string $state_class Name of the state class.
+ * @param PaymentContext $context Context for the new state.
*
- * @return AbstractPaymentState | ConcreteState The generated payment state instance.
+ * @return ConcreteState The generated payment state instance.
* @throws ContainerException When the dependency container cannot instantiate the state.
* @throws StateTransitionException When the class name is not a state.
*/
- public function create_state( string $state_class, PaymentContext $context ): AbstractPaymentState {
+ public function create_state( /*class-string*/ $state_class, PaymentContext $context ): AbstractPaymentState {
if ( ! is_subclass_of( $state_class, AbstractPaymentState::class ) ) {
throw new StateTransitionException(
esc_html(
diff --git a/templates/plugins-page/plugins-page-wrapper.php b/templates/plugins-page/plugins-page-wrapper.php
new file mode 100644
index 00000000000..b89e5789c1a
--- /dev/null
+++ b/templates/plugins-page/plugins-page-wrapper.php
@@ -0,0 +1,24 @@
+
+
+
+
diff --git a/tests/e2e-pw/specs/basic.spec.ts b/tests/e2e-pw/specs/basic.spec.ts
index 910176fdc21..f8c49d71d70 100644
--- a/tests/e2e-pw/specs/basic.spec.ts
+++ b/tests/e2e-pw/specs/basic.spec.ts
@@ -22,8 +22,9 @@ test.describe(
'/wp-admin/admin.php?page=wc-admin&path=/payments/overview'
);
await page.waitForLoadState( 'domcontentloaded' );
- const logo = page.getByAltText( 'WooPayments logo' );
- await expect( logo ).toBeVisible();
+ await expect(
+ page.getByRole( 'heading', { name: 'Overview' } )
+ ).toBeVisible();
} );
} );
diff --git a/tests/e2e-pw/specs/merchant/merchant-payment-gateways-confirmation.spec.ts b/tests/e2e-pw/specs/merchant/merchant-payment-gateways-confirmation.spec.ts
index 6197cfa5fcb..f4981c22798 100644
--- a/tests/e2e-pw/specs/merchant/merchant-payment-gateways-confirmation.spec.ts
+++ b/tests/e2e-pw/specs/merchant/merchant-payment-gateways-confirmation.spec.ts
@@ -8,7 +8,8 @@ import { test, expect, Page } from '@playwright/test';
*/
import { useMerchant } from '../../utils/helpers';
-test.describe( 'payment gateways disable confirmation', () => {
+// Skipping the test for now as it is flaky on GH action runs. See #8875.
+test.skip( 'payment gateways disable confirmation', () => {
useMerchant();
const getToggle = ( page: Page ) =>
diff --git a/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js b/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js
index 903d5d53593..1d4239fe590 100644
--- a/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js
+++ b/tests/e2e/specs/wcpay/shopper/shopper-klarna-checkout.spec.js
@@ -102,70 +102,45 @@ describe( 'Klarna checkout', () => {
await shopper.placeOrder();
- // Klarna is rendered in an iframe, so we need to get its reference.
- // Sometimes the iframe is updated (or removed from the page),
- // this function has been created so that we always get the most updated reference.
- const getNewKlarnaIframe = async () => {
- const klarnaFrameHandle = await page.waitForSelector(
- '#klarna-apf-iframe'
- );
-
- return await klarnaFrameHandle.contentFrame();
- };
-
- let klarnaIframe = await getNewKlarnaIframe();
-
- const frameNavigationHandler = async ( frame ) => {
- if ( frame.url().includes( 'klarna.com' ) ) {
- const newKlarnaIframe = await getNewKlarnaIframe();
+ await page.waitForSelector( '#phone' );
- if ( frame === newKlarnaIframe ) {
- klarnaIframe = newKlarnaIframe;
- }
- }
- };
-
- // Add frame navigation event listener.
- page.on( 'framenavigated', frameNavigationHandler );
+ await page.waitFor( 2000 );
- // Waiting for the redirect & the Klarna iframe to load within the Stripe test page.
- // this is the "confirm phone number" page - we just click "continue".
- await klarnaIframe.waitForSelector( '#phone' );
- await klarnaIframe
+ await page
.waitForSelector( '#onContinue' )
.then( ( button ) => button.click() );
+ await page.waitFor( 2000 );
+
// This is where the OTP code is entered.
- await klarnaIframe.waitForSelector( '#phoneOtp' );
- await expect( klarnaIframe ).toFill( 'input#otp_field', '123456' );
+ await page.waitForSelector( '#phoneOtp' );
+
+ await page.waitFor( 2000 );
+
+ await expect( page ).toFill( 'input#otp_field', '123456' );
// Select Payment Plan - 4 weeks & click continue.
- await klarnaIframe
+ await page
.waitForSelector( 'button#pay_over_time__label' )
.then( ( button ) => button.click() );
await page.waitFor( 2000 );
- await klarnaIframe
+ await page
.waitForSelector( 'button[data-testid="select-payment-category"' )
.then( ( button ) => button.click() );
await page.waitFor( 2000 );
// Payment summary page. Click continue.
- await klarnaIframe
+ await page
.waitForSelector( 'button[data-testid="pick-plan"]' )
.then( ( button ) => button.click() );
await page.waitFor( 2000 );
- // At this point, the event listener is not needed anymore.
- page.removeListener( 'framenavigated', frameNavigationHandler );
-
- await page.waitFor( 2000 );
-
// Confirm payment.
- await klarnaIframe
+ await page
.waitForSelector( 'button#buy_button' )
.then( ( button ) => button.click() );
@@ -174,6 +149,8 @@ describe( 'Klarna checkout', () => {
waitUntil: 'networkidle0',
} );
+ await page.waitForSelector( 'h1.entry-title' );
+
await expect( page ).toMatch( 'Order received' );
} );
} );
diff --git a/tests/qit/README.md b/tests/qit/README.md
index a1d2e951709..896cc9b0ab9 100644
--- a/tests/qit/README.md
+++ b/tests/qit/README.md
@@ -1,18 +1,21 @@
## WooCommerce Payments QIT tests
-We currently only use the security tests from the [QIT toolkit](https://woocommerce.github.io/qit-documentation/#/) and these can be run locally.
+We currently only use the security tests from the [QIT toolkit](https://qit.woo.com/docs/) and these can be run locally.
#### Setup and running
- Create `local.env` inside the `tests/qit/config/` directory by copying the variables from `default.env`.
- To get the actual values for local config, refer to this [secret store](https://mc.a8c.com/secret-store/?secret_id=11043) link.
- Once configured, the first time you run the `npm` command, it should create a local auth file which will be used for subsequent runs.
-- For running, use:
+- Currently, two types of tests are available through the `npm` command: Security and PHPStan tests. PHPStan tests can also be run against the local development build.
+- For running, use one of the following commands based on your requirements:
```
- npm run test:qit
+ npm run test:qit-security
+ npm run test:qit-phpstan
+ npm run test:qit-phpstan-local
```
-- The command uses the `build:release` command to create `woocommerce-payments.zip` at the root of the directory which is then uploaded and used for the QIT tests.
+- The commands use the `build:release` to create `woocommerce-payments.zip` at the root of the directory which is then uploaded and used for the QIT tests.
#### Analysing results
diff --git a/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php b/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php
index 06606c14902..e7bc456c46b 100644
--- a/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php
+++ b/tests/unit/admin/test-class-wc-rest-payments-onboarding-controller.php
@@ -64,59 +64,6 @@ public function test_get_business_types() {
$this->assertSame( [ 'data' => $mock_business_types ], $response->get_data() );
}
- public function test_get_required_verification_information_with_missing_params() {
- $request = new WP_REST_Request( 'GET', '', [ 'foo' => 'bar' ] );
- $response = $this->controller->get_required_verification_information( $request );
-
- $this->assertSame( 400, $response->status );
- $this->assertSame(
- [ 'result' => WC_REST_Payments_Onboarding_Controller::RESULT_BAD_REQUEST ],
- $response->get_data()
- );
- }
-
- public function test_get_required_verification_information() {
- $mock_requirements = [
- 'business_profile.url',
- 'business_profile.mcc',
- 'representative.first_name',
- 'representative.last_name',
- 'representative.dob.day',
- 'representative.dob.month',
- 'representative.dob.year',
- 'representative.phone',
- 'representative.email',
- 'representative.address.line1',
- 'representative.address.postal_code',
- 'representative.address.city',
- 'representative.address.state',
- 'representative.ssn_last_4',
- 'company.name',
- 'company.tax_id',
- 'tos_acceptance.ip',
- 'tos_acceptance.date',
- 'external_account',
- ];
-
- $this->mock_onboarding_service
- ->expects( $this->once() )
- ->method( 'get_required_verification_information' )
- ->willReturn( $mock_requirements );
-
- $request = new WP_REST_Request( 'GET' );
- $request->set_url_params(
- [
- 'country' => Country_Code::UNITED_STATES,
- 'type' => 'company',
- 'structure' => 'sole_proprietor',
- ]
- );
- $response = $this->controller->get_required_verification_information( $request );
-
- $this->assertSame( 200, $response->status );
- $this->assertSame( [ 'data' => $mock_requirements ], $response->get_data() );
- }
-
public function test_get_progressive_onboarding_eligible() {
$this->mock_api_client
->expects( $this->once() )
diff --git a/tests/unit/duplicate-detection/test-class-duplicates-detection-service.php b/tests/unit/duplicate-detection/test-class-duplicates-detection-service.php
index 2dd028d4203..23b696d751c 100644
--- a/tests/unit/duplicate-detection/test-class-duplicates-detection-service.php
+++ b/tests/unit/duplicate-detection/test-class-duplicates-detection-service.php
@@ -66,7 +66,7 @@ public function test_two_cc_both_enabled() {
$result = $this->service->find_duplicates();
$this->assertCount( 1, $result );
- $this->assertEquals( 'card', $result[0] );
+ $this->assertEquals( 'card', array_keys( $result )[0] );
}
public function test_two_cc_one_enabled() {
@@ -83,7 +83,7 @@ public function test_two_apms_enabled() {
$result = $this->service->find_duplicates();
$this->assertCount( 1, $result );
- $this->assertEquals( Giropay_Payment_Method::PAYMENT_METHOD_STRIPE_ID, $result[0] );
+ $this->assertEquals( Giropay_Payment_Method::PAYMENT_METHOD_STRIPE_ID, array_keys( $result )[0] );
}
public function test_two_bnpls_enabled() {
@@ -92,7 +92,7 @@ public function test_two_bnpls_enabled() {
$result = $this->service->find_duplicates();
$this->assertCount( 1, $result );
- $this->assertEquals( Klarna_Payment_Method::PAYMENT_METHOD_STRIPE_ID, $result[0] );
+ $this->assertEquals( Klarna_Payment_Method::PAYMENT_METHOD_STRIPE_ID, array_keys( $result )[0] );
}
public function test_two_prbs_enabled() {
@@ -103,7 +103,7 @@ public function test_two_prbs_enabled() {
$result = $this->service->find_duplicates();
- $this->assertEquals( 'apple_pay_google_pay', $result[0] );
+ $this->assertEquals( 'apple_pay_google_pay', array_keys( $result )[0] );
}
public function test_duplicate_not_enabled_in_woopayments() {
diff --git a/tests/unit/src/Internal/Payment/FactorTest.php b/tests/unit/src/Internal/Payment/FactorTest.php
index e1e1e14ba71..c570d6dcfe0 100644
--- a/tests/unit/src/Internal/Payment/FactorTest.php
+++ b/tests/unit/src/Internal/Payment/FactorTest.php
@@ -36,6 +36,7 @@ public function test_get_all_factors() {
'IPP_CAPTURE',
'STRIPE_LINK',
'PAYMENT_REQUEST',
+ 'EXPRESS_CHECKOUT_ELEMENT',
];
$result = Factor::get_all_factors();
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php
index 0628ec6384d..e39a0c73b91 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php
@@ -22,10 +22,12 @@
use WCPay\Exceptions\API_Exception;
use WCPay\Exceptions\Fraud_Prevention_Enabled_Exception;
use WCPay\Exceptions\Process_Payment_Exception;
+use WCPay\Exceptions\Order_ID_Mismatch_Exception;
use WCPay\Fraud_Prevention\Fraud_Prevention_Service;
use WCPay\Internal\Payment\Factor;
use WCPay\Internal\Payment\Router;
use WCPay\Internal\Payment\State\CompletedState;
+use WCPay\Internal\Payment\State\PaymentErrorState;
use WCPay\Internal\Service\Level3Service;
use WCPay\Internal\Service\OrderService;
use WCPay\Internal\Service\PaymentProcessingService;
@@ -2559,6 +2561,31 @@ public function test_process_payment_for_order_rejects_with_cached_minimum_amoun
$this->card_gateway->process_payment_for_order( WC()->cart, $pi );
}
+ public function test_process_payment_for_order_rejects_with_order_id_mismatch() {
+ $order = WC_Helper_Order::create_order();
+ $intent_meta_order_id = 0;
+ $woopay_intent_id = 'woopay_invalid_intent_id_mock';
+ $payment_intent = WC_Helper_Intention::create_intention(
+ [
+ 'status' => 'success',
+ 'metadata' => [ 'order_id' => (string) $intent_meta_order_id ],
+ ]
+ );
+
+ $_POST['platform-checkout-intent'] = $woopay_intent_id;
+
+ $payment_information = new Payment_Information( 'pm_test', $order, null, null, null, null, null, '', 'card' );
+
+ $this->mock_wcpay_request( Get_Intention::class, 1, $woopay_intent_id )
+ ->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( $payment_intent );
+
+ $this->expectException( 'WCPay\Exceptions\Order_ID_Mismatch_Exception' );
+ $this->expectExceptionMessage( 'We're not able to process this payment. Please try again later. WooPayMeta: intent_meta_order_id: ' . $intent_meta_order_id . ', order_id: ' . $order->get_id() );
+ $this->card_gateway->process_payment_for_order( WC()->cart, $payment_information );
+ }
+
public function test_set_mandate_data_to_payment_intent_if_not_required() {
$payment_method = 'woocommerce_payments_sepa_debit';
$order = WC_Helper_Order::create_order();
@@ -3635,6 +3662,34 @@ public function test_new_process_payment() {
);
}
+ public function test_new_process_payment_throw_exception() {
+ // The new payment process is only accessible in dev mode.
+ WC_Payments::mode()->dev();
+
+ $mock_service = $this->createMock( PaymentProcessingService::class );
+ $mock_router = $this->createMock( Router::class );
+ $order = WC_Helper_Order::create_order();
+ $mock_state = $this->createMock( PaymentErrorState::class );
+
+ wcpay_get_test_container()->replace( PaymentProcessingService::class, $mock_service );
+ wcpay_get_test_container()->replace( Router::class, $mock_router );
+
+ $mock_router->expects( $this->once() )
+ ->method( 'should_use_new_payment_process' )
+ ->willReturn( true );
+
+ // Assert: The new service is called.
+ $mock_service->expects( $this->once() )
+ ->method( 'process_payment' )
+ ->with( $order->get_id() )
+ ->willReturn( $mock_state );
+
+ $this->expectException( Exception::class );
+ $this->expectExceptionMessage( 'The payment process could not be completed.' );
+
+ $this->card_gateway->process_payment( $order->get_id() );
+ }
+
public function test_process_payment_rate_limiter_enabled_throw_exception() {
$order = WC_Helper_Order::create_order();
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 40329be06fd..2fc3948b716 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
@@ -79,6 +79,13 @@ class WC_Payments_Express_Checkout_Button_Display_Handler_Test extends WCPAY_Uni
*/
private $mock_express_checkout_helper;
+ /**
+ * Express Checkout Ajax Handler instance.
+ *
+ * @var WC_Payments_Express_Checkout_Ajax_Handler
+ */
+ private $mock_express_checkout_ajax_handler;
+
/**
* Sets up things all tests need.
*/
@@ -117,6 +124,14 @@ public function set_up() {
)
->getMock();
+ $this->mock_express_checkout_ajax_handler = $this->getMockBuilder( WC_Payments_Express_Checkout_Ajax_Handler::class )
+ ->setConstructorArgs(
+ [
+ $this->mock_express_checkout_helper,
+ ]
+ )
+ ->getMock();
+
$this->mock_woopay_button_handler = $this->getMockBuilder( WC_Payments_WooPay_Button_Handler::class )
->setConstructorArgs(
[
@@ -156,6 +171,7 @@ public function set_up() {
$this->mock_wcpay_account,
$this->mock_wcpay_gateway,
$this->mock_express_checkout_helper,
+ $this->mock_express_checkout_ajax_handler,
]
)
->setMethods(
@@ -165,14 +181,15 @@ 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->mock_express_checkout_ece_button_handler,
- $this->mock_express_checkout_helper
- );
- $this->express_checkout_button_display_handler->init();
+ $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->mock_express_checkout_ece_button_handler,
+ $this->mock_express_checkout_ajax_handler,
+ $this->mock_express_checkout_helper
+ );
+ $this->express_checkout_button_display_handler->init();
add_filter(
'woocommerce_available_payment_gateways',
diff --git a/tests/unit/test-class-wc-payments-features.php b/tests/unit/test-class-wc-payments-features.php
index a27ff2c432f..a8e61c36ad4 100644
--- a/tests/unit/test-class-wc-payments-features.php
+++ b/tests/unit/test-class-wc-payments-features.php
@@ -183,7 +183,6 @@ public function test_is_woopay_express_checkout_enabled_returns_false_when_woopa
public function test_is_woopay_direct_checkout_enabled_returns_true() {
$this->set_feature_flag_option( WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' );
- $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME, '1' );
$this->set_feature_flag_option( WC_Payments_Features::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' );
$this->mock_cache->method( 'get' )->willReturn(
[
@@ -196,7 +195,6 @@ public function test_is_woopay_direct_checkout_enabled_returns_true() {
public function test_is_woopay_direct_checkout_enabled_returns_false_when_flag_is_false() {
$this->set_feature_flag_option( WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' );
- $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME, '1' );
$this->set_feature_flag_option( WC_Payments_Features::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '0' );
$this->mock_cache->method( 'get' )->willReturn(
[
@@ -209,7 +207,6 @@ public function test_is_woopay_direct_checkout_enabled_returns_false_when_flag_i
public function test_is_woopay_direct_checkout_enabled_returns_false_when_woopay_eligible_is_false() {
$this->set_feature_flag_option( WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' );
- $this->set_feature_flag_option( WC_Payments_Features::WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME, '1' );
$this->set_feature_flag_option( WC_Payments_Features::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' );
$this->mock_cache->method( 'get' )->willReturn(
[
@@ -220,7 +217,7 @@ public function test_is_woopay_direct_checkout_enabled_returns_false_when_woopay
$this->assertFalse( WC_Payments_Features::is_woopay_direct_checkout_enabled() );
}
- public function test_is_woopay_direct_checkout_enabled_returns_false_when_first_party_auth_is_disabled() {
+ public function test_is_woopay_direct_checkout_enabled_returns_true_when_first_party_auth_is_disabled() {
$this->set_feature_flag_option( WC_Payments_Features::WOOPAY_EXPRESS_CHECKOUT_FLAG_NAME, '1' );
$this->set_feature_flag_option( WC_Payments_Features::WOOPAY_FIRST_PARTY_AUTH_FLAG_NAME, '0' );
$this->set_feature_flag_option( WC_Payments_Features::WOOPAY_DIRECT_CHECKOUT_FLAG_NAME, '1' );
@@ -230,7 +227,7 @@ public function test_is_woopay_direct_checkout_enabled_returns_false_when_first_
'platform_direct_checkout_eligible' => true,
]
);
- $this->assertFalse( WC_Payments_Features::is_woopay_direct_checkout_enabled() );
+ $this->assertTrue( WC_Payments_Features::is_woopay_direct_checkout_enabled() );
}
public function test_is_wcpay_frt_review_feature_active_returns_true() {
diff --git a/tests/unit/test-class-wc-payments-onboarding-service.php b/tests/unit/test-class-wc-payments-onboarding-service.php
index 7f32038c975..22a1274f9bb 100644
--- a/tests/unit/test-class-wc-payments-onboarding-service.php
+++ b/tests/unit/test-class-wc-payments-onboarding-service.php
@@ -141,21 +141,6 @@ public function test_filters_registered_properly() {
$this->assertNotFalse( has_filter( 'admin_body_class', [ $this->onboarding_service, 'add_admin_body_classes' ] ) );
}
- public function test_get_required_verification_information() {
- $mock_requirements = [ 'requirement1', 'requirement2', 'requirement3' ];
-
- $this->mock_api_client
- ->expects( $this->once() )
- ->method( 'get_onboarding_required_verification_information' )
- ->with( Country_Code::UNITED_STATES, 'company', 'sole_propietorship' )
- ->willReturn( $mock_requirements );
-
- $this->assertEquals(
- $mock_requirements,
- $this->onboarding_service->get_required_verification_information( Country_Code::UNITED_STATES, 'company', 'sole_propietorship' )
- );
- }
-
public function test_get_cached_business_types_with_no_server_connection() {
$this->mock_api_client
->expects( $this->once() )
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 e368d3c83ca..222fff3d742 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
@@ -366,6 +366,7 @@ public function test_tokenized_cart_address_state_normalization() {
public function test_tokenized_cart_address_postcode_normalization() {
$request = new WP_REST_Request();
+ $request->set_route( '/wc/store/v1/cart/update-customer' );
$request->set_header( 'X-WooPayments-Express-Payment-Request', 'true' );
$request->set_header( 'X-WooPayments-Express-Payment-Request-Nonce', wp_create_nonce( 'woopayments_tokenized_cart_nonce' ) );
$request->set_header( 'Content-Type', 'application/json' );
@@ -395,6 +396,42 @@ public function test_tokenized_cart_address_postcode_normalization() {
$this->assertSame( '90210', $billing_address['postcode'] );
}
+ public function test_tokenized_cart_avoid_address_postcode_normalization_if_route_incorrect() {
+ $request = new WP_REST_Request();
+ $request->set_route( '/wc/store/v1/checkout' );
+ $request->set_header( 'X-WooPayments-Express-Payment-Request', 'true' );
+ $request->set_header( 'X-WooPayments-Express-Payment-Request-Nonce', wp_create_nonce( 'woopayments_tokenized_cart_nonce' ) );
+ $request->set_header( 'Content-Type', 'application/json' );
+ $request->set_param(
+ 'shipping_address',
+ [
+ 'country' => 'CA',
+ 'postcode' => 'H3B',
+ 'state' => 'Colombie-Britannique',
+ ]
+ );
+ $request->set_param(
+ 'billing_address',
+ [
+ 'country' => 'CA',
+ 'postcode' => 'H3B',
+ 'state' => 'Colombie-Britannique',
+ ]
+ );
+
+ $this->pr->tokenized_cart_store_api_address_normalization( null, null, $request );
+
+ $shipping_address = $request->get_param( 'shipping_address' );
+ $billing_address = $request->get_param( 'billing_address' );
+
+ // this should be modified.
+ $this->assertSame( 'BC', $shipping_address['state'] );
+ $this->assertSame( 'BC', $billing_address['state'] );
+ // this shouldn't be modified.
+ $this->assertSame( 'H3B', $shipping_address['postcode'] );
+ $this->assertSame( 'H3B', $billing_address['postcode'] );
+ }
+
public function test_get_shipping_options_returns_shipping_options() {
$data = $this->pr->get_shipping_options( self::SHIPPING_ADDRESS );
diff --git a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
index e4a252c369a..ed7cc963b7e 100644
--- a/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
+++ b/tests/unit/wc-payment-api/test-class-wc-payments-api-client.php
@@ -347,25 +347,6 @@ public function test_get_onboarding_business_types() {
$this->payments_api_client->get_onboarding_business_types();
}
- /**
- * Test getting onboarding required verification information.
- *
- * @throws API_Exception
- */
- public function test_get_onboarding_required_verification_information() {
- $this->mock_http_client
- ->expects( $this->once() )
- ->method( 'remote_request' )
- ->with(
- $this->containsIdentical( 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/onboarding/required_verification_information?test_mode=0&country=country&type=type' ),
- null,
- true,
- true // get_onboarding_required_verification_information should use user token auth.
- );
-
- $this->payments_api_client->get_onboarding_required_verification_information( 'country', 'type' );
- }
-
public function test_get_link() {
$this->mock_http_client
->expects( $this->once() )
diff --git a/webpack/shared.js b/webpack/shared.js
index 60e6c3f333e..0a445ccf118 100644
--- a/webpack/shared.js
+++ b/webpack/shared.js
@@ -41,6 +41,7 @@ module.exports = {
'./client/subscription-product-onboarding/toast.js',
'product-details': './client/product-details/index.js',
'cart-block': './client/cart/blocks/index.js',
+ 'plugins-page': './client/plugins-page/index.js',
},
// Override webpack public path dynamically on every entry.
// Required for chunks loading to work on sites with JS concatenation.
diff --git a/woocommerce-payments.php b/woocommerce-payments.php
index cdb84deb13a..f49664ad999 100644
--- a/woocommerce-payments.php
+++ b/woocommerce-payments.php
@@ -8,10 +8,10 @@
* Text Domain: woocommerce-payments
* Domain Path: /languages
* WC requires at least: 7.6
- * WC tested up to: 8.9.1
+ * WC tested up to: 8.9.3
* Requires at least: 6.0
* Requires PHP: 7.3
- * Version: 7.7.0
+ * Version: 7.8.0
* Requires Plugins: woocommerce
*
* @package WooCommerce\Payments
@@ -423,7 +423,8 @@ function register_woopay_extension() {
'before_woocommerce_init',
function () {
if ( class_exists( '\Automattic\WooCommerce\Utilities\FeaturesUtil' ) ) {
- \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', 'woocommerce-payments/woocommerce-payments.php', true );
+ \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'cart_checkout_blocks', __FILE__, true );
+ \Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility( 'custom_order_tables', __FILE__, true );
}
}
);