@indiegems/gem-web-sdk 16.3.0 → 16.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,8 +21,8 @@ async function onBuyPressed(offer) {
21
21
  ```
22
22
 
23
23
  **Your game asks; the Arcade decides.** Nothing your game sends carries a URL, a price or a card
24
- field. The Arcade builds every link, reads every price, draws every purchase sheet and takes the
25
- payment itself. You get an outcome.
24
+ field. The Arcade builds every link, reads every price, opens every checkout and takes the payment
25
+ itself. You get an outcome.
26
26
 
27
27
  - [1. Two surfaces, and which one is real money](#1-two-surfaces-and-which-one-is-real-money)
28
28
  - [2. Nothing rejects](#2-nothing-rejects)
@@ -37,11 +37,11 @@ payment itself. You get an outcome.
37
37
 
38
38
  ## 1. Two surfaces, and which one is real money
39
39
 
40
- | | **`gem.arcade`** | **`gem.store`** |
41
- | ------------ | ------------------------------------------------ | ----------------------------------------------- |
42
- | What it does | app-store listings, another game, similar games | the Arcade Store's own SKUs |
43
- | Money | none | **real money**, through the Arcade's own sheet |
44
- | Who draws it | the Arcade (a new tab, a QR code, the next game) | the Arcade Store's sheet, over your paused game |
40
+ | | **`gem.arcade`** | **`gem.store`** |
41
+ | ------------ | ------------------------------------------------ | --------------------------------------------------- |
42
+ | What it does | app-store listings, another game, similar games | the Arcade Store's own SKUs |
43
+ | Money | none | **real money**, through the Arcade Store's checkout |
44
+ | Who draws it | the Arcade (a new tab, a QR code, the next game) | the checkout, over your paused and hidden game |
45
45
 
46
46
  `gem.store` is not `gem.purchases`. **`gem.purchases` spends Gems or Coins on your game's own
47
47
  items** ([`inventory-and-catalogs.md`](inventory-and-catalogs.md)). **`gem.store` sells the Arcade
@@ -221,7 +221,7 @@ Gem pack, a `grant` naming the currency and amount.
221
221
  currency, already formatted. Never parse it, compare two of them, or compute a discount from it.
222
222
  There is no numeric price.
223
223
  - **Re-read offers when your game resumes.** What is on sale can change while the player is away.
224
- - **Do not imitate the Arcade's chrome.** Your offer list is yours to style; the sheet that takes
224
+ - **Do not imitate the Arcade's chrome.** Your offer list is yours to style; the checkout that takes
225
225
  the money is the Arcade's, and a game that draws something that looks like it confuses players
226
226
  about which one is real.
227
227
  - `unavailable` / `store_off` means the store is not offered here (a TV, an in-app browser that
@@ -234,12 +234,15 @@ Gem pack, a `grant` naming the currency and amount.
234
234
  const { outcome, reason } = await gem.store.purchase(offer.skuId);
235
235
  ```
236
236
 
237
- `purchase(skuId)` asks the Arcade to open its store's sheet for one SKU from `offers()`. The Arcade
238
- pauses your game, shows the offer with its own price, and takes the payment on the provider's page
239
- or the operating system's purchase sheet. Your game is resumed before the answer arrives, so your
240
- own celebration is visible.
237
+ `purchase(skuId)` asks the Arcade to open checkout for one SKU from `offers()`. The Arcade pauses
238
+ and hides your game and opens checkout **straight away**, with no confirmation screen of its own
239
+ first: the payment provider's page on the web, the app store's own purchase sheet in the Arcade's
240
+ apps. That checkout is where the player confirms and pays. Your game is resumed before the answer
241
+ arrives, so your own celebration is visible.
241
242
 
242
- - **Call it from the press** ([§4](#4-call-the-acting-ones-from-a-press)).
243
+ - **Call it from the press** ([§4](#4-call-the-acting-ones-from-a-press)). The player's press on
244
+ your Buy button is what opens checkout, so make that button say plainly what it buys and for how
245
+ much (`priceDisplay`).
243
246
  - **It has no timeout.** A payment takes as long as it takes, so the call waits for as long as the
244
247
  Arcade is connected.
245
248
  - `ok`: the player paid. **Read balances and inventory through `gem.me`**; the answer carries no
@@ -252,14 +255,16 @@ own celebration is visible.
252
255
  const stop = gem.store.onSettled(({ skuId }) => refreshBalances());
253
256
  ```
254
257
 
255
- - `declined` / `cancelled`: the player closed the sheet. Return to your screen quietly and **do not
256
- ask again unprompted**: the Arcade backs off after a dismissal, and the next ask is
257
- `declined` / `busy` for a while.
258
+ - `declined` / `cancelled`: the player closed the checkout without paying. Return to your screen
259
+ quietly and **do not ask again unprompted**: the Arcade backs off after the player closes one, and
260
+ the next ask is `declined` / `busy` for a while, longer each time. An offer the player cannot buy
261
+ again yet also answers `declined` / `cancelled`: the Arcade tells the player when it can be
262
+ bought, opens nothing, and does not back off.
258
263
  - `unavailable` with `store_off`, `sign_in_required` or `not_found` (a SKU that is not in
259
264
  `offers()` right now), or with a payment provider's own failure code: nothing was bought. The
260
265
  first three show nothing.
261
266
  - `unavailable` / `error`: **the outcome is unknown, and the player may have paid.** You get it
262
- when the bridge to the Arcade closes while the sheet is open, which can happen after the payment
267
+ when the bridge to the Arcade closes while checkout is open, which can happen after the payment
263
268
  went through, and when the Arcade has no reliable answer. Never tell the player nothing was
264
269
  bought. Re-read balances and inventory through `gem.me`, keep your `gem.store.onSettled`
265
270
  listener, and re-read again when your game resumes:
@@ -205,9 +205,9 @@ Nothing sells Coins or achievement points, alone or in a bundle. The arcade's ow
205
205
  Gems, and the Gems a player bought there spend in your game exactly as the ones they earned do: your
206
206
  game cannot tell the two apart and prices nothing differently for either. Your game never sells
207
207
  either itself. It may put the arcade store's own Gem packs and Boosts in front of a player from a
208
- button of its own, through `gem.store`, and the arcade prices them, shows its own sheet and takes
209
- the money ([arcade-and-store.md](arcade-and-store.md)). The real-money product that changes what
210
- your game pays is a **Boost**, which a player always buys on the arcade's own sheet:
208
+ button of its own, through `gem.store`, and the arcade prices them, opens its own checkout and
209
+ takes the money ([arcade-and-store.md](arcade-and-store.md)). The real-money product that changes
210
+ what your game pays is a **Boost**, which a player always buys in the arcade's own checkout:
211
211
 
212
212
  - **It multiplies what Gems and Coins grants pay, and the ceilings with them.** A player on a 2×
213
213
  Boost earns twice as much per grant, may earn twice as much per window and per day, and may hold
@@ -247,7 +247,7 @@ animates every payout into the right total. Gems and Coins follow the same rules
247
247
  wallet top-up, or anything shaped like the arcade's purchase sheet: a game screen that looks like
248
248
  platform payment chrome is an imitation of it, and players cannot tell the two apart. Your shop's
249
249
  own Buy button is yours, and so is a "Get Gems" button that calls `gem.store.purchase` from the
250
- player's press: the sheet it opens is the arcade store's own
250
+ player's press: the checkout it opens is the arcade store's own
251
251
  ([arcade-and-store.md](arcade-and-store.md)). When a purchase is refused for
252
252
  `insufficient_funds`, say so in your shop's own words ("Not enough Coins") and stop there: a
253
253
  refusal never opens the store by itself.
package/docs/brand-art.md CHANGED
@@ -296,7 +296,9 @@ decision of that date); the ruling's other rules are unchanged. In rules:
296
296
 
297
297
  - **Keep no Gems counter.** Your game shows no Gems balance, running total or wallet of its own, on
298
298
  any screen. The arcade's wallet is the only place Gems accrue, and the only place a total is shown
299
- — in the Gem Arcade, the Gem menu, which the player opens from the Gem button.
299
+ — in the Gem Arcade, the Gem menu, which the player opens from the Gem button, and beside the Gem
300
+ for a game that asks the arcade to draw it there (`gem.setWalletVisible(true)`,
301
+ [the Arcade's wallet](what-a-game-must-do.md#the-arcades-wallet-in-the-gem-menu-and-beside-the-gem-when-you-ask)).
300
302
  - **Draw no Gems flight or celebration of your own.** No particles flying to a corner, no counting-up
301
303
  number, no burst of gems. `gem.cueReward` is the animation, and the arcade draws it.
302
304
  - **Show no Gems amount at all when you are not connected to the arcade.** Without the arcade there is
package/docs/changelog.md CHANGED
@@ -16,6 +16,12 @@ Its entries are grouped under up to five headings:
16
16
  - **Rebuild**: a type changed. Recompile against the new version; code that compiles today keeps compiling unless the entry says otherwise.
17
17
  - **New capability**: something a game can now do. Nothing changes unless you use it.
18
18
 
19
+ ## 16.3.1
20
+
21
+ Upgrading from 16.3.0. Impact: none.
22
+
23
+ Nothing in this release asks anything of a game.
24
+
19
25
  ## 16.3.0
20
26
 
21
27
  Upgrading from 16.2.0. Impact: none.
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "16.3.0"
53
+ "@indiegems/gem-web-sdk": "16.3.1"
54
54
  }
55
55
  ```
56
56
 
@@ -828,7 +828,7 @@ inventory, never on a reason, and do not retry because of one.
828
828
  that looks like the arcade's purchase sheet or wallet. A game screen shaped like platform payment
829
829
  chrome is an imitation of it. Your Buy button is yours, and taking a player's money is the
830
830
  arcade's alone: a "Get Gems" button of yours calls `gem.store.purchase` from the player's press,
831
- and the arcade store's own sheet does the rest ([arcade-and-store.md](arcade-and-store.md)). An
831
+ and the arcade store's own checkout does the rest ([arcade-and-store.md](arcade-and-store.md)). An
832
832
  `insufficient_funds` refusal never opens it by itself.
833
833
 
834
834
  ### What Coins and Gems may buy: the spend scope
@@ -926,8 +926,9 @@ buyButton.disabled = !gems || gems.balance < price; // can the player afford it?
926
926
  ```
927
927
 
928
928
  **Read it to decide, never to display.** Platform-currency display belongs to the arcade: its
929
- wallet — the Gem menu, in the Gem Arcade — shows the player's Gems and Coins totals while your
930
- game runs, and your game keeps no
929
+ wallet — the Gem menu, in the Gem Arcade, and beside the Gem for a game that asks with
930
+ `gem.setWalletVisible(true)` ([the Arcade's wallet](what-a-game-must-do.md#the-arcades-wallet-in-the-gem-menu-and-beside-the-gem-when-you-ask))
931
+ — shows the player's Gems and Coins totals while your game runs, and your game keeps no
931
932
  counter, balance or wallet total of either of its own ([brand-art.md](brand-art.md) §5). Use the
932
933
  balance to enable a purchase or to say "not enough Gems" — not to draw a number.
933
934
 
@@ -18,8 +18,8 @@
18
18
  },
19
19
  {
20
20
  "file": "arcade-and-store.md",
21
- "sha256": "d792225c078f9488e3c67f2fe4af1128f7f742ee829173dcd014b03e11eb32f0",
22
- "updated": "2026-10-29T20:35:07Z"
21
+ "sha256": "e055b2b1ec540da9fae8f41c025de99b0faec29cef8fca35285ee34e23120941",
22
+ "updated": "2026-10-29T23:35:13Z"
23
23
  },
24
24
  {
25
25
  "file": "arcade-avatars.md",
@@ -28,8 +28,8 @@
28
28
  },
29
29
  {
30
30
  "file": "arcade-economy.md",
31
- "sha256": "aca343bf98c0e111bca7d27f504bf71b43802b98e5cd649792dfd7c5f30b1b16",
32
- "updated": "2026-10-28T18:37:42Z"
31
+ "sha256": "f0bdc98d765fa4794090fc035d2c32d9c125cbdae58ba89fb5e76fcf1fb9da94",
32
+ "updated": "2026-10-29T23:35:13Z"
33
33
  },
34
34
  {
35
35
  "file": "async-opponents.md",
@@ -38,8 +38,8 @@
38
38
  },
39
39
  {
40
40
  "file": "brand-art.md",
41
- "sha256": "99ef1d5703bca12128b38db8352a102e11444de82903a73973942f074da4fc74",
42
- "updated": "2026-10-29T20:35:07Z"
41
+ "sha256": "b7f8dda1afdb8d56cadee752e6b09fc62238372b68ebc8a746518e528f52e167",
42
+ "updated": "2026-10-29T21:40:17Z"
43
43
  },
44
44
  {
45
45
  "file": "branding.md",
@@ -48,8 +48,8 @@
48
48
  },
49
49
  {
50
50
  "file": "changelog.md",
51
- "sha256": "600e6a0c79c48d4fa82e0993eac822df6629ab737ec6053eafab19c743ea19b8",
52
- "updated": "2026-10-29T21:40:20Z"
51
+ "sha256": "e66efedcc9662bea610d3d4edc4e0125f3078e2b49059770f4553d57f37368b3",
52
+ "updated": "2026-10-29T23:50:25Z"
53
53
  },
54
54
  {
55
55
  "file": "daily-challenges.md",
@@ -78,13 +78,13 @@
78
78
  },
79
79
  {
80
80
  "file": "getting-started.md",
81
- "sha256": "50f47be4de5a38a04e49cf1e864f66f8eab4739d8658d6984bb85c61400a0893",
82
- "updated": "2026-10-29T21:40:20Z"
81
+ "sha256": "5f6b7c1e3763cb647379ccc96fc8eb4e5f22551f60cfe56bc6a957312f431fe3",
82
+ "updated": "2026-10-29T23:50:25Z"
83
83
  },
84
84
  {
85
85
  "file": "inventory-and-catalogs.md",
86
- "sha256": "e7bf9f38bf4ede778fdaca200186b782870f409949743e4e85f22f9c919cd19c",
87
- "updated": "2026-10-28T18:37:42Z"
86
+ "sha256": "032b911f515a7c81b0bcee6065300049651fef8b2108e7930e1d1354eada7a87",
87
+ "updated": "2026-10-29T23:35:13Z"
88
88
  },
89
89
  {
90
90
  "file": "leaderboards.md",
@@ -138,8 +138,8 @@
138
138
  },
139
139
  {
140
140
  "file": "server-bundles.md",
141
- "sha256": "721aeb9ac64b9564477a5dffb3b5cc70d6d0c1b222d79ab7090413a0f9b4ef5a",
142
- "updated": "2026-10-29T21:40:20Z"
141
+ "sha256": "387d213d3770f21dccb32c94aac57b5af32bcac8bbe5ca9a6c3c2d8d51e5a2a2",
142
+ "updated": "2026-10-29T23:50:25Z"
143
143
  },
144
144
  {
145
145
  "file": "serverless-functions-js.md",
@@ -163,8 +163,8 @@
163
163
  },
164
164
  {
165
165
  "file": "what-a-game-must-do.md",
166
- "sha256": "e020dca72e597dae0bfa1a71e3263dbb3234f9515d464944e66d7f89612f8db1",
167
- "updated": "2026-10-28T00:47:43Z"
166
+ "sha256": "9d6f2121306bfee8b886d9555a644faa843d16f0c5200e257698336d747136b1",
167
+ "updated": "2026-10-29T21:40:17Z"
168
168
  }
169
169
  ]
170
170
  }
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
80
80
  also publishes on its download host, with the hash beside it:
81
81
 
82
82
  ```bash
83
- curl -fsSL -O https://downloads.gemarcade.com/sdk/v16.3.0/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v16.3.0/gem-web-sdk.tgz.sri
83
+ curl -fsSL -O https://downloads.gemarcade.com/sdk/v16.3.1/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v16.3.1/gem-web-sdk.tgz.sri
85
85
  openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
86
86
  ```
87
87
 
@@ -783,11 +783,13 @@ game, a framed or full-window game on a desktop, a tablet or a TV in native full
783
783
  opens [the Arcade's menu](#the-arcades-menu): Leave game, the player's Gems, Coins and
784
784
  achievement score, mute, the player's Arcade-wide settings and the rest. Not your settings: those
785
785
  are [your own button](#game-settings).
786
- - **Nothing else.** The player's wallets (Gems, Coins, their achievement score) are **not** drawn
787
- over your game: the player opens the Gem menu to see them, and every platform-currency payout
788
- flies into the Gem button ([the Arcade's wallet](#the-arcades-wallet-in-the-gem-menu-never-drawn-by-you)).
789
- When the player switches the frame-rate readout on, it sits beside the gem and is reserved the
790
- same way.
786
+ - **Nothing else, unless you ask.** The player's wallets (Gems, Coins, their achievement score) are
787
+ **not** drawn over a game that says nothing: the player opens the Gem menu to see them, and every
788
+ platform-currency payout flies into the Gem button. A game that wants them on screen asks with
789
+ `gem.setWalletVisible(true)`, and the Arcade draws them beside the Gem
790
+ ([the Arcade's wallet](#the-arcades-wallet-in-the-gem-menu-and-beside-the-gem-when-you-ask)).
791
+ When the player switches the frame-rate readout on, it sits beside the gem; the readout and any
792
+ wallets you asked for are reserved the same way as the Gem.
791
793
 
792
794
  ### The top of the screen
793
795
 
@@ -809,7 +811,10 @@ where "back" and "settings" are once, across every game:
809
811
  SDK gives you as `--gem-reserved-tl-center-y` (below), never on the middle of its reserved area.
810
812
  - **Your settings button is there on every screen.** It goes top-right even where no Arcade control
811
813
  is: another embedder may keep one in that corner, so place it from the reserved areas below and it
812
- moves aside for whatever is there.
814
+ moves aside for whatever is there. Inside the Gem Arcade you may leave it out, on every screen
815
+ at once, when the Arcade's menu reaches everything it did
816
+ ([a game whose menu rows replace its button](#leaving-your-settings-button-out-inside-the-arcade));
817
+ Back then takes the end of the row.
813
818
 
814
819
  The parts of your frame under the Gem and any other Arcade control are **reserved**: keep
815
820
  essential UI and every touch target out of them. **Never hard-code their size or position.** They
@@ -1071,11 +1076,16 @@ This is the whole model, and every other page that mentions settings links here:
1071
1076
  them ([the Arcade's Music and Sound switches](#the-arcades-music-and-sound-switches-onaudiochannels)).
1072
1077
  - **Every game draws its own settings button, top-right ([the top of the
1073
1078
  screen](#the-top-of-the-screen)), and its own settings screen.** The Arcade's menu never opens it.
1079
+ Inside the Gem Arcade the button may go when the Arcade's menu holds every setting and action of
1080
+ your screen ([below](#leaving-your-settings-button-out-inside-the-arcade)); the screen stays,
1081
+ for every other embedder.
1074
1082
  - **A game should also declare its settings — all of them, up to eight — to the Arcade's menu**,
1075
1083
  with `gem.settings.declare`: the Arcade's Settings screen lists every one, and the first
1076
1084
  colour-help toggle fills the Color slot of its fixed quick row (the first music toggle fills Music
1077
1085
  for a game that does not pass `onAudioChannels`). It is a second way
1078
- to reach them, never the only one, and never a second mute or vibration switch ([mirror your
1086
+ to reach them — the only one only where a game
1087
+ [leaves its button out inside the Arcade](#leaving-your-settings-button-out-inside-the-arcade) —
1088
+ and never a second mute or vibration switch ([mirror your
1079
1089
  settings into the Arcade's menu](#mirror-your-settings-into-the-arcades-menu-gemsettings)).
1080
1090
  - **A setting starts from `recommended` until the player changes it in your game, and then the
1081
1091
  player's choice in your game wins.** Save only what the player chose, never a recommended value.
@@ -1222,11 +1232,41 @@ player's Arcade-wide ones, in two places:
1222
1232
  slot that nothing fills stays in its place, greyed and inert. Nothing else of yours reaches the
1223
1233
  quick row.
1224
1234
 
1225
- **It is a mirror, never a replacement: keep your own settings button and screen**, with every
1226
- setting you declare on it too. An embedder that draws no game settings shows none of this, and the
1227
- Arcade may leave an entry out when it is short of room — your own screen is what remains. A game
1228
- that dropped its button because the Arcade's menu had its settings would have settings nobody can
1229
- reach.
1235
+ **It is a mirror of your own screen: keep that screen**, with every setting you declare on it too.
1236
+ An embedder that draws no game settings shows none of this — your own screen is what remains there.
1237
+ Your own button stays too, except inside the Gem Arcade under the conditions in the next section:
1238
+ a game that dropped its button anywhere else, or while a setting was missing from the menu, would
1239
+ have settings nobody can reach.
1240
+
1241
+ #### Leaving your settings button out inside the Arcade
1242
+
1243
+ A game may leave its own settings button out inside the Gem Arcade (the owner, 2026-10-29), when
1244
+ the Arcade's menu reaches everything the button did:
1245
+
1246
+ - **Every toggle, slider and choice on your settings screen is declared** with
1247
+ `gem.settings.declare`, so the screen has eight of them at most.
1248
+ - **Your mute and vibration are the quick row's**, wired both ways as
1249
+ [Sound](#sound-your-own-mute-control-and-the-arcades) says.
1250
+ - **Every action on the screen is one of your own menu rows** — Replay the tutorial, How to play,
1251
+ Credits — with [`gem.setMenu({ items })`](#the-arcades-menu), so four at most.
1252
+
1253
+ Anything more (a ninth setting, a text field, a key-binding screen) keeps the button. **Hide it only
1254
+ while `gem.setMenu` returns `true`**: that is an Arcade that has connected and draws a menu. It
1255
+ returns `false` before the bridge is up, under an embedder with no menu and in a plain tab, and
1256
+ there the button stays, with a way out of your own where there is no menu. Ask again when the frame
1257
+ connects (`onStateChange` in `createGem`) and when the language changes, since your rows carry
1258
+ your words. Hide it on every screen alike, never on some, and let Back take the end of the top row.
1259
+
1260
+ ```js
1261
+ let arcadeMenu = false;
1262
+ function offerMenu() {
1263
+ arcadeMenu = gem.setMenu({
1264
+ items: [{ id: 'replay-tutorial', label: t('Replay the tutorial'), onSelect: replayTutorial }],
1265
+ });
1266
+ document.documentElement.classList.toggle('arcade-menu', arcadeMenu); // CSS hides the button
1267
+ }
1268
+ offerMenu(); // and again from onStateChange, and on every language change
1269
+ ```
1230
1270
 
1231
1271
  ```js
1232
1272
  // Built from the same state your own settings screen shows: every setting on it.
@@ -1498,8 +1538,9 @@ gem.setMenu({
1498
1538
  ([p2p-and-parties.md](p2p-and-parties.md) §7.4) needs nothing more for this.
1499
1539
 
1500
1540
  `gem.setMenu` returns `false` under an embedder with no menu, and outside any embedder. Your
1501
- settings button is there either way; what such a game also needs is a way out of its own, a control
1502
- that calls `gem.exit()`.
1541
+ settings button is there then, even in a game that
1542
+ [leaves it out inside the Arcade](#leaving-your-settings-button-out-inside-the-arcade); what such a
1543
+ game also needs is a way out of its own, a control that calls `gem.exit()`.
1503
1544
 
1504
1545
  Your own **back or home** button — the one that moves around inside your game — goes **top-right,
1505
1546
  just left of your settings button** ([the top of the screen](#the-top-of-the-screen)). It never
@@ -1650,24 +1691,35 @@ its music out than cut it, say. The Arcade still shows its button for your game
1650
1691
  so this is a promise: your `onPrefs` handler must go quiet on `muted: true` and come back on `false`,
1651
1692
  and the release checklist checks that it does.
1652
1693
 
1653
- ## The Arcade's wallet: in the Gem menu, never drawn by you
1694
+ ## The Arcade's wallet: in the Gem menu, and beside the Gem when you ask
1654
1695
 
1655
1696
  The player's platform balances — Gems, Coins (`ARCADE_TICKETS`) and their achievement score —
1656
1697
  belong to the Arcade. **Your game never draws one**, and never shows a pending state for one: the
1657
1698
  Arcade takes every figure from the platform and shows them itself.
1658
1699
 
1659
- **The Gem Arcade draws no wallet over your game** (since 2026-10-11). Nothing of it sits in your top
1660
- row, at rest or when a balance moves. The player opens the Gem menu — the Gem button, in whichever
1661
- corner it sits — to see their Gems, Coins and achievement score, and every payout
1700
+ **The Gem Arcade draws no wallet over a game that says nothing** (since 2026-10-11). The player
1701
+ opens the Gem menu — the Gem button, in whichever corner it sits — to see their Gems, Coins and
1702
+ achievement score, and every payout
1662
1703
  [`gem.cueReward`](inventory-and-catalogs.md#showing-the-reward-land-in-the-arcades-wallet) announces
1663
1704
  flies into the Gem button. A payout you do not cue flies too, from the middle of your frame, so cue
1664
1705
  the ones the player earns on screen and the flight starts where they are looking.
1665
1706
 
1666
- **`gem.setWalletVisible` changes nothing on the Gem Arcade.** It is still part of the contract, so a
1667
- game that calls it keeps working, and it is the call to make for an embedder that draws wallets over
1668
- games: hide them while you play, show them on your menus. Under the Gem Arcade there is no wallet to
1669
- show or hide and no wallet rectangle in `gem.viewport.reserved`. The `only` option is ignored there
1670
- as well.
1707
+ **A game that wants them on screen asks** (the owner, 2026-10-29):
1708
+
1709
+ ```js
1710
+ gem.setWalletVisible(true, { only: ['GEMS', 'ARCADE_TICKETS'] }); // add 'score' for the achievement score
1711
+ ```
1712
+
1713
+ - **The Arcade draws them beside the Gem**, from the platform's own figures, and reserves them like
1714
+ the Gem: they arrive in `gem.viewport.reserved` and widen the Gem's corner in the
1715
+ `--gem-reserved-*` variables, so a top row laid out from those moves over by itself.
1716
+ - **`only` names the wallets.** Without it every wallet shows, the achievement score's chip
1717
+ included; a slug the platform does not serve names nothing.
1718
+ - **It is screen state, not a toggle**, held across a reconnect: ask once for every screen, or per
1719
+ screen, and `gem.setWalletVisible(false)` puts them away (during play, say). Nothing tells the
1720
+ game a balance changed, and nothing needs to: the figures are the Arcade's.
1721
+ - **An Arcade from before this change ignores the call**: nothing breaks, and the wallets stay in
1722
+ the Gem menu. Another embedder that draws wallets over games honours it the same way.
1671
1723
 
1672
1724
  ## No accidental text selection
1673
1725
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "16.3.0",
3
+ "version": "16.3.1",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -3771,8 +3771,8 @@
3771
3771
  " arcade's store for one of the store's own SKUs -",
3772
3772
  " gem.requestStorePurchase, gated on `store` -",
3773
3773
  " but only from the player's press on a button of the game's; that",
3774
- " sheet is the Arcade Store's, under its own rules, and nothing opens",
3775
- " it by itself on a refusal here.",
3774
+ " checkout is the Arcade Store's, under its own rules, and nothing",
3775
+ " opens it by itself on a refusal here.",
3776
3776
  " - MONEY IS TAKEN ONLY ON THE PAYMENT PROVIDER'S PAGE OR THE OPERATING",
3777
3777
  " SYSTEM'S PURCHASE SHEET, which a player reaches through the parent's",
3778
3778
  " own chrome. Nothing a frame sends carries a price or a card field,",
@@ -5342,7 +5342,9 @@
5342
5342
  "rail's own formatted string, and a game shows it exactly as given,",
5343
5343
  "never parsing it or computing a discount from it. No balance. And no",
5344
5344
  "per-offer availability flag: one would reveal a recent purchase through",
5345
- "its cooldown, so the parent's sheet explains a cooldown instead.",
5345
+ "its cooldown, so the parent explains a cooldown in its own chrome",
5346
+ "instead, and a purchase of that offer is answered `declined` /",
5347
+ "`cancelled`.",
5346
5348
  "",
5347
5349
  "THE NAMES ARE THE PLATFORM'S, not a game developer's: the Arcade",
5348
5350
  "Store's own catalog copy, which is why `name` and `description` sit",
@@ -5438,14 +5440,14 @@
5438
5440
  "direction": "frameToParent",
5439
5441
  "transport": "port",
5440
5442
  "$comment": [
5441
- "Ask the parent to open the Arcade Store's purchase sheet for ONE SKU it",
5443
+ "Ask the parent to open the Arcade Store's checkout for ONE SKU it",
5442
5444
  "listed in gem.storeOffersResult - real money, not Gems. Gated by the",
5443
5445
  "`store` capability. Answered by `gem.storePurchaseResult`.",
5444
5446
  "",
5445
5447
  "THE GAME ASKS; THE PLATFORM SELLS. Nothing in this message is a price,",
5446
- "a card field, a URL or a token. The parent renders its own sheet from",
5447
- "platform records, takes money only on the payment provider's page or",
5448
- "the operating system's purchase sheet, and tells the game the outcome.",
5448
+ "a card field, a URL or a token. The parent looks the SKU up in its own",
5449
+ "listing, takes money only on the payment provider's page or the",
5450
+ "operating system's purchase sheet, and tells the game the outcome.",
5449
5451
  "",
5450
5452
  "AN ACTING MESSAGE, under every rule gem.requestOpenApp states: a press",
5451
5453
  "the parent can see (a missing `userActivation` API fails closed), one",
@@ -5456,17 +5458,26 @@
5456
5458
  " would list to this player right now; anything else is `unavailable`",
5457
5459
  " with nothing shown.",
5458
5460
  "",
5459
- " THE STORE'S OWN SHEET. Over the paused game, hidden not dimmed, with",
5460
- " the parent's bar saying the Arcade Store is open, game chrome frozen,",
5461
- " and focus never landing on a money control. \"Not now\" has the",
5462
- " default focus. Buy arms visibly for a moment and then takes a FRESH",
5463
- " press; input during arming restarts it.",
5461
+ " STRAIGHT TO CHECKOUT, with no confirmation of the parent's own first,",
5462
+ " by owner decision: games are developed in-house and trusted, as for",
5463
+ " gem.requestOpenGame. The press in the game opens the",
5464
+ " payment provider's page on the web, or the operating system's",
5465
+ " purchase sheet in an app, over the paused game: hidden, not dimmed.",
5466
+ " That checkout is the confirmation - it has its own pay control, and",
5467
+ " no money moves without the player's press there.",
5468
+ "",
5469
+ " AN OFFER COOLING DOWN OPENS NOTHING. When the listed SKU cannot be",
5470
+ " bought again yet, the parent says when, in its own chrome, and",
5471
+ " answers `declined` / `cancelled` - the answer a closed checkout gets,",
5472
+ " so the cooldown, which would reveal a recent purchase, never crosses",
5473
+ " the bridge. It starts no back-off: the player turned nothing down.",
5464
5474
  "",
5465
5475
  " BACK OFF AFTER A DISMISSAL. `cancelled` is disclosed, so after the",
5466
- " player dismisses a sheet a game started, further asks from that game",
5476
+ " player closes a checkout a game started, further asks from that game",
5467
5477
  " are `declined` / `busy`, unrendered, for an interval that grows with",
5468
5478
  " each dismissal (the Arcade: 30 s, 2 min, 10 min, then the rest of the",
5469
- " session).",
5479
+ " session). Only the player closing it starts the back-off: a failed",
5480
+ " payment, `error` and `ok` / `pending` do not.",
5470
5481
  "",
5471
5482
  " RESUME, THEN ANSWER. The result is sent after the game has been told",
5472
5483
  " it is resumed, so the game's own celebration is visible.",
@@ -5508,9 +5519,10 @@
5508
5519
  " `reason: pending`, the provider has not settled yet (or",
5509
5520
  " the player left before it did): `gem.storePurchaseSettled`",
5510
5521
  " follows when it does, and a game re-reads on resume too.",
5511
- " declined - nothing was bought: the player dismissed the sheet",
5512
- " (`cancelled`), or no press, another acting message open",
5513
- " or a back-off running (`busy`).",
5522
+ " declined - nothing was bought: the player closed the checkout,",
5523
+ " or the offer is cooling down (`cancelled`); or no",
5524
+ " press, another acting message open or a back-off",
5525
+ " running (`busy`).",
5514
5526
  " unavailable - no sale the parent can vouch for. With `store_off`",
5515
5527
  " (the store withheld), `sign_in_required` (a guest),",
5516
5528
  " `not_found` (an unknown SKU) or the provider's own",