Commit 43c53aef8a for woocommerce

commit 43c53aef8a9e66a5f76b927fc67e66724ba8305a
Author: Taha Paksu <3295+tpaksu@users.noreply.github.com>
Date:   Sat Jan 3 11:56:36 2026 +0300

    Add support for collectable shipping methods in checkout block editor (#62623)

    * Add support for collectable shipping methods in local pickup functionality

    * Add changefile(s) from automation for the following project(s): woocommerce

    * Fix code review issues, fix lint error

    * Revert changes

    * Revert remaining files

    * Implement local pickup method locations retrieval and add unit tests

    * Revert Cart change

    * Update local pickup method locations retrieval in Cart block

    * Filter out disabled locations on checkout block data

    * remove local pickup preload on cart, because cart shipping calculator was removed

    * filter out invalid pickup method

    * Replace BGN with EUR in tests

    ---------

    Co-authored-by: github-actions <github-actions@github.com>
    Co-authored-by: Nadir Seghir <nadir.seghir@gmail.com>

diff --git a/plugins/woocommerce/changelog/62623-fix-WOOPLUG-6076-shipping-method-block-local-pickup b/plugins/woocommerce/changelog/62623-fix-WOOPLUG-6076-shipping-method-block-local-pickup
new file mode 100644
index 0000000000..255a438047
--- /dev/null
+++ b/plugins/woocommerce/changelog/62623-fix-WOOPLUG-6076-shipping-method-block-local-pickup
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Add support for collectable shipping methods in local pickup functionality
\ No newline at end of file
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php b/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php
index 3b33fb0b25..23f6aed9fd 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Cart.php
@@ -243,35 +243,8 @@ class Cart extends AbstractBlock {
 		$this->asset_data_registry->add( 'hasDarkEditorStyleSupport', current_theme_supports( 'dark-editor-style' ) );
 		$this->asset_data_registry->register_page_id( isset( $attributes['checkoutPageId'] ) ? $attributes['checkoutPageId'] : 0 );
 		$this->asset_data_registry->add( 'isBlockTheme', wp_is_block_theme() );
-
-		$pickup_location_settings = LocalPickupUtils::get_local_pickup_settings();
-		$local_pickup_method_ids  = LocalPickupUtils::get_local_pickup_method_ids();
-
-		$this->asset_data_registry->add( 'localPickupEnabled', $pickup_location_settings['enabled'] );
-		$this->asset_data_registry->add( 'collectableMethodIds', $local_pickup_method_ids );
 		$this->asset_data_registry->add( 'shippingMethodsExist', CartCheckoutUtils::shipping_methods_exist() > 0 );

-		$is_block_editor = $this->is_block_editor();
-
-		if ( $is_block_editor && ! $this->asset_data_registry->exists( 'localPickupLocations' ) ) {
-			// Locations are passed to the client in admin to show a realistic preview in the editor.
-			$this->asset_data_registry->add(
-				'localPickupLocations',
-				array_filter(
-					array_map(
-						function ( $location ) {
-							if ( ! $location['enabled'] ) {
-								return null;
-							}
-							$location['formatted_address'] = wc()->countries->get_formatted_address( $location['address'], ', ' );
-							return $location;
-						},
-						get_option( 'pickup_location_pickup_locations', array() )
-					)
-				)
-			);
-		}
-
 		// Hydrate the following data depending on admin or frontend context.
 		if ( ! is_admin() && ! WC()->is_rest_api_request() ) {
 			$this->asset_data_registry->hydrate_api_request( '/wc/store/v1/cart' );
diff --git a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
index d520e491bf..510a74ed05 100644
--- a/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
+++ b/plugins/woocommerce/src/Blocks/BlockTypes/Checkout.php
@@ -467,16 +467,21 @@ class Checkout extends AbstractBlock {

 		$is_block_editor = $this->is_block_editor();

-		if ( $is_block_editor && ! $this->asset_data_registry->exists( 'localPickupLocations' ) ) {
+		if ( $is_block_editor ) {
 			$this->asset_data_registry->add(
 				'localPickupLocations',
-				array_map(
-					function ( $location ) {
-						$location['formatted_address'] = wc()->countries->get_formatted_address( $location['address'], ', ' );
-						return $location;
-					},
-					get_option( 'pickup_location_pickup_locations', array() )
-				)
+				array_filter(
+					array_map(
+						function ( $location ) {
+							if ( ! wc_string_to_bool( $location['enabled'] ) ) {
+								return null;
+							}
+							$location['formatted_address'] = wc()->countries->get_formatted_address( $location['address'], ', ' );
+							return $location;
+						},
+						LocalPickupUtils::get_local_pickup_method_locations()
+					)
+				),
 			);
 		}

diff --git a/plugins/woocommerce/src/StoreApi/Utilities/LocalPickupUtils.php b/plugins/woocommerce/src/StoreApi/Utilities/LocalPickupUtils.php
index 53b9989271..2fb461a9c6 100644
--- a/plugins/woocommerce/src/StoreApi/Utilities/LocalPickupUtils.php
+++ b/plugins/woocommerce/src/StoreApi/Utilities/LocalPickupUtils.php
@@ -103,4 +103,73 @@ class LocalPickupUtils {
 	public static function is_local_pickup_method( $method_id ) {
 		return in_array( $method_id, self::get_local_pickup_method_ids(), true );
 	}
+
+	/**
+	 * Gets local pickup locations for block editor preview, including placeholder
+	 * locations for custom shipping methods that support local pickup.
+	 *
+	 * This method combines the built-in pickup_location locations with placeholder
+	 * entries for any other shipping methods that declare 'local-pickup' support.
+	 * This allows custom shipping methods to appear in the block editor preview.
+	 *
+	 * @return array Array of pickup locations with the following structure:
+	 *               - 'name' (string) The location name.
+	 *               - 'enabled' (bool) Whether the location is enabled.
+	 *               - 'address' (array) Address array with keys: address_1, city, state, postcode, country.
+	 *               - 'details' (string) Additional details about the location.
+	 *               - 'method_id' (string) The shipping method ID this location belongs to.
+	 *
+	 * @since 10.5.0
+	 */
+	public static function get_local_pickup_method_locations() {
+		// Get the built-in pickup locations.
+		$builtin_locations = get_option( 'pickup_location_pickup_locations', array() );
+
+		// Add method_id to built-in locations.
+		foreach ( $builtin_locations as $index => $location ) {
+			$builtin_locations[ $index ]['method_id'] = 'pickup_location';
+		}
+
+		// Get all shipping methods that support local-pickup.
+		$shipping_methods = WC()->shipping()->get_shipping_methods();
+
+		// Get store base address for placeholder locations.
+		$base_country = WC()->countries->get_base_country();
+		$base_state   = WC()->countries->get_base_state();
+
+		$custom_method_locations = array();
+
+		foreach ( $shipping_methods as $method ) {
+			// Skip if method doesn't support local-pickup.
+			if ( ! $method->supports( 'local-pickup' ) ) {
+				continue;
+			}
+
+			// Skip the built-in pickup_location method (already handled above).
+			if ( 'pickup_location' === $method->id ) {
+				continue;
+			}
+
+			// Create a placeholder location for this custom method.
+			$custom_method_locations[] = array(
+				'name'      => $method->get_method_title(),
+				'enabled'   => true,
+				'address'   => array(
+					'address_1' => '123 Main Street',
+					'city'      => 'Sample City',
+					'state'     => $base_state,
+					'postcode'  => '12345',
+					'country'   => $base_country,
+				),
+				'details'   => sprintf(
+					/* translators: %s: shipping method title */
+					__( 'Pickup location for %s', 'woocommerce' ),
+					$method->get_method_title()
+				),
+				'method_id' => $method->id,
+			);
+		}
+
+		return array_merge( $builtin_locations, $custom_method_locations );
+	}
 }
diff --git a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/data/data-crud.test.js b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/data/data-crud.test.js
index 236a3b9676..c6dc9006aa 100644
--- a/plugins/woocommerce/tests/e2e-pw/tests/api-tests/data/data-crud.test.js
+++ b/plugins/woocommerce/tests/e2e-pw/tests/api-tests/data/data-crud.test.js
@@ -1758,8 +1758,8 @@ test.describe( 'Data API tests', () => {
 						},
 						{
 							code: 'BG',
-							name: 'Bulgarian lev',
-							currency_code: 'BGN',
+							name: 'Euro',
+							currency_code: 'EUR',
 							currency_pos: 'right_space',
 							decimal_sep: ',',
 							dimension_unit: 'cm',
@@ -3416,8 +3416,8 @@ test.describe( 'Data API tests', () => {
 					},
 					{
 						code: 'BG',
-						name: 'Bulgarian lev',
-						currency_code: 'BGN',
+						name: 'Euro',
+						currency_code: 'EUR',
 						currency_pos: 'right_space',
 						decimal_sep: ',',
 						dimension_unit: 'cm',
diff --git a/plugins/woocommerce/tests/php/src/StoreApi/Mocks/FakeLocalPickupShippingMethod.php b/plugins/woocommerce/tests/php/src/StoreApi/Mocks/FakeLocalPickupShippingMethod.php
new file mode 100644
index 0000000000..6f5edd56e3
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/StoreApi/Mocks/FakeLocalPickupShippingMethod.php
@@ -0,0 +1,31 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\StoreApi\Mocks;
+
+use WC_Shipping_Method;
+
+/**
+ * Fake shipping method that supports local pickup for testing.
+ */
+class FakeLocalPickupShippingMethod extends WC_Shipping_Method {
+
+	/**
+	 * Constructor.
+	 *
+	 * @param int $instance_id Instance ID.
+	 */
+	public function __construct( $instance_id = 0 ) {
+		$this->id           = 'test_local_pickup';
+		$this->instance_id  = $instance_id;
+		$this->method_title = 'Test Local Pickup';
+		$this->supports     = array( 'shipping-zones', 'instance-settings', 'local-pickup' );
+	}
+
+	/**
+	 * Calculate shipping - not used in tests.
+	 *
+	 * @param array $package Package array.
+	 */
+	public function calculate_shipping( $package = array() ) {}
+}
diff --git a/plugins/woocommerce/tests/php/src/StoreApi/Mocks/FakeRegularShippingMethod.php b/plugins/woocommerce/tests/php/src/StoreApi/Mocks/FakeRegularShippingMethod.php
new file mode 100644
index 0000000000..3f05613b93
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/StoreApi/Mocks/FakeRegularShippingMethod.php
@@ -0,0 +1,31 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\StoreApi\Mocks;
+
+use WC_Shipping_Method;
+
+/**
+ * Fake shipping method that does NOT support local pickup for testing.
+ */
+class FakeRegularShippingMethod extends WC_Shipping_Method {
+
+	/**
+	 * Constructor.
+	 *
+	 * @param int $instance_id Instance ID.
+	 */
+	public function __construct( $instance_id = 0 ) {
+		$this->id           = 'test_regular';
+		$this->instance_id  = $instance_id;
+		$this->method_title = 'Test Regular Shipping';
+		$this->supports     = array( 'shipping-zones', 'instance-settings' );
+	}
+
+	/**
+	 * Calculate shipping - not used in tests.
+	 *
+	 * @param array $package Package array.
+	 */
+	public function calculate_shipping( $package = array() ) {}
+}
diff --git a/plugins/woocommerce/tests/php/src/StoreApi/Utilities/LocalPickupUtilsTest.php b/plugins/woocommerce/tests/php/src/StoreApi/Utilities/LocalPickupUtilsTest.php
new file mode 100644
index 0000000000..a7540c72a3
--- /dev/null
+++ b/plugins/woocommerce/tests/php/src/StoreApi/Utilities/LocalPickupUtilsTest.php
@@ -0,0 +1,278 @@
+<?php
+declare( strict_types = 1 );
+
+namespace Automattic\WooCommerce\Tests\StoreApi\Utilities;
+
+use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils;
+use Automattic\WooCommerce\Tests\StoreApi\Mocks\FakeLocalPickupShippingMethod;
+use Automattic\WooCommerce\Tests\StoreApi\Mocks\FakeRegularShippingMethod;
+use WC_Shipping_Method;
+
+/**
+ * Tests for LocalPickupUtils class.
+ */
+class LocalPickupUtilsTest extends \WC_Unit_Test_Case {
+
+	/**
+	 * Original shipping methods backup.
+	 *
+	 * @var array
+	 */
+	private $original_shipping_methods;
+
+	/**
+	 * Mocked pickup locations value.
+	 *
+	 * @var array|null
+	 */
+	private $mocked_pickup_locations = null;
+
+	/**
+	 * Set up test fixtures.
+	 */
+	public function setUp(): void {
+		parent::setUp();
+
+		// Store original shipping methods to restore later.
+		$this->original_shipping_methods = WC()->shipping()->shipping_methods;
+
+		// Add filter to intercept option retrieval.
+		add_filter( 'pre_option_pickup_location_pickup_locations', array( $this, 'filter_pickup_locations' ) );
+	}
+
+	/**
+	 * Tear down test fixtures.
+	 */
+	public function tearDown(): void {
+		// Restore original shipping methods.
+		WC()->shipping()->shipping_methods = $this->original_shipping_methods;
+
+		// Remove the filter.
+		remove_filter( 'pre_option_pickup_location_pickup_locations', array( $this, 'filter_pickup_locations' ) );
+
+		// Reset mocked value.
+		$this->mocked_pickup_locations = null;
+
+		parent::tearDown();
+	}
+
+	/**
+	 * Filter callback to mock pickup locations option.
+	 *
+	 * @return array|false The mocked pickup locations or false to use real value.
+	 */
+	public function filter_pickup_locations() {
+		if ( null !== $this->mocked_pickup_locations ) {
+			return $this->mocked_pickup_locations;
+		}
+		return false;
+	}
+
+	/**
+	 * Helper to set mocked pickup locations.
+	 *
+	 * @param array $locations The locations to mock.
+	 */
+	private function set_pickup_locations( array $locations ): void {
+		$this->mocked_pickup_locations = $locations;
+	}
+
+	/**
+	 * @testdox Should return empty array when no locations and no custom methods exist.
+	 */
+	public function test_returns_empty_array_when_no_locations_and_no_custom_methods(): void {
+		$this->set_pickup_locations( array() );
+
+		WC()->shipping()->shipping_methods = array();
+
+		$result = LocalPickupUtils::get_local_pickup_method_locations();
+
+		$this->assertIsArray( $result );
+		$this->assertEmpty( $result );
+	}
+
+	/**
+	 * @testdox Should return built-in locations with method_id added.
+	 */
+	public function test_returns_builtin_locations_with_method_id(): void {
+		$this->set_pickup_locations(
+			array(
+				array(
+					'name'    => 'Store Location',
+					'enabled' => true,
+					'address' => array(
+						'address_1' => '123 Main St',
+						'city'      => 'Anytown',
+						'state'     => 'CA',
+						'postcode'  => '12345',
+						'country'   => 'US',
+					),
+					'details' => 'Open 9-5',
+				),
+			)
+		);
+
+		WC()->shipping()->shipping_methods = array();
+
+		$result = LocalPickupUtils::get_local_pickup_method_locations();
+
+		$this->assertCount( 1, $result );
+		$this->assertArrayHasKey( 'method_id', $result[0] );
+		$this->assertSame( 'pickup_location', $result[0]['method_id'] );
+		$this->assertSame( 'Store Location', $result[0]['name'] );
+	}
+
+	/**
+	 * @testdox Should create placeholder location for custom shipping method with local-pickup support.
+	 */
+	public function test_creates_placeholder_for_custom_local_pickup_method(): void {
+		$this->set_pickup_locations( array() );
+
+		WC()->shipping()->shipping_methods = array(
+			'test_local_pickup' => new FakeLocalPickupShippingMethod(),
+		);
+
+		$result = LocalPickupUtils::get_local_pickup_method_locations();
+
+		$this->assertCount( 1, $result );
+		$this->assertSame( 'test_local_pickup', $result[0]['method_id'] );
+		$this->assertSame( 'Test Local Pickup', $result[0]['name'] );
+		$this->assertTrue( $result[0]['enabled'] );
+		$this->assertArrayHasKey( 'address', $result[0] );
+		$this->assertSame( '123 Main Street', $result[0]['address']['address_1'] );
+		$this->assertSame( 'Sample City', $result[0]['address']['city'] );
+		$this->assertSame( '12345', $result[0]['address']['postcode'] );
+	}
+
+	/**
+	 * @testdox Should not create placeholder for shipping method without local-pickup support.
+	 */
+	public function test_does_not_create_placeholder_for_regular_shipping_method(): void {
+		$this->set_pickup_locations( array() );
+
+		WC()->shipping()->shipping_methods = array(
+			'test_regular' => new FakeRegularShippingMethod(),
+		);
+
+		$result = LocalPickupUtils::get_local_pickup_method_locations();
+
+		$this->assertIsArray( $result );
+		$this->assertEmpty( $result );
+	}
+
+	/**
+	 * @testdox Should combine built-in locations with custom method placeholders.
+	 */
+	public function test_combines_builtin_locations_with_custom_method_placeholders(): void {
+		$this->set_pickup_locations(
+			array(
+				array(
+					'name'    => 'Store Location',
+					'enabled' => true,
+					'address' => array(
+						'address_1' => '123 Main St',
+						'city'      => 'Anytown',
+						'state'     => 'CA',
+						'postcode'  => '12345',
+						'country'   => 'US',
+					),
+					'details' => 'Open 9-5',
+				),
+			)
+		);
+
+		WC()->shipping()->shipping_methods = array(
+			'test_local_pickup' => new FakeLocalPickupShippingMethod(),
+		);
+
+		$result = LocalPickupUtils::get_local_pickup_method_locations();
+
+		$this->assertCount( 2, $result );
+
+		$this->assertSame( 'pickup_location', $result[0]['method_id'] );
+		$this->assertSame( 'Store Location', $result[0]['name'] );
+
+		$this->assertSame( 'test_local_pickup', $result[1]['method_id'] );
+		$this->assertSame( 'Test Local Pickup', $result[1]['name'] );
+	}
+
+	/**
+	 * @testdox Should not duplicate pickup_location method in results.
+	 */
+	public function test_does_not_duplicate_builtin_pickup_location_method(): void {
+		$this->set_pickup_locations(
+			array(
+				array(
+					'name'    => 'Store Location',
+					'enabled' => true,
+					'address' => array(
+						'address_1' => '123 Main St',
+						'city'      => 'Anytown',
+						'state'     => 'CA',
+						'postcode'  => '12345',
+						'country'   => 'US',
+					),
+					'details' => 'Open 9-5',
+				),
+			)
+		);
+
+		$pickup_location_mock     = $this->createMock( WC_Shipping_Method::class );
+		$pickup_location_mock->id = 'pickup_location';
+		$pickup_location_mock->method( 'supports' )->willReturn( true );
+		$pickup_location_mock->method( 'get_method_title' )->willReturn( 'Local Pickup' );
+
+		WC()->shipping()->shipping_methods = array(
+			'pickup_location' => $pickup_location_mock,
+		);
+
+		$result = LocalPickupUtils::get_local_pickup_method_locations();
+
+		$this->assertCount( 1, $result );
+		$this->assertSame( 'pickup_location', $result[0]['method_id'] );
+		$this->assertSame( 'Store Location', $result[0]['name'] );
+	}
+
+	/**
+	 * @testdox Should use store base country and state for placeholder address.
+	 */
+	public function test_uses_store_base_location_for_placeholder(): void {
+		add_filter(
+			'pre_option_woocommerce_default_country',
+			function () {
+				return 'GB:LND';
+			}
+		);
+
+		$this->set_pickup_locations( array() );
+
+		WC()->shipping()->shipping_methods = array(
+			'test_local_pickup' => new FakeLocalPickupShippingMethod(),
+		);
+
+		$result = LocalPickupUtils::get_local_pickup_method_locations();
+
+		$this->assertCount( 1, $result );
+		$this->assertSame( 'GB', $result[0]['address']['country'] );
+		$this->assertSame( 'LND', $result[0]['address']['state'] );
+
+		remove_all_filters( 'pre_option_woocommerce_default_country' );
+	}
+
+	/**
+	 * @testdox Should include details with method title in placeholder.
+	 */
+	public function test_includes_details_with_method_title(): void {
+		$this->set_pickup_locations( array() );
+
+		WC()->shipping()->shipping_methods = array(
+			'test_local_pickup' => new FakeLocalPickupShippingMethod(),
+		);
+
+		$result = LocalPickupUtils::get_local_pickup_method_locations();
+
+		$this->assertCount( 1, $result );
+		$this->assertArrayHasKey( 'details', $result[0] );
+		$this->assertStringContainsString( 'Test Local Pickup', $result[0]['details'] );
+	}
+}