Cookies not being sent to webview for iOS
Bug description:
Web view should be rendering a web page that has a cookie authentication. Instead it is rendering sign in page. This is happening on iOS after I upgraded to react native 0.73+. It was working properly before the upgrade. Android is working fine.
I have both thirdPartyCookiesEnabled={true} and sharedCookiesEnabled={true} enabled. Also in handleNavigationStateChange I reset the cookies for iOS:
async function resetCookies() {
const cookies = await CookieManager.getAll(Platform.OS === 'ios');
Object.keys(cookies).forEach(key => {
const cookie = cookies[key];
if (cookie.domain === domain) {
CookieManager.clearByName(url, cookie.name, Platform.OS === 'ios');
}
});
}
I also tried to sync the cookies manually with this in the onLoad and the onNavigationStateChange of <WebView />, but it still didn't work.
const synchronizeCookiesiOS = async () => {
if (Platform.OS === 'ios') {
try {
const allCookies = await CookieManager.getAll(true);
for (const cookieKey of Object.keys(allCookies)) {
const cookie = allCookies[cookieKey];
if (cookie) {
await CookieManager.set('https://' + cookie.domain, cookie);
}
}
} catch (error) {
console.error('Error synchronizing cookies:', error);
}
}
};
Expected behavior:
Web view should render account page, not sign in page
Environment:
- OS: Android, iOS
- OS version: Android 34+, iOS 14+
- react-native version: 0.73.6
- react-native-webview version: 13.8.1
One thing that could be causing this is that you are not setting the webkit boolean to true here: await CookieManager.set('https://' + cookie.domain, cookie); In resetCookies function you are reading those cookies from WebKit WKHTTPCookieStore storage. Also, you should turn sharedCookiesEnabled off for ios when using WebKit.
Thanks @karel-suchomel-ed! That fixed it for the initial Webview load, but when I leave the webview and then return to it, the cookies are not being synced and I am back to the sign in screen.
Any ideas on why re-entering the Webview, the cookies don't work?
<WebView
style={styles.webview}
source={{ uri: url }}
javaScriptEnabled
domStorageEnabled
startInLoadingState={false}
injectedJavaScript={script}
onLoad={synchronizeCookiesiOS}
onMessage={() => {
// nothing
}}
onNavigationStateChange={handleNavigationStateChange}
thirdPartyCookiesEnabled={true}
/>
I think that for your use case you could use something similar that I used for sharing sessions between RN and WebView. In our app we had a PHPSESSID cookie encoded in a JWT token and this worked for me (not 1:1 code):
const Example = () => {
...
const [isReady, setIsReady] = useState(false)
useFocusEffect(
useCallback(() => {
const getCookieHeaderString = async () => {
const token = getUserAccessToken()
if (token) {
const cookie = getCookieFromToken(token)
if (Platform.OS === 'android') {
await CookieManager.clearAll(useWebKit)
}
await CookieManager.set(
url,
{
name: 'PHPSESSID',
value: cookie.sub,
httpOnly: true,
secure: true
},
useWebKit
)
}
setIsReady(true)
}
if (customer) {
getCookieHeaderString()
} else {
setIsReady(true)
}
return () => {
setIsReady(false)
}
}, [customer])
)
if (isReady) {
return null
}
return (
<Webview
...
/>
)
}
The key was to set the cookie before every webview mount. I didn't experience any false positives this way, even tho the documentation warns about this. If you need to persist browser history between WebView mounts, you could write your own logic and handle the URL with state. I was using this hook for managing the history stacks:
export default function useBrowserHistory(homepage: string) {
const [backStack, setBackStack] = useState<string[]>([homepage])
const [forwardStack, setForwardStack] = useState<string[]>([])
const visit = useCallback(
(url: string) => {
if (!backStack.includes(url)) {
setForwardStack([])
setBackStack((prev) => [...prev, url])
}
},
[backStack]
)
const back = useCallback(
(steps: number = 0) => {
let i = steps
const tempBackStack = [...backStack]
const tempForwardStack = [...forwardStack]
while (tempBackStack.length > 1 && i-- > 0) {
tempForwardStack.push(tempBackStack[tempBackStack.length - 1])
tempBackStack.pop()
}
setForwardStack(tempForwardStack)
setBackStack(tempBackStack)
return tempBackStack[tempBackStack.length - 1]
},
[backStack, forwardStack]
)
const forward = useCallback(
(steps: number = 0) => {
let i = steps
const tempBackStack = [...backStack]
const tempForwardStack = [...forwardStack]
while (tempForwardStack.length > 0 && i-- > 0) {
tempBackStack.push(tempForwardStack[tempForwardStack.length - 1])
tempForwardStack.pop()
}
setForwardStack(tempForwardStack)
setBackStack(tempBackStack)
return tempBackStack[tempBackStack.length - 1]
},
[backStack, forwardStack]
)
const resetHistory = useCallback(() => {
setBackStack([])
setForwardStack([])
}, [])
return {
visit,
back,
forward,
backStack,
forwardStack,
resetHistory
}
}
I only reset the cookies for Android, because I had some issues overwriting other cookies there.
You should also store that session cookie in secure storage (MMKV for example), so you can set it before WebView load, because session cookies are wiped. I took inspiration from this: https://stackoverflow.com/questions/62057393/how-to-keep-last-web-session-active-in-react-native-webview but instead of passing the cookie through cookie header in source I set it via the CookieManager.