@explorins/pers-sdk-react-native 2.0.5 → 2.1.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.
@@ -1,18 +1,19 @@
1
- import { useCallback } from 'react';
1
+ import { useCallback, useMemo } from 'react';
2
2
  import { usePersSDK } from '../providers/PersSDKProvider';
3
+ import { Web3Manager } from '@explorins/pers-sdk/web3';
3
4
  import type {
4
5
  TokenBalance,
5
6
  TokenBalanceRequest,
6
7
  TokenCollectionRequest,
7
8
  TokenCollection,
8
- TokenMetadata
9
+ TokenMetadata,
10
+ AccountOwnedTokensResult
9
11
  } from '@explorins/pers-sdk/web3';
10
12
  import type { ChainData } from '@explorins/pers-sdk/web3-chain';
11
13
  import type { TokenDTO } from '@explorins/pers-shared';
12
- import type { AccountOwnedTokensResult } from '@explorins/pers-sdk';
13
14
 
14
15
  // Re-export for convenience
15
- export type { AccountOwnedTokensResult } from '@explorins/pers-sdk';
16
+ export type { AccountOwnedTokensResult } from '@explorins/pers-sdk/web3';
16
17
 
17
18
  /**
18
19
  * React hook for Web3 operations in the PERS SDK
@@ -71,6 +72,15 @@ export const useWeb3 = () => {
71
72
  console.warn('SDK not authenticated. Some web3 operations may fail.');
72
73
  }
73
74
 
75
+ /**
76
+ * Create Web3Manager instance lazily
77
+ * Web3Manager is now separate from PersSDK since v2.0.11+
78
+ */
79
+ const web3 = useMemo(() => {
80
+ if (!sdk) return null;
81
+ return new Web3Manager(sdk.api());
82
+ }, [sdk]);
83
+
74
84
  /**
75
85
  * Retrieves token balance for a specific wallet and contract
76
86
  *
@@ -91,18 +101,18 @@ export const useWeb3 = () => {
91
101
  * ```
92
102
  */
93
103
  const getTokenBalance = useCallback(async (request: TokenBalanceRequest): Promise<TokenBalance> => {
94
- if (!isInitialized || !sdk) {
104
+ if (!isInitialized || !web3) {
95
105
  throw new Error('SDK not initialized. Call initialize() first.');
96
106
  }
97
107
 
98
108
  try {
99
- const result = await sdk.web3.getTokenBalance(request);
109
+ const result = await web3.getTokenBalance(request);
100
110
  return result;
101
111
  } catch (error) {
102
112
  console.error('Failed to fetch token balance:', error);
103
113
  throw error;
104
114
  }
105
- }, [sdk, isInitialized]);
115
+ }, [web3, isInitialized]);
106
116
 
107
117
  /**
108
118
  * Retrieves metadata for a specific token (useful for NFTs)
@@ -124,88 +134,88 @@ export const useWeb3 = () => {
124
134
  * ```
125
135
  */
126
136
  const getTokenMetadata = useCallback(async (request: TokenBalanceRequest): Promise<TokenMetadata | null> => {
127
- if (!isInitialized || !sdk) {
137
+ if (!isInitialized || !web3) {
128
138
  throw new Error('SDK not initialized. Call initialize() first.');
129
139
  }
130
140
 
131
141
  try {
132
- const result = await sdk.web3.getTokenMetadata(request);
142
+ const result = await web3.getTokenMetadata(request);
133
143
  return result;
134
144
  } catch (error) {
135
145
  console.error('Failed to fetch token metadata:', error);
136
146
  throw error;
137
147
  }
138
- }, [sdk, isInitialized]);
148
+ }, [web3, isInitialized]);
139
149
 
140
150
  const getTokenCollection = useCallback(async (request: TokenCollectionRequest): Promise<TokenCollection> => {
141
- if (!isInitialized || !sdk) {
151
+ if (!isInitialized || !web3) {
142
152
  throw new Error('SDK not initialized. Call initialize() first.');
143
153
  }
144
154
 
145
155
  try {
146
- const result = await sdk.web3.getTokenCollection(request);
156
+ const result = await web3.getTokenCollection(request);
147
157
  return result;
148
158
  } catch (error) {
149
159
  console.error('Failed to fetch token collection:', error);
150
160
  throw error;
151
161
  }
152
- }, [sdk, isInitialized]);
162
+ }, [web3, isInitialized]);
153
163
 
