@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 +46 -1
- package/dist/looplay-sdk.cjs.js +324 -76
- package/dist/looplay-sdk.cjs.js.map +1 -1
- package/dist/looplay-sdk.esm.js +324 -76
- 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/apps/LooplaySDK.types.d.ts +35 -0
- package/dist/types/apps/api-client.d.ts +26 -6
- package/dist/types/apps/service-client.d.ts +21 -10
- package/dist/types/iframe/iframe-auth.d.ts +24 -3
- package/dist/types/iframe/types.d.ts +20 -0
- package/dist/types/looplay-sdk.d.ts +4 -0
- package/package.json +1 -1
package/dist/looplay-sdk.esm.js
CHANGED
|
@@ -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
|
|
261
|
-
|
|
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
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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", {
|
|
737
|
-
|
|
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
|
|
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
|
-
|
|
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", {
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
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(
|
|
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(
|
|
902
|
-
|
|
903
|
-
|
|
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 };
|