Plugin Directory

Changeset 3473063


Ignore:
Timestamp:
03/02/2026 08:00:41 PM (4 weeks ago)
Author:
routedev
Message:

introducing compatibility with woocommerce blocks and several bug fixes

Location:
routeapp/trunk
Files:
5 added
5 edited

Legend:

Unmodified
Added
Removed
  • routeapp/trunk/admin/class-wc-settings-routeapp.php

    r2769494 r3473063  
    141141                    ),
    142142                    array(
    143                         'name' => __( 'Choose the place where Route Widget will appear on checkout page', 'routeapp' ),
     143                        'name' => __( 'Choose the place where Route Widget will appear on classic checkout page', 'routeapp' ),
    144144                        'type' => 'select',
    145145                        '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  
    9595     */
    9696    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;
    97102
    98103    /**
     
    165170        add_action('woocommerce_blocks_checkout_update_order_meta', array($this, 'routeapp_save_order_status_to_order'), 10, 1);
    166171        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'));
    167184    }
    168185
     
    15001517
    15011518    /**
     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    /**
    15021548     * Check if product inside order item is virtual, return true if product is not valid
    1503      * @param $item
     1549     * @param array|WC_Order_Item_Product $item Cart item array or order item object.
    15041550     * @return bool
    15051551     */
    15061552    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        }
    15101560
    15111561        $product = wc_get_product($productId);
     
    15251575    public function processCartItem($cart_item, $allowed_bundle_parents) {
    15261576
     1577        $cart_item = $this->normalize_item_for_bundle_calc($cart_item);
     1578
    15271579        $cart_item_subtotal = $cart_item['line_subtotal'];
    15281580
    1529         //support for WooCommerce Product Bundles
     1581        //support for WooCommerce Product Bundles (only when product data is available; order items may have null)
    15301582        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'];
    15441598        }
    15451599
     
    15531607                //skip bundle item children (price is on parent)
    15541608                return ['allowed_bundle_parents' => $allowed_bundle_parents];
    1555             } else {
     1609            }
     1610            if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
    15561611                //grab subtotal from bundle child
    15571612                $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity'];
     
    15781633    public function getCartItemSubtotal($cart_item, $allowed_bundle_parents) {
    15791634
     1635        $cart_item = $this->normalize_item_for_bundle_calc($cart_item);
     1636
    15801637        $cart_item_subtotal = $cart_item['line_subtotal'];
    15811638
    1582         //support for WooCommerce Product Bundles
     1639        //support for WooCommerce Product Bundles (only when product data is available; order items may have null)
    15831640        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'];
    15921651        }
    15931652
     
    16011660                //skip bundle item children (price is on parent)
    16021661                return ['allowed_bundle_parents' => $allowed_bundle_parents];
    1603             } else {
     1662            }
     1663            if (isset($cart_item['data']) && $cart_item['data'] instanceof \WC_Product) {
    16041664                //grab subtotal from bundle child
    16051665                $cart_item_subtotal = $cart_item['data']->get_data()['price'] * $cart_item['quantity'];
     
    18351895    }
    18361896
     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    }
    18372054
    18382055}
  • routeapp/trunk/public/js/routeapp-public-pbc.js

    r3433074 r3473063  
    5252        }
    5353
     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
    5494        function updateFee() {
    5595            let checkbox = RouteConfig.checkbox == Route.Coverage.ActiveByDefault;
     
    62102                },
    63103                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                    }
    65114
    66115                    // Track protection change using RouteAmplitudeAnalytics
  • routeapp/trunk/readme.txt

    r3459382 r3473063  
    66Requires at least: 4.0
    77Tested up to: 6.7.1
    8 Stable tag: 2.2.37
     8Stable tag: 2.3.0
    99Requires PHP: 5.6
    1010License: GPLv2 or later
     
    106106
    107107== 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)
    108112
    109113= 2.2.37 =
  • routeapp/trunk/routeapp.php

    r3459382 r3473063  
    1010 * Plugin URI:        https://route.com/for-merchants/
    1111 * 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.37
     12 * Version:           2.3.0
    1313 * Author:            Route
    1414 * Author URI:        https://route.com/
     
    2626 * Currently plugin version.
    2727 */
    28 define( 'ROUTEAPP_VERSION', '2.2.37' );
     28define( 'ROUTEAPP_VERSION', '2.3.0' );
    2929
    3030/**
Note: See TracChangeset for help on using the changeset viewer.