@pixels-online/pixels-client-js-sdk 1.15.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.
@@ -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) {
@@ -1508,7 +1616,7 @@ const meetsBaseConditions = ({ conditions, playerSnap, addDetails, }) => {
1508
1616
  }
1509
1617
  }
1510
1618
  // Validate link count conditions
1511
- if (conditions?.links) {
1619
+ if (conditions?.links && 'entityLinks' in playerSnap) {
1512
1620
  for (const [linkType, constraint] of Object.entries(conditions.links)) {
1513
1621
  const linkCount = playerSnap.entityLinks?.filter((link) => link.kind === linkType).length || 0;
1514
1622
  if (constraint.min !== undefined) {
@@ -1565,7 +1673,7 @@ const meetsBaseConditions = ({ conditions, playerSnap, addDetails, }) => {
1565
1673
  return { isValid: false };
1566
1674
  }
1567
1675
  }
1568
- if (conditions?.identifiers?.platforms?.length) {
1676
+ if (conditions?.identifiers?.platforms?.length && 'identifiers' in playerSnap) {
1569
1677
  const playerPlatforms = new Set(playerSnap.identifiers?.map((i) => i.platform.toLowerCase()) || []);
1570
1678
  const isAndBehaviour = conditions.identifiers.behaviour === 'AND';
1571
1679
  const platformsToCheck = conditions.identifiers.platforms;
@@ -1602,7 +1710,7 @@ const meetsSurfacingConditions = ({ surfacingConditions, playerSnap, context, pl
1602
1710
  return { isValid: false };
1603
1711
  }
1604
1712
  if (surfacingConditions?.targetEntityTypes?.length) {
1605
- const playerTarget = playerSnap.target || 'default';
1713
+ const playerTarget = playerSnap.entityKind || 'default';
1606
1714
  // check if entity type is allowed
1607
1715
  if (!surfacingConditions.targetEntityTypes.includes(playerTarget)) {
1608
1716
  return { isValid: false };
@@ -1727,6 +1835,8 @@ const hasConditions = (conditions) => {
1727
1835
  return true;
1728
1836
  if (compCond.linkedCompletions)
1729
1837
  return true;
1838
+ if (compCond.dynamicTracker?.conditions?.length)
1839
+ return true;
1730
1840
  return false;
1731
1841
  };
1732
1842
  const meetsCompletionConditions = ({ completionConditions, completionTrackers, playerSnap, addDetails = false, }) => {
@@ -1950,6 +2060,22 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
1950
2060
  return { isValid: false };
1951
2061
  }
1952
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
+ }
1953
2079
  const r = meetsBaseConditions({
1954
2080
  conditions,
1955
2081
  playerSnap,
@@ -2104,38 +2230,35 @@ function evaluateDynamicCondition(dynamicObj, cond) {
2104
2230
  const val = dynamicObj[cond.key];
2105
2231
  if (val === undefined)
2106
2232
  return false;
2233
+ const isNumber = typeof val === 'number';
2234
+ const compareTo = isNumber ? Number(cond.compareTo) : String(cond.compareTo);
2107
2235
  switch (cond.operator) {
2108
2236
  case '==':
2109
- return val === cond.compareTo;
2237
+ return val === compareTo;
2110
2238
  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;
2239
+ return val !== compareTo;
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
+ }
2138
2260
  }
2261
+ return false;
2139
2262
  }
2140
2263
  /**
2141
2264
  * Evaluates a group of dynamic conditions with logical links (AND, OR, AND NOT).
@@ -2168,12 +2291,34 @@ function meetsDynamicConditions(dynamicObj, dynamicGroup) {
2168
2291
  }
2169
2292
  return result;
2170
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
+ }
2171
2315
 
2172
2316
  const offerListenerEvents = ['claim_offer'];
2173
2317
  const PlayerOfferStatuses = [
2174
2318
  // 'inQueue', // fuck this shit. just don't surface offers if their offer plate is full.
2175
2319
  'surfaced',
2176
2320
  'viewed',
2321
+ 'completed', // Individual completionConditions met, waiting for claimableConditions (e.g., siblings)
2177
2322
  'claimable',
2178
2323
  'claimed',
2179
2324
  'expired',
@@ -2214,5 +2359,5 @@ const rewardSchema = {
2214
2359
  image: String,
2215
2360
  };
2216
2361
 
2217
- export { AssetHelper, ConnectionState, EventEmitter, OfferEvent, OfferStore, OfferwallClient, PlayerOfferStatuses, SSEConnection, hasConditions, meetsBaseConditions, meetsCompletionConditions, meetsCompletionConditionsBeforeExpiry, meetsDynamicConditions, meetsSurfacingConditions, offerListenerEvents, rewardKinds, rewardSchema };
2362
+ export { AssetHelper, ConnectionState, EventEmitter, OfferEvent, OfferStore, OfferwallClient, PlayerOfferStatuses, SSEConnection, hasConditions, meetsBaseConditions, meetsClaimableConditions, meetsCompletionConditions, meetsCompletionConditionsBeforeExpiry, meetsDynamicConditions, meetsSurfacingConditions, offerListenerEvents, rewardKinds, rewardSchema };
2218
2363
  //# sourceMappingURL=index.esm.js.map