react-native-dynamic-resource-loader 0.2.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.
@@ -0,0 +1,20 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "DynamicResourceLoader"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/Rob117//react-native-dynamic-resource-loader.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift,cpp}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+ install_modules_dependencies(s)
20
+ end
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Robert Sherling
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,207 @@
1
+ # react-native-dynamic-resource-loader
2
+
3
+ Load iOS [On-Demand Resources](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/On_Demand_Resources_Guide/) and Android [Play Asset Delivery](https://developer.android.com/guide/playcore/asset-delivery) from React Native. Download tagged assets at runtime to reduce your initial app size.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ npm install react-native-dynamic-resource-loader
9
+ ```
10
+
11
+ ## Xcode Setup
12
+
13
+ Before using the library, you need to configure On-Demand Resources in your Xcode project:
14
+
15
+ 1. **Enable ODR** — In your Xcode project, go to Build Settings and set `ENABLE_ON_DEMAND_RESOURCES = YES`.
16
+
17
+ 2. **Add resources and assign tags** — Add files (images, data files, etc.) to your Xcode project's Resources build phase. Select each file in Xcode, open the File Inspector, and add one or more tags under "On Demand Resource Tags".
18
+
19
+ 3. **Register tags** — Xcode does this automatically when you assign tags through the UI. You'll see them listed in your project's attributes under `KnownAssetTags`.
20
+
21
+ 4. **Debug builds** — When running from the command line (e.g. `yarn ios`), Xcode's local asset server isn't available. Set `EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = YES` in your Debug build settings so ODR assets are embedded directly in the app bundle during development.
22
+
23
+ Multiple resources can share the same tag. When you download a tag, iOS fetches all resources with that tag together.
24
+
25
+ ## Android Setup
26
+
27
+ Android uses [Play Asset Delivery](https://developer.android.com/guide/playcore/asset-delivery) (PAD) to deliver assets on demand. Each iOS "tag" maps to an Android "asset pack".
28
+
29
+ ### 1. Add the PAD dependency
30
+
31
+ In your app's `android/app/build.gradle`:
32
+
33
+ ```groovy
34
+ dependencies {
35
+ implementation("com.google.android.play:asset-delivery:2.3.0")
36
+ }
37
+ ```
38
+
39
+ ### 2. Create an asset pack module
40
+
41
+ For each tag (e.g. `kichilogo`), create a directory at `android/kichilogo/` with this structure:
42
+
43
+ ```
44
+ android/kichilogo/
45
+ ├── build.gradle
46
+ └── src/main/assets/
47
+ └── your_file.png
48
+ ```
49
+
50
+ `android/kichilogo/build.gradle`:
51
+ ```groovy
52
+ apply plugin: 'com.android.asset-pack'
53
+
54
+ assetPack {
55
+ packName = "kichilogo"
56
+ dynamicDelivery {
57
+ deliveryType = "on-demand"
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### 3. Wire up Gradle
63
+
64
+ In `android/settings.gradle`, add:
65
+ ```groovy
66
+ include ':kichilogo'
67
+ ```
68
+
69
+ In `android/app/build.gradle`, inside the `android {}` block, add:
70
+ ```groovy
71
+ assetPacks = [":kichilogo"]
72
+ ```
73
+
74
+ ### 4. Local testing
75
+
76
+ On-demand asset packs are **not included** in a regular `./gradlew installDebug` APK. There is no Android equivalent to iOS's `EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE`. To test locally, use `bundletool` with the `--local-testing` flag, which tells the Play Core library to serve on-demand packs from the device's local storage instead of the Play Store.
77
+
78
+ Install `bundletool`:
79
+ ```bash
80
+ brew install bundletool
81
+ ```
82
+
83
+ Build and install:
84
+ ```bash
85
+ cd android
86
+
87
+ # 1. Build an AAB (Android App Bundle)
88
+ ./gradlew bundleDebug
89
+
90
+ # 2. Convert to APKs with --local-testing
91
+ bundletool build-apks \
92
+ --bundle=app/build/outputs/bundle/debug/app-debug.aab \
93
+ --output=app.apks \
94
+ --local-testing
95
+
96
+ # 3. Install on device/emulator
97
+ bundletool install-apks --apks=app.apks
98
+ ```
99
+
100
+ The `AssetPackManager` API works normally — `fetch()` will "download" instantly from the local copy.
101
+
102
+ > **Note:** You need to re-run these steps whenever you change assets or native code. JS-only changes still work with Metro hot reload after the initial install.
103
+
104
+ ## Usage
105
+
106
+ ```js
107
+ import {
108
+ checkResourcesAvailable,
109
+ downloadResources,
110
+ endAccessingResources,
111
+ getResourcePath,
112
+ setPreservationPriority,
113
+ } from 'react-native-dynamic-resource-loader';
114
+ ```
115
+
116
+ ### Download resources and get file paths
117
+
118
+ ```js
119
+ // Download all resources tagged "level1"
120
+ const success = await downloadResources(['level1']);
121
+
122
+ // Look up individual files by name (you know which files you tagged)
123
+ const spritePath = await getResourcePath('enemy_sprite', 'png');
124
+ const mapPath = await getResourcePath('level_map', 'json');
125
+ ```
126
+
127
+ ### Check if resources are already cached
128
+
129
+ ```js
130
+ const available = await checkResourcesAvailable(['level1']);
131
+ if (!available) {
132
+ await downloadResources(['level1']);
133
+ }
134
+ ```
135
+
136
+ ### Release resources when done
137
+
138
+ Tell the OS it can purge these resources when storage is low (on Android, removes the asset pack):
139
+
140
+ ```js
141
+ endAccessingResources(['level1']);
142
+ ```
143
+
144
+ ### Set preservation priority
145
+
146
+ Control which resources iOS purges first (0.0 = purge first, 1.0 = keep longest). No-op on Android:
147
+
148
+ ```js
149
+ setPreservationPriority(0.8, ['level1']);
150
+ ```
151
+
152
+ ## API
153
+
154
+ | Method | Returns | Description |
155
+ |--------|---------|-------------|
156
+ | `downloadResources(tags)` | `Promise<boolean>` | Download resources for the given tags. Checks cache first. |
157
+ | `checkResourcesAvailable(tags)` | `Promise<boolean>` | Check if tagged resources are already on device without downloading. |
158
+ | `endAccessingResources(tags)` | `void` | Release resources so the OS can purge them (iOS) or remove the pack (Android). |
159
+ | `getResourcePath(name, type)` | `Promise<string>` | Get the absolute file path for a resource after download. |
160
+ | `setPreservationPriority(priority, tags)` | `void` | Set purge priority (0.0-1.0). iOS only; no-op on Android. |
161
+
162
+ ## Full Example
163
+
164
+ ```js
165
+ import { useEffect, useState } from 'react';
166
+ import {
167
+ downloadResources,
168
+ getResourcePath,
169
+ endAccessingResources,
170
+ } from 'react-native-dynamic-resource-loader';
171
+
172
+ function MyComponent() {
173
+ const [imagePath, setImagePath] = useState(null);
174
+
175
+ useEffect(() => {
176
+ let mounted = true;
177
+
178
+ downloadResources(['avatars'])
179
+ .then(() => getResourcePath('profile_pic', 'png'))
180
+ .then((path) => {
181
+ if (mounted) setImagePath(path);
182
+ })
183
+ .catch((e) => console.log('ODR failed:', e.message));
184
+
185
+ return () => {
186
+ mounted = false;
187
+ endAccessingResources(['avatars']);
188
+ };
189
+ }, []);
190
+
191
+ return imagePath ? <Image source={{ uri: `file://${imagePath}` }} /> : null;
192
+ }
193
+ ```
194
+
195
+ ## Contributing
196
+
197
+ - [Development workflow](CONTRIBUTING.md#development-workflow)
198
+ - [Sending a pull request](CONTRIBUTING.md#sending-a-pull-request)
199
+ - [Code of conduct](CODE_OF_CONDUCT.md)
200
+
201
+ ## License
202
+
203
+ MIT
204
+
205
+ ---
206
+
207
+ Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
@@ -0,0 +1,68 @@
1
+ buildscript {
2
+ ext.DynamicResourceLoader = [
3
+ kotlinVersion: "2.0.21",
4
+ minSdkVersion: 24,
5
+ compileSdkVersion: 36,
6
+ targetSdkVersion: 36
7
+ ]
8
+
9
+ ext.getExtOrDefault = { prop ->
10
+ if (rootProject.ext.has(prop)) {
11
+ return rootProject.ext.get(prop)
12
+ }
13
+
14
+ return DynamicResourceLoader[prop]
15
+ }
16
+
17
+ repositories {
18
+ google()
19
+ mavenCentral()
20
+ }
21
+
22
+ dependencies {
23
+ classpath "com.android.tools.build:gradle:8.7.2"
24
+ // noinspection DifferentKotlinGradleVersion
25
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
26
+ }
27
+ }
28
+
29
+
30
+ apply plugin: "com.android.library"
31
+ apply plugin: "kotlin-android"
32
+
33
+ apply plugin: "com.facebook.react"
34
+
35
+ android {
36
+ namespace "com.dynamicresourceloader"
37
+
38
+ compileSdkVersion getExtOrDefault("compileSdkVersion")
39
+
40
+ defaultConfig {
41
+ minSdkVersion getExtOrDefault("minSdkVersion")
42
+ targetSdkVersion getExtOrDefault("targetSdkVersion")
43
+ }
44
+
45
+ buildFeatures {
46
+ buildConfig true
47
+ }
48
+
49
+ buildTypes {
50
+ release {
51
+ minifyEnabled false
52
+ }
53
+ }
54
+
55
+ lint {
56
+ disable "GradleCompatible"
57
+ }
58
+
59
+ compileOptions {
60
+ sourceCompatibility JavaVersion.VERSION_1_8
61
+ targetCompatibility JavaVersion.VERSION_1_8
62
+ }
63
+ }
64
+
65
+ dependencies {
66
+ implementation "com.facebook.react:react-android"
67
+ implementation "com.google.android.play:asset-delivery:2.3.0"
68
+ }
@@ -0,0 +1,2 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ </manifest>
@@ -0,0 +1,154 @@
1
+ package com.dynamicresourceloader
2
+
3
+ import com.facebook.react.bridge.Arguments
4
+ import com.facebook.react.bridge.Promise
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.bridge.ReadableArray
7
+ import com.google.android.play.core.assetpacks.AssetPackManager
8
+ import com.google.android.play.core.assetpacks.AssetPackManagerFactory
9
+ import com.google.android.play.core.assetpacks.AssetPackState
10
+ import com.google.android.play.core.assetpacks.AssetPackStateUpdateListener
11
+ import com.google.android.play.core.assetpacks.AssetPackStates
12
+ import com.google.android.play.core.assetpacks.model.AssetPackStatus
13
+ import java.io.File
14
+
15
+ class DynamicResourceLoaderModule(reactContext: ReactApplicationContext) :
16
+ NativeDynamicResourceLoaderSpec(reactContext) {
17
+
18
+ private val assetPackManager: AssetPackManager =
19
+ AssetPackManagerFactory.getInstance(reactContext)
20
+
21
+ private fun emitProgress(tag: String, bytesDownloaded: Long, totalBytes: Long, fraction: Double, status: String) {
22
+ emitOnDownloadProgress(Arguments.createMap().apply {
23
+ putString("tag", tag)
24
+ putDouble("bytesDownloaded", bytesDownloaded.toDouble())
25
+ putDouble("totalBytes", totalBytes.toDouble())
26
+ putDouble("fractionCompleted", fraction)
27
+ putString("status", status)
28
+ })
29
+ }
30
+
31
+ private fun ReadableArray.toStringList(): List<String> {
32
+ val list = mutableListOf<String>()
33
+ for (i in 0 until size()) {
34
+ getString(i)?.let { list.add(it) }
35
+ }
36
+ return list
37
+ }
38
+
39
+ override fun checkResourcesAvailable(tags: ReadableArray, promise: Promise) {
40
+ val packNames = tags.toStringList()
41
+ assetPackManager.getPackStates(packNames)
42
+ .addOnSuccessListener { states: AssetPackStates ->
43
+ val allCompleted = packNames.all { name ->
44
+ states.packStates()[name]?.status() == AssetPackStatus.COMPLETED
45
+ }
46
+ promise.resolve(allCompleted)
47
+ }
48
+ .addOnFailureListener { e ->
49
+ promise.reject("ERR_CHECK_RESOURCES", e.message, e)
50
+ }
51
+ }
52
+
53
+ override fun downloadResources(tags: ReadableArray, promise: Promise) {
54
+ val packNames = tags.toStringList()
55
+
56
+ assetPackManager.getPackStates(packNames)
57
+ .addOnSuccessListener { states: AssetPackStates ->
58
+ val allCompleted = packNames.all { name ->
59
+ states.packStates()[name]?.status() == AssetPackStatus.COMPLETED
60
+ }
61
+ if (allCompleted) {
62
+ for (name in packNames) {
63
+ emitProgress(name, 1, 1, 1.0, "completed")
64
+ }
65
+ promise.resolve(true)
66
+ return@addOnSuccessListener
67
+ }
68
+
69
+ val pending = packNames.filter { name ->
70
+ states.packStates()[name]?.status() != AssetPackStatus.COMPLETED
71
+ }.toMutableSet()
72
+ var settled = false
73
+
74
+ val listener = object : AssetPackStateUpdateListener {
75
+ override fun onStateUpdate(state: AssetPackState) {
76
+ if (settled) return
77
+
78
+ val name = state.name()
79
+ val bytesDownloaded = state.bytesDownloaded()
80
+ val totalBytes = state.totalBytesToDownload()
81
+ val fraction = if (totalBytes > 0) bytesDownloaded.toDouble() / totalBytes.toDouble() else 0.0
82
+
83
+ when (state.status()) {
84
+ AssetPackStatus.DOWNLOADING, AssetPackStatus.TRANSFERRING -> {
85
+ emitProgress(name, bytesDownloaded, totalBytes, fraction, "downloading")
86
+ }
87
+ AssetPackStatus.WAITING_FOR_WIFI -> {
88
+ emitProgress(name, bytesDownloaded, totalBytes, fraction, "waiting")
89
+ }
90
+ AssetPackStatus.COMPLETED -> {
91
+ emitProgress(name, totalBytes, totalBytes, 1.0, "completed")
92
+ pending.remove(name)
93
+ if (pending.isEmpty()) {
94
+ settled = true
95
+ assetPackManager.unregisterListener(this)
96
+ promise.resolve(true)
97
+ }
98
+ }
99
+ AssetPackStatus.FAILED -> {
100
+ emitProgress(name, bytesDownloaded, totalBytes, fraction, "failed")
101
+ settled = true
102
+ assetPackManager.unregisterListener(this)
103
+ promise.reject(
104
+ "ERR_DOWNLOAD_FAILED",
105
+ "Asset pack '$name' failed with error code ${state.errorCode()}"
106
+ )
107
+ }
108
+ AssetPackStatus.REQUIRES_USER_CONFIRMATION -> {
109
+ settled = true
110
+ assetPackManager.unregisterListener(this)
111
+ promise.reject(
112
+ "ERR_REQUIRES_USER_CONFIRMATION",
113
+ "Asset pack '$name' requires user confirmation"
114
+ )
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ assetPackManager.registerListener(listener)
121
+ assetPackManager.fetch(packNames)
122
+ }
123
+ .addOnFailureListener { e ->
124
+ promise.reject("ERR_DOWNLOAD_RESOURCES", e.message, e)
125
+ }
126
+ }
127
+
128
+ override fun endAccessingResources(tags: ReadableArray) {
129
+ val packNames = tags.toStringList()
130
+ for (name in packNames) {
131
+ assetPackManager.removePack(name)
132
+ }
133
+ }
134
+
135
+ override fun getResourcePath(resourceName: String, ofType: String, promise: Promise) {
136
+ val locations = assetPackManager.packLocations
137
+ for ((_, location) in locations) {
138
+ val file = File(location.assetsPath(), "$resourceName.$ofType")
139
+ if (file.exists()) {
140
+ promise.resolve(file.absolutePath)
141
+ return
142
+ }
143
+ }
144
+ promise.reject("RESOURCE_NOT_FOUND", "Could not find $resourceName.$ofType in any asset pack")
145
+ }
146
+
147
+ override fun setPreservationPriority(priority: Double, tags: ReadableArray) {
148
+ // No-op on Android — Play Asset Delivery has no preservation priority equivalent
149
+ }
150
+
151
+ companion object {
152
+ const val NAME = NativeDynamicResourceLoaderSpec.NAME
153
+ }
154
+ }
@@ -0,0 +1,31 @@
1
+ package com.dynamicresourceloader
2
+
3
+ import com.facebook.react.BaseReactPackage
4
+ import com.facebook.react.bridge.NativeModule
5
+ import com.facebook.react.bridge.ReactApplicationContext
6
+ import com.facebook.react.module.model.ReactModuleInfo
7
+ import com.facebook.react.module.model.ReactModuleInfoProvider
8
+ import java.util.HashMap
9
+
10
+ class DynamicResourceLoaderPackage : BaseReactPackage() {
11
+ override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? {
12
+ return if (name == DynamicResourceLoaderModule.NAME) {
13
+ DynamicResourceLoaderModule(reactContext)
14
+ } else {
15
+ null
16
+ }
17
+ }
18
+
19
+ override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
20
+ mapOf(
21
+ DynamicResourceLoaderModule.NAME to ReactModuleInfo(
22
+ name = DynamicResourceLoaderModule.NAME,
23
+ className = DynamicResourceLoaderModule.NAME,
24
+ canOverrideExistingModule = false,
25
+ needsEagerInit = false,
26
+ isCxxModule = false,
27
+ isTurboModule = true
28
+ )
29
+ )
30
+ }
31
+ }
@@ -0,0 +1,5 @@
1
+ #import <DynamicResourceLoaderSpec/DynamicResourceLoaderSpec.h>
2
+
3
+ @interface DynamicResourceLoader : NativeDynamicResourceLoaderSpecBase <NativeDynamicResourceLoaderSpec>
4
+
5
+ @end
@@ -0,0 +1,83 @@
1
+ #import "DynamicResourceLoader.h"
2
+ #import <DynamicResourceLoader-Swift.h>
3
+
4
+ @implementation DynamicResourceLoader {
5
+ DynamicResourceLoaderImpl *_impl;
6
+ }
7
+
8
+ - (instancetype)init {
9
+ self = [super init];
10
+ if (self) {
11
+ _impl = [[DynamicResourceLoaderImpl alloc] init];
12
+ }
13
+ return self;
14
+ }
15
+
16
+ - (void)checkResourcesAvailable:(NSArray<NSString *> *)tags
17
+ resolve:(RCTPromiseResolveBlock)resolve
18
+ reject:(RCTPromiseRejectBlock)reject {
19
+ [_impl checkResourcesAvailable:tags
20
+ resolve:^(BOOL available) {
21
+ resolve(@(available));
22
+ }
23
+ reject:^(NSString *code, NSString *message, NSError *error) {
24
+ reject(code, message, error);
25
+ }];
26
+ }
27
+
28
+ - (void)downloadResources:(NSArray<NSString *> *)tags
29
+ resolve:(RCTPromiseResolveBlock)resolve
30
+ reject:(RCTPromiseRejectBlock)reject {
31
+ [_impl downloadResources:tags
32
+ progress:^(NSString *tag, int64_t bytesDownloaded, int64_t totalBytes, double fractionCompleted, NSString *status) {
33
+ [self emitOnDownloadProgress:@{
34
+ @"tag": tag,
35
+ @"bytesDownloaded": @(bytesDownloaded),
36
+ @"totalBytes": @(totalBytes),
37
+ @"fractionCompleted": @(fractionCompleted),
38
+ @"status": status,
39
+ }];
40
+ }
41
+ resolve:^(BOOL success) {
42
+ resolve(@(success));
43
+ }
44
+ reject:^(NSString *code, NSString *message, NSError *error) {
45
+ reject(code, message, error);
46
+ }];
47
+ }
48
+
49
+ - (void)endAccessingResources:(NSArray<NSString *> *)tags {
50
+ [_impl endAccessingResources:tags];
51
+ }
52
+
53
+ - (void)getResourcePath:(NSString *)resourceName
54
+ ofType:(NSString *)ofType
55
+ resolve:(RCTPromiseResolveBlock)resolve
56
+ reject:(RCTPromiseRejectBlock)reject {
57
+ [_impl getResourcePath:resourceName
58
+ ofType:ofType
59
+ resolve:^(NSString *path) {
60
+ resolve(path);
61
+ }
62
+ reject:^(NSString *code, NSString *message, NSError *error) {
63
+ reject(code, message, error);
64
+ }];
65
+ }
66
+
67
+ - (void)setPreservationPriority:(double)priority
68
+ tags:(NSArray<NSString *> *)tags {
69
+ [_impl setPreservationPriority:priority forTags:tags];
70
+ }
71
+
72
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
73
+ (const facebook::react::ObjCTurboModule::InitParams &)params
74
+ {
75
+ return std::make_shared<facebook::react::NativeDynamicResourceLoaderSpecJSI>(params);
76
+ }
77
+
78
+ + (NSString *)moduleName
79
+ {
80
+ return @"DynamicResourceLoader";
81
+ }
82
+
83
+ @end
@@ -0,0 +1,110 @@
1
+ import Foundation
2
+
3
+ @objcMembers
4
+ public class DynamicResourceLoaderImpl: NSObject {
5
+ private var activeRequests: [String: NSBundleResourceRequest] = [:]
6
+ private var progressObservations: [String: NSKeyValueObservation] = [:]
7
+
8
+ private func requestKey(for tags: [String]) -> String {
9
+ return tags.sorted().joined(separator: ",")
10
+ }
11
+
12
+ public func checkResourcesAvailable(
13
+ _ tags: [String],
14
+ resolve: @escaping (Bool) -> Void,
15
+ reject: @escaping (String, String, NSError) -> Void
16
+ ) {
17
+ let tagSet = Set(tags)
18
+ let request = NSBundleResourceRequest(tags: tagSet)
19
+
20
+ request.conditionallyBeginAccessingResources { available in
21
+ if available {
22
+ request.endAccessingResources()
23
+ }
24
+ resolve(available)
25
+ }
26
+ }
27
+
28
+ public func downloadResources(
29
+ _ tags: [String],
30
+ progress progressCallback: ((String, Int64, Int64, Double, String) -> Void)?,
31
+ resolve: @escaping (Bool) -> Void,
32
+ reject: @escaping (String, String, NSError) -> Void
33
+ ) {
34
+ let key = requestKey(for: tags)
35
+ let tagSet = Set(tags)
36
+ let request = NSBundleResourceRequest(tags: tagSet)
37
+
38
+ request.conditionallyBeginAccessingResources { [weak self] available in
39
+ if available {
40
+ self?.activeRequests[key] = request
41
+ for tag in tags {
42
+ progressCallback?(tag, 1, 1, 1.0, "completed")
43
+ }
44
+ resolve(true)
45
+ return
46
+ }
47
+
48
+ if let progressCallback = progressCallback {
49
+ let observation = request.progress.observe(\.fractionCompleted) { progress, _ in
50
+ for tag in tags {
51
+ progressCallback(
52
+ tag,
53
+ progress.completedUnitCount,
54
+ progress.totalUnitCount,
55
+ progress.fractionCompleted,
56
+ "downloading"
57
+ )
58
+ }
59
+ }
60
+ self?.progressObservations[key] = observation
61
+ }
62
+
63
+ request.beginAccessingResources { [weak self] error in
64
+ self?.progressObservations.removeValue(forKey: key)
65
+ if let error = error as NSError? {
66
+ for tag in tags {
67
+ progressCallback?(tag, 0, 0, 0.0, "failed")
68
+ }
69
+ reject("DOWNLOAD_FAILED", error.localizedDescription, error)
70
+ return
71
+ }
72
+ self?.activeRequests[key] = request
73
+ for tag in tags {
74
+ progressCallback?(tag, 1, 1, 1.0, "completed")
75
+ }
76
+ resolve(true)
77
+ }
78
+ }
79
+ }
80
+
81
+ public func endAccessingResources(_ tags: [String]) {
82
+ let key = requestKey(for: tags)
83
+ guard let request = activeRequests[key] else { return }
84
+ request.endAccessingResources()
85
+ activeRequests.removeValue(forKey: key)
86
+ }
87
+
88
+ public func getResourcePath(
89
+ _ resourceName: String,
90
+ ofType type: String,
91
+ resolve: @escaping (String) -> Void,
92
+ reject: @escaping (String, String, NSError) -> Void
93
+ ) {
94
+ if let path = Bundle.main.path(forResource: resourceName, ofType: type) {
95
+ resolve(path)
96
+ } else {
97
+ let error = NSError(
98
+ domain: "DynamicResourceLoader",
99
+ code: 1,
100
+ userInfo: [NSLocalizedDescriptionKey: "Resource '\(resourceName).\(type)' not found"]
101
+ )
102
+ reject("RESOURCE_NOT_FOUND", error.localizedDescription, error)
103
+ }
104
+ }
105
+
106
+ public func setPreservationPriority(_ priority: Double, forTags tags: [String]) {
107
+ let tagSet = Set(tags)
108
+ Bundle.main.setPreservationPriority(priority, forTags: tagSet)
109
+ }
110
+ }
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+
3
+ import { TurboModuleRegistry } from 'react-native';
4
+ export default TurboModuleRegistry.getEnforcing('DynamicResourceLoader');
5
+ //# sourceMappingURL=NativeDynamicResourceLoader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["TurboModuleRegistry","getEnforcing"],"sourceRoot":"../../src","sources":["NativeDynamicResourceLoader.ts"],"mappings":";;AAAA,SAASA,mBAAmB,QAA0B,cAAc;AAoBpE,eAAeA,mBAAmB,CAACC,YAAY,CAAO,uBAAuB,CAAC","ignoreList":[]}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ //# sourceMappingURL=codegen-types.d.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../src","sources":["codegen-types.d.ts"],"mappings":"","ignoreList":[]}
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ import DynamicResourceLoader from "./NativeDynamicResourceLoader.js";
4
+ export function checkResourcesAvailable(tags) {
5
+ return DynamicResourceLoader.checkResourcesAvailable(tags);
6
+ }
7
+ export function downloadResources(tags) {
8
+ return DynamicResourceLoader.downloadResources(tags);
9
+ }
10
+ export function endAccessingResources(tags) {
11
+ DynamicResourceLoader.endAccessingResources(tags);
12
+ }
13
+ export function getResourcePath(resourceName, ofType) {
14
+ return DynamicResourceLoader.getResourcePath(resourceName, ofType);
15
+ }
16
+ export function setPreservationPriority(priority, tags) {
17
+ DynamicResourceLoader.setPreservationPriority(priority, tags);
18
+ }
19
+ export function onDownloadProgress(handler) {
20
+ return DynamicResourceLoader.onDownloadProgress(handler);
21
+ }
22
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["DynamicResourceLoader","checkResourcesAvailable","tags","downloadResources","endAccessingResources","getResourcePath","resourceName","ofType","setPreservationPriority","priority","onDownloadProgress","handler"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,OAAOA,qBAAqB,MAAM,kCAA+B;AAKjE,OAAO,SAASC,uBAAuBA,CACrCC,IAA2B,EACT;EAClB,OAAOF,qBAAqB,CAACC,uBAAuB,CAACC,IAAI,CAAC;AAC5D;AAEA,OAAO,SAASC,iBAAiBA,CAC/BD,IAA2B,EACT;EAClB,OAAOF,qBAAqB,CAACG,iBAAiB,CAACD,IAAI,CAAC;AACtD;AAEA,OAAO,SAASE,qBAAqBA,CAACF,IAA2B,EAAQ;EACvEF,qBAAqB,CAACI,qBAAqB,CAACF,IAAI,CAAC;AACnD;AAEA,OAAO,SAASG,eAAeA,CAC7BC,YAAoB,EACpBC,MAAc,EACG;EACjB,OAAOP,qBAAqB,CAACK,eAAe,CAACC,YAAY,EAAEC,MAAM,CAAC;AACpE;AAEA,OAAO,SAASC,uBAAuBA,CACrCC,QAAgB,EAChBP,IAA2B,EACrB;EACNF,qBAAqB,CAACQ,uBAAuB,CAACC,QAAQ,EAAEP,IAAI,CAAC;AAC/D;AAEA,OAAO,SAASQ,kBAAkBA,CAChCC,OAA+C,EAC/C;EACA,OAAOX,qBAAqB,CAACU,kBAAkB,CAACC,OAAO,CAAC;AAC1D","ignoreList":[]}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1 @@
1
+ {"type":"module"}
@@ -0,0 +1,20 @@
1
+ import { type TurboModule } from 'react-native';
2
+ import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes';
3
+ export type DownloadProgressEvent = {
4
+ tag: string;
5
+ bytesDownloaded: number;
6
+ totalBytes: number;
7
+ fractionCompleted: number;
8
+ status: string;
9
+ };
10
+ export interface Spec extends TurboModule {
11
+ checkResourcesAvailable(tags: ReadonlyArray<string>): Promise<boolean>;
12
+ downloadResources(tags: ReadonlyArray<string>): Promise<boolean>;
13
+ endAccessingResources(tags: ReadonlyArray<string>): void;
14
+ getResourcePath(resourceName: string, ofType: string): Promise<string>;
15
+ setPreservationPriority(priority: number, tags: ReadonlyArray<string>): void;
16
+ readonly onDownloadProgress: EventEmitter<DownloadProgressEvent>;
17
+ }
18
+ declare const _default: Spec;
19
+ export default _default;
20
+ //# sourceMappingURL=NativeDynamicResourceLoader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NativeDynamicResourceLoader.d.ts","sourceRoot":"","sources":["../../../src/NativeDynamicResourceLoader.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AACrE,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2CAA2C,CAAC;AAE9E,MAAM,MAAM,qBAAqB,GAAG;IAClC,GAAG,EAAE,MAAM,CAAC;IACZ,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,WAAW,IAAK,SAAQ,WAAW;IACvC,uBAAuB,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvE,iBAAiB,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACjE,qBAAqB,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IACzD,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACvE,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC7E,QAAQ,CAAC,kBAAkB,EAAE,YAAY,CAAC,qBAAqB,CAAC,CAAC;CAClE;;AAED,wBAA+E"}
@@ -0,0 +1,9 @@
1
+ import type { DownloadProgressEvent } from './NativeDynamicResourceLoader';
2
+ export type { DownloadProgressEvent };
3
+ export declare function checkResourcesAvailable(tags: ReadonlyArray<string>): Promise<boolean>;
4
+ export declare function downloadResources(tags: ReadonlyArray<string>): Promise<boolean>;
5
+ export declare function endAccessingResources(tags: ReadonlyArray<string>): void;
6
+ export declare function getResourcePath(resourceName: string, ofType: string): Promise<string>;
7
+ export declare function setPreservationPriority(priority: number, tags: ReadonlyArray<string>): void;
8
+ export declare function onDownloadProgress(handler: (event: DownloadProgressEvent) => void): import("react-native").EventSubscription;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAE3E,YAAY,EAAE,qBAAqB,EAAE,CAAC;AAEtC,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAC1B,OAAO,CAAC,OAAO,CAAC,CAElB;AAED,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAC1B,OAAO,CAAC,OAAO,CAAC,CAElB;AAED,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,IAAI,CAEvE;AAED,wBAAgB,eAAe,CAC7B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,CAAC,CAEjB;AAED,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,GAC1B,IAAI,CAEN;AAED,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,4CAGhD"}
package/package.json ADDED
@@ -0,0 +1,169 @@
1
+ {
2
+ "name": "react-native-dynamic-resource-loader",
3
+ "version": "0.2.0",
4
+ "description": "Load dynamic files from the App and Play stores",
5
+ "main": "./lib/module/index.js",
6
+ "types": "./lib/typescript/src/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "source": "./src/index.tsx",
10
+ "types": "./lib/typescript/src/index.d.ts",
11
+ "default": "./lib/module/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "src",
17
+ "lib",
18
+ "android",
19
+ "ios",
20
+ "cpp",
21
+ "*.podspec",
22
+ "react-native.config.js",
23
+ "!ios/build",
24
+ "!android/build",
25
+ "!android/gradle",
26
+ "!android/gradlew",
27
+ "!android/gradlew.bat",
28
+ "!android/local.properties",
29
+ "!**/__tests__",
30
+ "!**/__fixtures__",
31
+ "!**/__mocks__",
32
+ "!**/.*"
33
+ ],
34
+ "scripts": {
35
+ "example": "yarn workspace react-native-dynamic-resource-loader-example",
36
+ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
37
+ "prepare": "bob build",
38
+ "typecheck": "tsc",
39
+ "lint": "eslint \"**/*.{js,ts,tsx}\"",
40
+ "test": "jest",
41
+ "release": "release-it --only-version"
42
+ },
43
+ "keywords": [
44
+ "react-native",
45
+ "ios",
46
+ "android"
47
+ ],
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/Rob117//react-native-dynamic-resource-loader.git"
51
+ },
52
+ "author": "Robert Sherling <5183016+Rob117@users.noreply.github.com> (https://github.com/Rob117/)",
53
+ "license": "MIT",
54
+ "bugs": {
55
+ "url": "https://github.com/Rob117//react-native-dynamic-resource-loader/issues"
56
+ },
57
+ "homepage": "https://github.com/Rob117//react-native-dynamic-resource-loader#readme",
58
+ "publishConfig": {
59
+ "registry": "https://registry.npmjs.org/"
60
+ },
61
+ "devDependencies": {
62
+ "@commitlint/config-conventional": "^19.8.1",
63
+ "@eslint/compat": "^1.3.2",
64
+ "@eslint/eslintrc": "^3.3.1",
65
+ "@eslint/js": "^9.35.0",
66
+ "@react-native/babel-preset": "0.83.0",
67
+ "@react-native/eslint-config": "0.83.0",
68
+ "@release-it/conventional-changelog": "^10.0.1",
69
+ "@types/jest": "^29.5.14",
70
+ "@types/react": "^19.2.0",
71
+ "commitlint": "^19.8.1",
72
+ "del-cli": "^6.0.0",
73
+ "eslint": "^9.35.0",
74
+ "eslint-config-prettier": "^10.1.8",
75
+ "eslint-plugin-prettier": "^5.5.4",
76
+ "jest": "^29.7.0",
77
+ "lefthook": "^2.0.3",
78
+ "prettier": "^2.8.8",
79
+ "react": "19.2.0",
80
+ "react-native": "0.83.0",
81
+ "react-native-builder-bob": "^0.40.18",
82
+ "release-it": "^19.0.4",
83
+ "turbo": "^2.5.6",
84
+ "typescript": "^5.9.2"
85
+ },
86
+ "peerDependencies": {
87
+ "react": "*",
88
+ "react-native": "*"
89
+ },
90
+ "workspaces": [
91
+ "example"
92
+ ],
93
+ "packageManager": "yarn@4.11.0",
94
+ "react-native-builder-bob": {
95
+ "source": "src",
96
+ "output": "lib",
97
+ "targets": [
98
+ [
99
+ "module",
100
+ {
101
+ "esm": true
102
+ }
103
+ ],
104
+ [
105
+ "typescript",
106
+ {
107
+ "project": "tsconfig.build.json"
108
+ }
109
+ ]
110
+ ]
111
+ },
112
+ "codegenConfig": {
113
+ "name": "DynamicResourceLoaderSpec",
114
+ "type": "modules",
115
+ "jsSrcsDir": "src",
116
+ "android": {
117
+ "javaPackageName": "com.dynamicresourceloader"
118
+ }
119
+ },
120
+ "prettier": {
121
+ "quoteProps": "consistent",
122
+ "singleQuote": true,
123
+ "tabWidth": 2,
124
+ "trailingComma": "es5",
125
+ "useTabs": false
126
+ },
127
+ "jest": {
128
+ "preset": "react-native",
129
+ "modulePathIgnorePatterns": [
130
+ "<rootDir>/example/node_modules",
131
+ "<rootDir>/lib/"
132
+ ]
133
+ },
134
+ "commitlint": {
135
+ "extends": [
136
+ "@commitlint/config-conventional"
137
+ ]
138
+ },
139
+ "release-it": {
140
+ "git": {
141
+ "commitMessage": "chore: release ${version}",
142
+ "tagName": "v${version}"
143
+ },
144
+ "npm": {
145
+ "publish": true
146
+ },
147
+ "github": {
148
+ "release": true
149
+ },
150
+ "plugins": {
151
+ "@release-it/conventional-changelog": {
152
+ "preset": {
153
+ "name": "angular"
154
+ }
155
+ }
156
+ }
157
+ },
158
+ "create-react-native-library": {
159
+ "type": "turbo-module",
160
+ "languages": "kotlin-objc",
161
+ "tools": [
162
+ "eslint",
163
+ "jest",
164
+ "lefthook",
165
+ "release-it"
166
+ ],
167
+ "version": "0.57.1"
168
+ }
169
+ }
@@ -0,0 +1,21 @@
1
+ import { TurboModuleRegistry, type TurboModule } from 'react-native';
2
+ import type { EventEmitter } from 'react-native/Libraries/Types/CodegenTypes';
3
+
4
+ export type DownloadProgressEvent = {
5
+ tag: string;
6
+ bytesDownloaded: number;
7
+ totalBytes: number;
8
+ fractionCompleted: number;
9
+ status: string;
10
+ };
11
+
12
+ export interface Spec extends TurboModule {
13
+ checkResourcesAvailable(tags: ReadonlyArray<string>): Promise<boolean>;
14
+ downloadResources(tags: ReadonlyArray<string>): Promise<boolean>;
15
+ endAccessingResources(tags: ReadonlyArray<string>): void;
16
+ getResourcePath(resourceName: string, ofType: string): Promise<string>;
17
+ setPreservationPriority(priority: number, tags: ReadonlyArray<string>): void;
18
+ readonly onDownloadProgress: EventEmitter<DownloadProgressEvent>;
19
+ }
20
+
21
+ export default TurboModuleRegistry.getEnforcing<Spec>('DynamicResourceLoader');
@@ -0,0 +1,9 @@
1
+ // Type declarations for react-native codegen types that are blocked
2
+ // by the react-native-strict-api custom condition in tsconfig.json.
3
+ declare module 'react-native/Libraries/Types/CodegenTypes' {
4
+ import type { EventSubscription } from 'react-native';
5
+
6
+ export type EventEmitter<T> = (
7
+ handler: (event: T) => void | Promise<void>
8
+ ) => EventSubscription;
9
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,40 @@
1
+ import DynamicResourceLoader from './NativeDynamicResourceLoader';
2
+ import type { DownloadProgressEvent } from './NativeDynamicResourceLoader';
3
+
4
+ export type { DownloadProgressEvent };
5
+
6
+ export function checkResourcesAvailable(
7
+ tags: ReadonlyArray<string>
8
+ ): Promise<boolean> {
9
+ return DynamicResourceLoader.checkResourcesAvailable(tags);
10
+ }
11
+
12
+ export function downloadResources(
13
+ tags: ReadonlyArray<string>
14
+ ): Promise<boolean> {
15
+ return DynamicResourceLoader.downloadResources(tags);
16
+ }
17
+
18
+ export function endAccessingResources(tags: ReadonlyArray<string>): void {
19
+ DynamicResourceLoader.endAccessingResources(tags);
20
+ }
21
+
22
+ export function getResourcePath(
23
+ resourceName: string,
24
+ ofType: string
25
+ ): Promise<string> {
26
+ return DynamicResourceLoader.getResourcePath(resourceName, ofType);
27
+ }
28
+
29
+ export function setPreservationPriority(
30
+ priority: number,
31
+ tags: ReadonlyArray<string>
32
+ ): void {
33
+ DynamicResourceLoader.setPreservationPriority(priority, tags);
34
+ }
35
+
36
+ export function onDownloadProgress(
37
+ handler: (event: DownloadProgressEvent) => void
38
+ ) {
39
+ return DynamicResourceLoader.onDownloadProgress(handler);
40
+ }