@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 CHANGED
@@ -1,7 +1,45 @@
1
- # Looplay
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
- ### Rewarded ads via parent window
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
- ### Drop-in `<script>` tag (no build step required)
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.trackPlay(...) / trackMatch(...) / emitGameEvent(...) from your game code
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
+ ```
@@ -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 listGames(query) {
252
- return this.http.request("GET", "/sdk/games", { query });
253
- }
254
- async getGameDetail(gameId, bearerToken) {
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
- async listRecentPlayed(bearerToken, query) {
260
- return this.http.request("GET", "/sdk/games/recent-play", { bearerToken, query });
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 trackPlay(auth, gameId, attemptId, playTimeSeconds) {
270
- const body = { playTimeSeconds, attemptId };
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 trackMatchEnd(auth, gameId, body) {
279
- await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/end`, {
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 emitGameEvent(auth, gameId, body) {
287
- return this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/emit`, {
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 issueTrackingSession(anonId) {
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
- // Game assets — spend coin balance on assets defined by the game's
311
- // creator. Coin balance itself comes from getBalances()/getMyBalance().
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
- async purchaseGameAsset(bearerToken, gameId, assetCode, body = {}) {
319
- return this.http.request(
320
- "POST",
321
- `/sdk/games/${encodeURIComponent(gameId)}/assets/${encodeURIComponent(assetCode)}/purchase`,
322
- { bearerToken, body }
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.issueTrackingSession(anonId).then((dto) => {
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
- async getGameDetail(gameId) {
456
- const token = await this.getAccessToken?.();
457
- return this.raw.getGameDetail(gameId, token);
458
- }
459
- async listRecentPlayed(query) {
460
- const token = await this.requireToken();
461
- return this.raw.listRecentPlayed(token, query);
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
- /** Call once when the game view opens, before any play/match tracking. */
464
- 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) {
465
499
  const auth = await this.resolveTrackingAuth();
466
- return this.raw.trackView(auth, gameId);
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` reuses this id across calls until the next
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(gameId, playTimeSeconds) {
513
+ async trackPlay(gameKey, playTimeSeconds) {
478
514
  const auth = await this.resolveTrackingAuth();
479
515
  const attemptId = this.currentAttemptId ?? this.startAttempt();
480
- return this.raw.trackPlay(auth, gameId, attemptId, playTimeSeconds);
516
+ const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
517
+ return this.raw.completeGameplayAttempt(gameKey, auth, { playTimeSeconds, attemptId, attemptToken });
481
518
  }
482
- async trackMatch(gameId, body) {
519
+ async trackMatch(gameKey, body) {
483
520
  const auth = await this.resolveTrackingAuth();
484
- return this.raw.trackMatchEnd(auth, gameId, body);
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(gameId, actionCode, opts) {
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.emitGameEvent(auth, gameId, body);
537
+ return this.raw.recordGameplayAction(gameKey, auth, body);
495
538
  }
496
- /** Lists assets purchasable in `gameId`, defined by that game's creator. */
497
- async listGameAssets(gameId) {
498
- const token = await this.getAccessToken?.();
499
- return this.raw.listGameAssets(gameId, token);
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
- /** Spends coin balance to purchase an asset see PurchaseGameAssetRequest for dedupe/quantity. */
502
- async purchaseGameAsset(gameId, assetCode, opts) {
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.purchaseGameAsset(token, gameId, assetCode, opts);
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.getGameDetail(params.gameId);
692
+ await this.api.listGameAssets(params.gameId);
639
693
  }
640
694
  };
641
695