@indiegems/gem-web-sdk 24.0.1 → 24.1.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.
package/docs/changelog.md CHANGED
@@ -17,6 +17,38 @@ 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.1.0
21
+
22
+ Upgrading from 24.0.2. Impact: required.
23
+
24
+ Declining a pass's sheet no longer takes the pass off sale, so a game may open a pass's sheet as often as it likes. A held pass may or may not be offered again, so a game should follow `passOnSale` and assume neither. The only API change is the parent conformance kit version, which moves from 9 to 10 and asks a game to change nothing beyond a rebuild.
25
+
26
+ ### Rebuild
27
+
28
+ - `PARENT_CONFORMANCE_VERSION` is now `10` instead of `9`. This constant belongs to the parent conformance kit, which Arcade parents use. A game does not use it, and no message, field, outcome or offer kind was added. Recompiling is enough, and no adapter member is new. A parent that passed kit 9 passes kit 10 unchanged.
29
+
30
+ Only code that compares this constant to the literal `9` needs updating:
31
+
32
+ ```ts
33
+ // before
34
+ if (PARENT_CONFORMANCE_VERSION === 9) { /* ... */ }
35
+ // after
36
+ if (PARENT_CONFORMANCE_VERSION === 10) { /* ... */ }
37
+ ```
38
+
39
+ ### Fixes and improvements
40
+
41
+ - Declining a pass's sheet no longer takes the pass off sale. `passOnSale` and the reward preview's `upsells` no longer change after declines, and there is no daily limit on pass offers. The Arcade now records how often a sheet is opened and how long it stays open, for reporting only. Remove any game logic that assumed a pass would stop being offered after two declines.
42
+ - Whether a player who already holds a pass is offered it again is the parent's decision. A pass left out of the holder's listing answers `gem.requestStoreOfferSheet` with one `unavailable` and shows nothing. A pass in the listing opens exactly one sheet. Follow `passOnSale` for a holder as you would for anyone. Do not keep a "Get VIP" button up for a holder on the assumption that its sheet will open, and do not assume a pass lasts 30 days.
43
+
44
+ ## 24.0.2
45
+
46
+ Upgrading from 24.0.1. Impact: none.
47
+
48
+ 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.
49
+
50
+ Nothing in this release asks anything of a game.
51
+
20
52
  ## 24.0.1
21
53
 
22
54
  Upgrading from 24.0.0. Impact: none.
@@ -29,61 +61,176 @@ Nothing in this release asks anything of a game.
29
61
 
30
62
  Upgrading from 23.3.0. Impact: required.
31
63
 
64
+ 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.
65
+
32
66
  ### Must change
33
67
 
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.
68
+ - **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.
69
+
70
+ ```ts
71
+ // before
72
+ if (offer.kind === "gem_pack") showPack(offer);
73
+
74
+ // after
75
+ if (offer.kind === "gem_pack" || offer.kind === "coin_pack") showPack(offer);
76
+ ```
77
+ - **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.
78
+
79
+ ```ts
80
+ if (gem.player.isGuest) offerSignUpGift();
81
+ ```
82
+ - **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:
83
+ - a pass listed by slug;
84
+ - a pass not on sale;
85
+ - a pass sold to a guest and to a holder;
86
+ - a pass left out after two declined sheets;
87
+ - a minor's listing, which must be indistinguishable from not on sale.
88
+
89
+ Games themselves do not run the kit.
37
90
 
38
91
  ### Should change
39
92
 
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](deprecation-policy.md#4-what-is-deprecated-today).
93
+ - **`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
94
 
42
- ### May stop
95
+ ```ts
96
+ // before
97
+ const ends = pass.endsAt; // Date-like instant
98
+
99
+ // after
100
+ if (pass.daysLeft !== undefined) label = `${pass.daysLeft} days left`;
101
+ ```
43
102
 
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.
103
+ Check the `daysLeft` type in the reference for how an open-ended pass reads.
104
+
105
+ See [the deprecation policy](deprecation-policy.md#4-what-is-deprecated-today).
45
106
 
46
107
  ### Rebuild
47
108
 
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.
109
+ - **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
110
 
50
111
  ### New capability
51
112
 
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.
113
+ - **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
114
 
58
- ## 23.3.0
115
+ ```ts
116
+ if (await gem.store.passOnSale("season-pass")) {
117
+ const outcome = await gem.store.showPass("season-pass");
118
+ }
119
+ ```
120
+
121
+ 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.
122
+ - **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:
123
+ - `grants`: items, game currencies, and rolls as "N rolls from {table}", never an outcome;
124
+ - `platformGrants`: `base`, `total` and a `breakdown` crediting the held pass or boost, with `on_complete` mappings reported as "if it completes";
125
+ - `bonuses`: a `BonusPreview` per bonus with `eligible: true | false | 'unknown'` and its `unmet` requirements;
126
+ - `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.
127
+
128
+ ```ts
129
+ const preview = await gem.gameConfig.preview({ recipe: "forge_sword" });
130
+ for (const up of preview.upsells) offerPass(up);
131
+ ```
132
+
133
+ Check `PreviewError` for refusals.
134
+ - **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).
135
+
136
+ ```ts
137
+ const earned = gem.earnings.fromReceipt(receipt.changes[0]);
138
+ ```
139
+ - **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.
140
+
141
+ ```ts
142
+ const passes = await gem.match.playerPasses(playerId);
143
+ const boost = await gem.match.playerBoost(playerId);
144
+ ```
59
145
 
