get_meta( 'last4' ) ) {
@@ -73,6 +114,35 @@ public function show_woopay_payment_method_name( $payment_method_title, $abstrac
return ob_get_clean();
}
+ /**
+ * Add the BNPL logo to the payment method name on the order received page.
+ *
+ * @param WC_Payment_Gateway_WCPay $gateway the gateway being shown.
+ * @param WCPay\Payment_Methods\UPE_Payment_Method $payment_method the payment method being shown.
+ *
+ * @return string|false
+ */
+ public function show_bnpl_payment_method_name( $gateway, $payment_method ) {
+ $method_logo_url = apply_filters(
+ 'wc_payments_thank_you_page_bnpl_payment_method_logo_url',
+ $payment_method->get_payment_method_icon_for_location( 'checkout', false, $gateway->get_account_country() ),
+ $payment_method->get_id()
+ );
+
+ // If we don't have a logo URL here for some reason, bail.
+ if ( ! $method_logo_url ) {
+ return false;
+ }
+
+ ob_start();
+ ?>
+
+
+
+ set_gateway_id( $gateway_id );
$token->set_email( $payment_method[ Payment_Method::LINK ]['email'] );
break;
+ case Payment_Method::CARD_PRESENT:
+ $token = new WC_Payment_Token_CC();
+ $token->set_gateway_id( CC_Payment_Gateway::GATEWAY_ID );
+ $token->set_expiry_month( $payment_method[ Payment_Method::CARD_PRESENT ]['exp_month'] );
+ $token->set_expiry_year( $payment_method[ Payment_Method::CARD_PRESENT ]['exp_year'] );
+ $token->set_card_type( strtolower( $payment_method[ Payment_Method::CARD_PRESENT ]['brand'] ) );
+ $token->set_last4( $payment_method[ Payment_Method::CARD_PRESENT ]['last4'] );
+ break;
default:
$token = new WC_Payment_Token_CC();
$token->set_gateway_id( CC_Payment_Gateway::GATEWAY_ID );
diff --git a/includes/class-wc-payments-utils.php b/includes/class-wc-payments-utils.php
index 062648d3242..44eea0da051 100644
--- a/includes/class-wc-payments-utils.php
+++ b/includes/class-wc-payments-utils.php
@@ -546,11 +546,12 @@ public static function convert_to_stripe_locale( string $locale ): string {
* Generally, only Stripe exceptions with type of `card_error` should be displayed.
* Other API errors should be redacted (https://stripe.com/docs/api/errors#errors-message).
*
- * @param Exception $e Exception to get the message from.
+ * @param Exception $e Exception to get the message from.
+ * @param boolean $blocked_by_fraud_rules Whether the payment was blocked by the fraud rules. Defaults to false.
*
* @return string
*/
- public static function get_filtered_error_message( Exception $e ) {
+ public static function get_filtered_error_message( Exception $e, bool $blocked_by_fraud_rules = false ) {
$error_message = method_exists( $e, 'getLocalizedMessage' ) ? $e->getLocalizedMessage() : $e->getMessage();
// These notices can be shown when placing an order or adding a new payment method, so we aim for
@@ -580,7 +581,7 @@ public static function get_filtered_error_message( Exception $e ) {
$error_message = __( 'We\'re not able to process this request. Please refresh the page and try again.', 'woocommerce-payments' );
} elseif ( $e instanceof API_Exception && ! empty( $e->get_error_type() ) && 'card_error' !== $e->get_error_type() ) {
$error_message = __( 'We\'re not able to process this request. Please refresh the page and try again.', 'woocommerce-payments' );
- } elseif ( $e instanceof API_Exception && 'card_error' === $e->get_error_type() && 'incorrect_zip' === $e->get_error_code() ) {
+ } elseif ( $e instanceof API_Exception && 'card_error' === $e->get_error_type() && 'incorrect_zip' === $e->get_error_code() && ! $blocked_by_fraud_rules ) {
$error_message = __( 'We couldnβt verify the postal code in your billing address. Make sure the information is current with your card issuing bank and try again.', 'woocommerce-payments' );
}
@@ -1097,4 +1098,57 @@ public static function is_cart_page(): bool {
public static function is_cart_block(): bool {
return has_block( 'woocommerce/cart' ) || ( wp_is_block_theme() && is_cart() );
}
+
+ /**
+ * Gets the current active theme transient for a given location
+ * Falls back to 'stripe' if no transients are set.
+ *
+ * @param string $location The theme location.
+ * @param string $context The theme location to fall back to if both transients are set.
+ * @return string
+ */
+ public static function get_active_upe_theme_transient_for_location( string $location = 'checkout', string $context = 'blocks' ) {
+ $themes = \WC_Payment_Gateway_WCPay::APPEARANCE_THEME_TRANSIENTS;
+ $active_theme = false;
+
+ // If an invalid location is sent, we fallback to trying $themes[ 'checkout' ][ 'block' ].
+ if ( ! isset( $themes[ $location ] ) ) {
+ $active_theme = get_transient( $themes['checkout']['blocks'] );
+ } elseif ( ! isset( $themes[ $location ][ $context ] ) ) {
+ // If the location is valid but the context is invalid, we fallback to trying $themes[ $location ][ 'block' ].
+ $active_theme = get_transient( $themes[ $location ]['blocks'] );
+ } else {
+ $active_theme = get_transient( $themes[ $location ][ $context ] );
+ }
+
+ // If $active_theme is still false here, that means that $themes[ $location ][ $context ] is not set, so we try $themes[ $location ][ 'classic' ].
+ if ( ! $active_theme ) {
+ $active_theme = get_transient( $themes[ $location ][ 'blocks' === $context ? 'classic' : 'blocks' ] );
+ }
+
+ // If $active_theme is still false here, nothing at the location is set so we'll try all locations.
+ if ( ! $active_theme ) {
+ foreach ( $themes as $location_const => $contexts ) {
+ // We don't need to check the same location again.
+ if ( $location_const === $location ) {
+ continue;
+ }
+
+ foreach ( $contexts as $context => $transient ) {
+ $active_theme = get_transient( $transient );
+ if ( $active_theme ) {
+ break 2; // This will break both loops.
+ }
+ }
+ }
+ }
+
+ // If $active_theme is still false, we don't have any theme set in the transients, so we fallback to 'stripe'.
+ if ( $active_theme ) {
+ return $active_theme;
+ }
+
+ // Fallback to 'stripe' if no transients are set.
+ return 'stripe';
+ }
}
diff --git a/includes/class-wc-payments.php b/includes/class-wc-payments.php
index 22c21a3c4f4..dc82d7a4304 100644
--- a/includes/class-wc-payments.php
+++ b/includes/class-wc-payments.php
@@ -41,6 +41,7 @@
use WCPay\WooPay\WooPay_Scheduler;
use WCPay\WooPay\WooPay_Session;
use WCPay\Compatibility_Service;
+use WCPay\Duplicates_Detection_Service;
/**
* Main class for the WooPayments extension. Its responsibility is to initialize the extension.
@@ -284,6 +285,13 @@ class WC_Payments {
*/
private static $compatibility_service;
+ /**
+ * Instance of Duplicates_Detection_Service, created in init function
+ *
+ * @var Duplicates_Detection_Service
+ */
+ private static $duplicates_detection_service;
+
/**
* Entry point to the initialization logic.
*/
@@ -338,6 +346,7 @@ public static function init() {
include_once __DIR__ . '/core/server/request/trait-use-test-mode-only-when-dev-mode.php';
include_once __DIR__ . '/core/server/request/class-generic.php';
include_once __DIR__ . '/core/server/request/class-get-intention.php';
+ include_once __DIR__ . '/core/server/request/class-get-reporting-payment-activity.php';
include_once __DIR__ . '/core/server/request/class-get-payment-process-factors.php';
include_once __DIR__ . '/core/server/request/class-create-intention.php';
include_once __DIR__ . '/core/server/request/class-update-intention.php';
@@ -459,6 +468,7 @@ public static function init() {
include_once __DIR__ . '/class-wc-payments-incentives-service.php';
include_once __DIR__ . '/class-compatibility-service.php';
include_once __DIR__ . '/multi-currency/wc-payments-multi-currency.php';
+ include_once __DIR__ . '/class-duplicates-detection-service.php';
self::$woopay_checkout_service = new Checkout_Service();
self::$woopay_checkout_service->init();
@@ -493,6 +503,7 @@ public static function init() {
self::$incentives_service = new WC_Payments_Incentives_Service( self::$database_cache );
self::$duplicate_payment_prevention_service = new Duplicate_Payment_Prevention_Service();
self::$compatibility_service = new Compatibility_Service( self::$api_client );
+ self::$duplicates_detection_service = new Duplicates_Detection_Service();
( new WooPay_Scheduler( self::$api_client ) )->init();
@@ -527,7 +538,7 @@ public static function init() {
foreach ( $payment_methods as $payment_method ) {
self::$payment_method_map[ $payment_method->get_id() ] = $payment_method;
- $split_gateway = new WC_Payment_Gateway_WCPay( self::$api_client, self::$account, self::$customer_service, self::$token_service, self::$action_scheduler_service, $payment_method, $payment_methods, self::$failed_transaction_rate_limiter, self::$order_service, self::$duplicate_payment_prevention_service, self::$localization_service, self::$fraud_service );
+ $split_gateway = new WC_Payment_Gateway_WCPay( self::$api_client, self::$account, self::$customer_service, self::$token_service, self::$action_scheduler_service, $payment_method, $payment_methods, self::$failed_transaction_rate_limiter, self::$order_service, self::$duplicate_payment_prevention_service, self::$localization_service, self::$fraud_service, self::$duplicates_detection_service );
// Card gateway hooks are registered once below.
if ( 'card' !== $payment_method->get_id() ) {
@@ -573,7 +584,7 @@ public static function init() {
);
if ( [] !== $enabled_bnpl_payment_methods ) {
add_action( 'woocommerce_single_product_summary', [ __CLASS__, 'load_stripe_bnpl_site_messaging' ], 10 );
- add_action( 'woocommerce_proceed_to_checkout', [ __CLASS__, 'load_stripe_bnpl_site_messaging' ], 10 );
+ add_action( 'woocommerce_proceed_to_checkout', [ __CLASS__, 'load_stripe_bnpl_site_messaging' ], 5 );
add_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after', [ __CLASS__, 'load_stripe_bnpl_site_messaging' ] );
add_action( 'wc_ajax_wcpay_get_cart_total', [ __CLASS__, 'ajax_get_cart_total' ] );
}
@@ -1004,6 +1015,10 @@ public static function init_rest_api() {
$accounts_controller = new WC_REST_Payments_Terminal_Locations_Controller( self::$api_client );
$accounts_controller->register_routes();
+ include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-reporting-controller.php';
+ $reporting_controller = new WC_REST_Payments_Reporting_Controller( self::$api_client );
+ $reporting_controller->register_routes();
+
include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-settings-controller.php';
$settings_controller = new WC_REST_Payments_Settings_Controller( self::$api_client, self::get_gateway(), self::$account );
$settings_controller->register_routes();
@@ -1032,6 +1047,10 @@ public static function init_rest_api() {
$refunds_controller = new WC_REST_Payments_Refunds_Controller( self::$api_client );
$refunds_controller->register_routes();
+ include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-survey-controller.php';
+ $survey_controller = new WC_REST_Payments_Survey_Controller( self::get_wc_payments_http() );
+ $survey_controller->register_routes();
+
if ( WC_Payments_Features::is_documents_section_enabled() ) {
include_once WCPAY_ABSPATH . 'includes/admin/class-wc-rest-payments-documents-controller.php';
$documents_controller = new WC_REST_Payments_Documents_Controller( self::$api_client );
@@ -1306,6 +1325,26 @@ public static function get_order_service(): WC_Payments_Order_Service {
return self::$order_service;
}
+ /**
+ * Returns the token service instance.
+ *
+ * @return WC_Payments_Token_Service
+ */
+ public static function get_token_service(): WC_Payments_Token_Service {
+ return self::$token_service;
+ }
+
+ /**
+ * Sets the token service instance. This is needed only for tests.
+ *
+ * @param WC_Payments_Token_Service $token_service Instance of WC_Payments_Token_Service.
+ *
+ * @return void
+ */
+ public static function set_token_service( WC_Payments_Token_Service $token_service ) {
+ self::$token_service = $token_service;
+ }
+
/**
* Sets the customer service instance. This is needed only for tests.
*
@@ -1771,6 +1810,7 @@ public static function add_wcpay_options_to_woocommerce_permissions_list( $permi
'wcpay_capability_request_dismissed_notices',
'wcpay_onboarding_eligibility_modal_dismissed',
'wcpay_next_deposit_notice_dismissed',
+ 'wcpay_duplicate_payment_method_notices_dismissed',
],
true
);
diff --git a/includes/class-woopay-tracker.php b/includes/class-woopay-tracker.php
index 04312026ebc..511aeafc5cd 100644
--- a/includes/class-woopay-tracker.php
+++ b/includes/class-woopay-tracker.php
@@ -13,6 +13,7 @@
use WC_Payments_Features;
use WCPay\Constants\Country_Code;
use WP_Error;
+use Exception;
defined( 'ABSPATH' ) || exit; // block direct access.
@@ -42,6 +43,13 @@ class WooPay_Tracker extends Jetpack_Tracks_Client {
*/
private $http;
+ /**
+ * Base URL for stats counter.
+ *
+ * @var string
+ */
+ private static $pixel_base_url = 'https://pixel.wp.com/g.gif';
+
/**
* Constructor.
@@ -63,8 +71,8 @@ public function __construct( $http ) {
add_action( 'woocommerce_after_single_product', [ $this, 'classic_product_page_view' ] );
add_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_after', [ $this, 'blocks_checkout_start' ] );
add_action( 'woocommerce_blocks_enqueue_cart_block_scripts_after', [ $this, 'blocks_cart_page_view' ] );
- add_action( 'woocommerce_checkout_order_processed', [ $this, 'checkout_order_processed' ] );
- add_action( 'woocommerce_blocks_checkout_order_processed', [ $this, 'checkout_order_processed' ] );
+ add_action( 'woocommerce_checkout_order_processed', [ $this, 'checkout_order_processed' ], 10, 2 );
+ add_action( 'woocommerce_blocks_checkout_order_processed', [ $this, 'checkout_order_processed' ], 10, 2 );
add_action( 'woocommerce_payments_save_user_in_woopay', [ $this, 'must_save_payment_method_to_platform' ] );
add_action( 'before_woocommerce_pay_form', [ $this, 'pay_for_order_page_view' ] );
add_action( 'woocommerce_thankyou', [ $this, 'thank_you_page_view' ] );
@@ -370,7 +378,6 @@ public function tracks_get_identity() {
];
}
-
/**
* Record a Tracks event that the classic checkout page has loaded.
*/
@@ -445,13 +452,56 @@ public function pay_for_order_page_view() {
}
/**
- * Record a Tracks event that the order has been processed.
+ * Bump a counter. No user identifiable information is sent.
+ *
+ * @param string $group The group to bump the stat in.
+ * @param string $stat_name The name of the stat to bump.
+ *
+ * @return bool
+ */
+ public function bump_stats( $group, $stat_name ) {
+ $pixel_url = sprintf(
+ self::$pixel_base_url . '?v=wpcom-no-pv&x_%s=%s',
+ $group,
+ $stat_name
+ );
+
+ $response = wp_remote_get( esc_url_raw( $pixel_url ) );
+
+ if ( is_wp_error( $response ) ) {
+ return false;
+ }
+
+ if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Record that the order has been processed.
*/
- public function checkout_order_processed() {
- $is_woopay_order = ( isset( $_SERVER['HTTP_USER_AGENT'] ) && 'WooPay' === $_SERVER['HTTP_USER_AGENT'] );
- // Don't track WooPay orders. They will be tracked on WooPay side with more flow specific details.
- if ( ! $is_woopay_order ) {
- $this->maybe_record_wcpay_shopper_event( 'checkout_order_placed' );
+ public function checkout_order_processed( $order_id ) {
+
+ $payment_gateway = wc_get_payment_gateway_by_order( $order_id );
+ $properties = [ 'payment_title' => 'other' ];
+
+ // If the order was placed using WooCommerce Payments, record the payment title using Tracks.
+ if (strpos( $payment_gateway->id, 'woocommerce_payments') === 0 ) {
+ $order = wc_get_order( $order_id );
+ $payment_title = $order->get_payment_method_title();
+ $properties = [ 'payment_title' => $payment_title ];
+
+ $is_woopay_order = ( isset( $_SERVER['HTTP_USER_AGENT'] ) && 'WooPay' === $_SERVER['HTTP_USER_AGENT'] );
+
+ // Don't track WooPay orders. They will be tracked on WooPay side with more details.
+ if ( ! $is_woopay_order ) {
+ $this->maybe_record_wcpay_shopper_event( 'checkout_order_placed', $properties );
+ }
+ // If the order was placed using a different payment gateway, just increment a counter.
+ } else {
+ $this->bump_stats( 'wcpay_order_completed_gateway', 'other' );
}
}
diff --git a/includes/core/server/class-request.php b/includes/core/server/class-request.php
index ddf1f042e8d..f42588573ce 100644
--- a/includes/core/server/class-request.php
+++ b/includes/core/server/class-request.php
@@ -162,6 +162,7 @@ abstract class Request {
WC_Payments_API_Client::FRAUD_OUTCOMES_API => 'fraud_outcomes',
WC_Payments_API_Client::FRAUD_RULESET_API => 'fraud_ruleset',
WC_Payments_API_Client::COMPATIBILITY_API => 'compatibility',
+ WC_Payments_API_Client::REPORTING_API => 'reporting',
];
/**
diff --git a/includes/core/server/request/class-get-reporting-payment-activity.php b/includes/core/server/request/class-get-reporting-payment-activity.php
new file mode 100644
index 00000000000..d9be6cf33eb
--- /dev/null
+++ b/includes/core/server/request/class-get-reporting-payment-activity.php
@@ -0,0 +1,81 @@
+set_param( 'date_start', $date_start );
+ }
+
+ /**
+ * Sets the end date for the payment activity data.
+ *
+ * @param string|null $date_end The end date in the format 'YYYY-MM-DDT00:00:00' or null.
+ * @return void
+ */
+ public function set_date_end( string $date_end ) {
+ // TBD - validation.
+ $this->set_param( 'date_end', $date_end );
+ }
+
+ /**
+ * Sets the timezone for the reporting data.
+ *
+ * @param string|null $timezone The timezone to set or null.
+ * @return void
+ */
+ public function set_timezone( ?string $timezone ) {
+ $this->set_param( 'timezone', $timezone ?? 'UTC' );
+ }
+}
diff --git a/includes/core/server/request/class-list-fraud-outcome-transactions.php b/includes/core/server/request/class-list-fraud-outcome-transactions.php
index d7d3770c7c5..4d01cff2666 100644
--- a/includes/core/server/request/class-list-fraud-outcome-transactions.php
+++ b/includes/core/server/request/class-list-fraud-outcome-transactions.php
@@ -11,6 +11,7 @@
use WC_Payments_Utils;
use WC_Payments_API_Client;
use WCPay\Constants\Fraud_Meta_Box_Type;
+use WCPay\Fraud_Prevention\Models\Rule;
/**
* Request class for getting intents.
@@ -37,6 +38,10 @@ class List_Fraud_Outcome_Transactions extends Paginated {
* @throws Invalid_Request_Parameter_Exception
*/
public function get_api(): string {
+ $status = $this->status ?? 'null';
+ if ( ! Rule::is_valid_fraud_outcome_status( $status ) ) {
+ throw new Invalid_Request_Parameter_Exception( "Invalid fraud outcome status provided: $status", 'invalid_fraud_outcome_status' );
+ }
return WC_Payments_API_Client::FRAUD_OUTCOMES_API . '/status/' . $this->status;
}
diff --git a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php
index 6a16348b260..acc9ffd3c44 100644
--- a/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php
+++ b/includes/express-checkout/class-wc-payments-express-checkout-button-display-handler.php
@@ -130,7 +130,7 @@ public function display_express_checkout_buttons() {
* @return bool
*/
private function is_pay_for_order_flow_supported() {
- return ( WC_Payments_Features::is_pay_for_order_flow_enabled() && class_exists( '\Automattic\WooCommerce\Blocks\Package' ) && version_compare( \Automattic\WooCommerce\Blocks\Package::get_version(), '11.1.0', '>=' ) );
+ return ( class_exists( '\Automattic\WooCommerce\Blocks\Package' ) && version_compare( \Automattic\WooCommerce\Blocks\Package::get_version(), '11.1.0', '>=' ) );
}
/**
diff --git a/includes/multi-currency/MultiCurrency.php b/includes/multi-currency/MultiCurrency.php
index 49c07c0fe8e..c0698eddf1c 100644
--- a/includes/multi-currency/MultiCurrency.php
+++ b/includes/multi-currency/MultiCurrency.php
@@ -252,6 +252,8 @@ public function init_hooks() {
add_action( 'woocommerce_created_customer', [ $this, 'set_new_customer_currency_meta' ] );
}
+ add_filter( 'wcpay_payment_fields_js_config', [ $this, 'add_props_to_wcpay_js_config' ] );
+
$this->currency_switcher_block->init_hooks();
}
@@ -314,8 +316,6 @@ public function init() {
// Update the customer currencies option after an order status change.
add_action( 'woocommerce_order_status_changed', [ $this, 'maybe_update_customer_currencies_option' ] );
- $this->maybe_add_cache_cookie();
-
static::$is_initialized = true;
}
@@ -388,6 +388,19 @@ public function enqueue_admin_scripts() {
WC_Payments_Utils::enqueue_style( 'WCPAY_MULTI_CURRENCY_SETTINGS' );
}
+ /**
+ * Add multi-currency specific props to the WCPay JS config.
+ *
+ * @param array $config The JS config that will be loaded on the frontend.
+ *
+ * @return array The updated JS config.
+ */
+ public function add_props_to_wcpay_js_config( $config ) {
+ $config['isMultiCurrencyEnabled'] = true;
+
+ return $config;
+ }
+
/**
* Wipes the cached currency data option, forcing to re-fetch the data from WPCOM.
*
@@ -815,8 +828,6 @@ public function update_selected_currency( string $currency_code, bool $persist_c
} else {
add_action( 'wp_loaded', [ $this, 'recalculate_cart' ] );
}
-
- $this->maybe_add_cache_cookie();
}
/**
@@ -1642,17 +1653,4 @@ private function log_and_throw_invalid_currency_exception( $method, $currency_co
private function is_customer_currencies_data_valid( $currencies ) {
return ! empty( $currencies ) && is_array( $currencies );
}
-
- /**
- * Sets the cache cookie for currency code and exchange rate.
- *
- * This private method sets the 'wcpay_currency' cookie if HTTP headers
- * have not been sent. This cookie stores the selected currency's code and its exchange rate,
- * and is intended exclusively for caching purposes, not for application logic.
- */
- private function maybe_add_cache_cookie() {
- if ( ! headers_sent() && ! is_admin() && ! defined( 'DOING_CRON' ) && ! Utils::is_admin_api_request() ) {
- wc_setcookie( 'wcpay_currency', sprintf( '%s_%s', $this->get_selected_currency()->get_code(), $this->get_selected_currency()->get_rate() ), time() + HOUR_IN_SECONDS );
- }
- }
}
diff --git a/includes/payment-methods/class-affirm-payment-method.php b/includes/payment-methods/class-affirm-payment-method.php
index a51967ef3bb..f65794e555d 100644
--- a/includes/payment-methods/class-affirm-payment-method.php
+++ b/includes/payment-methods/class-affirm-payment-method.php
@@ -30,6 +30,7 @@ public function __construct( $token_service ) {
$this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID;
$this->title = __( 'Affirm', 'woocommerce-payments' );
$this->is_reusable = false;
+ $this->is_bnpl = true;
$this->icon_url = plugins_url( 'assets/images/payment-methods/affirm-logo.svg', WCPAY_PLUGIN_FILE );
$this->dark_icon_url = plugins_url( 'assets/images/payment-methods/affirm-logo-dark.svg', WCPAY_PLUGIN_FILE );
$this->currencies = [ Currency_Code::UNITED_STATES_DOLLAR, Currency_Code::CANADIAN_DOLLAR ];
diff --git a/includes/payment-methods/class-afterpay-payment-method.php b/includes/payment-methods/class-afterpay-payment-method.php
index 6df0697c473..01690b64f1b 100644
--- a/includes/payment-methods/class-afterpay-payment-method.php
+++ b/includes/payment-methods/class-afterpay-payment-method.php
@@ -29,6 +29,7 @@ public function __construct( $token_service ) {
$this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID;
$this->title = __( 'Afterpay', 'woocommerce-payments' );
$this->is_reusable = false;
+ $this->is_bnpl = true;
$this->icon_url = plugins_url( 'assets/images/payment-methods/afterpay-logo.svg', WCPAY_PLUGIN_FILE );
$this->currencies = [ Currency_Code::UNITED_STATES_DOLLAR, Currency_Code::CANADIAN_DOLLAR, Currency_Code::AUSTRALIAN_DOLLAR, Currency_Code::NEW_ZEALAND_DOLLAR, Currency_Code::POUND_STERLING ];
$this->countries = [ Country_Code::UNITED_STATES, Country_Code::CANADA, Country_Code::AUSTRALIA, Country_Code::NEW_ZEALAND, Country_Code::UNITED_KINGDOM ];
diff --git a/includes/payment-methods/class-klarna-payment-method.php b/includes/payment-methods/class-klarna-payment-method.php
index 2e446ead208..fa30154c226 100644
--- a/includes/payment-methods/class-klarna-payment-method.php
+++ b/includes/payment-methods/class-klarna-payment-method.php
@@ -30,6 +30,7 @@ public function __construct( $token_service ) {
$this->stripe_id = self::PAYMENT_METHOD_STRIPE_ID;
$this->title = __( 'Klarna', 'woocommerce-payments' );
$this->is_reusable = false;
+ $this->is_bnpl = true;
$this->icon_url = plugins_url( 'assets/images/payment-methods/klarna-pill.svg', WCPAY_PLUGIN_FILE );
$this->currencies = [ Currency_Code::UNITED_STATES_DOLLAR, Currency_Code::POUND_STERLING, Currency_Code::EURO, Currency_Code::DANISH_KRONE, Currency_Code::NORWEGIAN_KRONE, Currency_Code::SWEDISH_KRONA ];
$this->accept_only_domestic_payment = true;
diff --git a/includes/payment-methods/class-upe-payment-method.php b/includes/payment-methods/class-upe-payment-method.php
index 2cdd1a9cb01..51d091328fd 100644
--- a/includes/payment-methods/class-upe-payment-method.php
+++ b/includes/payment-methods/class-upe-payment-method.php
@@ -90,6 +90,13 @@ abstract class UPE_Payment_Method {
*/
protected $dark_icon_url;
+ /**
+ * Is the payment method a BNPL (Buy Now Pay Later) method?
+ *
+ * @var boolean
+ */
+ protected $is_bnpl = false;
+
/**
* Supported customer locations for which charges for a payment method can be processed
* Empty if all customer locations are supported
@@ -198,6 +205,16 @@ public function is_reusable() {
return $this->is_reusable;
}
+ /**
+ * Returns boolean dependent on whether payment method
+ * will support BNPL (Buy Now Pay Later) payments
+ *
+ * @return bool
+ */
+ public function is_bnpl() {
+ return $this->is_bnpl;
+ }
+
/**
* Returns boolean dependent on whether payment method will accept charges
* with chosen currency
@@ -258,6 +275,24 @@ public function get_dark_icon( string $account_country = null ) {
return isset( $this->dark_icon_url ) ? $this->dark_icon_url : $this->get_icon( $account_country );
}
+ /**
+ * Gets the theme appropriate icon for the payment method for a given location and context.
+ *
+ * @param string $location The location to get the icon for.
+ * @param boolean $is_blocks Whether the icon is for blocks.
+ * @param string $account_country Optional account country.
+ * @return string
+ */
+ public function get_payment_method_icon_for_location( string $location = 'checkout', bool $is_blocks = true, string $account_country = null ) {
+ $appearance_theme = WC_Payments_Utils::get_active_upe_theme_transient_for_location( $location, $is_blocks ? 'blocks' : 'classic' );
+
+ if ( 'night' === $appearance_theme ) {
+ return $this->get_dark_icon( $account_country );
+ }
+
+ return $this->get_icon( $account_country );
+ }
+
/**
* Returns payment method supported countries
*
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 ba66b392dec..8c3e6c29ba9 100644
--- a/includes/wc-payment-api/class-wc-payments-api-client.php
+++ b/includes/wc-payment-api/class-wc-payments-api-client.php
@@ -79,6 +79,7 @@ class WC_Payments_API_Client {
const FRAUD_OUTCOMES_API = 'fraud_outcomes';
const FRAUD_RULESET_API = 'fraud_ruleset';
const COMPATIBILITY_API = 'compatibility';
+ const REPORTING_API = 'reporting/payment_activity';
/**
* Common keys in API requests/responses that we might want to redact.
@@ -1860,6 +1861,9 @@ protected function request( $params, $api, $method, $is_site_specific = true, $u
$response_code = null;
$last_exception = null;
+ // The header intention is to give us insights into request latency between store and backend.
+ $headers['X-Request-Initiated'] = microtime( true );
+
try {
$response = $this->http_client->remote_request(
[
diff --git a/includes/wc-payment-api/class-wc-payments-http.php b/includes/wc-payment-api/class-wc-payments-http.php
index ba7c6fe4a52..65081e8be10 100644
--- a/includes/wc-payment-api/class-wc-payments-http.php
+++ b/includes/wc-payment-api/class-wc-payments-http.php
@@ -49,7 +49,7 @@ public function init_hooks() {
* @param array $args - The arguments to passed to Jetpack.
* @param string $body - The body passed on to the HTTP request.
* @param bool $is_site_specific - If true, the site ID will be included in the request url. Defaults to true.
- * @param bool $use_user_token - If true, the request will be signed with the user token rather than blog token. Defaults to false.
+ * @param bool $use_user_token - If true, the request will be signed with the Jetpack connection owner user token rather than blog token. Defaults to false.
*
* @return array HTTP response on success.
* @throws API_Exception - If not connected or request failed.
diff --git a/includes/woopay-user/class-woopay-save-user.php b/includes/woopay-user/class-woopay-save-user.php
index c0dd0e9f965..78096190fba 100644
--- a/includes/woopay-user/class-woopay-save-user.php
+++ b/includes/woopay-user/class-woopay-save-user.php
@@ -42,7 +42,7 @@ public function register_checkout_page_scripts() {
return;
}
- if ( ! $this->woopay_util->is_country_available( $gateways['woocommerce_payments'] ) ) {
+ if ( ! $this->woopay_util->is_country_available() ) {
return;
}
@@ -57,6 +57,16 @@ public function register_checkout_page_scripts() {
);
WC_Payments::register_script_with_dependencies( 'WCPAY_WOOPAY', 'dist/woopay' );
+ $account_data = WC_Payments::get_account_service()->get_cached_account_data();
+
+ wp_localize_script(
+ 'WCPAY_WOOPAY',
+ 'woopayCheckout',
+ [
+ 'PRE_CHECK_SAVE_MY_INFO' => isset( $account_data['pre_check_save_my_info'] ) ? $account_data['pre_check_save_my_info'] : false,
+ ]
+ );
+
wp_enqueue_script( 'WCPAY_WOOPAY' );
}
diff --git a/includes/woopay/class-woopay-session.php b/includes/woopay/class-woopay-session.php
index 839b4d943c3..7e04124c6cf 100644
--- a/includes/woopay/class-woopay-session.php
+++ b/includes/woopay/class-woopay-session.php
@@ -47,6 +47,9 @@ class WooPay_Session {
'@^\/wc\/store(\/v[\d]+)?\/checkout\/(?P
[\d]+)@',
'@^\/wc\/store(\/v[\d]+)?\/checkout$@',
'@^\/wc\/store(\/v[\d]+)?\/order\/(?P[\d]+)@',
+ // The route below is not a Store API route. However, this REST endpoint is used by WooPay to indirectly reach the Store API.
+ // By adding it to this list, we're able to identify the user and load the correct session for this route.
+ '@^\/wc\/v3\/woopay\/session$@',
];
/**
@@ -337,15 +340,66 @@ public static function get_frontend_init_session_request() {
return WooPay_Utilities::encrypt_and_sign_data( $session );
}
+ /**
+ * Retrieves cart data from the current session.
+ *
+ * If the request doesn't come from WooPay, this uses the same strategy in
+ * `hydrate_from_api` on the Checkout Block to retrieve cart data.
+ *
+ * @param int|null $order_id Pay-for-order order ID.
+ * @param string|null $key Pay-for-order key.
+ * @param string|null $billing_email Pay-for-order billing email.
+ * @param WP_REST_Request|null $woopay_request The WooPay request object.
+ * @return array The cart data.
+ */
+ private static function get_cart_data( $is_pay_for_order, $order_id, $key, $billing_email, $woopay_request ) {
+ if ( ! $woopay_request ) {
+ return ! $is_pay_for_order
+ ? rest_preload_api_request( [], '/wc/store/v1/cart' )['/wc/store/v1/cart']['body']
+ : rest_preload_api_request( [], "/wc/store/v1/order/" . urlencode( $order_id ) . "?key=" . urlencode( $key ) . "&billing_email=" . urlencode( $billing_email ) )[ "/wc/store/v1/order/" . urlencode( $order_id ) . "?key=" . urlencode( $key ) . "&billing_email=" . urlencode( $billing_email ) ]['body'];
+ }
+
+ $cart_request = new WP_REST_Request( 'GET', '/wc/store/v1/cart' );
+ $cart_request->set_header( 'Cart-Token', $woopay_request->get_header('cart_token') );
+ return rest_do_request( $cart_request )->get_data();
+ }
+
+ /**
+ * Retrieves checkout data from the current session.
+ *
+ * If the request doesn't come from WooPay, this uses the same strategy in
+ * `hydrate_from_api` on the Checkout Block to retrieve checkout data.
+ *
+ * @param WP_REST_Request $woopay_request The WooPay request object.
+ * @return mixed The checkout data.
+ */
+ private static function get_checkout_data( $woopay_request ) {
+ add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
+
+ if ( ! $woopay_request ) {
+ $preloaded_checkout_data = rest_preload_api_request( [], '/wc/store/v1/checkout' );
+ $checkout_data = isset( $preloaded_checkout_data['/wc/store/v1/checkout'] ) ? $preloaded_checkout_data['/wc/store/v1/checkout']['body'] : '';
+ } else {
+ $checkout_request = new WP_REST_Request( 'GET', '/wc/store/v1/checkout' );
+ $checkout_request->set_header( 'Cart-Token', $woopay_request->get_header('cart_token') );
+ $checkout_data = rest_do_request( $checkout_request )->get_data();
+ }
+
+ remove_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
+
+ return $checkout_data;
+ }
+
/**
* Returns the initial session request data.
*
- * @param int|null $order_id Pay-for-order order ID.
+ * @param int|null $order_id Pay-for-order order ID.
* @param string|null $key Pay-for-order key.
* @param string|null $billing_email Pay-for-order billing email.
+ * @param WP_REST_Request|null $woopay_request The WooPay request object.
* @return array The initial session request data without email and user_session.
*/
- public static function get_init_session_request( $order_id = null, $key = null, $billing_email = null ) {
+ public static function get_init_session_request( $order_id = null, $key = null, $billing_email = null, $woopay_request = null ) {
$user = wp_get_current_user();
$is_pay_for_order = null !== $order_id;
$order = wc_get_order( $order_id );
@@ -381,14 +435,12 @@ public static function get_init_session_request( $order_id = null, $key = null,
include_once WCPAY_ABSPATH . 'includes/compat/blocks/class-blocks-data-extractor.php';
$blocks_data_extractor = new Blocks_Data_Extractor();
- // This uses the same logic as the Checkout block in hydrate_from_api to get the cart and checkout data.
- $cart_data = ! $is_pay_for_order
- ? rest_preload_api_request( [], '/wc/store/v1/cart' )['/wc/store/v1/cart']['body']
- : rest_preload_api_request( [], "/wc/store/v1/order/" . urlencode( $order_id ) . "?key=" . urlencode( $key ) . "&billing_email=" . urlencode( $billing_email ) )[ "/wc/store/v1/order/" . urlencode( $order_id ) . "?key=" . urlencode( $key ) . "&billing_email=" . urlencode( $billing_email ) ]['body'];
- add_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
- $preloaded_checkout_data = rest_preload_api_request( [], '/wc/store/v1/checkout' );
- remove_filter( 'woocommerce_store_api_disable_nonce_check', '__return_true' );
- $checkout_data = isset( $preloaded_checkout_data['/wc/store/v1/checkout'] ) ? $preloaded_checkout_data['/wc/store/v1/checkout']['body'] : '';
+ $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 );
+
+ if ( $woopay_request ) {
+ $order_id = $checkout_data['order_id'] ?? null;
+ }
$email = ! empty( $_POST['email'] ) ? wc_clean( wp_unslash( $_POST['email'] ) ) : '';
@@ -560,20 +612,21 @@ public static function ajax_get_woopay_minimum_session_data() {
/**
* Return WooPay minimum session data.
- *
+ *
* @return array Array of minimum session data used by WooPay or false on failures.
*/
public static function get_woopay_minimum_session_data() {
if ( ! WC_Payments_Features::is_client_secret_encryption_eligible() ) {
return [];
}
-
- $blog_id = Jetpack_Options::get_option('id');
+
+ $blog_id = Jetpack_Options::get_option( 'id' );
if ( empty( $blog_id ) ) {
return [];
}
$data = [
+ 'wcpay_version' => WCPAY_VERSION_NUMBER,
'blog_id' => $blog_id,
'blog_rest_url' => get_rest_url(),
'blog_checkout_url' => wc_get_checkout_url(),
diff --git a/includes/woopay/class-woopay-utilities.php b/includes/woopay/class-woopay-utilities.php
index d2f049c377b..59ec36d3772 100644
--- a/includes/woopay/class-woopay-utilities.php
+++ b/includes/woopay/class-woopay-utilities.php
@@ -252,30 +252,19 @@ public static function get_woopay_url() {
return defined( 'PLATFORM_CHECKOUT_HOST' ) ? PLATFORM_CHECKOUT_HOST : self::DEFAULT_WOOPAY_URL;
}
- /**
- * Returns true if an extension WooPay supports is installed .
- *
- * @return bool
- */
- public function has_adapted_extension_installed() {
- foreach ( self::ADAPTED_EXTENSIONS as $supported_extension ) {
- if ( in_array( $supported_extension, apply_filters( 'active_plugins', get_option( 'active_plugins' ) ), true ) ) {
- return true;
- }
- }
-
- return false;
- }
-
/**
* Return an array with encrypted and signed data.
- *
+ *
* @param array $data The data to be encrypted and signed.
* @return array The encrypted and signed data.
*/
public static function encrypt_and_sign_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 [];
+ }
+
$message = wp_json_encode( $data );
// Generate an initialization vector (IV) for encryption.
diff --git a/package-lock.json b/package-lock.json
index 9ad9ec2587a..82be17a7a26 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "woocommerce-payments",
- "version": "7.5.3",
+ "version": "7.6.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "woocommerce-payments",
- "version": "7.5.3",
+ "version": "7.6.0",
"hasInstallScript": true,
"license": "GPL-3.0-or-later",
"dependencies": {
@@ -25,6 +25,7 @@
"devDependencies": {
"@automattic/color-studio": "2.3.1",
"@jest/test-sequencer": "29.5.0",
+ "@playwright/test": "1.43.1",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.7",
"@testing-library/jest-dom": "5.14.1",
"@testing-library/react": "11.2.5",
@@ -33,6 +34,7 @@
"@types/canvas-confetti": "1.6.4",
"@types/intl-tel-input": "17.0.4",
"@types/lodash": "4.14.170",
+ "@types/node": "20.9.0",
"@types/react": "17.0.2",
"@types/react-transition-group": "4.4.6",
"@types/wordpress__components": "19.10.5",
@@ -7542,6 +7544,21 @@
"node": ">= 8"
}
},
+ "node_modules/@playwright/test": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz",
+ "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==",
+ "dev": true,
+ "dependencies": {
+ "playwright": "1.43.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz",
@@ -8587,10 +8604,13 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.2.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.3.tgz",
- "integrity": "sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==",
- "dev": true
+ "version": "20.9.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
+ "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~5.26.4"
+ }
},
"node_modules/@types/normalize-package-data": {
"version": "2.4.1",
@@ -10294,9 +10314,9 @@
}
},
"node_modules/@woocommerce/csv-export/node_modules/@types/node": {
- "version": "16.18.65",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.65.tgz",
- "integrity": "sha512-5E9WgTy95B7i90oISjui9U5Zu7iExUPfU4ygtv4yXEy6zJFE3oQYHCnh5H1jZRPkjphJt2Ml3oQW6M0qtK534A==",
+ "version": "16.18.68",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.68.tgz",
+ "integrity": "sha512-sG3hPIQwJLoewrN7cr0dwEy+yF5nD4D/4FxtQpFciRD/xwUzgD+G05uxZHv5mhfXo4F9Jkp13jjn0CC2q325sg==",
"dev": true
},
"node_modules/@woocommerce/currency": {
@@ -19158,9 +19178,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001565",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz",
- "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==",
+ "version": "1.0.30001488",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz",
+ "integrity": "sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==",
"dev": true,
"funding": [
{
@@ -33877,6 +33897,36 @@
"node": ">=8"
}
},
+ "node_modules/playwright": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz",
+ "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==",
+ "dev": true,
+ "dependencies": {
+ "playwright-core": "1.43.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz",
+ "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==",
+ "dev": true,
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
"node_modules/please-upgrade-node": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
@@ -40375,6 +40425,12 @@
"through": "^2.3.8"
}
},
+ "node_modules/undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "dev": true
+ },
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
@@ -48210,6 +48266,15 @@
"fastq": "^1.6.0"
}
},
+ "@playwright/test": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz",
+ "integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==",
+ "dev": true,
+ "requires": {
+ "playwright": "1.43.1"
+ }
+ },
"@pmmmwh/react-refresh-webpack-plugin": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.7.tgz",
@@ -49007,10 +49072,13 @@
"dev": true
},
"@types/node": {
- "version": "20.2.3",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.3.tgz",
- "integrity": "sha512-pg9d0yC4rVNWQzX8U7xb4olIOFuuVL9za3bzMT2pu2SU0SNEi66i2qrvhE2qt0HvkhuCaWJu7pLNOt/Pj8BIrw==",
- "dev": true
+ "version": "20.9.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
+ "integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
+ "dev": true,
+ "requires": {
+ "undici-types": "~5.26.4"
+ }
},
"@types/normalize-package-data": {
"version": "2.4.1",
@@ -50468,9 +50536,9 @@
},
"dependencies": {
"@types/node": {
- "version": "16.18.65",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.65.tgz",
- "integrity": "sha512-5E9WgTy95B7i90oISjui9U5Zu7iExUPfU4ygtv4yXEy6zJFE3oQYHCnh5H1jZRPkjphJt2Ml3oQW6M0qtK534A==",
+ "version": "16.18.68",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.68.tgz",
+ "integrity": "sha512-sG3hPIQwJLoewrN7cr0dwEy+yF5nD4D/4FxtQpFciRD/xwUzgD+G05uxZHv5mhfXo4F9Jkp13jjn0CC2q325sg==",
"dev": true
}
}
@@ -57506,9 +57574,9 @@
}
},
"caniuse-lite": {
- "version": "1.0.30001565",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001565.tgz",
- "integrity": "sha512-xrE//a3O7TP0vaJ8ikzkD2c2NgcVUvsEe2IvFTntV4Yd1Z9FVzh+gW+enX96L0psrbaFMcVcH2l90xNuGDWc8w==",
+ "version": "1.0.30001488",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001488.tgz",
+ "integrity": "sha512-NORIQuuL4xGpIy6iCCQGN4iFjlBXtfKWIenlUuyZJumLRIindLb7wXM+GO8erEhb7vXfcnf4BAg2PrSDN5TNLQ==",
"dev": true
},
"canvas-confetti": {
@@ -69129,6 +69197,22 @@
}
}
},
+ "playwright": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz",
+ "integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==",
+ "dev": true,
+ "requires": {
+ "fsevents": "2.3.2",
+ "playwright-core": "1.43.1"
+ }
+ },
+ "playwright-core": {
+ "version": "1.43.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz",
+ "integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==",
+ "dev": true
+ },
"please-upgrade-node": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/please-upgrade-node/-/please-upgrade-node-3.2.0.tgz",
@@ -74145,6 +74229,12 @@
"through": "^2.3.8"
}
},
+ "undici-types": {
+ "version": "5.26.5",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+ "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+ "dev": true
+ },
"unicode-canonical-property-names-ecmascript": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz",
diff --git a/package.json b/package.json
index 14235ed3ad4..dcc53a83834 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "woocommerce-payments",
- "version": "7.5.3",
+ "version": "7.6.0",
"main": "webpack.config.js",
"author": "Automattic",
"license": "GPL-3.0-or-later",
@@ -37,6 +37,9 @@
"test:e2e": "NODE_CONFIG_DIR='tests/e2e/config' JEST_PUPPETEER_CONFIG='tests/e2e/config/jest-puppeteer-headless.config.js' wp-scripts test-e2e --config tests/e2e/config/jest.config.js",
"test:e2e-dev": "NODE_CONFIG_DIR='tests/e2e/config' JEST_PUPPETEER_CONFIG='tests/e2e/config/jest-puppeteer.config.js' wp-scripts test-e2e --config tests/e2e/config/jest.config.js --puppeteer-interactive",
"test:e2e-performance": "NODE_CONFIG_DIR='tests/e2e/config' wp-scripts test-e2e --config tests/e2e/config/jest.performance.config.js",
+ "test:e2e-pw": "./tests/e2e-pw/test-e2e-pw.sh",
+ "test:e2e-pw-ui": "./tests/e2e-pw/test-e2e-pw-ui.sh",
+ "test:e2e-pw-ci": "npx playwright test --config=tests/e2e-pw/playwright.config.ts",
"test:update-snapshots": "npm run test:js -- --updateSnapshot",
"test:php": "./bin/run-tests.sh",
"test:php-coverage": "./bin/check-test-coverage.sh",
@@ -85,6 +88,7 @@
"devDependencies": {
"@automattic/color-studio": "2.3.1",
"@jest/test-sequencer": "29.5.0",
+ "@playwright/test": "1.43.1",
"@pmmmwh/react-refresh-webpack-plugin": "0.5.7",
"@testing-library/jest-dom": "5.14.1",
"@testing-library/react": "11.2.5",
@@ -93,6 +97,7 @@
"@types/canvas-confetti": "1.6.4",
"@types/intl-tel-input": "17.0.4",
"@types/lodash": "4.14.170",
+ "@types/node": "20.9.0",
"@types/react": "17.0.2",
"@types/react-transition-group": "4.4.6",
"@types/wordpress__components": "19.10.5",
diff --git a/psalm-baseline.xml b/psalm-baseline.xml
index 17038230df6..b358e46babb 100644
--- a/psalm-baseline.xml
+++ b/psalm-baseline.xml
@@ -100,4 +100,11 @@
$stripe_billing_migrator
+
+
+ wcs_get_subscriptions_for_order( $order )
+ wcs_is_manual_renewal_required()
+ wcs_order_contains_subscription( $order_id )
+
+
diff --git a/readme.txt b/readme.txt
index dc97710888a..5ab94edab84 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.4
Requires PHP: 7.3
-Stable tag: 7.5.3
+Stable tag: 7.6.0
License: GPLv2 or later
License URI: http://www.gnu.org/licenses/gpl-2.0.html
@@ -94,6 +94,42 @@ Please note that our support for the checkout block is still experimental and th
== Changelog ==
+= 7.6.0 - 2024-05-08 =
+* Add - Add additional data to Compatibility service
+* Add - Add User Satisfaction Survey for Payments Overview Widget
+* Add - Detect payment methods enabled by multiple payment gateways.
+* Add - Display BNPL payment method logos on the thank you page.
+* Add - Non user-facing changes. Behind feature flag. Add tooltip messages to tiles within Payment activity widget
+* Add - Not user-facing: hidden behind feature flag. Use Reporting API to fetch and populate data in the Payment Activity widget.
+* Add - Pre check save my info for eligible contries
+* Add - Support for starting auto-renewing subscriptions for In-Person Payments.
+* Fix - Add notice when no rules are enabled in advanced fraud settings
+* Fix - Adjust positioning of BNPL messaging on the classic cart page.
+* Fix - Avoid updating billing details for legacy card objects.
+* Fix - Ensure the WooPay preview in the admin dashboard matches the actual implementation.
+* Fix - fix: BNPL announcement link.
+* Fix - fix: Stripe terms warning at checkout when Link is enabled
+* Fix - Fix issue with transient check related to fraud protection settings.
+* Fix - Fix order notes entry and risk meta box content when a payment is blocked due to AVS checks while the corresponding advanced fraud rule is enabled.
+* Fix - Fix type error for fraud outcome API.
+* Fix - Fix WooPay tracks user ID for logged in users.
+* Fix - Hide Fraud info banner until first transaction happens
+* Fix - Improve merchant session request with preloaded data.
+* Fix - Improve signing of minimum WooPay session data.
+* Fix - Make sure an explicit currency code is present in the cart and checkout blocks when multi-currency is enabled
+* Fix - Prevent Stripe Link from triggering the checkout fields warning
+* Fix - Remove risk review request from the transactions page.
+* Fix - Resolves "Invalid recurring shipping method" errors when purchasing multiple subscriptions with Apple Pay and Google Pay.
+* Fix - Revert: Add Multi-Currency Support to Page Caching via Cookies.
+* Update - Add source param to onboarding and complete KYC links
+* Update - Add support of a third-party plugin with PRBs into duplicates detection mechanism.
+* Update - Remove feature flag for the pay-for-order flow
+* Dev - Add Playwright e2e test suite ready for incremental migration and visual regression testing
+* Dev - Avoid warnings about fatal error during plugin update due to problems with plugin initialization.
+* Dev - Remove legacy method from `WooPay_Utilities`.
+* Dev - Remove obsolete docker-compose key `version`
+* Dev - Upgraded jetpack sync package version.
+
= 7.5.3 - 2024-04-22 =
* Fix - Fix subscription renewals exceptions
* Dev - Remove deprecated param from asset data registry interface.
diff --git a/tests/e2e-pw/README.md b/tests/e2e-pw/README.md
new file mode 100644
index 00000000000..84bf18a95ff
--- /dev/null
+++ b/tests/e2e-pw/README.md
@@ -0,0 +1,102 @@
+# Playwright end-to-end tests π
+
+Playwright e2e tests can be found in the `./tests/e2e-pw/specs` directory. These will run in parallel with the existing Puppeteer e2e tests and are intended to replace them as they are migrated.
+
+## Setup local e2e environment
+
+See [tests/e2e/README.md](/tests/e2e/README.md) for detailed e2e environment setup instructions.
+
+1. `npm run test:e2e-setup`
+1. `npm run test:e2e-up`
+
+## Running Playwright e2e tests
+
+- `npm run test:e2e-pw` headless run from within a linux docker container.
+- `npm run test:e2e-pw-ui` runs tests in interactive UI mode from within a linux docker container β recommended for authoring tests and re-running failed tests.
+- `npm run test:e2e-pw keyword` runs tests only with a specific keyword in the file name, e.g. `dispute` or `checkout`.
+- `npm run test:e2e-pw --update-snapshots` updates snapshots.
+
+## FAQs
+
+**How do I wait for a page or element to load?**
+
+Since [Playwright automatically waits](https://playwright.dev/docs/actionability) for elements to be present in the page before interacting with them, you probably don't need to explicitly wait for elements to load. For example, all of the following locators will automatically wait for the element to be present and stable before asserting or interacting with it:
+
+```ts
+await expect( page.getByRole( 'heading', { name: 'Sign up' } ) ).toBeVisible();
+await page.getByRole( 'checkbox', { name: 'Subscribe' } ).check();
+await page.getByRole( 'button', { name: /submit/i } ).click();
+```
+
+In some cases, you may need to wait for the page to reach a certain load state before interacting with it. You can use `await page.waitForLoadState( 'domcontentloaded' );` to wait for the page to finish loading.
+
+**What is the best way to target elements in the page?**
+
+Prefer the use of [user-facing attribute or test-id locators](https://playwright.dev/docs/locators#locating-elements) to target elements in the page. This will make the tests more resilient to changes to implementation details, such as class names.
+
+```ts
+// Prefer locating by role, label, text, or test id when possible. See https://playwright.dev/docs/locators
+await page.getByRole( 'button', { name: 'All deposits' } ).click();
+await page.getByLabel( 'Select a deposit status' ).selectOption( 'Pending' );
+await expect( page.getByText( 'Order received' ) ).toBeVisible();
+await page.getByTestId( 'accept-dispute-button' ).click();
+
+// Use CSS selectors as a last resort
+await page.locator( 'button.components-button.is-secondary' );
+```
+
+**How do I create a visual regression test?**
+
+Visual regression tests are captured by the [`toHaveScreenshot()` function](https://playwright.dev/docs/api/class-pageassertions#page-assertions-to-have-screenshot-2). This function takes a screenshot of a page or element and compares it to a reference image. If the images are different, the test will fail.
+
+```ts
+await expect( page ).toHaveScreenshot();
+
+await expect(
+ page.getByRole( 'button', { name: 'All deposits' } )
+).toHaveScreenshot();
+```
+
+**How can I act as shopper or merchant in a test?**
+
+1. To switch between `shopper` and `merchant` role in a test, use the `getShopper` and `getMerchant` function:
+
+```ts
+import { getShopper, getMerchant } from './utils/helpers';
+
+test( 'should do things as shopper and merchant', async ( { browser } ) => {
+ const { shopperPage } = await getShopper( browser );
+ const { merchantPage } = await getMerchant( browser );
+
+ // do things as shopper
+ await shopperPage.goto( '/cart/' );
+
+ // do things as merchant
+ await merchantPage.goto( '/wp-admin/admin.php?page=wc-settings' );
+} );
+```
+
+2. To act as `shopper` or `merchant` for an entire test suite (`describe`), use the helper function `useShopper` or `useMerchant` from `tests/e2e-pw/utils/helpers.ts`:
+
+```ts
+import { useShopper } from '../utils/helpers';
+
+test.describe( 'Sign in as customer', () => {
+ useShopper();
+ test( 'Load customer my account page', async ( { page } ) => {
+ // do things as shopper
+ await page.goto( '/my-account' );
+ } );
+} );
+```
+
+**How can I investigate and interact with a test failures?**
+
+- **Github Action test runs**
+ - View GitHub checks in the "Checks" tab of a PR
+ - Click on the "E2E Playwright Tests" job to see the job summary
+ - Download the `playwright-report.zip` artifact, extract and copy the `playwright-report` directory to the root of the WooPayments repository
+ - Run `npx playwright show-report` to open the report in a browser
+- **Local test runs**:
+ - Local test reports will output in the `playwright-report` directory
+ - Run `npx playwright show-report` to open the report in a browser
diff --git a/tests/e2e-pw/config/default.ts b/tests/e2e-pw/config/default.ts
new file mode 100644
index 00000000000..a3742804047
--- /dev/null
+++ b/tests/e2e-pw/config/default.ts
@@ -0,0 +1,275 @@
+export const config = {
+ users: {
+ admin: {
+ username: 'admin',
+ password: 'password',
+ email: 'e2e-wcpay-admin@woo.com',
+ },
+ customer: {
+ username: 'customer',
+ password: 'password',
+ email: 'e2e-wcpay-customer@woo.com',
+ },
+ 'subscriptions-customer': {
+ username: 'subscriptions-customer',
+ password: 'password',
+ email: 'e2e-wcpay-customer@woo.com',
+ },
+ guest: {
+ email: 'e2e-wcpay-guest@woo.com',
+ },
+ },
+ products: {
+ simple: {
+ name: 'Beanie',
+ },
+ variable: {
+ name: 'Variable Product with Three Variations',
+ },
+ grouped: {
+ name: 'Grouped Product with Three Children',
+ },
+ },
+ addresses: {
+ admin: {
+ store: {
+ firstname: 'I am',
+ lastname: 'Admin',
+ company: 'Automattic',
+ country: 'United States (US)',
+ addressfirstline: '60 29th Street #343',
+ addresssecondline: 'store',
+ countryandstate: 'United States (US) β California',
+ city: 'San Francisco',
+ state: 'CA',
+ postcode: '94110',
+ email: 'e2e-wcpay-subscriptions-customer@woo.com',
+ },
+ },
+ customer: {
+ billing: {
+ firstname: 'I am',
+ lastname: 'Customer',
+ company: 'Automattic',
+ country: 'United States (US)',
+ addressfirstline: '60 29th Street #343',
+ addresssecondline: 'billing',
+ city: 'San Francisco',
+ state: 'CA',
+ postcode: '94110',
+ phone: '123456789',
+ email: 'e2e-wcpay-customer@woo.com',
+ },
+ shipping: {
+ firstname: 'I am',
+ lastname: 'Recipient',
+ company: 'Automattic',
+ country: 'United States (US)',
+ addressfirstline: '60 29th Street #343',
+ addresssecondline: 'shipping',
+ city: 'San Francisco',
+ state: 'CA',
+ postcode: '94110',
+ phone: '123456789',
+ email: 'e2e-wcpay-customer@woo.com',
+ },
+ },
+ 'subscriptions-customer': {
+ billing: {
+ first_name: 'I am',
+ last_name: 'Subscriptions Customer',
+ company: 'Automattic',
+ country: 'United States (US)',
+ address_1: '60 29th Street #343',
+ address_2: 'billing',
+ city: 'San Francisco',
+ state: 'CA',
+ postcode: '94110',
+ phone: '123456789',
+ email: 'e2e-wcpay-subscriptions-customer@woo.com',
+ },
+ shipping: {
+ first_name: 'I am',
+ last_name: 'Subscriptions Recipient',
+ company: 'Automattic',
+ country: 'United States (US)',
+ address_1: '60 29th Street #343',
+ address_2: 'shipping',
+ city: 'San Francisco',
+ state: 'CA',
+ postcode: '94110',
+ phone: '123456789',
+ email: 'e2e-wcpay-subscriptions-customer@woo.com',
+ },
+ },
+ },
+ cards: {
+ basic: {
+ number: '4242424242424242',
+ expires: {
+ month: '02',
+ year: '45',
+ },
+ cvc: '424',
+ label: 'Visa ending in 4242',
+ },
+ basic2: {
+ number: '4111111111111111',
+ expires: {
+ month: '11',
+ year: '45',
+ },
+ cvc: '123',
+ label: 'Visa ending in 1111',
+ },
+ basic3: {
+ number: '378282246310005',
+ expires: {
+ month: '11',
+ year: '45',
+ },
+ cvc: '1234',
+ label: 'Amex ending in 0005',
+ },
+ '3ds': {
+ number: '4000002760003184',
+ expires: {
+ month: '03',
+ year: '45',
+ },
+ cvc: '525',
+ label: 'Visa ending in 3184',
+ },
+ '3dsOTP': {
+ number: '4000002500003155',
+ expires: {
+ month: '04',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 3155',
+ },
+ '3ds2': {
+ number: '4000000000003220',
+ expires: {
+ month: '04',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 3220',
+ },
+ 'disputed-fraudulent': {
+ number: '4000000000000259',
+ expires: {
+ month: '05',
+ year: '45',
+ },
+ cvc: '525',
+ label: 'Visa ending in 0259',
+ },
+ 'disputed-unreceived': {
+ number: '4000000000002685',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 2685',
+ },
+ declined: {
+ number: '4000000000000002',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 0002',
+ },
+ 'declined-funds': {
+ number: '4000000000009995',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 9995',
+ },
+ 'declined-incorrect': {
+ number: '4242424242424241',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 4241',
+ },
+ 'declined-expired': {
+ number: '4000000000000069',
+ expires: {
+ month: '06',
+ year: '25',
+ },
+ cvc: '626',
+ label: 'Visa ending in 0069',
+ },
+ 'declined-cvc': {
+ number: '4000000000000127',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 0127',
+ },
+ 'declined-processing': {
+ number: '4000000000000119',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 0119',
+ },
+ 'declined-3ds': {
+ number: '4000008400001629',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '626',
+ label: 'Visa ending in 1629',
+ },
+ 'invalid-exp-date': {
+ number: '4242424242424242',
+ expires: {
+ month: '11',
+ year: '12',
+ },
+ cvc: '123',
+ label: 'Visa ending in 4242',
+ },
+ 'invalid-cvv-number': {
+ number: '4242424242424242',
+ expires: {
+ month: '06',
+ year: '45',
+ },
+ cvc: '11',
+ label: 'Visa ending in 4242',
+ },
+ },
+ onboardingwizard: {
+ industry: 'Test industry',
+ numberofproducts: '1 - 10',
+ sellingelsewhere: 'No',
+ },
+ settings: {
+ shipping: {
+ zonename: 'United States',
+ zoneregions: 'United States (US)',
+ shippingmethod: 'Free shipping',
+ },
+ },
+};
+
+export type CustomerAddress = typeof config.addresses.customer.billing;
diff --git a/tests/e2e-pw/docker-compose.yml b/tests/e2e-pw/docker-compose.yml
new file mode 100644
index 00000000000..2919d6ab568
--- /dev/null
+++ b/tests/e2e-pw/docker-compose.yml
@@ -0,0 +1,12 @@
+services:
+ playwright:
+ # When updating the Playwright version in the image tag below, make sure to update the npm `@playwright/test` package.json version as well.
+ image: mcr.microsoft.com/playwright:v1.43.1-jammy
+ working_dir: /woopayments
+ volumes:
+ - $PWD:/woopayments
+ environment:
+ - "BASE_URL=http://host.docker.internal:8084"
+ ports:
+ - "8077:8077"
+ ipc: host
diff --git a/tests/e2e-pw/playwright.config.ts b/tests/e2e-pw/playwright.config.ts
new file mode 100644
index 00000000000..74ab435eb9a
--- /dev/null
+++ b/tests/e2e-pw/playwright.config.ts
@@ -0,0 +1,70 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+/**
+ * External dependencies
+ */
+import { defineConfig, devices } from '@playwright/test';
+
+const { BASE_URL } = process.env;
+
+/**
+ * See https://playwright.dev/docs/test-configuration.
+ */
+export default defineConfig( {
+ testDir: './specs/',
+ /* Run tests in files in parallel */
+ fullyParallel: false,
+ /* Fail the build on CI if you accidentally left test.only in the source code. */
+ forbidOnly: !! process.env.CI,
+ /* Retry on CI only */
+ retries: process.env.CI ? 2 : 0,
+ /* Opt out of parallel tests. */
+ workers: 1,
+ /* Reporters to use. See https://playwright.dev/docs/test-reporters */
+ reporter: process.env.CI
+ ? [
+ // If running on CI, also use the GitHub Actions reporter
+ [ 'github' ],
+ [ 'html' ],
+ ]
+ : [ [ 'html', { open: 'never' } ] ],
+ outputDir: './test-results',
+ /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
+ use: {
+ baseURL: BASE_URL ?? 'http://localhost:8084',
+ screenshot: 'only-on-failure',
+ trace: 'retain-on-failure',
+ video: 'on-first-retry',
+ viewport: { width: 1280, height: 720 },
+ },
+ timeout: 60 * 1000, // Default is 30s, somteimes it is not enough for local tests due to long setup.
+ expect: {
+ toHaveScreenshot: { maxDiffPixelRatio: 0.025 },
+ //=* Increase expect timeout to 10 seconds. See https://playwright.dev/docs/test-timeouts#set-expect-timeout-in-the-config.*/
+ timeout: 10 * 1000,
+ },
+ snapshotPathTemplate: '{testDir}/__snapshots__/{testFilePath}/{arg}{ext}',
+
+ /* Configure projects for major browsers */
+ projects: [
+ {
+ name: 'basic',
+ use: { ...devices[ 'Desktop Chrome' ] },
+ testMatch: /basic.spec.ts/,
+ dependencies: [ 'setup' ],
+ },
+ {
+ name: 'merchant',
+ use: { ...devices[ 'Desktop Chrome' ] },
+ testDir: './specs/merchant',
+ dependencies: [ 'setup' ],
+ },
+ {
+ name: 'shopper',
+ use: { ...devices[ 'Desktop Chrome' ] },
+ testDir: './specs/shopper',
+ dependencies: [ 'setup' ],
+ },
+ // Setup project
+ { name: 'setup', testMatch: /.*\.setup\.ts/ },
+ ],
+} );
diff --git a/tests/e2e-pw/specs/auth.setup.ts b/tests/e2e-pw/specs/auth.setup.ts
new file mode 100644
index 00000000000..595ff6c94dd
--- /dev/null
+++ b/tests/e2e-pw/specs/auth.setup.ts
@@ -0,0 +1,129 @@
+/* eslint-disable no-console */
+/**
+ * External dependencies
+ */
+import { test as setup, expect } from '@playwright/test';
+import fs from 'fs';
+
+/**
+ * Internal dependencies
+ */
+import { config } from '../config/default';
+import {
+ merchantStorageFile,
+ customerStorageFile,
+ wpAdminLogin,
+} from '../utils/helpers';
+
+// See https://playwright.dev/docs/auth#multiple-signed-in-roles
+const {
+ users: { admin, customer },
+} = config;
+
+const isAuthStateStale = ( authStateFile: string ) => {
+ const authFileExists = fs.existsSync( authStateFile );
+
+ if ( ! authFileExists ) {
+ return true;
+ }
+
+ const authStateMtimeMs = fs.statSync( authStateFile ).mtimeMs;
+ const hourInMs = 1000 * 60 * 60;
+ // Invalidate auth state if it's older than a 3 hours.
+ const isStale = Date.now() - authStateMtimeMs > hourInMs * 3;
+ return isStale;
+};
+
+setup( 'authenticate as admin', async ( { page } ) => {
+ // For local development, use existing state if it exists and isn't stale.
+ if ( ! process.env.CI ) {
+ if ( ! isAuthStateStale( merchantStorageFile ) ) {
+ console.log( 'Using existing merchant state.' );
+ return;
+ }
+ }
+
+ // Sign in as admin user and save state
+ let adminLoggedIn = false;
+ const adminRetries = 5;
+ for ( let i = 0; i < adminRetries; i++ ) {
+ try {
+ console.log( 'Trying to log-in as admin...' );
+ await wpAdminLogin( page, admin );
+ await page.waitForLoadState( 'domcontentloaded' );
+ await page.goto( `/wp-admin` );
+ await page.waitForLoadState( 'domcontentloaded' );
+
+ await expect(
+ page.getByRole( 'heading', { name: 'Dashboard' } )
+ ).toBeVisible();
+
+ console.log( 'Logged-in as admin successfully.' );
+ adminLoggedIn = true;
+ break;
+ } catch ( e ) {
+ console.log(
+ `Admin log-in failed, Retrying... ${ i }/${ adminRetries }`
+ );
+ console.log( e );
+ }
+ }
+
+ if ( ! adminLoggedIn ) {
+ throw new Error(
+ 'Cannot proceed e2e test, as admin login failed. Please check if the test site has been setup correctly.'
+ );
+ }
+
+ // End of authentication steps.
+
+ await page.context().storageState( { path: merchantStorageFile } );
+} );
+
+setup( 'authenticate as customer', async ( { page } ) => {
+ // For local development, use existing state if it exists and isn't stale.
+ if ( ! process.env.CI ) {
+ if ( ! isAuthStateStale( customerStorageFile ) ) {
+ console.log( 'Using existing customer state.' );
+ return;
+ }
+ }
+
+ // Sign in as customer user and save state
+ let customerLoggedIn = false;
+ const customerRetries = 5;
+ for ( let i = 0; i < customerRetries; i++ ) {
+ try {
+ console.log( 'Trying to log-in as customer...' );
+ await wpAdminLogin( page, customer );
+
+ await page.goto( `/my-account` );
+ await expect(
+ page.locator(
+ '.woocommerce-MyAccount-navigation-link--customer-logout'
+ )
+ ).toBeVisible();
+ await expect(
+ page.locator( 'div.woocommerce-MyAccount-content > p >> nth=0' )
+ ).toContainText( 'Hello' );
+
+ console.log( 'Logged-in as customer successfully.' );
+ customerLoggedIn = true;
+ break;
+ } catch ( e ) {
+ console.log(
+ `Customer log-in failed. Retrying... ${ i }/${ customerRetries }`
+ );
+ console.log( e );
+ }
+ }
+
+ if ( ! customerLoggedIn ) {
+ throw new Error(
+ 'Cannot proceed e2e test, as customer login failed. Please check if the test site has been setup correctly.'
+ );
+ }
+ // End of authentication steps.
+
+ await page.context().storageState( { path: customerStorageFile } );
+} );
diff --git a/tests/e2e-pw/specs/basic.spec.ts b/tests/e2e-pw/specs/basic.spec.ts
new file mode 100644
index 00000000000..910176fdc21
--- /dev/null
+++ b/tests/e2e-pw/specs/basic.spec.ts
@@ -0,0 +1,39 @@
+/**
+ * External dependencies
+ */
+import { test, expect } from '@playwright/test';
+import { useMerchant, useShopper } from '../utils/helpers';
+
+test.describe(
+ 'A basic set of tests to ensure WP, wp-admin and my-account load',
+ () => {
+ test( 'Load the home page', async ( { page } ) => {
+ await page.goto( '/' );
+ const title = page.locator( 'h1.site-title' );
+ await expect( title ).toHaveText(
+ /WooCommerce Payments E2E site/i
+ );
+ } );
+
+ test.describe( 'Sign in as admin', () => {
+ useMerchant();
+ test( 'Load Payments Overview', async ( { page } ) => {
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-admin&path=/payments/overview'
+ );
+ await page.waitForLoadState( 'domcontentloaded' );
+ const logo = page.getByAltText( 'WooPayments logo' );
+ await expect( logo ).toBeVisible();
+ } );
+ } );
+
+ test.describe( 'Sign in as customer', () => {
+ useShopper();
+ test( 'Load customer my account page', async ( { page } ) => {
+ await page.goto( '/my-account' );
+ const title = page.locator( 'h1.entry-title' );
+ await expect( title ).toHaveText( 'My account' );
+ } );
+ } );
+ }
+);
diff --git a/tests/e2e-pw/specs/merchant/__snapshots__/merchant-admin-deposits.spec.ts/Merchant-deposits-Select-deposits-list-advanced-filters-1.png b/tests/e2e-pw/specs/merchant/__snapshots__/merchant-admin-deposits.spec.ts/Merchant-deposits-Select-deposits-list-advanced-filters-1.png
new file mode 100644
index 00000000000..a982090d756
Binary files /dev/null and b/tests/e2e-pw/specs/merchant/__snapshots__/merchant-admin-deposits.spec.ts/Merchant-deposits-Select-deposits-list-advanced-filters-1.png differ
diff --git a/tests/e2e-pw/specs/merchant/__snapshots__/merchant-disputes-view-details-via-order-notice.spec.ts/Disputes-View-dispute-details-via-disputed-o-f9e9d-ils-when-disputed-order-notice-button-clicked-1.png b/tests/e2e-pw/specs/merchant/__snapshots__/merchant-disputes-view-details-via-order-notice.spec.ts/Disputes-View-dispute-details-via-disputed-o-f9e9d-ils-when-disputed-order-notice-button-clicked-1.png
new file mode 100644
index 00000000000..843ee4afe8b
Binary files /dev/null and b/tests/e2e-pw/specs/merchant/__snapshots__/merchant-disputes-view-details-via-order-notice.spec.ts/Disputes-View-dispute-details-via-disputed-o-f9e9d-ils-when-disputed-order-notice-button-clicked-1.png differ
diff --git a/tests/e2e-pw/specs/merchant/merchant-admin-deposits.spec.ts b/tests/e2e-pw/specs/merchant/merchant-admin-deposits.spec.ts
new file mode 100644
index 00000000000..c0e20768b36
--- /dev/null
+++ b/tests/e2e-pw/specs/merchant/merchant-admin-deposits.spec.ts
@@ -0,0 +1,61 @@
+/**
+ * External dependencies
+ */
+import { test, expect } from '@playwright/test';
+import { useMerchant } from '../../utils/helpers';
+
+test.describe( 'Merchant deposits', () => {
+ // Use the merchant user for this test suite.
+ useMerchant();
+
+ test( 'Load the deposits list page', async ( { page } ) => {
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-admin&path=/payments/deposits'
+ );
+
+ // Wait for the deposits table to load.
+ await page
+ .locator( '.woocommerce-table__table.is-loading' )
+ .waitFor( { state: 'hidden' } );
+
+ expect(
+ page.getByRole( 'heading', {
+ name: 'Deposit history',
+ } )
+ ).toBeVisible();
+ } );
+
+ test( 'Select deposits list advanced filters', async ( { page } ) => {
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-admin&path=/payments/deposits'
+ );
+
+ // Wait for the deposits table to load.
+ await page
+ .locator( '.woocommerce-table__table.is-loading' )
+ .waitFor( { state: 'hidden' } );
+
+ // Open the advanced filters.
+ await page.getByRole( 'button', { name: 'All deposits' } ).click();
+ await page.getByRole( 'button', { name: 'Advanced filters' } ).click();
+
+ // Select a filter
+ await page.getByRole( 'button', { name: 'Add a Filter' } ).click();
+ await page.getByRole( 'button', { name: 'Status' } ).click();
+
+ // Select a filter option
+ await page
+ .getByLabel( 'Select a deposit status', {
+ exact: true,
+ } )
+ .selectOption( 'Pending' );
+
+ // Scroll to the top to ensure the sticky header doesn't cover the filters.
+ await page.evaluate( () => {
+ window.scrollTo( 0, 0 );
+ } );
+ await expect(
+ page.locator( '.woocommerce-filters' ).last()
+ ).toHaveScreenshot();
+ } );
+} );
diff --git a/tests/e2e-pw/specs/merchant/merchant-disputes-view-details-via-order-notice.spec.ts b/tests/e2e-pw/specs/merchant/merchant-disputes-view-details-via-order-notice.spec.ts
new file mode 100644
index 00000000000..cd53d8dce72
--- /dev/null
+++ b/tests/e2e-pw/specs/merchant/merchant-disputes-view-details-via-order-notice.spec.ts
@@ -0,0 +1,82 @@
+/**
+ * External dependencies
+ */
+import { test, expect } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import * as shopper from '../../utils/shopper';
+import { config } from '../../config/default';
+import { getMerchant, getShopper } from '../../utils/helpers';
+import * as merchant from '../../utils/merchant';
+
+test.describe(
+ 'Disputes > View dispute details via disputed order notice',
+ () => {
+ let orderId: string;
+
+ test.beforeEach( async ( { browser } ) => {
+ const { shopperPage } = await getShopper( browser );
+ // Place an order to dispute later
+ await shopperPage.goto( '/cart/' );
+ await shopper.addCartProduct( shopperPage );
+
+ await shopperPage.goto( '/checkout/' );
+ await shopper.fillBillingAddress(
+ shopperPage,
+ config.addresses.customer.billing
+ );
+ await shopper.fillCardDetails(
+ shopperPage,
+ config.cards[ 'disputed-fraudulent' ]
+ );
+ await shopper.placeOrder( shopperPage );
+
+ // Get the order ID
+ const orderIdField = shopperPage.locator(
+ '.woocommerce-order-overview__order.order > strong'
+ );
+ orderId = await orderIdField.innerText();
+ } );
+
+ test( 'should navigate to dispute details when disputed order notice button clicked', async ( {
+ browser,
+ } ) => {
+ const { merchantPage } = await getMerchant( browser );
+ await merchant.goToOrder( merchantPage, orderId );
+
+ // If WC < 7.9, return early since the order dispute notice is not present.
+ const orderPaymentDetailsContainerVisible = await merchantPage
+ .locator( '#wcpay-order-payment-details-container' )
+ .isVisible();
+ if ( ! orderPaymentDetailsContainerVisible ) {
+ // eslint-disable-next-line no-console
+ console.log(
+ 'Skipping test since the order dispute notice is not present in WC < 7.9'
+ );
+ return;
+ }
+
+ // Click the order dispute notice.
+ await merchantPage
+ .getByRole( 'button', {
+ name: 'Respond now',
+ } )
+ .click();
+
+ // Verify we see the dispute details on the transaction details merchantPage.
+ await expect(
+ merchantPage.getByText(
+ 'The cardholder claims this is an unauthorized transaction.',
+ { exact: true }
+ )
+ ).toBeVisible();
+
+ // Visual regression test for the dispute notice.
+ await expect(
+ merchantPage.locator( '.dispute-notice' )
+ ).toHaveScreenshot();
+ } );
+ }
+);
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
new file mode 100644
index 00000000000..6197cfa5fcb
--- /dev/null
+++ b/tests/e2e-pw/specs/merchant/merchant-payment-gateways-confirmation.spec.ts
@@ -0,0 +1,115 @@
+/**
+ * External dependencies
+ */
+import { test, expect, Page } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+import { useMerchant } from '../../utils/helpers';
+
+test.describe( 'payment gateways disable confirmation', () => {
+ useMerchant();
+
+ const getToggle = ( page: Page ) =>
+ page.getByRole( 'link', {
+ name: '"WooPayments" payment method is currently',
+ } );
+
+ const getModalHeading = ( page: Page ) =>
+ page.getByRole( 'heading', { name: 'Disable WooPayments' } );
+
+ const getSaveButton = ( page: Page ) =>
+ page.getByRole( 'button', { name: 'Save changes' } );
+
+ const getCancelButton = ( page: Page ) =>
+ page.getByRole( 'button', { name: 'Cancel' } );
+
+ const getDisableButton = ( page: Page ) =>
+ page.getByRole( 'button', { name: 'Disable' } );
+
+ const waitForToggleLoading = ( page: Page ) =>
+ page
+ .locator( '.woocommerce-input-toggle--loading' )
+ .waitFor( { state: 'hidden' } );
+
+ test.beforeEach( async ( { page } ) => {
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-settings&tab=checkout§ion'
+ );
+
+ // If WCPay enabled, disable it
+ if ( ( await getToggle( page ).innerText() ) === 'Yes' ) {
+ // Click the "Disable WCPay" toggle button
+ await getToggle( page ).click();
+
+ // Modal should be displayed
+ await expect( getModalHeading( page ) ).toBeVisible();
+ }
+ } );
+
+ test.afterAll( async ( { browser } ) => {
+ // Ensure WCPay is enabled after the tests, even if they fail
+ const page = await browser.newPage();
+ await page.goto(
+ '/wp-admin/admin.php?page=wc-settings&tab=checkout§ion'
+ );
+
+ if ( ( await getToggle( page ).innerText() ) === 'No' ) {
+ await getToggle( page ).click();
+ await waitForToggleLoading( page );
+ await getSaveButton( page ).click();
+ }
+
+ await expect( getToggle( page ) ).toHaveText( 'Yes' );
+ } );
+
+ test( 'should show the confirmation modal when disabling WCPay', async ( {
+ page,
+ } ) => {
+ // Clicking "Cancel" should not disable WCPay
+ await getCancelButton( page ).click();
+
+ // After clicking "Cancel", the modal should close and WCPay should still be enabled, even after refresh
+ await expect( getModalHeading( page ) ).not.toBeVisible();
+ await getSaveButton( page ).click();
+ await expect( getToggle( page ) ).toHaveText( 'Yes' );
+ } );
+
+ test( 'should disable WCPay after confirming, then enable again without confirming', async ( {
+ page,
+ } ) => {
+ // Clicking "Disable" should disable WCPay
+ await getDisableButton( page ).click();
+
+ // After clicking "Disable", the modal should close
+ await expect( getModalHeading( page ) ).not.toBeVisible();
+
+ // and refreshing the page should show WCPay become disabled
+ await waitForToggleLoading( page );
+ await getSaveButton( page ).click();
+
+ // now we can re-enable it with no issues
+ await getToggle( page ).click();
+ await waitForToggleLoading( page );
+ await getSaveButton( page ).click();
+ await expect( getToggle( page ) ).toHaveText( 'Yes' );
+ } );
+
+ test( 'should show the modal even after clicking the cancel button multiple times', async ( {
+ page,
+ } ) => {
+ // Clicking "Cancel" should not disable WCPay
+ await getCancelButton( page ).click();
+
+ // After clicking "Cancel", the modal should close and WCPay should still be enabled
+ await expect( getModalHeading( page ) ).not.toBeVisible();
+ await expect( getToggle( page ) ).not.toHaveClass(
+ 'woocommerce-input-toggle--disabled'
+ );
+
+ // trying again to disable it - the modal should display again
+ await getToggle( page ).click();
+ await expect( getModalHeading( page ) ).toBeVisible();
+ } );
+} );
diff --git a/tests/e2e-pw/specs/shopper/shopper-checkout-purchase.spec.ts b/tests/e2e-pw/specs/shopper/shopper-checkout-purchase.spec.ts
new file mode 100644
index 00000000000..b062ab0098a
--- /dev/null
+++ b/tests/e2e-pw/specs/shopper/shopper-checkout-purchase.spec.ts
@@ -0,0 +1,42 @@
+/**
+ * External dependencies
+ */
+import { test, expect } from '@playwright/test';
+
+/**
+ * Internal dependencies
+ */
+
+import { config } from '../../config/default';
+import * as shopper from '../../utils/shopper';
+
+test.describe( 'Successful purchase', () => {
+ test.beforeEach( async ( { page } ) => {
+ await shopper.addCartProduct( page );
+
+ await page.goto( '/checkout/' );
+ await shopper.fillBillingAddress(
+ page,
+ config.addresses.customer.billing
+ );
+ } );
+
+ test( 'using a basic card', async ( { page } ) => {
+ await shopper.fillCardDetails( page );
+ await shopper.placeOrder( page );
+
+ await expect(
+ page.getByText( 'Order received' ).first()
+ ).toBeVisible();
+ } );
+
+ test( 'using a 3DS card', async ( { page } ) => {
+ await shopper.fillCardDetails( page, config.cards[ '3ds' ] );
+ await shopper.placeOrder( page );
+ await shopper.confirmCardAuthentication( page );
+
+ await expect(
+ page.getByText( 'Order received' ).first()
+ ).toBeVisible();
+ } );
+} );
diff --git a/tests/e2e-pw/test-e2e-pw-ui.sh b/tests/e2e-pw/test-e2e-pw-ui.sh
new file mode 100755
index 00000000000..d035bb07cd7
--- /dev/null
+++ b/tests/e2e-pw/test-e2e-pw-ui.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env bash
+
+echo "π Running Playwright e2e tests in interactive UI mode.";
+echo "";
+echo "Open http://localhost:8077 in your browser to see the UI.";
+
+docker compose -f ./tests/e2e-pw/docker-compose.yml run --rm -it --service-ports playwright \
+ npx playwright test --config=tests/e2e-pw/playwright.config.ts --ui --ui-host=0.0.0.0 --ui-port=8077 "$@"
diff --git a/tests/e2e-pw/test-e2e-pw.sh b/tests/e2e-pw/test-e2e-pw.sh
new file mode 100755
index 00000000000..33a73f65946
--- /dev/null
+++ b/tests/e2e-pw/test-e2e-pw.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+echo "π Running Playwright e2e tests in default headless mode.";
+
+docker compose -f ./tests/e2e-pw/docker-compose.yml run --rm -it --service-ports playwright \
+ npx playwright test --config=tests/e2e-pw/playwright.config.ts "$@"
diff --git a/tests/e2e-pw/utils/helpers.ts b/tests/e2e-pw/utils/helpers.ts
new file mode 100644
index 00000000000..3112e819f68
--- /dev/null
+++ b/tests/e2e-pw/utils/helpers.ts
@@ -0,0 +1,80 @@
+/**
+ * External dependencies
+ */
+import path from 'path';
+import { test, Page, Browser, BrowserContext } from '@playwright/test';
+
+export const merchantStorageFile = path.resolve(
+ __dirname,
+ '../.auth/merchant.json'
+);
+
+export const customerStorageFile = path.resolve(
+ __dirname,
+ '../.auth/customer.json'
+);
+
+/**
+ * Logs in to the WordPress admin as a given user.
+ */
+export const wpAdminLogin = async (
+ page: Page,
+ user: { username: string; password: string }
+): void => {
+ await page.goto( `/wp-admin` );
+ await page.getByLabel( 'Username or Email Address' ).fill( user.username );
+ await page.getByLabel( 'Password', { exact: true } ).fill( user.password ); // Need exact match to avoid resolving "Show password" button.
+ await page.getByRole( 'button', { name: 'Log In' } ).click();
+};
+
+/**
+ * Sets the shopper as the authenticated user for a test suite (describe).
+ */
+export const useShopper = (): void => {
+ test.use( {
+ storageState: customerStorageFile,
+ } );
+};
+
+/**
+ * Sets the merchant as the authenticated user for a test suite (describe).
+ */
+export const useMerchant = (): void => {
+ test.use( {
+ storageState: merchantStorageFile,
+ } );
+};
+
+/**
+ * Returns the merchant authenticated page and context.
+ * Allows switching between merchant and shopper contexts within a single test.
+ */
+export const getMerchant = async (
+ browser: Browser
+): Promise< {
+ merchantPage: Page;
+ merchantContext: BrowserContext;
+} > => {
+ const merchantContext = await browser.newContext( {
+ storageState: merchantStorageFile,
+ } );
+ const merchantPage = await merchantContext.newPage();
+ return { merchantPage, merchantContext };
+};
+
+/**
+ * Returns the shopper authenticated page and context.
+ * Allows switching between merchant and shopper contexts within a single test.
+ */
+export const getShopper = async (
+ browser: Browser
+): Promise< {
+ shopperPage: Page;
+ shopperContext: BrowserContext;
+} > => {
+ const shopperContext = await browser.newContext( {
+ storageState: customerStorageFile,
+ } );
+ const shopperPage = await shopperContext.newPage();
+ return { shopperPage, shopperContext };
+};
diff --git a/tests/e2e-pw/utils/merchant.ts b/tests/e2e-pw/utils/merchant.ts
new file mode 100644
index 00000000000..0f358e24491
--- /dev/null
+++ b/tests/e2e-pw/utils/merchant.ts
@@ -0,0 +1,11 @@
+/**
+ * External dependencies
+ */
+import { Page } from 'playwright/test';
+
+export const goToOrder = async (
+ page: Page,
+ orderId: string
+): Promise< void > => {
+ await page.goto( `/wp-admin/post.php?post=${ orderId }&action=edit` );
+};
diff --git a/tests/e2e-pw/utils/shopper.ts b/tests/e2e-pw/utils/shopper.ts
new file mode 100644
index 00000000000..e3f59b9ae02
--- /dev/null
+++ b/tests/e2e-pw/utils/shopper.ts
@@ -0,0 +1,116 @@
+/**
+ * External dependencies
+ */
+import { Page } from 'playwright/test';
+
+/**
+ * Internal dependencies
+ */
+
+import { config, CustomerAddress } from '../config/default';
+
+export const fillBillingAddress = async (
+ page: Page,
+ billingAddress: CustomerAddress
+): Promise< void > => {
+ await page
+ .locator( '#billing_first_name' )
+ .fill( billingAddress.firstname );
+ await page.locator( '#billing_last_name' ).fill( billingAddress.lastname );
+ await page.locator( '#billing_company' ).fill( billingAddress.company );
+ await page
+ .locator( '#billing_country' )
+ .selectOption( billingAddress.country );
+ await page
+ .locator( '#billing_address_1' )
+ .fill( billingAddress.addressfirstline );
+ await page
+ .locator( '#billing_address_2' )
+ .fill( billingAddress.addresssecondline );
+ await page.locator( '#billing_city' ).fill( billingAddress.city );
+ await page.locator( '#billing_state' ).selectOption( billingAddress.state );
+ await page.locator( '#billing_postcode' ).fill( billingAddress.postcode );
+ await page.locator( '#billing_phone' ).fill( billingAddress.phone );
+ await page.locator( '#billing_email' ).fill( billingAddress.email );
+};
+
+export const placeOrder = async ( page: Page ): Promise< void > => {
+ await page.locator( '#place_order' ).click();
+};
+
+export const addCartProduct = async (
+ page: Page,
+ productId = 16 // Beanie
+): Promise< void > => {
+ await page.goto( `/shop/?add-to-cart=${ productId }` );
+};
+
+export const fillCardDetails = async (
+ page: Page,
+ card = config.cards.basic
+): Promise< void > => {
+ if (
+ await page.$(
+ '#payment .payment_method_woocommerce_payments .wcpay-upe-element'
+ )
+ ) {
+ const frameHandle = await page.waitForSelector(
+ '#payment .payment_method_woocommerce_payments .wcpay-upe-element iframe'
+ );
+
+ const stripeFrame = await frameHandle.contentFrame();
+
+ if ( ! stripeFrame ) return;
+
+ await stripeFrame.locator( '[name="number"]' ).fill( card.number );
+
+ await stripeFrame
+ .locator( '[name="expiry"]' )
+ .fill( card.expires.month + card.expires.year );
+
+ await stripeFrame.locator( '[name="cvc"]' ).fill( card.cvc );
+
+ const zip = stripeFrame.locator( '[name="postalCode"]' );
+
+ if ( await zip.isVisible() ) {
+ await zip.fill( '90210' );
+ }
+ } else {
+ const frameHandle = await page.waitForSelector(
+ '#payment #wcpay-card-element iframe[name^="__privateStripeFrame"]'
+ );
+ const stripeFrame = await frameHandle.contentFrame();
+
+ if ( ! stripeFrame ) return;
+
+ await stripeFrame.locator( '[name="cardnumber"]' ).fill( card.number );
+
+ await stripeFrame
+ .locator( '[name="exp-date"]' )
+ .fill( card.expires.month + card.expires.year );
+
+ await stripeFrame.locator( '[name="cvc"]' ).fill( card.cvc );
+ }
+};
+
+export const confirmCardAuthentication = async (
+ page: Page,
+ authorize = true
+): Promise< void > => {
+ // Stripe card input also uses __privateStripeFrame as a prefix, so need to make sure we wait for an iframe that
+ // appears at the top of the DOM.
+ const stripeFrame = page.frameLocator(
+ 'body>div>iframe[name^="__privateStripeFrame"]'
+ );
+ if ( ! stripeFrame ) return;
+
+ const challengeFrame = stripeFrame.frameLocator(
+ 'iframe[name="stripe-challenge-frame"]'
+ );
+ if ( ! challengeFrame ) return;
+
+ const button = challengeFrame.getByRole( 'button', {
+ name: authorize ? 'Complete' : 'Fail',
+ } );
+ await button.click();
+};
diff --git a/tests/e2e/env/docker-compose.yml b/tests/e2e/env/docker-compose.yml
index e42ce325921..c12211436e9 100644
--- a/tests/e2e/env/docker-compose.yml
+++ b/tests/e2e/env/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
wordpress:
build: ./wordpress-xdebug
@@ -19,6 +17,14 @@ services:
- ${WCP_ROOT}:/var/www/html/wp-content/plugins/woocommerce-payments
- ${E2E_ROOT}/deps/${DEV_TOOLS_DIR}:/var/www/html/wp-content/plugins/${DEV_TOOLS_DIR}
- ${E2E_ROOT}/deps/woocommerce-subscriptions:/var/www/html/wp-content/plugins/woocommerce-subscriptions
+ environment:
+ WORDPRESS_CONFIG_EXTRA: |
+ /* Dynamic WP hostname to allow Playwright container to access site via `host.docker.internal` hostname. */
+ /* `$$_` ensures `$_` is not escaped (https://github.com/docker-library/wordpress/pull/142#issuecomment-478561857) */
+ define('DOCKER_HOST', $$_SERVER['HTTP_X_ORIGINAL_HOST'] ?? $$_SERVER['HTTP_HOST'] ?? 'localhost');
+ define('DOCKER_REQUEST_URL', ( ! empty( $$_SERVER['HTTPS'] ) ? 'https://' : 'http://' ) . DOCKER_HOST);
+ define('WP_SITEURL', DOCKER_REQUEST_URL);
+ define('WP_HOME', DOCKER_REQUEST_URL);
db:
container_name: wcp_e2e_mysql
image: mariadb:10.5.8
diff --git a/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php b/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php
index e53b8383e8c..48c94e75869 100644
--- a/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php
+++ b/tests/unit/admin/test-class-wc-rest-payments-orders-controller.php
@@ -44,6 +44,16 @@ class WC_REST_Payments_Orders_Controller_Test extends WCPAY_UnitTestCase {
*/
private $order_service;
+ /**
+ * @var WC_Payments_Token_Service|MockObject
+ */
+ private $mock_token_service;
+
+ /**
+ * @var WC_Payments_Token_Service
+ */
+ private $original_token_service;
+
/**
* @var string
*/
@@ -68,19 +78,27 @@ public function set_up() {
$this->mock_api_client = $this->createMock( WC_Payments_API_Client::class );
$this->mock_gateway = $this->createMock( WC_Payment_Gateway_WCPay::class );
$this->mock_customer_service = $this->createMock( WC_Payments_Customer_Service::class );
+ $this->mock_token_service = $this->createMock( WC_Payments_Token_Service::class );
$this->order_service = $this->getMockBuilder( 'WC_Payments_Order_Service' )
->setConstructorArgs( [ $this->mock_api_client ] )
->setMethods( [ 'attach_intent_info_to_order' ] )
->getMock();
+ $this->original_token_service = WC_Payments::get_token_service();
+ WC_Payments::set_token_service( $this->mock_token_service );
$this->controller = new WC_REST_Payments_Orders_Controller(
$this->mock_api_client,
$this->mock_gateway,
$this->mock_customer_service,
- $this->order_service
+ $this->order_service,
);
}
+ public function tear_down() {
+ WC_Payments::set_token_service( $this->original_token_service );
+ parent::tear_down();
+ }
+
public function test_capture_terminal_payment_success() {
$order = $this->create_mock_order();
$mock_intent = WC_Helper_Intention::create_intention(
@@ -868,9 +886,6 @@ public function test_capture_authorization_not_found() {
$this->assertSame( 404, $data['status'] );
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_invalid_order_id() {
$request = new WP_REST_Request( 'POST' );
$request->set_body_params(
@@ -887,9 +902,6 @@ public function test_create_customer_invalid_order_id() {
$this->assertEquals( 404, $data['status'] );
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_from_order_guest_without_customer_id() {
$order = WC_Helper_Order::create_order( 0 );
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
@@ -956,9 +968,6 @@ function ( $argument ) {
);
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_from_order_guest_with_customer_id() {
$order = WC_Helper_Order::create_order( 0 );
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
@@ -1009,9 +1018,6 @@ function ( $argument ) {
$this->assertSame( 'cus_guest', $result_order->get_meta( '_stripe_customer_id' ) );
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_from_order_non_guest_with_customer_id() {
$order = WC_Helper_Order::create_order();
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
@@ -1053,9 +1059,6 @@ public function test_create_customer_from_order_non_guest_with_customer_id() {
$this->assertSame( 'cus_exist', $result_order->get_meta( '_stripe_customer_id' ) );
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_from_order_with_invalid_status() {
$order = WC_Helper_Order::create_order();
$order->set_status( Order_Status::COMPLETED );
@@ -1088,9 +1091,6 @@ public function test_create_customer_from_order_with_invalid_status() {
$this->assertEquals( 400, $data['status'] );
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_from_order_non_guest_with_customer_id_from_order_meta() {
$order = WC_Helper_Order::create_order();
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
@@ -1133,9 +1133,6 @@ public function test_create_customer_from_order_non_guest_with_customer_id_from_
$this->assertSame( 'cus_exist', $result_order->get_meta( '_stripe_customer_id' ) );
}
- /**
- * @expectedDeprecated create_customer
- */
public function test_create_customer_from_order_non_guest_without_customer_id() {
$order = WC_Helper_Order::create_order();
$customer_data = WC_Payments_Customer_Service::map_customer_data( $order );
@@ -1737,4 +1734,268 @@ private function create_charge_object() {
return new WC_Payments_API_Charge( $this->mock_charge_id, 1500, $created );
}
+
+ public function test_capture_terminal_payment_with_subscription_product_sets_generated_card_on_user() {
+ $order = $this->create_mock_order();
+
+ $subscription = new WC_Subscription();
+ $subscription->set_parent( $order );
+ $this->mock_wcs_order_contains_subscription( true );
+ $this->mock_wcs_get_subscriptions_for_order( [ $subscription ] );
+ $this->mock_wcs_is_manual_renewal_required( false );
+
+ $generated_card_id = 'pm_generatedCardId';
+
+ $mock_intent = WC_Helper_Intention::create_intention(
+ [
+ 'charge' => [
+ 'payment_method_details' => [
+ 'type' => 'card_present',
+ 'card_present' => [
+ 'generated_card' => $generated_card_id,
+ ],
+ ],
+ ],
+ 'metadata' => [
+ 'order_id' => $order->get_id(),
+ ],
+ 'status' => Intent_Status::REQUIRES_CAPTURE,
+ ]
+ );
+
+ $request = $this->mock_wcpay_request( Get_Intention::class, 1, $this->mock_intent_id );
+
+ $request->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( $mock_intent );
+
+ $this->mock_gateway
+ ->expects( $this->once() )
+ ->method( 'capture_charge' )
+ ->with( $this->isInstanceOf( WC_Order::class ) )
+ ->willReturn(
+ [
+ 'status' => Intent_Status::SUCCEEDED,
+ 'id' => $this->mock_intent_id,
+ ]
+ );
+
+ $this->order_service
+ ->expects( $this->once() )
+ ->method( 'attach_intent_info_to_order' )
+ ->with(
+ $this->isInstanceOf( WC_Order::class ),
+ $mock_intent,
+ );
+
+ $this->mock_token_service
+ ->expects( $this->once() )
+ ->method( 'add_payment_method_to_user' )
+ ->with(
+ $generated_card_id,
+ $this->isInstanceOf( WP_User::class )
+ );
+
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'order_id' => $order->get_id(),
+ 'payment_intent_id' => $this->mock_intent_id,
+ ]
+ );
+
+ $response = $this->controller->capture_terminal_payment( $request );
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEquals( 'woocommerce_payments', $subscription->get_payment_method() );
+ }
+
+ /**
+ * @dataProvider provider_capture_terminal_payment_with_subscription_product_sets_manual_renewal
+ */
+ public function test_capture_terminal_payment_with_subscription_product_sets_manual_renewal( bool $manual_renewal_required_setting, bool $initial_subscription_manual_renewal, bool $expected_subscription_manual_renewal ) {
+ $order = $this->create_mock_order();
+
+ $subscription = new WC_Subscription();
+ $subscription->set_parent( $order );
+ $subscription->set_requires_manual_renewal( $initial_subscription_manual_renewal );
+ $this->mock_wcs_order_contains_subscription( true );
+ $this->mock_wcs_get_subscriptions_for_order( [ $subscription ] );
+ $this->mock_wcs_is_manual_renewal_required( $manual_renewal_required_setting );
+
+ $generated_card_id = 'pm_generatedCardId';
+
+ $mock_intent = WC_Helper_Intention::create_intention(
+ [
+ 'charge' => [
+ 'payment_method_details' => [
+ 'type' => 'card_present',
+ 'card_present' => [
+ 'generated_card' => $generated_card_id,
+ ],
+ ],
+ ],
+ 'metadata' => [
+ 'order_id' => $order->get_id(),
+ ],
+ 'status' => Intent_Status::REQUIRES_CAPTURE,
+ ]
+ );
+
+ $request = $this->mock_wcpay_request( Get_Intention::class, 1, $this->mock_intent_id );
+
+ $request->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( $mock_intent );
+
+ $this->mock_gateway
+ ->expects( $this->once() )
+ ->method( 'capture_charge' )
+ ->with( $this->isInstanceOf( WC_Order::class ) )
+ ->willReturn(
+ [
+ 'status' => Intent_Status::SUCCEEDED,
+ 'id' => $this->mock_intent_id,
+ ]
+ );
+
+ $this->order_service
+ ->expects( $this->once() )
+ ->method( 'attach_intent_info_to_order' )
+ ->with(
+ $this->isInstanceOf( WC_Order::class ),
+ $mock_intent,
+ );
+
+ $this->mock_token_service
+ ->expects( $this->once() )
+ ->method( 'add_payment_method_to_user' )
+ ->with(
+ $generated_card_id,
+ $this->isInstanceOf( WP_User::class )
+ );
+
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'order_id' => $order->get_id(),
+ 'payment_intent_id' => $this->mock_intent_id,
+ ]
+ );
+
+ $response = $this->controller->capture_terminal_payment( $request );
+ $this->assertEquals( 200, $response->get_status() );
+ $this->assertEquals( $expected_subscription_manual_renewal, $subscription->is_manual() );
+ }
+
+ /**
+ * bool $manual_renewal_required_setting
+ * bool $initial_subscription_manual_renewal
+ * bool $expected_subscription_manual_renewal
+ */
+ public function provider_capture_terminal_payment_with_subscription_product_sets_manual_renewal(): array {
+ return [
+ [ true, true, true ],
+ [ false, true, false ],
+ [ true, false, false ], // even if manual_renewal_required, we won't set it to manual_renewal if it started as automatic.
+ [ false, false, false ],
+ ];
+ }
+
+ /**
+ * Cleanup after all tests.
+ */
+ public static function tear_down_after_class() {
+ WC_Subscriptions::set_wcs_order_contains_subscription( null );
+ WC_Subscriptions::set_wcs_get_subscriptions_for_order( null );
+ WC_Subscriptions::set_wcs_is_manual_renewal_required( null );
+ parent::tear_down_after_class();
+ }
+
+ private function mock_wcs_order_contains_subscription( $value ) {
+ WC_Subscriptions::set_wcs_order_contains_subscription(
+ function ( $order ) use ( $value ) {
+ return $value;
+ }
+ );
+ }
+
+ private function mock_wcs_get_subscriptions_for_order( $value ) {
+ WC_Subscriptions::set_wcs_get_subscriptions_for_order(
+ function ( $order ) use ( $value ) {
+ return $value;
+ }
+ );
+ }
+
+ private function mock_wcs_is_manual_renewal_required( $value ) {
+ WC_Subscriptions::set_wcs_is_manual_renewal_required(
+ function () use ( $value ) {
+ return $value;
+ }
+ );
+ }
+
+ public function test_capture_terminal_payment_with_subscription_product_returns_success_even_if_no_generated_card() {
+ $order = $this->create_mock_order();
+
+ $subscription = new WC_Subscription();
+ $subscription->set_parent( $order );
+ $this->mock_wcs_order_contains_subscription( true );
+ $this->mock_wcs_get_subscriptions_for_order( [ $subscription ] );
+
+ $mock_intent = WC_Helper_Intention::create_intention(
+ [
+ 'charge' => [
+ 'payment_method_details' => [
+ 'type' => 'card_present',
+ 'card_present' => [],
+ ],
+ ],
+ 'metadata' => [
+ 'order_id' => $order->get_id(),
+ ],
+ 'status' => Intent_Status::REQUIRES_CAPTURE,
+ ]
+ );
+
+ $request = $this->mock_wcpay_request( Get_Intention::class, 1, $this->mock_intent_id );
+
+ $request->expects( $this->once() )
+ ->method( 'format_response' )
+ ->willReturn( $mock_intent );
+
+ $this->mock_gateway
+ ->expects( $this->once() )
+ ->method( 'capture_charge' )
+ ->with( $this->isInstanceOf( WC_Order::class ) )
+ ->willReturn(
+ [
+ 'status' => Intent_Status::SUCCEEDED,
+ 'id' => $this->mock_intent_id,
+ ]
+ );
+
+ $this->order_service
+ ->expects( $this->once() )
+ ->method( 'attach_intent_info_to_order' )
+ ->with(
+ $this->isInstanceOf( WC_Order::class ),
+ $mock_intent,
+ );
+
+ $this->mock_token_service
+ ->expects( $this->never() )
+ ->method( 'add_payment_method_to_user' );
+
+ $request = new WP_REST_Request( 'POST' );
+ $request->set_body_params(
+ [
+ 'order_id' => $order->get_id(),
+ 'payment_intent_id' => $this->mock_intent_id,
+ ]
+ );
+
+ $response = $this->controller->capture_terminal_payment( $request );
+ $this->assertSame( 200, $response->status );
+ }
}
diff --git a/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php b/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php
index 79486949b83..6bfaff2b816 100644
--- a/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php
+++ b/tests/unit/admin/test-class-wc-rest-payments-settings-controller.php
@@ -12,6 +12,7 @@
use WCPay\Constants\Payment_Method;
use WCPay\Database_Cache;
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Payment_Methods\Eps_Payment_Method;
use WCPay\Payment_Methods\CC_Payment_Method;
use WCPay\Payment_Methods\Bancontact_Payment_Method;
@@ -60,6 +61,14 @@ class WC_REST_Payments_Settings_Controller_Test extends WCPAY_UnitTestCase {
* @var WC_Payments_Account|MockObject
*/
private $mock_wcpay_account;
+
+ /**
+ * Mock Duplicate_Payment_Prevention_Service.
+ *
+ * @var Duplicates_Detection_Service|MockObject
+ */
+ private $mock_duplicates_detection_service;
+
/**
* @var Database_Cache|MockObject
*/
@@ -111,17 +120,18 @@ public function set_up() {
->disableOriginalConstructor()
->getMock();
- $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
- $this->mock_db_cache = $this->createMock( Database_Cache::class );
- $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
- $customer_service = new WC_Payments_Customer_Service( $this->mock_api_client, $this->mock_wcpay_account, $this->mock_db_cache, $this->mock_session_service );
- $token_service = new WC_Payments_Token_Service( $this->mock_api_client, $customer_service );
- $order_service = new WC_Payments_Order_Service( $this->mock_api_client );
- $action_scheduler_service = new WC_Payments_Action_Scheduler_Service( $this->mock_api_client, $order_service );
- $mock_rate_limiter = $this->createMock( Session_Rate_Limiter::class );
- $mock_dpps = $this->createMock( Duplicate_Payment_Prevention_Service::class );
- $this->mock_localization_service = $this->createMock( WC_Payments_Localization_Service::class );
- $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class );
+ $this->mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
+ $this->mock_db_cache = $this->createMock( Database_Cache::class );
+ $this->mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
+ $customer_service = new WC_Payments_Customer_Service( $this->mock_api_client, $this->mock_wcpay_account, $this->mock_db_cache, $this->mock_session_service );
+ $token_service = new WC_Payments_Token_Service( $this->mock_api_client, $customer_service );
+ $order_service = new WC_Payments_Order_Service( $this->mock_api_client );
+ $action_scheduler_service = new WC_Payments_Action_Scheduler_Service( $this->mock_api_client, $order_service );
+ $mock_rate_limiter = $this->createMock( Session_Rate_Limiter::class );
+ $mock_dpps = $this->createMock( Duplicate_Payment_Prevention_Service::class );
+ $this->mock_localization_service = $this->createMock( WC_Payments_Localization_Service::class );
+ $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class );
+ $this->mock_duplicates_detection_service = $this->createMock( Duplicates_Detection_Service::class );
$mock_payment_methods = [];
$payment_method_classes = [
@@ -165,7 +175,8 @@ public function set_up() {
$order_service,
$mock_dpps,
$this->mock_localization_service,
- $this->mock_fraud_service
+ $this->mock_fraud_service,
+ $this->mock_duplicates_detection_service
);
$this->controller = new WC_REST_Payments_Settings_Controller( $this->mock_api_client, $this->gateway, $this->mock_wcpay_account );
diff --git a/tests/unit/admin/test-class-wc-rest-payments-survey-controller.php b/tests/unit/admin/test-class-wc-rest-payments-survey-controller.php
new file mode 100644
index 00000000000..cf0480ec993
--- /dev/null
+++ b/tests/unit/admin/test-class-wc-rest-payments-survey-controller.php
@@ -0,0 +1,122 @@
+http_client_stub = $this->getMockBuilder( WC_Payments_Http::class )->disableOriginalConstructor()->setMethods( [ 'remote_request' ] )->getMock();
+ $this->controller = new WC_REST_Payments_Survey_Controller( $this->http_client_stub );
+ }
+
+ public function test_empty_request_returns_400_status_code() {
+ $request = new WP_REST_Request( 'POST', self::ROUTE . '/payments-overview' );
+
+ $response = $this->controller->submit_payments_overview_survey( $request );
+
+ $this->assertEquals( 400, $response->get_status() );
+ }
+
+ public function test_empty_rating_returns_400_status_code() {
+ $request = new WP_REST_Request( 'POST', self::ROUTE . '/payments-overview' );
+ $request->set_body_params(
+ [
+ 'comments' => 'test comment',
+ ]
+ );
+
+ $response = $this->controller->submit_payments_overview_survey( $request );
+
+ $this->assertEquals( 400, $response->get_status() );
+ }
+
+
+ public function test_valid_request_forwards_data_to_jetpack() {
+ $request_url = WC_Payments_API_Client::ENDPOINT_BASE . '/marketing/survey';
+
+ $this->http_client_stub
+ ->expects( $this->any() )
+ ->method( 'remote_request' )
+ ->with(
+ // Check the request argument URL is the same.
+ $this->callback(
+ function ( $argument ) use ( $request_url ) {
+ return $request_url === $argument['url'];
+ }
+ ),
+ $this->logicalAnd(
+ $this->callback(
+ function ( $argument ) {
+ $json_body = json_decode( $argument, true );
+ return 'wcpay-payment-activity' === $json_body['survey_id'];
+ }
+ ),
+ $this->callback(
+ function ( $argument ) {
+ $json_body = json_decode( $argument, true );
+ return 'happy' === $json_body['survey_responses']['rating'];
+ }
+ ),
+ $this->callback(
+ function ( $argument ) {
+ $json_body = json_decode( $argument, true );
+ return 'test comment' === $json_body['survey_responses']['comments']['text'];
+ }
+ ),
+ ),
+ $this->isTrue(),
+ $this->isTrue(),
+ )
+ ->willReturn(
+ [
+ 'body' => '{"err": ""}',
+ 'response' => [ 'code' => 200 ],
+ ]
+ );
+
+ $request = new WP_REST_Request( 'POST', self::ROUTE . '/payments-overview' );
+ $request->set_body_params(
+ [
+ 'rating' => 'happy',
+ 'comments' => 'test comment',
+ ]
+ );
+
+ $response = $this->controller->submit_payments_overview_survey( $request );
+
+ $this->assertEquals( 200, $response->get_status() );
+ }
+}
diff --git a/tests/unit/admin/test-class-wc-rest-payments-tos-controller.php b/tests/unit/admin/test-class-wc-rest-payments-tos-controller.php
index 9597390a9a0..f2f5b4274f7 100644
--- a/tests/unit/admin/test-class-wc-rest-payments-tos-controller.php
+++ b/tests/unit/admin/test-class-wc-rest-payments-tos-controller.php
@@ -9,6 +9,7 @@
use WCPay\Core\Server\Request\Add_Account_Tos_Agreement;
use WCPay\Database_Cache;
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Payment_Methods\CC_Payment_Method;
use WCPay\Session_Rate_Limiter;
@@ -55,16 +56,17 @@ public function set_up() {
->disableOriginalConstructor()
->getMock();
- $mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
- $mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class );
- $mock_db_cache = $this->createMock( Database_Cache::class );
- $mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
- $customer_service = new WC_Payments_Customer_Service( $mock_api_client, $mock_wcpay_account, $mock_db_cache, $mock_session_service );
- $token_service = new WC_Payments_Token_Service( $mock_api_client, $customer_service );
- $order_service = new WC_Payments_Order_Service( $this->createMock( WC_Payments_API_Client::class ) );
- $action_scheduler_service = new WC_Payments_Action_Scheduler_Service( $mock_api_client, $order_service );
- $mock_dpps = $this->createMock( Duplicate_Payment_Prevention_Service::class );
- $mock_payment_method = $this->createMock( CC_Payment_Method::class );
+ $mock_wcpay_account = $this->createMock( WC_Payments_Account::class );
+ $mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class );
+ $mock_db_cache = $this->createMock( Database_Cache::class );
+ $mock_session_service = $this->createMock( WC_Payments_Session_Service::class );
+ $customer_service = new WC_Payments_Customer_Service( $mock_api_client, $mock_wcpay_account, $mock_db_cache, $mock_session_service );
+ $token_service = new WC_Payments_Token_Service( $mock_api_client, $customer_service );
+ $order_service = new WC_Payments_Order_Service( $this->createMock( WC_Payments_API_Client::class ) );
+ $action_scheduler_service = new WC_Payments_Action_Scheduler_Service( $mock_api_client, $order_service );
+ $mock_dpps = $this->createMock( Duplicate_Payment_Prevention_Service::class );
+ $mock_payment_method = $this->createMock( CC_Payment_Method::class );
+ $mock_duplicates_detection_service = $this->createMock( Duplicates_Detection_Service::class );
$this->gateway = new WC_Payment_Gateway_WCPay(
$mock_api_client,
@@ -78,7 +80,8 @@ public function set_up() {
$order_service,
$mock_dpps,
$this->createMock( WC_Payments_Localization_Service::class ),
- $mock_fraud_service
+ $mock_fraud_service,
+ $mock_duplicates_detection_service
);
$this->controller = new WC_REST_Payments_Tos_Controller( $mock_api_client, $this->gateway, $mock_wcpay_account );
diff --git a/tests/unit/bootstrap.php b/tests/unit/bootstrap.php
index a73e0120e4a..4865390b965 100755
--- a/tests/unit/bootstrap.php
+++ b/tests/unit/bootstrap.php
@@ -87,6 +87,7 @@ function () {
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-terminal-locations-controller.php';
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-tos-controller.php';
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-settings-controller.php';
+ require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-survey-controller.php';
require_once $_plugin_dir . 'includes/admin/tracks/class-tracker.php';
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-reader-controller.php';
require_once $_plugin_dir . 'includes/admin/class-wc-rest-payments-files-controller.php';
diff --git a/tests/unit/core/server/request/test-class-list-fraud-outcome-transactions-request.php b/tests/unit/core/server/request/test-class-list-fraud-outcome-transactions-request.php
index 2e08bf04425..9a77afb8537 100644
--- a/tests/unit/core/server/request/test-class-list-fraud-outcome-transactions-request.php
+++ b/tests/unit/core/server/request/test-class-list-fraud-outcome-transactions-request.php
@@ -6,6 +6,7 @@
*/
use PHPUnit\Framework\MockObject\MockObject;
+use WCPay\Core\Exceptions\Server\Request\Invalid_Request_Parameter_Exception;
use WCPay\Core\Server\Request\List_Fraud_Outcome_Transactions;
/**
@@ -68,6 +69,7 @@ public function test_list_fraud_outcome_transactions_request() {
$this->assertSame( 'GET', $request->get_method() );
$this->assertSame( WC_Payments_API_Client::FRAUD_OUTCOMES_API . '/status/' . $status, $request->get_api() );
}
+
public function test_list_fraud_outcome_transactions_request_using_from_rest_request_function() {
$page = 2;
$page_size = 50;
@@ -569,4 +571,29 @@ public function test_list_fraud_outcome_transactions_request_filters_out_non_blo
$this->assertEquals( $expected, $result );
}
+
+ /**
+ * Checks to see if the get_api method throws an exception if an invalid status is passed.
+ *
+ * @param ?string $status The status to check.
+ *
+ * @return void
+ *
+ * @dataProvider provider_get_api_exception_on_invalid_status
+ */
+ public function test_get_api_exception_on_invalid_status( $status ): void {
+ $request = new List_Fraud_Outcome_Transactions( $this->mock_api_client, $this->mock_wc_payments_http_client );
+ $request->set_status( $status );
+
+ $status = $status ?? 'null';
+
+ $this->expectException( Invalid_Request_Parameter_Exception::class );
+ $this->expectExceptionMessage( "Invalid fraud outcome status provided: $status" );
+
+ $request->get_api();
+ }
+
+ public function provider_get_api_exception_on_invalid_status(): array {
+ return [ [ 'invalid' ], [ null ] ];
+ }
}
diff --git a/tests/unit/duplicate-detection/class-test-gateway.php b/tests/unit/duplicate-detection/class-test-gateway.php
new file mode 100644
index 00000000000..99735fb8a7a
--- /dev/null
+++ b/tests/unit/duplicate-detection/class-test-gateway.php
@@ -0,0 +1,27 @@
+form_fields = [
+ 'payment_request' => [
+ 'default' => 'no',
+ ],
+ ];
+ }
+}
diff --git a/tests/unit/duplicate-detection/test-class-duplicates-detection-service.php b/tests/unit/duplicate-detection/test-class-duplicates-detection-service.php
new file mode 100644
index 00000000000..2dd028d4203
--- /dev/null
+++ b/tests/unit/duplicate-detection/test-class-duplicates-detection-service.php
@@ -0,0 +1,129 @@
+service = new Duplicates_Detection_Service();
+
+ $this->woopayments_gateway = new Test_Gateway();
+ $this->gateway_from_another_plugin = new Test_Gateway();
+
+ $this->cached_gateways = WC()->payment_gateways()->payment_gateways;
+ WC()->payment_gateways()->payment_gateways = [ $this->woopayments_gateway, $this->gateway_from_another_plugin ];
+ }
+
+ public function tear_down() {
+ WC()->payment_gateways()->payment_gateways = $this->cached_gateways;
+ }
+
+ public function test_two_cc_both_enabled() {
+ $this->set_duplicates( 'card', 'yes', 'yes' );
+
+ $result = $this->service->find_duplicates();
+
+ $this->assertCount( 1, $result );
+ $this->assertEquals( 'card', $result[0] );
+ }
+
+ public function test_two_cc_one_enabled() {
+ $this->set_duplicates( CC_Payment_Method::PAYMENT_METHOD_STRIPE_ID, 'yes', 'no' );
+
+ $result = $this->service->find_duplicates();
+
+ $this->assertEmpty( $result );
+ }
+
+ public function test_two_apms_enabled() {
+ $this->set_duplicates( Giropay_Payment_Method::PAYMENT_METHOD_STRIPE_ID, 'yes', 'yes' );
+
+ $result = $this->service->find_duplicates();
+
+ $this->assertCount( 1, $result );
+ $this->assertEquals( Giropay_Payment_Method::PAYMENT_METHOD_STRIPE_ID, $result[0] );
+ }
+
+ public function test_two_bnpls_enabled() {
+ $this->set_duplicates( Klarna_Payment_Method::PAYMENT_METHOD_STRIPE_ID, 'yes', 'yes' );
+
+ $result = $this->service->find_duplicates();
+
+ $this->assertCount( 1, $result );
+ $this->assertEquals( Klarna_Payment_Method::PAYMENT_METHOD_STRIPE_ID, $result[0] );
+ }
+
+ public function test_two_prbs_enabled() {
+ $this->set_duplicates( CC_Payment_Method::PAYMENT_METHOD_STRIPE_ID, 'yes', 'yes' );
+ $this->woopayments_gateway->update_option( 'payment_request', 'yes' );
+ $this->woopayments_gateway->enabled = 'yes';
+ $this->gateway_from_another_plugin->id = 'apple_pay';
+
+ $result = $this->service->find_duplicates();
+
+ $this->assertEquals( 'apple_pay_google_pay', $result[0] );
+ }
+
+ public function test_duplicate_not_enabled_in_woopayments() {
+ $this->set_duplicates( CC_Payment_Method::PAYMENT_METHOD_STRIPE_ID, 'yes', 'yes' );
+ $this->woopayments_gateway->id = 'not_woopayments_card';
+
+ $result = $this->service->find_duplicates();
+
+ $this->assertEmpty( $result );
+ }
+
+ private function set_duplicates( $id, $woopayments_gateway_enabled, $gateway_from_another_plugin_enabled ) {
+ $this->woopayments_gateway->enabled = $woopayments_gateway_enabled;
+ $this->gateway_from_another_plugin->enabled = $gateway_from_another_plugin_enabled;
+
+ if ( 'card' === $id ) {
+ $this->woopayments_gateway->id = 'woocommerce_payments';
+ } else {
+ $this->woopayments_gateway->id = 'woocommerce_payments_' . $id;
+ }
+ $this->gateway_from_another_plugin->id = 'another_plugin_' . $id;
+ }
+}
diff --git a/tests/unit/helpers/class-wc-helper-subscriptions.php b/tests/unit/helpers/class-wc-helper-subscriptions.php
index 3d361a6446d..a2c99a77071 100644
--- a/tests/unit/helpers/class-wc-helper-subscriptions.php
+++ b/tests/unit/helpers/class-wc-helper-subscriptions.php
@@ -90,6 +90,13 @@ function wcs_get_orders_with_meta_query( $args ) {
return ( WC_Subscriptions::$wcs_get_orders_with_meta_query )( $args );
}
+function wcs_is_manual_renewal_required() {
+ if ( ! WC_Subscriptions::$wcs_is_manual_renewal_required ) {
+ return;
+ }
+ return ( WC_Subscriptions::$wcs_is_manual_renewal_required )();
+}
+
/**
* Class WC_Subscriptions.
*
@@ -187,6 +194,13 @@ class WC_Subscriptions {
*/
public static $wcs_order_contains_renewal = null;
+ /**
+ * wcs_is_manual_renewal_required mock.
+ *
+ * @var function
+ */
+ public static $wcs_is_manual_renewal_required = null;
+
public static function set_wcs_order_contains_subscription( $function ) {
self::$wcs_order_contains_subscription = $function;
}
@@ -234,4 +248,8 @@ public static function wcs_order_contains_renewal( $function ) {
public static function is_duplicate_site() {
return false;
}
+
+ public static function set_wcs_is_manual_renewal_required( $function ) {
+ self::$wcs_is_manual_renewal_required = $function;
+ }
}
diff --git a/tests/unit/payment-methods/test-class-upe-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-payment-gateway.php
index 8260f8707b5..137a283a944 100644
--- a/tests/unit/payment-methods/test-class-upe-payment-gateway.php
+++ b/tests/unit/payment-methods/test-class-upe-payment-gateway.php
@@ -33,6 +33,7 @@
use WC_Payments_Localization_Service;
use WCPay\Core\Server\Request\Create_And_Confirm_Intention;
use WCPay\Database_Cache;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Internal\Service\Level3Service;
use WCPay\Internal\Service\OrderService;
@@ -170,6 +171,13 @@ class UPE_Payment_Gateway_Test extends WCPAY_UnitTestCase {
*/
private $mock_fraud_service;
+ /**
+ * Mock Duplicates Detection Service.
+ *
+ * @var Duplicates_Detection_Service
+ */
+ private $mock_duplicates_detection_service;
+
/**
* Pre-test setup
*/
@@ -230,8 +238,9 @@ public function set_up() {
$this->mock_dpps = $this->createMock( Duplicate_Payment_Prevention_Service::class );
- $this->mock_localization_service = $this->createMock( WC_Payments_Localization_Service::class );
- $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class );
+ $this->mock_localization_service = $this->createMock( WC_Payments_Localization_Service::class );
+ $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class );
+ $this->mock_duplicates_detection_service = $this->createMock( Duplicates_Detection_Service::class );
$this->mock_payment_methods = [];
$payment_method_classes = [
@@ -294,6 +303,7 @@ public function set_up() {
$this->mock_dpps,
$this->mock_localization_service,
$this->mock_fraud_service,
+ $this->mock_duplicates_detection_service,
]
)
->setMethods(
@@ -961,7 +971,8 @@ public function test_get_upe_available_payment_methods( $payment_methods, $expec
$this->mock_order_service,
$this->mock_dpps,
$this->mock_localization_service,
- $this->mock_fraud_service
+ $this->mock_fraud_service,
+ $this->mock_duplicates_detection_service
);
$this->assertEquals( $expected_result, $gateway->get_upe_available_payment_methods() );
diff --git a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php
index 22653a4cba9..8ac1db139a5 100644
--- a/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php
+++ b/tests/unit/payment-methods/test-class-upe-split-payment-gateway.php
@@ -35,6 +35,7 @@
use WC_Payments_Localization_Service;
use WCPay\Core\Server\Request\Create_And_Confirm_Intention;
use WCPay\Database_Cache;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Internal\Service\Level3Service;
use WCPay\Internal\Service\OrderService;
/**
@@ -158,6 +159,13 @@ class UPE_Split_Payment_Gateway_Test extends WCPAY_UnitTestCase {
*/
private $mock_fraud_service;
+ /**
+ * Mock Duplicates Detection Service.
+ *
+ * @var Duplicates_Detection_Service
+ */
+ private $mock_duplicates_detection_service;
+
/**
* Mapping for payment ID to payment method.
*
@@ -247,8 +255,9 @@ public function set_up() {
$this->mock_dpps = $this->createMock( Duplicate_Payment_Prevention_Service::class );
- $this->mock_localization_service = $this->createMock( WC_Payments_Localization_Service::class );
- $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class );
+ $this->mock_localization_service = $this->createMock( WC_Payments_Localization_Service::class );
+ $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class );
+ $this->mock_duplicates_detection_service = $this->createMock( Duplicates_Detection_Service::class );
// Arrange: Define a $_POST array which includes the payment method,
// so that get_payment_method_from_request() does not throw error.
@@ -281,6 +290,7 @@ public function set_up() {
$this->mock_dpps,
$this->mock_localization_service,
$this->mock_fraud_service,
+ $this->mock_duplicates_detection_service,
]
)
->setMethods(
@@ -1060,6 +1070,7 @@ public function test_get_payment_methods_with_request_context() {
$this->mock_dpps,
$this->mock_localization_service,
$this->mock_fraud_service,
+ $this->mock_duplicates_detection_service,
]
)
->setMethods( [ 'get_payment_methods_from_gateway_id' ] )
@@ -1105,6 +1116,7 @@ public function test_get_payment_methods_without_request_context() {
$this->mock_dpps,
$this->mock_localization_service,
$this->mock_fraud_service,
+ $this->mock_duplicates_detection_service,
]
)
->setMethods( [ 'get_payment_methods_from_gateway_id' ] )
@@ -1149,6 +1161,7 @@ public function test_get_payment_methods_without_request_context_or_token() {
$this->mock_dpps,
$this->mock_localization_service,
$this->mock_fraud_service,
+ $this->mock_duplicates_detection_service,
]
)
->setMethods(
@@ -1202,6 +1215,7 @@ public function test_get_payment_methods_from_gateway_id_upe() {
$this->mock_dpps,
$this->mock_localization_service,
$this->mock_fraud_service,
+ $this->mock_duplicates_detection_service,
]
)
->onlyMethods(
diff --git a/tests/unit/test-class-compatibility-service.php b/tests/unit/test-class-compatibility-service.php
index b293cb5a2e2..0d2747ed7c9 100644
--- a/tests/unit/test-class-compatibility-service.php
+++ b/tests/unit/test-class-compatibility-service.php
@@ -176,11 +176,15 @@ public function test_add_compatibility_onboarding_data() {
private function get_mock_compatibility_data( array $args = [] ): array {
return array_merge(
[
- 'woopayments_version' => WCPAY_VERSION_NUMBER,
- 'woocommerce_version' => WC_VERSION,
- 'blog_theme' => $this->stylesheet,
- 'active_plugins' => $this->active_plugins,
- 'post_types_count' => $this->post_types_count,
+ 'woopayments_version' => WCPAY_VERSION_NUMBER,
+ 'woocommerce_version' => WC_VERSION,
+ 'woocommerce_permalinks' => get_option( 'woocommerce_permalinks' ),
+ 'woocommerce_shop' => get_permalink( wc_get_page_id( 'shop' ) ),
+ 'woocommerce_cart' => get_permalink( wc_get_page_id( 'cart' ) ),
+ 'woocommerce_checkout' => get_permalink( wc_get_page_id( 'checkout' ) ),
+ 'blog_theme' => $this->stylesheet,
+ 'active_plugins' => $this->active_plugins,
+ 'post_types_count' => $this->post_types_count,
],
$args
);
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-payment-types.php b/tests/unit/test-class-wc-payment-gateway-wcpay-payment-types.php
index 8caaadb9ea4..c4ae5f729ee 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay-payment-types.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay-payment-types.php
@@ -9,6 +9,7 @@
use WCPay\Core\Server\Request\Create_And_Confirm_Intention;
use WCPay\Constants\Payment_Method;
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Session_Rate_Limiter;
use WCPay\Fraud_Prevention\Fraud_Prevention_Service;
use WCPay\Payment_Methods\CC_Payment_Method;
@@ -153,6 +154,7 @@ public function set_up() {
$mock_dpps,
$this->createMock( WC_Payments_Localization_Service::class ),
$this->createMock( WC_Payments_Fraud_Service::class ),
+ $this->createMock( Duplicates_Detection_Service::class ),
]
)
->setMethods(
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php
index 79f6cd75d9b..3962272376d 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay-process-payment.php
@@ -15,6 +15,7 @@
use WCPay\Exceptions\Connection_Exception;
use WCPay\Session_Rate_Limiter;
use WCPay\Constants\Payment_Method;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Payment_Methods\CC_Payment_Method;
// Need to use WC_Mock_Data_Store.
@@ -167,6 +168,7 @@ public function set_up() {
$this->mock_dpps,
$this->createMock( WC_Payments_Localization_Service::class ),
$this->createMock( WC_Payments_Fraud_Service::class ),
+ $this->createMock( Duplicates_Detection_Service::class ),
]
)
->setMethods(
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-process-refund.php b/tests/unit/test-class-wc-payment-gateway-wcpay-process-refund.php
index 49746ef3ca3..24f06d99933 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay-process-refund.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay-process-refund.php
@@ -12,6 +12,7 @@
use WCPay\Core\Server\Request\Refund_Charge;
use WCPay\Core\Server\Response;
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Exceptions\API_Exception;
use WCPay\Payment_Methods\CC_Payment_Method;
use WCPay\Session_Rate_Limiter;
@@ -104,7 +105,8 @@ public function set_up() {
$this->mock_order_service,
$mock_dpps,
$this->createMock( WC_Payments_Localization_Service::class ),
- $this->createMock( WC_Payments_Fraud_Service::class )
+ $this->createMock( WC_Payments_Fraud_Service::class ),
+ $this->createMock( Duplicates_Detection_Service::class )
);
}
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-payment-method-order-note.php b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-payment-method-order-note.php
index 9715d61a193..fd4352ac0b2 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-payment-method-order-note.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-payment-method-order-note.php
@@ -6,6 +6,7 @@
*/
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Payment_Methods\CC_Payment_Method;
use WCPay\Session_Rate_Limiter;
@@ -139,7 +140,8 @@ public function set_up() {
$this->mock_order_service,
$mock_dpps,
$this->createMock( WC_Payments_Localization_Service::class ),
- $this->createMock( WC_Payments_Fraud_Service::class )
+ $this->createMock( WC_Payments_Fraud_Service::class ),
+ $this->createMock( Duplicates_Detection_Service::class ),
);
$this->wcpay_gateway->init_hooks();
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php
index 434bbd971fd..622e7cbe1d9 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions-process-payment.php
@@ -10,6 +10,7 @@
use WCPay\Constants\Order_Status;
use WCPay\Constants\Intent_Status;
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Payment_Methods\CC_Payment_Method;
use WCPay\Session_Rate_Limiter;
@@ -160,6 +161,7 @@ public function set_up() {
$mock_dpps,
$this->createMock( WC_Payments_Localization_Service::class ),
$this->createMock( WC_Payments_Fraud_Service::class ),
+ $this->createMock( Duplicates_Detection_Service::class ),
]
)
->setMethods(
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php
index a5b33c1581c..151b3b919fe 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay-subscriptions.php
@@ -8,6 +8,7 @@
use PHPUnit\Framework\MockObject\MockObject;
use WCPay\Core\Server\Request\Create_And_Confirm_Intention;
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Exceptions\API_Exception;
use WCPay\Internal\Service\Level3Service;
use WCPay\Internal\Service\OrderService;
@@ -102,6 +103,13 @@ class WC_Payment_Gateway_WCPay_Subscriptions_Test extends WCPAY_UnitTestCase {
*/
private $mock_fraud_service;
+ /**
+ * Mock Duplicates Detection Service.
+ *
+ * @var Duplicates_Detection_Service
+ */
+ private $mock_duplicates_detection_service;
+
public function set_up() {
parent::set_up();
@@ -136,8 +144,9 @@ public function set_up() {
$this->mock_dpps = $this->createMock( Duplicate_Payment_Prevention_Service::class );
- $this->mock_localization_service = $this->createMock( WC_Payments_Localization_Service::class );
- $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class );
+ $this->mock_localization_service = $this->createMock( WC_Payments_Localization_Service::class );
+ $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class );
+ $this->mock_duplicates_detection_service = $this->createMock( Duplicates_Detection_Service::class );
$mock_payment_method = $this->getMockBuilder( CC_Payment_Method::class )
->setConstructorArgs( [ $this->mock_token_service ] )
@@ -156,7 +165,8 @@ public function set_up() {
$this->order_service,
$this->mock_dpps,
$this->mock_localization_service,
- $this->mock_fraud_service
+ $this->mock_fraud_service,
+ $this->mock_duplicates_detection_service,
);
$this->wcpay_gateway->init_hooks();
WC_Payments::set_gateway( $this->wcpay_gateway );
@@ -830,7 +840,8 @@ public function test_adds_custom_payment_meta_input_fallback_until_subs_3_0_7()
$this->order_service,
$this->mock_dpps,
$this->mock_localization_service,
- $this->mock_fraud_service
+ $this->mock_fraud_service,
+ $this->mock_duplicates_detection_service,
);
// Ensure the has_attached_integration_hooks property is set to false so callbacks can be attached in maybe_init_subscriptions().
@@ -864,7 +875,8 @@ public function test_does_not_add_custom_payment_meta_input_fallback_for_subs_3_
$this->order_service,
$this->mock_dpps,
$this->mock_localization_service,
- $this->mock_fraud_service
+ $this->mock_fraud_service,
+ $this->mock_duplicates_detection_service,
);
$this->assertFalse( has_action( 'woocommerce_admin_order_data_after_billing_address' ) );
diff --git a/tests/unit/test-class-wc-payment-gateway-wcpay.php b/tests/unit/test-class-wc-payment-gateway-wcpay.php
index b1dc008ac7d..c485a19ecb3 100644
--- a/tests/unit/test-class-wc-payment-gateway-wcpay.php
+++ b/tests/unit/test-class-wc-payment-gateway-wcpay.php
@@ -17,6 +17,7 @@
use WCPay\Constants\Intent_Status;
use WCPay\Constants\Payment_Method;
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Exceptions\Amount_Too_Small_Exception;
use WCPay\Exceptions\API_Exception;
use WCPay\Exceptions\Process_Payment_Exception;
@@ -177,6 +178,13 @@ class WC_Payment_Gateway_WCPay_Test extends WCPAY_UnitTestCase {
*/
private $mock_fraud_service;
+ /**
+ * Mock Duplicates Detection Service.
+ *
+ * @var Duplicates_Detection_Service
+ */
+ private $mock_duplicates_detection_service;
+
/**
* Pre-test setup
*/
@@ -229,7 +237,8 @@ public function set_up() {
'currency_code' => 'usd',
]
);
- $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class );
+ $this->mock_fraud_service = $this->createMock( WC_Payments_Fraud_Service::class );
+ $this->mock_duplicates_detection_service = $this->createMock( Duplicates_Detection_Service::class );
$this->mock_payment_method = $this->getMockBuilder( CC_Payment_Method::class )
->setConstructorArgs( [ $this->mock_token_service ] )
@@ -897,6 +906,7 @@ public function test_process_redirect_setup_intent_succeded() {
$this->mock_dpps,
$this->mock_localization_service,
$this->mock_fraud_service,
+ $this->mock_duplicates_detection_service,
]
)
->onlyMethods(
@@ -1004,6 +1014,7 @@ public function test_process_redirect_payment_save_payment_token() {
$this->mock_dpps,
$this->mock_localization_service,
$this->mock_fraud_service,
+ $this->mock_duplicates_detection_service,
]
)
->onlyMethods(
@@ -2778,6 +2789,207 @@ public function test_process_payment_rejects_if_invalid_fraud_prevention_token()
$this->card_gateway->process_payment( $order->get_id() );
}
+ public function test_process_payment_marks_order_as_blocked_for_fraud() {
+ $order = WC_Helper_Order::create_order();
+
+ $this->mock_wcpay_account
+ ->expects( $this->any() )
+ ->method( 'is_stripe_connected' )
+ ->willReturn( true );
+
+ $fraud_prevention_service_mock = $this->get_fraud_prevention_service_mock();
+
+ $fraud_prevention_service_mock
+ ->expects( $this->once() )
+ ->method( 'is_enabled' )
+ ->willReturn( true );
+
+ $fraud_prevention_service_mock
+ ->expects( $this->once() )
+ ->method( 'verify_token' )
+ ->with( 'correct-token' )
+ ->willReturn( true );
+
+ $_POST['wcpay-fraud-prevention-token'] = 'correct-token';
+
+ $this->mock_rate_limiter
+ ->expects( $this->once() )
+ ->method( 'is_limited' )
+ ->willReturn( false );
+
+ $mock_order_service = $this->getMockBuilder( WC_Payments_Order_Service::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $mock_order_service
+ ->expects( $this->once() )
+ ->method( 'mark_order_blocked_for_fraud' );
+
+ $mock_wcpay_gateway = $this->get_partial_mock_for_gateway(
+ [ 'prepare_payment_information', 'process_payment_for_order' ],
+ [ WC_Payments_Order_Service::class => $mock_order_service ]
+ );
+
+ $error_message = "There's a problem with this payment. Please try again or use a different payment method.";
+
+ $mock_wcpay_gateway
+ ->expects( $this->once() )
+ ->method( 'prepare_payment_information' );
+ $mock_wcpay_gateway
+ ->expects( $this->once() )
+ ->method( 'process_payment_for_order' )
+ ->willThrowException( new API_Exception( $error_message, 'wcpay_blocked_by_fraud_rule', 400, 'card_error' ) );
+
+ $this->expectException( Exception::class );
+ $this->expectExceptionMessage( $error_message );
+
+ $mock_wcpay_gateway->process_payment( $order->get_id() );
+ }
+
+ public function test_process_payment_marks_order_as_blocked_for_fraud_avs_mismatch() {
+ $ruleset_config = [
+ [
+ 'key' => 'avs_verification',
+ 'outcome' => 'block',
+ 'check' => [
+ 'key' => 'avs_mismatch',
+ 'operator' => 'equals',
+ 'value' => true,
+ ],
+ ],
+ ];
+ set_transient( 'wcpay_fraud_protection_settings', $ruleset_config, DAY_IN_SECONDS );
+
+ $order = WC_Helper_Order::create_order();
+
+ $this->mock_wcpay_account
+ ->expects( $this->any() )
+ ->method( 'is_stripe_connected' )
+ ->willReturn( true );
+
+ $fraud_prevention_service_mock = $this->get_fraud_prevention_service_mock();
+
+ $fraud_prevention_service_mock
+ ->expects( $this->once() )
+ ->method( 'is_enabled' )
+ ->willReturn( true );
+
+ $fraud_prevention_service_mock
+ ->expects( $this->once() )
+ ->method( 'verify_token' )
+ ->with( 'correct-token' )
+ ->willReturn( true );
+
+ $_POST['wcpay-fraud-prevention-token'] = 'correct-token';
+
+ $this->mock_rate_limiter
+ ->expects( $this->once() )
+ ->method( 'is_limited' )
+ ->willReturn( false );
+
+ $mock_order_service = $this->getMockBuilder( WC_Payments_Order_Service::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $mock_order_service
+ ->expects( $this->once() )
+ ->method( 'mark_order_blocked_for_fraud' );
+
+ $mock_wcpay_gateway = $this->get_partial_mock_for_gateway(
+ [ 'prepare_payment_information', 'process_payment_for_order' ],
+ [ WC_Payments_Order_Service::class => $mock_order_service ]
+ );
+
+ $error_message = "There's a problem with this payment. Please try again or use a different payment method.";
+
+ $mock_wcpay_gateway
+ ->expects( $this->once() )
+ ->method( 'prepare_payment_information' );
+ $mock_wcpay_gateway
+ ->expects( $this->once() )
+ ->method( 'process_payment_for_order' )
+ ->willThrowException( new API_Exception( $error_message, 'incorrect_zip', 400, 'card_error' ) );
+
+ $this->expectException( Exception::class );
+ $this->expectExceptionMessage( $error_message );
+
+ $mock_wcpay_gateway->process_payment( $order->get_id() );
+
+ delete_transient( 'wcpay_fraud_protection_settings' );
+ }
+
+ public function test_process_payment_marks_order_as_blocked_for_postal_code_mismatch() {
+ $ruleset_config = [
+ [
+ 'key' => 'address_mismatch',
+ 'outcome' => 'block',
+ 'check' => [
+ 'key' => 'address_mismatch',
+ 'operator' => 'equals',
+ 'value' => true,
+ ],
+ ],
+ ];
+ set_transient( 'wcpay_fraud_protection_settings', $ruleset_config, DAY_IN_SECONDS );
+
+ $order = WC_Helper_Order::create_order();
+
+ $this->mock_wcpay_account
+ ->expects( $this->any() )
+ ->method( 'is_stripe_connected' )
+ ->willReturn( true );
+
+ $fraud_prevention_service_mock = $this->get_fraud_prevention_service_mock();
+
+ $fraud_prevention_service_mock
+ ->expects( $this->once() )
+ ->method( 'is_enabled' )
+ ->willReturn( true );
+
+ $fraud_prevention_service_mock
+ ->expects( $this->once() )
+ ->method( 'verify_token' )
+ ->with( 'correct-token' )
+ ->willReturn( true );
+
+ $_POST['wcpay-fraud-prevention-token'] = 'correct-token';
+
+ $this->mock_rate_limiter
+ ->expects( $this->once() )
+ ->method( 'is_limited' )
+ ->willReturn( false );
+
+ $mock_order_service = $this->getMockBuilder( WC_Payments_Order_Service::class )
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $mock_order_service
+ ->expects( $this->never() )
+ ->method( 'mark_order_blocked_for_fraud' );
+
+ $mock_wcpay_gateway = $this->get_partial_mock_for_gateway(
+ [ 'prepare_payment_information', 'process_payment_for_order' ],
+ [ WC_Payments_Order_Service::class => $mock_order_service ]
+ );
+
+ $error_message = 'We couldnβt verify the postal code in your billing address. Make sure the information is current with your card issuing bank and try again.';
+
+ $mock_wcpay_gateway
+ ->expects( $this->once() )
+ ->method( 'prepare_payment_information' );
+ $mock_wcpay_gateway
+ ->expects( $this->once() )
+ ->method( 'process_payment_for_order' )
+ ->willThrowException( new API_Exception( $error_message, 'incorrect_zip', 400, 'card_error' ) );
+
+ $this->expectException( Exception::class );
+ $this->expectExceptionMessage( $error_message );
+
+ $mock_wcpay_gateway->process_payment( $order->get_id() );
+
+ delete_transient( 'wcpay_fraud_protection_settings' );
+ }
+
public function test_process_payment_continues_if_valid_fraud_prevention_token() {
$order = WC_Helper_Order::create_order();
@@ -2981,27 +3193,40 @@ public function test_is_in_test_mode() {
/**
* Create a partial mock for WC_Payment_Gateway_WCPay class.
*
- * @param array $methods Method names that need to be mocked.
+ * @param array $methods Method names that need to be mocked.
+ * @param array $constructor_replacement Array of constructor arguments that need to be replaced.
+ * The key is the class name and the value is the replacement object.
+ * [ WC_Payments_Order_Service::class => $mock_order_service ]
+ *
* @return MockObject|WC_Payment_Gateway_WCPay
*/
- private function get_partial_mock_for_gateway( array $methods = [] ) {
+ private function get_partial_mock_for_gateway( array $methods = [], array $constructor_replacement = [] ) {
+ $constructor_args = [
+ $this->mock_api_client,
+ $this->mock_wcpay_account,
+ $this->mock_customer_service,
+ $this->mock_token_service,
+ $this->mock_action_scheduler_service,
+ $this->mock_payment_method,
+ [ $this->mock_payment_method ],
+ $this->mock_rate_limiter,
+ $this->order_service,
+ $this->mock_dpps,
+ $this->mock_localization_service,
+ $this->mock_fraud_service,
+ $this->mock_duplicates_detection_service,
+ ];
+
+ foreach ( $constructor_replacement as $key => $value ) {
+ foreach ( $constructor_args as $index => $arg ) {
+ if ( $arg instanceof $key ) {
+ $constructor_args[ $index ] = $value;
+ }
+ }
+ }
+
return $this->getMockBuilder( WC_Payment_Gateway_WCPay::class )
- ->setConstructorArgs(
- [
- $this->mock_api_client,
- $this->mock_wcpay_account,
- $this->mock_customer_service,
- $this->mock_token_service,
- $this->mock_action_scheduler_service,
- $this->mock_payment_method,
- [ $this->mock_payment_method ],
- $this->mock_rate_limiter,
- $this->order_service,
- $this->mock_dpps,
- $this->mock_localization_service,
- $this->mock_fraud_service,
- ]
- )
+ ->setConstructorArgs( $constructor_args )
->setMethods( $methods )
->getMock();
}
@@ -3475,7 +3700,8 @@ private function init_gateways() {
$this->order_service,
$this->mock_dpps,
$this->mock_localization_service,
- $this->mock_fraud_service
+ $this->mock_fraud_service,
+ $this->mock_duplicates_detection_service
);
}
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 1bd4dbeeafc..8a6a1b7f4f3 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
@@ -7,6 +7,7 @@
use PHPUnit\Framework\MockObject\MockObject;
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Payment_Methods\CC_Payment_Method;
use WCPay\Session_Rate_Limiter;
use WCPay\WooPay\WooPay_Utilities;
@@ -177,7 +178,8 @@ private function make_wcpay_gateway() {
$mock_order_service,
$mock_dpps,
$this->createMock( WC_Payments_Localization_Service::class ),
- $this->createMock( WC_Payments_Fraud_Service::class )
+ $this->createMock( WC_Payments_Fraud_Service::class ),
+ $this->createMock( Duplicates_Detection_Service::class )
);
}
diff --git a/tests/unit/test-class-wc-payments-express-checkout-button-helper.php b/tests/unit/test-class-wc-payments-express-checkout-button-helper.php
index 8b3312dcf30..3dc7878ba39 100644
--- a/tests/unit/test-class-wc-payments-express-checkout-button-helper.php
+++ b/tests/unit/test-class-wc-payments-express-checkout-button-helper.php
@@ -6,6 +6,7 @@
*/
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Payment_Methods\CC_Payment_Method;
use WCPay\Session_Rate_Limiter;
@@ -91,7 +92,8 @@ private function make_wcpay_gateway() {
$mock_order_service,
$mock_dpps,
$this->createMock( WC_Payments_Localization_Service::class ),
- $this->createMock( WC_Payments_Fraud_Service::class )
+ $this->createMock( WC_Payments_Fraud_Service::class ),
+ $this->createMock( Duplicates_Detection_Service::class )
);
}
diff --git a/tests/unit/test-class-wc-payments-order-success-page.php b/tests/unit/test-class-wc-payments-order-success-page.php
index 20c0edeaf27..df92e94a482 100644
--- a/tests/unit/test-class-wc-payments-order-success-page.php
+++ b/tests/unit/test-class-wc-payments-order-success-page.php
@@ -24,7 +24,7 @@ public function set_up() {
public function test_show_woopay_payment_method_name_empty_order() {
$method_name = 'Credit card';
- $result = $this->payments_order_success_page->show_woopay_payment_method_name( $method_name, null );
+ $result = $this->payments_order_success_page->show_woocommerce_payments_payment_method_name( $method_name, null );
$this->assertSame( $method_name, $result );
}
@@ -34,7 +34,7 @@ public function test_show_woopay_payment_method_name_without_woopay_meta() {
$order->save();
$method_name = 'Credit card';
- $result = $this->payments_order_success_page->show_woopay_payment_method_name( $method_name, $order );
+ $result = $this->payments_order_success_page->show_woocommerce_payments_payment_method_name( $method_name, $order );
$this->assertSame( $method_name, $result );
}
@@ -43,13 +43,14 @@ public function test_show_woopay_payment_method_name_order_with_woopay_meta() {
$order = WC_Helper_Order::create_order();
$order->add_meta_data( 'is_woopay', true );
$order->add_meta_data( 'last4', '1234' );
+ $order->set_payment_method( 'woocommerce_payments' );
$order->save();
add_filter( 'woocommerce_is_order_received_page', '__return_true' );
- $result = $this->payments_order_success_page->show_woopay_payment_method_name( 'Credit card', $order );
+ $result = $this->payments_order_success_page->show_woocommerce_payments_payment_method_name( 'Credit card', $order );
remove_filter( 'woocommerce_is_order_received_page', '__return_true' );
- $this->assertStringContainsString( 'wc-payment-gateway-method-name-woopay-wrapper', $result );
+ $this->assertStringContainsString( 'wc-payment-gateway-method-logo-wrapper woopay', $result );
$this->assertStringContainsString( 'img alt="WooPay"', $result );
$this->assertStringContainsString( sprintf( 'Card ending in %s', $order->get_meta( 'last4' ) ), $result );
}
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 fe703f8ee94..672a4cdaeac 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
@@ -7,6 +7,7 @@
use WCPay\Constants\Country_Code;
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Payment_Methods\CC_Payment_Method;
use WCPay\Session_Rate_Limiter;
@@ -223,7 +224,8 @@ private function make_wcpay_gateway() {
$mock_order_service,
$mock_dpps,
$this->createMock( WC_Payments_Localization_Service::class ),
- $this->createMock( WC_Payments_Fraud_Service::class )
+ $this->createMock( WC_Payments_Fraud_Service::class ),
+ $this->createMock( Duplicates_Detection_Service::class )
);
}
diff --git a/tests/unit/test-class-wc-payments-utils.php b/tests/unit/test-class-wc-payments-utils.php
index a1a016a4c14..27c9c8e2c3b 100644
--- a/tests/unit/test-class-wc-payments-utils.php
+++ b/tests/unit/test-class-wc-payments-utils.php
@@ -555,4 +555,53 @@ public function test_get_filtered_error_status_code_with_api_exception() {
public function test_get_filtered_error_status_code_with_api_exception_and_402_status() {
$this->assertSame( 400, WC_Payments_Utils::get_filtered_error_status_code( new \WCPay\Exceptions\API_Exception( 'Error: Your card was declined.', 'card_declined', 402 ) ) );
}
+
+ private function delete_appearance_theme_transients( $transients ) {
+ foreach ( $transients as $location => $contexts ) {
+ foreach ( $contexts as $context => $transient ) {
+ delete_transient( $transient );
+ }
+ }
+ }
+
+ private function set_appearance_theme_transients( $transients ) {
+ foreach ( $transients as $location => $contexts ) {
+ foreach ( $contexts as $context => $transient ) {
+ set_transient( $transient, $location . '_' . $context . '_value', DAY_IN_SECONDS );
+ }
+ }
+ }
+
+ public function test_get_active_upe_theme_transient_for_location() {
+ $theme_transients = \WC_Payment_Gateway_WCPay::APPEARANCE_THEME_TRANSIENTS;
+
+ // Test with no transients set.
+ $this->assertSame( 'stripe', WC_Payments_Utils::get_active_upe_theme_transient_for_location( 'checkout', 'blocks' ) );
+
+ // Set the transients.
+ $this->set_appearance_theme_transients( $theme_transients );
+
+ // Test with transients set.
+ // Test with invalid location.
+ $this->assertSame( 'checkout_blocks_value', WC_Payments_Utils::get_active_upe_theme_transient_for_location( 'invalid_location', 'blocks' ) );
+
+ // Test with valid location and invalid context.
+ $this->assertSame( 'checkout_blocks_value', WC_Payments_Utils::get_active_upe_theme_transient_for_location( 'checkout', 'invalid_context' ) );
+
+ // Test with valid location and context.
+ foreach ( $theme_transients as $location => $contexts ) {
+ foreach ( $contexts as $context => $transient ) {
+ // Our transient for the product page is the same transient for both block and classic.
+ if ( 'product_page' === $location ) {
+ $this->assertSame( 'product_page_classic_value', WC_Payments_Utils::get_active_upe_theme_transient_for_location( $location, 'blocks' ) );
+ $this->assertSame( 'product_page_classic_value', WC_Payments_Utils::get_active_upe_theme_transient_for_location( $location, 'classic' ) );
+ } else {
+ $this->assertSame( $location . '_' . $context . '_value', WC_Payments_Utils::get_active_upe_theme_transient_for_location( $location, $context ) );
+ }
+ }
+ }
+
+ // Remove the transients.
+ $this->delete_appearance_theme_transients( $theme_transients );
+ }
}
diff --git a/tests/unit/test-class-wc-payments-woopay-button-handler.php b/tests/unit/test-class-wc-payments-woopay-button-handler.php
index 1c87f05a4b7..dbb5cefe960 100644
--- a/tests/unit/test-class-wc-payments-woopay-button-handler.php
+++ b/tests/unit/test-class-wc-payments-woopay-button-handler.php
@@ -6,6 +6,7 @@
*/
use WCPay\Duplicate_Payment_Prevention_Service;
+use WCPay\Duplicates_Detection_Service;
use WCPay\Payment_Methods\CC_Payment_Method;
use WCPay\Session_Rate_Limiter;
use WCPay\WooPay\WooPay_Utilities;
@@ -163,7 +164,8 @@ private function make_wcpay_gateway() {
$mock_order_service,
$mock_dpps,
$this->createMock( WC_Payments_Localization_Service::class ),
- $this->createMock( WC_Payments_Fraud_Service::class )
+ $this->createMock( WC_Payments_Fraud_Service::class ),
+ $this->createMock( Duplicates_Detection_Service::class )
);
}
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 6193794361d..e4a252c369a 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
@@ -258,14 +258,15 @@ public function test_get_onboarding_data() {
];
$user_data = [
- 'user_id' => 1,
- 'ip_address' => '0.0.0.0',
- 'browser' => [
+ 'user_id' => 1,
+ 'ip_address' => '0.0.0.0',
+ 'browser' => [
'user_agent' => 'Unit Test Agent/0.1.0',
'accept_language' => 'en-US,en;q=0.5',
'content_language' => 'en-US,en;q=0.5',
],
- 'referer' => 'https://example.com',
+ 'referer' => 'https://example.com',
+ 'onboarding_source' => 'test_source',
];
$account_data = [];
@@ -337,16 +338,7 @@ public function test_get_onboarding_business_types() {
->expects( $this->once() )
->method( 'remote_request' )
->with(
- [
- 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/onboarding/business_types?test_mode=0',
- 'method' => 'GET',
- 'headers' => [
- 'Content-Type' => 'application/json; charset=utf-8',
- 'User-Agent' => 'Unit Test Agent/0.1.0',
- ],
- 'timeout' => 70,
- 'connect_timeout' => 70,
- ],
+ $this->containsIdentical( 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/onboarding/business_types?test_mode=0' ),
null,
true,
true // get_onboarding_business_types should use user token auth.
@@ -365,16 +357,7 @@ public function test_get_onboarding_required_verification_information() {
->expects( $this->once() )
->method( 'remote_request' )
->with(
- [
- 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/onboarding/required_verification_information?test_mode=0&country=country&type=type',
- 'method' => 'GET',
- 'headers' => [
- 'Content-Type' => 'application/json; charset=utf-8',
- 'User-Agent' => 'Unit Test Agent/0.1.0',
- ],
- 'timeout' => 70,
- 'connect_timeout' => 70,
- ],
+ $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.
@@ -432,16 +415,7 @@ public function test_get_currency_rates() {
->expects( $this->once() )
->method( 'remote_request' )
->with(
- [
- 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/currency/rates?test_mode=0¤cy_from=USD',
- 'method' => 'GET',
- 'headers' => [
- 'Content-Type' => 'application/json; charset=utf-8',
- 'User-Agent' => 'Unit Test Agent/0.1.0',
- ],
- 'timeout' => 70,
- 'connect_timeout' => 70,
- ],
+ $this->containsIdentical( 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/currency/rates?test_mode=0¤cy_from=USD' ),
null,
true,
false
@@ -581,16 +555,7 @@ public function test_delete_terminal_location_success() {
->expects( $this->once() )
->method( 'remote_request' )
->with(
- [
- 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/terminal/locations/tml_XXXXXXX?test_mode=0',
- 'method' => 'DELETE',
- 'headers' => [
- 'Content-Type' => 'application/json; charset=utf-8',
- 'User-Agent' => 'Unit Test Agent/0.1.0',
- ],
- 'timeout' => 70,
- 'connect_timeout' => 70,
- ],
+ $this->containsIdentical( 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/terminal/locations/tml_XXXXXXX?test_mode=0' ),
null,
true,
false
@@ -751,16 +716,7 @@ public function test_cancel_subscription() {
->expects( $this->once() )
->method( 'remote_request' )
->with(
- [
- 'url' => 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/subscriptions/sub_test?test_mode=0',
- 'method' => 'DELETE',
- 'headers' => [
- 'Content-Type' => 'application/json; charset=utf-8',
- 'User-Agent' => 'Unit Test Agent/0.1.0',
- ],
- 'timeout' => 70,
- 'connect_timeout' => 70,
- ],
+ $this->containsIdentical( 'https://public-api.wordpress.com/wpcom/v2/sites/%s/wcpay/subscriptions/sub_test?test_mode=0' ),
null,
true,
false
@@ -1320,11 +1276,15 @@ private function validate_default_remote_request_params( $data, $url, $method )
private function get_mock_compatibility_data( array $args = [] ): array {
return array_merge(
[
- 'woopayments_version' => WCPAY_VERSION_NUMBER,
- 'woocommerce_version' => WC_VERSION,
- 'blog_theme' => 'default',
- 'active_plugins' => [],
- 'post_types_count' => [
+ 'woopayments_version' => WCPAY_VERSION_NUMBER,
+ 'woocommerce_version' => WC_VERSION,
+ 'woocommerce_permalinks' => get_option( 'woocommerce_permalinks' ),
+ 'woocommerce_shop' => get_permalink( wc_get_page_id( 'shop' ) ),
+ 'woocommerce_cart' => get_permalink( wc_get_page_id( 'cart' ) ),
+ 'woocommerce_checkout' => get_permalink( wc_get_page_id( 'checkout' ) ),
+ 'blog_theme' => 'default',
+ 'active_plugins' => [],
+ 'post_types_count' => [
'post' => 0,
'page' => 0,
'attachment' => 0,
diff --git a/woocommerce-payments.php b/woocommerce-payments.php
index 8594e2dee63..f6c14d6d5c3 100644
--- a/woocommerce-payments.php
+++ b/woocommerce-payments.php
@@ -11,7 +11,7 @@
* WC tested up to: 8.7.0
* Requires at least: 6.0
* Requires PHP: 7.3
- * Version: 7.5.3
+ * Version: 7.6.0
* Requires Plugins: woocommerce
*
* @package WooCommerce\Payments