@indiegems/gem-web-sdk 24.0.1 → 24.0.2

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.
package/CHANGELOG.md CHANGED
@@ -17,6 +17,14 @@ A release opens with a short summary of what it is about, and its entries are gr
17
17
  - **New capability**: something a game can now do. Nothing changes unless you use it.
18
18
  - **Fixes and improvements**: bugs fixed and behaviour improved without a change to the API. Nothing to change in your game.
19
19
 
20
+ ## 24.0.2
21
+
22
+ Upgrading from 24.0.1. Impact: none.
23
+
24
+ This release changes nothing in the public API and asks nothing of a game. The changes are internal: CI checks that the telemetry event lists stay in sync, telemetry documentation, and test vectors for the economy preview.
25
+
26
+ Nothing in this release asks anything of a game.
27
+
20
28
  ## 24.0.1
21
29
 
22
30
  Upgrading from 24.0.0. Impact: none.
@@ -29,61 +37,176 @@ Nothing in this release asks anything of a game.
29
37
 
30
38
  Upgrading from 23.3.0. Impact: required.
31
39
 
40
+ This release is mostly the economy-reset work: games can sell a pass by its slug, preview what a config grants including boosts and passes, read earnings from receipts, and gate on a player's passes and boost from a dedicated server. Pass `endsAt` is deprecated in favour of `daysLeft`. A game should review how it tells guests apart and how it reads the offer `kind` of a coin pack, and anyone running the parent conformance kit must adopt kit version 9.
41
+
32
42
  ### Must change
33
43
 
34
- - Stop using the store offers call answering sign_in_required as a test for a guest. A current Arcade now answers a guest with ok, because a guest can buy. Use GemPlayer.isGuest instead. It is true only when the Arcade says the player is a guest who can sign up here, and false for a full account, for a surface with no sign-up, and for an older Arcade that does not say. It follows a guest who signs up in place.
35
- - Check any code that branches on the offer kind. ArcadeOffer.kind is now typed as ArcadeOfferKind, and a pack of a platform currency other than GEMS is reported as coin_pack rather than gem_pack. The gem_pack value is deprecated. Handle coin_pack wherever you handle gem_pack, or such packs will fall through your branches.
36
- - If your tests run the parent conformance kit against an Arcade host or a stand-in for one, update them for kit version 9. The version constant moved from 8 to 9, there is a new storeScenario member on the parent under test, and there are six new pass cases. Supply a store scenario so those cases can run.
44
+ - **Offer `kind` has a new value, and non-GEMS packs now report it.** `ArcadeOffer.kind` is now typed `ArcadeOfferKind`. A pack of any platform currency other than GEMS is reported as `coin_pack`. `gem_pack` is deprecated and stays for GEMS packs. Code that matches `kind === "gem_pack"` to find every currency pack will miss coin packs, and an exhaustive `switch` over `kind` will stop compiling.
45
+
46
+ ```ts
47
+ // before
48
+ if (offer.kind === "gem_pack") showPack(offer);
49
+
50
+ // after
51
+ if (offer.kind === "gem_pack" || offer.kind === "coin_pack") showPack(offer);
52
+ ```
53
+ - **Do not use `gem.store.offers` answering `sign_in_required` to detect a guest.** A current Arcade answers a guest's `offers` with `ok`, because a guest can buy. On an older Arcade `signInAvailable` reads `true` for every player. For a sign-up gift or any guest-only prompt, read `gem.player.isGuest`. It is `true` only when the Arcade says the player is a guest who can sign up here. It is `false` for a full account, a surface with no sign-up, and an older Arcade that does not say, so no one with an account is offered a gift. It follows a guest who signs up in place.
54
+
55
+ ```ts
56
+ if (gem.player.isGuest) offerSignUpGift();
57
+ ```
58
+ - **Parent conformance kit is now version 9.** `PARENT_CONFORMANCE_VERSION` went from 8 to 9. If you maintain an Arcade parent or a surface that runs the kit, add the new `storeScenario` member (a `StoreScenario`) to your `ParentUnderTest` adapter. The kit adds six pass cases:
59
+ - a pass listed by slug;
60
+ - a pass not on sale;
61
+ - a pass sold to a guest and to a holder;
62
+ - a pass left out after two declined sheets;
63
+ - a minor's listing, which must be indistinguishable from not on sale.
64
+
65
+ Games themselves do not run the kit.
37
66
 
38
67
  ### Should change
39
68
 
40
- - GamePass.endsAt is deprecated on the browser, function and server halves. An exact end instant is a value two games could match to link one player's identities, so the platform will stop sending it and it will read null. Move to GamePass.daysLeft, which gives whole days remaining, capped at 7. A known guest is answered as not holding the pass. See [the deprecation policy](docs/deprecation-policy.md#4-what-is-deprecated-today).
69
+ - **`GamePass.endsAt` is deprecated; use `GamePass.daysLeft`.** An exact end instant could be matched across games to link one player's identities, so the platform will stop sending it and `endsAt` will read `null`. `daysLeft` is whole days, capped at 7. Show it as a countdown instead of computing from a timestamp.
41
70
 
