Koru Calculator Widget

Product calculator for VTEX product pages. The shopper enters a measurement and the widget resolves how many units are needed, validates real price and stock, and adds them to the cart.

Koru Calculator Widget is a VTEX IO app declared as a theme block on the product page. It solves the case of products that are sold by unit but consumed by measurement: paint that covers 10 m² per can, feed sold in 20 kg bags, fencing supplied in 10 m rolls.

The shopper enters the measurement they need — m², meters, kg, m³, or units — and the widget computes ceil(measurement / yield), reads real price and stock from the VTEX Catalog, displays the total, and adds the item to the native cart with the exact quantity.

This app is not configured from Koru

Unlike other Koru Suite apps, Koru is used only for licensing here: enabling the website and the app. Functional behavior is defined on the store side, in two places: the VTEX catalog specifications (which formula to use and how much each product yields) and the block props (title, button label, Website ID). The formula and the yield are always read from the catalog; there is no way to set them from the block.

What the Calculator Widget does not do

It does not modify prices or stock in VTEX; it does not create products, SKUs, or specifications; it does not process payments, and it does not interfere with checkout beyond writing a traceability note in the OrderForm; formulas cannot be edited from a panel; and it does not replace the theme's buy button for regular purchases.

Before you start

TaskTypical owner
Install the app in the VTEX account and link/publish the themeVTEX agency, developer, or technical lead
Activate the app for the website in Koru SuiteRed Clover / Koru Suite administrator
Enter the Website ID in the blockTechnical lead or theme implementer
Decide which categories get the widgetEcommerce manager
Load yield and unit of measurement in the catalogCatalog or product team
Validate price, stock, and calculated quantity on the product pageQA or ecommerce manager
Adjust appearance with CSSTheme frontend developer

A single person may cover several roles in a small store.

Required access

Before installing, confirm that you have:

  • A valid VTEX Admin session in the store account.
  • The official VTEX CLI, installed and up to date.
  • Permission to install apps and link the theme in the chosen workspace.
  • Access to Catalog → Specifications to create and assign the yield and unit fields.
  • The store's Website ID in Koru Suite, with Koru Calculator Widget active for that website.

The app does not request or store merchant credentials: the backend uses the app's own authToken to reach the Catalog. No AppKey/AppToken is required.

Credentials and identifiers

CredentialEntered manually?WhereSource
Koru Website ID requiredYeswebsiteId prop of the calculator-widget block (Site Editor or product.jsonc)Koru, in the store's website record. It is the only Koru value entered by hand. Without it, the widget shows the unavailable notice and never renders the calculator.
Koru App IDNoFixed in the app codeAlready included in the build. Not configurable by the implementer.
Koru URLNoFixed in the app codehttps://www.korusuite.com
VTEX AppKey / AppTokenNot usedThe app uses its own VTEX IO token.

The Website ID is not a secret or a password: it travels in the frontend as a query parameter during license validation and is visible in the block's HTML. Koru only validates that the Website ID + App ID combination is active. The app's Client Secret never reaches the frontend.

Installation

This app is installed only as a VTEX IO theme dependency (Store Framework). There is no <script> or Google Tag Manager version.

Install the app in the account

vtex login {store-account}
vtex use {workspace} --create
vtex whoami                                  # verify account and workspace before continuing
vtex install soluciones4fpartnerar.calculator-widget@0.x
vtex list                                    # confirm it appears in the list

The app is published from the official soluciones4fpartnerar account: install that VTEX App ID as is in your store account.

Declare the theme dependency

In the store theme's manifest.json:

{
  "dependencies": {
    "soluciones4fpartnerar.calculator-widget": "0.x"
  }
}

Changes to dependencies are not hot-reloaded: restart the theme's vtex link after adding it.

Declare the block on the product page

The block is named calculator-widget and requires product context (vtex.product-context): it must live inside store.product or inside a block that provides the product context. Anywhere else it cannot resolve a skuId and renders nothing.

In the theme's product.jsonc:

