@looplay/sdk 0.8.2 → 0.8.4
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 +43 -11
- package/dist/looplay-sdk.cjs.js +64 -94
- package/dist/looplay-sdk.cjs.js.map +1 -1
- package/dist/looplay-sdk.esm.js +64 -94
- 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 +18 -31
- package/dist/types/apps/api-client.d.ts +13 -18
- package/dist/types/apps/service-client.d.ts +10 -11
- package/dist/types/iframe/iframe-auth.d.ts +2 -2
- package/dist/types/looplay-sdk.d.ts +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @looplay/sdk
|
|
2
2
|
|
|
3
3
|
Client SDK for games integrating with Looplay: player tracking (views, play
|
|
4
|
-
|
|
4
|
+
sessions, matches, custom actions), the game's coin store (read-only), and
|
|
5
5
|
authentication.
|
|
6
6
|
|
|
7
7
|
API Reference: https://docs.looplay.gg/build-on-loopplay/looplay-sdk
|
|
@@ -63,29 +63,54 @@ backward compatibility, but you should set one regardless).
|
|
|
63
63
|
```ts
|
|
64
64
|
import { LooplayIframeAuth } from '@looplay/sdk';
|
|
65
65
|
|
|
66
|
+
// 1. Initialize the SDK
|
|
66
67
|
export const looplayAuth = new LooplayIframeAuth({
|
|
67
68
|
appId: 'YOUR_APP_ID', // from your creator dashboard
|
|
68
69
|
apiUrl: 'https://api.looplay.gg', // required for standalone builds; optional inside the iframe
|
|
69
70
|
parentOrigin: import.meta.env.VITE_PARENT_ORIGIN, // restrict the accepted postMessage origin
|
|
71
|
+
|
|
72
|
+
// Optional: Auto-request rewarded ads after match activity
|
|
73
|
+
autoRequestAds: {
|
|
74
|
+
enabled: true,
|
|
75
|
+
afterActionCount: 3, // show ad every 3 actions/matches
|
|
76
|
+
chance: 0.5, // 50% chance to show ad
|
|
77
|
+
cooldownMs: 60000, // min 1 minute between requests
|
|
78
|
+
maxRequestsPerSession: 5,
|
|
79
|
+
}
|
|
70
80
|
});
|
|
71
81
|
|
|
82
|
+
// 2. Start listening for auth and lifecycle events
|
|
72
83
|
looplayAuth.init();
|
|
73
84
|
looplayAuth.initLifecycleTracking(); // auto-emits GAME_STARTED once per session
|
|
74
85
|
|
|
75
|
-
// Anywhere in the game — identical code whether hosted on Looplay or not:
|
|
86
|
+
// 3. Anywhere in the game — identical code whether hosted on Looplay or not:
|
|
76
87
|
looplayAuth.canTrack();
|
|
77
88
|
looplayAuth.subscribe((state) => { /* re-render when auth arrives/clears */ });
|
|
89
|
+
|
|
78
90
|
await looplayAuth.trackView();
|
|
79
|
-
|
|
91
|
+
|
|
92
|
+
// Call once when the user presses Play
|
|
93
|
+
looplayAuth.startPlaySession();
|
|
94
|
+
|
|
95
|
+
// Periodically track play time
|
|
80
96
|
await looplayAuth.trackPlay(playTimeSeconds);
|
|
97
|
+
|
|
98
|
+
// Track when a match/level ends
|
|
81
99
|
await looplayAuth.trackMatch(matchId, { durationSeconds, isWin: true });
|
|
100
|
+
|
|
101
|
+
// Emit custom actions for quests/analytics
|
|
82
102
|
await looplayAuth.emitGameEvent('CUSTOM_ACTION', { value: 1 });
|
|
83
|
-
|
|
103
|
+
|
|
104
|
+
// Manually request rewarded ads (returns true if watched)
|
|
105
|
+
const adWatched = await looplayAuth.requestAds();
|
|
106
|
+
if (adWatched) {
|
|
107
|
+
// Give reward to the player
|
|
108
|
+
}
|
|
84
109
|
```
|
|
85
110
|
|
|
86
|
-
Every play
|
|
111
|
+
Every play session is backed by a short-lived, server-signed token — the SDK
|
|
87
112
|
fetches and caches it automatically the first time `trackPlay`/`trackMatch`/
|
|
88
|
-
`emitGameEvent` needs it after `
|
|
113
|
+
`emitGameEvent` needs it after `startPlaySession()`. You never see or manage
|
|
89
114
|
this token directly.
|
|
90
115
|
|
|
91
116
|
### Tracking call reference
|
|
@@ -93,8 +118,8 @@ this token directly.
|
|
|
93
118
|
| Call | When |
|
|
94
119
|
| --- | --- |
|
|
95
120
|
| `trackView()` | Once when the game view opens, before any play/match tracking |
|
|
96
|
-
| `
|
|
97
|
-
| `trackPlay(playTimeSeconds?)` | Periodically / on pause, to report accumulated play time for the current
|
|
121
|
+
| `startPlaySession()` | Once when the user presses Play. Synchronous — returns an id immediately |
|
|
122
|
+
| `trackPlay(playTimeSeconds?)` | Periodically / on pause, to report accumulated play time for the current play session |
|
|
98
123
|
| `trackMatch(matchId, { durationSeconds, isCompleted?, isWin? })` | Once per round/match end. `matchId` is the idempotency anchor — safe to call twice |
|
|
99
124
|
| `emitGameEvent(actionCode, { value?, refId?, payload? })` | For quest/task progress and custom analytics — e.g. `'LEVEL_UP'`, `'ITEM_COLLECTED'` |
|
|
100
125
|
|
|
@@ -115,7 +140,7 @@ await sdk.init({ gameId: 'YOUR_APP_ID', verifyMode: 'strict' });
|
|
|
115
140
|
// `gameId` here is the game's public appId (same as `x-game-key`/`appId` above).
|
|
116
141
|
|
|
117
142
|
await sdk.trackView();
|
|
118
|
-
sdk.
|
|
143
|
+
sdk.startPlaySession();
|
|
119
144
|
await sdk.trackPlay(playTimeSeconds);
|
|
120
145
|
await sdk.trackMatch(matchId, { durationSeconds, isWin: true });
|
|
121
146
|
await sdk.emit('CUSTOM_ACTION', { value: 1 });
|
|
@@ -145,7 +170,7 @@ const result = await sdk.api!.purchaseStoreOffer(gameId, 'starter_pack', { quant
|
|
|
145
170
|
```
|
|
146
171
|
|
|
147
172
|
`purchaseStoreOffer` signs a short-lived checkout token and redeems it in
|
|
148
|
-
one call — the same safety mechanism as the play-
|
|
173
|
+
one call — the same safety mechanism as the play-session token used for
|
|
149
174
|
tracking, not a static secret. This is enough for creators with no backend
|
|
150
175
|
of their own.
|
|
151
176
|
|
|
@@ -238,6 +263,13 @@ request as `false` and continue normally.
|
|
|
238
263
|
If the parent never answers, `requestAds()` times out after 5 seconds by
|
|
239
264
|
default and resolves `false`.
|
|
240
265
|
|
|
266
|
+
```ts
|
|
267
|
+
const isWatched = await looplayAuth.requestAds();
|
|
268
|
+
if (isWatched) {
|
|
269
|
+
// Grant reward to player
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
241
273
|
```ts
|
|
242
274
|
window.addEventListener('message', async (event) => {
|
|
243
275
|
const data = event.data;
|
|
@@ -282,7 +314,7 @@ directly, no bundler needed:
|
|
|
282
314
|
looplayAuth.init();
|
|
283
315
|
looplayAuth.initLifecycleTracking();
|
|
284
316
|
|
|
285
|
-
// call looplayAuth.trackView() /
|
|
317
|
+
// call looplayAuth.trackView() / startPlaySession() / trackPlay(...) /
|
|
286
318
|
// trackMatch(...) / emitGameEvent(...) from your game code
|
|
287
319
|
</script>
|
|
288
320
|
```
|
package/dist/looplay-sdk.cjs.js
CHANGED
|
@@ -261,16 +261,16 @@ var ServiceClient = class {
|
|
|
261
261
|
});
|
|
262
262
|
return true;
|
|
263
263
|
}
|
|
264
|
-
/** Signs a fresh `
|
|
265
|
-
async
|
|
266
|
-
return this.http.request("POST", "/sdk/games/
|
|
264
|
+
/** Signs a fresh `playSessionToken` — required by `completePlaySession`/`completeGameplayMatch`/`recordGameplayAction`. */
|
|
265
|
+
async startPlaySession(gameKey, auth = {}, body = {}) {
|
|
266
|
+
return this.http.request("POST", "/sdk/games/play-sessions/start", {
|
|
267
267
|
...auth,
|
|
268
268
|
headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
|
|
269
269
|
body
|
|
270
270
|
});
|
|
271
271
|
}
|
|
272
|
-
async
|
|
273
|
-
await this.http.request("POST", "/sdk/games/
|
|
272
|
+
async completePlaySession(gameKey, auth, body) {
|
|
273
|
+
await this.http.request("POST", "/sdk/games/play-sessions/complete", {
|
|
274
274
|
...auth,
|
|
275
275
|
headers: { "x-game-key": gameKey, ...this.trackingHeaders(auth) },
|
|
276
276
|
body
|
|
@@ -302,6 +302,16 @@ var ServiceClient = class {
|
|
|
302
302
|
headers: { "x-looplay-anon-id": anonId }
|
|
303
303
|
});
|
|
304
304
|
}
|
|
305
|
+
/**
|
|
306
|
+
* Lightweight integration heartbeat for review/liveops. This intentionally
|
|
307
|
+
* does not record a view/play metric.
|
|
308
|
+
*/
|
|
309
|
+
async detectGameIntegration(gameKey) {
|
|
310
|
+
await this.http.request("POST", "/sdk/games/integration/detect", {
|
|
311
|
+
headers: { "x-game-key": gameKey }
|
|
312
|
+
});
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
305
315
|
trackingHeaders(auth) {
|
|
306
316
|
return {
|
|
307
317
|
"x-game-tracking-session-id": auth.trackingSessionId,
|
|
@@ -339,7 +349,7 @@ var ServiceClient = class {
|
|
|
339
349
|
// `x-game-key` + the purchasing player's own bearer token, no backend or
|
|
340
350
|
// secret pair required. Safe to call directly from client/browser code —
|
|
341
351
|
// `checkoutToken` (short-lived, server-signed) plays the same role as
|
|
342
|
-
// `
|
|
352
|
+
// `playSessionToken` on the tracking routes. `ApiClient.purchaseStoreOffer`
|
|
343
353
|
// wraps this pair for you.
|
|
344
354
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
345
355
|
/** Signs a `checkoutToken` for one purchase — required by `purchaseMyStoreOffer`. */
|
|
@@ -410,30 +420,6 @@ var ServiceClient = class {
|
|
|
410
420
|
async getMyBalance(bearerToken) {
|
|
411
421
|
return this.http.request("GET", "/sdk/user/balance", { bearerToken });
|
|
412
422
|
}
|
|
413
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
414
|
-
// Referrals
|
|
415
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
416
|
-
async listReferrals(bearerToken, query) {
|
|
417
|
-
return this.http.request("GET", "/sdk/referrals", { bearerToken, query });
|
|
418
|
-
}
|
|
419
|
-
async setReferral(bearerToken, body) {
|
|
420
|
-
return this.http.request("POST", "/sdk/referrals/set", { bearerToken, body });
|
|
421
|
-
}
|
|
422
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
423
|
-
// Tasks
|
|
424
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
425
|
-
async listTasks(bearerToken, query) {
|
|
426
|
-
return this.http.request("GET", "/sdk/task", { bearerToken, query: { ...query } });
|
|
427
|
-
}
|
|
428
|
-
async listFinishedTasks(bearerToken, query) {
|
|
429
|
-
return this.http.request("GET", "/sdk/task/finished", { bearerToken, query: { ...query } });
|
|
430
|
-
}
|
|
431
|
-
async startTask(bearerToken, body) {
|
|
432
|
-
return this.http.request("POST", "/sdk/task/start", { bearerToken, body });
|
|
433
|
-
}
|
|
434
|
-
async claimTask(bearerToken, body) {
|
|
435
|
-
return this.http.request("POST", "/sdk/task/claim", { bearerToken, body });
|
|
436
|
-
}
|
|
437
423
|
};
|
|
438
424
|
|
|
439
425
|
// src/apps/api-client.ts
|
|
@@ -453,9 +439,9 @@ var ApiClient = class {
|
|
|
453
439
|
getAnonymousId;
|
|
454
440
|
trackingSession;
|
|
455
441
|
trackingSessionPromise;
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
442
|
+
currentPlaySessionId;
|
|
443
|
+
playSession;
|
|
444
|
+
playSessionPromise;
|
|
459
445
|
constructor(options) {
|
|
460
446
|
if (!options.baseUrl) throw new MissingBaseUrlError();
|
|
461
447
|
this.raw = new ServiceClient({
|
|
@@ -470,6 +456,9 @@ var ApiClient = class {
|
|
|
470
456
|
unsafeRaw() {
|
|
471
457
|
return this.raw;
|
|
472
458
|
}
|
|
459
|
+
async detectIntegration(gameKey) {
|
|
460
|
+
return this.raw.detectGameIntegration(gameKey);
|
|
461
|
+
}
|
|
473
462
|
// Real-time (balance_change / store_purchase_completed WebSocket events)
|
|
474
463
|
// deliberately lives outside this class — import `WsClient` from
|
|
475
464
|
// '@looplay/sdk/realtime' and construct it with the same `getAccessToken`
|
|
@@ -525,30 +514,30 @@ var ApiClient = class {
|
|
|
525
514
|
return this.trackingSessionPromise;
|
|
526
515
|
}
|
|
527
516
|
/**
|
|
528
|
-
* Signs (and caches until near expiry) the `
|
|
529
|
-
* `trackPlay`/`trackMatch`/`emit` for the current
|
|
517
|
+
* Signs (and caches until near expiry) the `playSessionToken` required by
|
|
518
|
+
* `trackPlay`/`trackMatch`/`emit` for the current play session — does not
|
|
530
519
|
* swallow failures, unlike `ensureTrackingSession`, since these calls
|
|
531
520
|
* are rejected outright without a valid token.
|
|
532
521
|
*/
|
|
533
|
-
async
|
|
522
|
+
async ensurePlaySession(gameKey, auth, playSessionId) {
|
|
534
523
|
const now = Date.now();
|
|
535
|
-
if (this.
|
|
536
|
-
return this.
|
|
524
|
+
if (this.playSession && this.playSession.gameKey === gameKey && this.playSession.playSessionId === playSessionId && this.playSession.expiresAtMs > now + 1e4) {
|
|
525
|
+
return this.playSession;
|
|
537
526
|
}
|
|
538
|
-
if (!this.
|
|
539
|
-
this.
|
|
540
|
-
this.
|
|
527
|
+
if (!this.playSessionPromise) {
|
|
528
|
+
this.playSessionPromise = this.raw.startPlaySession(gameKey, auth, { playSessionId }).then((dto) => {
|
|
529
|
+
this.playSession = {
|
|
541
530
|
gameKey,
|
|
542
|
-
|
|
543
|
-
|
|
531
|
+
playSessionId: dto.playSessionId,
|
|
532
|
+
playSessionToken: dto.playSessionToken,
|
|
544
533
|
expiresAtMs: new Date(dto.expiresAt).getTime()
|
|
545
534
|
};
|
|
546
|
-
return this.
|
|
535
|
+
return this.playSession;
|
|
547
536
|
}).finally(() => {
|
|
548
|
-
this.
|
|
537
|
+
this.playSessionPromise = void 0;
|
|
549
538
|
});
|
|
550
539
|
}
|
|
551
|
-
return this.
|
|
540
|
+
return this.playSessionPromise;
|
|
552
541
|
}
|
|
553
542
|
/**
|
|
554
543
|
* Call once when the game view opens, before any play/match tracking.
|
|
@@ -559,35 +548,35 @@ var ApiClient = class {
|
|
|
559
548
|
return this.raw.recordGameView(gameKey, auth);
|
|
560
549
|
}
|
|
561
550
|
/**
|
|
562
|
-
* Starts a new play
|
|
551
|
+
* Starts a new play session and returns its id. Call when the user
|
|
563
552
|
* presses Play; `trackPlay`/`trackMatch`/`emit` reuse this id (and the
|
|
564
|
-
* signed `
|
|
565
|
-
* `
|
|
553
|
+
* signed `playSessionToken` backing it) across calls until the next
|
|
554
|
+
* `startPlaySession()`, auto-starting one on first use if none was started.
|
|
566
555
|
*/
|
|
567
|
-
|
|
568
|
-
this.
|
|
569
|
-
this.
|
|
570
|
-
return this.
|
|
556
|
+
startPlaySession() {
|
|
557
|
+
this.currentPlaySessionId = generateId();
|
|
558
|
+
this.playSession = void 0;
|
|
559
|
+
return this.currentPlaySessionId;
|
|
571
560
|
}
|
|
572
561
|
async trackPlay(gameKey, playTimeSeconds) {
|
|
573
562
|
const auth = await this.resolveTrackingAuth();
|
|
574
|
-
const
|
|
575
|
-
const {
|
|
576
|
-
return this.raw.
|
|
563
|
+
const playSessionId = this.currentPlaySessionId ?? this.startPlaySession();
|
|
564
|
+
const { playSessionToken } = await this.ensurePlaySession(gameKey, auth, playSessionId);
|
|
565
|
+
return this.raw.completePlaySession(gameKey, auth, { playTimeSeconds, playSessionId, playSessionToken });
|
|
577
566
|
}
|
|
578
567
|
async trackMatch(gameKey, body) {
|
|
579
568
|
const auth = await this.resolveTrackingAuth();
|
|
580
|
-
const
|
|
581
|
-
const {
|
|
582
|
-
return this.raw.completeGameplayMatch(gameKey, auth, { ...body,
|
|
569
|
+
const playSessionId = this.currentPlaySessionId ?? this.startPlaySession();
|
|
570
|
+
const { playSessionToken } = await this.ensurePlaySession(gameKey, auth, playSessionId);
|
|
571
|
+
return this.raw.completeGameplayMatch(gameKey, auth, { ...body, playSessionId, playSessionToken });
|
|
583
572
|
}
|
|
584
573
|
async emit(gameKey, actionCode, opts) {
|
|
585
574
|
const auth = await this.resolveTrackingAuth();
|
|
586
|
-
const
|
|
587
|
-
const {
|
|
575
|
+
const playSessionId = this.currentPlaySessionId ?? this.startPlaySession();
|
|
576
|
+
const { playSessionToken } = await this.ensurePlaySession(gameKey, auth, playSessionId);
|
|
588
577
|
const body = {
|
|
589
|
-
|
|
590
|
-
|
|
578
|
+
playSessionId,
|
|
579
|
+
playSessionToken,
|
|
591
580
|
actionCode,
|
|
592
581
|
value: opts?.value,
|
|
593
582
|
refId: opts?.refId,
|
|
@@ -615,7 +604,7 @@ var ApiClient = class {
|
|
|
615
604
|
}
|
|
616
605
|
/**
|
|
617
606
|
* Spends coin balance to purchase a store offer — signs a checkout intent
|
|
618
|
-
* first, then redeems it, mirroring the
|
|
607
|
+
* first, then redeems it, mirroring the play session flow above.
|
|
619
608
|
* Self-serve: only the player's own access token is used, no secret pair
|
|
620
609
|
* required, safe to call directly from the game client. Also see
|
|
621
610
|
* `onStorePurchase` to react to the result in real time from any tab.
|
|
@@ -650,30 +639,6 @@ var ApiClient = class {
|
|
|
650
639
|
const token = await this.requireToken();
|
|
651
640
|
return this.raw.getBalanceHistory(token, query);
|
|
652
641
|
}
|
|
653
|
-
async listReferrals(query) {
|
|
654
|
-
const token = await this.requireToken();
|
|
655
|
-
return this.raw.listReferrals(token, query);
|
|
656
|
-
}
|
|
657
|
-
async setReferral(body) {
|
|
658
|
-
const token = await this.requireToken();
|
|
659
|
-
return this.raw.setReferral(token, body);
|
|
660
|
-
}
|
|
661
|
-
async listTasks(query) {
|
|
662
|
-
const token = await this.requireToken();
|
|
663
|
-
return this.raw.listTasks(token, query);
|
|
664
|
-
}
|
|
665
|
-
async listFinishedTasks(query) {
|
|
666
|
-
const token = await this.requireToken();
|
|
667
|
-
return this.raw.listFinishedTasks(token, query);
|
|
668
|
-
}
|
|
669
|
-
async startTask(body) {
|
|
670
|
-
const token = await this.requireToken();
|
|
671
|
-
return this.raw.startTask(token, body);
|
|
672
|
-
}
|
|
673
|
-
async claimTask(body) {
|
|
674
|
-
const token = await this.requireToken();
|
|
675
|
-
return this.raw.claimTask(token, body);
|
|
676
|
-
}
|
|
677
642
|
};
|
|
678
643
|
|
|
679
644
|
// src/looplay-sdk.ts
|
|
@@ -699,6 +664,7 @@ var LooplaySDK = class {
|
|
|
699
664
|
async init(params) {
|
|
700
665
|
this.gameId = params.gameId;
|
|
701
666
|
await this.auth?.init();
|
|
667
|
+
await this.detectIntegration(params);
|
|
702
668
|
await this.verifyGameId(params);
|
|
703
669
|
this.initialized = true;
|
|
704
670
|
}
|
|
@@ -720,11 +686,11 @@ var LooplaySDK = class {
|
|
|
720
686
|
if (!this.gameId) throw new NotInitializedError();
|
|
721
687
|
return this.api.trackView(this.gameId);
|
|
722
688
|
}
|
|
723
|
-
/** Starts a new play
|
|
724
|
-
|
|
689
|
+
/** Starts a new play session; call when the user presses Play. Returns the play session id. */
|
|
690
|
+
startPlaySession() {
|
|
725
691
|
this.assertInitialized();
|
|
726
692
|
if (!this.api) throw new MissingBaseUrlError();
|
|
727
|
-
return this.api.
|
|
693
|
+
return this.api.startPlaySession();
|
|
728
694
|
}
|
|
729
695
|
async trackPlay(playTimeSeconds) {
|
|
730
696
|
this.assertInitialized();
|
|
@@ -767,6 +733,10 @@ var LooplaySDK = class {
|
|
|
767
733
|
if (!this.api) throw new MissingBaseUrlError();
|
|
768
734
|
await this.api.listGameAssets(params.gameId);
|
|
769
735
|
}
|
|
736
|
+
async detectIntegration(params) {
|
|
737
|
+
if (!this.api) return;
|
|
738
|
+
await this.api.detectIntegration(params.gameId);
|
|
739
|
+
}
|
|
770
740
|
};
|
|
771
741
|
|
|
772
742
|
// src/auth/jwt.ts
|
|
@@ -1092,9 +1062,9 @@ var LooplayIframeAuth = class {
|
|
|
1092
1062
|
this.debug("trackView dispatched", { gameId });
|
|
1093
1063
|
return this.getClient().trackView(gameId);
|
|
1094
1064
|
}
|
|
1095
|
-
/** Starts a new play
|
|
1096
|
-
|
|
1097
|
-
return this.getClient().
|
|
1065
|
+
/** Starts a new play session; call when the user presses Play. Returns the play session id. */
|
|
1066
|
+
startPlaySession() {
|
|
1067
|
+
return this.getClient().startPlaySession();
|
|
1098
1068
|
}
|
|
1099
1069
|
trackPlay(playTimeSeconds) {
|
|
1100
1070
|
const gameId = this.getGameId();
|