@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.
@@ -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();
@@ -650,6 +738,7 @@ var TelegramAuthProvider = class {
650
738
  // src/iframe/iframe-auth.ts
651
739
  var DEFAULT_ANON_ID_STORAGE_KEY = "looplay:anon-id";
652
740
  var DEFAULT_ANONYMOUS_FALLBACK_TIMEOUT_MS = 4e3;
741
+ var DEFAULT_REQUEST_ADS_TIMEOUT_MS = 5e3;
653
742
  var INITIAL_STATE = {
654
743
  jwt: null,
655
744
  gameId: null,
@@ -699,6 +788,12 @@ var LooplayIframeAuth = class {
699
788
  options;
700
789
  messageListener = null;
701
790
  anonymousFallbackTimer = null;
791
+ pendingAdsRequests = /* @__PURE__ */ new Map();
792
+ requestAdsSequence = 0;
793
+ adsRequestInFlight = false;
794
+ adsRequestsSent = 0;
795
+ lastAdsRequestAt = 0;
796
+ autoAdsActionCount = 0;
702
797
  constructor(options = {}) {
703
798
  this.options = options;
704
799
  }
@@ -733,8 +828,57 @@ var LooplayIframeAuth = class {
733
828
  this.debug("standalone mode detected; auth request skipped");
734
829
  return;
735
830
  }
736
- this.debug("requesting auth from parent", { targetOrigin: this.resolveParentOrigin() ?? "*" });
737
- window.parent.postMessage({ type: "LOOPLAY_AUTH_REQUEST" }, this.resolveParentOrigin() ?? "*");
831
+ this.debug("requesting auth from parent", {
832
+ targetOrigin: this.resolveParentOrigin() ?? "*"
833
+ });
834
+ window.parent.postMessage(
835
+ { type: "LOOPLAY_AUTH_REQUEST" },
836
+ this.resolveParentOrigin() ?? "*"
837
+ );
838
+ }
839
+ /**
840
+ * Requests the parent host to show a rewarded ad.
841
+ * In a Telegram Mini App setup, the game sends this request from the iframe
842
+ * and the parent page (the Mini App shell) bridges it to Adsgram, then
843
+ * responds with `LOOPLAY_ADS_WATCHED` after the ad finishes.
844
+ */
845
+ requestAds(options = {}) {
846
+ if (typeof window === "undefined" || window.parent === window) {
847
+ this.debug("requestAds skipped; standalone mode detected");
848
+ return Promise.resolve(false);
849
+ }
850
+ if (this.adsRequestInFlight) {
851
+ this.debug("requestAds skipped; another ad request is already in flight");
852
+ return Promise.resolve(false);
853
+ }
854
+ const parentOrigin = this.resolveParentOrigin() ?? "*";
855
+ const requestId = `ads-${Date.now()}-${++this.requestAdsSequence}`;
856
+ const timeoutMs = options.timeoutMs ?? DEFAULT_REQUEST_ADS_TIMEOUT_MS;
857
+ this.adsRequestInFlight = true;
858
+ this.adsRequestsSent += 1;
859
+ this.lastAdsRequestAt = Date.now();
860
+ this.debug("requesting rewarded ad from parent", {
861
+ requestId,
862
+ targetOrigin: parentOrigin,
863
+ gameId: this.getGameId()
864
+ });
865
+ return new Promise((resolve) => {
866
+ const timer = setTimeout(() => {
867
+ this.pendingAdsRequests.delete(requestId);
868
+ this.adsRequestInFlight = false;
869
+ this.debug("requestAds timed out", { requestId, timeoutMs });
870
+ resolve(false);
871
+ }, timeoutMs);
872
+ this.pendingAdsRequests.set(requestId, { resolve, timer });
873
+ window.parent.postMessage(
874
+ {
875
+ type: "LOOPLAY_REQUEST_ADS",
876
+ requestId,
877
+ gameId: this.getGameId()
878
+ },
879
+ parentOrigin
880
+ );
881
+ });
738
882
  }
739
883
  /** Idempotent; attaches the postMessage listener (if embedded) and arms the anonymous fallback. */
740
884
  init() {
@@ -758,14 +902,13 @@ var LooplayIframeAuth = class {
758
902
  this.messageListener = (event) => {
759
903
  if (event.source !== window.parent) return;
760
904
  if (parentOrigin && event.origin !== parentOrigin) {
761
- this.debug("ignored auth message from unexpected origin", {
905
+ this.debug("ignored message from unexpected origin", {
762
906
  expectedOrigin: parentOrigin,
763
907
  receivedOrigin: event.origin
764
908
  });
765
909
  return;
766
910
  }
767
- if (!isRecord(event.data) || event.data.type !== "LOOPLAY_AUTH") return;
768
- this.handleAuthMessage(event.data, event.origin);
911
+ this.handleIncomingMessage(event.data, event.origin);
769
912
  };
770
913
  window.addEventListener("message", this.messageListener);
771
914
  this.anonymousFallbackTimer = setTimeout(() => {
@@ -782,6 +925,11 @@ var LooplayIframeAuth = class {
782
925
  if (this.anonymousFallbackTimer !== null) {
783
926
  clearTimeout(this.anonymousFallbackTimer);
784
927
  }
928
+ for (const pending of this.pendingAdsRequests.values()) {
929
+ if (pending.timer !== null) clearTimeout(pending.timer);
930
+ pending.resolve(false);
931
+ }
932
+ this.pendingAdsRequests.clear();
785
933
  this.messageListener = null;
786
934
  this.anonymousFallbackTimer = null;
787
935
  this.initialized = false;
@@ -800,14 +948,36 @@ var LooplayIframeAuth = class {
800
948
  });
801
949
  return this.client;
802
950
  }
951
+ /** Call once when the game view opens, before any play/match tracking. */
952
+ trackView() {
953
+ const gameId = this.getGameId();
954
+ if (!this.canTrack() || !gameId) {
955
+ this.debug("trackView skipped; auth is not ready", {
956
+ hasGameId: Boolean(gameId)
957
+ });
958
+ return Promise.resolve(false);
959
+ }
960
+ this.debug("trackView dispatched", { gameId });
961
+ return this.getClient().trackView(gameId);
962
+ }
963
+ /** Starts a new play attempt; call when the user presses Play. Returns the attempt id. */
964
+ startAttempt() {
965
+ return this.getClient().startAttempt();
966
+ }
803
967
  trackPlay(playTimeSeconds) {
804
968
  const gameId = this.getGameId();
805
969
  if (!this.canTrack() || !gameId) {
806
- this.debug("trackPlay skipped; auth is not ready", { hasGameId: Boolean(gameId) });
970
+ this.debug("trackPlay skipped; auth is not ready", {
971
+ hasGameId: Boolean(gameId)
972
+ });
807
973
  return Promise.resolve(false);
808
974
  }
809
975
  this.debug("trackPlay dispatched", { gameId, playTimeSeconds });
810
- return this.getClient().trackPlay(gameId, playTimeSeconds);
976
+ const result = this.getClient().trackPlay(gameId, playTimeSeconds);
977
+ void result.then((ok) => {
978
+ if (ok) this.recordAutoRequestAdsActivity();
979
+ }).catch(() => void 0);
980
+ return result;
811
981
  }
812
982
  trackMatch(matchId, opts) {
813
983
  const gameId = this.getGameId();
@@ -816,12 +986,16 @@ var LooplayIframeAuth = class {
816
986
  return Promise.resolve(false);
817
987
  }
818
988
  this.debug("trackMatch dispatched", { gameId, matchId, ...opts });
819
- return this.getClient().trackMatch(gameId, {
989
+ const result = this.getClient().trackMatch(gameId, {
820
990
  matchId,
821
991
  matchDurationSeconds: opts.durationSeconds,
822
992
  isCompleted: opts.isCompleted,
823
993
  isWin: opts.isWin
824
994
  });
995
+ void result.then((ok) => {
996
+ if (ok) this.recordAutoRequestAdsActivity();
997
+ }).catch(() => void 0);
998
+ return result;
825
999
  }
826
1000
  emitGameEvent(actionCode, opts) {
827
1001
  const gameId = this.getGameId();
@@ -834,7 +1008,8 @@ var LooplayIframeAuth = class {
834
1008
  }
835
1009
  /** Idempotent; auto-emits GAME_STARTED exactly once per distinct auth session (bearer or anonymous). */
836
1010
  initLifecycleTracking() {
837
- if (this.lifecycleTrackingInitialized || typeof window === "undefined") return;
1011
+ if (this.lifecycleTrackingInitialized || typeof window === "undefined")
1012
+ return;
838
1013
  this.lifecycleTrackingInitialized = true;
839
1014
  this.debug("lifecycle tracking initialized");
840
1015
  this.subscribe(() => {
@@ -884,7 +1059,9 @@ ${identity}`;
884
1059
  origin,
885
1060
  receivedAt: Date.now()
886
1061
  });
887
- this.debug("auth message missing game identity; falling back to anonymous tracking");
1062
+ this.debug(
1063
+ "auth message missing game identity; falling back to anonymous tracking"
1064
+ );
888
1065
  this.activateAnonymousMode();
889
1066
  return;
890
1067
  }
@@ -898,10 +1075,13 @@ ${identity}`;
898
1075
  origin,
899
1076
  receivedAt: Date.now()
900
1077
  });
901
- this.debug("auth message missing jwt; falling back to anonymous tracking", {
902
- gameId: resolvedGameId,
903
- apiUrl: resolvedApiUrl
904
- });
1078
+ this.debug(
1079
+ "auth message missing jwt; falling back to anonymous tracking",
1080
+ {
1081
+ gameId: resolvedGameId,
1082
+ apiUrl: resolvedApiUrl
1083
+ }
1084
+ );
905
1085
  this.activateAnonymousMode();
906
1086
  return;
907
1087
  }
@@ -915,6 +1095,45 @@ ${identity}`;
915
1095
  receivedAt: Date.now()
916
1096
  });
917
1097
  }
1098
+ handleRequestAdsResult(data) {
1099
+ if (data.type !== "LOOPLAY_ADS_WATCHED") return;
1100
+ const requestId = toOptionalString(data.requestId);
1101
+ if (!requestId) return;
1102
+ const pending = this.pendingAdsRequests.get(requestId);
1103
+ if (!pending) return;
1104
+ this.pendingAdsRequests.delete(requestId);
1105
+ if (pending.timer !== null) clearTimeout(pending.timer);
1106
+ this.adsRequestInFlight = false;
1107
+ this.debug("requestAds watched message received", { requestId });
1108
+ pending.resolve(true);
1109
+ }
1110
+ handleRequestAdsUnavailable(data) {
1111
+ if (data.type !== "LOOPLAY_ADS_UNAVAILABLE") return;
1112
+ const requestId = toOptionalString(data.requestId);
1113
+ if (!requestId) return;
1114
+ const pending = this.pendingAdsRequests.get(requestId);
1115
+ if (!pending) return;
1116
+ this.pendingAdsRequests.delete(requestId);
1117
+ if (pending.timer !== null) clearTimeout(pending.timer);
1118
+ this.adsRequestInFlight = false;
1119
+ this.debug("requestAds unavailable message received", { requestId });
1120
+ pending.resolve(false);
1121
+ }
1122
+ handleIncomingMessage(data, origin) {
1123
+ if (!isRecord(data)) return;
1124
+ if (data.type === "LOOPLAY_AUTH") {
1125
+ this.handleAuthMessage(data, origin);
1126
+ return;
1127
+ }
1128
+ if (data.type === "LOOPLAY_ADS_WATCHED") {
1129
+ this.handleRequestAdsResult(data);
1130
+ return;
1131
+ }
1132
+ if (data.type === "LOOPLAY_ADS_UNAVAILABLE") {
1133
+ this.handleRequestAdsUnavailable(data);
1134
+ return;
1135
+ }
1136
+ }
918
1137
  setState(next) {
919
1138
  this.state = next;
920
1139
  this.client = null;
@@ -939,6 +1158,35 @@ ${identity}`;
939
1158
  debug(message, details) {
940
1159
  this.options.onDebug?.(message, details);
941
1160
  }
1161
+ recordAutoRequestAdsActivity() {
1162
+ this.autoAdsActionCount += 1;
1163
+ const config = this.options.autoRequestAds;
1164
+ if (!config?.enabled) return;
1165
+ if (this.adsRequestInFlight) return;
1166
+ if (!this.canTrack()) return;
1167
+ const now = Date.now();
1168
+ if (config.cooldownMs && now - this.lastAdsRequestAt < config.cooldownMs) {
1169
+ return;
1170
+ }
1171
+ if (config.maxRequestsPerSession && this.adsRequestsSent >= config.maxRequestsPerSession) {
1172
+ return;
1173
+ }
1174
+ const threshold = config.afterActionCount ?? 0;
1175
+ const shouldRequest = threshold > 0 && this.autoAdsActionCount % threshold === 0;
1176
+ if (!shouldRequest) return;
1177
+ const chance = config.chance ?? 1;
1178
+ if (chance <= 0 || Math.random() > chance) {
1179
+ this.debug("auto requestAds skipped by chance", {
1180
+ actionCount: this.autoAdsActionCount,
1181
+ chance
1182
+ });
1183
+ return;
1184
+ }
1185
+ this.debug("auto requestAds triggered", {
1186
+ actionCount: this.autoAdsActionCount
1187
+ });
1188
+ void this.requestAds();
1189
+ }
942
1190
  };
943
1191
 
944
1192
  export { ApiClient, BrowserLocalStorageAuthStorage, HttpClient, HttpError, LooplayAuth, LooplayIframeAuth, LooplaySDK, LooplaySDKError, MemoryAuthStorage, MissingAuthError, MissingBaseUrlError, NotAuthenticatedError, NotInitializedError, ServiceClient, TelegramAuthProvider };