PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 11.1.0-beta.2
WooCommerce v11.1.0-beta.2
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 7.8.0-beta.1 7.8.0-beta.2 7.8.0-rc.1 7.8.0-rc.2 7.8.1 7.8.2 7.8.3 7.8.4 7.9.0 7.9.0-beta.1 7.9.0-beta.2 7.9.0-rc.2 7.9.0-rc.3 7.9.1 7.9.2 8.0.0 8.0.0-beta.1 8.0.0-beta.2 8.0.0-rc.1 8.0.0-rc.2 8.0.1 8.0.2 8.0.3 8.0.4 8.0.5 8.1.0 8.1.0-beta.1 8.1.0-rc.1 8.1.0-rc.2 8.1.1 8.1.2 8.1.3 8.1.4 8.2.0 8.2.0-beta.1 8.2.0-rc.1 8.2.0-rc.2 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.3.0 8.3.0-beta.1 8.3.0-rc.1 8.3.0-rc.2 8.3.1 8.3.2 8.3.3 8.3.4 8.4.0 8.4.0-beta.1 8.4.0-rc.1 8.4.1 8.4.2 8.4.3 8.5.0 8.5.0-beta.1 8.5.0-rc.1 8.5.1 8.5.2 8.5.3 8.5.4 8.5.5 8.6.0 8.6.0-beta.1 8.6.0-rc.1 8.6.1 8.6.2 8.6.3 8.6.4 8.7.0 8.7.0-beta.1 8.7.0-beta.2 8.7.0-rc.1 8.7.1 8.7.2 8.7.3 8.8.0 8.8.0-beta.1 8.8.0-rc.1 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.8.6 8.8.7 8.9.0 8.9.0-beta.1 8.9.0-rc.1 8.9.1 8.9.2 8.9.3 8.9.4 8.9.5 9.0.0 9.0.0-beta.1 9.0.0-beta.2 9.0.0-rc.1 9.0.1 9.0.2 9.0.3 9.0.4 9.1.0 9.1.0-beta.1 9.1.0-rc.1 9.1.1 9.1.2 9.1.3 9.1.4 9.1.5 9.1.6 9.2.0 9.2.0-beta.1 9.2.0-rc.1 9.2.1 9.2.2 9.2.3 9.2.4 9.2.5 9.3.0 9.3.0-beta.1 9.3.0-rc.1 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.3.6 9.4.0 9.4.0-beta.1 9.4.0-beta.2 9.4.0-rc.1 9.4.0-rc.2 9.4.0-rc.3 9.4.0-rc.4 9.4.1 9.4.2 9.4.3 9.4.4 9.4.5 9.5.0 9.5.0-beta.1 9.5.0-beta.2 9.5.0-rc.1 9.5.1 9.5.2 9.5.3 9.5.4 9.6.0 9.6.0-beta.1 9.6.0-beta.2 9.6.0-rc.1 9.6.1 9.6.2 9.6.3 9.6.4 9.7.0 9.7.0-beta.1 9.7.0-rc.1 9.7.1 9.7.2 9.7.3 9.8.0 9.8.0-beta.1 9.8.0-rc.1 9.8.1 9.8.2 9.8.3 9.8.4 9.8.5 9.8.6 9.8.7 9.9.0 9.9.0-beta.1 9.9.0-rc.1 9.9.1 9.9.2 9.9.3 9.9.4 9.9.5 9.9.6 9.9.7 3.7.3 7.1.2 3.8.0 7.2.0 3.8.0-beta.1 7.2.0-beta.1 3.8.0-rc.1 7.2.0-beta.2 3.8.0-rc.2 7.2.0-rc.1 3.8.1 7.2.0-rc.2 3.8.2 7.2.1 3.8.3 7.2.2 3.9.0 7.2.3 3.9.0-beta.1 7.2.4 3.9.0-beta.2 7.3.0 3.9.0-rc.1 7.3.0-beta.1 3.9.0-rc.2 7.3.0-beta.2 3.9.0-rc.3 7.3.0-rc.1 3.9.0-rc.4 7.3.0-rc.2 3.9.1 7.3.1 3.9.2 7.4.0 3.9.3 7.4.0-beta.1 3.9.4 7.4.0-beta.2 3.9.5 7.4.0-rc.1 4.0.0 7.4.0-rc.2 4.0.0-beta.1 7.4.1 4.0.0-rc.1 7.4.2 4.0.0-rc.2 7.5.0 4.0.1 7.5.0-beta.1 4.0.2 7.5.0-beta.2 4.0.3 7.5.0-rc.1 4.0.4 7.5.1 4.1.0 7.5.2 4.1.0-beta.1 7.6.0 4.1.0-beta.2 7.6.0-beta.1 4.1.0-rc.1 7.6.0-beta.2 4.1.0-rc.2 7.6.0-rc.1 4.1.1 7.6.0-rc.2 4.1.2 7.6.0-rc.3 4.1.3 7.6.1 4.1.4 7.6.2 4.2.0 7.7.0 4.2.0-RC.1 7.7.0-beta.1 4.2.0-RC.2 7.7.0-beta.2 4.2.0-beta.1 7.7.0-rc.1 4.2.1 7.7.1 4.2.2 7.7.2 4.2.3 7.7.3 4.2.4 7.8.0 4.2.5 4.3.0 4.3.0-beta.1 4.3.0-rc.1 4.3.0-rc.2 4.3.0-rc.3 4.3.1 4.3.2 4.3.3 4.3.4 4.3.5 4.3.6 4.4.0 4.4.0-beta.1 4.4.0-rc.1 4.4.1 4.4.2 4.4.3 4.4.4 4.5.0 4.5.0-beta.1 4.5.0-rc.1 4.5.0-rc.3 4.5.1 4.5.2 4.5.3 4.5.4 4.5.5 4.6.0 4.6.0-beta.1 4.6.0-rc.1 4.6.1 4.6.2 4.6.3 4.6.4 4.6.5 4.7.0 4.7.0-beta.1 4.7.0-beta.2 4.7.0-rc.1 4.7.1 4.7.1-beta.1 4.7.2 4.7.3 4.7.4 4.8.0 4.8.0-beta.1 4.8.0-rc.1 4.8.0-rc.2 4.8.1 4.8.2 4.8.3 4.9.0 4.9.0-beta.1 4.9.0-rc.1 4.9.0-rc.2 4.9.1 4.9.2 4.9.3 4.9.4 4.9.5 5.0.0 5.0.0-beta.1 5.0.0-beta.2 5.0.0-rc.1 5.0.0-rc.2 5.0.0-rc.3 5.0.1 5.0.2 5.0.3 5.1.0 5.1.0-beta.1 5.1.0-rc.1 trunk 5.1.1 10.0.0 5.1.2 10.0.0-rc.1 5.1.3 10.0.0-rc.2 5.2.0 10.0.1 5.2.0-beta.1 10.0.2 5.2.0-rc.1 10.0.3 5.2.0-rc.2 10.0.4 5.2.1 10.0.5 5.2.2 10.0.6 5.2.3 10.1.0 5.2.4 10.1.0-rc.1 5.2.5 10.1.0-rc.2 5.3.0 10.1.0-rc.3 5.3.0-beta.1 10.1.0-rc.4 5.3.0-rc.1 10.1.1 5.3.0-rc.2 10.1.2 5.3.1 10.1.3 5.3.2 10.1.4 5.3.3 10.2.0 5.4.0 10.2.0-beta.1 5.4.0-beta.1 10.2.0-beta.2 5.4.0-rc.1 10.2.0-rc.1 5.4.1 10.2.1 5.4.2 10.2.2 5.4.3 10.2.3 5.4.4 10.2.4 5.4.5 10.3.0 5.5.0 10.3.0-beta.1 5.5.0-beta.1 10.3.0-beta.2 5.5.0-rc.1 10.3.0-rc.1 5.5.0-rc.2 10.3.0-rc.2 5.5.1 10.3.1 5.5.2 10.3.2 5.5.3 10.3.3 5.5.4 10.3.4 5.5.5 10.3.5 5.6.0 10.3.6 5.6.0-beta.1 10.3.7 5.6.0-rc.1 10.3.8 5.6.0-rc.2 10.4.0 5.6.1 10.4.0-beta.1 5.6.2 10.4.0-beta.2 5.6.3 10.4.0-rc.1 5.7.0 10.4.1 5.7.0-beta.1 10.4.2 5.7.0-rc.1 10.4.3 5.7.1 10.4.4 5.7.2 10.5.0 5.7.3 10.5.0-beta.1 5.8.0 10.5.0-beta.2 5.8.0-beta.1 10.5.0-rc.1 5.8.0-beta.2 10.5.0-rc.2 5.8.0-rc.1 10.5.0-rc.3 5.8.1 10.5.1 5.8.2 10.5.2 5.9.0 10.5.3 5.9.0-beta.1 10.6.0 5.9.0-rc.1 10.6.0-beta.1 5.9.0-rc.2 10.6.0-beta.2 5.9.1 10.6.0-rc.1 5.9.2 10.6.1 6.0.0 10.6.2 6.0.0-beta.1 10.7.0 6.0.0-rc.1 10.7.0-beta.1 6.0.1 10.7.0-beta.2 6.0.2 10.7.0-rc.1 6.1.0 3.0.0 6.1.0-beta.1 3.0.1 6.1.0-rc.1 3.0.2 6.1.0-rc.2 3.0.3 6.1.1 3.0.4 6.1.2 3.0.5 6.1.3 3.0.6 6.2.0 3.0.7 6.2.0-beta.1 3.0.8 6.2.0-rc.1 3.0.9 6.2.0-rc.2 3.1.0 6.2.1 3.1.1 6.2.2 3.1.2 6.2.3 3.2.0 6.3.0 3.2.1 6.3.0-beta.1 3.2.2 6.3.0-rc.1 3.2.3 6.3.0-rc.2 3.2.4 6.3.1 3.2.5 6.3.2 3.2.6 6.4.0 3.3.0 6.4.0-beta.1 3.3.1 6.4.0-rc.1 3.3.2 6.4.1 3.3.2-rc.1 6.4.2 3.3.3 6.5.0 3.3.4 6.5.0-beta.1 3.3.5 6.5.0-rc.1 3.3.6 6.5.0-rc.2 3.4.0 6.5.1 3.4.0-beta.1 6.5.2 3.4.0-rc.2 6.6.0 3.4.1 6.6.0-beta.1 3.4.2 6.6.0-rc.1 3.4.3 6.6.0-rc.2 3.4.4 6.6.1 3.4.5 6.6.2 3.4.6 6.7.0 3.4.7 6.7.0-beta.1 3.4.8 6.7.0-beta.2 3.5.0 6.7.0-rc.1 3.5.0-beta.1 6.7.1 3.5.0-rc.1 6.8.0 3.5.0-rc.2 6.8.0-beta.1 3.5.1 6.8.0-beta.2 3.5.10 6.8.0-rc.1 3.5.2 6.8.1 3.5.3 6.8.2 3.5.4 6.8.3 3.5.5 6.9.0 3.5.6 6.9.0-beta.1 3.5.7 6.9.0-beta.2 3.5.8 6.9.0-rc.1 3.5.9 6.9.1 3.6.0 6.9.2 3.6.0-beta.1 6.9.3 3.6.0-rc.1 6.9.4 3.6.0-rc.2 6.9.5 3.6.0-rc.3 7.0.0 3.6.1 7.0.0-beta.1 3.6.2 7.0.0-beta.2 3.6.3 7.0.0-beta.3 3.6.4 7.0.0-rc.1 3.6.5 7.0.0-rc.2 3.6.6 7.0.1 3.6.7 7.0.2 3.7.0 7.1.0 3.7.0-beta.1 7.1.0-beta.1 3.7.0-rc.1 7.1.0-beta.2 3.7.0-rc.2 7.1.0-rc.1 3.7.1 7.1.0-rc.2 3.7.2 7.1.1
woocommerce / src / Blocks / Shipping / ShippingController.php
woocommerce / src / Blocks / Shipping Last commit date
PickupLocation.php 1 month ago PickupLocationsRestController.php 2 weeks ago ShippingController.php 2 weeks ago
ShippingController.php
653 lines
1 <?php
2 namespace Automattic\WooCommerce\Blocks\Shipping;
3
4 use Automattic\WooCommerce\Blocks\Assets\Api as AssetApi;
5 use Automattic\WooCommerce\Blocks\Assets\AssetDataRegistry;
6 use Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils;
7 use Automattic\WooCommerce\Enums\ProductTaxStatus;
8 use Automattic\WooCommerce\Enums\TaxDisplayMode;
9 use Automattic\WooCommerce\StoreApi\Utilities\LocalPickupUtils;
10 use Automattic\WooCommerce\Utilities\ArrayUtil;
11 use WC_Customer;
12 use WC_Shipping_Rate;
13
14 /**
15 * ShippingController class.
16 *
17 * @internal
18 */
19 class ShippingController {
20
21 /**
22 * Script handle used for enqueueing the scripts needed for managing the Local Pickup Shipping Settings.
23 */
24 private const LOCAL_PICKUP_ADMIN_JS_HANDLE = 'wc-shipping-method-pickup-location';
25
26 /**
27 * Instance of the asset API.
28 *
29 * @var AssetApi
30 */
31 protected $asset_api;
32
33 /**
34 * Instance of the asset data registry.
35 *
36 * @var AssetDataRegistry
37 */
38 protected $asset_data_registry;
39
40 /**
41 * Whether local pickup is enabled.
42 *
43 * @var bool
44 */
45 private $local_pickup_enabled;
46
47 /**
48 * Constructor.
49 *
50 * @param AssetApi $asset_api Instance of the asset API.
51 * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
52 */
53 public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry ) {
54 $this->asset_api = $asset_api;
55 $this->asset_data_registry = $asset_data_registry;
56 $this->local_pickup_enabled = LocalPickupUtils::is_local_pickup_enabled();
57 }
58
59 /**
60 * Initialization method.
61 */
62 public function init() {
63 if ( is_admin() ) {
64 $this->asset_data_registry->add(
65 'countryStates',
66 function () {
67 return WC()->countries->get_states();
68 }
69 );
70 }
71 $this->asset_data_registry->add( 'shippingCostRequiresAddress', get_option( 'woocommerce_shipping_cost_requires_address', false ) === 'yes' );
72 add_action( 'rest_api_init', array( $this, 'register_settings' ) );
73 add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
74 add_action( 'admin_footer', array( $this, 'hydrate_client_settings' ), 0 );
75 add_action( 'woocommerce_load_shipping_methods', array( $this, 'register_local_pickup' ) );
76 add_filter( 'woocommerce_local_pickup_methods', array( $this, 'register_local_pickup_method' ) );
77 add_filter( 'woocommerce_order_hide_shipping_address', array( $this, 'hide_shipping_address_for_local_pickup' ), 10 );
78 add_filter( 'woocommerce_customer_taxable_address', array( $this, 'filter_taxable_address' ) );
79 add_filter( 'woocommerce_order_get_tax_location', array( $this, 'filter_order_tax_location' ), 10, 2 );
80 add_filter( 'woocommerce_shipping_settings', array( $this, 'remove_shipping_settings' ) );
81 add_filter( 'woocommerce_shipping_packages', array( $this, 'filter_shipping_packages' ) );
82 add_filter( 'pre_update_option_woocommerce_pickup_location_settings', array( $this, 'flush_cache' ) );
83 add_filter( 'pre_update_option_pickup_location_pickup_locations', array( $this, 'flush_cache' ) );
84 add_filter( 'woocommerce_shipping_packages', array( $this, 'remove_shipping_if_no_address' ), 11 );
85 add_filter( 'woocommerce_order_shipping_to_display', array( $this, 'show_local_pickup_details' ), 10, 2 );
86 }
87
88 /**
89 * Inject collection details onto the order received page.
90 *
91 * @param string $return_value Return value.
92 * @param \WC_Order $order Order object.
93 * @return string
94 */
95 public function show_local_pickup_details( $return_value, $order ) {
96 // Confirm order is valid before proceeding further.
97 if ( ! $order instanceof \WC_Order ) {
98 return $return_value;
99 }
100
101 $shipping_method_ids = ArrayUtil::select( $order->get_shipping_methods(), 'get_method_id', ArrayUtil::SELECT_BY_OBJECT_METHOD );
102 $shipping_method_id = current( $shipping_method_ids );
103
104 // Ensure order used pickup location method, otherwise bail.
105 if ( 'pickup_location' !== $shipping_method_id ) {
106 return $return_value;
107 }
108
109 $shipping_method = current( $order->get_shipping_methods() );
110 $details = $shipping_method->get_meta( 'pickup_details' );
111 $location = $shipping_method->get_meta( 'pickup_location' );
112 $address = $shipping_method->get_meta( 'pickup_address' );
113 $cost = $shipping_method->get_total();
114
115 $lines = array();
116
117 if ( $location ) {
118 $lines[] = sprintf(
119 // Translators: %s location name.
120 __( 'Collection from <strong>%s</strong>:', 'woocommerce' ),
121 $location
122 );
123 }
124
125 if ( $address ) {
126 $lines[] = nl2br( esc_html( str_replace( ',', ', ', $address ) ) );
127 }
128
129 if ( $details ) {
130 $lines[] = wp_kses_post( $details );
131 }
132
133 if ( $cost > 0 ) {
134 $tax_display = get_option( 'woocommerce_tax_display_cart' );
135 $tax = $shipping_method->get_total_tax();
136
137 // Format cost with tax handling.
138 if ( TaxDisplayMode::EXCLUSIVE === $tax_display ) {
139 // Show pickup cost excluding tax.
140 $formatted_cost = wc_price( $cost, array( 'currency' => $order->get_currency() ) );
141 if ( (float) $tax > 0 && $order->get_prices_include_tax() ) {
142 /**
143 * Hook to add tax label to pickup cost.
144 *
145 * @since 6.0.0
146 * @param string $tax_label Tax label.
147 * @param \WC_Order $order Order object.
148 * @param string $tax_display Tax display.
149 * @return string
150 */
151 $formatted_cost .= apply_filters(
152 'woocommerce_order_shipping_to_display_tax_label',
153 '&nbsp;<small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>',
154 $order,
155 $tax_display
156 );
157 }
158 } else {
159 // Show pickup cost including tax.
160 $formatted_cost = wc_price(
161 (float) $cost + (float) $tax,
162 array( 'currency' => $order->get_currency() )
163 );
164 if ( (float) $tax > 0 && ! $order->get_prices_include_tax() ) {
165 /**
166 * Hook to add tax label to pickup cost.
167 *
168 * @since 6.0.0
169 * @param string $tax_label Tax label.
170 * @param \WC_Order $order Order object.
171 * @param string $tax_display Tax display.
172 * @return string
173 */
174 $formatted_cost .= apply_filters(
175 'woocommerce_order_shipping_to_display_tax_label',
176 '&nbsp;<small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>',
177 $order,
178 $tax_display
179 );
180 }
181 }
182
183 $lines[] = '<br>' . sprintf(
184 // Translators: %s is the formatted price.
185 __( 'Pickup cost: %s', 'woocommerce' ),
186 $formatted_cost
187 );
188 }
189
190 // If nothing is available, return original.
191 if ( empty( $lines ) ) {
192 return $return_value;
193 }
194
195 // Join all the lines with a <br> separator.
196 return implode( '<br>', $lines );
197 }
198
199 /**
200 * When using the cart and checkout blocks this method is used to adjust core shipping settings via a filter hook.
201 *
202 * @param array $settings The default WC shipping settings.
203 * @return array|mixed The filtered settings.
204 */
205 public function remove_shipping_settings( $settings ) {
206 if ( CartCheckoutUtils::is_cart_block_default() ) {
207 foreach ( $settings as $index => $setting ) {
208 if ( 'woocommerce_enable_shipping_calc' === $setting['id'] ) {
209 $settings[ $index ]['desc_tip'] = sprintf(
210 /* translators: %s: URL to the documentation. */
211 __( 'This feature is not available when using the <a href="%s">Cart and checkout blocks</a>. Shipping will be calculated at checkout.', 'woocommerce' ),
212 'https://woocommerce.com/document/woocommerce-store-editing/customizing-cart-and-checkout/'
213 );
214 $settings[ $index ]['disabled'] = true;
215 $settings[ $index ]['value'] = 'no';
216 break;
217 }
218 }
219 }
220
221 return $settings;
222 }
223
224 /**
225 * Register Local Pickup settings for rest api.
226 */
227 public function register_settings() {
228 register_setting(
229 'options',
230 'woocommerce_pickup_location_settings',
231 array(
232 'type' => 'object',
233 'description' => 'WooCommerce Local Pickup Method Settings',
234 'default' => array(),
235 'show_in_rest' => array(
236 'name' => 'pickup_location_settings',
237 'schema' => array(
238 'type' => 'object',
239 'properties' => array(
240 'enabled' => array(
241 'description' => __( 'If enabled, this method will appear on the block based checkout.', 'woocommerce' ),
242 'type' => 'string',
243 'enum' => array( 'yes', 'no' ),
244 ),
245 'title' => array(
246 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
247 'type' => 'string',
248 ),
249 'tax_status' => array(
250 'description' => __( 'If a cost is defined, this controls if taxes are applied to that cost.', 'woocommerce' ),
251 'type' => 'string',
252 'enum' => array( ProductTaxStatus::TAXABLE, ProductTaxStatus::NONE ),
253 ),
254 'cost' => array(
255 'description' => __( 'Optional cost to charge for local pickup.', 'woocommerce' ),
256 'type' => 'string',
257 ),
258 ),
259 ),
260 ),
261 )
262 );
263 register_setting(
264 'options',
265 'pickup_location_pickup_locations',
266 array(
267 'type' => 'array',
268 'description' => 'WooCommerce Local Pickup Locations',
269 'default' => array(),
270 'show_in_rest' => array(
271 'name' => 'pickup_locations',
272 'schema' => array(
273 'type' => 'array',
274 'items' => array(
275 'type' => 'object',
276 'properties' => array(
277 'name' => array(
278 'type' => 'string',
279 ),
280 'address' => array(
281 'type' => 'object',
282 'properties' => array(
283 'address_1' => array(
284 'type' => 'string',
285 ),
286 'city' => array(
287 'type' => 'string',
288 ),
289 'state' => array(
290 'type' => 'string',
291 ),
292 'postcode' => array(
293 'type' => 'string',
294 ),
295 'country' => array(
296 'type' => 'string',
297 ),
298 ),
299 ),
300 'details' => array(
301 'type' => 'string',
302 ),
303 'enabled' => array(
304 'type' => 'boolean',
305 ),
306 ),
307 ),
308 ),
309 ),
310 )
311 );
312
313 // Save route guarded by manage_woocommerce (via wc_rest_check_manager_permissions)
314 // so Shop Managers can save Local Pickup settings without needing manage_options.
315 ( new PickupLocationsRestController() )->register_routes();
316 }
317
318 /**
319 * Hydrate client settings
320 */
321 public function hydrate_client_settings() {
322 if ( ! wp_script_is( self::LOCAL_PICKUP_ADMIN_JS_HANDLE, 'enqueued' ) ) {
323 // Only hydrate the settings if the script dependent on them is enqueued.
324 return;
325 }
326
327 $locations = get_option( 'pickup_location_pickup_locations', array() );
328
329 $formatted_pickup_locations = array();
330 foreach ( $locations as $location ) {
331 $formatted_pickup_locations[] = array(
332 'name' => $location['name'],
333 'address' => $location['address'],
334 'details' => $location['details'],
335 'enabled' => wc_string_to_bool( $location['enabled'] ),
336 );
337 }
338
339 $has_legacy_pickup = false;
340
341 // Get all shipping zones.
342 $shipping_zones = \WC_Shipping_Zones::get_zones( 'admin' );
343 $international_shipping_zone = new \WC_Shipping_Zone( 0 );
344
345 // Loop through each shipping zone.
346 foreach ( $shipping_zones as $shipping_zone ) {
347 // Get all registered rates for this shipping zone.
348 $shipping_methods = $shipping_zone['shipping_methods'];
349 // Loop through each registered rate.
350 foreach ( $shipping_methods as $shipping_method ) {
351 if ( 'local_pickup' === $shipping_method->id && 'yes' === $shipping_method->enabled ) {
352 $has_legacy_pickup = true;
353 break 2;
354 }
355 }
356 }
357
358 foreach ( $international_shipping_zone->get_shipping_methods( true ) as $shipping_method ) {
359 if ( 'local_pickup' === $shipping_method->id ) {
360 $has_legacy_pickup = true;
361 break;
362 }
363 }
364
365 $settings = array(
366 'pickupLocationSettings' => LocalPickupUtils::get_local_pickup_settings(),
367 'pickupLocations' => $formatted_pickup_locations,
368 'readonlySettings' => array(
369 'hasLegacyPickup' => $has_legacy_pickup,
370 'storeCountry' => WC()->countries->get_base_country(),
371 'storeState' => WC()->countries->get_base_state(),
372 ),
373 );
374
375 wp_add_inline_script(
376 self::LOCAL_PICKUP_ADMIN_JS_HANDLE,
377 sprintf(
378 'var hydratedScreenSettings = %s;',
379 wp_json_encode( $settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES )
380 ),
381 'before'
382 );
383 }
384 /**
385 * Load admin scripts.
386 */
387 public function admin_scripts() {
388 $this->asset_api->register_script( self::LOCAL_PICKUP_ADMIN_JS_HANDLE, 'assets/client/blocks/wc-shipping-method-pickup-location.js', array(), true );
389 }
390
391 /**
392 * Registers the Local Pickup shipping method used by the Checkout Block.
393 */
394 public function register_local_pickup() {
395 if ( CartCheckoutUtils::is_checkout_block_default() ) {
396 $wc_instance = WC();
397 if ( is_object( $wc_instance ) && method_exists( $wc_instance, 'shipping' ) && is_object( $wc_instance->shipping ) && method_exists( $wc_instance->shipping, 'register_shipping_method' ) ) {
398 $wc_instance->shipping->register_shipping_method( new PickupLocation() );
399 } else {
400 wc_get_logger()->error( 'Error registering pickup location: WC()->shipping->register_shipping_method is not available', array( 'source' => 'shipping-controller' ) );
401 }
402 }
403 }
404
405 /**
406 * Declares the Pickup Location shipping method as a Local Pickup method for WooCommerce.
407 *
408 * @param array $methods Shipping method ids.
409 * @return array
410 */
411 public function register_local_pickup_method( $methods ) {
412 $methods[] = 'pickup_location';
413 return $methods;
414 }
415
416 /**
417 * Hides the shipping address on the order confirmation page when local pickup is selected.
418 *
419 * @param array $pickup_methods Method ids.
420 * @return array
421 */
422 public function hide_shipping_address_for_local_pickup( $pickup_methods ) {
423 return array_merge( $pickup_methods, LocalPickupUtils::get_local_pickup_method_ids() );
424 }
425
426 /**
427 * Everytime we save or update local pickup settings, we flush the shipping
428 * transient group.
429 *
430 * @param array $settings The setting array we're saving.
431 * @return array $settings The setting array we're saving.
432 */
433 public function flush_cache( $settings ) {
434 \WC_Cache_Helper::get_transient_version( 'shipping', true );
435 return $settings;
436 }
437 /**
438 * Filter the location used for taxes based on the chosen pickup location.
439 *
440 * @param array $address Location args.
441 * @return array
442 */
443 public function filter_taxable_address( $address ) {
444
445 if ( null === WC()->session ) {
446 return $address;
447 }
448 // We only need to select from the first package, since pickup_location only supports a single package.
449 $chosen_method = current( WC()->session->get( 'chosen_shipping_methods', array() ) ) ?? '';
450 $chosen_method_id = explode( ':', $chosen_method )[0];
451 $chosen_method_instance = explode( ':', $chosen_method )[1] ?? 0;
452
453 // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
454 if ( $chosen_method_id && true === apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) && in_array( $chosen_method_id, LocalPickupUtils::get_local_pickup_method_ids(), true ) ) {
455 $pickup_locations = get_option( 'pickup_location_pickup_locations', array() );
456 $pickup_location = $pickup_locations[ $chosen_method_instance ] ?? array();
457
458 if ( isset( $pickup_location['address'], $pickup_location['address']['country'] ) && ! empty( $pickup_location['address']['country'] ) ) {
459 $address = array(
460 $pickup_locations[ $chosen_method_instance ]['address']['country'],
461 $pickup_locations[ $chosen_method_instance ]['address']['state'],
462 $pickup_locations[ $chosen_method_instance ]['address']['postcode'],
463 $pickup_locations[ $chosen_method_instance ]['address']['city'],
464 );
465 }
466 }
467
468 return $address;
469 }
470
471 /**
472 * Filter the tax location for an order so local pickup orders are taxed at the chosen pickup location's address.
473 *
474 * An order's stored shipping/billing address is not the correct tax location for local pickup: the customer
475 * collects from the store, so tax must be based on the pickup location. `WC_Abstract_Order::get_tax_location()`
476 * already forces the shop base address for local pickup orders; this filter refines that to the specific pickup
477 * location's address, which is captured on the shipping line item at purchase time.
478 *
479 * This is the order-side counterpart to {@see self::filter_taxable_address()}, which performs the equivalent
480 * override for the cart via the customer's taxable address. Resolving the address from the order (rather than the
481 * customer session) keeps the result correct for any order tax read, including admin recalculation, background
482 * jobs, and multiple orders handled within a single request.
483 *
484 * @since 11.0.0
485 *
486 * @param array $location Tax location with 'country', 'state', 'postcode' and 'city' keys.
487 * @param \WC_Abstract_Order $order Order the tax location is being resolved for.
488 * @return array
489 */
490 public function filter_order_tax_location( $location, $order ) {
491 if ( ! $order instanceof \WC_Abstract_Order ) {
492 return $location;
493 }
494
495 // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
496 if ( true !== apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) ) {
497 return $location;
498 }
499
500 // Use the same canonical local pickup method list that WC_Abstract_Order::get_tax_location() uses to force
501 // the base address. Relying on the currently-registered methods (LocalPickupUtils::get_local_pickup_method_ids())
502 // would miss legacy or deregistered methods on existing orders, leaving them taxed at the store base instead.
503 // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Documented in WC_Abstract_Order::get_tax_location().
504 $local_pickup_method_ids = apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) );
505
506 foreach ( $order->get_shipping_methods() as $shipping_method ) {
507 if ( ! in_array( $shipping_method->get_method_id(), $local_pickup_method_ids, true ) ) {
508 continue;
509 }
510
511 $pickup_address = $shipping_method->get_meta( '_pickup_location_address' );
512
513 if ( is_array( $pickup_address ) && ! empty( $pickup_address['country'] ) ) {
514 return array(
515 'country' => $pickup_address['country'],
516 'state' => $pickup_address['state'] ?? '',
517 'postcode' => $pickup_address['postcode'] ?? '',
518 'city' => $pickup_address['city'] ?? '',
519 );
520 }
521 }
522
523 return $location;
524 }
525
526 /**
527 * Local Pickup requires all packages to support local pickup. This is because the entire order must be picked up
528 * so that all packages get the same tax rates applied during checkout.
529 *
530 * If a shipping package does not support local pickup (e.g. if disabled by an extension), this filters the option
531 * out for all packages. This will in turn disable the "pickup" toggle in Block Checkout.
532 *
533 * @param array $packages Array of shipping packages.
534 * @return array
535 */
536 public function filter_shipping_packages( $packages ) {
537 // Check all packages for an instance of a collectable shipping method.
538 $valid_packages = array_filter(
539 $packages,
540 function ( $package ) {
541 $shipping_method_ids = ArrayUtil::select( $package['rates'] ?? array(), 'get_method_id', ArrayUtil::SELECT_BY_OBJECT_METHOD );
542 return ! empty( array_intersect( LocalPickupUtils::get_local_pickup_method_ids(), $shipping_method_ids ) );
543 }
544 );
545
546 // Remove pickup location from rates arrays if not all packages can be picked up or support local pickup.
547 if ( count( $valid_packages ) !== count( $packages ) ) {
548 $packages = array_map(
549 function ( $package ) {
550 if ( ! is_array( $package['rates'] ) ) {
551 $package['rates'] = array();
552 return $package;
553 }
554 $package['rates'] = array_filter(
555 $package['rates'],
556 function ( $rate ) {
557 return ! in_array( $rate->get_method_id(), LocalPickupUtils::get_local_pickup_method_ids(), true );
558 }
559 );
560 return $package;
561 },
562 $packages
563 );
564 }
565
566 return $packages;
567 }
568
569 /**
570 * Remove shipping (i.e. delivery, not local pickup) if "Hide shipping costs until an address is entered" is enabled,
571 * and no address has been entered yet.
572 *
573 * Only applies to block checkout because pickup is chosen separately to shipping in that context.
574 *
575 * @param array $packages Array of shipping packages.
576 * @return array
577 */
578 public function remove_shipping_if_no_address( $packages ) {
579 if ( 'shortcode' === WC()->cart->cart_context ) {
580 return $packages;
581 }
582
583 $shipping_cost_requires_address = wc_string_to_bool( get_option( 'woocommerce_shipping_cost_requires_address', 'no' ) );
584
585 // Return early here for a small performance gain if we don't need to hide shipping costs until an address is entered.
586 if ( ! $shipping_cost_requires_address ) {
587 return $packages;
588 }
589
590 $customer = WC()->customer;
591
592 if ( $customer instanceof WC_Customer && $customer->has_full_shipping_address() ) {
593 return $packages;
594 }
595
596 return array_map(
597 function ( $package ) {
598 // Package rates is always an array due to a check in core.
599 $package['rates'] = array_filter(
600 $package['rates'],
601 function ( $rate ) {
602 return $rate instanceof WC_Shipping_Rate && in_array( $rate->get_method_id(), LocalPickupUtils::get_local_pickup_method_ids(), true );
603 }
604 );
605 return $package;
606 },
607 $packages
608 );
609 }
610
611 /**
612 * Track local pickup settings changes.
613 *
614 * @deprecated 11.0.0 Tracking now happens inside PickupLocationsRestController::update_settings().
615 *
616 * @param bool $served Whether the request has already been served.
617 * @param \WP_REST_Response $result The response object.
618 * @param \WP_REST_Request $request The request object.
619 * @return bool
620 */
621 public function track_local_pickup( $served, $result, $request ) {
622 unset( $result, $request );
623
624 wc_deprecated_function( __METHOD__, '11.0.0' );
625 return $served;
626 }
627
628 /**
629 * Check if legacy local pickup is activated in any of the shipping zones or in the Rest of the World zone.
630 *
631 * @since 8.8.0
632 *
633 * @return bool
634 */
635 public static function is_legacy_local_pickup_active() {
636 $rest_of_the_world = \WC_Shipping_Zones::get_zone_by( 'zone_id', 0 );
637 $shipping_zones = \WC_Shipping_Zones::get_zones();
638 $rest_of_the_world_data = $rest_of_the_world->get_data();
639 $rest_of_the_world_data['shipping_methods'] = $rest_of_the_world->get_shipping_methods();
640 array_unshift( $shipping_zones, $rest_of_the_world_data );
641
642 foreach ( $shipping_zones as $zone ) {
643 foreach ( $zone['shipping_methods'] as $method ) {
644 if ( 'local_pickup' === $method->id && $method->is_enabled() ) {
645 return true;
646 }
647 }
648 }
649
650 return false;
651 }
652 }
653