@stackedapp/utils 1.11.1 → 1.12.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.
@@ -12,14 +12,22 @@ export declare const meetsBaseConditions: ({ conditions, playerSnap, addDetails,
12
12
  additionalData?: StackedBaseUserExtra;
13
13
  }) => {
14
14
  isValid: boolean;
15
+ canClaim: boolean;
16
+ percentCompleted: number;
15
17
  conditionData?: undefined;
16
18
  } | {
19
+ /** @deprecated Use canClaim instead */
17
20
  isValid: boolean;
21
+ canClaim: boolean;
22
+ percentCompleted: number;
18
23
  conditionData: {
19
24
  isMet: boolean;
20
25
  kind?: keyof StackedBaseConditions;
26
+ /** @deprecated Use percentCompleted instead */
21
27
  trackerAmount?: number;
28
+ /** @deprecated Use percentCompleted instead */
22
29
  trackerGoal?: number;
30
+ percentCompleted: number;
23
31
  text: string;
24
32
  }[] | undefined;
25
33
  };
@@ -41,15 +49,23 @@ export declare const meetsLinkedEntityOffersCondition: ({ linkedEntityOffers, ma
41
49
  };
42
50
  export declare const offerMeetsCompletionConditions: (offer: StackedOffer, snapshot: StackedSnapshot, additionalData?: StackedBaseUserExtra) => {
43
51
  isValid: boolean;
52
+ canClaim: boolean;
53
+ percentCompleted: number;
44
54
  availableClaimsNow: number;
45
55
  conditionData?: undefined;
46
56
  } | {
57
+ /** @deprecated Use canClaim instead */
47
58
  isValid: boolean;
59
+ canClaim: boolean;
60
+ percentCompleted: number;
48
61
  conditionData: {
49
62
  isMet: boolean;
50
63
  kind?: keyof StackedCompletionConditions | "context";
64
+ /** @deprecated Use percentCompleted instead */
51
65
  trackerAmount?: number;
66
+ /** @deprecated Use percentCompleted instead */
52
67
  trackerGoal?: number;
68
+ percentCompleted: number;
53
69
  text: string;
54
70
  }[];
55
71
  availableClaimsNow: number;
@@ -67,15 +83,23 @@ export declare const meetsCompletionConditions: ({ completionConditions, complet
67
83
  additionalData?: StackedBaseUserExtra;
68
84
  }) => {
69
85
  isValid: boolean;
86
+ canClaim: boolean;
87
+ percentCompleted: number;
70
88
  availableClaimsNow: number;
71
89
  conditionData?: undefined;
72
90
  } | {
91
+ /** @deprecated Use canClaim instead */
73
92
  isValid: boolean;
93
+ canClaim: boolean;
94
+ percentCompleted: number;
74
95
  conditionData: {
75
96
  isMet: boolean;
76
97
  kind?: keyof StackedCompletionConditions | "context";
98
+ /** @deprecated Use percentCompleted instead */
77
99
  trackerAmount?: number;
100
+ /** @deprecated Use percentCompleted instead */
78
101
  trackerGoal?: number;
102
+ percentCompleted: number;
79
103
  text: string;
80
104
  }[];
81
105
  availableClaimsNow: number;
@@ -10,6 +10,58 @@ const template_1 = require("./template");
10
10
  const dynamic_1 = require("./dynamic");
11
11
  const blockchain_utils_1 = require("./blockchain_utils");
12
12
  exports.DEFAULT_ENTITY_KIND = '_default';
13
+ const calculatePercent = (amount, goal, isMet) => {
14
+ if (goal && goal > 0 && amount !== undefined) {
15
+ return Math.min(100, (amount / goal) * 100);
16
+ }
17
+ return isMet ? 100 : 0;
18
+ };
19
+ const calculateDynamicConditionPercent = (dynamicObj, cond) => {
20
+ if (!dynamicObj)
21
+ return 0;
22
+ const val = dynamicObj[cond.key];
23
+ if (typeof val !== 'number')
24
+ return 0;
25
+ const compareTo = typeof cond.compareTo === 'number' ? cond.compareTo : 0;
26
+ switch (cond.operator) {
27
+ case '>=':
28
+ case '>':
29
+ return compareTo > 0 ? Math.min(100, (val / compareTo) * 100) : val >= compareTo ? 100 : 0;
30
+ case '==':
31
+ return val === compareTo ? 100 : 0;
32
+ case '!=':
33
+ return val !== compareTo ? 100 : 0;
34
+ case '<=':
35
+ case '<':
36
+ return val <= compareTo ? 100 : 0; // Binary for max-style
37
+ default:
38
+ return 0;
39
+ }
40
+ };
41
+ const calculateDynamicGroupPercent = (dynamicObj, group) => {
42
+ if (!group?.conditions?.length)
43
+ return 100;
44
+ const percentages = group.conditions.map((c) => calculateDynamicConditionPercent(dynamicObj, c));
45
+ if (!group.links?.length) {
46
+ return percentages.reduce((sum, p) => sum + p, 0) / percentages.length;
47
+ }
48
+ let result = percentages[0];
49
+ for (let i = 0; i < group.links.length; i++) {
50
+ const nextPct = percentages[i + 1];
51
+ switch (group.links[i]) {
52
+ case 'AND':
53
+ result = (result + nextPct) / 2;
54
+ break;
55
+ case 'OR':
56
+ result = Math.max(result, nextPct);
57
+ break;
58
+ case 'AND NOT':
59
+ result = (result + (100 - nextPct)) / 2;
60
+ break;
61
+ }
62
+ }
63
+ return result;
64
+ };
13
65
  const meetsBaseConditions = ({ conditions, playerSnap, addDetails,
14
66
  /** this exists if calling meetsBaseConditions from meetsCompletionConditions. but surfacing
15
67
  * check doesn't use this since we don't have a playerOffer at surfacing time
@@ -20,48 +72,57 @@ additionalData, }) => {
20
72
  const conditionData = [];
21
73
  let isValid = true;
22
74
  if (conditions?.minDaysInGame) {
23
- const isDisqualify = (playerSnap.daysInGame || 0) < conditions.minDaysInGame;
75
+ const trackerAmount = playerSnap.daysInGame || 0;
76
+ const trackerGoal = conditions.minDaysInGame;
77
+ const isDisqualify = trackerAmount < trackerGoal;
24
78
  if (addDetails) {
25
79
  conditionData.push({
26
80
  isMet: !isDisqualify,
27
81
  kind: 'minDaysInGame',
28
- trackerAmount: playerSnap.daysInGame || 0,
29
- trackerGoal: conditions.minDaysInGame,
30
- text: `More than ${conditions.minDaysInGame} Days in Game`,
82
+ trackerAmount,
83
+ trackerGoal,
84
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
85
+ text: `More than ${trackerGoal} Days in Game`,
31
86
  });
32
87
  if (isDisqualify)
33
88
  isValid = false;
34
89
  }
35
90
  else {
36
91
  if (isDisqualify)
37
- return { isValid: false };
92
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
38
93
  }
39
94
  }
40
95
  if (conditions?.minTrustScore) {
41
- const isDisqualify = (playerSnap.trustScore || 0) < conditions.minTrustScore;
96
+ const trackerAmount = playerSnap.trustScore || 0;
97
+ const trackerGoal = conditions.minTrustScore;
98
+ const isDisqualify = trackerAmount < trackerGoal;
42
99
  if (addDetails) {
43
100
  conditionData.push({
44
101
  isMet: !isDisqualify,
45
102
  kind: 'minTrustScore',
46
- trackerAmount: playerSnap.trustScore || 0,
47
- trackerGoal: conditions.minTrustScore,
48
- text: `More than ${conditions.minTrustScore} Rep`,
103
+ trackerAmount,
104
+ trackerGoal,
105
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
106
+ text: `More than ${trackerGoal} Rep`,
49
107
  });
50
108
  if (isDisqualify)
51
109
  isValid = false;
52
110
  }
53
111
  else {
54
112
  if (isDisqualify)
55
- return { isValid: false };
113
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
56
114
  }
57
115
  }
58
116
  if (conditions?.maxTrustScore) {
59
- const isDisqualify = (playerSnap.trustScore || 0) > conditions.maxTrustScore;
117
+ const trackerAmount = playerSnap.trustScore || 0;
118
+ const isDisqualify = trackerAmount > conditions.maxTrustScore;
60
119
  if (addDetails) {
61
120
  conditionData.push({
62
121
  isMet: !isDisqualify,
63
122
  kind: 'maxTrustScore',
64
- trackerAmount: playerSnap.trustScore || 0,
123
+ trackerAmount,
124
+ trackerGoal: conditions.maxTrustScore,
125
+ percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
65
126
  text: `Less than ${conditions.maxTrustScore} Rep`,
66
127
  });
67
128
  if (isDisqualify)
@@ -69,7 +130,7 @@ additionalData, }) => {
69
130
  }
70
131
  else {
71
132
  if (isDisqualify)
72
- return { isValid: false };
133
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
73
134
  }
74
135
  }
75
136
  for (const key in conditions?.achievements) {
@@ -83,6 +144,7 @@ additionalData, }) => {
83
144
  kind: 'achievements',
84
145
  trackerAmount: 0,
85
146
  trackerGoal: 1,
147
+ percentCompleted: 0,
86
148
  text: `Have the achievement ${a.name}`,
87
149
  });
88
150
  if (isDisqualify)
@@ -90,25 +152,28 @@ additionalData, }) => {
90
152
  }
91
153
  else {
92
154
  if (isDisqualify)
93
- return { isValid: false };
155
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
94
156
  }
95
157
  }
96
158
  if (a.minCount) {
97
- const isDisqualify = (playerAchData?.count || 0) < a.minCount;
159
+ const trackerAmount = playerAchData?.count || 0;
160
+ const trackerGoal = a.minCount;
161
+ const isDisqualify = trackerAmount < trackerGoal;
98
162
  if (addDetails) {
99
163
  conditionData.push({
100
164
  isMet: !isDisqualify,
101
165
  kind: 'achievements',
102
- trackerAmount: playerAchData?.count || 0,
103
- trackerGoal: a.minCount,
104
- text: `Have the achievement ${a.name} more than ${a.minCount} times`,
166
+ trackerAmount,
167
+ trackerGoal,
168
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
169
+ text: `Have the achievement ${a.name} more than ${trackerGoal} times`,
105
170
  });
106
171
  if (isDisqualify)
107
172
  isValid = false;
108
173
  }
109
174
  else {
110
175
  if (isDisqualify)
111
- return { isValid: false };
176
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
112
177
  }
113
178
  }
114
179
  }
@@ -116,30 +181,36 @@ additionalData, }) => {
116
181
  const c = conditions.currencies[key];
117
182
  const playerCurrencyData = playerSnap.currencies?.[key];
118
183
  if (c.min) {
119
- const isDisqualify = (playerCurrencyData?.balance || 0) < c.min;
184
+ const trackerAmount = playerCurrencyData?.balance || 0;
185
+ const trackerGoal = c.min;
186
+ const isDisqualify = trackerAmount < trackerGoal;
120
187
  if (addDetails) {
121
188
  conditionData.push({
122
189
  isMet: !isDisqualify,
123
190
  kind: 'currencies',
124
- trackerAmount: playerCurrencyData?.balance || 0,
125
- trackerGoal: c.min,
126
- text: `Have more than ${c.min} ${c.name}`,
191
+ trackerAmount,
192
+ trackerGoal,
193
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
194
+ text: `Have more than ${trackerGoal} ${c.name}`,
127
195
  });
128
196
  if (isDisqualify)
129
197
  isValid = false;
130
198
  }
131
199
  else {
132
200
  if (isDisqualify)
133
- return { isValid: false };
201
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
134
202
  }
135
203
  }
136
204
  if (c.max) {
137
- const isDisqualify = (playerCurrencyData?.balance || 0) > c.max;
205
+ const trackerAmount = playerCurrencyData?.balance || 0;
206
+ const isDisqualify = trackerAmount > c.max;
138
207
  if (addDetails) {
139
208
  conditionData.push({
140
209
  isMet: !isDisqualify,
141
210
  kind: 'currencies',
142
- trackerAmount: playerCurrencyData?.balance || 0,
211
+ trackerAmount,
212
+ trackerGoal: c.max,
213
+ percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
143
214
  text: `Have less than ${c.max} ${c.name}`,
144
215
  });
145
216
  if (isDisqualify)
@@ -147,43 +218,49 @@ additionalData, }) => {
147
218
  }
148
219
  else {
149
220
  if (isDisqualify)
150
- return { isValid: false };
221
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
151
222
  }
152
223
  }
153
224
  if (c.in) {
154
- const isDisqualify = (playerCurrencyData?.in || 0) < c.in;
225
+ const trackerAmount = playerCurrencyData?.in || 0;
226
+ const trackerGoal = c.in;
227
+ const isDisqualify = trackerAmount < trackerGoal;
155
228
  if (addDetails) {
156
229
  conditionData.push({
157
230
  isMet: !isDisqualify,
158
231
  kind: 'currencies',
159
- trackerAmount: playerCurrencyData?.in || 0,
160
- trackerGoal: c.in,
161
- text: `Deposit at least ${c.in} ${c.name}`,
232
+ trackerAmount,
233
+ trackerGoal,
234
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
235
+ text: `Deposit at least ${trackerGoal} ${c.name}`,
162
236
  });
163
237
  if (isDisqualify)
164
238
  isValid = false;
165
239
  }
166
240
  else {
167
241
  if (isDisqualify)
168
- return { isValid: false };
242
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
169
243
  }
170
244
  }
171
245
  if (c.out) {
172
- const isDisqualify = (playerCurrencyData?.out || 0) < c.out;
246
+ const trackerAmount = playerCurrencyData?.out || 0;
247
+ const trackerGoal = c.out;
248
+ const isDisqualify = trackerAmount < trackerGoal;
173
249
  if (addDetails) {
174
250
  conditionData.push({
175
251
  isMet: !isDisqualify,
176
252
  kind: 'currencies',
177
- trackerAmount: playerCurrencyData?.out || 0,
178
- trackerGoal: c.out,
179
- text: `Withdraw at least ${c.out} ${c.name}`,
253
+ trackerAmount,
254
+ trackerGoal,
255
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
256
+ text: `Withdraw at least ${trackerGoal} ${c.name}`,
180
257
  });
181
258
  if (isDisqualify)
182
259
  isValid = false;
183
260
  }
184
261
  else {
185
262
  if (isDisqualify)
186
- return { isValid: false };
263
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
187
264
  }
188
265
  }
189
266
  }
@@ -191,30 +268,36 @@ additionalData, }) => {
191
268
  const l = conditions.levels[key];
192
269
  const playerLevelData = playerSnap.levels?.[key];
193
270
  if (l.min) {
194
- const isDisqualify = (playerLevelData?.level || 0) < l.min;
271
+ const trackerAmount = playerLevelData?.level || 0;
272
+ const trackerGoal = l.min;
273
+ const isDisqualify = trackerAmount < trackerGoal;
195
274
  if (addDetails) {
196
275
  conditionData.push({
197
276
  isMet: !isDisqualify,
198
277
  kind: 'levels',
199
- trackerAmount: playerLevelData?.level || 0,
200
- trackerGoal: l.min,
201
- text: `Be above level ${l.min} ${l.name}`,
278
+ trackerAmount,
279
+ trackerGoal,
280
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
281
+ text: `Be above level ${trackerGoal} ${l.name}`,
202
282
  });
203
283
  if (isDisqualify)
204
284
  isValid = false;
205
285
  }
206
286
  else {
207
287
  if (isDisqualify)
208
- return { isValid: false };
288
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
209
289
  }
210
290
  }
211
291
  if (l.max) {
212
- const isDisqualify = (playerLevelData?.level || 0) > l.max;
292
+ const trackerAmount = playerLevelData?.level || 0;
293
+ const isDisqualify = trackerAmount > l.max;
213
294
  if (addDetails) {
214
295
  conditionData.push({
215
296
  isMet: !isDisqualify,
216
297
  kind: 'levels',
217
- trackerAmount: playerLevelData?.level || 0,
298
+ trackerAmount,
299
+ trackerGoal: l.max,
300
+ percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
218
301
  text: `Be under level ${l.max} ${l.name}`,
219
302
  });
220
303
  if (isDisqualify)
@@ -222,7 +305,7 @@ additionalData, }) => {
222
305
  }
223
306
  else {
224
307
  if (isDisqualify)
225
- return { isValid: false };
308
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
226
309
  }
227
310
  }
228
311
  }
@@ -230,13 +313,16 @@ additionalData, }) => {
230
313
  for (const questId in conditions.quests) {
231
314
  const quest = conditions.quests[questId];
232
315
  const playerQuestData = playerSnap.quests?.[questId];
233
- const isDisqualify = playerQuestData ? (playerQuestData?.completions || 0) < (quest.completions || 0) : true; // if player has no data for this quest, they haven't completed it
316
+ const trackerAmount = playerQuestData?.completions || 0;
317
+ const trackerGoal = quest.completions || 1;
318
+ const isDisqualify = playerQuestData ? trackerAmount < (quest.completions || 0) : true;
234
319
  if (addDetails) {
235
320
  conditionData.push({
236
321
  isMet: !isDisqualify,
237
322
  kind: 'quests',
238
- trackerAmount: playerQuestData?.completions || 0,
239
- trackerGoal: quest.completions || 1,
323
+ trackerAmount,
324
+ trackerGoal,
325
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
240
326
  text: quest.completions === 1
241
327
  ? `Complete the quest ${quest.name}`
242
328
  : (quest.completions || 0) < 1
@@ -248,7 +334,7 @@ additionalData, }) => {
248
334
  }
249
335
  else {
250
336
  if (isDisqualify)
251
- return { isValid: false };
337
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
252
338
  }
253
339
  }
254
340
  }
@@ -256,30 +342,36 @@ additionalData, }) => {
256
342
  const m = conditions.memberships[key];
257
343
  const playerMembershipsData = playerSnap.memberships?.[key];
258
344
  if (m.minCount) {
259
- const isDisqualify = (playerMembershipsData?.count || 0) < m.minCount;
345
+ const trackerAmount = playerMembershipsData?.count || 0;
346
+ const trackerGoal = m.minCount;
347
+ const isDisqualify = trackerAmount < trackerGoal;
260
348
  if (addDetails) {
261
349
  conditionData.push({
262
350
  isMet: !isDisqualify,
263
351
  kind: 'memberships',
264
- trackerAmount: playerMembershipsData?.count || 0,
265
- trackerGoal: m.minCount,
266
- text: m.minCount > 1 ? `Have at least ${m.minCount} ${m.name} memberships` : `Have a ${m.name} membership`,
352
+ trackerAmount,
353
+ trackerGoal,
354
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
355
+ text: trackerGoal > 1 ? `Have at least ${trackerGoal} ${m.name} memberships` : `Have a ${m.name} membership`,
267
356
  });
268
357
  if (isDisqualify)
269
358
  isValid = false;
270
359
  }
271
360
  else {
272
361
  if (isDisqualify)
273
- return { isValid: false };
362
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
274
363
  }
275
364
  }
276
365
  if (m.maxCount) {
277
- const isDisqualify = (playerMembershipsData?.count || 0) > m.maxCount;
366
+ const trackerAmount = playerMembershipsData?.count || 0;
367
+ const isDisqualify = trackerAmount > m.maxCount;
278
368
  if (addDetails) {
279
369
  conditionData.push({
280
370
  isMet: !isDisqualify,
281
371
  kind: 'memberships',
282
- trackerAmount: playerMembershipsData?.count || 0,
372
+ trackerAmount,
373
+ trackerGoal: m.maxCount,
374
+ percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
283
375
  text: `Have less than ${m.maxCount} ${m.name} memberships`,
284
376
  });
285
377
  if (isDisqualify)
@@ -287,35 +379,41 @@ additionalData, }) => {
287
379
  }
288
380
  else {
289
381
  if (isDisqualify)
290
- return { isValid: false };
382
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
291
383
  }
292
384
  }
293
385
  const timeOwned = (playerMembershipsData?.expiresAt || 0) - Date.now();
294
386
  if (m.minMs) {
387
+ const trackerAmount = Number((timeOwned / (1000 * 60 * 60 * 24)).toFixed(1));
388
+ const trackerGoal = Number((m.minMs / (1000 * 60 * 60 * 24)).toFixed(1));
295
389
  const isDisqualify = timeOwned < m.minMs;
296
390
  if (addDetails) {
297
391
  conditionData.push({
298
392
  isMet: !isDisqualify,
299
393
  kind: 'memberships',
300
- trackerAmount: Number((timeOwned / (1000 * 60 * 60 * 24)).toFixed(1)),
301
- trackerGoal: Number((m.minMs / (1000 * 60 * 60 * 24)).toFixed(1)),
302
- text: `Own ${m.name} membership for at least ${(m.minMs / (1000 * 60 * 60 * 24)).toFixed(1)} days`,
394
+ trackerAmount,
395
+ trackerGoal,
396
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
397
+ text: `Own ${m.name} membership for at least ${trackerGoal} days`,
303
398
  });
304
399
  if (isDisqualify)
305
400
  isValid = false;
306
401
  }
307
402
  else {
308
403
  if (isDisqualify)
309
- return { isValid: false };
404
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
310
405
  }
311
406
  }
312
407
  if (m.maxMs) {
408
+ const trackerAmount = Number((timeOwned / (1000 * 60 * 60 * 24)).toFixed(1));
313
409
  const isDisqualify = timeOwned > m.maxMs;
314
410
  if (addDetails) {
315
411
  conditionData.push({
316
412
  isMet: !isDisqualify,
317
413
  kind: 'memberships',
318
- trackerAmount: Number((timeOwned / (1000 * 60 * 60 * 24)).toFixed(1)),
414
+ trackerAmount,
415
+ trackerGoal: Number((m.maxMs / (1000 * 60 * 60 * 24)).toFixed(1)),
416
+ percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
319
417
  text: `Own ${m.name} membership for less than ${(m.maxMs / (1000 * 60 * 60 * 24)).toFixed(1)} days`,
320
418
  });
321
419
  if (isDisqualify)
@@ -323,7 +421,7 @@ additionalData, }) => {
323
421
  }
324
422
  else {
325
423
  if (isDisqualify)
326
- return { isValid: false };
424
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
327
425
  }
328
426
  }
329
427
  }
@@ -331,21 +429,24 @@ additionalData, }) => {
331
429
  const s = conditions.stakedTokens[key];
332
430
  const playerStakedData = playerSnap.stakedTokens?.[key];
333
431
  if (s.min) {
334
- const isDisqualify = (playerStakedData?.balance || 0) < s.min;
432
+ const trackerAmount = playerStakedData?.balance || 0;
433
+ const trackerGoal = s.min;
434
+ const isDisqualify = trackerAmount < trackerGoal;
335
435
  if (addDetails) {
336
436
  conditionData.push({
337
437
  isMet: !isDisqualify,
338
438
  kind: 'stakedTokens',
339
- trackerAmount: playerStakedData?.balance || 0,
340
- trackerGoal: s.min,
341
- text: `Have at least ${s.min} ${s.name} staked`,
439
+ trackerAmount,
440
+ trackerGoal,
441
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
442
+ text: `Have at least ${trackerGoal} ${s.name} staked`,
342
443
  });
343
444
  if (isDisqualify)
344
445
  isValid = false;
345
446
  }
346
447
  else {
347
448
  if (isDisqualify)
348
- return { isValid: false };
449
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
349
450
  }
350
451
  }
351
452
  }
@@ -355,13 +456,16 @@ additionalData, }) => {
355
456
  // linkType should always exist. and be default is none was specified
356
457
  const linkCount = playerSnap.entityLinks?.filter((link) => (link.kind || exports.DEFAULT_ENTITY_KIND) === linkType).length || 0;
357
458
  if (constraint.min !== undefined) {
358
- const isDisqualify = linkCount < constraint.min;
459
+ const trackerAmount = linkCount;
460
+ const trackerGoal = constraint.min;
461
+ const isDisqualify = trackerAmount < trackerGoal;
359
462
  if (addDetails) {
360
463
  conditionData.push({
361
464
  isMet: !isDisqualify,
362
465
  kind: 'links',
363
- trackerAmount: linkCount,
364
- trackerGoal: constraint.min,
466
+ trackerAmount,
467
+ trackerGoal,
468
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
365
469
  text: constraint.template
366
470
  ? (0, template_1.renderTemplate)(constraint.template, {
367
471
  current: linkCount,
@@ -369,23 +473,26 @@ additionalData, }) => {
369
473
  max: constraint.max ?? 0,
370
474
  type: linkType,
371
475
  })
372
- : `At least ${constraint.min} ${linkType} link(s)`,
476
+ : `At least ${trackerGoal} ${linkType} link(s)`,
373
477
  });
374
478
  if (isDisqualify)
375
479
  isValid = false;
376
480
  }
377
481
  else {
378
482
  if (isDisqualify)
379
- return { isValid: false };
483
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
380
484
  }
381
485
  }
382
486
  if (constraint.max !== undefined) {
383
- const isDisqualify = linkCount > constraint.max;
487
+ const trackerAmount = linkCount;
488
+ const isDisqualify = trackerAmount > constraint.max;
384
489
  if (addDetails) {
385
490
  conditionData.push({
386
491
  isMet: !isDisqualify,
387
492
  kind: 'links',
388
- trackerAmount: linkCount,
493
+ trackerAmount,
494
+ trackerGoal: constraint.max,
495
+ percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
389
496
  text: constraint.template
390
497
  ? (0, template_1.renderTemplate)(constraint.template, {
391
498
  current: linkCount,
@@ -400,7 +507,7 @@ additionalData, }) => {
400
507
  }
401
508
  else {
402
509
  if (isDisqualify)
403
- return { isValid: false };
510
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
404
511
  }
405
512
  }
406
513
  }
@@ -408,22 +515,42 @@ additionalData, }) => {
408
515
  // Evaluate dynamic conditions
409
516
  if (conditions?.dynamic?.conditions?.length) {
410
517
  const resolvedConditions = (0, template_1.replaceDynamicConditionKeys)(conditions.dynamic.conditions, playerOffer?.trackers || {});
411
- const dynamicResult = meetsDynamicConditions(playerSnap.dynamic, {
518
+ const dynamicObj = playerSnap.dynamic || {};
519
+ const dynamicResult = meetsDynamicConditions(dynamicObj, {
412
520
  ...conditions.dynamic,
413
521
  conditions: resolvedConditions,
414
522
  });
415
523
  if (addDetails) {
524
+ // Extract tracker amount and goal from the first condition for display
525
+ let trackerAmount;
526
+ let trackerGoal;
527
+ if (resolvedConditions.length > 0) {
528
+ const firstCond = resolvedConditions[0];
529
+ if (typeof firstCond.compareTo === 'number') {
530
+ trackerGoal = firstCond.compareTo;
531
+ }
532
+ const val = dynamicObj[firstCond.key];
533
+ trackerAmount = typeof val === 'number' ? val : 0;
534
+ }
535
+ // Calculate percentage using the dynamic group helper for AND/OR logic
536
+ const percentCompleted = calculateDynamicGroupPercent(dynamicObj, {
537
+ ...conditions.dynamic,
538
+ conditions: resolvedConditions,
539
+ });
416
540
  conditionData.push({
417
541
  isMet: dynamicResult,
418
542
  kind: 'dynamic',
419
- text: (0, template_1.renderTemplate)(conditions.dynamic.template, playerSnap.dynamic) || 'Dynamic conditions',
543
+ trackerAmount,
544
+ trackerGoal,
545
+ percentCompleted,
546
+ text: (0, template_1.renderTemplate)(conditions.dynamic.template, dynamicObj) || 'Dynamic conditions',
420
547
  });
421
548
  if (!dynamicResult)
422
549
  isValid = false;
423
550
  }
424
551
  else {
425
552
  if (!dynamicResult)
426
- return { isValid: false };
553
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
427
554
  }
428
555
  }
429
556
  if (conditions?.identifiers?.platforms?.length && 'identifiers' in playerSnap) {
@@ -446,6 +573,7 @@ additionalData, }) => {
446
573
  kind: 'identifiers',
447
574
  trackerAmount: isMet ? 1 : 0,
448
575
  trackerGoal: 1,
576
+ percentCompleted: isMet ? 100 : 0, // Binary
449
577
  text: displayText,
450
578
  });
451
579
  if (!isMet)
@@ -453,7 +581,7 @@ additionalData, }) => {
453
581
  }
454
582
  else {
455
583
  if (!isMet)
456
- return { isValid: false };
584
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
457
585
  }
458
586
  }
459
587
  // Evaluate token balance conditions
@@ -466,30 +594,36 @@ additionalData, }) => {
466
594
  totalBalance += fetchedBalances[balanceKey] || 0;
467
595
  }
468
596
  if (tokenCond.min !== undefined) {
469
- const isDisqualify = totalBalance < tokenCond.min;
597
+ const trackerAmount = totalBalance;
598
+ const trackerGoal = tokenCond.min;
599
+ const isDisqualify = trackerAmount < trackerGoal;
470
600
  if (addDetails) {
471
601
  conditionData.push({
472
602
  isMet: !isDisqualify,
473
603
  kind: 'tokenBalances',
474
- trackerAmount: totalBalance,
475
- trackerGoal: tokenCond.min,
476
- text: `Have at least ${tokenCond.min} ${tokenCond.name || 'tokens'}`,
604
+ trackerAmount,
605
+ trackerGoal,
606
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
607
+ text: `Have at least ${trackerGoal} ${tokenCond.name || 'tokens'}`,
477
608
  });
478
609
  if (isDisqualify)
479
610
  isValid = false;
480
611
  }
481
612
  else {
482
613
  if (isDisqualify)
483
- return { isValid: false };
614
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
484
615
  }
485
616
  }
486
617
  if (tokenCond.max !== undefined) {
487
- const isDisqualify = totalBalance > tokenCond.max;
618
+ const trackerAmount = totalBalance;
619
+ const isDisqualify = trackerAmount > tokenCond.max;
488
620
  if (addDetails) {
489
621
  conditionData.push({
490
622
  isMet: !isDisqualify,
491
623
  kind: 'tokenBalances',
492
- trackerAmount: totalBalance,
624
+ trackerAmount,
625
+ trackerGoal: tokenCond.max,
626
+ percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
493
627
  text: `Have at most ${tokenCond.max} ${tokenCond.name || 'tokens'}`,
494
628
  });
495
629
  if (isDisqualify)
@@ -497,11 +631,23 @@ additionalData, }) => {
497
631
  }
498
632
  else {
499
633
  if (isDisqualify)
500
- return { isValid: false };
634
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
501
635
  }
502
636
  }
503
637
  }
504
- return { isValid, conditionData: addDetails ? conditionData : undefined };
638
+ // Calculate top-level percentCompleted as average of all condition percentages
639
+ const percentCompleted = conditionData.length > 0
640
+ ? conditionData.reduce((sum, c) => sum + c.percentCompleted, 0) / conditionData.length
641
+ : isValid
642
+ ? 100
643
+ : 0;
644
+ return {
645
+ /** @deprecated Use canClaim instead */
646
+ isValid,
647
+ canClaim: isValid,
648
+ percentCompleted,
649
+ conditionData: addDetails ? conditionData : undefined,
650
+ };
505
651
  };
506
652
  exports.meetsBaseConditions = meetsBaseConditions;
507
653
  const hasCompletionConditions = (conditions) => {
@@ -603,6 +749,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
603
749
  kind: 'context',
604
750
  trackerAmount: hasTrackedContext ? 1 : 0,
605
751
  trackerGoal: 1,
752
+ percentCompleted: !isDisqualify ? 100 : 0, // Binary
606
753
  text: completionConditions.context.name,
607
754
  });
608
755
  if (isDisqualify)
@@ -610,7 +757,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
610
757
  }
611
758
  else {
612
759
  if (isDisqualify)
613
- return { isValid: false, availableClaimsNow: 0 };
760
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
614
761
  }
615
762
  }
