@pixels-online/pixels-client-js-sdk 1.13.0 → 1.15.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.
@@ -605,22 +605,28 @@
605
605
  }, backoffDelay);
606
606
  }
607
607
  disconnect() {
608
- if (this.reconnectTimeout) {
609
- clearTimeout(this.reconnectTimeout);
610
- this.reconnectTimeout = null;
608
+ try {
609
+ if (this.reconnectTimeout) {
610
+ clearTimeout(this.reconnectTimeout);
611
+ this.reconnectTimeout = null;
612
+ }
613
+ if (this.eventSource) {
614
+ this.logger.log('Closing SSE connection');
615
+ this.eventSource.close();
616
+ this.eventSource = null;
617
+ this.setConnectionState(exports.ConnectionState.DISCONNECTED);
618
+ this.eventEmitter.emit(exports.OfferEvent.DISCONNECTED, {
619
+ reason: 'manual',
620
+ timestamp: new Date(),
621
+ });
622
+ }
623
+ this.isConnecting = false;
624
+ this.reconnectAttempts = 0;
611
625
  }
612
- if (this.eventSource) {
613
- this.logger.log('Closing SSE connection');
614
- this.eventSource.close();
615
- this.eventSource = null;
616
- this.setConnectionState(exports.ConnectionState.DISCONNECTED);
617
- this.eventEmitter.emit(exports.OfferEvent.DISCONNECTED, {
618
- reason: 'manual',
619
- timestamp: new Date(),
620
- });
626
+ catch (error) {
627
+ // should be safe to ignore, likely mid-stream error during disconnect
628
+ this.logger.log('Error during disconnect:', error);
621
629
  }
622
- this.isConnecting = false;
623
- this.reconnectAttempts = 0;
624
630
  }
625
631
  isConnected() {
626
632
  return this.eventSource?.getReadyState() === ReadyState.OPEN;
@@ -1169,6 +1175,19 @@
1169
1175
  }
1170
1176
  }
1171
1177
 
1178
+ const keyPattern = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
1179
+ // renders template by replacing {keyName} with values from dynamic
1180
+ function renderTemplate(template, dynamic) {
1181
+ if (!template)
1182
+ return '';
1183
+ return template.replace(keyPattern, (match, key) => {
1184
+ if (dynamic && dynamic[key] !== undefined) {
1185
+ return String(dynamic[key]);
1186
+ }
1187
+ return '{?}'; // indicate missing key
1188
+ });
1189
+ }
1190
+
1172
1191
  const meetsBaseConditions = ({ conditions, playerSnap, addDetails, }) => {
1173
1192
  const conditionData = [];
1174
1193
  let isValid = true;
@@ -1494,6 +1513,92 @@
1494
1513
  }
1495
1514
  }
1496
1515
  }