154
164
  const resolveIPFSUrl = useCallback(async (url: string, chainId: number): Promise<string> => {
155
- if (!isInitialized || !sdk) {
165
+ if (!isInitialized || !web3) {
156
166
  throw new Error('SDK not initialized. Call initialize() first.');
157
167
  }
158
168
 
159
169
  try {
160
- const result = await sdk.web3.resolveIPFSUrl(url, chainId);
170
+ const result = await web3.resolveIPFSUrl(url, chainId);
161
171
  return result;
162
172
  } catch (error) {
163
173
  console.error('Failed to resolve IPFS URL:', error);
164
174
  throw error;
165
175
  }
166
- }, [sdk, isInitialized]);
176
+ }, [web3, isInitialized]);
167
177
 
168
178
  const fetchAndProcessMetadata = useCallback(async (tokenUri: string, chainId: number): Promise<TokenMetadata | null> => {
169
- if (!isInitialized || !sdk) {
179
+ if (!isInitialized || !web3) {
170
180
  throw new Error('SDK not initialized. Call initialize() first.');
171
181
  }
172
182
 
173
183
  try {
174
- const result = await sdk.web3.fetchAndProcessMetadata(tokenUri, chainId);
184
+ const result = await web3.fetchAndProcessMetadata(tokenUri, chainId);
175
185
  return result;
176
186
  } catch (error) {
177
187
  console.error('Failed to fetch and process metadata:', error);
178
188
  throw error;
179
189
  }
180
- }, [sdk, isInitialized]);
190
+ }, [web3, isInitialized]);
181
191
 
182
192
  const getChainDataById = useCallback(async (chainId: number): Promise<ChainData | null> => {
183
- if (!isInitialized || !sdk) {
193
+ if (!isInitialized || !web3) {
184
194
  throw new Error('SDK not initialized. Call initialize() first.');
185
195
  }
186
196
 
187
197
  try {
188
- const result = await sdk.web3.getChainDataById(chainId);
198
+ const result = await web3.getChainDataById(chainId);
189
199
  return result;
190
200
  } catch (error) {
191
201
  console.error('Failed to fetch chain data:', error);
192
202
  throw error;
193
203
  }
194
- }, [sdk, isInitialized]);
204
+ }, [web3, isInitialized]);
195
205
 
196
206
  const getExplorerUrl = useCallback(async (chainId: number, address: string, type: 'address' | 'tx'): Promise<string> => {
197
- if (!isInitialized || !sdk) {
207
+ if (!isInitialized || !web3) {
198
208
  throw new Error('SDK not initialized. Call initialize() first.');
199
209
  }
200
210
 
201
211
  try {
202
- const result = await sdk.web3.getExplorerUrl(chainId, address, type);
212
+ const result = await web3.getExplorerUrl(chainId, address, type);
203
213
  return result;
204
214
  } catch (error) {
205
215
  console.error('Failed to generate explorer URL:', error);
206
216
  throw error;
207
217
  }
208
- }, [sdk, isInitialized]);
218
+ }, [web3, isInitialized]);
209
219
 
210
220
  // ==========================================
211
221
  // HELPER METHODS (delegating to core SDK)
@@ -227,9 +237,9 @@ export const useWeb3 = () => {
227
237
  * @see {@link getAccountOwnedTokensFromContract} - Recommended helper that handles this automatically
228
238
  */
229
239
  const extractTokenIds = useCallback((token: TokenDTO): string[] | undefined => {
230
- // Pure function - delegates to core SDK (no initialization required)
231
- return sdk?.web3.extractTokenIds(token);
232
- }, [sdk]);
240
+ // Pure function - delegates to Web3Manager (no initialization required)
241
+ return web3?.extractTokenIds(token);
242
+ }, [web3]);
233
243
 
