Commit 7f485054a9 for woocommerce

commit 7f485054a9a62aa92fcd185052cf1b1f44cc74b0
Author: Leonardo Lopes de Albuquerque <leonardo.albuquerque@automattic.com>
Date:   Sun Jan 11 12:04:14 2026 -0300

    Fraud Protection: Display blocked notice on add payment method page  (#62757)

    * Fraud Protection: Display blocked notice on add payment method page

    Add user-friendly messaging to the My Account add payment method page
    for blocked sessions. The page was already protected (no gateways available)
    by https://github.com/woocommerce/woocommerce/pull/62691 but lacked an
    explanation for the user.

    * Add generic session blocked message to display on payment methods page

    ---------

    Co-authored-by: Luiz Reis <luiz.reis@automattic.com>

diff --git a/plugins/woocommerce/src/Internal/FraudProtection/BlockedSessionNotice.php b/plugins/woocommerce/src/Internal/FraudProtection/BlockedSessionNotice.php
index 317ea1c980..02f9e50545 100644
--- a/plugins/woocommerce/src/Internal/FraudProtection/BlockedSessionNotice.php
+++ b/plugins/woocommerce/src/Internal/FraudProtection/BlockedSessionNotice.php
@@ -52,20 +52,39 @@ class BlockedSessionNotice implements RegisterHooksInterface {
 	 * @return void
 	 */
 	public function register(): void {
-		add_action( 'woocommerce_before_checkout_form', array( $this, 'display_blocked_notice' ), 1, 0 );
+		add_action( 'woocommerce_before_checkout_form', array( $this, 'display_checkout_blocked_notice' ), 1, 0 );
+		add_action( 'before_woocommerce_add_payment_method', array( $this, 'display_generic_blocked_notice' ), 1, 0 );
 	}

 	/**
 	 * Display blocked notice on shortcode checkout page.
 	 *
-	 * Shows a user-friendly message explaining that the request cannot be
+	 * Shows a checkout-specific message explaining that the purchase cannot be
+	 * completed online and provides contact information for support.
+	 *
+	 * @internal
+	 *
+	 * @return void
+	 */
+	public function display_checkout_blocked_notice(): void {
+		if ( ! $this->session_manager->is_session_blocked() ) {
+			return;
+		}
+
+		wc_print_notice( $this->get_message_html( 'checkout' ), 'error' );
+	}
+
+	/**
+	 * Display blocked notice for non-checkout pages.
+	 *
+	 * Shows a generic message explaining that the request cannot be
 	 * processed online and provides contact information for support.
 	 *
 	 * @internal
 	 *
 	 * @return void
 	 */
-	public function display_blocked_notice(): void {
+	public function display_generic_blocked_notice(): void {
 		if ( ! $this->session_manager->is_session_blocked() ) {
 			return;
 		}
@@ -76,16 +95,26 @@ class BlockedSessionNotice implements RegisterHooksInterface {
 	/**
 	 * Get the blocked session message as HTML.
 	 *
-	 * Includes a mailto link for the support email. Used by shortcode checkout.
+	 * Includes a mailto link for the support email.
 	 *
+	 * @param string $context Message context: 'checkout' for purchase-specific message, 'generic' for general use.
 	 * @return string HTML message with mailto link.
 	 */
-	public function get_message_html(): string {
+	public function get_message_html( string $context = 'generic' ): string {
 		$email = WC()->mailer()->get_from_address();

+		if ( 'checkout' === $context ) {
+			return sprintf(
+				/* translators: %1$s: mailto link, %2$s: email address */
+				__( 'We are unable to process this request online. Please <a href="%1$s">contact support (%2$s)</a> to complete your purchase.', 'woocommerce' ),
+				esc_url( 'mailto:' . $email ),
+				esc_html( $email )
+			);
+		}
+
 		return sprintf(
 			/* translators: %1$s: mailto link, %2$s: email address */
-			__( 'We are unable to process this request online. Please <a href="%1$s">contact support (%2$s)</a> to complete your purchase.', 'woocommerce' ),
+			__( 'We are unable to process this request online. Please <a href="%1$s">contact support (%2$s)</a> for assistance.', 'woocommerce' ),
 			esc_url( 'mailto:' . $email ),
 			esc_html( $email )
 		);
@@ -96,14 +125,23 @@ class BlockedSessionNotice implements RegisterHooksInterface {
 	 *
 	 * Used by Store API responses where HTML is not supported.
 	 *
+	 * @param string $context Message context: 'checkout' for purchase-specific message, 'generic' for general use.
 	 * @return string Plaintext message with email address.
 	 */
-	public function get_message_plaintext(): string {
+	public function get_message_plaintext( string $context = 'generic' ): string {
 		$email = WC()->mailer()->get_from_address();

+		if ( 'checkout' === $context ) {
+			return sprintf(
+				/* translators: %s: support email address */
+				__( 'We are unable to process this request online. Please contact support (%s) to complete your purchase.', 'woocommerce' ),
+				$email
+			);
+		}
+
 		return sprintf(
 			/* translators: %s: support email address */
-			__( 'We are unable to process this request online. Please contact support (%s) to complete your purchase.', 'woocommerce' ),
+			__( 'We are unable to process this request online. Please contact support (%s) for assistance.', 'woocommerce' ),
 			$email
 		);
 	}
diff --git a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
index d488769982..cc82c98bd7 100644
--- a/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
+++ b/plugins/woocommerce/src/StoreApi/Routes/V1/Checkout.php
@@ -161,7 +161,7 @@ class Checkout extends AbstractCartRoute {
 			&& wc_get_container()->get( SessionClearanceManager::class )->is_session_blocked() ) {
 			$response = $this->get_route_error_response(
 				'woocommerce_rest_checkout_error',
-				wc_get_container()->get( BlockedSessionNotice::class )->get_message_plaintext(),
+				wc_get_container()->get( BlockedSessionNotice::class )->get_message_plaintext( 'checkout' ),
 				403
 			);
 		}
diff --git a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
index 10e5628bf6..9aa32670d8 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/StoreApi/Routes/Checkout.php
@@ -1839,5 +1839,6 @@ class Checkout extends MockeryTestCase {
 		$this->assertEquals( 403, $response->get_status(), 'Should return 403 when session is blocked' );
 		$this->assertEquals( 'woocommerce_rest_checkout_error', $response->get_data()['code'] );
 		$this->assertStringContainsString( 'unable to process this request online', $response->get_data()['message'] );
+		$this->assertStringContainsString( 'to complete your purchase', $response->get_data()['message'], 'Should use checkout-specific message' );
 	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlockedSessionNoticeTest.php b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlockedSessionNoticeTest.php
index 33452e182b..2cc710b7b4 100644
--- a/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlockedSessionNoticeTest.php
+++ b/plugins/woocommerce/tests/php/src/Internal/FraudProtection/BlockedSessionNoticeTest.php
@@ -53,11 +53,12 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
 	public function tearDown(): void {
 		parent::tearDown();
 		remove_all_actions( 'woocommerce_before_checkout_form' );
+		remove_all_actions( 'before_woocommerce_add_payment_method' );
 		delete_option( 'woocommerce_email_from_address' );
 	}

 	/**
-	 * @testdox Should display error notice when woocommerce_before_checkout_form action fires for blocked sessions.
+	 * @testdox Should display checkout-specific error notice when woocommerce_before_checkout_form action fires for blocked sessions.
 	 */
 	public function test_checkout_action_displays_blocked_message(): void {
 		$this->mock_session_manager->method( 'is_session_blocked' )->willReturn( true );
@@ -67,6 +68,7 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
 		$output = ob_get_clean();

 		$this->assertStringContainsString( 'unable to process this request online', $output, 'Should display blocked message on checkout' );
+		$this->assertStringContainsString( 'to complete your purchase', $output, 'Should display checkout-specific message' );
 		$this->assertStringContainsString( 'support@example.com', $output, 'Should include support email in message' );
 		$this->assertStringContainsString( 'mailto:support@example.com', $output, 'Should include mailto link' );
 	}
@@ -85,10 +87,39 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
 	}

 	/**
-	 * @testdox get_message_html should return the expected HTML message with mailto link.
+	 * @testdox Should display generic error notice when before_woocommerce_add_payment_method action fires for blocked sessions.
 	 */
-	public function test_get_message_html(): void {
-		$message = $this->sut->get_message_html();
+	public function test_add_payment_method_action_displays_blocked_message(): void {
+		$this->mock_session_manager->method( 'is_session_blocked' )->willReturn( true );
+
+		ob_start();
+		do_action( 'before_woocommerce_add_payment_method' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+		$output = ob_get_clean();
+
+		$this->assertStringContainsString( 'unable to process this request online', $output, 'Should display blocked message on add payment method page' );
+		$this->assertStringContainsString( 'for assistance', $output, 'Should display generic message' );
+		$this->assertStringContainsString( 'support@example.com', $output, 'Should include support email in message' );
+		$this->assertStringContainsString( 'mailto:support@example.com', $output, 'Should include mailto link' );
+	}
+
+	/**
+	 * @testdox Should not display message when add payment method action fires for non-blocked sessions.
+	 */
+	public function test_add_payment_method_action_no_message_for_non_blocked_session(): void {
+		$this->mock_session_manager->method( 'is_session_blocked' )->willReturn( false );
+
+		ob_start();
+		do_action( 'before_woocommerce_add_payment_method' ); // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
+		$output = ob_get_clean();
+
+		$this->assertEmpty( $output, 'Non-blocked sessions should not display any message' );
+	}
+
+	/**
+	 * @testdox get_message_html should return checkout-specific message when context is 'checkout'.
+	 */
+	public function test_get_message_html_checkout_context(): void {
+		$message = $this->sut->get_message_html( 'checkout' );

 		$this->assertEquals(
 			'We are unable to process this request online. Please <a href="mailto:support@example.com">contact support (support@example.com)</a> to complete your purchase.',
@@ -97,14 +128,40 @@ class BlockedSessionNoticeTest extends \WC_Unit_Test_Case {
 	}

 	/**
-	 * @testdox get_message_plaintext should return the expected plaintext message.
+	 * @testdox get_message_html should return generic message when context is 'generic' or not specified.
+	 */
+	public function test_get_message_html_generic_context(): void {
+		$message_default  = $this->sut->get_message_html();
+		$message_explicit = $this->sut->get_message_html( 'generic' );
+
+		$expected = 'We are unable to process this request online. Please <a href="mailto:support@example.com">contact support (support@example.com)</a> for assistance.';
+
+		$this->assertEquals( $expected, $message_default, 'Default context should return generic message' );
+		$this->assertEquals( $expected, $message_explicit, 'Explicit generic context should return generic message' );
+	}
+
+	/**
+	 * @testdox get_message_plaintext should return checkout-specific message when context is 'checkout'.
 	 */
-	public function test_get_message_plaintext(): void {
-		$message = $this->sut->get_message_plaintext();
+	public function test_get_message_plaintext_checkout_context(): void {
+		$message = $this->sut->get_message_plaintext( 'checkout' );

 		$this->assertEquals(
 			'We are unable to process this request online. Please contact support (support@example.com) to complete your purchase.',
 			$message
 		);
 	}
+
+	/**
+	 * @testdox get_message_plaintext should return generic message when context is 'generic' or not specified.
+	 */
+	public function test_get_message_plaintext_generic_context(): void {
+		$message_default  = $this->sut->get_message_plaintext();
+		$message_explicit = $this->sut->get_message_plaintext( 'generic' );
+
+		$expected = 'We are unable to process this request online. Please contact support (support@example.com) for assistance.';
+
+		$this->assertEquals( $expected, $message_default, 'Default context should return generic message' );
+		$this->assertEquals( $expected, $message_explicit, 'Explicit generic context should return generic message' );
+	}
 }