@indiegems/gem-web-sdk 16.3.1 → 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.
@@ -2,7 +2,7 @@
2
2
  "schema_version": 2,
3
3
  "source": {
4
4
  "repo": "IndieGems/gem-web-sdk",
5
- "version": "16.3.1"
5
+ "version": "17.0.0"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -5537,6 +5537,22 @@
5537
5537
  "defaultValue": null,
5538
5538
  "guides": []
5539
5539
  },
5540
+ {
5541
+ "id": "StoreApi.showOfferAvailable",
5542
+ "name": "showOfferAvailable",
5543
+ "anchor": "storeapi-showofferavailable",
5544
+ "group": "store",
5545
+ "signature": "readonly showOfferAvailable: boolean",
5546
+ "documented": true,
5547
+ "summary": "Whether [StoreApi.showOffer](api-reference-game.md#storeapi-showoffer) answers here, synchronously. An older\nArcade can sell through `purchase` and still not draw this sheet, so read\nthis, not [StoreApi.available](api-reference-game.md#storeapi-available), before you draw a button that calls\n`showOffer`. `false` until the bridge is up.",
5548
+ "deprecated": null,
5549
+ "params": [],
5550
+ "returns": null,
5551
+ "throws": null,
5552
+ "example": null,
5553
+ "defaultValue": null,
5554
+ "guides": []
5555
+ },
5540
5556
  {
5541
5557
  "id": "StoreApi.offers",
5542
5558
  "name": "offers",
@@ -5576,6 +5592,29 @@
5576
5592
  "defaultValue": null,
5577
5593
  "guides": []
5578
5594
  },
