Bez popisu

WebView.ios.js 20KB

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