42
- ### May stop
71
+ ```ts
72
+ // before
73
+ const ends = pass.endsAt; // Date-like instant
74
+
75
+ // after
76
+ if (pass.daysLeft !== undefined) label = `${pass.daysLeft} days left`;
77
+ ```
78
+
79
+ Check the `daysLeft` type in the reference for how an open-ended pass reads.
43
80
 
44
- - If your game works out expected rewards or the effect of a player's boost by hand, you can drop that arithmetic. The config preview now computes the multiplier, platform grants with their rounding, and bonus eligibility for you.
81
+ See [the deprecation policy](docs/deprecation-policy.md#4-what-is-deprecated-today).
45
82
 
46
83
  ### Rebuild
47
84
 
48
- - Types changed in two places. The store offers result now allows an optional pass and optional earn multipliers on each offer. The server match object gains playerPasses and playerBoost. Recompiling is enough, unless your code supplies its own stand-in for the match object, which then needs the two new members.
85
+ - **Types gained members; recompiling is enough.** `GemServer.match` and `GemMatchServer.match` gained `playerPasses` and `playerBoost`, and the offers result type gained optional `pass` and `earnMultipliers`. Existing code that only calls them compiles unchanged. Test doubles or fakes that implement `match` need the two new methods.
49
86
 
50
87
  ### New capability
51
88
 
52
- - Offers now say which pass they sell and which earn multipliers they carry. Each ArcadeOffer has an optional pass and optional earnMultipliers, so a game can show what an offer raises before the player buys it.
53
- - A game can sell a pass by its slug. StoreApi.passOnSale tells you whether the pass is on sale here, and StoreApi.showPass opens its sheet. The ShowPassOutcome it returns is ok only once a fresh read of the player's passes shows the pass held, and otherwise ok or pending as that type describes.
54
- - A game can preview what a config change would grant before applying it. You pass a config id, never an amount or a pass. The preview works out the player's multiplier, holdings and local offers, and reports grants, platform grants with a breakdown crediting a held pass or Boost, bonuses with eligibility and unmet requirements, and upsells for passes the player could buy. Rolls are reported as a count from a table, never an outcome. Errors and targets have their own types.
55
- - Earnings can be read from a receipt. The new earnings API turns a receipt's platform grant change into the same platform grant shape the preview uses, from the base amount granted and the boost source. It is available on the browser, on the function half, and on the server and match server. The function half resolves the currency from the invoking player's currencies, while the server half takes a currency id only.
56
- - A dedicated game server can now read a player's passes and Boost in a match. Use the match player passes read to gate on a pass, using the same GamePass shape as the browser. The match player Boost read returns a MatchPlayerBoost with its multipliers, for display only. Both reads go through optional runtime hooks, host.passes and host.boost, and an image without a hook, or a listen host, is refused by name. A refusal surfaces as an API error that keeps its original code.
89
+ - **Sell a pass by its slug.** `gem.store.showPass(slug)` opens the Arcade's store sheet for a pass and answers a `ShowPassOutcome`. It answers `ok` only once a fresh read of `gem.me.passes` shows the pass held; see `ShowPassOutcome` for the other outcomes, including `pending`. `gem.store.passOnSale(slug)` tells you whether the store sells that pass here, so you can hide a button when it does not. A guest can buy, and a minor's listing is indistinguishable from not on sale.
57
90
 
58
- ## 23.3.0
91
+ ```ts
92
+ if (await gem.store.passOnSale("season-pass")) {
93
+ const outcome = await gem.store.showPass("season-pass");
94
+ }
95
+ ```
96
+
97
+ Offers also carry `pass` (the slug of a pass they sell) and `earnMultipliers` (`ArcadeOfferEarnMultiplier`: a `currencySlug` and a `multiplierBp` in basis points). Use them to show what a pass or boost raises.
98
+ - **Preview what a config grants.** `gem.gameConfig.preview` takes a config id, either `{ recipe }` or `{ progression, add? }`, never an amount or a pass. The SDK works out the result from the loaded config and the player's boost, passes and the offers on sale here. The `ConfigPreview` lists:
99
+ - `grants`: items, game currencies, and rolls as "N rolls from {table}", never an outcome;
100
+ - `platformGrants`: `base`, `total` and a `breakdown` crediting the held pass or boost, with `on_complete` mappings reported as "if it completes";
101
+ - `bonuses`: a `BonusPreview` per bonus with `eligible: true | false | 'unknown'` and its `unmet` requirements;
102
+ - `upsells`: a `PassUpsell` for each pass on sale that the player does not hold, listed only when it would raise a payout or make a bonus eligible.
103
+
104
+ ```ts
105
+ const preview = await gem.gameConfig.preview({ recipe: "forge_sword" });
106
+ for (const up of preview.upsells) offerPass(up);
107
+ ```
108
+
109
+ Check `PreviewError` for refusals.
110
+ - **Read earnings from a receipt.** `gem.earnings.fromReceipt(change)` turns a receipt's `platform_grant` change into the same platform-grant shape the preview uses (`PlatformGrantPreview`, with a `PlatformGrantSource`), built from `base_granted` and the boost source. It is available in the browser, on the function half (the currency slug comes from the invoking player's currencies), and on the server half (by currency id only).
111
+
112
+ ```ts
113
+ const earned = gem.earnings.fromReceipt(receipt.changes[0]);
114
+ ```
115
+ - **Gate on a player's passes and boost from a dedicated server.** `gem.match.playerPasses(playerId)` answers the same `GamePass` shape as the browser. It is what a server should gate on. `gem.match.playerBoost(playerId)` answers a `MatchPlayerBoost` (made of `MatchBoostMultiplier`s) and is for display only.
116
+
117
+ ```ts
118
+ const passes = await gem.match.playerPasses(playerId);
119
+ const boost = await gem.match.playerBoost(playerId);
120
+ ```
121
+
122
+ A refusal arrives as a `GemApiError` that keeps the API's code, such as `client_permission_denied` or `player_not_in_match`. These reads go through the runtime hooks `host.passes(playerId)` and `host.boost(playerId)`, which answer a `GemServerHostRead` (`{ status, body }`). Hosts implement them optionally. A runtime image without them, or a listen host, refuses the call by name.
59
123
 
