No Description

WebView.ios.tsx 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525
  1. import React from 'react';
  2. import {
  3. ActivityIndicator,
  4. StyleSheet,
  5. Text,
  6. UIManager,
  7. View,
  8. requireNativeComponent,
  9. NativeModules,
  10. Image,
  11. findNodeHandle,
  12. NativeSyntheticEvent,
  13. } from 'react-native';
  14. import invariant from 'invariant';
  15. import {
  16. defaultOriginWhitelist,
  17. createOnShouldStartLoadWithRequest,
  18. } from './WebViewShared';
  19. import {
  20. WebViewSourceUri,
  21. WebViewError,
  22. WebViewErrorEvent,
  23. WebViewMessageEvent,
  24. WebViewNavigationEvent,
  25. WebViewSharedProps,
  26. WebViewSource,
  27. WebViewProgressEvent,
  28. } from './types/WebViewTypes';
  29. const BGWASH = 'rgba(255,255,255,0.8)';
  30. const styles = StyleSheet.create({
  31. container: {
  32. flex: 1,
  33. },
  34. errorContainer: {
  35. flex: 1,
  36. justifyContent: 'center',
  37. alignItems: 'center',
  38. backgroundColor: BGWASH,
  39. },
  40. errorText: {
  41. fontSize: 14,
  42. textAlign: 'center',
  43. marginBottom: 2,
  44. },
  45. errorTextTitle: {
  46. fontSize: 15,
  47. fontWeight: '500',
  48. marginBottom: 10,
  49. },
  50. hidden: {
  51. height: 0,
  52. flex: 0, // disable 'flex:1' when hiding a View
  53. },
  54. loadingView: {
  55. backgroundColor: BGWASH,
  56. flex: 1,
  57. justifyContent: 'center',
  58. alignItems: 'center',
  59. height: 100,
  60. },
  61. webView: {
  62. backgroundColor: '#ffffff',
  63. },
  64. });
  65. type DecelerationRate = number | 'normal' | 'fast';
  66. // Imported from https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollView/processDecelerationRate.js
  67. function processDecelerationRate(
  68. decelerationRate?: DecelerationRate,
  69. ): number | undefined {
  70. if (decelerationRate === 'normal') {
  71. return 0.998;
  72. }
  73. if (decelerationRate === 'fast') {
  74. return 0.99;
  75. }
  76. return decelerationRate;
  77. }
  78. const { RNCWKWebViewManager, RNCUIWebViewManager } = NativeModules;
  79. const RNCUIWebView = requireNativeComponent('RNCUIWebView');
  80. const RNCWKWebView = requireNativeComponent('RNCWKWebView');
  81. const WebViewState: {
  82. IDLE: 'IDLE';
  83. LOADING: 'LOADING';
  84. ERROR: 'ERROR';
  85. } = {
  86. IDLE: 'IDLE',
  87. LOADING: 'LOADING',
  88. ERROR: 'ERROR',
  89. };
  90. const NavigationType = {
  91. click: 'click',
  92. formsubmit: 'formsubmit',
  93. backforward: 'backforward',
  94. reload: 'reload',
  95. formresubmit: 'formresubmit',
  96. other: 'other',
  97. };
  98. const JSNavigationScheme = 'react-js-navigation';
  99. type State = {
  100. viewState: 'IDLE' | 'LOADING' | 'ERROR';
  101. lastErrorEvent: WebViewError | null;
  102. };
  103. const DataDetectorTypes = [
  104. 'phoneNumber',
  105. 'link',
  106. 'address',
  107. 'calendarEvent',
  108. 'trackingNumber',
  109. 'flightNumber',
  110. 'lookupSuggestion',
  111. 'none',
  112. 'all',
  113. ];
  114. const defaultRenderLoading = () => (
  115. <View style={styles.loadingView}>
  116. <ActivityIndicator />
  117. </View>
  118. );
  119. const defaultRenderError = (
  120. errorDomain: string | undefined,
  121. errorCode: number,
  122. errorDesc: string,
  123. ) => (
  124. <View style={styles.errorContainer}>
  125. <Text style={styles.errorTextTitle}>Error loading page</Text>
  126. <Text style={styles.errorText}>{`Domain: ${errorDomain}`}</Text>
  127. <Text style={styles.errorText}>{`Error Code: ${errorCode}`}</Text>
  128. <Text style={styles.errorText}>{`Description: ${errorDesc}`}</Text>
  129. </View>
  130. );
  131. /**
  132. * `WebView` renders web content in a native view.
  133. *
  134. *```
  135. * import React, { Component } from 'react';
  136. * import { WebView } from 'react-native';
  137. *
  138. * class MyWeb extends Component {
  139. * render() {
  140. * return (
  141. * <WebView
  142. * source={{uri: 'https://github.com/facebook/react-native'}}
  143. * style={{marginTop: 20}}
  144. * />
  145. * );
  146. * }
  147. * }
  148. *```
  149. *
  150. * You can use this component to navigate back and forth in the web view's
  151. * history and configure various properties for the web content.
  152. */
  153. class WebView extends React.Component<WebViewSharedProps, State> {
  154. static JSNavigationScheme = JSNavigationScheme;
  155. static NavigationType = NavigationType;
  156. static defaultProps = {
  157. useWebKit: true,
  158. originWhitelist: defaultOriginWhitelist,
  159. };
  160. static isFileUploadSupported = async () =>
  161. // no native implementation for iOS, depends only on permissions
  162. true;
  163. state: State = {
  164. viewState: this.props.startInLoadingState
  165. ? WebViewState.LOADING
  166. : WebViewState.IDLE,
  167. lastErrorEvent: null,
  168. };
  169. webViewRef = React.createRef<React.ComponentClass>();
  170. // eslint-disable-next-line camelcase, react/sort-comp
  171. UNSAFE_componentWillMount() {
  172. if (
  173. this.props.useWebKit === true
  174. && this.props.scalesPageToFit !== undefined
  175. ) {
  176. console.warn(
  177. 'The scalesPageToFit property is not supported when useWebKit = true',
  178. );
  179. }
  180. if (
  181. !this.props.useWebKit
  182. && this.props.allowsBackForwardNavigationGestures
  183. ) {
  184. console.warn(
  185. 'The allowsBackForwardNavigationGestures property is not supported when useWebKit = false',
  186. );
  187. }
  188. }
  189. componentDidUpdate(prevProps: WebViewSharedProps) {
  190. if (!(prevProps.useWebKit && this.props.useWebKit)) {
  191. return;
  192. }
  193. this.showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback');
  194. this.showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction');
  195. this.showRedboxOnPropChanges(prevProps, 'dataDetectorTypes');
  196. if (this.props.scalesPageToFit !== undefined) {
  197. console.warn(
  198. 'The scalesPageToFit property is not supported when useWebKit = true',
  199. );
  200. }
  201. }
  202. getCommands() {
  203. if (!this.props.useWebKit) {
  204. return UIManager.RNCUIWebView.Commands;
  205. }
  206. return UIManager.RNCWKWebView.Commands;
  207. }
  208. /**
  209. * Go forward one page in the web view's history.
  210. */
  211. goForward = () => {
  212. UIManager.dispatchViewManagerCommand(
  213. this.getWebViewHandle(),
  214. this.getCommands().goForward,
  215. null,
  216. );
  217. };
  218. /**
  219. * Go back one page in the web view's history.
  220. */
  221. goBack = () => {
  222. UIManager.dispatchViewManagerCommand(
  223. this.getWebViewHandle(),
  224. this.getCommands().goBack,
  225. null,
  226. );
  227. };
  228. /**
  229. * Reloads the current page.
  230. */
  231. reload = () => {
  232. this.setState({ viewState: WebViewState.LOADING });
  233. UIManager.dispatchViewManagerCommand(
  234. this.getWebViewHandle(),
  235. this.getCommands().reload,
  236. null,
  237. );
  238. };
  239. /**
  240. * Stop loading the current page.
  241. */
  242. stopLoading = () => {
  243. UIManager.dispatchViewManagerCommand(
  244. this.getWebViewHandle(),
  245. this.getCommands().stopLoading,
  246. null,
  247. );
  248. };
  249. /**
  250. * Posts a message to the web view, which will emit a `message` event.
  251. * Accepts one argument, `data`, which must be a string.
  252. *
  253. * In your webview, you'll need to something like the following.
  254. *
  255. * ```js
  256. * document.addEventListener('message', e => { document.title = e.data; });
  257. * ```
  258. */
  259. postMessage = (data: string) => {
  260. UIManager.dispatchViewManagerCommand(
  261. this.getWebViewHandle(),
  262. this.getCommands().postMessage,
  263. [String(data)],
  264. );
  265. };
  266. /**
  267. * Injects a javascript string into the referenced WebView. Deliberately does not
  268. * return a response because using eval() to return a response breaks this method
  269. * on pages with a Content Security Policy that disallows eval(). If you need that
  270. * functionality, look into postMessage/onMessage.
  271. */
  272. injectJavaScript = (data: string) => {
  273. UIManager.dispatchViewManagerCommand(
  274. this.getWebViewHandle(),
  275. this.getCommands().injectJavaScript,
  276. [data],
  277. );
  278. };
  279. /**
  280. * We return an event with a bunch of fields including:
  281. * url, title, loading, canGoBack, canGoForward
  282. */
  283. updateNavigationState = (event: WebViewNavigationEvent) => {
  284. if (this.props.onNavigationStateChange) {
  285. this.props.onNavigationStateChange(event.nativeEvent);
  286. }
  287. };
  288. /**
  289. * Returns the native `WebView` node.
  290. */
  291. getWebViewHandle = () => findNodeHandle(this.webViewRef.current);
  292. onLoadingStart = (event: WebViewNavigationEvent) => {
  293. const { onLoadStart } = this.props;
  294. if (onLoadStart) {
  295. onLoadStart(event);
  296. }
  297. this.updateNavigationState(event);
  298. };
  299. onLoadingError = (event: WebViewErrorEvent) => {
  300. event.persist(); // persist this event because we need to store it
  301. const { onError, onLoadEnd } = this.props;
  302. if (onError) {
  303. onError(event);
  304. }
  305. if (onLoadEnd) {
  306. onLoadEnd(event);
  307. }
  308. // eslint-disable-next-line no-console
  309. console.warn('Encountered an error loading page', event.nativeEvent);
  310. this.setState({
  311. lastErrorEvent: event.nativeEvent,
  312. viewState: WebViewState.ERROR,
  313. });
  314. };
  315. onLoadingFinish = (event: WebViewNavigationEvent) => {
  316. const { onLoad, onLoadEnd } = this.props;
  317. if (onLoad) {
  318. onLoad(event);
  319. }
  320. if (onLoadEnd) {
  321. onLoadEnd(event);
  322. }
  323. this.setState({
  324. viewState: WebViewState.IDLE,
  325. });
  326. this.updateNavigationState(event);
  327. };
  328. onMessage = (event: WebViewMessageEvent) => {
  329. const { onMessage } = this.props;
  330. if (onMessage) {
  331. onMessage(event);
  332. }
  333. };
  334. onLoadingProgress = (event: NativeSyntheticEvent<WebViewProgressEvent>) => {
  335. const { onLoadProgress } = this.props;
  336. if (onLoadProgress) {
  337. onLoadProgress(event);
  338. }
  339. };
  340. onShouldStartLoadWithRequestCallback = (
  341. shouldStart: boolean,
  342. url: string,
  343. lockIdentifier: number,
  344. ) => {
  345. const nativeConfig = this.props.nativeConfig || {};
  346. let { viewManager } = nativeConfig;
  347. if (this.props.useWebKit) {
  348. viewManager = viewManager || RNCWKWebViewManager;
  349. } else {
  350. viewManager = viewManager || RNCUIWebViewManager;
  351. }
  352. invariant(viewManager != null, 'viewManager expected to be non-null');
  353. viewManager.startLoadWithResult(!!shouldStart, lockIdentifier);
  354. };
  355. showRedboxOnPropChanges = (
  356. prevProps: WebViewSharedProps,
  357. propName: keyof WebViewSharedProps,
  358. ) => {
  359. if (this.props[propName] !== prevProps[propName]) {
  360. console.error(
  361. `Changes to property ${propName} do nothing after the initial render.`,
  362. );
  363. }
  364. };
  365. render() {
  366. let otherView = null;
  367. let scalesPageToFit;
  368. if (this.props.useWebKit) {
  369. ({ scalesPageToFit } = this.props);
  370. } else {
  371. ({ scalesPageToFit = true } = this.props);
  372. }
  373. if (this.state.viewState === WebViewState.LOADING) {
  374. otherView = (this.props.renderLoading || defaultRenderLoading)();
  375. } else if (this.state.viewState === WebViewState.ERROR) {
  376. const errorEvent = this.state.lastErrorEvent;
  377. if (errorEvent) {
  378. otherView = (this.props.renderError || defaultRenderError)(
  379. errorEvent.domain,
  380. errorEvent.code,
  381. errorEvent.description,
  382. );
  383. } else {
  384. invariant(errorEvent != null, 'lastErrorEvent expected to be non-null');
  385. }
  386. } else if (this.state.viewState !== WebViewState.IDLE) {
  387. console.error(
  388. `RNCWebView invalid state encountered: ${this.state.viewState}`,
  389. );
  390. }
  391. const webViewStyles = [styles.container, styles.webView, this.props.style];
  392. if (
  393. this.state.viewState === WebViewState.LOADING
  394. || this.state.viewState === WebViewState.ERROR
  395. ) {
  396. // if we're in either LOADING or ERROR states, don't show the webView
  397. webViewStyles.push(styles.hidden);
  398. }
  399. const nativeConfig = this.props.nativeConfig || {};
  400. const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
  401. this.onShouldStartLoadWithRequestCallback,
  402. this.props.originWhitelist,
  403. this.props.onShouldStartLoadWithRequest,
  404. );
  405. const decelerationRate = processDecelerationRate(
  406. this.props.decelerationRate,
  407. );
  408. let source: WebViewSource = this.props.source || {};
  409. if (!this.props.source && this.props.html) {
  410. source = { html: this.props.html };
  411. } else if (!this.props.source && this.props.url) {
  412. source = { uri: this.props.url };
  413. }
  414. const messagingEnabled = typeof this.props.onMessage === 'function';
  415. let NativeWebView = nativeConfig.component;
  416. if (this.props.useWebKit) {
  417. NativeWebView = NativeWebView || RNCWKWebView;
  418. } else {
  419. NativeWebView = NativeWebView || RNCUIWebView;
  420. }
  421. const webView = (
  422. <NativeWebView
  423. ref={this.webViewRef}
  424. key="webViewKey"
  425. style={webViewStyles}
  426. source={Image.resolveAssetSource(source as WebViewSourceUri)} // typing issue of not compatible of WebViewSourceHtml in react native.
  427. injectedJavaScript={this.props.injectedJavaScript}
  428. bounces={this.props.bounces}
  429. scrollEnabled={this.props.scrollEnabled}
  430. pagingEnabled={this.props.pagingEnabled}
  431. decelerationRate={decelerationRate}
  432. contentInset={this.props.contentInset}
  433. automaticallyAdjustContentInsets={
  434. this.props.automaticallyAdjustContentInsets
  435. }
  436. hideKeyboardAccessoryView={this.props.hideKeyboardAccessoryView}
  437. allowsBackForwardNavigationGestures={
  438. this.props.allowsBackForwardNavigationGestures
  439. }
  440. userAgent={this.props.userAgent}
  441. onLoadingStart={this.onLoadingStart}
  442. onLoadingFinish={this.onLoadingFinish}
  443. onLoadingError={this.onLoadingError}
  444. onLoadingProgress={this.onLoadingProgress}
  445. messagingEnabled={messagingEnabled}
  446. onMessage={this.onMessage}
  447. onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
  448. scalesPageToFit={scalesPageToFit}
  449. allowsInlineMediaPlayback={this.props.allowsInlineMediaPlayback}
  450. mediaPlaybackRequiresUserAction={
  451. this.props.mediaPlaybackRequiresUserAction
  452. }
  453. dataDetectorTypes={this.props.dataDetectorTypes}
  454. allowsLinkPreview={this.props.allowsLinkPreview}
  455. {...nativeConfig.props}
  456. />
  457. );
  458. return (
  459. <View style={styles.container}>
  460. {webView}
  461. {otherView}
  462. </View>
  463. );
  464. }
  465. }
  466. export default WebView;