React Native - Webview set cookies policy - ios

To give you context, our app is a react-native web wrapper (something similar to what Cordova or ionic does).
The app downloads an updated version of the web bundle that is stored locally after the user opens the app, and then the WebView component uses it as a source to show the app to the user.
There are some parts of this web app that uses web sockets, so the issue is that considering its large traffic, the socket/exchange service has a bunch of nodes/replicas and in order to keep connections alive the load balancer uses Set-Cookie headers to make sure that the client is going to the right service node in every request
So, what we're trying is:
Enable Set-Cookie behavior within a WebView
Get Set-Cookie response header manually from a request that is happening within a WebView
Our react-native application looks like:
import React from 'react';
import {Platform, StyleSheet, View} from 'react-native';
import {WebView} from 'react-native-webview';
type WrapperProps = {
path: string;
};
function Wrapper({path}: WrapperProps) {
return (
<View style={styles.container}>
<WebView
allowFileAccess
allowUniversalAccessFromFileURLs
allowingReadAccessToURL={path}
javaScriptEnabled
originWhitelist={['*']}
sharedCookiesEnabled
source={{
uri:
Platform.OS === 'ios'
? `file://${path}/index.html`
: `${path}/index.html`,
}}
injectedJavaScript={`(function () {
window.ReactNativeWebView.postMessage(JSON.stringify(document.coookie));
var open = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function (method, url) {
this.addEventListener("load", function () {
var responseHeaders = this.getAllResponseHeaders();
var setCookies = this.getResponseHeader("Set-Cookie"); // always return null
window.ReactNativeWebView.postMessage(JSON.stringify({ setCookies }));
window.ReactNativeWebView.postMessage(
JSON.stringify({ responseHeaders }) // return all headers except for Set-Cookie
);
});
open.apply(this, arguments);
};
})();
`}
onMessage={e => {
console.log({onMessageEvent: e.nativeEvent.data});
}}
style={styles.webview}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
webview: {
height: '100%',
width: '100%',
},
});
export default React.memo(Wrapper);
Do you have any clue how to solve this issue?
P.S: We already read How to set WKWebView cookie to accept Policy - But it's for swift.

I managed to
Enable Set-Cookie behavior within a WebView
by establishing a socket connection using socket.io-client, so the web socket requests within the WebView will already include cookies in request headers (which will keep connections alive).
Considering react-native-webview uses WKWebView on iOS, there is no way to access cookies in response headers. However, in this particular scenario you can create the socket connection from outside the web view, handle cookies (it seems socket.io-client already does), and then they should be available in the requests within the WebView.
In order to setup socket io in your react-native app, you will need to
Install socket.io-client#2.1.1 (I tried different versions but this was the only one that worked)
Assign window.navigator.userAgent = 'react-native'; (since ES6 modules are hoisted, this assignment must not be done in the same file as react-native and socket io imports)
Establish your socket connection.
So it should look like:
// userAgent.js
window.navigator.userAgent = 'react-native';
// Wrapper.tsx
import React from 'react';
import {Platform, StyleSheet, View} from 'react-native';
import {WebView} from 'react-native-webview';
// user agent MUST be imported before socket io
import './userAgent';
import io from 'socket.io-client';
export const socket = io('https://your.host.com/', {
path: '/path/to/socket.io/',
jsonp: false,
});
type WrapperProps = {
path: string;
};
function Wrapper({path}: WrapperProps) {
return (
<View style={styles.container}>
<WebView
allowFileAccess
allowUniversalAccessFromFileURLs
allowingReadAccessToURL={path}
javaScriptEnabled
originWhitelist={['*']}
sharedCookiesEnabled
source={{
uri:
Platform.OS === 'ios'
? `file://${path}/index.html`
: `${path}/index.html`,
}}
style={styles.webview}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
webview: {
height: '100%',
width: '100%',
},
});
export default React.memo(Wrapper);

Related

Deeplinking with a domain name

