PluginProbe
Route ‑ Shipping Protection / 2.4.12
Route ‑ Shipping Protection v2.4.12
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.12, at public/class-routeapp-public.php

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