Prevent Reload in React Native WebView - reactjs

I'm running a separate web app inside a WebView component, within a React Native app, and I'm trying to get them communicating properly.
React Native to WebView works fine. I can call webView.postMessage(...) and receive it in document.addEventListener("message", ...) without any problems.
However, when I try to go the other way (WebView to Native) the call to window.postMessage triggers a url change via window.location which seems to reload the entire WebView, and breaks the routing solution inside it.
The react-native-community/react-native-webview component seems to have the same problem.
Is there any way to message the native app from inside a web view without changing the URL or causing a page reload?

You can use onShouldStartLoadWithRequest props for IOS Example:-
import React, {Component, useCallback} from 'react';
import {
BackHandler,
Platform,
SafeAreaView,
ActivityIndicator,
StyleSheet,
Dimensions,
Linking,
} from 'react-native';
import {WebView} from 'react-native-webview';
import Spinner from 'react-native-loading-spinner-overlay';
class SupportPayWebView extends Component {
constructor(props) {
super(props);
}
webView = {
canGoBack: false,
ref: null,
};
loader = {
show: true,
};
onAndroidBackPress = () => {
if (this.webView.canGoBack && this.webView.ref) {
this.webView.ref.goBack();
return true;
}
return false;
};
componentWillMount() {
if (Platform.OS === 'android') {
BackHandler.addEventListener(
'hardwareBackPress',
this.onAndroidBackPress,
);
} else {
BackHandler.addEventListener('hardwareBackPress', this.backHandler);
}
}
componentWillUnmount() {
if (Platform.OS === 'android') {
BackHandler.removeEventListener('hardwareBackPress');
} else {
BackHandler.removeEventListener('hardwareBackPress', this.backHandler);
}
}
backHandler = () => {
this.webView.ref.goBack();
return true;
};
ActivityIndicatorLoadingView() {
return (
<ActivityIndicator
color="#009688"
size="large"
style={styles.ActivityIndicatorStyle}
/>
);
}
onShouldStartLoadWithRequest = (navigator) => {
this.webView.ref.stopLoading();
return false;
};
render() {
return (
<>
<WebView
style={styles.WebViewStyle}
onLoadEnd={() => {
this.loader.show = false;
}}
injectedJavaScript={
"const meta = document.createElement('meta'); meta.setAttribute('content', 'width=device-width, initial-scale=0.5, maximum-scale=0.5, user-scalable=0'); meta.setAttribute('name', 'viewport'); document.getElementsByTagName('head')[0].appendChild(meta); "
}
scalesPageToFit={true}
automaticallyAdjustContentInsets={true}
scrollEnabled={true}
javaScriptEnabled={true}
domStorageEnabled={true}
renderLoading={this.ActivityIndicatorLoadingView}
startInLoadingState={true}
bounces={false}
source={{uri: '}}
ref={(webView) => {
this.webView.ref = webView;
}}
onShouldStartLoadWithRequest={this.onShouldStartLoadWithRequest}
sharedCookiesEnabled={true}
// scalesPageToFit={false}
javaScriptEnabledAndroid={true}
userAgent="Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2228.0 Safari/537.36"
// decelerationRate="normal"
/>
</>
// </SafeAreaView>
);
}
}
const styles = StyleSheet.create({
WebViewStyle: {
justifyContent: 'center',
alignItems: 'center',
marginTop: Platform.OS === 'ios' ? 35 : 0,
width: '100%',
height: '100%',
resizeMode: 'cover',
flex: 1,
},
ActivityIndicatorStyle: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center',
},
});

Related

React Native Webview make back button on android go back

When I click the hardware button on android the app closes, I want to go back on the previous page,
This is my code
import { StatusBar } from 'expo-status-bar';
import React, { useState } from 'react';
import { ActivityIndicator, Linking, SafeAreaView, StyleSheet, Text, View } from 'react-native';
import { WebView } from 'react-native-webview';
export default function App() {
const [isLoadong, setLoading] = useState(false);
return (
<SafeAreaView style={styles.safeArea}>
<WebView
originWhiteList={['*']}
source={{ uri: 'https://google.com' }}
style={styles.container }
onLoadStart={(syntheticEvent) => {
setLoading(true);
}}
onShouldStartLoadWithRequest={(event)=>{
if (event.navigationType === 'click') {
if (!event.url.match(/(google\.com\/*)/) ) {
Linking.openURL(event.url)
return false
}
return true
}else{
return true;
}
}}
onLoadEnd={(syntheticEvent) => {
setLoading(false);
}} />
{isLoadong && (
<ActivityIndicator
color="#234356"
size="large"
style={styles.loading}
/>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#234356'
},
loading: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
},
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
There are built-in goBack() method available in react-native-webview libraries
you can use the API method to implement back navigation of webview.
For this, you have to get the reference of react-native-webview component and call method from the reference object.
also, you are able to put the listener on the Android Native Back Button Press event to call the goBack() method of webview.
try the following code...
import { StatusBar } from 'expo-status-bar';
import React, { useState, useRef, useEffect } from 'react';
import { ActivityIndicator, Linking, SafeAreaView, StyleSheet, BackHandler } from 'react-native';
import { WebView } from 'react-native-webview';
export default function App() {
const webViewRef = useRef()
const [isLoadong, setLoading] = useState(false);
const handleBackButtonPress = () => {
try {
webViewRef.current?.goBack()
} catch (err) {
console.log("[handleBackButtonPress] Error : ", err.message)
}
}
useEffect(() => {
BackHandler.addEventListener("hardwareBackPress", handleBackButtonPress)
return () => {
BackHandler.removeEventListener("hardwareBackPress", handleBackButtonPress)
};
}, []);
return (
<SafeAreaView style={styles.safeArea}>
<WebView
originWhiteList={['*']}
source={{ uri: 'https://google.com' }}
style={styles.container}
ref={webViewRef}
onLoadStart={(syntheticEvent) => {
setLoading(true);
}}
onShouldStartLoadWithRequest={(event)=>{
if (event.navigationType === 'click') {
if (!event.url.match(/(google\.com\/*)/) ) {
Linking.openURL(event.url)
return false
}
return true
}
else{
return true;
}
}}
onLoadEnd={(syntheticEvent) => {
setLoading(false);
}}
/>
{isLoadong && (
<ActivityIndicator
color="#234356"
size="large"
style={styles.loading}
/>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: '#234356'
},
loading: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
bottom: 0,
alignItems: 'center',
justifyContent: 'center'
},
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
first add ref for access your webview like that:
<WebView
ref={WEBVIEW_REF}
then for access to Hardware Back Button you can use this:
import { BackHandler } from 'react-native';
constructor(props) {
super(props)
this.handleBackButtonClick = this.handleBackButtonClick.bind(this);
}
componentWillMount() {
BackHandler.addEventListener('hardwareBackPress', this.handleBackButtonClick);
}
componentWillUnmount() {
BackHandler.removeEventListener('hardwareBackPress', this.handleBackButtonClick);
}
handleBackButtonClick() {
this.refs[WEBVIEW_REF].goBack();
return true;
}
in handleBackButtonClick you can do back for webview and add this.refs[WEBVIEW_REF].goBack(); . I Hope that's helpful:)
Here is a simple solution using the magic of React's State.
Hope this helps.
import React, { useRef, useState } from 'react'
export default function Component () {
// This is used to save the reference of your webview, so you can control it
const webViewRef = useRef(null);
// This state saves whether your WebView can go back
const [webViewcanGoBack, setWebViewcanGoBack] = useState(false);
const goBack = () => {
// Getting the webview reference
const webView = webViewRef.current
if (webViewcanGoBack)
// Do stuff here if your webview can go back
else
// Do stuff here if your webview can't go back
}
return (
<WebView
source={{ uri: `Your URL` }}
ref={webViewRef}
javaScriptEnabled={true}
onLoadProgress={({ nativeEvent }) => {
// This function is called everytime your web view loads a page
// and here we change the state of can go back
setWebViewcanGoBack(nativeEvent.canGoBack)
}}
/>
)
}

How to access next url from browser click from react-native-webview

I have a webview source that I have opened using react-native-webview, I want to access any and every url and console it whenever I click on the webview, so that I can use it as source for another webview. However am unable to figure how to do it
I tried using NavigationStateChange and onShouldLoadSTartwithrequest but that did not help.
below is my code
import React, {useState, useRef, useEffect} from 'react';
import {WebView} from 'react-native-webview';
import {
View,
SafeAreaView,
ActivityIndicator,
StyleSheet,
TouchableOpacity,
Text,
Linking,
Alert,
BackHandler,
} from 'react-native';
import Footer from '../components/Footer';
import {useBackHandler} from '#react-native-community/hooks';
import OnlineConsultationWebviewScreen from './OnlineConsultationWebviewScreen';
export default function ConsultationHomeScreen(props) {
const uri = props.route.params.uri;
const [canGoBack, setCanGoBack] = useState(false);
const [canGoForward, setCanGoForward] = useState(false);
const [currentUrl, setCurrentUrl] = useState('');
const webviewRef = useRef(null);
const renderLoadingView = () => {
return (
<View style={{flex: 1, alignItems: 'center', justifyContent: 'center'}}>
<ActivityIndicator size="large" />
</View>
);
};
const onMessage = (e) => {
// retrieve event data
var data = e.nativeEvent.data;
// maybe parse stringified JSON
try {
data = JSON.parse(data);
} catch (e) {}
// check if this message concerns us
if ('object' == typeof data && data.external_url_open) {
// proceed with URL open request
return Alert.alert(
'External URL',
'Do you want to open this URL in your browser?',
[
{text: 'Cancel', style: 'cancel'},
{text: 'OK', onPress: () => Linking.openURL(data.external_url_open)},
],
{cancelable: false},
);
}
};
const jsCode = `!function(){var e=function(e,n,t){if(n=n.replace(/^on/g,""),"addEventListener"in window)e.addEventListener(n,t,!1);else if("attachEvent"in window)e.attachEvent("on"+n,t);else{var o=e["on"+n];e["on"+n]=o?function(e){o(e),t(e)}:t}return e},n=document.querySelectorAll("a[href]");if(n)for(var t in n)n.hasOwnProperty(t)&&e(n[t],"onclick",function(e){new RegExp("^https?://"+location.host,"gi").test(this.href)||(e.preventDefault(),window.postMessage(JSON.stringify({external_url_open:this.href})))})}();`;
return (
<SafeAreaView style={{flex: 1}}>
<WebView
source={{
uri: uri,
}}
renderLoading={renderLoadingView}
javaScriptEnabled={true}
domStorageEnabled={true}
startInLoadingState={true}
ref={webviewRef}
injectedJavaScript={jsCode}
onMessage={onMessage}
onError={console.error.bind(console, 'error')}
// onShouldStartLoadWithRequest={(event) => {
// if (event.url !== uri ){
// Linking.openURL(event.url);
// console.log('Event', event.url);
// return false;
// }
// return true;
// }}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
tabBarContainer: {
padding: 20,
flexDirection: 'row',
justifyContent: 'space-around',
backgroundColor: '#b43757',
},
button: {
color: 'white',
fontSize: 24,
},
});
Am stuck with this since long, please let me know how do I access any and every click and console it, so that I can sue it later as a new source for Webview.
Any suggestion would be great.
Try this :
<WebView
ref="webview"
source={uri}
onNavigationStateChange={this._onNavigationStateChange.bind(this)}
javaScriptEnabled = {true}
domStorageEnabled = {true}
injectedJavaScript = {this.state.cookie}
startInLoadingState={false}
/>
Add this function :
_onNavigationStateChange(webViewState){
console.log(webViewState.url)
}
FYI webviewState object consists of, use the url property:
{
canGoBack: bool,
canGoForward: bool,
loading: bool,
target: number,
title: string,
url: string,
}
let me know if it helps

react native multiple video with Swiper component play pause issue

I have multiple videos in the swiper to show videos one by one, but all the videos are loaded and playing at the same time and audios are messed up, I want current video only play at a time.
import * as React from 'react';
import { Text, View, StyleSheet,Image, Dimensions } from 'react-native';
import { Constants } from 'expo';
import { Video } from 'expo';
import Swiper from './Swiper';
import InViewPort from './InViewport';
const screenWidth = Dimensions.get('window').width ;
const screenHeight = Dimensions.get('window').height;
export default class App extends React.Component {
constructor(props) {
super(props);
// Your source data
this.state = {
images: {},
muted : false,
paused: true,
};
this.player = Array();
this.onChangeImage = this.onChangeImage.bind(this);
}
videoError(err){
console.warn(err);
}
pauseVideo = () => {
var curr = this.state.currentIndex;
console.warn(curr);
if(this.player[curr]) {
this.setState({paused: true });
}
}
playVideo = () => {
var curr = this.state.currentIndex;
console.warn(curr);
if(this.player[curr]) {
this.setState({paused: false});
}
}
handlePlaying = (isVisible) => {
isVisible ? this.playVideo() : this.pauseVideo();
}
onChangeImage (index) {
this.setState({ currentIndex: index});
}
render() {
let items = Array.apply(null, Array(15)).map((v, i) => {
return {
id: i,
caption: i + 1,
source: { uri: 'http://placehold.it/200x200?text=' + (i + 1) },
dimension: '{ width: 150, height: 150 }',
};
});
return(
<View style={styles.DefaultView}>
<Swiper
showsPagination={false}
onIndexChanged={this.onChangeImage}
index={0}
>
{items.map((item, key) => {
if(key==1 || key ==5){
return (
<InViewPort onChange={this.handlePlaying} key={key}>
<Video onError={this.videoError}
muted={this.state.muted}
paused={this.state.paused}
source={{uri: 'http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4' }}
style={styles.backgroundVideo}
ref={(ref) => {
this.player[key] = ref;
}}
controls={true}
/>
</InViewPort>
)
}else{
return(
<Image
resizeMode='contain'
style={{width:screenWidth, height: screenHeight}}
source={item.source}
key={key}
/>
)
}
})}
</Swiper>
</View>
)
}
}
const styles = StyleSheet.create({
scrollView: {
flex: 1,
flexDirection: 'row',
},
DefaultView: {
flex: 1,
backgroundColor: '#000',
width: screenWidth,
justifyContent:'center',
alignItems:'center'
},
iconContainer: {
flexDirection: "row",
justifyContent: "space-evenly",
width: 150,
},
backgroundVideo: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
width: screenWidth,
height: 300,
marginTop:'50%',
position:'absolute',
},
});
I need some idea on this, we have a player reference to be used, also swiper component have onIndexChanged which will trigger when we moved to next video, how we can link the reference of the player to onIndexChanged and when we do swipe how we make it current video only to play?
As per Andrew suggestion I have used InPortView component too determine the current view of swipe, but still I am not sure how to make reference for video elements to be used in the functions for play and pause the concern video.
Components used:
For video react-native-video
For Swiper : react-native-swiper
Updated Full code with Expo example : Expo Snack
So taking your snack. I managed to get it to work.
I moved the Video out into its own component and passed a few additional props to it, the index in the array and the currentIndex showing.
export default class App extends React.Component {
constructor(props) {
super(props);
// Your source data
this.state = {
images: {},
muted : false,
paused: true,
currentIndex: 0
};
}
onChangeImage = (index) => {
console.log('currentIndex ', index)
this.setState({ currentIndex: index});
}
render() {
let items = Array.apply(null, Array(15)).map((v, i) => {
return {
id: i,
caption: i + 1,
source: { uri: 'http://placehold.it/200x200?text=' + (i + 1) },
dimension: '{ width: 150, height: 150 }',
};
});
return(
<View style={styles.DefaultView}>
<Swiper
showsPagination={false}
onIndexChanged={this.onChangeImage}
index={0}
>
{items.map((item, key) => {
if(key==1 || key ==5){
return (
<VideoPlayer key={key} index={key} currentIndex={this.state.currentIndex}/>
)
}else{
return(
<Image
resizeMode='contain'
style={{width:screenWidth, height: screenHeight}}
source={item.source}
key={key}
/>
)
}
})}
</Swiper>
</View>
)
}
}
The video component uses react-native-inviewport to help handle whether or not it is in the viewport. However it doesn't play nicely with react-native-swiper but it is possible to get it to work.
export default class VideoPlayer extends React.Component {
pauseVideo = () => {
if(this.video) {
this.video.pauseAsync();
}
}
playVideo = () => {
if(this.video) {
this.video.playAsync();
}
}
handlePlaying = (isVisible) => {
this.props.index === this.props.currentIndex ? this.playVideo() : this.pauseVideo();
}
render() {
return (
<View style={styles.container}>
<InViewPort onChange={this.handlePlaying}>
<Video
ref={ref => {this.video = ref}}
source={{ uri: 'http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4' }}
rate={1.0}
volume={1.0}
isMuted={false}
resizeMode="cover"
shouldPlay
style={{ width: WIDTH, height: 300 }}
/>
</InViewPort>
</View>
)
}
}
When I used the InViewPort component alone it seemed to think that the video in position 6 was in the viewport and would play it. So what I use the InviewPort is to perform a check to compare the index of the video with the currentIndex if they match play the video otherwise pause. I suppose this could be updated to use componentDidUpdate to handle the changes in the props. However, additional checks will need to be performed when the component mounts so that it doesn't play the video.
Here is my snack with it working. https://snack.expo.io/#andypandy/swiper-video

Why won't my Custom React Native Component Import Correctly

Currently, I have a simple React Native Expo app setup. I have two components App and QRreader.
I am trying to import the QRreader component into my main App component.
The Main App component code...
import React, { Component } from 'react';
import { Button, Text, View, StyleSheet } from 'react-native';
import { Constants, WebBrowser } from 'expo';
import QRreader from './qr';
export default class App extends Component {
state = {
result: null,
};
render() {
return (
<View style={styles.container}>
<QRreader/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
},
});
The QR component code...
import React, { Component } from 'react';
import { Text, View, StyleSheet, Alert } from 'react-native';
import { Constants, BarCodeScanner, Permissions } from 'expo';
export default class QRreader extends Component {
state = {
hasCameraPermission: null
};
componentDidMount() {
this._requestCameraPermission();
}
_requestCameraPermission = async () => {
const { status } = await Permissions.askAsync(Permissions.CAMERA);
this.setState({
hasCameraPermission: status === 'granted',
});
};
_handleBarCodeRead = data => {
Alert.alert(
'Scan successful!',
JSON.stringify(data)
);
};
render() {
return (
<View style={styles.container}>
{this.state.hasCameraPermission === null ?
<Text>Requesting for camera permission</Text> :
this.state.hasCameraPermission === false ?
<Text>Camera permission is not granted</Text> :
<BarCodeScanner
onBarCodeRead={this._handleBarCodeRead}
style={{ height: 200, width: 200 }}
/>
}
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
}
});
I tried different variations of the import using "./" "." "qr.js" "qr"
Im getting an error Unable to resolve module "qr.js" Module does not exist in the main module map.
My file structure is Here
You haven't registered your main module yet.
AppRegistry.registerComponent('Main', () => App); Please add this line to the bottom of your class and check if the problem persists.
hmm...so it looked like I had to restart the Expo project for it to work without adding any additional code.
Just out of curiosity?
Where would I add AppRegistry.registerComponent('Main', () => App); exactly? and why would I have to do this?

Uploading an image in react-native using react-native-image-picker

I have just initialized a basic react-native project and its running in the emulator. I have as well installed this package https://github.com/react-community/react-native-image-picker
and i am trying to upload an image. The code is simple as i have just added some code to handle image upload
/**
* Sample React Native App
* https://github.com/facebook/react-native
* #flow
*/
import React, { Component } from 'react';
import {
AppRegistry,
StyleSheet,
Button,
Text,
Image,
Alert,
View
} from 'react-native';
var ImagePicker = require('react-native-image-picker');
var options = {
title: 'Select Avatar',
customButtons: [
{name: 'fb', title: 'Choose Photo from Facebook'},
],
storageOptions: {
skipBackup: true,
path: 'images'
}
};
const onPressLearnMore = () => {
ImagePicker.launchImageLibrary(options, (response) => {
let source = { uri: response.uri };
this.setState({
avatarSource: source
});
});
//Alert.alert('Button has been pressed!');
};
export default class AwesomeProject extends Component {
constructor() {
super()
this.state = {
avatarSource: 'image.jpg'
}
}
render() {
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Text style={styles.instructions}>
To get started, edit index.android.js
</Text>
<Button onPress={onPressLearnMore} title="Upload Image" color="#841584" accessibilityLabel="Learn more about this purple button" />
<Image source={this.state.avatarSource} style={styles.uploadAvatar} />
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject);
I get this error when i run this on the emulator
undefined is not a function (evaluating '_this.setState({
avatarSource: source
})')
<unknown>
index.android.bundle?platform=android&dev=true&hot=false&minify=false:1274:19
__invokeCallback
index.android.bundle?platform=android&dev=true&hot=false&minify=false:4818:21
<unknown>
index.android.bundle?platform=android&dev=true&hot=false&minify=false:4664:32
__guard
index.android.bundle?platform=android&dev=true&hot=false&minify=false:4753:11
invokeCallbackAndReturnFlushedQueue
index.android.bundle?platform=android&dev=true&hot=false&minify=false:4663:19
You should write the function onPressLearnMore inside your AwesomeProject component, and don't forget to bind in order to use this
export default class AwesomeProject extends Component {
constructor(){
...
this.onPressLearnMore = this.onPressLearnMore.bind(this)
}
onPressLearnMore(){
//you can use this.setState
}
render(){
...
}
}

Resources