@explorins/pers-sdk-react-native 2.2.2 → 2.3.0-alpha.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/index.js +2232 -144
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -2624,6 +2624,8 @@ exports.ApiKeyType = void 0;
|
|
|
2624
2624
|
ApiKeyType["BLOCKCHAIN_READER_JWT"] = "BLOCKCHAIN_READER_JWT";
|
|
2625
2625
|
ApiKeyType["TRANSACTION_JWT_ACCESS_TOKEN"] = "TRANSACTION_JWT_ACCESS_TOKEN";
|
|
2626
2626
|
ApiKeyType["WEBHOOK_OUTBOUND_JWT"] = "WEBHOOK_OUTBOUND_JWT";
|
|
2627
|
+
/** One-Time Token for passwordless auth flows (magic link, SMS, etc.) */
|
|
2628
|
+
ApiKeyType["ONE_TIME_TOKEN"] = "ONE_TIME_TOKEN";
|
|
2627
2629
|
// TODO: this needs to be removed. However, there is still dependency
|
|
2628
2630
|
ApiKeyType["TENANT_LEGACY_ADMIN"] = "TENANT_ADMIN_JWT";
|
|
2629
2631
|
// TENANT_SYSTEM_API_SECRET = 'TENANT_SYSTEM_API_SECRET',
|
|
@@ -3184,6 +3186,629 @@ exports.WebhookCallbackStatus = void 0;
|
|
|
3184
3186
|
WebhookCallbackStatus["CANCELLED"] = "CANCELLED";
|
|
3185
3187
|
})(exports.WebhookCallbackStatus || (exports.WebhookCallbackStatus = {}));
|
|
3186
3188
|
|
|
3189
|
+
/**
|
|
3190
|
+
* One-Time Token Authentication Methods
|
|
3191
|
+
*
|
|
3192
|
+
* Defines the passwordless authentication methods supported by OTT flow.
|
|
3193
|
+
* Used in magic link, SMS verification, and email code authentication.
|
|
3194
|
+
*/
|
|
3195
|
+
const OttAuthMethod = {
|
|
3196
|
+
/** Magic link sent via email */
|
|
3197
|
+
MAGIC_LINK: 'magic-link',
|
|
3198
|
+
/** Verification code sent via SMS */
|
|
3199
|
+
SMS: 'sms',
|
|
3200
|
+
/** Verification code sent via email */
|
|
3201
|
+
EMAIL_CODE: 'email-code',
|
|
3202
|
+
};
|
|
3203
|
+
|
|
3204
|
+
/**
|
|
3205
|
+
* Base array of all string-valued user model property keys.
|
|
3206
|
+
* Single source of truth for user field names across the codebase.
|
|
3207
|
+
*/
|
|
3208
|
+
const userModelFieldsArray = [
|
|
3209
|
+
'id',
|
|
3210
|
+
'email',
|
|
3211
|
+
'identifierEmail',
|
|
3212
|
+
'firstName',
|
|
3213
|
+
'lastName',
|
|
3214
|
+
'externalId',
|
|
3215
|
+
'instagramAccountId',
|
|
3216
|
+
'googleAccountName',
|
|
3217
|
+
];
|
|
3218
|
+
/** Iterable array of all user model fields */
|
|
3219
|
+
const userModelFields = [...userModelFieldsArray];
|
|
3220
|
+
/**
|
|
3221
|
+
* Fields that can be used to uniquely identify/lookup a user.
|
|
3222
|
+
* Used for wildcard search, user lookup queries.
|
|
3223
|
+
*/
|
|
3224
|
+
const uniqueUserIdentifierKeysArray = [
|
|
3225
|
+
'id',
|
|
3226
|
+
'email',
|
|
3227
|
+
'identifierEmail',
|
|
3228
|
+
'instagramAccountId',
|
|
3229
|
+
'externalId',
|
|
3230
|
+
];
|
|
3231
|
+
// Export the array
|
|
3232
|
+
const uniqueUserIdentifierKeys = [...uniqueUserIdentifierKeysArray];
|
|
3233
|
+
// Define identifier type configurations
|
|
3234
|
+
const identifierTypes = {
|
|
3235
|
+
id: {
|
|
3236
|
+
key: 'id',
|
|
3237
|
+
detect: (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value), // UUID pattern
|
|
3238
|
+
process: (value) => value
|
|
3239
|
+
},
|
|
3240
|
+
email: {
|
|
3241
|
+
key: 'email',
|
|
3242
|
+
detect: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
|
|
3243
|
+
process: (value) => value
|
|
3244
|
+
},
|
|
3245
|
+
instagramAccountId: {
|
|
3246
|
+
key: 'instagramAccountId',
|
|
3247
|
+
detect: (value) => /^@/.test(value) || value.toLowerCase().includes('instagram'),
|
|
3248
|
+
process: (value) => value.replace(/^@/, '') // Remove @ if present for consistency
|
|
3249
|
+
},
|
|
3250
|
+
externalId: {
|
|
3251
|
+
key: 'externalId',
|
|
3252
|
+
detect: () => true, // Default fallback
|
|
3253
|
+
process: (value) => value
|
|
3254
|
+
}
|
|
3255
|
+
};
|
|
3256
|
+
/**
|
|
3257
|
+
* Register a new identifier type configuration
|
|
3258
|
+
* @param key The identifier key
|
|
3259
|
+
* @param config The identifier configuration (without the key)
|
|
3260
|
+
*/
|
|
3261
|
+
const registerIdentifierType = (key, config) => {
|
|
3262
|
+
if (!uniqueUserIdentifierKeysArray.includes(key)) {
|
|
3263
|
+
console.warn(`Warning: Registering identifier type '${key}' that is not in uniqueUserIdentifierKeys`);
|
|
3264
|
+
}
|
|
3265
|
+
identifierTypes[key] = {
|
|
3266
|
+
key,
|
|
3267
|
+
...config
|
|
3268
|
+
};
|
|
3269
|
+
};
|
|
3270
|
+
/**
|
|
3271
|
+
* Detects the type of identifier from a wildcard input
|
|
3272
|
+
* @param wildcardValue The value to analyze
|
|
3273
|
+
* @returns Object containing the type and processed value
|
|
3274
|
+
*/
|
|
3275
|
+
const detectIdentifierType = (wildcardValue) => {
|
|
3276
|
+
if (wildcardValue === null || wildcardValue === undefined) {
|
|
3277
|
+
return { type: 'externalId', value: '' };
|
|
3278
|
+
}
|
|
3279
|
+
const value = String(wildcardValue);
|
|
3280
|
+
// Try each identifier type in priority order (id first to catch UUIDs)
|
|
3281
|
+
for (const type of ['id', 'email', 'instagramAccountId', 'externalId']) {
|
|
3282
|
+
const config = identifierTypes[type];
|
|
3283
|
+
if (config.detect(value)) {
|
|
3284
|
+
return { type, value: config.process(value) };
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
// Default fallback should never be reached due to externalId's detect always returning true
|
|
3288
|
+
return { type: 'externalId', value };
|
|
3289
|
+
};
|
|
3290
|
+
/**
|
|
3291
|
+
* Checks if an object is a properly formed UserIdentifierObject
|
|
3292
|
+
* @param obj Object to check
|
|
3293
|
+
* @returns Boolean indicating if the object is a UserIdentifierObject
|
|
3294
|
+
*/
|
|
3295
|
+
const isUserIdentifierObject = (obj) => {
|
|
3296
|
+
return obj &&
|
|
3297
|
+
obj.type === 'identifierObject' &&
|
|
3298
|
+
uniqueUserIdentifierKeys.every(key => key in obj);
|
|
3299
|
+
};
|
|
3300
|
+
/**
|
|
3301
|
+
* Creates a UserIdentifierObject with all required fields, setting missing values to null
|
|
3302
|
+
* @param params Partial parameters for the identifier object
|
|
3303
|
+
* @returns A properly formed UserIdentifierObject
|
|
3304
|
+
*/
|
|
3305
|
+
const createUserIdentifierObject = (params) => {
|
|
3306
|
+
// Extract the wildcard or use empty string
|
|
3307
|
+
const wildcard = params.wildcard || '';
|
|
3308
|
+
// Create base object with all identifier fields set to null
|
|
3309
|
+
const identifierObject = {
|
|
3310
|
+
type: 'identifierObject',
|
|
3311
|
+
id: null,
|
|
3312
|
+
email: null,
|
|
3313
|
+
instagramAccountId: null,
|
|
3314
|
+
externalId: null,
|
|
3315
|
+
identifierEmail: null,
|
|
3316
|
+
wildcard
|
|
3317
|
+
};
|
|
3318
|
+
// Override nulls with any provided values
|
|
3319
|
+
uniqueUserIdentifierKeys.forEach(key => {
|
|
3320
|
+
if (params[key]) {
|
|
3321
|
+
identifierObject[key] = params[key];
|
|
3322
|
+
}
|
|
3323
|
+
});
|
|
3324
|
+
// If wildcard is provided but no specific fields, auto-detect the field type
|
|
3325
|
+
if (wildcard && !params.id && !params.email && !params.instagramAccountId && !params.externalId) {
|
|
3326
|
+
const detected = detectIdentifierType(wildcard);
|
|
3327
|
+
identifierObject[detected.type] = detected.value;
|
|
3328
|
+
}
|
|
3329
|
+
return identifierObject;
|
|
3330
|
+
};
|
|
3331
|
+
|
|
3332
|
+
/**
|
|
3333
|
+
* Validation Types
|
|
3334
|
+
* Shared type definitions for field validation.
|
|
3335
|
+
*/
|
|
3336
|
+
// ============================================================================
|
|
3337
|
+
// Operator Text Mapping
|
|
3338
|
+
// ============================================================================
|
|
3339
|
+
const operatorText = {
|
|
3340
|
+
'<': 'less than',
|
|
3341
|
+
'<=': 'less than or equal to',
|
|
3342
|
+
'>': 'greater than',
|
|
3343
|
+
'>=': 'greater than or equal to',
|
|
3344
|
+
'==': 'equal to',
|
|
3345
|
+
'!=': 'not equal to',
|
|
3346
|
+
};
|
|
3347
|
+
|
|
3348
|
+
/**
|
|
3349
|
+
* Standalone Validators
|
|
3350
|
+
* Pure validation functions that can be used independently.
|
|
3351
|
+
* Shared between backend and frontend.
|
|
3352
|
+
*/
|
|
3353
|
+
// ============================================================================
|
|
3354
|
+
// Regex Patterns
|
|
3355
|
+
// ============================================================================
|
|
3356
|
+
/** RFC-compliant email regex */
|
|
3357
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
3358
|
+
const EMAIL_MAX_LENGTH = 254; // RFC 5321
|
|
3359
|
+
/** Basic URL pattern - http(s) with domain */
|
|
3360
|
+
const URL_REGEX = /^https?:\/\/[^\s/$.?#].[^\s]*$/i;
|
|
3361
|
+
/** Ethereum address format: 0x followed by 40 hexadecimal characters */
|
|
3362
|
+
const WALLET_ADDRESS_REGEX = /^0x[a-fA-F0-9]{40}$/;
|
|
3363
|
+
/** UUID v4 format */
|
|
3364
|
+
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
3365
|
+
// ============================================================================
|
|
3366
|
+
// Validators
|
|
3367
|
+
// ============================================================================
|
|
3368
|
+
/**
|
|
3369
|
+
* Validates email format according to RFC standards.
|
|
3370
|
+
* RFC 5321 length limit (254 characters) enforced.
|
|
3371
|
+
*/
|
|
3372
|
+
function isValidEmail(email) {
|
|
3373
|
+
if (!email || email.length > EMAIL_MAX_LENGTH)
|
|
3374
|
+
return false;
|
|
3375
|
+
return EMAIL_REGEX.test(email);
|
|
3376
|
+
}
|
|
3377
|
+
/**
|
|
3378
|
+
* Validates URL format (http/https only).
|
|
3379
|
+
*/
|
|
3380
|
+
function isValidUrl(url) {
|
|
3381
|
+
if (!url)
|
|
3382
|
+
return false;
|
|
3383
|
+
return URL_REGEX.test(url);
|
|
3384
|
+
}
|
|
3385
|
+
/**
|
|
3386
|
+
* Validates Ethereum wallet address format.
|
|
3387
|
+
* Format: 0x followed by 40 hexadecimal characters.
|
|
3388
|
+
*/
|
|
3389
|
+
function isValidWalletAddress(address) {
|
|
3390
|
+
if (!address)
|
|
3391
|
+
return false;
|
|
3392
|
+
return WALLET_ADDRESS_REGEX.test(address);
|
|
3393
|
+
}
|
|
3394
|
+
/**
|
|
3395
|
+
* Validates that a string is not empty and meets minimum length.
|
|
3396
|
+
*/
|
|
3397
|
+
function isValidNonEmptyString(value, minLength = 1) {
|
|
3398
|
+
return typeof value === 'string' && value.trim().length >= minLength;
|
|
3399
|
+
}
|
|
3400
|
+
/**
|
|
3401
|
+
* Validates UUID v4 format.
|
|
3402
|
+
*/
|
|
3403
|
+
function isValidUuid(uuid) {
|
|
3404
|
+
if (!uuid)
|
|
3405
|
+
return false;
|
|
3406
|
+
return UUID_REGEX.test(uuid);
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
// ============================================================================
|
|
3410
|
+
// Comparison Functions
|
|
3411
|
+
// ============================================================================
|
|
3412
|
+
/**
|
|
3413
|
+
* Compare two values using the specified operator.
|
|
3414
|
+
* Works with numbers, dates, and strings.
|
|
3415
|
+
*/
|
|
3416
|
+
const compareValues = (a, b, op) => {
|
|
3417
|
+
switch (op) {
|
|
3418
|
+
case '<': return a < b;
|
|
3419
|
+
case '<=': return a <= b;
|
|
3420
|
+
case '>': return a > b;
|
|
3421
|
+
case '>=': return a >= b;
|
|
3422
|
+
case '==': return a === b;
|
|
3423
|
+
case '!=': return a !== b;
|
|
3424
|
+
}
|
|
3425
|
+
};
|
|
3426
|
+
/** Get human-readable operator text for error messages. */
|
|
3427
|
+
const getOperatorText = (op) => operatorText[op];
|
|
3428
|
+
// ============================================================================
|
|
3429
|
+
// Single Field Validation
|
|
3430
|
+
// ============================================================================
|
|
3431
|
+
/**
|
|
3432
|
+
* Validate a single field value against validation rules.
|
|
3433
|
+
* Does not check cross-field comparisons (use validateFieldComparisons for that).
|
|
3434
|
+
*/
|
|
3435
|
+
const validateFieldValue = (value, rules, fieldType) => {
|
|
3436
|
+
// No rules = valid
|
|
3437
|
+
if (!rules) {
|
|
3438
|
+
return null;
|
|
3439
|
+
}
|
|
3440
|
+
const stringValue = value !== null && value !== undefined ? String(value) : '';
|
|
3441
|
+
const isEmpty = value === null || value === undefined || value === '';
|
|
3442
|
+
// Skip validation if empty - required-ness is determined by requiredUserFields
|
|
3443
|
+
// (the merged field on RedemptionDTO), not by a validation rule.
|
|
3444
|
+
if (isEmpty) {
|
|
3445
|
+
return null;
|
|
3446
|
+
}
|
|
3447
|
+
// Type-specific validation
|
|
3448
|
+
switch (fieldType) {
|
|
3449
|
+
case 'text':
|
|
3450
|
+
if (rules.minLength && stringValue.length < rules.minLength) {
|
|
3451
|
+
return { field: '', message: `Minimum ${rules.minLength} characters required`, code: 'minLength' };
|
|
3452
|
+
}
|
|
3453
|
+
if (rules.maxLength && stringValue.length > rules.maxLength) {
|
|
3454
|
+
return { field: '', message: `Maximum ${rules.maxLength} characters allowed`, code: 'maxLength' };
|
|
3455
|
+
}
|
|
3456
|
+
if (rules.pattern && !new RegExp(rules.pattern).test(stringValue)) {
|
|
3457
|
+
return { field: '', message: rules.patternMessage || 'Invalid format', code: 'pattern' };
|
|
3458
|
+
}
|
|
3459
|
+
break;
|
|
3460
|
+
case 'email':
|
|
3461
|
+
if (stringValue.length > EMAIL_MAX_LENGTH || !EMAIL_REGEX.test(stringValue)) {
|
|
3462
|
+
return { field: '', message: 'Invalid email format', code: 'email' };
|
|
3463
|
+
}
|
|
3464
|
+
if (rules.maxLength && stringValue.length > rules.maxLength) {
|
|
3465
|
+
return { field: '', message: `Maximum ${rules.maxLength} characters allowed`, code: 'maxLength' };
|
|
3466
|
+
}
|
|
3467
|
+
break;
|
|
3468
|
+
case 'url':
|
|
3469
|
+
if (!URL_REGEX.test(stringValue)) {
|
|
3470
|
+
return { field: '', message: 'Invalid URL format', code: 'url' };
|
|
3471
|
+
}
|
|
3472
|
+
if (rules.maxLength && stringValue.length > rules.maxLength) {
|
|
3473
|
+
return { field: '', message: `Maximum ${rules.maxLength} characters allowed`, code: 'maxLength' };
|
|
3474
|
+
}
|
|
3475
|
+
break;
|
|
3476
|
+
case 'number':
|
|
3477
|
+
const numValue = typeof value === 'number' ? value : parseFloat(stringValue);
|
|
3478
|
+
if (isNaN(numValue)) {
|
|
3479
|
+
return { field: '', message: 'Must be a valid number', code: 'type' };
|
|
3480
|
+
}
|
|
3481
|
+
if (rules.min !== undefined && numValue < rules.min) {
|
|
3482
|
+
return { field: '', message: `Minimum value is ${rules.min}`, code: 'min' };
|
|
3483
|
+
}
|
|
3484
|
+
if (rules.max !== undefined && numValue > rules.max) {
|
|
3485
|
+
return { field: '', message: `Maximum value is ${rules.max}`, code: 'max' };
|
|
3486
|
+
}
|
|
3487
|
+
break;
|
|
3488
|
+
case 'boolean':
|
|
3489
|
+
if (typeof value !== 'boolean' && stringValue !== 'true' && stringValue !== 'false') {
|
|
3490
|
+
return { field: '', message: 'Must be true or false', code: 'type' };
|
|
3491
|
+
}
|
|
3492
|
+
break;
|
|
3493
|
+
case 'date':
|
|
3494
|
+
const dateValue = new Date(stringValue);
|
|
3495
|
+
if (isNaN(dateValue.getTime())) {
|
|
3496
|
+
return { field: '', message: 'Must be a valid date', code: 'type' };
|
|
3497
|
+
}
|
|
3498
|
+
const now = new Date();
|
|
3499
|
+
if (rules.minDate) {
|
|
3500
|
+
const minDate = rules.minDate === 'now' ? now : new Date(rules.minDate);
|
|
3501
|
+
if (dateValue < minDate) {
|
|
3502
|
+
return {
|
|
3503
|
+
field: '',
|
|
3504
|
+
message: rules.minDate === 'now' ? 'Date must be in the future' : `Date must be after ${rules.minDate}`,
|
|
3505
|
+
code: 'minDate'
|
|
3506
|
+
};
|
|
3507
|
+
}
|
|
3508
|
+
}
|
|
3509
|
+
if (rules.maxDate) {
|
|
3510
|
+
const maxDate = rules.maxDate === 'now' ? now : new Date(rules.maxDate);
|
|
3511
|
+
if (dateValue > maxDate) {
|
|
3512
|
+
return {
|
|
3513
|
+
field: '',
|
|
3514
|
+
message: rules.maxDate === 'now' ? 'Date must be in the past' : `Date must be before ${rules.maxDate}`,
|
|
3515
|
+
code: 'maxDate'
|
|
3516
|
+
};
|
|
3517
|
+
}
|
|
3518
|
+
}
|
|
3519
|
+
break;
|
|
3520
|
+
case 'objectArray':
|
|
3521
|
+
if (!Array.isArray(value)) {
|
|
3522
|
+
return { field: '', message: 'Must be an array', code: 'type' };
|
|
3523
|
+
}
|
|
3524
|
+
if (rules.minItems !== undefined && value.length < rules.minItems) {
|
|
3525
|
+
return { field: '', message: `Minimum ${rules.minItems} items required`, code: 'minItems' };
|
|
3526
|
+
}
|
|
3527
|
+
if (rules.maxItems !== undefined && value.length > rules.maxItems) {
|
|
3528
|
+
return { field: '', message: `Maximum ${rules.maxItems} items allowed`, code: 'maxItems' };
|
|
3529
|
+
}
|
|
3530
|
+
break;
|
|
3531
|
+
}
|
|
3532
|
+
return null;
|
|
3533
|
+
};
|
|
3534
|
+
// ============================================================================
|
|
3535
|
+
// Cross-Field / Literal Comparison Validation
|
|
3536
|
+
// ============================================================================
|
|
3537
|
+
/** Type guard for field comparison */
|
|
3538
|
+
const isFieldComparison = (c) => 'field' in c && c.field !== undefined;
|
|
3539
|
+
/**
|
|
3540
|
+
* Validate comparison rules for a single field.
|
|
3541
|
+
* Supports comparing to another field OR a literal value.
|
|
3542
|
+
*/
|
|
3543
|
+
const validateFieldComparisons = (fieldKey, value, comparisons, allData, fieldType) => {
|
|
3544
|
+
if (!comparisons || comparisons.length === 0)
|
|
3545
|
+
return [];
|
|
3546
|
+
const errors = [];
|
|
3547
|
+
for (const comparison of comparisons) {
|
|
3548
|
+
// Skip if this field's value is empty
|
|
3549
|
+
if (value === null || value === undefined || value === '')
|
|
3550
|
+
continue;
|
|
3551
|
+
// Get the comparison target (field value or literal)
|
|
3552
|
+
const isField = isFieldComparison(comparison);
|
|
3553
|
+
const targetValue = isField ? allData[comparison.field] : comparison.value;
|
|
3554
|
+
const targetLabel = isField ? comparison.field : String(comparison.value);
|
|
3555
|
+
// Skip if comparing to field and that field is empty
|
|
3556
|
+
if (isField && (targetValue === null || targetValue === undefined || targetValue === ''))
|
|
3557
|
+
continue;
|
|
3558
|
+
let isValid = true;
|
|
3559
|
+
const { operator, message } = comparison;
|
|
3560
|
+
switch (fieldType) {
|
|
3561
|
+
case 'date': {
|
|
3562
|
+
const thisDate = new Date(String(value));
|
|
3563
|
+
const otherDate = new Date(String(targetValue));
|
|
3564
|
+
if (isNaN(thisDate.getTime()) || isNaN(otherDate.getTime()))
|
|
3565
|
+
continue;
|
|
3566
|
+
isValid = compareValues(thisDate.getTime(), otherDate.getTime(), operator);
|
|
3567
|
+
break;
|
|
3568
|
+
}
|
|
3569
|
+
case 'number': {
|
|
3570
|
+
const thisNum = typeof value === 'number' ? value : parseFloat(String(value));
|
|
3571
|
+
const otherNum = typeof targetValue === 'number' ? targetValue : parseFloat(String(targetValue));
|
|
3572
|
+
if (isNaN(thisNum) || isNaN(otherNum))
|
|
3573
|
+
continue;
|
|
3574
|
+
isValid = compareValues(thisNum, otherNum, operator);
|
|
3575
|
+
break;
|
|
3576
|
+
}
|
|
3577
|
+
case 'boolean': {
|
|
3578
|
+
const thisBool = typeof value === 'boolean' ? value : value === 'true';
|
|
3579
|
+
const otherBool = typeof targetValue === 'boolean' ? targetValue : targetValue === 'true';
|
|
3580
|
+
isValid = operator === '==' ? thisBool === otherBool :
|
|
3581
|
+
operator === '!=' ? thisBool !== otherBool : true;
|
|
3582
|
+
break;
|
|
3583
|
+
}
|
|
3584
|
+
default: {
|
|
3585
|
+
isValid = compareValues(String(value), String(targetValue), operator);
|
|
3586
|
+
break;
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
if (!isValid) {
|
|
3590
|
+
errors.push({
|
|
3591
|
+
field: fieldKey,
|
|
3592
|
+
message: message || `Must be ${getOperatorText(operator)} ${targetLabel}`,
|
|
3593
|
+
code: `comparison:${operator}:${targetLabel}`,
|
|
3594
|
+
});
|
|
3595
|
+
}
|
|
3596
|
+
}
|
|
3597
|
+
return errors;
|
|
3598
|
+
};
|
|
3599
|
+
// ============================================================================
|
|
3600
|
+
// Object Array Item Validation
|
|
3601
|
+
// ============================================================================
|
|
3602
|
+
/**
|
|
3603
|
+
* Validate each item in an object array against the object schema.
|
|
3604
|
+
* Checks required properties, type validation, and validates each property.
|
|
3605
|
+
*
|
|
3606
|
+
* @param fieldKey - Parent field key (for error reporting)
|
|
3607
|
+
* @param items - Array of objects to validate
|
|
3608
|
+
* @param schema - Schema defining expected properties
|
|
3609
|
+
* @returns Array of validation errors with item index in field path (e.g., "contacts[0].phone")
|
|
3610
|
+
*/
|
|
3611
|
+
const validateObjectArrayItems = (fieldKey, items, schema) => {
|
|
3612
|
+
const errors = [];
|
|
3613
|
+
if (!Array.isArray(items))
|
|
3614
|
+
return errors;
|
|
3615
|
+
items.forEach((item, index) => {
|
|
3616
|
+
if (typeof item !== 'object' || item === null) {
|
|
3617
|
+
errors.push({
|
|
3618
|
+
field: `${fieldKey}[${index}]`,
|
|
3619
|
+
code: 'type',
|
|
3620
|
+
message: 'Item must be an object',
|
|
3621
|
+
});
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
const obj = item;
|
|
3625
|
+
// Check each schema property
|
|
3626
|
+
for (const prop of schema) {
|
|
3627
|
+
const value = obj[prop.key];
|
|
3628
|
+
const propPath = `${fieldKey}[${index}].${prop.key}`;
|
|
3629
|
+
const isEmpty = value === null || value === undefined || value === '';
|
|
3630
|
+
// Required check for nested properties (ObjectFieldSchema.required)
|
|
3631
|
+
if (prop.required && isEmpty) {
|
|
3632
|
+
errors.push({
|
|
3633
|
+
field: propPath,
|
|
3634
|
+
code: 'required',
|
|
3635
|
+
message: `${prop.label} is required`,
|
|
3636
|
+
});
|
|
3637
|
+
continue;
|
|
3638
|
+
}
|
|
3639
|
+
if (isEmpty)
|
|
3640
|
+
continue;
|
|
3641
|
+
// Convert ObjectFieldSchema inline rules to FieldValidationRules
|
|
3642
|
+
const rules = {
|
|
3643
|
+
min: prop.min,
|
|
3644
|
+
max: prop.max,
|
|
3645
|
+
minLength: prop.minLength,
|
|
3646
|
+
maxLength: prop.maxLength,
|
|
3647
|
+
pattern: prop.pattern,
|
|
3648
|
+
patternMessage: prop.patternMessage,
|
|
3649
|
+
};
|
|
3650
|
+
// Validate the property value
|
|
3651
|
+
const error = validateFieldValue(value, rules, prop.fieldType);
|
|
3652
|
+
if (error) {
|
|
3653
|
+
errors.push({ ...error, field: propPath });
|
|
3654
|
+
}
|
|
3655
|
+
}
|
|
3656
|
+
// Cross-property comparisons within the same object item
|
|
3657
|
+
// e.g., checkOutDate > checkInDate within each reservation
|
|
3658
|
+
for (const prop of schema) {
|
|
3659
|
+
const value = obj[prop.key];
|
|
3660
|
+
if (value === null || value === undefined || value === '')
|
|
3661
|
+
continue;
|
|
3662
|
+
// ObjectFieldSchema doesn't currently have comparisons, but we could
|
|
3663
|
+
// validate date fields against 'now' using minDate/maxDate if we extend the schema
|
|
3664
|
+
}
|
|
3665
|
+
});
|
|
3666
|
+
return errors;
|
|
3667
|
+
};
|
|
3668
|
+
// ============================================================================
|
|
3669
|
+
// Full Data Validation
|
|
3670
|
+
// ============================================================================
|
|
3671
|
+
/**
|
|
3672
|
+
* Validate all fields in a data object against their definitions.
|
|
3673
|
+
* Runs both single-field and comparison validations.
|
|
3674
|
+
*
|
|
3675
|
+
* @param data - The data object to validate
|
|
3676
|
+
* @param definitions - Field definitions with validation rules
|
|
3677
|
+
* @returns Array of all validation errors
|
|
3678
|
+
*/
|
|
3679
|
+
const validateAllFields = (data, definitions) => {
|
|
3680
|
+
const errors = [];
|
|
3681
|
+
const definitionMap = new Map(definitions.map(d => [d.key, d]));
|
|
3682
|
+
for (const [key, value] of Object.entries(data)) {
|
|
3683
|
+
const def = definitionMap.get(key);
|
|
3684
|
+
if (!def)
|
|
3685
|
+
continue;
|
|
3686
|
+
// Single-field validation
|
|
3687
|
+
const error = validateFieldValue(value, def.validation, def.fieldType);
|
|
3688
|
+
if (error) {
|
|
3689
|
+
errors.push({ ...error, field: key });
|
|
3690
|
+
}
|
|
3691
|
+
// Comparison validation
|
|
3692
|
+
if (def.validation?.comparisons) {
|
|
3693
|
+
const comparisonErrors = validateFieldComparisons(key, value, def.validation.comparisons, data, def.fieldType);
|
|
3694
|
+
errors.push(...comparisonErrors);
|
|
3695
|
+
}
|
|
3696
|
+
// Object array item validation (validate each item's properties against objectSchema)
|
|
3697
|
+
if (def.fieldType === 'objectArray' && def.objectSchema && Array.isArray(value)) {
|
|
3698
|
+
const itemErrors = validateObjectArrayItems(key, value, def.objectSchema);
|
|
3699
|
+
errors.push(...itemErrors);
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
return errors;
|
|
3703
|
+
};
|
|
3704
|
+
|
|
3705
|
+
/**
|
|
3706
|
+
* Built-in user profile fields that can be required for redemptions.
|
|
3707
|
+
* These are a subset of UserModelField - excludes 'id' and 'identifierEmail'
|
|
3708
|
+
* since those are system-managed, not user-fillable profile fields.
|
|
3709
|
+
*
|
|
3710
|
+
* Used for:
|
|
3711
|
+
* - RedemptionType base required fields (RedemptionType.baseRequiredUserFields)
|
|
3712
|
+
* - Redemption specific required fields (Redemption.specificRequiredUserFields)
|
|
3713
|
+
* - User profile validation before redemption claims
|
|
3714
|
+
* - Form generation for missing field prompts
|
|
3715
|
+
*/
|
|
3716
|
+
// Fields that can be required for user profile validation (excludes system fields)
|
|
3717
|
+
const builtInUserFieldsArray = [
|
|
3718
|
+
'email',
|
|
3719
|
+
'firstName',
|
|
3720
|
+
'lastName',
|
|
3721
|
+
'externalId',
|
|
3722
|
+
'instagramAccountId',
|
|
3723
|
+
'googleAccountName',
|
|
3724
|
+
];
|
|
3725
|
+
// Export the array for iteration
|
|
3726
|
+
const builtInUserFields = [...builtInUserFieldsArray];
|
|
3727
|
+
/**
|
|
3728
|
+
* Static metadata mapping for all built-in user fields.
|
|
3729
|
+
* Used by validation services and UI form generation.
|
|
3730
|
+
*/
|
|
3731
|
+
const builtInUserFieldMetadata = {
|
|
3732
|
+
email: {
|
|
3733
|
+
label: 'Email Address',
|
|
3734
|
+
description: 'User email address',
|
|
3735
|
+
fieldType: 'email',
|
|
3736
|
+
},
|
|
3737
|
+
firstName: {
|
|
3738
|
+
label: 'First Name',
|
|
3739
|
+
description: 'User first name',
|
|
3740
|
+
fieldType: 'text',
|
|
3741
|
+
},
|
|
3742
|
+
lastName: {
|
|
3743
|
+
label: 'Last Name',
|
|
3744
|
+
description: 'User last name',
|
|
3745
|
+
fieldType: 'text',
|
|
3746
|
+
},
|
|
3747
|
+
externalId: {
|
|
3748
|
+
label: 'External ID',
|
|
3749
|
+
description: 'External identifier from third-party system',
|
|
3750
|
+
fieldType: 'text',
|
|
3751
|
+
},
|
|
3752
|
+
instagramAccountId: {
|
|
3753
|
+
label: 'Instagram Account',
|
|
3754
|
+
description: 'Instagram account ID',
|
|
3755
|
+
fieldType: 'text',
|
|
3756
|
+
},
|
|
3757
|
+
googleAccountName: {
|
|
3758
|
+
label: 'Google Account',
|
|
3759
|
+
description: 'Google account name',
|
|
3760
|
+
fieldType: 'text',
|
|
3761
|
+
},
|
|
3762
|
+
};
|
|
3763
|
+
/**
|
|
3764
|
+
* Check if a string is a valid built-in user field
|
|
3765
|
+
*/
|
|
3766
|
+
const isBuiltInUserField = (value) => {
|
|
3767
|
+
return builtInUserFields.includes(value);
|
|
3768
|
+
};
|
|
3769
|
+
const getBuiltInFieldDefinition = (field) => {
|
|
3770
|
+
if (!isBuiltInUserField(field)) {
|
|
3771
|
+
return null;
|
|
3772
|
+
}
|
|
3773
|
+
const metadata = builtInUserFieldMetadata[field];
|
|
3774
|
+
const sortOrder = builtInUserFields.indexOf(field);
|
|
3775
|
+
return {
|
|
3776
|
+
id: `builtin:${field}`,
|
|
3777
|
+
tenantId: null,
|
|
3778
|
+
entityType: 'user',
|
|
3779
|
+
key: field,
|
|
3780
|
+
label: metadata.label,
|
|
3781
|
+
description: metadata.description,
|
|
3782
|
+
fieldType: metadata.fieldType,
|
|
3783
|
+
objectSchema: null,
|
|
3784
|
+
selectOptions: null,
|
|
3785
|
+
selectOptionsSource: null,
|
|
3786
|
+
validation: null,
|
|
3787
|
+
sortOrder,
|
|
3788
|
+
createdAt: null,
|
|
3789
|
+
updatedAt: null,
|
|
3790
|
+
isBuiltIn: true,
|
|
3791
|
+
};
|
|
3792
|
+
};
|
|
3793
|
+
/**
|
|
3794
|
+
* Get all built-in field definitions as an array.
|
|
3795
|
+
* Useful for populating field definitions list.
|
|
3796
|
+
*/
|
|
3797
|
+
const getAllBuiltInFieldDefinitions = () => {
|
|
3798
|
+
return builtInUserFields.map(field => getBuiltInFieldDefinition(field));
|
|
3799
|
+
};
|
|
3800
|
+
/**
|
|
3801
|
+
* Entity types for custom fields and dynamic select options.
|
|
3802
|
+
* Used for both field definitions (entityType) and select option sources.
|
|
3803
|
+
* - 'user': User profile fields (stored in user.customData)
|
|
3804
|
+
* - 'business': Business profile fields (stored in business.customData)
|
|
3805
|
+
* - 'campaign': Campaign-specific custom fields
|
|
3806
|
+
* - 'redemption': Redemption-specific custom fields
|
|
3807
|
+
*/
|
|
3808
|
+
const customFieldEntityTypesArray = ['user', 'business', 'campaign', 'redemption'];
|
|
3809
|
+
/** All supported custom field entity types for iteration/validation */
|
|
3810
|
+
const customFieldEntityTypes = [...customFieldEntityTypesArray];
|
|
3811
|
+
|
|
3187
3812
|
/**
|
|
3188
3813
|
* Core Entity Types
|
|
3189
3814
|
*
|
|
@@ -3346,119 +3971,17 @@ function isPingMessage(msg) {
|
|
|
3346
3971
|
return msg.type === 'ping';
|
|
3347
3972
|
}
|
|
3348
3973
|
|
|
3349
|
-
const apiPublicKeyTestPrefix = 'pk_test_';
|
|
3350
|
-
const testnetPrefix = 'testnet-';
|
|
3351
|
-
const testnetShortPrefix = 'tn-';
|
|
3352
|
-
|
|
3353
|
-
// Define the keys as a constant array
|
|
3354
|
-
const uniqueUserIdentifierKeysArray = [
|
|
3355
|
-
'id',
|
|
3356
|
-
'email',
|
|
3357
|
-
'identifierEmail',
|
|
3358
|
-
'instagramAccountId',
|
|
3359
|
-
'externalId', // This is a special key that can be used to search all fields
|
|
3360
|
-
];
|
|
3361
|
-
// Export the array
|
|
3362
|
-
const uniqueUserIdentifierKeys = [...uniqueUserIdentifierKeysArray];
|
|
3363
|
-
// Define identifier type configurations
|
|
3364
|
-
const identifierTypes = {
|
|
3365
|
-
id: {
|
|
3366
|
-
key: 'id',
|
|
3367
|
-
detect: (value) => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value), // UUID pattern
|
|
3368
|
-
process: (value) => value
|
|
3369
|
-
},
|
|
3370
|
-
email: {
|
|
3371
|
-
key: 'email',
|
|
3372
|
-
detect: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
|
|
3373
|
-
process: (value) => value
|
|
3374
|
-
},
|
|
3375
|
-
instagramAccountId: {
|
|
3376
|
-
key: 'instagramAccountId',
|
|
3377
|
-
detect: (value) => /^@/.test(value) || value.toLowerCase().includes('instagram'),
|
|
3378
|
-
process: (value) => value.replace(/^@/, '') // Remove @ if present for consistency
|
|
3379
|
-
},
|
|
3380
|
-
externalId: {
|
|
3381
|
-
key: 'externalId',
|
|
3382
|
-
detect: () => true, // Default fallback
|
|
3383
|
-
process: (value) => value
|
|
3384
|
-
}
|
|
3385
|
-
};
|
|
3386
3974
|
/**
|
|
3387
|
-
*
|
|
3388
|
-
* @param key The identifier key
|
|
3389
|
-
* @param config The identifier configuration (without the key)
|
|
3975
|
+
* Check if a string matches UUID format
|
|
3390
3976
|
*/
|
|
3391
|
-
const
|
|
3392
|
-
|
|
3393
|
-
console.warn(`Warning: Registering identifier type '${key}' that is not in uniqueUserIdentifierKeys`);
|
|
3394
|
-
}
|
|
3395
|
-
identifierTypes[key] = {
|
|
3396
|
-
key,
|
|
3397
|
-
...config
|
|
3398
|
-
};
|
|
3399
|
-
};
|
|
3400
|
-
/**
|
|
3401
|
-
* Detects the type of identifier from a wildcard input
|
|
3402
|
-
* @param wildcardValue The value to analyze
|
|
3403
|
-
* @returns Object containing the type and processed value
|
|
3404
|
-
*/
|
|
3405
|
-
const detectIdentifierType = (wildcardValue) => {
|
|
3406
|
-
if (wildcardValue === null || wildcardValue === undefined) {
|
|
3407
|
-
return { type: 'externalId', value: '' };
|
|
3408
|
-
}
|
|
3409
|
-
const value = String(wildcardValue);
|
|
3410
|
-
// Try each identifier type in priority order (id first to catch UUIDs)
|
|
3411
|
-
for (const type of ['id', 'email', 'instagramAccountId', 'externalId']) {
|
|
3412
|
-
const config = identifierTypes[type];
|
|
3413
|
-
if (config.detect(value)) {
|
|
3414
|
-
return { type, value: config.process(value) };
|
|
3415
|
-
}
|
|
3416
|
-
}
|
|
3417
|
-
// Default fallback should never be reached due to externalId's detect always returning true
|
|
3418
|
-
return { type: 'externalId', value };
|
|
3419
|
-
};
|
|
3420
|
-
/**
|
|
3421
|
-
* Checks if an object is a properly formed UserIdentifierObject
|
|
3422
|
-
* @param obj Object to check
|
|
3423
|
-
* @returns Boolean indicating if the object is a UserIdentifierObject
|
|
3424
|
-
*/
|
|
3425
|
-
const isUserIdentifierObject = (obj) => {
|
|
3426
|
-
return obj &&
|
|
3427
|
-
obj.type === 'identifierObject' &&
|
|
3428
|
-
uniqueUserIdentifierKeys.every(key => key in obj);
|
|
3429
|
-
};
|
|
3430
|
-
/**
|
|
3431
|
-
* Creates a UserIdentifierObject with all required fields, setting missing values to null
|
|
3432
|
-
* @param params Partial parameters for the identifier object
|
|
3433
|
-
* @returns A properly formed UserIdentifierObject
|
|
3434
|
-
*/
|
|
3435
|
-
const createUserIdentifierObject = (params) => {
|
|
3436
|
-
// Extract the wildcard or use empty string
|
|
3437
|
-
const wildcard = params.wildcard || '';
|
|
3438
|
-
// Create base object with all identifier fields set to null
|
|
3439
|
-
const identifierObject = {
|
|
3440
|
-
type: 'identifierObject',
|
|
3441
|
-
id: null,
|
|
3442
|
-
email: null,
|
|
3443
|
-
instagramAccountId: null,
|
|
3444
|
-
externalId: null,
|
|
3445
|
-
identifierEmail: null,
|
|
3446
|
-
wildcard
|
|
3447
|
-
};
|
|
3448
|
-
// Override nulls with any provided values
|
|
3449
|
-
uniqueUserIdentifierKeys.forEach(key => {
|
|
3450
|
-
if (params[key]) {
|
|
3451
|
-
identifierObject[key] = params[key];
|
|
3452
|
-
}
|
|
3453
|
-
});
|
|
3454
|
-
// If wildcard is provided but no specific fields, auto-detect the field type
|
|
3455
|
-
if (wildcard && !params.id && !params.email && !params.instagramAccountId && !params.externalId) {
|
|
3456
|
-
const detected = detectIdentifierType(wildcard);
|
|
3457
|
-
identifierObject[detected.type] = detected.value;
|
|
3458
|
-
}
|
|
3459
|
-
return identifierObject;
|
|
3977
|
+
const isUUID = (value) => {
|
|
3978
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
|
|
3460
3979
|
};
|
|
3461
3980
|
|
|
3981
|
+
const apiPublicKeyTestPrefix = 'pk_test_';
|
|
3982
|
+
const testnetPrefix = 'testnet-';
|
|
3983
|
+
const testnetShortPrefix = 'tn-';
|
|
3984
|
+
|
|
3462
3985
|
// Use this pattern:
|
|
3463
3986
|
const NativeTokenTypes = {
|
|
3464
3987
|
ERC20: 'ERC20', // default; should be used for fungible tokens eg. credit tokens; There should be only one active ERC20 token per programme
|
|
@@ -3599,7 +4122,8 @@ function isValidCampaignClaimRelation(value) {
|
|
|
3599
4122
|
*/
|
|
3600
4123
|
const VALID_REDEMPTION_RELATIONS = [
|
|
3601
4124
|
'redeemCount',
|
|
3602
|
-
'ownerBusiness'
|
|
4125
|
+
'ownerBusiness',
|
|
4126
|
+
'requiredUserFieldDefinitions'
|
|
3603
4127
|
];
|
|
3604
4128
|
/**
|
|
3605
4129
|
* Type guard to validate redemption include relation at runtime
|
|
@@ -3697,6 +4221,134 @@ function normalizeFilters(filters) {
|
|
|
3697
4221
|
};
|
|
3698
4222
|
}
|
|
3699
4223
|
|
|
4224
|
+
/**
|
|
4225
|
+
* Analytics Constants
|
|
4226
|
+
*
|
|
4227
|
+
* Shared definitions for consistent analytics across frontend and backend.
|
|
4228
|
+
*/
|
|
4229
|
+
/**
|
|
4230
|
+
* Active User Definition
|
|
4231
|
+
*
|
|
4232
|
+
* A user qualifies as "active" in a given period if they performed
|
|
4233
|
+
* any of the qualifying actions during that period.
|
|
4234
|
+
*
|
|
4235
|
+
* @example
|
|
4236
|
+
* // Display in UI
|
|
4237
|
+
* `${engagementRate}% engagement. ${ACTIVE_USER_DEFINITION.description}`
|
|
4238
|
+
*
|
|
4239
|
+
* // Result: "25.5% engagement. Active = sent transaction, claimed, or redeemed in period"
|
|
4240
|
+
*/
|
|
4241
|
+
const ACTIVE_USER_DEFINITION = {
|
|
4242
|
+
/** Actions that qualify a user as "active" */
|
|
4243
|
+
qualifyingActions: ['TRANSACTION_SEND', 'CLAIM_COMPLETED', 'REDEMPTION_COMPLETED'],
|
|
4244
|
+
/** Short description for UI display */
|
|
4245
|
+
description: 'Active = sent transaction, claimed, or redeemed in period',
|
|
4246
|
+
/** Detailed explanation for tooltips/documentation */
|
|
4247
|
+
details: {
|
|
4248
|
+
TRANSACTION_SEND: 'User initiated a transaction (spending, transferring, or purchasing)',
|
|
4249
|
+
CLAIM_COMPLETED: 'User successfully claimed a campaign reward',
|
|
4250
|
+
REDEMPTION_COMPLETED: 'User successfully redeemed an offer',
|
|
4251
|
+
},
|
|
4252
|
+
/** What does NOT count as active */
|
|
4253
|
+
excluded: [
|
|
4254
|
+
'Receiving tokens (passive)',
|
|
4255
|
+
'Failed or pending claims/redemptions',
|
|
4256
|
+
'System-generated transactions (REDEMPTION_RECEIVE)',
|
|
4257
|
+
],
|
|
4258
|
+
};
|
|
4259
|
+
/**
|
|
4260
|
+
* Engagement rate formula
|
|
4261
|
+
* engagementRate = (activeUsers / totalUsers) × 100
|
|
4262
|
+
*/
|
|
4263
|
+
const ENGAGEMENT_RATE_FORMULA = 'activeUsers / totalUsers × 100';
|
|
4264
|
+
/**
|
|
4265
|
+
* Transaction statuses that count for analytics
|
|
4266
|
+
*/
|
|
4267
|
+
const ANALYTICS_VALID_TRANSACTION_STATUSES = ['SUCCEEDED', 'BROADCASTED'];
|
|
4268
|
+
/**
|
|
4269
|
+
* Process statuses that count for analytics (claims, redemptions)
|
|
4270
|
+
*/
|
|
4271
|
+
const ANALYTICS_VALID_PROCESS_STATUS = 'COMPLETED';
|
|
4272
|
+
|
|
4273
|
+
/**
|
|
4274
|
+
* Image CDN Configuration
|
|
4275
|
+
*
|
|
4276
|
+
* Shared constants and types for Image CDN (CloudFront + Lambda@Edge)
|
|
4277
|
+
* Used by backend Lambda handler and frontend SDKs.
|
|
4278
|
+
*
|
|
4279
|
+
* @see documentation/api-docs/image-cdn.md
|
|
4280
|
+
*/
|
|
4281
|
+
// ============================================
|
|
4282
|
+
// CONSTANTS
|
|
4283
|
+
// ============================================
|
|
4284
|
+
/** Maximum output dimension when no preset specified */
|
|
4285
|
+
const IMAGE_CDN_MAX_DIMENSION = 2048;
|
|
4286
|
+
/** Default quality percentage */
|
|
4287
|
+
const IMAGE_CDN_DEFAULT_QUALITY = 80;
|
|
4288
|
+
/** Default fit mode */
|
|
4289
|
+
const IMAGE_CDN_DEFAULT_FIT = 'inside';
|
|
4290
|
+
/** Default format (auto-detect based on browser Accept header) */
|
|
4291
|
+
const IMAGE_CDN_DEFAULT_FORMAT = 'auto';
|
|
4292
|
+
/** Available presets with their dimensions */
|
|
4293
|
+
const IMAGE_CDN_PRESETS = {
|
|
4294
|
+
thumb: { width: 150, height: 150 },
|
|
4295
|
+
medium: { width: 600, height: 600 },
|
|
4296
|
+
large: { width: 1200, height: 1200 },
|
|
4297
|
+
hero: { width: 1920, height: 800 },
|
|
4298
|
+
avatar: { width: 200, height: 200 },
|
|
4299
|
+
card: { width: 400, height: 300 },
|
|
4300
|
+
};
|
|
4301
|
+
/** Supported input/output formats */
|
|
4302
|
+
const IMAGE_CDN_SUPPORTED_FORMATS = ['jpeg', 'jpg', 'png', 'webp', 'gif', 'avif'];
|
|
4303
|
+
/** Cache TTL in seconds (1 year) */
|
|
4304
|
+
const IMAGE_CDN_CACHE_TTL = 31536000;
|
|
4305
|
+
/** Maximum input file size in bytes (10MB) */
|
|
4306
|
+
const IMAGE_CDN_MAX_INPUT_SIZE = 10 * 1024 * 1024;
|
|
4307
|
+
// ============================================
|
|
4308
|
+
// HELPER FUNCTIONS
|
|
4309
|
+
// ============================================
|
|
4310
|
+
/**
|
|
4311
|
+
* Build CDN URL with options
|
|
4312
|
+
*
|
|
4313
|
+
* @example
|
|
4314
|
+
* buildImageCdnUrl('https://cdn.example.com/photo.jpg', { preset: 'thumb' })
|
|
4315
|
+
* // => 'https://cdn.example.com/photo.jpg?preset=thumb'
|
|
4316
|
+
*
|
|
4317
|
+
* buildImageCdnUrl('https://cdn.example.com/photo.jpg', { preset: 'large', q: 95 })
|
|
4318
|
+
* // => 'https://cdn.example.com/photo.jpg?preset=large&q=95'
|
|
4319
|
+
*/
|
|
4320
|
+
function buildImageCdnUrl(baseUrl, options = {}) {
|
|
4321
|
+
const params = new URLSearchParams();
|
|
4322
|
+
if (options.preset)
|
|
4323
|
+
params.set('preset', options.preset);
|
|
4324
|
+
if (options.q !== undefined)
|
|
4325
|
+
params.set('q', options.q.toString());
|
|
4326
|
+
if (options.f)
|
|
4327
|
+
params.set('f', options.f);
|
|
4328
|
+
if (options.fit)
|
|
4329
|
+
params.set('fit', options.fit);
|
|
4330
|
+
const query = params.toString();
|
|
4331
|
+
return query ? `${baseUrl}?${query}` : baseUrl;
|
|
4332
|
+
}
|
|
4333
|
+
/**
|
|
4334
|
+
* Check if a preset name is valid
|
|
4335
|
+
*/
|
|
4336
|
+
function isValidPreset(preset) {
|
|
4337
|
+
return preset in IMAGE_CDN_PRESETS;
|
|
4338
|
+
}
|
|
4339
|
+
/**
|
|
4340
|
+
* Get dimensions for a preset
|
|
4341
|
+
*/
|
|
4342
|
+
function getPresetDimensions(preset) {
|
|
4343
|
+
return IMAGE_CDN_PRESETS[preset];
|
|
4344
|
+
}
|
|
4345
|
+
/**
|
|
4346
|
+
* Check if a file extension is a supported image format
|
|
4347
|
+
*/
|
|
4348
|
+
function isSupportedImageFormat(extension) {
|
|
4349
|
+
return IMAGE_CDN_SUPPORTED_FORMATS.includes(extension.toLowerCase());
|
|
4350
|
+
}
|
|
4351
|
+
|
|
3700
4352
|
/**
|
|
3701
4353
|
* Chain Data Types
|
|
3702
4354
|
*
|
|
@@ -3831,6 +4483,12 @@ const CommonErrorCodes = {
|
|
|
3831
4483
|
FIREBASE_AUTH_ERROR: 'FIREBASE_AUTH_ERROR',
|
|
3832
4484
|
MULTIPLE_CONTEXT_SELECTION_REQUIRED: 'MULTIPLE_CONTEXT_SELECTION_REQUIRED',
|
|
3833
4485
|
ADMIN_TENANT_REQUIRED: 'ADMIN_TENANT_REQUIRED',
|
|
4486
|
+
// Magic link errors
|
|
4487
|
+
MAGIC_LINK_EMAIL_SEND_FAILED: 'MAGIC_LINK_EMAIL_SEND_FAILED',
|
|
4488
|
+
MAGIC_LINK_EMAIL_MISMATCH: 'MAGIC_LINK_EMAIL_MISMATCH',
|
|
4489
|
+
MAGIC_LINK_VERIFICATION_FAILED: 'MAGIC_LINK_VERIFICATION_FAILED',
|
|
4490
|
+
// Rate limiting (generic)
|
|
4491
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
3834
4492
|
// Generic fallbacks
|
|
3835
4493
|
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
3836
4494
|
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
@@ -10999,7 +11657,7 @@ class DefaultAuthProvider {
|
|
|
10999
11657
|
/** SDK package name */
|
|
11000
11658
|
const SDK_NAME = "@explorins/pers-sdk";
|
|
11001
11659
|
/** SDK version - injected from package.json at build time */
|
|
11002
|
-
const SDK_VERSION = "2.1
|
|
11660
|
+
const SDK_VERSION = "2.2.0-alpha.1";
|
|
11003
11661
|
/** Full SDK identifier for headers */
|
|
11004
11662
|
const SDK_USER_AGENT = `${SDK_NAME}/${SDK_VERSION}`;
|
|
11005
11663
|
|
|
@@ -12715,10 +13373,11 @@ class UserStatusManager {
|
|
|
12715
13373
|
* ```
|
|
12716
13374
|
*/
|
|
12717
13375
|
class TokenManager {
|
|
12718
|
-
constructor(apiClient) {
|
|
13376
|
+
constructor(apiClient, eventEmitter) {
|
|
12719
13377
|
this.apiClient = apiClient;
|
|
12720
13378
|
const tokenApi = new TokenApi(apiClient);
|
|
12721
13379
|
this.tokenService = new TokenService(tokenApi);
|
|
13380
|
+
this.eventEmitter = eventEmitter;
|
|
12722
13381
|
}
|
|
12723
13382
|
/**
|
|
12724
13383
|
* Get all available tokens
|
|
@@ -19519,6 +20178,429 @@ class WalletEventsManager {
|
|
|
19519
20178
|
}
|
|
19520
20179
|
}
|
|
19521
20180
|
|
|
20181
|
+
/**
|
|
20182
|
+
* Custom Field Definitions API
|
|
20183
|
+
*
|
|
20184
|
+
* Low-level API for custom field definition CRUD operations.
|
|
20185
|
+
* Used internally by CustomFieldDefinitionManager.
|
|
20186
|
+
*
|
|
20187
|
+
* @internal
|
|
20188
|
+
*/
|
|
20189
|
+
class CustomFieldApi {
|
|
20190
|
+
constructor(apiClient) {
|
|
20191
|
+
this.apiClient = apiClient;
|
|
20192
|
+
this.basePath = '/custom-field-definitions';
|
|
20193
|
+
}
|
|
20194
|
+
/**
|
|
20195
|
+
* List all custom field definitions
|
|
20196
|
+
* Backend returns non-paginated array sorted by sortOrder
|
|
20197
|
+
*/
|
|
20198
|
+
async getDefinitions(entityType) {
|
|
20199
|
+
const params = new URLSearchParams();
|
|
20200
|
+
if (entityType) {
|
|
20201
|
+
params.append('entityType', entityType);
|
|
20202
|
+
}
|
|
20203
|
+
const query = params.toString();
|
|
20204
|
+
const url = query ? `${this.basePath}?${query}` : this.basePath;
|
|
20205
|
+
return this.apiClient.get(url);
|
|
20206
|
+
}
|
|
20207
|
+
/**
|
|
20208
|
+
* Get a single custom field definition by ID
|
|
20209
|
+
*/
|
|
20210
|
+
async getDefinition(id) {
|
|
20211
|
+
return this.apiClient.get(`${this.basePath}/${id}`);
|
|
20212
|
+
}
|
|
20213
|
+
/**
|
|
20214
|
+
* Create a new custom field definition
|
|
20215
|
+
*/
|
|
20216
|
+
async createDefinition(data) {
|
|
20217
|
+
return this.apiClient.post(this.basePath, data);
|
|
20218
|
+
}
|
|
20219
|
+
/**
|
|
20220
|
+
* Update an existing custom field definition
|
|
20221
|
+
*/
|
|
20222
|
+
async updateDefinition(id, data) {
|
|
20223
|
+
return this.apiClient.put(`${this.basePath}/${id}`, data);
|
|
20224
|
+
}
|
|
20225
|
+
/**
|
|
20226
|
+
* Delete a custom field definition
|
|
20227
|
+
*/
|
|
20228
|
+
async deleteDefinition(id) {
|
|
20229
|
+
return this.apiClient.delete(`${this.basePath}/${id}`);
|
|
20230
|
+
}
|
|
20231
|
+
/**
|
|
20232
|
+
* Get usage count for a field definition
|
|
20233
|
+
*/
|
|
20234
|
+
async getUsages(id) {
|
|
20235
|
+
return this.apiClient.get(`${this.basePath}/${id}/usages`);
|
|
20236
|
+
}
|
|
20237
|
+
/**
|
|
20238
|
+
* Resolve dynamic select options for a field
|
|
20239
|
+
*/
|
|
20240
|
+
async resolveSelectOptions(id) {
|
|
20241
|
+
return this.apiClient.get(`${this.basePath}/${id}/options`);
|
|
20242
|
+
}
|
|
20243
|
+
}
|
|
20244
|
+
|
|
20245
|
+
/**
|
|
20246
|
+
* Custom Field Definition Manager - Manage tenant-specific custom fields
|
|
20247
|
+
*
|
|
20248
|
+
* Provides CRUD operations for custom field definitions that extend the built-in
|
|
20249
|
+
* user profile fields. Custom fields are defined per-tenant and can be used for
|
|
20250
|
+
* additional user data collection, redemption requirements, and form validation.
|
|
20251
|
+
*
|
|
20252
|
+
* @group Managers
|
|
20253
|
+
* @category Custom Fields
|
|
20254
|
+
*
|
|
20255
|
+
* @example Basic Operations
|
|
20256
|
+
* ```typescript
|
|
20257
|
+
* // List all user custom fields
|
|
20258
|
+
* const fields = await sdk.customFields.getDefinitions();
|
|
20259
|
+
*
|
|
20260
|
+
* // Create a new custom field
|
|
20261
|
+
* const field = await sdk.customFields.createDefinition({
|
|
20262
|
+
* key: 'employee_id',
|
|
20263
|
+
* label: 'Employee ID',
|
|
20264
|
+
* fieldType: 'text',
|
|
20265
|
+
* validation: { required: true, pattern: '^E[0-9]{5}$' }
|
|
20266
|
+
* });
|
|
20267
|
+
*
|
|
20268
|
+
* // Update a field
|
|
20269
|
+
* await sdk.customFields.updateDefinition(field.id, {
|
|
20270
|
+
* label: 'Company Employee ID'
|
|
20271
|
+
* });
|
|
20272
|
+
*
|
|
20273
|
+
* // Delete a field
|
|
20274
|
+
* await sdk.customFields.deleteDefinition(field.id);
|
|
20275
|
+
* ```
|
|
20276
|
+
*
|
|
20277
|
+
* @example Validation
|
|
20278
|
+
* ```typescript
|
|
20279
|
+
* // Validate user custom data against definitions
|
|
20280
|
+
* const definitions = await sdk.customFields.getDefinitions();
|
|
20281
|
+
* const errors = sdk.customFields.validateUserData(
|
|
20282
|
+
* { employee_id: 'INVALID' },
|
|
20283
|
+
* definitions
|
|
20284
|
+
* );
|
|
20285
|
+
* if (errors.length > 0) {
|
|
20286
|
+
* console.log('Validation errors:', errors);
|
|
20287
|
+
* }
|
|
20288
|
+
* ```
|
|
20289
|
+
*/
|
|
20290
|
+
class CustomFieldDefinitionManager {
|
|
20291
|
+
constructor(apiClient, events) {
|
|
20292
|
+
this.events = events;
|
|
20293
|
+
this.api = new CustomFieldApi(apiClient);
|
|
20294
|
+
}
|
|
20295
|
+
/**
|
|
20296
|
+
* List all custom field definitions for the tenant
|
|
20297
|
+
*
|
|
20298
|
+
* Retrieves all custom field definitions, optionally filtered by entity type.
|
|
20299
|
+
* Results are sorted by sortOrder (ascending).
|
|
20300
|
+
*
|
|
20301
|
+
* @param options - Query options including entity type filter
|
|
20302
|
+
* @returns Array of custom field definitions
|
|
20303
|
+
*
|
|
20304
|
+
* @example List All User Fields
|
|
20305
|
+
* ```typescript
|
|
20306
|
+
* const fields = await sdk.customFields.getDefinitions();
|
|
20307
|
+
* console.log(`Found ${fields.length} custom fields`);
|
|
20308
|
+
*
|
|
20309
|
+
* fields.forEach(field => {
|
|
20310
|
+
* console.log(`${field.key}: ${field.label} (${field.fieldType})`);
|
|
20311
|
+
* });
|
|
20312
|
+
* ```
|
|
20313
|
+
*
|
|
20314
|
+
* @example Filter by Entity Type
|
|
20315
|
+
* ```typescript
|
|
20316
|
+
* // Get only business custom fields
|
|
20317
|
+
* const businessFields = await sdk.customFields.getDefinitions({
|
|
20318
|
+
* entityType: 'business'
|
|
20319
|
+
* });
|
|
20320
|
+
* ```
|
|
20321
|
+
*/
|
|
20322
|
+
async getDefinitions(options) {
|
|
20323
|
+
return this.api.getDefinitions(options?.entityType);
|
|
20324
|
+
}
|
|
20325
|
+
/**
|
|
20326
|
+
* Get a single custom field definition by ID
|
|
20327
|
+
*
|
|
20328
|
+
* @param id - The UUID of the custom field definition
|
|
20329
|
+
* @returns The custom field definition
|
|
20330
|
+
* @throws {PersApiError} When definition not found (404)
|
|
20331
|
+
*
|
|
20332
|
+
* @example
|
|
20333
|
+
* ```typescript
|
|
20334
|
+
* try {
|
|
20335
|
+
* const field = await sdk.customFields.getDefinition('uuid-here');
|
|
20336
|
+
* console.log('Field:', field.label);
|
|
20337
|
+
* } catch (error) {
|
|
20338
|
+
* if (error.statusCode === 404) {
|
|
20339
|
+
* console.log('Field not found');
|
|
20340
|
+
* }
|
|
20341
|
+
* }
|
|
20342
|
+
* ```
|
|
20343
|
+
*/
|
|
20344
|
+
async getDefinition(id) {
|
|
20345
|
+
return this.api.getDefinition(id);
|
|
20346
|
+
}
|
|
20347
|
+
/**
|
|
20348
|
+
* Create a new custom field definition
|
|
20349
|
+
*
|
|
20350
|
+
* Creates a new custom field for the tenant. The field key must be unique
|
|
20351
|
+
* within the tenant and entity type combination.
|
|
20352
|
+
*
|
|
20353
|
+
* @param data - The field definition data
|
|
20354
|
+
* @returns The created custom field definition
|
|
20355
|
+
* @throws {PersApiError} When key already exists (409) or validation fails (400)
|
|
20356
|
+
*
|
|
20357
|
+
* @example Text Field with Pattern
|
|
20358
|
+
* ```typescript
|
|
20359
|
+
* const employeeIdField = await sdk.customFields.createDefinition({
|
|
20360
|
+
* key: 'employee_id',
|
|
20361
|
+
* label: 'Employee ID',
|
|
20362
|
+
* description: 'Your company employee ID (E + 5 digits)',
|
|
20363
|
+
* fieldType: 'text',
|
|
20364
|
+
* validation: {
|
|
20365
|
+
* required: true,
|
|
20366
|
+
* pattern: '^E[0-9]{5}$',
|
|
20367
|
+
* patternMessage: 'Must be E followed by 5 digits'
|
|
20368
|
+
* },
|
|
20369
|
+
* sortOrder: 1
|
|
20370
|
+
* });
|
|
20371
|
+
* ```
|
|
20372
|
+
*
|
|
20373
|
+
* @example Date Field with Comparison
|
|
20374
|
+
* ```typescript
|
|
20375
|
+
* const checkOutField = await sdk.customFields.createDefinition({
|
|
20376
|
+
* key: 'check_out',
|
|
20377
|
+
* label: 'Check-out Date',
|
|
20378
|
+
* fieldType: 'date',
|
|
20379
|
+
* validation: {
|
|
20380
|
+
* required: true,
|
|
20381
|
+
* comparisons: [
|
|
20382
|
+
* { field: 'check_in', operator: '>', message: 'Must be after check-in' }
|
|
20383
|
+
* ]
|
|
20384
|
+
* },
|
|
20385
|
+
* sortOrder: 2
|
|
20386
|
+
* });
|
|
20387
|
+
* ```
|
|
20388
|
+
*
|
|
20389
|
+
* @example Select Field with Static Options
|
|
20390
|
+
* ```typescript
|
|
20391
|
+
* const departmentField = await sdk.customFields.createDefinition({
|
|
20392
|
+
* key: 'department',
|
|
20393
|
+
* label: 'Department',
|
|
20394
|
+
* fieldType: 'select',
|
|
20395
|
+
* selectOptions: [
|
|
20396
|
+
* { value: 'engineering', label: 'Engineering' },
|
|
20397
|
+
* { value: 'sales', label: 'Sales' },
|
|
20398
|
+
* { value: 'hr', label: 'Human Resources' }
|
|
20399
|
+
* ],
|
|
20400
|
+
* validation: { required: true }
|
|
20401
|
+
* });
|
|
20402
|
+
* ```
|
|
20403
|
+
*
|
|
20404
|
+
* @example Select Field with Dynamic Options
|
|
20405
|
+
* ```typescript
|
|
20406
|
+
* const hotelField = await sdk.customFields.createDefinition({
|
|
20407
|
+
* key: 'preferred_hotel',
|
|
20408
|
+
* label: 'Preferred Hotel',
|
|
20409
|
+
* fieldType: 'select',
|
|
20410
|
+
* selectOptionsSource: {
|
|
20411
|
+
* entity: 'business',
|
|
20412
|
+
* valueField: 'id',
|
|
20413
|
+
* labelField: 'name',
|
|
20414
|
+
* filter: { tags: ['hotel'], isActive: true }
|
|
20415
|
+
* },
|
|
20416
|
+
* validation: { required: true }
|
|
20417
|
+
* });
|
|
20418
|
+
* ```
|
|
20419
|
+
*/
|
|
20420
|
+
async createDefinition(data) {
|
|
20421
|
+
const result = await this.api.createDefinition(data);
|
|
20422
|
+
// Using 'user' domain until pers-shared adds 'customField' domain
|
|
20423
|
+
this.events?.emitSuccess({
|
|
20424
|
+
domain: 'user',
|
|
20425
|
+
type: 'definition_created',
|
|
20426
|
+
userMessage: `Custom field "${result.label}" created`,
|
|
20427
|
+
details: { id: result.id, key: result.key }
|
|
20428
|
+
});
|
|
20429
|
+
return result;
|
|
20430
|
+
}
|
|
20431
|
+
/**
|
|
20432
|
+
* Update an existing custom field definition
|
|
20433
|
+
*
|
|
20434
|
+
* Updates a custom field definition. Note that key and entityType cannot
|
|
20435
|
+
* be changed after creation.
|
|
20436
|
+
*
|
|
20437
|
+
* @param id - The UUID of the custom field definition to update
|
|
20438
|
+
* @param data - The fields to update (partial update supported)
|
|
20439
|
+
* @returns The updated custom field definition
|
|
20440
|
+
* @throws {PersApiError} When definition not found (404) or validation fails (400)
|
|
20441
|
+
*
|
|
20442
|
+
* @example Update Label and Validation
|
|
20443
|
+
* ```typescript
|
|
20444
|
+
* const updated = await sdk.customFields.updateDefinition('uuid-here', {
|
|
20445
|
+
* label: 'Employee ID (Required)',
|
|
20446
|
+
* validation: {
|
|
20447
|
+
* required: true,
|
|
20448
|
+
* minLength: 6,
|
|
20449
|
+
* maxLength: 6
|
|
20450
|
+
* }
|
|
20451
|
+
* });
|
|
20452
|
+
* ```
|
|
20453
|
+
*
|
|
20454
|
+
* @example Update Sort Order
|
|
20455
|
+
* ```typescript
|
|
20456
|
+
* await sdk.customFields.updateDefinition('uuid-here', {
|
|
20457
|
+
* sortOrder: 5
|
|
20458
|
+
* });
|
|
20459
|
+
* ```
|
|
20460
|
+
*/
|
|
20461
|
+
async updateDefinition(id, data) {
|
|
20462
|
+
const result = await this.api.updateDefinition(id, data);
|
|
20463
|
+
// Using 'user' domain until pers-shared adds 'customField' domain
|
|
20464
|
+
this.events?.emitSuccess({
|
|
20465
|
+
domain: 'user',
|
|
20466
|
+
type: 'definition_updated',
|
|
20467
|
+
userMessage: `Custom field "${result.label}" updated`,
|
|
20468
|
+
details: { id: result.id, key: result.key }
|
|
20469
|
+
});
|
|
20470
|
+
return result;
|
|
20471
|
+
}
|
|
20472
|
+
/**
|
|
20473
|
+
* Delete a custom field definition (soft delete)
|
|
20474
|
+
*
|
|
20475
|
+
* Soft deletes a custom field definition. Existing user data in customData
|
|
20476
|
+
* is preserved, but the field will no longer appear in forms or validation.
|
|
20477
|
+
*
|
|
20478
|
+
* ⚠️ Consider the impact on existing user data before deleting.
|
|
20479
|
+
*
|
|
20480
|
+
* @param id - The UUID of the custom field definition to delete
|
|
20481
|
+
* @throws {PersApiError} When definition not found (404)
|
|
20482
|
+
*
|
|
20483
|
+
* @example
|
|
20484
|
+
* ```typescript
|
|
20485
|
+
* try {
|
|
20486
|
+
* await sdk.customFields.deleteDefinition('uuid-here');
|
|
20487
|
+
* console.log('Field deleted');
|
|
20488
|
+
* } catch (error) {
|
|
20489
|
+
* console.log('Failed to delete:', error.message);
|
|
20490
|
+
* }
|
|
20491
|
+
* ```
|
|
20492
|
+
*/
|
|
20493
|
+
async deleteDefinition(id) {
|
|
20494
|
+
await this.api.deleteDefinition(id);
|
|
20495
|
+
// Using 'user' domain until pers-shared adds 'customField' domain
|
|
20496
|
+
this.events?.emitSuccess({
|
|
20497
|
+
domain: 'user',
|
|
20498
|
+
type: 'definition_deleted',
|
|
20499
|
+
userMessage: 'Custom field deleted',
|
|
20500
|
+
details: { id }
|
|
20501
|
+
});
|
|
20502
|
+
}
|
|
20503
|
+
/**
|
|
20504
|
+
* Resolve dynamic select options for a field
|
|
20505
|
+
*
|
|
20506
|
+
* For fields with selectOptionsSource (dynamic options from entities),
|
|
20507
|
+
* this resolves the actual options by querying the source entity.
|
|
20508
|
+
*
|
|
20509
|
+
* @param id - The UUID of the custom field definition
|
|
20510
|
+
* @returns Array of select options
|
|
20511
|
+
*
|
|
20512
|
+
* @example
|
|
20513
|
+
* ```typescript
|
|
20514
|
+
* const field = await sdk.customFields.getDefinition('hotel-field-id');
|
|
20515
|
+
*
|
|
20516
|
+
* if (field.selectOptionsSource) {
|
|
20517
|
+
* // Options come from entity - need to resolve
|
|
20518
|
+
* const options = await sdk.customFields.resolveSelectOptions(field.id);
|
|
20519
|
+
* console.log('Available hotels:', options);
|
|
20520
|
+
* } else {
|
|
20521
|
+
* // Static options
|
|
20522
|
+
* console.log('Options:', field.selectOptions);
|
|
20523
|
+
* }
|
|
20524
|
+
* ```
|
|
20525
|
+
*/
|
|
20526
|
+
async resolveSelectOptions(id) {
|
|
20527
|
+
const response = await this.api.resolveSelectOptions(id);
|
|
20528
|
+
return response.options;
|
|
20529
|
+
}
|
|
20530
|
+
/**
|
|
20531
|
+
* Validate user custom data against field definitions
|
|
20532
|
+
*
|
|
20533
|
+
* Client-side validation using the same rules as the backend. Use this
|
|
20534
|
+
* to validate form data before submission.
|
|
20535
|
+
*
|
|
20536
|
+
* @param data - User's custom data (key-value pairs)
|
|
20537
|
+
* @param definitions - Custom field definitions to validate against
|
|
20538
|
+
* @returns Array of validation errors (empty if valid)
|
|
20539
|
+
*
|
|
20540
|
+
* @example Form Validation
|
|
20541
|
+
* ```typescript
|
|
20542
|
+
* const definitions = await sdk.customFields.getDefinitions();
|
|
20543
|
+
* const formData = {
|
|
20544
|
+
* employee_id: 'E123', // Missing a digit
|
|
20545
|
+
* department: 'engineering'
|
|
20546
|
+
* };
|
|
20547
|
+
*
|
|
20548
|
+
* const errors = sdk.customFields.validateUserData(formData, definitions);
|
|
20549
|
+
* if (errors.length > 0) {
|
|
20550
|
+
* errors.forEach(err => {
|
|
20551
|
+
* console.log(`${err.field}: ${err.message}`);
|
|
20552
|
+
* });
|
|
20553
|
+
* return; // Don't submit
|
|
20554
|
+
* }
|
|
20555
|
+
*
|
|
20556
|
+
* // No errors, safe to submit
|
|
20557
|
+
* await sdk.users.updateCurrentUser({ customData: formData });
|
|
20558
|
+
* ```
|
|
20559
|
+
*/
|
|
20560
|
+
validateUserData(data, definitions) {
|
|
20561
|
+
// Convert CustomFieldDefinitionDTO to FieldDefinition for validation
|
|
20562
|
+
const fieldDefinitions = definitions.map(def => ({
|
|
20563
|
+
key: def.key,
|
|
20564
|
+
fieldType: def.fieldType,
|
|
20565
|
+
label: def.label,
|
|
20566
|
+
validation: def.validation ?? undefined,
|
|
20567
|
+
}));
|
|
20568
|
+
return validateAllFields(data, fieldDefinitions);
|
|
20569
|
+
}
|
|
20570
|
+
/**
|
|
20571
|
+
* Validate a single field value
|
|
20572
|
+
*
|
|
20573
|
+
* Validates a single value against a field's rules. Useful for
|
|
20574
|
+
* real-time validation as the user types.
|
|
20575
|
+
*
|
|
20576
|
+
* @param value - The value to validate
|
|
20577
|
+
* @param definition - The field definition
|
|
20578
|
+
* @returns Validation error or null if valid
|
|
20579
|
+
*
|
|
20580
|
+
* @example Real-time Validation
|
|
20581
|
+
* ```typescript
|
|
20582
|
+
* const employeeIdField = definitions.find(d => d.key === 'employee_id');
|
|
20583
|
+
*
|
|
20584
|
+
* const handleBlur = (value: string) => {
|
|
20585
|
+
* const error = sdk.customFields.validateFieldValue(value, employeeIdField);
|
|
20586
|
+
* if (error) {
|
|
20587
|
+
* setFieldError('employee_id', error.message);
|
|
20588
|
+
* } else {
|
|
20589
|
+
* clearFieldError('employee_id');
|
|
20590
|
+
* }
|
|
20591
|
+
* };
|
|
20592
|
+
* ```
|
|
20593
|
+
*/
|
|
20594
|
+
validateFieldValue(value, definition) {
|
|
20595
|
+
const error = validateFieldValue(value, definition.validation ?? null, definition.fieldType);
|
|
20596
|
+
if (error) {
|
|
20597
|
+
// Add field key to error
|
|
20598
|
+
return { ...error, field: definition.key };
|
|
20599
|
+
}
|
|
20600
|
+
return null;
|
|
20601
|
+
}
|
|
20602
|
+
}
|
|
20603
|
+
|
|
19522
20604
|
/**
|
|
19523
20605
|
* @fileoverview PERS SDK - Platform-agnostic TypeScript SDK with High-Level Managers
|
|
19524
20606
|
*
|
|
@@ -19962,7 +21044,7 @@ class PersSDK {
|
|
|
19962
21044
|
*/
|
|
19963
21045
|
get tokens() {
|
|
19964
21046
|
if (!this._tokens) {
|
|
19965
|
-
this._tokens = new TokenManager(this.apiClient);
|
|
21047
|
+
this._tokens = new TokenManager(this.apiClient, this._events);
|
|
19966
21048
|
}
|
|
19967
21049
|
return this._tokens;
|
|
19968
21050
|
}
|
|
@@ -20207,6 +21289,37 @@ class PersSDK {
|
|
|
20207
21289
|
}
|
|
20208
21290
|
return this._webhooks;
|
|
20209
21291
|
}
|
|
21292
|
+
/**
|
|
21293
|
+
* Custom Field Definition Manager - Manage tenant-specific custom fields
|
|
21294
|
+
*
|
|
21295
|
+
* Provides CRUD operations for custom field definitions that extend the built-in
|
|
21296
|
+
* user profile fields. Custom fields are tenant-specific and support validation.
|
|
21297
|
+
*
|
|
21298
|
+
* @returns CustomFieldDefinitionManager instance
|
|
21299
|
+
*
|
|
21300
|
+
* @example Basic Operations
|
|
21301
|
+
* ```typescript
|
|
21302
|
+
* // List all custom fields
|
|
21303
|
+
* const fields = await sdk.customFields.getDefinitions();
|
|
21304
|
+
*
|
|
21305
|
+
* // Create a new field
|
|
21306
|
+
* const field = await sdk.customFields.createDefinition({
|
|
21307
|
+
* key: 'employee_id',
|
|
21308
|
+
* label: 'Employee ID',
|
|
21309
|
+
* fieldType: 'text',
|
|
21310
|
+
* validation: { required: true }
|
|
21311
|
+
* });
|
|
21312
|
+
*
|
|
21313
|
+
* // Validate user data
|
|
21314
|
+
* const errors = sdk.customFields.validateUserData(formData, fields.data);
|
|
21315
|
+
* ```
|
|
21316
|
+
*/
|
|
21317
|
+
get customFields() {
|
|
21318
|
+
if (!this._customFields) {
|
|
21319
|
+
this._customFields = new CustomFieldDefinitionManager(this.apiClient, this._events);
|
|
21320
|
+
}
|
|
21321
|
+
return this._customFields;
|
|
21322
|
+
}
|
|
20210
21323
|
/**
|
|
20211
21324
|
* Wallet Events Manager - Real-time blockchain events for user's wallets
|
|
20212
21325
|
*
|
|
@@ -40894,6 +42007,122 @@ function spelunkMessage(value) {
|
|
|
40894
42007
|
return result;
|
|
40895
42008
|
}
|
|
40896
42009
|
|
|
42010
|
+
/**
|
|
42011
|
+
* Get multiple ERC-1155 token balances using native balanceOfBatch function.
|
|
42012
|
+
* This is the most efficient method - it's built into the ERC-1155 standard.
|
|
42013
|
+
*
|
|
42014
|
+
* @param contract - ERC-1155 contract instance
|
|
42015
|
+
* @param accountAddress - Account to check balances for
|
|
42016
|
+
* @param tokenIds - Array of tokenIds to check
|
|
42017
|
+
* @returns Array of BatchBalanceResult with balance for each tokenId
|
|
42018
|
+
*
|
|
42019
|
+
* @example
|
|
42020
|
+
* ```typescript
|
|
42021
|
+
* const results = await getERC1155BalanceOfBatch(
|
|
42022
|
+
* contract,
|
|
42023
|
+
* '0xabc...',
|
|
42024
|
+
* ['1', '2', '3', '4', '5']
|
|
42025
|
+
* );
|
|
42026
|
+
* // 1 RPC call returns all balances
|
|
42027
|
+
* ```
|
|
42028
|
+
*/
|
|
42029
|
+
async function getERC1155BalanceOfBatch(contract, accountAddress, tokenIds) {
|
|
42030
|
+
if (tokenIds.length === 0) {
|
|
42031
|
+
return [];
|
|
42032
|
+
}
|
|
42033
|
+
try {
|
|
42034
|
+
// ERC-1155 balanceOfBatch requires arrays of same length
|
|
42035
|
+
// accounts[i] corresponds to ids[i]
|
|
42036
|
+
const accounts = tokenIds.map(() => accountAddress);
|
|
42037
|
+
// Call native balanceOfBatch - 1 RPC call for ALL tokenIds
|
|
42038
|
+
const balances = await contract.balanceOfBatch(accounts, tokenIds);
|
|
42039
|
+
return tokenIds.map((tokenId, index) => ({
|
|
42040
|
+
tokenId,
|
|
42041
|
+
balance: balances[index],
|
|
42042
|
+
success: true
|
|
42043
|
+
}));
|
|
42044
|
+
}
|
|
42045
|
+
catch (error) {
|
|
42046
|
+
console.warn('[ERC1155] balanceOfBatch failed:', error);
|
|
42047
|
+
return tokenIds.map(tokenId => ({
|
|
42048
|
+
tokenId,
|
|
42049
|
+
balance: 0n,
|
|
42050
|
+
success: false,
|
|
42051
|
+
error: `balanceOfBatch failed: ${error}`
|
|
42052
|
+
}));
|
|
42053
|
+
}
|
|
42054
|
+
}
|
|
42055
|
+
/**
|
|
42056
|
+
* Get multiple token balances for ERC-1155 contracts.
|
|
42057
|
+
* Uses the native balanceOfBatch function (1 RPC call for ALL tokens).
|
|
42058
|
+
* Falls back to individual calls only if balanceOfBatch fails.
|
|
42059
|
+
*
|
|
42060
|
+
* @param provider - Ethers provider
|
|
42061
|
+
* @param contractAddress - Token contract address
|
|
42062
|
+
* @param contractInterface - Contract interface (must include balanceOfBatch for ERC-1155)
|
|
42063
|
+
* @param accountAddress - Account to check balances for
|
|
42064
|
+
* @param tokenIds - Array of tokenIds to check
|
|
42065
|
+
* @param isERC1155 - Whether the contract is ERC-1155 (default: true)
|
|
42066
|
+
* @returns Array of BatchBalanceResult
|
|
42067
|
+
*/
|
|
42068
|
+
async function getBatchBalancesWithFallback(provider, contractAddress, contractInterface, accountAddress, tokenIds, isERC1155 = true) {
|
|
42069
|
+
if (tokenIds.length === 0)
|
|
42070
|
+
return [];
|
|
42071
|
+
const contract = new Contract(contractAddress, contractInterface, provider);
|
|
42072
|
+
// For single token, direct call is more efficient
|
|
42073
|
+
if (tokenIds.length === 1) {
|
|
42074
|
+
try {
|
|
42075
|
+
const balance = isERC1155
|
|
42076
|
+
? await contract.balanceOf(accountAddress, tokenIds[0])
|
|
42077
|
+
: await contract.balanceOf(accountAddress);
|
|
42078
|
+
return [{
|
|
42079
|
+
tokenId: tokenIds[0],
|
|
42080
|
+
balance: balance,
|
|
42081
|
+
success: true
|
|
42082
|
+
}];
|
|
42083
|
+
}
|
|
42084
|
+
catch (error) {
|
|
42085
|
+
return [{
|
|
42086
|
+
tokenId: tokenIds[0],
|
|
42087
|
+
balance: 0n,
|
|
42088
|
+
success: false,
|
|
42089
|
+
error: `${error}`
|
|
42090
|
+
}];
|
|
42091
|
+
}
|
|
42092
|
+
}
|
|
42093
|
+
// For ERC-1155: Use native balanceOfBatch (1 RPC call for ALL tokens)
|
|
42094
|
+
if (isERC1155) {
|
|
42095
|
+
const results = await getERC1155BalanceOfBatch(contract, accountAddress, tokenIds);
|
|
42096
|
+
// If native call succeeded, return results
|
|
42097
|
+
if (results.some(r => r.success)) {
|
|
42098
|
+
return results;
|
|
42099
|
+
}
|
|
42100
|
+
// Otherwise fall through to individual calls
|
|
42101
|
+
}
|
|
42102
|
+
// Fallback: individual calls in parallel (for non-ERC1155 or if balanceOfBatch failed)
|
|
42103
|
+
const promises = tokenIds.map(async (tokenId) => {
|
|
42104
|
+
try {
|
|
42105
|
+
const balance = isERC1155
|
|
42106
|
+
? await contract.balanceOf(accountAddress, tokenId)
|
|
42107
|
+
: await contract.balanceOf(accountAddress);
|
|
42108
|
+
return {
|
|
42109
|
+
tokenId,
|
|
42110
|
+
balance: balance,
|
|
42111
|
+
success: true
|
|
42112
|
+
};
|
|
42113
|
+
}
|
|
42114
|
+
catch (error) {
|
|
42115
|
+
return {
|
|
42116
|
+
tokenId,
|
|
42117
|
+
balance: 0n,
|
|
42118
|
+
success: false,
|
|
42119
|
+
error: `${error}`
|
|
42120
|
+
};
|
|
42121
|
+
}
|
|
42122
|
+
});
|
|
42123
|
+
return Promise.all(promises);
|
|
42124
|
+
}
|
|
42125
|
+
|
|
40897
42126
|
const PERS_STATUS_TOKEN_ETHERS_METHOD_BUILDER = (contract, methodName) => {
|
|
40898
42127
|
try {
|
|
40899
42128
|
// Use unknown first, then cast to the expected type
|
|
@@ -41505,7 +42734,10 @@ class TokenDomainService {
|
|
|
41505
42734
|
try {
|
|
41506
42735
|
const abi = convertAbiToInterface(params.abi);
|
|
41507
42736
|
const analysis = this.contractService.analyzeContract(abi);
|
|
41508
|
-
|
|
42737
|
+
// ERC-1155 balanceOfBatch can handle large batches efficiently (1 RPC call)
|
|
42738
|
+
// Default to 100 for ERC-1155, 20 for ERC-721 (which has no batch function)
|
|
42739
|
+
const defaultBatchSize = analysis.isERC1155 ? 100 : 20;
|
|
42740
|
+
const batchSize = Math.min(params.batchSize || defaultBatchSize, analysis.isERC1155 ? 200 : 50);
|
|
41509
42741
|
// ERC-1155: Requires specific token IDs
|
|
41510
42742
|
if (analysis.isERC1155) {
|
|
41511
42743
|
if (!params.tokenIds?.length) {
|
|
@@ -41559,31 +42791,44 @@ class TokenDomainService {
|
|
|
41559
42791
|
async processTokenBatch(params, tokenIds, batchSize, tokenStandard) {
|
|
41560
42792
|
const tokens = [];
|
|
41561
42793
|
const errors = [];
|
|
42794
|
+
const abi = convertAbiToInterface(params.abi);
|
|
41562
42795
|
// Process in batches
|
|
41563
42796
|
for (let i = 0; i < tokenIds.length; i += batchSize) {
|
|
41564
42797
|
const batch = tokenIds.slice(i, i + batchSize);
|
|
41565
|
-
|
|
41566
|
-
|
|
41567
|
-
|
|
41568
|
-
|
|
41569
|
-
|
|
41570
|
-
|
|
41571
|
-
|
|
41572
|
-
|
|
41573
|
-
|
|
41574
|
-
|
|
41575
|
-
|
|
41576
|
-
|
|
41577
|
-
|
|
41578
|
-
|
|
41579
|
-
|
|
41580
|
-
|
|
41581
|
-
|
|
41582
|
-
|
|
41583
|
-
|
|
41584
|
-
|
|
41585
|
-
|
|
41586
|
-
|
|
42798
|
+
let validTokens;
|
|
42799
|
+
if (tokenStandard === 'ERC-721') {
|
|
42800
|
+
// For ERC-721: Skip balance check, we know ownership from enumeration or explicit tokenIds
|
|
42801
|
+
validTokens = batch.map(tokenId => ({
|
|
42802
|
+
tokenId,
|
|
42803
|
+
balance: 1,
|
|
42804
|
+
hasBalance: true,
|
|
42805
|
+
metadata: null
|
|
42806
|
+
}));
|
|
42807
|
+
}
|
|
42808
|
+
else {
|
|
42809
|
+
// For ERC-1155: Use native balanceOfBatch (1 RPC call for entire batch)
|
|
42810
|
+
const batchResults = await this.web3Api.getBatchTokenBalances({
|
|
42811
|
+
accountAddress: params.accountAddress,
|
|
42812
|
+
contractAddress: params.contractAddress,
|
|
42813
|
+
abi,
|
|
42814
|
+
tokenIds: batch,
|
|
42815
|
+
chainId: params.chainId,
|
|
42816
|
+
isERC1155: true
|
|
42817
|
+
});
|
|
42818
|
+
// Convert batch results to TokenBalance format
|
|
42819
|
+
validTokens = batchResults
|
|
42820
|
+
.filter(r => r.success && r.balance > 0n)
|
|
42821
|
+
.map(r => ({
|
|
42822
|
+
tokenId: r.tokenId,
|
|
42823
|
+
balance: Number(r.balance),
|
|
42824
|
+
hasBalance: true,
|
|
42825
|
+
metadata: null
|
|
42826
|
+
}));
|
|
42827
|
+
// Track errors
|
|
42828
|
+
batchResults.filter(r => !r.success).forEach(r => {
|
|
42829
|
+
errors.push(`${r.tokenId}: ${r.error}`);
|
|
42830
|
+
});
|
|
42831
|
+
}
|
|
41587
42832
|
// Step 2: Get metadata for tokens that have balance
|
|
41588
42833
|
if (validTokens.length > 0) {
|
|
41589
42834
|
const metadataPromises = validTokens.map(token => this.getTokenMetadata({
|
|
@@ -41707,6 +42952,627 @@ class ContractDomainService {
|
|
|
41707
42952
|
}
|
|
41708
42953
|
}
|
|
41709
42954
|
|
|
42955
|
+
/**
|
|
42956
|
+
* TokenCollectionManager - Auto-refresh token collections on wallet events
|
|
42957
|
+
*
|
|
42958
|
+
* This manager provides:
|
|
42959
|
+
* 1. Cached token collection storage with automatic refresh
|
|
42960
|
+
* 2. Auto-subscription to wallet events (transfers, mints, burns)
|
|
42961
|
+
* 3. Targeted invalidation based on contract address
|
|
42962
|
+
* 4. Subscriber pattern for reactive updates
|
|
42963
|
+
*
|
|
42964
|
+
* @example
|
|
42965
|
+
* ```typescript
|
|
42966
|
+
* // Get from SDK
|
|
42967
|
+
* const manager = sdk.tokens.collectionManager;
|
|
42968
|
+
*
|
|
42969
|
+
* // Subscribe to collection updates
|
|
42970
|
+
* const unsubscribe = manager.subscribe((collection, contractAddress) => {
|
|
42971
|
+
* console.log(`Collection updated for ${contractAddress}`);
|
|
42972
|
+
* updateUI(collection);
|
|
42973
|
+
* });
|
|
42974
|
+
*
|
|
42975
|
+
* // Fetch collection (auto-cached, auto-refreshed on wallet events)
|
|
42976
|
+
* const collection = await manager.getCollection({
|
|
42977
|
+
* accountAddress: '0x...',
|
|
42978
|
+
* contractAddress: '0x...',
|
|
42979
|
+
* abi: contractAbi,
|
|
42980
|
+
* chainId: 39123,
|
|
42981
|
+
* tokenIds: ['1', '2', '3']
|
|
42982
|
+
* });
|
|
42983
|
+
*
|
|
42984
|
+
* // Cleanup
|
|
42985
|
+
* unsubscribe();
|
|
42986
|
+
* ```
|
|
42987
|
+
*/
|
|
42988
|
+
/**
|
|
42989
|
+
* Manages token collections with automatic refresh on wallet events
|
|
42990
|
+
*/
|
|
42991
|
+
class TokenCollectionManager {
|
|
42992
|
+
constructor(tokenService, eventEmitter, config) {
|
|
42993
|
+
this.tokenService = tokenService;
|
|
42994
|
+
this.eventEmitter = eventEmitter;
|
|
42995
|
+
this.collections = new Map();
|
|
42996
|
+
this.handlers = new Set();
|
|
42997
|
+
this.pendingRefresh = new Map();
|
|
42998
|
+
this.config = {
|
|
42999
|
+
autoRefreshOnWalletEvents: config?.autoRefreshOnWalletEvents ?? true,
|
|
43000
|
+
eventDebounceMs: config?.eventDebounceMs ?? 500,
|
|
43001
|
+
debug: config?.debug ?? false
|
|
43002
|
+
};
|
|
43003
|
+
if (this.config.autoRefreshOnWalletEvents && eventEmitter) {
|
|
43004
|
+
this.setupEventSubscription();
|
|
43005
|
+
}
|
|
43006
|
+
}
|
|
43007
|
+
/**
|
|
43008
|
+
* Subscribe to wallet events for auto-refresh
|
|
43009
|
+
*/
|
|
43010
|
+
setupEventSubscription() {
|
|
43011
|
+
if (!this.eventEmitter) {
|
|
43012
|
+
return;
|
|
43013
|
+
}
|
|
43014
|
+
this.eventUnsubscribe = this.eventEmitter.subscribe((event) => {
|
|
43015
|
+
const contractAddress = event.details?.contractAddress;
|
|
43016
|
+
this.handleWalletEvent(contractAddress, event.type);
|
|
43017
|
+
}, { domains: ['wallet'] });
|
|
43018
|
+
}
|
|
43019
|
+
/**
|
|
43020
|
+
* Get a token collection (cached, auto-refreshed on wallet events)
|
|
43021
|
+
*
|
|
43022
|
+
* @param params - Collection parameters
|
|
43023
|
+
* @returns Promise resolving to the token collection
|
|
43024
|
+
*/
|
|
43025
|
+
async getCollection(params) {
|
|
43026
|
+
const key = this.getCacheKey(params);
|
|
43027
|
+
const cached = this.collections.get(key);
|
|
43028
|
+
// Return cached if available and not stale
|
|
43029
|
+
if (cached?.lastCollection) {
|
|
43030
|
+
this.log(`Cache HIT for ${key}`);
|
|
43031
|
+
return cached.lastCollection;
|
|
43032
|
+
}
|
|
43033
|
+
this.log(`Cache MISS for ${key}, fetching...`);
|
|
43034
|
+
return this.fetchAndCache(params);
|
|
43035
|
+
}
|
|
43036
|
+
/**
|
|
43037
|
+
* Force refresh a specific collection
|
|
43038
|
+
*
|
|
43039
|
+
* @param contractAddress - Contract address to refresh
|
|
43040
|
+
* @param accountAddress - Account address to refresh
|
|
43041
|
+
* @returns Promise that resolves when refresh is complete
|
|
43042
|
+
*/
|
|
43043
|
+
async refreshCollection(contractAddress, accountAddress) {
|
|
43044
|
+
const toRefresh = [];
|
|
43045
|
+
for (const [key, entry] of this.collections) {
|
|
43046
|
+
if (entry.params.contractAddress === contractAddress) {
|
|
43047
|
+
if (!accountAddress || entry.params.accountAddress === accountAddress) {
|
|
43048
|
+
toRefresh.push(key);
|
|
43049
|
+
}
|
|
43050
|
+
}
|
|
43051
|
+
}
|
|
43052
|
+
await Promise.all(toRefresh.map(key => {
|
|
43053
|
+
const entry = this.collections.get(key);
|
|
43054
|
+
if (entry) {
|
|
43055
|
+
return this.refreshEntry(key, entry, 'refresh');
|
|
43056
|
+
}
|
|
43057
|
+
}));
|
|
43058
|
+
}
|
|
43059
|
+
/**
|
|
43060
|
+
* Subscribe to collection change events
|
|
43061
|
+
*
|
|
43062
|
+
* @param handler - Callback when any tracked collection changes
|
|
43063
|
+
* @returns Unsubscribe function
|
|
43064
|
+
*/
|
|
43065
|
+
subscribe(handler) {
|
|
43066
|
+
this.handlers.add(handler);
|
|
43067
|
+
return () => this.handlers.delete(handler);
|
|
43068
|
+
}
|
|
43069
|
+
/**
|
|
43070
|
+
* Get all currently tracked collections
|
|
43071
|
+
*/
|
|
43072
|
+
getTrackedCollections() {
|
|
43073
|
+
return Array.from(this.collections.entries()).map(([_, entry]) => ({
|
|
43074
|
+
contractAddress: entry.params.contractAddress,
|
|
43075
|
+
accountAddress: entry.params.accountAddress,
|
|
43076
|
+
collection: entry.lastCollection
|
|
43077
|
+
}));
|
|
43078
|
+
}
|
|
43079
|
+
/**
|
|
43080
|
+
* Check if a collection is currently being tracked
|
|
43081
|
+
*/
|
|
43082
|
+
isTracking(contractAddress, accountAddress) {
|
|
43083
|
+
const key = `${contractAddress}:${accountAddress}`;
|
|
43084
|
+
return this.collections.has(key);
|
|
43085
|
+
}
|
|
43086
|
+
/**
|
|
43087
|
+
* Clear tracking for a specific collection
|
|
43088
|
+
*/
|
|
43089
|
+
untrack(contractAddress, accountAddress) {
|
|
43090
|
+
if (accountAddress) {
|
|
43091
|
+
const key = `${contractAddress}:${accountAddress}`;
|
|
43092
|
+
this.collections.delete(key);
|
|
43093
|
+
this.log(`Untracked ${key}`);
|
|
43094
|
+
}
|
|
43095
|
+
else {
|
|
43096
|
+
// Untrack all for this contract
|
|
43097
|
+
for (const key of this.collections.keys()) {
|
|
43098
|
+
if (key.startsWith(`${contractAddress}:`)) {
|
|
43099
|
+
this.collections.delete(key);
|
|
43100
|
+
}
|
|
43101
|
+
}
|
|
43102
|
+
this.log(`Untracked all collections for ${contractAddress}`);
|
|
43103
|
+
}
|
|
43104
|
+
}
|
|
43105
|
+
/**
|
|
43106
|
+
* Clear all tracked collections
|
|
43107
|
+
*/
|
|
43108
|
+
clearAll() {
|
|
43109
|
+
this.collections.clear();
|
|
43110
|
+
this.log('Cleared all tracked collections');
|
|
43111
|
+
}
|
|
43112
|
+
/**
|
|
43113
|
+
* Handle wallet events (with debouncing)
|
|
43114
|
+
*/
|
|
43115
|
+
handleWalletEvent(contractAddress, eventType) {
|
|
43116
|
+
// Determine which collections to refresh
|
|
43117
|
+
const keysToRefresh = [];
|
|
43118
|
+
for (const [key, entry] of this.collections) {
|
|
43119
|
+
// If no specific contract, refresh all; otherwise match contract
|
|
43120
|
+
if (!contractAddress || entry.params.contractAddress.toLowerCase() === contractAddress.toLowerCase()) {
|
|
43121
|
+
keysToRefresh.push(key);
|
|
43122
|
+
}
|
|
43123
|
+
}
|
|
43124
|
+
if (keysToRefresh.length === 0) {
|
|
43125
|
+
this.log(`No tracked collections affected by event for ${contractAddress || 'all contracts'}`);
|
|
43126
|
+
return;
|
|
43127
|
+
}
|
|
43128
|
+
// Debounce refresh per key
|
|
43129
|
+
for (const key of keysToRefresh) {
|
|
43130
|
+
const existing = this.pendingRefresh.get(key);
|
|
43131
|
+
if (existing) {
|
|
43132
|
+
clearTimeout(existing);
|
|
43133
|
+
}
|
|
43134
|
+
const timeout = setTimeout(async () => {
|
|
43135
|
+
this.pendingRefresh.delete(key);
|
|
43136
|
+
const entry = this.collections.get(key);
|
|
43137
|
+
if (entry) {
|
|
43138
|
+
this.log(`Refreshing ${key} due to wallet event: ${eventType}`);
|
|
43139
|
+
await this.refreshEntry(key, entry, eventType);
|
|
43140
|
+
}
|
|
43141
|
+
}, this.config.eventDebounceMs);
|
|
43142
|
+
this.pendingRefresh.set(key, timeout);
|
|
43143
|
+
}
|
|
43144
|
+
}
|
|
43145
|
+
/**
|
|
43146
|
+
* Fetch and cache a collection
|
|
43147
|
+
*/
|
|
43148
|
+
async fetchAndCache(params) {
|
|
43149
|
+
const key = this.getCacheKey(params);
|
|
43150
|
+
// Mark as refreshing to prevent duplicate fetches
|
|
43151
|
+
const existing = this.collections.get(key);
|
|
43152
|
+
if (existing?.refreshing) {
|
|
43153
|
+
// Wait for existing refresh to complete
|
|
43154
|
+
return existing.lastCollection || this.emptyCollection(params);
|
|
43155
|
+
}
|
|
43156
|
+
this.collections.set(key, {
|
|
43157
|
+
params,
|
|
43158
|
+
lastCollection: null,
|
|
43159
|
+
timestamp: Date.now(),
|
|
43160
|
+
refreshing: true
|
|
43161
|
+
});
|
|
43162
|
+
try {
|
|
43163
|
+
const collection = await this.tokenService.getTokenCollection(params);
|
|
43164
|
+
this.collections.set(key, {
|
|
43165
|
+
params,
|
|
43166
|
+
lastCollection: collection,
|
|
43167
|
+
timestamp: Date.now(),
|
|
43168
|
+
refreshing: false
|
|
43169
|
+
});
|
|
43170
|
+
this.log(`Cached collection for ${key}: ${collection.tokensRetrieved} tokens`);
|
|
43171
|
+
return collection;
|
|
43172
|
+
}
|
|
43173
|
+
catch (error) {
|
|
43174
|
+
// Remove failed entry
|
|
43175
|
+
this.collections.delete(key);
|
|
43176
|
+
throw error;
|
|
43177
|
+
}
|
|
43178
|
+
}
|
|
43179
|
+
/**
|
|
43180
|
+
* Refresh a cached entry and notify handlers
|
|
43181
|
+
*/
|
|
43182
|
+
async refreshEntry(key, entry, source) {
|
|
43183
|
+
if (entry.refreshing) {
|
|
43184
|
+
this.log(`Skipping refresh for ${key} - already refreshing`);
|
|
43185
|
+
return;
|
|
43186
|
+
}
|
|
43187
|
+
entry.refreshing = true;
|
|
43188
|
+
try {
|
|
43189
|
+
const newCollection = await this.tokenService.getTokenCollection(entry.params);
|
|
43190
|
+
entry.lastCollection = newCollection;
|
|
43191
|
+
entry.timestamp = Date.now();
|
|
43192
|
+
entry.refreshing = false;
|
|
43193
|
+
this.log(`Refreshed ${key}: ${newCollection.tokensRetrieved} tokens`);
|
|
43194
|
+
// Notify subscribers
|
|
43195
|
+
for (const handler of this.handlers) {
|
|
43196
|
+
try {
|
|
43197
|
+
handler(newCollection, entry.params.contractAddress, {
|
|
43198
|
+
type: source === 'refresh' ? 'refresh' : 'wallet_event',
|
|
43199
|
+
source
|
|
43200
|
+
});
|
|
43201
|
+
}
|
|
43202
|
+
catch (handlerError) {
|
|
43203
|
+
console.error('[TokenCollectionManager] Handler error:', handlerError);
|
|
43204
|
+
}
|
|
43205
|
+
}
|
|
43206
|
+
}
|
|
43207
|
+
catch (error) {
|
|
43208
|
+
entry.refreshing = false;
|
|
43209
|
+
console.error(`[TokenCollectionManager] Refresh failed for ${key}:`, error);
|
|
43210
|
+
}
|
|
43211
|
+
}
|
|
43212
|
+
/**
|
|
43213
|
+
* Generate cache key from params
|
|
43214
|
+
*/
|
|
43215
|
+
getCacheKey(params) {
|
|
43216
|
+
return `${params.contractAddress}:${params.accountAddress}`;
|
|
43217
|
+
}
|
|
43218
|
+
/**
|
|
43219
|
+
* Create empty collection placeholder
|
|
43220
|
+
*/
|
|
43221
|
+
emptyCollection(params) {
|
|
43222
|
+
return {
|
|
43223
|
+
accountAddress: params.accountAddress,
|
|
43224
|
+
contractAddress: params.contractAddress,
|
|
43225
|
+
totalBalance: 0,
|
|
43226
|
+
tokensRetrieved: 0,
|
|
43227
|
+
tokens: [],
|
|
43228
|
+
note: 'Loading...'
|
|
43229
|
+
};
|
|
43230
|
+
}
|
|
43231
|
+
/**
|
|
43232
|
+
* Debug logging
|
|
43233
|
+
*/
|
|
43234
|
+
log(message, data) {
|
|
43235
|
+
if (this.config.debug) {
|
|
43236
|
+
console.log(`[TokenCollectionManager] ${message}`, data || '');
|
|
43237
|
+
}
|
|
43238
|
+
}
|
|
43239
|
+
/**
|
|
43240
|
+
* Cleanup resources
|
|
43241
|
+
*/
|
|
43242
|
+
destroy() {
|
|
43243
|
+
// Clear pending refreshes
|
|
43244
|
+
for (const timeout of this.pendingRefresh.values()) {
|
|
43245
|
+
clearTimeout(timeout);
|
|
43246
|
+
}
|
|
43247
|
+
this.pendingRefresh.clear();
|
|
43248
|
+
// Unsubscribe from events
|
|
43249
|
+
this.eventUnsubscribe?.();
|
|
43250
|
+
// Clear handlers and collections
|
|
43251
|
+
this.handlers.clear();
|
|
43252
|
+
this.collections.clear();
|
|
43253
|
+
this.log('Destroyed');
|
|
43254
|
+
}
|
|
43255
|
+
}
|
|
43256
|
+
|
|
43257
|
+
/**
|
|
43258
|
+
* BalanceManager - Auto-refresh ERC-20 token balances on wallet events
|
|
43259
|
+
*
|
|
43260
|
+
* This manager provides:
|
|
43261
|
+
* 1. Cached balance storage with automatic refresh
|
|
43262
|
+
* 2. Auto-subscription to wallet events (transfers)
|
|
43263
|
+
* 3. Targeted invalidation based on contract address
|
|
43264
|
+
* 4. Subscriber pattern for reactive updates
|
|
43265
|
+
*
|
|
43266
|
+
* @example
|
|
43267
|
+
* ```typescript
|
|
43268
|
+
* // Get from SDK
|
|
43269
|
+
* const manager = sdk.tokens.balanceManager;
|
|
43270
|
+
*
|
|
43271
|
+
* // Subscribe to balance updates
|
|
43272
|
+
* const unsubscribe = manager.subscribe((balance, contractAddress) => {
|
|
43273
|
+
* console.log(`Balance updated for ${contractAddress}: ${balance}`);
|
|
43274
|
+
* updateUI(balance);
|
|
43275
|
+
* });
|
|
43276
|
+
*
|
|
43277
|
+
* // Fetch balance (auto-cached, auto-refreshed on wallet events)
|
|
43278
|
+
* const balance = await manager.getBalance({
|
|
43279
|
+
* accountAddress: '0x...',
|
|
43280
|
+
* contractAddress: '0x...',
|
|
43281
|
+
* abi: contractAbi,
|
|
43282
|
+
* chainId: 39123
|
|
43283
|
+
* });
|
|
43284
|
+
*
|
|
43285
|
+
* // Manual refresh
|
|
43286
|
+
* await manager.refreshBalance(contractAddress);
|
|
43287
|
+
*
|
|
43288
|
+
* // Cleanup
|
|
43289
|
+
* unsubscribe();
|
|
43290
|
+
* ```
|
|
43291
|
+
*/
|
|
43292
|
+
/**
|
|
43293
|
+
* Manages ERC-20 token balances with automatic refresh on wallet events
|
|
43294
|
+
*/
|
|
43295
|
+
class BalanceManager {
|
|
43296
|
+
constructor(tokenService, eventEmitter, config) {
|
|
43297
|
+
this.tokenService = tokenService;
|
|
43298
|
+
this.eventEmitter = eventEmitter;
|
|
43299
|
+
this.balances = new Map();
|
|
43300
|
+
this.handlers = new Set();
|
|
43301
|
+
this.pendingRefresh = new Map();
|
|
43302
|
+
this.config = {
|
|
43303
|
+
autoRefreshOnWalletEvents: config?.autoRefreshOnWalletEvents ?? true,
|
|
43304
|
+
eventDebounceMs: config?.eventDebounceMs ?? 500,
|
|
43305
|
+
debug: config?.debug ?? false
|
|
43306
|
+
};
|
|
43307
|
+
if (this.config.autoRefreshOnWalletEvents && eventEmitter) {
|
|
43308
|
+
this.setupEventSubscription();
|
|
43309
|
+
}
|
|
43310
|
+
}
|
|
43311
|
+
/**
|
|
43312
|
+
* Subscribe to wallet events for auto-refresh
|
|
43313
|
+
*/
|
|
43314
|
+
setupEventSubscription() {
|
|
43315
|
+
if (!this.eventEmitter) {
|
|
43316
|
+
return;
|
|
43317
|
+
}
|
|
43318
|
+
this.eventUnsubscribe = this.eventEmitter.subscribe((event) => {
|
|
43319
|
+
const contractAddress = event.details?.contractAddress;
|
|
43320
|
+
this.handleWalletEvent(contractAddress, event.type);
|
|
43321
|
+
}, { domains: ['wallet'] });
|
|
43322
|
+
}
|
|
43323
|
+
/**
|
|
43324
|
+
* Get a token balance (cached, auto-refreshed on wallet events)
|
|
43325
|
+
*
|
|
43326
|
+
* @param params - Balance parameters
|
|
43327
|
+
* @returns Promise resolving to the token balance
|
|
43328
|
+
*/
|
|
43329
|
+
async getBalance(params) {
|
|
43330
|
+
const key = this.getCacheKey(params);
|
|
43331
|
+
const cached = this.balances.get(key);
|
|
43332
|
+
// Return cached if available and not stale
|
|
43333
|
+
if (cached?.lastBalance) {
|
|
43334
|
+
this.log(`Cache HIT for ${key}`);
|
|
43335
|
+
return cached.lastBalance;
|
|
43336
|
+
}
|
|
43337
|
+
this.log(`Cache MISS for ${key}, fetching...`);
|
|
43338
|
+
return this.fetchAndCache(params);
|
|
43339
|
+
}
|
|
43340
|
+
/**
|
|
43341
|
+
* Force refresh a specific balance
|
|
43342
|
+
*
|
|
43343
|
+
* @param contractAddress - Contract address to refresh
|
|
43344
|
+
* @param accountAddress - Account address to refresh (optional, refreshes all if not provided)
|
|
43345
|
+
* @returns Promise that resolves when refresh is complete
|
|
43346
|
+
*/
|
|
43347
|
+
async refreshBalance(contractAddress, accountAddress) {
|
|
43348
|
+
const toRefresh = [];
|
|
43349
|
+
for (const [key, entry] of this.balances) {
|
|
43350
|
+
if (entry.params.contractAddress.toLowerCase() === contractAddress.toLowerCase()) {
|
|
43351
|
+
if (!accountAddress || entry.params.accountAddress.toLowerCase() === accountAddress.toLowerCase()) {
|
|
43352
|
+
toRefresh.push(key);
|
|
43353
|
+
}
|
|
43354
|
+
}
|
|
43355
|
+
}
|
|
43356
|
+
await Promise.all(toRefresh.map(key => {
|
|
43357
|
+
const entry = this.balances.get(key);
|
|
43358
|
+
if (entry) {
|
|
43359
|
+
return this.refreshEntry(key, entry, 'refresh');
|
|
43360
|
+
}
|
|
43361
|
+
}));
|
|
43362
|
+
}
|
|
43363
|
+
/**
|
|
43364
|
+
* Force refresh all tracked balances
|
|
43365
|
+
*
|
|
43366
|
+
* @returns Promise that resolves when all refreshes are complete
|
|
43367
|
+
*/
|
|
43368
|
+
async refreshAll() {
|
|
43369
|
+
const refreshPromises = Array.from(this.balances.entries()).map(([key, entry]) => this.refreshEntry(key, entry, 'refresh'));
|
|
43370
|
+
await Promise.all(refreshPromises);
|
|
43371
|
+
}
|
|
43372
|
+
/**
|
|
43373
|
+
* Subscribe to balance change events
|
|
43374
|
+
*
|
|
43375
|
+
* @param handler - Callback when any tracked balance changes
|
|
43376
|
+
* @returns Unsubscribe function
|
|
43377
|
+
*/
|
|
43378
|
+
subscribe(handler) {
|
|
43379
|
+
this.handlers.add(handler);
|
|
43380
|
+
return () => this.handlers.delete(handler);
|
|
43381
|
+
}
|
|
43382
|
+
/**
|
|
43383
|
+
* Get all currently tracked balances
|
|
43384
|
+
*/
|
|
43385
|
+
getTrackedBalances() {
|
|
43386
|
+
return Array.from(this.balances.entries()).map(([_, entry]) => ({
|
|
43387
|
+
contractAddress: entry.params.contractAddress,
|
|
43388
|
+
accountAddress: entry.params.accountAddress,
|
|
43389
|
+
balance: entry.lastBalance
|
|
43390
|
+
}));
|
|
43391
|
+
}
|
|
43392
|
+
/**
|
|
43393
|
+
* Check if a balance is currently being tracked
|
|
43394
|
+
*/
|
|
43395
|
+
isTracking(contractAddress, accountAddress) {
|
|
43396
|
+
const key = `${contractAddress.toLowerCase()}:${accountAddress.toLowerCase()}`;
|
|
43397
|
+
return this.balances.has(key);
|
|
43398
|
+
}
|
|
43399
|
+
/**
|
|
43400
|
+
* Clear tracking for a specific balance
|
|
43401
|
+
*/
|
|
43402
|
+
untrack(contractAddress, accountAddress) {
|
|
43403
|
+
if (accountAddress) {
|
|
43404
|
+
const key = `${contractAddress.toLowerCase()}:${accountAddress.toLowerCase()}`;
|
|
43405
|
+
this.balances.delete(key);
|
|
43406
|
+
this.log(`Untracked ${key}`);
|
|
43407
|
+
}
|
|
43408
|
+
else {
|
|
43409
|
+
// Untrack all for this contract
|
|
43410
|
+
const toDelete = [];
|
|
43411
|
+
for (const key of this.balances.keys()) {
|
|
43412
|
+
if (key.startsWith(`${contractAddress.toLowerCase()}:`)) {
|
|
43413
|
+
toDelete.push(key);
|
|
43414
|
+
}
|
|
43415
|
+
}
|
|
43416
|
+
toDelete.forEach(key => this.balances.delete(key));
|
|
43417
|
+
this.log(`Untracked all balances for ${contractAddress}`);
|
|
43418
|
+
}
|
|
43419
|
+
}
|
|
43420
|
+
/**
|
|
43421
|
+
* Clear all tracked balances
|
|
43422
|
+
*/
|
|
43423
|
+
clearAll() {
|
|
43424
|
+
this.balances.clear();
|
|
43425
|
+
this.log('Cleared all tracked balances');
|
|
43426
|
+
}
|
|
43427
|
+
/**
|
|
43428
|
+
* Handle wallet events (with debouncing)
|
|
43429
|
+
*/
|
|
43430
|
+
handleWalletEvent(contractAddress, eventType) {
|
|
43431
|
+
// Determine which balances to refresh
|
|
43432
|
+
const keysToRefresh = [];
|
|
43433
|
+
for (const [key, entry] of this.balances) {
|
|
43434
|
+
// If no specific contract, refresh all; otherwise match contract
|
|
43435
|
+
if (!contractAddress || entry.params.contractAddress.toLowerCase() === contractAddress.toLowerCase()) {
|
|
43436
|
+
keysToRefresh.push(key);
|
|
43437
|
+
}
|
|
43438
|
+
}
|
|
43439
|
+
if (keysToRefresh.length === 0) {
|
|
43440
|
+
this.log(`No tracked balances affected by event for ${contractAddress || 'all contracts'}`);
|
|
43441
|
+
return;
|
|
43442
|
+
}
|
|
43443
|
+
// Debounce refresh per key
|
|
43444
|
+
for (const key of keysToRefresh) {
|
|
43445
|
+
const existing = this.pendingRefresh.get(key);
|
|
43446
|
+
if (existing) {
|
|
43447
|
+
clearTimeout(existing);
|
|
43448
|
+
}
|
|
43449
|
+
const timeout = setTimeout(async () => {
|
|
43450
|
+
this.pendingRefresh.delete(key);
|
|
43451
|
+
const entry = this.balances.get(key);
|
|
43452
|
+
if (entry) {
|
|
43453
|
+
this.log(`Refreshing ${key} due to wallet event: ${eventType}`);
|
|
43454
|
+
await this.refreshEntry(key, entry, eventType);
|
|
43455
|
+
}
|
|
43456
|
+
}, this.config.eventDebounceMs);
|
|
43457
|
+
this.pendingRefresh.set(key, timeout);
|
|
43458
|
+
}
|
|
43459
|
+
}
|
|
43460
|
+
/**
|
|
43461
|
+
* Fetch and cache a balance
|
|
43462
|
+
*/
|
|
43463
|
+
async fetchAndCache(params) {
|
|
43464
|
+
const key = this.getCacheKey(params);
|
|
43465
|
+
// Mark as refreshing to prevent duplicate fetches
|
|
43466
|
+
const existing = this.balances.get(key);
|
|
43467
|
+
if (existing?.refreshing) {
|
|
43468
|
+
// Wait for existing refresh to complete
|
|
43469
|
+
return existing.lastBalance || this.emptyBalance();
|
|
43470
|
+
}
|
|
43471
|
+
this.balances.set(key, {
|
|
43472
|
+
params,
|
|
43473
|
+
lastBalance: null,
|
|
43474
|
+
timestamp: Date.now(),
|
|
43475
|
+
refreshing: true
|
|
43476
|
+
});
|
|
43477
|
+
try {
|
|
43478
|
+
const balance = await this.tokenService.getTokenBalance({
|
|
43479
|
+
...params,
|
|
43480
|
+
tokenId: null // ERC-20 has no tokenId
|
|
43481
|
+
});
|
|
43482
|
+
this.balances.set(key, {
|
|
43483
|
+
params,
|
|
43484
|
+
lastBalance: balance,
|
|
43485
|
+
timestamp: Date.now(),
|
|
43486
|
+
refreshing: false
|
|
43487
|
+
});
|
|
43488
|
+
this.log(`Cached balance for ${key}: ${balance.balance}`);
|
|
43489
|
+
return balance;
|
|
43490
|
+
}
|
|
43491
|
+
catch (error) {
|
|
43492
|
+
// Remove failed entry
|
|
43493
|
+
this.balances.delete(key);
|
|
43494
|
+
throw error;
|
|
43495
|
+
}
|
|
43496
|
+
}
|
|
43497
|
+
/**
|
|
43498
|
+
* Refresh a cached entry and notify handlers
|
|
43499
|
+
*/
|
|
43500
|
+
async refreshEntry(key, entry, source) {
|
|
43501
|
+
if (entry.refreshing) {
|
|
43502
|
+
this.log(`Skipping refresh for ${key} - already refreshing`);
|
|
43503
|
+
return;
|
|
43504
|
+
}
|
|
43505
|
+
entry.refreshing = true;
|
|
43506
|
+
try {
|
|
43507
|
+
const newBalance = await this.tokenService.getTokenBalance({
|
|
43508
|
+
...entry.params,
|
|
43509
|
+
tokenId: null // ERC-20 has no tokenId
|
|
43510
|
+
});
|
|
43511
|
+
entry.lastBalance = newBalance;
|
|
43512
|
+
entry.timestamp = Date.now();
|
|
43513
|
+
entry.refreshing = false;
|
|
43514
|
+
this.log(`Refreshed ${key}: ${newBalance.balance}`);
|
|
43515
|
+
// Notify subscribers
|
|
43516
|
+
for (const handler of this.handlers) {
|
|
43517
|
+
try {
|
|
43518
|
+
handler(newBalance, entry.params.contractAddress, {
|
|
43519
|
+
type: source === 'refresh' ? 'refresh' : 'wallet_event',
|
|
43520
|
+
source
|
|
43521
|
+
});
|
|
43522
|
+
}
|
|
43523
|
+
catch (handlerError) {
|
|
43524
|
+
console.error('[BalanceManager] Handler error:', handlerError);
|
|
43525
|
+
}
|
|
43526
|
+
}
|
|
43527
|
+
}
|
|
43528
|
+
catch (error) {
|
|
43529
|
+
entry.refreshing = false;
|
|
43530
|
+
console.error(`[BalanceManager] Refresh failed for ${key}:`, error);
|
|
43531
|
+
}
|
|
43532
|
+
}
|
|
43533
|
+
/**
|
|
43534
|
+
* Generate cache key from params
|
|
43535
|
+
*/
|
|
43536
|
+
getCacheKey(params) {
|
|
43537
|
+
return `${params.contractAddress.toLowerCase()}:${params.accountAddress.toLowerCase()}`;
|
|
43538
|
+
}
|
|
43539
|
+
/**
|
|
43540
|
+
* Create empty balance placeholder
|
|
43541
|
+
*/
|
|
43542
|
+
emptyBalance() {
|
|
43543
|
+
return {
|
|
43544
|
+
tokenId: null,
|
|
43545
|
+
balance: 0,
|
|
43546
|
+
hasBalance: false,
|
|
43547
|
+
metadata: null
|
|
43548
|
+
};
|
|
43549
|
+
}
|
|
43550
|
+
/**
|
|
43551
|
+
* Debug logging
|
|
43552
|
+
*/
|
|
43553
|
+
log(message, data) {
|
|
43554
|
+
if (this.config.debug) {
|
|
43555
|
+
console.log(`[BalanceManager] ${message}`, data || '');
|
|
43556
|
+
}
|
|
43557
|
+
}
|
|
43558
|
+
/**
|
|
43559
|
+
* Cleanup resources
|
|
43560
|
+
*/
|
|
43561
|
+
destroy() {
|
|
43562
|
+
// Clear pending refreshes
|
|
43563
|
+
for (const timeout of this.pendingRefresh.values()) {
|
|
43564
|
+
clearTimeout(timeout);
|
|
43565
|
+
}
|
|
43566
|
+
this.pendingRefresh.clear();
|
|
43567
|
+
// Unsubscribe from events
|
|
43568
|
+
this.eventUnsubscribe?.();
|
|
43569
|
+
// Clear handlers and balances
|
|
43570
|
+
this.handlers.clear();
|
|
43571
|
+
this.balances.clear();
|
|
43572
|
+
this.log('Destroyed');
|
|
43573
|
+
}
|
|
43574
|
+
}
|
|
43575
|
+
|
|
41710
43576
|
/**
|
|
41711
43577
|
* Web3ApplicationService - Application layer entrance point
|
|
41712
43578
|
* Orchestrates domain services and provides clean public interface
|
|
@@ -41815,6 +43681,30 @@ class Web3InfrastructureApi {
|
|
|
41815
43681
|
return 0;
|
|
41816
43682
|
}
|
|
41817
43683
|
}
|
|
43684
|
+
/**
|
|
43685
|
+
* Get multiple ERC-1155 token balances in a single RPC call using native balanceOfBatch.
|
|
43686
|
+
* Falls back to individual calls if balanceOfBatch fails.
|
|
43687
|
+
*
|
|
43688
|
+
* @param request - Batch balance request
|
|
43689
|
+
* @returns Array of balance results per tokenId
|
|
43690
|
+
*/
|
|
43691
|
+
async getBatchTokenBalances(request) {
|
|
43692
|
+
try {
|
|
43693
|
+
const ethersProvider = await this.web3ChainService.getEthersProviderByChainId(request.chainId);
|
|
43694
|
+
const results = await getBatchBalancesWithFallback(ethersProvider, request.contractAddress, request.abi, request.accountAddress, request.tokenIds, request.isERC1155 ?? true);
|
|
43695
|
+
return results;
|
|
43696
|
+
}
|
|
43697
|
+
catch (error) {
|
|
43698
|
+
console.error(`Failed to get batch token balances:`, error);
|
|
43699
|
+
// Return empty results on failure
|
|
43700
|
+
return request.tokenIds.map(tokenId => ({
|
|
43701
|
+
tokenId,
|
|
43702
|
+
balance: 0n,
|
|
43703
|
+
success: false,
|
|
43704
|
+
error: `${error}`
|
|
43705
|
+
}));
|
|
43706
|
+
}
|
|
43707
|
+
}
|
|
41818
43708
|
async getTokenUri(request) {
|
|
41819
43709
|
try {
|
|
41820
43710
|
const ethersProvider = await this.web3ChainService.getEthersProviderByChainId(request.chainId);
|
|
@@ -42052,17 +43942,11 @@ async function getExplorerUrlByChainId(getChainData, chainId, address, type) {
|
|
|
42052
43942
|
* ```
|
|
42053
43943
|
*/
|
|
42054
43944
|
class Web3Manager {
|
|
42055
|
-
|
|
42056
|
-
// When ready, add:
|
|
42057
|
-
// 1. constructor param: private events?: PersEventEmitter
|
|
42058
|
-
// 2. Subscribe to contract events (Transfer, Approval, etc.)
|
|
42059
|
-
// 3. Emit via: this.events?.emitSuccess({ domain: 'web3', type: 'NFT_TRANSFERRED', ... })
|
|
42060
|
-
// 4. Filter to only user's address (not all transfers)
|
|
42061
|
-
// 5. Add cleanup for event listeners on SDK destroy
|
|
42062
|
-
constructor(apiClient, tenantManager) {
|
|
43945
|
+
constructor(apiClient, tenantManager, eventEmitter) {
|
|
42063
43946
|
this.apiClient = apiClient;
|
|
42064
43947
|
// Use provided TenantManager or create one
|
|
42065
43948
|
this.tenantManager = tenantManager || new TenantManager(apiClient);
|
|
43949
|
+
this.eventEmitter = eventEmitter;
|
|
42066
43950
|
// Initialize Web3 Chain service
|
|
42067
43951
|
const web3ChainApi = new Web3ChainApi(apiClient);
|
|
42068
43952
|
this.web3ChainService = new Web3ChainService(web3ChainApi);
|
|
@@ -42070,6 +43954,75 @@ class Web3Manager {
|
|
|
42070
43954
|
const web3InfrastructureApi = new Web3InfrastructureApi(this.web3ChainService);
|
|
42071
43955
|
const ipfsInfrastructureApi = new IPFSInfrastructureApi(this.tenantManager);
|
|
42072
43956
|
this.web3ApplicationService = new Web3ApplicationService(web3InfrastructureApi, ipfsInfrastructureApi);
|
|
43957
|
+
// Initialize domain services for managers
|
|
43958
|
+
const contractDomainService = new ContractDomainService();
|
|
43959
|
+
const metadataDomainService = new MetadataDomainService(ipfsInfrastructureApi);
|
|
43960
|
+
this.tokenDomainService = new TokenDomainService(web3InfrastructureApi, metadataDomainService, contractDomainService);
|
|
43961
|
+
}
|
|
43962
|
+
/**
|
|
43963
|
+
* Get the TokenCollectionManager for NFT collection operations.
|
|
43964
|
+
*
|
|
43965
|
+
* Features:
|
|
43966
|
+
* - Auto-refresh on wallet events (transfers, mints, burns)
|
|
43967
|
+
* - Cached collections
|
|
43968
|
+
* - Subscriber pattern for reactive updates
|
|
43969
|
+
*
|
|
43970
|
+
* @example
|
|
43971
|
+
* ```typescript
|
|
43972
|
+
* const manager = sdk.web3.collectionManager;
|
|
43973
|
+
*
|
|
43974
|
+
* // Subscribe to updates
|
|
43975
|
+
* const unsubscribe = manager.subscribe((collection, contractAddress) => {
|
|
43976
|
+
* console.log(`Collection updated for ${contractAddress}`);
|
|
43977
|
+
* });
|
|
43978
|
+
*
|
|
43979
|
+
* // Fetch collection (auto-cached, auto-refreshed)
|
|
43980
|
+
* const collection = await manager.getCollection({...});
|
|
43981
|
+
*
|
|
43982
|
+
* // Manual refresh
|
|
43983
|
+
* await manager.refreshCollection(contractAddress);
|
|
43984
|
+
* ```
|
|
43985
|
+
*/
|
|
43986
|
+
get collectionManager() {
|
|
43987
|
+
if (!this._collectionManager) {
|
|
43988
|
+
this._collectionManager = new TokenCollectionManager(this.tokenDomainService, this.eventEmitter);
|
|
43989
|
+
}
|
|
43990
|
+
return this._collectionManager;
|
|
43991
|
+
}
|
|
43992
|
+
/**
|
|
43993
|
+
* Get the BalanceManager for ERC-20 (credit token) balance operations.
|
|
43994
|
+
*
|
|
43995
|
+
* Features:
|
|
43996
|
+
* - Auto-refresh on wallet events (transfers)
|
|
43997
|
+
* - Cached balances
|
|
43998
|
+
* - Subscriber pattern for reactive updates
|
|
43999
|
+
*
|
|
44000
|
+
* @example
|
|
44001
|
+
* ```typescript
|
|
44002
|
+
* const manager = sdk.web3.balanceManager;
|
|
44003
|
+
*
|
|
44004
|
+
* // Subscribe to balance updates
|
|
44005
|
+
* const unsubscribe = manager.subscribe((balance, contractAddress) => {
|
|
44006
|
+
* console.log(`Balance updated: ${balance.balance}`);
|
|
44007
|
+
* });
|
|
44008
|
+
*
|
|
44009
|
+
* // Fetch balance (auto-cached, auto-refreshed)
|
|
44010
|
+
* const balance = await manager.getBalance({
|
|
44011
|
+
* accountAddress: '0x...',
|
|
44012
|
+
* contractAddress: '0x...',
|
|
44013
|
+
* abi: creditTokenAbi,
|
|
44014
|
+
* chainId: 39123
|
|
44015
|
+
* });
|
|
44016
|
+
*
|
|
44017
|
+
* // Manual refresh
|
|
44018
|
+
* await manager.refreshBalance(contractAddress);
|
|
44019
|
+
* ```
|
|
44020
|
+
*/
|
|
44021
|
+
get balanceManager() {
|
|
44022
|
+
if (!this._balanceManager) {
|
|
44023
|
+
this._balanceManager = new BalanceManager(this.tokenDomainService, this.eventEmitter);
|
|
44024
|
+
}
|
|
44025
|
+
return this._balanceManager;
|
|
42073
44026
|
}
|
|
42074
44027
|
/**
|
|
42075
44028
|
* Get token balance for a specific token
|
|
@@ -42268,6 +44221,18 @@ class Web3Manager {
|
|
|
42268
44221
|
maxTokens
|
|
42269
44222
|
};
|
|
42270
44223
|
}
|
|
44224
|
+
/**
|
|
44225
|
+
* Cleanup resources.
|
|
44226
|
+
*
|
|
44227
|
+
* Call this when the SDK is being destroyed to clean up
|
|
44228
|
+
* event subscriptions and cached data.
|
|
44229
|
+
*/
|
|
44230
|
+
destroy() {
|
|
44231
|
+
this._collectionManager?.destroy();
|
|
44232
|
+
this._balanceManager?.destroy();
|
|
44233
|
+
this._collectionManager = undefined;
|
|
44234
|
+
this._balanceManager = undefined;
|
|
44235
|
+
}
|
|
42271
44236
|
}
|
|
42272
44237
|
|
|
42273
44238
|
/**
|
|
@@ -43159,6 +45124,83 @@ class BrowserFetchClientAdapter {
|
|
|
43159
45124
|
}
|
|
43160
45125
|
}
|
|
43161
45126
|
|
|
45127
|
+
/**
|
|
45128
|
+
* Build optimized image URL with automatic IPFS resolution and CDN options
|
|
45129
|
+
*
|
|
45130
|
+
* Single entry point for all image URL building:
|
|
45131
|
+
* - IPFS URIs → resolved via gateway + CDN options applied
|
|
45132
|
+
* - HTTP URLs → CDN options applied directly
|
|
45133
|
+
*
|
|
45134
|
+
* @param url - Image URL (ipfs://, https://, or any URL)
|
|
45135
|
+
* @param options - CDN optimization options (preset, quality, format, fit)
|
|
45136
|
+
* @param gateway - IPFS gateway domain (required for ipfs:// URLs)
|
|
45137
|
+
* @returns Optimized URL with query parameters
|
|
45138
|
+
*
|
|
45139
|
+
* @example
|
|
45140
|
+
* ```typescript
|
|
45141
|
+
* // IPFS thumbnail
|
|
45142
|
+
* buildImageUrl('ipfs://QmHash', { preset: 'thumb' }, 'cdn.example.com')
|
|
45143
|
+
* // => 'https://cdn.example.com/ipfs/QmHash?preset=thumb'
|
|
45144
|
+
*
|
|
45145
|
+
* // IPFS hero with high quality
|
|
45146
|
+
* buildImageUrl('ipfs://QmHash', { preset: 'hero', q: 95, fit: 'cover' }, 'cdn.example.com')
|
|
45147
|
+
* // => 'https://cdn.example.com/ipfs/QmHash?preset=hero&q=95&fit=cover'
|
|
45148
|
+
*
|
|
45149
|
+
* // HTTP URL (gateway ignored)
|
|
45150
|
+
* buildImageUrl('https://example.com/photo.jpg', { preset: 'card' })
|
|
45151
|
+
* // => 'https://example.com/photo.jpg?preset=card'
|
|
45152
|
+
*
|
|
45153
|
+
* // No options - just IPFS resolution
|
|
45154
|
+
* buildImageUrl('ipfs://QmHash', undefined, 'cdn.example.com')
|
|
45155
|
+
* // => 'https://cdn.example.com/ipfs/QmHash'
|
|
45156
|
+
* ```
|
|
45157
|
+
*/
|
|
45158
|
+
function buildImageUrl(url, options, gateway) {
|
|
45159
|
+
if (!url)
|
|
45160
|
+
return '';
|
|
45161
|
+
let resolvedUrl = url;
|
|
45162
|
+
// Resolve IPFS to HTTP via gateway
|
|
45163
|
+
if (url.startsWith('ipfs://')) {
|
|
45164
|
+
if (!gateway) {
|
|
45165
|
+
console.warn('[buildImageUrl] IPFS URL requires gateway parameter');
|
|
45166
|
+
return url;
|
|
45167
|
+
}
|
|
45168
|
+
const cid = url.replace('ipfs://', '');
|
|
45169
|
+
resolvedUrl = `https://${gateway}/ipfs/${cid}`;
|
|
45170
|
+
}
|
|
45171
|
+
// Apply CDN options
|
|
45172
|
+
return buildImageCdnUrl(resolvedUrl, options);
|
|
45173
|
+
}
|
|
45174
|
+
/**
|
|
45175
|
+
* Check if a URL is an IPFS URI
|
|
45176
|
+
*
|
|
45177
|
+
* @param url - URL to check
|
|
45178
|
+
* @returns True if URL starts with ipfs://
|
|
45179
|
+
*/
|
|
45180
|
+
function isIpfsUrl(url) {
|
|
45181
|
+
return !!url && url.startsWith('ipfs://');
|
|
45182
|
+
}
|
|
45183
|
+
/**
|
|
45184
|
+
* Extract CID from IPFS URI
|
|
45185
|
+
*
|
|
45186
|
+
* @param ipfsUri - IPFS URI (ipfs://Qm... or ipfs://baf...)
|
|
45187
|
+
* @returns CID string, or null if not a valid IPFS URI
|
|
45188
|
+
*
|
|
45189
|
+
* @example
|
|
45190
|
+
* ```typescript
|
|
45191
|
+
* extractIpfsCid('ipfs://QmHash123')
|
|
45192
|
+
* // => 'QmHash123'
|
|
45193
|
+
*
|
|
45194
|
+
* extractIpfsCid('https://example.com/image.jpg')
|
|
45195
|
+
* // => null
|
|
45196
|
+
* ```
|
|
45197
|
+
*/
|
|
45198
|
+
function extractIpfsCid(ipfsUri) {
|
|
45199
|
+
if (!isIpfsUrl(ipfsUri))
|
|
45200
|
+
return null;
|
|
45201
|
+
return ipfsUri.replace('ipfs://', '');
|
|
45202
|
+
}
|
|
45203
|
+
|
|
43162
45204
|
/**
|
|
43163
45205
|
* React hook for loading and managing token balances
|
|
43164
45206
|
*
|
|
@@ -46833,8 +48875,11 @@ const useTriggerSources = () => {
|
|
|
46833
48875
|
};
|
|
46834
48876
|
};
|
|
46835
48877
|
|
|
48878
|
+
exports.ACTIVE_USER_DEFINITION = ACTIVE_USER_DEFINITION;
|
|
46836
48879
|
exports.ANALYTICS_GROUP_BY_TYPES = ANALYTICS_GROUP_BY_TYPES;
|
|
46837
48880
|
exports.ANALYTICS_METRIC_TYPES = ANALYTICS_METRIC_TYPES;
|
|
48881
|
+
exports.ANALYTICS_VALID_PROCESS_STATUS = ANALYTICS_VALID_PROCESS_STATUS;
|
|
48882
|
+
exports.ANALYTICS_VALID_TRANSACTION_STATUSES = ANALYTICS_VALID_TRANSACTION_STATUSES;
|
|
46838
48883
|
exports.AUTH_STORAGE_KEYS = AUTH_STORAGE_KEYS;
|
|
46839
48884
|
exports.AnalyticsApi = AnalyticsApi;
|
|
46840
48885
|
exports.AnalyticsManager = AnalyticsManager;
|
|
@@ -46865,6 +48910,7 @@ exports.CampaignService = CampaignService;
|
|
|
46865
48910
|
exports.ChainTypes = ChainTypes;
|
|
46866
48911
|
exports.ClientTransactionType = ClientTransactionType;
|
|
46867
48912
|
exports.CommonErrorCodes = CommonErrorCodes;
|
|
48913
|
+
exports.CustomFieldDefinitionManager = CustomFieldDefinitionManager;
|
|
46868
48914
|
exports.DEFAULT_PERS_CONFIG = DEFAULT_PERS_CONFIG;
|
|
46869
48915
|
exports.DPOP_STORAGE_KEYS = DPOP_STORAGE_KEYS;
|
|
46870
48916
|
exports.DPoPManager = DPoPManager;
|
|
@@ -46873,6 +48919,9 @@ exports.Domains = Domains;
|
|
|
46873
48919
|
exports.DonationApi = DonationApi;
|
|
46874
48920
|
exports.DonationManager = DonationManager;
|
|
46875
48921
|
exports.DonationService = DonationService;
|
|
48922
|
+
exports.EMAIL_MAX_LENGTH = EMAIL_MAX_LENGTH;
|
|
48923
|
+
exports.EMAIL_REGEX = EMAIL_REGEX;
|
|
48924
|
+
exports.ENGAGEMENT_RATE_FORMULA = ENGAGEMENT_RATE_FORMULA;
|
|
46876
48925
|
exports.EntityTypes = EntityTypes;
|
|
46877
48926
|
exports.ErrorDomains = ErrorDomains;
|
|
46878
48927
|
exports.ErrorUtils = ErrorUtils;
|
|
@@ -46880,6 +48929,14 @@ exports.FATAL_AUTH_CODES = FATAL_AUTH_CODES;
|
|
|
46880
48929
|
exports.FileApi = FileApi;
|
|
46881
48930
|
exports.FileManager = FileManager;
|
|
46882
48931
|
exports.FileService = FileService;
|
|
48932
|
+
exports.IMAGE_CDN_CACHE_TTL = IMAGE_CDN_CACHE_TTL;
|
|
48933
|
+
exports.IMAGE_CDN_DEFAULT_FIT = IMAGE_CDN_DEFAULT_FIT;
|
|
48934
|
+
exports.IMAGE_CDN_DEFAULT_FORMAT = IMAGE_CDN_DEFAULT_FORMAT;
|
|
48935
|
+
exports.IMAGE_CDN_DEFAULT_QUALITY = IMAGE_CDN_DEFAULT_QUALITY;
|
|
48936
|
+
exports.IMAGE_CDN_MAX_DIMENSION = IMAGE_CDN_MAX_DIMENSION;
|
|
48937
|
+
exports.IMAGE_CDN_MAX_INPUT_SIZE = IMAGE_CDN_MAX_INPUT_SIZE;
|
|
48938
|
+
exports.IMAGE_CDN_PRESETS = IMAGE_CDN_PRESETS;
|
|
48939
|
+
exports.IMAGE_CDN_SUPPORTED_FORMATS = IMAGE_CDN_SUPPORTED_FORMATS;
|
|
46883
48940
|
exports.IndexedDBTokenStorage = IndexedDBTokenStorage;
|
|
46884
48941
|
exports.LocalStorageTokenStorage = LocalStorageTokenStorage;
|
|
46885
48942
|
exports.LogoutRequired = LogoutRequired;
|
|
@@ -46888,6 +48945,7 @@ exports.MemoryTokenStorage = MemoryTokenStorage;
|
|
|
46888
48945
|
exports.NativeTokenTypes = NativeTokenTypes;
|
|
46889
48946
|
exports.NetworkError = NetworkError;
|
|
46890
48947
|
exports.NodeHttpClientAdapter = NodeHttpClientAdapter;
|
|
48948
|
+
exports.OttAuthMethod = OttAuthMethod;
|
|
46891
48949
|
exports.PaymentApi = PurchaseApi;
|
|
46892
48950
|
exports.PaymentService = PaymentService;
|
|
46893
48951
|
exports.PersApiClient = PersApiClient;
|
|
@@ -46932,6 +48990,8 @@ exports.TransactionService = TransactionService;
|
|
|
46932
48990
|
exports.TriggerSourceApi = TriggerSourceApi;
|
|
46933
48991
|
exports.TriggerSourceManager = TriggerSourceManager;
|
|
46934
48992
|
exports.TriggerSourceService = TriggerSourceService;
|
|
48993
|
+
exports.URL_REGEX = URL_REGEX;
|
|
48994
|
+
exports.UUID_REGEX = UUID_REGEX;
|
|
46935
48995
|
exports.UserApi = UserApi;
|
|
46936
48996
|
exports.UserManager = UserManager;
|
|
46937
48997
|
exports.UserService = UserService;
|
|
@@ -46947,6 +49007,7 @@ exports.VALID_RELATIONS = VALID_RELATIONS;
|
|
|
46947
49007
|
exports.VALID_TOKEN_METADATA_RELATIONS = VALID_TOKEN_METADATA_RELATIONS;
|
|
46948
49008
|
exports.VALID_TRANSACTION_RELATIONS = VALID_TRANSACTION_RELATIONS;
|
|
46949
49009
|
exports.VALID_USER_RELATIONS = VALID_USER_RELATIONS;
|
|
49010
|
+
exports.WALLET_ADDRESS_REGEX = WALLET_ADDRESS_REGEX;
|
|
46950
49011
|
exports.WalletEventsManager = WalletEventsManager;
|
|
46951
49012
|
exports.WebDPoPCryptoProvider = WebDPoPCryptoProvider;
|
|
46952
49013
|
exports.WebhookApi = WebhookApi;
|
|
@@ -46955,6 +49016,8 @@ exports.WebhookService = WebhookService;
|
|
|
46955
49016
|
exports.apiPublicKeyTestPrefix = apiPublicKeyTestPrefix;
|
|
46956
49017
|
exports.buildApiRoot = buildApiRoot;
|
|
46957
49018
|
exports.buildBurnRequest = buildBurnRequest;
|
|
49019
|
+
exports.buildImageCdnUrl = buildImageCdnUrl;
|
|
49020
|
+
exports.buildImageUrl = buildImageUrl;
|
|
46958
49021
|
exports.buildMintRequest = buildMintRequest;
|
|
46959
49022
|
exports.buildPOSBurnRequest = buildPOSBurnRequest;
|
|
46960
49023
|
exports.buildPOSTransferRequest = buildPOSTransferRequest;
|
|
@@ -46963,47 +49026,67 @@ exports.buildPendingTransactionData = buildPendingTransactionData;
|
|
|
46963
49026
|
exports.buildSubmissionRequest = buildSubmissionRequest;
|
|
46964
49027
|
exports.buildTransferRequest = buildTransferRequest;
|
|
46965
49028
|
exports.buildWalletEventsWsUrl = buildWalletEventsWsUrl;
|
|
49029
|
+
exports.builtInUserFieldMetadata = builtInUserFieldMetadata;
|
|
49030
|
+
exports.builtInUserFields = builtInUserFields;
|
|
49031
|
+
exports.compareValues = compareValues;
|
|
46966
49032
|
exports.createPersEventsClient = createPersEventsClient;
|
|
46967
49033
|
exports.createPersSDK = createPersSDK;
|
|
46968
49034
|
exports.createReactNativeAuthProvider = createReactNativeAuthProvider;
|
|
46969
49035
|
exports.createUserIdentifierObject = createUserIdentifierObject;
|
|
46970
49036
|
exports.createUserStatusSDK = createUserStatusSDK;
|
|
49037
|
+
exports.customFieldEntityTypes = customFieldEntityTypes;
|
|
46971
49038
|
exports.detectEnvironment = detectEnvironment;
|
|
46972
49039
|
exports.detectIdentifierType = detectIdentifierType;
|
|
46973
49040
|
exports.environment = environment;
|
|
46974
49041
|
exports.extractData = extractData;
|
|
46975
49042
|
exports.extractDeadlineFromSigningData = extractDeadlineFromSigningData;
|
|
49043
|
+
exports.extractIpfsCid = extractIpfsCid;
|
|
46976
49044
|
exports.extractPagination = extractPagination;
|
|
46977
49045
|
exports.fetchAllPages = fetchAllPages;
|
|
49046
|
+
exports.getAllBuiltInFieldDefinitions = getAllBuiltInFieldDefinitions;
|
|
49047
|
+
exports.getBuiltInFieldDefinition = getBuiltInFieldDefinition;
|
|
46978
49048
|
exports.getMetadataFromTokenUnitResponse = getMetadataFromTokenUnitResponse;
|
|
49049
|
+
exports.getOperatorText = getOperatorText;
|
|
49050
|
+
exports.getPresetDimensions = getPresetDimensions;
|
|
46979
49051
|
exports.getSigningUrl = getSigningUrl;
|
|
46980
49052
|
exports.globalCacheService = globalCacheService;
|
|
46981
49053
|
exports.hasMinimumRole = hasMinimumRole;
|
|
46982
49054
|
exports.initializeReactNativePolyfills = initializeReactNativePolyfills;
|
|
49055
|
+
exports.isBuiltInUserField = isBuiltInUserField;
|
|
46983
49056
|
exports.isConnectedMessage = isConnectedMessage;
|
|
46984
49057
|
exports.isErrorMessage = isErrorMessage;
|
|
46985
49058
|
exports.isEventMessage = isEventMessage;
|
|
46986
49059
|
exports.isFatalAuthErrorInMessage = isFatalAuthErrorInMessage;
|
|
49060
|
+
exports.isIpfsUrl = isIpfsUrl;
|
|
46987
49061
|
exports.isPaginatedResponse = isPaginatedResponse;
|
|
46988
49062
|
exports.isPingMessage = isPingMessage;
|
|
46989
49063
|
exports.isServerMessage = isServerMessage;
|
|
46990
49064
|
exports.isSubscribedMessage = isSubscribedMessage;
|
|
49065
|
+
exports.isSupportedImageFormat = isSupportedImageFormat;
|
|
49066
|
+
exports.isUUID = isUUID;
|
|
46991
49067
|
exports.isUnsubscribedMessage = isUnsubscribedMessage;
|
|
46992
49068
|
exports.isUserIdentifierObject = isUserIdentifierObject;
|
|
46993
49069
|
exports.isValidBusinessMembershipRelation = isValidBusinessMembershipRelation;
|
|
46994
49070
|
exports.isValidCampaignClaimRelation = isValidCampaignClaimRelation;
|
|
46995
49071
|
exports.isValidCampaignRelation = isValidCampaignRelation;
|
|
49072
|
+
exports.isValidEmail = isValidEmail;
|
|
49073
|
+
exports.isValidNonEmptyString = isValidNonEmptyString;
|
|
49074
|
+
exports.isValidPreset = isValidPreset;
|
|
46996
49075
|
exports.isValidRedemptionRedeemRelation = isValidRedemptionRedeemRelation;
|
|
46997
49076
|
exports.isValidRedemptionRelation = isValidRedemptionRelation;
|
|
46998
49077
|
exports.isValidRelation = isValidRelation;
|
|
46999
49078
|
exports.isValidTokenMetadataRelation = isValidTokenMetadataRelation;
|
|
47000
49079
|
exports.isValidTransactionRelation = isValidTransactionRelation;
|
|
49080
|
+
exports.isValidUrl = isValidUrl;
|
|
47001
49081
|
exports.isValidUserRelation = isValidUserRelation;
|
|
49082
|
+
exports.isValidUuid = isValidUuid;
|
|
49083
|
+
exports.isValidWalletAddress = isValidWalletAddress;
|
|
47002
49084
|
exports.mergeWithDefaults = mergeWithDefaults;
|
|
47003
49085
|
exports.needsExternalSigning = needsExternalSigning;
|
|
47004
49086
|
exports.needsSubmission = needsSubmission;
|
|
47005
49087
|
exports.normalizeFilters = normalizeFilters;
|
|
47006
49088
|
exports.normalizeToPaginated = normalizeToPaginated;
|
|
49089
|
+
exports.operatorText = operatorText;
|
|
47007
49090
|
exports.registerIdentifierType = registerIdentifierType;
|
|
47008
49091
|
exports.testnetPrefix = testnetPrefix;
|
|
47009
49092
|
exports.testnetShortPrefix = testnetShortPrefix;
|
|
@@ -47031,5 +49114,10 @@ exports.useTriggerSources = useTriggerSources;
|
|
|
47031
49114
|
exports.useUserStatus = useUserStatus;
|
|
47032
49115
|
exports.useUsers = useUsers;
|
|
47033
49116
|
exports.useWeb3 = useWeb3;
|
|
49117
|
+
exports.userModelFields = userModelFields;
|
|
49118
|
+
exports.validateAllFields = validateAllFields;
|
|
49119
|
+
exports.validateFieldComparisons = validateFieldComparisons;
|
|
49120
|
+
exports.validateFieldValue = validateFieldValue;
|
|
49121
|
+
exports.validateObjectArrayItems = validateObjectArrayItems;
|
|
47034
49122
|
exports.warnIfProblematicEnvironment = warnIfProblematicEnvironment;
|
|
47035
49123
|
//# sourceMappingURL=index.js.map
|