@explorins/pers-sdk-react-native 2.2.2 → 2.3.0-alpha.2
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/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useBookings.d.ts +55 -0
- package/dist/hooks/useBookings.d.ts.map +1 -0
- package/dist/hooks/useBookings.js +153 -0
- package/dist/index.js +2743 -144
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/hooks/index.ts +5 -1
- package/src/hooks/useBookings.ts +191 -0
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,585 @@ 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 'phone':
|
|
3521
|
+
// Basic phone validation - allows digits, spaces, dashes, parentheses, plus
|
|
3522
|
+
const phoneRegex = /^[\d\s\-\(\)\+]+$/;
|
|
3523
|
+
if (!phoneRegex.test(stringValue)) {
|
|
3524
|
+
return { field: '', message: 'Invalid phone number format', code: 'phone' };
|
|
3525
|
+
}
|
|
3526
|
+
if (rules.minLength && stringValue.replace(/\D/g, '').length < rules.minLength) {
|
|
3527
|
+
return { field: '', message: `Phone number too short`, code: 'minLength' };
|
|
3528
|
+
}
|
|
3529
|
+
if (rules.maxLength && stringValue.length > rules.maxLength) {
|
|
3530
|
+
return { field: '', message: `Maximum ${rules.maxLength} characters allowed`, code: 'maxLength' };
|
|
3531
|
+
}
|
|
3532
|
+
break;
|
|
3533
|
+
case 'select':
|
|
3534
|
+
// Single selection - just validate it's a string (option validation done separately)
|
|
3535
|
+
if (typeof value !== 'string') {
|
|
3536
|
+
return { field: '', message: 'Must be a string', code: 'type' };
|
|
3537
|
+
}
|
|
3538
|
+
break;
|
|
3539
|
+
case 'multiselect':
|
|
3540
|
+
if (!Array.isArray(value)) {
|
|
3541
|
+
return { field: '', message: 'Must be an array', code: 'type' };
|
|
3542
|
+
}
|
|
3543
|
+
if (rules.minItems !== undefined && value.length < rules.minItems) {
|
|
3544
|
+
return { field: '', message: `Select at least ${rules.minItems} options`, code: 'minItems' };
|
|
3545
|
+
}
|
|
3546
|
+
if (rules.maxItems !== undefined && value.length > rules.maxItems) {
|
|
3547
|
+
return { field: '', message: `Select at most ${rules.maxItems} options`, code: 'maxItems' };
|
|
3548
|
+
}
|
|
3549
|
+
// Validate each item is a string
|
|
3550
|
+
if (!value.every(item => typeof item === 'string')) {
|
|
3551
|
+
return { field: '', message: 'All selections must be strings', code: 'type' };
|
|
3552
|
+
}
|
|
3553
|
+
break;
|
|
3554
|
+
}
|
|
3555
|
+
return null;
|
|
3556
|
+
};
|
|
3557
|
+
// ============================================================================
|
|
3558
|
+
// Cross-Field / Literal Comparison Validation
|
|
3559
|
+
// ============================================================================
|
|
3560
|
+
/** Type guard for field comparison */
|
|
3561
|
+
const isFieldComparison = (c) => 'field' in c && c.field !== undefined;
|
|
3562
|
+
/**
|
|
3563
|
+
* Validate comparison rules for a single field.
|
|
3564
|
+
* Supports comparing to another field OR a literal value.
|
|
3565
|
+
*/
|
|
3566
|
+
const validateFieldComparisons = (fieldKey, value, comparisons, allData, fieldType) => {
|
|
3567
|
+
if (!comparisons || comparisons.length === 0)
|
|
3568
|
+
return [];
|
|
3569
|
+
const errors = [];
|
|
3570
|
+
for (const comparison of comparisons) {
|
|
3571
|
+
// Skip if this field's value is empty
|
|
3572
|
+
if (value === null || value === undefined || value === '')
|
|
3573
|
+
continue;
|
|
3574
|
+
// Get the comparison target (field value or literal)
|
|
3575
|
+
const isField = isFieldComparison(comparison);
|
|
3576
|
+
const targetValue = isField ? allData[comparison.field] : comparison.value;
|
|
3577
|
+
const targetLabel = isField ? comparison.field : String(comparison.value);
|
|
3578
|
+
// Skip if comparing to field and that field is empty
|
|
3579
|
+
if (isField && (targetValue === null || targetValue === undefined || targetValue === ''))
|
|
3580
|
+
continue;
|
|
3581
|
+
let isValid = true;
|
|
3582
|
+
const { operator, message } = comparison;
|
|
3583
|
+
switch (fieldType) {
|
|
3584
|
+
case 'date': {
|
|
3585
|
+
const thisDate = new Date(String(value));
|
|
3586
|
+
const otherDate = new Date(String(targetValue));
|
|
3587
|
+
if (isNaN(thisDate.getTime()) || isNaN(otherDate.getTime()))
|
|
3588
|
+
continue;
|
|
3589
|
+
isValid = compareValues(thisDate.getTime(), otherDate.getTime(), operator);
|
|
3590
|
+
break;
|
|
3591
|
+
}
|
|
3592
|
+
case 'number': {
|
|
3593
|
+
const thisNum = typeof value === 'number' ? value : parseFloat(String(value));
|
|
3594
|
+
const otherNum = typeof targetValue === 'number' ? targetValue : parseFloat(String(targetValue));
|
|
3595
|
+
if (isNaN(thisNum) || isNaN(otherNum))
|
|
3596
|
+
continue;
|
|
3597
|
+
isValid = compareValues(thisNum, otherNum, operator);
|
|
3598
|
+
break;
|
|
3599
|
+
}
|
|
3600
|
+
case 'boolean': {
|
|
3601
|
+
const thisBool = typeof value === 'boolean' ? value : value === 'true';
|
|
3602
|
+
const otherBool = typeof targetValue === 'boolean' ? targetValue : targetValue === 'true';
|
|
3603
|
+
isValid = operator === '==' ? thisBool === otherBool :
|
|
3604
|
+
operator === '!=' ? thisBool !== otherBool : true;
|
|
3605
|
+
break;
|
|
3606
|
+
}
|
|
3607
|
+
default: {
|
|
3608
|
+
isValid = compareValues(String(value), String(targetValue), operator);
|
|
3609
|
+
break;
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
if (!isValid) {
|
|
3613
|
+
errors.push({
|
|
3614
|
+
field: fieldKey,
|
|
3615
|
+
message: message || `Must be ${getOperatorText(operator)} ${targetLabel}`,
|
|
3616
|
+
code: `comparison:${operator}:${targetLabel}`,
|
|
3617
|
+
});
|
|
3618
|
+
}
|
|
3619
|
+
}
|
|
3620
|
+
return errors;
|
|
3621
|
+
};
|
|
3622
|
+
// ============================================================================
|
|
3623
|
+
// Full Data Validation
|
|
3624
|
+
// ============================================================================
|
|
3625
|
+
/**
|
|
3626
|
+
* Validate all fields in a data object against their definitions.
|
|
3627
|
+
* Runs both single-field and comparison validations.
|
|
3628
|
+
*
|
|
3629
|
+
* @param data - The data object to validate
|
|
3630
|
+
* @param definitions - Field definitions with validation rules
|
|
3631
|
+
* @returns Array of all validation errors
|
|
3632
|
+
*/
|
|
3633
|
+
const validateAllFields = (data, definitions) => {
|
|
3634
|
+
const errors = [];
|
|
3635
|
+
const definitionMap = new Map(definitions.map(d => [d.key, d]));
|
|
3636
|
+
for (const [key, value] of Object.entries(data)) {
|
|
3637
|
+
const def = definitionMap.get(key);
|
|
3638
|
+
if (!def)
|
|
3639
|
+
continue;
|
|
3640
|
+
// Single-field validation
|
|
3641
|
+
const error = validateFieldValue(value, def.validation, def.fieldType);
|
|
3642
|
+
if (error) {
|
|
3643
|
+
errors.push({ ...error, field: key });
|
|
3644
|
+
}
|
|
3645
|
+
// Comparison validation
|
|
3646
|
+
if (def.validation?.comparisons) {
|
|
3647
|
+
const comparisonErrors = validateFieldComparisons(key, value, def.validation.comparisons, data, def.fieldType);
|
|
3648
|
+
errors.push(...comparisonErrors);
|
|
3649
|
+
}
|
|
3650
|
+
}
|
|
3651
|
+
return errors;
|
|
3652
|
+
};
|
|
3653
|
+
|
|
3654
|
+
/**
|
|
3655
|
+
* Built-in user profile fields that can be required for redemptions.
|
|
3656
|
+
* These are a subset of UserModelField - excludes 'id' and 'identifierEmail'
|
|
3657
|
+
* since those are system-managed, not user-fillable profile fields.
|
|
3658
|
+
*
|
|
3659
|
+
* Used for:
|
|
3660
|
+
* - RedemptionType base required fields (RedemptionType.baseRequiredUserFields)
|
|
3661
|
+
* - Redemption specific required fields (Redemption.specificRequiredUserFields)
|
|
3662
|
+
* - User profile validation before redemption claims
|
|
3663
|
+
* - Form generation for missing field prompts
|
|
3664
|
+
*/
|
|
3665
|
+
// Fields that can be required for user profile validation (excludes system fields)
|
|
3666
|
+
const builtInUserFieldsArray = [
|
|
3667
|
+
'email',
|
|
3668
|
+
'firstName',
|
|
3669
|
+
'lastName',
|
|
3670
|
+
'externalId',
|
|
3671
|
+
'instagramAccountId',
|
|
3672
|
+
'googleAccountName',
|
|
3673
|
+
];
|
|
3674
|
+
// Export the array for iteration
|
|
3675
|
+
const builtInUserFields = [...builtInUserFieldsArray];
|
|
3676
|
+
/**
|
|
3677
|
+
* Static metadata mapping for all built-in user fields.
|
|
3678
|
+
* Used by validation services and UI form generation.
|
|
3679
|
+
*/
|
|
3680
|
+
const builtInUserFieldMetadata = {
|
|
3681
|
+
email: {
|
|
3682
|
+
label: 'Email Address',
|
|
3683
|
+
description: 'User email address',
|
|
3684
|
+
fieldType: 'email',
|
|
3685
|
+
},
|
|
3686
|
+
firstName: {
|
|
3687
|
+
label: 'First Name',
|
|
3688
|
+
description: 'User first name',
|
|
3689
|
+
fieldType: 'text',
|
|
3690
|
+
},
|
|
3691
|
+
lastName: {
|
|
3692
|
+
label: 'Last Name',
|
|
3693
|
+
description: 'User last name',
|
|
3694
|
+
fieldType: 'text',
|
|
3695
|
+
},
|
|
3696
|
+
externalId: {
|
|
3697
|
+
label: 'External ID',
|
|
3698
|
+
description: 'External identifier from third-party system',
|
|
3699
|
+
fieldType: 'text',
|
|
3700
|
+
},
|
|
3701
|
+
instagramAccountId: {
|
|
3702
|
+
label: 'Instagram Account',
|
|
3703
|
+
description: 'Instagram account ID',
|
|
3704
|
+
fieldType: 'text',
|
|
3705
|
+
},
|
|
3706
|
+
googleAccountName: {
|
|
3707
|
+
label: 'Google Account',
|
|
3708
|
+
description: 'Google account name',
|
|
3709
|
+
fieldType: 'text',
|
|
3710
|
+
},
|
|
3711
|
+
};
|
|
3712
|
+
/**
|
|
3713
|
+
* Check if a string is a valid built-in user field
|
|
3714
|
+
*/
|
|
3715
|
+
const isBuiltInUserField = (value) => {
|
|
3716
|
+
return builtInUserFields.includes(value);
|
|
3717
|
+
};
|
|
3718
|
+
const getBuiltInFieldDefinition = (field) => {
|
|
3719
|
+
if (!isBuiltInUserField(field)) {
|
|
3720
|
+
return null;
|
|
3721
|
+
}
|
|
3722
|
+
const metadata = builtInUserFieldMetadata[field];
|
|
3723
|
+
const sortOrder = builtInUserFields.indexOf(field);
|
|
3724
|
+
return {
|
|
3725
|
+
id: `builtin:${field}`,
|
|
3726
|
+
tenantId: null,
|
|
3727
|
+
entityType: 'user',
|
|
3728
|
+
key: field,
|
|
3729
|
+
label: metadata.label,
|
|
3730
|
+
description: metadata.description,
|
|
3731
|
+
fieldType: metadata.fieldType,
|
|
3732
|
+
selectOptions: null,
|
|
3733
|
+
selectOptionsSource: null,
|
|
3734
|
+
validation: null,
|
|
3735
|
+
sortOrder,
|
|
3736
|
+
createdAt: null,
|
|
3737
|
+
updatedAt: null,
|
|
3738
|
+
isBuiltIn: true,
|
|
3739
|
+
};
|
|
3740
|
+
};
|
|
3741
|
+
/**
|
|
3742
|
+
* Get all built-in field definitions as an array.
|
|
3743
|
+
* Useful for populating field definitions list.
|
|
3744
|
+
*/
|
|
3745
|
+
const getAllBuiltInFieldDefinitions = () => {
|
|
3746
|
+
return builtInUserFields.map(field => getBuiltInFieldDefinition(field));
|
|
3747
|
+
};
|
|
3748
|
+
/**
|
|
3749
|
+
* Entity types for custom fields and dynamic select options.
|
|
3750
|
+
* Used for both field definitions (entityType) and select option sources.
|
|
3751
|
+
* - 'user': User profile fields (stored in user.customData)
|
|
3752
|
+
* - 'business': Business profile fields (stored in business.customData)
|
|
3753
|
+
* - 'campaign': Campaign-specific custom fields
|
|
3754
|
+
* - 'redemption': Redemption-specific custom fields
|
|
3755
|
+
*/
|
|
3756
|
+
const customFieldEntityTypesArray = ['user', 'business', 'campaign', 'redemption'];
|
|
3757
|
+
/** All supported custom field entity types for iteration/validation */
|
|
3758
|
+
const customFieldEntityTypes = [...customFieldEntityTypesArray];
|
|
3759
|
+
/**
|
|
3760
|
+
* Entity types available for select options source (includes static sources)
|
|
3761
|
+
* - Database entities: user, business, campaign, redemption
|
|
3762
|
+
* - Static sources: country (ISO 3166-1 country list)
|
|
3763
|
+
*/
|
|
3764
|
+
const selectOptionsSourceEntityArray = [...customFieldEntityTypesArray, 'country'];
|
|
3765
|
+
/** All supported select options source entities for iteration/validation */
|
|
3766
|
+
const selectOptionsSourceEntities = [...selectOptionsSourceEntityArray];
|
|
3767
|
+
|
|
3187
3768
|
/**
|
|
3188
3769
|
* Core Entity Types
|
|
3189
3770
|
*
|
|
@@ -3346,119 +3927,17 @@ function isPingMessage(msg) {
|
|
|
3346
3927
|
return msg.type === 'ping';
|
|
3347
3928
|
}
|
|
3348
3929
|
|
|
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
|
-
/**
|
|
3387
|
-
* Register a new identifier type configuration
|
|
3388
|
-
* @param key The identifier key
|
|
3389
|
-
* @param config The identifier configuration (without the key)
|
|
3390
|
-
*/
|
|
3391
|
-
const registerIdentifierType = (key, config) => {
|
|
3392
|
-
if (!uniqueUserIdentifierKeysArray.includes(key)) {
|
|
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
3930
|
/**
|
|
3431
|
-
*
|
|
3432
|
-
* @param params Partial parameters for the identifier object
|
|
3433
|
-
* @returns A properly formed UserIdentifierObject
|
|
3931
|
+
* Check if a string matches UUID format
|
|
3434
3932
|
*/
|
|
3435
|
-
const
|
|
3436
|
-
|
|
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;
|
|
3933
|
+
const isUUID = (value) => {
|
|
3934
|
+
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
3935
|
};
|
|
3461
3936
|
|
|
3937
|
+
const apiPublicKeyTestPrefix = 'pk_test_';
|
|
3938
|
+
const testnetPrefix = 'testnet-';
|
|
3939
|
+
const testnetShortPrefix = 'tn-';
|
|
3940
|
+
|
|
3462
3941
|
// Use this pattern:
|
|
3463
3942
|
const NativeTokenTypes = {
|
|
3464
3943
|
ERC20: 'ERC20', // default; should be used for fungible tokens eg. credit tokens; There should be only one active ERC20 token per programme
|
|
@@ -3599,7 +4078,9 @@ function isValidCampaignClaimRelation(value) {
|
|
|
3599
4078
|
*/
|
|
3600
4079
|
const VALID_REDEMPTION_RELATIONS = [
|
|
3601
4080
|
'redeemCount',
|
|
3602
|
-
'ownerBusiness'
|
|
4081
|
+
'ownerBusiness',
|
|
4082
|
+
'requiredUserInfo',
|
|
4083
|
+
'booking'
|
|
3603
4084
|
];
|
|
3604
4085
|
/**
|
|
3605
4086
|
* Type guard to validate redemption include relation at runtime
|
|
@@ -3697,6 +4178,216 @@ function normalizeFilters(filters) {
|
|
|
3697
4178
|
};
|
|
3698
4179
|
}
|
|
3699
4180
|
|
|
4181
|
+
/**
|
|
4182
|
+
* Location types for bookings.
|
|
4183
|
+
* Broad categories for different booking contexts.
|
|
4184
|
+
*/
|
|
4185
|
+
const LOCATION_TYPES = [
|
|
4186
|
+
'property', // Physical property (hotel, resort, vacation rental)
|
|
4187
|
+
'service', // Service-based booking (spa, salon, restaurant)
|
|
4188
|
+
'event', // Event/activity booking (concert, tour, class)
|
|
4189
|
+
'experience', // Experience package (multi-activity, bundle)
|
|
4190
|
+
'destination', // Destination/location-based (city pass, area access)
|
|
4191
|
+
];
|
|
4192
|
+
|
|
4193
|
+
/**
|
|
4194
|
+
* Unified data source tracking for analytics across all entities.
|
|
4195
|
+
*
|
|
4196
|
+
* Use this type to track the origin/channel of data for:
|
|
4197
|
+
* - Users (registration source)
|
|
4198
|
+
* - Claims (how the claim was initiated)
|
|
4199
|
+
* - Redeems (redemption channel)
|
|
4200
|
+
* - Bookings (booking source)
|
|
4201
|
+
* - Purchases (transaction origin)
|
|
4202
|
+
*
|
|
4203
|
+
* ## HTTP Headers for SDK/API
|
|
4204
|
+
*
|
|
4205
|
+
* | Header | Purpose | Examples |
|
|
4206
|
+
* |---------------------|----------------------------|-------------------------------------------------------|
|
|
4207
|
+
* | `x-source-channel` | How the request arrived | `web`, `mobile`, `api`, `admin`, `webhook`, `import` |
|
|
4208
|
+
* | `x-source-medium` | Marketing channel type | `email`, `referral`, `direct`, `organic`, `paid` |
|
|
4209
|
+
* | `x-source-campaign` | Campaign ID for attribution| `summer_promo_2026`, `welcome_series`, `partner_launch`|
|
|
4210
|
+
* | `x-source` | Traffic origin reference | `google`, `facebook`, `partner_xyz`, `qr_code_lobby` |
|
|
4211
|
+
*
|
|
4212
|
+
* ## Example Scenarios
|
|
4213
|
+
*
|
|
4214
|
+
* | Scenario | channel | medium | campaign | source |
|
|
4215
|
+
* |-----------------------------|-----------|------------|--------------|--------------|
|
|
4216
|
+
* | User clicks email link | `web` | `email` | `summer_promo`| `mailchimp` |
|
|
4217
|
+
* | Mobile app registration | `mobile` | `app` | - | - |
|
|
4218
|
+
* | Partner API integration | `api` | `affiliate`| - | `partner_xyz`|
|
|
4219
|
+
* | QR code scan in hotel | `mobile` | `referral` | `lobby_qr` | `qr_code` |
|
|
4220
|
+
* | Admin creates user | `admin` | - | - | - |
|
|
4221
|
+
* | Webhook from Stripe | `webhook` | - | - | `stripe` |
|
|
4222
|
+
*
|
|
4223
|
+
* @example
|
|
4224
|
+
* // Simple usage - just channel
|
|
4225
|
+
* const source: DataSource = { channel: 'web' };
|
|
4226
|
+
*
|
|
4227
|
+
* // Full attribution tracking
|
|
4228
|
+
* const source: DataSource = {
|
|
4229
|
+
* channel: 'web',
|
|
4230
|
+
* medium: 'email',
|
|
4231
|
+
* campaign: 'summer_promo_2026',
|
|
4232
|
+
* source: 'mailchimp',
|
|
4233
|
+
* referrer: 'https://email.example.com/campaign/123'
|
|
4234
|
+
* };
|
|
4235
|
+
*/
|
|
4236
|
+
/**
|
|
4237
|
+
* Primary ingestion channels for data source tracking.
|
|
4238
|
+
*/
|
|
4239
|
+
const DATA_SOURCE_CHANNELS = [
|
|
4240
|
+
'web', // Web application/browser
|
|
4241
|
+
'mobile', // Mobile app (iOS/Android)
|
|
4242
|
+
'api', // Direct API integration
|
|
4243
|
+
'admin', // Admin panel/back-office
|
|
4244
|
+
'system', // System-generated (automated)
|
|
4245
|
+
'import', // Bulk import/CSV upload
|
|
4246
|
+
'webhook', // External webhook callback
|
|
4247
|
+
'sync', // Synchronization from external system
|
|
4248
|
+
'migration', // Data migration
|
|
4249
|
+
];
|
|
4250
|
+
/**
|
|
4251
|
+
* Type guard to check if a value is a valid DataSourceChannel.
|
|
4252
|
+
*/
|
|
4253
|
+
function isDataSourceChannel(value) {
|
|
4254
|
+
return typeof value === 'string' && DATA_SOURCE_CHANNELS.includes(value);
|
|
4255
|
+
}
|
|
4256
|
+
/**
|
|
4257
|
+
* Default data source for manual/admin operations.
|
|
4258
|
+
*/
|
|
4259
|
+
const DEFAULT_DATA_SOURCE = {
|
|
4260
|
+
channel: 'admin',
|
|
4261
|
+
};
|
|
4262
|
+
|
|
4263
|
+
/**
|
|
4264
|
+
* Analytics Constants
|
|
4265
|
+
*
|
|
4266
|
+
* Shared definitions for consistent analytics across frontend and backend.
|
|
4267
|
+
*/
|
|
4268
|
+
/**
|
|
4269
|
+
* Active User Definition
|
|
4270
|
+
*
|
|
4271
|
+
* A user qualifies as "active" in a given period if they performed
|
|
4272
|
+
* any of the qualifying actions during that period.
|
|
4273
|
+
*
|
|
4274
|
+
* @example
|
|
4275
|
+
* // Display in UI
|
|
4276
|
+
* `${engagementRate}% engagement. ${ACTIVE_USER_DEFINITION.description}`
|
|
4277
|
+
*
|
|
4278
|
+
* // Result: "25.5% engagement. Active = sent transaction, claimed, or redeemed in period"
|
|
4279
|
+
*/
|
|
4280
|
+
const ACTIVE_USER_DEFINITION = {
|
|
4281
|
+
/** Actions that qualify a user as "active" */
|
|
4282
|
+
qualifyingActions: ['TRANSACTION_SEND', 'CLAIM_COMPLETED', 'REDEMPTION_COMPLETED'],
|
|
4283
|
+
/** Short description for UI display */
|
|
4284
|
+
description: 'Active = sent transaction, claimed, or redeemed in period',
|
|
4285
|
+
/** Detailed explanation for tooltips/documentation */
|
|
4286
|
+
details: {
|
|
4287
|
+
TRANSACTION_SEND: 'User initiated a transaction (spending, transferring, or purchasing)',
|
|
4288
|
+
CLAIM_COMPLETED: 'User successfully claimed a campaign reward',
|
|
4289
|
+
REDEMPTION_COMPLETED: 'User successfully redeemed an offer',
|
|
4290
|
+
},
|
|
4291
|
+
/** What does NOT count as active */
|
|
4292
|
+
excluded: [
|
|
4293
|
+
'Receiving tokens (passive)',
|
|
4294
|
+
'Failed or pending claims/redemptions',
|
|
4295
|
+
'System-generated transactions (REDEMPTION_RECEIVE)',
|
|
4296
|
+
],
|
|
4297
|
+
};
|
|
4298
|
+
/**
|
|
4299
|
+
* Engagement rate formula
|
|
4300
|
+
* engagementRate = (activeUsers / totalUsers) × 100
|
|
4301
|
+
*/
|
|
4302
|
+
const ENGAGEMENT_RATE_FORMULA = 'activeUsers / totalUsers × 100';
|
|
4303
|
+
/**
|
|
4304
|
+
* Transaction statuses that count for analytics
|
|
4305
|
+
*/
|
|
4306
|
+
const ANALYTICS_VALID_TRANSACTION_STATUSES = ['SUCCEEDED', 'BROADCASTED'];
|
|
4307
|
+
/**
|
|
4308
|
+
* Process statuses that count for analytics (claims, redemptions)
|
|
4309
|
+
*/
|
|
4310
|
+
const ANALYTICS_VALID_PROCESS_STATUS = 'COMPLETED';
|
|
4311
|
+
|
|
4312
|
+
/**
|
|
4313
|
+
* Image CDN Configuration
|
|
4314
|
+
*
|
|
4315
|
+
* Shared constants and types for Image CDN (CloudFront + Lambda@Edge)
|
|
4316
|
+
* Used by backend Lambda handler and frontend SDKs.
|
|
4317
|
+
*
|
|
4318
|
+
* @see documentation/api-docs/image-cdn.md
|
|
4319
|
+
*/
|
|
4320
|
+
// ============================================
|
|
4321
|
+
// CONSTANTS
|
|
4322
|
+
// ============================================
|
|
4323
|
+
/** Maximum output dimension when no preset specified */
|
|
4324
|
+
const IMAGE_CDN_MAX_DIMENSION = 2048;
|
|
4325
|
+
/** Default quality percentage */
|
|
4326
|
+
const IMAGE_CDN_DEFAULT_QUALITY = 80;
|
|
4327
|
+
/** Default fit mode */
|
|
4328
|
+
const IMAGE_CDN_DEFAULT_FIT = 'inside';
|
|
4329
|
+
/** Default format (auto-detect based on browser Accept header) */
|
|
4330
|
+
const IMAGE_CDN_DEFAULT_FORMAT = 'auto';
|
|
4331
|
+
/** Available presets with their dimensions */
|
|
4332
|
+
const IMAGE_CDN_PRESETS = {
|
|
4333
|
+
thumb: { width: 150, height: 150 },
|
|
4334
|
+
medium: { width: 600, height: 600 },
|
|
4335
|
+
large: { width: 1200, height: 1200 },
|
|
4336
|
+
hero: { width: 1920, height: 800 },
|
|
4337
|
+
avatar: { width: 200, height: 200 },
|
|
4338
|
+
card: { width: 400, height: 300 },
|
|
4339
|
+
};
|
|
4340
|
+
/** Supported input/output formats */
|
|
4341
|
+
const IMAGE_CDN_SUPPORTED_FORMATS = ['jpeg', 'jpg', 'png', 'webp', 'gif', 'avif'];
|
|
4342
|
+
/** Cache TTL in seconds (1 year) */
|
|
4343
|
+
const IMAGE_CDN_CACHE_TTL = 31536000;
|
|
4344
|
+
/** Maximum input file size in bytes (10MB) */
|
|
4345
|
+
const IMAGE_CDN_MAX_INPUT_SIZE = 10 * 1024 * 1024;
|
|
4346
|
+
// ============================================
|
|
4347
|
+
// HELPER FUNCTIONS
|
|
4348
|
+
// ============================================
|
|
4349
|
+
/**
|
|
4350
|
+
* Build CDN URL with options
|
|
4351
|
+
*
|
|
4352
|
+
* @example
|
|
4353
|
+
* buildImageCdnUrl('https://cdn.example.com/photo.jpg', { preset: 'thumb' })
|
|
4354
|
+
* // => 'https://cdn.example.com/photo.jpg?preset=thumb'
|
|
4355
|
+
*
|
|
4356
|
+
* buildImageCdnUrl('https://cdn.example.com/photo.jpg', { preset: 'large', q: 95 })
|
|
4357
|
+
* // => 'https://cdn.example.com/photo.jpg?preset=large&q=95'
|
|
4358
|
+
*/
|
|
4359
|
+
function buildImageCdnUrl(baseUrl, options = {}) {
|
|
4360
|
+
const params = new URLSearchParams();
|
|
4361
|
+
if (options.preset)
|
|
4362
|
+
params.set('preset', options.preset);
|
|
4363
|
+
if (options.q !== undefined)
|
|
4364
|
+
params.set('q', options.q.toString());
|
|
4365
|
+
if (options.f)
|
|
4366
|
+
params.set('f', options.f);
|
|
4367
|
+
if (options.fit)
|
|
4368
|
+
params.set('fit', options.fit);
|
|
4369
|
+
const query = params.toString();
|
|
4370
|
+
return query ? `${baseUrl}?${query}` : baseUrl;
|
|
4371
|
+
}
|
|
4372
|
+
/**
|
|
4373
|
+
* Check if a preset name is valid
|
|
4374
|
+
*/
|
|
4375
|
+
function isValidPreset(preset) {
|
|
4376
|
+
return preset in IMAGE_CDN_PRESETS;
|
|
4377
|
+
}
|
|
4378
|
+
/**
|
|
4379
|
+
* Get dimensions for a preset
|
|
4380
|
+
*/
|
|
4381
|
+
function getPresetDimensions(preset) {
|
|
4382
|
+
return IMAGE_CDN_PRESETS[preset];
|
|
4383
|
+
}
|
|
4384
|
+
/**
|
|
4385
|
+
* Check if a file extension is a supported image format
|
|
4386
|
+
*/
|
|
4387
|
+
function isSupportedImageFormat(extension) {
|
|
4388
|
+
return IMAGE_CDN_SUPPORTED_FORMATS.includes(extension.toLowerCase());
|
|
4389
|
+
}
|
|
4390
|
+
|
|
3700
4391
|
/**
|
|
3701
4392
|
* Chain Data Types
|
|
3702
4393
|
*
|
|
@@ -3831,6 +4522,12 @@ const CommonErrorCodes = {
|
|
|
3831
4522
|
FIREBASE_AUTH_ERROR: 'FIREBASE_AUTH_ERROR',
|
|
3832
4523
|
MULTIPLE_CONTEXT_SELECTION_REQUIRED: 'MULTIPLE_CONTEXT_SELECTION_REQUIRED',
|
|
3833
4524
|
ADMIN_TENANT_REQUIRED: 'ADMIN_TENANT_REQUIRED',
|
|
4525
|
+
// Magic link errors
|
|
4526
|
+
MAGIC_LINK_EMAIL_SEND_FAILED: 'MAGIC_LINK_EMAIL_SEND_FAILED',
|
|
4527
|
+
MAGIC_LINK_EMAIL_MISMATCH: 'MAGIC_LINK_EMAIL_MISMATCH',
|
|
4528
|
+
MAGIC_LINK_VERIFICATION_FAILED: 'MAGIC_LINK_VERIFICATION_FAILED',
|
|
4529
|
+
// Rate limiting (generic)
|
|
4530
|
+
RATE_LIMITED: 'RATE_LIMITED',
|
|
3834
4531
|
// Generic fallbacks
|
|
3835
4532
|
INTERNAL_ERROR: 'INTERNAL_ERROR',
|
|
3836
4533
|
VALIDATION_ERROR: 'VALIDATION_ERROR',
|
|
@@ -10999,7 +11696,7 @@ class DefaultAuthProvider {
|
|
|
10999
11696
|
/** SDK package name */
|
|
11000
11697
|
const SDK_NAME = "@explorins/pers-sdk";
|
|
11001
11698
|
/** SDK version - injected from package.json at build time */
|
|
11002
|
-
const SDK_VERSION = "2.
|
|
11699
|
+
const SDK_VERSION = "2.2.0-alpha.2";
|
|
11003
11700
|
/** Full SDK identifier for headers */
|
|
11004
11701
|
const SDK_USER_AGENT = `${SDK_NAME}/${SDK_VERSION}`;
|
|
11005
11702
|
|
|
@@ -11335,6 +12032,24 @@ class PersApiClient {
|
|
|
11335
12032
|
else if (this.mergedConfig.apiProjectKey) {
|
|
11336
12033
|
headers['x-project-key'] = this.mergedConfig.apiProjectKey;
|
|
11337
12034
|
}
|
|
12035
|
+
// Add data source tracking headers
|
|
12036
|
+
const dataSource = this.mergedConfig.dataSource;
|
|
12037
|
+
if (dataSource) {
|
|
12038
|
+
headers['x-source-channel'] = dataSource.channel;
|
|
12039
|
+
if (dataSource.medium) {
|
|
12040
|
+
headers['x-source-medium'] = dataSource.medium;
|
|
12041
|
+
}
|
|
12042
|
+
if (dataSource.campaign) {
|
|
12043
|
+
headers['x-source-campaign'] = dataSource.campaign;
|
|
12044
|
+
}
|
|
12045
|
+
if (dataSource.source) {
|
|
12046
|
+
headers['x-source'] = dataSource.source;
|
|
12047
|
+
}
|
|
12048
|
+
// Note: referrer is typically set by browser, but can be overridden
|
|
12049
|
+
if (dataSource.referrer) {
|
|
12050
|
+
headers['Referer'] = dataSource.referrer;
|
|
12051
|
+
}
|
|
12052
|
+
}
|
|
11338
12053
|
return headers;
|
|
11339
12054
|
}
|
|
11340
12055
|
// ==========================================
|
|
@@ -12715,10 +13430,11 @@ class UserStatusManager {
|
|
|
12715
13430
|
* ```
|
|
12716
13431
|
*/
|
|
12717
13432
|
class TokenManager {
|
|
12718
|
-
constructor(apiClient) {
|
|
13433
|
+
constructor(apiClient, eventEmitter) {
|
|
12719
13434
|
this.apiClient = apiClient;
|
|
12720
13435
|
const tokenApi = new TokenApi(apiClient);
|
|
12721
13436
|
this.tokenService = new TokenService(tokenApi);
|
|
13437
|
+
this.eventEmitter = eventEmitter;
|
|
12722
13438
|
}
|
|
12723
13439
|
/**
|
|
12724
13440
|
* Get all available tokens
|
|
@@ -19519,6 +20235,835 @@ class WalletEventsManager {
|
|
|
19519
20235
|
}
|
|
19520
20236
|
}
|
|
19521
20237
|
|
|
20238
|
+
/**
|
|
20239
|
+
* Custom Field Definitions API
|
|
20240
|
+
*
|
|
20241
|
+
* Low-level API for custom field definition CRUD operations.
|
|
20242
|
+
* Used internally by CustomFieldDefinitionManager.
|
|
20243
|
+
*
|
|
20244
|
+
* @internal
|
|
20245
|
+
*/
|
|
20246
|
+
class CustomFieldApi {
|
|
20247
|
+
constructor(apiClient) {
|
|
20248
|
+
this.apiClient = apiClient;
|
|
20249
|
+
this.basePath = '/custom-field-definitions';
|
|
20250
|
+
}
|
|
20251
|
+
/**
|
|
20252
|
+
* List all custom field definitions
|
|
20253
|
+
* Backend returns non-paginated array sorted by sortOrder
|
|
20254
|
+
*/
|
|
20255
|
+
async getDefinitions(entityType) {
|
|
20256
|
+
const params = new URLSearchParams();
|
|
20257
|
+
if (entityType) {
|
|
20258
|
+
params.append('entityType', entityType);
|
|
20259
|
+
}
|
|
20260
|
+
const query = params.toString();
|
|
20261
|
+
const url = query ? `${this.basePath}?${query}` : this.basePath;
|
|
20262
|
+
return this.apiClient.get(url);
|
|
20263
|
+
}
|
|
20264
|
+
/**
|
|
20265
|
+
* Get a single custom field definition by ID
|
|
20266
|
+
*/
|
|
20267
|
+
async getDefinition(id) {
|
|
20268
|
+
return this.apiClient.get(`${this.basePath}/${id}`);
|
|
20269
|
+
}
|
|
20270
|
+
/**
|
|
20271
|
+
* Create a new custom field definition
|
|
20272
|
+
*/
|
|
20273
|
+
async createDefinition(data) {
|
|
20274
|
+
return this.apiClient.post(this.basePath, data);
|
|
20275
|
+
}
|
|
20276
|
+
/**
|
|
20277
|
+
* Update an existing custom field definition
|
|
20278
|
+
*/
|
|
20279
|
+
async updateDefinition(id, data) {
|
|
20280
|
+
return this.apiClient.put(`${this.basePath}/${id}`, data);
|
|
20281
|
+
}
|
|
20282
|
+
/**
|
|
20283
|
+
* Delete a custom field definition
|
|
20284
|
+
*/
|
|
20285
|
+
async deleteDefinition(id) {
|
|
20286
|
+
return this.apiClient.delete(`${this.basePath}/${id}`);
|
|
20287
|
+
}
|
|
20288
|
+
/**
|
|
20289
|
+
* Get usage count for a field definition
|
|
20290
|
+
*/
|
|
20291
|
+
async getUsages(id) {
|
|
20292
|
+
return this.apiClient.get(`${this.basePath}/${id}/usages`);
|
|
20293
|
+
}
|
|
20294
|
+
/**
|
|
20295
|
+
* Resolve dynamic select options for a field
|
|
20296
|
+
*/
|
|
20297
|
+
async resolveSelectOptions(id) {
|
|
20298
|
+
return this.apiClient.get(`${this.basePath}/${id}/options`);
|
|
20299
|
+
}
|
|
20300
|
+
}
|
|
20301
|
+
|
|
20302
|
+
/**
|
|
20303
|
+
* Custom Field Definition Manager - Manage tenant-specific custom fields
|
|
20304
|
+
*
|
|
20305
|
+
* Provides CRUD operations for custom field definitions that extend the built-in
|
|
20306
|
+
* user profile fields. Custom fields are defined per-tenant and can be used for
|
|
20307
|
+
* additional user data collection, redemption requirements, and form validation.
|
|
20308
|
+
*
|
|
20309
|
+
* @group Managers
|
|
20310
|
+
* @category Custom Fields
|
|
20311
|
+
*
|
|
20312
|
+
* @example Basic Operations
|
|
20313
|
+
* ```typescript
|
|
20314
|
+
* // List all user custom fields
|
|
20315
|
+
* const fields = await sdk.customFields.getDefinitions();
|
|
20316
|
+
*
|
|
20317
|
+
* // Create a new custom field
|
|
20318
|
+
* const field = await sdk.customFields.createDefinition({
|
|
20319
|
+
* key: 'employee_id',
|
|
20320
|
+
* label: 'Employee ID',
|
|
20321
|
+
* fieldType: 'text',
|
|
20322
|
+
* validation: { required: true, pattern: '^E[0-9]{5}$' }
|
|
20323
|
+
* });
|
|
20324
|
+
*
|
|
20325
|
+
* // Update a field
|
|
20326
|
+
* await sdk.customFields.updateDefinition(field.id, {
|
|
20327
|
+
* label: 'Company Employee ID'
|
|
20328
|
+
* });
|
|
20329
|
+
*
|
|
20330
|
+
* // Delete a field
|
|
20331
|
+
* await sdk.customFields.deleteDefinition(field.id);
|
|
20332
|
+
* ```
|
|
20333
|
+
*
|
|
20334
|
+
* @example Validation
|
|
20335
|
+
* ```typescript
|
|
20336
|
+
* // Validate user custom data against definitions
|
|
20337
|
+
* const definitions = await sdk.customFields.getDefinitions();
|
|
20338
|
+
* const errors = sdk.customFields.validateUserData(
|
|
20339
|
+
* { employee_id: 'INVALID' },
|
|
20340
|
+
* definitions
|
|
20341
|
+
* );
|
|
20342
|
+
* if (errors.length > 0) {
|
|
20343
|
+
* console.log('Validation errors:', errors);
|
|
20344
|
+
* }
|
|
20345
|
+
* ```
|
|
20346
|
+
*/
|
|
20347
|
+
class CustomFieldDefinitionManager {
|
|
20348
|
+
constructor(apiClient, events) {
|
|
20349
|
+
this.events = events;
|
|
20350
|
+
this.api = new CustomFieldApi(apiClient);
|
|
20351
|
+
}
|
|
20352
|
+
/**
|
|
20353
|
+
* List all custom field definitions for the tenant
|
|
20354
|
+
*
|
|
20355
|
+
* Retrieves all custom field definitions, optionally filtered by entity type.
|
|
20356
|
+
* Results are sorted by sortOrder (ascending).
|
|
20357
|
+
*
|
|
20358
|
+
* @param options - Query options including entity type filter
|
|
20359
|
+
* @returns Array of custom field definitions
|
|
20360
|
+
*
|
|
20361
|
+
* @example List All User Fields
|
|
20362
|
+
* ```typescript
|
|
20363
|
+
* const fields = await sdk.customFields.getDefinitions();
|
|
20364
|
+
* console.log(`Found ${fields.length} custom fields`);
|
|
20365
|
+
*
|
|
20366
|
+
* fields.forEach(field => {
|
|
20367
|
+
* console.log(`${field.key}: ${field.label} (${field.fieldType})`);
|
|
20368
|
+
* });
|
|
20369
|
+
* ```
|
|
20370
|
+
*
|
|
20371
|
+
* @example Filter by Entity Type
|
|
20372
|
+
* ```typescript
|
|
20373
|
+
* // Get only business custom fields
|
|
20374
|
+
* const businessFields = await sdk.customFields.getDefinitions({
|
|
20375
|
+
* entityType: 'business'
|
|
20376
|
+
* });
|
|
20377
|
+
* ```
|
|
20378
|
+
*/
|
|
20379
|
+
async getDefinitions(options) {
|
|
20380
|
+
return this.api.getDefinitions(options?.entityType);
|
|
20381
|
+
}
|
|
20382
|
+
/**
|
|
20383
|
+
* Get a single custom field definition by ID
|
|
20384
|
+
*
|
|
20385
|
+
* @param id - The UUID of the custom field definition
|
|
20386
|
+
* @returns The custom field definition
|
|
20387
|
+
* @throws {PersApiError} When definition not found (404)
|
|
20388
|
+
*
|
|
20389
|
+
* @example
|
|
20390
|
+
* ```typescript
|
|
20391
|
+
* try {
|
|
20392
|
+
* const field = await sdk.customFields.getDefinition('uuid-here');
|
|
20393
|
+
* console.log('Field:', field.label);
|
|
20394
|
+
* } catch (error) {
|
|
20395
|
+
* if (error.statusCode === 404) {
|
|
20396
|
+
* console.log('Field not found');
|
|
20397
|
+
* }
|
|
20398
|
+
* }
|
|
20399
|
+
* ```
|
|
20400
|
+
*/
|
|
20401
|
+
async getDefinition(id) {
|
|
20402
|
+
return this.api.getDefinition(id);
|
|
20403
|
+
}
|
|
20404
|
+
/**
|
|
20405
|
+
* Create a new custom field definition
|
|
20406
|
+
*
|
|
20407
|
+
* Creates a new custom field for the tenant. The field key must be unique
|
|
20408
|
+
* within the tenant and entity type combination.
|
|
20409
|
+
*
|
|
20410
|
+
* @param data - The field definition data
|
|
20411
|
+
* @returns The created custom field definition
|
|
20412
|
+
* @throws {PersApiError} When key already exists (409) or validation fails (400)
|
|
20413
|
+
*
|
|
20414
|
+
* @example Text Field with Pattern
|
|
20415
|
+
* ```typescript
|
|
20416
|
+
* const employeeIdField = await sdk.customFields.createDefinition({
|
|
20417
|
+
* key: 'employee_id',
|
|
20418
|
+
* label: 'Employee ID',
|
|
20419
|
+
* description: 'Your company employee ID (E + 5 digits)',
|
|
20420
|
+
* fieldType: 'text',
|
|
20421
|
+
* validation: {
|
|
20422
|
+
* required: true,
|
|
20423
|
+
* pattern: '^E[0-9]{5}$',
|
|
20424
|
+
* patternMessage: 'Must be E followed by 5 digits'
|
|
20425
|
+
* },
|
|
20426
|
+
* sortOrder: 1
|
|
20427
|
+
* });
|
|
20428
|
+
* ```
|
|
20429
|
+
*
|
|
20430
|
+
* @example Date Field with Comparison
|
|
20431
|
+
* ```typescript
|
|
20432
|
+
* const checkOutField = await sdk.customFields.createDefinition({
|
|
20433
|
+
* key: 'check_out',
|
|
20434
|
+
* label: 'Check-out Date',
|
|
20435
|
+
* fieldType: 'date',
|
|
20436
|
+
* validation: {
|
|
20437
|
+
* required: true,
|
|
20438
|
+
* comparisons: [
|
|
20439
|
+
* { field: 'check_in', operator: '>', message: 'Must be after check-in' }
|
|
20440
|
+
* ]
|
|
20441
|
+
* },
|
|
20442
|
+
* sortOrder: 2
|
|
20443
|
+
* });
|
|
20444
|
+
* ```
|
|
20445
|
+
*
|
|
20446
|
+
* @example Select Field with Static Options
|
|
20447
|
+
* ```typescript
|
|
20448
|
+
* const departmentField = await sdk.customFields.createDefinition({
|
|
20449
|
+
* key: 'department',
|
|
20450
|
+
* label: 'Department',
|
|
20451
|
+
* fieldType: 'select',
|
|
20452
|
+
* selectOptions: [
|
|
20453
|
+
* { value: 'engineering', label: 'Engineering' },
|
|
20454
|
+
* { value: 'sales', label: 'Sales' },
|
|
20455
|
+
* { value: 'hr', label: 'Human Resources' }
|
|
20456
|
+
* ],
|
|
20457
|
+
* validation: { required: true }
|
|
20458
|
+
* });
|
|
20459
|
+
* ```
|
|
20460
|
+
*
|
|
20461
|
+
* @example Select Field with Dynamic Options
|
|
20462
|
+
* ```typescript
|
|
20463
|
+
* const hotelField = await sdk.customFields.createDefinition({
|
|
20464
|
+
* key: 'preferred_hotel',
|
|
20465
|
+
* label: 'Preferred Hotel',
|
|
20466
|
+
* fieldType: 'select',
|
|
20467
|
+
* selectOptionsSource: {
|
|
20468
|
+
* entity: 'business',
|
|
20469
|
+
* valueField: 'id',
|
|
20470
|
+
* labelField: 'name',
|
|
20471
|
+
* filter: { tags: ['hotel'], isActive: true }
|
|
20472
|
+
* },
|
|
20473
|
+
* validation: { required: true }
|
|
20474
|
+
* });
|
|
20475
|
+
* ```
|
|
20476
|
+
*/
|
|
20477
|
+
async createDefinition(data) {
|
|
20478
|
+
const result = await this.api.createDefinition(data);
|
|
20479
|
+
// Using 'user' domain until pers-shared adds 'customField' domain
|
|
20480
|
+
this.events?.emitSuccess({
|
|
20481
|
+
domain: 'user',
|
|
20482
|
+
type: 'definition_created',
|
|
20483
|
+
userMessage: `Custom field "${result.label}" created`,
|
|
20484
|
+
details: { id: result.id, key: result.key }
|
|
20485
|
+
});
|
|
20486
|
+
return result;
|
|
20487
|
+
}
|
|
20488
|
+
/**
|
|
20489
|
+
* Update an existing custom field definition
|
|
20490
|
+
*
|
|
20491
|
+
* Updates a custom field definition. Note that key and entityType cannot
|
|
20492
|
+
* be changed after creation.
|
|
20493
|
+
*
|
|
20494
|
+
* @param id - The UUID of the custom field definition to update
|
|
20495
|
+
* @param data - The fields to update (partial update supported)
|
|
20496
|
+
* @returns The updated custom field definition
|
|
20497
|
+
* @throws {PersApiError} When definition not found (404) or validation fails (400)
|
|
20498
|
+
*
|
|
20499
|
+
* @example Update Label and Validation
|
|
20500
|
+
* ```typescript
|
|
20501
|
+
* const updated = await sdk.customFields.updateDefinition('uuid-here', {
|
|
20502
|
+
* label: 'Employee ID (Required)',
|
|
20503
|
+
* validation: {
|
|
20504
|
+
* required: true,
|
|
20505
|
+
* minLength: 6,
|
|
20506
|
+
* maxLength: 6
|
|
20507
|
+
* }
|
|
20508
|
+
* });
|
|
20509
|
+
* ```
|
|
20510
|
+
*
|
|
20511
|
+
* @example Update Sort Order
|
|
20512
|
+
* ```typescript
|
|
20513
|
+
* await sdk.customFields.updateDefinition('uuid-here', {
|
|
20514
|
+
* sortOrder: 5
|
|
20515
|
+
* });
|
|
20516
|
+
* ```
|
|
20517
|
+
*/
|
|
20518
|
+
async updateDefinition(id, data) {
|
|
20519
|
+
const result = await this.api.updateDefinition(id, data);
|
|
20520
|
+
// Using 'user' domain until pers-shared adds 'customField' domain
|
|
20521
|
+
this.events?.emitSuccess({
|
|
20522
|
+
domain: 'user',
|
|
20523
|
+
type: 'definition_updated',
|
|
20524
|
+
userMessage: `Custom field "${result.label}" updated`,
|
|
20525
|
+
details: { id: result.id, key: result.key }
|
|
20526
|
+
});
|
|
20527
|
+
return result;
|
|
20528
|
+
}
|
|
20529
|
+
/**
|
|
20530
|
+
* Delete a custom field definition (soft delete)
|
|
20531
|
+
*
|
|
20532
|
+
* Soft deletes a custom field definition. Existing user data in customData
|
|
20533
|
+
* is preserved, but the field will no longer appear in forms or validation.
|
|
20534
|
+
*
|
|
20535
|
+
* ⚠️ Consider the impact on existing user data before deleting.
|
|
20536
|
+
*
|
|
20537
|
+
* @param id - The UUID of the custom field definition to delete
|
|
20538
|
+
* @throws {PersApiError} When definition not found (404)
|
|
20539
|
+
*
|
|
20540
|
+
* @example
|
|
20541
|
+
* ```typescript
|
|
20542
|
+
* try {
|
|
20543
|
+
* await sdk.customFields.deleteDefinition('uuid-here');
|
|
20544
|
+
* console.log('Field deleted');
|
|
20545
|
+
* } catch (error) {
|
|
20546
|
+
* console.log('Failed to delete:', error.message);
|
|
20547
|
+
* }
|
|
20548
|
+
* ```
|
|
20549
|
+
*/
|
|
20550
|
+
async deleteDefinition(id) {
|
|
20551
|
+
await this.api.deleteDefinition(id);
|
|
20552
|
+
// Using 'user' domain until pers-shared adds 'customField' domain
|
|
20553
|
+
this.events?.emitSuccess({
|
|
20554
|
+
domain: 'user',
|
|
20555
|
+
type: 'definition_deleted',
|
|
20556
|
+
userMessage: 'Custom field deleted',
|
|
20557
|
+
details: { id }
|
|
20558
|
+
});
|
|
20559
|
+
}
|
|
20560
|
+
/**
|
|
20561
|
+
* Resolve dynamic select options for a field
|
|
20562
|
+
*
|
|
20563
|
+
* For fields with selectOptionsSource (dynamic options from entities),
|
|
20564
|
+
* this resolves the actual options by querying the source entity.
|
|
20565
|
+
*
|
|
20566
|
+
* @param id - The UUID of the custom field definition
|
|
20567
|
+
* @returns Array of select options
|
|
20568
|
+
*
|
|
20569
|
+
* @example
|
|
20570
|
+
* ```typescript
|
|
20571
|
+
* const field = await sdk.customFields.getDefinition('hotel-field-id');
|
|
20572
|
+
*
|
|
20573
|
+
* if (field.selectOptionsSource) {
|
|
20574
|
+
* // Options come from entity - need to resolve
|
|
20575
|
+
* const options = await sdk.customFields.resolveSelectOptions(field.id);
|
|
20576
|
+
* console.log('Available hotels:', options);
|
|
20577
|
+
* } else {
|
|
20578
|
+
* // Static options
|
|
20579
|
+
* console.log('Options:', field.selectOptions);
|
|
20580
|
+
* }
|
|
20581
|
+
* ```
|
|
20582
|
+
*/
|
|
20583
|
+
async resolveSelectOptions(id) {
|
|
20584
|
+
const response = await this.api.resolveSelectOptions(id);
|
|
20585
|
+
return response.options;
|
|
20586
|
+
}
|
|
20587
|
+
/**
|
|
20588
|
+
* Validate user custom data against field definitions
|
|
20589
|
+
*
|
|
20590
|
+
* Client-side validation using the same rules as the backend. Use this
|
|
20591
|
+
* to validate form data before submission.
|
|
20592
|
+
*
|
|
20593
|
+
* @param data - User's custom data (key-value pairs)
|
|
20594
|
+
* @param definitions - Custom field definitions to validate against
|
|
20595
|
+
* @returns Array of validation errors (empty if valid)
|
|
20596
|
+
*
|
|
20597
|
+
* @example Form Validation
|
|
20598
|
+
* ```typescript
|
|
20599
|
+
* const definitions = await sdk.customFields.getDefinitions();
|
|
20600
|
+
* const formData = {
|
|
20601
|
+
* employee_id: 'E123', // Missing a digit
|
|
20602
|
+
* department: 'engineering'
|
|
20603
|
+
* };
|
|
20604
|
+
*
|
|
20605
|
+
* const errors = sdk.customFields.validateUserData(formData, definitions);
|
|
20606
|
+
* if (errors.length > 0) {
|
|
20607
|
+
* errors.forEach(err => {
|
|
20608
|
+
* console.log(`${err.field}: ${err.message}`);
|
|
20609
|
+
* });
|
|
20610
|
+
* return; // Don't submit
|
|
20611
|
+
* }
|
|
20612
|
+
*
|
|
20613
|
+
* // No errors, safe to submit
|
|
20614
|
+
* await sdk.users.updateCurrentUser({ customData: formData });
|
|
20615
|
+
* ```
|
|
20616
|
+
*/
|
|
20617
|
+
validateUserData(data, definitions) {
|
|
20618
|
+
// Convert CustomFieldDefinitionDTO to FieldDefinition for validation
|
|
20619
|
+
const fieldDefinitions = definitions.map(def => ({
|
|
20620
|
+
key: def.key,
|
|
20621
|
+
fieldType: def.fieldType,
|
|
20622
|
+
label: def.label,
|
|
20623
|
+
validation: def.validation ?? undefined,
|
|
20624
|
+
}));
|
|
20625
|
+
return validateAllFields(data, fieldDefinitions);
|
|
20626
|
+
}
|
|
20627
|
+
/**
|
|
20628
|
+
* Validate a single field value
|
|
20629
|
+
*
|
|
20630
|
+
* Validates a single value against a field's rules. Useful for
|
|
20631
|
+
* real-time validation as the user types.
|
|
20632
|
+
*
|
|
20633
|
+
* @param value - The value to validate
|
|
20634
|
+
* @param definition - The field definition
|
|
20635
|
+
* @returns Validation error or null if valid
|
|
20636
|
+
*
|
|
20637
|
+
* @example Real-time Validation
|
|
20638
|
+
* ```typescript
|
|
20639
|
+
* const employeeIdField = definitions.find(d => d.key === 'employee_id');
|
|
20640
|
+
*
|
|
20641
|
+
* const handleBlur = (value: string) => {
|
|
20642
|
+
* const error = sdk.customFields.validateFieldValue(value, employeeIdField);
|
|
20643
|
+
* if (error) {
|
|
20644
|
+
* setFieldError('employee_id', error.message);
|
|
20645
|
+
* } else {
|
|
20646
|
+
* clearFieldError('employee_id');
|
|
20647
|
+
* }
|
|
20648
|
+
* };
|
|
20649
|
+
* ```
|
|
20650
|
+
*/
|
|
20651
|
+
validateFieldValue(value, definition) {
|
|
20652
|
+
const error = validateFieldValue(value, definition.validation ?? null, definition.fieldType);
|
|
20653
|
+
if (error) {
|
|
20654
|
+
// Add field key to error
|
|
20655
|
+
return { ...error, field: definition.key };
|
|
20656
|
+
}
|
|
20657
|
+
return null;
|
|
20658
|
+
}
|
|
20659
|
+
}
|
|
20660
|
+
|
|
20661
|
+
/**
|
|
20662
|
+
* Low-level API for booking operations
|
|
20663
|
+
*
|
|
20664
|
+
* Provides direct HTTP calls to the booking endpoints.
|
|
20665
|
+
* For most use cases, prefer using BookingManager or BookingService.
|
|
20666
|
+
*
|
|
20667
|
+
* @internal
|
|
20668
|
+
*/
|
|
20669
|
+
class BookingApi {
|
|
20670
|
+
constructor(apiClient) {
|
|
20671
|
+
this.apiClient = apiClient;
|
|
20672
|
+
this.basePath = '/bookings';
|
|
20673
|
+
}
|
|
20674
|
+
/**
|
|
20675
|
+
* List all bookings with optional filters
|
|
20676
|
+
*
|
|
20677
|
+
* @param options - Filter options (userId, businessId, status)
|
|
20678
|
+
* @returns Promise resolving to array of bookings
|
|
20679
|
+
*/
|
|
20680
|
+
async getAll(options) {
|
|
20681
|
+
const params = new URLSearchParams();
|
|
20682
|
+
if (options?.userId) {
|
|
20683
|
+
params.append('userId', options.userId);
|
|
20684
|
+
}
|
|
20685
|
+
if (options?.businessId) {
|
|
20686
|
+
params.append('businessId', options.businessId);
|
|
20687
|
+
}
|
|
20688
|
+
if (options?.status) {
|
|
20689
|
+
params.append('status', options.status);
|
|
20690
|
+
}
|
|
20691
|
+
const queryString = params.toString();
|
|
20692
|
+
const url = queryString ? `${this.basePath}?${queryString}` : this.basePath;
|
|
20693
|
+
return this.apiClient.get(url);
|
|
20694
|
+
}
|
|
20695
|
+
/**
|
|
20696
|
+
* Get a single booking by ID
|
|
20697
|
+
*
|
|
20698
|
+
* @param id - Booking UUID
|
|
20699
|
+
* @returns Promise resolving to booking data
|
|
20700
|
+
*/
|
|
20701
|
+
async getById(id) {
|
|
20702
|
+
return this.apiClient.get(`${this.basePath}/${id}`);
|
|
20703
|
+
}
|
|
20704
|
+
/**
|
|
20705
|
+
* Create a new booking
|
|
20706
|
+
*
|
|
20707
|
+
* @param data - Booking creation data
|
|
20708
|
+
* @returns Promise resolving to created booking
|
|
20709
|
+
*/
|
|
20710
|
+
async create(data) {
|
|
20711
|
+
return this.apiClient.post(this.basePath, data);
|
|
20712
|
+
}
|
|
20713
|
+
/**
|
|
20714
|
+
* Update an existing booking
|
|
20715
|
+
*
|
|
20716
|
+
* @param id - Booking UUID
|
|
20717
|
+
* @param data - Booking update data
|
|
20718
|
+
* @returns Promise resolving to updated booking
|
|
20719
|
+
*/
|
|
20720
|
+
async update(id, data) {
|
|
20721
|
+
return this.apiClient.put(`${this.basePath}/${id}`, data);
|
|
20722
|
+
}
|
|
20723
|
+
/**
|
|
20724
|
+
* Delete a booking
|
|
20725
|
+
*
|
|
20726
|
+
* @param id - Booking UUID
|
|
20727
|
+
* @returns Promise resolving to success status
|
|
20728
|
+
*/
|
|
20729
|
+
async delete(id) {
|
|
20730
|
+
return this.apiClient.delete(`${this.basePath}/${id}`);
|
|
20731
|
+
}
|
|
20732
|
+
}
|
|
20733
|
+
|
|
20734
|
+
/**
|
|
20735
|
+
* Booking Service - Business logic layer for booking operations
|
|
20736
|
+
*
|
|
20737
|
+
* Provides booking management functionality with validation and
|
|
20738
|
+
* business rule enforcement.
|
|
20739
|
+
*
|
|
20740
|
+
* @internal
|
|
20741
|
+
*/
|
|
20742
|
+
class BookingService {
|
|
20743
|
+
constructor(bookingApi) {
|
|
20744
|
+
this.bookingApi = bookingApi;
|
|
20745
|
+
}
|
|
20746
|
+
/**
|
|
20747
|
+
* Get all bookings with optional filters
|
|
20748
|
+
*
|
|
20749
|
+
* @param options - Filter options
|
|
20750
|
+
* @returns Promise resolving to array of bookings
|
|
20751
|
+
*/
|
|
20752
|
+
async getAll(options) {
|
|
20753
|
+
return this.bookingApi.getAll(options);
|
|
20754
|
+
}
|
|
20755
|
+
/**
|
|
20756
|
+
* Get bookings for a specific user
|
|
20757
|
+
*
|
|
20758
|
+
* @param userId - User ID to filter by
|
|
20759
|
+
* @param status - Optional status filter
|
|
20760
|
+
* @returns Promise resolving to user's bookings
|
|
20761
|
+
*/
|
|
20762
|
+
async getByUser(userId, status) {
|
|
20763
|
+
return this.bookingApi.getAll({ userId, status });
|
|
20764
|
+
}
|
|
20765
|
+
/**
|
|
20766
|
+
* Get bookings for a specific business
|
|
20767
|
+
*
|
|
20768
|
+
* @param businessId - Business ID to filter by
|
|
20769
|
+
* @param status - Optional status filter
|
|
20770
|
+
* @returns Promise resolving to business's bookings
|
|
20771
|
+
*/
|
|
20772
|
+
async getByBusiness(businessId, status) {
|
|
20773
|
+
return this.bookingApi.getAll({ businessId, status });
|
|
20774
|
+
}
|
|
20775
|
+
/**
|
|
20776
|
+
* Get a single booking by ID
|
|
20777
|
+
*
|
|
20778
|
+
* @param id - Booking UUID
|
|
20779
|
+
* @returns Promise resolving to booking data
|
|
20780
|
+
*/
|
|
20781
|
+
async getById(id) {
|
|
20782
|
+
return this.bookingApi.getById(id);
|
|
20783
|
+
}
|
|
20784
|
+
/**
|
|
20785
|
+
* Create a new booking
|
|
20786
|
+
*
|
|
20787
|
+
* @param data - Booking creation data
|
|
20788
|
+
* @returns Promise resolving to created booking
|
|
20789
|
+
*/
|
|
20790
|
+
async create(data) {
|
|
20791
|
+
// Validate check-out is after check-in
|
|
20792
|
+
if (data.checkInDate && data.checkOutDate) {
|
|
20793
|
+
const checkIn = new Date(data.checkInDate);
|
|
20794
|
+
const checkOut = new Date(data.checkOutDate);
|
|
20795
|
+
if (checkOut <= checkIn) {
|
|
20796
|
+
throw new Error('Check-out date must be after check-in date');
|
|
20797
|
+
}
|
|
20798
|
+
}
|
|
20799
|
+
return this.bookingApi.create(data);
|
|
20800
|
+
}
|
|
20801
|
+
/**
|
|
20802
|
+
* Update an existing booking
|
|
20803
|
+
*
|
|
20804
|
+
* @param id - Booking UUID
|
|
20805
|
+
* @param data - Booking update data
|
|
20806
|
+
* @returns Promise resolving to updated booking
|
|
20807
|
+
*/
|
|
20808
|
+
async update(id, data) {
|
|
20809
|
+
// Validate dates if both provided
|
|
20810
|
+
if (data.checkInDate && data.checkOutDate) {
|
|
20811
|
+
const checkIn = new Date(data.checkInDate);
|
|
20812
|
+
const checkOut = new Date(data.checkOutDate);
|
|
20813
|
+
if (checkOut <= checkIn) {
|
|
20814
|
+
throw new Error('Check-out date must be after check-in date');
|
|
20815
|
+
}
|
|
20816
|
+
}
|
|
20817
|
+
return this.bookingApi.update(id, data);
|
|
20818
|
+
}
|
|
20819
|
+
/**
|
|
20820
|
+
* Delete a booking
|
|
20821
|
+
*
|
|
20822
|
+
* @param id - Booking UUID
|
|
20823
|
+
* @returns Promise resolving to success status
|
|
20824
|
+
*/
|
|
20825
|
+
async delete(id) {
|
|
20826
|
+
return this.bookingApi.delete(id);
|
|
20827
|
+
}
|
|
20828
|
+
/**
|
|
20829
|
+
* Check if a user has an active or future booking
|
|
20830
|
+
*
|
|
20831
|
+
* Useful for eligibility checks on redemptions.
|
|
20832
|
+
*
|
|
20833
|
+
* @param userId - User ID to check
|
|
20834
|
+
* @param options - Validation options (status filter, business filter)
|
|
20835
|
+
* @returns Promise resolving to true if user has valid booking
|
|
20836
|
+
*/
|
|
20837
|
+
async hasValidBooking(userId, options) {
|
|
20838
|
+
const queryOptions = {
|
|
20839
|
+
userId,
|
|
20840
|
+
businessId: options?.businessId
|
|
20841
|
+
};
|
|
20842
|
+
// If a specific status is requested, use it
|
|
20843
|
+
if (options?.status && options.status !== 'active_future') {
|
|
20844
|
+
queryOptions.status = options.status === 'any' ? undefined : options.status;
|
|
20845
|
+
const bookings = await this.bookingApi.getAll(queryOptions);
|
|
20846
|
+
return bookings.length > 0;
|
|
20847
|
+
}
|
|
20848
|
+
// Default: check active+future (active_future or undefined)
|
|
20849
|
+
const activeBookings = await this.bookingApi.getAll({ ...queryOptions, status: 'active' });
|
|
20850
|
+
if (activeBookings.length > 0)
|
|
20851
|
+
return true;
|
|
20852
|
+
const futureBookings = await this.bookingApi.getAll({ ...queryOptions, status: 'future' });
|
|
20853
|
+
return futureBookings.length > 0;
|
|
20854
|
+
}
|
|
20855
|
+
}
|
|
20856
|
+
|
|
20857
|
+
/**
|
|
20858
|
+
* Booking Manager - High-level interface for booking operations
|
|
20859
|
+
*
|
|
20860
|
+
* Provides a simplified API for managing bookings (hotel stays, reservations, etc.)
|
|
20861
|
+
* Used for tracking user stays and eligibility checks on redemptions.
|
|
20862
|
+
*
|
|
20863
|
+
* @group Managers
|
|
20864
|
+
* @category Booking Management
|
|
20865
|
+
*
|
|
20866
|
+
* @example List Bookings
|
|
20867
|
+
* ```typescript
|
|
20868
|
+
* // Get all bookings
|
|
20869
|
+
* const allBookings = await sdk.bookings.getAll();
|
|
20870
|
+
*
|
|
20871
|
+
* // Filter by user
|
|
20872
|
+
* const userBookings = await sdk.bookings.getAll({ userId: 'user-123' });
|
|
20873
|
+
*
|
|
20874
|
+
* // Filter by status
|
|
20875
|
+
* const activeBookings = await sdk.bookings.getAll({ status: 'active' });
|
|
20876
|
+
* ```
|
|
20877
|
+
*
|
|
20878
|
+
* @example Create Booking
|
|
20879
|
+
* ```typescript
|
|
20880
|
+
* const booking = await sdk.bookings.create({
|
|
20881
|
+
* userId: 'user-123',
|
|
20882
|
+
* locationName: 'Grand Hotel',
|
|
20883
|
+
* checkInDate: '2026-06-01',
|
|
20884
|
+
* checkOutDate: '2026-06-05',
|
|
20885
|
+
* guests: 2
|
|
20886
|
+
* });
|
|
20887
|
+
* ```
|
|
20888
|
+
*
|
|
20889
|
+
* @example Check Eligibility
|
|
20890
|
+
* ```typescript
|
|
20891
|
+
* // Check if user has valid (active or future) booking
|
|
20892
|
+
* const hasBooking = await sdk.bookings.hasValidBooking('user-123');
|
|
20893
|
+
* if (hasBooking) {
|
|
20894
|
+
* // User is eligible for booking-required redemption
|
|
20895
|
+
* }
|
|
20896
|
+
* ```
|
|
20897
|
+
*/
|
|
20898
|
+
class BookingManager {
|
|
20899
|
+
constructor(apiClient, events) {
|
|
20900
|
+
this.apiClient = apiClient;
|
|
20901
|
+
this.events = events;
|
|
20902
|
+
const bookingApi = new BookingApi(apiClient);
|
|
20903
|
+
this.bookingService = new BookingService(bookingApi);
|
|
20904
|
+
}
|
|
20905
|
+
/**
|
|
20906
|
+
* Get all bookings with optional filters
|
|
20907
|
+
*
|
|
20908
|
+
* @param options - Filter options (userId, businessId, status)
|
|
20909
|
+
* @returns Promise resolving to array of bookings
|
|
20910
|
+
*
|
|
20911
|
+
* @example Basic Usage
|
|
20912
|
+
* ```typescript
|
|
20913
|
+
* // All bookings
|
|
20914
|
+
* const bookings = await sdk.bookings.getAll();
|
|
20915
|
+
*
|
|
20916
|
+
* // Filter by user
|
|
20917
|
+
* const userBookings = await sdk.bookings.getAll({ userId: 'user-123' });
|
|
20918
|
+
*
|
|
20919
|
+
* // Filter by status
|
|
20920
|
+
* const activeBookings = await sdk.bookings.getAll({ status: 'active' });
|
|
20921
|
+
* const futureBookings = await sdk.bookings.getAll({ status: 'future' });
|
|
20922
|
+
* const pastBookings = await sdk.bookings.getAll({ status: 'past' });
|
|
20923
|
+
* ```
|
|
20924
|
+
*/
|
|
20925
|
+
async getAll(options) {
|
|
20926
|
+
return this.bookingService.getAll(options);
|
|
20927
|
+
}
|
|
20928
|
+
/**
|
|
20929
|
+
* Get bookings for a specific user
|
|
20930
|
+
*
|
|
20931
|
+
* Convenience method for filtering bookings by user ID.
|
|
20932
|
+
*
|
|
20933
|
+
* @param userId - User ID to filter by
|
|
20934
|
+
* @param status - Optional status filter
|
|
20935
|
+
* @returns Promise resolving to user's bookings
|
|
20936
|
+
*/
|
|
20937
|
+
async getByUser(userId, status) {
|
|
20938
|
+
return this.bookingService.getByUser(userId, status);
|
|
20939
|
+
}
|
|
20940
|
+
/**
|
|
20941
|
+
* Get bookings for a specific business
|
|
20942
|
+
*
|
|
20943
|
+
* Convenience method for filtering bookings by business ID.
|
|
20944
|
+
*
|
|
20945
|
+
* @param businessId - Business ID to filter by
|
|
20946
|
+
* @param status - Optional status filter
|
|
20947
|
+
* @returns Promise resolving to business's bookings
|
|
20948
|
+
*/
|
|
20949
|
+
async getByBusiness(businessId, status) {
|
|
20950
|
+
return this.bookingService.getByBusiness(businessId, status);
|
|
20951
|
+
}
|
|
20952
|
+
/**
|
|
20953
|
+
* Get a single booking by ID
|
|
20954
|
+
*
|
|
20955
|
+
* @param id - Booking UUID
|
|
20956
|
+
* @returns Promise resolving to booking data
|
|
20957
|
+
* @throws {PersApiError} When booking not found
|
|
20958
|
+
*/
|
|
20959
|
+
async getById(id) {
|
|
20960
|
+
return this.bookingService.getById(id);
|
|
20961
|
+
}
|
|
20962
|
+
/**
|
|
20963
|
+
* Create a new booking
|
|
20964
|
+
*
|
|
20965
|
+
* Creates a booking for a user at a specific location. Used for tracking
|
|
20966
|
+
* hotel stays, reservations, etc. for eligibility checks.
|
|
20967
|
+
*
|
|
20968
|
+
* @param data - Booking creation data
|
|
20969
|
+
* @returns Promise resolving to created booking
|
|
20970
|
+
* @throws {Error} When check-out date is not after check-in date
|
|
20971
|
+
*
|
|
20972
|
+
* @example Create Hotel Booking
|
|
20973
|
+
* ```typescript
|
|
20974
|
+
* const booking = await sdk.bookings.create({
|
|
20975
|
+
* userId: 'user-123',
|
|
20976
|
+
* locationName: 'Grand Hotel',
|
|
20977
|
+
* locationType: 'hotel',
|
|
20978
|
+
* checkInDate: '2026-06-01',
|
|
20979
|
+
* checkOutDate: '2026-06-05',
|
|
20980
|
+
* confirmationNumber: 'CONF123',
|
|
20981
|
+
* guests: 2,
|
|
20982
|
+
* notes: 'Ocean view room'
|
|
20983
|
+
* });
|
|
20984
|
+
* ```
|
|
20985
|
+
*/
|
|
20986
|
+
async create(data) {
|
|
20987
|
+
const result = await this.bookingService.create(data);
|
|
20988
|
+
// TODO: Add event emission when 'booking' domain is added to pers-shared
|
|
20989
|
+
// this.events?.emitSuccess({ domain: 'booking', type: 'booking_created', ... });
|
|
20990
|
+
return result;
|
|
20991
|
+
}
|
|
20992
|
+
/**
|
|
20993
|
+
* Update an existing booking
|
|
20994
|
+
*
|
|
20995
|
+
* @param id - Booking UUID
|
|
20996
|
+
* @param data - Booking update data (partial update supported)
|
|
20997
|
+
* @returns Promise resolving to updated booking
|
|
20998
|
+
* @throws {PersApiError} When booking not found
|
|
20999
|
+
* @throws {Error} When check-out date is not after check-in date
|
|
21000
|
+
*
|
|
21001
|
+
* @example Update Booking Dates
|
|
21002
|
+
* ```typescript
|
|
21003
|
+
* const updated = await sdk.bookings.update('booking-123', {
|
|
21004
|
+
* checkInDate: '2026-06-02',
|
|
21005
|
+
* checkOutDate: '2026-06-07'
|
|
21006
|
+
* });
|
|
21007
|
+
* ```
|
|
21008
|
+
*/
|
|
21009
|
+
async update(id, data) {
|
|
21010
|
+
const result = await this.bookingService.update(id, data);
|
|
21011
|
+
// TODO: Add event emission when 'booking' domain is added to pers-shared
|
|
21012
|
+
// this.events?.emitSuccess({ domain: 'booking', type: 'booking_updated', ... });
|
|
21013
|
+
return result;
|
|
21014
|
+
}
|
|
21015
|
+
/**
|
|
21016
|
+
* Delete a booking
|
|
21017
|
+
*
|
|
21018
|
+
* @param id - Booking UUID
|
|
21019
|
+
* @returns Promise resolving to success status
|
|
21020
|
+
* @throws {PersApiError} When booking not found
|
|
21021
|
+
*/
|
|
21022
|
+
async delete(id) {
|
|
21023
|
+
const result = await this.bookingService.delete(id);
|
|
21024
|
+
// TODO: Add event emission when 'booking' domain is added to pers-shared
|
|
21025
|
+
// this.events?.emitSuccess({ domain: 'booking', type: 'booking_deleted', ... });
|
|
21026
|
+
return result;
|
|
21027
|
+
}
|
|
21028
|
+
/**
|
|
21029
|
+
* Check if a user has a valid (active or future) booking
|
|
21030
|
+
*
|
|
21031
|
+
* Useful for eligibility checks on redemptions that require a valid booking.
|
|
21032
|
+
*
|
|
21033
|
+
* @param userId - User ID to check
|
|
21034
|
+
* @param options - Validation options (status filter, business filter)
|
|
21035
|
+
* @returns Promise resolving to true if user has valid booking
|
|
21036
|
+
*
|
|
21037
|
+
* @example Eligibility Check
|
|
21038
|
+
* ```typescript
|
|
21039
|
+
* const hasBooking = await sdk.bookings.hasValidBooking('user-123');
|
|
21040
|
+
*
|
|
21041
|
+
* if (!hasBooking) {
|
|
21042
|
+
* console.log('User must have an active or upcoming booking');
|
|
21043
|
+
* }
|
|
21044
|
+
*
|
|
21045
|
+
* // Check for active booking only
|
|
21046
|
+
* const hasActive = await sdk.bookings.hasValidBooking('user-123', { status: 'active' });
|
|
21047
|
+
*
|
|
21048
|
+
* // Check for booking at specific business
|
|
21049
|
+
* const hasHotelBooking = await sdk.bookings.hasValidBooking('user-123', {
|
|
21050
|
+
* businessId: 'hotel-123'
|
|
21051
|
+
* });
|
|
21052
|
+
* ```
|
|
21053
|
+
*/
|
|
21054
|
+
async hasValidBooking(userId, options) {
|
|
21055
|
+
return this.bookingService.hasValidBooking(userId, options);
|
|
21056
|
+
}
|
|
21057
|
+
/**
|
|
21058
|
+
* Get the booking service for advanced operations
|
|
21059
|
+
*
|
|
21060
|
+
* @returns BookingService instance with full API access
|
|
21061
|
+
*/
|
|
21062
|
+
getBookingService() {
|
|
21063
|
+
return this.bookingService;
|
|
21064
|
+
}
|
|
21065
|
+
}
|
|
21066
|
+
|
|
19522
21067
|
/**
|
|
19523
21068
|
* @fileoverview PERS SDK - Platform-agnostic TypeScript SDK with High-Level Managers
|
|
19524
21069
|
*
|
|
@@ -19962,7 +21507,7 @@ class PersSDK {
|
|
|
19962
21507
|
*/
|
|
19963
21508
|
get tokens() {
|
|
19964
21509
|
if (!this._tokens) {
|
|
19965
|
-
this._tokens = new TokenManager(this.apiClient);
|
|
21510
|
+
this._tokens = new TokenManager(this.apiClient, this._events);
|
|
19966
21511
|
}
|
|
19967
21512
|
return this._tokens;
|
|
19968
21513
|
}
|
|
@@ -20207,6 +21752,78 @@ class PersSDK {
|
|
|
20207
21752
|
}
|
|
20208
21753
|
return this._webhooks;
|
|
20209
21754
|
}
|
|
21755
|
+
/**
|
|
21756
|
+
* Custom Field Definition Manager - Manage tenant-specific custom fields
|
|
21757
|
+
*
|
|
21758
|
+
* Provides CRUD operations for custom field definitions that extend the built-in
|
|
21759
|
+
* user profile fields. Custom fields are tenant-specific and support validation.
|
|
21760
|
+
*
|
|
21761
|
+
* @returns CustomFieldDefinitionManager instance
|
|
21762
|
+
*
|
|
21763
|
+
* @example Basic Operations
|
|
21764
|
+
* ```typescript
|
|
21765
|
+
* // List all custom fields
|
|
21766
|
+
* const fields = await sdk.customFields.getDefinitions();
|
|
21767
|
+
*
|
|
21768
|
+
* // Create a new field
|
|
21769
|
+
* const field = await sdk.customFields.createDefinition({
|
|
21770
|
+
* key: 'employee_id',
|
|
21771
|
+
* label: 'Employee ID',
|
|
21772
|
+
* fieldType: 'text',
|
|
21773
|
+
* validation: { required: true }
|
|
21774
|
+
* });
|
|
21775
|
+
*
|
|
21776
|
+
* // Validate user data
|
|
21777
|
+
* const errors = sdk.customFields.validateUserData(formData, fields.data);
|
|
21778
|
+
* ```
|
|
21779
|
+
*/
|
|
21780
|
+
get customFields() {
|
|
21781
|
+
if (!this._customFields) {
|
|
21782
|
+
this._customFields = new CustomFieldDefinitionManager(this.apiClient, this._events);
|
|
21783
|
+
}
|
|
21784
|
+
return this._customFields;
|
|
21785
|
+
}
|
|
21786
|
+
/**
|
|
21787
|
+
* Booking manager - High-level booking operations
|
|
21788
|
+
*
|
|
21789
|
+
* Provides methods for managing user bookings (hotel stays, reservations, etc.)
|
|
21790
|
+
* Used for eligibility checks on redemptions that require valid bookings.
|
|
21791
|
+
*
|
|
21792
|
+
* @returns BookingManager instance
|
|
21793
|
+
*
|
|
21794
|
+
* @example List User Bookings
|
|
21795
|
+
* ```typescript
|
|
21796
|
+
* // Get all bookings for a user
|
|
21797
|
+
* const bookings = await sdk.bookings.getByUser('user-123');
|
|
21798
|
+
*
|
|
21799
|
+
* // Filter by status
|
|
21800
|
+
* const activeBookings = await sdk.bookings.getAll({ status: 'active' });
|
|
21801
|
+
* ```
|
|
21802
|
+
*
|
|
21803
|
+
* @example Create Booking
|
|
21804
|
+
* ```typescript
|
|
21805
|
+
* const booking = await sdk.bookings.create({
|
|
21806
|
+
* userId: 'user-123',
|
|
21807
|
+
* locationName: 'Grand Hotel',
|
|
21808
|
+
* checkInDate: '2026-06-01',
|
|
21809
|
+
* checkOutDate: '2026-06-05'
|
|
21810
|
+
* });
|
|
21811
|
+
* ```
|
|
21812
|
+
*
|
|
21813
|
+
* @example Eligibility Check
|
|
21814
|
+
* ```typescript
|
|
21815
|
+
* const hasBooking = await sdk.bookings.hasValidBooking('user-123');
|
|
21816
|
+
* if (hasBooking) {
|
|
21817
|
+
* // User is eligible for booking-required redemption
|
|
21818
|
+
* }
|
|
21819
|
+
* ```
|
|
21820
|
+
*/
|
|
21821
|
+
get bookings() {
|
|
21822
|
+
if (!this._bookings) {
|
|
21823
|
+
this._bookings = new BookingManager(this.apiClient, this._events);
|
|
21824
|
+
}
|
|
21825
|
+
return this._bookings;
|
|
21826
|
+
}
|
|
20210
21827
|
/**
|
|
20211
21828
|
* Wallet Events Manager - Real-time blockchain events for user's wallets
|
|
20212
21829
|
*
|
|
@@ -40894,6 +42511,122 @@ function spelunkMessage(value) {
|
|
|
40894
42511
|
return result;
|
|
40895
42512
|
}
|
|
40896
42513
|
|
|
42514
|
+
/**
|
|
42515
|
+
* Get multiple ERC-1155 token balances using native balanceOfBatch function.
|
|
42516
|
+
* This is the most efficient method - it's built into the ERC-1155 standard.
|
|
42517
|
+
*
|
|
42518
|
+
* @param contract - ERC-1155 contract instance
|
|
42519
|
+
* @param accountAddress - Account to check balances for
|
|
42520
|
+
* @param tokenIds - Array of tokenIds to check
|
|
42521
|
+
* @returns Array of BatchBalanceResult with balance for each tokenId
|
|
42522
|
+
*
|
|
42523
|
+
* @example
|
|
42524
|
+
* ```typescript
|
|
42525
|
+
* const results = await getERC1155BalanceOfBatch(
|
|
42526
|
+
* contract,
|
|
42527
|
+
* '0xabc...',
|
|
42528
|
+
* ['1', '2', '3', '4', '5']
|
|
42529
|
+
* );
|
|
42530
|
+
* // 1 RPC call returns all balances
|
|
42531
|
+
* ```
|
|
42532
|
+
*/
|
|
42533
|
+
async function getERC1155BalanceOfBatch(contract, accountAddress, tokenIds) {
|
|
42534
|
+
if (tokenIds.length === 0) {
|
|
42535
|
+
return [];
|
|
42536
|
+
}
|
|
42537
|
+
try {
|
|
42538
|
+
// ERC-1155 balanceOfBatch requires arrays of same length
|
|
42539
|
+
// accounts[i] corresponds to ids[i]
|
|
42540
|
+
const accounts = tokenIds.map(() => accountAddress);
|
|
42541
|
+
// Call native balanceOfBatch - 1 RPC call for ALL tokenIds
|
|
42542
|
+
const balances = await contract.balanceOfBatch(accounts, tokenIds);
|
|
42543
|
+
return tokenIds.map((tokenId, index) => ({
|
|
42544
|
+
tokenId,
|
|
42545
|
+
balance: balances[index],
|
|
42546
|
+
success: true
|
|
42547
|
+
}));
|
|
42548
|
+
}
|
|
42549
|
+
catch (error) {
|
|
42550
|
+
console.warn('[ERC1155] balanceOfBatch failed:', error);
|
|
42551
|
+
return tokenIds.map(tokenId => ({
|
|
42552
|
+
tokenId,
|
|
42553
|
+
balance: 0n,
|
|
42554
|
+
success: false,
|
|
42555
|
+
error: `balanceOfBatch failed: ${error}`
|
|
42556
|
+
}));
|
|
42557
|
+
}
|
|
42558
|
+
}
|
|
42559
|
+
/**
|
|
42560
|
+
* Get multiple token balances for ERC-1155 contracts.
|
|
42561
|
+
* Uses the native balanceOfBatch function (1 RPC call for ALL tokens).
|
|
42562
|
+
* Falls back to individual calls only if balanceOfBatch fails.
|
|
42563
|
+
*
|
|
42564
|
+
* @param provider - Ethers provider
|
|
42565
|
+
* @param contractAddress - Token contract address
|
|
42566
|
+
* @param contractInterface - Contract interface (must include balanceOfBatch for ERC-1155)
|
|
42567
|
+
* @param accountAddress - Account to check balances for
|
|
42568
|
+
* @param tokenIds - Array of tokenIds to check
|
|
42569
|
+
* @param isERC1155 - Whether the contract is ERC-1155 (default: true)
|
|
42570
|
+
* @returns Array of BatchBalanceResult
|
|
42571
|
+
*/
|
|
42572
|
+
async function getBatchBalancesWithFallback(provider, contractAddress, contractInterface, accountAddress, tokenIds, isERC1155 = true) {
|
|
42573
|
+
if (tokenIds.length === 0)
|
|
42574
|
+
return [];
|
|
42575
|
+
const contract = new Contract(contractAddress, contractInterface, provider);
|
|
42576
|
+
// For single token, direct call is more efficient
|
|
42577
|
+
if (tokenIds.length === 1) {
|
|
42578
|
+
try {
|
|
42579
|
+
const balance = isERC1155
|
|
42580
|
+
? await contract.balanceOf(accountAddress, tokenIds[0])
|
|
42581
|
+
: await contract.balanceOf(accountAddress);
|
|
42582
|
+
return [{
|
|
42583
|
+
tokenId: tokenIds[0],
|
|
42584
|
+
balance: balance,
|
|
42585
|
+
success: true
|
|
42586
|
+
}];
|
|
42587
|
+
}
|
|
42588
|
+
catch (error) {
|
|
42589
|
+
return [{
|
|
42590
|
+
tokenId: tokenIds[0],
|
|
42591
|
+
balance: 0n,
|
|
42592
|
+
success: false,
|
|
42593
|
+
error: `${error}`
|
|
42594
|
+
}];
|
|
42595
|
+
}
|
|
42596
|
+
}
|
|
42597
|
+
// For ERC-1155: Use native balanceOfBatch (1 RPC call for ALL tokens)
|
|
42598
|
+
if (isERC1155) {
|
|
42599
|
+
const results = await getERC1155BalanceOfBatch(contract, accountAddress, tokenIds);
|
|
42600
|
+
// If native call succeeded, return results
|
|
42601
|
+
if (results.some(r => r.success)) {
|
|
42602
|
+
return results;
|
|
42603
|
+
}
|
|
42604
|
+
// Otherwise fall through to individual calls
|
|
42605
|
+
}
|
|
42606
|
+
// Fallback: individual calls in parallel (for non-ERC1155 or if balanceOfBatch failed)
|
|
42607
|
+
const promises = tokenIds.map(async (tokenId) => {
|
|
42608
|
+
try {
|
|
42609
|
+
const balance = isERC1155
|
|
42610
|
+
? await contract.balanceOf(accountAddress, tokenId)
|
|
42611
|
+
: await contract.balanceOf(accountAddress);
|
|
42612
|
+
return {
|
|
42613
|
+
tokenId,
|
|
42614
|
+
balance: balance,
|
|
42615
|
+
success: true
|
|
42616
|
+
};
|
|
42617
|
+
}
|
|
42618
|
+
catch (error) {
|
|
42619
|
+
return {
|
|
42620
|
+
tokenId,
|
|
42621
|
+
balance: 0n,
|
|
42622
|
+
success: false,
|
|
42623
|
+
error: `${error}`
|
|
42624
|
+
};
|
|
42625
|
+
}
|
|
42626
|
+
});
|
|
42627
|
+
return Promise.all(promises);
|
|
42628
|
+
}
|
|
42629
|
+
|
|
40897
42630
|
const PERS_STATUS_TOKEN_ETHERS_METHOD_BUILDER = (contract, methodName) => {
|
|
40898
42631
|
try {
|
|
40899
42632
|
// Use unknown first, then cast to the expected type
|
|
@@ -41505,7 +43238,10 @@ class TokenDomainService {
|
|
|
41505
43238
|
try {
|
|
41506
43239
|
const abi = convertAbiToInterface(params.abi);
|
|
41507
43240
|
const analysis = this.contractService.analyzeContract(abi);
|
|
41508
|
-
|
|
43241
|
+
// ERC-1155 balanceOfBatch can handle large batches efficiently (1 RPC call)
|
|
43242
|
+
// Default to 100 for ERC-1155, 20 for ERC-721 (which has no batch function)
|
|
43243
|
+
const defaultBatchSize = analysis.isERC1155 ? 100 : 20;
|
|
43244
|
+
const batchSize = Math.min(params.batchSize || defaultBatchSize, analysis.isERC1155 ? 200 : 50);
|
|
41509
43245
|
// ERC-1155: Requires specific token IDs
|
|
41510
43246
|
if (analysis.isERC1155) {
|
|
41511
43247
|
if (!params.tokenIds?.length) {
|
|
@@ -41559,31 +43295,44 @@ class TokenDomainService {
|
|
|
41559
43295
|
async processTokenBatch(params, tokenIds, batchSize, tokenStandard) {
|
|
41560
43296
|
const tokens = [];
|
|
41561
43297
|
const errors = [];
|
|
43298
|
+
const abi = convertAbiToInterface(params.abi);
|
|
41562
43299
|
// Process in batches
|
|
41563
43300
|
for (let i = 0; i < tokenIds.length; i += batchSize) {
|
|
41564
43301
|
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
|
-
|
|
43302
|
+
let validTokens;
|
|
43303
|
+
if (tokenStandard === 'ERC-721') {
|
|
43304
|
+
// For ERC-721: Skip balance check, we know ownership from enumeration or explicit tokenIds
|
|
43305
|
+
validTokens = batch.map(tokenId => ({
|
|
43306
|
+
tokenId,
|
|
43307
|
+
balance: 1,
|
|
43308
|
+
hasBalance: true,
|
|
43309
|
+
metadata: null
|
|
43310
|
+
}));
|
|
43311
|
+
}
|
|
43312
|
+
else {
|
|
43313
|
+
// For ERC-1155: Use native balanceOfBatch (1 RPC call for entire batch)
|
|
43314
|
+
const batchResults = await this.web3Api.getBatchTokenBalances({
|
|
43315
|
+
accountAddress: params.accountAddress,
|
|
43316
|
+
contractAddress: params.contractAddress,
|
|
43317
|
+
abi,
|
|
43318
|
+
tokenIds: batch,
|
|
43319
|
+
chainId: params.chainId,
|
|
43320
|
+
isERC1155: true
|
|
43321
|
+
});
|
|
43322
|
+
// Convert batch results to TokenBalance format
|
|
43323
|
+
validTokens = batchResults
|
|
43324
|
+
.filter(r => r.success && r.balance > 0n)
|
|
43325
|
+
.map(r => ({
|
|
43326
|
+
tokenId: r.tokenId,
|
|
43327
|
+
balance: Number(r.balance),
|
|
43328
|
+
hasBalance: true,
|
|
43329
|
+
metadata: null
|
|
43330
|
+
}));
|
|
43331
|
+
// Track errors
|
|
43332
|
+
batchResults.filter(r => !r.success).forEach(r => {
|
|
43333
|
+
errors.push(`${r.tokenId}: ${r.error}`);
|
|
43334
|
+
});
|
|
43335
|
+
}
|
|
41587
43336
|
// Step 2: Get metadata for tokens that have balance
|
|
41588
43337
|
if (validTokens.length > 0) {
|
|
41589
43338
|
const metadataPromises = validTokens.map(token => this.getTokenMetadata({
|
|
@@ -41707,6 +43456,627 @@ class ContractDomainService {
|
|
|
41707
43456
|
}
|
|
41708
43457
|
}
|
|
41709
43458
|
|
|
43459
|
+
/**
|
|
43460
|
+
* TokenCollectionManager - Auto-refresh token collections on wallet events
|
|
43461
|
+
*
|
|
43462
|
+
* This manager provides:
|
|
43463
|
+
* 1. Cached token collection storage with automatic refresh
|
|
43464
|
+
* 2. Auto-subscription to wallet events (transfers, mints, burns)
|
|
43465
|
+
* 3. Targeted invalidation based on contract address
|
|
43466
|
+
* 4. Subscriber pattern for reactive updates
|
|
43467
|
+
*
|
|
43468
|
+
* @example
|
|
43469
|
+
* ```typescript
|
|
43470
|
+
* // Get from SDK
|
|
43471
|
+
* const manager = sdk.tokens.collectionManager;
|
|
43472
|
+
*
|
|
43473
|
+
* // Subscribe to collection updates
|
|
43474
|
+
* const unsubscribe = manager.subscribe((collection, contractAddress) => {
|
|
43475
|
+
* console.log(`Collection updated for ${contractAddress}`);
|
|
43476
|
+
* updateUI(collection);
|
|
43477
|
+
* });
|
|
43478
|
+
*
|
|
43479
|
+
* // Fetch collection (auto-cached, auto-refreshed on wallet events)
|
|
43480
|
+
* const collection = await manager.getCollection({
|
|
43481
|
+
* accountAddress: '0x...',
|
|
43482
|
+
* contractAddress: '0x...',
|
|
43483
|
+
* abi: contractAbi,
|
|
43484
|
+
* chainId: 39123,
|
|
43485
|
+
* tokenIds: ['1', '2', '3']
|
|
43486
|
+
* });
|
|
43487
|
+
*
|
|
43488
|
+
* // Cleanup
|
|
43489
|
+
* unsubscribe();
|
|
43490
|
+
* ```
|
|
43491
|
+
*/
|
|
43492
|
+
/**
|
|
43493
|
+
* Manages token collections with automatic refresh on wallet events
|
|
43494
|
+
*/
|
|
43495
|
+
class TokenCollectionManager {
|
|
43496
|
+
constructor(tokenService, eventEmitter, config) {
|
|
43497
|
+
this.tokenService = tokenService;
|
|
43498
|
+
this.eventEmitter = eventEmitter;
|
|
43499
|
+
this.collections = new Map();
|
|
43500
|
+
this.handlers = new Set();
|
|
43501
|
+
this.pendingRefresh = new Map();
|
|
43502
|
+
this.config = {
|
|
43503
|
+
autoRefreshOnWalletEvents: config?.autoRefreshOnWalletEvents ?? true,
|
|
43504
|
+
eventDebounceMs: config?.eventDebounceMs ?? 500,
|
|
43505
|
+
debug: config?.debug ?? false
|
|
43506
|
+
};
|
|
43507
|
+
if (this.config.autoRefreshOnWalletEvents && eventEmitter) {
|
|
43508
|
+
this.setupEventSubscription();
|
|
43509
|
+
}
|
|
43510
|
+
}
|
|
43511
|
+
/**
|
|
43512
|
+
* Subscribe to wallet events for auto-refresh
|
|
43513
|
+
*/
|
|
43514
|
+
setupEventSubscription() {
|
|
43515
|
+
if (!this.eventEmitter) {
|
|
43516
|
+
return;
|
|
43517
|
+
}
|
|
43518
|
+
this.eventUnsubscribe = this.eventEmitter.subscribe((event) => {
|
|
43519
|
+
const contractAddress = event.details?.contractAddress;
|
|
43520
|
+
this.handleWalletEvent(contractAddress, event.type);
|
|
43521
|
+
}, { domains: ['wallet'] });
|
|
43522
|
+
}
|
|
43523
|
+
/**
|
|
43524
|
+
* Get a token collection (cached, auto-refreshed on wallet events)
|
|
43525
|
+
*
|
|
43526
|
+
* @param params - Collection parameters
|
|
43527
|
+
* @returns Promise resolving to the token collection
|
|
43528
|
+
*/
|
|
43529
|
+
async getCollection(params) {
|
|
43530
|
+
const key = this.getCacheKey(params);
|
|
43531
|
+
const cached = this.collections.get(key);
|
|
43532
|
+
// Return cached if available and not stale
|
|
43533
|
+
if (cached?.lastCollection) {
|
|
43534
|
+
this.log(`Cache HIT for ${key}`);
|
|
43535
|
+
return cached.lastCollection;
|
|
43536
|
+
}
|
|
43537
|
+
this.log(`Cache MISS for ${key}, fetching...`);
|
|
43538
|
+
return this.fetchAndCache(params);
|
|
43539
|
+
}
|
|
43540
|
+
/**
|
|
43541
|
+
* Force refresh a specific collection
|
|
43542
|
+
*
|
|
43543
|
+
* @param contractAddress - Contract address to refresh
|
|
43544
|
+
* @param accountAddress - Account address to refresh
|
|
43545
|
+
* @returns Promise that resolves when refresh is complete
|
|
43546
|
+
*/
|
|
43547
|
+
async refreshCollection(contractAddress, accountAddress) {
|
|
43548
|
+
const toRefresh = [];
|
|
43549
|
+
for (const [key, entry] of this.collections) {
|
|
43550
|
+
if (entry.params.contractAddress === contractAddress) {
|
|
43551
|
+
if (!accountAddress || entry.params.accountAddress === accountAddress) {
|
|
43552
|
+
toRefresh.push(key);
|
|
43553
|
+
}
|
|
43554
|
+
}
|
|
43555
|
+
}
|
|
43556
|
+
await Promise.all(toRefresh.map(key => {
|
|
43557
|
+
const entry = this.collections.get(key);
|
|
43558
|
+
if (entry) {
|
|
43559
|
+
return this.refreshEntry(key, entry, 'refresh');
|
|
43560
|
+
}
|
|
43561
|
+
}));
|
|
43562
|
+
}
|
|
43563
|
+
/**
|
|
43564
|
+
* Subscribe to collection change events
|
|
43565
|
+
*
|
|
43566
|
+
* @param handler - Callback when any tracked collection changes
|
|
43567
|
+
* @returns Unsubscribe function
|
|
43568
|
+
*/
|
|
43569
|
+
subscribe(handler) {
|
|
43570
|
+
this.handlers.add(handler);
|
|
43571
|
+
return () => this.handlers.delete(handler);
|
|
43572
|
+
}
|
|
43573
|
+
/**
|
|
43574
|
+
* Get all currently tracked collections
|
|
43575
|
+
*/
|
|
43576
|
+
getTrackedCollections() {
|
|
43577
|
+
return Array.from(this.collections.entries()).map(([_, entry]) => ({
|
|
43578
|
+
contractAddress: entry.params.contractAddress,
|
|
43579
|
+
accountAddress: entry.params.accountAddress,
|
|
43580
|
+
collection: entry.lastCollection
|
|
43581
|
+
}));
|
|
43582
|
+
}
|
|
43583
|
+
/**
|
|
43584
|
+
* Check if a collection is currently being tracked
|
|
43585
|
+
*/
|
|
43586
|
+
isTracking(contractAddress, accountAddress) {
|
|
43587
|
+
const key = `${contractAddress}:${accountAddress}`;
|
|
43588
|
+
return this.collections.has(key);
|
|
43589
|
+
}
|
|
43590
|
+
/**
|
|
43591
|
+
* Clear tracking for a specific collection
|
|
43592
|
+
*/
|
|
43593
|
+
untrack(contractAddress, accountAddress) {
|
|
43594
|
+
if (accountAddress) {
|
|
43595
|
+
const key = `${contractAddress}:${accountAddress}`;
|
|
43596
|
+
this.collections.delete(key);
|
|
43597
|
+
this.log(`Untracked ${key}`);
|
|
43598
|
+
}
|
|
43599
|
+
else {
|
|
43600
|
+
// Untrack all for this contract
|
|
43601
|
+
for (const key of this.collections.keys()) {
|
|
43602
|
+
if (key.startsWith(`${contractAddress}:`)) {
|
|
43603
|
+
this.collections.delete(key);
|
|
43604
|
+
}
|
|
43605
|
+
}
|
|
43606
|
+
this.log(`Untracked all collections for ${contractAddress}`);
|
|
43607
|
+
}
|
|
43608
|
+
}
|
|
43609
|
+
/**
|
|
43610
|
+
* Clear all tracked collections
|
|
43611
|
+
*/
|
|
43612
|
+
clearAll() {
|
|
43613
|
+
this.collections.clear();
|
|
43614
|
+
this.log('Cleared all tracked collections');
|
|
43615
|
+
}
|
|
43616
|
+
/**
|
|
43617
|
+
* Handle wallet events (with debouncing)
|
|
43618
|
+
*/
|
|
43619
|
+
handleWalletEvent(contractAddress, eventType) {
|
|
43620
|
+
// Determine which collections to refresh
|
|
43621
|
+
const keysToRefresh = [];
|
|
43622
|
+
for (const [key, entry] of this.collections) {
|
|
43623
|
+
// If no specific contract, refresh all; otherwise match contract
|
|
43624
|
+
if (!contractAddress || entry.params.contractAddress.toLowerCase() === contractAddress.toLowerCase()) {
|
|
43625
|
+
keysToRefresh.push(key);
|
|
43626
|
+
}
|
|
43627
|
+
}
|
|
43628
|
+
if (keysToRefresh.length === 0) {
|
|
43629
|
+
this.log(`No tracked collections affected by event for ${contractAddress || 'all contracts'}`);
|
|
43630
|
+
return;
|
|
43631
|
+
}
|
|
43632
|
+
// Debounce refresh per key
|
|
43633
|
+
for (const key of keysToRefresh) {
|
|
43634
|
+
const existing = this.pendingRefresh.get(key);
|
|
43635
|
+
if (existing) {
|
|
43636
|
+
clearTimeout(existing);
|
|
43637
|
+
}
|
|
43638
|
+
const timeout = setTimeout(async () => {
|
|
43639
|
+
this.pendingRefresh.delete(key);
|
|
43640
|
+
const entry = this.collections.get(key);
|
|
43641
|
+
if (entry) {
|
|
43642
|
+
this.log(`Refreshing ${key} due to wallet event: ${eventType}`);
|
|
43643
|
+
await this.refreshEntry(key, entry, eventType);
|
|
43644
|
+
}
|
|
43645
|
+
}, this.config.eventDebounceMs);
|
|
43646
|
+
this.pendingRefresh.set(key, timeout);
|
|
43647
|
+
}
|
|
43648
|
+
}
|
|
43649
|
+
/**
|
|
43650
|
+
* Fetch and cache a collection
|
|
43651
|
+
*/
|
|
43652
|
+
async fetchAndCache(params) {
|
|
43653
|
+
const key = this.getCacheKey(params);
|
|
43654
|
+
// Mark as refreshing to prevent duplicate fetches
|
|
43655
|
+
const existing = this.collections.get(key);
|
|
43656
|
+
if (existing?.refreshing) {
|
|
43657
|
+
// Wait for existing refresh to complete
|
|
43658
|
+
return existing.lastCollection || this.emptyCollection(params);
|
|
43659
|
+
}
|
|
43660
|
+
this.collections.set(key, {
|
|
43661
|
+
params,
|
|
43662
|
+
lastCollection: null,
|
|
43663
|
+
timestamp: Date.now(),
|
|
43664
|
+
refreshing: true
|
|
43665
|
+
});
|
|
43666
|
+
try {
|
|
43667
|
+
const collection = await this.tokenService.getTokenCollection(params);
|
|
43668
|
+
this.collections.set(key, {
|
|
43669
|
+
params,
|
|
43670
|
+
lastCollection: collection,
|
|
43671
|
+
timestamp: Date.now(),
|
|
43672
|
+
refreshing: false
|
|
43673
|
+
});
|
|
43674
|
+
this.log(`Cached collection for ${key}: ${collection.tokensRetrieved} tokens`);
|
|
43675
|
+
return collection;
|
|
43676
|
+
}
|
|
43677
|
+
catch (error) {
|
|
43678
|
+
// Remove failed entry
|
|
43679
|
+
this.collections.delete(key);
|
|
43680
|
+
throw error;
|
|
43681
|
+
}
|
|
43682
|
+
}
|
|
43683
|
+
/**
|
|
43684
|
+
* Refresh a cached entry and notify handlers
|
|
43685
|
+
*/
|
|
43686
|
+
async refreshEntry(key, entry, source) {
|
|
43687
|
+
if (entry.refreshing) {
|
|
43688
|
+
this.log(`Skipping refresh for ${key} - already refreshing`);
|
|
43689
|
+
return;
|
|
43690
|
+
}
|
|
43691
|
+
entry.refreshing = true;
|
|
43692
|
+
try {
|
|
43693
|
+
const newCollection = await this.tokenService.getTokenCollection(entry.params);
|
|
43694
|
+
entry.lastCollection = newCollection;
|
|
43695
|
+
entry.timestamp = Date.now();
|
|
43696
|
+
entry.refreshing = false;
|
|
43697
|
+
this.log(`Refreshed ${key}: ${newCollection.tokensRetrieved} tokens`);
|
|
43698
|
+
// Notify subscribers
|
|
43699
|
+
for (const handler of this.handlers) {
|
|
43700
|
+
try {
|
|
43701
|
+
handler(newCollection, entry.params.contractAddress, {
|
|
43702
|
+
type: source === 'refresh' ? 'refresh' : 'wallet_event',
|
|
43703
|
+
source
|
|
43704
|
+
});
|
|
43705
|
+
}
|
|
43706
|
+
catch (handlerError) {
|
|
43707
|
+
console.error('[TokenCollectionManager] Handler error:', handlerError);
|
|
43708
|
+
}
|
|
43709
|
+
}
|
|
43710
|
+
}
|
|
43711
|
+
catch (error) {
|
|
43712
|
+
entry.refreshing = false;
|
|
43713
|
+
console.error(`[TokenCollectionManager] Refresh failed for ${key}:`, error);
|
|
43714
|
+
}
|
|
43715
|
+
}
|
|
43716
|
+
/**
|
|
43717
|
+
* Generate cache key from params
|
|
43718
|
+
*/
|
|
43719
|
+
getCacheKey(params) {
|
|
43720
|
+
return `${params.contractAddress}:${params.accountAddress}`;
|
|
43721
|
+
}
|
|
43722
|
+
/**
|
|
43723
|
+
* Create empty collection placeholder
|
|
43724
|
+
*/
|
|
43725
|
+
emptyCollection(params) {
|
|
43726
|
+
return {
|
|
43727
|
+
accountAddress: params.accountAddress,
|
|
43728
|
+
contractAddress: params.contractAddress,
|
|
43729
|
+
totalBalance: 0,
|
|
43730
|
+
tokensRetrieved: 0,
|
|
43731
|
+
tokens: [],
|
|
43732
|
+
note: 'Loading...'
|
|
43733
|
+
};
|
|
43734
|
+
}
|
|
43735
|
+
/**
|
|
43736
|
+
* Debug logging
|
|
43737
|
+
*/
|
|
43738
|
+
log(message, data) {
|
|
43739
|
+
if (this.config.debug) {
|
|
43740
|
+
console.log(`[TokenCollectionManager] ${message}`, data || '');
|
|
43741
|
+
}
|
|
43742
|
+
}
|
|
43743
|
+
/**
|
|
43744
|
+
* Cleanup resources
|
|
43745
|
+
*/
|
|
43746
|
+
destroy() {
|
|
43747
|
+
// Clear pending refreshes
|
|
43748
|
+
for (const timeout of this.pendingRefresh.values()) {
|
|
43749
|
+
clearTimeout(timeout);
|
|
43750
|
+
}
|
|
43751
|
+
this.pendingRefresh.clear();
|
|
43752
|
+
// Unsubscribe from events
|
|
43753
|
+
this.eventUnsubscribe?.();
|
|
43754
|
+
// Clear handlers and collections
|
|
43755
|
+
this.handlers.clear();
|
|
43756
|
+
this.collections.clear();
|
|
43757
|
+
this.log('Destroyed');
|
|
43758
|
+
}
|
|
43759
|
+
}
|
|
43760
|
+
|
|
43761
|
+
/**
|
|
43762
|
+
* BalanceManager - Auto-refresh ERC-20 token balances on wallet events
|
|
43763
|
+
*
|
|
43764
|
+
* This manager provides:
|
|
43765
|
+
* 1. Cached balance storage with automatic refresh
|
|
43766
|
+
* 2. Auto-subscription to wallet events (transfers)
|
|
43767
|
+
* 3. Targeted invalidation based on contract address
|
|
43768
|
+
* 4. Subscriber pattern for reactive updates
|
|
43769
|
+
*
|
|
43770
|
+
* @example
|
|
43771
|
+
* ```typescript
|
|
43772
|
+
* // Get from SDK
|
|
43773
|
+
* const manager = sdk.tokens.balanceManager;
|
|
43774
|
+
*
|
|
43775
|
+
* // Subscribe to balance updates
|
|
43776
|
+
* const unsubscribe = manager.subscribe((balance, contractAddress) => {
|
|
43777
|
+
* console.log(`Balance updated for ${contractAddress}: ${balance}`);
|
|
43778
|
+
* updateUI(balance);
|
|
43779
|
+
* });
|
|
43780
|
+
*
|
|
43781
|
+
* // Fetch balance (auto-cached, auto-refreshed on wallet events)
|
|
43782
|
+
* const balance = await manager.getBalance({
|
|
43783
|
+
* accountAddress: '0x...',
|
|
43784
|
+
* contractAddress: '0x...',
|
|
43785
|
+
* abi: contractAbi,
|
|
43786
|
+
* chainId: 39123
|
|
43787
|
+
* });
|
|
43788
|
+
*
|
|
43789
|
+
* // Manual refresh
|
|
43790
|
+
* await manager.refreshBalance(contractAddress);
|
|
43791
|
+
*
|
|
43792
|
+
* // Cleanup
|
|
43793
|
+
* unsubscribe();
|
|
43794
|
+
* ```
|
|
43795
|
+
*/
|
|
43796
|
+
/**
|
|
43797
|
+
* Manages ERC-20 token balances with automatic refresh on wallet events
|
|
43798
|
+
*/
|
|
43799
|
+
class BalanceManager {
|
|
43800
|
+
constructor(tokenService, eventEmitter, config) {
|
|
43801
|
+
this.tokenService = tokenService;
|
|
43802
|
+
this.eventEmitter = eventEmitter;
|
|
43803
|
+
this.balances = new Map();
|
|
43804
|
+
this.handlers = new Set();
|
|
43805
|
+
this.pendingRefresh = new Map();
|
|
43806
|
+
this.config = {
|
|
43807
|
+
autoRefreshOnWalletEvents: config?.autoRefreshOnWalletEvents ?? true,
|
|
43808
|
+
eventDebounceMs: config?.eventDebounceMs ?? 500,
|
|
43809
|
+
debug: config?.debug ?? false
|
|
43810
|
+
};
|
|
43811
|
+
if (this.config.autoRefreshOnWalletEvents && eventEmitter) {
|
|
43812
|
+
this.setupEventSubscription();
|
|
43813
|
+
}
|
|
43814
|
+
}
|
|
43815
|
+
/**
|
|
43816
|
+
* Subscribe to wallet events for auto-refresh
|
|
43817
|
+
*/
|
|
43818
|
+
setupEventSubscription() {
|
|
43819
|
+
if (!this.eventEmitter) {
|
|
43820
|
+
return;
|
|
43821
|
+
}
|
|
43822
|
+
this.eventUnsubscribe = this.eventEmitter.subscribe((event) => {
|
|
43823
|
+
const contractAddress = event.details?.contractAddress;
|
|
43824
|
+
this.handleWalletEvent(contractAddress, event.type);
|
|
43825
|
+
}, { domains: ['wallet'] });
|
|
43826
|
+
}
|
|
43827
|
+
/**
|
|
43828
|
+
* Get a token balance (cached, auto-refreshed on wallet events)
|
|
43829
|
+
*
|
|
43830
|
+
* @param params - Balance parameters
|
|
43831
|
+
* @returns Promise resolving to the token balance
|
|
43832
|
+
*/
|
|
43833
|
+
async getBalance(params) {
|
|
43834
|
+
const key = this.getCacheKey(params);
|
|
43835
|
+
const cached = this.balances.get(key);
|
|
43836
|
+
// Return cached if available and not stale
|
|
43837
|
+
if (cached?.lastBalance) {
|
|
43838
|
+
this.log(`Cache HIT for ${key}`);
|
|
43839
|
+
return cached.lastBalance;
|
|
43840
|
+
}
|
|
43841
|
+
this.log(`Cache MISS for ${key}, fetching...`);
|
|
43842
|
+
return this.fetchAndCache(params);
|
|
43843
|
+
}
|
|
43844
|
+
/**
|
|
43845
|
+
* Force refresh a specific balance
|
|
43846
|
+
*
|
|
43847
|
+
* @param contractAddress - Contract address to refresh
|
|
43848
|
+
* @param accountAddress - Account address to refresh (optional, refreshes all if not provided)
|
|
43849
|
+
* @returns Promise that resolves when refresh is complete
|
|
43850
|
+
*/
|
|
43851
|
+
async refreshBalance(contractAddress, accountAddress) {
|
|
43852
|
+
const toRefresh = [];
|
|
43853
|
+
for (const [key, entry] of this.balances) {
|
|
43854
|
+
if (entry.params.contractAddress.toLowerCase() === contractAddress.toLowerCase()) {
|
|
43855
|
+
if (!accountAddress || entry.params.accountAddress.toLowerCase() === accountAddress.toLowerCase()) {
|
|
43856
|
+
toRefresh.push(key);
|
|
43857
|
+
}
|
|
43858
|
+
}
|
|
43859
|
+
}
|
|
43860
|
+
await Promise.all(toRefresh.map(key => {
|
|
43861
|
+
const entry = this.balances.get(key);
|
|
43862
|
+
if (entry) {
|
|
43863
|
+
return this.refreshEntry(key, entry, 'refresh');
|
|
43864
|
+
}
|
|
43865
|
+
}));
|
|
43866
|
+
}
|
|
43867
|
+
/**
|
|
43868
|
+
* Force refresh all tracked balances
|
|
43869
|
+
*
|
|
43870
|
+
* @returns Promise that resolves when all refreshes are complete
|
|
43871
|
+
*/
|
|
43872
|
+
async refreshAll() {
|
|
43873
|
+
const refreshPromises = Array.from(this.balances.entries()).map(([key, entry]) => this.refreshEntry(key, entry, 'refresh'));
|
|
43874
|
+
await Promise.all(refreshPromises);
|
|
43875
|
+
}
|
|
43876
|
+
/**
|
|
43877
|
+
* Subscribe to balance change events
|
|
43878
|
+
*
|
|
43879
|
+
* @param handler - Callback when any tracked balance changes
|
|
43880
|
+
* @returns Unsubscribe function
|
|
43881
|
+
*/
|
|
43882
|
+
subscribe(handler) {
|
|
43883
|
+
this.handlers.add(handler);
|
|
43884
|
+
return () => this.handlers.delete(handler);
|
|
43885
|
+
}
|
|
43886
|
+
/**
|
|
43887
|
+
* Get all currently tracked balances
|
|
43888
|
+
*/
|
|
43889
|
+
getTrackedBalances() {
|
|
43890
|
+
return Array.from(this.balances.entries()).map(([_, entry]) => ({
|
|
43891
|
+
contractAddress: entry.params.contractAddress,
|
|
43892
|
+
accountAddress: entry.params.accountAddress,
|
|
43893
|
+
balance: entry.lastBalance
|
|
43894
|
+
}));
|
|
43895
|
+
}
|
|
43896
|
+
/**
|
|
43897
|
+
* Check if a balance is currently being tracked
|
|
43898
|
+
*/
|
|
43899
|
+
isTracking(contractAddress, accountAddress) {
|
|
43900
|
+
const key = `${contractAddress.toLowerCase()}:${accountAddress.toLowerCase()}`;
|
|
43901
|
+
return this.balances.has(key);
|
|
43902
|
+
}
|
|
43903
|
+
/**
|
|
43904
|
+
* Clear tracking for a specific balance
|
|
43905
|
+
*/
|
|
43906
|
+
untrack(contractAddress, accountAddress) {
|
|
43907
|
+
if (accountAddress) {
|
|
43908
|
+
const key = `${contractAddress.toLowerCase()}:${accountAddress.toLowerCase()}`;
|
|
43909
|
+
this.balances.delete(key);
|
|
43910
|
+
this.log(`Untracked ${key}`);
|
|
43911
|
+
}
|
|
43912
|
+
else {
|
|
43913
|
+
// Untrack all for this contract
|
|
43914
|
+
const toDelete = [];
|
|
43915
|
+
for (const key of this.balances.keys()) {
|
|
43916
|
+
if (key.startsWith(`${contractAddress.toLowerCase()}:`)) {
|
|
43917
|
+
toDelete.push(key);
|
|
43918
|
+
}
|
|
43919
|
+
}
|
|
43920
|
+
toDelete.forEach(key => this.balances.delete(key));
|
|
43921
|
+
this.log(`Untracked all balances for ${contractAddress}`);
|
|
43922
|
+
}
|
|
43923
|
+
}
|
|
43924
|
+
/**
|
|
43925
|
+
* Clear all tracked balances
|
|
43926
|
+
*/
|
|
43927
|
+
clearAll() {
|
|
43928
|
+
this.balances.clear();
|
|
43929
|
+
this.log('Cleared all tracked balances');
|
|
43930
|
+
}
|
|
43931
|
+
/**
|
|
43932
|
+
* Handle wallet events (with debouncing)
|
|
43933
|
+
*/
|
|
43934
|
+
handleWalletEvent(contractAddress, eventType) {
|
|
43935
|
+
// Determine which balances to refresh
|
|
43936
|
+
const keysToRefresh = [];
|
|
43937
|
+
for (const [key, entry] of this.balances) {
|
|
43938
|
+
// If no specific contract, refresh all; otherwise match contract
|
|
43939
|
+
if (!contractAddress || entry.params.contractAddress.toLowerCase() === contractAddress.toLowerCase()) {
|
|
43940
|
+
keysToRefresh.push(key);
|
|
43941
|
+
}
|
|
43942
|
+
}
|
|
43943
|
+
if (keysToRefresh.length === 0) {
|
|
43944
|
+
this.log(`No tracked balances affected by event for ${contractAddress || 'all contracts'}`);
|
|
43945
|
+
return;
|
|
43946
|
+
}
|
|
43947
|
+
// Debounce refresh per key
|
|
43948
|
+
for (const key of keysToRefresh) {
|
|
43949
|
+
const existing = this.pendingRefresh.get(key);
|
|
43950
|
+
if (existing) {
|
|
43951
|
+
clearTimeout(existing);
|
|
43952
|
+
}
|
|
43953
|
+
const timeout = setTimeout(async () => {
|
|
43954
|
+
this.pendingRefresh.delete(key);
|
|
43955
|
+
const entry = this.balances.get(key);
|
|
43956
|
+
if (entry) {
|
|
43957
|
+
this.log(`Refreshing ${key} due to wallet event: ${eventType}`);
|
|
43958
|
+
await this.refreshEntry(key, entry, eventType);
|
|
43959
|
+
}
|
|
43960
|
+
}, this.config.eventDebounceMs);
|
|
43961
|
+
this.pendingRefresh.set(key, timeout);
|
|
43962
|
+
}
|
|
43963
|
+
}
|
|
43964
|
+
/**
|
|
43965
|
+
* Fetch and cache a balance
|
|
43966
|
+
*/
|
|
43967
|
+
async fetchAndCache(params) {
|
|
43968
|
+
const key = this.getCacheKey(params);
|
|
43969
|
+
// Mark as refreshing to prevent duplicate fetches
|
|
43970
|
+
const existing = this.balances.get(key);
|
|
43971
|
+
if (existing?.refreshing) {
|
|
43972
|
+
// Wait for existing refresh to complete
|
|
43973
|
+
return existing.lastBalance || this.emptyBalance();
|
|
43974
|
+
}
|
|
43975
|
+
this.balances.set(key, {
|
|
43976
|
+
params,
|
|
43977
|
+
lastBalance: null,
|
|
43978
|
+
timestamp: Date.now(),
|
|
43979
|
+
refreshing: true
|
|
43980
|
+
});
|
|
43981
|
+
try {
|
|
43982
|
+
const balance = await this.tokenService.getTokenBalance({
|
|
43983
|
+
...params,
|
|
43984
|
+
tokenId: null // ERC-20 has no tokenId
|
|
43985
|
+
});
|
|
43986
|
+
this.balances.set(key, {
|
|
43987
|
+
params,
|
|
43988
|
+
lastBalance: balance,
|
|
43989
|
+
timestamp: Date.now(),
|
|
43990
|
+
refreshing: false
|
|
43991
|
+
});
|
|
43992
|
+
this.log(`Cached balance for ${key}: ${balance.balance}`);
|
|
43993
|
+
return balance;
|
|
43994
|
+
}
|
|
43995
|
+
catch (error) {
|
|
43996
|
+
// Remove failed entry
|
|
43997
|
+
this.balances.delete(key);
|
|
43998
|
+
throw error;
|
|
43999
|
+
}
|
|
44000
|
+
}
|
|
44001
|
+
/**
|
|
44002
|
+
* Refresh a cached entry and notify handlers
|
|
44003
|
+
*/
|
|
44004
|
+
async refreshEntry(key, entry, source) {
|
|
44005
|
+
if (entry.refreshing) {
|
|
44006
|
+
this.log(`Skipping refresh for ${key} - already refreshing`);
|
|
44007
|
+
return;
|
|
44008
|
+
}
|
|
44009
|
+
entry.refreshing = true;
|
|
44010
|
+
try {
|
|
44011
|
+
const newBalance = await this.tokenService.getTokenBalance({
|
|
44012
|
+
...entry.params,
|
|
44013
|
+
tokenId: null // ERC-20 has no tokenId
|
|
44014
|
+
});
|
|
44015
|
+
entry.lastBalance = newBalance;
|
|
44016
|
+
entry.timestamp = Date.now();
|
|
44017
|
+
entry.refreshing = false;
|
|
44018
|
+
this.log(`Refreshed ${key}: ${newBalance.balance}`);
|
|
44019
|
+
// Notify subscribers
|
|
44020
|
+
for (const handler of this.handlers) {
|
|
44021
|
+
try {
|
|
44022
|
+
handler(newBalance, entry.params.contractAddress, {
|
|
44023
|
+
type: source === 'refresh' ? 'refresh' : 'wallet_event',
|
|
44024
|
+
source
|
|
44025
|
+
});
|
|
44026
|
+
}
|
|
44027
|
+
catch (handlerError) {
|
|
44028
|
+
console.error('[BalanceManager] Handler error:', handlerError);
|
|
44029
|
+
}
|
|
44030
|
+
}
|
|
44031
|
+
}
|
|
44032
|
+
catch (error) {
|
|
44033
|
+
entry.refreshing = false;
|
|
44034
|
+
console.error(`[BalanceManager] Refresh failed for ${key}:`, error);
|
|
44035
|
+
}
|
|
44036
|
+
}
|
|
44037
|
+
/**
|
|
44038
|
+
* Generate cache key from params
|
|
44039
|
+
*/
|
|
44040
|
+
getCacheKey(params) {
|
|
44041
|
+
return `${params.contractAddress.toLowerCase()}:${params.accountAddress.toLowerCase()}`;
|
|
44042
|
+
}
|
|
44043
|
+
/**
|
|
44044
|
+
* Create empty balance placeholder
|
|
44045
|
+
*/
|
|
44046
|
+
emptyBalance() {
|
|
44047
|
+
return {
|
|
44048
|
+
tokenId: null,
|
|
44049
|
+
balance: 0,
|
|
44050
|
+
hasBalance: false,
|
|
44051
|
+
metadata: null
|
|
44052
|
+
};
|
|
44053
|
+
}
|
|
44054
|
+
/**
|
|
44055
|
+
* Debug logging
|
|
44056
|
+
*/
|
|
44057
|
+
log(message, data) {
|
|
44058
|
+
if (this.config.debug) {
|
|
44059
|
+
console.log(`[BalanceManager] ${message}`, data || '');
|
|
44060
|
+
}
|
|
44061
|
+
}
|
|
44062
|
+
/**
|
|
44063
|
+
* Cleanup resources
|
|
44064
|
+
*/
|
|
44065
|
+
destroy() {
|
|
44066
|
+
// Clear pending refreshes
|
|
44067
|
+
for (const timeout of this.pendingRefresh.values()) {
|
|
44068
|
+
clearTimeout(timeout);
|
|
44069
|
+
}
|
|
44070
|
+
this.pendingRefresh.clear();
|
|
44071
|
+
// Unsubscribe from events
|
|
44072
|
+
this.eventUnsubscribe?.();
|
|
44073
|
+
// Clear handlers and balances
|
|
44074
|
+
this.handlers.clear();
|
|
44075
|
+
this.balances.clear();
|
|
44076
|
+
this.log('Destroyed');
|
|
44077
|
+
}
|
|
44078
|
+
}
|
|
44079
|
+
|
|
41710
44080
|
/**
|
|
41711
44081
|
* Web3ApplicationService - Application layer entrance point
|
|
41712
44082
|
* Orchestrates domain services and provides clean public interface
|
|
@@ -41815,6 +44185,30 @@ class Web3InfrastructureApi {
|
|
|
41815
44185
|
return 0;
|
|
41816
44186
|
}
|
|
41817
44187
|
}
|
|
44188
|
+
/**
|
|
44189
|
+
* Get multiple ERC-1155 token balances in a single RPC call using native balanceOfBatch.
|
|
44190
|
+
* Falls back to individual calls if balanceOfBatch fails.
|
|
44191
|
+
*
|
|
44192
|
+
* @param request - Batch balance request
|
|
44193
|
+
* @returns Array of balance results per tokenId
|
|
44194
|
+
*/
|
|
44195
|
+
async getBatchTokenBalances(request) {
|
|
44196
|
+
try {
|
|
44197
|
+
const ethersProvider = await this.web3ChainService.getEthersProviderByChainId(request.chainId);
|
|
44198
|
+
const results = await getBatchBalancesWithFallback(ethersProvider, request.contractAddress, request.abi, request.accountAddress, request.tokenIds, request.isERC1155 ?? true);
|
|
44199
|
+
return results;
|
|
44200
|
+
}
|
|
44201
|
+
catch (error) {
|
|
44202
|
+
console.error(`Failed to get batch token balances:`, error);
|
|
44203
|
+
// Return empty results on failure
|
|
44204
|
+
return request.tokenIds.map(tokenId => ({
|
|
44205
|
+
tokenId,
|
|
44206
|
+
balance: 0n,
|
|
44207
|
+
success: false,
|
|
44208
|
+
error: `${error}`
|
|
44209
|
+
}));
|
|
44210
|
+
}
|
|
44211
|
+
}
|
|
41818
44212
|
async getTokenUri(request) {
|
|
41819
44213
|
try {
|
|
41820
44214
|
const ethersProvider = await this.web3ChainService.getEthersProviderByChainId(request.chainId);
|
|
@@ -42052,17 +44446,11 @@ async function getExplorerUrlByChainId(getChainData, chainId, address, type) {
|
|
|
42052
44446
|
* ```
|
|
42053
44447
|
*/
|
|
42054
44448
|
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) {
|
|
44449
|
+
constructor(apiClient, tenantManager, eventEmitter) {
|
|
42063
44450
|
this.apiClient = apiClient;
|
|
42064
44451
|
// Use provided TenantManager or create one
|
|
42065
44452
|
this.tenantManager = tenantManager || new TenantManager(apiClient);
|
|
44453
|
+
this.eventEmitter = eventEmitter;
|
|
42066
44454
|
// Initialize Web3 Chain service
|
|
42067
44455
|
const web3ChainApi = new Web3ChainApi(apiClient);
|
|
42068
44456
|
this.web3ChainService = new Web3ChainService(web3ChainApi);
|
|
@@ -42070,6 +44458,75 @@ class Web3Manager {
|
|
|
42070
44458
|
const web3InfrastructureApi = new Web3InfrastructureApi(this.web3ChainService);
|
|
42071
44459
|
const ipfsInfrastructureApi = new IPFSInfrastructureApi(this.tenantManager);
|
|
42072
44460
|
this.web3ApplicationService = new Web3ApplicationService(web3InfrastructureApi, ipfsInfrastructureApi);
|
|
44461
|
+
// Initialize domain services for managers
|
|
44462
|
+
const contractDomainService = new ContractDomainService();
|
|
44463
|
+
const metadataDomainService = new MetadataDomainService(ipfsInfrastructureApi);
|
|
44464
|
+
this.tokenDomainService = new TokenDomainService(web3InfrastructureApi, metadataDomainService, contractDomainService);
|
|
44465
|
+
}
|
|
44466
|
+
/**
|
|
44467
|
+
* Get the TokenCollectionManager for NFT collection operations.
|
|
44468
|
+
*
|
|
44469
|
+
* Features:
|
|
44470
|
+
* - Auto-refresh on wallet events (transfers, mints, burns)
|
|
44471
|
+
* - Cached collections
|
|
44472
|
+
* - Subscriber pattern for reactive updates
|
|
44473
|
+
*
|
|
44474
|
+
* @example
|
|
44475
|
+
* ```typescript
|
|
44476
|
+
* const manager = sdk.web3.collectionManager;
|
|
44477
|
+
*
|
|
44478
|
+
* // Subscribe to updates
|
|
44479
|
+
* const unsubscribe = manager.subscribe((collection, contractAddress) => {
|
|
44480
|
+
* console.log(`Collection updated for ${contractAddress}`);
|
|
44481
|
+
* });
|
|
44482
|
+
*
|
|
44483
|
+
* // Fetch collection (auto-cached, auto-refreshed)
|
|
44484
|
+
* const collection = await manager.getCollection({...});
|
|
44485
|
+
*
|
|
44486
|
+
* // Manual refresh
|
|
44487
|
+
* await manager.refreshCollection(contractAddress);
|
|
44488
|
+
* ```
|
|
44489
|
+
*/
|
|
44490
|
+
get collectionManager() {
|
|
44491
|
+
if (!this._collectionManager) {
|
|
44492
|
+
this._collectionManager = new TokenCollectionManager(this.tokenDomainService, this.eventEmitter);
|
|
44493
|
+
}
|
|
44494
|
+
return this._collectionManager;
|
|
44495
|
+
}
|
|
44496
|
+
/**
|
|
44497
|
+
* Get the BalanceManager for ERC-20 (credit token) balance operations.
|
|
44498
|
+
*
|
|
44499
|
+
* Features:
|
|
44500
|
+
* - Auto-refresh on wallet events (transfers)
|
|
44501
|
+
* - Cached balances
|
|
44502
|
+
* - Subscriber pattern for reactive updates
|
|
44503
|
+
*
|
|
44504
|
+
* @example
|
|
44505
|
+
* ```typescript
|
|
44506
|
+
* const manager = sdk.web3.balanceManager;
|
|
44507
|
+
*
|
|
44508
|
+
* // Subscribe to balance updates
|
|
44509
|
+
* const unsubscribe = manager.subscribe((balance, contractAddress) => {
|
|
44510
|
+
* console.log(`Balance updated: ${balance.balance}`);
|
|
44511
|
+
* });
|
|
44512
|
+
*
|
|
44513
|
+
* // Fetch balance (auto-cached, auto-refreshed)
|
|
44514
|
+
* const balance = await manager.getBalance({
|
|
44515
|
+
* accountAddress: '0x...',
|
|
44516
|
+
* contractAddress: '0x...',
|
|
44517
|
+
* abi: creditTokenAbi,
|
|
44518
|
+
* chainId: 39123
|
|
44519
|
+
* });
|
|
44520
|
+
*
|
|
44521
|
+
* // Manual refresh
|
|
44522
|
+
* await manager.refreshBalance(contractAddress);
|
|
44523
|
+
* ```
|
|
44524
|
+
*/
|
|
44525
|
+
get balanceManager() {
|
|
44526
|
+
if (!this._balanceManager) {
|
|
44527
|
+
this._balanceManager = new BalanceManager(this.tokenDomainService, this.eventEmitter);
|
|
44528
|
+
}
|
|
44529
|
+
return this._balanceManager;
|
|
42073
44530
|
}
|
|
42074
44531
|
/**
|
|
42075
44532
|
* Get token balance for a specific token
|
|
@@ -42268,6 +44725,18 @@ class Web3Manager {
|
|
|
42268
44725
|
maxTokens
|
|
42269
44726
|
};
|
|
42270
44727
|
}
|
|
44728
|
+
/**
|
|
44729
|
+
* Cleanup resources.
|
|
44730
|
+
*
|
|
44731
|
+
* Call this when the SDK is being destroyed to clean up
|
|
44732
|
+
* event subscriptions and cached data.
|
|
44733
|
+
*/
|
|
44734
|
+
destroy() {
|
|
44735
|
+
this._collectionManager?.destroy();
|
|
44736
|
+
this._balanceManager?.destroy();
|
|
44737
|
+
this._collectionManager = undefined;
|
|
44738
|
+
this._balanceManager = undefined;
|
|
44739
|
+
}
|
|
42271
44740
|
}
|
|
42272
44741
|
|
|
42273
44742
|
/**
|
|
@@ -43159,6 +45628,83 @@ class BrowserFetchClientAdapter {
|
|
|
43159
45628
|
}
|
|
43160
45629
|
}
|
|
43161
45630
|
|
|
45631
|
+
/**
|
|
45632
|
+
* Build optimized image URL with automatic IPFS resolution and CDN options
|
|
45633
|
+
*
|
|
45634
|
+
* Single entry point for all image URL building:
|
|
45635
|
+
* - IPFS URIs → resolved via gateway + CDN options applied
|
|
45636
|
+
* - HTTP URLs → CDN options applied directly
|
|
45637
|
+
*
|
|
45638
|
+
* @param url - Image URL (ipfs://, https://, or any URL)
|
|
45639
|
+
* @param options - CDN optimization options (preset, quality, format, fit)
|
|
45640
|
+
* @param gateway - IPFS gateway domain (required for ipfs:// URLs)
|
|
45641
|
+
* @returns Optimized URL with query parameters
|
|
45642
|
+
*
|
|
45643
|
+
* @example
|
|
45644
|
+
* ```typescript
|
|
45645
|
+
* // IPFS thumbnail
|
|
45646
|
+
* buildImageUrl('ipfs://QmHash', { preset: 'thumb' }, 'cdn.example.com')
|
|
45647
|
+
* // => 'https://cdn.example.com/ipfs/QmHash?preset=thumb'
|
|
45648
|
+
*
|
|
45649
|
+
* // IPFS hero with high quality
|
|
45650
|
+
* buildImageUrl('ipfs://QmHash', { preset: 'hero', q: 95, fit: 'cover' }, 'cdn.example.com')
|
|
45651
|
+
* // => 'https://cdn.example.com/ipfs/QmHash?preset=hero&q=95&fit=cover'
|
|
45652
|
+
*
|
|
45653
|
+
* // HTTP URL (gateway ignored)
|
|
45654
|
+
* buildImageUrl('https://example.com/photo.jpg', { preset: 'card' })
|
|
45655
|
+
* // => 'https://example.com/photo.jpg?preset=card'
|
|
45656
|
+
*
|
|
45657
|
+
* // No options - just IPFS resolution
|
|
45658
|
+
* buildImageUrl('ipfs://QmHash', undefined, 'cdn.example.com')
|
|
45659
|
+
* // => 'https://cdn.example.com/ipfs/QmHash'
|
|
45660
|
+
* ```
|
|
45661
|
+
*/
|
|
45662
|
+
function buildImageUrl(url, options, gateway) {
|
|
45663
|
+
if (!url)
|
|
45664
|
+
return '';
|
|
45665
|
+
let resolvedUrl = url;
|
|
45666
|
+
// Resolve IPFS to HTTP via gateway
|
|
45667
|
+
if (url.startsWith('ipfs://')) {
|
|
45668
|
+
if (!gateway) {
|
|
45669
|
+
console.warn('[buildImageUrl] IPFS URL requires gateway parameter');
|
|
45670
|
+
return url;
|
|
45671
|
+
}
|
|
45672
|
+
const cid = url.replace('ipfs://', '');
|
|
45673
|
+
resolvedUrl = `https://${gateway}/ipfs/${cid}`;
|
|
45674
|
+
}
|
|
45675
|
+
// Apply CDN options
|
|
45676
|
+
return buildImageCdnUrl(resolvedUrl, options);
|
|
45677
|
+
}
|
|
45678
|
+
/**
|
|
45679
|
+
* Check if a URL is an IPFS URI
|
|
45680
|
+
*
|
|
45681
|
+
* @param url - URL to check
|
|
45682
|
+
* @returns True if URL starts with ipfs://
|
|
45683
|
+
*/
|
|
45684
|
+
function isIpfsUrl(url) {
|
|
45685
|
+
return !!url && url.startsWith('ipfs://');
|
|
45686
|
+
}
|
|
45687
|
+
/**
|
|
45688
|
+
* Extract CID from IPFS URI
|
|
45689
|
+
*
|
|
45690
|
+
* @param ipfsUri - IPFS URI (ipfs://Qm... or ipfs://baf...)
|
|
45691
|
+
* @returns CID string, or null if not a valid IPFS URI
|
|
45692
|
+
*
|
|
45693
|
+
* @example
|
|
45694
|
+
* ```typescript
|
|
45695
|
+
* extractIpfsCid('ipfs://QmHash123')
|
|
45696
|
+
* // => 'QmHash123'
|
|
45697
|
+
*
|
|
45698
|
+
* extractIpfsCid('https://example.com/image.jpg')
|
|
45699
|
+
* // => null
|
|
45700
|
+
* ```
|
|
45701
|
+
*/
|
|
45702
|
+
function extractIpfsCid(ipfsUri) {
|
|
45703
|
+
if (!isIpfsUrl(ipfsUri))
|
|
45704
|
+
return null;
|
|
45705
|
+
return ipfsUri.replace('ipfs://', '');
|
|
45706
|
+
}
|
|
45707
|
+
|
|
43162
45708
|
/**
|
|
43163
45709
|
* React hook for loading and managing token balances
|
|
43164
45710
|
*
|
|
@@ -46833,8 +49379,11 @@ const useTriggerSources = () => {
|
|
|
46833
49379
|
};
|
|
46834
49380
|
};
|
|
46835
49381
|
|
|
49382
|
+
exports.ACTIVE_USER_DEFINITION = ACTIVE_USER_DEFINITION;
|
|
46836
49383
|
exports.ANALYTICS_GROUP_BY_TYPES = ANALYTICS_GROUP_BY_TYPES;
|
|
46837
49384
|
exports.ANALYTICS_METRIC_TYPES = ANALYTICS_METRIC_TYPES;
|
|
49385
|
+
exports.ANALYTICS_VALID_PROCESS_STATUS = ANALYTICS_VALID_PROCESS_STATUS;
|
|
49386
|
+
exports.ANALYTICS_VALID_TRANSACTION_STATUSES = ANALYTICS_VALID_TRANSACTION_STATUSES;
|
|
46838
49387
|
exports.AUTH_STORAGE_KEYS = AUTH_STORAGE_KEYS;
|
|
46839
49388
|
exports.AnalyticsApi = AnalyticsApi;
|
|
46840
49389
|
exports.AnalyticsManager = AnalyticsManager;
|
|
@@ -46851,6 +49400,9 @@ exports.AuthService = AuthService;
|
|
|
46851
49400
|
exports.AuthTokenManager = AuthTokenManager;
|
|
46852
49401
|
exports.AuthenticationError = AuthenticationError;
|
|
46853
49402
|
exports.BaseTokenService = BaseTokenService;
|
|
49403
|
+
exports.BookingApi = BookingApi;
|
|
49404
|
+
exports.BookingManager = BookingManager;
|
|
49405
|
+
exports.BookingService = BookingService;
|
|
46854
49406
|
exports.BrowserFetchClientAdapter = BrowserFetchClientAdapter;
|
|
46855
49407
|
exports.BusinessApi = BusinessApi;
|
|
46856
49408
|
exports.BusinessManager = BusinessManager;
|
|
@@ -46865,6 +49417,9 @@ exports.CampaignService = CampaignService;
|
|
|
46865
49417
|
exports.ChainTypes = ChainTypes;
|
|
46866
49418
|
exports.ClientTransactionType = ClientTransactionType;
|
|
46867
49419
|
exports.CommonErrorCodes = CommonErrorCodes;
|
|
49420
|
+
exports.CustomFieldDefinitionManager = CustomFieldDefinitionManager;
|
|
49421
|
+
exports.DATA_SOURCE_CHANNELS = DATA_SOURCE_CHANNELS;
|
|
49422
|
+
exports.DEFAULT_DATA_SOURCE = DEFAULT_DATA_SOURCE;
|
|
46868
49423
|
exports.DEFAULT_PERS_CONFIG = DEFAULT_PERS_CONFIG;
|
|
46869
49424
|
exports.DPOP_STORAGE_KEYS = DPOP_STORAGE_KEYS;
|
|
46870
49425
|
exports.DPoPManager = DPoPManager;
|
|
@@ -46873,6 +49428,9 @@ exports.Domains = Domains;
|
|
|
46873
49428
|
exports.DonationApi = DonationApi;
|
|
46874
49429
|
exports.DonationManager = DonationManager;
|
|
46875
49430
|
exports.DonationService = DonationService;
|
|
49431
|
+
exports.EMAIL_MAX_LENGTH = EMAIL_MAX_LENGTH;
|
|
49432
|
+
exports.EMAIL_REGEX = EMAIL_REGEX;
|
|
49433
|
+
exports.ENGAGEMENT_RATE_FORMULA = ENGAGEMENT_RATE_FORMULA;
|
|
46876
49434
|
exports.EntityTypes = EntityTypes;
|
|
46877
49435
|
exports.ErrorDomains = ErrorDomains;
|
|
46878
49436
|
exports.ErrorUtils = ErrorUtils;
|
|
@@ -46880,7 +49438,16 @@ exports.FATAL_AUTH_CODES = FATAL_AUTH_CODES;
|
|
|
46880
49438
|
exports.FileApi = FileApi;
|
|
46881
49439
|
exports.FileManager = FileManager;
|
|
46882
49440
|
exports.FileService = FileService;
|
|
49441
|
+
exports.IMAGE_CDN_CACHE_TTL = IMAGE_CDN_CACHE_TTL;
|
|
49442
|
+
exports.IMAGE_CDN_DEFAULT_FIT = IMAGE_CDN_DEFAULT_FIT;
|
|
49443
|
+
exports.IMAGE_CDN_DEFAULT_FORMAT = IMAGE_CDN_DEFAULT_FORMAT;
|
|
49444
|
+
exports.IMAGE_CDN_DEFAULT_QUALITY = IMAGE_CDN_DEFAULT_QUALITY;
|
|
49445
|
+
exports.IMAGE_CDN_MAX_DIMENSION = IMAGE_CDN_MAX_DIMENSION;
|
|
49446
|
+
exports.IMAGE_CDN_MAX_INPUT_SIZE = IMAGE_CDN_MAX_INPUT_SIZE;
|
|
49447
|
+
exports.IMAGE_CDN_PRESETS = IMAGE_CDN_PRESETS;
|
|
49448
|
+
exports.IMAGE_CDN_SUPPORTED_FORMATS = IMAGE_CDN_SUPPORTED_FORMATS;
|
|
46883
49449
|
exports.IndexedDBTokenStorage = IndexedDBTokenStorage;
|
|
49450
|
+
exports.LOCATION_TYPES = LOCATION_TYPES;
|
|
46884
49451
|
exports.LocalStorageTokenStorage = LocalStorageTokenStorage;
|
|
46885
49452
|
exports.LogoutRequired = LogoutRequired;
|
|
46886
49453
|
exports.MEMBERSHIP_ROLE_HIERARCHY = MEMBERSHIP_ROLE_HIERARCHY;
|
|
@@ -46888,6 +49455,7 @@ exports.MemoryTokenStorage = MemoryTokenStorage;
|
|
|
46888
49455
|
exports.NativeTokenTypes = NativeTokenTypes;
|
|
46889
49456
|
exports.NetworkError = NetworkError;
|
|
46890
49457
|
exports.NodeHttpClientAdapter = NodeHttpClientAdapter;
|
|
49458
|
+
exports.OttAuthMethod = OttAuthMethod;
|
|
46891
49459
|
exports.PaymentApi = PurchaseApi;
|
|
46892
49460
|
exports.PaymentService = PaymentService;
|
|
46893
49461
|
exports.PersApiClient = PersApiClient;
|
|
@@ -46932,6 +49500,8 @@ exports.TransactionService = TransactionService;
|
|
|
46932
49500
|
exports.TriggerSourceApi = TriggerSourceApi;
|
|
46933
49501
|
exports.TriggerSourceManager = TriggerSourceManager;
|
|
46934
49502
|
exports.TriggerSourceService = TriggerSourceService;
|
|
49503
|
+
exports.URL_REGEX = URL_REGEX;
|
|
49504
|
+
exports.UUID_REGEX = UUID_REGEX;
|
|
46935
49505
|
exports.UserApi = UserApi;
|
|
46936
49506
|
exports.UserManager = UserManager;
|
|
46937
49507
|
exports.UserService = UserService;
|
|
@@ -46947,6 +49517,7 @@ exports.VALID_RELATIONS = VALID_RELATIONS;
|
|
|
46947
49517
|
exports.VALID_TOKEN_METADATA_RELATIONS = VALID_TOKEN_METADATA_RELATIONS;
|
|
46948
49518
|
exports.VALID_TRANSACTION_RELATIONS = VALID_TRANSACTION_RELATIONS;
|
|
46949
49519
|
exports.VALID_USER_RELATIONS = VALID_USER_RELATIONS;
|
|
49520
|
+
exports.WALLET_ADDRESS_REGEX = WALLET_ADDRESS_REGEX;
|
|
46950
49521
|
exports.WalletEventsManager = WalletEventsManager;
|
|
46951
49522
|
exports.WebDPoPCryptoProvider = WebDPoPCryptoProvider;
|
|
46952
49523
|
exports.WebhookApi = WebhookApi;
|
|
@@ -46955,6 +49526,8 @@ exports.WebhookService = WebhookService;
|
|
|
46955
49526
|
exports.apiPublicKeyTestPrefix = apiPublicKeyTestPrefix;
|
|
46956
49527
|
exports.buildApiRoot = buildApiRoot;
|
|
46957
49528
|
exports.buildBurnRequest = buildBurnRequest;
|
|
49529
|
+
exports.buildImageCdnUrl = buildImageCdnUrl;
|
|
49530
|
+
exports.buildImageUrl = buildImageUrl;
|
|
46958
49531
|
exports.buildMintRequest = buildMintRequest;
|
|
46959
49532
|
exports.buildPOSBurnRequest = buildPOSBurnRequest;
|
|
46960
49533
|
exports.buildPOSTransferRequest = buildPOSTransferRequest;
|
|
@@ -46963,48 +49536,70 @@ exports.buildPendingTransactionData = buildPendingTransactionData;
|
|
|
46963
49536
|
exports.buildSubmissionRequest = buildSubmissionRequest;
|
|
46964
49537
|
exports.buildTransferRequest = buildTransferRequest;
|
|
46965
49538
|
exports.buildWalletEventsWsUrl = buildWalletEventsWsUrl;
|
|
49539
|
+
exports.builtInUserFieldMetadata = builtInUserFieldMetadata;
|
|
49540
|
+
exports.builtInUserFields = builtInUserFields;
|
|
49541
|
+
exports.compareValues = compareValues;
|
|
46966
49542
|
exports.createPersEventsClient = createPersEventsClient;
|
|
46967
49543
|
exports.createPersSDK = createPersSDK;
|
|
46968
49544
|
exports.createReactNativeAuthProvider = createReactNativeAuthProvider;
|
|
46969
49545
|
exports.createUserIdentifierObject = createUserIdentifierObject;
|
|
46970
49546
|
exports.createUserStatusSDK = createUserStatusSDK;
|
|
49547
|
+
exports.customFieldEntityTypes = customFieldEntityTypes;
|
|
46971
49548
|
exports.detectEnvironment = detectEnvironment;
|
|
46972
49549
|
exports.detectIdentifierType = detectIdentifierType;
|
|
46973
49550
|
exports.environment = environment;
|
|
46974
49551
|
exports.extractData = extractData;
|
|
46975
49552
|
exports.extractDeadlineFromSigningData = extractDeadlineFromSigningData;
|
|
49553
|
+
exports.extractIpfsCid = extractIpfsCid;
|
|
46976
49554
|
exports.extractPagination = extractPagination;
|
|
46977
49555
|
exports.fetchAllPages = fetchAllPages;
|
|
49556
|
+
exports.getAllBuiltInFieldDefinitions = getAllBuiltInFieldDefinitions;
|
|
49557
|
+
exports.getBuiltInFieldDefinition = getBuiltInFieldDefinition;
|
|
46978
49558
|
exports.getMetadataFromTokenUnitResponse = getMetadataFromTokenUnitResponse;
|
|
49559
|
+
exports.getOperatorText = getOperatorText;
|
|
49560
|
+
exports.getPresetDimensions = getPresetDimensions;
|
|
46979
49561
|
exports.getSigningUrl = getSigningUrl;
|
|
46980
49562
|
exports.globalCacheService = globalCacheService;
|
|
46981
49563
|
exports.hasMinimumRole = hasMinimumRole;
|
|
46982
49564
|
exports.initializeReactNativePolyfills = initializeReactNativePolyfills;
|
|
49565
|
+
exports.isBuiltInUserField = isBuiltInUserField;
|
|
46983
49566
|
exports.isConnectedMessage = isConnectedMessage;
|
|
49567
|
+
exports.isDataSourceChannel = isDataSourceChannel;
|
|
46984
49568
|
exports.isErrorMessage = isErrorMessage;
|
|
46985
49569
|
exports.isEventMessage = isEventMessage;
|
|
46986
49570
|
exports.isFatalAuthErrorInMessage = isFatalAuthErrorInMessage;
|
|
49571
|
+
exports.isIpfsUrl = isIpfsUrl;
|
|
46987
49572
|
exports.isPaginatedResponse = isPaginatedResponse;
|
|
46988
49573
|
exports.isPingMessage = isPingMessage;
|
|
46989
49574
|
exports.isServerMessage = isServerMessage;
|
|
46990
49575
|
exports.isSubscribedMessage = isSubscribedMessage;
|
|
49576
|
+
exports.isSupportedImageFormat = isSupportedImageFormat;
|
|
49577
|
+
exports.isUUID = isUUID;
|
|
46991
49578
|
exports.isUnsubscribedMessage = isUnsubscribedMessage;
|
|
46992
49579
|
exports.isUserIdentifierObject = isUserIdentifierObject;
|
|
46993
49580
|
exports.isValidBusinessMembershipRelation = isValidBusinessMembershipRelation;
|
|
46994
49581
|
exports.isValidCampaignClaimRelation = isValidCampaignClaimRelation;
|
|
46995
49582
|
exports.isValidCampaignRelation = isValidCampaignRelation;
|
|
49583
|
+
exports.isValidEmail = isValidEmail;
|
|
49584
|
+
exports.isValidNonEmptyString = isValidNonEmptyString;
|
|
49585
|
+
exports.isValidPreset = isValidPreset;
|
|
46996
49586
|
exports.isValidRedemptionRedeemRelation = isValidRedemptionRedeemRelation;
|
|
46997
49587
|
exports.isValidRedemptionRelation = isValidRedemptionRelation;
|
|
46998
49588
|
exports.isValidRelation = isValidRelation;
|
|
46999
49589
|
exports.isValidTokenMetadataRelation = isValidTokenMetadataRelation;
|
|
47000
49590
|
exports.isValidTransactionRelation = isValidTransactionRelation;
|
|
49591
|
+
exports.isValidUrl = isValidUrl;
|
|
47001
49592
|
exports.isValidUserRelation = isValidUserRelation;
|
|
49593
|
+
exports.isValidUuid = isValidUuid;
|
|
49594
|
+
exports.isValidWalletAddress = isValidWalletAddress;
|
|
47002
49595
|
exports.mergeWithDefaults = mergeWithDefaults;
|
|
47003
49596
|
exports.needsExternalSigning = needsExternalSigning;
|
|
47004
49597
|
exports.needsSubmission = needsSubmission;
|
|
47005
49598
|
exports.normalizeFilters = normalizeFilters;
|
|
47006
49599
|
exports.normalizeToPaginated = normalizeToPaginated;
|
|
49600
|
+
exports.operatorText = operatorText;
|
|
47007
49601
|
exports.registerIdentifierType = registerIdentifierType;
|
|
49602
|
+
exports.selectOptionsSourceEntities = selectOptionsSourceEntities;
|
|
47008
49603
|
exports.testnetPrefix = testnetPrefix;
|
|
47009
49604
|
exports.testnetShortPrefix = testnetShortPrefix;
|
|
47010
49605
|
exports.uniqueUserIdentifierKeys = uniqueUserIdentifierKeys;
|
|
@@ -47031,5 +49626,9 @@ exports.useTriggerSources = useTriggerSources;
|
|
|
47031
49626
|
exports.useUserStatus = useUserStatus;
|
|
47032
49627
|
exports.useUsers = useUsers;
|
|
47033
49628
|
exports.useWeb3 = useWeb3;
|
|
49629
|
+
exports.userModelFields = userModelFields;
|
|
49630
|
+
exports.validateAllFields = validateAllFields;
|
|
49631
|
+
exports.validateFieldComparisons = validateFieldComparisons;
|
|
49632
|
+
exports.validateFieldValue = validateFieldValue;
|
|
47034
49633
|
exports.warnIfProblematicEnvironment = warnIfProblematicEnvironment;
|
|
47035
49634
|
//# sourceMappingURL=index.js.map
|