@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.
- package/CHANGELOG.md +30 -0
- package/dist/game.d.ts +13 -2
- 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 +3 -3
- 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 +13 -2
- package/docs/analytics.md +40 -0
- package/docs/api/api-reference-function.md +3 -2
- package/docs/api/api-reference-game.md +157 -7
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +147 -7
- package/docs/arcade-and-store.md +4 -2
- package/docs/changelog.md +30 -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 +113 -8
- 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/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
|
@@ -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
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
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 }
|