@pixels-online/pixels-client-js-sdk 1.14.0 → 1.16.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/core/OfferwallClient.d.ts +28 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.esm.js +531 -73
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +533 -72
- package/dist/index.js.map +1 -1
- package/dist/offerwall-sdk.umd.js +533 -72
- package/dist/offerwall-sdk.umd.js.map +1 -1
- package/dist/types/blockchain/user_wallet.d.ts +20 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/linking.d.ts +17 -0
- package/dist/types/offer.d.ts +140 -21
- package/dist/types/player.d.ts +132 -32
- package/dist/types/reward.d.ts +10 -9
- package/dist/utils/conditions.d.ts +31 -3
- package/dist/utils/template.d.ts +2 -0
- package/package.json +2 -1
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) {
|
|
@@ -1171,6 +1279,19 @@ class OfferwallClient {
|
|
|
1171
1279
|
}
|
|
1172
1280
|
}
|
|
1173
1281
|
|
|
1282
|
+
const keyPattern = /\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
1283
|
+
// renders template by replacing {keyName} with values from dynamic
|
|
1284
|
+
function renderTemplate(template, dynamic) {
|
|
1285
|
+
if (!template)
|
|
1286
|
+
return '';
|
|
1287
|
+
return template.replace(keyPattern, (match, key) => {
|
|
1288
|
+
if (dynamic && dynamic[key] !== undefined) {
|
|
1289
|
+
return String(dynamic[key]);
|
|
1290
|
+
}
|
|
1291
|
+
return '{?}'; // indicate missing key
|
|
1292
|
+
});
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1174
1295
|
const meetsBaseConditions = ({ conditions, playerSnap, addDetails, }) => {
|
|
1175
1296
|
const conditionData = [];
|
|
1176
1297
|
let isValid = true;
|
|
@@ -1496,6 +1617,92 @@ const meetsBaseConditions = ({ conditions, playerSnap, addDetails, }) => {
|
|
|
1496
1617
|
}
|
|
1497
1618
|
}
|
|
1498
1619
|
}
|
|
1620
|
+
// Validate link count conditions
|
|
1621
|
+
if (conditions?.links && 'entityLinks' in playerSnap) {
|
|
1622
|
+
for (const [linkType, constraint] of Object.entries(conditions.links)) {
|
|
1623
|
+
const linkCount = playerSnap.entityLinks?.filter((link) => link.kind === linkType).length || 0;
|
|
1624
|
+
if (constraint.min !== undefined) {
|
|
1625
|
+
const isDisqualify = linkCount < constraint.min;
|
|
1626
|
+
if (addDetails) {
|
|
1627
|
+
conditionData.push({
|
|
1628
|
+
isMet: !isDisqualify,
|
|
1629
|
+
kind: 'links',
|
|
1630
|
+
trackerAmount: linkCount,
|
|
1631
|
+
text: `At least ${constraint.min} ${linkType} link(s)`,
|
|
1632
|
+
});
|
|
1633
|
+
if (isDisqualify)
|
|
1634
|
+
isValid = false;
|
|
1635
|
+
}
|
|
1636
|
+
else {
|
|
1637
|
+
if (isDisqualify)
|
|
1638
|
+
return { isValid: false };
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (constraint.max !== undefined) {
|
|
1642
|
+
const isDisqualify = linkCount > constraint.max;
|
|
1643
|
+
if (addDetails) {
|
|
1644
|
+
conditionData.push({
|
|
1645
|
+
isMet: !isDisqualify,
|
|
1646
|
+
kind: 'links',
|
|
1647
|
+
trackerAmount: linkCount,
|
|
1648
|
+
text: `At most ${constraint.max} ${linkType} link(s)`,
|
|
1649
|
+
});
|
|
1650
|
+
if (isDisqualify)
|
|
1651
|
+
isValid = false;
|
|
1652
|
+
}
|
|
1653
|
+
else {
|
|
1654
|
+
if (isDisqualify)
|
|
1655
|
+
return { isValid: false };
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
// Evaluate dynamic conditions
|
|
1661
|
+
if (conditions?.dynamic?.conditions?.length) {
|
|
1662
|
+
const dynamicResult = meetsDynamicConditions(playerSnap.dynamic || {}, conditions.dynamic);
|
|
1663
|
+
if (addDetails) {
|
|
1664
|
+
conditionData.push({
|
|
1665
|
+
isMet: dynamicResult,
|
|
1666
|
+
kind: 'dynamic',
|
|
1667
|
+
text: renderTemplate(conditions.dynamic.template, playerSnap.dynamic) ||
|
|
1668
|
+
'Dynamic conditions',
|
|
1669
|
+
});
|
|
1670
|
+
if (!dynamicResult)
|
|
1671
|
+
isValid = false;
|
|
1672
|
+
}
|
|
1673
|
+
else {
|
|
1674
|
+
if (!dynamicResult)
|
|
1675
|
+
return { isValid: false };
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
if (conditions?.identifiers?.platforms?.length && 'identifiers' in playerSnap) {
|
|
1679
|
+
const playerPlatforms = new Set(playerSnap.identifiers?.map((i) => i.platform.toLowerCase()) || []);
|
|
1680
|
+
const isAndBehaviour = conditions.identifiers.behaviour === 'AND';
|
|
1681
|
+
const platformsToCheck = conditions.identifiers.platforms;
|
|
1682
|
+
let isMet;
|
|
1683
|
+
let displayText;
|
|
1684
|
+
if (isAndBehaviour) {
|
|
1685
|
+
isMet = platformsToCheck.every((platform) => playerPlatforms.has(platform.toLowerCase()));
|
|
1686
|
+
displayText = `Link all: ${platformsToCheck.join(', ')}`;
|
|
1687
|
+
}
|
|
1688
|
+
else {
|
|
1689
|
+
isMet = platformsToCheck.some((platform) => playerPlatforms.has(platform.toLowerCase()));
|
|
1690
|
+
displayText = `Link any: ${platformsToCheck.join(', ')}`;
|
|
1691
|
+
}
|
|
1692
|
+
if (addDetails) {
|
|
1693
|
+
conditionData.push({
|
|
1694
|
+
isMet,
|
|
1695
|
+
kind: 'identifiers',
|
|
1696
|
+
text: displayText,
|
|
1697
|
+
});
|
|
1698
|
+
if (!isMet)
|
|
1699
|
+
isValid = false;
|
|
1700
|
+
}
|
|
1701
|
+
else {
|
|
1702
|
+
if (!isMet)
|
|
1703
|
+
return { isValid: false };
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1499
1706
|
return { isValid, conditionData: addDetails ? conditionData : undefined };
|
|
1500
1707
|
};
|
|
1501
1708
|
const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, playerOffers, }) => {
|
|
@@ -1504,6 +1711,13 @@ const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, pl
|
|
|
1504
1711
|
// context is not in the list of surfacing contexts, so we don't want to surface this offer
|
|
1505
1712
|
return { isValid: false };
|
|
1506
1713
|
}
|
|
1714
|
+
if (surfacingConditions?.targetEntityTypes?.length) {
|
|
1715
|
+
const playerTarget = playerSnap.entityKind || 'default';
|
|
1716
|
+
// check if entity type is allowed
|
|
1717
|
+
if (!surfacingConditions.targetEntityTypes.includes(playerTarget)) {
|
|
1718
|
+
return { isValid: false };
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1507
1721
|
const conditions = surfacingConditions;
|
|
1508
1722
|
if (conditions?.andTags?.length) {
|
|
1509
1723
|
// check if player has all of the tags
|
|
@@ -1542,12 +1756,6 @@ const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, pl
|
|
|
1542
1756
|
(!playerSnap.dateSignedUp || playerSnap.dateSignedUp > conditions.maxDateSignedUp)) {
|
|
1543
1757
|
return { isValid: false };
|
|
1544
1758
|
}
|
|
1545
|
-
// Check dynamic conditions if present
|
|
1546
|
-
if (conditions.dynamic?.conditions?.length) {
|
|
1547
|
-
if (!meetsDynamicConditions(playerSnap.dynamic || {}, conditions.dynamic)) {
|
|
1548
|
-
return { isValid: false };
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
1759
|
const completedOfferIds = new Set();
|
|
1552
1760
|
for (const pOffer of playerOffers?.values() || []) {
|
|
1553
1761
|
if (pOffer.status === 'claimed' || pOffer.status === 'claimable') {
|
|
@@ -1583,6 +1791,10 @@ const hasConditions = (conditions) => {
|
|
|
1583
1791
|
return true;
|
|
1584
1792
|
if (conditions.minDaysInGame)
|
|
1585
1793
|
return true;
|
|
1794
|
+
if (conditions.dynamic?.conditions?.length)
|
|
1795
|
+
return true;
|
|
1796
|
+
if (conditions.identifiers?.platforms?.length)
|
|
1797
|
+
return true;
|
|
1586
1798
|
const surCond = conditions;
|
|
1587
1799
|
if (surCond.contexts?.length)
|
|
1588
1800
|
return true;
|
|
@@ -1602,6 +1814,12 @@ const hasConditions = (conditions) => {
|
|
|
1602
1814
|
return true;
|
|
1603
1815
|
if (surCond.completedOffers?.length)
|
|
1604
1816
|
return true;
|
|
1817
|
+
if (surCond.programmatic)
|
|
1818
|
+
return true;
|
|
1819
|
+
if (surCond.targetEntityTypes?.length)
|
|
1820
|
+
return true;
|
|
1821
|
+
if (surCond.links && Object.keys(surCond.links).length > 0)
|
|
1822
|
+
return true;
|
|
1605
1823
|
const compCond = conditions;
|
|
1606
1824
|
if (compCond.context)
|
|
1607
1825
|
return true;
|
|
@@ -1617,6 +1835,10 @@ const hasConditions = (conditions) => {
|
|
|
1617
1835
|
return true;
|
|
1618
1836
|
if (compCond.loginStreak)
|
|
1619
1837
|
return true;
|
|
1838
|
+
if (compCond.linkedCompletions)
|
|
1839
|
+
return true;
|
|
1840
|
+
if (compCond.dynamicTracker?.conditions?.length)
|
|
1841
|
+
return true;
|
|
1620
1842
|
return false;
|
|
1621
1843
|
};
|
|
1622
1844
|
const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, addDetails = false, }) => {
|
|
@@ -1730,51 +1952,107 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
|
|
|
1730
1952
|
}
|
|
1731
1953
|
}
|
|
1732
1954
|
if (conditions?.social) {
|
|
1733
|
-
const hasAttachedContent = !!completionTrackers?.social?.videoId;
|
|
1734
1955
|
const tSocial = completionTrackers?.social;
|
|
1735
1956
|
const cSocial = completionConditions.social;
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1957
|
+
const mode = cSocial?.mode || 'attach';
|
|
1958
|
+
const hasContent = Boolean(mode === 'accumulate'
|
|
1959
|
+
? tSocial?.mode === 'accumulate'
|
|
1960
|
+
: tSocial && tSocial.mode !== 'accumulate' && !!tSocial.videoId);
|
|
1961
|
+
const minLikes = cSocial?.minLikes || 0;
|
|
1962
|
+
const minViews = cSocial?.minViews || 0;
|
|
1963
|
+
const minComments = cSocial?.minComments || 0;
|
|
1964
|
+
const likes = tSocial?.likes || 0;
|
|
1965
|
+
const views = tSocial?.views || 0;
|
|
1966
|
+
const comments = tSocial?.comments || 0;
|
|
1967
|
+
let isDisqualify = !hasContent;
|
|
1968
|
+
if (likes < minLikes || views < minViews || comments < minComments) {
|
|
1744
1969
|
isDisqualify = true;
|
|
1745
1970
|
}
|
|
1746
1971
|
if (addDetails) {
|
|
1747
|
-
// Build detailed text about requirements
|
|
1748
1972
|
const platformMap = {
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1973
|
+
tiktok: 'TikTok',
|
|
1974
|
+
instagram: 'Instagram',
|
|
1975
|
+
youtube: 'YouTube',
|
|
1752
1976
|
};
|
|
1753
|
-
const platformText = conditions.social.platforms
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1977
|
+
const platformText = conditions.social.platforms
|
|
1978
|
+
.map((platform) => platformMap[platform])
|
|
1979
|
+
.join(' | ');
|
|
1980
|
+
const requiredWords = cSocial?.requiredWords ?? [];
|
|
1981
|
+
if (mode === 'accumulate') {
|
|
1982
|
+
const matchCount = (tSocial?.mode === 'accumulate' && tSocial.matchCount) || 0;
|
|
1983
|
+
conditionData.push({
|
|
1984
|
+
isMet: hasContent,
|
|
1985
|
+
kind: 'social',
|
|
1986
|
+
trackerAmount: matchCount,
|
|
1987
|
+
text: hasContent
|
|
1988
|
+
? `Found ${matchCount} matching ${platformText} post${matchCount !== 1 ? 's' : ''}`
|
|
1989
|
+
: requiredWords.length > 0
|
|
1990
|
+
? `Post ${platformText} content with ${requiredWords.map((w) => `"${w}"`).join(', ')}`
|
|
1991
|
+
: `Post ${platformText} content`,
|
|
1992
|
+
});
|
|
1993
|
+
}
|
|
1994
|
+
else {
|
|
1995
|
+
const title = (tSocial && tSocial.mode !== 'accumulate' && tSocial.title) || undefined;
|
|
1996
|
+
conditionData.push({
|
|
1997
|
+
isMet: hasContent,
|
|
1998
|
+
kind: 'social',
|
|
1999
|
+
trackerAmount: 0,
|
|
2000
|
+
text: !hasContent
|
|
2001
|
+
? requiredWords.length > 0
|
|
2002
|
+
? `Attach a ${platformText} post with ${requiredWords.map((w) => `"${w}"`).join(', ')} in the title`
|
|
2003
|
+
: `Attach a ${platformText} post`
|
|
2004
|
+
: `Attached: ${title}`,
|
|
2005
|
+
});
|
|
2006
|
+
}
|
|
2007
|
+
if (minLikes > 0) {
|
|
2008
|
+
conditionData.push({
|
|
2009
|
+
isMet: hasContent && likes >= minLikes,
|
|
2010
|
+
kind: 'social',
|
|
2011
|
+
trackerAmount: likes,
|
|
2012
|
+
text: mode === 'accumulate'
|
|
2013
|
+
? `Combined ${minLikes} Likes`
|
|
2014
|
+
: `Reach ${minLikes} Likes`,
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
if (minViews > 0) {
|
|
2018
|
+
conditionData.push({
|
|
2019
|
+
isMet: hasContent && views >= minViews,
|
|
2020
|
+
kind: 'social',
|
|
2021
|
+
trackerAmount: views,
|
|
2022
|
+
text: mode === 'accumulate'
|
|
2023
|
+
? `Combined ${minViews} Views`
|
|
2024
|
+
: `Reach ${minViews} Views`,
|
|
2025
|
+
});
|
|
1767
2026
|
}
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
:
|
|
2027
|
+
if (minComments > 0) {
|
|
2028
|
+
conditionData.push({
|
|
2029
|
+
isMet: hasContent && comments >= minComments,
|
|
2030
|
+
kind: 'social',
|
|
2031
|
+
trackerAmount: comments,
|
|
2032
|
+
text: mode === 'accumulate'
|
|
2033
|
+
? `Combined ${minComments} Comments`
|
|
2034
|
+
: `Reach ${minComments} Comments`,
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
if (isDisqualify)
|
|
2038
|
+
isValid = false;
|
|
2039
|
+
}
|
|
2040
|
+
else {
|
|
2041
|
+
if (isDisqualify)
|
|
2042
|
+
return { isValid: false };
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
// Linked completions - wait for N linked entities to complete
|
|
2046
|
+
if (conditions?.linkedCompletions?.min) {
|
|
2047
|
+
const currentCount = completionTrackers?.linkedCompletions || 0;
|
|
2048
|
+
const requiredCount = conditions.linkedCompletions.min;
|
|
2049
|
+
const isDisqualify = currentCount < requiredCount;
|
|
2050
|
+
if (addDetails) {
|
|
1773
2051
|
conditionData.push({
|
|
1774
|
-
isMet:
|
|
1775
|
-
kind: '
|
|
1776
|
-
trackerAmount,
|
|
1777
|
-
text:
|
|
2052
|
+
isMet: !isDisqualify,
|
|
2053
|
+
kind: 'linkedCompletions',
|
|
2054
|
+
trackerAmount: currentCount,
|
|
2055
|
+
text: `Wait for ${requiredCount} linked ${requiredCount === 1 ? 'entity' : 'entities'} to complete`,
|
|
1778
2056
|
});
|
|
1779
2057
|
if (isDisqualify)
|
|
1780
2058
|
isValid = false;
|
|
@@ -1784,6 +2062,22 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
|
|
|
1784
2062
|
return { isValid: false };
|
|
1785
2063
|
}
|
|
1786
2064
|
}
|
|
2065
|
+
if (conditions?.dynamicTracker?.conditions?.length) {
|
|
2066
|
+
const dynamicResult = meetsDynamicConditions(completionTrackers?.dynamicTracker || {}, conditions.dynamicTracker);
|
|
2067
|
+
if (addDetails) {
|
|
2068
|
+
conditionData.push({
|
|
2069
|
+
isMet: dynamicResult,
|
|
2070
|
+
kind: 'dynamic',
|
|
2071
|
+
text: renderTemplate(conditions.dynamicTracker.template, completionTrackers?.dynamicTracker) || 'Dynamic conditions',
|
|
2072
|
+
});
|
|
2073
|
+
if (!dynamicResult)
|
|
2074
|
+
isValid = false;
|
|
2075
|
+
}
|
|
2076
|
+
else {
|
|
2077
|
+
if (!dynamicResult)
|
|
2078
|
+
return { isValid: false };
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
1787
2081
|
const r = meetsBaseConditions({
|
|
1788
2082
|
conditions,
|
|
1789
2083
|
playerSnap,
|
|
@@ -1795,6 +2089,136 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
|
|
|
1795
2089
|
}
|
|
1796
2090
|
return { isValid: true, conditionData: [] };
|
|
1797
2091
|
};
|
|
2092
|
+
/**
|
|
2093
|
+
* Checks if completion conditions were met before a specific expiry time.
|
|
2094
|
+
* Returns true if all relevant condition fields were updated before expiryTime.
|
|
2095
|
+
*
|
|
2096
|
+
* @param completionConditions - The completion conditions to check
|
|
2097
|
+
* @param completionTrackers - The completion trackers (for buyItem, spendCurrency, etc.)
|
|
2098
|
+
* @param playerSnap - The player snapshot with field timestamps
|
|
2099
|
+
* @param expiryTime - The expiry timestamp in milliseconds
|
|
2100
|
+
* @returns true if all conditions were met before expiry, false otherwise
|
|
2101
|
+
*/
|
|
2102
|
+
const meetsCompletionConditionsBeforeExpiry = ({ completionConditions, completionTrackers, playerSnap, expiryTime, }) => {
|
|
2103
|
+
if (!completionConditions)
|
|
2104
|
+
return false;
|
|
2105
|
+
// Check if there are actually any conditions to evaluate
|
|
2106
|
+
if (!hasConditions(completionConditions))
|
|
2107
|
+
return false;
|
|
2108
|
+
// First check if conditions are actually met
|
|
2109
|
+
const conditionsMet = meetsCompletionConditions({
|
|
2110
|
+
completionConditions,
|
|
2111
|
+
completionTrackers,
|
|
2112
|
+
playerSnap,
|
|
2113
|
+
});
|
|
2114
|
+
if (!conditionsMet.isValid)
|
|
2115
|
+
return false;
|
|
2116
|
+
const lastSnapshotUpdate = new Date(playerSnap.snapshotLastUpdated).getTime();
|
|
2117
|
+
/**
|
|
2118
|
+
* Checks if a field was updated after the expiry time.
|
|
2119
|
+
* Returns true if updated AFTER or AT expiry (violates grace period).
|
|
2120
|
+
* Returns false if updated BEFORE expiry (allows grace period).
|
|
2121
|
+
*/
|
|
2122
|
+
function wasUpdatedAfterExpiry(data) {
|
|
2123
|
+
let lastUpdated;
|
|
2124
|
+
if (typeof data === 'object' && data !== null && !(data instanceof Date)) {
|
|
2125
|
+
// Object with optional lastUpdated field
|
|
2126
|
+
lastUpdated = data.lastUpdated
|
|
2127
|
+
? new Date(data.lastUpdated).getTime()
|
|
2128
|
+
: lastSnapshotUpdate;
|
|
2129
|
+
}
|
|
2130
|
+
else if (data instanceof Date) {
|
|
2131
|
+
lastUpdated = data.getTime();
|
|
2132
|
+
}
|
|
2133
|
+
else if (typeof data === 'string' || typeof data === 'number') {
|
|
2134
|
+
lastUpdated = new Date(data).getTime();
|
|
2135
|
+
}
|
|
2136
|
+
else {
|
|
2137
|
+
// No data provided, use snapshot timestamp
|
|
2138
|
+
lastUpdated = lastSnapshotUpdate;
|
|
2139
|
+
}
|
|
2140
|
+
return lastUpdated >= expiryTime;
|
|
2141
|
+
}
|
|
2142
|
+
if (completionConditions.currencies) {
|
|
2143
|
+
for (const currencyId in completionConditions.currencies) {
|
|
2144
|
+
const currency = playerSnap.currencies?.[currencyId];
|
|
2145
|
+
if (!currency)
|
|
2146
|
+
continue;
|
|
2147
|
+
if (wasUpdatedAfterExpiry(currency))
|
|
2148
|
+
return false;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
if (completionConditions.levels) {
|
|
2152
|
+
for (const skillId in completionConditions.levels) {
|
|
2153
|
+
const level = playerSnap.levels?.[skillId];
|
|
2154
|
+
if (!level)
|
|
2155
|
+
continue;
|
|
2156
|
+
if (wasUpdatedAfterExpiry(level))
|
|
2157
|
+
return false;
|
|
2158
|
+
}
|
|
2159
|
+
}
|
|
2160
|
+
if (completionConditions.quests) {
|
|
2161
|
+
for (const questId in completionConditions.quests) {
|
|
2162
|
+
const quest = playerSnap.quests?.[questId];
|
|
2163
|
+
if (!quest)
|
|
2164
|
+
continue;
|
|
2165
|
+
if (wasUpdatedAfterExpiry(quest))
|
|
2166
|
+
return false;
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
if (completionConditions.memberships) {
|
|
2170
|
+
for (const membershipId in completionConditions.memberships) {
|
|
2171
|
+
const membership = playerSnap.memberships?.[membershipId];
|
|
2172
|
+
if (!membership)
|
|
2173
|
+
continue;
|
|
2174
|
+
if (wasUpdatedAfterExpiry(membership))
|
|
2175
|
+
return false;
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
if (completionConditions.achievements) {
|
|
2179
|
+
for (const achievementId in completionConditions.achievements) {
|
|
2180
|
+
const achievement = playerSnap.achievements?.[achievementId];
|
|
2181
|
+
if (!achievement)
|
|
2182
|
+
continue;
|
|
2183
|
+
if (wasUpdatedAfterExpiry(achievement))
|
|
2184
|
+
return false;
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
if (completionConditions.stakedTokens) {
|
|
2188
|
+
for (const tokenId in completionConditions.stakedTokens) {
|
|
2189
|
+
const stakedToken = playerSnap.stakedTokens?.[tokenId];
|
|
2190
|
+
if (!stakedToken)
|
|
2191
|
+
continue;
|
|
2192
|
+
const lastStakeTime = new Date(stakedToken.lastStake ?? 0).getTime();
|
|
2193
|
+
const lastUnstakeTime = new Date(stakedToken.lastUnstake ?? 0).getTime();
|
|
2194
|
+
const lastUpdated = Math.max(lastStakeTime, lastUnstakeTime);
|
|
2195
|
+
if (lastUpdated >= expiryTime)
|
|
2196
|
+
return false;
|
|
2197
|
+
}
|
|
2198
|
+
}
|
|
2199
|
+
if (completionConditions.minTrustScore !== undefined ||
|
|
2200
|
+
completionConditions.maxTrustScore !== undefined) {
|
|
2201
|
+
if (wasUpdatedAfterExpiry(playerSnap.trustLastUpdated))
|
|
2202
|
+
return false;
|
|
2203
|
+
}
|
|
2204
|
+
if (completionConditions.minDaysInGame !== undefined) {
|
|
2205
|
+
if (wasUpdatedAfterExpiry(playerSnap.daysInGameLastUpdated))
|
|
2206
|
+
return false;
|
|
2207
|
+
}
|
|
2208
|
+
if (completionConditions.login || completionConditions.loginStreak) {
|
|
2209
|
+
if (wasUpdatedAfterExpiry())
|
|
2210
|
+
return false;
|
|
2211
|
+
}
|
|
2212
|
+
if (completionConditions.social) {
|
|
2213
|
+
// Check if social content was attached/validated after expiry
|
|
2214
|
+
if (completionTrackers?.social?.lastChecked) {
|
|
2215
|
+
if (wasUpdatedAfterExpiry(completionTrackers.social.lastChecked))
|
|
2216
|
+
return false;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
// All conditions were met before expiry
|
|
2220
|
+
return true;
|
|
2221
|
+
};
|
|
1798
2222
|
/**
|
|
1799
2223
|
* Checks if a dynamic object meets a set of dynamic field conditions.
|
|
1800
2224
|
* @param dynamicObj - The object with any key and string or number value.
|
|
@@ -1808,38 +2232,35 @@ function evaluateDynamicCondition(dynamicObj, cond) {
|
|
|
1808
2232
|
const val = dynamicObj[cond.key];
|
|
1809
2233
|
if (val === undefined)
|
|
1810
2234
|
return false;
|
|
2235
|
+
const isNumber = typeof val === 'number';
|
|
2236
|
+
const compareTo = isNumber ? Number(cond.compareTo) : String(cond.compareTo);
|
|
1811
2237
|
switch (cond.operator) {
|
|
1812
2238
|
case '==':
|
|
1813
|
-
return val ===
|
|
2239
|
+
return val === compareTo;
|
|
1814
2240
|
case '!=':
|
|
1815
|
-
return val !==
|
|
1816
|
-
case '>':
|
|
1817
|
-
return (typeof val === 'number' &&
|
|
1818
|
-
typeof cond.compareTo === 'number' &&
|
|
1819
|
-
val > cond.compareTo);
|
|
1820
|
-
case '>=':
|
|
1821
|
-
return (typeof val === 'number' &&
|
|
1822
|
-
typeof cond.compareTo === 'number' &&
|
|
1823
|
-
val >= cond.compareTo);
|
|
1824
|
-
case '<':
|
|
1825
|
-
return (typeof val === 'number' &&
|
|
1826
|
-
typeof cond.compareTo === 'number' &&
|
|
1827
|
-
val < cond.compareTo);
|
|
1828
|
-
case '<=':
|
|
1829
|
-
return (typeof val === 'number' &&
|
|
1830
|
-
typeof cond.compareTo === 'number' &&
|
|
1831
|
-
val <= cond.compareTo);
|
|
1832
|
-
case 'has':
|
|
1833
|
-
return (typeof val === 'string' &&
|
|
1834
|
-
typeof cond.compareTo === 'string' &&
|
|
1835
|
-
val.includes(cond.compareTo));
|
|
1836
|
-
case 'not_has':
|
|
1837
|
-
return (typeof val === 'string' &&
|
|
1838
|
-
typeof cond.compareTo === 'string' &&
|
|
1839
|
-
!val.includes(cond.compareTo));
|
|
1840
|
-
default:
|
|
1841
|
-
return false;
|
|
2241
|
+
return val !== compareTo;
|
|
1842
2242
|
}
|
|
2243
|
+
if (isNumber && typeof compareTo === 'number') {
|
|
2244
|
+
switch (cond.operator) {
|
|
2245
|
+
case '>':
|
|
2246
|
+
return val > compareTo;
|
|
2247
|
+
case '>=':
|
|
2248
|
+
return val >= compareTo;
|
|
2249
|
+
case '<':
|
|
2250
|
+
return val < compareTo;
|
|
2251
|
+
case '<=':
|
|
2252
|
+
return val <= compareTo;
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
else if (!isNumber && typeof compareTo === 'string') {
|
|
2256
|
+
switch (cond.operator) {
|
|
2257
|
+
case 'has':
|
|
2258
|
+
return val.includes(compareTo);
|
|
2259
|
+
case 'not_has':
|
|
2260
|
+
return !val.includes(compareTo);
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
return false;
|
|
1843
2264
|
}
|
|
1844
2265
|
/**
|
|
1845
2266
|
* Evaluates a group of dynamic conditions with logical links (AND, OR, AND NOT).
|
|
@@ -1872,11 +2293,34 @@ function meetsDynamicConditions(dynamicObj, dynamicGroup) {
|
|
|
1872
2293
|
}
|
|
1873
2294
|
return result;
|
|
1874
2295
|
}
|
|
2296
|
+
/**
|
|
2297
|
+
* Checks if a PlayerOffer meets its claimable conditions (completed -> claimable transition).
|
|
2298
|
+
* @param claimableConditions - The offer's claimableConditions (from IOffer)
|
|
2299
|
+
* @param claimableTrackers - The player offer's claimableTrackers
|
|
2300
|
+
*/
|
|
2301
|
+
function meetsClaimableConditions({ claimableConditions, playerOfferTrackers, claimableTrackers, }) {
|
|
2302
|
+
if (!claimableConditions) {
|
|
2303
|
+
return { isValid: true };
|
|
2304
|
+
}
|
|
2305
|
+
if (claimableConditions.siblingCompletions) {
|
|
2306
|
+
const siblingCount = playerOfferTrackers?.siblingPlayerOffer_ids?.length ?? 0;
|
|
2307
|
+
let completedCount = claimableTrackers?.siblingCompletions ?? 0;
|
|
2308
|
+
if (completedCount == -1)
|
|
2309
|
+
completedCount = siblingCount; // treat -1 as all completed
|
|
2310
|
+
// if siblings exist but not all are completed, return false
|
|
2311
|
+
if (siblingCount > 0 && completedCount < siblingCount) {
|
|
2312
|
+
return { isValid: false };
|
|
2313
|
+
}
|
|
2314
|
+
}
|
|
2315
|
+
return { isValid: true };
|
|
2316
|
+
}
|
|
1875
2317
|
|
|
2318
|
+
const offerListenerEvents = ['claim_offer'];
|
|
1876
2319
|
const PlayerOfferStatuses = [
|
|
1877
|
-
'inQueue',
|
|
2320
|
+
// 'inQueue', // fuck this shit. just don't surface offers if their offer plate is full.
|
|
1878
2321
|
'surfaced',
|
|
1879
2322
|
'viewed',
|
|
2323
|
+
'completed', // Individual completionConditions met, waiting for claimableConditions (e.g., siblings)
|
|
1880
2324
|
'claimable',
|
|
1881
2325
|
'claimed',
|
|
1882
2326
|
'expired',
|
|
@@ -1889,12 +2333,26 @@ const rewardKinds = [
|
|
|
1889
2333
|
'exp',
|
|
1890
2334
|
'trust_points',
|
|
1891
2335
|
'loyalty_currency', // loyalty currency that the player can exchange for rewards like on-chain via withdraw, etc.
|
|
2336
|
+
'discount', // handled by the external dev, using the rewardId to identify what it is for in their system
|
|
1892
2337
|
/** on-chain rewards require the builder to send funds to a custodial wallet that we use to send to player wallets*/
|
|
1893
2338
|
];
|
|
1894
2339
|
const rewardSchema = {
|
|
1895
2340
|
_id: false,
|
|
1896
2341
|
kind: { type: String, enum: rewardKinds },
|
|
1897
|
-
rewardId:
|
|
2342
|
+
rewardId: {
|
|
2343
|
+
type: String,
|
|
2344
|
+
validate: {
|
|
2345
|
+
validator: function (value) {
|
|
2346
|
+
// Require rewardId for item, coins, loyalty_currency, exp, and discount kinds
|
|
2347
|
+
const requiresRewardId = ['item', 'coins', 'loyalty_currency', 'exp', 'discount'].includes(this.kind);
|
|
2348
|
+
if (requiresRewardId) {
|
|
2349
|
+
return !!value;
|
|
2350
|
+
}
|
|
2351
|
+
return true;
|
|
2352
|
+
},
|
|
2353
|
+
message: 'rewardId is required for reward kinds: item, coins, loyalty_currency, exp, and discount',
|
|
2354
|
+
},
|
|
2355
|
+
},
|
|
1898
2356
|
skillId: String,
|
|
1899
2357
|
currencyId: String, // could be a loyalty currency
|
|
1900
2358
|
itemId: String,
|
|
@@ -1911,9 +2369,12 @@ exports.PlayerOfferStatuses = PlayerOfferStatuses;
|
|
|
1911
2369
|
exports.SSEConnection = SSEConnection;
|
|
1912
2370
|
exports.hasConditions = hasConditions;
|
|
1913
2371
|
exports.meetsBaseConditions = meetsBaseConditions;
|
|
2372
|
+
exports.meetsClaimableConditions = meetsClaimableConditions;
|
|
1914
2373
|
exports.meetsCompletionConditions = meetsCompletionConditions;
|
|
2374
|
+
exports.meetsCompletionConditionsBeforeExpiry = meetsCompletionConditionsBeforeExpiry;
|
|
1915
2375
|
exports.meetsDynamicConditions = meetsDynamicConditions;
|
|
1916
2376
|
exports.meetsSurfacingConditions = meetsSurfacingConditions;
|
|
2377
|
+
exports.offerListenerEvents = offerListenerEvents;
|
|
1917
2378
|
exports.rewardKinds = rewardKinds;
|
|
1918
2379
|
exports.rewardSchema = rewardSchema;
|
|
1919
2380
|
//# sourceMappingURL=index.js.map
|