616
763
  if (conditions?.buyItem) {
@@ -627,6 +774,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
627
774
  kind: 'buyItem',
628
775
  trackerAmount: trackerValue,
629
776
  trackerGoal: scaledAmount,
777
+ percentCompleted: calculatePercent(trackerValue, scaledAmount, !isDisqualify),
630
778
  text: `Buy ${scaledAmount} ${conditions.buyItem.name}`,
631
779
  });
632
780
  if (isDisqualify)
@@ -634,7 +782,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
634
782
  }
635
783
  else {
636
784
  if (isDisqualify)
637
- return { isValid: false, availableClaimsNow: 0 };
785
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
638
786
  }
639
787
  }
640
788
  if (conditions?.spendCurrency) {
@@ -651,6 +799,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
651
799
  kind: 'spendCurrency',
652
800
  trackerAmount: trackerValue,
653
801
  trackerGoal: scaledAmount,
802
+ percentCompleted: calculatePercent(trackerValue, scaledAmount, !isDisqualify),
654
803
  text: `Spend ${scaledAmount} ${conditions.spendCurrency.name}`,
655
804
  });
656
805
  if (isDisqualify)
@@ -658,7 +807,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
658
807
  }
659
808
  else {
660
809
  if (isDisqualify)
661
- return { isValid: false, availableClaimsNow: 0 };
810
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
662
811
  }
