Commit 564180a5ed for woocommerce

commit 564180a5edfd14086150e9dcbd5427d859aaea37
Author: Luiz Reis <luiz.reis@automattic.com>
Date:   Wed Feb 4 17:16:11 2026 -0300

    Fraud Protection: Add Blackbox JS script loading (#63116)

    * Add Blackbox JS script loading for fraud protection

    Load the Blackbox JS telemetry SDK on checkout, pay-for-order, and
    add-payment-method pages when fraud protection is enabled. A small
    init script calls Blackbox.configure() with the site's API key and
    Jetpack blog_id. Skips loading if blog_id is unavailable.

diff --git a/plugins/woocommerce/client/legacy/js/frontend/fraud-protection/blackbox-init.js b/plugins/woocommerce/client/legacy/js/frontend/fraud-protection/blackbox-init.js
new file mode 100644
index 0000000000..8c51eed96e
--- /dev/null
+++ b/plugins/woocommerce/client/legacy/js/frontend/fraud-protection/blackbox-init.js
@@ -0,0 +1,23 @@
+/**
+ * Woo Fraud Protection - Blackbox Initialization
+ *
+ * Configures the Blackbox JS SDK with the site's API key and blog ID.
+ * Loaded on checkout, pay-for-order, and add-payment-method pages.
+ */
+( function () {
+	'use strict';
+
+	var config = window.wcBlackboxConfig;
+	if ( ! config ) {
+		return;
+	}
+
+	if ( ! window.Blackbox || ! window.Blackbox.configure ) {
+		return;
+	}
+
+	window.Blackbox.configure( {
+		apiKey: config.apiKey,
+		blogId: config.blogId,
+	} );
+} )();
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/BlackboxScriptHandler.php b/plugins/woocommerce/src/Internal/FraudProtection/BlackboxScriptHandler.php
new file mode 100644
index 0000000000..5fa39a0ab6
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/FraudProtection/BlackboxScriptHandler.php
@@ -0,0 +1,151 @@
+<?php
+/**
+ * BlackboxScriptHandler class file.
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\FraudProtection;
+
+use Automattic\Jetpack\Constants;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Handles loading Blackbox JS telemetry script on payment method pages.
+ *
+ * Enqueues the external Blackbox JS SDK and a small initialization script
+ * on checkout, pay-for-order, and add-payment-method pages. The init script
+ * calls Blackbox.configure() with the site's API key and Jetpack blog ID.
+ *
+ * @since 10.6.0
+ * @internal This class is part of the internal API and is subject to change without notice.
+ */
+class BlackboxScriptHandler {
+
+	/**
+	 * Blackbox JS SDK URL.
+	 */
+	private const BLACKBOX_JS_URL = 'https://blackbox-api.wp.com/v1/dist/v.js';
+
+	/**
+	 * API key identifying WooCommerce as a Blackbox client.
+	 */
+	private const API_KEY = 'woocommerce';
+
+	/**
+	 * Register hooks for Blackbox script loading.
+	 *
+	 * Called from FraudProtectionController::on_init() which already checks
+	 * if the feature is enabled.
+	 *
+	 * @return void
+	 */
+	public function register(): void {
+		add_action( 'wp_enqueue_scripts', array( $this, 'maybe_enqueue_scripts' ) );
+	}
+
+	/**
+	 * Conditionally enqueue Blackbox scripts on payment method pages.
+	 *
+	 * Loads scripts on checkout (including custom pages with the checkout block),
+	 * pay-for-order, and add-payment-method pages.
+	 * Extensions can use the `woocommerce_fraud_protection_enqueue_blackbox_scripts`
+	 * filter to load scripts on additional pages (e.g., product pages for express payments).
+	 *
+	 * @return void
+	 */
+	public function maybe_enqueue_scripts(): void {
+		global $wp;
+
+		$should_enqueue = is_checkout() ||
+			has_block( 'woocommerce/checkout' ) ||
+			is_checkout_pay_page() ||
+			// Check add-payment-method query_var to avoid loading on regular payment methods page.
+			( is_add_payment_method_page() && isset( $wp->query_vars['add-payment-method'] ) );
+
+		/**
+		 * Filter whether to enqueue Blackbox fraud protection scripts on the current page.
+		 *
+		 * By default, scripts are loaded on checkout, pay-for-order, and add-payment-method pages.
+		 * Extensions can return true to load scripts on additional pages where payment methods
+		 * are rendered (e.g., product pages for express checkout buttons).
+		 *
+		 * @since 10.6.0
+		 *
+		 * @param bool $should_enqueue Whether to enqueue Blackbox scripts on the current page.
+		 */
+		$should_enqueue = (bool) apply_filters( 'woocommerce_fraud_protection_enqueue_blackbox_scripts', $should_enqueue );
+
+		if ( ! $should_enqueue ) {
+			return;
+		}
+
+		$blog_id = $this->get_blog_id();
+
+		if ( ! $blog_id ) {
+			FraudProtectionController::log(
+				'error',
+				'Blackbox scripts not loaded: Jetpack blog ID not available. Is the site connected to Jetpack?'
+			);
+			return;
+		}
+
+		$this->enqueue_scripts( $blog_id );
+	}
+
+	/**
+	 * Enqueue the Blackbox SDK and initialization scripts.
+	 *
+	 * @param int $blog_id The Jetpack blog ID.
+	 * @return void
+	 */
+	private function enqueue_scripts( int $blog_id ): void {
+		wp_enqueue_script(
+			'wc-fraud-protection-blackbox',
+			self::BLACKBOX_JS_URL,
+			array(),
+			null, // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- External SDK, version managed by Blackbox CDN.
+			array( 'in_footer' => true )
+		);
+
+		$suffix = Constants::is_true( 'SCRIPT_DEBUG' ) ? '' : '.min';
+
+		// Enqueue the Woo Fraud Protection init script.
+		wp_enqueue_script(
+			'wc-fraud-protection-blackbox-init',
+			plugins_url( 'assets/js/frontend/fraud-protection/blackbox-init' . $suffix . '.js', WC_PLUGIN_FILE ),
+			array( 'wc-fraud-protection-blackbox' ),
+			WC_VERSION,
+			array( 'in_footer' => true )
+		);
+
+		wp_localize_script(
+			'wc-fraud-protection-blackbox-init',
+			'wcBlackboxConfig',
+			array(
+				'apiKey' => self::API_KEY,
+				'blogId' => $blog_id,
+			)
+		);
+	}
+
+	/**
+	 * Get the Jetpack blog ID.
+	 *
+	 * @return int|false Blog ID or false if not available.
+	 */
+	private function get_blog_id() {
+		if ( ! class_exists( \Jetpack_Options::class ) ) {
+			return false;
+		}
+
+		$blog_id = \Jetpack_Options::get_option( 'id' );
+
+		if ( ! is_numeric( $blog_id ) || (int) $blog_id <= 0 ) {
+			return false;
+		}
+
+		return (int) $blog_id;
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionController.php b/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionController.php
index 1039e68e46..2a13c43c85 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionController.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionController.php
@@ -38,6 +38,13 @@ class FraudProtectionController implements RegisterHooksInterface {
 	 */
 	private BlockedSessionNotice $blocked_session_notice;

+	/**
+	 * Blackbox script handler instance.
+	 *
+	 * @var BlackboxScriptHandler
+	 */
+	private BlackboxScriptHandler $blackbox_script_handler;
+
 	/**
 	 * Register hooks.
 	 */
@@ -52,15 +59,18 @@ class FraudProtectionController implements RegisterHooksInterface {
 	 *
 	 * @internal
 	 *
-	 * @param FeaturesController   $features_controller      The instance of FeaturesController to use.
-	 * @param BlockedSessionNotice $blocked_session_notice   The instance of BlockedSessionNotice to use.
+	 * @param FeaturesController    $features_controller      The instance of FeaturesController to use.
+	 * @param BlockedSessionNotice  $blocked_session_notice   The instance of BlockedSessionNotice to use.
+	 * @param BlackboxScriptHandler $blackbox_script_handler  The instance of BlackboxScriptHandler to use.
 	 */
 	final public function init(
 		FeaturesController $features_controller,
-		BlockedSessionNotice $blocked_session_notice
+		BlockedSessionNotice $blocked_session_notice,
+		BlackboxScriptHandler $blackbox_script_handler
 	): void {
-		$this->features_controller    = $features_controller;
-		$this->blocked_session_notice = $blocked_session_notice;
+		$this->features_controller     = $features_controller;
+		$this->blocked_session_notice  = $blocked_session_notice;
+		$this->blackbox_script_handler = $blackbox_script_handler;
 	}

 	/**
@@ -75,6 +85,7 @@ class FraudProtectionController implements RegisterHooksInterface {
 		}

 		$this->blocked_session_notice->register();
+		$this->blackbox_script_handler->register();
 	}

 	/**
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlackboxScriptHandlerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlackboxScriptHandlerTest.php
new file mode 100644
index 0000000000..b3ac8f3331
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlackboxScriptHandlerTest.php
@@ -0,0 +1,231 @@
+<?php
+/**
+ * BlackboxScriptHandlerTest class file.
+ */
+
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
+
+use Automattic\WooCommerce\Internal\FraudProtection\BlackboxScriptHandler;
+use Automattic\WooCommerce\RestApi\UnitTests\LoggerSpyTrait;
+use WC_Unit_Test_Case;
+
+/**
+ * Tests for BlackboxScriptHandler.
+ *
+ * @covers \Automattic\WooCommerce\Internal\FraudProtection\BlackboxScriptHandler
+ */
+class BlackboxScriptHandlerTest extends WC_Unit_Test_Case {
+
+	use LoggerSpyTrait;
+
+	/**
+	 * The System Under Test.
+	 *
+	 * @var BlackboxScriptHandler
+	 */
+	private $sut;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		$this->sut = new BlackboxScriptHandler();
+		$this->sut->register();
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		parent::tearDown();
+		remove_all_filters( 'woocommerce_fraud_protection_enqueue_blackbox_scripts' );
+		remove_all_filters( 'woocommerce_is_checkout' );
+		remove_all_filters( 'pre_option_jetpack_options' );
+		remove_all_filters( 'pre_option_woocommerce_myaccount_page_id' );
+		wp_dequeue_script( 'wc-fraud-protection-blackbox' );
+		wp_dequeue_script( 'wc-fraud-protection-blackbox-init' );
+		wp_deregister_script( 'wc-fraud-protection-blackbox' );
+		wp_deregister_script( 'wc-fraud-protection-blackbox-init' );
+
+		// Clean up global query vars and post.
+		global $wp, $post;
+		unset( $wp->query_vars['order-pay'] );
+		unset( $wp->query_vars['add-payment-method'] );
+		$post = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Test teardown cleanup.
+	}
+
+	/**
+	 * @testdox Should enqueue Blackbox scripts on checkout page.
+	 */
+	public function test_enqueues_scripts_on_checkout(): void {
+		$this->mock_jetpack_blog_id( 12345 );
+		$this->mock_wc_page( 'checkout' );
+
+		do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+
+		$this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should be enqueued on checkout' );
+		$this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox-init', 'enqueued' ), 'Blackbox init script should be enqueued on checkout' );
+	}
+
+	/**
+	 * @testdox Should enqueue Blackbox scripts on pay-for-order page.
+	 */
+	public function test_enqueues_scripts_on_pay_for_order(): void {
+		$this->mock_jetpack_blog_id( 12345 );
+		$this->mock_wc_page( 'order-pay' );
+
+		do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+
+		$this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should be enqueued on pay-for-order' );
+		$this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox-init', 'enqueued' ), 'Blackbox init script should be enqueued on pay-for-order' );
+	}
+
+	/**
+	 * @testdox Should enqueue Blackbox scripts on add-payment-method page.
+	 */
+	public function test_enqueues_scripts_on_add_payment_method(): void {
+		$this->mock_jetpack_blog_id( 12345 );
+		$this->mock_wc_page( 'add-payment-method' );
+
+		do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+
+		$this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should be enqueued on add-payment-method' );
+		$this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox-init', 'enqueued' ), 'Blackbox init script should be enqueued on add-payment-method' );
+	}
+
+	/**
+	 * @testdox Should enqueue Blackbox scripts on a custom page with the checkout block.
+	 */
+	public function test_enqueues_scripts_on_custom_checkout_block_page(): void {
+		$this->mock_jetpack_blog_id( 12345 );
+		$this->mock_wc_page( 'custom-blocks-checkout' );
+
+		do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+
+		$this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should be enqueued on custom checkout block page' );
+		$this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox-init', 'enqueued' ), 'Blackbox init script should be enqueued on custom checkout block page' );
+	}
+
+	/**
+	 * @testdox Should not enqueue Blackbox scripts on non-payment pages.
+	 */
+	public function test_does_not_enqueue_scripts_on_other_pages(): void {
+		$this->markTestSkipped( 'Flaky in full suite due to is_checkout returning true (despite the resets).' );
+
+		$this->mock_jetpack_blog_id( 12345 );
+
+		do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+
+		$this->assertFalse( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should not be enqueued on non-payment pages' );
+		$this->assertFalse( wp_script_is( 'wc-fraud-protection-blackbox-init', 'enqueued' ), 'Blackbox init script should not be enqueued on non-payment pages' );
+	}
+
+	/**
+	 * @testdox Should not enqueue scripts and log error when Jetpack blog ID is unavailable.
+	 */
+	public function test_does_not_enqueue_scripts_without_blog_id(): void {
+		$this->mock_wc_page( 'checkout' );
+
+		do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+
+		$this->assertFalse( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should not be enqueued without blog ID' );
+		$this->assertLogged( 'error', 'Jetpack blog ID not available' );
+	}
+
+	/**
+	 * @testdox Should pass correct config data via wp_localize_script.
+	 */
+	public function test_passes_correct_config_data(): void {
+		$this->mock_jetpack_blog_id( 42 );
+		$this->mock_wc_page( 'checkout' );
+
+		do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+
+		$data = wp_scripts()->get_data( 'wc-fraud-protection-blackbox-init', 'data' );
+		$this->assertStringContainsString( '"woocommerce"', $data, 'Should contain API key' );
+		$this->assertStringContainsString( '"42"', $data, 'Should contain blog ID' );
+	}
+
+	/**
+	 * @testdox Should allow extensions to enable scripts on additional pages via filter.
+	 */
+	public function test_filter_enables_scripts_on_custom_pages(): void {
+		$this->mock_jetpack_blog_id( 12345 );
+		add_filter( 'woocommerce_fraud_protection_enqueue_blackbox_scripts', '__return_true' );
+
+		do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+
+		$this->assertTrue( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should be enqueued when filter returns true' );
+	}
+
+	/**
+	 * @testdox Should allow extensions to disable scripts on checkout via filter.
+	 */
+	public function test_filter_disables_scripts_on_checkout(): void {
+		$this->mock_jetpack_blog_id( 12345 );
+		$this->mock_wc_page( 'checkout' );
+		add_filter( 'woocommerce_fraud_protection_enqueue_blackbox_scripts', '__return_false' );
+
+		do_action( 'wp_enqueue_scripts' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+
+		$this->assertFalse( wp_script_is( 'wc-fraud-protection-blackbox', 'enqueued' ), 'Blackbox SDK should not be enqueued when filter returns false' );
+	}
+
+	/**
+	 * Mock the Jetpack blog ID via the pre_option_jetpack_options filter.
+	 *
+	 * @param int $blog_id The blog ID to return.
+	 */
+	private function mock_jetpack_blog_id( int $blog_id ): void {
+		add_filter(
+			'pre_option_jetpack_options',
+			function () use ( $blog_id ) {
+				return array( 'id' => $blog_id );
+			}
+		);
+	}
+
+	/**
+	 * Mock a WooCommerce page URL.
+	 *
+	 * @param string $page The page to mock (e.g., 'checkout', 'custom-blocks-checkout', 'order-pay', 'add-payment-method').
+	 */
+	private function mock_wc_page( string $page ): void {
+		global $wp, $post, $wp_query;
+
+		switch ( $page ) {
+			case 'checkout':
+				add_filter( 'woocommerce_is_checkout', '__return_true' );
+				break;
+			case 'custom-blocks-checkout':
+				$post = $this->factory()->post->create_and_get( // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Test needs to simulate a page with checkout block.
+					array( 'post_content' => '<!-- wp:woocommerce/checkout --><div class="wp-block-woocommerce-checkout"></div><!-- /wp:woocommerce/checkout -->' )
+				);
+				break;
+			case 'order-pay':
+				$wp->query_vars['order-pay'] = true;
+				add_filter( 'woocommerce_is_checkout', '__return_true' );
+				break;
+			case 'add-payment-method':
+				$page_id = $this->factory()->post->create(
+					array(
+						'post_type'  => 'page',
+						'post_title' => 'My account',
+					)
+				);
+				add_filter(
+					'pre_option_woocommerce_myaccount_page_id',
+					function () use ( $page_id ) {
+						return $page_id;
+					}
+				);
+				$this->go_to( '?page_id=' . $page_id );
+				$wp->query_vars['add-payment-method'] = true;
+				break;
+		}
+	}
+}