@indiegems/gem-web-sdk 16.3.0 → 17.0.0

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.
@@ -204,10 +204,10 @@ player got.
204
204
  Nothing sells Coins or achievement points, alone or in a bundle. The arcade's own store sells
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
- 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:
207
+ either itself. It may put the arcade store's own Gem packs, Boosts and passes in front of a player
208
+ from a button of its own, through `gem.store`, and the arcade prices them, opens its own checkout
209
+ and 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
@@ -246,8 +246,9 @@ animates every payout into the right total. Gems and Coins follow the same rules
246
246
  - **No top-up or payment screen of your own.** Never draw a "buy Coins" offer, a checkout, a
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
- 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
249
+ own Buy button is yours, and so is a "Get Gems" button that calls `gem.store.purchase`, or a "Get
250
+ VIP" button that calls `gem.store.showOffer`, from the player's press: the checkout each one
251
+ leads to is the arcade store's own
251
252
  ([arcade-and-store.md](arcade-and-store.md)). When a purchase is refused for
252
253
  `insufficient_funds`, say so in your shop's own words ("Not enough Coins") and stop there: a
253
254
  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,39 @@ 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
+ ## 17.0.0
20
+
21
+ *Written from the API facts alone; no summary was generated for this release.*
22
+
23
+ Upgrading from 16.3.1. Impact: required.
24
+
25
+ ### Must change
26
+
27
+ - PARENT_CONFORMANCE_VERSION changed its signature.
28
+ - FRAME_TO_PARENT changed its signature.
29
+ - FrameMessage changed its signature.
30
+ - GemCapability changed its signature.
31
+ - PARENT_TO_FRAME changed its signature.
32
+ - ParentMessage changed its signature.
33
+
34
+ ### New capability
35
+
36
+ - StoreApi.showOffer was added.
37
+ - StoreApi.showOfferAvailable was added.
38
+ - GemHost.answerStoreOfferSheet was added.
39
+ - GemRequestStoreOfferSheet was added.
40
+ - GemStoreOfferSheetResult was added.
41
+ - HostCallbacks.onRequestStoreOfferSheet was added.
42
+ - HostOptions.onRequestStoreOfferSheet was added.
43
+ - validateGemRequestStoreOfferSheet was added.
44
+ - validateGemStoreOfferSheetResult was added.
45
+
46
+ ## 16.3.1
47
+
48
+ Upgrading from 16.3.0. Impact: none.
49
+
50
+ Nothing in this release asks anything of a game.
51
+
19
52
  ## 16.3.0
20
53
 
21
54
  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": "17.0.0"
54
54
  }
55
55
  ```
56
56
 
@@ -827,8 +827,9 @@ inventory, never on a reason, and do not retry because of one.
827
827
  ⚠️ **Never draw a top-up or payment screen of your own** — no checkout, no wallet top-up, nothing
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
- 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
830
+ arcade's alone: a "Get Gems" button of yours calls `gem.store.purchase` from the player's press, a
831
+ "Get VIP" button calls `gem.store.showOffer`, and the arcade store's own checkout does the rest
832
+ ([arcade-and-store.md](arcade-and-store.md)). An
832
833
  `insufficient_funds` refusal never opens it by itself.
833
834
 
834
835
  ### What Coins and Gems may buy: the spend scope
@@ -926,8 +927,9 @@ buyButton.disabled = !gems || gems.balance < price; // can the player afford it?
926
927
  ```
927
928
 
928
929
  **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
