@stackedapp/utils 1.11.2 → 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
  }
@@ -425,11 +532,17 @@ additionalData, }) => {
425
532
  const val = dynamicObj[firstCond.key];
426
533
  trackerAmount = typeof val === 'number' ? val : 0;
427
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
+ });
428
540
  conditionData.push({
429
541
  isMet: dynamicResult,
430
542
  kind: 'dynamic',
431
543
  trackerAmount,
432
544
  trackerGoal,
545
+ percentCompleted,
433
546
  text: (0, template_1.renderTemplate)(conditions.dynamic.template, dynamicObj) || 'Dynamic conditions',
434
547
  });
435
548
  if (!dynamicResult)
@@ -437,7 +550,7 @@ additionalData, }) => {
437
550
  }
438
551
  else {
439
552
  if (!dynamicResult)
440
- return { isValid: false };
553
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
441
554
  }
442
555
  }
443
556
  if (conditions?.identifiers?.platforms?.length && 'identifiers' in playerSnap) {
@@ -460,6 +573,7 @@ additionalData, }) => {
460
573
  kind: 'identifiers',
461
574
  trackerAmount: isMet ? 1 : 0,
462
575
  trackerGoal: 1,
576
+ percentCompleted: isMet ? 100 : 0, // Binary
463
577
  text: displayText,
464
578
  });
465
579
  if (!isMet)
@@ -467,7 +581,7 @@ additionalData, }) => {
467
581
  }
468
582
  else {
469
583
  if (!isMet)
470
- return { isValid: false };
584
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
471
585
  }
472
586
  }
473
587
  // Evaluate token balance conditions
@@ -480,30 +594,36 @@ additionalData, }) => {
480
594
  totalBalance += fetchedBalances[balanceKey] || 0;
481
595
  }
482
596
  if (tokenCond.min !== undefined) {
483
- const isDisqualify = totalBalance < tokenCond.min;
597
+ const trackerAmount = totalBalance;
598
+ const trackerGoal = tokenCond.min;
599
+ const isDisqualify = trackerAmount < trackerGoal;
484
600
  if (addDetails) {
485
601
  conditionData.push({
486
602
  isMet: !isDisqualify,
487
603
  kind: 'tokenBalances',
488
- trackerAmount: totalBalance,
489
- trackerGoal: tokenCond.min,
490
- 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'}`,
491
608
  });
492
609
  if (isDisqualify)
493
610
  isValid = false;
494
611
  }
495
612
  else {
496
613
  if (isDisqualify)
497
- return { isValid: false };
614
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
498
615
  }
499
616
  }
500
617
  if (tokenCond.max !== undefined) {
501
- const isDisqualify = totalBalance > tokenCond.max;
618
+ const trackerAmount = totalBalance;
619
+ const isDisqualify = trackerAmount > tokenCond.max;
502
620
  if (addDetails) {
503
621
  conditionData.push({
504
622
  isMet: !isDisqualify,
505
623
  kind: 'tokenBalances',
506
- trackerAmount: totalBalance,
624
+ trackerAmount,
625
+ trackerGoal: tokenCond.max,
626
+ percentCompleted: !isDisqualify ? 100 : 0, // Binary for max conditions
507
627
  text: `Have at most ${tokenCond.max} ${tokenCond.name || 'tokens'}`,
508
628
  });
509
629
  if (isDisqualify)
@@ -511,11 +631,23 @@ additionalData, }) => {
511
631
  }
512
632
  else {
513
633
  if (isDisqualify)
514
- return { isValid: false };
634
+ return { isValid: false, canClaim: false, percentCompleted: 0 };
515
635
  }
516
636
  }
517
637
  }
518
- 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
+ };
519
651
  };
520
652
  exports.meetsBaseConditions = meetsBaseConditions;
521
653
  const hasCompletionConditions = (conditions) => {
@@ -617,6 +749,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
617
749
  kind: 'context',
618
750
  trackerAmount: hasTrackedContext ? 1 : 0,
619
751
  trackerGoal: 1,
752
+ percentCompleted: !isDisqualify ? 100 : 0, // Binary
620
753
  text: completionConditions.context.name,
621
754
  });
622
755
  if (isDisqualify)
