@explorins/pers-sdk-react-native 2.3.6 → 2.3.8

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.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1266 -1244
  3. package/dist/__tests__/PersSDKProvider.test.js +33 -0
  4. package/dist/hooks/useBusiness.d.ts +2 -2
  5. package/dist/hooks/useBusiness.d.ts.map +1 -1
  6. package/dist/hooks/useBusiness.js +2 -2
  7. package/dist/hooks/useIPFSUrl.d.ts +3 -3
  8. package/dist/hooks/useIPFSUrl.d.ts.map +1 -1
  9. package/dist/hooks/useIPFSUrl.js +7 -7
  10. package/dist/hooks/useTransactions.d.ts.map +1 -1
  11. package/dist/hooks/useTransactions.js +13 -5
  12. package/dist/hooks/useWeb3.js +4 -4
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2412 -492
  16. package/dist/index.js.map +1 -1
  17. package/dist/providers/PersSDKProvider.d.ts +4 -0
  18. package/dist/providers/PersSDKProvider.d.ts.map +1 -1
  19. package/dist/providers/PersSDKProvider.js +14 -0
  20. package/dist/providers/react-native-http-client.d.ts +2 -0
  21. package/dist/providers/react-native-http-client.d.ts.map +1 -1
  22. package/dist/providers/react-native-http-client.js +5 -2
  23. package/dist/storage/rn-secure-storage.d.ts.map +1 -1
  24. package/dist/storage/rn-secure-storage.js +21 -8
  25. package/package.json +3 -3
  26. package/src/__tests__/PersSDKProvider.test.tsx +484 -420
  27. package/src/__tests__/setup.ts +40 -40
  28. package/src/hooks/index.ts +60 -60
  29. package/src/hooks/useAnalytics.ts +327 -327
  30. package/src/hooks/useAuth.ts +265 -265
  31. package/src/hooks/useBookings.ts +191 -191
  32. package/src/hooks/useBusiness.ts +251 -251
  33. package/src/hooks/useCampaigns.ts +500 -500
  34. package/src/hooks/useDonations.ts +66 -66
  35. package/src/hooks/useEvents.ts +372 -372
  36. package/src/hooks/useFiles.ts +155 -155
  37. package/src/hooks/useIPFSUrl.ts +192 -192
  38. package/src/hooks/usePurchases.ts +65 -65
  39. package/src/hooks/useRedemptions.ts +467 -467
  40. package/src/hooks/useTenants.ts +72 -72
  41. package/src/hooks/useTokenBalances.ts +385 -385
  42. package/src/hooks/useTokens.ts +313 -313
  43. package/src/hooks/useTransactionSigner.ts +611 -611
  44. package/src/hooks/useTransactions.ts +299 -290
  45. package/src/hooks/useTriggerSources.ts +301 -301
  46. package/src/hooks/useUserStatus.ts +61 -61
  47. package/src/hooks/useUsers.ts +167 -167
  48. package/src/hooks/useWeb3.ts +355 -355
  49. package/src/index.ts +777 -774
  50. package/src/polyfills/index.ts +160 -160
  51. package/src/polyfills/web3-polyfills.ts +97 -97
  52. package/src/providers/PersSDKProvider.tsx +393 -374
  53. package/src/providers/react-native-auth-provider.ts +110 -110
  54. package/src/providers/react-native-http-client.ts +150 -145
  55. package/src/providers/rn-dpop-provider.ts +190 -190
  56. package/src/storage/async-storage-token-storage.ts +185 -185
  57. package/src/storage/rn-secure-storage.ts +182 -170
  58. package/src/types/react-native-globals.d.ts +45 -45
  59. package/src/utils/index.ts +8 -8
  60. package/src/utils/platform-detection.ts +86 -86
  61. package/dist/index-CzLxqePZ.js +0 -7717
  62. package/dist/index-CzLxqePZ.js.map +0 -1
