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, comindex.html(obrigatório), scripts, estilos e umapp.jsonopcional. - 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
{
"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ó existecustomer, que deixa o app saber o nick e a skin de quem está logado. Sem ela,hystore.customer()é recusado.
3. Escreva a página
<!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
* { 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
(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:
<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
- Salve. A prévia do editor (e o link de prévia) mostram o app com os arquivos do rascunho.
- Teste girar e pôr no carrinho.
- 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
{
"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/..., comolist_assetsdo MCP eGET /v1/theme/assetsinformam). - Tamanho: 10 apps por tema, 20 arquivos e 512 KB por app. Nomes em letras
minúsculas, números,
-e_; arquivos.html,.js,.cssou.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.cloudno 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
heightnoapp.jsonou chamehystore.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()).