{
  "flex-layout.col#details-column": {
    "children": [
      "vtex.store-components:product-name",
      "flex-layout.row#product-prices",
      "calculator-widget",
      "flex-basic#buy-button"
    ]
  },
 
  "calculator-widget": {
    "props": {
      "title": "Calculate how much you need",
      "ctaText": "Add to cart",
      "websiteId": "YOUR-WEBSITE-ID"
    }
  }
}
  • If the theme references dependency blocks with their full prefix, use soluciones4fpartnerar.calculator-widget:calculator-widget both in children and in the declaration key.
  • The block is declared with "allowed": []: it accepts no child blocks.

Validate on the product page

Open a product in the category where you placed the block. You should briefly see "Verificando disponibilidad…" and then the calculator, with the fields that match the product's unit of measurement.

Multiple instances in the same store

The block supports the standard VTEX #id instance pattern, for example to render a variant elsewhere on the product page:

{
  "calculator-widget#compact": {
    "props": {
      "title": "Quick calculator",
      "websiteId": "YOUR-WEBSITE-ID",
      "blockClass": "compact"
    }
  }
}
  • Each instance needs its own websiteId: the prop is not inherited between instances.
  • The block does not set its own position — it renders wherever you place it in the block tree. Because it needs product context, every instance must live on the product page.

The component does not declare blockClass in its schema, so the derived modifier handle (...--compact) is not guaranteed. If you need to style instances separately, inspect the rendered HTML first to confirm which classes are generated before writing CSS.

Visual customization with CSS

Default styles come from Tachyons with VTEX design tokens (t-heading-5, c-on-base, bg-action-primary, b--muted-4), so the widget automatically inherits the account's colors, typography, and button style. The app's .css files do not hardcode colors.

Every element also exposes a CSS handle. The theme override file is:

styles/css/soluciones4fpartnerar.calculator-widget.css
.cta {
  border-radius: 0;
  text-transform: uppercase;
}
 
.stockAlert {
  font-weight: 600;
}

Use direct handles in the app override file. Do not use :global() to target this app from another app's CSS file: the VTEX build rejects it.

Available handles:

GroupHandles
Container and titlecontainer, title
Input fieldsfields, field, fieldLabel, fieldInput, formulaSelect
Calculation summarysummary, summaryRow, summaryLabel, summaryValue
Statesloading, error, stockAlert, addedMsg
Buttoncta, ctaDisabled
Koru license gatekoruLoading, koruError, koruErrorTitle, koruErrorText

Updating and uninstalling

Select the correct account/workspace, confirm it, and install the range again:

vtex whoami
vtex install soluciones4fpartnerar.calculator-widget@0.x
vtex list

Uninstalling applies to the current workspace:

vtex uninstall soluciones4fpartnerar.calculator-widget

The websiteId lives in the theme block configuration, not in app settings: if you uninstall and reinstall the app, the prop remains in the theme.

Activation in Koru Suite

The app is license-gated. If the website and the app are not active in Koru, the widget is replaced by the notice "Widget no disponible — Esta aplicación no está habilitada para este sitio. Comuníquese con Korusuite."

Get the Website ID

It is available in Koru, in the store's website record. Koru Suite activates the app for that website; the implementer only copies the ID.

Enter it in the block

The websiteId prop of calculator-widget, in the Site Editor or directly in product.jsonc. It is stored as part of the theme block configuration: it is not a VTEX app setting nor Master Data.

Confirm the license is active

Open a product page containing the block. You should briefly see "Verificando disponibilidad…" and then the calculator. If you see the "Comuníquese con Korusuite" notice instead, the Website ID is wrong or empty, or the app is not enabled for that website.

The gate runs client-side from the shopper's browser. No Koru login or VTEX Admin session is involved: the only thing validated is that the Website ID + App ID pair is authorized.

SituationApp behavior
No websiteId in the blockUnavailable notice; Koru is not called
Koru responds authorizedRenders the widget and caches the response in localStorage for 300 seconds (5 min)
Koru responds unauthorizedUnavailable notice. Negative responses are not cached: as soon as the app is activated in Koru, the next page load renders it
Network failure or response without a verdictRetries up to 3 times with exponential backoff (1 s, 2 s). If it still fails, it shows the same notice
Koru editor previewAuthorizes without a network call

The app's calculation backend is a public route and does not pass through the Koru gate. The gate controls whether the widget renders, not access to the endpoint.

Configuration

