Skip to content

Developer guide

app/addons/kv_free_shipping_progress_bar/
├── addon.xml scheme 3.0, settings (ROOT,STOREFRONT), uninstall function
├── init.php registers the dispatch_before_display hook
├── func.php template API, hook handler, admin preview, uninstall
├── controllers/frontend/kv_free_shipping_progress_bar.php AJAX refresh endpoint (mode "refresh")
├── controllers/backend/kv_free_shipping_progress_bar_license.php "Check now" / cron license check
├── schemas/block_manager/blocks.post.php block type "kv_free_shipping_progress_bar"
└── Tygh/Addons/KvFreeShippingProgressBar/ PSR-0 classes
├── Config.php validated settings (colors, amounts, enums)
├── GoalCalculator.php session cart → view data
├── ShippingThresholdDetector.php free-shipping threshold from manual rates
├── PromotionGoalDetector.php unlockable cart promotions
├── License/Client.php license check client (generated from the shared Kavio Labs template)
└── Upgrade/Synchronizer.php self-healing in-place upgrades
design/backend/templates/addons/kv_free_shipping_progress_bar/settings/detected_goals.tpl
design/backend/templates/addons/kv_free_shipping_progress_bar/license/info.tpl License tab (points to the card)
design/backend/templates/addons/kv_free_shipping_progress_bar/license/card.tpl License card (add-on page) / banner
design/backend/templates/addons/kv_free_shipping_progress_bar/license/status_bar.tpl status chip/banner for own admin pages
design/backend/templates/addons/kv_free_shipping_progress_bar/license/badge.tpl badge in Add-ons > Manage add-ons
design/backend/templates/addons/kv_free_shipping_progress_bar/hooks/addons/ tabs_content.pre.tpl (card), manage.post.tpl (badge)
design/backend/css/addons/kv_free_shipping_progress_bar/license.less card, chip and badge styles (admin LESS variables)
var/themes_repository/responsive/templates/addons/kv_free_shipping_progress_bar/
├── components/bar.tpl stable holder (AJAX target) + state
├── components/bar_content.tpl message, progressbar, markers, unlocked list, vendor rows
├── components/icon.tpl inline SVG icons
├── blocks/progress_bar.tpl Block Manager template
├── views/kv_free_shipping_progress_bar/refresh.tpl
└── hooks/
├── index/{styles.post,scripts.post,body.pre}.tpl
├── checkout/{minicart.pre,payment_options.pre,shipping_rates.pre}.tpl
└── products/{notification_items.post,product_detail_bottom.post}.tpl
var/themes_repository/responsive/css/addons/kv_free_shipping_progress_bar/styles.less
js/addons/kv_free_shipping_progress_bar/func.js
var/langs/en/addons/kv_free_shipping_progress_bar.po

No core files are modified, and no database tables are created.

Type Hook Purpose
PHP dispatch_before_display Adds kv_fspb_state (cart fingerprint) to every storefront AJAX response
PHP before_dispatch (admin area and console only; static handler License\Client::onBeforeDispatch) Runs the due license check and shows license notices
Schema block_manager/blocks (cart_content cache) License state in the cart block’s cache key
Template (admin) addons:tabs_content (pre) License card at the top of the add-on page
Template (admin) addons:manage (post) License badge, moved into the add-on’s row (4.17–4.21 have no per-row hook)
Template (admin) index:styles (post) license.less
Template index:styles (post), index:scripts (post) Registers LESS and JS
Template index:body (pre) Sticky bar
Template checkout:minicart (pre) Mini-cart placement (core blocks/cart_content.tpl)
Template checkout:payment_options (pre) Cart page placement (guarded by $location == "cart")
Template checkout:shipping_rates (pre) Checkout placement (lite checkout shipping step)
Template products:notification_items (post) “Added to cart” popup (guarded by checkout.add)
Template products:product_detail_bottom (post) Product page (lazy)

All hooks exist in CS-Cart 4.17.2 (Ultimate and Multi-Vendor) and in Multi-Vendor 4.18.3 and 4.21.2.

