@explorins/pers-sdk-react-native 2.0.5 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,
@@ -270,6 +271,9 @@ export type { OnStatusUpdateFn, StatusUpdateData, SigningStatusType } from './ho
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,19 +132,37 @@ 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) {
138
- initialize(config).catch(err => {
143
+ initialize(config).then(async () => {
144
+ // Validate stored tokens on startup
145
+ // SDK's initialize() already calls ensureValidToken() which handles expired tokens
146
+ // This provides an additional safety layer for missing/corrupted tokens
147
+ if (authProvider && sdk) {
148
+ try {
149
+ const hasToken = await sdk.auth.hasValidAuth();
150
+ if (!hasToken) {
151
+ console.log('[PersSDK] No tokens found on startup, ensuring clean state');
152
+ await authProvider.clearTokens();
153
+ setAuthenticationState(null, false);
154
+ }
155
+ // Note: Token expiration validation happens automatically in SDK's initialize()
156
+ // which calls ensureValidToken() → checks expiration → triggers AUTH_FAILED if needed
157
+ } catch (error) {
158
+ console.warn('[PersSDK] Token validation on startup failed:', error);
159
+ }
160
+ }
161
+ }).catch(err => {
139
162
  console.error('Auto-initialization failed:', err);
140
163
  });
141
164
  }
142
- }, [config, isInitialized, initialize]);
143
-
144
- const setAuthenticationState = useCallback((user: UserDTO | AdminDTO | null, isAuthenticated: boolean) => {
145
- setUser(user);
146
- setIsAuthenticated(isAuthenticated);
147
- }, []);
165
+ }, [config, isInitialized, initialize, authProvider, sdk, setAuthenticationState]);
148
166
 
149
167
  const refreshUserData = useCallback(async (): Promise<void> => {
150
168
  if (!sdk || !isAuthenticated || !isInitialized) {
@@ -190,8 +208,15 @@ export const PersSDKProvider: React.FC<{
190
208
  }
191
209
 
192
210
  // If authentication failed, clear state
211
+ // Frontend app can observe isAuthenticated state change to show custom UI
193
212
  if (status === 'auth_failed') {
194
- console.log('[PersSDK] Authentication failed, clearing state');
213
+ console.log('[PersSDK] Authentication failed - session expired');
214
+
215
+ // Note: Token clearing already handled by SDK's handleAuthFailure()
216
+ // which calls authProvider.clearTokens() with robust retry logic
217
+
218
+ // Clear React state to sync with SDK
219
+ // This triggers re-render, allowing app to show login screen
195
220
  setAuthenticationState(null, false);
196
221
  }
197
222
  };
@@ -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