@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.
@@ -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 password; it
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.
@@ -111,6 +111,8 @@ rewarded_placements:
111
111
  | `max_held` | No grant while the player already holds at least this many of the granted item. 1 to 100, and only on a placement whose grants are all items. |
112
112
  | `without_ads` | **Optional:** `grant` or `nothing`, and `nothing` when you leave it out. What a press gets when no ad plays: where no ad serves your channel at all, and where one does but no ad was available for this press or it failed to play (§8). `grant` pays the reward with no ad, within your caps, so write it out only for a reward that is fine to give free. |
113
113
  | `allowance_currency_slug` | Optional: one of your own currencies that each grant also spends 1 of. Use it when another path in your game must share the same daily cap. It must not be `client_writable` and must have `transfer_policy: none`. A grant is refused while it is empty (§3). ⚠️ For an allowance your game grants per use, put it in a bucket that never resets (`reset_interval: never`) for now, even though the upload warns `rewarded_placement_allowance_no_reset` (below). |
114
+ | `ad` | **Optional:** `true` or `false`, and `true` when you leave it out. Whether watching an ad unlocks the placement. Write `false` only beside `platform_prices`, for a placement a price alone unlocks. |
115
+ | `platform_prices` | **Optional:** Coin and Gem prices that also unlock the placement, beside the ad or instead of it, written as an item's prices are. Leave it out for none. See [Coins and Gems beside the ad](#coins-and-gems-beside-the-ad), below. |
114
116
 
115
117
  ⚠️ **Until a gem-api fix ships, an allowance in a resetting bucket refuses a player's first
116
118
  press.** A grant that creates the player's balance of the allowance currency is not stamped with
@@ -170,6 +172,91 @@ pays without one. If both said `grant`, the bigger reward would cost the player
170
172
  everyone would press that button. Leaving it out of the bigger one, which is `nothing`, keeps it a
171
173
  reward for watching.
172
174
 