Tygh\Addons\KvFreeShippingProgressBar\License\Client verifies the license against the Kavio Labs license server.

  • fn_kv_free_shipping_progress_bar_is_licensed() is the storefront gate. When it returns false, fn_kv_free_shipping_progress_bar_is_enabled() returns false for every placement, fn_kv_free_shipping_progress_bar_get_bar_data() returns an empty bar and the Block Manager block renders nothing. It reads the stored state only (one storage read per request) and never contacts the server.
  • The core cart block (cart_content) is cached for guests with an empty cart. Its cache key includes the license state (blocks.post.php, callable_handlers), so the mini-cart bar appears or disappears at once.
  • Client::boot() (init.php) registers a before_dispatch handler for the admin area and the console only. On admin page views and console requests it runs the check when it is due (key changed, or 24 hours after the last check; 1 hour after a failure) and shows the admin notice once per session.
  • Request: POST https://cs-cart.kaviolabs.com/index.php?dispatch=kv_licenses.check with JSON {"v":1, "license", "addon", "version", "hosts":[…], "platform":{"version", "edition"}, "nonce"}, sent through Tygh\Http (3 s connect, 5 s total; kept out of the CS-Cart request log). The answer is accepted only when its Ed25519 signature matches a public key embedded in the client and it echoes the nonce.
  • State: storage key kv_license_kv_free_shipping_progress_bar (?:storage_data), or var/kv_storage/kv_free_shipping_progress_bar/license.json while the full page cache is active, protected by an HMAC with the store’s crypt key.
  • Cron: php admin.php --dispatch=kv_free_shipping_progress_bar_license.check.
  • Admin UI: License\Client assigns the view data of the license card to $kv_lic_kv_free_shipping_progress_bar in its before_dispatch handler, only for administrators with the Manage settings privilege (update_settings); vendors never get it. The templates only read this variable, so they render nothing (and no error) while an older version of the PHP code is loaded, for example in the request that uploads a new zip over the installed add-on. PHP code can call fn_kv_free_shipping_progress_bar_license_ui($context). The add-on has no admin pages of its own, so it uses no status bar.
  • Development stores only (http_host and https_host are development hosts): the constants KV_LICENSE_SERVER_URL, KV_LICENSE_TEST_HOST (act as if the development storefronts ran on that host) and KV_LICENSE_TEST_TIME_OFFSET (seconds). They are ignored on production hosts.
  1. A placement template includes components/bar.tpl with placement, layout and bar_id.
  2. bar.tpl calls fn_kv_free_shipping_progress_bar_get_bar_data(). The result is cached per request and per cart fingerprint, and errors are caught: on failure the bar is hidden and the page is never broken.
  3. GoalCalculator::calculate():
    • reads $cart['product_groups'] (calculated by the core) and filters out groups that need no shipping, groups shipped by the marketplace and download-only groups;
    • per group, runs ShippingThresholdDetector::detectThreshold() on each shipping method of the group (rate_info = the customer’s rate area). The package is built like Shippings::_getPackageInfo(): free-shipping and download products are excluded for methods flagged Use for free shipping;
    • in Ultimate without calculated groups (empty or restored cart) it loads the storefront’s methods with Shippings::getShippingsList() for the customer location;
    • adds promotion goals from PromotionGoalDetector::detect() (core fn_get_promotions() + fn_check_promotion_conditions() on a copy of the cart with a simulated subtotal);
    • maps every goal onto the cart subtotal axis (target = subtotal + remaining), sorts the goals and builds the markers.
  4. bar_content.tpl renders the messages with __() and the amounts with common/price.tpl.

View data (fn_kv_free_shipping_progress_bar_get_bar_data())

