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