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