234
244
  /**
235
245
  * Get owned tokens from a specific token contract for any blockchain address.
@@ -269,12 +279,12 @@ export const useWeb3 = () => {
269
279
  token: TokenDTO,
270
280
  maxTokens: number = 50
271
281
  ): Promise<AccountOwnedTokensResult> => {
272
- if (!isInitialized || !sdk) {
282
+ if (!isInitialized || !web3) {
273
283
  throw new Error('SDK not initialized. Call initialize() first.');
274
284
  }
275
285
 
276
- return sdk.web3.getAccountOwnedTokensFromContract(accountAddress, token, maxTokens);
277
- }, [sdk, isInitialized]);
286
+ return web3.getAccountOwnedTokensFromContract(accountAddress, token, maxTokens);
287
+ }, [web3, isInitialized]);
278
288
 
279
289
  /**
280
290
  * Build a TokenCollectionRequest from a TokenDTO.
@@ -293,11 +303,11 @@ export const useWeb3 = () => {
293
303
  token: TokenDTO,
294
304
  maxTokens: number = 50
295
305
  ): TokenCollectionRequest => {
296
- if (!sdk) {
306
+ if (!web3) {
297
307
  throw new Error('SDK not initialized. Call initialize() first.');
298
308
  }
299
- return sdk.web3.buildCollectionRequest(accountAddress, token, maxTokens);
300
- }, [sdk]);
309
+ return web3.buildCollectionRequest(accountAddress, token, maxTokens);
310
+ }, [web3]);
301
311
 
302
312
  return {
303
313
  getTokenBalance,
@@ -311,7 +321,7 @@ export const useWeb3 = () => {
311
321
  extractTokenIds,
312
322
  getAccountOwnedTokensFromContract,
313
323
  buildCollectionRequest,
314
- isAvailable: isInitialized && !!sdk?.web3,
324
+ isAvailable: isInitialized && !!web3,
315
325
  };
316
326
  };
317
327
 
package/src/index.ts CHANGED
@@ -247,6 +247,7 @@ export {
247
247
  export {
248
248
  useAuth,
249
249
  useTokens,
250
+ useTokenBalances,
250
251
  useTransactions,
251
252
  useTransactionSigner,
252
253
  SigningStatus,
@@ -265,11 +266,14 @@ export {
265
266
  } from './hooks';
266
267
 
267
268
  // Re-export signing status types for convenience
268
- export type { OnStatusUpdateFn, StatusUpdateData, SigningStatusType } from './hooks';
269
+ export type { OnStatusUpdateFn, StatusUpdateData, SigningStatusType, TransactionSigningResult, SubmissionResult, AuthenticatedUser } from './hooks';
269
270
 
270
271
  // Re-export event types for convenience
271
272
  export type { EventsHook, PersEvent, EventHandler, EventFilter, Unsubscribe } from './hooks';
272
273
 
274
+ // Re-export token balance types for convenience
275
+ export type { TokenBalanceWithToken, UseTokenBalancesOptions, UseTokenBalancesResult } from './hooks';
276
+
273
277
  // ==============================================================================
274
278
  // PLATFORM ADAPTERS
275
279
  // ==============================================================================
@@ -420,31 +424,108 @@ export {
420
424
  buildTransferRequest,
421
425
  buildPOSTransferRequest,
422
426
  buildPOSBurnRequest,
423
- buildSubmissionRequest
427
+ buildSubmissionRequest,
428
+ ClientTransactionType,
429
+ extractDeadlineFromSigningData,
430
+ buildPendingTransactionData
424
431
  } from '@explorins/pers-sdk/transaction';
425
432
 
426
- export type { POSAuthorizationOptions } from '@explorins/pers-sdk/transaction';
433
+ export type { POSAuthorizationOptions, PendingTransactionParams } from '@explorins/pers-sdk/transaction';
427
434
 
428
435
  // ==============================================================================
429
436
  // ERROR CLASSES (for instanceof checks)
430
437
  // ==============================================================================
431
438
 
432
439
  /**
433
- * SDK Error classes for type checking in catch blocks
440
+ * Structured error handling for PERS SDK
441
+ *
442
+ * The SDK provides comprehensive error classes and utilities for consistent error handling
443
+ * across all operations. Errors follow the StructuredError pattern from @explorins/pers-shared.
444
+ *
445
+ * **Error Classes:**
446
+ * - `PersApiError` - API errors with structured backend details
447
+ * - `AuthenticationError` - Authentication/authorization failures (401)
448
+ *
449
+ * **Error Utilities:**
450
+ * - `ErrorUtils.getMessage(error)` - Extract user-friendly message
451
+ * - `ErrorUtils.getStatus(error)` - Get HTTP status code
452
+ * - `ErrorUtils.isRetryable(error)` - Check if operation should be retried
453
+ * - `ErrorUtils.isTokenExpiredError(error)` - Detect token expiration
434
454
  *
435
455
  * @example
456
+ * **Basic Error Handling:**
436
457
  * ```typescript
437
- * import { PersApiError } from '@explorins/pers-sdk-react-native';
458
+ * import { PersApiError, ErrorUtils } from '@explorins/pers-sdk-react-native';
438
459
  *
439
460
  * try {
440
461
  * await sdk.campaigns.claimCampaign({ campaignId });
441
462
  * } catch (error) {
442
463
  * if (error instanceof PersApiError) {
443
- * console.log(error.message); // Clean backend message
444
- * console.log(error.code); // e.g., 'CAMPAIGN_BUSINESS_REQUIRED'
445
- * console.log(error.status); // e.g., 400
464
+ * // Structured error with backend details
465
+ * console.log('Error code:', error.code); // 'CAMPAIGN_BUSINESS_REQUIRED'
466
+ * console.log('Status:', error.status); // 400
467
+ * console.log('Message:', error.message); // Backend error message
468
+ * console.log('User message:', error.userMessage); // User-friendly message
469
+ * console.log('Retryable:', error.retryable); // false
470
+ * } else {
471
+ * // Generic error fallback
472
+ * const message = ErrorUtils.getMessage(error);
473
+ * console.error('Operation failed:', message);
446
474
  * }
447
475
  * }
448
476
  * ```
477
+ *
478
+ * @example
479
+ * **Error Utilities:**
480
+ * ```typescript
481
+ * import { ErrorUtils } from '@explorins/pers-sdk-react-native';
482
+ *
483
+ * try {
484
+ * await someOperation();
485
+ * } catch (error) {
486
+ * const status = ErrorUtils.getStatus(error); // Extract status code
487
+ * const message = ErrorUtils.getMessage(error); // Extract message
488
+ * const retryable = ErrorUtils.isRetryable(error); // Check if retryable
489
+ *
490
+ * if (ErrorUtils.isTokenExpiredError(error)) {
491
+ * // Handle token expiration
492
+ * await refreshToken();
493
+ * } else if (retryable) {
494
+ * // Retry operation
495
+ * await retry(someOperation);
496
+ * } else {
497
+ * // Show error to user
498
+ * showError(message);
499
+ * }
500
+ * }
501
+ * ```
502
+ *
503
+ * @example
504
+ * **React Native Error Display:**
505
+ * ```typescript
506
+ * import { PersApiError, ErrorUtils } from '@explorins/pers-sdk-react-native';
507
+ * import { Alert } from 'react-native';
508
+ *
509
+ * const handleError = (error: unknown) => {
510
+ * if (error instanceof PersApiError) {
511
+ * // Show structured error
512
+ * Alert.alert(
513
+ * 'Error',
514
+ * error.userMessage || error.message,
515
+ * [
516
+ * { text: 'OK' },
517
+ * error.retryable && { text: 'Retry', onPress: retry }
518
+ * ].filter(Boolean)
519
+ * );
520
+ * } else {
521
+ * // Show generic error
522
+ * Alert.alert('Error', ErrorUtils.getMessage(error));
523
+ * }
524
+ * };
525
+ * ```
449
526
  */