I have the following code in my App.js:
import React, { useState, useRef, useEffect } from 'react';
import { SafeAreaView, Text } from 'react-native';
import { NavigationContainer, useLinking } from '#react-navigation/native';
import { createStackNavigator } from '#react-navigation/stack';
const Stack = createStackNavigator();
const Screen1 = () => <SafeAreaView><Text>Screen1</Text></SafeAreaView>;
const Screen2 = () => <SafeAreaView><Text>Screen2</Text></SafeAreaView>;
export default function App() {
const ref = useRef();
const [isReady, setIsReady] = useState(false);
const [initialState, setInitialState] = useState();
const { getInitialState } = useLinking(ref, {
prefixes: ['http://example.com', 'mychat://'],
config: {
screens: {
Screen2: 'screen-2',
},
},
});
useEffect(() => {
getInitialState().then((state) => {
if (state !== undefined) setInitialState(state);
setIsReady(true);
});
}, [getInitialState]);
if (!isReady) return null;
return (
<NavigationContainer ref={ref} initialState={initialState}>
<Stack.Navigator>
<Stack.Screen name='Screen1' component={Screen1} />
<Stack.Screen name='Screen2' component={Screen2} />
</Stack.Navigator>
</NavigationContainer>
);
}
Most of them are copied from https://reactnavigation.org/docs/deep-linking/ and https://reactnavigation.org/docs/use-linking/.
In the docs there is prefixes: ['https://mychat.com', 'mychat://'], I just changed https://mychat.com to http://example.com. But it doesn't seem to work.
When I open the following links in Safari:
mychat:// (works, gets redirected to app Screen1)
mychat://screen-2 (works, gets redirected to app Screen2)
http://example.com (just opens the link in the browser, no popup to redirect to app)
What change do I need to make to redirect the domain name to the mobile app? Am I missing something?
You need to use a domain that you have access to alongside a server.
Your server needs to host a couple of files, typically within the .well-known directory:
apple-app-site-association (note the .json is not needed)
assetlinks.json
You also need to enable some entitlements within your app for iOS, this may also be true for Android. On iOS, this will be enabling the Associated Domains entitlement alongside an entry for webcredentials:yourdomain.com
The documentation is pretty good to go through to give an understanding on what needs to be done in order to achieve Universal Links
https://developer.apple.com/library/archive/documentation/General/Conceptual/AppSearch/UniversalLinks.html
https://developer.android.com/training/app-links/verify-site-associations
Examples:
iOS - https://stackoverflow.com/.well-known/apple-app-site-association
Android - https://stackoverflow.com/.well-known/assetlinks.json

React native: TypeError: null is not an object (evaluating 'SplashScreen.preventAutoHide')

