import React from 'react'; import { ActivityIndicator, StyleSheet, UIManager, View, requireNativeComponent, NativeModules, Image, NativeSyntheticEvent, findNodeHandle, } from 'react-native'; import invariant from 'invariant'; import WebViewShared from './WebViewShared'; import { WebViewSourceUri, WebViewError, WebViewErrorEvent, WebViewMessageEvent, WebViewNavigationEvent, WebViewSharedProps, WebViewSource, WebViewProgressEvent, } from './types/WebViewTypes'; 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, }, }); const WebViewState: { IDLE: 'IDLE'; LOADING: 'LOADING'; ERROR: 'ERROR'; } = { IDLE: 'IDLE', LOADING: 'LOADING', ERROR: 'ERROR', }; const isWebViewUriSource = ( source: WebViewSource, ): source is WebViewSourceUri => typeof source !== 'number' && !('html' in source); const defaultRenderLoading = (): React.ReactNode => ( ); type State = { viewState: 'IDLE' | 'LOADING' | 'ERROR'; lastErrorEvent: WebViewError | null; }; const RNCWebView = requireNativeComponent('RNCWebView'); /** * Renders a native WebView. */ export default class WebView extends React.Component< WebViewSharedProps, State > { static defaultProps = { overScrollMode: 'always', javaScriptEnabled: true, thirdPartyCookiesEnabled: true, scalesPageToFit: true, allowFileAccess: false, saveFormDataDisabled: false, originWhitelist: WebViewShared.defaultOriginWhitelist, }; static isFileUploadSupported = async (): Promise => // native implementation should return "true" only for Android 5+ NativeModules.RNCWebView.isFileUploadSupported(); state: State = { viewState: this.props.startInLoadingState ? WebViewState.LOADING : WebViewState.IDLE, lastErrorEvent: null, }; webViewRef = React.createRef(); goForward = (): void => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RNCWebView.Commands.goForward, null, ); }; goBack = (): void => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RNCWebView.Commands.goBack, null, ); }; reload = (): void => { this.setState({ viewState: WebViewState.LOADING, }); UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RNCWebView.Commands.reload, null, ); }; stopLoading = (): void => { UIManager.dispatchViewManagerCommand( this.getWebViewHandle(), UIManager.RNCWebView.Commands.stopLoading, null, ); }; postMessage = (data: string): void => { 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: string): void => { 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: WebViewNavigationEvent): void => { if (this.props.onNavigationStateChange) { this.props.onNavigationStateChange(event.nativeEvent); } }; getWebViewHandle = (): number | null => findNodeHandle(this.webViewRef.current); onLoadingStart = (event: WebViewNavigationEvent): void => { const { onLoadStart } = this.props; if (onLoadStart) { onLoadStart(event); } this.updateNavigationState(event); }; onLoadingError = (event: WebViewErrorEvent): void => { 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): void => { const { onLoad, onLoadEnd } = this.props; if (onLoad) { onLoad(event); } if (onLoadEnd) { onLoadEnd(event); } this.setState({ viewState: WebViewState.IDLE, }); this.updateNavigationState(event); }; onMessage = (event: WebViewMessageEvent): void => { const { onMessage } = this.props; if (onMessage) { onMessage(event); } }; onLoadingProgress = ( event: NativeSyntheticEvent, ): void => { const { onLoadProgress } = this.props; if (onLoadProgress) { onLoadProgress(event); } }; render(): React.ReactNode { 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; if (errorEvent) { otherView = this.props.renderError && this.props.renderError( errorEvent.domain, errorEvent.code, errorEvent.description, ); } else { invariant(errorEvent != null, 'lastErrorEvent expected to be non-null'); } } else if (this.state.viewState !== WebViewState.IDLE) { // eslint-disable-next-line no-console console.error( `RNCWebView 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 (isWebViewUriSource(source)) { if (source.method === 'POST' && source.headers) { // eslint-disable-next-line no-console console.warn( 'WebView: `source.headers` is not supported when using POST.', ); } else if (source.method === 'GET' && source.body) { // eslint-disable-next-line no-console console.warn('WebView: `source.body` is not supported when using GET.'); } } const nativeConfig = this.props.nativeConfig || {}; const originWhitelist = (this.props.originWhitelist || []).map( WebViewShared.originWhitelistToRegex, ); const NativeWebView = nativeConfig.component || RNCWebView; const webView = ( ); return ( {webView} {otherView} ); } }