175
+ ### Coins and Gems beside the ad
176
+
177
+ A placement can also be unlocked for the player's **Coins** (`ARCADE_TICKETS`) or **Gems** (`GEMS`),
178
+ beside its ad or instead of it. Give it `platform_prices`, the same shape an item's prices take,
179
+ at most one per currency, each a positive whole number:
180
+
181
+ ```yaml
182
+ rewarded_placements:
183
+ - placement: classic_revive
184
+ grants:
185
+ - { type: item_grant, item_definition_slug: revive_token, quantity: 1 }
186
+ max_per_day: 5 # limits the ad only
187
+ platform_prices:
188
+ - { currency: ARCADE_TICKETS, price: 50 } # 50 Coins
189
+ - { currency: GEMS, price: 5 } # 5 Gems
190
+ - placement: skip_level
191
+ grants:
192
+ - { type: item_grant, item_definition_slug: level_skip, quantity: 1 }
193
+ ad: false # a price alone unlocks this one
194
+ platform_prices:
195
+ - { currency: GEMS, price: 10 }
196
+ ```
197
+
198
+ Your game asks exactly as it does for an ad: `requestAd` from the press (§3), then re-read your
199
+ inventory. The Arcade, not your game, shows the player the choices that press has: a chooser when
200
+ there are several, such as **Watch an ad**, **50 Coins** and **5 Gems**, or a confirmation when a
201
+ price is the only one. A price the player cannot afford is shown but cannot be picked. Paying takes
202
+ the price from the player's wallet and grants the reward in one step: never the reward without the
203
+ payment, never charged twice, and the payment given back if the reward cannot be granted. Nothing
204
+ is bought with real money here. The answer is `shown` whenever the Arcade put a choice up, whatever
205
+ the player then did, so **branch on your inventory, never on the outcome**, as for an ad.
206
+
207
+ - **Your caps limit the ad only.** `max_per_day`, `cooldown_seconds`, `max_held` and
208
+ `allowance_currency_slug` decide when the ad can pay. The prices stay on offer when the ad
209
+ cannot, and a paid unlock does not count toward them. Only the player's balance limits how often
210
+ they pay, so price the reward for that. The grant still refuses a reward that would not fit
211
+ (a full stack, say), and the payment is then given back.
212
+ - **Not with `without_ads: grant`,** which would give the reward free and make the price pointless.
213
+ The upload refuses it, and `ad: false` with no `platform_prices` too, since then nothing unlocks
214
+ the placement.
215
+ - **Each currency's spend scope applies,** as it does to an item's price
216
+ ([`inventory-and-catalogs.md`](inventory-and-catalogs.md#what-coins-and-gems-may-buy-the-spend-scope)):
217
+ where Coins may price cosmetics only, a Coin price on a placement that grants anything else is
218
+ refused. Gems may price any placement. `gemctl platform config` lists which currencies your
219
+ game may price in, and each one's spend scope: author against it, never against a list you
220
+ remember.
221
+ - **A placement's own prices do not make its reward "sold for a platform currency"** in §1's sense,
222
+ so the ad keeps serving beside them as it did before.
223
+
224
+ ⚠️ **The upload warns `rewarded_paid_unlocks_not_live` while the platform does not let players pay
225
+ for a placement yet.** Until then your prices are not offered, the placement unlocks by its ad
226
+ alone, `ad: false` included, and `rewardedStatus` carries no `options` (§5). Nothing in your game
227
+ needs to change when it goes live: draw the button from `rewardedStatus` and it follows.
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
+
173
260
  ## 3. Ask from the press, then re-read
174
261
 
175
262
  ```js
@@ -342,14 +429,32 @@ const status = await gem.ads.rewardedStatus('classic_revive');
342
429
  watchAdButton.hidden = !status.grantable;
343
430
  ```
344
431
 
345
- | field | what it says |
346
- | ------------------ | -------------------------------------------------------------------------- |
347
- | `declared` | the config promoted to the channel being played names this placement |
348
- | `grantable` | pressing the button could pay right now — **not** that an ad will play |
349
- | `remainingToday` | grants left today under the daily cap; absent when the placement has none |
350
- | `nextEligibleAt` | when a running cooldown ends, in epoch milliseconds; absent when none runs |
351
- | `heldLimitReached` | the player already holds the placement's `max_held` — spend one first |
352
- | `resetsAt` | when today's count resets, the next 00:00 UTC, in epoch milliseconds |
432
+ **For a placement with prices** (§2), `grantable` is `true` when the ad could pay **or** a price
433
+ could, so the button stays up through the ad's caps. Label it from the answer: name the ad only
434
+ when `adGrantable` is `true`, and each price in `options`, whose `kind` is `coins` or `gems`.
435
+ Neither says what the player holds; the Arcade shows that when they press. Both are in SDK
436
+ releases after 23.1.3.
437
+
438
+ ```js
439
+ const status = await gem.ads.rewardedStatus('classic_revive');
440
+ const ways = [
441
+ ...(status.adGrantable ? ['watch an ad'] : []),
442
+ ...(status.options ?? []).map((o) => `${o.price} ${o.kind === 'coins' ? 'Coins' : 'Gems'}`),
443
+ ];
444
+ reviveButton.textContent = `Revive: ${ways.join(' or ')}`; // "Revive: watch an ad or 5 Gems"
445
+ reviveButton.hidden = !status.grantable;
446
+ ```
447
+
448
+ | field | what it says |
449
+ | ------------------ | ------------------------------------------------------------------------------------------ |
450
+ | `declared` | the config promoted to the channel being played names this placement |
451
+ | `grantable` | pressing the button could pay right now — **not** that an ad will play |
452
+ | `adGrantable` | the ad itself could pay right now: `grantable` without the prices |
453
+ | `options` | the Coin and Gem prices on offer, each `{ kind, currencyId, price }`; absent when none are |
454
+ | `remainingToday` | grants left today under the daily cap; absent when the placement has none |
455
+ | `nextEligibleAt` | when a running cooldown ends, in epoch milliseconds; absent when none runs |
456
+ | `heldLimitReached` | the player already holds the placement's `max_held` — spend one first |
457
+ | `resetsAt` | when today's count resets, the next 00:00 UTC, in epoch milliseconds |
353
458
 
354
459
  Without a play token, when the read fails or takes longer than ten seconds, or for a placement
355
460
  outside the grammar, it answers not declared and not grantable with no `resetsAt`, and the reason
@@ -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.1.3/gem-web-sdk.tgz
84
- curl -fsSL https://downloads.gemarcade.com/sdk/v23.1.3/gem-web-sdk.tgz.sri
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
- Two things before you reach for it: **save first**, because the Arcade may sign the player in by
2408
- taking the whole page away, and your game then starts again rather than hearing an answer; and on
2409
- `signed_in`, **re-read what you loaded**, because the player underneath your game may not be the
2410
- one who was there a moment ago. [`player-sign-in.md`](player-sign-in.md) is the page.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indiegems/gem-web-sdk",
3
- "version": "23.1.3",
3
+ "version": "23.3.0",
4
4
  "description": "postMessage bridge contract between gem-web and browser-playable games",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -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 }