@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 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
+ ```
@@ -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 listGames(query) {
258
- return this.http.request("GET", "/sdk/games", { query });
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(auth, gameId, body = {}) {
277
- return this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/attempts/start`, {
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(auth, gameId, body) {
284
- await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/attempts/complete`, {
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(auth, gameId, body) {
292
- await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/matches/complete`, {
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(auth, gameId, body) {
300
- return this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/actions/record`, {
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
- // Game store (JWT tier, keyed by `storeId`) — spend coin balance on offers
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
- async listGameAssetsForGameKey(gameKey) {
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
- async listMyOwnedAssetsForGameKey(bearerToken, gameKey) {
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
- async listMyStorePurchasesForGameKey(bearerToken, gameKey) {
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 public/anonymous purchase route exists for the runtime tier
417
- // purchases move real coin balance and always require the JWT tier
418
- // (createStoreCheckoutIntent/purchaseStoreOffer above) or the
419
- // creator-secret tier, which this SDK intentionally never wires up.
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(auth, gameId, attemptId) {
474
+ async ensureGameplayAttempt(gameKey, auth, attemptId) {
559
475
  const now = Date.now();
560
- if (this.gameplayAttempt && this.gameplayAttempt.gameId === gameId && this.gameplayAttempt.attemptId === attemptId && this.gameplayAttempt.expiresAtMs > now + 1e4) {
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(auth, gameId, { attemptId }).then((dto) => {
480
+ this.gameplayAttemptPromise = this.raw.startGameplayAttempt(gameKey, auth, { attemptId }).then((dto) => {
565
481
  this.gameplayAttempt = {
566
- gameId,
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
- async getGameDetail(gameId) {
579
- const token = await this.getAccessToken?.();
580
- return this.raw.getGameDetail(gameId, token);
581
- }
582
- async resolveStoreId(gameId) {
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(auth, gameId);
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(gameId, playTimeSeconds) {
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(auth, gameId, attemptId);
612
- return this.raw.completeGameplayAttempt(auth, gameId, { playTimeSeconds, attemptId, attemptToken });
516
+ const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
517
+ return this.raw.completeGameplayAttempt(gameKey, auth, { playTimeSeconds, attemptId, attemptToken });
613
518
  }
614
- async trackMatch(gameId, body) {
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(auth, gameId, attemptId);
618
- return this.raw.completeGameplayMatch(auth, gameId, { ...body, attemptId, attemptToken });
522
+ const { attemptToken } = await this.ensureGameplayAttempt(gameKey, auth, attemptId);
523
+ return this.raw.completeGameplayMatch(gameKey, auth, { ...body, attemptId, attemptToken });
619
524
  }
620
- async emit(gameId, actionCode, opts) {
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(auth, gameId, attemptId);
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(auth, gameId, body);
537
+ return this.raw.recordGameplayAction(gameKey, auth, body);
633
538
  }
634
- /** Fetches the storefront (sections + offers) for `gameId`, defined by that game's creator. */
635
- async getStore(gameId) {
636
- const storeId = await this.resolveStoreId(gameId);
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
- /** Purchase history for the authenticated player in `gameId`'s store. */
640
- async listMyStorePurchases(gameId) {
641
- const [token, storeId] = await Promise.all([this.requireToken(), this.resolveStoreId(gameId)]);
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
- * Spends coin balance to purchase a store offer — signs a checkout intent
646
- * first, then redeems it, mirroring the gameplay attempt flow above.
647
- */
648
- async purchaseStoreOffer(gameId, offerCode, opts) {
649
- const [token, storeId] = await Promise.all([this.requireToken(), this.resolveStoreId(gameId)]);
650
- const intent = await this.raw.createStoreCheckoutIntent(token, {
651
- storeId,
652
- offerCode,
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.getGameDetail(params.gameId);
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;