Configuration is split across three places, and none of them is Koru:

  1. Block props — titles and license.
  2. VTEX catalog specifications — which formula each product uses and how much it yields. This is where the logic lives.
  3. Theme CSS — appearance (see Installation).

Block props

Field (Site Editor)PropRequired / DefaultWhat it controls
TitletitleOptional — default "Calculá cuánto necesitás"Widget heading. If passed empty, the <h3> is not rendered.
Button labelctaTextOptional — default "Agregar al carrito"CTA label. While adding it shows "Agregando…" and on success "✓ ¡Agregado!" (fixed strings).
Koru Website IDwebsiteIdRequired — default ""License. Empty means the widget is denied.

The formula and the yield are not props. The block does not expose them and there is no way to override them from the Site Editor: they come from the catalog.

Catalog specifications

Create two specification fields in VTEX Admin (Catalog → Specifications, or from the category's specification group) and assign them to the categories where the widget runs. The app looks them up by exact name, trying the candidates in order:

ValueAccepted names (in order)TypeLevelRequired
Yield — how much one sales unit coversValorRendimientoRendimientoNumeric, or text containing a number (decimal point: 2.5)SKU or productIn practice yes: if missing, the yield falls back to 1 and the calculated quantity equals the raw measurement
Unit of measurement — how that yield is measuredUnidadMedidaUnidadRendimientoTextProductYes, if you want the widget to pick the formula automatically

Rules to keep in mind:

  • The field name is what matters, not the specification group.
  • The yield must be a number greater than 0. Empty, 0, or non-numeric values are ignored and the cascade continues.
  • The unit is read from the product, not the SKU, and is used for two things: choosing the formula and displaying Rendimiento X <unidad> c/u in the summary.

Unit of measurement → active formula

The value of UnidadMedida determines which formula the widget uses and which fields the shopper is asked for:

UnidadMedidaFormula(s) enabledFields requestedGross calculation
kgpesoRequired weight (kg)peso
m2area and superficie → the shopper picks in a selectorArea: width + height · Surface: direct m²ancho * alto or superficie
mlongitudLength (m)largo
m3volumenwidth + height + depthancho * alto * profundidad
uncantidadQuantity (units)cantidad
empty or unrecognizedall 7 formulas in a selectordepends on the choicedepends on the choice

perimetro is not mapped to any unit. Since the block cannot pin a formula, the only way to offer it is to leave the product with an empty or unrecognized unit: all 7 formulas are then listed and the shopper chooses.

Accepted aliases (case, spaces, accents, and periods are normalized):

Resolves toAccepted values
kgkg, kgs, kilo, kilos, kilogramo, kilogramos
m2m2, , mt2, metro cuadrado, metros cuadrados
m3m3, , mt3, metro cúbico, metros cúbicos
mm, ml, mt, metro, metros, metro lineal, metros lineales
unun, u, unidad, unidades

Any other value counts as "unrecognized" and all 7 formulas are offered. Recommendation: always load the short form (kg, m2, m, m3, un).

Available formulas

idVisible labelExpressionInputs
areaArea (width × height)ancho * altoancho (m), alto (m)
perimetroPerimeter (2 × (width + height))2 * (ancho + alto)ancho (m), alto (m)
volumenVolume (width × height × depth)ancho * alto * profundidadancho, alto, profundidad (m)
longitudLinear lengthlargolargo (m)
pesoWeight (required kg)pesopeso (kg)
superficieSurface to cover (direct m²)superficiesuperficie (m²)
cantidadQuantity (units)cantidadcantidad (un)

Formulas are not editable from any panel: they live in the app code. Adding or changing one requires publishing a new app version. The client never sends the expression: it sends a formulaId that the backend validates against the whitelist, and evaluation runs in restricted mode (no eval, no new Function, no function definitions or assignments).

How the calculation works

Yield cascade

The yield is resolved server-side, taking the first valid value (greater than 0) in this sequence:

SKU specification via the private Catalog

Covers SKUs with different yields inside the same product — for example, a small 1.2 m² rug versus a large 1.8 m² one. Read immediately: it does not depend on reindexing.

Item level of the Catalog's public search.

Product level. Depends on the Catalog reindex.

Payload override

The backend accepts a rendimientoUnidad value in the body, but the block never sends it: it is only reachable by calling the endpoint manually.

1 as the last resort

If no source returns a valid value, the yield is 1 and the calculated quantity ends up being the raw measurement.

Final calculation

gross          = expression(shopper inputs)
final_quantity = max(0, ceil(gross / yield))
total_price    = unit_price * final_quantity
stock_alert    = final_quantity > available_stock

precio_unitario and stock_disponible come from the Catalog's public search (Price and AvailableQuantity of the SKU's first seller). If the SKU has no offer, both return 0.