60
- *Written from the API facts alone; no summary was generated for this release.*
146
+ 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.
147
+
148
+ ### Fixes and improvements
149
+
150
+ - `gem.me.passes` now answers "not held" for a known guest without calling the server.
151
+ - A guest's `gem.store.offers` now answers `ok` on a current Arcade, since a guest can buy.
152
+
153
+ ## 23.3.0
61
154
 
62
155
  Upgrading from 23.2.0. Impact: required.
63
156
 
64
- ### Must change
157
+ 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.
158
+
159
+ ### Rebuild
65
160
 
66
- - FrameCallbacks.onPlayer changed its signature.
67
- - FrameOptions.onPlayer changed its signature.
161
+ - 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:
162
+
163
+ ```ts
164
+ // before
165
+ onPlayer(player) { render(player); }
166
+
167
+ // after
168
+ onPlayer(player, signInAvailable) { render(player, signInAvailable); }
169
+ ```
68
170
 
69
171
  ### New capability
70
172
 
71
- - AccountApi.onSignInAvailableChange was added.
72
- - AccountApi.signInAvailable was added.
73
- - GemPlayer.signInAvailable was added.
74
- - SendPlayerInput.signInAvailable was added.
173
+ - `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.
174
+
175
+ The value is:
176
+
177
+ - `false` before a player has arrived.
178
+ - `false` wherever the parent has no `signIn`.
179
+ - `true` when the parent is older than the field, which is how `requestSignIn` already behaved there.
180
+
181
+ `requestSignIn` itself is unchanged. Check the signal before drawing a sign-in button:
182
+
183
+ ```ts
184
+ function updateSignInButton(available: boolean) {
185
+ signInButton.hidden = !available;
186
+ }
187
+
188
+ updateSignInButton(gem.account.signInAvailable);
189
+ gem.account.onSignInAvailableChange((available) => updateSignInButton(available));
190
+ ```
191
+ - 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.
192
+
193
+ 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`.
194
+
195
+ 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:
196
+
197
+ ```ts
198
+ if (gem.account.signInAvailable === true && isGuest(player)) {
199
+ offerSignUpGift();
200
+ }
201
+ ```
75
202
 
76
203
  ## 23.2.0
77
204
 
78
- *Written from the API facts alone; no summary was generated for this release.*
205
+ Upgrading from 23.1.3. Impact: recommended.
79
206
 
80
- Upgrading from 23.1.3. Impact: none.
207
+ `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
208
 
82
209
  ### New capability
83
210
 
84
- - RewardedOption was added.
85
- - RewardedStatus.adGrantable was added.
86
- - RewardedStatus.options was added.
211
+ - A rewarded placement can now also be unlocked for platform Coins or Gems. The status you get from `rewardedStatus` has two new fields:
212
+
213
+ - `adGrantable` is true when the ad alone can unlock the placement. Ad caps limit only this value.
214
+ - `options` is the list of `RewardedOption` entries, the prices configured for the placement. It says nothing about the current player.
215
+
216
+ `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.
217
+
218
+ Use the new fields to label the button, for example:
219
+
220
+ ```ts
221
+ if (status.adGrantable) {
222
+ showWatchAdButton();
223
+ }
224
+ if (status.options.length > 0) {
225
+ showPriceButtons(status.options);
226
+ }
227
+ ```
228
+
229
+ Currencies and scope for placement prices are set with `gemctl platform config`.
230
+
231
+ ### Fixes and improvements
232
+
233
+ - `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
234
 
88
235
  ## 23.1.3
89
236
 
@@ -105,23 +252,32 @@ Nothing in this release asks anything of a game.
105
252
 
106
253
  ## 23.1.0
107
254
 
108
- *Written from the API facts alone; no summary was generated for this release.*
109
-
110
255
  Upgrading from 23.0.6. Impact: required.
111
256
 
257
+ 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.
258
+
112
259
  ### Must change
113
260
 
114
- - PARENT_CONFORMANCE_VERSION changed its signature.
115
- - GemCapability changed its signature.
116
- - GemHost.sendPrefs changed its signature.
117
- - GemPrefs.recommended changed its signature.
261
+ - 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.
262
+
263
+ ### Rebuild
264
+
265
+ - `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
266
 