663
812
  }
664
813
  if (conditions?.depositCurrency) {
@@ -675,6 +824,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
675
824
  kind: 'depositCurrency',
676
825
  trackerAmount: trackerValue,
677
826
  trackerGoal: scaledAmount,
827
+ percentCompleted: calculatePercent(trackerValue, scaledAmount, !isDisqualify),
678
828
  text: `Deposit ${scaledAmount} ${conditions.depositCurrency.name}`,
679
829
  });
680
830
  if (isDisqualify)
@@ -682,7 +832,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
682
832
  }
683
833
  else {
684
834
  if (isDisqualify)
685
- return { isValid: false, availableClaimsNow: 0 };
835
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
686
836
  }
687
837
  }
688
838
  if (conditions?.login) {
@@ -693,6 +843,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
693
843
  kind: 'login',
694
844
  trackerAmount: isMet ? 1 : 0,
695
845
  trackerGoal: 1,
846
+ percentCompleted: isMet ? 100 : 0, // Binary
696
847
  text: `Login to the game`,
697
848
  });
698
849
  if (!isMet)
@@ -700,28 +851,30 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
700
851
  }
701
852
  else {
702
853
  if (!isMet)
703
- return { isValid: false, availableClaimsNow: 0 };
854
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
704
855
  }
705
856
  }
