@indiegems/gem-web-sdk 23.2.0 → 23.3.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/CHANGELOG.md +18 -0
- package/dist/game.d.ts +3 -1
- package/dist/gem-web-sdk.bridge.js +2 -2
- package/dist/gem-web-sdk.conformance.js +2 -2
- package/dist/gem-web-sdk.esm.js +2 -2
- package/dist/gem-web-sdk.function.js +1 -1
- package/dist/gem-web-sdk.game.js +3 -3
- package/dist/gem-web-sdk.min.js +3 -3
- package/dist/gem-web-sdk.sdk.js +3 -3
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +3 -1
- package/dist/sdk.d.ts +3 -1
- package/docs/analytics.md +40 -0
- package/docs/api/api-reference-function.md +3 -2
- package/docs/api/api-reference-game.md +57 -2
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +48 -4
- package/docs/arcade-and-store.md +4 -2
- package/docs/changelog.md +18 -0
- package/docs/getting-started.md +1 -1
- package/docs/manifest.json +16 -16
- package/docs/player-sign-in.md +51 -3
- package/docs/rewarded-ads.md +31 -0
- package/docs/server-bundles.md +2 -2
- package/docs/what-a-game-must-do.md +7 -4
- package/package.json +1 -1
- package/schema/bridge.v1.json +41 -0
package/docs/api/reference.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schema_version": 2,
|
|
3
3
|
"source": {
|
|
4
4
|
"repo": "IndieGems/gem-web-sdk",
|
|
5
|
-
"version": "23.
|
|
5
|
+
"version": "23.3.0"
|
|
6
6
|
},
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -3929,7 +3929,7 @@
|
|
|
3929
3929
|
"group": "account",
|
|
3930
3930
|
"signature": "type SignInOutcomeKind = \"signed_in\" | \"declined\" | \"unavailable\"",
|
|
3931
3931
|
"documented": true,
|
|
3932
|
-
"summary": "The complete vocabulary of answers. Three values, and there will not be a\nfourth.\n\n- `signed_in` — the player is signed in, and [Gem.identity](api-reference-game.md#gem-identity),\n [Gem.player](api-reference-game.md#gem-player) and [Gem.me](api-reference-game.md#gem-me) already answer for their account.\n Re-read anything you loaded before asking. Also the answer for a player who\n was signed in all along.\n- `declined` — the player closed the prompt, or the arcade chose not to show\n one. Return to the game silently.\n- `unavailable` — the arcade could not offer sign-in, or never answered.\n\nNo reason is given for a `declined`, and one will never be added: why a\nsign-in did not happen is a fact about the player's account, and the arcade\nis the one that explains those.",
|
|
3932
|
+
"summary": "The complete vocabulary of answers. Three values, and there will not be a\nfourth.\n\n- `signed_in` — the player is signed in, and [Gem.identity](api-reference-game.md#gem-identity),\n [Gem.player](api-reference-game.md#gem-player) and [Gem.me](api-reference-game.md#gem-me) already answer for their account.\n Re-read anything you loaded before asking. Also the answer for a player who\n was signed in all along.\n- `declined` — the player closed the prompt, or the arcade chose not to show\n one. Return to the game silently.\n- `unavailable` — the arcade could not offer sign-in, or never answered.\n [AccountApi.signInAvailable](api-reference-game.md#accountapi-signinavailable) says so before you draw the button.\n\nNo reason is given for a `declined`, and one will never be added: why a\nsign-in did not happen is a fact about the player's account, and the arcade\nis the one that explains those.",
|
|
3933
3933
|
"deprecated": null,
|
|
3934
3934
|
"params": [],
|
|
3935
3935
|
"returns": null,
|
|
@@ -3986,6 +3986,50 @@
|
|
|
3986
3986
|
"defaultValue": null,
|
|
3987
3987
|
"guides": []
|
|
3988
3988
|
},
|
|
3989
|
+
{
|
|
3990
|
+
"id": "AccountApi.signInAvailable",
|
|
3991
|
+
"name": "signInAvailable",
|
|
3992
|
+
"anchor": "accountapi-signinavailable",
|
|
3993
|
+
"group": "account",
|
|
3994
|
+
"signature": "readonly signInAvailable: boolean",
|
|
3995
|
+
"documented": true,
|
|
3996
|
+
"summary": "Whether [AccountApi.requestSignIn](api-reference-game.md#accountapi-requestsignin) can show this player a sign-in\nhere. Read it before you draw a \"Sign in to claim\" button, and hide the\noffer when it is `false`.\n\n- `true` — a guest the arcade can offer sign-up to. An ask may still\n answer `declined` at a busy moment.\n- `false` — an ask would show nothing. The player is already signed in\n (it answers `signed_in` at once; every player in a current Gem Arcade\n app build, v3.33.174 and later, is), or there is no Arcade sign-in here\n (it answers `unavailable`). Also `false` until the player is known.\n\nOn an arcade older than this signal it reads `true` for every player, a\nfull account included, as before it existed. So gate a sign-up gift on it\nand on the guest test you use today; see docs/player-sign-in.md.\n[AccountApi.onSignInAvailableChange](api-reference-game.md#accountapi-onsigninavailablechange) tells you when it changes.",
|
|
3997
|
+
"deprecated": null,
|
|
3998
|
+
"params": [],
|
|
3999
|
+
"returns": null,
|
|
4000
|
+
"throws": null,
|
|
4001
|
+
"example": "```ts\nconst draw = (available: boolean) => {\n signInButton.hidden = !available;\n};\ndraw(gem.account.signInAvailable);\ngem.account.onSignInAvailableChange(draw);\n```",
|
|
4002
|
+
"defaultValue": null,
|
|
4003
|
+
"guides": [
|
|
4004
|
+
{
|
|
4005
|
+
"slug": "player-sign-in",
|
|
4006
|
+
"anchor": null
|
|
4007
|
+
}
|
|
4008
|
+
]
|
|
4009
|
+
},
|
|
4010
|
+
{
|
|
4011
|
+
"id": "AccountApi.onSignInAvailableChange",
|
|
4012
|
+
"name": "onSignInAvailableChange",
|
|
4013
|
+
"anchor": "accountapi-onsigninavailablechange",
|
|
4014
|
+
"group": "account",
|
|
4015
|
+
"signature": "onSignInAvailableChange(listener: (available: boolean) => void): () => void",
|
|
4016
|
+
"documented": true,
|
|
4017
|
+
"summary": "Runs `listener` whenever [AccountApi.signInAvailable](api-reference-game.md#accountapi-signinavailable) changes: when\nthe player arrives, when the arcade changes its answer (a guest who signs\nup), and when the connection closes. Not on a re-sent player with the same\nanswer.",
|
|
4018
|
+
"deprecated": null,
|
|
4019
|
+
"params": [
|
|
4020
|
+
{
|
|
4021
|
+
"name": "listener",
|
|
4022
|
+
"type": "(available: boolean) => void",
|
|
4023
|
+
"optional": false,
|
|
4024
|
+
"summary": null
|
|
4025
|
+
}
|
|
4026
|
+
],
|
|
4027
|
+
"returns": "A function that stops the listener.",
|
|
4028
|
+
"throws": null,
|
|
4029
|
+
"example": null,
|
|
4030
|
+
"defaultValue": null,
|
|
4031
|
+
"guides": []
|
|
4032
|
+
},
|
|
3989
4033
|
{
|
|
3990
4034
|
"id": "AccountApi.requestSignIn",
|
|
3991
4035
|
"name": "requestSignIn",
|
|
@@ -4735,7 +4779,7 @@
|
|
|
4735
4779
|
"group": "analytics",
|
|
4736
4780
|
"signature": "experiment(key: string, count: number): Promise<number>",
|
|
4737
4781
|
"documented": true,
|
|
4738
|
-
"summary": "Which arm of one of your experiments this player gets — the same answer on\nevery call, every refresh and every device. Use it wherever you would have\npicked an arm with `Math.random()`, which deals a player a new arm on each\nvisit and makes the test unreadable.\n\nPass a COUNT and get an index, `0` to `count - 1`; pass a LIST of strings\nand get one of them. A string listed twice is twice as likely.\n\n```js\nconst tutorial = await gem.analytics.experiment('tutorial_length', ['short', 'long']);\nconst layout = await gem.analytics.experiment('shop_layout', 3); // 0, 1 or 2\n```\n\nThe selection is a hash of the key and the signed-in player's id — FNV-1a\n32-bit over `\"<key>:<playerId>\"`, then MurmurHash3's 32-bit finalizer, mod\nthe arm count; see\n[experimentSelection](api-reference-game.md#experimentselection), which computes the same answer for an id you\nhold. Hashing the key in is what keeps your experiments independent: the\nplayers in one experiment's first arm are not the same players in every\nother experiment's first arm.\n\n⚠️ **Keep the key and the arms fixed for the life of the experiment.** A new\ncount or list re-deals every player. To start over on purpose, use a new\nkey.\n\n⚠️ **NOTHING IS RECORDED FOR YOU.** The selection is not sent anywhere, and\nthe SDK does not add it to your events. To analyse an experiment, declare a\ndimension for it on the events you want to break down, with the arms as its\nvalues, named after the experiment, and pass the selection there yourself:\n`gem.analytics.track('tutorial_finished', { tutorial_length: tutorial })`.\nNever the platform's `variant` field, which belongs to store-page\nexperiments.\n\nWAITS FOR THE PLAYER. The id arrives with the first play token, so a call\nduring boot waits for it, and rejects as [Gem.awaitIdentity](api-reference-game.md#gem-awaitidentity) does\nwhen none arrives — outside the Arcade, the Gem Editor or the dev host.\nThere is no random fallback: catch the rejection and play your control arm.\nA guest is selected by their guest id, which is stable for as long as their\nguest account lasts; once they sign in to an account they are that\naccount's player, and its selection applies.\n\nIn a serverless function it selects for the player who invoked it, with\nno wait, and rejects when a game server invoked it.\n\nWorks with analytics turned off: it reads no setting and sends nothing.",
|
|
4782
|
+
"summary": "Which arm of one of your experiments this player gets — the same answer on\nevery call, every refresh and every device. Use it wherever you would have\npicked an arm with `Math.random()`, which deals a player a new arm on each\nvisit and makes the test unreadable.\n\nPass a COUNT and get an index, `0` to `count - 1`; pass a LIST of strings\nand get one of them. A string listed twice is twice as likely.\n\n```js\nconst tutorial = await gem.analytics.experiment('tutorial_length', ['short', 'long']);\nconst layout = await gem.analytics.experiment('shop_layout', 3); // 0, 1 or 2\n```\n\nThe selection is a hash of the key and the signed-in player's id — FNV-1a\n32-bit over `\"<key>:<playerId>\"`, then MurmurHash3's 32-bit finalizer, mod\nthe arm count; see\n[experimentSelection](api-reference-game.md#experimentselection), which computes the same answer for an id you\nhold. Hashing the key in is what keeps your experiments independent: the\nplayers in one experiment's first arm are not the same players in every\nother experiment's first arm.\n\n⚠️ **Keep the key and the arms fixed for the life of the experiment.** A new\ncount or list re-deals every player. To start over on purpose, use a new\nkey.\n\n⚠️ **NOTHING IS RECORDED FOR YOU.** The selection is not sent anywhere, and\nthe SDK does not add it to your events. To analyse an experiment, declare a\ndimension for it on the events you want to break down, with the arms as its\nvalues, named after the experiment, and pass the selection there yourself:\n`gem.analytics.track('tutorial_finished', { tutorial_length: tutorial })`.\nNever the platform's `variant` field, which belongs to store-page\nexperiments. For the platform's results to show the test, also declare it\nunder `analytics_tests` in your game config (see docs/analytics.md).\n\nWAITS FOR THE PLAYER. The id arrives with the first play token, so a call\nduring boot waits for it, and rejects as [Gem.awaitIdentity](api-reference-game.md#gem-awaitidentity) does\nwhen none arrives — outside the Arcade, the Gem Editor or the dev host.\nThere is no random fallback: catch the rejection and play your control arm.\nA guest is selected by their guest id, which is stable for as long as their\nguest account lasts; once they sign in to an account they are that\naccount's player, and its selection applies.\n\nIn a serverless function it selects for the player who invoked it, with\nno wait, and rejects when a game server invoked it.\n\nWorks with analytics turned off: it reads no setting and sends nothing.",
|
|
4739
4783
|
"deprecated": null,
|
|
4740
4784
|
"params": [
|
|
4741
4785
|
{
|
|
@@ -31018,7 +31062,7 @@
|
|
|
31018
31062
|
"group": "analytics",
|
|
31019
31063
|
"signature": "experiment(key: string, count: number): Promise<number>",
|
|
31020
31064
|
"documented": true,
|
|
31021
|
-
"summary": "Which arm of one of your experiments this player gets — the same answer on\nevery call, every refresh and every device. Use it wherever you would have\npicked an arm with `Math.random()`, which deals a player a new arm on each\nvisit and makes the test unreadable.\n\nPass a COUNT and get an index, `0` to `count - 1`; pass a LIST of strings\nand get one of them. A string listed twice is twice as likely.\n\n```js\nconst tutorial = await gem.analytics.experiment('tutorial_length', ['short', 'long']);\nconst layout = await gem.analytics.experiment('shop_layout', 3); // 0, 1 or 2\n```\n\nThe selection is a hash of the key and the signed-in player's id — FNV-1a\n32-bit over `\"<key>:<playerId>\"`, then MurmurHash3's 32-bit finalizer, mod\nthe arm count; see\n`experimentSelection`, which computes the same answer for an id you\nhold. Hashing the key in is what keeps your experiments independent: the\nplayers in one experiment's first arm are not the same players in every\nother experiment's first arm.\n\n⚠️ **Keep the key and the arms fixed for the life of the experiment.** A new\ncount or list re-deals every player. To start over on purpose, use a new\nkey.\n\n⚠️ **NOTHING IS RECORDED FOR YOU.** The selection is not sent anywhere, and\nthe SDK does not add it to your events. To analyse an experiment, declare a\ndimension for it on the events you want to break down, with the arms as its\nvalues, named after the experiment, and pass the selection there yourself:\n`gem.analytics.track('tutorial_finished', { tutorial_length: tutorial })`.\nNever the platform's `variant` field, which belongs to store-page\nexperiments.\n\nWAITS FOR THE PLAYER. The id arrives with the first play token, so a call\nduring boot waits for it, and rejects as `Gem.awaitIdentity` does\nwhen none arrives — outside the Arcade, the Gem Editor or the dev host.\nThere is no random fallback: catch the rejection and play your control arm.\nA guest is selected by their guest id, which is stable for as long as their\nguest account lasts; once they sign in to an account they are that\naccount's player, and its selection applies.\n\nIn a serverless function it selects for the player who invoked it, with\nno wait, and rejects when a game server invoked it.\n\nWorks with analytics turned off: it reads no setting and sends nothing.",
|
|
31065
|
+
"summary": "Which arm of one of your experiments this player gets — the same answer on\nevery call, every refresh and every device. Use it wherever you would have\npicked an arm with `Math.random()`, which deals a player a new arm on each\nvisit and makes the test unreadable.\n\nPass a COUNT and get an index, `0` to `count - 1`; pass a LIST of strings\nand get one of them. A string listed twice is twice as likely.\n\n```js\nconst tutorial = await gem.analytics.experiment('tutorial_length', ['short', 'long']);\nconst layout = await gem.analytics.experiment('shop_layout', 3); // 0, 1 or 2\n```\n\nThe selection is a hash of the key and the signed-in player's id — FNV-1a\n32-bit over `\"<key>:<playerId>\"`, then MurmurHash3's 32-bit finalizer, mod\nthe arm count; see\n`experimentSelection`, which computes the same answer for an id you\nhold. Hashing the key in is what keeps your experiments independent: the\nplayers in one experiment's first arm are not the same players in every\nother experiment's first arm.\n\n⚠️ **Keep the key and the arms fixed for the life of the experiment.** A new\ncount or list re-deals every player. To start over on purpose, use a new\nkey.\n\n⚠️ **NOTHING IS RECORDED FOR YOU.** The selection is not sent anywhere, and\nthe SDK does not add it to your events. To analyse an experiment, declare a\ndimension for it on the events you want to break down, with the arms as its\nvalues, named after the experiment, and pass the selection there yourself:\n`gem.analytics.track('tutorial_finished', { tutorial_length: tutorial })`.\nNever the platform's `variant` field, which belongs to store-page\nexperiments. For the platform's results to show the test, also declare it\nunder `analytics_tests` in your game config (see docs/analytics.md).\n\nWAITS FOR THE PLAYER. The id arrives with the first play token, so a call\nduring boot waits for it, and rejects as `Gem.awaitIdentity` does\nwhen none arrives — outside the Arcade, the Gem Editor or the dev host.\nThere is no random fallback: catch the rejection and play your control arm.\nA guest is selected by their guest id, which is stable for as long as their\nguest account lasts; once they sign in to an account they are that\naccount's player, and its selection applies.\n\nIn a serverless function it selects for the player who invoked it, with\nno wait, and rejects when a game server invoked it.\n\nWorks with analytics turned off: it reads no setting and sends nothing.",
|
|
31022
31066
|
"deprecated": null,
|
|
31023
31067
|
"params": [
|
|
31024
31068
|
{
|
package/docs/arcade-and-store.md
CHANGED
|
@@ -223,7 +223,8 @@ policy refused. Allow that host rather than a wildcard.
|
|
|
223
223
|
|
|
224
224
|
```js
|
|
225
225
|
const { outcome, reason, offers } = await gem.store.offers();
|
|
226
|
-
if (outcome === 'unavailable' && reason === 'sign_in_required'
|
|
226
|
+
if (outcome === 'unavailable' && reason === 'sign_in_required' && gem.account.signInAvailable)
|
|
227
|
+
showSignInButton(); // gem.account.requestSignIn
|
|
227
228
|
for (const offer of offers) drawOffer(offer);
|
|
228
229
|
```
|
|
229
230
|
|
|
@@ -443,7 +444,8 @@ player, and the route refuses it with `403`. The body is `{ pass, held, ends_at
|
|
|
443
444
|
or the lounge, and either way nobody got in by editing the page. Redraw the door when the function
|
|
444
445
|
says no.
|
|
445
446
|
- **A guest has no VIP and cannot buy it from here.** Their door is shut, and `offers()` answers
|
|
446
|
-
`unavailable` / `sign_in_required`, so offer `gem.account.requestSignIn` instead of the button
|
|
447
|
+
`unavailable` / `sign_in_required`, so offer `gem.account.requestSignIn` instead of the button,
|
|
448
|
+
when `gem.account.signInAvailable` says the Arcade can show them a sign-in.
|
|
447
449
|
|
|
448
450
|
### When the door moves by itself
|
|
449
451
|
|
package/docs/changelog.md
CHANGED
|
@@ -16,6 +16,24 @@ 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.3.0
|
|
20
|
+
|
|
21
|
+
*Written from the API facts alone; no summary was generated for this release.*
|
|
22
|
+
|
|
23
|
+
Upgrading from 23.2.0. Impact: required.
|
|
24
|
+
|
|
25
|
+
### Must change
|
|
26
|
+
|
|
27
|
+
- FrameCallbacks.onPlayer changed its signature.
|
|
28
|
+
- FrameOptions.onPlayer changed its signature.
|
|
29
|
+
|
|
30
|
+
### New capability
|
|
31
|
+
|
|
32
|
+
- AccountApi.onSignInAvailableChange was added.
|
|
33
|
+
- AccountApi.signInAvailable was added.
|
|
34
|
+
- GemPlayer.signInAvailable was added.
|
|
35
|
+
- SendPlayerInput.signInAvailable was added.
|
|
36
|
+
|
|
19
37
|
## 23.2.0
|
|
20
38
|
|
|
21
39
|
*Written from the API facts alone; no summary was generated for this release.*
|
package/docs/getting-started.md
CHANGED
package/docs/manifest.json
CHANGED
|
@@ -13,13 +13,13 @@
|
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
"file": "analytics.md",
|
|
16
|
-
"sha256": "
|
|
17
|
-
"updated": "2026-11-
|
|
16
|
+
"sha256": "3f81025a763f96d0ca17f57377042a3e3890797983265cdb1721902b700aa3a6",
|
|
17
|
+
"updated": "2026-11-09T18:54:26Z"
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
"file": "arcade-and-store.md",
|
|
21
|
-
"sha256": "
|
|
22
|
-
"updated": "2026-11-
|
|
21
|
+
"sha256": "5b880e515ce0d65ffff77b97c93844dbcc14d7ab55d89edabaa54f4858376216",
|
|
22
|
+
"updated": "2026-11-09T16:45:36Z"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
25
25
|
"file": "arcade-avatars.md",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"file": "changelog.md",
|
|
51
|
-
"sha256": "
|
|
52
|
-
"updated": "2026-11-
|
|
51
|
+
"sha256": "85e888011b15aa2e9ebb56d938b9a8165489167bcebda236cba9e4f09937044f",
|
|
52
|
+
"updated": "2026-11-10T15:57:23Z"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"file": "daily-challenges.md",
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"file": "getting-started.md",
|
|
81
|
-
"sha256": "
|
|
82
|
-
"updated": "2026-11-
|
|
81
|
+
"sha256": "bdadeee9b46529d4ef9543a2be61cd096e6b200c20dbe2ab1b361d23139d3e64",
|
|
82
|
+
"updated": "2026-11-10T15:57:23Z"
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
"file": "graphics-route.md",
|
|
@@ -118,13 +118,13 @@
|
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
"file": "player-sign-in.md",
|
|
121
|
-
"sha256": "
|
|
122
|
-
"updated": "2026-11-
|
|
121
|
+
"sha256": "f5066f2382bac9336c697d3646843f546295686a959726e877431ec7dc2daea3",
|
|
122
|
+
"updated": "2026-11-09T18:39:56Z"
|
|
123
123
|
},
|
|
124
124
|
{
|
|
125
125
|
"file": "rewarded-ads.md",
|
|
126
|
-
"sha256": "
|
|
127
|
-
"updated": "2026-11-
|
|
126
|
+
"sha256": "36394d5faee67f3907139b1fe8c8210a6074a6eac593bc070b567625e71c6129",
|
|
127
|
+
"updated": "2026-11-09T19:34:44Z"
|
|
128
128
|
},
|
|
129
129
|
{
|
|
130
130
|
"file": "room-shapes.md",
|
|
@@ -143,8 +143,8 @@
|
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
"file": "server-bundles.md",
|
|
146
|
-
"sha256": "
|
|
147
|
-
"updated": "2026-11-
|
|
146
|
+
"sha256": "921591332e0a9fc52d36e3372af26a5ea808768e1bbd49826d765b7e4615ab53",
|
|
147
|
+
"updated": "2026-11-10T15:57:23Z"
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
"file": "serverless-functions-js.md",
|
|
@@ -168,8 +168,8 @@
|
|
|
168
168
|
},
|
|
169
169
|
{
|
|
170
170
|
"file": "what-a-game-must-do.md",
|
|
171
|
-
"sha256": "
|
|
172
|
-
"updated": "2026-11-
|
|
171
|
+
"sha256": "c5400f43a1218de0d2838d4332dce4c05afe21ff04e352d2e201ef5bb4268bed",
|
|
172
|
+
"updated": "2026-11-09T18:39:56Z"
|
|
173
173
|
}
|
|
174
174
|
]
|
|
175
175
|
}
|
package/docs/player-sign-in.md
CHANGED
|
@@ -17,8 +17,56 @@ signInButton.onclick = async () => {
|
|
|
17
17
|
};
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
That is the whole surface. Your game never draws a sign-in form and never handles a
|
|
21
|
-
could not be trusted with either, and it does not need to be.
|
|
20
|
+
That is almost the whole surface. Your game never draws a sign-in form and never handles a
|
|
21
|
+
password; it could not be trusted with either, and it does not need to be.
|
|
22
|
+
|
|
23
|
+
## Before you draw the button
|
|
24
|
+
|
|
25
|
+
Not every player can be shown a sign-in. Before you draw "Sign in to claim", read
|
|
26
|
+
`gem.account.signInAvailable`, and listen for it to change:
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
const drawUpsell = (available) => {
|
|
30
|
+
signInButton.hidden = !available;
|
|
31
|
+
};
|
|
32
|
+
drawUpsell(gem.account.signInAvailable);
|
|
33
|
+
gem.account.onSignInAvailableChange(drawUpsell);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
- **`true`**: pressing the button will normally put the Arcade's sign-in up for this player. It can
|
|
37
|
+
still answer `declined` at a busy moment, such as while the Arcade has a sheet or a menu open.
|
|
38
|
+
- **`false`**: pressing it would show nothing. Don't draw the offer. Either the player is already
|
|
39
|
+
signed in (an ask answers `signed_in` at once), or nothing can be shown here (it answers
|
|
40
|
+
`unavailable`). It is also `false` until the player is known, and again once the game is
|
|
41
|
+
disconnected.
|
|
42
|
+
|
|
43
|
+
**In current Gem Arcade app builds (v3.33.174 and later), every player is already signed in.** The
|
|
44
|
+
app signs each player in to a full account: a device account, Play Games or Game Center, or another
|
|
45
|
+
sign-in. So `signInAvailable` is `false` there, and an ask answers `signed_in` at once, with nothing
|
|
46
|
+
shown. Don't draw a sign-in offer in the app. Older app builds run an older Arcade, where the next
|
|
47
|
+
paragraph applies: `signInAvailable` reads `true` there.
|
|
48
|
+
|
|
49
|
+
On an Arcade older than this signal, `signInAvailable` reads `true` for every player, a full
|
|
50
|
+
account included. That is how a sign-in ask behaved before the signal existed. So a game that reads
|
|
51
|
+
it keeps its button there, and a player who is already signed in and presses it gets `signed_in`
|
|
52
|
+
at once, with nothing shown. Keep handling every answer quietly (section 3).
|
|
53
|
+
|
|
54
|
+
`onSignInAvailableChange` runs only when the answer changes: when the player arrives, when a guest
|
|
55
|
+
signs up in place, and when the game is disconnected. It returns a function that stops it.
|
|
56
|
+
|
|
57
|
+
### Gating a sign-up gift
|
|
58
|
+
|
|
59
|
+
If your game gives a gift for signing up, offer it only while `signInAvailable` is `true`. That
|
|
60
|
+
means a guest the Arcade can sign up here, so you don't need to work out who is a guest from
|
|
61
|
+
`gem.store.offers()` answering `unavailable` / `sign_in_required`.
|
|
62
|
+
|
|
63
|
+
On an older Arcade the signal can't do that job alone. It reads `true` for every player there, so a
|
|
64
|
+
gift gated on it alone is offered to players who already have an account. Pressing it answers
|
|
65
|
+
`signed_in` at once, which a player who was signed in all along gets too, so that answer is no
|
|
66
|
+
proof of a sign-up either. Your game can't tell which kind of Arcade it is running in, so for now
|
|
67
|
+
gate the gift on both: `signInAvailable` is `true` **and** the guest test you use today, such as
|
|
68
|
+
`offers()` answering `sign_in_required`. On a current Arcade the two agree for a guest. On an older
|
|
69
|
+
one, the second keeps the gift from players who are already signed in.
|
|
22
70
|
|
|
23
71
|
## 1. Save before you ask
|
|
24
72
|
|
|
@@ -56,7 +104,7 @@ and nothing has changed; hide the upsell.
|
|
|
56
104
|
|---|---|---|
|
|
57
105
|
| `signed_in` | the player is signed in | re-read, then hide the upsell |
|
|
58
106
|
| `declined` | the player closed the prompt, or the Arcade chose not to show one | return to the game silently |
|
|
59
|
-
| `unavailable` | the Arcade does not offer this here, or never answered | return to the game silently |
|
|
107
|
+
| `unavailable` | the Arcade does not offer this here, or never answered | return to the game silently; `signInAvailable` was `false` for most of these |
|
|
60
108
|
|
|
61
109
|
`requestSignIn` always resolves when it resolves at all. There is no error branch and no
|
|
62
110
|
`try`/`catch` to write.
|
package/docs/rewarded-ads.md
CHANGED
|
@@ -226,6 +226,37 @@ for a placement yet.** Until then your prices are not offered, the placement unl
|
|
|
226
226
|
alone, `ad: false` included, and `rewardedStatus` carries no `options` (§5). Nothing in your game
|
|
227
227
|
needs to change when it goes live: draw the button from `rewardedStatus` and it follows.
|
|
228
228
|
|
|
229
|
+
### A placement or the store?
|
|
230
|
+
|
|
231
|
+
Now that a placement can take Coins and Gems, pick by what the player is buying:
|
|
232
|
+
|
|
233
|
+
- **A single-reward button** (a hint, an extra life, a skip) is a **rewarded placement**, with
|
|
234
|
+
`platform_prices` for Coins and Gems. Whether it offers the ad, Coins, Gems or any mix is then a
|
|
235
|
+
config switch (`ad` and `platform_prices`), not code: the button and its `requestAd` stay the
|
|
236
|
+
same, and the Arcade shows the player the choices.
|
|
237
|
+
- **A real shop** (several items, quantities, locked items) is **store items and recipes** with
|
|
238
|
+
`platform_prices`
|
|
239
|
+
([`inventory-and-catalogs.md`](inventory-and-catalogs.md#16-selling-for-a-platform-currency)).
|
|
240
|
+
A placement grants one fixed reward per press: it has no list of items, no quantity, and no
|
|
241
|
+
prerequisite to lock anything behind.
|
|
242
|
+
|
|
243
|
+
A hint button the player can unlock with an ad or 2 Gems:
|
|
244
|
+
|
|
245
|
+
```yaml
|
|
246
|
+
rewarded_placements:
|
|
247
|
+
- placement: hint # exactly the string your game passes
|
|
248
|
+
grants:
|
|
249
|
+
- { type: item_grant, item_definition_slug: hint_token, quantity: 1 }
|
|
250
|
+
max_per_day: 5 # limits the ad only
|
|
251
|
+
platform_prices:
|
|
252
|
+
- { currency: GEMS, price: 2 }
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Add `{ currency: ARCADE_TICKETS, price: 20 }` beside it for Coins where Coins' spend scope lets
|
|
256
|
+
them price a hint (above), and `ad: false` to drop the ad. Do not also sell `hint_token` in your
|
|
257
|
+
shop: the reward then counts as sold for a platform currency (§1), the upload warns, and outside
|
|
258
|
+
test ads its ad is only served where the platform verifies completion with the ad provider.
|
|
259
|
+
|
|
229
260
|
## 3. Ask from the press, then re-read
|
|
230
261
|
|
|
231
262
|
```js
|
package/docs/server-bundles.md
CHANGED
|
@@ -80,8 +80,8 @@ To check the download yourself: the package npm serves is, byte for byte, the fi
|
|
|
80
80
|
also publishes on its download host, with the hash beside it:
|
|
81
81
|
|
|
82
82
|
```bash
|
|
83
|
-
curl -fsSL -O https://downloads.gemarcade.com/sdk/v23.
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/v23.
|
|
83
|
+
curl -fsSL -O https://downloads.gemarcade.com/sdk/v23.3.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v23.3.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
|
@@ -2404,10 +2404,13 @@ Not on the list above either. When a player reaches for something only an accoun
|
|
|
2404
2404
|
`gem.account.requestSignIn` asks the Arcade to sign them in, in its own chrome. Your game draws no
|
|
2405
2405
|
form and passes nothing.
|
|
2406
2406
|
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2407
|
+
Draw the offer only while `gem.account.signInAvailable` is `true`: it is `false` for a player
|
|
2408
|
+
already signed in (every player in a current Gem Arcade app build, v3.33.174 and later, is), and
|
|
2409
|
+
wherever nothing can be shown. Two
|
|
2410
|
+
things before you reach for it: **save first**, because the Arcade may sign the player in by taking
|
|
2411
|
+
the whole page away, and your game then starts again rather than hearing an answer; and on `signed_in`, **re-read what you loaded**, because the player
|
|
2412
|
+
underneath your game may not be the one who was there a moment ago.
|
|
2413
|
+
[`player-sign-in.md`](player-sign-in.md) is the page.
|
|
2411
2414
|
|
|
2412
2415
|
## A Share button, if your game has something worth sending a friend
|
|
2413
2416
|
|
package/package.json
CHANGED
package/schema/bridge.v1.json
CHANGED
|
@@ -840,6 +840,47 @@
|
|
|
840
840
|
]
|
|
841
841
|
}
|
|
842
842
|
}
|
|
843
|
+
},
|
|
844
|
+
"signInAvailable": {
|
|
845
|
+
"kind": "boolean",
|
|
846
|
+
"optional": true,
|
|
847
|
+
"$comment": [
|
|
848
|
+
"Whether a `gem.requestSignIn` from a player's press would put the",
|
|
849
|
+
"Arcade's sign-in prompt up for THIS player HERE.",
|
|
850
|
+
"The SDK reads it as `gem.account.signInAvailable`, so a game hides a",
|
|
851
|
+
"\"Sign in to claim\" offer that could show nothing.",
|
|
852
|
+
"",
|
|
853
|
+
"`true`: a browser guest on a surface that offers sign-up. An ask can",
|
|
854
|
+
"still answer `declined` at a busy moment (a sheet, a menu, a parked",
|
|
855
|
+
"game, a second ask); those holds are momentary and not part of this",
|
|
856
|
+
"field. `false`: an ask shows nothing — it answers `signed_in` at once",
|
|
857
|
+
"(a full account, which every player in the Gem Arcade app is) or",
|
|
858
|
+
"`unavailable` (a garden that offers no sign-up, a surface with no",
|
|
859
|
+
"sign-in). A parent",
|
|
860
|
+
"that knows sends it on EVERY `gem.player`, `false` included, and",
|
|
861
|
+
"re-sends the message when it changes (a guest who signs up in place).",
|
|
862
|
+
"",
|
|
863
|
+
"ABSENT means an older parent, and a frame reads it as `true`, which is",
|
|
864
|
+
"what `gem.requestSignIn` did there before this field: a game that adopts",
|
|
865
|
+
"it keeps its button on an old host. So a parent must never omit it to",
|
|
866
|
+
"mean `false`.",
|
|
867
|
+
"",
|
|
868
|
+
"A BOOLEAN, NEVER AN ENUM. A new enum value is breaking here: an N-1",
|
|
869
|
+
"frame's validator would refuse the whole message and cost the game its",
|
|
870
|
+
"player's name.",
|
|
871
|
+
"",
|
|
872
|
+
"Platform-assigned, so it sits outside `untrusted`. Ungated like the rest",
|
|
873
|
+
"of `gem.player`; it means something only beside the `signIn`",
|
|
874
|
+
"capability, without which the SDK reads `false` whatever it says.",
|
|
875
|
+
"",
|
|
876
|
+
"DISCLOSURE. `true` tells a game \"this is a browser guest\": the fact",
|
|
877
|
+
"`gem.requestSignIn` already discloses, since a",
|
|
878
|
+
"full account answers `signed_in` at once and a guest's answer follows a",
|
|
879
|
+
"person. `false` reveals less: it does not tell a signed-in player from",
|
|
880
|
+
"a surface with no sign-in. And `gem.player` is withheld until the launch is",
|
|
881
|
+
"confirmed, so an unconfirmed launch learns nothing. It carries no",
|
|
882
|
+
"sanction, session or age band."
|
|
883
|
+
]
|
|
843
884
|
}
|
|
844
885
|
},
|
|
845
886
|
"rateLimit": { "perMinute": 12 }
|