@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.
- package/README.md +117 -4
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useTokenBalances.d.ts +140 -0
- package/dist/hooks/useTokenBalances.d.ts.map +1 -0
- package/dist/hooks/useTokenBalances.js +213 -0
- package/dist/hooks/useWeb3.d.ts +2 -3
- package/dist/hooks/useWeb3.d.ts.map +1 -1
- package/dist/hooks/useWeb3.js +42 -32
- package/dist/index.d.ts +80 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29625 -29153
- package/dist/index.js.map +1 -1
- package/dist/providers/PersSDKProvider.d.ts.map +1 -1
- package/dist/providers/PersSDKProvider.js +95 -7
- package/dist/storage/async-storage-token-storage.d.ts.map +1 -1
- package/dist/storage/async-storage-token-storage.js +15 -5
- package/dist/storage/rn-secure-storage.d.ts.map +1 -1
- package/dist/storage/rn-secure-storage.js +24 -10
- package/package.json +2 -4
- package/src/hooks/index.ts +7 -1
- package/src/hooks/useTokenBalances.ts +290 -0
- package/src/hooks/useWeb3.ts +45 -35
- package/src/index.ts +89 -8
- package/src/providers/PersSDKProvider.tsx +106 -8
- package/src/storage/async-storage-token-storage.ts +14 -5
- package/src/storage/rn-secure-storage.ts +27 -10
package/src/hooks/useWeb3.ts
CHANGED
|
@@ -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 || !
|
|
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
|
|
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
|
-
}, [
|
|
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 || !
|
|
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
|
|
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
|
-
}, [
|
|
148
|
+
}, [web3, isInitialized]);
|
|
139
149
|
|
|
140
150
|
const getTokenCollection = useCallback(async (request: TokenCollectionRequest): Promise<TokenCollection> => {
|
|
141
|
-
if (!isInitialized || !
|
|
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
|
|
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
|
-
}, [
|
|
162
|
+
}, [web3, isInitialized]);
|
|
153
163
|
|
|
154
164
|
const resolveIPFSUrl = useCallback(async (url: string, chainId: number): Promise<string> => {
|
|
155
|
-
if (!isInitialized || !
|
|
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
|
|
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
|
-
}, [
|
|
176
|
+
}, [web3, isInitialized]);
|
|
167
177
|
|
|
168
178
|
const fetchAndProcessMetadata = useCallback(async (tokenUri: string, chainId: number): Promise<TokenMetadata | null> => {
|
|
169
|
-
if (!isInitialized || !
|
|
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
|
|
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
|
-
}, [
|
|
190
|
+
}, [web3, isInitialized]);
|
|
181
191
|
|
|
182
192
|
const getChainDataById = useCallback(async (chainId: number): Promise<ChainData | null> => {
|
|
183
|
-
if (!isInitialized || !
|
|
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
|
|
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
|
-
}, [
|
|
204
|
+
}, [web3, isInitialized]);
|
|
195
205
|
|
|
196
206
|
const getExplorerUrl = useCallback(async (chainId: number, address: string, type: 'address' | 'tx'): Promise<string> => {
|
|
197
|
-
if (!isInitialized || !
|
|
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
|
|
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
|
-
}, [
|
|
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
|
|
231
|
-
return
|
|
232
|
-
}, [
|
|
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 || !
|
|
282
|
+
if (!isInitialized || !web3) {
|
|
273
283
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
274
284
|
}
|
|
275
285
|
|
|
276
|
-
return
|
|
277
|
-
}, [
|
|
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 (!
|
|
306
|
+
if (!web3) {
|
|
297
307
|
throw new Error('SDK not initialized. Call initialize() first.');
|
|
298
308
|
}
|
|
299
|
-
return
|
|
300
|
-
}, [
|
|
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 && !!
|
|
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
|
-
*
|
|
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
|
-
*
|
|
444
|
-
* console.log(error.code);
|
|
445
|
-
* console.log(error.status);
|
|
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 {
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
//
|
|
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.
|
|
152
|
+
console.error('[ReactNativeSecureStorage] Failed to clear AsyncStorage:', e);
|
|
136
153
|
}
|
|
137
154
|
}
|
|
138
155
|
|