@explorins/pers-sdk-react-native 2.2.0 → 2.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -3152,6 +3152,8 @@ exports.WebhookSource = void 0;
3152
3152
  WebhookSource["USER"] = "USER";
3153
3153
  WebhookSource["BUSINESS"] = "BUSINESS";
3154
3154
  WebhookSource["TENANT"] = "TENANT";
3155
+ // System-level internal invocation (e.g., token metadata webhook fetch)
3156
+ WebhookSource["INTERNAL"] = "INTERNAL";
3155
3157
  // External verified sources
3156
3158
  WebhookSource["STRIPE"] = "STRIPE";
3157
3159
  // Allow any external traffic (no verification)
@@ -3216,6 +3218,12 @@ const EntityTypes = {
3216
3218
  SIGNING_ACCOUNT: 'signing_account',
3217
3219
  API_KEY: 'api_key',
3218
3220
  WEBHOOK: 'webhook',
3221
+ // Service domains
3222
+ ANALYTICS: 'analytics',
3223
+ CHAIN: 'chain',
3224
+ FILE: 'file',
3225
+ JWKS: 'jwks',
3226
+ PROJECT: 'project',
3219
3227
  };
3220
3228
  /**
3221
3229
  * Error-specific domains (extends EntityTypes)
@@ -3551,7 +3559,8 @@ function isValidTransactionRelation(value) {
3551
3559
  const VALID_CAMPAIGN_RELATIONS = [
3552
3560
  'triggerSources',
3553
3561
  'businesses',
3554
- 'claimCount'
3562
+ 'claimCount',
3563
+ 'ownerBusiness'
3555
3564
  ];
3556
3565
  /**
3557
3566
  * Type guard to validate campaign include relation at runtime
@@ -3589,7 +3598,8 @@ function isValidCampaignClaimRelation(value) {
3589
3598
  * Used for runtime validation
3590
3599
  */
3591
3600
  const VALID_REDEMPTION_RELATIONS = [
3592
- 'redeemCount'
3601
+ 'redeemCount',
3602
+ 'ownerBusiness'
3593
3603
  ];
3594
3604
  /**
3595
3605
  * Type guard to validate redemption include relation at runtime
@@ -3647,6 +3657,26 @@ function isValidUserRelation(value) {
3647
3657
  return VALID_USER_RELATIONS.includes(value);
3648
3658
  }
3649
3659
 
3660
+ /**
3661
+ * Array of all valid token metadata include relations
3662
+ * Used for runtime validation
3663
+ */
3664
+ const VALID_TOKEN_METADATA_RELATIONS = [
3665
+ 'ownerBusiness',
3666
+ 'mintCount',
3667
+ 'burnCount',
3668
+ 'token'
3669
+ ];
3670
+ /**
3671
+ * Type guard to validate token metadata include relation at runtime
3672
+ *
3673
+ * @param value - String to validate
3674
+ * @returns True if value is a valid TokenMetadataIncludeRelation
3675
+ */
3676
+ function isValidTokenMetadataRelation(value) {
3677
+ return VALID_TOKEN_METADATA_RELATIONS.includes(value);
3678
+ }
3679
+
3650
3680
  /**
3651
3681
  * Normalizes filter options to use new field names while accepting deprecated ones.
3652
3682
  * Use at controller level to support both old and new API contracts.
@@ -3726,6 +3756,9 @@ const CommonErrorCodes = {
3726
3756
  TOKEN_NOT_ACTIVE: 'TOKEN_NOT_ACTIVE',
3727
3757
  TOKEN_ID_REQUIRED: 'TOKEN_ID_REQUIRED',
3728
3758
  TOKEN_TYPE_NOT_FOUND: 'TOKEN_TYPE_NOT_FOUND',
3759
+ TOKEN_UNIT_NOT_FOUND: 'TOKEN_UNIT_NOT_FOUND',
3760
+ NFT_EXPIRED: 'NFT_EXPIRED',
3761
+ NFT_BUSINESS_RESTRICTION: 'NFT_BUSINESS_RESTRICTION',
3729
3762
  // Wallet domain
3730
3763
  WALLET_MISSING_SIGNING: 'WALLET_MISSING_SIGNING',
3731
3764
  WALLET_NOT_FOUND: 'WALLET_NOT_FOUND',
@@ -3765,6 +3798,9 @@ const CommonErrorCodes = {
3765
3798
  CAMPAIGN_DAILY_LIMIT_REACHED: 'CAMPAIGN_DAILY_LIMIT_REACHED',
3766
3799
  CAMPAIGN_GLOBAL_LIMIT_REACHED: 'CAMPAIGN_GLOBAL_LIMIT_REACHED',
3767
3800
  CAMPAIGN_GLOBAL_DAILY_LIMIT_REACHED: 'CAMPAIGN_GLOBAL_DAILY_LIMIT_REACHED',
3801
+ CAMPAIGN_NOT_FOUND: 'CAMPAIGN_NOT_FOUND',
3802
+ GEOGRAPHIC_RESTRICTION: 'GEOGRAPHIC_RESTRICTION',
3803
+ LOCATION_VERIFICATION_REQUIRED: 'LOCATION_VERIFICATION_REQUIRED',
3768
3804
  // Trigger domain
3769
3805
  TRIGGER_NOT_FOUND: 'TRIGGER_NOT_FOUND',
3770
3806
  // Trigger Source domain
@@ -3780,6 +3816,12 @@ const CommonErrorCodes = {
3780
3816
  EXTERNAL_SERVICE_ERROR: 'EXTERNAL_SERVICE_ERROR',
3781
3817
  AUTHENTICATION_REQUIRED: 'AUTHENTICATION_REQUIRED',
3782
3818
  AUTHORIZATION_FAILED: 'AUTHORIZATION_FAILED',
3819
+ CONFIGURATION_MISSING: 'CONFIGURATION_MISSING',
3820
+ // Redemption domain
3821
+ USER_STATUS_RESTRICTED: 'USER_STATUS_RESTRICTED',
3822
+ REDEMPTION_ALREADY_PROCESSING: 'REDEMPTION_ALREADY_PROCESSING',
3823
+ MAX_REDEMPTION: 'MAX_REDEMPTION',
3824
+ USER_LIMIT: 'USER_LIMIT',
3783
3825
  // Authentication errors
3784
3826
  INVALID_TOKEN: 'INVALID_TOKEN',
3785
3827
  TOKEN_EXPIRED: 'TOKEN_EXPIRED',
@@ -3787,6 +3829,8 @@ const CommonErrorCodes = {
3787
3829
  REFRESH_TOKEN_EXPIRED: 'REFRESH_TOKEN_EXPIRED',
3788
3830
  REFRESH_TOKEN_REVOKED: 'REFRESH_TOKEN_REVOKED',
3789
3831
  FIREBASE_AUTH_ERROR: 'FIREBASE_AUTH_ERROR',
3832
+ MULTIPLE_CONTEXT_SELECTION_REQUIRED: 'MULTIPLE_CONTEXT_SELECTION_REQUIRED',
3833
+ ADMIN_TENANT_REQUIRED: 'ADMIN_TENANT_REQUIRED',
3790
3834
  // Generic fallbacks
3791
3835
  INTERNAL_ERROR: 'INTERNAL_ERROR',
3792
3836
  VALIDATION_ERROR: 'VALIDATION_ERROR',
@@ -3883,12 +3927,12 @@ class ErrorUtils {
3883
3927
  return false;
3884
3928
  const err = error;
3885
3929
  // Check for explicit token expired indicators
3886
- if (err?.code === 'TOKEN_EXPIRED' || err?.errorCode === 'TOKEN_EXPIRED') {
3930
+ if (err?.code === CommonErrorCodes.TOKEN_EXPIRED || err?.errorCode === CommonErrorCodes.TOKEN_EXPIRED) {
3887
3931
  return true;
3888
3932
  }
3889
3933
  // Check in response data
3890
3934
  const apiError = err?.response?.data || err;
3891
- if (apiError?.code === 'TOKEN_EXPIRED' || apiError?.errorCode === 'TOKEN_EXPIRED') {
3935
+ if (apiError?.code === CommonErrorCodes.TOKEN_EXPIRED || apiError?.errorCode === CommonErrorCodes.TOKEN_EXPIRED) {
3892
3936
  return true;
3893
3937
  }
3894
3938
  // Any 401 error is treated as token expiration
@@ -3958,7 +4002,9 @@ class ErrorUtils {
3958
4002
  // Try extraction strategies in priority order
3959
4003
  const candidates = [
3960
4004
  this.parseHttpClientError(error), // React Native format
3961
- httpError.response?.data, // Axios/fetch format
4005
+ httpError.response && typeof httpError.response === 'object'
4006
+ ? httpError.response.data
4007
+ : undefined, // Axios/fetch format
3962
4008
  httpError.error, // Angular HttpErrorResponse
3963
4009
  httpError // Direct error object
3964
4010
  ];
@@ -3977,8 +4023,9 @@ class ErrorUtils {
3977
4023
  * Check if object has structured error fields from backend (StructuredError)
3978
4024
  */
