@looplay/sdk 0.8.3 → 0.8.4

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @looplay/sdk
2
2
 
3
3
  Client SDK for games integrating with Looplay: player tracking (views, play
4
- attempts, matches, custom actions), the game's coin store (read-only), and
4
+ sessions, matches, custom actions), the game's coin store (read-only), and
5
5
  authentication.
6
6
 
7
7
  API Reference: https://docs.looplay.gg/build-on-loopplay/looplay-sdk
@@ -90,7 +90,7 @@ looplayAuth.subscribe((state) => { /* re-render when auth arrives/clears */ });
90
90
  await looplayAuth.trackView();
91
91
 
92
92
  // Call once when the user presses Play
93
- looplayAuth.startAttempt();
93
+ looplayAuth.startPlaySession();
94
94
 
95
95
  // Periodically track play time
96
96
  await looplayAuth.trackPlay(playTimeSeconds);
@@ -108,9 +108,9 @@ if (adWatched) {
108
108
  }
109
109
  ```
110
110
 
111
- Every play attempt is backed by a short-lived, server-signed token — the SDK
111
+ Every play session is backed by a short-lived, server-signed token — the SDK
112
112
  fetches and caches it automatically the first time `trackPlay`/`trackMatch`/
113
- `emitGameEvent` needs it after `startAttempt()`. You never see or manage
113
+ `emitGameEvent` needs it after `startPlaySession()`. You never see or manage
114
114
  this token directly.
115
115
 
116
116
  ### Tracking call reference
@@ -118,8 +118,8 @@ this token directly.
118
118
  | Call | When |
119
119
  | --- | --- |
120
120
  | `trackView()` | Once when the game view opens, before any play/match tracking |
121
- | `startAttempt()` | Once when the user presses Play. Synchronous — returns an id immediately |
122
- | `trackPlay(playTimeSeconds?)` | Periodically / on pause, to report accumulated play time for the current attempt |
121
+ | `startPlaySession()` | Once when the user presses Play. Synchronous — returns an id immediately |
122
+ | `trackPlay(playTimeSeconds?)` | Periodically / on pause, to report accumulated play time for the current play session |
123
123
  | `trackMatch(matchId, { durationSeconds, isCompleted?, isWin? })` | Once per round/match end. `matchId` is the idempotency anchor — safe to call twice |
124
124
  | `emitGameEvent(actionCode, { value?, refId?, payload? })` | For quest/task progress and custom analytics — e.g. `'LEVEL_UP'`, `'ITEM_COLLECTED'` |
125
125
 
@@ -140,7 +140,7 @@ await sdk.init({ gameId: 'YOUR_APP_ID', verifyMode: 'strict' });
140
140
  // `gameId` here is the game's public appId (same as `x-game-key`/`appId` above).
141
141
 
142
142
  await sdk.trackView();
143
- sdk.startAttempt();
143
+ sdk.startPlaySession();
144
144
  await sdk.trackPlay(playTimeSeconds);
145
145
  await sdk.trackMatch(matchId, { durationSeconds, isWin: true });
146
146
  await sdk.emit('CUSTOM_ACTION', { value: 1 });
@@ -170,7 +170,7 @@ const result = await sdk.api!.purchaseStoreOffer(gameId, 'starter_pack', { quant
170
170
  ```
171
171
 
172
172
  `purchaseStoreOffer` signs a short-lived checkout token and redeems it in
173
- one call — the same safety mechanism as the play-attempt token used for
173
+ one call — the same safety mechanism as the play-session token used for
174
174
  tracking, not a static secret. This is enough for creators with no backend
175
175
  of their own.
176
176
 
@@ -314,7 +314,7 @@ directly, no bundler needed:
314
314
  looplayAuth.init();
315
315
  looplayAuth.initLifecycleTracking();
316
316
 
317
- // call looplayAuth.trackView() / startAttempt() / trackPlay(...) /
317
+ // call looplayAuth.trackView() / startPlaySession() / trackPlay(...) /
318
318
  // trackMatch(...) / emitGameEvent(...) from your game code
319
319
  </script>
