@proveanything/smartlinks 1.9.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/API_SUMMARY.md +1 -1
- package/dist/docs/utils.md +55 -4
- package/dist/index.d.ts +1 -1
- package/dist/utils/conditions.d.ts +9 -1
- package/dist/utils/conditions.js +442 -115
- package/docs/API_SUMMARY.md +1 -1
- package/docs/utils.md +55 -4
- package/package.json +1 -1
package/dist/docs/API_SUMMARY.md
CHANGED
package/dist/docs/utils.md
CHANGED
|
@@ -280,6 +280,8 @@ const path = utils.buildPortalPath(params)
|
|
|
280
280
|
|
|
281
281
|
The `validateCondition` function helps determine if content should be shown or hidden based on various criteria like geography, device type, user status, dates, and more.
|
|
282
282
|
|
|
283
|
+
Enable verbose tracing per invocation with `debugConditions`, or set `globalThis.SMARTLINKS_CONDITION_DEBUG = true` in a browser/devtools session to trace every evaluation.
|
|
284
|
+
|
|
283
285
|
### Basic Usage
|
|
284
286
|
|
|
285
287
|
```typescript
|
|
@@ -291,7 +293,6 @@ const canShow = await utils.validateCondition({
|
|
|
291
293
|
type: 'and',
|
|
292
294
|
conditions: [{
|
|
293
295
|
type: 'country',
|
|
294
|
-
useRegions: true,
|
|
295
296
|
regions: ['eu'],
|
|
296
297
|
contains: true
|
|
297
298
|
}]
|
|
@@ -328,7 +329,6 @@ await utils.validateCondition({
|
|
|
328
329
|
type: 'and',
|
|
329
330
|
conditions: [{
|
|
330
331
|
type: 'country',
|
|
331
|
-
useRegions: true,
|
|
332
332
|
regions: ['eu', 'uk'],
|
|
333
333
|
contains: true // true = show IN these regions, false = hide IN these regions
|
|
334
334
|
}]
|
|
@@ -336,6 +336,20 @@ await utils.validateCondition({
|
|
|
336
336
|
user: { valid: true, location: { country: 'FR' } }
|
|
337
337
|
})
|
|
338
338
|
|
|
339
|
+
// Regions and explicit countries can be combined
|
|
340
|
+
await utils.validateCondition({
|
|
341
|
+
condition: {
|
|
342
|
+
type: 'and',
|
|
343
|
+
conditions: [{
|
|
344
|
+
type: 'country',
|
|
345
|
+
regions: ['eu'],
|
|
346
|
+
countries: ['CH'],
|
|
347
|
+
contains: true
|
|
348
|
+
}]
|
|
349
|
+
},
|
|
350
|
+
user: { valid: true, location: { country: 'CH' } }
|
|
351
|
+
})
|
|
352
|
+
|
|
339
353
|
// Or specific countries
|
|
340
354
|
await utils.validateCondition({
|
|
341
355
|
condition: {
|
|
@@ -582,7 +596,7 @@ await utils.validateCondition({
|
|
|
582
596
|
conditions: [
|
|
583
597
|
{ type: 'user', userType: 'valid' },
|
|
584
598
|
{ type: 'device', displays: ['mobile'], contains: true },
|
|
585
|
-
{ type: 'country',
|
|
599
|
+
{ type: 'country', regions: ['eu'], contains: true }
|
|
586
600
|
]
|
|
587
601
|
},
|
|
588
602
|
user: { valid: true, location: { country: 'FR' } },
|
|
@@ -630,12 +644,49 @@ const showNewFeature = await utils.validateCondition({
|
|
|
630
644
|
condition: {
|
|
631
645
|
type: 'and',
|
|
632
646
|
conditions: [
|
|
633
|
-
{ type: 'country',
|
|
647
|
+
{ type: 'country', regions: ['northamerica'], contains: true },
|
|
634
648
|
{ type: 'date', dateTest: 'after', afterDate: '2026-03-01' }
|
|
635
649
|
]
|
|
636
650
|
},
|
|
637
651
|
user: { valid: true, location: { country: 'US' } }
|
|
638
652
|
})
|
|
653
|
+
|
|
654
|
+
### Debug Logging
|
|
655
|
+
|
|
656
|
+
Trace which condition is being evaluated, whether it passed, and why:
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
await utils.validateCondition({
|
|
660
|
+
condition: {
|
|
661
|
+
type: 'and',
|
|
662
|
+
conditions: [
|
|
663
|
+
{ type: 'user', userType: 'valid' },
|
|
664
|
+
{ type: 'country', regions: ['eu'], contains: true }
|
|
665
|
+
]
|
|
666
|
+
},
|
|
667
|
+
user: { valid: true, location: { country: 'DE' } },
|
|
668
|
+
debugConditions: true
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
// Or enable globally in the browser console/devtools
|
|
672
|
+
globalThis.SMARTLINKS_CONDITION_DEBUG = true
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
If you want to route logs somewhere specific, pass a logger:
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
await utils.validateCondition({
|
|
679
|
+
condition: {
|
|
680
|
+
type: 'and',
|
|
681
|
+
conditions: [{ type: 'user', userType: 'valid' }]
|
|
682
|
+
},
|
|
683
|
+
user: { valid: true },
|
|
684
|
+
debugConditions: {
|
|
685
|
+
label: 'checkout-gate',
|
|
686
|
+
logger: (...args) => console.log(...args)
|
|
687
|
+
}
|
|
688
|
+
})
|
|
689
|
+
```
|
|
639
690
|
```
|
|
640
691
|
|
|
641
692
|
#### Mobile-Only Features
|
package/dist/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { iframe } from "./iframe";
|
|
|
5
5
|
export * as cache from './cache';
|
|
6
6
|
export { IframeResponder, isAdminFromRoles, buildIframeSrc, } from './iframeResponder';
|
|
7
7
|
export * as utils from './utils';
|
|
8
|
-
export type { PortalPathParams, ConditionParams, ConditionSet, Condition, UserInfo, ProductInfo, ProofInfo, CollectionInfo, } from './utils';
|
|
8
|
+
export type { PortalPathParams, ConditionParams, ConditionDebugOptions, ConditionDebugLogger, ConditionSet, Condition, UserInfo, ProductInfo, ProofInfo, CollectionInfo, } from './utils';
|
|
9
9
|
export type { LoginResponse, VerifyTokenResponse, AccountInfoResponse, AuthLocation, } from "./api/auth";
|
|
10
10
|
export type { UserAccountRegistrationRequest, } from "./types/auth";
|
|
11
11
|
export type { CommunicationEvent, CommsQueryByUser, CommsRecipientIdsQuery, CommsRecipientsWithoutActionQuery, CommsRecipientsWithActionQuery, RecipientId, RecipientWithOutcome, LogCommunicationEventBody, LogBulkCommunicationEventsBody, AppendResult, AppendBulkResult, CommsSettings, TopicConfig, CommsSettingsGetResponse, CommsSettingsPatchBody, CommsPublicTopicsResponse, UnsubscribeQuery, UnsubscribeResponse, CommsConsentUpsertRequest, CommsPreferencesUpsertRequest, CommsSubscribeRequest, CommsSubscribeResponse, CommsSubscriptionCheckQuery, CommsSubscriptionCheckResponse, CommsListMethodsQuery, CommsListMethodsResponse, RegisterEmailMethodRequest, RegisterSmsMethodRequest, RegisterMethodResponse, SubscriptionsResolveRequest, SubscriptionsResolveResponse, } from "./types/comms";
|
|
@@ -23,6 +23,7 @@ export interface BaseCondition {
|
|
|
23
23
|
export interface CountryCondition extends BaseCondition {
|
|
24
24
|
type: 'country';
|
|
25
25
|
countries?: string[];
|
|
26
|
+
/** @deprecated Regions are applied automatically when regions is provided. */
|
|
26
27
|
useRegions?: boolean;
|
|
27
28
|
regions?: RegionKey[];
|
|
28
29
|
contains: boolean;
|
|
@@ -209,9 +210,17 @@ export interface ConditionParams {
|
|
|
209
210
|
latitude: number;
|
|
210
211
|
longitude: number;
|
|
211
212
|
}>;
|
|
213
|
+
/** Enable verbose condition evaluation logging for this invocation */
|
|
214
|
+
debugConditions?: boolean | ConditionDebugOptions;
|
|
212
215
|
/** Any additional custom fields for value-based conditions */
|
|
213
216
|
[key: string]: any;
|
|
214
217
|
}
|
|
218
|
+
export type ConditionDebugLogger = (...args: any[]) => void;
|
|
219
|
+
export interface ConditionDebugOptions {
|
|
220
|
+
enabled?: boolean;
|
|
221
|
+
logger?: ConditionDebugLogger;
|
|
222
|
+
label?: string;
|
|
223
|
+
}
|
|
215
224
|
/**
|
|
216
225
|
* Validates if a condition set passes based on the provided parameters.
|
|
217
226
|
*
|
|
@@ -246,7 +255,6 @@ export interface ConditionParams {
|
|
|
246
255
|
* type: 'and',
|
|
247
256
|
* conditions: [{
|
|
248
257
|
* type: 'country',
|
|
249
|
-
* useRegions: true,
|
|
250
258
|
* regions: ['eu'],
|
|
251
259
|
* contains: true
|
|
252
260
|
* }]
|
package/dist/utils/conditions.js
CHANGED
|
@@ -16,6 +16,139 @@ export const REGION_COUNTRIES = {
|
|
|
16
16
|
};
|
|
17
17
|
// Condition cache
|
|
18
18
|
const conditionCache = {};
|
|
19
|
+
const CONDITION_DEBUG_GLOBAL_KEY = 'SMARTLINKS_CONDITION_DEBUG';
|
|
20
|
+
function defaultConditionDebugLogger(...args) {
|
|
21
|
+
if (typeof console === 'undefined')
|
|
22
|
+
return;
|
|
23
|
+
if (typeof console.debug === 'function') {
|
|
24
|
+
console.debug(...args);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (typeof console.log === 'function') {
|
|
28
|
+
console.log(...args);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function getGlobalConditionDebugOptions() {
|
|
32
|
+
if (typeof globalThis === 'undefined') {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
const value = globalThis[CONDITION_DEBUG_GLOBAL_KEY];
|
|
36
|
+
if (typeof value === 'boolean') {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
if (value && typeof value === 'object') {
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
function resolveConditionDebugState(params) {
|
|
45
|
+
var _a, _b, _c;
|
|
46
|
+
const globalOptions = getGlobalConditionDebugOptions();
|
|
47
|
+
const localOptions = params.debugConditions;
|
|
48
|
+
const mergedOptions = Object.assign(Object.assign({}, (typeof globalOptions === 'object' ? globalOptions : {})), (typeof localOptions === 'object' ? localOptions : {}));
|
|
49
|
+
let enabled = false;
|
|
50
|
+
if (typeof globalOptions === 'boolean') {
|
|
51
|
+
enabled = globalOptions;
|
|
52
|
+
}
|
|
53
|
+
else if (typeof globalOptions === 'object' && globalOptions) {
|
|
54
|
+
enabled = (_a = globalOptions.enabled) !== null && _a !== void 0 ? _a : true;
|
|
55
|
+
}
|
|
56
|
+
if (typeof localOptions === 'boolean') {
|
|
57
|
+
enabled = localOptions;
|
|
58
|
+
}
|
|
59
|
+
else if (typeof localOptions === 'object' && localOptions) {
|
|
60
|
+
enabled = (_b = localOptions.enabled) !== null && _b !== void 0 ? _b : true;
|
|
61
|
+
}
|
|
62
|
+
if (!enabled) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
depth: 0,
|
|
67
|
+
label: mergedOptions.label,
|
|
68
|
+
logger: (_c = mergedOptions.logger) !== null && _c !== void 0 ? _c : defaultConditionDebugLogger,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
function createChildDebugState(state) {
|
|
72
|
+
if (!state) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
return Object.assign(Object.assign({}, state), { depth: state.depth + 1 });
|
|
76
|
+
}
|
|
77
|
+
function logConditionDebug(state, message, context) {
|
|
78
|
+
if (!state) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const indent = ' '.repeat(state.depth);
|
|
82
|
+
const prefix = state.label
|
|
83
|
+
? `[smartlinks:conditions:${state.label}]`
|
|
84
|
+
: '[smartlinks:conditions]';
|
|
85
|
+
if (context) {
|
|
86
|
+
state.logger(`${prefix} ${indent}${message}`, context);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
state.logger(`${prefix} ${indent}${message}`);
|
|
90
|
+
}
|
|
91
|
+
function summarizeConditionSet(condition) {
|
|
92
|
+
var _a, _b, _c;
|
|
93
|
+
return `${(_a = condition.type) !== null && _a !== void 0 ? _a : 'and'} (${(_c = (_b = condition.conditions) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0} conditions)`;
|
|
94
|
+
}
|
|
95
|
+
function summarizeCondition(condition) {
|
|
96
|
+
var _a, _b;
|
|
97
|
+
switch (condition.type) {
|
|
98
|
+
case 'country':
|
|
99
|
+
return `country regions=${((_a = condition.regions) === null || _a === void 0 ? void 0 : _a.join(',')) || 'none'} countries=${((_b = condition.countries) === null || _b === void 0 ? void 0 : _b.join(',')) || 'none'} contains=${condition.contains}`;
|
|
100
|
+
case 'version':
|
|
101
|
+
return `version [${condition.versions.join(', ')}] contains=${condition.contains}`;
|
|
102
|
+
case 'device':
|
|
103
|
+
return `device [${condition.displays.join(', ')}] contains=${condition.contains}`;
|
|
104
|
+
case 'condition':
|
|
105
|
+
return `condition ref=${condition.conditionId} passes=${condition.passes}`;
|
|
106
|
+
case 'user':
|
|
107
|
+
return `user type=${condition.userType}`;
|
|
108
|
+
case 'product':
|
|
109
|
+
return `product ids=${condition.productIds.join(', ')} contains=${condition.contains}`;
|
|
110
|
+
case 'tag':
|
|
111
|
+
return `tag tags=${condition.tags.join(', ')} contains=${condition.contains}`;
|
|
112
|
+
case 'date':
|
|
113
|
+
return `date test=${condition.dateTest}`;
|
|
114
|
+
case 'geofence':
|
|
115
|
+
return `geofence contains=${condition.contains}`;
|
|
116
|
+
case 'value':
|
|
117
|
+
return `value field=${condition.field} ${condition.validationType} ${String(condition.value)}`;
|
|
118
|
+
case 'itemStatus':
|
|
119
|
+
return `itemStatus ${condition.statusType}`;
|
|
120
|
+
default:
|
|
121
|
+
return 'unknown condition';
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
async function evaluateConditionEntry(condition, params) {
|
|
125
|
+
switch (condition.type) {
|
|
126
|
+
case 'country':
|
|
127
|
+
return validateCountry(condition, params);
|
|
128
|
+
case 'version':
|
|
129
|
+
return validateVersion(condition, params);
|
|
130
|
+
case 'device':
|
|
131
|
+
return validateDevice(condition, params);
|
|
132
|
+
case 'condition':
|
|
133
|
+
return validateNestedCondition(condition, params);
|
|
134
|
+
case 'user':
|
|
135
|
+
return validateUser(condition, params);
|
|
136
|
+
case 'product':
|
|
137
|
+
return validateProduct(condition, params);
|
|
138
|
+
case 'tag':
|
|
139
|
+
return validateTag(condition, params);
|
|
140
|
+
case 'date':
|
|
141
|
+
return validateDate(condition, params);
|
|
142
|
+
case 'geofence':
|
|
143
|
+
return validateGeofence(condition, params);
|
|
144
|
+
case 'value':
|
|
145
|
+
return validateValue(condition, params);
|
|
146
|
+
case 'itemStatus':
|
|
147
|
+
return validateItemStatus(condition, params);
|
|
148
|
+
default:
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
19
152
|
/**
|
|
20
153
|
* Validates if a condition set passes based on the provided parameters.
|
|
21
154
|
*
|
|
@@ -50,7 +183,6 @@ const conditionCache = {};
|
|
|
50
183
|
* type: 'and',
|
|
51
184
|
* conditions: [{
|
|
52
185
|
* type: 'country',
|
|
53
|
-
* useRegions: true,
|
|
54
186
|
* regions: ['eu'],
|
|
55
187
|
* contains: true
|
|
56
188
|
* }]
|
|
@@ -85,9 +217,13 @@ const conditionCache = {};
|
|
|
85
217
|
* ```
|
|
86
218
|
*/
|
|
87
219
|
export async function validateCondition(params) {
|
|
88
|
-
var _a, _b;
|
|
220
|
+
var _a, _b, _c;
|
|
221
|
+
const internalParams = params;
|
|
222
|
+
(_a = internalParams.__conditionDebugState) !== null && _a !== void 0 ? _a : (internalParams.__conditionDebugState = resolveConditionDebugState(params));
|
|
223
|
+
const debugState = internalParams.__conditionDebugState;
|
|
89
224
|
// If no condition specified, pass by default
|
|
90
225
|
if (!params.conditionId && !params.condition) {
|
|
226
|
+
logConditionDebug(debugState, 'No condition supplied; passing by default.');
|
|
91
227
|
return true;
|
|
92
228
|
}
|
|
93
229
|
let cond = params.condition;
|
|
@@ -95,86 +231,78 @@ export async function validateCondition(params) {
|
|
|
95
231
|
if (params.conditionId) {
|
|
96
232
|
// Check cache first
|
|
97
233
|
if (!conditionCache[params.conditionId]) {
|
|
234
|
+
logConditionDebug(debugState, 'Condition cache miss.', { conditionId: params.conditionId });
|
|
98
235
|
// Try to fetch if function provided
|
|
99
236
|
if (params.fetchCondition && params.collection) {
|
|
100
237
|
const fetchedCond = await params.fetchCondition(params.collection.id, params.conditionId);
|
|
101
238
|
if (fetchedCond) {
|
|
102
239
|
conditionCache[params.conditionId] = fetchedCond;
|
|
240
|
+
logConditionDebug(debugState, 'Fetched condition into cache.', {
|
|
241
|
+
conditionId: params.conditionId,
|
|
242
|
+
conditionSet: summarizeConditionSet(fetchedCond),
|
|
243
|
+
});
|
|
103
244
|
}
|
|
104
245
|
}
|
|
105
246
|
}
|
|
106
247
|
if (!conditionCache[params.conditionId]) {
|
|
248
|
+
logConditionDebug(debugState, 'Condition not found; passing by default.', { conditionId: params.conditionId });
|
|
107
249
|
return true;
|
|
108
250
|
}
|
|
109
251
|
// Prevent infinite recursion
|
|
110
|
-
(
|
|
252
|
+
(_b = params.conditionStack) !== null && _b !== void 0 ? _b : (params.conditionStack = []);
|
|
111
253
|
params.conditionStack.push(params.conditionId);
|
|
112
254
|
cond = conditionCache[params.conditionId];
|
|
113
255
|
}
|
|
256
|
+
else {
|
|
257
|
+
logConditionDebug(debugState, 'Evaluating inline condition set.', {
|
|
258
|
+
conditionSet: cond ? summarizeConditionSet(cond) : 'none',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
114
261
|
if (!cond) {
|
|
262
|
+
logConditionDebug(debugState, 'Resolved condition set is empty; passing by default.');
|
|
115
263
|
return true;
|
|
116
264
|
}
|
|
117
265
|
// Default to AND logic
|
|
118
|
-
(
|
|
266
|
+
const conditionType = (_c = cond.type) !== null && _c !== void 0 ? _c : 'and';
|
|
119
267
|
// Empty condition set passes
|
|
120
268
|
if (!cond.conditions || cond.conditions.length === 0) {
|
|
269
|
+
logConditionDebug(debugState, 'Condition set has no entries; passing by default.', {
|
|
270
|
+
conditionSet: summarizeConditionSet(cond),
|
|
271
|
+
});
|
|
121
272
|
return true;
|
|
122
273
|
}
|
|
274
|
+
logConditionDebug(debugState, 'Condition set start.', {
|
|
275
|
+
logic: conditionType,
|
|
276
|
+
conditionCount: cond.conditions.length,
|
|
277
|
+
conditionId: params.conditionId,
|
|
278
|
+
});
|
|
123
279
|
// Evaluate each condition
|
|
124
|
-
for (const c of cond.conditions) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
case 'version':
|
|
131
|
-
result = await validateVersion(c, params);
|
|
132
|
-
break;
|
|
133
|
-
case 'device':
|
|
134
|
-
result = await validateDevice(c, params);
|
|
135
|
-
break;
|
|
136
|
-
case 'condition':
|
|
137
|
-
result = await validateNestedCondition(c, params);
|
|
138
|
-
break;
|
|
139
|
-
case 'user':
|
|
140
|
-
result = await validateUser(c, params);
|
|
141
|
-
break;
|
|
142
|
-
case 'product':
|
|
143
|
-
result = await validateProduct(c, params);
|
|
144
|
-
break;
|
|
145
|
-
case 'tag':
|
|
146
|
-
result = await validateTag(c, params);
|
|
147
|
-
break;
|
|
148
|
-
case 'date':
|
|
149
|
-
result = await validateDate(c, params);
|
|
150
|
-
break;
|
|
151
|
-
case 'geofence':
|
|
152
|
-
result = await validateGeofence(c, params);
|
|
153
|
-
break;
|
|
154
|
-
case 'value':
|
|
155
|
-
result = await validateValue(c, params);
|
|
156
|
-
break;
|
|
157
|
-
case 'itemStatus':
|
|
158
|
-
result = await validateItemStatus(c, params);
|
|
159
|
-
break;
|
|
160
|
-
default:
|
|
161
|
-
// Unknown condition type, skip
|
|
162
|
-
continue;
|
|
280
|
+
for (const [index, c] of cond.conditions.entries()) {
|
|
281
|
+
logConditionDebug(debugState, `Condition ${index + 1}/${cond.conditions.length} start: ${summarizeCondition(c)}`);
|
|
282
|
+
const evaluation = await evaluateConditionEntry(c, internalParams);
|
|
283
|
+
if (!evaluation) {
|
|
284
|
+
logConditionDebug(debugState, `Condition ${index + 1} skipped: unknown type.`, { type: c.type });
|
|
285
|
+
continue;
|
|
163
286
|
}
|
|
287
|
+
logConditionDebug(debugState, `Condition ${index + 1} result: ${evaluation.passed ? 'PASS' : 'FAIL'}`, Object.assign({ type: c.type, detail: evaluation.detail }, evaluation.context));
|
|
164
288
|
// AND logic: if any condition fails, entire set fails
|
|
165
|
-
if (!
|
|
289
|
+
if (!evaluation.passed && conditionType === 'and') {
|
|
290
|
+
logConditionDebug(debugState, 'Condition set short-circuited to FAIL (AND logic).');
|
|
166
291
|
return false;
|
|
167
292
|
}
|
|
168
293
|
// OR logic: if any condition passes, entire set passes
|
|
169
|
-
if (
|
|
294
|
+
if (evaluation.passed && conditionType === 'or') {
|
|
295
|
+
logConditionDebug(debugState, 'Condition set short-circuited to PASS (OR logic).');
|
|
170
296
|
return true;
|
|
171
297
|
}
|
|
172
298
|
}
|
|
173
299
|
// AND: all passed
|
|
174
|
-
if (
|
|
300
|
+
if (conditionType === 'and') {
|
|
301
|
+
logConditionDebug(debugState, 'Condition set result: PASS.');
|
|
175
302
|
return true;
|
|
176
303
|
}
|
|
177
304
|
// OR: all failed
|
|
305
|
+
logConditionDebug(debugState, 'Condition set result: FAIL.');
|
|
178
306
|
return false;
|
|
179
307
|
}
|
|
180
308
|
/**
|
|
@@ -184,27 +312,42 @@ async function validateCountry(condition, params) {
|
|
|
184
312
|
var _a, _b, _c;
|
|
185
313
|
const country = (_b = (_a = params.user) === null || _a === void 0 ? void 0 : _a.location) === null || _b === void 0 ? void 0 : _b.country;
|
|
186
314
|
if (!country) {
|
|
187
|
-
return
|
|
315
|
+
return {
|
|
316
|
+
passed: false,
|
|
317
|
+
detail: 'User country was not available.',
|
|
318
|
+
};
|
|
188
319
|
}
|
|
189
|
-
// Build country list from
|
|
190
|
-
|
|
191
|
-
if (
|
|
192
|
-
countryList = [];
|
|
320
|
+
// Build country list from direct countries and any configured regions.
|
|
321
|
+
const countryList = [...(condition.countries || [])];
|
|
322
|
+
if ((_c = condition.regions) === null || _c === void 0 ? void 0 : _c.length) {
|
|
193
323
|
for (const region of condition.regions) {
|
|
194
324
|
if (REGION_COUNTRIES[region]) {
|
|
195
325
|
countryList.push(...REGION_COUNTRIES[region]);
|
|
196
326
|
}
|
|
197
327
|
}
|
|
198
|
-
// Remove duplicates
|
|
199
|
-
countryList = [...new Set(countryList)];
|
|
200
328
|
}
|
|
201
|
-
|
|
202
|
-
|
|
329
|
+
const normalizedCountryList = [...new Set(countryList)];
|
|
330
|
+
if (!normalizedCountryList.length) {
|
|
331
|
+
return {
|
|
332
|
+
passed: false,
|
|
333
|
+
detail: 'No countries or regions were configured on the condition.',
|
|
334
|
+
};
|
|
203
335
|
}
|
|
204
|
-
const inList =
|
|
336
|
+
const inList = normalizedCountryList.includes(country);
|
|
205
337
|
// contains=true: pass if country IS in list
|
|
206
338
|
// contains=false: pass if country is NOT in list
|
|
207
|
-
return
|
|
339
|
+
return {
|
|
340
|
+
passed: condition.contains ? inList : !inList,
|
|
341
|
+
detail: condition.contains
|
|
342
|
+
? `Country ${country} ${inList ? 'matched' : 'did not match'} the allowed list.`
|
|
343
|
+
: `Country ${country} ${inList ? 'matched' : 'did not match'} the blocked list.`,
|
|
344
|
+
context: {
|
|
345
|
+
country,
|
|
346
|
+
regions: condition.regions,
|
|
347
|
+
countryList: normalizedCountryList,
|
|
348
|
+
contains: condition.contains,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
208
351
|
}
|
|
209
352
|
/**
|
|
210
353
|
* Validate version-based condition
|
|
@@ -213,7 +356,15 @@ async function validateVersion(condition, params) {
|
|
|
213
356
|
var _a, _b;
|
|
214
357
|
const version = (_b = (_a = params.stats) === null || _a === void 0 ? void 0 : _a.version) !== null && _b !== void 0 ? _b : null;
|
|
215
358
|
const inList = condition.versions.includes(version);
|
|
216
|
-
return
|
|
359
|
+
return {
|
|
360
|
+
passed: condition.contains ? inList : !inList,
|
|
361
|
+
detail: `Version ${version !== null && version !== void 0 ? version : 'null'} ${inList ? 'matched' : 'did not match'} the configured list.`,
|
|
362
|
+
context: {
|
|
363
|
+
version,
|
|
364
|
+
versions: condition.versions,
|
|
365
|
+
contains: condition.contains,
|
|
366
|
+
},
|
|
367
|
+
};
|
|
217
368
|
}
|
|
218
369
|
/**
|
|
219
370
|
* Validate device/platform condition
|
|
@@ -225,25 +376,53 @@ async function validateDevice(condition, params) {
|
|
|
225
376
|
const mobile = (_b = params.stats) === null || _b === void 0 ? void 0 : _b.mobile;
|
|
226
377
|
for (const display of displays) {
|
|
227
378
|
if (display === 'android' && (platform === null || platform === void 0 ? void 0 : platform.android)) {
|
|
228
|
-
return
|
|
379
|
+
return {
|
|
380
|
+
passed: condition.contains,
|
|
381
|
+
detail: 'Matched android platform.',
|
|
382
|
+
context: { matchedDisplay: display, contains: condition.contains, platform, mobile },
|
|
383
|
+
};
|
|
229
384
|
}
|
|
230
385
|
if (display === 'ios' && (platform === null || platform === void 0 ? void 0 : platform.ios)) {
|
|
231
|
-
return
|
|
386
|
+
return {
|
|
387
|
+
passed: condition.contains,
|
|
388
|
+
detail: 'Matched ios platform.',
|
|
389
|
+
context: { matchedDisplay: display, contains: condition.contains, platform, mobile },
|
|
390
|
+
};
|
|
232
391
|
}
|
|
233
392
|
if (display === 'win' && (platform === null || platform === void 0 ? void 0 : platform.win)) {
|
|
234
|
-
return
|
|
393
|
+
return {
|
|
394
|
+
passed: condition.contains,
|
|
395
|
+
detail: 'Matched win platform.',
|
|
396
|
+
context: { matchedDisplay: display, contains: condition.contains, platform, mobile },
|
|
397
|
+
};
|
|
235
398
|
}
|
|
236
399
|
if (display === 'mac' && (platform === null || platform === void 0 ? void 0 : platform.mac)) {
|
|
237
|
-
return
|
|
400
|
+
return {
|
|
401
|
+
passed: condition.contains,
|
|
402
|
+
detail: 'Matched mac platform.',
|
|
403
|
+
context: { matchedDisplay: display, contains: condition.contains, platform, mobile },
|
|
404
|
+
};
|
|
238
405
|
}
|
|
239
406
|
if (display === 'desktop' && !mobile) {
|
|
240
|
-
return
|
|
407
|
+
return {
|
|
408
|
+
passed: condition.contains,
|
|
409
|
+
detail: 'Matched desktop device mode.',
|
|
410
|
+
context: { matchedDisplay: display, contains: condition.contains, platform, mobile },
|
|
411
|
+
};
|
|
241
412
|
}
|
|
242
413
|
if (display === 'mobile' && mobile) {
|
|
243
|
-
return
|
|
414
|
+
return {
|
|
415
|
+
passed: condition.contains,
|
|
416
|
+
detail: 'Matched mobile device mode.',
|
|
417
|
+
context: { matchedDisplay: display, contains: condition.contains, platform, mobile },
|
|
418
|
+
};
|
|
244
419
|
}
|
|
245
420
|
}
|
|
246
|
-
return
|
|
421
|
+
return {
|
|
422
|
+
passed: !condition.contains,
|
|
423
|
+
detail: 'No configured display matched the current platform/device.',
|
|
424
|
+
context: { displays, contains: condition.contains, platform, mobile },
|
|
425
|
+
};
|
|
247
426
|
}
|
|
248
427
|
/**
|
|
249
428
|
* Validate nested condition reference
|
|
@@ -251,48 +430,90 @@ async function validateDevice(condition, params) {
|
|
|
251
430
|
async function validateNestedCondition(condition, params) {
|
|
252
431
|
const newParams = Object.assign({}, params);
|
|
253
432
|
newParams.conditionId = condition.conditionId;
|
|
433
|
+
newParams.__conditionDebugState = createChildDebugState(params.__conditionDebugState);
|
|
254
434
|
// Prevent infinite recursion
|
|
255
435
|
newParams.conditionStack = [...(newParams.conditionStack || [])];
|
|
256
436
|
if (newParams.conditionStack.includes(condition.conditionId)) {
|
|
257
|
-
return
|
|
437
|
+
return {
|
|
438
|
+
passed: true,
|
|
439
|
+
detail: `Nested condition ${condition.conditionId} skipped to avoid recursion.`,
|
|
440
|
+
context: { conditionId: condition.conditionId, conditionStack: newParams.conditionStack },
|
|
441
|
+
};
|
|
258
442
|
}
|
|
259
443
|
const result = await validateCondition(newParams);
|
|
260
|
-
return
|
|
444
|
+
return {
|
|
445
|
+
passed: condition.passes ? result : !result,
|
|
446
|
+
detail: `Nested condition ${condition.conditionId} ${result ? 'passed' : 'failed'} and passes=${condition.passes}.`,
|
|
447
|
+
context: { conditionId: condition.conditionId, nestedResult: result, passes: condition.passes },
|
|
448
|
+
};
|
|
261
449
|
}
|
|
262
450
|
/**
|
|
263
451
|
* Validate user-based condition
|
|
264
452
|
*/
|
|
265
453
|
async function validateUser(condition, params) {
|
|
266
|
-
var _a, _b, _c, _d, _e, _f, _g;
|
|
454
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x;
|
|
267
455
|
const userType = condition.userType;
|
|
268
456
|
switch (userType) {
|
|
269
457
|
case 'valid':
|
|
270
458
|
// User is logged in
|
|
271
|
-
return
|
|
459
|
+
return {
|
|
460
|
+
passed: (_b = (_a = params.user) === null || _a === void 0 ? void 0 : _a.valid) !== null && _b !== void 0 ? _b : false,
|
|
461
|
+
detail: `User valid flag is ${(_d = (_c = params.user) === null || _c === void 0 ? void 0 : _c.valid) !== null && _d !== void 0 ? _d : false}.`,
|
|
462
|
+
};
|
|
272
463
|
case 'invalid':
|
|
273
464
|
// User is not logged in
|
|
274
|
-
return
|
|
465
|
+
return {
|
|
466
|
+
passed: !((_f = (_e = params.user) === null || _e === void 0 ? void 0 : _e.valid) !== null && _f !== void 0 ? _f : false),
|
|
467
|
+
detail: `User valid flag is ${(_h = (_g = params.user) === null || _g === void 0 ? void 0 : _g.valid) !== null && _h !== void 0 ? _h : false}.`,
|
|
468
|
+
};
|
|
275
469
|
case 'owner':
|
|
276
470
|
// User owns the proof
|
|
277
|
-
return
|
|
278
|
-
(
|
|
279
|
-
|
|
280
|
-
|
|
471
|
+
return {
|
|
472
|
+
passed: !!(params.proof &&
|
|
473
|
+
((_j = params.user) === null || _j === void 0 ? void 0 : _j.valid) &&
|
|
474
|
+
params.user.uid &&
|
|
475
|
+
params.user.uid === params.proof.userId),
|
|
476
|
+
detail: 'Owner check compares user.uid with proof.userId.',
|
|
477
|
+
context: {
|
|
478
|
+
userId: (_k = params.user) === null || _k === void 0 ? void 0 : _k.uid,
|
|
479
|
+
proofUserId: (_l = params.proof) === null || _l === void 0 ? void 0 : _l.userId,
|
|
480
|
+
userValid: (_o = (_m = params.user) === null || _m === void 0 ? void 0 : _m.valid) !== null && _o !== void 0 ? _o : false,
|
|
481
|
+
},
|
|
482
|
+
};
|
|
281
483
|
case 'admin':
|
|
282
484
|
// User is admin of the collection
|
|
283
|
-
return
|
|
284
|
-
(
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
485
|
+
return {
|
|
486
|
+
passed: !!(params.collection &&
|
|
487
|
+
((_p = params.user) === null || _p === void 0 ? void 0 : _p.valid) &&
|
|
488
|
+
params.user.uid &&
|
|
489
|
+
params.collection.roles &&
|
|
490
|
+
params.collection.roles[params.user.uid]),
|
|
491
|
+
detail: 'Admin check looks for a role entry on collection.roles.',
|
|
492
|
+
context: {
|
|
493
|
+
userId: (_q = params.user) === null || _q === void 0 ? void 0 : _q.uid,
|
|
494
|
+
userValid: (_s = (_r = params.user) === null || _r === void 0 ? void 0 : _r.valid) !== null && _s !== void 0 ? _s : false,
|
|
495
|
+
hasRoles: !!((_t = params.collection) === null || _t === void 0 ? void 0 : _t.roles),
|
|
496
|
+
},
|
|
497
|
+
};
|
|
288
498
|
case 'group':
|
|
289
499
|
// User is member of specific group
|
|
290
500
|
// TODO: Implement group membership check
|
|
291
|
-
return
|
|
292
|
-
(
|
|
293
|
-
|
|
501
|
+
return {
|
|
502
|
+
passed: !!(params.collection &&
|
|
503
|
+
((_u = params.user) === null || _u === void 0 ? void 0 : _u.valid) &&
|
|
504
|
+
params.user.groups),
|
|
505
|
+
detail: 'Group condition currently checks only that groups are present on the user.',
|
|
506
|
+
context: {
|
|
507
|
+
groupId: condition.groupId,
|
|
508
|
+
groups: (_v = params.user) === null || _v === void 0 ? void 0 : _v.groups,
|
|
509
|
+
userValid: (_x = (_w = params.user) === null || _w === void 0 ? void 0 : _w.valid) !== null && _x !== void 0 ? _x : false,
|
|
510
|
+
},
|
|
511
|
+
};
|
|
294
512
|
default:
|
|
295
|
-
return
|
|
513
|
+
return {
|
|
514
|
+
passed: true,
|
|
515
|
+
detail: `Unknown userType ${userType}; passing by default.`,
|
|
516
|
+
};
|
|
296
517
|
}
|
|
297
518
|
}
|
|
298
519
|
/**
|
|
@@ -303,10 +524,18 @@ async function validateProduct(condition, params) {
|
|
|
303
524
|
const productId = (_a = params.product) === null || _a === void 0 ? void 0 : _a.id;
|
|
304
525
|
// No product ID available
|
|
305
526
|
if (!productId) {
|
|
306
|
-
return
|
|
527
|
+
return {
|
|
528
|
+
passed: !condition.contains,
|
|
529
|
+
detail: 'Product ID was not available.',
|
|
530
|
+
context: { contains: condition.contains, productIds: condition.productIds },
|
|
531
|
+
};
|
|
307
532
|
}
|
|
308
533
|
const inList = condition.productIds.includes(productId);
|
|
309
|
-
return
|
|
534
|
+
return {
|
|
535
|
+
passed: condition.contains ? inList : !inList,
|
|
536
|
+
detail: `Product ${productId} ${inList ? 'matched' : 'did not match'} the configured list.`,
|
|
537
|
+
context: { productId, productIds: condition.productIds, contains: condition.contains },
|
|
538
|
+
};
|
|
310
539
|
}
|
|
311
540
|
/**
|
|
312
541
|
* Validate tag-based condition
|
|
@@ -316,19 +545,35 @@ async function validateTag(condition, params) {
|
|
|
316
545
|
const productId = (_a = params.product) === null || _a === void 0 ? void 0 : _a.id;
|
|
317
546
|
// No product
|
|
318
547
|
if (!productId) {
|
|
319
|
-
return
|
|
548
|
+
return {
|
|
549
|
+
passed: !condition.contains,
|
|
550
|
+
detail: 'Product ID was not available.',
|
|
551
|
+
context: { contains: condition.contains, tags: condition.tags },
|
|
552
|
+
};
|
|
320
553
|
}
|
|
321
554
|
// No tags on product
|
|
322
555
|
if (!((_b = params.product) === null || _b === void 0 ? void 0 : _b.tags)) {
|
|
323
|
-
return
|
|
556
|
+
return {
|
|
557
|
+
passed: !condition.contains,
|
|
558
|
+
detail: 'Product tags were not available.',
|
|
559
|
+
context: { productId, contains: condition.contains, tags: condition.tags },
|
|
560
|
+
};
|
|
324
561
|
}
|
|
325
562
|
// Check if any condition tag exists on product
|
|
326
563
|
for (const tag of condition.tags) {
|
|
327
564
|
if (params.product.tags[tag]) {
|
|
328
|
-
return
|
|
565
|
+
return {
|
|
566
|
+
passed: condition.contains,
|
|
567
|
+
detail: `Product ${productId} matched tag ${tag}.`,
|
|
568
|
+
context: { productId, matchedTag: tag, contains: condition.contains },
|
|
569
|
+
};
|
|
329
570
|
}
|
|
330
571
|
}
|
|
331
|
-
return
|
|
572
|
+
return {
|
|
573
|
+
passed: !condition.contains,
|
|
574
|
+
detail: `Product ${productId} did not match any configured tag.`,
|
|
575
|
+
context: { productId, tags: condition.tags, contains: condition.contains },
|
|
576
|
+
};
|
|
332
577
|
}
|
|
333
578
|
/**
|
|
334
579
|
* Validate date-based condition
|
|
@@ -337,21 +582,39 @@ async function validateDate(condition, params) {
|
|
|
337
582
|
const now = Date.now();
|
|
338
583
|
switch (condition.dateTest) {
|
|
339
584
|
case 'before':
|
|
340
|
-
if (!condition.beforeDate)
|
|
341
|
-
return false;
|
|
342
|
-
|
|
585
|
+
if (!condition.beforeDate) {
|
|
586
|
+
return { passed: false, detail: 'beforeDate was not provided.' };
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
passed: now < Date.parse(condition.beforeDate),
|
|
590
|
+
detail: `Current time is ${now < Date.parse(condition.beforeDate) ? 'before' : 'not before'} ${condition.beforeDate}.`,
|
|
591
|
+
context: { now, beforeDate: condition.beforeDate },
|
|
592
|
+
};
|
|
343
593
|
case 'after':
|
|
344
|
-
if (!condition.afterDate)
|
|
345
|
-
return false;
|
|
346
|
-
|
|
594
|
+
if (!condition.afterDate) {
|
|
595
|
+
return { passed: false, detail: 'afterDate was not provided.' };
|
|
596
|
+
}
|
|
597
|
+
return {
|
|
598
|
+
passed: now > Date.parse(condition.afterDate),
|
|
599
|
+
detail: `Current time is ${now > Date.parse(condition.afterDate) ? 'after' : 'not after'} ${condition.afterDate}.`,
|
|
600
|
+
context: { now, afterDate: condition.afterDate },
|
|
601
|
+
};
|
|
347
602
|
case 'between':
|
|
348
|
-
if (!condition.rangeDate || condition.rangeDate.length !== 2)
|
|
349
|
-
return false;
|
|
603
|
+
if (!condition.rangeDate || condition.rangeDate.length !== 2) {
|
|
604
|
+
return { passed: false, detail: 'rangeDate must contain exactly two entries.' };
|
|
605
|
+
}
|
|
350
606
|
const start = Date.parse(condition.rangeDate[0]);
|
|
351
607
|
const end = Date.parse(condition.rangeDate[1]);
|
|
352
|
-
return
|
|
608
|
+
return {
|
|
609
|
+
passed: now > start && now < end,
|
|
610
|
+
detail: `Current time is ${now > start && now < end ? 'within' : 'outside'} the configured date range.`,
|
|
611
|
+
context: { now, start, end, rangeDate: condition.rangeDate },
|
|
612
|
+
};
|
|
353
613
|
default:
|
|
354
|
-
return
|
|
614
|
+
return {
|
|
615
|
+
passed: false,
|
|
616
|
+
detail: `Unsupported dateTest ${condition.dateTest}.`,
|
|
617
|
+
};
|
|
355
618
|
}
|
|
356
619
|
}
|
|
357
620
|
/**
|
|
@@ -374,18 +637,39 @@ async function validateGeofence(condition, params) {
|
|
|
374
637
|
lng = location.longitude;
|
|
375
638
|
}
|
|
376
639
|
catch (error) {
|
|
377
|
-
return
|
|
640
|
+
return {
|
|
641
|
+
passed: false,
|
|
642
|
+
detail: 'getLocation threw while resolving user coordinates.',
|
|
643
|
+
context: { error },
|
|
644
|
+
};
|
|
378
645
|
}
|
|
379
646
|
}
|
|
380
647
|
if (lat === undefined || lng === undefined) {
|
|
381
|
-
return
|
|
648
|
+
return {
|
|
649
|
+
passed: false,
|
|
650
|
+
detail: 'Latitude/longitude were not available.',
|
|
651
|
+
};
|
|
382
652
|
}
|
|
383
653
|
// Check if outside bounding box
|
|
384
654
|
const outside = lat > condition.top ||
|
|
385
655
|
lat < condition.bottom ||
|
|
386
656
|
lng < condition.left ||
|
|
387
657
|
lng > condition.right;
|
|
388
|
-
return
|
|
658
|
+
return {
|
|
659
|
+
passed: condition.contains ? !outside : outside,
|
|
660
|
+
detail: `Coordinates are ${outside ? 'outside' : 'inside'} the configured geofence.`,
|
|
661
|
+
context: {
|
|
662
|
+
latitude: lat,
|
|
663
|
+
longitude: lng,
|
|
664
|
+
contains: condition.contains,
|
|
665
|
+
bounds: {
|
|
666
|
+
top: condition.top,
|
|
667
|
+
bottom: condition.bottom,
|
|
668
|
+
left: condition.left,
|
|
669
|
+
right: condition.right,
|
|
670
|
+
},
|
|
671
|
+
},
|
|
672
|
+
};
|
|
389
673
|
}
|
|
390
674
|
/**
|
|
391
675
|
* Validate value comparison condition
|
|
@@ -399,7 +683,10 @@ async function validateValue(condition, params) {
|
|
|
399
683
|
base = base[field];
|
|
400
684
|
}
|
|
401
685
|
else {
|
|
402
|
-
return
|
|
686
|
+
return {
|
|
687
|
+
passed: false,
|
|
688
|
+
detail: `Field ${condition.field} was not found in the provided params.`,
|
|
689
|
+
};
|
|
403
690
|
}
|
|
404
691
|
}
|
|
405
692
|
// Convert value to correct type
|
|
@@ -411,18 +698,37 @@ async function validateValue(condition, params) {
|
|
|
411
698
|
val = parseInt(val, 10);
|
|
412
699
|
}
|
|
413
700
|
// Perform comparison
|
|
701
|
+
let passed = false;
|
|
414
702
|
switch (condition.validationType) {
|
|
415
703
|
case 'equal':
|
|
416
|
-
|
|
704
|
+
passed = base == val;
|
|
705
|
+
break;
|
|
417
706
|
case 'not':
|
|
418
|
-
|
|
707
|
+
passed = base != val;
|
|
708
|
+
break;
|
|
419
709
|
case 'greater':
|
|
420
|
-
|
|
710
|
+
passed = base > val;
|
|
711
|
+
break;
|
|
421
712
|
case 'less':
|
|
422
|
-
|
|
713
|
+
passed = base < val;
|
|
714
|
+
break;
|
|
423
715
|
default:
|
|
424
|
-
return
|
|
716
|
+
return {
|
|
717
|
+
passed: false,
|
|
718
|
+
detail: `Unsupported validationType ${condition.validationType}.`,
|
|
719
|
+
};
|
|
425
720
|
}
|
|
721
|
+
return {
|
|
722
|
+
passed,
|
|
723
|
+
detail: `Compared field ${condition.field} (${String(base)}) with ${String(val)} using ${condition.validationType}.`,
|
|
724
|
+
context: {
|
|
725
|
+
field: condition.field,
|
|
726
|
+
fieldValue: base,
|
|
727
|
+
comparisonValue: val,
|
|
728
|
+
fieldType: condition.fieldType,
|
|
729
|
+
validationType: condition.validationType,
|
|
730
|
+
},
|
|
731
|
+
};
|
|
426
732
|
}
|
|
427
733
|
/**
|
|
428
734
|
* Validate item status condition
|
|
@@ -430,19 +736,40 @@ async function validateValue(condition, params) {
|
|
|
430
736
|
async function validateItemStatus(condition, params) {
|
|
431
737
|
switch (condition.statusType) {
|
|
432
738
|
case 'isClaimable':
|
|
433
|
-
return
|
|
739
|
+
return {
|
|
740
|
+
passed: !!(params.proof && params.proof.claimable),
|
|
741
|
+
detail: 'Checked proof.claimable for truthiness.',
|
|
742
|
+
};
|
|
434
743
|
case 'notClaimable':
|
|
435
|
-
return
|
|
744
|
+
return {
|
|
745
|
+
passed: !!(params.proof && !params.proof.claimable),
|
|
746
|
+
detail: 'Checked proof.claimable for falsiness.',
|
|
747
|
+
};
|
|
436
748
|
case 'noProof':
|
|
437
|
-
return
|
|
749
|
+
return {
|
|
750
|
+
passed: !params.proof,
|
|
751
|
+
detail: 'Checked that no proof object was provided.',
|
|
752
|
+
};
|
|
438
753
|
case 'hasProof':
|
|
439
|
-
return
|
|
754
|
+
return {
|
|
755
|
+
passed: !!params.proof,
|
|
756
|
+
detail: 'Checked that a proof object was provided.',
|
|
757
|
+
};
|
|
440
758
|
case 'isVirtual':
|
|
441
|
-
return
|
|
759
|
+
return {
|
|
760
|
+
passed: !!(params.proof && params.proof.virtual),
|
|
761
|
+
detail: 'Checked proof.virtual for truthiness.',
|
|
762
|
+
};
|
|
442
763
|
case 'notVirtual':
|
|
443
|
-
return
|
|
764
|
+
return {
|
|
765
|
+
passed: !!(params.proof && !params.proof.virtual),
|
|
766
|
+
detail: 'Checked proof.virtual for falsiness.',
|
|
767
|
+
};
|
|
444
768
|
default:
|
|
445
|
-
return
|
|
769
|
+
return {
|
|
770
|
+
passed: false,
|
|
771
|
+
detail: `Unsupported statusType ${condition.statusType}.`,
|
|
772
|
+
};
|
|
446
773
|
}
|
|
447
774
|
}
|
|
448
775
|
/**
|
package/docs/API_SUMMARY.md
CHANGED
package/docs/utils.md
CHANGED
|
@@ -280,6 +280,8 @@ const path = utils.buildPortalPath(params)
|
|
|
280
280
|
|
|
281
281
|
The `validateCondition` function helps determine if content should be shown or hidden based on various criteria like geography, device type, user status, dates, and more.
|
|
282
282
|
|
|
283
|
+
Enable verbose tracing per invocation with `debugConditions`, or set `globalThis.SMARTLINKS_CONDITION_DEBUG = true` in a browser/devtools session to trace every evaluation.
|
|
284
|
+
|
|
283
285
|
### Basic Usage
|
|
284
286
|
|
|
285
287
|
```typescript
|
|
@@ -291,7 +293,6 @@ const canShow = await utils.validateCondition({
|
|
|
291
293
|
type: 'and',
|
|
292
294
|
conditions: [{
|
|
293
295
|
type: 'country',
|
|
294
|
-
useRegions: true,
|
|
295
296
|
regions: ['eu'],
|
|
296
297
|
contains: true
|
|
297
298
|
}]
|
|
@@ -328,7 +329,6 @@ await utils.validateCondition({
|
|
|
328
329
|
type: 'and',
|
|
329
330
|
conditions: [{
|
|
330
331
|
type: 'country',
|
|
331
|
-
useRegions: true,
|
|
332
332
|
regions: ['eu', 'uk'],
|
|
333
333
|
contains: true // true = show IN these regions, false = hide IN these regions
|
|
334
334
|
}]
|
|
@@ -336,6 +336,20 @@ await utils.validateCondition({
|
|
|
336
336
|
user: { valid: true, location: { country: 'FR' } }
|
|
337
337
|
})
|
|
338
338
|
|
|
339
|
+
// Regions and explicit countries can be combined
|
|
340
|
+
await utils.validateCondition({
|
|
341
|
+
condition: {
|
|
342
|
+
type: 'and',
|
|
343
|
+
conditions: [{
|
|
344
|
+
type: 'country',
|
|
345
|
+
regions: ['eu'],
|
|
346
|
+
countries: ['CH'],
|
|
347
|
+
contains: true
|
|
348
|
+
}]
|
|
349
|
+
},
|
|
350
|
+
user: { valid: true, location: { country: 'CH' } }
|
|
351
|
+
})
|
|
352
|
+
|
|
339
353
|
// Or specific countries
|
|
340
354
|
await utils.validateCondition({
|
|
341
355
|
condition: {
|
|
@@ -582,7 +596,7 @@ await utils.validateCondition({
|
|
|
582
596
|
conditions: [
|
|
583
597
|
{ type: 'user', userType: 'valid' },
|
|
584
598
|
{ type: 'device', displays: ['mobile'], contains: true },
|
|
585
|
-
{ type: 'country',
|
|
599
|
+
{ type: 'country', regions: ['eu'], contains: true }
|
|
586
600
|
]
|
|
587
601
|
},
|
|
588
602
|
user: { valid: true, location: { country: 'FR' } },
|
|
@@ -630,12 +644,49 @@ const showNewFeature = await utils.validateCondition({
|
|
|
630
644
|
condition: {
|
|
631
645
|
type: 'and',
|
|
632
646
|
conditions: [
|
|
633
|
-
{ type: 'country',
|
|
647
|
+
{ type: 'country', regions: ['northamerica'], contains: true },
|
|
634
648
|
{ type: 'date', dateTest: 'after', afterDate: '2026-03-01' }
|
|
635
649
|
]
|
|
636
650
|
},
|
|
637
651
|
user: { valid: true, location: { country: 'US' } }
|
|
638
652
|
})
|
|
653
|
+
|
|
654
|
+
### Debug Logging
|
|
655
|
+
|
|
656
|
+
Trace which condition is being evaluated, whether it passed, and why:
|
|
657
|
+
|
|
658
|
+
```typescript
|
|
659
|
+
await utils.validateCondition({
|
|
660
|
+
condition: {
|
|
661
|
+
type: 'and',
|
|
662
|
+
conditions: [
|
|
663
|
+
{ type: 'user', userType: 'valid' },
|
|
664
|
+
{ type: 'country', regions: ['eu'], contains: true }
|
|
665
|
+
]
|
|
666
|
+
},
|
|
667
|
+
user: { valid: true, location: { country: 'DE' } },
|
|
668
|
+
debugConditions: true
|
|
669
|
+
})
|
|
670
|
+
|
|
671
|
+
// Or enable globally in the browser console/devtools
|
|
672
|
+
globalThis.SMARTLINKS_CONDITION_DEBUG = true
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
If you want to route logs somewhere specific, pass a logger:
|
|
676
|
+
|
|
677
|
+
```typescript
|
|
678
|
+
await utils.validateCondition({
|
|
679
|
+
condition: {
|
|
680
|
+
type: 'and',
|
|
681
|
+
conditions: [{ type: 'user', userType: 'valid' }]
|
|
682
|
+
},
|
|
683
|
+
user: { valid: true },
|
|
684
|
+
debugConditions: {
|
|
685
|
+
label: 'checkout-gate',
|
|
686
|
+
logger: (...args) => console.log(...args)
|
|
687
|
+
}
|
|
688
|
+
})
|
|
689
|
+
```
|
|
639
690
|
```
|
|
640
691
|
|
|
641
692
|
#### Mobile-Only Features
|