3979
4025
  static hasStructuredErrorFields(obj) {
3980
- return obj && typeof obj === 'object' &&
3981
- ((obj.code && obj.category) || (obj.status && (obj.detail || obj.message || obj.title)));
4026
+ return obj !== null && typeof obj === 'object' &&
4027
+ (('code' in obj && 'category' in obj) ||
4028
+ ('status' in obj && ('detail' in obj || 'message' in obj || 'title' in obj)));
3982
4029
  }
3983
4030
  /**
3984
4031
  * Create fallback error structure when no structured error found
@@ -3989,9 +4036,9 @@ class ErrorUtils {
3989
4036
  title: 'Request failed',
3990
4037
  message: this.getMessage(error),
3991
4038
  detail: this.getMessage(error),
3992
- code: 'REQUEST_FAILED',
4039
+ code: CommonErrorCodes.TECHNICAL_ERROR,
3993
4040
  category: exports.ErrorCategory.TECHNICAL,
3994
- domain: 'api',
4041
+ domain: ErrorDomains.EXTERNAL,
3995
4042
  retryable: this.isRetryable(error)
3996
4043
  };
3997
4044
  }
@@ -4070,7 +4117,7 @@ class PersApiError extends Error {
4070
4117
  this.method = method;
4071
4118
  this.name = 'PersApiError';
4072
4119
  // Use backend error details directly
4073
- this.code = backendError?.code || 'API_ERROR';
4120
+ this.code = backendError?.code || CommonErrorCodes.TECHNICAL_ERROR;
4074
4121
  this.domain = backendError?.domain || ErrorDomains.EXTERNAL;
4075
4122
  this.category = backendError?.category || exports.ErrorCategory.TECHNICAL;
4076
4123
  this.retryable = backendError?.retryable ?? (statusCode ? statusCode >= 500 : false);
@@ -4094,7 +4141,7 @@ class AuthenticationError extends PersApiError {
4094
4141
  class NetworkError extends Error {
4095
4142
  constructor(message) {
4096
4143
  super(message);
4097
- this.code = SdkErrorCodes.NETWORK_ERROR;
4144
+ this.code = CommonErrorCodes.EXTERNAL_SERVICE_ERROR;
4098
4145
  this.domain = ErrorDomains.EXTERNAL;
4099
4146
  this.category = exports.ErrorCategory.INFRASTRUCTURE;
4100
4147
  this.retryable = true;
@@ -5142,6 +5189,66 @@ class TokenApi {
5142
5189
  }
5143
5190
  return this.apiClient.get(url);
5144
5191
  }
5192
+ /**
5193
+ * Get all token metadata with filtering, pagination, and include relations
5194
+ *
5195
+ * Useful for displaying rewards (ERC1155) or stamps (ERC721) in tables.
5196
+ * Non-admin users always get active metadata only.
5197
+ *
5198
+ * @param options - Filter, pagination, and include options
5199
+ * @returns Paginated response with token metadata
5200
+ *
5201
+ * @example Get all active rewards with mint/burn counts
5202
+ * ```typescript
5203
+ * const rewards = await tokenApi.getTokenMetadata({
5204
+ * tokenType: 'ERC1155',
5205
+ * active: true,
5206
+ * limit: 10,
5207
+ * include: ['mintCount', 'burnCount', 'token']
5208
+ * });
5209
+ * ```
5210
+ *
5211
+ * @example Get stamps with owner business included
5212
+ * ```typescript
5213
+ * const stamps = await tokenApi.getTokenMetadata({
5214
+ * tokenType: 'ERC721',
5215
+ * search: 'gold',
5216
+ * include: ['ownerBusiness', 'token']
5217
+ * });
5218
+ * ```
5219
+ */
5220
+ async getTokenMetadata(options) {
5221
+ const params = buildPaginationParams(options);
5222
+ if (options?.active !== undefined) {
5223
+ params.set('active', String(options.active));
5224
+ }
5225
+ if (options?.tokenType) {
5226
+ params.set('tokenType', options.tokenType);
5227
+ }
5228
+ if (options?.tokenId) {
5229
+ params.set('tokenId', options.tokenId);
5230
+ }
5231
+ if (options?.search) {
5232
+ params.set('search', options.search);
5233
+ }
5234
+ // Handle tags as string or array
5235
+ if (options?.tags) {
5236
+ const tagsArray = Array.isArray(options.tags) ? options.tags : [options.tags];
5237
+ if (tagsArray.length > 0) {
5238
+ params.set('tags', tagsArray.join(','));
5239
+ }
5240
+ }
5241
+ // Filter by owner business
5242
+ if (options?.ownerBusinessId) {
5243
+ params.set('ownerBusinessId', options.ownerBusinessId);
5244
+ }
5245
+ // Include relations (ownerBusiness, mintCount, burnCount, token)
5246
+ if (options?.include && options.include.length > 0) {
5247
+ params.set('include', options.include.join(','));
5248
+ }
5249
+ const response = await this.apiClient.get(`${this.basePath}/metadata?${params.toString()}`);
5250
+ return normalizeToPaginated(response);
5251
+ }
5145
5252
  // ==========================================
5146
5253
  // ADMIN OPERATIONS
5147
5254
  // ==========================================
@@ -5198,6 +5305,30 @@ class TokenApi {
5198
5305
  async createTokenType(tokenType) {
5199
5306
  return this.apiClient.post(`${this.basePath}/types`, tokenType);
5200
5307
  }
5308
+ /**
5309
+ * ADMIN: Delete token metadata (soft delete)
5310
+ *
5311
+ * Soft deletes token metadata by setting deletedAt timestamp.
5312
+ * Will fail with 409 Conflict if metadata is in use by active campaigns or redemptions.
5313
+ *
5314
+ * @param metadataId - ID of the token metadata to delete
5315
+ * @throws {PersApiError} 409 Conflict if metadata is in use
5316
+ *
5317
+ * @example
5318
+ * ```typescript
5319
+ * try {
5320
+ * await tokenApi.deleteTokenMetadata('metadata-123');
5321
+ * console.log('Metadata deleted');
5322
+ * } catch (error) {
5323
+ * if (error.statusCode === 409) {
5324
+ * console.log('Cannot delete: metadata is in use');
5325
+ * }
5326
+ * }
5327
+ * ```
5328
+ */
5329
+ async deleteTokenMetadata(metadataId) {
5330
+ return this.apiClient.delete(`${this.basePath}/metadata/${metadataId}`);
5331
+ }
5201
5332
  }
