Commit 771516e98cd for woocommerce

commit 771516e98cd3ecf2303c3a3bfbe90ade220ddd73
Author: Albert Juhé Lluveras <contact@albertjuhe.com>
Date:   Wed Aug 5 07:56:27 2026 +0200

    Return early in Product block if product is not viewable (#67332)

    * Return early in Product block if product is not viewable

    * Add changelog

    * Fixes

    * Add tests

    * PHPStan

    * Avoid regular expression in tests

    * Fix tests

    * Make it so the password form redirects to the current page instead of the product page

diff --git a/plugins/woocommerce/changelog/fix-WOO6-93 b/plugins/woocommerce/changelog/fix-WOO6-93
new file mode 100644
index 00000000000..24ba76d362c
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-WOO6-93
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Return early in Product block if product is not viewable
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/SingleProduct.php b/plugins/woocommerce/src/Blocks/BlockTypes/SingleProduct.php
index 7db69b6efa2..ccdb0e872ed 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/SingleProduct.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/SingleProduct.php
@@ -184,18 +184,40 @@ class SingleProduct extends AbstractBlock {
 	protected function render( $attributes, $content, $block ) {
 		$product = wc_get_product( $block->context['postId'] );

-		if ( ! $product instanceof \WC_Product ) {
+		if (
+			! $product instanceof \WC_Product ||
+			! $product->is_viewable()
+		) {
 			return '';
 		}

+		$product_id = $product->get_id();
+
+		if ( post_password_required( $product_id ) ) {
+			$password_form = get_the_password_form( $product_id );
+			$html          = new \WP_HTML_Tag_Processor( $password_form );
+			$current_url   = home_url( add_query_arg( null, null ) );
+
+			while ( $html->next_tag( array( 'tag_name' => 'input' ) ) ) {
+				if ( 'redirect_to' !== $html->get_attribute( 'name' ) ) {
+					continue;
+				}
+
+				$html->set_attribute( 'value', $current_url );
+				break;
+			}
+
+			return $html->get_updated_html();
+		}
+
 		// Load product into the shared products store.
 		wc_interactivity_api_load_product(
 			'I acknowledge that using experimental APIs means my theme or plugin will inevitably break in the next version of WooCommerce',
-			$product->get_id()
+			$product_id
 		);

 		$interactivity_context = array(
-			'productId'   => $product->get_id(),
+			'productId'   => $product_id,
 			'variationId' => null,
 		);

diff --git a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/SingleProduct.php b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/SingleProduct.php
index 548eaf58200..5d3a0d2be2a 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/SingleProduct.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/BlockTypes/SingleProduct.php
@@ -147,4 +147,70 @@ class SingleProduct extends \WP_UnitTestCase {
 			$this->delete_product_with_gallery_attachments( $data );
 		}
 	}
+
+	/**
+	 * @testdox Password-protected products render the password form instead of product content.
+	 */
+	public function test_password_protected_product_renders_password_form() {
+		$product = WC_Helper_Product::create_simple_product();
+		$product->set_post_password( 'secret' );
+		$product->save();
+
+		try {
+			$markup = do_blocks(
+				sprintf(
+					'<!-- wp:woocommerce/single-product {"productId":%d} -->
+<div class="wp-block-woocommerce-single-product"><p>VISIBLE_PRODUCT_CONTENT</p></div>
+<!-- /wp:woocommerce/single-product -->',
+					$product->get_id()
+				)
+			);
+
+			$this->assertStringContainsString(
+				'This content is password-protected',
+				$markup,
+				'Password-protected products should render the password form.'
+			);
+			$this->assertStringNotContainsString(
+				'VISIBLE_PRODUCT_CONTENT',
+				$markup,
+				'Password-protected products should not render block content.'
+			);
+			$this->assertStringNotContainsString(
+				'?product=' . $product->get_slug(),
+				$markup,
+				'Password form should redirect to the current page instead of the product page.'
+			);
+		} finally {
+			WC_Helper_Product::delete_product( $product->get_id() );
+		}
+	}
+
+	/**
+	 * @testdox Draft products that are not viewable render no content.
+	 */
+	public function test_draft_product_renders_nothing() {
+		wp_set_current_user( 0 );
+
+		$product = WC_Helper_Product::create_simple_product( true, array( 'status' => 'draft' ) );
+
+		try {
+			$markup = do_blocks(
+				sprintf(
+					'<!-- wp:woocommerce/single-product {"productId":%d} -->
+<div class="wp-block-woocommerce-single-product"><p>VISIBLE_PRODUCT_CONTENT</p></div>
+<!-- /wp:woocommerce/single-product -->',
+					$product->get_id()
+				)
+			);
+
+			$this->assertSame(
+				'',
+				$markup,
+				'Draft products should not render any Single Product block output.'
+			);
+		} finally {
+			WC_Helper_Product::delete_product( $product->get_id() );
+		}
+	}
 }
diff --git a/plugins/woocommerce/tests/php/src/Blocks/Utils/WC_Product_Custom.php b/plugins/woocommerce/tests/php/src/Blocks/Utils/WC_Product_Custom.php
index 0e0edef3e7b..926c6da0e61 100644
--- a/plugins/woocommerce/tests/php/src/Blocks/Utils/WC_Product_Custom.php
+++ b/plugins/woocommerce/tests/php/src/Blocks/Utils/WC_Product_Custom.php
@@ -11,15 +11,6 @@ use WC_Product;
  * Custom product class.
  */
 class WC_Product_Custom extends WC_Product {
-	/**
-	 * Initialize custom product.
-	 *
-	 * @param WC_Product|int $product Product instance or ID.
-	 */
-	public function __construct( $product = 0 ) {
-		parent::__construct();
-	}
-
 	/**
 	 * Get internal type.
 	 *