60
- *Written from the API facts alone; no summary was generated for this release.*
124
+ ### Fixes and improvements
125
+
126
+ - `gem.me.passes` now answers "not held" for a known guest without calling the server.
127
+ - A guest's `gem.store.offers` now answers `ok` on a current Arcade, since a guest can buy.
128
+
129
+ ## 23.3.0
61
130
 
62
131
  Upgrading from 23.2.0. Impact: required.
63
132
 
64
- ### Must change
133
+ This release adds `signInAvailable`, a signal that tells a game whether a sign-in ask can show the player anything, so the game can decide whether to draw a sign-in button or offer a sign-up gift. The API changes are additive: `gem.account` gains a value and a change listener, and the `onPlayer` callback gains an optional second argument. In the Gem Arcade app every player is signed in, so the signal is `false` there.
134
+
135
+ ### Rebuild
65
136
 
66
- - FrameCallbacks.onPlayer changed its signature.
67
- - FrameOptions.onPlayer changed its signature.
137
+ - The `onPlayer` callback now receives an optional second argument, `signInAvailable?: boolean`, in both `FrameOptions` and `FrameCallbacks`. Existing handlers that take only `player` still compile, so recompiling is enough. A handler that wants the value can take the second parameter:
138
+
139
+ ```ts
140
+ // before
141
+ onPlayer(player) { render(player); }
142
+
143
+ // after
144
+ onPlayer(player, signInAvailable) { render(player, signInAvailable); }
145
+ ```
68
146
 
69
147
  ### New capability
70
148
 
71
- - AccountApi.onSignInAvailableChange was added.
72
- - AccountApi.signInAvailable was added.
73
- - GemPlayer.signInAvailable was added.
74
- - SendPlayerInput.signInAvailable was added.
149
+ - `gem.account.signInAvailable` says whether asking the player to sign in can show anything. `gem.account.onSignInAvailableChange` lets a game react when that value changes.
150
+
151
+ The value is:
152
+
153
+ - `false` before a player has arrived.
154
+ - `false` wherever the parent has no `signIn`.
155
+ - `true` when the parent is older than the field, which is how `requestSignIn` already behaved there.
156
+
157
+ `requestSignIn` itself is unchanged. Check the signal before drawing a sign-in button:
158
+
159
+ ```ts
160
+ function updateSignInButton(available: boolean) {
161
+ signInButton.hidden = !available;
162
+ }
163
+
164
+ updateSignInButton(gem.account.signInAvailable);
165
+ gem.account.onSignInAvailableChange((available) => updateSignInButton(available));
166
+ ```
167
+ - The player object now carries an optional boolean `signInAvailable` (`GemPlayer.signInAvailable`), which is the value that `gem.account.signInAvailable` reads. `SendPlayerInput` accepts the same field.
168
+
169
+ In the Gem Arcade app every player is a full account, so `signInAvailable` is `false` there and a sign-in ask answers `signed_in` at once. To gate a sign-up gift, test `signInAvailable === true`.
170
+
171
+ On an older Arcade the signal reads `true` for every player. Until the Arcade is updated, combine it with the game's existing guest test:
172
+
173
+ ```ts
174
+ if (gem.account.signInAvailable === true && isGuest(player)) {
175
+ offerSignUpGift();
176
+ }
177
+ ```
75
178
 
76
179
  ## 23.2.0
77
180
 
78
- *Written from the API facts alone; no summary was generated for this release.*
181
+ Upgrading from 23.1.3. Impact: recommended.
79
182
 