706
857
  if (conditions?.loginStreak) {
707
858
  // player's login streak snapshot right now - their login streak when offer was surfaced = their login streak since the offer was surfaced
708
859
  // if their login streak since the offer was surfaced is less than the required login streak, then they are not yet eligible for the offer
709
- const streakSinceOffer = (playerSnap.loginStreak || 0) - (completionTrackers?.currentLoginStreak || 0);
710
- const isDisqualify = streakSinceOffer + 1 < conditions.loginStreak;
860
+ const trackerAmount = (playerSnap.loginStreak || 0) - (completionTrackers?.currentLoginStreak || 0) + 1;
861
+ const trackerGoal = conditions.loginStreak;
862
+ const isDisqualify = trackerAmount < trackerGoal;
711
863
  if (addDetails) {
712
864
  conditionData.push({
713
865
  isMet: !isDisqualify,
714
866
  kind: 'loginStreak',
715
- trackerAmount: streakSinceOffer + 1,
716
- trackerGoal: conditions.loginStreak,
717
- text: `Login streak of ${conditions.loginStreak || 0} days`,
867
+ trackerAmount,
868
+ trackerGoal,
869
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
870
+ text: `Login streak of ${trackerGoal || 0} days`,
718
871
  });
719
872
  if (isDisqualify)
720
873
  isValid = false;
721
874
  }
722
875
  else {
723
876
  if (isDisqualify)
724
- return { isValid: false, availableClaimsNow: 0 };
877
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
725
878
  }
726
879
  }