@@ -624,7 +757,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
624
757
  }
625
758
  else {
626
759
  if (isDisqualify)
627
- return { isValid: false, availableClaimsNow: 0 };
760
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
628
761
  }
629
762
  }
630
763
  if (conditions?.buyItem) {
@@ -641,6 +774,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
641
774
  kind: 'buyItem',
642
775
  trackerAmount: trackerValue,
643
776
  trackerGoal: scaledAmount,
777
+ percentCompleted: calculatePercent(trackerValue, scaledAmount, !isDisqualify),
644
778
  text: `Buy ${scaledAmount} ${conditions.buyItem.name}`,
645
779
  });
646
780
  if (isDisqualify)
@@ -648,7 +782,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
648
782
  }
649
783
  else {
650
784
  if (isDisqualify)
651
- return { isValid: false, availableClaimsNow: 0 };
785
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
652
786
  }
653
787
  }
654
788
  if (conditions?.spendCurrency) {
@@ -665,6 +799,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
665
799
  kind: 'spendCurrency',
666
800
  trackerAmount: trackerValue,
667
801
  trackerGoal: scaledAmount,
802
+ percentCompleted: calculatePercent(trackerValue, scaledAmount, !isDisqualify),
668
803
  text: `Spend ${scaledAmount} ${conditions.spendCurrency.name}`,
669
804
  });
670
805
  if (isDisqualify)
@@ -672,7 +807,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
672
807
  }
673
808
  else {
674
809
  if (isDisqualify)
675
- return { isValid: false, availableClaimsNow: 0 };
810
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
676
811
  }
677
812
  }
678
813
  if (conditions?.depositCurrency) {
@@ -689,6 +824,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
689
824
  kind: 'depositCurrency',
690
825
  trackerAmount: trackerValue,
691
826
  trackerGoal: scaledAmount,
827
+ percentCompleted: calculatePercent(trackerValue, scaledAmount, !isDisqualify),
692
828
  text: `Deposit ${scaledAmount} ${conditions.depositCurrency.name}`,
693
829
  });
694
830
  if (isDisqualify)
@@ -696,7 +832,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
696
832
  }
697
833
  else {
698
834
  if (isDisqualify)
699
- return { isValid: false, availableClaimsNow: 0 };
835
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
700
836
  }
701
837
  }
702
838
  if (conditions?.login) {
@@ -707,6 +843,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
707
843
  kind: 'login',
708
844
  trackerAmount: isMet ? 1 : 0,
709
845
  trackerGoal: 1,
846
+ percentCompleted: isMet ? 100 : 0, // Binary
710
847
  text: `Login to the game`,
711
848
  });
712
849
  if (!isMet)
@@ -714,28 +851,30 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
714
851
  }
715
852
  else {
716
853
  if (!isMet)
717
- return { isValid: false, availableClaimsNow: 0 };
854
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
718
855
  }
719
856
  }
