@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/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
- * Creates a UserIdentifierObject with all required fields, setting missing values to null
3432
- * @param params Partial parameters for the identifier object
3433
- * @returns A properly formed UserIdentifierObject
3931
+ * Check if a string matches UUID format
3434
3932
  */
3435
- const createUserIdentifierObject = (params) => {
3436
- // Extract the wildcard or use empty string
3437
- const wildcard = params.wildcard || '';
3438
- // Create base object with all identifier fields set to null
3439
- const identifierObject = {
3440
- type: 'identifierObject',
3441
- id: null,
3442
- email: null,
3443
- instagramAccountId: null,
3444
- externalId: null,
3445
- identifierEmail: null,
3446
- wildcard
3447
- };
3448
- // Override nulls with any provided values
3449
- uniqueUserIdentifierKeys.forEach(key => {
3450
- if (params[key]) {
3451
- identifierObject[key] = params[key];
3452
- }
3453
- });
3454
- // If wildcard is provided but no specific fields, auto-detect the field type
3455
- if (wildcard && !params.id && !params.email && !params.instagramAccountId && !params.externalId) {
3456
- const detected = detectIdentifierType(wildcard);
3457
- identifierObject[detected.type] = detected.value;
3458
- }
3459
- return identifierObject;
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.1.39";
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
- const batchSize = Math.min(params.batchSize || 10, 20); // Cap at 20 for safety
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
- // Step 1: Check balance for each token (handle ERC-721 vs ERC-1155 differences)
41566
- const balancePromises = batch.map(tokenId => {
41567
- if (tokenStandard === 'ERC-721') {
41568
- // For ERC-721: Skip balance check, we know ownership from enumeration or explicit tokenIds
41569
- return Promise.resolve({
41570
- tokenId,
41571
- balance: 1,
41572
- hasBalance: true,
41573
- metadata: null
41574
- });
41575
- }
41576
- else {
41577
- // For ERC-1155: Check actual balance
41578
- return this.getTokenBalance({ ...params, tokenId })
41579
- .catch(error => {
41580
- errors.push(`${tokenId}: ${error.message}`);
41581
- return null;
41582
- });
41583
- }
41584
- });
41585
- const balanceResults = await Promise.all(balancePromises);
41586
- const validTokens = balanceResults.filter((token) => token !== null && token.hasBalance);
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
- // TODO: Add PersEventEmitter support for blockchain events
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