/** * 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 { WebViewErrorEvent, WebViewEvent, 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: ?WebViewErrorEvent, startInLoadingState: boolean, |}; type WebViewPropsAndroid = $ReadOnly<{| ...WebViewSharedProps, onNavigationStateChange?: (event: WebViewEvent) => any, onContentSizeChange?: (event: WebViewEvent) => any, /** * Sets whether Geolocation is enabled. The default is false. * @platform android */ geolocationEnabled?: ?boolean, /** * Boolean that sets whether JavaScript running in the context of a file * scheme URL should be allowed to access content from any origin. * Including accessing content from other file scheme URLs * @platform android */ allowUniversalAccessFromFileURLs?: ?boolean, /** * Used on Android only, controls whether form autocomplete data should be saved * @platform android */ saveFormDataDisabled?: ?boolean, /* * Used on Android only, controls whether the given list of URL prefixes should * make {@link com.facebook.react.views.webview.ReactWebViewClient} to launch a * default activity intent for those URL instead of loading it within the webview. * Use this to list URLs that WebView cannot handle, e.g. a PDF url. * @platform android */ urlPrefixesForDefaultIntent?: $ReadOnlyArray, |}>; /** * Renders a native WebView. */ class WebView extends React.Component { static defaultProps = { javaScriptEnabled: true, thirdPartyCookiesEnabled: true, scalesPageToFit: true, saveFormDataDisabled: false, originWhitelist: WebViewShared.defaultOriginWhitelist, }; state = { viewState: WebViewState.IDLE, lastErrorEvent: (null: ?WebViewErrorEvent), 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 = this.props.source || ({}: WebViewSource); 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: WebViewEvent) => { if (this.props.onNavigationStateChange) { this.props.onNavigationStateChange(event.nativeEvent); } }; getWebViewHandle = () => { return ReactNative.findNodeHandle(this.refs[RCT_WEBVIEW_REF]); }; onLoadingStart = (event: WebViewEvent) => { const onLoadStart = this.props.onLoadStart; onLoadStart && onLoadStart(event); this.updateNavigationState(event); }; onLoadingError = (event: WebViewEvent) => { 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: WebViewEvent) => { const { onLoad, onLoadEnd } = this.props; onLoad && onLoad(event); onLoadEnd && onLoadEnd(event); this.setState({ viewState: WebViewState.IDLE, }); this.updateNavigationState(event); }; onMessage = (event: WebViewEvent) => { 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;