@looplay/sdk 0.4.0 → 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.
@@ -740,6 +740,7 @@ var TelegramAuthProvider = class {
740
740
  // src/iframe/iframe-auth.ts
741
741
  var DEFAULT_ANON_ID_STORAGE_KEY = "looplay:anon-id";
742
742
  var DEFAULT_ANONYMOUS_FALLBACK_TIMEOUT_MS = 4e3;
743
+ var DEFAULT_REQUEST_ADS_TIMEOUT_MS = 5e3;
743
744
  var INITIAL_STATE = {
744
745
  jwt: null,
745
746
  gameId: null,
@@ -789,6 +790,12 @@ var LooplayIframeAuth = class {
789
790
  options;
790
791
  messageListener = null;
791
792
  anonymousFallbackTimer = null;
793
+ pendingAdsRequests = /* @__PURE__ */ new Map();
794
+ requestAdsSequence = 0;
795
+ adsRequestInFlight = false;
796
+ adsRequestsSent = 0;
797
+ lastAdsRequestAt = 0;
798
+ autoAdsActionCount = 0;
792
799
  constructor(options = {}) {
793
800
  this.options = options;
794
801
  }
@@ -823,8 +830,57 @@ var LooplayIframeAuth = class {
823
830
  this.debug("standalone mode detected; auth request skipped");
824
831
  return;
825
832
  }
826
- this.debug("requesting auth from parent", { targetOrigin: this.resolveParentOrigin() ?? "*" });
827
- 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
+ });
828
884
  }
829
885
  /** Idempotent; attaches the postMessage listener (if embedded) and arms the anonymous fallback. */
830
886
  init() {
@@ -848,14 +904,13 @@ var LooplayIframeAuth = class {
848
904
  this.messageListener = (event) => {
849
905
  if (event.source !== window.parent) return;
850
906
  if (parentOrigin && event.origin !== parentOrigin) {
851
- this.debug("ignored auth message from unexpected origin", {
907
+ this.debug("ignored message from unexpected origin", {
852
908
  expectedOrigin: parentOrigin,
853
909
  receivedOrigin: event.origin
854
910
  });
855
911
  return;
856
912
  }
857
- if (!isRecord(event.data) || event.data.type !== "LOOPLAY_AUTH") return;
858
- this.handleAuthMessage(event.data, event.origin);
913
+ this.handleIncomingMessage(event.data, event.origin);
859
914
  };
860
915
  window.addEventListener("message", this.messageListener);
861
916
  this.anonymousFallbackTimer = setTimeout(() => {
@@ -872,6 +927,11 @@ var LooplayIframeAuth = class {
872
927
  if (this.anonymousFallbackTimer !== null) {
873
928
  clearTimeout(this.anonymousFallbackTimer);
874
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();
875
935
  this.messageListener = null;
876
936
  this.anonymousFallbackTimer = null;
877
937
  this.initialized = false;
@@ -894,7 +954,9 @@ var LooplayIframeAuth = class {
894
954
  trackView() {
895
955
  const gameId = this.getGameId();
896
956
  if (!this.canTrack() || !gameId) {
897
- this.debug("trackView skipped; auth is not ready", { hasGameId: Boolean(gameId) });
957
+ this.debug("trackView skipped; auth is not ready", {
958
+ hasGameId: Boolean(gameId)
959
+ });
898
960
  return Promise.resolve(false);
899
961
  }
900
962
  this.debug("trackView dispatched", { gameId });
@@ -907,11 +969,17 @@ var LooplayIframeAuth = class {
907
969
  trackPlay(playTimeSeconds) {
908
970
  const gameId = this.getGameId();
909
971
  if (!this.canTrack() || !gameId) {
910
- 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
+ });
911
975
  return Promise.resolve(false);
912
976
  }
913
977
  this.debug("trackPlay dispatched", { gameId, playTimeSeconds });
914
- 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;
915
983
  }
916
984
  trackMatch(matchId, opts) {
917
985
  const gameId = this.getGameId();
@@ -920,12 +988,16 @@ var LooplayIframeAuth = class {
920
988
  return Promise.resolve(false);
921
989
  }
922
990
  this.debug("trackMatch dispatched", { gameId, matchId, ...opts });
923
- return this.getClient().trackMatch(gameId, {
991
+ const result = this.getClient().trackMatch(gameId, {
924
992
  matchId,
925
993
  matchDurationSeconds: opts.durationSeconds,
926
994
  isCompleted: opts.isCompleted,
927
995
  isWin: opts.isWin
928
996
  });
997
+ void result.then((ok) => {
998
+ if (ok) this.recordAutoRequestAdsActivity();
999
+ }).catch(() => void 0);
1000
+ return result;
929
1001
  }
930
1002
  emitGameEvent(actionCode, opts) {
931
1003
  const gameId = this.getGameId();
@@ -938,7 +1010,8 @@ var LooplayIframeAuth = class {
938
1010
  }
939
1011
  /** Idempotent; auto-emits GAME_STARTED exactly once per distinct auth session (bearer or anonymous). */
940
1012
  initLifecycleTracking() {
941
- if (this.lifecycleTrackingInitialized || typeof window === "undefined") return;
1013
+ if (this.lifecycleTrackingInitialized || typeof window === "undefined")
1014
+ return;
942
1015
  this.lifecycleTrackingInitialized = true;
943
1016
  this.debug("lifecycle tracking initialized");
944
1017
  this.subscribe(() => {
@@ -988,7 +1061,9 @@ ${identity}`;
988
1061
  origin,
989
1062
  receivedAt: Date.now()
990
1063
  });
991
- 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
+ );
992
1067
  this.activateAnonymousMode();
993
1068
  return;
994
1069
  }
@@ -1002,10 +1077,13 @@ ${identity}`;
1002
1077
  origin,
1003
1078
  receivedAt: Date.now()
1004
1079
  });
1005
- this.debug("auth message missing jwt; falling back to anonymous tracking", {
1006
- gameId: resolvedGameId,
1007
- apiUrl: resolvedApiUrl
1008
- });
1080
+ this.debug(
1081
+ "auth message missing jwt; falling back to anonymous tracking",
1082
+ {
1083
+ gameId: resolvedGameId,
1084
+ apiUrl: resolvedApiUrl
1085
+ }
1086
+ );
1009
1087
  this.activateAnonymousMode();
1010
1088
  return;
1011
1089
  }
@@ -1019,6 +1097,45 @@ ${identity}`;
1019
1097
  receivedAt: Date.now()
1020
1098
  });
1021
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
+ }
1022
1139
  setState(next) {
1023
1140
  this.state = next;
1024
1141
  this.client = null;
@@ -1043,6 +1160,35 @@ ${identity}`;
1043
1160
  debug(message, details) {
1044
1161
  this.options.onDebug?.(message, details);
1045
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
+ }
1046
1192
  };
1047
1193
 
1048
1194
  exports.ApiClient = ApiClient;