@pixels-online/pixels-client-js-sdk 1.18.0 → 1.20.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.esm.js CHANGED
@@ -628,25 +628,33 @@ class SSEConnection {
628
628
  }
629
629
 
630
630
  class OfferStore {
631
- constructor(config) {
631
+ constructor(config, client) {
632
+ this.client = client;
632
633
  this.offers = new Map();
633
- this.player = null;
634
+ this.players = new Map();
634
635
  this.logger = createLogger(config, 'OfferStore');
635
636
  }
636
- getPlayer() {
637
- return this.player || null;
637
+ getPlayer(targetId = this.client.getSelfId()) {
638
+ if (!targetId)
639
+ return null;
640
+ return this.players.get(targetId) || null;
638
641
  }
639
642
  setPlayer(player) {
640
- this.player = player;
643
+ this.players.set(player.gameData.playerId, player);
641
644
  this.logger.log('Updated player:', player);
642
645
  }
643
646
  /**
644
647
  * Set all offers (replaces existing)
645
648
  */
646
- setOffers(offers) {
647
- this.offers.clear();
649
+ setOffers(offers, target) {
650
+ const targetPlayer = target || this.getPlayer();
651
+ if (!targetPlayer) {
652
+ this.logger.warn('No target player to set offers for');
653
+ return;
654
+ }
655
+ this.offers.set(targetPlayer.gameData.playerId, new Map());
648
656
  offers.forEach((offer) => {
649
- this.offers.set(offer.instanceId, offer);
657
+ this.offers.get(targetPlayer.gameData.playerId).set(offer.instanceId, offer);
650
658
  });
651
659
  this.logger.log(`Set ${offers.length} offers`);
652
660
  }
@@ -654,16 +662,23 @@ class OfferStore {
654
662
  * Add or update a single offer
655
663
  */
656
664
  upsertOffer(offer) {
657
- const previousOffer = this.offers.get(offer.instanceId);
658
- this.offers.set(offer.instanceId, offer);
665
+ let playerOffers = this.offers.get(offer.playerId);
666
+ if (!playerOffers) {
667
+ playerOffers = new Map();
668
+ this.offers.set(offer.playerId, playerOffers);
669
+ }
670
+ const previousOffer = playerOffers.get(offer.instanceId);
671
+ playerOffers.set(offer.instanceId, offer);
659
672
  this.logger.log(`${previousOffer ? 'Updated' : 'Added'} offer:`, offer.instanceId);
660
673
  return previousOffer;
661
674
  }
662
675
  /**
663
676
  * Remove an offer
664
677
  */
665
- removeOffer(offerId) {
666
- const removed = this.offers.delete(offerId);
678
+ removeOffer(offerId, targetId = this.client.getSelfId()) {
679
+ if (!targetId)
680
+ return false;
681
+ const removed = this.offers.get(targetId)?.delete(offerId) || false;
667
682
  if (removed) {
668
683
  this.logger.log(`Removed offer:`, offerId);
669
684
  }
@@ -672,26 +687,30 @@ class OfferStore {
672
687
  /**
673
688
  * Get a single offer
674
689
  */
675
- getOffer(offerId) {
676
- return this.offers.get(offerId);
690
+ getOffer(offerId, targetId = this.client.getSelfId()) {
691
+ if (!targetId)
692
+ return undefined;
693
+ return this.offers.get(targetId)?.get(offerId);
677
694
  }
678
695
  /**
679
696
  * Get all offers
680
697
  */
681
- getAllOffers() {
682
- return Array.from(this.offers.values());
698
+ getAllOffers(targetId = this.client.getSelfId()) {
699
+ if (!targetId)
700
+ return [];
701
+ return Array.from(this.offers.get(targetId)?.values() || []);
683
702
  }
684
703
  /**
685
704
  * Get offers filtered by status
686
705
  */
687
- getOffersByStatus(status) {
688
- return this.getAllOffers().filter((offer) => offer.status === status);
706
+ getOffersByStatus(status, targetId = this.client.getSelfId()) {
707
+ return this.getAllOffers(targetId).filter((offer) => offer.status === status);
689
708
  }
690
709
  /**
691
710
  * Get active offers (not expired, not claimed)
692
711
  */
693
- getActiveOffers() {
694
- return this.getAllOffers().filter((offer) => {
712
+ getActiveOffers(targetId = this.client.getSelfId()) {
713
+ return this.getAllOffers(targetId).filter((offer) => {
695
714
  if (!offer.status)
696
715
  return false; // Must have a status
697
716
  return (offer.status === 'surfaced' ||
@@ -702,14 +721,14 @@ class OfferStore {
702
721
  /**
703
722
  * Get claimable offers
704
723
  */
705
- getClaimableOffers() {
706
- return this.getOffersByStatus('claimable');
724
+ getClaimableOffers(targetId = this.client.getSelfId()) {
725
+ return this.getOffersByStatus('claimable', targetId);
707
726
  }
708
727
  /**
709
728
  * Check if an offer has expired
710
729
  */
711
- isOfferExpired(offerId) {
712
- const offer = this.getOffer(offerId);
730
+ isOfferExpired(offerId, targetId = this.client.getSelfId()) {
731
+ const offer = this.getOffer(offerId, targetId);
713
732
  if (!offer)
714
733
  return true;
715
734
  // Check status
@@ -722,17 +741,29 @@ class OfferStore {
722
741
  return false;
723
742
  }
724
743
  /**
725
- * Clear all offers
744
+ * Clear all offers for a specific player
745
+ */
746
+ clear(targetId = this.client.getSelfId()) {
747
+ if (!targetId)
748
+ return;
749
+ this.offers.set(targetId, new Map());
750
+ this.logger.log('Cleared all offers for player:', targetId);
751
+ }
752
+ /**
753
+ * Clear all offers for all players
726
754
  */
727
- clear() {
755
+ clearAll() {
728
756
  this.offers.clear();
729
- this.logger.log('Cleared all offers');
757
+ this.logger.log('Cleared all offers for all players');
730
758
  }
731
759
  /**
732
- * Get offer count
760
+ * Get offer count (for self player)
733
761
  */
734
762
  get size() {
735
- return this.offers.size;
763
+ const selfId = this.client.getSelfId();
764
+ if (!selfId)
765
+ return 0;
766
+ return this.offers.get(selfId)?.size || 0;
736
767
  }
737
768
  }
738
769
 
@@ -835,7 +866,9 @@ class AssetHelper {
835
866
  return null;
836
867
  }
837
868
  setCurrencyAssetContents(currencies) {
838
- this.currencies = currencies;
869
+ Object.keys(currencies).forEach((key) => {
870
+ this.currencies[key] = currencies[key];
871
+ });
839
872
  }
840
873
  resolveReward(reward) {
841
874
  if (reward.kind === 'loyalty_currency' && reward.rewardId) {
@@ -891,6 +924,7 @@ class OfferwallClient {
891
924
  constructor(config) {
892
925
  this.isInitializing = false;
893
926
  this.pendingStackedToken = null;
927
+ this.selfId = null;
894
928
  this.config = {
895
929
  autoConnect: config.autoConnect ?? false,
896
930
  reconnect: config.reconnect ?? true,
@@ -903,7 +937,7 @@ class OfferwallClient {
903
937
  this.hooks = this.config.hooks || {};
904
938
  this.logger = createLogger(this.config, 'OfferwallClient');
905
939
  this.eventEmitter = new EventEmitter(this.config);
906
- this.offerStore = new OfferStore(this.config);
940
+ this.offerStore = new OfferStore(this.config, this);
907
941
  this.tokenManager = new TokenManager(this.config);
908
942
  this.assetHelper = new AssetHelper(this.config);
909
943
  this.sseConnection = new SSEConnection(this.config, this.eventEmitter, this.tokenManager);
@@ -944,6 +978,9 @@ class OfferwallClient {
944
978
  get assets() {
945
979
  return this.assetHelper;
946
980
  }
981
+ getSelfId() {
982
+ return this.selfId;
983
+ }
947
984
  /**
948
985
  * Initialize the offerwall client and connect
949
986
  */
@@ -1006,8 +1043,9 @@ class OfferwallClient {
1006
1043
  if (this.sseConnection) {
1007
1044
  this.sseConnection.disconnect();
1008
1045
  }
1009
- this.offerStore.clear();
1046
+ this.offerStore.clearAll();
1010
1047
  this.tokenManager.clearToken();
1048
+ this.selfId = null;
1011
1049
  if (this.hooks.afterDisconnect) {
1012
1050
  await this.hooks.afterDisconnect();
1013
1051
  }
@@ -1015,8 +1053,8 @@ class OfferwallClient {
1015
1053
  /**
1016
1054
  * Claim rewards for an offer
1017
1055
  */
1018
- async claimReward(instanceId) {
1019
- const offer = this.offerStore.getOffer(instanceId);
1056
+ async claimReward(instanceId, targetId = this.getSelfId()) {
1057
+ const offer = this.offerStore.getOffer(instanceId, targetId);
1020
1058
  if (!offer) {
1021
1059
  throw new Error(`Offer ${instanceId} not found`);
1022
1060
  }
@@ -1031,7 +1069,7 @@ class OfferwallClient {
1031
1069
  }
1032
1070
  }
1033
1071
  try {
1034
- const response = await this.claimOfferAPI(instanceId);
1072
+ const response = await this.claimOfferAPI(instanceId, targetId);
1035
1073
  const updatedOffer = { ...offer, status: 'claimed' };
1036
1074
  this.offerStore.upsertOffer(updatedOffer);
1037
1075
  this.eventEmitter.emit(OfferEvent.OFFER_CLAIMED, {
@@ -1087,7 +1125,7 @@ class OfferwallClient {
1087
1125
  });
1088
1126
  */
1089
1127
  this.eventEmitter.on(OfferEvent.OFFER_SURFACED, ({ offer }) => {
1090
- this.offerStore.upsertOffer(offer);
1128
+ this.offerStore.upsertOffer(offer); // should always be selfId
1091
1129
  this.logger.log(`Surfaced offer: ${offer.instanceId}`);
1092
1130
  });
1093
1131
  }
@@ -1117,34 +1155,40 @@ class OfferwallClient {
1117
1155
  }
1118
1156
  return response.json();
1119
1157
  }
1120
- async claimOfferAPI(instanceId) {
1158
+ async claimOfferAPI(instanceId, targetId = null) {
1121
1159
  return this.postWithAuth('/v1/client/reward/claim', {
1122
1160
  instanceId,
1123
1161
  kind: 'offer',
1162
+ targetId: targetId || undefined,
1124
1163
  });
1125
1164
  }
1126
- getPlayer() {
1127
- return this.offerStore.getPlayer();
1165
+ getPlayer(targetId = this.getSelfId()) {
1166
+ return this.offerStore.getPlayer(targetId);
1128
1167
  }
1129
- getOffers() {
1130
- return this.offerStore.getAllOffers();
1168
+ getOffers(targetId = this.getSelfId()) {
1169
+ return this.offerStore.getAllOffers(targetId);
1131
1170
  }
1132
- async refreshOffersAndPlayer() {
1171
+ async refreshOffersAndPlayer(targetId = null) {
1133
1172
  try {
1134
- const { offers, player } = await this.getOffersAndPlayer();
1135
- this.offerStore.setOffers(offers);
1173
+ const { offers, player } = await this.getOffersAndPlayer(targetId);
1174
+ if (targetId == null) {
1175
+ this.selfId = player.gameData.playerId;
1176
+ }
1136
1177
  this.offerStore.setPlayer(player);
1178
+ this.offerStore.setOffers(offers, player);
1137
1179
  this.eventEmitter.emit(OfferEvent.REFRESH, { offers, player: player });
1138
1180
  this.logger.log('Refreshed offers and player snapshot');
1181
+ return offers;
1139
1182
  }
1140
1183
  catch (error) {
1141
1184
  this.handleError(error, 'refreshOffersAndPlayer');
1142
1185
  throw error;
1143
1186
  }
1144
1187
  }
1145
- async getOffersAndPlayer() {
1188
+ async getOffersAndPlayer(targetId = null) {
1146
1189
  const data = await this.postWithAuth('/v1/client/player/campaigns', {
1147
1190
  viewingCampaigns: true,
1191
+ targetId: targetId || undefined,
1148
1192
  });
1149
1193
  if (!data.offers || !Array.isArray(data.offers)) {
1150
1194
  throw new Error('No offers returned from offers endpoint');
@@ -1277,6 +1321,8 @@ class OfferwallClient {
1277
1321
  }
1278
1322
  }
1279
1323
 
1324
+ const DEFAULT_ENTITY_KIND = '_default';
1325
+
1280
1326
  const keyPattern = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
1281
1327
  /**
1282
1328
  * This replaces {keyName} keys from the template with corresponding values from the dynamic object.
@@ -1314,11 +1360,25 @@ function replaceDynamicConditionKeys(conditions, trackers) {
1314
1360
  }));
1315
1361
  }
1316
1362
 
1363
+ const dynamicTrackerToPrimitive = (dynaTrack) => {
1364
+ const primitive = {};
1365
+ for (const key in dynaTrack) {
1366
+ primitive[key] = dynaTrack[key].value || 0;
1367
+ }
1368
+ return primitive;
1369
+ };
1370
+
1371
+ const addressNetworkId = (contractAddress, network) => {
1372
+ return `${contractAddress.toLowerCase()}:${network.toUpperCase()}`;
1373
+ };
1374
+
1317
1375
  const meetsBaseConditions = ({ conditions, playerSnap, addDetails,
1318
1376
  /** this exists if calling meetsBaseConditions from meetsCompletionConditions. but surfacing
1319
1377
  * check doesn't use this since we don't have a playerOffer at surfacing time
1320
1378
  */
1321
- playerOffer, }) => {
1379
+ playerOffer,
1380
+ /** Additional data like fetched token balances that isn't part of playerSnap */
1381
+ additionalData, }) => {
1322
1382
  const conditionData = [];
1323
1383
  let isValid = true;
1324
1384
  if (conditions?.minDaysInGame) {
@@ -1646,7 +1706,8 @@ playerOffer, }) => {
1646
1706
  // Validate link count conditions
1647
1707
  if (conditions?.links && 'entityLinks' in playerSnap) {
1648
1708
  for (const [linkType, constraint] of Object.entries(conditions.links)) {
1649
- const linkCount = playerSnap.entityLinks?.filter((link) => link.kind === linkType).length || 0;
1709
+ // linkType should always exist. and be default is none was specified
1710
+ const linkCount = playerSnap.entityLinks?.filter((link) => (link.kind || DEFAULT_ENTITY_KIND) === linkType).length || 0;
1650
1711
  if (constraint.min !== undefined) {
1651
1712
  const isDisqualify = linkCount < constraint.min;
1652
1713
  if (addDetails) {
@@ -1733,16 +1794,60 @@ playerOffer, }) => {
1733
1794
  return { isValid: false };
1734
1795
  }
1735
1796
  }
1797
+ // Evaluate token balance conditions
1798
+ for (const tokenCond of conditions?.tokenBalances || []) {
1799
+ const contracts = tokenCond.contracts || [];
1800
+ let totalBalance = 0;
1801
+ const fetchedBalances = aggregateTokenBalances(additionalData);
1802
+ for (const contract of contracts) {
1803
+ const balanceKey = addressNetworkId(contract.contractAddress, contract.network);
1804
+ totalBalance += fetchedBalances[balanceKey] || 0;
1805
+ }
1806
+ if (tokenCond.min !== undefined) {
1807
+ const isDisqualify = totalBalance < tokenCond.min;
1808
+ if (addDetails) {
1809
+ conditionData.push({
1810
+ isMet: !isDisqualify,
1811
+ kind: 'tokenBalances',
1812
+ trackerAmount: totalBalance,
1813
+ text: `Have at least ${tokenCond.min} ${tokenCond.name || 'tokens'}`,
1814
+ });
1815
+ if (isDisqualify)
1816
+ isValid = false;
1817
+ }
1818
+ else {
1819
+ if (isDisqualify)
1820
+ return { isValid: false };
1821
+ }
1822
+ }
1823
+ if (tokenCond.max !== undefined) {
1824
+ const isDisqualify = totalBalance > tokenCond.max;
1825
+ if (addDetails) {
1826
+ conditionData.push({
1827
+ isMet: !isDisqualify,
1828
+ kind: 'tokenBalances',
1829
+ trackerAmount: totalBalance,
1830
+ text: `Have at most ${tokenCond.max} ${tokenCond.name || 'tokens'}`,
1831
+ });
1832
+ if (isDisqualify)
1833
+ isValid = false;
1834
+ }
1835
+ else {
1836
+ if (isDisqualify)
1837
+ return { isValid: false };
1838
+ }
1839
+ }
1840
+ }
1736
1841
  return { isValid, conditionData: addDetails ? conditionData : undefined };
1737
1842
  };
1738
- const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, }) => {
1843
+ const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, additionalData, }) => {
1739
1844
  if (surfacingConditions?.contexts?.length &&
1740
1845
  !surfacingConditions.contexts?.includes(context || '')) {
1741
1846
  // context is not in the list of surfacing contexts, so we don't want to surface this offer
1742
1847
  return { isValid: false };
1743
1848
  }
1744
1849
  if (surfacingConditions?.targetEntityTypes?.length) {
1745
- const playerTarget = playerSnap.entityKind || 'default';
1850
+ const playerTarget = playerSnap.entityKind || DEFAULT_ENTITY_KIND;
1746
1851
  // check if entity type is allowed
1747
1852
  if (!surfacingConditions.targetEntityTypes.includes(playerTarget)) {
1748
1853
  return { isValid: false };
@@ -1823,7 +1928,7 @@ const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, pl
1823
1928
  }
1824
1929
  }
1825
1930
  }
1826
- return meetsBaseConditions({ conditions, playerSnap });
1931
+ return meetsBaseConditions({ conditions, playerSnap, additionalData });
1827
1932
  };
1828
1933
  const hasConditions = (conditions) => {
1829
1934
  if (!conditions)
@@ -1881,6 +1986,8 @@ const hasConditions = (conditions) => {
1881
1986
  return true;
1882
1987
  if (surCond.networkRestrictions?.length)
1883
1988
  return true;
1989
+ if (surCond.linkedEntityOffers?.offer_id)
1990
+ return true;
1884
1991
  const compCond = conditions;
1885
1992
  if (compCond.context)
1886
1993
  return true;
@@ -1900,23 +2007,47 @@ const hasConditions = (conditions) => {
1900
2007
  return true;
1901
2008
  if (compCond.dynamicTracker?.conditions?.length)
1902
2009
  return true;
2010
+ if (conditions.tokenBalances?.length)
2011
+ return true;
2012
+ if (Object.keys(compCond.contractInteractions || {}).length > 0)
2013
+ return true;
1903
2014
  return false;
1904
2015
  };
1905
- const offerMeetsCompletionConditions = (offer, snapshot) => {
2016
+ const meetsLinkedEntityOffersCondition = ({ linkedEntityOffers, matchingLinks, linkedPOfferMap, }) => {
2017
+ if (!linkedPOfferMap)
2018
+ return { isValid: false };
2019
+ const linkedPlayerOffer_ids = [];
2020
+ for (const link of matchingLinks) {
2021
+ const key = `${link.playerId}:${linkedEntityOffers.offer_id}`;
2022
+ const po = linkedPOfferMap.get(key);
2023
+ if (po) {
2024
+ linkedPlayerOffer_ids.push(po._id.toString());
2025
+ }
2026
+ }
2027
+ if (linkedPlayerOffer_ids.length > 0) {
2028
+ return { isValid: true, linkedPlayerOffer_ids };
2029
+ }
2030
+ return { isValid: false };
2031
+ };
2032
+ const offerMeetsCompletionConditions = (offer, snapshot, additionalData) => {
1906
2033
  return meetsCompletionConditions({
1907
2034
  completionConditions: offer.completionConditions || {},
1908
2035
  completionTrackers: offer.completionTrackers,
1909
2036
  playerSnap: snapshot,
1910
2037
  playerOffer: offer,
1911
2038
  addDetails: true,
2039
+ maxClaimCount: offer.maxClaimCount,
2040
+ additionalData,
1912
2041
  });
1913
2042
  };
1914
- const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, }) => {
2043
+ const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, additionalData, }) => {
1915
2044
  if (completionConditions) {
1916
2045
  const conditions = completionConditions;
1917
2046
  // For multi-claim offers, scale cumulative requirements by (claimedCount + 1)
1918
2047
  const shouldScale = maxClaimCount === -1 || (maxClaimCount && maxClaimCount > 1);
1919
- const claimMultiplier = shouldScale ? (playerOffer.claimedCount || 0) + 1 : 1;
2048
+ const claimMultiplier = shouldScale
2049
+ ? (playerOffer.trackers?.claimedCount || 0) + 1
2050
+ : 1;
1920
2051
  const conditionData = [];
1921
2052
  let isValid = true;
1922
2053
  let maxTotalClaimsFromScaling = Infinity;
@@ -2047,12 +2178,14 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2047
2178
  }
2048
2179
  }
2049
2180
  if (conditions?.social) {
2050
- const tSocial = completionTrackers?.social;
2181
+ const tSocialAccumulate = completionTrackers?.social;
2182
+ const tSocialAttach = completionTrackers?.social;
2051
2183
  const cSocial = completionConditions.social;
2052
2184
  const mode = cSocial?.mode || 'attach';
2185
+ const tSocial = mode === 'accumulate' ? tSocialAccumulate : tSocialAttach;
2053
2186
  const hasContent = Boolean(mode === 'accumulate'
2054
- ? tSocial?.mode === 'accumulate'
2055
- : tSocial && tSocial.mode !== 'accumulate' && !!tSocial.videoId);
2187
+ ? tSocialAccumulate?.matchCount > 0
2188
+ : tSocialAttach?.videoId);
2056
2189
  // Only scale social metrics in accumulate mode (attach mode is single content)
2057
2190
  const socialMultiplier = mode === 'accumulate' ? claimMultiplier : 1;
2058
2191
  const minLikes = (cSocial?.minLikes || 0) * socialMultiplier;
@@ -2087,7 +2220,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2087
2220
  .join(' | ');
2088
2221
  const requiredWords = cSocial?.requiredWords ?? [];
2089
2222
  if (mode === 'accumulate') {
2090
- const matchCount = (tSocial?.mode === 'accumulate' && tSocial.matchCount) || 0;
2223
+ const matchCount = tSocialAccumulate?.matchCount || 0;
2091
2224
  conditionData.push({
2092
2225
  isMet: hasContent,
2093
2226
  kind: 'social',
@@ -2100,7 +2233,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2100
2233
  });
2101
2234
  }
2102
2235
  else {
2103
- const title = (tSocial && tSocial.mode !== 'accumulate' && tSocial.title) || undefined;
2236
+ const title = tSocialAttach?.title;
2104
2237
  conditionData.push({
2105
2238
  isMet: hasContent,
2106
2239
  kind: 'social',
@@ -2177,22 +2310,22 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2177
2310
  if (conditions?.dynamicTracker?.conditions?.length) {
2178
2311
  const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamicTracker.conditions, playerOffer?.trackers || {});
2179
2312
  // now we have the game-defined conditions with {{}} keys populated. feed these conditions into evaluator
2180
- const dynamicResult = meetsDynamicConditions(completionTrackers?.dynamicTracker, {
2313
+ const dynamicResult = meetsDynamicConditions(dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {}), {
2181
2314
  ...conditions.dynamicTracker,
2182
2315
  conditions: resolvedConditions,
2183
2316
  }, claimMultiplier);
2184
2317
  if (shouldScale) {
2185
- const dynamicMax = getMaxClaimsForDynamicGroup(completionTrackers?.dynamicTracker || {}, {
2318
+ const dynamicMax = getMaxClaimsForDynamicGroup(dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {}), {
2186
2319
  ...conditions.dynamicTracker,
2187
2320
  conditions: resolvedConditions,
2188
- }, playerOffer.claimedCount || 0);
2321
+ }, playerOffer?.trackers?.claimedCount || 0);
2189
2322
  updateMax(dynamicMax);
2190
2323
  }
2191
2324
  if (addDetails) {
2192
2325
  conditionData.push({
2193
2326
  isMet: dynamicResult,
2194
- kind: 'dynamic',
2195
- text: renderTemplate(conditions.dynamicTracker.template, completionTrackers?.dynamicTracker) || 'Dynamic conditions',
2327
+ kind: 'dynamicTracker',
2328
+ text: renderTemplate(conditions.dynamicTracker.template, dynamicTrackerToPrimitive(completionTrackers?.dynamicTracker || {})) || 'Dynamic conditions',
2196
2329
  });
2197
2330
  if (!dynamicResult)
2198
2331
  isValid = false;
@@ -2202,19 +2335,64 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
2202
2335
  return { isValid: false, availableClaimsNow: 0 };
2203
2336
  }
2204
2337
  }
2338
+ // Evaluate contractInteractions completion trackers
2339
+ if (conditions?.contractInteractions) {
2340
+ for (const [conditionId, condition] of Object.entries(conditions.contractInteractions)) {
2341
+ const baseAmount = condition.amount || 0;
2342
+ const scaledAmount = baseAmount * claimMultiplier;
2343
+ const trackerValue = completionTrackers?.contractInteractions?.[conditionId] || 0;
2344
+ const isDisqualify = trackerValue < scaledAmount;
2345
+ if (shouldScale && baseAmount > 0) {
2346
+ updateMax(Math.floor(trackerValue / baseAmount));
2347
+ }
2348
+ if (addDetails) {
2349
+ let displayText;
2350
+ const eventType = condition.event;
2351
+ const name = condition.name || 'tokens';
2352
+ if (eventType === 'spend') {
2353
+ displayText = `Spend ${scaledAmount} ${name}`;
2354
+ }
2355
+ else if (eventType === 'earn') {
2356
+ displayText = `Earn ${scaledAmount} ${name}`;
2357
+ }
2358
+ else if (eventType === 'gain') {
2359
+ displayText = `Gain ${scaledAmount} ${name}`;
2360
+ }
2361
+ else if (eventType === 'lose') {
2362
+ displayText = `Lose ${scaledAmount} ${name}`;
2363
+ }
2364
+ else {
2365
+ displayText = `${name}: ${scaledAmount}`;
2366
+ }
2367
+ conditionData.push({
2368
+ isMet: !isDisqualify,
2369
+ kind: 'contractInteractions',
2370
+ trackerAmount: trackerValue,
2371
+ text: displayText,
2372
+ });
2373
+ if (isDisqualify)
2374
+ isValid = false;
2375
+ }
2376
+ else {
2377
+ if (isDisqualify)
2378
+ return { isValid: false, availableClaimsNow: 0 };
2379
+ }
2380
+ }
2381
+ }
2205
2382
  const r = meetsBaseConditions({
2206
2383
  conditions,
2207
2384
  playerSnap,
2208
2385
  addDetails: true,
2209
2386
  playerOffer,
2387
+ additionalData,
2210
2388
  });
2211
2389
  isValid = isValid && r.isValid;
2212
2390
  conditionData.push(...(r.conditionData || []));
2213
2391
  if (maxClaimCount && maxClaimCount > 0) {
2214
2392
  updateMax(maxClaimCount);
2215
2393
  }
2216
- const claimedCount = playerOffer.claimedCount || 0;
2217
- let availableClaimsNow = !isValid
2394
+ const claimedCount = playerOffer?.trackers?.claimedCount || 0;
2395
+ const availableClaimsNow = !isValid
2218
2396
  ? 0
2219
2397
  : maxTotalClaimsFromScaling === Infinity
2220
2398
  ? -1
@@ -2565,6 +2743,19 @@ function meetsClaimableConditions({ claimableConditions, playerOfferTrackers, cl
2565
2743
  }
2566
2744
  return { isValid: true };
2567
2745
  }
2746
+ // returns contractAddress:network -> balance
2747
+ function aggregateTokenBalances(data) {
2748
+ const aggregatedBalances = {};
2749
+ for (const { balances } of data?.cryptoWallets || []) {
2750
+ for (const [key, balance] of Object.entries(balances)) {
2751
+ if (!aggregatedBalances[key]) {
2752
+ aggregatedBalances[key] = 0;
2753
+ }
2754
+ aggregatedBalances[key] += balance;
2755
+ }
2756
+ }
2757
+ return aggregatedBalances;
2758
+ }
2568
2759
 
2569
2760
  const offerListenerEvents = ['claim_offer'];
2570
2761
  const PlayerOfferStatuses = [
@@ -2612,5 +2803,5 @@ const rewardSchema = {
2612
2803
  image: String,
2613
2804
  };
2614
2805
 
2615
- export { AssetHelper, ConnectionState, EventEmitter, OfferEvent, OfferStore, OfferwallClient, PlayerOfferStatuses, SSEConnection, getMaxClaimsForDynamicCondition, getMaxClaimsForDynamicGroup, hasConditions, meetsBaseConditions, meetsClaimableConditions, meetsCompletionConditions, meetsCompletionConditionsBeforeExpiry, meetsDynamicConditions, meetsSurfacingConditions, offerListenerEvents, offerMeetsCompletionConditions, rewardKinds, rewardSchema };
2806
+ export { AssetHelper, ConnectionState, DEFAULT_ENTITY_KIND, EventEmitter, OfferEvent, OfferStore, OfferwallClient, PlayerOfferStatuses, SSEConnection, aggregateTokenBalances, getMaxClaimsForDynamicCondition, getMaxClaimsForDynamicGroup, hasConditions, meetsBaseConditions, meetsClaimableConditions, meetsCompletionConditions, meetsCompletionConditionsBeforeExpiry, meetsDynamicConditions, meetsLinkedEntityOffersCondition, meetsSurfacingConditions, offerListenerEvents, offerMeetsCompletionConditions, rewardKinds, rewardSchema };
2616
2807
  //# sourceMappingURL=index.esm.js.map