720
857
  if (conditions?.loginStreak) {
721
858
  // player's login streak snapshot right now - their login streak when offer was surfaced = their login streak since the offer was surfaced
722
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
723
- const streakSinceOffer = (playerSnap.loginStreak || 0) - (completionTrackers?.currentLoginStreak || 0);
724
- 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;
725
863
  if (addDetails) {
726
864
  conditionData.push({
727
865
  isMet: !isDisqualify,
728
866
  kind: 'loginStreak',
729
- trackerAmount: streakSinceOffer + 1,
730
- trackerGoal: conditions.loginStreak,
731
- 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`,
732
871
  });
733
872
  if (isDisqualify)
734
873
  isValid = false;
735
874
  }
736
875
  else {
737
876
  if (isDisqualify)
738
- return { isValid: false, availableClaimsNow: 0 };
877
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
739
878
  }
740
879
  }
741
880
  if (conditions?.social) {
@@ -768,6 +907,21 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
768
907
  if (baseComments > 0)
769
908
  updateMax(Math.floor(comments / baseComments));
770
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;
771
925
  if (addDetails) {
772
926
  const platformMap = {
773
927
  tiktok: 'TikTok',
@@ -779,10 +933,11 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
779
933
  if (mode === 'accumulate') {
780
934
  const matchCount = tSocialAccumulate?.matchCount || 0;
781
935
  conditionData.push({
782
- isMet: hasContent,
936
+ isMet: hasContent && !isDisqualify,
783
937
  kind: 'social',
784
938
  trackerAmount: matchCount,
785
939
  trackerGoal: 1,
940
+ percentCompleted: socialPercent,
786
941
  text: hasContent
787
942
  ? `Found ${matchCount} matching ${platformText} post${matchCount !== 1 ? 's' : ''}`
788
943
  : requiredWords.length > 0
@@ -793,10 +948,11 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
793
948
  else {
794
949
  const title = tSocialAttach?.title;
795
950
  conditionData.push({
796
- isMet: hasContent,
951
+ isMet: hasContent && !isDisqualify,
797
952
  kind: 'social',
798
953
  trackerAmount: hasContent ? 1 : 0,
799
954
  trackerGoal: 1,
955
+ percentCompleted: socialPercent,
800
956
  text: !hasContent
801
957
  ? requiredWords.length > 0
802
958
  ? `Attach a ${platformText} post with ${requiredWords.map((w) => `"${w}"`).join(', ')} in the title`
@@ -810,6 +966,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
810
966
  kind: 'social',
811
967
  trackerAmount: likes,
812
968
  trackerGoal: minLikes,
969
+ percentCompleted: calculatePercent(likes, minLikes, hasContent && likes >= minLikes),
813
970
  text: mode === 'accumulate' ? `Combined ${minLikes} Likes` : `Reach ${minLikes} Likes`,
814
971
  });
815
972
  }
@@ -819,6 +976,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
819
976
  kind: 'social',
820
977
  trackerAmount: views,
821
978
  trackerGoal: minViews,
979
+ percentCompleted: calculatePercent(views, minViews, hasContent && views >= minViews),
822
980
  text: mode === 'accumulate' ? `Combined ${minViews} Views` : `Reach ${minViews} Views`,
823
981
  });
824
982
  }
@@ -828,6 +986,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
828
986
  kind: 'social',
829
987
  trackerAmount: comments,
830
988
  trackerGoal: minComments,
989
+ percentCompleted: calculatePercent(comments, minComments, hasContent && comments >= minComments),
831
990
  text: mode === 'accumulate' ? `Combined ${minComments} Comments` : `Reach ${minComments} Comments`,
832
991
  });
833
992
  }
@@ -836,37 +995,38 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
836
995
  }
837
996
  else {
838
997
  if (isDisqualify)
839
- return { isValid: false, availableClaimsNow: 0 };
998
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
840
999
  }
841
1000
  }
842
1001
  // Linked completions - wait for N linked entities to complete
843
1002
  if (conditions?.linkedCompletions?.min) {
844
1003
  const baseMin = conditions.linkedCompletions.min;
845
- const currentCount = completionTrackers?.linkedCompletions || 0;
846
- const scaledMin = baseMin * claimMultiplier;
847
- const isDisqualify = currentCount < scaledMin;
1004
+ const trackerAmount = completionTrackers?.linkedCompletions || 0;
1005
+ const trackerGoal = baseMin * claimMultiplier;
1006
+ const isDisqualify = trackerAmount < trackerGoal;
848
1007
  if (shouldScale && baseMin > 0) {
849
- updateMax(Math.floor(currentCount / baseMin));
1008
+ updateMax(Math.floor(trackerAmount / baseMin));
850
1009
  }
851
1010
  if (addDetails) {
852
1011
  conditionData.push({
853
1012
  isMet: !isDisqualify,
854
1013
  kind: 'linkedCompletions',
855
- trackerAmount: currentCount,
856
- trackerGoal: scaledMin,
1014
+ trackerAmount,
1015
+ trackerGoal,
1016
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
857
1017
  text: conditions.linkedCompletions.template
858
1018
  ? (0, template_1.renderTemplate)(conditions.linkedCompletions.template, {
859
- current: currentCount,
860
- required: scaledMin,
1019
+ current: trackerAmount,
1020
+ required: trackerGoal,
861
1021
  })
862
- : `Wait for ${scaledMin} linked ${scaledMin === 1 ? 'entity' : 'entities'} to complete`,
1022
+ : `Wait for ${trackerGoal} linked ${trackerGoal === 1 ? 'entity' : 'entities'} to complete`,
863
1023
  });
864
1024
  if (isDisqualify)
865
1025
  isValid = false;
866
1026
  }
867
1027
  else {
868
1028
  if (isDisqualify)
869
- return { isValid: false, availableClaimsNow: 0 };
1029
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
870
1030
  }
871
1031
  }
872
1032
  if (conditions?.dynamicTracker?.conditions?.length) {
@@ -896,11 +1056,17 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
896
1056
  const val = primitiveTrackers[firstCond.key];
897
1057
  trackerAmount = typeof val === 'number' ? val : 0;
898
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
+ });
899
1064
  conditionData.push({
900
1065
  isMet: dynamicResult,
901
1066
  kind: 'dynamicTracker',
902
1067
  trackerAmount,
903
1068
  trackerGoal,
1069
+ percentCompleted,
904
1070
  text: (0, template_1.renderTemplate)(conditions.dynamicTracker.template, primitiveTrackers) || 'Dynamic conditions',
905
1071
  });
906
1072
  if (!dynamicResult)
@@ -908,29 +1074,30 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
908
1074
  }
909
1075
  else {
910
1076
  if (!dynamicResult)
911
- return { isValid: false, availableClaimsNow: 0 };
1077
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
912
1078
  }
913
1079
  }
914
1080
  // Evaluate contractInteractions completion trackers
915
1081
  if (conditions?.contractInteractions) {
916
1082
  for (const [conditionId, condition] of Object.entries(conditions.contractInteractions)) {
917
1083
  const baseAmount = condition.amount || 0;
918
- const scaledAmount = baseAmount * claimMultiplier;
919
- const trackerValue = completionTrackers?.contractInteractions?.[conditionId] || 0;
920
- const isDisqualify = trackerValue < scaledAmount;
1084
+ const trackerGoal = baseAmount * claimMultiplier;
1085
+ const trackerAmount = completionTrackers?.contractInteractions?.[conditionId] || 0;
1086
+ const isDisqualify = trackerAmount < trackerGoal;
921
1087
  if (shouldScale && baseAmount > 0) {
922
- updateMax(Math.floor(trackerValue / baseAmount));
1088
+ updateMax(Math.floor(trackerAmount / baseAmount));
923
1089
  }
924
1090
  if (addDetails) {
925
1091
  const displayText = (0, template_1.renderTemplate)(condition.template, {
926
- current: trackerValue,
927
- amount: scaledAmount,
1092
+ current: trackerAmount,
1093
+ amount: trackerGoal,
928
1094
  });
929
1095
  conditionData.push({
930
1096
  isMet: !isDisqualify,
931
1097
  kind: 'contractInteractions',
932
- trackerAmount: trackerValue,
933
- trackerGoal: scaledAmount,
1098
+ trackerAmount,
1099
+ trackerGoal,
1100
+ percentCompleted: calculatePercent(trackerAmount, trackerGoal, !isDisqualify),
934
1101
  text: displayText,
935
1102
  });
936
1103
  if (isDisqualify)
@@ -938,7 +1105,7 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
938
1105
  }
939
1106
  else {
940
1107
  if (isDisqualify)
941
- return { isValid: false, availableClaimsNow: 0 };
1108
+ return { isValid: false, canClaim: false, percentCompleted: 0, availableClaimsNow: 0 };
942
1109
  }
943
1110
  }
944
1111
  }
@@ -960,9 +1127,22 @@ const meetsCompletionConditions = ({ completionConditions, completionTrackers, p
960
1127
  : maxTotalClaimsFromScaling === Infinity
961
1128
  ? -1
962
1129
  : Math.max(0, maxTotalClaimsFromScaling - claimedCount);
963
- 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
+ };
964
1144
  }
965
- return { isValid: true, conditionData: [], availableClaimsNow: -1 };
1145
+ return { isValid: true, canClaim: true, percentCompleted: 100, conditionData: [], availableClaimsNow: -1 };
966
1146
  };
967
1147
  exports.meetsCompletionConditions = meetsCompletionConditions;
968
1148
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackedapp/utils",
3
- "version": "1.11.2",
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",