feat(iOS): Add onFileDownload callback (#1214)

`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
This commit is contained in:
trcoffman 2020-04-29 09:09:22 -07:00 committed by GitHub
parent 694b07faa8
commit a6010d93e0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 176 additions and 22 deletions

View File

@ -67,6 +67,8 @@ static NSDictionary* customCertificatesForHost;
UIScrollViewDelegate,
#endif // !TARGET_OS_OSX
RCTAutoInsetsProtocol>
@property (nonatomic, copy) RCTDirectEventBlock onFileDownload;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingStart;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingFinish;
@property (nonatomic, copy) RCTDirectEventBlock onLoadingError;
@ -973,24 +975,42 @@ static NSDictionary* customCertificatesForHost;
decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse
decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler
{
WKNavigationResponsePolicy policy = WKNavigationResponsePolicyAllow;
if (_onHttpError && navigationResponse.forMainFrame) {
if ([navigationResponse.response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSInteger statusCode = response.statusCode;
if (statusCode >= 400) {
NSMutableDictionary<NSString *, id> *event = [self baseEvent];
[event addEntriesFromDictionary: @{
NSMutableDictionary<NSString *, id> *httpErrorEvent = [self baseEvent];
[httpErrorEvent addEntriesFromDictionary: @{
@"url": response.URL.absoluteString,
@"statusCode": @(statusCode)
}];
_onHttpError(event);
_onHttpError(httpErrorEvent);
}
NSString *disposition = nil;
if (@available(iOS 13, *)) {
disposition = [response valueForHTTPHeaderField:@"Content-Disposition"];
}
BOOL isAttachment = disposition != nil && [disposition hasPrefix:@"attachment"];
if (isAttachment || !navigationResponse.canShowMIMEType) {
if (_onFileDownload) {
policy = WKNavigationResponsePolicyCancel;
NSMutableDictionary<NSString *, id> *downloadEvent = [self baseEvent];
[downloadEvent addEntriesFromDictionary: @{
@"downloadUrl": (response.URL).absoluteString,
}];
_onFileDownload(downloadEvent);
}
}
}
}
}
decisionHandler(WKNavigationResponsePolicyAllow);
decisionHandler(policy);
}
/**

View File

@ -34,6 +34,7 @@ RCT_EXPORT_MODULE()
}
RCT_EXPORT_VIEW_PROPERTY(source, NSDictionary)
RCT_EXPORT_VIEW_PROPERTY(onFileDownload, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onLoadingStart, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onLoadingFinish, RCTDirectEventBlock)
RCT_EXPORT_VIEW_PROPERTY(onLoadingError, RCTDirectEventBlock)

View File

@ -226,9 +226,23 @@ You can control **single** or **multiple** file selection by specifing the [`mul
##### iOS
For iOS, all you need to do is specify the permissions in your `ios/[project]/Info.plist` file:
On iOS, you are going to have to supply your own code to download files. You can supply an `onFileDownload` callback
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
will be given to `onFileDownload`. From that callback you can then download that file however you would like to do so.
Save to gallery:
NOTE: iOS 13+ is needed for the best possible download experience. On iOS 13 Apple added an API for accessing HTTP response headers, which
is used to determine if an HTTP response should be a download. On iOS 12 or older, only MIME types that cannot be rendered by the webview will
trigger calls to `onFileDownload`.
Example:
```javascript
onFileDownload = ({ nativeEvent }) => {
const { downloadUrl } = nativeEvent;
// --> Your download code goes here <--
}
```
To be able to save images to the gallery you need to specify this permission in your `ios/[project]/Info.plist` file:
```
<key>NSPhotoLibraryAddUsageDescription</key>
@ -237,7 +251,8 @@ Save to gallery:
##### Android
Add permission in AndroidManifest.xml:
On Android, integration with the DownloadManager is built-in.
All you have to do to support downloads is add these permissions in AndroidManifest.xml:
```xml
<manifest ...>

View File

@ -65,6 +65,7 @@ This document lays out the current public properties and methods for the React N
- [`sharedCookiesEnabled`](Reference.md#sharedCookiesEnabled)
- [`textZoom`](Reference.md#textZoom)
- [`ignoreSilentHardwareSwitch`](Reference.md#ignoreSilentHardwareSwitch)
- [`onFileDownload`](Reference.md#onFileDownload)
## Methods Index
@ -1138,6 +1139,38 @@ When set to true the hardware silent switch is ignored. Default: `false`
| ------- | -------- | -------- |
| boolean | No | iOS |
### `onFileDownload`
This property is iOS-only.
Function that is invoked when the client needs to download a file.
iOS 13+ only: If the webview navigates to a URL that results in an HTTP
response with a Content-Disposition header 'attachment...', then
this will be called.
iOS 8+: If the MIME type indicates that the content is not renderable by the
webview, that will also cause this to be called. On iOS versions before 13,
this is the only condition that will cause this function to be called.
The application will need to provide its own code to actually download
the file.
If not provided, the default is to let the webview try to render the file.
Example:
```jsx
<WebView
source={{ uri: 'https://reactnative.dev' }}
onFileDownload={ ( { nativeEvent: { downloadUrl } } ) => {
// You use downloadUrl which is a string to download files however you want.
}}
/>
```
| Type | Required | Platform |
| ------- | -------- | -------- |
| function | No | iOS |
## Methods
### `extraNativeComponentConfig()`

View File

@ -13,6 +13,7 @@ import {
import Alerts from './examples/Alerts';
import Scrolling from './examples/Scrolling';
import Background from './examples/Background';
import Downloads from './examples/Downloads';
import Uploads from './examples/Uploads';
import Injection from './examples/Injection';
@ -41,6 +42,14 @@ const TESTS = {
return <Background />;
},
},
Downloads: {
title: 'Downloads',
testId: 'downloads',
description: 'File downloads test',
render() {
return <Downloads />;
},
},
Uploads: {
title: 'Uploads',
testId: 'uploads',
@ -115,6 +124,11 @@ export default class App extends Component<Props, State> {
title="Injection"
onPress={() => this._changeTest('Injection')}
/>
{Platform.OS == "ios" && <Button
testID="testType_downloads"
title="Downloads"
onPress={() => this._changeTest('Downloads')}
/>}
{Platform.OS === 'android' && <Button
testID="testType_uploads"
title="Uploads"

View File

@ -0,0 +1,55 @@
import React, {Component} from 'react';
import {Alert, Platform, View} from 'react-native';
import WebView, {FileDownload} from 'react-native-webview';
const HTML = `
<!DOCTYPE html>\n
<html>
<head>
<title>Downloads</title>
<meta http-equiv="content-type" content="text/html; charset=utf-8">
<meta name="viewport" content="width=320, user-scalable=no">
<style type="text/css">
body {
margin: 0;
padding: 0;
font: 62.5% arial, sans-serif;
background: #ccc;
}
</style>
</head>
<body>
<a href="https://www.7-zip.org/a/7za920.zip">Example zip file download</a>
</body>
</html>
`;
type Props = {};
type State = {};
export default class Downloads extends Component<Props, State> {
state = {};
onFileDownload = ({ nativeEvent }: { nativeEvent: FileDownload } ) => {
Alert.alert("File download detected", nativeEvent.downloadUrl);
};
render() {
const platformProps = Platform.select({
ios: {
onFileDownload: this.onFileDownload,
},
});
return (
<View style={{ height: 120 }}>
<WebView
source={{html: HTML}}
automaticallyAdjustContentInsets={false}
{...platformProps}
/>
</View>
);
}
}

View File

@ -182,7 +182,7 @@ PODS:
- React-cxxreact (= 0.61.5)
- React-jsi (= 0.61.5)
- React-jsinspector (0.61.5)
- react-native-webview (8.2.0):
- react-native-webview (9.3.0):
- React
- React-RCTActionSheet (0.61.5):
- React-Core/RCTActionSheetHeaders (= 0.61.5)
@ -326,7 +326,7 @@ SPEC CHECKSUMS:
React-jsi: cb2cd74d7ccf4cffb071a46833613edc79cdf8f7
React-jsiexecutor: d5525f9ed5f782fdbacb64b9b01a43a9323d2386
React-jsinspector: fa0ecc501688c3c4c34f28834a76302233e29dc0
react-native-webview: 1db33907230d0eb344964d6f3bb56b9ee77e25a4
react-native-webview: 60d883f994e96a560756c14592552e02a06d604d
React-RCTActionSheet: 600b4d10e3aea0913b5a92256d2719c0cdd26d76
React-RCTAnimation: 791a87558389c80908ed06cc5dfc5e7920dfa360
React-RCTBlob: d89293cc0236d9cb0933d85e430b0bbe81ad1d72
@ -339,6 +339,6 @@ SPEC CHECKSUMS:
ReactCommon: 198c7c8d3591f975e5431bec1b0b3b581aa1c5dd
Yoga: f2a7cd4280bfe2cca5a7aed98ba0eb3d1310f18b
PODFILE CHECKSUM: 2b0bdb79b803eefe541da6f9be6b06e99063bbfd
PODFILE CHECKSUM: c2e136f84288494fa269d79892a8a1cf7acd7c71
COCOAPODS: 1.8.4

2
index.d.ts vendored
View File

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

View File

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

View File

@ -113,6 +113,10 @@ export interface WebViewNavigation extends WebViewNativeEvent {
mainDocumentURL?: string;
}
export interface FileDownload {
downloadUrl: string;
}
export type DecelerationRateConstant = 'normal' | 'fast';
export interface WebViewMessage extends WebViewNativeEvent {
@ -141,6 +145,8 @@ export type WebViewProgressEvent = NativeSyntheticEvent<
export type WebViewNavigationEvent = NativeSyntheticEvent<WebViewNavigation>;
export type FileDownloadEvent = NativeSyntheticEvent<FileDownload>;
export type WebViewMessageEvent = NativeSyntheticEvent<WebViewMessage>;
export type WebViewErrorEvent = NativeSyntheticEvent<WebViewError>;
@ -302,6 +308,7 @@ export interface IOSNativeWebViewProps extends CommonNativeWebViewProps {
onContentProcessDidTerminate?: (event: WebViewTerminatedEvent) => void;
injectedJavaScriptForMainFrameOnly?: boolean;
injectedJavaScriptBeforeContentLoadedForMainFrameOnly?: boolean;
onFileDownload?: (event: FileDownloadEvent) => void;
}
export interface MacOSNativeWebViewProps extends CommonNativeWebViewProps {
@ -512,6 +519,24 @@ export interface IOSWebViewProps extends WebViewSharedProps {
* @platform ios
*/
injectedJavaScriptBeforeContentLoadedForMainFrameOnly?: boolean;
/**
* Function that is invoked when the client needs to download a file.
*
* iOS 13+ only: If the webview navigates to a URL that results in an HTTP
* response with a Content-Disposition header 'attachment...', then
* this will be called.
*
* iOS 8+: If the MIME type indicates that the content is not renderable by the
* webview, that will also cause this to be called. On iOS versions before 13,
* this is the only condition that will cause this function to be called.
*
* The application will need to provide its own code to actually download
* the file.
*
* If not provided, the default is to let the webview try to render the file.
*/
onFileDownload?: (event: FileDownloadEvent) => void;
}
export interface MacOSWebViewProps extends WebViewSharedProps {

View File

@ -1502,11 +1502,6 @@
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e"
integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==
"@types/uuid@^7.0.2":
version "7.0.2"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-7.0.2.tgz#d680a9c596ef84abf5c4c07a32ffd66d582526f8"
integrity sha512-8Ly3zIPTnT0/8RCU6Kg/G3uTICf9sRwYOpUzSIM3503tLIKcnJPRuinHhXngJUy2MntrEf6dlpOHXJju90Qh5w==
"@types/yargs-parser@*":
version "13.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-13.0.0.tgz#453743c5bbf9f1bed61d959baab5b06be029b2d0"
@ -10159,11 +10154,6 @@ uuid@^3.3.2, uuid@^3.3.3:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.3.tgz#4568f0216e78760ee1dbf3a4d2cf53e224112866"
integrity sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==
uuid@^7.0.3:
version "7.0.3"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==
v8-compile-cache@^2.0.3:
version "2.1.0"
resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e"