80
- Upgrading from 23.1.3. Impact: none.
183
+ `rewardedStatus` now reports the Coin and Gem prices a rewarded placement can be unlocked with, as well as whether the ad can be watched. `grantable` now means the ad or any price can unlock the placement, so check any code that treats it as meaning an ad is available.
81
184
 
82
185
  ### New capability
83
186
 
84
- - RewardedOption was added.
85
- - RewardedStatus.adGrantable was added.
86
- - RewardedStatus.options was added.
187
+ - A rewarded placement can now also be unlocked for platform Coins or Gems. The status you get from `rewardedStatus` has two new fields:
188
+
189
+ - `adGrantable` is true when the ad alone can unlock the placement. Ad caps limit only this value.
190
+ - `options` is the list of `RewardedOption` entries, the prices configured for the placement. It says nothing about the current player.
191
+
192
+ `grantable` is now true when the ad or any price can unlock the placement. A price counts only when the Arcade reports it as grantable. If the Arcade reports prices but no ad information, `adGrantable` reads as false.
193
+
194
+ Use the new fields to label the button, for example:
195
+
196
+ ```ts
197
+ if (status.adGrantable) {
198
+ showWatchAdButton();
199
+ }
200
+ if (status.options.length > 0) {
201
+ showPriceButtons(status.options);
202
+ }
203
+ ```
204
+
205
+ Currencies and scope for placement prices are set with `gemctl platform config`.
206
+
207
+ ### Fixes and improvements
208
+
209
+ - `grantable` on the rewarded status now stays true when a Coin or Gem price can unlock the placement, even if the ad's caps have been reached. If your game used `grantable` to decide whether to show a "watch ad" button, switch to `adGrantable`.
87
210
 
88
211
  ## 23.1.3
89
212
 
@@ -105,23 +228,32 @@ Nothing in this release asks anything of a game.
105
228
 
106
229
  ## 23.1.0
107
230
 
108
- *Written from the API facts alone; no summary was generated for this release.*
109
-
110
231
  Upgrading from 23.0.6. Impact: required.
111
232
 
233
+ Games can now read the player's Arcade Text size and scale their UI to match by opting in with `onTextScale`. Games that don't opt in see no change. Anyone running the parent conformance kit must update their parent adapter for kit version 8.
234
+
112
235
  ### Must change
113
236
 
114
- - PARENT_CONFORMANCE_VERSION changed its signature.
115
- - GemCapability changed its signature.
116
- - GemHost.sendPrefs changed its signature.
117
- - GemPrefs.recommended changed its signature.
237
+ - The parent conformance kit moved from version 7 to 8 and adds a `setTextScale` member to the `ParentUnderTest` adapter. If you run the kit against a parent or host implementation, add `setTextScale` to your adapter so the three new text-scale cases can run. Games that don't run the kit have nothing to change.
238
+
239
+ ### Rebuild
240
+
241
+ - `GemCapability` gains the `"textScale"` member, and `GemPrefs.recommended` and the `GemHost.sendPrefs` argument gain an optional `textScale?: number`. Recompiling is enough for most games. Only code with an exhaustive `switch` or a `Record<GemCapability, …>` needs a `textScale` case or key added.
118
242
 
119
243
  ### New capability
120
244
 
121
- - ParentUnderTest.setTextScale was added.
122
- - Gem.textScale was added.
123
- - GemOptions.onTextScale was added.
124
- - GemRecommendedSettings.textScale was added.
245
+ - A game can now follow the player's Arcade Text size. The value arrives as `gem.prefs.recommended.textScale`, an optional integer from 100 to 200. Passing `onTextScale` to `createGem` opts in and advertises the new `textScale` capability. The callback fires on the first push and on every change, and `gem.textScale` holds the latest value. The SDK applies nothing itself, so the game must apply the value. The host only sends the value to games that advertise the capability.
246
+
247
+ ```ts
248
+ const gem = await createGem({
249
+ onTextScale: (scale) => {
250
+ // 100 = normal size; apply it to the root font size
251
+ document.documentElement.style.fontSize = `${scale}%`;
252
+ },
253
+ });
254
+ ```
255
+
256
+ For the scale to take effect, size your UI in `rem` units.
125
257
 
126
258
  ## 23.0.6
127
259
 
@@ -161,50 +293,65 @@ Nothing in this release asks anything of a game.
161
293
 
162
294
  ## 23.0.0
163
295
 
164
- *Written from the API facts alone; no summary was generated for this release.*
165
-
166
296
  Upgrading from 22.0.0. Impact: required.
167
297
 
298
+ This release adds a shared Friends window to the Arcade, opened with `gem.friends.inviteToPlay()` and `gem.friends.setInviteParams()`, and makes `gem.me.inventory.onChanged` fire for inventory changes the game did not make through the SDK. Games that only call the SDK need no code changes. Anyone who hosts games in their own parent page (a `GemHost` embedder) or runs the parent conformance kit must update for conformance version 7 and the wider message types.
299
+
168
300
  ### Must change
169
301
 
