trcoffman пре 4 година
родитељ
комит
1408c9a4b9
No account linked to committer's email address

+ 14
- 3
docs/Guide.md Прегледај датотеку

@@ -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 Прегледај датотеку

@@ -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"

+ 1
- 1
example/examples/Alerts.tsx Прегледај датотеку

@@ -1,7 +1,7 @@
1 1
 import React, {Component} from 'react';
2 2
 import {Text, View} from 'react-native';
3 3
 
4
-import WebView from 'react-native-webview';
4
+import WebView from '../..';
5 5
 
6 6
 const HTML = `
7 7
 <!DOCTYPE html>\n

+ 1
- 1
example/examples/Background.tsx Прегледај датотеку

@@ -1,7 +1,7 @@
1 1
 import React, {Component} from 'react';
2 2
 import {Text, View} from 'react-native';
3 3
 
4
-import WebView from 'react-native-webview';
4
+import WebView from '../..';
5 5
 
6 6
 const HTML = `
7 7
 <!DOCTYPE html>\n

+ 55
- 0
example/examples/Downloads.tsx Прегледај датотеку

@@ -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
+}

+ 1
- 1
example/examples/Scrolling.tsx Прегледај датотеку

@@ -1,7 +1,7 @@
1 1
 import React, {Component} from 'react';
2 2
 import {Button, Text, View} from 'react-native';
3 3
 
4
-import WebView from 'react-native-webview';
4
+import WebView from '../..';
5 5
 
6 6
 const HTML = `
7 7
 <!DOCTYPE html>\n

+ 1
- 1
example/examples/Uploads.tsx Прегледај датотеку

@@ -1,7 +1,7 @@
1 1
 import React, {Component} from 'react';
2 2
 import {Button, Linking, Text, View} from 'react-native';
3 3
 
4
-import WebView from 'react-native-webview';
4
+import WebView from '../..';
5 5
 
6 6
 const HTML = `
7 7
 <!DOCTYPE html>\n

+ 2
- 2
example/ios/Podfile.lock Прегледај датотеку

@@ -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

+ 8
- 59
example/tsconfig.json Прегледај датотеку

@@ -1,62 +1,11 @@
1
- 
2 1
 {
3 2
   "compilerOptions": {
4
-    /* Basic Options */
5
-    "target": "esnext",                       /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
6
-    "module": "commonjs",                     /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
7
-    "lib": ["es6"],                           /* Specify library files to be included in the compilation. */
8
-    "allowJs": true,                          /* Allow javascript files to be compiled. */
9
-    // "checkJs": true,                       /* Report errors in .js files. */
10
-    "jsx": "react-native",                    /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
11
-    // "declaration": true,                   /* Generates corresponding '.d.ts' file. */
12
-    // "sourceMap": true,                     /* Generates corresponding '.map' file. */
13
-    // "outFile": "./",                       /* Concatenate and emit output to single file. */
14
-    // "outDir": "./",                        /* Redirect output structure to the directory. */
15
-    // "rootDir": "./",                       /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
16
-    // "removeComments": true,                /* Do not emit comments to output. */
17
-    "noEmit": true,                           /* Do not emit outputs. */
18
-    "incremental": true,                      /* Enable incremental compilation */
19
-    // "importHelpers": true,                 /* Import emit helpers from 'tslib'. */
20
-    // "downlevelIteration": true,            /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
21
-    "isolatedModules": true,                  /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
22
-
23
-    /* Strict Type-Checking Options */
24
-    "strict": true,                           /* Enable all strict type-checking options. */
25
-    // "noImplicitAny": true,                 /* Raise error on expressions and declarations with an implied 'any' type. */
26
-    // "strictNullChecks": true,              /* Enable strict null checks. */
27
-    // "strictFunctionTypes": true,           /* Enable strict checking of function types. */
28
-    // "strictPropertyInitialization": true,  /* Enable strict checking of property initialization in classes. */
29
-    // "noImplicitThis": true,                /* Raise error on 'this' expressions with an implied 'any' type. */
30
-    // "alwaysStrict": true,                  /* Parse in strict mode and emit "use strict" for each source file. */
31
-
32
-    /* Additional Checks */
33
-    // "noUnusedLocals": true,                /* Report errors on unused locals. */
34
-    // "noUnusedParameters": true,            /* Report errors on unused parameters. */
35
-    // "noImplicitReturns": true,             /* Report error when not all code paths in function return a value. */
36
-    // "noFallthroughCasesInSwitch": true,    /* Report errors for fallthrough cases in switch statement. */
37
-
38
-    /* Module Resolution Options */
39
-    "moduleResolution": "node",               /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
40
-    "baseUrl": "./",                          /* Base directory to resolve non-absolute module names. */
41
-    // "paths": {},                           /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
42
-    // "rootDirs": [],                        /* List of root folders whose combined content represents the structure of the project at runtime. */
43
-    // "typeRoots": [],                       /* List of folders to include type definitions from. */
44
-    // "types": [],                           /* Type declaration files to be included in compilation. */
45
-    "allowSyntheticDefaultImports": true,     /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
46
-    "esModuleInterop": true                   /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
47
-    // "preserveSymlinks": true,              /* Do not resolve the real path of symlinks. */
48
-
49
-    /* Source Map Options */
50
-    // "sourceRoot": "./",                    /* Specify the location where debugger should locate TypeScript files instead of source locations. */
51
-    // "mapRoot": "./",                       /* Specify the location where debugger should locate map files instead of generated locations. */
52
-    // "inlineSourceMap": true,               /* Emit a single file with source maps instead of having a separate file. */
53
-    // "inlineSources": true,                 /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
54
-
55
-    /* Experimental Options */
56
-    // "experimentalDecorators": true,        /* Enables experimental support for ES7 decorators. */
57
-    // "emitDecoratorMetadata": true,         /* Enables experimental support for emitting type metadata for decorators. */
58
-  },
59
-  "exclude": [
60
-    "node_modules", "babel.config.js", "metro.config.js", "jest.config.js"
61
-  ]
3
+    "allowSyntheticDefaultImports": true,
4
+    "jsx": "react-native",
5
+    "lib": ["dom", "esnext"],
6
+    "moduleResolution": "node",
7
+    "noEmit": true,
8
+    "skipLibCheck": true,
9
+    "resolveJsonModule": true
10
+  }
62 11
 }

+ 1
- 1
index.d.ts Прегледај датотеку

@@ -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 Прегледај датотеку

@@ -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 Прегледај датотеку

@@ -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 Прегледај датотеку

@@ -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 Прегледај датотеку

@@ -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 {