@looplay/sdk 0.2.1 → 0.5.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
@@ -41,6 +41,51 @@ looplayAuth.subscribe((state) => { /* re-render when auth arrives/clears */ });
41
41
  await looplayAuth.trackPlay(playTimeSeconds);
42
42
  await looplayAuth.trackMatch(matchId, { durationSeconds, isWin: true });
43
43
  await looplayAuth.emitGameEvent('CUSTOM_ACTION', { value: 1 });
44
+ await looplayAuth.requestAds();
45
+ ```
46
+
47
+ ### Rewarded ads via parent window
48
+
49
+ When the game runs inside an iframe, you can ask the parent page to open a
50
+ rewarded ad by sending `LOOPLAY_REQUEST_ADS`. This is the right pattern for a
51
+ Telegram Mini App shell: the game stays inside the iframe, and the parent page
52
+ owns the Adsgram integration. The parent should bridge that message to Adsgram
53
+ (or any other rewarded-ad SDK) and then reply with `LOOPLAY_ADS_WATCHED` using
54
+ the same `requestId` after the ad finishes.
55
+
56
+ If there is no ad available, the parent should reply with
57
+ `LOOPLAY_ADS_UNAVAILABLE` for the same `requestId`. The SDK will resolve the
58
+ request as `false` and continue normally.
59
+
60
+ If the parent never answers, `requestAds()` times out after 5 seconds by
61
+ default and resolves `false`.
62
+
63
+ ```ts
64
+ window.addEventListener('message', async (event) => {
65
+ const data = event.data;
66
+ if (!data || data.type !== 'LOOPLAY_REQUEST_ADS') return;
67
+
68
+ try {
69
+ // Replace this with your Adsgram integration.
70
+ await showRewardedAd(data.gameId);
71
+
72
+ event.source?.postMessage(
73
+ {
74
+ type: 'LOOPLAY_ADS_WATCHED',
75
+ requestId: data.requestId,
76
+ },
77
+ event.origin
78
+ );
79
+ } catch {
80
+ event.source?.postMessage(
81
+ {
82
+ type: 'LOOPLAY_ADS_UNAVAILABLE',
83
+ requestId: data.requestId,
84
+ },
85
+ event.origin
86
+ );
87
+ }
88
+ });
44
89
  ```
45
90
 
46
91
  ### Drop-in `<script>` tag (no build step required)
@@ -85,4 +130,4 @@ session (including the refresh token) in `localStorage`, which is convenient
85
130
  but readable by any script on the page (XSS exposure). Prefer it only for
86
131
  games where that tradeoff is acceptable; for higher-security needs, keep the
87
132
  default in-memory storage or implement an `AuthStorage` backed by a more
88
- restrictive mechanism.
133
+ restrictive mechanism.
@@ -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();
@@ -652,6 +740,7 @@ var TelegramAuthProvider = class {
652
740
  // src/iframe/iframe-auth.ts
653
741
  var DEFAULT_ANON_ID_STORAGE_KEY = "looplay:anon-id";
654
742
  var DEFAULT_ANONYMOUS_FALLBACK_TIMEOUT_MS = 4e3;
743
+ var DEFAULT_REQUEST_ADS_TIMEOUT_MS = 5e3;
655
744
  var INITIAL_STATE = {
656
745
  jwt: null,
657
746
  gameId: null,
@@ -701,6 +790,12 @@ var LooplayIframeAuth = class {
701
790
  options;
702
791
  messageListener = null;
703
792
  anonymousFallbackTimer = null;
793
+ pendingAdsRequests = /* @__PURE__ */ new Map();
794
+ requestAdsSequence = 0;
795
+ adsRequestInFlight = false;
796
+ adsRequestsSent = 0;
797
+ lastAdsRequestAt = 0;
798
+ autoAdsActionCount = 0;
704
799
  constructor(options = {}) {
705
800
  this.options = options;
706
801
  }
@@ -735,8 +830,57 @@ var LooplayIframeAuth = class {
735
830
  this.debug("standalone mode detected; auth request skipped");
736
831
  return;
737
832
  }
738
- this.debug("requesting auth from parent", { targetOrigin: this.resolveParentOrigin() ?? "*" });
739
- window.parent.postMessage({ type: "LOOPLAY_AUTH_REQUEST" }, this.resolveParentOrigin() ?? "*");
833
+ this.debug("requesting auth from parent", {
834
+ targetOrigin: this.resolveParentOrigin() ?? "*"
835
+ });
836
+ window.parent.postMessage(
837
+ { type: "LOOPLAY_AUTH_REQUEST" },
838
+ this.resolveParentOrigin() ?? "*"
839
+ );
840
+ }
841
+ /**
842
+ * Requests the parent host to show a rewarded ad.
843
+ * In a Telegram Mini App setup, the game sends this request from the iframe
844
+ * and the parent page (the Mini App shell) bridges it to Adsgram, then
845
+ * responds with `LOOPLAY_ADS_WATCHED` after the ad finishes.
846
+ */
847
+ requestAds(options = {}) {
848
+ if (typeof window === "undefined" || window.parent === window) {
849
+ this.debug("requestAds skipped; standalone mode detected");
850
+ return Promise.resolve(false);
851
+ }
852
+ if (this.adsRequestInFlight) {
853
+ this.debug("requestAds skipped; another ad request is already in flight");
854
+ return Promise.resolve(false);
855
+ }
856
+ const parentOrigin = this.resolveParentOrigin() ?? "*";
857
+ const requestId = `ads-${Date.now()}-${++this.requestAdsSequence}`;
858
+ const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_ADS_TIMEOUT_MS;
859
+ this.adsRequestInFlight = true;
860
+ this.adsRequestsSent += 1;
861
+ this.lastAdsRequestAt = Date.now();
862
+ this.debug("requesting rewarded ad from parent", {
863
+ requestId,
864
+ targetOrigin: parentOrigin,
865
+ gameId: this.getGameId()
866
+ });
867
+ return new Promise((resolve) => {
868
+ const timer = setTimeout(() => {
869
+ this.pendingAdsRequests.delete(requestId);
870
+ this.adsRequestInFlight = false;
871
+ this.debug("requestAds timed out", { requestId, timeoutMs });
872
+ resolve(false);
873
+ }, timeoutMs);
874
+ this.pendingAdsRequests.set(requestId, { resolve, timer });
875
+ window.parent.postMessage(
876
+ {
877
+ type: "LOOPLAY_REQUEST_ADS",
878
+ requestId,
879
+ gameId: this.getGameId()
880
+ },
881
+ parentOrigin
882
+ );
883
+ });
740
884
  }
741
885
  /** Idempotent; attaches the postMessage listener (if embedded) and arms the anonymous fallback. */
742
886
  init() {
@@ -760,14 +904,13 @@ var LooplayIframeAuth = class {
760
904
  this.messageListener = (event) => {
761
905
  if (event.source !== window.parent) return;
762
906
  if (parentOrigin && event.origin !== parentOrigin) {
763
- this.debug("ignored auth message from unexpected origin", {
907
+ this.debug("ignored message from unexpected origin", {
764
908
  expectedOrigin: parentOrigin,
765
909
  receivedOrigin: event.origin
766
910
  });
767
911
  return;
768
912
  }
769
- if (!isRecord(event.data) || event.data.type !== "LOOPLAY_AUTH") return;
770
- this.handleAuthMessage(event.data, event.origin);
913
+ this.handleIncomingMessage(event.data, event.origin);
771
914
  };
772
915
  window.addEventListener("message", this.messageListener);
773
916
  this.anonymousFallbackTimer = setTimeout(() => {
@@ -784,6 +927,11 @@ var LooplayIframeAuth = class {
784
927
  if (this.anonymousFallbackTimer !== null) {
785
928
  clearTimeout(this.anonymousFallbackTimer);
786
929
  }
930
+ for (const pending of this.pendingAdsRequests.values()) {
931
+ if (pending.timer !== null) clearTimeout(pending.timer);
932
+ pending.resolve(false);
933
+ }
934
+ this.pendingAdsRequests.clear();
787
935
  this.messageListener = null;
788
936
  this.anonymousFallbackTimer = null;
789
937
  this.initialized = false;
@@ -802,14 +950,36 @@ var LooplayIframeAuth = class {
802
950
  });
803
951
  return this.client;
804
952
  }
953
+ /** Call once when the game view opens, before any play/match tracking. */
954
+ trackView() {
955
+ const gameId = this.getGameId();
956
+ if (!this.canTrack() || !gameId) {
957
+ this.debug("trackView skipped; auth is not ready", {
958
+ hasGameId: Boolean(gameId)
959
+ });
960
+ return Promise.resolve(false);
961
+ }
962
+ this.debug("trackView dispatched", { gameId });
963
+ return this.getClient().trackView(gameId);
964
+ }
965
+ /** Starts a new play attempt; call when the user presses Play. Returns the attempt id. */
966
+ startAttempt() {
967
+ return this.getClient().startAttempt();
968
+ }
805
969
  trackPlay(playTimeSeconds) {
806
970
  const gameId = this.getGameId();
807
971
  if (!this.canTrack() || !gameId) {
808
- this.debug("trackPlay skipped; auth is not ready", { hasGameId: Boolean(gameId) });
972
+ this.debug("trackPlay skipped; auth is not ready", {
973
+ hasGameId: Boolean(gameId)
974
+ });
809
975
  return Promise.resolve(false);
810
976
  }
811
977
  this.debug("trackPlay dispatched", { gameId, playTimeSeconds });
812
- return this.getClient().trackPlay(gameId, playTimeSeconds);
978
+ const result = this.getClient().trackPlay(gameId, playTimeSeconds);
979
+ void result.then((ok) => {
980
+ if (ok) this.recordAutoRequestAdsActivity();
981
+ }).catch(() => void 0);
982
+ return result;
813
983
  }
814
984
  trackMatch(matchId, opts) {
815
985
  const gameId = this.getGameId();
@@ -818,12 +988,16 @@ var LooplayIframeAuth = class {
818
988
  return Promise.resolve(false);
819
989
  }
820
990
  this.debug("trackMatch dispatched", { gameId, matchId, ...opts });
821
- return this.getClient().trackMatch(gameId, {
991
+ const result = this.getClient().trackMatch(gameId, {
822
992
  matchId,
823
993
  matchDurationSeconds: opts.durationSeconds,
824
994
  isCompleted: opts.isCompleted,
825
995
  isWin: opts.isWin
826
996
  });
997
+ void result.then((ok) => {
998
+ if (ok) this.recordAutoRequestAdsActivity();
999
+ }).catch(() => void 0);
1000
+ return result;
827
1001
  }
828
1002
  emitGameEvent(actionCode, opts) {
829
1003
  const gameId = this.getGameId();
@@ -836,7 +1010,8 @@ var LooplayIframeAuth = class {
836
1010
  }
837
1011
  /** Idempotent; auto-emits GAME_STARTED exactly once per distinct auth session (bearer or anonymous). */
838
1012
  initLifecycleTracking() {
839
- if (this.lifecycleTrackingInitialized || typeof window === "undefined") return;
1013
+ if (this.lifecycleTrackingInitialized || typeof window === "undefined")
1014
+ return;
840
1015
  this.lifecycleTrackingInitialized = true;
841
1016
  this.debug("lifecycle tracking initialized");
842
1017
  this.subscribe(() => {
@@ -886,7 +1061,9 @@ ${identity}`;
886
1061
  origin,
