@indiegems/gem-web-sdk 23.1.3 → 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.
@@ -1,6 +1,6 @@
1
1
  # The browser surface — API reference
2
2
 
3
- Generated from `@indiegems/gem-web-sdk/sdk` v23.1.3, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
3
+ Generated from `@indiegems/gem-web-sdk/sdk` v23.3.0, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
4
4
 
5
5
  `@indiegems/gem-web-sdk/sdk` is the one import a game writes, wherever its code runs. `@indiegems/gem-web-sdk/game` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
6
6
 
@@ -284,6 +284,12 @@ this config server-side; the grant lands on your game's inventory, and
284
284
  re-reading `gem.me.inventory` is the only way to learn that it did. Whether
285
285
  the button could pay right now is [AdsApi.rewardedStatus](#adsapi-rewardedstatus).
286
286
 
287
+ A row may also carry `platform_prices`, the Coin and Gem prices that unlock
288
+ it beside the ad, and `ad: false` when only a price does (`ad` is absent
289
+ otherwise, which means the ad unlocks it). Both are what you declared, not
290
+ what is offered now: label the button from `rewardedStatus`'s
291
+ `adGrantable` and `options`, which say what this platform serves.
292
+
287
293
  Absent when your game declares none.
288
294
 
289
295
  **Guides.** [rewarded-ads](../rewarded-ads.md)
@@ -3117,6 +3123,7 @@ fourth.
3117
3123
  - `declined` — the player closed the prompt, or the arcade chose not to show
3118
3124
  one. Return to the game silently.
3119
3125
  - `unavailable` — the arcade could not offer sign-in, or never answered.
3126
+ [AccountApi.signInAvailable](#accountapi-signinavailable) says so before you draw the button.
3120
3127
 
3121
3128
  No reason is given for a `declined`, and one will never be added: why a
3122
3129
  sign-in did not happen is a fact about the player's account, and the arcade
@@ -3156,6 +3163,59 @@ interface AccountApi
3156
3163
 
3157
3164
  The account surface. `gem.account` is one of these.
3158
3165
 
3166
+ <a id="accountapi-signinavailable"></a>
3167
+
3168
+ ## AccountApi.signInAvailable
3169
+
3170
+ ```ts
3171
+ readonly signInAvailable: boolean
3172
+ ```
3173
+
3174
+ Whether [AccountApi.requestSignIn](#accountapi-requestsignin) can show this player a sign-in
3175
+ here. Read it before you draw a "Sign in to claim" button, and hide the
3176
+ offer when it is `false`.
3177
+
3178
+ - `true` — a guest the arcade can offer sign-up to. An ask may still
3179
+ answer `declined` at a busy moment.
3180
+ - `false` — an ask would show nothing. The player is already signed in
3181
+ (it answers `signed_in` at once; every player in a current Gem Arcade
3182
+ app build, v3.33.174 and later, is), or there is no Arcade sign-in here
3183
+ (it answers `unavailable`). Also `false` until the player is known.
3184
+
3185
+ On an arcade older than this signal it reads `true` for every player, a
3186
+ full account included, as before it existed. So gate a sign-up gift on it
3187
+ and on the guest test you use today; see docs/player-sign-in.md.
3188
+ [AccountApi.onSignInAvailableChange](#accountapi-onsigninavailablechange) tells you when it changes.
3189
+
3190
+ ```ts
3191
+ const draw = (available: boolean) => {
3192
+ signInButton.hidden = !available;
3193
+ };
3194
+ draw(gem.account.signInAvailable);
3195
+ gem.account.onSignInAvailableChange(draw);
3196
+ ```
3197
+
3198
+ **Guides.** [player-sign-in](../player-sign-in.md)
3199
+
3200
+ <a id="accountapi-onsigninavailablechange"></a>
3201
+
3202
+ ## AccountApi.onSignInAvailableChange
3203
+
3204
+ ```ts
3205
+ onSignInAvailableChange(listener: (available: boolean) => void): () => void
3206
+ ```
3207
+
3208
+ Runs `listener` whenever [AccountApi.signInAvailable](#accountapi-signinavailable) changes: when
3209
+ the player arrives, when the arcade changes its answer (a guest who signs
3210
+ up), and when the connection closes. Not on a re-sent player with the same
3211
+ answer.
3212
+
3213
+ | Parameter | Type |
3214
+ | --- | --- |
3215
+ | `listener` | `(available: boolean) => void` |
3216
+
3217
+ **Returns.** A function that stops the listener.
3218
+
3159
3219
  <a id="accountapi-requestsignin"></a>
3160
3220
 
3161
3221
  ## AccountApi.requestSignIn
@@ -3419,6 +3479,13 @@ means the platform would grant if an ad is watched or, where no ad serves,
3419
3479
  under the placement's `without_ads: grant`; a player can still press the
3420
3480
  button and get `no_ad`, for reasons your game is not told.
3421
3481
 
3482
+ A placement your config also prices in Coins or Gems (`platform_prices`)
3483
+ carries those prices in `options`, where the platform lets players pay for
3484
+ one, and `adGrantable` says whether the ad itself could pay. Label your
3485
+ button from both: name the ad only when `adGrantable` is `true`, and each
3486
+ price in `options`. The Arcade then shows the player those choices when
3487
+ they press, and the press answers and is re-read exactly as an ad's is.
3488
+
3422
3489
  NOTHING REJECTS, like everything else on `gem.ads`. Without a play token,
3423
3490
  when the read fails or takes longer than ten seconds, or for a `placement`
3424
3491
  outside the grammar [AdRequest.placement](#adrequest-placement) documents, it resolves not
@@ -3450,6 +3517,52 @@ if (status.nextEligibleAt !== undefined) {
3450
3517
 
3451
3518
  **Guides.** [rewarded-ads](../rewarded-ads.md)
3452
3519
 
3520
+ <a id="rewardedoption"></a>
3521
+
3522
+ ## RewardedOption
3523
+
3524
+ ```ts
3525
+ interface RewardedOption
3526
+ ```
3527
+
3528
+ One price that also unlocks a rewarded placement, in
3529
+ [RewardedStatus.options](#rewardedstatus-options): what your config's `platform_prices` set for
3530
+ it, as the platform serves it. Label your button from it ("5 Gems").
3531
+
3532
+ It says nothing about the player: not their balance, and not whether they
3533
+ can afford it. The Arcade shows them that when they press.
3534
+
3535
+ <a id="rewardedoption-kind"></a>
3536
+
3537
+ ## RewardedOption.kind
3538
+
3539
+ ```ts
3540
+ readonly kind: "coins" | "gems"
3541
+ ```
3542
+
3543
+ Which platform currency: `coins` (Coins, `ARCADE_TICKETS`) or `gems`
3544
+ (Gems, `GEMS`).
3545
+
3546
+ <a id="rewardedoption-currencyid"></a>
3547
+
3548
+ ## RewardedOption.currencyId
3549
+
3550
+ ```ts
3551
+ readonly currencyId: string
3552
+ ```
3553
+
3554
+ The platform currency's id, as your config's `platform_prices` resolved it.
3555
+
3556
+ <a id="rewardedoption-price"></a>
3557
+
3558
+ ## RewardedOption.price
3559
+
3560
+ ```ts
3561
+ readonly price: number
3562
+ ```
3563
+
3564
+ The price, a positive whole number of the currency.
3565
+
3453
3566
  <a id="rewardedstatus"></a>
3454
3567
 
3455
3568
  ## RewardedStatus
@@ -3495,12 +3608,48 @@ readonly grantable: boolean
3495
3608
 
3496
3609
  Whether pressing your button could pay right now: the platform would grant
3497
3610
  the reward if an ad is watched or, where no ad serves, under the
3498
- placement's `without_ads: grant`.
3611
+ placement's `without_ads: grant` — or, where [RewardedStatus.options](#rewardedstatus-options)
3612
+ is present, if the player pays one of its prices.
3613
+
3614
+ **Not a promise that an ad will play, and not a promise of a reward.** For
3615
+ the ad, it is `false` whenever a cap is spent, a cooldown is running, the
3616
+ player holds `max_held`, rewarded is off on this channel, or the arcade
3617
+ running your game does not take ad requests at all. The cap, the cooldown
3618
+ and `max_held` limit the ad only: a placement with `options` stays
3619
+ grantable through them, because the player can still pay. It never says
3620
+ whether the player can afford a price.
3621
+
3622
+ <a id="rewardedstatus-adgrantable"></a>
3623
+
3624
+ ## RewardedStatus.adGrantable
3625
+
3626
+ ```ts
3627
+ readonly adGrantable: boolean
3628
+ ```
3629
+
3630
+ Whether the AD could pay right now: [RewardedStatus.grantable](#rewardedstatus-grantable) for
3631
+ the ad alone, leaving out the prices. Name the ad on your button only when
3632
+ this is `true`.
3633
+
3634
+ The same as `grantable` for a placement with no `options`. `false` for a
3635
+ placement whose config switches the ad off (`ad: false`) where the
3636
+ platform lets players pay for it, and on every answer where `grantable` is.
3637
+
3638
+ <a id="rewardedstatus-options"></a>
3639
+
3640
+ ## RewardedStatus.options
3641
+
3642
+ ```ts
3643
+ readonly options?: readonly RewardedOption[]
3644
+ ```
3645
+
3646
+ The Coin and Gem prices that also unlock the placement, from your config's
3647
+ `platform_prices`, at most one per currency.
3499
3648
 
3500
- **Not a promise that an ad will play, and not a promise of a reward.** It
3501
- is `false` whenever a cap is spent, a cooldown is running, the player holds
3502
- `max_held`, rewarded is off on this channel, or the arcade running your
3503
- game does not take ad requests at all.
3649
+ ABSENT — no key — for a placement with no prices, on any answer where
3650
+ `declared` is `false`, and wherever the platform does not yet let players
3651
+ pay for a placement: there your prices are not offered and the placement
3652
+ unlocks by its ad alone. Nothing here says what the player holds.
3504
3653
 
3505
3654
  <a id="rewardedstatus-remainingtoday"></a>
3506
3655
 
@@ -3903,7 +4052,8 @@ dimension for it on the events you want to break down, with the arms as its
3903
4052
  values, named after the experiment, and pass the selection there yourself:
3904
4053
  `gem.analytics.track('tutorial_finished', { tutorial_length: tutorial })`.
3905
4054
  Never the platform's `variant` field, which belongs to store-page
3906
- experiments.
4055
+ experiments. For the platform's results to show the test, also declare it
4056
+ under `analytics_tests` in your game config (see docs/analytics.md).
3907
4057
 
3908
4058
  WAITS FOR THE PLAYER. The id arrives with the first play token, so a call
3909
4059
  during boot waits for it, and rejects as [Gem.awaitIdentity](#gem-awaitidentity) does
@@ -1,6 +1,6 @@
1
1
  # The server surface — API reference
2
2
 
3
- Generated from `@indiegems/gem-web-sdk/sdk` v23.1.3, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
3
+ Generated from `@indiegems/gem-web-sdk/sdk` v23.3.0, the version of the package this file ships in. Every symbol below carries an explicit anchor, its `anchor` in `reference.json` beside this file.
4
4
 
5
5
  `@indiegems/gem-web-sdk/sdk` is the one import a game writes, wherever its code runs. `@indiegems/gem-web-sdk/server` exports exactly the symbols on this page and nothing else, for a build that wants only this half.
6
6
 
@@ -2,7 +2,7 @@
2
2
  "schema_version": 2,
3
3
  "source": {
4
4
  "repo": "IndieGems/gem-web-sdk",
5
- "version": "23.1.3"
5
+ "version": "23.3.0"
6
6
  },
7
7
  "entries": [
8
8
  {
@@ -397,7 +397,7 @@
397
397
  "group": "config",
398
398
  "signature": "rewarded_placements?: SnapshotRewardedPlacement[]",
399
399
  "documented": true,
400
- "summary": "Your rewarded placements — what pressing a rewarded button grants. An\narray, one row per placement, each naming itself in `placement`, with its\n`grants`, the caps the platform enforces on it (`max_per_day`,\n`cooldown_seconds`, `max_held`) and what happens where no ad serves\n(`without_ads`, which is `nothing` when the config leaves it out).\n\nRead it to LABEL your button — \"+1 Revive\" — never to decide a reward. A\ngrant is an `item_grant`, with a `quantity` of the item its\n`item_definition_id` names, or a `currency_grant`, with an `amount` of the\ncurrency its `currency_id` names; the word on the button is that\ndefinition's `display_name`, from [GameConfig.item](api-reference-game.md#gameconfig-item) or\n[GameConfig.currency](api-reference-game.md#gameconfig-currency):\n\n```js\nconst row = config?.rewarded_placements?.find((p) => p.placement === 'classic_revive');\nconst grant = row?.grants[0];\nconst label =\n grant?.type === 'item_grant'\n ? `+${grant.quantity} ${config.item(grant.item_definition_id)?.display_name ?? ''}`\n : grant?.type === 'currency_grant'\n ? `+${grant.amount} ${config.currency(grant.currency_id)?.display_name ?? ''}`\n : undefined;\n```\n\nWhether an ask grants anything is the platform's decision, made against\nthis config server-side; the grant lands on your game's inventory, and\nre-reading `gem.me.inventory` is the only way to learn that it did. Whether\nthe button could pay right now is [AdsApi.rewardedStatus](api-reference-game.md#adsapi-rewardedstatus).\n\nAbsent when your game declares none.",
400
+ "summary": "Your rewarded placements — what pressing a rewarded button grants. An\narray, one row per placement, each naming itself in `placement`, with its\n`grants`, the caps the platform enforces on it (`max_per_day`,\n`cooldown_seconds`, `max_held`) and what happens where no ad serves\n(`without_ads`, which is `nothing` when the config leaves it out).\n\nRead it to LABEL your button — \"+1 Revive\" — never to decide a reward. A\ngrant is an `item_grant`, with a `quantity` of the item its\n`item_definition_id` names, or a `currency_grant`, with an `amount` of the\ncurrency its `currency_id` names; the word on the button is that\ndefinition's `display_name`, from [GameConfig.item](api-reference-game.md#gameconfig-item) or\n[GameConfig.currency](api-reference-game.md#gameconfig-currency):\n\n```js\nconst row = config?.rewarded_placements?.find((p) => p.placement === 'classic_revive');\nconst grant = row?.grants[0];\nconst label =\n grant?.type === 'item_grant'\n ? `+${grant.quantity} ${config.item(grant.item_definition_id)?.display_name ?? ''}`\n : grant?.type === 'currency_grant'\n ? `+${grant.amount} ${config.currency(grant.currency_id)?.display_name ?? ''}`\n : undefined;\n```\n\nWhether an ask grants anything is the platform's decision, made against\nthis config server-side; the grant lands on your game's inventory, and\nre-reading `gem.me.inventory` is the only way to learn that it did. Whether\nthe button could pay right now is [AdsApi.rewardedStatus](api-reference-game.md#adsapi-rewardedstatus).\n\nA row may also carry `platform_prices`, the Coin and Gem prices that unlock\nit beside the ad, and `ad: false` when only a price does (`ad` is absent\notherwise, which means the ad unlocks it). Both are what you declared, not\nwhat is offered now: label the button from `rewardedStatus`'s\n`adGrantable` and `options`, which say what this platform serves.\n\nAbsent when your game declares none.",
401
401
  "deprecated": null,
402
402
  "params": [],
403
403
  "returns": null,
@@ -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",
@@ -4170,7 +4214,7 @@
4170
4214
  "group": "ads",
4171
4215
  "signature": "rewardedStatus(placement: string): Promise<RewardedStatus>",
4172
4216
  "documented": true,
4173
- "summary": "Whether a rewarded placement could pay right now, so you can decide whether\nto draw its button. **It never says whether an ad will play.**\n\nThe answer comes from your own `rewarded_placements` config — its daily\ncap, cooldown and `max_held` — applied to this player's use of it, as the\nplatform holds them. Nothing in it describes the player. `grantable: true`\nmeans the platform would grant if an ad is watched or, where no ad serves,\nunder the placement's `without_ads: grant`; a player can still press the\nbutton and get `no_ad`, for reasons your game is not told.\n\nNOTHING REJECTS, like everything else on `gem.ads`. Without a play token,\nwhen the read fails or takes longer than ten seconds, or for a `placement`\noutside the grammar [AdRequest.placement](api-reference-game.md#adrequest-placement) documents, it resolves not\ndeclared and not grantable, with no `resetsAt`, and the reason goes to your\n`onError` handler.\n\nIt is a read of the platform, so call it when you draw the button — on a\nresults or death screen — not every frame. Calls for one placement that\noverlap share one request. Draw the rest of the screen first rather than\nholding it on this answer.\n\n⚠️ It grants nothing and reserves nothing. The reward is granted by the\nplatform when the player asks with `requestAd`; re-read `gem.me.inventory`\nafter that ask settles, whatever it settled as.",
4217
+ "summary": "Whether a rewarded placement could pay right now, so you can decide whether\nto draw its button. **It never says whether an ad will play.**\n\nThe answer comes from your own `rewarded_placements` config — its daily\ncap, cooldown and `max_held` — applied to this player's use of it, as the\nplatform holds them. Nothing in it describes the player. `grantable: true`\nmeans the platform would grant if an ad is watched or, where no ad serves,\nunder the placement's `without_ads: grant`; a player can still press the\nbutton and get `no_ad`, for reasons your game is not told.\n\nA placement your config also prices in Coins or Gems (`platform_prices`)\ncarries those prices in `options`, where the platform lets players pay for\none, and `adGrantable` says whether the ad itself could pay. Label your\nbutton from both: name the ad only when `adGrantable` is `true`, and each\nprice in `options`. The Arcade then shows the player those choices when\nthey press, and the press answers and is re-read exactly as an ad's is.\n\nNOTHING REJECTS, like everything else on `gem.ads`. Without a play token,\nwhen the read fails or takes longer than ten seconds, or for a `placement`\noutside the grammar [AdRequest.placement](api-reference-game.md#adrequest-placement) documents, it resolves not\ndeclared and not grantable, with no `resetsAt`, and the reason goes to your\n`onError` handler.\n\nIt is a read of the platform, so call it when you draw the button — on a\nresults or death screen — not every frame. Calls for one placement that\noverlap share one request. Draw the rest of the screen first rather than\nholding it on this answer.\n\n⚠️ It grants nothing and reserves nothing. The reward is granted by the\nplatform when the player asks with `requestAd`; re-read `gem.me.inventory`\nafter that ask settles, whatever it settled as.",
4174
4218
  "deprecated": null,
4175
4219
  "params": [
4176
4220
  {
@@ -4191,6 +4235,70 @@
4191
4235
  }
4192
4236
  ]
4193
4237
  },
4238
+ {
4239
+ "id": "RewardedOption",
4240
+ "name": "RewardedOption",
4241
+ "anchor": "rewardedoption",
4242
+ "group": "ads",
4243
+ "signature": "interface RewardedOption",
4244
+ "documented": true,
4245
+ "summary": "One price that also unlocks a rewarded placement, in\n[RewardedStatus.options](api-reference-game.md#rewardedstatus-options): what your config's `platform_prices` set for\nit, as the platform serves it. Label your button from it (\"5 Gems\").\n\nIt says nothing about the player: not their balance, and not whether they\ncan afford it. The Arcade shows them that when they press.",
4246
+ "deprecated": null,
4247
+ "params": [],
4248
+ "returns": null,
4249
+ "throws": null,
4250
+ "example": null,
4251
+ "defaultValue": null,
4252
+ "guides": []
4253
+ },
4254
+ {
4255
+ "id": "RewardedOption.kind",
4256
+ "name": "kind",
4257
+ "anchor": "rewardedoption-kind",
4258
+ "group": "ads",
4259
+ "signature": "readonly kind: \"coins\" | \"gems\"",
4260
+ "documented": true,
4261
+ "summary": "Which platform currency: `coins` (Coins, `ARCADE_TICKETS`) or `gems`\n(Gems, `GEMS`).",
4262
+ "deprecated": null,
4263
+ "params": [],
4264
+ "returns": null,
4265
+ "throws": null,
4266
+ "example": null,
4267
+ "defaultValue": null,
4268
+ "guides": []
4269
+ },
4270
+ {
4271
+ "id": "RewardedOption.currencyId",
4272
+ "name": "currencyId",
4273
+ "anchor": "rewardedoption-currencyid",
4274
+ "group": "ads",
4275
+ "signature": "readonly currencyId: string",
4276
+ "documented": true,
4277
+ "summary": "The platform currency's id, as your config's `platform_prices` resolved it.",
4278
+ "deprecated": null,
4279
+ "params": [],
4280
+ "returns": null,
4281
+ "throws": null,
4282
+ "example": null,
4283
+ "defaultValue": null,
4284
+ "guides": []
4285
+ },
4286
+ {
4287
+ "id": "RewardedOption.price",
4288
+ "name": "price",
4289
+ "anchor": "rewardedoption-price",
4290
+ "group": "ads",
4291
+ "signature": "readonly price: number",
4292
+ "documented": true,
4293
+ "summary": "The price, a positive whole number of the currency.",
4294
+ "deprecated": null,
4295
+ "params": [],
4296
+ "returns": null,
4297
+ "throws": null,
4298
+ "example": null,
4299
+ "defaultValue": null,
4300
+ "guides": []
4301
+ },
4194
4302
  {
4195
4303
  "id": "RewardedStatus",
4196
4304
  "name": "RewardedStatus",
@@ -4230,7 +4338,39 @@
4230
4338
  "group": "ads",
4231
4339
  "signature": "readonly grantable: boolean",
4232
4340
  "documented": true,
4233
- "summary": "Whether pressing your button could pay right now: the platform would grant\nthe reward if an ad is watched or, where no ad serves, under the\nplacement's `without_ads: grant`.\n\n**Not a promise that an ad will play, and not a promise of a reward.** It\nis `false` whenever a cap is spent, a cooldown is running, the player holds\n`max_held`, rewarded is off on this channel, or the arcade running your\ngame does not take ad requests at all.",
4341
+ "summary": "Whether pressing your button could pay right now: the platform would grant\nthe reward if an ad is watched or, where no ad serves, under the\nplacement's `without_ads: grant` — or, where [RewardedStatus.options](api-reference-game.md#rewardedstatus-options)\nis present, if the player pays one of its prices.\n\n**Not a promise that an ad will play, and not a promise of a reward.** For\nthe ad, it is `false` whenever a cap is spent, a cooldown is running, the\nplayer holds `max_held`, rewarded is off on this channel, or the arcade\nrunning your game does not take ad requests at all. The cap, the cooldown\nand `max_held` limit the ad only: a placement with `options` stays\ngrantable through them, because the player can still pay. It never says\nwhether the player can afford a price.",
4342
+ "deprecated": null,
4343
+ "params": [],
4344
+ "returns": null,
4345
+ "throws": null,
4346
+ "example": null,
4347
+ "defaultValue": null,
4348
+ "guides": []
4349
+ },
4350
+ {
4351
+ "id": "RewardedStatus.adGrantable",
4352
+ "name": "adGrantable",
4353
+ "anchor": "rewardedstatus-adgrantable",
4354
+ "group": "ads",
4355
+ "signature": "readonly adGrantable: boolean",
4356
+ "documented": true,
4357
+ "summary": "Whether the AD could pay right now: [RewardedStatus.grantable](api-reference-game.md#rewardedstatus-grantable) for\nthe ad alone, leaving out the prices. Name the ad on your button only when\nthis is `true`.\n\nThe same as `grantable` for a placement with no `options`. `false` for a\nplacement whose config switches the ad off (`ad: false`) where the\nplatform lets players pay for it, and on every answer where `grantable` is.",
4358
+ "deprecated": null,
4359
+ "params": [],
4360
+ "returns": null,
4361
+ "throws": null,
4362
+ "example": null,
4363
+ "defaultValue": null,
4364
+ "guides": []
4365
+ },
4366
+ {
4367
+ "id": "RewardedStatus.options",
4368
+ "name": "options",
4369
+ "anchor": "rewardedstatus-options",
4370
+ "group": "ads",
4371
+ "signature": "readonly options?: readonly RewardedOption[]",
4372
+ "documented": true,
4373
+ "summary": "The Coin and Gem prices that also unlock the placement, from your config's\n`platform_prices`, at most one per currency.\n\nABSENT — no key — for a placement with no prices, on any answer where\n`declared` is `false`, and wherever the platform does not yet let players\npay for a placement: there your prices are not offered and the placement\nunlocks by its ad alone. Nothing here says what the player holds.",
4234
4374
  "deprecated": null,
4235
4375
  "params": [],
4236
4376
  "returns": null,
@@ -4639,7 +4779,7 @@
4639
4779
  "group": "analytics",
4640
4780
  "signature": "experiment(key: string, count: number): Promise<number>",
4641
4781
  "documented": true,
4642
- "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.",
4643
4783
  "deprecated": null,
4644
4784
  "params": [
4645
4785
  {
@@ -30922,7 +31062,7 @@
30922
31062
  "group": "analytics",
30923
31063
  "signature": "experiment(key: string, count: number): Promise<number>",
30924
31064
  "documented": true,
30925
- "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.",
30926
31066
  "deprecated": null,
30927
31067
  "params": [
30928
31068
  {
@@ -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') showSignInButton(); // gem.account.requestSignIn
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,36 @@ 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
+
37
+ ## 23.2.0
38
+
39
+ *Written from the API facts alone; no summary was generated for this release.*
40
+
41
+ Upgrading from 23.1.3. Impact: none.
42
+
43
+ ### New capability
44
+
45
+ - RewardedOption was added.
46
+ - RewardedStatus.adGrantable was added.
47
+ - RewardedStatus.options was added.
48
+
19
49
  ## 23.1.3
20
50
 
21
51
  Upgrading from 23.1.2. Impact: none.
@@ -50,7 +50,7 @@ either way.
50
50
  ```jsonc
51
51
  // package.json
52
52
  "dependencies": {
53
- "@indiegems/gem-web-sdk": "23.1.3"
53
+ "@indiegems/gem-web-sdk": "23.3.0"
54
54
  }
55
55
  ```
56
56
 
@@ -13,13 +13,13 @@
13
13
  },
14
14
  {
15
15
  "file": "analytics.md",
16
- "sha256": "5814655859e3d6b223f94b71f1b064783b0f242d409c3ec3078089cb4b7c8ec5",
17
- "updated": "2026-11-04T17:17:14Z"
16
+ "sha256": "3f81025a763f96d0ca17f57377042a3e3890797983265cdb1721902b700aa3a6",
17
+ "updated": "2026-11-09T18:54:26Z"
18
18
  },
19
19
  {
20
20
  "file": "arcade-and-store.md",
21
- "sha256": "79bd7829d5a6577d3619d7fd831b74065df883e11eadd1b8153754d97a88474a",
22
- "updated": "2026-11-02T18:42:37Z"
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": "e6195c6d12e52535a21a4a3291268a82a445b85e6a76f0049b981ff9ab374232",
52
- "updated": "2026-11-09T03:45:00Z"
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": "aaed9165f80ea79b32eae8509dc2ef0af436b3e3df1d23cac30941c8268ee022",
82
- "updated": "2026-11-09T03:45:00Z"
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": "22c798ff32ee15dda88f08f6701146f83fe92568c737d9321edd3e2550cb2df9",
122
- "updated": "2026-11-09T02:37:14Z"
121
+ "sha256": "f5066f2382bac9336c697d3646843f546295686a959726e877431ec7dc2daea3",
122
+ "updated": "2026-11-09T18:39:56Z"
123
123
  },
124
124
  {
125
125
  "file": "rewarded-ads.md",
126
- "sha256": "81b421d4bde3f71b40c7a5a173e6a2c114a315215a11491e488872ff9e1909dc",
127
- "updated": "2026-11-07T16:40:01Z"
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": "3274318b99daed546ee192381e40962c9e1e62436cbf4edfb61340548d6dc493",
147
- "updated": "2026-11-09T03:45:00Z"
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": "f70fe9fad67c6b386bddc53f1f876bb78f6a5e11c35f565bb93e925d65a99874",
172
- "updated": "2026-11-06T22:48:07Z"
171
+ "sha256": "c5400f43a1218de0d2838d4332dce4c05afe21ff04e352d2e201ef5bb4268bed",
172
+ "updated": "2026-11-09T18:39:56Z"
173
173
  }
174
174
  ]
175
175
  }