This document explains how session management works between the native React Native app and WebView, including authentication, cookies, and state synchronization.
Session management in the hybrid app ensures that:
- User authentication state is synchronized between native and web
- Cookies are properly shared and maintained
- User data is consistently available across both environments
- Logout events are handled properly in both contexts
// Native login flow
const handleLogin = async credentials => {
// 1. Authenticate with backend
const response = await authAPI.login(credentials);
// 2. Store user data in Redux
dispatch(setUserData(response.user));
// 3. Store authentication tokens
await AsyncStorage.setItem('authToken', response.token);
// 4. Set cookies for WebView
await setWebViewCookies(response.cookies);
// 5. Navigate to WebView
navigate(ROUTES.WEB_VIEW);
};// src/components/common/web-component/web-component.tsx
const injectCookies = async (domain: string) => {
try {
// Get cookies from native storage
const cookies1 = await CookieManager.get('https://' + domain);
const cookies2 = await CookieManager.get(Config.DOMAIN);
const cookies = {...cookies1, ...cookies2};
// Inject into Android WebView cookie store
for (const [name, v] of Object.entries(cookies)) {
let expires;
if (v.expires) {
expires = isNaN(new Date(v.expires).getTime())
? undefined
: new Date(v.expires).toISOString();
}
await CookieManager.set('https://' + domain, {
name,
value: v.value,
domain,
path: '/',
secure: true,
httpOnly: false,
...(expires ? {expires} : {}),
});
}
// Build JS string to inject into window.document
const js = Object.entries(cookies)
.map(([name, v]) => {
return `document.cookie = "${name}=${v.value}; path=/; domain=${domain}; Secure; SameSite=None";`;
})
.join('\n');
setCookieScript(js);
} catch (e) {
console.error('Error building injected cookies:', e);
}
};// src/helper/web-view-scripts.ts
export const addUserDataInWebStore = (userData: any) => {
return `(function waitForStore() {
const interval = setInterval(function () {
if (window.fpi?.store?.dispatch) {
console.log("Injecting mobile user data into Redux store");
// Dispatch to Redux store
window.fpi.store.dispatch({
type: "auth/setUserData",
payload: ${JSON.stringify({
...userData,
isUserLogin: true,
user_exists: true,
})}
});
// Set global variable for easy access
window.mobileUserData = ${JSON.stringify({
...userData,
isUserLogin: true,
user_exists: true,
})};
// Trigger custom event for other components
window.dispatchEvent(new CustomEvent('mobileUserDataLoaded', {
detail: window.mobileUserData
}));
clearInterval(interval);
}
}, 100);
})();`;
};// src/shared/hooks/use-web-view.tsx
export const useWebView = () => {
const context = useContext(WebViewContext);
const {fpi} = useFpi();
const userData = useSelector(fpi.getters.USER_DATA);
// Sync session state with WebView
const syncSessionState = useCallback(() => {
if (context.currentWebViewRef?.current && userData) {
const script = addUserDataInWebStore(userData);
context.currentWebViewRef.current.injectJavaScript(script);
}
}, [context.currentWebViewRef, userData]);
return {
...context,
syncSessionState,
};
};// src/helper/web-view-scripts.ts
export const listenToLogoutEvent = () => {
return `(function waitForFPI() {
const interval = setInterval(function () {
if (window?.FPI?.event) {
window.FPI.event.on("user.logout", (e) => {
// Send logout event to native
window?.ReactNativeWebView?.postMessage(
JSON.stringify({eventName: "user.logout", eventData: e})
);
});
clearInterval(interval);
}
}, 100);
})();`;
};// src/shared/hooks/use-webview-event-handler.tsx
const handleLogout = async () => {
try {
// 1. Clear all cookies
await CookieManager.clearAll();
// 2. Clear native storage
await AsyncStorage.multiRemove(['authToken', 'userData']);
// 3. Clear Redux state
dispatch(clearUserData());
// 4. Reset WebView state
updateCurrentPath?.('/');
setCanGoBack?.(false);
// 5. Navigate to login
navigationReset(ROUTES.LOGIN as never);
console.log('Logout completed successfully');
} catch (error) {
console.error('Error during logout:', error);
}
};// Trigger logout from native
const triggerWebViewLogout = () => {
if (currentWebViewRef?.current) {
const logoutScript = `
if (window?.FPI?.event) {
window.FPI.event.emit("user.logout", { source: "native" });
}
true;
`;
currentWebViewRef.current.injectJavaScript(logoutScript);
}
};// src/components/common/web-component/web-component.tsx
export default function WebComponent({
initialUrl,
injectJs,
loadCookies = true,
routeKey,
permissions = [],
}: WebComponentProps) {
const [cookieScript, setCookieScript] = useState('');
// Inject cookies before content loads
useEffect(() => {
if (loadCookies) {
injectCookies(webSiteDomain);
}
}, [loadCookies, webSiteDomain]);
return (
<WebView
// ... other props
injectedJavaScriptBeforeContentLoaded={
cookieScript + nativeNavigation() + getWebViewLocation()
}
sharedCookiesEnabled={true}
thirdPartyCookiesEnabled={true}
/>
);
}// Sync cookies from WebView to native
const syncCookiesFromWebView = async () => {
try {
const script = `
const cookies = document.cookie.split(';').map(cookie => {
const [name, value] = cookie.trim().split('=');
return { name, value };
});
window.ReactNativeWebView.postMessage(JSON.stringify({
eventName: 'cookiesUpdate',
eventData: cookies
}));
true;
`;
currentWebViewRef?.current?.injectJavaScript(script);
} catch (error) {
console.error('Error syncing cookies:', error);
}
};
// Handle cookie updates from WebView
const handleCookieUpdate = async (cookies: any[]) => {
try {
for (const cookie of cookies) {
await CookieManager.set(Config.DOMAIN, {
name: cookie.name,
value: cookie.value,
domain: Config.DOMAIN,
path: '/',
secure: true,
httpOnly: false,
});
}
} catch (error) {
console.error('Error updating cookies:', error);
}
};// Sync user data when WebView becomes active
useEffect(() => {
const unsubscribe = navigation.addListener('focus', () => {
setIsFocused(true);
setCurrentWebViewRef(routeKey);
// Sync user data if available
if (userData && Object.keys(userData).length > 0) {
const syncScript = addUserDataInWebStore(userData);
webViewRef.current?.injectJavaScript(syncScript);
}
});
return unsubscribe;
}, [navigation, userData]);// Monitor authentication state changes
useEffect(() => {
if (userData && isFocused) {
// User is logged in and WebView is focused
const authScript = `
if (window.fpi?.store) {
window.fpi.store.dispatch({
type: "auth/setUserData",
payload: ${JSON.stringify(userData)}
});
}
true;
`;
currentWebViewRef?.current?.injectJavaScript(authScript);
} else if (!userData && isFocused) {
// User is logged out and WebView is focused
const logoutScript = `
if (window.fpi?.store) {
window.fpi.store.dispatch({
type: "auth/clearUserData"
});
}
true;
`;
currentWebViewRef?.current?.injectJavaScript(logoutScript);
}
}, [userData, isFocused]);const setSecureCookies = async (cookies: any[]) => {
for (const cookie of cookies) {
await CookieManager.set(Config.DOMAIN, {
name: cookie.name,
value: cookie.value,
domain: Config.DOMAIN,
path: '/',
secure: true, // HTTPS only
httpOnly: false, // Allow JavaScript access
sameSite: 'None', // Cross-site requests
expires: cookie.expires,
});
}
};// Store and retrieve authentication tokens
const storeAuthToken = async (token: string) => {
try {
await AsyncStorage.setItem('authToken', token);
// Also store in secure storage if available
if (Platform.OS === 'ios') {
await Keychain.setInternetCredentials('authToken', 'user', token);
}
} catch (error) {
console.error('Error storing auth token:', error);
}
};
const getAuthToken = async () => {
try {
let token = await AsyncStorage.getItem('authToken');
if (!token && Platform.OS === 'ios') {
const credentials = await Keychain.getInternetCredentials('authToken');
token = credentials.password;
}
return token;
} catch (error) {
console.error('Error retrieving auth token:', error);
return null;
}
};const logSessionState = () => {
console.log('=== Session State Debug ===');
console.log('User Data:', userData);
console.log('Auth Token:', getAuthToken());
console.log('WebView Focused:', isFocused);
console.log('Current WebView Ref:', !!currentWebViewRef?.current);
console.log('Cookies:', CookieManager.get(Config.DOMAIN));
};
// Add to WebView component
useEffect(() => {
if (__DEV__) {
logSessionState();
}
}, [userData, isFocused]);const debugWebViewSession = () => {
const debugScript = `
console.log('=== WebView Session Debug ===');
console.log('Mobile User Data:', window.mobileUserData);
console.log('FPI Store State:', window.fpi?.store?.getState());
console.log('Cookies:', document.cookie);
console.log('Local Storage:', localStorage);
true;
`;
currentWebViewRef?.current?.injectJavaScript(debugScript);
};- Always inject cookies before content loads
- Sync user data when WebView becomes focused
- Handle authentication state changes properly
- Implement fallback mechanisms for failed authentication
- Handle network errors gracefully
- Provide user feedback for session issues
- Minimize cookie injection overhead
- Use efficient state synchronization
- Implement proper cleanup on logout
- Use secure cookie settings
- Implement proper token storage
- Validate all session data
- Cookies not persisting: Check domain and path settings
- User data not syncing: Verify WebView focus state
- Logout not working: Check event listeners and handlers
- Authentication state mismatch: Verify data injection timing
// Check session state
logSessionState();
// Debug WebView session
debugWebViewSession();
// Force session sync
syncSessionState();
// Clear all session data
await handleLogout();This comprehensive session management guide ensures proper authentication and state synchronization between native and WebView components.