/**
* 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.
*
*/
import React from 'react';
import {
ActivityIndicator,
Text,
UIManager as NotTypedUIManager,
View,
requireNativeComponent,
NativeModules,
Image,
findNodeHandle,
ImageSourcePropType,
} from 'react-native';
import invariant from 'invariant';
import {
defaultOriginWhitelist,
createOnShouldStartLoadWithRequest,
getViewManagerConfig,
} from './WebViewShared';
import {
WebViewErrorEvent,
WebViewMessageEvent,
WebViewNavigationEvent,
WebViewProgressEvent,
IOSWebViewProps,
DecelerationRateConstant,
NativeWebViewIOS,
ViewManager,
State,
CustomUIManager,
WebViewNativeConfig,
} from './WebViewTypes';
import styles from './WebView.styles';
const UIManager = NotTypedUIManager as CustomUIManager;
const { resolveAssetSource } = Image;
let didWarnAboutUIWebViewUsage = false;
// Imported from https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollView/processDecelerationRate.js
const processDecelerationRate = (
decelerationRate: DecelerationRateConstant | number | undefined,
) => {
let newDecelerationRate = decelerationRate;
if (newDecelerationRate === 'normal') {
newDecelerationRate = 0.998;
} else if (newDecelerationRate === 'fast') {
newDecelerationRate = 0.99;
}
return newDecelerationRate;
};
const RNCUIWebViewManager = NativeModules.RNCUIWebViewManager as ViewManager;
const RNCWKWebViewManager = NativeModules.RNCWKWebViewManager as ViewManager;
const RNCUIWebView: typeof NativeWebViewIOS = requireNativeComponent(
'RNCUIWebView',
);
const RNCWKWebView: typeof NativeWebViewIOS = requireNativeComponent(
'RNCWKWebView',
);
const defaultRenderLoading = () => (
);
const defaultRenderError = (
errorDomain: string | undefined,
errorCode: number,
errorDesc: string,
) => (
Error loading page
{`Domain: ${errorDomain}`}
{`Error Code: ${errorCode}`}
{`Description: ${errorDesc}`}
);
class WebView extends React.Component {
static defaultProps = {
useWebKit: true,
cacheEnabled: true,
originWhitelist: defaultOriginWhitelist,
useSharedProcessPool: true,
};
static isFileUploadSupported = async () => {
// no native implementation for iOS, depends only on permissions
return true;
};
state: State = {
viewState: this.props.startInLoadingState ? 'LOADING' : 'IDLE',
lastErrorEvent: null,
};
webViewRef = React.createRef();
// eslint-disable-next-line camelcase
UNSAFE_componentWillMount() {
if (!this.props.useWebKit && !didWarnAboutUIWebViewUsage) {
didWarnAboutUIWebViewUsage = true;
console.warn(
'UIWebView is deprecated and will be removed soon, please use WKWebView (do not override useWebkit={true} prop),'
+ ' more infos here: https://github.com/react-native-community/react-native-webview/issues/312',
);
}
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',
);
}
if (!this.props.useWebKit && this.props.incognito) {
console.warn(
'The incognito property is not supported when useWebKit = false',
);
}
}
// eslint-disable-next-line react/sort-comp
getCommands = () =>
!this.props.useWebKit
? getViewManagerConfig('RNCUIWebView').Commands
: getViewManagerConfig('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: '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 = () => {
const nodeHandle = findNodeHandle(this.webViewRef.current);
invariant(nodeHandle != null, 'nodeHandle expected to be non-null');
return nodeHandle as number;
};
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 (onLoadEnd) {
onLoadEnd(event);
}
if (onError) {
onError(event);
}
console.warn('Encountered an error loading page', event.nativeEvent);
this.setState({
lastErrorEvent: event.nativeEvent,
viewState: 'ERROR',
});
};
onLoadingFinish = (event: WebViewNavigationEvent) => {
const { onLoad, onLoadEnd } = this.props;
if (onLoad) {
onLoad(event);
}
if (onLoadEnd) {
onLoadEnd(event);
}
this.setState({
viewState: 'IDLE',
});
this.updateNavigationState(event);
};
onMessage = (event: WebViewMessageEvent) => {
const { onMessage } = this.props;
if (onMessage) {
onMessage(event);
}
};
onLoadingProgress = (event: WebViewProgressEvent) => {
const { onLoadProgress } = this.props;
if (onLoadProgress) {
onLoadProgress(event);
}
};
onShouldStartLoadWithRequestCallback = (
shouldStart: boolean,
_url: string,
lockIdentifier: number,
) => {
let { viewManager }: WebViewNativeConfig = this.props.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);
};
componentDidUpdate(prevProps: IOSWebViewProps) {
if (!(prevProps.useWebKit && this.props.useWebKit)) {
return;
}
this.showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback');
this.showRedboxOnPropChanges(prevProps, 'incognito');
this.showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction');
this.showRedboxOnPropChanges(prevProps, 'dataDetectorTypes');
if (this.props.scalesPageToFit !== undefined) {
console.warn(
'The scalesPageToFit property is not supported when useWebKit = true',
);
}
}
showRedboxOnPropChanges(
prevProps: IOSWebViewProps,
propName: keyof IOSWebViewProps,
) {
if (this.props[propName] !== prevProps[propName]) {
console.error(
`Changes to property ${propName} do nothing after the initial render.`,
);
}
}
render() {
const {
decelerationRate: decelerationRateProp,
nativeConfig = {},
onMessage,
onShouldStartLoadWithRequest: onShouldStartLoadWithRequestProp,
originWhitelist,
renderError,
renderLoading,
scalesPageToFit = this.props.useWebKit ? undefined : true,
style,
useWebKit,
...otherProps
} = this.props;
let otherView = null;
if (this.state.viewState === 'LOADING') {
otherView = (renderLoading || defaultRenderLoading)();
} else if (this.state.viewState === 'ERROR') {
const errorEvent = this.state.lastErrorEvent;
invariant(errorEvent != null, 'lastErrorEvent expected to be non-null');
otherView = (renderError || defaultRenderError)(
errorEvent.domain,
errorEvent.code,
errorEvent.description,
);
} else if (this.state.viewState !== 'IDLE') {
console.error(
`RNCWebView invalid state encountered: ${this.state.viewState}`,
);
}
const webViewStyles = [styles.container, styles.webView, style];
if (
this.state.viewState === 'LOADING'
|| this.state.viewState === 'ERROR'
) {
// if we're in either LOADING or ERROR states, don't show the webView
webViewStyles.push(styles.hidden);
}
const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
this.onShouldStartLoadWithRequestCallback,
// casting cause it's in the default props
originWhitelist as ReadonlyArray,
onShouldStartLoadWithRequestProp,
);
const decelerationRate = processDecelerationRate(decelerationRateProp);
let NativeWebView = nativeConfig.component as typeof NativeWebViewIOS;
if (useWebKit) {
NativeWebView = NativeWebView || RNCWKWebView;
} else {
NativeWebView = NativeWebView || RNCUIWebView;
}
const webView = (
);
return (
{webView}
{otherView}
);
}
}
export default WebView;