@indiegems/gem-web-sdk 23.2.0 → 24.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.
@@ -1,8 +1,9 @@
1
1
  # Arcade features and the Arcade Store
2
2
 
3
3
  Your game can reach a few things the Arcade owns: its own app and yours in the app stores, another
4
- game, games like yours, and the **Arcade Store**, which sells Gem packs, Boosts, passes and
5
- bundles for real money.
4
+ game, games like yours, and the **Arcade Store**, which sells Coin packs, passes such as VIP,
5
+ Boosts and bundles for real money. **Only the Arcade sells Coins**: a game never sells currency
6
+ for money.
6
7
 
7
8
  ```js
8
9
  // From the press on your own "More games like this" card.
@@ -12,17 +13,17 @@ async function onCardPressed(card) {
12
13
  if (outcome !== 'ok') showCardAsUnavailable(card);
13
14
  }
14
15
 
15
- // From the press on your own "Get Gems" button.
16
+ // From the press on your own "Get Coins" button.
16
17
  async function onBuyPressed(offer) {
17
18
  const { outcome, reason } = await gem.store.purchase(offer.skuId);
18
19
  // Read through gem.me; nothing here carries an amount. `error` is "outcome unknown" (§9).
19
20
  if (outcome === 'ok' || reason === 'error') await refreshBalances();
20
21
  }
21
22
 
22
- // From the press on your own "Get VIP" button: the Arcade draws the offer.
23
+ // From the press on your own "Get VIP" button: the Arcade draws the pass, found by its slug.
23
24
  async function onVipPressed() {
24
- const { outcome, reason } = await gem.store.showOffer(VIP_SKU);
25
- if (outcome === 'ok' || reason === 'error') await refreshBalances();
25
+ const { held } = await gem.store.showPass('vip');
26
+ if (held) drawVipBadge(); // a fresh read already shows VIP held (§10)
26
27
  }
27
28
  ```
28
29
 
@@ -51,10 +52,10 @@ itself. You get an outcome.
51
52
  | Money | none | **real money**, through the Arcade Store's checkout |
52
53
  | Who draws it | the Arcade (a new tab, a QR code, the next game) | your card or the Arcade's sheet, then the checkout |
53
54
 
54
- `gem.store` is not `gem.purchases`. **`gem.purchases` spends Gems or Coins on your game's own
55
- items** ([`inventory-and-catalogs.md`](inventory-and-catalogs.md)). **`gem.store` sells the Arcade
56
- Store's own SKUs for the player's money.** A game that wants a "Get Gems" button uses `gem.store`;
57
- a game selling a sword for 50 Gems uses `gem.purchases`.
55
+ `gem.store` is not `gem.purchases`. **`gem.purchases` spends Coins on your game's own items**
56
+ ([`inventory-and-catalogs.md`](inventory-and-catalogs.md)). **`gem.store` sells the Arcade Store's
57
+ own SKUs for the player's money.** A game that wants a "Get Coins" button uses `gem.store`; a game
58
+ selling a sword for 250 Coins uses `gem.purchases`.
58
59
 
59
60
  ## 2. Nothing rejects
60
61
 
@@ -78,7 +79,7 @@ An Arcade that does not offer a feature answers every call for it at once with `
78
79
  ```js
79
80
  if (gem.arcade.available.openGame) showMoreGamesSection();
80
81
  if (gem.arcade.available.appLinks) showGetTheAppButton();
81
- if (gem.store.available) showGetGemsButton();
82
+ if (gem.store.available) showGetCoinsButton();
82
83
  if (gem.store.showOfferAvailable) showGetVipButton();
83
84
  ```
84
85
 
@@ -222,15 +223,26 @@ policy refused. Allow that host rather than a wildcard.
222
223
  ## 8. The Arcade Store's offers
223
224
 
224
225
  ```js