930
+ wallet — the Gem menu, in the Gem Arcade, and beside the Gem for a game that asks with
931
+ `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))
932
+ — shows the player's Gems and Coins totals while your game runs, and your game keeps no
931
933
  counter, balance or wallet total of either of its own ([brand-art.md](brand-art.md) §5). Use the
932
934
  balance to enable a purchase or to say "not enough Gems" — not to draw a number.
933
935
 
@@ -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": "7320826529435d290159c8b0707f69819124607d1b38160dbc85f63b385fdea9",
22
+ "updated": "2026-10-30T13:36:37Z"
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": "b72474b4694842a1fda030ab9df79987d2906f7a8c922e8630622d747a4b2c3c",
32
+ "updated": "2026-10-30T13:36:37Z"
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": "b795d5ed73558a4d355196971fb4bd9689ca15d38fd01d9a67b4cf6c0e8ff25d",
52
+ "updated": "2026-10-30T14:09:44Z"
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": "a69a8be43e655a71717c1d15ff3e9136103c17eebd9074c2f3dab4fad07d2689",
82
+ "updated": "2026-10-30T14:09:44Z"
83
83
  },
84
84
  {
85
85
  "file": "inventory-and-catalogs.md",
86
- "sha256": "e7bf9f38bf4ede778fdaca200186b782870f409949743e4e85f22f9c919cd19c",
87
- "updated": "2026-10-28T18:37:42Z"
86
+ "sha256": "9c898355af92ab7e17035bad964dce57b26e64624b18e83149b88ae10b96fd9c",
87
+ "updated": "2026-10-30T13:36:37Z"
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": "d00bc2ea3896c2e262619bf87b2892dd954c9ef87d37dfb5881bb6ef7439401a",
142
+ "updated": "2026-10-30T14:09:44Z"
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/v17.0.0/gem-web-sdk.tgz
84
+ curl -fsSL https://downloads.gemarcade.com/sdk/v17.0.0/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": "17.0.0",
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",
@@ -5569,6 +5581,114 @@
5569
5581
  }
5570
5582
  },
5571
5583
  "rateLimit": { "perMinute": 30 }
5584
+ },
5585
+ {
5586
+ "type": "gem.requestStoreOfferSheet",
5587
+ "direction": "frameToParent",
5588
+ "transport": "port",
5589
+ "$comment": [
5590
+ "Ask the parent to SHOW one SKU it listed in gem.storeOffersResult, in",
5591
+ "the Arcade's own one-offer sheet over the paused game - real money, not",
5592
+ "Gems. Gated by the `storeOfferSheet` capability. Answered by",
5593
+ "`gem.storeOfferSheetResult`.",
5594
+ "",
5595
+ "THE ARCADE DRAWS THE OFFER; THE GAME ONLY POINTS AT IT. gem.requestStorePurchase",
5596
+ "is for an offer the game drew itself from gem.storeOffersResult, and goes",
5597
+ "straight to checkout. This is for an offer whose pitch the bridge cannot",
5598
+ "carry: its perks, a sale, what a holder already has. All of that stays",
5599
+ "with the parent, so none of it - least of all whether this player holds",
5600
+ "a pass - crosses to the game. The sheet's Buy opens the same checkout",
5601
+ "gem.requestStorePurchase opens.",
5602
+ "",
5603
+ "EVERY RULE OF gem.requestStorePurchase HOLDS, and it shares them rather",
5604
+ "than having its own: an acting message (a press the parent can see, one",
5605
+ "at a time across all of them, both checked before `skuId` is resolved,",
5606
+ "the answer bound to this frame session); only a SKU the parent would",
5607
+ "list now; an offer cooling down opens nothing and answers `declined` /",
5608
+ "`cancelled`; the SAME back-off, so a game cannot escape one by switching",
5609
+ "message; resume, then answer; evidence, not accounting.",
5610
+ "",
5611
+ "THE SHEET IS NOT A SECOND CONFIRMATION, and it does not change",
5612
+ "gem.requestStorePurchase's straight-to-checkout rule. It is the pitch;",
5613
+ "the checkout is still the confirmation, with its own pay control. The",
5614
+ "player leaving the sheet without pressing Buy is a dismissal, exactly",
5615
+ "as closing a checkout is, and starts the back-off.",
5616
+ "",
5617
+ "A PARENT THAT ADVERTISES `storeOfferSheet` ADVERTISES `store`: the",
5618
+ "game's SKU comes from gem.storeOffersResult, and a purchase that answers",
5619
+ "`ok` / `pending` here is followed by gem.storePurchaseSettled.",
5620
+ "",
5621
+ "Every field is untrusted text: match `skuId` against the offers you",
5622
+ "read, never interpolate it into a path, a query or a log line."
5623
+ ],
5624
+ "fields": {
5625
+ "requestId": {
5626
+ "kind": "string",
5627
+ "maxLength": 64,
5628
+ "$comment": "Opaque and frame-chosen. Echo-only, and never a key on the money path: the parent derives its own request identity."
5629
+ },
5630
+ "skuId": {
5631
+ "kind": "string",
5632
+ "maxLength": 64,
5633
+ "$comment": "One `skuId` from gem.storeOffersResult. Frame-chosen: matched by the parent against the listing it would show now."
5634
+ }
5635
+ },
5636
+ "rateLimit": {
5637
+ "perMinute": 12,
5638
+ "$comment": "gem.requestStorePurchase's limit and its argument: it bounds a BUGGY game, not a hostile one. What bounds the player's experience is the activation check, the one-at-a-time rule and the back-off."
5639
+ }
5640
+ },
5641
+ {
5642
+ "type": "gem.storeOfferSheetResult",
5643
+ "direction": "parentToFrame",
5644
+ "transport": "port",
5645
+ "$comment": [
5646
+ "The answer to one gem.requestStoreOfferSheet, carrying exactly what",
5647
+ "gem.storePurchaseResult carries and meaning the same. Gated by the",
5648
+ "`storeOfferSheet` capability.",
5649
+ "",
5650
+ " ok - the player paid. With no reason, fulfilment has",
5651
+ " settled; with `reason: pending`,",
5652
+ " gem.storePurchaseSettled follows when it does.",
5653
+ " declined - nothing was bought: the player left the sheet or closed",
5654
+ " the checkout, or the offer is cooling down (`cancelled`);",
5655
+ " or no press, another acting message open or a back-off",
5656
+ " running (`busy`).",
5657
+ " unavailable - no sale the parent can vouch for. With `store_off`,",
5658
+ " `sign_in_required`, `not_found` or the provider's own",
5659
+ " failure code, nothing was bought. With `error`, the",
5660
+ " OUTCOME IS UNKNOWN and the player may have paid. The SDK",
5661
+ " resolves the same `unavailable` / `error` when the port",
5662
+ " closes under an open sheet.",
5663
+ "",
5664
+ "`cancelled` DOES NOT SAY WHICH. The game is not told whether the player",
5665
+ "left the sheet or the checkout, and nothing here says what the sheet",
5666
+ "showed.",
5667
+ "",
5668
+ "NO TIMEOUT ON THE GAME'S SIDE while the port is alive: the parent always",
5669
+ "answers, and a player may read a sheet for as long as they like.",
5670
+ "NEVER AUTO-RETRIED. Only a fresh press may ask again."
5671
+ ],
5672
+ "fields": {
5673
+ "requestId": {
5674
+ "kind": "string",
5675
+ "maxLength": 64,
5676
+ "$comment": "Echoes the gem.requestStoreOfferSheet that asked. Echo-only."
5677
+ },
5678
+ "outcome": {
5679
+ "kind": "enum",
5680
+ "values": ["ok", "declined", "unavailable"],
5681
+ "$comment": "Closed at three, as gem.purchaseResult's."
5682
+ },
5683
+ "reason": {
5684
+ "kind": "string",
5685
+ "maxLength": 64,
5686
+ "format": "code",
5687
+ "optional": true,
5688
+ "$comment": "An open code, on any outcome: `pending` with `ok`; `cancelled` or `busy` with `declined`; `store_off`, `sign_in_required`, `not_found`, `error` or the payment provider's own failure code with `unavailable`. Normalized to the code grammar before it is sent, as gem.storePurchaseResult's. A receiver must not fail on a code it does not know."
5689
+ }
5690
+ },
5691
+ "rateLimit": { "perMinute": 30 }
5572
5692
  }
5573
5693
  ],
5574
5694
 
@@ -295,6 +295,13 @@
295
295
  "gem.storePurchaseResult",
296
296
  "gem.storePurchaseSettled"
297
297
  ]
298
+ },
299
+ "storeOfferSheet": {
300
+ "implemented": true,
301
+ "gates": [
302
+ "gem.requestStoreOfferSheet",
303
+ "gem.storeOfferSheetResult"
304
+ ]
298
305
  }
299
306
  }
300
307
  }