@indiegems/gem-web-sdk 22.0.0 → 23.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
  "schema_version": 2,
3
3
  "source": {
4
4
  "repo": "IndieGems/gem-web-sdk",
5
- "version": "22.0.0"
5
+ "version": "23.0.1"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -1155,7 +1155,7 @@
1155
1155
  "group": "saves",
1156
1156
  "signature": "refresh(): void",
1157
1157
  "documented": true,
1158
- "summary": "Throw away what is cached, so the next [InventoryApi.snapshot](api-reference-game.md#inventoryapi-snapshot) really\nasks.\n\nFor when something you did not do changed the inventory — a purchase on\nanother device, a grant from a server. The SDK already drops the cache\nitself after any change it made.",
1158
+ "summary": "Throw away what is cached, so the next [InventoryApi.snapshot](api-reference-game.md#inventoryapi-snapshot) really\nasks.\n\nFor when something you did not do changed the inventory — a purchase on\nanother device, a grant from a server — and nothing told you. The SDK\nalready drops the cache itself after any change it made, and on an\nArcade's hint that this game's inventory changed on the server.",
1159
1159
  "deprecated": null,
1160
1160
  "params": [],
1161
1161
  "returns": null,
@@ -1171,7 +1171,7 @@
1171
1171
  "group": "saves",
1172
1172
  "signature": "onChanged(listener: () => void): () => void",
1173
1173
  "documented": true,
1174
- "summary": "Run something whenever this inventory changes, and get back a function that\nstops listening.\n\n⚠️ LOCAL CHANGES ONLY — this fires for changes THIS game made through this\nSDK. It does NOT fire when an inventory changes anywhere else: another\ndevice, a game server, an admin tool, or a gift from another player. Do not\nbuild \"keep the bag live\" on it; use it to react to your own writes, and\ncall [InventoryApi.refresh](api-reference-game.md#inventoryapi-refresh) when you have reason to think something\nelse moved.",
1174
+ "summary": "Run something whenever this inventory changes, and get back a function that\nstops listening.\n\nFires for every change THIS game made through this SDK, and — on an Arcade\nthat relays it — for any change to this game's inventory on the server: a\ngrant from your function or server, a purchase, a gift accepted, a support\ngrant, another device. It is a cue to re-read, not a receipt: it carries\nnothing, so call [InventoryApi.snapshot](api-reference-game.md#inventoryapi-snapshot) for what changed. After your\nown function's grant, show the function's answer and let this redraw.\n\nAn older Arcade sends no hint, and there it fires for your own writes\nonly. Keep a fallback that does not depend on it: refresh on\n`visibilitychange` to `visible` and after any action that could involve\nanother player. Do not poll.",
1175
1175
  "deprecated": null,
1176
1176
  "params": [
1177
1177
  {
@@ -10095,6 +10095,38 @@
10095
10095
  "defaultValue": null,
10096
10096
  "guides": []
10097
10097
  },
10098
+ {
10099
+ "id": "PlayInviteRequest",
10100
+ "name": "PlayInviteRequest",
10101
+ "anchor": "playinviterequest",
10102
+ "group": "friends",
10103
+ "signature": "interface PlayInviteRequest",
10104
+ "documented": true,
10105
+ "summary": "What `gem.friends.inviteToPlay` takes.",
10106
+ "deprecated": null,
10107
+ "params": [],
10108
+ "returns": null,
10109
+ "throws": null,
10110
+ "example": null,
10111
+ "defaultValue": null,
10112
+ "guides": []
10113
+ },
10114
+ {
10115
+ "id": "PlayInviteRequest.params",
10116
+ "name": "params",
10117
+ "anchor": "playinviterequest-params",
10118
+ "group": "friends",
10119
+ "signature": "readonly params?: Readonly<Record<string, string>>",
10120
+ "documented": true,
10121
+ "summary": "Your own launch parameters for what the Arcade's link and invites open —\n`{ room: joinCode }` for a room joined by code — exactly as they should\nreach your game, on `gem.share.request`'s rules: at most 16, keys a letter\nthen up to 63 letters, digits, `_` or `-`, string values of at most 512\ncharacters. For this one opening, they replace what\n`gem.friends.setInviteParams` declared. Omit it to use that declaration.",
10122
+ "deprecated": null,
10123
+ "params": [],
10124
+ "returns": null,
10125
+ "throws": null,
10126
+ "example": null,
10127
+ "defaultValue": null,
10128
+ "guides": []
10129
+ },
10098
10130
  {
10099
10131
  "id": "FriendsApi",
10100
10132
  "name": "FriendsApi",
@@ -10157,6 +10189,52 @@
10157
10189
  "defaultValue": null,
10158
10190
  "guides": []
10159
10191
  },
10192
+ {
10193
+ "id": "FriendsApi.inviteToPlay",
10194
+ "name": "inviteToPlay",
10195
+ "anchor": "friendsapi-invitetoplay",
10196
+ "group": "friends",
10197
+ "signature": "inviteToPlay(request?: PlayInviteRequest): Promise<boolean>",
10198
+ "documented": true,
10199
+ "summary": "Ask the Arcade to open ITS invite sheet, so the player can invite friends\nto come and play this game with them.\n\nThe Arcade lists the player's friends in its own chrome and sends an\ninvite only when the player presses Invite there. A friend who accepts is\nopened into this game by the Arcade, and their quick-join seats them in\nthe player's room when there is a seat: your game makes no call for it,\nand reads them as a room member who shares the player's `groupId`. Your\ngame names nobody and writes no text.\n\nAn Arcade that does not offer the sheet answers `false` at once, and asks\nnothing. Hide the button on `gem.frame.parentSupports('playInvites')`\nrather than letting a player press one that cannot work.\n\n⚠️ **Call it ONLY from a player's press**, never on a timer or as a screen\nopens. The Arcade answers an ask with no press behind it `false`, and\nshows nothing.\n\nRESOLVES `true` when the Arcade opened its sheet, and `false` when it did\nnot: outside the Arcade, without `playInvites`, with another sheet\nalready open, without a press, or when no answer arrived. **That is all it\nsays** — never whom the player invited, how many, or whether any invite\nwas sent — so never gate a reward on it. Never rejects.\n\nCalls made while an ask is open share its answer.\n\n**The window's share link and its invites are one system.** Pass\n`params` — your own launch parameters, `{ room: joinCode }` for a room\njoined by code — and a friend who accepts and anyone who opens the link\narrive with them, on `gem.share.request`'s rules. Omitted, the window uses\nwhat [FriendsApi.setInviteParams](api-reference-game.md#friendsapi-setinviteparams) declared. A param those rules\nrefuse is said on the console and nothing is asked: the call resolves\n`false`.",
10200
+ "deprecated": null,
10201
+ "params": [
10202
+ {
10203
+ "name": "request",
10204
+ "type": "PlayInviteRequest",
10205
+ "optional": true,
10206
+ "summary": null
10207
+ }
10208
+ ],
10209
+ "returns": null,
10210
+ "throws": null,
10211
+ "example": "```ts\ninviteButton.onclick = async () => {\n const shown = await gem.friends.inviteToPlay({ params: { room: joinCode } });\n if (!shown) showJoinCode(); // the Arcade's sheet did not open: your own fallback\n};\n```",
10212
+ "defaultValue": null,
10213
+ "guides": []
10214
+ },
10215
+ {
10216
+ "id": "FriendsApi.setInviteParams",
10217
+ "name": "setInviteParams",
10218
+ "anchor": "friendsapi-setinviteparams",
10219
+ "group": "friends",
10220
+ "signature": "setInviteParams(params: Readonly<Record<string, string>> | null): boolean",
10221
+ "documented": true,
10222
+ "summary": "Say what the Arcade's Friends window opens when the PLAYER opens it from\nthe Arcade's own menu over your game: your launch parameters, carried by\nits share link and its friend invites alike, on `gem.share.request`'s\nrules. Pass `null` to clear.\n\nCall it when the player enters a room joined by code —\n`setInviteParams({ room: joinCode })` — and `setInviteParams(null)` when\nthey leave it, so the window never invites anyone into a room that is\ngone. The latest call stands until the next; the SDK holds it and says it\nagain after a reconnect. An explicit `inviteToPlay({ params })` wins for\nthat one opening. Nothing comes back, and nothing changes on screen.",
10223
+ "deprecated": null,
10224
+ "params": [
10225
+ {
10226
+ "name": "params",
10227
+ "type": "Readonly<Record<string, string>> | null",
10228
+ "optional": false,
10229
+ "summary": null
10230
+ }
10231
+ ],
10232
+ "returns": "whether the declaration reached the Arcade now. `false` outside\n the Arcade, without `playInvites`, before the bridge is up (it is sent on\n connect) and for params the share rules refuse, which are said on the\n console; the last declaration then stands.",
10233
+ "throws": null,
10234
+ "example": "```ts\n// On entering a room joined by code:\ngem.friends.setInviteParams({ room: joinCode });\n// ...and on leaving it:\ngem.friends.setInviteParams(null);\n```",
10235
+ "defaultValue": null,
10236
+ "guides": []
10237
+ },
10160
10238
  {
10161
10239
  "id": "FriendsApi.inbox",
10162
10240
  "name": "inbox",
@@ -10553,7 +10631,7 @@
10553
10631
  "group": "functions",
10554
10632
  "signature": "invoke(input: unknown, options?: FunctionInvokeOptions): Promise<FunctionResult>",
10555
10633
  "documented": true,
10556
- "summary": "Call the function published on your channel, and get its answer.\n\nThe input is sent as your function's `input`, byte for byte. Pass a\n`Uint8Array`, an `ArrayBuffer` or another typed array to send bytes as they\nare. Pass anything else and it is sent as its JSON text — a string too, so\n`'ping'` arrives as `\"ping\"` with its quotes.\n\nThe call is made once. It is never repeated for you, because your function\nmay have granted or written something: see [GemFunctionError.retryable](api-reference-game.md#gemfunctionerror-retryable)\nfor whether repeating it could help.\n\n```js\nconst result = await gem.functions.invoke({ v: 2, runId, finishMs });\nconst { accepted, why } = result.json();\n```\n\nWhen the call pays a reward the player sees, pass the reward's `cueId`, the\nsame one the page gives `gem.cueReward` for it, and the Gem Arcade flies it\nonce:\n\n```js\nconst cueId = crypto.randomUUID(); // one per award, also passed to the cue\nawait gem.functions.invoke({ claim: 'daily' }, { cueId });\n```",
10634
+ "summary": "Call the function published on your channel, and get its answer.\n\nThe input is sent as your function's `input`, byte for byte. Pass a\n`Uint8Array`, an `ArrayBuffer` or another typed array to send bytes as they\nare. Pass anything else and it is sent as its JSON text — a string too, so\n`'ping'` arrives as `\"ping\"` with its quotes.\n\nThe call is made once. It is never repeated for you, because your function\nmay have granted or written something: see [GemFunctionError.retryable](api-reference-game.md#gemfunctionerror-retryable)\nfor whether repeating it could help.\n\nThe answer is the receipt: when this resolves, what your function says it\ngranted has been written. A game shows it at once and re-reads the\ninventory in the background (`gem.me.inventory.refresh()`, then\n`snapshot()`, or on `gem.me.inventory.onChanged`); never hold the player on\na loop that re-reads until the grant shows.\n\n```js\nconst result = await gem.functions.invoke({ v: 2, runId, finishMs });\nconst { accepted, why } = result.json();\n```\n\nWhen the call pays a reward the player sees, pass the reward's `cueId`, the\nsame one the page gives `gem.cueReward` for it, and the Gem Arcade flies it\nonce:\n\n```js\nconst cueId = crypto.randomUUID(); // one per award, also passed to the cue\nawait gem.functions.invoke({ claim: 'daily' }, { cueId });\n```",
10557
10635
  "deprecated": null,
10558
10636
  "params": [
10559
10637
  {
@@ -13554,7 +13632,7 @@
13554
13632
  "group": "start",
13555
13633
  "signature": "readonly friends: FriendsApi",
13556
13634
  "documented": true,
13557
- "summary": "The signed-in player's accepted friends — display identity only,\nread-only, paginated — and the one ask that reaches past them:\n[FriendsApi.challenge](api-reference-game.md#friendsapi-challenge), which asks the Arcade to send a friend a\nchallenge after the player confirms it in the Arcade's own sheet. The\nreceiving side reads the challenges this player holds in this game:\n[FriendsApi.launchedChallenge](api-reference-game.md#friendsapi-launchedchallenge) for the one this frame was launched\nwith, [FriendsApi.inbox](api-reference-game.md#friendsapi-inbox) for all of them, and\n[FriendsApi.markAnswered](api-reference-game.md#friendsapi-markanswered) once one has been played.\n\nEvery accepted friend appears whether or not they have played this game;\nan invite-a-friend flow needs exactly the ones who have not. The graph\nitself — requests, accepts, blocks, presence — stays on the Arcade: there\nis nothing here to mutate but a challenge's own answered mark, and nothing\nthat says who is online.\n\nA player surface only. In a context with no player — a game server — the\nplatform refuses the read; a server has no friend list.",
13635
+ "summary": "The signed-in player's accepted friends — display identity only,\nread-only, paginated — and the asks that reach past them:\n[FriendsApi.challenge](api-reference-game.md#friendsapi-challenge), which asks the Arcade to send a friend a\nchallenge after the player confirms it in the Arcade's own sheet, and\n[FriendsApi.inviteToPlay](api-reference-game.md#friendsapi-invitetoplay), which asks the Arcade to open its own\ninvite-to-play sheet. The receiving side reads the challenges this player\nholds in this game:\n[FriendsApi.launchedChallenge](api-reference-game.md#friendsapi-launchedchallenge) for the one this frame was launched\nwith, [FriendsApi.inbox](api-reference-game.md#friendsapi-inbox) for all of them, and\n[FriendsApi.markAnswered](api-reference-game.md#friendsapi-markanswered) once one has been played.\n\nEvery accepted friend appears whether or not they have played this game;\nan invite-a-friend flow needs exactly the ones who have not. The graph\nitself — requests, accepts, blocks, presence — stays on the Arcade: there\nis nothing here to mutate but a challenge's own answered mark, and nothing\nthat says who is online.\n\nA player surface only. In a context with no player — a game server — the\nplatform refuses the read; a server has no friend list.",
13558
13636
  "deprecated": null,
13559
13637
  "params": [],
13560
13638
  "returns": null,
@@ -22499,7 +22577,7 @@
22499
22577
  "group": "rooms",
22500
22578
  "signature": "readonly playWith: PlayWithOutcome | null",
22501
22579
  "documented": true,
22502
- "summary": "What the last [RoomManager.quickJoin](api-reference-game.md#roommanager-quickjoin) did when the Arcade opened\nyour game to play with someone — a friend whose Join the player pressed,\nor their party's leader. Read-only.\n\n- `'joined'` — the player was seated in that person's room, as a member\n sharing their `groupId`.\n- `'unavailable'` — that room could not be joined (no seat, a match that\n admits nobody late, another mode), so the quick-join went on as it\n always does. No reason is given.\n- `null` — no quick-join has asked: the Arcade did not open the game to\n play with anyone, or nothing has quick-joined yet.\n\nYour game makes no call for this, names nobody and is told nobody's name:\nthe Arcade adds a launch key of its own, an opaque ticket, and every\n`quickJoin` passes it on. Nothing else\nchanges — the room is the one `quickJoin` answers with, `joined` fires as\nfor any quick-join, and no `seated` follows, because your own call made\nthe seat. Use it for a line of text (\"You joined your friend's game\") or\nto skip a mode picker, never to decide anything of value.",
22580
+ "summary": "What the last [RoomManager.quickJoin](api-reference-game.md#roommanager-quickjoin) did when the Arcade opened\nyour game to play with someone — a friend whose Join the player pressed,\na friend's invite, or their party's leader. Read-only.\n\n- `'joined'` — the player was seated in that person's room, as a member\n sharing their `groupId`.\n- `'unavailable'` — that room could not be joined (no seat, a match that\n admits nobody late, another mode), so the quick-join went on as it\n always does. No reason is given.\n- `null` — no quick-join has asked: the Arcade did not open the game to\n play with anyone, or nothing has quick-joined yet.\n\nYour game makes no call for this, names nobody and is told nobody's name:\nthe Arcade adds a launch key of its own, an opaque ticket, and every\n`quickJoin` passes it on. Nothing else\nchanges — the room is the one `quickJoin` answers with, `joined` fires as\nfor any quick-join, and no `seated` follows, because your own call made\nthe seat. Use it for a line of text (\"You joined your friend's game\") or\nto skip a mode picker, never to decide anything of value.",
22503
22581
  "deprecated": null,
22504
22582
  "params": [],
22505
22583
  "returns": null,
@@ -27110,7 +27188,7 @@
27110
27188
  "group": "functions",
27111
27189
  "signature": "invoke(input: unknown, options?: FunctionInvokeOptions): Promise<FunctionResult>",
27112
27190
  "documented": true,
27113
- "summary": "Call the function published on your channel, and get its answer.\n\nThe input is sent as your function's `input`, byte for byte. Pass a\n`Uint8Array`, an `ArrayBuffer` or another typed array to send bytes as they\nare. Pass anything else and it is sent as its JSON text — a string too, so\n`'ping'` arrives as `\"ping\"` with its quotes.\n\nThe call is made once. It is never repeated for you, because your function\nmay have granted or written something: see [GemFunctionError.retryable](api-reference-server.md#gemfunctionerror-retryable)\nfor whether repeating it could help.\n\n```js\nconst result = await gem.functions.invoke({ v: 2, runId, finishMs });\nconst { accepted, why } = result.json();\n```\n\nWhen the call pays a reward the player sees, pass the reward's `cueId`, the\nsame one the page gives `gem.cueReward` for it, and the Gem Arcade flies it\nonce:\n\n```js\nconst cueId = crypto.randomUUID(); // one per award, also passed to the cue\nawait gem.functions.invoke({ claim: 'daily' }, { cueId });\n```",
27191
+ "summary": "Call the function published on your channel, and get its answer.\n\nThe input is sent as your function's `input`, byte for byte. Pass a\n`Uint8Array`, an `ArrayBuffer` or another typed array to send bytes as they\nare. Pass anything else and it is sent as its JSON text — a string too, so\n`'ping'` arrives as `\"ping\"` with its quotes.\n\nThe call is made once. It is never repeated for you, because your function\nmay have granted or written something: see [GemFunctionError.retryable](api-reference-server.md#gemfunctionerror-retryable)\nfor whether repeating it could help.\n\nThe answer is the receipt: when this resolves, what your function says it\ngranted has been written. A game shows it at once and re-reads the\ninventory in the background (`gem.me.inventory.refresh()`, then\n`snapshot()`, or on `gem.me.inventory.onChanged`); never hold the player on\na loop that re-reads until the grant shows.\n\n```js\nconst result = await gem.functions.invoke({ v: 2, runId, finishMs });\nconst { accepted, why } = result.json();\n```\n\nWhen the call pays a reward the player sees, pass the reward's `cueId`, the\nsame one the page gives `gem.cueReward` for it, and the Gem Arcade flies it\nonce:\n\n```js\nconst cueId = crypto.randomUUID(); // one per award, also passed to the cue\nawait gem.functions.invoke({ claim: 'daily' }, { cueId });\n```",
27114
27192
  "deprecated": null,
27115
27193
  "params": [
27116
27194
  {
@@ -32511,7 +32589,7 @@
32511
32589
  "group": "inventory",
32512
32590
  "signature": "refresh(): void",
32513
32591
  "documented": true,
32514
- "summary": "Throw away what is cached, so the next [InventoryApi.snapshot](api-reference-function.md#inventoryapi-snapshot) really\nasks.\n\nFor when something you did not do changed the inventory — a purchase on\nanother device, a grant from a server. The SDK already drops the cache\nitself after any change it made.",
32592
+ "summary": "Throw away what is cached, so the next [InventoryApi.snapshot](api-reference-function.md#inventoryapi-snapshot) really\nasks.\n\nFor when something you did not do changed the inventory — a purchase on\nanother device, a grant from a server — and nothing told you. The SDK\nalready drops the cache itself after any change it made, and on an\nArcade's hint that this game's inventory changed on the server.",
32515
32593
  "deprecated": null,
32516
32594
  "params": [],
32517
32595
  "returns": null,
@@ -32527,7 +32605,7 @@
32527
32605
  "group": "inventory",
32528
32606
  "signature": "onChanged(listener: () => void): () => void",
32529
32607
  "documented": true,
32530
- "summary": "Run something whenever this inventory changes, and get back a function that\nstops listening.\n\n⚠️ LOCAL CHANGES ONLY — this fires for changes THIS game made through this\nSDK. It does NOT fire when an inventory changes anywhere else: another\ndevice, a game server, an admin tool, or a gift from another player. Do not\nbuild \"keep the bag live\" on it; use it to react to your own writes, and\ncall [InventoryApi.refresh](api-reference-function.md#inventoryapi-refresh) when you have reason to think something\nelse moved.",
32608
+ "summary": "Run something whenever this inventory changes, and get back a function that\nstops listening.\n\nFires for every change THIS game made through this SDK, and — on an Arcade\nthat relays it — for any change to this game's inventory on the server: a\ngrant from your function or server, a purchase, a gift accepted, a support\ngrant, another device. It is a cue to re-read, not a receipt: it carries\nnothing, so call [InventoryApi.snapshot](api-reference-function.md#inventoryapi-snapshot) for what changed. After your\nown function's grant, show the function's answer and let this redraw.\n\nAn older Arcade sends no hint, and there it fires for your own writes\nonly. Keep a fallback that does not depend on it: refresh on\n`visibilitychange` to `visible` and after any action that could involve\nanother player. Do not poll.",
32531
32609
  "deprecated": null,
32532
32610
  "params": [
32533
32611
  {
@@ -36279,6 +36357,38 @@
36279
36357
  "defaultValue": null,
36280
36358
  "guides": []
36281
36359
  },
36360
+ {
36361
+ "id": "PlayInviteRequest",
36362
+ "name": "PlayInviteRequest",
36363
+ "anchor": "playinviterequest",
36364
+ "group": "friends",
36365
+ "signature": "interface PlayInviteRequest",
36366
+ "documented": true,
36367
+ "summary": "What `gem.friends.inviteToPlay` takes.",
36368
+ "deprecated": null,
36369
+ "params": [],
36370
+ "returns": null,
36371
+ "throws": null,
36372
+ "example": null,
36373
+ "defaultValue": null,
36374
+ "guides": []
36375
+ },
36376
+ {
36377
+ "id": "PlayInviteRequest.params",
36378
+ "name": "params",
36379
+ "anchor": "playinviterequest-params",
36380
+ "group": "friends",
36381
+ "signature": "readonly params?: Readonly<Record<string, string>>",
36382
+ "documented": true,
36383
+ "summary": "Your own launch parameters for what the Arcade's link and invites open —\n`{ room: joinCode }` for a room joined by code — exactly as they should\nreach your game, on `gem.share.request`'s rules: at most 16, keys a letter\nthen up to 63 letters, digits, `_` or `-`, string values of at most 512\ncharacters. For this one opening, they replace what\n`gem.friends.setInviteParams` declared. Omit it to use that declaration.",
36384
+ "deprecated": null,
36385
+ "params": [],
36386
+ "returns": null,
36387
+ "throws": null,
36388
+ "example": null,
36389
+ "defaultValue": null,
36390
+ "guides": []
36391
+ },
36282
36392
  {
36283
36393
  "id": "FriendsApi",
36284
36394
  "name": "FriendsApi",
@@ -36341,6 +36451,52 @@
36341
36451
  "defaultValue": null,
36342
36452
  "guides": []
36343
36453
  },
36454
+ {
36455
+ "id": "FriendsApi.inviteToPlay",
36456
+ "name": "inviteToPlay",
36457
+ "anchor": "friendsapi-invitetoplay",
36458
+ "group": "friends",
36459
+ "signature": "inviteToPlay(request?: PlayInviteRequest): Promise<boolean>",
36460
+ "documented": true,
36461
+ "summary": "Ask the Arcade to open ITS invite sheet, so the player can invite friends\nto come and play this game with them.\n\nThe Arcade lists the player's friends in its own chrome and sends an\ninvite only when the player presses Invite there. A friend who accepts is\nopened into this game by the Arcade, and their quick-join seats them in\nthe player's room when there is a seat: your game makes no call for it,\nand reads them as a room member who shares the player's `groupId`. Your\ngame names nobody and writes no text.\n\nAn Arcade that does not offer the sheet answers `false` at once, and asks\nnothing. Hide the button on `gem.frame.parentSupports('playInvites')`\nrather than letting a player press one that cannot work.\n\n⚠️ **Call it ONLY from a player's press**, never on a timer or as a screen\nopens. The Arcade answers an ask with no press behind it `false`, and\nshows nothing.\n\nRESOLVES `true` when the Arcade opened its sheet, and `false` when it did\nnot: outside the Arcade, without `playInvites`, with another sheet\nalready open, without a press, or when no answer arrived. **That is all it\nsays** — never whom the player invited, how many, or whether any invite\nwas sent — so never gate a reward on it. Never rejects.\n\nCalls made while an ask is open share its answer.\n\n**The window's share link and its invites are one system.** Pass\n`params` — your own launch parameters, `{ room: joinCode }` for a room\njoined by code — and a friend who accepts and anyone who opens the link\narrive with them, on `gem.share.request`'s rules. Omitted, the window uses\nwhat [FriendsApi.setInviteParams](api-reference-function.md#friendsapi-setinviteparams) declared. A param those rules\nrefuse is said on the console and nothing is asked: the call resolves\n`false`.",
36462
+ "deprecated": null,
36463
+ "params": [
36464
+ {
36465
+ "name": "request",
36466
+ "type": "PlayInviteRequest",
36467
+ "optional": true,
36468
+ "summary": null
36469
+ }
36470
+ ],
36471
+ "returns": null,
36472
+ "throws": null,
36473
+ "example": "```ts\ninviteButton.onclick = async () => {\n const shown = await gem.friends.inviteToPlay({ params: { room: joinCode } });\n if (!shown) showJoinCode(); // the Arcade's sheet did not open: your own fallback\n};\n```",
36474
+ "defaultValue": null,
36475
+ "guides": []
36476
+ },
36477
+ {
36478
+ "id": "FriendsApi.setInviteParams",
36479
+ "name": "setInviteParams",
36480
+ "anchor": "friendsapi-setinviteparams",
36481
+ "group": "friends",
36482
+ "signature": "setInviteParams(params: Readonly<Record<string, string>> | null): boolean",
36483
+ "documented": true,
36484
+ "summary": "Say what the Arcade's Friends window opens when the PLAYER opens it from\nthe Arcade's own menu over your game: your launch parameters, carried by\nits share link and its friend invites alike, on `gem.share.request`'s\nrules. Pass `null` to clear.\n\nCall it when the player enters a room joined by code —\n`setInviteParams({ room: joinCode })` — and `setInviteParams(null)` when\nthey leave it, so the window never invites anyone into a room that is\ngone. The latest call stands until the next; the SDK holds it and says it\nagain after a reconnect. An explicit `inviteToPlay({ params })` wins for\nthat one opening. Nothing comes back, and nothing changes on screen.",
36485
+ "deprecated": null,
36486
+ "params": [
36487
+ {
36488
+ "name": "params",
36489
+ "type": "Readonly<Record<string, string>> | null",
36490
+ "optional": false,
36491
+ "summary": null
36492
+ }
36493
+ ],
36494
+ "returns": "whether the declaration reached the Arcade now. `false` outside\n the Arcade, without `playInvites`, before the bridge is up (it is sent on\n connect) and for params the share rules refuse, which are said on the\n console; the last declaration then stands.",
36495
+ "throws": null,
36496
+ "example": "```ts\n// On entering a room joined by code:\ngem.friends.setInviteParams({ room: joinCode });\n// ...and on leaving it:\ngem.friends.setInviteParams(null);\n```",
36497
+ "defaultValue": null,
36498
+ "guides": []
36499
+ },
36344
36500
  {
36345
36501
  "id": "FriendsApi.inbox",
36346
36502
  "name": "inbox",
package/docs/changelog.md CHANGED
@@ -16,6 +16,59 @@ 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
+ ## 23.0.1
20
+
21
+ Upgrading from 23.0.0. Impact: none.
22
+
23
+ Nothing in this release asks anything of a game.
24
+
25
+ ## 23.0.0
26
+
27
+ *Written from the API facts alone; no summary was generated for this release.*
28
+
29
+ Upgrading from 22.0.0. Impact: required.
30
+
31
+ ### Must change
32
+
33
+ - PARENT_CONFORMANCE_VERSION changed its signature.
34
+ - GemFrame.requestArcade changed its signature.
35
+ - FRAME_TO_PARENT changed its signature.
36
+ - FrameMessage changed its signature.
37
+ - GemCapability changed its signature.
38
+ - PARENT_TO_FRAME changed its signature.
39
+ - ParentMessage changed its signature.
40
+
41
+ ### New capability
42
+
43
+ - ParentFixture.channel was added.
44
+ - ParentFixture.gameId was added.
45
+ - ParentUnderTest.deliverSocket was added.
46
+ - CharactersApi.refreshHeld was added.
47
+ - FriendsApi.inviteToPlay was added.
48
+ - FriendsApi.setInviteParams was added.
49
+ - FriendsApiDependencies.inviteToPlay was added.
50
+ - FriendsApiDependencies.setInviteParams was added.
51
+ - PlayInviteRequest was added.
52
+ - FrameCallbacks.onInventoryChanged was added.
53
+ - FrameCallbacks.onPlayInviteResult was added.
54
+ - FrameOptions.onInventoryChanged was added.
55
+ - FrameOptions.onPlayInviteResult was added.
56
+ - GemHost.answerPlayInvite was added.
57
+ - GemHost.relayInventoryEvent was added.
58
+ - GemHost.sendInventoryChanged was added.
59
+ - GemInventoryChanged was added.
60
+ - GemInviteParams was added.
61
+ - GemPlayInviteResult was added.
62
+ - GemRequestPlayInvite was added.
63
+ - HostCallbacks.onInviteParams was added.
64
+ - HostCallbacks.onRequestPlayInvite was added.
65
+ - HostOptions.onInviteParams was added.
66
+ - HostOptions.onRequestPlayInvite was added.
67
+ - validateGemInventoryChanged was added.
68
+ - validateGemInviteParams was added.
69
+ - validateGemPlayInviteResult was added.
70
+ - validateGemRequestPlayInvite was added.
71
+
19
72
  ## 22.0.0
20
73
 
21
74
  Upgrading from 21.1.1. Impact: required.
package/docs/friends.md CHANGED
@@ -11,8 +11,9 @@ for (const friend of page.friends) {
11
11
  }
12
12
  ```
13
13
 
14
- That read, one ask — [sending a friend a challenge](#friend-challenges), which the Arcade
15
- confirms with the player and sends itself — and [reading the challenges the player
14
+ That read, two asks — [sending a friend a challenge](#friend-challenges), which the Arcade
15
+ confirms with the player and sends itself, and [inviting friends to play](#inviting-a-friend-to-play),
16
+ which opens the Arcade's own invite sheet — and [reading the challenges the player
16
17
  received](#reading-the-challenges-a-player-received) in your game are the whole surface. The
17
18
  boundary around them is the thing to understand before you design against it.
18
19
 
@@ -36,8 +37,10 @@ Everything else about the friend graph stays on the Arcade, by design:
36
37
  answered (`gem.friends.markAnswered`), which records that they played it and tells nobody.
37
38
  - **No states.** Pending requests, blocks and relationship history do not appear. An accepted
38
39
  friend appears; everyone else does not exist on this surface.
39
- - **No presence, no invites.** Nothing here says who is online or delivers an invite — an invite
40
- is something happening *in* the graph, and it is rendered where the graph lives. See the
40
+ - **No presence, and no invites delivered to your game.** Nothing here says who is online, and an
41
+ invite is something happening _in_ the graph, so it is rendered where the graph lives. Your game
42
+ may [ask the Arcade to open its own invite sheet](#inviting-a-friend-to-play), and that is the
43
+ whole of it: the Arcade picks no one for you, and tells you nothing back. See the
41
44
  deliberate-absence notes in [`rooms-and-matches.md`](rooms-and-matches.md). A friend challenge
42
45
  is not an invite to play together: it is a card in the friend's Arcade inbox, written by the
43
46
  platform from your template, that launches your game when they tap it — whenever they next open
@@ -46,7 +49,7 @@ Everything else about the friend graph stays on the Arcade, by design:
46
49
  Two properties worth designing around rather than discovering:
47
50
 
48
51
  - **Every accepted friend appears, whether or not they have played your game.** That is the
49
- point, not an oversight — an invite-a-friend flow needs exactly the friends who have *not*
52
+ point, not an oversight — an invite-a-friend flow needs exactly the friends who have _not_
50
53
  played it. If you only want friends with progress in your game, join the list against your own
51
54
  data (a friend's `playerId` is the same id leaderboard rows and transfers carry).
52
55
  - **It is a player surface only.** On a dedicated server there is no `gem.friends`, and the route
@@ -132,12 +135,12 @@ the card already says who sent it, and a name in a param is one nobody checked.
132
135
  `challenge()` resolves for every answer the player or the platform gives, and none of them
133
136
  carries a reason:
134
137
 
135
- | outcome | what happened | what to show |
136
- |---|---|---|
137
- | `accepted` | the player pressed Send and the platform took it | "Challenge sent", or nothing — the Arcade already said so |
138
- | `cancelled` | the player closed the sheet, or another prompt already held the screen | **nothing** |
139
- | `refused` | the platform will not send this one — a guest or signed out, not friends with that player, or no access to this channel | **nothing** |
140
- | `unavailable` | the Arcade could not offer this, or no answer arrived | **nothing**, and never retry it on your own |
138
+ | outcome | what happened | what to show |
139
+ | ------------- | ----------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
140
+ | `accepted` | the player pressed Send and the platform took it | "Challenge sent", or nothing — the Arcade already said so |
141
+ | `cancelled` | the player closed the sheet, or another prompt already held the screen | **nothing** |
142
+ | `refused` | the platform will not send this one — a guest or signed out, not friends with that player, or no access to this channel | **nothing** |
143
+ | `unavailable` | the Arcade could not offer this, or no answer arrived | **nothing**, and never retry it on your own |
141
144
 
142
145
  ⚠️ **`accepted` is not a delivery receipt.** The platform may decide not to deliver a challenge
143
146
  — the friend muted your game, has had enough challenges today, or cannot receive them — and
@@ -171,6 +174,7 @@ existing `instanceof GemApiError` handling keeps working:
171
174
  digits, `_` and `-`, a plain object of strings and integers, at most 8 keys — so most of these
172
175
  reach you before anything is shown to the player. `detail` is the platform's sentence, for your
173
176
  console, never for a player.
177
+
174
178
  - **`rate_limited`** — the player has sent too many. `retryAfterSeconds` is the earliest a fresh
175
179
  press could succeed, and it can be up to a day.
176
180
 
@@ -199,9 +203,10 @@ const challenge = await gem.friends.launchedChallenge();
199
203
  if (challenge?.template === 'beat_code_jar') {
200
204
  // Check every param before it does anything, as you would a launch key: the template
201
205
  // constrained it when it was sent, but your config may have changed since.
202
- const jar = typeof challenge.params.jar === 'string' && /^[A-Z0-9]{7}$/.test(challenge.params.jar)
203
- ? challenge.params.jar
204
- : null;
206
+ const jar =
207
+ typeof challenge.params.jar === 'string' && /^[A-Z0-9]{7}$/.test(challenge.params.jar)
208
+ ? challenge.params.jar
209
+ : null;
205
210
  if (jar) {
206
211
  startJar(jar, {
207
212
  from: challenge.from, // who sent it, from the platform's records
@@ -273,6 +278,106 @@ while (page.nextCursor) {
273
278
 
274
279
  For the challenge this launch carried, use `launchedChallenge()` rather than searching the inbox.
275
280
 
281
+ ## Inviting a friend to play
282
+
283
+ Your game may ask the Arcade to open **its own invite sheet**, where the player picks friends to
284
+ invite to come and play your game with them, now:
285
+
286
+ ```js
287
+ inviteButton.hidden = !gem.frame.parentSupports('playInvites');
288
+ inviteButton.onclick = async () => {
289
+ const shown = await gem.friends.inviteToPlay();
290
+ if (!shown) showJoinCode(); // the sheet did not open: offer your own way in
291
+ };
292
+ ```
293
+
294
+ The Arcade lists the player's friends in its own chrome, pauses your game while the sheet is
295
+ open, and sends an invite each time the player presses Invite there. A friend who accepts is
296
+ opened into your game by the Arcade, and their own quick-join seats them in the player's room
297
+ when it has a seat — your game makes no call for it, is not told who is coming, and finds them in
298
+ the room as a member who shares the player's `groupId`. [Landing in a friend's
299
+ room](rooms-and-matches.md#5e-landing-in-a-friends-room-play-with) is how they arrive.
300
+
301
+ An invite is live, unlike a challenge: it reaches a friend who is on the Arcade now, and nothing
302
+ waits in an inbox. For "beat my score whenever you next play", send a
303
+ [friend challenge](#friend-challenges) instead.
304
+
305
+ ### Invites and share links are one window
306
+
307
+ The sheet is the Arcade's **Friends window**. The player also opens it from the Arcade's own menu
308
+ over your game, without your game asking. It holds the friends to invite **and** a link to share
309
+ with anyone else, and both open your game the same way: with **your launch parameters**, on
310
+ exactly [`gem.share.request`'s rules](getting-started.md#linking-to-your-game). So a game whose
311
+ rooms are joined by a code says the code once, and a friend who accepts an invite and a stranger
312
+ who opens the shared link both arrive with it.
313
+
314
+ There are two ways to say it, for the window's two ways in:
315
+
316
+ - **`gem.friends.setInviteParams(params)`** — a standing declaration, for the window the PLAYER
317
+ opens from the Arcade's menu. Call it when the player enters a room joined by code, and
318
+ `setInviteParams(null)` when they leave it, so the window never invites anyone into a room that
319
+ is gone. The latest call stands; the SDK holds it and says it again after a reconnect.
320
+ - **`gem.friends.inviteToPlay({ params })`** — from your own Invite button, for that one opening.
321
+ It wins over the declaration. Omit `params` to use the declaration.
322
+
323
+ A room joined by code, end to end:
324
+
325
+ ```js
326
+ // The host makes the room and says what the window should open.
327
+ const room = await gem.rooms.create({/* your template */});
328
+ gem.friends.setInviteParams({ room: room.joinCode });
329
+
330
+ // Your own Invite button, if you draw one, needs nothing more:
331
+ inviteButton.onclick = () => gem.friends.inviteToPlay();
332
+
333
+ // Leaving the room clears it.
334
+ gem.friends.setInviteParams(null);
335
+
336
+ // Arrival is unchanged: whoever opens the link or accepts the invite launches your game
337
+ // with ?room=<code>, and you join as you already do for a shared link.
338
+ const code = new URLSearchParams(location.search).get('room');
339
+ if (code) await gem.rooms.joinByCode(code);
340
+ ```
341
+
342
+ The params name what a link opens and never a person, so they tell the Arcade nothing about the
343
+ player's friends, and nothing about who was invited comes back. A param the share rules refuse —
344
+ a reserved key, more than 16, a bad key or value — is said on the console and nothing is sent:
345
+ `inviteToPlay` resolves `false`, and `setInviteParams` returns `false` and leaves the last
346
+ declaration standing. A game with a quick-join hub needs no params at all: the friend's
347
+ quick-join already seats them in the player's room (above).
348
+
349
+ ### One answer, and nothing else
350
+
351
+ `inviteToPlay()` resolves one boolean. It never rejects.
352
+
353
+ | resolves | what happened |
354
+ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
355
+ | `true` | the Arcade opened its invite sheet |
356
+ | `false` | it did not: outside the Arcade or without `playInvites` (answered at once, with nothing asked), no press behind the ask, another sheet or prompt already open, a player the Arcade offers no invites to, or no answer arrived |
357
+
358
+ **What your game never learns:** whom the player invited, how many, whether any invite was sent,
359
+ delivered or accepted, or why the sheet did not open. Who a player invites is a fact about the
360
+ people they know, and whether an invite landed is a fact about somebody else's settings; neither
361
+ is your game's. `true` means a sheet opened, not that anybody was invited.
362
+
363
+ **Your game names nobody and writes nothing.** No player, no text: the friends on the sheet are
364
+ the Arcade's own list, and the words on the sheet and on the invite are the Arcade's. Your launch
365
+ params say what the link opens, as a share's do, and nothing else.
366
+
367
+ ### Rules that are not optional
368
+
369
+ - **Call it only from a player's press.** The Arcade answers an ask with no press behind it
370
+ `false`, and shows nothing. Never on a timer, as a screen opens, or after a long `await`.
371
+ - **Never gate progress or a reward on it.** You cannot tell whether anyone was invited, by
372
+ design, so a reward for it is a reward for pressing a button.
373
+ - **Never retry it on your own.** `false` is an answer: offer your own way in instead, such as a
374
+ join code or a [share link](getting-started.md#linking-to-your-game).
375
+ - **One sheet at a time.** Calls made while an ask is open share its answer, and an ask that
376
+ arrives while the Arcade already shows a sheet is answered `false`.
377
+ - **Hide the button where it cannot work.** `gem.frame.parentSupports('playInvites')` is false
378
+ outside the Arcade — a local dev host included — and `inviteToPlay()` answers `false` there at
379
+ once, asking nothing. Try the real flow on your game's dev channel in the Arcade.
380
+
276
381
  ## Inspecting a player: game profiles
277
382
 
278
383
  The "inspect this player" screen — tap a roster row, a leaderboard entry or a friend, see who
@@ -296,7 +401,7 @@ profile. Entries arrive ordered by key, each naming its slot's declared `visibil
296
401
 
297
402
  Three properties to design around:
298
403
 
299
- - **A game shapes its profile in the game config, not here.** Profile content *is* the game's
404
+ - **A game shapes its profile in the game config, not here.** Profile content _is_ the game's
300
405
  showcase slots — save schemas declared with a `visibility` of `friends` or `public` — and the
301
406
  write path is `gem.showcase.set` (or your dedicated server, for values a modified client must
302
407
  not forge). There is no profile write on this surface, and identity is the player's own,
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "22.0.0"
53
+ "@indiegems/gem-web-sdk": "23.0.1"
54
54
  }
55
55
  ```
56
56
 
@@ -308,6 +308,12 @@ whether they shared, cancelled, or closed the panel. Where the arcade handles it
308
308
  `gem.share.available` is true; outside the arcade, before the bridge is up, and anywhere the
309
309
  arcade does not share your game, it is false.
310
310
 
311
+ **The Arcade's Friends window shares the same link.** The player can open it from the Arcade's
312
+ menu over your game to invite friends and share a link, and both carry the launch parameters you
313
+ give `gem.friends.setInviteParams` or `gem.friends.inviteToPlay`, on the rules below. A game whose
314
+ rooms are joined by code declares the code there; see
315
+ [Invites and share links are one window](friends.md#invites-and-share-links-are-one-window).
316
+
311
317
  `request` checks every parameter against the rules below **before** anything is sent, and
312
318
  throws on the first one the arcade would refuse, naming it: more than 16, a key that breaks the
313
319
  key rule or is one the arcade will not carry, or a value that is not a string, is too long, or
@@ -386,9 +392,10 @@ the arcade applies:
386
392
  the sign-in return marker.
387
393
  - Attribution and campaign keys: `_s`, `utm_*` (every key with that prefix, matched
388
394
  case-sensitively, so UTM_X and utmost are yours), `fbclid`, `ttclid`, `gclid`, `gbraid`,
389
- `wbraid`, `msclkid`. The arcade strips the visitor's own campaign and click ids, but a link
390
- a game shares, or one anyone writes by hand, can still put `utm_*` and the click ids into
391
- your frame, restored from their escaped form.
395
+ `wbraid`, `msclkid`, and `gem_link`, a search ad's link code on your store page. The arcade
396
+ strips the visitor's own campaign and click ids, but a link a game shares, or one anyone
397
+ writes by hand, can still put `utm_*` and the click ids into your frame, restored from their
398
+ escaped form.
392
399
 
393
400
  A link your game shares can still carry most of them: see
394
401
  [Sharing a reserved key](#sharing-a-reserved-key) below.