225
- const { outcome, reason, offers } = await gem.store.offers();
226
- if (outcome === 'unavailable' && reason === 'sign_in_required') showSignInButton(); // gem.account.requestSignIn
227
- for (const offer of offers) drawOffer(offer);
226
+ const { outcome, offers } = await gem.store.offers();
227
+ for (const offer of offers) if (offer.kind === 'coin_pack') drawCoinPack(offer);
228
228
  ```
229
229
 
230
- `offers()` lists what the Arcade Store sells this player, here, right now: Gem packs, Boosts,
231
- passes and bundles. Each offer has a `skuId`, a `kind` (`gem_pack`, `boost`, `pass` or `bundle`),
232
- the store's own `name` and `description`, an optional `imageUrl`, a `priceDisplay`, and, for a
233
- Gem pack, a `grant` naming the currency and amount.
230
+ `offers()` lists what the Arcade Store sells this player, here, right now: Coin packs, passes,
231
+ Boosts and bundles. Each offer has:
232
+
233
+ - a `skuId`, which `purchase` and `showOffer` take;
234
+ - a `kind`: `coin_pack`, `pass`, `boost` or `bundle`. `gem_pack` is deprecated: Gems are being
235
+ retired and a Gem pack is no longer sold. Keep a `default` arm;
236
+ - the store's own `name` and `description`, an optional `imageUrl`, and a `priceDisplay`;
237
+ - for a Coin pack, a `grant` naming the currency (`ARCADE_TICKETS`) and the amount;
238
+ - for a pass, its slug in `pass` (VIP is `vip`), which `showPass` takes;
239
+ - `earnMultipliers`: for an offer that makes the player earn a currency faster while they hold it,
240
+ such as VIP's +50% Coins, one `{ currencySlug, multiplierBp }` per currency. Empty otherwise.
241
+ **Never multiply a reward by it yourself**; the platform applies it when it pays.
242
+
243
+ **A guest is answered too.** Buying takes a guest through sign-up in the Arcade's own screens, then
244
+ back to what they chose, so a guest sees your "Get Coins" and "Get VIP" buttons. An older Arcade
245
+ answers a guest `unavailable` / `sign_in_required`: hide the buttons there.
234
246
 
235
247
  - **Show `priceDisplay` exactly as given.** It is the payment rail's own price in the player's
236
248
  currency, already formatted. Never parse it, compare two of them, or compute a discount from it.
@@ -294,16 +306,17 @@ arrives, so your own celebration is visible.
294
306
 
295
307
  ```js
296
308
  // Decide on the screen you draw the button on.
297
- const { offers } = await gem.store.offers();
298
- const vip = offers.find((offer) => offer.skuId === VIP_SKU);
299
- if (gem.store.showOfferAvailable && vip) showGetVipButton();
309
+ if (gem.store.showOfferAvailable && (await gem.store.passOnSale('vip'))) showGetVipButton();
300
310
  else hideGetVipButton();
301
311
 
302
312
  // From the press on that button.
