@kesha-antonov/react-native-background-downloader 4.0.3 → 4.1.2-alpha.0

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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## v4.1.0
4
+
5
+ > 📖 **Upgrading from v4.0.x?** See the [Migration Guide](./MIGRATION.md) for the MMKV dependency change.
6
+
7
+ ### ⚠️ Breaking Changes
8
+
9
+ - **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.
10
+
11
+ ### ✨ New Features
12
+
13
+ - **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`.
14
+
15
+ ### 🐛 Bug Fixes
16
+
17
+ - **Duplicate Class Errors:** Fixed potential duplicate class errors when app uses both this library and `react-native-mmkv` by changing MMKV to `compileOnly` dependency
18
+
19
+ ### 📚 Documentation
20
+
21
+ - Added documentation for MMKV dependency requirements in README
22
+ - Updated Platform-Specific Limitations section with MMKV setup instructions
23
+ - Added Expo plugin options documentation
24
+
25
+ ---
26
+
3
27
  ## v4.0.0
4
28
 
5
29
  > 📖 **Upgrading from v3.x?** See the [Migration Guide](./MIGRATION.md) for detailed instructions.
package/README.md CHANGED
@@ -126,7 +126,7 @@ For anything **`< 0.60`** run the following link command
126
126
 
127
127
  #### Option 1: Using Expo Config Plugin (Recommended for Expo/EAS users)
128
128
 
129
- If you're using Expo or EAS Build, you can use the included Expo config plugin to automatically configure the iOS native code:
129
+ If you're using Expo or EAS Build, you can use the included Expo config plugin to automatically configure the native code:
130
130
 
131
131
  **In your `app.json`:**
132
132
  ```json
@@ -152,10 +152,37 @@ export default {
152
152
  }
153
153
  ```
154
154
 
155
+ **Plugin Options:**
156
+
157
+ You can customize the plugin behavior with options:
158
+
159
+ ```js
160
+ // app.config.js
161
+ export default {
162
+ expo: {
163
+ name: "Your App",
164
+ plugins: [
165
+ ["@kesha-antonov/react-native-background-downloader", {
166
+ // Set to false if you're already using react-native-mmkv
167
+ addMmkvDependency: true,
168
+ // Customize the MMKV version (default: '2.2.4')
169
+ mmkvVersion: "2.2.4"
170
+ }]
171
+ ]
172
+ }
173
+ }
174
+ ```
175
+
176
+ | Option | Type | Default | Description |
177
+ |--------|------|---------|-------------|
178
+ | `addMmkvDependency` | boolean | `true` | Whether to automatically add MMKV dependency on Android. Set to `false` if you're using `react-native-mmkv`. |
179
+ | `mmkvVersion` | string | `'2.2.4'` | The version of MMKV to use on Android. |
180
+
155
181
  The plugin will automatically:
156
- - Add the required import to your AppDelegate (Objective-C) or Bridging Header (Swift)
157
- - Add the `handleEventsForBackgroundURLSession` method to your AppDelegate
158
- - Handle both React Native < 0.77 (Objective-C) and >= 0.77 (Swift) projects
182
+ - **iOS:** Add the required import to your AppDelegate (Objective-C) or Bridging Header (Swift)
183
+ - **iOS:** Add the `handleEventsForBackgroundURLSession` method to your AppDelegate
184
+ - **iOS:** Handle both React Native < 0.77 (Objective-C) and >= 0.77 (Swift) projects
185
+ - **Android:** Add the required MMKV dependency (unless `addMmkvDependency: false`)
159
186
 
160
187
  After adding the plugin, run:
161
188
  ```bash
@@ -439,19 +466,6 @@ import {
439
466
  } from '@kesha-antonov/react-native-background-downloader'
