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;
};