@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 +46 -1
- package/dist/looplay-sdk.cjs.js +161 -15
- package/dist/looplay-sdk.cjs.js.map +1 -1
- package/dist/looplay-sdk.esm.js +161 -15
- package/dist/looplay-sdk.esm.js.map +1 -1
- package/dist/looplay-sdk.min.js +2 -2
- package/dist/looplay-sdk.min.js.map +1 -1
- package/dist/types/iframe/iframe-auth.d.ts +20 -3
- package/dist/types/iframe/types.d.ts +20 -0
- package/package.json +1 -1
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.
|
package/dist/looplay-sdk.cjs.js
CHANGED
|
@@ -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", {
|
|
827
|
-
|
|
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
|
|
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
|
-
|
|
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", {
|
|
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", {
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
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(
|
|
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(
|
|
1006
|
-
|
|
1007
|
-
|
|
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;
|