Commit 922cc40ed9 for woocommerce

commit 922cc40ed9c61c15c73231d53ea939768c56b731
Author: Leonardo Lopes de Albuquerque <leonardo.albuquerque@automattic.com>
Date:   Mon Dec 29 14:51:34 2025 -0300

    [Fraud Protection] Created button to connect to jetpack if not connected yet (#62517)

    * Created button to connect to jetpack if not connected yet

    * Fixed linting errors

    * Removed unused log method

    * Now the connection button only shows up when fraud protection is enabled

    * Prevented duplicated fields from showing up

    * Added tests

    * enqueued jquery in case it was not enqueued yet

    * Fixed double url encode

    * Fixed test linting errors

    * More linting fixes

    * Created button to connect to jetpack if not connected yet

    * Fixed linting errors

    * Removed unused log method

    * Now the connection button only shows up when fraud protection is enabled

    * Prevented duplicated fields from showing up

    * Added tests

    * enqueued jquery in case it was not enqueued yet

    * Fixed double url encode

    * Fixed test linting errors

    * More linting fixes

    * Fixed version

    * Fixed sentense case

    * Changed the jetpack connection button to a link and removed unecessary js

    * Removed unused method

    * Removed duplicated logger

    * Moved the Jetpack connection functions to it's own class

    * Fixed the jetpack connection check

    * Refactored the JetpackConnectionManager class to use the JetpackConnection class methods

    * Fixed reverted changes during merge

    * Fixed setense case

    * Removed unecessary line setting the redirect_url again

    * made sure to init jetpack manager when calling get_authorization_url

    * Added dockbloc

    * Fixed connection check

    * Removed unecessary jetpack connection check

    * Fixed linting errors

    * Fixed from string

    * Removed handle_enqueue_admin_scripts test

    * Added since tag

    * Passed the errors array as a context to the log function intead of trying to access them directly

    * Fixed existing Manager instance check

    * Lint fixes

    * Fixed Manager instance check

    * Removed tests for inexistent method

diff --git a/plugins/woocommerce/includes/class-woocommerce.php b/plugins/woocommerce/includes/class-woocommerce.php
index 5cc81887ee..dc8595243f 100644
--- a/plugins/woocommerce/includes/class-woocommerce.php
+++ b/plugins/woocommerce/includes/class-woocommerce.php
@@ -376,6 +376,7 @@ final class WooCommerce {
 		$container->get( Automattic\WooCommerce\Internal\CostOfGoodsSold\CostOfGoodsSoldController::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\FraudProtection\FraudProtectionController::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\FraudProtection\CartEventTracker::class )->register();
+		$container->get( Automattic\WooCommerce\Internal\FraudProtection\AdminSettingsHandler::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\Admin\Settings\PaymentsController::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\Admin\Settings\PaymentsProviders\WooPayments\WooPaymentsController::class )->register();
 		$container->get( Automattic\WooCommerce\Internal\Utilities\LegacyRestApiStub::class )->register();
diff --git a/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php b/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php
index 8832995ec4..35310b361b 100644
--- a/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php
+++ b/plugins/woocommerce/src/Admin/API/OnboardingPlugins.php
@@ -8,11 +8,8 @@
 namespace Automattic\WooCommerce\Admin\API;

 defined( 'ABSPATH' ) || exit;
-
-use ActionScheduler;
-use Automattic\Jetpack\Connection\Manager;
-use Automattic\WooCommerce\Admin\Features\Features;
 use Automattic\WooCommerce\Admin\PluginsHelper;
+use Automattic\WooCommerce\Internal\Jetpack\JetpackConnection;
 use WC_REST_Data_Controller;
 use WP_Error;
 use WP_REST_Request;
@@ -221,7 +218,6 @@ class OnboardingPlugins extends WC_REST_Data_Controller {
 		return $response;
 	}

-
 	/**
 	 * Return Jetpack authorization URL.
 	 *
@@ -230,83 +226,10 @@ class OnboardingPlugins extends WC_REST_Data_Controller {
 	 * @return array
 	 */
 	public function get_jetpack_authorization_url( WP_REST_Request $request ) {
-		$manager = new Manager( 'woocommerce' );
-		$errors  = new WP_Error();
-
-		// Register the site to wp.com.
-		if ( ! $manager->is_connected() ) {
-			$result = $manager->try_registration();
-			if ( is_wp_error( $result ) ) {
-				$errors->add( $result->get_error_code(), $result->get_error_message() );
-			}
-		}
-
-		$redirect_url = $request->get_param( 'redirect_url' );
-		$calypso_env  = defined( 'WOOCOMMERCE_CALYPSO_ENVIRONMENT' ) && in_array( WOOCOMMERCE_CALYPSO_ENVIRONMENT, array( 'development', 'wpcalypso', 'horizon', 'stage' ), true ) ? WOOCOMMERCE_CALYPSO_ENVIRONMENT : 'production';
-
-		$authorization_url = $manager->get_authorization_url( null, $redirect_url );
-		$authorization_url = add_query_arg( 'locale', $this->get_wpcom_locale(), $authorization_url );
-
-		if ( Features::is_enabled( 'use-wp-horizon' ) ) {
-			$calypso_env = 'horizon';
-		}
-
-		$color_scheme = get_user_option( 'admin_color', get_current_user_id() );
-		if ( ! $color_scheme ) {
-			// The default Core color schema is 'fresh'.
-			$color_scheme = 'fresh';
-		}
-
-		return array(
-			'success'      => ! $errors->has_errors(),
-			'errors'       => $errors->get_error_messages(),
-			'color_scheme' => $color_scheme,
-			'url'          => add_query_arg(
-				array(
-					'from'        => $request->get_param( 'from' ),
-					'calypso_env' => $calypso_env,
-				),
-				$authorization_url,
-			),
-		);
-	}
-
-	/**
-	 * Return a locale string for wpcom.
-	 *
-	 * @return string
-	 */
-	private function get_wpcom_locale() {
-		// List of locales that should be used with region code.
-		$locale_to_lang = array(
-			'bre'   => 'br',
-			'de_AT' => 'de-at',
-			'de_CH' => 'de-ch',
-			'de'    => 'de_formal',
-			'el'    => 'el-po',
-			'en_GB' => 'en-gb',
-			'es_CL' => 'es-cl',
-			'es_MX' => 'es-mx',
-			'fr_BE' => 'fr-be',
-			'fr_CA' => 'fr-ca',
-			'nl_BE' => 'nl-be',
-			'nl'    => 'nl_formal',
-			'pt_BR' => 'pt-br',
-			'sr'    => 'sr_latin',
-			'zh_CN' => 'zh-cn',
-			'zh_HK' => 'zh-hk',
-			'zh_SG' => 'zh-sg',
-			'zh_TW' => 'zh-tw',
+		return JetpackConnection::get_authorization_url(
+			$request->get_param( 'redirect_url' ),
+			$request->get_param( 'from' )
 		);
-
-		$system_locale = get_locale();
-		if ( isset( $locale_to_lang[ $system_locale ] ) ) {
-			// Return the locale with region code if it's in the list.
-			return $locale_to_lang[ $system_locale ];
-		}
-
-		// If the locale is not in the list, return the language code only.
-		return explode( '_', $system_locale )[0];
 	}

 	/**
diff --git a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/ExperimentalShippingRecommendation.php b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/ExperimentalShippingRecommendation.php
index ec161ca74f..d609fef009 100644
--- a/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/ExperimentalShippingRecommendation.php
+++ b/plugins/woocommerce/src/Admin/Features/OnboardingTasks/Tasks/ExperimentalShippingRecommendation.php
@@ -6,6 +6,7 @@ use Automattic\Jetpack\Connection\Manager;
 use Automattic\WooCommerce\Admin\Features\Features;
 use Automattic\WooCommerce\Admin\Features\OnboardingTasks\Task;
 use Automattic\WooCommerce\Admin\PluginsHelper;
+use Automattic\WooCommerce\Internal\Jetpack\JetpackConnection;

 /**
  * Shipping Task
@@ -89,7 +90,7 @@ class ExperimentalShippingRecommendation extends Task {
 	 * @return bool
 	 */
 	public static function has_jetpack_connected() {
-		$jetpack_connection_manager = new Manager( 'woocommerce' );
+		$jetpack_connection_manager = JetpackConnection::get_manager();

 		return $jetpack_connection_manager->is_connected() && $jetpack_connection_manager->has_connected_owner();
 	}
diff --git a/plugins/woocommerce/src/Internal/Admin/Settings/Utils.php b/plugins/woocommerce/src/Internal/Admin/Settings/Utils.php
index a25ffee101..6900f35bf5 100644
--- a/plugins/woocommerce/src/Internal/Admin/Settings/Utils.php
+++ b/plugins/woocommerce/src/Internal/Admin/Settings/Utils.php
@@ -3,7 +3,7 @@ declare( strict_types=1 );

 namespace Automattic\WooCommerce\Internal\Admin\Settings;

-use Automattic\WooCommerce\Admin\API\OnboardingPlugins;
+use Automattic\WooCommerce\Internal\Jetpack\JetpackConnection;
 use WP_REST_Request;

 defined( 'ABSPATH' ) || exit;
@@ -443,11 +443,7 @@ class Utils {
 	 * }
 	 */
 	public static function get_wpcom_connection_authorization( string $return_url ): array {
-		$plugin_onboarding = new OnboardingPlugins();
-
-		$request = new WP_REST_Request();
-		$request->set_param( 'redirect_url', $return_url );
-		$result = $plugin_onboarding->get_jetpack_authorization_url( $request );
+		$result = JetpackConnection::get_authorization_url( $return_url );

 		if ( ! empty( $result['url'] ) ) {
 			$result['url'] = add_query_arg(
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/AdminSettingsHandler.php b/plugins/woocommerce/src/Internal/FraudProtection/AdminSettingsHandler.php
new file mode 100644
index 0000000000..d89c2d22ef
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/FraudProtection/AdminSettingsHandler.php
@@ -0,0 +1,158 @@
+<?php
+/**
+ * AdminSettingsHandler class file.
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\FraudProtection;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Handles admin settings for fraud protection.
+ *
+ * @since 10.5.0
+ */
+class AdminSettingsHandler {
+
+	/**
+	 * Jetpack connection manager instance.
+	 *
+	 * @var JetpackConnectionManager
+	 */
+	private $connection_manager;
+
+	/**
+	 * Register hooks.
+	 */
+	public function register(): void {
+		add_filter( 'woocommerce_get_settings_advanced', array( $this, 'add_jetpack_connection_field' ), 100, 2 );
+		add_action( 'woocommerce_admin_field_jetpack_connection', array( $this, 'handle_output_jetpack_connection_field' ), 10, 1 );
+	}
+
+
+	/**
+	 * Initialize the class with dependencies.
+	 *
+	 * @internal
+	 *
+	 * @param JetpackConnectionManager $connection_manager Jetpack connection manager instance.
+	 * @return void
+	 */
+	final public function init( JetpackConnectionManager $connection_manager ): void {
+		$this->connection_manager = $connection_manager;
+	}
+
+	/**
+	 * Add Jetpack connection field to fraud protection settings.
+	 *
+	 * @internal
+	 *
+	 * @param array  $settings Existing settings.
+	 * @param string $current_section Current section name.
+	 * @return array Modified settings.
+	 */
+	public function add_jetpack_connection_field( $settings, $current_section ): array {
+		// Only add on the features section.
+		if ( 'features' !== $current_section ) {
+			return $settings;
+		}
+
+		// Check if field already exists to prevent duplicates.
+		foreach ( $settings as $setting ) {
+			if ( isset( $setting['id'] ) && 'woocommerce_fraud_protection_jetpack_connection' === $setting['id'] ) {
+				return $settings;
+			}
+		}
+
+		// Find the fraud_protection field and add Jetpack connection field after it.
+		$new_settings = array();
+		foreach ( $settings as $setting ) {
+			$new_settings[] = $setting;
+
+			// Add Jetpack connection field after fraud_protection checkbox.
+			if ( isset( $setting['id'] ) && 'woocommerce_feature_fraud_protection_enabled' === $setting['id'] ) {
+				$new_settings[] = array(
+					'id'    => 'woocommerce_fraud_protection_jetpack_connection',
+					'type'  => 'jetpack_connection',
+					'title' => __( 'Jetpack Connection', 'woocommerce' ),
+					'desc'  => __( 'Connect your site to Jetpack to enable fraud protection features.', 'woocommerce' ),
+				);
+			}
+		}
+
+		return $new_settings;
+	}
+
+	/**
+	 * Output the Jetpack connection field.
+	 *
+	 * @internal
+	 *
+	 * @param array $value Field configuration.
+	 * @return void
+	 */
+	public function handle_output_jetpack_connection_field( $value ): void { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
+		// Only show Jetpack connection when fraud protection is enabled.
+		if ( 'yes' !== get_option( 'woocommerce_feature_fraud_protection_enabled', 'no' ) ) {
+			return;
+		}
+
+		$this->output_jetpack_connection_status();
+	}
+
+	/**
+	 * Output the Jetpack connection status and button.
+	 *
+	 * @internal
+	 *
+	 * @return void
+	 */
+	private function output_jetpack_connection_status(): void {
+		// Get connection status from connection manager.
+		$connection_status = $this->connection_manager->get_connection_status();
+		?>
+		<tr valign="top">
+			<th scope="row" class="titledesc">
+				<label><?php esc_html_e( 'Jetpack Connection', 'woocommerce' ); ?></label>
+			</th>
+			<td class="forminp forminp-button">
+				<?php if ( ! $connection_status['connected'] ) : ?>
+					<?php
+					// Get authorization URL for connecting.
+					$redirect_url   = admin_url( 'admin.php?page=wc-settings&tab=advanced&section=features' );
+					$connection_url = $this->connection_manager->get_authorization_url( $redirect_url );
+
+					// If we couldn't get authorization URL, show error message.
+					if ( ! $connection_url ) :
+						?>
+						<p class="description" style="color: #dc3232;">
+							<?php echo esc_html( $connection_status['error'] ); ?>
+						</p>
+					<?php else : ?>
+						<a href="<?php echo esc_url( $connection_url ); ?>" class="button button-secondary jetpack_connection_button">
+							<?php esc_html_e( 'Connect to Jetpack', 'woocommerce' ); ?>
+						</a>
+						<p class="description">
+							<?php esc_html_e( 'Connect your site to Jetpack to enable fraud protection features.', 'woocommerce' ); ?>
+						</p>
+					<?php endif; ?>
+				<?php else : ?>
+					<span class="dashicons dashicons-yes-alt" style="color: #46b450;"></span>
+					<span><?php esc_html_e( 'Connected to Jetpack', 'woocommerce' ); ?></span>
+					<p class="description">
+						<?php
+						printf(
+							/* translators: %d: Blog ID */
+							esc_html__( 'Site ID: %d', 'woocommerce' ),
+							(int) $connection_status['blog_id']
+						);
+						?>
+					</p>
+				<?php endif; ?>
+			</td>
+		</tr>
+		<?php
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionController.php b/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionController.php
index a9d7e36747..3bb1ef72c0 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionController.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/FraudProtectionController.php
@@ -30,21 +30,31 @@ class FraudProtectionController implements RegisterHooksInterface {
 	 */
 	private FeaturesController $features_controller;

+	/**
+	 * Jetpack connection manager instance.
+	 *
+	 * @var JetpackConnectionManager
+	 */
+	private JetpackConnectionManager $connection_manager;
+
 	/**
 	 * Register hooks.
 	 */
 	public function register(): void {
 		add_action( 'init', array( $this, 'on_init' ) );
+		add_action( 'admin_notices', array( $this, 'on_admin_notices' ) );
 	}

 	/**
 	 * Initialize the instance, runs when the instance is created by the dependency injection container.
 	 *
 	 * @internal
-	 * @param FeaturesController $features_controller The instance of FeaturesController to use.
+	 * @param FeaturesController       $features_controller The instance of FeaturesController to use.
+	 * @param JetpackConnectionManager $connection_manager  The instance of JetpackConnectionManager to use.
 	 */
-	final public function init( FeaturesController $features_controller ): void {
+	final public function init( FeaturesController $features_controller, JetpackConnectionManager $connection_manager ): void {
 		$this->features_controller = $features_controller;
+		$this->connection_manager  = $connection_manager;
 	}

 	/**
@@ -62,6 +72,49 @@ class FraudProtectionController implements RegisterHooksInterface {
 		// For now, this is a placeholder for the infrastructure.
 	}

+	/**
+	 * Display admin notice when Jetpack connection is not available.
+	 *
+	 * @internal
+	 */
+	public function on_admin_notices(): void {
+		// Only show if feature is enabled.
+		if ( ! $this->feature_is_enabled() ) {
+			return;
+		}
+
+		// Only show on WooCommerce settings page.
+		$screen = get_current_screen();
+		if ( ! $screen || 'woocommerce_page_wc-settings' !== $screen->id ) {
+			return;
+		}
+
+		$connection_status = $this->connection_manager->get_connection_status();
+		if ( $connection_status['connected'] ) {
+			return;
+		}
+
+		$settings_url = admin_url( 'admin.php?page=wc-settings&tab=advanced&section=features' );
+
+		?>
+		<div class="notice notice-warning is-dismissible">
+			<p>
+				<strong><?php esc_html_e( 'Fraud protection warning:', 'woocommerce' ); ?></strong>
+				<?php echo esc_html( $connection_status['error'] ); ?>
+			</p>
+			<p>
+				<?php
+				printf(
+					/* translators: %s: Settings page URL */
+					wp_kses_post( __( 'Fraud protection will fail open and allow all sessions until connected. <a href="%s">Connect to Jetpack</a>', 'woocommerce' ) ),
+					esc_url( $settings_url )
+				);
+				?>
+			</p>
+		</div>
+		<?php
+	}
+
 	/**
 	 * Check if fraud protection feature is enabled.
 	 *
diff --git a/plugins/woocommerce/src/Internal/FraudProtection/JetpackConnectionManager.php b/plugins/woocommerce/src/Internal/FraudProtection/JetpackConnectionManager.php
new file mode 100644
index 0000000000..e3e904bd5a
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/FraudProtection/JetpackConnectionManager.php
@@ -0,0 +1,105 @@
+<?php
+/**
+ * JetpackConnectionManager class file.
+ */
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\FraudProtection;
+
+use Automattic\WooCommerce\Internal\Jetpack\JetpackConnection;
+
+defined( 'ABSPATH' ) || exit;
+
+/**
+ * Manages Jetpack connection status and validation for fraud protection.
+ *
+ * Provides centralized methods to check connection status, validate requirements,
+ * and handle connection-related errors gracefully.
+ *
+ * @since 10.5.0
+ */
+class JetpackConnectionManager {
+
+	/**
+	 * Get the Jetpack blog ID.
+	 *
+	 * @return int|null Blog ID if available, null otherwise.
+	 */
+	public function get_blog_id(): ?int {
+		// Get blog ID from Jetpack options.
+		$blog_id = \Jetpack_Options::get_option( 'id' );
+
+		return $blog_id ? (int) $blog_id : null;
+	}
+
+	/**
+	 * Get connection status with detailed error information.
+	 *
+	 * Returns an array with connection status and any error details.
+	 *
+	 * @return array {
+	 *     Connection status information.
+	 *
+	 *     @type bool   $connected    Whether the site is connected.
+	 *     @type string $error        Error message if not connected.
+	 *     @type string $error_code   Error code if not connected.
+	 *     @type int    $blog_id      Blog ID if available.
+	 * }
+	 */
+	public function get_connection_status(): array {
+		$status = array(
+			'connected'  => false,
+			'error'      => '',
+			'error_code' => '',
+			'blog_id'    => null,
+		);
+
+		// Check if connected.
+		if ( ! JetpackConnection::get_manager()->is_connected() ) {
+			$status['error']      = __( 'Site is not connected to WordPress.com. Please connect your site to enable fraud protection.', 'woocommerce' );
+			$status['error_code'] = 'not_connected';
+			return $status;
+		}
+
+		// Get blog ID.
+		$blog_id = $this->get_blog_id();
+		if ( ! $blog_id ) {
+			$status['error']      = __( 'Jetpack blog ID not found. Please reconnect your site to WordPress.com.', 'woocommerce' );
+			$status['error_code'] = 'no_blog_id';
+			return $status;
+		}
+
+		// All checks passed.
+		$status['connected'] = true;
+		$status['blog_id']   = $blog_id;
+
+		return $status;
+	}
+
+	/**
+	 * Get the Jetpack authorization URL for connecting the site.
+	 *
+	 * @param string $redirect_url URL to redirect to after authorization.
+	 * @return string|null Authorization URL or null on error.
+	 */
+	public function get_authorization_url( string $redirect_url = '' ): ?string {
+		// If no redirect URL provided, use current admin URL.
+		if ( empty( $redirect_url ) ) {
+			$redirect_url = admin_url( 'admin.php?page=wc-settings&tab=advanced&section=features' );
+		}
+
+		$authorization_data = JetpackConnection::get_authorization_url( $redirect_url, 'woocommerce-fraud-protection' );
+
+		if ( ! $authorization_data['success'] ) {
+			FraudProtectionController::log(
+				'error',
+				'Failed to get Jetpack authorization URL.',
+				$authorization_data['errors']
+			);
+			return null;
+		}
+
+		return $authorization_data['url'];
+	}
+}
diff --git a/plugins/woocommerce/src/Internal/Jetpack/JetpackConnection.php b/plugins/woocommerce/src/Internal/Jetpack/JetpackConnection.php
new file mode 100644
index 0000000000..29c4c877f4
--- /dev/null
+++ b/plugins/woocommerce/src/Internal/Jetpack/JetpackConnection.php
@@ -0,0 +1,129 @@
+<?php
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Internal\Jetpack;
+
+use Automattic\Jetpack\Connection\Manager;
+use Automattic\WooCommerce\Admin\Features\Features;
+use WP_Error;
+
+/**
+ * Jetpack Connection wrapper class.
+ *
+ * @since 8.3.0
+ */
+class JetpackConnection {
+	/**
+	 * Jetpack connection manager.
+	 *
+	 * @var Manager
+	 */
+	private static $manager;
+
+	/**
+	 * Get the Jetpack connection manager.
+	 *
+	 * @return Manager
+	 */
+	public static function get_manager() {
+		if ( ! self::$manager instanceof Manager ) {
+			self::$manager = new Manager( 'woocommerce' );
+		}
+
+		return self::$manager;
+	}
+
+	/**
+	 * Get the authorization URL for the Jetpack connection.
+	 *
+	 * @param mixed  $redirect_url Redirect URL.
+	 * @param string $from         From parameter.
+	 *
+	 * @return array {
+	 *     Authorization data.
+	 *
+	 *     @type bool   $success      Whether authorization URL generation succeeded.
+	 *     @type array  $errors       Array of error messages if any.
+	 *     @type string $color_scheme User's admin color scheme.
+	 *     @type string $url          The authorization URL.
+	 * }
+	 */
+	public static function get_authorization_url( $redirect_url, $from = '' ) {
+		$manager = self::get_manager();
+		$errors  = new WP_Error();
+
+		// Register the site to wp.com.
+		if ( ! $manager->is_connected() ) {
+			$result = $manager->try_registration();
+			if ( is_wp_error( $result ) ) {
+				$errors->add( $result->get_error_code(), $result->get_error_message() );
+			}
+		}
+
+		$calypso_env = defined( 'WOOCOMMERCE_CALYPSO_ENVIRONMENT' ) && in_array( WOOCOMMERCE_CALYPSO_ENVIRONMENT, array( 'development', 'wpcalypso', 'horizon', 'stage' ), true ) ? WOOCOMMERCE_CALYPSO_ENVIRONMENT : 'production';
+
+		$authorization_url = $manager->get_authorization_url( null, $redirect_url );
+		$authorization_url = add_query_arg( 'locale', self::get_wpcom_locale(), $authorization_url );
+
+		if ( Features::is_enabled( 'use-wp-horizon' ) ) {
+			$calypso_env = 'horizon';
+		}
+
+		$color_scheme = get_user_option( 'admin_color', get_current_user_id() );
+		if ( ! $color_scheme ) {
+			// The default Core color schema is 'fresh'.
+			$color_scheme = 'fresh';
+		}
+
+		return array(
+			'success'      => ! $errors->has_errors(),
+			'errors'       => $errors->get_error_messages(),
+			'color_scheme' => $color_scheme,
+			'url'          => add_query_arg(
+				array(
+					'from'        => $from,
+					'calypso_env' => $calypso_env,
+				),
+				$authorization_url,
+			),
+		);
+	}
+
+	/**
+	 * Return a locale string for wpcom.
+	 *
+	 * @return string
+	 */
+	private static function get_wpcom_locale() {
+		// List of locales that should be used with region code.
+		$locale_to_lang = array(
+			'bre'   => 'br',
+			'de_AT' => 'de-at',
+			'de_CH' => 'de-ch',
+			'de'    => 'de_formal',
+			'el'    => 'el-po',
+			'en_GB' => 'en-gb',
+			'es_CL' => 'es-cl',
+			'es_MX' => 'es-mx',
+			'fr_BE' => 'fr-be',
+			'fr_CA' => 'fr-ca',
+			'nl_BE' => 'nl-be',
+			'nl'    => 'nl_formal',
+			'pt_BR' => 'pt-br',
+			'sr'    => 'sr_latin',
+			'zh_CN' => 'zh-cn',
+			'zh_HK' => 'zh-hk',
+			'zh_SG' => 'zh-sg',
+			'zh_TW' => 'zh-tw',
+		);
+
+		$system_locale = get_locale();
+		if ( isset( $locale_to_lang[ $system_locale ] ) ) {
+			// Return the locale with region code if it's in the list.
+			return $locale_to_lang[ $system_locale ];
+		}
+
+		// If the locale is not in the list, return the language code only.
+		return explode( '_', $system_locale )[0];
+	}
+}
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/AdminSettingsHandlerTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/AdminSettingsHandlerTest.php
new file mode 100644
index 0000000000..f7256a7fc6
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/AdminSettingsHandlerTest.php
@@ -0,0 +1,283 @@
+<?php
+
+declare( strict_types=1 );
+
+namespace Automattic\WooCommerce\Tests\Internal\FraudProtection;
+
+use Automattic\WooCommerce\Internal\FraudProtection\AdminSettingsHandler;
+use Automattic\WooCommerce\Internal\FraudProtection\JetpackConnectionManager;
+
+/**
+ * Tests for the AdminSettingsHandler class.
+ */
+class AdminSettingsHandlerTest extends \WC_Unit_Test_Case {
+
+	/**
+	 * System under test.
+	 *
+	 * @var AdminSettingsHandler
+	 */
+	private $sut;
+
+	/**
+	 * Mock Jetpack connection manager.
+	 *
+	 * @var JetpackConnectionManager|\PHPUnit\Framework\MockObject\MockObject
+	 */
+	private $connection_manager_mock;
+
+	/**
+	 * Setup test.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		// Create mock connection manager.
+		$this->connection_manager_mock = $this->getMockBuilder( JetpackConnectionManager::class )
+			->disableOriginalConstructor()
+			->getMock();
+
+		// Get fresh instance from container.
+		$this->sut = wc_get_container()->get( AdminSettingsHandler::class );
+		$this->sut->init( $this->connection_manager_mock );
+	}
+
+	/**
+	 * Test that register method registers the expected hooks.
+	 */
+	public function test_register_registers_hooks(): void {
+		$this->sut->register();
+
+		// Check if the settings filter is registered.
+		$priority = has_filter( 'woocommerce_get_settings_advanced', array( $this->sut, 'add_jetpack_connection_field' ) );
+		$this->assertSame( 100, $priority, 'Settings filter should be registered with priority 100' );
+
+		// Check if the admin field action is registered.
+		$priority = has_action( 'woocommerce_admin_field_jetpack_connection', array( $this->sut, 'handle_output_jetpack_connection_field' ) );
+		$this->assertSame( 10, $priority, 'Admin field action should be registered with priority 10' );
+	}
+
+	/**
+	 * Test that add_jetpack_connection_field returns settings unchanged on non-features section.
+	 */
+	public function test_add_jetpack_connection_field_returns_unchanged_on_non_features_section(): void {
+		$this->sut->register();
+
+		$settings        = array(
+			array(
+				'id'   => 'some_setting',
+				'type' => 'text',
+			),
+		);
+		$current_section = 'general';
+		$result          = $this->sut->add_jetpack_connection_field( $settings, $current_section );
+
+		$this->assertSame( $settings, $result, 'Settings should be unchanged on non-features section' );
+	}
+
+	/**
+	 * Test that add_jetpack_connection_field adds field after fraud_protection on features section.
+	 */
+	public function test_add_jetpack_connection_field_adds_field_after_fraud_protection(): void {
+		$this->sut->register();
+
+		$settings = array(
+			array(
+				'id'   => 'some_other_feature',
+				'type' => 'checkbox',
+			),
+			array(
+				'id'   => 'woocommerce_feature_fraud_protection_enabled',
+				'type' => 'checkbox',
+			),
+			array(
+				'id'   => 'another_feature',
+				'type' => 'checkbox',
+			),
+		);
+
+		$result = $this->sut->add_jetpack_connection_field( $settings, 'features' );
+
+		// Should have one more setting.
+		$this->assertCount( 4, $result );
+
+		// The new field should be added after fraud_protection.
+		$this->assertSame( 'woocommerce_feature_fraud_protection_enabled', $result[1]['id'] );
+		$this->assertSame( 'woocommerce_fraud_protection_jetpack_connection', $result[2]['id'] );
+		$this->assertSame( 'jetpack_connection', $result[2]['type'] );
+		$this->assertSame( 'another_feature', $result[3]['id'] );
+	}
+
+	/**
+	 * Test that add_jetpack_connection_field doesn't add duplicate field.
+	 */
+	public function test_add_jetpack_connection_field_doesnt_duplicate(): void {
+		$this->sut->register();
+
+		$settings = array(
+			array(
+				'id'   => 'woocommerce_feature_fraud_protection_enabled',
+				'type' => 'checkbox',
+			),
+		);
+
+		// Call twice to check it doesn't duplicate.
+		$result1 = $this->sut->add_jetpack_connection_field( $settings, 'features' );
+		$result2 = $this->sut->add_jetpack_connection_field( $result1, 'features' );
+
+		// Should still only have 2 settings (fraud_protection + 1 jetpack_connection).
+		$this->assertCount( 2, $result2 );
+	}
+
+	/**
+	 * Test that handle_output_jetpack_connection_field does nothing when fraud protection disabled.
+	 */
+	public function test_handle_output_jetpack_connection_field_returns_early_when_disabled(): void {
+		// Disable fraud protection.
+		update_option( 'woocommerce_feature_fraud_protection_enabled', 'no' );
+
+		$this->sut->register();
+
+		// Mock connection manager should not be called.
+		$this->connection_manager_mock->expects( $this->never() )
+			->method( 'get_connection_status' );
+
+		// Capture output.
+		ob_start();
+		$this->sut->handle_output_jetpack_connection_field( array() );
+		$output = ob_get_clean();
+
+		// Should produce no output.
+		$this->assertEmpty( $output );
+	}
+
+	/**
+	 * Test that handle_output_jetpack_connection_field shows button when not connected.
+	 */
+	public function test_handle_output_jetpack_connection_field_shows_button_when_not_connected(): void {
+		// Enable fraud protection.
+		update_option( 'woocommerce_feature_fraud_protection_enabled', 'yes' );
+
+		$this->sut->register();
+
+		// Mock connection status - not connected.
+		$this->connection_manager_mock->expects( $this->once() )
+			->method( 'get_connection_status' )
+			->willReturn(
+				array(
+					'connected'  => false,
+					'error'      => 'Not connected',
+					'error_code' => 'not_connected',
+					'blog_id'    => null,
+				)
+			);
+
+		// Mock authorization URL.
+		$this->connection_manager_mock->expects( $this->once() )
+			->method( 'get_authorization_url' )
+			->willReturn( 'https://example.com/connect' );
+
+		// Capture output.
+		ob_start();
+		$this->sut->handle_output_jetpack_connection_field( array() );
+		$output = ob_get_clean();
+
+		// Should contain Connect button.
+		$this->assertStringContainsString( 'Connect to Jetpack', $output );
+		$this->assertStringContainsString( 'jetpack_connection_button', $output );
+		$this->assertStringContainsString( 'https://example.com/connect', $output );
+	}
+
+	/**
+	 * Test that handle_output_jetpack_connection_field shows connected status when connected.
+	 */
+	public function test_handle_output_jetpack_connection_field_shows_connected_status(): void {
+		// Enable fraud protection.
+		update_option( 'woocommerce_feature_fraud_protection_enabled', 'yes' );
+
+		$this->sut->register();
+
+		// Mock connection status - connected.
+		$this->connection_manager_mock->expects( $this->once() )
+			->method( 'get_connection_status' )
+			->willReturn(
+				array(
+					'connected'  => true,
+					'error'      => '',
+					'error_code' => '',
+					'blog_id'    => 12345,
+				)
+			);
+
+		// Should not call get_authorization_url when connected.
+		$this->connection_manager_mock->expects( $this->never() )
+			->method( 'get_authorization_url' );
+
+		// Capture output.
+		ob_start();
+		$this->sut->handle_output_jetpack_connection_field( array() );
+		$output = ob_get_clean();
+
+		// Should show connected status.
+		$this->assertStringContainsString( 'Connected to Jetpack', $output );
+		$this->assertStringContainsString( 'Site ID: 12345', $output );
+		$this->assertStringContainsString( 'dashicons-yes-alt', $output );
+	}
+
+	/**
+	 * Test that handle_output_jetpack_connection_field shows error when authorization URL fails.
+	 */
+	public function test_handle_output_jetpack_connection_field_shows_error_when_url_fails(): void {
+		// Enable fraud protection.
+		update_option( 'woocommerce_feature_fraud_protection_enabled', 'yes' );
+
+		$this->sut->register();
+
+		// Mock connection status - not connected.
+		$this->connection_manager_mock->expects( $this->once() )
+			->method( 'get_connection_status' )
+			->willReturn(
+				array(
+					'connected'  => false,
+					'error'      => 'Jetpack not available',
+					'error_code' => 'jetpack_not_available',
+					'blog_id'    => null,
+				)
+			);
+
+		// Mock authorization URL - returns null (failure).
+		$this->connection_manager_mock->expects( $this->once() )
+			->method( 'get_authorization_url' )
+			->willReturn( null );
+
+		// Capture output.
+		ob_start();
+		$this->sut->handle_output_jetpack_connection_field( array() );
+		$output = ob_get_clean();
+
+		// Should show error message.
+		$this->assertStringContainsString( 'Jetpack not available', $output );
+		$this->assertStringNotContainsString( 'Connect to Jetpack', $output );
+	}
+
+	/**
+	 * Cleanup after test.
+	 */
+	public function tearDown(): void {
+		parent::tearDown();
+
+		// Clean up options.
+		delete_option( 'woocommerce_feature_fraud_protection_enabled' );
+
+		// Remove hooks.
+		remove_all_filters( 'woocommerce_get_settings_advanced' );
+		remove_all_actions( 'woocommerce_admin_field_jetpack_connection' );
+		remove_all_actions( 'admin_enqueue_scripts' );
+
+		// Reset container.
+		wc_get_container()->reset_all_resolved();
+
+		// Clean up $_GET.
+		unset( $_GET['section'] );
+	}
+}