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