@explorins/pers-sdk-react-native 2.0.4 → 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.
@@ -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,
@@ -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';
@@ -1,5 +1,5 @@
1
1
  import React, { createContext, useContext, useState, ReactNode, useCallback, useRef, useEffect, useMemo } from 'react';
2
- import { Platform } from 'react-native';
2
+ import { Platform, AppState, AppStateStatus } from 'react-native';
3
3
  import { PersSDK, PersConfig, DefaultAuthProvider } from '@explorins/pers-sdk/core';
4
4
  import { ReactNativeHttpClient } from './react-native-http-client';
5
5
  import { createReactNativeAuthProvider } from './react-native-auth-provider';
@@ -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) {
@@ -160,6 +178,86 @@ export const PersSDKProvider: React.FC<{
160
178
  }
161
179
  }, [sdk, isAuthenticated, isInitialized]);
162
180
 
181
+ // Listen for authentication status changes and refresh user data when tokens are renewed
182
+ useEffect(() => {
183
+ if (!authProvider || !isInitialized) return;
184
+
185
+ // Access the config object with proper type safety
186
+ const providerConfig = (authProvider as any).config;
187
+ if (!providerConfig) return;
188
+
189
+ // Set up auth status change handler
190
+ const originalHandler = providerConfig.onAuthStatusChange;
191
+
192
+ const authStatusHandler = async (status: string) => {
193
+ console.log('[PersSDK] Auth status changed:', status);
194
+
195
+ // Call original handler first if it exists
196
+ if (originalHandler) {
197
+ await originalHandler(status);
198
+ }
199
+
200
+ // If token was refreshed successfully and user is authenticated, reload user data
201
+ if (status === 'authenticated' && isAuthenticated && sdk) {
202
+ try {
203
+ console.log('[PersSDK] Token refreshed, reloading user data...');
204
+ await refreshUserData();
205
+ } catch (error) {
206
+ console.error('[PersSDK] Failed to refresh user data after token renewal:', error);
207
+ }
208
+ }
209
+
210
+ // If authentication failed, clear state
211
+ // Frontend app can observe isAuthenticated state change to show custom UI
212
+ if (status === 'auth_failed') {
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
220
+ setAuthenticationState(null, false);
221
+ }
222
+ };
223
+
224
+ // Inject our handler into the auth provider config
225
+ providerConfig.onAuthStatusChange = authStatusHandler;
226
+
227
+ // Cleanup
228
+ return () => {
229
+ if (originalHandler) {
230
+ providerConfig.onAuthStatusChange = originalHandler;
231
+ }
232
+ };
233
+ }, [authProvider, isInitialized, isAuthenticated, sdk, refreshUserData, setAuthenticationState]);
234
+
235
+ // iOS/Android: Monitor app state and validate tokens when app becomes active
236
+ useEffect(() => {
237
+ if (!sdk || Platform.OS === 'web') {
238
+ return;
239
+ }
240
+
241
+ const handleAppStateChange = async (nextAppState: AppStateStatus) => {
242
+ if (nextAppState === 'active' && isAuthenticated) {
243
+ console.log('[PersSDK] App became active - validating tokens');
244
+ try {
245
+ // Trigger token validation when app resumes from background
246
+ // This ensures tokens are checked after extended inactivity
247
+ await sdk.auth.ensureValidToken();
248
+ } catch (error) {
249
+ console.warn('[PersSDK] Token validation failed on app resume:', error);
250
+ }
251
+ }
252
+ };
253
+
254
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
255
+
256
+ return () => {
257
+ subscription?.remove();
258
+ };
259
+ }, [sdk, isAuthenticated]);
260
+
163
261
  const contextValue: PersSDKContext = useMemo(() => ({
164
262
  // Main SDK instance
165
263
  sdk,
@@ -94,18 +94,23 @@ export class AsyncStorageTokenStorage implements TokenStorage {
94
94
 
95
95
  async set(key: string, value: string): Promise<void> {
96
96
  try {
97
- await this.asyncStorage.setItem(`${this.keyPrefix}${key}`, value);
97
+ const fullKey = `${this.keyPrefix}${key}`;
98
+ await this.asyncStorage.setItem(fullKey, value);
99
+ console.log(`[AsyncStorage] Token stored: ${key}`);
98
100
  } catch (error) {
99
- console.error(`Failed to store token ${key}:`, error);
101
+ console.error(`[AsyncStorage] Failed to store token ${key}:`, error);
100
102
  throw new Error(`Token storage failed: ${error}`);
101
103
  }
102
104
  }
103
105
 
104
106
  async get(key: string): Promise<string | null> {
105
107
  try {
106
- return await this.asyncStorage.getItem(`${this.keyPrefix}${key}`);
108
+ const fullKey = `${this.keyPrefix}${key}`;
109
+ const value = await this.asyncStorage.getItem(fullKey);
110
+ console.log(`[AsyncStorage] Token retrieved: ${key} - ${value ? 'exists' : 'null'}`);
111
+ return value;
107
112
  } catch (error) {
108
- console.error(`Failed to retrieve token ${key}:`, error);
113
+ console.error(`[AsyncStorage] Failed to retrieve token ${key}:`, error);
109
114
  return null;
110
115
  }
111
116
  }
@@ -124,10 +129,14 @@ export class AsyncStorageTokenStorage implements TokenStorage {
124
129
  const ourKeys = allKeys.filter(key => key.startsWith(this.keyPrefix));
125
130
 
126
131
  if (ourKeys.length > 0) {
132
+ console.log(`[AsyncStorage] Clearing ${ourKeys.length} token(s):`, ourKeys);
127
133
  await this.asyncStorage.multiRemove(ourKeys);
134
+ console.log('[AsyncStorage] All tokens cleared successfully');
135
+ } else {
136
+ console.log('[AsyncStorage] No tokens to clear');
128
137
  }
129
138
  } catch (error) {
130
- console.error('Failed to clear token storage:', error);
139
+ console.error('[AsyncStorage] Failed to clear token storage:', error);
131
140
  }
132
141
  }
133
142
  }
@@ -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