119
267
  ### New capability
120
268
 
121
- - ParentUnderTest.setTextScale was added.
122
- - Gem.textScale was added.
123
- - GemOptions.onTextScale was added.
124
- - GemRecommendedSettings.textScale was added.
269
+ - 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.
270
+
271
+ ```ts
272
+ const gem = await createGem({
273
+ onTextScale: (scale) => {
274
+ // 100 = normal size; apply it to the root font size
275
+ document.documentElement.style.fontSize = `${scale}%`;
276
+ },
277
+ });
278
+ ```
279
+
280
+ For the scale to take effect, size your UI in `rem` units.
125
281
 
126
282
  ## 23.0.6
127
283
 
@@ -161,50 +317,65 @@ Nothing in this release asks anything of a game.
161
317
 
162
318
  ## 23.0.0
163
319
 
164
- *Written from the API facts alone; no summary was generated for this release.*
165
-
166
320
  Upgrading from 22.0.0. Impact: required.
167
321
 
322
+ 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.
323
+
168
324
  ### Must change
169
325
 
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.
326
+ - 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:
327
+
328
+ - 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.
329
+ - 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.
330
+ - Five new inventory cases now run against every parent, including one checking that another game's inventory change is never relayed.
331
+
332
+ A parent that fails these cases does not pass conformance version 7.
333
+
334
+ ### May stop
335
+
336
+ - 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.
337
+
338
+ ### Rebuild
339
+
340
+ - 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.
341
+ - `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.
342
+ - `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.
343
+
344
+ ```ts
345
+ // before
346
+ requestArcade(msg: GemArcadeAsk): boolean
347
+ // after
348
+ requestArcade(msg: GemArcadeAsk | GemFriendsWindowAsk): boolean
349
+ ```
350
+
351
+ ### New capability
352
+
353
+ - 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.
354
+
355
+ ```ts
356
+ // On a player press (e.g. an "Invite friends" button):
357
+ const shown = await gem.friends.inviteToPlay({ params: { room: "ABCD" } });
358
+ // shown is false outside the Arcade, without the capability, without a user press,
359
+ // while another sheet is open, or on timeout. It never rejects.
360
+
361
+ // Standing declaration for the window opened from the Arcade menu:
362
+ gem.friends.setInviteParams({ room: "ABCD" });
363
+ gem.friends.setInviteParams(null); // clear it
364
+ ```
365
+
366
+ - `inviteToPlay({ params? })` returns `Promise<boolean>`. It is limited to 6 asks a minute.
367
+ - `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`.
368
+ - `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.
369
+ - For embedders: a parent built on `GemHost` can now serve the Friends window and relay inventory changes.
370
+
371
+ - **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 }`.
372
+ - **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`.
373
+ - **Validators**: `validateGemRequestPlayInvite`, `validateGemInviteParams`, `validateGemPlayInviteResult` and `validateGemInventoryChanged` check the new messages (`GemRequestPlayInvite`, `GemInviteParams`, `GemPlayInviteResult`, `GemInventoryChanged`).
374
+
375
+ ### Fixes and improvements
376
+
377
+ - `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`.
378
+ - A closed `GemHost` no longer sends a trailing `gem.inventoryChanged` after it has been closed.
208
379
 
209
380
  ## 22.0.0
210
381
 
@@ -345,30 +516,26 @@ Upgrading from 17.0.0. Impact: required.
345
516
 
346
517
  ## 17.0.0
347
518
 
348
- *Written from the API facts alone; no summary was generated for this release.*
349
-
350
519
  Upgrading from 16.3.1. Impact: required.
351
520
 
521
+ 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.
522
+
352
523
  ### Must change
353
524
 
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.
525
+ - 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.
526
+
527
+ ### Rebuild
528
+
529
+ - 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
530
 
361
531
  ### New capability
362
532
 
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.
533
+ - `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.
534
+
535
+ - Use `store.purchase` for an offer your game draws itself, and `store.showOffer` for the rest.
536
+ - `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.
537
+ - `showOffer` shares `purchase`'s bounds and acting lock, and waits with no timeout, as `purchase` does.
538
+ - 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
539
 
373
540
  ## 16.3.1
374
541
 
@@ -499,21 +666,42 @@ Upgrading from 15.0.0. Impact: none.
499
666
 
500
667
  ## 15.0.0
501
668
 
502
- *Written from the API facts alone; no summary was generated for this release.*
503
-
504
669
  Upgrading from 14.0.3. Impact: required.
505
670
 
506
- ### Must change
671
+ 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
672
 
508
- - GemCapability changed its signature.
509
- - GemHost.sendPrefs changed its signature.
673
+ ### Rebuild
674
+
675
+ - Two public types changed in additive ways. Recompiling is enough for almost every game and no code change is needed.
676
+
677
+ - `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.
678
+ - `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
679
 
511
680
  ### New capability
512
681
 
513
- - GemAudioChannels was added.
514
- - GemOptions.onAudioChannels was added.
515
- - GemPrefs.music was added.
516
- - GemPrefs.sounds was added.
682
+ - 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.
683
+
684
+ ```ts
685
+ const gem = createGem({
686
+ onAudioChannels: ({ music, sounds }: GemAudioChannels) => {
687
+ musicBus.setEnabled(music);
688
+ sfxBus.setEnabled(sounds);
689
+ },
690
+ });
691
+ ```
692
+
693
+ Behaviour:
694
+
695
+ - The handler is called on the first `gem.prefs` and again whenever either value changes.
696
+ - `gem.prefs.music` and `gem.prefs.sounds` are always present and default to `true`, including when the Arcade does not send them.
697
+ - `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.
698
+ - The new `GemAudioChannels` type describes the `{ music, sounds }` argument.
699
+
700
+ 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.
701
+
702
+ ### Fixes and improvements
703
+
704
+ - 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
705
 