Section titled “View data (fn_kv_free_shipping_progress_bar_get_bar_data())”
[
'show' => bool, // render the bar at all
'is_empty' => bool, // empty cart → intro texts
'subtotal' => float, // primary currency
'percent' => float, // 0–100 of the main bar
'show_main' => bool, // there are cart-level goals
'next_goal' => ?array, // nearest unreached goal
'last_reached' => ?array,
'reached_goals' => array,
'all_reached' => bool,
'markers' => array, // [position (%), reached, type, title, target]
'vendors' => array, // MV rows: company_id, name, remaining, threshold, reached, percent
'track_max' => float,
'state' => string, // 12-char cart fingerprint
'style' => string, // validated CSS custom properties
]
// goal: ['type' => free_shipping|gift|discount|reward, 'title', 'target', 'remaining', 'reached', 'source' => shipping|manual|promotion]
  • Every bar lives in a stable holder <div class="kv-fspb-holder" … id="…">…<!--id--></div>. Following the core convention, id is the last attribute and the closing comment is required by Tygh\Ajax. AJAX replaces only the holder’s inner HTML.

  • Holder IDs start with cart_status_kv_fspb_ or checkout_kv_fspb_, so the core result_ids wildcards (cart_status* on add to cart and mini-cart delete, cart*,checkout* on the cart form, checkout* on lite checkout) re-render them with no extra request.

  • dispatch_before_display assigns kv_fspb_state to each storefront AJAX response. func.js listens to ce.ajaxdone and compares it with the data-ca-kv-fspb-state of every bar. Out-of-date bars are re-rendered with one hidden request:

    GET index.php?dispatch=kv_free_shipping_progress_bar.refresh&result_ids=<ids>&bars[<id>][placement]=…&bars[<id>][layout]=…

    The endpoint accepts AJAX requests only (404 otherwise). It validates IDs (^(cart_status_kv_fspb|checkout_kv_fspb|kv_fspb_lazy)_[a-z0-9_]{1,64}$), placements (whitelist, must be enabled) and layouts, with at most 20 bars per request. It only reads the session cart.

  • Lazy bars (lazy=true, ID prefix kv_fspb_lazy_) render an empty holder. Use them inside block-cached content: the product page main block is cached for guests (cache_overrides_by_dispatch['products.view']). func.js loads them on ce.commoninit.

{include file="addons/kv_free_shipping_progress_bar/components/bar.tpl"
placement="block" {* minicart|cart|checkout|notification|product|sticky|block *}
layout="compact" {* full|compact *}
bar_id="cart_status_kv_fspb_my_header" {* unique, see prefixes above *}
}

Helpers callable from templates: fn_kv_free_shipping_progress_bar_get_bar_data(), fn_kv_free_shipping_progress_bar_is_enabled($placement), fn_kv_free_shipping_progress_bar_get_config().

All selectors are scoped under .kv-fspb / .kv-fspb-holder (BEM-like: __message, __track, __fill, __marker, __unlocked, __vendors, modifiers --compact, --full, --reached, --celebrate, --animated, --<placement>, --sticky-top|bottom). Themes can override these custom properties:

.kv-fspb {
--kv-fspb-fill: #1f8a4c;
--kv-fspb-reached: #1f8a4c;
--kv-fspb-track: rgba(0, 0, 0, .09);
--kv-fspb-text: inherit;
--kv-fspb-sticky-bg: #fff;
--kv-fspb-radius: 999px;
--kv-fspb-height: 8px;
}

Admin color settings are emitted as inline custom properties on .kv-fspb and win over theme CSS. RTL is supported ([dir="rtl"]), and animations are disabled under prefers-reduced-motion: reduce.

  • Bar: role="progressbar", aria-valuemin="0", aria-valuemax="100", aria-valuenow, aria-valuetext (plain-text message) and aria-label.
  • Holder: aria-live="polite", so updates after add to cart are announced.
  • Icons are decorative (aria-hidden). The sticky close button has an accessible label and a visible focus ring.
  • Different texts per placement: override components/bar_content.tpl in your theme, or call __() with your own variables in a custom block template that includes bar.tpl.
  • Own placement: include bar.tpl in any template hook with a unique bar_id (prefix cart_status_kv_fspb_).
  • Custom goal sources: the calculation is encapsulated in GoalCalculator. An injectable promotions provider is available as the 4th constructor argument for tests.
  • All SQL goes through db_* placeholders. Serialized data from the database is unserialized with allowed_classes => false.
  • Colors are validated against a hex pattern. Amounts are parsed strictly. Promotion and vendor names are HTML-escaped.
  • The refresh endpoint never changes state, and its input is whitelisted.
  • The license check endpoint (kv_free_shipping_progress_bar_license.check) accepts only CSRF-protected POST requests of administrators, and console requests. Vendors cannot use it.

fn_kv_free_shipping_progress_bar_uninstall() removes every block of type kv_free_shipping_progress_bar with Block::instance()->remove(), which also cleans snapping, descriptions and content. The core removes settings and language variables.

Icons are adapted from Feather (MIT License).

Tygh/Addons/KvFreeShippingProgressBar/License/Ed25519.php (checks the signature of the license server’s answers on servers whose PHP has no sodium extension) is derived from paragonie/sodium_compat, ISC License, Copyright (c) 2016-2023 Paragon Initiative Enterprises and Copyright (c) 2013-2019 Frank Denis. The full license notice is in the file header.