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