@pixels-online/pixels-client-js-sdk 1.21.0 → 2.1.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/dist/index.js CHANGED
@@ -95,7 +95,7 @@ class EventEmitter {
95
95
  }
96
96
  }
97
97
 
98
- exports.OfferEvent = void 0;
98
+ var OfferEvent;
99
99
  (function (OfferEvent) {
100
100
  OfferEvent["CONNECTED"] = "connected";
101
101
  OfferEvent["DISCONNECTED"] = "disconnected";
@@ -110,19 +110,19 @@ exports.OfferEvent = void 0;
110
110
  OFFER_UPDATED = 'offer_updated',
111
111
  SNAPSHOT_UPDATED = 'snapshot_updated',
112
112
  */
113
- })(exports.OfferEvent || (exports.OfferEvent = {}));
113
+ })(OfferEvent || (OfferEvent = {}));
114
114
 
115
115
  /**
116
116
  * Connection states for SSE connection
117
117
  */
118
- exports.ConnectionState = void 0;
118
+ var ConnectionState;
119
119
  (function (ConnectionState) {
120
120
  ConnectionState["DISCONNECTED"] = "disconnected";
121
121
  ConnectionState["CONNECTING"] = "connecting";
122
122
  ConnectionState["CONNECTED"] = "connected";
123
123
  ConnectionState["RECONNECTING"] = "reconnecting";
124
124
  ConnectionState["ERROR"] = "error";
125
- })(exports.ConnectionState || (exports.ConnectionState = {}));
125
+ })(ConnectionState || (ConnectionState = {}));
126
126
 
127
127
  /**
128
128
  * Custom EventSource implementation that supports headers
@@ -362,7 +362,7 @@ class SSEConnection {
362
362
  this.reconnectAttempts = 0;
363
363
  this.reconnectTimeout = null;
364
364
  this.isConnecting = false;
365
- this.connectionState = exports.ConnectionState.DISCONNECTED;
365
+ this.connectionState = ConnectionState.DISCONNECTED;
366
366
  this.serverSuggestedRetryTime = null;
367
367
  this.logger = createLogger(config, 'SSEConnection');
368
368
  this.endpoint = mapEnvToBuildOnApiUrl(config.env);
@@ -381,7 +381,7 @@ class SSEConnection {
381
381
  if (previousState === newState)
382
382
  return;
383
383
  this.connectionState = newState;
384
- this.eventEmitter.emit(exports.OfferEvent.CONNECTION_STATE_CHANGED, {
384
+ this.eventEmitter.emit(OfferEvent.CONNECTION_STATE_CHANGED, {
385
385
  state: newState,
386
386
  previousState,
387
387
  error,
@@ -402,7 +402,7 @@ class SSEConnection {
402
402
  return;
403
403
  }
404
404
  this.isConnecting = true;
405
- this.setConnectionState(exports.ConnectionState.CONNECTING);
405
+ this.setConnectionState(ConnectionState.CONNECTING);
406
406
  this.logger.log('Connecting to SSE endpoint...');
407
407
  try {
408
408
  // Create SSE URL
@@ -423,8 +423,8 @@ class SSEConnection {
423
423
  this.logger.log('SSE connection opened');
424
424
  this.isConnecting = false;
425
425
  this.reconnectAttempts = 0;
426
- this.setConnectionState(exports.ConnectionState.CONNECTED);
427
- this.eventEmitter.emit(exports.OfferEvent.CONNECTED, { timestamp: new Date() });
426
+ this.setConnectionState(ConnectionState.CONNECTED);
427
+ this.eventEmitter.emit(OfferEvent.CONNECTED, { timestamp: new Date() });
428
428
  resolve();
429
429
  };
430
430
  this.eventSource.onerror = (error) => {
@@ -435,7 +435,7 @@ class SSEConnection {
435
435
  error?.message === 'jwt-expired' ||
436
436
  error?.message === 'jwt-invalid') {
437
437
  this.tokenManager.clearToken();
438
- this.setConnectionState(exports.ConnectionState.DISCONNECTED);
438
+ this.setConnectionState(ConnectionState.DISCONNECTED);
439
439
  // Try to reconnect with fresh token if reconnect is enabled
440
440
  if (this.config.reconnect &&
441
441
  this.reconnectAttempts < (this.config.maxReconnectAttempts || 5)) {
@@ -453,24 +453,24 @@ class SSEConnection {
453
453
  ? 'Connection closed'
454
454
  : 'Connection error';
455
455
  const connectionError = new Error(errorMsg);
456
- this.setConnectionState(exports.ConnectionState.ERROR, connectionError);
456
+ this.setConnectionState(ConnectionState.ERROR, connectionError);
457
457
  if (this.eventSource?.getReadyState() === ReadyState.CLOSED) {
458
- this.eventEmitter.emit(exports.OfferEvent.CONNECTION_ERROR, {
458
+ this.eventEmitter.emit(OfferEvent.CONNECTION_ERROR, {
459
459
  error: connectionError,
460
460
  timestamp: new Date(),
461
461
  });
462
462
  this.handleReconnect();
463
463
  }
464
464
  else {
465
- this.eventEmitter.emit(exports.OfferEvent.CONNECTION_ERROR, {
465
+ this.eventEmitter.emit(OfferEvent.CONNECTION_ERROR, {
466
466
  error: connectionError,
467
467
  timestamp: new Date(),
468
468
  });
469
469
  reject(connectionError);
470
470
  }
471
471
  };
472
- this.eventSource.addEventListener(exports.OfferEvent.OFFER_SURFACED, (event) => {
473
- this.handleMessage(exports.OfferEvent.OFFER_SURFACED, event.data);
472
+ this.eventSource.addEventListener(OfferEvent.OFFER_SURFACED, (event) => {
473
+ this.handleMessage(OfferEvent.OFFER_SURFACED, event.data);
474
474
  });
475
475
  /*this.eventSource.addEventListener(OfferEvent.INIT, (event) => {
476
476
  this.handleMessage(OfferEvent.INIT, event.data);
@@ -547,7 +547,7 @@ class SSEConnection {
547
547
  });
548
548
  break;
549
549
  */
550
- case exports.OfferEvent.OFFER_SURFACED:
550
+ case OfferEvent.OFFER_SURFACED:
551
551
  if (!parsed.instanceId)
552
552
  throw new Error('OFFER_SURFACED message missing offer');
553
553
  let surface = true;
@@ -556,7 +556,7 @@ class SSEConnection {
556
556
  }
