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
Recommended owners
| Task | Typical owner |
|---|---|
| Install the app in the VTEX account and link/publish the theme | VTEX agency, developer, or technical lead |
| Activate the app for the website in Koru Suite | Red Clover / Koru Suite administrator |
| Enter the Website ID in the block | Technical lead or theme implementer |
| Decide which categories get the widget | Ecommerce manager |
| Load yield and unit of measurement in the catalog | Catalog or product team |
| Validate price, stock, and calculated quantity on the product page | QA or ecommerce manager |
| Adjust appearance with CSS | Theme 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
| Credential | Entered manually? | Where | Source |
|---|---|---|---|
| Koru Website ID required | Yes | websiteId 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 ID | No | Fixed in the app code | Already included in the build. Not configurable by the implementer. |
| Koru URL | No | Fixed in the app code | https://www.korusuite.com |
| VTEX AppKey / AppToken | Not used | — | The 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 listThe 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-widgetboth inchildrenand 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:
| Group | Handles |
|---|---|
| Container and title | container, title |
| Input fields | fields, field, fieldLabel, fieldInput, formulaSelect |
| Calculation summary | summary, summaryRow, summaryLabel, summaryValue |
| States | loading, error, stockAlert, addedMsg |
| Button | cta, ctaDisabled |
| Koru license gate | koruLoading, 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 listUninstalling applies to the current workspace:
vtex uninstall soluciones4fpartnerar.calculator-widgetThe 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.
| Situation | App behavior |
|---|---|
No websiteId in the block | Unavailable notice; Koru is not called |
| Koru responds authorized | Renders the widget and caches the response in localStorage for 300 seconds (5 min) |
| Koru responds unauthorized | Unavailable 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 verdict | Retries up to 3 times with exponential backoff (1 s, 2 s). If it still fails, it shows the same notice |
| Koru editor preview | Authorizes 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:
- Block props — titles and license.
- VTEX catalog specifications — which formula each product uses and how much it yields. This is where the logic lives.
- Theme CSS — appearance (see Installation).
Block props
| Field (Site Editor) | Prop | Required / Default | What it controls |
|---|---|---|---|
| Title | title | Optional — default "Calculá cuánto necesitás" | Widget heading. If passed empty, the <h3> is not rendered. |
| Button label | ctaText | Optional — default "Agregar al carrito" | CTA label. While adding it shows "Agregando…" and on success "✓ ¡Agregado!" (fixed strings). |
| Koru Website ID | websiteId | Required — 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:
| Value | Accepted names (in order) | Type | Level | Required |
|---|---|---|---|---|
| Yield — how much one sales unit covers | ValorRendimiento → Rendimiento | Numeric, or text containing a number (decimal point: 2.5) | SKU or product | In 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 measured | UnidadMedida → UnidadRendimiento | Text | Product | Yes, 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/uin 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:
UnidadMedida | Formula(s) enabled | Fields requested | Gross calculation |
|---|---|---|---|
kg | peso | Required weight (kg) | peso |
m2 | area and superficie → the shopper picks in a selector | Area: width + height · Surface: direct m² | ancho * alto or superficie |
m | longitud | Length (m) | largo |
m3 | volumen | width + height + depth | ancho * alto * profundidad |
un | cantidad | Quantity (units) | cantidad |
| empty or unrecognized | all 7 formulas in a selector | depends on the choice | depends 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 to | Accepted values |
|---|---|
kg | kg, kgs, kilo, kilos, kilogramo, kilogramos |
m2 | m2, m², mt2, metro cuadrado, metros cuadrados |
m3 | m3, m³, mt3, metro cúbico, metros cúbicos |
m | m, ml, mt, metro, metros, metro lineal, metros lineales |
un | un, 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
id | Visible label | Expression | Inputs |
|---|---|---|---|
area | Area (width × height) | ancho * alto | ancho (m), alto (m) |
perimetro | Perimeter (2 × (width + height)) | 2 * (ancho + alto) | ancho (m), alto (m) |
volumen | Volume (width × height × depth) | ancho * alto * profundidad | ancho, alto, profundidad (m) |
longitud | Linear length | largo | largo (m) |
peso | Weight (required kg) | peso | peso (kg) |
superficie | Surface to cover (direct m²) | superficie | superficie (m²) |
cantidad | Quantity (units) | cantidad | cantidad (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.
SKU specifier specification from the public search
Item level of the Catalog's public search.
Product specification from the 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_stockprecio_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
| Item | Value |
|---|---|
| Route | POST /_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 response | cantidad_final, precio_unitario, precio_total, stock_disponible, alerta_stock, rendimiento, unidad_rendimiento |
| Errors | 400 missing skuId or formulaId · 422 formulaId outside the whitelist, missing input variables, or the formula does not return a finite number |
| Cache | Cache-Control: no-store (price and stock always fresh) |
| Runtime | 256 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.
Recommended setup order
Install and link in a validation workspace
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
customDatawith the values entered by the shopper, for order traceability. - The browser stores the license cache in
localStorageunder the keykoru_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-storeand 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:
- Product context: the block must live inside
store.product. Without a SKU in theproduct-context, the component renders nothing. - Theme dependency: the app must be in the theme's
manifest.json, andvtex linkrestarted after adding it. - Block name:
calculator-widget, orsoluciones4fpartnerar.calculator-widget:calculator-widgetif the theme uses full prefixes. - Backend logs:
vtex logs soluciones4fpartnerar.calculator-widgetI 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.
- The license gate: one
fetchtokorusuite.comper page load, unless a valid 5-minute cache exists. While it responds, the widget area shows "Verificando disponibilidad…". - The calculation: one same-origin
POSTto the app backend per input change, with a 300 ms debounce and cancellation of the previous request, answered withno-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 linkrestarted. - Block declared inside the product page, with product context.
- Website ID entered and license confirmed on the product page.
-
ValorRendimientoandUnidadMedidaspecifications 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.