@looplay/sdk 0.5.1 → 0.7.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.
- package/README.md +141 -4
- package/dist/looplay-sdk.cjs.js +133 -79
- package/dist/looplay-sdk.cjs.js.map +1 -1
- package/dist/looplay-sdk.esm.js +133 -79
- 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 +121 -18
- package/dist/types/apps/api-client.d.ts +36 -17
- package/dist/types/apps/service-client.d.ts +20 -15
- package/dist/types/index.d.ts +0 -1
- package/dist/types/types.d.ts +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @looplay/sdk
|
|
2
|
+
|
|
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
|
|
5
|
+
authentication.
|
|
2
6
|
|
|
3
7
|
API Reference: https://docs.looplay.gg/build-on-loopplay/looplay-sdk
|
|
4
8
|
|
|
9
|
+
- [Install](#install)
|
|
10
|
+
- [Core concept: one SDK integration = one game](#core-concept-one-sdk-integration--one-game)
|
|
11
|
+
- [Tracking a game — hosted on Looplay or published anywhere else](#tracking-a-game--hosted-on-looplay-or-published-anywhere-else)
|
|
12
|
+
- [`LooplaySDK` — direct integration (your own backend/auth)](#looplaysdk--direct-integration-your-own-backendauth)
|
|
13
|
+
- [Store — read-only browsing](#store--read-only-browsing)
|
|
14
|
+
- [Purchases require your own backend](#purchases-require-your-own-backend)
|
|
15
|
+
- [Rewarded ads via parent window](#rewarded-ads-via-parent-window)
|
|
16
|
+
- [Drop-in `<script>` tag (no build step required)](#drop-in-script-tag-no-build-step-required)
|
|
17
|
+
- [Auth storage tradeoff](#auth-storage-tradeoff)
|
|
18
|
+
- [Errors](#errors)
|
|
19
|
+
|
|
20
|
+
## Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @looplay/sdk
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Core concept: one SDK integration = one game
|
|
27
|
+
|
|
28
|
+
Every tracking and store call is scoped to **one game**, identified by its
|
|
29
|
+
public `appId` (also called `gameKey` — the same value, sent as the
|
|
30
|
+
`x-game-key` header under the hood). There is no multi-game catalog
|
|
31
|
+
browsing in this SDK; register your game with a Looplay creator account
|
|
32
|
+
first and use that `appId` everywhere below.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { LooplayIframeAuth } from '@looplay/sdk';
|
|
36
|
+
|
|
37
|
+
export const looplayAuth = new LooplayIframeAuth({
|
|
38
|
+
appId: 'YOUR_APP_ID', // from your creator dashboard — required
|
|
39
|
+
apiUrl: 'https://api.looplay.gg',
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
5
43
|
## Tracking a game — hosted on Looplay or published anywhere else
|
|
6
44
|
|
|
7
45
|
`LooplayIframeAuth` is the single class for tracking, in two modes that the
|
|
@@ -38,13 +76,88 @@ looplayAuth.initLifecycleTracking(); // auto-emits GAME_STARTED once per session
|
|
|
38
76
|
// Anywhere in the game — identical code whether hosted on Looplay or not:
|
|
39
77
|
looplayAuth.canTrack();
|
|
40
78
|
looplayAuth.subscribe((state) => { /* re-render when auth arrives/clears */ });
|
|
79
|
+
await looplayAuth.trackView();
|
|
80
|
+
looplayAuth.startAttempt(); // call once when the user presses Play
|
|
41
81
|
await looplayAuth.trackPlay(playTimeSeconds);
|
|
42
82
|
await looplayAuth.trackMatch(matchId, { durationSeconds, isWin: true });
|
|
43
83
|
await looplayAuth.emitGameEvent('CUSTOM_ACTION', { value: 1 });
|
|
44
84
|
await looplayAuth.requestAds();
|
|
45
85
|
```
|
|
46
86
|
|
|
47
|
-
|
|
87
|
+
Every play attempt is backed by a short-lived, server-signed token — the SDK
|
|
88
|
+
fetches and caches it automatically the first time `trackPlay`/`trackMatch`/
|
|
89
|
+
`emitGameEvent` needs it after `startAttempt()`. You never see or manage
|
|
90
|
+
this token directly.
|
|
91
|
+
|
|
92
|
+
### Tracking call reference
|
|
93
|
+
|
|
94
|
+
| Call | When |
|
|
95
|
+
| --- | --- |
|
|
96
|
+
| `trackView()` | Once when the game view opens, before any play/match tracking |
|
|
97
|
+
| `startAttempt()` | Once when the user presses Play. Synchronous — returns an id immediately |
|
|
98
|
+
| `trackPlay(playTimeSeconds?)` | Periodically / on pause, to report accumulated play time for the current attempt |
|
|
99
|
+
| `trackMatch(matchId, { durationSeconds, isCompleted?, isWin? })` | Once per round/match end. `matchId` is the idempotency anchor — safe to call twice |
|
|
100
|
+
| `emitGameEvent(actionCode, { value?, refId?, payload? })` | For quest/task progress and custom analytics — e.g. `'LEVEL_UP'`, `'ITEM_COLLECTED'` |
|
|
101
|
+
|
|
102
|
+
## `LooplaySDK` — direct integration (your own backend/auth)
|
|
103
|
+
|
|
104
|
+
If you're not embedding via iframe (e.g. a native/Telegram Mini App
|
|
105
|
+
integration with your own auth flow), use `LooplaySDK` directly:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { LooplaySDK } from '@looplay/sdk';
|
|
109
|
+
|
|
110
|
+
const sdk = new LooplaySDK({
|
|
111
|
+
baseUrl: 'https://api.looplay.gg',
|
|
112
|
+
auth: { provider: telegramAuthProvider }, // any AuthProvider — see auth/types.ts
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
await sdk.init({ gameId: 'YOUR_APP_ID', verifyMode: 'strict' });
|
|
116
|
+
// `gameId` here is the game's public appId (same as `x-game-key`/`appId` above).
|
|
117
|
+
|
|
118
|
+
await sdk.trackView();
|
|
119
|
+
sdk.startAttempt();
|
|
120
|
+
await sdk.trackPlay(playTimeSeconds);
|
|
121
|
+
await sdk.trackMatch(matchId, { durationSeconds, isWin: true });
|
|
122
|
+
await sdk.emit('CUSTOM_ACTION', { value: 1 });
|
|
123
|
+
|
|
124
|
+
const profile = await sdk.getMyProfile();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
`verifyMode: 'strict'` resolves `gameId` against the backend on `init()` and
|
|
128
|
+
throws if the game doesn't exist or isn't live — a lightweight, read-only,
|
|
129
|
+
unauthenticated check. Use `'none'` (default) to skip it.
|
|
130
|
+
|
|
131
|
+
## Store — read-only browsing
|
|
132
|
+
|
|
133
|
+
The SDK exposes read-only access to the game's coin store — storefront,
|
|
134
|
+
public asset catalog, and (when authenticated) the caller's own owned
|
|
135
|
+
assets and purchase history:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const store = await sdk.api!.getStore(gameId); // sections + offers, no auth required
|
|
139
|
+
const assets = await sdk.api!.listGameAssets(gameId); // public asset catalog, no auth required
|
|
140
|
+
const owned = await sdk.api!.listMyGameAssets(gameId); // requires a logged-in user
|
|
141
|
+
const purchases = await sdk.api!.listMyStorePurchases(gameId); // requires a logged-in user
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Purchases require your own backend
|
|
145
|
+
|
|
146
|
+
There is **no purchase call in this SDK** — spending a player's coin
|
|
147
|
+
balance always requires the creator+game *secret* pair
|
|
148
|
+
(`x-creator-key`/`x-game-secret`), which must never be embedded in
|
|
149
|
+
client/browser code. To let players buy a store offer:
|
|
150
|
+
|
|
151
|
+
1. Implement a checkout endpoint on your own backend.
|
|
152
|
+
2. From that backend, call gbs-service directly with your secret pair:
|
|
153
|
+
`POST /sdk/games/store/checkout-intent` then `POST /sdk/games/store/purchase`.
|
|
154
|
+
3. Have the game client call your backend endpoint, not gbs-service
|
|
155
|
+
directly, for the purchase step.
|
|
156
|
+
|
|
157
|
+
Browsing (storefront, catalog, owned assets, purchase history) is safe
|
|
158
|
+
client-side and covered by the read-only calls above.
|
|
159
|
+
|
|
160
|
+
## Rewarded ads via parent window
|
|
48
161
|
|
|
49
162
|
When the game runs inside an iframe, you can ask the parent page to open a
|
|
50
163
|
rewarded ad by sending `LOOPLAY_REQUEST_ADS`. This is the right pattern for a
|
|
@@ -88,7 +201,7 @@ window.addEventListener('message', async (event) => {
|
|
|
88
201
|
});
|
|
89
202
|
```
|
|
90
203
|
|
|
91
|
-
|
|
204
|
+
## Drop-in `<script>` tag (no build step required)
|
|
92
205
|
|
|
93
206
|
Games not built from a Looplay template — plain HTML5, Construct, GameMaker
|
|
94
207
|
exports, a Unity WebGL wrapper page, etc. — can use the browser (IIFE) bundle
|
|
@@ -104,7 +217,8 @@ directly, no bundler needed:
|
|
|
104
217
|
looplayAuth.init();
|
|
105
218
|
looplayAuth.initLifecycleTracking();
|
|
106
219
|
|
|
107
|
-
// call looplayAuth.
|
|
220
|
+
// call looplayAuth.trackView() / startAttempt() / trackPlay(...) /
|
|
221
|
+
// trackMatch(...) / emitGameEvent(...) from your game code
|
|
108
222
|
</script>
|
|
109
223
|
```
|
|
110
224
|
|
|
@@ -131,3 +245,26 @@ but readable by any script on the page (XSS exposure). Prefer it only for
|
|
|
131
245
|
games where that tradeoff is acceptable; for higher-security needs, keep the
|
|
132
246
|
default in-memory storage or implement an `AuthStorage` backed by a more
|
|
133
247
|
restrictive mechanism.
|
|
248
|
+
|
|
249
|
+
## Errors
|
|
250
|
+
|
|
251
|
+
All SDK-thrown errors extend `LooplaySDKError`:
|
|
252
|
+
|
|
253
|
+
| Error | Thrown when |
|
|
254
|
+
| --- | --- |
|
|
255
|
+
| `NotInitializedError` | Calling `LooplaySDK` methods before `init()` |
|
|
256
|
+
| `MissingBaseUrlError` | Constructing without `baseUrl` |
|
|
257
|
+
| `MissingAuthError` | An operation needs `auth` and none was configured |
|
|
258
|
+
| `NotAuthenticatedError` | Calling an authenticated endpoint with no access token and (for tracking) no anonymous id |
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
import { NotAuthenticatedError } from '@looplay/sdk';
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
await sdk.getMyProfile();
|
|
265
|
+
} catch (err) {
|
|
266
|
+
if (err instanceof NotAuthenticatedError) {
|
|
267
|
+
// prompt login
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
package/dist/looplay-sdk.cjs.js
CHANGED
|
@@ -246,47 +246,49 @@ var ServiceClient = class {
|
|
|
246
246
|
return this.http.request("POST", "/account/logout", { body });
|
|
247
247
|
}
|
|
248
248
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
249
|
-
// Games
|
|
249
|
+
// Games — one SDK integration = one game, resolved on every call from the
|
|
250
|
+
// public `x-game-key` header (the game's `appId`). There is no multi-game
|
|
251
|
+
// catalog browsing here (list/search/detail across games) and no `:id`/
|
|
252
|
+
// `storeId` path segments — that's the app-facing surface, out of scope
|
|
253
|
+
// for this per-game SDK. Never use the creator-secret tier
|
|
254
|
+
// (`x-creator-key`/`x-game-secret`) from client/browser code — that would
|
|
255
|
+
// leak the secret to every player.
|
|
250
256
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
251
|
-
async
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
return this.http.request("GET", `/sdk/games/${encodeURIComponent(gameId)}`, {
|
|
256
|
-
bearerToken
|
|
257
|
+
async recordGameView(gameKey, auth = {}) {
|
|
258
|
+
await this.http.request("POST", "/sdk/games/views/record", {
|
|
259
|
+
...auth,
|
|
260
|
+
headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) }
|
|
257
261
|
});
|
|
262
|
+
return true;
|
|
258
263
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
async trackView(auth, gameId) {
|
|
263
|
-
await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/view`, {
|
|
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
267
|
...auth,
|
|
265
|
-
headers: this.trackingHeaders(auth)
|
|
268
|
+
headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
|
|
269
|
+
body
|
|
266
270
|
});
|
|
267
|
-
return true;
|
|
268
271
|
}
|
|
269
|
-
async
|
|
270
|
-
|
|
271
|
-
await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/play`, {
|
|
272
|
+
async completeGameplayAttempt(gameKey, auth, body) {
|
|
273
|
+
await this.http.request("POST", "/sdk/games/attempts/complete", {
|
|
272
274
|
...auth,
|
|
273
|
-
headers: this.trackingHeaders(auth),
|
|
275
|
+
headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
|
|
274
276
|
body
|
|
275
277
|
});
|
|
276
278
|
return true;
|
|
277
279
|
}
|
|
278
|
-
async
|
|
279
|
-
await this.http.request("POST",
|
|
280
|
+
async completeGameplayMatch(gameKey, auth, body) {
|
|
281
|
+
await this.http.request("POST", "/sdk/games/matches/complete", {
|
|
280
282
|
...auth,
|
|
281
|
-
headers: this.trackingHeaders(auth),
|
|
283
|
+
headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
|
|
282
284
|
body
|
|
283
285
|
});
|
|
284
286
|
return true;
|
|
285
287
|
}
|
|
286
|
-
async
|
|
287
|
-
return this.http.request("POST",
|
|
288
|
+
async recordGameplayAction(gameKey, auth, body) {
|
|
289
|
+
return this.http.request("POST", "/sdk/games/actions/record", {
|
|
288
290
|
...auth,
|
|
289
|
-
headers: this.trackingHeaders(auth),
|
|
291
|
+
headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
|
|
290
292
|
body
|
|
291
293
|
});
|
|
292
294
|
}
|
|
@@ -295,7 +297,7 @@ var ServiceClient = class {
|
|
|
295
297
|
* runtimes should still call it to get a `trackingSessionId` for unified
|
|
296
298
|
* tracing — see `GameTrackingSessionDto`.
|
|
297
299
|
*/
|
|
298
|
-
async
|
|
300
|
+
async bootstrapTrackingSession(anonId) {
|
|
299
301
|
return this.http.request("POST", "/sdk/games/tracking/bootstrap", {
|
|
300
302
|
headers: { "x-looplay-anon-id": anonId }
|
|
301
303
|
});
|
|
@@ -306,22 +308,36 @@ var ServiceClient = class {
|
|
|
306
308
|
"x-looplay-anon-token": auth.anonTrackingToken
|
|
307
309
|
};
|
|
308
310
|
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
async listGameAssets(gameId, bearerToken) {
|
|
314
|
-
return this.http.request("GET", `/sdk/games/${encodeURIComponent(gameId)}/assets`, {
|
|
315
|
-
bearerToken
|
|
311
|
+
/** Public storefront (sections + offers) for the integration's game. Read-only. */
|
|
312
|
+
async getStore(gameKey) {
|
|
313
|
+
return this.http.request("GET", "/sdk/games/store/current", {
|
|
314
|
+
headers: { "x-game-key": gameKey }
|
|
316
315
|
});
|
|
317
316
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
);
|
|
317
|
+
/** Public catalog of purchasable/ownable assets for the game. Read-only. */
|
|
318
|
+
async listGameAssets(gameKey) {
|
|
319
|
+
return this.http.request("GET", "/sdk/games/store/assets", {
|
|
320
|
+
headers: { "x-game-key": gameKey }
|
|
321
|
+
});
|
|
324
322
|
}
|
|
323
|
+
/** Assets the authenticated player already owns for this game. */
|
|
324
|
+
async listMyGameAssets(bearerToken, gameKey) {
|
|
325
|
+
return this.http.request("GET", "/sdk/games/store/assets/me", {
|
|
326
|
+
bearerToken,
|
|
327
|
+
headers: { "x-game-key": gameKey }
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
/** The authenticated player's past store purchases for this game. */
|
|
331
|
+
async listMyStorePurchases(bearerToken, gameKey) {
|
|
332
|
+
return this.http.request("GET", "/sdk/games/store/purchases/me", {
|
|
333
|
+
bearerToken,
|
|
334
|
+
headers: { "x-game-key": gameKey }
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
// No purchase route is reachable from client/browser code — spending coin
|
|
338
|
+
// balance always requires the creator+game secret pair
|
|
339
|
+
// (`x-creator-key`/`x-game-secret`), called from the game's own backend.
|
|
340
|
+
// This SDK intentionally never wires that tier up.
|
|
325
341
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
326
342
|
// Balance
|
|
327
343
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -352,11 +368,11 @@ var ServiceClient = class {
|
|
|
352
368
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
353
369
|
// Tasks
|
|
354
370
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
355
|
-
async listTasks(bearerToken) {
|
|
356
|
-
return this.http.request("GET", "/sdk/task", { bearerToken });
|
|
371
|
+
async listTasks(bearerToken, query) {
|
|
372
|
+
return this.http.request("GET", "/sdk/task", { bearerToken, query: { ...query } });
|
|
357
373
|
}
|
|
358
|
-
async listFinishedTasks(bearerToken) {
|
|
359
|
-
return this.http.request("GET", "/sdk/task/finished", { bearerToken });
|
|
374
|
+
async listFinishedTasks(bearerToken, query) {
|
|
375
|
+
return this.http.request("GET", "/sdk/task/finished", { bearerToken, query: { ...query } });
|
|
360
376
|
}
|
|
361
377
|
async startTask(bearerToken, body) {
|
|
362
378
|
return this.http.request("POST", "/sdk/task/start", { bearerToken, body });
|
|
@@ -384,6 +400,8 @@ var ApiClient = class {
|
|
|
384
400
|
trackingSession;
|
|
385
401
|
trackingSessionPromise;
|
|
386
402
|
currentAttemptId;
|
|
403
|
+
gameplayAttempt;
|
|
404
|
+
gameplayAttemptPromise;
|
|
387
405
|
constructor(options) {
|
|
388
406
|
if (!options.baseUrl) throw new MissingBaseUrlError();
|
|
389
407
|
this.raw = new ServiceClient({
|
|
@@ -394,15 +412,10 @@ var ApiClient = class {
|
|
|
394
412
|
this.getAccessToken = options.getAccessToken;
|
|
395
413
|
this.getAnonymousId = options.getAnonymousId;
|
|
396
414
|
}
|
|
397
|
-
/** Expose the underlying route-level client (requires manual bearerToken passing). */
|
|
415
|
+
/** Expose the underlying route-level client (requires manual bearerToken/gameKey passing). */
|
|
398
416
|
unsafeRaw() {
|
|
399
417
|
return this.raw;
|
|
400
418
|
}
|
|
401
|
-
// Public endpoints
|
|
402
|
-
async listGames(query) {
|
|
403
|
-
return this.raw.listGames(query);
|
|
404
|
-
}
|
|
405
|
-
// Authenticated endpoints
|
|
406
419
|
async requireToken() {
|
|
407
420
|
const token = await this.getAccessToken?.();
|
|
408
421
|
if (!token) throw new NotAuthenticatedError();
|
|
@@ -439,7 +452,7 @@ var ApiClient = class {
|
|
|
439
452
|
return this.trackingSession;
|
|
440
453
|
}
|
|
441
454
|
if (!this.trackingSessionPromise) {
|
|
442
|
-
this.trackingSessionPromise = this.raw.
|
|
455
|
+
this.trackingSessionPromise = this.raw.bootstrapTrackingSession(anonId).then((dto) => {
|
|
443
456
|
this.trackingSession = {
|
|
444
457
|
trackingSessionId: dto.trackingSessionId,
|
|
445
458
|
anonTrackingToken: dto.trackingToken,
|
|
@@ -452,57 +465,99 @@ var ApiClient = class {
|
|
|
452
465
|
}
|
|
453
466
|
return this.trackingSessionPromise;
|
|
454
467
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Signs (and caches until near expiry) the `attemptToken` required by
|
|
470
|
+
* `trackPlay`/`trackMatch`/`emit` for the current attempt — does not
|
|
471
|
+
* swallow failures, unlike `ensureTrackingSession`, since these calls
|
|
472
|
+
* are rejected outright without a valid token.
|
|
473
|
+
*/
|
|
474
|
+
async ensureGameplayAttempt(gameKey, auth, attemptId) {
|
|
475
|
+
const now = Date.now();
|
|
476
|
+
if (this.gameplayAttempt && this.gameplayAttempt.gameKey === gameKey && this.gameplayAttempt.attemptId === attemptId && this.gameplayAttempt.expiresAtMs > now + 1e4) {
|
|
477
|
+
return this.gameplayAttempt;
|
|
478
|
+
}
|
|
479
|
+
if (!this.gameplayAttemptPromise) {
|
|
480
|
+
this.gameplayAttemptPromise = this.raw.startGameplayAttempt(gameKey, auth, { attemptId }).then((dto) => {
|
|
481
|
+
this.gameplayAttempt = {
|
|
482
|
+
gameKey,
|
|
483
|
+
attemptId: dto.attemptId,
|
|
484
|
+
attemptToken: dto.attemptToken,
|
|
485
|
+
expiresAtMs: new Date(dto.expiresAt).getTime()
|
|
486
|
+
};
|
|
487
|
+
return this.gameplayAttempt;
|
|
488
|
+
}).finally(() => {
|
|
489
|
+
this.gameplayAttemptPromise = void 0;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
return this.gameplayAttemptPromise;
|
|
462
493
|
}
|
|
463
|
-
/**
|
|
464
|
-
|
|
494
|
+
/**
|
|
495
|
+
* Call once when the game view opens, before any play/match tracking.
|
|
496
|
+
* `gameKey` is the game's public `appId`.
|
|
497
|
+
*/
|
|
498
|
+
async trackView(gameKey) {
|
|
465
499
|
const auth = await this.resolveTrackingAuth();
|
|
466
|
-
return this.raw.
|
|
500
|
+
return this.raw.recordGameView(gameKey, auth);
|
|
467
501
|
}
|
|
468
502
|
/**
|
|
469
503
|
* Starts a new play attempt and returns its id. Call when the user
|
|
470
|
-
* presses Play; `trackPlay`
|
|
504
|
+
* presses Play; `trackPlay`/`trackMatch`/`emit` reuse this id (and the
|
|
505
|
+
* signed `attemptToken` backing it) across calls until the next
|
|
471
506
|
* `startAttempt()`, auto-starting one on first use if none was started.
|
|
472
507
|
*/
|
|
473
508
|
startAttempt() {
|
|
474
509
|
this.currentAttemptId = generateId();
|
|
510
|
+
this.gameplayAttempt = void 0;
|
|
475
511
|
return this.currentAttemptId;
|
|
476
512
|
}
|
|
477
|
-
async trackPlay(
|
|
513
|
+
async trackPlay(gameKey, playTimeSeconds) {
|
|
478
514
|
const auth = await this.resolveTrackingAuth();
|
|
479
515
|
const attemptId = this.currentAttemptId ?? this.startAttempt();
|
|
480
|
-
|
|
516
|
+
const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
|
|
517
|
+
return this.raw.completeGameplayAttempt(gameKey, auth, { playTimeSeconds, attemptId, attemptToken });
|
|
481
518
|
}
|
|
482
|
-
async trackMatch(
|
|
519
|
+
async trackMatch(gameKey, body) {
|
|
483
520
|
const auth = await this.resolveTrackingAuth();
|
|
484
|
-
|
|
521
|
+
const attemptId = this.currentAttemptId ?? this.startAttempt();
|
|
522
|
+
const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
|
|
523
|
+
return this.raw.completeGameplayMatch(gameKey, auth, { ...body, attemptId, attemptToken });
|
|
485
524
|
}
|
|
486
|
-
async emit(
|
|
525
|
+
async emit(gameKey, actionCode, opts) {
|
|
487
526
|
const auth = await this.resolveTrackingAuth();
|
|
527
|
+
const attemptId = this.currentAttemptId ?? this.startAttempt();
|
|
528
|
+
const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
|
|
488
529
|
const body = {
|
|
530
|
+
attemptId,
|
|
531
|
+
attemptToken,
|
|
489
532
|
actionCode,
|
|
490
533
|
value: opts?.value,
|
|
491
534
|
refId: opts?.refId,
|
|
492
535
|
payload: opts?.payload
|
|
493
536
|
};
|
|
494
|
-
return this.raw.
|
|
537
|
+
return this.raw.recordGameplayAction(gameKey, auth, body);
|
|
495
538
|
}
|
|
496
|
-
/**
|
|
497
|
-
async
|
|
498
|
-
|
|
499
|
-
|
|
539
|
+
/** Fetches the storefront (sections + offers) for the integration's game. Read-only, no auth required. */
|
|
540
|
+
async getStore(gameKey) {
|
|
541
|
+
return this.raw.getStore(gameKey);
|
|
542
|
+
}
|
|
543
|
+
/** Public catalog of purchasable/ownable assets for the game. Read-only, no auth required. */
|
|
544
|
+
async listGameAssets(gameKey) {
|
|
545
|
+
return this.raw.listGameAssets(gameKey);
|
|
546
|
+
}
|
|
547
|
+
/** Assets the authenticated player already owns for this game. */
|
|
548
|
+
async listMyGameAssets(gameKey) {
|
|
549
|
+
const token = await this.requireToken();
|
|
550
|
+
return this.raw.listMyGameAssets(token, gameKey);
|
|
500
551
|
}
|
|
501
|
-
/**
|
|
502
|
-
async
|
|
552
|
+
/** Purchase history for the authenticated player in the game's store. */
|
|
553
|
+
async listMyStorePurchases(gameKey) {
|
|
503
554
|
const token = await this.requireToken();
|
|
504
|
-
return this.raw.
|
|
555
|
+
return this.raw.listMyStorePurchases(token, gameKey);
|
|
505
556
|
}
|
|
557
|
+
// There is no purchaseStoreOffer() here by design — spending coin balance
|
|
558
|
+
// requires the creator+game secret pair, which must be called from the
|
|
559
|
+
// game's own backend, never from this client-side SDK. Implement a
|
|
560
|
+
// checkout endpoint on your backend that calls gbs-service directly.
|
|
506
561
|
async getMyProfile() {
|
|
507
562
|
const token = await this.requireToken();
|
|
508
563
|
return this.raw.getMyProfile(token);
|
|
@@ -527,13 +582,13 @@ var ApiClient = class {
|
|
|
527
582
|
const token = await this.requireToken();
|
|
528
583
|
return this.raw.setReferral(token, body);
|
|
529
584
|
}
|
|
530
|
-
async listTasks() {
|
|
585
|
+
async listTasks(query) {
|
|
531
586
|
const token = await this.requireToken();
|
|
532
|
-
return this.raw.listTasks(token);
|
|
587
|
+
return this.raw.listTasks(token, query);
|
|
533
588
|
}
|
|
534
|
-
async listFinishedTasks() {
|
|
589
|
+
async listFinishedTasks(query) {
|
|
535
590
|
const token = await this.requireToken();
|
|
536
|
-
return this.raw.listFinishedTasks(token);
|
|
591
|
+
return this.raw.listFinishedTasks(token, query);
|
|
537
592
|
}
|
|
538
593
|
async startTask(body) {
|
|
539
594
|
const token = await this.requireToken();
|
|
@@ -633,9 +688,8 @@ var LooplaySDK = class {
|
|
|
633
688
|
}
|
|
634
689
|
async verifyGameId(params) {
|
|
635
690
|
if ((params.verifyMode ?? "none") === "none") return;
|
|
636
|
-
if (!this.auth) throw new MissingAuthError();
|
|
637
691
|
if (!this.api) throw new MissingBaseUrlError();
|
|
638
|
-
await this.api.
|
|
692
|
+
await this.api.listGameAssets(params.gameId);
|
|
639
693
|
}
|
|
640
694
|
};
|
|
641
695
|
|