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