PluginProbe ʕ •ᴥ•ʔ
WooCommerce / 11.0.0-beta.1
WooCommerce v11.0.0-beta.1
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 6 days ago ShippingController.php 6 days ago
ShippingController.php
677 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 use WC_Tracks;
14
15 /**
16 * ShippingController class.
17 *
18 * @internal
19 */
20 class ShippingController {
21
22 /**
23 * Script handle used for enqueueing the scripts needed for managing the Local Pickup Shipping Settings.
24 */
25 private const LOCAL_PICKUP_ADMIN_JS_HANDLE = 'wc-shipping-method-pickup-location';
26
27 /**
28 * Instance of the asset API.
29 *
30 * @var AssetApi
31 */
32 protected $asset_api;
33
34 /**
35 * Instance of the asset data registry.
36 *
37 * @var AssetDataRegistry
38 */
39 protected $asset_data_registry;
40
41 /**
42 * Whether local pickup is enabled.
43 *
44 * @var bool
45 */
46 private $local_pickup_enabled;
47
48 /**
49 * Constructor.
50 *
51 * @param AssetApi $asset_api Instance of the asset API.
52 * @param AssetDataRegistry $asset_data_registry Instance of the asset data registry.
53 */
54 public function __construct( AssetApi $asset_api, AssetDataRegistry $asset_data_registry ) {
55 $this->asset_api = $asset_api;
56 $this->asset_data_registry = $asset_data_registry;
57 $this->local_pickup_enabled = LocalPickupUtils::is_local_pickup_enabled();
58 }
59
60 /**
61 * Initialization method.
62 */
63 public function init() {
64 if ( is_admin() ) {
65 $this->asset_data_registry->add(
66 'countryStates',
67 function () {
68 return WC()->countries->get_states();
69 }
70 );
71 }
72 $this->asset_data_registry->add( 'shippingCostRequiresAddress', get_option( 'woocommerce_shipping_cost_requires_address', false ) === 'yes' );
73 add_action( 'rest_api_init', array( $this, 'register_settings' ) );
74 add_action( 'admin_enqueue_scripts', array( $this, 'admin_scripts' ) );
75 add_action( 'admin_footer', array( $this, 'hydrate_client_settings' ), 0 );
76 add_action( 'woocommerce_load_shipping_methods', array( $this, 'register_local_pickup' ) );
77 add_filter( 'woocommerce_local_pickup_methods', array( $this, 'register_local_pickup_method' ) );
78 add_filter( 'woocommerce_order_hide_shipping_address', array( $this, 'hide_shipping_address_for_local_pickup' ), 10 );
79 add_filter( 'woocommerce_customer_taxable_address', array( $this, 'filter_taxable_address' ) );
80 add_filter( 'woocommerce_order_get_tax_location', array( $this, 'filter_order_tax_location' ), 10, 2 );
81 add_filter( 'woocommerce_shipping_settings', array( $this, 'remove_shipping_settings' ) );
82 add_filter( 'woocommerce_shipping_packages', array( $this, 'filter_shipping_packages' ) );
83 add_filter( 'pre_update_option_woocommerce_pickup_location_settings', array( $this, 'flush_cache' ) );
84 add_filter( 'pre_update_option_pickup_location_pickup_locations', array( $this, 'flush_cache' ) );
85 add_filter( 'woocommerce_shipping_packages', array( $this, 'remove_shipping_if_no_address' ), 11 );
86 add_filter( 'woocommerce_order_shipping_to_display', array( $this, 'show_local_pickup_details' ), 10, 2 );
87 add_action( 'rest_pre_serve_request', array( $this, 'track_local_pickup' ), 10, 4 );
88 }
89
90 /**
91 * Inject collection details onto the order received page.
92 *
93 * @param string $return_value Return value.
94 * @param \WC_Order $order Order object.
95 * @return string
96 */
97 public function show_local_pickup_details( $return_value, $order ) {
98 // Confirm order is valid before proceeding further.
99 if ( ! $order instanceof \WC_Order ) {
100 return $return_value;
101 }
102
103 $shipping_method_ids = ArrayUtil::select( $order->get_shipping_methods(), 'get_method_id', ArrayUtil::SELECT_BY_OBJECT_METHOD );
104 $shipping_method_id = current( $shipping_method_ids );
105
106 // Ensure order used pickup location method, otherwise bail.
107 if ( 'pickup_location' !== $shipping_method_id ) {
108 return $return_value;
109 }
110
111 $shipping_method = current( $order->get_shipping_methods() );
112 $details = $shipping_method->get_meta( 'pickup_details' );
113 $location = $shipping_method->get_meta( 'pickup_location' );
114 $address = $shipping_method->get_meta( 'pickup_address' );
115 $cost = $shipping_method->get_total();
116
117 $lines = array();
118
119 if ( $location ) {
120 $lines[] = sprintf(
121 // Translators: %s location name.
122 __( 'Collection from <strong>%s</strong>:', 'woocommerce' ),
123 $location
124 );
125 }
126
127 if ( $address ) {
128 $lines[] = nl2br( esc_html( str_replace( ',', ', ', $address ) ) );
129 }
130
131 if ( $details ) {
132 $lines[] = wp_kses_post( $details );
133 }
134
135 if ( $cost > 0 ) {
136 $tax_display = get_option( 'woocommerce_tax_display_cart' );
137 $tax = $shipping_method->get_total_tax();
138
139 // Format cost with tax handling.
140 if ( TaxDisplayMode::EXCLUSIVE === $tax_display ) {
141 // Show pickup cost excluding tax.
142 $formatted_cost = wc_price( $cost, array( 'currency' => $order->get_currency() ) );
143 if ( (float) $tax > 0 && $order->get_prices_include_tax() ) {
144 /**
145 * Hook to add tax label to pickup cost.
146 *
147 * @since 6.0.0
148 * @param string $tax_label Tax label.
149 * @param \WC_Order $order Order object.
150 * @param string $tax_display Tax display.
151 * @return string
152 */
153 $formatted_cost .= apply_filters(
154 'woocommerce_order_shipping_to_display_tax_label',
155 '&nbsp;<small class="tax_label">' . WC()->countries->ex_tax_or_vat() . '</small>',
156 $order,
157 $tax_display
158 );
159 }
160 } else {
161 // Show pickup cost including tax.
162 $formatted_cost = wc_price(
163 (float) $cost + (float) $tax,
164 array( 'currency' => $order->get_currency() )
165 );
166 if ( (float) $tax > 0 && ! $order->get_prices_include_tax() ) {
167 /**
168 * Hook to add tax label to pickup cost.
169 *
170 * @since 6.0.0
171 * @param string $tax_label Tax label.
172 * @param \WC_Order $order Order object.
173 * @param string $tax_display Tax display.
174 * @return string
175 */
176 $formatted_cost .= apply_filters(
177 'woocommerce_order_shipping_to_display_tax_label',
178 '&nbsp;<small class="tax_label">' . WC()->countries->inc_tax_or_vat() . '</small>',
179 $order,
180 $tax_display
181 );
182 }
183 }
184
185 $lines[] = '<br>' . sprintf(
186 // Translators: %s is the formatted price.
187 __( 'Pickup cost: %s', 'woocommerce' ),
188 $formatted_cost
189 );
190 }
191
192 // If nothing is available, return original.
193 if ( empty( $lines ) ) {
194 return $return_value;
195 }
196
197 // Join all the lines with a <br> separator.
198 return implode( '<br>', $lines );
199 }
200
201 /**
202 * When using the cart and checkout blocks this method is used to adjust core shipping settings via a filter hook.
203 *
204 * @param array $settings The default WC shipping settings.
205 * @return array|mixed The filtered settings.
206 */
207 public function remove_shipping_settings( $settings ) {
208 if ( CartCheckoutUtils::is_cart_block_default() ) {
209 foreach ( $settings as $index => $setting ) {
210 if ( 'woocommerce_enable_shipping_calc' === $setting['id'] ) {
211 $settings[ $index ]['desc_tip'] = sprintf(
212 /* translators: %s: URL to the documentation. */
213 __( 'This feature is not available when using the <a href="%s">Cart and checkout blocks</a>. Shipping will be calculated at checkout.', 'woocommerce' ),
214 'https://woocommerce.com/document/woocommerce-store-editing/customizing-cart-and-checkout/'
215 );
216 $settings[ $index ]['disabled'] = true;
217 $settings[ $index ]['value'] = 'no';
218 break;
219 }
220 }
221 }
222
223 return $settings;
224 }
225
226 /**
227 * Register Local Pickup settings for rest api.
228 */
229 public function register_settings() {
230 register_setting(
231 'options',
232 'woocommerce_pickup_location_settings',
233 array(
234 'type' => 'object',
235 'description' => 'WooCommerce Local Pickup Method Settings',
236 'default' => array(),
237 'show_in_rest' => array(
238 'name' => 'pickup_location_settings',
239 'schema' => array(
240 'type' => 'object',
241 'properties' => array(
242 'enabled' => array(
243 'description' => __( 'If enabled, this method will appear on the block based checkout.', 'woocommerce' ),
244 'type' => 'string',
245 'enum' => array( 'yes', 'no' ),
246 ),
247 'title' => array(
248 'description' => __( 'This controls the title which the user sees during checkout.', 'woocommerce' ),
249 'type' => 'string',
250 ),
251 'tax_status' => array(
252 'description' => __( 'If a cost is defined, this controls if taxes are applied to that cost.', 'woocommerce' ),
253 'type' => 'string',
254 'enum' => array( ProductTaxStatus::TAXABLE, ProductTaxStatus::NONE ),
255 ),
256 'cost' => array(
257 'description' => __( 'Optional cost to charge for local pickup.', 'woocommerce' ),
258 'type' => 'string',
259 ),
260 ),
261 ),
262 ),
263 )
264 );
265 register_setting(
266 'options',
267 'pickup_location_pickup_locations',
268 array(
269 'type' => 'array',
270 'description' => 'WooCommerce Local Pickup Locations',
271 'default' => array(),
272 'show_in_rest' => array(
273 'name' => 'pickup_locations',
274 'schema' => array(
275 'type' => 'array',
276 'items' => array(
277 'type' => 'object',
278 'properties' => array(
279 'name' => array(
280 'type' => 'string',
281 ),
282 'address' => array(
283 'type' => 'object',
284 'properties' => array(
285 'address_1' => array(
286 'type' => 'string',
287 ),
288 'city' => array(
289 'type' => 'string',
290 ),
291 'state' => array(
292 'type' => 'string',
293 ),
294 'postcode' => array(
295 'type' => 'string',
296 ),
297 'country' => array(
298 'type' => 'string',
299 ),
300 ),
301 ),
302 'details' => array(
303 'type' => 'string',
304 ),
305 'enabled' => array(
306 'type' => 'boolean',
307 ),
308 ),
309 ),
310 ),
311 ),
312 )
313 );
314 }
315
316 /**
317 * Hydrate client settings
318 */
319 public function hydrate_client_settings() {
320 if ( ! wp_script_is( self::LOCAL_PICKUP_ADMIN_JS_HANDLE, 'enqueued' ) ) {
321 // Only hydrate the settings if the script dependent on them is enqueued.
322 return;
323 }
324
325 $locations = get_option( 'pickup_location_pickup_locations', array() );
326
327 $formatted_pickup_locations = array();
328 foreach ( $locations as $location ) {
329 $formatted_pickup_locations[] = array(
330 'name' => $location['name'],
331 'address' => $location['address'],
332 'details' => $location['details'],
333 'enabled' => wc_string_to_bool( $location['enabled'] ),
334 );
335 }
336
337 $has_legacy_pickup = false;
338
339 // Get all shipping zones.
340 $shipping_zones = \WC_Shipping_Zones::get_zones( 'admin' );
341 $international_shipping_zone = new \WC_Shipping_Zone( 0 );
342
343 // Loop through each shipping zone.
344 foreach ( $shipping_zones as $shipping_zone ) {
345 // Get all registered rates for this shipping zone.
346 $shipping_methods = $shipping_zone['shipping_methods'];
347 // Loop through each registered rate.
348 foreach ( $shipping_methods as $shipping_method ) {
349 if ( 'local_pickup' === $shipping_method->id && 'yes' === $shipping_method->enabled ) {
350 $has_legacy_pickup = true;
351 break 2;
352 }
353 }
354 }
355
356 foreach ( $international_shipping_zone->get_shipping_methods( true ) as $shipping_method ) {
357 if ( 'local_pickup' === $shipping_method->id ) {
358 $has_legacy_pickup = true;
359 break;
360 }
361 }
362
363 $settings = array(
364 'pickupLocationSettings' => LocalPickupUtils::get_local_pickup_settings(),
365 'pickupLocations' => $formatted_pickup_locations,
366 'readonlySettings' => array(
367 'hasLegacyPickup' => $has_legacy_pickup,
368 'storeCountry' => WC()->countries->get_base_country(),
369 'storeState' => WC()->countries->get_base_state(),
370 ),
371 );
372
373 wp_add_inline_script(
374 self::LOCAL_PICKUP_ADMIN_JS_HANDLE,
375 sprintf(
376 'var hydratedScreenSettings = %s;',
377 wp_json_encode( $settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES )
378 ),
379 'before'
380 );
381 }
382 /**
383 * Load admin scripts.
384 */
385 public function admin_scripts() {
386 $this->asset_api->register_script( self::LOCAL_PICKUP_ADMIN_JS_HANDLE, 'assets/client/blocks/wc-shipping-method-pickup-location.js', array(), true );
387 }
388
389 /**
390 * Registers the Local Pickup shipping method used by the Checkout Block.
391 */
392 public function register_local_pickup() {
393 if ( CartCheckoutUtils::is_checkout_block_default() ) {
394 $wc_instance = WC();
395 if ( is_object( $wc_instance ) && method_exists( $wc_instance, 'shipping' ) && is_object( $wc_instance->shipping ) && method_exists( $wc_instance->shipping, 'register_shipping_method' ) ) {
396 $wc_instance->shipping->register_shipping_method( new PickupLocation() );
397 } else {
398 wc_get_logger()->error( 'Error registering pickup location: WC()->shipping->register_shipping_method is not available', array( 'source' => 'shipping-controller' ) );
399 }
400 }
401 }
402
403 /**
404 * Declares the Pickup Location shipping method as a Local Pickup method for WooCommerce.
405 *
406 * @param array $methods Shipping method ids.
407 * @return array
408 */
409 public function register_local_pickup_method( $methods ) {
410 $methods[] = 'pickup_location';
411 return $methods;
412 }
413
414 /**
415 * Hides the shipping address on the order confirmation page when local pickup is selected.
416 *
417 * @param array $pickup_methods Method ids.
418 * @return array
419 */
420 public function hide_shipping_address_for_local_pickup( $pickup_methods ) {
421 return array_merge( $pickup_methods, LocalPickupUtils::get_local_pickup_method_ids() );
422 }
423
424 /**
425 * Everytime we save or update local pickup settings, we flush the shipping
426 * transient group.
427 *
428 * @param array $settings The setting array we're saving.
429 * @return array $settings The setting array we're saving.
430 */
431 public function flush_cache( $settings ) {
432 \WC_Cache_Helper::get_transient_version( 'shipping', true );
433 return $settings;
434 }
435 /**
436 * Filter the location used for taxes based on the chosen pickup location.
437 *
438 * @param array $address Location args.
439 * @return array
440 */
441 public function filter_taxable_address( $address ) {
442
443 if ( null === WC()->session ) {
444 return $address;
445 }
446 // We only need to select from the first package, since pickup_location only supports a single package.
447 $chosen_method = current( WC()->session->get( 'chosen_shipping_methods', array() ) ) ?? '';
448 $chosen_method_id = explode( ':', $chosen_method )[0];
449 $chosen_method_instance = explode( ':', $chosen_method )[1] ?? 0;
450
451 // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
452 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 ) ) {
453 $pickup_locations = get_option( 'pickup_location_pickup_locations', array() );
454 $pickup_location = $pickup_locations[ $chosen_method_instance ] ?? array();
455
456 if ( isset( $pickup_location['address'], $pickup_location['address']['country'] ) && ! empty( $pickup_location['address']['country'] ) ) {
457 $address = array(
458 $pickup_locations[ $chosen_method_instance ]['address']['country'],
459 $pickup_locations[ $chosen_method_instance ]['address']['state'],
460 $pickup_locations[ $chosen_method_instance ]['address']['postcode'],
461 $pickup_locations[ $chosen_method_instance ]['address']['city'],
462 );
463 }
464 }
465
466 return $address;
467 }
468
469 /**
470 * Filter the tax location for an order so local pickup orders are taxed at the chosen pickup location's address.
471 *
472 * An order's stored shipping/billing address is not the correct tax location for local pickup: the customer
473 * collects from the store, so tax must be based on the pickup location. `WC_Abstract_Order::get_tax_location()`
474 * already forces the shop base address for local pickup orders; this filter refines that to the specific pickup
475 * location's address, which is captured on the shipping line item at purchase time.
476 *
477 * This is the order-side counterpart to {@see self::filter_taxable_address()}, which performs the equivalent
478 * override for the cart via the customer's taxable address. Resolving the address from the order (rather than the
479 * customer session) keeps the result correct for any order tax read, including admin recalculation, background
480 * jobs, and multiple orders handled within a single request.
481 *
482 * @since 11.0.0
483 *
484 * @param array $location Tax location with 'country', 'state', 'postcode' and 'city' keys.
485 * @param \WC_Abstract_Order $order Order the tax location is being resolved for.
486 * @return array
487 */
488 public function filter_order_tax_location( $location, $order ) {
489 if ( ! $order instanceof \WC_Abstract_Order ) {
490 return $location;
491 }
492
493 // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment
494 if ( true !== apply_filters( 'woocommerce_apply_base_tax_for_local_pickup', true ) ) {
495 return $location;
496 }
497
498 // Use the same canonical local pickup method list that WC_Abstract_Order::get_tax_location() uses to force
499 // the base address. Relying on the currently-registered methods (LocalPickupUtils::get_local_pickup_method_ids())
500 // would miss legacy or deregistered methods on existing orders, leaving them taxed at the store base instead.
501 // phpcs:ignore WooCommerce.Commenting.CommentHooks.MissingHookComment -- Documented in WC_Abstract_Order::get_tax_location().
502 $local_pickup_method_ids = apply_filters( 'woocommerce_local_pickup_methods', array( 'legacy_local_pickup', 'local_pickup' ) );
503
504 foreach ( $order->get_shipping_methods() as $shipping_method ) {
505 if ( ! in_array( $shipping_method->get_method_id(), $local_pickup_method_ids, true ) ) {
506 continue;
507 }
508
509 $pickup_address = $shipping_method->get_meta( '_pickup_location_address' );
510
511 if ( is_array( $pickup_address ) && ! empty( $pickup_address['country'] ) ) {
512 return array(
513 'country' => $pickup_address['country'],
514 'state' => $pickup_address['state'] ?? '',
515 'postcode' => $pickup_address['postcode'] ?? '',
516 'city' => $pickup_address['city'] ?? '',
517 );
518 }
519 }
520
521 return $location;
522 }
523
524 /**
525 * Local Pickup requires all packages to support local pickup. This is because the entire order must be picked up
526 * so that all packages get the same tax rates applied during checkout.
527 *
528 * If a shipping package does not support local pickup (e.g. if disabled by an extension), this filters the option
529 * out for all packages. This will in turn disable the "pickup" toggle in Block Checkout.
530 *
531 * @param array $packages Array of shipping packages.
532 * @return array
533 */
534 public function filter_shipping_packages( $packages ) {
535 // Check all packages for an instance of a collectable shipping method.
536 $valid_packages = array_filter(
537 $packages,
538 function ( $package ) {
539 $shipping_method_ids = ArrayUtil::select( $package['rates'] ?? array(), 'get_method_id', ArrayUtil::SELECT_BY_OBJECT_METHOD );
540 return ! empty( array_intersect( LocalPickupUtils::get_local_pickup_method_ids(), $shipping_method_ids ) );
541 }
542 );
543
544 // Remove pickup location from rates arrays if not all packages can be picked up or support local pickup.
545 if ( count( $valid_packages ) !== count( $packages ) ) {
546 $packages = array_map(
547 function ( $package ) {
548 if ( ! is_array( $package['rates'] ) ) {
549 $package['rates'] = array();
550 return $package;
551 }
552 $package['rates'] = array_filter(
553 $package['rates'],
554 function ( $rate ) {
555 return ! in_array( $rate->get_method_id(), LocalPickupUtils::get_local_pickup_method_ids(), true );
556 }
557 );
558 return $package;
559 },
560 $packages
561 );
562 }
563
564 return $packages;
565 }
566
567 /**
568 * Remove shipping (i.e. delivery, not local pickup) if "Hide shipping costs until an address is entered" is enabled,
569 * and no address has been entered yet.
570 *
571 * Only applies to block checkout because pickup is chosen separately to shipping in that context.
572 *
573 * @param array $packages Array of shipping packages.
574 * @return array
575 */
576 public function remove_shipping_if_no_address( $packages ) {
577 if ( 'shortcode' === WC()->cart->cart_context ) {
578 return $packages;
579 }
580
581 $shipping_cost_requires_address = wc_string_to_bool( get_option( 'woocommerce_shipping_cost_requires_address', 'no' ) );
582
583 // Return early here for a small performance gain if we don't need to hide shipping costs until an address is entered.
584 if ( ! $shipping_cost_requires_address ) {
585 return $packages;
586 }
587
588 $customer = WC()->customer;
589
590 if ( $customer instanceof WC_Customer && $customer->has_full_shipping_address() ) {
591 return $packages;
592 }
593
594 return array_map(
595 function ( $package ) {
596 // Package rates is always an array due to a check in core.
597 $package['rates'] = array_filter(
598 $package['rates'],
599 function ( $rate ) {
600 return $rate instanceof WC_Shipping_Rate && in_array( $rate->get_method_id(), LocalPickupUtils::get_local_pickup_method_ids(), true );
601 }
602 );
603 return $package;
604 },
605 $packages
606 );
607 }
608
609 /**
610 * Track local pickup settings changes via Store API
611 *
612 * @param bool $served Whether the request has already been served.
613 * @param \WP_REST_Response $result The response object.
614 * @param \WP_REST_Request $request The request object.
615 * @return bool
616 */
617 public function track_local_pickup( $served, $result, $request ) {
618 if ( '/wp/v2/settings' !== $request->get_route() ) {
619 return $served;
620 }
621 // Param name here comes from the show_in_rest['name'] value when registering the setting.
622 if ( ! $request->get_param( 'pickup_location_settings' ) && ! $request->get_param( 'pickup_locations' ) ) {
623 return $served;
624 }
625
626 $event_name = 'local_pickup_save_changes';
627
628 $settings = $request->get_param( 'pickup_location_settings' );
629 $locations = $request->get_param( 'pickup_locations' );
630
631 $data = array(
632 'local_pickup_enabled' => 'yes' === $settings['enabled'] ? true : false,
633 'title' => __( 'Pickup', 'woocommerce' ) === $settings['title'],
634 'price' => '' === $settings['cost'] ? true : false,
635 'cost' => '' === $settings['cost'] ? 0 : $settings['cost'],
636 'taxes' => $settings['tax_status'],
637 'total_pickup_locations' => count( $locations ),
638 'pickup_locations_enabled' => count(
639 array_filter(
640 $locations,
641 function ( $location ) {
642 return $location['enabled']; }
643 )
644 ),
645 );
646
647 WC_Tracks::record_event( $event_name, $data );
648
649 return $served;
650 }
651
652 /**
653 * Check if legacy local pickup is activated in any of the shipping zones or in the Rest of the World zone.
654 *
655 * @since 8.8.0
656 *
657 * @return bool
658 */
659 public static function is_legacy_local_pickup_active() {
660 $rest_of_the_world = \WC_Shipping_Zones::get_zone_by( 'zone_id', 0 );
661 $shipping_zones = \WC_Shipping_Zones::get_zones();
662 $rest_of_the_world_data = $rest_of_the_world->get_data();
663 $rest_of_the_world_data['shipping_methods'] = $rest_of_the_world->get_shipping_methods();
664 array_unshift( $shipping_zones, $rest_of_the_world_data );
665
666 foreach ( $shipping_zones as $zone ) {
667 foreach ( $zone['shipping_methods'] as $method ) {
668 if ( 'local_pickup' === $method->id && $method->is_enabled() ) {
669 return true;
670 }
671 }
672 }
673
674 return false;
675 }
676 }
677