Changeset 3473063
- Timestamp:
- 03/02/2026 08:00:41 PM (4 weeks ago)
- Location:
- routeapp/trunk
- Files:
-
- 5 added
- 5 edited
-
admin/class-wc-settings-routeapp.php (modified) (1 diff)
-
blocks (added)
-
blocks/route-protection (added)
-
blocks/route-protection/block.json (added)
-
blocks/route-protection/frontend.js (added)
-
blocks/route-protection/index.js (added)
-
public/class-routeapp-public.php (modified) (8 diffs)
-
public/js/routeapp-public-pbc.js (modified) (2 diffs)
-
readme.txt (modified) (2 diffs)
-
routeapp.php (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
routeapp/trunk/admin/class-wc-settings-routeapp.php
r2769494 r3473063 141 141 ), 142 142 array( 143 'name' => __( 'Choose the place where Route Widget will appear on c heckout page', 'routeapp' ),143 'name' => __( 'Choose the place where Route Widget will appear on classic checkout page', 'routeapp' ), 144 144 'type' => 'select', 145 145 'desc' => "<p class='description'>You can edit the place where the route widget will appear. You can check <a href='https://docs.woocommerce.com/wc-apidocs/hook-docs.html' target='_blank'>here</a> for further explanation about how hooks works.</p>", -
routeapp/trunk/public/class-routeapp-public.php
r3459382 r3473063 95 95 */ 96 96 private $route_app_setup_helper; 97 98 /** 99 * Whether the custom Route checkout block has been rendered on this request. 100 */ 101 private $route_block_rendered = false; 97 102 98 103 /** … … 165 170 add_action('woocommerce_blocks_checkout_update_order_meta', array($this, 'routeapp_save_order_status_to_order'), 10, 1); 166 171 add_action('woocommerce_store_api_checkout_update_order_meta', array($this, 'routeapp_save_order_status_to_order'), 10, 1); 172 173 // Register callback for WooCommerce blocks checkout cart updates 174 add_action('woocommerce_blocks_loaded', array($this, 'routeapp_register_blocks_checkout_callback')); 175 176 // Inject Route widget before order summary in checkout block (default placement) 177 add_filter('render_block_woocommerce/checkout-order-summary-block', array($this, 'routeapp_checkout_block_before_order_summary'), 10, 3); 178 179 // Register the Route Protection custom checkout block 180 add_action('init', array($this, 'routeapp_register_checkout_block')); 181 182 // Register IntegrationInterface for WooCommerce Blocks 183 add_action('woocommerce_blocks_checkout_block_registration', array($this, 'routeapp_register_blocks_integration')); 167 184 } 168 185 … … 1500 1517 1501 1518 /** 1519 * Normalize cart item (array) or order item (WC_Order_Item_Product) to a common array format. 1520 * Ensures compatibility with WooCommerce Product Bundles when used in order context. 1521 * 1522 * @param array|WC_Order_Item_Product $item Cart item array or order item object. 1523 * @return array Normalized item with keys: data, line_subtotal, quantity, product_id, variation_id, key, bundled_items, bundled_by. 1524 */ 1525 private function normalize_item_for_bundle_calc($item) { 1526 if (is_object($item) && method_exists($item, 'get_product')) { 1527 /** @var WC_Order_Item_Product $item */ 1528 $product = $item->get_product(); 1529 $bundled_items = $item->get_meta('_bundled_items', true); 1530 $bundled_by = $item->get_meta('_bundled_by', true); 1531 // Use _bundle_cart_key so parent matches child's _bundled_by (Product Bundles uses same hash). 1532 $key = $item->get_meta('_bundle_cart_key', true) ?: $item->get_id(); 1533 return [ 1534 'data' => $product, 1535 'line_subtotal' => (float) $item->get_subtotal(), 1536 'quantity' => (int) $item->get_quantity(), 1537 'product_id' => $item->get_product_id(), 1538 'variation_id' => $item->get_variation_id(), 1539 'key' => $key, 1540 'bundled_items' => !empty($bundled_items) ? $bundled_items : null, 1541 'bundled_by' => !empty($bundled_by) ? $bundled_by : null, 1542 ]; 1543 } 1544 return $item; 1545 } 1546 1547 /** 1502 1548 * Check if product inside order item is virtual, return true if product is not valid 1503 * @param $item1549 * @param array|WC_Order_Item_Product $item Cart item array or order item object. 1504 1550 * @return bool 1505 1551 */ 1506 1552 public function isProductVirtual($item) { 1507 $productId = isset($item['variation_id']) && !empty($item['variation_id']) ? 1508 $item['variation_id'] : 1509 $item['product_id']; 1553 if (is_object($item) && method_exists($item, 'get_product_id')) { 1554 $productId = $item->get_variation_id() ? $item->get_variation_id() : $item->get_product_id(); 1555 } else { 1556 $productId = isset($item['variation_id']) && !empty($item['variation_id']) ? 1557 $item['variation_id'] : 1558 $item['product_id']; 1559 } 1510 1560 1511 1561 $product = wc_get_product($productId); … … 1525 1575 public function processCartItem($cart_item, $allowed_bundle_parents) { 1526 1576 1577 $cart_item = $this->normalize_item_for_bundle_calc($cart_item); 1578 1527 1579 $cart_item_subtotal = $cart_item['line_subtotal']; 1528 1580 1529 //support for WooCommerce Product Bundles 1581 //support for WooCommerce Product Bundles (only when product data is available; order items may have null) 1530 1582 if (isset($cart_item['bundled_items'])) { 1531 if ($cart_item['data']->get_data()['price'] > 0) { 1532 //grab item price from parent bundle if exist, return directly 1533 $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity']; 1534 $data = [ 1535 'id' => strval($cart_item['product_id']), 1536 'quantity' => intval($cart_item['quantity']), 1537 'unit_price' => strval(round($cart_item_subtotal / $cart_item['quantity'], 2)) 1538 ]; 1539 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'data' => $data]; 1540 } else{ 1541 //add bundle parent key to allowed array 1542 $allowed_bundle_parents[] = $cart_item['key']; 1543 } 1583 if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) { 1584 $price = $cart_item['data']->get_data()['price']; 1585 if ($price > 0) { 1586 //grab item price from parent bundle if exist, return directly 1587 $cart_item_subtotal = $price * $cart_item['quantity']; 1588 $data = [ 1589 'id' => strval($cart_item['product_id']), 1590 'quantity' => intval($cart_item['quantity']), 1591 'unit_price' => strval(round($cart_item_subtotal / $cart_item['quantity'], 2)) 1592 ]; 1593 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'data' => $data]; 1594 } 1595 } 1596 //add bundle parent key to allowed array 1597 $allowed_bundle_parents[] = $cart_item['key']; 1544 1598 } 1545 1599 … … 1553 1607 //skip bundle item children (price is on parent) 1554 1608 return ['allowed_bundle_parents' => $allowed_bundle_parents]; 1555 } else { 1609 } 1610 if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) { 1556 1611 //grab subtotal from bundle child 1557 1612 $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity']; … … 1578 1633 public function getCartItemSubtotal($cart_item, $allowed_bundle_parents) { 1579 1634 1635 $cart_item = $this->normalize_item_for_bundle_calc($cart_item); 1636 1580 1637 $cart_item_subtotal = $cart_item['line_subtotal']; 1581 1638 1582 //support for WooCommerce Product Bundles 1639 //support for WooCommerce Product Bundles (only when product data is available; order items may have null) 1583 1640 if (isset($cart_item['bundled_items'])) { 1584 if ($cart_item['data']->get_data()['price'] > 0) { 1585 //grab item price from parent bundle if exist, return directly 1586 $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity']; 1587 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'subtotal' => $cart_item_subtotal]; 1588 } else{ 1589 //add bundle parent key to allowed array 1590 $allowed_bundle_parents[] = $cart_item['key']; 1591 } 1641 if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) { 1642 $price = $cart_item['data']->get_data()['price']; 1643 if ($price > 0) { 1644 //grab item price from parent bundle if exist, return directly 1645 $cart_item_subtotal = $price * $cart_item['quantity']; 1646 return ['allowed_bundle_parents' => $allowed_bundle_parents, 'subtotal' => $cart_item_subtotal]; 1647 } 1648 } 1649 //add bundle parent key to allowed array 1650 $allowed_bundle_parents[] = $cart_item['key']; 1592 1651 } 1593 1652 … … 1601 1660 //skip bundle item children (price is on parent) 1602 1661 return ['allowed_bundle_parents' => $allowed_bundle_parents]; 1603 } else { 1662 } 1663 if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) { 1604 1664 //grab subtotal from bundle child 1605 1665 $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity']; … … 1835 1895 } 1836 1896 1897 /** 1898 * Register callback for WooCommerce blocks checkout cart updates 1899 * This ensures the Route widget toggle properly updates the cart in blocks checkout 1900 * 1901 * @return void 1902 */ 1903 public function routeapp_register_blocks_checkout_callback() { 1904 if ( function_exists( 'woocommerce_store_api_register_update_callback' ) ) { 1905 woocommerce_store_api_register_update_callback( 1906 array( 1907 'namespace' => 'route-widget-integration', 1908 'callback' => function( $data ) { 1909 // Update session if checkbox value is provided 1910 if ( isset( $data['checkbox'] ) && WC()->session ) { 1911 $checkboxValue = filter_var( $data['checkbox'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ); 1912 1913 // Treat invalid values as false to keep session state deterministic. 1914 WC()->session->set( 'checkbox_checked', true === $checkboxValue ); 1915 } 1916 1917 // The cart will be recalculated automatically, triggering woocommerce_cart_calculate_fees 1918 return true; 1919 }, 1920 ) 1921 ); 1922 } 1923 } 1924 1925 /** 1926 * Inject Route widget before the checkout block order summary (default placement for blocks checkout). 1927 * 1928 * @param string $block_content The block content. 1929 * @param array $block The full block, including name and attributes. 1930 * @param object $instance The block instance. 1931 * @return string Modified block content with Route widget prepended when applicable. 1932 */ 1933 public function routeapp_checkout_block_before_order_summary( $blockContent, $block, $instance ) { 1934 if ( is_admin() && ! wp_doing_ajax() ) { 1935 return $blockContent; 1936 } 1937 1938 if ( $this->route_block_rendered || $this->is_route_block_in_checkout_page() ) { 1939 return $blockContent; 1940 } 1941 1942 if ( ! function_exists( 'WC' ) || ! WC()->cart || ! WC()->session ) { 1943 return $blockContent; 1944 } 1945 if ( empty( $this->routeapp_get_public_token() ) ) { 1946 return $blockContent; 1947 } 1948 1949 WC()->cart->calculate_totals(); 1950 1951 $routeHtml = do_shortcode( '[route]' ); 1952 if ( empty( $routeHtml ) ) { 1953 return $blockContent; 1954 } 1955 1956 return $routeHtml . $blockContent; 1957 } 1958 1959 /** 1960 * Register the Route Protection custom checkout block. 1961 * Hooked to 'init'. 1962 */ 1963 public function routeapp_register_checkout_block() { 1964 if ( ! function_exists( 'register_block_type' ) ) { 1965 return; 1966 } 1967 1968 $block_dir = plugin_dir_path( dirname( __FILE__ ) ) . 'blocks/route-protection'; 1969 if ( ! file_exists( $block_dir . '/block.json' ) ) { 1970 return; 1971 } 1972 1973 wp_register_script( 1974 'route-protection-editor', 1975 plugins_url( 'blocks/route-protection/index.js', dirname( __FILE__ ) ), 1976 array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wc-blocks-checkout' ), 1977 defined( 'ROUTEAPP_VERSION' ) ? ROUTEAPP_VERSION : '1.0.0', 1978 true 1979 ); 1980 1981 register_block_type( 1982 $block_dir, 1983 array( 1984 'editor_script' => 'route-protection-editor', 1985 'render_callback' => array( $this, 'render_route_protection_block' ), 1986 ) 1987 ); 1988 } 1989 1990 /** 1991 * Server-side render callback for the route/route-protection block. 1992 * 1993 * @param array $attributes Block attributes. 1994 * @param string $content Block inner content. 1995 * @return string 1996 */ 1997 public function render_route_protection_block( $attributes, $content ) { 1998 if ( is_admin() && ! wp_doing_ajax() ) { 1999 return ''; 2000 } 2001 if ( ! function_exists( 'WC' ) || ! WC()->cart || ! WC()->session ) { 2002 return ''; 2003 } 2004 if ( empty( $this->routeapp_get_public_token() ) ) { 2005 return ''; 2006 } 2007 2008 WC()->cart->calculate_totals(); 2009 2010 $routeHtml = do_shortcode( '[route]' ); 2011 2012 $this->route_block_rendered = true; 2013 2014 return $routeHtml; 2015 } 2016 2017 /** 2018 * Register the Route Protection IntegrationInterface with WooCommerce Blocks. 2019 * 2020 * @param \Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry $integration_registry 2021 */ 2022 public function routeapp_register_blocks_integration( $integration_registry ) { 2023 $file = plugin_dir_path( dirname( __FILE__ ) ) . 'includes/class-routeapp-blocks-integration.php'; 2024 if ( ! class_exists( 'Routeapp_Blocks_Integration' ) && file_exists( $file ) ) { 2025 require_once $file; 2026 } 2027 if ( class_exists( 'Routeapp_Blocks_Integration' ) ) { 2028 $integration_registry->register( new Routeapp_Blocks_Integration() ); 2029 } 2030 } 2031 2032 /** 2033 * Check whether the checkout page contains the route/route-protection block. 2034 * 2035 * @return bool 2036 */ 2037 private function is_route_block_in_checkout_page() { 2038 if ( ! function_exists( 'wc_get_page_id' ) || ! function_exists( 'has_block' ) ) { 2039 return false; 2040 } 2041 2042 $checkout_page_id = wc_get_page_id( 'checkout' ); 2043 if ( $checkout_page_id <= 0 ) { 2044 return false; 2045 } 2046 2047 $post = get_post( $checkout_page_id ); 2048 if ( ! $post ) { 2049 return false; 2050 } 2051 2052 return has_block( 'route/route-protection', $post ); 2053 } 1837 2054 1838 2055 } -
routeapp/trunk/public/js/routeapp-public-pbc.js
r3433074 r3473063 52 52 } 53 53 54 /** 55 * Check if WooCommerce blocks checkout is available 56 * @returns {boolean} 57 */ 58 function isBlocksCheckoutAvailable() { 59 return typeof wc !== 'undefined' && 60 typeof wc.blocksCheckout !== 'undefined' && 61 typeof wc.blocksCheckout.extensionCartUpdate === 'function'; 62 } 63 64 /** 65 * Trigger cart revalidation for WooCommerce blocks checkout 66 * This ensures the cart visually updates when Route is toggled 67 */ 68 function triggerBlocksCheckoutCartUpdate() { 69 if (isBlocksCheckoutAvailable()) { 70 try { 71 wc.blocksCheckout.extensionCartUpdate({ 72 namespace: 'route-widget-integration', 73 data: { 74 checkbox: RouteConfig.checkbox === Route.Coverage.ActiveByDefault 75 } 76 }).then(function() { 77 // Cart update successful - the frontend will automatically refresh 78 }).catch(function(error) { 79 console.error('Route widget: Cart update failed:', error); 80 // Fallback to traditional checkout update 81 triggerCheckoutUpdate(); 82 }); 83 } catch (error) { 84 console.error('Route widget: Error triggering blocks checkout update:', error); 85 // Fallback to traditional checkout update 86 triggerCheckoutUpdate(); 87 } 88 } else { 89 // Fallback to traditional checkout update if blocks checkout is not available 90 triggerCheckoutUpdate(); 91 } 92 } 93 54 94 function updateFee() { 55 95 let checkbox = RouteConfig.checkbox == Route.Coverage.ActiveByDefault; … … 62 102 }, 63 103 success: function () { 64 RouteConfig.is_cart_page ? triggerCartUpdate() : triggerCheckoutUpdate(); 104 if (RouteConfig.is_cart_page) { 105 triggerCartUpdate(); 106 } else { 107 // Use blocks checkout update if available, otherwise fallback to traditional 108 if (isBlocksCheckoutAvailable()) { 109 triggerBlocksCheckoutCartUpdate(); 110 } else { 111 triggerCheckoutUpdate(); 112 } 113 } 65 114 66 115 // Track protection change using RouteAmplitudeAnalytics -
routeapp/trunk/readme.txt
r3459382 r3473063 6 6 Requires at least: 4.0 7 7 Tested up to: 6.7.1 8 Stable tag: 2. 2.378 Stable tag: 2.3.0 9 9 Requires PHP: 5.6 10 10 License: GPLv2 or later … … 106 106 107 107 == Changelog == 108 109 = 2.3.0 = 110 * Add compatibility with checkout WooCommerce Blocks 111 * Fix fatal error when WooCommerce Product Bundles is active and Route fee is calculated for admin/draft orders (order items with null product) 108 112 109 113 = 2.2.37 = -
routeapp/trunk/routeapp.php
r3459382 r3473063 10 10 * Plugin URI: https://route.com/for-merchants/ 11 11 * Description: Route allows shoppers to insure their orders with one-click during checkout, adding a layer of 3rd party trust while improving the customer shopping experience. 12 * Version: 2. 2.3712 * Version: 2.3.0 13 13 * Author: Route 14 14 * Author URI: https://route.com/ … … 26 26 * Currently plugin version. 27 27 */ 28 define( 'ROUTEAPP_VERSION', '2. 2.37' );28 define( 'ROUTEAPP_VERSION', '2.3.0' ); 29 29 30 30 /**
Note: See TracChangeset
for help on using the changeset viewer.