887
1062
  receivedAt: Date.now()
888
1063
  });
889
- this.debug("auth message missing game identity; falling back to anonymous tracking");
1064
+ this.debug(
1065
+ "auth message missing game identity; falling back to anonymous tracking"
1066
+ );
890
1067
  this.activateAnonymousMode();
891
1068
  return;
892
1069
  }
@@ -900,10 +1077,13 @@ ${identity}`;
900
1077
  origin,
901
1078
  receivedAt: Date.now()
902
1079
  });
903
- this.debug("auth message missing jwt; falling back to anonymous tracking", {
904
- gameId: resolvedGameId,
905
- apiUrl: resolvedApiUrl
906
- });
1080
+ this.debug(
1081
+ "auth message missing jwt; falling back to anonymous tracking",
1082
+ {
1083
+ gameId: resolvedGameId,
1084
+ apiUrl: resolvedApiUrl
1085
+ }
1086
+ );
907
1087
  this.activateAnonymousMode();
908
1088
  return;
909
1089
  }
@@ -917,6 +1097,45 @@ ${identity}`;
917
1097
  receivedAt: Date.now()
918
1098
  });
919
1099
  }
1100
+ handleRequestAdsResult(data) {
1101
+ if (data.type !== "LOOPLAY_ADS_WATCHED") return;
1102
+ const requestId = toOptionalString(data.requestId);
1103
+ if (!requestId) return;
1104
+ const pending = this.pendingAdsRequests.get(requestId);
1105
+ if (!pending) return;
1106
+ this.pendingAdsRequests.delete(requestId);
1107
+ if (pending.timer !== null) clearTimeout(pending.timer);
1108
+ this.adsRequestInFlight = false;
1109
+ this.debug("requestAds watched message received", { requestId });
1110
+ pending.resolve(true);
1111
+ }
1112
+ handleRequestAdsUnavailable(data) {
1113
+ if (data.type !== "LOOPLAY_ADS_UNAVAILABLE") return;
1114
+ const requestId = toOptionalString(data.requestId);
1115
+ if (!requestId) return;
1116
+ const pending = this.pendingAdsRequests.get(requestId);
1117
+ if (!pending) return;
1118
+ this.pendingAdsRequests.delete(requestId);
1119
+ if (pending.timer !== null) clearTimeout(pending.timer);
1120
+ this.adsRequestInFlight = false;
1121
+ this.debug("requestAds unavailable message received", { requestId });
1122
+ pending.resolve(false);
1123
+ }
1124
+ handleIncomingMessage(data, origin) {
1125
+ if (!isRecord(data)) return;
1126
+ if (data.type === "LOOPLAY_AUTH") {
1127
+ this.handleAuthMessage(data, origin);
1128
+ return;
1129
+ }
1130
+ if (data.type === "LOOPLAY_ADS_WATCHED") {
1131
+ this.handleRequestAdsResult(data);
1132
+ return;
1133
+ }
1134
+ if (data.type === "LOOPLAY_ADS_UNAVAILABLE") {
1135
+ this.handleRequestAdsUnavailable(data);
1136
+ return;
1137
+ }
1138
+ }
920
1139
  setState(next) {
921
1140
  this.state = next;
922
1141
  this.client = null;
@@ -941,6 +1160,35 @@ ${identity}`;
941
1160
  debug(message, details) {
942
1161
  this.options.onDebug?.(message, details);
943
1162
  }
1163
+ recordAutoRequestAdsActivity() {
1164
+ this.autoAdsActionCount += 1;
1165
+ const config = this.options.autoRequestAds;
1166
+ if (!config?.enabled) return;
1167
+ if (this.adsRequestInFlight) return;
1168
+ if (!this.canTrack()) return;
1169
+ const now = Date.now();
1170
+ if (config.cooldownMs && now - this.lastAdsRequestAt < config.cooldownMs) {
1171
+ return;
1172
+ }
1173
+ if (config.maxRequestsPerSession && this.adsRequestsSent >= config.maxRequestsPerSession) {
1174
+ return;
1175
+ }
1176
+ const threshold = config.afterActionCount ?? 0;
1177
+ const shouldRequest = threshold > 0 && this.autoAdsActionCount % threshold === 0;
1178
+ if (!shouldRequest) return;
1179
+ const chance = config.chance ?? 1;
1180
+ if (chance <= 0 || Math.random() > chance) {
1181
+ this.debug("auto requestAds skipped by chance", {
1182
+ actionCount: this.autoAdsActionCount,
1183
+ chance
1184
+ });
1185
+ return;
1186
+ }
1187
+ this.debug("auto requestAds triggered", {
1188
+ actionCount: this.autoAdsActionCount
1189
+ });
1190
+ void this.requestAds();
1191
+ }
944
1192
  };
945
1193
 
946
1194
  exports.ApiClient = ApiClient;