@pixels-online/pixels-client-js-sdk 1.15.0 → 1.17.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.
@@ -896,6 +896,7 @@
896
896
  class OfferwallClient {
897
897
  constructor(config) {
898
898
  this.isInitializing = false;
899
+ this.pendingStackedToken = null;
899
900
  this.config = {
900
901
  autoConnect: config.autoConnect ?? false,
901
902
  reconnect: config.reconnect ?? true,
@@ -913,12 +914,24 @@
913
914
  this.assetHelper = new AssetHelper(this.config);
914
915
  this.sseConnection = new SSEConnection(this.config, this.eventEmitter, this.tokenManager);
915
916
  this.setupInternalListeners();
917
+ this.setupStackedLinkDetection();
916
918
  if (this.config.autoConnect) {
917
919
  this.initialize().catch((err) => {
918
920
  this.logger.error('Auto-initialization failed:', err);
919
921
  });
920
922
  }
921
923
  }
924
+ setupStackedLinkDetection() {
925
+ if (!this.config.stackedLink?.autoConsume) {
926
+ return;
927
+ }
928
+ const token = this.detectStackedLinkToken();
929
+ if (token) {
930
+ this.logger.log('Detected stackedToken in URL');
931
+ this.pendingStackedToken = token;
932
+ this.clearStackedTokenFromUrl();
933
+ }
934
+ }
922
935
  /**
923
936
  * Get the offer store instance
924
937
  */
@@ -949,9 +962,19 @@
949
962
  this.isInitializing = true;
950
963
  await this.refreshOffersAndPlayer();
951
964
  await this.connect();
965
+ // Process pending Stacked link token if exists and autoConsume is enabled
966
+ if (this.pendingStackedToken && this.config.stackedLink?.autoConsume) {
967
+ this.logger.log('Processing pending Stacked link token');
968
+ const result = await this.consumeStackedLinkToken(this.pendingStackedToken);
969
+ this.pendingStackedToken = null;
970
+ if (this.config.stackedLink?.onComplete) {
971
+ this.config.stackedLink.onComplete(result);
972
+ }
973
+ }
952
974
  this.isInitializing = false;
953
975
  }
954
976
  catch (error) {
977
+ this.isInitializing = false;
955
978
  this.handleError(error, 'initialize');
956
979
  throw error;
957
980
  }
@@ -1167,6 +1190,91 @@
1167
1190
  return null;
1168
1191
  return `${dashboardBaseUrl}/auth/enter?token=${token}&gameId=${gameId}`;
1169
1192
  }
