No Description

WebView.ios.js 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691
  1. 'use strict';
  2. import React from 'react';
  3. import PropTypes from 'prop-types';
  4. import ReactNative from 'react-native'
  5. import {
  6. ActivityIndicator,
  7. EdgeInsetsPropType,
  8. Linking,
  9. StyleSheet,
  10. Text,
  11. UIManager,
  12. View,
  13. ViewPropTypes,
  14. requireNativeComponent,
  15. NativeModules,
  16. Image
  17. } from 'react-native';
  18. import invariant from 'fbjs/lib/invariant';
  19. import keyMirror from 'fbjs/lib/keyMirror';
  20. import deprecatedPropType from 'deprecated-prop-type';
  21. import WebViewShared from './WebViewShared';
  22. const resolveAssetSource = Image.resolveAssetSource;
  23. // Imported from https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollView/processDecelerationRate.js
  24. function processDecelerationRate(decelerationRate) {
  25. if (decelerationRate === 'normal') {
  26. decelerationRate = 0.998;
  27. } else if (decelerationRate === 'fast') {
  28. decelerationRate = 0.99;
  29. }
  30. return decelerationRate;
  31. }
  32. const RNCWebViewManager = NativeModules.WebViewManager;
  33. const BGWASH = 'rgba(255,255,255,0.8)';
  34. const RCT_WEBVIEW_REF = 'webview';
  35. const WebViewState = keyMirror({
  36. IDLE: null,
  37. LOADING: null,
  38. ERROR: null,
  39. });
  40. const NavigationType = keyMirror({
  41. click: true,
  42. formsubmit: true,
  43. backforward: true,
  44. reload: true,
  45. formresubmit: true,
  46. other: true,
  47. });
  48. const JSNavigationScheme = 'react-js-navigation';
  49. // type ErrorEvent = {
  50. // domain: any,
  51. // code: any,
  52. // description: any,
  53. // };
  54. // type Event = Object;
  55. const DataDetectorTypes = [
  56. 'phoneNumber',
  57. 'link',
  58. 'address',
  59. 'calendarEvent',
  60. 'none',
  61. 'all',
  62. ];
  63. const defaultRenderLoading = () => (
  64. <View style={styles.loadingView}>
  65. <ActivityIndicator />
  66. </View>
  67. );
  68. const defaultRenderError = (errorDomain, errorCode, errorDesc) => (
  69. <View style={styles.errorContainer}>
  70. <Text style={styles.errorTextTitle}>Error loading page</Text>
  71. <Text style={styles.errorText}>{'Domain: ' + errorDomain}</Text>
  72. <Text style={styles.errorText}>{'Error Code: ' + errorCode}</Text>
  73. <Text style={styles.errorText}>{'Description: ' + errorDesc}</Text>
  74. </View>
  75. );
  76. /**
  77. * `WebView` renders web content in a native view.
  78. *
  79. *```
  80. * import React, { Component } from 'react';
  81. * import { WebView } from 'react-native';
  82. *
  83. * class MyWeb extends Component {
  84. * render() {
  85. * return (
  86. * <WebView
  87. * source={{uri: 'https://github.com/facebook/react-native'}}
  88. * style={{marginTop: 20}}
  89. * />
  90. * );
  91. * }
  92. * }
  93. *```
  94. *
  95. * You can use this component to navigate back and forth in the web view's
  96. * history and configure various properties for the web content.
  97. */
  98. class WebView extends React.Component {
  99. static JSNavigationScheme = JSNavigationScheme;
  100. static NavigationType = NavigationType;
  101. static propTypes = {
  102. ...ViewPropTypes,
  103. html: deprecatedPropType(
  104. PropTypes.string,
  105. 'Use the `source` prop instead.',
  106. ),
  107. url: deprecatedPropType(PropTypes.string, 'Use the `source` prop instead.'),
  108. /**
  109. * Loads static html or a uri (with optional headers) in the WebView.
  110. */
  111. source: PropTypes.oneOfType([
  112. PropTypes.shape({
  113. /*
  114. * The URI to load in the `WebView`. Can be a local or remote file.
  115. */
  116. uri: PropTypes.string,
  117. /*
  118. * The HTTP Method to use. Defaults to GET if not specified.
  119. * NOTE: On Android, only GET and POST are supported.
  120. */
  121. method: PropTypes.string,
  122. /*
  123. * Additional HTTP headers to send with the request.
  124. * NOTE: On Android, this can only be used with GET requests.
  125. */
  126. headers: PropTypes.object,
  127. /*
  128. * The HTTP body to send with the request. This must be a valid
  129. * UTF-8 string, and will be sent exactly as specified, with no
  130. * additional encoding (e.g. URL-escaping or base64) applied.
  131. * NOTE: On Android, this can only be used with POST requests.
  132. */
  133. body: PropTypes.string,
  134. }),
  135. PropTypes.shape({
  136. /*
  137. * A static HTML page to display in the WebView.
  138. */
  139. html: PropTypes.string,
  140. /*
  141. * The base URL to be used for any relative links in the HTML.
  142. */
  143. baseUrl: PropTypes.string,
  144. }),
  145. /*
  146. * Used internally by packager.
  147. */
  148. PropTypes.number,
  149. ]),
  150. /**
  151. * Function that returns a view to show if there's an error.
  152. */
  153. renderError: PropTypes.func, // view to show if there's an error
  154. /**
  155. * Function that returns a loading indicator.
  156. */
  157. renderLoading: PropTypes.func,
  158. /**
  159. * Function that is invoked when the `WebView` has finished loading.
  160. */
  161. onLoad: PropTypes.func,
  162. /**
  163. * Function that is invoked when the `WebView` load succeeds or fails.
  164. */
  165. onLoadEnd: PropTypes.func,
  166. /**
  167. * Function that is invoked when the `WebView` starts loading.
  168. */
  169. onLoadStart: PropTypes.func,
  170. /**
  171. * Function that is invoked when the `WebView` load fails.
  172. */
  173. onError: PropTypes.func,
  174. /**
  175. * Boolean value that determines whether the web view bounces
  176. * when it reaches the edge of the content. The default value is `true`.
  177. * @platform ios
  178. */
  179. bounces: PropTypes.bool,
  180. /**
  181. * A floating-point number that determines how quickly the scroll view
  182. * decelerates after the user lifts their finger. You may also use the
  183. * string shortcuts `"normal"` and `"fast"` which match the underlying iOS
  184. * settings for `UIScrollViewDecelerationRateNormal` and
  185. * `UIScrollViewDecelerationRateFast` respectively:
  186. *
  187. * - normal: 0.998
  188. * - fast: 0.99 (the default for iOS web view)
  189. * @platform ios
  190. */
  191. decelerationRate: PropTypes.oneOfType([
  192. PropTypes.oneOf(['fast', 'normal']),
  193. PropTypes.number,
  194. ]),
  195. /**
  196. * Boolean value that determines whether scrolling is enabled in the
  197. * `WebView`. The default value is `true`.
  198. * @platform ios
  199. */
  200. scrollEnabled: PropTypes.bool,
  201. /**
  202. * Controls whether to adjust the content inset for web views that are
  203. * placed behind a navigation bar, tab bar, or toolbar. The default value
  204. * is `true`.
  205. */
  206. automaticallyAdjustContentInsets: PropTypes.bool,
  207. /**
  208. * The amount by which the web view content is inset from the edges of
  209. * the scroll view. Defaults to {top: 0, left: 0, bottom: 0, right: 0}.
  210. * @platform ios
  211. */
  212. contentInset: EdgeInsetsPropType,
  213. /**
  214. * Function that is invoked when the `WebView` loading starts or ends.
  215. */
  216. onNavigationStateChange: PropTypes.func,
  217. /**
  218. * A function that is invoked when the webview calls `window.postMessage`.
  219. * Setting this property will inject a `postMessage` global into your
  220. * webview, but will still call pre-existing values of `postMessage`.
  221. *
  222. * `window.postMessage` accepts one argument, `data`, which will be
  223. * available on the event object, `event.nativeEvent.data`. `data`
  224. * must be a string.
  225. */
  226. onMessage: PropTypes.func,
  227. /**
  228. * Boolean value that forces the `WebView` to show the loading view
  229. * on the first load.
  230. */
  231. startInLoadingState: PropTypes.bool,
  232. /**
  233. * The style to apply to the `WebView`.
  234. */
  235. style: ViewPropTypes.style,
  236. /**
  237. * Determines the types of data converted to clickable URLs in the web view's content.
  238. * By default only phone numbers are detected.
  239. *
  240. * You can provide one type or an array of many types.
  241. *
  242. * Possible values for `dataDetectorTypes` are:
  243. *
  244. * - `'phoneNumber'`
  245. * - `'link'`
  246. * - `'address'`
  247. * - `'calendarEvent'`
  248. * - `'none'`
  249. * - `'all'`
  250. *
  251. * @platform ios
  252. */
  253. dataDetectorTypes: PropTypes.oneOfType([
  254. PropTypes.oneOf(DataDetectorTypes),
  255. PropTypes.arrayOf(PropTypes.oneOf(DataDetectorTypes)),
  256. ]),
  257. /**
  258. * Boolean value to enable JavaScript in the `WebView`. Used on Android only
  259. * as JavaScript is enabled by default on iOS. The default value is `true`.
  260. * @platform android
  261. */
  262. javaScriptEnabled: PropTypes.bool,
  263. /**
  264. * Boolean value to enable third party cookies in the `WebView`. Used on
  265. * Android Lollipop and above only as third party cookies are enabled by
  266. * default on Android Kitkat and below and on iOS. The default value is `true`.
  267. * @platform android
  268. */
  269. thirdPartyCookiesEnabled: PropTypes.bool,
  270. /**
  271. * Boolean value to control whether DOM Storage is enabled. Used only in
  272. * Android.
  273. * @platform android
  274. */
  275. domStorageEnabled: PropTypes.bool,
  276. /**
  277. * Set this to provide JavaScript that will be injected into the web page
  278. * when the view loads.
  279. */
  280. injectedJavaScript: PropTypes.string,
  281. /**
  282. * Sets the user-agent for the `WebView`.
  283. * @platform android
  284. */
  285. userAgent: PropTypes.string,
  286. /**
  287. * Boolean that controls whether the web content is scaled to fit
  288. * the view and enables the user to change the scale. The default value
  289. * is `true`.
  290. */
  291. scalesPageToFit: PropTypes.bool,
  292. /**
  293. * Function that allows custom handling of any web view requests. Return
  294. * `true` from the function to continue loading the request and `false`
  295. * to stop loading.
  296. * @platform ios
  297. */
  298. onShouldStartLoadWithRequest: PropTypes.func,
  299. /**
  300. * Boolean that determines whether HTML5 videos play inline or use the
  301. * native full-screen controller. The default value is `false`.
  302. *
  303. * **NOTE** : In order for video to play inline, not only does this
  304. * property need to be set to `true`, but the video element in the HTML
  305. * document must also include the `webkit-playsinline` attribute.
  306. * @platform ios
  307. */
  308. allowsInlineMediaPlayback: PropTypes.bool,
  309. /**
  310. * Boolean that determines whether HTML5 audio and video requires the user
  311. * to tap them before they start playing. The default value is `true`.
  312. */
  313. mediaPlaybackRequiresUserAction: PropTypes.bool,
  314. /**
  315. * List of origin strings to allow being navigated to. The strings allow
  316. * wildcards and get matched against *just* the origin (not the full URL).
  317. * If the user taps to navigate to a new page but the new page is not in
  318. * this whitelist, we will open the URL in Safari.
  319. * The default whitelisted origins are "http://*" and "https://*".
  320. */
  321. originWhitelist: PropTypes.arrayOf(PropTypes.string),
  322. /**
  323. * Function that accepts a string that will be passed to the WebView and
  324. * executed immediately as JavaScript.
  325. */
  326. injectJavaScript: PropTypes.func,
  327. /**
  328. * Specifies the mixed content mode. i.e WebView will allow a secure origin to load content from any other origin.
  329. *
  330. * Possible values for `mixedContentMode` are:
  331. *
  332. * - `'never'` (default) - WebView will not allow a secure origin to load content from an insecure origin.
  333. * - `'always'` - WebView will allow a secure origin to load content from any other origin, even if that origin is insecure.
  334. * - `'compatibility'` - WebView will attempt to be compatible with the approach of a modern web browser with regard to mixed content.
  335. * @platform android
  336. */
  337. mixedContentMode: PropTypes.oneOf(['never', 'always', 'compatibility']),
  338. /**
  339. * Override the native component used to render the WebView. Enables a custom native
  340. * WebView which uses the same JavaScript as the original WebView.
  341. */
  342. nativeConfig: PropTypes.shape({
  343. /*
  344. * The native component used to render the WebView.
  345. */
  346. component: PropTypes.any,
  347. /*
  348. * Set props directly on the native component WebView. Enables custom props which the
  349. * original WebView doesn't pass through.
  350. */
  351. props: PropTypes.object,
  352. /*
  353. * Set the ViewManager to use for communication with the native side.
  354. * @platform ios
  355. */
  356. viewManager: PropTypes.object,
  357. }),
  358. };
  359. static defaultProps = {
  360. originWhitelist: WebViewShared.defaultOriginWhitelist,
  361. scalesPageToFit: true,
  362. };
  363. state = {
  364. viewState: WebViewState.IDLE,
  365. lastErrorEvent: null,
  366. startInLoadingState: true,
  367. };
  368. UNSAFE_componentWillMount() {
  369. if (this.props.startInLoadingState) {
  370. this.setState({ viewState: WebViewState.LOADING });
  371. }
  372. }
  373. render() {
  374. let otherView = null;
  375. if (this.state.viewState === WebViewState.LOADING) {
  376. otherView = (this.props.renderLoading || defaultRenderLoading)();
  377. } else if (this.state.viewState === WebViewState.ERROR) {
  378. const errorEvent = this.state.lastErrorEvent;
  379. invariant(errorEvent != null, 'lastErrorEvent expected to be non-null');
  380. otherView = (this.props.renderError || defaultRenderError)(
  381. errorEvent.domain,
  382. errorEvent.code,
  383. errorEvent.description,
  384. );
  385. } else if (this.state.viewState !== WebViewState.IDLE) {
  386. console.error(
  387. 'RNCWebView invalid state encountered: ' + this.state.loading,
  388. );
  389. }
  390. const webViewStyles = [styles.container, styles.webView, this.props.style];
  391. if (
  392. this.state.viewState === WebViewState.LOADING ||
  393. this.state.viewState === WebViewState.ERROR
  394. ) {
  395. // if we're in either LOADING or ERROR states, don't show the webView
  396. webViewStyles.push(styles.hidden);
  397. }
  398. const nativeConfig = this.props.nativeConfig || {};
  399. const viewManager = nativeConfig.viewManager || RNCWebViewManager;
  400. const compiledWhitelist = (this.props.originWhitelist || []).map(
  401. WebViewShared.originWhitelistToRegex,
  402. );
  403. const onShouldStartLoadWithRequest = (event) => {
  404. let shouldStart = true;
  405. const { url } = event.nativeEvent;
  406. const origin = WebViewShared.extractOrigin(url);
  407. const passesWhitelist = compiledWhitelist.some(x =>
  408. new RegExp(x).test(origin),
  409. );
  410. shouldStart = shouldStart && passesWhitelist;
  411. if (!passesWhitelist) {
  412. Linking.openURL(url);
  413. }
  414. if (this.props.onShouldStartLoadWithRequest) {
  415. shouldStart =
  416. shouldStart &&
  417. this.props.onShouldStartLoadWithRequest(event.nativeEvent);
  418. }
  419. viewManager.startLoadWithResult(
  420. !!shouldStart,
  421. event.nativeEvent.lockIdentifier,
  422. );
  423. };
  424. const decelerationRate = processDecelerationRate(
  425. this.props.decelerationRate,
  426. );
  427. const source = this.props.source || {};
  428. if (this.props.html) {
  429. source.html = this.props.html;
  430. } else if (this.props.url) {
  431. source.uri = this.props.url;
  432. }
  433. const messagingEnabled = typeof this.props.onMessage === 'function';
  434. const NativeWebView = nativeConfig.component || RNCWebView;
  435. const webView = (
  436. <NativeWebView
  437. ref={RCT_WEBVIEW_REF}
  438. key="webViewKey"
  439. style={webViewStyles}
  440. source={resolveAssetSource(source)}
  441. injectedJavaScript={this.props.injectedJavaScript}
  442. bounces={this.props.bounces}
  443. scrollEnabled={this.props.scrollEnabled}
  444. decelerationRate={decelerationRate}
  445. contentInset={this.props.contentInset}
  446. automaticallyAdjustContentInsets={
  447. this.props.automaticallyAdjustContentInsets
  448. }
  449. onLoadingStart={this._onLoadingStart}
  450. onLoadingFinish={this._onLoadingFinish}
  451. onLoadingError={this._onLoadingError}
  452. messagingEnabled={messagingEnabled}
  453. onMessage={this._onMessage}
  454. onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
  455. scalesPageToFit={this.props.scalesPageToFit}
  456. allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}
  457. mediaPlaybackRequiresUserAction={
  458. this.props.mediaPlaybackRequiresUserAction
  459. }
  460. dataDetectorTypes={this.props.dataDetectorTypes}
  461. {...nativeConfig.props}
  462. />
  463. );
  464. return (
  465. <View style={styles.container}>
  466. {webView}
  467. {otherView}
  468. </View>
  469. );
  470. }
  471. /**
  472. * Go forward one page in the web view's history.
  473. */
  474. goForward = () => {
  475. UIManager.dispatchViewManagerCommand(
  476. this.getWebViewHandle(),
  477. UIManager.RNCWebView.Commands.goForward,
  478. null,
  479. );
  480. };
  481. /**
  482. * Go back one page in the web view's history.
  483. */
  484. goBack = () => {
  485. UIManager.dispatchViewManagerCommand(
  486. this.getWebViewHandle(),
  487. UIManager.RNCWebView.Commands.goBack,
  488. null,
  489. );
  490. };
  491. /**
  492. * Reloads the current page.
  493. */
  494. reload = () => {
  495. this.setState({ viewState: WebViewState.LOADING });
  496. UIManager.dispatchViewManagerCommand(
  497. this.getWebViewHandle(),
  498. UIManager.RNCWebView.Commands.reload,
  499. null,
  500. );
  501. };
  502. /**
  503. * Stop loading the current page.
  504. */
  505. stopLoading = () => {
  506. UIManager.dispatchViewManagerCommand(
  507. this.getWebViewHandle(),
  508. UIManager.RNCWebView.Commands.stopLoading,
  509. null,
  510. );
  511. };
  512. /**
  513. * Posts a message to the web view, which will emit a `message` event.
  514. * Accepts one argument, `data`, which must be a string.
  515. *
  516. * In your webview, you'll need to something like the following.
  517. *
  518. * ```js
  519. * document.addEventListener('message', e => { document.title = e.data; });
  520. * ```
  521. */
  522. postMessage = data => {
  523. UIManager.dispatchViewManagerCommand(
  524. this.getWebViewHandle(),
  525. UIManager.RNCWebView.Commands.postMessage,
  526. [String(data)],
  527. );
  528. };
  529. /**
  530. * Injects a javascript string into the referenced WebView. Deliberately does not
  531. * return a response because using eval() to return a response breaks this method
  532. * on pages with a Content Security Policy that disallows eval(). If you need that
  533. * functionality, look into postMessage/onMessage.
  534. */
  535. injectJavaScript = data => {
  536. UIManager.dispatchViewManagerCommand(
  537. this.getWebViewHandle(),
  538. UIManager.RNCWebView.Commands.injectJavaScript,
  539. [data],
  540. );
  541. };
  542. /**
  543. * We return an event with a bunch of fields including:
  544. * url, title, loading, canGoBack, canGoForward
  545. */
  546. _updateNavigationState = (event) => {
  547. if (this.props.onNavigationStateChange) {
  548. this.props.onNavigationStateChange(event.nativeEvent);
  549. }
  550. };
  551. /**
  552. * Returns the native `WebView` node.
  553. */
  554. getWebViewHandle = () => {
  555. return ReactNative.findNodeHandle(this.refs[RCT_WEBVIEW_REF]);
  556. };
  557. _onLoadingStart = (event) => {
  558. const onLoadStart = this.props.onLoadStart;
  559. onLoadStart && onLoadStart(event);
  560. this._updateNavigationState(event);
  561. };
  562. _onLoadingError = (event) => {
  563. event.persist(); // persist this event because we need to store it
  564. const { onError, onLoadEnd } = this.props;
  565. onError && onError(event);
  566. onLoadEnd && onLoadEnd(event);
  567. console.warn('Encountered an error loading page', event.nativeEvent);
  568. this.setState({
  569. lastErrorEvent: event.nativeEvent,
  570. viewState: WebViewState.ERROR,
  571. });
  572. };
  573. _onLoadingFinish = (event) => {
  574. const { onLoad, onLoadEnd } = this.props;
  575. onLoad && onLoad(event);
  576. onLoadEnd && onLoadEnd(event);
  577. this.setState({
  578. viewState: WebViewState.IDLE,
  579. });
  580. this._updateNavigationState(event);
  581. };
  582. _onMessage = (event) => {
  583. const { onMessage } = this.props;
  584. onMessage && onMessage(event);
  585. };
  586. }
  587. const RNCWebView = requireNativeComponent('RNCWebView');
  588. const styles = StyleSheet.create({
  589. container: {
  590. flex: 1,
  591. },
  592. errorContainer: {
  593. flex: 1,
  594. justifyContent: 'center',
  595. alignItems: 'center',
  596. backgroundColor: BGWASH,
  597. },
  598. errorText: {
  599. fontSize: 14,
  600. textAlign: 'center',
  601. marginBottom: 2,
  602. },
  603. errorTextTitle: {
  604. fontSize: 15,
  605. fontWeight: '500',
  606. marginBottom: 10,
  607. },
  608. hidden: {
  609. height: 0,
  610. flex: 0, // disable 'flex:1' when hiding a View
  611. },
  612. loadingView: {
  613. backgroundColor: BGWASH,
  614. flex: 1,
  615. justifyContent: 'center',
  616. alignItems: 'center',
  617. height: 100,
  618. },
  619. webView: {
  620. backgroundColor: '#ffffff',
  621. },
  622. });
  623. module.exports = WebView;