HystoreWiki

6 min de lecturaVer en Markdown

Apps aisladas

Una app es una pequeña página con JavaScript de verdad (ruleta, calculadora de precio, quiz, mini juego, animación) que el tema coloca en cualquier lugar de la tienda. Corre aislada, en un marco en otro dominio, y habla con la tienda solo por un puente con reglas: lee el catálogo público, pone productos en el carrito y cambia de página. Nunca llega a precios, pagos, cuentas ni datos de otros visitantes.

Cómo funciona

  • La app vive en apps/<nombre>/ en el tema, con index.html (obligatorio), scripts, estilos y un app.json opcional.
  • El tema la coloca en una página con <hystore-app data-app="<nombre>"></hystore-app>.
  • La tienda abre la app en un marco en hystoreapps.cloud, con una política que bloquea la red (fetch, XHR, WebSocket), cookies, almacenamiento, formularios, ventanas nuevas y la navegación de la página de la tienda.
  • La app llama a window.hystore, el puente, cargado por <script src="/sdk/v1.js">.

Paso a paso: una ruleta de productos

Vamos a hacer una ruleta que sortea un producto de la categoría kits y deja ponerlo en el carrito. Son cuatro archivos y una línea en el tema.

1. Crea la carpeta de la app

En el editor del panel, haz clic en el + de la carpeta apps y ponle el nombre roleta: crea index.html, app.js y app.json de una vez. Con la herramienta hystore o la IA, crea los archivos de abajo en apps/roleta/.

apps/roleta/index.html   la página (obligatoria)
apps/roleta/app.js       el script
apps/roleta/app.css      el estilo
apps/roleta/app.json     nombre, altura y permisos

2. Declara la app

apps/roleta/app.jsonJSON
{
  "name": "Ruleta de kits",
  "description": "Sortea un kit de la tienda",
  "height": 460,
  "permissions": ["customer"]
}
  • height: la altura del marco, de 40 a 2000 píxeles (320 si falta).
  • name: lo leen los lectores de pantalla.
  • permissions: solo existe customer, que deja a la app saber el nick y la skin de quien inició sesión. Sin él, hystore.customer() se rechaza.

3. Escribe la página

apps/roleta/index.htmlHTML
<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="stylesheet" href="app.css">
</head>
<body>
  <p id="hello" class="hello">Gira y descubre tu kit</p>
  <div class="stage">
    <div class="pointer"></div>
    <div id="wheel" class="wheel"></div>
  </div>
  <button id="spin" class="spin" type="button" disabled>Girar</button>
  <div id="result" class="result" aria-live="polite"></div>

  <script src="/sdk/v1.js"></script>
  <script src="app.js"></script>
</body>
</html>

/sdk/v1.js es el puente. Los otros archivos son de la propia app, con dirección relativa (app.css, app.js).

4. Dale estilo