1193
+ // ==================== Stacked Link Methods ====================
1194
+ /**
1195
+ * Detect if there's a Stacked link token in the current URL
1196
+ * @returns The token string if found, null otherwise
1197
+ */
1198
+ detectStackedLinkToken() {
1199
+ if (typeof window === 'undefined')
1200
+ return null;
1201
+ try {
1202
+ const params = new URLSearchParams(window.location.search);
1203
+ const token = params.get('stackedToken');
1204
+ if (token && token.length > 0) {
1205
+ return token;
1206
+ }
1207
+ // for SPA routers that use hash routing
1208
+ if (window.location.hash) {
1209
+ const hashQuery = window.location.hash.split('?')[1];
1210
+ if (hashQuery) {
1211
+ const hashParams = new URLSearchParams(hashQuery);
1212
+ const hashToken = hashParams.get('stackedToken');
1213
+ if (hashToken && hashToken.length > 0) {
1214
+ return hashToken;
1215
+ }
1216
+ }
1217
+ }
1218
+ return null;
1219
+ }
1220
+ catch (error) {
1221
+ this.logger.error('Error detecting stacked token:', error);
1222
+ return null;
1223
+ }
1224
+ }
1225
+ /**
1226
+ * Clear the stackedToken from the URL without page reload
1227
+ */
1228
+ clearStackedTokenFromUrl() {
1229
+ if (typeof window === 'undefined')
1230
+ return;
1231
+ try {
1232
+ const url = new URL(window.location.href);
1233
+ url.searchParams.delete('stackedToken');
1234
+ window.history.replaceState({}, '', url.toString());
1235
+ this.logger.log('Cleared stackedToken from URL');
1236
+ }
1237
+ catch (error) {
1238
+ this.logger.error('Error clearing stacked token from URL:', error);
1239
+ }
1240
+ }
1241
+ /**
1242
+ * Consume a Stacked link token to link the current game player
1243
+ * to a Stacked unified user account.
1244
+ *
1245
+ * IMPORTANT: The player must be authenticated (have a valid JWT from tokenProvider)
1246
+ * before calling this method.
1247
+ *
1248
+ * @param token The Stacked link token from the URL
1249
+ * @returns Promise resolving to the link result
1250
+ */
1251
+ async consumeStackedLinkToken(token) {
1252
+ try {
1253
+ const data = await this.postWithAuth('/v1/auth/stacked_link/exchange', { stackedToken: token });
1254
+ return {
1255
+ linked: data.linked,
1256
+ alreadyLinked: data.alreadyLinked,
1257
+ };
1258
+ }
1259
+ catch (error) {
1260
+ this.logger.error('Error consuming stacked link token:', error);
1261
+ return {
1262
+ linked: false,
1263
+ };
1264
+ }
1265
+ }
1266
+ /**
1267
+ * Detects the token from URL, consumes it if player is authenticated.
1268
+ * @returns Promise resolving to the link result, or null if no token found
1269
+ */
1270
+ async processStackedLinkToken() {
1271
+ const token = this.detectStackedLinkToken();
1272
+ if (!token) {
1273
+ return null;
1274
+ }
1275
+ this.clearStackedTokenFromUrl();
1276
+ return this.consumeStackedLinkToken(token);
1277
+ }
1170
1278
  handleError(error, context) {
1171
1279
  this.logger.error(`Error in ${context}:`, error);
1172
1280
  if (this.hooks.onError) {
@@ -1176,19 +1284,47 @@
1176
1284
  }
1177
1285
 
1178
1286
  const keyPattern = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
1179
- // renders template by replacing {keyName} with values from dynamic
1287
+ /**
1288
+ * This replaces {keyName} keys from the template with corresponding values from the dynamic object.
1289
+ */
1180
1290
  function renderTemplate(template, dynamic) {
1181
1291
  if (!template)
1182
1292
  return '';
1183
- return template.replace(keyPattern, (match, key) => {
1293
+ return template.replace(keyPattern, (_match, key) => {
1294
+ if (dynamic && typeof dynamic[key] === 'boolean') {
1295
+ return dynamic[key] ? '✓' : '✗';
1296
+ }
1184
1297
  if (dynamic && dynamic[key] !== undefined) {
1185
1298
  return String(dynamic[key]);
1186
1299
  }
1187
1300
  return '{?}'; // indicate missing key
1188
1301
  });
1189
1302
  }
1303
+ /**
1304
+ * This replaces {{keyName}} in dynamic condition keys with corresponding values from
1305
+ * the PlayerOffer.trackers
1306
+ *
1307
+ * eg. a condition high_score_pet-{{surfacerPlayerId}} with high_score_pet-12345
1308
+ */
1309
+ function replaceDynamicConditionKey(key, trackers) {
1310
+ return key?.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (match, p1) => {
1311
+ const value = trackers[p1];
1312
+ return value !== undefined ? String(value) : match;
1313
+ });
1314
+ }
1315
+ /** this replaces all of the dynamic conditions.keys by calling replaceDynamicConditionKey */
1316
+ function replaceDynamicConditionKeys(conditions, trackers) {
1317
+ return conditions.map((condition) => ({
1318
+ ...condition,
1319
+ key: replaceDynamicConditionKey(condition.key, trackers),
1320
+ }));
1321
+ }
1190
1322
 
1191
- const meetsBaseConditions = ({ conditions, playerSnap, addDetails, }) => {
1323
+ const meetsBaseConditions = ({ conditions, playerSnap, addDetails,
1324
+ /** this exists if calling meetsBaseConditions from meetsCompletionConditions. but surfacing
1325
+ * check doesn't use this since we don't have a playerOffer at surfacing time
1326
+ */
1327
+ playerOffer, }) => {
1192
1328
  const conditionData = [];
1193
1329
  let isValid = true;
1194
1330
  if (conditions?.minDaysInGame) {
@@ -1514,7 +1650,7 @@
1514
1650
  }
1515
1651
  }
1516
1652
  // Validate link count conditions
1517
- if (conditions?.links) {
1653
+ if (conditions?.links && 'entityLinks' in playerSnap) {
1518
1654
  for (const [linkType, constraint] of Object.entries(conditions.links)) {
1519
1655
  const linkCount = playerSnap.entityLinks?.filter((link) => link.kind === linkType).length || 0;
1520
1656
  if (constraint.min !== undefined) {
@@ -1555,7 +1691,11 @@
1555
1691
  }
1556
1692
  // Evaluate dynamic conditions
1557
1693
  if (conditions?.dynamic?.conditions?.length) {
1558
- const dynamicResult = meetsDynamicConditions(playerSnap.dynamic || {}, conditions.dynamic);
1694
+ const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamic.conditions, playerOffer?.trackers || {});
1695
+ const dynamicResult = meetsDynamicConditions(playerSnap.dynamic, {
1696
+ ...conditions.dynamic,
1697
+ conditions: resolvedConditions,
1698
+ });
1559
1699
  if (addDetails) {
1560
1700
  conditionData.push({
1561
1701
  isMet: dynamicResult,
@@ -1571,7 +1711,7 @@
1571
1711
  return { isValid: false };
1572
1712
  }
1573
1713
  }
1574
- if (conditions?.identifiers?.platforms?.length) {
1714
+ if (conditions?.identifiers?.platforms?.length && 'identifiers' in playerSnap) {
1575
1715
  const playerPlatforms = new Set(playerSnap.identifiers?.map((i) => i.platform.toLowerCase()) || []);
1576
1716
  const isAndBehaviour = conditions.identifiers.behaviour === 'AND';
1577
1717
  const platformsToCheck = conditions.identifiers.platforms;
@@ -1608,7 +1748,7 @@
1608
1748
  return { isValid: false };
1609
1749
  }
1610
1750
  if (surfacingConditions?.targetEntityTypes?.length) {
1611
- const playerTarget = playerSnap.target || 'default';
1751
+ const playerTarget = playerSnap.entityKind || 'default';
1612
1752
  // check if entity type is allowed
1613
1753
  if (!surfacingConditions.targetEntityTypes.includes(playerTarget)) {
1614
1754
  return { isValid: false };
@@ -1664,6 +1804,31 @@
1664
1804
  return { isValid: false };
1665
1805
  }
1666
1806
  }
1807
+ if (conditions.allowedCountries?.length) {
1808
+ const playerCountry = playerSnap.ip?.countryCode;
1809
+ if (!playerCountry || !conditions.allowedCountries.includes(playerCountry)) {
1810
+ return { isValid: false };
1811
+ }
1812
+ }
1813
+ if (conditions.restrictedCountries?.length) {
1814
+ const playerCountry = playerSnap.ip?.countryCode;
1815
+ if (!playerCountry) {
1816
+ return { isValid: false };
1817
+ }
1818
+ if (conditions.restrictedCountries.includes(playerCountry)) {
1819
+ return { isValid: false };
1820
+ }
1821
+ }
1822
+ if (conditions.networkRestrictions?.length) {
1823
+ if (!playerSnap.ip) {
1824
+ return { isValid: false };
1825
+ }
1826
+ for (const restriction of conditions.networkRestrictions) {
1827
+ if (playerSnap.ip[restriction]) {
1828
+ return { isValid: false };
1829
+ }
1830
+ }
1831
+ }
1667
1832
  return meetsBaseConditions({ conditions, playerSnap });
1668
1833
  };
1669
1834
  const hasConditions = (conditions) => {
@@ -1716,6 +1881,12 @@
1716
1881
  return true;
1717
1882
  if (surCond.links && Object.keys(surCond.links).length > 0)
1718
1883
  return true;
1884
+ if (surCond.allowedCountries?.length)
1885
+ return true;
1886
+ if (surCond.restrictedCountries?.length)
1887
+ return true;
1888
+ if (surCond.networkRestrictions?.length)
1889
+ return true;
1719
1890
  const compCond = conditions;
1720
1891
  if (compCond.context)
1721
1892
  return true;
@@ -1733,11 +1904,25 @@
1733
1904
  return true;
1734
1905
  if (compCond.linkedCompletions)
1735
1906
  return true;
1907
+ if (compCond.dynamicTracker?.conditions?.length)
1908
+ return true;
1736
1909
  return false;
1737
1910
  };
1738
- const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, addDetails = false, }) => {
1911
+ const offerMeetsCompletionConditions = (offer, snapshot) => {
1912
+ return meetsCompletionConditions({
1913
+ completionConditions: offer.completionConditions || {},
1914
+ completionTrackers: offer.completionTrackers,
1915
+ playerSnap: snapshot,
1916
+ playerOffer: offer,
1917
+ addDetails: true,
1918
+ });
1919
+ };
1920
+ const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, playerOffer, addDetails = false, maxClaimCount, }) => {
1739
1921
  if (completionConditions) {
1740
1922
  const conditions = completionConditions;
1923
+ // For multi-claim offers, scale cumulative requirements by (claimedCount + 1)
1924
+ const shouldScale = maxClaimCount === -1 || (maxClaimCount && maxClaimCount > 1);
1925
+ const claimMultiplier = shouldScale ? (playerOffer.claimedCount || 0) + 1 : 1;
1741
1926
  const conditionData = [];
1742
1927
  let isValid = true;
1743
1928
  if (completionConditions?.context?.id) {
@@ -1760,13 +1945,14 @@
1760
1945
  }
1761
1946
  }
1762
1947
  if (conditions?.buyItem) {
1763
- const isDisqualify = (completionTrackers?.buyItem || 0) < (conditions.buyItem.amount || 1);
1948
+ const scaledAmount = (conditions.buyItem.amount || 1) * claimMultiplier;
1949
+ const isDisqualify = (completionTrackers?.buyItem || 0) < scaledAmount;
1764
1950
  if (addDetails) {
1765
1951
  conditionData.push({
1766
1952
  isMet: !isDisqualify,
1767
1953
  kind: 'buyItem',
1768
1954
  trackerAmount: completionTrackers?.buyItem || 0,
1769
- text: `Buy ${conditions.buyItem.amount || 1} ${conditions.buyItem.name}`,
1955
+ text: `Buy ${scaledAmount} ${conditions.buyItem.name}`,
1770
1956
  });
1771
1957
  if (isDisqualify)
1772
1958
  isValid = false;
@@ -1777,13 +1963,14 @@
1777
1963
  }
1778
1964
  }
1779
1965
  if (conditions?.spendCurrency) {
1780
- const isDisqualify = (completionTrackers?.spendCurrency || 0) < (conditions.spendCurrency.amount || 1);
1966
+ const scaledAmount = (conditions.spendCurrency.amount || 1) * claimMultiplier;
1967
+ const isDisqualify = (completionTrackers?.spendCurrency || 0) < scaledAmount;
1781
1968
  if (addDetails) {
1782
1969
  conditionData.push({
1783
1970
  isMet: !isDisqualify,
1784
1971
  kind: 'spendCurrency',
1785
1972
  trackerAmount: completionTrackers?.spendCurrency || 0,
1786
- text: `Spend ${conditions.spendCurrency.amount || 1} ${conditions.spendCurrency.name}`,
1973
+ text: `Spend ${scaledAmount} ${conditions.spendCurrency.name}`,
1787
1974
  });
1788
1975
  if (isDisqualify)
1789
1976
  isValid = false;
@@ -1794,15 +1981,17 @@
1794
1981
  }
1795
1982
  }
1796
1983
  if (conditions?.depositCurrency) {
1797
- const isDisqualify = (completionTrackers?.depositCurrency || 0) <
1798
- (conditions.depositCurrency.amount || 1);
1984
+ const scaledAmount = (conditions.depositCurrency.amount || 1) * claimMultiplier;
1985
+ const isDisqualify = (completionTrackers?.depositCurrency || 0) < scaledAmount;
1799
1986
  if (addDetails) {
1800
1987
  conditionData.push({
1801
1988
  isMet: !isDisqualify,
1802
1989
  kind: 'depositCurrency',
1803
1990
  trackerAmount: completionTrackers?.depositCurrency || 0,
1804
- text: `Deposit ${conditions.depositCurrency.amount || 1} ${conditions.depositCurrency.name}`,
1991
+ text: `Deposit ${scaledAmount} ${conditions.depositCurrency.name}`,
1805
1992
  });
1993
+ if (isDisqualify)
1994
+ isValid = false;
1806
1995
  }
1807
1996
  else {
1808
1997
  if (isDisqualify)
@@ -1810,12 +1999,13 @@
1810
1999
  }
1811
2000
  }
1812
2001
  if (conditions?.login) {
1813
- const isMet = completionTrackers?.login || false;
2002
+ const isMet = new Date(playerSnap.snapshotLastUpdated || 0).getTime() >
2003
+ new Date(playerOffer.createdAt || 0).getTime();
1814
2004
  if (addDetails) {
1815
2005
  conditionData.push({
1816
2006
  isMet,
1817
2007
  kind: 'login',
1818
- trackerAmount: completionTrackers?.login ? 1 : 0,
2008
+ trackerAmount: isMet ? 1 : 0,
1819
2009
  text: `Login to the game`,
1820
2010
  });
1821
2011
  isValid = isMet;
@@ -1852,9 +2042,11 @@
1852
2042
  const hasContent = Boolean(mode === 'accumulate'
1853
2043
  ? tSocial?.mode === 'accumulate'
1854
2044
  : tSocial && tSocial.mode !== 'accumulate' && !!tSocial.videoId);
1855
- const minLikes = cSocial?.minLikes || 0;
1856
- const minViews = cSocial?.minViews || 0;
1857
- const minComments = cSocial?.minComments || 0;
2045
+ // Only scale social metrics in accumulate mode (attach mode is single content)
2046
+ const socialMultiplier = mode === 'accumulate' ? claimMultiplier : 1;
2047
+ const minLikes = (cSocial?.minLikes || 0) * socialMultiplier;
2048
+ const minViews = (cSocial?.minViews || 0) * socialMultiplier;
2049
+ const minComments = (cSocial?.minComments || 0) * socialMultiplier;
1858
2050
  const likes = tSocial?.likes || 0;
1859
2051
  const views = tSocial?.views || 0;
1860
2052
  const comments = tSocial?.comments || 0;
@@ -1939,14 +2131,14 @@
1939
2131
  // Linked completions - wait for N linked entities to complete
1940
2132
  if (conditions?.linkedCompletions?.min) {
1941
2133
  const currentCount = completionTrackers?.linkedCompletions || 0;
1942
- const requiredCount = conditions.linkedCompletions.min;
1943
- const isDisqualify = currentCount < requiredCount;
2134
+ const scaledMin = conditions.linkedCompletions.min * claimMultiplier;
2135
+ const isDisqualify = currentCount < scaledMin;
1944
2136
  if (addDetails) {
1945
2137
  conditionData.push({
1946
2138
  isMet: !isDisqualify,
1947
2139
  kind: 'linkedCompletions',
1948
2140
  trackerAmount: currentCount,
1949
- text: `Wait for ${requiredCount} linked ${requiredCount === 1 ? 'entity' : 'entities'} to complete`,
2141
+ text: `Wait for ${scaledMin} linked ${scaledMin === 1 ? 'entity' : 'entities'} to complete`,
1950
2142
  });
1951
2143
  if (isDisqualify)
1952
2144
  isValid = false;
@@ -1956,10 +2148,32 @@
1956
2148
  return { isValid: false };
1957
2149
  }
1958
2150
  }
2151
+ if (conditions?.dynamicTracker?.conditions?.length) {
2152
+ const resolvedConditions = replaceDynamicConditionKeys(conditions.dynamicTracker.conditions, playerOffer?.trackers || {});
2153
+ // now we have the game-defined conditions with {{}} keys populated. feed these conditions into evaluator
2154
+ const dynamicResult = meetsDynamicConditions(completionTrackers?.dynamicTracker, {
2155
+ ...conditions.dynamicTracker,
2156
+ conditions: resolvedConditions,
2157
+ }, claimMultiplier);
2158
+ if (addDetails) {
2159
+ conditionData.push({
2160
+ isMet: dynamicResult,
2161
+ kind: 'dynamic',
2162
+ text: renderTemplate(conditions.dynamicTracker.template, completionTrackers?.dynamicTracker) || 'Dynamic conditions',
2163
+ });
2164
+ if (!dynamicResult)
2165
+ isValid = false;
2166
+ }
2167
+ else {
2168
+ if (!dynamicResult)
2169
+ return { isValid: false };
2170
+ }
2171
+ }
1959
2172
  const r = meetsBaseConditions({
1960
2173
  conditions,
1961
2174
  playerSnap,
1962
2175
  addDetails: true,
2176
+ playerOffer,
1963
2177
  });
1964
2178
  isValid = isValid && r.isValid;
1965
2179
  conditionData.push(...(r.conditionData || []));
@@ -1974,10 +2188,9 @@
1974
2188
  * @param completionConditions - The completion conditions to check
1975
2189
  * @param completionTrackers - The completion trackers (for buyItem, spendCurrency, etc.)
1976
2190
  * @param playerSnap - The player snapshot with field timestamps
1977
- * @param expiryTime - The expiry timestamp in milliseconds
1978
2191
  * @returns true if all conditions were met before expiry, false otherwise
1979
2192
  */
1980
- const meetsCompletionConditionsBeforeExpiry = ({ completionConditions, completionTrackers, playerSnap, expiryTime, }) => {
2193
+ const meetsCompletionConditionsBeforeExpiry = ({ completionConditions, completionTrackers, playerSnap, playerOffer, maxClaimCount, }) => {
1981
2194
  if (!completionConditions)
1982
2195
  return false;
1983
2196
  // Check if there are actually any conditions to evaluate
@@ -1987,10 +2200,15 @@
1987
2200
  const conditionsMet = meetsCompletionConditions({
1988
2201
  completionConditions,
1989
2202
  completionTrackers,
2203
+ playerOffer,
1990
2204
  playerSnap,
2205
+ maxClaimCount,
1991
2206
  });
1992
2207
  if (!conditionsMet.isValid)
1993
2208
  return false;
2209
+ if (!playerOffer.expiresAt)
2210
+ return true;
2211
+ const expiryTime = new Date(playerOffer.expiresAt).getTime();
1994
2212
  const lastSnapshotUpdate = new Date(playerSnap.snapshotLastUpdated).getTime();
1995
2213
  /**
1996
2214
  * Checks if a field was updated after the expiry time.
@@ -2101,66 +2319,80 @@
2101
2319
  * Checks if a dynamic object meets a set of dynamic field conditions.
2102
2320
  * @param dynamicObj - The object with any key and string or number value.
2103
2321
  * @param conditions - Array of conditions to check.
2322
+ * @param claimMultiplier - Multiplier to scale conditions (used for numeric comparisons).
2104
2323
  * @returns true if all conditions are met, false otherwise.
2105
2324
  */
2106
2325
  /**
2107
2326
  * Evaluates a single dynamic condition against the dynamic object.
2108
2327
  */
2109
- function evaluateDynamicCondition(dynamicObj, cond) {
2328
+ function evaluateDynamicCondition(dynamicObj, cond, claimMultiplier = 1) {
2329
+ if (!dynamicObj)
2330
+ return false;
2110
2331
  const val = dynamicObj[cond.key];
2111
2332
  if (val === undefined)
2112
2333
  return false;
2334
+ const isNumber = typeof val === 'number';
2335
+ const isBoolean = typeof val === 'boolean';
2336
+ if (isBoolean) {
2337
+ switch (cond.operator) {
2338
+ case '==':
2339
+ return val === Boolean(cond.compareTo);
2340
+ case '!=':
2341
+ return val !== Boolean(cond.compareTo);
2342
+ default:
2343
+ return false;
2344
+ }
2345
+ }
2346
+ const compareTo = isNumber ? Number(cond.compareTo) : String(cond.compareTo);
2113
2347
  switch (cond.operator) {
2114
2348
  case '==':
2115
- return val === cond.compareTo;
2349
+ return val === compareTo;
2116
2350
  case '!=':
2117
- return val !== cond.compareTo;
2118
- case '>':
2119
- return (typeof val === 'number' &&
2120
- typeof cond.compareTo === 'number' &&
2121
- val > cond.compareTo);
2122
- case '>=':
2123
- return (typeof val === 'number' &&
2124
- typeof cond.compareTo === 'number' &&
2125
- val >= cond.compareTo);
2126
- case '<':
2127
- return (typeof val === 'number' &&
2128
- typeof cond.compareTo === 'number' &&
2129
- val < cond.compareTo);
2130
- case '<=':
2131
- return (typeof val === 'number' &&
2132
- typeof cond.compareTo === 'number' &&
2133
- val <= cond.compareTo);
2134
- case 'has':
2135
- return (typeof val === 'string' &&
2136
- typeof cond.compareTo === 'string' &&
2137
- val.includes(cond.compareTo));
2138
- case 'not_has':
2139
- return (typeof val === 'string' &&
2140
- typeof cond.compareTo === 'string' &&
2141
- !val.includes(cond.compareTo));
2142
- default:
2143
- return false;
2351
+ return val !== compareTo;
2352
+ }
2353
+ if (isNumber && typeof compareTo === 'number') {
2354
+ switch (cond.operator) {
2355
+ case '>':
2356
+ return val > compareTo * claimMultiplier;
2357
+ case '>=':
2358
+ return val >= compareTo * claimMultiplier;
2359
+ case '<':
2360
+ return val < compareTo * claimMultiplier;
2361
+ case '<=':
2362
+ return val <= compareTo * claimMultiplier;
2363
+ }
2144
2364
  }
2365
+ else if (!isNumber && typeof compareTo === 'string') {
2366
+ switch (cond.operator) {
2367
+ case 'has':
2368
+ return val.includes(compareTo);
2369
+ case 'not_has':
2370
+ return !val.includes(compareTo);
2371
+ }
2372
+ }
2373
+ return false;
2145
2374
  }
2146
2375
  /**
2147
2376
  * Evaluates a group of dynamic conditions with logical links (AND, OR, AND NOT).
2148
2377
  * @param dynamicObj - The player's dynamic object with any key and string or number value.
2149
2378
  * @param dynamicGroup - The group of conditions and links to check.
2379
+ * @param claimMultiplier - Multiplier to scale conditions (used for numeric comparisons).
2150
2380
  * @returns true if the group evaluates to true, false otherwise.
2151
2381
  */
2152
- function meetsDynamicConditions(dynamicObj, dynamicGroup) {
2382
+ function meetsDynamicConditions(dynamicObj, dynamicGroup, claimMultiplier = 1) {
2153
2383
  const { conditions, links } = dynamicGroup;
2154
2384
  if (!conditions || conditions.length === 0)
2155
2385
  return true;
2386
+ if (!dynamicObj)
2387
+ return false;
2156
2388
  // If no links, treat as AND between all conditions
2157
2389
  if (!links || links.length === 0) {
2158
- return conditions.every((cond) => evaluateDynamicCondition(dynamicObj, cond));
2390
+ return conditions.every((cond) => evaluateDynamicCondition(dynamicObj, cond, claimMultiplier));
2159
2391
  }
2160
2392
  // Evaluate the first condition
2161
- let result = evaluateDynamicCondition(dynamicObj, conditions[0]);
2393
+ let result = evaluateDynamicCondition(dynamicObj, conditions[0], claimMultiplier);
2162
2394
  for (let i = 0; i < links.length; i++) {
2163
- const nextCond = evaluateDynamicCondition(dynamicObj, conditions[i + 1]);
2395
+ const nextCond = evaluateDynamicCondition(dynamicObj, conditions[i + 1], claimMultiplier);
2164
2396
  const link = links[i];
2165
2397
  if (link === 'AND') {
2166
2398
  result = result && nextCond;
@@ -2174,12 +2406,34 @@
2174
2406
  }
2175
2407
  return result;
2176
2408
  }
2409
+ /**
2410
+ * Checks if a PlayerOffer meets its claimable conditions (completed -> claimable transition).
2411
+ * @param claimableConditions - The offer's claimableConditions (from IOffer)
2412
+ * @param claimableTrackers - The player offer's claimableTrackers
2413
+ */
2414
+ function meetsClaimableConditions({ claimableConditions, playerOfferTrackers, claimableTrackers, }) {
2415
+ if (!claimableConditions) {
2416
+ return { isValid: true };
2417
+ }
2418
+ if (claimableConditions.siblingCompletions) {
2419
+ const siblingCount = playerOfferTrackers?.siblingPlayerOffer_ids?.length ?? 0;
2420
+ let completedCount = claimableTrackers?.siblingCompletions ?? 0;
2421
+ if (completedCount == -1)
2422
+ completedCount = siblingCount; // treat -1 as all completed
2423
+ // if siblings exist but not all are completed, return false
2424
+ if (siblingCount > 0 && completedCount < siblingCount) {
2425
+ return { isValid: false };
2426
+ }
2427
+ }
2428
+ return { isValid: true };
2429
+ }
2177
2430
 
2178
2431
  const offerListenerEvents = ['claim_offer'];
2179
2432
  const PlayerOfferStatuses = [
2180
2433
  // 'inQueue', // fuck this shit. just don't surface offers if their offer plate is full.
2181
2434
  'surfaced',
2182
2435
  'viewed',
2436
+ 'completed', // Individual completionConditions met, waiting for claimableConditions (e.g., siblings)
2183
2437
  'claimable',
2184
2438
  'claimed',
2185
2439
  'expired',
@@ -2228,11 +2482,13 @@
2228
2482
  exports.SSEConnection = SSEConnection;
2229
2483
  exports.hasConditions = hasConditions;
2230
2484
  exports.meetsBaseConditions = meetsBaseConditions;
2485
+ exports.meetsClaimableConditions = meetsClaimableConditions;
2231
2486
  exports.meetsCompletionConditions = meetsCompletionConditions;
2232
2487
  exports.meetsCompletionConditionsBeforeExpiry = meetsCompletionConditionsBeforeExpiry;
2233
2488
  exports.meetsDynamicConditions = meetsDynamicConditions;
2234
2489
  exports.meetsSurfacingConditions = meetsSurfacingConditions;
2235
2490
  exports.offerListenerEvents = offerListenerEvents;
2491
+ exports.offerMeetsCompletionConditions = offerMeetsCompletionConditions;
2236
2492
  exports.rewardKinds = rewardKinds;
2237
2493
  exports.rewardSchema = rewardSchema;
2238
2494