@looplay/sdk 0.5.1 → 0.6.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.
@@ -31,6 +31,12 @@ var NotAuthenticatedError = class extends LooplaySDKError {
31
31
  this.name = "NotAuthenticatedError";
32
32
  }
33
33
  };
34
+ var MissingStoreError = class extends LooplaySDKError {
35
+ constructor(gameId) {
36
+ super(`Game ${gameId} has no storefront configured.`);
37
+ this.name = "MissingStoreError";
38
+ }
39
+ };
34
40
 
35
41
  // src/auth/storage.ts
36
42
  var MemoryAuthStorage = class {
@@ -259,32 +265,39 @@ var ServiceClient = class {
259
265
  async listRecentPlayed(bearerToken, query) {
260
266
  return this.http.request("GET", "/sdk/games/recent-play", { bearerToken, query });
261
267
  }
262
- async trackView(auth, gameId) {
263
- await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/view`, {
268
+ async recordGameView(auth, gameId) {
269
+ await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/views/record`, {
264
270
  ...auth,
265
271
  headers: this.trackingHeaders(auth)
266
272
  });
267
273
  return true;
268
274
  }
269
- async trackPlay(auth, gameId, attemptId, playTimeSeconds) {
270
- const body = { playTimeSeconds, attemptId };
271
- await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/play`, {
275
+ /** Signs a fresh `attemptToken` — required by `completeGameplayAttempt`/`completeGameplayMatch`/`recordGameplayAction`. */
276
+ async startGameplayAttempt(auth, gameId, body = {}) {
277
+ return this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/attempts/start`, {
278
+ ...auth,
279
+ headers: this.trackingHeaders(auth),
280
+ body
281
+ });
282
+ }
283
+ async completeGameplayAttempt(auth, gameId, body) {
284
+ await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/attempts/complete`, {
272
285
  ...auth,
273
286
  headers: this.trackingHeaders(auth),
274
287
  body
275
288
  });
276
289
  return true;
277
290
  }
278
- async trackMatchEnd(auth, gameId, body) {
279
- await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/end`, {
291
+ async completeGameplayMatch(auth, gameId, body) {
292
+ await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/matches/complete`, {
280
293
  ...auth,
281
294
  headers: this.trackingHeaders(auth),
282
295
  body
283
296
  });
284
297
  return true;
285
298
  }
286
- async emitGameEvent(auth, gameId, body) {
287
- return this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/emit`, {
299
+ async recordGameplayAction(auth, gameId, body) {
300
+ return this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/actions/record`, {
288
301
  ...auth,
289
302
  headers: this.trackingHeaders(auth),
290
303
  body
@@ -295,7 +308,7 @@ var ServiceClient = class {
295
308
  * runtimes should still call it to get a `trackingSessionId` for unified
296
309
  * tracing — see `GameTrackingSessionDto`.
297
310
  */
298
- async issueTrackingSession(anonId) {
311
+ async bootstrapTrackingSession(anonId) {
299
312
  return this.http.request("POST", "/sdk/games/tracking/bootstrap", {
300
313
  headers: { "x-looplay-anon-id": anonId }
301
314
  });
@@ -307,22 +320,104 @@ var ServiceClient = class {
307
320
  };
308
321
  }
309
322
  // ─────────────────────────────────────────────────────────────────────────────
310
- // Game assets — spend coin balance on assets defined by the game's
311
- // creator. Coin balance itself comes from getBalances()/getMyBalance().
323
+ // Game store (JWT tier, keyed by `storeId`) — spend coin balance on offers
324
+ // (single asset or bundle) defined by the game's creator. Coin balance
325
+ // itself comes from getBalances()/getMyBalance(). Purchases require a
326
+ // signed `checkoutToken` from `createStoreCheckoutIntent` first.
312
327
  // ─────────────────────────────────────────────────────────────────────────────
313
- async listGameAssets(gameId, bearerToken) {
314
- return this.http.request("GET", `/sdk/games/${encodeURIComponent(gameId)}/assets`, {
328
+ async getStoreByStoreId(storeId) {
329
+ return this.http.request("GET", `/sdk/games/store/${encodeURIComponent(storeId)}`);
330
+ }
331
+ async listMyStorePurchasesByStoreId(bearerToken, storeId) {
332
+ return this.http.request("GET", `/sdk/games/store/${encodeURIComponent(storeId)}/purchases/me`, {
315
333
  bearerToken
316
334
  });
317
335
  }
318
- async purchaseGameAsset(bearerToken, gameId, assetCode, body = {}) {
319
- return this.http.request(
320
- "POST",
321
- `/sdk/games/${encodeURIComponent(gameId)}/assets/${encodeURIComponent(assetCode)}/purchase`,
322
- { bearerToken, body }
323
- );
336
+ async createStoreCheckoutIntent(bearerToken, body, trackingSessionId) {
337
+ return this.http.request("POST", "/sdk/games/store/by-store-id/checkout-intent", {
338
+ bearerToken,
339
+ headers: { "x-game-tracking-session-id": trackingSessionId },
340
+ body
341
+ });
342
+ }
343
+ async purchaseStoreOffer(bearerToken, body, trackingSessionId) {
344
+ return this.http.request("POST", "/sdk/games/store/by-store-id/purchase", {
345
+ bearerToken,
346
+ headers: { "x-game-tracking-session-id": trackingSessionId },
347
+ body
348
+ });
324
349
  }
325
350
  // ─────────────────────────────────────────────────────────────────────────────
351
+ // Runtime tier (`x-game-key`) — for a game embedding the SDK with its own
352
+ // public `appId`, without needing a resolved `gameId`/`storeId` first or a
353
+ // logged-in user (anonymous tracking still works via `TrackingAuth.anonId`).
354
+ // Never use the creator-secret tier (`x-creator-key`/`x-game-secret`) from
355
+ // client/browser code — that would leak the secret to every player.
356
+ // ─────────────────────────────────────────────────────────────────────────────
357
+ async recordGameViewForGameKey(gameKey, auth = {}) {
358
+ await this.http.request("POST", "/sdk/games/runtime/views/record", {
359
+ ...auth,
360
+ headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) }
361
+ });
362
+ return true;
363
+ }
364
+ async startGameplayAttemptForGameKey(gameKey, auth = {}, body = {}) {
365
+ return this.http.request("POST", "/sdk/games/runtime/attempts/start", {
366
+ ...auth,
367
+ headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
368
+ body
369
+ });
370
+ }
371
+ async completeGameplayAttemptForGameKey(gameKey, auth = {}, body) {
372
+ await this.http.request("POST", "/sdk/games/runtime/attempts/complete", {
373
+ ...auth,
374
+ headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
375
+ body
376
+ });
377
+ return true;
378
+ }
379
+ async completeGameplayMatchForGameKey(gameKey, auth = {}, body) {
380
+ await this.http.request("POST", "/sdk/games/runtime/matches/complete", {
381
+ ...auth,
382
+ headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
383
+ body
384
+ });
385
+ return true;
386
+ }
387
+ async recordGameplayActionForGameKey(gameKey, auth = {}, body) {
388
+ return this.http.request("POST", "/sdk/games/runtime/actions/record", {
389
+ ...auth,
390
+ headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
391
+ body
392
+ });
393
+ }
394
+ async getStoreForGameKey(gameKey) {
395
+ return this.http.request("GET", "/sdk/games/store/current", {
396
+ headers: { "x-game-key": gameKey }
397
+ });
398
+ }
399
+ async listGameAssetsForGameKey(gameKey) {
400
+ return this.http.request("GET", "/sdk/games/store/assets", {
401
+ headers: { "x-game-key": gameKey }
402
+ });
403
+ }
404
+ async listMyOwnedAssetsForGameKey(bearerToken, gameKey) {
405
+ return this.http.request("GET", "/sdk/games/store/assets/me", {
406
+ bearerToken,
407
+ headers: { "x-game-key": gameKey }
408
+ });
409
+ }
410
+ async listMyStorePurchasesForGameKey(bearerToken, gameKey) {
411
+ return this.http.request("GET", "/sdk/games/store/purchases/me", {
412
+ bearerToken,
413
+ headers: { "x-game-key": gameKey }
414
+ });
415
+ }
416
+ // No public/anonymous purchase route exists for the runtime tier —
417
+ // purchases move real coin balance and always require the JWT tier
418
+ // (createStoreCheckoutIntent/purchaseStoreOffer above) or the
419
+ // creator-secret tier, which this SDK intentionally never wires up.
420
+ // ─────────────────────────────────────────────────────────────────────────────
326
421
  // Balance
327
422
  // ─────────────────────────────────────────────────────────────────────────────
328
423
  async getBalances(bearerToken, query) {
@@ -352,11 +447,11 @@ var ServiceClient = class {
352
447
  // ─────────────────────────────────────────────────────────────────────────────
353
448
  // Tasks
354
449
  // ─────────────────────────────────────────────────────────────────────────────
355
- async listTasks(bearerToken) {
356
- return this.http.request("GET", "/sdk/task", { bearerToken });
450
+ async listTasks(bearerToken, query) {
451
+ return this.http.request("GET", "/sdk/task", { bearerToken, query: { ...query } });
357
452
  }
358
- async listFinishedTasks(bearerToken) {
359
- return this.http.request("GET", "/sdk/task/finished", { bearerToken });
453
+ async listFinishedTasks(bearerToken, query) {
454
+ return this.http.request("GET", "/sdk/task/finished", { bearerToken, query: { ...query } });
360
455
  }
361
456
  async startTask(bearerToken, body) {
362
457
  return this.http.request("POST", "/sdk/task/start", { bearerToken, body });
@@ -384,6 +479,8 @@ var ApiClient = class {
384
479
  trackingSession;
385
480
  trackingSessionPromise;
386
481
  currentAttemptId;
482
+ gameplayAttempt;
483
+ gameplayAttemptPromise;
387
484
  constructor(options) {
388
485
  if (!options.baseUrl) throw new MissingBaseUrlError();
389
486
  this.raw = new ServiceClient({
@@ -439,7 +536,7 @@ var ApiClient = class {
439
536
  return this.trackingSession;
440
537
  }
441
538
  if (!this.trackingSessionPromise) {
442
- this.trackingSessionPromise = this.raw.issueTrackingSession(anonId).then((dto) => {
539
+ this.trackingSessionPromise = this.raw.bootstrapTrackingSession(anonId).then((dto) => {
443
540
  this.trackingSession = {
444
541
  trackingSessionId: dto.trackingSessionId,
445
542
  anonTrackingToken: dto.trackingToken,
@@ -452,10 +549,42 @@ var ApiClient = class {
452
549
  }
453
550
  return this.trackingSessionPromise;
454
551
  }
552
+ /**
553
+ * Signs (and caches until near expiry) the `attemptToken` required by
554
+ * `trackPlay`/`trackMatch`/`emit` for the current attempt — does not
555
+ * swallow failures, unlike `ensureTrackingSession`, since these calls
556
+ * are rejected outright without a valid token.
557
+ */
558
+ async ensureGameplayAttempt(auth, gameId, attemptId) {
559
+ const now = Date.now();
560
+ if (this.gameplayAttempt && this.gameplayAttempt.gameId === gameId && this.gameplayAttempt.attemptId === attemptId && this.gameplayAttempt.expiresAtMs > now + 1e4) {
561
+ return this.gameplayAttempt;
562
+ }
563
+ if (!this.gameplayAttemptPromise) {
564
+ this.gameplayAttemptPromise = this.raw.startGameplayAttempt(auth, gameId, { attemptId }).then((dto) => {
565
+ this.gameplayAttempt = {
566
+ gameId,
567
+ attemptId: dto.attemptId,
568
+ attemptToken: dto.attemptToken,
569
+ expiresAtMs: new Date(dto.expiresAt).getTime()
570
+ };
571
+ return this.gameplayAttempt;
572
+ }).finally(() => {
573
+ this.gameplayAttemptPromise = void 0;
574
+ });
575
+ }
576
+ return this.gameplayAttemptPromise;
577
+ }
455
578
  async getGameDetail(gameId) {
456
579
  const token = await this.getAccessToken?.();
457
580
  return this.raw.getGameDetail(gameId, token);
458
581
  }
582
+ async resolveStoreId(gameId) {
583
+ const detail = await this.getGameDetail(gameId);
584
+ const storeId = detail?.storeId;
585
+ if (!storeId) throw new MissingStoreError(gameId);
586
+ return storeId;
587
+ }
459
588
  async listRecentPlayed(query) {
460
589
  const token = await this.requireToken();
461
590
  return this.raw.listRecentPlayed(token, query);
@@ -463,45 +592,74 @@ var ApiClient = class {
463
592
  /** Call once when the game view opens, before any play/match tracking. */
464
593
  async trackView(gameId) {
465
594
  const auth = await this.resolveTrackingAuth();
466
- return this.raw.trackView(auth, gameId);
595
+ return this.raw.recordGameView(auth, gameId);
467
596
  }
468
597
  /**
469
598
  * Starts a new play attempt and returns its id. Call when the user
470
- * presses Play; `trackPlay` reuses this id across calls until the next
599
+ * presses Play; `trackPlay`/`trackMatch`/`emit` reuse this id (and the
600
+ * signed `attemptToken` backing it) across calls until the next
471
601
  * `startAttempt()`, auto-starting one on first use if none was started.
472
602
  */
473
603
  startAttempt() {
474
604
  this.currentAttemptId = generateId();
605
+ this.gameplayAttempt = void 0;
475
606
  return this.currentAttemptId;
476
607
  }
477
608
  async trackPlay(gameId, playTimeSeconds) {
478
609
  const auth = await this.resolveTrackingAuth();
479
610
  const attemptId = this.currentAttemptId ?? this.startAttempt();
480
- return this.raw.trackPlay(auth, gameId, attemptId, playTimeSeconds);
611
+ const { attemptToken } = await this.ensureGameplayAttempt(auth, gameId, attemptId);
612
+ return this.raw.completeGameplayAttempt(auth, gameId, { playTimeSeconds, attemptId, attemptToken });
481
613
  }
482
614
  async trackMatch(gameId, body) {
483
615
  const auth = await this.resolveTrackingAuth();
484
- return this.raw.trackMatchEnd(auth, gameId, body);
616
+ const attemptId = this.currentAttemptId ?? this.startAttempt();
617
+ const { attemptToken } = await this.ensureGameplayAttempt(auth, gameId, attemptId);
618
+ return this.raw.completeGameplayMatch(auth, gameId, { ...body, attemptId, attemptToken });
485
619
  }
486
620
  async emit(gameId, actionCode, opts) {
487
621
  const auth = await this.resolveTrackingAuth();
622
+ const attemptId = this.currentAttemptId ?? this.startAttempt();
623
+ const { attemptToken } = await this.ensureGameplayAttempt(auth, gameId, attemptId);
488
624
  const body = {
625
+ attemptId,
626
+ attemptToken,
489
627
  actionCode,
490
628
  value: opts?.value,
491
629
  refId: opts?.refId,
492
630
  payload: opts?.payload
493
631
  };
494
- return this.raw.emitGameEvent(auth, gameId, body);
632
+ return this.raw.recordGameplayAction(auth, gameId, body);
495
633
  }
496
- /** Lists assets purchasable in `gameId`, defined by that game's creator. */
497
- async listGameAssets(gameId) {
498
- const token = await this.getAccessToken?.();
499
- return this.raw.listGameAssets(gameId, token);
634
+ /** Fetches the storefront (sections + offers) for `gameId`, defined by that game's creator. */
635
+ async getStore(gameId) {
636
+ const storeId = await this.resolveStoreId(gameId);
637
+ return this.raw.getStoreByStoreId(storeId);
500
638
  }
501
- /** Spends coin balance to purchase an asset — see PurchaseGameAssetRequest for dedupe/quantity. */
502
- async purchaseGameAsset(gameId, assetCode, opts) {
503
- const token = await this.requireToken();
504
- return this.raw.purchaseGameAsset(token, gameId, assetCode, opts);
639
+ /** Purchase history for the authenticated player in `gameId`'s store. */
640
+ async listMyStorePurchases(gameId) {
641
+ const [token, storeId] = await Promise.all([this.requireToken(), this.resolveStoreId(gameId)]);
642
+ return this.raw.listMyStorePurchasesByStoreId(token, storeId);
643
+ }
644
+ /**
645
+ * Spends coin balance to purchase a store offer — signs a checkout intent
646
+ * first, then redeems it, mirroring the gameplay attempt flow above.
647
+ */
648
+ async purchaseStoreOffer(gameId, offerCode, opts) {
649
+ const [token, storeId] = await Promise.all([this.requireToken(), this.resolveStoreId(gameId)]);
650
+ const intent = await this.raw.createStoreCheckoutIntent(token, {
651
+ storeId,
652
+ offerCode,
653
+ requestId: opts?.requestId,
654
+ quantity: opts?.quantity
655
+ });
656
+ return this.raw.purchaseStoreOffer(token, {
657
+ storeId,
658
+ offerCode,
659
+ requestId: intent.requestId,
660
+ quantity: intent.quantity,
661
+ checkoutToken: intent.checkoutToken
662
+ });
505
663
  }
506
664
  async getMyProfile() {
507
665
  const token = await this.requireToken();
@@ -527,13 +685,13 @@ var ApiClient = class {
527
685
  const token = await this.requireToken();
528
686
  return this.raw.setReferral(token, body);
529
687
  }
530
- async listTasks() {
688
+ async listTasks(query) {
531
689
  const token = await this.requireToken();
532
- return this.raw.listTasks(token);
690
+ return this.raw.listTasks(token, query);
533
691
  }
534
- async listFinishedTasks() {
692
+ async listFinishedTasks(query) {
535
693
  const token = await this.requireToken();
536
- return this.raw.listFinishedTasks(token);
694
+ return this.raw.listFinishedTasks(token, query);
537
695
  }
538
696
  async startTask(body) {
539
697
  const token = await this.requireToken();
@@ -1198,6 +1356,7 @@ exports.LooplaySDKError = LooplaySDKError;
1198
1356
  exports.MemoryAuthStorage = MemoryAuthStorage;
1199
1357
  exports.MissingAuthError = MissingAuthError;
1200
1358
  exports.MissingBaseUrlError = MissingBaseUrlError;
1359
+ exports.MissingStoreError = MissingStoreError;
1201
1360
  exports.NotAuthenticatedError = NotAuthenticatedError;
1202
1361
  exports.NotInitializedError = NotInitializedError;
1203
1362
  exports.ServiceClient = ServiceClient;