@explorins/pers-sdk-react-native 2.2.2 → 2.3.0-alpha.1

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