Browser SDK
Shop widget
Put the GameCoin shop inside your web game with one script, in a modal or in a page element, and hear about the balance and the purchases from your code.
On this page
Overview
gamecoin-widget.js shows the hosted shop inside your web game, in an iframe, so the player never leaves the page. It is one file, with no dependency and no build step (ES2017). It adds a global GameCoinShop.
| Hosted shop (a link) | Shop widget (this page) | Browser SDK | |
|---|---|---|---|
| What the player sees | A page of GameCoin, in the browser or an in-app web view | The same shop, in a window over your game, or inside an element of your page | Your own screens |
| Your code | A link with a player token | One script and GameCoinShop.open(…) | GameCoin.init(…) and your own buttons |
| Good for | Phones, apps, engines without a good checkout | Web games that want the shop without leaving the page | Games that draw their own shop |
| Needs | Shop turned on | Shop turned on, allowed origins, a player token | A publishable key |
The widget does not replace the SDK: they can live together. The SDK reads and spends; the widget is the screen where the player buys packs, types a code and reads their balance. The SDK can also open the widget for you, as its player, with gc.openShop().
How it works:
- Your page calls
GameCoinShop.open({ gameSlug, token }). - The widget opens a dialog with an iframe on
/shop/<game slug>/embedof your GameCoin host. - GameCoin lets only the pages of your allowed origins show that iframe (
Content-Security-Policy: frame-ancestors, on this one path), and refuses everyone else. - The player buys. The payment page cannot be shown in an iframe, so it opens in its own window; the shop follows the order by itself and tells your page when it ends.
Before you start
You need three things:
| What | Where |
|---|---|
| A shop turned on | The dashboard (in French for now), your game, « Boutique », then « Activer la boutique ». See The hosted shop. |
| The allowed origins of your game | « Réglages », « Origines autorisées ». One origin per line, for example https://play.example.com. See Allowed origins. |
| A player token | Made by your server for the player who is playing. See Get the player token. Not needed if you open the shop with the browser SDK: see openShop(). |
Install
Load the file with a classic script tag, from your GameCoin host. The widget takes the host it was loaded from as the place to call, so you pass nothing else.
<script src="https://gamecoin.apilow.com/sdk/gamecoin-widget.js"></script>GameCoinShop.version is the version of the file ("1.0.0"). The file is also a CommonJS module (module.exports is a function that builds a widget from { window, document, scriptOrigin }), which is what its tests use. If the script is not loaded from a <script src>, pass baseUrl to every call.
You can download the file and ship it with your game. The shop itself always comes from your GameCoin host, so the page of your game must be allowed to frame it: if you use a Content-Security-Policy, add the host to frame-src. The widget builds its dialog with the CSSOM only (no <style> tag, no style attribute), so it needs nothing in style-src.
A minimal page
<!doctype html>
<html lang="en">
<body>
<button id="shop">Open the shop</button>
<script src="https://gamecoin.apilow.com/sdk/gamecoin-widget.js"></script>
<script>
document.getElementById("shop").addEventListener("click", async () => {
// Your server makes the player token (see "Get the player token").
const { token } = await (await fetch("/api/shop-token", { method: "POST" })).json();
GameCoinShop.open({ gameSlug: "my-game", token, locale: "en" });
});
GameCoinShop.on("purchased", ({ orderId, status }) => {
// Never trust this message alone: ask YOUR server to read the order, then refresh the wallet.
fetch("/api/orders/" + encodeURIComponent(orderId) + "/check", { method: "POST" });
});
</script>
</body>
</html>Call open from a click, a key press or a tap: the player's browser then lets the shop open the payment window without blocking it.
Open a modal
GameCoinShop.open(options) → { close() }It opens a dialog over your page and returns a handle; GameCoinShop.close() closes it too. Only one modal at a time: opening a second one closes the first.
const shop = GameCoinShop.open({ gameSlug: "my-game", token, locale: "nl", theme: "dark" });
// later, for example when the level starts:
shop.close();GameCoinShop.mount(element, options) → { destroy() }element is an element or a CSS selector. The shop fills the width of the element; its iframe grows with the content, so the page scrolls, not the iframe. There is no dialog, no scroll lock and no close button. Pick theme to match the page behind it. Several mounts can live together; destroy() removes one.
const stand = GameCoinShop.mount("#shop-stand", { gameSlug: "my-game", token, theme: "light" });
// when the player leaves the screen:
stand.destroy();Options
| Option | Type | Default | Meaning |
|---|---|---|---|
gameSlug | string | required | The slug of your game (lowercase letters, digits and dashes). |
token | string | required | A short-lived player token, from your server or the client API. |
locale | "fr", "en" or "nl" | The lang of your page, then the browser language, then "en" | The language of the shop and of the widget's own texts. |
theme | "light" or "dark" | "light" | The colors of the shop. The background is transparent; the modal draws its own. |
baseUrl | string | The origin of the <script src> | Where GameCoin lives. https only (http for localhost). Only the origin counts. |
title | string | « Shop » in the language | The accessible name of the dialog and of the iframe. |
loadTimeoutMs | number | 15000 | How long to wait for the shop before offering « Try again ». |
A wrong option throws a GameCoinShopError (name, code) before anything is shown: INVALID_OPTIONS for a bad value, INVALID_EVENT for an unknown event name. The message never contains the token.
Events
GameCoinShop.on(event, handler) → function that removes the handler
GameCoinShop.off(event, handler)The handler receives one object. The events are the same for the modal and for mounted shops.
| Event | Object | When |
|---|---|---|
ready | { slug, env } | The shop is loaded. env is "test" or "live". Once per load. |
balance | { balances: [{ currency, paid, bonus, total }] } | On load, and after each change (a gift code, a purchase). Use it to show a number; it is not a proof. |
purchased | { orderId, status } | An order of this player ended: fulfilled, failed, canceled, expired… |
close | {} | The player closed the modal (button, Escape or a click on the backdrop), or you called close(). |
error | { code, message } | SESSION_EXPIRED: the token is missing, expired or refused, ask your server for a new one and open again. LOAD_FAILED: the shop did not answer in time. |
GameCoinShop.on("balance", ({ balances }) => {
const gems = balances.find((line) => line.currency === "gems");
if (gems) hud.setGems(gems.total);
});
GameCoinShop.on("error", async ({ code }) => {
if (code === "SESSION_EXPIRED") {
GameCoinShop.close();
const { token } = await (await fetch("/api/shop-token", { method: "POST" })).json();
GameCoinShop.open({ gameSlug: "my-game", token });
}
});Warning
purchased comes from a browser, so anyone can send it. Always read the order again on your server (GET /orders/{order}, or the order.fulfilled webhook) before you give anything in your game that depends on a purchase, such as a reward or an unlock. The coins and items of the pack are credited by GameCoin itself, whatever the event says: your server reads the wallet to know them.
Get the player token
The widget needs a player token: a token of one hour that names one player, one game and one environment (Players and tokens). Make it on your server, when the player taps the button, and hand it to the page.
curl -X POST "https://gamecoin.apilow.com/api/v1/players/ext%3Auser-42/tokens" \
-H "Authorization: Bearer $GAMECOIN_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{}'// An Express route behind your own login: whoever can call it can act as that player.
app.post("/api/shop-token", requireLogin, async (req, res) => {
const { token } = await gamecoin.players.createToken(ext(req.user.id));
res.json({ token });
});token = gamecoin.players.create_token(ext("user-42"))
# return token.token to the page$token = $gamecoin->players->createToken(PlayerRef::ext('user-42'));
// return $token->token to the pagetoken, err := client.Players.CreateToken(ctx, gamecoin.Ext("user-42"))
// return token.Token to the pagevar token = await gamecoin.Players.CreateTokenAsync(PlayerRef.Ext("user-42"));
// return token.Token to the pageThe token carries the environment: a test token opens the shop in test (a banner says so, and the payments are simulated), a live token in live. It must be for the same game as gameSlug.
A game without a server: openShop()
The browser SDK holds the player token (it creates an anonymous player, remembers it and renews its token), so a game without a server does not make a token at all. It asks the SDK to open the shop:
<script src="https://gamecoin.apilow.com/sdk/gamecoin.js"></script>
<script>
(async () => {
const gc = await GameCoin.init({ publishableKey: "gc_pk_test_…" });
document.getElementById("shop").onclick = () => gc.openShop({ gameSlug: "my-game", locale: "en", theme: "dark" });
gc.on("change", ({ wallet }) => draw(wallet)); // after a purchase or a gift code, the SDK has already read the wallet
gc.on("shop:close", () => resumeGame());
})();
</script>gc.openShop(options) does what the rest of this page describes, with the player of the SDK:
- it loads
gamecoin-widget.jsfor you, once, from the same place asgamecoin.js(you can leave the<script>of the widget out of your page); - it gives the widget the player's token, and renews it first when it has less than 5 minutes left. The token goes to the widget and nowhere else: your code never sees it, and it is never in a log, an event or an error;
- it takes
gameSlug,locale,theme,titleandloadTimeoutMs(the table of Options, withouttokenandbaseUrl), and returns{ close() }; - it relays the events of the shop on the client:
"shop:purchased"({ orderId, status }),"shop:close"and"shop:error"({ code, message }). After a purchase, or when the shop shows a balance that your game does not know yet (a gift code), it reads the wallet again and fires the SDK'schangeevent withreason: "shop".
With a playerToken from your own server, openShop() works the same way and uses that token. See Open the shop for the details, the errors and the events. The warning above stays true: read the order on your server before you rely on "shop:purchased".
openShop() opens the modal only. To put the shop inside an element of your page (mount), call GameCoinShop.mount with a token that you hold, made by your server as above.
Allowed origins
Only the web pages you list may show the shop in an iframe. They are the allowed origins of the game, the same list that opens the client API to a browser. An origin is scheme://host[:port] with no path, for example https://play.example.com or https://my-game.itch.zone. Every origin is written in full: there is no wildcard.
| Environment | List | The shop can be framed by |
|---|---|---|
live | Not empty | Exactly these origins |
live | Empty | Nobody: the widget shows « The shop could not be loaded ». |
test | Not empty | Exactly these origins |
test | Empty | http://localhost and http://127.0.0.1, on any port |
The list is mandatory in live. Write the origin of the page that holds your game, which is not always the one of your website: a game on itch.io runs on an itch.zone address, a game in a portal runs on the portal's address. Open the game, and read location.origin in the console.
Only https is accepted, and http only for the local machine (http://localhost:5173 for your development server). Anything else you type is refused by the settings and ignored by the shop.
Under the hood, the page /shop/<game slug>/embed answers Content-Security-Policy: frame-ancestors <your origins>, and only that path: every other page of GameCoin answers frame-ancestors 'none' and cannot be framed at all. The shop also sends its messages only to the origin of its parent, and only if that origin is in your list.
Security
- The token is short. It lasts one hour, names one player, and is only good for the shop and the client API of that game. Make it when the player opens the shop, not in advance, and never log it. The widget never writes it to the console, to an event or to an error message.
- The token goes to GameCoin and nowhere else. It is in the address of the iframe (your page made it, so your page knows it), and the iframe is loaded with
referrerpolicy="origin": GameCoin learns the origin of your page, never its path or its query. The shop swaps the token for a signed ticket that lasts one hour and works only for that player and that game, and its pages never send the token in aReferer. - The iframe is sandboxed. It can run scripts and forms, and open the payment window; it can not navigate your page, ask for the camera, or open alert boxes. It has no
allowpermission. - Messages are checked on both sides. Your page only believes a message that comes from its own iframe (
event.source) and from the origin of your GameCoin host, in one of the shapes of the table above; anything else is dropped. The shop sends nothing to a page that is not in your allowed origins, and never uses*as a target. - Read the order on your server before you rely on
purchased: see the warning above. - Live needs a verified studio, like the rest of the live environment.
Accessibility
The modal follows the usual pattern of a dialog:
- It has the role
dialog,aria-modal="true"and an accessible name (title, « Shop » in the language). The iframe has a title too. - The focus goes to the close button when it opens, stays in the dialog (the rest of your page is made inert, so a screen reader and the Tab key cannot reach it), and returns to the element that had it when the dialog closes.
- Escape closes it, whether the focus is in your page or in the shop.
Shift+Tabon the close button goes into the shop, not out of the dialog. - The page behind cannot scroll while the dialog is open, and the scrollbar does not make your layout jump. The previous values are restored exactly.
- The close button is at least 44 pixels wide and tall, and named in the language (« Fermer », « Close », « Sluiten »).
- With
prefers-reduced-motion: reducethere is no animation. - The shop inside is built for WCAG 2.1 AA: labels on every field, errors announced, visible focus, and it is made for phones first.
When the shop does not open
| What you see | Why | What to do |
|---|---|---|
« The shop could not be loaded » and error with LOAD_FAILED | The origin of your page is not in the allowed origins, the shop is turned off, the game slug is wrong, or the network is down. | Check « Réglages » and « Boutique »; open https://gamecoin.apilow.com/shop/<game slug> in a tab. The widget offers « Try again ». |
« This shop session has expired… » and error with SESSION_EXPIRED | The token is missing, expired, made for another game, a live token for a studio that is not verified, or the player is blocked. | Ask your server for a new token and open again. |
GameCoinShopError with INVALID_OPTIONS | A wrong option, for example a gameSlug with capitals. | Read the message: it names the option. |
| The payment window does not open | The browser blocked a pop-up not opened from a click. | Open the shop from a click. The shop tells the player to allow pop-ups. |
| Nothing happens after the purchase | The purchased event ends the order; the coins are in the wallet. | Read the order and the wallet on your server and refresh your screen. |
GDevelop
Warning
These two recipes were written from the documentation of the engines, not run in them. Treat them as a starting point and test them in a web export. If a call has another name in your version, the idea is the same: load the script, give it a token, listen to the events.
The widget is for web builds (HTML5 export): the origin of the page is the address where you host the export, and it must be in the allowed origins. A native app (Android, iOS, desktop) has no web origin: use the hosted shop link there.
- Make your server return a player token (see Get the player token). Ask for it with the action « Send a request to a web page » and keep the answer in a global variable called
playerToken. - Add the global variables
shopOpen,shopPurchased(booleans) andlastOrderId(text). - Under the condition « Button clicked », add a JavaScript code event with the code below. It adds the script at run time when it is not there yet: the widget reads its own address from the
<script>element, so nothing else is needed. - In your events, react to
shopPurchased: sendlastOrderIdto your server, read the order there, refresh the wallet, then setshopPurchasedback tofalse.
const variables = runtimeScene.getVariables();
const token = variables.get("playerToken").getAsString();
function openShop() {
if (!window.gcShopWired) {
window.gcShopWired = true; // add the listeners once, however many times the shop is opened
GameCoinShop.on("purchased", ({ orderId }) => {
variables.get("lastOrderId").setString(orderId);
variables.get("shopPurchased").setBoolean(true);
});
GameCoinShop.on("close", () => variables.get("shopOpen").setBoolean(false));
}
GameCoinShop.open({ gameSlug: "my-game", token, locale: "en" });
variables.get("shopOpen").setBoolean(true);
}
if (window.GameCoinShop) {
openShop();
} else {
const script = document.createElement("script");
script.src = "https://YOUR-GAMECOIN-HOST/sdk/gamecoin-widget.js";
script.onload = openShop;
document.head.appendChild(script);
}Pause your game while shopOpen is true if you want to: the modal makes the page behind it inert, but the game keeps running.
Construct
Warning
Written from the documentation of the engine and not run in Construct. Test it in a web export before you rely on it.
The widget is for web exports (HTML5 website, itch.io, a portal): the address where the export runs must be in the allowed origins. For a native wrapper, use the hosted shop link instead.
- Make your server return a player token and ask for it with the AJAX object (« Request URL »). Keep the answer in a global variable
PlayerToken. - Add a Function called
ShopPurchasedwith one parameter,OrderId. Put your own events in it: send the order to your server, read it there, refresh the wallet. - On « Button » is clicked, run the Browser action « Execute JavaScript » with the code below. Replace
TOKENby the variablePlayerToken(put it in the expression of the action, between quotes).
const token = "TOKEN";
function openShop() {
if (!window.gcShopWired) {
window.gcShopWired = true; // add the listener once
GameCoinShop.on("purchased", ({ orderId }) => c3_callFunction("ShopPurchased", [orderId]));
}
GameCoinShop.open({ gameSlug: "my-game", token, locale: "en" });
}
if (window.GameCoinShop) {
openShop();
} else {
const script = document.createElement("script");
script.src = "https://YOUR-GAMECOIN-HOST/sdk/gamecoin-widget.js";
script.onload = openShop;
document.head.appendChild(script);
}c3_callFunction is the call that Construct gives to JavaScript to run one of your Functions.
Good practice
- Open the shop from a click or a tap, and make the token at that moment.
- Keep
testandliveapart: the token says which one the shop uses. - Handle
SESSION_EXPIREDby asking for a new token. Do not keep a token in storage for more than an hour. - Read orders and wallets on your server. The widget tells your page that something happened; your server knows what.
- Pause or dim your game while the modal is open: the game behind it is made inert, but it keeps running.