303
- const { outcome, reason } = await gem.store.showOffer(VIP_SKU);
304
- if (outcome === 'ok' || reason === 'error') await refreshBalances();
313
+ const { outcome, reason, held } = await gem.store.showPass('vip');
305
314
  ```
306
315
 
316
+ For a pass, use **`showPass(slug)`**: it finds the pass's offer by its slug and shows it, so your
317
+ game never names a SKU ([below](#a-pass-by-its-slug-showpass)). `showOffer(skuId)` is the same
318
+ sheet for any other SKU, such as a bundle.
319
+
307
320
  `showOffer(skuId)` asks the Arcade to draw one SKU from `offers()` in its own sheet, over your paused
308
321
  and hidden game. The Arcade draws the art, what the player gets, a sale if one is running, and, for a
309
322
  pass the player already holds, how long it has left. The sheet's Buy opens the same checkout
@@ -311,13 +324,13 @@ pass the player already holds, how long it has left. The sheet's Buy opens the s
311
324
 
312
325
  Pick by who can say what the offer is:
313
326
 
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 |
327
+ | | **You draw it: `offers()` + `purchase()`** | **The Arcade draws it: `showPass()` or `showOffer()`** |
328
+ | --------------- | ---------------------------------------------------- | --------------------------------------------------------------- |
329
+ | Use it for | Coin packs: a name, an amount and a price say it all | VIP and other passes (`showPass`), and bundles |
330
+ | Your button | says what it buys and shows `priceDisplay` | says what it opens, such as "Get VIP" |
331
+ | The press opens | the checkout, straight away | the Arcade's sheet, then the checkout if the player presses Buy |
332
+ | Perks and sales | not available to you | always current, in the player's language |
333
+ | A holder sees | the same Buy button | what they already have, and how to add time |
321
334
 
322
335
  - **Never write an offer's perks into your game.** They change without a new build of your game, and
323
336
  yours would go stale. Your button only needs the offer's name.
@@ -327,7 +340,9 @@ Pick by who can say what the offer is:
327
340
  - **The sheet does not tell you what the player holds.** It tells the player, so a button that
328
341
  stays up for a holder is fine: they see what they already have, and choose from there. To change
329
342
  your own screen for a holder, read `gem.me.passes` ([§11](#11-what-the-player-holds-and-a-vip-door)).
330
- - **Hide the button when the SKU is not in `offers()`.** A SKU that is not on sale here answers
343
+ - **Hide the button when the pass is not on sale.** `passOnSale(slug)` says so, and is `false`
344
+ where the store is off, for a player the platform won't sell it to, once the player has declined
345
+ its sheet twice today, and on an older Arcade. A pass that is not on sale answers
331
346
  `unavailable` / `not_found`, and the player would have pressed a button that does nothing.
332
347
  - **Read `showOfferAvailable`, not `available`.** An older Arcade can sell through `purchase` and
333
348
  still not draw this sheet. It answers `unavailable` / `unsupported` and sends nothing. Hide the
@@ -336,16 +351,72 @@ Pick by who can say what the offer is:
336
351
  Every rule of `purchase` holds ([§9](#9-buying-from-the-arcade-store)), and the answers are the
337
352
  same, with one addition. `declined` / `cancelled` also covers the player leaving the sheet without
338
353
  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.
354
+ next press on either button opens it again. A pass is the exception: after the player declines its
355
+ sheet **twice in a day**, the Arcade stops offering it to them until tomorrow, and `passOnSale`
356
+ answers `false`. Your game cannot reset that count.
357
+
358
+ ### A pass, by its slug: `showPass`
359
+
360
+ `showPass(slug)` is `offers()` plus `showOffer()`, with one promise of its own: **`ok` means a fresh
361
+ read of `gem.me.passes` shows the pass held.** A recipe or claim your game runs next gets the pass's
362
+ bonus, with no reload. That makes it safe to call at the moment the player is about to claim
363
+ something:
364
+
365
+ | Answer | `held` | What happened | Then |
366
+ | ----------------------------- | ------- | ------------------------------------------------------------------- | ------------------------------------------------------------------ |
367
+ | `ok` | `true` | the player bought it, and it is held now | run the recipe or claim: the bonus applies |
368
+ | `ok` / `pending` | `false` | the player paid, and the pass has not arrived yet (Ask to Buy, say) | run it now, without the bonus; nothing is held back for later |
369
+ | `declined` / `cancelled` | `false` | they closed the sheet | run it as it is, and don't ask again at this moment |
370
+ | `unavailable` / `not_found` | `false` | not on sale to this player here, or an older Arcade | run it as it is, and hide the button |
371
+ | `unavailable` / `unsupported` | `false` | this Arcade draws no sheet | run it as it is, and hide the button |
372
+ | `unavailable` / `error` | `false` | **the outcome is unknown, and the player may have paid** | run it; `gem.me.passes.on('changed')` hears the pass if it arrives |
373
+
374
+ - **Pass it the slug the platform gave you,** never a SKU. VIP is `vip`.
375
+ - **A guest** goes through sign-up first, then back to the sheet. A guest who signs up and buys
376
+ answers `ok` like anyone else.
377
+ - **After a paid sheet it waits a little.** It reads `gem.me.passes` for about ten seconds before it
378
+ settles for `pending`, because the payment provider's notice can trail the checkout.
379
+ - **A player who already holds the pass** sees how long they have left and can add time.
380
+
381
+ ### At a reward: the VIP extra roll
382
+
383
+ The moment a pass sells best is the moment it would pay: a chest the player is about to open, where
384
+ VIP adds a roll. **Your game never names the pass and never works out the extra.** It asks
385
+ `gem.gameConfig.preview` what the claim gives, and the answer's `upsells` says what each pass on
386
+ sale here would add:
387
+
388
+ ```js
389
+ // The recipe carries `bonuses: [{ id: 'vip_roll', requires: [{ kind: 'holds_pass', pass: 'vip' }],
390
+ // grants: [{ type: 'roll', loot_table_id: '<chest_common>', count: 1 }] }]` in your game config.
391
+ openButton.onclick = async () => {
392
+ const preview = await gem.gameConfig.preview({ recipe: 'open_chest' });
393
+ const upsell = preview.upsells[0];
394
+ if (upsell) {
395
+ // "VIP: +1 roll, 45 Coins" — upsell.name, and the extras locked, never what a roll would land on.
396
+ const choice = await vipCard.ask(upsell); // Get VIP and Continue, with equal weight
397
+ if (choice === 'get') await gem.store.showPass(upsell.pass);
398
+ openButton.focus(); // focus goes back where it was
399
+ }
400
+ const order = await gem.me.mutations.executeRecipe(CHESTS, 'open_chest');
401
+ // The base always runs; the VIP roll applies if the pass is held now.
402
+ };
403
+ ```
404
+
405
+ `upsells` is already empty wherever no upsell may be shown: for a minor, a store that is off, a pass
406
+ the player declined twice today or already holds, and a claim no pass would change. So an empty
407
+ list means draw no card. **Upsell only before the grant**, once per reward moment: the platform
408
+ holds nothing back, so a pass bought afterwards cannot reach a chest already opened.
409
+ [inventory-and-catalogs.md](inventory-and-catalogs.md#offering-vip-before-a-reward-the-upsell) has
410
+ the rest of the rules, every one a must, and `bonuses` in full.
340
411
 
341
412
  ## 11. What the player holds, and a VIP door
342
413
 
343
414
  ```js
