Browse Source

feat(iOS): Add onFileDownload callback to help iOS apps implement file downloads.

`onFileDownload` is called with the URL that you can use to download the file.
When RNCWebView detects that the HTTP response should result in a file download,
`onFileDownload` is called. The client can then provide code to download
the file.

RNCWebView determines that a file download should take place if either of the
following is true:
1. The HTTP response contains a `Content-Disposition` header that is of type
  'attachment'
2. The MIME type of the response cannot be rendered by the iOS WebView
Tyler Coffman 4 years ago
parent
commit
17bd1e5356
9 changed files with 131 additions and 11 deletions
  1. 14
    3
      docs/Guide.md
  2. 14
    0
      example/App.tsx
  3. 55
    0
      example/examples/Downloads.tsx
  4. 2
    2
      example/ios/Podfile.lock
  5. 1
    1
      index.d.ts
  6. 22
    5
      ios/RNCWebView.m
  7. 1
    0
      ios/RNCWebViewManager.m
  8. 1
    0
      src/WebView.ios.tsx
  9. 21
    0
      src/WebViewTypes.ts

+ 14
- 3
docs/Guide.md View File

@@ -230,9 +230,19 @@ You can control **single** or **multiple** file selection by specifing the [`mul
230 230
 
231 231
 ##### iOS
232 232
 
233
-For iOS, all you need to do is specify the permissions in your `ios/[project]/Info.plist` file:
233
+On iOS, you are going to have to supply your own code to download files. You can supply an `onFileDownload` callback
234
+to the WebView component as a prop. If RNCWebView determines that a file download needs to take place, the URL where you can download the file
235
+will be given to `onFileDownload`. From that callback you can then download that file however you would like to do so.
236
+
237
+Example:
238
+```javascript
239
+  onFileDownload = ({ nativeEvent }) => {
240
+    const { downloadUrl } = nativeEvent;
241
+    // --> Your download code goes here <--
242
+  }
243
+```
234 244
 
235
-Save to gallery:
245
+To be able to save images to the gallery you need to specify this permission in your `ios/[project]/Info.plist` file:
236 246
 
237 247
 ```
238 248
 <key>NSPhotoLibraryAddUsageDescription</key>
@@ -241,7 +251,8 @@ Save to gallery:
241 251
 
242 252
 ##### Android
243 253
 
244
-Add permission in AndroidManifest.xml:
254
+On Android, integration with the DownloadManager is built-in. 
255
+All you have to do to support downloads is add these permissions in AndroidManifest.xml:
245 256
 
246 257
 ```xml
247 258
 <manifest ...>

+ 14
- 0
example/App.tsx View File

@@ -13,6 +13,7 @@ import {
13 13
 import Alerts from './examples/Alerts';
14 14
 import Scrolling from './examples/Scrolling';
15 15
 import Background from './examples/Background';
16
+import Downloads from './examples/Downloads';
16 17
 import Uploads from './examples/Uploads';
17 18
 
18 19
 const TESTS = {
@@ -40,6 +41,14 @@ const TESTS = {
40 41
       return <Background />;
41 42
     },
42 43
   },
44
+  Downloads: {
45
+    title: 'Downloads',
46
+    testId: 'downloads',
47
+    description: 'File downloads test',
48
+    render() {
49
+      return <Downloads />;
50
+    },
51
+  },
43 52
   Uploads: {
44 53
     title: 'Uploads',
45 54
     testId: 'uploads',
@@ -101,6 +110,11 @@ export default class App extends Component<Props, State> {
101 110
             title="Background"
102 111
             onPress={() => this._changeTest('Background')}
103 112
           />
113
+          {Platform.OS == "ios" && <Button
114
+            testID="testType_downloads"
115
+            title="Downloads"
116
+            onPress={() => this._changeTest('Downloads')}
117
+          />}
104 118
           {Platform.OS === 'android' && <Button
105 119
             testID="testType_uploads"
106 120
             title="Uploads"

+ 55
- 0
example/examples/Downloads.tsx View File

@@ -0,0 +1,55 @@
1
+import React, {Component} from 'react';
2
+import {Alert, Platform, View} from 'react-native';
3
+
4
+import WebView, {FileDownload} from '../..';
5
+
6
+const HTML = `
7
+<!DOCTYPE html>\n
8
+<html>
9
+  <head>
10
+    <title>Downloads</title>
11
+    <meta http-equiv="content-type" content="text/html; charset=utf-8">
12
+    <meta name="viewport" content="width=320, user-scalable=no">
13
+    <style type="text/css">
14
+      body {
15
+        margin: 0;
16
+        padding: 0;
17
+        font: 62.5% arial, sans-serif;
18
+        background: #ccc;
19
+      }
20
+    </style>
21
+  </head>
22
+  <body>
23
+    <a href="https://www.7-zip.org/a/7za920.zip">Example zip file download</a>
24
+  </body>
25
+</html>
26
+`;
27
+
28
+type Props = {};
29
+type State = {};
30
+
31
+export default class Downloads extends Component<Props, State> {
32
+  state = {};
33
+
34
+  onFileDownload = ({ nativeEvent }: { nativeEvent: FileDownload } ) => {
35
+    Alert.alert("File download detected", nativeEvent.downloadUrl);
36
+  };
37
+ 
38
+  render() {
39
+    const platformProps = Platform.select({
40
+      ios: {
41
+        onFileDownload: this.onFileDownload,
42
+      },
43
+    });
44
+
45
+    return (
46
+      <View style={{ height: 120 }}>
47
+        <WebView
48
+          source={{html: HTML}}
49
+          automaticallyAdjustContentInsets={false}
50
+          {...platformProps}
51
+        />
52
+      </View>
53
+    );
54
+  }
55
+}

+ 2
- 2
example/ios/Podfile.lock View File

@@ -182,7 +182,7 @@ PODS:
182 182
     - React-cxxreact (= 0.61.5)
183 183
     - React-jsi (= 0.61.5)
184 184
   - React-jsinspector (0.61.5)
185
-  - react-native-webview (8.0.6):
185
+  - react-native-webview (8.1.0):
186 186
     - React
187 187
   - React-RCTActionSheet (0.61.5):
188 188
     - React-Core/RCTActionSheetHeaders (= 0.61.5)
@@ -326,7 +326,7 @@ SPEC CHECKSUMS:
326 326
   React-jsi: cb2cd74d7ccf4cffb071a46833613edc79cdf8f7
327 327
   React-jsiexecutor: d5525f9ed5f782fdbacb64b9b01a43a9323d2386
328 328
   React-jsinspector: fa0ecc501688c3c4c34f28834a76302233e29dc0
329
-  react-native-webview: 222d83c9c489e09b5d3541519110a637490ad4fa
329
+  react-native-webview: 88d83b4df2e792cfc36c952b1c496e2ac01cf0b8
330 330
   React-RCTActionSheet: 600b4d10e3aea0913b5a92256d2719c0cdd26d76
331 331
   React-RCTAnimation: 791a87558389c80908ed06cc5dfc5e7920dfa360
332 332
   React-RCTBlob: d89293cc0236d9cb0933d85e430b0bbe81ad1d72

+ 1
- 1
index.d.ts View File

@@ -2,7 +2,7 @@ import { Component } from 'react';
2 2
 // eslint-disable-next-line
3 3
 import { IOSWebViewProps, AndroidWebViewProps } from './lib/WebViewTypes';
4 4
 
5
-export { WebViewMessageEvent, WebViewNavigation } from "./lib/WebViewTypes";
5
+export { FileDownload, WebViewMessageEvent, WebViewNavigation } from "./lib/WebViewTypes";
6 6
 
7 7
 export type WebViewProps = IOSWebViewProps & AndroidWebViewProps;
8 8
 

+ 22
- 5
ios/RNCWebView.m View File

@@ -67,6 +67,8 @@ static NSDictionary* customCertificatesForHost;
67 67
     UIScrollViewDelegate,
68 68
 #endif // !TARGET_OS_OSX
69 69
     RCTAutoInsetsProtocol>
70
+
71
+@property (nonatomic, copy) RCTDirectEventBlock onFileDownload;
70 72
 @property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
71 73
 @property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
72 74
 @property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
@@ -1061,24 +1063,39 @@ static NSDictionary* customCertificatesForHost;
1061 1063
   decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse
1062 1064
                     decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
1063 1065
 {
1066
+  WKNavigationResponsePolicy policy = WKNavigationResponsePolicyAllow;
1064 1067
   if (_onHttpError && navigationResponse.forMainFrame) {
1065 1068
     if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) {
1066 1069
       NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
1067 1070
       NSInteger statusCode = response.statusCode;
1071
+      NSString *disposition = [response valueForHTTPHeaderField:@"Content-Disposition"];
1068 1072
 
1069 1073
       if (statusCode >= 400) {
1070
-        NSMutableDictionary<NSString *, id> *event = [self baseEvent];
1071
-        [event addEntriesFromDictionary: @{
1074
+        NSMutableDictionary<NSString *, id> *httpErrorEvent = [self baseEvent];
1075
+        [httpErrorEvent addEntriesFromDictionary: @{
1072 1076
           @"url": response.URL.absoluteString,
1073 1077
           @"statusCode": @(statusCode)
1074 1078
         }];
1075 1079
 
1076
-        _onHttpError(event);
1080
+        _onHttpError(httpErrorEvent);
1081
+      }
1082
+
1083
+      BOOL isAttachment = disposition != nil && [disposition hasPrefix:@"attachment"];
1084
+      if (isAttachment || !navigationResponse.canShowMIMEType) {
1085
+        if (_onFileDownload) {
1086
+          policy = WKNavigationResponsePolicyCancel;
1087
+
1088
+          NSMutableDictionary<NSString *, id> *downloadEvent = [self baseEvent];
1089
+          [downloadEvent addEntriesFromDictionary: @{
1090
+            @"downloadUrl": (response.URL).absoluteString,
1091
+          }];
1092
+          _onFileDownload(downloadEvent);
1093
+        }
1077 1094
       }
1078 1095
     }
1079
-  }  
1096
+  }
1080 1097
 
1081
-  decisionHandler(WKNavigationResponsePolicyAllow);
1098
+  decisionHandler(policy);
1082 1099
 }
1083 1100
 
1084 1101
 /**

+ 1
- 0
ios/RNCWebViewManager.m View File

@@ -34,6 +34,7 @@ RCT_EXPORT_MODULE()
34 34
 }
35 35
 
36 36
 RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary)
37
+RCT_EXPORT_VIEW_PROPERTY(onFileDownload, RCTDirectEventBlock)
37 38
 RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock)
38 39
 RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock)
39 40
 RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock)

+ 1
- 0
src/WebView.ios.tsx View File

@@ -338,6 +338,7 @@ class WebView extends React.Component<IOSWebViewProps, State> {
338 338
         onLoadingError={this.onLoadingError}
339 339
         onLoadingFinish={this.onLoadingFinish}
340 340
         onLoadingProgress={this.onLoadingProgress}
341
+        onFileDownload={this.props.onFileDownload}
341 342
         onLoadingStart={this.onLoadingStart}
342 343
         onHttpError={this.onHttpError}
343 344
         onMessage={this.onMessage}

+ 21
- 0
src/WebViewTypes.ts View File

@@ -104,6 +104,10 @@ export interface WebViewNavigation extends WebViewNativeEvent {
104 104
   mainDocumentURL?: string;
105 105
 }
106 106
 
107
+export interface FileDownload {
108
+  downloadUrl: string;
109
+}
110
+
107 111
 export type DecelerationRateConstant = 'normal' | 'fast';
108 112
 
109 113
 export interface WebViewMessage extends WebViewNativeEvent {
@@ -132,6 +136,8 @@ export type WebViewProgressEvent = NativeSyntheticEvent<
132 136
 
133 137
 export type WebViewNavigationEvent = NativeSyntheticEvent<WebViewNavigation>;
134 138
 
139
+export type FileDownloadEvent = NativeSyntheticEvent<FileDownload>;
140
+
135 141
 export type WebViewMessageEvent = NativeSyntheticEvent<WebViewMessage>;
136 142
 
137 143
 export type WebViewErrorEvent = NativeSyntheticEvent<WebViewError>;
@@ -290,6 +296,7 @@ export interface IOSNativeWebViewProps extends CommonNativeWebViewProps {
290 296
   scrollEnabled?: boolean;
291 297
   useSharedProcessPool?: boolean;
292 298
   onContentProcessDidTerminate?: (event: WebViewTerminatedEvent) => void;
299
+  onFileDownload?: (event: FileDownloadEvent) => void;
293 300
 }
294 301
 
295 302
 export interface MacOSNativeWebViewProps extends CommonNativeWebViewProps {
@@ -482,6 +489,20 @@ export interface IOSWebViewProps extends WebViewSharedProps {
482 489
    * @platform ios
483 490
    */
484 491
   onContentProcessDidTerminate?: (event: WebViewTerminatedEvent) => void;
492
+
493
+  /**
494
+   * Function that is invoked when the client needs to download a file.
495
+   * If the webview navigates to a URL that results in an HTTP response with a
496
+   * Content-Disposition header 'attachment...', then this will be called.
497
+   * If the MIME type indicates that the content is not renderable by the
498
+   * webview, that will also cause this to be called.
499
+   * 
500
+   * The application will need to provide its own code to actually download
501
+   * the file.
502
+   * 
503
+   * If not provided, the default is to let the webview try to render the file.
504
+   */
505
+  onFileDownload?: (event: FileDownloadEvent) => void;
485 506
 }
486 507
 
487 508
 export interface MacOSWebViewProps extends WebViewSharedProps {