170
- - PARENT_CONFORMANCE_VERSION changed its signature.
171
- - GemFrame.requestArcade changed its signature.
172
- - FRAME_TO_PARENT changed its signature.
173
- - FrameMessage changed its signature.
174
- - GemCapability changed its signature.
175
- - PARENT_TO_FRAME changed its signature.
176
- - ParentMessage changed its signature.
177
-
178
- ### New capability
179
-
180
- - ParentFixture.channel was added.
181
- - ParentFixture.gameId was added.
182
- - ParentUnderTest.deliverSocket was added.
183
- - CharactersApi.refreshHeld was added.
184
- - FriendsApi.inviteToPlay was added.
185
- - FriendsApi.setInviteParams was added.
186
- - FriendsApiDependencies.inviteToPlay was added.
187
- - FriendsApiDependencies.setInviteParams was added.
188
- - PlayInviteRequest was added.
189
- - FrameCallbacks.onInventoryChanged was added.
190
- - FrameCallbacks.onPlayInviteResult was added.
191
- - FrameOptions.onInventoryChanged was added.
192
- - FrameOptions.onPlayInviteResult was added.
193
- - GemHost.answerPlayInvite was added.
194
- - GemHost.relayInventoryEvent was added.
195
- - GemHost.sendInventoryChanged was added.
196
- - GemInventoryChanged was added.
197
- - GemInviteParams was added.
198
- - GemPlayInviteResult was added.
199
- - GemRequestPlayInvite was added.
200
- - HostCallbacks.onInviteParams was added.
201
- - HostCallbacks.onRequestPlayInvite was added.
202
- - HostOptions.onInviteParams was added.
203
- - HostOptions.onRequestPlayInvite was added.
204
- - validateGemInventoryChanged was added.
205
- - validateGemInviteParams was added.
206
- - validateGemPlayInviteResult was added.
207
- - validateGemRequestPlayInvite was added.
302
+ - The parent conformance kit moves from version 6 to **7**. If you embed games with your own parent and run the kit, update your test setup:
303
+
304
+ - The fixture now names a game and a channel (`ParentFixture.gameId`, `ParentFixture.channel`). Use the values the kit gives you, and make your parent's inventory relay check the token's game and channel against them.
305
+ - Your adapter (`ParentUnderTest`) gains `deliverSocket`. The kit uses it to push socket events into your parent so the new inventory cases can run. Implement it so the socket event reaches the same relay path a real socket would.
306
+ - Five new inventory cases now run against every parent, including one checking that another game's inventory change is never relayed.
307
+
308
+ A parent that fails these cases does not pass conformance version 7.
309
+
310
+ ### May stop
311
+
312
+ - You can stop polling or manually re-reading inventory after a reward lands. Previously `gem.me.inventory.onChanged` fired only for writes made through the SDK, so games re-read inventory after `gem.functions.invoke`. When the Arcade relays `gem.inventoryChanged` (it must advertise the `inventoryChanged` capability), the SDK drops its cached inventory reads for `gem.me` and every character it holds, and `onChanged` fires. This covers grants from your function or server, purchases, gifts and changes from another device.
313
+
314
+ ### Rebuild
315
+
316
+ - The message unions and message-name lists grew. `FrameMessage` and `FRAME_TO_PARENT` gain `gem.requestPlayInvite` and `gem.inviteParams`. `ParentMessage` and `PARENT_TO_FRAME` gain `gem.inventoryChanged` and `gem.playInviteResult`. Recompiling is enough for most code. If you have an exhaustive `switch` over these unions, or a table keyed by the message names, add cases for the new messages, or the build will fail or the messages will be unhandled.
317
+ - `GemCapability` gains two members: `playInvites` and `inventoryChanged`. Recompiling is enough unless you have an exhaustive `switch` or a `Record<GemCapability, ...>`. In that case, add entries for the two new values.
318
+ - `GemFrame.requestArcade` now takes `GemArcadeAsk | GemFriendsWindowAsk` instead of `GemArcadeAsk`. Callers are unaffected and recompiling is enough. If you implement or wrap `GemFrame` yourself (for example in a test double), widen your `requestArcade` parameter to the new union.
319
+
320
+ ```ts
321
+ // before
322
+ requestArcade(msg: GemArcadeAsk): boolean
323
+ // after
324
+ requestArcade(msg: GemArcadeAsk | GemFriendsWindowAsk): boolean
325
+ ```
326
+
327
+ ### New capability
328
+
329
+ - A game can ask the Arcade to open its shared **Friends window**, and can declare the launch params that window carries when the player opens it from the Arcade menu. Share and friend invites now use the same launch params. Both calls need the `playInvites` capability.
330
+
331
+ ```ts
332
+ // On a player press (e.g. an "Invite friends" button):
333
+ const shown = await gem.friends.inviteToPlay({ params: { room: "ABCD" } });
334
+ // shown is false outside the Arcade, without the capability, without a user press,
335
+ // while another sheet is open, or on timeout. It never rejects.
336
+
337
+ // Standing declaration for the window opened from the Arcade menu:
338
+ gem.friends.setInviteParams({ room: "ABCD" });
339
+ gem.friends.setInviteParams(null); // clear it
340
+ ```
341
+
342
+ - `inviteToPlay({ params? })` returns `Promise<boolean>`. It is limited to 6 asks a minute.
343
+ - `setInviteParams(params | null)` is limited to 30 a minute. The SDK holds the value and re-sends it on every handshake. It re-checks the value against the parent's `shareRemap`.
344
+ - `params` has the same shape and rules as the params of `gem.share.request`. Where `gem.share.request` throws on invalid params, the two invite calls log a warning and send nothing.
345
+ - For embedders: a parent built on `GemHost` can now serve the Friends window and relay inventory changes.
346
+
347
+ - **Friends window**: pass `onRequestPlayInvite` and `onInviteParams` (in `HostOptions`/`HostCallbacks`) to handle the game's `gem.requestPlayInvite` and `gem.inviteParams` messages. Call `host.answerPlayInvite(...)` to send back `gem.playInviteResult { requestId, shown }`.
348
+ - **Inventory**: `GemHost` relays the game's `game_inventory_changed` socket event as `gem.inventoryChanged`. It crosses only after a token, only for the token's game and channel, and only to a frame that advertised the `inventoryChanged` capability. No fields are copied, and sends are coalesced to at most one every two seconds. If you parse the socket yourself, call `sendInventoryChanged` for the same send; `relayInventoryEvent` is also new on `GemHost`.
349
+ - **Validators**: `validateGemRequestPlayInvite`, `validateGemInviteParams`, `validateGemPlayInviteResult` and `validateGemInventoryChanged` check the new messages (`GemRequestPlayInvite`, `GemInviteParams`, `GemPlayInviteResult`, `GemInventoryChanged`).
350
+
351
+ ### Fixes and improvements
352
+
353
+ - `gem.me.inventory.onChanged` now fires when the game's inventory changes on the server (a grant from your function or server, a purchase, a gift, or a change from another device). Before, it fired only for writes made through the SDK, so games had to poll after `gem.functions.invoke` to see a reward land. This needs an Arcade that relays `gem.inventoryChanged`.
354
+ - A closed `GemHost` no longer sends a trailing `gem.inventoryChanged` after it has been closed.
208
355
 