1516
+ // Validate link count conditions
1517
+ if (conditions?.links) {
1518
+ for (const [linkType, constraint] of Object.entries(conditions.links)) {
1519
+ const linkCount = playerSnap.entityLinks?.filter((link) => link.kind === linkType).length || 0;
1520
+ if (constraint.min !== undefined) {
1521
+ const isDisqualify = linkCount < constraint.min;
1522
+ if (addDetails) {
1523
+ conditionData.push({
1524
+ isMet: !isDisqualify,
1525
+ kind: 'links',
1526
+ trackerAmount: linkCount,
1527
+ text: `At least ${constraint.min} ${linkType} link(s)`,
1528
+ });
1529
+ if (isDisqualify)
1530
+ isValid = false;
1531
+ }
1532
+ else {
1533
+ if (isDisqualify)
1534
+ return { isValid: false };
1535
+ }
1536
+ }
1537
+ if (constraint.max !== undefined) {
1538
+ const isDisqualify = linkCount > constraint.max;
1539
+ if (addDetails) {
1540
+ conditionData.push({
1541
+ isMet: !isDisqualify,
1542
+ kind: 'links',
1543
+ trackerAmount: linkCount,
1544
+ text: `At most ${constraint.max} ${linkType} link(s)`,
1545
+ });
1546
+ if (isDisqualify)
1547
+ isValid = false;
1548
+ }
1549
+ else {
1550
+ if (isDisqualify)
1551
+ return { isValid: false };
1552
+ }
1553
+ }
1554
+ }
1555
+ }
1556
+ // Evaluate dynamic conditions
1557
+ if (conditions?.dynamic?.conditions?.length) {
1558
+ const dynamicResult = meetsDynamicConditions(playerSnap.dynamic || {}, conditions.dynamic);
1559
+ if (addDetails) {
1560
+ conditionData.push({
1561
+ isMet: dynamicResult,
1562
+ kind: 'dynamic',
1563
+ text: renderTemplate(conditions.dynamic.template, playerSnap.dynamic) ||
1564
+ 'Dynamic conditions',
1565
+ });
1566
+ if (!dynamicResult)
1567
+ isValid = false;
1568
+ }
1569
+ else {
1570
+ if (!dynamicResult)
1571
+ return { isValid: false };
1572
+ }
1573
+ }
1574
+ if (conditions?.identifiers?.platforms?.length) {
1575
+ const playerPlatforms = new Set(playerSnap.identifiers?.map((i) => i.platform.toLowerCase()) || []);
1576
+ const isAndBehaviour = conditions.identifiers.behaviour === 'AND';
1577
+ const platformsToCheck = conditions.identifiers.platforms;
1578
+ let isMet;
1579
+ let displayText;
1580
+ if (isAndBehaviour) {
1581
+ isMet = platformsToCheck.every((platform) => playerPlatforms.has(platform.toLowerCase()));
1582
+ displayText = `Link all: ${platformsToCheck.join(', ')}`;
1583
+ }
1584
+ else {
1585
+ isMet = platformsToCheck.some((platform) => playerPlatforms.has(platform.toLowerCase()));
1586
+ displayText = `Link any: ${platformsToCheck.join(', ')}`;
1587
+ }
1588
+ if (addDetails) {
1589
+ conditionData.push({
1590
+ isMet,
1591
+ kind: 'identifiers',
1592
+ text: displayText,
1593
+ });
1594
+ if (!isMet)
1595
+ isValid = false;
1596
+ }
1597
+ else {
1598
+ if (!isMet)
1599
+ return { isValid: false };
1600
+ }
1601
+ }
1497
1602
  return { isValid, conditionData: addDetails ? conditionData : undefined };
1498
1603
  };
1499
1604
  const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, }) => {
@@ -1502,6 +1607,13 @@
1502
1607
  // context is not in the list of surfacing contexts, so we don't want to surface this offer
1503
1608
  return { isValid: false };
1504
1609
  }
1610
+ if (surfacingConditions?.targetEntityTypes?.length) {
1611
+ const playerTarget = playerSnap.target || 'default';
1612
+ // check if entity type is allowed
1613
+ if (!surfacingConditions.targetEntityTypes.includes(playerTarget)) {
1614
+ return { isValid: false };
1615
+ }
1616
+ }
1505
1617
  const conditions = surfacingConditions;
1506
1618
  if (conditions?.andTags?.length) {
1507
1619
  // check if player has all of the tags
@@ -1540,12 +1652,6 @@
1540
1652
  (!playerSnap.dateSignedUp || playerSnap.dateSignedUp > conditions.maxDateSignedUp)) {
1541
1653
  return { isValid: false };
1542
1654
  }
1543
- // Check dynamic conditions if present
1544
- if (conditions.dynamic?.conditions?.length) {
1545
- if (!meetsDynamicConditions(playerSnap.dynamic || {}, conditions.dynamic)) {
1546
- return { isValid: false };
1547
- }
1548
- }
1549
1655
  const completedOfferIds = new Set();
