├── .gitignore ├── tsconfig.json ├── dist ├── index.d.ts ├── index.js.map └── index.js ├── LICENSE ├── package.json ├── src └── index.ts ├── README.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "esModuleInterop": true, 5 | "isolatedModules": true, 6 | "target": "esnext", 7 | "outDir": "dist", 8 | "moduleResolution": "node", 9 | "sourceMap": true, 10 | "lib": ["es6"], 11 | "rootDir": "src" 12 | }, 13 | "include": ["src"], 14 | "exclude": ["node_modules"] 15 | } 16 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import * as FileSystem from "expo-file-system"; 2 | import { NotificationContentInput } from "expo-notifications"; 3 | export declare type EFDL_NotificationType = { 4 | notification: "managed" | "custom" | "none"; 5 | }; 6 | export declare type EDFL_NotificationContent = { 7 | downloading: NotificationContentInput; 8 | finished: NotificationContentInput; 9 | error: NotificationContentInput; 10 | }; 11 | export interface EFDL_Options { 12 | notificationType?: EFDL_NotificationType; 13 | notificationContent?: EDFL_NotificationContent; 14 | downloadProgressCallback?: FileSystem.DownloadProgressCallback; 15 | } 16 | export declare type EDFL_NotificationState = "downloading" | "finished" | "error"; 17 | export declare function downloadToFolder(uri: string, filename: string, folder: string, channelId: string, options?: EFDL_Options): Promise; 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Farhan Kathawala 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expo-file-dl", 3 | "version": "2.0.0", 4 | "description": "Download files to a specified folder and notify the user while the download is happening and when the download finishes", 5 | "keywords": [ 6 | "expo", 7 | "expo-notifications", 8 | "expo-file-downloader", 9 | "file-downloader", 10 | "react-native", 11 | "react-native-file-download" 12 | ], 13 | "homepage": "https://github.com/kathawala/expo-file-dl", 14 | "bugs": { 15 | "url": "https://github.com/kathawala/expo-file-dl/issues", 16 | "email": "farhansayshi@gmail.com" 17 | }, 18 | "main": "dist/index.js", 19 | "types": "dist/index.d.ts", 20 | "files": [ 21 | "dist/**/*" 22 | ], 23 | "funding": { 24 | "type": "individual", 25 | "url": "https://liberapay.com/kathawala/donate" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/kathawala/expo-file-dl" 30 | }, 31 | "author": "Farhan Kathawala ", 32 | "license": "MIT", 33 | "scripts": { 34 | "build": "tsc", 35 | "prepare": "yarn run build" 36 | }, 37 | "dependencies": { 38 | "expo-file-system": "^10.0.0", 39 | "expo-media-library": "^11.0.0", 40 | "expo-notifications": "^0.9.0", 41 | "expo-sharing": "^9.0.0" 42 | }, 43 | "peerDependencies": { 44 | "react": "*", 45 | "react-native": "*" 46 | }, 47 | "devDependencies": { 48 | "@types/react": "^16.9.56", 49 | "@types/react-native": "^0.63.35", 50 | "react-native-unimodules": "^0.11.0", 51 | "typescript": "^4.0.5" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | import * as FileSystem from "expo-file-system"; 2 | import * as Notifications from "expo-notifications"; 3 | import * as MediaLibrary from "expo-media-library"; 4 | import * as Sharing from "expo-sharing"; 5 | import { AndroidNotificationPriority, } from "expo-notifications"; 6 | import { Platform } from "react-native"; 7 | const ios = Platform.OS === "ios"; 8 | const imageFileExts = [ 9 | "jpg", 10 | "jpeg", 11 | "tiff", 12 | "tif", 13 | "raw", 14 | "dng", 15 | "png", 16 | "gif", 17 | "bmp", 18 | "heic", 19 | "webp", 20 | ]; 21 | const baseNotificationRequestInput = { 22 | identifier: "", 23 | content: { 24 | title: "", 25 | body: "", 26 | vibrate: [250], 27 | priority: AndroidNotificationPriority.HIGH, 28 | autoDismiss: true, 29 | sticky: false, 30 | }, 31 | trigger: { 32 | channelId: "", 33 | }, 34 | }; 35 | function initBaseNotificationRequestInput(filename, channelId) { 36 | let baseNotificationRI = { 37 | ...baseNotificationRequestInput, 38 | content: { 39 | ...baseNotificationRequestInput.content, 40 | title: filename, 41 | }, 42 | trigger: { 43 | channelId, 44 | seconds: 1, 45 | repeats: false, 46 | }, 47 | }; 48 | return baseNotificationRI; 49 | } 50 | function getNotifParams(baseNotificationRI, nState, nContent) { 51 | let identifier = ""; 52 | let body = ""; 53 | let sticky = false; 54 | let customNotifContent = {}; 55 | switch (nState) { 56 | case "downloading": 57 | identifier = `dl${baseNotificationRI.content.title}`; 58 | body = "Downloading..."; 59 | sticky = true; 60 | if (nContent !== undefined) 61 | customNotifContent = nContent.downloading; 62 | break; 63 | case "finished": 64 | identifier = `fin${baseNotificationRI.content.title}`; 65 | body = "Completed!"; 66 | sticky = false; 67 | if (nContent !== undefined) 68 | customNotifContent = nContent.finished; 69 | break; 70 | case "error": 71 | identifier = `err${baseNotificationRI.content.title}`; 72 | body = "Failed to download"; 73 | sticky = false; 74 | if (nContent !== undefined) 75 | customNotifContent = nContent.error; 76 | break; 77 | default: 78 | break; 79 | } 80 | return { 81 | ...baseNotificationRI, 82 | identifier, 83 | content: { 84 | ...baseNotificationRI.content, 85 | body, 86 | sticky, 87 | ...customNotifContent, 88 | }, 89 | }; 90 | } 91 | async function dismissAndShowErr(notifToDismissId, errNotificationRI) { 92 | if (notifToDismissId !== undefined) { 93 | await Notifications.dismissNotificationAsync(notifToDismissId); 94 | } 95 | await Notifications.scheduleNotificationAsync(errNotificationRI); 96 | return; 97 | } 98 | async function downloadFile(uri, fileUri, downloadProgressCallback) { 99 | if (downloadProgressCallback) { 100 | const downloadResumable = FileSystem.createDownloadResumable(uri, fileUri, {}, downloadProgressCallback); 101 | return await downloadResumable.downloadAsync(); 102 | } 103 | else { 104 | return await FileSystem.downloadAsync(uri, fileUri); 105 | } 106 | } 107 | // NOTE: This function assumes permissions have been granted and does not 108 | // take responsibilty for whether permissions are granted or not 109 | // IT WILL SILENTLY FAIL IF YOU DON'T REQUEST AND GET MEDIA_LIBRARY permissions 110 | export async function downloadToFolder(uri, filename, folder, channelId, options) { 111 | let baseNotificationRI = initBaseNotificationRequestInput(filename, channelId); 112 | const customNotifContent = options && 113 | options.notificationType && 114 | options.notificationType.notification === "custom" 115 | ? options.notificationContent 116 | : undefined; 117 | const skipNotifications = options && 118 | options.notificationType && 119 | options.notificationType.notification === "none"; 120 | const dlNotificationRI = getNotifParams(baseNotificationRI, "downloading", customNotifContent); 121 | const errNotificationRI = getNotifParams(baseNotificationRI, "error", customNotifContent); 122 | const finNotificationRI = getNotifParams(baseNotificationRI, "finished", customNotifContent); 123 | if (!skipNotifications) 124 | await Notifications.scheduleNotificationAsync(dlNotificationRI); 125 | const fileUri = `${FileSystem.documentDirectory}${filename}`; 126 | const downloadedFile = await downloadFile(uri, fileUri, options?.downloadProgressCallback); 127 | if (downloadedFile.status != 200) { 128 | if (!skipNotifications) 129 | await dismissAndShowErr(dlNotificationRI.identifier, errNotificationRI); 130 | return false; 131 | } 132 | try { 133 | // if this is not an image file on iOS 134 | // we use "Sharing" library and quit early (let iOS handle it) 135 | if (ios && 136 | imageFileExts.every((x) => !downloadedFile.uri.toLocaleLowerCase().endsWith(x))) { 137 | const UTI = "public.item"; 138 | const shareResult = await Sharing.shareAsync(downloadedFile.uri, { 139 | UTI, 140 | }); 141 | return true; 142 | } 143 | // the file is either a photo on iOS or any file type on Android 144 | // in which case we can download the file directly to the Download folder 145 | const asset = await MediaLibrary.createAssetAsync(downloadedFile.uri); 146 | const album = await MediaLibrary.getAlbumAsync(folder); 147 | if (album == null) { 148 | await MediaLibrary.createAlbumAsync(folder, asset, false); 149 | } 150 | else { 151 | await MediaLibrary.addAssetsToAlbumAsync([asset], album, false); 152 | } 153 | } 154 | catch (e) { 155 | console.log(`ERROR: ${e}`); 156 | if (!skipNotifications) 157 | await dismissAndShowErr(dlNotificationRI.identifier, errNotificationRI); 158 | return false; 159 | } 160 | if (skipNotifications) { 161 | return true; 162 | } 163 | else { 164 | if (dlNotificationRI.identifier !== undefined) { 165 | await Notifications.dismissNotificationAsync(dlNotificationRI.identifier); 166 | } 167 | await Notifications.scheduleNotificationAsync(finNotificationRI); 168 | return true; 169 | } 170 | } 171 | //# sourceMappingURL=index.js.map -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as FileSystem from "expo-file-system"; 2 | import * as Notifications from "expo-notifications"; 3 | import * as MediaLibrary from "expo-media-library"; 4 | import * as Sharing from "expo-sharing"; 5 | import { 6 | AndroidNotificationPriority, 7 | NotificationContentInput, 8 | NotificationRequestInput, 9 | } from "expo-notifications"; 10 | import { Platform } from "react-native"; 11 | 12 | const ios = Platform.OS === "ios"; 13 | const imageFileExts = [ 14 | "jpg", 15 | "jpeg", 16 | "tiff", 17 | "tif", 18 | "raw", 19 | "dng", 20 | "png", 21 | "gif", 22 | "bmp", 23 | "heic", 24 | "webp", 25 | ]; 26 | 27 | export type EFDL_NotificationType = { 28 | notification: "managed" | "custom" | "none"; 29 | }; 30 | 31 | export type EDFL_NotificationContent = { 32 | downloading: NotificationContentInput; 33 | finished: NotificationContentInput; 34 | error: NotificationContentInput; 35 | }; 36 | 37 | export interface EFDL_Options { 38 | notificationType?: EFDL_NotificationType; 39 | notificationContent?: EDFL_NotificationContent; 40 | downloadProgressCallback?: FileSystem.DownloadProgressCallback; 41 | } 42 | 43 | export type EDFL_NotificationState = "downloading" | "finished" | "error"; 44 | 45 | const baseNotificationRequestInput: NotificationRequestInput = { 46 | identifier: "", 47 | content: { 48 | title: "", 49 | body: "", 50 | vibrate: [250], 51 | priority: AndroidNotificationPriority.HIGH, 52 | autoDismiss: true, 53 | sticky: false, 54 | }, 55 | trigger: { 56 | channelId: "", 57 | }, 58 | }; 59 | 60 | function initBaseNotificationRequestInput( 61 | filename: string, 62 | channelId: string 63 | ): NotificationRequestInput { 64 | let baseNotificationRI = { 65 | ...baseNotificationRequestInput, 66 | content: { 67 | ...baseNotificationRequestInput.content, 68 | title: filename, 69 | }, 70 | trigger: { 71 | channelId, 72 | seconds: 1, 73 | repeats: false, 74 | }, 75 | }; 76 | return baseNotificationRI; 77 | } 78 | 79 | function getNotifParams( 80 | baseNotificationRI: NotificationRequestInput, 81 | nState: EDFL_NotificationState, 82 | nContent?: EDFL_NotificationContent 83 | ): NotificationRequestInput { 84 | let identifier = ""; 85 | let body = ""; 86 | let sticky = false; 87 | let customNotifContent: NotificationContentInput = {}; 88 | switch (nState) { 89 | case "downloading": 90 | identifier = `dl${baseNotificationRI.content.title}`; 91 | body = "Downloading..."; 92 | sticky = true; 93 | if (nContent !== undefined) customNotifContent = nContent.downloading; 94 | break; 95 | case "finished": 96 | identifier = `fin${baseNotificationRI.content.title}`; 97 | body = "Completed!"; 98 | sticky = false; 99 | if (nContent !== undefined) customNotifContent = nContent.finished; 100 | break; 101 | case "error": 102 | identifier = `err${baseNotificationRI.content.title}`; 103 | body = "Failed to download"; 104 | sticky = false; 105 | if (nContent !== undefined) customNotifContent = nContent.error; 106 | break; 107 | default: 108 | break; 109 | } 110 | 111 | return { 112 | ...baseNotificationRI, 113 | identifier, 114 | content: { 115 | ...baseNotificationRI.content, 116 | body, 117 | sticky, 118 | ...customNotifContent, 119 | }, 120 | }; 121 | } 122 | 123 | async function dismissAndShowErr( 124 | notifToDismissId: string | undefined, 125 | errNotificationRI: NotificationRequestInput 126 | ): Promise { 127 | if (notifToDismissId !== undefined) { 128 | await Notifications.dismissNotificationAsync(notifToDismissId); 129 | } 130 | await Notifications.scheduleNotificationAsync(errNotificationRI); 131 | return; 132 | } 133 | 134 | async function downloadFile( 135 | uri: string, 136 | fileUri: string, 137 | downloadProgressCallback?: FileSystem.DownloadProgressCallback 138 | ) { 139 | if (downloadProgressCallback) { 140 | const downloadResumable = FileSystem.createDownloadResumable( 141 | uri, 142 | fileUri, 143 | {}, 144 | downloadProgressCallback! 145 | ); 146 | return await downloadResumable.downloadAsync(); 147 | } else { 148 | return await FileSystem.downloadAsync(uri, fileUri); 149 | } 150 | } 151 | 152 | // NOTE: This function assumes permissions have been granted and does not 153 | // take responsibilty for whether permissions are granted or not 154 | // IT WILL SILENTLY FAIL IF YOU DON'T REQUEST AND GET MEDIA_LIBRARY permissions 155 | export async function downloadToFolder( 156 | uri: string, 157 | filename: string, 158 | folder: string, 159 | channelId: string, 160 | options?: EFDL_Options 161 | ): Promise { 162 | let baseNotificationRI: NotificationRequestInput = initBaseNotificationRequestInput( 163 | filename, 164 | channelId 165 | ); 166 | const customNotifContent = 167 | options && 168 | options.notificationType && 169 | options.notificationType.notification === "custom" 170 | ? options.notificationContent 171 | : undefined; 172 | const skipNotifications = 173 | options && 174 | options.notificationType && 175 | options.notificationType.notification === "none"; 176 | const dlNotificationRI: NotificationRequestInput = getNotifParams( 177 | baseNotificationRI, 178 | "downloading", 179 | customNotifContent 180 | ); 181 | const errNotificationRI: NotificationRequestInput = getNotifParams( 182 | baseNotificationRI, 183 | "error", 184 | customNotifContent 185 | ); 186 | const finNotificationRI: NotificationRequestInput = getNotifParams( 187 | baseNotificationRI, 188 | "finished", 189 | customNotifContent 190 | ); 191 | 192 | if (!skipNotifications) 193 | await Notifications.scheduleNotificationAsync(dlNotificationRI); 194 | 195 | const fileUri: string = `${FileSystem.documentDirectory}${filename}`; 196 | const downloadedFile: FileSystem.FileSystemDownloadResult = await downloadFile( 197 | uri, 198 | fileUri, 199 | options?.downloadProgressCallback 200 | ); 201 | 202 | if (downloadedFile.status != 200) { 203 | if (!skipNotifications) 204 | await dismissAndShowErr(dlNotificationRI.identifier, errNotificationRI); 205 | return false; 206 | } 207 | 208 | try { 209 | // if this is not an image file on iOS 210 | // we use "Sharing" library and quit early (let iOS handle it) 211 | if ( 212 | ios && 213 | imageFileExts.every( 214 | (x) => !downloadedFile.uri.toLocaleLowerCase().endsWith(x) 215 | ) 216 | ) { 217 | const UTI = "public.item"; 218 | const shareResult = await Sharing.shareAsync(downloadedFile.uri, { 219 | UTI, 220 | }); 221 | return true; 222 | } 223 | 224 | // the file is either a photo on iOS or any file type on Android 225 | // in which case we can download the file directly to the Download folder 226 | const asset = await MediaLibrary.createAssetAsync(downloadedFile.uri); 227 | const album = await MediaLibrary.getAlbumAsync(folder); 228 | if (album == null) { 229 | await MediaLibrary.createAlbumAsync(folder, asset, false); 230 | } else { 231 | await MediaLibrary.addAssetsToAlbumAsync([asset], album, false); 232 | } 233 | } catch (e) { 234 | console.log(`ERROR: ${e}`); 235 | if (!skipNotifications) 236 | await dismissAndShowErr(dlNotificationRI.identifier, errNotificationRI); 237 | return false; 238 | } 239 | 240 | if (skipNotifications) { 241 | return true; 242 | } else { 243 | if (dlNotificationRI.identifier !== undefined) { 244 | await Notifications.dismissNotificationAsync(dlNotificationRI.identifier); 245 | } 246 | await Notifications.scheduleNotificationAsync(finNotificationRI); 247 | return true; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![banner](https://storage.googleapis.com/gh-assets/expo-file-dl.png) 3 | 4 | 5 | # expo-file-dl 6 | 7 | 8 | ![GitHub last commit](https://img.shields.io/github/last-commit/kathawala/expo-file-dl) 9 | ![npm version](https://img.shields.io/npm/v/expo-file-dl) 10 | ![npm downloads weekly](https://img.shields.io/npm/dw/expo-file-dl) 11 | 12 | 13 | A library which allows you to download files to an arbitrary folder on the mobile device while updating you on the download progress and displaying notifications to the user about the status of the file download. Downloading files to a folder in Expo isn't super-obvious so this library is meant to bridge the gap a bit. 14 | To use this library you need to be using `expo-notifications` (bare and managed workflow both supported) and need to have the following in your app: 15 | 16 | 1. An existing notification channel 17 | 2. A notification-handler function 18 | 3. `CAMERA_ROLL` permissions granted by the user 19 | 20 | 21 | 22 | 37 | 38 | 39 | # Demo-Preview 40 | 41 | ![screencap](https://storage.googleapis.com/gh-assets/managed.gif) 42 | 43 | # Table of contents 44 | 45 | 48 | 49 | - [Table of contents](#table-of-contents) 50 | - [Installation](#installation) 51 | - [Managed Expo project](#managed-expo-project) 52 | - [Bare Expo project or plain React-Native project](#bare-expo-project-or-plain-react-native-project) 53 | - [Usage](#usage) 54 | - [downloadToFolder](#downloadtofolder) 55 | - [Development](#development) 56 | - [Contribute](#contribute) 57 | - [Sponsor](#sponsor) 58 | - [Liberapay](#liberapay) 59 | - [PayPal](#paypal) 60 | - [Adding new features or fixing bugs](#adding-new-features-or-fixing-bugs) 61 | 62 | # Installation 63 | [(Back to top)](#table-of-contents) 64 | 65 | ## Managed Expo project 66 | 67 | Just run 68 | 69 | ``` 70 | yarn add expo-file-dl 71 | ``` 72 | 73 | ## Bare Expo project or plain React-Native project 74 | 75 | First, you need to install `react-native-unimodules` if you haven't already. 76 | Follow [these instructions](https://docs.expo.io/bare/installing-unimodules/) to do so. 77 | 78 | Then, add `android:requestLegacyExternalStorage="true"` to your `AndroidManifest.xml` like so 79 | 80 | ```xml 81 | 82 | 83 | ... 84 | 85 | 86 | ``` 87 | 88 | Then add this to your `app.json` file 89 | 90 | ``` 91 | { 92 | "expo": { 93 | ... 94 | "android": { 95 | ... 96 | "useNextNotificationsApi": true, 97 | } 98 | } 99 | } 100 | ``` 101 | 102 | Finally, run 103 | 104 | ``` 105 | yarn add expo-file-dl 106 | ``` 107 | 108 | # Usage 109 | [(Back to top)](#table-of-contents) 110 | 111 | To see a full-code working example, you can check out this example app: [expo-file-dl-example](https://github.com/kathawala/expo-file-dl-example) 112 | 113 | There is a `bare` branch which has a working app using the bare workflow in addition to the `master` branch which uses the managed workflow 114 | 115 | To use the following functions, you need to have: 116 | 117 | 1. created a `NotificationChannel` using `Notifications.setNotificationChannelAsync` ([docs](https://docs.expo.io/versions/v39.0.0/sdk/notifications/#setnotificationchannelasyncidentifier-string-channel-notificationchannelinput-promisenotificationchannel--null)) 118 | 2. set up a `NotificationHandler` using `Notifications.setNotificationHandler` ([docs](https://docs.expo.io/versions/v39.0.0/sdk/notifications/#setnotificationchannelasyncidentifier-string-channel-notificationchannelinput-promisenotificationchannel--null)) 119 | 3. been granted `CAMERA_ROLL` permissions by the user, multiple ways to do this, one way is through `Permissions.askAsync(Permissions.CAMERA_ROLL)` ([docs](https://docs.expo.io/versions/v39.0.0/sdk/permissions/#permissionsaskasynctypes)) 120 | 121 | ## downloadToFolder 122 | 123 | simplest invocation 124 | 125 | ```jsx 126 | import { downloadToFolder } from 'expo-file-dl'; 127 | 128 | ... 129 | 130 |