@indiegems/gem-web-sdk 19.0.2 → 20.0.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 +12 -0
- package/dist/conformance.d.ts +1 -1
- package/dist/gem-web-sdk.conformance.js +3 -3
- package/dist/gem-web-sdk.esm.js +1 -1
- package/dist/gem-web-sdk.function.js +1 -1
- package/dist/gem-web-sdk.game.js +2 -2
- package/dist/gem-web-sdk.min.js +2 -2
- package/dist/gem-web-sdk.sdk.js +2 -2
- package/dist/gem-web-sdk.server.js +1 -1
- package/docs/api/api-reference-function.md +1 -1
- package/docs/api/api-reference-game.md +11 -10
- package/docs/api/api-reference-server.md +1 -1
- package/docs/api/reference.json +4 -4
- package/docs/arcade-and-store.md +48 -43
- package/docs/changelog.md +12 -0
- package/docs/getting-started.md +1 -1
- package/docs/inventory-and-catalogs.md +23 -22
- package/docs/manifest.json +10 -10
- package/docs/server-bundles.md +2 -2
- package/package.json +1 -1
- package/schema/bridge.v1.json +55 -58
package/docs/arcade-and-store.md
CHANGED
|
@@ -33,7 +33,7 @@ itself. You get an outcome.
|
|
|
33
33
|
- [1. Two surfaces, and which one is real money](#1-two-surfaces-and-which-one-is-real-money)
|
|
34
34
|
- [2. Nothing rejects](#2-nothing-rejects)
|
|
35
35
|
- [3. What is available here](#3-what-is-available-here)
|
|
36
|
-
- [4.
|
|
36
|
+
- [4. Leaving takes a press; buying does not](#4-leaving-takes-a-press-buying-does-not)
|
|
37
37
|
- [5. App links](#5-app-links)
|
|
38
38
|
- [6. Opening another game](#6-opening-another-game)
|
|
39
39
|
- [7. Similar games, as cards](#7-similar-games-as-cards)
|
|
@@ -88,20 +88,25 @@ the bridge is up. A feature can be off for reasons you
|
|
|
88
88
|
cannot see from inside the frame: a TV, an in-app browser that cannot take payment, an app build
|
|
89
89
|
where it is withheld. Do not guess; read it.
|
|
90
90
|
|
|
91
|
-
## 4.
|
|
91
|
+
## 4. Leaving takes a press; buying does not
|
|
92
92
|
|
|
93
93
|
Four calls put something in front of the player: `gem.arcade.openApp`, `gem.arcade.openGame`,
|
|
94
|
-
`gem.store.purchase` and `gem.store.showOffer`.
|
|
95
|
-
|
|
96
|
-
`
|
|
97
|
-
|
|
94
|
+
`gem.store.purchase` and `gem.store.showOffer`.
|
|
95
|
+
|
|
96
|
+
- **Leaving the game takes a press.** Call `openApp` and `openGame` from the player's own press
|
|
97
|
+
handler, in the same turn as the press. The Arcade checks that a person pressed, and refuses an
|
|
98
|
+
ask nobody pressed for with `declined` / `busy`, showing nothing. Never on a timer, never on
|
|
99
|
+
load, never in a retry loop.
|
|
100
|
+
- **Buying does not.** `gem.store.purchase` and `gem.store.showOffer` open their checkout or sheet
|
|
101
|
+
after an `await`, from a timer, or where the browser cannot report a press: the checkout is where
|
|
102
|
+
the player confirms, and your game cannot press it. Nothing backs off either, so the player's
|
|
103
|
+
next press after closing a checkout opens it again. Still ask only because the player chose to
|
|
104
|
+
buy.
|
|
98
105
|
- **Controllers count.** A gamepad press the Arcade sees counts as a press. Do not add a keyboard or
|
|
99
106
|
pointer workaround for pad players.
|
|
100
107
|
- **One at a time.** Only one of these is open at once, across both surfaces and the Arcade's other
|
|
101
108
|
sheets (a Gem purchase, a share, a friend challenge). A second ask while one is open is
|
|
102
109
|
`declined` / `busy`. The SDK answers it for you without sending.
|
|
103
|
-
- **Never on a timer, never on load, never in a retry loop.** None of them would have a press, so
|
|
104
|
-
all of them would be refused.
|
|
105
110
|
|
|
106
111
|
The reads (`appLinks`, `similarGames`, `offers`) need no press. A second call while one is open
|
|
107
112
|
shares the first one's answer.
|
|
@@ -250,9 +255,10 @@ first: the payment provider's page on the web, the app store's own purchase shee
|
|
|
250
255
|
apps. That checkout is where the player confirms and pays. Your game is resumed before the answer
|
|
251
256
|
arrives, so your own celebration is visible.
|
|
252
257
|
|
|
253
|
-
- **Call it
|
|
254
|
-
|
|
255
|
-
much
|
|
258
|
+
- **Call it when the player chooses to buy** ([§4](#4-leaving-takes-a-press-buying-does-not)).
|
|
259
|
+
No press is needed — an `await` before it is fine — but the player's press on your Buy button is
|
|
260
|
+
what they expect to open checkout, so make that button say plainly what it buys and for how much
|
|
261
|
+
(`priceDisplay`).
|
|
256
262
|
- **It has no timeout.** A payment takes as long as it takes, so the call waits for as long as the
|
|
257
263
|
Arcade is connected.
|
|
258
264
|
- `ok`: the player paid. **Read balances and inventory through `gem.me`**; the answer carries no
|
|
@@ -266,10 +272,11 @@ arrives, so your own celebration is visible.
|
|
|
266
272
|
```
|
|
267
273
|
|
|
268
274
|
- `declined` / `cancelled`: the player closed the checkout without paying. Return to your screen
|
|
269
|
-
quietly and **do not ask again unprompted
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
275
|
+
quietly and **do not ask again unprompted**. Nothing backs off: the player's next press opens the
|
|
276
|
+
checkout again, however many times they closed it. An offer the player cannot buy again yet also
|
|
277
|
+
answers `declined` / `cancelled`: the Arcade tells the player when it can be bought, and opens
|
|
278
|
+
nothing.
|
|
279
|
+
- `declined` / `busy`: another checkout or Arcade sheet was already open. Nothing was shown.
|
|
273
280
|
- `unavailable` with `store_off`, `sign_in_required` or `not_found` (a SKU that is not in
|
|
274
281
|
`offers()` right now), or with a payment provider's own failure code: nothing was bought. The
|
|
275
282
|
first three show nothing.
|
|
@@ -304,13 +311,13 @@ pass the player already holds, how long it has left. The sheet's Buy opens the s
|
|
|
304
311
|
|
|
305
312
|
Pick by who can say what the offer is:
|
|
306
313
|
|
|
307
|
-
| | **You draw it: `offers()` + `purchase()`**
|
|
308
|
-
| --------------- |
|
|
309
|
-
| Use it for | Gem packs: a name, an amount and a price say it all
|
|
310
|
-
| Your button | says what it buys and shows `priceDisplay`
|
|
311
|
-
| The press opens | the checkout, straight away
|
|
312
|
-
| Perks and sales | not available to you
|
|
313
|
-
| A holder sees | the same Buy button
|
|
314
|
+
| | **You draw it: `offers()` + `purchase()`** | **The Arcade draws it: `showOffer()`** |
|
|
315
|
+
| --------------- | --------------------------------------------------- | --------------------------------------------------------------- |
|
|
316
|
+
| Use it for | Gem packs: a name, an amount and a price say it all | VIP and other passes, and bundles |
|
|
317
|
+
| Your button | says what it buys and shows `priceDisplay` | says what it opens, such as "Get VIP" |
|
|
318
|
+
| The press opens | the checkout, straight away | the Arcade's sheet, then the checkout if the player presses Buy |
|
|
319
|
+
| Perks and sales | not available to you | always current, in the player's language |
|
|
320
|
+
| A holder sees | the same Buy button | what they already have |
|
|
314
321
|
|
|
315
322
|
- **Never write an offer's perks into your game.** They change without a new build of your game, and
|
|
316
323
|
yours would go stale. Your button only needs the offer's name.
|
|
@@ -328,10 +335,8 @@ Pick by who can say what the offer is:
|
|
|
328
335
|
|
|
329
336
|
Every rule of `purchase` holds ([§9](#9-buying-from-the-arcade-store)), and the answers are the
|
|
330
337
|
same, with one addition. `declined` / `cancelled` also covers the player leaving the sheet without
|
|
331
|
-
pressing Buy, and you are not told which of the two they closed.
|
|
332
|
-
|
|
333
|
-
while. The press shows nothing during a back-off, so give the player something else to do on that
|
|
334
|
-
screen.
|
|
338
|
+
pressing Buy, and you are not told which of the two they closed. Nothing backs off: the player's
|
|
339
|
+
next press on either button opens it again.
|
|
335
340
|
|
|
336
341
|
## 11. What the player holds, and a VIP door
|
|
337
342
|
|
|
@@ -445,14 +450,14 @@ player, and the route refuses it with `403`. The body is `{ pass, held, ends_at
|
|
|
445
450
|
While something listens to `on('changed')`, the SDK reads again at each of these, and calls you only
|
|
446
451
|
if the answer differs:
|
|
447
452
|
|
|
448
|
-
| Moment
|
|
449
|
-
|
|
|
450
|
-
| `store.purchase` or `store.showOffer` answers `ok`
|
|
451
|
-
| Either answers `unavailable` / `error`
|
|
452
|
-
| `gem.store.onSettled` fires
|
|
453
|
-
| Your game resumes
|
|
454
|
-
| The player signs in
|
|
455
|
-
| A held pass reaches `endsAt`
|
|
453
|
+
| Moment | Why you cannot rely on something else |
|
|
454
|
+
| -------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
455
|
+
| `store.purchase` or `store.showOffer` answers `ok` | `onSettled` does not fire after a plain `ok` |
|
|
456
|
+
| Either answers `unavailable` / `error` | The outcome is unknown, and the player may have paid |
|
|
457
|
+
| `gem.store.onSettled` fires | `ok` / `pending` still reads as not held |
|
|
458
|
+
| Your game resumes | A purchase in the Arcade's own store, from the Gem menu, sends no event at all |
|
|
459
|
+
| The player signs in | A guest who signs in may hold VIP |
|
|
460
|
+
| A held pass reaches `endsAt` | VIP does not renew, so the door has to lock by itself |
|
|
456
461
|
|
|
457
462
|
A read that fails changes nothing, and is tried again later. A read that was already on its way when
|
|
458
463
|
a purchase ended is never reused, so a stale answer cannot shut the door on the buyer. If you read
|
|
@@ -473,15 +478,15 @@ Until then, decide in a function, or leave a VIP door out of a dedicated-server
|
|
|
473
478
|
|
|
474
479
|
## 12. Outcomes and reasons
|
|
475
480
|
|
|
476
|
-
| Call
|
|
477
|
-
|
|
|
478
|
-
| `appLinks`
|
|
479
|
-
| `openApp`
|
|
480
|
-
| `openGame`
|
|
481
|
-
| `similarGames`
|
|
482
|
-
| `store.offers`
|
|
483
|
-
| `store.purchase`
|
|
484
|
-
| `store.showOffer` | paid, or `pending`
|
|
481
|
+
| Call | `ok` | `declined` | `unavailable` |
|
|
482
|
+
| ----------------- | ------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
483
|
+
| `appLinks` | listings to draw | | `unsupported`, `error` |
|
|
484
|
+
| `openApp` | opened, or `handed_off` | `busy` | `already_in_app`, `unsupported`, `error` |
|
|
485
|
+
| `openGame` | the next game is starting | `busy`, `same_game` | `not_found`, `unsupported`, `error` |
|
|
486
|
+
| `similarGames` | cards, possibly none | | `unsupported`, `error` |
|
|
487
|
+
| `store.offers` | offers, possibly none | | `sign_in_required`, `store_off`, `unsupported`, `error` |
|
|
488
|
+
| `store.purchase` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
|
|
489
|
+
| `store.showOffer` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
|
|
485
490
|
|
|
486
491
|
Every reason is a short code of lower-case letters and underscores. The Arcade turns a payment
|
|
487
492
|
provider's own code into that shape before it reaches you.
|
package/docs/changelog.md
CHANGED
|
@@ -16,6 +16,18 @@ 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
|
+
## 20.0.0
|
|
20
|
+
|
|
21
|
+
Upgrading from 19.0.2. Impact: required.
|
|
22
|
+
|
|
23
|
+
### Must change
|
|
24
|
+
|
|
25
|
+
- The parent conformance version moved from 3 to 4. If your game, test setup or tooling checks against the conformance version or runs the parent conformance checks, update it to expect version 4. Checks written for version 3 will now fail where they expect a purchase to be refused without a player press, or expect purchases to be refused after a player dismissed a checkout or sheet.
|
|
26
|
+
|
|
27
|
+
### May stop
|
|
28
|
+
|
|
29
|
+
- Purchases no longer need a player press. A purchase asked from a timer or after an await opens its checkout, and the checkout is the confirmation. Games can drop code that holds a purchase request back until a tap or click. Games can also drop code that waits out a pause after a player dismisses a checkout or sheet, because the next request opens it again. Two things are unchanged: leaving the game still needs a press, and a purchase asked while another surface is open is still declined as busy.
|
|
30
|
+
|
|
19
31
|
## 19.0.2
|
|
20
32
|
|
|
21
33
|
Upgrading from 19.0.1. Impact: none.
|
package/docs/getting-started.md
CHANGED
|
@@ -591,7 +591,6 @@ everything under `platform_prices` may carry a price, and a currency under neith
|
|
|
591
591
|
all. The grant rows also carry the platform's ceilings, and a game does not declare its own; a
|
|
592
592
|
`max_per_window` on a mapping is ignored, with a warning.
|
|
593
593
|
|
|
594
|
-
|
|
595
594
|
A browser-only game cannot award a platform currency without a server **or a serverless function**.
|
|
596
595
|
That is the price of the platform never trusting a client. A game with no dedicated server writes
|
|
597
596
|
the source progression from a function the player calls, on a progression that opts in with
|
|
@@ -657,10 +656,10 @@ subject. [brand-art.md](brand-art.md) has both, how to draw them, and the rest o
|
|
|
657
656
|
|
|
658
657
|
Every game can choose between two purchase owners for the same published platform price:
|
|
659
658
|
|
|
660
|
-
| Call
|
|
661
|
-
|
|
|
662
|
-
| `gem.purchases.request(...)`
|
|
663
|
-
| `gem.me.mutations.purchase(...)` | your game
|
|
659
|
+
| Call | Who owns confirmation | Transport | Result |
|
|
660
|
+
| -------------------------------- | --------------------- | ------------------------------------------------------------- | ---------------------------------------- |
|
|
661
|
+
| `gem.purchases.request(...)` | the platform | postMessage bridge; platform sheet always opens | `ok`, `declined`, or `unavailable` |
|
|
662
|
+
| `gem.me.mutations.purchase(...)` | your game | direct authenticated SDK request; no bridge or platform sheet | an order-style result or a `GemApiError` |
|
|
664
663
|
|
|
665
664
|
There is no approval step. Both paths are available to every game.
|
|
666
665
|
|
|
@@ -818,21 +817,21 @@ local lookup. The purchase catalog is keyed by ids. Recipes also require `catalo
|
|
|
818
817
|
One vocabulary covers both paths: the sheet's `res.reason` and the direct path's `err.code` are
|
|
819
818
|
the same strings.
|
|
820
819
|
|
|
821
|
-
| Code
|
|
822
|
-
|
|
|
823
|
-
| `unknown_currency`
|
|
824
|
-
| `currency_not_in_game_payable`
|
|
825
|
-
| `not_purchasable`
|
|
826
|
-
| `no_price_in_currency`
|
|
827
|
-
| `busy`
|
|
828
|
-
| `cancelled`
|
|
829
|
-
| `insufficient_funds`
|
|
830
|
-
| `price_mismatch`, `config_version_stale`
|
|
831
|
-
| `platform_currency_unavailable`
|
|
832
|
-
| `not_available`
|
|
833
|
-
| `recipe_locked`
|
|
834
|
-
| `recipe_on_cooldown`, `recipe_max_uses_exceeded`, `item_max_stack_exceeded`, `recipe_costs_unmet`, `quantity_invalid` | the recipe or item refused this purchase before any charge
|
|
835
|
-
| `error`
|
|
820
|
+
| Code | What happened | What to do |
|
|
821
|
+
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
822
|
+
| `unknown_currency` | the slug is not an active platform currency | write `ARCADE_TICKETS` or `GEMS`; the direct path's message lists what is active |
|
|
823
|
+
| `currency_not_in_game_payable` | the currency exists but cannot pay for game items | price in a currency `gemctl platform config` lists under `platform_prices` |
|
|
824
|
+
| `not_purchasable` | the target is not in the purchase catalog on this channel, or cannot be bought | send the id (not the slug); promote the config version that prices it |
|
|
825
|
+
| `no_price_in_currency` | the target has no price in that currency | ask in the currency it is priced in, or add the price |
|
|
826
|
+
| `busy` | a purchase is already in flight, or a sheet is open | ask again only from a fresh player action |
|
|
827
|
+
| `cancelled` | the player dismissed the sheet | nothing — return to the shop |
|
|
828
|
+
| `insufficient_funds` | the player cannot afford it | say so in your shop's words; never draw a top-up screen |
|
|
829
|
+
| `price_mismatch`, `config_version_stale` | the price or config changed since your catalog read; nothing was charged | re-read the catalog and re-render |
|
|
830
|
+
| `platform_currency_unavailable` | the currency is not available to this purchase right now | show the store as unavailable |
|
|
831
|
+
| `not_available` | the recipe is outside its availability window, or inactive; nothing was charged | hide or disable the row until its window opens |
|
|
832
|
+
| `recipe_locked` | the recipe's prerequisites are not met; nothing was charged | show the lock from `recipeAvailability().lockReasons` |
|
|
833
|
+
| `recipe_on_cooldown`, `recipe_max_uses_exceeded`, `item_max_stack_exceeded`, `recipe_costs_unmet`, `quantity_invalid` | the recipe or item refused this purchase before any charge | reflect it on the row; `recipeAvailability` has the detail |
|
|
834
|
+
| `error` | anything else, or no reliable answer | one neutral "store unavailable"; after `unavailable`, re-read inventory |
|
|
836
835
|
|
|
837
836
|
Reasons are for your shop's state and your logs. Branch money on `outcome` and on a re-read of
|
|
838
837
|
inventory, never on a reason, and do not retry because of one.
|
|
@@ -964,7 +963,9 @@ const gems = allowances.find((a) => a.currencySlug === 'GEMS');
|
|
|
964
963
|
// 0 or less once `resetsAt` has passed: the read is stale, so say nothing about when.
|
|
965
964
|
const hours = gems?.resetsAt ? Math.ceil((gems.resetsAt - Date.now()) / 3_600_000) : 0;
|
|
966
965
|
if (gems && gems.remaining > 0) {
|
|
967
|
-
hud.setText(
|
|
966
|
+
hud.setText(
|
|
967
|
+
`You can still earn ${gems.remaining} Gems` + (hours > 0 ? `, more in ${hours}h` : ''),
|
|
968
|
+
);
|
|
968
969
|
} else if (gems?.resetsAt) {
|
|
969
970
|
// Spent for now: say when more arrives, never "0 left".
|
|
970
971
|
hud.setText(hours > 0 ? `More Gems in ${hours}h` : 'More Gems soon');
|
|
@@ -1001,7 +1002,7 @@ end of the reported period when both of the platform's ceilings are spent.
|
|
|
1001
1002
|
|
|
1002
1003
|
**Display only. Never gate gameplay or a grant on it.**
|
|
1003
1004
|
|
|
1004
|
-
- ✅ ``
|
|
1005
|
+
- ✅ ``hud.setText(`${gems.remaining} Gems left to earn today`)``
|
|
1005
1006
|
- ❌ `if (gems.remaining > 0) awardDailyBonus()`
|
|
1006
1007
|
|
|
1007
1008
|
Grant anyway: the platform trims every grant to what is left, and the receipt is the authority on
|
package/docs/manifest.json
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
"file": "arcade-and-store.md",
|
|
21
|
-
"sha256": "
|
|
22
|
-
"updated": "2026-
|
|
21
|
+
"sha256": "79bd7829d5a6577d3619d7fd831b74065df883e11eadd1b8153754d97a88474a",
|
|
22
|
+
"updated": "2026-11-02T18:42:37Z"
|
|
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": "37f3bde69baa7c0280e7d8a58800c1509b3b61c76a684428832bf094f298478b",
|
|
52
|
+
"updated": "2026-11-02T19:11:02Z"
|
|
53
53
|
},
|
|
54
54
|
{
|
|
55
55
|
"file": "daily-challenges.md",
|
|
@@ -78,13 +78,13 @@
|
|
|
78
78
|
},
|
|
79
79
|
{
|
|
80
80
|
"file": "getting-started.md",
|
|
81
|
-
"sha256": "
|
|
82
|
-
"updated": "2026-11-
|
|
81
|
+
"sha256": "896f7d29d2017e5c42fec20386bce31eacf3f5143b7134c7607574b452c039bf",
|
|
82
|
+
"updated": "2026-11-02T19:11:02Z"
|
|
83
83
|
},
|
|
84
84
|
{
|
|
85
85
|
"file": "inventory-and-catalogs.md",
|
|
86
|
-
"sha256": "
|
|
87
|
-
"updated": "2026-11-
|
|
86
|
+
"sha256": "dab6e42f4ad5cc2a63c2adb11e85053f963cd3166d1c447394cd6f532bb6c006",
|
|
87
|
+
"updated": "2026-11-02T18:42:37Z"
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
"file": "leaderboards.md",
|
|
@@ -138,8 +138,8 @@
|
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"file": "server-bundles.md",
|
|
141
|
-
"sha256": "
|
|
142
|
-
"updated": "2026-11-
|
|
141
|
+
"sha256": "655a5835bf9d1c13997784de57aaf54566d72641bfe220da8a37903d5fad044c",
|
|
142
|
+
"updated": "2026-11-02T19:11:02Z"
|
|
143
143
|
},
|
|
144
144
|
{
|
|
145
145
|
"file": "serverless-functions-js.md",
|
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/
|
|
84
|
-
curl -fsSL https://downloads.gemarcade.com/sdk/
|
|
83
|
+
curl -fsSL -O https://downloads.gemarcade.com/sdk/v20.0.0/gem-web-sdk.tgz
|
|
84
|
+
curl -fsSL https://downloads.gemarcade.com/sdk/v20.0.0/gem-web-sdk.tgz.sri
|
|
85
85
|
openssl dgst -sha384 -binary gem-web-sdk.tgz | openssl base64 -A
|
|
86
86
|
```
|
|
87
87
|
|
package/package.json
CHANGED
package/schema/bridge.v1.json
CHANGED
|
@@ -3819,11 +3819,11 @@
|
|
|
3819
3819
|
" balance a player paid for is the rest of this list, and these four are",
|
|
3820
3820
|
" the ones the old argument stood in for:",
|
|
3821
3821
|
"",
|
|
3822
|
-
" - THE CONFIRMATION IS THE PARENT'S
|
|
3823
|
-
"
|
|
3824
|
-
"
|
|
3825
|
-
" -
|
|
3826
|
-
"
|
|
3822
|
+
" - THE CONFIRMATION IS THE PARENT'S. The sheet is drawn in the",
|
|
3823
|
+
" parent's own chrome from platform records, and only the player's own",
|
|
3824
|
+
" press on its confirm control spends.",
|
|
3825
|
+
" - THE CONFIRM CONTROL IS QUIET AFTER PAINT (below; the arcade holds",
|
|
3826
|
+
" the quiet period for 400 ms).",
|
|
3827
3827
|
" - A SHORTFALL IS NEVER A SALE. `insufficient_funds` gets no top-up, no",
|
|
3828
3828
|
" upsell and no way into a store from THIS sheet. A game MAY open the",
|
|
3829
3829
|
" arcade's store for one of the store's own SKUs -",
|
|
@@ -3842,18 +3842,14 @@
|
|
|
3842
3842
|
" price, quantity and total distinctly so a 10x multiplier is never hidden",
|
|
3843
3843
|
" behind one figure.",
|
|
3844
3844
|
"",
|
|
3845
|
-
"
|
|
3846
|
-
"
|
|
3847
|
-
"
|
|
3848
|
-
" parent
|
|
3849
|
-
"
|
|
3850
|
-
"
|
|
3851
|
-
"
|
|
3852
|
-
"",
|
|
3853
|
-
" BACK OFF AFTER A CANCEL. After a `cancelled`, answer further asks from",
|
|
3854
|
-
" that game `declined` / `busy` without rendering, for an interval that",
|
|
3855
|
-
" grows with each consecutive cancel. Without it, 'ask, learn cancelled,",
|
|
3856
|
-
" ask again' is a re-prompt loop a game can write in three lines.",
|
|
3845
|
+
" NO PRESS, AND NO BACK-OFF (owner decision).",
|
|
3846
|
+
" Arcade games are developed in-house and trusted, and a limit between",
|
|
3847
|
+
" a player and a purchase refused players who wanted to buy. So the",
|
|
3848
|
+
" parent renders the sheet for an ask from a timer or after an await as",
|
|
3849
|
+
" for one from a click - `navigator.userActivation` is not read - and a",
|
|
3850
|
+
" sheet the player closed never makes the next ask wait: it is answered",
|
|
3851
|
+
" `cancelled`, and the game's next ask opens a sheet. The sheet itself",
|
|
3852
|
+
" is the confirmation; a game cannot press it.",
|
|
3857
3853
|
"",
|
|
3858
3854
|
" ONE PROMPT AT A TIME, and not instantly actionable. A gem.requestPurchase",
|
|
3859
3855
|
" arriving while a prompt is open is answered `declined` / `busy` without",
|
|
@@ -3960,9 +3956,9 @@
|
|
|
3960
3956
|
"EVERY OBLIGATION ON `gem.requestPurchase` APPLIES HERE, UNCHANGED:",
|
|
3961
3957
|
"payable currencies only, resolved exactly; price it yourself from",
|
|
3962
3958
|
"platform records, under the player's session (the offer's price in",
|
|
3963
|
-
"`currencySlug`); the confirmation is the parent's own sheet,
|
|
3964
|
-
"
|
|
3965
|
-
"
|
|
3959
|
+
"`currencySlug`); the confirmation is the parent's own sheet, with no",
|
|
3960
|
+
"press required and no back-off; one prompt at a time, quiet after",
|
|
3961
|
+
"paint; one purchase per ask; meter it in money, server-side;",
|
|
3966
3962
|
"treat every field as untrusted text. A shortfall is never a sale.",
|
|
3967
3963
|
"",
|
|
3968
3964
|
"AND THREE OF ITS OWN:",
|
|
@@ -4056,13 +4052,14 @@
|
|
|
4056
4052
|
"a game nothing it cannot already read.",
|
|
4057
4053
|
"",
|
|
4058
4054
|
"THE ONE ADDED DISCLOSURE IS `cancelled`: what the player chose in",
|
|
4059
|
-
"platform UI. It
|
|
4060
|
-
"
|
|
4061
|
-
"
|
|
4062
|
-
"
|
|
4063
|
-
"
|
|
4064
|
-
"
|
|
4065
|
-
"
|
|
4055
|
+
"platform UI. It first shipped behind two parent-side controls - user",
|
|
4056
|
+
"activation, and a back-off after a cancel - against a game that asks,",
|
|
4057
|
+
"learns 'cancelled' and asks again. Both were removed by owner decision:",
|
|
4058
|
+
"Arcade games are developed in-house and trusted,",
|
|
4059
|
+
"and the back-off refused players who wanted to buy. A parent sends",
|
|
4060
|
+
"`cancelled` when the player dismissed a sheet they could afford, and",
|
|
4061
|
+
"answers the game's next ask on its merits. A parent older than that",
|
|
4062
|
+
"decision may send `error` in its place; read both as 'not bought'.",
|
|
4066
4063
|
"",
|
|
4067
4064
|
"EVERY OUTCOME RESOLVES. Only a protocol failure rejects. A player tapping",
|
|
4068
4065
|
"Cancel is an ordinary thing that happens in every shop, and rejecting there",
|
|
@@ -4098,7 +4095,7 @@
|
|
|
4098
4095
|
"outcome": {
|
|
4099
4096
|
"kind": "enum",
|
|
4100
4097
|
"values": ["ok", "declined", "unavailable"],
|
|
4101
|
-
"$comment": "THREE VALUES, AND THAT IS THE COMPLETE VOCABULARY OF WHAT TO DO. The WHY is `reason`, a bounded string, so the explanations can grow without this enum growing - which matters because `check-schema-freeze` treats a new enum value as breaking (an N-1 receiver refuses it, and a refused `declined` becomes a two-minute timeout and an `unavailable`). A game branches its money logic on this field and on a re-read of inventory, never on `reason`. THE TIMING CHANNEL: a refusal the parent reaches without rendering anything is separable from a human Cancel by latency. That used to be the gap the no-reason rule left open; with `reason` sent, it tells a game nothing `reason` and `gem.platform.currencies.get()` do not already say. What still matters is how fast a game can ask,
|
|
4098
|
+
"$comment": "THREE VALUES, AND THAT IS THE COMPLETE VOCABULARY OF WHAT TO DO. The WHY is `reason`, a bounded string, so the explanations can grow without this enum growing - which matters because `check-schema-freeze` treats a new enum value as breaking (an N-1 receiver refuses it, and a refused `declined` becomes a two-minute timeout and an `unavailable`). A game branches its money logic on this field and on a re-read of inventory, never on `reason`. THE TIMING CHANNEL: a refusal the parent reaches without rendering anything is separable from a human Cancel by latency. That used to be the gap the no-reason rule left open; with `reason` sent, it tells a game nothing `reason` and `gem.platform.currencies.get()` do not already say. What still matters is how fast a game can ask: games are trusted, so nothing paces a game's asks beyond one prompt at a time and the rate limit."
|
|
4102
4099
|
},
|
|
4103
4100
|
"reason": {
|
|
4104
4101
|
"kind": "string",
|
|
@@ -4138,9 +4135,8 @@
|
|
|
4138
4135
|
" the target is not in the purchase catalog, declined not_purchasable",
|
|
4139
4136
|
" or is not purchasable",
|
|
4140
4137
|
" the target has no price in that currency declined no_price_in_currency",
|
|
4141
|
-
" a sheet is already open, a purchase is
|
|
4142
|
-
" flight
|
|
4143
|
-
" the ask arrived without user activation",
|
|
4138
|
+
" a sheet is already open, or a purchase is declined busy",
|
|
4139
|
+
" in flight",
|
|
4144
4140
|
" the player dismissed a sheet they could declined cancelled",
|
|
4145
4141
|
" afford",
|
|
4146
4142
|
" a shortfall shown on the sheet, or gem-api declined insufficient_funds",
|
|
@@ -5013,8 +5009,9 @@
|
|
|
5013
5009
|
"`appLinks` capability. Answered by `gem.openAppResult`.",
|
|
5014
5010
|
"",
|
|
5015
5011
|
"AN ACTING MESSAGE. It leaves the game, so the parent holds it to the",
|
|
5016
|
-
"rules every acting message shares (gem.requestOpenGame
|
|
5017
|
-
"gem.requestStorePurchase
|
|
5012
|
+
"rules every acting message shares (gem.requestOpenGame states them",
|
|
5013
|
+
"too; a purchase - gem.requestStorePurchase, gem.requestPurchase -",
|
|
5014
|
+
"shares all of them but the press):",
|
|
5018
5015
|
"",
|
|
5019
5016
|
" A PERSON MUST HAVE PRESSED. Read `navigator.userActivation.isActive`",
|
|
5020
5017
|
" synchronously on receipt, before any `await`, or accept a controller",
|
|
@@ -5507,10 +5504,12 @@
|
|
|
5507
5504
|
"listing, takes money only on the payment provider's page or the",
|
|
5508
5505
|
"operating system's purchase sheet, and tells the game the outcome.",
|
|
5509
5506
|
"",
|
|
5510
|
-
"AN ACTING MESSAGE
|
|
5511
|
-
"
|
|
5512
|
-
"
|
|
5513
|
-
"
|
|
5507
|
+
"AN ACTING MESSAGE: one acting message at a time across all of them,",
|
|
5508
|
+
"checked before `skuId` is resolved, and the answer bound to this frame",
|
|
5509
|
+
"session. UNLIKE gem.requestOpenApp AND gem.requestOpenGame IT NEEDS NO",
|
|
5510
|
+
"PRESS (owner decision): a purchase asked from a",
|
|
5511
|
+
"timer or after an await opens its checkout as one asked from a click",
|
|
5512
|
+
"does, because the checkout is the confirmation. And:",
|
|
5514
5513
|
"",
|
|
5515
5514
|
" ONLY A SKU IT WOULD LIST. `skuId` must be one gem.storeOffersResult",
|
|
5516
5515
|
" would list to this player right now; anything else is `unavailable`",
|
|
@@ -5528,14 +5527,13 @@
|
|
|
5528
5527
|
" bought again yet, the parent says when, in its own chrome, and",
|
|
5529
5528
|
" answers `declined` / `cancelled` - the answer a closed checkout gets,",
|
|
5530
5529
|
" so the cooldown, which would reveal a recent purchase, never crosses",
|
|
5531
|
-
" the bridge.
|
|
5530
|
+
" the bridge.",
|
|
5532
5531
|
"",
|
|
5533
|
-
" BACK
|
|
5534
|
-
"
|
|
5535
|
-
"
|
|
5536
|
-
"
|
|
5537
|
-
"
|
|
5538
|
-
" payment, `error` and `ok` / `pending` do not.",
|
|
5532
|
+
" NO BACK-OFF (owner decision). A player who",
|
|
5533
|
+
" closed a checkout and asks again gets the checkout again, however many",
|
|
5534
|
+
" times; a player who paid and buys the same SKU again gets a new",
|
|
5535
|
+
" checkout. Only one checkout is on screen at a time, so an ask while",
|
|
5536
|
+
" one is open is `busy` - the moment it ends, the next ask opens.",
|
|
5539
5537
|
"",
|
|
5540
5538
|
" RESUME, THEN ANSWER. The result is sent after the game has been told",
|
|
5541
5539
|
" it is resumed, so the game's own celebration is visible.",
|
|
@@ -5560,7 +5558,7 @@
|
|
|
5560
5558
|
},
|
|
5561
5559
|
"rateLimit": {
|
|
5562
5560
|
"perMinute": 12,
|
|
5563
|
-
"$comment": "gem.requestPurchase's limit and its argument: it bounds a BUGGY game, not a hostile one. What bounds the player's experience is the
|
|
5561
|
+
"$comment": "gem.requestPurchase's limit and its argument: it bounds a BUGGY game, not a hostile one, at a rate no player buying by hand reaches. What bounds the player's experience is the one-at-a-time rule."
|
|
5564
5562
|
}
|
|
5565
5563
|
},
|
|
5566
5564
|
{
|
|
@@ -5578,9 +5576,9 @@
|
|
|
5578
5576
|
" the player left before it did): `gem.storePurchaseSettled`",
|
|
5579
5577
|
" follows when it does, and a game re-reads on resume too.",
|
|
5580
5578
|
" declined - nothing was bought: the player closed the checkout,",
|
|
5581
|
-
" or the offer is cooling down (`cancelled`); or
|
|
5582
|
-
"
|
|
5583
|
-
"
|
|
5579
|
+
" or the offer is cooling down (`cancelled`); or another",
|
|
5580
|
+
" acting message was open (`busy`). Either way the",
|
|
5581
|
+
" game's next ask is answered on its merits.",
|
|
5584
5582
|
" unavailable - no sale the parent can vouch for. With `store_off`",
|
|
5585
5583
|
" (the store withheld), `sign_in_required` (a guest),",
|
|
5586
5584
|
" `not_found` (an unknown SKU) or the provider's own",
|
|
@@ -5659,18 +5657,17 @@
|
|
|
5659
5657
|
"gem.requestStorePurchase opens.",
|
|
5660
5658
|
"",
|
|
5661
5659
|
"EVERY RULE OF gem.requestStorePurchase HOLDS, and it shares them rather",
|
|
5662
|
-
"than having its own: an acting message (
|
|
5663
|
-
"
|
|
5664
|
-
"
|
|
5665
|
-
"
|
|
5666
|
-
"
|
|
5667
|
-
"message; resume, then answer; evidence, not accounting.",
|
|
5660
|
+
"than having its own: an acting message (one at a time across all of",
|
|
5661
|
+
"them, checked before `skuId` is resolved, no press needed, the answer",
|
|
5662
|
+
"bound to this frame session); only a SKU the parent would list now; an",
|
|
5663
|
+
"offer cooling down opens nothing and answers `declined` / `cancelled`;",
|
|
5664
|
+
"no back-off; resume, then answer; evidence, not accounting.",
|
|
5668
5665
|
"",
|
|
5669
5666
|
"THE SHEET IS NOT A SECOND CONFIRMATION, and it does not change",
|
|
5670
5667
|
"gem.requestStorePurchase's straight-to-checkout rule. It is the pitch;",
|
|
5671
5668
|
"the checkout is still the confirmation, with its own pay control. The",
|
|
5672
5669
|
"player leaving the sheet without pressing Buy is a dismissal, exactly",
|
|
5673
|
-
"as closing a checkout is
|
|
5670
|
+
"as closing a checkout is.",
|
|
5674
5671
|
"",
|
|
5675
5672
|
"A PARENT THAT ADVERTISES `storeOfferSheet` ADVERTISES `store`: the",
|
|
5676
5673
|
"game's SKU comes from gem.storeOffersResult, and a purchase that answers",
|
|
@@ -5693,7 +5690,7 @@
|
|
|
5693
5690
|
},
|
|
5694
5691
|
"rateLimit": {
|
|
5695
5692
|
"perMinute": 12,
|
|
5696
|
-
"$comment": "gem.requestStorePurchase's limit and its argument: it bounds a BUGGY game, not a hostile one. What bounds the player's experience is the
|
|
5693
|
+
"$comment": "gem.requestStorePurchase's limit and its argument: it bounds a BUGGY game, not a hostile one, at a rate no player buying by hand reaches. What bounds the player's experience is the one-at-a-time rule."
|
|
5697
5694
|
}
|
|
5698
5695
|
},
|
|
5699
5696
|
{
|
|
@@ -5710,8 +5707,8 @@
|
|
|
5710
5707
|
" gem.storePurchaseSettled follows when it does.",
|
|
5711
5708
|
" declined - nothing was bought: the player left the sheet or closed",
|
|
5712
5709
|
" the checkout, or the offer is cooling down (`cancelled`);",
|
|
5713
|
-
" or
|
|
5714
|
-
"
|
|
5710
|
+
" or another acting message was open (`busy`). Either way",
|
|
5711
|
+
" the game's next ask is answered on its merits.",
|
|
5715
5712
|
" unavailable - no sale the parent can vouch for. With `store_off`,",
|
|
5716
5713
|
" `sign_in_required`, `not_found` or the provider's own",
|
|
5717
5714
|
" failure code, nothing was bought. With `error`, the",
|