557
557
  if (surface) {
558
558
  this.logger.log('Offer surfaced hook returned true, popping notification for offer:', parsed);
559
- this.eventEmitter.emit(exports.OfferEvent.OFFER_SURFACED, {
559
+ this.eventEmitter.emit(OfferEvent.OFFER_SURFACED, {
560
560
  offer: parsed,
561
561
  });
562
562
  }
@@ -567,7 +567,7 @@ class SSEConnection {
567
567
  }
568
568
  catch (error) {
569
569
  this.logger.log('Error parsing SSE message:', error);
570
- this.eventEmitter.emit(exports.OfferEvent.ERROR, {
570
+ this.eventEmitter.emit(OfferEvent.ERROR, {
571
571
  error: error,
572
572
  context: 'sse_message_parse',
573
573
  });
@@ -580,9 +580,9 @@ class SSEConnection {
580
580
  }
581
581
  if (this.reconnectAttempts >= (this.config.maxReconnectAttempts || 5)) {
582
582
  this.logger.log('Max reconnection attempts reached');
583
- this.setConnectionState(exports.ConnectionState.DISCONNECTED);
583
+ this.setConnectionState(ConnectionState.DISCONNECTED);
584
584
  this.disconnect();
585
- this.eventEmitter.emit(exports.OfferEvent.DISCONNECTED, {
585
+ this.eventEmitter.emit(OfferEvent.DISCONNECTED, {
586
586
  reason: 'max_reconnect_attempts',
587
587
  timestamp: new Date(),
588
588
  });
@@ -592,7 +592,7 @@ class SSEConnection {
592
592
  // Use server-suggested retry time if available, otherwise use config delay with exponential backoff
593
593
  const baseDelay = this.serverSuggestedRetryTime || this.config.reconnectDelay || 1000;
594
594
  const backoffDelay = baseDelay * Math.pow(2, this.reconnectAttempts - 1);
595
- this.setConnectionState(exports.ConnectionState.RECONNECTING);
595
+ this.setConnectionState(ConnectionState.RECONNECTING);
596
596
  this.logger.log(`Reconnecting in ${backoffDelay}ms (attempt ${this.reconnectAttempts}`);
597
597
  this.reconnectTimeout = window.setTimeout(() => {
598
598
  this.eventSource = null;
@@ -611,8 +611,8 @@ class SSEConnection {
611
611
  this.logger.log('Closing SSE connection');
612
612
  this.eventSource.close();
613
613
  this.eventSource = null;
614
- this.setConnectionState(exports.ConnectionState.DISCONNECTED);
615
- this.eventEmitter.emit(exports.OfferEvent.DISCONNECTED, {
614
+ this.setConnectionState(ConnectionState.DISCONNECTED);
615
+ this.eventEmitter.emit(OfferEvent.DISCONNECTED, {
616
616
  reason: 'manual',
617
617
  timestamp: new Date(),
618
618
  });
@@ -643,7 +643,12 @@ class OfferStore {
643
643
  return this.players.get(targetId) || null;
644
644
  }
645
645
  setPlayer(player) {
646
- this.players.set(player.gameData.playerId, player);
646
+ const playerId = player.snapshot?.playerId;
647
+ if (!playerId) {
648
+ this.logger.warn('No playerId in player snapshot');
649
+ return;
650
+ }
651
+ this.players.set(playerId, player);
647
652
  this.logger.log('Updated player:', player);
648
653
  }
649
654
  /**
@@ -651,13 +656,14 @@ class OfferStore {
651
656
  */
652
657
  setOffers(offers, target) {
653
658
  const targetPlayer = target || this.getPlayer();
654
- if (!targetPlayer) {
659
+ if (!targetPlayer?.snapshot?.playerId) {
655
660
  this.logger.warn('No target player to set offers for');
656
661
  return;
657
662
  }
658
- this.offers.set(targetPlayer.gameData.playerId, new Map());
663
+ const playerId = targetPlayer.snapshot.playerId;
664
+ this.offers.set(playerId, new Map());
659
665
  offers.forEach((offer) => {
660
- this.offers.get(targetPlayer.gameData.playerId).set(offer.instanceId, offer);
666
+ this.offers.get(playerId).set(offer.instanceId, offer);
661
667
  });
662
668
  this.logger.log(`Set ${offers.length} offers`);
663
669
  }
@@ -695,6 +701,17 @@ class OfferStore {
695
701
  return undefined;
696
702
  return this.offers.get(targetId)?.get(offerId);
697
703
  }
704
+ /**
705
+ * Find an offer by instanceId across all players in the store
706
+ */
707
+ findOfferById(instanceId) {
708
+ for (const playerOffers of this.offers.values()) {
709
+ const offer = playerOffers.get(instanceId);
710
+ if (offer)
711
+ return offer;
712
+ }
713
+ return undefined;
714
+ }
698
715
  /**
699
716
  * Get all offers
700
717
  */
@@ -1055,8 +1072,8 @@ class OfferwallClient {
1055
1072
  /**
1056
1073
  * Claim rewards for an offer
1057
1074
  */
1058
- async claimReward(instanceId, targetId = this.getSelfId()) {
1059
- const offer = this.offerStore.getOffer(instanceId, targetId);
1075
+ async claimReward(instanceId) {
1076
+ const offer = this.offerStore.findOfferById(instanceId);
1060
1077
  if (!offer) {
1061
1078
  throw new Error(`Offer ${instanceId} not found`);
1062
1079
  }
@@ -1071,10 +1088,10 @@ class OfferwallClient {
1071
1088
  }
1072
1089
  }
1073
1090
  try {
1074
- const response = await this.claimOfferAPI(instanceId, targetId);
1091
+ const response = await this.claimOfferAPI(instanceId, offer.playerId);
1075
1092
  const updatedOffer = { ...offer, status: 'claimed' };
1076
1093
  this.offerStore.upsertOffer(updatedOffer);
1077
- this.eventEmitter.emit(exports.OfferEvent.OFFER_CLAIMED, {
1094
+ this.eventEmitter.emit(OfferEvent.OFFER_CLAIMED, {
1078
1095
  instanceId,
1079
1096
  });
1080
1097
  if (this.hooks.afterOfferClaim) {
@@ -1097,7 +1114,7 @@ class OfferwallClient {
1097
1114
  * Get current connection state
1098
1115
  */
1099
1116
  getConnectionState() {
1100
- return this.sseConnection?.getConnectionState() || exports.ConnectionState.DISCONNECTED;
1117
+ return this.sseConnection?.getConnectionState() || ConnectionState.DISCONNECTED;
1101
1118
  }
1102
1119
  setupInternalListeners() {
1103
1120
  /*
@@ -1126,7 +1143,7 @@ class OfferwallClient {
1126
1143
  this.handleError(error, context);
1127
1144
  });
1128
1145
  */
1129
- this.eventEmitter.on(exports.OfferEvent.OFFER_SURFACED, ({ offer }) => {
1146
+ this.eventEmitter.on(OfferEvent.OFFER_SURFACED, ({ offer }) => {
1130
1147
  this.offerStore.upsertOffer(offer); // should always be selfId
1131
1148
  this.logger.log(`Surfaced offer: ${offer.instanceId}`);
1132
1149
  });
@@ -1174,11 +1191,11 @@ class OfferwallClient {
1174
1191
  try {
1175
1192
  const { offers, player } = await this.getOffersAndPlayer(targetId);
1176
1193
  if (targetId == null) {
1177
- this.selfId = player.gameData.playerId;
1194
+ this.selfId = player.snapshot?.playerId ?? null;
1178
1195
  }
1179
1196
  this.offerStore.setPlayer(player);
1180
1197
  this.offerStore.setOffers(offers, player);
1181
- this.eventEmitter.emit(exports.OfferEvent.REFRESH, { offers, player: player });
1198
+ this.eventEmitter.emit(OfferEvent.REFRESH, { offers, player: player });
1182
1199
  this.logger.log('Refreshed offers and player snapshot');
1183
1200
  return offers;
1184
1201
  }
@@ -1323,1542 +1340,8 @@ class OfferwallClient {
1323
1340
  }
1324
1341
  }
1325
1342
 
1326
- const DEFAULT_ENTITY_KIND = '_default';
1327
-
1328
- const keyPattern = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
1329
- /**
1330
- * This replaces {keyName} keys from the template with corresponding values from the dynamic object.
1331
- */
1332
- function renderTemplate(template, dynamic) {
1333
- if (!template)
1334
- return '';
1335
- return template.replace(keyPattern, (_match, key) => {
1336
- if (dynamic && typeof dynamic[key] === 'boolean') {
1337
- return dynamic[key] ? '✓' : '✗';
1338
- }
1339
- if (dynamic && dynamic[key] !== undefined) {
1340
- return String(dynamic[key]);
1341
- }
1342
- return '{?}'; // indicate missing key
1343
- });
1344
- }
1345
- /**
1346
- * This replaces {{keyName}} in dynamic condition keys with corresponding values from
1347
- * the PlayerOffer.trackers
1348
- *
1349
- * eg. a condition high_score_pet-{{surfacerPlayerId}} with high_score_pet-12345
1350
- */
1351
- function replaceDynamicConditionKey(key, trackers) {
1352
- return key?.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (match, p1) => {
1353
- const value = trackers[p1];
1354
- return value !== undefined ? String(value) : match;
1355
- });
1356
- }
1357
- /** this replaces all of the dynamic conditions.keys by calling replaceDynamicConditionKey */
1358
- function replaceDynamicConditionKeys(conditions, trackers) {
1359
- return conditions.map((condition) => ({
1360
- ...condition,
1361
- key: replaceDynamicConditionKey(condition.key, trackers),
1362
- }));
1363
- }
1364
-
1365
- const dynamicTrackerToPrimitive = (dynaTrack) => {
1366
- const primitive = {};
1367
- for (const key in dynaTrack) {
1368
- primitive[key] = dynaTrack[key].value || 0;
1369
- }
1370
- return primitive;
1371
- };
1372
-
1373
- const addressNetworkId = (contractAddress, network) => {
1374
- return `${contractAddress.toLowerCase()}:${network.toUpperCase()}`;
1375
- };
1376
-
1377
- const meetsBaseConditions = ({ conditions, playerSnap, addDetails,
1378
- /** this exists if calling meetsBaseConditions from meetsCompletionConditions. but surfacing
1379
- * check doesn't use this since we don't have a playerOffer at surfacing time
1380
- */
1381
- playerOffer,
1382
- /** Additional data like fetched token balances that isn't part of playerSnap */
1383
- additionalData, }) => {
1384
- const conditionData = [];
1385
- let isValid = true;
1386
- if (conditions?.minDaysInGame) {
1387
- const isDisqualify = (playerSnap.daysInGame || 0) < conditions.minDaysInGame;
1388
- if (addDetails) {
1389
- conditionData.push({
1390
- isMet: !isDisqualify,
1391
- kind: 'minDaysInGame',
1392
- trackerAmount: playerSnap.daysInGame || 0,
1393
- trackerGoal: conditions.minDaysInGame,
1394
- text: `More than ${conditions.minDaysInGame} Days in Game`,
1395
- });
1396
- if (isDisqualify)
1397
- isValid = false;
1398
- }
1399
- else {
1400
- if (isDisqualify)
1401
- return { isValid: false };
1402
- }
1403
- }
1404
- if (conditions?.minTrustScore) {
1405
- const isDisqualify = (playerSnap.trustScore || 0) < conditions.minTrustScore;
1406
- if (addDetails) {
1407
- conditionData.push({
1408
- isMet: !isDisqualify,
1409
- kind: 'minTrustScore',
1410
- trackerAmount: playerSnap.trustScore || 0,
1411
- trackerGoal: conditions.minTrustScore,
1412
- text: `More than ${conditions.minTrustScore} Rep`,
1413
- });
1414
- if (isDisqualify)
1415
- isValid = false;
1416
- }
1417
- else {
1418
- if (isDisqualify)
1419
- return { isValid: false };
1420
- }
1421
- }
1422
- if (conditions?.maxTrustScore) {
1423
- const isDisqualify = (playerSnap.trustScore || 0) > conditions.maxTrustScore;
1424
- if (addDetails) {
1425
- conditionData.push({
1426
- isMet: !isDisqualify,
1427
- kind: 'maxTrustScore',
1428
- trackerAmount: playerSnap.trustScore || 0,
1429
- text: `Less than ${conditions.maxTrustScore} Rep`,
1430
- });
1431
- if (isDisqualify)
1432
- isValid = false;
1433
- }
1434
- else {
1435
- if (isDisqualify)
1436
- return { isValid: false };
1437
- }
1438
- }
1439
- for (const key in conditions?.achievements) {
1440
- const a = conditions.achievements[key];
1441
- const playerAchData = playerSnap.achievements?.[key];
1442
- if (!playerAchData) {
1443
- if (addDetails) {
1444
- conditionData.push({
1445
- isMet: false,
1446
- kind: 'achievements',
1447
- trackerAmount: 0,
1448
- trackerGoal: 1,
1449
- text: `Have the achievement ${a.name}`,
1450
- });
1451
- isValid = false;
1452
- }
1453
- else {
1454
- return { isValid: false };
1455
- }
1456
- }
1457
- if (a.minCount) {
1458
- const isDisqualify = (playerAchData?.count || 0) < a.minCount;
1459
- if (addDetails) {
1460
- conditionData.push({
1461
- isMet: !isDisqualify,
1462
- kind: 'achievements',
1463
- trackerAmount: playerAchData?.count || 0,
1464
- trackerGoal: a.minCount,
1465
- text: `Have the achievement ${a.name} more than ${a.minCount} times`,
1466
- });
1467
- if (isDisqualify)
1468
- isValid = false;
1469
- }
1470
- else {
1471
- if (isDisqualify)
1472
- return { isValid: false };
1473
- }
1474
- }
1475
- }
1476
- for (const key in conditions?.currencies) {
1477
- const c = conditions.currencies[key];
1478
- const playerCurrencyData = playerSnap.currencies?.[key];
1479
- if (c.min) {
1480
- const isDisqualify = (playerCurrencyData?.balance || 0) < c.min;
1481
- if (addDetails) {
1482
- conditionData.push({
1483
- isMet: !isDisqualify,
1484
- kind: 'currencies',
1485
- trackerAmount: playerCurrencyData?.balance || 0,
1486
- trackerGoal: c.min,
1487
- text: `Have more than ${c.min} ${c.name}`,
1488
- });
1489
- if (isDisqualify)
1490
- isValid = false;
1491
- }
1492
- else {
1493
- if (isDisqualify)
1494
- return { isValid: false };
1495
- }
1496
- }
1497
- if (c.max) {
1498
- const isDisqualify = (playerCurrencyData?.balance || 0) > c.max;
1499
- if (addDetails) {
1500
- conditionData.push({
1501
- isMet: !isDisqualify,
1502
- kind: 'currencies',
1503
- trackerAmount: playerCurrencyData?.balance || 0,
1504
- text: `Have less than ${c.max} ${c.name}`,
1505
- });
1506
- if (isDisqualify)
1507
- isValid = false;
1508
- }
1509
- else {
1510
- if (isDisqualify)
1511
- return { isValid: false };
1512
- }
1513
- }
1514
- if (c.in) {
1515
- const isDisqualify = (playerCurrencyData?.in || 0) < c.in;
1516
- if (addDetails) {
1517
- conditionData.push({
1518
- isMet: !isDisqualify,
1519
- kind: 'currencies',
1520
- trackerAmount: playerCurrencyData?.in || 0,
1521
- trackerGoal: c.in,
1522
- text: `Deposit at least ${c.in} ${c.name}`,
1523
- });
1524
- if (isDisqualify)
1525
- isValid = false;
1526
- }
1527
- else {
1528
- if (isDisqualify)
1529
- return { isValid: false };
1530
- }
1531
- }
1532
- if (c.out) {
1533
- const isDisqualify = (playerCurrencyData?.out || 0) < c.out;
1534
- if (addDetails) {
1535
- conditionData.push({
1536
- isMet: !isDisqualify,
1537
- kind: 'currencies',
1538
- trackerAmount: playerCurrencyData?.out || 0,
1539
- trackerGoal: c.out,
1540
- text: `Withdraw at least ${c.out} ${c.name}`,
1541
- });
1542
- if (isDisqualify)
1543
- isValid = false;
1544
- }
1545
- else {
1546
- if (isDisqualify)
1547
- return { isValid: false };
1548
- }
1549
- }
1550
- }
1551
- for (const key in conditions?.levels) {
1552
- const l = conditions.levels[key];
1553
- const playerLevelData = playerSnap.levels?.[key];
1554
- if (l.min) {
1555
- const isDisqualify = (playerLevelData?.level || 0) < l.min;
1556
- if (addDetails) {
1557
- conditionData.push({
1558
- isMet: !isDisqualify,
1559
- kind: 'levels',
1560
- trackerAmount: playerLevelData?.level || 0,
1561
- trackerGoal: l.min,
1562
- text: `Be above level ${l.min} ${l.name}`,
1563
- });
1564
- if (isDisqualify)
1565
- isValid = false;
1566
- }
1567
- else {
1568
- if (isDisqualify)
1569
- return { isValid: false };
1570
- }
1571
- }
1572
- if (l.max) {
1573
- const isDisqualify = (playerLevelData?.level || 0) > l.max;
1574
- if (addDetails) {
1575
- conditionData.push({
1576
- isMet: !isDisqualify,
1577
- kind: 'levels',
1578
- trackerAmount: playerLevelData?.level || 0,
1579
- text: `Be under level ${l.max} ${l.name}`,
1580
- });
1581
- if (isDisqualify)
1582
- isValid = false;
1583
- }
1584
- else {
1585
- if (isDisqualify)
1586
- return { isValid: false };
1587
- }
1588
- }
1589
- }
1590
- if (conditions?.quests) {
1591
- for (const questId in conditions.quests) {
1592
- const quest = conditions.quests[questId];
1593
- const playerQuestData = playerSnap.quests?.[questId];
1594
- const isDisqualify = playerQuestData
1595
- ? (playerQuestData?.completions || 0) < (quest.completions || 0)
1596
- : true; // if player has no data for this quest, they haven't completed it
1597
- if (addDetails) {
1598
- conditionData.push({
1599
- isMet: !isDisqualify,
1600
- kind: 'quests',
1601
- trackerAmount: playerQuestData?.completions || 0,
1602
- trackerGoal: quest.completions || 1,
1603
- text: quest.completions === 1
1604
- ? `Complete the quest ${quest.name}`
1605
- : (quest.completions || 0) < 1
1606
- ? `Start the quest ${quest.name}`
1607
- : `Complete the quest ${quest.name} ${quest.completions} times`,
1608
- });
1609
- if (isDisqualify)
1610
- isValid = false;
1611
- }
1612
- else {
1613
- if (isDisqualify)
1614
- return { isValid: false };
1615
- }
1616
- }
1617
- }
1618
- for (const key in conditions?.memberships) {
1619
- const m = conditions.memberships[key];
1620
- const playerMembershipsData = playerSnap.memberships?.[key];
1621
- if (m.minCount) {
1622
- const isDisqualify = (playerMembershipsData?.count || 0) < m.minCount;
1623
- if (addDetails) {
1624
- conditionData.push({
1625
- isMet: !isDisqualify,
1626
- kind: 'memberships',
1627
- trackerAmount: playerMembershipsData?.count || 0,
1628
- trackerGoal: m.minCount,
1629
- text: m.minCount > 1
1630
- ? `Have at least ${m.minCount} ${m.name} memberships`
1631
- : `Have a ${m.name} membership`,
1632
- });
1633
- if (isDisqualify)
1634
- isValid = false;
1635
- }
1636
- else {
1637
- if (isDisqualify)
1638
- return { isValid: false };
1639
- }
1640
- }
1641
- if (m.maxCount) {
1642
- const isDisqualify = (playerMembershipsData?.count || 0) > m.maxCount;
1643
- if (addDetails) {
1644
- conditionData.push({
1645
- isMet: !isDisqualify,
1646
- kind: 'memberships',
1647
- trackerAmount: playerMembershipsData?.count || 0,
1648
- text: `Have less than ${m.maxCount} ${m.name} memberships`,
1649
- });
1650
- if (isDisqualify)
1651
- isValid = false;
1652
- }
1653
- else {
1654
- if (isDisqualify)
1655
- return { isValid: false };
1656
- }
1657
- }
1658
- const timeOwned = (playerMembershipsData?.expiresAt || 0) - Date.now();
1659
- if (m.minMs) {
1660
- const isDisqualify = timeOwned < m.minMs;
1661
- if (addDetails) {
1662
- conditionData.push({
1663
- isMet: !isDisqualify,
1664
- kind: 'memberships',
1665
- trackerAmount: Number((timeOwned / (1000 * 60 * 60 * 24)).toFixed(1)),
1666
- trackerGoal: Number((m.minMs / (1000 * 60 * 60 * 24)).toFixed(1)),
1667
- text: `Own ${m.name} membership for at least ${(m.minMs /
1668
- (1000 * 60 * 60 * 24)).toFixed(1)} days`,
1669
- });
1670
- if (isDisqualify)
1671
- isValid = false;
1672
- }
1673
- else {
1674
- if (isDisqualify)
1675
- return { isValid: false };
1676
- }
1677
- }
1678
- if (m.maxMs) {
1679
- const isDisqualify = timeOwned > m.maxMs;
1680
- if (addDetails) {
1681
- conditionData.push({
1682
- isMet: !isDisqualify,
1683
- kind: 'memberships',
1684
- trackerAmount: Number((timeOwned / (1000 * 60 * 60 * 24)).toFixed(1)),
1685
- text: `Own ${m.name} membership for less than ${(m.maxMs /
1686
- (1000 * 60 * 60 * 24)).toFixed(1)} days`,
1687
- });
1688
- if (isDisqualify)
1689
- isValid = false;
1690
- }
1691
- else {
1692
- if (isDisqualify)
1693
- return { isValid: false };
1694
- }
1695
- }
1696
- }
1697
- for (const key in conditions?.stakedTokens) {
1698
- const s = conditions.stakedTokens[key];
1699
- const playerStakedData = playerSnap.stakedTokens?.[key];
1700
- if (s.min) {
1701
- const isDisqualify = (playerStakedData?.balance || 0) < s.min;
1702
- if (addDetails) {
1703
- conditionData.push({
1704
- isMet: !isDisqualify,
1705
- kind: 'stakedTokens',
1706
- trackerAmount: playerStakedData?.balance || 0,
1707
- trackerGoal: s.min,
1708
- text: `Have at least ${s.min} ${s.name} staked`,
1709
- });
1710
- if (isDisqualify)
1711
- isValid = false;
1712
- }
1713
- else {
1714
- if (isDisqualify)
1715
- return { isValid: false };
1716
- }
1717
- }
1718
- }
1719
- // Validate link count conditions
1720
- if (conditions?.links && 'entityLinks' in playerSnap) {
1721
- for (const [linkType, constraint] of Object.entries(conditions.links)) {
1722
- // linkType should always exist. and be default is none was specified
1723
- const linkCount = playerSnap.entityLinks?.filter((link) => (link.kind || DEFAULT_ENTITY_KIND) === linkType).length || 0;
1724
- if (constraint.min !== undefined) {
1725
- const isDisqualify = linkCount < constraint.min;
1726
- if (addDetails) {
1727
- conditionData.push({
1728
- isMet: !isDisqualify,
1729
- kind: 'links',
1730
- trackerAmount: linkCount,
1731
- trackerGoal: constraint.min,
1732
- text: constraint.template
1733
- ? renderTemplate(constraint.template, {
1734
- current: linkCount,
1735
- min: constraint.min ?? 0,
1736
- max: constraint.max ?? 0,
1737
- type: linkType,
1738
- })
1739
- : `At least ${constraint.min} ${linkType} link(s)`,
1740
- });
1741
- if (isDisqualify)
1742
- isValid = false;
1743
- }
1744
- else {
1745
- if (isDisqualify)
1746
- return { isValid: false };
1747
- }
1748
- }
1749
- if (constraint.max !== undefined) {
1750
- const isDisqualify = linkCount > constraint.max;
1751
- if (addDetails) {
1752
- conditionData.push({
1753
- isMet: !isDisqualify,
1754
- kind: 'links',
1755
- trackerAmount: linkCount,
1756
- text: constraint.template
1757
- ? renderTemplate(constraint.template, {
1758
- current: linkCount,
1759
- min: constraint.min ?? 0,
1760
- max: constraint.max ?? 0,
1761
- type: linkType,
1762
- })
1763
- : `At most ${constraint.max} ${linkType} link(s)`,
1764
- });
1765
- if (isDisqualify)
1766
- isValid = false;
1767
- }
1768
- else {
1769
- if (isDisqualify)
1770
- return { isValid: false };
1771
- }
1772
- }
1773
- }
1774
- }
1775
- // Evaluate dynamic conditions
1776
- if (conditions?.dynamic?.conditions?.length) {
1777
- const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamic.conditions, playerOffer?.trackers || {});
1778
- const dynamicResult = meetsDynamicConditions(playerSnap.dynamic, {
1779
- ...conditions.dynamic,
1780
- conditions: resolvedConditions,
1781
- });
1782
- if (addDetails) {
1783
- conditionData.push({
1784
- isMet: dynamicResult,
1785
- kind: 'dynamic',
1786
- text: renderTemplate(conditions.dynamic.template, playerSnap.dynamic) ||
1787
- 'Dynamic conditions',
1788
- });
1789
- if (!dynamicResult)
1790
- isValid = false;
1791
- }
1792
- else {
1793
- if (!dynamicResult)
1794
- return { isValid: false };
1795
- }
1796
- }
1797
- if (conditions?.identifiers?.platforms?.length && 'identifiers' in playerSnap) {
1798
- const playerPlatforms = new Set(playerSnap.identifiers?.map((i) => i.platform.toLowerCase()) || []);
1799
- const isAndBehaviour = conditions.identifiers.behaviour === 'AND';
1800
- const platformsToCheck = conditions.identifiers.platforms;
1801
- let isMet;
1802
- let displayText;
1803
- if (isAndBehaviour) {
1804
- isMet = platformsToCheck.every((platform) => playerPlatforms.has(platform.toLowerCase()));
1805
- displayText = `Link all: ${platformsToCheck.join(', ')}`;
1806
- }
1807
- else {
1808
- isMet = platformsToCheck.some((platform) => playerPlatforms.has(platform.toLowerCase()));
1809
- displayText = `Link any: ${platformsToCheck.join(', ')}`;
1810
- }
1811
- if (addDetails) {
1812
- conditionData.push({
1813
- isMet,
1814
- kind: 'identifiers',
1815
- trackerAmount: isMet ? 1 : 0,
1816
- trackerGoal: 1,
1817
- text: displayText,
1818
- });
1819
- if (!isMet)
1820
- isValid = false;
1821
- }
1822
- else {
1823
- if (!isMet)
1824
- return { isValid: false };
1825
- }
1826
- }
1827
- // Evaluate token balance conditions
1828
- for (const tokenCond of conditions?.tokenBalances || []) {
1829
- const contracts = tokenCond.contracts || [];
1830
- let totalBalance = 0;
1831
- const fetchedBalances = aggregateTokenBalances(additionalData);
1832
- for (const contract of contracts) {
1833
- const balanceKey = addressNetworkId(contract.contractAddress, contract.network);
1834
- totalBalance += fetchedBalances[balanceKey] || 0;
1835
- }
1836
- if (tokenCond.min !== undefined) {
1837
- const isDisqualify = totalBalance < tokenCond.min;
1838
- if (addDetails) {
1839
- conditionData.push({
1840
- isMet: !isDisqualify,
1841
- kind: 'tokenBalances',
1842
- trackerAmount: totalBalance,
1843
- trackerGoal: tokenCond.min,
1844
- text: `Have at least ${tokenCond.min} ${tokenCond.name || 'tokens'}`,
1845
- });
1846
- if (isDisqualify)
1847
- isValid = false;
1848
- }
1849
- else {
1850
- if (isDisqualify)
1851
- return { isValid: false };
1852
- }
1853
- }
1854
- if (tokenCond.max !== undefined) {
1855
- const isDisqualify = totalBalance > tokenCond.max;
1856
- if (addDetails) {
1857
- conditionData.push({
1858
- isMet: !isDisqualify,
1859
- kind: 'tokenBalances',
1860
- trackerAmount: totalBalance,
1861
- text: `Have at most ${tokenCond.max} ${tokenCond.name || 'tokens'}`,
1862
- });
1863
- if (isDisqualify)
1864
- isValid = false;
1865
- }
1866
- else {
1867
- if (isDisqualify)
1868
- return { isValid: false };
1869
- }
1870
- }
1871
- }
1872
- return { isValid, conditionData: addDetails ? conditionData : undefined };
1873
- };
1874
- const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, ignoreRequiredCompletions, additionalData, }) => {
1875
- if (surfacingConditions?.contexts?.length &&
1876
- !surfacingConditions.contexts?.includes(context || '')) {
1877
- // context is not in the list of surfacing contexts, so we don't want to surface this offer
1878
- return { isValid: false };
1879
- }
1880
- if (surfacingConditions?.targetEntityTypes?.length) {
1881
- const playerTarget = playerSnap.entityKind || DEFAULT_ENTITY_KIND;
1882
- // check if entity type is allowed
1883
- if (!surfacingConditions.targetEntityTypes.includes(playerTarget)) {
1884
- return { isValid: false };
1885
- }
1886
- }
1887
- const conditions = surfacingConditions;
1888
- if (conditions?.andTags?.length) {
1889
- // check if player has all of the tags
1890
- const hasAllTags = conditions.andTags.every((tag) => playerSnap.tags?.includes(tag));
1891
- if (!hasAllTags) {
1892
- return { isValid: false };
1893
- }
1894
- }
1895
- if (conditions?.orTags?.length) {
1896
- // check if player has any of the tags
1897
- const hasAnyTags = conditions.orTags.some((tag) => playerSnap.tags?.includes(tag));
1898
- if (!hasAnyTags) {
1899
- return { isValid: false };
1900
- }
1901
- }
1902
- if (conditions?.notTags?.length) {
1903
- // check if player has any of the tags
1904
- const hasAnyTags = conditions.notTags.some((tag) => playerSnap.tags?.includes(tag));
1905
- if (hasAnyTags) {
1906
- return { isValid: false };
1907
- }
1908
- }
1909
- if (conditions?.maxDaysInGame &&
1910
- (playerSnap.daysInGame ?? 0) > conditions.maxDaysInGame) {
1911
- return { isValid: false };
1912
- }
1913
- if (conditions.loginStreak && (playerSnap.loginStreak || 0) < conditions.loginStreak) {
1914
- return { isValid: false };
1915
- }
1916
- // Check dateSignedUp conditions
1917
- if (conditions.minDateSignedUp &&
1918
- (!playerSnap.dateSignedUp || playerSnap.dateSignedUp < conditions.minDateSignedUp)) {
1919
- return { isValid: false };
1920
- }
1921
- if (conditions.maxDateSignedUp &&
1922
- (!playerSnap.dateSignedUp || playerSnap.dateSignedUp > conditions.maxDateSignedUp)) {
1923
- return { isValid: false };
1924
- }
1925
- const completedOfferIds = new Set();
1926
- for (const pOffer of playerOffers?.values() || []) {
1927
- if (pOffer.status === 'claimed' || pOffer.status === 'claimable') {
1928
- completedOfferIds.add(pOffer.offer_id.toString());
1929
- }
1930
- }
1931
- if (conditions.completedOffers?.length) {
1932
- const hasCompletedAllOffers = conditions.completedOffers.every((offerId) => completedOfferIds.has(offerId));
1933
- if (!hasCompletedAllOffers && !ignoreRequiredCompletions) {
1934
- return { isValid: false };
1935
- }
1936
- }
1937
- if (conditions.allowedCountries?.length) {
1938
- const playerCountry = playerSnap.ip?.countryCode;
1939
- if (!playerCountry || !conditions.allowedCountries.includes(playerCountry)) {
1940
- return { isValid: false };
1941
- }
1942
- }
1943
- if (conditions.restrictedCountries?.length) {
1944
- const playerCountry = playerSnap.ip?.countryCode;
1945
- if (!playerCountry) {
1946
- return { isValid: false };
1947
- }
1948
- if (conditions.restrictedCountries.includes(playerCountry)) {
1949
- return { isValid: false };
1950
- }
1951
- }
1952
- if (conditions.networkRestrictions?.length) {
1953
- if (!playerSnap.ip) {
1954
- return { isValid: false };
1955
- }
1956
- for (const restriction of conditions.networkRestrictions) {
1957
- if (playerSnap.ip[restriction]) {
1958
- return { isValid: false };
1959
- }
1960
- }
1961
- }
1962
- return meetsBaseConditions({ conditions, playerSnap, additionalData });
1963
- };
1964
- const hasConditions = (conditions) => {
1965
- if (!conditions)
1966
- return false;
1967
- if (Object.keys(conditions.currencies || {}).length > 0)
1968
- return true;
1969
- if (Object.keys(conditions.levels || {}).length > 0)
1970
- return true;
1971
- if (Object.keys(conditions.stakedTokens || {}).length > 0)
1972
- return true;
1973
- if (Object.keys(conditions.memberships || {}).length > 0)
1974
- return true;
1975
- if (Object.keys(conditions.quests || {}).length > 0)
1976
- return true;
1977
- if (conditions.minTrustScore)
1978
- return true;
1979
- if (conditions.maxTrustScore)
1980
- return true;
1981
- if (conditions.achievements)
1982
- return true;
1983
- if (conditions.minDaysInGame)
1984
- return true;
1985
- if (conditions.dynamic?.conditions?.length)
1986
- return true;
1987
- if (conditions.identifiers?.platforms?.length)
1988
- return true;
1989
- const surCond = conditions;
1990
- if (surCond.contexts?.length)
1991
- return true;
1992
- if (surCond.andTags?.length)
1993
- return true;
1994
- if (surCond.orTags?.length)
1995
- return true;
1996
- if (surCond.notTags?.length)
1997
- return true;
1998
- if (surCond.maxDaysInGame)
1999
- return true;
2000
- if (surCond.loginStreak)
2001
- return true;
2002
- if (surCond.minDateSignedUp)
2003
- return true;
2004
- if (surCond.maxDateSignedUp)
2005
- return true;
2006
- if (surCond.completedOffers?.length)
2007
- return true;
2008
- if (surCond.programmatic)
2009
- return true;
2010
- if (surCond.targetEntityTypes?.length)
2011
- return true;
2012
- if (surCond.links && Object.keys(surCond.links).length > 0)
2013
- return true;
2014
- if (surCond.allowedCountries?.length)
2015
- return true;
2016
- if (surCond.restrictedCountries?.length)
2017
- return true;
2018
- if (surCond.networkRestrictions?.length)
2019
- return true;
2020
- if (surCond.linkedEntityOffers?.offer_id)
2021
- return true;
2022
- const compCond = conditions;
2023
- if (compCond.context)
2024
- return true;
2025
- if (compCond.buyItem)
2026
- return true;
2027
- if (compCond.spendCurrency)
2028
- return true;
2029
- if (compCond.depositCurrency)
2030
- return true;
2031
- if (compCond.social)
2032
- return true;
2033
- if (compCond.login)
2034
- return true;
2035
- if (compCond.loginStreak)
2036
- return true;
2037
- if (compCond.linkedCompletions)
2038
- return true;
2039
- if (compCond.dynamicTracker?.conditions?.length)
2040
- return true;
2041
- if (conditions.tokenBalances?.length)
2042
- return true;
2043
- if (Object.keys(compCond.contractInteractions || {}).length > 0)
2044
- return true;
2045
- return false;
2046
- };
2047
- const meetsLinkedEntityOffersCondition = ({ linkedEntityOffers, matchingLinks, linkedPOfferMap, }) => {
2048
- if (!linkedPOfferMap)
2049
- return { isValid: false };
2050
- const linkedPlayerOffer_ids = [];
2051
- for (const link of matchingLinks) {
2052
- const key = `${link.playerId}:${linkedEntityOffers.offer_id}`;
2053
- const po = linkedPOfferMap.get(key);
2054
- if (po) {
2055
- linkedPlayerOffer_ids.push(po._id.toString());
2056
- }
2057
- }
2058
- if (linkedPlayerOffer_ids.length > 0) {
2059
- return { isValid: true, linkedPlayerOffer_ids };
2060
- }
2061
- return { isValid: false };
2062
- };
2063
- const offerMeetsCompletionConditions = (offer, snapshot, additionalData) => {
2064
- return meetsCompletionConditions({
2065
- completionConditions: offer.completionConditions || {},
2066
- completionTrackers: offer.completionTrackers,
2067
- playerSnap: snapshot,
2068
- playerOffer: offer,
2069
- addDetails: true,
2070
- maxClaimCount: offer.maxClaimCount,
2071
- additionalData,
2072
- });
2073
- };
2074
- const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, additionalData, }) => {
2075
- if (completionConditions) {
2076
- const conditions = completionConditions;
2077
- // For multi-claim offers, scale cumulative requirements by (claimedCount + 1)
2078
- const shouldScale = maxClaimCount === -1 || (maxClaimCount && maxClaimCount > 1);
2079
- const claimMultiplier = shouldScale
2080
- ? (playerOffer.trackers?.claimedCount || 0) + 1
2081
- : 1;
2082
- const conditionData = [];
2083
- let isValid = true;
2084
- let maxTotalClaimsFromScaling = Infinity;
2085
- const updateMax = (limit) => (maxTotalClaimsFromScaling = Math.min(maxTotalClaimsFromScaling, limit));
2086
- if (completionConditions?.context?.id) {
2087
- const hasTrackedContext = completionTrackers?.context &&
2088
- completionConditions.context.id === completionTrackers.context;
2089
- const isDisqualify = !hasTrackedContext;
2090
- if (addDetails) {
2091
- conditionData.push({
2092
- isMet: !isDisqualify,
2093
- kind: 'context',
2094
- trackerAmount: hasTrackedContext ? 1 : 0,
2095
- trackerGoal: 1,
2096
- text: completionConditions.context.name,
2097
- });
2098
- if (isDisqualify)
2099
- isValid = false;
2100
- }
2101
- else {
2102
- if (isDisqualify)
2103
- return { isValid: false, availableClaimsNow: 0 };
2104
- }
2105
- }
2106
- if (conditions?.buyItem) {
2107
- const baseAmount = conditions.buyItem.amount || 1;
2108
- const scaledAmount = baseAmount * claimMultiplier;
2109
- const trackerValue = completionTrackers?.buyItem || 0;
2110
- const isDisqualify = trackerValue < scaledAmount;
2111
- if (shouldScale && baseAmount > 0) {
2112
- updateMax(Math.floor(trackerValue / baseAmount));
2113
- }
2114
- if (addDetails) {
2115
- conditionData.push({
2116
- isMet: !isDisqualify,
2117
- kind: 'buyItem',
2118
- trackerAmount: trackerValue,
2119
- trackerGoal: scaledAmount,
2120
- text: `Buy ${scaledAmount} ${conditions.buyItem.name}`,
2121
- });
2122
- if (isDisqualify)
2123
- isValid = false;
2124
- }
2125
- else {
2126
- if (isDisqualify)
2127
- return { isValid: false, availableClaimsNow: 0 };
2128
- }
2129
- }
2130
- if (conditions?.spendCurrency) {
2131
- const baseAmount = conditions.spendCurrency.amount || 1;
2132
- const scaledAmount = baseAmount * claimMultiplier;
2133
- const trackerValue = completionTrackers?.spendCurrency || 0;
2134
- const isDisqualify = trackerValue < scaledAmount;
2135
- if (shouldScale && baseAmount > 0) {
2136
- updateMax(Math.floor(trackerValue / baseAmount));
2137
- }
2138
- if (addDetails) {
2139
- conditionData.push({
2140
- isMet: !isDisqualify,
2141
- kind: 'spendCurrency',
2142
- trackerAmount: trackerValue,
2143
- trackerGoal: scaledAmount,
2144
- text: `Spend ${scaledAmount} ${conditions.spendCurrency.name}`,
2145
- });
2146
- if (isDisqualify)
2147
- isValid = false;
2148
- }
2149
- else {
2150
- if (isDisqualify)
2151
- return { isValid: false, availableClaimsNow: 0 };
2152
- }
2153
- }
2154
- if (conditions?.depositCurrency) {
2155
- const baseAmount = conditions.depositCurrency.amount || 1;
2156
- const scaledAmount = baseAmount * claimMultiplier;
2157
- const trackerValue = completionTrackers?.depositCurrency || 0;
2158
- const isDisqualify = trackerValue < scaledAmount;
2159
- if (shouldScale && baseAmount > 0) {
2160
- updateMax(Math.floor(trackerValue / baseAmount));
2161
- }
2162
- if (addDetails) {
2163
- conditionData.push({
2164
- isMet: !isDisqualify,
2165
- kind: 'depositCurrency',
2166
- trackerAmount: trackerValue,
2167
- trackerGoal: scaledAmount,
2168
- text: `Deposit ${scaledAmount} ${conditions.depositCurrency.name}`,
2169
- });
2170
- if (isDisqualify)
2171
- isValid = false;
2172
- }
2173
- else {
2174
- if (isDisqualify)
2175
- return { isValid: false, availableClaimsNow: 0 };
2176
- }
2177
- }
2178
- if (conditions?.login) {
2179
- const isMet = new Date(playerSnap.snapshotLastUpdated || 0).getTime() >
2180
- new Date(playerOffer.createdAt || 0).getTime();
2181
- if (addDetails) {
2182
- conditionData.push({
2183
- isMet,
2184
- kind: 'login',
2185
- trackerAmount: isMet ? 1 : 0,
2186
- trackerGoal: 1,
2187
- text: `Login to the game`,
2188
- });
2189
- if (!isMet)
2190
- isValid = false;
2191
- }
2192
- else {
2193
- if (!isMet)
2194
- return { isValid: false, availableClaimsNow: 0 };
2195
- }
2196
- }
2197
- if (conditions?.loginStreak) {
2198
- // player's login streak snapshot right now - their login streak when offer was surfaced = their login streak since the offer was surfaced
2199
- // if their login streak since the offer was surfaced is less than the required login streak, then they are not yet eligible for the offer
2200
- const streakSinceOffer = (playerSnap.loginStreak || 0) - (completionTrackers?.currentLoginStreak || 0);
2201
- const isDisqualify = streakSinceOffer + 1 < conditions.loginStreak;
2202
- if (addDetails) {
2203
- conditionData.push({
2204
- isMet: !isDisqualify,
2205
- kind: 'loginStreak',
2206
- trackerAmount: streakSinceOffer + 1,
2207
- trackerGoal: conditions.loginStreak,
2208
- text: `Login streak of ${conditions.loginStreak || 0} days`,
2209
- });
2210
- if (isDisqualify)
2211
- isValid = false;
2212
- }
2213
- else {
2214
- if (isDisqualify)
2215
- return { isValid: false, availableClaimsNow: 0 };
2216
- }
2217
- }
2218
- if (conditions?.social) {
2219
- const tSocialAccumulate = completionTrackers?.social;
2220
- const tSocialAttach = completionTrackers?.social;
2221
- const cSocial = completionConditions.social;
2222
- const mode = cSocial?.mode || 'attach';
2223
- const tSocial = mode === 'accumulate' ? tSocialAccumulate : tSocialAttach;
2224
- const hasContent = Boolean(mode === 'accumulate'
2225
- ? tSocialAccumulate?.matchCount > 0
2226
- : tSocialAttach?.videoId);
2227
- // Only scale social metrics in accumulate mode (attach mode is single content)
2228
- const socialMultiplier = mode === 'accumulate' ? claimMultiplier : 1;
2229
- const minLikes = (cSocial?.minLikes || 0) * socialMultiplier;
2230
- const minViews = (cSocial?.minViews || 0) * socialMultiplier;
2231
- const minComments = (cSocial?.minComments || 0) * socialMultiplier;
2232
- const likes = tSocial?.likes || 0;
2233
- const views = tSocial?.views || 0;
2234
- const comments = tSocial?.comments || 0;
2235
- let isDisqualify = !hasContent;
2236
- if (likes < minLikes || views < minViews || comments < minComments) {
2237
- isDisqualify = true;
2238
- }
2239
- if (shouldScale && mode === 'accumulate' && hasContent) {
2240
- const baseLikes = cSocial?.minLikes || 0;
2241
- const baseViews = cSocial?.minViews || 0;
2242
- const baseComments = cSocial?.minComments || 0;
2243
- if (baseLikes > 0)
2244
- updateMax(Math.floor(likes / baseLikes));
2245
- if (baseViews > 0)
2246
- updateMax(Math.floor(views / baseViews));
2247
- if (baseComments > 0)
2248
- updateMax(Math.floor(comments / baseComments));
2249
- }
2250
- if (addDetails) {
2251
- const platformMap = {
2252
- tiktok: 'TikTok',
2253
- instagram: 'Instagram',
2254
- youtube: 'YouTube',
2255
- };
2256
- const platformText = conditions.social.platforms
2257
- .map((platform) => platformMap[platform])
2258
- .join(' | ');
2259
- const requiredWords = cSocial?.requiredWords ?? [];
2260
- if (mode === 'accumulate') {
2261
- const matchCount = tSocialAccumulate?.matchCount || 0;
2262
- conditionData.push({
2263
- isMet: hasContent,
2264
- kind: 'social',
2265
- trackerAmount: matchCount,
2266
- trackerGoal: 1,
2267
- text: hasContent
2268
- ? `Found ${matchCount} matching ${platformText} post${matchCount !== 1 ? 's' : ''}`
2269
- : requiredWords.length > 0
2270
- ? `Post ${platformText} content with ${requiredWords.map((w) => `"${w}"`).join(', ')}`
2271
- : `Post ${platformText} content`,
2272
- });
2273
- }
2274
- else {
2275
- const title = tSocialAttach?.title;
2276
- conditionData.push({
2277
- isMet: hasContent,
2278
- kind: 'social',
2279
- trackerAmount: hasContent ? 1 : 0,
2280
- trackerGoal: 1,
2281
- text: !hasContent
2282
- ? requiredWords.length > 0
2283
- ? `Attach a ${platformText} post with ${requiredWords.map((w) => `"${w}"`).join(', ')} in the title`
2284
- : `Attach a ${platformText} post`
2285
- : `Attached: ${title}`,
2286
- });
2287
- }
2288
- if (minLikes > 0) {
2289
- conditionData.push({
2290
- isMet: hasContent && likes >= minLikes,
2291
- kind: 'social',
2292
- trackerAmount: likes,
2293
- trackerGoal: minLikes,
2294
- text: mode === 'accumulate'
2295
- ? `Combined ${minLikes} Likes`
2296
- : `Reach ${minLikes} Likes`,
2297
- });
2298
- }
2299
- if (minViews > 0) {
2300
- conditionData.push({
2301
- isMet: hasContent && views >= minViews,
2302
- kind: 'social',
2303
- trackerAmount: views,
2304
- trackerGoal: minViews,
2305
- text: mode === 'accumulate'
2306
- ? `Combined ${minViews} Views`
2307
- : `Reach ${minViews} Views`,
2308
- });
2309
- }
2310
- if (minComments > 0) {
2311
- conditionData.push({
2312
- isMet: hasContent && comments >= minComments,
2313
- kind: 'social',
2314
- trackerAmount: comments,
2315
- trackerGoal: minComments,
2316
- text: mode === 'accumulate'
2317
- ? `Combined ${minComments} Comments`
2318
- : `Reach ${minComments} Comments`,
2319
- });
2320
- }
2321
- if (isDisqualify)
2322
- isValid = false;
2323
- }
2324
- else {
2325
- if (isDisqualify)
2326
- return { isValid: false, availableClaimsNow: 0 };
2327
- }
2328
- }
2329
- // Linked completions - wait for N linked entities to complete
2330
- if (conditions?.linkedCompletions?.min) {
2331
- const baseMin = conditions.linkedCompletions.min;
2332
- const currentCount = completionTrackers?.linkedCompletions || 0;
2333
- const scaledMin = baseMin * claimMultiplier;
2334
- const isDisqualify = currentCount < scaledMin;
2335
- if (shouldScale && baseMin > 0) {
2336
- updateMax(Math.floor(currentCount / baseMin));
2337
- }
2338
- if (addDetails) {
2339
- conditionData.push({
2340
- isMet: !isDisqualify,
2341
- kind: 'linkedCompletions',
2342
- trackerAmount: currentCount,
2343
- trackerGoal: scaledMin,
2344
- text: conditions.linkedCompletions.template
2345
- ? renderTemplate(conditions.linkedCompletions.template, {
2346
- current: currentCount,
2347
- required: scaledMin,
2348
- })
2349
- : `Wait for ${scaledMin} linked ${scaledMin === 1 ? 'entity' : 'entities'} to complete`,
2350
- });
2351
- if (isDisqualify)
2352
- isValid = false;
2353
- }
2354
- else {
2355
- if (isDisqualify)
2356
- return { isValid: false, availableClaimsNow: 0 };
2357
- }
2358
- }
2359
- if (conditions?.dynamicTracker?.conditions?.length) {
2360
- const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamicTracker.conditions, playerOffer?.trackers || {});
2361
- // now we have the game-defined conditions with {{}} keys populated. feed these conditions into evaluator
2362
- const dynamicResult = meetsDynamicConditions(dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {}), {
2363
- ...conditions.dynamicTracker,
2364
- conditions: resolvedConditions,
2365
- }, claimMultiplier);
2366
- if (shouldScale) {
2367
- const dynamicMax = getMaxClaimsForDynamicGroup(dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {}), {
2368
- ...conditions.dynamicTracker,
2369
- conditions: resolvedConditions,
2370
- }, playerOffer?.trackers?.claimedCount || 0);
2371
- updateMax(dynamicMax);
2372
- }
2373
- if (addDetails) {
2374
- conditionData.push({
2375
- isMet: dynamicResult,
2376
- kind: 'dynamicTracker',
2377
- text: renderTemplate(conditions.dynamicTracker.template, dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {})) || 'Dynamic conditions',
2378
- });
2379
- if (!dynamicResult)
2380
- isValid = false;
2381
- }
2382
- else {
2383
- if (!dynamicResult)
2384
- return { isValid: false, availableClaimsNow: 0 };
2385
- }
2386
- }
2387
- // Evaluate contractInteractions completion trackers
2388
- if (conditions?.contractInteractions) {
2389
- for (const [conditionId, condition] of Object.entries(conditions.contractInteractions)) {
2390
- const baseAmount = condition.amount || 0;
2391
- const scaledAmount = baseAmount * claimMultiplier;
2392
- const trackerValue = completionTrackers?.contractInteractions?.[conditionId] || 0;
2393
- const isDisqualify = trackerValue < scaledAmount;
2394
- if (shouldScale && baseAmount > 0) {
2395
- updateMax(Math.floor(trackerValue / baseAmount));
2396
- }
2397
- if (addDetails) {
2398
- const displayText = renderTemplate(condition.template, {
2399
- current: trackerValue,
2400
- amount: scaledAmount,
2401
- });
2402
- conditionData.push({
2403
- isMet: !isDisqualify,
2404
- kind: 'contractInteractions',
2405
- trackerAmount: trackerValue,
2406
- trackerGoal: scaledAmount,
2407
- text: displayText,
2408
- });
2409
- if (isDisqualify)
2410
- isValid = false;
2411
- }
2412
- else {
2413
- if (isDisqualify)
2414
- return { isValid: false, availableClaimsNow: 0 };
2415
- }
2416
- }
2417
- }
2418
- const r = meetsBaseConditions({
2419
- conditions,
2420
- playerSnap,
2421
- addDetails: true,
2422
- playerOffer,
2423
- additionalData,
2424
- });
2425
- isValid = isValid && r.isValid;
2426
- conditionData.push(...(r.conditionData || []));
2427
- if (maxClaimCount && maxClaimCount > 0) {
2428
- updateMax(maxClaimCount);
2429
- }
2430
- const claimedCount = playerOffer?.trackers?.claimedCount || 0;
2431
- const availableClaimsNow = !isValid
2432
- ? 0
2433
- : maxTotalClaimsFromScaling === Infinity
2434
- ? -1
2435
- : Math.max(0, maxTotalClaimsFromScaling - claimedCount);
2436
- return { isValid, conditionData, availableClaimsNow };
2437
- }
2438
- return { isValid: true, conditionData: [], availableClaimsNow: -1 };
2439
- };
2440
- /**
2441
- * Checks if completion conditions were met before a specific expiry time.
2442
- * Returns true if all relevant condition fields were updated before expiryTime.
2443
- *
2444
- * @param completionConditions - The completion conditions to check
2445
- * @param completionTrackers - The completion trackers (for buyItem, spendCurrency, etc.)
2446
- * @param playerSnap - The player snapshot with field timestamps
2447
- * @returns true if all conditions were met before expiry, false otherwise
2448
- */
2449
- const meetsCompletionConditionsBeforeExpiry = ({ completionConditions, completionTrackers, playerSnap, playerOffer, maxClaimCount, }) => {
2450
- if (!completionConditions)
2451
- return false;
2452
- // Check if there are actually any conditions to evaluate
2453
- if (!hasConditions(completionConditions))
2454
- return false;
2455
- // First check if conditions are actually met
2456
- const conditionsMet = meetsCompletionConditions({
2457
- completionConditions,
2458
- completionTrackers,
2459
- playerOffer,
2460
- playerSnap,
2461
- maxClaimCount,
2462
- });
2463
- if (!conditionsMet.isValid)
2464
- return false;
2465
- if (!playerOffer.expiresAt)
2466
- return true;
2467
- const expiryTime = new Date(playerOffer.expiresAt).getTime();
2468
- const lastSnapshotUpdate = new Date(playerSnap.snapshotLastUpdated).getTime();
2469
- /**
2470
- * Checks if a field was updated after the expiry time.
2471
- * Returns true if updated AFTER or AT expiry (violates grace period).
2472
- * Returns false if updated BEFORE expiry (allows grace period).
2473
- */
2474
- function wasUpdatedAfterExpiry(data) {
2475
- let lastUpdated;
2476
- if (typeof data === 'object' && data !== null && !(data instanceof Date)) {
2477
- // Object with optional lastUpdated field
2478
- lastUpdated = data.lastUpdated
2479
- ? new Date(data.lastUpdated).getTime()
2480
- : lastSnapshotUpdate;
2481
- }
2482
- else if (data instanceof Date) {
2483
- lastUpdated = data.getTime();
2484
- }
2485
- else if (typeof data === 'string' || typeof data === 'number') {
2486
- lastUpdated = new Date(data).getTime();
2487
- }
2488
- else {
2489
- // No data provided, use snapshot timestamp
2490
- lastUpdated = lastSnapshotUpdate;
2491
- }
2492
- return lastUpdated >= expiryTime;
2493
- }
2494
- if (completionConditions.currencies) {
2495
- for (const currencyId in completionConditions.currencies) {
2496
- const currency = playerSnap.currencies?.[currencyId];
2497
- if (!currency)
2498
- continue;
2499
- if (wasUpdatedAfterExpiry(currency))
2500
- return false;
2501
- }
2502
- }
2503
- if (completionConditions.levels) {
2504
- for (const skillId in completionConditions.levels) {
2505
- const level = playerSnap.levels?.[skillId];
2506
- if (!level)
2507
- continue;
2508
- if (wasUpdatedAfterExpiry(level))
2509
- return false;
2510
- }
2511
- }
2512
- if (completionConditions.quests) {
2513
- for (const questId in completionConditions.quests) {
2514
- const quest = playerSnap.quests?.[questId];
2515
- if (!quest)
2516
- continue;
2517
- if (wasUpdatedAfterExpiry(quest))
2518
- return false;
2519
- }
2520
- }
2521
- if (completionConditions.memberships) {
2522
- for (const membershipId in completionConditions.memberships) {
2523
- const membership = playerSnap.memberships?.[membershipId];
2524
- if (!membership)
2525
- continue;
2526
- if (wasUpdatedAfterExpiry(membership))
2527
- return false;
2528
- }
2529
- }
2530
- if (completionConditions.achievements) {
2531
- for (const achievementId in completionConditions.achievements) {
2532
- const achievement = playerSnap.achievements?.[achievementId];
2533
- if (!achievement)
2534
- continue;
2535
- if (wasUpdatedAfterExpiry(achievement))
2536
- return false;
2537
- }
2538
- }
2539
- if (completionConditions.stakedTokens) {
2540
- for (const tokenId in completionConditions.stakedTokens) {
2541
- const stakedToken = playerSnap.stakedTokens?.[tokenId];
2542
- if (!stakedToken)
2543
- continue;
2544
- const lastStakeTime = new Date(stakedToken.lastStake ?? 0).getTime();
2545
- const lastUnstakeTime = new Date(stakedToken.lastUnstake ?? 0).getTime();
2546
- const lastUpdated = Math.max(lastStakeTime, lastUnstakeTime);
2547
- if (lastUpdated >= expiryTime)
2548
- return false;
2549
- }
2550
- }
2551
- if (completionConditions.minTrustScore !== undefined ||
2552
- completionConditions.maxTrustScore !== undefined) {
2553
- if (wasUpdatedAfterExpiry(playerSnap.trustLastUpdated))
2554
- return false;
2555
- }
2556
- if (completionConditions.minDaysInGame !== undefined) {
2557
- if (wasUpdatedAfterExpiry(playerSnap.daysInGameLastUpdated))
2558
- return false;
2559
- }
2560
- if (completionConditions.login || completionConditions.loginStreak) {
2561
- if (wasUpdatedAfterExpiry())
2562
- return false;
2563
- }
2564
- if (completionConditions.social) {
2565
- // Check if social content was attached/validated after expiry
2566
- if (completionTrackers?.social?.lastChecked) {
2567
- if (wasUpdatedAfterExpiry(completionTrackers.social.lastChecked))
2568
- return false;
2569
- }
2570
- }
2571
- // All conditions were met before expiry
2572
- return true;
2573
- };
2574
- /**
2575
- * Checks if a dynamic object meets a set of dynamic field conditions.
2576
- * @param dynamicObj - The object with any key and string or number value.
2577
- * @param conditions - Array of conditions to check.
2578
- * @param claimMultiplier - Multiplier to scale conditions (used for numeric comparisons).
2579
- * @returns true if all conditions are met, false otherwise.
2580
- */
2581
- /**
2582
- * Evaluates a single dynamic condition against the dynamic object.
2583
- */
2584
- function evaluateDynamicCondition(dynamicObj, cond, claimMultiplier = 1) {
2585
- if (!dynamicObj)
2586
- return false;
2587
- const val = dynamicObj[cond.key];
2588
- if (val == undefined)
2589
- return false;
2590
- const isNumber = typeof val === 'number';
2591
- const isBoolean = typeof val === 'boolean';
2592
- if (isBoolean) {
2593
- switch (cond.operator) {
2594
- case '==':
2595
- return val === Boolean(cond.compareTo);
2596
- case '!=':
2597
- return val !== Boolean(cond.compareTo);
2598
- default:
2599
- return false;
2600
- }
2601
- }
2602
- const compareTo = isNumber ? Number(cond.compareTo) : String(cond.compareTo);
2603
- if (isNumber && typeof compareTo === 'number') {
2604
- const skipMultiplier = cond.operator === '==' || cond.operator === '!=';
2605
- const scaledCompareTo = skipMultiplier ? compareTo : compareTo * claimMultiplier;
2606
- switch (cond.operator) {
2607
- case '==':
2608
- return val === scaledCompareTo;
2609
- case '!=':
2610
- return val !== scaledCompareTo;
2611
- case '>':
2612
- return val > scaledCompareTo;
2613
- case '>=':
2614
- return val >= scaledCompareTo;
2615
- case '<':
2616
- return val < scaledCompareTo;
2617
- case '<=':
2618
- return val <= scaledCompareTo;
2619
- }
2620
- }
2621
- else if (!isNumber && typeof compareTo === 'string') {
2622
- switch (cond.operator) {
2623
- case '==':
2624
- return val === compareTo;
2625
- case '!=':
2626
- return val !== compareTo;
2627
- case 'has':
2628
- return val.includes(compareTo);
2629
- case 'not_has':
2630
- return !val.includes(compareTo);
2631
- }
2632
- }
2633
- return false;
2634
- }
2635
- /**
2636
- * Calculates the maximum number of claims supported by a single dynamic condition.
2637
- */
2638
- function getMaxClaimsForDynamicCondition(dynamicObj, cond) {
2639
- if (!dynamicObj)
2640
- return 0;
2641
- const val = dynamicObj[cond.key];
2642
- if (val === undefined)
2643
- return 0;
2644
- if (typeof val === 'number') {
2645
- const base = Number(cond.compareTo);
2646
- if (isNaN(base)) {
2647
- return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
2648
- }
2649
- switch (cond.operator) {
2650
- case '>=':
2651
- if (base === 0)
2652
- return val >= 0 ? Infinity : 0;
2653
- if (base < 0)
2654
- return val >= base ? Infinity : 0;
2655
- return Math.max(0, Math.floor(val / base));
2656
- case '>':
2657
- if (base === 0)
2658
- return val > 0 ? Infinity : 0;
2659
- if (base < 0)
2660
- return val > base ? Infinity : 0;
2661
- if (val <= 0)
2662
- return 0;
2663
- return Math.max(0, Math.ceil(val / base) - 1);
2664
- case '==':
2665
- return val === base ? Infinity : 0;
2666
- case '!=':
2667
- return val !== base ? Infinity : 0;
2668
- case '<=':
2669
- if (base === 0)
2670
- return val <= 0 ? Infinity : 0;
2671
- if (base > 0)
2672
- return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
2673
- if (val >= 0)
2674
- return 0;
2675
- return Math.max(0, Math.floor(val / base));
2676
- case '<':
2677
- if (base === 0)
2678
- return val < 0 ? Infinity : 0;
2679
- if (base > 0)
2680
- return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
2681
- if (val >= 0)
2682
- return 0;
2683
- return Math.max(0, Math.ceil(val / base) - 1);
2684
- }
2685
- }
2686
- // we don't scale the rest, they are always true or always false
2687
- return evaluateDynamicCondition(dynamicObj, cond, 1) ? Infinity : 0;
2688
- }
2689
- /**
2690
- * Calculates the maximum number of claims supported by a group of dynamic conditions.
2691
- */
2692
- function getMaxClaimsForDynamicGroup(dynamicObj, dynamicGroup, currentClaimCount = 0) {
2693
- const { conditions, links } = dynamicGroup;
2694
- if (!conditions || conditions.length === 0)
2695
- return Infinity;
2696
- // AND only
2697
- if (!links || links.length === 0 || links.every((l) => l === 'AND')) {
2698
- let minClaims = Infinity;
2699
- for (const cond of conditions) {
2700
- const max = getMaxClaimsForDynamicCondition(dynamicObj, cond);
2701
- if (max === 0)
2702
- return 0;
2703
- minClaims = Math.min(minClaims, max);
2704
- }
2705
- return minClaims;
2706
- }
2707
- // OR only
2708
- if (links.every((l) => l === 'OR')) {
2709
- let maxClaims = 0;
2710
- for (const cond of conditions) {
2711
- const max = getMaxClaimsForDynamicCondition(dynamicObj, cond);
2712
- if (max === Infinity)
2713
- return Infinity;
2714
- maxClaims = Math.max(maxClaims, max);
2715
- }
2716
- return maxClaims;
2717
- }
2718
- // mixed:
2719
- const maxIterations = 100;
2720
- for (let n = currentClaimCount + 1; n <= currentClaimCount + maxIterations; n++) {
2721
- if (!meetsDynamicConditions(dynamicObj, dynamicGroup, n)) {
2722
- return n - 1;
2723
- }
2724
- }
2725
- return currentClaimCount + maxIterations;
2726
- }
2727
- /**
2728
- * Evaluates a group of dynamic conditions with logical links (AND, OR, AND NOT).
2729
- * @param dynamicObj - The player's dynamic object with any key and string or number value.
2730
- * @param dynamicGroup - The group of conditions and links to check.
2731
- * @param claimMultiplier - Multiplier to scale conditions (used for numeric comparisons).
2732
- * @returns true if the group evaluates to true, false otherwise.
2733
- */
2734
- function meetsDynamicConditions(dynamicObj, dynamicGroup, claimMultiplier = 1) {
2735
- const { conditions, links } = dynamicGroup;
2736
- if (!conditions || conditions.length === 0)
2737
- return true;
2738
- if (!dynamicObj)
2739
- return false;
2740
- // If no links, treat as AND between all conditions
2741
- if (!links || links.length === 0) {
2742
- return conditions.every((cond) => evaluateDynamicCondition(dynamicObj, cond, claimMultiplier));
2743
- }
2744
- // Evaluate the first condition
2745
- let result = evaluateDynamicCondition(dynamicObj, conditions[0], claimMultiplier);
2746
- for (let i = 0; i < links.length; i++) {
2747
- const nextCond = evaluateDynamicCondition(dynamicObj, conditions[i + 1], claimMultiplier);
2748
- const link = links[i];
2749
- if (link === 'AND') {
2750
- result = result && nextCond;
2751
- }
2752
- else if (link === 'OR') {
2753
- result = result || nextCond;
2754
- }
2755
- else if (link === 'AND NOT') {
2756
- result = result && !nextCond;
2757
- }
2758
- }
2759
- return result;
2760
- }
2761
- /**
2762
- * Checks if a PlayerOffer meets its claimable conditions (completed -> claimable transition).
2763
- * @param claimableConditions - The offer's claimableConditions (from IOffer)
2764
- * @param claimableTrackers - The player offer's claimableTrackers
2765
- */
2766
- function meetsClaimableConditions({ claimableConditions, playerOfferTrackers, claimableTrackers, }) {
2767
- if (!claimableConditions) {
2768
- return { isValid: true };
2769
- }
2770
- if (claimableConditions.siblingCompletions) {
2771
- const siblingCount = playerOfferTrackers?.siblingPlayerOffer_ids?.length ?? 0;
2772
- let completedCount = claimableTrackers?.siblingCompletions ?? 0;
2773
- if (completedCount == -1)
2774
- completedCount = siblingCount; // treat -1 as all completed
2775
- // if siblings exist but not all are completed, return false
2776
- if (siblingCount > 0 && completedCount < siblingCount) {
2777
- return { isValid: false };
2778
- }
2779
- }
2780
- return { isValid: true };
2781
- }
2782
- // returns contractAddress:network -> balance
2783
- function aggregateTokenBalances(data) {
2784
- const aggregatedBalances = {};
2785
- for (const { balances } of data?.cryptoWallets || []) {
2786
- for (const [key, balance] of Object.entries(balances)) {
2787
- if (!aggregatedBalances[key]) {
2788
- aggregatedBalances[key] = 0;
2789
- }
2790
- aggregatedBalances[key] += balance;
2791
- }
2792
- }
2793
- return aggregatedBalances;
2794
- }
2795
-
2796
- const offerListenerEvents = ['claim_offer'];
2797
- const PlayerOfferStatuses = [
2798
- // 'inQueue', // fuck this shit. just don't surface offers if their offer plate is full.
2799
- 'surfaced',
2800
- 'viewed',
2801
- 'completed', // Individual completionConditions met, waiting for claimableConditions (e.g., siblings)
2802
- 'claimable',
2803
- 'claimed',
2804
- 'expired',
2805
- ];
2806
-
2807
- // Use a const assertion for the array and infer the union type directly
2808
- const rewardKinds = [
2809
- 'item',
2810
- 'coins',
2811
- 'exp',
2812
- 'trust_points',
2813
- 'loyalty_currency', // loyalty currency that the player can exchange for rewards like on-chain via withdraw, etc.
2814
- 'discount', // handled by the external dev, using the rewardId to identify what it is for in their system
2815
- /** on-chain rewards require the builder to send funds to a custodial wallet that we use to send to player wallets*/
2816
- ];
2817
- const rewardSchema = {
2818
- _id: false,
2819
- kind: { type: String, enum: rewardKinds },
2820
- rewardId: {
2821
- type: String,
2822
- validate: {
2823
- validator: function (value) {
2824
- // Require rewardId for item, coins, loyalty_currency, exp, and discount kinds
2825
- const requiresRewardId = ['item', 'coins', 'loyalty_currency', 'exp', 'discount'].includes(this.kind);
2826
- if (requiresRewardId) {
2827
- return !!value;
2828
- }
2829
- return true;
2830
- },
2831
- message: 'rewardId is required for reward kinds: item, coins, loyalty_currency, exp, and discount',
2832
- },
2833
- },
2834
- skillId: String,
2835
- currencyId: String, // could be a loyalty currency
2836
- itemId: String,
2837
- amount: Number,
2838
- name: String,
2839
- image: String,
2840
- };
2841
-
2842
- exports.AssetHelper = AssetHelper;
2843
- exports.DEFAULT_ENTITY_KIND = DEFAULT_ENTITY_KIND;
2844
1343
  exports.EventEmitter = EventEmitter;
2845
1344
  exports.OfferStore = OfferStore;
2846
1345
  exports.OfferwallClient = OfferwallClient;
2847
- exports.PlayerOfferStatuses = PlayerOfferStatuses;
2848
1346
  exports.SSEConnection = SSEConnection;
2849
- exports.aggregateTokenBalances = aggregateTokenBalances;
2850
- exports.getMaxClaimsForDynamicCondition = getMaxClaimsForDynamicCondition;
2851
- exports.getMaxClaimsForDynamicGroup = getMaxClaimsForDynamicGroup;
2852
- exports.hasConditions = hasConditions;
2853
- exports.meetsBaseConditions = meetsBaseConditions;
2854
- exports.meetsClaimableConditions = meetsClaimableConditions;
2855
- exports.meetsCompletionConditions = meetsCompletionConditions;
2856
- exports.meetsCompletionConditionsBeforeExpiry = meetsCompletionConditionsBeforeExpiry;
2857
- exports.meetsDynamicConditions = meetsDynamicConditions;
2858
- exports.meetsLinkedEntityOffersCondition = meetsLinkedEntityOffersCondition;
2859
- exports.meetsSurfacingConditions = meetsSurfacingConditions;
2860
- exports.offerListenerEvents = offerListenerEvents;
2861
- exports.offerMeetsCompletionConditions = offerMeetsCompletionConditions;
2862
- exports.rewardKinds = rewardKinds;
2863
- exports.rewardSchema = rewardSchema;
2864
1347
  //# sourceMappingURL=index.js.map