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)." );
+ }
+ }
}