@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/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
|
@@ -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
|
|
263
|
-
|
|
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
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
|
|
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", {
|
|
739
|
-
|
|
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
|
|
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
|
-
|
|
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", {
|
|
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
|
-
|
|
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
|
-
|
|
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")
|
|
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(
|
|
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(
|
|
904
|
-
|
|
905
|
-
|
|
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;
|