Isolated apps
An app is a small page with real JavaScript (a prize wheel, a price calculator, a quiz, a mini game, an animation) that the theme places anywhere in the store. It runs isolated, in a frame on another domain, and talks to the store only through a bridge with rules: it reads the public catalog, adds products to the cart and changes pages. It never reaches prices, payment, accounts or other visitors' data.
How it works
- The app lives in
apps/<name>/in the theme, withindex.html(required), scripts, styles and an optionalapp.json. - The theme places it on a page with
<hystore-app data-app="<name>"></hystore-app>. - The store opens the app in a frame on
hystoreapps.cloud, with a policy that blocks the network (fetch, XHR, WebSocket), cookies, storage, forms, new windows and navigating the store page. - The app calls
window.hystore, the bridge, loaded by<script src="/sdk/v1.js">.
Step by step: a product wheel
Let's build a wheel that draws a product from the kits category and lets the
visitor add it to the cart. Four files and one line in the theme.
1. Create the app folder
In the dashboard editor, click the + of the apps folder and name it
roleta: it creates index.html, app.js and app.json at once. With the
hystore tool or the AI, create the files below in apps/roleta/.
apps/roleta/index.html the page (required)
apps/roleta/app.js the script
apps/roleta/app.css the style
apps/roleta/app.json name, height and permissions
2. Declare the app
{
"name": "Kit wheel",
"description": "Draws a kit from the store",
"height": 460,
"permissions": ["customer"]
}
height: the frame's height, 40 to 2000 pixels (320 when missing).name: read by screen readers.permissions: onlycustomerexists, which lets the app know the nick and skin of whoever is signed in. Without it,hystore.customer()is refused.
3. Write the page
<!doctype html>
<html lang="en">
<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">Spin and find your kit</p>
<div class="stage">
<div class="pointer"></div>
<div id="wheel" class="wheel"></div>
</div>
<button id="spin" class="spin" type="button" disabled>Spin</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 is the bridge. The other files belong to the app, with relative
addresses (app.css, app.js).
4. Style it
* { 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. Write the script
(function () {
var wheel = document.getElementById('wheel');
var spin = document.getElementById('spin');
var result = document.getElementById('result');
var products = [];
var turns = 0;
// The store's colour mode and language.
hystore.context().then(function (ctx) {
document.documentElement.classList.toggle('light', ctx.colorMode === 'light');
});
// The nick of whoever is signed in (needs "customer" in app.json).
hystore.customer().then(function (me) {
if (me.loggedIn) document.getElementById('hello').textContent = 'Good luck, ' + me.nick + '!';
}).catch(function () { /* without the permission: the default text stays */ });
// The public products of the "kits" category.
hystore.products({ category: 'kits', limit: 8 }).then(function (list) {
products = list.filter(function (p) { return !p.soldOut; });
if (!products.length) {
result.textContent = 'No kit available right now.';
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 = 'You got: ' + product.name + ' for ' + product.priceText;
var buy = document.createElement('button');
buy.type = 'button';
buy.textContent = 'Add to cart';
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 = 'Sold out';
// 'login': the store already asked the person to sign in.
});
});
text.appendChild(buy);
result.appendChild(text);
}
})();
6. Place the app on a page
In a section or page of the theme, where the app should show:
<section class="roleta">
<h2>{{ section.settings.title }}</h2>
<hystore-app data-app="roleta"></hystore-app>
</section>
{% schema %}
{
"name": "Kit wheel",
"settings": [
{ "id": "title", "type": "text", "label": "Title", "default": "Try your luck" }
]
}
{% endschema %}
Then add the section in Customize theme > Sections of the page, or in
templates/home.json.
7. Look at the preview and publish
- Save. The editor's preview (and the preview link) show the app with the draft files.
- Try spinning and adding to the cart.
- Publish the theme. The store then uses the files of the published version.
The bridge: window.hystore
Every function returns a Promise. When refused, it rejects with an error whose message is the reason (see Errors).
| Function | What it does | Returns |
|---|---|---|
hystore.context() |
language, colour mode and store | { locale, colorMode, store: { name, currency } } |
hystore.products(options) |
active products, only what a visitor sees | list of products |
hystore.categories(options) |
active categories | list of categories |
hystore.addToCart(id, quantity) |
adds the product to the cart (quantity 1 to 99) | 'added', 'login' or 'sold_out' |
hystore.navigate(path) |
opens a store page, such as /loja or /noticias |
true |
hystore.open(place) |
opens 'cart', 'login', 'account' or 'search' |
true |
hystore.customer() |
who is looking (with customer in app.json) |
{ loggedIn, nick, skinUrl } |
hystore.resize(height) |
changes the frame's height (40 to 4000) | the height applied |
hystore.autosize() |
follows the height of the app's page by itself | nothing |
Options of products and categories
| Option | Type | Example |
|---|---|---|
category |
category slug | 'kits' |
server |
server id | 'survival' |
query |
search text (up to 80 characters) | 'sword' |
limit |
1 to 48 (12 when missing) | 6 |
categories only takes server.
A product
{
"id": "0f558e5e-636a-4e5a-9739-24c711463ebf",
"name": "Warrior kit",
"slug": "warrior-kit",
"url": "/loja/survival/kits/produto/warrior-kit",
"price": 19.9,
"priceText": "$19.90",
"originalPrice": 29.9,
"originalPriceText": "$29.90",
"onSale": true,
"image": "https://...",
"shortDescription": "Sword, armour and 64 apples",
"durationDays": null,
"minQuantity": 1,
"soldOut": false,
"digital": false,
"categorySlug": "kits",
"serverId": "survival"
}
The price is only for showing: the cart uses the store's price, and the app
only sends the product id. A category has id, name, slug, url,
description, image and productCount.
Errors
| Message | When |
|---|---|
bad_payload |
wrong argument (an id that is not a product, a quantity outside 1 to 99, a path outside the store) |
permission |
customer() without "customer" in app.json |
rate_limited |
too many calls in a row; wait a little |
failed |
the store could not answer right now |
hystore: open this app inside the store |
the app was opened outside the store (straight at its address) |
hystore: no answer |
the store did not answer in 10 seconds |
Rules and limits
- Load only the app's own files and
/sdk/v1.js. Scripts and styles from other sites,<iframe>,<object>,<embed>and<form>are refused when saved, with the line. - Images:
data:images or the theme's images by their full address (https://theme-assets.hystore.com.br/..., as the MCPlist_assetsandGET /v1/theme/assetsgive). - Size: 10 apps per theme, 20 files and 512 KB per app. Names in lowercase
letters, numbers,
-and_; files.html,.js,.cssor.json. - Each message to the bridge has up to 4096 characters. Adding to the cart:
up to 5 in a row, then one every 2 seconds. Changing pages and opening
places: 2 in a row, then one every 2 seconds. Above that the answer is
rate_limited, and a flood of messages is ignored. - The app never knows anyone's e-mail, balance, orders or cart.
Test and fix
- The app is blank: open the browser's developer tools on the preview,
pick the
hystoreapps.cloudframe in the console and read the error. A file blocked by the policy (an outside script, for example) shows there. - "open this app inside the store": you opened the app's address directly. It only talks to the store inside the frame.
- The height does not fit: adjust
heightinapp.jsonor callhystore.autosize()at the start of the script. - I changed it and nothing shows: the live store uses the published version. Look at the preview (which uses the draft) or publish.
Ideas
- VIP calculator: the player picks days and sees the right kit, with
products({ category: 'vips' }). - A "which kit fits you" quiz, ending in
addToCart. - An animated goal counter, reading
context()for the colour mode. - A click mini game with the player's nick (
customer()).
