@explorins/pers-sdk-react-native 2.1.20 → 2.1.22

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
@@ -3717,6 +3717,109 @@ const TRANSACTION_FORMAT_DESCRIPTIONS = {
3717
3717
  [TRANSACTION_FORMATS.EIP_712]: 'EIP-712 structured data for smart wallet meta-transactions'
3718
3718
  };
3719
3719
 
3720
+ /**
3721
+ * Error Codes extracted from actual domain error classes
3722
+ *
3723
+ * This object maps all error codes from domain error classes to provide
3724
+ * type-safe constants for API documentation and usage.
3725
+ *
3726
+ * Using object pattern for better performance and developer experience:
3727
+ * - Single export vs 50+ individual exports
3728
+ * - Namespaced access: CommonErrorCodes.USER_NOT_FOUND
3729
+ * - Better tree-shaking and bundle optimization
3730
+ * - Cleaner type definition
3731
+ */
3732
+ const CommonErrorCodes = {
3733
+ // User domain
3734
+ USER_NOT_FOUND: 'USER_NOT_FOUND',
3735
+ USER_ALREADY_EXISTS: 'USER_ALREADY_EXISTS',
3736
+ USER_NOT_AUTHORIZED: 'USER_NOT_AUTHORIZED',
3737
+ // Token domain
3738
+ TOKEN_NOT_FOUND: 'TOKEN_NOT_FOUND',
3739
+ TOKEN_NOT_ACTIVE: 'TOKEN_NOT_ACTIVE',
3740
+ TOKEN_ID_REQUIRED: 'TOKEN_ID_REQUIRED',
3741
+ TOKEN_TYPE_NOT_FOUND: 'TOKEN_TYPE_NOT_FOUND',
3742
+ // Wallet domain
3743
+ WALLET_MISSING_SIGNING: 'WALLET_MISSING_SIGNING',
3744
+ WALLET_NOT_FOUND: 'WALLET_NOT_FOUND',
3745
+ SMART_WALLET_PROCESSING_FAILED: 'SMART_WALLET_PROCESSING_FAILED',
3746
+ // Balance domain
3747
+ INSUFFICIENT_BALANCE: 'INSUFFICIENT_BALANCE',
3748
+ // Contract domain
3749
+ CONTRACT_NOT_FOUND: 'CONTRACT_NOT_FOUND',
3750
+ // Business domain
3751
+ BUSINESS_NOT_FOUND: 'BUSINESS_NOT_FOUND',
3752
+ BUSINESS_NOT_MINTER: 'BUSINESS_NOT_MINTER',
3753
+ // Membership domain
3754
+ MEMBERSHIP_NOT_FOUND: 'MEMBERSHIP_NOT_FOUND',
3755
+ MEMBERSHIP_ALREADY_EXISTS: 'MEMBERSHIP_ALREADY_EXISTS',
3756
+ MEMBERSHIP_LAST_OWNER: 'MEMBERSHIP_LAST_OWNER',
3757
+ // Transaction domain
3758
+ TRANSACTION_NOT_FOUND: 'TRANSACTION_NOT_FOUND',
3759
+ TRANSACTION_SIGNATURE_INVALID: 'TRANSACTION_SIGNATURE_INVALID',
3760
+ TRANSACTION_ALREADY_COMPLETED: 'TRANSACTION_ALREADY_COMPLETED',
3761
+ TRANSACTION_NOT_PENDING: 'TRANSACTION_NOT_PENDING',
3762
+ TRANSACTION_EXPIRED: 'TRANSACTION_EXPIRED',
3763
+ TRANSACTION_VALIDATION_ERROR: 'TRANSACTION_VALIDATION_ERROR',
3764
+ UNEXPECTED_VALIDATION_SCENARIO: 'UNEXPECTED_VALIDATION_SCENARIO',
3765
+ // Campaign domain
3766
+ CAMPAIGN_USER_INFO_REQUIRED: 'CAMPAIGN_USER_INFO_REQUIRED',
3767
+ CAMPAIGN_BUSINESS_REQUIRED: 'CAMPAIGN_BUSINESS_REQUIRED',
3768
+ CAMPAIGN_CONDITION_NOT_MET: 'CAMPAIGN_CONDITION_NOT_MET',
3769
+ CAMPAIGN_ALREADY_CLAIMED: 'CAMPAIGN_ALREADY_CLAIMED',
3770
+ CAMPAIGN_NO_TRIGGER: 'CAMPAIGN_NO_TRIGGER',
3771
+ CAMPAIGN_LOCATION_REQUIRED: 'CAMPAIGN_LOCATION_REQUIRED',
3772
+ CAMPAIGN_DISTANCE_EXCEEDED: 'CAMPAIGN_DISTANCE_EXCEEDED',
3773
+ CAMPAIGN_NOT_ACTIVE: 'CAMPAIGN_NOT_ACTIVE',
3774
+ CAMPAIGN_ENDED: 'CAMPAIGN_ENDED',
3775
+ CAMPAIGN_NOT_STARTED: 'CAMPAIGN_NOT_STARTED',
3776
+ CAMPAIGN_CLAIM_LIMIT_REACHED: 'CAMPAIGN_CLAIM_LIMIT_REACHED',
3777
+ CAMPAIGN_COOLDOWN_ACTIVE: 'CAMPAIGN_COOLDOWN_ACTIVE',
3778
+ CAMPAIGN_DAILY_LIMIT_REACHED: 'CAMPAIGN_DAILY_LIMIT_REACHED',
3779
+ CAMPAIGN_GLOBAL_LIMIT_REACHED: 'CAMPAIGN_GLOBAL_LIMIT_REACHED',
3780
+ CAMPAIGN_GLOBAL_DAILY_LIMIT_REACHED: 'CAMPAIGN_GLOBAL_DAILY_LIMIT_REACHED',
3781
+ // Trigger domain
3782
+ TRIGGER_NOT_FOUND: 'TRIGGER_NOT_FOUND',
3783
+ // Trigger Source domain
3784
+ TRIGGER_SOURCE_NOT_FOUND: 'TRIGGER_SOURCE_NOT_FOUND',
3785
+ // Generic domain errors
3786
+ RESOURCE_ALREADY_EXISTS: 'RESOURCE_ALREADY_EXISTS',
3787
+ RESOURCE_IN_USE: 'RESOURCE_IN_USE',
3788
+ // Technical errors
3789
+ TECHNICAL_ERROR: 'TECHNICAL_ERROR',
3790
+ ORCHESTRATION_ERROR: 'ORCHESTRATION_ERROR',
3791
+ // System errors
3792
+ SYSTEM_ERROR: 'SYSTEM_ERROR',
3793
+ EXTERNAL_SERVICE_ERROR: 'EXTERNAL_SERVICE_ERROR',
3794
+ AUTHENTICATION_REQUIRED: 'AUTHENTICATION_REQUIRED',
3795
+ AUTHORIZATION_FAILED: 'AUTHORIZATION_FAILED',
3796
+ // Authentication errors
3797
+ INVALID_TOKEN: 'INVALID_TOKEN',
3798
+ TOKEN_EXPIRED: 'TOKEN_EXPIRED',
3799
+ TOKEN_REVOKED: 'TOKEN_REVOKED',
3800
+ REFRESH_TOKEN_EXPIRED: 'REFRESH_TOKEN_EXPIRED',
3801
+ REFRESH_TOKEN_REVOKED: 'REFRESH_TOKEN_REVOKED',
3802
+ FIREBASE_AUTH_ERROR: 'FIREBASE_AUTH_ERROR',
3803
+ // Generic fallbacks
3804
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
3805
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
3806
+ BUSINESS_RULE_VIOLATION: 'BUSINESS_RULE_VIOLATION',
3807
+ RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND',
3808
+ // Webhook domain
3809
+ WEBHOOK_NOT_FOUND: 'WEBHOOK_NOT_FOUND',
3810
+ WEBHOOK_INACTIVE: 'WEBHOOK_INACTIVE',
3811
+ WEBHOOK_SOURCE_NOT_ALLOWED: 'WEBHOOK_SOURCE_NOT_ALLOWED',
3812
+ WEBHOOK_SIGNATURE_INVALID: 'WEBHOOK_SIGNATURE_INVALID',
3813
+ WEBHOOK_FORWARD_FAILED: 'WEBHOOK_FORWARD_FAILED',
3814
+ WEBHOOK_TIMEOUT: 'WEBHOOK_TIMEOUT',
3815
+ // Conflict errors (409)
3816
+ RESOURCE_CONFLICT: 'RESOURCE_CONFLICT',
3817
+ CONCURRENT_MODIFICATION: 'CONCURRENT_MODIFICATION',
3818
+ // Timeout errors (504)
3819
+ REQUEST_TIMEOUT: 'REQUEST_TIMEOUT',
3820
+ GATEWAY_TIMEOUT: 'GATEWAY_TIMEOUT'
3821
+ };
3822
+
3720
3823
  /**
3721
3824
  * JWT token expiration utilities
3722
3825
  * Native implementation without external dependencies for optimal performance
@@ -6817,6 +6920,22 @@ class RedemptionApi {
6817
6920
  async createRedemptionType(redemptionType) {
6818
6921
  return this.apiClient.post(this.typesPath, redemptionType);
6819
6922
  }
6923
+ /**
6924
+ * ADMIN: Update redemption type
6925
+ *
6926
+ * @param redemptionType - Updated redemption type data (must include id)
6927
+ */
6928
+ async updateRedemptionType(redemptionType) {
6929
+ return this.apiClient.put(`${this.typesPath}/${redemptionType.id}`, redemptionType);
6930
+ }
6931
+ /**
6932
+ * ADMIN: Delete redemption type
6933
+ *
6934
+ * @param id - Redemption type ID to delete
6935
+ */
6936
+ async deleteRedemptionType(id) {
6937
+ return this.apiClient.delete(`${this.typesPath}/${id}`);
6938
+ }
6820
6939
  // ==========================================
