PluginProbe
Route ‑ Shipping Protection / 2.4.18
Route ‑ Shipping Protection v2.4.18
2.4.19 2.4.18 2.4.17 2.4.16 2.4.15 2.4.14 2.4.13 2.4.12 2.4.11 2.4.10 2.4.9 2.4.8 2.4.7 2.4.6 2.4.5 2.4.4 2.4.3 2.4.2 2.2.4 2.2.5 2.2.6 2.2.8 2.2.9 2.3.0 2.3.1 All 153 releases
routeapp / public / class-routeapp-public.php

class-routeapp-public.php in Route ‑ Shipping Protection 2.4.18, at public/class-routeapp-public.php

2,601 lines 98.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The public-facing functionality of the plugin.
4 *
5 * @link https://route.com/
6 * @since 1.0.0
7 *
8 * @package Routeapp
9 * @subpackage Routeapp/public
10 */
11
12 use Automattic\WooCommerce\Utilities\OrderUtil;
13
14 /**
15 * The public-facing functionality of routeapp.
16 *
17 *
18 * @package Routeapp
19 * @subpackage Routeapp/public
20 * @author Route App <[email protected]>
21 */
22
23 class Routeapp_Public {
24
25 const HTTP_SUCCESS_CODE = 200;
26 const HTTP_BAD_REQUEST_CODE = 400;
27 const HTTP_CREATED_CODE = 201;
28 const HTTP_CONFLICT_CODE = 409;
29 const HTTP_NOT_FOUND = 404;
30 const HTTP_INTERNAL_SERVER_ERROR_CODE = 500;
31 const DEFAULT_MAX_USD_SUBTOTAL_ALLOWED = 5000;
32 const DEFAULT_MIN_USD_SUBTOTAL_ALLOWED = 0;
33 const ROUTE_LABEL = 'Route Shipping Protection';
34 const DEFAULT_CHECKOUT_WEBHOOK = 'woocommerce_checkout_before_order_review';
35 const ROUTE_PROTECTION_SKU = 'ROUTEINS';
36 const ROUTE_FEE_ITEM_META = '_routeapp_protection_fee';
37 const ROUTE_FEE_SKU_META = '_routeapp_protection_sku';
38 const ALLOWED_QUOTE_TYPE = 'paid_by_customer';
39 const PICKUP_SHIPPING_METHOD_IDS = array( 'local_pickup', 'pickup_location' );
40
41 const AMPLITUDE_API_KEY = [
42 'production' => '6d4f9158886e0c6d78e3bd97663ba920',
43 'stage' => '99a869d62986f6415610c6741057bc92',
44 'dev' => '527af176070cf4b775c6a718f0737f0a'
45 ];
46
47 /**
48 * The ID of this plugin.
49 *
50 * @since 1.0.0
51 * @access private
52 * @var string $plugin_name The ID of this plugin.
53 */
54 private $plugin_name;
55
56 private $route_insurance_data = null;
57
58 /**
59 * The version of this plugin.
60 *
61 * @since 1.0.0
62 * @access private
63 * @var string $version The current version of this plugin.
64 */
65 private $version;
66
67 /**
68 * Routeapp API instance
69 * @since 1.0.0
70 * @access private
71 * @var string $routeapp_api Routeapp API Client
72 */
73 public $routeapp_api_client;
74
75
76 /**
77 * Route Shipment Tracking Module
78 */
79 private $routeapp_shipment_tracking;
80
81 /**
82 * Last Request
83 */
84 private $routeapp_last_request;
85
86 /**
87 * Route Logger
88 */
89 private $routeapp_logger;
90
91 /**
92 * Route Admin Fee
93 */
94 private $routeapp_admin_fee;
95
96 /**
97 * Route Setup
98 */
99 private $route_app_setup_helper;
100
101 /**
102 * Whether the custom Route checkout block has been rendered on this request.
103 */
104 private $route_block_rendered = false;
105
106 /**
107 * Initialize the class and set its properties.
108 *
109 * @since 1.0.0
110 * @param string $plugin_name The name of the plugin.
111 * @param string $version The version of this plugin.
112 */
113 public function __construct($plugin_name, $version)
114 {
115
116 $this->plugin_name = $plugin_name;
117 $this->version = $version;
118 $this->routeapp_api_client = new Routeapp_API_Client($this->routeapp_get_public_token(), $this->routeapp_get_secret_token());
119 $this->routeapp_shipment_tracking = new Routeapp_Shipment_Tracking();
120 $this->routeapp_logger = new Routeapp_Logger_Sentry();
121 $this->routeapp_admin_fee = new Routeapp_Admin_Route_Fee();
122 $this->route_app_setup_helper = new Route_Setup();
123 Route_Setup::ensure_store_hash();
124 $this->add_actions_and_filters();
125 }
126
127 public function add_actions_and_filters() {
128 //CHECKOUT HOOK
129 $checkout_hook = $this->routeapp_get_checkout_hook();
130 add_action($checkout_hook, array($this, 'checkout_route_insurance'), 1, 0);
131
132 add_action('woocommerce_cart_calculate_fees', array($this, 'checkout_route_insurance_fee'), 20, 1);
133 add_action('wp_ajax_woo_get_ajax_data', array($this, 'checkout_route_insurance_set_session'));
134 add_action('wp_ajax_nopriv_woo_get_ajax_data', array($this, 'checkout_route_insurance_set_session'));
135
136 add_action('wp_ajax_woo_check_widget', array($this, 'check_route_widget'));
137 add_action('wp_ajax_nopriv_woo_check_widget', array($this, 'check_route_widget'));
138
139 add_action( 'woocommerce_checkout_create_order_line_item', [ $this, 'routeapp_checkout_create_order_line_item' ], 10, 4 );
140 add_action( 'woocommerce_checkout_create_order_fee_item', [ $this, 'routeapp_checkout_create_order_fee_item' ], 10, 4 );
141 add_action( 'woocommerce_ajax_add_order_item_meta', [$this, 'routeapp_ajax_add_order_item_meta'], 10, 2 );
142 add_filter( 'woocommerce_hidden_order_itemmeta', [$this,'routeapp_woocommerce_hidden_order_itemmeta'], 10, 1);
143
144 add_action( 'added_post_meta', array( $this, 'routeapp_update_tracking_order_api' ), 20, 3 );
145 add_action( 'updated_post_meta', array( $this, 'routeapp_update_tracking_order_api' ), 20, 3 );
146
147 add_action('woocommerce_after_order_object_save', array($this, 'routeapp_update_tracking_order_api_HPOS'), 20, 2);
148
149 add_action( 'added_order_item_meta', array( $this, 'routeapp_update_tracking_order_item_api' ), 20, 3 );
150 add_action( 'updated_order_item_meta', array( $this, 'routeapp_update_tracking_order_item_api' ), 20, 3 );
151 add_action('woocommerce_order_status_processing', array($this, 'routeapp_mark_order'), 10, 1);
152 add_action('woocommerce_order_status_completed', array($this, 'routeapp_mark_order'), 8, 2);
153 add_action('woocommerce_new_order', array($this, 'routeapp_save_quote_to_order'), 10, 1);
154 add_action('rest_api_init', array($this, 'routeapp_register_status_route'));
155 add_action('rest_api_init', array($this, 'routeapp_register_setup_route'));
156 add_action('wp_ajax_get_route_checkout', array($this, 'routeapp_register_checkout_route'));
157 add_action('wp_ajax_nopriv_get_route_checkout', array($this, 'routeapp_register_checkout_route'));
158 add_action('wp_head', array($this, 'wc_routeapp_ajaxurl'));
159 add_action('woocommerce_init', array($this, 'register_shortcode'));
160 add_action('woocommerce_rest_prepare_order_note', array($this, 'check_order_note'));
161 add_action('wp_insert_comment', array($this, 'check_order_note_by_note_id'));
162
163 add_action('wp_ajax_routeapp_add_admin_fee', array($this, 'routeapp_add_admin_fee'));
164 add_action('wp_ajax_nopriv_routeapp_add_admin_fee', array($this, 'routeapp_add_admin_fee'));
165 add_action('wp_ajax_routeapp_remove_admin_fee', array($this, 'routeapp_remove_admin_fee'));
166 add_action('wp_ajax_nopriv_routeapp_remove_admin_fee', array($this, 'routeapp_remove_admin_fee'));
167 add_action('wp_ajax_routeapp_refresh_admin_fee_button', array($this, 'routeapp_refresh_admin_fee_button'));
168 add_action('wc_avatax_api_fee_line_data', array($this, 'routeapp_edit_avatax_fee'));
169
170 // ShipStation: exclude Route fee on XML (legacy) and REST (4.9+) export paths.
171 add_filter('woocommerce_shipstation_no_shipping_item', array($this, 'routeapp_exclude_route_from_shipstation'), 10, 3);
172 add_filter('woocommerce_shipstation_fulfillment_item', array($this, 'routeapp_strip_route_from_shipstation_fulfillment_item'), 10, 4);
173 add_filter('woocommerce_shipstation_export_order_xml', array($this, 'routeapp_strip_route_from_shipstation_xml'), 10, 1);
174
175 add_action('woocommerce_order_details_after_order_table', array($this, 'order_updates_route_tracking'), 1, 1);
176
177 add_action('woocommerce_blocks_checkout_update_order_meta', array($this, 'routeapp_save_order_status_to_order'), 10, 1);
178 add_action('woocommerce_store_api_checkout_update_order_meta', array($this, 'routeapp_save_order_status_to_order'), 10, 1);
179
180 // Register callback for WooCommerce blocks checkout cart updates
181 add_action('woocommerce_blocks_loaded', array($this, 'routeapp_register_blocks_checkout_callback'));
182
183 // Inject Route widget before order summary in checkout block (default placement)
184 add_filter('render_block_woocommerce/checkout-order-summary-block', array($this, 'routeapp_checkout_block_before_order_summary'), 10, 3);
185
186 // Register the Route Protection custom checkout block
187 add_action('init', array($this, 'routeapp_register_checkout_block'));
188
189 // Register IntegrationInterface for WooCommerce Blocks
190 add_action('woocommerce_blocks_checkout_block_registration', array($this, 'routeapp_register_blocks_integration'));
191
192 // Enqueue widget assets early on cart/checkout for stable script order with Blocks SSR.
193 add_action('wp_enqueue_scripts', array($this, 'routeapp_maybe_enqueue_widget_assets'), 20);
194 add_action('woocommerce_blocks_enqueue_checkout_block_scripts_before', array($this, 'routeapp_maybe_enqueue_widget_assets'));
195 }
196
197 /**
198 * Hide order line_item meta_data from Route App
199 *
200 * @param $arr
201 * @return mixed
202 */
203 function routeapp_woocommerce_hidden_order_itemmeta($arr) {
204 $arr[] = '_routeapp_source_product_id';
205 $arr[] = '_routeapp_image_url';
206 $arr[] = '_routeapp_virtual';
207 $arr[] = '_routeapp_weight_value';
208 $arr[] = '_routeapp_weight_unit';
209 $arr[] = '_routeapp_origin_location';
210 $arr[] = self::ROUTE_FEE_ITEM_META;
211 $arr[] = self::ROUTE_FEE_SKU_META;
212 return $arr;
213 }
214
215 /**
216 * Fill metadata
217 *
218 * @param $item WC_Order_Item
219 * @param $product WC_Product
220 * @param $entity_type enum('order_item','item')
221 * @return void
222 */
223 public static function fillMetaData($item, $product, $entity_type='order_item') {
224
225 $sourceProductId = $product->get_id();
226 $productImageUrl = (wp_get_attachment_url($product->get_image_id())) ? wp_get_attachment_url($product->get_image_id()) : null;
227 $weightValue = $product->get_weight();
228
229 $originLocation = json_encode([
230 'address' => get_option('woocommerce_store_address'),
231 'address2' => get_option('woocommerce_store_address_2'),
232 'store_city' => get_option('woocommerce_store_city'),
233 'store_postcode' => get_option('woocommerce_store_postcode'),
234 'default_country' => get_option('woocommerce_default_country'),
235 ]);
236
237
238 switch($entity_type) {
239 case 'order_item':
240 //add source_product_id as order line_item meta_data
241 self::addItemMeta($item, '_routeapp_source_product_id', $sourceProductId);
242
243 //add image_url as order line_item meta_data
244 self::addItemMeta($item, '_routeapp_image_url', $productImageUrl);
245
246 //add weight_value as order line_item meta_data
247 self::addItemMeta($item, '_routeapp_weight_value', $weightValue);
248
249 //add weight_unit as order line_item meta_data
250 self::addItemMeta($item, '_routeapp_weight_unit', get_option('woocommerce_weight_unit'));
251
252 //check if product is virtual
253 if ($product->is_virtual()) {
254 //add virtual as order line_item meta_data
255 self::addItemMeta($item, '_routeapp_virtual', 1);
256 }
257 //add origin location as order line_item meta_data
258 self::addItemMeta($item, '_routeapp_origin_location', $originLocation);
259 break;
260 case 'item':
261 //add source_product_id as order line_item meta_data
262 self::addOrderItemMeta($item->get_id(), '_routeapp_source_product_id', $sourceProductId);
263
264 //add image_url as order line_item meta_data
265 self::addOrderItemMeta($item->get_id(), '_routeapp_image_url', $productImageUrl);
266
267 //add weight_value as order line_item meta_data
268 self::addOrderItemMeta($item->get_id(), '_routeapp_weight_value', $weightValue);
269
270 //add weight_unit as order line_item meta_data
271 self::addOrderItemMeta($item->get_id(), '_routeapp_weight_unit', get_option('woocommerce_weight_unit'));
272
273 //check if product is virtual
274 if ($product->is_virtual()) {
275 //add virtual as order line_item meta_data
276 self::addOrderItemMeta($item->get_id(), '_routeapp_virtual', 1);
277 }
278 //add origin location as order line_item meta_data
279 self::addOrderItemMeta($item->get_id(), '_routeapp_origin_location', $originLocation);
280 break;
281 }
282 }
283
284 /**
285 * Add order line_item meta_data on ajax add to cart
286 *
287 * @param $item_id String
288 * @param $item WC_Order_Item
289 * @return false|void
290 */
291 public static function routeapp_ajax_add_order_item_meta( $item_id, $item) {
292 $product = $item->get_product(); // Get the WC_Product object
293 if ($product instanceof WC_Product) {
294 try {
295 self::fillMetaData($item, $product, 'item');
296 }catch (Exception $exception) {
297 return false;
298 }
299 }
300 }
301
302 /**
303 * @param $itemId WC_Order_Item ID
304 * @param $key String
305 * @param $value String
306 * @return bool
307 */
308 private static function addOrderItemMeta($itemId, $key, $value) {
309 try {
310 $fieldExists = wc_get_order_item_meta( $itemId, $key, true);
311 if (!$fieldExists) {
312 wc_add_order_item_meta($itemId, $key, $value);
313 }
314 }catch (Exception $exception) {
315 return false;
316 }
317 return true;
318 }
319
320 /**
321 * @param $item WC_Order_Item
322 * @param $key String
323 * @param $value String
324 * @return bool
325 */
326 private static function addItemMeta($item, $key, $value) {
327 try {
328 $fieldExists = $item->get_meta($key, true);
329 if (!$fieldExists) {
330 $item->add_meta_data( $key, $value );
331 }
332 }catch (Exception $exception) {
333 return false;
334 }
335 return true;
336 }
337
338 /**
339 * Add order line_item meta_data on checkout add to cart
340 *
341 * @param $item WC_Order_Item
342 * @param $cart_item_key String
343 * @param $values Object
344 * @param $order WC_Order
345 * @return false|void
346 */
347 public function routeapp_checkout_create_order_line_item( $item, $cart_item_key, $values, $order ) {
348 if ( $order instanceof WC_Order ) {
349 $product = $item->get_product(); // Get the WC_Product object
350 if ($product instanceof WC_Product) {
351 try {
352 self::fillMetaData($item, $product, 'order_item');
353 }catch (Exception $exception) {
354 return false;
355 }
356 }
357 }
358 }
359
360 /**
361 * Tag the Route protection fee on checkout so exporters can identify it
362 * without relying on a translated fee name.
363 *
364 * Accepted limitation: WC cart fees have no SKU/meta, so we match by name
365 * here. That is reliable at creation time because Route just set the fee
366 * name via routeapp_get_insurance_label() in the same request/locale.
367 * Once marked, later lookups use ROUTE_FEE_ITEM_META / ROUTE_FEE_SKU_META.
368 *
369 * @param WC_Order_Item_Fee $item Fee order item.
370 * @param string $fee_key Cart fee key.
371 * @param object $fee Cart fee object.
372 * @param WC_Order $order Order being created.
373 */
374 public function routeapp_checkout_create_order_fee_item( $item, $fee_key, $fee, $order ) {
375 $fee_name = '';
376 if ( is_object( $fee ) && isset( $fee->name ) ) {
377 $fee_name = $fee->name;
378 } elseif ( is_callable( array( $item, 'get_name' ) ) ) {
379 $fee_name = $item->get_name();
380 }
381
382 if ( self::routeapp_is_route_protection_fee_name( $fee_name ) ) {
383 self::routeapp_mark_protection_fee_item( $item );
384 }
385 }
386
387 public function routeapp_add_admin_fee() {
388 $this->routeapp_admin_fee->routeapp_add_admin_fee();
389 }
390
391 public function routeapp_remove_admin_fee() {
392 $this->routeapp_admin_fee->routeapp_remove_admin_fee();
393 }
394
395 public function routeapp_refresh_admin_fee_button() {
396 $this->routeapp_admin_fee->routeapp_refresh_admin_fee_button();
397 }
398
399 public function register_shortcode() {
400 add_shortcode( 'route', array( $this, 'route_widget_shortcode' ) );
401 }
402
403 public function get_allowed_quote_type() {
404 return self::ALLOWED_QUOTE_TYPE;
405 }
406
407 public function check_route_widget() {
408 if (!isset($_POST) || !$_POST['shipping_method']) {
409 wp_send_json_error('its missing shipping method on the request');
410 return;
411 }
412 $shipping_method = isset($_POST['shipping_method']) ? $_POST['shipping_method'] : '';
413 $response = [];
414 $response['show_widget'] = $this->routeapp_is_shipping_method_allowed($shipping_method);
415 wp_send_json_success( $response );
416 return;
417 }
418
419 /**
420 * set routeapp session
421 *
422 * @since 1.0.0
423 */
424 public function checkout_route_insurance_set_session() {
425 if ( isset($_POST['checkbox']) && $_POST['checkbox'] === 'true' ){
426 WC()->session->set('checkbox_checked', true );
427 } else {
428 WC()->session->set( 'checkbox_checked', false );
429 }
430 }
431
432 /**
433 * Add route insurance fee in checkout and cart.
434 * Only runs during woocommerce_cart_calculate_fees. The old
435 * woocommerce_checkout_before_order_review binding is gone — it fired
436 * after totals were already calculated and re-added the fee after opt-out.
437 *
438 * @since 1.0.0
439 * @param Object $cart_or_checkout
440 */
441 public function checkout_route_insurance_fee( $cart_or_checkout ) {
442 if ( (is_admin() && ! defined( 'DOING_AJAX' )) ) return;
443
444 // Widget AJAX / fragment hooks must not add fees (re-adds after opt-out, races coupons).
445 if ( function_exists( 'doing_action' ) && ! doing_action( 'woocommerce_cart_calculate_fees' ) ) {
446 return;
447 }
448
449 // Determine if we received a WC_Cart or WC_Checkout object
450 $is_checkout_object = is_a( $cart_or_checkout, 'WC_Checkout' );
451 $cart = $is_checkout_object ? WC()->cart : $cart_or_checkout;
452
453 // Use WC()->cart as fallback if not a valid WC_Cart
454 if ( is_null( $cart ) || ! is_a( $cart, 'WC_Cart' ) ) {
455 $cart = WC()->cart;
456 }
457
458 $checkbox = WC()->session->get( 'checkbox_checked' );
459
460 $cartRef = $cart->get_cart_hash();
461 $cartTotal = round($this->get_cart_subtotal_with_only_shippable_items($cart), 2);
462 $currency = get_woocommerce_currency();
463 $cartItems = $this->get_cart_shippable_items($cart);
464 $route_insurance_quote = $this->routeapp_get_quote_from_api($cartRef, $cartTotal, $currency, $cartItems);
465 $this->route_insurance_data = $route_insurance_quote;
466
467 $route_insurance_amount = false;
468 //only add fee if the quote is covered by customer and has a valid amount
469 if (isset($route_insurance_quote->premium->amount) &&
470 (isset($route_insurance_quote->payment_responsible->type)) && $route_insurance_quote->payment_responsible->type==self::ALLOWED_QUOTE_TYPE) {
471 $route_insurance_amount = $route_insurance_quote->premium->amount;
472 }
473
474 if (is_null($checkbox) &&
475 isset($route_insurance_quote->payment_responsible) &&
476 property_exists($route_insurance_quote->payment_responsible, 'ToggleState')) {
477 $checkbox = $route_insurance_quote->payment_responsible->ToggleState;
478 WC()->session->set('checkbox_checked', $checkbox);
479 }
480
481 // First checkout paint still defaults ON when ToggleState is missing
482 // (pre-existing behavior). Do not default ON during checkout AJAX —
483 // that re-added the fee after opt-out when the session was empty.
484 if ( $this->routeapp_should_default_fee_opt_in( $checkbox ) ) {
485 $checkbox = true;
486 if ( WC()->session ) {
487 WC()->session->set( 'checkbox_checked', true );
488 }
489 }
490
491 Route_Setup::set_setup_check_widget(Route_Setup::SETUP_CHECK_WIDGET_PHP);
492 if ( ($checkbox === TRUE || $checkbox === 1) && $route_insurance_amount > 0 && $this->routeapp_is_shipping_method_allowed()) {
493 $cart->add_fee( $this->routeapp_get_insurance_label(),
494 $route_insurance_amount,
495 $this->routeapp_get_fee_taxable(),
496 $this->routeapp_get_taxable_class());
497 }
498 }
499
500 /**
501 * WooCommerce / CheckoutWC totals refresh (update_order_review, etc.).
502 *
503 * @return bool
504 */
505 public function routeapp_is_checkout_ajax_refresh() {
506 if ( ! empty( $_REQUEST['wc-ajax'] ) ) {
507 return true;
508 }
509
510 return defined( 'DOING_AJAX' ) && DOING_AJAX;
511 }
512
513 /**
514 * Keep the pre-existing first-load default (fee ON) when there is no
515 * session choice and no ToggleState. Skip that default on AJAX refreshes.
516 *
517 * @param mixed $checkbox Session checkbox value.
518 * @return bool
519 */
520 public function routeapp_should_default_fee_opt_in( $checkbox ) {
521 return is_null( $checkbox )
522 && function_exists( 'is_checkout' )
523 && is_checkout()
524 && ! $this->routeapp_is_checkout_ajax_refresh();
525 }
526
527 /**
528 * Create checkout
529 * @since 1.0.0
530 *
531 */
532 public function checkout_route_insurance() {
533 echo do_shortcode('[route]');
534 }
535
536 public function order_updates_route_tracking($order) {
537 if ($this->routeapp_route_show_order_updates()) {
538 echo $this->route_show_thankyoupage_asset($order);
539 }
540 }
541
542 public function route_show_thankyoupage_asset($order) {
543 $output = '';
544
545 $site = parse_url(get_site_url(get_current_blog_id()), PHP_URL_HOST);
546 $response = Routeapp_API_Client::getInstance()->asset_settings($site);
547
548 if (is_wp_error($response) ||
549 (isset($response['response']['code']) && $response['response']['code'] == 404) ||
550 is_null($response['body'])) {
551 return $output;
552 } else {
553
554 $body = json_decode($response['body']);
555
556 if (!is_null($body) &&
557 !is_null($body->asset_settings) &&
558 !is_null($body->asset_settings->asset_live) &&
559 $body->asset_settings->asset_live) {
560 $raw_html = $body->asset_settings->asset_content->raw_html;
561 $css = $body->asset_settings->asset_content->css_url;
562 $js = isset($body->asset_settings->asset_content->js_url) ? $body->asset_settings->asset_content->js_url : '';
563 $jsPath = '';
564 if (!empty($js)) {
565 $jsPath = '<script type="text/javascript" src="' . $js . '"></script>';
566 }
567 $output = '<link rel="stylesheet" href="' . $css . '" type="text/css" />' .
568 $jsPath .
569 '<section class=woocommerce-order-updates>' .
570 '<h2 class="woocommerce-order-updates__title">Order Updates</h2>' .
571 '</section>' .
572 '<div><span class="os-order-number" style="display:none;">'.$order->get_id().'</span></div>'.
573 '<div>' .
574 $raw_html .
575 '</div>' .
576 '<div>&nbsp;</div>';
577
578 wp_enqueue_script($this->plugin_name . '-analytics', 'https://cdn.routeapp.io/route-analytics/route-analytics.js', array(), $this->version, false);
579 wp_enqueue_script($this->plugin_name, plugin_dir_url(__FILE__) . 'js/routeapp-analytics.js', array('jquery'), $this->version, false);
580 }
581 }
582
583 return $output;
584 }
585
586 /**
587 * Enqueue Route widget JS/CSS with correct dependency order.
588 * routeapp-public-pbc.js must load after route-protection-widget.js (Route.Protection).
589 */
590 public function routeapp_enqueue_widget_assets() {
591 if ( is_admin() || empty( $this->routeapp_get_public_token() ) ) {
592 return;
593 }
594 if ( ! function_exists( 'WC' ) || ! WC()->cart ) {
595 return;
596 }
597
598 $custom_env = getenv( 'ROUTEAPP_ENVIRONMENT_ENDPOINT' );
599 if ( is_null( $custom_env ) || ! $custom_env ) {
600 $custom_env = isset( $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] ) ? $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] : '';
601 }
602 $custom_env = empty( $custom_env ) ? 'production' : $custom_env;
603 if ( ! isset( self::AMPLITUDE_API_KEY[ $custom_env ] ) ) {
604 $custom_env = 'production';
605 }
606
607 $widget_handle = $this->plugin_name . '-widget';
608 wp_enqueue_script(
609 $widget_handle,
610 'https://protection-widget.route.com/route-protection-widget.js',
611 array(),
612 $this->version,
613 true
614 );
615
616 $amplitude_key = self::AMPLITUDE_API_KEY[ $custom_env ];
617
618 // Load Amplitude SDK (standard version, not snippet)
619 wp_enqueue_script( 'amplitude-js', "https://cdn.amplitude.com/script/$amplitude_key.js", array(), null, true );
620 // Enqueue Amplitude Analytics module (depends on jQuery and Amplitude SDK)
621 wp_enqueue_script(
622 $this->plugin_name . '-amplitude-analytics',
623 plugin_dir_url( __FILE__ ) . 'js/amplitude-analytics.js',
624 array( 'jquery', 'amplitude-js' ),
625 $this->version,
626 true
627 );
628 $pbc_deps = array( 'jquery', $widget_handle, $this->plugin_name . '-amplitude-analytics' );
629 if ( $this->routeapp_should_add_wc_blocks_checkout_dependency() ) {
630 $pbc_deps[] = 'wc-blocks-checkout';
631 }
632
633 /**
634 * Filter script dependencies for routeapp-public-pbc.js.
635 *
636 * @param string[] $pbc_deps Script handles routeapp-public-pbc.js depends on.
637 */
638 $pbc_deps = apply_filters( 'routeapp_pbc_script_dependencies', $pbc_deps );
639
640 // Main script depends on amplitude-analytics when available.
641 wp_enqueue_script(
642 $this->plugin_name,
643 plugin_dir_url( __FILE__ ) . 'js/routeapp-public-pbc.js',
644 $pbc_deps,
645 $this->version,
646 true
647 );
648
649 /**
650 * Calculate total Route fee
651 * Uses the premium amount from route insurance data if available,
652 * otherwise defaults to 0.98 as a fallback fee amount
653 */
654 $totalRouteFee = ( is_object( $this->route_insurance_data ) && isset( $this->route_insurance_data->premium->amount ) )
655 ? (float) $this->route_insurance_data->premium->amount
656 : 0.98;
657
658 // Pass cart data to JavaScript - send to amplitude-analytics.js
659 wp_localize_script(
660 $this->plugin_name . '-amplitude-analytics',
661 'cartData',
662 array(
663 'cartId' => WC()->cart->get_cart_hash(),
664 'amplitudeKey' => $amplitude_key,
665 'widgetId' => uniqid( 'widget-', true ),
666 'subtotal' => round( $this->get_cart_subtotal_with_only_shippable_items( WC()->cart ), 2 ),
667 'totalRouteFee' => $totalRouteFee,
668 'environment' => $custom_env,
669 'currency' => get_woocommerce_currency(),
670 'merchantId' => $this->routeapp_api_client->get_merchant_id(),
671 )
672 );
673
674 wp_localize_script(
675 $this->plugin_name,
676 'routeappWidgetConfig',
677 array(
678 'excludedShippingMethods' => array_values( (array) $this->routeapp_get_excluded_shipping_methods() ),
679 )
680 );
681
682 wp_enqueue_style(
683 $this->plugin_name . '-widget',
684 plugin_dir_url( __FILE__ ) . 'css/routeapp-widget.css',
685 array(),
686 $this->version,
687 'all'
688 );
689 }
690
691 /**
692 * Enqueue widget assets on cart, checkout, and Blocks checkout script hook.
693 */
694 public function routeapp_maybe_enqueue_widget_assets() {
695 if ( is_admin() || empty( $this->routeapp_get_public_token() ) ) {
696 return;
697 }
698 if ( function_exists( 'is_cart' ) && function_exists( 'is_checkout' ) && ! is_cart() && ! is_checkout() ) {
699 return;
700 }
701 $this->routeapp_enqueue_widget_assets();
702 }
703
704 public function route_widget_shortcode()
705 {
706 if(is_admin() || empty($this->routeapp_get_public_token())) return;
707
708 // Enqueue only outside cart/checkout; wp_enqueue_scripts handles those pages.
709 if ( function_exists( 'is_cart' ) && function_exists( 'is_checkout' ) && ! is_cart() && ! is_checkout() ) {
710 $this->routeapp_enqueue_widget_assets();
711 }
712
713 $custom_env = getenv('ROUTEAPP_ENVIRONMENT_ENDPOINT');
714 if (is_null($custom_env) || !$custom_env) {
715 $custom_env = isset($_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT']) ? $_SERVER['ROUTEAPP_ENVIRONMENT_ENDPOINT'] : '';
716 }
717 $custom_env = empty($custom_env) ? 'production' : $custom_env;
718
719 global $wp;
720 $parsedUrl = parse_url(home_url( $wp->request));
721
722 switch($custom_env) {
723 case 'stage':
724 $custom_env = 'Route.Environment.Dev';
725 break;
726 default:
727 $custom_env = 'Route.Environment.Production';
728 break;
729 }
730 $checkbox = WC()->session->get( 'checkbox_checked' );
731 $checkbox = $checkbox ? 'Route.Coverage.ActiveByDefault' : 'Route.Coverage.InactiveByDefault';
732
733 $divId = is_checkout() ? 'route-checkout-radio' : 'route-cart-radio';
734
735 $return = '<div id="'.$divId.'">' .
736 '<input type="hidden" value="'. get_woocommerce_currency() .'" class="routeapp-currency">'.
737 '<input type="hidden" value="'. round($this->get_cart_subtotal_with_only_shippable_items(WC()->cart), 2) .'" class="routeapp-subtotal">'.
738 '<input type="hidden" value="' . admin_url('admin-ajax.php') . '" class="routeapp-ajax-url">'.
739 '<input type="hidden" value="' . $parsedUrl['host'] . '" class="routeapp-store-domain">'.
740 '<input type="hidden" value="' . get_option('blogname') . '" class="routeapp-store-name">'.
741 '<input type="hidden" value="' . $this->routeapp_api_client->get_merchant_id() . '" class="routeapp-merchant-id">'.
742 '<input type="hidden" value="' . $custom_env . '" class="routeapp-env">'.
743 '<input type="hidden" value="' . $checkbox .'" class="routeapp-checkbox-cookie">';
744
745 $chosen_methods = ( WC()->session ) ? WC()->session->get( 'chosen_shipping_methods' ) : array();
746 if ( ! empty( $chosen_methods[0] ) && ! $this->routeapp_is_shipping_method_allowed( $chosen_methods[0] ) ) {
747 $return .= '<input type="hidden" value="true" class="routeapp-invalid-shipping-method">';
748 }
749 return $return . '<div id="RouteWidget" class="route-div"></div></div>' ;
750 }
751
752
753 public function wc_routeapp_ajaxurl() {
754 echo '<script type="text/javascript">' .
755 'var wc_routeapp_ajaxurl = "' . admin_url('admin-ajax.php') . '"' .
756 '</script>';
757 }
758
759 /**
760 * get amount from RouteApp API
761 *
762 * @param $cartRef
763 * @param $cartTotal
764 * @param $currency
765 * @param $cartItems
766 * @return false|mixed
767 */
768 public function routeapp_get_quote_from_api($cartRef, $cartTotal, $currency, $cartItems) {
769 if (!$cartRef) return false;
770 if (!$cartTotal) return false;
771 $currency = !$currency ? get_woocommerce_currency() : $currency;
772
773 $response = $this->routeapp_api_client->get_quote($cartRef, $cartTotal, $currency, $cartItems);
774
775 if (is_wp_error($response)) {
776 return false;
777 } else {
778 if (isset($response['response']['code']) && $response['response']['code'] == 401) {
779 return false;
780 }
781 $price_data = json_decode($response['body']);
782 Route_Setup::set_setup_check_widget(Route_Setup::SETUP_CHECK_WIDGET_JS);
783 return $price_data;
784 }
785 }
786
787 /**
788 * create order data sending for API
789 * @since 1.0.0
790 * @param integer $order_id
791 */
792 public function create_order_data_api($order_id) {
793
794 $order = wc_get_order( $order_id );
795
796 if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
797 && OrderUtil::custom_orders_table_usage_is_enabled() ) {
798 // HPOS usage is enabled.
799 $quote = $order->get_meta('_routeapp_quote');
800 } else {
801 // Traditional CPT-based orders are in use.
802 $quote = get_post_meta($order->get_id(), '_routeapp_quote', true);
803 }
804
805 $insurance_selected = false;
806 $insurance_amount = false;
807
808 foreach ($order->get_items('fee') as $fee) {
809 if (self::routeapp_is_route_protection_fee($fee)) {
810 $insurance_selected = true;
811 $insurance_amount = $fee->get_amount();
812 if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
813 && OrderUtil::custom_orders_table_usage_is_enabled() ) {
814 // HPOS usage is enabled.
815 $order->update_meta_data('_routeapp_route_charge', $fee->get_amount() );
816 $order->save();
817 } else {
818 // Traditional CPT-based orders are in use.
819 update_post_meta( $order->get_id(), '_routeapp_route_charge', $fee->get_amount() );
820 }
821 }
822 }
823 $quoteId = 'Invalid';
824 if ($quote) {
825 $quoteDecoded = json_decode($quote);
826 $quoteId = $quoteDecoded->id;
827 }
828
829 $destination = array(
830 "first_name" => $order->get_billing_first_name(),
831 "last_name" => $order->get_billing_last_name(),
832 "street_address1" => $order->get_shipping_address_1(),
833 "street_address2" => $order->get_shipping_address_2(),
834 "city" => $order->get_shipping_city(),
835 "province" => $order->get_shipping_state(),
836 "zip" => $order->get_shipping_postcode(),
837 "country_code" => $order->get_shipping_country(),
838 );
839 $customerDetails = array(
840 "first_name" => $order->get_billing_first_name(),
841 "last_name" => $order->get_billing_last_name(),
842 "email" => $order->get_billing_email(),
843 "phone" => $order->get_billing_phone()
844 );
845 $order_data = $order->get_data();
846 $is_shipping_method_allowed = $this->routeapp_is_shipping_method_allowed(false, $order);
847
848 $itemdata = array();
849
850 foreach ($order->get_items() as $item) {
851 ## Using WC_Order_Item_Product methods ##
852 $product = $item->get_product(); // Get the WC_Product object
853
854 if ($product instanceof WC_Product) {
855
856 $isProductVirtual = (bool)$product->is_virtual();
857
858 $arrItem = array(
859 "delivery_method" => $isProductVirtual ? 'digital' :
860 ($is_shipping_method_allowed ? 'ship_to_home' : 'ship_to_store'),
861 "is_insured" => $isProductVirtual ? false :
862 ($is_shipping_method_allowed ? true : false),
863 "source_product_id" => $product->get_id(),
864 "source_id" => strval($item->get_id()),
865 "sku" => $product->get_sku(),
866 "name" => $product->get_title(),
867 "price" => $product->get_price(),
868 "quantity" => $item->get_quantity(),
869 "image_url" => (wp_get_attachment_url($product->get_image_id())) ? wp_get_attachment_url($product->get_image_id()) : null,
870 "upc" => ""// WooCommerce doesn’t provide UPC by default
871 );
872
873 array_push($itemdata, $arrItem);
874 }
875
876 }
877
878 if ( class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
879 && OrderUtil::custom_orders_table_usage_is_enabled() ) {
880 // HPOS usage is enabled.
881 $order->update_meta_data('_routeapp_route_protection', $insurance_selected );
882 $order->update_meta_data('_routeapp_version', ROUTEAPP_VERSION );
883
884 $order->save();
885 } else {
886 // Traditional CPT-based orders are in use.
887 update_post_meta( $order->get_id(), '_routeapp_route_protection', $insurance_selected );
888 update_post_meta( $order->get_id(), '_routeapp_version', ROUTEAPP_VERSION );
889 }
890
891 $orderData = array(
892 'quote_id' => $quoteId,
893 'source_order_id' => $order->get_id(),
894 'source_order_number' => $order->get_id(),
895 'subtotal' => $order->get_subtotal(),
896 'order_total' => $order->get_total(),
897 'shipping_total' => $order->get_shipping_total(),
898 'discounts_total' => $order->get_discount_total(),
899 'source_created_on' => date(DATE_ATOM, strtotime($order->get_date_created())),
900 'source_updated_on' => date(DATE_ATOM, strtotime($order->get_date_modified())),
901 'taxes' => $order_data['total_tax'],
902 'insurance_selected' => $insurance_selected,
903 'customer_details' => $customerDetails,
904 'shipping_details' => $destination,
905 'line_items' => $itemdata,
906 'currency' => $order->get_currency(),
907 'amount_covered' => $this->get_order_subtotal($order),
908 'origin' => 'woocommerce-module',
909 );
910 //if customer covers the order, pass the paid to insure with Route Fee value
911 if ($insurance_selected && $insurance_amount) {
912 $orderData['paid_to_insure'] = $insurance_amount;
913 }
914
915 return $orderData;
916 }
917
918 /**
919 * Update tracking order data sending for API
920 * @since 1.0.0
921 * @param integer $item_id
922 * @param object $order
923 * @param string $post
924 */
925 public function routeapp_update_tracking_order_api($item_id, $order, $post) {
926 if (is_null($order) || get_post_type($order) !== 'shop_order' || $post == '_edit_lock') {
927 return false;
928 }
929 $this->routeapp_shipment_tracking->update($order, $this);
930 }
931
932 /**
933 * Update tracking order data sending when HPOS is enable
934 * @param object $order
935 * @param object $data_store
936 */
937 public function routeapp_update_tracking_order_api_HPOS($order, $data_store) {
938 static $already_executed = false;
939
940 if ($already_executed) {
941 return false; // Prevent recursion
942 }
943
944 $already_executed = true;
945
946 // Ensure the $order object is valid and of type 'shop_order'
947 if (!$order || $order->get_type() !== 'shop_order') {
948 return false;
949 }
950
951 $this->routeapp_shipment_tracking->update($order->get_id(), $this);
952 }
953
954 /**
955 * Update tracking order item data sending for API
956 * @since 1.0.0
957 * @param integer $order_id
958 * @param object $post
959 * @param boolean $update
960 */
961 public function routeapp_update_tracking_order_item_api($item_id, $order_item_id, $post) {
962 if (is_null($order_item_id) ||
963 !is_admin() ||
964 $this->_isShippingTrackingCall($post)
965 ) {
966 return false;
967 }
968 $this->routeapp_shipment_tracking->update($order_item_id, $this);
969 }
970
971 /**
972 * Check if the post comes for shipment tracking
973 * @param $post
974 * @return bool
975 */
976 private function _isShippingTrackingCall($post) {
977 $isNotShippingTracking = $post != '_wc_shipment_tracking_items' &&
978 $post != '_bst_tracking_number' &&
979 $post != 'mimo_tracking_number' &&
980 $post != '_aftership_tracking_number' &&
981 $post != 'ywot_tracking_code' &&
982 $post != 'ph_shipment_tracking_shipping_service' &&
983 $post != 'ph_shipment_tracking_ids' &&
984 $post != 'usps_evs_label_details_array' &&
985 $post != '_order_trackno' &&
986 $post != '_order_trackno1' &&
987 $post != '_order_trackno2' &&
988 $post != '_order_trackno3' &&
989 $post != '_order_trackno4' &&
990 $post != '_vi_wot_order_item_tracking_data' &&
991 $post != 'wc_connect_labels';
992
993 return apply_filters('routeapp_meta_key_is_not_shipping_related', $isNotShippingTracking, $post);
994 }
995
996
997 public function routeapp_update_merchant_api( $merchant_id, $merchantData ) {
998 if ( is_null($merchant_id) ) {
999 return false;
1000 }
1001
1002 $response = $this->routeapp_api_client->update_merchant( $merchant_id, $merchantData );
1003 if ( is_wp_error($response) ) {
1004 return false;
1005 }
1006
1007 return true;
1008 }
1009
1010
1011 /**
1012 * Updates Route-related metadata to the order after it is placed in draft status.
1013 *
1014 * This method is triggered when an order is created in draft status and is responsible for
1015 * adding meta fields with the list Route sync status.
1016 * This prevents incorrect synchronization of draft orders.
1017 *
1018 * @param int $order_id The ID of the WooCommerce order.
1019 */
1020 public function routeapp_save_order_status_to_order( $order_id ) {
1021 if ( ! $order_id || ! WC()->session ) {
1022 return;
1023 }
1024
1025 try {
1026 $order = wc_get_order( $order_id );
1027
1028 if ( ! $order ) {
1029 throw new Exception( "Order with ID {$order_id} could not be retrieved." );
1030 }
1031 $is_hpos = class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
1032 && OrderUtil::custom_orders_table_usage_is_enabled();
1033
1034 $acceptedOrderStatuses = get_option('routeapp_included_order_statuses') ? get_option('routeapp_included_order_statuses') : [];
1035 $acceptedCanceledStatuses = get_option('routeapp_cancel_order_statuses') ? get_option('routeapp_cancel_order_statuses') : [];
1036
1037 $updated = false;
1038 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_included_order_statuses', $acceptedOrderStatuses );
1039 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_cancel_order_statuses', $acceptedCanceledStatuses );
1040
1041 if ($updated && $is_hpos) {
1042 $order->save();
1043 }
1044 } catch ( Exception $e ) {
1045 error_log( "An error occurred while updating Route order status meta fields for order ID {$order_id}: " . $e->getMessage() );
1046 }
1047 }
1048
1049
1050 /**
1051 * Save into order meta_data the quote from the last request made on session
1052 *
1053 * @param $order_id
1054 */
1055 public function routeapp_save_quote_to_order($order_id )
1056 {
1057 if (!$order_id && !WC()->session) {
1058 return;
1059 }
1060 $order = wc_get_order($order_id);
1061 if (!$order) {
1062 return;
1063 }
1064 //MSS-6248, Support High-Performance Order Storage (HPOS); fall back to legacy order storage when disabled.
1065 $is_hpos = class_exists('Automattic\WooCommerce\Utilities\OrderUtil')
1066 && OrderUtil::custom_orders_table_usage_is_enabled();
1067
1068 $updated = false;
1069
1070 if ($is_hpos) {
1071 $quote = $order->get_meta('_routeapp_quote');
1072 } else {
1073 // Traditional CPT-based orders are in use.
1074 $quote = get_post_meta($order->get_id(), '_routeapp_quote', true);
1075 }
1076
1077 if (!$quote) {
1078 $cartRef = $order->get_cart_hash();
1079 if (!$cartRef) {
1080 //create uniq ID for backend orders
1081 $cartRef = uniqid();
1082 }
1083 $main_key = $this->routeapp_api_client->get_cache_api_session_key().'-'.$cartRef;
1084 $key = $main_key.'-latest';
1085 if (WC()->session) {
1086 $cached = WC()->session->get($key);
1087 if (isset($cached) && is_array($cached) && isset($cached['body'])) {
1088
1089 $fees = $order->get_items('fee');
1090 if (!empty($fees)) {
1091 foreach ($fees as $fee) {
1092 if (self::routeapp_is_route_protection_fee($fee)) {
1093 $insurance_amount = $fee->get_amount();
1094 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_route_charge', $insurance_amount );
1095 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_route_protection', true );
1096 }
1097 }
1098 } else {
1099 try {
1100 // Check if WC()->cart exists and is accessible
1101 if (class_exists('WC_Cart') && isset(WC()->cart) && WC()->cart instanceof WC_Cart) {
1102 // Fallback to WC()->cart->get_fees() if no fees are present in the order
1103 $cart_fees = WC()->cart->get_fees();
1104 if (!empty($cart_fees)) {
1105 foreach ($cart_fees as $cart_fee) {
1106 if (self::routeapp_is_route_protection_fee_name($cart_fee->name)) {
1107 $insurance_amount = $cart_fee->amount; // Correct method for cart fee amount
1108 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_route_charge', $insurance_amount );
1109 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_route_protection', true );
1110 }
1111 }
1112 }
1113 }
1114 } catch (Exception $e) {
1115 // Log the exception and ensure the application flow continues
1116 error_log("An error occurred while processing cart fees: " . $e->getMessage());
1117 }
1118
1119 }
1120 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_quote', $cached['body'] );
1121
1122 //clear cache!
1123 WC()->session->__unset($main_key);
1124 WC()->session->__unset($key);
1125 }
1126 }
1127 }
1128
1129 //Add orders status meta data
1130 $acceptedOrderStatuses = get_option('routeapp_included_order_statuses') ? get_option('routeapp_included_order_statuses') : [];
1131 $acceptedCanceledStatuses = get_option('routeapp_cancel_order_statuses') ? get_option('routeapp_cancel_order_statuses') : [];
1132
1133 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_included_order_statuses', $acceptedOrderStatuses );
1134 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_cancel_order_statuses', $acceptedCanceledStatuses );
1135
1136 //MSS-6533: Add ship to store flag and excluded shipping methods meta data to the order
1137 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos,'_routeapp_order_ship_to_store', $this->routeapp_resolve_ship_to_store_flag( $order ) );
1138 $updated |= $this->updateRouteOrderMetaData( $order, $is_hpos, '_routeapp_excluded_shipping_methods', $this->routeapp_get_excluded_shipping_methods());
1139
1140 // Save order if there is any update
1141 if ($updated && $is_hpos) {
1142 $order->save();
1143 }
1144 }
1145
1146 /**
1147 * Mark order on woocommerce thank you hook
1148 * @since 1.0.0
1149 * @param integer $order_id
1150 *
1151 */
1152 public function routeapp_mark_order($order_id)
1153 {
1154 if (!$order_id) {
1155 return;
1156 }
1157
1158 $order = wc_get_order($order_id);
1159
1160 //order cancel action will be handled by webhook event
1161 if ($order->get_status() === 'cancelled') {
1162 return;
1163 }
1164
1165 // Init the setup for readiness report
1166 $this->_setup_readiness_report_check();
1167
1168 if (WC()->session) {
1169 WC()->session->__unset('checkbox_checked');
1170 }
1171 }
1172
1173 /**
1174 * Check order note (look for shipment tracking data on it)
1175 * @param $noteId id
1176 * @return mixed
1177 */
1178 public function check_order_note_by_note_id($noteId)
1179 {
1180 $comment = get_comment($noteId);
1181 $order = wc_get_order($comment->comment_post_ID);
1182 if ($order && $order->get_id()) {
1183 $this->routeapp_shipment_tracking->parse_order_notes($order->get_id(), $this);
1184 }
1185 }
1186
1187 /**
1188 * Check order note (look for shipment tracking data on it)
1189 * @param $response WP_REST_Response
1190 * @return mixed
1191 */
1192 public function check_order_note($response)
1193 {
1194 $note = $response->get_data();
1195 $comment = get_comment($note['id']);
1196 $order = wc_get_order($comment->comment_post_ID);
1197 if ($order && $order->get_id()) {
1198 $this->routeapp_shipment_tracking->parse_order_notes($order->get_id(), $this);
1199 }
1200 return $response;
1201 }
1202 /**
1203 * Cancel the tracking order sending for API
1204 * @since 1.0.0
1205 * @param integer $order_id
1206 * @param integer $tracking_number
1207 * @param array $product_ids
1208 */
1209
1210 public function routeapp_cancel_tracking_order( $order_id, $tracking_number, $product_ids ) {
1211 if ( is_null($order_id) ) {
1212 return false;
1213 }
1214 $this->routeapp_shipment_tracking->cancel($order_id, $tracking_number, $product_ids, $this);
1215 }
1216
1217 /**
1218 * Create user at Route
1219 * @param $data
1220 * @return array|bool|mixed|object
1221 */
1222 public function routeapp_create_user($data){
1223 $response = $this->routeapp_api_client->create_user($data);
1224
1225 $this->routeapp_last_request = $response;
1226
1227 if ( is_wp_error( $response ) ) {
1228 return false;
1229 } else {
1230 $body = json_decode($response['body']);
1231
1232 if ($response['response']['code'] != self::HTTP_CREATED_CODE) {
1233 return false;
1234 }
1235
1236 return $body;
1237 }
1238 }
1239
1240 /**
1241 * Create user at Route
1242 *
1243 * @param $username
1244 * @param $password
1245 *
1246 * @return array|bool|mixed|object
1247 */
1248 public function routeapp_user_login($username, $password){
1249 $response = $this->routeapp_api_client->login_user($username, $password);
1250
1251 $this->routeapp_last_request = $response;
1252
1253 if ( is_wp_error( $response ) ) {
1254 return false;
1255 } else {
1256 $body = json_decode($response['body']);
1257
1258 if ($response['response']['code'] != self::HTTP_SUCCESS_CODE) {
1259 return false;
1260 }
1261
1262 return $body;
1263 }
1264 }
1265
1266 /**
1267 * Creates merchant at Route
1268 *
1269 * @param $data
1270 * @return array|bool|mixed|object
1271 */
1272 public function routeapp_create_merchant($data){
1273
1274 $response = $this->routeapp_api_client->create_merchant($data);
1275 $this->routeapp_last_request = $response;
1276
1277 if ( is_wp_error( $response ) ) {
1278 return false;
1279 } else {
1280 $body = json_decode($response['body']);
1281
1282 if ($response['response']['code'] != self::HTTP_CREATED_CODE && $response['response']['code'] != self::HTTP_SUCCESS_CODE) {
1283 return false;
1284 }
1285
1286 return $body;
1287 }
1288 }
1289
1290 /**
1291 * Creates merchant at Route
1292 *
1293 * @param $data
1294 * @return array|bool|mixed|object
1295 */
1296 public function routeapp_get_merchants(){
1297
1298 $response = $this->routeapp_api_client->get_merchants();
1299 $this->routeapp_last_request = $response;
1300
1301 if ( is_wp_error( $response ) ) {
1302 return false;
1303 } else {
1304 $body = json_decode($response['body']);
1305
1306 if ($response['response']['code'] != self::HTTP_CREATED_CODE && $response['response']['code'] != self::HTTP_SUCCESS_CODE) {
1307 return false;
1308 }
1309
1310 return $body;
1311 }
1312 }
1313
1314 /**
1315 * Activate user account
1316 *
1317 * @param $email
1318 *
1319 * @return array|bool|mixed|object
1320 */
1321 public function routeapp_activate_account($email){
1322
1323 $response = $this->routeapp_api_client->activate_account(['email' => $email]);
1324 $this->routeapp_last_request = $response;
1325
1326 if ( is_wp_error( $response ) ) {
1327 return false;
1328 } else {
1329 $body = json_decode($response['body']);
1330
1331 if ($response['response']['code'] != self::HTTP_CREATED_CODE && $response['response']['code'] != self::HTTP_SUCCESS_CODE) {
1332 return false;
1333 }
1334
1335 return $body;
1336 }
1337 }
1338
1339 public function routeapp_is_shipping_method_allowed($shipping_method=false, $order=false) {
1340
1341 if ($shipping_method || $order) {
1342 if ($shipping_method) {
1343 $shipping_method = explode(':', $shipping_method);
1344 $shipping_method = $shipping_method[0];
1345 }
1346 if ($order) {
1347 $shipping_method = @array_shift($order->get_shipping_methods());
1348 if(isset($shipping_method['method_id']))
1349 {
1350 $shipping_method = $shipping_method['method_id'];
1351 } else {
1352 return false;
1353 }
1354 }
1355 if ($this->routeapp_is_shipping_method_excluded($shipping_method)) {
1356 return false;
1357 }
1358 } else {
1359 if (WC()->session->get('shipping_for_package_0')) {
1360 foreach( WC()->session->get('shipping_for_package_0')['rates'] as $method_id => $rate ){
1361 if( WC()->session->get('chosen_shipping_methods')[0] == $method_id ){
1362 $shippingMethod = explode(':', $rate->id);
1363 $shippingMethod = $shippingMethod[0];
1364 if ($this->routeapp_is_shipping_method_excluded($shippingMethod)) {
1365 return false;
1366 }
1367 }
1368 }
1369 }
1370 }
1371 return true;
1372 }
1373
1374 /**
1375 * Whether a shipping method is configured as in-store pickup (excluded from protection).
1376 * WooCommerce Blocks local pickup uses pickup_location; classic checkout uses local_pickup.
1377 *
1378 * @param string $shipping_method Method id, optionally with instance suffix (e.g. pickup_location:0).
1379 * @return bool
1380 */
1381 public function routeapp_is_shipping_method_excluded($shipping_method) {
1382 $shipping_method = explode(':', (string) $shipping_method)[0];
1383 $excluded = $this->routeapp_get_excluded_shipping_methods();
1384 if (!is_array($excluded) || empty($excluded) || $shipping_method === '') {
1385 return false;
1386 }
1387 if (in_array($shipping_method, $excluded, true)) {
1388 return true;
1389 }
1390 if (in_array($shipping_method, self::PICKUP_SHIPPING_METHOD_IDS, true)) {
1391 return (bool) array_intersect(self::PICKUP_SHIPPING_METHOD_IDS, $excluded);
1392 }
1393 return false;
1394 }
1395
1396 public function routeapp_get_insurance_label() {
1397 return __(self::ROUTE_LABEL, 'routeapp');
1398 }
1399
1400 public function routeapp_is_opt_in() {
1401 $merchant = $this->routeapp_api_client->get_merchant();
1402 return isset($merchant->merchant_preferences->opt_in) && $merchant->merchant_preferences->opt_in == true;
1403 }
1404
1405 public function routeapp_get_public_token() {
1406 return get_option('routeapp_public_token');
1407 }
1408
1409 public static function routeapp_get_taxable_class() {
1410 return get_option('routeapp_taxable_class');
1411 }
1412
1413 public static function routeapp_get_checkout_hook() {
1414 return get_option('routeapp_checkout_hook') ? get_option('routeapp_checkout_hook') : self::DEFAULT_CHECKOUT_WEBHOOK;
1415 }
1416
1417 public static function routeapp_get_checkout_hook_options() {
1418 return [
1419 'woocommerce_before_checkout_form' => 'woocommerce_before_checkout_form',
1420 'woocommerce_checkout_before_customer_details' => 'woocommerce_checkout_before_customer_details',
1421 'woocommerce_checkout_billing' => 'woocommerce_checkout_billing',
1422 'woocommerce_before_checkout_billing_form' => 'woocommerce_before_checkout_billing_form',
1423 'woocommerce_after_checkout_billing_form' => 'woocommerce_after_checkout_billing_form',
1424 'woocommerce_before_checkout_registration_form' => 'woocommerce_before_checkout_registration_form',
1425 'woocommerce_after_checkout_registration_form' => 'woocommerce_after_checkout_registration_form',
1426 'woocommerce_checkout_shipping' => 'woocommerce_checkout_shipping',
1427 'woocommerce_before_checkout_shipping_form' => 'woocommerce_before_checkout_shipping_form',
1428 'woocommerce_after_checkout_shipping_form' => 'woocommerce_after_checkout_shipping_form',
1429 'woocommerce_before_order_notes' => 'woocommerce_before_order_notes',
1430 'woocommerce_after_order_notes' => 'woocommerce_after_order_notes',
1431 'woocommerce_checkout_after_customer_details' => 'woocommerce_checkout_after_customer_details',
1432 'woocommerce_checkout_before_order_review_heading' => 'woocommerce_checkout_before_order_review_heading',
1433 'woocommerce_checkout_order_review' => 'woocommerce_checkout_order_review',
1434 'woocommerce_checkout_before_order_review' => 'woocommerce_checkout_before_order_review',
1435 'woocommerce_review_order_before_cart_contents' => 'woocommerce_review_order_before_cart_contents',
1436 'woocommerce_review_order_after_cart_contents' => 'woocommerce_review_order_after_cart_contents',
1437 'woocommerce_review_order_before_shipping' => 'woocommerce_review_order_before_shipping',
1438 'woocommerce_review_order_after_shipping' => 'woocommerce_review_order_after_shipping',
1439 'woocommerce_review_order_before_order_total' => 'woocommerce_review_order_before_order_total',
1440 'woocommerce_review_order_after_order_total' => 'woocommerce_review_order_after_order_total',
1441 'woocommerce_review_order_before_payment' => 'woocommerce_review_order_before_payment',
1442 'woocommerce_review_order_before_submit' => 'woocommerce_review_order_before_submit',
1443 'woocommerce_review_order_after_submit' => 'woocommerce_review_order_after_submit',
1444 'woocommerce_review_order_after_payment' => 'woocommerce_review_order_after_payment',
1445 'woocommerce_checkout_after_order_review' => 'woocommerce_checkout_after_order_review',
1446 'woocommerce_after_checkout_form' => 'woocommerce_after_checkout_form'
1447 ];
1448 }
1449
1450 public static function routeapp_get_fee_taxable() {
1451 if (get_option('routeapp_route_fee_taxable') == 'yes') return true;
1452 return false;
1453 }
1454
1455 public function routeapp_get_excluded_shipping_methods() {
1456 return get_option('routeapp_excluded_shipping_methods') ?
1457 get_option('routeapp_excluded_shipping_methods') :
1458 [];
1459 }
1460
1461 public function routeapp_get_secret_token() {
1462 return get_option('routeapp_secret_token');
1463 }
1464
1465 public function routeapp_route_enabled_extra_columns() {
1466 return get_option('routeapp_route_enable_extra_columns') !== "no";
1467 }
1468
1469 public function routeapp_get_fresh_installed(){
1470 return get_option('routeapp_route_fresh_installed');
1471 }
1472
1473 public function routeapp_route_show_order_updates() {
1474 return get_option('routeapp_route_show_order_updates', 'yes') == 'yes';
1475 }
1476
1477 private static function routeapp_get_failed_registration(){
1478 return get_option('routeapp_failed_registration');
1479 }
1480
1481 /**
1482 * If it's installation has failed at user step creation step
1483 *
1484 * @return bool
1485 */
1486 public function has_user_creation_failed(){
1487 return self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_USER;
1488 }
1489
1490 /**
1491 * If it's installation has failed at user step creation step due to conflict
1492 *
1493 * @return bool
1494 */
1495 public function has_user_creation_conflicted(){
1496 return !Route_Setup::has_valid_merchant_tokens() && (self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_USER_DUPLICATED || self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_USER_LOGIN_FAILED);
1497 }
1498
1499 /**
1500 * If it's installation has failed at Merchant creation step
1501 *
1502 * @return bool
1503 */
1504 public function has_merchant_creation_failed(){
1505 return self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_MERCHANT && !Route_Setup::has_valid_merchant_tokens();
1506 }
1507 /**
1508 * If it's installation has failed at Merchant creation step
1509 *
1510 * @return bool
1511 */
1512 public function has_merchant_creation_conflicted(){
1513 return self::routeapp_get_failed_registration() == Route_Setup::FAILED_REGISTRATION_STEP_MERCHANT_DUPLICATED && !Route_Setup::has_valid_merchant_tokens();
1514 }
1515
1516 /**
1517 * If it's installation has failed at Merchant creation step
1518 *
1519 * @return bool
1520 */
1521 public function successful_registration(){
1522 return self::routeapp_get_failed_registration() === Route_Setup::REGISTRATION_FIXED || self::routeapp_get_failed_registration() === Route_Setup::REGISTRATION_STEP_USER_LOGIN_SUCCESS;
1523 }
1524
1525 /**
1526 * Get registration step at setup flow
1527 * @return mixed
1528 */
1529 private static function registration_step(){
1530 return get_option(Route_Setup::REGISTRATION_STEP);
1531 }
1532
1533 /**
1534 * Check if at the merchant create flow it was already redirected
1535 * @return bool
1536 */
1537 public static function was_redirected(){
1538 return self::registration_step() == Route_Setup::REDIRECTED;
1539 }
1540
1541 /**
1542 * Set it as redirected to Route after registration is complete
1543 * @return void
1544 */
1545 public function set_as_redirected(){
1546 update_option(Route_Setup::REGISTRATION_STEP, Route_Setup::REDIRECTED);
1547 }
1548
1549 public function routeapp_register_status_route() {
1550 register_rest_route( 'route', 'status', array(
1551 'methods' => 'GET',
1552 'callback' => function() {
1553 return rest_ensure_response( $this->routeapp_build_status_response() );
1554 },
1555 'permission_callback' => '__return_true'
1556 )
1557 );
1558 }
1559
1560 /**
1561 * Build the associative array returned by GET /wp-json/route/status.
1562 *
1563 * Kept separate from the REST callback so tests can exercise the response
1564 * shape directly without REST dispatch. Never returns raw credentials —
1565 * `routeapp_user_token` and `routeapp_user_id` are intentionally excluded;
1566 * `public_token`, `secret_key`, and `routeapp_store_hash` are masked via
1567 * {@see Routeapp_Public::_encodeToken()}. `routeapp_merchant_id` is
1568 * exposed raw — it is the merchant identifier Route support matches
1569 * against, and already appears in outbound webhook URLs.
1570 *
1571 * @return array Flat associative array keyed by setting name.
1572 */
1573 private function routeapp_build_status_response() {
1574 $store_hash = Route_Setup::get_store_hash();
1575 $widget_install_ts = get_option( Route_Setup::SETUP_CHECK_WIDGET_INSTALLATION_DATE, null );
1576 $widget_install_human = null;
1577 if ( ! empty( $widget_install_ts ) && is_numeric( $widget_install_ts ) ) {
1578 $widget_install_ts = (int) $widget_install_ts;
1579 $widget_install_human = wp_date( 'Y-m-d H:i:s', $widget_install_ts );
1580 } else {
1581 $widget_install_ts = null;
1582 }
1583
1584 return array(
1585 'version' => ROUTEAPP_VERSION,
1586 'public_token' => !empty($this->routeapp_get_public_token()) ?
1587 $this->_encodeToken($this->routeapp_get_public_token()) :
1588 false,
1589 'secret_key' => !empty($this->routeapp_get_secret_token()) ?
1590 $this->_encodeToken($this->routeapp_get_secret_token()) :
1591 false,
1592 'route_insurance_label' => $this->routeapp_get_insurance_label(),
1593 'extra_columns_enabled' => $this->routeapp_route_enabled_extra_columns(),
1594 'failed_registration' => $this->routeapp_get_failed_registration(),
1595 'base_currency' => get_woocommerce_currency(),
1596 'date' => array(
1597 'date' => date('Y-m-d H:i:s'),
1598 'timezone_type' => date('P'),
1599 'timezone' => date_default_timezone_get()
1600 ),
1601 'routeapp_route_fee_taxable' => $this->routeapp_get_fee_taxable(),
1602 'routeapp_taxable_class' => $this->routeapp_get_taxable_class(),
1603 'routeapp_excluded_shipping_methods' => $this->routeapp_get_excluded_shipping_methods(),
1604 'routeapp_route_show_order_updates' => $this->routeapp_route_show_order_updates(),
1605 'routeapp_checkout_hook' => $this->routeapp_get_checkout_hook(),
1606 'routeapp_have_webhook_secret' => $this->_routeapp_have_webhook_secret(),
1607 'routeapp_included_order_statuses' => get_option( 'routeapp_included_order_statuses', array() ),
1608 'routeapp_cancel_order_statuses' => get_option( 'routeapp_cancel_order_statuses', array() ),
1609 'routeapp_external_tracking_providers' => get_option( 'routeapp_external_tracking_providers', '' ),
1610 'routeapp_order_recover_from' => get_option( 'routeapp_order_recover_from', '' ),
1611 'routeapp_order_recover_to' => get_option( 'routeapp_order_recover_to', '' ),
1612 'routeapp_order_recover_reconcile_backend' => get_option( 'routeapp_order_recover_reconcile_backend', 'no' ),
1613 'routeapp_merchant_id' => get_option( Routeapp_API_Client::ROUTEAPP_MERCHANT_ID, '' ),
1614 'routeapp_store_hash' => ! empty( $store_hash ) ? $this->_encodeToken( $store_hash ) : false,
1615 'routeapp_last_install_date' => get_option( '_routeapp_last_install_date', '' ),
1616 'routeapp_webhooks_created' => (bool) get_option( '_routeapp_webhooks_created', false ),
1617 'routeapp_route_registration_step' => get_option( Route_Setup::REGISTRATION_STEP, '' ),
1618 'routeapp_user_conflict_auto_redirect_done' => (bool) get_option( Route_Setup::USER_CONFLICT_AUTO_REDIRECT_DONE_OPTION, false ),
1619 'routeapp_route_fresh_installed' => $this->routeapp_get_fresh_installed(),
1620 'route_setup_check_widget_installation_date' => $widget_install_ts,
1621 'route_setup_check_widget_installation_date_human' => $widget_install_human,
1622 'route_setup_check_widget_php' => (bool) get_option( Route_Setup::SETUP_CHECK_WIDGET_PHP, false ),
1623 'route_setup_check_widget_js' => (bool) get_option( Route_Setup::SETUP_CHECK_WIDGET_JS, false ),
1624 'route_setup_check_widget_api_call' => (bool) get_option( Route_Setup::SETUP_CHECK_WIDGET_API_CALL, false ),
1625 );
1626 }
1627
1628 private function _routeapp_have_webhook_secret() {
1629 return get_option('_routeapp_webhooks_secret') ? 'Yes' : 'No';
1630 }
1631
1632 private function _encodeToken($token) {
1633 return substr($token, 0, 5) . '...' . substr($token, -5);
1634 }
1635
1636 public function routeapp_register_checkout_route(){
1637
1638 if (!isset(WC()->session) || !isset($_POST) || !isset($_POST['shipping_method'])) {
1639 return rest_ensure_response( [] );
1640 }
1641
1642 $shipping_method = $_POST['shipping_method'];
1643 if ($shipping_method === 'false' || $shipping_method === '') {
1644 $shipping_method = false;
1645 }
1646
1647 //check first if shipping method is allowed
1648 if ($shipping_method) {
1649 $show_widget = $this->routeapp_is_shipping_method_allowed($shipping_method);
1650 if (!$show_widget) {
1651 wp_send_json(['hide-widget' => true]);
1652 return;
1653 }
1654 }
1655
1656 $cart = WC()->cart;
1657 $response= [
1658 'routeapp-subtotal' => $cart ? round($this->get_cart_subtotal_with_only_shippable_items($cart), 2) : 0,
1659 ];
1660
1661 wp_send_json( $response );
1662 }
1663
1664 /**
1665 * Add route to recreate User and Merchant Accounts
1666 * if it has failed at the first try
1667 */
1668 public function routeapp_register_setup_route () {
1669 register_rest_route( 'route', 'recreate_user', array(
1670 'methods' => 'GET',
1671 'callback' => [Route_Setup::class, 'retry_user'],
1672 'permission_callback' => '__return_true'
1673 ) );
1674 register_rest_route( 'route', 'recreate_merchant', array(
1675 'methods' => 'GET',
1676 'callback' => [Route_Setup::class, 'retry_merchant'],
1677 'permission_callback' => '__return_true'
1678 ) );
1679 register_rest_route( 'route', 'user_login', array(
1680 'methods' => 'POST',
1681 'callback' => [Route_Setup::class, 'user_login'],
1682 'permission_callback' => '__return_true'
1683 ) );
1684 }
1685
1686 public function routeapp_last_request_has_failed(){
1687 if ( is_wp_error( $this->routeapp_last_request ) || !isset($this->routeapp_last_request)) {
1688 return true;
1689 } else {
1690 if ($this->routeapp_last_request['response']['code'] != self::HTTP_CREATED_CODE && $this->routeapp_last_request['response']['code'] != self::HTTP_SUCCESS_CODE) {
1691 return true;
1692 }
1693
1694 return false;
1695 }
1696 }
1697
1698 public function routeapp_last_request_has_success() {
1699 if ( is_wp_error( $this->routeapp_last_request ) || !isset($this->routeapp_last_request)) {
1700 return false;
1701 }
1702 return $this->routeapp_last_request['response']['code'] === self::HTTP_SUCCESS_CODE;
1703 }
1704
1705 public function routeapp_last_request_has_conflicted(){
1706 if ( is_wp_error( $this->routeapp_last_request ) || !isset($this->routeapp_last_request)) {
1707 return false;
1708 } else {
1709 if ($this->routeapp_last_request['response']['code'] == self::HTTP_CONFLICT_CODE) {
1710 return true;
1711 }
1712
1713 return false;
1714 }
1715 }
1716
1717 public function routeapp_log($event, $extraData = false) {
1718 $this->routeapp_logger->sentry_log($event, $extraData);
1719 }
1720
1721 private function _setup_readiness_report_check()
1722 {
1723 if ($this->route_app_setup_helper->can_send_compatibility_report()) {
1724 /**
1725 * API CALL for incompatibility test
1726 */
1727 $api = new Routeapp_API_Compatibility();
1728 $api->send($this->route_app_setup_helper->prepare_compatibility_data());
1729 }
1730 }
1731
1732 public function get_order_subtotal($order)
1733 {
1734 $subtotal = 0;
1735
1736 if (is_a($order, 'WC_Order')) {
1737 $order_items = $order->get_items();
1738
1739 foreach ($order_items as $order_item_id => $order_item) {
1740 if ($this->isProductVirtual($order_item)) {
1741 //if the product is not found or virtual, skip it
1742 continue;
1743 }
1744 $subtotal += $order_item->get_subtotal();
1745 }
1746 }
1747 return $subtotal;
1748 }
1749
1750 /**
1751 * Normalize cart item (array) or order item (WC_Order_Item_Product) to a common array format.
1752 * Ensures compatibility with WooCommerce Product Bundles when used in order context.
1753 *
1754 * @param array|WC_Order_Item_Product $item Cart item array or order item object.
1755 * @return array Normalized item with keys: data, line_subtotal, quantity, product_id, variation_id, key, bundled_items, bundled_by.
1756 */
1757 private function normalize_item_for_bundle_calc($item) {
1758 if (is_object($item) && method_exists($item, 'get_product')) {
1759 /** @var WC_Order_Item_Product $item */
1760 $product = $item->get_product();
1761 $bundled_items = $item->get_meta('_bundled_items', true);
1762 $bundled_by = $item->get_meta('_bundled_by', true);
1763 // Use _bundle_cart_key so parent matches child's _bundled_by (Product Bundles uses same hash).
1764 $key = $item->get_meta('_bundle_cart_key', true) ?: $item->get_id();
1765 return [
1766 'data' => $product,
1767 'line_subtotal' => (float) $item->get_subtotal(),
1768 'quantity' => (int) $item->get_quantity(),
1769 'product_id' => $item->get_product_id(),
1770 'variation_id' => $item->get_variation_id(),
1771 'key' => $key,
1772 'bundled_items' => !empty($bundled_items) ? $bundled_items : null,
1773 'bundled_by' => !empty($bundled_by) ? $bundled_by : null,
1774 ];
1775 }
1776 return $item;
1777 }
1778
1779 /**
1780 * Check if product inside order item is virtual, return true if product is not valid
1781 * @param array|WC_Order_Item_Product $item Cart item array or order item object.
1782 * @return bool
1783 */
1784 public function isProductVirtual($item) {
1785 if (is_object($item) && method_exists($item, 'get_product_id')) {
1786 $productId = $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id();
1787 } else {
1788 $productId = isset($item['variation_id']) && !empty($item['variation_id']) ?
1789 $item['variation_id'] :
1790 $item['product_id'];
1791 }
1792
1793 $product = wc_get_product($productId);
1794 if ($product) {
1795 return $product->is_virtual();
1796 }
1797 return true;
1798 }
1799
1800 /**
1801 * Process cart item
1802 *
1803 * @param $cart_item
1804 * @param $allowed_bundle_parents
1805 * @return array
1806 */
1807 public function processCartItem($cart_item, $allowed_bundle_parents) {
1808
1809 $cart_item = $this->normalize_item_for_bundle_calc($cart_item);
1810
1811 $cart_item_subtotal = $cart_item['line_subtotal'];
1812
1813 //support for WooCommerce Product Bundles (only when product data is available; order items may have null)
1814 if (isset($cart_item['bundled_items'])) {
1815 if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
1816 $price = $cart_item['data']->get_data()['price'];
1817 if ($price > 0) {
1818 //grab item price from parent bundle if exist, return directly
1819 $cart_item_subtotal = $price * $cart_item['quantity'];
1820 $data = [
1821 'id' => strval($cart_item['product_id']),
1822 'quantity' => intval($cart_item['quantity']),
1823 'unit_price' => strval(round($cart_item_subtotal / $cart_item['quantity'], 2))
1824 ];
1825 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'data' => $data];
1826 }
1827 }
1828 //add bundle parent key to allowed array
1829 $allowed_bundle_parents[] = $cart_item['key'];
1830 }
1831
1832 if ($this->isProductVirtual($cart_item)) {
1833 //if the product is not found or virtual, skip it
1834 return ['allowed_bundle_parents' => $allowed_bundle_parents];
1835 }
1836
1837 if (isset($cart_item['bundled_by'])) {
1838 if (!in_array($cart_item['bundled_by'], $allowed_bundle_parents)) {
1839 //skip bundle item children (price is on parent)
1840 return ['allowed_bundle_parents' => $allowed_bundle_parents];
1841 }
1842 if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
1843 //grab subtotal from bundle child
1844 $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity'];
1845 }
1846 }
1847
1848 return [
1849 'allowed_bundle_parents' => $allowed_bundle_parents,
1850 'data' => [
1851 'id' => strval($cart_item['product_id']),
1852 'quantity' => intval($cart_item['quantity']),
1853 'unit_price' => strval(round($cart_item_subtotal / $cart_item['quantity'], 2))
1854 ]
1855 ];
1856 }
1857
1858 /**
1859 * Get cart item subtotal
1860 *
1861 * @param $cart_item
1862 * @param $allowed_bundle_parents
1863 * @return array
1864 */
1865 public function getCartItemSubtotal($cart_item, $allowed_bundle_parents) {
1866
1867 $cart_item = $this->normalize_item_for_bundle_calc($cart_item);
1868
1869 $cart_item_subtotal = $cart_item['line_subtotal'];
1870
1871 //support for WooCommerce Product Bundles (only when product data is available; order items may have null)
1872 if (isset($cart_item['bundled_items'])) {
1873 if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
1874 $price = $cart_item['data']->get_data()['price'];
1875 if ($price > 0) {
1876 //grab item price from parent bundle if exist, return directly
1877 $cart_item_subtotal = $price * $cart_item['quantity'];
1878 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'subtotal' => $cart_item_subtotal];
1879 }
1880 }
1881 //add bundle parent key to allowed array
1882 $allowed_bundle_parents[] = $cart_item['key'];
1883 }
1884
1885 if ($this->isProductVirtual($cart_item)) {
1886 //if the product is not found or virtual, skip it
1887 return ['allowed_bundle_parents' => $allowed_bundle_parents];
1888 }
1889
1890 if (isset($cart_item['bundled_by'])) {
1891 if (!in_array($cart_item['bundled_by'], $allowed_bundle_parents)) {
1892 //skip bundle item children (price is on parent)
1893 return ['allowed_bundle_parents' => $allowed_bundle_parents];
1894 }
1895 if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
1896 //grab subtotal from bundle child
1897 $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity'];
1898 }
1899 }
1900 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'subtotal' => $cart_item_subtotal];
1901 }
1902
1903 /**
1904 * get cart shippable items to grab quote on RouteApp
1905 *
1906 * @param $cart
1907 * @param string $object
1908 * @return array
1909 */
1910 public function get_cart_shippable_items($cart, $object='cart')
1911 {
1912 $cartItems = [];
1913 if (!$cart) {
1914 return $cartItems;
1915 }
1916 $allowed_bundle_parents = [];
1917 if ($object=='cart') {
1918 foreach ( $cart->get_cart() as $cart_item ) {
1919 $cartItemData = $this->processCartItem($cart_item, $allowed_bundle_parents);
1920 $allowed_bundle_parents = $cartItemData['allowed_bundle_parents'];
1921 if (isset($cartItemData['data'])) {
1922 $cartItems[] = $cartItemData['data'];
1923 }
1924 }
1925 } else {
1926 foreach ( $cart->get_items() as $cart_item ) {
1927 $cartItemData = $this->processCartItem($cart_item, $allowed_bundle_parents);
1928 $allowed_bundle_parents = $cartItemData['allowed_bundle_parents'];
1929 if (isset($cartItemData['data'])) {
1930 $cartItems[] = $cartItemData['data'];
1931 }
1932 }
1933 }
1934
1935 return $cartItems;
1936 }
1937
1938 /**
1939 * get subtotal for shippable items only
1940 *
1941 * @param $cart
1942 * @param string $object
1943 * @return float|int|mixed
1944 */
1945 public function get_cart_subtotal_with_only_shippable_items($cart, $object='cart')
1946 {
1947 $subtotal = 0;
1948 if (!$cart) {
1949 return $subtotal;
1950 }
1951 $allowed_bundle_parents = [];
1952
1953 if ($object=='cart') {
1954 foreach ( $cart->get_cart() as $cart_item ) {
1955 $itemSubtotal = $this->getCartItemSubtotal($cart_item, $allowed_bundle_parents);
1956 $allowed_bundle_parents = $itemSubtotal['allowed_bundle_parents'];
1957 if (isset($itemSubtotal['subtotal'])) {
1958 $subtotal += $itemSubtotal['subtotal'];
1959 }
1960 }
1961 } else {
1962 foreach ( $cart->get_items() as $cart_item ) {
1963 $itemSubtotal = $this->getCartItemSubtotal($cart_item, $allowed_bundle_parents);
1964 $allowed_bundle_parents = $itemSubtotal['allowed_bundle_parents'];
1965 if (isset($itemSubtotal['subtotal'])) {
1966 $subtotal += $itemSubtotal['subtotal'];
1967 }
1968 }
1969 }
1970 return $subtotal;
1971 }
1972
1973 /**
1974 * Edit Avatax fee line (send tax class name instead item_id)
1975 */
1976 public function routeapp_edit_avatax_fee($fee_line, $fee = null)
1977 {
1978 if (isset($fee_line['description']) && $fee_line['description'] == $this->routeapp_get_insurance_label()) {
1979 $fee_line['itemCode'] = $this->routeapp_get_taxable_class();
1980 }
1981
1982 return $fee_line;
1983 }
1984
1985 /**
1986 * Mark a fee order item as Route Shipping Protection.
1987 *
1988 * Writes fee item meta:
1989 * - _routeapp_protection_fee = yes
1990 * - _routeapp_protection_sku = ROUTEINS
1991 *
1992 * Purpose: leave a stable identity on the WC_Order_Item_Fee that does not
1993 * depend on the (possibly translated) fee name. Called when the fee is
1994 * created at checkout and from the admin order fee path.
1995 *
1996 * @param mixed $item Fee order item.
1997 */
1998 public static function routeapp_mark_protection_fee_item( $item ) {
1999 if ( ! is_object( $item ) || ! is_callable( array( $item, 'update_meta_data' ) ) ) {
2000 return;
2001 }
2002
2003 $item->update_meta_data( self::ROUTE_FEE_ITEM_META, 'yes' );
2004 $item->update_meta_data( self::ROUTE_FEE_SKU_META, self::ROUTE_PROTECTION_SKU );
2005 }
2006
2007 /**
2008 * Whether a fee name refers to Route Shipping Protection (EN or translated).
2009 *
2010 * Name matching is a legacy fallback only. New fees are identified via
2011 * ROUTE_FEE_ITEM_META / ROUTE_FEE_SKU_META. Besides the English constant and
2012 * the current-locale translation, we also resolve the store locale label when
2013 * it differs from the request locale (e.g. admin export vs checkout).
2014 *
2015 * Hot path: prefer strict string matches before html_entity_decode().
2016 *
2017 * @param string $name Fee name.
2018 * @return bool
2019 */
2020 public static function routeapp_is_route_protection_fee_name( $name ) {
2021 $name = trim( (string) $name );
2022 if ( '' === $name ) {
2023 return false;
2024 }
2025
2026 // Common case: plain English constant / translated label, no entities.
2027 if ( $name === self::ROUTE_LABEL ) {
2028 return true;
2029 }
2030
2031 $labels = self::routeapp_get_protection_fee_label_candidates();
2032 foreach ( $labels as $label ) {
2033 if ( $name === $label ) {
2034 return true;
2035 }
2036 }
2037
2038 // Only decode when needed (e.g. "Route Shipping Protection" stored as entities).
2039 if ( false === strpos( $name, '&' ) ) {
2040 return false;
2041 }
2042
2043 $decoded = trim( html_entity_decode( $name, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) );
2044 if ( '' === $decoded || $decoded === $name ) {
2045 return false;
2046 }
2047
2048 if ( $decoded === self::ROUTE_LABEL ) {
2049 return true;
2050 }
2051
2052 foreach ( $labels as $label ) {
2053 if ( $decoded === $label ) {
2054 return true;
2055 }
2056 }
2057
2058 return false;
2059 }
2060
2061 /**
2062 * Possible Route fee labels for the current request / store locales.
2063 *
2064 * Cached per request to avoid repeated locale switching in fee loops.
2065 *
2066 * @return string[]
2067 */
2068 private static function routeapp_get_protection_fee_label_candidates() {
2069 static $cached_labels = null;
2070 static $cached_locale_key = null;
2071
2072 $current_locale = function_exists( 'determine_locale' )
2073 ? determine_locale()
2074 : ( function_exists( 'get_locale' ) ? get_locale() : '' );
2075 $site_locale = function_exists( 'get_locale' ) ? get_locale() : '';
2076 $locale_key = $current_locale . '|' . $site_locale;
2077
2078 if ( null !== $cached_labels && $cached_locale_key === $locale_key ) {
2079 return $cached_labels;
2080 }
2081
2082 $labels = array();
2083
2084 if ( function_exists( '__' ) ) {
2085 $labels[] = __( self::ROUTE_LABEL, 'routeapp' );
2086
2087 // When the admin/user locale differs from the site locale, also check the
2088 // store translation used at checkout for older orders without fee meta.
2089 if ( function_exists( 'switch_to_locale' )
2090 && function_exists( 'restore_previous_locale' )
2091 && $site_locale
2092 && $site_locale !== $current_locale
2093 && switch_to_locale( $site_locale ) ) {
2094 $labels[] = __( self::ROUTE_LABEL, 'routeapp' );
2095 restore_previous_locale();
2096 }
2097 }
2098
2099 $cached_labels = array_values( array_unique( array_filter( $labels ) ) );
2100 $cached_locale_key = $locale_key;
2101
2102 return $cached_labels;
2103 }
2104
2105 /**
2106 * Whether an order item is the Route Shipping Protection fee.
2107 *
2108 * Prefers explicit fee item meta for all newly created fees; falls back to
2109 * fee name only for legacy pre-migration orders.
2110 *
2111 * @param mixed $item Order item.
2112 * @return bool
2113 */
2114 public static function routeapp_is_route_protection_fee( $item ) {
2115 if ( ! is_object( $item ) ) {
2116 return false;
2117 }
2118
2119 if ( is_callable( array( $item, 'get_type' ) ) && 'fee' !== $item->get_type() ) {
2120 return false;
2121 }
2122
2123 if ( is_callable( array( $item, 'get_meta' ) ) ) {
2124 $flag = $item->get_meta( self::ROUTE_FEE_ITEM_META, true );
2125 if ( 'yes' === $flag || true === $flag || '1' === $flag || 1 === $flag ) {
2126 return true;
2127 }
2128
2129 $sku = $item->get_meta( self::ROUTE_FEE_SKU_META, true );
2130 if ( self::ROUTE_PROTECTION_SKU === $sku ) {
2131 return true;
2132 }
2133 }
2134
2135 $name = is_callable( array( $item, 'get_name' ) ) ? $item->get_name() : '';
2136 if ( ! self::routeapp_is_route_protection_fee_name( $name ) ) {
2137 return false;
2138 }
2139
2140 // Legacy path: visible when an older fee lacks identity meta (locale gaps).
2141 static $logged_name_fallback = false;
2142 if ( ! $logged_name_fallback && function_exists( 'error_log' ) ) {
2143 $logged_name_fallback = true;
2144 error_log(
2145 'RouteApp: identified Route protection fee via name fallback (legacy order without fee meta). Name: ' . $name
2146 );
2147 }
2148
2149 return true;
2150 }
2151
2152 /**
2153 * Exclude Route fee from ShipStation item export (XML legacy + REST 4.9+).
2154 *
2155 * Used by woocommerce_shipstation_no_shipping_item on both export paths.
2156 *
2157 * @param bool $exclude Whether the item should be excluded.
2158 * @param mixed $product Product object or false for fees.
2159 * @param mixed $item Order item.
2160 * @return bool
2161 */
2162 public function routeapp_exclude_route_from_shipstation( $exclude, $product, $item ) {
2163 if ( self::routeapp_is_route_protection_fee( $item ) ) {
2164 return true;
2165 }
2166
2167 return $exclude;
2168 }
2169
2170 /**
2171 * Strip Route fee from ShipStation REST fulfillment item payload (4.8+/4.9+).
2172 *
2173 * @param array $fulfillment_item Fulfillment item data.
2174 * @param mixed $order Order object.
2175 * @param mixed $item Order item.
2176 * @param array $extra_args Extra args.
2177 * @return array
2178 */
2179 public function routeapp_strip_route_from_shipstation_fulfillment_item( $fulfillment_item, $order, $item, $extra_args = array() ) {
2180 if ( self::routeapp_is_route_protection_fee( $item ) ) {
2181 return array();
2182 }
2183
2184 return $fulfillment_item;
2185 }
2186
2187 /**
2188 * Legacy alias kept for backwards compatibility with anything calling the old method name.
2189 *
2190 * @param mixed $order_xml Order XML DOM element.
2191 * @return mixed
2192 */
2193 public function routeapp_add_sku_to_route_node( $order_xml ) {
2194 return $this->routeapp_strip_route_from_shipstation_xml( $order_xml );
2195 }
2196
2197 /**
2198 * Remove Route Shipping Protection from ShipStation XML order export (legacy path).
2199 *
2200 * @param mixed $order_xml Order XML DOM element.
2201 * @return mixed
2202 */
2203 public function routeapp_strip_route_from_shipstation_xml( $order_xml ) {
2204 if ( ! ( $order_xml instanceof DOMElement ) ) {
2205 return $order_xml;
2206 }
2207
2208 $items_nodes = $order_xml->getElementsByTagName( 'Items' );
2209 foreach ( $items_nodes as $item_node ) {
2210 $to_remove = array();
2211
2212 foreach ( $item_node->childNodes as $child ) {
2213 if ( ! ( $child instanceof DOMElement ) || 'Item' !== $child->nodeName ) {
2214 continue;
2215 }
2216
2217 if ( $this->routeapp_shipstation_xml_item_is_route( $child ) ) {
2218 $to_remove[] = $child;
2219 }
2220 }
2221
2222 foreach ( $to_remove as $node ) {
2223 $item_node->removeChild( $node );
2224 }
2225 }
2226
2227 return $order_xml;
2228 }
2229
2230 /**
2231 * Detect Route fee nodes inside a ShipStation XML <Item>.
2232 *
2233 * @param DOMElement $item Item element.
2234 * @return bool
2235 */
2236 private function routeapp_shipstation_xml_item_is_route( DOMElement $item ) {
2237 foreach ( $item->childNodes as $child ) {
2238 if ( ! ( $child instanceof DOMElement ) ) {
2239 continue;
2240 }
2241
2242 if ( 'Name' === $child->nodeName && self::routeapp_is_route_protection_fee_name( $child->nodeValue ) ) {
2243 return true;
2244 }
2245
2246 if ( 'SKU' === $child->nodeName && self::ROUTE_PROTECTION_SKU === trim( $child->nodeValue ) ) {
2247 return true;
2248 }
2249
2250 if ( 'Options' !== $child->nodeName ) {
2251 continue;
2252 }
2253
2254 foreach ( $child->getElementsByTagName( 'Option' ) as $option ) {
2255 $opt_name = '';
2256 $opt_value = '';
2257
2258 foreach ( $option->childNodes as $opt_child ) {
2259 if ( ! ( $opt_child instanceof DOMElement ) ) {
2260 continue;
2261 }
2262 if ( 'Name' === $opt_child->nodeName ) {
2263 $opt_name = trim( $opt_child->nodeValue );
2264 }
2265 if ( 'Value' === $opt_child->nodeName ) {
2266 $opt_value = trim( strip_tags( $opt_child->nodeValue ) );
2267 }
2268 }
2269
2270 if ( self::ROUTE_FEE_ITEM_META === $opt_name && in_array( $opt_value, array( 'yes', '1' ), true ) ) {
2271 return true;
2272 }
2273
2274 if ( in_array( $opt_name, array( self::ROUTE_FEE_SKU_META, 'SKU' ), true )
2275 && self::ROUTE_PROTECTION_SKU === $opt_value ) {
2276 return true;
2277 }
2278 }
2279 }
2280
2281 return false;
2282 }
2283
2284 /**
2285 * Update order meta only if changed (HPOS & CPT safe)
2286 *
2287 * @return bool True if meta changed
2288 */
2289 public function updateRouteOrderMetaData(
2290 WC_Order $order,
2291 bool $is_hpos,
2292 string $meta_key,
2293 $meta_value
2294 ): bool {
2295 try {
2296 if ( ! $order || empty( $meta_key ) ) {
2297 return false;
2298 }
2299
2300 $normalized = is_scalar( $meta_value )
2301 ? (string) $meta_value
2302 : wp_json_encode( $meta_value );
2303
2304 if ( $is_hpos ) {
2305 $current = $order->get_meta( $meta_key, true );
2306 $current = is_scalar( $current ) ? (string) $current : wp_json_encode( $current );
2307
2308
2309 if ( (string) $current === (string) $normalized ) {
2310 return false;
2311 }
2312
2313 $order->update_meta_data( $meta_key, $normalized );
2314 return true;
2315
2316 }
2317
2318 $order_id = $order->get_id();
2319 $current = get_post_meta( $order_id, $meta_key, true );
2320 $current = is_scalar( $current ) ? (string) $current : wp_json_encode( $current );
2321
2322
2323 if ( (string) $current === (string) $normalized ) {
2324 return false;
2325 }
2326
2327 update_post_meta( $order_id, $meta_key, $normalized );
2328 return true;
2329
2330 } catch ( Exception $e ) {
2331 error_log(
2332 'RouteApp: error updating order meta (' . $meta_key . '): ' . $e->getMessage()
2333 );
2334 return false;
2335 }
2336 }
2337
2338 //MSS-6533
2339 /**
2340 * Resolve the local pickup flag in a FAIL-SAFE way.
2341 *
2342 * Priority:
2343 * 1. Order shipping items
2344 * 3. Checkout session (fallback only)
2345 *
2346 * @param WC_Order|null $order
2347 *
2348 * @return int 1 = local pickup, 0 = not local pickup
2349 */
2350 public function routeapp_resolve_ship_to_store_flag( WC_Order $order = null): int {
2351 $excluded_methods = $this->routeapp_get_excluded_shipping_methods();
2352 if ( empty( $excluded_methods ) || ! is_array( $excluded_methods ) ) {
2353 return 0;
2354 }
2355
2356 // Order-based (most reliable).
2357 if ( $order instanceof WC_Order ) {
2358 $shipping_items = $order->get_shipping_methods();
2359 if ( ! empty( $shipping_items ) ) {
2360 foreach ( $shipping_items as $shipping_item ) {
2361 if ( ! $shipping_item instanceof WC_Order_Item_Shipping ) {
2362 continue;
2363 }
2364 $method_id = $shipping_item->get_method_id();
2365 if ( empty( $method_id ) ) {
2366 continue;
2367 }
2368 $method_id = explode( ':', $method_id )[0];
2369 if ( $this->routeapp_is_shipping_method_excluded( $method_id ) ) {
2370 return 1;
2371 }
2372 }
2373 return 0;
2374 }
2375 }
2376
2377 // Session fallback (checkout only).
2378 if ( ! function_exists( 'WC' ) || ! WC()->session ) {
2379 return 0;
2380 }
2381 $chosen = WC()->session->get( 'chosen_shipping_methods' );
2382 $packages = WC()->session->get( 'shipping_for_package_0' );
2383 if ( empty( $chosen[0] ) || empty( $packages['rates'] ) ) {
2384 return 0;
2385 }
2386 $chosen_id = $chosen[0];
2387 if ( ! isset( $packages['rates'][ $chosen_id ] ) ) {
2388 return 0;
2389 }
2390 $method_id = explode( ':', $packages['rates'][ $chosen_id ]->id )[0];
2391 if ( $this->routeapp_is_shipping_method_excluded( $method_id ) ) {
2392 return 1;
2393 }
2394
2395 return 0;
2396 }
2397
2398 /**
2399 * Register callback for WooCommerce blocks checkout cart updates
2400 * This ensures the Route widget toggle properly updates the cart in blocks checkout
2401 *
2402 * @return void
2403 */
2404 public function routeapp_register_blocks_checkout_callback() {
2405 if ( function_exists( 'woocommerce_store_api_register_update_callback' ) ) {
2406 woocommerce_store_api_register_update_callback(
2407 array(
2408 'namespace' => 'route-widget-integration',
2409 'callback' => function( $data ) {
2410 // Update session if checkbox value is provided
2411 if ( isset( $data['checkbox'] ) && WC()->session ) {
2412 $checkboxValue = filter_var( $data['checkbox'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE );
2413
2414 // Treat invalid values as false to keep session state deterministic.
2415 WC()->session->set( 'checkbox_checked', true === $checkboxValue );
2416 }
2417
2418 // The cart will be recalculated automatically, triggering woocommerce_cart_calculate_fees
2419 return true;
2420 },
2421 )
2422 );
2423 }
2424 }
2425
2426 /**
2427 * Inject Route widget before the checkout block order summary (default placement for blocks checkout).
2428 *
2429 * @param string $block_content The block content.
2430 * @param array $block The full block, including name and attributes.
2431 * @param object $instance The block instance.
2432 * @return string Modified block content with Route widget prepended when applicable.
2433 */
2434 public function routeapp_checkout_block_before_order_summary( $blockContent, $block, $instance ) {
2435 if ( is_admin() && ! wp_doing_ajax() ) {
2436 return $blockContent;
2437 }
2438
2439 if ( $this->route_block_rendered || $this->is_route_block_in_checkout_page() ) {
2440 return $blockContent;
2441 }
2442
2443 if ( ! function_exists( 'WC' ) || ! WC()->cart || ! WC()->session ) {
2444 return $blockContent;
2445 }
2446 if ( empty( $this->routeapp_get_public_token() ) ) {
2447 return $blockContent;
2448 }
2449
2450 WC()->cart->calculate_totals();
2451
2452 $routeHtml = do_shortcode( '[route]' );
2453 if ( empty( $routeHtml ) ) {
2454 return $blockContent;
2455 }
2456
2457 return $routeHtml . $blockContent;
2458 }
2459
2460 /**
2461 * Register the Route Protection custom checkout block.
2462 * Hooked to 'init'.
2463 */
2464 public function routeapp_register_checkout_block() {
2465 if ( ! function_exists( 'register_block_type' ) ) {
2466 return;
2467 }
2468
2469 $block_dir = plugin_dir_path( dirname( __FILE__ ) ) . 'blocks/route-protection';
2470 if ( ! file_exists( $block_dir . '/block.json' ) ) {
2471 return;
2472 }
2473
2474 wp_register_script(
2475 'route-protection-editor',
2476 plugins_url( 'blocks/route-protection/index.js', dirname( __FILE__ ) ),
2477 array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wc-blocks-checkout' ),
2478 defined( 'ROUTEAPP_VERSION' ) ? ROUTEAPP_VERSION : '1.0.0',
2479 true
2480 );
2481
2482 register_block_type(
2483 $block_dir,
2484 array(
2485 'editor_script' => 'route-protection-editor',
2486 'render_callback' => array( $this, 'render_route_protection_block' ),
2487 )
2488 );
2489 }
2490
2491 /**
2492 * Server-side render callback for the route/route-protection block.
2493 *
2494 * @param array $attributes Block attributes.
2495 * @param string $content Block inner content.
2496 * @return string
2497 */
2498 public function render_route_protection_block( $attributes, $content ) {
2499 if ( is_admin() && ! wp_doing_ajax() ) {
2500 return '';
2501 }
2502 if ( ! function_exists( 'WC' ) || ! WC()->cart || ! WC()->session ) {
2503 return '';
2504 }
2505 if ( empty( $this->routeapp_get_public_token() ) ) {
2506 return '';
2507 }
2508
2509 WC()->cart->calculate_totals();
2510
2511 $routeHtml = do_shortcode( '[route]' );
2512
2513 $this->route_block_rendered = true;
2514
2515 return $routeHtml;
2516 }
2517
2518 /**
2519 * Register the Route Protection IntegrationInterface with WooCommerce Blocks.
2520 *
2521 * @param \Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry $integration_registry
2522 */
2523 public function routeapp_register_blocks_integration( $integration_registry ) {
2524 $file = plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-routeapp-blocks-integration.php';
2525 if ( ! class_exists( 'Routeapp_Blocks_Integration' ) && file_exists( $file ) ) {
2526 require_once $file;
2527 }
2528 if ( class_exists( 'Routeapp_Blocks_Integration' ) ) {
2529 $integration_registry->register( new Routeapp_Blocks_Integration() );
2530 }
2531 }
2532
2533 /**
2534 * Whether the store checkout page uses the WooCommerce Checkout block.
2535 *
2536 * @return bool
2537 */
2538 private function routeapp_is_wc_blocks_checkout_page() {
2539 if ( class_exists( '\Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils' ) ) {
2540 return \Automattic\WooCommerce\Blocks\Utils\CartCheckoutUtils::is_checkout_block_default();
2541 }
2542
2543 if ( ! function_exists( 'wc_get_page_id' ) || ! function_exists( 'has_block' ) ) {
2544 return false;
2545 }
2546
2547 $checkout_page_id = wc_get_page_id( 'checkout' );
2548 return $checkout_page_id > 0 && has_block( 'woocommerce/checkout', $checkout_page_id );
2549 }
2550
2551 /**
2552 * Whether routeapp-public-pbc.js should depend on wc-blocks-checkout.
2553 *
2554 * Classic checkout stores often deregister block scripts after they are registered.
2555 * Adding wc-blocks-checkout as a dependency in that case causes WordPress to silently
2556 * drop routeapp-public-pbc.js when the handle is later deregistered.
2557 *
2558 * @return bool
2559 */
2560 private function routeapp_should_add_wc_blocks_checkout_dependency() {
2561 if ( ! wp_script_is( 'wc-blocks-checkout', 'registered' ) ) {
2562 return false;
2563 }
2564
2565 if ( doing_action( 'woocommerce_blocks_enqueue_checkout_block_scripts_before' ) ) {
2566 return true;
2567 }
2568
2569 if ( ! function_exists( 'is_checkout' ) || ! is_checkout() ) {
2570 return false;
2571 }
2572
2573 return $this->routeapp_is_wc_blocks_checkout_page();
2574 }
2575
2576 /**
2577 * Check whether the checkout page contains the route/route-protection block.
2578 *
2579 * @return bool
2580 */
2581 private function is_route_block_in_checkout_page() {
2582 if ( ! function_exists( 'wc_get_page_id' ) || ! function_exists( 'has_block' ) ) {
2583 return false;
2584 }
2585
2586 $checkout_page_id = wc_get_page_id( 'checkout' );
2587 if ( $checkout_page_id <= 0 ) {
2588 return false;
2589 }
2590
2591 $post = get_post( $checkout_page_id );
2592 if ( ! $post ) {
2593 return false;
2594 }
2595
2596 return has_block( 'route/route-protection', $post );
2597 }
2598
2599 }
2600
2601