518
706
  ## 14.0.3
519
707
 
@@ -865,33 +1053,57 @@ Upgrading from 8.0.0. Impact: required.
865
1053
 
866
1054
  ## 8.0.0
867
1055
 
868
- *Written from the API facts alone; no summary was generated for this release.*
869
-
870
1056
  Upgrading from 7.9.1. Impact: required.
871
1057
 
872
- ### Must change
1058
+ 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.
1059
+
1060
+ ### Rebuild
1061
+
1062
+ - `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.
1063
+ - `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.
1064
+
1065
+ ### New capability
1066
+
1067
+ - **Read friend challenges a player received.** `gem.friends` gains three calls for the receiving side of friend challenges:
1068
+
1069
+ - `gem.friends.launchedChallenge(opts?)` resolves to the `FriendChallenge` the player launched your game from, or `null` for an ordinary launch.
1070
+ - `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.
1071
+ - `gem.friends.markAnswered(id)` marks a challenge answered. It is idempotent.
1072
+
1073
+ ```ts
1074
+ const gem = await createGem();
1075
+ const challenge = await gem.friends.launchedChallenge();
1076
+ if (challenge && !challenge.answeredAt) {
1077
+ // start the challenge round, then when the player finishes:
1078
+ await gem.friends.markAnswered(challenge.id);
1079
+ }
1080
+ ```
1081
+
1082
+ Things to know:
1083
+
1084
+ - `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.
1085
+ - `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.
1086
+ - `launchedChallenge` is not cached. Check `answeredAt` on each call to tell a replay apart.
1087
+ - `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.
1088
+ - `limit` in `inbox` and `list` must be a non-negative integer. A negative, fractional or `NaN` limit is refused with `invalid`.
1089
+ - Challenge `params` come from another player, so check each one before using it.
1090
+ - **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.
1091
+
1092
+ ```ts
1093
+ if (gem.vibration.available) {
1094
+ const ok = gem.vibration.vibrate([30, 40, 30]);
1095
+ }
1096
+ ```
1097
+
1098
+ 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`.
1099
+
1100
+ The same rules apply on both routes:
1101
+
1102
+ - The frame must have had user activation.
1103
+ - A pattern has at most 99 entries, each 0 to 10 000 ms, and a 10 s total.
1104
+ - At most 600 calls per minute are allowed.
873
1105
 
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.
1106
+ 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
1107
 
896
1108
  ## 7.9.1
897
1109
 
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "24.0.1"
53
+ "@indiegems/gem-web-sdk": "24.1.0"
54
54
  }
55
55
  ```
56
56