6821
6940
  // TOKEN UNIT MANAGEMENT (Admin)
6822
6941
  // ==========================================
@@ -6974,6 +7093,27 @@ class RedemptionService {
6974
7093
  async deleteRedemptionTokenUnit(redemptionId, redemptionTokenUnitId) {
6975
7094
  return this.redemptionApi.deleteRedemptionTokenUnit(redemptionId, redemptionTokenUnitId);
6976
7095
  }
7096
+ // ==========================================
7097
+ // REDEMPTION TYPE MANAGEMENT (Admin)
7098
+ // ==========================================
7099
+ /**
7100
+ * ADMIN: Create redemption type
7101
+ */
7102
+ async createRedemptionType(data) {
7103
+ return this.redemptionApi.createRedemptionType(data);
7104
+ }
7105
+ /**
7106
+ * ADMIN: Update redemption type
7107
+ */
7108
+ async updateRedemptionType(data) {
7109
+ return this.redemptionApi.updateRedemptionType(data);
7110
+ }
7111
+ /**
7112
+ * ADMIN: Delete redemption type
7113
+ */
7114
+ async deleteRedemptionType(id) {
7115
+ return this.redemptionApi.deleteRedemptionType(id);
7116
+ }
6977
7117
  }
6978
7118
 
6979
7119
  /**
@@ -8236,8 +8376,6 @@ class TenantService {
8236
8376
  *
8237
8377
  * Provides a simplified API for common tenant management tasks while maintaining
8238
8378
  * access to the full tenant SDK for advanced use cases.
8239
- *
8240
- * Also provides chain-agnostic IPFS URL resolution using tenant configuration.
8241
8379
  */
