Skip to content
Skip the menu

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.

View as Markdown

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 seesA page of GameCoin, in the browser or an in-app web viewThe same shop, in a window over your game, or inside an element of your pageYour own screens
Your codeA link with a player tokenOne script and GameCoinShop.open(…)GameCoin.init(…) and your own buttons
Good forPhones, apps, engines without a good checkoutWeb games that want the shop without leaving the pageGames that draw their own shop
NeedsShop turned onShop turned on, allowed origins, a player tokenA 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:

  1. Your page calls GameCoinShop.open({ gameSlug, token }).
  2. The widget opens a dialog with an iframe on /shop/<game slug>/embed of your GameCoin host.
  3. 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.
  4. 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:

WhatWhere
A shop turned onThe 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 tokenMade 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.

HTML
<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

index.html
<!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.

JavaScript
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.

JavaScript
const stand = GameCoinShop.mount("#shop-stand", { gameSlug: "my-game", token, theme: "light" });
// when the player leaves the screen:
stand.destroy();

Options

OptionTypeDefaultMeaning
gameSlugstringrequiredThe slug of your game (lowercase letters, digits and dashes).
tokenstringrequiredA 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.
baseUrlstringThe origin of the <script src>Where GameCoin lives. https only (http for localhost). Only the origin counts.
titlestring« Shop » in the languageThe accessible name of the dialog and of the iframe.
loadTimeoutMsnumber15000How 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.

EventObjectWhen
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.
JavaScript
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.

LangageLanguage
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 '{}'

The 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:

index.html
<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.js for you, once, from the same place as gamecoin.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, title and loadTimeoutMs (the table of Options, without token and baseUrl), 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's change event with reason: "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.

EnvironmentListThe shop can be framed by
liveNot emptyExactly these origins
liveEmptyNobody: the widget shows « The shop could not be loaded ».
testNot emptyExactly these origins
testEmptyhttp://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 a Referer.
  • 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 allow permission.
  • 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+Tab on 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: reduce there 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 seeWhyWhat to do
« The shop could not be loaded » and error with LOAD_FAILEDThe 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_EXPIREDThe 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_OPTIONSA wrong option, for example a gameSlug with capitals.Read the message: it names the option.
The payment window does not openThe 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 purchaseThe 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.

  1. 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.
  2. Add the global variables shopOpen, shopPurchased (booleans) and lastOrderId (text).
  3. 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.
  4. In your events, react to shopPurchased: send lastOrderId to your server, read the order there, refresh the wallet, then set shopPurchased back to false.
GDevelop: JavaScript code event
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.

  1. 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.
  2. Add a Function called ShopPurchased with one parameter, OrderId. Put your own events in it: send the order to your server, read it there, refresh the wallet.
  3. On « Button » is clicked, run the Browser action « Execute JavaScript » with the code below. Replace TOKEN by the variable PlayerToken (put it in the expression of the action, between quotes).
Construct: Browser, Execute JavaScript
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 test and live apart: the token says which one the shop uses.
  • Handle SESSION_EXPIRED by 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.