import React from 'react';
import {
ActivityIndicator,
StyleSheet,
Text,
UIManager,
View,
requireNativeComponent,
NativeModules,
Image,
findNodeHandle,
NativeSyntheticEvent,
} from 'react-native';
import invariant from 'invariant';
import {
defaultOriginWhitelist,
createOnShouldStartLoadWithRequest,
} from './WebViewShared';
import {
WebViewSourceUri,
WebViewError,
WebViewErrorEvent,
WebViewMessageEvent,
WebViewNavigationEvent,
WebViewSharedProps,
WebViewSource,
WebViewProgressEvent,
} from './types/WebViewTypes';
const BGWASH = 'rgba(255,255,255,0.8)';
const styles = StyleSheet.create({
container: {
flex: 1,
},
errorContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: BGWASH,
},
errorText: {
fontSize: 14,
textAlign: 'center',
marginBottom: 2,
},
errorTextTitle: {
fontSize: 15,
fontWeight: '500',
marginBottom: 10,
},
hidden: {
height: 0,
flex: 0, // disable 'flex:1' when hiding a View
},
loadingView: {
backgroundColor: BGWASH,
flex: 1,
justifyContent: 'center',
alignItems: 'center',
height: 100,
},
webView: {
backgroundColor: '#ffffff',
},
});
type DecelerationRate = number | 'normal' | 'fast';
// Imported from https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollView/processDecelerationRate.js
function processDecelerationRate(
decelerationRate?: DecelerationRate,
): number | undefined {
if (decelerationRate === 'normal') {
return 0.998;
}
if (decelerationRate === 'fast') {
return 0.99;
}
return decelerationRate;
}
const { RNCWKWebViewManager, RNCUIWebViewManager } = NativeModules;
const RNCUIWebView = requireNativeComponent('RNCUIWebView');
const RNCWKWebView = requireNativeComponent('RNCWKWebView');
const WebViewState: {
IDLE: 'IDLE';
LOADING: 'LOADING';
ERROR: 'ERROR';
} = {
IDLE: 'IDLE',
LOADING: 'LOADING',
ERROR: 'ERROR',
};
const NavigationType = {
click: 'click',
formsubmit: 'formsubmit',
backforward: 'backforward',
reload: 'reload',
formresubmit: 'formresubmit',
other: 'other',
};
const JSNavigationScheme = 'react-js-navigation';
type State = {
viewState: 'IDLE' | 'LOADING' | 'ERROR';
lastErrorEvent: WebViewError | null;
};
const DataDetectorTypes = [
'phoneNumber',
'link',
'address',
'calendarEvent',
'trackingNumber',
'flightNumber',
'lookupSuggestion',
'none',
'all',
];
const defaultRenderLoading = () => (
);
const defaultRenderError = (
errorDomain: string | undefined,
errorCode: number,
errorDesc: string,
) => (
Error loading page
{`Domain: ${errorDomain}`}
{`Error Code: ${errorCode}`}
{`Description: ${errorDesc}`}
);
/**
* `WebView` renders web content in a native view.
*
*```
* import React, { Component } from 'react';
* import { WebView } from 'react-native';
*
* class MyWeb extends Component {
* render() {
* return (
*
* );
* }
* }
*```
*
* You can use this component to navigate back and forth in the web view's
* history and configure various properties for the web content.
*/
class WebView extends React.Component {
static JSNavigationScheme = JSNavigationScheme;
static NavigationType = NavigationType;
static defaultProps = {
useWebKit: true,
originWhitelist: defaultOriginWhitelist,
};
static isFileUploadSupported = async () =>
// no native implementation for iOS, depends only on permissions
true;
state: State = {
viewState: this.props.startInLoadingState
? WebViewState.LOADING
: WebViewState.IDLE,
lastErrorEvent: null,
};
webViewRef = React.createRef();
// eslint-disable-next-line camelcase, react/sort-comp
UNSAFE_componentWillMount() {
if (
this.props.useWebKit === true
&& this.props.scalesPageToFit !== undefined
) {
console.warn(
'The scalesPageToFit property is not supported when useWebKit = true',
);
}
if (
!this.props.useWebKit
&& this.props.allowsBackForwardNavigationGestures
) {
console.warn(
'The allowsBackForwardNavigationGestures property is not supported when useWebKit = false',
);
}
}
componentDidUpdate(prevProps: WebViewSharedProps) {
if (!(prevProps.useWebKit && this.props.useWebKit)) {
return;
}
this.showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback');
this.showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction');
this.showRedboxOnPropChanges(prevProps, 'dataDetectorTypes');
if (this.props.scalesPageToFit !== undefined) {
console.warn(
'The scalesPageToFit property is not supported when useWebKit = true',
);
}
}
getCommands() {
if (!this.props.useWebKit) {
return UIManager.RNCUIWebView.Commands;
}
return UIManager.RNCWKWebView.Commands;
}
/**
* Go forward one page in the web view's history.
*/
goForward = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().goForward,
null,
);
};
/**
* Go back one page in the web view's history.
*/
goBack = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().goBack,
null,
);
};
/**
* Reloads the current page.
*/
reload = () => {
this.setState({ viewState: WebViewState.LOADING });
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().reload,
null,
);
};
/**
* Stop loading the current page.
*/
stopLoading = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().stopLoading,
null,
);
};
/**
* Posts a message to the web view, which will emit a `message` event.
* Accepts one argument, `data`, which must be a string.
*
* In your webview, you'll need to something like the following.
*
* ```js
* document.addEventListener('message', e => { document.title = e.data; });
* ```
*/
postMessage = (data: string) => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().postMessage,
[String(data)],
);
};
/**
* Injects a javascript string into the referenced WebView. Deliberately does not
* return a response because using eval() to return a response breaks this method
* on pages with a Content Security Policy that disallows eval(). If you need that
* functionality, look into postMessage/onMessage.
*/
injectJavaScript = (data: string) => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
this.getCommands().injectJavaScript,
[data],
);
};
/**
* We return an event with a bunch of fields including:
* url, title, loading, canGoBack, canGoForward
*/
updateNavigationState = (event: WebViewNavigationEvent) => {
if (this.props.onNavigationStateChange) {
this.props.onNavigationStateChange(event.nativeEvent);
}
};
/**
* Returns the native `WebView` node.
*/
getWebViewHandle = () => findNodeHandle(this.webViewRef.current);
onLoadingStart = (event: WebViewNavigationEvent) => {
const { onLoadStart } = this.props;
if (onLoadStart) {
onLoadStart(event);
}
this.updateNavigationState(event);
};
onLoadingError = (event: WebViewErrorEvent) => {
event.persist(); // persist this event because we need to store it
const { onError, onLoadEnd } = this.props;
if (onError) {
onError(event);
}
if (onLoadEnd) {
onLoadEnd(event);
}
// eslint-disable-next-line no-console
console.warn('Encountered an error loading page', event.nativeEvent);
this.setState({
lastErrorEvent: event.nativeEvent,
viewState: WebViewState.ERROR,
});
};
onLoadingFinish = (event: WebViewNavigationEvent) => {
const { onLoad, onLoadEnd } = this.props;
if (onLoad) {
onLoad(event);
}
if (onLoadEnd) {
onLoadEnd(event);
}
this.setState({
viewState: WebViewState.IDLE,
});
this.updateNavigationState(event);
};
onMessage = (event: WebViewMessageEvent) => {
const { onMessage } = this.props;
if (onMessage) {
onMessage(event);
}
};
onLoadingProgress = (event: NativeSyntheticEvent) => {
const { onLoadProgress } = this.props;
if (onLoadProgress) {
onLoadProgress(event);
}
};
onShouldStartLoadWithRequestCallback = (
shouldStart: boolean,
url: string,
lockIdentifier: number,
) => {
const nativeConfig = this.props.nativeConfig || {};
let { viewManager } = nativeConfig;
if (this.props.useWebKit) {
viewManager = viewManager || RNCWKWebViewManager;
} else {
viewManager = viewManager || RNCUIWebViewManager;
}
invariant(viewManager != null, 'viewManager expected to be non-null');
viewManager.startLoadWithResult(!!shouldStart, lockIdentifier);
};
showRedboxOnPropChanges = (
prevProps: WebViewSharedProps,
propName: keyof WebViewSharedProps,
) => {
if (this.props[propName] !== prevProps[propName]) {
console.error(
`Changes to property ${propName} do nothing after the initial render.`,
);
}
};
render() {
let otherView = null;
let scalesPageToFit;
if (this.props.useWebKit) {
({ scalesPageToFit } = this.props);
} else {
({ scalesPageToFit = true } = this.props);
}
if (this.state.viewState === WebViewState.LOADING) {
otherView = (this.props.renderLoading || defaultRenderLoading)();
} else if (this.state.viewState === WebViewState.ERROR) {
const errorEvent = this.state.lastErrorEvent;
if (errorEvent) {
otherView = (this.props.renderError || defaultRenderError)(
errorEvent.domain,
errorEvent.code,
errorEvent.description,
);
} else {
invariant(errorEvent != null, 'lastErrorEvent expected to be non-null');
}
} else if (this.state.viewState !== WebViewState.IDLE) {
console.error(
`RNCWebView invalid state encountered: ${this.state.viewState}`,
);
}
const webViewStyles = [styles.container, styles.webView, this.props.style];
if (
this.state.viewState === WebViewState.LOADING
|| this.state.viewState === WebViewState.ERROR
) {
// if we're in either LOADING or ERROR states, don't show the webView
webViewStyles.push(styles.hidden);
}
const nativeConfig = this.props.nativeConfig || {};
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
this.onShouldStartLoadWithRequestCallback,
this.props.originWhitelist,
this.props.onShouldStartLoadWithRequest,
);
const decelerationRate = processDecelerationRate(
this.props.decelerationRate,
);
let source: WebViewSource = this.props.source || {};
if (!this.props.source && this.props.html) {
source = { html: this.props.html };
} else if (!this.props.source && this.props.url) {
source = { uri: this.props.url };
}
const messagingEnabled = typeof this.props.onMessage === 'function';
let NativeWebView = nativeConfig.component;
if (this.props.useWebKit) {
NativeWebView = NativeWebView || RNCWKWebView;
} else {
NativeWebView = NativeWebView || RNCUIWebView;
}
const webView = (
);
return (
{webView}
{otherView}
);
}
}
export default WebView;