1550
1656
  for (const pOffer of playerOffers?.values() || []) {
1551
1657
  if (pOffer.status === 'claimed' || pOffer.status === 'claimable') {
@@ -1581,6 +1687,10 @@
1581
1687
  return true;
1582
1688
  if (conditions.minDaysInGame)
1583
1689
  return true;
1690
+ if (conditions.dynamic?.conditions?.length)
1691
+ return true;
1692
+ if (conditions.identifiers?.platforms?.length)
1693
+ return true;
1584
1694
  const surCond = conditions;
1585
1695
  if (surCond.contexts?.length)
1586
1696
  return true;
@@ -1600,6 +1710,12 @@
1600
1710
  return true;
1601
1711
  if (surCond.completedOffers?.length)
1602
1712
  return true;
1713
+ if (surCond.programmatic)
1714
+ return true;
1715
+ if (surCond.targetEntityTypes?.length)
1716
+ return true;
1717
+ if (surCond.links && Object.keys(surCond.links).length > 0)
1718
+ return true;
1603
1719
  const compCond = conditions;
1604
1720
  if (compCond.context)
1605
1721
  return true;
@@ -1615,6 +1731,8 @@
1615
1731
  return true;
1616
1732
  if (compCond.loginStreak)
1617
1733
  return true;
1734
+ if (compCond.linkedCompletions)
1735
+ return true;
1618
1736
  return false;
1619
1737
  };
1620
1738
  const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, addDetails = false, }) => {
@@ -1728,51 +1846,107 @@
1728
1846
  }
1729
1847
  }