Backend endpoint

ItemValue
RoutePOST /_v/calculator-widget/calculate (public, same-origin)
Body sent by the block{ "skuId": "123", "formulaId": "area", "inputs": { "ancho": 3, "alto": 2 } }
Optional overrides accepted (not used by the block)rendimientoUnidad (number), rendimientoSpecName (string), unidadSpecName (string)
200 responsecantidad_final, precio_unitario, precio_total, stock_disponible, alerta_stock, rendimiento, unidad_rendimiento
Errors400 missing skuId or formulaId · 422 formulaId outside the whitelist, missing input variables, or the formula does not return a finite number
CacheCache-Control: no-store (price and stock always fresh)
Runtime256 MB, 10 s timeout, 2–4 replicas

The block calls this endpoint with a 300 ms debounce whenever the inputs, the selected SKU, or the chosen formula change, and cancels the previous request. Nothing is recalculated on the client: it only renders the response.

What happens when adding to the cart

Item is added

The selected SKU is added with the calculated quantity through vtex.order-items.

addToCart event

Dispatched through usePixel for analytics and to open the minicart (the theme's minicart.v2 opens automatically when openOnAddDefault: true, which is the default).

Best-effort traceability

The calculation detail — skuId, formulaId, entered values, and result — is stored in the OrderForm's customData. If it fails, it does not block the add-to-cart action.

Requirement for traceability

For the calculation detail to persist on the order, the app must be registered as a customData app in the account's checkout, with the calculatorwidget appId. Without that registration the cart still works, but the data is silently discarded. Coordinate it with Koru before publishing to master.

Confirm with vtex whoami before installing. Do not start directly in master.

Enter the Website ID and confirm the license

The product page must render the calculator, not the unavailable notice.

Create the specifications and assign them to the category

ValorRendimiento (numeric) and UnidadMedida (text), assigned to the categories where the widget runs.

Load a pilot product

Yield greater than 0 and the unit in short form. If the yield changes per variation, load it at SKU level.

Validate the calculation against the catalog

Confirm that the requested fields match the unit, that the quantity equals ceil(measurement / yield), and that price and stock match the SKU.

Test the add-to-cart flow

Verify that the minicart opens with the calculated quantity and that the item is correct in checkout.

Adjust the CSS and roll out to the rest of the catalog

Only then extend the specifications to the remaining categories and promote to master.

Data and privacy

  • The app has no storage of its own: no Master Data, no VBase.
  • The only persisted data is the OrderForm customData with the values entered by the shopper, for order traceability.
  • The browser stores the license cache in localStorage under the key koru_widget_<WEBSITE_ID>_<APP_ID>, for 300 seconds.
  • No merchant credentials are requested or stored. There are no Koru secrets in the browser.
  • The calculation endpoint responds with no-store and does not record personal data: it receives measurements, not shopper identity.

Functional limits

The current version:

  • Requires product context: it works only on the product page.
  • Does not allow pinning the formula or the yield from the block or the Site Editor.
  • Does not allow creating or editing formulas without publishing a new app version.
  • Adds the item with a single seller: it is not designed for multi-seller or marketplace stores.
  • Reads price and stock from the sales channel resolved by the public search. In stores with multiple sales channels or trade-policy pricing, the displayed price may not match the shopper's policy.
  • Shows the stock alert as a visual warning, without blocking the purchase.
  • Ships its internal copy in Spanish; it is not translatable from the theme.
  • Has no <script> or Google Tag Manager version.

Troubleshooting

The app was activated in Koru but the "Comuníquese con Korusuite" notice remains

Negative responses are not cached, so a Koru activation takes effect on the next page load. What is cached is the positive response, for 300 seconds (5 min), in localStorage under the key koru_widget_<WEBSITE_ID>_<APP_ID>. To force it: delete that key from localStorage or wait 5 minutes.

If the notice persists, verify that the block's websiteId matches Koru exactly (no spaces) and that the app is enabled for that website.

The widget does not appear at all — neither the calculator nor the error notice

In this order:

  1. Product context: the block must live inside store.product. Without a SKU in the product-context, the component renders nothing.
  2. Theme dependency: the app must be in the theme's manifest.json, and vtex link restarted after adding it.
  3. Block name: calculator-widget, or soluciones4fpartnerar.calculator-widget:calculator-widget if the theme uses full prefixes.
  4. Backend logs:
vtex logs soluciones4fpartnerar.calculator-widget

I changed UnidadMedida or the yield and the widget did not notice

UnidadMedida and the product-level yield reach the storefront through the search index: the change is not instant, you must wait for the Catalog reindex and hard-refresh the product page. You can verify the current value at:

https://{account}.vtexcommercestable.com.br/api/catalog_system/pub/products/search/?fq=productId:{id}

A yield loaded at SKU level is read through the private Catalog and applies immediately.

It asks for fields that do not match the product

For example, width and height on a product sold by kg: UnidadMedida is empty, misspelled, or outside the accepted aliases. Load kg, m2, m, m3, or un.

A selector with 7 formulas appears

That is the expected behavior when the unit is not recognized. Load a valid unit if you want the widget to choose on its own. Conversely, if you need to offer perimetro, this is the only way.

The calculated quantity equals the entered measurement

As if the product yielded 1: the yield specification is missing, or its value is 0, non-numeric, or uses a comma instead of a period. Load ValorRendimiento greater than 0 on the product or the SKU.

The yield does not change when switching variation

It is loaded at product level. To make it vary per variation, load it at SKU level: the widget resends the selected skuId and recalculates on its own.

Total price is 0 and stock is 0

The SKU has no offer/price in the sales channel returned by the public search. Review the SKU's price and availability. As a side effect, with stock 0 the stock alert always shows.

It says "Stock insuficiente" but still allows adding to the cart

This is intentional: the stock alert is a visual warning. The button is disabled only when the calculated quantity is 0 or while the item is being added.

The shopper leaves a field empty or types text

While any formula field is missing, the backend is not called and there is no result. Non-numeric values are discarded on the server; if that leaves a formula variable without a value, the widget shows "No se pudo calcular. Revisá los valores e intentá de nuevo.".

Inputs are numeric with min=0, but the backend does not reject negatives: it accepts any finite number and then clamps the final quantity with max(0, …).

Does it affect site performance?

The widget is fully client-side when loading: it does not block product page rendering, but it makes two kinds of request from the browser.

  1. The license gate: one fetch to korusuite.com per page load, unless a valid 5-minute cache exists. While it responds, the widget area shows "Verificando disponibilidad…".
  2. The calculation: one same-origin POST to the app backend per input change, with a 300 ms debounce and cancellation of the previous request, answered with no-store (not cacheable by design, because it returns real price and stock).

Does it depend on other apps?

The block depends on vtex.product-context, vtex.order-items, vtex.order-manager, vtex.css-handles, vtex.render-runtime, vtex.format-currency, and vtex.pixel-manager, plus vtex.store@2.x as a peer dependency. The summary currency is resolved by vtex.format-currency using the store configuration.

Checklist before publishing

  • App installed in the correct account and workspace.
  • Dependency declared in the theme and vtex link restarted.
  • Block declared inside the product page, with product context.
  • Website ID entered and license confirmed on the product page.
  • ValorRendimiento and UnidadMedida specifications created and assigned to the widget's categories.
  • Yield greater than 0 on the product or the SKU.
  • Unit of measurement loaded in a recognized short form.
  • SKU-level yield where it varies per variation.
  • Quantity, price, and stock validated against the catalog on a pilot product.
  • Add-to-cart and minicart tested end to end.
  • Checkout customData registration coordinated, if traceability is required.
  • Override CSS reviewed on desktop and mobile.

Useful details when requesting support

Report the VTEX account, workspace, product page URL, productId and skuId, the loaded unit of measurement and yield, the entered measurement, the quantity you expected, and the quantity the widget returned. Avoid sending credentials, cookies, or personal data that is not required for diagnosis.

Koru Calculator Widget — Developers · Koru Suite