450
- export { PersApiError, AuthenticationError } from '@explorins/pers-sdk/core';
527
+ export {
528
+ PersApiError,
529
+ AuthenticationError,
530
+ ErrorUtils
531
+ } from '@explorins/pers-sdk/core';
@@ -132,6 +132,11 @@ export const PersSDKProvider: React.FC<{
132
132
  }
133
133
  }, [isInitialized]);
134
134
 
135
+ const setAuthenticationState = useCallback((user: UserDTO | AdminDTO | null, isAuthenticated: boolean) => {
136
+ setUser(user);
137
+ setIsAuthenticated(isAuthenticated);
138
+ }, []);
139
+
135
140
  // Auto-initialize if config is provided
136
141
  useEffect(() => {
137
142
  if (config && !isInitialized && !initializingRef.current) {
@@ -141,11 +146,6 @@ export const PersSDKProvider: React.FC<{
141
146
  }
142
147
  }, [config, isInitialized, initialize]);
143
148
 
144
- const setAuthenticationState = useCallback((user: UserDTO | AdminDTO | null, isAuthenticated: boolean) => {
145
- setUser(user);
146
- setIsAuthenticated(isAuthenticated);
147
- }, []);
148
-
149
149
  const refreshUserData = useCallback(async (): Promise<void> => {
150
150
  if (!sdk || !isAuthenticated || !isInitialized) {
151
151
  throw new Error('SDK not initialized or not authenticated. Cannot refresh user data.');
@@ -159,53 +159,38 @@ export const PersSDKProvider: React.FC<{
159
159
  throw error;
160
160
  }
161
161
  }, [sdk, isAuthenticated, isInitialized]);
162
-
163
- // Listen for authentication status changes and refresh user data when tokens are renewed
162
+ // Listen for authentication events from core SDK
163
+ // Set up immediately when SDK is created (don't wait for isInitialized)
164
+ // to catch session_restored events that fire during SDK initialization
164
165
  useEffect(() => {
165
- if (!authProvider || !isInitialized) return;
166
+ if (!sdk) return;
166
167
 
167
- // Access the config object with proper type safety
168
- const providerConfig = (authProvider as any).config;
169
- if (!providerConfig) return;
168
+ const unsubscribe = sdk.events.subscribe((event) => {
169
+ if (event.domain !== 'authentication') return;
170
170
 
171
- // Set up auth status change handler
172
- const originalHandler = providerConfig.onAuthStatusChange;
173
-
174
- const authStatusHandler = async (status: string) => {
175
- console.log('[PersSDK] Auth status changed:', status);
176
-
177
- // Call original handler first if it exists
178
- if (originalHandler) {
179
- await originalHandler(status);
171
+ // Session restored successfully - sync React state
172
+ if (event.type === 'session_restored') {
173
+ console.log('[PersSDK] Session restoration event received, syncing state...');
174
+ sdk.users.getCurrentUser()
175
+ .then(userData => {
176
+ setAuthenticationState(userData, true);
177
+ })
178
+ .catch(error => {
179
+ console.error('[PersSDK] Failed to sync restored session:', error);
180
+ });
180
181
  }
181
-
182
- // If token was refreshed successfully and user is authenticated, reload user data
183
- if (status === 'authenticated' && isAuthenticated && sdk) {
184
- try {
185
- console.log('[PersSDK] Token refreshed, reloading user data...');
186
- await refreshUserData();
187
- } catch (error) {
188
- console.error('[PersSDK] Failed to refresh user data after token renewal:', error);
189
- }
190
- }
191
-
192
- // If authentication failed, clear state
193
- if (status === 'auth_failed') {
194
- console.log('[PersSDK] Authentication failed, clearing state');
182
+
183
+ // Session restoration failed or auth error - clear React state
184
+ if (event.type === 'session_restoration_failed' || event.code === 'AUTH_FAILED') {
185
+ console.log('[PersSDK] Authentication failed - clearing session');
195
186
  setAuthenticationState(null, false);
196
187
  }
197
- };
198
-
199
- // Inject our handler into the auth provider config
200
- providerConfig.onAuthStatusChange = authStatusHandler;
201
-
202
- // Cleanup
188
+ }, { domains: ['authentication'] });
189
+
203
190
  return () => {
204
- if (originalHandler) {
205
- providerConfig.onAuthStatusChange = originalHandler;
206
- }
191
+ unsubscribe();
207
192
  };
208
- }, [authProvider, isInitialized, isAuthenticated, sdk, refreshUserData, setAuthenticationState]);
193
+ }, [sdk, setAuthenticationState]);
209
194
 
210
195
  // iOS/Android: Monitor app state and validate tokens when app becomes active
211
196
  useEffect(() => {
@@ -117,22 +117,39 @@ export class ReactNativeSecureStorage implements TokenStorage {
117
117
  }
118
118
 
119
119
  async clear(): Promise<void> {
120
- // Clear all known secure keys
120
+ const clearPromises: Promise<void>[] = [];
121
+
122
+ // Clear all known secure keys with retry logic
121
123
  for (const key of this.SECURE_KEYS) {
122
- try {
123
- if (Keychain) {
124
- await Keychain.resetGenericPassword({ service: this.getKeyName(key) });
125
- }
126
- } catch (e) {
127
- console.warn(`[ReactNativeSecureStorage] Failed to clear keychain key ${key}`, e);
128
- }
124
+ clearPromises.push(
125
+ (async () => {
126
+ try {
127
+ if (Keychain) {
128
+ await Keychain.resetGenericPassword({ service: this.getKeyName(key) });
129
+ }
130
+ } catch (e) {
131
+ console.warn(`[ReactNativeSecureStorage] Retry clearing ${key}`);
132
+ // Retry once
133
+ try {
134
+ if (Keychain) {
135
+ await Keychain.resetGenericPassword({ service: this.getKeyName(key) });
136
+ }
137
+ } catch (retryError) {
138
+ console.error(`[ReactNativeSecureStorage] Failed to clear ${key} after retry:`, retryError);
139
+ }
140
+ }
141
+ })()
142
+ );
129
143
  }
130
144
 
131
- // Clear AsyncStorage keys related to PERS
145
+ // Wait for all Keychain clearing to complete (or fail)
146
+ await Promise.allSettled(clearPromises);
147
+
148
+ // Always clear AsyncStorage fallback
132
149
  try {
133
150
  await this.fallbackStorage.clear();
134
151
  } catch (e) {
135
- console.warn('[ReactNativeSecureStorage] Failed to clear AsyncStorage', e);
152
+ console.error('[ReactNativeSecureStorage] Failed to clear AsyncStorage:', e);
136
153
  }
137
154
  }
138
155