344
415
  const passes = await gem.me.passes.list();
345
- // [{ pass: 'vip', held: true, endsAt: '2026-11-29T12:34:56Z' }]
416
+ // [{ pass: 'vip', held: true, daysLeft: 7, endsAt: '2026-11-29T12:34:56Z' }]
346
417
 
347
418
  const vip = await gem.me.passes.get('vip'); // one pass by its slug
348
- if (vip.held) drawVipBadge();
419
+ if (vip.held) drawVipBadge(vip.daysLeft === 7 ? '7+ days left' : `${vip.daysLeft} days left`);
349
420
  ```
350
421
 
351
422
  `gem.me.passes` says which store passes the player holds. **VIP is the pass `vip`**, its offering
@@ -354,12 +425,15 @@ slug. Key on the slug, which is stable and not localized, and never on a display
354
425
  - **It lists every pass games may see, held or not.** An admin marks a pass as visible to games, and
355
426
  the list has one entry for each, so its shape never says what the player bought. A pass that is not
356
427
  marked is left out, and `get` reads a pass that is not listed as not held.
357
- - **A pass is a slug, whether it is held, and when it ends.** No price, no name, no perks, nothing
358
- about how the player got it, and nothing about a pass that has ended.
359
- - **`endsAt` is the exact end,** grace and any queued period included, and `null` when the pass is not
360
- held or is held with no end, which support can grant. Read `held` first: `null` alone never means
361
- ended.
362
- - **A guest reads as not holding.** Every pass is `held: false`, and that is no error.
428
+ - **A pass is a slug, whether it is held, and how many days it has left.** No price, no name, no
429
+ perks, nothing about how the player got it, and nothing about a pass that has ended.
430
+ - **`daysLeft` is whole days, rounded up and capped at 7**: `7` means "7 or more", and is also what
431
+ a pass held with no end reads (support can grant one). `0` when it is not held. Show `7` as
432
+ "7+ days left", never as a date.
433
+ - **`endsAt` is deprecated.** It is the exact end, to the second, and two games could match that
434
+ value to link one player's accounts, so the platform will stop sending it. Use `daysLeft`.
435
+ - **A guest holds nothing.** When the Arcade says the player is a guest, every pass reads
436
+ `held: false` without a call to the platform, and that is no error.
363
437
  - **A read that fails rejects with a `GemApiError`, as every `gem.me` read does.** That is unknown,
364
438
  not "not held", so keep what you drew. It is the one call on this page that rejects.
365
439
  - **It is a read over your play token,** not an ask of the Arcade. It needs no press, and works
@@ -401,7 +475,7 @@ door.onPress = async () => {
401
475
  };
402
476
 
403
477
  // 3. BUYING. From the press on your own "How to get VIP" button.
404
- howToGetVipButton.onclick = () => gem.store.showOffer(VIP_SKU);
478
+ howToGetVipButton.onclick = () => gem.store.showPass('vip');
405
479
  ```
