Bez popisu

WebView.ios.js 20KB

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