@indiegems/gem-web-sdk 7.0.0 → 7.1.1
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/brand/ticket-hero-128.png +0 -0
- package/brand/ticket-hero-256.png +0 -0
- package/brand/ticket-hero-384.png +0 -0
- package/brand/ticket-hero-512.png +0 -0
- package/brand/ticket-hero-768.png +0 -0
- package/brand/ticket-icon-20-1.5x.png +0 -0
- package/brand/ticket-icon-20-1x.png +0 -0
- package/brand/ticket-icon-20-2x.png +0 -0
- package/brand/ticket-icon-20-3x.png +0 -0
- package/brand/ticket-icon-32-1.5x.png +0 -0
- package/brand/ticket-icon-32-1x.png +0 -0
- package/brand/ticket-icon-32-2x.png +0 -0
- package/brand/ticket-icon-32-3x.png +0 -0
- package/dist/game.d.ts +34 -1
- package/dist/gem-web-sdk.bridge.js +1 -1
- package/dist/gem-web-sdk.esm.js +2 -2
- 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-api.js +1 -1
- package/dist/gem-web-sdk.server.js +2 -2
- package/dist/index.d.ts +31 -2
- package/dist/sdk.d.ts +149 -30
- package/dist/server-api.d.ts +93 -23
- package/dist/server.d.ts +109 -23
- package/package.json +1 -1
- package/schema/bridge.v1.json +245 -30
package/schema/bridge.v1.json
CHANGED
|
@@ -583,6 +583,80 @@
|
|
|
583
583
|
"format": "url",
|
|
584
584
|
"optional": true,
|
|
585
585
|
"$comment": "LEGACY — the earlier spelling of `avatarIconUrl`, and the same value. Same pin caveat. A current frame reads it only when `avatarIconUrl` is absent."
|
|
586
|
+
},
|
|
587
|
+
"boost": {
|
|
588
|
+
"kind": "object",
|
|
589
|
+
"optional": true,
|
|
590
|
+
"$comment": [
|
|
591
|
+
"The player's Boost, FOR DISPLAY ONLY — a platform rule, not advice. A",
|
|
592
|
+
"Boost is the Arcade's one real-money product: it",
|
|
593
|
+
"multiplies what the player EARNS in platform currencies (Gems and",
|
|
594
|
+
"Tickets) and raises their earning limits. It never touches achievement",
|
|
595
|
+
"points.",
|
|
596
|
+
"",
|
|
597
|
+
"ADVISORY. It may lag: it is pushed on connect and again when the",
|
|
598
|
+
"parent's copy changes, never on the instant a boost starts or ends. The",
|
|
599
|
+
"platform applies the real multiplier server-side when it pays, whatever",
|
|
600
|
+
"a game renders. A game may SHOW it — a \"2x\" badge on a reward screen —",
|
|
601
|
+
"and must never gate content, difficulty, matchmaking, prices or power on",
|
|
602
|
+
"it. It is also trivially forgeable inside the game's own realm, which is",
|
|
603
|
+
"the second reason nothing may key on it.",
|
|
604
|
+
"",
|
|
605
|
+
"Platform-assigned numbers only, so it sits on the trusted side of the",
|
|
606
|
+
"`untrusted` split: currency ids and slugs, multipliers, a day count. NO",
|
|
607
|
+
"tier, SKU, price, source (paid, comp, trial) or instant: free and paid",
|
|
608
|
+
"boosts look identical to a game, and `expiresInDays` is whole days",
|
|
609
|
+
"rounded up because a second-precision expiry is a near-unique",
|
|
610
|
+
"per-player value two games could join on to link a player's two",
|
|
611
|
+
"game-scoped identities.",
|
|
612
|
+
"",
|
|
613
|
+
"ABSENT MEANS THE PARENT DID NOT SAY — an older parent, a backend without",
|
|
614
|
+
"boosts, or a read that has not landed. A frame reads absence as 1x",
|
|
615
|
+
"everywhere, which is also what an unboosted player's message says.",
|
|
616
|
+
"",
|
|
617
|
+
"Additive and optional, so no `v` bump, and ungated like the rest of",
|
|
618
|
+
"`gem.player`: no capability, because a frame that ignores the field",
|
|
619
|
+
"loses nothing but a badge."
|
|
620
|
+
],
|
|
621
|
+
"fields": {
|
|
622
|
+
"multipliers": {
|
|
623
|
+
"kind": "array",
|
|
624
|
+
"maxItems": 8,
|
|
625
|
+
"$comment": "One entry per boostable platform currency the parent knows of, in the parent's display order. `multiplierBp` is basis points: 10000 is 1x (not boosted), 20000 is 2x. A currency missing from the list is 1x. A parent drops an entry it cannot fit in these bounds rather than losing the whole message to it.",
|
|
626
|
+
"items": {
|
|
627
|
+
"kind": "object",
|
|
628
|
+
"fields": {
|
|
629
|
+
"currencyId": { "kind": "string", "maxLength": 64 },
|
|
630
|
+
"currencySlug": {
|
|
631
|
+
"kind": "string",
|
|
632
|
+
"maxLength": 32,
|
|
633
|
+
"$comment": "The currency's slug, e.g. GEMS or ARCADE_TICKETS — the same slug `gem.rewardCue` and `gem.requestPurchase` take."
|
|
634
|
+
},
|
|
635
|
+
"multiplierBp": { "kind": "int", "min": 10000, "max": 1000000 }
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
},
|
|
639
|
+
"expiresInDays": {
|
|
640
|
+
"kind": "int",
|
|
641
|
+
"min": 1,
|
|
642
|
+
"max": 7,
|
|
643
|
+
"optional": true,
|
|
644
|
+
"$comment": [
|
|
645
|
+
"Whole days until the SOONEST multiplied currency drops back, rounded up",
|
|
646
|
+
"so a boost with any time left reads at least 1, and CAPPED AT 7: 7 means",
|
|
647
|
+
"\"7 or more\". Absent when no currency is multiplied. A count of days,",
|
|
648
|
+
"never an instant.",
|
|
649
|
+
"",
|
|
650
|
+
"THE CAP IS A PRIVACY BOUND, not a display choice. An exact count lets a",
|
|
651
|
+
"game watch it jump by a purchase's term (30 days) and infer that the",
|
|
652
|
+
"player just paid, and when. Capped, a boost with more than a week left",
|
|
653
|
+
"looks the same whether it was bought yesterday or a month ago. The",
|
|
654
|
+
"player's own account surfaces keep the exact figure; only what crosses",
|
|
655
|
+
"here is capped, and a parent clamps rather than letting a larger value",
|
|
656
|
+
"cost the frame the whole message."
|
|
657
|
+
]
|
|
658
|
+
}
|
|
659
|
+
}
|
|
586
660
|
}
|
|
587
661
|
},
|
|
588
662
|
"rateLimit": { "perMinute": 12 }
|
|
@@ -2104,11 +2178,22 @@
|
|
|
2104
2178
|
"player's one real wallet (gem-api's rule), so a cue confirms the same",
|
|
2105
2179
|
"way everywhere; a grant the receipt reports suppressed confirms nothing.",
|
|
2106
2180
|
"",
|
|
2181
|
+
"AT THE DAILY MAXIMUM the platform trims a grant to what is left of the",
|
|
2182
|
+
"player's day across all games, down to 0 once it is spent, and sends the",
|
|
2183
|
+
"parent `platform_daily_maximum_reached`. A parent resolves a cue it holds",
|
|
2184
|
+
"then into its own \"daily max\" state rather than letting it fade, and",
|
|
2185
|
+
"tells the player itself. That event is the player's, never relayed into",
|
|
2186
|
+
"the frame: the game learns nothing from it, and needs to do nothing",
|
|
2187
|
+
"differently.",
|
|
2188
|
+
"",
|
|
2107
2189
|
"Gated by `rewardCue`: sent only to a parent that advertised it; to any",
|
|
2108
2190
|
"other, `gem.cueReward` returns false and the game does NOTHING. There is",
|
|
2109
2191
|
"no fallback celebration: Gems display belongs to the arcade (the",
|
|
2110
2192
|
"creator's ruling, 2026-09-24), so a game draws no Gems flight, counter or",
|
|
2111
|
-
"celebration of its own.
|
|
2193
|
+
"celebration of its own. The same holds for Tickets (`ARCADE_TICKETS`): the",
|
|
2194
|
+
"arcade's wallet shows Gems and Tickets side by side and flies a payout",
|
|
2195
|
+
"into the one `currencySlug` names. `origin` is the centre of the game's icon for",
|
|
2196
|
+
"that currency — its gem, or its ticket."
|
|
2112
2197
|
],
|
|
2113
2198
|
"fields": {
|
|
2114
2199
|
"currencySlug": { "kind": "string", "maxLength": 32 },
|
|
@@ -2233,14 +2318,20 @@
|
|
|
2233
2318
|
"the following is not offering a degraded store, it is offering a game a",
|
|
2234
2319
|
"button that charges a player:",
|
|
2235
2320
|
"",
|
|
2236
|
-
" EARNED CURRENCIES ONLY. `currencySlug` is frame-chosen and this
|
|
2237
|
-
" not constrain which currency it names. The argument that
|
|
2238
|
-
" this without a top-up flow binds a currency a player EARNS
|
|
2239
|
-
" Gems
|
|
2240
|
-
"
|
|
2241
|
-
"
|
|
2242
|
-
"
|
|
2243
|
-
"
|
|
2321
|
+
" PAYABLE, EARNED CURRENCIES ONLY. `currencySlug` is frame-chosen and this",
|
|
2322
|
+
" schema does not constrain which currency it names. The argument that let",
|
|
2323
|
+
" this ship without a top-up flow binds a currency a player EARNS: short of",
|
|
2324
|
+
" Gems or Tickets the answer is to go and play, and neither is sold for",
|
|
2325
|
+
" money. It does not bind a currency a player BUYS. So resolve the slug",
|
|
2326
|
+
" against the platform's own currency records - an active currency the",
|
|
2327
|
+
" platform marks payable in games - and refuse the rest, `declined` with",
|
|
2328
|
+
" `unknown_currency` or `currency_not_in_game_payable`. Resolve it EXACTLY,",
|
|
2329
|
+
" with no",
|
|
2330
|
+
" alias table of your own: the retired `TICKETS` meant GEMS, and resolving",
|
|
2331
|
+
" it charged Gems to games that meant Tickets. An item may carry prices in",
|
|
2332
|
+
" more than one payable currency, so",
|
|
2333
|
+
" the frame does choose which wallet pays; the prompt names that currency",
|
|
2334
|
+
" and its price from platform records, which keeps the choice the player's.",
|
|
2244
2335
|
"",
|
|
2245
2336
|
" THE FRAME CHOOSES THE AMOUNT. Not the unit price - but `targetId` x",
|
|
2246
2337
|
" `currencySlug` x `quantity` is nevertheless the total, up to ten times the",
|
|
@@ -2248,12 +2339,26 @@
|
|
|
2248
2339
|
" price, quantity and total distinctly so a 10x multiplier is never hidden",
|
|
2249
2340
|
" behind one figure.",
|
|
2250
2341
|
"",
|
|
2342
|
+
" ONLY ON USER ACTIVATION. Render nothing, and answer `declined` with",
|
|
2343
|
+
" reason `busy`, unless `navigator.userActivation.isActive` is true when",
|
|
2344
|
+
" the ask arrives. A click or key press in the game frame activates the",
|
|
2345
|
+
" parent page too, so a Buy button works and a timer does not. It is the",
|
|
2346
|
+
" browser's own signal, so keyboard and assistive-technology activation",
|
|
2347
|
+
" count. This is the control that makes `cancelled` (gem.purchaseResult)",
|
|
2348
|
+
" safe to disclose: a prompt costs a real player's gesture.",
|
|
2349
|
+
"",
|
|
2350
|
+
" BACK OFF AFTER A CANCEL. After a `cancelled`, answer further asks from",
|
|
2351
|
+
" that game `declined` / `busy` without rendering, for an interval that",
|
|
2352
|
+
" grows with each consecutive cancel. Without it, 'ask, learn cancelled,",
|
|
2353
|
+
" ask again' is a re-prompt loop a game can write in three lines.",
|
|
2354
|
+
"",
|
|
2251
2355
|
" ONE PROMPT AT A TIME, and not instantly actionable. A gem.requestPurchase",
|
|
2252
|
-
" arriving while a prompt is open is answered `declined` without
|
|
2253
|
-
" anything. Twelve modals a minute is inside the rate limit
|
|
2254
|
-
" successful consent-fatigue attack; and because the frame
|
|
2255
|
-
" it asks, a confirm control that is actionable the
|
|
2256
|
-
" tap-jacking target - quiet input on it briefly
|
|
2356
|
+
" arriving while a prompt is open is answered `declined` / `busy` without",
|
|
2357
|
+
" rendering anything. Twelve modals a minute is inside the rate limit",
|
|
2358
|
+
" below and is a successful consent-fatigue attack; and because the frame",
|
|
2359
|
+
" chooses the moment it asks, a confirm control that is actionable the",
|
|
2360
|
+
" instant it paints is a tap-jacking target - quiet input on it briefly",
|
|
2361
|
+
" after paint.",
|
|
2257
2362
|
"",
|
|
2258
2363
|
" ONE PURCHASE PER ASK. Each gem.requestPurchase is a distinct purchase and",
|
|
2259
2364
|
" carries its own idempotency key. Do NOT collapse repeated asks: a player",
|
|
@@ -2308,7 +2413,7 @@
|
|
|
2308
2413
|
"currencySlug": {
|
|
2309
2414
|
"kind": "string",
|
|
2310
2415
|
"maxLength": 32,
|
|
2311
|
-
"$comment": "Which platform currency to pay in,
|
|
2416
|
+
"$comment": "Which platform currency to pay in, by its exact slug: GEMS or ARCADE_TICKETS. The item must carry a price in it; the parent resolves that price itself and refuses when there is none (`no_price_in_currency`). There are no aliases: `TICKETS` is a retired name for GEMS and is answered `unknown_currency`, never resolved. FRAME-CHOSEN AND UNCONSTRAINED HERE: nothing in this schema limits it to a currency the player earned rather than bought, so the parent checks it against the platform's own currency records. See the PAYABLE, EARNED CURRENCIES ONLY obligation above - this field is where that whole argument lands."
|
|
2312
2417
|
},
|
|
2313
2418
|
"quantity": {
|
|
2314
2419
|
"kind": "int",
|
|
@@ -2336,11 +2441,32 @@
|
|
|
2336
2441
|
"$comment": [
|
|
2337
2442
|
"The answer to one gem.requestPurchase. Gated by the `purchases` capability.",
|
|
2338
2443
|
"",
|
|
2339
|
-
"
|
|
2340
|
-
"
|
|
2341
|
-
"
|
|
2342
|
-
"
|
|
2343
|
-
"
|
|
2444
|
+
"THREE OUTCOMES, AND A REASON ON THE TWO THAT ARE NOT `ok`. `outcome` says",
|
|
2445
|
+
"what the game should DO;",
|
|
2446
|
+
"`reason` says WHY, so a developer can fix a config mistake instead of",
|
|
2447
|
+
"reading 'failed'. The mapping from condition to reason is normative and is",
|
|
2448
|
+
"written on the `reason` field below.",
|
|
2449
|
+
"",
|
|
2450
|
+
"WHY THIS MESSAGE NOW CARRIES A REASON, WHEN IT USED TO FORBID ONE. The old",
|
|
2451
|
+
"rule - no reason, 'no insufficient funds and there will not be one' -",
|
|
2452
|
+
"protected the player's wallet from untrusted game code: a decline that",
|
|
2453
|
+
"said 'could not afford' let a game walk a balance down a 1/2/4/8 price",
|
|
2454
|
+
"ladder, one prompt at a time. Platform-currency access for games made that",
|
|
2455
|
+
"wallet readable directly - `gem.platform.currencies.get()` hands a game",
|
|
2456
|
+
"the exact balance of every active platform currency - and spendable",
|
|
2457
|
+
"without a sheet through `gem.me.mutations.purchase`, whose refusals",
|
|
2458
|
+
"already say `insufficient_funds`. The owner accepted that as risk M3",
|
|
2459
|
+
"(2026-09-21). So `insufficient_funds` and the config-mistake reasons tell",
|
|
2460
|
+
"a game nothing it cannot already read.",
|
|
2461
|
+
"",
|
|
2462
|
+
"THE ONE ADDED DISCLOSURE IS `cancelled`: what the player chose in",
|
|
2463
|
+
"platform UI. It is behavioural data, and it is what makes an instant",
|
|
2464
|
+
"re-prompt loop cheap - ask, learn 'cancelled', ask again. It ships with",
|
|
2465
|
+
"two parent-side controls, stated as obligations on gem.requestPurchase:",
|
|
2466
|
+
"ONLY ON USER ACTIVATION (no prompt without a real gesture) and BACK OFF",
|
|
2467
|
+
"AFTER A CANCEL (further asks answered `busy`, unrendered, for a growing",
|
|
2468
|
+
"interval). A parent that cannot enforce both must not send `cancelled`;",
|
|
2469
|
+
"it sends `error` in its place.",
|
|
2344
2470
|
"",
|
|
2345
2471
|
"EVERY OUTCOME RESOLVES. Only a protocol failure rejects. A player tapping",
|
|
2346
2472
|
"Cancel is an ordinary thing that happens in every shop, and rejecting there",
|
|
@@ -2351,16 +2477,21 @@
|
|
|
2351
2477
|
" ok - the purchase completed and the grant is on the player's",
|
|
2352
2478
|
" game-scope inventory. Re-read inventory rather than assuming",
|
|
2353
2479
|
" what landed; `purchaseId` names the server's record of it.",
|
|
2354
|
-
" declined -
|
|
2355
|
-
"
|
|
2480
|
+
" declined - a DEFINITE refusal before any debit. Return to the shop",
|
|
2481
|
+
" SILENTLY: no dialog, no 'purchase failed' copy. The player",
|
|
2482
|
+
" already saw the parent's answer where there was one, and an",
|
|
2356
2483
|
" error toast after a deliberate Cancel reads as a bug.",
|
|
2357
|
-
"
|
|
2358
|
-
"
|
|
2359
|
-
"
|
|
2360
|
-
"
|
|
2361
|
-
"
|
|
2362
|
-
" '
|
|
2363
|
-
"
|
|
2484
|
+
" `reason` is for the game's logs and shop state - grey out a",
|
|
2485
|
+
" `recipe_locked` row, fix an `unknown_currency` slug - never",
|
|
2486
|
+
" for a top-up or payment screen of the game's own.",
|
|
2487
|
+
" unavailable - no reliable answer. One neutral 'store unavailable'. Never",
|
|
2488
|
+
" auto-retried; only a fresh player action may ask again. AND",
|
|
2489
|
+
" RE-READ INVENTORY: 'unavailable' means no answer arrived,",
|
|
2490
|
+
" not that nothing happened - the parent may have prompted,",
|
|
2491
|
+
" charged and answered while the answer was lost at a gate or",
|
|
2492
|
+
" a timeout. A reason never makes it more certain. 'declined'",
|
|
2493
|
+
" is the only outcome that says a purchase definitely did not",
|
|
2494
|
+
" happen."
|
|
2364
2495
|
],
|
|
2365
2496
|
"fields": {
|
|
2366
2497
|
"requestId": {
|
|
@@ -2371,7 +2502,91 @@
|
|
|
2371
2502
|
"outcome": {
|
|
2372
2503
|
"kind": "enum",
|
|
2373
2504
|
"values": ["ok", "declined", "unavailable"],
|
|
2374
|
-
"$comment": "THREE VALUES, AND THAT IS THE COMPLETE VOCABULARY
|
|
2505
|
+
"$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, and user activation plus the cancel back-off bound that at human speed."
|
|
2506
|
+
},
|
|
2507
|
+
"reason": {
|
|
2508
|
+
"kind": "string",
|
|
2509
|
+
"maxLength": 64,
|
|
2510
|
+
"format": "code",
|
|
2511
|
+
"optional": true,
|
|
2512
|
+
"$comment": [
|
|
2513
|
+
"WHY a purchase did not complete. Sent ONLY with `declined` and",
|
|
2514
|
+
"`unavailable`, never with `ok`; a receiver ignores one on `ok`. OPTIONAL:",
|
|
2515
|
+
"a parent that predates it sends none, and a receiver reads an absent",
|
|
2516
|
+
"reason as `error`.",
|
|
2517
|
+
"",
|
|
2518
|
+
"A BOUNDED STRING, NOT AN ENUM, deliberately. An enum here would be closed",
|
|
2519
|
+
"for good: `check-schema-freeze` refuses an added value, and an older SDK",
|
|
2520
|
+
"validating a new one refuses the WHOLE message, turning a `declined` into a",
|
|
2521
|
+
"two-minute timeout and an `unavailable`. `format: code` is `^[a-z_]+$`, so",
|
|
2522
|
+
"with the bound this is `^[a-z_]{1,64}$`: a machine code, never prose or",
|
|
2523
|
+
"markup. Precedent: gem.exit's `reason`. A receiver maps a value it does not",
|
|
2524
|
+
"know to `error`, so a new code costs an old game an `error`, not a hang.",
|
|
2525
|
+
"",
|
|
2526
|
+
"ONE VOCABULARY FOR BOTH PURCHASE PATHS. The value equals the `code` the",
|
|
2527
|
+
"game gets from `gem.me.mutations.purchase` for the same refusal, so a game",
|
|
2528
|
+
"handles both with one switch. gem-api's purchase wire codes pass VERBATIM;",
|
|
2529
|
+
"`unknown_currency`, `no_price_in_currency`, `busy`, `cancelled` and `error`",
|
|
2530
|
+
"are the parent's (and the SDK's) own. `currency_not_in_game_payable` and",
|
|
2531
|
+
"`not_purchasable` are gem-api's, which a parent may also detect first.",
|
|
2532
|
+
"",
|
|
2533
|
+
"THE MAPPING IS NORMATIVE, AND IT IS AN ALLOW-LIST. A parent sends exactly",
|
|
2534
|
+
"these. Anything it meets that is not listed is sent as `error`, never",
|
|
2535
|
+
"forwarded - so a new gem-api code, a detail field or an account state",
|
|
2536
|
+
"cannot leak through by default:",
|
|
2537
|
+
"",
|
|
2538
|
+
" CONDITION OUTCOME REASON",
|
|
2539
|
+
" the slug is not an active platform declined unknown_currency",
|
|
2540
|
+
" currency - including the retired TICKETS",
|
|
2541
|
+
" the currency is not payable in games declined currency_not_in_game_payable",
|
|
2542
|
+
" the target is not in the purchase catalog, declined not_purchasable",
|
|
2543
|
+
" or is not purchasable",
|
|
2544
|
+
" the target has no price in that currency declined no_price_in_currency",
|
|
2545
|
+
" a sheet is already open, a purchase is in declined busy",
|
|
2546
|
+
" flight, a cancel back-off is running, or",
|
|
2547
|
+
" the ask arrived without user activation",
|
|
2548
|
+
" the player dismissed a sheet they could declined cancelled",
|
|
2549
|
+
" afford",
|
|
2550
|
+
" a shortfall shown on the sheet, or gem-api declined insufficient_funds",
|
|
2551
|
+
" 400 insufficient_funds",
|
|
2552
|
+
" gem-api price_mismatch or declined the code, verbatim",
|
|
2553
|
+
" config_version_stale (both refused",
|
|
2554
|
+
" before any debit)",
|
|
2555
|
+
" gem-api platform_currency_unavailable declined the code, verbatim",
|
|
2556
|
+
" gem-api not_available (the recipe is declined the code, verbatim",
|
|
2557
|
+
" outside its availability window, or",
|
|
2558
|
+
" inactive)",
|
|
2559
|
+
" gem-api recipe_locked declined recipe_locked - the",
|
|
2560
|
+
" CODE ONLY; the",
|
|
2561
|
+
" response's",
|
|
2562
|
+
" details.lock_reasons",
|
|
2563
|
+
" is never forwarded",
|
|
2564
|
+
" gem-api recipe_on_cooldown, declined the code, verbatim",
|
|
2565
|
+
" recipe_max_uses_exceeded,",
|
|
2566
|
+
" item_max_stack_exceeded,",
|
|
2567
|
+
" recipe_costs_unmet, quantity_invalid",
|
|
2568
|
+
" gem-api confirmation_required - NEVER REACHES THE",
|
|
2569
|
+
" GAME: the parent",
|
|
2570
|
+
" handles it itself,",
|
|
2571
|
+
" because it reveals",
|
|
2572
|
+
" the player's age",
|
|
2573
|
+
" gem-api 429 (its limiter is per IP, so it declined error",
|
|
2574
|
+
" would describe other people's activity),",
|
|
2575
|
+
" 403, nonce conflicts, and every account",
|
|
2576
|
+
" state - sanction, age, region",
|
|
2577
|
+
" 5xx, a network failure, a timeout, an `ok` unavailable error, or absent",
|
|
2578
|
+
" the frame could not verify, and a charge",
|
|
2579
|
+
" whose refund failed",
|
|
2580
|
+
"",
|
|
2581
|
+
"`declined` is for a definite refusal before any debit; a parent that",
|
|
2582
|
+
"answers such a refusal `unavailable` is wrong, because `unavailable` means",
|
|
2583
|
+
"'no reliable answer'. A reason never makes `unavailable` more certain: the",
|
|
2584
|
+
"game re-reads inventory after it, whatever the reason says.",
|
|
2585
|
+
"",
|
|
2586
|
+
"ADVISORY, AND NO DEFENCE AGAINST A HOSTILE PARENT. A lying parent can",
|
|
2587
|
+
"mislead a game's copy, never its ledger: games branch money on `outcome`",
|
|
2588
|
+
"and on inventory, and the SDK never retries because of a reason."
|
|
2589
|
+
]
|
|
2375
2590
|
},
|
|
2376
2591
|
"purchaseId": {
|
|
2377
2592
|
"kind": "string",
|