My react native app was working just fine before I used expo eject. I ejected it because I now intend to build and release the app to the ios app store. As soon as I attempt to start the ejected app using react-native run-ios after it's been ejected I get the exception below.
Please could someone help to understand what's causing this issue and how to tackle it?
react Native versions as follows:
react-native-cli: 2.0.1
react-native: 0.61.5
TypeError: null is not an object (evaluating 'SplashScreen.preventAutoHide')
This error is located at:
in AppLoading (at AppLoading.js:52)
in AppLoading (at App.js:464)
in App (at renderApplication.js:40)
in RCTView (at AppContainer.js:101)
in RCTView (at AppContainer.js:119)
in AppContainer (at renderApplication.js:39)
preventAutoHide
SplashScreen.js:4:21
AppLoading#constructor
AppLoadingNativeWrapper.js:6:8
renderRoot
[native code]:0
runRootCallback
[native code]:0
renderApplication
renderApplication.js:52:52
runnables.appKey.run
AppRegistry.js:116:10
runApplication
AppRegistry.js:197:26
callFunctionReturnFlushedQueue
[native code]:0
The AppLoading component is not available in the bare workflow. As #gaurav-roy said you have to refactor your code.
Install the expo-splash-screen package with npm install expo-splash-screen
Add a splash-screen to your Android and iOS projects. Run npm run expo-splash-screen --help and follow the instructions of this CLI tool. (Because of a bug you might have to run the command again with the -p "ios" flag if it only adds the SplashScreen for Android after running it.
Change your code inside App.tsx in a similar way as in this example.
If you're working with hooks you probably want to add an useEffect
hook with an empty dependency list which runs an async function. Here an example of how it could be done:
const App = (props: Props) => {
const [isLoadingComplete, setLoadingComplete] = useState(false);
const init = async () => {
try {
// Keep on showing the SlashScreen
await SplashScreen.preventAutoHideAsync();
await loadResourcesAsync();
} catch (e) {
console.warn(e);
} finally {
setLoadingComplete(true);
// Hiding the SplashScreen
await SplashScreen.hideAsync();
}
useEffect(() => {
init();
}, []);
const renderApp = () => {
if (!isLoadingComplete && !props.skipLoadingScreen) {
return null;
}
return (
<Main />
);
};
return <StoreProvider>{renderApp()}</StoreProvider>;
}
As its evident from docs , SplashScreen is an inbuilt api for expo apps, and since you ejected it , it throws an error since it cant be used.
You can see this in the docs expo splashscreen .
First you should download npm i expo-splash-screen
And then change your import statement to :
import * as SplashScreen from 'expo-splash-screen';
Hope it helps. feel free for doubts
After looking through this SO page and then digging into some links, especially this expo page where they kind of provide a solution for this, I was able to get my app running after about 3 hours of struggle. They haven't added any functional component example, so I am sharing my code below in case someone came here looking for the solution.
import { Asset } from "expo-asset";
import * as Font from "expo-font";
import React, { useState, useEffect } from "react";
import { Platform, StatusBar, StyleSheet, View } from "react-native";
import { Ionicons } from "#expo/vector-icons";
import * as SplashScreen from 'expo-splash-screen';
import AppNavigator from "./navigation/AppNavigator";
export default props => {
const [isLoadingComplete, setLoadingComplete] = useState(false);
const theme = {
...DefaultTheme,
roundness: 2,
colors: {
...DefaultTheme.colors,
primary: "#E4002B",
accent: "#E4002B",
},
};
useEffect(() => {
async function asyncTasks() {
try {
await SplashScreen.preventAutoHideAsync();
} catch (e) {
console.warn(e);
}
await loadResourcesAsync()
setLoadingComplete(true);
}
asyncTasks()
}, []);
return (
!isLoadingComplete && !props.skipLoadingScreen ? null :
<View style={styles.container}>
{Platform.OS === "ios" && <StatusBar barStyle="default" />}
<AppNavigator />
</View>
);
}
async function loadResourcesAsync() {
await Promise.all([
Asset.loadAsync([
require("./assets/images/logo.png") // Load your resources here (if any)
]),
Font.loadAsync({
// You can remove this if you are not loading any fonts
"space-mono": require("./assets/fonts/SpaceMono-Regular.ttf"),
}),
]);
await SplashScreen.hideAsync();
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
});
This solved it for me for an ejected expo app. Looks like expo was referencing it wrongly.
https://github.com/expo/expo/issues/7718#issuecomment-610508510
What worked for me was updating node_modules/expo/build/launch/splashScreen.js to the following as suggested by adamsolomon1986 in the repo (issue #7718):
import { NativeModules } from 'react-native';
import* as SplashScreen from 'expo-splash-screen'
export function preventAutoHide() {
if (SplashScreen.preventAutoHide) {
SplashScreen.preventAutoHide();
}
}
export function hide() {
if (SplashScreen.hide) {
SplashScreen.hide();
}
}
//# sourceMappingURL=SplashScreen.js.map

(React Native) Load local HTML file into WebView

I try to load the local .html file into WebView in React Native:
// load local .html file
const PolicyHTML = require('./Policy.html');
// PolicyHTML is just a number of `1`
console.log('PolicyHTML:', PolicyHTML);
// will cause an error: JSON value '1' of type NSNumber cannot be converted to NSString
<WebView html={PolicyHTML} />
The .html file should be read as a string, not as a resource representative.
How can I load the .html file into WebView in React Native?
By the way, what is the type of those resource representatives from require()? Is it number?
try it:
const PolicyHTML = require('./Policy.html');
<WebView
source={PolicyHTML}
style={{flex: 1}}
/>
I come across this post searching for loading static html.
If your html code is retrieved using, for example, an API, you can render WebView in this way:
<WebView
originWhitelist={['*']}
source={{ html: html, baseUrl: '' }}
/>
Notice that originWhitelistis required as explained in the documentation:
Note that static html will require setting of originWhitelist for
example to ["*"].
<View style={{flex: 1}}>
<WebView
style={{flex: 1}}
source={require("./resources/index.html")}
/>
</View>
To make WebView, the parent has to has a dimension or flex:1. We could set the WebView to flex: 1 too so that it fills up the parent.
If you need to serve local assets as well, then:
put all assets together with index.html into android/app/src/main/assets/www (You can copy them there with gradle task)
Then:
var uri = Platform.OS == "android" ?
"file:///android_asset/www/index.html" :
"./web/www/index.html"
return <WebView source={{ uri }} />
** For iOS didn't tested, please add instruction, how assets should be stored
With Expo tools and generally using Expo:
import { WebView } from "react-native-webview";
import { readAsStringAsync } from "expo-file-system";
import { useAssets } from "expo-asset";
export const MusicSheet = () => {
const [index, indexLoadingError] = useAssets(
require("../assets/musicsheetview/index.html")
);
const [html, setHtml] = useState("");
if (index) {
readAsStringAsync(index[0].localUri).then((data) => {
setHtml(data);
});
}
return (
<View style={styles.container}>
<WebView
onLoad={() => {}}
source={{ html }}
onMessage={(event) => {}}
/>
</View>
);
};
const styles = StyleSheet.create({
container: {
height: 100,
display: "flex",
},
});
Try this :
Add your .html file in your project.
Write such lines of code in the file where you want to use WebView Component
const OurStoryHTML = require ('./OurStory.html')
<WebView
source={OurStoryHTML}
style={{flex: 1, marginTop : 44}}
/>
It may help you.
If you're working with assets, project directories is different on the device's directory once the project is build and you can't simply reference them via string url.
Expo
If using expo, you have to require every asset then use useAssets on the require to cache them to the local storage of the device.
useAssets will return an object that contains a localUri
(this is the uri of the image that has been cached)
you can then use the localUri and put it as the src of the image
import { useAssets } from 'expo-asset';
/* . . . */
const IndexHTML = require('./assets/index.html');
const myImage = require('./assets/splash.png');
// url link after image is cached to the device
const [imgSrc, setImgSrc] = useState('');
const [image, imerr] = useAssets(myImage);
const [html, error] = useAssets(IndexHTML);
const webViewProps = {
javaScriptEnabled: true,
androidLayerType: 'hardware',
originWhitelist: ['*'],
allowFileAccess: true,
domStorageEnabled: true,
mixedContentMode: 'always',
allowUniversalAccessFromFileURLs: true,
onLoad: () => {
console.log(image[0].localUri);
setImgSrc(image[0].localUri);
},
source: {
html: '<img src="' + imgSrc + '"/>',
},
};
return <WebView {...webViewProps} />
const webViewProps = {
...
source: IndexHTML,
};
Note: for the expo apporach, files referenced in IndexHTML will not be found
The trick is to turn your html into a string literal to utilize template strings.
Then you have to manually require each of those assets to concatenate localUrl
require() has limited types supported and you need to add a metro.config.js in your root folder.
it will give errors if you require() a .js file since it reads it as a module rather, the workaround approach would be to bundle your assets
const { getDefaultConfig } = require('expo/metro-config');
const config = getDefaultConfig(__dirname);
config.resolver.assetExts.push(
// Adds support for asset file types
'css', 'ppt', 'obj'
);
module.exports = config;
Moreover, expo can hot reload changes done with the cached assets.
React Native
If you have the android folder in your directory, navigate to
android > app > build.gradle
then add
android {
/* link assets to the local storage of device */
sourceSets {
main { assets.srcDirs = ['src/main/assets', '../../source/assets/'] }
// [do not touch, 'relative to your asset'] }
}
. . .
finally, the relative folder you linked in gradle can be accessed through
file:///android_asset/
for ex. file:///android_asset/index.html -> /asset/index.html
return <WebView source={{uri: `file:///android_asset/index.html`}} />
For IOS, here's how
On the other hand, you have to rebuild vanilla react to see the changes in the assets.. which takes about 20 minutes or so
Alternative
A quick solution would be to integrate a static server, but this is a recent fork of the react-native-static-server that only works in vanilla react native.

React Native and iOS share button

I am trying to access iOS' share button where you can share content to all services, including messages etc...
Any idea how I could do this? Thanks
You now have a simple Share API in react-native.
import { Share } from "react-native"
Share.share(
{
title: "a title",
message: "some message",
// or
url: imageReference
},
(options)
);
See http://facebook.github.io/react-native/docs/share.html
You can achieve this out of the box in React Native - just use ActionSheetIOS.showShareActionSheetWithOptions. See the documentation here.
You might want to check out the react-native-share package, it should cover your usecase. You can also see more relevant packages on JS.Coach
It's much easier than you think. Adding to #MoOx answer further.
With the new share api available, you can easily share information with your React Native app by just using it with all variables and configuration. (see here)
import React from 'react';
import { Share, View, Button } from 'react-native';
const ShareExample = () => {
const onShare = async () => {
try {
const result = await Share.share({
message:
'React Native | A framework for building native apps using React',
});
if (result.action === Share.sharedAction) {
if (result.activityType) {
// shared with activity type of result.activityType
} else {
// shared
}
} else if (result.action === Share.dismissedAction) {
// dismissed
}
} catch (error) {
alert(error.message);
}
};
return (
<View style={{ marginTop: 50 }}>
<Button onPress={onShare} title="Share" />
</View>
);
};
export default ShareExample;

Communicate with <webview> in Electron

I have a <webview> in my Electron app. I'd like to have safe "foreign" communication, the way I would with an iframe through postMessage. So for example:
webview.executeJavaScript("window.parent.postMessage('all done!')");
Is my only choice for communication with this subwebview to turn on nodeIntegration so that I can use sendToHost? Turning on all of nodeIntegration just for this one feature seems like overkill.
You can access Electron APIs in the webview preload script, including IPC, even when nodeIntegration is disabled. Your preload script can inject functions into the global namespace that will then be accessible within the page loaded in the webview. A simple example:
webview-preload.js:
const { ipcRenderer } = require('electron')
global.pingHost = () => {
ipcRenderer.sendToHost('ping')
}
webview-index.html:
<script>
pingHost()
</script>
window-index.html:
<script>
const webview = document.getElementById('mywebview')
webview.addEventListener('ipc-message', event => {
// prints "ping"
console.log(event.channel)
})
</script>
Easiest way
Communication is
Note:
(main.js or app.js or background.js or process.js ) no need to pass (directly pass component to component),i succesffully implemented in electron:3.1.10
for print html webview.
Window To Webview
example1.html
<webview id="paper" style="width:300px;height:800px" src="file:///static/mywebview.html" nodeintegration></webview>
example1.js
var webview = document.getElementById("paper");
webview.send("ping",data);
getting data from mycomponent or window(i send directly form component)
mywebview.html
<!---what data you want show----!>
mywebview.js
const {
ipcRenderer
} = require('electron')
//data from window
ipcRenderer.on('ping', (e, data) => { console.log(data) })
webview to window
Webview to window(direct pass to component)
mywebview.js
ipcRenderer.sendToHost("readyCompanyInfo",data)
in my window eg i use vue (mycomponent.vue or mypage)
example1.html
const ipcRenderer = require("electron").ipcRenderer;
webview.addEventListener("ipc-message",(event)=>{
const {args,channel}=event;
if(channel=="readyCompanyInfo")
{
console.log(channel,args)
//here you can see data what u passed from webview to window
console.log(args[0])
}
})

Resources