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

## Overview

`gamecoin-widget.js` shows the [hosted shop](/docs/guides/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()`](#a-game-without-a-server-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:

| What | Where |
|---|---|
| A shop turned on | The dashboard (in French for now), your game, « Boutique », then « Activer la boutique ». See [The hosted shop](/docs/guides/hosted-shop#turn-it-on). |
| The **allowed origins** of your game | « Réglages », « Origines autorisées ». One origin per line, for example `https://play.example.com`. See [Allowed origins](#allowed-origins). |
| A **player token** | Made by your server for the player who is playing. See [Get the player token](#get-the-player-token). Not needed if you open the shop with the browser SDK: see [`openShop()`](#a-game-without-a-server-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

```html title="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

```text
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();
```

## Mount the shop in your page

```text
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

| 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

```text
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. |

```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](/docs/guides/webhooks)) 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](/docs/api/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](/docs/concepts/players-and-tokens)). Make it **on your server**, when the player taps the button, and hand it to the page.

<!-- tabs:start -->
```bash tab="curl"
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 '{}'
```
```javascript tab="Node.js"
// 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 });
});
```
```python tab="Python"
token = gamecoin.players.create_token(ext("user-42"))
# return token.token to the page
```
```php tab="PHP"
$token = $gamecoin->players->createToken(PlayerRef::ext('user-42'));
// return $token->token to the page
```
```go tab="Go"
token, err := client.Players.CreateToken(ctx, gamecoin.Ext("user-42"))
// return token.Token to the page
```
```csharp tab="C#"
var token = await gamecoin.Players.CreateTokenAsync(PlayerRef.Ext("user-42"));
// return token.Token to the page
```
<!-- tabs:end -->

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](/docs/sdk/browser) 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:

```html title="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](#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](/docs/sdk/browser#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](/docs/api/client) 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 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 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](/docs/api/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](/docs/guides/hosted-shop) link there.

1. Make your server return a player token (see [Get the player token](#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`.

```javascript title="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](/docs/guides/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).

```javascript title="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.