5595
+ {
5596
+ "id": "StoreApi.showOffer",
5597
+ "name": "showOffer",
5598
+ "anchor": "storeapi-showoffer",
5599
+ "group": "store",
5600
+ "signature": "showOffer(skuId: string): Promise<ArcadeOutcome>",
5601
+ "documented": true,
5602
+ "summary": "Ask the arcade to show one SKU from [StoreApi.offers](api-reference-game.md#storeapi-offers) in its own\nsheet, over your paused and hidden game: its art, what the player gets, a\nsale if one is on, and, for a pass the player already holds, how long it\nhas left. The sheet's Buy opens the same checkout as\n[StoreApi.purchase](api-reference-game.md#storeapi-purchase). **Call it from the player's press handler.**\n\nUse it for an offer you would have to hardcode a pitch for: VIP and other\npasses, and bundles. Your button only needs to say what it opens. Draw a\nGem pack yourself and use `purchase` instead. Check\n[StoreApi.showOfferAvailable](api-reference-game.md#storeapi-showofferavailable) first, and that the SKU is in `offers`.\n\nIt answers exactly as `purchase` does, with one addition: `declined` /\n`cancelled` also covers the player leaving the sheet without pressing Buy,\nand you are not told which of the two they closed. That starts the same\nback-off, so do not ask again unprompted. `unavailable` / `unsupported`\nmeans this Arcade does not draw the sheet, and nothing was sent.\n`unavailable` / `error` means **the outcome is unknown**: re-read balances\nand inventory through `gem.me`, and keep listening with\n[StoreApi.onSettled](api-reference-game.md#storeapi-onsettled).\n\nNO TIMEOUT: it waits for as long as the arcade is connected, because a\nplayer may read a sheet and pay for as long as they like.",
5603
+ "deprecated": null,
5604
+ "params": [
5605
+ {
5606
+ "name": "skuId",
5607
+ "type": "string",
5608
+ "optional": false,
5609
+ "summary": null
5610
+ }
5611
+ ],
5612
+ "returns": "Always resolves; never rejects.",
5613
+ "throws": null,
5614
+ "example": null,
5615
+ "defaultValue": null,
5616
+ "guides": []
5617
+ },
5579
5618
  {
5580
5619
  "id": "StoreApi.onSettled",
5581
5620
  "name": "onSettled",
@@ -12790,7 +12829,7 @@
12790
12829
  "group": "start",
12791
12830
  "signature": "readonly store: StoreApi",
12792
12831
  "documented": true,
12793
- "summary": "**Real money.** The Arcade Store's own Gem packs, Boosts, passes and\nbundles, sold for the player's money through the arcade's own purchase\nsheet. To spend Gems or Coins on your game's own items, use\n[Gem.purchases](api-reference-game.md#gem-purchases).\n\nList what the store sells here with `offers()`, show each `priceDisplay`\nexactly as given, and call `purchase(skuId)` from the player's press. After\n`ok`, re-read balances through [Gem.me](api-reference-game.md#gem-me). Nothing rejects.",
12832
+ "summary": "**Real money.** The Arcade Store's own Gem packs, Boosts, passes and\nbundles, sold for the player's money through the arcade's own purchase\nsheet. To spend Gems or Coins on your game's own items, use\n[Gem.purchases](api-reference-game.md#gem-purchases).\n\nList what the store sells here with `offers()`, show each `priceDisplay`\nexactly as given, and call `purchase(skuId)` from the player's press. For a\npass, a bundle or anything else whose pitch you would have to hardcode, call\n`showOffer(skuId)` instead, and the arcade draws the offer. After `ok`,\nre-read balances through [Gem.me](api-reference-game.md#gem-me). Nothing rejects.",
12794
12833
  "deprecated": null,
12795
12834
  "params": [],
12796
12835
  "returns": null,
@@ -18,6 +18,12 @@ async function onBuyPressed(offer) {
18
18
  // Read through gem.me; nothing here carries an amount. `error` is "outcome unknown" (§9).
19
19
  if (outcome === 'ok' || reason === 'error') await refreshBalances();
20
20
  }
21
+
22
+ // From the press on your own "Get VIP" button: the Arcade draws the offer.
23
+ async function onVipPressed() {
24
+ const { outcome, reason } = await gem.store.showOffer(VIP_SKU);
25
+ if (outcome === 'ok' || reason === 'error') await refreshBalances();
26
+ }
21
27
  ```
22
28
 
23
29
  **Your game asks; the Arcade decides.** Nothing your game sends carries a URL, a price or a card
@@ -33,7 +39,8 @@ itself. You get an outcome.
33
39
  - [7. Similar games, as cards](#7-similar-games-as-cards)
34
40
  - [8. The Arcade Store's offers](#8-the-arcade-stores-offers)
35
41
  - [9. Buying from the Arcade Store](#9-buying-from-the-arcade-store)
36
- - [10. Outcomes and reasons](#10-outcomes-and-reasons)
42
+ - [10. Letting the Arcade show one offer](#10-letting-the-arcade-show-one-offer)
43
+ - [11. Outcomes and reasons](#11-outcomes-and-reasons)
37
44
 
38
45
  ## 1. Two surfaces, and which one is real money
39
46
 
@@ -41,7 +48,7 @@ itself. You get an outcome.
41
48
  | ------------ | ------------------------------------------------ | --------------------------------------------------- |
42
49
  | What it does | app-store listings, another game, similar games | the Arcade Store's own SKUs |
43
50
  | 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 |
51
+ | Who draws it | the Arcade (a new tab, a QR code, the next game) | your card or the Arcade's sheet, then the checkout |
45
52
 
46
53
  `gem.store` is not `gem.purchases`. **`gem.purchases` spends Gems or Coins on your game's own
47
54
  items** ([`inventory-and-catalogs.md`](inventory-and-catalogs.md)). **`gem.store` sells the Arcade
@@ -60,7 +67,7 @@ three and never grows:
60
67
 
61
68
  `reason` is a short code that says why. The set is open: a new Arcade build may send a reason your
62
69
  game has never seen, so branch on `outcome` and treat `reason` as a hint. The codes in use today are
63
- in [§10](#10-outcomes-and-reasons).
70
+ in [§11](#11-outcomes-and-reasons).
64
71
 
65
72
  ## 3. What is available here
66
73
 
@@ -71,17 +78,19 @@ An Arcade that does not offer a feature answers every call for it at once with `
71
78
  if (gem.arcade.available.openGame) showMoreGamesSection();
72
79
  if (gem.arcade.available.appLinks) showGetTheAppButton();
73
80
  if (gem.store.available) showGetGemsButton();
81
+ if (gem.store.showOfferAvailable) showGetVipButton();
74
82
  ```
75
83
 
76
- `gem.arcade.available` is `{ appLinks, openGame }`, and `gem.store.available` is one boolean. All of
77
- them are synchronous and `false` until the bridge is up. A feature can be off for reasons you
84
+ `gem.arcade.available` is `{ appLinks, openGame }`, and `gem.store.available` and
85
+ `gem.store.showOfferAvailable` are one boolean each. All of them are synchronous and `false` until
86
+ the bridge is up. A feature can be off for reasons you
78
87
  cannot see from inside the frame: a TV, an in-app browser that cannot take payment, an app build
79
88
  where it is withheld. Do not guess; read it.
80
89
 
81
90
  ## 4. Call the acting ones from a press
82
91
 
83
- Three calls put something in front of the player: `gem.arcade.openApp`, `gem.arcade.openGame` and
84
- `gem.store.purchase`. **Call each one from the player's own press handler**, in the same turn as the
92
+ Four calls put something in front of the player: `gem.arcade.openApp`, `gem.arcade.openGame`,
93
+ `gem.store.purchase` and `gem.store.showOffer`. **Call each one from the player's own press handler**, in the same turn as the
85
94
  press. The Arcade checks that a person pressed, and refuses an ask nobody pressed for with
86
95
  `declined` / `busy`, showing nothing.
87
96
 
@@ -273,7 +282,57 @@ arrives, so your own celebration is visible.
273
282
  if (outcome === 'unavailable' && reason === 'error') await refreshBalances(); // not "nothing bought"
274
283
  ```
275
284
 
276
- ## 10. Outcomes and reasons
285
+ ## 10. Letting the Arcade show one offer
286
+
287
+ ```js
288
+ // Decide on the screen you draw the button on.
289
+ const { offers } = await gem.store.offers();
290
+ const vip = offers.find((offer) => offer.skuId === VIP_SKU);
291
+ if (gem.store.showOfferAvailable && vip) showGetVipButton();
292
+ else hideGetVipButton();
293
+
294
+ // From the press on that button.
295
+ const { outcome, reason } = await gem.store.showOffer(VIP_SKU);
296
+ if (outcome === 'ok' || reason === 'error') await refreshBalances();
297
+ ```
298
+
299
+ `showOffer(skuId)` asks the Arcade to draw one SKU from `offers()` in its own sheet, over your paused
300
+ and hidden game. The Arcade draws the art, what the player gets, a sale if one is running, and, for a
301
+ pass the player already holds, how long it has left. The sheet's Buy opens the same checkout
302
+ `purchase` opens.
303
+
304
+ Pick by who can say what the offer is:
305
+
306
+ | | **You draw it: `offers()` + `purchase()`** | **The Arcade draws it: `showOffer()`** |
307
+ | --------------- | --------------------------------------------------------- | ------------------------------------------------------------ |
308
+ | Use it for | Gem packs: a name, an amount and a price say it all | VIP and other passes, and bundles |
309
+ | Your button | says what it buys and shows `priceDisplay` | says what it opens, such as "Get VIP" |
310
+ | The press opens | the checkout, straight away | the Arcade's sheet, then the checkout if the player presses Buy |
311
+ | Perks and sales | not available to you | always current, in the player's language |
312
+ | A holder sees | the same Buy button | what they already have |
313
+
314
+ - **Never write an offer's perks into your game.** They change without a new build of your game, and
315
+ yours would go stale. Your button only needs the offer's name.
316
+ - **Do not show a price on the button.** The sheet shows the right one for this player, which a
317
+ sale or a pass can change, and `priceDisplay` does not say either of those. If you want a price on
318
+ screen, draw the offer yourself and use `purchase`.
319
+ - **You cannot tell whether the player holds a pass.** The Arcade keeps that to itself, and the
320
+ sheet tells the player. So the button stays up for a holder, and that is fine: the sheet is what
321
+ they see, and they choose from there.
322
+ - **Hide the button when the SKU is not in `offers()`.** A SKU that is not on sale here answers
323
+ `unavailable` / `not_found`, and the player would have pressed a button that does nothing.
324
+ - **Read `showOfferAvailable`, not `available`.** An older Arcade can sell through `purchase` and
325
+ still not draw this sheet. It answers `unavailable` / `unsupported` and sends nothing. Hide the
326
+ button there.
327
+
328
+ Every rule of `purchase` holds ([§9](#9-buying-from-the-arcade-store)), and the answers are the
329
+ same, with one addition. `declined` / `cancelled` also covers the player leaving the sheet without
330
+ pressing Buy, and you are not told which of the two they closed. That starts the same back-off as a
331
+ closed checkout, shared with `purchase`: the next ask of either kind is `declined` / `busy` for a
332
+ while. The press shows nothing during a back-off, so give the player something else to do on that
333
+ screen.
334
+
335
+ ## 11. Outcomes and reasons
277
336
 
278
337
  | Call | `ok` | `declined` | `unavailable` |
279
338
  | ---------------- | ------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- |
@@ -283,6 +342,7 @@ arrives, so your own celebration is visible.
283
342
  | `similarGames` | cards, possibly none | | `unsupported`, `error` |
284
343
  | `store.offers` | offers, possibly none | | `sign_in_required`, `store_off`, `unsupported`, `error` |
285
344
  | `store.purchase` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
345
+ | `store.showOffer` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
286
346
 
287
347
  Every reason is a short code of lower-case letters and underscores. The Arcade turns a payment
288
348
  provider's own code into that shape before it reaches you.
@@ -296,5 +356,5 @@ Four reasons come from the SDK itself rather than the Arcade:
296
356
  - `error`: no reliable answer arrived. The ask could not be sent, a read or an open waited out its
297
357
  timeout (30 seconds for a read, two minutes for `openApp` and `openGame`), the bridge closed
298
358
  before the answer came, or the Arcade said it had no reliable answer. For a read, try again
299
- later. For `store.purchase` it means **the outcome is unknown**, never "nothing was bought"
300
- ([§9](#9-buying-from-the-arcade-store)).
359
+ later. For `store.purchase` and `store.showOffer` it means **the outcome is unknown**, never
360
+ "nothing was bought" ([§9](#9-buying-from-the-arcade-store)).
@@ -204,9 +204,9 @@ 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, opens its own checkout and
209
- takes the money ([arcade-and-store.md](arcade-and-store.md)). The real-money product that changes
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
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×
@@ -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 checkout 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/changelog.md CHANGED
@@ -16,6 +16,33 @@ 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
+
19
46
  ## 16.3.1
20
47
 
21
48
  Upgrading from 16.3.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.1"
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 checkout 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
@@ -18,8 +18,8 @@
18
18
  },
19
19
  {
20
20
  "file": "arcade-and-store.md",
21
- "sha256": "e055b2b1ec540da9fae8f41c025de99b0faec29cef8fca35285ee34e23120941",
22
- "updated": "2026-10-29T23:35:13Z"
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": "f0bdc98d765fa4794090fc035d2c32d9c125cbdae58ba89fb5e76fcf1fb9da94",
32
- "updated": "2026-10-29T23:35:13Z"
31
+ "sha256": "b72474b4694842a1fda030ab9df79987d2906f7a8c922e8630622d747a4b2c3c",
32
+ "updated": "2026-10-30T13:36:37Z"
33
33
  },
34
34
  {
35
35
  "file": "async-opponents.md",
@@ -48,8 +48,8 @@
48
48
  },
49
49
  {
50
50
  "file": "changelog.md",
51
- "sha256": "e66efedcc9662bea610d3d4edc4e0125f3078e2b49059770f4553d57f37368b3",
52
- "updated": "2026-10-29T23:50:25Z"
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": "5f6b7c1e3763cb647379ccc96fc8eb4e5f22551f60cfe56bc6a957312f431fe3",
82
- "updated": "2026-10-29T23:50:25Z"
81
+ "sha256": "a69a8be43e655a71717c1d15ff3e9136103c17eebd9074c2f3dab4fad07d2689",
82
+ "updated": "2026-10-30T14:09:44Z"
83
83
  },
84
84
  {
85
85
  "file": "inventory-and-catalogs.md",
86
- "sha256": "032b911f515a7c81b0bcee6065300049651fef8b2108e7930e1d1354eada7a87",
87
- "updated": "2026-10-29T23:35:13Z"
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": "387d213d3770f21dccb32c94aac57b5af32bcac8bbe5ca9a6c3c2d8d51e5a2a2",
142
- "updated": "2026-10-29T23:50:25Z"
141
+ "sha256": "d00bc2ea3896c2e262619bf87b2892dd954c9ef87d37dfb5881bb6ef7439401a",
142
+ "updated": "2026-10-30T14:09:44Z"
143
143
  },
144
144
  {
145
145
  "file": "serverless-functions-js.md",
@@ -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.1/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v16.3.1/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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "16.3.1",
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",
@@ -5581,6 +5581,114 @@
5581
5581
  }
5582
5582
  },
5583
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 }
5584
5692
  }
5585
5693
  ],
5586
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
  }