5202
5333
 
5203
5334
  /**
@@ -5251,6 +5382,62 @@ class TokenService {
5251
5382
  async getTokenByContractAddress(contractAddress, contractTokenId) {
5252
5383
  return this.tokenApi.getTokenByContractAddress(contractAddress, contractTokenId);
5253
5384
  }
5385
+ /**
5386
+ * Get all token metadata with filtering, pagination, and include relations
5387
+ *
5388
+ * Useful for displaying rewards (ERC1155) or stamps (ERC721) in tables.
5389
+ * Non-admin users always get active metadata only.
5390
+ *
5391
+ * @param options - Filter, pagination, and include options
5392
+ * @returns Paginated response with token metadata
5393
+ *
5394
+ * @example Get rewards with counts
5395
+ * ```typescript
5396
+ * const rewards = await tokenService.getTokenMetadata({
5397
+ * tokenType: 'ERC1155',
5398
+ * include: ['mintCount', 'burnCount', 'token']
5399
+ * });
5400
+ * ```
5401
+ */
5402
+ async getTokenMetadata(options) {
5403
+ return this.tokenApi.getTokenMetadata(options);
5404
+ }
5405
+ /**
5406
+ * Get rewards (ERC1155 token metadata) with filtering, pagination, and include relations
5407
+ *
5408
+ * Convenience method for fetching reward metadata.
5409
+ *
5410
+ * @param options - Filter, pagination, and include options (tokenType is set automatically)
5411
+ * @returns Paginated response with reward metadata
5412
+ *
5413
+ * @example Get rewards with mint/burn counts and token info
5414
+ * ```typescript
5415
+ * const rewards = await tokenService.getRewards({
5416
+ * include: ['mintCount', 'burnCount', 'token', 'ownerBusiness']
5417
+ * });
5418
+ * ```
5419
+ */
5420
+ async getRewards(options) {
5421
+ return this.tokenApi.getTokenMetadata({ ...options, tokenType: NativeTokenTypes.ERC1155 });
5422
+ }
5423
+ /**
5424
+ * Get stamps (ERC721 token metadata) with filtering, pagination, and include relations
5425
+ *
5426
+ * Convenience method for fetching stamp metadata.
5427
+ *
5428
+ * @param options - Filter, pagination, and include options (tokenType is set automatically)
5429
+ * @returns Paginated response with stamp metadata
5430
+ *
5431
+ * @example Get stamps with mint/burn counts and token info
5432
+ * ```typescript
5433
+ * const stamps = await tokenService.getStamps({
5434
+ * include: ['mintCount', 'burnCount', 'token', 'ownerBusiness']
5435
+ * });
5436
+ * ```
5437
+ */
5438
+ async getStamps(options) {
5439
+ return this.tokenApi.getTokenMetadata({ ...options, tokenType: NativeTokenTypes.ERC721 });
5440
+ }
5254
5441
  // ==========================================
5255
5442
  // ADMIN OPERATIONS
5256
5443
  // ==========================================
@@ -5303,6 +5490,17 @@ class TokenService {
5303
5490
  async createTokenType(tokenType) {
5304
5491
  return this.tokenApi.createTokenType(tokenType);
5305
5492
  }
5493
+ /**
5494
+ * ADMIN: Delete token metadata (soft delete)
5495
+ *
5496
+ * Soft deletes token metadata. Will fail if metadata is in use.
5497
+ *
5498
+ * @param metadataId - ID of the token metadata to delete
5499
+ * @throws {PersApiError} 409 Conflict if metadata is in use by campaigns/redemptions
5500
+ */
5501
+ async deleteTokenMetadata(metadataId) {
5502
+ return this.tokenApi.deleteTokenMetadata(metadataId);
5503
+ }
5306
5504
  }
5307
5505
 
5308
5506
  /**
@@ -5377,7 +5575,7 @@ class BusinessApi {
5377
5575
  /**
5378
5576
  * Get all businesses with role-based filtering and pagination
5379
5577
  *
5380
- * Endpoint: GET /businesses?active={boolean}&sanitize={mode}&page={number}&limit={number}
5578
+ * Endpoint: GET /businesses?active={boolean}&sanitize={mode}&page={number}&limit={number}&businessTypeId={number}
5381
5579
  * Auth: @ApiSecurity('projectKey') (enhanced with role-based filtering)
5382
5580
  *
5383
5581
  * Note:
@@ -5400,8 +5598,15 @@ class BusinessApi {
5400
5598
  * });
5401
5599
  *
5402
5600
  * // Filter by tag(s)
5403
- * const tagged = await businessApi.getBusinesses({ tag: 'restaurant' });
5404
- * const multiTag = await businessApi.getBusinesses({ tag: ['restaurant', 'cafe'] });
5601
+ * const tagged = await businessApi.getBusinesses({ tags: 'restaurant' });
5602
+ * const multiTag = await businessApi.getBusinesses({ tags: ['restaurant', 'cafe'] });
5603
+ *
5604
+ * // Filter by business type
5605
+ * const hotels = await businessApi.getBusinesses({ businessTypeId: 1 });
5606
+ *
5607
+ * // Filter by location (geocoding)
5608
+ * const dohaBusinesses = await businessApi.getBusinesses({ city: 'Doha', countryCode: 'QA' });
5609
+ * const westBay = await businessApi.getBusinesses({ neighborhood: 'West Bay' });
5405
5610
  * ```
5406
5611
  */
