Commit 72754252cdd for woocommerce

commit 72754252cdd6cfea4e2c77fc3e923e10ee26a43c
Author: SH Sajal Chowdhury <72102985+shsajalchowdhury@users.noreply.github.com>
Date:   Mon Jun 22 19:02:33 2026 +0600

    Fix legacy script shim loading strategy mismatch breaking third-party payment gateways (#64431)

diff --git a/plugins/woocommerce/changelog/fix-63892-legacy-shim-loading-strategy b/plugins/woocommerce/changelog/fix-63892-legacy-shim-loading-strategy
new file mode 100644
index 00000000000..4ee95f57763
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-63892-legacy-shim-loading-strategy
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Fix strategy mismatch between legacy alias scripts and their real counterparts. Scripts with legacy handles now use blocking strategy consistently, since WordPress discards loading strategies on alias scripts (src=false) since 6.3.
diff --git a/plugins/woocommerce/includes/class-wc-frontend-scripts.php b/plugins/woocommerce/includes/class-wc-frontend-scripts.php
index ded29a5c1bf..ad783c71fde 100644
--- a/plugins/woocommerce/includes/class-wc-frontend-scripts.php
+++ b/plugins/woocommerce/includes/class-wc-frontend-scripts.php
@@ -139,11 +139,11 @@ class WC_Frontend_Scripts {
 	 * Register a script for use.
 	 *
 	 * @uses   wp_register_script()
-	 * @param  string   $handle    Name of the script. Should be unique.
-	 * @param  string   $path      Full URL of the script, or path of the script relative to the WordPress root directory.
-	 * @param  string[] $deps      An array of registered script handles this script depends on.
-	 * @param  string   $version   String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added.
-	 * @param  boolean  $in_footer Whether to enqueue the script before </body> instead of in the <head>. Default 'false'.
+	 * @param  string                                          $handle    Name of the script. Should be unique.
+	 * @param  string                                          $path      Full URL of the script, or path of the script relative to the WordPress root directory.
+	 * @param  string[]                                        $deps      An array of registered script handles this script depends on.
+	 * @param  string                                          $version   String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added.
+	 * @param  bool|array{strategy?: string, in_footer?: bool} $in_footer Whether to enqueue the script before </body> (boolean), or an array of arguments such as 'strategy' and 'in_footer'. Default array( 'strategy' => 'defer' ).
 	 */
 	private static function register_script( $handle, $path, $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = array( 'strategy' => 'defer' ) ) {
 		self::$registered_scripts[] = $handle;
@@ -154,11 +154,11 @@ class WC_Frontend_Scripts {
 	 * Register and enqueue a script for use.
 	 *
 	 * @uses   wp_enqueue_script()
-	 * @param  string   $handle    Name of the script. Should be unique.
-	 * @param  string   $path      Full URL of the script, or path of the script relative to the WordPress root directory.
-	 * @param  string[] $deps      An array of registered script handles this script depends on.
-	 * @param  string   $version   String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added.
-	 * @param  boolean  $in_footer Whether to enqueue the script before </body> instead of in the <head>. Default 'false'.
+	 * @param  string                                          $handle    Name of the script. Should be unique.
+	 * @param  string                                          $path      Full URL of the script, or path of the script relative to the WordPress root directory.
+	 * @param  string[]                                        $deps      An array of registered script handles this script depends on.
+	 * @param  string                                          $version   String specifying script version number, if it has one, which is added to the URL as a query string for cache busting purposes. If version is set to false, a version number is automatically added equal to current installed WordPress version. If set to null, no version is added.
+	 * @param  bool|array{strategy?: string, in_footer?: bool} $in_footer Whether to enqueue the script before </body> (boolean), or an array of arguments such as 'strategy' and 'in_footer'. Default array( 'strategy' => 'defer' ).
 	 */
 	private static function enqueue_script( $handle, $path = '', $deps = array( 'jquery' ), $version = WC_VERSION, $in_footer = array( 'strategy' => 'defer' ) ) {
 		if ( ! in_array( $handle, self::$registered_scripts, true ) && $path ) {
@@ -414,10 +414,25 @@ class WC_Frontend_Scripts {
 		$register_scripts = self::get_scripts();

 		foreach ( $register_scripts as $name => $props ) {
-			self::register_script( $name, $props['src'], $props['deps'], $props['version'] );
-
-			if ( isset( $props['legacy_handle'] ) ) {
-				self::register_script( $props['legacy_handle'], false, array( $name ), $props['version'], true );
+			$is_legacy_handle = isset( $props['legacy_handle'] );
+
+			/*
+			 * Scripts with legacy alias handles must use a blocking strategy.
+			 * WordPress (since 6.3) silently discards loading strategies on alias
+			 * scripts (registered with src=false). If the real script uses defer
+			 * but the alias cannot inherit it, the strategy mismatch breaks
+			 * dependency resolution for third-party code that depends on the
+			 * legacy handle (e.g. payment gateways using 'jquery-payment').
+			 *
+			 * Using blocking for both the real script and its alias ensures
+			 * consistent execution order through the dependency chain.
+			 */
+			$in_footer = $is_legacy_handle ? true : array( 'strategy' => 'defer' );
+
+			self::register_script( $name, $props['src'], $props['deps'], $props['version'], $in_footer );
+
+			if ( $is_legacy_handle ) {
+				self::register_script( $props['legacy_handle'], false, array( $name ), $props['version'], $in_footer );
 			}
 		}
 	}
diff --git a/plugins/woocommerce/phpstan-baseline.neon b/plugins/woocommerce/phpstan-baseline.neon
index bb02ff523fc..b97b98be32e 100644
--- a/plugins/woocommerce/phpstan-baseline.neon
+++ b/plugins/woocommerce/phpstan-baseline.neon
@@ -12066,18 +12066,6 @@ parameters:
 			count: 1
 			path: includes/class-wc-form-handler.php

-		-
-			message: '#^Default value of the parameter \#5 \$in_footer \(array\<string, string\>\) of method WC_Frontend_Scripts\:\:enqueue_script\(\) is incompatible with type bool\.$#'
-			identifier: parameter.defaultValue
-			count: 1
-			path: includes/class-wc-frontend-scripts.php
-
-		-
-			message: '#^Default value of the parameter \#5 \$in_footer \(array\<string, string\>\) of method WC_Frontend_Scripts\:\:register_script\(\) is incompatible with type bool\.$#'
-			identifier: parameter.defaultValue
-			count: 1
-			path: includes/class-wc-frontend-scripts.php
-
 		-
 			message: '#^Method WC_Frontend_Scripts\:\:enqueue_block_assets\(\) has no return type specified\.$#'
 			identifier: missingType.return
diff --git a/plugins/woocommerce/tests/php/includes/class-wc-frontend-scripts-test.php b/plugins/woocommerce/tests/php/includes/class-wc-frontend-scripts-test.php
index 3bee3ef9035..66b4e1813e1 100644
--- a/plugins/woocommerce/tests/php/includes/class-wc-frontend-scripts-test.php
+++ b/plugins/woocommerce/tests/php/includes/class-wc-frontend-scripts-test.php
@@ -232,4 +232,38 @@ class WC_Frontend_Scripts_Test extends WC_Unit_Test_Case {
 		$this->assertNotContains( 'cod', $data['gateways_with_custom_place_order_button'] );
 		$this->assertNotContains( 'cheque', $data['gateways_with_custom_place_order_button'] );
 	}
+
+	/**
+	 * Test that scripts with legacy handles and their aliases use blocking strategy.
+	 *
+	 * WordPress (since 6.3) discards loading strategies on alias scripts
+	 * (src=false). To avoid strategy mismatches, both the real script and
+	 * its legacy alias must be registered as blocking (in_footer=true).
+	 */
+	public function test_legacy_handle_scripts_use_blocking_strategy(): void {
+		$reflection = new ReflectionClass( 'WC_Frontend_Scripts' );
+		$method     = $reflection->getMethod( 'register_scripts' );
+		$method->setAccessible( true );
+		$method->invoke( null );
+
+		$get_scripts_method = $reflection->getMethod( 'get_scripts' );
+		$get_scripts_method->setAccessible( true );
+		$scripts = $get_scripts_method->invoke( null );
+
+		foreach ( $scripts as $name => $props ) {
+			if ( ! isset( $props['legacy_handle'] ) ) {
+				continue;
+			}
+
+			$legacy_handle = $props['legacy_handle'];
+
+			// Real script must be blocking (no defer strategy).
+			$real_strategy = wp_scripts()->get_data( $name, 'strategy' );
+			$this->assertFalse( $real_strategy, "Real handle '{$name}' should not have a loading strategy (blocking)." );
+
+			// Alias script must also be blocking.
+			$legacy_strategy = wp_scripts()->get_data( $legacy_handle, 'strategy' );
+			$this->assertFalse( $legacy_strategy, "Legacy handle '{$legacy_handle}' should not have a loading strategy (blocking)." );
+		}
+	}
 }