/** * Copyright (c) 2018-present, Infinite Red, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * @format * @noflow */ 'use strict'; import React from 'react'; import PropTypes from 'prop-types'; import ReactNative from 'react-native' import { ActivityIndicator, EdgeInsetsPropType, Linking, ScrollView, StyleSheet, Text, UIManager, View, ViewPropTypes, requireNativeComponent, NativeModules, Image } from 'react-native'; import invariant from 'fbjs/lib/invariant'; import keyMirror from 'fbjs/lib/keyMirror'; import deprecatedPropType from 'deprecated-prop-type'; import WebViewShared from './WebViewShared'; const resolveAssetSource = Image.resolveAssetSource; // Imported from https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollView/processDecelerationRate.js function processDecelerationRate(decelerationRate) { if (decelerationRate === 'normal') { decelerationRate = 0.998; } else if (decelerationRate === 'fast') { decelerationRate = 0.99; } return decelerationRate; } const RNCWebViewManager = NativeModules.WebViewManager; const BGWASH = 'rgba(255,255,255,0.8)'; const RCT_WEBVIEW_REF = 'webview'; const WebViewState = keyMirror({ IDLE: null, LOADING: null, ERROR: null, }); const NavigationType = keyMirror({ click: true, formsubmit: true, backforward: true, reload: true, formresubmit: true, other: true, }); const JSNavigationScheme = 'react-js-navigation'; // type ErrorEvent = { // domain: any, // code: any, // description: any, // }; // type Event = Object; const DataDetectorTypes = [ 'phoneNumber', 'link', 'address', 'calendarEvent', 'none', 'all', ]; const defaultRenderLoading = () => ( ); const defaultRenderError = (errorDomain, errorCode, errorDesc) => ( 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 propTypes = { ...ViewPropTypes, html: deprecatedPropType( PropTypes.string, 'Use the `source` prop instead.', ), url: deprecatedPropType(PropTypes.string, 'Use the `source` prop instead.'), /** * Loads static html or a uri (with optional headers) in the WebView. */ source: PropTypes.oneOfType([ PropTypes.shape({ /* * The URI to load in the `WebView`. Can be a local or remote file. */ uri: PropTypes.string, /* * The HTTP Method to use. Defaults to GET if not specified. * NOTE: On Android, only GET and POST are supported. */ method: PropTypes.string, /* * Additional HTTP headers to send with the request. * NOTE: On Android, this can only be used with GET requests. */ headers: PropTypes.object, /* * The HTTP body to send with the request. This must be a valid * UTF-8 string, and will be sent exactly as specified, with no * additional encoding (e.g. URL-escaping or base64) applied. * NOTE: On Android, this can only be used with POST requests. */ body: PropTypes.string, }), PropTypes.shape({ /* * A static HTML page to display in the WebView. */ html: PropTypes.string, /* * The base URL to be used for any relative links in the HTML. */ baseUrl: PropTypes.string, }), /* * Used internally by packager. */ PropTypes.number, ]), /** * Function that returns a view to show if there's an error. */ renderError: PropTypes.func, // view to show if there's an error /** * Function that returns a loading indicator. */ renderLoading: PropTypes.func, /** * Function that is invoked when the `WebView` has finished loading. */ onLoad: PropTypes.func, /** * Function that is invoked when the `WebView` load succeeds or fails. */ onLoadEnd: PropTypes.func, /** * Function that is invoked when the `WebView` starts loading. */ onLoadStart: PropTypes.func, /** * Function that is invoked when the `WebView` load fails. */ onError: PropTypes.func, /** * Boolean value that determines whether the web view bounces * when it reaches the edge of the content. The default value is `true`. * @platform ios */ bounces: PropTypes.bool, /** * A floating-point number that determines how quickly the scroll view * decelerates after the user lifts their finger. You may also use the * string shortcuts `"normal"` and `"fast"` which match the underlying iOS * settings for `UIScrollViewDecelerationRateNormal` and * `UIScrollViewDecelerationRateFast` respectively: * * - normal: 0.998 * - fast: 0.99 (the default for iOS web view) * @platform ios */ decelerationRate: PropTypes.oneOfType([ PropTypes.oneOf(['fast', 'normal']), PropTypes.number, ]), /** * Boolean value that determines whether scrolling is enabled in the * `WebView`. The default value is `true`. * @platform ios */ scrollEnabled: PropTypes.bool, /** * Controls whether to adjust the content inset for web views that are * placed behind a navigation bar, tab bar, or toolbar. The default value * is `true`. */ automaticallyAdjustContentInsets: PropTypes.bool, /** * The amount by which the web view content is inset from the edges of * the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}. * @platform ios */ contentInset: EdgeInsetsPropType, /** * Function that is invoked when the `WebView` loading starts or ends. */ onNavigationStateChange: PropTypes.func, /** * A function that is invoked when the webview calls `window.postMessage`. * Setting this property will inject a `postMessage` global into your * webview, but will still call pre-existing values of `postMessage`. * * `window.postMessage` accepts one argument, `data`, which will be * available on the event object, `event.nativeEvent.data`. `data` * must be a string. */ onMessage: PropTypes.func, /** * Boolean value that forces the `WebView` to show the loading view * on the first load. */ startInLoadingState: PropTypes.bool, /** * The style to apply to the `WebView`. */ style: ViewPropTypes.style, /** * Determines the types of data converted to clickable URLs in the web view's content. * By default only phone numbers are detected. * * You can provide one type or an array of many types. * * Possible values for `dataDetectorTypes` are: * * - `'phoneNumber'` * - `'link'` * - `'address'` * - `'calendarEvent'` * - `'none'` * - `'all'` * * @platform ios */ dataDetectorTypes: PropTypes.oneOfType([ PropTypes.oneOf(DataDetectorTypes), PropTypes.arrayOf(PropTypes.oneOf(DataDetectorTypes)), ]), /** * Boolean value to enable JavaScript in the `WebView`. Used on Android only * as JavaScript is enabled by default on iOS. The default value is `true`. * @platform android */ javaScriptEnabled: PropTypes.bool, /** * Boolean value to enable third party cookies in the `WebView`. Used on * Android Lollipop and above only as third party cookies are enabled by * default on Android Kitkat and below and on iOS. The default value is `true`. * @platform android */ thirdPartyCookiesEnabled: PropTypes.bool, /** * Boolean value to control whether DOM Storage is enabled. Used only in * Android. * @platform android */ domStorageEnabled: PropTypes.bool, /** * Set this to provide JavaScript that will be injected into the web page * when the view loads. */ injectedJavaScript: PropTypes.string, /** * Sets the user-agent for the `WebView`. * @platform android */ userAgent: PropTypes.string, /** * Boolean that controls whether the web content is scaled to fit * the view and enables the user to change the scale. The default value * is `true`. */ scalesPageToFit: PropTypes.bool, /** * Function that allows custom handling of any web view requests. Return * `true` from the function to continue loading the request and `false` * to stop loading. * @platform ios */ onShouldStartLoadWithRequest: PropTypes.func, /** * Boolean that determines whether HTML5 videos play inline or use the * native full-screen controller. The default value is `false`. * * **NOTE** : In order for video to play inline, not only does this * property need to be set to `true`, but the video element in the HTML * document must also include the `webkit-playsinline` attribute. * @platform ios */ allowsInlineMediaPlayback: PropTypes.bool, /** * Boolean that determines whether HTML5 audio and video requires the user * to tap them before they start playing. The default value is `true`. */ mediaPlaybackRequiresUserAction: PropTypes.bool, /** * List of origin strings to allow being navigated to. The strings allow * wildcards and get matched against *just* the origin (not the full URL). * If the user taps to navigate to a new page but the new page is not in * this whitelist, we will open the URL in Safari. * The default whitelisted origins are "http://*" and "https://*". */ originWhitelist: PropTypes.arrayOf(PropTypes.string), /** * Function that accepts a string that will be passed to the WebView and * executed immediately as JavaScript. */ injectJavaScript: PropTypes.func, /** * Specifies the mixed content mode. i.e WebView will allow a secure origin to load content from any other origin. * * Possible values for `mixedContentMode` are: * * - `'never'` (default) - WebView will not allow a secure origin to load content from an insecure origin. * - `'always'` - WebView will allow a secure origin to load content from any other origin, even if that origin is insecure. * - `'compatibility'` - WebView will attempt to be compatible with the approach of a modern web browser with regard to mixed content. * @platform android */ mixedContentMode: PropTypes.oneOf(['never', 'always', 'compatibility']), /** * Override the native component used to render the WebView. Enables a custom native * WebView which uses the same JavaScript as the original WebView. */ nativeConfig: PropTypes.shape({ /* * The native component used to render the WebView. */ component: PropTypes.any, /* * Set props directly on the native component WebView. Enables custom props which the * original WebView doesn't pass through. */ props: PropTypes.object, /* * Set the ViewManager to use for communication with the native side. * @platform ios */ viewManager: PropTypes.object, }), }; static defaultProps = { originWhitelist: WebViewShared.defaultOriginWhitelist, scalesPageToFit: true, }; 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 || defaultRenderError)( errorEvent.domain, errorEvent.code, errorEvent.description, ); } else if (this.state.viewState !== WebViewState.IDLE) { console.error( 'RNCWebView invalid state encountered: ' + this.state.loading, ); } 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 viewManager = nativeConfig.viewManager || RNCWebViewManager; const compiledWhitelist = (this.props.originWhitelist || []).map( WebViewShared.originWhitelistToRegex, ); const onShouldStartLoadWithRequest = (event) => { let shouldStart = true; const { url } = event.nativeEvent; const origin = WebViewShared.extractOrigin(url); const passesWhitelist = compiledWhitelist.some(x => new RegExp(x).test(origin), ); shouldStart = shouldStart && passesWhitelist; if (!passesWhitelist) { Linking.openURL(url); } if (this.props.onShouldStartLoadWithRequest) { shouldStart = shouldStart && this.props.onShouldStartLoadWithRequest(event.nativeEvent); } viewManager.startLoadWithResult( !!shouldStart, event.nativeEvent.lockIdentifier, ); }; const decelerationRate = processDecelerationRate( this.props.decelerationRate, ); const source = this.props.source || {}; if (this.props.html) { source.html = this.props.html; } else if (this.props.url) { source.uri = this.props.url; } const messagingEnabled = typeof this.props.onMessage === 'function'; const NativeWebView = nativeConfig.component || RNCWebView; const webView = ( ); return ( {webView} {otherView} ); } /** * Go forward one page in the web view's history. */ goForward = () => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RNCWebView.Commands.goForward, null, ); }; /** * Go back one page in the web view's history. */ goBack = () => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RNCWebView.Commands.goBack, null, ); }; /** * Reloads the current page. */ reload = () => { this.setState({ viewState: WebViewState.LOADING }); UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RNCWebView.Commands.reload, null, ); }; /** * Stop loading the current page. */ stopLoading = () => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RNCWebView.Commands.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 => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RNCWebView.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 => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RNCWebView.Commands.injectJavaScript, [data], ); }; /** * We return an event with a bunch of fields including: * url, title, loading, canGoBack, canGoForward */ _updateNavigationState = (event) => { if (this.props.onNavigationStateChange) { this.props.onNavigationStateChange(event.nativeEvent); } }; /** * Returns the native `WebView` node. */ getWebViewHandle = () => { return ReactNative.findNodeHandle(this.refs[RCT_WEBVIEW_REF]); }; _onLoadingStart = (event) => { const onLoadStart = this.props.onLoadStart; onLoadStart && onLoadStart(event); this._updateNavigationState(event); }; _onLoadingError = (event) => { 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) => { const { onLoad, onLoadEnd } = this.props; onLoad && onLoad(event); onLoadEnd && onLoadEnd(event); this.setState({ viewState: WebViewState.IDLE, }); this._updateNavigationState(event); }; _onMessage = (event) => { const { onMessage } = this.props; onMessage && onMessage(event); }; } const RNCWebView = requireNativeComponent('RNCWebView'); 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', }, }); module.exports = WebView;