727
880
  if (conditions?.social) {
@@ -754,6 +907,21 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
754
907
  if (baseComments > 0)
755
908
  updateMax(Math.floor(comments / baseComments));
756
909
  }
910
+ // Calculate social percentage - average of all applicable metrics
911
+ const socialPercentages = [];
912
+ if (minLikes > 0)
913
+ socialPercentages.push(calculatePercent(likes, minLikes, likes >= minLikes));
914
+ if (minViews > 0)
915
+ socialPercentages.push(calculatePercent(views, minViews, views >= minViews));
916
+ if (minComments > 0)
917
+ socialPercentages.push(calculatePercent(comments, minComments, comments >= minComments));
918
+ if (!hasContent)
919
+ socialPercentages.push(0);
920
+ const socialPercent = socialPercentages.length > 0
921
+ ? socialPercentages.reduce((a, b) => a + b, 0) / socialPercentages.length
922
+ : hasContent
923
+ ? 100
924
+ : 0;
757
925
  if (addDetails) {
758
926
  const platformMap = {
759
927
  tiktok: 'TikTok',
@@ -765,10 +933,11 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
765
933
  if (mode === 'accumulate') {
766
934
  const matchCount = tSocialAccumulate?.matchCount || 0;
767
935
  conditionData.push({
768
- isMet: hasContent,
936
+ isMet: hasContent && !isDisqualify,
769
937
  kind: 'social',
770
938
  trackerAmount: matchCount,
771
939
  trackerGoal: 1,
940
+ percentCompleted: socialPercent,
772
941
  text: hasContent
773
942
  ? `Found ${matchCount} matching ${platformText} post${matchCount !== 1 ? 's' : ''}`
774
943
  : requiredWords.length > 0
@@ -779,10 +948,11 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
779
948
  else {
780
949
  const title = tSocialAttach?.title;
781
950
  conditionData.push({
782
- isMet: hasContent,
951
+ isMet: hasContent && !isDisqualify,
783
952
  kind: 'social',
784
953
  trackerAmount: hasContent ? 1 : 0,
785
954
  trackerGoal: 1,
955
+ percentCompleted: socialPercent,
786
956
  text: !hasContent
787
957
  ? requiredWords.length > 0
788
958
  ? `Attach a ${platformText} post with ${requiredWords.map((w) => `"${w}"`).join(', ')} in the title`
@@ -796,6 +966,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
796
966
  kind: 'social',
797
967
  trackerAmount: likes,
798
968
  trackerGoal: minLikes,
969
+ percentCompleted: calculatePercent(likes, minLikes, hasContent && likes >= minLikes),
799
970
  text: mode === 'accumulate' ? `Combined ${minLikes} Likes` : `Reach ${minLikes} Likes`,
800
971
  });
801
972
  }
@@ -805,6 +976,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
805
976
  kind: 'social',
806
977
  trackerAmount: views,
807
978
  trackerGoal: minViews,
979
+ percentCompleted: calculatePercent(views, minViews, hasContent && views >= minViews),
808
980
  text: mode === 'accumulate' ? `Combined ${minViews} Views` : `Reach ${minViews} Views`,
809
981
  });
810
982
  }
@@ -814,6 +986,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
814
986
  kind: 'social',
815
987
  trackerAmount: comments,
816
988
  trackerGoal: minComments,
989
+ percentCompleted: calculatePercent(comments, minComments, hasContent && comments >= minComments),
817
990
  text: mode === 'accumulate' ? `Combined ${minComments} Comments` : `Reach ${minComments} Comments`,
818
991
  });
