@indiegems/gem-web-sdk 24.0.1 → 24.1.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.
@@ -318,9 +318,8 @@ game never names a SKU ([below](#a-pass-by-its-slug-showpass)). `showOffer(skuId
318
318
  sheet for any other SKU, such as a bundle.
319
319
 
320
320
  `showOffer(skuId)` asks the Arcade to draw one SKU from `offers()` in its own sheet, over your paused
321
- and hidden game. The Arcade draws the art, what the player gets, a sale if one is running, and, for a
322
- pass the player already holds, how long it has left. The sheet's Buy opens the same checkout
323
- `purchase` opens.
321
+ and hidden game. The Arcade draws the art, what the player gets and a sale if one is running. The
322
+ sheet's Buy opens the same checkout `purchase` opens.
324
323
 
325
324
  Pick by who can say what the offer is:
326
325
 
@@ -330,20 +329,24 @@ Pick by who can say what the offer is:
330
329
  | Your button | says what it buys and shows `priceDisplay` | says what it opens, such as "Get VIP" |
331
330
  | The press opens | the checkout, straight away | the Arcade's sheet, then the checkout if the player presses Buy |
332
331
  | 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 |
332
+ | A held pass | your button cannot tell | the Arcade decides whether the holder is offered it again |
334
333
 
335
334
  - **Never write an offer's perks into your game.** They change without a new build of your game, and
336
335
  yours would go stale. Your button only needs the offer's name.
337
336
  - **Do not show a price on the button.** The sheet shows the right one for this player, which a
338
337
  sale or a pass can change, and `priceDisplay` does not say either of those. If you want a price on
339
338
  screen, draw the offer yourself and use `purchase`.
340
- - **The sheet does not tell you what the player holds.** It tells the player, so a button that
341
- stays up for a holder is fine: they see what they already have, and choose from there. To change
342
- your own screen for a holder, read `gem.me.passes` ([§11](#11-what-the-player-holds-and-a-vip-door)).
339
+ - **The sheet does not tell you what the player holds.** To change your own screen for a holder,
340
+ read `gem.me.passes` ([§11](#11-what-the-player-holds-and-a-vip-door)).
341
+ - **A player who already holds a pass may or may not be offered it again.** The Arcade decides,
342
+ and your game must not assume either. `passOnSale` answers for a holder as it does for anyone:
343
+ where it is `false`, `showPass` answers `unavailable` / `not_found` and opens nothing.
343
344
  - **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
346
- `unavailable` / `not_found`, and the player would have pressed a button that does nothing.
345
+ where the store is off, for a player the platform won't sell it to, and on an older Arcade. A
346
+ player closing its sheet never takes it off sale. A pass that is not on sale answers
347
+ `unavailable` / `not_found`, and the player would have pressed a button that does nothing. To
348
+ test as a VIP holder before VIP is on sale, give yourself VIP with gemctl
349
+ ([§11](#testing-what-a-vip-player-sees)).
347
350
  - **Read `showOfferAvailable`, not `available`.** An older Arcade can sell through `purchase` and
348
351
  still not draw this sheet. It answers `unavailable` / `unsupported` and sends nothing. Hide the
349
352
  button there.
@@ -351,9 +354,10 @@ Pick by who can say what the offer is:
351
354
  Every rule of `purchase` holds ([§9](#9-buying-from-the-arcade-store)), and the answers are the
352
355
  same, with one addition. `declined` / `cancelled` also covers the player leaving the sheet without
353
356
  pressing Buy, and you are not told which of the two they closed. Nothing backs off: the player's
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
+ next press on either button opens it again. That holds for a pass too: **there is no limit on how
358
+ often your game opens a pass's sheet**, and a player closing it any number of times leaves
359
+ `passOnSale` and `upsells` as they were. The Arcade records how often each pass sheet is opened in
360
+ your game and how long it stays open, for reporting; it never uses that to stop an offer.
357
361
 
358
362
  ### A pass, by its slug: `showPass`
359
363
 
@@ -376,7 +380,8 @@ something:
376
380
  answers `ok` like anyone else.
377
381
  - **After a paid sheet it waits a little.** It reads `gem.me.passes` for about ten seconds before it
378
382
  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.
383
+ - **A player who already holds the pass** may or may not be offered it again. Where they are not,
384
+ the answer is `unavailable` / `not_found`, as for any pass that is not on sale to the player.
380
385
 
381
386
  ### At a reward: the VIP extra roll
382
387
 
@@ -393,7 +398,7 @@ openButton.onclick = async () => {
393
398
  const upsell = preview.upsells[0];
394
399
  if (upsell) {
395
400
  // "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
401
+ const choice = await vipCard.ask(upsell); // Get VIP, or Continue
397
402
  if (choice === 'get') await gem.store.showPass(upsell.pass);
398
403
  openButton.focus(); // focus goes back where it was
399
404
  }
@@ -403,11 +408,11 @@ openButton.onclick = async () => {
403
408
  ```
404
409
 
405
410
  `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.
411
+ the player already holds, and a claim no pass would change. A player declining a pass's sheet does
412
+ not empty it. So an empty list means draw no card. **The offer comes before the grant**: the
413
+ platform holds nothing back, so a pass bought afterwards cannot reach a chest already opened.
409
414
  [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.
415
+ how the upsell works, the one rule for it (disclose a bonus roll's odds), and `bonuses` in full.
411
416
 
412
417
  ## 11. What the player holds, and a VIP door
413
418
 
@@ -416,7 +421,7 @@ const passes = await gem.me.passes.list();
416
421
  // [{ pass: 'vip', held: true, daysLeft: 7, endsAt: '2026-11-29T12:34:56Z' }]
417
422
 
418
423
  const vip = await gem.me.passes.get('vip'); // one pass by its slug
419
- if (vip.held) drawVipBadge(vip.daysLeft === 7 ? '7+ days left' : `${vip.daysLeft} days left`);
424
+ if (vip.held) drawVipBadge(vip.daysLeft < 7 ? `VIP · ${vip.daysLeft} days left` : 'VIP');
420
425
  ```
421
426
 
422
427
  `gem.me.passes` says which store passes the player holds. **VIP is the pass `vip`**, its offering
@@ -428,8 +433,9 @@ slug. Key on the slug, which is stable and not localized, and never on a display
428
433
  - **A pass is a slug, whether it is held, and how many days it has left.** No price, no name, no
429
434
  perks, nothing about how the player got it, and nothing about a pass that has ended.
430
435
  - **`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.
436
+ a pass held with no end reads (support can grant one). `0` when it is not held. **`7` is not a
437
+ week's notice:** a pass can read `7` for as long as it is held, and a lower count can rise again.
438
+ Show a count only below `7`, or show `7` as "7+ days left". Never show a date.
433
439
  - **`endsAt` is deprecated.** It is the exact end, to the second, and two games could match that
434
440
  value to link one player's accounts, so the platform will stop sending it. Use `daysLeft`.
435
441
  - **A guest holds nothing.** When the Arcade says the player is a guest, every pass reads
@@ -579,6 +585,26 @@ export default defineServer((gem) => ({
579
585
  `gem.match.playerBoost(playerId)` reads the player's Boost for **display** — "2x Coins" on a results
580
586
  screen — never for a decision.
581
587
 
588
+ ### Testing what a VIP player sees
589
+
590
+ VIP may not be on sale where you test yet, so `passOnSale('vip')` reads `false` and you cannot buy
591
+ it to test your door, badge or upsell. Give it instead, for 1 to 30 days, with gemctl:
592
+
593
+ ```sh
594
+ gemctl org test-accounts pass grant <org-id> me vip --days 7 # your own account
595
+ gemctl org test-accounts pass grant <org-id> "Playtest 3" vip --days 7 # one of the org's test accounts
596
+ ```
597
+
598
+ - **You need the Contributor role or above** in the organization. `me` reaches your own account and
599
+ no one else's; any other name is one of the organization's test accounts.
600
+ - **It is not a purchase.** VIP held this way reads like a bought one in `gem.me.passes`,
601
+ `gem.match.playerPasses` and a `holds_pass` requirement, but no purchase is recorded, it is never
602
+ revenue, and nothing renews it: it ends after the days you gave.
603
+ - **One VIP at a time per account.** An account that already holds VIP, granted or bought, is
604
+ refused until it ends. The organization's audit log records every grant.
605
+ - **A personal access token is refused:** sign in to gemctl as yourself.
606
+ `gemctl org test-accounts pass grant --help` has the rest.
607
+
582
608
  ## 12. Outcomes and reasons
583
609
 
584
610
  | Call | `ok` | `declined` | `unavailable` |