mirror of
https://github.com/status-im/react-native-webview.git
synced 2025-02-22 16:58:34 +00:00
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:
parent
694b07faa8
commit
a6010d93e0
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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)
|
||||
|
@ -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 ...>
|
||||
|
@ -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()`
|
||||
|
@ -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"
|
||||
|
55
example/examples/Downloads.tsx
Normal file
55
example/examples/Downloads.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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
2
index.d.ts
vendored
@ -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;
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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 {
|
||||
|
10
yarn.lock
10
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user