5407
5612
  async getBusinesses(options) {
@@ -5421,6 +5626,25 @@ class BusinessApi {
5421
5626
  if (options?.sanitize) {
5422
5627
  params.set('sanitize', options.sanitize);
5423
5628
  }
5629
+ if (options?.businessTypeId !== undefined) {
5630
+ params.set('businessTypeId', String(options.businessTypeId));
5631
+ }
5632
+ // Location filter options (geocoding)
5633
+ if (options?.city) {
5634
+ params.set('city', options.city);
5635
+ }
5636
+ if (options?.countryCode) {
5637
+ params.set('countryCode', options.countryCode);
5638
+ }
5639
+ if (options?.country) {
5640
+ params.set('country', options.country);
5641
+ }
5642
+ if (options?.neighborhood) {
5643
+ params.set('neighborhood', options.neighborhood);
5644
+ }
5645
+ if (options?.district) {
5646
+ params.set('district', options.district);
5647
+ }
5424
5648
  const response = await this.apiClient.get(`${this.basePath}?${params.toString()}`);
5425
5649
  return normalizeToPaginated(response);
5426
5650
  }
@@ -5767,20 +5991,16 @@ class BusinessService {
5767
5991
  return this.businessApi.getBusinessByAccount(accountAddress);
5768
5992
  }
5769
5993
  /**
5770
- * Get businesses by type (client-side filtering)
5771
- * @deprecated Consider implementing server-side filtering for better performance
5994
+ * Get businesses by type (server-side filtering)
5995
+ * Uses the businessTypeId parameter for efficient server-side filtering.
5772
5996
  */
5773
5997
  async getBusinessesByType(typeId, options) {
5774
- const response = await this.getActiveBusinesses(options);
5775
- const filtered = response.data.filter((business) => business.businessType && business.businessType.id === parseInt(typeId));
5776
- return {
5777
- data: filtered,
5778
- pagination: {
5779
- ...response.pagination,
5780
- total: filtered.length,
5781
- pages: Math.ceil(filtered.length / (response.pagination.limit || 20))
5782
- }
5783
- };
5998
+ const businessTypeId = parseInt(typeId, 10);
5999
+ return this.businessApi.getBusinesses({
6000
+ ...options,
6001
+ businessTypeId,
6002
+ active: options?.active ?? true // Default to active businesses
6003
+ });
5784
6004
  }
5785
6005
  // ==========================================
5786
6006
  // ADMIN OPERATIONS
@@ -7019,6 +7239,14 @@ class RedemptionService {
7019
7239
  async getRedemptions(options) {
7020
7240
  return this.redemptionApi.getRedemptions(options);
7021
7241
  }
7242
+ /**
7243
+ * Get a specific redemption by ID
7244
+ * @param id - Redemption UUID
7245
+ * @param include - Optional relations to include: 'redeemCount'
7246
+ */
7247
+ async getRedemptionById(id, include) {
7248
+ return this.redemptionApi.getRedemptionById(id, include);
7249
+ }
7022
7250
  /**
7023
7251
  * Convenience: Get active redemptions (public)
7024
7252
  */
@@ -7118,6 +7346,12 @@ class RedemptionService {
7118
7346
  async updateRedemptionType(data) {
7119
7347
  return this.redemptionApi.updateRedemptionType(data);
7120
7348
  }
7349
+ /**
7350
+ * ADMIN: Delete redemption
7351
+ */
7352
+ async deleteRedemption(id) {
7353
+ return this.redemptionApi.deleteRedemption(id);
7354
+ }
7121
7355
  /**
7122
7356
  * ADMIN: Delete redemption type
7123
7357
  */
@@ -9053,7 +9287,7 @@ class TriggerSourceApi {
9053
9287
  /**
9054
9288
  * PUBLIC: Get trigger sources with optional filters and pagination
9055
9289
  *
9056
- * @param options - Filter and pagination options
9290
+ * @param options - Filter and pagination options (includes location filters)
9057
9291
  * @returns Paginated list of trigger sources
9058
9292
  *
9059
9293
  * @example
@@ -9067,6 +9301,12 @@ class TriggerSourceApi {
9067
9301
  * page: 1,
9068
9302
  * limit: 20
9069
9303
  * });
9304
+ *
9305
+ * // Filter by location (geocoding)
9306
+ * const dubaiSources = await triggerSourceApi.getTriggerSources({
9307
+ * city: 'Dubai',
9308
+ * countryCode: 'AE'
9309
+ * });
9070
9310
  * ```
9071
9311
  */
9072
9312
  async getTriggerSources(options) {
@@ -9079,6 +9319,19 @@ class TriggerSourceApi {
9079
9319
  params.set('campaignId', options.campaignId);
9080
9320
  if (options?.active !== undefined)
9081
9321
  params.set('active', String(options.active));
9322
+ if (options?.includeDeleted)
9323
+ params.set('includeDeleted', String(options.includeDeleted));
9324
+ // Location filter options (geocoding)
9325
+ if (options?.city)
9326
+ params.set('city', options.city);
9327
+ if (options?.countryCode)
9328
+ params.set('countryCode', options.countryCode);
9329
+ if (options?.country)
9330
+ params.set('country', options.country);
9331
+ if (options?.neighborhood)
9332
+ params.set('neighborhood', options.neighborhood);
9333
+ if (options?.district)
9334
+ params.set('district', options.district);
9082
9335
  const response = await this.apiClient.get(`/trigger-sources?${params.toString()}`);
9083
9336
  return normalizeToPaginated(response);
9084
9337
  }
@@ -9575,13 +9828,17 @@ class AuthService {
9575
9828
  * Returns true for:
9576
9829
  * - Known fatal error codes (REFRESH_TOKEN_EXPIRED, TOKEN_REVOKED, etc.)
9577
9830
  * - Fatal error codes in error messages (wrapped errors)
9578
- * - 401/403 HTTP status codes
9579
9831
  *
9580
- * Returns false for (retryable errors):
9832
+ * Returns false for (retryable/recoverable):
9581
9833
  * - Network errors
9582
9834
  * - Timeouts
9583
9835
  * - 5xx server errors
9584
9836
  * - TOKEN_EXPIRED (normal refresh trigger)
9837
+ * - Raw 401/403 without fatal code (should try refresh first)
9838
+ *
9839
+ * NOTE: We intentionally DO NOT treat raw 401/403 status as fatal.
9840
+ * The HTTP client handles 401 by attempting token refresh first.
9841
+ * Only if refresh fails WITH a fatal code do we logout.
9585
9842
  */
9586
9843
  isDefinitiveAuthFailure(error) {
9587
9844
  if (!error)
@@ -9596,11 +9853,12 @@ class AuthService {
9596
9853
  if (isFatalAuthErrorInMessage(errorMessage)) {
9597
9854
  return true;
9598
9855
  }
9599
- // Check HTTP status if available
9600
- const status = errorAny?.status || errorAny?.response?.status;
9601
- if (status === 401 || status === 403) {
9602
- return true;
9603
- }
9856
+ // NOTE: We intentionally DO NOT check HTTP status codes here.
9857
+ // A raw 401/403 without a fatal error code should trigger a refresh attempt,
9858
+ // not an immediate logout. The HTTP client (pers-api-client.ts) handles this:
9859
+ // 1. 401 received → try refresh token
9860
+ // 2. Refresh succeeds → retry original request
9861
+ // 3. Refresh fails with fatal CODE → then logout
9604
9862
  // Everything else is considered retryable
9605
9863
  return false;
9606
9864
  }
@@ -9658,6 +9916,9 @@ class AuthService {
9658
9916
  * Extracts context (tenantId/businessId) from the current refresh token to ensure
9659
9917
  * the recovered session maintains the same context.
9660
9918
  *
9919
+ * This is a PUBLIC method that can be called by TokenRefreshManager when internal
9920
+ * refresh fails, to try external (provider token) recovery before giving up.
9921
+ *
9661
9922
  * @returns true if session was successfully recovered
9662
9923
  */
9663
9924
  async attemptProviderTokenRecovery() {
@@ -9684,12 +9945,17 @@ class AuthService {
9684
9945
  const contextClaims = refreshToken ? decodeJwtPayload(refreshToken) : null;
9685
9946
  // Re-authenticate using provider token based on auth type
9686
9947
  // Pass the context from the previous session to avoid MULTIPLE_CONTEXT_SELECTION_REQUIRED
9948
+ // Note: In AuthJWTPayload, when accountType=BUSINESS, sub contains the businessId
9687
9949
  switch (authType) {
9688
9950
  case exports.AccountOwnerType.USER:
9689
9951
  await this.loginUser(providerToken);
9690
9952
  break;
9691
9953
  case exports.AccountOwnerType.BUSINESS:
9692
- await this.loginBusiness(providerToken, contextClaims?.businessId ? { businessId: contextClaims.businessId } : undefined);
9954
+ // For BUSINESS auth, sub contains the businessId when accountType is BUSINESS
9955
+ const businessId = contextClaims?.accountType === exports.AccountOwnerType.BUSINESS
9956
+ ? contextClaims.sub
9957
+ : undefined;
9958
+ await this.loginBusiness(providerToken, businessId ? { businessId } : undefined);
9693
9959
  break;
9694
9960
  case exports.AccountOwnerType.TENANT:
9695
9961
  await this.loginTenantAdmin(providerToken, contextClaims?.tenantId ? { tenantId: contextClaims.tenantId } : undefined);
@@ -9805,6 +10071,8 @@ class TokenRefreshManager {
9805
10071
  // Retry configuration for transient failures
9806
10072
  this.MAX_RETRY_ATTEMPTS = 3;
9807
10073
  this.RETRY_DELAY_MS = 1000;
10074
+ // Active refresh promise for deduplication
10075
+ this.activeRefreshPromise = null;
9808
10076
  }
9809
10077
  /**
9810
10078
  * Ensures the access token is valid, refreshing if needed.
@@ -9905,11 +10173,27 @@ class TokenRefreshManager {
9905
10173
  return new Promise(resolve => setTimeout(resolve, ms));
9906
10174
  }
9907
10175
  /**
9908
- * @deprecated Use ensureValidToken() instead
10176
+ * Attempt token refresh: internal first, then provider token fallback.
10177
+ * DEDUPLICATES concurrent calls - all callers share the same promise.
9909
10178
  */
9910
10179
  async attemptInternalRefresh() {
9911
- const result = await this.attemptRefreshWithRetry();
9912
- return result.success;
10180
+ if (this.activeRefreshPromise) {
10181
+ return this.activeRefreshPromise;
10182
+ }
10183
+ this.activeRefreshPromise = (async () => {
10184
+ const result = await this.attemptRefreshWithRetry();
10185
+ if (result.success)
10186
+ return result;
10187
+ // Internal failed - try provider token recovery
10188
+ const recovered = await this.authService.attemptProviderTokenRecovery();
10189
+ return recovered ? { success: true, retryable: false } : result;
10190
+ })();
10191
+ try {
10192
+ return await this.activeRefreshPromise;
10193
+ }
10194
+ finally {
10195
+ this.activeRefreshPromise = null;
10196
+ }
9913
10197
  }
9914
10198
  }
9915
10199
 
@@ -10457,17 +10741,16 @@ class DPoPManager {
10457
10741
  * Generate a short fingerprint of the public key for debugging
10458
10742
  * This helps identify if the same key is being used across operations
10459
10743
  */
10460
- getPublicKeyFingerprint(publicKey) {
10461
- try {
10462
- // Use the 'x' coordinate of the EC key as a fingerprint
10463
- const x = publicKey.x || '';
10464
- const y = publicKey.y || '';
10465
- return `${x.substring(0, 8)}...${y.substring(0, 4)}`;
10466
- }
10467
- catch {
10468
- return 'unknown';
10469
- }
10470
- }
10744
+ /* private getPublicKeyFingerprint(publicKey: JsonWebKey): string {
10745
+ try {
10746
+ // Use the 'x' coordinate of the EC key as a fingerprint
10747
+ const x = publicKey.x || '';
10748
+ const y = publicKey.y || '';
10749
+ return `${x.substring(0, 8)}...${y.substring(0, 4)}`;
10750
+ } catch {
10751
+ return 'unknown';
10752
+ }
10753
+ } */
10471
10754
  /**
10472
10755
  * Clears DPoP keys (e.g. on logout)
10473
10756
  */
@@ -10716,7 +10999,7 @@ class DefaultAuthProvider {
10716
10999
  /** SDK package name */
10717
11000
  const SDK_NAME = "@explorins/pers-sdk";
10718
11001
  /** SDK version - injected from package.json at build time */
10719
- const SDK_VERSION = "2.1.33";
11002
+ const SDK_VERSION = "2.1.39";
10720
11003
  /** Full SDK identifier for headers */
10721
11004
  const SDK_USER_AGENT = `${SDK_NAME}/${SDK_VERSION}`;
10722
11005
 
@@ -10826,12 +11109,16 @@ class PersApiClient {
10826
11109
  }
10827
11110
  const { retryCount = 0, responseType = 'json', bypassAuth = false } = options || {};
10828
11111
  const url = `${this.apiRoot}${endpoint}`;
10829
- // SMART TOKEN VALIDATION: Only check if we suspect the token might be expired
11112
+ // TOKEN VALIDATION: Ensure token is valid before request
11113
+ // This blocks the request until token validation completes - prevents 401s from expired tokens
10830
11114
  if (!bypassAuth && this.mergedConfig.authProvider && retryCount === 0) {
10831
- // Fire-and-forget validation - don't block the request unless we know there's an issue
10832
- this.ensureValidToken().catch(error => {
10833
- console.debug('[PersApiClient] Background token validation failed:', error);
10834
- });
11115
+ try {
11116
+ await this.ensureValidToken();
11117
+ }
11118
+ catch (error) {
11119
+ console.debug('[PersApiClient] Token validation failed:', error);
11120
+ // Continue with request - it will fail with 401 and trigger retry logic
11121
+ }
10835
11122
  }
10836
11123
  const requestOptions = {
10837
11124
  headers: await this.getHeaders(!bypassAuth, method, url),
@@ -10859,49 +11146,63 @@ class PersApiClient {
10859
11146
  return result;
10860
11147
  }
10861
11148
  catch (error) {
10862
- // Error handling - proactive token refresh should prevent most 401s
10863
11149
  const status = ErrorUtils.getStatus(error);
10864
11150
  const errorMessage = ErrorUtils.getMessage(error);
10865
- // Handle 401 errors centrally through AuthService
11151
+ // Handle 401 Unauthorized errors
10866
11152
  if (status === 401) {
10867
11153
  const backendError = ErrorUtils.extractBackendErrorDetails(error);
10868
- // Check for FATAL auth error CODES (not just 401 status)
10869
- // Fatal codes: REFRESH_TOKEN_EXPIRED, TOKEN_REVOKED, etc. → immediate logout
10870
- // Non-fatal codes: TOKEN_EXPIRED → try refresh first
11154
+ // ===========================================
11155
+ // FATAL AUTH ERRORS → Immediate logout
11156
+ // ===========================================
11157
+ // Fatal codes: REFRESH_TOKEN_EXPIRED, TOKEN_REVOKED, INVALID_REFRESH_TOKEN, etc.
10871
11158
  if (this.authService.isFatalAuthError(backendError.code)) {
10872
- // Definitive auth failure - no retry, session is invalid
11159
+ console.warn(`[PersApiClient] Fatal auth error: ${backendError.code}`);
10873
11160
  await this.authService.handleAuthFailure();
10874
11161
  this.emitErrorEvent(backendError, errorMessage, endpoint, method, status, error);
10875
11162
  throw new AuthenticationError(errorMessage, endpoint, method, backendError);
10876
11163
  }
10877
- // If this IS a refresh request that failed with 401, don't retry to avoid infinite loop
11164
+ // ===========================================
11165
+ // REFRESH REQUEST FAILED → Logout (prevents infinite loop)
11166
+ // ===========================================
10878
11167
  if (options?.isRefreshRequest) {
10879
- // Refresh failed - this is a real auth failure
11168
+ console.warn('[PersApiClient] Refresh request itself failed with 401');
10880
11169
  await this.authService.handleAuthFailure();
10881
11170
  this.emitErrorEvent(backendError, errorMessage, endpoint, method, status, error);
10882
11171
  throw new AuthenticationError(errorMessage, endpoint, method, backendError);
10883
11172
  }
10884
- // TOKEN_EXPIRED or unknown 401: try refresh once, then retry
11173
+ // ===========================================
11174
+ // RETRYABLE 401 (TOKEN_EXPIRED) → Try refresh, then retry
11175
+ // ===========================================
10885
11176
  if (retryCount === 0 && this.mergedConfig.authProvider) {
10886
- try {
10887
- const refreshSuccessful = await this.refreshManager.attemptInternalRefresh();
10888
- if (refreshSuccessful) {
10889
- // Emit TOKEN_REFRESHED event so WS clients can retry if they gave up
10890
- this._events?.emitSuccess({
10891
- type: 'token_refreshed',
10892
- domain: 'authentication',
10893
- userMessage: 'Authentication token refreshed',
10894
- });
10895
- // Retry with new token
10896
- return await this.request(method, endpoint, body, { ...options, retryCount: 1 });
10897
- }
11177
+ const refreshResult = await this.refreshManager.attemptInternalRefresh();
11178
+ if (refreshResult.success) {
11179
+ // Refresh succeeded → retry request with new token
11180
+ this._events?.emitSuccess({
11181
+ type: 'token_refreshed',
11182
+ domain: 'authentication',
11183
+ userMessage: 'Authentication token refreshed',
11184
+ });
11185
+ return await this.request(method, endpoint, body, { ...options, retryCount: 1 });
10898
11186
  }
10899
- catch (refreshError) {
10900
- // Refresh failed - fall through to handleAuthFailure
11187
+ // Refresh failed - check if it's a definitive failure
11188
+ if (!refreshResult.retryable) {
11189
+ // Non-retryable failure (e.g., refresh token expired) → logout
11190
+ console.warn('[PersApiClient] Refresh failed with non-retryable error, logging out');
11191
+ await this.authService.handleAuthFailure();
10901
11192
  }
11193
+ else {
11194
+ // Retryable failure (network error, etc.) → DON'T logout, let app retry later
11195
+ console.warn('[PersApiClient] Refresh failed with retryable error, NOT logging out');
11196
+ }
11197
+ this.emitErrorEvent(backendError, errorMessage, endpoint, method, status, error);
11198
+ throw new AuthenticationError(errorMessage, endpoint, method, backendError);
10902
11199
  }
10903
- // All recovery attempts failed
10904
- await this.authService.handleAuthFailure();
11200
+ // ===========================================
11201
+ // RETRY FAILED (retryCount > 0) → Something wrong, but DON'T logout
11202
+ // ===========================================
11203
+ // We already refreshed successfully, but retry still got 401
11204
+ // This is unexpected - don't logout, let app investigate
11205
+ console.warn('[PersApiClient] Retry with new token still got 401 - not logging out');
10905
11206
  this.emitErrorEvent(backendError, errorMessage, endpoint, method, status, error);
10906
11207
  throw new AuthenticationError(errorMessage, endpoint, method, backendError);
10907
11208
  }
@@ -10927,9 +11228,9 @@ class PersApiClient {
10927
11228
  }); */
10928
11229
  this._events?.emitError({
10929
11230
  domain: errorDetails.domain || 'external',
10930
- type: errorDetails.code || 'API_ERROR',
10931
- userMessage: errorDetails.message || errorMessage, // Event system uses userMessage for display
10932
- code: errorDetails.code,
11231
+ type: 'api_error', // Event type discriminator (fixed value)
11232
+ userMessage: errorDetails.message || errorMessage,
11233
+ code: errorDetails.code, // Actual error code from backend
10933
11234
  details: {
10934
11235
  endpoint,
10935
11236
  method,
@@ -12565,6 +12866,107 @@ class TokenManager {
12565
12866
  async getTokenByContract(contractAddress, contractTokenId = null) {
12566
12867
  return this.tokenService.getTokenByContractAddress(contractAddress, contractTokenId);
12567
12868
  }
12869
+ /**
12870
+ * Get all token metadata with filtering, pagination, and include relations
12871
+ *
12872
+ * Retrieves token metadata (rewards/stamps) with server-side filtering, pagination,
12873
+ * and optional include relations for additional data.
12874
+ * Useful for displaying rewards (ERC1155) or stamps (ERC721) in admin tables.
12875
+ * Non-admin users always get active metadata only.
12876
+ *
12877
+ * @param options - Filter, pagination, and include options
12878
+ * @returns Promise resolving to paginated token metadata
12879
+ *
12880
+ * @example Get paginated rewards with counts
12881
+ * ```typescript
12882
+ * const rewards = await sdk.tokens.getTokenMetadata({
12883
+ * tokenType: 'ERC1155',
12884
+ * active: true,
12885
+ * page: 1,
12886
+ * limit: 10,
12887
+ * sortBy: 'name',
12888
+ * sortOrder: 'ASC',
12889
+ * include: ['mintCount', 'burnCount', 'token']
12890
+ * });
12891
+ *
12892
+ * rewards.data.forEach(r => {
12893
+ * console.log(`${r.name}: ${r.mintCount} minted, ${r.burnCount} burned`);
12894
+ * console.log(`Contract: ${r.included?.token?.contractAddress}`);
12895
+ * });
12896
+ * ```
12897
+ *
12898
+ * @example Get stamps with owner business
12899
+ * ```typescript
12900
+ * const stamps = await sdk.tokens.getTokenMetadata({
12901
+ * tokenType: 'ERC721',
12902
+ * search: 'gold',
12903
+ * include: ['ownerBusiness', 'token']
12904
+ * });
12905
+ *
12906
+ * stamps.data.forEach(s => {
12907
+ * console.log(`${s.name} owned by ${s.included?.ownerBusiness?.displayName}`);
12908
+ * });
12909
+ * ```
12910
+ */
12911
+ async getTokenMetadata(options) {
12912
+ return this.tokenService.getTokenMetadata(options);
12913
+ }
12914
+ /**
12915
+ * Get rewards (ERC1155 token metadata) with filtering, pagination, and include relations
12916
+ *
12917
+ * Convenience method for fetching reward metadata. Rewards are ERC1155 tokens
12918
+ * that represent collectible items, vouchers, or other redeemable rewards.
12919
+ *
12920
+ * @param options - Filter, pagination, and include options (tokenType is set automatically)
12921
+ * @returns Promise resolving to paginated reward metadata
12922
+ *
12923
+ * @example Get active rewards with mint counts
12924
+ * ```typescript
12925
+ * const rewards = await sdk.tokens.getRewards({
12926
+ * active: true,
12927
+ * limit: 10,
12928
+ * include: ['mintCount', 'burnCount', 'token', 'ownerBusiness']
12929
+ * });
12930
+ *
12931
+ * rewards.data.forEach(r => {
12932
+ * console.log(`${r.name}: ${r.mintCount ?? 0} minted`);
12933
+ * if (r.included?.token) {
12934
+ * console.log(` Chain: ${r.included.token.chainId}, Contract: ${r.included.token.contractAddress}`);
12935
+ * }
12936
+ * });
12937
+ * ```
12938
+ */
12939
+ async getRewards(options) {
12940
+ return this.tokenService.getRewards(options);
12941
+ }
12942
+ /**
12943
+ * Get stamps (ERC721 token metadata) with filtering, pagination, and include relations
12944
+ *
12945
+ * Convenience method for fetching stamp metadata. Stamps are ERC721 tokens
12946
+ * that represent achievements, badges, or collectible status markers.
12947
+ *
12948
+ * @param options - Filter, pagination, and include options (tokenType is set automatically)
12949
+ * @returns Promise resolving to paginated stamp metadata
12950
+ *
12951
+ * @example Get active stamps with all includes
12952
+ * ```typescript
12953
+ * const stamps = await sdk.tokens.getStamps({
12954
+ * active: true,
12955
+ * limit: 10,
12956
+ * include: ['mintCount', 'burnCount', 'token', 'ownerBusiness']
12957
+ * });
12958
+ *
12959
+ * stamps.data.forEach(s => {
12960
+ * console.log(`${s.name}: ${s.mintCount ?? 0} minted`);
12961
+ * if (s.included?.ownerBusiness) {
12962
+ * console.log(` Owner: ${s.included.ownerBusiness.displayName}`);
12963
+ * }
12964
+ * });
12965
+ * ```
12966
+ */
12967
+ async getStamps(options) {
12968
+ return this.tokenService.getStamps(options);
12969
+ }
12568
12970
  /**
12569
12971
  * Admin: Create new token
12570
12972
  *
@@ -12656,6 +13058,32 @@ class TokenManager {
12656
13058
  async toggleTokenActive(tokenId) {
12657
13059
  return this.tokenService.toggleTokenActive(tokenId);
12658
13060
  }
13061
+ /**
13062
+ * Admin: Delete token metadata (soft delete)
13063
+ *
13064
+ * Soft deletes token metadata by setting deletedAt timestamp.
13065
+ * Requires administrator privileges. Will fail with 409 Conflict
13066
+ * if metadata is currently in use by active campaigns or redemptions.
13067
+ *
13068
+ * @param metadataId - ID of the token metadata to delete
13069
+ * @throws {PersApiError} When not authenticated as admin
13070
+ * @throws {PersApiError} 409 Conflict if metadata is in use
13071
+ *
13072
+ * @example
13073
+ * ```typescript
13074
+ * try {
13075
+ * await sdk.tokens.deleteTokenMetadata('metadata-123');
13076
+ * console.log('Token metadata deleted');
13077
+ * } catch (error) {
13078
+ * if (error.statusCode === 409) {
13079
+ * console.log('Cannot delete: metadata is used by campaigns/redemptions');
13080
+ * }
13081
+ * }
13082
+ * ```
13083
+ */
13084
+ async deleteTokenMetadata(metadataId) {
13085
+ return this.tokenService.deleteTokenMetadata(metadataId);
13086
+ }
12659
13087
  /**
12660
13088
  * Get the full token SDK for advanced operations
12661
13089
  *
@@ -14469,6 +14897,52 @@ class RedemptionManager {
14469
14897
  async getRedemptions(options) {
14470
14898
  return this.redemptionService.getRedemptions(options);
14471
14899
  }
14900
+ /**
14901
+ * Get a specific redemption by ID
14902
+ *
14903
+ * Retrieves detailed information about a specific redemption offer.
14904
+ * Useful when you already have the redemption ID and need full details.
14905
+ *
14906
+ * @param id - Redemption UUID
14907
+ * @param include - Optional relations to include: 'redeemCount'
14908
+ * @returns Promise resolving to the redemption
14909
+ *
14910
+ * @example Basic usage
14911
+ * ```typescript
14912
+ * const redemption = await sdk.redemptions.getRedemptionById('7baf4d39-0bd6-45ca-9c66-8c0d655767c3');
14913
+ * console.log(redemption.name);
14914
+ * console.log(redemption.description);
14915
+ * ```
14916
+ *
14917
+ * @example With redeem count
14918
+ * ```typescript
14919
+ * const redemption = await sdk.redemptions.getRedemptionById(
14920
+ * 'redemption-123',
14921
+ * ['redeemCount']
14922
+ * );
14923
+ * console.log(`${redemption.name} has been redeemed ${redemption.redeemCount} times`);
14924
+ * ```
14925
+ *
14926
+ * @example Get redemption details for display
14927
+ * ```typescript
14928
+ * const redemption = await sdk.redemptions.getRedemptionById(id);
14929
+ *
14930
+ * console.log('Redemption Details:');
14931
+ * console.log(`Name: ${redemption.name}`);
14932
+ * console.log(`Description: ${redemption.description}`);
14933
+ * console.log(`Active: ${redemption.isActive}`);
14934
+ *
14935
+ * if (redemption.tokenUnits?.length) {
14936
+ * console.log('Cost:');
14937
+ * redemption.tokenUnits.forEach(unit => {
14938
+ * console.log(` ${unit.amount} ${unit.token.symbol}`);
14939
+ * });
14940
+ * }
14941
+ * ```
14942
+ */
14943
+ async getRedemptionById(id, include) {
14944
+ return this.redemptionService.getRedemptionById(id, include);
14945
+ }
14472
14946
  /**
14473
14947
  * Get available redemption types
14474
14948
  *
@@ -15003,6 +15477,21 @@ class RedemptionManager {
15003
15477
  async deleteRedemptionType(id) {
15004
15478
  return this.redemptionService.deleteRedemptionType(id);
15005
15479
  }
15480
+ /**
15481
+ * Delete a redemption (soft delete)
15482
+ *
15483
+ * @param id - Redemption ID to delete
15484
+ * @returns Promise resolving to true if deleted successfully
15485
+ *
15486
+ * @example
15487
+ * ```typescript
15488
+ * const success = await sdk.redemptions.deleteRedemption('redemption-123');
15489
+ * console.log('Deleted:', success);
15490
+ * ```
15491
+ */
15492
+ async deleteRedemption(id) {
15493
+ return this.redemptionService.deleteRedemption(id);
15494
+ }
15006
15495
  /**
15007
15496
  * Get the full redemption service for advanced operations
15008
15497
  *
@@ -21157,6 +21646,99 @@ const useTokens = () => {
21157
21646
  throw error;
21158
21647
  }
21159
21648
  }, [sdk, isInitialized]);
21649
+ /**
21650
+ * Get all token metadata with filtering and pagination
21651
+ *
21652
+ * Retrieves token metadata (rewards/stamps) with server-side filtering.
21653
+ * Use tokenType: 'ERC1155' for rewards, 'ERC721' for stamps.
21654
+ *
21655
+ * @param options - Filter and pagination options
21656
+ * @returns Promise resolving to paginated token metadata
21657
+ * @throws Error if SDK is not initialized
21658
+ *
21659
+ * @example
21660
+ * ```typescript
21661
+ * const { getTokenMetadata } = useTokens();
21662
+ * const rewards = await getTokenMetadata({
21663
+ * tokenType: 'ERC1155',
21664
+ * active: true,
21665
+ * limit: 10
21666
+ * });
21667
+ * ```
21668
+ */
21669
+ const getTokenMetadata = react.useCallback(async (options) => {
21670
+ if (!isInitialized || !sdk) {
21671
+ throw new Error('SDK not initialized. Call initialize() first.');
21672
+ }
21673
+ try {
21674
+ const result = await sdk.tokens.getTokenMetadata(options);
21675
+ return result;
21676
+ }
21677
+ catch (error) {
21678
+ console.error('Failed to fetch token metadata:', error);
21679
+ throw error;
21680
+ }
21681
+ }, [sdk, isInitialized]);
21682
+ /**
21683
+ * Get rewards (ERC1155 token metadata) with filtering and pagination
21684
+ *
21685
+ * Convenience method for fetching reward metadata. Rewards are ERC1155 tokens
21686
+ * that represent collectible items, vouchers, or other redeemable rewards.
21687
+ *
21688
+ * @param options - Filter and pagination options
21689
+ * @returns Promise resolving to paginated reward metadata
21690
+ * @throws Error if SDK is not initialized
21691
+ *
21692
+ * @example
21693
+ * ```typescript
21694
+ * const { getRewards } = useTokens();
21695
+ * const rewards = await getRewards({ active: true, limit: 10 });
21696
+ * console.log(`Found ${rewards.pagination.total} rewards`);
21697
+ * ```
21698
+ */
21699
+ const getRewards = react.useCallback(async (options) => {
21700
+ if (!isInitialized || !sdk) {
21701
+ throw new Error('SDK not initialized. Call initialize() first.');
21702
+ }
21703
+ try {
21704
+ const result = await sdk.tokens.getRewards(options);
21705
+ return result;
21706
+ }
21707
+ catch (error) {
21708
+ console.error('Failed to fetch rewards:', error);
21709
+ throw error;
21710
+ }
21711
+ }, [sdk, isInitialized]);
21712
+ /**
21713
+ * Get stamps (ERC721 token metadata) with filtering and pagination
21714
+ *
21715
+ * Convenience method for fetching stamp metadata. Stamps are ERC721 tokens
21716
+ * that represent achievements, badges, or collectible status markers.
21717
+ *
21718
+ * @param options - Filter and pagination options
21719
+ * @returns Promise resolving to paginated stamp metadata
21720
+ * @throws Error if SDK is not initialized
21721
+ *
21722
+ * @example
21723
+ * ```typescript
21724
+ * const { getStamps } = useTokens();
21725
+ * const stamps = await getStamps({ active: true, limit: 10 });
21726
+ * console.log(`Found ${stamps.pagination.total} stamps`);
21727
+ * ```
21728
+ */
21729
+ const getStamps = react.useCallback(async (options) => {
21730
+ if (!isInitialized || !sdk) {
21731
+ throw new Error('SDK not initialized. Call initialize() first.');
21732
+ }
21733
+ try {
21734
+ const result = await sdk.tokens.getStamps(options);
21735
+ return result;
21736
+ }
21737
+ catch (error) {
21738
+ console.error('Failed to fetch stamps:', error);
21739
+ throw error;
21740
+ }
21741
+ }, [sdk, isInitialized]);
21160
21742
  return {
21161
21743
  getTokens,
21162
21744
  getActiveCreditToken,
@@ -21164,6 +21746,9 @@ const useTokens = () => {
21164
21746
  getTokenTypes,
21165
21747
  getStatusTokens,
21166
21748
  getTokenByContract,
21749
+ getTokenMetadata,
21750
+ getRewards,
21751
+ getStamps,
21167
21752
  isAvailable: isInitialized && !!sdk?.tokens,
21168
21753
  };
21169
21754
  };
@@ -44400,6 +44985,25 @@ const useRedemptions = () => {
44400
44985
  /**
44401
44986
  * Redeem a redemption offer with optional dynamic context
44402
44987
  *
44988
+ * ## When to use `redeem()` vs `createTransaction()`
44989
+ *
44990
+ * | Use Case | Method | Auto-Signs |
44991
+ * |----------|--------|------------|
44992
+ * | **Voucher redemption** | `redeem()` | ✅ Yes |
44993
+ * | **Reward redemption** | `redeem()` | ✅ Yes |
44994
+ * | **Direct token transfer** | `createTransaction()` | ⚠️ Depends |
44995
+ * | **POS burn with QR** | `buildPOSBurnRequest()` | ❌ Manual |
44996
+ *
44997
+ * **Use `redeem()` for voucher/reward redemptions.** It automatically:
44998
+ * 1. Creates redemption record
44999
+ * 2. Triggers passkey/biometric prompt
45000
+ * 3. Signs the transaction
45001
+ * 4. Submits to blockchain
45002
+ * 5. Returns result with `transactionHash`
45003
+ *
45004
+ * **No admin approval required** - The "PENDING" state is brief (seconds).
45005
+ * The user's passkey signature IS the approval.
45006
+ *
44403
45007
  * Supports dynamic context for personalized NFT generation when the redemption
44404
45008
  * issues ERC721 tokens. Context can include validity dates, AI prompt placeholders,
44405
45009
  * and custom metadata.
@@ -44408,11 +45012,21 @@ const useRedemptions = () => {
44408
45012
  * @param options - Optional settings
44409
45013
  * @param options.context - Dynamic context for NFT personalization (validity dates, AI prompts)
44410
45014
  * @param options.onStatusUpdate - Callback for transaction signing status updates
44411
- * @returns Promise resolving to redemption result
45015
+ * @returns Promise resolving to redemption result with `transactionHash` on success
44412
45016
  *
44413
- * @example Basic redemption
45017
+ * @example Basic redemption (most common)
44414
45018
  * ```typescript
44415
- * const result = await redeem('redemption-123');
45019
+ * const { redeem } = useRedemptions();
45020
+ *
45021
+ * const result = await redeem('redemption-123', {
45022
+ * onStatusUpdate: (status, message) => {
45023
+ * console.log(`Status: ${status} - ${message}`);
45024
+ * }
45025
+ * });
45026
+ *
45027
+ * if (result.transactionHash) {
45028
+ * console.log('Redemption complete!', result.transactionHash);
45029
+ * }
44416
45030
  * ```
44417
45031
  *
44418
45032
  * @example Redemption with dynamic context (personalized NFT)
@@ -44443,6 +45057,8 @@ const useRedemptions = () => {
44443
45057
  * }
44444
45058
  * });
44445
45059
  * ```
45060
+ *
45061
+ * @see {@link useTransactions.createTransaction | createTransaction()} for direct transfers without redemption flow
44446
45062
  */
44447
45063
  const redeem = react.useCallback(async (redemptionId, options) => {
44448
45064
  if (!isInitialized || !sdk) {
@@ -44592,8 +45208,43 @@ const useRedemptions = () => {
44592
45208
  throw error;
44593
45209
  }
44594
45210
  }, [sdk, isInitialized]);
45211
+ /**
45212
+ * Get a specific redemption by ID
45213
+ *
45214
+ * Retrieves detailed information about a specific redemption offer.
45215
+ *
45216
+ * @param id - Redemption UUID
45217
+ * @param include - Optional relations to include: 'redeemCount'
45218
+ * @returns Promise resolving to the redemption
45219
+ *
45220
+ * @example Basic usage
45221
+ * ```typescript
45222
+ * const redemption = await getRedemptionById('7baf4d39-0bd6-45ca-9c66-8c0d655767c3');
45223
+ * console.log(redemption.name);
45224
+ * ```
45225
+ *
45226
+ * @example With redeem count
45227
+ * ```typescript
45228
+ * const redemption = await getRedemptionById('redemption-123', ['redeemCount']);
45229
+ * console.log(`${redemption.name} has been redeemed ${redemption.redeemCount} times`);
45230
+ * ```
45231
+ */
45232
+ const getRedemptionById = react.useCallback(async (id, include) => {
45233
+ if (!isInitialized || !sdk) {
45234
+ throw new Error('SDK not initialized. Call initialize() first.');
45235
+ }
45236
+ try {
45237
+ const result = await sdk.redemptions.getRedemptionById(id, include);
45238
+ return result;
45239
+ }
45240
+ catch (error) {
45241
+ console.error('Failed to fetch redemption by ID:', error);
45242
+ throw error;
45243
+ }
45244
+ }, [sdk, isInitialized]);
44595
45245
  return {
44596
45246
  getRedemptions,
45247
+ getRedemptionById,
44597
45248
  getUserRedemptions,
44598
45249
  redeem,
44599
45250
  getRedemptionTypes,
@@ -46293,6 +46944,7 @@ exports.VALID_CAMPAIGN_RELATIONS = VALID_CAMPAIGN_RELATIONS;
46293
46944
  exports.VALID_REDEMPTION_REDEEM_RELATIONS = VALID_REDEMPTION_REDEEM_RELATIONS;
46294
46945
  exports.VALID_REDEMPTION_RELATIONS = VALID_REDEMPTION_RELATIONS;
46295
46946
  exports.VALID_RELATIONS = VALID_RELATIONS;
46947
+ exports.VALID_TOKEN_METADATA_RELATIONS = VALID_TOKEN_METADATA_RELATIONS;
46296
46948
  exports.VALID_TRANSACTION_RELATIONS = VALID_TRANSACTION_RELATIONS;
46297
46949
  exports.VALID_USER_RELATIONS = VALID_USER_RELATIONS;
46298
46950
  exports.WalletEventsManager = WalletEventsManager;
@@ -46344,6 +46996,7 @@ exports.isValidCampaignRelation = isValidCampaignRelation;
46344
46996
  exports.isValidRedemptionRedeemRelation = isValidRedemptionRedeemRelation;
46345
46997
  exports.isValidRedemptionRelation = isValidRedemptionRelation;
46346
46998
  exports.isValidRelation = isValidRelation;
46999
+ exports.isValidTokenMetadataRelation = isValidTokenMetadataRelation;
46347
47000
  exports.isValidTransactionRelation = isValidTransactionRelation;
46348
47001
  exports.isValidUserRelation = isValidUserRelation;
46349
47002
  exports.mergeWithDefaults = mergeWithDefaults;