819
992
  }
@@ -822,87 +995,109 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
822
995
  }
823
996
  else {
824
997
  if (isDisqualify)
825
- return { isValid: false, availableClaimsNow: 0 };
998
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
826
999
  }
827
1000
  }
828
1001
  // Linked completions - wait for N linked entities to complete
829
1002
  if (conditions?.linkedCompletions?.min) {
830
1003
  const baseMin = conditions.linkedCompletions.min;
831
- const currentCount = completionTrackers?.linkedCompletions || 0;
832
- const scaledMin = baseMin * claimMultiplier;
833
- const isDisqualify = currentCount < scaledMin;
1004
+ const trackerAmount = completionTrackers?.linkedCompletions || 0;
1005
+ const trackerGoal = baseMin * claimMultiplier;
1006
+ const isDisqualify = trackerAmount < trackerGoal;
834
1007
  if (shouldScale && baseMin > 0) {
835
- updateMax(Math.floor(currentCount / baseMin));
1008
+ updateMax(Math.floor(trackerAmount / baseMin));
836
1009
  }
837
1010
  if (addDetails) {
838
1011
  conditionData.push({
839
1012
  isMet: !isDisqualify,
840
1013
  kind: 'linkedCompletions',
841
- trackerAmount: currentCount,
842
- trackerGoal: scaledMin,
1014
+ trackerAmount,
1015
+ trackerGoal,
1016
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
843
1017
  text: conditions.linkedCompletions.template
844
1018
  ? (0, template_1.renderTemplate)(conditions.linkedCompletions.template, {
845
- current: currentCount,
846
- required: scaledMin,
1019
+ current: trackerAmount,
1020
+ required: trackerGoal,
847
1021
  })
848
- : `Wait for ${scaledMin} linked ${scaledMin === 1 ? 'entity' : 'entities'} to complete`,
1022
+ : `Wait for ${trackerGoal} linked ${trackerGoal === 1 ? 'entity' : 'entities'} to complete`,
849
1023
  });
850
1024
  if (isDisqualify)
851
1025
  isValid = false;
852
1026
  }
853
1027
  else {
854
1028
  if (isDisqualify)
855
- return { isValid: false, availableClaimsNow: 0 };
1029
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
856
1030
  }
857
1031
  }
