HystoreWiki

5 min de leituraVer em Markdown

Apps isolados

Um app é uma pequena página com JavaScript de verdade (roleta, calculadora de preço, quiz, mini jogo, animação) que o tema coloca em qualquer lugar da loja. Ele roda isolado, num quadro em outro domínio, e conversa com a loja só por uma ponte com regras: lê o catálogo público, põe produto no carrinho e troca de página. Nunca chega em preço, pagamento, conta ou dados de outros visitantes.

Como funciona

  • O app mora em apps/<nome>/ no tema, com index.html (obrigatório), scripts, estilos e um app.json opcional.
  • O tema o coloca numa página com <hystore-app data-app="<nome>"></hystore-app>.
  • A loja abre o app num quadro em hystoreapps.cloud, com uma política que bloqueia rede (fetch, XHR, WebSocket), cookies, armazenamento, formulários, janelas novas e navegação da página da loja.
  • O app chama window.hystore, a ponte, carregada por <script src="/sdk/v1.js">.

Passo a passo: uma roleta de produtos

Vamos fazer uma roleta que sorteia um produto da categoria kits e deixa pôr no carrinho. São quatro arquivos e uma linha no tema.

1. Crie a pasta do app

No editor do painel, clique no + da pasta apps e dê o nome roleta: ele cria index.html, app.js e app.json de uma vez. Com a ferramenta hystore ou a IA, crie os arquivos abaixo em apps/roleta/.

apps/roleta/index.html   a página (obrigatória)
apps/roleta/app.js       o script
apps/roleta/app.css      o estilo
apps/roleta/app.json     nome, altura e permissões

2. Declare o app

apps/roleta/app.jsonJSON
{
  "name": "Roleta de kits",
  "description": "Sorteia um kit da loja",
  "height": 460,
  "permissions": ["customer"]
}
  • height: a altura do quadro, de 40 a 2000 pixels (320 se faltar).
  • name: lido por leitores de tela.
  • permissions: só existe customer, que deixa o app saber o nick e a skin de quem está logado. Sem ela, hystore.customer() é recusado.

3. Escreva a página

apps/roleta/index.htmlHTML
<!doctype html>
<html lang="pt-BR">
<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">Gire e descubra o seu 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 é a ponte. Os outros arquivos são do próprio app, com endereço relativo (app.css, app.js).

4. Dê o 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. Escreva o 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;

  // Cor e língua da loja.
  hystore.context().then(function (ctx) {
    document.documentElement.classList.toggle('light', ctx.colorMode === 'light');
  });

  // O nick de quem está logado (precisa de "customer" no app.json).
  hystore.customer().then(function (me) {
    if (me.loggedIn) document.getElementById('hello').textContent = 'Boa sorte, ' + me.nick + '!';
  }).catch(function () { /* sem a permissão: fica o texto padrão */ });

  // Os produtos públicos da categoria "kits".
  hystore.products({ category: 'kits', limit: 8 }).then(function (list) {
    products = list.filter(function (p) { return !p.soldOut; });
    if (!products.length) {
      result.textContent = 'Nenhum kit disponível agora.';
      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 = 'Saiu: ' + product.name + ' por ' + product.priceText;
    var buy = document.createElement('button');
    buy.type = 'button';
    buy.textContent = 'Pôr no carrinho';
    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 = 'Esgotado';
        // 'login': a loja já pediu para a pessoa entrar.
      });
    });
    text.appendChild(buy);
    result.appendChild(text);
  }
})();

6. Coloque o app numa página

Numa seção ou página do tema, onde o app deve aparecer:

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

Depois adicione a seção em Personalizar tema > Seções da página, ou no templates/home.json.

7. Veja na prévia e publique

  1. Salve. A prévia do editor (e o link de prévia) mostram o app com os arquivos do rascunho.
  2. Teste girar e pôr no carrinho.
  3. Publique o tema. A loja passa a usar os arquivos da versão publicada.

A ponte: window.hystore

Todas as funções devolvem uma Promise. Em caso de recusa, ela é rejeitada com um erro cuja mensagem é o motivo (veja Erros).

Função O que faz Devolve
hystore.context() língua, modo de cor e loja { locale, colorMode, store: { name, currency } }
hystore.products(opções) produtos ativos, só o que um visitante vê lista de produtos
hystore.categories(opções) categorias ativas lista de categorias
hystore.addToCart(id, quantidade) põe o produto no carrinho (quantidade de 1 a 99) 'added', 'login' ou 'sold_out'
hystore.navigate(caminho) abre uma página da loja, como /loja ou /noticias true
hystore.open(lugar) abre 'cart', 'login', 'account' ou 'search' true
hystore.customer() quem está olhando (com customer no app.json) { loggedIn, nick, skinUrl }
hystore.resize(altura) muda a altura do quadro (40 a 4000) a altura aplicada
hystore.autosize() segue a altura da página do app sozinho nada

Opções de products e categories

Opção Tipo Exemplo
category slug da categoria 'kits'
server id do servidor 'survival'
query texto da busca (até 80 letras) 'espada'
limit 1 a 48 (12 se faltar) 6

categories aceita só server.

Um produto

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

O preço é só para mostrar: o carrinho usa o preço da loja, e o app manda só o id do produto. Uma categoria traz id, name, slug, url, description, image e productCount.

Erros

Mensagem Quando
bad_payload argumento errado (id que não é de produto, quantidade fora de 1 a 99, caminho que não é da loja)
permission customer() sem "customer" no app.json
rate_limited muitas chamadas seguidas; espere um pouco
failed a loja não conseguiu responder agora
hystore: open this app inside the store o app foi aberto fora da loja (direto no endereço)
hystore: no answer a loja não respondeu em 10 segundos

Regras e limites

  • Carregue só os arquivos do próprio app e o /sdk/v1.js. Scripts e estilos de outros sites, <iframe>, <object>, <embed> e <form> são recusados ao salvar, com a linha.
  • Imagens: imagens data: ou as imagens do tema pelo endereço completo (https://theme-assets.hystore.com.br/..., como list_assets do MCP e GET /v1/theme/assets informam).
  • Tamanho: 10 apps por tema, 20 arquivos e 512 KB por app. Nomes em letras minúsculas, números, - e _; arquivos .html, .js, .css ou .json.
  • Cada mensagem para a ponte tem até 4096 caracteres. Pôr no carrinho: até 5 seguidas, depois uma a cada 2 segundos. Trocar de página e abrir lugares: 2 seguidas, depois uma a cada 2 segundos. Acima disso a resposta é rate_limited, e uma enxurrada de mensagens é ignorada.
  • O app não sabe e-mail, saldo, pedidos nem o carrinho de ninguém.

Testar e corrigir

  • O app ficou em branco: abra as ferramentas do navegador na prévia, escolha o quadro hystoreapps.cloud no console e veja o erro. Um arquivo recusado pela política (um script de fora, por exemplo) aparece ali.
  • "open this app inside the store": você abriu o endereço do app direto. Ele só fala com a loja dentro do quadro.
  • A altura não cabe: ajuste height no app.json ou chame hystore.autosize() no começo do script.
  • Mudei e não aparece: a loja no ar usa a versão publicada. Veja na prévia (que usa o rascunho) ou publique.

Ideias

  • Calculadora de VIP: o jogador escolhe dias e vê o kit certo, com products({ category: 'vips' }).
  • Quiz "qual kit combina com você", terminando em addToCart.
  • Contador de meta com animação, lendo context() para o modo de cor.
  • Mini jogo de clique com o nick do jogador (customer()).