209
356
  ## 22.0.0
210
357
 
@@ -345,30 +492,26 @@ Upgrading from 17.0.0. Impact: required.
345
492
 
346
493
  ## 17.0.0
347
494
 
348
- *Written from the API facts alone; no summary was generated for this release.*
349
-
350
495
  Upgrading from 16.3.1. Impact: required.
351
496
 
497
+ This release adds `gem.store.showOffer`, which lets a game point at a single SKU and have the Arcade draw the offer in its own sheet, for promotions whose pitch the bridge cannot carry (pass perks, sales, what a holder already has). Games that only use `gem.store.purchase` need no code changes beyond recompiling. If you run your own parent with `GemHost`, the conformance version moves to 3 and you should re-run the conformance kit.
498
+
352
499
  ### Must change
353
500
 
354
- - PARENT_CONFORMANCE_VERSION changed its signature.
355
- - FRAME_TO_PARENT changed its signature.
356
- - FrameMessage changed its signature.
357
- - GemCapability changed its signature.
358
- - PARENT_TO_FRAME changed its signature.
359
- - ParentMessage changed its signature.
501
+ - The parent conformance version went from `2` to `3`. If you embed games with `GemHost` (your own parent page) rather than only building a game for the Arcade, re-run the conformance kit: kit 3 adds three store-offer-sheet cases. Its checkout back-off now also holds a sheet back, so a game cannot get around a back-off by switching from a purchase request to a sheet request. A game that only runs inside the Arcade has nothing to change for this.
502
+
503
+ ### Rebuild
504
+
505
+ - The message and capability unions each gained a member: `GemCapability` now includes `"storeOfferSheet"`, `FrameMessage` includes `GemRequestStoreOfferSheet` (`"gem.requestStoreOfferSheet"` in `FRAME_TO_PARENT`), and `ParentMessage` includes `GemStoreOfferSheetResult` (`"gem.storeOfferSheetResult"` in `PARENT_TO_FRAME`). Recompiling is enough for most games. Only code that switches exhaustively over one of these unions, or over those constant lists, needs a new case for the added member.
360
506
 
361
507
  ### New capability
362
508
 