8242
8380
  class TenantManager {
8243
8381
  constructor(apiClient) {
@@ -8281,52 +8419,6 @@ class TenantManager {
8281
8419
  async getClientConfig() {
8282
8420
  return this.cache.getOrSet('clientConfig', () => this.tenantService.getRemoteClientConfig(), CacheTTL.LONG);
8283
8421
  }
8284
- // ==========================================
8285
- // IPFS OPERATIONS (Chain-agnostic)
8286
- // ==========================================
8287
- /**
8288
- * Resolve IPFS URL to HTTPS URL using tenant's configured gateway.
8289
- *
8290
- * This is chain-agnostic - IPFS gateway is configured at the tenant level,
8291
- * not per-chain. Use this for resolving any ipfs:// URLs (images, metadata, etc.).
8292
- *
8293
- * @param url - URL to resolve (can be ipfs:// or https://)
8294
- * @returns Resolved HTTPS URL
8295
- * @throws Error if tenant's ipfsGatewayDomain is not configured
8296
- *
8297
- * @example
8298
- * ```typescript
8299
- * const imageUrl = await sdk.tenant.resolveIPFSUrl('ipfs://QmXxx.../image.png');
8300
- * // Returns: 'https://pers.mypinata.cloud/ipfs/QmXxx.../image.png'
8301
- *
8302
- * // Non-IPFS URLs pass through unchanged
8303
- * const httpUrl = await sdk.tenant.resolveIPFSUrl('https://example.com/image.png');
8304
- * // Returns: 'https://example.com/image.png'
8305
- * ```
8306
- */
8307
- async resolveIPFSUrl(url) {
8308
- if (!url || !url.startsWith('ipfs://')) {
8309
- return url;
8310
- }
8311
- const gateway = await this.getIpfsGatewayDomain();
8312
- return url.replace('ipfs://', `https://${gateway}/ipfs/`);
8313
- }
8314
- /**
8315
- * Get IPFS gateway domain from tenant configuration (cached).
8316
- *
8317
- * @returns IPFS gateway domain (e.g., 'pers.mypinata.cloud')
8318
- * @throws Error if ipfsGatewayDomain is not configured for this tenant
8319
- */
8320
- async getIpfsGatewayDomain() {
8321
- return this.cache.getOrSet('ipfsGateway', async () => {
8322
- const config = await this.getClientConfig();
8323
- if (!config.ipfsGatewayDomain) {
8324
- throw new Error('IPFS gateway domain not configured for tenant. ' +
8325
- 'Please configure ipfsGatewayDomain in tenant settings.');
8326
- }
8327
- return config.ipfsGatewayDomain;
8328
- }, CacheTTL.GATEWAY);
8329
- }
8330
8422
  /**
8331
8423
  * Get Google API key from tenant configuration (cached).
8332
8424
  *
@@ -8408,6 +8500,100 @@ class TenantManager {
8408
8500
  clearCache() {
8409
8501
  this.cache.clear();
8410
8502
  }
8503
+ // ==========================================
8504
+ // IPFS OPERATIONS (Lightweight, no Web3 deps)
8505
+ // ==========================================
8506
+ /**
8507
+ * Get the IPFS gateway domain from tenant config (cached)
8508
+ *
8509
+ * @returns Promise resolving to IPFS gateway domain (e.g., 'gateway.pinata.cloud')
8510
+ * @example
8511
+ * ```typescript
8512
+ * const gateway = await sdk.tenants.getIpfsGatewayDomain();
8513
+ * // Returns 'gateway.pinata.cloud' or tenant-specific gateway
8514
+ * ```
8515
+ */
8516
+ async getIpfsGatewayDomain() {
8517
+ return this.cache.getOrSet('ipfsGateway', async () => {
8518
+ const config = await this.getClientConfig();
8519
+ return config.ipfsGatewayDomain || 'gateway.pinata.cloud';
8520
+ }, CacheTTL.LONG);
8521
+ }
8522
+ /**
8523
+ * Resolve an IPFS URL to an HTTP gateway URL
8524
+ *
8525
+ * @param url - The URL to resolve (ipfs://, ar://, or HTTP URL)
8526
+ * @returns Promise resolving to HTTP gateway URL
8527
+ *
8528
+ * @example
8529
+ * ```typescript
8530
+ * const httpUrl = await sdk.tenants.resolveIpfsUrl('ipfs://QmXxx...');
8531
+ * // Returns 'https://gateway.pinata.cloud/ipfs/QmXxx...'
8532
+ * ```
8533
+ */
8534
+ async resolveIpfsUrl(url) {
8535
+ if (!url)
8536
+ return '';
8537
+ // Already an HTTP URL
8538
+ if (url.startsWith('http://') || url.startsWith('https://')) {
8539
+ return url;
8540
+ }
8541
+ const gateway = await this.getIpfsGatewayDomain();
8542
+ // Handle ipfs:// protocol
8543
+ if (url.startsWith('ipfs://')) {
8544
+ const cid = url.replace('ipfs://', '');
8545
+ return `https://${gateway}/ipfs/${cid}`;
8546
+ }
8547
+ // Handle ar:// (Arweave) protocol
8548
+ if (url.startsWith('ar://')) {
8549
+ const txId = url.replace('ar://', '');
8550
+ return `https://arweave.net/${txId}`;
8551
+ }
8552
+ return url;
8553
+ }
8554
+ /**
8555
+ * Fetch and process token metadata from IPFS/HTTP URI
8556
+ *
8557
+ * @param tokenUri - The token URI (ipfs://, ar://, or HTTP)
8558
+ * @returns Promise resolving to processed metadata or null on error
8559
+ *
8560
+ * @example
8561
+ * ```typescript
8562
+ * const metadata = await sdk.tenants.fetchAndProcessMetadata('ipfs://QmXxx.../metadata.json');
8563
+ * if (metadata) {
8564
+ * console.log(metadata.name, metadata.imageUrl);
8565
+ * }
8566
+ * ```
8567
+ */
8568
+ async fetchAndProcessMetadata(tokenUri) {
8569
+ try {
8570
+ const resolvedUri = await this.resolveIpfsUrl(tokenUri);
8571
+ const response = await fetch(resolvedUri);
8572
+ if (!response.ok) {
8573
+ console.warn(`Failed to fetch metadata from ${resolvedUri}: ${response.status}`);
8574
+ return null;
8575
+ }
8576
+ const rawMetadata = await response.json();
8577
+ // Normalize the metadata - resolve image URLs
8578
+ const imageUrl = rawMetadata.image || rawMetadata.image_url || rawMetadata.imageUrl || '';
8579
+ const resolvedImageUrl = await this.resolveIpfsUrl(imageUrl);
8580
+ const animationUrl = rawMetadata.animation_url || rawMetadata.animationUrl || '';
8581
+ const resolvedAnimationUrl = animationUrl ? await this.resolveIpfsUrl(animationUrl) : undefined;
8582
+ return {
8583
+ name: rawMetadata.name || 'Untitled',
8584
+ description: rawMetadata.description || '',
8585
+ imageUrl: resolvedImageUrl,
8586
+ animationUrl: resolvedAnimationUrl,
8587
+ externalUrl: rawMetadata.external_url || rawMetadata.externalUrl,
8588
+ attributes: rawMetadata.attributes,
8589
+ ...rawMetadata // Include any additional properties
8590
+ };
8591
+ }
8592
+ catch (error) {
8593
+ console.warn(`Error fetching metadata from ${tokenUri}:`, error);
8594
+ return null;
8595
+ }
8596
+ }
8411
8597
  }
8412
8598
 
8413
8599
  /**
@@ -9144,6 +9330,19 @@ exports.AuthStatus = void 0;
9144
9330
  function isExtendedProvider(provider) {
9145
9331
  return 'setAuthType' in provider && typeof provider.setAuthType === 'function';
9146
9332
  }
9333
+ /**
9334
+ * Fatal auth error codes that require immediate logout.
9335
+ * These indicate the session is completely invalid and cannot be recovered.
9336
+ * TOKEN_EXPIRED is NOT in this list - it's the normal "please refresh" case.
9337
+ *
9338
+ * Values validated against CommonErrorCodes from @explorins/pers-shared at compile time.
9339
+ */
9340
+ const FATAL_AUTH_CODES = [
9341
+ 'REFRESH_TOKEN_EXPIRED',
9342
+ 'REFRESH_TOKEN_REVOKED',
9343
+ 'INVALID_TOKEN',
9344
+ 'TOKEN_REVOKED'
9345
+ ];
9147
9346
  /**
9148
9347
  * Platform-agnostic authentication service
9149
9348
  * Handles login, token refresh, and storage operations
@@ -9153,6 +9352,7 @@ class AuthService {
9153
9352
  this.authApi = authApi;
9154
9353
  this.authProvider = authProvider;
9155
9354
  this.activeRefreshPromise = null;
9355
+ this.activeAuthFailurePromise = null;
9156
9356
  this.currentAuthStatus = exports.AuthStatus.UNAUTHENTICATED;
9157
9357
  }
9158
9358
  /**
@@ -9291,13 +9491,42 @@ class AuthService {
9291
9491
  }
9292
9492
  }
9293
9493
  /**
9294
- * Handle authentication failure with proper cleanup
9295
- * Centralized method to avoid redundant token cleanup logic
9494
+ * Check if an error code indicates a fatal auth failure.
9495
+ * Fatal errors require immediate logout - the session cannot be recovered.
9496
+ */
9497
+ isFatalAuthError(errorCode) {
9498
+ return FATAL_AUTH_CODES.includes(errorCode);
9499
+ }
9500
+ /**
9501
+ * Handle authentication failure with proper cleanup.
9502
+ * Called when backend returns fatal auth error codes or refresh fails.
9296
9503
  */
9297
9504
  async handleAuthFailure() {
9505
+ // Already failed - nothing to do
9298
9506
  if (this.currentAuthStatus === exports.AuthStatus.AUTH_FAILED) {
9299
9507
  return;
9300
9508
  }
9509
+ // If refresh is currently in progress, don't fail - wait for refresh result
9510
+ if (this.activeRefreshPromise) {
9511
+ return;
9512
+ }
9513
+ // If auth failure is already in progress, wait for it
9514
+ if (this.activeAuthFailurePromise) {
9515
+ return await this.activeAuthFailurePromise;
9516
+ }
9517
+ // Create the failure promise to deduplicate concurrent calls
9518
+ this.activeAuthFailurePromise = this.performAuthFailure();
9519
+ try {
9520
+ await this.activeAuthFailurePromise;
9521
+ }
9522
+ finally {
9523
+ this.activeAuthFailurePromise = null;
9524
+ }
9525
+ }
9526
+ /**
9527
+ * Performs the actual auth failure cleanup
9528
+ */
9529
+ async performAuthFailure() {
9301
9530
  // Clear active refresh operations to prevent hanging promises
9302
9531
  this.activeRefreshPromise = null;
9303
9532
  // Clear all stored tokens
@@ -9419,10 +9648,8 @@ class TokenRefreshManager {
9419
9648
  return true;
9420
9649
  }
9421
9650
  catch (error) {
9422
- const isAuthError = error && typeof error === 'object' && 'status' in error && error.status === 401;
9423
- if (isAuthError) {
9424
- await this.authService.handleAuthFailure();
9425
- }
9651
+ // Don't call handleAuthFailure here - let the caller decide
9652
+ // This prevents duplicate auth failure handling in race conditions
9426
9653
  return false;
9427
9654
  }
9428
9655
  }
@@ -9955,15 +10182,6 @@ class DefaultAuthProvider {
9955
10182
  const hasIndexedDB = this.canEnableDPoP();
9956
10183
  const canUseDPoP = hasCustomCryptoProvider || hasIndexedDB;
9957
10184
  this.dpopEnabled = !!(config.dpop?.enabled && canUseDPoP);
9958
- // Debug logging for DPoP configuration
9959
- console.log('[DefaultAuthProvider] DPoP config:', {
9960
- requestedEnabled: config.dpop?.enabled ?? false,
9961
- hasCustomCryptoProvider,
9962
- hasIndexedDB,
9963
- canUseDPoP,
9964
- finalEnabled: this.dpopEnabled,
9965
- cryptoProviderType: config.dpop?.cryptoProvider?.constructor?.name || 'none'
9966
- });
9967
10185
  if (config.dpop?.enabled && !canUseDPoP) {
9968
10186
  console.warn('[DefaultAuthProvider] DPoP disabled: IndexedDB not available and no custom crypto provider supplied. CryptoKey persistence requires IndexedDB or a custom provider.');
9969
10187
  }
@@ -10134,7 +10352,7 @@ class DefaultAuthProvider {
10134
10352
  /** SDK package name */
10135
10353
  const SDK_NAME = "@explorins/pers-sdk";
10136
10354
  /** SDK version - injected from package.json at build time */
10137
- const SDK_VERSION = "2.1.27";
10355
+ const SDK_VERSION = "2.1.31";
10138
10356
  /** Full SDK identifier for headers */
10139
10357
  const SDK_USER_AGENT = `${SDK_NAME}/${SDK_VERSION}`;
10140
10358
 
@@ -10283,12 +10501,21 @@ class PersApiClient {
10283
10501
  // Handle 401 errors centrally through AuthService
10284
10502
  if (status === 401) {
10285
10503
  const backendError = ErrorUtils.extractBackendErrorDetails(error);
10504
+ // Check for fatal auth errors that require immediate logout
10505
+ if (this.authService.isFatalAuthError(backendError.code)) {
10506
+ // Definitive auth failure - no retry, session is invalid
10507
+ await this.authService.handleAuthFailure();
10508
+ this.emitErrorEvent(backendError, errorMessage, endpoint, method, status, error);
10509
+ throw new AuthenticationError(errorMessage, endpoint, method, backendError);
10510
+ }
10286
10511
  // If this IS a refresh request that failed with 401, don't retry to avoid infinite loop
10287
10512
  if (options?.isRefreshRequest) {
10513
+ // Refresh failed - this is a real auth failure
10514
+ await this.authService.handleAuthFailure();
10288
10515
  this.emitErrorEvent(backendError, errorMessage, endpoint, method, status, error);
10289
10516
  throw new AuthenticationError(errorMessage, endpoint, method, backendError);
10290
10517
  }
10291
- // For regular requests: try refresh once, then fail
10518
+ // TOKEN_EXPIRED or unknown 401: try refresh once, then retry
10292
10519
  if (retryCount === 0 && this.mergedConfig.authProvider) {
10293
10520
  try {
10294
10521
  const refreshSuccessful = await this.refreshManager.attemptInternalRefresh();
@@ -10299,13 +10526,15 @@ class PersApiClient {
10299
10526
  domain: 'authentication',
10300
10527
  userMessage: 'Authentication token refreshed',
10301
10528
  });
10529
+ // Retry with new token
10302
10530
  return await this.request(method, endpoint, body, { ...options, retryCount: 1 });
10303
10531
  }
10304
10532
  }
10305
10533
  catch (refreshError) {
10306
- // Silent refresh failure
10534
+ // Refresh failed - fall through to handleAuthFailure
10307
10535
  }
10308
10536
  }
10537
+ // All recovery attempts failed
10309
10538
  await this.authService.handleAuthFailure();
10310
10539
  this.emitErrorEvent(backendError, errorMessage, endpoint, method, status, error);
10311
10540
  throw new AuthenticationError(errorMessage, endpoint, method, backendError);
@@ -12266,6 +12495,69 @@ class BusinessManager {
12266
12495
  async getBusinessTypes(options) {
12267
12496
  return this.businessService.getBusinessTypes();
12268
12497
  }
12498
+ /**
12499
+ * Admin: Create a new business type
12500
+ *
12501
+ * Creates a new business type category in the system.
12502
+ * Requires administrator privileges.
12503
+ *
12504
+ * @param data - Business type data (name, description, code, iconUrl)
12505
+ * @returns Promise resolving to the created business type
12506
+ * @throws {PersApiError} When not authenticated as admin or validation fails
12507
+ *
12508
+ * @example
12509
+ * ```typescript
12510
+ * const newType = await sdk.business.createBusinessType({
12511
+ * name: 'Restaurant',
12512
+ * description: 'Food service establishments',
12513
+ * code: 'RESTAURANT',
12514
+ * iconUrl: 'https://example.com/restaurant-icon.png'
12515
+ * });
12516
+ * ```
12517
+ */
12518
+ async createBusinessType(data) {
12519
+ return this.businessApi.createBusinessType(data);
12520
+ }
12521
+ /**
12522
+ * Admin: Update an existing business type
12523
+ *
12524
+ * Updates a business type's properties.
12525
+ * Requires administrator privileges.
12526
+ *
12527
+ * @param data - Updated business type data (must include id)
12528
+ * @returns Promise resolving to the updated business type
12529
+ * @throws {PersApiError} When not authenticated as admin or type not found
12530
+ *
12531
+ * @example
12532
+ * ```typescript
12533
+ * const updated = await sdk.business.updateBusinessType({
12534
+ * id: 123,
12535
+ * name: 'Fine Dining Restaurant',
12536
+ * description: 'Upscale dining establishments'
12537
+ * });
12538
+ * ```
12539
+ */
12540
+ async updateBusinessType(data) {
12541
+ return this.businessApi.updateBusinessType(data);
12542
+ }
12543
+ /**
12544
+ * Admin: Delete a business type
12545
+ *
12546
+ * Deletes a business type. Will fail if type is in use by businesses.
12547
+ * Requires administrator privileges.
12548
+ *
12549
+ * @param id - Business type ID to delete
12550
+ * @returns Promise resolving to true if deleted successfully
12551
+ * @throws {PersApiError} When not authenticated as admin or type not found/in use
12552
+ *
12553
+ * @example
12554
+ * ```typescript
12555
+ * const success = await sdk.business.deleteBusinessType(123);
12556
+ * ```
12557
+ */
12558
+ async deleteBusinessType(id) {
12559
+ return this.businessApi.deleteBusinessType(id);
12560
+ }
12269
12561
  /**
12270
12562
  * Get businesses by type
12271
12563
  *
@@ -14274,6 +14566,77 @@ class RedemptionManager {
14274
14566
  async deleteRedemptionTokenUnit(redemptionId, tokenUnitId) {
14275
14567
  return this.redemptionService.deleteRedemptionTokenUnit(redemptionId, tokenUnitId);
14276
14568
  }
14569
+ // ==========================================
14570
+ // REDEMPTION TYPE MANAGEMENT (Admin)
14571
+ // ==========================================
14572
+ /**
14573
+ * Admin: Create redemption type
14574
+ *
14575
+ * Creates a new redemption type category for organizing redemption offers.
14576
+ * Requires administrator privileges.
14577
+ *
14578
+ * @param data - Redemption type data (name, description, imageUrl)
14579
+ * @returns Promise resolving to created redemption type
14580
+ * @throws {PersApiError} When not authenticated as admin or validation fails
14581
+ *
14582
+ * @example
14583
+ * ```typescript
14584
+ * const newType = await sdk.redemptions.createRedemptionType({
14585
+ * name: 'Premium Rewards',
14586
+ * description: 'High-value exclusive rewards',
14587
+ * imageUrl: 'https://example.com/premium-icon.png'
14588
+ * });
14589
+ *
14590
+ * console.log('Created type:', newType.name);
14591
+ * ```
14592
+ */
14593
+ async createRedemptionType(data) {
14594
+ return this.redemptionService.createRedemptionType(data);
14595
+ }
14596
+ /**
14597
+ * Admin: Update redemption type
14598
+ *
14599
+ * Updates an existing redemption type's properties.
14600
+ * Requires administrator privileges.
14601
+ *
14602
+ * @param data - Updated redemption type data (must include id)
14603
+ * @returns Promise resolving to updated redemption type
14604
+ * @throws {PersApiError} When not authenticated as admin or type not found
14605
+ *
14606
+ * @example
14607
+ * ```typescript
14608
+ * const updated = await sdk.redemptions.updateRedemptionType({
14609
+ * id: 123,
14610
+ * name: 'VIP Rewards',
14611
+ * description: 'Updated description',
14612
+ * imageUrl: 'https://example.com/vip-icon.png'
14613
+ * });
14614
+ *
14615
+ * console.log('Updated type:', updated.name);
14616
+ * ```
14617
+ */
14618
+ async updateRedemptionType(data) {
14619
+ return this.redemptionService.updateRedemptionType(data);
14620
+ }
14621
+ /**
14622
+ * Admin: Delete redemption type
14623
+ *
14624
+ * Deletes a redemption type. Will fail if type is in use by redemptions.
14625
+ * Requires administrator privileges.
14626
+ *
14627
+ * @param id - Redemption type ID to delete
14628
+ * @returns Promise resolving to true if deleted successfully
14629
+ * @throws {PersApiError} When not authenticated as admin or type not found/in use
14630
+ *
14631
+ * @example
14632
+ * ```typescript
14633
+ * const success = await sdk.redemptions.deleteRedemptionType(123);
14634
+ * console.log('Deleted:', success);
14635
+ * ```
14636
+ */
14637
+ async deleteRedemptionType(id) {
14638
+ return this.redemptionService.deleteRedemptionType(id);
14639
+ }
14277
14640
  /**
14278
14641
  * Get the full redemption service for advanced operations
14279
14642
  *
@@ -19429,15 +19792,6 @@ function createReactNativeAuthProvider(projectKey, config = {}) {
19429
19792
  enabled: dpop.enabled ?? true,
19430
19793
  cryptoProvider: dpop.cryptoProvider
19431
19794
  } : undefined;
19432
- // Always log DPoP config for debugging (critical for mobile auth)
19433
- console.log('[ReactNativeAuthProvider] Config:', {
19434
- platform: reactNative.Platform.OS,
19435
- isWebPlatform,
19436
- dpopEnabled: dpopConfig?.enabled ?? false,
19437
- hasCryptoProvider: !!dpopConfig?.cryptoProvider,
19438
- cryptoProviderType: dpopConfig?.cryptoProvider?.constructor?.name || 'none',
19439
- storageType: tokenStorage.constructor.name
19440
- });
19441
19795
  // Return DefaultAuthProvider configured with platform-appropriate storage
19442
19796
  return new DefaultAuthProvider({
19443
19797
  authType,
@@ -19764,13 +20118,6 @@ const PersSDKProvider = ({ children, config }) => {
19764
20118
  if (reactNative.Platform.OS !== 'web' && isDpopEnabled && !dpopSettings.cryptoProvider) {
19765
20119
  dpopSettings.cryptoProvider = new ReactNativeDPoPProvider();
19766
20120
  }
19767
- // Log DPoP settings before creating auth provider
19768
- console.log('[PersSDKProvider] DPoP settings:', {
19769
- platform: reactNative.Platform.OS,
19770
- isDpopEnabled,
19771
- hasCryptoProvider: !!dpopSettings.cryptoProvider,
19772
- cryptoProviderType: dpopSettings.cryptoProvider?.constructor?.name || 'none'
19773
- });
19774
20121
  // Create platform-aware auth provider if not provided
19775
20122
  let authProvider;
19776
20123
  if (config.authProvider) {
@@ -40279,6 +40626,13 @@ class MetadataDomainService {
40279
40626
  const cacheKey = `resolve:${url}`;
40280
40627
  return this.cache.getOrSet(cacheKey, () => this.ipfsApi.resolveIPFSUrl(url), CacheTTL.GATEWAY);
40281
40628
  }
40629
+ /**
40630
+ * Get IPFS gateway domain from tenant configuration.
40631
+ * Useful for pipes that need to resolve URLs synchronously after initial fetch.
40632
+ */
40633
+ async getIpfsGatewayDomain() {
40634
+ return this.ipfsApi.getIpfsGatewayDomain();
40635
+ }
40282
40636
  /**
40283
40637
  * Clear all cached metadata and URLs for this service
40284
40638
  */
@@ -40384,6 +40738,15 @@ class Web3ApplicationService {
40384
40738
  async resolveIPFSUrl(url) {
40385
40739
  return this.metadataDomainService.resolveIPFSUrl(url);
40386
40740
  }
40741
+ /**
40742
+ * Get IPFS gateway domain from tenant configuration.
40743
+ * Useful for pipes that need to resolve URLs synchronously after initial fetch.
40744
+ *
40745
+ * @returns Gateway domain (e.g., 'pers.mypinata.cloud')
40746
+ */
40747
+ async getIpfsGatewayDomain() {
40748
+ return this.metadataDomainService.getIpfsGatewayDomain();
40749
+ }
40387
40750
  /**
40388
40751
  * Fetch and process metadata from any URI with IPFS conversion.
40389
40752
  *
@@ -40504,25 +40867,26 @@ class IPFSInfrastructureApi {
40504
40867
  throw new Error(`HTTP error! status: ${response.status}`);
40505
40868
  }
40506
40869
  const rawMetadata = await response.json();
40507
- // Resolve IPFS URLs for media fields
40508
- const resolvedImageUrl = rawMetadata.image
40509
- ? await this.resolveIPFSUrl(rawMetadata.image)
40510
- : '';
40511
- const resolvedAnimationUrl = rawMetadata.animation_url
40512
- ? await this.resolveIPFSUrl(rawMetadata.animation_url)
40513
- : undefined;
40870
+ // Resolve any IPFS URLs in string fields
40871
+ for (const key of Object.keys(rawMetadata)) {
40872
+ const value = rawMetadata[key];
40873
+ if (typeof value === 'string' && value.startsWith('ipfs://')) {
40874
+ rawMetadata[key] = await this.resolveIPFSUrl(value);
40875
+ }
40876
+ }
40514
40877
  // Extract custom properties (anything not in ERC standard)
40515
40878
  const customProperties = Object.fromEntries(Object.entries(rawMetadata).filter(([key]) => !['name', 'description', 'image', 'animation_url', 'external_url', 'attributes'].includes(key)));
40516
- // Return normalized SDK format (typed as TokenMetadata)
40517
- return {
40518
- name: rawMetadata.name || '',
40519
- description: rawMetadata.description || '',
40520
- imageUrl: resolvedImageUrl,
40521
- animationUrl: resolvedAnimationUrl,
40522
- externalUrl: rawMetadata.external_url || undefined,
40523
- attributes: rawMetadata.attributes || [],
40879
+ // Return normalized SDK format
40880
+ const result = {
40881
+ name: String(rawMetadata.name ?? ''),
40882
+ description: String(rawMetadata.description ?? ''),
40883
+ imageUrl: String(rawMetadata.image ?? ''),
40884
+ animationUrl: rawMetadata.animation_url ? String(rawMetadata.animation_url) : undefined,
40885
+ externalUrl: rawMetadata.external_url ? String(rawMetadata.external_url) : undefined,
40886
+ attributes: Array.isArray(rawMetadata.attributes) ? rawMetadata.attributes : [],
40524
40887
  ...customProperties
40525
40888
  };
40889
+ return result;
40526
40890
  }
40527
40891
  catch (error) {
40528
40892
  console.error('Error fetching metadata:', error);
@@ -40645,8 +41009,12 @@ async function getExplorerUrlByChainId(getChainData, chainId, address, type) {
40645
41009
  *
40646
41010
  * ## IPFS Resolution
40647
41011
  *
40648
- * For IPFS URL resolution, use `sdk.tenant.resolveIPFSUrl()` instead.
40649
- * IPFS is chain-agnostic and configured at the tenant level.
41012
+ * **Note:** IPFS is chain-agnostic. Use `sdk.tenants` for IPFS operations:
41013
+ *
41014
+ * ```typescript
41015
+ * const httpsUrl = await sdk.tenants.resolveIpfsUrl('ipfs://QmXxx.../image.png');
41016
+ * const metadata = await sdk.tenants.fetchAndProcessMetadata('ipfs://QmXxx.../metadata.json');
41017
+ * ```
40650
41018
  */
40651
41019
  class Web3Manager {
40652
41020
  // TODO: Add PersEventEmitter support for blockchain events
@@ -40700,9 +41068,10 @@ class Web3Manager {
40700
41068
  *
40701
41069
  * @param tokenUri - Token URI to fetch metadata from
40702
41070
  * @returns Promise resolving to processed metadata or null if not found
41071
+ * @deprecated Use sdk.tenants.fetchAndProcessMetadata() instead - IPFS is chain-agnostic
40703
41072
  */
40704
41073
  async fetchAndProcessMetadata(tokenUri) {
40705
- return this.web3ApplicationService.fetchAndProcessMetadata(tokenUri);
41074
+ return this.tenantManager.fetchAndProcessMetadata(tokenUri);
40706
41075
  }
40707
41076
  /**
40708
41077
  * Get blockchain chain data by chain ID
@@ -41010,16 +41379,27 @@ const useWeb3 = () => {
41010
41379
  /**
41011
41380
  * Resolve IPFS URL to HTTP gateway URL
41012
41381
  *
41013
- * @deprecated Use `sdk.tenant.resolveIPFSUrl()` directly - IPFS is chain-agnostic
41382
+ * Converts an ipfs:// URL to an HTTPS URL using the tenant's configured IPFS gateway.
41383
+ * Non-IPFS URLs are returned unchanged.
41384
+ *
41385
+ * Note: Uses sdk.tenants for IPFS resolution (chain-agnostic).
41386
+ *
41014
41387
  * @param url - IPFS URL to resolve (ipfs://...)
41015
41388
  * @returns Promise resolving to HTTP gateway URL
41389
+ *
41390
+ * @example
41391
+ * ```typescript
41392
+ * const { resolveIPFSUrl } = useWeb3();
41393
+ * const httpsUrl = await resolveIPFSUrl('ipfs://QmHash...');
41394
+ * // Returns: 'https://gateway.pinata.cloud/ipfs/QmHash...'
41395
+ * ```
41016
41396
  */
41017
41397
  const resolveIPFSUrl = react.useCallback(async (url) => {
41018
41398
  if (!isInitialized || !sdk) {
41019
41399
  throw new Error('SDK not initialized. Call initialize() first.');
41020
41400
  }
41021
41401
  try {
41022
- const result = await sdk.tenants.resolveIPFSUrl(url);
41402
+ const result = await sdk.tenants.resolveIpfsUrl(url);
41023
41403
  return result;
41024
41404
  }
41025
41405
  catch (error) {
@@ -41030,22 +41410,24 @@ const useWeb3 = () => {
41030
41410
  /**
41031
41411
  * Fetch and process token metadata from a URI
41032
41412
  *
41413
+ * Note: Uses sdk.tenants for metadata fetching (chain-agnostic IPFS).
41414
+ *
41033
41415
  * @param tokenUri - Token URI to fetch metadata from
41034
41416
  * @returns Promise resolving to processed metadata or null
41035
41417
  */
41036
41418
  const fetchAndProcessMetadata = react.useCallback(async (tokenUri) => {
41037
- if (!isInitialized || !web3) {
41419
+ if (!isInitialized || !sdk) {
41038
41420
  throw new Error('SDK not initialized. Call initialize() first.');
41039
41421
  }
41040
41422
  try {
41041
- const result = await web3.fetchAndProcessMetadata(tokenUri);
41423
+ const result = await sdk.tenants.fetchAndProcessMetadata(tokenUri);
41042
41424
  return result;
41043
41425
  }
41044
41426
  catch (error) {
41045
41427
  console.error('Failed to fetch and process metadata:', error);
41046
41428
  throw error;
41047
41429
  }
41048
- }, [web3, isInitialized]);
41430
+ }, [sdk, isInitialized]);
41049
41431
  const getChainDataById = react.useCallback(async (chainId) => {
41050
41432
  if (!isInitialized || !web3) {
41051
41433
  throw new Error('SDK not initialized. Call initialize() first.');
@@ -42680,8 +43062,7 @@ const useTransactions = () => {
42680
43062
  throw new Error('SDK not initialized. Call initialize() first.');
42681
43063
  }
42682
43064
  try {
42683
- const result = await sdk.transactions.getTransactionById(transactionId, include);
42684
- return result;
43065
+ return await sdk.transactions.getTransactionById(transactionId, include);
42685
43066
  }
42686
43067
  catch (error) {
42687
43068
  console.error('Failed to fetch transaction:', error);
@@ -42730,8 +43111,7 @@ const useTransactions = () => {
42730
43111
  throw new Error('SDK not initialized. Call initialize() first.');
42731
43112
  }
42732
43113
  try {
42733
- const result = await sdk.transactions.getUserTransactionHistory(options);
42734
- return result;
43114
+ return await sdk.transactions.getUserTransactionHistory(options);
42735
43115
  }
42736
43116
  catch (error) {
42737
43117
  console.error('Failed to fetch transaction history:', error);
@@ -43771,6 +44151,181 @@ const useRedemptions = () => {
43771
44151
  };
43772
44152
  };
43773
44153
 
44154
+ /**
44155
+ * React hook for resolving media URLs (IPFS or HTTPS).
44156
+ *
44157
+ * Automatically handles IPFS URLs by resolving them to HTTPS via tenant's gateway.
44158
+ * Non-IPFS URLs are returned as-is. Handles loading and error states.
44159
+ *
44160
+ * Uses sdk.tenants for IPFS resolution (lightweight, no Web3 deps).
44161
+ *
44162
+ * @param url - The URL to resolve (can be ipfs://, https://, or undefined)
44163
+ * @returns Object with resolved URL, loading state, and error
44164
+ *
44165
+ * @example
44166
+ * ```tsx
44167
+ * function NFTImage({ metadataUri }: { metadataUri?: string }) {
44168
+ * const { url, isLoading } = useResolvedUrl(metadataUri);
44169
+ *
44170
+ * if (isLoading) return <ActivityIndicator />;
44171
+ * return <Image source={{ uri: url }} />;
44172
+ * }
44173
+ * ```
44174
+ */
44175
+ function useResolvedUrl(url) {
44176
+ const { sdk, isInitialized } = usePersSDK();
44177
+ const [resolvedUrl, setResolvedUrl] = react.useState(null);
44178
+ const [isLoading, setIsLoading] = react.useState(false);
44179
+ const [error, setError] = react.useState(null);
44180
+ react.useEffect(() => {
44181
+ if (!url || !isInitialized || !sdk) {
44182
+ setResolvedUrl(null);
44183
+ return;
44184
+ }
44185
+ // Non-IPFS URLs don't need resolution
44186
+ if (!url.startsWith('ipfs://')) {
44187
+ setResolvedUrl(url);
44188
+ return;
44189
+ }
44190
+ setIsLoading(true);
44191
+ setError(null);
44192
+ sdk.tenants.resolveIpfsUrl(url)
44193
+ .then((resolved) => {
44194
+ setResolvedUrl(resolved);
44195
+ })
44196
+ .catch((err) => {
44197
+ setError(err instanceof Error ? err.message : 'Failed to resolve URL');
44198
+ setResolvedUrl(null);
44199
+ })
44200
+ .finally(() => {
44201
+ setIsLoading(false);
44202
+ });
44203
+ }, [url, isInitialized, sdk]);
44204
+ return {
44205
+ /** Resolved HTTPS URL (or original if not IPFS) */
44206
+ url: resolvedUrl,
44207
+ /** Whether resolution is in progress */
44208
+ isLoading,
44209
+ /** Error message if resolution failed */
44210
+ error,
44211
+ };
44212
+ }
44213
+ /**
44214
+ * React hook for fetching NFT/token metadata from a URI.
44215
+ *
44216
+ * Handles IPFS resolution automatically and returns parsed metadata
44217
+ * with all nested IPFS URLs already resolved to HTTPS.
44218
+ *
44219
+ * Uses sdk.tenants for metadata fetching (lightweight, no Web3 deps).
44220
+ *
44221
+ * @param metadataUri - The metadata URI to fetch (can be ipfs:// or https://)
44222
+ * @returns Object with metadata, loading state, error, and refresh function
44223
+ *
44224
+ * @example
44225
+ * ```tsx
44226
+ * function NFTDetails({ metadataUri }: { metadataUri?: string }) {
44227
+ * const { metadata, isLoading, error } = useMetadata(metadataUri);
44228
+ *
44229
+ * if (isLoading) return <ActivityIndicator />;
44230
+ * if (error) return <Text>Error: {error}</Text>;
44231
+ * if (!metadata) return null;
44232
+ *
44233
+ * return (
44234
+ * <View>
44235
+ * <Image source={{ uri: metadata.imageUrl }} />
44236
+ * <Text>{metadata.name}</Text>
44237
+ * <Text>{metadata.description}</Text>
44238
+ * </View>
44239
+ * );
44240
+ * }
44241
+ * ```
44242
+ */
44243
+ function useMetadata(metadataUri) {
44244
+ const { sdk, isInitialized } = usePersSDK();
44245
+ const [metadata, setMetadata] = react.useState(null);
44246
+ const [isLoading, setIsLoading] = react.useState(false);
44247
+ const [error, setError] = react.useState(null);
44248
+ const fetchMetadata = react.useCallback(async () => {
44249
+ if (!metadataUri || !isInitialized || !sdk) {
44250
+ setMetadata(null);
44251
+ return;
44252
+ }
44253
+ setIsLoading(true);
44254
+ setError(null);
44255
+ try {
44256
+ const result = await sdk.tenants.fetchAndProcessMetadata(metadataUri);
44257
+ setMetadata(result);
44258
+ }
44259
+ catch (err) {
44260
+ setError(err instanceof Error ? err.message : 'Failed to fetch metadata');
44261
+ setMetadata(null);
44262
+ }
44263
+ finally {
44264
+ setIsLoading(false);
44265
+ }
44266
+ }, [metadataUri, isInitialized, sdk]);
44267
+ react.useEffect(() => {
44268
+ fetchMetadata();
44269
+ }, [fetchMetadata]);
44270
+ return {
44271
+ /** Parsed metadata with resolved IPFS URLs (imageUrl, animationUrl, etc.) */
44272
+ metadata,
44273
+ /** Whether fetch is in progress */
44274
+ isLoading,
44275
+ /** Error message if fetch failed */
44276
+ error,
44277
+ /** Manually refresh metadata */
44278
+ refresh: fetchMetadata,
44279
+ };
44280
+ }
44281
+ /**
44282
+ * React hook for loading transaction/token metadata.
44283
+ *
44284
+ * Convenience wrapper around useMetadata specifically for transaction items.
44285
+ * Pass a transaction's metadataUri to get the NFT image, name, description, etc.
44286
+ *
44287
+ * @param transaction - Transaction object with optional metadataUri field
44288
+ * @returns Object with metadata, loading state, and image URL
44289
+ *
44290
+ * @example
44291
+ * ```tsx
44292
+ * import { useTransactionMetadata } from '@explorins/pers-sdk-react-native';
44293
+ *
44294
+ * function TransactionItem({ tx }: { tx: TransactionDTO }) {
44295
+ * const { metadata, isLoading, imageUrl } = useTransactionMetadata(tx);
44296
+ *
44297
+ * if (isLoading) return <ActivityIndicator />;
44298
+ *
44299
+ * return (
44300
+ * <View>
44301
+ * {imageUrl && <Image source={{ uri: imageUrl }} style={{ width: 60, height: 60 }} />}
44302
+ * <Text>{metadata?.name || tx.type}</Text>
44303
+ * <Text>{tx.amount} tokens</Text>
44304
+ * </View>
44305
+ * );
44306
+ * }
44307
+ * ```
44308
+ */
44309
+ function useTransactionMetadata(transaction) {
44310
+ const { metadata, isLoading, error, refresh } = useMetadata(transaction?.metadataUri);
44311
+ return {
44312
+ /** Parsed metadata with resolved URLs */
44313
+ metadata,
44314
+ /** Whether metadata is loading */
44315
+ isLoading,
44316
+ /** Error message if fetch failed */
44317
+ error,
44318
+ /** Convenience: resolved image URL (or undefined) */
44319
+ imageUrl: metadata?.imageUrl,
44320
+ /** Convenience: token/NFT name (or undefined) */
44321
+ name: metadata?.name,
44322
+ /** Convenience: token/NFT description (or undefined) */
44323
+ description: metadata?.description,
44324
+ /** Manually refresh metadata */
44325
+ refresh,
44326
+ };
44327
+ }
44328
+
43774
44329
  const usePurchases = () => {
43775
44330
  const { sdk, isInitialized, isAuthenticated } = usePersSDK();
43776
44331
  const createPaymentIntent = react.useCallback(async (amount, currency, receiptEmail, description) => {
@@ -45201,6 +45756,7 @@ exports.CampaignManager = CampaignManager;
45201
45756
  exports.CampaignService = CampaignService;
45202
45757
  exports.ChainTypes = ChainTypes;
45203
45758
  exports.ClientTransactionType = ClientTransactionType;
45759
+ exports.CommonErrorCodes = CommonErrorCodes;
45204
45760
  exports.DEFAULT_PERS_CONFIG = DEFAULT_PERS_CONFIG;
45205
45761
  exports.DPOP_STORAGE_KEYS = DPOP_STORAGE_KEYS;
45206
45762
  exports.DPoPManager = DPoPManager;
@@ -45348,12 +45904,16 @@ exports.useCampaigns = useCampaigns;
45348
45904
  exports.useDonations = useDonations;
45349
45905
  exports.useEvents = useEvents;
45350
45906
  exports.useFiles = useFiles;
45907
+ exports.useIPFSUrl = useResolvedUrl;
45908
+ exports.useMetadata = useMetadata;
45351
45909
  exports.usePersSDK = usePersSDK;
45352
45910
  exports.usePurchases = usePurchases;
45353
45911
  exports.useRedemptions = useRedemptions;
45912
+ exports.useResolvedUrl = useResolvedUrl;
45354
45913
  exports.useTenants = useTenants;
45355
45914
  exports.useTokenBalances = useTokenBalances;
45356
45915
  exports.useTokens = useTokens;
45916
+ exports.useTransactionMetadata = useTransactionMetadata;
45357
45917
  exports.useTransactionSigner = useTransactionSigner;
45358
45918
  exports.useTransactions = useTransactions;
45359
45919
  exports.useTriggerSources = useTriggerSources;