858
1032
  if (conditions?.dynamicTracker?.conditions?.length) {
859
1033
  const resolvedConditions = (0, template_1.replaceDynamicConditionKeys)(conditions.dynamicTracker.conditions, playerOffer?.trackers || {});
860
- // now we have the game-defined conditions with {{}} keys populated. feed these conditions into evaluator
861
- const dynamicResult = meetsDynamicConditions((0, dynamic_1.dynamicTrackerToPrimitive)(completionTrackers?.dynamicTracker || {}), {
1034
+ // Convert { key: { value: X } } to { key: X } format
1035
+ const primitiveTrackers = (0, dynamic_1.dynamicTrackerToPrimitive)(completionTrackers?.dynamicTracker || {});
1036
+ const dynamicResult = meetsDynamicConditions(primitiveTrackers, {
862
1037
  ...conditions.dynamicTracker,
863
1038
  conditions: resolvedConditions,
864
1039
  }, claimMultiplier);
865
1040
  if (shouldScale) {
866
- const dynamicMax = getMaxClaimsForDynamicGroup((0, dynamic_1.dynamicTrackerToPrimitive)(completionTrackers?.dynamicTracker || {}), {
1041
+ const dynamicMax = getMaxClaimsForDynamicGroup(primitiveTrackers, {
867
1042
  ...conditions.dynamicTracker,
868
1043
  conditions: resolvedConditions,
869
1044
  }, playerOffer?.trackers?.claimedCount || 0);
870
1045
  updateMax(dynamicMax);
871
1046
  }
872
1047
  if (addDetails) {
1048
+ // Extract tracker amount and goal from the first condition for display
1049
+ let trackerAmount;
1050
+ let trackerGoal;
1051
+ if (resolvedConditions.length > 0) {
1052
+ const firstCond = resolvedConditions[0];
1053
+ if (typeof firstCond.compareTo === 'number') {
1054
+ trackerGoal = firstCond.compareTo;
1055
+ }
1056
+ const val = primitiveTrackers[firstCond.key];
1057
+ trackerAmount = typeof val === 'number' ? val : 0;
1058
+ }
1059
+ // Calculate percentage using the dynamic group helper for AND/OR logic
1060
+ const percentCompleted = calculateDynamicGroupPercent(primitiveTrackers, {
1061
+ ...conditions.dynamicTracker,
1062
+ conditions: resolvedConditions,
1063
+ });
873
1064
  conditionData.push({
874
1065
  isMet: dynamicResult,
875
1066
  kind: 'dynamicTracker',
876
- text: (0, template_1.renderTemplate)(conditions.dynamicTracker.template, (0, dynamic_1.dynamicTrackerToPrimitive)(completionTrackers?.dynamicTracker || {})) || 'Dynamic conditions',
1067
+ trackerAmount,
1068
+ trackerGoal,
1069
+ percentCompleted,
1070
+ text: (0, template_1.renderTemplate)(conditions.dynamicTracker.template, primitiveTrackers) || 'Dynamic conditions',
877
1071
  });
878
1072
  if (!dynamicResult)
879
1073
  isValid = false;
880
1074
  }
881
1075
  else {
882
1076
  if (!dynamicResult)
883
- return { isValid: false, availableClaimsNow: 0 };
1077
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
884
1078
  }
885
1079
  }
886
1080
  // Evaluate contractInteractions completion trackers
887
1081
  if (conditions?.contractInteractions) {
888
1082
  for (const [conditionId, condition] of Object.entries(conditions.contractInteractions)) {
889
1083
  const baseAmount = condition.amount || 0;
890
- const scaledAmount = baseAmount * claimMultiplier;
891
- const trackerValue = completionTrackers?.contractInteractions?.[conditionId] || 0;
892
- const isDisqualify = trackerValue < scaledAmount;
1084
+ const trackerGoal = baseAmount * claimMultiplier;
1085
+ const trackerAmount = completionTrackers?.contractInteractions?.[conditionId] || 0;
1086
+ const isDisqualify = trackerAmount < trackerGoal;
893
1087
  if (shouldScale && baseAmount > 0) {
894
- updateMax(Math.floor(trackerValue / baseAmount));
1088
+ updateMax(Math.floor(trackerAmount / baseAmount));
895
1089
  }
896
1090
  if (addDetails) {
897
1091
  const displayText = (0, template_1.renderTemplate)(condition.template, {
898
- current: trackerValue,
899
- amount: scaledAmount,
1092
+ current: trackerAmount,
1093
+ amount: trackerGoal,
900
1094
  });
901
1095
  conditionData.push({
902
1096
  isMet: !isDisqualify,
903
1097
  kind: 'contractInteractions',
904
- trackerAmount: trackerValue,
905
- trackerGoal: scaledAmount,
1098
+ trackerAmount,
1099
+ trackerGoal,
1100
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
906
1101
  text: displayText,
907
1102
  });
908
1103
  if (isDisqualify)
@@ -910,7 +1105,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
910
1105
  }
911
1106
  else {
912
1107
  if (isDisqualify)
913
- return { isValid: false, availableClaimsNow: 0 };
1108
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
914
1109
  }
915
1110
  }
916
1111
  }
@@ -932,9 +1127,22 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
932
1127
  : maxTotalClaimsFromScaling === Infinity
933
1128
  ? -1
934
1129
  : Math.max(0, maxTotalClaimsFromScaling - claimedCount);
935
- return { isValid, conditionData, availableClaimsNow };
1130
+ // Calculate top-level percentCompleted as average of all condition percentages
1131
+ const percentCompleted = conditionData.length > 0
1132
+ ? conditionData.reduce((sum, c) => sum + c.percentCompleted, 0) / conditionData.length
1133
+ : isValid
1134
+ ? 100
1135
+ : 0;
1136
+ return {
1137
+ /** @deprecated Use canClaim instead */
1138
+ isValid,
1139
+ canClaim: isValid,
1140
+ percentCompleted,
1141
+ conditionData,
1142
+ availableClaimsNow,
1143
+ };
936
1144
  }
937
- return { isValid: true, conditionData: [], availableClaimsNow: -1 };
1145
+ return { isValid: true, canClaim: true, percentCompleted: 100, conditionData: [], availableClaimsNow: -1 };
938
1146
  };
939
1147
  exports.meetsCompletionConditions = meetsCompletionConditions;
940
1148
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackedapp/utils",
3
- "version": "1.11.1",
3
+ "version": "1.12.0",
4
4
  "description": "Public utilities for Stacked platform SDK",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",