363
- - StoreApi.showOffer was added.
364
- - StoreApi.showOfferAvailable was added.
365
- - GemHost.answerStoreOfferSheet was added.
366
- - GemRequestStoreOfferSheet was added.
367
- - GemStoreOfferSheetResult was added.
368
- - HostCallbacks.onRequestStoreOfferSheet was added.
369
- - HostOptions.onRequestStoreOfferSheet was added.
370
- - validateGemRequestStoreOfferSheet was added.
371
- - validateGemStoreOfferSheetResult was added.
509
+ - `store.showOffer` hands the Arcade one SKU and the Arcade draws the offer in its own sheet. Use it when the pitch is something your game cannot draw faithfully: a pass's perks, a sale, or what a holder already has. The sheet's Buy button opens the same checkout as a normal purchase, and none of the pitch details cross to the game, so the game never redraws a promotion.
510
+
511
+ - Use `store.purchase` for an offer your game draws itself, and `store.showOffer` for the rest.
512
+ - `store.showOfferAvailable` is a synchronous check you can call first to see whether the Arcade will draw a sheet, so you can fall back or hide the entry point.
513
+ - `showOffer` shares `purchase`'s bounds and acting lock, and waits with no timeout, as `purchase` does.
514
+ - For parents built on `GemHost`, there is a new request and result pair for the offer sheet. Pass `onRequestStoreOfferSheet` in `HostOptions` (it is also on `HostCallbacks`) to receive a `GemRequestStoreOfferSheet`. When the player is done with the sheet, answer it with `GemHost.answerStoreOfferSheet`, which sends a `GemStoreOfferSheetResult` to the game. `validateGemRequestStoreOfferSheet` and `validateGemStoreOfferSheetResult` validate the two message shapes.
372
515
 
373
516
  ## 16.3.1
374
517
 
@@ -499,21 +642,42 @@ Upgrading from 15.0.0. Impact: none.
499
642
 
500
643
  ## 15.0.0
501
644
 
502
- *Written from the API facts alone; no summary was generated for this release.*
503
-
504
645
  Upgrading from 14.0.3. Impact: required.
505
646
 
506
- ### Must change
647
+ This release adds the Arcade's Music and Sound switches for games: pass `onAudioChannels` to `createGem` and the game is told separately when to play music and when to play sound effects. Nothing in the existing API is removed. The release is marked required because public types changed (`GemCapability`, `GemHost.sendPrefs`), so rebuild the game against it.
507
648
 
508
- - GemCapability changed its signature.
509
- - GemHost.sendPrefs changed its signature.
649
+ ### Rebuild
650
+
651
+ - Two public types changed in additive ways. Recompiling is enough for almost every game and no code change is needed.
652
+
653
+ - `GemCapability` gained the `"audioChannels"` token. Only a game that switches exhaustively over `GemCapability` (for example with a `never` check in the default branch) will fail to compile. Add a case for `"audioChannels"` there.
654
+ - `GemHost.sendPrefs` now accepts optional `music?: boolean` and `sounds?: boolean` in its `prefs` argument. Existing calls stay valid. This only matters to a game or test harness that implements or calls the host side.
510
655
 
511
656
  ### New capability
512
657
 
513
- - GemAudioChannels was added.
514
- - GemOptions.onAudioChannels was added.
515
- - GemPrefs.music was added.
516
- - GemPrefs.sounds was added.
658
+ - The Arcade's Gem menu has Music and Sound switches. A game can now honour them by passing `onAudioChannels` to `createGem`. The SDK cannot split a game's audio itself, so it advertises the `audioChannels` capability only when the game passes this handler.
659
+
660
+ ```ts
661
+ const gem = createGem({
662
+ onAudioChannels: ({ music, sounds }: GemAudioChannels) => {
663
+ musicBus.setEnabled(music);
664
+ sfxBus.setEnabled(sounds);
665
+ },
666
+ });
667
+ ```
668
+
669
+ Behaviour:
670
+
671
+ - The handler is called on the first `gem.prefs` and again whenever either value changes.
672
+ - `gem.prefs.music` and `gem.prefs.sounds` are always present and default to `true`, including when the Arcade does not send them.
673
+ - `muted` stays the master mute, which the SDK enforces. It is not folded into `music` or `sounds`, so check both if you need the effective state.
674
+ - The new `GemAudioChannels` type describes the `{ music, sounds }` argument.
675
+
676
+ A game that does not pass `onAudioChannels` keeps working. The Arcade falls back to setting `muted: true` while both switches are off. It drives Music through the game's declared `icon: 'music'` setting (`gem.settingChange`) where there is one.
677
+
678
+ ### Fixes and improvements
679
+
680
+ - The Gem menu's quick-row Music switch previously worked only for games that declared an `icon: 'music'` setting, and Sound was only the master mute. Both are now independent switches that every game can honour through `onAudioChannels`.
517
681
 
518
682
  ## 14.0.3
519
683
 
@@ -865,33 +1029,57 @@ Upgrading from 8.0.0. Impact: required.
865
1029
 
866
1030
  ## 8.0.0
867
1031
 
868
- *Written from the API facts alone; no summary was generated for this release.*
869
-
870
1032
  Upgrading from 7.9.1. Impact: required.
871
1033
 
