@looplay/sdk 0.2.1 → 0.4.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.
@@ -247,107 +247,141 @@ var ServiceClient = class {
247
247
  // Games
248
248
  // ─────────────────────────────────────────────────────────────────────────────
249
249
  async listGames(query) {
250
- return this.http.request("GET", "/games", { query });
250
+ return this.http.request("GET", "/sdk/games", { query });
251
251
  }
252
252
  async getGameDetail(gameId, bearerToken) {
253
- return this.http.request("GET", `/games/${encodeURIComponent(gameId)}`, {
253
+ return this.http.request("GET", `/sdk/games/${encodeURIComponent(gameId)}`, {
254
254
  bearerToken
255
255
  });
256
256
  }
257
257
  async listRecentPlayed(bearerToken, query) {
258
- return this.http.request("GET", "/games/recent-play", { bearerToken, query });
258
+ return this.http.request("GET", "/sdk/games/recent-play", { bearerToken, query });
259
259
  }
260
- async trackPlay(auth, gameId, playTimeSeconds) {
261
- const body = { playTimeSeconds };
262
- await this.http.request("POST", `/games/${encodeURIComponent(gameId)}/play`, {
260
+ async trackView(auth, gameId) {
261
+ await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/view`, {
263
262
  ...auth,
263
+ headers: this.trackingHeaders(auth)
264
+ });
265
+ return true;
266
+ }
267
+ async trackPlay(auth, gameId, attemptId, playTimeSeconds) {
268
+ const body = { playTimeSeconds, attemptId };
269
+ await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/play`, {
270
+ ...auth,
271
+ headers: this.trackingHeaders(auth),
264
272
  body
265
273
  });
266
274
  return true;
267
275
  }
268
276
  async trackMatchEnd(auth, gameId, body) {
269
- await this.http.request("POST", `/games/${encodeURIComponent(gameId)}/end`, {
277
+ await this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/end`, {
270
278
  ...auth,
279
+ headers: this.trackingHeaders(auth),
271
280
  body
272
281
  });
273
282
  return true;
274
283
  }
275
284
  async emitGameEvent(auth, gameId, body) {
276
- return this.http.request("POST", `/games/${encodeURIComponent(gameId)}/emit`, {
285
+ return this.http.request("POST", `/sdk/games/${encodeURIComponent(gameId)}/emit`, {
277
286
  ...auth,
287
+ headers: this.trackingHeaders(auth),
278
288
  body
279
289
  });
280
290
  }
291
+ /**
292
+ * Anonymous runtimes must call this before any tracking call; logged-in
293
+ * runtimes should still call it to get a `trackingSessionId` for unified
294
+ * tracing — see `GameTrackingSessionDto`.
295
+ */
296
+ async issueTrackingSession(anonId) {
297
+ return this.http.request("POST", "/sdk/games/tracking/bootstrap", {
298
+ headers: { "x-looplay-anon-id": anonId }
299
+ });
300
+ }
301
+ trackingHeaders(auth) {
302
+ return {
303
+ "x-game-tracking-session-id": auth.trackingSessionId,
304
+ "x-looplay-anon-token": auth.anonTrackingToken
305
+ };
306
+ }
307
+ // ─────────────────────────────────────────────────────────────────────────────
308
+ // Game assets — spend coin balance on assets defined by the game's
309
+ // creator. Coin balance itself comes from getBalances()/getMyBalance().
310
+ // ─────────────────────────────────────────────────────────────────────────────
311
+ async listGameAssets(gameId, bearerToken) {
312
+ return this.http.request("GET", `/sdk/games/${encodeURIComponent(gameId)}/assets`, {
313
+ bearerToken
314
+ });
315
+ }
316
+ async purchaseGameAsset(bearerToken, gameId, assetCode, body = {}) {
317
+ return this.http.request(
318
+ "POST",
319
+ `/sdk/games/${encodeURIComponent(gameId)}/assets/${encodeURIComponent(assetCode)}/purchase`,
320
+ { bearerToken, body }
321
+ );
322
+ }
281
323
  // ─────────────────────────────────────────────────────────────────────────────
282
324
  // Balance
283
325
  // ─────────────────────────────────────────────────────────────────────────────
284
326
  async getBalances(bearerToken, query) {
285
- return this.http.request("GET", "/balance", { bearerToken, query });
327
+ return this.http.request("GET", "/sdk/balance", { bearerToken, query });
286
328
  }
287
329
  async getBalanceHistory(bearerToken, query) {
288
- return this.http.request("GET", "/balance/history", { bearerToken, query });
330
+ return this.http.request("GET", "/sdk/balance/history", { bearerToken, query });
289
331
  }
290
332
  // ─────────────────────────────────────────────────────────────────────────────
291
333
  // User
292
334
  // ─────────────────────────────────────────────────────────────────────────────
293
335
  async getMyProfile(bearerToken) {
294
- return this.http.request("GET", "/user/profile", { bearerToken });
336
+ return this.http.request("GET", "/sdk/user/profile", { bearerToken });
295
337
  }
296
338
  async getMyBalance(bearerToken) {
297
- return this.http.request("GET", "/user/balance", { bearerToken });
339
+ return this.http.request("GET", "/sdk/user/balance", { bearerToken });
298
340
  }
299
341
  // ─────────────────────────────────────────────────────────────────────────────
300
342
  // Referrals
301
343
  // ─────────────────────────────────────────────────────────────────────────────
302
344
  async listReferrals(bearerToken, query) {
303
- return this.http.request("GET", "/referrals", { bearerToken, query });
345
+ return this.http.request("GET", "/sdk/referrals", { bearerToken, query });
304
346
  }
305
347
  async setReferral(bearerToken, body) {
306
- return this.http.request("POST", "/referrals/set", { bearerToken, body });
348
+ return this.http.request("POST", "/sdk/referrals/set", { bearerToken, body });
307
349
  }
308
350
  // ─────────────────────────────────────────────────────────────────────────────
309
- // Tasks / Quests
351
+ // Tasks
310
352
  // ─────────────────────────────────────────────────────────────────────────────
311
353
  async listTasks(bearerToken) {
312
- return this.http.request("GET", "/task", { bearerToken });
354
+ return this.http.request("GET", "/sdk/task", { bearerToken });
313
355
  }
314
356
  async listFinishedTasks(bearerToken) {
315
- return this.http.request("GET", "/task/finished", { bearerToken });
357
+ return this.http.request("GET", "/sdk/task/finished", { bearerToken });
316
358
  }
317
359
  async startTask(bearerToken, body) {
318
- return this.http.request("POST", "/task/start", { bearerToken, body });
360
+ return this.http.request("POST", "/sdk/task/start", { bearerToken, body });
319
361
  }
320
362
  async claimTask(bearerToken, body) {
321
- return this.http.request("POST", "/task/claim", { bearerToken, body });
322
- }
323
- async getPlatformQuests(bearerToken, query) {
324
- return this.http.request("GET", "/quest/platform", { bearerToken, query });
325
- }
326
- async getCampaignQuests(bearerToken, query) {
327
- return this.http.request("GET", "/quest/campaign", { bearerToken, query });
328
- }
329
- async claimQuest(bearerToken, questProgressId) {
330
- return this.http.request("POST", `/quest/${encodeURIComponent(questProgressId)}/claim`, {
331
- bearerToken
332
- });
333
- }
334
- async claimMilestone(bearerToken, milestoneId, body) {
335
- return this.http.request(
336
- "POST",
337
- `/quest/milestone/${encodeURIComponent(milestoneId)}/claim`,
338
- {
339
- bearerToken,
340
- body
341
- }
342
- );
363
+ return this.http.request("POST", "/sdk/task/claim", { bearerToken, body });
343
364
  }
344
365
  };
345
366
 
346
367
  // src/apps/api-client.ts
368
+ function generateId() {
369
+ if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
370
+ return crypto.randomUUID();
371
+ }
372
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
373
+ const r = Math.random() * 16 | 0;
374
+ const v = c === "x" ? r : r & 3 | 8;
375
+ return v.toString(16);
376
+ });
377
+ }
347
378
  var ApiClient = class {
348
379
  raw;
349
380
  getAccessToken;
350
381
  getAnonymousId;
382
+ trackingSession;
383
+ trackingSessionPromise;
384
+ currentAttemptId;
351
385
  constructor(options) {
352
386
  if (!options.baseUrl) throw new MissingBaseUrlError();
353
387
  this.raw = new ServiceClient({
@@ -375,14 +409,46 @@ var ApiClient = class {
375
409
  /**
376
410
  * Resolves auth for the tracking endpoints: a real bearer token if logged
377
411
  * in, otherwise an anonymous device id. Throws only when neither is
378
- * available — anonymous tracking still requires *some* identity.
412
+ * available — anonymous tracking still requires *some* identity. Also
413
+ * attaches a `trackingSessionId`/`anonTrackingToken` (bootstrapped and
414
+ * cached lazily) — the backend's tracking contract expects these even for
415
+ * logged-in callers, for unified tracing.
379
416
  */
380
417
  async resolveTrackingAuth() {
381
418
  const bearerToken = await this.getAccessToken?.();
382
- if (bearerToken) return { bearerToken };
383
- const anonId = this.getAnonymousId?.();
384
- if (anonId) return { anonId };
385
- throw new NotAuthenticatedError();
419
+ const anonId = bearerToken ? void 0 : this.getAnonymousId?.();
420
+ if (!bearerToken && !anonId) throw new NotAuthenticatedError();
421
+ const session = await this.ensureTrackingSession(anonId);
422
+ return {
423
+ bearerToken,
424
+ anonId,
425
+ trackingSessionId: session?.trackingSessionId,
426
+ anonTrackingToken: session?.anonTrackingToken
427
+ };
428
+ }
429
+ /**
430
+ * Bootstraps (and caches until near expiry) the platform tracking
431
+ * session. Swallows failures — tracking calls still work without it,
432
+ * just without unified session tracing.
433
+ */
434
+ async ensureTrackingSession(anonId) {
435
+ const now = Date.now();
436
+ if (this.trackingSession && this.trackingSession.expiresAtMs > now + 3e4) {
437
+ return this.trackingSession;
438
+ }
439
+ if (!this.trackingSessionPromise) {
440
+ this.trackingSessionPromise = this.raw.issueTrackingSession(anonId).then((dto) => {
441
+ this.trackingSession = {
442
+ trackingSessionId: dto.trackingSessionId,
443
+ anonTrackingToken: dto.trackingToken,
444
+ expiresAtMs: new Date(dto.expiresAt).getTime()
445
+ };
446
+ return this.trackingSession;
447
+ }).catch(() => void 0).finally(() => {
448
+ this.trackingSessionPromise = void 0;
449
+ });
450
+ }
451
+ return this.trackingSessionPromise;
386
452
  }
387
453
  async getGameDetail(gameId) {
388
454
  const token = await this.getAccessToken?.();
@@ -392,9 +458,24 @@ var ApiClient = class {
392
458
  const token = await this.requireToken();
393
459
  return this.raw.listRecentPlayed(token, query);
394
460
  }
461
+ /** Call once when the game view opens, before any play/match tracking. */
462
+ async trackView(gameId) {
463
+ const auth = await this.resolveTrackingAuth();
464
+ return this.raw.trackView(auth, gameId);
465
+ }
466
+ /**
467
+ * Starts a new play attempt and returns its id. Call when the user
468
+ * presses Play; `trackPlay` reuses this id across calls until the next
469
+ * `startAttempt()`, auto-starting one on first use if none was started.
470
+ */
471
+ startAttempt() {
472
+ this.currentAttemptId = generateId();
473
+ return this.currentAttemptId;
474
+ }
395
475
  async trackPlay(gameId, playTimeSeconds) {
396
476
  const auth = await this.resolveTrackingAuth();
397
- return this.raw.trackPlay(auth, gameId, playTimeSeconds);
477
+ const attemptId = this.currentAttemptId ?? this.startAttempt();
478
+ return this.raw.trackPlay(auth, gameId, attemptId, playTimeSeconds);
398
479
  }
399
480
  async trackMatch(gameId, body) {
400
481
  const auth = await this.resolveTrackingAuth();
@@ -410,6 +491,16 @@ var ApiClient = class {
410
491
  };
411
492
  return this.raw.emitGameEvent(auth, gameId, body);
412
493
  }
494
+ /** Lists assets purchasable in `gameId`, defined by that game's creator. */
495
+ async listGameAssets(gameId) {
496
+ const token = await this.getAccessToken?.();
497
+ return this.raw.listGameAssets(gameId, token);
498
+ }
499
+ /** Spends coin balance to purchase an asset — see PurchaseGameAssetRequest for dedupe/quantity. */
500
+ async purchaseGameAsset(gameId, assetCode, opts) {
501
+ const token = await this.requireToken();
502
+ return this.raw.purchaseGameAsset(token, gameId, assetCode, opts);
503
+ }
413
504
  async getMyProfile() {
414
505
  const token = await this.requireToken();
415
506
  return this.raw.getMyProfile(token);
@@ -450,22 +541,6 @@ var ApiClient = class {
450
541
  const token = await this.requireToken();
451
542
  return this.raw.claimTask(token, body);
452
543
  }
453
- async getPlatformQuests(query) {
454
- const token = await this.requireToken();
455
- return this.raw.getPlatformQuests(token, query);
456
- }
457
- async getCampaignQuests(query) {
458
- const token = await this.requireToken();
459
- return this.raw.getCampaignQuests(token, query);
460
- }
461
- async claimQuest(questProgressId) {
462
- const token = await this.requireToken();
463
- return this.raw.claimQuest(token, questProgressId);
464
- }
465
- async claimMilestone(milestoneId, body) {
466
- const token = await this.requireToken();
467
- return this.raw.claimMilestone(token, milestoneId, body);
468
- }
469
544
  };
470
545
 
471
546
  // src/looplay-sdk.ts
@@ -505,6 +580,19 @@ var LooplaySDK = class {
505
580
  if (!this.api) throw new MissingBaseUrlError();
506
581
  return this.api.getMyProfile();
507
582
  }
583
+ /** Call once when the game view opens, before any play/match tracking. */
584
+ async trackView() {
585
+ this.assertInitialized();
586
+ if (!this.api) throw new MissingBaseUrlError();
587
+ if (!this.gameId) throw new NotInitializedError();
588
+ return this.api.trackView(this.gameId);
589
+ }
590
+ /** Starts a new play attempt; call when the user presses Play. Returns the attempt id. */
591
+ startAttempt() {
592
+ this.assertInitialized();
593
+ if (!this.api) throw new MissingBaseUrlError();
594
+ return this.api.startAttempt();
595
+ }
508
596
  async trackPlay(playTimeSeconds) {
509
597
  this.assertInitialized();
510
598
  if (!this.api) throw new MissingBaseUrlError();
@@ -800,6 +888,20 @@ var LooplayIframeAuth = class {
800
888
  });
801
889
  return this.client;
802
890
  }
891
+ /** Call once when the game view opens, before any play/match tracking. */
892
+ trackView() {
893
+ const gameId = this.getGameId();
894
+ if (!this.canTrack() || !gameId) {
895
+ this.debug("trackView skipped; auth is not ready", { hasGameId: Boolean(gameId) });
896
+ return Promise.resolve(false);
897
+ }
898
+ this.debug("trackView dispatched", { gameId });
899
+ return this.getClient().trackView(gameId);
900
+ }
901
+ /** Starts a new play attempt; call when the user presses Play. Returns the attempt id. */
902
+ startAttempt() {
903
+ return this.getClient().startAttempt();
904
+ }
803
905
  trackPlay(playTimeSeconds) {
804
906
  const gameId = this.getGameId();
805
907
  if (!this.canTrack() || !gameId) {