@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 +9 -9
- package/dist/looplay-sdk.cjs.js +64 -94
- package/dist/looplay-sdk.cjs.js.map +1 -1
- package/dist/looplay-sdk.esm.js +64 -94
- package/dist/looplay-sdk.esm.js.map +1 -1
- package/dist/looplay-sdk.min.js +2 -2
- package/dist/looplay-sdk.min.js.map +1 -1
- package/dist/types/apps/LooplaySDK.types.d.ts +18 -31
- package/dist/types/apps/api-client.d.ts +13 -18
- package/dist/types/apps/service-client.d.ts +10 -11
- package/dist/types/iframe/iframe-auth.d.ts +2 -2
- package/dist/types/looplay-sdk.d.ts +3 -2
- package/package.json +1 -1
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
|
-
|
|
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.
|
|
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
|
|
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 `
|
|
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
|
-
| `
|
|
122
|
-
| `trackPlay(playTimeSeconds?)` | Periodically / on pause, to report accumulated play time for the current
|
|
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.
|
|
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-
|
|
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() /
|
|
317
|
+
// call looplayAuth.trackView() / startPlaySession() / trackPlay(...) /
|
|
318
318
|
// trackMatch(...) / emitGameEvent(...) from your game code
|
|
319
319
|
</script>
|
|
320
320
|
```
|
package/dist/looplay-sdk.cjs.js
CHANGED
|
@@ -261,16 +261,16 @@ var ServiceClient = class {
|
|
|
261
261
|
});
|
|
262
262
|
return true;
|
|
263
263
|
}
|
|
264
|
-
/** Signs a fresh `
|
|
265
|
-
async
|
|
266
|
-
return this.http.request("POST", "/sdk/games/
|
|
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
|
|
273
|
-
await this.http.request("POST", "/sdk/games/
|
|
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
|
-
// `
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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 `
|
|
529
|
-
* `trackPlay`/`trackMatch`/`emit` for the current
|
|
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
|
|
522
|
+
async ensurePlaySession(gameKey, auth, playSessionId) {
|
|
534
523
|
const now = Date.now();
|
|
535
|
-
if (this.
|
|
536
|
-
return this.
|
|
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.
|
|
539
|
-
this.
|
|
540
|
-
this.
|
|
527
|
+
if (!this.playSessionPromise) {
|
|
528
|
+
this.playSessionPromise = this.raw.startPlaySession(gameKey, auth, { playSessionId }).then((dto) => {
|
|
529
|
+
this.playSession = {
|
|
541
530
|
gameKey,
|
|
542
|
-
|
|
543
|
-
|
|
531
|
+
playSessionId: dto.playSessionId,
|
|
532
|
+
playSessionToken: dto.playSessionToken,
|
|
544
533
|
expiresAtMs: new Date(dto.expiresAt).getTime()
|
|
545
534
|
};
|
|
546
|
-
return this.
|
|
535
|
+
return this.playSession;
|
|
547
536
|
}).finally(() => {
|
|
548
|
-
this.
|
|
537
|
+
this.playSessionPromise = void 0;
|
|
549
538
|
});
|
|
550
539
|
}
|
|
551
|
-
return this.
|
|
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
|
|
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 `
|
|
565
|
-
* `
|
|
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
|
-
|
|
568
|
-
this.
|
|
569
|
-
this.
|
|
570
|
-
return this.
|
|
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
|
|
575
|
-
const {
|
|
576
|
-
return this.raw.
|
|
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
|
|
581
|
-
const {
|
|
582
|
-
return this.raw.completeGameplayMatch(gameKey, auth, { ...body,
|
|
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
|
|
587
|
-
const {
|
|
575
|
+
const playSessionId = this.currentPlaySessionId ?? this.startPlaySession();
|
|
576
|
+
const { playSessionToken } = await this.ensurePlaySession(gameKey, auth, playSessionId);
|
|
588
577
|
const body = {
|
|
589
|
-
|
|
590
|
-
|
|
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
|
|
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
|
|
724
|
-
|
|
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.
|
|
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
|
|
1096
|
-
|
|
1097
|
-
return this.getClient().
|
|
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();
|