> ## Documentation Index
> Fetch the complete documentation index at: https://docs.commercy.com.ar/llms.txt
> Use this file to discover all available pages before exploring further.

# Scripts en la tienda

> Cómo tu app agrega un widget o un script a la tienda online del comercio: declaración, SRI, carga y window.Commercy.

Un script de tienda le permite a tu app agregar un widget o un fragmento de código a la tienda online de un comercio: un botón de chat, un botón de WhatsApp, un banner de cuotas, un popup que el comercio configuró. Declarás hasta **3** scripts en tu app, el equipo de Commercy los revisa, y cuando el comercio instala tu app los cargamos en su tienda.

Un script corre con el mismo origen que la tienda, así que tiene mucho poder. Por eso es el permiso más controlado del marketplace: pasa por revisión, el navegador verifica que el archivo no cambió (SRI), el comercio lo ve y lo puede apagar cuando quiera, y nosotros podemos cortarlo al instante.

## Qué es y qué no es

* **Es** código tuyo, servido desde tu dominio, que se carga en las páginas públicas de la tienda: inicio, catálogo, ficha de producto, carrito, búsqueda, páginas y contacto.
* **No es** una forma de tocar el checkout. Nunca cargamos scripts de apps en el checkout ni en la confirmación de la compra, en «Mi cuenta», en el login ni el registro, en la recuperación de clave, en el botón de arrepentimiento, ni en la vista previa del editor de la tienda. Ver «Rutas excluidas».
* **No es** una API: no recibís datos de clientes ni podés escribir en Commercy desde la tienda. Lo que sabe tu script es lo que ve el visitante, más el contexto de `window.Commercy` (más abajo).
* **En esta versión** sólo se aceptan scripts **necesarios** para la función que el comercio instaló. Ver la sección «Qué cuenta como necesario».

## Las tres llaves

Un script se carga en una tienda sólo si se cumplen las tres a la vez:

1. **Commercy habilitó los scripts en tu app.** La capacidad la prende el equipo de Commercy al aprobar tu app, y la puede apagar en cualquier momento.
2. **El comercio concedió el scope `storefront_scripts`** al instalar tu app (ver [Scopes y permisos](https://docs.commercy.com.ar/scopes.md)). El comercio ve un aviso destacado: «Agregar código a tu tienda online».
3. **La declaración está aprobada.** Si tu app ya está publicada, lo que cambiás pasa por revisión y mientras tanto sigue corriendo la versión aprobada.

Además valen las condiciones de siempre: la instalación tiene que estar activa, el plan del comercio tiene que incluir las apps y la tienda no puede estar suspendida.

El comercio puede **apagar los scripts de tu app** desde el detalle de la app instalada, sin desinstalarla. Si la desinstala o revoca el acceso, también dejan de cargarse.

Si tu app ya estaba instalada y le agregás el scope `storefront_scripts`, cada comercio tiene que volver a autorizarla: hasta que lo haga no cargamos nada en su tienda.

## Cómo declarar un script

En el portal, abrí tu app y andá a la pestaña **Scripts**. Cada fila tiene:

| Campo | Qué es |
| - | - |
| `handle` | Identificador estable del script dentro de tu app: 2 a 40 caracteres, minúsculas, números, `-` y `_`, empieza con letra o número. No se puede repetir. |
| `name` | Nombre legible, hasta 40 caracteres. Es el que ve el comercio y el que aparece en los mensajes de la consola. |
| `src` | URL del archivo. Ver «Dónde puede vivir el archivo». |
| `integrity` | Hash SRI `sha384-...` del archivo. Ver «Hash SRI». |
| `loading` | Cuándo se carga: `interaction` (por defecto) o `idle`. Ver «Cuándo se carga». |
| `consent` | En esta versión tiene que ser `necessary`. |

También podés declararlos por API, con el `PATCH /api/v1/developers/apps/:id` que usa el portal:

```json theme={null}
{
  "storefrontScripts": [
    {
      "handle": "chat-widget",
      "name": "Botón de chat",
      "src": "https://cdn.tuapp.com/widget.v3.js",
      "integrity": "sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC",
      "loading": "interaction",
      "consent": "necessary"
    }
  ]
}
```

Además tenés que pedir el scope `storefront_scripts` en la pestaña **Scopes**: sin él, no podés enviar la app a revisión.

Los límites:

| Límite | Valor |
| - | - |
| Scripts por app | 3 |
| Scripts por tienda | 10 (si una tienda instala más, se cargan los de las apps instaladas primero) |
| Tamaño del archivo | 256 KB sugerido |

El tamaño no lo bloqueamos al guardar, pero el portal te avisa y la revisión lo mira.

## Dónde puede vivir el archivo

* Tiene que ser `https`.
* Tiene que estar en el **mismo dominio** que la **URL de tu app** (`appUrl`): `cdn.tuapp.com` sirve si tu app está en `app.tuapp.com`. Si tu `appUrl` no tiene un dominio propio, no podemos validar el script.
* No puede ser un dominio de Commercy ni el dominio de una tienda.
* No puede llevar usuario ni contraseña en la URL, ni usar un puerto menor a 1024, ni apuntar a una IP privada.

Si después cambiás el `appUrl` y un script queda en otro dominio, deja de cargarse.

## Hash SRI

Cada script lleva el hash `sha384` de su contenido. El navegador de cada visitante lo verifica antes de ejecutar el archivo: si alguien lo cambia, **no corre**. En el portal, el botón **Calcular hash** lo hace por vos. Desde una terminal:

```bash theme={null}
echo "sha384-$(openssl dgst -sha384 -binary widget.v3.js | openssl base64 -A)"
```

El resultado tiene que ser `sha384-` seguido de 64 caracteres.

### El archivo se sirve así

Para que el navegador pueda verificar el hash, tu servidor tiene que cumplir tres cosas:

* **Sin redirects.** Una respuesta `301` o `302` rompe la verificación y es una forma de cambiar el contenido sin que nos enteremos. La `src` tiene que responder `200` directo.
* **Con CORS.** La respuesta lleva `Access-Control-Allow-Origin: *`. Sin ese header, el navegador no ejecuta el script (cargamos con `crossorigin="anonymous"`).
* **Con `Content-Type: text/javascript`.**

Y en una **URL versionada** (por ejemplo `widget.v3.js`) cuyo contenido no cambia nunca.

### Cómo actualizar sin romper

Como el hash está atado al contenido, **no pises el archivo en la misma URL**: todas las tiendas dejarían de cargarlo hasta que apruebe la revisión.

1. Publicá el archivo nuevo en una URL nueva (`widget.v4.js`).
2. Dejá **vivo el archivo viejo** (`widget.v3.js`): las tiendas siguen cargándolo hasta que se apruebe el cambio.
3. Calculá el hash del nuevo, y cambiá `src` e `integrity` en el portal. Si tu app está publicada, el cambio pasa por revisión y se aplica al aprobarlo.
4. Cuando el cambio esté aprobado y pasaron unos minutos, ya podés retirar el archivo viejo.

## Cuándo se carga

Cargamos el script con `async`, así que nunca bloquea la página.

* **`interaction`** (por defecto): después de la primera interacción del visitante (un toque, una tecla, un scroll o un movimiento de la rueda). Si el visitante no interactúa, el script no se carga. Es lo mejor para el rendimiento de la tienda.
* **`idle`**: cuando termina de cargar la página y el navegador está libre. Se puede usar, pero la revisión te va a pedir que justifiques por qué no alcanza con `interaction`.

Se carga una sola vez por documento. Si el archivo no se puede cargar (hash que no coincide, falta CORS, un redirect), el navegador lo bloquea y en la consola de la tienda aparece: `[Commercy] No se pudo cargar el script "<handle>" de <tu app>. Revisá el hash SRI y que el archivo se sirva con CORS y sin redirects.`

## Qué cuenta como necesario

En esta versión sólo aceptamos `consent: "necessary"`. Un script es necesario cuando **sin él no anda la función que el comercio instaló y no hace perfilado del visitante**: un chat, un botón de WhatsApp, un banner de cuotas, un popup que configuró el comercio.

**No es necesario**, y por lo tanto todavía no se puede declarar: la medición de audiencia, los mapas de calor, la grabación de sesiones, los píxeles publicitarios, el remarketing y el envío de eventos a plataformas de anuncios. Esos casos llegan más adelante, con el consentimiento del visitante.

Si declarás `analytics` o `marketing`, la API responde con el motivo `consent_unsupported`.

## `window.Commercy`

Antes de cargar el primer script definimos `window.Commercy`: un objeto **congelado** (no lo podés modificar ni reemplazar) y **sin datos personales**. No incluye email, nombre, teléfono, dirección, documentos ni ids de clientes, ni tokens.

| Propiedad o método | Qué devuelve |
| - | - |
| `Commercy.version` | `1` |
| `Commercy.store` | `{ id, shopId, name, domain, currency, locale }`. `locale` es siempre `"es-AR"`; `shopId` puede ser `null`. |
| `Commercy.getPage()` | `{ type, path, product }`. `type` es `home`, `product`, `catalog`, `search`, `cart`, `page`, `contact` u `other`. `path` no incluye la query. `product` es `{ id, name, price }` en una ficha de producto y `null` en el resto. |
| `Commercy.getCart()` | `{ itemCount, subtotal, currency, items }`. Cada item es `{ productId, variantSku, name, quantity, unitPrice }`. |
| `Commercy.getCustomer()` | `{ loggedIn }` y nada más. |
| `Commercy.on(event, cb)` | Se suscribe a un evento. Devuelve una función para desuscribirse. |
| `Commercy.off(event, cb)` | Se desuscribe. |

Cada llamada a un `get` devuelve una foto nueva, congelada. No se puede emitir eventos desde un script: sólo escuchar.

### Eventos

Son cuatro. Todos los payloads están congelados.

| Evento | Cuándo | Payload |
| - | - | - |
| `page_viewed` | Al entrar a una página permitida y en cada cambio de página. | `{ type, path }` |
| `product_viewed` | Al ver la ficha de un producto. | `{ productId, name, price, currency }` |
| `product_added_to_cart` | Cuando el visitante agrega un producto y el server lo aceptó. | `{ productId, variantSku, name, price, quantity, currency }` (`quantity` es la cantidad **agregada**) |
| `checkout_started` | Cuando el visitante pasa del carrito al checkout. | `{ itemCount, subtotal, currency, items }` (la forma de `getCart()`) |

Como tu script se carga tarde, `page_viewed` y `product_viewed` se **reenvían** al suscribirte: recibís el último de la página actual. Los otros dos no se repiten. Si un callback tira un error, lo atrapamos y no afecta a los demás.

Un `on()` con un evento desconocido, o con algo que no es una función, se ignora y deja un aviso en la consola. Hay un tope de 50 listeners por evento.

### Ejemplo

```js theme={null}
// commercy-script-example
(function () {
  'use strict';
  if (!window.Commercy) return;
  var C = window.Commercy;

  function send(name, data) {
    // Mandá lo que necesites a tu backend. Acá sólo lo mostramos.
    console.log('[mi-widget]', name, data);
  }

  C.on('page_viewed', function (p) {
    send('page', { type: p.type, path: p.path, store: C.store.domain });
  });

  C.on('product_viewed', function (p) {
    send('product', { id: p.productId, price: p.price, currency: p.currency });
  });

  C.on('product_added_to_cart', function (p) {
    send('add', { id: p.productId, quantity: p.quantity });
  });

  C.on('checkout_started', function (cart) {
    send('checkout', { items: cart.itemCount, total: cart.subtotal, currency: cart.currency });
  });
})();
```

## Rutas excluidas

Estas rutas nunca llevan scripts de apps, y `window.Commercy` no existe en ellas:

`/checkout` (incluye la confirmación de la compra), `/cuenta`, `/login`, `/registro`, `/auth`, `/forgot-password`, `/reset-password`, `/arrepentimiento`, `/preview` y las rutas del Hub de pago (`/confirm`, `/hub`) y las rutas internas de desarrollo del editor.

Si el visitante navega desde una página con tu script hacia una ruta excluida, **recargamos el documento** para que tu script no siga vivo ahí. Tampoco cargamos nada en la vista previa del editor ni en el borrador de la tienda.

## Política de comportamiento

Con un script en la tienda tenés acceso a datos sensibles del visitante. Por eso:

* **Prohibido** leer campos de formularios, y leer o escribir el `localStorage` o las cookies de Commercy (`auth-storage`, `cart-id`).
* **Prohibido** leer o modificar el DOM de las rutas excluidas.
* **Prohibido** pedir credenciales o datos de pago.
* **Obligatorio** envolver tu código en un closure (como en el ejemplo), no depender de jQuery ni del JavaScript del tema de la tienda, y no usar `document.write`.
* Cargá lo mínimo: la revisión mide el Lighthouse mobile de una tienda y tu script no puede empeorarlo en más de 10 puntos.

Incumplir esta política es causal de suspensión de la app.

## Cómo probar

Mientras tu app está en borrador, los scripts sólo corren en **tu tienda de desarrollo** y en tus **tiendas demo**.

1. Declará el script y pedí el scope `storefront_scripts`.
2. Pedile a soporte que habilite los scripts en tu app, para poder probarlos en borrador. Pasale el id de tu app.
3. Instalá la app en tu tienda de desarrollo aceptando el scope.
4. Abrí la tienda, interactuá con la página (un scroll alcanza) y mirá la consola.

En borrador podés usar un `src` en `http://localhost` (también `127.0.0.1` y `::1`), que no necesita estar en el dominio de tu app. Para enviar la app a revisión el `src` tiene que ser `https` y público.

Después del envío, la revisión mira que el hash coincida, que el archivo se sirva sin redirects y con CORS desde una URL versionada, que la carga sea diferida, que sea un script necesario y que no empeore el rendimiento. Ver [Revisión y publicación](https://docs.commercy.com.ar/revision.md).

## Errores de validación

Cuando algo de la declaración no es válido, la API responde `400` y `details` trae un item por cada problema, con `{ field, reason }`. El `field` indica qué script (por ejemplo `storefrontScripts[0].src`).

| `reason` | Qué pasó | Qué hacer |
| - | - | - |
| `duplicate_handle` | Dos scripts con el mismo `handle`. | Usá un `handle` distinto para cada uno. |
| `invalid_url` | La `src` no es una URL válida. | Revisá la URL. |
| `not_https` | La `src` no es `https`. | Servila por `https`. |
| `credentials` | La URL lleva usuario o contraseña. | Sacalos de la URL. |
| `port` | La URL usa un puerto menor a 1024. | Usá el puerto estándar de `https`. |
| `private_ip` | La URL apunta a una IP privada. | Usá un dominio público. |
| `localhost` | La `src` apunta a `localhost` o a un host interno donde no se permite. | En borrador sólo se acepta `http://localhost`; para enviar a revisión o publicar usá `https` público. |
| `commercy_domain` | La `src` es un dominio de Commercy. | Alojá el archivo en tu dominio. |
| `store_domain` | La `src` es el dominio de una tienda. | Alojá el archivo en tu dominio. |
| `app_url_required` | Tu app no tiene una `appUrl` con dominio propio. | Cargá la URL de tu app en el portal. |
| `not_app_domain` | La `src` no está en el dominio de tu `appUrl`. | Usá el mismo dominio (o un subdominio) que tu `appUrl`. |
| `consent_unsupported` | El `consent` no es `necessary`. | Sólo se aceptan scripts necesarios en esta versión. |
| `scope_required:storefront_scripts` | Declaraste scripts pero no pediste el scope. Aparece al enviar a revisión. | Agregá `storefront_scripts` en la pestaña **Scopes**. |

## Ejemplos

Dos scripts de ejemplo, listos para copiar y adaptar:

* [`whatsapp-widget-script`](https://github.com/KonixDev/commercy-app-examples/tree/main/examples/whatsapp-widget-script): un widget de WhatsApp para la tienda.
* [`installments-banner-script`](https://github.com/KonixDev/commercy-app-examples/tree/main/examples/installments-banner-script): un banner de cuotas para la tienda.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.