import React, { createContext, useContext, useState, ReactNode, useCallback, useRef, useEffect, useMemo } from 'react'; import { Platform, AppState, AppStateStatus } from 'react-native'; import { PersSDK, PersConfig, DefaultAuthProvider } from '@explorins/pers-sdk/core'; import { ReactNativeHttpClient } from './react-native-http-client'; import { createReactNativeAuthProvider } from './react-native-auth-provider'; import { ReactNativeDPoPProvider } from './rn-dpop-provider'; import { UserDTO, AdminDTO } from '@explorins/pers-shared'; import { detectReactNativePlatform } from '../utils/platform-detection'; // Import manager types for TypeScript import type { AuthManager, UserManager, TokenManager, BusinessManager, CampaignManager, RedemptionManager, TransactionManager, PurchaseManager, TenantManager, AnalyticsManager, DonationManager } from '@explorins/pers-sdk/core'; // Re-export PersConfig for external use export type { PersConfig } from '@explorins/pers-sdk/core'; /** * Context interface for PERS SDK React Native integration * * ## Session Restoration Behavior * * Session restoration is **asynchronous**. When the app starts: * 1. `isInitialized` becomes `true` when SDK is ready * 2. Session restoration happens automatically * 3. `isAuthenticated` and `user` update when restoration completes * * **Always check `isInitialized` AND `isAuthenticated` before using `user`:** * * ```tsx * const { user, isInitialized, isAuthenticated } = usePersSDK(); * * if (!isInitialized) return ; * if (!isAuthenticated || !user) return ; * return ; * ``` * * **Why `user` might be null on app restart:** * - Access tokens expire after 1 hour (auto-refreshed if app is active) * - Refresh tokens expire after 30 days (user must re-login) * - Session restoration is in progress * * Listen for auth events to debug: * ```typescript * sdk.events.subscribe((event) => { * console.log('[Auth Event]', event.type, event.details); * }, { domains: ['authentication'], replay: true }); * ``` * * | Event | Meaning | * |-------|---------| * | `session_restored` | Tokens valid, user logged in | * | `session_restoration_failed` | Tokens expired, needs re-login | * * @property sdk - Main SDK instance (null until initialized) * @property authProvider - Platform-specific auth provider * @property isInitialized - Whether SDK has been initialized (not whether user is authenticated!) * @property isAuthenticated - Whether user is currently authenticated * @property user - Current user data (null if not authenticated OR restoration in progress) */ export interface PersSDKContext { // Main SDK instance sdk: PersSDK | null; // Platform-specific providers authProvider: DefaultAuthProvider | null; // State isInitialized: boolean; isAuthenticated: boolean; user: UserDTO | AdminDTO | null; // Methods initialize: (config: PersConfig) => Promise; setAuthenticationState: (user: UserDTO | AdminDTO | null, isAuthenticated: boolean) => void; refreshUserData: () => Promise; restoreSession: () => Promise; } // Create the context const SDKContext = createContext(null); /** * PERS SDK Provider for React Native * * Wraps your app to provide SDK context to all child components. * Handles platform-specific initialization (DPoP, storage, etc.). * * @param config - SDK configuration (see PersConfig) * @param config.apiProjectKey - Your PERS project key (required) * @param config.environment - 'staging' | 'production' (default: 'staging') * @param config.captureWalletEvents - Enable real-time blockchain events (default: true) * @param config.dpop - DPoP configuration for enhanced security * * @example Basic usage * ```tsx * * * * ``` * * @example Disable wallet events * ```tsx * * * * ``` */ export const PersSDKProvider: React.FC<{ children: ReactNode; config?: PersConfig; }> = ({ children, config }) => { const initializingRef = useRef(false); const eventUnsubscribeRef = useRef<(() => void) | null>(null); // State refs for stable functions to read current values const sdkRef = useRef(null); const isInitializedRef = useRef(false); const isAuthenticatedRef = useRef(false); const [sdk, setSdk] = useState(null); const [authProvider, setAuthProvider] = useState(null); const [isInitialized, setIsInitialized] = useState(false); const [isAuthenticated, setIsAuthenticated] = useState(false); const [user, setUser] = useState(null); // Keep state refs in sync immediately (not in useEffect to avoid race conditions) sdkRef.current = sdk; isInitializedRef.current = isInitialized; isAuthenticatedRef.current = isAuthenticated; const initialize = useCallback(async (config: PersConfig) => { // Prevent multiple initializations if (isInitialized || initializingRef.current) { return; } initializingRef.current = true; try { // Create HTTP client const httpClient = new ReactNativeHttpClient(); const dpopConfig = config.dpop ?? {}; const isDpopEnabled = dpopConfig.enabled ?? true; // Prepare DPoP settings for Auth Provider const dpopSettings = { enabled: isDpopEnabled, cryptoProvider: dpopConfig.cryptoProvider }; // Inject Native DPoP Provider (crypto-based) for mobile platforms if not already provided if (Platform.OS !== 'web' && isDpopEnabled && !dpopSettings.cryptoProvider) { dpopSettings.cryptoProvider = new ReactNativeDPoPProvider(); } // Create platform-aware auth provider if not provided let authProvider: DefaultAuthProvider; if (config.authProvider) { authProvider = config.authProvider as DefaultAuthProvider; } else { // Use our platform-aware auth provider that automatically selects LocalStorage (web) or AsyncStorage (mobile) authProvider = createReactNativeAuthProvider(config.apiProjectKey || 'default-project', { debug: false, dpop: dpopSettings }); } // Enhanced config with platform-appropriate auth provider const sdkConfig: PersConfig = { ...config, authProvider, dpop: dpopSettings }; // Initialize PersSDK with platform-aware auth provider const sdkInstance = new PersSDK(httpClient, sdkConfig); // Auto-configure data source and platform for React Native // Native apps MUST set channel as backend cannot auto-detect without browser User-Agent if (Platform.OS !== 'web') { sdkInstance.setDataSource({ channel: 'mobile', source: config.dataSource?.source || 'app', medium: config.dataSource?.medium, campaign: config.dataSource?.campaign, }); // Auto-detect platform info asynchronously detectReactNativePlatform().then(platformInfo => { sdkInstance.setPlatform(platformInfo); }).catch(error => { console.debug('[PersSDK] Platform detection failed:', error); }); } // Subscribe to auth events BEFORE exposing SDK to consumers // This ensures no events are missed due to React timing eventUnsubscribeRef.current = sdkInstance.events.subscribe((event) => { if (event.type === 'session_restored' || event.type === 'login_success') { const details = event.details as { user?: UserDTO | AdminDTO }; if (details?.user) { setUser(details.user); setIsAuthenticated(true); } } if (event.type === 'logout_success' || event.type === 'session_restoration_failed') { setUser(null); setIsAuthenticated(false); } }, { domains: ['authentication'], replay: true }); setAuthProvider(authProvider); setSdk(sdkInstance); setIsInitialized(true); } catch (error) { console.error('Failed to initialize PERS SDK:', error); initializingRef.current = false; throw error; } finally { initializingRef.current = false; } }, [isInitialized]); // Cleanup subscription on unmount useEffect(() => { return () => { if (eventUnsubscribeRef.current) { eventUnsubscribeRef.current(); eventUnsubscribeRef.current = null; } }; }, []); const setAuthenticationState = useCallback((user: UserDTO | AdminDTO | null, isAuthenticated: boolean) => { setUser(user); setIsAuthenticated(isAuthenticated); }, []); // Auto-initialize if config is provided useEffect(() => { if (config && !isInitialized && !initializingRef.current) { initialize(config).catch(err => { console.error('Auto-initialization failed:', err); }); } }, [config, isInitialized, initialize]); const refreshUserData = useCallback(async (): Promise => { // Read from refs to get current values const currentSdk = sdkRef.current; const currentIsInitialized = isInitializedRef.current; if (!currentSdk || !currentIsInitialized) { throw new Error('SDK not initialized. Cannot refresh user data.'); } try { const freshUserData = await currentSdk.users.getCurrentUser(); setUser(freshUserData); } catch (error) { console.error('Failed to refresh user data:', error); throw error; } }, []); // No dependencies - reads from refs const restoreSession = useCallback(async (): Promise => { // Read from refs to get current values const currentSdk = sdkRef.current; const currentIsInitialized = isInitializedRef.current; if (!currentSdk || !currentIsInitialized) { throw new Error('SDK not initialized. Call initialize() first.'); } try { const userData = await currentSdk.restoreSession(); if (userData) { setAuthenticationState(userData, true); } return userData; } catch (error) { console.error('[PersSDK] Failed to restore session:', error); throw error; } }, [setAuthenticationState]); // iOS/Android: Monitor app state and validate tokens when app becomes active useEffect(() => { if (!sdk || Platform.OS === 'web') { return; } const handleAppStateChange = async (nextAppState: AppStateStatus) => { if (nextAppState === 'active' && isAuthenticated) { try { // Trigger token validation when app resumes from background // This ensures tokens are checked after extended inactivity await sdk.auth.ensureValidToken(); } catch (error) { console.warn('[PersSDK] Token validation failed on app resume:', error); } } }; const subscription = AppState.addEventListener('change', handleAppStateChange); return () => { subscription?.remove(); }; }, [sdk, isAuthenticated]); const contextValue: PersSDKContext = useMemo(() => ({ // Main SDK instance sdk, // Platform-specific providers authProvider, // State isInitialized, isAuthenticated, user, // Methods - expose functions directly, not through refs initialize, setAuthenticationState, refreshUserData, restoreSession, }), [ sdk, authProvider, isInitialized, isAuthenticated, user, initialize, setAuthenticationState, refreshUserData, restoreSession ]); return ( {children} ); }; // Custom hook to use the SDK context export const usePersSDK = (): PersSDKContext => { const context = useContext(SDKContext); if (!context) { throw new Error('usePersSDK must be used within a PersSDKProvider'); } return context; };