872
- ### Must change
1034
+ This release adds `gem.vibration`, which vibrates on iPhones and other browsers without `navigator.vibrate`, and `gem.friends` reads for the friend challenges a player received. Rebuild your game against the new types: `GemCapability`, `FrameMessage` and `BucketConfig` all gained members, which can break exhaustive code.
1035
+
1036
+ ### Rebuild
1037
+
1038
+ - `GemCapability` gains `"vibration"`, `FrameMessage` gains `GemVibrate`, and `FRAME_TO_PARENT` gains `"gem.vibrate"`. Recompiling is enough for most games. If you have an exhaustive `switch` over these unions, or a `Record<GemCapability, …>`, add a case or entry for the new member. If you use `FRAME_TO_PARENT` as a list of known message types, expect one more entry.
1039
+ - `BucketConfig` gains an optional `max_balance?: number | null`, a per-bucket holding cap that is omitted when there is none. Recompiling is enough. Code that reads bucket configs should treat a missing or `null` value as no cap.
1040
+
1041
+ ### New capability
1042
+
1043
+ - **Read friend challenges a player received.** `gem.friends` gains three calls for the receiving side of friend challenges:
1044
+
1045
+ - `gem.friends.launchedChallenge(opts?)` resolves to the `FriendChallenge` the player launched your game from, or `null` for an ordinary launch.
1046
+ - `gem.friends.inbox(opts?)` resolves to a `FriendChallengesPage` of every challenge the player received, paged with an opaque cursor like `list`. Answered challenges are included and sorted last, so you can recognise a relaunch.
1047
+ - `gem.friends.markAnswered(id)` marks a challenge answered. It is idempotent.
1048
+
1049
+ ```ts
1050
+ const gem = await createGem();
1051
+ const challenge = await gem.friends.launchedChallenge();
1052
+ if (challenge && !challenge.answeredAt) {
1053
+ // start the challenge round, then when the player finishes:
1054
+ await gem.friends.markAnswered(challenge.id);
1055
+ }
1056
+ ```
1057
+
1058
+ Things to know:
1059
+
1060
+ - `createGem` reads `gemChallenge` from `location.search` when it runs. If your game reads its own launch keys and cleans the URL with `history.replaceState`, do that **after** `createGem`, otherwise `launchedChallenge` silently resolves `null`. Use `launchedChallenge` instead of reading the URL yourself.
1061
+ - `launchedChallenge` never rejects for an ordinary launch. It resolves `null` for no challenge key, a value that is not a UUID, or the platform's uniform not-found answer (`inbox_item_not_found`), as with a shared link or guest launch. Any other 404 still rejects.
1062
+ - `launchedChallenge` is not cached. Check `answeredAt` on each call to tell a replay apart.
1063
+ - `markAnswered` rejects with `not_found` for an unknown challenge or someone else's, and with `invalid` locally for a value that cannot be an id.
1064
+ - `limit` in `inbox` and `list` must be a non-negative integer. A negative, fractional or `NaN` limit is refused with `invalid`.
1065
+ - Challenge `params` come from another player, so check each one before using it.
1066
+ - **Vibrate on every platform that can.** `navigator.vibrate` is missing in every WebKit browser, including the Gem Arcade iOS app, and in Firefox 129+. `gem.vibration` is a drop-in for it, taking the same pattern and returning the same boolean.
1067
+
1068
+ ```ts
1069
+ if (gem.vibration.available) {
1070
+ const ok = gem.vibration.vibrate([30, 40, 30]);
1071
+ }
1072
+ ```
1073
+
1074
+ The SDK picks the route by feature detection. If `navigator.vibrate` exists and the device has a coarse pointer, it vibrates locally in the frame. Otherwise it asks the Arcade, if the Arcade supports vibration. If neither works, `available` is `false` and `vibrate` returns `false`.
1075
+
1076
+ The same rules apply on both routes:
1077
+
1078
+ - The frame must have had user activation.
1079
+ - A pattern has at most 99 entries, each 0 to 10 000 ms, and a 10 s total.
1080
+ - At most 600 calls per minute are allowed.
873
1081
 
874
- - BucketConfig changed its signature.
875
- - FRAME_TO_PARENT changed its signature.
876
- - FrameMessage changed its signature.
877
- - GemCapability changed its signature.
878
-
879
- ### New capability
880
-
881
- - FriendChallenge was added.
882
- - FriendChallengesPage was added.
883
- - FriendsApi.inbox was added.
884
- - FriendsApi.launchedChallenge was added.
885
- - FriendsApi.markAnswered was added.
886
- - FriendsApiDependencies.launchedChallengeId was added.
887
- - ListFriendChallengesOptions was added.
888
- - Gem.vibration was added.
889
- - GemFrame.requestVibrate was added.
890
- - VibrationApi was added.
891
- - GemVibrate was added.
892
- - HostCallbacks.onVibrate was added.
893
- - HostOptions.onVibrate was added.
894
- - validateGemVibrate was added.
1082
+ A refused call returns `false` and logs one warning per session through the game's raw log option. `GemFrame.requestVibrate` is the low-level message call behind it. Most games should use `gem.vibration`.
895
1083
 
896
1084
  ## 7.9.1
897
1085