406
480
 
407
481
  The function, in Rust, because only the platform's answer counts:
@@ -439,25 +513,32 @@ Ok(answer.to_string().into_bytes())
439
513
  "Me" on that route is the player who invoked the function. A function your server invoked has no
440
514
  player, and the route refuses it with `403`. The body is `{ pass, held, ends_at }` for each pass.
441
515
 
516
+ **Gate on the pass, never on the boost.** VIP also shows in `gem.player.boost` as +50% Coins, but a
517
+ boost is display-only and a player can hold one without VIP. What a pass unlocks is decided by
518
+ `held` on this read, or by a `requires: holds_pass` on your config, checked by the platform
519
+ ([platform rules §8](platform-rules.md#8-a-players-boost-is-for-display-never-a-gate)).
520
+
442
521
  - **If the door and the function disagree, the function is right.** The player sees "How to get VIP"
443
522
  or the lounge, and either way nobody got in by editing the page. Redraw the door when the function
444
523
  says no.
445
- - **A guest has no VIP and cannot buy it from here.** Their door is shut, and `offers()` answers
446
- `unavailable` / `sign_in_required`, so offer `gem.account.requestSignIn` instead of the button.
524
+ - **A guest has no VIP, and can buy it from here.** Their door is shut, and `showPass('vip')` takes
525
+ them through sign-up, then back to the sheet. On an older Arcade, which answers a guest
526
+ `unavailable` / `sign_in_required`, offer `gem.account.requestSignIn` instead, when
527
+ `gem.account.signInAvailable` says the Arcade can show them a sign-in.
447
528
 
448
529
  ### When the door moves by itself
449
530
 
450
531
  While something listens to `on('changed')`, the SDK reads again at each of these, and calls you only
451
532
  if the answer differs:
452
533
 
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 |
534
+ | Moment | Why you cannot rely on something else |
535
+ | --------------------------------------------------- | ------------------------------------------------------------------------------ |
536
+ | `store.purchase`, `showOffer` or `showPass` is `ok` | `onSettled` does not fire after a plain `ok` |
537
+ | Either answers `unavailable` / `error` | The outcome is unknown, and the player may have paid |
538
+ | `gem.store.onSettled` fires | `ok` / `pending` still reads as not held |
539
+ | Your game resumes | A purchase in the Arcade's own store, from the Gem menu, sends no event at all |
540
+ | The player signs in | A guest who signs in may hold VIP |
541
+ | A held pass reaches `endsAt` | VIP does not renew, so the door has to lock by itself |
461
542
 
462
543
  A read that fails changes nothing, and is tried again later. A read that was already on its way when
463
544
  a purchase ended is never reused, so a stale answer cannot shut the door on the buyer. If you read
@@ -467,14 +548,36 @@ without a listener, call `list()` yourself at those moments; it is always fresh.
467
548
 
468
549
  ### On a dedicated server
469
550
 
470
- The platform has a read for this,
471
- `GET /v1/games/{game_id}/matches/{match_id}/players/{player_id}/passes`, with the same body, for a
472
- player in your live match. **A server bundle cannot call it yet**, and the SDK has no method for it,
473
- because it needs the allocation nonce: the route requires `X-Alloc-Nonce`, which the platform's
474
- runtime holds and never hands to your bundle. The boost read has the same limit. Reaching it takes a
475
- hook in the runtime, like the one behind `gem.match.end()`.
551
+ `gem.match.playerPasses(playerId)` reads which passes a player in your match holds, in the shape
552
+ `gem.me.passes.list()` gives: `{ pass, held, daysLeft }` for each pass marked visible to games. On a
553
+ server it is the decision itself, because the browser is not in the call:
554
+
555
+ ```js
556
+ export default defineServer((gem) => ({
557
+ async join(playerId) {
558
+ let vip = false;
559
+ try {
560
+ vip = (await gem.match.playerPasses(playerId)).some((p) => p.pass === 'vip' && p.held);
561
+ } catch {
562
+ // unknown: keep the lounge shut, and let the player ask again
563
+ }
564
+ if (vip) admitToLounge(playerId);
565
+ },
566
+ }));
567
+ ```
568
+
569
+ - **The platform half of the runtime makes the read**, with the match's own proof, which never
570
+ enters your bundle. So does `gem.match.end()`.
571
+ - **It throws on a runtime image that predates it, and on a listen host** (a player's browser
572
+ hosting a P2P match), which has no such proof. Decide in a function there.
573
+ - **A player outside your match** is refused `not_found` (`player_not_in_match`), and nothing about
574
+ them is read.
575
+ - **It may be refused `forbidden` (`client_permission_denied`)** until the platform grants game
576
+ servers the pass read. That is unknown, not "not held": keep the door shut and say so, rather
577
+ than telling a VIP they have no VIP.
476
578
 
477
- Until then, decide in a function, or leave a VIP door out of a dedicated-server match.
579
+ `gem.match.playerBoost(playerId)` reads the player's Boost for **display** — "2x Coins" on a results
580
+ screen — never for a decision.
478
581
 
479
582
  ## 12. Outcomes and reasons
480
583
 
@@ -484,9 +587,10 @@ Until then, decide in a function, or leave a VIP door out of a dedicated-server
484
587
  | `openApp` | opened, or `handed_off` | `busy` | `already_in_app`, `unsupported`, `error` |
485
588
  | `openGame` | the next game is starting | `busy`, `same_game` | `not_found`, `unsupported`, `error` |
486
589
  | `similarGames` | cards, possibly none | | `unsupported`, `error` |
487
- | `store.offers` | offers, possibly none | | `sign_in_required`, `store_off`, `unsupported`, `error` |
590
+ | `store.offers` | offers, possibly none | | `store_off`, `sign_in_required` (an older Arcade's guest), `unsupported`, `error` |
488
591
  | `store.purchase` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
489
592
  | `store.showOffer` | paid, or `pending` | `cancelled`, `busy` | `sign_in_required`, `store_off`, `not_found`, the provider's reason, `unsupported`, `error` (outcome unknown) |
593
+ | `store.showPass` | held, or `pending` | `cancelled`, `busy` | `not_found`, `store_off`, the provider's reason, `invalid_argument`, `unsupported`, `error` (outcome unknown) |
490
594
 
491
595
  Every reason is a short code of lower-case letters and underscores. The Arcade turns a payment
492
596
  provider's own code into that shape before it reaches you.
@@ -495,10 +599,11 @@ Four reasons come from the SDK itself rather than the Arcade:
495
599
 
496
600
  - `unsupported`: this Arcade does not offer the feature. Nothing was sent.
497
601
  - `invalid_argument`: the SDK refused the argument (an unknown `target`, a `ref` that is not an id
498
- or a web address, a `limit` outside 1–24, an empty `skuId`). Nothing was sent.
602
+ or a web address, a `limit` outside 1–24, an empty `skuId` or pass slug). Nothing was sent.
499
603
  - `busy`: another acting ask from your game is still open. Nothing was sent.
500
604
  - `error`: no reliable answer arrived. The ask could not be sent, a read or an open waited out its
501
605
  timeout (30 seconds for a read, two minutes for `openApp` and `openGame`), the bridge closed
502
606
  before the answer came, or the Arcade said it had no reliable answer. For a read, try again
503
- later. For `store.purchase` and `store.showOffer` it means **the outcome is unknown**, never
607
+ later. For `store.purchase`, `store.showOffer` and `store.showPass` it means **the outcome is
608
+ unknown**, never
504
609
  "nothing was bought" ([§9](#9-buying-from-the-arcade-store)).