@@ -1,375 +1,394 @@
1
- import React, { createContext, useContext, useState, ReactNode, useCallback, useRef, useEffect, useMemo } from 'react';
2
- import { Platform, AppState, AppStateStatus } from 'react-native';
3
- import { PersSDK, PersConfig, DefaultAuthProvider } from '@explorins/pers-sdk/core';
4
- import { ReactNativeHttpClient } from './react-native-http-client';
5
- import { createReactNativeAuthProvider } from './react-native-auth-provider';
6
- import { ReactNativeDPoPProvider } from './rn-dpop-provider';
7
- import { UserDTO, AdminDTO } from '@explorins/pers-shared';
8
- import { detectReactNativePlatform } from '../utils/platform-detection';
9
-
10
- // Import manager types for TypeScript
11
- import type {
12
- AuthManager,
13
- UserManager,
14
- TokenManager,
15
- BusinessManager,
16
- CampaignManager,
17
- RedemptionManager,
18
- TransactionManager,
19
- PurchaseManager,
20
- TenantManager,
21
- AnalyticsManager,
22
- DonationManager
23
- } from '@explorins/pers-sdk/core';
24
-
25
- // Re-export PersConfig for external use
26
- export type { PersConfig } from '@explorins/pers-sdk/core';
27
-
28
- /**
29
- * Context interface for PERS SDK React Native integration
30
- *
31
- * ## Session Restoration Behavior
32
- *
33
- * Session restoration is **asynchronous**. When the app starts:
34
- * 1. `isInitialized` becomes `true` when SDK is ready
35
- * 2. Session restoration happens automatically
36
- * 3. `isAuthenticated` and `user` update when restoration completes
37
- *
38
- * **Always check `isInitialized` AND `isAuthenticated` before using `user`:**
39
- *
40
- * ```tsx
41
- * const { user, isInitialized, isAuthenticated } = usePersSDK();
42
- *
43
- * if (!isInitialized) return <LoadingSpinner />;
44
- * if (!isAuthenticated || !user) return <LoginScreen />;
45
- * return <WalletContent user={user} />;
46
- * ```
47
- *
48
- * **Why `user` might be null on app restart:**
49
- * - Access tokens expire after 1 hour (auto-refreshed if app is active)
50
- * - Refresh tokens expire after 30 days (user must re-login)
51
- * - Session restoration is in progress
52
- *
53
- * Listen for auth events to debug:
54
- * ```typescript
55
- * sdk.events.subscribe((event) => {
56
- * console.log('[Auth Event]', event.type, event.details);
57
- * }, { domains: ['authentication'], replay: true });
58
- * ```
59
- *
60
- * | Event | Meaning |
61
- * |-------|---------|
62
- * | `session_restored` | Tokens valid, user logged in |
63
- * | `session_restoration_failed` | Tokens expired, needs re-login |
64
- *
65
- * @property sdk - Main SDK instance (null until initialized)
66
- * @property authProvider - Platform-specific auth provider
67
- * @property isInitialized - Whether SDK has been initialized (not whether user is authenticated!)
68
- * @property isAuthenticated - Whether user is currently authenticated
69
- * @property user - Current user data (null if not authenticated OR restoration in progress)
70
- */
71
- export interface PersSDKContext {
72
- // Main SDK instance
73
- sdk: PersSDK | null;
74
-
75
- // Platform-specific providers
76
- authProvider: DefaultAuthProvider | null;
77
-
78
- // State
79
- isInitialized: boolean;
80
- isAuthenticated: boolean;
81
- user: UserDTO | AdminDTO | null;
82
-
83
- // Methods
84
- initialize: (config: PersConfig) => Promise<void>;
85
- setAuthenticationState: (user: UserDTO | AdminDTO | null, isAuthenticated: boolean) => void;
86
- refreshUserData: () => Promise<void>;
87
- restoreSession: () => Promise<UserDTO | null>;
88
- }
89
-
90
- // Create the context
91
- const SDKContext = createContext<PersSDKContext | null>(null);
92
-
93
- /**
94
- * PERS SDK Provider for React Native
95
- *
96
- * Wraps your app to provide SDK context to all child components.
97
- * Handles platform-specific initialization (DPoP, storage, etc.).
98
- *
99
- * @param config - SDK configuration (see PersConfig)
100
- * @param config.apiProjectKey - Your PERS project key (required)
101
- * @param config.environment - 'staging' | 'production' (default: 'staging')
102
- * @param config.captureWalletEvents - Enable real-time blockchain events (default: true)
103
- * @param config.dpop - DPoP configuration for enhanced security
104
- *
105
- * @example Basic usage
106
- * ```tsx
107
- * <PersSDKProvider config={{ apiProjectKey: 'my-project' }}>
108
- * <App />
109
- * </PersSDKProvider>
110
- * ```
111
- *
112
- * @example Disable wallet events
113
- * ```tsx
114
- * <PersSDKProvider config={{
115
- * apiProjectKey: 'my-project',
116
- * captureWalletEvents: false // Disable auto-connect to blockchain events
117
- * }}>
118
- * <App />
119
- * </PersSDKProvider>
120
- * ```
121
- */
122
- export const PersSDKProvider: React.FC<{
123
- children: ReactNode;
124
- config?: PersConfig;
125
- }> = ({ children, config }) => {
126
- const initializingRef = useRef(false);
127
- const eventUnsubscribeRef = useRef<(() => void) | null>(null);
128
-
129
- // State refs for stable functions to read current values
130
- const sdkRef = useRef<PersSDK | null>(null);
131
- const isInitializedRef = useRef(false);
132
- const isAuthenticatedRef = useRef(false);
133
-
134
- const [sdk, setSdk] = useState<PersSDK | null>(null);
135
- const [authProvider, setAuthProvider] = useState<DefaultAuthProvider | null>(null);
136
-
137
- const [isInitialized, setIsInitialized] = useState(false);
138
- const [isAuthenticated, setIsAuthenticated] = useState(false);
139
- const [user, setUser] = useState<UserDTO | AdminDTO | null>(null);
140
-
141
- // Keep state refs in sync immediately (not in useEffect to avoid race conditions)
142
- sdkRef.current = sdk;
143
- isInitializedRef.current = isInitialized;
144
- isAuthenticatedRef.current = isAuthenticated;
145
-
146
- const initialize = useCallback(async (config: PersConfig) => {
147
- // Prevent multiple initializations
148
- if (isInitialized || initializingRef.current) {
149
- return;
150
- }
151
-
152
- initializingRef.current = true;
153
-
154
- try {
155
- // Create HTTP client
156
- const httpClient = new ReactNativeHttpClient();
157
-
158
- const dpopConfig = config.dpop ?? {};
159
-
160
- const isDpopEnabled = dpopConfig.enabled ?? true;
161
-
162
- // Prepare DPoP settings for Auth Provider
163
- const dpopSettings = {
164
- enabled: isDpopEnabled,
165
- cryptoProvider: dpopConfig.cryptoProvider
166
- };
167
-
168
- // Inject Native DPoP Provider (crypto-based) for mobile platforms if not already provided
169
- if (Platform.OS !== 'web' && isDpopEnabled && !dpopSettings.cryptoProvider) {
170
- dpopSettings.cryptoProvider = new ReactNativeDPoPProvider();
171
- }
172
-
173
- // Create platform-aware auth provider if not provided
174
- let authProvider: DefaultAuthProvider;
175
- if (config.authProvider) {
176
- authProvider = config.authProvider as DefaultAuthProvider;
177
- } else {
178
- // Use our platform-aware auth provider that automatically selects LocalStorage (web) or AsyncStorage (mobile)
179
- authProvider = createReactNativeAuthProvider(config.apiProjectKey || 'default-project', {
180
- debug: false,
181
- dpop: dpopSettings
182
- });
183
- }
184
-
185
- // Enhanced config with platform-appropriate auth provider
186
- const sdkConfig: PersConfig = {
187
- ...config,
188
- authProvider,
189
- dpop: dpopSettings
190
- };
191
-
192
- // Initialize PersSDK with platform-aware auth provider
193
- const sdkInstance = new PersSDK(httpClient, sdkConfig);
194
-
195
- // Auto-configure data source and platform for React Native
196
- // Native apps MUST set channel as backend cannot auto-detect without browser User-Agent
197
- if (Platform.OS !== 'web') {
198
- sdkInstance.setDataSource({
199
- channel: 'mobile',
200
- source: config.dataSource?.source || 'app',
201
- medium: config.dataSource?.medium,
202
- campaign: config.dataSource?.campaign,
203
- });
204
-
205
- // Auto-detect platform info asynchronously
206
- detectReactNativePlatform().then(platformInfo => {
207
- sdkInstance.setPlatform(platformInfo);
208
- }).catch(error => {
209
- console.debug('[PersSDK] Platform detection failed:', error);
210
- });
211
- }
212
-
213
- // Subscribe to auth events BEFORE exposing SDK to consumers
214
- // This ensures no events are missed due to React timing
215
- eventUnsubscribeRef.current = sdkInstance.events.subscribe((event) => {
216
- if (event.type === 'session_restored' || event.type === 'login_success') {
217
- const details = event.details as { user?: UserDTO | AdminDTO };
218
- if (details?.user) {
219
- setUser(details.user);
220
- setIsAuthenticated(true);
221
- }
222
- }
223
-
224
- if (event.type === 'logout_success' || event.type === 'session_restoration_failed') {
225
- setUser(null);
226
- setIsAuthenticated(false);
227
- }
228
- }, { domains: ['authentication'], replay: true });
229
-
230
- setAuthProvider(authProvider);
231
- setSdk(sdkInstance);
232
- setIsInitialized(true);
233
- } catch (error) {
234
- console.error('Failed to initialize PERS SDK:', error);
235
- initializingRef.current = false;
236
- throw error;
237
- } finally {
238
- initializingRef.current = false;
239
- }
240
- }, [isInitialized]);
241
-
242
- // Cleanup subscription on unmount
243
- useEffect(() => {
244
- return () => {
245
- if (eventUnsubscribeRef.current) {
246
- eventUnsubscribeRef.current();
247
- eventUnsubscribeRef.current = null;
248
- }
249
- };
250
- }, []);
251
-
252
- const setAuthenticationState = useCallback((user: UserDTO | AdminDTO | null, isAuthenticated: boolean) => {
253
- setUser(user);
254
- setIsAuthenticated(isAuthenticated);
255
- }, []);
256
-
257
- // Auto-initialize if config is provided
258
- useEffect(() => {
259
- if (config && !isInitialized && !initializingRef.current) {
260
- initialize(config).catch(err => {
261
- console.error('Auto-initialization failed:', err);
262
- });
263
- }
264
- }, [config, isInitialized, initialize]);
265
-
266
- const refreshUserData = useCallback(async (): Promise<void> => {
267
- // Read from refs to get current values
268
- const currentSdk = sdkRef.current;
269
- const currentIsInitialized = isInitializedRef.current;
270
-
271
- if (!currentSdk || !currentIsInitialized) {
272
- throw new Error('SDK not initialized. Cannot refresh user data.');
273
- }
274
-
275
- try {
276
- const freshUserData = await currentSdk.users.getCurrentUser();
277
- setUser(freshUserData);
278
- } catch (error) {
279
- console.error('Failed to refresh user data:', error);
280
- throw error;
281
- }
282
- }, []); // No dependencies - reads from refs
283
-
284
- const restoreSession = useCallback(async (): Promise<UserDTO | null> => {
285
- // Read from refs to get current values
286
- const currentSdk = sdkRef.current;
287
- const currentIsInitialized = isInitializedRef.current;
288
-
289
- if (!currentSdk || !currentIsInitialized) {
290
- throw new Error('SDK not initialized. Call initialize() first.');
291
- }
292
-
293
- try {
294
- const userData = await currentSdk.restoreSession();
295
- if (userData) {
296
- setAuthenticationState(userData, true);
297
- }
298
- return userData;
299
- } catch (error) {
300
- console.error('[PersSDK] Failed to restore session:', error);
301
- throw error;
302
- }
303
- }, [setAuthenticationState]);
304
-
305
- // iOS/Android: Monitor app state and validate tokens when app becomes active
306
- useEffect(() => {
307
- if (!sdk || Platform.OS === 'web') {
308
- return;
309
- }
310
-
311
- const handleAppStateChange = async (nextAppState: AppStateStatus) => {
312
- if (nextAppState === 'active' && isAuthenticated) {
313
- try {
314
- // Trigger token validation when app resumes from background
315
- // This ensures tokens are checked after extended inactivity
316
- await sdk.auth.ensureValidToken();
317
- } catch (error) {
318
- console.warn('[PersSDK] Token validation failed on app resume:', error);
319
- }
320
- }
321
- };
322
-
323
- const subscription = AppState.addEventListener('change', handleAppStateChange);
324
-
325
- return () => {
326
- subscription?.remove();
327
- };
328
- }, [sdk, isAuthenticated]);
329
-
330
- const contextValue: PersSDKContext = useMemo(() => ({
331
- // Main SDK instance
332
- sdk,
333
-
334
- // Platform-specific providers
335
- authProvider,
336
-
337
- // State
338
- isInitialized,
339
- isAuthenticated,
340
- user,
341
-
342
- // Methods - expose functions directly, not through refs
343
- initialize,
344
- setAuthenticationState,
345
- refreshUserData,
346
- restoreSession,
347
- }), [
348
- sdk,
349
- authProvider,
350
- isInitialized,
351
- isAuthenticated,
352
- user,
353
- initialize,
354
- setAuthenticationState,
355
- refreshUserData,
356
- restoreSession
357
- ]);
358
-
359
- return (
360
- <SDKContext.Provider value={contextValue}>
361
- {children}
362
- </SDKContext.Provider>
363
- );
364
- };
365
-
366
- // Custom hook to use the SDK context
367
- export const usePersSDK = (): PersSDKContext => {
368
- const context = useContext(SDKContext);
369
-
370
- if (!context) {
371
- throw new Error('usePersSDK must be used within a PersSDKProvider');
372
- }
373
-
374
- return context;
1
+ import React, { createContext, useContext, useState, ReactNode, useCallback, useRef, useEffect, useMemo } from 'react';
2
+ import { Platform, AppState, AppStateStatus } from 'react-native';
3
+ import { PersSDK, PersConfig, DefaultAuthProvider } from '@explorins/pers-sdk/core';
4
+ import { ReactNativeHttpClient } from './react-native-http-client';
5
+ import { createReactNativeAuthProvider } from './react-native-auth-provider';
6
+ import { ReactNativeDPoPProvider } from './rn-dpop-provider';
7
+ import { UserDTO, AdminDTO } from '@explorins/pers-shared';
8
+ import { detectReactNativePlatform } from '../utils/platform-detection';
9
+
10
+ // Import manager types for TypeScript
11
+ import type {
12
+ AuthManager,
13
+ UserManager,
14
+ TokenManager,
15
+ BusinessManager,
16
+ CampaignManager,
17
+ RedemptionManager,
18
+ TransactionManager,
19
+ PurchaseManager,
20
+ TenantManager,
21
+ AnalyticsManager,
22
+ DonationManager
23
+ } from '@explorins/pers-sdk/core';
24
+
25
+ // Re-export PersConfig for external use
26
+ export type { PersConfig } from '@explorins/pers-sdk/core';
27
+
28
+ /**
29
+ * Context interface for PERS SDK React Native integration
30
+ *
31
+ * ## Session Restoration Behavior
32
+ *
33
+ * Session restoration is **asynchronous**. When the app starts:
34
+ * 1. `isInitialized` becomes `true` when SDK is ready
35
+ * 2. Session restoration happens automatically
36
+ * 3. `isAuthenticated` and `user` update when restoration completes
37
+ *
38
+ * **Always check `isInitialized` AND `isAuthenticated` before using `user`:**
39
+ *
40
+ * ```tsx
41
+ * const { user, isInitialized, isAuthenticated } = usePersSDK();
42
+ *
43
+ * if (!isInitialized) return <LoadingSpinner />;
44
+ * if (!isAuthenticated || !user) return <LoginScreen />;
45
+ * return <WalletContent user={user} />;
46
+ * ```
47
+ *
48
+ * **Why `user` might be null on app restart:**
49
+ * - Access tokens expire after 1 hour (auto-refreshed if app is active)
50
+ * - Refresh tokens expire after 30 days (user must re-login)
51
+ * - Session restoration is in progress
52
+ *
53
+ * Listen for auth events to debug:
54
+ * ```typescript
55
+ * sdk.events.subscribe((event) => {
56
+ * console.log('[Auth Event]', event.type, event.details);
57
+ * }, { domains: ['authentication'], replay: true });
58
+ * ```
59
+ *
60
+ * | Event | Meaning |
61
+ * |-------|---------|
62
+ * | `session_restored` | Tokens valid, user logged in |
63
+ * | `session_restoration_failed` | Tokens expired, needs re-login |
64
+ *
65
+ * @property sdk - Main SDK instance (null until initialized)
66
+ * @property authProvider - Platform-specific auth provider
67
+ * @property isInitialized - Whether SDK has been initialized (not whether user is authenticated!)
68
+ * @property isAuthenticated - Whether user is currently authenticated
69
+ * @property user - Current user data (null if not authenticated OR restoration in progress)
70
+ */
71
+ export interface PersSDKContext {
72
+ // Main SDK instance
73
+ sdk: PersSDK | null;
74
+
75
+ // Platform-specific providers
76
+ authProvider: DefaultAuthProvider | null;
77
+
78
+ // State
79
+ isInitialized: boolean;
80
+ isAuthenticated: boolean;
81
+ user: UserDTO | AdminDTO | null;
82
+ /** Content locale in effect (normalized BCP 47 tag), or null when localization is off */
83
+ locale: string | null;
84
+
85
+ // Methods
86
+ initialize: (config: PersConfig) => Promise<void>;
87
+ /** Change the content locale (`'auto'`, a BCP 47 tag, or null). Refetch content afterwards. */
88
+ setLocale: (locale: string | null) => void;
89
+ setAuthenticationState: (user: UserDTO | AdminDTO | null, isAuthenticated: boolean) => void;
90
+ refreshUserData: () => Promise<void>;
91
+ restoreSession: () => Promise<UserDTO | null>;
92
+ }
93
+
94
+ // Create the context
95
+ const SDKContext = createContext<PersSDKContext | null>(null);
96
+
97
+ /**
98
+ * PERS SDK Provider for React Native
99
+ *
100
+ * Wraps your app to provide SDK context to all child components.
101
+ * Handles platform-specific initialization (DPoP, storage, etc.).
102
+ *
103
+ * @param config - SDK configuration (see PersConfig)
104
+ * @param config.apiProjectKey - Your PERS project key (required)
105
+ * @param config.environment - 'staging' | 'production' (default: 'staging')
106
+ * @param config.captureWalletEvents - Enable real-time blockchain events (default: true)
107
+ * @param config.dpop - DPoP configuration for enhanced security
108
+ *
109
+ * @example Basic usage
110
+ * ```tsx
111
+ * <PersSDKProvider config={{ apiProjectKey: 'my-project' }}>
112
+ * <App />
113
+ * </PersSDKProvider>
114
+ * ```
115
+ *
116
+ * @example Disable wallet events
117
+ * ```tsx
118
+ * <PersSDKProvider config={{
119
+ * apiProjectKey: 'my-project',
120
+ * captureWalletEvents: false // Disable auto-connect to blockchain events
121
+ * }}>
122
+ * <App />
123
+ * </PersSDKProvider>
124
+ * ```
125
+ */
126
+ export const PersSDKProvider: React.FC<{
127
+ children: ReactNode;
128
+ config?: PersConfig;
129
+ }> = ({ children, config }) => {
130
+ const initializingRef = useRef(false);
131
+ const eventUnsubscribeRef = useRef<(() => void) | null>(null);
132
+
133
+ // State refs for stable functions to read current values
134
+ const sdkRef = useRef<PersSDK | null>(null);
135
+ const isInitializedRef = useRef(false);
136
+ const isAuthenticatedRef = useRef(false);
137
+
138
+ const [sdk, setSdk] = useState<PersSDK | null>(null);
139
+ const [authProvider, setAuthProvider] = useState<DefaultAuthProvider | null>(null);
140
+
141
+ const [isInitialized, setIsInitialized] = useState(false);
142
+ const [isAuthenticated, setIsAuthenticated] = useState(false);
143
+ const [user, setUser] = useState<UserDTO | AdminDTO | null>(null);
144
+ const [locale, setLocaleState] = useState<string | null>(null);
145
+
146
+ // Keep state refs in sync immediately (not in useEffect to avoid race conditions)
147
+ sdkRef.current = sdk;
148
+ isInitializedRef.current = isInitialized;
149
+ isAuthenticatedRef.current = isAuthenticated;
150
+
151
+ const initialize = useCallback(async (config: PersConfig) => {
152
+ // Prevent multiple initializations
153
+ if (isInitialized || initializingRef.current) {
154
+ return;
155
+ }
156
+
157
+ initializingRef.current = true;
158
+
159
+ try {
160
+ // Create HTTP client
161
+ const httpClient = new ReactNativeHttpClient();
162
+
163
+ const dpopConfig = config.dpop ?? {};
164
+
165
+ const isDpopEnabled = dpopConfig.enabled ?? true;
166
+
167
+ // Prepare DPoP settings for Auth Provider
168
+ const dpopSettings = {
169
+ enabled: isDpopEnabled,
170
+ cryptoProvider: dpopConfig.cryptoProvider
171
+ };
172
+
173
+ // Inject Native DPoP Provider (crypto-based) for mobile platforms if not already provided
174
+ if (Platform.OS !== 'web' && isDpopEnabled && !dpopSettings.cryptoProvider) {
175
+ dpopSettings.cryptoProvider = new ReactNativeDPoPProvider();
176
+ }
177
+
178
+ // Create platform-aware auth provider if not provided
179
+ let authProvider: DefaultAuthProvider;
180
+ if (config.authProvider) {
181
+ authProvider = config.authProvider as DefaultAuthProvider;
182
+ } else {
183
+ // Use our platform-aware auth provider that automatically selects LocalStorage (web) or AsyncStorage (mobile)
184
+ authProvider = createReactNativeAuthProvider(config.apiProjectKey || 'default-project', {
185
+ debug: false,
186
+ dpop: dpopSettings
187
+ });
188
+ }
189
+
190
+ // Enhanced config with platform-appropriate auth provider
191
+ const sdkConfig: PersConfig = {
192
+ ...config,
193
+ authProvider,
194
+ dpop: dpopSettings
195
+ };
196
+
197
+ // Initialize PersSDK with platform-aware auth provider
198
+ const sdkInstance = new PersSDK(httpClient, sdkConfig);
199
+
200
+ // Auto-configure data source and platform for React Native
201
+ // Native apps MUST set channel as backend cannot auto-detect without browser User-Agent
202
+ if (Platform.OS !== 'web') {
203
+ sdkInstance.setDataSource({
204
+ channel: 'mobile',
205
+ source: config.dataSource?.source || 'app',
206
+ medium: config.dataSource?.medium,
207
+ campaign: config.dataSource?.campaign,
208
+ });
209
+
210
+ // Auto-detect platform info asynchronously
211
+ detectReactNativePlatform().then(platformInfo => {
212
+ sdkInstance.setPlatform(platformInfo);
213
+ }).catch(error => {
214
+ console.debug('[PersSDK] Platform detection failed:', error);
215
+ });
216
+ }
217
+
218
+ // Subscribe to auth events BEFORE exposing SDK to consumers
219
+ // This ensures no events are missed due to React timing
220
+ eventUnsubscribeRef.current = sdkInstance.events.subscribe((event) => {
221
+ if (event.type === 'session_restored' || event.type === 'login_success') {
222
+ const details = event.details as { user?: UserDTO | AdminDTO };
223
+ if (details?.user) {
224
+ setUser(details.user);
225
+ setIsAuthenticated(true);
226
+ }
227
+ }
228
+
229
+ if (event.type === 'logout_success' || event.type === 'session_restoration_failed') {
230
+ setUser(null);
231
+ setIsAuthenticated(false);
232
+ }
233
+ }, { domains: ['authentication'], replay: true });
234
+
235
+ setAuthProvider(authProvider);
236
+ setSdk(sdkInstance);
237
+ setLocaleState(sdkInstance.getLocale());
238
+ setIsInitialized(true);
239
+ } catch (error) {
240
+ console.error('Failed to initialize PERS SDK:', error);
241
+ initializingRef.current = false;
242
+ throw error;
243
+ } finally {
244
+ initializingRef.current = false;
245
+ }
246
+ }, [isInitialized]);
247
+
248
+ // Cleanup subscription on unmount
249
+ useEffect(() => {
250
+ return () => {
251
+ if (eventUnsubscribeRef.current) {
252
+ eventUnsubscribeRef.current();
253
+ eventUnsubscribeRef.current = null;
254
+ }
255
+ };
256
+ }, []);
257
+
258
+ const setAuthenticationState = useCallback((user: UserDTO | AdminDTO | null, isAuthenticated: boolean) => {
259
+ setUser(user);
260
+ setIsAuthenticated(isAuthenticated);
261
+ }, []);
262
+
263
+ // Auto-initialize if config is provided
264
+ useEffect(() => {
265
+ if (config && !isInitialized && !initializingRef.current) {
266
+ initialize(config).catch(err => {
267
+ console.error('Auto-initialization failed:', err);
268
+ });
269
+ }
270
+ }, [config, isInitialized, initialize]);
271
+
272
+ const setLocale = useCallback((next: string | null) => {
273
+ const currentSdk = sdkRef.current;
274
+ if (!currentSdk) {
275
+ throw new Error('SDK not initialized. Call initialize() first.');
276
+ }
277
+ currentSdk.setLocale(next);
278
+ setLocaleState(currentSdk.getLocale());
279
+ }, []);
280
+
281
+ const refreshUserData = useCallback(async (): Promise<void> => {
282
+ // Read from refs to get current values
283
+ const currentSdk = sdkRef.current;
284
+ const currentIsInitialized = isInitializedRef.current;
285
+
286
+ if (!currentSdk || !currentIsInitialized) {
287
+ throw new Error('SDK not initialized. Cannot refresh user data.');
288
+ }
289
+
290
+ try {
291
+ const freshUserData = await currentSdk.users.getCurrentUser();
292
+ setUser(freshUserData);
293
+ } catch (error) {
294
+ console.error('Failed to refresh user data:', error);
295
+ throw error;
296
+ }
297
+ }, []); // No dependencies - reads from refs
298
+
299
+ const restoreSession = useCallback(async (): Promise<UserDTO | null> => {
300
+ // Read from refs to get current values
301
+ const currentSdk = sdkRef.current;
302
+ const currentIsInitialized = isInitializedRef.current;
303
+
304
+ if (!currentSdk || !currentIsInitialized) {
305
+ throw new Error('SDK not initialized. Call initialize() first.');
306
+ }
307
+
308
+ try {
309
+ const userData = await currentSdk.restoreSession();
310
+ if (userData) {
311
+ setAuthenticationState(userData, true);
312
+ }
313
+ return userData;
314
+ } catch (error) {
315
+ console.error('[PersSDK] Failed to restore session:', error);
316
+ throw error;
317
+ }
318
+ }, [setAuthenticationState]);
319
+
320
+ // iOS/Android: Monitor app state and validate tokens when app becomes active
321
+ useEffect(() => {
322
+ if (!sdk || Platform.OS === 'web') {
323
+ return;
324
+ }
325
+
326
+ const handleAppStateChange = async (nextAppState: AppStateStatus) => {
327
+ if (nextAppState === 'active' && isAuthenticated) {
328
+ try {
329
+ // Trigger token validation when app resumes from background
330
+ // This ensures tokens are checked after extended inactivity
331
+ await sdk.auth.ensureValidToken();
332
+ } catch (error) {
333
+ console.warn('[PersSDK] Token validation failed on app resume:', error);
334
+ }
335
+ }
336
+ };
337
+
338
+ const subscription = AppState.addEventListener('change', handleAppStateChange);
339
+
340
+ return () => {
341
+ subscription?.remove();
342
+ };
343
+ }, [sdk, isAuthenticated]);
344
+
345
+ const contextValue: PersSDKContext = useMemo(() => ({
346
+ // Main SDK instance
347
+ sdk,
348
+
349
+ // Platform-specific providers
350
+ authProvider,
351
+
352
+ // State
353
+ isInitialized,
354
+ isAuthenticated,
355
+ user,
356
+ locale,
357
+
358
+ // Methods - expose functions directly, not through refs
359
+ initialize,
360
+ setLocale,
361
+ setAuthenticationState,
362
+ refreshUserData,
363
+ restoreSession,
364
+ }), [
365
+ sdk,
366
+ authProvider,
367
+ isInitialized,
368
+ isAuthenticated,
369
+ user,
370
+ locale,
371
+ initialize,
372
+ setLocale,
373
+ setAuthenticationState,
374
+ refreshUserData,
375
+ restoreSession
376
+ ]);
377
+
378
+ return (
379
+ <SDKContext.Provider value={contextValue}>
380
+ {children}
381
+ </SDKContext.Provider>
382
+ );
383
+ };
384
+
385
+ // Custom hook to use the SDK context
386
+ export const usePersSDK = (): PersSDKContext => {
387
+ const context = useContext(SDKContext);
388
+
389
+ if (!context) {
390
+ throw new Error('usePersSDK must be used within a PersSDKProvider');
391
+ }
392
+
393
+ return context;
375
394
  };