320
320
  ```
@@ -261,16 +261,16 @@ var ServiceClient = class {
261
261
  });
262
262
  return true;
263
263
  }
264
- /** Signs a fresh `attemptToken` — required by `completeGameplayAttempt`/`completeGameplayMatch`/`recordGameplayAction`. */
265
- async startGameplayAttempt(gameKey, auth = {}, body = {}) {
266
- return this.http.request("POST", "/sdk/games/attempts/start", {
264
+ /** Signs a fresh `playSessionToken` — required by `completePlaySession`/`completeGameplayMatch`/`recordGameplayAction`. */
265
+ async startPlaySession(gameKey, auth = {}, body = {}) {
266
+ return this.http.request("POST", "/sdk/games/play-sessions/start", {
267
267
  ...auth,
268
268
  headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
269
269
  body
270
270
  });
271
271
  }
272
- async completeGameplayAttempt(gameKey, auth, body) {
273
- await this.http.request("POST", "/sdk/games/attempts/complete", {
272
+ async completePlaySession(gameKey, auth, body) {
273
+ await this.http.request("POST", "/sdk/games/play-sessions/complete", {
274
274
  ...auth,
275
275
  headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
276
276
  body
@@ -302,6 +302,16 @@ var ServiceClient = class {
302
302
  headers: { "x-looplay-anon-id": anonId }
303
303
  });
304
304
  }
305
+ /**
306
+ * Lightweight integration heartbeat for review/liveops. This intentionally
307
+ * does not record a view/play metric.
308
+ */
309
+ async detectGameIntegration(gameKey) {
310
+ await this.http.request("POST", "/sdk/games/integration/detect", {
311
+ headers: { "x-game-key": gameKey }
312
+ });
313
+ return true;
314
+ }
305
315
  trackingHeaders(auth) {
306
316
  return {
307
317
  "x-game-tracking-session-id": auth.trackingSessionId,
@@ -339,7 +349,7 @@ var ServiceClient = class {
339
349
  // `x-game-key` + the purchasing player's own bearer token, no backend or
340
350
  // secret pair required. Safe to call directly from client/browser code —
341
351
  // `checkoutToken` (short-lived, server-signed) plays the same role as
342
- // `attemptToken` on the tracking routes. `ApiClient.purchaseStoreOffer`
352
+ // `playSessionToken` on the tracking routes. `ApiClient.purchaseStoreOffer`
343
353
  // wraps this pair for you.
344
354
  // ─────────────────────────────────────────────────────────────────────────────
345
355
  /** Signs a `checkoutToken` for one purchase — required by `purchaseMyStoreOffer`. */
@@ -410,30 +420,6 @@ var ServiceClient = class {
410
420
  async getMyBalance(bearerToken) {
411
421
  return this.http.request("GET", "/sdk/user/balance", { bearerToken });
412
422
  }
413
- // ─────────────────────────────────────────────────────────────────────────────
414
- // Referrals
415
- // ─────────────────────────────────────────────────────────────────────────────
416
- async listReferrals(bearerToken, query) {
417
- return this.http.request("GET", "/sdk/referrals", { bearerToken, query });
418
- }
419
- async setReferral(bearerToken, body) {
420
- return this.http.request("POST", "/sdk/referrals/set", { bearerToken, body });
421
- }
422
- // ─────────────────────────────────────────────────────────────────────────────
423
- // Tasks
424
- // ─────────────────────────────────────────────────────────────────────────────
425
- async listTasks(bearerToken, query) {
426
- return this.http.request("GET", "/sdk/task", { bearerToken, query: { ...query } });
427
- }
428
- async listFinishedTasks(bearerToken, query) {
429
- return this.http.request("GET", "/sdk/task/finished", { bearerToken, query: { ...query } });
430
- }
431
- async startTask(bearerToken, body) {
432
- return this.http.request("POST", "/sdk/task/start", { bearerToken, body });
433
- }
434
- async claimTask(bearerToken, body) {
435
- return this.http.request("POST", "/sdk/task/claim", { bearerToken, body });
436
- }
437
423
  };
438
424
 
439
425
  // src/apps/api-client.ts
@@ -453,9 +439,9 @@ var ApiClient = class {
453
439
  getAnonymousId;
454
440
  trackingSession;
455
441
  trackingSessionPromise;
456
- currentAttemptId;
457
- gameplayAttempt;
458
- gameplayAttemptPromise;
442
+ currentPlaySessionId;
443
+ playSession;
444
+ playSessionPromise;
459
445
  constructor(options) {
460
446
  if (!options.baseUrl) throw new MissingBaseUrlError();
461
447
  this.raw = new ServiceClient({
@@ -470,6 +456,9 @@ var ApiClient = class {
470
456
  unsafeRaw() {
471
457
  return this.raw;
472
458
  }
459
+ async detectIntegration(gameKey) {
460
+ return this.raw.detectGameIntegration(gameKey);
461
+ }
473
462
  // Real-time (balance_change / store_purchase_completed WebSocket events)
474
463
  // deliberately lives outside this class — import `WsClient` from
475
464
  // '@looplay/sdk/realtime' and construct it with the same `getAccessToken`
@@ -525,30 +514,30 @@ var ApiClient = class {
525
514
  return this.trackingSessionPromise;
526
515
  }
527
516
  /**
528
- * Signs (and caches until near expiry) the `attemptToken` required by
529
- * `trackPlay`/`trackMatch`/`emit` for the current attempt — does not
517
+ * Signs (and caches until near expiry) the `playSessionToken` required by
518
+ * `trackPlay`/`trackMatch`/`emit` for the current play session — does not
530
519
  * swallow failures, unlike `ensureTrackingSession`, since these calls
531
520
  * are rejected outright without a valid token.
532
521
  */
533
- async ensureGameplayAttempt(gameKey, auth, attemptId) {
522
+ async ensurePlaySession(gameKey, auth, playSessionId) {
534
523
  const now = Date.now();
535
- if (this.gameplayAttempt && this.gameplayAttempt.gameKey === gameKey && this.gameplayAttempt.attemptId === attemptId && this.gameplayAttempt.expiresAtMs > now + 1e4) {
536
- return this.gameplayAttempt;
524
+ if (this.playSession && this.playSession.gameKey === gameKey && this.playSession.playSessionId === playSessionId && this.playSession.expiresAtMs > now + 1e4) {
525
+ return this.playSession;
537
526
  }
538
- if (!this.gameplayAttemptPromise) {
539
- this.gameplayAttemptPromise = this.raw.startGameplayAttempt(gameKey, auth, { attemptId }).then((dto) => {
540
- this.gameplayAttempt = {
527
+ if (!this.playSessionPromise) {
528
+ this.playSessionPromise = this.raw.startPlaySession(gameKey, auth, { playSessionId }).then((dto) => {
529
+ this.playSession = {
541
530
  gameKey,
542
- attemptId: dto.attemptId,
543
- attemptToken: dto.attemptToken,
531
+ playSessionId: dto.playSessionId,
532
+ playSessionToken: dto.playSessionToken,
544
533
  expiresAtMs: new Date(dto.expiresAt).getTime()
545
534
  };
546
- return this.gameplayAttempt;
535
+ return this.playSession;
547
536
  }).finally(() => {
548
- this.gameplayAttemptPromise = void 0;
537
+ this.playSessionPromise = void 0;
549
538
  });
550
539
  }
551
- return this.gameplayAttemptPromise;
540
+ return this.playSessionPromise;
552
541
  }
553
542
  /**
554
543
  * Call once when the game view opens, before any play/match tracking.
@@ -559,35 +548,35 @@ var ApiClient = class {
559
548
  return this.raw.recordGameView(gameKey, auth);
560
549
  }
561
550
  /**
562
- * Starts a new play attempt and returns its id. Call when the user
551
+ * Starts a new play session and returns its id. Call when the user
563
552
  * presses Play; `trackPlay`/`trackMatch`/`emit` reuse this id (and the
564
- * signed `attemptToken` backing it) across calls until the next
565
- * `startAttempt()`, auto-starting one on first use if none was started.
553
+ * signed `playSessionToken` backing it) across calls until the next
554
+ * `startPlaySession()`, auto-starting one on first use if none was started.
566
555
  */
567
- startAttempt() {
568
- this.currentAttemptId = generateId();
569
- this.gameplayAttempt = void 0;
570
- return this.currentAttemptId;
556
+ startPlaySession() {
557
+ this.currentPlaySessionId = generateId();
558
+ this.playSession = void 0;
559
+ return this.currentPlaySessionId;
571
560
  }
572
561
  async trackPlay(gameKey, playTimeSeconds) {
573
562
  const auth = await this.resolveTrackingAuth();
574
- const attemptId = this.currentAttemptId ?? this.startAttempt();
575
- const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
576
- return this.raw.completeGameplayAttempt(gameKey, auth, { playTimeSeconds, attemptId, attemptToken });
563
+ const playSessionId = this.currentPlaySessionId ?? this.startPlaySession();
564
+ const { playSessionToken } = await this.ensurePlaySession(gameKey, auth, playSessionId);
565
+ return this.raw.completePlaySession(gameKey, auth, { playTimeSeconds, playSessionId, playSessionToken });
577
566
  }
578
567
  async trackMatch(gameKey, body) {
579
568
  const auth = await this.resolveTrackingAuth();
580
- const attemptId = this.currentAttemptId ?? this.startAttempt();
581
- const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
582
- return this.raw.completeGameplayMatch(gameKey, auth, { ...body, attemptId, attemptToken });
569
+ const playSessionId = this.currentPlaySessionId ?? this.startPlaySession();
570
+ const { playSessionToken } = await this.ensurePlaySession(gameKey, auth, playSessionId);
571
+ return this.raw.completeGameplayMatch(gameKey, auth, { ...body, playSessionId, playSessionToken });
583
572
  }
584
573
  async emit(gameKey, actionCode, opts) {
585
574
  const auth = await this.resolveTrackingAuth();
586
- const attemptId = this.currentAttemptId ?? this.startAttempt();
587
- const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
575
+ const playSessionId = this.currentPlaySessionId ?? this.startPlaySession();
576
+ const { playSessionToken } = await this.ensurePlaySession(gameKey, auth, playSessionId);
588
577
  const body = {
589
- attemptId,
590
- attemptToken,
578
+ playSessionId,
579
+ playSessionToken,
591
580
  actionCode,
592
581
  value: opts?.value,
593
582
  refId: opts?.refId,
@@ -615,7 +604,7 @@ var ApiClient = class {
615
604
  }
616
605
  /**
617
606
  * Spends coin balance to purchase a store offer — signs a checkout intent
618
- * first, then redeems it, mirroring the gameplay attempt flow above.
607
+ * first, then redeems it, mirroring the play session flow above.
619
608
  * Self-serve: only the player's own access token is used, no secret pair
620
609
  * required, safe to call directly from the game client. Also see
621
610
  * `onStorePurchase` to react to the result in real time from any tab.
@@ -650,30 +639,6 @@ var ApiClient = class {
650
639
  const token = await this.requireToken();
651
640
  return this.raw.getBalanceHistory(token, query);
652
641
  }
653
- async listReferrals(query) {
654
- const token = await this.requireToken();
655
- return this.raw.listReferrals(token, query);
656
- }
657
- async setReferral(body) {
658
- const token = await this.requireToken();
659
- return this.raw.setReferral(token, body);
660
- }
661
- async listTasks(query) {
662
- const token = await this.requireToken();
663
- return this.raw.listTasks(token, query);
664
- }
665
- async listFinishedTasks(query) {
666
- const token = await this.requireToken();
667
- return this.raw.listFinishedTasks(token, query);
668
- }
669
- async startTask(body) {
670
- const token = await this.requireToken();
671
- return this.raw.startTask(token, body);
672
- }
673
- async claimTask(body) {
674
- const token = await this.requireToken();
675
- return this.raw.claimTask(token, body);
676
- }
677
642
  };
678
643
 
679
644
  // src/looplay-sdk.ts
@@ -699,6 +664,7 @@ var LooplaySDK = class {
699
664
  async init(params) {
700
665
  this.gameId = params.gameId;
701
666
  await this.auth?.init();
667
+ await this.detectIntegration(params);
702
668
  await this.verifyGameId(params);
703
669
  this.initialized = true;
704
670
  }
@@ -720,11 +686,11 @@ var LooplaySDK = class {
720
686
  if (!this.gameId) throw new NotInitializedError();
721
687
  return this.api.trackView(this.gameId);
722
688
  }
723
- /** Starts a new play attempt; call when the user presses Play. Returns the attempt id. */
724
- startAttempt() {
689
+ /** Starts a new play session; call when the user presses Play. Returns the play session id. */
690
+ startPlaySession() {
725
691
  this.assertInitialized();
726
692
  if (!this.api) throw new MissingBaseUrlError();
727
- return this.api.startAttempt();
693
+ return this.api.startPlaySession();
728
694
  }
729
695
  async trackPlay(playTimeSeconds) {
730
696
  this.assertInitialized();
@@ -767,6 +733,10 @@ var LooplaySDK = class {
767
733
  if (!this.api) throw new MissingBaseUrlError();
768
734
  await this.api.listGameAssets(params.gameId);
769
735
  }
736
+ async detectIntegration(params) {
737
+ if (!this.api) return;
738
+ await this.api.detectIntegration(params.gameId);
739
+ }
770
740
  };
771
741
 
772
742
  // src/auth/jwt.ts
@@ -1092,9 +1062,9 @@ var LooplayIframeAuth = class {
1092
1062
  this.debug("trackView dispatched", { gameId });
1093
1063
  return this.getClient().trackView(gameId);
1094
1064
  }
1095
- /** Starts a new play attempt; call when the user presses Play. Returns the attempt id. */
1096
- startAttempt() {
1097
- return this.getClient().startAttempt();
1065
+ /** Starts a new play session; call when the user presses Play. Returns the play session id. */
1066
+ startPlaySession() {
1067
+ return this.getClient().startPlaySession();
1098
1068
  }
1099
1069
  trackPlay(playTimeSeconds) {
1100
1070
  const gameId = this.getGameId();