apps/roleta/app.cssCSS
* { box-sizing: border-box; }
body { margin: 0; padding: 16px; font: 15px/1.5 system-ui, sans-serif; color: #e8eef8; background: transparent; text-align: center; }
html.light body { color: #0b1526; }
.hello { margin: 0 0 12px; font-weight: 700; }
.stage { position: relative; width: 260px; height: 260px; margin: 0 auto 16px; }
.wheel { width: 100%; height: 100%; border-radius: 50%; border: 6px solid #f5b544; transition: transform 4s cubic-bezier(.17, .67, .12, .99); }
.pointer { position: absolute; top: -6px; left: 50%; z-index: 1; transform: translateX(-50%); border: 12px solid transparent; border-top: 22px solid #f5b544; }
.spin { padding: 10px 28px; border: 0; border-radius: 12px; background: #0ea5e9; color: #fff; font-weight: 700; cursor: pointer; }
.spin:disabled { opacity: .5; cursor: default; }
.result { min-height: 64px; margin-top: 14px; }
.result button { margin-left: 8px; padding: 6px 14px; border: 0; border-radius: 10px; background: #f5b544; color: #1c1305; font-weight: 700; cursor: pointer; }

5. Escribe el script

apps/roleta/app.jsJavaScript
(function () {
  var wheel = document.getElementById('wheel');
  var spin = document.getElementById('spin');
  var result = document.getElementById('result');
  var products = [];
  var turns = 0;

  // El modo de color y el idioma de la tienda.
  hystore.context().then(function (ctx) {
    document.documentElement.classList.toggle('light', ctx.colorMode === 'light');
  });

  // El nick de quien inició sesión (necesita "customer" en app.json).
  hystore.customer().then(function (me) {
    if (me.loggedIn) document.getElementById('hello').textContent = '¡Suerte, ' + me.nick + '!';
  }).catch(function () { /* sin el permiso: queda el texto estándar */ });

  // Los productos públicos de la categoría "kits".
  hystore.products({ category: 'kits', limit: 8 }).then(function (list) {
    products = list.filter(function (p) { return !p.soldOut; });
    if (!products.length) {
      result.textContent = 'Ningún kit disponible ahora.';
      return;
    }
    paint();
    spin.disabled = false;
  });

  function paint() {
    var slice = 360 / products.length;
    var colors = ['#0ea5e9', '#1d4ed8'];
    var stops = products.map(function (p, i) {
      return colors[i % 2] + ' ' + i * slice + 'deg ' + (i + 1) * slice + 'deg';
    });
    wheel.style.background = 'conic-gradient(' + stops.join(', ') + ')';
  }

  spin.addEventListener('click', function () {
    var i = Math.floor(Math.random() * products.length);
    var slice = 360 / products.length;
    turns += 5;
    wheel.style.transform = 'rotate(' + (turns * 360 - (i * slice + slice / 2)) + 'deg)';
    spin.disabled = true;
    setTimeout(function () {
      show(products[i]);
      spin.disabled = false;
    }, 4200);
  });

  function show(product) {
    result.textContent = '';
    var text = document.createElement('p');
    text.textContent = 'Salió: ' + product.name + ' por ' + product.priceText;
    var buy = document.createElement('button');
    buy.type = 'button';
    buy.textContent = 'Poner en el carrito';
    buy.addEventListener('click', function () {
      hystore.addToCart(product.id, 1).then(function (answer) {
        if (answer === 'added') hystore.open('cart');
        else if (answer === 'sold_out') buy.textContent = 'Agotado';
        // 'login': la tienda ya le pidió a la persona que inicie sesión.
      });
    });
    text.appendChild(buy);
    result.appendChild(text);
  }
})();

6. Coloca la app en una página

En una sección o página del tema, donde la app debe aparecer:

sections/roleta.liquidLiquid
<section class="roleta">
  <h2>{{ section.settings.title }}</h2>
  <hystore-app data-app="roleta"></hystore-app>
</section>
{% schema %}
{
  "name": "Ruleta de kits",
  "settings": [
    { "id": "title", "type": "text", "label": "Título", "default": "Prueba tu suerte" }
  ]
}
{% endschema %}

Después agrega la sección en Personalizar tema > Secciones de la página, o en templates/home.json.

7. Mira la vista previa y publica

  1. Guarda. La vista previa del editor (y el enlace de vista previa) muestran la app con los archivos del borrador.
  2. Prueba girar y poner en el carrito.
  3. Publica el tema. La tienda pasa a usar los archivos de la versión publicada.

El puente: window.hystore

Todas las funciones devuelven una Promise. Si se rechaza, falla con un error cuyo mensaje es el motivo (mira Errores).

Función Qué hace Devuelve
hystore.context() idioma, modo de color y tienda { locale, colorMode, store: { name, currency } }
hystore.products(opciones) productos activos, solo lo que ve un visitante lista de productos
hystore.categories(opciones) categorías activas lista de categorías
hystore.addToCart(id, cantidad) pone el producto en el carrito (cantidad de 1 a 99) 'added', 'login' o 'sold_out'
hystore.navigate(ruta) abre una página de la tienda, como /loja o /noticias true
hystore.open(lugar) abre 'cart', 'login', 'account' o 'search' true
hystore.customer() quién está mirando (con customer en app.json) { loggedIn, nick, skinUrl }
hystore.resize(altura) cambia la altura del marco (40 a 4000) la altura aplicada
hystore.autosize() sigue sola la altura de la página de la app nada

Opciones de products y categories

Opción Tipo Ejemplo
category slug de la categoría 'kits'
server id del servidor 'survival'
query texto de búsqueda (hasta 80 caracteres) 'espada'
limit 1 a 48 (12 si falta) 6

categories solo acepta server.

Un producto

JSON
{
  "id": "0f558e5e-636a-4e5a-9739-24c711463ebf",
  "name": "Kit Guerrero",
  "slug": "kit-guerrero",
  "url": "/loja/survival/kits/produto/kit-guerrero",
  "price": 19.9,
  "priceText": "US$ 19,90",
  "originalPrice": 29.9,
  "originalPriceText": "US$ 29,90",
  "onSale": true,
  "image": "https://...",
  "shortDescription": "Espada, armadura y 64 manzanas",
  "durationDays": null,
  "minQuantity": 1,
  "soldOut": false,
  "digital": false,
  "categorySlug": "kits",
  "serverId": "survival"
}

El precio es solo para mostrar: el carrito usa el precio de la tienda, y la app manda solo el id del producto. Una categoría trae id, name, slug, url, description, image y productCount.

Errores

Mensaje Cuándo
bad_payload argumento equivocado (un id que no es de producto, cantidad fuera de 1 a 99, una ruta que no es de la tienda)
permission customer() sin "customer" en app.json
rate_limited demasiadas llamadas seguidas; espera un poco
failed la tienda no pudo responder ahora
hystore: open this app inside the store la app se abrió fuera de la tienda (directo en su dirección)
hystore: no answer la tienda no respondió en 10 segundos

Reglas y límites

  • Carga solo los archivos de la propia app y /sdk/v1.js. Scripts y estilos de otros sitios, <iframe>, <object>, <embed> y <form> se rechazan al guardar, con la línea.
  • Imágenes: imágenes data: o las imágenes del tema por su dirección completa (https://theme-assets.hystore.com.br/..., como informan list_assets del MCP y GET /v1/theme/assets).
  • Tamaño: 10 apps por tema, 20 archivos y 512 KB por app. Nombres en letras minúsculas, números, - y _; archivos .html, .js, .css o .json.
  • Cada mensaje al puente tiene hasta 4096 caracteres. Poner en el carrito: hasta 5 seguidas, después una cada 2 segundos. Cambiar de página y abrir lugares: 2 seguidas, después una cada 2 segundos. Arriba de eso la respuesta es rate_limited, y una avalancha de mensajes se ignora.
  • La app nunca sabe el correo, el saldo, los pedidos ni el carrito de nadie.

Probar y corregir

  • La app quedó en blanco: abre las herramientas del navegador en la vista previa, elige el marco hystoreapps.cloud en la consola y mira el error. Un archivo bloqueado por la política (un script de afuera, por ejemplo) aparece ahí.
  • "open this app inside the store": abriste la dirección de la app directo. Solo habla con la tienda dentro del marco.
  • La altura no alcanza: ajusta height en app.json o llama a hystore.autosize() al inicio del script.
  • Lo cambié y no aparece: la tienda en el aire usa la versión publicada. Mira la vista previa (que usa el borrador) o publica.

Ideas

  • Calculadora de VIP: el jugador elige los días y ve el kit correcto, con products({ category: 'vips' }).
  • Un quiz "qué kit va contigo", que termina en addToCart.
  • Un contador de meta animado, que lee context() para el modo de color.
  • Un mini juego de clics con el nick del jugador (customer()).