@kesha-antonov/react-native-background-downloader 4.5.3 → 4.5.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,294 @@
1
+ # Changelog
2
+
3
+ ## v4.5.3
4
+
5
+ ### ✨ New Features
6
+
7
+ - **Android: Notification Grouping Mode (`summaryOnly`):** Added `mode` option to `NotificationsGroupingConfig`. Set `mode: 'summaryOnly'` to show only the summary notification for a group while individual download notifications are minimized (ultra-silent, no alert). Useful for keeping the notification shade clean during large batch downloads.
8
+ - `'individual'` (default) — all notifications shown, current behavior unchanged
9
+ - `'summaryOnly'` — only the group summary notification is shown with aggregate progress; individual notifications are invisible/silent
10
+ - **Android: Progress-Based Summary Notification:** In `summaryOnly` mode, the group summary notification now displays aggregate progress (total bytes downloaded / total bytes) across all downloads in the group.
11
+ - **Android: Auto-Remove Completed Downloads from Group:** Completed downloads are now automatically removed from notification groups, keeping the summary accurate.
12
+
13
+ ### 🏗️ Architecture Changes
14
+
15
+ - **JS: `NotificationGroupingMode` Type:** New exported type `'individual' | 'summaryOnly'` for the `mode` field in `NotificationsGroupingConfig`.
16
+ - **Android: Ultra-Silent Notification Channel:** Added `NOTIFICATION_CHANNEL_ULTRA_SILENT_ID` (`IMPORTANCE_MIN`) channel used for individual notifications in `summaryOnly` mode.
17
+ - **Android: `updateSummaryNotificationForGroup()`:** New method that dispatches to the correct summary update strategy based on grouping mode.
18
+
19
+ ---
20
+
21
+ ## v4.5.2
22
+
23
+ ### ✨ New Features
24
+
25
+ - **Update Headers on Paused Downloads:** Added ability to update headers (e.g., refresh auth tokens) on paused download tasks before resuming. Use `task.setDownloadParams()` to update headers while paused, then `task.resume()` to continue the download with new headers.
26
+ - **Use case:** User pauses a large download, returns hours/days later when auth token has expired. Now you can refresh the token and resume without restarting the download.
27
+ - **iOS:** Creates a fresh request with HTTP Range header and updated headers on resume
28
+ - **Android:** Updates both in-memory headers and persisted paused download state
29
+
30
+ ### 🏗️ Architecture Changes
31
+
32
+ - **JS: `setDownloadParams()` Now Async:** The `DownloadTask.setDownloadParams()` method is now async and returns `Promise<boolean>` indicating whether native headers were updated (true when task is paused).
33
+ - **Native: Added `updateTaskHeaders` Method:** New native method on iOS and Android to update headers for paused tasks.
34
+
35
+ ### 📚 Documentation
36
+
37
+ - Added "Updating headers on paused downloads" section to README
38
+ - Added `setDownloadParams()` method documentation to API.md
39
+ - Added iOS "Updating Headers on Paused Downloads" section to PLATFORM_NOTES.md
40
+
41
+ ---
42
+
43
+ ## v4.5.1
44
+
45
+ ### 🏗️ Architecture Changes
46
+
47
+ - **Android: UIDT Code Refactoring:** Extracted 980-line monolithic `UIDTDownloadJobService.kt` into modular components:
48
+ - `uidt/UIDTJobState.kt` - Data classes, constants, job registry
49
+ - `uidt/UIDTNotificationManager.kt` - All notification logic
50
+ - `uidt/UIDTJobManager.kt` - Job scheduling, cancel, pause, resume
51
+ - `utils/ProgressUtils.kt` - Progress calculation utilities
52
+ - Backward compatibility maintained via companion object delegates
53
+ - **Android: Removed Redundant jobScheduler.cancel():** In `pauseJob()`, removed unnecessary `jobScheduler.cancel()` after `jobFinished(params, false)` since `wantsReschedule=false` already tells the system the job is complete.
54
+
55
+ ---
56
+
57
+ ## v4.5.0
58
+
59
+ ### ✨ New Features
60
+
61
+ - **Android: Global Notification Configuration:** Added `showNotificationsEnabled` and `notificationsGrouping` config options for controlling UIDT notifications globally via `setConfig()`.
62
+ - **Android: Customizable Paused Notification Text:** Added `downloadPaused` to `NotificationTexts` interface for customizing the "Paused" notification text.
63
+ - **Android: Notification Update Throttling:** Notification updates are now synced with `progressInterval` for consistent UI/notification progress display.
64
+
65
+ ### 🐛 Bug Fixes
66
+
67
+ - **Android: Paused Downloads Continuing in Background:** Fixed paused UIDT downloads continuing to download in background after app restart. Now UIDT job is fully cancelled on pause, with a detached "Paused" notification shown.
68
+ - **Android: Duplicate Notifications After Resume:** Fixed duplicate notifications appearing when resuming downloads after app restart. Now uses stable notification IDs based on `configId.hashCode()`.
69
+ - **Android: Notification Not Updating After Resume:** Fixed notification stuck on old progress after resuming. Now resets notification timing on resume and shows correct progress immediately.
70
+ - **Android: Notification Updating While Paused:** Fixed notification progress updating even when download is paused.
71
+ - **Android: Stale Notifications on App Close:** All download notifications are now cancelled when the app closes via `invalidate()`.
72
+
73
+ ### 💥 Breaking Changes
74
+
75
+ - **Removed Per-Task Notification Options:** `isNotificationVisible` and `notificationTitle` removed from `DownloadParams` and `UploadParams`. Use global `setConfig({ showNotificationsEnabled, notificationsGrouping })` instead.
76
+
77
+ ### 🏗️ Architecture Changes
78
+
79
+ - **Android: Pause Behavior on Android 14+:** Complete redesign of pause/resume for User-Initiated Data Transfer (UIDT) jobs:
80
+ - **Problem:** UIDT jobs continue running in background even after app closes, causing "paused" downloads to secretly continue downloading.
81
+ - **Solution:** On pause, the UIDT job is properly terminated via `jobFinished(params, false)`. Download state is persisted to disk for resumption via HTTP Range headers.
82
+ - **UX:** A detached "Paused" notification (using `JOB_END_NOTIFICATION_POLICY_DETACH`) remains visible after job termination. On resume, a new UIDT job is created.
83
+ - **Follows Google's UIDT best practices:** State saved even without `onStopJob`, `jobFinished()` called on completion, notifications updated periodically with throttling.
84
+ - **Android: Separate Notification Channels:** Added separate channels for visible (`IMPORTANCE_LOW`) and silent (`IMPORTANCE_MIN`) notifications.
85
+
86
+ ### ✨ Improvements
87
+
88
+ - **Android: Cleaner Notifications:** Added `setOnlyAlertOnce(true)` and `setShowWhen(false)` to all notifications for less intrusive updates.
89
+ - **Example App: Persistent Notification Settings:** Show Notifications and Notification Grouping toggles are now persisted with MMKV.
90
+ - **Example App: Android 13+ Permission Request:** Added POST_NOTIFICATIONS permission request when enabling notifications.
91
+
92
+ ### 📚 Documentation
93
+
94
+ - Updated README with notification behavior during pause/resume
95
+ - Updated API.md with new notification configuration options
96
+ - Documented that notifications are removed when app closes
97
+
98
+ ---
99
+
100
+ ## v4.4.5
101
+
102
+ ### 🐛 Bug Fixes
103
+
104
+ - **Android: Stop Task Not Working on Android 14+:** Fixed `stopTask()` not actually stopping UIDT downloads on Android 14+. The JobScheduler job was cancelled but the underlying HTTP download continued. Now properly calls `resumableDownloader.cancel()` before removing from active jobs.
105
+ - **Android: Paused Tasks Not Persisting Across App Restarts:** Fixed paused UIDT downloads losing their state when the app was restarted. Added `getJobDownloadState()` to retrieve download state from active UIDT jobs and `savePausedDownloadState()` to properly persist pause state.
106
+ - **Android: ACCESS_NETWORK_STATE Permission:** Added missing permission required for JobScheduler network connectivity constraints on Android 14+.
107
+ - **Android: Downloaded Files List Showing Incomplete Files:** The "Downloaded Files" section in the example app now correctly filters out files that have active (non-DONE) download tasks, preventing incomplete files from appearing in the list.
108
+
109
+ ### 🧹 Code Cleanup
110
+
111
+ - **Removed Verbose Debug Logs:** Cleaned up extensive debug logging in `StorageManager`, `Downloader`, and `RNBackgroundDownloaderModuleImpl` that was cluttering production logs. Removed serialization/deserialization logs, verification reads, and per-item iteration logs while keeping error logging.
112
+ - **Simplified Kotlin Code:** Removed unnecessary `else` blocks containing only debug/warning logs from `pauseTask()` and `resumeTask()` methods for cleaner code.
113
+
114
+ ### ✨ Improvements
115
+
116
+ - **TypeScript: Added `destination` to Task Info:** The `destination` field is now returned from `getExistingDownloadTasks()` for paused downloads, allowing the app to know where the file will be saved.
117
+
118
+ ### 📚 Documentation
119
+
120
+ - Added `skipMmkvDependency` option documentation to README for Expo plugin
121
+
122
+ ---
123
+
124
+ ## v4.4.4
125
+
126
+ ### 🐛 Bug Fixes
127
+
128
+ - **Expo Plugin: Fixed TypeScript Types:** Corrected TypeScript type definitions in the Expo config plugin.
129
+
130
+ ---
131
+
132
+ ## v4.4.3
133
+
134
+ ### ✨ New Features
135
+
136
+ - **Expo Plugin: Auto-detect react-native-mmkv:** The Expo config plugin now automatically detects if `react-native-mmkv` is installed and skips adding the MMKV dependency to avoid duplicate class errors. Use `skipMmkvDependency: true` option to manually skip if needed.
137
+ - **Android: Version from package.json:** Android native code now reads the library version from `package.json` instead of hardcoding it.
138
+
139
+ ---
140
+
141
+ ## v4.4.2
142
+
143
+ ### 🐛 Bug Fixes
144
+
145
+ - **Kotlin 2.0 Compatibility:** Fixed compilation error with Kotlin 2.0 (React Native 0.77+) by updating `progressReporter` to use named parameter syntax. This ensures compatibility with both Kotlin 1.9 (RN 0.76) and Kotlin 2.x (RN 0.77+).
146
+
147
+ ---
148
+
149
+ ## v4.4.1
150
+
151
+ ### 🐛 Bug Fixes
152
+
153
+ - **Android: Paused Tasks Persistence:** Fixed paused downloads not being restored after app restart on Android. Added persistent storage for paused download state using MMKV/SharedPreferences.
154
+ - **iOS: Improved Pause/Resume Handling:** Better handling of pause/resume operations on app restarts for iOS.
155
+ - **Upload Task App Restart Recovery:** Fixed upload tasks not being recoverable after app restart (#143). Added persistent storage for upload task configurations.
156
+
157
+ ### ✨ Improvements
158
+
159
+ - **Example App:** Added task list display with animations and improved UI for managing downloads.
160
+
161
+ ### 📚 Documentation
162
+
163
+ - Updated README with clearer MMKV dependency instructions
164
+ - Added information about resuming tasks after app restarts
165
+ - Updated authors section
166
+
167
+ ---
168
+
169
+ ## v4.4.0
170
+
171
+ ### ✨ New Features
172
+
173
+ - **Android 16 UIDT Support:** Downloads are now automatically marked as User-Initiated Data Transfers on Android 16+ (API 36) to prevent thermal throttling and job quota restrictions. Downloads will continue reliably even under moderate thermal conditions (~40°C).
174
+
175
+ ### 🐛 Bug Fixes
176
+
177
+ - **iOS MMKV Conflict Fix:** Removed hard MMKV dependency from iOS podspec to prevent symbol conflicts with `react-native-mmkv`. Apps using `react-native-mmkv` no longer experience crashes (EXC_BAD_ACCESS) on iOS.
178
+
179
+ ### 📦 Dependencies & Infrastructure
180
+
181
+ - **New Android Permission:** Added `RUN_USER_INITIATED_JOBS` permission for Android 16+ UIDT support
182
+ - **iOS MMKV Dependency:** MMKV is no longer a hard dependency in the podspec. Apps not using `react-native-mmkv` must add `pod 'MMKV', '>= 1.0.0'` to their Podfile.
183
+
184
+ ### 📚 Documentation
185
+
186
+ - Added documentation about Android 16+ UIDT support in README
187
+ - Added iOS MMKV dependency section in README (similar to Android section)
188
+ - Added migration guide for iOS MMKV dependency change
189
+
190
+ ---
191
+
192
+ ## v4.2.0
193
+
194
+ > 📖 **Upgrading from v4.1.x?** See the [Migration Guide](./MIGRATION.md) for details on the new Android pause/resume functionality.
195
+
196
+ ### ✨ New Features
197
+
198
+ - **Android Pause/Resume Support:** Android now fully supports `task.pause()` and `task.resume()` methods using HTTP Range headers. Downloads can be paused and resumed just like on iOS.
199
+ - **Background Download Service:** Added `ResumableDownloadService` - a foreground service that ensures downloads continue even when the app is in the background or the screen is off.
200
+ - **WakeLock Support:** Downloads maintain a partial wake lock to prevent the device from sleeping during active downloads.
201
+ - **`bytesTotal` Unknown Size Handling:** When the server doesn't provide a `Content-Length` header, `bytesTotal` now returns `-1` instead of `0` to distinguish "unknown size" from "zero bytes".
202
+
203
+ ### 🐛 Bug Fixes
204
+
205
+ - **Android Pause Error:** Fixed `COULD_NOT_FIND` error when pausing downloads on Android by properly tracking pausing state
206
+ - **Temp File Cleanup:** Fixed temp files (`.tmp`) not being deleted when stopping or deleting paused downloads
207
+ - **Content-Length Handling:** Fixed progress percentage calculation when server doesn't provide Content-Length header
208
+
209
+ ### 📦 Dependencies & Infrastructure
210
+
211
+ - **New Android Permissions:** Added `FOREGROUND_SERVICE`, `FOREGROUND_SERVICE_DATA_SYNC`, and `WAKE_LOCK` permissions for background download support
212
+ - **New Service:** Added `ResumableDownloadService` with `dataSync` foreground service type
213
+
214
+ ### 📚 Documentation
215
+
216
+ - Updated README to reflect Android pause/resume support
217
+ - Removed "iOS only" notes from pause/resume documentation
218
+ - Added documentation about `bytesTotal` returning `-1` for unknown sizes
219
+ - Updated Android DownloadManager Limitations section with new pause/resume implementation details
220
+
221
+ ---
222
+
223
+ ## v4.1.0
224
+
225
+ > 📖 **Upgrading from v4.0.x?** See the [Migration Guide](./MIGRATION.md) for the MMKV dependency change.
226
+
227
+ ### ⚠️ Breaking Changes
228
+
229
+ - **MMKV Dependency Changed to `compileOnly`:** The MMKV dependency is now `compileOnly` instead of `implementation` to avoid duplicate class errors when the app also uses `react-native-mmkv`. Apps not using `react-native-mmkv` must now explicitly add the MMKV dependency.
230
+
231
+ ### ✨ New Features
232
+
233
+ - **Expo Plugin Android Support:** The Expo config plugin now automatically adds the MMKV dependency on Android. Use `addMmkvDependency: false` option if you're already using `react-native-mmkv`.
234
+
235
+ ### 🐛 Bug Fixes
236
+
237
+ - **Duplicate Class Errors:** Fixed potential duplicate class errors when app uses both this library and `react-native-mmkv` by changing MMKV to `compileOnly` dependency
238
+
239
+ ### 📚 Documentation
240
+
241
+ - Added documentation for MMKV dependency requirements in README
242
+ - Updated Platform-Specific Limitations section with MMKV setup instructions
243
+ - Added Expo plugin options documentation
244
+
245
+ ---
246
+
247
+ ## v4.0.0
248
+
249
+ > 📖 **Upgrading from v3.x?** See the [Migration Guide](./MIGRATION.md) for detailed instructions.
250
+
251
+ ### ⚠️ Breaking Changes
252
+
253
+ - **API Renamed:** `checkForExistingDownloads()` → `getExistingDownloadTasks()` - Now returns a Promise with better naming
254
+ - **API Renamed:** `download()` → `createDownloadTask()` - Downloads now require explicit `.start()` call
255
+ - **Download Tasks Start Explicitly:** Tasks created with `createDownloadTask()` are now in `PENDING` state and must call `.start()` to begin downloading
256
+ - **New Config Option:** Added `progressMinBytes` to `setConfig()` - controls minimum bytes change before progress callback fires (default: 1MB)
257
+ - **Source Structure Changed:** Code moved from `lib/` to `src/` directory with proper TypeScript types
258
+
259
+ ### ✨ New Features
260
+
261
+ - **React Native New Architecture Support:** Full TurboModules support for both iOS and Android
262
+ - **Expo Config Plugin:** Added automatic iOS native code integration for Expo projects via `app.plugin.js`
263
+ - **Android Kotlin Migration:** All Java code converted to Kotlin
264
+ - **`maxRedirects` Option:** Configure maximum redirects for Android downloads (resolves #15)
265
+ - **`progressMinBytes` Option:** Hybrid progress reporting - callbacks fire based on time interval OR bytes downloaded
266
+ - **Android 15+ Support:** Added support for 16KB memory page sizes
267
+ - **Architecture Fallback:** Comprehensive x86/ARMv7 support with SharedPreferences fallback
268
+
269
+ ### 🐛 Bug Fixes
270
+
271
+ - **iOS Pause/Resume:** Fixed pause and resume functionality on iOS
272
+ - **RN 0.78+ Compatibility:** Fixed bridge checks with safe emitter checks
273
+ - **New Architecture Events:** Fixed `downloadBegin` and `downloadProgress` events emission
274
+ - **Android Background Downloads:** Fixed completed files not moving to destination
275
+ - **Progress Callback Unknown Total:** Fixed progress callback not firing when total bytes unknown
276
+ - **Android 12 MMKV Crash:** Added robust error handling
277
+ - **`checkForExistingDownloads` TypeError:** Fixed TypeError on Android with architecture fallback
278
+ - **Firebase Performance Compatibility:** Fixed `completeHandler` method compatibility on Android
279
+ - **Slow Connection Handling:** Better handling of slow-responding URLs with timeouts
280
+ - **Android OldArch Export:** Fixed module method export issue (#79)
281
+ - **MMKV Compatibility:** Support for react-native-mmkv 4+ with mmkv-shared dependency
282
+
283
+ ### 📦 Dependencies & Infrastructure
284
+
285
+ - **React Native:** Updated example app to RN 0.81.4
286
+ - **TypeScript:** Full TypeScript types in `src/types.ts`
287
+ - **iOS Native:** Converted from `.m` to `.mm` (Objective-C++)
288
+ - **Package Manager:** Switched to yarn as preferred package manager
289
+
290
+ ### 📚 Documentation
291
+
292
+ - Added documentation for `progressMinBytes` option
293
+ - Updated README for React Native 0.77+ instructions
294
+ - Improved Expo config plugin examples
package/README.md CHANGED
@@ -15,14 +15,14 @@
15
15
  <h1 align="center">React Native Background Downloader</h1>
16
16
 
17
17
  <p align="center">
18
- Download and upload large files on iOS & Android — even when your app is in the background or terminated.
18
+ Download and upload large files on iOS & Android — even when your app is in the background or terminated by the OS.
19
19
  </p>
20
20
 
21
21
  ---
22
22
 
23
23
  ## ✨ Features
24
24
 
25
- - 📥 **Background Downloads** - Downloads continue even when app is in background or terminated
25
+ - 📥 **Background Downloads** - Downloads continue even when app is in background or terminated by the OS
26
26
  - 📤 **Background Uploads** - Upload files reliably in the background
27
27
  - ⏸️ **Pause/Resume** - Full pause and resume support on both iOS and Android
28
28
  - 🔄 **Re-attach to Downloads** - Reconnect to ongoing downloads after app restart
@@ -37,7 +37,7 @@
37
37
  **The Problem:** Standard network requests in React Native are tied to your app's lifecycle. When the user switches to another app or the OS terminates your app to free memory, your downloads stop. For small files this is fine, but for large files (videos, podcasts, documents) this creates a frustrating user experience.
38
38
 
39
39
  **The Solution:** Both iOS and Android provide system-level APIs for background file transfers:
40
- - **iOS:** [`NSURLSession`](https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background) - handles downloads in a separate process, continuing even after your app is terminated
40
+ - **iOS:** [`NSURLSession`](https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background) - handles downloads in a separate process, continuing even after your app is terminated by the OS. Note: if the user explicitly force-kills the app via the App Switcher, iOS cancels all background tasks — this is an iOS system limitation that cannot be overridden
41
41
  - **Android:** A combination of [`DownloadManager`](https://developer.android.com/reference/android/app/DownloadManager) for system-managed downloads, [Foreground Services](https://developer.android.com/develop/background-work/services/foreground-services) for pause/resume support, and [MMKV](https://github.com/Tencent/MMKV) for persistent state storage
42
42
 
43
43
  **The Challenge:** These APIs are powerful but complex. Downloads run in a separate process, so your app might restart from scratch while downloads are still in progress. Keeping your UI in sync with background downloads requires careful state management.
@@ -53,6 +53,7 @@
53
53
  - [📦 Installation](#-installation)
54
54
  - [Expo Projects](#expo-projects)
55
55
  - [Bare React Native Projects](#bare-react-native-projects)
56
+ - [MMKV version comparison](#mmkv-version-comparison)
56
57
  - [🚀 Usage](#-usage)
57
58
  - [Downloading a file](#downloading-a-file)
58
59
  - [Re-Attaching to background tasks](#re-attaching-to-background-tasks)
@@ -113,7 +114,7 @@ export default {
113
114
  expo: {
114
115
  plugins: [
115
116
  ["@kesha-antonov/react-native-background-downloader", {
116
- mmkvVersion: "2.2.4", // Customize MMKV version on Android
117
+ mmkvVersion: "1.3.16", // Customize MMKV version on Android
117
118
  skipMmkvDependency: true // Skip if you want to add MMKV manually
118
119
  }]
119
120
  ]
@@ -123,8 +124,8 @@ export default {
123
124
 
124
125
  | Option | Type | Default | Description |
125
126
  |--------|------|---------|-------------|
126
- | `mmkvVersion` | string | `'2.2.4'` | The version of [MMKV](https://github.com/Tencent/MMKV/releases) to use on Android. |
127
- | `skipMmkvDependency` | boolean | `false` | Skip adding MMKV dependency. Set to `true` if you're using [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) to avoid duplicate class errors. The plugin auto-detects `react-native-mmkv` but you can use this option to explicitly skip. |
127
+ | `mmkvVersion` | string | `'1.3.16'` | The version of [MMKV](https://github.com/Tencent/MMKV/releases) to use on Android. See [MMKV version comparison](#mmkv-version-comparison) for details. |
128
+ | `skipMmkvDependency` | boolean | `false` | Skip adding MMKV dependency. Set to `true` if you're using [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) to avoid duplicate class errors. The plugin auto-detects `react-native-mmkv` but you can use this option to explicitly skip. See [MMKV version comparison](#mmkv-version-comparison). |
128
129
 
129
130
  </details>
130
131
 
@@ -214,11 +215,29 @@ Add MMKV to your `android/app/build.gradle`:
214
215
 
215
216
  ```gradle
216
217
  dependencies {
217
- implementation 'com.tencent:mmkv-shared:2.2.4'
218
+ implementation 'com.tencent:mmkv-shared:1.3.16'
218
219
  }
219
220
  ```
220
221
 
221
- > **Note:** If you're already using [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) in your project, skip this step — it already includes MMKV.
222
+ > **Note:** If you're already using [react-native-mmkv](https://github.com/mrousavy/react-native-mmkv) in your project, skip this step — it already includes MMKV. Note that `react-native-mmkv` v4.x uses [Margelo's fork of MMKV](https://github.com/margelo/MMKV) (`io.github.zhongwuzw:mmkv`) which re-adds armeabi-v7a (32-bit ARM) support that was dropped in the official MMKV 2.x release.
223
+
224
+ > **⚠️ armeabi-v7a (32-bit ARM) users:** MMKV 2.x dropped 32-bit ABI support (since v2.0.0). If you need armeabi-v7a support and get a CMake error like `No compatible library found for //mmkv/mmkv`, use the MMKV 1.3.x LTS series instead — it supports both armeabi-v7a **and** 16KB page sizes (since v1.3.14):
225
+ > ```gradle
226
+ > dependencies {
227
+ > implementation 'com.tencent:mmkv-shared:1.3.16'
228
+ > }
229
+ > ```
230
+
231
+ #### MMKV version comparison
232
+
233
+ | Dependency | armeabi-v7a (32-bit) | arm64-v8a | 16KB page size | Recommended for |
234
+ |---|:---:|:---:|:---:|---|
235
+ | `com.tencent:mmkv-shared:1.3.16` (**default**) | ✅ | ✅ | ✅ (since 1.3.14) | Most apps — broadest device coverage |
236
+ | `com.tencent:mmkv-shared:2.x` | ❌ | ✅ | ✅ | 64-bit only apps (no legacy devices) |
237
+ | `io.github.zhongwuzw:mmkv:2.3.0` ([Margelo fork](https://github.com/margelo/MMKV)) | ✅ | ✅ | ✅ | Used automatically by `react-native-mmkv` v4.x — skip manual dependency |
238
+ | `react-native-mmkv` (already in project) | ✅ | ✅ | ✅ | If you already use `react-native-mmkv` — skip Step 4 entirely |
239
+
240
+ **TL;DR:** Use the default `1.3.16`. If you already have `react-native-mmkv` in your project, skip Step 4.
222
241
 
223
242
  ## 🚀 Usage
224
243
 
@@ -836,6 +855,8 @@ This can happen with slow-responding servers. The library automatically adds kee
836
855
  <summary><strong>Duplicate class errors with react-native-mmkv (Android)</strong></summary>
837
856
 
838
857
  If you're using `react-native-mmkv`, you don't need to add the MMKV dependency manually - it's already included. The library uses `compileOnly` to avoid conflicts.
858
+
859
+ `react-native-mmkv` v4.x uses [Margelo's fork of MMKV](https://github.com/margelo/MMKV) (`io.github.zhongwuzw:mmkv`) which re-adds armeabi-v7a (32-bit ARM) support, so you have full ABI coverage including 32-bit devices when using `react-native-mmkv`.
839
860
  </details>
840
861
 
841
862
  <details>
@@ -36,6 +36,8 @@ android {
36
36
  versionName '1.0'
37
37
 
38
38
  // Support for 16KB memory page sizes (Android 15+)
39
+ // Note: MMKV 2.x dropped armeabi-v7a support. If your app targets armeabi-v7a,
40
+ // downgrade to MMKV 1.x in your app's build.gradle (see README for details).
39
41
  ndk {
40
42
  abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'
41
43
  }
@@ -78,8 +80,8 @@ dependencies {
78
80
  // MMKV dependency for persistent download state storage
79
81
  // Uses compileOnly to avoid duplicate class errors when app also uses react-native-mmkv
80
82
  // The app must provide MMKV dependency (either directly or via react-native-mmkv)
81
- // MMKV 2.0.0+ is required for 16KB memory page size support (Android 15+)
82
- compileOnly 'com.tencent:mmkv-shared:2.0.0'
83
+ // MMKV 1.3.14+ supports both armeabi-v7a (32-bit) and 16KB page sizes (Android 15+)
84
+ compileOnly 'com.tencent:mmkv-shared:1.3.16'
83
85
 
84
86
  implementation 'com.google.code.gson:gson:2.12.1'
85
87
  }
@@ -111,8 +111,18 @@ class Downloader(private val context: Context, private val storageManager: com.e
111
111
 
112
112
  /**
113
113
  * Set the download listener for resumable downloads.
114
+ * Also sets the UIDT listener on Android 14+ so that ongoing UIDT jobs
115
+ * (e.g. ones that survived app termination) can forward events to JS after
116
+ * the app is reopened.
114
117
  */
115
118
  fun setResumableDownloadListener(listener: ResumableDownloader.DownloadListener) {
119
+ // For UIDT jobs (Android 14+), set the listener directly on the registry.
120
+ // This reconnects event forwarding after app restart when UIDT jobs are
121
+ // already running but UIDTJobRegistry.downloadListener was null in the
122
+ // fresh process.
123
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
124
+ UIDTDownloadJobService.downloadListener = listener
125
+ }
116
126
  executeWhenServiceReady {
117
127
  downloadService?.setDownloadListener(listener)
118
128
  }
@@ -225,6 +225,8 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
225
225
  }
226
226
 
227
227
  override fun onComplete(id: String, location: String, bytesDownloaded: Long, bytesTotal: Long) {
228
+ // Drop any buffered progress for this task so it cannot arrive in JS after downloadComplete
229
+ progressReporter.clearPendingReport(id)
228
230
  eventEmitter.emitComplete(id, location, bytesDownloaded, bytesTotal)
229
231
 
230
232
  // Clean up all download state
@@ -234,6 +236,8 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
234
236
  }
235
237
 
236
238
  override fun onError(id: String, error: String, errorCode: Int) {
239
+ // Drop any buffered progress for this task so it cannot arrive in JS after downloadFailed
240
+ progressReporter.clearPendingReport(id)
237
241
  eventEmitter.emitFailed(id, error, errorCode)
238
242
 
239
243
  // Clean up all download state
@@ -385,6 +389,9 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
385
389
  stopTaskProgress(config.id)
386
390
 
387
391
  synchronized(sharedLock) {
392
+ // Drop any buffered progress that slipped past stopTaskProgress's clearPendingReport
393
+ // (the polling thread may have re-added it between clearPendingReport and this lock)
394
+ progressReporter.clearPendingReport(config.id)
388
395
  when (status) {
389
396
  DownloadManager.STATUS_SUCCESSFUL -> {
390
397
  onSuccessfulDownload(config, downloadStatus)
@@ -148,7 +148,7 @@ class UIDTDownloadJobService : JobService() {
148
148
  }
149
149
  val url = extras.getString(UIDTConstants.KEY_URL) ?: return false
150
150
  val destination = extras.getString(UIDTConstants.KEY_DESTINATION) ?: return false
151
- val startByte = extras.getLong(UIDTConstants.KEY_START_BYTE, 0)
151
+ val startByteFromExtras = extras.getLong(UIDTConstants.KEY_START_BYTE, 0)
152
152
  val totalBytes = extras.getLong(UIDTConstants.KEY_TOTAL_BYTES, -1)
153
153
 
154
154
  // Extract group info from metadata (for notification grouping)
@@ -165,8 +165,36 @@ class UIDTDownloadJobService : JobService() {
165
165
  RNBackgroundDownloaderModuleImpl.logE(UIDTConstants.TAG, "Failed to parse metadata: ${e.message}")
166
166
  }
167
167
 
168
- // Retrieve headers
169
- val headers = UIDTJobRegistry.pendingHeaders.remove(configId) ?: emptyMap()
168
+ // Resolve headers and start byte.
169
+ // In-memory pendingHeaders is populated in the same process (scheduleDownload / onStopJob).
170
+ // The disk-persisted resume state is the fallback for a fresh process after a restart.
171
+ val inMemoryHeaders = UIDTJobRegistry.pendingHeaders.remove(configId)
172
+ val diskResumeState = UIDTJobRegistry.loadResumeState(this, configId)
173
+ // Clear disk state now that we've consumed it.
174
+ UIDTJobRegistry.clearResumeState(this, configId)
175
+
176
+ val headers: Map<String, String>
177
+ val startByte: Long
178
+ when {
179
+ inMemoryHeaders != null -> {
180
+ // Same-process restart: use in-memory headers.
181
+ // Prefer the disk byte position if it is more advanced than the extras
182
+ // (written by onStopJob with the actual download offset).
183
+ headers = inMemoryHeaders
184
+ startByte = if (diskResumeState != null && diskResumeState.second > startByteFromExtras)
185
+ diskResumeState.second else startByteFromExtras
186
+ }
187
+ diskResumeState != null -> {
188
+ // Cross-process restart: use the persisted state.
189
+ headers = diskResumeState.first
190
+ startByte = diskResumeState.second
191
+ }
192
+ else -> {
193
+ headers = emptyMap()
194
+ startByte = startByteFromExtras
195
+ }
196
+ }
197
+ RNBackgroundDownloaderModuleImpl.logD(UIDTConstants.TAG, "onStartJob: configId=$configId, startByte=$startByte, headers=${headers.size}")
170
198
 
171
199
  // Create notification for UIDT job (required)
172
200
  val notificationId = UIDTNotificationManager.getNotificationIdForConfig(configId)
@@ -233,8 +261,12 @@ class UIDTDownloadJobService : JobService() {
233
261
  // Save state for potential resume
234
262
  val state = jobState.resumableDownloader.getState(configId)
235
263
  if (state != null) {
236
- // Store resume info
264
+ val bytesDownloaded = state.bytesDownloaded.get()
265
+ // Keep in-memory headers so the same-process reschedule works.
237
266
  UIDTJobRegistry.pendingHeaders[configId] = state.headers
267
+ // Persist to disk so a fresh process can resume from the right position.
268
+ UIDTJobRegistry.saveResumeState(this, configId, state.headers, bytesDownloaded)
269
+ RNBackgroundDownloaderModuleImpl.logD(UIDTConstants.TAG, "onStopJob: saved resume state for $configId at $bytesDownloaded bytes")
238
270
  }
239
271
  }
240
272
  UIDTJobRegistry.activeJobs.remove(configId)
@@ -364,6 +396,9 @@ class UIDTDownloadJobService : JobService() {
364
396
  // Signal job completion - this triggers the REMOVE policy
365
397
  jobFinished(params, false)
366
398
 
399
+ // Clear persisted resume state - no longer needed after successful completion
400
+ UIDTJobRegistry.clearResumeState(this@UIDTDownloadJobService, id)
401
+
367
402
  // Notify external listener
368
403
  UIDTJobRegistry.downloadListener?.onComplete(id, location, bytesDownloaded, bytesTotal)
369
404
  }
@@ -402,6 +437,9 @@ class UIDTDownloadJobService : JobService() {
402
437
  // Signal job completion with no reschedule - this triggers the REMOVE policy
403
438
  jobFinished(params, false)
404
439
 
440
+ // Clear persisted resume state - no longer needed after failure
441
+ UIDTJobRegistry.clearResumeState(this@UIDTDownloadJobService, id)
442
+
405
443
  // Notify external listener
406
444
  UIDTJobRegistry.downloadListener?.onError(id, error, errorCode)
407
445
  }
@@ -66,6 +66,9 @@ object UIDTJobManager {
66
66
 
67
67
  // Store headers for later retrieval (PersistableBundle can't store Map<String, String>)
68
68
  UIDTJobRegistry.pendingHeaders[configId] = headers
69
+ // Also persist to disk so headers survive process death and are available
70
+ // in onStartJob even when the process is restarted by the JobScheduler.
71
+ UIDTJobRegistry.saveResumeState(context, configId, headers, startByte)
69
72
 
70
73
  // Create extras bundle
71
74
  val extras = PersistableBundle().apply {
@@ -77,9 +80,14 @@ object UIDTJobManager {
77
80
  putString(UIDTConstants.KEY_METADATA, metadata)
78
81
  }
79
82
 
80
- // Build network request - require internet connectivity
83
+ // Build network request - require internet connectivity.
84
+ // Remove NET_CAPABILITY_NOT_VPN (added by Builder default) so that VPN networks
85
+ // (e.g. Proton VPN, full-tunnel VPNs) are accepted. Without this, the JobScheduler
86
+ // only considers non-VPN networks; a kill-switch VPN blocks that traffic and the
87
+ // job never starts, causing callbacks to never fire.
81
88
  val networkRequest = NetworkRequest.Builder()
82
89
  .addCapability(android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET)
90
+ .removeCapability(android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
83
91
  .build()
84
92
 
85
93
  // Build the job with UIDT flag
@@ -150,6 +158,8 @@ object UIDTJobManager {
150
158
  jobScheduler.cancel(jobId)
151
159
  UIDTJobRegistry.pendingHeaders.remove(configId)
152
160
  UIDTJobRegistry.activeJobs.remove(configId)
161
+ // Clear persisted resume state so stale headers/bytes don't affect future downloads
162
+ UIDTJobRegistry.clearResumeState(context, configId)
153
163
 
154
164
  // Update summary notification if grouping was enabled
155
165
  if (jobState != null && config.groupingEnabled && jobState.groupId.isNotEmpty()) {
@@ -1,7 +1,10 @@
1
1
  package com.eko.uidt
2
2
 
3
3
  import android.app.job.JobParameters
4
+ import android.content.Context
5
+ import com.eko.RNBackgroundDownloaderModuleImpl
4
6
  import com.eko.ResumableDownloader
7
+ import org.json.JSONObject
5
8
  import java.util.concurrent.ConcurrentHashMap
6
9
 
7
10
  /**
@@ -127,6 +130,65 @@ data class UIDTJobInfo(
127
130
  * Singleton for managing active UIDT jobs state.
128
131
  */
129
132
  object UIDTJobRegistry {
133
+
134
+ private const val PREFS_NAME = "rnbd_uidt_resume"
135
+ private const val KEY_BYTES_PREFIX = "bytes_"
136
+ private const val KEY_HEADERS_PREFIX = "headers_"
137
+
138
+ /**
139
+ * Persist UIDT resume state (headers + byte position) to disk so it
140
+ * survives process death and can be used when the job is rescheduled in a
141
+ * new process.
142
+ */
143
+ fun saveResumeState(context: Context, configId: String, headers: Map<String, String>, bytesDownloaded: Long) {
144
+ try {
145
+ val headersJson = JSONObject(headers as Map<*, *>).toString()
146
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
147
+ .putLong("$KEY_BYTES_PREFIX$configId", bytesDownloaded)
148
+ .putString("$KEY_HEADERS_PREFIX$configId", headersJson)
149
+ .apply()
150
+ RNBackgroundDownloaderModuleImpl.logD(UIDTConstants.TAG, "Saved UIDT resume state for $configId: bytes=$bytesDownloaded")
151
+ } catch (e: Exception) {
152
+ RNBackgroundDownloaderModuleImpl.logE(UIDTConstants.TAG, "Failed to save UIDT resume state for $configId: ${e.message}")
153
+ }
154
+ }
155
+
156
+ /**
157
+ * Load persisted UIDT resume state for a download.
158
+ * Returns (headers, bytesDownloaded) or null if no state was saved.
159
+ */
160
+ fun loadResumeState(context: Context, configId: String): Pair<Map<String, String>, Long>? {
161
+ try {
162
+ val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
163
+ val bytesDownloaded = prefs.getLong("$KEY_BYTES_PREFIX$configId", -1L)
164
+ val headersJson = prefs.getString("$KEY_HEADERS_PREFIX$configId", null)
165
+ if (bytesDownloaded < 0 || headersJson == null) return null
166
+ val json = JSONObject(headersJson)
167
+ val headers = mutableMapOf<String, String>()
168
+ for (key in json.keys()) headers[key] = json.getString(key)
169
+ RNBackgroundDownloaderModuleImpl.logD(UIDTConstants.TAG, "Loaded UIDT resume state for $configId: bytes=$bytesDownloaded, headers=${headers.size}")
170
+ return Pair(headers, bytesDownloaded)
171
+ } catch (e: Exception) {
172
+ RNBackgroundDownloaderModuleImpl.logE(UIDTConstants.TAG, "Failed to load UIDT resume state for $configId: ${e.message}")
173
+ return null
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Clear persisted UIDT resume state for a download once it is no longer needed.
179
+ */
180
+ fun clearResumeState(context: Context, configId: String) {
181
+ try {
182
+ context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit()
183
+ .remove("$KEY_BYTES_PREFIX$configId")
184
+ .remove("$KEY_HEADERS_PREFIX$configId")
185
+ .apply()
186
+ RNBackgroundDownloaderModuleImpl.logD(UIDTConstants.TAG, "Cleared UIDT resume state for $configId")
187
+ } catch (e: Exception) {
188
+ RNBackgroundDownloaderModuleImpl.logE(UIDTConstants.TAG, "Failed to clear UIDT resume state for $configId: ${e.message}")
189
+ }
190
+ }
191
+
130
192
  // Track active jobs for pause/resume
131
193
  val activeJobs = ConcurrentHashMap<String, JobState>()
132
194
 
package/ios/.DS_Store ADDED
Binary file
@@ -66,6 +66,13 @@ static CompletionHandler storedCompletionHandler;
66
66
  // Controls whether debug logs are sent to JS
67
67
  BOOL isLogsEnabled;
68
68
 
69
+ #ifdef RCT_NEW_ARCH_ENABLED
70
+ // Queue of events that arrived before the TurboModule event emitter callback was set.
71
+ // This prevents crashes (std::bad_function_call / SIGABRT) when NSURLSession delegate
72
+ // callbacks fire before JS has registered event listeners.
73
+ NSMutableArray<NSDictionary *> *pendingEmitEvents;
74
+ #endif
75
+
69
76
  // Upload-specific instance variables
70
77
  NSMutableDictionary<NSNumber *, RNBGDUploadTaskConfig *> *uploadTaskToConfigMap;
71
78
  NSMutableDictionary<NSString *, NSURLSessionUploadTask *> *idToUploadTaskMap;
@@ -233,6 +240,10 @@ static const int kMaxEventRetries = 50; // 50 retries * 100ms = 5 seconds max w
233
240
  isSessionActivated = NO;
234
241
  pendingDownloads = [[NSMutableArray alloc] init];
235
242
 
243
+ #ifdef RCT_NEW_ARCH_ENABLED
244
+ pendingEmitEvents = [[NSMutableArray alloc] init];
245
+ #endif
246
+
236
247
  // Initialize upload-specific data structures
237
248
  NSData *uploadTaskToConfigMapData = [mmkv getDataForKey:ID_TO_UPLOAD_CONFIG_MAP_KEY];
238
249
  NSMutableDictionary *uploadTaskToConfigMapDataDefault = [[NSMutableDictionary alloc] init];
@@ -1140,6 +1151,36 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
1140
1151
  }
1141
1152
  }
1142
1153
 
1154
+ #ifdef RCT_NEW_ARCH_ENABLED
1155
+ #pragma mark - Safe event emission (New Architecture)
1156
+
1157
+ // Safely emit an event, queuing it if the TurboModule event emitter callback
1158
+ // has not been registered yet. This prevents std::bad_function_call crashes
1159
+ // when NSURLSession delegate callbacks fire before JS has registered listeners
1160
+ // (e.g., background session delivering completions from a prior app session).
1161
+ - (void)safeEmitEvent:(NSString *)eventName value:(id)value {
1162
+ @synchronized (pendingEmitEvents) {
1163
+ if (_eventEmitterCallback) {
1164
+ _eventEmitterCallback(std::string([eventName UTF8String]), value);
1165
+ } else {
1166
+ [pendingEmitEvents addObject:@{@"name": eventName, @"value": value}];
1167
+ }
1168
+ }
1169
+ }
1170
+
1171
+ - (void)setEventEmitterCallback:(EventEmitterCallbackWrapper *)eventEmitterCallbackWrapper {
1172
+ @synchronized (pendingEmitEvents) {
1173
+ [super setEventEmitterCallback:eventEmitterCallbackWrapper];
1174
+
1175
+ // Flush any events that arrived before the callback was set
1176
+ for (NSDictionary *event in pendingEmitEvents) {
1177
+ _eventEmitterCallback(std::string([event[@"name"] UTF8String]), event[@"value"]);
1178
+ }
1179
+ [pendingEmitEvents removeAllObjects];
1180
+ }
1181
+ }
1182
+ #endif
1183
+
1143
1184
  #pragma mark - NSURLSessionDownloadDelegate methods
1144
1185
  - (void)URLSession:(nonnull NSURLSession *)session downloadTask:(nonnull NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(nonnull NSURL *)location {
1145
1186
  @synchronized (sharedLock) {
@@ -1161,6 +1202,9 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
1161
1202
  [self sendDebugLog:[NSString stringWithFormat:@"didFinishDownloadingToURL: error - %@", error.localizedDescription] taskId:taskConfig.id];
1162
1203
  }
1163
1204
 
1205
+ // Drop any buffered progress for this task so it cannot arrive in JS after downloadComplete
1206
+ [progressReports removeObjectForKey:taskConfig.id];
1207
+
1164
1208
  [self sendDownloadCompletionEvent:taskConfig task:downloadTask error:error];
1165
1209
 
1166
1210
  [self removeTaskFromMap:downloadTask];
@@ -1170,7 +1214,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
1170
1214
  - (void)sendDownloadCompletionEvent:(RNBGDTaskConfig *)taskConfig task:(NSURLSessionDownloadTask *)task error:(NSError *)error {
1171
1215
  if (error) {
1172
1216
  #ifdef RCT_NEW_ARCH_ENABLED
1173
- [self emitOnDownloadFailed:@{
1217
+ [self safeEmitEvent:@"onDownloadFailed" value:@{
1174
1218
  @"id": taskConfig.id,
1175
1219
  @"error": [error localizedDescription],
1176
1220
  @"errorCode": @(error.code)
@@ -1184,7 +1228,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
1184
1228
  #endif
1185
1229
  } else {
1186
1230
  #ifdef RCT_NEW_ARCH_ENABLED
1187
- [self emitOnDownloadComplete:@{
1231
+ [self safeEmitEvent:@"onDownloadComplete" value:@{
1188
1232
  @"id": taskConfig.id,
1189
1233
  @"location": taskConfig.destination,
1190
1234
  @"bytesDownloaded": @(task.countOfBytesReceived),
@@ -1243,7 +1287,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
1243
1287
  responseHeaders = @{};
1244
1288
  }
1245
1289
  #ifdef RCT_NEW_ARCH_ENABLED
1246
- [self emitOnDownloadBegin:@{
1290
+ [self safeEmitEvent:@"onDownloadBegin" value:@{
1247
1291
  @"id": taskConfig.id,
1248
1292
  @"expectedBytes": @(expectedBytes),
1249
1293
  @"headers": responseHeaders
@@ -1290,7 +1334,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
1290
1334
  NSDate *now = [NSDate date];
1291
1335
  if ([now timeIntervalSinceDate:lastProgressReportedAt] > progressInterval) {
1292
1336
  #ifdef RCT_NEW_ARCH_ENABLED
1293
- [self emitOnDownloadProgress:[progressReports allValues]];
1337
+ [self safeEmitEvent:@"onDownloadProgress" value:[progressReports allValues]];
1294
1338
  #else
1295
1339
  [self sendEventWithName:@"downloadProgress" body:[progressReports allValues]];
1296
1340
  #endif
@@ -1385,7 +1429,7 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
1385
1429
 
1386
1430
  // Handle failure
1387
1431
  #ifdef RCT_NEW_ARCH_ENABLED
1388
- [self emitOnDownloadFailed:@{
1432
+ [self safeEmitEvent:@"onDownloadFailed" value:@{
1389
1433
  @"id": taskConfig.id,
1390
1434
  @"error": [error localizedDescription],
1391
1435
  @"errorCode": @(error.code)
@@ -1764,7 +1808,7 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
1764
1808
  if (!taskConfig.reportedBegin) {
1765
1809
  taskConfig.reportedBegin = YES;
1766
1810
  #ifdef RCT_NEW_ARCH_ENABLED
1767
- [self emitOnUploadBegin:@{
1811
+ [self safeEmitEvent:@"onUploadBegin" value:@{
1768
1812
  @"id": taskConfig.id,
1769
1813
  @"expectedBytes": @(totalBytesExpectedToSend)
1770
1814
  }];
@@ -1801,7 +1845,7 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
1801
1845
  NSDate *now = [NSDate date];
1802
1846
  if ([now timeIntervalSinceDate:lastUploadProgressReportedAt] > progressInterval) {
1803
1847
  #ifdef RCT_NEW_ARCH_ENABLED
1804
- [self emitOnUploadProgress:[uploadProgressReports allValues]];
1848
+ [self safeEmitEvent:@"onUploadProgress" value:[uploadProgressReports allValues]];
1805
1849
  #else
1806
1850
  [self sendEventWithName:@"uploadProgress" body:[uploadProgressReports allValues]];
1807
1851
  #endif
@@ -1847,7 +1891,7 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
1847
1891
 
1848
1892
  DLog(taskConfig.id, @"[RNBackgroundDownloader] - [handleUploadCompletion] error: %@", error);
1849
1893
  #ifdef RCT_NEW_ARCH_ENABLED
1850
- [self emitOnUploadFailed:@{
1894
+ [self safeEmitEvent:@"onUploadFailed" value:@{
1851
1895
  @"id": taskConfig.id,
1852
1896
  @"error": [error localizedDescription],
1853
1897
  @"errorCode": @(error.code)
@@ -1873,7 +1917,7 @@ RCT_EXPORT_METHOD(getExistingUploadTasks:(RCTPromiseResolveBlock)resolve rejecte
1873
1917
 
1874
1918
  DLog(taskConfig.id, @"[RNBackgroundDownloader] - [handleUploadCompletion] success, responseCode: %ld", (long)responseCode);
1875
1919
  #ifdef RCT_NEW_ARCH_ENABLED
1876
- [self emitOnUploadComplete:@{
1920
+ [self safeEmitEvent:@"onUploadComplete" value:@{
1877
1921
  @"id": taskConfig.id,
1878
1922
  @"responseCode": @(responseCode),
1879
1923
  @"responseBody": responseBody,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kesha-antonov/react-native-background-downloader",
3
- "version": "4.5.3",
3
+ "version": "4.5.4",
4
4
  "description": "A library for React-Native to help you download large files on iOS and Android both in the foreground and most importantly in the background.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -3,12 +3,12 @@ interface PluginOptions {
3
3
  /**
4
4
  * Options for the MMKV dependency on Android.
5
5
  * Pass a string to specify the version, or an object with version property.
6
- * @default '2.2.4'
6
+ * @default '1.3.16'
7
7
  * @example
8
8
  * // Use default version
9
9
  * ["@kesha-antonov/react-native-background-downloader"]
10
10
  * // Specify version
11
- * ["@kesha-antonov/react-native-background-downloader", { mmkvVersion: "2.2.4" }]
11
+ * ["@kesha-antonov/react-native-background-downloader", { mmkvVersion: "1.3.16" }]
12
12
  */
13
13
  mmkvVersion?: string;
14
14
  /**
@@ -37,7 +37,7 @@ const config_plugins_1 = require("@expo/config-plugins");
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const withRNBackgroundDownloader = (config, options) => {
40
- const { mmkvVersion = '2.2.4', skipMmkvDependency = false } = options || {};
40
+ const { mmkvVersion = '1.3.16', skipMmkvDependency = false } = options || {};
41
41
  // Auto-detect react-native-mmkv in dependencies
42
42
  const hasReactNativeMmkv = checkForReactNativeMmkv(config);
43
43
  const shouldSkipMmkv = skipMmkvDependency || hasReactNativeMmkv;
@@ -1,6 +0,0 @@
1
- {
2
- "name": "@kesha-antonov/react-native-background-downloader-expo-plugin",
3
- "version": "1.0.0",
4
- "main": "build/index.js",
5
- "types": "build/index.d.ts"
6
- }