440
467
  ```
441
468
 
442
- **Default Export:**
443
-
444
- ```typescript
445
- import RNBackgroundDownloader from '@kesha-antonov/react-native-background-downloader'
446
-
447
- // Contains all the above as properties:
448
- RNBackgroundDownloader.setConfig
449
- RNBackgroundDownloader.createDownloadTask
450
- RNBackgroundDownloader.getExistingDownloadTasks
451
- RNBackgroundDownloader.completeHandler
452
- RNBackgroundDownloader.directories
453
- ```
454
-
455
469
  ### `createDownloadTask(options)`
456
470
 
457
471
  Download a file to destination
@@ -561,6 +575,23 @@ An absolute path to the app's documents directory. It is recommended that you us
561
575
 
562
576
  ## Platform-Specific Limitations
563
577
 
578
+ ### Android MMKV Dependency
579
+
580
+ This library uses MMKV for persistent storage of download state on Android. The MMKV dependency is declared as `compileOnly`, meaning your app must provide it.
581
+
582
+ **If you're using `react-native-mmkv`:** No additional setup needed - `react-native-mmkv` already provides the required MMKV dependency.
583
+
584
+ **If you're NOT using `react-native-mmkv`:** Add the MMKV dependency to your app's `android/app/build.gradle`:
585
+
586
+ ```gradle
587
+ dependencies {
588
+ // ... other dependencies
589
+ implementation 'com.tencent:mmkv-shared:2.2.4' // or newer
590
+ }
591
+ ```
592
+
593
+ **Note:** MMKV 2.0.0+ is required for Android 15+ support (16KB memory page sizes).
594
+
564
595
  ### Android DownloadManager Limitations
565
596
 
566
597
  The Android implementation uses the system's `DownloadManager` service, which has some limitations compared to iOS:
@@ -5,6 +5,10 @@ def isNewArchitectureEnabled() {
5
5
  return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
6
6
  }
7
7
 
8
+ if (isNewArchitectureEnabled()) {
9
+ apply plugin: 'com.facebook.react'
10
+ }
11
+
8
12
  def safeExtGet(prop, fallback) {
9
13
  rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
10
14
  }
@@ -63,10 +67,11 @@ dependencies {
63
67
  //noinspection GradleDynamicVersion
64
68
  implementation 'com.facebook.react:react-native:+'
65
69
 
66
- // MMKV dependency updated for 16KB page size support
67
- // MMKV 2.2.0+ includes support for 16KB memory page sizes required by Android 15+
68
- // Uses mmkv-shared to maintain compatibility with react-native-mmkv 4+
69
- implementation 'com.tencent:mmkv-shared:2.2.0'
70
+ // MMKV dependency for persistent download state storage
71
+ // Uses compileOnly to avoid duplicate class errors when app also uses react-native-mmkv
72
+ // The app must provide MMKV dependency (either directly or via react-native-mmkv)
73
+ // MMKV 2.0.0+ is required for 16KB memory page size support (Android 15+)
74
+ compileOnly 'com.tencent:mmkv-shared:2.0.0'
70
75
 
71
76
  implementation 'com.google.code.gson:gson:2.12.1'
72
77
  }
@@ -713,7 +713,10 @@ class RNBackgroundDownloaderModuleImpl(private val reactContext: ReactApplicatio
713
713
  synchronized(sharedLock) {
714
714
  try {
715
715
  val gson = Gson()
716
- val str = gson.toJson(downloadIdToConfig)
716
+ // Create a defensive copy to prevent ConcurrentModificationException
717
+ // when Gson iterates over the map while another thread modifies it
718
+ val mapCopy = HashMap(downloadIdToConfig)
719
+ val str = gson.toJson(mapCopy)
717
720
 
718
721
  if (isMMKVAvailable && mmkv != null) {
719
722
  mmkv!!.encode("${NAME}_downloadIdToConfig", str)
@@ -1,6 +1,7 @@
1
1
  package com.eko
2
2
 
3
3
  import com.facebook.react.bridge.ReactApplicationContext
4
+ import com.facebook.react.bridge.ReactMethod
4
5
  import com.facebook.react.module.annotations.ReactModule
5
6
 
6
7
  @ReactModule(name = RNBackgroundDownloaderModuleImpl.NAME)
@@ -12,7 +13,7 @@ class RNBackgroundDownloaderModule(reactContext: ReactApplicationContext) :
12
13
  override fun getName(): String = RNBackgroundDownloaderModuleImpl.NAME
13
14
 
14
15
  override fun getTypedExportedConstants(): Map<String, Any>? {
15
- return impl.constants
16
+ return impl.getConstants()
16
17
  }
17
18
 
18
19
  override fun initialize() {
@@ -53,11 +54,13 @@ class RNBackgroundDownloaderModule(reactContext: ReactApplicationContext) :
53
54
  impl.getExistingDownloadTasks(promise)
54
55
  }
55
56
 
56
- override fun addListener(eventName: String) {
57
+ @ReactMethod
58
+ fun addListener(eventName: String) {
57
59
  impl.addListener(eventName)
58
60
  }
59
61
 
60
- override fun removeListeners(count: Double) {
62
+ @ReactMethod
63
+ fun removeListeners(count: Double) {
61
64
  impl.removeListeners(count.toInt())
62
65
  }
63
66
  }
@@ -170,6 +170,9 @@ RCT_EXPORT_MODULE();
170
170
  decodeErrorRetriedIds = [[NSMutableSet alloc] init];
171
171
 
172
172
  [self registerBridgeListener];
173
+
174
+ // Initialize session early to receive background events on app relaunch
175
+ [self lazyRegisterSession];
173
176
  }
174
177
 
175
178
  return self;
@@ -307,12 +310,12 @@ RCT_EXPORT_METHOD(download: (NSDictionary *) options) {
307
310
  DLog(identifier, @"[RNBackgroundDownloader] - [download]");
308
311
  NSString *url = options[@"url"];
309
312
  NSString *destination = options[@"destination"];
310
- NSString *metadata = options[@"metadata"];
313
+ NSString *metadata = options[@"metadata"] ?: @"";
311
314
  NSDictionary *headers = options[@"headers"];
312
315
 
313
316
  NSNumber *progressIntervalScope = options[@"progressInterval"];
314
317
  if (progressIntervalScope) {
315
- progressInterval = [progressIntervalScope intValue] / 1000;
318
+ progressInterval = [progressIntervalScope intValue] / 1000.0;
316
319
  [mmkv setFloat:progressInterval forKey:PROGRESS_INTERVAL_KEY];
317
320
  }
318
321
 
@@ -708,7 +711,15 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
708
711
  }];
709
712
  #endif
710
713
  } else {
711
- NSDictionary *responseHeaders = ((NSHTTPURLResponse *)task.response).allHeaderFields;
714
+ // Safely extract response headers - may be nil if response is not an HTTP response
715
+ NSDictionary *responseHeaders = nil;
716
+ if ([task.response isKindOfClass:[NSHTTPURLResponse class]]) {
717
+ responseHeaders = ((NSHTTPURLResponse *)task.response).allHeaderFields;
718
+ }
719
+ // Use empty dictionary if headers are nil to prevent NSDictionary nil insertion crash
720
+ if (responseHeaders == nil) {
721
+ responseHeaders = @{};
722
+ }
712
723
  #ifdef RCT_NEW_ARCH_ENABLED
713
724
  [self emitOnDownloadComplete:@{
714
725
  @"id": taskConfig.id,
@@ -762,7 +773,15 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
762
773
  }
763
774
 
764
775
  if ([self canSendEvents]) {
765
- NSDictionary *responseHeaders = ((NSHTTPURLResponse *)task.response).allHeaderFields;
776
+ // Safely extract response headers - may be nil if response is not an HTTP response
777
+ NSDictionary *responseHeaders = nil;
778
+ if ([task.response isKindOfClass:[NSHTTPURLResponse class]]) {
779
+ responseHeaders = ((NSHTTPURLResponse *)task.response).allHeaderFields;
780
+ }
781
+ // Use empty dictionary if headers are nil to prevent NSDictionary nil insertion crash
782
+ if (responseHeaders == nil) {
783
+ responseHeaders = @{};
784
+ }
766
785
  #ifdef RCT_NEW_ARCH_ENABLED
767
786
  [self emitOnDownloadBegin:@{
768
787
  @"id": taskConfig.id,
@@ -834,11 +853,17 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
834
853
  DLog(taskConfig.id, @"[RNBackgroundDownloader] - [didCompleteWithError] error: %@", error);
835
854
 
836
855
  // NSURLErrorCancelled (-999) is used for paused or cancelled tasks
837
- NSData *resumeData = task.error.userInfo[NSURLSessionDownloadTaskResumeData];
856
+ // Extract resume data first before checking isPausedTask
857
+ NSData *resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
838
858
  BOOL isPausedTask = (error.code == NSURLErrorCancelled && resumeData != nil);
839
859
 
840
860
  if (isPausedTask) {
841
861
  taskConfig.errorCode = error.code;
862
+
863
+ // Store resume data so we can resume the download later
864
+ if (taskConfig.id && resumeData) {
865
+ idToResumeDataMap[taskConfig.id] = resumeData;
866
+ }
842
867
  DLog(taskConfig.id, @"[RNBackgroundDownloader] - [didCompleteWithError] task was paused, ignoring error for %@", taskConfig.id);
843
868
  return;
844
869
  }
@@ -918,6 +943,11 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
918
943
  }
919
944
 
920
945
  - (NSError *)getServerError:(NSURLSessionDownloadTask *)downloadTask {
946
+ // Safely check if response is an HTTP response
947
+ if (![downloadTask.response isKindOfClass:[NSHTTPURLResponse class]]) {
948
+ return nil; // No HTTP response, can't determine server error
949
+ }
950
+
921
951
  NSInteger statusCode = ((NSHTTPURLResponse *)downloadTask.response).statusCode;
922
952
 
923
953
  // 200: OK, 206: Partial Content (for resumed downloads)
@@ -938,6 +968,17 @@ RCT_EXPORT_METHOD(getExistingDownloadTasks: (RCTPromiseResolveBlock)resolve reje
938
968
  // Relative paths are used to recreate the Absolute path.
939
969
  NSString *rootPath = [self getRootPathFromPath:taskConfig.destination];
940
970
  NSString *fileRelativePath = [self getRelativeFilePathFromPath:taskConfig.destination];
971
+
972
+ // Check for nil paths to prevent crash
973
+ if (rootPath == nil || fileRelativePath == nil) {
974
+ if (saveError) {
975
+ *saveError = [NSError errorWithDomain:NSURLErrorDomain
976
+ code:NSURLErrorFileDoesNotExist
977
+ userInfo:@{NSLocalizedDescriptionKey: @"Invalid destination path"}];
978
+ }
979
+ return NO;
980
+ }
981
+
941
982
  NSString *fileAbsolutePath = [rootPath stringByAppendingPathComponent:fileRelativePath];
942
983
  NSURL *destinationURL = [NSURL fileURLWithPath:fileAbsolutePath];
943
984
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kesha-antonov/react-native-background-downloader",
3
- "version": "4.0.3",
3
+ "version": "4.1.2-alpha.0",
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",
@@ -1,3 +1,16 @@
1
1
  import { ConfigPlugin } from '@expo/config-plugins';
2
- declare const withRNBackgroundDownloader: ConfigPlugin;
2
+ interface PluginOptions {
3
+ /**
4
+ * Whether to automatically add the MMKV dependency to the Android app.
5
+ * Set to false if you're already using react-native-mmkv or want to manage the dependency yourself.
6
+ * @default true
7
+ */
8
+ addMmkvDependency?: boolean;
9
+ /**
10
+ * The version of MMKV to use. Only used if addMmkvDependency is true.
11
+ * @default '2.2.4'
12
+ */
13
+ mmkvVersion?: string;
14
+ }
15
+ declare const withRNBackgroundDownloader: ConfigPlugin<PluginOptions | void>;
3
16
  export default withRNBackgroundDownloader;
@@ -36,8 +36,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  const config_plugins_1 = require("@expo/config-plugins");
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
- const withRNBackgroundDownloader = (config) => {
40
- // Handle AppDelegate modifications
39
+ const withRNBackgroundDownloader = (config, options) => {
40
+ const { addMmkvDependency = true, mmkvVersion = '2.2.4' } = options || {};
41
+ // Handle iOS AppDelegate modifications
41
42
  config = (0, config_plugins_1.withAppDelegate)(config, (config) => {
42
43
  if (config.modResults.language === 'objc') {
43
44
  // For Objective-C AppDelegate.m (React Native < 0.77)
@@ -53,8 +54,30 @@ const withRNBackgroundDownloader = (config) => {
53
54
  }
54
55
  return config;
55
56
  });
57
+ // Handle Android MMKV dependency
58
+ if (addMmkvDependency)
59
+ config = (0, config_plugins_1.withAppBuildGradle)(config, (config) => {
60
+ config.modResults.contents = addMmkvDependencyAndroid(config.modResults.contents, mmkvVersion);
61
+ return config;
62
+ });
56
63
  return config;
57
64
  };
65
+ function addMmkvDependencyAndroid(buildGradleContents, mmkvVersion) {
66
+ // Check if MMKV dependency is already present
67
+ if (buildGradleContents.includes('com.tencent:mmkv'))
68
+ return buildGradleContents;
69
+ // Find the dependencies block and add MMKV
70
+ const dependenciesRegex = /dependencies\s*\{/;
71
+ const match = buildGradleContents.match(dependenciesRegex);
72
+ if (match) {
73
+ const insertPosition = buildGradleContents.indexOf(match[0]) + match[0].length;
74
+ const mmkvDependency = `\n // MMKV is required by @kesha-antonov/react-native-background-downloader\n // If you're using react-native-mmkv, you can disable this in the plugin options\n implementation 'com.tencent:mmkv-shared:${mmkvVersion}'`;
75
+ buildGradleContents = buildGradleContents.slice(0, insertPosition) +
76
+ mmkvDependency +
77
+ buildGradleContents.slice(insertPosition);
78
+ }
79
+ return buildGradleContents;
80
+ }
58
81
  function addObjCSupport(appDelegateContents) {
59
82
  // Add import if not already present
60
83
  if (!appDelegateContents.includes('#import <RNBackgroundDownloader.h>')) {
@@ -29,7 +29,7 @@ if (isTurboModuleEnabled)
29
29
  else
30
30
  RNBackgroundDownloader = NativeModules.RNBackgroundDownloader
31
31
 
32
- export default class DownloadTask {
32
+ export class DownloadTask {
33
33
  id: string = ''
34
34
  metadata: Metadata = {}
35
35
 
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { NativeModules, Platform, TurboModuleRegistry, NativeEventEmitter, NativeModule } from 'react-native'
2
- import DownloadTask from './DownloadTask'
2
+ import { DownloadTask } from './DownloadTask'
3
3
  import { Config, DownloadParams, Headers, TaskInfo, TaskInfoNative } from './types'
4
4
  import { config, log, DEFAULT_PROGRESS_INTERVAL, DEFAULT_PROGRESS_MIN_BYTES } from './config'
5
5
  import type { Spec } from './NativeRNBackgroundDownloader'
@@ -305,11 +305,4 @@ export const directories = {
305
305
  },
306
306
  }
307
307
 
308
- export default {
309
- setConfig,
310
- createDownloadTask,
311
- getExistingDownloadTasks,
312
- completeHandler,
313
-
314
- directories,
315
- }
308
+ export type * from './types'