Bez popisu

WebView.ios.tsx 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. import React from 'react';
  2. import {
  3. ActivityIndicator,
  4. Text,
  5. UIManager as NotTypedUIManager,
  6. View,
  7. requireNativeComponent,
  8. NativeModules,
  9. Image,
  10. findNodeHandle,
  11. ImageSourcePropType,
  12. } from 'react-native';
  13. import invariant from 'invariant';
  14. import {
  15. defaultOriginWhitelist,
  16. createOnShouldStartLoadWithRequest,
  17. getViewManagerConfig,
  18. } from './WebViewShared';
  19. import {
  20. WebViewErrorEvent,
  21. WebViewMessageEvent,
  22. WebViewNavigationEvent,
  23. WebViewProgressEvent,
  24. IOSWebViewProps,
  25. DecelerationRateConstant,
  26. NativeWebViewIOS,
  27. ViewManager,
  28. State,
  29. CustomUIManager,
  30. WebViewNativeConfig,
  31. } from './WebViewTypes';
  32. import styles from './WebView.styles';
  33. const UIManager = NotTypedUIManager as CustomUIManager;
  34. const { resolveAssetSource } = Image;
  35. let didWarnAboutUIWebViewUsage = false;
  36. // Imported from https://github.com/facebook/react-native/blob/master/Libraries/Components/ScrollView/processDecelerationRate.js
  37. const processDecelerationRate = (
  38. decelerationRate: DecelerationRateConstant | number | undefined,
  39. ) => {
  40. let newDecelerationRate = decelerationRate;
  41. if (newDecelerationRate === 'normal') {
  42. newDecelerationRate = 0.998;
  43. } else if (newDecelerationRate === 'fast') {
  44. newDecelerationRate = 0.99;
  45. }
  46. return newDecelerationRate;
  47. };
  48. const RNCUIWebViewManager = NativeModules.RNCUIWebViewManager as ViewManager;
  49. const RNCWKWebViewManager = NativeModules.RNCWKWebViewManager as ViewManager;
  50. const RNCUIWebView: typeof NativeWebViewIOS = requireNativeComponent(
  51. 'RNCUIWebView',
  52. );
  53. const RNCWKWebView: typeof NativeWebViewIOS = requireNativeComponent(
  54. 'RNCWKWebView',
  55. );
  56. const defaultRenderLoading = () => (
  57. <View style={styles.loadingView}>
  58. <ActivityIndicator />
  59. </View>
  60. );
  61. const defaultRenderError = (
  62. errorDomain: string | undefined,
  63. errorCode: number,
  64. errorDesc: string,
  65. ) => (
  66. <View style={styles.errorContainer}>
  67. <Text style={styles.errorTextTitle}>Error loading page</Text>
  68. <Text style={styles.errorText}>{`Domain: ${errorDomain}`}</Text>
  69. <Text style={styles.errorText}>{`Error Code: ${errorCode}`}</Text>
  70. <Text style={styles.errorText}>{`Description: ${errorDesc}`}</Text>
  71. </View>
  72. );
  73. class WebView extends React.Component<IOSWebViewProps, State> {
  74. static defaultProps = {
  75. useWebKit: true,
  76. cacheEnabled: true,
  77. originWhitelist: defaultOriginWhitelist,
  78. useSharedProcessPool: true,
  79. };
  80. static isFileUploadSupported = async () => {
  81. // no native implementation for iOS, depends only on permissions
  82. return true;
  83. };
  84. state: State = {
  85. viewState: this.props.startInLoadingState ? 'LOADING' : 'IDLE',
  86. lastErrorEvent: null,
  87. };
  88. webViewRef = React.createRef<NativeWebViewIOS>();
  89. // eslint-disable-next-line camelcase
  90. UNSAFE_componentWillMount() {
  91. if (!this.props.useWebKit && !didWarnAboutUIWebViewUsage) {
  92. didWarnAboutUIWebViewUsage = true;
  93. console.warn(
  94. 'UIWebView is deprecated and will be removed soon, please use WKWebView (do not override useWebkit={true} prop),'
  95. + ' more infos here: https://github.com/react-native-community/react-native-webview/issues/312',
  96. );
  97. }
  98. if (
  99. this.props.useWebKit === true
  100. && this.props.scalesPageToFit !== undefined
  101. ) {
  102. console.warn(
  103. 'The scalesPageToFit property is not supported when useWebKit = true',
  104. );
  105. }
  106. if (
  107. !this.props.useWebKit
  108. && this.props.allowsBackForwardNavigationGestures
  109. ) {
  110. console.warn(
  111. 'The allowsBackForwardNavigationGestures property is not supported when useWebKit = false',
  112. );
  113. }
  114. if (!this.props.useWebKit && this.props.incognito) {
  115. console.warn(
  116. 'The incognito property is not supported when useWebKit = false',
  117. );
  118. }
  119. }
  120. // eslint-disable-next-line react/sort-comp
  121. getCommands = () =>
  122. !this.props.useWebKit
  123. ? getViewManagerConfig('RNCUIWebView').Commands
  124. : getViewManagerConfig('RNCWKWebView').Commands;
  125. /**
  126. * Go forward one page in the web view's history.
  127. */
  128. goForward = () => {
  129. UIManager.dispatchViewManagerCommand(
  130. this.getWebViewHandle(),
  131. this.getCommands().goForward,
  132. null,
  133. );
  134. };
  135. /**
  136. * Go back one page in the web view's history.
  137. */
  138. goBack = () => {
  139. UIManager.dispatchViewManagerCommand(
  140. this.getWebViewHandle(),
  141. this.getCommands().goBack,
  142. null,
  143. );
  144. };
  145. /**
  146. * Reloads the current page.
  147. */
  148. reload = () => {
  149. this.setState({ viewState: 'LOADING' });
  150. UIManager.dispatchViewManagerCommand(
  151. this.getWebViewHandle(),
  152. this.getCommands().reload,
  153. null,
  154. );
  155. };
  156. /**
  157. * Stop loading the current page.
  158. */
  159. stopLoading = () => {
  160. UIManager.dispatchViewManagerCommand(
  161. this.getWebViewHandle(),
  162. this.getCommands().stopLoading,
  163. null,
  164. );
  165. };
  166. /**
  167. * Posts a message to the web view, which will emit a `message` event.
  168. * Accepts one argument, `data`, which must be a string.
  169. *
  170. * In your webview, you'll need to something like the following.
  171. *
  172. * ```js
  173. * document.addEventListener('message', e => { document.title = e.data; });
  174. * ```
  175. */
  176. postMessage = (data: string) => {
  177. UIManager.dispatchViewManagerCommand(
  178. this.getWebViewHandle(),
  179. this.getCommands().postMessage,
  180. [String(data)],
  181. );
  182. };
  183. /**
  184. * Injects a javascript string into the referenced WebView. Deliberately does not
  185. * return a response because using eval() to return a response breaks this method
  186. * on pages with a Content Security Policy that disallows eval(). If you need that
  187. * functionality, look into postMessage/onMessage.
  188. */
  189. injectJavaScript = (data: string) => {
  190. UIManager.dispatchViewManagerCommand(
  191. this.getWebViewHandle(),
  192. this.getCommands().injectJavaScript,
  193. [data],
  194. );
  195. };
  196. /**
  197. * We return an event with a bunch of fields including:
  198. * url, title, loading, canGoBack, canGoForward
  199. */
  200. updateNavigationState = (event: WebViewNavigationEvent) => {
  201. if (this.props.onNavigationStateChange) {
  202. this.props.onNavigationStateChange(event.nativeEvent);
  203. }
  204. };
  205. /**
  206. * Returns the native `WebView` node.
  207. */
  208. getWebViewHandle = () => {
  209. const nodeHandle = findNodeHandle(this.webViewRef.current);
  210. invariant(nodeHandle != null, 'nodeHandle expected to be non-null');
  211. return nodeHandle as number;
  212. };
  213. onLoadingStart = (event: WebViewNavigationEvent) => {
  214. const { onLoadStart } = this.props;
  215. if (onLoadStart) {
  216. onLoadStart(event);
  217. }
  218. this.updateNavigationState(event);
  219. };
  220. onLoadingError = (event: WebViewErrorEvent) => {
  221. event.persist(); // persist this event because we need to store it
  222. const { onError, onLoadEnd } = this.props;
  223. if (onLoadEnd) {
  224. onLoadEnd(event);
  225. }
  226. if (onError) {
  227. onError(event);
  228. }
  229. console.warn('Encountered an error loading page', event.nativeEvent);
  230. this.setState({
  231. lastErrorEvent: event.nativeEvent,
  232. viewState: 'ERROR',
  233. });
  234. };
  235. onLoadingFinish = (event: WebViewNavigationEvent) => {
  236. const { onLoad, onLoadEnd } = this.props;
  237. if (onLoad) {
  238. onLoad(event);
  239. }
  240. if (onLoadEnd) {
  241. onLoadEnd(event);
  242. }
  243. this.setState({
  244. viewState: 'IDLE',
  245. });
  246. this.updateNavigationState(event);
  247. };
  248. onMessage = (event: WebViewMessageEvent) => {
  249. const { onMessage } = this.props;
  250. if (onMessage) {
  251. onMessage(event);
  252. }
  253. };
  254. onLoadingProgress = (event: WebViewProgressEvent) => {
  255. const { onLoadProgress } = this.props;
  256. if (onLoadProgress) {
  257. onLoadProgress(event);
  258. }
  259. };
  260. onShouldStartLoadWithRequestCallback = (
  261. shouldStart: boolean,
  262. _url: string,
  263. lockIdentifier: number,
  264. ) => {
  265. let { viewManager }: WebViewNativeConfig = this.props.nativeConfig || {};
  266. if (this.props.useWebKit) {
  267. viewManager = viewManager || RNCWKWebViewManager;
  268. } else {
  269. viewManager = viewManager || RNCUIWebViewManager;
  270. }
  271. invariant(viewManager != null, 'viewManager expected to be non-null');
  272. viewManager.startLoadWithResult(!!shouldStart, lockIdentifier);
  273. };
  274. componentDidUpdate(prevProps: IOSWebViewProps) {
  275. if (!(prevProps.useWebKit && this.props.useWebKit)) {
  276. return;
  277. }
  278. this.showRedboxOnPropChanges(prevProps, 'allowsInlineMediaPlayback');
  279. this.showRedboxOnPropChanges(prevProps, 'incognito');
  280. this.showRedboxOnPropChanges(prevProps, 'mediaPlaybackRequiresUserAction');
  281. this.showRedboxOnPropChanges(prevProps, 'dataDetectorTypes');
  282. if (this.props.scalesPageToFit !== undefined) {
  283. console.warn(
  284. 'The scalesPageToFit property is not supported when useWebKit = true',
  285. );
  286. }
  287. }
  288. showRedboxOnPropChanges(
  289. prevProps: IOSWebViewProps,
  290. propName: keyof IOSWebViewProps,
  291. ) {
  292. if (this.props[propName] !== prevProps[propName]) {
  293. console.error(
  294. `Changes to property ${propName} do nothing after the initial render.`,
  295. );
  296. }
  297. }
  298. render() {
  299. const {
  300. decelerationRate: decelerationRateProp,
  301. nativeConfig = {},
  302. onMessage,
  303. onShouldStartLoadWithRequest: onShouldStartLoadWithRequestProp,
  304. originWhitelist,
  305. renderError,
  306. renderLoading,
  307. scalesPageToFit = this.props.useWebKit ? undefined : true,
  308. style,
  309. useWebKit,
  310. ...otherProps
  311. } = this.props;
  312. let otherView = null;
  313. if (this.state.viewState === 'LOADING') {
  314. otherView = (renderLoading || defaultRenderLoading)();
  315. } else if (this.state.viewState === 'ERROR') {
  316. const errorEvent = this.state.lastErrorEvent;
  317. invariant(errorEvent != null, 'lastErrorEvent expected to be non-null');
  318. otherView = (renderError || defaultRenderError)(
  319. errorEvent.domain,
  320. errorEvent.code,
  321. errorEvent.description,
  322. );
  323. } else if (this.state.viewState !== 'IDLE') {
  324. console.error(
  325. `RNCWebView invalid state encountered: ${this.state.viewState}`,
  326. );
  327. }
  328. const webViewStyles = [styles.container, styles.webView, style];
  329. if (
  330. this.state.viewState === 'LOADING'
  331. || this.state.viewState === 'ERROR'
  332. ) {
  333. // if we're in either LOADING or ERROR states, don't show the webView
  334. webViewStyles.push(styles.hidden);
  335. }
  336. const onShouldStartLoadWithRequest = createOnShouldStartLoadWithRequest(
  337. this.onShouldStartLoadWithRequestCallback,
  338. // casting cause it's in the default props
  339. originWhitelist as ReadonlyArray<string>,
  340. onShouldStartLoadWithRequestProp,
  341. );
  342. const decelerationRate = processDecelerationRate(decelerationRateProp);
  343. let NativeWebView = nativeConfig.component as typeof NativeWebViewIOS;
  344. if (useWebKit) {
  345. NativeWebView = NativeWebView || RNCWKWebView;
  346. } else {
  347. NativeWebView = NativeWebView || RNCUIWebView;
  348. }
  349. const webView = (
  350. <NativeWebView
  351. key="webViewKey"
  352. {...otherProps}
  353. decelerationRate={decelerationRate}
  354. messagingEnabled={typeof onMessage === 'function'}
  355. onLoadingError={this.onLoadingError}
  356. onLoadingFinish={this.onLoadingFinish}
  357. onLoadingProgress={this.onLoadingProgress}
  358. onLoadingStart={this.onLoadingStart}
  359. onMessage={this.onMessage}
  360. onShouldStartLoadWithRequest={onShouldStartLoadWithRequest}
  361. ref={this.webViewRef}
  362. scalesPageToFit={scalesPageToFit}
  363. // TODO: find a better way to type this.
  364. source={resolveAssetSource(this.props.source as ImageSourcePropType)}
  365. style={webViewStyles}
  366. {...nativeConfig.props}
  367. />
  368. );
  369. return (
  370. <View style={styles.container}>
  371. {webView}
  372. {otherView}
  373. </View>
  374. );
  375. }
  376. }
  377. export default WebView;