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§ion=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§ion=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§ion=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'] );
+ }
+}