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