/**
* Copyright (c) 2015-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
'use strict';
import React from 'react';
import ReactNative from 'react-native';
import {
ActivityIndicator,
StyleSheet,
UIManager,
View,
Image,
requireNativeComponent
} from 'react-native';
import invariant from 'fbjs/lib/invariant';
import keyMirror from 'fbjs/lib/keyMirror';
import WebViewShared from './WebViewShared';
import type {
WebViewEvent,
WebViewError,
WebViewErrorEvent,
WebViewMessageEvent,
WebViewNavigation,
WebViewNavigationEvent,
WebViewSharedProps,
WebViewSource,
} from './WebViewTypes';
const resolveAssetSource = Image.resolveAssetSource;
const RCT_WEBVIEW_REF = 'webview';
const WebViewState = keyMirror({
IDLE: null,
LOADING: null,
ERROR: null,
});
const defaultRenderLoading = () => (
);
type State = {|
viewState: WebViewState,
lastErrorEvent: ?WebViewError,
startInLoadingState: boolean,
|};
/**
* Renders a native WebView.
*/
class WebView extends React.Component {
static defaultProps = {
javaScriptEnabled: true,
thirdPartyCookiesEnabled: true,
scalesPageToFit: true,
allowFileAccess: false,
saveFormDataDisabled: false,
originWhitelist: WebViewShared.defaultOriginWhitelist,
};
state = {
viewState: WebViewState.IDLE,
lastErrorEvent: null,
startInLoadingState: true,
};
UNSAFE_componentWillMount() {
if (this.props.startInLoadingState) {
this.setState({ viewState: WebViewState.LOADING });
}
}
render() {
let otherView = null;
if (this.state.viewState === WebViewState.LOADING) {
otherView = (this.props.renderLoading || defaultRenderLoading)();
} else if (this.state.viewState === WebViewState.ERROR) {
const errorEvent = this.state.lastErrorEvent;
invariant(errorEvent != null, 'lastErrorEvent expected to be non-null');
otherView =
this.props.renderError &&
this.props.renderError(
errorEvent.domain,
errorEvent.code,
errorEvent.description,
);
} else if (this.state.viewState !== WebViewState.IDLE) {
console.error(
'RCTWebView invalid state encountered: ' + this.state.viewState,
);
}
const webViewStyles = [styles.container, 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);
}
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 };
}
if (source.method === 'POST' && source.headers) {
console.warn(
'WebView: `source.headers` is not supported when using POST.',
);
} else if (source.method === 'GET' && source.body) {
console.warn('WebView: `source.body` is not supported when using GET.');
}
const nativeConfig = this.props.nativeConfig || {};
const originWhitelist = (this.props.originWhitelist || []).map(
WebViewShared.originWhitelistToRegex,
);
let NativeWebView = nativeConfig.component || RCTWebView;
const webView = (
);
return (
{webView}
{otherView}
);
}
goForward = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.goForward,
null,
);
};
goBack = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.goBack,
null,
);
};
reload = () => {
this.setState({
viewState: WebViewState.LOADING,
});
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.reload,
null,
);
};
stopLoading = () => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.stopLoading,
null,
);
};
postMessage = (data: string) => {
UIManager.dispatchViewManagerCommand(
this.getWebViewHandle(),
UIManager.RCTWebView.Commands.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(),
UIManager.RCTWebView.Commands.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);
}
};
getWebViewHandle = () => {
return ReactNative.findNodeHandle(this.refs[RCT_WEBVIEW_REF]);
};
onLoadingStart = (event: WebViewNavigationEvent) => {
const onLoadStart = this.props.onLoadStart;
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;
onError && onError(event);
onLoadEnd && onLoadEnd(event);
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;
onLoad && onLoad(event);
onLoadEnd && onLoadEnd(event);
this.setState({
viewState: WebViewState.IDLE,
});
this.updateNavigationState(event);
};
onMessage = (event: WebViewMessageEvent) => {
const { onMessage } = this.props;
onMessage && onMessage(event);
};
}
const RCTWebView = requireNativeComponent('RCTWebView');
const styles = StyleSheet.create({
container: {
flex: 1,
},
hidden: {
height: 0,
flex: 0, // disable 'flex:1' when hiding a View
},
loadingView: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
loadingProgressBar: {
height: 20,
},
});
module.exports = WebView;