@looplay/sdk 0.6.0 → 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 +74 -179
- package/dist/looplay-sdk.cjs.js.map +1 -1
- package/dist/looplay-sdk.esm.js +75 -179
- 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 +13 -23
- package/dist/types/apps/api-client.d.ts +18 -23
- package/dist/types/apps/service-client.d.ts +14 -22
- package/dist/types/errors.d.ts +0 -3
- 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
|
@@ -31,12 +31,6 @@ 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
|
-
};
|
|
40
34
|
|
|
41
35
|
// src/auth/storage.ts
|
|
42
36
|
var MemoryAuthStorage = class {
|
|
@@ -252,54 +246,49 @@ var ServiceClient = class {
|
|
|
252
246
|
return this.http.request("POST", "/account/logout", { body });
|
|
253
247
|
}
|
|
254
248
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
255
|
-
// 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.
|
|
256
256
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
257
|
-
async
|
|
258
|
-
|
|
259
|
-
}
|
|
260
|
-
async getGameDetail(gameId, bearerToken) {
|
|
261
|
-
return this.http.request("GET", `/sdk/games/${encodeURIComponent(gameId)}`, {
|
|
262
|
-
bearerToken
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
async listRecentPlayed(bearerToken, query) {
|
|
266
|
-
return this.http.request("GET", "/sdk/games/recent-play", { bearerToken, query });
|
|
267
|
-
}
|
|
268
|
-
async recordGameView(auth, gameId) {
|
|
269
|
-
await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/views/record`, {
|
|
257
|
+
async recordGameView(gameKey, auth = {}) {
|
|
258
|
+
await this.http.request("POST", "/sdk/games/views/record", {
|
|
270
259
|
...auth,
|
|
271
|
-
headers: this.trackingHeaders(auth)
|
|
260
|
+
headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) }
|
|
272
261
|
});
|
|
273
262
|
return true;
|
|
274
263
|
}
|
|
275
264
|
/** Signs a fresh `attemptToken` — required by `completeGameplayAttempt`/`completeGameplayMatch`/`recordGameplayAction`. */
|
|
276
|
-
async startGameplayAttempt(
|
|
277
|
-
return this.http.request("POST",
|
|
265
|
+
async startGameplayAttempt(gameKey, auth = {}, body = {}) {
|
|
266
|
+
return this.http.request("POST", "/sdk/games/attempts/start", {
|
|
278
267
|
...auth,
|
|
279
|
-
headers: this.trackingHeaders(auth),
|
|
268
|
+
headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
|
|
280
269
|
body
|
|
281
270
|
});
|
|
282
271
|
}
|
|
283
|
-
async completeGameplayAttempt(
|
|
284
|
-
await this.http.request("POST",
|
|
272
|
+
async completeGameplayAttempt(gameKey, auth, body) {
|
|
273
|
+
await this.http.request("POST", "/sdk/games/attempts/complete", {
|
|
285
274
|
...auth,
|
|
286
|
-
headers: this.trackingHeaders(auth),
|
|
275
|
+
headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
|
|
287
276
|
body
|
|
288
277
|
});
|
|
289
278
|
return true;
|
|
290
279
|
}
|
|
291
|
-
async completeGameplayMatch(
|
|
292
|
-
await this.http.request("POST",
|
|
280
|
+
async completeGameplayMatch(gameKey, auth, body) {
|
|
281
|
+
await this.http.request("POST", "/sdk/games/matches/complete", {
|
|
293
282
|
...auth,
|
|
294
|
-
headers: this.trackingHeaders(auth),
|
|
283
|
+
headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
|
|
295
284
|
body
|
|
296
285
|
});
|
|
297
286
|
return true;
|
|
298
287
|
}
|
|
299
|
-
async recordGameplayAction(
|
|
300
|
-
return this.http.request("POST",
|
|
288
|
+
async recordGameplayAction(gameKey, auth, body) {
|
|
289
|
+
return this.http.request("POST", "/sdk/games/actions/record", {
|
|
301
290
|
...auth,
|
|
302
|
-
headers: this.trackingHeaders(auth),
|
|
291
|
+
headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
|
|
303
292
|
body
|
|
304
293
|
});
|
|
305
294
|
}
|
|
@@ -319,104 +308,36 @@ var ServiceClient = class {
|
|
|
319
308
|
"x-looplay-anon-token": auth.anonTrackingToken
|
|
320
309
|
};
|
|
321
310
|
}
|
|
322
|
-
|
|
323
|
-
|
|
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.
|
|
327
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
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`, {
|
|
333
|
-
bearerToken
|
|
334
|
-
});
|
|
335
|
-
}
|
|
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
|
-
});
|
|
349
|
-
}
|
|
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) {
|
|
311
|
+
/** Public storefront (sections + offers) for the integration's game. Read-only. */
|
|
312
|
+
async getStore(gameKey) {
|
|
395
313
|
return this.http.request("GET", "/sdk/games/store/current", {
|
|
396
314
|
headers: { "x-game-key": gameKey }
|
|
397
315
|
});
|
|
398
316
|
}
|
|
399
|
-
|
|
317
|
+
/** Public catalog of purchasable/ownable assets for the game. Read-only. */
|
|
318
|
+
async listGameAssets(gameKey) {
|
|
400
319
|
return this.http.request("GET", "/sdk/games/store/assets", {
|
|
401
320
|
headers: { "x-game-key": gameKey }
|
|
402
321
|
});
|
|
403
322
|
}
|
|
404
|
-
|
|
323
|
+
/** Assets the authenticated player already owns for this game. */
|
|
324
|
+
async listMyGameAssets(bearerToken, gameKey) {
|
|
405
325
|
return this.http.request("GET", "/sdk/games/store/assets/me", {
|
|
406
326
|
bearerToken,
|
|
407
327
|
headers: { "x-game-key": gameKey }
|
|
408
328
|
});
|
|
409
329
|
}
|
|
410
|
-
|
|
330
|
+
/** The authenticated player's past store purchases for this game. */
|
|
331
|
+
async listMyStorePurchases(bearerToken, gameKey) {
|
|
411
332
|
return this.http.request("GET", "/sdk/games/store/purchases/me", {
|
|
412
333
|
bearerToken,
|
|
413
334
|
headers: { "x-game-key": gameKey }
|
|
414
335
|
});
|
|
415
336
|
}
|
|
416
|
-
// No
|
|
417
|
-
//
|
|
418
|
-
// (
|
|
419
|
-
//
|
|
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.
|
|
420
341
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
421
342
|
// Balance
|
|
422
343
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -491,15 +412,10 @@ var ApiClient = class {
|
|
|
491
412
|
this.getAccessToken = options.getAccessToken;
|
|
492
413
|
this.getAnonymousId = options.getAnonymousId;
|
|
493
414
|
}
|
|
494
|
-
/** Expose the underlying route-level client (requires manual bearerToken passing). */
|
|
415
|
+
/** Expose the underlying route-level client (requires manual bearerToken/gameKey passing). */
|
|
495
416
|
unsafeRaw() {
|
|
496
417
|
return this.raw;
|
|
497
418
|
}
|
|
498
|
-
// Public endpoints
|
|
499
|
-
async listGames(query) {
|
|
500
|
-
return this.raw.listGames(query);
|
|
501
|
-
}
|
|
502
|
-
// Authenticated endpoints
|
|
503
419
|
async requireToken() {
|
|
504
420
|
const token = await this.getAccessToken?.();
|
|
505
421
|
if (!token) throw new NotAuthenticatedError();
|
|
@@ -555,15 +471,15 @@ var ApiClient = class {
|
|
|
555
471
|
* swallow failures, unlike `ensureTrackingSession`, since these calls
|
|
556
472
|
* are rejected outright without a valid token.
|
|
557
473
|
*/
|
|
558
|
-
async ensureGameplayAttempt(
|
|
474
|
+
async ensureGameplayAttempt(gameKey, auth, attemptId) {
|
|
559
475
|
const now = Date.now();
|
|
560
|
-
if (this.gameplayAttempt && this.gameplayAttempt.
|
|
476
|
+
if (this.gameplayAttempt && this.gameplayAttempt.gameKey === gameKey && this.gameplayAttempt.attemptId === attemptId && this.gameplayAttempt.expiresAtMs > now + 1e4) {
|
|
561
477
|
return this.gameplayAttempt;
|
|
562
478
|
}
|
|
563
479
|
if (!this.gameplayAttemptPromise) {
|
|
564
|
-
this.gameplayAttemptPromise = this.raw.startGameplayAttempt(
|
|
480
|
+
this.gameplayAttemptPromise = this.raw.startGameplayAttempt(gameKey, auth, { attemptId }).then((dto) => {
|
|
565
481
|
this.gameplayAttempt = {
|
|
566
|
-
|
|
482
|
+
gameKey,
|
|
567
483
|
attemptId: dto.attemptId,
|
|
568
484
|
attemptToken: dto.attemptToken,
|
|
569
485
|
expiresAtMs: new Date(dto.expiresAt).getTime()
|
|
@@ -575,24 +491,13 @@ var ApiClient = class {
|
|
|
575
491
|
}
|
|
576
492
|
return this.gameplayAttemptPromise;
|
|
577
493
|
}
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
async
|
|
583
|
-
const detail = await this.getGameDetail(gameId);
|
|
584
|
-
const storeId = detail?.storeId;
|
|
585
|
-
if (!storeId) throw new MissingStoreError(gameId);
|
|
586
|
-
return storeId;
|
|
587
|
-
}
|
|
588
|
-
async listRecentPlayed(query) {
|
|
589
|
-
const token = await this.requireToken();
|
|
590
|
-
return this.raw.listRecentPlayed(token, query);
|
|
591
|
-
}
|
|
592
|
-
/** Call once when the game view opens, before any play/match tracking. */
|
|
593
|
-
async trackView(gameId) {
|
|
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) {
|
|
594
499
|
const auth = await this.resolveTrackingAuth();
|
|
595
|
-
return this.raw.recordGameView(
|
|
500
|
+
return this.raw.recordGameView(gameKey, auth);
|
|
596
501
|
}
|
|
597
502
|
/**
|
|
598
503
|
* Starts a new play attempt and returns its id. Call when the user
|
|
@@ -605,22 +510,22 @@ var ApiClient = class {
|
|
|
605
510
|
this.gameplayAttempt = void 0;
|
|
606
511
|
return this.currentAttemptId;
|
|
607
512
|
}
|
|
608
|
-
async trackPlay(
|
|
513
|
+
async trackPlay(gameKey, playTimeSeconds) {
|
|
609
514
|
const auth = await this.resolveTrackingAuth();
|
|
610
515
|
const attemptId = this.currentAttemptId ?? this.startAttempt();
|
|
611
|
-
const { attemptToken } = await this.ensureGameplayAttempt(
|
|
612
|
-
return this.raw.completeGameplayAttempt(
|
|
516
|
+
const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
|
|
517
|
+
return this.raw.completeGameplayAttempt(gameKey, auth, { playTimeSeconds, attemptId, attemptToken });
|
|
613
518
|
}
|
|
614
|
-
async trackMatch(
|
|
519
|
+
async trackMatch(gameKey, body) {
|
|
615
520
|
const auth = await this.resolveTrackingAuth();
|
|
616
521
|
const attemptId = this.currentAttemptId ?? this.startAttempt();
|
|
617
|
-
const { attemptToken } = await this.ensureGameplayAttempt(
|
|
618
|
-
return this.raw.completeGameplayMatch(
|
|
522
|
+
const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
|
|
523
|
+
return this.raw.completeGameplayMatch(gameKey, auth, { ...body, attemptId, attemptToken });
|
|
619
524
|
}
|
|
620
|
-
async emit(
|
|
525
|
+
async emit(gameKey, actionCode, opts) {
|
|
621
526
|
const auth = await this.resolveTrackingAuth();
|
|
622
527
|
const attemptId = this.currentAttemptId ?? this.startAttempt();
|
|
623
|
-
const { attemptToken } = await this.ensureGameplayAttempt(
|
|
528
|
+
const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
|
|
624
529
|
const body = {
|
|
625
530
|
attemptId,
|
|
626
531
|
attemptToken,
|
|
@@ -629,38 +534,30 @@ var ApiClient = class {
|
|
|
629
534
|
refId: opts?.refId,
|
|
630
535
|
payload: opts?.payload
|
|
631
536
|
};
|
|
632
|
-
return this.raw.recordGameplayAction(
|
|
537
|
+
return this.raw.recordGameplayAction(gameKey, auth, body);
|
|
633
538
|
}
|
|
634
|
-
/** Fetches the storefront (sections + offers) for
|
|
635
|
-
async getStore(
|
|
636
|
-
|
|
637
|
-
return this.raw.getStoreByStoreId(storeId);
|
|
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);
|
|
638
542
|
}
|
|
639
|
-
/**
|
|
640
|
-
async
|
|
641
|
-
|
|
642
|
-
return this.raw.listMyStorePurchasesByStoreId(token, storeId);
|
|
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);
|
|
643
546
|
}
|
|
644
|
-
/**
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
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
|
-
});
|
|
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);
|
|
551
|
+
}
|
|
552
|
+
/** Purchase history for the authenticated player in the game's store. */
|
|
553
|
+
async listMyStorePurchases(gameKey) {
|
|
554
|
+
const token = await this.requireToken();
|
|
555
|
+
return this.raw.listMyStorePurchases(token, gameKey);
|
|
663
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.
|
|
664
561
|
async getMyProfile() {
|
|
665
562
|
const token = await this.requireToken();
|
|
666
563
|
return this.raw.getMyProfile(token);
|
|
@@ -791,9 +688,8 @@ var LooplaySDK = class {
|
|
|
791
688
|
}
|
|
792
689
|
async verifyGameId(params) {
|
|
793
690
|
if ((params.verifyMode ?? "none") === "none") return;
|
|
794
|
-
if (!this.auth) throw new MissingAuthError();
|
|
795
691
|
if (!this.api) throw new MissingBaseUrlError();
|
|
796
|
-
await this.api.
|
|
692
|
+
await this.api.listGameAssets(params.gameId);
|
|
797
693
|
}
|
|
798
694
|
};
|
|
799
695
|
|
|
@@ -1356,7 +1252,6 @@ exports.LooplaySDKError = LooplaySDKError;
|
|
|
1356
1252
|
exports.MemoryAuthStorage = MemoryAuthStorage;
|
|
1357
1253
|
exports.MissingAuthError = MissingAuthError;
|
|
1358
1254
|
exports.MissingBaseUrlError = MissingBaseUrlError;
|
|
1359
|
-
exports.MissingStoreError = MissingStoreError;
|
|
1360
1255
|
exports.NotAuthenticatedError = NotAuthenticatedError;
|
|
1361
1256
|
exports.NotInitializedError = NotInitializedError;
|
|
1362
1257
|
exports.ServiceClient = ServiceClient;
|