1730
1848
  if (conditions?.social) {
1731
- const hasAttachedContent = !!completionTrackers?.social?.videoId;
1732
1849
  const tSocial = completionTrackers?.social;
1733
1850
  const cSocial = completionConditions.social;
1734
- let isDisqualify = !hasAttachedContent;
1735
- if (cSocial?.minLikes && tSocial && (tSocial.likes || 0) < cSocial.minLikes) {
1736
- isDisqualify = true;
1737
- }
1738
- if (cSocial?.minViews && tSocial && (tSocial.views || 0) < cSocial.minViews) {
1739
- isDisqualify = true;
1740
- }
1741
- if (cSocial?.minComments && tSocial && (tSocial.comments || 0) < cSocial.minComments) {
1851
+ const mode = cSocial?.mode || 'attach';
1852
+ const hasContent = Boolean(mode === 'accumulate'
1853
+ ? tSocial?.mode === 'accumulate'
1854
+ : tSocial && tSocial.mode !== 'accumulate' && !!tSocial.videoId);
1855
+ const minLikes = cSocial?.minLikes || 0;
1856
+ const minViews = cSocial?.minViews || 0;
1857
+ const minComments = cSocial?.minComments || 0;
1858
+ const likes = tSocial?.likes || 0;
1859
+ const views = tSocial?.views || 0;
1860
+ const comments = tSocial?.comments || 0;
1861
+ let isDisqualify = !hasContent;
1862
+ if (likes < minLikes || views < minViews || comments < minComments) {
1742
1863
  isDisqualify = true;
1743
1864
  }
1744
1865
  if (addDetails) {
1745
- // Build detailed text about requirements
1746
1866
  const platformMap = {
1747
- 'tiktok': 'TikTok',
1748
- 'instagram': 'Instagram',
1749
- 'youtube': 'YouTube',
1867
+ tiktok: 'TikTok',
1868
+ instagram: 'Instagram',
1869
+ youtube: 'YouTube',
1750
1870
  };
1751
- const platformText = conditions.social.platforms.map(platform => platformMap[platform]).join(' | ');
1752
- let displayText = `Attach a ${platformText} post`;
1753
- if (hasAttachedContent && tSocial) {
1754
- let socialTexts = [];
1755
- if (cSocial?.minLikes ?? 0 > 0) {
1756
- socialTexts.push(`${cSocial.minLikes} Likes (${tSocial.likes}/${cSocial.minLikes})`);
1757
- }
1758
- if (cSocial?.minViews ?? 0 > 0) {
1759
- socialTexts.push(`${cSocial.minViews} Views (${tSocial.views}/${cSocial.minViews})`);
1760
- }
1761
- if (cSocial?.minComments ?? 0 > 0) {
1762
- socialTexts.push(`${cSocial.minComments} Comments (${tSocial.comments}/${cSocial.minComments})`);
1763
- }
1764
- displayText = `Reach:\n${socialTexts.join('\n')}`;
1871
+ const platformText = conditions.social.platforms
1872
+ .map((platform) => platformMap[platform])
1873
+ .join(' | ');
1874
+ const requiredWords = cSocial?.requiredWords ?? [];
1875
+ if (mode === 'accumulate') {
1876
+ const matchCount = (tSocial?.mode === 'accumulate' && tSocial.matchCount) || 0;
1877
+ conditionData.push({
1878
+ isMet: hasContent,
1879
+ kind: 'social',
1880
+ trackerAmount: matchCount,
1881
+ text: hasContent
1882
+ ? `Found ${matchCount} matching ${platformText} post${matchCount !== 1 ? 's' : ''}`
1883
+ : requiredWords.length > 0
1884
+ ? `Post ${platformText} content with ${requiredWords.map((w) => `"${w}"`).join(', ')}`
1885
+ : `Post ${platformText} content`,
1886
+ });
1887
+ }
1888
+ else {
1889
+ const title = (tSocial && tSocial.mode !== 'accumulate' && tSocial.title) || undefined;
1890
+ conditionData.push({
1891
+ isMet: hasContent,
1892
+ kind: 'social',
1893
+ trackerAmount: 0,
1894
+ text: !hasContent
1895
+ ? requiredWords.length > 0
1896
+ ? `Attach a ${platformText} post with ${requiredWords.map((w) => `"${w}"`).join(', ')} in the title`
1897
+ : `Attach a ${platformText} post`
1898
+ : `Attached: ${title}`,
1899
+ });
1765
1900
  }
1766
- const trackerAmount = (cSocial?.minViews ?? 0) > 0 && (tSocial?.likes ?? 0) < (cSocial.minLikes ?? 0)
1767
- ? (tSocial?.likes ?? 0)
1768
- : (cSocial?.minComments ?? 0) > 0 && (tSocial?.comments ?? 0) < (cSocial.minComments ?? 0)
1769
- ? (tSocial?.comments ?? 0)
1770
- : (tSocial?.views ?? 0);
1901
+ if (minLikes > 0) {
1902
+ conditionData.push({
1903
+ isMet: hasContent && likes >= minLikes,
1904
+ kind: 'social',
1905
+ trackerAmount: likes,
1906
+ text: mode === 'accumulate'
1907
+ ? `Combined ${minLikes} Likes`
1908
+ : `Reach ${minLikes} Likes`,
1909
+ });
1910
+ }
1911
+ if (minViews > 0) {
1912
+ conditionData.push({
1913
+ isMet: hasContent && views >= minViews,
1914
+ kind: 'social',
1915
+ trackerAmount: views,
1916
+ text: mode === 'accumulate'
1917
+ ? `Combined ${minViews} Views`
1918
+ : `Reach ${minViews} Views`,
1919
+ });
1920
+ }
1921
+ if (minComments > 0) {
1922
+ conditionData.push({
1923
+ isMet: hasContent && comments >= minComments,
1924
+ kind: 'social',
1925
+ trackerAmount: comments,
1926
+ text: mode === 'accumulate'
1927
+ ? `Combined ${minComments} Comments`
1928
+ : `Reach ${minComments} Comments`,
1929
+ });
1930
+ }
1931
+ if (isDisqualify)
1932
+ isValid = false;
1933
+ }
1934
+ else {
1935
+ if (isDisqualify)
1936
+ return { isValid: false };
1937
+ }
1938
+ }
1939
+ // Linked completions - wait for N linked entities to complete
1940
+ if (conditions?.linkedCompletions?.min) {
1941
+ const currentCount = completionTrackers?.linkedCompletions || 0;
1942
+ const requiredCount = conditions.linkedCompletions.min;
1943
+ const isDisqualify = currentCount < requiredCount;
1944
+ if (addDetails) {
1771
1945
  conditionData.push({
1772
- isMet: hasAttachedContent,
1773
- kind: 'social',
1774
- trackerAmount,
1775
- text: displayText,
1946
+ isMet: !isDisqualify,
1947
+ kind: 'linkedCompletions',
1948
+ trackerAmount: currentCount,
1949
+ text: `Wait for ${requiredCount} linked ${requiredCount === 1 ? 'entity' : 'entities'} to complete`,
1776
1950
  });
1777
1951
  if (isDisqualify)
1778
1952
  isValid = false;
@@ -1793,6 +1967,136 @@
1793
1967
  }
1794
1968
  return { isValid: true, conditionData: [] };
1795
1969
  };
1970
+ /**
1971
+ * Checks if completion conditions were met before a specific expiry time.
1972
+ * Returns true if all relevant condition fields were updated before expiryTime.
1973
+ *
1974
+ * @param completionConditions - The completion conditions to check
1975
+ * @param completionTrackers - The completion trackers (for buyItem, spendCurrency, etc.)
1976
+ * @param playerSnap - The player snapshot with field timestamps
1977
+ * @param expiryTime - The expiry timestamp in milliseconds
1978
+ * @returns true if all conditions were met before expiry, false otherwise
1979
+ */
1980
+ const meetsCompletionConditionsBeforeExpiry = ({ completionConditions, completionTrackers, playerSnap, expiryTime, }) => {
1981
+ if (!completionConditions)
1982
+ return false;
1983
+ // Check if there are actually any conditions to evaluate
1984
+ if (!hasConditions(completionConditions))
1985
+ return false;
1986
+ // First check if conditions are actually met
1987
+ const conditionsMet = meetsCompletionConditions({
1988
+ completionConditions,
1989
+ completionTrackers,
1990
+ playerSnap,
1991
+ });
1992
+ if (!conditionsMet.isValid)
1993
+ return false;
1994
+ const lastSnapshotUpdate = new Date(playerSnap.snapshotLastUpdated).getTime();
1995
+ /**
1996
+ * Checks if a field was updated after the expiry time.
1997
+ * Returns true if updated AFTER or AT expiry (violates grace period).
1998
+ * Returns false if updated BEFORE expiry (allows grace period).
1999
+ */
2000
+ function wasUpdatedAfterExpiry(data) {
2001
+ let lastUpdated;
2002
+ if (typeof data === 'object' && data !== null && !(data instanceof Date)) {
2003
+ // Object with optional lastUpdated field
2004
+ lastUpdated = data.lastUpdated
2005
+ ? new Date(data.lastUpdated).getTime()
2006
+ : lastSnapshotUpdate;
2007
+ }
2008
+ else if (data instanceof Date) {
2009
+ lastUpdated = data.getTime();
2010
+ }
2011
+ else if (typeof data === 'string' || typeof data === 'number') {
2012
+ lastUpdated = new Date(data).getTime();
2013
+ }
2014
+ else {
2015
+ // No data provided, use snapshot timestamp
2016
+ lastUpdated = lastSnapshotUpdate;
2017
+ }
2018
+ return lastUpdated >= expiryTime;
2019
+ }
2020
+ if (completionConditions.currencies) {
2021
+ for (const currencyId in completionConditions.currencies) {
2022
+ const currency = playerSnap.currencies?.[currencyId];
2023
+ if (!currency)
2024
+ continue;
2025
+ if (wasUpdatedAfterExpiry(currency))
2026
+ return false;
2027
+ }
2028
+ }
2029
+ if (completionConditions.levels) {
2030
+ for (const skillId in completionConditions.levels) {
2031
+ const level = playerSnap.levels?.[skillId];
2032
+ if (!level)
2033
+ continue;
2034
+ if (wasUpdatedAfterExpiry(level))
2035
+ return false;
2036
+ }
2037
+ }
2038
+ if (completionConditions.quests) {
2039
+ for (const questId in completionConditions.quests) {
2040
+ const quest = playerSnap.quests?.[questId];
2041
+ if (!quest)
2042
+ continue;
2043
+ if (wasUpdatedAfterExpiry(quest))
2044
+ return false;
2045
+ }
2046
+ }
2047
+ if (completionConditions.memberships) {
2048
+ for (const membershipId in completionConditions.memberships) {
2049
+ const membership = playerSnap.memberships?.[membershipId];
2050
+ if (!membership)
2051
+ continue;
2052
+ if (wasUpdatedAfterExpiry(membership))
2053
+ return false;
2054
+ }
2055
+ }
2056
+ if (completionConditions.achievements) {
2057
+ for (const achievementId in completionConditions.achievements) {
2058
+ const achievement = playerSnap.achievements?.[achievementId];
2059
+ if (!achievement)
2060
+ continue;
2061
+ if (wasUpdatedAfterExpiry(achievement))
2062
+ return false;
2063
+ }
2064
+ }
2065
+ if (completionConditions.stakedTokens) {
2066
+ for (const tokenId in completionConditions.stakedTokens) {
2067
+ const stakedToken = playerSnap.stakedTokens?.[tokenId];
2068
+ if (!stakedToken)
2069
+ continue;
2070
+ const lastStakeTime = new Date(stakedToken.lastStake ?? 0).getTime();
2071
+ const lastUnstakeTime = new Date(stakedToken.lastUnstake ?? 0).getTime();
2072
+ const lastUpdated = Math.max(lastStakeTime, lastUnstakeTime);
2073
+ if (lastUpdated >= expiryTime)
2074
+ return false;
2075
+ }
2076
+ }
2077
+ if (completionConditions.minTrustScore !== undefined ||
2078
+ completionConditions.maxTrustScore !== undefined) {
2079
+ if (wasUpdatedAfterExpiry(playerSnap.trustLastUpdated))
2080
+ return false;
2081
+ }
2082
+ if (completionConditions.minDaysInGame !== undefined) {
2083
+ if (wasUpdatedAfterExpiry(playerSnap.daysInGameLastUpdated))
2084
+ return false;
2085
+ }
2086
+ if (completionConditions.login || completionConditions.loginStreak) {
2087
+ if (wasUpdatedAfterExpiry())
2088
+ return false;
2089
+ }
2090
+ if (completionConditions.social) {
2091
+ // Check if social content was attached/validated after expiry
2092
+ if (completionTrackers?.social?.lastChecked) {
2093
+ if (wasUpdatedAfterExpiry(completionTrackers.social.lastChecked))
2094
+ return false;
2095
+ }
2096
+ }
2097
+ // All conditions were met before expiry
2098
+ return true;
2099
+ };
1796
2100
  /**
1797
2101
  * Checks if a dynamic object meets a set of dynamic field conditions.
1798
2102
  * @param dynamicObj - The object with any key and string or number value.
@@ -1871,8 +2175,9 @@
1871
2175
  return result;
1872
2176
  }
1873
2177
 
2178
+ const offerListenerEvents = ['claim_offer'];
1874
2179
  const PlayerOfferStatuses = [
1875
- 'inQueue',
2180
+ // 'inQueue', // fuck this shit. just don't surface offers if their offer plate is full.
1876
2181
  'surfaced',
1877
2182
  'viewed',
1878
2183
  'claimable',
@@ -1887,12 +2192,26 @@
1887
2192
  'exp',
1888
2193
  'trust_points',
1889
2194
  'loyalty_currency', // loyalty currency that the player can exchange for rewards like on-chain via withdraw, etc.
2195
+ 'discount', // handled by the external dev, using the rewardId to identify what it is for in their system
1890
2196
  /** on-chain rewards require the builder to send funds to a custodial wallet that we use to send to player wallets*/
1891
2197
  ];
1892
2198
  const rewardSchema = {
1893
2199
  _id: false,
1894
2200
  kind: { type: String, enum: rewardKinds },
1895
- rewardId: String,
2201
+ rewardId: {
2202
+ type: String,
2203
+ validate: {
2204
+ validator: function (value) {
2205
+ // Require rewardId for item, coins, loyalty_currency, exp, and discount kinds
2206
+ const requiresRewardId = ['item', 'coins', 'loyalty_currency', 'exp', 'discount'].includes(this.kind);
2207
+ if (requiresRewardId) {
2208
+ return !!value;
2209
+ }
2210
+ return true;
2211
+ },
2212
+ message: 'rewardId is required for reward kinds: item, coins, loyalty_currency, exp, and discount',
2213
+ },
2214
+ },
1896
2215
  skillId: String,
1897
2216
  currencyId: String, // could be a loyalty currency
1898
2217
  itemId: String,
@@ -1910,8 +2229,10 @@
1910
2229
  exports.hasConditions = hasConditions;
1911
2230
  exports.meetsBaseConditions = meetsBaseConditions;
1912
2231
  exports.meetsCompletionConditions = meetsCompletionConditions;
2232
+ exports.meetsCompletionConditionsBeforeExpiry = meetsCompletionConditionsBeforeExpiry;
1913
2233
  exports.meetsDynamicConditions = meetsDynamicConditions;
1914
2234
  exports.meetsSurfacingConditions = meetsSurfacingConditions;
2235
+ exports.offerListenerEvents = offerListenerEvents;
1915
2236
  exports.rewardKinds = rewardKinds;
1916
2237
  exports.rewardSchema = rewardSchema;
1917
2238